"use client" import * as React from "react" import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" import { IconCircleCheck, IconLoader, IconClock, IconCircleX, IconPlayerStop, } from "@tabler/icons-react" import { cn } from "@/lib/utils" import type { ScanStage, ScanRecord, StageProgress, StageStatus } from "@/types/scan.types" /** 阶段名称中文映射(支持驼峰和下划线两种格式) */ const STAGE_LABELS: Record = { // 驼峰命名(后端返回格式) subdomainDiscovery: "子域名发现", portScan: "端口扫描", siteScan: "站点扫描", directoryScan: "目录扫描", urlFetch: "URL 抓取", vulnScan: "漏洞扫描", // 下划线命名(engine_config 格式) subdomain_discovery: "子域名发现", port_scan: "端口扫描", site_scan: "站点扫描", directory_scan: "目录扫描", url_fetch: "URL 抓取", vuln_scan: "漏洞扫描", } /** 获取阶段中文名称 */ function getStageName(stage: string): string { return STAGE_LABELS[stage] || stage } /** * 扫描阶段详情 */ interface StageDetail { stage: ScanStage // 阶段名称(来自 engine_config key) status: StageStatus duration?: string // 耗时,如 "2m30s" detail?: string // 额外信息,如 "发现 120 个子域名" resultCount?: number // 结果数量 } /** * 扫描进度数据 */ export interface ScanProgressData { id: number targetName: string engineName: string status: string progress: number currentStage?: ScanStage startedAt?: string errorMessage?: string // 错误信息(失败时有值) stages: StageDetail[] } interface ScanProgressDialogProps { open: boolean onOpenChange: (open: boolean) => void data: ScanProgressData | null } /** 扫描状态配置(与 scan-history 状态颜色一致) */ const SCAN_STATUS_CONFIG: Record = { running: { label: "扫描中", className: "bg-blue-500/15 text-blue-600 border-blue-500/30 dark:text-blue-400" }, cancelled: { label: "已取消", className: "bg-gray-500/15 text-gray-600 border-gray-500/30 dark:text-gray-400" }, completed: { label: "已完成", className: "bg-emerald-500/15 text-emerald-600 border-emerald-500/30 dark:text-emerald-400" }, failed: { label: "失败", className: "bg-red-500/15 text-red-600 border-red-500/30 dark:text-red-400" }, initiated: { label: "等待中", className: "bg-amber-500/15 text-amber-600 border-amber-500/30 dark:text-amber-400" }, } /** * 闪烁点动效(与 scan-history 一致) */ function PulsingDot({ className }: { className?: string }) { return ( ) } /** * 扫描状态图标(用于标题,与 scan-history 状态列动效一致) */ function ScanStatusIcon({ status }: { status: string }) { switch (status) { case "running": return case "completed": return case "cancelled": return case "failed": return case "initiated": return default: return } } /** * 扫描状态徽章 */ function ScanStatusBadge({ status }: { status: string }) { const config = SCAN_STATUS_CONFIG[status] || { label: status, className: "bg-muted text-muted-foreground" } return ( {config.label} ) } /** * 阶段状态图标 */ function StageStatusIcon({ status }: { status: StageStatus }) { switch (status) { case "completed": return case "running": return case "failed": return case "cancelled": return default: return } } /** * 单个阶段行 */ function StageRow({ stage }: { stage: StageDetail }) { return (
{getStageName(stage.stage)} {stage.detail && (

{stage.detail}

)}
{/* 状态/耗时 */} {stage.status === "running" && ( 进行中 )} {stage.status === "completed" && stage.duration && ( {stage.duration} )} {stage.status === "pending" && ( 等待中 )} {stage.status === "failed" && ( 失败 )} {stage.status === "cancelled" && ( 已取消 )}
) } /** * 扫描进度弹窗 */ export function ScanProgressDialog({ open, onOpenChange, data, }: ScanProgressDialogProps) { if (!data) return null return ( 扫描进度 {/* 基本信息 */}
目标 {data.targetName}
引擎 {data.engineName}
{data.startedAt && (
开始时间 {formatDateTime(data.startedAt)}
)}
状态
{/* 错误信息(失败时显示) */} {data.errorMessage && (

错误原因

{data.errorMessage}

)}
{/* 总进度 */}
总进度 {data.progress}%
{/* 阶段列表 */}
{data.stages.map((stage) => ( ))}
) } /** * 格式化时长(秒 -> 可读字符串) */ function formatDuration(seconds?: number): string | undefined { if (seconds === undefined || seconds === null) return undefined if (seconds < 1) return "<1s" if (seconds < 60) return `${Math.round(seconds)}s` const minutes = Math.floor(seconds / 60) const secs = Math.round(seconds % 60) return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m` } /** * 格式化日期时间(ISO 字符串 -> 可读格式) */ function formatDateTime(isoString?: string): string { if (!isoString) return "" try { const date = new Date(isoString) return date.toLocaleString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false, }) } catch { return isoString } } /** 从 summary 中获取阶段对应的结果数量 */ function getStageResultCount(stageName: string, summary: ScanRecord["summary"]): number | undefined { if (!summary) return undefined switch (stageName) { case "subdomain_discovery": case "subdomainDiscovery": return summary.subdomains case "site_scan": case "siteScan": return summary.websites case "directory_scan": case "directoryScan": return summary.directories case "url_fetch": case "urlFetch": return summary.endpoints case "vuln_scan": case "vulnScan": return summary.vulnerabilities?.total default: return undefined } } /** * 从 ScanRecord 构建 ScanProgressData * * 阶段名称直接来自 engine_config 的 key,无需映射 * 阶段顺序按 order 字段排序,与 Flow 执行顺序一致 */ export function buildScanProgressData(scan: ScanRecord): ScanProgressData { const stages: StageDetail[] = [] if (scan.stageProgress) { // 按 order 排序后遍历 const sortedEntries = Object.entries(scan.stageProgress) .sort(([, a], [, b]) => (a.order ?? 0) - (b.order ?? 0)) for (const [stageName, progress] of sortedEntries) { const resultCount = progress.status === "completed" ? getStageResultCount(stageName, scan.summary) : undefined stages.push({ stage: stageName, status: progress.status, duration: formatDuration(progress.duration), detail: progress.detail || progress.error || progress.reason, resultCount, }) } } return { id: scan.id, targetName: scan.targetName, engineName: scan.engineName, status: scan.status, progress: scan.progress, currentStage: scan.currentStage, startedAt: scan.createdAt, errorMessage: scan.errorMessage, stages, } }