/** * 工单详情弹窗 — 状态流、信息栅格、SQL、执行结果与审批时间线 * 打开时拉取完整详情;审批/执行完成后经 onActionDone 通知页面刷新 */ import { useState, useEffect, useCallback } from 'react'; import { Eye, CheckCircle, XCircle, Play, FileCode, Clock, X, Loader2, AlertTriangle } from 'lucide-react'; import { ticketApi } from '@/api/ticket'; import { QuickReviewDialog } from './QuickReviewDialog'; import { ConfirmDialog } from '@/components/ConfirmDialog'; import { ModalOverlay } from '@/components/ModalOverlay'; import { getStatusIndex, parseExecuteResult } from '../utils'; import type { ApprovalRecord, TicketItem } from '../types'; import { TicketStatusBadge } from './TicketStatusBadge'; interface TicketDetailDialogProps { ticket: TicketItem; onClose: () => void; canApprove: boolean; canExecute: boolean; onActionDone: () => void; } export function TicketDetailDialog({ ticket: initialTicket, onClose, canApprove, canExecute, onActionDone, }: TicketDetailDialogProps) { const [loading, setLoading] = useState(false); const [reviewAction, setReviewAction] = useState<'approve' | 'reject' | null>(null); const [confirmingExecute, setConfirmingExecute] = useState(false); const [ticket, setTicket] = useState(initialTicket); const [approvals, setApprovals] = useState([]); // Load full detail on mount useEffect(() => { (async () => { try { const detail = (await ticketApi.get(ticket.id)) as any; setTicket((prev) => ({ ...prev, ...detail })); setApprovals(detail.approvals || []); } catch { /* ignore */ } })(); }, [ticket.id]); 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((action: 'approve' | 'reject') => { setReviewAction(action); }, []); const confirmReview = useCallback( async (comment: string) => { if (!reviewAction) return; setLoading(true); try { await ticketApi.review(ticket.id, { action: reviewAction, comment }); setReviewAction(null); onActionDone(); } catch { /* ignore */ } finally { setLoading(false); } }, [reviewAction, ticket.id, onActionDone], ); // 执行:影响面复述确认(opsx-sec 模式) const handleExecute = useCallback(() => { setConfirmingExecute(true); }, []); const confirmExecute = useCallback(async () => { setConfirmingExecute(false); setLoading(true); try { await ticketApi.execute(ticket.id); onActionDone(); } catch { /* ignore */ } finally { setLoading(false); } }, [ticket.id, onActionDone]); return ( <>
{/* Header */}
{ticket.ticket_no || `TK-${ticket.id}`}
{canApprove && ticket.is_approver && (ticket.status === 'pending' || ticket.status === 'reviewing') && ( <> )} {canExecute && ticket.is_executor && 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() }, ].map((item) => (
{item.label} {item.value}
))}
{/* SQL Content */}

SQL 语句

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

执行结果

{/* Summary row */}
状态
{execResult.status === 'success' ? '执行成功' : '执行失败'}
总耗时
{execResult.total_ms}ms
SQL 数量
{execResult.items?.length || 0}
{/* Per-SQL result table */} {execResult.items && execResult.items.length > 0 && (
{execResult.items.map((item, idx) => ( ))}
# SQL 语句 执行结果 影响行数 执行时间 错误信息
{idx + 1}
                                {item.sql}
                              
{item.status === 'success' ? '成功' : '失败'} {item.affected_rows} {item.duration_ms}ms {item.error && ( {item.error.length > 80 ? item.error.substring(0, 80) + '...' : item.error} )}
)} {ticket.execute_time && (
执行时间: {new Date(ticket.execute_time).toLocaleString()}
)}
)} {/* Execute Result — plain text fallback for non-JSON results */} {!execResult && ticket.execute_result && (

执行结果

{ticket.execute_result}
{ticket.execute_time && (
执行时间: {new Date(ticket.execute_time).toLocaleString()}
)}
)} {/* Failure reason for failed/rejected tickets without execute_result */} {(ticket.status === 'failed' || ticket.status === 'rejected') && !ticket.execute_result && (

失败原因

{ticket.status === 'rejected' ? '工单已被拒绝' : '执行失败,请查看审批记录了解详情'}
)} {/* Timeline — approval history */} {approvals.length > 0 && (

操作时间线

{/* Submit step */}
提交申请
{ticket.applicant_name || `用户#${ticket.applicant_id}`} ·{' '} {new Date(ticket.create_time).toLocaleString()}
{ticket.reason && (
{ticket.reason}
)}
{/* Group approvals by step_number */} {Object.values( approvals.reduce( (acc, a) => { const key = `${a.step_number}_${a.step_name || ''}`; if (!acc[key]) acc[key] = { step: a, users: [] }; acc[key].users.push(a); return acc; }, {} as Record, ), ).map(({ step, users }, idx, arr) => { const isCompleted = users.some((u) => u.action); const actor = users.find((u) => u.actual_approver_id); const StepIcon = step.step_type === 'execute' ? Play : Eye; const isLast = idx === arr.length - 1; return (
{!isLast &&
}
{step.step_name || (step.step_type === 'execute' ? '执行变更' : '审批')} {!isCompleted && ( (待处理) )}
指派人:{' '} {users .map((u) => u.approver_name) .filter(Boolean) .join(', ') || '-'}
{actor && actor.completed_at && (
{actor.actual_approver_name} ·{' '} {actor.action === 'approve' ? '通过' : actor.action === 'reject' ? '拒绝' : '执行'} ·{' '} {new Date(actor.completed_at).toLocaleString()} {actor.comment && ( ({actor.comment}) )}
)}
); })}
)}
{reviewAction && ( setReviewAction(null)} /> )} {confirmingExecute && ( setConfirmingExecute(false)} /> )} ); }