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] 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(/