feat(admin): 渠道状态三态化 + 顶栏后端失联指示 — 不再把「读不到」画成「未连接」
起因:面板把「取数失败(后端不可达)」渲染成红色「未连接」,主人因此把「后端没在跑」误读成「微信适配器挂了」(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:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user