Files
opsxsql/SQL平台评估与优化/sql_platform_optimization_sec06.md
T
logan 746a000e8a feat: 全面UI优化 — 5套主题系统 + 设计体系 + 菜单重组 + 页面优化
1. 主题系统扩展 (5套主题)
   - ocean-deep(深海蓝) / light-professional(明亮白) / midnight-purple(午夜紫)
   - pine-green(松林绿) / orange-vitality(活力橙)
   - useTheme hook 重写支持5主题 + 向后兼容
   - AppearanceSettings 5主题预览卡片

2. 设计体系 (index.css)
   - 按钮系统: dg-btn-primary/secondary/ghost/danger + 尺寸变体
   - 徽章系统: dg-badge-* (8色)
   - 卡片/表格: dg-card-section, dg-table-row, dg-table-header
   - 状态点: dg-status-dot-* (4色)
   - 动效关键帧: slideInLeft, scaleIn

3. 菜单重组 (menu.ts)
   - 5分组: 概览/数据源/工单/权限/系统
   - 6个图标更新: Code2/FileEdit/ShieldCheck/FolderLock/ScrollText/Timer
   - 新增 permission 权限码字段

4. Layout 侧边栏增强
   - 左侧渐变指示条 + 发光效果
   - hover 微动画 + 图标变色
   - 面包屑导航头部
   - 主题切换循环5主题

5. 核心页面优化
   - TicketsPage: 设计系统类 + 颜色对齐
   - QueriesPage: 去Card依赖 + dg-table/dg-btn统一
   - Breadcrumb组件 + useBreadcrumb hook
2026-05-27 01:59:08 +08:00

35 KiB
Raw Blame History

6. 菜单体系全面优化

菜单导航是SQL-Platform最核心的交互骨架,直接影响用户的工作流效率和平台整体体验。当前菜单体系存在分组逻辑不够清晰、图标体系不统一、权限控制粒度较粗、缺少面包屑导航等结构性问题。本章将从菜单结构重组、图标规范化、权限映射优化和面包屑导航四个维度进行系统性重构。


6.1 菜单结构重组方案

6.1.1 重组设计思路

当前菜单的六个分组存在三个核心问题:其一,"配置平台"分组仅包含"系统设置"一项,空间利用率极低;其二,"自动任务"归类于"系统配置"不够合理,它与运维监控的关联度远高于系统配置;其三,"工单管理"的命名偏向内部管理视角,对实际执行数据操作的开发者而言不够直观。

重组方案遵循"以用户角色为中心、以任务流为导向"的原则,将原有6个分组精简为6个更具语义清晰度的分组。核心调整包括:将"概览"升级为"工作台"并增加快捷入口;将"工单管理"重命名为"数据操作";将"自动任务"移至"运维中心";将孤立的"系统设置"与新增"个人中心"组成"平台设置"。

6.1.2 优化后的完整菜单定义

// /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 图标动态导入实现

为避免一次性导入全部图标导致的包体积膨胀,采用字符串名称 + 动态导入方案:

// /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<LucideProps> | null => {
  const Icon = (Icons as Record<string, React.ComponentType<LucideProps>>)[iconName];
  return Icon || null;
};

/** 在 SidebarMenuItem 组件中的使用方式 */
// const IconComponent = getMenuIcon(item.icon);
// {IconComponent && <IconComponent className={iconClassNames} size={18} />}

6.2.4 图标颜色与尺寸规范

