From 105c7825b49f5558ed4b5f83ae161a0762ceecfc Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 26 Oct 2026 19:21:15 +0800 Subject: [PATCH 01/25] fix(dispatch): synchronize loading evidence and review round validation --- src/api/dispatch-loading-documents.js | 10 +++-- src/components/DispatchLoadingDocuments.vue | 28 ++++++++++-- tests/dispatch-loading-documents.test.mjs | 50 +++++++++++++++++++-- 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/api/dispatch-loading-documents.js b/src/api/dispatch-loading-documents.js index 1894179..3c9757e 100644 --- a/src/api/dispatch-loading-documents.js +++ b/src/api/dispatch-loading-documents.js @@ -13,7 +13,7 @@ function slotPath(no, slot) { return `${path(no)}/${slot}` } export function normalizeLoadingDocuments(value, no) { - if (value?.dispatchNo !== no || !/^\d+$/.test(String(value.recordVersion ?? '')) || !Array.isArray(value.photos) + if (value?.dispatchNo !== no || !/^\d+$/.test(String(value.recordVersion ?? '')) || !/^[1-9]\d*$/.test(String(value.reviewRound ?? '')) || !Array.isArray(value.photos) || !['LOADING', 'AFTER_LOADING', 'DELIVERING', 'DELIVERED'].includes(value.stage)) throw new Error('装车资料与当前排车事实不一致') const files = new Set(), counts = {} for (const photo of value.photos) { @@ -24,6 +24,10 @@ export function normalizeLoadingDocuments(value, no) { return { ...value, editable: value.editable === true, complete: value.complete === true } } export async function getLoadingDocuments(no) { return normalizeLoadingDocuments(await overseasRequest('GET', path(no)), no) } -export function uploadLoadingPhoto(no, slot, filePath, key) { return overseasUpload(`${slotPath(no, slot)}/uploads?idempotencyKey=${encodeURIComponent(key)}`, filePath) } +export function uploadLoadingPhoto(no, slot, filePath, key, reviewRound) { return overseasUpload(`${slotPath(no, slot)}/uploads?idempotencyKey=${encodeURIComponent(key)}&reviewRound=${encodeURIComponent(reviewRound)}`, filePath) } export async function bindLoadingPhoto(no, slot, body) { return normalizeLoadingDocuments(await overseasRequest('PUT', slotPath(no, slot), body), no) } -export function readLoadingPhoto(no, photo) { return overseasDownload(`${slotPath(no, photo.slot)}/files/${encodeURIComponent(photo.fileId)}/content`, { expectedContentTypePrefix: 'image/' }) } +export function readLoadingPhoto(no, photo) { + const pdf = photo.slot === 'LOADING_SCENE' && photo.contentType === 'application/pdf' + if (!pdf && !['image/jpeg', 'image/png'].includes(photo.contentType)) throw new Error('装车资料格式无效') + return overseasDownload(`${slotPath(no, photo.slot)}/files/${encodeURIComponent(photo.fileId)}/content`, { expectedContentTypePrefix: pdf ? 'application/pdf' : photo.contentType }) +} diff --git a/src/components/DispatchLoadingDocuments.vue b/src/components/DispatchLoadingDocuments.vue index 464b612..276606a 100644 --- a/src/components/DispatchLoadingDocuments.vue +++ b/src/components/DispatchLoadingDocuments.vue @@ -3,9 +3,10 @@ 装车照片 {{ busy ? '同步中' : snapshot ? `${completedCategories}/5 类已保存` : '待同步' }} - + 每类至少 1 张、最多 5 张,上传后自动保存,可逐张更换。 + 司机驾驶证、车牌照片在理货审核通过后自动带入,可补拍或更换;PDF 驾驶证可点击查看。 保存状态尚未核实,请先刷新资料后再上传。 {{ error.message }}requestId:{{ error.requestId }}traceId:{{ error.traceId }} @@ -19,7 +20,8 @@ - + + 查看大图 @@ -48,6 +50,7 @@ export default { key() { return `loading-${Date.now()}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}` }, async previews() { await Promise.allSettled((this.snapshot?.photos || []).map(async photo => { + if (photo.contentType === 'application/pdf') return if (this.images[photo.fileId]) return const image = await readLoadingPhoto(this.number, photo); this.ensureScope(); this.images[photo.fileId] = image })) @@ -58,8 +61,10 @@ export default { this.snapshot = value; this.needsRefresh = false; await this.previews() }, async load() { await this.run(() => this.refreshFacts()) }, + async refresh() { await this.run(async () => { await this.refreshFacts(); return true }, true) }, async choose(slot, replaceFileId = null) { if (this.busy || !this.editable) return + const reviewRound = this.snapshot.reviewRound const capacity = replaceFileId ? 1 : this.limit - this.photosFor(slot).length if (capacity <= 0) return let saved = 0, reconciled = false @@ -70,12 +75,14 @@ export default { if (chosen.tempFiles?.some(file => file.size > 10 * 1024 * 1024)) throw new Error('请选择不超过10MiB的图片') // 逐张以最新版本绑定;部分失败保留前面已保存照片,不重放失败上传。 for (const filePath of paths) { - const photo = await uploadLoadingPhoto(this.number, slot, filePath, this.key()); this.ensureScope() + const uploadKey = this.key() + const photo = await uploadLoadingPhoto(this.number, slot, filePath, uploadKey, reviewRound); this.ensureScope() if (photo?.slot !== slot || !photo.fileId) throw new Error('上传结果与装车照片框不一致') const latest = await getLoadingDocuments(this.number); this.ensureScope(); this.snapshot = latest + if (latest.reviewRound !== reviewRound) { reconciled = true; throw new Error('审核轮次已变化,请核对新车辆、司机后重新选择资料') } if (!this.editable) throw new Error('当前任务不可修改装车照片,请刷新任务查看') try { - const value = await bindLoadingPhoto(this.number, slot, { recordVersion: latest.recordVersion, idempotencyKey: this.key(), fileId: photo.fileId, ...(replaceFileId ? { replaceFileId } : {}) }); this.ensureScope() + const value = await bindLoadingPhoto(this.number, slot, { recordVersion: latest.recordVersion, idempotencyKey: this.key(), fileId: photo.fileId, uploadKey, ...(replaceFileId ? { replaceFileId } : {}) }); this.ensureScope() this.snapshot = value } catch (cause) { // 保存可能已成功;只重读最新事实,失败时必须先刷新再继续。 @@ -92,6 +99,19 @@ export default { if ((saved || reconciled) && this.active) this.$emit('saved') }, preview(url) { uni.previewImage({ current: url, urls: [url] }) }, + async viewPdf(photo) { + await this.run(async () => { + const browser = typeof window !== 'undefined' && typeof document !== 'undefined' + const popup = browser ? window.open('', '_blank') : null + if (browser && !popup) throw new Error('请允许打开驾驶证预览窗口') + if (popup) popup.opener = null + try { + const path = await readLoadingPhoto(this.number, photo); this.ensureScope() + if (popup) popup.location.href = path + else await new Promise((resolve, reject) => uni.openDocument({ filePath: path, fileType: 'pdf', showMenu: true, success: resolve, fail: reject })) + } catch (cause) { if (popup) popup.close(); throw cause } + }) + }, async run(work, notify = false) { if (this.busy) return const revision = ++this.revision diff --git a/tests/dispatch-loading-documents.test.mjs b/tests/dispatch-loading-documents.test.mjs index eacf9f5..1c0c540 100644 --- a/tests/dispatch-loading-documents.test.mjs +++ b/tests/dispatch-loading-documents.test.mjs @@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises' import { loadingPhotoLabels, loadingPhotoLimit, loadingPhotoSlots, normalizeLoadingDocuments, getLoadingDocuments, bindLoadingPhoto, uploadLoadingPhoto } from '../src/api/dispatch-loading-documents.js' import { session } from '../src/stores/session.js' const source = (await readFile(new URL('../src/components/DispatchLoadingDocuments.vue', import.meta.url), 'utf8')).match(/ + + diff --git a/src/pages/home/home.vue b/src/pages/home/home.vue index e82f296..b13382f 100644 --- a/src/pages/home/home.vue +++ b/src/pages/home/home.vue @@ -8,15 +8,15 @@ {{ greeting }},{{ displayName }} {{ warehouseLabel }} - {{ availableCount }}项可处理 + 作业待办 - {{ group.name }}{{ group.operations.length }} + {{ group.name }} @@ -28,8 +28,10 @@ + + diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue index a9b0f42..847bda6 100644 --- a/src/pages/operation/operation.vue +++ b/src/pages/operation/operation.vue @@ -1,5 +1,5 @@ - {{ error.message }} - code={{ error.code }}requestId={{ error.requestId }}traceId={{ error.traceId }} + {{ error.message }} + 错误码:{{ error.code }}请求号:{{ error.requestId }}跟踪号:{{ error.traceId }} @@ -147,7 +160,7 @@ export default { components: { PortalHeader, ReceivingSessionSummary, ScanTargetInput }, - data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, candidateLoading: false, busy: false, bootstrapped: false, detailOpen: false, domesticPhotosExpanded: false, cartonInput: '', palletInput: '', packageCount: '', candidateError: null, error: null, scanQueue: [], toast: { visible: false, message: '', type: '' } } }, + data() { return { user: session.currentUser, moduleKey: 'query', operationName: '库存查询', loading: false, candidateLoading: false, busy: false, bootstrapped: false, detailOpen: false, domesticPhotosExpanded: false, cartonInput: '', palletInput: '', packageCount: '', candidateError: null, error: null, candidateErrorDetailsOpen: false, errorDetailsOpen: false, scanQueue: [], toast: { visible: false, message: '', type: '' } } }, computed: { inboundMode() { return this.moduleKey === 'inbound' }, photoSteps() { return RECEIVING_PHOTO_ROLES.map((role) => ({ role, label: ({ LICENSE_PLATE: '车牌照片', CONTAINER_SEAL: '集装箱封签', DOOR_OPEN: '开柜现场' })[role] })) }, @@ -162,6 +175,7 @@ canFinishOnsite() { return Boolean(this.current && ['SIGNED', 'RECEIVING'].includes(this.current.status)) }, pageTitle() { return { candidates: '待卸货集装箱', detail: '柜号详情', sign: '拆柜拍照签收', receive: '当前柜收货', completed: '入库完成' }[this.phase] } }, + watch: { error() { this.errorDetailsOpen = false }, candidateError() { this.candidateErrorDetailsOpen = false } }, onLoad(options) { this.moduleKey = options.module || 'query'; const key = options.op || (this.moduleKey === 'inbound' ? 'receiving' : 'inventoryQuery'); this.operationName = ({ receiving: '拆柜收货', stocktake: '库存盘点', move: '商品移库', pallet: '托盘管理', relabelReceive: '换标列表', relabelExecute: '换标执行', relabelReview: '换标复核', inventoryQuery: '库存查询', locationQuery: '库位查询', palletQuery: '托盘查询' })[key] || '库存查询' }, onShow() { if (!requireSession()) return; this.user = session.currentUser; this._pageActive = true; this.startScanner(); if (this.inboundMode && (!this.bootstrapped || this.phase === 'candidates' || this._resumeReceiving)) { if (!this.busy) { this._resumeReceiving = false; this.bootstrap() } } }, onHide() { this.suspendReceiving() }, onUnload() { this.suspendReceiving() }, beforeUnmount() { this.suspendReceiving() }, @@ -250,10 +264,11 @@ diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue index 024495e..c26a904 100644 --- a/src/pages/outbound/outbound.vue +++ b/src/pages/outbound/outbound.vue @@ -2,7 +2,7 @@ - + 出库任务 当前任务出库作业详情 @@ -14,9 +14,13 @@ {{ error.message }}错误码:{{ error.code }}requestId:{{ error.requestId }}traceId:{{ error.traceId }}网络连接失败,请刷新核对任务状态后重试。