diff --git a/track-uniapp/src/pages/scan/components/TaskTreeNode.vue b/track-uniapp/src/pages/scan/components/TaskTreeNode.vue
index b3657c8..d9dcbbf 100644
--- a/track-uniapp/src/pages/scan/components/TaskTreeNode.vue
+++ b/track-uniapp/src/pages/scan/components/TaskTreeNode.vue
@@ -128,13 +128,8 @@ export default {
uni.previewImage({ urls: fullUrls, current: index });
},
confirmDelete(rec) {
- uni.showModal({
- title: "删除记录",
- content: "确定删除这条记录吗?",
- success: (res) => {
- if (res.confirm) this.$emit("action", { task: this.task, type: "deleteRecord", record: rec });
- },
- });
+ // 🚀 交给父页面统一做「双重确认倒计时」,避免原生 showModal 取消行为异常
+ this.$emit("action", { task: this.task, type: "deleteRecord", record: rec });
},
statusLabel(s) {
const map = { PENDING: "待接收", WIP: "进行中", COMPLETED: "已完成", REJECTED: "已驳回", ARCHIVED: "已入库" };
diff --git a/track-uniapp/src/pages/scan/detail.vue b/track-uniapp/src/pages/scan/detail.vue
index 13f9128..53573f5 100644
--- a/track-uniapp/src/pages/scan/detail.vue
+++ b/track-uniapp/src/pages/scan/detail.vue
@@ -96,7 +96,7 @@
备注 *
-
+
@@ -110,7 +110,7 @@
⏳
-
+
@@ -127,13 +127,13 @@
@tap="receiveTaskName = opt">{{ opt }}
-
+
-
+
@@ -148,7 +148,7 @@
交接备注 *
{{ transferForm.isWarehouse ? '产品将入库并从个人待办中移除' : '将创建新任务指派给 ' + (transferUserName || '—') }}
-
+
@@ -161,7 +161,7 @@
派发备注 *
-
+
@@ -196,6 +196,19 @@
+
+
+
+
+
@@ -224,6 +237,11 @@ export default {
actionPopup: { visible: false, type: "", task: null }, actionLoading: false, rejectReason: "", receiveRemark: "", receiveTaskName: "",
transferForm: { selectedUserId: "", isWarehouse: false, note: "" },
spawnForm: { assignee_id: "", remark: "" },
+ // 🛡️ 双重确认倒计时:避免误触
+ confirming: "", // 当前倒计时中的操作 key('' = 无)
+ confirmCount: 5, // 剩余秒数
+ confirmTimer: null, // 定时器句柄
+ confirmDlg: { visible: false, title: "", content: "", action: null }, // 确认框类操作弹窗
// 💬 留言板
messages: [],
// 🚀 字典版本号:驱动子组件强制重建,解决 userNameMap 非响应式问题
@@ -236,7 +254,7 @@ export default {
},
computed: {
userLabels() { return this.users.map(u => `${u.full_name} (${u.username})`); },
- canDeleteImage() { if (!this.recordPopup.task) return true; if (!this.currentUser) return true; const frozen = ["COMPLETED","REJECTED","ARCHIVED","CANCELED"]; if (frozen.includes(this.recordPopup.task.status)) return false; return this.currentUser.username === this.recordPopup.task.assignee_id; },
+ canDeleteImage() { if (!this.recordPopup.task) return true; if (!this.currentUser) return true; const frozen = ["COMPLETED","REJECTED","ARCHIVED","CANCELED"]; if (frozen.includes(this.recordPopup.task.status)) return false; const assignee = this.recordPopup.task.assignee_id; return assignee == this.currentUserId || assignee == this.currentUsername || (this.currentUser && this.currentUser.id == assignee) || (this.currentUser && this.currentUser.username == assignee); },
transferUserName() { const u = this.userOptions.find(u => u.id === this.transferForm.selectedUserId); return u ? u.name : ""; },
spawnUserName() { const u = this.userOptions.find(u => u.id === this.spawnForm.assignee_id); return u ? u.name : ""; },
userGridOptions() { return (this.userOptions || []).map(u => ({ id: u.id, name: u.name })); },
@@ -271,6 +289,8 @@ export default {
onLoad(options) { this.loadUsers(); this.loadCurrentUser(); const sn = options.serial || ""; if (sn) { this.doQuery(sn); return; } /* 🚀 兜底: 从 taskId 反查 product */ const tid = options.taskId || ""; if (tid) this.doQueryByTask(tid); },
// 🚀 onShow 生命周期:每次页面显示时刷新留言板(解决从聊天室退回不更新问题)
onShow() { if (this.product?.id) { this.fetchMessages(); } },
+ // 🚀 页面卸载:清理确认倒计时定时器,避免泄漏
+ onUnload() { this.clearConfirm(); },
methods: {
formatUserName, formatUserAvatar,
formatName(name) { if (!name) return ""; return name.length === 2 ? name[0] + " " + name[1] : name; },
@@ -317,7 +337,7 @@ export default {
closeRecordPopup() { this.recordPopup = { visible: false, task: null }; },
async handleChooseImage() { const maxSlots = 9 - (this.recordForm.images.length + this.recordForm.pendingCount); if (maxSlots <= 0) return; const chooseRes = await new Promise((resolve, reject) => { uni.chooseImage({ count: maxSlots, sizeType: ["compressed"], sourceType: ["camera", "album"], success: resolve, fail: reject }); }).catch(() => null); if (!chooseRes || !chooseRes.tempFilePaths || !chooseRes.tempFilePaths.length) return; let compressSkipCount = 0; const compressedPaths = []; for (const p of chooseRes.tempFilePaths) { try { const compressed = await new Promise((resolve, reject) => { uni.compressImage({ src: p, quality: 60, success: resolve, fail: reject }); }); compressedPaths.push(compressed.tempFilePath); } catch { compressSkipCount++; } } if (!compressedPaths.length) return; this.isUploading = true; this.recordForm.pendingCount += compressedPaths.length; for (const path of compressedPaths) { const url = await this.uploadFile(path); if (url) this.recordForm.images.push(url); this.recordForm.pendingCount--; } this.isUploading = false; },
uploadFile(filePath) { return new Promise((resolve) => { uni.uploadFile({ url: getBaseUrl() + "/upload/", filePath, name: "file", success(res) { try { const data = JSON.parse(res.data); resolve(data.url || null); } catch { resolve(null); } }, fail: () => resolve(null) }); }); },
- removeRecordImage(i) { this.recordForm.images.splice(i, 1); },
+ removeRecordImage(i) { this.openConfirmDlg({ title: "删除图片", content: "确定删除这张图片吗?", action: () => { this.recordForm.images.splice(i, 1); } }); },
previewRecordImage(i) { uni.previewImage({ urls: this.recordForm.images, current: i }); },
async doSaveRecord() { if (this.isUploading) return; this.recordSaving = true; try { const payload = { remark: this.recordForm.remark.trim(), images: this.recordForm.images }; if (this.recordForm.recordId) await put(`/records/${this.recordForm.recordId}`, payload); else await patch(`/tasks/${this.recordPopup.task.id}/records`, payload); uni.showToast({ title: "已保存", icon: "success" }); this.closeRecordPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.recordSaving = false; } },
@@ -332,12 +352,68 @@ export default {
this.spawnForm = { assignee_id: "", remark: "" };
},
handleViewRecords(task) { uni.navigateTo({ url: `/pages/scan/records?taskId=${task.id}` }); },
- async doDeleteRecord(record) { try { await request({ url: `/records/${record.id}`, method: "DELETE" }); uni.showToast({ title: "记录已删除", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
- confirmEndBranch(task) { uni.showModal({ title: "结束分支", content: `确定结束「${task.task_name}」吗?`, success: (res) => { if (res.confirm) this.doEndBranch(task); } }); },
- confirmRecall(task) { uni.showModal({ title: "撤回转交", content: `确定撤回「${task.task_name}」吗?撤回后任务将回到您的手中。`, success: (res) => { if (res.confirm) this.doRecall(task); } }); },
+ async doDeleteRecord(record) { this.openConfirmDlg({ title: "删除记录", content: "确定删除这条记录吗?删除后不可恢复。", action: async () => { try { await request({ url: `/records/${record.id}`, method: "DELETE" }); uni.showToast({ title: "记录已删除", icon: "success" }); this.doQuery(this.product.serial_number); } catch (e) { uni.showToast({ title: (e && e.data && e.data.detail) || "删除失败", icon: "none" }); } } }); },
+ confirmEndBranch(task) { this.openConfirmDlg({ title: "结束分支", content: `确定结束「${task.task_name}」吗?`, action: () => this.doEndBranch(task) }); },
+ confirmRecall(task) { this.openConfirmDlg({ title: "撤回转交", content: `确定撤回「${task.task_name}」吗?撤回后任务将回到您的手中。`, action: () => this.doRecall(task) }); },
async doRecall(task) { try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${task.id}/recall?operator_id=${encodeURIComponent(opId)}`); uni.showToast({ title: "已撤回", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
async doEndBranch(task) { try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${task.id}/end?operator_id=${encodeURIComponent(opId)}`); uni.showToast({ title: "分支已结束", icon: "success" }); this.doQuery(this.product.serial_number); } catch {} },
closeActionPopup() { this.actionPopup = { visible: false, type: "", task: null }; },
+
+ // ═══ 双重确认倒计时(防误触) ═══
+ // 第一次点击进入 5 秒倒计时(期间再次点击 = 取消反悔),倒计时结束再点击才真正执行
+ confirmBtn(key, doAction) {
+ if (this.confirming === key) {
+ if (this.confirmCount <= 0) {
+ this.clearConfirm();
+ doAction();
+ } else {
+ // 倒计时中再次点击 → 取消(反悔)
+ this.clearConfirm();
+ }
+ return;
+ }
+ this.clearConfirm();
+ this.confirming = key;
+ this.confirmCount = 5;
+ this.confirmTimer = setInterval(() => {
+ this.confirmCount -= 1;
+ if (this.confirmCount <= 0) clearInterval(this.confirmTimer);
+ }, 1000);
+ },
+ clearConfirm() {
+ if (this.confirmTimer) clearInterval(this.confirmTimer);
+ this.confirmTimer = null;
+ this.confirming = "";
+ this.confirmCount = 5;
+ },
+ confirmLabel(key, baseText) {
+ if (this.confirming === key) return this.confirmCount > 0 ? `再次确认 (${this.confirmCount}s)` : "确认执行";
+ return baseText;
+ },
+ // 确认框类操作(结束分支/删除记录/撤回转交):弹倒计时确认弹窗
+ openConfirmDlg({ title, content, action }) {
+ this.clearConfirm();
+ this.confirmDlg = { visible: true, title, content, action };
+ this.confirming = "dlg";
+ this.confirmCount = 5;
+ this.confirmTimer = setInterval(() => {
+ this.confirmCount -= 1;
+ if (this.confirmCount <= 0) clearInterval(this.confirmTimer);
+ }, 1000);
+ },
+ confirmDlgConfirm() {
+ this.confirmBtn("dlg", () => {
+ const action = this.confirmDlg.action;
+ this.confirmDlg.visible = false;
+ this.confirmDlg.action = null;
+ if (action) action();
+ });
+ },
+ confirmDlgCancel() {
+ this.clearConfirm();
+ this.confirmDlg.visible = false;
+ this.confirmDlg.action = null;
+ },
async doReceive() { this.actionLoading = true; try { const remark = this.receiveRemark.trim() || undefined; const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/receive?operator_id=${encodeURIComponent(opId)}`, { remark, task_name: this.receiveTaskName }); uni.showToast({ title: "已接收", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
async doReject() { this.actionLoading = true; try { const opId = this.currentUsername || this.currentUserId; await post(`/tasks/${this.actionPopup.task.id}/reject?operator_id=${encodeURIComponent(opId)}`, { reason: this.rejectReason.trim() }); uni.showToast({ title: "已驳回", icon: "success" }); this.closeActionPopup(); this.doQuery(this.product.serial_number); } catch {} finally { this.actionLoading = false; } },
// 转交 — 互斥选择
@@ -435,6 +511,8 @@ export default {
.btn-primary[disabled] { opacity: 0.5; }
.btn-danger { flex: 1; height: 42px; border: none; border-radius: 10px; background: #dc2626; color: #fff; font-size: 14px; font-weight: 600; line-height: 42px; }
.btn-danger[disabled] { opacity: 0.5; }
+.btn-counting { background: #f59e0b !important; }
+.cd-tip { display: block; text-align: center; font-size: 12px; color: #b45309; background: #fffbeb; border: 1px solid #fcd34d; border-radius: 8px; padding: 6px 10px; margin: 8px 0 0; line-height: 1.4; }
.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; }