feat: extend supply workbench demo and map navigation

This commit is contained in:
2026-09-23 13:19:30 +08:00
parent bebcf2a06c
commit 1752b9ef84
45 changed files with 2137 additions and 449 deletions
@@ -2,17 +2,17 @@ import { useCallback, useEffect, useState, type ReactNode } from "react";
import {
Ban,
CalendarClock,
Check,
CheckCircle2,
CircleDot,
ClipboardList,
Clock3,
Database,
GitBranch,
LoaderCircle,
Pencil,
Plus,
RefreshCw,
Sparkles,
Trash2,
Wrench,
X
} from "lucide-react";
import { Button } from "@/shared/ui/button";
@@ -33,11 +33,12 @@ import type {
type ResourceView = "orders" | "data";
type Inventory = { workOrders: number; testData: number };
export function ManagedTestResources({ client, canWrite, capabilities, onInventoryChange }: {
export function ManagedTestResources({ client, canWrite, capabilities, onInventoryChange, onAgentTrigger }: {
client: PeripheralSampleClient;
canWrite: boolean;
capabilities: PeripheralContext["managed_resources"];
onInventoryChange?: (inventory: Inventory) => void;
onAgentTrigger?: (workOrder: PeripheralWorkOrder) => Promise<void>;
}) {
const [orders, setOrders] = useState<PeripheralWorkOrder[]>([]);
const [records, setRecords] = useState<PeripheralTestDataRecord[]>([]);
@@ -81,9 +82,9 @@ export function ManagedTestResources({ client, canWrite, capabilities, onInvento
};
return <WorkbenchSection
title="外围任务编排"
description="一个任务包含多个子任务,每个子任务按派发、执行、复令推进。"
icon={<GitBranch size={15} />}
title="外围工单与测试数据"
description="管理独立的合成工单和测试数据;仅用于演示,不代表正式工单系统。"
icon={<ClipboardList size={15} />}
tone="teal"
bodyClassName="space-y-3"
actions={<Button size="icon-sm" variant="ghost" aria-label="刷新测试工单和数据" disabled={Boolean(busy)} onClick={() => void refresh()}><RefreshCw size={14} className={busy === "refresh" ? "animate-spin" : ""} /></Button>}
@@ -92,12 +93,12 @@ export function ManagedTestResources({ client, canWrite, capabilities, onInvento
{capabilities.work_orders !== "available" ? <StoragePending resource="测试工单" tables="work_orders / work_order_revisions" /> : null}
{capabilities.test_data !== "available" ? <StoragePending resource="测试数据" tables="test_data_records / test_data_record_revisions" /> : null}
<div role="group" aria-label="外围样板资源类型" className="grid grid-cols-2 gap-1 rounded-xl bg-slate-100 p-1">
<ResourceSwitch active={view === "orders"} count={orders.length} icon={<GitBranch size={13} />} onClick={() => setView("orders")}>任务结构</ResourceSwitch>
<ResourceSwitch active={view === "orders"} count={orders.length} icon={<ClipboardList size={13} />} onClick={() => setView("orders")}>工单列表</ResourceSwitch>
<ResourceSwitch active={view === "data"} count={records.length} icon={<Database size={13} />} onClick={() => setView("data")}>测试数据</ResourceSwitch>
</div>
{view === "orders" ? (
capabilities.work_orders === "available"
? <WorkOrders orders={orders} canWrite={canWrite} busy={busy} mutate={mutate} client={client} />
? <WorkOrders orders={orders} canWrite={canWrite} busy={busy} mutate={mutate} client={client} onAgentTrigger={onAgentTrigger} />
: null
) : (
capabilities.test_data === "available"
@@ -133,8 +134,9 @@ function StoragePending({ resource, tables }: { resource: string; tables: string
</WorkbenchNotice>;
}
function WorkOrders({ orders, canWrite, busy, mutate, client }: ResourceProps & {
function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }: ResourceProps & {
orders: PeripheralWorkOrder[];
onAgentTrigger?: (workOrder: PeripheralWorkOrder) => Promise<void>;
}) {
const [editorOpen, setEditorOpen] = useState(false);
const [editing, setEditing] = useState<PeripheralWorkOrder | null>(null);
@@ -142,6 +144,10 @@ function WorkOrders({ orders, canWrite, busy, mutate, client }: ResourceProps &
const [summary, setSummary] = useState("");
const [status, setStatus] = useState<PeripheralWorkOrder["status"]>("draft");
const [priority, setPriority] = useState<PeripheralWorkOrder["priority"]>("normal");
const [agentTriggerFailure, setAgentTriggerFailure] = useState<{
message: string;
workOrder: PeripheralWorkOrder;
} | null>(null);
const clear = () => {
setEditing(null);
@@ -163,29 +169,67 @@ function WorkOrders({ orders, canWrite, busy, mutate, client }: ResourceProps &
setStatus(item.status);
setPriority(item.priority);
};
const triggerAgent = async (workOrder: PeripheralWorkOrder) => {
if (!onAgentTrigger) {
setAgentTriggerFailure({ message: "当前工作台未连接 Agent 事件入口。", workOrder });
return;
}
try {
await onAgentTrigger(workOrder);
setAgentTriggerFailure(null);
} catch (error) {
setAgentTriggerFailure({ message: message(error), workOrder });
}
};
const createWaterQualityComplaint = () => void mutate("create-water-quality-order", async () => {
const workOrder = await client.createWorkOrder({
title: "居民反映自来水有异味",
summary: "演示小区多户居民反映晨间用水有异味,请核查上游水质及二次供水环节。",
status: "open",
priority: "high",
assignee: null,
scheduled_at: null,
work_order_type: "water_quality_complaint",
location: "演示小区",
scenario_code: "WQ-SECONDARY-SUPPLY-001"
});
await triggerAgent(workOrder);
});
return <div>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0"><h4 className="text-xs font-semibold text-slate-800">任务层级</h4><p className="mt-0.5 text-[11px] leading-4 text-slate-500">管理主任务下的子任务及其阶段状态。</p></div>
<Button size="sm" variant="outline" disabled={!canWrite || Boolean(busy)} onClick={create}><Plus size={13} />新增子任务</Button>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0"><h4 className="text-xs font-semibold text-slate-800">测试工单</h4><p className="mt-0.5 text-[11px] leading-4 text-slate-500">每张工单独立展示派发、执行和复令步骤,步骤状态互不混用。</p></div>
<div className="flex flex-wrap gap-2">
<Button size="sm" disabled={!canWrite || Boolean(busy)} onClick={createWaterQualityComplaint}>
{busy === "create-water-quality-order" ? <LoaderCircle className="animate-spin" /> : <Sparkles size={13} />}
水质投诉演示
</Button>
<Button size="sm" variant="outline" disabled={!canWrite || Boolean(busy)} onClick={create}><Plus size={13} />新增工单</Button>
</div>
</div>
{editorOpen ? <div className="mt-3 rounded-xl bg-cyan-50/60 p-3 ring-1 ring-inset ring-cyan-200">
<EditorHeading title={editing ? "编辑子任务" : "新增子任务"} onClose={clear} />
{agentTriggerFailure ? <WorkbenchNotice className="mt-3" tone="warning" title="工单已创建,Agent 待触发">
<p>{agentTriggerFailure.message}</p>
<Button className="mt-2" size="sm" variant="outline" disabled={Boolean(busy)} onClick={() => void mutate("retry-agent-trigger", () => triggerAgent(agentTriggerFailure.workOrder))}>
{busy === "retry-agent-trigger" ? <LoaderCircle className="animate-spin" /> : <RefreshCw size={13} />}重试触发
</Button>
</WorkbenchNotice> : null}
{editorOpen ? <div className="mt-3 rounded-xl bg-white p-3 ring-1 ring-inset ring-slate-200">
<EditorHeading title={editing ? "编辑工单" : "新增工单"} onClose={clear} />
<label className="mt-2 block text-xs font-medium text-slate-600">标题<Input className="mt-1 h-9" value={title} onChange={(event) => setTitle(event.target.value)} disabled={!canWrite || Boolean(busy)} /></label>
<label className="mt-2 block text-xs font-medium text-slate-600">说明<Textarea className="mt-1 min-h-16" value={summary} onChange={(event) => setSummary(event.target.value)} disabled={!canWrite || Boolean(busy)} /></label>
<div className="mt-2 grid grid-cols-2 gap-2"><FieldSelect label="子任务状态" value={status} onValueChange={(value) => setStatus(value as typeof status)} disabled={!canWrite || Boolean(busy)} options={[
<div className="mt-2 grid grid-cols-2 gap-2"><FieldSelect label="工单状态" value={status} onValueChange={(value) => setStatus(value as typeof status)} disabled={!canWrite || Boolean(busy)} options={[
{ value: "draft", label: "草稿" }, { value: "open", label: "待处理" }, { value: "in_progress", label: "处理中" }, { value: "completed", label: "已完成" }, { value: "cancelled", label: "已取消" }
]} /><FieldSelect label="优先级" value={priority} onValueChange={(value) => setPriority(value as typeof priority)} disabled={!canWrite || Boolean(busy)} options={[
{ value: "low", label: "低" }, { value: "normal", label: "普通" }, { value: "high", label: "高" }, { value: "urgent", label: "紧急" }
]} /></div>
<div className="mt-3 flex gap-2"><Button size="sm" disabled={!canWrite || Boolean(busy) || !title.trim() || !summary.trim()} onClick={() => void mutate("save-order", async () => {
if (editing) await client.updateWorkOrder(editing.work_order_id, { expected_revision: editing.revision, title: title.trim(), summary: summary.trim(), status, priority, clear_assignee: false, clear_scheduled_at: false });
else await client.createWorkOrder({ title: title.trim(), summary: summary.trim(), status, priority, assignee: null, scheduled_at: null });
else await client.createWorkOrder({ title: title.trim(), summary: summary.trim(), status, priority, assignee: null, scheduled_at: null, work_order_type: "general", location: null, scenario_code: null });
clear();
})}>{busy === "save-order" ? <LoaderCircle className="animate-spin" /> : editing ? <Pencil /> : <Plus />}{editing ? "保存修改" : "创建子任务"}</Button>
})}>{busy === "save-order" ? <LoaderCircle className="animate-spin" /> : editing ? <Pencil /> : <Plus />}{editing ? "保存修改" : "创建工单"}</Button>
<Button size="sm" variant="ghost" onClick={clear} disabled={Boolean(busy)}>取消</Button></div>
</div> : null}
<TaskHierarchy
<WorkOrderList
orders={orders}
canWrite={canWrite}
busy={busy}
@@ -198,36 +242,23 @@ function WorkOrders({ orders, canWrite, busy, mutate, client }: ResourceProps &
</div>;
}
function TaskHierarchy({ orders, canWrite, busy, onEdit, onDelete }: {
function WorkOrderList({ orders, canWrite, busy, onEdit, onDelete }: {
orders: PeripheralWorkOrder[];
canWrite: boolean;
busy: string | null;
onEdit: (item: PeripheralWorkOrder) => void;
onDelete: (item: PeripheralWorkOrder) => void;
}) {
const completed = orders.filter((item) => item.status === "completed").length;
const parentStatus = taskStatus(orders);
return <section aria-label="任务层级" className="mt-3 overflow-hidden rounded-xl bg-slate-50/80 ring-1 ring-inset ring-slate-200">
<header className="border-b border-cyan-100 bg-cyan-50/70 px-3.5 py-3 text-slate-900">
<div className="flex items-start gap-3">
<span className="grid size-8 shrink-0 place-items-center rounded-lg bg-white text-cyan-700 shadow-xs ring-1 ring-inset ring-cyan-100"><ClipboardList size={16} /></span>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2"><span className="text-[10px] font-semibold text-cyan-700">任务</span><StatusBadge tone={parentStatus.tone}>{parentStatus.label}</StatusBadge></div>
<h5 className="mt-1 truncate text-sm font-semibold">外围合成数据核查</h5>
<p className="mt-1 text-[10px] text-slate-500"><span className="font-mono">DEMO-WO-001</span> · {orders.length} 个子任务 · {completed}/{orders.length} 完成</p>
</div>
</div>
</header>
{orders.length ? <ol className="relative space-y-2 p-3 before:absolute before:bottom-8 before:left-[25px] before:top-3 before:w-px before:bg-cyan-200">
{orders.map((item, index) => <li key={item.work_order_id} className="relative pl-7">
<span className="absolute left-0 top-3 z-10 grid size-5 place-items-center rounded-full bg-white text-[10px] font-semibold tabular-nums text-cyan-800 ring-1 ring-inset ring-cyan-300">{index + 1}</span>
<SubtaskCard item={item} index={index} canWrite={canWrite} busy={busy} onEdit={() => onEdit(item)} onDelete={() => onDelete(item)} />
return <section aria-label="工单列表" className="mt-3">
{orders.length ? <ol className="space-y-2">
{orders.map((item, index) => <li key={item.work_order_id}>
<WorkOrderCard item={item} index={index} canWrite={canWrite} busy={busy} onEdit={() => onEdit(item)} onDelete={() => onDelete(item)} />
</li>)}
</ol> : <div className="p-3"><WorkbenchEmptyState icon={<Wrench size={15} />} title="当前任务还没有子任务" description="新增子任务后,可在这里查看每项任务的派发、执行和复令状态。" /></div>}
</ol> : <WorkbenchEmptyState icon={<ClipboardList size={15} />} title="暂无测试工单" description="新增工单后,可在这里查看接口返回的工单字段和状态。" />}
</section>;
}
function SubtaskCard({ item, index, canWrite, busy, onEdit, onDelete }: {
function WorkOrderCard({ item, index, canWrite, busy, onEdit, onDelete }: {
item: PeripheralWorkOrder;
index: number;
canWrite: boolean;
@@ -235,47 +266,75 @@ function SubtaskCard({ item, index, canWrite, busy, onEdit, onDelete }: {
onEdit: () => void;
onDelete: () => void;
}) {
return <article className="rounded-xl bg-white p-3 shadow-[0_1px_3px_rgba(15,23,42,0.1)]">
return <article className="rounded-xl bg-slate-50 p-3 ring-1 ring-inset ring-slate-200">
<div className="flex items-start gap-2">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-1.5"><span className="text-[10px] font-semibold text-slate-400">子任务 {String(index + 1).padStart(2, "0")}</span><StatusBadge tone={orderStatusTone(item.status)}>{orderStatus(item.status)}</StatusBadge></div>
<div className="flex flex-wrap items-center gap-1.5"><span className="text-[10px] font-semibold text-slate-400">工单 {String(index + 1).padStart(2, "0")}</span><StatusBadge tone={orderStatusTone(item.status)}>{orderStatus(item.status)}</StatusBadge>{item.work_order_type === "water_quality_complaint" ? <StatusBadge tone="warning">水质投诉</StatusBadge> : null}</div>
<h6 className="mt-1 truncate text-xs font-semibold text-slate-900">{item.title}</h6>
<p className="mt-1 line-clamp-2 text-[11px] leading-4 text-slate-500">{item.summary}</p>
{item.location ? <p className="mt-1 text-[10px] text-slate-400">位置:{item.location}</p> : null}
</div>
<div className="flex shrink-0 gap-0.5"><Button size="icon-sm" variant="ghost" aria-label={`编辑子任务 ${item.title}`} disabled={!canWrite || Boolean(busy)} onClick={onEdit}><Pencil size={14} /></Button><Button size="icon-sm" variant="ghost" className="text-rose-700" aria-label={`删除子任务 ${item.title}`} disabled={!canWrite || Boolean(busy)} onClick={onDelete}><Trash2 size={14} /></Button></div>
<div className="flex shrink-0 gap-0.5"><Button size="icon-sm" variant="ghost" aria-label={`编辑工单 ${item.title}`} disabled={!canWrite || Boolean(busy)} onClick={onEdit}><Pencil size={14} /></Button><Button size="icon-sm" variant="ghost" className="text-rose-700" aria-label={`删除工单 ${item.title}`} disabled={!canWrite || Boolean(busy)} onClick={onDelete}><Trash2 size={14} /></Button></div>
</div>
<StageProgress status={item.status} title={item.title} />
<WorkOrderSteps status={item.status} title={item.title} />
<div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-slate-100 pt-2 text-[10px] text-slate-500">
<span>{priorityLabel(item.priority)}优先级</span><span>r{item.revision}</span>{item.scheduled_at ? <span className="inline-flex items-center gap-1"><CalendarClock size={11} />{formatTimestamp(item.scheduled_at)}</span> : null}
<span>{priorityLabel(item.priority)}优先级</span>
<span className="max-w-full truncate font-mono" title={item.work_order_id}>工单号 {item.work_order_id}</span>
<span>r{item.revision}</span>
{item.assignee ? <span>处理人:{item.assignee}</span> : null}
{item.scheduled_at ? <span className="inline-flex items-center gap-1"><CalendarClock size={11} />{formatTimestamp(item.scheduled_at)}</span> : null}
</div>
</article>;
}
type StageStatus = "done" | "current" | "pending" | "skipped";
type WorkOrderStepStatus = "done" | "current" | "pending" | "skipped";
function StageProgress({ status, title }: { status: PeripheralWorkOrder["status"]; title: string }) {
const statuses = stageStatuses(status);
const stages = ["派发", "执行", "复令"];
return <div className="mt-3 rounded-lg bg-slate-50 px-2.5 py-2" aria-label={`子任务阶段 ${title}`}>
<p className="mb-2 text-[10px] font-semibold text-slate-500">子任务阶段</p>
<ol className="grid grid-cols-3">
{stages.map((stage, index) => {
const stageStatus = statuses[index];
return <li key={stage} className="relative min-w-0 text-center">
{index < stages.length - 1 ? <span className={cn("absolute left-[calc(50%+9px)] right-[calc(-50%+9px)] top-2 h-px", stageStatus === "done" ? "bg-cyan-400" : "bg-slate-300")} aria-hidden="true" /> : null}
<span className={cn(
"relative z-10 mx-auto grid size-4 place-items-center rounded-full text-[9px] font-semibold",
stageStatus === "done" && "bg-cyan-700 text-white",
stageStatus === "current" && "bg-white text-cyan-700 ring-2 ring-inset ring-cyan-500",
stageStatus === "pending" && "bg-white text-slate-400 ring-1 ring-inset ring-slate-300",
stageStatus === "skipped" && "bg-slate-200 text-slate-500"
)}>{stageStatus === "done" ? <Check size={10} /> : stageStatus === "current" ? <CircleDot size={10} /> : stageStatus === "skipped" ? <Ban size={9} /> : index + 1}</span>
<p className="mt-1 truncate text-[10px] font-semibold text-slate-700">{stage}</p>
<p className={cn("truncate text-[9px]", stageStatus === "current" ? "font-semibold text-cyan-700" : stageStatus === "done" ? "text-cyan-700" : "text-slate-400")}>{stageStatusLabel(stageStatus)}</p>
const WORK_ORDER_STEPS = [
{ id: "dispatch", title: "派发", description: "确认处理班组与计划窗口" },
{ id: "execution", title: "执行", description: "按工单要求处置并留痕" },
{ id: "reply", title: "复令", description: "回填结果并完成调度复核" }
] as const;
function WorkOrderSteps({ status, title }: {
status: PeripheralWorkOrder["status"];
title: string;
}) {
const statuses = workOrderStepStatuses(status);
const completed = statuses.filter((item) => item === "done").length;
return <section aria-label={`工单步骤 ${title}`} className="mt-3 border-t border-slate-200/80 pt-2.5">
<div className="mb-2 flex items-center justify-between gap-2">
<p className="text-[10px] font-semibold text-slate-500">工单步骤</p>
<span className="text-[10px] tabular-nums text-slate-400">{completed}/{WORK_ORDER_STEPS.length} 完成</span>
</div>
<ol className="grid gap-1.5">
{WORK_ORDER_STEPS.map((step, index) => {
const stepStatus = statuses[index];
return <li key={step.id} className="grid min-w-0 grid-cols-[24px_minmax(0,1fr)_auto] items-center gap-2 rounded-lg bg-white px-2 py-2 shadow-[0_1px_3px_rgba(15,23,42,0.1)]">
<WorkOrderStepIcon status={stepStatus} step={index + 1} />
<span className="min-w-0">
<span className="block text-[11px] font-semibold text-slate-800">{step.title}</span>
<span className="block truncate text-[10px] text-slate-400">{step.description}</span>
</span>
<StatusBadge tone={workOrderStepTone(stepStatus)}>{workOrderStepStatusLabel(stepStatus)}</StatusBadge>
</li>;
})}
</ol>
</div>;
</section>;
}
function WorkOrderStepIcon({ status, step }: { status: WorkOrderStepStatus; step: number }) {
const className = cn(
"grid size-6 place-items-center rounded-full",
status === "done" && "bg-emerald-600 text-white",
status === "current" && "bg-cyan-50 text-cyan-700 ring-2 ring-inset ring-cyan-500",
status === "pending" && "bg-slate-100 text-slate-500 ring-1 ring-inset ring-slate-200",
status === "skipped" && "bg-slate-200 text-slate-500"
);
if (status === "done") return <span className={className}><CheckCircle2 size={13} /></span>;
if (status === "current") return <span className={className}><CircleDot size={13} /></span>;
if (status === "skipped") return <span className={className}><Ban size={12} /></span>;
return <span className={className}><Clock3 size={12} /><span className="sr-only">步骤 {step}</span></span>;
}
function TestData({ records, canWrite, busy, mutate, client }: ResourceProps & {
@@ -316,7 +375,7 @@ function TestData({ records, canWrite, busy, mutate, client }: ResourceProps & {
return <div>
<ResourceHeading title="测试数据记录" description="用于验证测点回放、缺失值和质量状态。" actionLabel="新增测试数据" canWrite={canWrite} busy={busy} onAdd={create} />
{editorOpen ? <div className="mt-3 rounded-xl bg-cyan-50/60 p-3 ring-1 ring-inset ring-cyan-200">
{editorOpen ? <div className="mt-3 rounded-xl bg-white p-3 ring-1 ring-inset ring-slate-200">
<EditorHeading title={editing ? "编辑测试数据" : "新增测试数据"} onClose={clear} />
<label className="mt-2 block text-xs font-medium text-slate-600">名称<Input className="mt-1 h-9" value={label} onChange={(event) => setLabel(event.target.value)} disabled={!canWrite || Boolean(busy)} /></label>
<div className="mt-2 grid grid-cols-2 gap-2"><label className="block text-xs font-medium text-slate-600">测点<Input className="mt-1 h-9" value={pointId} onChange={(event) => setPointId(event.target.value)} disabled={!canWrite || Boolean(busy)} /></label><label className="block text-xs font-medium text-slate-600">单位<Input className="mt-1 h-9" value={unit} onChange={(event) => setUnit(event.target.value)} disabled={!canWrite || Boolean(busy)} /></label></div>
@@ -351,7 +410,7 @@ function ResourceHeading({ title, description, actionLabel, canWrite, busy, onAd
}
function EditorHeading({ title, onClose }: { title: string; onClose: () => void }) {
return <div className="flex items-center justify-between gap-2"><p className="text-xs font-semibold text-cyan-950">{title}</p><Button size="icon-sm" variant="ghost" aria-label="关闭编辑表单" onClick={onClose}><X size={14} /></Button></div>;
return <div className="flex items-center justify-between gap-2"><p className="text-xs font-semibold text-slate-800">{title}</p><Button size="icon-sm" variant="ghost" aria-label="关闭编辑表单" onClick={onClose}><X size={14} /></Button></div>;
}
type ResourceProps = {
@@ -366,21 +425,15 @@ function formatTimestamp(value: string) { return value.replace("T", " ").replace
function message(error: unknown) { return error instanceof Error ? error.message : "外围测试资源请求失败"; }
function orderStatus(value: PeripheralWorkOrder["status"]) { return ({ draft: "草稿", open: "待处理", in_progress: "处理中", completed: "已完成", cancelled: "已取消" } satisfies Record<typeof value, string>)[value]; }
function orderStatusTone(value: PeripheralWorkOrder["status"]): StatusTone { return ({ draft: "neutral", open: "info", in_progress: "warning", completed: "success", cancelled: "danger" } satisfies Record<typeof value, StatusTone>)[value]; }
function taskStatus(orders: PeripheralWorkOrder[]): { label: string; tone: StatusTone } {
if (!orders.length) return { label: "待编排", tone: "neutral" };
if (orders.every((item) => item.status === "completed")) return { label: "已完成", tone: "success" };
if (orders.every((item) => item.status === "cancelled")) return { label: "已取消", tone: "danger" };
if (orders.some((item) => item.status === "in_progress")) return { label: "进行中", tone: "info" };
return { label: "待处理", tone: "warning" };
}
function stageStatuses(status: PeripheralWorkOrder["status"]): [StageStatus, StageStatus, StageStatus] {
function workOrderStepStatuses(status: PeripheralWorkOrder["status"]): [WorkOrderStepStatus, WorkOrderStepStatus, WorkOrderStepStatus] {
if (status === "completed") return ["done", "done", "done"];
if (status === "in_progress") return ["done", "current", "pending"];
if (status === "open") return ["current", "pending", "pending"];
if (status === "cancelled") return ["skipped", "skipped", "skipped"];
return ["pending", "pending", "pending"];
}
function stageStatusLabel(status: StageStatus) { return ({ done: "已完成", current: "当前阶段", pending: "待开始", skipped: "已跳过" } satisfies Record<StageStatus, string>)[status]; }
function workOrderStepTone(status: WorkOrderStepStatus): StatusTone { return ({ done: "success", current: "info", pending: "neutral", skipped: "neutral" } satisfies Record<WorkOrderStepStatus, StatusTone>)[status]; }
function workOrderStepStatusLabel(status: WorkOrderStepStatus) { return ({ done: "已完成", current: "当前步骤", pending: "待开始", skipped: "已跳过" } satisfies Record<WorkOrderStepStatus, string>)[status]; }
function priorityLabel(value: PeripheralWorkOrder["priority"]) { return ({ low: "低", normal: "普通", high: "高", urgent: "紧急" } satisfies Record<typeof value, string>)[value]; }
function qualityLabel(value: PeripheralTestDataRecord["quality"]) { return ({ valid: "有效", unknown: "未知", missing: "缺失" } satisfies Record<typeof value, string>)[value]; }
function qualityTone(value: PeripheralTestDataRecord["quality"]): StatusTone { return ({ valid: "success", unknown: "neutral", missing: "warning" } satisfies Record<typeof value, StatusTone>)[value]; }