From 6656d73b56a2d8ce3728a90440c4625aa9a26dcd Mon Sep 17 00:00:00 2001 From: ji ruo <79234113+RuoJi6@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:48:08 +0800 Subject: [PATCH] feat: add finding date range filter --- db/findings.go | 14 ++ server/findings_groups.go | 2 + web/src/app/(main)/function/findings/page.tsx | 42 +++++- web/src/components/finding-date-filter.tsx | 137 ++++++++++++++++++ web/src/lib/api.ts | 2 + web/src/lib/types.ts | 2 + 6 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 web/src/components/finding-date-filter.tsx diff --git a/db/findings.go b/db/findings.go index 7ec541e..35ccb93 100644 --- a/db/findings.go +++ b/db/findings.go @@ -160,6 +160,8 @@ type FindingFilter struct { Query string // 名称/类型/摘要/证据/报告正文的模糊检索关键词 Sort string // "severity" | "time" TimeDirection string // "asc" | "desc"; applies to time, not the severity ranking + From string // RFC3339 timestamp, inclusive + To string // RFC3339 timestamp, inclusive // AssetScope 是资产树的节点 key(a: / c: / r: / __none__), // 选中一个节点等于选中它的整棵子树。空 = 不按资产筛选。 AssetScope string @@ -199,6 +201,18 @@ func (f FindingFilter) where() (string, []any) { add("severity", f.Severity) add("status", f.Status) add("vulnclass", f.VulnClass) + if from := strings.TrimSpace(f.From); from != "" { + if parsed, err := time.Parse(time.RFC3339, from); err == nil { + args = append(args, parsed) + conds = append(conds, fmt.Sprintf("f.created_at >= $%d", len(args))) + } + } + if to := strings.TrimSpace(f.To); to != "" { + if parsed, err := time.Parse(time.RFC3339, to); err == nil { + args = append(args, parsed) + conds = append(conds, fmt.Sprintf("f.created_at <= $%d", len(args))) + } + } // task_id 是 bigint 列,按整数比较(不能走上面的文本 add);空/非法值忽略。 if f.TaskID == FindingUnassignedTask { conds = append(conds, "(f.task_id IS NULL OR t.id IS NULL)") diff --git a/server/findings_groups.go b/server/findings_groups.go index b6f4118..1ceced4 100644 --- a/server/findings_groups.go +++ b/server/findings_groups.go @@ -37,6 +37,8 @@ func findingFilterFromQuery(q url.Values) db.FindingFilter { Query: q.Get("q"), Sort: q.Get("sort"), TimeDirection: q.Get("time_direction"), + From: strings.TrimSpace(q.Get("from")), + To: strings.TrimSpace(q.Get("to")), AssetScope: strings.TrimSpace(q.Get("asset_scope")), } } diff --git a/web/src/app/(main)/function/findings/page.tsx b/web/src/app/(main)/function/findings/page.tsx index 4ce291c..f07bab8 100644 --- a/web/src/app/(main)/function/findings/page.tsx +++ b/web/src/app/(main)/function/findings/page.tsx @@ -18,6 +18,7 @@ import { import { toast } from "sonner"; import { FindingCaseList } from "@/components/finding-case-list"; +import { FindingDateFilter, type FindingDateRange } from "@/components/finding-date-filter"; import { FindingRetestDialog } from "@/components/finding-retest-dialog"; import { StatusBadge } from "@/components/status-badge"; import { TablePagination } from "@/components/table-pagination"; @@ -142,6 +143,7 @@ export default function FindingsPage() { const [sort, setSort] = React.useState<"severity" | "time">("severity"); const [search, setSearch] = React.useState(""); const [query, setQuery] = React.useState(""); + const [dateRange, setDateRange] = React.useState({}); const [expanded, setExpanded] = React.useState(null); const [flat, setFlat] = React.useState(EMPTY_FLAT_STATE); const [flatPage, setFlatPage] = React.useState(1); @@ -167,7 +169,16 @@ export default function FindingsPage() { const retestRefreshVersion = React.useRef(0); const [deepenDescription, setDeepenDescription] = React.useState(""); const [deepening, setDeepening] = React.useState(false); - const filterFingerprint = JSON.stringify([severity, status, vulnclass, task, sort, query]); + const filterFingerprint = JSON.stringify([ + severity, + status, + vulnclass, + task, + sort, + query, + dateRange.from, + dateRange.to, + ]); const activeFilterFingerprint = React.useRef(filterFingerprint); activeFilterFingerprint.current = filterFingerprint; @@ -310,6 +321,7 @@ export default function FindingsPage() { task, query, sort, + ...dateRange, assetScope: view === "asset" ? (assetScope ?? undefined) : undefined, }, ids: [...selectedIds], @@ -374,7 +386,19 @@ export default function FindingsPage() { // Polling keeps the last successful snapshot visible. setFlat((current) => ({ ...current, loading: false })); } - }, [activeAssetScope, filterFingerprint, flatPage, flatPageSize, severity, status, vulnclass, task, query, sort]); + }, [ + activeAssetScope, + dateRange, + filterFingerprint, + flatPage, + flatPageSize, + severity, + status, + vulnclass, + task, + query, + sort, + ]); // loadAssetTree 取整棵资产树。树不随选中节点变化(否则选一下就塌成一条链), // 所以这里不带 assetScope。 @@ -384,7 +408,7 @@ export default function FindingsPage() { const request = ++assetTreeRequest.current; setAssetTree((current) => ({ ...current, loading: true })); try { - const result = await api.findingAssetTree({ severity, status, vulnclass, task, query, sort }); + const result = await api.findingAssetTree({ severity, status, vulnclass, task, query, sort, ...dateRange }); if (request !== assetTreeRequest.current || activeFilterFingerprint.current !== requestFilter) return; setAssetTree({ nodes: result.nodes ?? [], @@ -399,7 +423,7 @@ export default function FindingsPage() { setAssetTree((current) => ({ ...current, loading: false })); toast.error(`资产树加载失败:${(e as Error).message}`); } - }, [filterFingerprint, severity, status, vulnclass, task, query, sort]); + }, [dateRange, filterFingerprint, severity, status, vulnclass, task, query, sort]); const refreshGroups = React.useCallback(async () => { const requestFilter = filterFingerprint; @@ -415,6 +439,7 @@ export default function FindingsPage() { task, query, sort, + ...dateRange, }); if (request !== groupsRequest.current || activeFilterFingerprint.current !== requestFilter) return; setGroups(result.items); @@ -423,7 +448,7 @@ export default function FindingsPage() { } catch { // Polling keeps the last successful snapshot visible. } - }, [filterFingerprint, page, pageSize, severity, status, vulnclass, task, query, sort]); + }, [dateRange, filterFingerprint, page, pageSize, severity, status, vulnclass, task, query, sort]); const loadGroup = React.useCallback( async (key: string, groupPage: number, groupPageSize: number) => { @@ -454,6 +479,7 @@ export default function FindingsPage() { task: key, query, sort, + ...dateRange, }, true, ); @@ -488,7 +514,7 @@ export default function FindingsPage() { })); } }, - [filterFingerprint, severity, status, vulnclass, query, sort], + [dateRange, filterFingerprint, severity, status, vulnclass, query, sort], ); React.useEffect(() => { @@ -980,6 +1006,8 @@ export default function FindingsPage() { + +