diff --git a/frontend-web/src/assets/icon/down-logo.svg b/frontend-web/src/assets/icon/down-logo.svg new file mode 100644 index 00000000..19343061 --- /dev/null +++ b/frontend-web/src/assets/icon/down-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/icon/nav-active.svg b/frontend-web/src/assets/icon/nav-active.svg new file mode 100644 index 00000000..776703e6 --- /dev/null +++ b/frontend-web/src/assets/icon/nav-active.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/icon/nav-line-logo.svg b/frontend-web/src/assets/icon/nav-line-logo.svg new file mode 100644 index 00000000..0f31a8e2 --- /dev/null +++ b/frontend-web/src/assets/icon/nav-line-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/components/page-sdebar.css b/frontend-web/src/components/page-sdebar.css index 06ad158e..f4710c40 100644 --- a/frontend-web/src/components/page-sdebar.css +++ b/frontend-web/src/components/page-sdebar.css @@ -1,42 +1,322 @@ -.sidebar-contents{ - width:98px; - min-height:986px; - background:rgba(3, 38, 80, 0.8); - border:1px solid; - border-color:rgba(43, 143, 254, 0.6); - border-radius:4px; - backdrop-filter:blur(2px); +.sidebar-contnet { + position: fixed; + left: 0; + top: 4rem; + z-index: 40; + width: 98px; + height: calc(100vh - 4rem); + flex-shrink: 0; +} + +.sidebar-contents { + width: 98px; + min-height: calc(100% - 20px); + background: rgba(3, 38, 80, 0.8); + border: 1px solid; + border-color: rgba(43, 143, 254, 0.6); + border-radius: 4px; + backdrop-filter: blur(2px); display: flex; flex-direction: column; align-items: center; padding: 12px 10px; + position: relative; + height: calc(100% - 20px); + box-sizing: border-box; + transform: translate(10px, 10px); } -.item-nav{ - width:100%; - padding:14px 0; - border-bottom:1px solid rgba(85, 166, 254, 0.2); - font-family:'Noto Sans S Chinese', sans-serif; - font-weight:500; - color:rgba(255, 255, 255, 0.8); - font-size:20px; - line-height:26px; - text-align:center; - cursor:pointer; - transition:background 0.2s ease, color 0.2s ease; +.item-nav-group { + width: 100%; } -.item-nav:hover{ - background:rgba(85, 166, 254, 0.12); - color:#ffffff; +.item-nav-wrap { + position: relative; + width: 100%; } -.item-nav__label{ - display:block; - width:3em; - margin:0 auto; - padding-left:6px; - box-sizing:border-box; - letter-spacing:6px; - word-break:break-all; +.item-nav-group:first-child .item-nav { + margin-top: 0; +} + +.item-nav { + width: 100%; + height: 75px; + margin-top: 13px; + font-family: Noto Sans S Chinese; + font-weight: 500; + color: #fff; + font-size: 20px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + cursor: pointer; + background: #000c1c26; + border: 1px solid; + border-color: #55a6fe33; + box-shadow: 0 0 20px #00b9ff26 inset; + position: relative; +} + +.item-nav:hover, +.item-nav-active, +.item-nav-hovered { + color: #ffffff; + background: linear-gradient(134.25deg, + rgba(0, 98, 217, 0.59) 2.28%, + rgba(0, 98, 217, 0.3) 99.59%) !important; + background-size: cover; + border: 1px solid; + border-color: rgba(255, 255, 255, 0.2); + position: relative; +} + +.item-nav:hover::before, +.item-nav-active::before, +.item-nav-hovered::before { + content: ''; + position: absolute; + right: -10px; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-left: 10px solid rgba(0, 98, 217, 0.45); + pointer-events: none; +} + +.item-nav:hover::after, +.item-nav-active::after, +.item-nav-hovered::after { + content: ''; + position: absolute; + left: 0; + top: 29px; + width: 77px; + height: 45px; + background: url('../assets/icon/nav-line-logo.svg') no-repeat; + background-size: cover; +} + +.item-nav__label { + display: block; + width: 3em; + margin: 0 auto; + padding-left: 6px; + box-sizing: border-box; + letter-spacing: 6px; + word-break: break-all; +} + +.item-nav-content { + width: 155px; + height: calc(100vh - 4rem - 20px); + min-height: calc(100vh - 4rem - 20px); + background: rgba(3, 38, 80, 0.8); + border: 1px solid; + border-color: rgba(43, 143, 254, 0.6); + border-radius: 4px; + backdrop-filter: blur(2px); + position: absolute; + left: 108px; + top: 0; + z-index: 1000; + padding: 14px 13px 18px 16px; + overflow-y: auto; + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.2s ease, visibility 0.2s ease; +} + +.item-nav-content::before { + content: ''; + position: absolute; + left: -30px; + top: 0; + width: 30px; + height: 100%; +} + +.item-nav-hover-bridge { + position: absolute; + right: -30px; + top: 0; + width: 30px; + height: 100%; + z-index: 999; +} + +.item-nav-content--visible { + opacity: 1; + visibility: visible; + pointer-events: auto; +} + +.sidebar-flyout-list { + display: flex; + flex-direction: column; + gap: 2px; + position: relative; +} + +.sidebar-flyout-list--level-1, +.sidebar-flyout-list--level-2, +.sidebar-flyout-list--level-3 { + margin-left: 19px; + padding-left: 14px; +} + +.sidebar-flyout-node { + position: relative; +} + +.sidebar-flyout-node--has-children.sidebar-flyout-node--expanded>.sidebar-flyout-list::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 1px; + height: 100%; + background: rgba(43, 143, 254, 0.22); +} + +.sidebar-flyout-item { + width: 100%; + min-height: 34px; + display: flex; + align-items: center; + gap: 9px; + border: 0; + background: transparent; + padding: 3px 0; + font-family: Noto Sans S Chinese; + font-size: 18px; + line-height: 16px; + font-weight: 500; + letter-spacing: 0; + color: rgba(255, 255, 255, 0.8); + text-align: left; + cursor: pointer; + transition: color 0.2s ease, text-shadow 0.2s ease; +} + +.sidebar-flyout-item:hover, +.sidebar-flyout-item--active { + color: #48ddff; + text-shadow: 0 0 8px rgba(72, 221, 255, 0.25); +} + +.sidebar-flyout-item--leaf { + justify-content: center; + padding-left: 0; +} + +.sidebar-flyout-list--level-1>.sidebar-flyout-node>.sidebar-flyout-item--leaf, +.sidebar-flyout-list--level-2>.sidebar-flyout-node>.sidebar-flyout-item--leaf, +.sidebar-flyout-list--level-3>.sidebar-flyout-node>.sidebar-flyout-item--leaf { + padding-left: 0; +} + +.sidebar-flyout-arrow { + width: 9px; + height: 9px; + flex: 0 0 9px; + background: url('../assets/icon/down-logo.svg') center / contain no-repeat; + transform: rotate(-90deg); + transition: transform 0.2s ease; +} + +.sidebar-flyout-node--expanded>.sidebar-flyout-item>.sidebar-flyout-arrow { + transform: rotate(0deg); +} + +.sidebar-flyout-label { + flex: 1; + white-space: nowrap; +} + +.sidebar-collapse-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 8px; + cursor: pointer; +} + +.sidebar-collapse-btn__icon { + width: 16px; + height: 16px; + flex: 0 0 16px; + color: #80e9ff; +} + +.sidebar-collapse-btn__label { + font-size: 14px; + line-height: 1; + white-space: nowrap; +} + +.sidebar-collapse-btn--inline { + width: calc(100% - 6px); + min-height: 58px; + margin-top: auto; + margin-bottom: 2px; + flex-direction: column; + gap: 6px; + border: 1px solid #55a6fe33; + background: #000c1c26; + color: rgba(255, 255, 255, 0.9); + box-shadow: inset 0 0 16px #00b9ff1f; + transition: background-color 0.2s ease, border-color 0.2s ease; +} + +.sidebar-collapse-btn--inline .sidebar-collapse-btn__label { + font-size: 16px; + line-height: 1.1; +} + +.sidebar-collapse-btn--inline:hover { + background: rgba(0, 98, 217, 0.3); + border-color: rgba(255, 255, 255, 0.25); +} + +.sidebar-collapse-btn--floating { + position: fixed; + left: 10px; + bottom: 10px; + z-index: 60; + width: 36px; + height: 36px; + min-height: 36px; + padding: 0 10px; + border: 1px solid rgba(128, 233, 255, 0.35); + background: rgba(3, 38, 80, 0.82); + color: rgba(205, 245, 255, 0.95); + backdrop-filter: blur(2px); + transition: width 0.22s ease, opacity 0.2s ease, background-color 0.2s ease, border-color 0.2s ease; + overflow: hidden; +} + +.sidebar-collapse-btn--floating:hover { + width: 104px; + background: rgba(7, 52, 110, 0.9); + border-color: rgba(128, 233, 255, 0.65); +} + +.sidebar-collapse-btn--floating .sidebar-collapse-btn__label { + max-width: 0; + opacity: 0; + transform: translateX(-4px); + transition: max-width 0.22s ease, opacity 0.16s ease, transform 0.22s ease; +} + +.sidebar-collapse-btn--floating:hover .sidebar-collapse-btn__label { + max-width: 64px; + opacity: 1; + transform: translateX(0); } \ No newline at end of file diff --git a/frontend-web/src/components/page-sidebar-dark.tsx b/frontend-web/src/components/page-sidebar-dark.tsx index cf24537d..96158764 100644 --- a/frontend-web/src/components/page-sidebar-dark.tsx +++ b/frontend-web/src/components/page-sidebar-dark.tsx @@ -1,311 +1,91 @@ -import { useEffect, useState } from "react"; -import type { ElementType } from "react"; +import { useEffect, useMemo, useRef, useState } 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 { PanelLeftOpen, PanelLeftClose } 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 { + defaultExpandedIds, + filterMenuItemsByRole, + sidebarMenuItems, + type MenuItem, +} from "@/core/page-layout/sidebar-menu"; import './page-sdebar.css' -type MenuItem = { - id: string; - label: string; - icon: ElementType; - path?: string; - adminOnly?: boolean; - children?: MenuItem[]; -}; - type PageSidebarDarkProps = { + /** 是否收起(true 时只渲染左下角浮动入口,sidebar 主体隐藏) */ 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"; +export function PageSidebarDark({ collapsed, onToggleCollapsed }: PageSidebarDarkProps) { const isAdmin = getStoredAuth()?.system_role === "admin"; - const visibleMenuItems = filterMenuItemsByRole(menuItems, !!isAdmin); - const { t } = useI18n(); - const { data: unreadCount = 0 } = useUnreadCount(); - const [expandedIds, setExpandedIds] = useState>(defaultExpandedIds); - const [settingsOpen, setSettingsOpen] = useState(false); - const [settingsDefaultSection, setSettingsDefaultSection] = - useState("appearance"); - const [notificationOpen, setNotificationOpen] = useState(false); - const [mounted, setMounted] = useState(false); + const visibleMenuItems = useMemo( + () => filterMenuItemsByRole(sidebarMenuItems, !!isAdmin), + [isAdmin], + ); + // defaultExpandedIds 是共享的 ReadonlySet,复制一份给 useState(避免外部突变) + const [expandedIds, setExpandedIds] = useState>(() => new Set(defaultExpandedIds)); const navigate = useNavigate(); const { pathname } = useLocation(); + // pathname 变化时取消任何未触发的 hover 关闭计时(cleanup 在卸载时也跑) useEffect(() => { - setMounted(true); + return () => { + if (hoverCloseTimerRef.current !== null) { + window.clearTimeout(hoverCloseTimerRef.current); + } + }; }, []); - const openSettings = (section: SettingsSection) => { - setSettingsDefaultSection(section); - setSettingsOpen(true); + const handleNavHoverEnter = (id: string) => { + if (hoverCloseTimerRef.current !== null) { + window.clearTimeout(hoverCloseTimerRef.current); + hoverCloseTimerRef.current = null; + } + setHoveredTopId(id); }; + const scheduleNavPanelClose = () => { + if (hoverCloseTimerRef.current !== null) { + window.clearTimeout(hoverCloseTimerRef.current); + } + hoverCloseTimerRef.current = window.setTimeout(() => { + setHoveredTopId(null); + hoverCloseTimerRef.current = null; + }, 180); + }; + + const closeNavPanel = () => { + if (hoverCloseTimerRef.current !== null) { + window.clearTimeout(hoverCloseTimerRef.current); + hoverCloseTimerRef.current = null; + } + setHoveredTopId(null); + }; + + const hasActiveChild = (item: MenuItem): boolean => { + if (item.path && pathname === item.path) { + return true; + } + + return item.children?.some(hasActiveChild) ?? false; + }; + + // lazy init:仅在首次 render 计算一次 active id;之后由下面的 useEffect 跟随 pathname 同步 + const [activeTopId, setActiveTopId] = useState( + () => visibleMenuItems.find(hasActiveChild)?.id ?? visibleMenuItems[0]?.id ?? "qa-center", + ); + const [hoveredTopId, setHoveredTopId] = useState(null); + const hoverCloseTimerRef = useRef(null); + + useEffect(() => { + const matchedTopItem = visibleMenuItems.find(hasActiveChild); + if (matchedTopItem) { + setActiveTopId(matchedTopItem.id); + } + closeNavPanel(); + }, [pathname, visibleMenuItems]); + const toggleExpanded = (id: string) => { setExpandedIds((prev) => { const next = new Set(prev); @@ -318,27 +98,140 @@ export function PageSidebarDark({ }); }; + const findFirstRoutePath = (items?: MenuItem[]): string | null => { + if (!items?.length) { + return null; + } + + for (const child of items) { + if (child.path) { + return child.path; + } + const nestedPath = findFirstRoutePath(child.children); + if (nestedPath) { + return nestedPath; + } + } + + return null; + }; + + const handleTopItemClick = (item: MenuItem) => { + setActiveTopId(item.id); + + if (item.children?.length) { + const firstPath = findFirstRoutePath(item.children); + if (firstPath) { + closeNavPanel(); + navigate(firstPath); + } + return; + } + + if (item.path) { + closeNavPanel(); + navigate(item.path); + } + }; + + const handleMenuItemClick = (item: MenuItem) => { + if (item.children?.length) { + toggleExpanded(item.id); + return; + } + + if (item.path) { + closeNavPanel(); + navigate(item.path); + } + }; + + const renderFlyoutMenu = (items: MenuItem[], level = 0) => ( +
+ {items.map((item) => { + const hasChildren = !!item.children?.length; + const isExpanded = expandedIds.has(item.id); + const isSelected = item.path ? pathname === item.path : hasActiveChild(item); + + return ( +
+ + {hasChildren && isExpanded ? renderFlyoutMenu(item.children ?? [], level + 1) : null} +
+ ); + })} +
+ ); + // collapsed 状态:sidebar 主体完全消失(包括占据的宽度),只渲染左下角浮动入口 + if (collapsed) { + return ( + + ); + } + return ( - <> - - - {/* TODO: 在这里编写深色主题的侧边栏 HTML 结构 */} -
-
- {visibleMenuItems.map((item) => ( -
- {item.label} +
- + ); } diff --git a/frontend-web/src/components/page-sidebar.tsx b/frontend-web/src/components/page-sidebar.tsx index 11dec345..10512957 100644 --- a/frontend-web/src/components/page-sidebar.tsx +++ b/frontend-web/src/components/page-sidebar.tsx @@ -1,40 +1,14 @@ 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"; @@ -50,14 +24,12 @@ 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[]; -}; +import { + defaultExpandedIds, + filterMenuItemsByRole, + sidebarMenuItems, + type MenuItem, +} from "@/core/page-layout/sidebar-menu"; type PageSidebarProps = { collapsed: boolean; @@ -66,272 +38,6 @@ type PageSidebarProps = { 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, @@ -339,10 +45,11 @@ export function PageSidebar({ const { theme } = useTheme(); const isDark = theme === "dark"; const isAdmin = getStoredAuth()?.system_role === "admin"; - const visibleMenuItems = filterMenuItemsByRole(menuItems, !!isAdmin); + const visibleMenuItems = filterMenuItemsByRole(sidebarMenuItems, !!isAdmin); const { t } = useI18n(); const { data: unreadCount = 0 } = useUnreadCount(); - const [expandedIds, setExpandedIds] = useState>(defaultExpandedIds); + // defaultExpandedIds 是共享的 ReadonlySet,复制一份给 useState(避免外部突变) + const [expandedIds, setExpandedIds] = useState>(() => new Set(defaultExpandedIds)); const [settingsOpen, setSettingsOpen] = useState(false); const [settingsDefaultSection, setSettingsDefaultSection] = useState("appearance"); diff --git a/frontend-web/src/components/ui/card.tsx b/frontend-web/src/components/ui/card.tsx index eb693c16..8a613603 100644 --- a/frontend-web/src/components/ui/card.tsx +++ b/frontend-web/src/components/ui/card.tsx @@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
) { return (
); @@ -65,7 +65,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) { return (
); diff --git a/frontend-web/src/components/ui/input-group.tsx b/frontend-web/src/components/ui/input-group.tsx index bb535dbb..ec7272b4 100644 --- a/frontend-web/src/components/ui/input-group.tsx +++ b/frontend-web/src/components/ui/input-group.tsx @@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) { data-slot="input-group" role="group" className={cn( - "group/input-group border-input/50 dark:bg-background/80 relative flex w-full items-center rounded-md border bg-white/80 shadow-xs transition-[color,box-shadow] outline-none", + "group/input-group relative flex w-full items-center rounded-md border border-input/50 bg-white/80 shadow-xs transition-[color,box-shadow] outline-none dark:border-[#1B477E] dark:bg-[#022248]", "h-9 min-w-0 has-[>textarea]:h-auto", // Variants based on alignment. diff --git a/frontend-web/src/components/ui/input.tsx b/frontend-web/src/components/ui/input.tsx index 481e6788..6537bb0f 100644 --- a/frontend-web/src/components/ui/input.tsx +++ b/frontend-web/src/components/ui/input.tsx @@ -8,9 +8,12 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) { type={type} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", - "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", - "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] md:text-sm", + "border-input placeholder:text-muted-foreground", + "dark:border-[#1B477E] dark:bg-[#022248] dark:text-[#AACEFF] dark:placeholder:text-[#566B7F] rounded-[4px] text-[16px]", + "file:text-foreground selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", + "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:focus-visible:border-[#3b82f6] dark:focus-visible:ring-[#3b82f6]/30", + "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", className, )} {...props} diff --git a/frontend-web/src/components/ui/tabs.tsx b/frontend-web/src/components/ui/tabs.tsx index 41a53cc7..9bbb70e2 100644 --- a/frontend-web/src/components/ui/tabs.tsx +++ b/frontend-web/src/components/ui/tabs.tsx @@ -30,7 +30,8 @@ const tabsListVariants = cva( { variants: { variant: { - default: "bg-muted", + default: + "bg-muted dark:group-data-[orientation=horizontal]/tabs:h-8! dark:items-stretch dark:gap-0 dark:overflow-hidden dark:rounded-[2px] dark:border dark:border-[#55A6FE] dark:bg-[#022248] dark:p-0", line: "gap-1 bg-transparent", }, }, @@ -40,60 +41,65 @@ const tabsListVariants = cva( }, ); -const TabsList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant = "default", ...props }, ref) => { - return ( - - ); -}); - -TabsList.displayName = TabsPrimitive.List.displayName; +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant = "default", ...props }, ref) => { + return ( + + ); +}); -const TabsTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - return ( - - ); -}); - -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; +TabsList.displayName = TabsPrimitive.List.displayName; -const TabsContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { - return ( - - ); -}); - -TabsContent.displayName = TabsPrimitive.Content.displayName; +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); + +TabsContent.displayName = TabsPrimitive.Content.displayName; export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }; diff --git a/frontend-web/src/components/ui/textarea.tsx b/frontend-web/src/components/ui/textarea.tsx index d55b5a4b..5bdc6ef7 100644 --- a/frontend-web/src/components/ui/textarea.tsx +++ b/frontend-web/src/components/ui/textarea.tsx @@ -7,7 +7,9 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {