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

1137 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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<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 图标颜色与尺寸规范
```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<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
```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<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 或轮询接口从后端获取:
```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<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,
});
};
```
角标在侧边栏的渲染逻辑:
```tsx
// 角标渲染(嵌入 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 路径分离,便于国际化和文案调整。
```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<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 组件
```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 (
<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 面包屑与布局的集成
```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 (
<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 动画参数规范
```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, '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` 细粒度权限码替代粗粒度的角色数组;补充了面包屑导航完善位置感知能力。这些改进使导航体系从"可用"迈向"好用",为平台的长期功能扩展奠定了坚实的交互基础。