feat(admin): 渠道状态三态化 + 顶栏后端失联指示 — 不再把「读不到」画成「未连接」
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

起因:面板把「取数失败(后端不可达)」渲染成红色「未连接」,主人因此把「后端没在跑」误读成「微信适配器挂了」(AdapterDetailPage 的 record?.connected ?? false)。
- 新增 channels/channelStatus.ts(纯逻辑三态:ready/pending/unknown + 记录级 connected/disconnected/unconfigured/unknown/pending;局部类型,未动 lib/types.ts)
- 新增 channelStatusView.tsx:ChannelStatusTag(三态 Tag + 「数据可能过期」)+ ChannelLoadError(失败/过期横幅 + 重试)
- 覆盖 13 处渲染点(channels/ 全目录 + AdminApp):未知态一律灰色「状态未知」+ 原因,绝不落红色「未连接」;未知态禁用连接/断开与「新建/编辑配置」(避免 prefill 空列表误建/覆盖);读取失败不再冒充「尚未保存任何渠道配置」「暂无消息」「未声明能力」等事实性谎言
- 顺带修:WeixinLogin 首次 login-status 读取失败被 .catch 静默吞成「未登录」,现如实报「读取失败(与后端失联)」
- AdminApp 顶栏新增 useBackendHealth + BackendStatusTag:HTTP /api/v1/health 探活(15s 循环),**探测成功前不显示「在线」**;WS 状态作为独立 Tag 展示,两者不混算
- 验收:npm run build 绿(177 模块)+ 新增 45 断言 + 既有 54 断言回归 + renderToStaticMarkup 真渲染冒烟(unknown 渲染不含「未连接」也不含 err 色;未探测成功不显示在线)
- ListPage 主函数 125→71 行(改前已超 80 行红线,行为等价)
This commit is contained in:
AskaEth committed 2026-10-03 19:16:22 +08:00
1 parent a51aadeef4
commit 37fb3cd265
12 files changed
+782 -192

No files matched your search

@@ -0,0 +1,65 @@
import { Button, Tag } from "../../../components/primitives";
import type { ChannelStatus, ChannelStatusKind } from "./channelStatus";
const dot: Record<ChannelStatusKind, string> = {
connected: "bg-[var(--mu-ok)]",
disconnected: "bg-[var(--mu-err)]",
unconfigured: "bg-[var(--mu-text-lo)]",
unknown: "bg-[var(--mu-text-lo)]",
pending: "bg-[var(--mu-text-lo)] mu-blink",
};
/** The one honest place an adapter's connection state is rendered, so list page,
* detail page and overview can never drift apart: 已连接 / 未连接 / 状态未知.
* `showHint` off renders the compact form for dense surfaces (table row, card) —
* the surrounding banner already carries the failure reason there. */
export function ChannelStatusTag({
status,
showHint = true,
className = "",
}: {
status: ChannelStatus;
showHint?: boolean;
className?: string;
}): React.ReactNode {
return (
<span className={`inline-flex flex-wrap items-center gap-2 ${className}`}>
<Tag tone={status.tone}>
<span className={`inline-block h-1.5 w-1.5 rounded-full ${dot[status.kind]}`} />
{status.label}
</Tag>
{status.stale ? <Tag tone="warn">数据可能过期</Tag> : null}
{showHint && status.hint ? <span className="text-xs text-[var(--mu-text-lo)]">{status.hint}</span> : null}
</span>
);
}
/** Read-failure banner with a retry. `stale` means an older copy is still on screen,
* so it warns about it instead of hiding the data behind an error. Failure is
* named as "与后端失联" so a stopped gateway is never read as a broken adapter. */
export function ChannelLoadError({
error,
reload,
stale = false,
what = "渠道数据",
}: {
error: string | null;
reload: () => void;
stale?: boolean;
what?: string;
}): React.ReactNode {
if (!error) return null;
return (
<div className="flex flex-wrap items-center gap-2 rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)]/60 px-3 py-2 text-xs text-[var(--mu-text-mid)]">
<Tag tone={stale ? "warn" : "err"}>{stale ? "数据可能过期" : "读取失败"}</Tag>
<span className="min-w-0 flex-1">
{stale
? `${what}本次刷新失败(与后端失联):${error}`
: `${what}读取失败(与后端失联):${error}`}
</span>
<Button size="sm" variant="ghost" onClick={reload}>
重试
</Button>
</div>
);
}