diff --git a/web/src/app/(main)/function/findings/_components/findings-table.tsx b/web/src/app/(main)/function/findings/_components/findings-table.tsx index cfa65c8..b308d1b 100644 --- a/web/src/app/(main)/function/findings/_components/findings-table.tsx +++ b/web/src/app/(main)/function/findings/_components/findings-table.tsx @@ -9,13 +9,15 @@ import { ChevronRightIcon, FileTextIcon, FlaskConicalIcon, + FolderIcon, + FolderOpenIcon, RotateCcwIcon, ShieldAlertIcon, Trash2Icon, } from "lucide-react"; import { CopyButton } from "@/components/copy-button"; -import { FindingCaseFolder } from "@/components/finding-case-list"; +import { FindingCaseMembers, FindingSeverityCounts } from "@/components/finding-case-list"; import { Markdown } from "@/components/markdown"; import { StatusBadge } from "@/components/status-badge"; import { @@ -92,6 +94,7 @@ const COLUMN_COUNT = 9; interface FindingsTableProps { items: Finding[]; caseRows?: FindingCaseListRow[]; + hideHeader?: boolean; matchedIds?: number[]; presentation?: "records" | "task" | "asset"; selectedIds: Set; @@ -119,6 +122,7 @@ interface FindingsTableProps { export function FindingsTable({ items, caseRows, + hideHeader = false, matchedIds, presentation = "records", selectedIds, @@ -149,67 +153,65 @@ export function FindingsTable({ const rows: FindingCaseListRow[] = caseRows ?? items.map((finding) => ({ finding, matched_ids: [] })); + const shared: Omit = { + presentation, + selectedIds, + onToggleSelected, + onToggleSelectedPage, + expandedKey, + onToggleRow, + reports, + edit, + onEditChange, + saving, + onSave, + onStatusChange, + onRetest, + activeRetests, + onDeepen, + onDelete, + }; + return ( /* 固定列宽保证展开内容不撑开表格;窄屏只在表格内部横向滚动。 */ - - - - onToggleSelectedPage(selectableIds, checked === true)} - aria-label={selectAllLabel} - /> - - - 严重度 - 漏洞名称 - 资产 - 状态 - 所属任务 - 时间 - 操作 - - + {hideHeader ? ( + + + + + + + + + + + + ) : null} + {!hideHeader ? ( + + + + onToggleSelectedPage(selectableIds, checked === true)} + aria-label={selectAllLabel} + /> + + + 严重度 + 漏洞名称 + 资产 + 状态 + 所属任务 + 时间 + 操作 + + + ) : null} {rows.map((row) => { - if (row.case) - return ( - - - ( - - )} - /> - - - ); + if (row.case) return ; const f = row.finding; if (!f) return null; const rowKey = findingRowKey(f); @@ -508,3 +510,120 @@ export function FindingsTable({
); } + +// Folder summaries occupy the same columns as the current view's ordinary rows. +// Expanded members reuse those rows without a second heading or column header. +function FindingFolderTableRows({ + row, + shared, +}: { + row: FindingCaseListRow; + shared: Omit; +}) { + const [open, setOpen] = React.useState(false); + const group = row.case; + if (!group) return null; + const taskLabel = row.task_name?.trim() ? row.task_name : row.task_description; + const assets = row.assets ?? []; + const assetCount = row.asset_count ?? assets.length; + return ( + <> + + + + + + + + + +
+ {open ? ( + + ) : ( + + )} +
+ + {group.title} + + + {group.count} 条上报 · {group.report_version !== group.version ? "统一报告待更新" : "统一报告已生成"} + +
+
+
+ + {assets.length ? ( +
+ {assets.slice(0, 3).map((a) => ( + + {a.label} + + ))} + {assetCount > 3 ? +{assetCount - 3} : null} +
+ ) : ( + — + )} +
+ + + — + + + + {group.task_id ? ( + + {taskLabel || `任务 #${group.task_id}`} + + + ) : ( + — + )} + + + {row.last_found_at ? fmtTime(row.last_found_at) : "—"} + + + + +
+ {open ? ( + + +
+ ( + + )} + /> +
+
+
+ ) : null} + + ); +} diff --git a/web/src/components/finding-case-list.tsx b/web/src/components/finding-case-list.tsx index 10500a3..105ce1d 100644 --- a/web/src/components/finding-case-list.tsx +++ b/web/src/components/finding-case-list.tsx @@ -463,7 +463,7 @@ export function FindingCaseFolder({ onSelect, contextTask, readOnly = false, - presentation = "records", + presentation = "compact", className, renderRecords, }: { @@ -512,46 +512,48 @@ export function FindingCaseFolder({ 漏洞文件夹 · {group.count} 条原始上报 ·{" "} {group.report_version !== group.version ? "统一报告待更新" : "统一报告已生成"} -
- {presentation !== "task" && group.task_id ? ( - - 所属任务 #{group.task_id} - {taskLabel ? ` · ${taskLabel}` : ""} - - ) : null} - {(row.assets ?? []).length ? ( - - 资产: - {row.assets?.map((a) => ( - - {a.label} - - ))} - {(row.asset_count ?? 0) > 4 ? +{(row.asset_count ?? 0) - 4} : null} - - ) : ( - 未关联资产 - )} - {row.last_found_at ? ( - - ) : null} -
+ {presentation !== "compact" ? ( +
+ {presentation !== "task" && group.task_id ? ( + + 所属任务 #{group.task_id} + {taskLabel ? ` · ${taskLabel}` : ""} + + ) : null} + {(row.assets ?? []).length ? ( + + 资产: + {row.assets?.map((a) => ( + + {a.label} + + ))} + {(row.asset_count ?? 0) > 4 ? +{(row.asset_count ?? 0) - 4} : null} + + ) : ( + 未关联资产 + )} + {row.last_found_at ? ( + + ) : null} +
+ ) : null}
严 / 高 / 中 / 低 @@ -566,7 +568,6 @@ export function FindingCaseFolder({ aria-label={`${group.title}的原始子报告`} className="border-t bg-muted/20 px-4 py-4 sm:px-6" > -

原始子报告 · 各自等级和证据保留