From 616638a6c88a9111e07820625267ed7a64212d35 Mon Sep 17 00:00:00 2001 From: duxingchen Date: Thu, 6 Aug 2026 12:51:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=92=A4=E5=9B=9E=E6=94=B9=E4=B8=BACANCELED?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A0=87=E8=AE=B0+TaskRecord=E7=95=99?= =?UTF-8?q?=E7=97=95+TreeCanvas=E7=81=B0=E5=BA=A6=E6=B8=B2=E6=9F=93+?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E7=8A=B6=E6=80=81=E6=98=A0=E5=B0=84=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/models/task.py | 1 + backend/app/services/task_service.py | 40 +++++++++---------- .../src/pages/scan/components/TreeCanvas.vue | 5 ++- .../pages/scan/components/WorkspaceArea.vue | 2 +- track-uniapp/src/pages/scan/detail.vue | 2 +- track-uniapp/src/pages/tasks/index.vue | 2 +- 6 files changed, 27 insertions(+), 25 deletions(-) diff --git a/backend/app/models/task.py b/backend/app/models/task.py index ff919d6..50543dc 100644 --- a/backend/app/models/task.py +++ b/backend/app/models/task.py @@ -14,6 +14,7 @@ TASK_STATUS_WIP = "WIP" # 进行中 TASK_STATUS_COMPLETED = "COMPLETED" # 已完成 TASK_STATUS_REJECTED = "REJECTED" # 已驳回 TASK_STATUS_ARCHIVED = "ARCHIVED" # 已入库 +TASK_STATUS_CANCELED = "CANCELED" # 已撤回/已作废 class Task(Base): diff --git a/backend/app/services/task_service.py b/backend/app/services/task_service.py index 3d07c95..34ba79c 100644 --- a/backend/app/services/task_service.py +++ b/backend/app/services/task_service.py @@ -6,7 +6,7 @@ from sqlalchemy import select, delete from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from app.models.task import Task, TaskRecord, TASK_STATUS_PENDING, TASK_STATUS_WIP, TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED +from app.models.task import Task, TaskRecord, TASK_STATUS_PENDING, TASK_STATUS_WIP, TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED, TASK_STATUS_CANCELED, TASK_STATUS_ARCHIVED from app.core.time_utils import get_beijing_time from app.models.product import Product from app.models.task_log import TaskLog @@ -313,43 +313,43 @@ async def end_task( async def recall_task( db: AsyncSession, task_id: uuid.UUID, operator_id: str | None = None ) -> TaskResponse: - """撤回 PENDING 转交任务,恢复父任务为 WIP。""" + """撤回 PENDING 转交任务:状态标记为 CANCELED,恢复父任务为 WIP。""" task = await _get_task_or_404(db, task_id) if task.status != TASK_STATUS_PENDING: raise HTTPException(status_code=409, detail="只有待接收(PENDING)的任务可以撤回") - # 如果有父任务,恢复父任务状态 + # 标记作废(不物理删除) + task.status = TASK_STATUS_CANCELED + task.completed_at = get_beijing_time() + + # 写入撤回记录 + await _create_task_log(db, task_id, action_type="recall", operator_id=operator_id, + remark=f"撤回转交「{task.task_name}」→ {task.assignee_id},任务已作废") + # 同时写入 TaskRecord 留痕 + recall_record = TaskRecord(task_id=task.id, remark=f"[撤回] 转交至 {task.assignee_id} 的「{task.task_name}」已撤回", images="[]") + db.add(recall_record) + + # 恢复父任务 if task.parent_task_id: parent = await _get_task_or_404(db, task.parent_task_id) if parent.status == TASK_STATUS_COMPLETED: parent.status = TASK_STATUS_WIP parent.completed_at = None - + await _create_task_log(db, parent.id, action_type="recall", operator_id=operator_id, + remark=f"撤回转交「{task.task_name}」,恢复为 WIP") product_result = await db.execute(select(Product).where(Product.id == parent.product_id)) product = product_result.scalar_one_or_none() if product and parent.assignee_id: product.current_location_id = parent.assignee_id product.overall_status = parent.task_name - await _create_task_log(db, parent.id, action_type="recall", operator_id=operator_id, - remark=f"撤回转交「{task.task_name}」→ {task.assignee_id},恢复父任务为 WIP") - - # 先删除关联的 task_logs 和 task_records - await db.execute(delete(TaskLog).where(TaskLog.task_id == task_id)) - await db.execute(delete(TaskRecord).where(TaskRecord.task_id == task_id)) - await db.flush() - - # 删除 PENDING 子任务 - await db.delete(task) - await db.commit() if task.parent_task_id: - parent = await _get_task_or_404(db, task.parent_task_id) - return _to_response(parent) - else: - return _to_response(task) + refreshed = await _get_task_or_404(db, task.parent_task_id) + return _to_response(refreshed) + return _to_response(task) # ============================================================ @@ -396,7 +396,7 @@ async def _check_children_done(db: AsyncSession, task_id: uuid.UUID): ) children = result.scalars().all() incomplete = [c for c in children if c.status not in ( - TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED, ARCHIVED_STATUS + TASK_STATUS_COMPLETED, TASK_STATUS_REJECTED, TASK_STATUS_CANCELED, ARCHIVED_STATUS )] if incomplete: names = "、".join(c.task_name for c in incomplete) diff --git a/track-uniapp/src/pages/scan/components/TreeCanvas.vue b/track-uniapp/src/pages/scan/components/TreeCanvas.vue index caedfd5..0a89b24 100644 --- a/track-uniapp/src/pages/scan/components/TreeCanvas.vue +++ b/track-uniapp/src/pages/scan/components/TreeCanvas.vue @@ -144,8 +144,8 @@ export default { const angle = Math.atan2(dy, dx) * 180 / Math.PI; return { left: line.x1 + 'px', top: line.y1 + 'px', width: len + 'px', transform: `rotate(${angle}deg)`, transformOrigin: '0 0' }; }, - statusLabel(s) { const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" }; return map[s] || s; }, - statusColor(s) { switch (s) { case "PENDING": return "s-yellow"; case "WIP": return "s-blue"; case "COMPLETED": return "s-green"; case "REJECTED": return "s-red"; default: return "s-gray"; } } + statusLabel(s) { const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库", CANCELED: "已撤回" }; return map[s] || s; }, + statusColor(s) { switch (s) { case "PENDING": return "s-yellow"; case "WIP": return "s-blue"; case "COMPLETED": return "s-green"; case "REJECTED": return "s-red"; case "CANCELED": return "s-canceled"; default: return "s-gray"; } } } }; @@ -163,6 +163,7 @@ export default { .canvas-node.s-blue { border-left-color: #3b82f6; } .canvas-node.s-green { border-left-color: #22c55e; } .canvas-node.s-red { border-left-color: #ef4444; } +.canvas-node.s-canceled { border-left-color: #9ca3af; opacity: 0.5; filter: grayscale(0.6); } .cn-header { display: flex; align-items: center; justify-content: space-between; } .badge-main { font-size: 10px; padding: 2px 8px; border-radius: 6px; background: #2563eb; color: #fff; font-weight: 700; } .badge-sub { font-size: 10px; padding: 2px 8px; border-radius: 6px; background: #ede9fe; color: #7c3aed; font-weight: 700; } diff --git a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue index 74120ab..0a75441 100644 --- a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue +++ b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue @@ -102,7 +102,7 @@