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

+144
View File
@@ -0,0 +1,144 @@
import type { ReactNode } from "react";
import { EmptyState, Tag, type TagTone } from "../components/primitives";
export interface PageHeaderProps {
title: string;
subtitle?: string;
actions?: ReactNode;
}
export function PageHeader({ title, subtitle, actions }: PageHeaderProps): ReactNode {
return (
<div className="flex flex-wrap items-end gap-3">
<div>
<h2 className="text-lg font-semibold">{title}</h2>
{subtitle ? <p className="text-sm text-[var(--mu-text-lo)]">{subtitle}</p> : null}
</div>
{actions ? <div className="ml-auto flex items-center gap-2">{actions}</div> : null}
</div>
);
}
export interface NoticeProps {
tone?: TagTone;
children: ReactNode;
}
/** A soft inline notice — used for "只读/后端 M10+ 提供" hints. */
export function Notice({ tone = "neutral", children }: NoticeProps): ReactNode {
return (
<div className="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={tone} className="mr-1.5">{tone === "warn" ? "待实现" : tone === "err" ? "错误" : "提示"}</Tag>
{children}
</div>
);
}
export interface StatCardProps {
icon?: ReactNode;
label: string;
value: ReactNode;
sub?: ReactNode;
tone?: TagTone;
}
export function StatCard({ icon, label, value, sub, tone = "accent" }: StatCardProps): ReactNode {
return (
<div className="liquid-glass flex flex-col gap-1.5 rounded-[var(--mu-radius-lg)] p-4">
<div className="flex items-center gap-2 text-xs text-[var(--mu-text-lo)]">
{icon ? <span className="text-base">{icon}</span> : null}
<span>{label}</span>
</div>
<div className="text-2xl font-semibold leading-tight">{value}</div>
{sub ? <div className="text-xs text-[var(--mu-text-lo)]">{sub}</div> : null}
<Tag tone={tone} className="w-fit">{tone}</Tag>
</div>
);
}
export function CardGrid({ children }: { children: ReactNode }): ReactNode {
return <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">{children}</div>;
}
export interface AdminColumn<T> {
key: string;
title: string;
render: (row: T) => ReactNode;
className?: string;
}
export interface AdminTableProps<T> {
columns: AdminColumn<T>[];
rows: readonly T[] | null;
loading: boolean;
error: string | null;
emptyTitle?: string;
rowKey: (row: T) => string;
}
/** A strict, generic data table. Loading/error/empty states are handled here so
* every module has a complete surface without duplicating state rendering. */
export function AdminTable<T>({
columns,
rows,
loading,
error,
emptyTitle = "暂无数据",
rowKey,
}: AdminTableProps<T>): ReactNode {
if (loading) {
return <div className="px-4 py-10 text-center text-xs text-[var(--mu-text-lo)]">加载中…</div>;
}
if (error) {
return <div className="px-4 py-10 text-center text-xs text-[var(--mu-err)]">{error}</div>;
}
if (!rows || rows.length === 0) {
return <EmptyState icon="🗂" title={emptyTitle} />;
}
return (
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="border-b border-[var(--mu-glass-border)] text-left">
{columns.map((c) => (
<th
key={c.key}
className={`px-3 py-2 text-xs font-semibold uppercase tracking-wider text-[var(--mu-text-lo)] ${c.className ?? ""}`}
>
{c.title}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={rowKey(row)} className="border-b border-[var(--mu-glass-border)]/60 last:border-0 hover:bg-[var(--mu-surface-3)]/60">
{columns.map((c) => (
<td key={c.key} className={`px-3 py-2.5 align-top ${c.className ?? ""}`}>
{c.render(row)}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
export interface KeyValueRowProps {
label: string;
value: ReactNode;
mono?: boolean;
}
export function KeyValueRow({ label, value, mono = false }: KeyValueRowProps): ReactNode {
return (
<div className="flex items-start justify-between gap-4 py-1.5">
<span className="shrink-0 text-xs text-[var(--mu-text-lo)]">{label}</span>
<span className={`min-w-0 text-right text-sm text-[var(--mu-text-hi)] ${mono ? "font-mono" : ""}`}>
{value}
</span>
</div>
);
}