feat: rewrite workflows page as dense list
This commit is contained in:
108
src/features/workflows/components/workflow-table.tsx
Normal file
108
src/features/workflows/components/workflow-table.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
81
src/features/workflows/components/workflow-toolbar.tsx
Normal file
81
src/features/workflows/components/workflow-toolbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -3,16 +3,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardEyebrow, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { MetricChip } from "@/components/ui/metric-chip";
|
||||
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 {
|
||||
WorkflowToolbar,
|
||||
type WorkflowFilterStatus,
|
||||
} from "@/features/workflows/components/workflow-toolbar";
|
||||
import { WorkflowTable } from "@/features/workflows/components/workflow-table";
|
||||
|
||||
type FilterStatus = OrderStatus | "all";
|
||||
type PaginationData = {
|
||||
page: number;
|
||||
limit: number;
|
||||
@@ -28,9 +27,9 @@ type WorkflowLookupProps = {
|
||||
onOpenWorkflow?: (orderId: string) => void;
|
||||
onPageChange?: (page: number) => void;
|
||||
onQuerySubmit?: (query: string) => void;
|
||||
onStatusChange?: (status: FilterStatus) => void;
|
||||
onStatusChange?: (status: WorkflowFilterStatus) => void;
|
||||
selectedQuery?: string;
|
||||
selectedStatus?: FilterStatus;
|
||||
selectedStatus?: WorkflowFilterStatus;
|
||||
totalPages?: number;
|
||||
};
|
||||
|
||||
@@ -52,17 +51,6 @@ const DEFAULT_PAGINATION: PaginationData = {
|
||||
total: 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({
|
||||
currentPage = 1,
|
||||
isLoading = false,
|
||||
@@ -76,171 +64,45 @@ export function WorkflowLookup({
|
||||
selectedStatus = "all",
|
||||
totalPages = 0,
|
||||
}: WorkflowLookupProps) {
|
||||
const [lookupValue, setLookupValue] = useState("");
|
||||
const [queryValue, setQueryValue] = useState(selectedQuery);
|
||||
const normalizedLookup = lookupValue.trim();
|
||||
const canLookup = /^\d+$/.test(normalizedLookup);
|
||||
const effectiveTotalPages = Math.max(totalPages, 1);
|
||||
|
||||
useEffect(() => {
|
||||
setQueryValue(selectedQuery);
|
||||
}, [selectedQuery]);
|
||||
|
||||
return (
|
||||
<section className="space-y-8">
|
||||
<section className="space-y-6">
|
||||
<PageHeader
|
||||
eyebrow="Workflow lookup"
|
||||
title="流程追踪"
|
||||
description="当前首页已经接入真实最近流程列表,并保留订单号直达能力,方便快速排查。"
|
||||
meta="真实列表入口"
|
||||
description="流程页承担排查和定位职责,首屏优先显示状态、失败信息和进入详情的操作。"
|
||||
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]">
|
||||
{message}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<MetricChip label="mode" value="真实流程列表" />
|
||||
<MetricChip label="rows" value={items.length} />
|
||||
<MetricChip label="message" value="流程追踪已接入真实后端" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[320px_minmax(0,1fr)]">
|
||||
<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)]"
|
||||
<p className="text-sm text-[var(--ink-muted)]">{message}</p>
|
||||
|
||||
<WorkflowToolbar
|
||||
currentPage={currentPage}
|
||||
query={queryValue}
|
||||
status={selectedStatus}
|
||||
totalPages={totalPages}
|
||||
onPageChange={onPageChange}
|
||||
onQueryChange={setQueryValue}
|
||||
onQuerySubmit={onQuerySubmit}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
<Button
|
||||
className="w-full"
|
||||
disabled={!canLookup}
|
||||
onClick={() => onOpenWorkflow?.(normalizedLookup)}
|
||||
>
|
||||
打开流程详情
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="space-y-2">
|
||||
<CardEyebrow>Placeholder index</CardEyebrow>
|
||||
<div className="space-y-1">
|
||||
<CardTitle>流程索引占位</CardTitle>
|
||||
<CardDescription>
|
||||
这里已经接入真实后端最近流程列表,继续沿用首版查询页结构。
|
||||
</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)]"
|
||||
<WorkflowTable
|
||||
isLoading={isLoading}
|
||||
items={items}
|
||||
onOpenWorkflow={onOpenWorkflow}
|
||||
/>
|
||||
<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 ? (
|
||||
items.map((item) => (
|
||||
<div
|
||||
key={item.workflowId}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -251,7 +113,8 @@ export function WorkflowLookupScreen() {
|
||||
const [message, setMessage] = useState(DEFAULT_MESSAGE);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedQuery, setSelectedQuery] = useState("");
|
||||
const [selectedStatus, setSelectedStatus] = useState<FilterStatus>("all");
|
||||
const [selectedStatus, setSelectedStatus] =
|
||||
useState<WorkflowFilterStatus>("all");
|
||||
const [pagination, setPagination] = useState<PaginationData>(DEFAULT_PAGINATION);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -102,6 +102,10 @@ export function adaptWorkflowLookupItem(
|
||||
| "workflow_type"
|
||||
| "workflow_status"
|
||||
| "current_step"
|
||||
| "failure_count"
|
||||
| "review_task_status"
|
||||
| "revision_count"
|
||||
| "pending_manual_confirm"
|
||||
| "updated_at"
|
||||
>,
|
||||
): WorkflowLookupItemVM {
|
||||
@@ -113,6 +117,10 @@ export function adaptWorkflowLookupItem(
|
||||
statusMeta: getOrderStatusMeta(workflow.workflow_status),
|
||||
currentStep: workflow.current_step,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,6 +174,10 @@ export type WorkflowLookupItemVM = {
|
||||
statusMeta: StatusMeta;
|
||||
currentStep: WorkflowStepName | null;
|
||||
currentStepLabel: string;
|
||||
failureCount: number;
|
||||
reviewTaskStatus: ReviewTaskStatus | null;
|
||||
revisionCount: number;
|
||||
pendingManualConfirm: boolean;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -70,6 +70,10 @@ test("proxies workflow lookup items from the backend workflows list api", async
|
||||
},
|
||||
currentStep: "review",
|
||||
currentStepLabel: "人工审核",
|
||||
failureCount: 0,
|
||||
reviewTaskStatus: "revision_uploaded",
|
||||
revisionCount: 1,
|
||||
pendingManualConfirm: true,
|
||||
updatedAt: "2026-03-27T14:00:03Z",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -16,15 +16,21 @@ const WORKFLOW_ITEMS: WorkflowLookupItemVM[] = [
|
||||
},
|
||||
currentStep: "review",
|
||||
currentStepLabel: "人工审核",
|
||||
failureCount: 2,
|
||||
reviewTaskStatus: "revision_uploaded",
|
||||
revisionCount: 1,
|
||||
pendingManualConfirm: true,
|
||||
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} />);
|
||||
|
||||
expect(screen.getByText("流程追踪首页当前显示真实后端最近流程。")).toBeInTheDocument();
|
||||
expect(screen.getByText("订单 #4201")).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "流程类型" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "失败次数" })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("流程状态筛选")).toBeInTheDocument();
|
||||
expect(screen.getByText("#4201")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("supports workflow status filtering and pagination actions", () => {
|
||||
|
||||
@@ -79,6 +79,9 @@ test("maps workflow lookup status and current step labels", () => {
|
||||
status: "running",
|
||||
currentStep: "fusion",
|
||||
currentStepLabel: "融合",
|
||||
failureCount: 0,
|
||||
revisionCount: 0,
|
||||
pendingManualConfirm: false,
|
||||
statusMeta: {
|
||||
label: "处理中",
|
||||
tone: "info",
|
||||
|
||||
Reference in New Issue
Block a user