feat(admin): 仪表盘次级概览卡接真数据(A5) — 8卡聚合,存储卡诚实占位
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- Dashboard.tsx 去掉占位块 → OverviewSection; 新增 admin/modules/dashboard/(MiniMetric/useDashboardExtras/overviewMetrics/OverviewSection)
- 8卡用现有 lib/api 导出: LLM组/RAG文档/自动化/已装插件(含toolCount)/用户/网关请求/LLM调用+子代理/存储(缺只读端点→诚实占位)
- 端点报错→值'—'+徽标'端点报错'+hint后端原文(不崩不伪造)
- 仅碰 Dashboard.tsx+dashboard/*, 未动 api.ts/types.ts(避免并发冲突)
- 前端 npm run build 绿(156 modules)
This commit is contained in:
AskaEth committed 2026-09-30 08:44:00 +08:00
1 parent f28995c1e5
commit ca2faa0f95
5 files changed
+343 -23

No files matched your search

@@ -0,0 +1,54 @@
import type { ReactNode } from "react";
import { Icon, type IconName } from "../../../components/Icon";
import { Tag, type TagTone } from "../../../components/primitives";
import { navigateAdmin, type AdminSectionId } from "../../router";
/** Honest per-card state: real data landed / still loading / endpoint failed /
* no endpoint exists in lib/api.ts (a placeholder, never a fabricated value). */
export type MetricState = "ok" | "loading" | "error" | "placeholder";
/** One derived overview card (design/10 §4: 卡片多而全, 详情在各独立页面). */
export interface MiniMetricSpec {
icon: IconName;
label: string;
value: string;
hint: string;
state: MetricState;
/** Admin module this card drills down to; absent = not a link. */
section?: AdminSectionId;
}
const STATE_BADGE: Record<MetricState, { label: string; tone: TagTone }> = {
ok: { label: "已接入", tone: "ok" },
loading: { label: "加载中", tone: "neutral" },
error: { label: "端点报错", tone: "err" },
placeholder: { label: "占位", tone: "warn" },
};
export function MiniMetric({ icon, label, value, hint, state, section }: MiniMetricSpec): ReactNode {
const badge = STATE_BADGE[state];
const body = (
<>
<div className="flex items-center gap-1.5 text-xs text-[var(--mu-text-lo)]">
<Icon name={icon} size={14} />
<span className="truncate">{label}</span>
<Tag tone={badge.tone} className="ml-auto shrink-0">{badge.label}</Tag>
</div>
<div className="mt-1.5 text-lg font-semibold leading-tight">{value}</div>
<div className="mt-0.5 break-words text-xs text-[var(--mu-text-lo)]">{hint}</div>
</>
);
const base =
"rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)]/60 p-3";
if (!section) return <div className={base}>{body}</div>;
return (
<button
type="button"
title="点击下钻"
onClick={() => navigateAdmin(section)}
className={`${base} text-left transition-all duration-200 ease-[var(--mu-spring)] hover:border-[var(--mu-accent)]/50`}
>
{body}
</button>
);
}