## 6. 菜单体系全面优化 菜单导航是SQL-Platform最核心的交互骨架,直接影响用户的工作流效率和平台整体体验。当前菜单体系存在分组逻辑不够清晰、图标体系不统一、权限控制粒度较粗、缺少面包屑导航等结构性问题。本章将从菜单结构重组、图标规范化、权限映射优化和面包屑导航四个维度进行系统性重构。 --- ### 6.1 菜单结构重组方案 #### 6.1.1 重组设计思路 当前菜单的六个分组存在三个核心问题:其一,"配置平台"分组仅包含"系统设置"一项,空间利用率极低;其二,"自动任务"归类于"系统配置"不够合理,它与运维监控的关联度远高于系统配置;其三,"工单管理"的命名偏向内部管理视角,对实际执行数据操作的开发者而言不够直观。 重组方案遵循"以用户角色为中心、以任务流为导向"的原则,将原有6个分组精简为6个更具语义清晰度的分组。核心调整包括:将"概览"升级为"工作台"并增加快捷入口;将"工单管理"重命名为"数据操作";将"自动任务"移至"运维中心";将孤立的"系统设置"与新增"个人中心"组成"平台设置"。 #### 6.1.2 优化后的完整菜单定义 ```typescript // /frontend/src/constants/menu.ts import type { LucideIcon } from 'lucide-react'; /** 权限码类型 —— 替代粗粒度的 roles 数组 */ export type PermissionCode = | 'dashboard:view' | 'todo:view' | 'query:quick_access' | 'ticket:view' | 'ticket:create' | 'query:view' | 'sql_assistant:use' | 'datasource:view' | 'datasource:manage' | 'data_dict:view' | 'user:view' | 'user:manage' | 'role:view' | 'role:manage' | 'perm_group:view' | 'perm_group:manage' | 'workflow:view' | 'workflow:manage' | 'audit_log:view' | 'auto_task:view' | 'auto_task:manage' | 'announcement:view' | 'settings:system' | 'settings:personal'; /** 菜单分组定义 */ export interface MenuSection { id: string; label: string; icon?: LucideIcon; order: number; } export const menuSections: MenuSection[] = [ { id: 'workspace', label: '工作台', order: 10 }, { id: 'dataops', label: '数据操作', order: 20 }, { id: 'datasource', label: '数据源', order: 30 }, { id: 'permission', label: '权限中心', order: 40 }, { id: 'ops', label: '运维中心', order: 50 }, { id: 'platform', label: '平台设置', order: 60 }, ]; /** Badge 数据源类型 */ export type BadgeSource = 'pending_count' | 'unread_count' | 'task_fail_count' | 'announcement_count' | none; /** 菜单项定义 */ export interface MenuItem { id: string; label: string; icon: string; // lucide-react 图标名称 section: string; // 对应 menuSections.id permissions: PermissionCode[]; // 细粒度权限码(满足任一即显示) badge?: BadgeSource; // 动态角标数据源 shortcut?: string; // 快捷键(如 Ctrl+1) order: number; // 组内排序 } export const menuItems: MenuItem[] = [ // ─── 工作台 ─── { id: 'dashboard', label: '仪表盘', icon: 'LayoutDashboard', section: 'workspace', permissions: ['dashboard:view'], order: 10, }, { id: 'my-todo', label: '我的待办', icon: 'CheckCircle2', section: 'workspace', permissions: ['ticket:view', 'workflow:view'], badge: 'pending_count', order: 20, }, { id: 'quick-queries', label: '快速查询', icon: 'Zap', section: 'workspace', permissions: ['query:view', 'query:quick_access'], order: 30, }, // ─── 数据操作 ─── { id: 'tickets', label: '数据变更', icon: 'ClipboardList', section: 'dataops', permissions: ['ticket:view'], badge: 'pending_count', order: 10, }, { id: 'queries', label: '查询数据', icon: 'Search', section: 'dataops', permissions: ['query:view'], order: 20, }, { id: 'sql-assistant', label: 'SQL助手', icon: 'Wand2', section: 'dataops', permissions: ['sql_assistant:use', 'query:view'], order: 30, }, // ─── 数据源 ─── { id: 'instances', label: '数据源管理', icon: 'Database', section: 'datasource', permissions: ['datasource:view'], order: 10, }, { id: 'data-dict', label: '数据字典', icon: 'BookOpen', section: 'datasource', permissions: ['data_dict:view', 'datasource:view'], order: 20, }, // ─── 权限中心 ─── { id: 'users', label: '用户管理', icon: 'Users', section: 'permission', permissions: ['user:view'], order: 10, }, { id: 'roles', label: '角色权限', icon: 'UserCog', section: 'permission', permissions: ['role:view'], order: 20, }, { id: 'instance-groups', label: '权限组', icon: 'Shield', section: 'permission', permissions: ['perm_group:view'], order: 30, }, { id: 'workflow', label: '审批流程', icon: 'GitBranch', section: 'permission', permissions: ['workflow:view'], order: 40, }, // ─── 运维中心 ─── { id: 'audit-logs', label: '审计日志', icon: 'FileText', section: 'ops', permissions: ['audit_log:view'], order: 10, }, { id: 'auto-task', label: '自动任务', icon: 'Clock', section: 'ops', permissions: ['auto_task:view'], badge: 'task_fail_count', order: 20, }, { id: 'announcements', label: '系统公告', icon: 'Megaphone', section: 'ops', permissions: ['announcement:view'], badge: 'announcement_count', order: 30, }, // ─── 平台设置 ─── { id: 'settings', label: '系统设置', icon: 'Settings', section: 'platform', permissions: ['settings:system'], order: 10, }, { id: 'profile', label: '个人中心', icon: 'UserCircle', section: 'platform', permissions: ['settings:personal'], order: 20, }, ]; /** 根据分组 ID 获取菜单项 */ export const getMenuItemsBySection = (sectionId: string): MenuItem[] => { return menuItems .filter((item) => item.section === sectionId) .sort((a, b) => a.order - b.order); }; /** 根据权限码过滤可见菜单项 */ export const filterMenuByPermissions = ( items: MenuItem[], userPermissions: PermissionCode[] ): MenuItem[] => { return items.filter((item) => item.permissions.some((perm) => userPermissions.includes(perm)) ); }; ``` #### 6.1.3 重组收益 | 维度 | 调整前 | 调整后 | 收益 | |------|--------|--------|------| | 分组数量 | 6个(含1个单项目分组) | 6个(每个分组2-4项) | 分布均匀,视觉平衡 | | 语义清晰度 | "工单管理"偏管理视角 | "数据操作"以操作为导向 | 降低认知成本 | | 功能关联性 | 自动任务与系统配置弱关联 | 自动任务归入运维中心 | 逻辑一致性提升 | | 快捷入口 | 无 | 工作台聚合待办+快速查询 | 减少用户操作路径 | | 可扩展性 | 新增功能无处安放 | 各分组预留扩展空间 | 支持未来功能迭代 | --- ### 6.2 图标体系规范化 #### 6.2.1 现状问题分析 当前系统存在两套图标体系并行的问题:侧边栏使用 `lucide-react` 矢量图标,而设置页面使用 emoji 图标(🎨⚙️🛡️🔔🔐)。这种不一致带来三个负面影响:视觉风格割裂,emoji 在不同操作系统渲染差异大;专业感下降,emoji 偏向社交/休闲场景;无障碍支持差,emoji 的屏幕阅读器朗读行为不可控。 #### 6.2.2 图标映射表 以下是重组后全部菜单项的图标映射,统一使用 `lucide-react` 图标库: | 菜单项 ID | 菜单标签 | 图标名称 (Lucide) | 选用理由 | |-----------|----------|-------------------|----------| | `dashboard` | 仪表盘 | `LayoutDashboard` | 网格布局隐喻,dashboard 标准语义 | | `my-todo` | 我的待办 | `CheckCircle2` | 待办完成状态的圆形勾选隐喻 | | `quick-queries` | 快速查询 | `Zap` | 闪电符号表达"快速"语义 | | `tickets` | 数据变更 | `ClipboardList` | 工单列表的标准图标,保持不变 | | `queries` | 查询数据 | `Search` | 搜索查询的标准语义 | | `sql-assistant` | SQL助手 | `Wand2` | 魔杖隐喻"智能辅助/魔法般的体验" | | `instances` | 数据源管理 | `Database` | 数据库标准图标,保持不变 | | `data-dict` | 数据字典 | `BookOpen` | 打开的书籍隐喻"字典/参考" | | `users` | 用户管理 | `Users` | 用户群像标准图标,保持不变 | | `roles` | 角色权限 | `UserCog` | 用户+齿轮表达"用户配置" | | `instance-groups` | 权限组 | `Shield` | 盾牌隐喻"权限保护",替代原 Lock | | `workflow` | 审批流程 | `GitBranch` | 分支流程图标,保持不变 | | `audit-logs` | 审计日志 | `FileText` | 文件记录标准图标,保持不变 | | `auto-task` | 自动任务 | `Clock` | 时钟隐喻"定时/自动化",保持不变 | | `announcements` | 系统公告 | `Megaphone` | 喇叭隐喻"广播/公告" | | `settings` | 系统设置 | `Settings` | 齿轮标准图标,保持不变 | | `profile` | 个人中心 | `UserCircle` | 圆形用户头像隐喻"个人信息" | #### 6.2.3 图标动态导入实现 为避免一次性导入全部图标导致的包体积膨胀,采用字符串名称 + 动态导入方案: ```typescript // /frontend/src/constants/menu.ts import * as Icons from 'lucide-react'; import type { LucideProps } from 'lucide-react'; import React from 'react'; /** 图标名称到组件的动态映射 */ export const getMenuIcon = ( iconName: string ): React.ComponentType | null => { const Icon = (Icons as Record>)[iconName]; return Icon || null; }; /** 在 SidebarMenuItem 组件中的使用方式 */ // const IconComponent = getMenuIcon(item.icon); // {IconComponent && } ``` #### 6.2.4 图标颜色与尺寸规范 ```css /* /frontend/src/styles/menu-icons.css */ /* ─── 基础尺寸 ─── */ .menu-icon { width: 18px; height: 18px; flex-shrink: 0; transition: color 150ms ease, transform 150ms ease; } /* ─── 颜色状态 ─── */ .menu-icon--default { color: var(--dg-text-secondary); /* #9ca3af */ } .menu-item:hover .menu-icon--default { color: var(--dg-text-primary); /* #e5e7eb */ transform: scale(1.05); } .menu-icon--active { color: rgb(96 165 250); /* text-blue-400 */ } /* ─── 分组标题图标(如使用)─── */ .menu-section-icon { color: var(--dg-text-muted); /* #6b7280 */ width: 16px; height: 16px; } /* ─── 角标容器 ─── */ .menu-badge { min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; font-size: 11px; font-weight: 600; background-color: rgb(239 68 68); /* red-500 */ color: white; display: inline-flex; align-items: center; justify-content: center; margin-left: auto; animation: badge-pulse 2s ease-in-out infinite; } @keyframes badge-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } } ``` #### 6.2.5 设置页 emoji 替换方案 原设置页面中使用了以下 emoji 图标,需全部替换为 lucide-react 对应图标: | 设置项 | 原 emoji | 替换为 Lucide 图标 | 图标名称 | |--------|----------|-------------------|----------| | 外观主题 | 🎨 | `Palette` | Palette | | 平台设置 | ⚙️ | `SlidersHorizontal` | SlidersHorizontal | | 安全设置 | 🛡️ | `ShieldCheck` | ShieldCheck | | 通知设置 | 🔔 | `Bell` | Bell | | 密码修改 | 🔐 | `KeyRound` | KeyRound | 替换后设置页面的图标导入: ```typescript // /frontend/src/pages/settings/SettingsPage.tsx import { Palette, SlidersHorizontal, ShieldCheck, Bell, KeyRound, } from 'lucide-react'; const settingsGroups = [ { id: 'appearance', label: '外观主题', icon: Palette }, { id: 'platform', label: '平台设置', icon: SlidersHorizontal }, { id: 'security', label: '安全设置', icon: ShieldCheck }, { id: 'notification', label: '通知设置', icon: Bell }, { id: 'password', label: '密码修改', icon: KeyRound }, ]; ``` --- ### 6.3 权限映射优化 #### 6.3.1 从角色到权限的范式迁移 原方案使用 `roles: ['admin', 'dba', 'developer', 'auditor']` 这种粗粒度的角色数组控制菜单可见性,存在三个结构性缺陷:角色名称硬编码,新增角色需修改多处代码;同一角色内不同用户的菜单可见性无法进一步细分;无法支持基于RBAC的动态权限变更。 新方案引入 `permissions: PermissionCode[]` 数组,每个菜单项声明所需的细粒度权限码。用户登录时后端返回该用户的权限码列表,前端通过权限交集运算判断菜单可见性。此设计遵循"后端控制权限,前端控制展示"的分层原则。 #### 6.3.2 权限码命名规范 权限码采用 `resource:action` 的标准格式: ```typescript // /frontend/src/constants/permissions.ts /** 权限码完整定义 */ export const PERMISSIONS = { // 工作台 DASHBOARD_VIEW: 'dashboard:view' as const, TODO_VIEW: 'todo:view' as const, QUERY_QUICK_ACCESS:'query:quick_access' as const, // 数据操作 TICKET_VIEW: 'ticket:view' as const, TICKET_CREATE: 'ticket:create' as const, TICKET_APPROVE: 'ticket:approve' as const, QUERY_VIEW: 'query:view' as const, QUERY_CREATE: 'query:create' as const, SQL_ASSISTANT_USE: 'sql_assistant:use' as const, // 数据源 DATASOURCE_VIEW: 'datasource:view' as const, DATASOURCE_MANAGE: 'datasource:manage' as const, DATA_DICT_VIEW: 'data_dict:view' as const, // 权限中心 USER_VIEW: 'user:view' as const, USER_MANAGE: 'user:manage' as const, ROLE_VIEW: 'role:view' as const, ROLE_MANAGE: 'role:manage' as const, PERM_GROUP_VIEW: 'perm_group:view' as const, PERM_GROUP_MANAGE: 'perm_group:manage' as const, WORKFLOW_VIEW: 'workflow:view' as const, WORKFLOW_MANAGE: 'workflow:manage' as const, // 运维中心 AUDIT_LOG_VIEW: 'audit_log:view' as const, AUTO_TASK_VIEW: 'auto_task:view' as const, AUTO_TASK_MANAGE: 'auto_task:manage' as const, ANNOUNCEMENT_VIEW: 'announcement:view' as const, // 平台设置 SETTINGS_SYSTEM: 'settings:system' as const, SETTINGS_PERSONAL: 'settings:personal' as const, } as const; /** 权限码联合类型 —— 从常量对象推导 */ export type PermissionCode = typeof PERMISSIONS[keyof typeof PERMISSIONS]; /** 角色到权限的映射模板 —— 后端可动态调整 */ export const ROLE_PERMISSION_TEMPLATES: Record = { admin: Object.values(PERMISSIONS), dba: [ PERMISSIONS.DASHBOARD_VIEW, PERMISSIONS.TODO_VIEW, PERMISSIONS.TICKET_VIEW, PERMISSIONS.TICKET_APPROVE, PERMISSIONS.QUERY_VIEW, PERMISSIONS.QUERY_CREATE, PERMISSIONS.SQL_ASSISTANT_USE, PERMISSIONS.DATASOURCE_VIEW, PERMISSIONS.DATASOURCE_MANAGE, PERMISSIONS.DATA_DICT_VIEW, PERMISSIONS.WORKFLOW_VIEW, PERMISSIONS.AUDIT_LOG_VIEW, PERMISSIONS.AUTO_TASK_VIEW, PERMISSIONS.ANNOUNCEMENT_VIEW, PERMISSIONS.SETTINGS_PERSONAL, ], developer: [ PERMISSIONS.DASHBOARD_VIEW, PERMISSIONS.TICKET_VIEW, PERMISSIONS.TICKET_CREATE, PERMISSIONS.QUERY_VIEW, PERMISSIONS.QUERY_CREATE, PERMISSIONS.SQL_ASSISTANT_USE, PERMISSIONS.DATASOURCE_VIEW, PERMISSIONS.DATA_DICT_VIEW, PERMISSIONS.WORKFLOW_VIEW, PERMISSIONS.ANNOUNCEMENT_VIEW, PERMISSIONS.SETTINGS_PERSONAL, ], auditor: [ PERMISSIONS.DASHBOARD_VIEW, PERMISSIONS.TICKET_VIEW, PERMISSIONS.QUERY_VIEW, PERMISSIONS.AUDIT_LOG_VIEW, PERMISSIONS.ANNOUNCEMENT_VIEW, PERMISSIONS.SETTINGS_PERSONAL, ], }; ``` #### 6.3.3 菜单可见性判断 Hook ```typescript // /frontend/src/hooks/useMenuPermissions.ts import { useMemo } from 'react'; import { useAuth } from '@/contexts/AuthContext'; import { menuSections, menuItems, filterMenuByPermissions, getMenuItemsBySection, } from '@/constants/menu'; import type { MenuItem, MenuSection } from '@/constants/menu'; export interface VisibleMenuGroup { section: MenuSection; items: MenuItem[]; } /** 获取当前用户可见的菜单结构 */ export const useVisibleMenu = (): VisibleMenuGroup[] => { const { userPermissions } = useAuth(); // 从 AuthContext 获取用户权限码数组 return useMemo(() => { // 按 order 排序分组 const sortedSections = [...menuSections].sort((a, b) => a.order - b.order); return sortedSections .map((section) => { const sectionItems = getMenuItemsBySection(section.id); const visibleItems = filterMenuByPermissions(sectionItems, userPermissions); return { section, items: visibleItems }; }) .filter((group) => group.items.length > 0); // 过滤空分组 }, [userPermissions]); }; /** 判断用户是否拥有某个菜单项的访问权限 */ export const useHasMenuPermission = (menuItemId: string): boolean => { const { userPermissions } = useAuth(); const item = menuItems.find((m) => m.id === menuItemId); if (!item) return false; return item.permissions.some((perm) => userPermissions.includes(perm)); }; /** 获取动态角标数据 */ export const useMenuBadges = (): Record => { const { badgeData } = useAuth(); // 从 AuthContext 获取角标数据 return useMemo(() => { const result: Record = {}; menuItems.forEach((item) => { if (item.badge && badgeData[item.badge] !== undefined) { result[item.id] = badgeData[item.badge]; } }); return result; }, [badgeData]); }; ``` #### 6.3.4 动态角标设计 菜单角标实时展示待处理事项数量,提醒用户关注。角标数据通过 WebSocket 或轮询接口从后端获取: ```typescript // /frontend/src/types/badge.ts /** 角标数据源类型 */ export interface BadgeData { pending_count: number; // 待审批工单数 unread_count: number; // 未读消息数 task_fail_count: number; // 失败自动任务数 announcement_count: number; // 未读公告数 } // /frontend/src/hooks/useBadgePolling.ts import { useQuery } from '@tanstack/react-query'; import type { BadgeData } from '@/types/badge'; const POLLING_INTERVAL = 60_000; // 60秒轮询 export const useBadgePolling = () => { return useQuery({ queryKey: ['menu-badges'], queryFn: async () => { const res = await fetch('/api/v1/badges'); if (!res.ok) throw new Error('Failed to fetch badges'); return res.json(); }, refetchInterval: POLLING_INTERVAL, staleTime: 30_000, }); }; ``` 角标在侧边栏的渲染逻辑: ```tsx // 角标渲染(嵌入 SidebarMenuItem 组件中) {badgeCount !== undefined && badgeCount > 0 && ( 99 ? 'bg-red-600' : 'bg-red-500'} text-white animate-[badge-pulse_2s_ease-in-out_infinite] `}> {badgeCount > 99 ? '99+' : badgeCount} )} ``` #### 6.3.5 权限迁移路径 | 阶段 | 操作 | 时间窗口 | |------|------|----------| | 第一阶段(并行期) | 同时保留 `roles` 和 `permissions` 字段,以 roles 为 fallback | 1-2 个迭代 | | 第二阶段(切换期) | 后端全面返回 permissions,前端以 permissions 为主 | 1 个迭代 | | 第三阶段(清理期) | 移除 roles 字段,权限判断完全基于 permissions | 1 个迭代 | --- ### 6.4 面包屑导航补充 #### 6.4.1 设计目标与定位 面包屑导航填补当前平台在"位置感知"维度的空白。当用户通过深层链接(如审批流程详情页)进入页面时,面包屑提供清晰的路径回溯能力。设计遵循三个原则:路径语义化(显示可读标签而非路由路径)、可交互(支持点击跳转)、与侧边栏激活状态联动。 #### 6.4.2 路径映射规则 面包屑的数据源不是路由配置本身,而是一个独立的路径映射表。这种解耦设计使面包屑标签与 URL 路径分离,便于国际化和文案调整。 ```typescript // /frontend/src/constants/breadcrumb.ts /** 面包屑路径映射项 */ export interface BreadcrumbMapping { path: string; // 路由路径(支持参数,如 /tickets/:id) label: string; // 面包屑显示文本 parent?: string; // 父级路径(用于构建完整面包屑链) icon?: string; // 可选的 lucide 图标名称 } /** 面包屑路径映射表 —— 按深度从浅到深排列 */ export const breadcrumbMappings: BreadcrumbMapping[] = [ // 工作台 { path: '/dashboard', label: '仪表盘', icon: 'LayoutDashboard' }, { path: '/my-todo', label: '我的待办', icon: 'CheckCircle2' }, { path: '/quick-queries',label: '快速查询', icon: 'Zap' }, // 数据操作 { path: '/tickets', label: '数据变更', icon: 'ClipboardList' }, { path: '/tickets/:id', label: '工单详情', parent: '/tickets' }, { path: '/queries', label: '查询数据', icon: 'Search' }, { path: '/queries/:id', label: '查询详情', parent: '/queries' }, { path: '/sql-assistant',label: 'SQL助手', icon: 'Wand2' }, // 数据源 { path: '/instances', label: '数据源管理', icon: 'Database' }, { path: '/instances/:id',label: '数据源详情', parent: '/instances' }, { path: '/data-dict', label: '数据字典', icon: 'BookOpen' }, // 权限中心 { path: '/users', label: '用户管理', icon: 'Users' }, { path: '/users/:id', label: '用户详情', parent: '/users' }, { path: '/roles', label: '角色权限', icon: 'UserCog' }, { path: '/instance-groups', label: '权限组', icon: 'Shield' }, { path: '/workflow', label: '审批流程', icon: 'GitBranch' }, // 运维中心 { path: '/audit-logs', label: '审计日志', icon: 'FileText' }, { path: '/auto-task', label: '自动任务', icon: 'Clock' }, { path: '/announcements',label: '系统公告', icon: 'Megaphone' }, // 平台设置 { path: '/settings', label: '系统设置', icon: 'Settings' }, { path: '/profile', label: '个人中心', icon: 'UserCircle' }, ]; ``` #### 6.4.3 面包屑构建工具函数 ```typescript // /frontend/src/utils/breadcrumb.ts import { breadcrumbMappings } from '@/constants/breadcrumb'; import type { BreadcrumbMapping } from '@/constants/breadcrumb'; export interface BreadcrumbItem { label: string; path: string; icon?: string; isActive: boolean; // 是否为当前页(不可点击) } /** 将路径模板匹配到实际路径 */ const matchPath = (template: string, actualPath: string): boolean => { const templateParts = template.split('/'); const actualParts = actualPath.split('/'); if (templateParts.length !== actualParts.length) return false; return templateParts.every((part, i) => { if (part.startsWith(':')) return true; // 参数段通配 return part === actualParts[i]; }); }; /** 根据当前路径构建面包屑链 */ export const buildBreadcrumbs = (currentPath: string): BreadcrumbItem[] => { const chain: BreadcrumbItem[] = []; const visited = new Set(); // 查找当前路径的映射 let currentMapping = breadcrumbMappings.find((m) => matchPath(m.path, currentPath) ); // 逐层向上追溯父级 while (currentMapping && !visited.has(currentMapping.path)) { visited.add(currentMapping.path); chain.unshift({ label: currentMapping.label, path: currentPath, // 当前页使用实际路径 icon: currentMapping.icon, isActive: chain.length === 0, }); if (currentMapping.parent) { currentMapping = breadcrumbMappings.find( (m) => m.path === currentMapping!.parent ); } else { break; } } return chain; }; /** 获取面包屑项的目标路径(处理参数化路径) */ export const resolveBreadcrumbPath = ( mapping: BreadcrumbMapping, currentPath: string ): string => { if (!mapping.path.includes(':')) return mapping.path; // 从当前路径提取参数值 const templateParts = mapping.path.split('/'); const currentParts = currentPath.split('/'); const resolvedParts = templateParts.map((part, i) => { if (part.startsWith(':')) return currentParts[i] || part; return part; }); return resolvedParts.join('/'); }; ``` #### 6.4.4 面包屑 React 组件 ```tsx // /frontend/src/components/layout/Breadcrumb.tsx import React from 'react'; import { Link, useLocation } from 'react-router-dom'; import { ChevronRight, Home } from 'lucide-react'; import { buildBreadcrumbs, resolveBreadcrumbPath } from '@/utils/breadcrumb'; import { breadcrumbMappings } from '@/constants/breadcrumb'; import { getMenuIcon } from '@/constants/menu'; export const Breadcrumb: React.FC = () => { const location = useLocation(); const items = buildBreadcrumbs(location.pathname); if (items.length <= 1) { // 顶层页面不显示面包屑 return null; } return ( ); }; ``` #### 6.4.5 面包屑与布局的集成 ```tsx // /frontend/src/components/layout/AppLayout.tsx —— 集成面包屑后的布局 import React from 'react'; import { Sidebar } from './Sidebar'; import { Breadcrumb } from './Breadcrumb'; import { TopBar } from './TopBar'; export const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children, }) => { return (
{/* 侧边栏 */} {/* 主内容区 */}
{/* 顶部栏 */} {/* 面包屑导航 */} {/* 页面内容 */}
{children}
); }; ``` #### 6.4.6 面包屑设计规范 | 设计要素 | 规范值 | 说明 | |----------|--------|------| | 字体大小 | `text-sm` (14px) | 小于页面标题,大于辅助文本 | | 字体颜色(默认) | `var(--dg-text-secondary)` | 非当前页 | | 字体颜色(当前页) | `var(--dg-text-primary)` | 加粗高亮 | | 图标尺寸 | 14px | 小于侧边栏图标,弱化层级 | | 分隔符 | `ChevronRight` 14px | 统一右箭头 | | 垂直间距 | `py-3` (12px) | 紧凑布局 | | 水平间距 | `px-6` (24px) | 与页面内容对齐 | | 最大显示层级 | 4 级 | 超过则折叠中间层级 | | 悬停交互 | 下划线 + 颜色过渡 | `transition-colors duration-150` | 面包屑的显示逻辑遵循"至少两级才展示"原则——当用户位于仪表盘等顶层页面时面包屑自动隐藏,避免冗余信息干扰。对于超过四级的深层路径,中间层级折叠为"..."下拉菜单,保持导航栏的简洁可读性。 --- ### 6.5 菜单 Hover/Active 动效设计 #### 6.5.1 动画参数规范 ```css /* /frontend/src/styles/menu-animations.css */ /* ─── 菜单项基础 ─── */ .menu-item { position: relative; display: flex; align-items: center; gap: 10px; padding: 9px 14px; margin: 2px 8px; border-radius: 8px; font-size: 13px; font-weight: 450; color: var(--dg-text-secondary); cursor: pointer; transition: background-color 180ms cubic-bezier(0.4, 0, 0.2, 1), color 150ms cubic-bezier(0.4, 0, 0.2, 1), transform 120ms cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; } /* ─── Hover 状态 ─── */ .menu-item:hover { background-color: rgba(255, 255, 255, 0.04); color: var(--dg-text-primary); transform: translateX(2px); } /* ─── Active 状态 ─── */ .menu-item--active { background-color: rgb(59 130 246 / 0.10); /* blue-500/10 */ color: rgb(96 165 250); /* blue-400 */ font-weight: 550; } /* 左侧指示条 */ .menu-item--active::before { content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 3px; height: 18px; background-color: rgb(96 165 250); /* blue-400 */ border-radius: 0 2px 2px 0; animation: indicator-slide-in 200ms cubic-bezier(0.4, 0, 0.2, 1); } @keyframes indicator-slide-in { from { height: 0; opacity: 0; } to { height: 18px; opacity: 1; } } /* ─── 点击反馈 ─── */ .menu-item:active { transform: scale(0.98); transition-duration: 80ms; } /* ─── 菜单分组标题 ─── */ .menu-section-title { padding: 16px 14px 6px; font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--dg-text-muted); user-select: none; } ``` #### 6.5.2 动画设计决策说明 | 动画 | 时长 | 缓动函数 | 设计意图 | |------|------|----------|----------| | Hover 背景色 | 180ms | `cubic-bezier(0.4, 0, 0.2, 1)` | Material Design 标准缓动,自然流畅 | | Hover 位移 | 120ms | 同上 | 轻微右移引导视觉向右,暗示可点击 | | 文字颜色 | 150ms | 同上 | 略快于背景色,形成层次 | | 指示条展开 | 200ms | 同上 | 从0到18px高度,带有生长感 | | 点击缩放 | 80ms | 线性 | 快速反馈,确认操作被接收 | 所有动画均采用 GPU 加速属性(`transform`、`opacity`、`background-color`),确保在 60fps 下流畅运行,不会造成布局抖动。 --- ### 6.6 收藏/快捷访问功能 #### 6.6.1 功能设计 "快速查询"菜单项(位于工作台分组)承载收藏/快捷访问功能。用户可在任何页面点击顶部栏的星形图标将当前页面加入收藏,收藏项会出现在快速查询的下拉面板中。 #### 6.6.2 数据模型与存储 ```typescript // /frontend/src/types/favorite.ts export interface FavoriteItem { id: string; // 唯一标识 label: string; // 显示名称(用户可编辑) path: string; // 路由路径 icon: string; // lucide 图标名称 createdAt: string; // ISO 时间戳 order: number; // 自定义排序 } /** 收藏数据持久化 —— localStorage + 云端同步 */ const FAVORITES_STORAGE_KEY = 'sql_platform_favorites'; export const favoriteStorage = { getAll(): FavoriteItem[] { try { const raw = localStorage.getItem(FAVORITES_STORAGE_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; } }, save(items: FavoriteItem[]): void { localStorage.setItem(FAVORITES_STORAGE_KEY, JSON.stringify(items)); }, add(item: Omit): FavoriteItem[] { const items = this.getAll(); if (items.some((f) => f.path === item.path)) return items; // 去重 const newItem: FavoriteItem = { ...item, order: items.length, createdAt: new Date().toISOString(), }; const updated = [...items, newItem]; this.save(updated); return updated; }, remove(id: string): FavoriteItem[] { const items = this.getAll().filter((f) => f.id !== id); this.save(items); return items; }, reorder(id: string, newOrder: number): FavoriteItem[] { const items = this.getAll(); const item = items.find((f) => f.id === id); if (!item) return items; item.order = newOrder; this.save(items.sort((a, b) => a.order - b.order)); return items; }, }; ``` --- ### 6.7 实施优先级与验收标准 #### 6.7.1 实施优先级 | 优先级 | 模块 | 预估工时 | 依赖项 | |--------|------|----------|--------| | P0 | 6.1 菜单结构重组 | 4h | 无 | | P0 | 6.2 图标体系规范化 | 3h | 6.1 | | P1 | 6.3 权限映射优化 | 6h | 后端接口配合 | | P1 | 6.4 面包屑导航 | 4h | 6.1 | | P2 | 6.5 菜单动效 | 2h | 6.1, 6.2 | | P2 | 6.6 收藏功能 | 4h | 无 | #### 6.7.2 验收标准 - [ ] 侧边栏展示重组后的6个分组,无空分组显示 - [ ] 全部菜单项使用 lucide-react 图标,系统无 emoji 图标残留 - [ ] 菜单可见性基于 permissions 数组判断,roles 字段已移除 - [ ] 面包屑导航在二级及以上页面正确显示路径链 - [ ] 菜单项 Hover/Active 动效流畅,无掉帧 - [ ] 动态角标正确显示待处理数量,超过99显示"99+" - [ ] 设置页面 emoji 已全部替换为 lucide 图标 --- ### 6.8 本章小结 本章从菜单结构重组、图标规范化、权限映射优化和面包屑导航四个维度对 SQL-Platform 的导航体系进行了系统性重构。核心改进包括:将原有6个分组重组为语义更清晰的工作台/数据操作/数据源/权限中心/运维中心/平台设置分组;建立了统一的 lucide-react 图标体系,彻底移除了 emoji 图标;引入 `PermissionCode` 细粒度权限码替代粗粒度的角色数组;补充了面包屑导航完善位置感知能力。这些改进使导航体系从"可用"迈向"好用",为平台的长期功能扩展奠定了坚实的交互基础。