mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 11:46:16 +08:00
feat(scan-history): add auto-refresh toggle and improve layout
- Add auto-refresh toggle switch to scan logs section for manual control - Implement flexible polling based on auto-refresh state and scan status - Restructure scan overview layout to use left-right split (stages + logs) - Move stage progress to left column with vulnerability statistics - Implement scrollable logs panel on right side with proper height constraints - Update component imports to use Switch and Label instead of Button - Add full-height flex layout to parent containers for proper scrolling - Refactor grid layout from 2-column to fixed-width left + flexible right - Update translations for new UI elements and labels - Improve responsive design with better flex constraints and min-height handling
This commit is contained in:
@@ -103,7 +103,7 @@ export default function ScanHistoryLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
|
||||
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6 h-full">
|
||||
{/* Header: Page label + Scan info */}
|
||||
<div className="flex items-center gap-2 text-sm px-4 lg:px-6">
|
||||
<span className="text-muted-foreground">{t("breadcrumb.scanHistory")}</span>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function ScanOverviewPage() {
|
||||
const scanId = Number(id)
|
||||
|
||||
return (
|
||||
<div className="px-4 lg:px-6">
|
||||
<div className="flex-1 flex flex-col min-h-0 px-4 lg:px-6">
|
||||
<ScanOverview scanId={scanId} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import React, { useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useTranslations, useLocale } from "next-intl"
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Server,
|
||||
Link2,
|
||||
FolderOpen,
|
||||
ShieldAlert,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Calendar,
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Loader2,
|
||||
PlayCircle,
|
||||
Cpu,
|
||||
HardDrive,
|
||||
} from "lucide-react"
|
||||
@@ -30,7 +28,9 @@ import {
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { useScan } from "@/hooks/use-scans"
|
||||
import { useScanLogs } from "@/hooks/use-scan-logs"
|
||||
import { ScanLogList } from "@/components/scan/scan-log-list"
|
||||
@@ -93,11 +93,14 @@ export function ScanOverview({ scanId }: ScanOverviewProps) {
|
||||
// Check if scan is running (for log polling)
|
||||
const isRunning = scan?.status === 'running' || scan?.status === 'initiated'
|
||||
|
||||
// Auto-refresh state (default: on when running)
|
||||
const [autoRefresh, setAutoRefresh] = useState(true)
|
||||
|
||||
// Logs hook
|
||||
const { logs, loading: logsLoading } = useScanLogs({
|
||||
scanId,
|
||||
enabled: !!scan,
|
||||
pollingInterval: isRunning ? 3000 : 0,
|
||||
pollingInterval: isRunning && autoRefresh ? 3000 : 0,
|
||||
})
|
||||
|
||||
// Format date helper
|
||||
@@ -233,7 +236,7 @@ export function ScanOverview({ scanId }: ScanOverviewProps) {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-6 flex-1 min-h-0">
|
||||
{/* Scan info + Status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-6 text-sm text-muted-foreground">
|
||||
@@ -299,126 +302,140 @@ export function ScanOverview({ scanId }: ScanOverviewProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Vulnerability + Stage Progress - Two columns */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Vulnerability Statistics Card */}
|
||||
<Link href={`/scan/history/${scanId}/vulnerabilities/`} className="block">
|
||||
<Card className="h-full hover:border-primary/50 transition-colors cursor-pointer">
|
||||
{/* Stage Progress + Logs - Left-Right Split Layout */}
|
||||
<div className="grid gap-4 md:grid-cols-[280px_1fr] flex-1 min-h-0">
|
||||
{/* Left Column: Stage Progress + Vulnerability Stats */}
|
||||
<div className="flex flex-col gap-4 min-h-0">
|
||||
{/* Stage Progress */}
|
||||
<Card className="flex-1 min-h-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldAlert className="h-4 w-4 text-muted-foreground" />
|
||||
<CardTitle className="text-sm font-medium">{t("vulnerabilitiesTitle")}</CardTitle>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs">
|
||||
{t("viewAll")}
|
||||
<ChevronRight className="h-3 w-3 ml-1" />
|
||||
</Button>
|
||||
<CardTitle className="text-sm font-medium">{t("stagesTitle")}</CardTitle>
|
||||
{scan.stageProgress && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{Object.values(scan.stageProgress).filter((p: any) => p.status === "completed").length}/
|
||||
{Object.keys(scan.stageProgress).length} {t("stagesCompleted")}
|
||||
</span>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Total count */}
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold">{vulnSummary.total}</span>
|
||||
<span className="text-sm text-muted-foreground">{t("totalFound")}</span>
|
||||
</div>
|
||||
|
||||
{/* Severity breakdown */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500" />
|
||||
<span className="text-sm text-muted-foreground">{t("severity.critical")}</span>
|
||||
<span className="text-sm font-medium ml-auto">{vulnSummary.critical}</span>
|
||||
<CardContent className="pt-0 flex flex-col flex-1 min-h-0">
|
||||
{scan.stageProgress && Object.keys(scan.stageProgress).length > 0 ? (
|
||||
<div className="space-y-1 flex-1 min-h-0 overflow-y-auto pr-1">
|
||||
{Object.entries(scan.stageProgress)
|
||||
.sort(([, a], [, b]) => ((a as any).order ?? 0) - ((b as any).order ?? 0))
|
||||
.map(([stageName, progress]) => {
|
||||
const stageProgress = progress as any
|
||||
const isRunning = stageProgress.status === "running"
|
||||
return (
|
||||
<div
|
||||
key={stageName}
|
||||
className={cn(
|
||||
"flex items-center justify-between py-2 rounded-md transition-colors text-sm",
|
||||
isRunning && "bg-[#d29922]/10 border border-[#d29922]/30",
|
||||
stageProgress.status === "completed" && "text-muted-foreground",
|
||||
stageProgress.status === "failed" && "bg-[#da3633]/10 text-[#da3633]",
|
||||
stageProgress.status === "cancelled" && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<StageStatusIcon status={stageProgress.status} />
|
||||
<span className={cn("truncate", isRunning && "font-medium text-foreground")}>
|
||||
{tProgress(`stages.${stageName}`)}
|
||||
</span>
|
||||
{isRunning && (
|
||||
<span className="text-[10px] text-[#d29922] shrink-0">←</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground font-mono shrink-0 ml-2">
|
||||
{stageProgress.status === "completed" && stageProgress.duration
|
||||
? formatStageDuration(stageProgress.duration)
|
||||
: stageProgress.status === "running"
|
||||
? tProgress("stage_running")
|
||||
: stageProgress.status === "pending"
|
||||
? "--"
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-orange-500" />
|
||||
<span className="text-sm text-muted-foreground">{t("severity.high")}</span>
|
||||
<span className="text-sm font-medium ml-auto">{vulnSummary.high}</span>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground text-center py-4">
|
||||
{t("noStages")}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500" />
|
||||
<span className="text-sm text-muted-foreground">{t("severity.medium")}</span>
|
||||
<span className="text-sm font-medium ml-auto">{vulnSummary.medium}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-blue-500" />
|
||||
<span className="text-sm text-muted-foreground">{t("severity.low")}</span>
|
||||
<span className="text-sm font-medium ml-auto">{vulnSummary.low}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
{/* Stage Progress Card */}
|
||||
<Card className="h-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 text-muted-foreground" />
|
||||
<CardTitle className="text-sm font-medium">{t("stagesTitle")}</CardTitle>
|
||||
{/* Vulnerability Stats - Compact */}
|
||||
<Link href={`/scan/history/${scanId}/vulnerabilities/`} className="block">
|
||||
<Card className="hover:border-primary/50 transition-colors cursor-pointer">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{t("vulnerabilitiesTitle")}</CardTitle>
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-red-500" />
|
||||
<span className="text-sm font-medium">{vulnSummary.critical}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-orange-500" />
|
||||
<span className="text-sm font-medium">{vulnSummary.high}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-yellow-500" />
|
||||
<span className="text-sm font-medium">{vulnSummary.medium}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-blue-500" />
|
||||
<span className="text-sm font-medium">{vulnSummary.low}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground ml-auto">
|
||||
{t("totalVulns", { count: vulnSummary.total })}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Logs */}
|
||||
<div className="flex flex-col min-h-0 rounded-lg overflow-hidden border">
|
||||
<div className="flex-1 min-h-0">
|
||||
<ScanLogList logs={logs} loading={logsLoading} />
|
||||
</div>
|
||||
{/* Bottom status bar */}
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-muted/50 border-t text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<span>{t("logsTitle")}</span>
|
||||
<Separator orientation="vertical" className="h-3" />
|
||||
<span>{logs.length} 条记录</span>
|
||||
{isRunning && autoRefresh && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-3" />
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="size-1.5 rounded-full bg-green-500 animate-pulse" />
|
||||
每 3 秒刷新
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{scan.stageProgress && Object.keys(scan.stageProgress).length > 0 ? (
|
||||
<div className="space-y-2 max-h-[200px] overflow-y-auto">
|
||||
{Object.entries(scan.stageProgress)
|
||||
.sort(([, a], [, b]) => ((a as any).order ?? 0) - ((b as any).order ?? 0))
|
||||
.map(([stageName, progress]) => {
|
||||
const stageProgress = progress as any
|
||||
return (
|
||||
<div
|
||||
key={stageName}
|
||||
className={cn(
|
||||
"flex items-center justify-between py-2 px-3 rounded-lg transition-colors",
|
||||
stageProgress.status === "running" && "bg-[#d29922]/10 border border-[#d29922]/20",
|
||||
stageProgress.status === "completed" && "bg-muted/50",
|
||||
stageProgress.status === "failed" && "bg-[#da3633]/10",
|
||||
stageProgress.status === "cancelled" && "bg-[#848d97]/10",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<StageStatusIcon status={stageProgress.status} />
|
||||
<span className="text-sm font-medium">{tProgress(`stages.${stageName}`)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-right">
|
||||
{stageProgress.status === "running" && (
|
||||
<Badge variant="outline" className="bg-[#d29922]/10 text-[#d29922] border-[#d29922]/20 text-xs">
|
||||
{tProgress("stage_running")}
|
||||
</Badge>
|
||||
)}
|
||||
{stageProgress.status === "completed" && stageProgress.duration && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{formatStageDuration(stageProgress.duration)}
|
||||
</span>
|
||||
)}
|
||||
{stageProgress.status === "pending" && (
|
||||
<span className="text-xs text-muted-foreground">{tProgress("stage_pending")}</span>
|
||||
)}
|
||||
{stageProgress.status === "failed" && (
|
||||
<Badge variant="outline" className="bg-[#da3633]/10 text-[#da3633] border-[#da3633]/20 text-xs">
|
||||
{tProgress("stage_failed")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground text-center py-4">
|
||||
{t("noStages")}
|
||||
{isRunning && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="log-auto-refresh"
|
||||
checked={autoRefresh}
|
||||
onCheckedChange={setAutoRefresh}
|
||||
className="scale-75"
|
||||
/>
|
||||
<Label htmlFor="log-auto-refresh" className="text-xs cursor-pointer">
|
||||
自动刷新
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Scan Logs */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">{t("logsTitle")}</h3>
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<ScanLogList logs={logs} loading={logsLoading} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useRef, useMemo } from "react"
|
||||
import { useMemo, useRef } from "react"
|
||||
import { AnsiLogViewer } from "@/components/settings/system-logs"
|
||||
import type { ScanLog } from "@/services/scan.service"
|
||||
|
||||
interface ScanLogListProps {
|
||||
@@ -14,98 +15,68 @@ interface ScanLogListProps {
|
||||
function formatTime(isoString: string): string {
|
||||
try {
|
||||
const date = new Date(isoString)
|
||||
return date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
})
|
||||
const h = String(date.getHours()).padStart(2, '0')
|
||||
const m = String(date.getMinutes()).padStart(2, '0')
|
||||
const s = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${h}:${m}:${s}`
|
||||
} catch {
|
||||
return isoString
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 转义,防止 XSS
|
||||
*/
|
||||
function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描日志列表组件
|
||||
*
|
||||
* 特性:
|
||||
* - 预渲染 HTML 字符串,减少 DOM 节点提升性能
|
||||
* - 颜色区分:info=默认, warning=黄色, error=红色
|
||||
* - 自动滚动到底部
|
||||
* 复用 AnsiLogViewer 组件
|
||||
*/
|
||||
export function ScanLogList({ logs, loading }: ScanLogListProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const isAtBottomRef = useRef(true) // 跟踪用户是否在底部
|
||||
// 稳定的 content 引用,只有内容真正变化时才更新
|
||||
const contentRef = useRef('')
|
||||
const lastLogCountRef = useRef(0)
|
||||
const lastLogIdRef = useRef<number | null>(null)
|
||||
|
||||
// 预渲染 HTML 字符串
|
||||
const htmlContent = useMemo(() => {
|
||||
// 将日志转换为纯文本格式
|
||||
const content = useMemo(() => {
|
||||
if (logs.length === 0) return ''
|
||||
|
||||
return logs.map(log => {
|
||||
// 检查是否真正需要更新
|
||||
const lastLog = logs[logs.length - 1]
|
||||
if (
|
||||
logs.length === lastLogCountRef.current &&
|
||||
lastLog?.id === lastLogIdRef.current
|
||||
) {
|
||||
// 日志没有变化,返回缓存的 content
|
||||
return contentRef.current
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
lastLogCountRef.current = logs.length
|
||||
lastLogIdRef.current = lastLog?.id ?? null
|
||||
|
||||
const newContent = logs.map(log => {
|
||||
const time = formatTime(log.createdAt)
|
||||
const content = escapeHtml(log.content)
|
||||
const levelStyle = log.level === 'error'
|
||||
? 'color:#ef4444'
|
||||
: log.level === 'warning'
|
||||
? 'color:#eab308'
|
||||
: ''
|
||||
|
||||
return `<div style="line-height:1.625;word-break:break-all;${levelStyle}"><span style="color:#6b7280">${time}</span> ${content}</div>`
|
||||
}).join('')
|
||||
const levelTag = log.level.toUpperCase()
|
||||
return `[${time}] [${levelTag}] ${log.content}`
|
||||
}).join('\n')
|
||||
|
||||
contentRef.current = newContent
|
||||
return newContent
|
||||
}, [logs])
|
||||
|
||||
// 监听滚动事件,检测用户是否在底部
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
|
||||
const handleScroll = () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = container
|
||||
// 允许 30px 的容差,认为在底部附近
|
||||
isAtBottomRef.current = scrollHeight - scrollTop - clientHeight < 30
|
||||
}
|
||||
|
||||
container.addEventListener('scroll', handleScroll)
|
||||
return () => container.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
if (loading && logs.length === 0) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center bg-[#1e1e1e] text-[#808080]">
|
||||
加载中...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 只有用户在底部时才自动滚动
|
||||
useEffect(() => {
|
||||
if (containerRef.current && isAtBottomRef.current) {
|
||||
containerRef.current.scrollTop = containerRef.current.scrollHeight
|
||||
}
|
||||
}, [htmlContent])
|
||||
if (logs.length === 0) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center bg-[#1e1e1e] text-[#808080]">
|
||||
暂无日志
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-[400px] overflow-y-auto font-mono text-[11px] p-3 bg-muted/30 rounded-lg"
|
||||
>
|
||||
{logs.length === 0 && !loading && (
|
||||
<div className="text-muted-foreground text-center py-8">
|
||||
暂无日志
|
||||
</div>
|
||||
)}
|
||||
{htmlContent && (
|
||||
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
||||
)}
|
||||
{loading && logs.length === 0 && (
|
||||
<div className="text-muted-foreground text-center py-8">
|
||||
加载中...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return <AnsiLogViewer content={content} />
|
||||
}
|
||||
|
||||
@@ -83,16 +83,18 @@ export function useScanLogs({
|
||||
return () => {
|
||||
isMounted.current = false
|
||||
}
|
||||
}, [scanId, enabled])
|
||||
}, [scanId, enabled, fetchLogs])
|
||||
|
||||
// 轮询
|
||||
useEffect(() => {
|
||||
if (!enabled) return
|
||||
|
||||
// pollingInterval <= 0 表示禁用轮询(避免 setInterval(0) 导致高频请求/卡顿)
|
||||
if (!pollingInterval || pollingInterval <= 0) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchLogs(true) // 增量查询
|
||||
fetchLogs(true) // 增量查询
|
||||
}, pollingInterval)
|
||||
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [enabled, pollingInterval, fetchLogs])
|
||||
|
||||
|
||||
@@ -747,9 +747,11 @@
|
||||
"assetsTitle": "Discovered Assets",
|
||||
"vulnerabilitiesTitle": "Vulnerabilities",
|
||||
"stagesTitle": "Scan Progress",
|
||||
"stagesCompleted": "completed",
|
||||
"logsTitle": "Scan Logs",
|
||||
"noStages": "No stage progress available",
|
||||
"totalFound": "total found",
|
||||
"totalVulns": "{count} total",
|
||||
"viewAll": "View All",
|
||||
"cards": {
|
||||
"websites": "Websites",
|
||||
|
||||
@@ -747,9 +747,11 @@
|
||||
"assetsTitle": "发现的资产",
|
||||
"vulnerabilitiesTitle": "漏洞统计",
|
||||
"stagesTitle": "扫描进度",
|
||||
"stagesCompleted": "完成",
|
||||
"logsTitle": "扫描日志",
|
||||
"noStages": "暂无阶段进度",
|
||||
"totalFound": "个漏洞",
|
||||
"totalVulns": "共 {count} 个",
|
||||
"viewAll": "查看全部",
|
||||
"cards": {
|
||||
"websites": "网站",
|
||||
|
||||
Reference in New Issue
Block a user