refactor(desktop): narrow home health board to expired/revoked accounts

Home 看板只保留已过期或已停用的账号,问题计数改用阻塞态 authState;新增 formatVerifiedAtLabel,统一校验通过时间的展示粒度。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 17:25:58 +08:00
parent 55e1c2e74b
commit 69fd182755
5 changed files with 41 additions and 22 deletions
@@ -114,6 +114,9 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
acc[item.health] = (acc[item.health] ?? 0) + 1;
return acc;
}, {});
const blockingAccountCount = accounts.filter((item) =>
["expired", "revoked", "challenge_required"].includes(item.authState),
).length;
return {
generatedAt: now,
@@ -138,7 +141,7 @@ function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeContr
queuedTasks: tasks.filter((item) => item.status === "queued").length,
issuesOpen:
tasks.filter((item) => ["unknown", "failed"].includes(item.status)).length
+ accounts.filter((item) => item.health !== "live").length,
+ blockingAccountCount,
healthCounts,
},
clients,
@@ -41,6 +41,21 @@ export function formatDateTime(value: number | null): string {
}).format(value);
}
export function formatVerifiedAtLabel(value: number | null): string {
if (!value) {
return "尚未完成校验";
}
const deltaMs = Math.abs(Date.now() - value);
if (deltaMs < 60_000) {
return "刚刚校验通过";
}
if (deltaMs < 60 * 60_000) {
return `${Math.max(1, Math.round(deltaMs / 60_000))} 分钟前校验通过`;
}
return `${formatDateTime(value)} 校验通过`;
}
export function titleCaseToken(value: string): string {
return value
.split(/[_-]/g)
@@ -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, titleCaseToken } from "../lib/formatters";
import { formatDateTime, formatRelativeTime, formatVerifiedAtLabel, titleCaseToken } from "../lib/formatters";
import { desktopPublishMediaCatalog } from "../lib/media-catalog";
import type { RuntimeAccount } from "../types";
@@ -126,7 +126,7 @@ function sessionStateLabel(account: AccountRow): string {
function verificationTimeLabel(account: AccountRow): string {
if (account.lastVerifiedAt) {
return `${formatRelativeTime(account.lastVerifiedAt)}校验通过`;
return formatVerifiedAtLabel(account.lastVerifiedAt);
}
if (account.probeState === "probing") {
return "正在校验";
@@ -11,7 +11,7 @@ import { computed, ref } from "vue";
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
import { showClientActionError } from "../lib/client-errors";
import { formatDateTime, formatRelativeTime } from "../lib/formatters";
import { formatDateTime, formatRelativeTime, formatVerifiedAtLabel } from "../lib/formatters";
import { desktopMonitoringMediaCatalog } from "../lib/media-catalog";
import type { RuntimeAccount } from "../types";
@@ -90,7 +90,7 @@ function authColor(account: AccountRow | null) {
function verificationLabel(account: AccountRow): string {
if (account.lastVerifiedAt) {
return `${formatRelativeTime(account.lastVerifiedAt)}校验通过`;
return formatVerifiedAtLabel(account.lastVerifiedAt);
}
if (account.probeState === "probing") {
return "正在执行首轮校验";
@@ -5,7 +5,7 @@ import { DesktopOutlined, LinkOutlined, ClockCircleOutlined, AlertOutlined } fro
import StatusBadge from "../components/StatusBadge.vue";
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
import { formatClock, formatRelativeTime, titleCaseToken } from "../lib/formatters";
import { healthTone, taskTone } from "../lib/runtime-ui";
import { healthTone } from "../lib/runtime-ui";
function translatePlatform(platform: string) {
const map: Record<string, string> = {
@@ -21,15 +21,14 @@ function translatePlatform(platform: string) {
return map[platform?.toLowerCase()] || titleCaseToken(platform);
}
function translateHealth(status: string) {
const map: Record<string, string> = {
live: "正常",
expired: "已过期",
disconnected: "已断开",
risk_control: "风控中",
needs_captcha: "需验证码",
};
return map[status?.toLowerCase()] || status;
function expiredAccountLabel(authState: string) {
switch (authState) {
case "revoked":
return "已停用";
case "expired":
default:
return "已过期";
}
}
function translateEventTitle(title: string) {
@@ -84,7 +83,9 @@ const tasks = computed(() => snapshot.value?.tasks ?? []);
const accounts = computed(() => snapshot.value?.accounts ?? []);
const activity = computed(() => snapshot.value?.activity ?? []);
const riskyAccounts = computed(() => accounts.value.filter((item) => item.health !== "live"));
const expiredAccounts = computed(() =>
accounts.value.filter((item) => ["expired", "revoked"].includes(item.authState)),
);
const subsystemCards = computed(() => {
const subsystems = snapshot.value?.subsystems ?? {};
@@ -158,23 +159,23 @@ const subsystemCards = computed(() => {
<div class="card-header">
<div class="header-text">
<h3>账号健康看板</h3>
<p>把需要修复登录态验证码或风险确认的账号单独拉出来</p>
<p>这里只展示已经确认过期需要重新授权的账号</p>
</div>
</div>
<div v-if="riskyAccounts.length > 0" class="info-list">
<div v-for="account in riskyAccounts" :key="account.id" class="info-row record-row">
<div v-if="expiredAccounts.length > 0" class="info-list">
<div v-for="account in expiredAccounts" :key="account.id" class="info-row record-row">
<div class="info-stack">
<span class="title">{{ account.displayName }}</span>
<span class="subtitle">{{ translatePlatform(account.platform) }} · <span class="mono-text">{{ formatUid(account.platformUid) }}</span></span>
</div>
<div class="info-stack" style="align-items: flex-end;">
<StatusBadge :tone="healthTone(account.health)" :label="translateHealth(account.health)" />
<span class="subtitle" style="margin-top: 6px;">{{ formatRelativeTime(account.lastSyncAt) }}</span>
<StatusBadge :tone="healthTone(account.health)" :label="expiredAccountLabel(account.authState)" />
<span class="subtitle" style="margin-top: 6px;">{{ formatRelativeTime(account.lastVerifiedAt ?? account.lastSyncAt) }}</span>
</div>
</div>
</div>
<div v-else class="empty-state">
<p>当前没有发现异常账号一切运行正常</p>
<p>当前没有已过期账号</p>
</div>
</article>