From f2deb54f3a1bf2ba9eb8e4a8cf9535fdb77338e7 Mon Sep 17 00:00:00 2001 From: afei A <57030625+NewHubBoy@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:18:50 +0800 Subject: [PATCH] feat: rewrite review queue as dense table --- .../reviews/components/review-filters.tsx | 86 ++++++++ .../reviews/components/review-queue.tsx | 193 +++++++++++------- .../reviews/review-workbench-list.tsx | 73 ++++++- .../reviews/review-workbench-list.test.tsx | 15 +- 4 files changed, 276 insertions(+), 91 deletions(-) create mode 100644 src/features/reviews/components/review-filters.tsx diff --git a/src/features/reviews/components/review-filters.tsx b/src/features/reviews/components/review-filters.tsx new file mode 100644 index 0000000..5ae9c11 --- /dev/null +++ b/src/features/reviews/components/review-filters.tsx @@ -0,0 +1,86 @@ +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { PageToolbar } from "@/components/ui/page-toolbar"; +import { Select } from "@/components/ui/select"; +import type { ReviewTaskStatus } from "@/lib/types/backend"; + +export type ReviewStatusFilter = "all" | "waiting_review"; +export type ReviewRevisionFilter = + | "all" + | "none" + | "revision_uploaded" + | "pending_manual_confirm"; + +type ReviewFiltersProps = { + query: string; + revisionFilter: ReviewRevisionFilter; + statusFilter: ReviewStatusFilter; + onQueryChange: (value: string) => void; + onRevisionFilterChange: (value: ReviewRevisionFilter) => void; + onStatusFilterChange: (value: ReviewStatusFilter) => void; + onRefresh: () => void; +}; + +const REVISION_LABELS: Record = { + pending: "无修订稿", + revision_uploaded: "已上传修订稿", + submitted: "已提交", +}; + +export function getRevisionFilterLabel( + reviewTaskStatus: ReviewTaskStatus, + pendingManualConfirm: boolean, +) { + if (pendingManualConfirm) { + return "修订待确认"; + } + + return REVISION_LABELS[reviewTaskStatus]; +} + +export function ReviewFilters({ + query, + revisionFilter, + statusFilter, + onQueryChange, + onRevisionFilterChange, + onStatusFilterChange, + onRefresh, +}: ReviewFiltersProps) { + return ( + + onQueryChange(event.target.value)} + /> + + + + + ); +} diff --git a/src/features/reviews/components/review-queue.tsx b/src/features/reviews/components/review-queue.tsx index 0b07402..010f703 100644 --- a/src/features/reviews/components/review-queue.tsx +++ b/src/features/reviews/components/review-queue.tsx @@ -1,9 +1,15 @@ import Link from "next/link"; -import { Card, CardContent, CardEyebrow, CardHeader } from "@/components/ui/card"; import { EmptyState } from "@/components/ui/empty-state"; -import { SectionTitle } from "@/components/ui/section-title"; import { StatusBadge } from "@/components/ui/status-badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import type { ReviewQueueVM } from "@/lib/types/view-models"; type ReviewQueueProps = { @@ -25,88 +31,119 @@ export function ReviewQueue({ isLoading, queue, }: ReviewQueueProps) { + if (isLoading) { + return ( +
+ 正在加载待审核队列… +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + if (queue?.state.kind === "business-empty") { + return ( + + ); + } + + if (!queue?.items.length) { + return null; + } + return ( - - - - - - {isLoading ? ( -
- 正在加载待审核队列… -
- ) : null} - - {!isLoading && error ? ( -
- {error} -
- ) : null} - - {!isLoading && !error && queue?.state.kind === "business-empty" ? ( - - ) : null} - - {!isLoading && !error && queue?.items.length ? ( -
- {queue.items.map((item) => ( + + + + 订单号 + workflowId + 当前状态 + 当前步骤 + 修订状态 + 失败次数 + 更新时间 + 操作 + + + + {queue.items.map((item) => ( + + #{item.orderId} + +
+
{item.workflowId}
+ {item.workflowType ? ( +
{item.workflowType}
+ ) : null} +
+
+ + + + +
+ + {item.currentStepLabel} +
+
+ +
+ {item.pendingManualConfirm ? ( + + 修订待确认 + + ) : null} + {item.latestRevisionAssetId ? ( + + v{item.latestRevisionVersion ?? "?"} + + ) : ( + 无修订稿 + )} + {item.hasMockAssets ? ( + + Mock 资产 + + ) : null} +
+
+ + {item.failureCount > 0 ? ( + + 失败 {item.failureCount} + + ) : ( + 0 + )} + + + {formatTimestamp(item.createdAt)} + + -
-
- Order #{item.orderId} -
-

- 审核目标 #{item.orderId} -

-

- 工作流 {item.workflowId} - {item.workflowType ? ` / ${item.workflowType}` : ""} -

-
-
- -
- -
- - {item.currentStepLabel} - {formatTimestamp(item.createdAt)} - {item.hasMockAssets ? ( - - Mock 资产 - - ) : null} - {item.failureCount > 0 ? ( - - 失败 {item.failureCount} - - ) : null} - {item.pendingManualConfirm ? ( - - 修订待确认 - - ) : null} -
+ 进入详情 - ))} - - ) : null} - - +
+
+ ))} +
+
); } diff --git a/src/features/reviews/review-workbench-list.tsx b/src/features/reviews/review-workbench-list.tsx index c812c55..2d8c84e 100644 --- a/src/features/reviews/review-workbench-list.tsx +++ b/src/features/reviews/review-workbench-list.tsx @@ -1,8 +1,13 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { PageHeader } from "@/components/ui/page-header"; +import { + ReviewFilters, + type ReviewRevisionFilter, + type ReviewStatusFilter, +} from "@/features/reviews/components/review-filters"; import { ReviewQueue } from "@/features/reviews/components/review-queue"; import type { ReviewQueueVM } from "@/lib/types/view-models"; @@ -19,6 +24,11 @@ export function ReviewWorkbenchListScreen() { const [queue, setQueue] = useState(null); const [queueError, setQueueError] = useState(null); const [isLoadingQueue, setIsLoadingQueue] = useState(true); + const [query, setQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [revisionFilter, setRevisionFilter] = + useState("all"); + const [reloadKey, setReloadKey] = useState(0); useEffect(() => { let active = true; @@ -59,21 +69,72 @@ export function ReviewWorkbenchListScreen() { return () => { active = false; }; - }, []); + }, [reloadKey]); + + const filteredQueue = useMemo(() => { + if (!queue) { + return null; + } + + const normalizedQuery = query.trim().toLowerCase(); + const nextItems = queue.items.filter((item) => { + const matchesQuery = + !normalizedQuery || + String(item.orderId).includes(normalizedQuery) || + item.workflowId.toLowerCase().includes(normalizedQuery); + + const matchesStatus = + statusFilter === "all" ? true : item.status === statusFilter; + + const matchesRevision = + revisionFilter === "all" + ? true + : revisionFilter === "none" + ? !item.latestRevisionAssetId + : revisionFilter === "revision_uploaded" + ? item.reviewTaskStatus === "revision_uploaded" + : item.pendingManualConfirm; + + return matchesQuery && matchesStatus && matchesRevision; + }); + + return { + ...queue, + items: nextItems, + state: + nextItems.length > 0 + ? queue.state + : { + kind: "business-empty" as const, + title: "当前筛选下没有待处理任务", + description: "调整关键词或修订状态后再试,或者刷新队列同步后端最新状态。", + }, + }; + }, [query, queue, revisionFilter, statusFilter]); return ( -
+
+ + setReloadKey((value) => value + 1)} />
); diff --git a/tests/features/reviews/review-workbench-list.test.tsx b/tests/features/reviews/review-workbench-list.test.tsx index 6e49b6d..d193c7b 100644 --- a/tests/features/reviews/review-workbench-list.test.tsx +++ b/tests/features/reviews/review-workbench-list.test.tsx @@ -36,7 +36,7 @@ afterEach(() => { vi.unstubAllGlobals(); }); -test("renders medium-density list rows that link into independent review detail pages", async () => { +test("renders a compact review queue table with triage columns", async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify(createPendingPayload()), { status: 200, @@ -50,13 +50,14 @@ test("renders medium-density list rows that link into independent review detail render(); - expect(await screen.findByText("审核目标 #101")).toBeInTheDocument(); - expect(screen.getByText(/工作流 wf-101/)).toBeInTheDocument(); - expect(screen.getByText("Mock 资产")).toBeInTheDocument(); - expect(screen.getByText("失败 2")).toBeInTheDocument(); - expect(screen.queryByText("审核动作面板")).not.toBeInTheDocument(); - expect(screen.getByRole("link", { name: /审核目标 #101/ })).toHaveAttribute( + expect(await screen.findByRole("columnheader", { name: "订单号" })).toBeInTheDocument(); + expect(screen.getByRole("columnheader", { name: "修订状态" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "刷新队列" })).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "进入详情" })).toHaveAttribute( "href", "/reviews/workbench/101", ); + expect(screen.getByText("Mock 资产")).toBeInTheDocument(); + expect(screen.getByText("失败 2")).toBeInTheDocument(); + expect(screen.queryByText("审核动作面板")).not.toBeInTheDocument(); });