mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-13 07:43:43 +08:00
新增媒体账号管理页面(媒体接入 H 阶段)
- src/api/media/{account,app,oauth}:4 个账号接口 + app 列表 + 取授权 URL
- src/views/media/account/index.vue:列表 + 搜索 + 详情对话框 + 启停 + 新增绑定
- src/views/media/account/oauth-result/index.vue:OAuth 回调结果落地页,
通过 window.opener.postMessage 通知父窗口刷新列表
- 路由 constantRoutes 加 hidden 路由 /media/account/oauth-result
- permission.ts whiteList 加上落地页路径,避免回调时被踢去 login
新增绑定交互:window.open 新窗口走授权,原列表保留状态;落地页发完成
事件后父窗口自动刷新;不删除账号(后端无 remove 端点)
This commit is contained in:
parent
9fd2b6f137
commit
f318fe2791
36
src/api/media/account/index.ts
Normal file
36
src/api/media/account/index.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { MediaAccountVO, MediaAccountQuery } from '@/api/media/account/types';
|
||||
|
||||
/** 分页查询媒体账号 */
|
||||
export const listMediaAccount = (query?: MediaAccountQuery): AxiosPromise<MediaAccountVO[]> => {
|
||||
return request({
|
||||
url: '/media/account/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
};
|
||||
|
||||
/** 查询媒体账号详情(token 已脱敏) */
|
||||
export const getMediaAccount = (id: string | number): AxiosPromise<MediaAccountVO> => {
|
||||
return request({
|
||||
url: '/media/account/' + id,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/** 禁用媒体账号 */
|
||||
export const disableMediaAccount = (id: string | number) => {
|
||||
return request({
|
||||
url: '/media/account/' + id + '/disable',
|
||||
method: 'put'
|
||||
});
|
||||
};
|
||||
|
||||
/** 启用媒体账号 */
|
||||
export const enableMediaAccount = (id: string | number) => {
|
||||
return request({
|
||||
url: '/media/account/' + id + '/enable',
|
||||
method: 'put'
|
||||
});
|
||||
};
|
||||
27
src/api/media/account/types.ts
Normal file
27
src/api/media/account/types.ts
Normal file
@ -0,0 +1,27 @@
|
||||
export interface MediaAccountVO {
|
||||
id: string | number;
|
||||
tenantId: string;
|
||||
platform: string;
|
||||
appId: string | number;
|
||||
externalId: string;
|
||||
/** ENABLED / DISABLED */
|
||||
status: string;
|
||||
/** HEALTHY / REAUTH_REQUIRED */
|
||||
authStatus: string;
|
||||
accessTokenMasked?: string;
|
||||
accessTokenExpiresAt?: string;
|
||||
refreshTokenMasked?: string;
|
||||
refreshTokenExpiresAt?: string;
|
||||
scope?: string;
|
||||
refreshRenewRemainingCount?: number;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
export interface MediaAccountQuery extends PageQuery {
|
||||
platform?: string;
|
||||
appId?: string | number;
|
||||
status?: string;
|
||||
authStatus?: string;
|
||||
externalId?: string;
|
||||
}
|
||||
12
src/api/media/app/index.ts
Normal file
12
src/api/media/app/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { MediaAppVO } from '@/api/media/app/types';
|
||||
|
||||
/** 列出可用媒体应用(按平台过滤) */
|
||||
export const listMediaApp = (platform?: string): AxiosPromise<MediaAppVO[]> => {
|
||||
return request({
|
||||
url: '/media/app/list',
|
||||
method: 'get',
|
||||
params: { platform }
|
||||
});
|
||||
};
|
||||
7
src/api/media/app/types.ts
Normal file
7
src/api/media/app/types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface MediaAppVO {
|
||||
id: string | number;
|
||||
platform: string;
|
||||
clientKey: string;
|
||||
/** 0=禁用 1=启用 */
|
||||
status: number;
|
||||
}
|
||||
12
src/api/media/oauth/index.ts
Normal file
12
src/api/media/oauth/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { AuthorizeUrlQuery } from '@/api/media/oauth/types';
|
||||
|
||||
/** 取媒体平台 OAuth 授权 URL */
|
||||
export const getAuthorizeUrl = (query: AuthorizeUrlQuery): AxiosPromise<string> => {
|
||||
return request({
|
||||
url: '/media/oauth/authorize-url',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
};
|
||||
4
src/api/media/oauth/types.ts
Normal file
4
src/api/media/oauth/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface AuthorizeUrlQuery {
|
||||
platform: string;
|
||||
mediaAppId: string | number;
|
||||
}
|
||||
@ -11,7 +11,7 @@ import { usePermissionStore } from '@/store/modules/permission';
|
||||
import { ElMessage } from 'element-plus/es';
|
||||
|
||||
NProgress.configure({ showSpinner: false });
|
||||
const whiteList = ['/login', '/register', '/social-callback', '/register*', '/register/*'];
|
||||
const whiteList = ['/login', '/register', '/social-callback', '/register*', '/register/*', '/media/account/oauth-result'];
|
||||
|
||||
const isWhiteList = (path: string) => {
|
||||
return whiteList.some((pattern) => isPathMatch(pattern, path));
|
||||
|
||||
@ -42,6 +42,11 @@ export const constantRoutes: RouteRecordRaw[] = [
|
||||
hidden: true,
|
||||
component: () => import('@/layout/components/SocialCallback/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/media/account/oauth-result',
|
||||
hidden: true,
|
||||
component: () => import('@/views/media/account/oauth-result/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/views/login.vue'),
|
||||
|
||||
324
src/views/media/account/index.vue
Normal file
324
src/views/media/account/index.vue
Normal file
@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="平台" prop="platform">
|
||||
<el-select v-model="queryParams.platform" placeholder="请选择平台" clearable style="width: 160px">
|
||||
<el-option v-for="p in platformOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="健康度" prop="authStatus">
|
||||
<el-select v-model="queryParams.authStatus" placeholder="请选择健康度" clearable style="width: 160px">
|
||||
<el-option label="正常" value="HEALTHY" />
|
||||
<el-option label="需重新授权" value="REAUTH_REQUIRED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 160px">
|
||||
<el-option label="启用" value="ENABLED" />
|
||||
<el-option label="禁用" value="DISABLED" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="外部ID" prop="externalId">
|
||||
<el-input v-model="queryParams.externalId" placeholder="open_id 等" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['media:account:bind']" type="primary" plain icon="Plus" @click="openBindDialog">新增绑定</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="accountList">
|
||||
<el-table-column label="ID" align="center" prop="id" width="80" />
|
||||
<el-table-column label="平台" align="center" prop="platform" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.platform }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="应用 ID" align="center" prop="appId" width="100" />
|
||||
<el-table-column label="外部 ID" align="center" prop="externalId" show-overflow-tooltip />
|
||||
<el-table-column label="状态" align="center" prop="status" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 'ENABLED' ? 'success' : 'info'">
|
||||
{{ scope.row.status === 'ENABLED' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="健康度" align="center" prop="authStatus" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.authStatus === 'HEALTHY' ? 'success' : 'danger'">
|
||||
{{ scope.row.authStatus === 'HEALTHY' ? '正常' : '需重新授权' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="scope" align="center" prop="scope" show-overflow-tooltip />
|
||||
<el-table-column label="access_token" align="center" prop="accessTokenMasked" width="160" show-overflow-tooltip />
|
||||
<el-table-column label="access_token 过期" align="center" prop="accessTokenExpiresAt" width="170">
|
||||
<template #default="scope">{{ parseTime(scope.row.accessTokenExpiresAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="refresh_token 过期" align="center" prop="refreshTokenExpiresAt" width="170">
|
||||
<template #default="scope">{{ parseTime(scope.row.refreshTokenExpiresAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="续期剩余" align="center" prop="refreshRenewRemainingCount" width="100" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="详情" placement="top">
|
||||
<el-button v-hasPermi="['media:account:query']" link type="primary" icon="View" @click="openDetail(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.status === 'ENABLED'" content="禁用" placement="top">
|
||||
<el-button v-hasPermi="['media:account:edit']" link type="warning" icon="CircleClose" @click="handleDisable(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else content="启用" placement="top">
|
||||
<el-button v-hasPermi="['media:account:edit']" link type="success" icon="CircleCheck" @click="handleEnable(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.authStatus === 'REAUTH_REQUIRED'" content="重新授权" placement="top">
|
||||
<el-button v-hasPermi="['media:account:bind']" link type="danger" icon="Refresh" @click="reauth(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog v-model="detailDialog.visible" :title="`媒体账号详情 #${detailDialog.data?.id ?? ''}`" width="640px" append-to-body>
|
||||
<el-descriptions v-if="detailDialog.data" :column="2" border>
|
||||
<el-descriptions-item label="ID">{{ detailDialog.data.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="租户">{{ detailDialog.data.tenantId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="平台">{{ detailDialog.data.platform }}</el-descriptions-item>
|
||||
<el-descriptions-item label="应用 ID">{{ detailDialog.data.appId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="外部 ID" :span="2">{{ detailDialog.data.externalId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ detailDialog.data.status === 'ENABLED' ? '启用' : '禁用' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="健康度">{{ detailDialog.data.authStatus === 'HEALTHY' ? '正常' : '需重新授权' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="scope" :span="2">{{ detailDialog.data.scope }}</el-descriptions-item>
|
||||
<el-descriptions-item label="access_token">{{ detailDialog.data.accessTokenMasked }}</el-descriptions-item>
|
||||
<el-descriptions-item label="access_token 过期">{{ parseTime(detailDialog.data.accessTokenExpiresAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="refresh_token">{{ detailDialog.data.refreshTokenMasked }}</el-descriptions-item>
|
||||
<el-descriptions-item label="refresh_token 过期">{{ parseTime(detailDialog.data.refreshTokenExpiresAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="续期剩余">{{ detailDialog.data.refreshRenewRemainingCount }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ parseTime(detailDialog.data.createTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{ parseTime(detailDialog.data.updateTime) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增绑定对话框 -->
|
||||
<el-dialog v-model="bindDialog.visible" title="新增媒体账号绑定" width="500px" append-to-body @closed="resetBindForm">
|
||||
<el-form ref="bindFormRef" :model="bindForm" :rules="bindRules" label-width="100px">
|
||||
<el-form-item label="平台" prop="platform">
|
||||
<el-select v-model="bindForm.platform" placeholder="请选择平台" style="width: 100%" @change="onPlatformChange">
|
||||
<el-option v-for="p in platformOptions" :key="p.value" :label="p.label" :value="p.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="媒体应用" prop="mediaAppId">
|
||||
<el-select v-model="bindForm.mediaAppId" placeholder="请先选择平台" :loading="bindDialog.appLoading" style="width: 100%">
|
||||
<el-option
|
||||
v-for="app in bindDialog.appList"
|
||||
:key="app.id"
|
||||
:label="`${app.clientKey} (id=${app.id})`"
|
||||
:value="app.id"
|
||||
:disabled="app.status !== 1"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
title="点击「前往授权」会打开新窗口跳到媒体平台授权页,授权完成后回到本系统的结果页"
|
||||
show-icon
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="bindDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="bindDialog.submitting" @click="submitBind">前往授权</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="MediaAccount" lang="ts">
|
||||
import { listMediaAccount, getMediaAccount, disableMediaAccount, enableMediaAccount } from '@/api/media/account';
|
||||
import { MediaAccountVO, MediaAccountQuery } from '@/api/media/account/types';
|
||||
import { listMediaApp } from '@/api/media/app';
|
||||
import { MediaAppVO } from '@/api/media/app/types';
|
||||
import { getAuthorizeUrl } from '@/api/media/oauth';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const platformOptions = [{ value: 'DOUYIN', label: '抖音' }];
|
||||
|
||||
const accountList = ref<MediaAccountVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const total = ref(0);
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const initQuery: MediaAccountQuery = {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
platform: undefined,
|
||||
status: undefined,
|
||||
authStatus: undefined,
|
||||
externalId: undefined
|
||||
};
|
||||
const queryParams = ref<MediaAccountQuery>({ ...initQuery });
|
||||
|
||||
const detailDialog = reactive<{ visible: boolean; data: MediaAccountVO | null }>({
|
||||
visible: false,
|
||||
data: null
|
||||
});
|
||||
|
||||
const bindFormRef = ref<ElFormInstance>();
|
||||
const bindDialog = reactive<{
|
||||
visible: boolean;
|
||||
appLoading: boolean;
|
||||
appList: MediaAppVO[];
|
||||
submitting: boolean;
|
||||
}>({
|
||||
visible: false,
|
||||
appLoading: false,
|
||||
appList: [],
|
||||
submitting: false
|
||||
});
|
||||
const bindForm = reactive<{ platform: string; mediaAppId: string | number | undefined }>({
|
||||
platform: 'DOUYIN',
|
||||
mediaAppId: undefined
|
||||
});
|
||||
const bindRules = {
|
||||
platform: [{ required: true, message: '请选择平台', trigger: 'change' }],
|
||||
mediaAppId: [{ required: true, message: '请选择媒体应用', trigger: 'change' }]
|
||||
};
|
||||
|
||||
const parseTime = (value?: string | number | Date) => (value ? proxy?.parseTime(value) : '-');
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await listMediaAccount(queryParams.value);
|
||||
accountList.value = res.rows;
|
||||
total.value = res.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value = { ...initQuery };
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
const openDetail = async (row: MediaAccountVO) => {
|
||||
const res = await getMediaAccount(row.id);
|
||||
detailDialog.data = res.data;
|
||||
detailDialog.visible = true;
|
||||
};
|
||||
|
||||
const handleDisable = async (row: MediaAccountVO) => {
|
||||
await proxy?.$modal.confirm(`确认禁用账号 #${row.id}(${row.externalId})?`);
|
||||
await disableMediaAccount(row.id);
|
||||
proxy?.$modal.msgSuccess('禁用成功');
|
||||
await getList();
|
||||
};
|
||||
|
||||
const handleEnable = async (row: MediaAccountVO) => {
|
||||
await proxy?.$modal.confirm(`确认启用账号 #${row.id}(${row.externalId})?`);
|
||||
await enableMediaAccount(row.id);
|
||||
proxy?.$modal.msgSuccess('启用成功');
|
||||
await getList();
|
||||
};
|
||||
|
||||
const reauth = async (row: MediaAccountVO) => {
|
||||
bindForm.platform = row.platform;
|
||||
bindForm.mediaAppId = row.appId;
|
||||
await loadApps(row.platform);
|
||||
bindDialog.visible = true;
|
||||
};
|
||||
|
||||
const openBindDialog = async () => {
|
||||
bindForm.platform = 'DOUYIN';
|
||||
bindForm.mediaAppId = undefined;
|
||||
await loadApps('DOUYIN');
|
||||
bindDialog.visible = true;
|
||||
};
|
||||
|
||||
const onPlatformChange = async (value: string) => {
|
||||
bindForm.mediaAppId = undefined;
|
||||
await loadApps(value);
|
||||
};
|
||||
|
||||
const loadApps = async (platform: string) => {
|
||||
bindDialog.appLoading = true;
|
||||
try {
|
||||
const res = await listMediaApp(platform);
|
||||
bindDialog.appList = res.data || [];
|
||||
} finally {
|
||||
bindDialog.appLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetBindForm = () => {
|
||||
bindFormRef.value?.resetFields();
|
||||
bindDialog.appList = [];
|
||||
bindDialog.submitting = false;
|
||||
};
|
||||
|
||||
const submitBind = () => {
|
||||
bindFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
bindDialog.submitting = true;
|
||||
try {
|
||||
const res = await getAuthorizeUrl({
|
||||
platform: bindForm.platform,
|
||||
mediaAppId: bindForm.mediaAppId as string | number
|
||||
});
|
||||
const url = res.data;
|
||||
if (!url) {
|
||||
proxy?.$modal.msgError('未获取到授权地址');
|
||||
return;
|
||||
}
|
||||
window.open(url, '_blank', 'noopener=no');
|
||||
bindDialog.visible = false;
|
||||
proxy?.$modal.msgSuccess('已在新窗口打开授权页,请完成授权后回到本页');
|
||||
} finally {
|
||||
bindDialog.submitting = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 监听 OAuth 落地页发来的完成消息,自动刷新列表
|
||||
const onOauthMessage = (event: MessageEvent) => {
|
||||
if (event.data === 'media-oauth-done') {
|
||||
getList();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', onOauthMessage);
|
||||
getList();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('message', onOauthMessage);
|
||||
});
|
||||
</script>
|
||||
139
src/views/media/account/oauth-result/index.vue
Normal file
139
src/views/media/account/oauth-result/index.vue
Normal file
@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="oauth-result-wrapper">
|
||||
<el-card class="result-card" shadow="hover">
|
||||
<div v-if="status === 'success'" class="result-success">
|
||||
<el-icon class="icon-success"><CircleCheckFilled /></el-icon>
|
||||
<h2>授权成功</h2>
|
||||
<p class="hint">媒体账号已绑定到当前租户</p>
|
||||
<el-descriptions :column="1" border class="info">
|
||||
<el-descriptions-item label="平台">{{ platform }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账号 ID">{{ accountId }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<div v-else class="result-fail">
|
||||
<el-icon class="icon-fail"><CircleCloseFilled /></el-icon>
|
||||
<h2>授权失败</h2>
|
||||
<p class="hint">{{ failMessage }}</p>
|
||||
<p class="error-code">错误码:{{ errorCode || '未知' }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button type="primary" @click="closeWindow">关闭窗口</el-button>
|
||||
<el-button @click="goAccountList">返回账号列表</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="MediaOAuthResult">
|
||||
import { CircleCheckFilled, CircleCloseFilled } from '@element-plus/icons-vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const status = computed(() => (route.query.status as string) || 'fail');
|
||||
const platform = computed(() => (route.query.platform as string) || '-');
|
||||
const accountId = computed(() => (route.query.accountId as string) || '-');
|
||||
const errorCode = computed(() => (route.query.code as string) || '');
|
||||
|
||||
const ERROR_MESSAGES: Record<string, string> = {
|
||||
USER_CANCELED: '用户取消了授权',
|
||||
STATE_INVALID: 'state 无效或解析失败,请重新发起授权',
|
||||
STATE_EXPIRED: 'state 已过期,请重新发起授权',
|
||||
STATE_PLATFORM_MISMATCH: 'state 与回调平台不匹配',
|
||||
ACCOUNT_BOUND_TO_OTHER_TENANT: '该媒体账号已被其他租户绑定',
|
||||
APP_NOT_FOUND: '找不到对应的媒体应用',
|
||||
APP_DISABLED: '媒体应用已禁用',
|
||||
SIGNATURE_FAILED: '媒体平台签名校验失败',
|
||||
INVALID_RESPONSE: '媒体平台返回数据格式异常',
|
||||
TRANSIENT_NETWORK: '调用媒体平台网络异常',
|
||||
TRANSIENT_PLATFORM_5XX: '媒体平台服务异常',
|
||||
TRANSIENT_RATE_LIMIT: '媒体平台请求被限流',
|
||||
OAUTH_REAUTH_REQUIRED: '授权失效,请重新授权',
|
||||
RENEW_QUOTA_EXHAUSTED: '续期额度已耗尽,请重新授权',
|
||||
UNKNOWN_PLATFORM: '未注册的媒体平台',
|
||||
UNSUPPORTED_TOKEN_MODE: '不支持的 token 模式'
|
||||
};
|
||||
|
||||
const failMessage = computed(() => ERROR_MESSAGES[errorCode.value] || '授权过程发生未知错误');
|
||||
|
||||
const closeWindow = () => {
|
||||
window.close();
|
||||
};
|
||||
|
||||
const goAccountList = () => {
|
||||
// 当前若是 OAuth 弹出的新窗口,opener 存在;否则按当前窗口跳转
|
||||
if (window.opener) {
|
||||
window.close();
|
||||
} else {
|
||||
router.push('/media/account');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 通知父窗口刷新列表
|
||||
if (status.value === 'success' && window.opener) {
|
||||
try {
|
||||
window.opener.postMessage('media-oauth-done', '*');
|
||||
} catch {
|
||||
// ignore cross-origin error
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.oauth-result-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
width: 480px;
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.icon-success,
|
||||
.icon-fail {
|
||||
font-size: 64px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.icon-fail {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: #606266;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-code {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: left;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user