feat: rewrite workflows page as dense list

This commit is contained in:
afei A
2026-03-28 00:28:51 +08:00
parent ae8ab2cf9c
commit 59d3f4d054
8 changed files with 250 additions and 173 deletions

View File

@@ -0,0 +1,108 @@
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import { StatusBadge } from "@/components/ui/status-badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { WorkflowLookupItemVM } from "@/lib/types/view-models";
type WorkflowTableProps = {
isLoading: boolean;
items: WorkflowLookupItemVM[];
onOpenWorkflow?: (orderId: string) => void;
};
function formatTimestamp(timestamp: string) {
return timestamp.replace("T", " ").replace("Z", " UTC");
}
export function WorkflowTable({
isLoading,
items,
onOpenWorkflow,
}: WorkflowTableProps) {
if (isLoading) {
return (
<div className="rounded-[var(--panel-radius)] border border-dashed border-[var(--border-strong)] bg-[var(--surface-muted)] px-5 py-6 text-sm text-[var(--ink-muted)]">
</div>
);
}
if (!items.length) {
return (
<EmptyState
eyebrow="Workflows empty"
title="当前筛选下没有流程"
description="调整关键词或状态后再试。"
/>
);
}
return (
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>workflowId</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={item.workflowId}>
<TableCell className="font-medium">#{item.orderId}</TableCell>
<TableCell>{item.workflowId}</TableCell>
<TableCell>{item.workflowType}</TableCell>
<TableCell>
<StatusBadge status={item.status} />
</TableCell>
<TableCell>
<div className="flex flex-col gap-1">
<StatusBadge variant="workflowStep" status={item.currentStep} />
<span className="text-xs text-[var(--ink-muted)]">
{item.currentStepLabel}
</span>
</div>
</TableCell>
<TableCell>{item.failureCount}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-2">
<span className="text-sm text-[var(--ink-strong)]">
{item.revisionCount}
</span>
{item.pendingManualConfirm ? (
<span className="rounded-full bg-[rgba(88,106,56,0.12)] px-2 py-0.5 font-[var(--font-mono)] text-[11px] uppercase tracking-[0.14em] text-[#50633b]">
</span>
) : null}
</div>
</TableCell>
<TableCell className="text-xs text-[var(--ink-muted)]">
{formatTimestamp(item.updatedAt)}
</TableCell>
<TableCell className="text-right">
<Button
variant="secondary"
onClick={() => onOpenWorkflow?.(String(item.orderId))}
>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,81 @@
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 { ORDER_STATUS_META } from "@/lib/types/status";
import type { OrderStatus } from "@/lib/types/backend";
export type WorkflowFilterStatus = OrderStatus | "all";
type WorkflowToolbarProps = {
currentPage: number;
query: string;
status: WorkflowFilterStatus;
totalPages: number;
onPageChange?: (page: number) => void;
onQueryChange: (value: string) => void;
onQuerySubmit?: (query: string) => void;
onStatusChange?: (value: WorkflowFilterStatus) => void;
};
export function WorkflowToolbar({
currentPage,
query,
status,
totalPages,
onPageChange,
onQueryChange,
onQuerySubmit,
onStatusChange,
}: WorkflowToolbarProps) {
const safeTotalPages = Math.max(totalPages, 1);
return (
<PageToolbar className="justify-between gap-4">
<div className="flex flex-1 flex-wrap items-center gap-3">
<Input
aria-label="流程关键词搜索"
className="min-w-[220px] flex-1 md:max-w-[320px]"
placeholder="搜索订单号或 workflow_id"
value={query}
onChange={(event) => onQueryChange(event.target.value)}
/>
<Button onClick={() => onQuerySubmit?.(query.trim())}></Button>
<Select
aria-label="流程状态筛选"
value={status}
onChange={(event) =>
onStatusChange?.(event.target.value as WorkflowFilterStatus)
}
>
<option value="all"></option>
{Object.entries(ORDER_STATUS_META).map(([value, meta]) => (
<option key={value} value={value}>
{meta.label}
</option>
))}
</Select>
</div>
<div className="flex items-center gap-3">
<span className="text-xs text-[var(--ink-muted)]">
{Math.min(currentPage, safeTotalPages)} / {safeTotalPages}
</span>
<Button
variant="secondary"
disabled={currentPage <= 1}
onClick={() => onPageChange?.(currentPage - 1)}
>
</Button>
<Button
variant="secondary"
disabled={currentPage >= safeTotalPages}
onClick={() => onPageChange?.(currentPage + 1)}
>
</Button>
</div>
</PageToolbar>
);
}

View File

@@ -3,16 +3,15 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"; import { MetricChip } from "@/components/ui/metric-chip";
import { Card, CardContent, CardDescription, CardEyebrow, CardHeader, CardTitle } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
import { PageHeader } from "@/components/ui/page-header"; import { PageHeader } from "@/components/ui/page-header";
import { StatusBadge } from "@/components/ui/status-badge";
import { ORDER_STATUS_META } from "@/lib/types/status";
import type { OrderStatus } from "@/lib/types/backend";
import type { WorkflowLookupItemVM } from "@/lib/types/view-models"; import type { WorkflowLookupItemVM } from "@/lib/types/view-models";
import {
WorkflowToolbar,
type WorkflowFilterStatus,
} from "@/features/workflows/components/workflow-toolbar";
import { WorkflowTable } from "@/features/workflows/components/workflow-table";
type FilterStatus = OrderStatus | "all";
type PaginationData = { type PaginationData = {
page: number; page: number;
limit: number; limit: number;
@@ -28,9 +27,9 @@ type WorkflowLookupProps = {
onOpenWorkflow?: (orderId: string) => void; onOpenWorkflow?: (orderId: string) => void;
onPageChange?: (page: number) => void; onPageChange?: (page: number) => void;
onQuerySubmit?: (query: string) => void; onQuerySubmit?: (query: string) => void;
onStatusChange?: (status: FilterStatus) => void; onStatusChange?: (status: WorkflowFilterStatus) => void;
selectedQuery?: string; selectedQuery?: string;
selectedStatus?: FilterStatus; selectedStatus?: WorkflowFilterStatus;
totalPages?: number; totalPages?: number;
}; };
@@ -52,17 +51,6 @@ const DEFAULT_PAGINATION: PaginationData = {
total: 0, total: 0,
totalPages: 0, totalPages: 0,
}; };
const WORKFLOW_STATUS_FILTER_OPTIONS: Array<{
label: string;
value: FilterStatus;
}> = [
{ value: "all", label: "全部状态" },
...Object.entries(ORDER_STATUS_META).map(([value, meta]) => ({
value: value as OrderStatus,
label: meta.label,
})),
];
export function WorkflowLookup({ export function WorkflowLookup({
currentPage = 1, currentPage = 1,
isLoading = false, isLoading = false,
@@ -76,171 +64,45 @@ export function WorkflowLookup({
selectedStatus = "all", selectedStatus = "all",
totalPages = 0, totalPages = 0,
}: WorkflowLookupProps) { }: WorkflowLookupProps) {
const [lookupValue, setLookupValue] = useState("");
const [queryValue, setQueryValue] = useState(selectedQuery); const [queryValue, setQueryValue] = useState(selectedQuery);
const normalizedLookup = lookupValue.trim();
const canLookup = /^\d+$/.test(normalizedLookup);
const effectiveTotalPages = Math.max(totalPages, 1);
useEffect(() => { useEffect(() => {
setQueryValue(selectedQuery); setQueryValue(selectedQuery);
}, [selectedQuery]); }, [selectedQuery]);
return ( return (
<section className="space-y-8"> <section className="space-y-6">
<PageHeader <PageHeader
eyebrow="Workflow lookup" eyebrow="Workflow lookup"
title="流程追踪" title="流程追踪"
description="当前首页已经接入真实最近流程列表,并保留订单号直达能力,方便快速排查。" description="流程页承担排查和定位职责,首屏优先显示状态、失败信息和进入详情的操作。"
meta="真实列表入口" meta="真实列表"
/> />
<div className="rounded-[28px] border border-[rgba(57,86,95,0.16)] bg-[rgba(57,86,95,0.1)] px-6 py-5 text-sm leading-7 text-[#2e4d56]"> <div className="flex flex-wrap gap-2">
{message} <MetricChip label="mode" value="真实流程列表" />
<MetricChip label="rows" value={items.length} />
<MetricChip label="message" value="流程追踪已接入真实后端" />
</div> </div>
<div className="grid gap-6 xl:grid-cols-[320px_minmax(0,1fr)]"> <p className="text-sm text-[var(--ink-muted)]">{message}</p>
<Card>
<CardHeader>
<div className="space-y-2">
<CardEyebrow>Direct lookup</CardEyebrow>
<div className="space-y-1">
<CardTitle></CardTitle>
<CardDescription>
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<input
value={lookupValue}
onChange={(event) => setLookupValue(event.target.value)}
placeholder="输入订单号,例如 4201"
className="min-h-12 w-full rounded-[24px] border border-[var(--border-soft)] bg-[var(--surface-muted)] px-4 text-sm text-[var(--ink-strong)] outline-none transition placeholder:text-[var(--ink-faint)] focus:border-[var(--accent-primary)] focus:bg-[var(--surface)]"
/>
<Button
className="w-full"
disabled={!canLookup}
onClick={() => onOpenWorkflow?.(normalizedLookup)}
>
</Button>
</CardContent>
</Card>
<Card> <WorkflowToolbar
<CardHeader> currentPage={currentPage}
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between"> query={queryValue}
<div className="space-y-2"> status={selectedStatus}
<CardEyebrow>Placeholder index</CardEyebrow> totalPages={totalPages}
<div className="space-y-1"> onPageChange={onPageChange}
<CardTitle></CardTitle> onQueryChange={setQueryValue}
<CardDescription> onQuerySubmit={onQuerySubmit}
沿 onStatusChange={onStatusChange}
</CardDescription> />
</div>
</div>
<div className="flex flex-col gap-3 lg:min-w-[360px]">
<label className="flex flex-col gap-2 text-sm text-[var(--ink-strong)]">
<span className="font-medium"></span>
<div className="flex gap-3">
<input
aria-label="流程关键词搜索"
value={queryValue}
onChange={(event) => setQueryValue(event.target.value)}
placeholder="搜索订单号或 workflow_id"
className="min-h-12 flex-1 rounded-[18px] border border-[var(--border-strong)] bg-[var(--surface-muted)] px-4 text-sm text-[var(--ink-strong)] outline-none transition placeholder:text-[var(--ink-faint)] focus:border-[var(--accent-primary)] focus:bg-[var(--surface)] focus:ring-2 focus:ring-[var(--accent-ring)]"
/>
<Button onClick={() => onQuerySubmit?.(queryValue.trim())}>
</Button>
</div>
</label>
<label className="flex flex-col gap-2 text-sm text-[var(--ink-strong)]">
<span className="font-medium"></span>
<select
aria-label="流程状态筛选"
className="min-h-12 rounded-[18px] border border-[var(--border-strong)] bg-[var(--surface-muted)] px-4 text-sm text-[var(--ink-strong)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)]"
value={selectedStatus}
onChange={(event) =>
onStatusChange?.(event.target.value as FilterStatus)
}
>
{WORKFLOW_STATUS_FILTER_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
{isLoading ? (
<div className="rounded-[24px] border border-dashed border-[var(--border-strong)] bg-[var(--surface-muted)] px-5 py-6 text-sm text-[var(--ink-muted)]">
</div>
) : null}
{!isLoading && items.length ? ( <WorkflowTable
items.map((item) => ( isLoading={isLoading}
<div items={items}
key={item.workflowId} onOpenWorkflow={onOpenWorkflow}
className="rounded-[24px] border border-[var(--border-soft)] bg-[var(--surface-muted)] px-4 py-4" />
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<p className="text-sm font-semibold text-[var(--ink-strong)]">
#{item.orderId}
</p>
<p className="text-xs text-[var(--ink-muted)]">
{item.workflowId} / {item.workflowType}
</p>
</div>
<StatusBadge status={item.status} />
</div>
<div className="mt-4 flex flex-wrap items-center gap-3 text-xs text-[var(--ink-muted)]">
<StatusBadge variant="workflowStep" status={item.currentStep} />
<span>{item.currentStepLabel}</span>
</div>
</div>
))
) : null}
{!isLoading && !items.length ? (
<EmptyState
eyebrow="Lookup empty"
title="暂无流程索引"
description="当前筛选条件下还没有可展示的流程记录。"
/>
) : null}
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-[var(--border-soft)] pt-3">
<p className="text-xs text-[var(--ink-muted)]">
{Math.min(currentPage, effectiveTotalPages)} / {effectiveTotalPages}
</p>
<div className="flex gap-3">
<Button
variant="secondary"
disabled={currentPage <= 1}
onClick={() => onPageChange?.(currentPage - 1)}
>
</Button>
<Button
variant="secondary"
disabled={currentPage >= effectiveTotalPages}
onClick={() => onPageChange?.(currentPage + 1)}
>
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
</section> </section>
); );
} }
@@ -251,7 +113,8 @@ export function WorkflowLookupScreen() {
const [message, setMessage] = useState(DEFAULT_MESSAGE); const [message, setMessage] = useState(DEFAULT_MESSAGE);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [selectedQuery, setSelectedQuery] = useState(""); const [selectedQuery, setSelectedQuery] = useState("");
const [selectedStatus, setSelectedStatus] = useState<FilterStatus>("all"); const [selectedStatus, setSelectedStatus] =
useState<WorkflowFilterStatus>("all");
const [pagination, setPagination] = useState<PaginationData>(DEFAULT_PAGINATION); const [pagination, setPagination] = useState<PaginationData>(DEFAULT_PAGINATION);
useEffect(() => { useEffect(() => {

View File

@@ -102,6 +102,10 @@ export function adaptWorkflowLookupItem(
| "workflow_type" | "workflow_type"
| "workflow_status" | "workflow_status"
| "current_step" | "current_step"
| "failure_count"
| "review_task_status"
| "revision_count"
| "pending_manual_confirm"
| "updated_at" | "updated_at"
>, >,
): WorkflowLookupItemVM { ): WorkflowLookupItemVM {
@@ -113,6 +117,10 @@ export function adaptWorkflowLookupItem(
statusMeta: getOrderStatusMeta(workflow.workflow_status), statusMeta: getOrderStatusMeta(workflow.workflow_status),
currentStep: workflow.current_step, currentStep: workflow.current_step,
currentStepLabel: getWorkflowStepMeta(workflow.current_step).label, currentStepLabel: getWorkflowStepMeta(workflow.current_step).label,
failureCount: workflow.failure_count,
reviewTaskStatus: workflow.review_task_status,
revisionCount: workflow.revision_count,
pendingManualConfirm: workflow.pending_manual_confirm,
updatedAt: workflow.updated_at, updatedAt: workflow.updated_at,
}; };
} }

View File

@@ -174,6 +174,10 @@ export type WorkflowLookupItemVM = {
statusMeta: StatusMeta; statusMeta: StatusMeta;
currentStep: WorkflowStepName | null; currentStep: WorkflowStepName | null;
currentStepLabel: string; currentStepLabel: string;
failureCount: number;
reviewTaskStatus: ReviewTaskStatus | null;
revisionCount: number;
pendingManualConfirm: boolean;
updatedAt: string; updatedAt: string;
}; };

View File

@@ -70,6 +70,10 @@ test("proxies workflow lookup items from the backend workflows list api", async
}, },
currentStep: "review", currentStep: "review",
currentStepLabel: "人工审核", currentStepLabel: "人工审核",
failureCount: 0,
reviewTaskStatus: "revision_uploaded",
revisionCount: 1,
pendingManualConfirm: true,
updatedAt: "2026-03-27T14:00:03Z", updatedAt: "2026-03-27T14:00:03Z",
}, },
], ],

View File

@@ -16,15 +16,21 @@ const WORKFLOW_ITEMS: WorkflowLookupItemVM[] = [
}, },
currentStep: "review", currentStep: "review",
currentStepLabel: "人工审核", currentStepLabel: "人工审核",
failureCount: 2,
reviewTaskStatus: "revision_uploaded",
revisionCount: 1,
pendingManualConfirm: true,
updatedAt: "2026-03-27T09:15:00Z", updatedAt: "2026-03-27T09:15:00Z",
}, },
]; ];
test("shows the real recent-workflows entry state", () => { test("renders workflows as a high-density table with shared toolbar controls", () => {
render(<WorkflowLookup items={WORKFLOW_ITEMS} />); render(<WorkflowLookup items={WORKFLOW_ITEMS} />);
expect(screen.getByText("流程追踪首页当前显示真实后端最近流程。")).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "流程类型" })).toBeInTheDocument();
expect(screen.getByText("订单 #4201")).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "失败次数" })).toBeInTheDocument();
expect(screen.getByLabelText("流程状态筛选")).toBeInTheDocument();
expect(screen.getByText("#4201")).toBeInTheDocument();
}); });
test("supports workflow status filtering and pagination actions", () => { test("supports workflow status filtering and pagination actions", () => {

View File

@@ -79,6 +79,9 @@ test("maps workflow lookup status and current step labels", () => {
status: "running", status: "running",
currentStep: "fusion", currentStep: "fusion",
currentStepLabel: "融合", currentStepLabel: "融合",
failureCount: 0,
revisionCount: 0,
pendingManualConfirm: false,
statusMeta: { statusMeta: {
label: "处理中", label: "处理中",
tone: "info", tone: "info",