feat(frontend/admin): M10 后台管理面板(/admin hash 路由 + 15 大模块 + RBAC(owner/admin/viewer) + 审计 + Miuix);Docs/api-client.md v0.3 契约补全;tsc+vite build 通过

This commit is contained in:
Aska committed 2026-09-06 07:55:04 +08:00
1 parent 4c48e94659
commit f43eb17030
26 files changed
+2253 -4

No files matched your search

@@ -0,0 +1,140 @@
import { useState } from "react";
import { useAppStore } from "../../store/useAppStore";
import { useAuditStore } from "../audit";
import { CardGrid, Notice, PageHeader, StatCard } from "../components";
import { Button, Surface, Tag } from "../../components/primitives";
type Tab = "logs" | "trace" | "metrics" | "audit";
const TABS: ReadonlyArray<{ id: Tab; label: string }> = [
{ id: "logs", label: "实时日志" },
{ id: "trace", label: "Tracing" },
{ id: "metrics", label: "指标" },
{ id: "audit", label: "审计" },
];
export function ObservabilityModule(): React.ReactNode {
const [tab, setTab] = useState<Tab>("logs");
const deviceUpdates = useAppStore((s) => s.deviceUpdates);
const audit = useAuditStore((s) => s.records);
const clearAudit = useAuditStore((s) => s.clear);
return (
<div className="space-y-5">
<PageHeader title="可观测性" subtitle="实时日志 tail · Tracing(LLM/工具/编排) · 指标 · 审计" />
<div className="flex flex-wrap items-center gap-2">
{TABS.map((t) => (
<button
key={t.id}
onClick={() => setTab(t.id)}
className={`rounded-[var(--mu-radius)] px-3 py-1.5 text-xs font-medium transition-all duration-200 ease-[var(--mu-spring)] ${
tab === t.id ? "bg-[var(--mu-accent)]/20 text-[var(--mu-accent-hi)]" : "bg-[var(--mu-surface-4)] text-[var(--mu-text-mid)]"
}`}
>
{t.label}
</button>
))}
</div>
{tab === "logs" ? <LogsTail lines={deviceUpdates} /> : null}
{tab === "trace" ? <TraceView /> : null}
{tab === "metrics" ? <MetricsView /> : null}
{tab === "audit" ? <AuditView records={audit} onClear={clearAudit} /> : null}
</div>
);
}
function LogsTail({ lines }: { lines: string[] }): React.ReactNode {
return (
<Surface className="space-y-3">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold">实时日志流(WS 事件回显)</h3>
<Tag tone="ok">tail</Tag>
</div>
{lines.length === 0 ? (
<Notice>暂无实时事件。接入 /ws/chat 后,此处回显 stream / tool / device_update / reminder 等事件。</Notice>
) : (
<pre className="max-h-80 overflow-auto whitespace-pre-wrap rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 font-mono text-xs text-[var(--mu-text-mid)]">
{lines.join("\n")}
</pre>
)}
<Notice>完整后端日志分级 tail / 过滤 / 导出需后端 M10+ 提供。</Notice>
</Surface>
);
}
function TraceView(): React.ReactNode {
return (
<Surface className="space-y-3">
<h3 className="text-sm font-semibold">Tracing 视图</h3>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
{["LLM 调用链", "工具链", "编排链"].map((x) => (
<div key={x} className="rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-3)]/60 p-4 text-center text-xs text-[var(--mu-text-mid)]">
<div className="mb-2 text-lg">{x}</div>
{x} 占位 —— 后端 M10+ 提供 span/trace 数据
</div>
))}
</div>
</Surface>
);
}
function MetricsView(): React.ReactNode {
return (
<div className="space-y-4">
<CardGrid>
<StatCard icon="⚙" label="请求 QPS" value="—" sub="网关指标占位" tone="accent" />
<StatCard icon="🤖" label="LLM 调用次数" value="—" sub="用量/费用占位" tone="neutral" />
<StatCard icon="🧩" label="子代理事件" value="—" sub="编排指标占位" tone="ok" />
<StatCard icon="🧠" label="工时/Token" value="—" sub="成本占位" tone="warn" />
</CardGrid>
<Notice>指标时序图 / 导出需后端 M10+ 提供。</Notice>
</div>
);
}
interface AuditViewProps {
records: ReturnType<typeof useAuditStore.getState>["records"];
onClear: () => void;
}
function AuditView({ records, onClear }: AuditViewProps): React.ReactNode {
return (
<Surface className="space-y-3">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold">审计日志(who / what / when / result)</h3>
<span className="text-xs text-[var(--mu-text-lo)]">管理 + 敏感操作全程落审计</span>
<span className="ml-auto">
<Button size="sm" variant="ghost" onClick={onClear}>
清空本地
</Button>
</span>
</div>
{records.length === 0 ? (
<Notice>暂无审计记录。本面板的管理/敏感操作(策略写入、主人档案编辑、API key 签发等)都会在此登记。</Notice>
) : (
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--mu-glass-border)] text-left text-xs uppercase tracking-wider text-[var(--mu-text-lo)]">
<th className="px-2 py-1.5">who</th>
<th className="px-2 py-1.5">what</th>
<th className="px-2 py-1.5">when</th>
<th className="px-2 py-1.5">result</th>
</tr>
</thead>
<tbody>
{records.map((r) => (
<tr key={r.id} className="border-b border-[var(--mu-glass-border)]/60 last:border-0">
<td className="px-2 py-2 text-xs">{r.who}</td>
<td className="px-2 py-2 text-xs font-mono">{r.what}</td>
<td className="px-2 py-2 text-xs text-[var(--mu-text-lo)]">{new Date(r.when).toLocaleString()}</td>
<td className="px-2 py-2"><Tag tone={r.result === "ok" ? "ok" : "err"}>{r.result}</Tag></td>
</tr>
))}
</tbody>
</table>
)}
</Surface>
);
}