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:
logan committed 2026-05-27 01:59:08 +08:00
1 parent 4e2df4d3d7
commit 746a000e8a
25 files changed
+7718 -178

No files matched your search

+37 -17
View File
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { useTheme } from '@/hooks/useTheme';
import { useTheme, THEME_OPTIONS, type Theme } from '@/hooks/useTheme';
import { LOGIN_STYLE_OPTIONS, type LoginStyle } from '@/pages/LoginPage';
import { Sun, Moon, Check, Save, Loader2 } from 'lucide-react';
import { Check, Save, Loader2 } from 'lucide-react';
import api from '@/api/request';
import { toast } from '@/hooks/useToast';
@@ -16,7 +16,15 @@ export default function AppearanceSettings() {
useEffect(() => {
api.get<{ theme?: string; login_style?: string }>('/user/preferences')
.then(prefs => {
if (prefs?.theme) setTheme(prefs.theme as 'dark' | 'light');
if (prefs?.theme) {
// Backward compat: map old values
const t = prefs.theme;
if (t === 'dark' || t === 'light') {
setTheme(t === 'dark' ? 'ocean-deep' : 'light-professional');
} else if (THEME_OPTIONS.some(o => o.id === t)) {
setTheme(t as Theme);
}
}
if (prefs?.login_style) setLoginStyle(prefs.login_style as LoginStyle);
})
.catch(() => {
@@ -27,7 +35,7 @@ export default function AppearanceSettings() {
.finally(() => setLoading(false));
}, []);
const handleThemeChange = (t: 'dark' | 'light') => {
const handleThemeChange = (t: Theme) => {
setTheme(t);
setDirty(true);
};
@@ -66,24 +74,36 @@ export default function AppearanceSettings() {
<div className="space-y-8">
{/* Theme Mode */}
<div>
<h3 className="text-sm font-semibold mb-3">主题模式</h3>
<div className="flex gap-3">
{([
{ id: 'dark' as const, icon: Moon, label: '深色模式' },
{ id: 'light' as const, icon: Sun, label: '浅色模式' },
]).map(opt => (
<h3 className="text-sm font-semibold mb-1">主题模式</h3>
<p className="text-xs text-[var(--dg-text-muted)] mb-4">选择界面配色方案,即时预览效果</p>
<div className="grid grid-cols-5 gap-3">
{THEME_OPTIONS.map(opt => (
<button
key={opt.id}
onClick={() => handleThemeChange(opt.id)}
className={`flex items-center gap-2 px-5 py-3 rounded-xl text-sm font-medium transition-all ${
theme === opt.id
? 'bg-blue-500/15 text-blue-400 ring-1 ring-blue-500/30'
: 'bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)]'
className={`group relative rounded-xl p-4 text-left transition-all bg-[var(--dg-bg-tertiary)] hover:bg-[var(--dg-bg-hover)] ${
theme === opt.id ? 'ring-2 ring-blue-500/50' : ''
}`}
>
<opt.icon className="w-4 h-4" />
{opt.label}
{theme === opt.id && <Check className="w-3.5 h-3.5 ml-1" />}
{/* Color preview swatch */}
<div
className="w-full h-6 rounded-md mb-3"
style={{
background: opt.isDark
? `linear-gradient(135deg, ${opt.primaryColor} 0%, color-mix(in srgb, ${opt.primaryColor} 40%, #1a1a2e) 50%, #0d1117 100%)`
: `linear-gradient(135deg, ${opt.primaryColor} 0%, color-mix(in srgb, ${opt.primaryColor} 60%, #ffffff) 50%, #f8fafc 100%)`,
}}
/>
{/* Active check mark */}
{theme === opt.id && (
<div className="absolute top-2 right-2 w-5 h-5 rounded-full bg-blue-500 flex items-center justify-center">
<Check className="w-3 h-3 text-white" />
</div>
)}
<h4 className="text-sm font-medium mb-0.5">{opt.name}</h4>
<p className="text-xs text-[var(--dg-text-muted)] leading-snug">{opt.description}</p>
</button>
))}
</div>
+37 -47
View File
@@ -7,7 +7,7 @@ import {
KeyRound, FolderOpen, Server, Zap, ClipboardPaste,
History, PanelRightClose, PanelRightOpen,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
// Card components replaced by design system utility classes
import { useActionPermission } from '@/hooks/useAuth';
import SqlEditor from '@/components/SqlEditor';
import SchemaSidebar from '@/components/SchemaSidebar';
@@ -321,28 +321,24 @@ export default function QueriesPage() {
<div className="flex items-center gap-2">
{instanceId && (
<button onClick={() => setShowSidebar(!showSidebar)}
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-lg border transition-all ${
showSidebar ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : 'text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${showSidebar ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}>
<Database className="w-3.5 h-3.5" />
{showSidebar ? '隐藏' : '导航'}
</button>
)}
<button onClick={() => setShowTemplates(!showTemplates)}
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium bg-[var(--dg-bg-tertiary)] hover:bg-[var(--dg-bg-hover)] border border-[var(--dg-border)] rounded-lg transition-all">
className="dg-btn dg-btn-ghost dg-btn-sm">
<Sparkles className="w-3.5 h-3.5 text-blue-400" />
模板 ({templates.length})
<ChevronDown className={`w-3 h-3 transition-transform ${showTemplates ? 'rotate-180' : ''}`} />
</button>
<button onClick={() => setShowHistory(!showHistory)}
className={`flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium rounded-lg border transition-all ${
showHistory ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : 'text-[var(--dg-text-secondary)] bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] hover:bg-[var(--dg-bg-hover)]'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${showHistory ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}>
{showHistory ? <PanelRightClose className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
历史 {history.length > 0 && <span className="text-[10px] px-1 py-0.5 rounded-full bg-[var(--dg-bg-primary)]">{history.length}</span>}
</button>
<button onClick={handleExecute} disabled={executing || !instanceId}
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg text-sm font-medium text-white hover:opacity-90 disabled:opacity-40 transition-all">
className="dg-btn dg-btn-primary">
{executing ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />}
{executing ? '执行中...' : '执行'}
</button>
@@ -351,37 +347,35 @@ export default function QueriesPage() {
{/* -- 模板选择器 -- */}
{showTemplates && (
<div className="p-3 bg-[var(--dg-bg-secondary)] rounded-lg border border-[var(--dg-border)]">
<div className="dg-card p-3">
<div className="flex items-center gap-2 mb-3 flex-wrap">
{templateCategories.map(cat => (
<button key={cat} onClick={() => setTemplateCategory(cat)}
className={`px-2.5 py-1 text-[11px] rounded-md transition-all font-medium ${
templateCategory === cat ? 'bg-blue-500/15 text-blue-400 border border-blue-500/30' : 'text-[var(--dg-text-muted)] hover:text-[var(--dg-text-secondary)] hover:bg-[var(--dg-bg-hover)] border border-transparent'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${templateCategory === cat ? 'bg-blue-500/15 text-blue-400 border-blue-500/30' : ''}`}>
{catLabel[cat] || cat}
</button>
))}
<div className="flex-1" />
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--dg-text-muted)]" />
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 dg-text-muted" />
<input value={templateSearch} onChange={e => setTemplateSearch(e.target.value)}
className="pl-7 pr-3 py-1 bg-[var(--dg-bg-primary)] border border-[var(--dg-border)] rounded-md text-xs focus:border-blue-400 focus:outline-none w-40"
placeholder="搜索模板..." />
</div>
</div>
{filteredTemplates.length === 0 ? (
<div className="text-center py-4 text-xs text-[var(--dg-text-muted)]">无匹配模板</div>
<div className="text-center py-4 text-xs dg-text-muted">无匹配模板</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 max-h-48 overflow-y-auto">
{filteredTemplates.map(tpl => (
<button key={tpl.id} onClick={() => handleApplyTemplate(tpl)}
className="text-left p-2.5 rounded-lg bg-[var(--dg-bg-primary)] hover:bg-[var(--dg-bg-hover)] transition-colors group border border-[var(--dg-border)] hover:border-[var(--dg-text-muted)]"
className="dg-card text-left p-2.5 hover:bg-[var(--dg-bg-hover)] group"
title={tpl.description}>
<div className="flex items-center gap-2 mb-0.5">
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-400 font-medium">{tpl.category}</span>
<span className="text-sm font-medium group-hover:text-blue-400 transition-colors truncate">{tpl.name}</span>
</div>
<p className="text-xs text-[var(--dg-text-muted)] line-clamp-1">{tpl.description}</p>
<p className="text-xs dg-text-muted line-clamp-1">{tpl.description}</p>
</button>
))}
</div>
@@ -405,7 +399,7 @@ export default function QueriesPage() {
<SqlEditor value={sql} onChange={setSql} placeholder={placeholderMap[dbType]}
language={editorLang} height="240px" onExecute={handleExecute}
schema={(dbType === 'mysql' || dbType === 'postgres' || dbType === 'postgresql') ? editorSchema : undefined} />
<div className="flex items-center justify-between px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border-t border-[var(--dg-border)] text-[11px] text-[var(--dg-text-muted)]">
<div className="flex items-center justify-between px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border-t border-[var(--dg-border)] text-[11px] dg-text-muted">
<span className="flex items-center gap-1.5">
{dbTypeIcon[dbType] && <img src={dbTypeIcon[dbType]} alt="" className="w-3 h-3 object-contain" />}
{dbTypeLabel[dbType]} 模式 · {sql.length} 字符
@@ -428,32 +422,32 @@ export default function QueriesPage() {
{/* -- 查询结果 -- */}
{result && (
<Card className="dg-card overflow-hidden">
<CardHeader className="border-b border-[var(--dg-border)] py-2.5">
<div className="dg-card overflow-hidden">
<div className="border-b border-[var(--dg-border)] py-2.5 px-4">
<div className="flex items-center justify-between flex-wrap gap-2">
<CardTitle className="text-sm font-semibold flex items-center gap-2">
<span className="text-sm font-semibold flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-400" />
查询结果
</CardTitle>
<div className="flex items-center gap-3 text-xs text-[var(--dg-text-muted)]">
</span>
<div className="flex items-center gap-3 text-xs dg-text-secondary">
<span className="flex items-center gap-1"><Clock className="w-3 h-3" />{result.execution_time_ms}ms</span>
<span>{result.rows?.length || 0} 行</span>
<span>{result.columns?.length || 0} 列</span>
{result.rows && result.rows.length > 0 && (
<button onClick={handleExportResult}
className="flex items-center gap-1 px-2.5 py-1 text-xs font-medium bg-green-500/15 text-green-400 border border-green-500/30 rounded-md hover:bg-green-500/25 transition-all">
className="dg-btn dg-btn-ghost dg-btn-sm">
<Download className="w-3 h-3" />导出 CSV
</button>
)}
</div>
</div>
</CardHeader>
<CardContent className="p-0">
</div>
<div className="p-0">
{result.columns && result.columns.length > 0 ? (
<div ref={resultContainerRef} onScroll={handleResultScroll} className="overflow-auto max-h-[350px]">
<table className="w-full text-sm">
<thead className="sticky top-0 z-10">
<tr className="border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]">
<tr className="dg-table-header">
<th className="text-left px-3 py-2 font-semibold text-xs text-[var(--dg-text-muted)] w-10">#</th>
{result.columns.map(col => (
<th key={col} onClick={() => handleSort(col)}
@@ -473,7 +467,7 @@ export default function QueriesPage() {
{(sortedResult?.rows || []).slice(visibleRange.start, visibleRange.end).map((row, i) => {
const ri = visibleRange.start + i;
return (
<tr key={ri} className="border-b border-[var(--dg-border-light)] hover:bg-[var(--dg-bg-hover)] transition-colors" style={{ height: rowHeight }}>
<tr key={ri} className="dg-table-row" style={{ height: rowHeight }}>
<td className="px-3 py-2 text-[10px] text-[var(--dg-text-muted)] font-mono">{ri + 1}</td>
{result.columns.map(col => {
const isSensitive = sensitiveKeywords.some(k => col.toLowerCase().includes(k));
@@ -492,14 +486,14 @@ export default function QueriesPage() {
</table>
</div>
) : (
<div className="p-6 text-center text-[var(--dg-text-muted)] flex flex-col items-center gap-2">
<div className="p-6 text-center dg-text-muted flex flex-col items-center gap-2">
<Eye className="w-5 h-5" />
<span className="text-sm">无结果数据</span>
{result.affected_rows > 0 && <span className="text-xs">影响行数: {result.affected_rows}</span>}
</div>
)}
</CardContent>
</Card>
</div>
</div>
)}
{/* -- 安全提示 -- */}
@@ -516,9 +510,9 @@ export default function QueriesPage() {
<span className="text-sm font-semibold flex items-center gap-2">
<History className="w-4 h-4 text-blue-400" />
查询历史
{history.length > 0 && <span className="text-xs text-[var(--dg-text-muted)] font-normal">({history.length})</span>}
{history.length > 0 && <span className="text-xs dg-text-muted font-normal">({history.length})</span>}
</span>
<button onClick={() => setShowHistory(false)} className="text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]">
<button onClick={() => setShowHistory(false)} className="dg-btn dg-btn-ghost dg-btn-sm p-1">
<PanelRightClose className="w-4 h-4" />
</button>
</div>
@@ -527,22 +521,18 @@ export default function QueriesPage() {
{history.length > 0 && (
<div className="px-4 pb-2 space-y-2">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--dg-text-muted)]" />
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3 h-3 dg-text-muted" />
<input className="w-full pl-8 pr-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-xs placeholder:text-[var(--dg-text-muted)] focus:border-blue-400 focus:outline-none"
placeholder="搜索历史..." value={searchHistory} onChange={e => setSearchHistory(e.target.value)} />
</div>
<div className="flex items-center gap-2">
<button onClick={() => setFavFilter(!favFilter)}
className={`flex items-center gap-1 px-2 py-1 text-[11px] rounded-md transition-all ${
favFilter ? 'bg-yellow-500/15 text-yellow-400 border border-yellow-500/30' : 'text-[var(--dg-text-muted)] hover:bg-[var(--dg-bg-hover)] border border-transparent'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${favFilter ? 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30' : ''}`}>
<Star className="w-3 h-3" />仅收藏
</button>
<div className="flex-1" />
<button onClick={handleClearHistory}
className={`flex items-center gap-1 px-2 py-1 text-[11px] rounded-md transition-all ${
clearConfirm ? 'text-red-400 bg-red-500/10' : 'text-[var(--dg-text-muted)] hover:text-red-400'
}`}>
className={`dg-btn dg-btn-ghost dg-btn-sm ${clearConfirm ? 'text-red-400 bg-red-500/10' : ''}`}>
{clearConfirm ? <CheckCircle className="w-3 h-3" /> : <X className="w-3 h-3" />}
{clearConfirm ? '确认清除' : '清除'}
</button>
@@ -553,29 +543,29 @@ export default function QueriesPage() {
{/* 历史列表 */}
<div className="flex-1 overflow-y-auto">
{history.length === 0 ? (
<div className="p-6 text-center text-xs text-[var(--dg-text-muted)]">暂无查询历史</div>
<div className="p-6 text-center text-xs dg-text-muted">暂无查询历史</div>
) : filteredHistory.length === 0 ? (
<div className="p-6 text-center text-xs text-[var(--dg-text-muted)]">没有匹配的记录</div>
<div className="p-6 text-center text-xs dg-text-muted">没有匹配的记录</div>
) : (
<div className="divide-y divide-[var(--dg-border-light)]">
<div className="space-y-1.5 px-3 py-2">
{filteredHistory.map(h => (
<div key={h.id} className="px-4 py-2.5 hover:bg-[var(--dg-bg-hover)] transition-colors group">
<div key={h.id} className={`dg-card px-3 py-2.5 group ${h.favorite ? 'bg-yellow-500/5 border-yellow-500/20' : ''}`}>
<div className="flex items-center gap-2 mb-1">
{h.db_type && dbTypeIcon[h.db_type] && <img src={dbTypeIcon[h.db_type]} alt="" className="w-3.5 h-3.5 object-contain" />}
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${h.status === 'success' ? 'bg-green-500/15 text-green-400' : 'bg-red-500/15 text-red-400'}`}>
{h.execution_time_ms}ms
</span>
<span className="text-[10px] text-[var(--dg-text-muted)]">{new Date(h.created_at).toLocaleTimeString()}</span>
<span className="text-[10px] dg-text-muted">{new Date(h.created_at).toLocaleTimeString()}</span>
<div className="flex-1" />
<button onClick={() => toggleFavorite(h.id)}
className={`p-0.5 rounded transition-colors ${h.favorite ? 'text-yellow-400' : 'text-[var(--dg-text-muted)] opacity-0 group-hover:opacity-100 hover:text-yellow-400'}`}>
className={`p-0.5 rounded transition-colors ${h.favorite ? 'text-yellow-400' : 'dg-text-muted opacity-0 group-hover:opacity-100 hover:text-yellow-400'}`}>
{h.favorite ? <Star className="w-3 h-3 fill-current" /> : <StarOff className="w-3 h-3" />}
</button>
</div>
<div className="flex items-center gap-1">
<code className="flex-1 text-[11px] font-mono text-blue-400 truncate">{h.sql.substring(0, 80)}</code>
<button onClick={() => setSql(h.sql)}
className="p-1 rounded text-[var(--dg-text-muted)] hover:text-blue-400 transition-colors opacity-0 group-hover:opacity-100"
className="dg-btn dg-btn-ghost dg-btn-sm p-1 opacity-0 group-hover:opacity-100"
title="粘贴到编辑器">
<ClipboardPaste className="w-3 h-3" />
</button>
+69 -46
View File
@@ -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>