feat: bootstrap auto virtual tryon admin frontend

This commit is contained in:
afei A
2026-03-27 23:38:50 +08:00
commit 98c6b741d6
119 changed files with 19046 additions and 0 deletions

View File

@@ -0,0 +1,377 @@
"use client";
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 { 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";
type FilterStatus = OrderStatus | "all";
type PaginationData = {
page: number;
limit: number;
total: number;
totalPages: number;
};
type OrdersHomeProps = {
currentPage?: number;
isLoadingRecent?: boolean;
message?: string;
onOpenOrder?: (orderId: string) => void;
onOpenWorkflow?: (orderId: string) => void;
onPageChange?: (page: number) => void;
onQuerySubmit?: (query: string) => void;
onStatusChange?: (status: FilterStatus) => void;
recentOrders: OrderSummaryVM[];
selectedQuery?: string;
selectedStatus?: FilterStatus;
totalPages?: number;
};
type OrdersOverviewEnvelope = {
data?: {
limit?: number;
items?: OrderSummaryVM[];
page?: number;
total?: number;
totalPages?: number;
};
message?: string;
};
const TITLE_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,
isLoadingRecent = false,
message = DEFAULT_MESSAGE,
onOpenOrder,
onOpenWorkflow,
onPageChange,
onQuerySubmit,
onStatusChange,
recentOrders,
selectedQuery = "",
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);
useEffect(() => {
setQueryValue(selectedQuery);
}, [selectedQuery]);
return (
<section className="space-y-8">
<PageHeader
eyebrow="Orders home"
title="订单总览"
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>
<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>
<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}
{!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>
);
}
export function OrdersHomeScreen() {
const router = useRouter();
const [recentOrders, setRecentOrders] = useState<OrderSummaryVM[]>([]);
const [message, setMessage] = useState(DEFAULT_MESSAGE);
const [isLoadingRecent, setIsLoadingRecent] = useState(true);
const [selectedQuery, setSelectedQuery] = useState("");
const [selectedStatus, setSelectedStatus] = useState<FilterStatus>("all");
const [pagination, setPagination] = useState<PaginationData>(DEFAULT_PAGINATION);
useEffect(() => {
let active = true;
async function loadRecentOrders() {
setIsLoadingRecent(true);
try {
const params = new URLSearchParams({
page: String(pagination.page),
limit: String(pagination.limit),
});
if (selectedStatus !== "all") {
params.set("status", selectedStatus);
}
if (selectedQuery.length > 0) {
params.set("query", selectedQuery);
}
const response = await fetch(`/api/dashboard/orders-overview?${params.toString()}`);
const payload = (await response.json()) as OrdersOverviewEnvelope;
if (!active) {
return;
}
setRecentOrders(payload.data?.items ?? []);
setPagination((current) => ({
page: payload.data?.page ?? current.page,
limit: payload.data?.limit ?? current.limit,
total: payload.data?.total ?? current.total,
totalPages: payload.data?.totalPages ?? current.totalPages,
}));
setMessage(payload.message ?? DEFAULT_MESSAGE);
} catch {
if (!active) {
return;
}
setRecentOrders([]);
setPagination((current) => ({
...current,
total: 0,
totalPages: 0,
}));
setMessage("最近访问记录加载失败,请稍后重试。");
} finally {
if (active) {
setIsLoadingRecent(false);
}
}
}
void loadRecentOrders();
return () => {
active = false;
};
}, [pagination.limit, pagination.page, selectedQuery, selectedStatus]);
return (
<OrdersHome
currentPage={pagination.page}
isLoadingRecent={isLoadingRecent}
message={message}
onPageChange={(page) =>
setPagination((current) => ({
...current,
page,
}))
}
onQuerySubmit={(query) => {
setSelectedQuery(query);
setPagination((current) => ({
...current,
page: 1,
}));
}}
recentOrders={recentOrders}
onStatusChange={(status) => {
setSelectedStatus(status);
setPagination((current) => ({
...current,
page: 1,
}));
}}
onOpenOrder={(orderId) => router.push(`/orders/${orderId}`)}
onOpenWorkflow={(orderId) => router.push(`/workflows/${orderId}`)}
selectedQuery={selectedQuery}
selectedStatus={selectedStatus}
totalPages={pagination.totalPages}
/>
);
}