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
This commit is contained in:
1 parent
4e2df4d3d7
commit
746a000e8a
25 files changed
+7718
-178
No files matched your search
@@ -0,0 +1,139 @@
|
||||
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 };
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
LayoutDashboard, Database, Server, Zap, Leaf,
|
||||
ClipboardList, Search, UserCog, Users, Lock, Shield, GitBranch, Bell,
|
||||
LogOut, Settings, Clock, Megaphone, Sun, Moon, BookOpen
|
||||
Code2, FileEdit, Users, ShieldCheck, FolderLock, Shield, GitBranch, Bell,
|
||||
LogOut, Settings, Timer, ScrollText, Megaphone, Sun, Moon, BookOpen,
|
||||
ChevronRight, Monitor
|
||||
} from 'lucide-react';
|
||||
import { menuSections } from '@/constants/menu';
|
||||
import { menuSections, menuItems } from '@/constants/menu';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
|
||||
const iconMap: Record<string, React.ElementType> = {
|
||||
LayoutDashboard, Database, Server, Zap, Leaf,
|
||||
ClipboardList, Search, UserCog, Users, Lock, Shield, GitBranch, Bell,
|
||||
Settings, Clock, Megaphone, BookOpen,
|
||||
Code2, FileEdit, Users, ShieldCheck, FolderLock, Shield, GitBranch, Bell,
|
||||
Settings, Timer, ScrollText, Megaphone, BookOpen,
|
||||
};
|
||||
|
||||
export default function Layout() {
|
||||
@@ -29,10 +30,25 @@ export default function Layout() {
|
||||
.catch(() => {});
|
||||
}, [location.pathname]);
|
||||
|
||||
const getPageTitle = () => {
|
||||
const item = visibleMenuItems.find(m => m.id === currentPath);
|
||||
return item?.label || '仪表盘';
|
||||
};
|
||||
// Build breadcrumb info from current path
|
||||
const breadcrumbInfo = useMemo(() => {
|
||||
const currentItem = menuItems.find(m => m.id === currentPath);
|
||||
if (!currentItem) {
|
||||
return { sectionLabel: '', pageLabel: '仪表盘' };
|
||||
}
|
||||
const section = menuSections.find(s => s.id === currentItem.section);
|
||||
return {
|
||||
sectionLabel: section?.label || '',
|
||||
pageLabel: currentItem.label,
|
||||
};
|
||||
}, [currentPath]);
|
||||
|
||||
const themeOption = useMemo(() => THEME_OPTIONS.find(o => o.id === theme), [theme]);
|
||||
const themeIcon = useMemo(() => {
|
||||
if (!themeOption) return <Monitor className="w-4 h-4" />;
|
||||
if (themeOption.isDark) return <Moon className="w-4 h-4" />;
|
||||
return <Sun className="w-4 h-4" />;
|
||||
}, [themeOption]);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-[var(--dg-bg-primary)] text-[var(--dg-text-primary)] overflow-hidden">
|
||||
@@ -68,16 +84,33 @@ export default function Layout() {
|
||||
key={item.id}
|
||||
onClick={() => navigate('/' + item.id)}
|
||||
className={
|
||||
'flex items-center mx-2 px-3 py-2 rounded-lg cursor-pointer transition-all duration-150 relative text-[13px] '
|
||||
'group flex items-center mx-2 px-3 py-2 rounded-lg cursor-pointer '
|
||||
+ 'transition-all duration-200 relative text-[13px] '
|
||||
+ (isActive
|
||||
? 'bg-blue-500/10 text-blue-400 font-medium'
|
||||
: 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)]')
|
||||
? 'bg-[var(--dg-blue)]/10 text-[var(--dg-blue)] font-medium'
|
||||
: 'text-[var(--dg-text-secondary)] '
|
||||
+ 'hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] '
|
||||
+ 'hover:ml-0.5')
|
||||
}
|
||||
>
|
||||
{/* Active indicator bar - gradient with glow */}
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-4 bg-blue-400 rounded-r" />
|
||||
<div
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 rounded-r animate-[slideInLeft_200ms_ease-out]"
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, var(--dg-blue), var(--dg-purple, var(--dg-blue)))',
|
||||
boxShadow: '0 0 8px 1px var(--dg-blue), 0 0 16px 2px color-mix(in srgb, var(--dg-blue) 40%, transparent)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Icon className="w-4 h-4 flex-shrink-0" />
|
||||
<Icon
|
||||
className={
|
||||
'w-4 h-4 flex-shrink-0 transition-colors duration-200 '
|
||||
+ (isActive
|
||||
? 'text-[var(--dg-blue)]'
|
||||
: 'group-hover:text-[var(--dg-blue)]')
|
||||
}
|
||||
/>
|
||||
<span className="ml-2.5">{item.label}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -92,27 +125,44 @@ export default function Layout() {
|
||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden">
|
||||
{/* 顶部栏 */}
|
||||
<header className="h-14 bg-[var(--dg-bg-secondary)] border-b border-[var(--dg-border)] flex items-center justify-between px-5 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-[var(--dg-text-muted)]">{getPageTitle()}</span>
|
||||
{/* Breadcrumb area */}
|
||||
<div className="flex items-center gap-1.5 text-sm">
|
||||
<span className="text-[var(--dg-text-muted)] font-medium">SQL-Platform</span>
|
||||
{breadcrumbInfo.sectionLabel && (
|
||||
<>
|
||||
<ChevronRight className="w-3.5 h-3.5 text-[var(--dg-text-muted)] opacity-50" />
|
||||
<span className="text-[var(--dg-text-muted)] opacity-70">{breadcrumbInfo.sectionLabel}</span>
|
||||
</>
|
||||
)}
|
||||
<ChevronRight className="w-3.5 h-3.5 text-[var(--dg-text-muted)] opacity-50" />
|
||||
<span className="text-[var(--dg-text-primary)] font-medium">{breadcrumbInfo.pageLabel}</span>
|
||||
</div>
|
||||
|
||||
{/* Right actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Theme toggle with tooltip */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] transition-colors"
|
||||
title={theme === 'dark' ? '浅色模式' : '深色模式'}
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] transition-all duration-200"
|
||||
title={`当前主题: ${themeOption?.name || theme}\n点击切换下一个主题`}
|
||||
>
|
||||
{theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
|
||||
{themeIcon}
|
||||
</button>
|
||||
|
||||
{/* Notification bell */}
|
||||
<button onClick={() => navigate('/tickets')}
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] transition-colors relative">
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] hover:text-[var(--dg-text-primary)] transition-all duration-200 relative">
|
||||
<Bell className="w-4 h-4" />
|
||||
{pendingCount > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 px-1 bg-red-500 rounded-full text-[10px] text-white font-bold flex items-center justify-center">
|
||||
<span className="absolute -top-0.5 -right-0.5 min-w-[16px] h-4 px-1 bg-red-500 rounded-full text-[10px] text-white font-bold flex items-center justify-center animate-pulse">
|
||||
{pendingCount > 99 ? '99+' : pendingCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="w-px h-5 bg-[var(--dg-border)] mx-1" />
|
||||
|
||||
{/* User area */}
|
||||
<div className="flex items-center gap-2 pl-1">
|
||||
<div className="w-7 h-7 rounded-full dg-gradient-br flex items-center justify-center text-white text-xs font-semibold flex-shrink-0">
|
||||
{user.avatar}
|
||||
@@ -120,7 +170,7 @@ export default function Layout() {
|
||||
<span className="text-sm text-[var(--dg-text-secondary)] max-w-[80px] truncate">{user.username}</span>
|
||||
<button
|
||||
onClick={() => { logout(); navigate('/login'); }}
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-red-500/10 hover:text-red-400 transition-colors"
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--dg-text-muted)] hover:bg-red-500/10 hover:text-red-400 transition-colors duration-200"
|
||||
title="退出登录"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
@@ -134,6 +184,22 @@ export default function Layout() {
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Keyframe animation for active indicator */}
|
||||
<style>{`
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%) scaleX(0);
|
||||
transform-origin: left;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scaleX(1);
|
||||
transform-origin: left;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
|
||||
export default function CenteredCard() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { toggleTheme, isDark } = useTheme();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, position: 'relative' }}>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
|
||||
export default function FullImage() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { toggleTheme, isDark } = useTheme();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, position: 'relative', overflow: 'hidden' }}>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
import { Sun, Moon, Database } from 'lucide-react';
|
||||
|
||||
export default function MinimalDark() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { toggleTheme, isDark } = useTheme();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 24, background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
import { Sun, Moon, Database } from 'lucide-react';
|
||||
|
||||
export default function SidePanel() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { toggleTheme, isDark } = useTheme();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLogin } from '@/hooks/useLogin';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useTheme, THEME_OPTIONS } from '@/hooks/useTheme';
|
||||
import { Sun, Moon, Database, Code, GitBranch, Bell, History, UserCog } from 'lucide-react';
|
||||
|
||||
const features = [
|
||||
@@ -13,7 +13,8 @@ const features = [
|
||||
|
||||
export default function SplitView() {
|
||||
const { username, password, loading, error, setUsername, setPassword, handleLogin } = useLogin();
|
||||
const { theme, toggleTheme, isDark } = useTheme();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const isDark = THEME_OPTIONS.find(o => o.id === theme)?.isDark ?? true;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', minHeight: '100vh', background: 'var(--dg-bg-primary)', color: 'var(--dg-text-primary)' }}>
|
||||
|
||||
Reference in new issue
Block a user