/* /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

替换后设置页面的图标导入:

// /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 的标准格式:

// /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<string, PermissionCode[]> = {
  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

// /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<string, number> => {
  const { badgeData } = useAuth(); // 从 AuthContext 获取角标数据

  return useMemo(() => {
    const result: Record<string, number> = {};
    menuItems.forEach((item) => {
      if (item.badge && badgeData[item.badge] !== undefined) {
        result[item.id] = badgeData[item.badge];
      }
    });
    return result;
  }, [badgeData]);
};

6.3.4 动态角标设计

菜单角标实时展示待处理事项数量,提醒用户关注。角标数据通过 WebSocket 或轮询接口从后端获取:

// /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<BadgeData>({
    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,
  });
};

角标在侧边栏的渲染逻辑:

// 角标渲染(嵌入 SidebarMenuItem 组件中)
{badgeCount !== undefined && badgeCount > 0 && (
  <span className={`
    ml-auto inline-flex items-center justify-center
    min-w-[18px] h-[18px] px-[5px] rounded-full
    text-[11px] font-semibold
    ${badgeCount > 99 ? 'bg-red-600' : 'bg-red-500'}
    text-white
    animate-[badge-pulse_2s_ease-in-out_infinite]
  `}>
    {badgeCount > 99 ? '99+' : badgeCount}
  </span>
)}

6.3.5 权限迁移路径

阶段 操作 时间窗口
第一阶段(并行期) 同时保留 roles 和 permissions 字段,以 roles 为 fallback 1-2 个迭代
第二阶段(切换期) 后端全面返回 permissions,前端以 permissions 为主 1 个迭代
第三阶段(清理期) 移除 roles 字段,权限判断完全基于 permissions 1 个迭代

6.4 面包屑导航补充

6.4.1 设计目标与定位

面包屑导航填补当前平台在"位置感知"维度的空白。当用户通过深层链接(如审批流程详情页)进入页面时,面包屑提供清晰的路径回溯能力。设计遵循三个原则:路径语义化(显示可读标签而非路由路径)、可交互(支持点击跳转)、与侧边栏激活状态联动。

6.4.2 路径映射规则

面包屑的数据源不是路由配置本身,而是一个独立的路径映射表。这种解耦设计使面包屑标签与 URL 路径分离,便于国际化和文案调整。

// /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 面包屑构建工具函数

// /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<string>();

  // 查找当前路径的映射
  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 组件

// /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 (
    <nav
      aria-label="面包屑导航"
      className="flex items-center gap-1 px-6 py-3 text-sm"
    >
      {/* 首页快捷入口 */}
      <Link
        to="/dashboard"
        className="
          flex items-center gap-1
          text-[var(--dg-text-muted)]
          hover:text-[var(--dg-text-primary)]
          transition-colors duration-150
        "
        title="返回仪表盘"
      >
        <Home size={14} />
      </Link>

      {items.length > 0 && (
        <ChevronRight
          size={14}
          className="text-[var(--dg-text-muted)] mx-0.5"
        />
      )}

      {/* 面包屑项 */}
      {items.map((item, index) => {
        const isLast = index === items.length - 1;
        const IconComponent = item.icon ? getMenuIcon(item.icon) : null;

        return (
          <React.Fragment key={`${item.path}-${index}`}>
            <div className="flex items-center gap-1.5">
              {IconComponent && (
                <IconComponent
                  size={14}
                  className={`
                    ${isLast ? 'text-blue-400' : 'text-[var(--dg-text-muted)]'}
                  `}
                />
              )}
              {isLast ? (
                // 当前页:不可点击,高亮显示
                <span
                  className="font-medium text-[var(--dg-text-primary)]"
                  aria-current="page"
                >
                  {item.label}
                </span>
              ) : (
                // 父级:可点击跳转
                <Link
                  to={item.path}
                  className="
                    text-[var(--dg-text-secondary)]
                    hover:text-[var(--dg-text-primary)]
                    hover:underline underline-offset-2
                    transition-colors duration-150
                  "
                >
                  {item.label}
                </Link>
              )}
            </div>

            {!isLast && (
              <ChevronRight
                size={14}
                className="text-[var(--dg-text-muted)] mx-0.5"
              />
            )}
          </React.Fragment>
        );
      })}
    </nav>
  );
};

6.4.5 面包屑与布局的集成

// /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 (
    <div className="flex h-screen bg-[var(--dg-bg-primary)]">
      {/* 侧边栏 */}
      <aside className="w-[220px] flex-shrink-0 border-r border-[var(--dg-border)]">
        <Sidebar />
      </aside>

      {/* 主内容区 */}
      <main className="flex-1 flex flex-col min-w-0 overflow-hidden">
        {/* 顶部栏 */}
        <TopBar />

        {/* 面包屑导航 */}
        <Breadcrumb />

        {/* 页面内容 */}
        <div
          className="
            flex-1 overflow-auto
            px-6 py-4
            scroll-smooth
          "
        >
          {children}
        </div>
      </main>
    </div>
  );
};

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 动画参数规范

/* /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 数据模型与存储

// /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, 'order'>): 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 细粒度权限码替代粗粒度的角色数组;补充了面包屑导航完善位置感知能力。这些改进使导航体系从"可用"迈向"好用",为平台的长期功能扩展奠定了坚实的交互基础。