feat: 任务列表支持勾选批量删除
- 每行任务左侧新增复选框; 表头「任务」列新增全选(仅作用于当前筛选结果) - 勾选后列表头部出现「删除所选(N)」按钮, 二次确认后批量删除 - 批量删除保留「至少保留一个任务」保护; 勾选集合随列表变化自动裁剪已删除 id - 单行删除行为不变
This commit is contained in:
parent
1bb28e3368
commit
5795c39a6f
@ -175,6 +175,26 @@ export function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTasks = async (ids: string[]) => {
|
||||
if (!ids.length) return;
|
||||
if (ids.length >= flows.length) {
|
||||
showToast("至少保留一个任务", "error");
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(`确定删除选中的 ${ids.length} 个任务吗?删除后不可恢复。`)) return;
|
||||
try {
|
||||
for (const id of ids) await window.rpa.flows.delete(id);
|
||||
await refreshFlows();
|
||||
if (activeFlowId && ids.includes(activeFlowId)) {
|
||||
setActiveFlowId(null);
|
||||
setPage("tasks");
|
||||
}
|
||||
showToast(`已删除 ${ids.length} 个任务`);
|
||||
} catch (deleteError) {
|
||||
showToast(deleteError instanceof Error ? deleteError.message : String(deleteError), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const runTask = async (id: string) => {
|
||||
try {
|
||||
const flow = await window.rpa.flows.get(id);
|
||||
@ -260,6 +280,7 @@ export function App() {
|
||||
onOpenEditor={(id) => void openEditor(id)}
|
||||
onRun={(id) => void runTask(id)}
|
||||
onDelete={(id) => void deleteTask(id)}
|
||||
onDeleteMany={(ids) => void deleteTasks(ids)}
|
||||
onRefresh={() => refreshFlows()}
|
||||
onToast={showToast}
|
||||
/>
|
||||
|
||||
@ -23,6 +23,7 @@ interface TasksPageProps {
|
||||
onOpenEditor: (id: string) => void;
|
||||
onRun: (id: string) => void;
|
||||
onDelete: (id: string) => void;
|
||||
onDeleteMany: (ids: string[]) => void;
|
||||
onRefresh: () => void | Promise<void>;
|
||||
onToast: (message: string, kind?: "ok" | "error") => void;
|
||||
}
|
||||
@ -46,9 +47,10 @@ function formatDuration(value?: number): string {
|
||||
|
||||
const RUN_LABEL = { succeeded: "成功", failed: "失败", cancelled: "已取消", never: "未运行" } as const;
|
||||
|
||||
export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onRun, onDelete, onRefresh, onToast }: TasksPageProps) {
|
||||
export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onRun, onDelete, onDeleteMany, onRefresh, onToast }: TasksPageProps) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [selectedId, setSelectedId] = useState<string | null>(flows[0]?.id || null);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
@ -70,12 +72,30 @@ export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onR
|
||||
if (!selectedId || !flows.some((flow) => flow.id === selectedId)) setSelectedId(flows[0].id);
|
||||
}, [flows, selectedId]);
|
||||
|
||||
// 勾选集合随任务列表变化裁剪,避免残留已删除任务的 id
|
||||
useEffect(() => {
|
||||
setCheckedIds((previous) => {
|
||||
const alive = new Set(flows.map((flow) => flow.id));
|
||||
const next = previous.filter((id) => alive.has(id));
|
||||
return next.length === previous.length ? previous : next;
|
||||
});
|
||||
}, [flows]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const keyword = query.trim().toLowerCase();
|
||||
if (!keyword) return flows;
|
||||
return flows.filter((flow) => flow.name.toLowerCase().includes(keyword));
|
||||
}, [flows, query]);
|
||||
|
||||
const toggleChecked = (id: string) => {
|
||||
setCheckedIds((previous) => (previous.includes(id) ? previous.filter((item) => item !== id) : [...previous, id]));
|
||||
};
|
||||
|
||||
const allFilteredChecked = filtered.length > 0 && filtered.every((flow) => checkedIds.includes(flow.id));
|
||||
const toggleAllFiltered = () => {
|
||||
setCheckedIds(allFilteredChecked ? [] : filtered.map((flow) => flow.id));
|
||||
};
|
||||
|
||||
const selected = flows.find((flow) => flow.id === selectedId) || filtered[0] || null;
|
||||
const enabledCount = flows.filter((flow) => flow.enabled !== false).length;
|
||||
const failedCount = flows.filter((flow) => flow.lastRunStatus === "failed").length;
|
||||
@ -112,6 +132,11 @@ export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onR
|
||||
<strong>任务列表</strong>
|
||||
<span className="panel-subtitle">按最近修改时间排序</span>
|
||||
</div>
|
||||
{checkedIds.length > 0 && (
|
||||
<button type="button" className="btn btn-danger task-batch-delete" onClick={() => onDeleteMany(checkedIds)}>
|
||||
<Trash2 className="icon" /> 删除所选({checkedIds.length})
|
||||
</button>
|
||||
)}
|
||||
<label className="task-search">
|
||||
<Search className="icon" />
|
||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索任务" aria-label="搜索任务" />
|
||||
@ -128,7 +153,16 @@ export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onR
|
||||
</div>
|
||||
|
||||
<div className="task-table-head" aria-hidden="true">
|
||||
<span>任务</span>
|
||||
<span className="task-name-head">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="task-check"
|
||||
checked={allFilteredChecked}
|
||||
onChange={toggleAllFiltered}
|
||||
title="全选"
|
||||
/>
|
||||
任务
|
||||
</span>
|
||||
<span>状态</span>
|
||||
<span>触发方式</span>
|
||||
<span>最近运行</span>
|
||||
@ -148,6 +182,14 @@ export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onR
|
||||
}}
|
||||
>
|
||||
<div className="task-name-cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="task-check"
|
||||
checked={checkedIds.includes(flow.id)}
|
||||
onChange={() => toggleChecked(flow.id)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label={`选择任务 ${flow.name}`}
|
||||
/>
|
||||
<span className="task-icon"><ListChecks className="icon" /></span>
|
||||
<span className="task-copy"><strong>{flow.name}</strong><small>{flow.nodeCount} 个节点</small></span>
|
||||
</div>
|
||||
|
||||
@ -2860,6 +2860,28 @@ textarea:focus-visible {
|
||||
box-shadow: inset 3px 0 0 var(--blue);
|
||||
}
|
||||
|
||||
/* 任务列表勾选框(单选/全选) */
|
||||
.task-check {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
flex: none;
|
||||
margin: 0;
|
||||
accent-color: var(--blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-name-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.task-batch-delete {
|
||||
margin-left: 12px;
|
||||
padding: 5px 12px;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.task-name-cell,
|
||||
.task-trigger,
|
||||
.task-updated,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user