初始化提交

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

+351
View File
@@ -0,0 +1,351 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { LogoButton } from "@/components/easter-egg/LogoButton";
import {
AdminIcon,
ArchiveIcon,
ChatIcon,
CharacterIcon,
CosmosIcon,
DriveIcon,
EvolutionIcon,
MemoryIcon,
ModelIcon,
PersonalityIcon,
RunIcon,
SettingsIcon,
TimeIcon,
ToolsIcon,
WorldIcon,
WuxingIcon,
} from "@/components/ui/Icons";
import { Badge } from "@/components/ui/Badge";
import { FEATURE_FLAGS } from "@/lib/config";
import { useAuthStore } from "@/lib/stores/auth-store";
import {
NEXT_THEME_LABEL,
useSettingsStore,
} from "@/lib/stores/settings-store";
import { cn } from "@/lib/utils/cn";
import type { ReactNode } from "react";
interface NavItem {
href: string;
label: string;
icon: ReactNode;
enabled?: boolean;
}
interface NavGroup {
title: string;
items: NavItem[];
}
/**
* 主导航分组:灵台(对话/轨迹)、法身(人物/人格/演化)、
* 内景(内驱力/世界/时态)、藏识(记忆/归档)、百工(工具/模型/设置)。
* 每组不超过 4 项,符合 product-ui §1 导航 IA ≤8 项的建议;
* 分组后侧栏视觉有序,降低认知负荷。
*/
const NAV_GROUPS: NavGroup[] = [
{
title: "灵台",
items: [
{ href: "/chat", label: "对话", icon: <ChatIcon /> },
{ href: "/runs", label: "轨迹", icon: <RunIcon /> },
],
},
{
title: "法身",
items: [
{ href: "/cosmos", label: "灵枢", icon: <CosmosIcon /> },
{ href: "/wuxing", label: "五行", icon: <WuxingIcon /> },
{ href: "/characters", label: "人物", icon: <CharacterIcon /> },
{ href: "/personality", label: "人格", icon: <PersonalityIcon /> },
{ href: "/evolution", label: "演化", icon: <EvolutionIcon /> },
],
},
{
title: "内景",
items: [
{
href: "/drive",
label: "内驱力",
icon: <DriveIcon />,
enabled: FEATURE_FLAGS.drivePanel,
},
{
href: "/world",
label: "世界",
icon: <WorldIcon />,
enabled: FEATURE_FLAGS.worldPanel,
},
{
href: "/time",
label: "时态",
icon: <TimeIcon />,
enabled: FEATURE_FLAGS.timePanel,
},
],
},
{
title: "藏识",
items: [
{ href: "/memory", label: "记忆", icon: <MemoryIcon /> },
{ href: "/archive", label: "归档", icon: <ArchiveIcon /> },
],
},
{
title: "百工",
items: [
{ href: "/tools", label: "工具", icon: <ToolsIcon /> },
{ href: "/models", label: "模型网关", icon: <ModelIcon /> },
{ href: "/settings", label: "设置", icon: <SettingsIcon /> },
],
},
];
export function Sidebar({
expanded = true,
onToggleFold,
onNavigate,
}: {
expanded?: boolean;
onToggleFold?: () => void;
onNavigate?: () => void;
}) {
const pathname = usePathname();
const router = useRouter();
const theme = useSettingsStore((s) => s.theme);
const toggleTheme = useSettingsStore((s) => s.toggleTheme);
const user = useAuthStore((s) => s.user);
const logout = useAuthStore((s) => s.logout);
const handleLogout = () => {
logout();
router.push("/login");
};
return (
<div
className={cn(
"flex h-full w-full flex-col pt-3 transition-[padding] duration-200",
expanded ? "px-3" : "px-0",
)}
>
{/* 品牌 + 折叠钮:窄态居中、宽态两端对齐(设计稿 sidebar-topbar 同源) */}
<div
className={cn(
"flex items-center gap-1 py-2",
expanded ? "justify-between px-1" : "justify-center px-0",
)}
>
<LogoButton compact={!expanded} />
{onToggleFold && (
<button
type="button"
onClick={onToggleFold}
aria-label={expanded ? "收起侧栏" : "展开侧栏"}
title={expanded ? "收起侧栏" : "展开侧栏"}
className="flex h-11 w-11 items-center justify-center rounded-lg text-muted transition-colors hover:bg-overlay-hover hover:text-fg"
>
<FoldIcon />
</button>
)}
</div>
{/* 分组导航 */}
<nav className={cn("mt-3 flex flex-col", expanded ? "gap-4" : "gap-3")}>
{NAV_GROUPS.map((group) => {
const visible = group.items.filter((i) => i.enabled !== false);
if (visible.length === 0) return null;
return (
<div key={group.title}>
{expanded && (
<p className="mb-1 px-3 text-[10px] font-medium uppercase tracking-[0.15em] text-muted/60">
{group.title}
</p>
)}
<div
className={cn("flex flex-col", expanded ? "gap-0.5" : "gap-1")}
>
{visible.map((item) => {
const active =
pathname === item.href ||
pathname.startsWith(`${item.href}/`);
return (
<Link
key={item.href}
href={item.href}
onClick={onNavigate}
aria-current={active ? "page" : undefined}
title={expanded ? undefined : item.label}
className={cn(
"flex items-center rounded-[10px] font-medium transition-all duration-200",
expanded
? "gap-2.5 px-3 py-2 text-sm"
: "justify-center px-0 py-2",
active
? "bg-patina-wash text-patina"
: "text-muted hover:bg-overlay-hover hover:text-fg",
)}
>
<span className="shrink-0 opacity-70">{item.icon}</span>
{expanded && item.label}
</Link>
);
})}
</div>
</div>
);
})}
</nav>
{/* 底部:用户卡片常驻 + 管理入口 */}
<div className="mt-auto border-t border-border-glass pt-3 pb-1">
{user && (
<div
className={cn(
"mb-2 flex items-center rounded-xl",
expanded ? "gap-2.5 px-3 py-2" : "justify-center px-0 py-2",
)}
>
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-patina text-xs font-medium text-ink">
{user.username.charAt(0).toUpperCase()}
</div>
{expanded && (
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-fg">
{user.username}
</p>
<p className="truncate text-[11px] text-muted">
{user.is_admin ? "管理员" : user.email}
</p>
</div>
)}
</div>
)}
{user?.is_admin && FEATURE_FLAGS.adminPanel && (
<Link
href="/admin"
onClick={onNavigate}
aria-current={pathname.startsWith("/admin") ? "page" : undefined}
title={expanded ? undefined : "司命台"}
className={cn(
"flex items-center rounded-[10px] font-medium transition-all duration-200",
expanded
? "gap-2.5 px-3 py-2 text-sm"
: "justify-center px-0 py-2",
pathname.startsWith("/admin")
? "bg-patina-wash text-patina"
: "text-muted hover:bg-overlay-hover hover:text-fg",
)}
>
<span className="shrink-0 text-cinnabar opacity-90">
<AdminIcon />
</span>
{expanded && "司命台"}
{expanded && (
<Badge tone="accent" className="ml-auto text-[10px]">
管理
</Badge>
)}
</Link>
)}
<button
type="button"
onClick={toggleTheme}
title={expanded ? undefined : NEXT_THEME_LABEL[theme]}
className={cn(
"mt-1 flex items-center rounded-[10px] font-medium transition-all duration-200",
expanded
? "w-full gap-2.5 px-3 py-2 text-xs"
: "mx-auto justify-center px-0 py-2",
"text-muted hover:bg-overlay-hover hover:text-fg",
)}
>
<svg
width="14"
height="14"
viewBox="0 0 16 16"
fill="none"
aria-hidden
>
<circle
cx="8"
cy="8"
r="3"
stroke="currentColor"
strokeWidth="1.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"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
/>
</svg>
{expanded && NEXT_THEME_LABEL[theme]}
</button>
<button
type="button"
onClick={handleLogout}
title={expanded ? undefined : "退出登录"}
className={cn(
"mt-0.5 flex items-center rounded-[10px] font-medium transition-all duration-200",
expanded
? "w-full gap-2.5 px-3 py-2 text-xs"
: "mx-auto justify-center px-0 py-2",
"text-muted hover:bg-overlay-hover hover:text-cinnabar",
)}
>
<svg
width="14"
height="14"
viewBox="0 0 16 16"
fill="none"
aria-hidden
>
<path
d="M6 2H3a1 1 0 00-1 1v10a1 1 0 001 1h3M11 11l3-3-3-3M14 8H6"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
{expanded && "退出登录"}
</button>
</div>
</div>
);
}
/** 侧栏折叠钮图标:面板 + 左竖线(设计稿 sidebar-foldbtn 同源) */
function FoldIcon() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden>
<rect
x="3"
y="4"
width="18"
height="16"
rx="2"
stroke="currentColor"
strokeWidth="1.6"
/>
<path
d="M9 4v16"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
);
}