Files
geo/apps/desktop-client/src/renderer/views/HomeView.vue
T
root 07881a66d0 style(desktop): refresh Home and Publish views with modern card layout
Inline the card styling on HomeView and PublishManagementView instead of
leaning on the generic MetricCard/SurfaceCard wrappers — larger numerics,
softer radii, clearer section headers, and a stats strip on the Publish hero.
No behavioral changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 16:04:39 +08:00

480 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed } from "vue";
import { DesktopOutlined, LinkOutlined, ClockCircleOutlined, AlertOutlined } from "@ant-design/icons-vue";
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";
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": "任务消费端已就绪",
"Desktop Stream Reconnecting": "单点事件流重连中",
"Desktop Stream Backoff": "单点事件流退避等待",
"Heartbeat Failed": "心跳连接失败",
"Account Sync Failed": "账号同步失败",
"Account Unbound": "账号已解绑",
"Specific Lease Missed": "指定任务领取失败",
"Pull Fallback Failed": "兜底拉取失败",
"Task Leased": "任务已领取",
"Task Completed As Failed": "任务执行失败",
"Task Completed As Unknown": "任务执行结果待确认",
"Task Succeeded": "任务执行成功",
"Task Failed": "任务执行失败",
"Lease Extend Failed": "租约续期失败",
"Desktop Client Token Expired": "客户端令牌已过期",
};
return map[title] || title;
}
function translateEventDetail(detail: string) {
const exactMap: Record<string, string> = {
"已启动 SSE、heartbeat 和 pull fallback。": "已启动 SSE、心跳上报和兜底拉取。",
"client heartbeat 已写回服务端。": "客户端心跳已回写到服务端。",
};
return (exactMap[detail] || detail)
.replace(/\brouting=rabbitmq-primary\b/gi, "路由:主消息队列通道")
.replace(/\brouting=db-recovery\b/gi, "路由:数据库兜底拉取")
.replace(/\bdesktop client\b/gi, "桌面客户端")
.replace(/\bclient heartbeat\b/gi, "客户端心跳")
.replace(/\bpull fallback\b/gi, "兜底拉取")
.replace(/\bheartbeat\b/gi, "心跳上报")
.replace(/\bClient\b/g, "客户端")
.replace(/\bunknown_error\b/gi, "未知错误");
}
function formatUid(uid: string) {
return (uid || "").replace(/^(?:platform:)+/gi, "");
}
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-container">
<div class="metrics-grid">
<article class="modern-card metric-card">
<div class="metric-head">
<div class="icon-wrap brand"><DesktopOutlined /></div>
<span class="eyebrow">在线节点</span>
</div>
<div class="metric-body">
<strong>{{ snapshot?.summary.onlineClients ?? 0 }}</strong>
<p>当前处于活跃状态的桌面客户端</p>
</div>
</article>
<article class="modern-card metric-card">
<div class="metric-head">
<div class="icon-wrap info"><LinkOutlined /></div>
<span class="eyebrow">已绑账号</span>
</div>
<div class="metric-body">
<strong>{{ snapshot?.summary.accountsBound ?? 0 }}</strong>
<p>已授权挂载至本空间的媒体账号</p>
</div>
</article>
<article class="modern-card metric-card">
<div class="metric-head">
<div class="icon-wrap warn"><ClockCircleOutlined /></div>
<span class="eyebrow">排队任务</span>
</div>
<div class="metric-body">
<strong>{{ snapshot?.summary.queuedTasks ?? 0 }}</strong>
<p>当前积压等待端侧领取的发布任务</p>
</div>
</article>
<article class="modern-card metric-card">
<div class="metric-head">
<div class="icon-wrap danger"><AlertOutlined /></div>
<span class="eyebrow">异常干预</span>
</div>
<div class="metric-body">
<strong>{{ snapshot?.summary.issuesOpen ?? 0 }}</strong>
<p>执行失败或需人工排查的阻断事项</p>
</div>
</article>
</div>
<div class="panels-grid">
<article class="modern-card panel-card">
<div class="card-header">
<div class="header-text">
<h3>账号健康看板</h3>
<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 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>
</div>
</div>
</div>
<div v-else class="empty-state">
<p>当前没有发现异常账号一切运行正常</p>
</div>
</article>
<article class="modern-card panel-card">
<div class="card-header">
<div class="header-text">
<h3>最近事件</h3>
<p>系统底层状态流转的历史快照主要用作诊断上下文</p>
</div>
</div>
<div v-if="activity.length > 0" class="timeline-list">
<div v-for="entry in activity" :key="entry.id" class="timeline-item record-row">
<div class="timeline-head">
<div style="display:flex; align-items:center; gap: 10px;">
<span :class="['feed-dot', entry.severity]"></span>
<span class="title">{{ translateEventTitle(entry.title) }}</span>
</div>
<span class="subtitle">{{ formatRelativeTime(entry.at) }}</span>
</div>
<div class="timeline-body">
<p>{{ translateEventDetail(entry.detail) }}</p>
</div>
</div>
</div>
<div v-else class="empty-state">
<p>当前没有最近事件记录</p>
</div>
</article>
</div>
<article class="modern-card panel-card">
<div class="card-header">
<div class="header-text">
<h3>引擎状态快照</h3>
<p>用于快速确认 transportlease manager vault 这三部分核心子系统是否处在正常工作状态</p>
</div>
</div>
<div class="subsystem-grid">
<div v-for="card in subsystemCards" :key="card.title" class="subsystem-block">
<span class="title">{{ card.title }}</span>
<div class="code-block">
<pre>{{ card.detail }}</pre>
</div>
</div>
</div>
</article>
</section>
</template>
<style scoped>
.page-container {
display: flex;
flex-direction: column;
gap: 24px;
padding-bottom: 40px;
max-width: 1400px;
}
/* Metrics Top Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 24px;
}
.modern-card {
display: flex;
flex-direction: column;
background: #ffffff;
border-radius: 20px;
border: 1px solid #eef2f6;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.02), 0 1px 2px -1px rgba(0, 0, 0, 0.01);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.modern-card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 20px -8px rgba(0, 0, 0, 0.04), 0 4px 6px -4px rgba(0, 0, 0, 0.02);
border-color: #e2e8f0;
}
.metric-card {
padding: 24px;
}
.metric-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.metric-head .eyebrow {
margin: 0;
color: #64748b;
font-size: 14px;
font-weight: 600;
}
.icon-wrap {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.icon-wrap.brand { background: #e0f2fe; color: #0284c7; }
.icon-wrap.info { background: #f1f5f9; color: #475569; }
.icon-wrap.warn { background: #fef9c3; color: #ca8a04; }
.icon-wrap.danger { background: #fee2e2; color: #dc2626; }
.metric-body strong {
display: block;
font-size: 36px;
font-weight: 800;
color: #0f172a;
font-feature-settings: 'tnum';
line-height: 1;
}
.metric-body p {
margin: 12px 0 0;
font-size: 13px;
color: #64748b;
line-height: 1.5;
}
/* Two Column Panels Grid */
.panels-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
}
.panel-card {
padding: 32px;
}
.card-header {
margin-bottom: 24px;
}
.header-text h3 {
margin: 0;
font-size: 20px;
font-weight: 700;
color: #0f172a;
}
.header-text p {
margin: 8px 0 0;
font-size: 14px;
color: #64748b;
line-height: 1.5;
}
/* List Styling */
.info-list, .timeline-list {
display: flex;
flex-direction: column;
gap: 12px;
max-height: 400px;
overflow-y: auto;
}
.record-row {
padding: 16px 20px;
border-radius: 14px;
background: #f8fafc;
border: 1px solid #f1f5f9;
transition: all 0.2s ease;
}
.record-row:hover {
background: #ffffff;
border-color: #e2e8f0;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.timeline-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.timeline-head {
display: flex;
justify-content: space-between;
align-items: center;
}
.timeline-body p {
margin: 0 0 0 18px;
font-size: 13px;
color: #475569;
line-height: 1.6;
}
.feed-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
background-color: #cbd5e1;
}
.feed-dot.success { background-color: #10b981; }
.feed-dot.warn { background-color: #f59e0b; }
.feed-dot.danger { background-color: #ef4444; }
.feed-dot.info { background-color: #0ea5e9; }
/* Subsystems Bottom Panel */
.subsystem-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
}
.subsystem-block {
display: flex;
flex-direction: column;
gap: 12px;
}
.subsystem-block .title {
font-size: 15px;
font-weight: 700;
color: #0f172a;
}
.code-block {
background: #f8fafc;
border: 1px solid #eef2f6;
border-radius: 14px;
padding: 20px;
overflow-x: auto;
}
.code-block pre {
margin: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 13px;
color: #475569;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
}
/* Base Typo */
.info-stack {
display: flex;
flex-direction: column;
gap: 4px;
}
.title {
color: #0f172a;
font-size: 14px;
font-weight: 600;
}
.subtitle {
color: #64748b;
font-size: 13px;
}
.mono-text {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.empty-state {
padding: 40px;
background: #f8fafc;
border-radius: 14px;
border: 1px dashed #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
}
.empty-state p {
margin: 0;
color: #94a3b8;
font-size: 14px;
}
/* Responsiveness */
@media (max-width: 1024px) {
.panels-grid, .subsystem-grid {
grid-template-columns: 1fr;
}
}
</style>