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
@@ -5,7 +5,6 @@ import {
|
||||
FileCode, Clock, Filter, X, Loader2, Users,
|
||||
ArrowUpDown, Ban, ChevronLeft, AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { statusMap, statusColorMap, dbTypeColorMap } from '@/lib/constants';
|
||||
import { useActionPermission } from '@/hooks/useAuth';
|
||||
import PageHeader from '@/components/PageHeader';
|
||||
@@ -104,6 +103,28 @@ const displayStatusColorMap: Record<string, string> = {
|
||||
rollback: 'bg-orange-500/15 text-orange-400',
|
||||
};
|
||||
|
||||
/** Design-system badge class per status */
|
||||
const dgBadgeStatusMap: Record<string, string> = {
|
||||
pending: 'dg-badge-blue',
|
||||
reviewing: 'dg-badge-blue',
|
||||
approved: 'dg-badge-blue',
|
||||
rejected: 'dg-badge-red',
|
||||
executing: 'dg-badge-orange',
|
||||
success: 'dg-badge-green',
|
||||
failed: 'dg-badge-red',
|
||||
rollback: 'dg-badge-orange',
|
||||
};
|
||||
|
||||
/** Design-system badge class per db type */
|
||||
const dgBadgeDbMap: Record<string, string> = {
|
||||
mysql: 'dg-badge-blue',
|
||||
postgres: 'dg-badge-purple',
|
||||
postgresql: 'dg-badge-purple',
|
||||
redis: 'dg-badge-red',
|
||||
mongo: 'dg-badge-green',
|
||||
mongodb: 'dg-badge-green',
|
||||
};
|
||||
|
||||
const statusFlow = ['pending', 'reviewing', 'approved', 'executing', 'success'];
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -211,13 +232,12 @@ export default function TicketsPage() {
|
||||
return result;
|
||||
}, [tickets, sortAsc]);
|
||||
|
||||
/* ---- Stats Cards ---- */
|
||||
const statusCards = useMemo(() => [
|
||||
{ key: '', label: '全部', count: stats?.total || 0, color: 'text-[var(--dg-text-primary)]', bg: 'bg-[var(--dg-bg-tertiary)]' },
|
||||
{ key: 'pending', label: '待审批', count: (stats?.pending || 0) + (stats?.reviewing || 0) + (stats?.approved || 0), color: 'text-blue-400', bg: 'bg-blue-400/10' },
|
||||
{ key: 'executing', label: '执行中', count: stats?.executing || 0, color: 'text-purple-400', bg: 'bg-purple-400/10' },
|
||||
{ key: 'success', label: '完成', count: stats?.success || 0, color: 'text-green-400', bg: 'bg-green-400/10' },
|
||||
{ key: 'failed', label: '失败', count: stats?.failed || 0, color: 'text-red-400', bg: 'bg-red-400/10' },
|
||||
/* ---- Status Summary Cards (design system) ---- */
|
||||
const statusSummaryCards = useMemo(() => [
|
||||
{ key: 'pending', label: '待审核', count: (stats?.pending || 0) + (stats?.reviewing || 0) + (stats?.approved || 0), dot: 'dg-status-dot-blue' },
|
||||
{ key: 'executing', label: '执行中', count: stats?.executing || 0, dot: 'dg-status-dot-orange' },
|
||||
{ key: 'success', label: '已完成', count: stats?.success || 0, dot: 'dg-status-dot-green' },
|
||||
{ key: 'failed', label: '失败/拒绝', count: (stats?.failed || 0) + (stats?.rejected || 0), dot: 'dg-status-dot-red' },
|
||||
], [stats]);
|
||||
|
||||
/* ---- Open detail dialog ---- */
|
||||
@@ -238,27 +258,31 @@ export default function TicketsPage() {
|
||||
} : undefined}
|
||||
/>
|
||||
|
||||
{/* Status Quick Filters */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{/* Status Summary Cards */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{stats === null ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="h-9 w-24 animate-pulse rounded-lg bg-[var(--dg-bg-tertiary)]" />
|
||||
Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="dg-card-section animate-pulse">
|
||||
<div className="dg-card-header">
|
||||
<div className="h-8 w-12 rounded bg-[var(--dg-bg-tertiary)]" />
|
||||
<div className="h-3 w-16 rounded bg-[var(--dg-bg-tertiary)]" />
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
statusCards.map(card => (
|
||||
statusSummaryCards.map(card => (
|
||||
<button
|
||||
key={card.key}
|
||||
onClick={() => { setStatusFilter(card.key); setPage(1); }}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
statusFilter === card.key
|
||||
? `${card.bg} ${card.color} ring-1 ring-current/20`
|
||||
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
|
||||
onClick={() => { setStatusFilter(statusFilter === card.key ? '' : card.key); setPage(1); }}
|
||||
className={`dg-card-section text-left transition-all cursor-pointer hover:ring-1 hover:ring-[var(--dg-border)] ${
|
||||
statusFilter === card.key ? 'ring-1 ring-blue-500/30 bg-blue-500/5' : ''
|
||||
}`}
|
||||
>
|
||||
{card.label}
|
||||
<span className={`text-xs px-1.5 py-0.5 rounded-full ${card.key === '' ? 'bg-[var(--dg-bg-hover)]' : 'bg-black/20'}`}>
|
||||
{card.count}
|
||||
</span>
|
||||
<div className="dg-card-header flex items-center justify-between">
|
||||
<span className="text-2xl font-bold">{card.count}</span>
|
||||
<span className={`dg-status-dot ${card.dot}`} />
|
||||
</div>
|
||||
<span className="text-xs text-[var(--dg-text-secondary)] mt-1 block">{card.label}</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
@@ -269,7 +293,7 @@ export default function TicketsPage() {
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--dg-text-muted)]" />
|
||||
<input
|
||||
className="w-full pl-10 pr-4 py-2.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-lg text-sm placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none transition-colors"
|
||||
className="dg-input w-full pl-10 pr-4 py-2.5"
|
||||
placeholder="搜索工单标题或SQL内容..."
|
||||
value={search}
|
||||
onChange={e => { setSearch(e.target.value); setPage(1); }}
|
||||
@@ -283,13 +307,13 @@ export default function TicketsPage() {
|
||||
</div>
|
||||
|
||||
{/* Ticket Table */}
|
||||
<Card className="dg-card">
|
||||
<CardContent className="p-0">
|
||||
<div className="dg-card overflow-hidden">
|
||||
<div className="p-0">
|
||||
{loading && tickets.length === 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
|
||||
<tr className="dg-table-header">
|
||||
<th className="text-left px-4 py-3 font-semibold">工单编号</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">实例名称</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">提交人</th>
|
||||
@@ -314,7 +338,7 @@ export default function TicketsPage() {
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
|
||||
<tr className="dg-table-header">
|
||||
<th className="text-left px-4 py-3 font-semibold">工单编号</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">实例名称</th>
|
||||
<th className="text-left px-4 py-3 font-semibold">提交人</th>
|
||||
@@ -332,7 +356,7 @@ export default function TicketsPage() {
|
||||
{sortedTickets.map(ticket => (
|
||||
<tr
|
||||
key={ticket.id}
|
||||
className="border-b border-[var(--dg-border-light)] hover:bg-[var(--dg-bg-hover)] transition-colors"
|
||||
className="dg-table-row"
|
||||
>
|
||||
{/* 工单编号 */}
|
||||
<td className="px-4 py-3.5">
|
||||
@@ -345,7 +369,7 @@ export default function TicketsPage() {
|
||||
{/* 实例名称 */}
|
||||
<td className="px-4 py-3.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`inline-flex px-1.5 py-0.5 rounded text-[10px] font-medium ${dbTypeColorMap[ticket.db_type] || ''}`}>
|
||||
<span className={`dg-badge ${dgBadgeDbMap[ticket.db_type] || 'dg-badge-blue'}`}>
|
||||
{ticket.db_type.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-sm">{ticket.instance_name || '-'}</span>
|
||||
@@ -380,8 +404,7 @@ export default function TicketsPage() {
|
||||
|
||||
{/* 状态 */}
|
||||
<td className="px-4 py-3.5">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${displayStatusColorMap[ticket.status] || ''}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${ticket.status === 'executing' || ticket.status === 'reviewing' ? 'animate-pulse' : ''}`} />
|
||||
<span className={`dg-badge ${dgBadgeStatusMap[ticket.status] || 'dg-badge-blue'}`}>
|
||||
{displayStatusMap[ticket.status] || statusMap[ticket.status] || ticket.status}
|
||||
</span>
|
||||
</td>
|
||||
@@ -396,7 +419,7 @@ export default function TicketsPage() {
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => openDetail(ticket.id)}
|
||||
className="flex items-center gap-1 text-blue-400 hover:text-blue-300 text-xs font-medium transition-colors"
|
||||
className="dg-btn dg-btn-sm dg-btn-ghost"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
{ticket.status === 'success' || ticket.status === 'failed' ? '详情' : '查看'}
|
||||
@@ -410,7 +433,7 @@ export default function TicketsPage() {
|
||||
fetchTickets(); fetchStats();
|
||||
} catch { /* ignore */ }
|
||||
}}
|
||||
className="flex items-center gap-1 text-green-400 hover:text-green-300 text-xs font-medium transition-colors"
|
||||
className="dg-btn dg-btn-sm dg-btn-ghost text-green-400 hover:text-green-300"
|
||||
>
|
||||
<CheckCircle className="w-3.5 h-3.5" />
|
||||
通过
|
||||
@@ -425,7 +448,7 @@ export default function TicketsPage() {
|
||||
fetchTickets(); fetchStats();
|
||||
} catch { /* ignore */ }
|
||||
}}
|
||||
className="flex items-center gap-1 text-red-400 hover:text-red-300 text-xs font-medium transition-colors"
|
||||
className="dg-btn dg-btn-sm dg-btn-ghost text-red-400 hover:text-red-300"
|
||||
>
|
||||
<XCircle className="w-3.5 h-3.5" />
|
||||
拒绝
|
||||
@@ -440,7 +463,7 @@ export default function TicketsPage() {
|
||||
fetchTickets(); fetchStats();
|
||||
} catch { /* ignore */ }
|
||||
}}
|
||||
className="flex items-center gap-1 text-purple-400 hover:text-purple-300 text-xs font-medium transition-colors"
|
||||
className="dg-btn dg-btn-sm dg-btn-ghost text-purple-400 hover:text-purple-300"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" />
|
||||
执行
|
||||
@@ -455,7 +478,7 @@ export default function TicketsPage() {
|
||||
fetchTickets(); fetchStats();
|
||||
} catch { /* ignore */ }
|
||||
}}
|
||||
className="flex items-center gap-1 text-amber-400 hover:text-amber-300 text-xs font-medium transition-colors"
|
||||
className="dg-btn dg-btn-sm dg-btn-ghost text-amber-400 hover:text-amber-300"
|
||||
>
|
||||
<Ban className="w-3.5 h-3.5" />
|
||||
撤销
|
||||
@@ -478,7 +501,7 @@ export default function TicketsPage() {
|
||||
<button
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage(p => p - 1)}
|
||||
className="px-3 py-1.5 rounded-md text-sm text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] disabled:opacity-50 transition-colors"
|
||||
className="dg-btn dg-btn-ghost dg-btn-sm disabled:opacity-50"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
@@ -497,8 +520,8 @@ export default function TicketsPage() {
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setPage(p)}
|
||||
className={`px-3 py-1.5 rounded-md text-sm transition-colors ${
|
||||
p === page ? 'bg-blue-500 text-white' : 'text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
|
||||
className={`dg-btn dg-btn-sm dg-btn-ghost ${
|
||||
p === page ? 'bg-blue-500 text-white hover:bg-blue-500' : ''
|
||||
}`}
|
||||
>
|
||||
{p}
|
||||
@@ -508,15 +531,15 @@ export default function TicketsPage() {
|
||||
<button
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(p => p + 1)}
|
||||
className="px-3 py-1.5 rounded-md text-sm text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] disabled:opacity-50 transition-colors"
|
||||
className="dg-btn dg-btn-ghost dg-btn-sm disabled:opacity-50"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detail Dialog */}
|
||||
{detailTicket && (
|
||||
@@ -581,7 +604,7 @@ function TicketDetailDialog({ ticket, onClose, canApprove, canExecute, onActionD
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--dg-border)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-xs text-[var(--dg-text-muted)]">{ticket.ticket_no || `TK-${ticket.id}`}</span>
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${displayStatusColorMap[ticket.status] || ''}`}>
|
||||
<span className={`dg-badge ${dgBadgeStatusMap[ticket.status] || 'dg-badge-blue'}`}>
|
||||
{displayStatusMap[ticket.status] || statusMap[ticket.status] || ticket.status}
|
||||
</span>
|
||||
</div>
|
||||
@@ -591,14 +614,14 @@ function TicketDetailDialog({ ticket, onClose, canApprove, canExecute, onActionD
|
||||
<button
|
||||
onClick={() => handleReview('approve')}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-green-500/15 text-green-400 border border-green-500/30 rounded-md hover:bg-green-500/25 disabled:opacity-50 transition-all"
|
||||
className="dg-btn dg-btn-sm bg-green-500/15 text-green-400 border border-green-500/30 hover:bg-green-500/25 disabled:opacity-50"
|
||||
>
|
||||
<CheckCircle className="w-3.5 h-3.5" /> 通过
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleReview('reject')}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-red-500/15 text-red-400 border border-red-500/30 rounded-md hover:bg-red-500/25 disabled:opacity-50 transition-all"
|
||||
className="dg-btn dg-btn-sm bg-red-500/15 text-red-400 border border-red-500/30 hover:bg-red-500/25 disabled:opacity-50"
|
||||
>
|
||||
<XCircle className="w-3.5 h-3.5" /> 拒绝
|
||||
</button>
|
||||
@@ -608,7 +631,7 @@ function TicketDetailDialog({ ticket, onClose, canApprove, canExecute, onActionD
|
||||
<button
|
||||
onClick={handleExecute}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium bg-purple-500/15 text-purple-400 border border-purple-500/30 rounded-md hover:bg-purple-500/25 disabled:opacity-50 transition-all"
|
||||
className="dg-btn dg-btn-sm bg-purple-500/15 text-purple-400 border border-purple-500/30 hover:bg-purple-500/25 disabled:opacity-50"
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" /> 执行
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user