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