mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-15 00:25:07 +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';
|
import { ElMessage } from 'element-plus/es';
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false });
|
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) => {
|
const isWhiteList = (path: string) => {
|
||||||
return whiteList.some((pattern) => isPathMatch(pattern, path));
|
return whiteList.some((pattern) => isPathMatch(pattern, path));
|
||||||
|
|||||||
@ -42,6 +42,11 @@ export const constantRoutes: RouteRecordRaw[] = [
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
component: () => import('@/layout/components/SocialCallback/index.vue')
|
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',
|
path: '/login',
|
||||||
component: () => import('@/views/login.vue'),
|
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