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 @@ - @@ -39,7 +38,6 @@ methods: { goBack() { navigateBackOr(this.fallbackUrl) }, goProfile() { this.closeMenu(); navigateToUnique('/pages/profile/profile') }, - goHistory() { this.closeMenu(); navigateToUnique('/pages/history/history') }, openMenu() { this.$refs.menuPopup.open() }, closeMenu() { if (this.$refs.menuPopup) this.$refs.menuPopup.close() }, handleMenuChange(event) { this.$emit('menu-change', Boolean(event.show)) } diff --git a/src/pages/dispatch/dispatch.vue b/src/pages/dispatch/dispatch.vue index 9b41a24..b5ab505 100644 --- a/src/pages/dispatch/dispatch.vue +++ b/src/pages/dispatch/dispatch.vue @@ -10,7 +10,7 @@ {{ current ? '扫码拣货' : '扫码查询排车' }} - + 正在加载排车事实… @@ -32,9 +32,7 @@ 任务摘要 - {{ current.dispatchNo }}{{ stageLabel(current) }} - 任务号 {{ current.taskNo || '—' }} - {{ current.actions.PICK?.blockReason || '拣货资格尚未允许,请核对两级审核及当前作业状态' }} + {{ stageLabel(current) }} {{ plannedPackages }}计划箱数{{ pickedPackages }}真实已拣{{ remainingPackages }}未拣箱数 @@ -185,7 +183,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) { @@ -227,7 +225,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 '未填写' @@ -288,7 +286,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(/