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()}>
+
+
+
+
+ {candidates.length ? (
+
+
依赖(点选其他任务)
+
+ {candidates.map((c) => {
+ const on = eDeps.includes(c.id);
+ return (
+
+ );
+ })}
+
+
) : null}
+
+
+
+
) : null}
{label ? (
@@ -267,7 +310,7 @@ function NewTaskPanel({ tasks, onCancel, onCreate, t }) {
);
}
-function TaskSection({ tasks, onToast, onCreate, onTakeover, t }) {
+function TaskSection({ tasks, onToast, onCreate, onTakeover, taskOps, t }) {
const { Button, SectionHead } = window.MaestroDesignSystem_a6a290;
const [expandedId, setExpandedId] = React.useState(null);
const [openIds, setOpenIds] = React.useState(() => new Set(['t1']));
@@ -279,7 +322,10 @@ function TaskSection({ tasks, onToast, onCreate, onTakeover, t }) {
const [flashId, setFlashId] = React.useState(null);
const byId = React.useMemo(() => kitFlatten(tasks), [tasks]);
const cplxOf = (task) => cplxOverride[task.id] || task.cplx;
- const onChangeCplx = (id, v) => setCplxOverride((m) => ({ ...m, [id]: v }));
+ const onChangeCplx = (id, v) => {
+ setCplxOverride((m) => ({ ...m, [id]: v })); // 乐观显示
+ if (taskOps && taskOps.patch) taskOps.patch(id, { complexity: v }); // 落库
+ };
const toggleOpen = (id) => setOpenIds((prev) => {
const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next;
});
@@ -347,7 +393,7 @@ function TaskSection({ tasks, onToast, onCreate, onTakeover, t }) {
{roots.map((tk) => (
+ byId={byId} cplxOf={cplxOf} onChangeCplx={onChangeCplx} flashId={flashId} onJump={onJump} visibleSet={visibleSet} onTakeover={onTakeover} taskOps={taskOps} />
))}