- 缘契域 domains/yuanqi:covenants(当前版)+ covenant_events(不可变历史);提议—问心—铭契—撤回全链;边界变更立即生效;多维关系并存,服务层拒绝 affinity/好感度单一键;浪漫维度成人资格标记位 - 道体铸魂与分灵 domains/jingsheng/soul.py:soul_forgings 确认流(锚点变更 pending→confirmed 应用并签发新魂籍版本,确认前人物锚点不变);分灵创建新人物(继承资料与谱系 lineage 链、不继承原身份,魂籍从 v1 起) - 河图域 domains/hetu:persistent_worlds 境界 + world_entities/world_relations 境界作用域重建 + 世谱挂接;境界正史视图(scope=persistent_world 归真集合);canonize 后置推进正史计数 - 三界分流:reality→息壤提取(不变);persistent_world→境界实录+正史推进;simulation→只记试演实录;factual_records 增 scope 列——境界/幻境实录与真界本相隔离 - 正影打通:引文 target_type=memory 时被引记忆 superseded → 返回 successor_memory_id 提示,不静默替换 - 前端:人物详情页缘契面板(提议维度选择/铭契/撤回/事件史)+ 世界页重建(境界目录/实体/正史概览)+ covenants/worlds api 库 - 退役:旧 personality/drive/evolution/world 模块、服务、端点、仓储、schema、前端五页与组件样式;迁移 007 退役九张旧表与 characters.current_personality_version_id - 验收(test_m6_domains 六用例):多维无单一好感度/铭契版本链/事件不可抹除/撤回立即生效/边界即时/铸魂确认流+魂籍版本/分灵谱系/境界正史不进真界(实录 scope 隔离+真界候选=0)/引文正影后继 - 四套件:后端 379、前端 199、BFF 4、神识 5 包全绿(2 个预存环境失败不变);迁移 007 净库+±1 干净
326 lines
9.7 KiB
TypeScript
326 lines
9.7 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname, useRouter } from "next/navigation";
|
|
|
|
import { LogoButton } from "@/components/easter-egg/LogoButton";
|
|
import {
|
|
AdminIcon,
|
|
ArchiveIcon,
|
|
MemoryIcon,
|
|
ChatIcon,
|
|
CharacterIcon,
|
|
ModelIcon,
|
|
SettingsIcon,
|
|
ToolsIcon,
|
|
WorldIcon,
|
|
} 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[];
|
|
}
|
|
|
|
/**
|
|
* 主导航分组:灵台(镜树)、法身(灵胚/人物)、内景(世界)、
|
|
* 藏识(记忆/归档)、百工(工具/神识阁/设置)。
|
|
* 旧人格/内驱力/五行/灵枢/轨迹页随 M6 退役(人格=道体锚点+缘契,轨迹并入镜树)。
|
|
*/
|
|
const NAV_GROUPS: NavGroup[] = [
|
|
{
|
|
title: "灵台",
|
|
items: [{ href: "/trees", label: "镜树", icon: <ChatIcon /> }],
|
|
},
|
|
{
|
|
title: "法身",
|
|
items: [
|
|
{ href: "/embryos", label: "灵胚", icon: <CharacterIcon /> },
|
|
{ href: "/characters", label: "人物", icon: <CharacterIcon /> },
|
|
],
|
|
},
|
|
{
|
|
title: "内景",
|
|
items: [
|
|
{
|
|
href: "/world",
|
|
label: "世界",
|
|
icon: <WorldIcon />,
|
|
enabled: FEATURE_FLAGS.worldPanel,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
title: "藏识",
|
|
items: [
|
|
{ href: "/memories", 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>
|
|
);
|
|
}
|