初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

@@ -0,0 +1,237 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useCallback } from "react";
import type { ReactNode } from "react";
import {
AdminIcon,
AgentIcon,
AuditIcon,
GoalIcon,
MemoryIcon,
PerceptionIcon,
PlanIcon,
TenantIcon,
ToolsIcon,
TraceIcon,
UserIcon,
WorldIcon,
} from "@/components/ui/Icons";
import { cn } from "@/lib/utils/cn";
type AdminNavItem = { href: string; label: string; icon: ReactNode };
/**
* 司命台治理导航(横向 — 设计稿 topbar-nav)。
*/
const ADMIN_NAV: AdminNavItem[] = [
{ href: "/admin", label: "概览", icon: <AdminIcon /> },
{ href: "/admin/audit", label: "起居注", icon: <AuditIcon /> },
{ href: "/admin/traces", label: "链路追踪", icon: <TraceIcon /> },
{ href: "/admin/users", label: "用户管理", icon: <UserIcon /> },
{ href: "/admin/tenants", label: "多租户", icon: <TenantIcon /> },
{
href: "/admin/characters/templates",
label: "人物模板",
icon: <AdminIcon />,
},
];
/** 九枢治理导航(设计稿 pivot nav)。与 pivots/[pivot] 路由对齐 */
const PIVOT_NAV: AdminNavItem[] = [
{ href: "/admin/goals", label: "太一", icon: <GoalIcon /> },
{ href: "/admin/safety", label: "司命", icon: <AdminIcon /> },
{ href: "/admin/perception", label: "烛照", icon: <PerceptionIcon /> },
{ href: "/admin/world", label: "河图", icon: <WorldIcon /> },
{ href: "/admin/plans", label: "洛书", icon: <PlanIcon /> },
{ href: "/admin/memory", label: "息壤", icon: <MemoryIcon /> },
{ href: "/admin/tools", label: "百工", icon: <ToolsIcon /> },
{ href: "/admin/agents", label: "天工阁", icon: <AgentIcon /> },
];
/**
* 司命台外壳:独立冷色壳层(admin-shell → slate 色温分化)。
* 设计稿 admin-preview.html §4 AdminShell 完全对齐。
*
* 壳层特性:
* - 独立冷色 CSS 变量(.admin-shell 激活 `--slate` 色系)
* - 顶部横向双层导航(治理 nav + 九枢 pivot nav)
* - 主题切换 + 返回主域入口
* - iOS safe-area 适配
*/
export function AdminShell({ children }: { children: ReactNode }) {
const pathname = usePathname();
/** 设计稿 toggleTheme — 通过触发表单更改 / 自定义事件 */
const toggleTheme = useCallback(() => {
const root = document.documentElement;
const isDark = root.classList.contains("dark");
if (isDark) {
root.classList.remove("dark");
} else {
root.classList.add("dark");
}
// 持久化到 settings
try {
const raw = localStorage.getItem("hunjing-settings");
if (raw) {
const parsed = JSON.parse(raw);
if (parsed.state) {
parsed.state.theme = isDark ? "light" : "dark";
localStorage.setItem("hunjing-settings", JSON.stringify(parsed));
}
}
} catch {
// silent
}
}, []);
const navItemClass = (href: string, opts?: { pivot?: boolean }) =>
cn(
"inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm font-medium transition-colors shrink-0",
opts?.pivot ? "text-xs" : "text-sm",
pathname === href || pathname.startsWith(`${href}/`)
? "bg-patina-wash text-patina"
: "text-muted hover:bg-overlay-hover hover:text-fg",
);
return (
<div className="relative flex min-h-screen flex-col admin-shell">
<header
className="sticky top-0 z-30 flex flex-wrap items-center gap-2 border-b px-4 py-2.5 md:gap-3 md:px-6"
style={{
background: "var(--glass)",
backdropFilter: "blur(16px) saturate(140%)",
borderColor: "var(--rule)",
paddingLeft: "calc(1rem + env(safe-area-inset-left, 0px))",
paddingRight: "calc(1rem + env(safe-area-inset-right, 0px))",
}}
>
{/* 品牌标志 — 设计稿 topbar-brand */}
<Link
href="/admin"
className="flex items-center gap-1.5 font-serif text-base font-bold tracking-wide text-fg shrink-0"
style={{ letterSpacing: "0.06em" }}
>
{/* 设计稿 SVG knob:描边铜镜无极标记 */}
<svg
className="shrink-0"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="8.5" />
<circle cx="12" cy="12" r="4.5" opacity="0.45" />
<circle
cx="12"
cy="12"
r="1.6"
stroke="none"
fill="var(--cinnabar)"
/>
</svg>
司命台
</Link>
{/* 分隔符 */}
<span className="shrink-0 text-fg-faint opacity-50 select-none">/</span>
{/* 司命治理导航 — 设计稿 topbar-nav */}
<nav
aria-label="司命台导航"
className="order-3 flex w-full items-center gap-0.5 overflow-x-auto md:order-none md:w-auto md:flex-shrink"
>
{ADMIN_NAV.map((item) => (
<Link
key={item.href}
href={item.href}
className={navItemClass(item.href)}
>
{item.label}
</Link>
))}
</nav>
{/* 九枢分隔 + 导航 — 设计稿中金色竖线 | 分隔 */}
<span className="mx-1 hidden shrink-0 select-none text-fg-faint opacity-40 md:inline">
|
</span>
<nav
aria-label="九枢导航"
className="order-4 flex w-full items-center gap-0.5 overflow-x-auto md:order-none md:w-auto"
>
{PIVOT_NAV.map((item) => (
<Link
key={item.href}
href={item.href}
className={navItemClass(item.href, { pivot: true })}
>
{item.label}
</Link>
))}
</nav>
{/* 弹性间隔 */}
<span className="flex-1 min-w-0" />
{/* 返回主域 — 设计稿 topbar-back */}
<Link
href="/chat"
className="flex shrink-0 items-center gap-1 rounded-lg px-2 py-1 text-xs font-medium text-muted transition-colors hover:bg-overlay-hover hover:text-fg"
title="返回主域"
>
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
aria-hidden
>
<path
d="M10 4l-4 4 4 4"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
主域
</Link>
{/* 主题切换 — 设计稿 topbar-back 风格 */}
<button
type="button"
onClick={toggleTheme}
className="flex shrink-0 items-center gap-1 rounded-lg px-2 py-1 text-xs font-medium text-muted transition-colors hover:bg-overlay-hover hover:text-fg"
title="切换主题"
>
<svg
width="13"
height="13"
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
>
<circle cx="8" cy="8" r="3" />
<path d="M8 1v2M8 13v2M2 8h2M12 8h2M3.5 3.5l1.5 1.5M11 11l1.5 1.5M3.5 12.5l1.5-1.5M11 5l1.5-1.5" />
</svg>
<span id="theme-label-admin">{/* 客户端渲染时显示当前主题 */}</span>
</button>
</header>
<main className="flex min-h-0 flex-1 flex-col overflow-hidden">
{children}
</main>
</div>
);
}