import { useEffect, useRef, useState } from "react"; import { Dialog, DialogOverlay, DialogPortal } from "@/components/ui/dialog"; import * as DialogPrimitive from "@radix-ui/react-dialog"; import { Terminal, CheckCircle, XCircle, Loader2, X as XIcon } from "lucide-react"; import { cn, calculateTaskProgress } from "@/shared/utils/utils"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; import { taskControl } from "@/shared/services/taskControl"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; interface TerminalProgressDialogProps { open: boolean; onOpenChange: (open: boolean) => void; taskId: string | null; taskType: "repository" | "zip"; } interface LogEntry { timestamp: string; message: string; type: "info" | "success" | "error" | "warning"; } export default function TerminalProgressDialog({ open, onOpenChange, taskId, taskType }: TerminalProgressDialogProps) { const [logs, setLogs] = useState([]); const [isCompleted, setIsCompleted] = useState(false); const [isFailed, setIsFailed] = useState(false); const [isCancelled, setIsCancelled] = useState(false); const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" })); const logsEndRef = useRef(null); const pollIntervalRef = useRef(null); const hasInitializedLogsRef = useRef(false); // 添加日志条目 const addLog = (message: string, type: LogEntry["type"] = "info") => { const timestamp = new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" }); setLogs(prev => [...prev, { timestamp, message, type }]); }; // 取消任务处理 const handleCancel = async () => { if (!taskId) return; if (!confirm('确定要取消此任务吗?已分析的结果将被保留。')) { return; } // 1. 标记任务为取消状态 taskControl.cancelTask(taskId); setIsCancelled(true); addLog("🛑 用户取消任务,正在停止...", "error"); // 2. 立即更新数据库状态 try { const { api } = await import("@/shared/config/database"); await api.updateAuditTask(taskId, { status: 'cancelled' } as any); addLog("✓ 任务状态已更新为已取消", "warning"); toast.success("任务已取消"); } catch (error) { console.error('更新取消状态失败:', error); toast.warning("任务已标记取消,后台正在停止..."); } }; // 自动滚动到底部 useEffect(() => { logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [logs]); // 实时更新光标处的时间 useEffect(() => { if (!open || isCompleted || isFailed || isCancelled) { return; } const timeInterval = setInterval(() => { setCurrentTime(new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit" })); }, 1000); return () => { clearInterval(timeInterval); }; }, [open, isCompleted, isFailed]); // 轮询任务状态 useEffect(() => { if (!open || !taskId) { // 清理状态 setLogs([]); setIsCompleted(false); setIsFailed(false); hasInitializedLogsRef.current = false; if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } return; } // 只初始化日志一次(防止React严格模式重复) if (!hasInitializedLogsRef.current) { hasInitializedLogsRef.current = true; // 初始化日志 addLog("🚀 审计任务已启动", "info"); addLog(`� 任务任ID: ${taskId}`, "info"); addLog(`� 任务类D型: ${taskType === "repository" ? "仓库审计" : "ZIP文件审计"}`, "info"); addLog("⏳ 正在初始化审计环境...", "info"); } let lastScannedFiles = 0; let lastIssuesCount = 0; let lastTotalLines = 0; let lastStatus = ""; let pollCount = 0; let hasDataChange = false; let isFirstPoll = true; // 开始轮询 const pollTask = async () => { // 如果任务已完成或失败,停止轮询 if (isCompleted || isFailed) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } return; } try { pollCount++; hasDataChange = false; const requestStartTime = Date.now(); // 使用 api.getAuditTaskById 获取任务状态 const { api } = await import("@/shared/config/database"); const task = await api.getAuditTaskById(taskId); const requestDuration = Date.now() - requestStartTime; if (!task) { addLog(`❌ 任务不存在 (${requestDuration}ms)`, "error"); throw new Error("任务不存在"); } // 检查是否有数据变化 const statusChanged = task.status !== lastStatus; const filesChanged = task.scanned_files !== lastScannedFiles; const issuesChanged = task.issues_count !== lastIssuesCount; const linesChanged = task.total_lines !== lastTotalLines; hasDataChange = statusChanged || filesChanged || issuesChanged || linesChanged; // 标记首次轮询已完成 if (isFirstPoll) { isFirstPoll = false; } // 只在有变化时显示请求/响应信息 if (hasDataChange) { addLog(`🔄 正在获取任务状态...`, "info"); addLog( `✓ 状态: ${task.status} | 文件: ${task.scanned_files}/${task.total_files} | 问题: ${task.issues_count} (${requestDuration}ms)`, "success" ); } // 更新上次状态 if (statusChanged) { lastStatus = task.status; } // 检查任务状态 if (task.status === "pending") { // 任务待处理(只在状态变化时显示) if (statusChanged && logs.filter(l => l.message.includes("等待开始执行")).length === 0) { addLog("⏳ 任务已创建,等待开始执行...", "info"); } } else if (task.status === "running") { // 首次进入运行状态 if (statusChanged && logs.filter(l => l.message.includes("开始扫描")).length === 0) { addLog("🔍 开始扫描代码文件...", "info"); if (task.project) { addLog(`📁 项目: ${task.project.name}`, "info"); if (task.branch_name) { addLog(`🌿 分支: ${task.branch_name}`, "info"); } } } // 显示进度更新(仅在有变化时) if (filesChanged && task.scanned_files > lastScannedFiles) { const progress = calculateTaskProgress(task.scanned_files, task.total_files); const filesProcessed = task.scanned_files - lastScannedFiles; addLog( `📊 扫描进度: ${task.scanned_files || 0}/${task.total_files || 0} 文件 (${progress}%) [+${filesProcessed}]`, "info" ); lastScannedFiles = task.scanned_files; } // 显示问题发现(仅在有变化时) if (issuesChanged && task.issues_count > lastIssuesCount) { const newIssues = task.issues_count - lastIssuesCount; addLog(`⚠️ 发现 ${newIssues} 个新问题 (总计: ${task.issues_count})`, "warning"); lastIssuesCount = task.issues_count; } // 显示代码行数(仅在有变化时) if (linesChanged && task.total_lines > lastTotalLines) { const newLines = task.total_lines - lastTotalLines; addLog(`📝 已分析 ${task.total_lines.toLocaleString()} 行代码 [+${newLines.toLocaleString()}]`, "info"); lastTotalLines = task.total_lines; } } else if (task.status === "completed") { // 任务完成 if (!isCompleted) { addLog("", "info"); // 空行分隔 addLog("✅ 代码扫描完成", "success"); addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "info"); addLog(`📊 总计扫描: ${task.total_files} 个文件`, "success"); addLog(`📝 总计分析: ${task.total_lines.toLocaleString()} 行代码`, "success"); addLog(`⚠️ 发现问题: ${task.issues_count} 个`, task.issues_count > 0 ? "warning" : "success"); // 解析问题类型分布 if (task.issues_count > 0) { try { const { api: apiImport } = await import("@/shared/config/database"); const issues = await apiImport.getAuditIssues(taskId); const severityCounts = { critical: issues.filter(i => i.severity === 'critical').length, high: issues.filter(i => i.severity === 'high').length, medium: issues.filter(i => i.severity === 'medium').length, low: issues.filter(i => i.severity === 'low').length }; if (severityCounts.critical > 0) { addLog(` 🔴 严重: ${severityCounts.critical} 个`, "error"); } if (severityCounts.high > 0) { addLog(` 🟠 高: ${severityCounts.high} 个`, "warning"); } if (severityCounts.medium > 0) { addLog(` 🟡 中等: ${severityCounts.medium} 个`, "warning"); } if (severityCounts.low > 0) { addLog(` 🟢 低: ${severityCounts.low} 个`, "info"); } } catch (e) { // 静默处理错误 } } addLog(`⭐ 质量评分: ${task.quality_score.toFixed(1)}/100`, "success"); addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "info"); addLog("🎉 审计任务已完成!", "success"); if (task.completed_at) { const startTime = new Date(task.created_at).getTime(); const endTime = new Date(task.completed_at).getTime(); const duration = Math.round((endTime - startTime) / 1000); addLog(`⏱️ 总耗时: ${duration} 秒`, "info"); } setIsCompleted(true); if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } } } else if (task.status === "cancelled") { // 任务被取消 if (!isCancelled) { addLog("", "info"); // 空行分隔 addLog("🛑 任务已被用户取消", "warning"); addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "warning"); addLog(`📊 完成统计:`, "info"); addLog(` • 已分析文件: ${task.scanned_files}/${task.total_files}`, "info"); addLog(` • 发现问题: ${task.issues_count} 个`, "info"); addLog(` • 代码行数: ${task.total_lines.toLocaleString()} 行`, "info"); addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "warning"); addLog("✓ 已分析的结果已保存到数据库", "success"); setIsCancelled(true); if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } } } else if (task.status === "failed") { // 任务失败 if (!isFailed) { addLog("", "info"); // 空行分隔 addLog("❌ 审计任务执行失败", "error"); addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); addLog("可能的原因:", "error"); addLog(" • 网络连接问题", "error"); addLog(" • 仓库访问权限不足", "error"); addLog(" • GitHub API 限流", "error"); addLog(" • 代码文件格式错误", "error"); addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); addLog("💡 建议: 检查网络连接和仓库配置后重试", "warning"); setIsFailed(true); if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } } } } catch (error: any) { addLog(`❌ ${error.message || "未知错误"}`, "error"); // 不中断轮询,继续尝试 } }; // 立即执行一次 pollTask(); // 设置定时轮询(每2秒) pollIntervalRef.current = window.setInterval(pollTask, 2000); // 清理函数 return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } }; }, [open, taskId, taskType]); // 获取日志颜色 - 使用优雅的深红色主题 const getLogColor = (type: LogEntry["type"]) => { switch (type) { case "success": return "text-emerald-400"; case "error": return "text-rose-400"; case "warning": return "text-amber-400"; default: return "text-gray-200"; } }; // 获取状态图标 const getStatusIcon = () => { if (isFailed) { return ; } if (isCompleted) { return ; } return ; }; return ( e.preventDefault()} onInteractOutside={(e) => e.preventDefault()} > {/* 无障碍访问标题 */} 审计进度监控 实时显示代码审计任务的执行进度和详细信息 {/* 终端头部 */}
审计进度监控 {getStatusIcon()}
{/* 终端内容 */}
{logs.map((log, index) => (
[{log.timestamp}] {log.message}
))} {/* 光标旋转闪烁效果 */} {!isCompleted && !isFailed && (
[{currentTime}]
)} {/* 添加自定义动画 */}
{/* 底部控制和提示 */}
{isCancelled ? "🛑 任务已取消,已分析的结果已保存" : isCompleted ? "✅ 任务已完成,可以关闭此窗口" : isFailed ? "❌ 任务失败,请检查配置后重试" : "⏳ 审计进行中,请勿关闭窗口,过程可能较慢,请耐心等待......"}
{/* 运行中显示取消按钮 */} {!isCompleted && !isFailed && !isCancelled && ( )} {/* 已完成/失败/取消显示关闭按钮 */} {(isCompleted || isFailed || isCancelled) && ( )}
); }