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