From ee6ab38c37098ffc7bf90a2c94ee543eecc152cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Mon, 15 Jun 2026 16:53:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(nav):=20=E6=B5=85=E8=89=B2=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=AF=BC=E8=88=AA=E9=87=8D=E6=9E=84=EF=BC=88=E9=A1=B6?= =?UTF-8?q?=E6=A0=8F=E4=B8=80=E7=BA=A7=20+=20=E4=BE=A7=E8=BE=B9=E6=A0=8F?= =?UTF-8?q?=E4=BA=8C=E7=BA=A7=E5=9B=BE=E6=A0=87=E5=8D=A1=20+=20=E4=B8=89?= =?UTF-8?q?=E7=BA=A7=E5=8F=B3=E4=BE=A7=E5=88=97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 浅色(红/蓝)主题改为与深色一致的导航逻辑,颜色经 --brand-* 变量自动切换: - 一级导航正常横排进顶栏(白字落在 banner 上,选中加粗 + 底部粗白线) - 二级导航为侧边栏图标卡片(图标主题色、文字 #222222、选中铺满列宽 + 右侧粗边) - 三级导航改到侧边栏二级右侧的「三级列」:激活二级项有三级时整列展开、 侧边栏 96→236px 整体变宽并带过渡动画,主内容左内边距同步平滑右移 - 顶栏三级横条改为仅深色用;浅色不支持侧边栏收起 - 任务管理二级项改用区分图标(ClipboardList/Activity/Gauge/Radar) - 字体统一用全局 --font-sans,修复雅黑兜底下加粗不生效 - 抽出 getActiveThirdLevelItems / useActiveThirdLevel 供侧边栏与 PageRoutes 共用 Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend-web/src/components/page-sdebar.css | 245 ++++++++++++++++++ frontend-web/src/components/page-sidebar.tsx | 203 ++++++++++++++- .../src/core/page-layout/sidebar-menu.ts | 37 ++- .../src/core/page-layout/use-sidebar-menu.ts | 14 + frontend-web/src/pages/PageRoutes.tsx | 15 +- .../src/strategy-components/Header.tsx | 40 +++ 6 files changed, 543 insertions(+), 11 deletions(-) diff --git a/frontend-web/src/components/page-sdebar.css b/frontend-web/src/components/page-sdebar.css index 7e22e50c..e6eb7677 100644 --- a/frontend-web/src/components/page-sdebar.css +++ b/frontend-web/src/components/page-sdebar.css @@ -498,3 +498,248 @@ background: linear-gradient(90deg, rgba(62, 178, 255, 0) 0%, #4ee6ff 50%, rgba(62, 178, 255, 0) 100%); box-shadow: 0 0 10px rgba(78, 230, 255, 0.72); } + +/* ══════════════════════════════════════════════════════════════════════ + 浅色主题导航(红/蓝两套,颜色经 --brand-* 变量自动切换) + 逻辑同深色:一级导航在顶栏、二级导航在侧边栏、三级导航在顶栏下方横条。 + 但一级导航不做深色那种围绕基座的特殊处理,正常横排即可。 + ══════════════════════════════════════════════════════════════════════ */ + +/* ── 顶栏一级导航(白字落在 red/blue-header banner 上)──────────────────── */ +.light-topnav { + display: flex; + align-items: center; + height: 64px; + min-width: 0; + gap: 6px; + /* 整体往右移(用百分比,随容器宽度自适应;约等于宽屏下的 200px 量级) */ + margin-left: 10%; + overflow-x: auto; + overflow-y: hidden; +} + +.light-topnav-item { + position: relative; + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + height: 60px; + padding: 0 18px; + border: 0; + background: transparent; + color: rgba(255, 255, 255, 0.85); + /* 复用全局字体栈(与「多智能体会商」等页面一致):Source Han Sans SC 在 Windows 上 + 常按字重分家族,放栈首会导致 font-weight 选不到粗体;用 --font-sans(Noto Sans SC + 领先)才能正常加粗。 */ + font-family: var(--font-sans); + font-size: 16px; + /* 普通项用 500(雅黑兜底下渲染为常规字面),选中项 700 才能看出粗体对比 */ + font-weight: 500; + line-height: 1; + white-space: nowrap; + cursor: pointer; + transition: color 0.18s ease; +} + +.light-topnav-item:hover { + color: #ffffff; +} + +/* 选中态:不改背景,文字加粗变亮 + 贴近导航栏底部一条粗白线 */ +.light-topnav-item--active, +.light-topnav-item--active:hover { + color: #ffffff; + font-weight: 700; +} + +.light-topnav-item--active::after { + content: ''; + position: absolute; + left: 12px; + right: 12px; + bottom: 0; + height: 4px; + border-radius: 3px 3px 0 0; + background: #ffffff; +} + +/* ── 侧边栏:二级图标卡片列(96px)+ 右侧三级列(展开时整体变宽,带过渡)──────── */ +.light-sidebar { + position: fixed; + left: 0; + top: 4rem; + z-index: 40; + width: 96px; + height: calc(100vh - 4rem); + display: flex; + flex-direction: row; + background: #ffffff; + border-right: 1px solid var(--border); + box-sizing: border-box; + overflow: hidden; + /* 展开三级列时整体变宽的过渡动画 */ + transition: width 0.3s ease; +} + +/* 展开三级列:96(二级) + 140(三级) = 236px */ +.light-sidebar--expanded { + width: 236px; +} + +/* 二级列:固定 96px,竖排图标卡片 + 底部设置 */ +.light-sidebar__primary { + flex: 0 0 96px; + width: 96px; + height: 100%; + display: flex; + flex-direction: column; + /* 横向 padding 置 0:让选中态浅红背景能铺满与列宽一致 */ + padding: 10px 0; + box-sizing: border-box; +} + +/* 三级列:固定 140px,随侧边栏展开淡入 */ +.light-sidebar__secondary { + flex: 0 0 140px; + width: 140px; + height: 100%; + border-left: 1px solid var(--border); + /* 横向 padding 置 0:让三级选中背景能铺满与列宽一致(文字内缩由 subitem 自身 padding 保证) */ + padding: 12px 0; + box-sizing: border-box; + overflow-y: auto; + overflow-x: hidden; + background: #ffffff; + opacity: 0; + transition: opacity 0.2s ease; +} + +.light-sidebar--expanded .light-sidebar__secondary { + opacity: 1; +} + +.light-sidebar__list { + flex: 1; + display: flex; + flex-direction: column; + gap: 6px; + overflow-y: auto; + overflow-x: hidden; +} + +.light-sidebar-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; + padding: 14px 4px; + border: 0; + border-radius: 0; + background: transparent; + color: #222222; + /* 复用全局字体栈,确保加粗生效(详见 .light-topnav-item 注释) */ + font-family: var(--font-sans); + font-size: 16px; + font-weight: 500; + line-height: 1.3; + text-align: center; + cursor: pointer; + transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease; +} + +.light-sidebar-item__icon { + flex-shrink: 0; + /* 图标统一用主题色(红主题红、蓝主题蓝) */ + color: var(--brand-900); + transition: color 0.18s ease, transform 0.18s ease; +} + +.light-sidebar-item__label { + display: block; + width: 100%; + word-break: break-word; +} + +.light-sidebar-item:hover { + background: var(--brand-200); +} + +.light-sidebar-item:hover .light-sidebar-item__icon { + color: var(--brand-900); + transform: scale(1.05); +} + +.light-sidebar-item--active { + background: var(--brand-200); + color: var(--brand-900); + font-weight: 600; + /* 选中态:去圆角去边框,浅红底铺满父元素宽度,右侧一条粗主题色边(用 inset 阴影避免挤压内容) */ + box-shadow: inset -4px 0 0 0 var(--brand-900); +} + +.light-sidebar-item--active .light-sidebar-item__icon { + color: var(--brand-900); +} + +/* 侧边栏底部:设置入口(浅色模式不支持收起,故无收起按钮) */ +.light-sidebar__footer { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 4px; + padding-top: 8px; + border-top: 1px solid var(--border); +} + +/* ── 三级列条目(左对齐图标 + 文字,参考 xiugai-1.png)─────────────────────── */ +.light-sidebar-subnav { + display: flex; + flex-direction: column; + gap: 2px; +} + +.light-sidebar-subitem { + position: relative; + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 10px; + border: 0; + background: transparent; + color: #222222; + font-family: var(--font-sans); + font-size: 14px; + font-weight: 500; + line-height: 1.3; + text-align: left; + cursor: pointer; + transition: background-color 0.18s ease, color 0.18s ease, box-shadow 0.18s ease; +} + +.light-sidebar-subitem__icon { + flex-shrink: 0; + /* 图标用主题色(红主题红、蓝主题蓝) */ + color: var(--brand-900); +} + +.light-sidebar-subitem__label { + flex: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.light-sidebar-subitem:hover { + background: var(--brand-200); +} + +.light-sidebar-subitem--active { + background: var(--brand-200); + color: var(--brand-900); + font-weight: 700; + box-shadow: inset -4px 0 0 0 var(--brand-900); +} diff --git a/frontend-web/src/components/page-sidebar.tsx b/frontend-web/src/components/page-sidebar.tsx index 58e59a72..42b556fe 100644 --- a/frontend-web/src/components/page-sidebar.tsx +++ b/frontend-web/src/components/page-sidebar.tsx @@ -22,14 +22,18 @@ import { NotificationDialog } from "@/components/workspace/notification-dialog"; import { SettingsDialog } from "@/components/workspace/settings"; import { useTheme } from "@/contexts/ThemeContext"; import { getStoredAuth } from "@/core/auth"; +import { isWujieEnv } from "@/core/embed/wujie"; import { useI18n } from "@/core/i18n/hooks"; import { useUnreadCount } from "@/core/notifications"; import { defaultExpandedIds, + getActiveThirdLevelItems, type MenuItem, } from "@/core/page-layout/sidebar-menu"; import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu"; +import "@/components/page-sdebar.css"; + type PageSidebarProps = { collapsed: boolean; onToggleCollapsed: () => void; @@ -37,7 +41,201 @@ type PageSidebarProps = { type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about"; -export function PageSidebar({ +const hasActiveChild = (item: MenuItem, pathname: string): boolean => { + if (item.path && pathname === item.path) { + return true; + } + return item.children?.some((child) => hasActiveChild(child, pathname)) ?? false; +}; + +const findFirstRoutePath = (items?: MenuItem[]): string | null => { + if (!items?.length) { + return null; + } + for (const child of items) { + if (child.path) { + return child.path; + } + const nested = findFirstRoutePath(child.children); + if (nested) { + return nested; + } + } + return null; +}; + +/** + * 浅色侧边栏入口。 + * + * - 普通桌面环境:渲染「二级导航图标卡片」(一级在顶栏、二级在侧边栏,参考 导航-1.png)。 + * - wujie 嵌入环境:顶栏被宿主隐藏,一级导航无处安放,故回落到原手风琴整树菜单 + * (保证嵌入模式下所有层级仍可达,零回归)。 + */ +export function PageSidebar({ collapsed, onToggleCollapsed }: PageSidebarProps) { + if (isWujieEnv()) { + return ( + + ); + } + return ; +} + +/** + * 浅色二级导航侧边栏(图标卡片,红/蓝主题色经 --brand-* 变量自动切换)。 + * 浅色模式不支持侧边栏收起,故无收起按钮、不接收 collapsed 相关 props。 + */ +function SecondaryNavSidebar() { + const isAdmin = getStoredAuth()?.system_role === "admin"; + const visibleMenuItems = useSidebarMenuItems(!!isAdmin); + const { t } = useI18n(); + const { data: unreadCount = 0 } = useUnreadCount(); + const [settingsOpen, setSettingsOpen] = useState(false); + const [settingsDefaultSection, setSettingsDefaultSection] = + useState("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); + }; + + // 当前激活的一级菜单(与顶栏一级导航联动);其 children 即侧边栏要渲染的二级导航。 + const activeTopItem = + visibleMenuItems.find((item) => hasActiveChild(item, pathname)) ?? + visibleMenuItems[0]; + const secondaryItems = activeTopItem?.children ?? []; + + const isItemActive = (item: MenuItem) => + item.path ? pathname === item.path : hasActiveChild(item, pathname); + + const handleItemClick = (item: MenuItem) => { + const path = item.path ?? findFirstRoutePath(item.children); + if (path) { + navigate(path); + } + }; + + // 当前激活二级项的三级子项;非空时侧边栏在右侧展开「三级列」并整体变宽(见 css 过渡)。 + const thirdItems = getActiveThirdLevelItems(visibleMenuItems, pathname); + const expanded = thirdItems.length > 0; + + return ( + <> + + + + + ); +} + +/** + * 旧版浅色整树手风琴侧边栏。仅用于 wujie 嵌入环境(顶栏被宿主隐藏时保证所有层级可达)。 + */ +function LegacyAccordionSidebar({ collapsed, onToggleCollapsed, }: PageSidebarProps) { @@ -66,9 +264,6 @@ export function PageSidebar({ 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"; diff --git a/frontend-web/src/core/page-layout/sidebar-menu.ts b/frontend-web/src/core/page-layout/sidebar-menu.ts index 908e596a..7e3ccbf5 100644 --- a/frontend-web/src/core/page-layout/sidebar-menu.ts +++ b/frontend-web/src/core/page-layout/sidebar-menu.ts @@ -9,15 +9,16 @@ */ import type { ElementType } from "react"; import { + Activity, BookOpen, Bot, Brain, CalendarClock, CircleHelp, + ClipboardList, Database, FileBox, FileText, - Folder, Gauge, Grid3X3, HelpCircle, @@ -29,6 +30,7 @@ import { ListChecks, ListTodo, Network, + Radar, Settings, Sparkles, Target, @@ -77,7 +79,7 @@ export const sidebarMenuItems: MenuItem[] = [ { id: "xk-center-task", label: "XK中任务", - icon: Folder, + icon: ClipboardList, children: [ { id: "xk-task-jc", @@ -96,7 +98,7 @@ export const sidebarMenuItems: MenuItem[] = [ { id: "coh-center-task", label: "COH中任务", - icon: Folder, + icon: Activity, children: [ { id: "coh-task-3qfx", @@ -121,7 +123,7 @@ export const sidebarMenuItems: MenuItem[] = [ { id: "pg-center-task", label: "PG中任务", - icon: Folder, + icon: Gauge, children: [ { id: "pg-task-pg", @@ -146,7 +148,7 @@ export const sidebarMenuItems: MenuItem[] = [ { id: "ty-center-task", label: "TY中任务", - icon: Folder, + icon: Radar, children: [ { id: "ty-task-xdty", @@ -296,6 +298,31 @@ export const defaultExpandedIds: ReadonlySet = new Set([ "comprehensive-management", ]); +/** 该菜单项自身或任意后代的 path 是否等于当前路由 */ +export function menuItemContainsPath(item: MenuItem, pathname: string): boolean { + if (item.path && item.path === pathname) { + return true; + } + return item.children?.some((child) => menuItemContainsPath(child, pathname)) ?? false; +} + +/** + * 在「一级 → 二级 → 三级」菜单中,取当前路由所在的「激活二级项」的三级子项。 + * 浅色侧边栏据此在二级右侧展开三级列;无三级则返回空数组(侧边栏保持窄栏)。 + */ +export function getActiveThirdLevelItems( + items: MenuItem[], + pathname: string, +): MenuItem[] { + const activeTop = + items.find((item) => menuItemContainsPath(item, pathname)) ?? items[0]; + const secondary = activeTop?.children ?? []; + const activeSecond = secondary.find( + (item) => (item.children?.length ?? 0) > 0 && menuItemContainsPath(item, pathname), + ); + return activeSecond?.children ?? []; +} + /** 按 admin 角色过滤菜单:非 admin 不展示 ``adminOnly: true`` 的项及其子项 */ export function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] { return items diff --git a/frontend-web/src/core/page-layout/use-sidebar-menu.ts b/frontend-web/src/core/page-layout/use-sidebar-menu.ts index 1f2ec6b0..006c767e 100644 --- a/frontend-web/src/core/page-layout/use-sidebar-menu.ts +++ b/frontend-web/src/core/page-layout/use-sidebar-menu.ts @@ -8,12 +8,15 @@ * 浅色 PageSidebar 与深色 PageSidebarDark 共用此 hook,保证两套皮肤同步。 */ import { useMemo } from "react"; +import { useLocation } from "react-router-dom"; import { Layout } from "lucide-react"; +import { getStoredAuth } from "@/core/auth"; import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps"; import { filterMenuItemsByRole, + getActiveThirdLevelItems, sidebarMenuItems, type MenuItem, } from "./sidebar-menu"; @@ -61,3 +64,14 @@ export function useSidebarMenuItems(isAdmin: boolean): MenuItem[] { [iframeApps, isAdmin], ); } + +/** + * 当前路由所在「激活二级项」的三级子项(浅色侧边栏右侧列用)。 + * 供 PageRoutes(主内容左内边距随展开自适应)与浅色侧边栏共用同一份判定逻辑。 + */ +export function useActiveThirdLevel(): MenuItem[] { + const isAdmin = getStoredAuth()?.system_role === "admin"; + const items = useSidebarMenuItems(!!isAdmin); + const { pathname } = useLocation(); + return useMemo(() => getActiveThirdLevelItems(items, pathname), [items, pathname]); +} diff --git a/frontend-web/src/pages/PageRoutes.tsx b/frontend-web/src/pages/PageRoutes.tsx index 178c1544..bb849769 100644 --- a/frontend-web/src/pages/PageRoutes.tsx +++ b/frontend-web/src/pages/PageRoutes.tsx @@ -5,8 +5,10 @@ import { PageSidebar } from "@/components/page-sidebar"; import { PageSidebarDark } from "@/components/page-sidebar-dark"; import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route"; import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled"; +import { isWujieEnv } from "@/core/embed/wujie"; import { useTheme } from "@/contexts/ThemeContext"; import { useLocation } from "react-router-dom"; +import { useActiveThirdLevel } from "@/core/page-layout/use-sidebar-menu"; import { useLayoutMode } from "@/core/system-settings/hooks"; import WorkspaceRoutes from "./WorkspaceRoutes"; @@ -22,6 +24,12 @@ function PageLayout() { const location = useLocation(); const { theme } = useTheme(); const isDark = theme === "dark"; + // 浅色非嵌入:侧边栏为新版「二级导航图标卡片」窄栏(96px);wujie 嵌入仍走旧版手风琴(64/256px)。 + const inWujie = useMemo(() => isWujieEnv(), []); + const lightNarrowSidebar = !isDark && !inWujie; + // 浅色窄栏在右侧展开「三级列」时整体变宽(96→236px),主内容左内边距随之过渡。 + const thirdLevel = useActiveThirdLevel(); + const lightExpanded = lightNarrowSidebar && thirdLevel.length > 0; // 界面模式(外观设置):用户显式选「全量/简洁」时以其为准;否则跟随管理员系统默认 // (default_layout_mode);两者都没有才回退到会话级 sidebarEnabled(外部门户/yUserId // 登录时置位)这一旧默认。这样无论是否走门户登录,「全量↔简洁」都能真正控制顶栏+侧边栏。 @@ -60,8 +68,11 @@ function PageLayout() { // 只剩左下角浮动按钮(fixed,叠在 main 上不影响布局)。 // 不加 transition:transition + sidebar 立刻 unmount 不同步会抽搐 ? (collapsed ? "pl-0" : "pl-[118px]") - // 浅色仍保留过渡动画(pl-16 ↔ pl-64 平滑收展) - : `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}` + : lightNarrowSidebar + // 浅色新版窄栏:展开三级列时 96→236px,主内容左内边距随之过渡 + ? `transition-[padding] duration-300 ${lightExpanded ? "pl-[236px]" : "pl-[96px]"}` + // 浅色旧版手风琴(wujie 嵌入):pl-16 ↔ pl-64 平滑收展 + : `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}` }`} > diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 7e4f144f..b27f18a5 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -139,6 +139,10 @@ const Header = ({ collapsed }: HeaderProps) => { return email ? email.split("@")[0] : "用户"; }, [auth?.email]); const showTopNav = isDark && !collapsed && !isWujieEnv(); + // 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。 + // 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏, + // 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。 + const showLightTopNav = !isDark && !isWujieEnv(); const hasActiveChild = (item: MenuItem): boolean => { if (item.path && pathname === item.path) { @@ -194,6 +198,20 @@ const Header = ({ collapsed }: HeaderProps) => { ); }; + const renderLightTopItem = (item: MenuItem) => { + const isSelected = item.path ? pathname === item.path : hasActiveChild(item); + return ( + + ); + }; + // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> @@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => { + ) : showLightTopNav ? ( + <> + {/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */} +
+
+
+ +
+ + CM助手 + +
+ +
+ +
+ {actionCluster} +
+ ) : ( <>
@@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => { )} + {/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */} {showTopNav && thirdSubNavItems.length ? (