feat(nav): 浅色模式导航重构(顶栏一级 + 侧边栏二级图标卡 + 三级右侧列)

浅色(红/蓝)主题改为与深色一致的导航逻辑,颜色经 --brand-* 变量自动切换:
- 一级导航正常横排进顶栏(白字落在 banner 上,选中加粗 + 底部粗白线)
- 二级导航为侧边栏图标卡片(图标主题色、文字 #222222、选中铺满列宽 + 右侧粗边)
- 三级导航改到侧边栏二级右侧的「三级列」:激活二级项有三级时整列展开、
  侧边栏 96→236px 整体变宽并带过渡动画,主内容左内边距同步平滑右移
- 顶栏三级横条改为仅深色用;浅色不支持侧边栏收起
- 任务管理二级项改用区分图标(ClipboardList/Activity/Gauge/Radar)
- 字体统一用全局 --font-sans,修复雅黑兜底下加粗不生效
- 抽出 getActiveThirdLevelItems / useActiveThirdLevel 供侧边栏与 PageRoutes 共用

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
董宏凯andClaude Opus 4.8 committed 2026-06-15 16:53:21 +08:00
1 parent fbd237531e
commit ee6ab38c37
6 files changed
+543 -11

No files matched your search

+245
View File
@@ -498,3 +498,248 @@
background: linear-gradient(90deg, rgba(62, 178, 255, 0) 0%, #4ee6ff 50%, rgba(62, 178, 255, 0) 100%); 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); 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);
}
+199 -4
View File
@@ -22,14 +22,18 @@ import { NotificationDialog } from "@/components/workspace/notification-dialog";
import { SettingsDialog } from "@/components/workspace/settings"; import { SettingsDialog } from "@/components/workspace/settings";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { getStoredAuth } from "@/core/auth"; import { getStoredAuth } from "@/core/auth";
import { isWujieEnv } from "@/core/embed/wujie";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { useUnreadCount } from "@/core/notifications"; import { useUnreadCount } from "@/core/notifications";
import { import {
defaultExpandedIds, defaultExpandedIds,
getActiveThirdLevelItems,
type MenuItem, type MenuItem,
} from "@/core/page-layout/sidebar-menu"; } from "@/core/page-layout/sidebar-menu";
import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu"; import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu";
import "@/components/page-sdebar.css";
type PageSidebarProps = { type PageSidebarProps = {
collapsed: boolean; collapsed: boolean;
onToggleCollapsed: () => void; onToggleCollapsed: () => void;
@@ -37,7 +41,201 @@ type PageSidebarProps = {
type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about"; 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 (
<LegacyAccordionSidebar
collapsed={collapsed}
onToggleCollapsed={onToggleCollapsed}
/>
);
}
return <SecondaryNavSidebar />;
}
/**
* 浅色二级导航侧边栏(图标卡片,红/蓝主题色经 --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<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);
};
// 当前激活的一级菜单(与顶栏一级导航联动);其 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 (
<>
<SettingsDialog
open={settingsOpen}
onOpenChange={setSettingsOpen}
defaultSection={settingsDefaultSection}
/>
<NotificationDialog
open={notificationOpen}
onOpenChange={setNotificationOpen}
/>
<aside className={`light-sidebar ${expanded ? "light-sidebar--expanded" : ""}`}>
{/* 一级列:二级图标卡片 + 底部设置 */}
<div className="light-sidebar__primary">
<nav className="light-sidebar__list" aria-label="二级菜单">
{secondaryItems.map((item) => {
const Icon = item.icon;
const active = isItemActive(item);
return (
<button
key={item.id}
type="button"
title={item.label}
onClick={() => handleItemClick(item)}
className={`light-sidebar-item ${active ? "light-sidebar-item--active" : ""}`}
>
<Icon className="light-sidebar-item__icon h-7 w-7" />
<span className="light-sidebar-item__label">{item.label}</span>
</button>
);
})}
</nav>
<div className="light-sidebar__footer">
{mounted ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
title={t.workspace.settingsAndMore}
className="light-sidebar-item"
>
<Settings className="light-sidebar-item__icon h-7 w-7" />
<span className="light-sidebar-item__label">设置</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="right"
sideOffset={8}
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 bg-[var(--brand-900)] px-1 text-[10px] font-medium leading-none text-white">
{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>
) : null}
</div>
</div>
{/* 二级列:三级导航(激活二级项有三级子项时展开,整体动画见 css width transition) */}
<div className="light-sidebar__secondary" aria-hidden={!expanded}>
<nav className="light-sidebar-subnav" aria-label="三级菜单">
{thirdItems.map((child) => {
const ChildIcon = child.icon;
const childActive = child.path
? pathname === child.path
: hasActiveChild(child, pathname);
return (
<button
key={child.id}
type="button"
title={child.label}
onClick={() => handleItemClick(child)}
className={`light-sidebar-subitem ${childActive ? "light-sidebar-subitem--active" : ""}`}
>
<ChildIcon className="light-sidebar-subitem__icon h-4 w-4" />
<span className="light-sidebar-subitem__label">{child.label}</span>
</button>
);
})}
</nav>
</div>
</aside>
</>
);
}
/**
* 旧版浅色整树手风琴侧边栏。仅用于 wujie 嵌入环境(顶栏被宿主隐藏时保证所有层级可达)。
*/
function LegacyAccordionSidebar({
collapsed, collapsed,
onToggleCollapsed, onToggleCollapsed,
}: PageSidebarProps) { }: PageSidebarProps) {
@@ -66,9 +264,6 @@ export function PageSidebar({
setSettingsOpen(true); 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 const panelClass = isDark
? "ml-4 space-y-1 border-l border-blue-800/30 pl-4" ? "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"; : "ml-4 space-y-1 border-l border-red-100 pl-4";
@@ -9,15 +9,16 @@
*/ */
import type { ElementType } from "react"; import type { ElementType } from "react";
import { import {
Activity,
BookOpen, BookOpen,
Bot, Bot,
Brain, Brain,
CalendarClock, CalendarClock,
CircleHelp, CircleHelp,
ClipboardList,
Database, Database,
FileBox, FileBox,
FileText, FileText,
Folder,
Gauge, Gauge,
Grid3X3, Grid3X3,
HelpCircle, HelpCircle,
@@ -29,6 +30,7 @@ import {
ListChecks, ListChecks,
ListTodo, ListTodo,
Network, Network,
Radar,
Settings, Settings,
Sparkles, Sparkles,
Target, Target,
@@ -77,7 +79,7 @@ export const sidebarMenuItems: MenuItem[] = [
{ {
id: "xk-center-task", id: "xk-center-task",
label: "XK中任务", label: "XK中任务",
icon: Folder, icon: ClipboardList,
children: [ children: [
{ {
id: "xk-task-jc", id: "xk-task-jc",
@@ -96,7 +98,7 @@ export const sidebarMenuItems: MenuItem[] = [
{ {
id: "coh-center-task", id: "coh-center-task",
label: "COH中任务", label: "COH中任务",
icon: Folder, icon: Activity,
children: [ children: [
{ {
id: "coh-task-3qfx", id: "coh-task-3qfx",
@@ -121,7 +123,7 @@ export const sidebarMenuItems: MenuItem[] = [
{ {
id: "pg-center-task", id: "pg-center-task",
label: "PG中任务", label: "PG中任务",
icon: Folder, icon: Gauge,
children: [ children: [
{ {
id: "pg-task-pg", id: "pg-task-pg",
@@ -146,7 +148,7 @@ export const sidebarMenuItems: MenuItem[] = [
{ {
id: "ty-center-task", id: "ty-center-task",
label: "TY中任务", label: "TY中任务",
icon: Folder, icon: Radar,
children: [ children: [
{ {
id: "ty-task-xdty", id: "ty-task-xdty",
@@ -296,6 +298,31 @@ export const defaultExpandedIds: ReadonlySet<string> = new Set([
"comprehensive-management", "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`` 的项及其子项 */ /** 按 admin 角色过滤菜单:非 admin 不展示 ``adminOnly: true`` 的项及其子项 */
export function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] { export function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] {
return items return items
@@ -8,12 +8,15 @@
* 浅色 PageSidebar 与深色 PageSidebarDark 共用此 hook,保证两套皮肤同步。 * 浅色 PageSidebar 与深色 PageSidebarDark 共用此 hook,保证两套皮肤同步。
*/ */
import { useMemo } from "react"; import { useMemo } from "react";
import { useLocation } from "react-router-dom";
import { Layout } from "lucide-react"; import { Layout } from "lucide-react";
import { getStoredAuth } from "@/core/auth";
import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps"; import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps";
import { import {
filterMenuItemsByRole, filterMenuItemsByRole,
getActiveThirdLevelItems,
sidebarMenuItems, sidebarMenuItems,
type MenuItem, type MenuItem,
} from "./sidebar-menu"; } from "./sidebar-menu";
@@ -61,3 +64,14 @@ export function useSidebarMenuItems(isAdmin: boolean): MenuItem[] {
[iframeApps, isAdmin], [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]);
}
+13 -2
View File
@@ -5,8 +5,10 @@ import { PageSidebar } from "@/components/page-sidebar";
import { PageSidebarDark } from "@/components/page-sidebar-dark"; import { PageSidebarDark } from "@/components/page-sidebar-dark";
import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route"; import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route";
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled"; import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
import { isWujieEnv } from "@/core/embed/wujie";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { useActiveThirdLevel } from "@/core/page-layout/use-sidebar-menu";
import { useLayoutMode } from "@/core/system-settings/hooks"; import { useLayoutMode } from "@/core/system-settings/hooks";
import WorkspaceRoutes from "./WorkspaceRoutes"; import WorkspaceRoutes from "./WorkspaceRoutes";
@@ -22,6 +24,12 @@ function PageLayout() {
const location = useLocation(); const location = useLocation();
const { theme } = useTheme(); const { theme } = useTheme();
const isDark = theme === "dark"; 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 // (default_layout_mode);两者都没有才回退到会话级 sidebarEnabled(外部门户/yUserId
// 登录时置位)这一旧默认。这样无论是否走门户登录,「全量↔简洁」都能真正控制顶栏+侧边栏。 // 登录时置位)这一旧默认。这样无论是否走门户登录,「全量↔简洁」都能真正控制顶栏+侧边栏。
@@ -60,8 +68,11 @@ function PageLayout() {
// 只剩左下角浮动按钮(fixed,叠在 main 上不影响布局)。 // 只剩左下角浮动按钮(fixed,叠在 main 上不影响布局)。
// 不加 transition:transition + sidebar 立刻 unmount 不同步会抽搐 // 不加 transition:transition + sidebar 立刻 unmount 不同步会抽搐
? (collapsed ? "pl-0" : "pl-[118px]") ? (collapsed ? "pl-0" : "pl-[118px]")
// 浅色仍保留过渡动画(pl-16 ↔ pl-64 平滑收展) : lightNarrowSidebar
: `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}` // 浅色新版窄栏:展开三级列时 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"}`
}`} }`}
> >
<Routes> <Routes>
@@ -139,6 +139,10 @@ const Header = ({ collapsed }: HeaderProps) => {
return email ? email.split("@")[0] : "用户"; return email ? email.split("@")[0] : "用户";
}, [auth?.email]); }, [auth?.email]);
const showTopNav = isDark && !collapsed && !isWujieEnv(); const showTopNav = isDark && !collapsed && !isWujieEnv();
// 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。
// 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏,
// 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。
const showLightTopNav = !isDark && !isWujieEnv();
const hasActiveChild = (item: MenuItem): boolean => { const hasActiveChild = (item: MenuItem): boolean => {
if (item.path && pathname === item.path) { 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 (
<button
key={item.id}
type="button"
className={`light-topnav-item ${isSelected ? "light-topnav-item--active" : ""}`}
onClick={() => handleNavItemClick(item)}
>
{item.label}
</button>
);
};
// 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用
const actionCluster = ( const actionCluster = (
<> <>
@@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => {
</div> </div>
</div> </div>
</> </>
) : showLightTopNav ? (
<>
{/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */}
<div className="flex h-full min-w-0 flex-1 items-center">
<div className="flex flex-shrink-0 items-center gap-2 pr-1">
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-white/20">
<FileText className="h-4 w-4 text-white" />
</div>
<span className="whitespace-nowrap text-base font-semibold text-white">
CM助手
</span>
</div>
<nav className="light-topnav" aria-label="一级菜单">
{visibleMenuItems.map(renderLightTopItem)}
</nav>
</div>
<div className="flex shrink-0 items-center gap-2 pl-2">
{actionCluster}
</div>
</>
) : ( ) : (
<> <>
<div className="flex h-full min-w-0 flex-1 items-center gap-3"> <div className="flex h-full min-w-0 flex-1 items-center gap-3">
@@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => {
</> </>
)} )}
</header> </header>
{/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */}
{showTopNav && thirdSubNavItems.length ? ( {showTopNav && thirdSubNavItems.length ? (
<nav className="third-submenu-bar" aria-label="三级菜单"> <nav className="third-submenu-bar" aria-label="三级菜单">
<div className="third-submenu-list"> <div className="third-submenu-list">