This commit is contained in:
Winnie
2026-08-05 17:46:05 +08:00
101 changed files with 8797 additions and 3323 deletions
+34 -145
View File
@@ -22,10 +22,12 @@ import {
} from "../../services/api";
import { useApi } from "../../context/AuthContext";
import Icon from "../../components/Icon";
import Icon from "../../components/common/Icon";
import { ScheduleList } from "./ScheduleList";
import { ArtifactList } from "./ArtifactList";
import { ScheduleCanvasHeader } from "./ScheduleCanvasHeader";
import "../../styles/schedule.css";
type Notice = {
tone: "success" | "error" | "info";
message: string;
@@ -222,8 +224,6 @@ function edgePath(
const curve = Math.max(70, Math.abs(x2 - x1) * 0.45);
return `M ${x1} ${y1} C ${x1 + curve} ${y1}, ${x2 - curve} ${y2}, ${x2} ${y2}`;
}
export default function SchedulePage({
onNotify,
onConnectionChange,
@@ -244,10 +244,8 @@ export default function SchedulePage({
const [busy, setBusy] = useState<string | null>(null);
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [newScheduleName, setNewScheduleName] = useState("");
const [contextMenu, setContextMenu] =
useState<ScheduleContextMenu | null>(null);
const [scheduleForm, setScheduleForm] =
useState<ScheduleForm>(EMPTY_SCHEDULE_FORM);
const [contextMenu, setContextMenu] = useState<ScheduleContextMenu | null>(null);
const [scheduleForm, setScheduleForm] = useState<ScheduleForm>(EMPTY_SCHEDULE_FORM);
const [nodeForm, setNodeForm] = useState<NodeForm>(EMPTY_NODE_FORM);
const [cronResult, setCronResult] = useState<CronPreview | null>(null);
const [runs, setRuns] = useState<ScheduleRunSummary[]>([]);
@@ -431,18 +429,14 @@ export default function SchedulePage({
};
}, [schedule?.schedule_id]);
const hasActiveRuns = runs.some(
(item) => item.run_status === "queued" || item.run_status === "running",
);
useEffect(() => {
const scheduleId = schedule?.schedule_id;
if (!scheduleId || !hasActiveRuns) return;
if (!scheduleId || !runs.some((item) => item.run_status === "queued" || item.run_status === "running")) return;
const timer = window.setInterval(() => {
void refreshRuns(scheduleId);
}, 1500);
return () => window.clearInterval(timer);
}, [schedule?.schedule_id, hasActiveRuns]);
}, [schedule?.schedule_id, runs]);
const handleError = async (
error: unknown,
@@ -561,7 +555,7 @@ export default function SchedulePage({
const selectedSchedule = target ?? schedule;
if (!selectedSchedule || busy) return;
setContextMenu(null);
if (!window.confirm(`确定删除调度${selectedSchedule.schedule_name}吗?`)) return;
if (!window.confirm(`确定删除调度"${selectedSchedule.schedule_name}"吗?`)) return;
setBusy("delete-schedule");
try {
await api.deleteSchedule(
@@ -621,7 +615,7 @@ export default function SchedulePage({
setContextMenu(null);
if (
!window.confirm(
`确定将${artifact.script_name} ${artifact.version_label}移出调度列表吗?\n`
`确定将"${artifact.script_name} ${artifact.version_label}"移出调度列表吗?\n`
+ "稳定版本本身和历史运行记录不会被删除。",
)
) return;
@@ -721,7 +715,7 @@ export default function SchedulePage({
if (positionDraftCount > 0) {
onNotify({
tone: "info",
message: "还有节点位置未保存,请先点击保存配置再运行",
message: "还有节点位置未保存,请先点击'保存配置'再运行",
});
return;
}
@@ -791,15 +785,6 @@ export default function SchedulePage({
}
};
const onArtifactDragStart = (
event: DragEvent<HTMLDivElement>,
artifact: ScheduleArtifact,
): void => {
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.setData(ARTIFACT_MIME, artifact.versions_id);
event.dataTransfer.setData("text/plain", artifact.versions_id);
};
const onCanvasDrop = (event: DragEvent<HTMLDivElement>): void => {
event.preventDefault();
const versionsId = event.dataTransfer.getData(ARTIFACT_MIME)
@@ -962,7 +947,7 @@ export default function SchedulePage({
const node = target ?? selectedNode;
if (!schedule || !node || busy) return;
setContextMenu(null);
if (!window.confirm(`确定删除节点${node.node_name}吗?`)) return;
if (!window.confirm(`确定删除节点"${node.node_name}"吗?`)) return;
const updated = await withMutation(
"delete-node",
() => api.deleteScheduleNode(
@@ -1094,124 +1079,30 @@ export default function SchedulePage({
<div className="schedule-workbench">
<aside className="schedule-left">
<section className="schedule-panel schedule-list-panel">
<div
className="schedule-panel__title"
onContextMenu={(event) => openContextMenu(event, { kind: "schedule-list" })}
>
<div><strong></strong><span>{schedules.length}</span></div>
</div>
<label className="schedule-search">
<Icon name="search" size={15} />
<input
value={scheduleKeyword}
placeholder="搜索调度名称"
onChange={(event) => setScheduleKeyword(event.target.value)}
/>
</label>
<div
className="schedule-list"
onContextMenu={(event) => openContextMenu(event, { kind: "schedule-list" })}
>
{loading ? (
<p className="schedule-empty"></p>
) : filteredSchedules.length ? (
filteredSchedules.map((item) => (
<button
className={`schedule-card${
schedule?.schedule_id === item.schedule_id
? " schedule-card--active"
: ""
}`}
key={item.schedule_id}
type="button"
onClick={() => void chooseSchedule(item.schedule_id)}
onContextMenu={(event) => openContextMenu(event, {
kind: "schedule",
schedule: item,
})}
>
<span className={`schedule-status${item.enabled ? " is-enabled" : ""}`} />
<span>
<strong>{item.schedule_name}</strong>
<small>
{item.node_count} · {item.enabled ? "已启用" : "未启用"}
</small>
</span>
</button>
))
) : (
<p className="schedule-empty"></p>
)}
</div>
</section>
<section className="schedule-panel schedule-artifacts-panel">
<div className="schedule-panel__title">
<div><strong></strong><span>{artifacts.length}</span></div>
</div>
<label className="schedule-search">
<Icon name="search" size={15} />
<input
value={artifactKeyword}
placeholder="搜索稳定版本"
onChange={(event) => setArtifactKeyword(event.target.value)}
/>
</label>
<p className="artifact-tip"> versions_id</p>
<div className="artifact-list">
{filteredArtifacts.length ? filteredArtifacts.map((artifact) => (
<div
className="artifact-card"
draggable
key={artifact.versions_id}
onDragStart={(event) => onArtifactDragStart(event, artifact)}
onContextMenu={(event) => openContextMenu(event, {
kind: "artifact",
artifact,
})}
onDoubleClick={() => void addArtifactAt(
artifact,
90 + (schedule?.nodes.length ?? 0) * 245,
130,
)}
>
<span className={`artifact-card__icon artifact-card__icon--${artifact.script_type}`}>
<Icon
name={artifact.script_type === "notebook" ? "notebook" : "python"}
size={17}
/>
</span>
<span>
<strong>{artifact.script_name}</strong>
<small>
{artifact.version_label} · {shortHash(artifact.content_hash)}
</small>
</span>
</div>
)) : (
<p className="schedule-empty">
</p>
)}
</div>
</section>
<ScheduleList
schedules={filteredSchedules}
selectedScheduleId={schedule?.schedule_id ?? null}
keyword={scheduleKeyword}
loading={loading}
onSelect={chooseSchedule}
onContextMenu={(event, item) => openContextMenu(event as ReactMouseEvent<HTMLElement | SVGElement>, item ? { kind: "schedule", schedule: item } : { kind: "schedule-list" })}
onKeywordChange={setScheduleKeyword}
/>
<ArtifactList
artifacts={filteredArtifacts}
keyword={artifactKeyword}
onDrop={(artifact, x, y) => void addArtifactAt(artifact, x, y)}
onDoubleClick={(artifact) => void addArtifactAt(artifact, 90 + (schedule?.nodes.length ?? 0) * 245, 130)}
onContextMenu={(event, artifact) => openContextMenu(event as ReactMouseEvent<HTMLElement | SVGElement>, { kind: "artifact", artifact })}
onKeywordChange={setArtifactKeyword}
/>
</aside>
<main className="schedule-center">
<div className="schedule-canvas-title">
<div>
<strong></strong>
<span>
线
</span>
</div>
{linkSourceId && (
<button type="button" onClick={() => setLinkSourceId(null)}>
线
</button>
)}
</div>
<ScheduleCanvasHeader
linkSourceId={linkSourceId}
onCancelLink={() => setLinkSourceId(null)}
/>
<div
className={`schedule-canvas${linkSourceId ? " is-linking" : ""}`}
ref={canvasRef}
@@ -1366,7 +1257,7 @@ export default function SchedulePage({
<div className="schedule-canvas-empty">
<Icon name="schedule" size={34} />
<strong></strong>
<p></p>
<p>"新建"</p>
<button type="button" onClick={openCreateScheduleDialog}>
<Icon name="plus" size={15} />
</button>
@@ -1579,8 +1470,6 @@ export default function SchedulePage({
</section>
);
}
function RunHistory({
runs,
nodes,