2026-04-19 14:18:20 +08:00
|
|
|
<script setup lang="ts">
|
2026-04-20 09:52:48 +08:00
|
|
|
import { computed } from "vue";
|
|
|
|
|
import { DesktopOutlined, LinkOutlined, ClockCircleOutlined, AlertOutlined } from "@ant-design/icons-vue";
|
2026-04-19 14:18:20 +08:00
|
|
|
|
|
|
|
|
import MetricCard from "../components/MetricCard.vue";
|
|
|
|
|
import StatusBadge from "../components/StatusBadge.vue";
|
|
|
|
|
import SurfaceCard from "../components/SurfaceCard.vue";
|
|
|
|
|
import { useDesktopRuntime } from "../composables/useDesktopRuntime";
|
|
|
|
|
import { formatClock, formatRelativeTime, titleCaseToken } from "../lib/formatters";
|
|
|
|
|
import { healthTone, taskTone } from "../lib/runtime-ui";
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
function translatePlatform(platform: string) {
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
toutiaohao: "头条号",
|
|
|
|
|
bilibili: "哔哩哔哩",
|
|
|
|
|
xiaohongshu: "小红书",
|
|
|
|
|
douyin: "抖音",
|
|
|
|
|
kuaishou: "快手",
|
|
|
|
|
wechat_oa: "微信公众号",
|
|
|
|
|
baijiahao: "百家号",
|
|
|
|
|
zhihu: "知乎",
|
|
|
|
|
};
|
|
|
|
|
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 translateEventTitle(title: string) {
|
|
|
|
|
const map: Record<string, string> = {
|
|
|
|
|
"Accounts Synced": "本地账号同步",
|
|
|
|
|
"Heartbeat Healthy": "心跳连接正常",
|
|
|
|
|
"Desktop Stream Connected": "单点事件流已连接",
|
|
|
|
|
"Desktop Runtime Started": "调度节点已启动",
|
|
|
|
|
"Desktop Runtime Armed": "任务消费端已就绪",
|
|
|
|
|
};
|
|
|
|
|
return map[title] || title;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatUid(uid: string) {
|
|
|
|
|
return (uid || "").replace(/^(?:platform:)+/gi, "");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 14:18:20 +08:00
|
|
|
const { snapshot, loading, refresh } = useDesktopRuntime();
|
|
|
|
|
|
|
|
|
|
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 subsystemCards = computed(() => {
|
|
|
|
|
const subsystems = snapshot.value?.subsystems ?? {};
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
title: "Transport",
|
|
|
|
|
detail: JSON.stringify(subsystems.transport ?? {}, null, 2),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "Lease Manager",
|
|
|
|
|
detail: JSON.stringify(subsystems.leaseManager ?? {}, null, 2),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "Vault",
|
|
|
|
|
detail: JSON.stringify(subsystems.vault ?? {}, null, 2),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<section class="page">
|
|
|
|
|
|
|
|
|
|
<div class="metric-grid">
|
|
|
|
|
<MetricCard
|
2026-04-20 09:52:48 +08:00
|
|
|
eyebrow="在线节点"
|
2026-04-19 14:18:20 +08:00
|
|
|
:value="snapshot?.summary.onlineClients ?? 0"
|
2026-04-20 09:52:48 +08:00
|
|
|
label="当前处于活跃状态的桌面客户端"
|
2026-04-19 14:18:20 +08:00
|
|
|
tone="brand"
|
2026-04-20 09:52:48 +08:00
|
|
|
:icon="DesktopOutlined"
|
2026-04-19 14:18:20 +08:00
|
|
|
/>
|
|
|
|
|
<MetricCard
|
2026-04-20 09:52:48 +08:00
|
|
|
eyebrow="已绑账号"
|
2026-04-19 14:18:20 +08:00
|
|
|
:value="snapshot?.summary.accountsBound ?? 0"
|
2026-04-20 09:52:48 +08:00
|
|
|
label="已授权挂载至本空间的媒体账号"
|
2026-04-19 14:18:20 +08:00
|
|
|
tone="info"
|
2026-04-20 09:52:48 +08:00
|
|
|
:icon="LinkOutlined"
|
2026-04-19 14:18:20 +08:00
|
|
|
/>
|
|
|
|
|
<MetricCard
|
2026-04-20 09:52:48 +08:00
|
|
|
eyebrow="排队任务"
|
2026-04-19 14:18:20 +08:00
|
|
|
:value="snapshot?.summary.queuedTasks ?? 0"
|
2026-04-20 09:52:48 +08:00
|
|
|
label="当前积压等待端侧领取的发布任务"
|
2026-04-19 14:18:20 +08:00
|
|
|
tone="warn"
|
2026-04-20 09:52:48 +08:00
|
|
|
:icon="ClockCircleOutlined"
|
2026-04-19 14:18:20 +08:00
|
|
|
/>
|
|
|
|
|
<MetricCard
|
2026-04-20 09:52:48 +08:00
|
|
|
eyebrow="异常干预"
|
2026-04-19 14:18:20 +08:00
|
|
|
:value="snapshot?.summary.issuesOpen ?? 0"
|
2026-04-20 09:52:48 +08:00
|
|
|
label="执行失败或需人工排查的阻断事项"
|
2026-04-19 14:18:20 +08:00
|
|
|
tone="danger"
|
2026-04-20 09:52:48 +08:00
|
|
|
:icon="AlertOutlined"
|
2026-04-19 14:18:20 +08:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="panel-grid">
|
|
|
|
|
<SurfaceCard
|
2026-04-20 09:52:48 +08:00
|
|
|
eyebrow="风险巡检"
|
2026-04-19 14:18:20 +08:00
|
|
|
title="账号健康看板"
|
|
|
|
|
description="把需要修复登录态、验证码或风险确认的账号单独拉出来。"
|
|
|
|
|
>
|
|
|
|
|
<div v-if="riskyAccounts.length > 0" class="list-stack">
|
|
|
|
|
<article v-for="account in riskyAccounts" :key="account.id" class="list-row">
|
2026-04-20 09:52:48 +08:00
|
|
|
<div class="list-head-content">
|
2026-04-19 14:18:20 +08:00
|
|
|
<strong>{{ account.displayName }}</strong>
|
2026-04-20 09:52:48 +08:00
|
|
|
<p>{{ translatePlatform(account.platform) }} · {{ formatUid(account.platformUid) }}</p>
|
2026-04-19 14:18:20 +08:00
|
|
|
</div>
|
|
|
|
|
<div class="list-side">
|
2026-04-20 09:52:48 +08:00
|
|
|
<StatusBadge :tone="healthTone(account.health)" :label="translateHealth(account.health)" />
|
2026-04-19 14:18:20 +08:00
|
|
|
<small>{{ formatRelativeTime(account.lastSyncAt) }}</small>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
</div>
|
|
|
|
|
<p v-else class="empty-text" style="margin-top: 16px;">当前没有异常账号。</p>
|
|
|
|
|
</SurfaceCard>
|
|
|
|
|
|
|
|
|
|
<SurfaceCard
|
2026-04-20 09:52:48 +08:00
|
|
|
eyebrow="事件追踪"
|
2026-04-19 14:18:20 +08:00
|
|
|
title="最近事件"
|
2026-04-20 09:52:48 +08:00
|
|
|
description="系统底层状态流转的历史快照,主要用作诊断上下文。"
|
2026-04-19 14:18:20 +08:00
|
|
|
>
|
|
|
|
|
<div v-if="activity.length > 0" class="timeline">
|
2026-04-20 09:52:48 +08:00
|
|
|
<article v-for="entry in activity" :key="entry.id" class="feed-card">
|
|
|
|
|
<div class="feed-head">
|
|
|
|
|
<div class="feed-title">
|
|
|
|
|
<span class="feed-dot" :class="entry.severity"></span>
|
|
|
|
|
<strong>{{ translateEventTitle(entry.title) }}</strong>
|
2026-04-19 14:18:20 +08:00
|
|
|
</div>
|
2026-04-20 09:52:48 +08:00
|
|
|
<small>{{ formatRelativeTime(entry.at) }}</small>
|
2026-04-19 14:18:20 +08:00
|
|
|
</div>
|
2026-04-20 09:52:48 +08:00
|
|
|
<p>{{ entry.detail }}</p>
|
2026-04-19 14:18:20 +08:00
|
|
|
</article>
|
|
|
|
|
</div>
|
|
|
|
|
<p v-else class="empty-text" style="margin-top: 16px;">当前没有最近事件记录。</p>
|
|
|
|
|
</SurfaceCard>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<SurfaceCard
|
2026-04-20 09:52:48 +08:00
|
|
|
eyebrow="核心子系统"
|
|
|
|
|
title="引擎状态快照"
|
2026-04-19 14:18:20 +08:00
|
|
|
description="用于快速确认 transport、lease manager 和 vault 这三段是否处在可工作状态。"
|
|
|
|
|
>
|
|
|
|
|
<div class="subsystem-grid">
|
|
|
|
|
<article v-for="card in subsystemCards" :key="card.title" class="subsystem-card">
|
|
|
|
|
<strong>{{ card.title }}</strong>
|
|
|
|
|
<pre>{{ card.detail }}</pre>
|
|
|
|
|
</article>
|
|
|
|
|
</div>
|
|
|
|
|
</SurfaceCard>
|
|
|
|
|
|
|
|
|
|
<p v-if="loading" class="loading-text">正在刷新运行快照…</p>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.page {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 22px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.metric-grid,
|
|
|
|
|
.panel-grid,
|
|
|
|
|
.subsystem-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.metric-grid {
|
|
|
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel-grid {
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-stack,
|
|
|
|
|
.timeline {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
max-height: 380px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding-right: 8px; /* For scrollbar breathing room */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 14px 16px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e6edf5;
|
|
|
|
|
background: #fafafb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-row strong {
|
|
|
|
|
display: block;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-row p {
|
|
|
|
|
margin: 6px 0 0;
|
2026-04-20 09:52:48 +08:00
|
|
|
color: #8c8c8c;
|
2026-04-19 14:18:20 +08:00
|
|
|
line-height: 1.55;
|
2026-04-20 09:52:48 +08:00
|
|
|
font-size: 13px;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-side {
|
|
|
|
|
display: grid;
|
|
|
|
|
justify-items: end;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.list-side small,
|
|
|
|
|
.timeline-head small {
|
|
|
|
|
color: var(--geo-color-text-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-text,
|
|
|
|
|
.loading-text {
|
|
|
|
|
margin: 0;
|
|
|
|
|
color: var(--geo-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
.feed-card {
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e6edf5;
|
|
|
|
|
background: #fafafb;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 6px;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
.feed-head {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: center;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
.feed-title {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
.feed-dot {
|
|
|
|
|
width: 6px;
|
|
|
|
|
height: 6px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: #1677ff;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
.feed-dot.success {
|
|
|
|
|
background: #52c41a;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
.feed-dot.warn {
|
|
|
|
|
background: #faad14;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 09:52:48 +08:00
|
|
|
.feed-dot.danger {
|
|
|
|
|
background: #ff4d4f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.feed-title strong {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.feed-head small {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: #8c8c8c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.feed-card p {
|
|
|
|
|
margin: 0 0 0 14px;
|
|
|
|
|
color: #595959;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
line-height: 1.5;
|
2026-04-19 14:18:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.subsystem-grid {
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
margin-top: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.subsystem-card {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
border: 1px solid #e6edf5;
|
|
|
|
|
background: #fafafb;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.subsystem-card strong {
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.subsystem-card pre {
|
|
|
|
|
margin: 12px 0 0;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
color: var(--geo-color-text-secondary);
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html[data-theme="dark"] .list-row,
|
2026-04-20 09:52:48 +08:00
|
|
|
html[data-theme="dark"] .subsystem-card {
|
2026-04-19 14:18:20 +08:00
|
|
|
background: rgba(255, 255, 255, 0.03);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1240px) {
|
|
|
|
|
.panel-grid,
|
|
|
|
|
.subsystem-grid,
|
|
|
|
|
.metric-grid {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
.timeline-head {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|