From 3cc77c98b5b3de63d124f9e5c9ef6820bb916402 Mon Sep 17 00:00:00 2001
From: liuyihe <2513762445@qq.com>
Date: Thu, 5 Nov 2026 16:03:22 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/DispatchLoadingDocuments.vue | 2 --
src/components/PortalHeader.vue | 2 --
src/pages/dispatch/dispatch.vue | 12 ++++----
src/pages/operation/operation.vue | 4 ++-
src/stores/receiving.js | 4 ++-
tests/receiving-lifecycle.test.mjs | 33 +++++++++++++++++++--
tests/receiving-photo-ui-contract.test.mjs | 5 ++--
7 files changed, 45 insertions(+), 17 deletions(-)
diff --git a/src/components/DispatchLoadingDocuments.vue b/src/components/DispatchLoadingDocuments.vue
index 276606a..1160a5c 100644
--- a/src/components/DispatchLoadingDocuments.vue
+++ b/src/components/DispatchLoadingDocuments.vue
@@ -5,8 +5,6 @@
{{ busy ? '同步中' : snapshot ? `${completedCategories}/5 类已保存` : '待同步' }}
- 每类至少 1 张、最多 5 张,上传后自动保存,可逐张更换。
- 司机驾驶证、车牌照片在理货审核通过后自动带入,可补拍或更换;PDF 驾驶证可点击查看。
保存状态尚未核实,请先刷新资料后再上传。
{{ error.message }}requestId:{{ error.requestId }}traceId:{{ error.traceId }}
diff --git a/src/components/PortalHeader.vue b/src/components/PortalHeader.vue
index 52b5cc8..78077e1 100644
--- a/src/components/PortalHeader.vue
+++ b/src/components/PortalHeader.vue
@@ -13,7 +13,6 @@
{{ current ? '扫码拣货' : '扫码查询排车' }}
-
+
正在加载排车事实…
@@ -32,9 +32,7 @@
任务摘要
- {{ current.dispatchNo }}{{ stageLabel(current) }}
- 任务号 {{ current.taskNo || '—' }}
- {{ current.actions.PICK?.blockReason || '拣货资格尚未允许,请核对两级审核及当前作业状态' }}
+ {{ stageLabel(current) }}
拣货进度
{{ plannedPackages }}计划箱数{{ pickedPackages }}真实已拣{{ remainingPackages }}未拣箱数
@@ -186,7 +184,7 @@ export default {
this.sourceChoices = []; this.choiceCartonCode = ''; this.error = null
},
async submitPick() { if (!this.canPick) return; try { const body = { recordVersion: requireDispatchVersion(this.current.recordVersion), positionId: this.selectedReturnCarton ? null : this.selectedPosition.positionId, packages: this.selectedReturnCarton ? 1 : Number(this.packages), productQuantity: this.selectedReturnCarton?.productQuantity || 0, cartonMark: this.selectedReturnCarton?.cartonMark || null, remark: null }; await this.submitCommand(this.current.dispatchNo, 'PICK', { ...body, dispatchItemId: this.selectedItem.dispatchItemId }) } catch (cause) { this.setError(cause, '拣货未提交') } },
- async prepareLoading() { if (!this.canComplete) return; await this.run(async () => { const data = await getDispatchLoading(this.current.dispatchNo); if (data.dispatchNo !== this.current.dispatchNo || data.pickingReconciled !== true || data.loadingComplete?.allowed !== true) throw new Error(data.loadingComplete?.blockReason || '装车前置未满足'); requireDispatchVersion(data.recordVersion); this.loadingPreview = data; this.loadingRows = data.pallets }) },
+ async prepareLoading() { if (!this.canComplete || this.busy || this.loading) return; await this.run(async () => { const data = await getDispatchLoading(this.current.dispatchNo); if (data.dispatchNo !== this.current.dispatchNo || data.pickingReconciled !== true || data.loadingComplete?.allowed !== true) throw new Error(data.loadingComplete?.blockReason || '装车前置未满足'); requireDispatchVersion(data.recordVersion); this.loadingPreview = data; this.loadingRows = data.pallets }) },
async completeLoading() { if (!this.loadingPreview || this.busy) return; try { if (!this.loadingRows.length || this.loadingRows.some(row => row.deliveryIndex == null || !Number.isInteger(row.deliveryIndex) || row.deliveryIndex < 1)) throw new Error('托盘卸货站尚未匹配,请后台核对'); await this.submitCommand(this.loadingPreview.dispatchNo, 'LOADING_COMPLETE', { recordVersion: requireDispatchVersion(this.loadingPreview.recordVersion), manifestHash: this.loadingPreview.manifestHash, remark: null }) } catch (cause) { this.setError(cause, '装车完成未确认') } },
updateReviewReason(event) { if (this.busy || this.loading) return; this.reviewReason = String(event.detail.value || '') },
async submitReview(decision) {
@@ -228,7 +226,7 @@ export default {
},
async retry() { if (this.current?.dispatchNo) await this.openTask(this.current.dispatchNo); else await this.loadTasks() },
async run(work, loading = false) { if (this.busy || this.loading) return; this.clearScanNotice(); this.error = null; this.successMessage = ''; if (loading) this.loading = true; else this.requestBusy = true; try { await work() } catch (cause) { this.setError(cause, '排车操作失败') } finally { this.loading = false; this.requestBusy = false } },
- setError(cause, fallback) { this.clearScanNotice(); this.error = toDispatchError(cause, fallback); playErrorSound() },
+ setError(cause, fallback) { this.clearScanNotice(); this.error = toDispatchError(cause, fallback); playErrorSound(); if (typeof uni !== 'undefined') uni.showToast?.({ title: this.error.message || fallback, icon: 'none', duration: 3000 }) },
stageLabel(task) { if (task.status === 'CANCELLED') return '已取消'; if (task.stage === 'BEFORE_LOADING') { if (task.actions?.LOADING_REVIEW?.allowed === true) return '待理货审核'; if ([task.reviews?.dispatchReview, task.reviews?.loadingReview].some(review => review?.status === 'REJECTED')) return '已退回客服处理' }; return ({ BEFORE_LOADING: '装车前', LOADING: '装车中', AFTER_LOADING: '装车后', DELIVERING: '派送中', DELIVERED: '派送完成' })[task.stage] || '状态待核实' },
reviewTime(value) { if (!value) return '未填写'; const date = new Date(new Date(value).getTime() + 8 * 60 * 60 * 1000); return Number.isNaN(date.getTime()) ? '时间无效' : date.toISOString().slice(0, 16).replace('T', ' ') },
reviewLabel(review) { return ({ NOT_SUBMITTED: '未提交', PENDING: '待审核', APPROVED: '已通过', REJECTED: '已驳回' })[review?.status] || '待核对' },
@@ -278,7 +276,7 @@ export default {
.review-actions button { flex: 1; min-width: 0; margin: 0; padding: 0 16rpx; min-height: 88rpx; line-height: 88rpx; font-size: 28rpx; }
.review-actions .primary-button { min-height: 88rpx; line-height: 88rpx; font-size: 28rpx; }
.review-card textarea { box-sizing: border-box; width: 100%; height: 160rpx; min-height: 160rpx; margin-top: 20rpx; padding: 16rpx; border: 1px solid #cbd5e1; border-radius: 8rpx; }
-.loading-overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, .46); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 40rpx; box-sizing: border-box; }
+.loading-overlay { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: rgba(15, 23, 42, .46); display: flex; align-items: center; justify-content: center; z-index: 100; padding: 40rpx; box-sizing: border-box; }
.loading-dialog { width: 100%; max-width: 620rpx; max-height: 88vh; overflow-y: auto; box-sizing: border-box; background: #fff; border-radius: 28rpx; padding: 36rpx 32rpx 32rpx; box-shadow: 0 24rpx 80rpx rgba(15, 23, 42, .18); }
.loading-dialog-heading { display: flex; flex-direction: column; gap: 10rpx; }
.loading-dialog-title { color: #142c49; font-size: 36rpx; font-weight: 700; line-height: 1.4; }
diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue
index bb592ea..79165da 100644
--- a/src/pages/operation/operation.vue
+++ b/src/pages/operation/operation.vue
@@ -310,7 +310,9 @@
@media(max-width:360px){.photo-role{grid-template-columns:68px minmax(0,1fr) 64px;gap:7px;padding:8px}.photo-preview{width:68px;height:64px}.photo-actions button{min-height:34px;padding:0 2px;line-height:34px}.photo-state{font-size:9px;line-height:13px}.domestic-photo-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}
.receiving-page{padding-bottom:env(safe-area-inset-bottom,0px)}
.receiving-content{box-sizing:border-box;padding-bottom:calc(24px + env(safe-area-inset-bottom,0px))}
- .receiving-content-with-action{padding-bottom:calc(152px + env(safe-area-inset-bottom,0px))}
+ .receiving-content-with-action{padding-bottom:calc(24px + env(safe-area-inset-bottom,0px))}
+ .sticky-action-dock{position:static;max-width:none;margin:12px 0 0;padding:0;background:transparent}
+ .sticky-action{padding:10px;box-shadow:none}
.primary-button,.secondary-button,.ghost-button{display:flex;box-sizing:border-box;align-items:center;justify-content:center;padding-top:0;padding-bottom:0;line-height:normal;text-align:center}
.sticky-action button{display:flex;align-items:center;justify-content:center;line-height:normal;text-align:center}
.receiving-list-page{background:#f1f5fd}
diff --git a/src/stores/receiving.js b/src/stores/receiving.js
index 6847a0d..e4a16cb 100644
--- a/src/stores/receiving.js
+++ b/src/stores/receiving.js
@@ -48,12 +48,14 @@ export function receivingProgress(session = receiving.current) {
export function setCurrentReceiving(value, owner = '') {
const nextOwner = photoOwner(value, owner)
const previous = previewOwner && previewOwner === nextOwner ? receiving.photoSlots : []
+ const keepUnsigned = receiving.current?.status === 'WAIT_SIGN' && value?.status === 'WAIT_SIGN'
previewOwner = nextOwner
receiving.current = value || null
receiving.selected = value || null
receiving.photoSlots = RECEIVING_PHOTO_ROLES.map((role) => {
const photo = (value?.photos || []).find((item) => item.role === role)
- if (!photo) return null
+ // 相机返回会重新读取 current;签收前已上传的文件尚未绑定,保留同一身份、同一柜会话的草稿。
+ if (!photo) return keepUnsigned ? previous.find(item => item?.role === role && item.fileId) || null : null
const old = previous.find(item => item?.role === role && String(item.fileId) === String(photo.fileId)
&& item.downloadPath === photo.downloadPath)
return old || { ...photo, fileId: String(photo.fileId), role, localPath: '', uploaded: true, status: 'idle' }
diff --git a/tests/receiving-lifecycle.test.mjs b/tests/receiving-lifecycle.test.mjs
index 0dba0bb..04c533c 100644
--- a/tests/receiving-lifecycle.test.mjs
+++ b/tests/receiving-lifecycle.test.mjs
@@ -2,7 +2,7 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import * as store from '../src/stores/receiving.js'
-import { normalizeReceivingSession, toReceivingError } from '../src/api/receiving-adapter.js'
+import { normalizeReceivingSession, toReceivingError, RECEIVING_PHOTO_ROLES } from '../src/api/receiving-adapter.js'
const source = readFileSync(new URL('../src/pages/operation/operation.vue', import.meta.url), 'utf8').match(/