feat(workbench): refine dynamic task collaboration
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user