feat(报告): 添加任务报告导出功能并优化样式
- 在 AuditTasks 页面添加快速扫描和 Agent 任务的报告导出功能 - 在 ReportExportDialog 中优化颜色样式以支持亮色/暗色模式 - 修复报告生成器中字段为空时的处理逻辑
This commit is contained in:
parent
87c501b55c
commit
c0ac7d0544
|
|
@ -344,7 +344,9 @@ class ReportGenerator:
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if issue.description %}
|
||||
<div class="issue-desc">{{ issue.description }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if issue.code_snippet %}
|
||||
<div class="code-snippet mono">{{ issue.code_snippet }}</div>
|
||||
|
|
@ -413,13 +415,24 @@ class ReportGenerator:
|
|||
item['severity'] = item.get('severity', 'low')
|
||||
item['severity_label'] = sev_labels.get(item['severity'], 'UNKNOWN')
|
||||
item['line'] = item.get('line_number') or item.get('line')
|
||||
|
||||
|
||||
# 确保代码片段存在 (处理可能的字段名差异)
|
||||
code = item.get('code_snippet') or item.get('code') or item.get('context')
|
||||
if isinstance(code, list):
|
||||
code = '\n'.join(code)
|
||||
item['code_snippet'] = code
|
||||
|
||||
item['code_snippet'] = code if code else None
|
||||
|
||||
# 确保 description 不为 None
|
||||
desc = item.get('description')
|
||||
if not desc or desc == 'None':
|
||||
desc = item.get('title', '') # 如果没有描述,使用标题
|
||||
item['description'] = desc
|
||||
|
||||
# 确保 suggestion 不为 None
|
||||
suggestion = item.get('suggestion')
|
||||
if suggestion == 'None' or suggestion is None:
|
||||
item['suggestion'] = None
|
||||
|
||||
processed.append(item)
|
||||
return processed
|
||||
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ const FORMAT_CONFIG: Record<ReportFormat, {
|
|||
icon: <FileText className="w-5 h-5" />,
|
||||
extension: ".md",
|
||||
mime: "text/markdown",
|
||||
color: "text-sky-400",
|
||||
bgColor: "bg-sky-500/10 border-sky-500/30",
|
||||
color: "text-sky-600 dark:text-sky-400",
|
||||
bgColor: "bg-sky-100 dark:bg-sky-500/10 border-sky-300 dark:border-sky-500/30",
|
||||
},
|
||||
json: {
|
||||
label: "JSON",
|
||||
|
|
@ -100,8 +100,8 @@ const FORMAT_CONFIG: Record<ReportFormat, {
|
|||
icon: <FileJson className="w-5 h-5" />,
|
||||
extension: ".json",
|
||||
mime: "application/json",
|
||||
color: "text-amber-400",
|
||||
bgColor: "bg-amber-500/10 border-amber-500/30",
|
||||
color: "text-amber-600 dark:text-amber-400",
|
||||
bgColor: "bg-amber-100 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30",
|
||||
},
|
||||
html: {
|
||||
label: "HTML",
|
||||
|
|
@ -109,8 +109,8 @@ const FORMAT_CONFIG: Record<ReportFormat, {
|
|||
icon: <FileCode className="w-5 h-5" />,
|
||||
extension: ".html",
|
||||
mime: "text/html",
|
||||
color: "text-emerald-400",
|
||||
bgColor: "bg-emerald-500/10 border-emerald-500/30",
|
||||
color: "text-emerald-600 dark:text-emerald-400",
|
||||
bgColor: "bg-emerald-100 dark:bg-emerald-500/10 border-emerald-300 dark:border-emerald-500/30",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -126,10 +126,10 @@ const DEFAULT_EXPORT_OPTIONS: ExportOptions = {
|
|||
|
||||
function getSeverityColor(severity: string): string {
|
||||
const colors: Record<string, string> = {
|
||||
critical: "text-rose-400",
|
||||
high: "text-orange-400",
|
||||
medium: "text-amber-400",
|
||||
low: "text-sky-400",
|
||||
critical: "text-rose-600 dark:text-rose-400",
|
||||
high: "text-orange-600 dark:text-orange-400",
|
||||
medium: "text-amber-600 dark:text-amber-400",
|
||||
low: "text-sky-600 dark:text-sky-400",
|
||||
info: "text-muted-foreground",
|
||||
};
|
||||
return colors[severity.toLowerCase()] || colors.info;
|
||||
|
|
@ -145,10 +145,10 @@ function formatBytes(bytes: number): string {
|
|||
|
||||
// 获取安全评分颜色
|
||||
function getScoreColor(score: number): { text: string; bg: string; glow: string } {
|
||||
if (score >= 80) return { text: "text-emerald-400", bg: "stroke-emerald-500", glow: "drop-shadow-[0_0_8px_rgba(16,185,129,0.5)]" };
|
||||
if (score >= 60) return { text: "text-amber-400", bg: "stroke-amber-500", glow: "drop-shadow-[0_0_8px_rgba(245,158,11,0.5)]" };
|
||||
if (score >= 40) return { text: "text-orange-400", bg: "stroke-orange-500", glow: "drop-shadow-[0_0_8px_rgba(249,115,22,0.5)]" };
|
||||
return { text: "text-rose-400", bg: "stroke-rose-500", glow: "drop-shadow-[0_0_8px_rgba(244,63,94,0.5)]" };
|
||||
if (score >= 80) return { text: "text-emerald-600 dark:text-emerald-400", bg: "stroke-emerald-500", glow: "" };
|
||||
if (score >= 60) return { text: "text-amber-600 dark:text-amber-400", bg: "stroke-amber-500", glow: "" };
|
||||
if (score >= 40) return { text: "text-orange-600 dark:text-orange-400", bg: "stroke-orange-500", glow: "" };
|
||||
return { text: "text-rose-600 dark:text-rose-400", bg: "stroke-rose-500", glow: "" };
|
||||
}
|
||||
|
||||
// ============ Sub Components ============
|
||||
|
|
@ -181,7 +181,7 @@ const CircularProgress = memo(function CircularProgress({
|
|||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
className="text-foreground/50"
|
||||
className="text-slate-300 dark:text-slate-700"
|
||||
/>
|
||||
{/* Progress circle */}
|
||||
<circle
|
||||
|
|
@ -224,23 +224,23 @@ const EnhancedStatsPanel = memo(function EnhancedStatsPanel({
|
|||
label: "漏洞总数",
|
||||
value: totalFindings,
|
||||
color: "text-foreground",
|
||||
iconColor: "text-rose-400",
|
||||
iconColor: "text-rose-600 dark:text-rose-400",
|
||||
trend: totalFindings > 0 ? "up" : null,
|
||||
},
|
||||
{
|
||||
icon: <AlertTriangle className="w-4 h-4" />,
|
||||
label: "高危问题",
|
||||
value: criticalAndHigh,
|
||||
color: criticalAndHigh > 0 ? "text-rose-400" : "text-muted-foreground",
|
||||
iconColor: "text-orange-400",
|
||||
color: criticalAndHigh > 0 ? "text-rose-600 dark:text-rose-400" : "text-muted-foreground",
|
||||
iconColor: "text-orange-600 dark:text-orange-400",
|
||||
trend: criticalAndHigh > 0 ? "critical" : null,
|
||||
},
|
||||
{
|
||||
icon: <CheckCircle2 className="w-4 h-4" />,
|
||||
label: "已验证",
|
||||
value: verified,
|
||||
color: "text-emerald-400",
|
||||
iconColor: "text-emerald-400",
|
||||
color: "text-emerald-600 dark:text-emerald-400",
|
||||
iconColor: "text-emerald-600 dark:text-emerald-400",
|
||||
trend: null,
|
||||
},
|
||||
];
|
||||
|
|
@ -272,7 +272,7 @@ const EnhancedStatsPanel = memo(function EnhancedStatsPanel({
|
|||
{stat.value}
|
||||
</span>
|
||||
{stat.trend === "critical" && stat.value > 0 && (
|
||||
<Zap className="w-3 h-3 text-rose-400 animate-pulse" />
|
||||
<Zap className="w-3 h-3 text-rose-600 dark:text-rose-400" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -1622,21 +1622,14 @@ export const ReportExportDialog = memo(function ReportExportDialog({
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl h-[90vh] bg-gradient-to-b from-[#0a0a0f] to-[#0d0d14] border-border/50 p-0 gap-0 overflow-hidden shadow-2xl shadow-black/50">
|
||||
{/* Header - 增强设计 */}
|
||||
<div className="relative px-6 py-5 border-b border-border/50 bg-gradient-to-r from-[#0d0d12] via-[#0f0f16] to-[#0d0d12]">
|
||||
{/* 装饰性背景元素 */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-20 -right-20 w-40 h-40 bg-primary/5 rounded-full blur-3xl" />
|
||||
<div className="absolute -bottom-10 -left-10 w-32 h-32 bg-sky-500/5 rounded-full blur-2xl" />
|
||||
</div>
|
||||
|
||||
<DialogContent className="max-w-5xl h-[90vh] bg-background border-border p-0 gap-0 overflow-hidden shadow-2xl">
|
||||
{/* Header */}
|
||||
<div className="relative px-6 py-5 border-b border-border bg-card">
|
||||
<DialogHeader className="relative">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative p-3 rounded-xl bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/30 shadow-lg shadow-primary/10">
|
||||
<div className="relative p-3 rounded-xl bg-primary/10 border border-primary/30">
|
||||
<FileDown className="w-6 h-6 text-primary" />
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 rounded-full bg-emerald-500 border-2 border-background animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle className="text-xl font-bold text-foreground flex items-center gap-2">
|
||||
|
|
@ -1824,7 +1817,7 @@ export const ReportExportDialog = memo(function ReportExportDialog({
|
|||
</div>
|
||||
|
||||
{/* Footer - 增强设计 */}
|
||||
<div className="px-6 py-4 border-t border-border/50 bg-gradient-to-r from-[#0d0d12] via-[#0f0f16] to-[#0d0d12]">
|
||||
<div className="px-6 py-4 border-t border-border bg-card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border ${FORMAT_CONFIG[activeFormat].bgColor}`}>
|
||||
|
|
|
|||
|
|
@ -24,16 +24,20 @@ import {
|
|||
Shield,
|
||||
Terminal,
|
||||
Bot,
|
||||
Zap
|
||||
Zap,
|
||||
Download
|
||||
} from "lucide-react";
|
||||
import { api } from "@/shared/config/database";
|
||||
import { apiClient } from "@/shared/api/serverClient";
|
||||
import type { AuditTask } from "@/shared/types";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import CreateTaskDialog from "@/components/audit/CreateTaskDialog";
|
||||
import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog";
|
||||
import ExportReportDialog from "@/components/reports/ExportReportDialog";
|
||||
import { calculateTaskProgress } from "@/shared/utils/utils";
|
||||
import { getAgentTasks, cancelAgentTask, type AgentTask } from "@/shared/api/agentTasks";
|
||||
import { getAgentTasks, cancelAgentTask, getAgentFindings, type AgentTask, type AgentFinding } from "@/shared/api/agentTasks";
|
||||
import ReportExportDialog from "@/pages/AgentAudit/components/ReportExportDialog";
|
||||
|
||||
// Zombie task detection config
|
||||
const ZOMBIE_TIMEOUT = 180000; // 3 minutes without progress is potentially stuck
|
||||
|
|
@ -59,6 +63,14 @@ export default function AuditTasks() {
|
|||
const [agentTasks, setAgentTasks] = useState<AgentTask[]>([]);
|
||||
const [agentLoading, setAgentLoading] = useState(true);
|
||||
const [cancellingAgentTaskId, setCancellingAgentTaskId] = useState<string | null>(null);
|
||||
const [exportingTaskId, setExportingTaskId] = useState<string | null>(null);
|
||||
const [showExportDialog, setShowExportDialog] = useState(false);
|
||||
const [exportTask, setExportTask] = useState<AuditTask | null>(null);
|
||||
const [exportIssues, setExportIssues] = useState<any[]>([]);
|
||||
// Agent 任务导出对话框状态
|
||||
const [showAgentExportDialog, setShowAgentExportDialog] = useState(false);
|
||||
const [exportAgentTask, setExportAgentTask] = useState<AgentTask | null>(null);
|
||||
const [exportAgentFindings, setExportAgentFindings] = useState<AgentFinding[]>([]);
|
||||
|
||||
// Zombie task detection: track progress and time for each task
|
||||
const taskProgressRef = useRef<Map<string, { progress: number; time: number }>>(new Map());
|
||||
|
|
@ -201,6 +213,40 @@ export default function AuditTasks() {
|
|||
}
|
||||
};
|
||||
|
||||
// 打开快速扫描任务导出对话框
|
||||
const handleOpenExportDialog = async (task: AuditTask) => {
|
||||
try {
|
||||
setExportingTaskId(task.id);
|
||||
// 获取任务的问题列表
|
||||
const issuesResponse = await apiClient.get(`/tasks/${task.id}/issues`);
|
||||
setExportTask(task);
|
||||
setExportIssues(issuesResponse.data || []);
|
||||
setShowExportDialog(true);
|
||||
} catch (error: any) {
|
||||
console.error('获取问题列表失败:', error);
|
||||
toast.error("获取问题列表失败");
|
||||
} finally {
|
||||
setExportingTaskId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 打开 Agent 任务导出对话框
|
||||
const handleOpenAgentExportDialog = async (task: AgentTask) => {
|
||||
try {
|
||||
setExportingTaskId(task.id);
|
||||
// 获取任务的 findings 列表
|
||||
const findings = await getAgentFindings(task.id);
|
||||
setExportAgentTask(task);
|
||||
setExportAgentFindings(findings);
|
||||
setShowAgentExportDialog(true);
|
||||
} catch (error: any) {
|
||||
console.error('获取 findings 列表失败:', error);
|
||||
toast.error("获取审计结果失败");
|
||||
} finally {
|
||||
setExportingTaskId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTasks = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
|
@ -711,6 +757,17 @@ export default function AuditTasks() {
|
|||
</Button>
|
||||
</>
|
||||
)}
|
||||
{(task.status === 'completed' || (task.findings_count != null && task.findings_count > 0)) && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="cyber-btn-outline h-9"
|
||||
onClick={() => handleOpenAgentExportDialog(task)}
|
||||
disabled={exportingTaskId === task.id}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{exportingTaskId === task.id ? '加载中...' : '导出报告'}
|
||||
</Button>
|
||||
)}
|
||||
{/* 任务详情按钮 */}
|
||||
<Link to={`/agent-audit/${task.id}`}>
|
||||
<Button size="sm" className="cyber-btn-outline h-9">
|
||||
|
|
@ -838,6 +895,17 @@ export default function AuditTasks() {
|
|||
{cancellingTaskId === task.id ? '取消中...' : '取消'}
|
||||
</Button>
|
||||
)}
|
||||
{(task.issues_count > 0 || task.status === 'completed') && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="cyber-btn-outline h-9"
|
||||
onClick={() => handleOpenExportDialog(task)}
|
||||
disabled={exportingTaskId === task.id}
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{exportingTaskId === task.id ? '加载中...' : '导出报告'}
|
||||
</Button>
|
||||
)}
|
||||
<Link to={`/tasks/${task.id}`}>
|
||||
<Button size="sm" className="cyber-btn-outline h-9">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
|
|
@ -892,6 +960,26 @@ export default function AuditTasks() {
|
|||
taskId={currentTaskId}
|
||||
taskType="repository"
|
||||
/>
|
||||
|
||||
{/* 快速扫描任务导出对话框 */}
|
||||
{exportTask && (
|
||||
<ExportReportDialog
|
||||
open={showExportDialog}
|
||||
onOpenChange={setShowExportDialog}
|
||||
task={exportTask}
|
||||
issues={exportIssues}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Agent 任务导出对话框 */}
|
||||
{exportAgentTask && (
|
||||
<ReportExportDialog
|
||||
open={showAgentExportDialog}
|
||||
onOpenChange={setShowAgentExportDialog}
|
||||
task={exportAgentTask}
|
||||
findings={exportAgentFindings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue