撤回改为CANCELED状态标记+TaskRecord留痕+TreeCanvas灰度渲染+全部状态映射更新
This commit is contained in:
@ -14,6 +14,7 @@ TASK_STATUS_WIP = "WIP" # 进行中
|
|||||||
TASK_STATUS_COMPLETED = "COMPLETED" # 已完成
|
TASK_STATUS_COMPLETED = "COMPLETED" # 已完成
|
||||||
TASK_STATUS_REJECTED = "REJECTED" # 已驳回
|
TASK_STATUS_REJECTED = "REJECTED" # 已驳回
|
||||||
TASK_STATUS_ARCHIVED = "ARCHIVED" # 已入库
|
TASK_STATUS_ARCHIVED = "ARCHIVED" # 已入库
|
||||||
|
TASK_STATUS_CANCELED = "CANCELED" # 已撤回/已作废
|
||||||
|
|
||||||
|
|
||||||
class Task(Base):
|
class Task(Base):
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from sqlalchemy import select, delete
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
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.core.time_utils import get_beijing_time
|
||||||
from app.models.product import Product
|
from app.models.product import Product
|
||||||
from app.models.task_log import TaskLog
|
from app.models.task_log import TaskLog
|
||||||
@ -313,43 +313,43 @@ async def end_task(
|
|||||||
async def recall_task(
|
async def recall_task(
|
||||||
db: AsyncSession, task_id: uuid.UUID, operator_id: str | None = None
|
db: AsyncSession, task_id: uuid.UUID, operator_id: str | None = None
|
||||||
) -> TaskResponse:
|
) -> TaskResponse:
|
||||||
"""撤回 PENDING 转交任务,恢复父任务为 WIP。"""
|
"""撤回 PENDING 转交任务:状态标记为 CANCELED,恢复父任务为 WIP。"""
|
||||||
task = await _get_task_or_404(db, task_id)
|
task = await _get_task_or_404(db, task_id)
|
||||||
|
|
||||||
if task.status != TASK_STATUS_PENDING:
|
if task.status != TASK_STATUS_PENDING:
|
||||||
raise HTTPException(status_code=409, detail="只有待接收(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:
|
if task.parent_task_id:
|
||||||
parent = await _get_task_or_404(db, task.parent_task_id)
|
parent = await _get_task_or_404(db, task.parent_task_id)
|
||||||
if parent.status == TASK_STATUS_COMPLETED:
|
if parent.status == TASK_STATUS_COMPLETED:
|
||||||
parent.status = TASK_STATUS_WIP
|
parent.status = TASK_STATUS_WIP
|
||||||
parent.completed_at = None
|
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_result = await db.execute(select(Product).where(Product.id == parent.product_id))
|
||||||
product = product_result.scalar_one_or_none()
|
product = product_result.scalar_one_or_none()
|
||||||
if product and parent.assignee_id:
|
if product and parent.assignee_id:
|
||||||
product.current_location_id = parent.assignee_id
|
product.current_location_id = parent.assignee_id
|
||||||
product.overall_status = parent.task_name
|
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()
|
await db.commit()
|
||||||
|
|
||||||
if task.parent_task_id:
|
if task.parent_task_id:
|
||||||
parent = await _get_task_or_404(db, task.parent_task_id)
|
refreshed = await _get_task_or_404(db, task.parent_task_id)
|
||||||
return _to_response(parent)
|
return _to_response(refreshed)
|
||||||
else:
|
return _to_response(task)
|
||||||
return _to_response(task)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@ -396,7 +396,7 @@ async def _check_children_done(db: AsyncSession, task_id: uuid.UUID):
|
|||||||
)
|
)
|
||||||
children = result.scalars().all()
|
children = result.scalars().all()
|
||||||
incomplete = [c for c in children if c.status not in (
|
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:
|
if incomplete:
|
||||||
names = "、".join(c.task_name for c in incomplete)
|
names = "、".join(c.task_name for c in incomplete)
|
||||||
|
|||||||
@ -144,8 +144,8 @@ export default {
|
|||||||
const angle = Math.atan2(dy, dx) * 180 / Math.PI;
|
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' };
|
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; },
|
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"; default: return "s-gray"; } }
|
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"; } }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -163,6 +163,7 @@ export default {
|
|||||||
.canvas-node.s-blue { border-left-color: #3b82f6; }
|
.canvas-node.s-blue { border-left-color: #3b82f6; }
|
||||||
.canvas-node.s-green { border-left-color: #22c55e; }
|
.canvas-node.s-green { border-left-color: #22c55e; }
|
||||||
.canvas-node.s-red { border-left-color: #ef4444; }
|
.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; }
|
.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-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; }
|
.badge-sub { font-size: 10px; padding: 2px 8px; border-radius: 6px; background: #ede9fe; color: #7c3aed; font-weight: 700; }
|
||||||
|
|||||||
@ -102,7 +102,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const STATUS_MAP = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
|
const STATUS_MAP = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库", CANCELED: "已撤回" };
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "WorkspaceArea",
|
name: "WorkspaceArea",
|
||||||
|
|||||||
@ -158,7 +158,7 @@ import TreeCanvas from "./components/TreeCanvas.vue";
|
|||||||
|
|
||||||
const OVERALL_OPTIONS = ["备货", "生产", "测试", "维修", "在库"];
|
const OVERALL_OPTIONS = ["备货", "生产", "测试", "维修", "在库"];
|
||||||
const TASK_NAME_OPTIONS = ["备货", "生产", "测试", "维修", "在库"];
|
const TASK_NAME_OPTIONS = ["备货", "生产", "测试", "维修", "在库"];
|
||||||
const STATUS_MAP = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
|
const STATUS_MAP = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库", CANCELED: "已撤回" };
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { WorkspaceArea, TreeCanvas },
|
components: { WorkspaceArea, TreeCanvas },
|
||||||
|
|||||||
@ -68,7 +68,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
statusLabel(s) {
|
statusLabel(s) {
|
||||||
const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
|
const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库", CANCELED: "已撤回" };
|
||||||
return map[s] || s;
|
return map[s] || s;
|
||||||
},
|
},
|
||||||
statusColor(s) {
|
statusColor(s) {
|
||||||
|
|||||||
Reference in New Issue
Block a user