feat(workbench): refine dynamic task collaboration

This commit is contained in:
2026-09-24 16:36:12 +08:00
parent b64d2b78bb
commit 87cc7785da
25 changed files with 1216 additions and 470 deletions
@@ -1,6 +1,5 @@
import {
CalendarClock,
CalendarPlus,
ChevronDown,
ChevronRight,
ClipboardList,
@@ -30,11 +29,6 @@ import {
DropdownMenuTrigger
} from "@/shared/ui/dropdown-menu";
import { StatusIcon as SemanticStatusIcon } from "@/shared/ui/status";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { Textarea } from "@/shared/ui/textarea";
import { DateTimeField } from "@/shared/ui/date-time-field";
import { FieldSelect } from "@/shared/ui/field-select";
import { ConditionDetailPanel, StatusPill } from "./scheduled-condition-detail-panel";
import {
WORKBENCH_TASK_CATEGORIES,
@@ -69,14 +63,6 @@ export type ScheduledConditionFeedPresentation =
| "desktop-floating"
| "mobile-sheet";
export type ScheduleCreateRequest = {
scheduleKey: string;
title: string;
summary: string;
intervalSeconds: number;
nextRunAt: string;
};
type ScheduledConditionFeedProps = {
presentation?: ScheduledConditionFeedPresentation;
conditions?: ScheduledConditionItem[];
@@ -99,11 +85,13 @@ type ScheduledConditionFeedProps = {
onOpenTaskCollaboration?: (taskId: string) => Promise<void>;
onTaskDecision?: (taskId: string, actionId: string, expectedVersion: number, decision: OperationalTaskDecision, comment?: string) => Promise<unknown>;
onTaskExecute?: (taskId: string, actionId: string, expectedVersion: number) => Promise<unknown>;
onTaskDraftUpdate?: (taskId: string, actionId: string, expectedVersion: number, payload: Record<string, unknown>) => Promise<unknown>;
onTaskConfirmDispatch?: (taskId: string, actionId: string, expectedVersion: number) => Promise<unknown>;
onTaskAccessDecision?: (taskId: string, accessRequestId: string, expectedVersion: number, decision: "approved" | "rejected", scopes: string[]) => Promise<unknown>;
onTaskReconcile?: (taskId: string, actionId: string, expectedVersion: number) => Promise<unknown>;
onLoadTaskEvidenceSnapshot?: (taskId: string, evidenceId: string) => Promise<OperationalTaskDataSnapshot>;
onTaskRefresh?: () => void;
onRefresh?: () => void | Promise<void>;
onCreateSchedule?: (input: ScheduleCreateRequest) => Promise<unknown>;
};
type ScheduledConditionFeedFocusRequest = {
@@ -133,11 +121,13 @@ export function ScheduledConditionFeed({
onOpenTaskCollaboration,
onTaskDecision,
onTaskExecute,
onTaskDraftUpdate,
onTaskConfirmDispatch,
onTaskAccessDecision,
onTaskReconcile,
onLoadTaskEvidenceSnapshot,
onTaskRefresh,
onRefresh,
onCreateSchedule
onRefresh
}: ScheduledConditionFeedProps) {
const [uncontrolledExpanded, setUncontrolledExpanded] = useState(false);
const [uncontrolledSelectedConditionId, setUncontrolledSelectedConditionId] = useState<
@@ -147,15 +137,6 @@ export function ScheduledConditionFeed({
const [statusFilter, setStatusFilter] =
useState<ConditionStatusFilterValue>(CONDITION_FILTER_ALL);
const [taskCategory, setTaskCategory] = useState<WorkbenchTaskCategory>("scheduled");
const [scheduleOpen, setScheduleOpen] = useState(false);
const [scheduleTitle, setScheduleTitle] = useState("");
const [scheduleKey, setScheduleKey] = useState("scada-diagnosis");
const [scheduleSummary, setScheduleSummary] = useState("");
const [scheduleIntervalMinutes, setScheduleIntervalMinutes] = useState("15");
const [scheduleIntervalPreset, setScheduleIntervalPreset] = useState("15");
const [scheduleNextRunAt, setScheduleNextRunAt] = useState(() => formatLocalDateTimeInput(new Date(Date.now() + 15 * 60_000)));
const [scheduleSubmitting, setScheduleSubmitting] = useState(false);
const [scheduleError, setScheduleError] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
const refreshingRef = useRef(false);
const mobileSheet = presentation === "mobile-sheet";
@@ -300,7 +281,6 @@ export function ScheduledConditionFeed({
function handleTaskCategoryChange(nextCategory: WorkbenchTaskCategory) {
setTaskCategory(nextCategory);
if (nextCategory !== "scheduled") setScheduleOpen(false);
setTaskFilter(CONDITION_FILTER_ALL);
setStatusFilter(CONDITION_FILTER_ALL);
updateSelectedConditionId(
@@ -308,34 +288,6 @@ export function ScheduledConditionFeed({
);
}
function handleToggleSchedule() {
if (!scheduleOpen) {
setTaskCategory("scheduled");
updateExpanded(true);
}
setScheduleOpen((current) => !current);
}
async function handleCreateSchedule() {
const title = scheduleTitle.trim();
const summary = scheduleSummary.trim();
const key = scheduleKey.trim();
const intervalMinutes = Number(scheduleIntervalMinutes);
const nextRunAt = new Date(scheduleNextRunAt);
if (!title || !summary || !key) { setScheduleError("请填写名称、流程和任务说明。"); return; }
if (!Number.isInteger(intervalMinutes) || intervalMinutes < 1) { setScheduleError("执行周期不能少于 1 分钟。"); return; }
if (Number.isNaN(nextRunAt.getTime())) { setScheduleError("请选择有效的下次执行时间。"); return; }
if (!onCreateSchedule) { setScheduleError("当前未连接定时流程配置接口。"); return; }
setScheduleSubmitting(true); setScheduleError(null);
try {
await onCreateSchedule({ scheduleKey: key, title, summary, intervalSeconds: intervalMinutes * 60, nextRunAt: nextRunAt.toISOString() });
setScheduleOpen(false); setScheduleTitle(""); setScheduleSummary("");
showMapNotice({ tone: "success", title: "定时任务已添加", message: "仅保存在当前演示工作台,不会提交后端调度。" });
} catch (error) {
setScheduleError(error instanceof Error ? error.message : "定时流程创建失败。");
} finally { setScheduleSubmitting(false); }
}
async function handleRefresh() {
if (!onRefresh || refreshingRef.current) return;
refreshingRef.current = true;
@@ -474,17 +426,6 @@ export function ScheduledConditionFeed({
<h2 className="truncate text-sm font-semibold leading-tight text-slate-950">工况任务</h2>
<p className="mt-0.5 truncate text-xs text-slate-500">{headerSummary}</p>
</div>
{onCreateSchedule ? (
<button
type="button"
aria-label="配置定时流程"
aria-expanded={scheduleOpen}
onClick={handleToggleSchedule}
className="grid h-10 w-10 shrink-0 place-items-center rounded-lg text-slate-500 transition-colors active:scale-95 [@media(hover:hover)]:hover:bg-blue-50 [@media(hover:hover)]:hover:text-blue-700"
>
<CalendarPlus size={17} aria-hidden="true" />
</button>
) : null}
{onRefresh ? (
<button
type="button"
@@ -542,26 +483,6 @@ export function ScheduledConditionFeed({
/>
) : null}
{scheduleOpen ? (
<section aria-label="配置定时流程" className={cn("mt-2 p-3", CONDITION_CONTENT_SURFACE_CLASS_NAME)}>
<label className="block text-xs font-medium text-slate-600">定时流程名称<Input className="mt-1 h-9" placeholder="例如夜间低压巡检" value={scheduleTitle} onChange={(event) => setScheduleTitle(event.target.value)} /></label>
<div className="mt-2"><FieldSelect label="执行流程" value={scheduleKey} onValueChange={setScheduleKey} options={[{ value: "scada-diagnosis", label: "SCADA 数据诊断" }, { value: "smart-dispatch", label: "管网智能调度" }, { value: "network-simulation", label: "管网定时模拟" }, { value: "pump-energy", label: "泵站能耗检查" }]} /></div>
<label className="mt-2 block text-xs font-medium text-slate-600">任务说明<Textarea className="mt-1 min-h-16" placeholder="说明本流程核查什么以及预期输出" value={scheduleSummary} onChange={(event) => setScheduleSummary(event.target.value)} /></label>
<div className="mt-2 grid gap-2 sm:grid-cols-2">
<div>
<FieldSelect label="执行周期" value={scheduleIntervalPreset} onValueChange={(value) => { setScheduleIntervalPreset(value); if (value !== "custom") setScheduleIntervalMinutes(value); }} options={[{ value: "5", label: "每 5 分钟" }, { value: "15", label: "每 15 分钟" }, { value: "30", label: "每 30 分钟" }, { value: "60", label: "每 60 分钟" }, { value: "custom", label: "自定义周期" }]} />
{scheduleIntervalPreset === "custom" ? <label className="mt-2 block text-xs font-medium text-slate-600">自定义分钟数<Input className="mt-1 h-9" inputMode="numeric" value={scheduleIntervalMinutes} onChange={(event) => setScheduleIntervalMinutes(event.target.value.replace(/\D/g, ""))} /></label> : null}
</div>
<DateTimeField label="下次执行时间" value={scheduleNextRunAt} onChange={setScheduleNextRunAt} />
</div>
{scheduleError ? <p role="alert" className="mt-2 text-xs leading-5 text-red-700">{scheduleError}</p> : null}
<div className="mt-2 flex justify-end gap-2">
<Button type="button" size="sm" variant="outline" disabled={scheduleSubmitting} onClick={() => setScheduleOpen(false)}>取消</Button>
<Button type="button" size="sm" disabled={scheduleSubmitting} onClick={() => void handleCreateSchedule()}>{scheduleSubmitting ? "创建中" : "创建定时流程"}</Button>
</div>
</section>
) : null}
<div
className={cn(
"scheduled-feed-layout mt-3 grid min-h-0",
@@ -595,6 +516,9 @@ export function ScheduledConditionFeed({
onContinueConversation={() => handleContinueConversation(selectedCondition)}
onTaskDecision={onTaskDecision}
onTaskExecute={onTaskExecute}
onTaskDraftUpdate={onTaskDraftUpdate}
onTaskConfirmDispatch={onTaskConfirmDispatch}
onTaskAccessDecision={onTaskAccessDecision}
onTaskReconcile={onTaskReconcile}
onLoadTaskEvidenceSnapshot={onLoadTaskEvidenceSnapshot}
onTaskRefresh={onTaskRefresh}
@@ -690,11 +614,6 @@ function TaskCategorySwitch({
);
}
function formatLocalDateTimeInput(value: Date) {
const local = new Date(value.getTime() - value.getTimezoneOffset() * 60_000);
return local.toISOString().slice(0, 16);
}
type ConditionTimelinePanelProps = {
expanded: boolean;
presentation: ScheduledConditionFeedPresentation;