重构: PENDING恢复驳回按钮 + 转交弹窗升级多分支裂变(无限加分支)

This commit is contained in:
2026-08-05 15:57:20 +08:00
parent c2db89d9d5
commit 30d35d7fa2

View File

@ -103,6 +103,10 @@
@tap="handleTaskAction({ task: activeTask, type: 'receive' })"> @tap="handleTaskAction({ task: activeTask, type: 'receive' })">
接收任务 接收任务
</button> </button>
<button v-if="activeTask.status === 'PENDING'" class="footer-btn footer-reject"
@tap="handleTaskAction({ task: activeTask, type: 'reject' })">
驳回任务
</button>
</view> </view>
</template> </template>
@ -235,50 +239,69 @@
<button class="btn-danger" :disabled="actionLoading || !rejectReason.trim()" @tap="doReject">确认驳回</button> <button class="btn-danger" :disabled="actionLoading || !rejectReason.trim()" @tap="doReject">确认驳回</button>
</view> </view>
</template> </template>
<!-- 转交 --> <!-- 转交支持多分支裂变 -->
<template v-if="actionPopup.type === 'transfer'"> <template v-if="actionPopup.type === 'transfer'">
<text class="popup-title">完工转交</text> <text class="popup-title">完工转交 · 裂变派发</text>
<!-- 工序选择 (Picker) --> <!-- 分支列表 -->
<view class="form-item"> <view v-for="(branch, idx) in transferForm.branches" :key="idx" class="branch-item">
<text class="form-label">下一道工序 <text class="required">*</text></text> <view class="branch-header">
<picker mode="selector" :range="processOptions" @change="onProcessChange"> <text class="branch-label">分支 {{ idx + 1 }}</text>
<view class="picker-value"> <text v-if="transferForm.branches.length > 1" class="branch-del"
<text :class="transferForm.next_task_name ? '' : 'picker-placeholder'"> @tap="removeBranch(idx)">🗑 删除</text>
{{ transferForm.next_task_name || '请选择下一道工序' }} </view>
</text>
<text class="picker-arrow"></text> <!-- 工序选择 -->
</view> <view class="form-item">
</picker> <text class="form-label">工序 <text class="required">*</text></text>
<picker mode="selector" :range="processOptions" @change="(e) => onBranchProcessChange(idx, e)">
<view class="picker-value">
<text :class="branch.task_name ? '' : 'picker-placeholder'">
{{ branch.task_name || '请选择工序' }}
</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<!-- 接收人入库分支不需要 -->
<view v-if="!isBranchWarehouse(idx)" class="form-item">
<text class="form-label">接收人 <text class="required">*</text></text>
<picker mode="selector" :range="userOptions" range-key="name" @change="(e) => onBranchUserChange(idx, e)">
<view class="picker-value">
<text :class="branchUserName(idx) ? '' : 'picker-placeholder'">
{{ branchUserName(idx) || '请选择接收人' }}
</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<view v-if="isBranchWarehouse(idx)" class="warehouse-hint">
📦 入库分支 直接归档无需指定接收人
</view>
</view> </view>
<!-- 接收人选择仅非入库时显示 --> <!-- 添加分支 -->
<view v-if="!isWarehouse" class="form-item"> <button class="btn-add-branch" @tap="addBranch"> 添加并行分支</button>
<text class="form-label">接收人 <text class="required">*</text></text>
<picker mode="selector" :range="userOptions" range-key="name" @change="onUserChange"> <!-- 备注 -->
<view class="picker-value"> <input v-model="transferForm.note" class="popup-input" placeholder="交接备注(选填)" style="margin-top:12px;" />
<text :class="selectedUserName ? '' : 'picker-placeholder'">
{{ selectedUserName || '请选择接收人' }} <!-- 预览 -->
</text> <view v-if="transferForm.branches.some(b => b.task_name)" class="preview-hint">
<text class="picker-arrow"></text> 将创建 {{ transferForm.branches.length }} 个并行任务
</view> <text v-for="(b, i) in transferForm.branches" :key="i" style="display:block;margin-top:2px;">
</picker> {{ i + 1 }}. {{ b.task_name || '' }} {{ branchUserName(i) || (isBranchWarehouse(i) ? '仓库' : '未选') }}
</text>
</view> </view>
<!-- 入库提示 -->
<view v-if="isWarehouse" class="warehouse-hint">
📦 该任务将直接入库任务状态变为已入库
</view>
<input v-model="transferForm.note" class="popup-input" placeholder="交接备注(选填)" />
<view v-if="transferForm.next_task_name" class="preview-hint">
{{ isWarehouse ? '产品将入库并从个人待办中移除' : '将创建 1 ' + transferForm.next_task_name + '任务指派给 ' + (selectedUserName || '') }}
</view>
<view class="popup-btns"> <view class="popup-btns">
<button class="btn-cancel" @tap="closeActionPopup">取消</button> <button class="btn-cancel" @tap="closeActionPopup">取消</button>
<button class="btn-primary" :disabled="actionLoading || !transferForm.next_task_name || (!isWarehouse && !transferForm.assignees.length)" @tap="doTransfer"> <button class="btn-primary"
{{ isWarehouse ? '📦 确认入库' : '确认转交' }} :disabled="actionLoading || !transferForm.branches.some(b => b.task_name) || !canSubmitTransfer"
@tap="doTransfer">
{{ actionLoading ? '提交中...' : '确认转交 (' + transferForm.branches.length + '分支)' }}
</button> </button>
</view> </view>
</template> </template>
@ -334,7 +357,7 @@ export default {
actionPopup: { visible: false, type: "", task: null }, actionPopup: { visible: false, type: "", task: null },
actionLoading: false, actionLoading: false,
rejectReason: "", rejectReason: "",
transferForm: { next_task_name: "", assignees: [], selectedUserId: "", warehouse: false, note: "" }, transferForm: { branches: [{ task_name: "", assignee_id: "" }], note: "" },
}; };
}, },
computed: { computed: {
@ -346,12 +369,12 @@ export default {
if (!this.currentUser) return true; if (!this.currentUser) return true;
return this.currentUser.username === this.recordPopup.task.assignee_id; return this.currentUser.username === this.recordPopup.task.assignee_id;
}, },
selectedUserName() { canSubmitTransfer() {
const u = this.userOptions.find(u => u.id === this.transferForm.selectedUserId); return this.transferForm.branches.every(b => {
return u ? u.name : ""; if (!b.task_name) return false;
}, if (b.task_name === "🏭 入库 (virtual_warehouse)") return true;
isWarehouse() { return !!b.assignee_id;
return this.transferForm.next_task_name === "🏭 入库 (virtual_warehouse)"; });
}, },
activeTask() { activeTask() {
// 查找树中第一个 WIP 或 PENDING 任务作为底部 Footer 的操作目标 // 查找树中第一个 WIP 或 PENDING 任务作为底部 Footer 的操作目标
@ -658,7 +681,7 @@ export default {
this.actionPopup = { visible: true, type, task }; this.actionPopup = { visible: true, type, task };
this.rejectReason = ""; this.rejectReason = "";
this.transferForm = { next_task_name: "", assignees: [], selectedUserId: "", warehouse: false, note: "" }; this.transferForm = { branches: [{ task_name: "", assignee_id: "" }], note: "" };
}, },
handleViewRecords(task) { handleViewRecords(task) {
uni.navigateTo({ url: `/pages/scan/records?taskId=${task.id}` }); uni.navigateTo({ url: `/pages/scan/records?taskId=${task.id}` });
@ -683,28 +706,46 @@ export default {
this.doQuery(this.product.serial_number); this.doQuery(this.product.serial_number);
} catch {} finally { this.actionLoading = false; } } catch {} finally { this.actionLoading = false; }
}, },
onProcessChange(e) { // ---- 多分支操作 ----
this.transferForm.next_task_name = this.processOptions[e.detail.value]; addBranch() {
}, if (this.transferForm.branches.length >= 5) {
onUserChange(e) { uni.showToast({ title: "最多 5 个并行分支", icon: "none" }); return;
const user = this.userOptions[e.detail.value];
if (user) {
this.transferForm.selectedUserId = user.id;
this.transferForm.assignees = [user.id];
} }
this.transferForm.branches.push({ task_name: "", assignee_id: "" });
},
removeBranch(idx) {
if (this.transferForm.branches.length <= 1) return;
this.transferForm.branches.splice(idx, 1);
},
onBranchProcessChange(idx, e) {
this.transferForm.branches[idx].task_name = this.processOptions[e.detail.value];
// 切换工序时清空接收人
this.transferForm.branches[idx].assignee_id = "";
},
onBranchUserChange(idx, e) {
const user = this.userOptions[e.detail.value];
if (user) this.transferForm.branches[idx].assignee_id = user.id;
},
isBranchWarehouse(idx) {
return this.transferForm.branches[idx].task_name === "🏭 入库 (virtual_warehouse)";
},
branchUserName(idx) {
const u = this.userOptions.find(u => u.id === this.transferForm.branches[idx].assignee_id);
return u ? u.name : "";
}, },
async doTransfer() { async doTransfer() {
this.actionLoading = true; this.actionLoading = true;
const finalAssignees = this.isWarehouse const branches = this.transferForm.branches.filter(b => b.task_name);
? ["virtual_warehouse"]
: [...this.transferForm.assignees];
try { try {
await post(`/tasks/${this.actionPopup.task.id}/transfer`, { await post(`/tasks/${this.actionPopup.task.id}/transfer`, {
next_assignees: finalAssignees, next_tasks: branches.map(b => ({
next_task_name: this.transferForm.next_task_name.trim(), task_name: b.task_name,
assignees: b.task_name === "🏭 入库 (virtual_warehouse)"
? ["virtual_warehouse"] : [b.assignee_id],
})),
note: this.transferForm.note.trim() || undefined, note: this.transferForm.note.trim() || undefined,
}); });
uni.showToast({ title: this.isWarehouse ? "已入库" : "转交成功", icon: "success" }); uni.showToast({ title: `已派发 ${branches.length} 个分支`, icon: "success" });
this.closeActionPopup(); this.closeActionPopup();
this.doQuery(this.product.serial_number); this.doQuery(this.product.serial_number);
} catch {} finally { this.actionLoading = false; } } catch {} finally { this.actionLoading = false; }
@ -793,6 +834,14 @@ export default {
.warehouse-hint { font-size: 13px; background: #ede9fe; color: #7c3aed; padding: 10px 14px; border-radius: 10px; margin: 8px 0; text-align: center; } .warehouse-hint { font-size: 13px; background: #ede9fe; color: #7c3aed; padding: 10px 14px; border-radius: 10px; margin: 8px 0; text-align: center; }
.preview-hint { font-size: 12px; background: #f0fdf4; color: #16a34a; padding: 8px 10px; border-radius: 8px; margin: 6px 0; } .preview-hint { font-size: 12px; background: #f0fdf4; color: #16a34a; padding: 8px 10px; border-radius: 8px; margin: 6px 0; }
/* 多分支裂变 */
.branch-item { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 12px; margin-bottom: 10px; }
.branch-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
.branch-label { font-size: 13px; font-weight: 700; color: #374151; }
.branch-del { font-size: 12px; color: #ef4444; font-weight: 600; padding: 2px 8px; }
.btn-add-branch { width: 100%; height: 40px; border: 2px dashed #93c5fd; border-radius: 10px; background: #eff6ff; color: #2563eb; font-size: 14px; font-weight: 700; line-height: 40px; margin: 4px 0; }
.btn-add-branch::after { border: none; }
/* Picker & 表单 */ /* Picker & 表单 */
.field-label { font-size: 14px; font-weight: 600; color: #374151; margin-top: 10px; margin-bottom: 4px; } .field-label { font-size: 14px; font-weight: 600; color: #374151; margin-top: 10px; margin-bottom: 4px; }
.required { color: #ef4444; } .required { color: #ef4444; }
@ -826,6 +875,7 @@ export default {
.footer-transfer { background: #dcfce7; color: #16a34a; } .footer-transfer { background: #dcfce7; color: #16a34a; }
.footer-record { background: #eff6ff; color: #2563eb; } .footer-record { background: #eff6ff; color: #2563eb; }
.footer-receive { background: #dbeafe; color: #1d4ed8; } .footer-receive { background: #dbeafe; color: #1d4ed8; }
.footer-reject { background: #fce4ec; color: #dc2626; }
/* Picker 选择器 */ /* Picker 选择器 */
.form-item { margin: 10px 0; } .form-item { margin: 10px 0; }