初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user