import { useState, useEffect, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { Plus, Eye, CheckCircle, XCircle, Play, 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'; import EmptyState from '@/components/EmptyState'; import { ticketApi } from '@/api/ticket'; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ interface TicketItem { id: number; ticket_no: string | null; title: string; db_type: string; instance_id: number; instance_name: string | null; target_database: string; sql_content: string; status: string; priority: string; applicant_id: number; applicant_name: string | null; approver_id: number | null; approver_name: string | null; current_approver_name: string | null; reason: string | null; audit_result: string | null; execute_result: string | null; create_time: string; review_time: string | null; execute_time: string | null; } interface TicketListResponse { total: number; page: number; page_size: number; items: TicketItem[]; } interface TicketStatistics { total: number; pending: number; reviewing: number; approved: number; rejected: number; executing: number; success: number; failed: number; } interface ExecuteResult { status: string; affected_rows: number; duration_ms: number; validation_passed: boolean; warnings: string[]; errors: string[]; raw_output: string; before_snapshot?: string; after_snapshot?: string; } /* ------------------------------------------------------------------ */ /* Constants */ /* ------------------------------------------------------------------ */ /** 展示用状态映射 — 按用户要求简化 */ const displayStatusMap: Record = { pending: '审核中', reviewing: '审核中', approved: '审核中', rejected: '已拒绝', executing: '执行中', success: '完成', failed: '失败', rollback: '已回滚', }; const displayStatusColorMap: Record = { pending: 'bg-blue-500/15 text-blue-400', reviewing: 'bg-blue-500/15 text-blue-400', approved: 'bg-blue-500/15 text-blue-400', rejected: 'bg-red-500/15 text-red-400', executing: 'bg-purple-500/15 text-purple-400', success: 'bg-green-500/15 text-green-400', failed: 'bg-red-500/15 text-red-400', rollback: 'bg-orange-500/15 text-orange-400', }; const statusFlow = ['pending', 'reviewing', 'approved', 'executing', 'success']; /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ function getStatusIndex(status: string): number { return statusFlow.indexOf(status); } function parseExecuteResult(raw: string | null): ExecuteResult | null { if (!raw) return null; try { return JSON.parse(raw) as ExecuteResult; } catch { return null; } } /* ------------------------------------------------------------------ */ /* Skeleton */ /* ------------------------------------------------------------------ */ function TableRowSkeleton() { return (
); } /* ------------------------------------------------------------------ */ /* Main Page */ /* ------------------------------------------------------------------ */ export default function TicketsPage() { const navigate = useNavigate(); const canCreate = useActionPermission('ticket.create'); const canApprove = useActionPermission('ticket.approve'); const canExecute = useActionPermission('ticket.execute'); const [search, setSearch] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [tickets, setTickets] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize] = useState(10); const [loading, setLoading] = useState(false); const [stats, setStats] = useState(null); const [sortAsc, setSortAsc] = useState(false); // Detail dialog const [detailTicket, setDetailTicket] = useState(null); const fetchTickets = useCallback(async () => { setLoading(true); try { const res = await ticketApi.list({ page, page_size: pageSize, status: statusFilter || undefined, keyword: search || undefined, }) as unknown as TicketListResponse; setTickets(res.items || []); setTotal(res.total); } catch { /* keep existing data on error */ } finally { setLoading(false); } }, [page, pageSize, statusFilter, search]); const fetchStats = useCallback(async () => { try { const res = await ticketApi.statistics() as unknown as TicketStatistics; setStats(res); } catch { /* ignore */ } }, []); useEffect(() => { fetchTickets(); }, [fetchTickets]); useEffect(() => { fetchStats(); }, [fetchStats]); const totalPages = Math.ceil(total / pageSize); const sortedTickets = useMemo(() => { const result = [...tickets]; result.sort((a, b) => { return sortAsc ? new Date(a.create_time).getTime() - new Date(b.create_time).getTime() : new Date(b.create_time).getTime() - new Date(a.create_time).getTime(); }); 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' }, ], [stats]); /* ---- Open detail dialog ---- */ const openDetail = useCallback(async (ticketId: number) => { try { const detail = await ticketApi.get(ticketId) as unknown as TicketItem; setDetailTicket(detail); } catch { /* ignore */ } }, []); return (
navigate('/tickets/create'), permission: 'ticket.create' } : undefined} /> {/* Status Quick Filters */}
{stats === null ? ( Array.from({ length: 5 }).map((_, i) => (
)) ) : ( statusCards.map(card => ( )) )}
{/* Search + Filters */}
{ setSearch(e.target.value); setPage(1); }} /> {search && ( )}
{/* Ticket Table */} {loading && tickets.length === 0 ? (
{Array.from({ length: 5 }).map((_, i) => )}
工单编号 实例名称 提交人 当前审核人 状态 操作
) : tickets.length === 0 ? ( ) : (
{sortedTickets.map(ticket => ( {/* 工单编号 */} {/* 实例名称 */} {/* 提交人 */} {/* 当前审核人 */} {/* 状态 */} {/* 创建时间 */} {/* 操作 */} ))}
工单编号 实例名称 提交人 当前审核人 状态 操作
{ticket.ticket_no || `TK-${ticket.id}`} {ticket.title}
{ticket.db_type.toUpperCase()} {ticket.instance_name || '-'}
{ticket.applicant_name || '-'}
{ticket.current_approver_name || (ticket.status === 'approved' ? '待执行' : '-')} {displayStatusMap[ticket.status] || statusMap[ticket.status] || ticket.status} {new Date(ticket.create_time).toLocaleString()}
{canApprove && (ticket.status === 'pending' || ticket.status === 'reviewing') && ( )} {canApprove && (ticket.status === 'pending' || ticket.status === 'reviewing') && ( )} {canExecute && ticket.status === 'approved' && ( )} {(ticket.status === 'pending' || ticket.status === 'reviewing') && ( )}
)} {/* Pagination */} {totalPages > 1 && (
共 {total} 条
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => { let p: number; if (totalPages <= 5) { p = i + 1; } else if (page <= 3) { p = i + 1; } else if (page >= totalPages - 2) { p = totalPages - 4 + i; } else { p = page - 2 + i; } return ( ); })}
)}
{/* Detail Dialog */} {detailTicket && ( setDetailTicket(null)} canApprove={canApprove} canExecute={canExecute} onActionDone={() => { setDetailTicket(null); fetchTickets(); fetchStats(); }} /> )}
); } /* ------------------------------------------------------------------ */ /* Ticket Detail Dialog */ /* ------------------------------------------------------------------ */ function TicketDetailDialog({ ticket, onClose, canApprove, canExecute, onActionDone }: { ticket: TicketItem; onClose: () => void; canApprove: boolean; canExecute: boolean; onActionDone: () => void; }) { const [loading, setLoading] = useState(false); const execResult = parseExecuteResult(ticket.execute_result); const currentStepIndex = getStatusIndex(ticket.status); const statusDisplay = [ { key: 'pending', label: '提交', icon: FileCode }, { key: 'reviewing', label: '审批', icon: Eye }, { key: 'approved', label: '通过', icon: CheckCircle }, { key: 'executing', label: '执行', icon: Loader2 }, { key: 'success', label: '完成', icon: CheckCircle }, ]; const handleReview = useCallback(async (action: 'approve' | 'reject') => { setLoading(true); try { await ticketApi.review(ticket.id, { action, comment: action === 'approve' ? '审批通过' : '拒绝' }); onActionDone(); } catch { /* ignore */ } finally { setLoading(false); } }, [ticket.id, onActionDone]); const handleExecute = useCallback(async () => { setLoading(true); try { await ticketApi.execute(ticket.id); onActionDone(); } catch { /* ignore */ } finally { setLoading(false); } }, [ticket.id, onActionDone]); return (
e.stopPropagation()} > {/* Header */}
{ticket.ticket_no || `TK-${ticket.id}`} {displayStatusMap[ticket.status] || statusMap[ticket.status] || ticket.status}
{canApprove && (ticket.status === 'pending' || ticket.status === 'reviewing') && ( <> )} {canExecute && ticket.status === 'approved' && ( )}
{/* Scrollable Content */}
{/* Status Flow */}
{statusDisplay.map((step, idx) => { const StepIcon = step.icon; const isCompleted = idx <= currentStepIndex && currentStepIndex >= 0; const isCurrent = idx === currentStepIndex; const isFailed = ticket.status === 'failed' || ticket.status === 'rejected'; const showFailed = isFailed && idx === currentStepIndex; return (
{showFailed ? : }
{showFailed ? (ticket.status === 'rejected' ? '已拒绝' : '失败') : step.label}
{idx < statusDisplay.length - 1 && (
= 0 ? 'bg-green-400/50' : 'bg-[var(--dg-border)]' }`} /> )}
); })}
{/* Info Grid */}
{[ { label: '提交人', value: ticket.applicant_name || '-' }, { label: '当前审核人', value: ticket.current_approver_name || (ticket.status === 'approved' ? '待执行' : '-') }, { label: '实例', value: ticket.instance_name || '-' }, { label: '目标库', value: ticket.target_database }, { label: '数据库类型', value: ticket.db_type.toUpperCase() }, { label: '创建时间', value: new Date(ticket.create_time).toLocaleString() }, { label: '变更原因', value: ticket.reason || '-' }, { label: '优先级', value: ticket.priority === 'high' ? '高' : ticket.priority === 'low' ? '低' : '中' }, ].map((item, idx) => (
{item.label} {item.value}
))}
{/* SQL Content */}

SQL 语句

              {ticket.sql_content}
            
{/* Execute Result — 结构化展示 */} {execResult && (

执行结果

{/* Summary */}
状态
{execResult.status === 'success' ? '执行成功' : '执行失败'}
影响行数
{execResult.affected_rows}
执行耗时
{execResult.duration_ms}ms
验证
{execResult.validation_passed ? '通过' : '未通过'}
{/* Raw Output */} {execResult.raw_output && (
原始输出
                    {execResult.raw_output}
                  
)} {/* Errors */} {execResult.errors && execResult.errors.length > 0 && (
错误信息
{execResult.errors.map((err, i) => (
{err}
))}
)} {/* Warnings */} {execResult.warnings && execResult.warnings.length > 0 && (
警告信息
{execResult.warnings.map((w, i) => (
{w}
))}
)} {ticket.execute_time && (
执行时间: {new Date(ticket.execute_time).toLocaleString()}
)}
)}
); }