diff --git a/app/src/api.js b/app/src/api.js index 9563dad..9bcbb72 100644 --- a/app/src/api.js +++ b/app/src/api.js @@ -32,6 +32,10 @@ export const api = { patchProject: (pid, body) => http('PATCH', `/api/projects/${pid}`, body), createProject: (body) => http('POST', '/api/projects', body), takeover: (taskId) => http('POST', `/api/tasks/${taskId}/takeover`), + patchTask: (taskId, body) => http('PATCH', `/api/tasks/${taskId}`, body), + cancelTask: (taskId) => http('POST', `/api/tasks/${taskId}/cancel`), + requeueTask: (taskId) => http('POST', `/api/tasks/${taskId}/requeue`), + deleteTask: (taskId) => http('DELETE', `/api/tasks/${taskId}`), // 附件上传走 multipart(不能用 JSON helper,content-type 由浏览器带 boundary) uploadAttachments: async (taskId, files) => { const fd = new FormData(); diff --git a/app/src/app.jsx b/app/src/app.jsx index d1a912b..874fef5 100644 --- a/app/src/app.jsx +++ b/app/src/app.jsx @@ -311,6 +311,25 @@ export function App() { setTakeoverInfo({ ...info, title: task.title }); } catch (e) { toast('warn', '接管失败:' + e.message); } }; + // 任务生命周期操作(编辑落库 / 取消 / 重投 / 删除),统一带刷新 + const taskOps = { + patch: async (taskId, body) => { + try { await api.patchTask(taskId, body); toast('ok', '已更新'); loadProject(currentId); } + catch (e) { toast('warn', '更新失败:' + e.message); } + }, + cancel: async (taskId) => { + try { await api.cancelTask(taskId); toast('warn', '已取消'); loadProject(currentId); loadGlobal(); } + catch (e) { toast('warn', '取消失败:' + e.message); } + }, + requeue: async (taskId) => { + try { await api.requeueTask(taskId); toast('ok', '已重投'); loadProject(currentId); loadGlobal(); } + catch (e) { toast('warn', '重投失败:' + e.message); } + }, + del: async (taskId) => { + try { await api.deleteTask(taskId); toast('warn', '已删除'); loadProject(currentId); loadGlobal(); } + catch (e) { toast('warn', '删除失败:' + e.message); } + }, + }; const [showNewProject, setShowNewProject] = React.useState(false); const createProject = async (body) => { try { @@ -342,7 +361,7 @@ export function App() { {tasks.length ? ( - + ) : (
{t.empty} diff --git a/design/ui_kits/console/TaskTree.jsx b/design/ui_kits/console/TaskTree.jsx index 8434fd3..f69de56 100644 --- a/design/ui_kits/console/TaskTree.jsx +++ b/design/ui_kits/console/TaskTree.jsx @@ -100,7 +100,7 @@ function FilterBar({ t, kw, setKw, cplxSet, toggleCplx, statusSet, toggleGroup, ); } -function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleOpen, t, byId, cplxOf, onChangeCplx, flashId, onJump, visibleSet, onTakeover }) { +function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleOpen, t, byId, cplxOf, onChangeCplx, flashId, onJump, visibleSet, onTakeover, taskOps }) { const { StatusChip } = window.MaestroDesignSystem_a6a290; const kids = (task.children || []).filter((k) => !visibleSet || visibleSet.has(k.id)); const open = openIds.has(task.id) || !!visibleSet; // 筛选时自动展开可见节点 @@ -140,13 +140,13 @@ function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleO
- {expanded ? : null} + {expanded ? : null} {kids.length && open ? (
{kids.map((k) => ( + byId={byId} cplxOf={cplxOf} onChangeCplx={onChangeCplx} flashId={flashId} onJump={onJump} visibleSet={visibleSet} onTakeover={onTakeover} taskOps={taskOps} /> ))}
) : null} @@ -154,23 +154,66 @@ function TaskRow({ task, depth, ctx, expandedId, setExpandedId, openIds, toggleO ); } -function TaskDetail({ task, t, byId, onJump, onTakeover }) { +function TaskDetail({ task, t, byId, onJump, onTakeover, taskOps }) { + const { Button, Input, Select } = window.MaestroDesignSystem_a6a290; const label = task.doc ? t.specLabel : task.ops ? t.opsLabel : null; const text = task.doc || task.ops; const attachments = (task._raw && task._raw.attachments) || []; + const ops = taskOps || {}; + const st = task.status; + const canCancel = !['done', 'cancelled'].includes(st); + const canRequeue = ['failed', 'needs_attention', 'blocked', 'cancelled'].includes(st); + + const [editing, setEditing] = React.useState(false); + const [eTitle, setETitle] = React.useState(task.title); + const [ePrio, setEPrio] = React.useState(String((task.prio || 'P1').replace('P', ''))); + const [eDeps, setEDeps] = React.useState(task.deps || []); + const btn = (color) => ({ fontFamily: 'var(--mono)', fontSize: 12, cursor: 'pointer', background: 'transparent', color: `var(--${color})`, border: `1px solid var(--${color}-dim)`, borderRadius: 'var(--radius-sm,4px)', padding: '3px 10px' }); + const candidates = [...byId.values()].filter((x) => x.id !== task.id); + const toggleDep = (id) => setEDeps((d) => (d.includes(id) ? d.filter((x) => x !== id) : [...d, id])); + const saveEdit = () => { + if (ops.patch) ops.patch(task.id, { title: eTitle.trim(), priority: Number(ePrio), deps: eDeps }); + setEditing(false); + }; + return (
- {onTakeover || attachments.length ? ( -
- {onTakeover ? ( - - ) : null} - {attachments.length ? ( - 📎 {attachments.map((a) => a.name).join(' · ')} + {/* 操作工具条:编辑 / 接管 / 重投 / 取消 / 删除 */} +
e.stopPropagation()}> + {ops.patch ? : null} + {onTakeover ? : null} + {ops.requeue && canRequeue ? : null} + {ops.cancel && canCancel ? : null} + {ops.del ? : null} + {attachments.length ? 📎 {attachments.map((a) => a.name).join(' · ')} : null} +
+ {/* 编辑表单 */} + {editing ? ( +
e.stopPropagation()}> + +
+