feat(desktop/renderer): surface re-authorize affordance for expired accounts

Replace the generic 重新校验 action with state-aware copy and an arrow
icon when authState is expired, and route the click through the bind flow
so users land directly on the platform login page. AiPlatformsView gains
the same shortcut and an inline alert explaining tasks are paused until
re-auth completes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 17:12:20 +08:00
parent b59435dec5
commit 2501f9db85
2 changed files with 92 additions and 19 deletions
@@ -6,7 +6,7 @@ import StatusBadge from "../components/StatusBadge.vue";
import SurfaceCard from "../components/SurfaceCard.vue";
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
import { showClientActionError } from "../lib/client-errors";
import { formatDateTime, formatRelativeTime, formatVerifiedAtLabel, titleCaseToken } from "../lib/formatters";
import { formatDateTime, formatRelativeTime, formatVerifiedAtLabel } from "../lib/formatters";
import { desktopPublishMediaCatalog } from "../lib/media-catalog";
import type { RuntimeAccount } from "../types";
@@ -18,6 +18,7 @@ const searchQuery = ref("");
const bindPendingPlatformId = ref<string | null>(null);
const consolePendingAccountId = ref<string | null>(null);
const unbindPendingAccountId = ref<string | null>(null);
const reauthorizePendingAccountId = ref<string | null>(null);
const probePendingAccountIds = ref(new Set<string>());
const actionSuccess = ref<string | null>(null);
@@ -59,8 +60,8 @@ function platformMeta(platform: string) {
return (
desktopPublishMediaCatalog.find((item) => item.id === platform) ?? {
id: platform,
label: titleCaseToken(platform),
shortName: titleCaseToken(platform).slice(0, 1),
label: "未知平台",
shortName: "未",
accent: "#64748b",
loginUrl: null,
logoUrl: null,
@@ -150,6 +151,15 @@ function sessionStateLabel(account: AccountRow): string {
}
function verificationTimeLabel(account: AccountRow): string {
switch (account.authState) {
case "expired":
return "需要重新授权";
case "revoked":
return "已停用";
case "challenge_required":
return account.authReason === "risk_control" ? "触发风控,需处理" : "需要人工验证";
}
if (account.lastVerifiedAt) {
return formatVerifiedAtLabel(account.lastVerifiedAt);
}
@@ -162,6 +172,16 @@ function verificationTimeLabel(account: AccountRow): string {
return "尚未校验";
}
function accountRecoveryActionLabel(account: AccountRow): string {
return authState(account) === "expired" ? "重新授权" : "重新校验";
}
function isRecoveryActionPending(account: AccountRow): boolean {
return authState(account) === "expired"
? reauthorizePendingAccountId.value === account.id || bindPendingPlatformId.value === account.platform
: isProbePending(account);
}
const filteredAccounts = computed(() => {
const keyword = searchQuery.value.trim().toLowerCase();
@@ -272,6 +292,26 @@ function isProbePending(account: AccountRow): boolean {
);
}
async function recoverAccount(account: AccountRow) {
if (authState(account) !== "expired") {
await verifyAccount(account);
return;
}
reauthorizePendingAccountId.value = account.id;
actionSuccess.value = null;
try {
const nextAccount = await window.desktopBridge.app.bindPublishAccount(account.platform);
actionSuccess.value = `${nextAccount.display_name} 已重新授权`;
await refresh();
} catch (error) {
showClientActionError("bind-account", error);
} finally {
reauthorizePendingAccountId.value = null;
}
}
const tableVirtual = computed(() => filteredAccounts.value.length > 20);
const tableScroll = computed(() => (tableVirtual.value ? { x: 960, y: 640 } : { x: 960 }));
@@ -290,7 +330,7 @@ const tableColumns = [
<section class="hero-card">
<div class="hero-header">
<div class="hero-title">
<p class="eyebrow">MEDIA ACCOUNT MANAGEMENT</p>
<p class="eyebrow">媒体授权中心</p>
<h2>媒体账号管理</h2>
</div>
<div class="hero-actions">
@@ -320,7 +360,7 @@ const tableColumns = [
</div>
</section>
<!-- Content Sections -->
<!-- 内容区 -->
<section class="content-section">
<div class="section-header">
<h3 class="section-title">选择平台进行授权</h3>
@@ -348,7 +388,6 @@ const tableColumns = [
<p>{{ platform.count > 0 ? `${platform.count} 个账号` : "未绑定" }}</p>
</div>
</div>
<!-- Dynamic Status Badge/Tag could be added here if needed -->
</div>
<div class="card-footer" style="padding-top: 16px; margin-top: auto; border-top: 1px dashed #e2e8f0; display:flex; justify-content: space-between; align-items:center;">
@@ -386,7 +425,7 @@ const tableColumns = [
</div>
</section>
<!-- Table Details Section -->
<!-- 表格详情区 -->
<section class="content-section">
<div class="hero-card" style="padding-bottom: 0;">
<div class="hero-header" style="padding: 24px 32px 16px; border-bottom: 1px solid #eef2f6; align-items: center;">
@@ -394,7 +433,7 @@ const tableColumns = [
<h3 class="section-title">授权账号列表</h3>
</div>
<!-- Filters ToolBar -->
<!-- 筛选工具栏 -->
<div style="display: flex; gap: 12px; align-items: center;">
<a-select v-model:value="selectedPlatform" style="width: 140px; border-radius: 8px;">
<a-select-option value="all">所有平台</a-select-option>
@@ -487,9 +526,12 @@ const tableColumns = [
<template #icon><LinkOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="重新校验" placement="top">
<a-button type="text" class="action-btn" :loading="isProbePending(record)" @click="verifyAccount(record)">
<template #icon><SyncOutlined /></template>
<a-tooltip :title="accountRecoveryActionLabel(record)" placement="top">
<a-button type="text" class="action-btn" :loading="isRecoveryActionPending(record)" @click="recoverAccount(record)">
<template #icon>
<ArrowRightOutlined v-if="authState(record) === 'expired'" />
<SyncOutlined v-else />
</template>
</a-button>
</a-tooltip>
<a-popconfirm title="确定解绑这个账号?" placement="topRight" @confirm="unbindAccount(record)">
@@ -517,7 +559,7 @@ const tableColumns = [
max-width: 1400px;
}
/* Hero Section */
/* 顶部信息区 */
.hero-card {
background: #ffffff;
border-radius: 20px;
@@ -565,7 +607,7 @@ const tableColumns = [
font-weight: 600;
}
/* Stats Strip */
/* 统计条 */
.stats-strip {
display: flex;
padding: 24px 40px;
@@ -595,7 +637,7 @@ const tableColumns = [
font-feature-settings: "tnum";
}
/* Content Section */
/* 内容区 */
.content-section {
display: flex;
flex-direction: column;
@@ -626,7 +668,7 @@ const tableColumns = [
gap: 20px;
}
/* Modern Card Layout */
/* 卡片布局 */
.modern-card {
display: flex;
flex-direction: column;
@@ -659,7 +701,7 @@ const tableColumns = [
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px; /* Reduced because content goes into footer */
margin-bottom: 8px;
}
.brand-info {
@@ -726,7 +768,7 @@ const tableColumns = [
height: 34px;
}
/* Modern Enterprise Table Styles */
/* 表格样式 */
:deep(.modern-table) {
padding: 0 32px 32px;
}
@@ -842,7 +884,7 @@ const tableColumns = [
align-items: center;
}
/* Responsiveness */
/* 响应式 */
@media (max-width: 1024px) {
.hero-header {
flex-direction: column;
@@ -95,7 +95,15 @@ function authColor(account: AccountRow | null) {
}
function accountActionAlert(account: AccountRow | null): string | null {
if (!account || account.authState !== "challenge_required") {
if (!account) {
return null;
}
if (account.authState === "expired") {
return "当前账号授权已过期,相关任务会暂停执行。请点击“重新授权”完成登录后再继续。";
}
if (account.authState !== "challenge_required") {
return null;
}
@@ -111,6 +119,15 @@ function accountActionAlert(account: AccountRow | null): string | null {
}
function verificationLabel(account: AccountRow): string {
switch (account.authState) {
case "expired":
return "需要重新授权";
case "revoked":
return "已停用";
case "challenge_required":
return account.authReason === "risk_control" ? "触发风控,需处理" : "需要人工验证";
}
if (account.lastVerifiedAt) {
return formatVerifiedAtLabel(account.lastVerifiedAt);
}
@@ -141,6 +158,10 @@ function sessionLabel(account: AccountRow): string {
}
}
function isReauthorizePending(account: AccountRow): boolean {
return bindPendingPlatformId.value === account.platform;
}
function onlineLabel(account: AccountRow): string {
return account.online ? "客户端在线" : "客户端离线";
}
@@ -307,6 +328,16 @@ async function unbindPlatform(account: AccountRow) {
<a-badge :status="platform.account.online ? 'success' : 'default'" :text="onlineLabel(platform.account)" />
</div>
<div class="action-group">
<a-tooltip v-if="platform.account.authState === 'expired'" title="重新授权" placement="top">
<a-button
type="text"
class="action-btn"
:loading="isReauthorizePending(platform.account)"
@click="bindPlatform(platform.id)"
>
<template #icon><ArrowRightOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip title="打开平台" placement="top">
<a-button
type="text"