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:
1 parent
4c48e94659
commit
f43eb17030
26 files changed
+2253
-4
No files matched your search
@@ -0,0 +1,166 @@
|
||||
import { useState } from "react";
|
||||
import { applyTheme, readTheme, type ThemeMode } from "../lib/theme";
|
||||
import { getAuthToken, getStoredRole } from "../lib/api";
|
||||
import { ADMIN_GROUPS, ADMIN_SECTIONS } from "./sections";
|
||||
import { navigateAdmin, type AdminSectionId } from "./router";
|
||||
import { Button, Tag } from "../components/primitives";
|
||||
import { DashboardModule } from "./modules/Dashboard";
|
||||
import { PersonasModule } from "./modules/Personas";
|
||||
import { SessionsModule } from "./modules/Sessions";
|
||||
import { MemoriesModule } from "./modules/Memories";
|
||||
import { PluginsModule } from "./modules/Plugins";
|
||||
import { ChannelsModule } from "./modules/Channels";
|
||||
import { IoTModule } from "./modules/IoT";
|
||||
import { RagModule } from "./modules/Rag";
|
||||
import { LlmModule } from "./modules/Llm";
|
||||
import { AutomationsModule } from "./modules/Automations";
|
||||
import { AgentsModule } from "./modules/Agents";
|
||||
import { BackthinkModule } from "./modules/Backthink";
|
||||
import { ObservabilityModule } from "./modules/Observability";
|
||||
import { UsersModule } from "./modules/Users";
|
||||
import { SystemModule } from "./modules/System";
|
||||
|
||||
function renderSection(section: AdminSectionId): React.ReactNode {
|
||||
switch (section) {
|
||||
case "dashboard": return <DashboardModule />;
|
||||
case "personas": return <PersonasModule />;
|
||||
case "sessions": return <SessionsModule />;
|
||||
case "memories": return <MemoriesModule />;
|
||||
case "plugins": return <PluginsModule />;
|
||||
case "channels": return <ChannelsModule />;
|
||||
case "iot": return <IoTModule />;
|
||||
case "rag": return <RagModule />;
|
||||
case "llm": return <LlmModule />;
|
||||
case "automations": return <AutomationsModule />;
|
||||
case "agents": return <AgentsModule />;
|
||||
case "backthink": return <BackthinkModule />;
|
||||
case "observability": return <ObservabilityModule />;
|
||||
case "users": return <UsersModule />;
|
||||
case "system": return <SystemModule />;
|
||||
default: return <DashboardModule />;
|
||||
}
|
||||
}
|
||||
|
||||
function AdminSidebar({ active }: { active: AdminSectionId }): React.ReactNode {
|
||||
return (
|
||||
<nav className="glass hidden h-full w-[var(--mu-sidebar-w)] shrink-0 flex-col gap-1 overflow-y-auto border-r border-[var(--mu-glass-border)] p-3 md:flex">
|
||||
{ADMIN_GROUPS.map((group) => (
|
||||
<div key={group} className="mb-1">
|
||||
<div className="px-2 py-2 text-xs font-semibold uppercase tracking-wider text-[var(--mu-text-lo)]">{group}</div>
|
||||
{ADMIN_SECTIONS.filter((s) => s.group === group).map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => navigateAdmin(s.id)}
|
||||
className={`flex w-full items-center gap-2.5 rounded-[var(--mu-radius)] px-3 py-2 text-sm text-left transition-all duration-200 ease-[var(--mu-spring)] active:scale-[0.98] ${
|
||||
s.id === active
|
||||
? "bg-[var(--mu-accent)]/15 font-medium text-[var(--mu-accent-hi)]"
|
||||
: "text-[var(--mu-text-mid)] hover:bg-[var(--mu-surface-4)]"
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">{s.icon}</span>
|
||||
<span>{s.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminTopBar({ section }: { section: AdminSectionId }): React.ReactNode {
|
||||
const role = getStoredRole();
|
||||
const [theme, setTheme] = useState<ThemeMode>(readTheme);
|
||||
const meta = ADMIN_SECTIONS.find((s) => s.id === section);
|
||||
const toggleTheme = () => {
|
||||
const next: ThemeMode = theme === "dark" ? "light" : "dark";
|
||||
applyTheme(next);
|
||||
setTheme(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="glass sticky top-0 z-30 flex h-[var(--mu-topbar-h)] items-center gap-3 border-b border-[var(--mu-glass-border)] px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src="/icon.svg" alt="Cyrene" className="h-7 w-7" />
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-semibold">Cyrene · 管理面板</div>
|
||||
<div className="text-[11px] text-[var(--mu-text-lo)]">{meta?.label ?? "仪表盘"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
{role ? <Tag tone={role === "owner" ? "accent" : role === "admin" ? "ok" : "neutral"}>{role}</Tag> : null}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
aria-label="切换主题"
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-[var(--mu-radius)] bg-[var(--mu-surface-4)] text-sm transition-all duration-200 ease-[var(--mu-spring)] hover:bg-[var(--mu-surface-5)] active:scale-95"
|
||||
>
|
||||
{theme === "dark" ? "☀" : "☾"}
|
||||
</button>
|
||||
<Button size="sm" variant="ghost" onClick={() => { window.location.hash = ""; }}>
|
||||
返回用户端
|
||||
</Button>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginGate(): React.ReactNode {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<div className="text-4xl">🔒</div>
|
||||
<div className="text-lg font-semibold">需要登录</div>
|
||||
<p className="max-w-md text-sm text-[var(--mu-text-lo)]">
|
||||
管理面板需持有 JWT。请先到用户端 <span className="text-[var(--mu-text-hi)]">设置 → 连接</span> 登录获取令牌后,再返回本页。
|
||||
</p>
|
||||
<Button variant="primary" onClick={() => { window.location.hash = ""; }}>前往用户端登录</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Forbidden403(): React.ReactNode {
|
||||
const role = getStoredRole();
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
|
||||
<div className="text-4xl">🚫</div>
|
||||
<div className="text-lg font-semibold">403 · 权限不足</div>
|
||||
<p className="max-w-md text-sm text-[var(--mu-text-lo)]">
|
||||
当前角色 <Tag tone="neutral">{role ?? "?"}</Tag>(viewer 只读)。管理面板需 <Tag tone="accent">owner</Tag> 或 <Tag tone="ok">admin</Tag> 角色;管理端写操作仅 owner。
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="primary" onClick={() => { window.location.hash = ""; }}>返回用户端</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminApp({ section }: { section: AdminSectionId }): React.ReactNode {
|
||||
// RBAC guard (design/10 §2 + design/11 §7). A logged-in viewer or an
|
||||
// unauthenticated user never sees the admin module; the gateway would also
|
||||
// reject them with 401/403.
|
||||
if (!getAuthToken() || !getStoredRole()) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<AdminTopBar section={section} />
|
||||
<LoginGate />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (getStoredRole() === "viewer") {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<AdminTopBar section={section} />
|
||||
<Forbidden403 />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<AdminTopBar section={section} />
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<AdminSidebar active={section} />
|
||||
<main className="relative flex-1 overflow-y-auto p-4 md:p-6">
|
||||
<div className="mx-auto max-w-6xl">{renderSection(section)}</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user