From 1fdc607c072e6d0349354d789c7628a10472996d Mon Sep 17 00:00:00 2001 From: duxingchen Date: Wed, 12 Aug 2026 15:24:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AE=A1=E7=90=86=E5=91=98=E4=BB=A3?= =?UTF-8?q?=E5=B7=A5=E6=A8=A1=E5=BC=8F=20=E2=80=94=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=9D=83=E9=99=90=E6=94=BE=E5=BC=80=20+=20=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E7=95=99=E7=97=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前端: - WorkspaceArea 新增 currentUserRole prop - isAssignee/canRecall: SUPER_ADMIN/SUPERVISOR 上帝视角直接放行 - 管理员代操作时显示黄色提示条: 正在以管理员身份代[原负责人]操作 - detail.vue: loadCurrentUser 提取并保存 user.role 后端: - _create_task_log 新增 task_assignee_id 参数 - operator_id!=task_assignee_id 时自动拼接 [管理员xx代办操作] - 12处调用点全部传入 task_assignee_id - receive_task 位置赋值确认为 task.assignee_id(非 operator_id) --- backend/app/services/task_service.py | 30 +++++++++++++++---- .../pages/scan/components/WorkspaceArea.vue | 21 ++++++++++++- track-uniapp/src/pages/scan/detail.vue | 5 ++-- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/backend/app/services/task_service.py b/backend/app/services/task_service.py index cdc88df..c7cd2a2 100644 --- a/backend/app/services/task_service.py +++ b/backend/app/services/task_service.py @@ -223,13 +223,19 @@ async def _create_task_log( action_type: str, operator_id: str | None = None, remark: str | None = None, + task_assignee_id: str | None = None, ) -> TaskLog: - """创建任务操作日志""" + """创建任务操作日志。自动检测管理员代办并拼接审计标记。""" + final_remark = remark or "" + # 管理员代办检测:操作人 ≠ 任务负责人 → 拼接审计追述 + if operator_id and task_assignee_id and operator_id != task_assignee_id: + proxy_note = f" [管理员 {operator_id} 代办操作]" + final_remark = (final_remark + proxy_note).strip() log = TaskLog( task_id=task_id, operator_id=operator_id, action_type=action_type, - remark=remark, + remark=final_remark or None, ) db.add(log) return log @@ -349,7 +355,8 @@ async def end_task( task.completed_at = now await _create_task_log(db, task_id, action_type="end", - operator_id=operator_id, remark=f"分支「{task.task_name}」已终止(无下游)") + operator_id=operator_id, remark=f"分支「{task.task_name}」已终止(无下游)", + task_assignee_id=task.assignee_id) # 🔧 位置回溯:分支结束后优先回溯到父任务负责人 await _recalc_product_location(db, task.product_id, task.id) @@ -414,7 +421,8 @@ async def recall_task( task.status = TASK_STATUS_CANCELED task.completed_at = now await _create_task_log(db, task_id, action_type="recall", operator_id=operator_id, - remark=f"撤回转交「{task.task_name}」→ {task.assignee_id}") + remark=f"撤回转交「{task.task_name}」→ {task.assignee_id}", + task_assignee_id=task.assignee_id) db.add(TaskRecord(task_id=task.id, remark=f"[撤回] 转交至 {task.assignee_id} 已撤回", images="[]")) # 2. 生成接力新任务(以撤回节点为父,还给操作人) @@ -432,7 +440,8 @@ async def recall_task( db.add(recovery) await db.flush() await _create_task_log(db, recovery.id, action_type="create", operator_id=operator_id, - remark=f"撤回接力:撤回「{task.task_name}」→ {task.assignee_id} 后重新指派给 {operator_id}") + remark=f"撤回接力:撤回「{task.task_name}」→ {task.assignee_id} 后重新指派给 {operator_id}", + task_assignee_id=recovery.assignee_id) db.add(TaskRecord(task_id=recovery.id, remark=f"[重新接手] 撤回转交后系统自动生成接力节点", images="[]")) # 3. 更新产品位置 @@ -479,7 +488,8 @@ async def spawn_subtask( db.add(TaskRecord(task_id=task.id, remark=data.remark or f"[派发协助] 分配给 {data.assignee_id}", images="[]")) await _create_task_log(db, child.id, action_type="create", operator_id=operator_id, - remark=f"协助分支(由「{task.task_name}」派发,分配给 {data.assignee_id})") + remark=f"协助分支(由「{task.task_name}」派发,分配给 {data.assignee_id})", + task_assignee_id=child.assignee_id) await db.commit() await db.refresh(child) return _to_response(child) @@ -546,6 +556,7 @@ async def receive_task( action_type="receive", operator_id=operator_id, remark=remark or f"操作员确认接收任务「{task.task_name}」", + task_assignee_id=task.assignee_id, ) db.add(TaskRecord(task_id=task.id, remark=remark or f"[接收] 操作员已确认接收", images="[]")) @@ -609,6 +620,7 @@ async def reject_task( action_type="reject", operator_id=operator_id, remark=f"品质驳回: {request.reason}", + task_assignee_id=task.assignee_id, ) # --- 2. 确定返工任务的负责人(追溯上一道工序的转交人) --- @@ -657,6 +669,7 @@ async def reject_task( action_type="create", operator_id=operator_id, remark=f"返工任务(驳回自「{task.task_name}」,原因: {request.reason}),分配给 {rework_assignee_id}", + task_assignee_id=rework_task.assignee_id, ) # 🔔 通知:品质驳回 @@ -745,6 +758,7 @@ async def transfer_task( action_type="complete", operator_id=operator_id, remark=request.note or f"完成任务「{task.task_name}」,转交至下一道工序", + task_assignee_id=task.assignee_id, ) db.add(TaskRecord(task_id=task.id, remark=request.note or f"[完工转交] 移交下一工序", images="[]")) @@ -801,6 +815,7 @@ async def transfer_task( action_type="create", operator_id=operator_id, remark=request.note or f"由任务「{task.task_name}」裂变转交创建,分配给 {nt.assignee_id}", + task_assignee_id=nt.assignee_id, ) # 🔔 通知:新任务派发 if nt.assignee_id: @@ -910,6 +925,7 @@ async def complete_task( action_type="complete", operator_id=request.operator_id, remark=request.remark or f"完成任务: {task.task_name}", + task_assignee_id=task.assignee_id, ) # --- 4. 可选:创建下一步任务(转交) --- @@ -941,6 +957,7 @@ async def complete_task( action_type="create", operator_id=request.operator_id, remark=f"由任务「{task.task_name}」完成后转交创建", + task_assignee_id=next_task.assignee_id, ) # 🔧 位置回溯:老接口也触发(父任务优先) @@ -1000,6 +1017,7 @@ async def create_subtask( action_type="create", operator_id=data.assignee_id, remark=f"创建子任务「{data.task_name}」,父任务: 「{parent.task_name}」", + task_assignee_id=subtask.assignee_id, ) await db.commit() diff --git a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue index 51cd582..c1451ab 100644 --- a/track-uniapp/src/pages/scan/components/WorkspaceArea.vue +++ b/track-uniapp/src/pages/scan/components/WorkspaceArea.vue @@ -78,6 +78,9 @@ {{ formatTaskTime(lockedTask) }} + + ⚠️ 正在以管理员身份代 [{{ formatUserName(lockedTask.assignee_id) || lockedTask.assignee_id }}] 操作 + @@ -111,6 +114,7 @@ export default { product: { type: Object, default: null }, currentUserId: { type: String, default: "" }, currentUsername: { type: String, default: "" }, + currentUserRole: { type: String, default: "" }, initialLockTaskId: { type: String, default: "" }, }, emits: ["action", "viewRecords"], @@ -162,9 +166,22 @@ export default { return result; }, lockedTask() { return this.focusTasks.find(t => t.id === this.lockedTaskId) || null; }, - isAssignee() { if (!this.lockedTask) return false; return this.lockedTask.assignee_id == this.currentUserId || this.lockedTask.assignee_id == this.currentUsername; }, + isAdmin() { return this.currentUserRole === 'SUPER_ADMIN' || this.currentUserRole === 'SUPERVISOR'; }, + isAssignee() { + if (!this.lockedTask) return false; + if (this.isAdmin) return true; // 管理员上帝视角 + return this.lockedTask.assignee_id == this.currentUserId + || this.lockedTask.assignee_id == this.currentUsername; + }, + isAdminProxy() { + // 管理员正在代操作非本人任务 + return this.isAdmin && this.lockedTask + && this.lockedTask.assignee_id != this.currentUserId + && this.lockedTask.assignee_id != this.currentUsername; + }, canRecall() { if (!this.lockedTask || this.lockedTask.status !== 'PENDING') return false; + if (this.isAdmin) return true; // 管理员可撤回任何转交 if (this.isAssignee) return false; if (!this.lockedTask.parent_task_id) return false; const parent = this.taskMap[this.lockedTask.parent_task_id]; @@ -281,6 +298,8 @@ export default { .btn-icon { font-size: 36rpx; margin-bottom: 6rpx; } .btn-txt { font-size: 24rpx; font-weight: 700; } .footer-readonly { justify-content: center; background: #fef2f2; } +.admin-proxy-notice { padding: 10rpx 20rpx; background: #fef9e7; border-top: 2rpx solid #fde68a; flex-shrink: 0; } +.proxy-text { font-size: 22rpx; color: #b45309; font-weight: 600; } .readonly-hint { font-size: 24rpx; color: #dc2626; font-weight: 600; } .footer-transfer { background: #dcfce7; color: #16a34a; } .footer-record { background: #eff6ff; color: #2563eb; } diff --git a/track-uniapp/src/pages/scan/detail.vue b/track-uniapp/src/pages/scan/detail.vue index 4323f4a..e23a200 100644 --- a/track-uniapp/src/pages/scan/detail.vue +++ b/track-uniapp/src/pages/scan/detail.vue @@ -46,6 +46,7 @@ @@ -218,7 +219,7 @@ export default { users: [], TASK_NAME_OPTIONS, createFirstVisible: false, isWarehouseTransfer: false, firstForm: { task_name: "", taskNameIdx: 0, assignee_id: "", assigneeLabel: "", assigneeIdx: 0, note: "" }, firstSaving: false, recordPopup: { visible: false, task: null }, recordForm: { recordId: null, remark: "", images: [], pendingCount: 0 }, recordSaving: false, isUploading: false, - currentUser: null, currentUserId: "", currentUsername: "", currentMode: "workspace", autoLockTaskId: "", + currentUser: null, currentUserId: "", currentUsername: "", currentUserRole: "", currentMode: "workspace", autoLockTaskId: "", processOptions: [], userOptions: [], actionPopup: { visible: false, type: "", task: null }, actionLoading: false, rejectReason: "", receiveRemark: "", receiveTaskName: "", transferForm: { selectedUserId: "", isWarehouse: false, note: "" }, @@ -302,7 +303,7 @@ export default { async doEditProduct() { this.editSaving = true; try { this.product = await patch(`/products/${this.product.id}`, { order_no: this.editForm.order_no.trim(), external_serial: this.editForm.external_serial.trim() }); uni.showToast({ title: "已保存", icon: "success" }); this.editProductVisible = false; } catch {} finally { this.editSaving = false; } }, async loadUsers() { try { const res = await get("/users/", { limit: 200, dept: "IRIS" }); this.users = (res || []).filter(u => u.department === "IRIS"); this.userOptions = this.users.map(u => ({ id: u.username || u.id, name: u.full_name || u.username })); setUserNameMap(this.users); this.dictVersion += 1; } catch (e) { console.error('[loadUsers] 获取用户列表失败:', e); } }, - loadCurrentUser() { try { let user = uni.getStorageSync("user"); if (typeof user === "string" && user) { try { user = JSON.parse(user); } catch (e) { user = null; } } if (user && typeof user === "object") { this.currentUser = user; this.currentUserId = String(user.id || ""); this.currentUsername = user.username || ""; } } catch {} }, + loadCurrentUser() { try { let user = uni.getStorageSync("user"); if (typeof user === "string" && user) { try { user = JSON.parse(user); } catch (e) { user = null; } } if (user && typeof user === "object") { this.currentUser = user; this.currentUserId = String(user.id || ""); this.currentUsername = user.username || ""; this.currentUserRole = user.role || ""; } } catch {} }, onTaskNameChange(e) { const idx = e.detail.value; this.firstForm.taskNameIdx = idx; this.firstForm.task_name = TASK_NAME_OPTIONS[idx]; }, onAssigneeChange(e) { const idx = e.detail.value; const u = this.users[idx]; if (u) { this.firstForm.assigneeIdx = idx; this.firstForm.assignee_id = u.username; this.firstForm.assigneeLabel = `${u.full_name} (${u.username})`; } },