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 } from "lucide-react"; import { cn } from "@/shared/utils/utils"; import * as VisuallyHidden from "@radix-ui/react-visually-hidden"; 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 [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 }]); }; // 自动滚动到底部 useEffect(() => { logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [logs]); // 实时更新光标处的时间 useEffect(() => { if (!open || isCompleted || isFailed) { 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 = task.total_files > 0 ? Math.round((task.scanned_files / task.total_files) * 100) : 0; const filesProcessed = task.scanned_files - lastScannedFiles; addLog( `📊 扫描进度: ${task.scanned_files}/${task.total_files} 文件 (${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 === "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}]
)} {/* 添加自定义动画 */}
{/* 底部提示 */}
{isCompleted ? "✅ 任务已完成,可以关闭此窗口" : isFailed ? "❌ 任务失败,请检查配置后重试" : "⏳ 审计进行中,请勿关闭窗口,过程可能较慢,请耐心等待......"} {(isCompleted || isFailed) && ( )}
); }