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:
1 parent
fbd237531e
commit
ee6ab38c37
6 files changed
+543
-11
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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 (
|
||||
<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,
|
||||
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";
|
||||
|
||||
@@ -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<string> = 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
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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"}`
|
||||
}`}
|
||||
>
|
||||
<Routes>
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
className={`light-topnav-item ${isSelected ? "light-topnav-item--active" : ""}`}
|
||||
onClick={() => handleNavItemClick(item)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用
|
||||
const actionCluster = (
|
||||
<>
|
||||
@@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
</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">
|
||||
@@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => {
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
{/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */}
|
||||
{showTopNav && thirdSubNavItems.length ? (
|
||||
<nav className="third-submenu-bar" aria-label="三级菜单">
|
||||
<div className="third-submenu-list">
|
||||
|
||||
Reference in new issue
Block a user