Skip to content

Commit c0ac7d0

Browse files
committed
feat(报告): 添加任务报告导出功能并优化样式
- 在 AuditTasks 页面添加快速扫描和 Agent 任务的报告导出功能 - 在 ReportExportDialog 中优化颜色样式以支持亮色/暗色模式 - 修复报告生成器中字段为空时的处理逻辑
1 parent 87c501b commit c0ac7d0

File tree

3 files changed

+132
-38
lines changed

3 files changed

+132
-38
lines changed

backend/app/services/report_generator.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,9 @@ class ReportGenerator:
344344
</div>
345345
{% endif %}
346346
347+
{% if issue.description %}
347348
<div class="issue-desc">{{ issue.description }}</div>
349+
{% endif %}
348350
349351
{% if issue.code_snippet %}
350352
<div class="code-snippet mono">{{ issue.code_snippet }}</div>
@@ -413,13 +415,24 @@ def _process_issues(cls, issues: List[Dict]) -> List[Dict]:
413415
item['severity'] = item.get('severity', 'low')
414416
item['severity_label'] = sev_labels.get(item['severity'], 'UNKNOWN')
415417
item['line'] = item.get('line_number') or item.get('line')
416-
418+
417419
# 确保代码片段存在 (处理可能的字段名差异)
418420
code = item.get('code_snippet') or item.get('code') or item.get('context')
419421
if isinstance(code, list):
420422
code = '\n'.join(code)
421-
item['code_snippet'] = code
422-
423+
item['code_snippet'] = code if code else None
424+
425+
# 确保 description 不为 None
426+
desc = item.get('description')
427+
if not desc or desc == 'None':
428+
desc = item.get('title', '') # 如果没有描述,使用标题
429+
item['description'] = desc
430+
431+
# 确保 suggestion 不为 None
432+
suggestion = item.get('suggestion')
433+
if suggestion == 'None' or suggestion is None:
434+
item['suggestion'] = None
435+
423436
processed.append(item)
424437
return processed
425438

frontend/src/pages/AgentAudit/components/ReportExportDialog.tsx

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -91,26 +91,26 @@ const FORMAT_CONFIG: Record<ReportFormat, {
9191
icon: <FileText className="w-5 h-5" />,
9292
extension: ".md",
9393
mime: "text/markdown",
94-
color: "text-sky-400",
95-
bgColor: "bg-sky-500/10 border-sky-500/30",
94+
color: "text-sky-600 dark:text-sky-400",
95+
bgColor: "bg-sky-100 dark:bg-sky-500/10 border-sky-300 dark:border-sky-500/30",
9696
},
9797
json: {
9898
label: "JSON",
9999
description: "结构化数据格式",
100100
icon: <FileJson className="w-5 h-5" />,
101101
extension: ".json",
102102
mime: "application/json",
103-
color: "text-amber-400",
104-
bgColor: "bg-amber-500/10 border-amber-500/30",
103+
color: "text-amber-600 dark:text-amber-400",
104+
bgColor: "bg-amber-100 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30",
105105
},
106106
html: {
107107
label: "HTML",
108108
description: "网页展示格式",
109109
icon: <FileCode className="w-5 h-5" />,
110110
extension: ".html",
111111
mime: "text/html",
112-
color: "text-emerald-400",
113-
bgColor: "bg-emerald-500/10 border-emerald-500/30",
112+
color: "text-emerald-600 dark:text-emerald-400",
113+
bgColor: "bg-emerald-100 dark:bg-emerald-500/10 border-emerald-300 dark:border-emerald-500/30",
114114
},
115115
};
116116

