@@ -32,22 +32,32 @@ import type {
ScheduledConditionItem ,
ScheduledConditionKpi ,
ScheduledConditionRecord ,
ScheduledConditionRiskLevel ,
ScheduledWorkOrderItem
} from "@/features/workbench/types" ;
import {
formatDuration ,
formatTime ,
formatTimeRange ,
getDetailAccentClassName ,
getKpiStatusClassName ,
getRiskLabel ,
getWorkOrderCurrentStage ,
getWorkOrderStagePillClassName ,
statusClassNames ,
statusLabels
} from "./scheduled-condition-feed-utils" ;
import { getConditionReportRiskLevel } from "./condition-report-risk" ;
const RUNNING_WORKFLOW_TICK_MS = 1 _000 ;
const STATUS_PILL_CLASS_NAME = "rounded-full border px-2 py-0.5 text-xs font-semibold leading-4" ;
const NEUTRAL_SURFACE_CLASS_NAME = "rounded-xl border border-slate-200/80 bg-white/95" ;
const INSET_SURFACE_CLASS_NAME = "rounded-xl border border-slate-200/70 bg-slate-50/80" ;
const REPORT_META_TILE_CLASS_NAME = "rounded-lg border border-slate-200/70 bg-slate-50/80 px-2 py-1.5" ;
const riskIndicatorClassNames : Record < ScheduledConditionRiskLevel , string > = {
normal : "border-slate-200 bg-slate-50 text-slate-600" ,
attention : "border-orange-200 bg-orange-50 text-orange-700" ,
critical : "border-red-200 bg-red-50 text-red-700"
} ;
type IncidentOperation = {
target : string ;
@@ -64,7 +74,9 @@ type SubmittedIncidentWorkOrder = {
operations : IncidentOperation [ ] ;
} ;
type ExecutionStepStatus = "done" | "current" | "pending" ;
type ExecutionStepStatus = "done" | "current" | "pending" | "confirmation" ;
type ExecutionBadgeTone = "active" | "confirmation" | "completed" | "pending" ;
type ExecutionStepMeta = {
label : string ;
@@ -97,7 +109,7 @@ export function StatusPill({
return (
< span
className = { cn (
"rounded-full border px-2 py-0.5 text-xs font-semibold leading-4" ,
STATUS_PILL_CLASS_NAME ,
isWorkOrder ? getWorkOrderStagePillClassName ( currentStage ? . id ) : statusClassNames [ condition . status ] ,
className
) }
@@ -112,13 +124,12 @@ export function ConditionDetailPanel({ condition, onContinueConversation }: Cond
condition . kind === "work_order"
? ` ${ condition . id } - ${ getWorkOrderCurrentStage ( condition ) ? . id ? ? condition . status } `
: condition . id ;
let content : React.ReactNode ;
if ( condition . kind === "work_order" ) {
content = < WorkOrderDetailPanel condition = { condition } / > ;
} else {
content = < ConditionRecordDetailPanel condition = { condition } onContinueConversation = { onContinueConversation } / > ;
}
const content =
condition . kind === "work_order" ? (
< WorkOrderDetailPanel condition = { condition } / >
) : (
< ConditionRecordDetailPanel condition = { condition } onContinueConversation = { onContinueConversation } / >
) ;
return (
< AnimatePresence initial = { false } mode = "wait" >
@@ -146,6 +157,7 @@ function ConditionRecordDetailPanel({
const insight = getConditionAnalysisInsight ( condition ) ;
const [ submittedWorkOrder , setSubmittedWorkOrder ] = useState < SubmittedIncidentWorkOrder | null > ( null ) ;
const header = getConditionDetailHeader ( condition ) ;
const riskLevel = getConditionReportRiskLevel ( condition . status , condition . riskLevel ) ;
const bodyKey = ` ${ condition . id } - ${ condition . status } - ${ condition . riskLevel } ` ;
function handleSubmitIncidentWorkOrder() {
@@ -161,7 +173,7 @@ function ConditionRecordDetailPanel({
return (
< DetailScroll >
< DetailShell condition = { condition } >
< DetailShell >
< DetailHeader
condition = { condition }
icon = { header . icon }
@@ -180,60 +192,62 @@ function ConditionRecordDetailPanel({
>
{ condition . status === "running" ? (
< RunningConditionBody condition = { condition } / >
) : condition . status === "error" ? (
< >
< ConditionStatusReport condition = { condition } tone = "red" / >
< DecisionSummaryBlock
icon = { ShieldAlert }
title = "异常判断"
tone = "red"
summary = { insight . summary }
notes = { insight . notes }
/ >
< SolutionOptionsList options = { insight . solutionOptions ? ? [ ] } / >
< IncidentWorkOrderPanel
condition = { condition }
submittedWorkOrder = { submittedWorkOrder }
onSubmitWorkOrder = { handleSubmitIncidentWorkOrder }
/ >
< DecisionActionStrip
icon = { Bot }
title = "Agent 继续分析"
value = { condition . recommendation ? ? "建议打开 Agent 会话,结合异常证据继续判断处置优先级。" }
actionLabel = "继续分析"
onAction = { onContinueConversation }
/ >
< / >
) : condition . status === "warning" || condition . riskLevel === "attention" ? (
< >
< ConditionStatusReport condition = { condition } tone = "orange" / >
< DecisionSummaryBlock
icon = { Lightbulb }
title = "关注说明"
tone = "orange"
summary = { insight . summary }
notes = { insight . notes }
/ >
< EscalationCriteriaList items = { insight . escalationCriteria ? ? [ ] } / >
< DecisionActionStrip
icon = { Bot }
title = "复核建议"
value = { condition . recommendation ? ? "建议保留为关注项,下一轮工况继续复核同一测点趋势。" }
actionLabel = "继续对话"
onAction = { onContinueConversation }
/ >
< / >
) : (
< >
< ConditionStatusReport condition = { condition } tone = "blue" / >
< EvidenceList items = { condition . evidence ? ? [ ] } / >
< DecisionActionStrip
icon = { Bot }
title = "Agent 建议"
value = { condition . recommendation ? ? "当前工况暂无新增处置建议,保持观察即可。" }
actionLabel = "继续对话 "
onAction = { onContinueConversation }
/ >
< ConditionStatusReport condition = { condition } riskLevel = { riskLevel } / >
{ riskLevel === "critical" ? (
< >
< DecisionSummaryBlock
icon = { ShieldAlert }
title = "异常判断"
iconClassName = "text-red-600 "
summary = { insight . summary }
notes = { insight . notes }
/ >
< SolutionOptionsList options = { insight . solutionOptions ? ? [ ] } / >
< IncidentWorkOrderPanel
condition = { condition }
submittedWorkOrder = { submittedWorkOrder }
onSubmitWorkOrder = { handleSubmitIncidentWorkOrder }
/ >
< DecisionActionStrip
icon = { Bot }
title = "Agent 继续分析"
value = { condition . recommendation ? ? "建议打开 Agent 会话,结合异常证据继续判断处置优先级。" }
actionLabel = "继续分析"
onAction = { onContinueConversation }
/ >
< / >
) : riskLevel === "attention" ? (
< >
< DecisionSummaryBlock
icon = { Lightbulb }
title = "关注说明"
iconClassName = "text-orange-600"
summary = { insight . summary }
notes = { insight . notes }
/ >
< EscalationCriteriaList items = { insight . escalationCriteria ? ? [ ] } / >
< DecisionActionStrip
icon = { Bot }
title = "复核建议"
value = { condition . recommendation ? ? "建议保留为关注项,下一轮工况继续复核同一测点趋势。" }
actionLabel = "继续对话"
onAction = { onContinueConversation }
/ >
< / >
) : (
< >
< EvidenceList items = { condition . evidence ? ? [ ] } / >
< DecisionActionStrip
icon = { Bot }
title = "Agent 建议"
value = { condition . recommendation ? ? "当前工况暂无新增处置建议,保持观察即可。" }
actionLabel = "继续对话"
onAction = { onContinueConversation }
/ >
< / >
) }
< / >
) }
< / motion.div >
@@ -249,7 +263,7 @@ function WorkOrderDetailPanel({ condition }: { condition: ScheduledWorkOrderItem
return (
< DetailScroll >
< DetailShell condition = { condition } >
< DetailShell >
< DetailHeader
condition = { condition }
icon = { ClipboardList }
@@ -310,22 +324,6 @@ function getConditionDetailHeader(condition: ScheduledConditionRecord) {
} ;
}
if ( condition . status === "error" ) {
return {
icon : ShieldAlert ,
iconClassName : "bg-red-50 text-red-700" ,
iconSpin : false
} ;
}
if ( condition . status === "warning" || condition . riskLevel === "attention" ) {
return {
icon : AlertTriangle ,
iconClassName : "bg-orange-50 text-orange-700" ,
iconSpin : false
} ;
}
return {
icon : FileText ,
iconClassName : undefined ,
@@ -342,21 +340,12 @@ function DetailScroll({ children }: { children: React.ReactNode }) {
}
type DetailShellProps = {
condition : ScheduledConditionItem ;
children : React.ReactNode ;
} ;
function DetailShell ( { condition , children } : DetailShellProps ) {
const isWorkOrder = condition . kind === "work_order" ;
function DetailShell ( { children } : DetailShellProps ) {
return (
< div
className = { cn (
"relative overflow-hidden border border-slate-200/80 bg-white/95 ring-1 ring-white/80" ,
"rounded-xl"
) }
>
< div className = { cn ( "absolute inset-y-0 left-0 w-1" , getDetailAccentClassName ( condition . status , isWorkOrder ) ) } aria-hidden = "true" / >
< div className = { cn ( NEUTRAL_SURFACE_CLASS_NAME , "overflow-hidden ring-1 ring-white/80" ) } >
{ children }
< / div >
) ;
@@ -367,18 +356,16 @@ type DetailHeaderProps = {
icon : typeof FileText ;
iconClassName? : string ;
iconSpin? : boolean ;
children? : React.ReactNode ;
} ;
function DetailHeader ( {
condition ,
icon : Icon ,
iconClassName ,
iconSpin = false ,
children
iconSpin = false
} : DetailHeaderProps ) {
return (
< div className = "bg-white/95 px-3 py-3 pl-4 " >
< div className = "bg-white/95 px-3 py-3" >
< div className = "grid gap-3 2xl:grid-cols-[minmax(0,1fr)_220px]" >
< div className = "flex min-w-0 items-start gap-2.5" >
< span
@@ -393,7 +380,9 @@ function DetailHeader({
< div className = "min-w-0 flex-1" >
< div className = "flex flex-wrap items-center gap-2" >
< span className = "font-mono text-xs font-semibold text-slate-500" > { formatTime ( condition . scheduledAt ) } < / span >
< StatusPill condition = { condition } / >
{ condition . kind === "work_order" || condition . status === "running" || condition . status === "pending" ? (
< StatusPill condition = { condition } / >
) : null }
< / div >
< h3 className = "mt-1 text-base font-semibold leading-5 text-slate-950" > { condition . title } < / h3 >
{ condition . detail ? < p className = "mt-1.5 text-xs leading-5 text-slate-700" > { condition . detail } < / p > : null }
@@ -402,44 +391,13 @@ function DetailHeader({
< DetailMetricGrid condition = { condition } / >
< / div >
{ children ? < div className = "mt-3" > { children } < / div > : null }
< / div >
) ;
}
type DetailTone = "violet" | "blue" | "red" | "orange" ;
const decisionSurfaceClassNames : Record < DetailTone , string > = {
red : "border-red-100 border-l-4 border-l-red-400" ,
orange : "border-orange-100 border-l-4 border-l-orange-400" ,
blue : "border-blue-100 border-l-4 border-l-blue-500" ,
violet : "border-violet-100 border-l-4 border-l-violet-500"
} ;
const detailDotClassNames : Record < DetailTone , string > = {
red : "bg-red-500" ,
orange : "bg-orange-500" ,
blue : "bg-blue-500" ,
violet : "bg-violet-500"
} ;
const taskExecutionBadgeClassNames : Record < DetailTone , string > = {
red : "border-red-100 bg-red-50 text-red-700" ,
orange : "border-orange-100 bg-orange-50 text-orange-700" ,
blue : "border-blue-100 bg-blue-50 text-blue-700" ,
violet : "border-violet-100 bg-violet-50 text-violet-700"
} ;
const taskExecutionCurrentStepClassNames : Record < DetailTone , string > = {
red : "border-red-100 bg-red-50/70" ,
orange : "border-orange-100 bg-orange-50/70" ,
blue : "border-blue-100 bg-blue-50/70" ,
violet : "border-violet-100 bg-violet-50/70"
} ;
function DetailDecisionBody ( { children } : { children : React.ReactNode } ) {
return (
< div className = "space-y-2.5 px-3 pb-3 pl-4 " >
< div className = "space-y-2.5 px-3 pb-3" >
{ children }
< / div >
) ;
@@ -448,24 +406,24 @@ function DetailDecisionBody({ children }: { children: React.ReactNode }) {
function DecisionSummaryBlock ( {
icon : Icon ,
title ,
tone ,
iconClassName ,
summary ,
notes
} : {
icon : typeof Bot ;
title : string ;
tone : DetailTone ;
iconClassName : string ;
summary : string ;
notes : string [ ] ;
} ) {
return (
< section className = { cn ( "rounded-xl border bg-white/95 px-3 py-3" , getDecisionSurfaceClassName ( tone ) ) } >
< SectionTitle icon = { Icon } title = { title } / >
< section className = { cn ( NEUTRAL_SURFACE_CLASS_NAME , "px-3 py-3" ) } >
< SectionTitle icon = { Icon } title = { title } iconClassName = { iconClassName } / >
< p className = "mt-2 text-xs leading-5 text-slate-700" > { summary } < / p >
< ul className = "mt-2 grid gap-1.5" >
{ notes . map ( ( note ) = > (
< li key = { note } className = "grid grid-cols-[7px_1fr] gap-2 text-xs leading-5 text-slate-700" >
< span className = { cn ( "mt-2 h-1.5 w-1.5 rounded-full" , getDetailDotClassName ( tone ) ) } aria-hidden = "true" / >
< span className = "mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" aria-hidden = "true" / >
< span > { note } < / span >
< / li >
) ) }
@@ -480,13 +438,13 @@ function SolutionOptionsList({
options : NonNullable < ScheduledConditionAnalysisInsight [ "solutionOptions" ] > ;
} ) {
return (
< section className = "rounded-xl border border-slate-200/80 bg-white/95 px-3 py-3">
< section className = { cn ( NEUTRAL_SURFACE_CLASS_NAME , " px-3 py-3") } >
< SectionEyebrow icon = { Route } label = "处置方案" / >
< div className = "mt-2 divide-y divide-slate-200/70 overflow-hidden rounded-xl border border-slate-200/70 bg-slate-50/80" >
< div className = { cn ( INSET_SURFACE_CLASS_NAME , "mt-2 divide-y divide-slate-200/70 overflow-hidden" ) } >
{ options . map ( ( option , index ) = > (
< article key = { option . id } className = "px-2.5 py-2" >
< div className = "flex items-center gap-2" >
< span className = "grid h-5 w-5 shrink-0 place-items-center rounded-lg bg-red-50 text-xs font-semibold text-red-7 00 ring-1 ring-red-1 00" >
< span className = "grid h-5 w-5 shrink-0 place-items-center rounded-lg bg-white text-xs font-semibold text-slate-6 00 ring-1 ring-slate-2 00" >
{ index + 1 }
< / span >
< h4 className = "min-w-0 truncate text-xs font-semibold leading-4 text-slate-900" > { option . title } < / h4 >
@@ -526,7 +484,7 @@ function IncidentWorkOrderPanel({
const steps : ExecutionStep [ ] = [
{ id : "analysis" , title : "事件分析" , status : "done" , description : "汇总异常证据、影响分区和风险等级。" } ,
{ id : "solution" , title : "形成方案" , status : "done" , description : "生成阀门、泵站和现场复核操作清单。" } ,
{ id : "confirm" , title : "人工确认" , status : submitted ? "done" : "current " , description : "调度员确认影响范围和操作边界。" } ,
{ id : "confirm" , title : "人工确认" , status : submitted ? "done" : "confirmation " , description : "调度员确认影响范围和操作边界。" } ,
{ id : "submit" , title : "提交工单" , status : submitted ? "done" : "pending" , description : "写入系统工单并通知执行岗位。" }
] ;
@@ -535,12 +493,12 @@ function IncidentWorkOrderPanel({
icon = { ClipboardCheck }
title = "任务执行"
badge = { submitted ? "已提交" : "待人工确认" }
badgeTone = { submitted ? "completed" : "confirmation" }
summary = {
submitted
? ` ${ submittedWorkOrder ? . code } 已进入系统工单流转,并通知相关班组执行管网要素操作。 `
: "异常分析形成处置方案后,由调度员确认并提交系统工单,通知现场或调度岗位操作阀门、泵站和相关测点。"
}
tone = "red"
meta = {
submittedWorkOrder
? [
@@ -562,9 +520,9 @@ function IncidentWorkOrderPanel({
aria-disabled = { submitted }
onClick = { onSubmitWorkOrder }
className = { cn (
"mt-1 flex min-h-9 w-full items-center justify-center gap-1.5 px-3 text-xs font-semibold leading-4 transition" ,
"mt-1 flex min-h-9 w-full items-center justify-center gap-1.5 px-3 text-xs font-semibold leading-4 transition-colors " ,
"rounded-xl" ,
submitted ? "cursor-not-allowed bg-green-50 text-green-700" : "bg-red -600 text-white hover:bg-red -700"
submitted ? "cursor-not-allowed bg-green-50 text-green-700" : "bg-blue -600 text-white hover:bg-blue -700"
) }
>
{ submitted ? < CheckCircle2 size = { 14 } aria-hidden = "true" / > : < Send size = { 14 } aria-hidden = "true" / > }
@@ -582,7 +540,7 @@ function IncidentOperationList({
submitted : boolean ;
} ) {
return (
< div className = "divide-y divide-slate-200/70 overflow-hidden rounded-xl border border-slate-200/70 bg-slate-50/80" >
< div className = { cn ( INSET_SURFACE_CLASS_NAME , "divide-y divide-slate-200/70 overflow-hidden" ) } >
{ operations . map ( ( operation ) = > (
< article key = { ` ${ operation . target } - ${ operation . action } ` } className = "px-2.5 py-2" >
< div className = "flex flex-wrap items-center justify-between gap-2" >
@@ -613,12 +571,12 @@ function IncidentOperationList({
function EscalationCriteriaList ( { items } : { items : string [ ] } ) {
return (
< section className = "rounded-xl border border-slate-200/80 bg-white/95 px-3 py-3">
< section className = { cn ( NEUTRAL_SURFACE_CLASS_NAME , " px-3 py-3") } >
< SectionEyebrow icon = { AlertTriangle } label = "升级条件" / >
< ol className = "mt-2 divide-y divide-slate-200/70 overflow-hidden rounded-xl border border-slate-200/70 bg-slate-50/80" >
< ol className = { cn ( INSET_SURFACE_CLASS_NAME , "mt-2 divide-y divide-slate-200/70 overflow-hidden" ) } >
{ items . map ( ( item , index ) = > (
< li key = { item } className = "grid grid-cols-[18px_1fr] gap-2 px-2 py-1.5 text-xs leading-5 text-slate-700" >
< span className = "mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg bg-orange-50 text-xs font-semibold text-orang e-7 00 ring-1 ring-orang e-1 00" >
< span className = "mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg bg-white text-xs font-semibold text-slat e-6 00 ring-1 ring-slat e-2 00" >
{ index + 1 }
< / span >
< span > { item } < / span >
@@ -631,12 +589,12 @@ function EscalationCriteriaList({ items }: { items: string[] }) {
function ConditionStatusReport ( {
condition ,
tone
riskLevel
} : {
condition : ScheduledConditionItem ;
tone : DetailTone ;
condition : ScheduledConditionRecord ;
riskLevel : ScheduledConditionRiskLevel ;
} ) {
if ( condition . kind !== "condition" || condition . status === "running" || ( ! condition . report && ! condition . kpis ? . length ) ) {
if ( ! condition . report && ! condition . kpis ? . length ) {
return null ;
}
@@ -645,18 +603,23 @@ function ConditionStatusReport({
const statusTime = formatTimeRange ( condition . scheduledAt , condition . durationMinutes ) ;
return (
< section className = { cn ( "overflow-hidden rounded-xl border bg-white/95" , getDecisionSurfaceClassName ( tone ) ) } >
< section className = { cn ( NEUTRAL_SURFACE_CLASS_NAME , "overflow-hidden" ) } >
< div className = "border-b border-slate-200/70 px-3 py-3" >
< div className = "flex flex-wrap items-center justify-between gap-2" >
< SectionTitle icon = { FileText } title = "完成状态报告" / >
< StatusPill condition = { condition } / >
< LifecyclePill / >
< / div >
< h4 className = "mt-2 text-sm font-semibold leading-5 text-slate-950" > { report ? . title ? ? condition . title } < / h4 >
{ report ? . conclusion ? < p className = "mt-1.5 text-xs leading-5 text-slate-700" > { report . conclusion } < / p > : null }
< div className = "mt-2 grid gap-1.5 sm:grid-cols-3 " >
< ReportMetaTile label = "报告时段" value = { statusTime } / >
< ReportMetaTile label = "完成状态" value = { statusLabels [ condition . status ] } / >
< ReportMetaTile label = "风险等级" value = { getRiskLabel ( condition . riskLevel ) } / >
< h4 className = "mt-2 min-w-0 text-sm font-semibold leading-5 text-slate-950" > { report ? . title ? ? condition . title } < / h4 >
{ report ? . conclusion ? (
< div className = "mt-1.5 flex min-w-0 flex-wrap items-start gap-2 " >
< p className = "min-w-[12rem] flex-1 text-xs leading-5 text-slate-700" > { report . conclusion } < / p >
< RiskBadge riskLevel = { riskLevel } / >
< / div >
) : null }
< div className = "mt-2 flex items-center gap-1.5 text-xs leading-5 text-slate-500" >
< Clock3 size = { 13 } aria-hidden = "true" / >
< span > 报 告 时 段 < / span >
< span className = "font-mono font-semibold tabular-nums text-slate-700" > { statusTime } < / span >
< / div >
< / div >
@@ -674,7 +637,7 @@ function ConditionStatusReport({
{ report ? . sections ? . length ? (
< div className = "grid gap-2 border-t border-slate-200/70 px-3 py-3" >
{ report . sections . map ( ( section ) = > (
< ReportSection key = { section . title } title = { section . title } items = { section . items } tone = { tone } / >
< ReportSection key = { section . title } title = { section . title } items = { section . items } / >
) ) }
< / div >
) : null }
@@ -682,9 +645,40 @@ function ConditionStatusReport({
) ;
}
function LifecyclePill() {
return (
< span className = "inline-flex items-center gap-1 rounded-full border border-green-100 bg-green-50 px-2 py-0.5 text-xs font-semibold leading-4 text-green-700" >
< CheckCircle2 size = { 12 } aria-hidden = "true" / >
< span > 完 成 < / span >
< / span >
) ;
}
function RiskBadge ( { riskLevel } : { riskLevel : ScheduledConditionRiskLevel } ) {
const Icon = riskLevel === "critical" ? ShieldAlert : riskLevel === "attention" ? AlertTriangle : CheckCircle2 ;
const label = getRiskLabel ( riskLevel ) ;
return (
< span
className = { cn (
"inline-flex shrink-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-semibold leading-4" ,
getRiskIndicatorClassName ( riskLevel )
) }
aria-label = { ` 风险等级: ${ label } ` }
>
< Icon size = { 12 } aria-hidden = "true" / >
< span > { label } < / span >
< / span >
) ;
}
function getRiskIndicatorClassName ( riskLevel : ScheduledConditionRiskLevel ) {
return riskIndicatorClassNames [ riskLevel ] ;
}
function ReportMetaTile ( { label , value } : { label : string ; value : string } ) {
return (
< div className = "rounded-lg border border-slate-200/70 bg-slate-50/80 px-2 py-1.5" >
< div className = { REPORT_META_TILE_CLASS_NAME } >
< div className = "text-xs font-semibold text-slate-400" > { label } < / div >
< div className = "mt-0.5 truncate text-xs font-semibold text-slate-700" title = { value } >
{ value }
@@ -695,20 +689,18 @@ function ReportMetaTile({ label, value }: { label: string; value: string }) {
function ReportSection ( {
title ,
items ,
tone
items
} : {
title : string ;
items : string [ ] ;
tone : DetailTone ;
} ) {
return (
< section className = "rounded-xl border border-slate-200/70 bg-slate-50/80 px-2.5 py-2">
< section className = { cn ( INSET_SURFACE_CLASS_NAME , " px-2.5 py-2") } >
< div className = "text-xs font-semibold text-slate-500" > { title } < / div >
< ul className = "mt-1.5 grid gap-1" >
{ items . map ( ( item ) = > (
< li key = { item } className = "grid grid-cols-[7px_1fr] gap-2 text-xs leading-5 text-slate-700" >
< span className = { cn ( "mt-2 h-1.5 w-1.5 rounded-full" , getDetailDotClassName ( tone ) ) } aria-hidden = "true" / >
< span className = "mt-2 h-1.5 w-1.5 rounded-full bg-slate-300" aria-hidden = "true" / >
< span > { item } < / span >
< / li >
) ) }
@@ -719,7 +711,7 @@ function ReportSection({
function KpiTile ( { kpi } : { kpi : ScheduledConditionKpi } ) {
return (
< div className = "min-w-0 rounded-xl border border-slate-200/70 bg-slate-50/80 px-2.5 py-2" >
< div className = { cn ( "min-w-0 rounded-xl border px-2.5 py-2" , getKpiSurfaceClassName ( kpi . status ) ) } >
< div className = "flex items-start justify-between gap-2" >
< div className = "min-w-0" >
< div className = "truncate text-xs font-semibold text-slate-500" title = { kpi . label } >
@@ -730,7 +722,7 @@ function KpiTile({ kpi }: { kpi: ScheduledConditionKpi }) {
{ kpi . unit ? < span className = "text-xs font-semibold text-slate-500" > { kpi . unit } < / span > : null }
< / div >
< / div >
< span className = { cn ( "shrink-0 rounded-full border px-1.5 py-0.5 text-xs font-semibold leading-4" , getKpiStatusClassName ( kpi . status ) ) } >
< span className = { cn ( "shrink-0 rounded-full border px-1.5 py-0.5 text-xs font-semibold leading-4" , getRiskIndicatorClassName ( kpi . status ) ) } >
{ getRiskLabel ( kpi . status ) }
< / span >
< / div >
@@ -745,6 +737,18 @@ function KpiTile({ kpi }: { kpi: ScheduledConditionKpi }) {
) ;
}
function getKpiSurfaceClassName ( riskLevel : ScheduledConditionRiskLevel ) {
if ( riskLevel === "critical" ) {
return "border-red-200/80 bg-red-50/80" ;
}
if ( riskLevel === "attention" ) {
return "border-orange-200/80 bg-orange-50/80" ;
}
return "border-slate-200/70 bg-slate-50/80" ;
}
function DecisionActionStrip ( {
icon : Icon ,
title ,
@@ -763,7 +767,7 @@ function DecisionActionStrip({
onAction ? : ( ) = > void ;
} ) {
return (
< div className = "flex flex-col gap-2 rounded-xl border border-slate-200/80 bg-white/95 px-3 py-3 min-[1500px]:flex-row min-[1500px]:items-center" >
< div className = { cn ( NEUTRAL_SURFACE_CLASS_NAME , "flex flex-col gap-2 px-3 py-3 min-[1500px]:flex-row min-[1500px]:items-center" ) } >
< div className = "min-w-0 flex-1" >
< SectionEyebrow icon = { Icon } label = { title } / >
< p className = "mt-1 text-xs leading-5 text-slate-700" > { value } < / p >
@@ -775,7 +779,7 @@ function DecisionActionStrip({
title = { actionTitle }
onClick = { onAction }
className = { cn (
"flex min-h-9 shrink-0 items-center justify-center gap-1.5 px-3 text-xs font-semibold leading-4 transition min-[1500px]:w-[104px]" ,
"flex min-h-9 shrink-0 items-center justify-center gap-1.5 px-3 text-xs font-semibold leading-4 transition-colors min-[1500px]:w-[104px]" ,
"rounded-xl" ,
actionDisabled
? "cursor-not-allowed bg-white/70 text-slate-500 opacity-80"
@@ -804,53 +808,61 @@ function SectionEyebrow({
) ;
}
function getDecisionSurfaceClassName ( tone : DetailTone ) {
return decisionSurfaceClassNames [ tone ] ;
function getTaskExecutionBadgeClassName ( tone : ExecutionBadgeTone ) {
if ( tone === "completed" ) {
return "border-green-100 bg-green-50 text-green-700" ;
}
if ( tone === "confirmation" ) {
return "border-orange-100 bg-orange-50 text-orange-700" ;
}
if ( tone === "active" ) {
return "border-blue-100 bg-blue-50 text-blue-700" ;
}
return "border-slate-200 bg-slate-50 text-slate-600" ;
}
function getDetailDotClassName ( tone : DetailTone ) {
return detailDotClassNames [ tone ] ;
}
function getTaskExecutionBadgeClassName ( tone : DetailTone ) {
return taskExecutionBadgeClassNames [ tone ] ;
}
function getTaskExecutionStepIconClassName ( status : ExecutionStepStatus , tone : DetailTone ) {
function getTaskExecutionStepIconClassName ( status : ExecutionStepStatus ) {
if ( status === "done" ) {
return "bg-green-500 text-white" ;
}
if ( status === "current" ) {
if ( tone === "red" ) {
return "bg-red-600 text-white" ;
}
if ( tone === "orange" ) {
return "bg-orange-500 text-white" ;
}
if ( tone === "violet" ) {
return "bg-violet-600 text-white" ;
}
return "bg-blue-600 text-white" ;
}
if ( status === "confirmation" ) {
return "bg-orange-500 text-white" ;
}
return "bg-slate-100 text-slate-400" ;
}
function getTaskExecutionCurrentStepClassName ( tone : DetailTone ) {
return taskExecutionCurrentStepClassNames [ tone ] ;
function getTaskExecutionStepSurfaceClassName ( status : ExecutionStepStatus ) {
if ( status === "current" ) {
return "border-blue-100 bg-blue-50/70" ;
}
if ( status === "confirmation" ) {
return "border-orange-100 bg-orange-50/70" ;
}
return "border-slate-200/70 bg-slate-50/80" ;
}
function getTaskExecutionStepStatusClassName ( status : ExecutionStepStatus , tone : DetailTone ) {
function getTaskExecutionStepStatusClassName ( status : ExecutionStepStatus ) {
if ( status === "done" ) {
return "border-green-100 bg-green-50 text-green-700" ;
}
if ( status === "current" ) {
return getTaskExecutionBadgeClassName ( tone ) ;
return "border-blue-100 bg-blue-50 text-blue-700" ;
}
if ( status === "confirmation" ) {
return "border-orange-100 bg-orange-50 text-orange-700" ;
}
return "border-slate-200 bg-white text-slate-500" ;
@@ -865,21 +877,37 @@ function getTaskExecutionStepStatusLabel(status: ExecutionStepStatus) {
return "当前" ;
}
if ( status === "confirmation" ) {
return "待人工确认" ;
}
return "待处理" ;
}
function DetailMetricGrid ( { condition } : { condition : ScheduledConditionItem } ) {
const durationLabel = condition . status === "running" ? "状态" : "耗时" ;
const durationValue = condition . status === "running" ? "实时执行" : formatDuration ( condition . durationMinutes , condition . status ) ;
const modelValue = condition . kind === "work_order" ? "预约工单" : condition . modelName ? ? "未记录" ;
const sessionValue = condition . kind === "work_order" ? formatTimeRange ( condition . scheduledAt , condition . durationMinutes ) : condition . sessionId ? ? "未记录" ;
const metrics =
condition . kind === "condition"
? [
{ label : "模型" , value : condition.modelName ? ? "未记录" } ,
{ label : "会话" , value : condition.sessionId ? ? "未记录" } ,
{ label : durationLabel , value : durationValue , className : "col-span-2" }
]
: [
{ label : "类型" , value : "预约工单" } ,
{ label : "窗口" , value : formatTimeRange ( condition . scheduledAt , condition . durationMinutes ) } ,
{ label : "风险" , value : getRiskLabel ( condition . riskLevel ) } ,
{ label : durationLabel , value : durationValue }
] ;
return (
< div className = "grid min-w-0 grid-cols-2 gap-1.5" >
< ReportMetaTile label = { condition . kind === "work_order" ? "类型" : "模型" } value = { modelValue } / >
< ReportMetaTile label = { condition . kind === "work_order" ? "窗口" : "会话" } value = { sessionValue } / >
< ReportMetaTile label = "风险" value = { getRiskLabel ( condition . riskLevel ) } / >
< ReportMetaTile label = { durationLabel } value = { durationValue } / >
{ metrics . map ( ( metric ) = > (
< div key = { metric . label } className = { metric . className } >
< ReportMetaTile label = { metric . label } value = { metric . value } / >
< / div >
) ) }
< / div >
) ;
}
@@ -908,8 +936,8 @@ function RunningWorkflowPanel({
icon = { Loader2 }
title = { workflow . title }
badge = "执行中"
badgeTone = "active"
summary = { ` ${ currentStep . title } : ${ currentStep . detail } ` }
tone = "blue"
meta = { [
{ label : "任务对象" , value : condition.title } ,
{ label : "当前进度" , value : ` ${ workflowState . progress } % ` } ,
@@ -920,10 +948,18 @@ function RunningWorkflowPanel({
) ;
}
function SectionTitle ( { icon : Icon , title } : { icon : typeof Bot ; title : string } ) {
function SectionTitle ( {
icon : Icon ,
title ,
iconClassName
} : {
icon : typeof Bot ;
title : string ;
iconClassName? : string ;
} ) {
return (
< div className = "flex items-center gap-1.5 text-xs font-semibold" >
< Icon size = { 13 } aria-hidden = "true" / >
< Icon size = { 13 } aria-hidden = "true" className = { iconClassName } / >
< span > { title } < / span >
< / div >
) ;
@@ -950,7 +986,7 @@ function TaskExecutionPanel({
summary ,
meta ,
steps ,
tone = "blue " ,
badgeTone = "pending " ,
children
} : {
icon : typeof Bot ;
@@ -959,15 +995,15 @@ function TaskExecutionPanel({
summary? : string ;
meta? : ExecutionStepMeta [ ] ;
steps : ExecutionStep [ ] ;
t one? : DetailTone ;
badgeT one? : ExecutionBadgeTone ;
children? : React.ReactNode ;
} ) {
return (
< section className = { cn ( "overflow-hidden rounded-xl border bg-white/95" , getDecisionSurfaceClassName ( tone ) ) } >
< section className = { cn ( NEUTRAL_SURFACE_CLASS_NAME , "overflow-hidden" ) } >
< div className = "border-b border-slate-200/70 px-3 py-3" >
< div className = "flex flex-wrap items-center justify-between gap-2" >
< SectionTitle icon = { Icon } title = { title } / >
< span className = { cn ( "rounded-full border px-2 py-0.5 text-xs font-semibold leading-4" , getTaskExecutionBadgeClassName ( tone ) ) } >
< span className = { cn ( "rounded-full border px-2 py-0.5 text-xs font-semibold leading-4" , getTaskExecutionBadgeClassName ( badgeTone ) ) } >
{ badge }
< / span >
< / div >
@@ -986,7 +1022,6 @@ function TaskExecutionPanel({
< TaskExecutionStepRow
key = { step . id }
step = { step }
tone = { tone }
isLast = { index === steps . length - 1 }
/ >
) ) }
@@ -998,11 +1033,9 @@ function TaskExecutionPanel({
function TaskExecutionStepRow ( {
step ,
tone ,
isLast
} : {
step : ExecutionStep ;
tone : DetailTone ;
isLast : boolean ;
} ) {
return (
@@ -1011,11 +1044,13 @@ function TaskExecutionStepRow({
{ ! isLast ? (
< span className = "absolute bottom-[-0.65rem] top-7 w-px bg-slate-200" aria-hidden = "true" / >
) : null }
< span className = { cn ( "relative z-10 grid h-7 w-7 place-items-center rounded-full ring-4 ring-white" , getTaskExecutionStepIconClassName ( step . status , tone ) ) } >
< span className = { cn ( "relative z-10 grid h-7 w-7 place-items-center rounded-full ring-4 ring-white" , getTaskExecutionStepIconClassName ( step . status ) ) } >
{ step . status === "done" ? (
< CheckCircle2 size = { 14 } aria-hidden = "true" / >
) : step . status === "current" ? (
< Loader2 size = { 14 } aria-hidden = "true" className = "animate-spin" / >
) : step . status === "confirmation" ? (
< AlertTriangle size = { 14 } aria-hidden = "true" / >
) : (
< Clock3 size = { 14 } aria-hidden = "true" / >
) }
@@ -1024,12 +1059,12 @@ function TaskExecutionStepRow({
< span
className = { cn (
"min-w-0 rounded-xl border px-2.5 py-2" ,
step . status === "current" ? getTaskExecutionCurrentStepClassName ( tone ) : "border-slate-200/70 bg-slate-50/80"
getTaskExecutionStepSurfaceClassName ( step . status )
) }
>
< span className = "flex min-w-0 flex-wrap items-center justify-between gap-2" >
< span className = "text-xs font-semibold text-slate-900" > { step . title } < / span >
< span className = { cn ( "rounded-full border px-1.5 py-0.5 text-xs font-semibold leading-4" , getTaskExecutionStepStatusClassName ( step . status , tone ) ) } >
< span className = { cn ( "rounded-full border px-1.5 py-0.5 text-xs font-semibold leading-4" , getTaskExecutionStepStatusClassName ( step . status ) ) } >
{ getTaskExecutionStepStatusLabel ( step . status ) }
< / span >
< / span >
@@ -1068,8 +1103,8 @@ function WorkOrderLifecyclePanel({ condition }: { condition: ScheduledWorkOrderI
icon = { ClipboardList }
title = "任务执行"
badge = { currentStage ? currentStage.title : condition.code }
badgeTone = { currentStage ? . id === "reply" ? "completed" : currentStage ? . id === "execution" ? "active" : "pending" }
summary = { condition . detail ? ? condition . summary }
tone = "blue"
meta = { [
{ label : "来源" , value : condition.source } ,
{ label : "位置" , value : condition.location } ,
@@ -1278,4 +1313,3 @@ function getConditionAnalysisInsight(condition: ScheduledConditionItem): Schedul
]
} ;
}