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) => {
|
const runTask = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
const flow = await window.rpa.flows.get(id);
|
const flow = await window.rpa.flows.get(id);
|
||||||
@ -260,6 +280,7 @@ export function App() {
|
|||||||
onOpenEditor={(id) => void openEditor(id)}
|
onOpenEditor={(id) => void openEditor(id)}
|
||||||
onRun={(id) => void runTask(id)}
|
onRun={(id) => void runTask(id)}
|
||||||
onDelete={(id) => void deleteTask(id)}
|
onDelete={(id) => void deleteTask(id)}
|
||||||
|
onDeleteMany={(ids) => void deleteTasks(ids)}
|
||||||
onRefresh={() => refreshFlows()}
|
onRefresh={() => refreshFlows()}
|
||||||
onToast={showToast}
|
onToast={showToast}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -23,6 +23,7 @@ interface TasksPageProps {
|
|||||||
onOpenEditor: (id: string) => void;
|
onOpenEditor: (id: string) => void;
|
||||||
onRun: (id: string) => void;
|
onRun: (id: string) => void;
|
||||||
onDelete: (id: string) => void;
|
onDelete: (id: string) => void;
|
||||||
|
onDeleteMany: (ids: string[]) => void;
|
||||||
onRefresh: () => void | Promise<void>;
|
onRefresh: () => void | Promise<void>;
|
||||||
onToast: (message: string, kind?: "ok" | "error") => 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;
|
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 [query, setQuery] = useState("");
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(flows[0]?.id || null);
|
const [selectedId, setSelectedId] = useState<string | null>(flows[0]?.id || null);
|
||||||
|
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
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);
|
if (!selectedId || !flows.some((flow) => flow.id === selectedId)) setSelectedId(flows[0].id);
|
||||||
}, [flows, selectedId]);
|
}, [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 filtered = useMemo(() => {
|
||||||
const keyword = query.trim().toLowerCase();
|
const keyword = query.trim().toLowerCase();
|
||||||
if (!keyword) return flows;
|
if (!keyword) return flows;
|
||||||
return flows.filter((flow) => flow.name.toLowerCase().includes(keyword));
|
return flows.filter((flow) => flow.name.toLowerCase().includes(keyword));
|
||||||
}, [flows, query]);
|
}, [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 selected = flows.find((flow) => flow.id === selectedId) || filtered[0] || null;
|
||||||
const enabledCount = flows.filter((flow) => flow.enabled !== false).length;
|
const enabledCount = flows.filter((flow) => flow.enabled !== false).length;
|
||||||
const failedCount = flows.filter((flow) => flow.lastRunStatus === "failed").length;
|
const failedCount = flows.filter((flow) => flow.lastRunStatus === "failed").length;
|
||||||
@ -112,6 +132,11 @@ export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onR
|
|||||||
<strong>任务列表</strong>
|
<strong>任务列表</strong>
|
||||||
<span className="panel-subtitle">按最近修改时间排序</span>
|
<span className="panel-subtitle">按最近修改时间排序</span>
|
||||||
</div>
|
</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">
|
<label className="task-search">
|
||||||
<Search className="icon" />
|
<Search className="icon" />
|
||||||
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索任务" aria-label="搜索任务" />
|
<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>
|
||||||
|
|
||||||
<div className="task-table-head" aria-hidden="true">
|
<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>
|
<span>触发方式</span>
|
||||||
<span>最近运行</span>
|
<span>最近运行</span>
|
||||||
@ -148,6 +182,14 @@ export function TasksPage({ flows, onCreate, onCreateExamples, onOpenEditor, onR
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="task-name-cell">
|
<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-icon"><ListChecks className="icon" /></span>
|
||||||
<span className="task-copy"><strong>{flow.name}</strong><small>{flow.nodeCount} 个节点</small></span>
|
<span className="task-copy"><strong>{flow.name}</strong><small>{flow.nodeCount} 个节点</small></span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2860,6 +2860,28 @@ textarea:focus-visible {
|
|||||||
box-shadow: inset 3px 0 0 var(--blue);
|
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-name-cell,
|
||||||
.task-trigger,
|
.task-trigger,
|
||||||
.task-updated,
|
.task-updated,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user