From 84b40f3004370be0bbf6fe2c55c1625418f44787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?artex=E4=BB=A3=E7=A0=81=E7=BC=96=E5=86=99?= Date: Mon, 26 Oct 2026 10:12:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E4=BB=BB=E5=8A=A1=E5=8F=91?= =?UTF-8?q?=E7=8E=B0/=E5=A4=8D=E6=B5=8B=E6=94=AF=E6=8C=81=E4=B8=A5?= =?UTF-8?q?=E9=87=8D=E5=BA=A6=E7=AD=9B=E9=80=89=E4=B8=8E=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 复测 tab:严重度 chip + sort=severity,左侧列表支持关键词搜索 - 发现/复测:输入框草稿 + 搜索按钮(或 Enter)才应用关键词 - 扩展 filterAndSortFindings 客户端关键词匹配并补充单测 --- .../tasks/detail/_tabs/findings-tab.tsx | 81 +++++++++++++----- .../tasks/detail/_tabs/retests-tab.tsx | 84 ++++++++++++++++--- web/src/lib/finding-severity.test.mjs | 44 ++++++++++ web/src/lib/finding-severity.ts | 11 ++- 4 files changed, 188 insertions(+), 32 deletions(-) diff --git a/web/src/app/(main)/function/tasks/detail/_tabs/findings-tab.tsx b/web/src/app/(main)/function/tasks/detail/_tabs/findings-tab.tsx index 577944f..a434bcc 100644 --- a/web/src/app/(main)/function/tasks/detail/_tabs/findings-tab.tsx +++ b/web/src/app/(main)/function/tasks/detail/_tabs/findings-tab.tsx @@ -4,13 +4,15 @@ import * as React from "react"; import Link from "next/link"; -import { ArrowDownIcon, ArrowUpIcon, ArrowUpRightIcon, ChevronRightIcon } from "lucide-react"; +import { ArrowDownIcon, ArrowUpIcon, ArrowUpRightIcon, ChevronRightIcon, SearchIcon } from "lucide-react"; import { toast } from "sonner"; import { FindingCaseList } from "@/components/finding-case-list"; import { StatusBadge } from "@/components/status-badge"; import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger } from "@/components/ui/select"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { api } from "@/lib/api"; @@ -139,6 +141,8 @@ function Row({ export function FindingsTab({ taskId }: { taskId: string }) { const [findings, setFindings] = React.useState([]); const [severity, setSeverity] = React.useState<"all" | Severity>("all"); + const [search, setSearch] = React.useState(""); + const [query, setQuery] = React.useState(""); const [sortPreference, setSortPreference] = useStoredSortPreference( FINDING_SORT_PREFERENCE_KEY, FINDING_SORT_FIELDS, @@ -179,28 +183,58 @@ export function FindingsTab({ taskId }: { taskId: string }) { } }, []); - // Discover inherited sources before severity filtering; finding-cases owns folder matching. + // Discover inherited sources before filtering; finding-cases owns folder matching. const taskFindings = findings.filter((f) => f.task_id === taskId || f.inherited); - const items = filterAndSortFindings(taskFindings, severity, sortPreference.direction); + const items = filterAndSortFindings(taskFindings, severity, sortPreference.direction, query); return (
- value && setSeverity(value as "all" | Severity)} - variant="outline" - size="sm" - spacing={0} - > - {SEVERITY_FILTER_OPTIONS.map(([val, label]) => ( - - {label} - - ))} - +
+ value && setSeverity(value as "all" | Severity)} + variant="outline" + size="sm" + spacing={0} + > + {SEVERITY_FILTER_OPTIONS.map(([val, label]) => ( + + {label} + + ))} + +
{ + event.preventDefault(); + setQuery(search.trim()); + }} + > + + setSearch(event.target.value)} + placeholder="检索漏洞内容" + aria-label="检索漏洞内容" + /> + + + + +
+
{[ @@ -214,7 +248,12 @@ export function FindingsTab({ taskId }: { taskId: string }) {

继承任务 #{source} · 只读

@@ -250,7 +289,9 @@ export function FindingsTab({ taskId }: { taskId: string }) { ))} {items.length === 0 && (

- {severity === "all" ? "本任务及直接关联任务暂无确认发现。" : "当前风险等级暂无原始上报。"} + {severity !== "all" || query + ? "当前筛选与搜索条件下暂无原始上报。" + : "本任务及直接关联任务暂无确认发现。"}

)} diff --git a/web/src/app/(main)/function/tasks/detail/_tabs/retests-tab.tsx b/web/src/app/(main)/function/tasks/detail/_tabs/retests-tab.tsx index ae36bf6..2f70ca4 100644 --- a/web/src/app/(main)/function/tasks/detail/_tabs/retests-tab.tsx +++ b/web/src/app/(main)/function/tasks/detail/_tabs/retests-tab.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import Link from "next/link"; -import { ArrowUpRightIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import { ArrowUpRightIcon, ChevronLeftIcon, ChevronRightIcon, SearchIcon } from "lucide-react"; import { FindingRetestPanel } from "@/components/finding-retest-panel"; import { StatusBadge } from "@/components/status-badge"; @@ -12,10 +12,13 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "@/components/ui/empty"; +import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { api } from "@/lib/api"; -import type { Finding, FindingsPage } from "@/lib/types"; +import { SEVERITY_FILTER_OPTIONS } from "@/lib/status"; +import type { Finding, FindingsPage, Severity } from "@/lib/types"; const PAGE_SIZE = 20; @@ -25,12 +28,22 @@ function findingLabel(finding: Finding) { export function RetestsTab({ taskId }: { taskId: string }) { const [page, setPage] = React.useState(1); - const [data, setData] = React.useState(null); + const [severity, setSeverity] = React.useState<"all" | Severity>("all"); + const [search, setSearch] = React.useState(""); + const [query, setQuery] = React.useState(""); + const [data, setData] = React.useState< + (FindingsPage & { taskId: string; severity: "all" | Severity; query: string }) | null + >(null); const [selectedId, setSelectedId] = React.useState(""); const [error, setError] = React.useState(""); const [refreshVersion, setRefreshVersion] = React.useState(0); const refresh = React.useCallback(() => setRefreshVersion((version) => version + 1), []); + // biome-ignore lint/correctness/useExhaustiveDependencies: Filter changes restart pagination. + React.useEffect(() => { + setPage(1); + }, [severity, query]); + // Only this task's own findings, including those without graph nodes. // Fetch history only for the selection, and wait for each poll to finish. React.useEffect(() => { @@ -38,14 +51,21 @@ export function RetestsTab({ taskId }: { taskId: string }) { let timer: ReturnType; async function load() { try { - const result = await api.findingsPage({ task: taskId, page, pageSize: PAGE_SIZE, sort: "time" }); + const result = await api.findingsPage({ + task: taskId, + page, + pageSize: PAGE_SIZE, + severity: severity === "all" ? undefined : severity, + query: query || undefined, + sort: "severity", + }); if (disposed) return; const lastPage = Math.max(1, Math.ceil(result.total / PAGE_SIZE)); if (page > lastPage) { setPage(lastPage); return; } - setData(result); + setData({ ...result, taskId, severity, query }); setSelectedId((current) => result.items.some((finding) => finding.id === current) ? current : (result.items[0]?.id ?? ""), ); @@ -62,14 +82,54 @@ export function RetestsTab({ taskId }: { taskId: string }) { disposed = true; clearTimeout(timer); }; - }, [taskId, page, refreshVersion]); + }, [taskId, page, refreshVersion, severity, query]); - const loaded = data?.page === page; + const loaded = data?.page === page && data.taskId === taskId && data.severity === severity && data.query === query; const findings = loaded ? data.items : []; const selected = findings.find((finding) => finding.id === selectedId); + const hasFilters = severity !== "all" || !!query; return (
+
+ value && setSeverity(value as "all" | Severity)} + variant="outline" + size="sm" + spacing={0} + > + {SEVERITY_FILTER_OPTIONS.map(([val, label]) => ( + + {label} + + ))} + +
{ + event.preventDefault(); + setQuery(search.trim()); + }} + > + + setSearch(event.target.value)} + placeholder="检索漏洞内容" + aria-label="检索漏洞内容" + /> + + + + +
+
{error ? ( @@ -83,7 +143,7 @@ export function RetestsTab({ taskId }: { taskId: string }) {
- 选择漏洞{data ? ` · ${data.total}` : ""} + 选择漏洞{loaded ? ` · ${data.total}` : ""} 查看本任务漏洞的复测记录,或发起新的复测。 @@ -109,13 +169,15 @@ export function RetestsTab({ taskId }: { taskId: string }) { {loaded && findings.length === 0 ? ( - 暂无可复测漏洞 - 本任务发现漏洞后,可在这里手动发起复测。 + {hasFilters ? "当前筛选暂无漏洞" : "暂无可复测漏洞"} + + {hasFilters ? "请调整风险等级或搜索关键词。" : "本任务发现漏洞后,可在这里手动发起复测。"} + ) : null} - {data && data.total > PAGE_SIZE ? ( + {loaded && data.total > PAGE_SIZE ? (