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
140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
import { useLocation, useNavigate } from 'react-router-dom';
|
|
import { ChevronRight, Home } from 'lucide-react';
|
|
import { menuItems, menuSections } from '@/constants/menu';
|
|
|
|
interface BreadcrumbItem {
|
|
label: string;
|
|
path?: string;
|
|
}
|
|
|
|
interface BreadcrumbProps {
|
|
items: BreadcrumbItem[];
|
|
}
|
|
|
|
export default function Breadcrumb({ items }: BreadcrumbProps) {
|
|
const navigate = useNavigate();
|
|
|
|
if (items.length === 0) return null;
|
|
|
|
return (
|
|
<nav aria-label="breadcrumb" className="flex items-center text-xs">
|
|
<ol className="flex items-center gap-1">
|
|
{items.map((item, index) => {
|
|
const isLast = index === items.length - 1;
|
|
|
|
return (
|
|
<li key={index} className="flex items-center gap-1">
|
|
{index > 0 && (
|
|
<ChevronRight className="w-3 h-3 text-[var(--dg-text-muted)] flex-shrink-0" />
|
|
)}
|
|
{isLast ? (
|
|
<span className="text-[var(--dg-text-primary)] font-medium">
|
|
{item.label}
|
|
</span>
|
|
) : item.path ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => navigate(item.path!)}
|
|
className="text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] transition-colors inline-flex items-center gap-1 cursor-pointer"
|
|
>
|
|
{index === 0 && <Home className="w-3 h-3" />}
|
|
{item.label}
|
|
</button>
|
|
) : (
|
|
<span className="text-[var(--dg-text-secondary)]">
|
|
{item.label}
|
|
</span>
|
|
)}
|
|
</li>
|
|
);
|
|
})}
|
|
</ol>
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hook that builds breadcrumb items based on the current route.
|
|
*
|
|
* It matches the current pathname against menuItems, then constructs:
|
|
* Home > Section label > Page label
|
|
*
|
|
* For detail/sub pages (e.g. /instances/:id, /tickets/:id, /tickets/create),
|
|
* it prepends the parent breadcrumb entries.
|
|
*/
|
|
export function useBreadcrumb(): { items: BreadcrumbItem[] } {
|
|
const location = useLocation();
|
|
const pathname = location.pathname;
|
|
|
|
const items: BreadcrumbItem[] = [];
|
|
|
|
// Find a direct menu item match by id (exact route)
|
|
const directMatch = menuItems.find(
|
|
(item) => item.id === pathname.replace(/^\//, '')
|
|
);
|
|
|
|
if (directMatch) {
|
|
// Exact menu match: Home > Section > Page
|
|
const section = menuSections.find((s) => s.id === directMatch.section);
|
|
|
|
items.push({ label: '首页', path: '/dashboard' });
|
|
if (section && section.id !== 'overview') {
|
|
items.push({ label: section.label });
|
|
}
|
|
items.push({ label: directMatch.label });
|
|
return { items };
|
|
}
|
|
|
|
// Parse the pathname for sub/detail pages
|
|
const segments = pathname.split('/').filter(Boolean); // e.g. ['instances', '123'] or ['tickets', 'create']
|
|
if (segments.length === 0) {
|
|
return { items: [{ label: '首页', path: '/dashboard' }] };
|
|
}
|
|
|
|
const rootSegment = segments[0];
|
|
const parentMenuItem = menuItems.find((item) => item.id === rootSegment);
|
|
|
|
if (!parentMenuItem) {
|
|
// Unknown route — just show Home
|
|
return { items: [{ label: '首页', path: '/dashboard' }] };
|
|
}
|
|
|
|
const section = menuSections.find((s) => s.id === parentMenuItem.section);
|
|
|
|
// Home
|
|
items.push({ label: '首页', path: '/dashboard' });
|
|
|
|
// Section (skip for overview section like dashboard)
|
|
if (section && section.id !== 'overview') {
|
|
items.push({ label: section.label });
|
|
}
|
|
|
|
// Parent page (clickable for detail pages)
|
|
if (segments.length > 1) {
|
|
items.push({ label: parentMenuItem.label, path: `/${rootSegment}` });
|
|
}
|
|
|
|
// Sub-page label
|
|
if (segments.length > 1) {
|
|
const subSegment = segments[1];
|
|
const isDetailPage = !isNaN(Number(subSegment));
|
|
|
|
if (isDetailPage) {
|
|
// Numeric ID — use a generic "详情" label
|
|
items.push({ label: '详情' });
|
|
} else {
|
|
// Named sub-pages like 'create'
|
|
const subPageLabels: Record<string, string> = {
|
|
create: '新建',
|
|
edit: '编辑',
|
|
};
|
|
items.push({ label: subPageLabels[subSegment] || subSegment });
|
|
}
|
|
} else {
|
|
// Single segment — this is the current page
|
|
items.push({ label: parentMenuItem.label });
|
|
}
|
|
|
|
return { items };
|
|
}
|