mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-18 17:38:49 +08:00
- 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 端点)
325 lines
14 KiB
Vue
325 lines
14 KiB
Vue
<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>
|