591 lines
18 KiB
TypeScript
591 lines
18 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import type { ElementType } from "react";
|
|
import { useNavigate, useLocation } from "react-router-dom";
|
|
import {
|
|
Bell,
|
|
Bot,
|
|
Brain,
|
|
CalendarClock,
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
ChevronsUpDown,
|
|
CircleHelp,
|
|
Database,
|
|
FileBox,
|
|
FileText,
|
|
Folder,
|
|
FolderOpen,
|
|
Gauge,
|
|
Grid3X3,
|
|
HelpCircle,
|
|
History,
|
|
Layout,
|
|
Library,
|
|
List,
|
|
ListChecks,
|
|
ListTodo,
|
|
MessageSquareCode,
|
|
MessageSquarePlus,
|
|
ShieldCheck,
|
|
Network,
|
|
NotebookPen,
|
|
Plus,
|
|
Settings,
|
|
Sparkles,
|
|
Target,
|
|
User,
|
|
Users,
|
|
Wrench,
|
|
} from "lucide-react";
|
|
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import { NotificationDialog } from "@/components/workspace/notification-dialog";
|
|
import { SettingsDialog } from "@/components/workspace/settings";
|
|
import { useTheme } from "@/contexts/ThemeContext";
|
|
import { getStoredAuth } from "@/core/auth";
|
|
import { useI18n } from "@/core/i18n/hooks";
|
|
import { useUnreadCount } from "@/core/notifications";
|
|
type MenuItem = {
|
|
id: string;
|
|
label: string;
|
|
icon: ElementType;
|
|
path?: string;
|
|
adminOnly?: boolean;
|
|
children?: MenuItem[];
|
|
};
|
|
|
|
type PageSidebarProps = {
|
|
collapsed: boolean;
|
|
onToggleCollapsed: () => void;
|
|
};
|
|
|
|
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about";
|
|
|
|
const menuItems: MenuItem[] = [
|
|
{
|
|
id: "qa-center",
|
|
label: "问答中心",
|
|
icon: HelpCircle,
|
|
children: [
|
|
{ id: "general-qa", label: "通用问答", icon: CircleHelp, path: "/page/workspace/chats/new" },
|
|
{ id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" },
|
|
// 「圆桌规划」入口暂时隐藏 —— 功能仍在 /page/roundtable/planning,只是不在侧边栏出现。
|
|
// 需要重新开放时取消注释即可。
|
|
// {
|
|
// id: "qa-roundtable",
|
|
// label: "圆桌规划",
|
|
// icon: Bot,
|
|
// path: "/page/roundtable/planning",
|
|
// },
|
|
{ id: "qa-zzjc", label: "zzjc支持", icon: Bot },
|
|
{ id: "qa-zzxd", label: "zzXD应用", icon: Layout },
|
|
{ id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" },
|
|
],
|
|
},
|
|
{
|
|
id: "light-app-center",
|
|
label: "轻应用",
|
|
icon: Grid3X3,
|
|
children: [
|
|
{
|
|
id: "light-app-note-management",
|
|
label: "笔记管理",
|
|
icon: NotebookPen,
|
|
children: [
|
|
{
|
|
id: "light-app-new-note",
|
|
label: "新建笔记",
|
|
icon: Plus,
|
|
path: "/page/strategy/light-app/new-note",
|
|
},
|
|
{
|
|
id: "light-app-note-list",
|
|
label: "笔记列表",
|
|
icon: List,
|
|
path: "/page/strategy/light-app/notes",
|
|
},
|
|
{
|
|
id: "light-app-note-qa",
|
|
label: "笔记问答",
|
|
icon: MessageSquareCode,
|
|
path: "/page/strategy/light-app/note-qa",
|
|
},
|
|
],
|
|
},
|
|
{ id: "light-app-square", label: "应用广场", icon: Layout },
|
|
{ id: "light-app-records", label: "运行记录", icon: History },
|
|
],
|
|
},
|
|
{
|
|
id: "task-management",
|
|
label: "任务管理",
|
|
icon: Target,
|
|
children: [
|
|
{
|
|
id: "xk-center-task",
|
|
label: "XK中任务",
|
|
icon: Folder,
|
|
children: [
|
|
{
|
|
id: "xk-task-jc",
|
|
label: "任务JC",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/xk-task-jc",
|
|
},
|
|
{
|
|
id: "xk-task-dispatch",
|
|
label: "任务调度",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/xk-task-dispatch",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "coh-center-task",
|
|
label: "COH中任务",
|
|
icon: Folder,
|
|
children: [
|
|
{
|
|
id: "coh-task-3qfx",
|
|
label: "3QFX",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/plan-task-sentiment",
|
|
},
|
|
{
|
|
id: "coh-task-fx",
|
|
label: "任务FX",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/plan-task/plan-task-fx",
|
|
},
|
|
{
|
|
id: "coh-task-xdfx",
|
|
label: "XDFX",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/plan-task/plan-task-action",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "pg-center-task",
|
|
label: "PG中任务",
|
|
icon: Folder,
|
|
children: [
|
|
{
|
|
id: "pg-task-pg",
|
|
label: "任务PG",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/effect-task-pg",
|
|
},
|
|
{
|
|
id: "pg-task-xdpg",
|
|
label: "XDPG",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/effect-action-pg",
|
|
},
|
|
{
|
|
id: "pg-task-info-pg",
|
|
label: "信息PG",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/effect-info-pg",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "ty-center-task",
|
|
label: "TY中任务",
|
|
icon: Folder,
|
|
children: [
|
|
{
|
|
id: "ty-task-xdty",
|
|
label: "XDTY任务",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/mnty-task-mechanism",
|
|
},
|
|
{
|
|
id: "ty-task-plan",
|
|
label: "方案TY任务",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/mnty-task-plan",
|
|
},
|
|
{
|
|
id: "ty-task-xxty",
|
|
label: "XXTY任务",
|
|
icon: ListTodo,
|
|
path: "/page/strategy/task-management/mnty-task-zsty",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "comprehensive-management",
|
|
label: "综合管理",
|
|
icon: Settings,
|
|
children: [
|
|
{ id: "mgmt-agents", label: "智能体管理", icon: Bot, path: "/page/strategy/agents" },
|
|
{ id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" },
|
|
{ id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" },
|
|
{
|
|
id: "mgmt-tasks",
|
|
label: "定时任务",
|
|
icon: ListChecks,
|
|
path: "/page/strategy/scheduled/tasks",
|
|
},
|
|
{
|
|
id: "mgmt-recommended-questions",
|
|
label: "推荐问题配置",
|
|
icon: MessageSquarePlus,
|
|
path: "/page/strategy/settings/recommended-questions",
|
|
adminOnly: true,
|
|
},
|
|
{
|
|
id: "mgmt-prompt-prefix",
|
|
label: "问答前缀配置",
|
|
icon: MessageSquareCode,
|
|
path: "/page/strategy/settings/prompt-prefix",
|
|
adminOnly: true,
|
|
},
|
|
{
|
|
id: "mgmt-private-square",
|
|
label: "私密广场配置",
|
|
icon: ShieldCheck,
|
|
path: "/page/strategy/settings/private-square",
|
|
adminOnly: true,
|
|
},
|
|
{
|
|
id: "mgmt-llm-metrics",
|
|
label: "大模型调用统计",
|
|
icon: Gauge,
|
|
path: "/page/strategy/settings/llm-metrics",
|
|
adminOnly: true,
|
|
},
|
|
{
|
|
id: "mgmt-users",
|
|
label: "用户管理",
|
|
icon: Users,
|
|
path: "/page/strategy/admin/users",
|
|
adminOnly: true,
|
|
},
|
|
{
|
|
id: "mgmt-admin-scheduled-tasks",
|
|
label: "定时任务管理",
|
|
icon: CalendarClock,
|
|
path: "/page/strategy/admin/scheduled-tasks",
|
|
adminOnly: true,
|
|
},
|
|
{
|
|
id: "mgmt-user-activity",
|
|
label: "用户活动审计",
|
|
icon: History,
|
|
path: "/page/strategy/admin/user-activity",
|
|
adminOnly: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "resource-management",
|
|
label: "资源管理",
|
|
icon: Database,
|
|
children: [
|
|
{
|
|
id: "resource-network-xwt",
|
|
label: "网络xwt资源管理",
|
|
icon: Network,
|
|
path: "/page/strategy/resource-management/resource-network-actor",
|
|
},
|
|
{
|
|
id: "resource-knowledge-base",
|
|
label: "知识库资源管理",
|
|
icon: Library,
|
|
path: "/page/strategy/resource-management/resource-knowledge-base",
|
|
},
|
|
{
|
|
id: "resource-evaluation-index",
|
|
label: "作战资源管理",
|
|
icon: FileBox,
|
|
path: "/page/strategy/resource-management/resource-evaluation-index",
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const defaultExpandedIds = new Set([
|
|
"qa-center",
|
|
"light-app-center",
|
|
"light-app-note-management",
|
|
"task-management",
|
|
"xk-center-task",
|
|
]);
|
|
|
|
function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] {
|
|
return items
|
|
.filter((item) => !item.adminOnly || isAdmin)
|
|
.map((item) =>
|
|
item.children
|
|
? { ...item, children: filterMenuItemsByRole(item.children, isAdmin) }
|
|
: item,
|
|
);
|
|
}
|
|
|
|
export function PageSidebar({
|
|
collapsed,
|
|
onToggleCollapsed,
|
|
}: PageSidebarProps) {
|
|
const { theme } = useTheme();
|
|
const isDark = theme === "dark";
|
|
const isAdmin = getStoredAuth()?.system_role === "admin";
|
|
const visibleMenuItems = filterMenuItemsByRole(menuItems, !!isAdmin);
|
|
const { t } = useI18n();
|
|
const { data: unreadCount = 0 } = useUnreadCount();
|
|
const [expandedIds, setExpandedIds] = useState<Set<string>>(defaultExpandedIds);
|
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
const [settingsDefaultSection, setSettingsDefaultSection] =
|
|
useState<SettingsSection>("appearance");
|
|
const [notificationOpen, setNotificationOpen] = useState(false);
|
|
const [mounted, setMounted] = useState(false);
|
|
const navigate = useNavigate();
|
|
const { pathname } = useLocation();
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
}, []);
|
|
|
|
const openSettings = (section: SettingsSection) => {
|
|
setSettingsDefaultSection(section);
|
|
setSettingsOpen(true);
|
|
};
|
|
|
|
const iconButtonClass = isDark
|
|
? "rounded-lg p-2 text-slate-400 transition-all duration-300 ease-out hover:scale-105 hover:bg-slate-800/50 hover:text-white"
|
|
: "rounded-lg p-2 text-gray-500 transition-all duration-300 ease-out hover:scale-105 hover:bg-red-50 hover:text-red-600";
|
|
const panelClass = isDark
|
|
? "ml-4 space-y-1 border-l border-blue-800/30 pl-4"
|
|
: "ml-4 space-y-1 border-l border-red-100 pl-4";
|
|
const labelClass = (compact: boolean, small = false) =>
|
|
`overflow-hidden whitespace-nowrap transition-all duration-300 ease-out ${
|
|
compact ? "max-w-0 opacity-0" : "max-w-[220px] opacity-100"
|
|
} ${small ? "text-xs" : "text-sm"} flex-1 font-medium`;
|
|
const menuButtonClass = (isActive: boolean, depth = 0) =>
|
|
`flex w-full items-center rounded-lg py-2 text-left transition-all duration-300 ease-out hover:-translate-y-0.5 theme-transition ${
|
|
collapsed && depth === 0 ? "justify-center px-2" : "gap-2 px-3"
|
|
} ${
|
|
isActive
|
|
? isDark
|
|
? "bg-blue-500/15 text-cyan-300 font-semibold"
|
|
: "bg-red-50 text-red-600 font-semibold"
|
|
: isDark
|
|
? "text-slate-400 hover:bg-slate-800/50 hover:text-white"
|
|
: "text-gray-600 hover:bg-red-50 hover:text-red-600"
|
|
}`;
|
|
const footerButtonClass = menuButtonClass(false, 0);
|
|
|
|
const toggleExpanded = (id: string) => {
|
|
setExpandedIds((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) {
|
|
next.delete(id);
|
|
} else {
|
|
next.add(id);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const renderLeaf = (item: MenuItem, depth = 0) => {
|
|
const Icon = item.icon;
|
|
const isActive = !!item.path && pathname === item.path;
|
|
|
|
return (
|
|
<button
|
|
key={item.id}
|
|
type="button"
|
|
title={collapsed && depth === 0 ? item.label : undefined}
|
|
onClick={() => item.path && navigate(item.path)}
|
|
className={menuButtonClass(isActive, depth)}
|
|
>
|
|
<Icon className={`${depth === 0 ? "h-5 w-5" : "h-4 w-4"} flex-shrink-0 transition-transform duration-300 ease-out`} />
|
|
{(!collapsed || depth > 0) && (
|
|
<span className={labelClass(false, depth > 0)}>
|
|
{item.label}
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
};
|
|
|
|
const renderBranch = (item: MenuItem, depth = 0) => {
|
|
const expanded = expandedIds.has(item.id);
|
|
const isTopLevel = depth === 0;
|
|
const Icon = depth > 0 && item.icon === Folder && expanded ? FolderOpen : item.icon;
|
|
|
|
return (
|
|
<div key={item.id} className="space-y-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (collapsed && isTopLevel) {
|
|
onToggleCollapsed();
|
|
return;
|
|
}
|
|
toggleExpanded(item.id);
|
|
}}
|
|
title={collapsed && isTopLevel ? item.label : undefined}
|
|
className={menuButtonClass(false, depth)}
|
|
>
|
|
<Icon
|
|
className={`${isTopLevel ? "h-5 w-5" : "h-4 w-4"} flex-shrink-0 transition-transform duration-300 ease-out ${
|
|
expanded && depth > 0 ? "scale-105" : ""
|
|
}`}
|
|
/>
|
|
{(!collapsed || !isTopLevel) && (
|
|
<span className={labelClass(false, depth > 0)}>
|
|
{item.label}
|
|
</span>
|
|
)}
|
|
{(!collapsed || !isTopLevel) && (
|
|
<ChevronRight
|
|
className={`h-4 w-4 transition-transform duration-300 ease-out ${expanded ? "rotate-90" : ""}`}
|
|
/>
|
|
)}
|
|
</button>
|
|
|
|
{(!collapsed || !isTopLevel) && item.children && (
|
|
<div
|
|
className={`${panelClass} overflow-hidden transition-all duration-300 ease-out ${
|
|
expanded ? "max-h-[1200px] opacity-100" : "max-h-0 opacity-0"
|
|
}`}
|
|
>
|
|
{item.children.map((child) =>
|
|
child.children ? renderBranch(child, depth + 1) : renderLeaf(child, depth + 1),
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<SettingsDialog
|
|
open={settingsOpen}
|
|
onOpenChange={setSettingsOpen}
|
|
defaultSection={settingsDefaultSection}
|
|
/>
|
|
<NotificationDialog
|
|
open={notificationOpen}
|
|
onOpenChange={setNotificationOpen}
|
|
/>
|
|
<aside
|
|
className={`fixed left-0 top-16 z-40 flex h-[calc(100vh-4rem)] flex-col overflow-hidden shadow-sm theme-transition transition-all duration-300 ${
|
|
isDark
|
|
? "border-r border-blue-800/30 bg-slate-900"
|
|
: "border-r border-red-100 bg-white"
|
|
} ${collapsed ? "w-16" : "w-64"}`}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={onToggleCollapsed}
|
|
title={collapsed ? "展开侧边栏" : "收起侧边栏"}
|
|
className={`absolute -right-3 top-6 z-50 flex h-6 w-6 items-center justify-center rounded-full shadow-md theme-transition ${
|
|
isDark
|
|
? "bg-slate-700 text-slate-300 hover:bg-slate-600"
|
|
: "bg-white text-gray-500 hover:bg-red-50 hover:text-red-600 border border-red-100"
|
|
}`}
|
|
>
|
|
{collapsed ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
|
|
</button>
|
|
|
|
<nav className={`flex-1 overflow-y-auto ${collapsed ? "p-2" : "p-4"} space-y-2 transition-all duration-300 ease-out`}>
|
|
{visibleMenuItems.map((item) =>
|
|
item.children ? renderBranch(item) : renderLeaf(item),
|
|
)}
|
|
</nav>
|
|
|
|
<div
|
|
className={`mt-auto shrink-0 theme-transition ${
|
|
isDark ? "border-t border-blue-800/30" : "border-t border-red-100"
|
|
} ${collapsed ? "p-2" : "p-4"}`}
|
|
>
|
|
{mounted ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
title={collapsed ? t.workspace.settingsAndMore : undefined}
|
|
className={footerButtonClass}
|
|
>
|
|
<Settings className="h-5 w-5 flex-shrink-0" />
|
|
{!collapsed && (
|
|
<>
|
|
<span className={labelClass(false)}>
|
|
{t.workspace.settingsAndMore}
|
|
</span>
|
|
<ChevronsUpDown className="h-4 w-4 flex-shrink-0" />
|
|
</>
|
|
)}
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align={collapsed ? "start" : "end"}
|
|
side={collapsed ? "right" : "top"}
|
|
sideOffset={4}
|
|
className="min-w-56 rounded-lg"
|
|
>
|
|
<DropdownMenuItem onClick={() => setNotificationOpen(true)}>
|
|
<Bell className="size-4" />
|
|
{t.notifications.title}
|
|
{unreadCount > 0 && (
|
|
<span
|
|
className={`ml-auto flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium leading-none text-white ${
|
|
isDark ? "bg-cyan-500" : "bg-red-500"
|
|
}`}
|
|
>
|
|
{unreadCount > 99 ? "99+" : unreadCount}
|
|
</span>
|
|
)}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => openSettings("tools")}>
|
|
<Wrench className="size-4" />
|
|
{t.settings.sections.tools}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => openSettings("account")}>
|
|
<User className="size-4" />
|
|
个人信息
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className={`${footerButtonClass} pointer-events-none`}
|
|
>
|
|
<Settings className="h-5 w-5 flex-shrink-0" />
|
|
{!collapsed && (
|
|
<span className={labelClass(false)}>
|
|
{t.workspace.settingsAndMore}
|
|
</span>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div
|
|
className={`pointer-events-none absolute right-0 top-0 h-32 w-32 rounded-full blur-3xl theme-transition ${
|
|
isDark ? "bg-blue-500/10" : "bg-red-500/5"
|
|
}`}
|
|
/>
|
|
<div
|
|
className={`pointer-events-none absolute bottom-20 right-0 h-24 w-24 rounded-full blur-3xl theme-transition ${
|
|
isDark ? "bg-cyan-400/5" : "bg-red-400/5"
|
|
}`}
|
|
/>
|
|
</aside>
|
|
</>
|
|
);
|
|
}
|