feat: rewrite orders page as dense list
This commit is contained in:
118
src/features/orders/components/orders-table.tsx
Normal file
118
src/features/orders/components/orders-table.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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 { OrderSummaryVM } from "@/lib/types/view-models";
|
||||||
|
|
||||||
|
type OrdersTableProps = {
|
||||||
|
isLoading: boolean;
|
||||||
|
items: OrderSummaryVM[];
|
||||||
|
onOpenOrder?: (orderId: string) => void;
|
||||||
|
onOpenWorkflow?: (orderId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatTimestamp(timestamp: string) {
|
||||||
|
return timestamp.replace("T", " ").replace("Z", " UTC");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OrdersTable({
|
||||||
|
isLoading,
|
||||||
|
items,
|
||||||
|
onOpenOrder,
|
||||||
|
onOpenWorkflow,
|
||||||
|
}: OrdersTableProps) {
|
||||||
|
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="Orders 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((order) => (
|
||||||
|
<TableRow key={order.orderId}>
|
||||||
|
<TableCell className="font-medium">#{order.orderId}</TableCell>
|
||||||
|
<TableCell>{order.workflowId ?? "未关联"}</TableCell>
|
||||||
|
<TableCell>{order.customerLevel}</TableCell>
|
||||||
|
<TableCell>{order.serviceMode}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<StatusBadge status={order.status} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<StatusBadge variant="workflowStep" status={order.currentStep} />
|
||||||
|
<span className="text-xs text-[var(--ink-muted)]">
|
||||||
|
{order.currentStepLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<span className="text-sm text-[var(--ink-strong)]">
|
||||||
|
{order.revisionCount}
|
||||||
|
</span>
|
||||||
|
{order.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(order.updatedAt)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => onOpenOrder?.(String(order.orderId))}
|
||||||
|
>
|
||||||
|
查看订单
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => onOpenWorkflow?.(String(order.orderId))}
|
||||||
|
>
|
||||||
|
查看流程
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
src/features/orders/components/orders-toolbar.tsx
Normal file
97
src/features/orders/components/orders-toolbar.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
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, ServiceMode } from "@/lib/types/backend";
|
||||||
|
|
||||||
|
export type OrderFilterStatus = OrderStatus | "all";
|
||||||
|
export type OrderFilterServiceMode = ServiceMode | "all";
|
||||||
|
|
||||||
|
type OrdersToolbarProps = {
|
||||||
|
currentPage: number;
|
||||||
|
query: string;
|
||||||
|
serviceMode: OrderFilterServiceMode;
|
||||||
|
status: OrderFilterStatus;
|
||||||
|
totalPages: number;
|
||||||
|
onPageChange?: (page: number) => void;
|
||||||
|
onQueryChange: (value: string) => void;
|
||||||
|
onQuerySubmit?: (query: string) => void;
|
||||||
|
onServiceModeChange: (value: OrderFilterServiceMode) => void;
|
||||||
|
onStatusChange?: (value: OrderFilterStatus) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function OrdersToolbar({
|
||||||
|
currentPage,
|
||||||
|
query,
|
||||||
|
serviceMode,
|
||||||
|
status,
|
||||||
|
totalPages,
|
||||||
|
onPageChange,
|
||||||
|
onQueryChange,
|
||||||
|
onQuerySubmit,
|
||||||
|
onServiceModeChange,
|
||||||
|
onStatusChange,
|
||||||
|
}: OrdersToolbarProps) {
|
||||||
|
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 OrderFilterStatus)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="all">全部状态</option>
|
||||||
|
{Object.entries(ORDER_STATUS_META).map(([value, meta]) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{meta.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
aria-label="服务模式筛选"
|
||||||
|
value={serviceMode}
|
||||||
|
onChange={(event) =>
|
||||||
|
onServiceModeChange(event.target.value as OrderFilterServiceMode)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="all">全部服务模式</option>
|
||||||
|
<option value="auto_basic">auto_basic</option>
|
||||||
|
<option value="semi_pro">semi_pro</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,16 @@
|
|||||||
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 { OrderSummaryVM } from "@/lib/types/view-models";
|
import type { OrderSummaryVM } from "@/lib/types/view-models";
|
||||||
|
import {
|
||||||
|
OrdersToolbar,
|
||||||
|
type OrderFilterServiceMode,
|
||||||
|
type OrderFilterStatus,
|
||||||
|
} from "@/features/orders/components/orders-toolbar";
|
||||||
|
import { OrdersTable } from "@/features/orders/components/orders-table";
|
||||||
|
|
||||||
type FilterStatus = OrderStatus | "all";
|
|
||||||
type PaginationData = {
|
type PaginationData = {
|
||||||
page: number;
|
page: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
@@ -28,10 +28,10 @@ type OrdersHomeProps = {
|
|||||||
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: OrderFilterStatus) => void;
|
||||||
recentOrders: OrderSummaryVM[];
|
recentOrders: OrderSummaryVM[];
|
||||||
selectedQuery?: string;
|
selectedQuery?: string;
|
||||||
selectedStatus?: FilterStatus;
|
selectedStatus?: OrderFilterStatus;
|
||||||
totalPages?: number;
|
totalPages?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,27 +47,13 @@ type OrdersOverviewEnvelope = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TITLE_MESSAGE = "最近订单已接入真实后端接口";
|
const TITLE_MESSAGE = "最近订单已接入真实后端接口";
|
||||||
const DEFAULT_MESSAGE = "当前首页展示真实最近订单,同时保留订单号直达入口,方便快速跳转详情和流程。";
|
const DEFAULT_MESSAGE = "当前页面直接展示真实订单列表,支持关键词、状态和分页操作。";
|
||||||
const DEFAULT_PAGINATION: PaginationData = {
|
const DEFAULT_PAGINATION: PaginationData = {
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 6,
|
limit: 6,
|
||||||
total: 0,
|
total: 0,
|
||||||
totalPages: 0,
|
totalPages: 0,
|
||||||
};
|
};
|
||||||
const ORDER_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,
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
|
|
||||||
function formatTimestamp(timestamp: string) {
|
|
||||||
return timestamp.replace("T", " ").replace("Z", " UTC");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OrdersHome({
|
export function OrdersHome({
|
||||||
currentPage = 1,
|
currentPage = 1,
|
||||||
@@ -83,190 +69,55 @@ export function OrdersHome({
|
|||||||
selectedStatus = "all",
|
selectedStatus = "all",
|
||||||
totalPages = 0,
|
totalPages = 0,
|
||||||
}: OrdersHomeProps) {
|
}: OrdersHomeProps) {
|
||||||
const [lookupValue, setLookupValue] = useState("");
|
|
||||||
const [queryValue, setQueryValue] = useState(selectedQuery);
|
const [queryValue, setQueryValue] = useState(selectedQuery);
|
||||||
const normalizedLookup = lookupValue.trim();
|
const [serviceModeFilter, setServiceModeFilter] =
|
||||||
const canLookup = /^\d+$/.test(normalizedLookup);
|
useState<OrderFilterServiceMode>("all");
|
||||||
const effectiveTotalPages = Math.max(totalPages, 1);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQueryValue(selectedQuery);
|
setQueryValue(selectedQuery);
|
||||||
}, [selectedQuery]);
|
}, [selectedQuery]);
|
||||||
|
|
||||||
|
const visibleOrders =
|
||||||
|
serviceModeFilter === "all"
|
||||||
|
? recentOrders
|
||||||
|
: recentOrders.filter((order) => order.serviceMode === serviceModeFilter);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-8">
|
<section className="space-y-6">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
eyebrow="Orders home"
|
eyebrow="Orders home"
|
||||||
title="订单总览"
|
title="订单总览"
|
||||||
description="这里是后台首页入口,当前已经接入真实最近订单列表,并保留订单号直达入口方便快速处理。"
|
description="订单页直接承担扫描、筛选和跳转职责,不再把首屏浪费在入口说明和大卡片上。"
|
||||||
meta="真实列表入口"
|
meta="真实列表页"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="rounded-[28px] border border-[rgba(145,104,46,0.2)] bg-[rgba(202,164,97,0.12)] px-6 py-5">
|
<div className="flex flex-wrap gap-2">
|
||||||
<p className="text-lg font-semibold tracking-[-0.02em] text-[#7a5323]">
|
<MetricChip label="mode" value="真实订单列表" />
|
||||||
{TITLE_MESSAGE}
|
<MetricChip label="rows" value={visibleOrders.length} />
|
||||||
</p>
|
<MetricChip label="message" value={TITLE_MESSAGE} />
|
||||||
<p className="mt-2 text-sm leading-7 text-[#7a5323]">
|
|
||||||
{message}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_380px]">
|
<p className="text-sm text-[var(--ink-muted)]">{message}</p>
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
<OrdersToolbar
|
||||||
<div className="space-y-2">
|
currentPage={currentPage}
|
||||||
<CardEyebrow>Direct lookup</CardEyebrow>
|
query={queryValue}
|
||||||
<div className="space-y-1">
|
serviceMode={serviceModeFilter}
|
||||||
<CardTitle>订单号直达</CardTitle>
|
status={selectedStatus}
|
||||||
<CardDescription>
|
totalPages={totalPages}
|
||||||
保留订单号和流程号的直接入口,适合在列表之外快速跳转到指定订单。
|
onPageChange={onPageChange}
|
||||||
</CardDescription>
|
onQueryChange={setQueryValue}
|
||||||
</div>
|
onQuerySubmit={onQuerySubmit}
|
||||||
</div>
|
onServiceModeChange={setServiceModeFilter}
|
||||||
</CardHeader>
|
onStatusChange={onStatusChange}
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<label className="block space-y-2">
|
|
||||||
<span className="text-sm font-medium text-[var(--ink-strong)]">
|
|
||||||
订单号
|
|
||||||
</span>
|
|
||||||
<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)]"
|
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
<Button
|
|
||||||
disabled={!canLookup}
|
|
||||||
onClick={() => onOpenOrder?.(normalizedLookup)}
|
|
||||||
>
|
|
||||||
打开订单详情
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
disabled={!canLookup}
|
|
||||||
onClick={() => onOpenWorkflow?.(normalizedLookup)}
|
|
||||||
>
|
|
||||||
打开流程详情
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
<OrdersTable
|
||||||
<CardHeader>
|
isLoading={isLoadingRecent}
|
||||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
items={visibleOrders}
|
||||||
<div className="space-y-2">
|
onOpenOrder={onOpenOrder}
|
||||||
<CardEyebrow>Recent visits</CardEyebrow>
|
onOpenWorkflow={onOpenWorkflow}
|
||||||
<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)]"
|
|
||||||
/>
|
/>
|
||||||
<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)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{ORDER_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">
|
|
||||||
{isLoadingRecent ? (
|
|
||||||
<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}
|
|
||||||
|
|
||||||
{!isLoadingRecent && recentOrders.length ? (
|
|
||||||
recentOrders.map((order) => (
|
|
||||||
<div
|
|
||||||
key={order.orderId}
|
|
||||||
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)]">
|
|
||||||
订单 #{order.orderId}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-[var(--ink-muted)]">
|
|
||||||
工作流 {order.workflowId ?? "未关联"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<StatusBadge status={order.status} />
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex flex-wrap items-center gap-3 text-xs text-[var(--ink-muted)]">
|
|
||||||
<StatusBadge variant="workflowStep" status={order.currentStep} />
|
|
||||||
<span>{order.currentStepLabel}</span>
|
|
||||||
<span>{formatTimestamp(order.updatedAt)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!isLoadingRecent && !recentOrders.length ? (
|
|
||||||
<EmptyState
|
|
||||||
eyebrow="No recent orders"
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -277,7 +128,8 @@ export function OrdersHomeScreen() {
|
|||||||
const [message, setMessage] = useState(DEFAULT_MESSAGE);
|
const [message, setMessage] = useState(DEFAULT_MESSAGE);
|
||||||
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
|
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
|
||||||
const [selectedQuery, setSelectedQuery] = useState("");
|
const [selectedQuery, setSelectedQuery] = useState("");
|
||||||
const [selectedStatus, setSelectedStatus] = useState<FilterStatus>("all");
|
const [selectedStatus, setSelectedStatus] =
|
||||||
|
useState<OrderFilterStatus>("all");
|
||||||
const [pagination, setPagination] = useState<PaginationData>(DEFAULT_PAGINATION);
|
const [pagination, setPagination] = useState<PaginationData>(DEFAULT_PAGINATION);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -62,16 +62,30 @@ export function adaptAsset(asset: AssetDto): AssetViewModel {
|
|||||||
export function adaptOrderSummary(
|
export function adaptOrderSummary(
|
||||||
order: Pick<
|
order: Pick<
|
||||||
OrderDetailResponseDto | OrderListItemDto,
|
OrderDetailResponseDto | OrderListItemDto,
|
||||||
"order_id" | "workflow_id" | "status" | "current_step" | "updated_at"
|
| "order_id"
|
||||||
|
| "workflow_id"
|
||||||
|
| "customer_level"
|
||||||
|
| "service_mode"
|
||||||
|
| "status"
|
||||||
|
| "current_step"
|
||||||
|
| "review_task_status"
|
||||||
|
| "revision_count"
|
||||||
|
| "pending_manual_confirm"
|
||||||
|
| "updated_at"
|
||||||
>,
|
>,
|
||||||
): OrderSummaryVM {
|
): OrderSummaryVM {
|
||||||
return {
|
return {
|
||||||
orderId: order.order_id,
|
orderId: order.order_id,
|
||||||
workflowId: order.workflow_id,
|
workflowId: order.workflow_id,
|
||||||
|
customerLevel: order.customer_level,
|
||||||
|
serviceMode: order.service_mode,
|
||||||
status: order.status,
|
status: order.status,
|
||||||
statusMeta: getOrderStatusMeta(order.status),
|
statusMeta: getOrderStatusMeta(order.status),
|
||||||
currentStep: order.current_step,
|
currentStep: order.current_step,
|
||||||
currentStepLabel: getWorkflowStepMeta(order.current_step).label,
|
currentStepLabel: getWorkflowStepMeta(order.current_step).label,
|
||||||
|
reviewTaskStatus: order.review_task_status,
|
||||||
|
revisionCount: order.revision_count,
|
||||||
|
pendingManualConfirm: order.pending_manual_confirm,
|
||||||
updatedAt: order.updated_at,
|
updatedAt: order.updated_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,10 +43,15 @@ export type AssetViewModel = {
|
|||||||
export type OrderSummaryVM = {
|
export type OrderSummaryVM = {
|
||||||
orderId: number;
|
orderId: number;
|
||||||
workflowId: string | null;
|
workflowId: string | null;
|
||||||
|
customerLevel: CustomerLevel;
|
||||||
|
serviceMode: ServiceMode;
|
||||||
status: OrderStatus;
|
status: OrderStatus;
|
||||||
statusMeta: StatusMeta;
|
statusMeta: StatusMeta;
|
||||||
currentStep: WorkflowStepName | null;
|
currentStep: WorkflowStepName | null;
|
||||||
currentStepLabel: string;
|
currentStepLabel: string;
|
||||||
|
reviewTaskStatus: ReviewTaskStatus | null;
|
||||||
|
revisionCount: number;
|
||||||
|
pendingManualConfirm: boolean;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ test("proxies recent orders overview from the backend list api", async () => {
|
|||||||
{
|
{
|
||||||
orderId: 3,
|
orderId: 3,
|
||||||
workflowId: "order-3",
|
workflowId: "order-3",
|
||||||
|
customerLevel: "mid",
|
||||||
|
serviceMode: "semi_pro",
|
||||||
status: "waiting_review",
|
status: "waiting_review",
|
||||||
statusMeta: {
|
statusMeta: {
|
||||||
label: "待审核",
|
label: "待审核",
|
||||||
@@ -70,6 +72,9 @@ test("proxies recent orders overview from the backend list api", async () => {
|
|||||||
},
|
},
|
||||||
currentStep: "review",
|
currentStep: "review",
|
||||||
currentStepLabel: "人工审核",
|
currentStepLabel: "人工审核",
|
||||||
|
reviewTaskStatus: "revision_uploaded",
|
||||||
|
revisionCount: 1,
|
||||||
|
pendingManualConfirm: true,
|
||||||
updatedAt: "2026-03-27T14:00:03Z",
|
updatedAt: "2026-03-27T14:00:03Z",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const RECENT_ORDERS: OrderSummaryVM[] = [
|
|||||||
{
|
{
|
||||||
orderId: 4201,
|
orderId: 4201,
|
||||||
workflowId: "wf-4201",
|
workflowId: "wf-4201",
|
||||||
|
customerLevel: "mid",
|
||||||
|
serviceMode: "semi_pro",
|
||||||
status: "waiting_review",
|
status: "waiting_review",
|
||||||
statusMeta: {
|
statusMeta: {
|
||||||
label: "待审核",
|
label: "待审核",
|
||||||
@@ -15,15 +17,20 @@ const RECENT_ORDERS: OrderSummaryVM[] = [
|
|||||||
},
|
},
|
||||||
currentStep: "review",
|
currentStep: "review",
|
||||||
currentStepLabel: "人工审核",
|
currentStepLabel: "人工审核",
|
||||||
|
reviewTaskStatus: "revision_uploaded",
|
||||||
|
revisionCount: 1,
|
||||||
|
pendingManualConfirm: true,
|
||||||
updatedAt: "2026-03-27T09:15:00Z",
|
updatedAt: "2026-03-27T09:15:00Z",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
test("shows the real recent-orders entry state", () => {
|
test("renders orders as a high-density table with shared toolbar controls", () => {
|
||||||
render(<OrdersHome recentOrders={RECENT_ORDERS} />);
|
render(<OrdersHome recentOrders={RECENT_ORDERS} />);
|
||||||
|
|
||||||
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 status filtering and pagination actions", () => {
|
test("supports status filtering and pagination actions", () => {
|
||||||
|
|||||||
@@ -68,9 +68,13 @@ test("maps order summary status metadata and current step labels", () => {
|
|||||||
expect(viewModel).toMatchObject({
|
expect(viewModel).toMatchObject({
|
||||||
orderId: 101,
|
orderId: 101,
|
||||||
workflowId: "wf-101",
|
workflowId: "wf-101",
|
||||||
|
customerLevel: "mid",
|
||||||
|
serviceMode: "semi_pro",
|
||||||
status: "waiting_review",
|
status: "waiting_review",
|
||||||
currentStep: "review",
|
currentStep: "review",
|
||||||
currentStepLabel: "人工审核",
|
currentStepLabel: "人工审核",
|
||||||
|
revisionCount: 0,
|
||||||
|
pendingManualConfirm: false,
|
||||||
statusMeta: {
|
statusMeta: {
|
||||||
label: "待审核",
|
label: "待审核",
|
||||||
tone: "warning",
|
tone: "warning",
|
||||||
|
|||||||
Reference in New Issue
Block a user