refactor(workbench): simplify appointment tasks

This commit is contained in:
2026-09-24 12:01:11 +08:00
parent 436d5500f1
commit ddf953b641
28 changed files with 443 additions and 1384 deletions
@@ -1,7 +1,7 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ScheduledConditionRecord, ScheduledWorkOrderItem } from "../types";
import { ConditionDetailPanel, WorkOrderSubtasksPanel } from "./scheduled-condition-detail-panel";
import { ConditionDetailPanel, WorkOrderActivityPanel } from "./scheduled-condition-detail-panel";
import { createVisibleTickerCards } from "./agent-task-ticker-utils";
import { createConditionConversationPrompt } from "../utils/scheduled-condition-prompts";
@@ -34,7 +34,7 @@ it("does not infer water-quality progress or conversation steps from elapsed tim
describe("scheduled condition backend boundaries", () => {
afterEach(cleanup);
it("keeps Chinese work-order time labels in the application font", () => {
it("shows a flat work order with its recorded activity", () => {
const workOrder: ScheduledWorkOrderItem = {
id: "demo-work-order",
kind: "work_order",
@@ -49,33 +49,17 @@ describe("scheduled condition backend boundaries", () => {
dispatcher: "调度岗",
priority: "normal",
dataSource: "mock",
subtasks: [{
id: "field-service",
title: "现场维修",
summary: "按预约窗口处理",
status: "pending",
scheduledAt: "2026-09-23T10:00:00Z",
location: "测试点",
assignee: "维修班组",
durationMinutes: 60,
replyWindowMinutes: 30,
stages: [{
id: "dispatch",
sequence: 1,
title: "派发",
status: "pending",
owner: "调度岗",
timeLabel: "待安排",
description: "确认预约窗口。"
}],
replyRequirements: ["回填处理结果"]
}]
location: "测试点",
assignee: "维修班组",
feedbackRequirements: ["回填处理结果"],
activity: [{ sequence: 1, occurredAt: "2026-09-23T09:00:00Z", actor: "调度岗",
kind: "created", title: "工单创建", detail: "确认预约窗口。" }]
};
render(<WorkOrderSubtasksPanel condition={workOrder} />);
const timeLabel = screen.getByText("待安排", { exact: true });
expect(timeLabel).toHaveClass("tabular-nums");
expect(timeLabel).not.toHaveClass("font-mono");
render(<WorkOrderActivityPanel condition={workOrder} />);
expect(screen.getByText("工单创建")).toBeVisible();
expect(screen.getByText("回填处理结果")).toBeVisible();
expect(screen.queryByText("子任务")).not.toBeInTheDocument();
});
it("shows only persisted evidence for a failed backend condition", () => {
@@ -54,10 +54,6 @@ import {
getStatusActivity,
getStatusTone,
getRiskLabel,
getWorkOrderCurrentSubtask,
getWorkOrderCurrentStage,
getWorkOrderStageActivity,
getWorkOrderStageTone,
statusLabels
} from "./scheduled-condition-feed-utils";
import { getConditionReportRiskLevel } from "./condition-report-risk";
@@ -125,25 +121,19 @@ export function StatusPill({
condition: ScheduledConditionItem;
className?: string;
}) {
const isWorkOrder = condition.kind === "work_order";
const isAgentEvent = condition.kind === "condition" && condition.taskId === "water-quality-complaint";
const currentStage = isWorkOrder ? getWorkOrderCurrentStage(condition) : null;
return (
<StatusBadge
tone={
isAgentEvent ? "neutral" : isWorkOrder
? getWorkOrderStageTone(currentStage?.status)
: getStatusTone(condition.status)
isAgentEvent ? "neutral" : getStatusTone(condition.status)
}
activity={
isAgentEvent ? "static" : isWorkOrder
? getWorkOrderStageActivity(currentStage?.status)
: getStatusActivity(condition.status)
isAgentEvent ? "static" : getStatusActivity(condition.status)
}
className={className}
>
{isAgentEvent ? "事件调查" : isWorkOrder ? currentStage?.title ?? "工单" : statusLabels[condition.status]}
{isAgentEvent ? "事件调查" : statusLabels[condition.status]}
</StatusBadge>
);
}
@@ -161,7 +151,7 @@ export function ConditionDetailPanel({
}: ConditionDetailPanelProps) {
const detailKey =
condition.kind === "work_order"
? `${condition.id}-${getWorkOrderCurrentSubtask(condition)?.id ?? "none"}-${getWorkOrderCurrentStage(condition)?.id ?? condition.status}`
? `${condition.id}-${condition.updatedAt}-${condition.status}`
: condition.id;
const content =
condition.kind === "work_order" ? (
@@ -226,7 +216,7 @@ function ConditionRecordDetailPanel({
const header = getConditionDetailHeader(condition);
const riskLevel = getConditionReportRiskLevel(condition.status, condition.riskLevel);
const bodyKey = `${condition.id}-${condition.status}-${condition.riskLevel}`;
const externalWorkOrder = condition.operationalTask?.work_order_snapshot?.subtasks
const externalWorkOrder = condition.operationalTask?.work_order_snapshot
? toScheduledWorkOrderSnapshot(condition.operationalTask.work_order_snapshot)
: null;
@@ -262,12 +252,12 @@ function ConditionRecordDetailPanel({
>
{condition.operationalTask ? (
<>
{externalWorkOrder ? <WorkOrderSubtasksPanel condition={externalWorkOrder} /> : null}
{externalWorkOrder ? <WorkOrderActivityPanel condition={externalWorkOrder} /> : null}
{condition.operationalTask.task_type === "external_work_order"
&& !condition.operationalTask.work_order_snapshot?.subtasks ? (
&& !condition.operationalTask.work_order_snapshot ? (
<DetailSectionCard>
<SectionTitle icon={ListChecks} title="预约过程" />
<p className="mt-2 text-xs leading-5 text-slate-500">后端尚未提供工单过程,不根据总状态推测阶段。</p>
<p className="mt-2 text-xs leading-5 text-slate-500">后端尚未提供工单快照,不根据任务状态推测现场进展。</p>
</DetailSectionCard>
) : null}
<AgentExecutionPlanPanel
@@ -456,9 +446,6 @@ function WorkOrderDetailPanel({
condition: ScheduledWorkOrderItem;
dataSource: WorkbenchWorkOrderDataSource;
}) {
const currentSubtask = getWorkOrderCurrentSubtask(condition);
const currentStage = getWorkOrderCurrentStage(condition);
return (
<DetailScroll>
<DetailShell>
@@ -471,17 +458,15 @@ function WorkOrderDetailPanel({
<SourceBoundaryNotice
title={dataSource === "backend" ? "后端工单记录" : "演示工单"}
description={dataSource === "backend"
? "工单的子任务、阶段、责任人和复令要求均来自后端持久化记录。"
? "工单状态、负责人、回执要求与活动记录均来自后端持久化记录。"
: "这条工单来自前端演示数据,不代表真实派发、执行或复令状态。"}
/>
<WorkOrderSubtasksPanel condition={condition} />
<WorkOrderActivityPanel condition={condition} />
<DecisionActionStrip
icon={CalendarClock}
title={currentSubtask && currentStage
? `${currentSubtask.title} · ${currentStage.title}`
: "工单流转"}
value={condition.recommendation ?? "按子任务及其阶段跟踪现场处置闭环。"}
actionLabel={currentStage?.id === "reply" ? "待复令" : "跟踪工单"}
title="工单跟踪"
value={condition.recommendation ?? "按工单活动记录跟踪现场处置。"}
actionLabel={condition.status === "awaiting_reply" ? "待复令" : "跟踪工单"}
actionDisabled
actionTitle="工单流程由调度和现场班组流转"
/>
@@ -1286,93 +1271,42 @@ function TaskExecutionStepRow({
);
}
export function WorkOrderSubtasksPanel({ condition }: { condition: ScheduledWorkOrderItem }) {
const currentSubtask = getWorkOrderCurrentSubtask(condition);
const completedCount = condition.subtasks.filter((subtask) =>
subtask.status === "completed" || subtask.status === "cancelled"
).length;
export function WorkOrderActivityPanel({ condition }: { condition: ScheduledWorkOrderItem }) {
return (
<DetailSectionCard>
<div className="flex flex-wrap items-center justify-between gap-2">
<SectionTitle icon={ListChecks} title="子任务" />
<StatusBadge tone={completedCount === condition.subtasks.length ? "success" : "info"}>
{completedCount}/{condition.subtasks.length} 完成
</StatusBadge>
<SectionTitle icon={ClipboardList} title="预约工单" />
<StatusBadge tone={getStatusTone(condition.status)}>{statusLabels[condition.status]}</StatusBadge>
</div>
<div className="mt-2 grid grid-cols-2 gap-1.5 sm:grid-cols-4">
<ReportMetaTile label="来源" value={condition.source} />
<ReportMetaTile label="派发人" value={condition.dispatcher} />
<ReportMetaTile label="负责人" value={condition.assignee ?? "待分配"} />
<ReportMetaTile label="优先级" value={condition.priority === "urgent" ? "紧急" : "普通"} />
<ReportMetaTile label="预计耗时" value={formatDuration(condition.durationMinutes, condition.status)} />
</div>
<div className="mt-2 grid gap-2">
{condition.subtasks.map((subtask, index) => {
const active = subtask.id === currentSubtask?.id;
const steps: ExecutionStep[] = subtask.stages.map((stage) => {
const awaitingReply = stage.id === "reply" && stage.status === "current";
return {
id: `${subtask.id}-${stage.id}`,
title: stage.title,
status: awaitingReply ? "confirmation" : stage.status,
statusLabel: awaitingReply ? "待复令" : undefined,
description: stage.description,
meta: [
{ label: "责任人", value: stage.owner },
{ label: "时间", value: stage.timeLabel }
]
};
});
return (
<Disclosure
key={subtask.id}
className="surface-control rounded-xl border border-slate-200/70 [&>summary]:px-3 [&>summary]:py-2.5"
initiallyOpen={active}
label={(
<span className="flex min-w-0 items-center justify-between gap-2">
<span className="min-w-0 truncate">{index + 1}. {subtask.title}</span>
<StatusBadge
tone={getWorkOrderSubtaskTone(subtask.status)}
activity={subtask.status === "running" ? "live" : "static"}
>
{getWorkOrderSubtaskStatusLabel(subtask.status)}
</StatusBadge>
</span>
)}
>
<div className="border-t border-slate-200/70 px-3 py-3">
<p className="text-xs leading-5 text-slate-700">{subtask.summary}</p>
<div className="mt-2 grid gap-1 text-xs leading-5 text-slate-600 sm:grid-cols-2">
<span><b className="text-slate-400">位置:</b>{subtask.location}</span>
<span><b className="text-slate-400">班组:</b>{subtask.assignee}</span>
<span><b className="text-slate-400">窗口:</b><span className="font-mono">{formatTimeRange(subtask.scheduledAt, subtask.durationMinutes)}</span></span>
<span><b className="text-slate-400">复令:</b>完成后 {subtask.replyWindowMinutes} 分钟内</span>
</div>
<ol className="mt-3 grid gap-2">
{steps.map((step, stepIndex) => (
<TaskExecutionStepRow
key={step.id}
step={step}
isLast={stepIndex === steps.length - 1}
/>
))}
</ol>
{subtask.replyRequirements.length > 0 ? (
<ul className="mt-3 grid gap-1.5 border-t border-slate-200/70 pt-3">
{subtask.replyRequirements.map((item) => (
<li key={item} className="flex gap-2 text-xs leading-5 text-slate-700">
<CheckCircle2 className="mt-0.5 shrink-0 text-blue-600" size={14} />
<span>{item}</span>
</li>
))}
</ul>
) : null}
</div>
</Disclosure>
);
})}
<ReportMetaTile label="预约时间" value={formatTime(condition.scheduledAt)} />
</div>
{condition.location ? <p className="mt-3 text-xs text-slate-600">位置:{condition.location}</p> : null}
{condition.feedbackRequirements.length ? (
<section className="mt-3 border-t border-slate-200/70 pt-3">
<h4 className="text-xs font-semibold text-slate-700">回执要求</h4>
<ul className="mt-2 grid gap-1.5">
{condition.feedbackRequirements.map((item, index) => <li key={`${index}-${item}`} className="flex gap-2 text-xs leading-5 text-slate-700"><CheckCircle2 className="mt-0.5 shrink-0 text-blue-600" size={14} />{item}</li>)}
</ul>
</section>
) : null}
<section className="mt-3 border-t border-slate-200/70 pt-3">
<h4 className="text-xs font-semibold text-slate-700">活动记录</h4>
{condition.activity.length ? (
<ol className="mt-2 border-l border-blue-200 pl-4">
{[...condition.activity].sort((a, b) => b.sequence - a.sequence).map((entry) => (
<li key={entry.sequence} className="relative pb-4 last:pb-0 before:absolute before:-left-[21px] before:top-1 before:h-2.5 before:w-2.5 before:rounded-full before:bg-blue-500">
<div className="flex flex-wrap items-baseline justify-between gap-1"><span className="text-xs font-semibold text-slate-800">{entry.title}</span><time className="text-[10px] text-slate-500">{formatTime(entry.occurredAt)}</time></div>
<p className="mt-1 text-xs leading-5 text-slate-600">{entry.detail}</p>
<p className="mt-1 text-[10px] text-slate-400">{entry.actor}</p>
</li>
))}
</ol>
) : <p className="mt-2 text-xs text-slate-500">暂无活动记录;不根据工单状态推测处置过程。</p>}
</section>
</DetailSectionCard>
);
}
@@ -1396,53 +1330,20 @@ function toScheduledWorkOrderSnapshot(snapshot: ExternalWorkOrderSnapshot): Sche
: "pending",
riskLevel: snapshot.priority === "urgent" || snapshot.priority === "high" ? "attention" : "normal",
updatedAt: Date.parse(snapshot.updated_at),
durationMinutes: snapshot.subtasks?.reduce((total, item) => total + item.duration_minutes, 0),
detail: snapshot.summary,
source: "外围工单系统",
dispatcher: "外围系统同步",
priority: snapshot.priority === "urgent" || snapshot.priority === "high" ? "urgent" : "normal",
dataSource: "backend",
subtasks: (snapshot.subtasks ?? []).map((subtask) => ({
id: subtask.id,
title: subtask.title,
summary: subtask.summary,
status: subtask.status,
scheduledAt: subtask.scheduled_at,
location: subtask.location,
assignee: subtask.assignee,
durationMinutes: subtask.duration_minutes,
replyWindowMinutes: subtask.reply_window_minutes,
stages: subtask.stages.map((stage) => ({
id: stage.id,
sequence: stage.sequence,
title: stage.title,
status: stage.status,
owner: stage.owner,
timeLabel: stage.time_label,
description: stage.description
})),
replyRequirements: subtask.reply_requirements
}))
location: snapshot.location ?? undefined,
assignee: snapshot.assignee ?? undefined,
feedbackRequirements: snapshot.feedback_requirements,
activity: snapshot.activity.map((entry) => ({ sequence: entry.sequence,
occurredAt: entry.occurred_at, actor: entry.actor, kind: entry.kind,
title: entry.title, detail: entry.detail }))
};
}
function getWorkOrderSubtaskTone(status: ScheduledWorkOrderItem["subtasks"][number]["status"]): StatusTone {
if (status === "completed") return "success";
if (status === "running") return "info";
if (status === "blocked") return "danger";
return "neutral";
}
function getWorkOrderSubtaskStatusLabel(status: ScheduledWorkOrderItem["subtasks"][number]["status"]) {
return {
pending: "待处理",
running: "处理中",
blocked: "受阻",
completed: "已完成",
cancelled: "已取消"
}[status];
}
function RunningEvidencePreview({ items }: { items: string[] }) {
return (
<DetailSectionCard>
@@ -1,12 +1,11 @@
import { describe, expect, it } from "vitest";
import {
groupScheduledConditions,
getStatusActivity,
getStatusTone,
getWorkOrderStageActivity,
getWorkOrderStageTone,
getWorkOrderCurrentSubtask
sortConditionsNewestFirst
} from "./scheduled-condition-feed-utils";
import type { ScheduledWorkOrderItem } from "../types";
import type { ScheduledConditionItem, ScheduledWorkOrderItem } from "../types";
describe("scheduled condition status presentation", () => {
it("maps lifecycle states to the shared semantic language", () => {
@@ -29,74 +28,73 @@ describe("scheduled condition status presentation", () => {
expect(getStatusActivity("awaiting_reply")).toBe("static");
});
it("maps work-order stage states to the shared semantic language", () => {
expect({
done: getWorkOrderStageTone("done"),
current: getWorkOrderStageTone("current"),
pending: getWorkOrderStageTone("pending"),
blocked: getWorkOrderStageTone("blocked"),
skipped: getWorkOrderStageTone("skipped")
}).toEqual({
done: "success",
current: "info",
pending: "neutral",
blocked: "danger",
skipped: "neutral"
});
expect(getWorkOrderStageActivity("current")).toBe("loading");
});
it("sorts mixed task representations by their full scheduled timestamp", () => {
const conditions = [
createCondition("older-condition", "condition", "2026-09-17T10:00:00+08:00"),
createCondition("newer-work-order", "work_order", "2026-09-18T09:00:00+08:00"),
createCondition("newest-condition", "condition", "2026-09-18T11:00:00+08:00")
];
it("prefers an executing subtask over a pending subtask at its dispatch stage", () => {
const workOrder = {
id: "work-order-1",
kind: "work_order",
code: "WO-1",
scheduledAt: "2026-09-17T10:00:00+08:00",
title: "多子任务工单",
summary: "测试当前子任务选择。",
status: "running",
riskLevel: "normal",
updatedAt: 1,
durationMinutes: 30,
source: "测试",
dispatcher: "调度中心",
priority: "normal",
subtasks: [
createSubtask("pending-subtask", "pending", "dispatch"),
createSubtask("running-subtask", "running", "execution")
]
} satisfies ScheduledWorkOrderItem;
expect(getWorkOrderCurrentSubtask(workOrder)?.id).toBe("running-subtask");
expect(sortConditionsNewestFirst(conditions).map((condition) => condition.id)).toEqual([
"newest-condition",
"newer-work-order",
"older-condition"
]);
const grouped = groupScheduledConditions(conditions);
expect(grouped.recentConditions.map((condition) => condition.id)).toEqual([
"newest-condition",
"older-condition"
]);
expect(grouped.futureWorkOrders.map((condition) => condition.id)).toEqual([
"newer-work-order"
]);
});
});
function createSubtask(
function createCondition(
id: string,
status: "pending" | "running",
currentStage: "dispatch" | "execution"
) {
kind: ScheduledConditionItem["kind"],
scheduledAt: string
): ScheduledConditionItem {
if (kind === "work_order") {
return {
...createWorkOrder(id),
scheduledAt
};
}
return {
id,
kind: "condition",
taskId: "scada-diagnosis",
sessionId: `session-${id}`,
title: id,
summary: id,
status,
status: "completed",
riskLevel: "normal",
scheduledAt,
updatedAt: Date.parse(scheduledAt)
};
}
function createWorkOrder(id: string): ScheduledWorkOrderItem {
return {
id,
kind: "work_order",
code: id,
scheduledAt: "2026-09-17T10:00:00+08:00",
location: "演示点位",
assignee: "演示班组",
durationMinutes: 15,
replyWindowMinutes: 10,
stages: [
{
id: currentStage,
sequence: 1,
title: currentStage,
status: "current" as const,
owner: "演示班组",
timeLabel: "10:00",
description: "当前阶段"
}
],
replyRequirements: []
title: id,
summary: id,
status: "pending",
riskLevel: "normal",
updatedAt: 1,
durationMinutes: 30,
source: "测试",
dispatcher: "调度中心",
priority: "normal",
location: "测试点",
assignee: "测试班组",
feedbackRequirements: [],
activity: []
};
}
@@ -8,9 +8,7 @@ import {
import type { StatusActivity, StatusTone } from "@/shared/ui/status";
import type {
ScheduledConditionItem,
ScheduledConditionItemStatus,
ScheduledWorkOrderItem,
ScheduledWorkOrderStage
ScheduledConditionItemStatus
} from "../types";
export const CONDITION_FILTER_ALL = "__all__";
@@ -55,22 +53,6 @@ const statusIcons: Record<ScheduledConditionItemStatus, typeof CheckCircle2> = {
pending: Clock3
};
const workOrderStageTones = {
done: "success",
current: "info",
pending: "neutral",
blocked: "danger",
skipped: "neutral"
} satisfies Record<ScheduledWorkOrderStage["status"], StatusTone>;
const workOrderStageActivities = {
done: "static",
current: "loading",
pending: "static",
blocked: "static",
skipped: "static"
} satisfies Record<ScheduledWorkOrderStage["status"], StatusActivity>;
const conditionStatusOrder: ScheduledConditionItemStatus[] = [
"running",
"awaiting_reply",
@@ -96,14 +78,14 @@ export type ConditionStatusFilterOption = {
count: number;
};
export function groupScheduledConditions(conditions: ScheduledConditionItem[]) {
const recentConditions = conditions
.filter((condition) => condition.kind === "condition")
.sort((a, b) => getConditionTime(b) - getConditionTime(a));
export function sortConditionsNewestFirst(conditions: ScheduledConditionItem[]) {
return [...conditions].sort((a, b) => getConditionTime(b) - getConditionTime(a));
}
const futureWorkOrders = conditions
.filter((condition) => condition.kind === "work_order")
.sort((a, b) => getConditionTime(a) - getConditionTime(b));
export function groupScheduledConditions(conditions: ScheduledConditionItem[]) {
const newestFirst = sortConditionsNewestFirst(conditions);
const recentConditions = newestFirst.filter((condition) => condition.kind === "condition");
const futureWorkOrders = newestFirst.filter((condition) => condition.kind === "work_order");
return { recentConditions, futureWorkOrders };
}
@@ -178,38 +160,6 @@ export function getRiskLabel(riskLevel: ScheduledConditionItem["riskLevel"]) {
return "正常";
}
export function getWorkOrderCurrentSubtask(condition: ScheduledWorkOrderItem) {
return (
condition.subtasks.find((subtask) => subtask.status === "running") ??
condition.subtasks.find((subtask) => subtask.status === "blocked") ??
condition.subtasks.find((subtask) =>
subtask.stages.some((stage) => stage.status === "current")
) ??
condition.subtasks.find((subtask) => subtask.status === "pending") ??
condition.subtasks.at(-1) ??
null
);
}
export function getWorkOrderCurrentStage(condition: ScheduledWorkOrderItem) {
const subtask = getWorkOrderCurrentSubtask(condition);
return (
subtask?.stages.find((stage) => stage.status === "current") ??
subtask?.stages.find((stage) => stage.status === "blocked") ??
subtask?.stages.find((stage) => stage.status === "pending") ??
subtask?.stages.at(-1) ??
null
);
}
export function getWorkOrderStageTone(status?: ScheduledWorkOrderStage["status"]) {
return status ? workOrderStageTones[status] : "neutral";
}
export function getWorkOrderStageActivity(status?: ScheduledWorkOrderStage["status"]) {
return status ? workOrderStageActivities[status] : "static";
}
export function getStatusIcon(status: ScheduledConditionItemStatus) {
return statusIcons[status];
}
@@ -46,14 +46,11 @@ import {
createStatusFilterOptions,
createTaskFilterOptions,
formatTime,
getConditionTime,
getStatusActivity,
getStatusIcon,
getStatusTone,
getWorkOrderCurrentStage,
getWorkOrderStageActivity,
getWorkOrderStageTone,
groupScheduledConditions,
sortConditionsNewestFirst,
type ConditionStatusFilterOption,
type ConditionStatusFilterValue,
type ConditionTaskFilterOption,
@@ -171,10 +168,6 @@ export function ScheduledConditionFeed({
() => groupScheduledConditions(categoryConditions),
[categoryConditions]
);
const allTimelineConditions = useMemo(
() => [...allRecentConditions, ...allFutureWorkOrders],
[allFutureWorkOrders, allRecentConditions]
);
const taskFilterOptions = useMemo(
() => createTaskFilterOptions(categoryConditions),
[categoryConditions]
@@ -203,8 +196,10 @@ export function ScheduledConditionFeed({
[filteredConditions]
);
const timelineConditions = useMemo(
() => [...recentConditions, ...futureWorkOrders],
[futureWorkOrders, recentConditions]
() => taskCategory === "appointment"
? sortConditionsNewestFirst([...recentConditions, ...futureWorkOrders])
: [...recentConditions, ...futureWorkOrders],
[futureWorkOrders, recentConditions, taskCategory]
);
const filterActive = taskFilter !== CONDITION_FILTER_ALL || statusFilter !== CONDITION_FILTER_ALL;
const totalConditionCount = allRecentConditions.length + allFutureWorkOrders.length;
@@ -214,8 +209,7 @@ export function ScheduledConditionFeed({
: `定时 ${conditionsByCategory.scheduled.length} · 调查 ${conditionsByCategory.investigation.length} · 预约 ${conditionsByCategory.appointment.length}`;
const selectedCondition =
timelineConditions.find((condition) => condition.id === selectedConditionId) ??
recentConditions[0] ??
futureWorkOrders[0] ??
timelineConditions[0] ??
null;
const focusConditionId = focusRequest?.conditionId;
const focusRequestId = focusRequest?.requestId;
@@ -294,7 +288,7 @@ export function ScheduledConditionFeed({
function handleToggleExpanded() {
updateExpanded(!expanded);
updateSelectedConditionId(
selectedConditionId ?? recentConditions[0]?.id ?? futureWorkOrders[0]?.id ?? null
selectedConditionId ?? timelineConditions[0]?.id ?? null
);
}
@@ -303,7 +297,9 @@ export function ScheduledConditionFeed({
if (nextCategory !== "scheduled") setScheduleOpen(false);
setTaskFilter(CONDITION_FILTER_ALL);
setStatusFilter(CONDITION_FILTER_ALL);
updateSelectedConditionId(conditionsByCategory[nextCategory][0]?.id ?? null);
updateSelectedConditionId(
sortConditionsNewestFirst(conditionsByCategory[nextCategory])[0]?.id ?? null
);
}
function handleToggleSchedule() {
@@ -562,19 +558,21 @@ export function ScheduledConditionFeed({
<ConditionTimelinePanel
expanded={expanded}
presentation={presentation}
loading={activeLoading}
emptyTitle={
backendError && taskCategory !== "scheduled"
? `${WORKBENCH_TASK_CATEGORIES.find((item) => item.id === taskCategory)?.label ?? "后端任务"}暂不可用`
: undefined
}
emptyDescription={
backendError && taskCategory !== "scheduled"
? backendError
: undefined
}
taskCategory={taskCategory}
loading={activeLoading}
emptyTitle={
backendError && taskCategory !== "scheduled"
? `${WORKBENCH_TASK_CATEGORIES.find((item) => item.id === taskCategory)?.label ?? "后端任务"}暂不可用`
: undefined
}
emptyDescription={
backendError && taskCategory !== "scheduled"
? backendError
: undefined
}
recentConditions={recentConditions}
futureWorkOrders={futureWorkOrders}
timelineConditions={timelineConditions}
taskFilter={taskFilter}
statusFilter={statusFilter}
taskFilterOptions={taskFilterOptions}
@@ -648,9 +646,11 @@ function formatLocalDateTimeInput(value: Date) {
type ConditionTimelinePanelProps = {
expanded: boolean;
presentation: ScheduledConditionFeedPresentation;
taskCategory: WorkbenchTaskCategory;
loading: boolean;
recentConditions: ScheduledConditionItem[];
futureWorkOrders: ScheduledConditionItem[];
timelineConditions: ScheduledConditionItem[];
taskFilter: ConditionTaskFilterValue;
statusFilter: ConditionStatusFilterValue;
taskFilterOptions: ConditionTaskFilterOption[];
@@ -670,9 +670,11 @@ type ConditionTimelinePanelProps = {
function ConditionTimelinePanel({
expanded,
presentation,
taskCategory,
loading,
recentConditions,
futureWorkOrders,
timelineConditions,
taskFilter,
statusFilter,
taskFilterOptions,
@@ -689,6 +691,7 @@ function ConditionTimelinePanel({
onSelectCondition
}: ConditionTimelinePanelProps) {
const filterActive = taskFilter !== CONDITION_FILTER_ALL || statusFilter !== CONDITION_FILTER_ALL;
const mainConditions = taskCategory === "appointment" ? timelineConditions : recentConditions;
return (
<section
@@ -718,7 +721,10 @@ function ConditionTimelinePanel({
onResetFilters={onResetFilters}
/>
) : null}
{(expanded || presentation === "mobile-sheet") && !loading && futureWorkOrders.length > 0 ? (
{taskCategory !== "appointment"
&& (expanded || presentation === "mobile-sheet")
&& !loading
&& futureWorkOrders.length > 0 ? (
<section className="mb-2">
<ConditionGroupHeader
icon={ClipboardList}
@@ -736,16 +742,16 @@ function ConditionTimelinePanel({
</section>
) : null}
<ConditionGroupHeader
icon={History}
icon={taskCategory === "appointment" ? ClipboardList : History}
title="最近工况"
count={loading ? 0 : recentConditions.length}
count={loading ? 0 : mainConditions.length}
/>
<div className="scheduled-feed-scroll -mr-2 min-h-0 flex-1 overflow-y-auto pb-4 pl-0.5 pr-2 pt-0.5">
{loading ? (
<ConditionTimelineSkeleton rows={expanded ? 7 : 6} />
) : recentConditions.length > 0 ? (
) : mainConditions.length > 0 ? (
<ConditionRows
conditions={recentConditions}
conditions={mainConditions}
selectedConditionId={selectedConditionId}
onSelectCondition={onSelectCondition}
/>
@@ -1078,18 +1084,8 @@ function ConditionTimelineRow({
onSelect
}: ConditionTimelineRowProps) {
const Icon = getStatusIcon(condition.status);
const isWorkOrder = condition.kind === "work_order";
const workOrderStage = isWorkOrder ? getWorkOrderCurrentStage(condition)?.status : undefined;
const tone = isWorkOrder
? condition.status === "running"
? getWorkOrderStageTone(workOrderStage)
: getStatusTone(condition.status)
: getStatusTone(condition.status);
const activity = isWorkOrder
? condition.status === "running"
? getWorkOrderStageActivity(workOrderStage)
: getStatusActivity(condition.status)
: getStatusActivity(condition.status);
const tone = getStatusTone(condition.status);
const activity = getStatusActivity(condition.status);
return (
<button
@@ -16,7 +16,7 @@ export const WORKBENCH_TASK_CATEGORIES = [
{
id: "appointment",
label: "预约任务",
sourceLabel: "后端 / 演示"
sourceLabel: "后端"
}
] as const satisfies readonly {
id: WorkbenchTaskCategory;