@@ -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-95 0" > { 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-80 0" > { 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 ] ; }