diff --git a/web/src/app/(main)/function/schedules/page.tsx b/web/src/app/(main)/function/schedules/page.tsx index 6b9a3ce..49ff3c9 100644 --- a/web/src/app/(main)/function/schedules/page.tsx +++ b/web/src/app/(main)/function/schedules/page.tsx @@ -444,6 +444,7 @@ export default function SchedulesPage() { const [query, setQuery] = React.useState(""); const [page, setPage] = React.useState(1); const [pageSize, setPageSize] = React.useState("10"); + const [selectedIds, setSelectedIds] = React.useState([]); const [expandedId, setExpandedId] = React.useState(null); const [timelineById, setTimelineById] = React.useState>({}); const [timelineLoadingId, setTimelineLoadingId] = React.useState(null); @@ -475,6 +476,13 @@ export default function SchedulesPage() { const size = Number(pageSize); const totalPages = Math.max(1, Math.ceil(filteredItems.length / size)); const visibleItems = filteredItems.slice((page - 1) * size, page * size); + const visibleIds = visibleItems.map((item) => item.id); + const selectedVisibleCount = visibleIds.filter((id) => selectedIds.includes(id)).length; + const allVisibleSelected = visibleItems.length > 0 && selectedVisibleCount === visibleItems.length; + const someVisibleSelected = selectedVisibleCount > 0 && !allVisibleSelected; + let headerSelectionState: boolean | "indeterminate" = false; + if (allVisibleSelected) headerSelectionState = true; + else if (someVisibleSelected) headerSelectionState = "indeterminate"; React.useEffect(() => { setPage((current) => Math.min(current, totalPages)); }, [totalPages]); @@ -486,6 +494,18 @@ export default function SchedulesPage() { setPageSize(value); setPage(1); }; + const toggleSelected = (id: number, checked: boolean) => { + setSelectedIds((current) => { + if (!checked) return current.filter((selected) => selected !== id); + return current.includes(id) ? current : [...current, id]; + }); + }; + const toggleAllVisible = (checked: boolean) => { + setSelectedIds((current) => { + if (checked) return Array.from(new Set([...current, ...visibleIds])); + return current.filter((id) => !visibleIds.includes(id)); + }); + }; const toggleTimeline = async (id: number) => { if (expandedId === id) { setExpandedId(null); @@ -553,6 +573,38 @@ export default function SchedulesPage() { toast.error((e as Error).message); } }; + const bulkRun = async () => { + const ids = [...selectedIds]; + if (!ids.length) return; + let started = 0; + const errors: string[] = []; + const results = await Promise.allSettled(ids.map((id) => api.runScheduleNow(id))); + results.forEach((result, index) => { + if (result.status === "rejected") { + errors.push(`计划 #${ids[index]}:${(result.reason as Error).message}`); + return; + } + started += result.value.started; + if (result.value.errors?.length) errors.push(`计划 #${ids[index]}:${result.value.errors.join(";")}`); + }); + if (errors.length) { + toast.warning(`已启动 ${started} 个任务,但有 ${errors.length} 项未完成:${errors.join(";")}`); + } else { + toast.success(`已立即运行 ${started} 个任务`); + } + setSelectedIds([]); + await load(); + }; + const bulkRemove = async () => { + const ids = [...selectedIds]; + if (!ids.length || !window.confirm(`确定删除选中的 ${ids.length} 个计划吗?`)) return; + const results = await Promise.allSettled(ids.map((id) => api.deleteSchedule(id))); + const failed = results.filter((result) => result.status === "rejected").length; + if (failed) toast.warning(`已删除 ${ids.length - failed} 个计划,${failed} 个计划删除失败`); + else toast.success(`已删除 ${ids.length} 个计划`); + setSelectedIds([]); + await load(); + }; let scheduleContent: React.ReactNode; if (loading) { scheduleContent = ( @@ -597,6 +649,24 @@ export default function SchedulesPage() { 共 {filteredItems.length} 条 + {selectedIds.length ? ( +
+ 已选 {selectedIds.length} 个计划 +
+ + + +
+
+ ) : null} {filteredItems.length === 0 ? ( 没有匹配的计划任务。 @@ -606,6 +676,13 @@ export default function SchedulesPage() { + + toggleAllVisible(checked === true)} + /> + 计划名称 时间线 运行周期 @@ -621,6 +698,13 @@ export default function SchedulesPage() { {visibleItems.map((item) => ( + + toggleSelected(item.id, checked === true)} + /> +
{item.name}
计划 #{item.id}
@@ -731,7 +815,7 @@ export default function SchedulesPage() {
{expandedId === item.id ? ( - +