feat: rewrite orders page as dense list

This commit is contained in:
afei A
2026-03-28 00:25:45 +08:00
parent edd03b03a7
commit ae8ab2cf9c
8 changed files with 299 additions and 197 deletions

View File

@@ -3,16 +3,16 @@
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 { 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 = {
page: number;
limit: number;
@@ -28,10 +28,10 @@ type OrdersHomeProps = {
onOpenWorkflow?: (orderId: string) => void;
onPageChange?: (page: number) => void;
onQuerySubmit?: (query: string) => void;
onStatusChange?: (status: FilterStatus) => void;
onStatusChange?: (status: OrderFilterStatus) => void;
recentOrders: OrderSummaryVM[];
selectedQuery?: string;
selectedStatus?: FilterStatus;
selectedStatus?: OrderFilterStatus;
totalPages?: number;
};
@@ -47,27 +47,13 @@ type OrdersOverviewEnvelope = {
};
const TITLE_MESSAGE = "最近订单已接入真实后端接口";
const DEFAULT_MESSAGE = "当前首页展示真实最近订单,同时保留订单号直达入口,方便快速跳转详情和流程。";
const DEFAULT_MESSAGE = "当前页面直接展示真实订单列表,支持关键词、状态和分页操作。";
const DEFAULT_PAGINATION: PaginationData = {
page: 1,
limit: 6,
total: 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({
currentPage = 1,
@@ -83,190 +69,55 @@ export function OrdersHome({
selectedStatus = "all",
totalPages = 0,
}: OrdersHomeProps) {
const [lookupValue, setLookupValue] = useState("");
const [queryValue, setQueryValue] = useState(selectedQuery);
const normalizedLookup = lookupValue.trim();
const canLookup = /^\d+$/.test(normalizedLookup);
const effectiveTotalPages = Math.max(totalPages, 1);
const [serviceModeFilter, setServiceModeFilter] =
useState<OrderFilterServiceMode>("all");
useEffect(() => {
setQueryValue(selectedQuery);
}, [selectedQuery]);
const visibleOrders =
serviceModeFilter === "all"
? recentOrders
: recentOrders.filter((order) => order.serviceMode === serviceModeFilter);
return (
<section className="space-y-8">
<section className="space-y-6">
<PageHeader
eyebrow="Orders home"
title="订单总览"
description="这里是后台首页入口,当前已经接入真实最近订单列表,并保留订单号直达入口方便快速处理。"
meta="真实列表入口"
description="订单页直接承担扫描、筛选和跳转职责,不再把首屏浪费在入口说明和大卡片上。"
meta="真实列表"
/>
<div className="rounded-[28px] border border-[rgba(145,104,46,0.2)] bg-[rgba(202,164,97,0.12)] px-6 py-5">
<p className="text-lg font-semibold tracking-[-0.02em] text-[#7a5323]">
{TITLE_MESSAGE}
</p>
<p className="mt-2 text-sm leading-7 text-[#7a5323]">
{message}
</p>
<div className="flex flex-wrap gap-2">
<MetricChip label="mode" value="真实订单列表" />
<MetricChip label="rows" value={visibleOrders.length} />
<MetricChip label="message" value={TITLE_MESSAGE} />
</div>
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_380px]">
<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">
<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>
<p className="text-sm text-[var(--ink-muted)]">{message}</p>
<Card>
<CardHeader>
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="space-y-2">
<CardEyebrow>Recent visits</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)]"
/>
<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}
<OrdersToolbar
currentPage={currentPage}
query={queryValue}
serviceMode={serviceModeFilter}
status={selectedStatus}
totalPages={totalPages}
onPageChange={onPageChange}
onQueryChange={setQueryValue}
onQuerySubmit={onQuerySubmit}
onServiceModeChange={setServiceModeFilter}
onStatusChange={onStatusChange}
/>
{!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>
<OrdersTable
isLoading={isLoadingRecent}
items={visibleOrders}
onOpenOrder={onOpenOrder}
onOpenWorkflow={onOpenWorkflow}
/>
</section>
);
}
@@ -277,7 +128,8 @@ export function OrdersHomeScreen() {
const [message, setMessage] = useState(DEFAULT_MESSAGE);
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
const [selectedQuery, setSelectedQuery] = useState("");
const [selectedStatus, setSelectedStatus] = useState<FilterStatus>("all");
const [selectedStatus, setSelectedStatus] =
useState<OrderFilterStatus>("all");
const [pagination, setPagination] = useState<PaginationData>(DEFAULT_PAGINATION);
useEffect(() => {