@@ -126,10 +126,10 @@ const DEFAULT_EXPORT_OPTIONS: ExportOptions = {
126126

127127
function getSeverityColor(severity: string): string {
128128
const colors: Record<string, string> = {
129-
critical: "text-rose-400",
130-
high: "text-orange-400",
131-
medium: "text-amber-400",
132-
low: "text-sky-400",
129+
critical: "text-rose-600 dark:text-rose-400",
130+
high: "text-orange-600 dark:text-orange-400",
131+
medium: "text-amber-600 dark:text-amber-400",
132+
low: "text-sky-600 dark:text-sky-400",
133133
info: "text-muted-foreground",
134134
};
135135
return colors[severity.toLowerCase()] || colors.info;
@@ -145,10 +145,10 @@ function formatBytes(bytes: number): string {
145145

146146
// 获取安全评分颜色
147147
function getScoreColor(score: number): { text: string; bg: string; glow: string } {
148-
if (score >= 80) return { text: "text-emerald-400", bg: "stroke-emerald-500", glow: "drop-shadow-[0_0_8px_rgba(16,185,129,0.5)]" };
149-
if (score >= 60) return { text: "text-amber-400", bg: "stroke-amber-500", glow: "drop-shadow-[0_0_8px_rgba(245,158,11,0.5)]" };
150-
if (score >= 40) return { text: "text-orange-400", bg: "stroke-orange-500", glow: "drop-shadow-[0_0_8px_rgba(249,115,22,0.5)]" };
151-
return { text: "text-rose-400", bg: "stroke-rose-500", glow: "drop-shadow-[0_0_8px_rgba(244,63,94,0.5)]" };
148+
if (score >= 80) return { text: "text-emerald-600 dark:text-emerald-400", bg: "stroke-emerald-500", glow: "" };
149+
if (score >= 60) return { text: "text-amber-600 dark:text-amber-400", bg: "stroke-amber-500", glow: "" };
150+
if (score >= 40) return { text: "text-orange-600 dark:text-orange-400", bg: "stroke-orange-500", glow: "" };
151+
return { text: "text-rose-600 dark:text-rose-400", bg: "stroke-rose-500", glow: "" };
152152
}
153153

154154
// ============ Sub Components ============
@@ -181,7 +181,7 @@ const CircularProgress = memo(function CircularProgress({
181181
fill="none"
182182
stroke="currentColor"
183183
strokeWidth={strokeWidth}
184-
className="text-foreground/50"
184+
className="text-slate-300 dark:text-slate-700"
185185
/>
186186
{/* Progress circle */}
187187
<circle
@@ -224,23 +224,23 @@ const EnhancedStatsPanel = memo(function EnhancedStatsPanel({
224224
label: "漏洞总数",
225225
value: totalFindings,
226226
color: "text-foreground",
227-
iconColor: "text-rose-400",
227+
iconColor: "text-rose-600 dark:text-rose-400",
228228
trend: totalFindings > 0 ? "up" : null,
229229
},
230230
{
231231
icon: <AlertTriangle className="w-4 h-4" />,
232232
label: "高危问题",
233233
value: criticalAndHigh,
234-
color: criticalAndHigh > 0 ? "text-rose-400" : "text-muted-foreground",
235-
iconColor: "text-orange-400",
234+
color: criticalAndHigh > 0 ? "text-rose-600 dark:text-rose-400" : "text-muted-foreground",
235+
iconColor: "text-orange-600 dark:text-orange-400",
236236
trend: criticalAndHigh > 0 ? "critical" : null,
237237
},
238238
{
239239
icon: <CheckCircle2 className="w-4 h-4" />,
240240
label: "已验证",
241241
value: verified,
242-
color: "text-emerald-400",
243-
iconColor: "text-emerald-400",
242+
color: "text-emerald-600 dark:text-emerald-400",
243+
iconColor: "text-emerald-600 dark:text-emerald-400",
244244
trend: null,
245245
},
246246
];
@@ -272,7 +272,7 @@ const EnhancedStatsPanel = memo(function EnhancedStatsPanel({
272272
{stat.value}
273273
</span>
274274
{stat.trend === "critical" && stat.value > 0 && (
275-
<Zap className="w-3 h-3 text-rose-400 animate-pulse" />
275+
<Zap className="w-3 h-3 text-rose-600 dark:text-rose-400" />
276276
)}
277277
</div>
278278

@@ -1622,21 +1622,14 @@ export const ReportExportDialog = memo(function ReportExportDialog({
16221622

16231623
return (
16241624
<Dialog open={open} onOpenChange={onOpenChange}>
1625-
<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">
1626-
{/* Header - 增强设计 */}
1627-
<div className="relative px-6 py-5 border-b border-border/50 bg-gradient-to-r from-[#0d0d12] via-[#0f0f16] to-[#0d0d12]">
1628-
{/* 装饰性背景元素 */}
1629-
<div className="absolute inset-0 overflow-hidden">
1630-
<div className="absolute -top-20 -right-20 w-40 h-40 bg-primary/5 rounded-full blur-3xl" />
1631-
<div className="absolute -bottom-10 -left-10 w-32 h-32 bg-sky-500/5 rounded-full blur-2xl" />
1632-
</div>
1633-
1625+
<DialogContent className="max-w-5xl h-[90vh] bg-background border-border p-0 gap-0 overflow-hidden shadow-2xl">
1626+
{/* Header */}
1627+
<div className="relative px-6 py-5 border-b border-border bg-card">
16341628
<DialogHeader className="relative">
16351629
<div className="flex items-center justify-between">
16361630
<div className="flex items-center gap-4">
1637-
<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">
1631+
<div className="relative p-3 rounded-xl bg-primary/10 border border-primary/30">
16381632
<FileDown className="w-6 h-6 text-primary" />
1639-
<div className="absolute -top-1 -right-1 w-3 h-3 rounded-full bg-emerald-500 border-2 border-background animate-pulse" />
16401633
</div>
16411634
<div>
16421635
<DialogTitle className="text-xl font-bold text-foreground flex items-center gap-2">
@@ -1824,7 +1817,7 @@ export const ReportExportDialog = memo(function ReportExportDialog({
18241817
</div>
18251818

18261819
{/* Footer - 增强设计 */}
1827-
<div className="px-6 py-4 border-t border-border/50 bg-gradient-to-r from-[#0d0d12] via-[#0f0f16] to-[#0d0d12]">
1820+
<div className="px-6 py-4 border-t border-border bg-card">
18281821
<div className="flex items-center justify-between">
18291822
<div className="flex items-center gap-3 text-xs text-muted-foreground">
18301823
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-lg border ${FORMAT_CONFIG[activeFormat].bgColor}`}>

frontend/src/pages/AuditTasks.tsx

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,20 @@ import {
2424
Shield,
2525
Terminal,
2626
Bot,
27-
Zap
27+
Zap,
28+
Download
2829
} from "lucide-react";
2930
import { api } from "@/shared/config/database";
31+
import { apiClient } from "@/shared/api/serverClient";
3032
import type { AuditTask } from "@/shared/types";
3133
import { Link, useNavigate } from "react-router-dom";
3234
import { toast } from "sonner";
3335
import CreateTaskDialog from "@/components/audit/CreateTaskDialog";
3436
import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog";
37+
import ExportReportDialog from "@/components/reports/ExportReportDialog";
3538
import { calculateTaskProgress } from "@/shared/utils/utils";
36-
import { getAgentTasks, cancelAgentTask, type AgentTask } from "@/shared/api/agentTasks";
39+
import { getAgentTasks, cancelAgentTask, getAgentFindings, type AgentTask, type AgentFinding } from "@/shared/api/agentTasks";
40+
import ReportExportDialog from "@/pages/AgentAudit/components/ReportExportDialog";
3741

3842
// Zombie task detection config
3943
const ZOMBIE_TIMEOUT = 180000; // 3 minutes without progress is potentially stuck
@@ -59,6 +63,14 @@ export default function AuditTasks() {
5963
const [agentTasks, setAgentTasks] = useState<AgentTask[]>([]);
6064
const [agentLoading, setAgentLoading] = useState(true);
6165
const [cancellingAgentTaskId, setCancellingAgentTaskId] = useState<string | null>(null);
66+
const [exportingTaskId, setExportingTaskId] = useState<string | null>(null);
67+
const [showExportDialog, setShowExportDialog] = useState(false);
68+
const [exportTask, setExportTask] = useState<AuditTask | null>(null);
69+
const [exportIssues, setExportIssues] = useState<any[]>([]);
70+
// Agent 任务导出对话框状态
71+
const [showAgentExportDialog, setShowAgentExportDialog] = useState(false);
72+
const [exportAgentTask, setExportAgentTask] = useState<AgentTask | null>(null);
73+
const [exportAgentFindings, setExportAgentFindings] = useState<AgentFinding[]>([]);
6274

6375
// Zombie task detection: track progress and time for each task
6476
const taskProgressRef = useRef<Map<string, { progress: number; time: number }>>(new Map());
@@ -201,6 +213,40 @@ export default function AuditTasks() {
201213
}
202214
};
203215

216+
// 打开快速扫描任务导出对话框
217+
const handleOpenExportDialog = async (task: AuditTask) => {
218+
try {
219+
setExportingTaskId(task.id);
220+
// 获取任务的问题列表
221+
const issuesResponse = await apiClient.get(`/tasks/${task.id}/issues`);
222+
setExportTask(task);
223+
setExportIssues(issuesResponse.data || []);
224+
setShowExportDialog(true);
225+
} catch (error: any) {
226+
console.error('获取问题列表失败:', error);
227+
toast.error("获取问题列表失败");
228+
} finally {
229+
setExportingTaskId(null);
230+
}
231+
};
232+
233+
// 打开 Agent 任务导出对话框
234+
const handleOpenAgentExportDialog = async (task: AgentTask) => {
235+
try {
236+
setExportingTaskId(task.id);
237+
// 获取任务的 findings 列表
238+
const findings = await getAgentFindings(task.id);
239+
setExportAgentTask(task);
240+
setExportAgentFindings(findings);
241+
setShowAgentExportDialog(true);
242+
} catch (error: any) {
243+
console.error('获取 findings 列表失败:', error);
244+
toast.error("获取审计结果失败");
245+
} finally {
246+
setExportingTaskId(null);
247+
}
248+
};
249+
204250
const loadTasks = async () => {
205251
try {
206252
setLoading(true);
@@ -711,6 +757,17 @@ export default function AuditTasks() {
711757
</Button>
712758
</>
713759
)}
760+
{(task.status === 'completed' || (task.findings_count != null && task.findings_count > 0)) && (
761+
<Button
762+
size="sm"
763+
className="cyber-btn-outline h-9"
764+
onClick={() => handleOpenAgentExportDialog(task)}
765+
disabled={exportingTaskId === task.id}
766+
>
767+
<Download className="w-4 h-4 mr-2" />
768+
{exportingTaskId === task.id ? '加载中...' : '导出报告'}
769+
</Button>
770+
)}
714771
{/* 任务详情按钮 */}
715772
<Link to={`/agent-audit/${task.id}`}>
716773
<Button size="sm" className="cyber-btn-outline h-9">
@@ -838,6 +895,17 @@ export default function AuditTasks() {
838895
{cancellingTaskId === task.id ? '取消中...' : '取消'}
839896
</Button>
840897
)}
898+
{(task.issues_count > 0 || task.status === 'completed') && (
899+
<Button
900+
size="sm"
901+
className="cyber-btn-outline h-9"
902+
onClick={() => handleOpenExportDialog(task)}
903+
disabled={exportingTaskId === task.id}
904+
>
905+
<Download className="w-4 h-4 mr-2" />
906+
{exportingTaskId === task.id ? '加载中...' : '导出报告'}
907+
</Button>
908+
)}
841909
<Link to={`/tasks/${task.id}`}>
842910
<Button size="sm" className="cyber-btn-outline h-9">
843911
<FileText className="w-4 h-4 mr-2" />
@@ -892,6 +960,26 @@ export default function AuditTasks() {
892960
taskId={currentTaskId}
893961
taskType="repository"
894962
/>
963+
964+
{/* 快速扫描任务导出对话框 */}
965+
{exportTask && (
966+
<ExportReportDialog
967+
open={showExportDialog}
968+
onOpenChange={setShowExportDialog}
969+
task={exportTask}
970+
issues={exportIssues}
971+
/>
972+
)}
973+
974+
{/* Agent 任务导出对话框 */}
975+
{exportAgentTask && (
976+
<ReportExportDialog
977+
open={showAgentExportDialog}
978+
onOpenChange={setShowAgentExportDialog}
979+
task={exportAgentTask}
980+
findings={exportAgentFindings}
981+
/>
982+
)}
895983
</div>
896984
);
897985
}

0 commit comments

Comments
 (0)