421 lines
12 KiB
TypeScript
421 lines
12 KiB
TypeScript
import { useEffect, useMemo, 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,
|
|
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";
|
|
import './page-sdebar.css'
|
|
|
|
type MenuItem = {
|
|
id: string;
|
|
label: string;
|
|
icon: ElementType;
|
|
path?: string;
|
|
adminOnly?: boolean;
|
|
children?: MenuItem[];
|
|
};
|
|
|
|
type PageSidebarDarkProps = {
|
|
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" },
|
|
{ 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 },
|
|
{ id: "pg-task-xdpg", label: "XDPG", icon: ListTodo },
|
|
{ id: "pg-task-info-pg", label: "信息PG", icon: ListTodo },
|
|
],
|
|
},
|
|
{
|
|
id: "ty-center-task",
|
|
label: "TY中任务",
|
|
icon: Folder,
|
|
children: [
|
|
{ id: "ty-task-xdty", label: "XDTY任务", icon: ListTodo },
|
|
{ id: "ty-task-plan", label: "方案TY任务", icon: ListTodo },
|
|
{ id: "ty-task-xxty", label: "XXTY任务", icon: ListTodo },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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-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 PageSidebarDark({
|
|
collapsed,
|
|
onToggleCollapsed,
|
|
}: PageSidebarDarkProps) {
|
|
const { theme } = useTheme();
|
|
const isDark = theme === "dark";
|
|
const isAdmin = getStoredAuth()?.system_role === "admin";
|
|
const visibleMenuItems = useMemo(
|
|
() => filterMenuItemsByRole(menuItems, !!isAdmin),
|
|
[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 hasActiveChild = (item: MenuItem): boolean => {
|
|
if (item.path && pathname === item.path) {
|
|
return true;
|
|
}
|
|
|
|
return item.children?.some(hasActiveChild) ?? false;
|
|
};
|
|
|
|
const initialActiveTopItem = visibleMenuItems.find(hasActiveChild) ?? visibleMenuItems[0];
|
|
const [activeTopId, setActiveTopId] = useState(initialActiveTopItem?.id ?? "qa-center");
|
|
|
|
useEffect(() => {
|
|
const matchedTopItem = visibleMenuItems.find(hasActiveChild);
|
|
if (matchedTopItem) {
|
|
setActiveTopId(matchedTopItem.id);
|
|
}
|
|
}, [pathname, visibleMenuItems]);
|
|
|
|
const activeTopItem = visibleMenuItems.find((item) => item.id === activeTopId) ?? initialActiveTopItem;
|
|
|
|
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 handleTopItemClick = (item: MenuItem) => {
|
|
setActiveTopId(item.id);
|
|
|
|
if (!item.children?.length && item.path) {
|
|
navigate(item.path);
|
|
}
|
|
};
|
|
|
|
const handleMenuItemClick = (item: MenuItem) => {
|
|
if (item.children?.length) {
|
|
toggleExpanded(item.id);
|
|
return;
|
|
}
|
|
|
|
if (item.path) {
|
|
navigate(item.path);
|
|
}
|
|
};
|
|
|
|
const renderFlyoutMenu = (items: MenuItem[], level = 0) => (
|
|
<div className={`sidebar-flyout-list sidebar-flyout-list--level-${level}`}>
|
|
{items.map((item) => {
|
|
const hasChildren = !!item.children?.length;
|
|
const isExpanded = expandedIds.has(item.id);
|
|
const isSelected = item.path ? pathname === item.path : hasActiveChild(item);
|
|
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
className={`sidebar-flyout-node sidebar-flyout-node--level-${level} ${hasChildren ? "sidebar-flyout-node--has-children" : ""} ${isExpanded ? "sidebar-flyout-node--expanded" : ""}`}
|
|
>
|
|
<button
|
|
type="button"
|
|
className={`sidebar-flyout-item ${isSelected ? "sidebar-flyout-item--active" : ""} ${hasChildren ? "sidebar-flyout-item--parent" : "sidebar-flyout-item--leaf"}`}
|
|
onClick={() => handleMenuItemClick(item)}
|
|
>
|
|
{hasChildren ? <span className="sidebar-flyout-arrow" aria-hidden="true" /> : null}
|
|
<span className="sidebar-flyout-label">{item.label}</span>
|
|
</button>
|
|
{hasChildren && isExpanded ? renderFlyoutMenu(item.children ?? [], level + 1) : null}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
return (
|
|
<>
|
|
<SettingsDialog
|
|
open={settingsOpen}
|
|
onOpenChange={setSettingsOpen}
|
|
defaultSection={settingsDefaultSection}
|
|
/>
|
|
<NotificationDialog
|
|
open={notificationOpen}
|
|
onOpenChange={setNotificationOpen}
|
|
/>
|
|
{/* TODO: 在这里编写深色主题的侧边栏 HTML 结构 */}
|
|
<div className="sidebar-contnet">
|
|
<div className="sidebar-contents">
|
|
<div className="item-nav-content">
|
|
{activeTopItem?.children?.length ? renderFlyoutMenu(activeTopItem.children) : null}
|
|
</div>
|
|
{visibleMenuItems.map((item) => (
|
|
<button
|
|
type="button"
|
|
key={item.id}
|
|
className={`item-nav ${item.id === activeTopId ? "item-nav-active" : ""}`}
|
|
onClick={() => handleTopItemClick(item)}
|
|
>
|
|
<span className="item-nav__label">{item.label}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|