diff --git a/src/pages/dispatch/dispatch.vue b/src/pages/dispatch/dispatch.vue
index 83c4b24..e57881b 100644
--- a/src/pages/dispatch/dispatch.vue
+++ b/src/pages/dispatch/dispatch.vue
@@ -7,6 +7,7 @@
排车拣货与发车
客户与当前海外仓由登录令牌和服务端确定;扫描排车单号或任务中的托盘码开始作业
+ 当前任务排车拣货详情
@@ -25,11 +26,12 @@
- 待处理排车任务
+ 待处理排车任务{{ tasks.length }} 个任务
暂无排车拣货任务可刷新列表,或直接扫描排车单号恢复作业。
@@ -40,7 +42,6 @@
{{ current.dispatchNo || '—' }}{{ statusLabel(current.status) }}
任务号 {{ current.taskNo || '—' }} · 车牌 {{ current.vehiclePlate || '—' }}
{{ plannedPackages }}计划箱数{{ pickedPackages }}已拣箱数{{ remainingPackages }}待拣箱数
-
@@ -95,8 +96,9 @@
export default {
components: { PortalHeader, ScanTargetInput },
- data() { return { user: session.currentUser, tasks: [], current: null, scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '' } },
+ data() { return { user: session.currentUser, tasks: [], current: null, scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false, resumeOnEntry: false } },
computed: {
+ recentTaskNo() { return fulfillment.dispatch.currentNo || '' },
packages() { return fulfillment.dispatch.packages },
selectedItem() { return this.current?.items?.find((item) => item.palletNo === fulfillment.dispatch.palletCode) || null },
selectedPosition() { return this.selectedItem?.positions?.find((item) => item.availablePackages > 0) || this.selectedItem?.positions?.[0] || null },
@@ -112,12 +114,19 @@
canComplete() { return this.plannedPackages > 0 && this.remainingPackages === 0 },
canDepart() { return this.pickingCompleted && this.allPicked && this.current?.canDepart === true && !this.departed }
},
+ onLoad(options) { this.resumeOnEntry = options?.resume === '1' },
onShow() {
if (!requireSession()) return
this.user = session.currentUser
restoreFulfillmentDraft(`${this.user?.id || ''}:${this.user?.warehouseId || ''}`)
this.startScanner()
- if (fulfillment.dispatch.currentNo) this.openTask(fulfillment.dispatch.currentNo)
+ if (!this.bootstrapped) {
+ this.bootstrapped = true
+ if (this.resumeOnEntry && fulfillment.dispatch.currentNo) this.openTask(fulfillment.dispatch.currentNo)
+ else this.loadTasks()
+ return
+ }
+ if (this.current?.dispatchNo) this.openTask(this.current.dispatchNo)
else this.loadTasks()
},
onHide() { this.stopScanner() },
diff --git a/src/pages/dispatch/fulfillment.scss b/src/pages/dispatch/fulfillment.scss
index d8b5522..a9cc34e 100644
--- a/src/pages/dispatch/fulfillment.scss
+++ b/src/pages/dispatch/fulfillment.scss
@@ -21,13 +21,22 @@
.offline-note { margin-top: 7px; color: #8a5a00; }
.list-toolbar, .task-head, .count-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.list-toolbar { margin: 16px 2px 10px; color: #233951; font-size: 13px; font-weight: 800; }
+ .list-title, .list-count, .detail-kicker, .detail-title, .task-enter { display: block; }
+ .list-count { margin-top: 2px; color: #8795a7; font-size: 10px; font-weight: 500; }
.list-toolbar button { min-height: 32px; margin: 0; padding: 0 12px; border-radius: 7px; color: #135ee8; font-size: 11px; line-height: 32px; background: #e9f2ff; }
.task-list, .item-list { display: flex; flex-direction: column; gap: 9px; }
.task-card { box-sizing: border-box; width: 100%; min-height: 76px; margin: 0; padding: 13px; border: 1px solid #dce5f0; border-radius: 11px; text-align: left; background: #fff; }
.task-card::after { border: 0; }
+ .task-code-row { display: flex; min-width: 0; align-items: center; gap: 7px; }
.business-code { color: #16304d; font-size: 15px; font-weight: 800; overflow-wrap: anywhere; }
+ .recent-badge { flex-shrink: 0; padding: 3px 6px; border-radius: 99px; color: #8a570c; font-size: 9px; line-height: 14px; background: #fff1dd; }
.status-badge { flex-shrink: 0; padding: 4px 7px; border-radius: 99px; color: #165fbf; font-size: 10px; background: #e6f0ff; }
.task-meta { margin-top: 6px; color: #718095; font-size: 10px; line-height: 16px; }
+ .task-enter { margin-top: 8px; color: #135ee8; font-size: 10px; font-weight: 700; text-align: right; }
+ .detail-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 4px 0 10px; padding: 10px 12px; border: 1px solid #bed4f6; border-radius: 11px; background: #eef5ff; }
+ .detail-kicker { color: #6580a4; font-size: 9px; }
+ .detail-title { margin-top: 2px; color: #153d74; font-size: 14px; font-weight: 800; }
+ .detail-toolbar button { flex-shrink: 0; min-height: 34px; margin: 0; padding: 0 11px; border-radius: 8px; color: #135ee8; font-size: 10px; line-height: 34px; background: #fff; }
.summary-card { display: block; }
.metrics { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); margin-top: 12px; padding: 10px 0; border-radius: 9px; background: #f5f8fc; }
.metrics view { display: flex; align-items: center; flex-direction: column; gap: 3px; color: #718095; font-size: 9px; }
diff --git a/src/pages/operation/operation.vue b/src/pages/operation/operation.vue
index 52de268..fda78eb 100644
--- a/src/pages/operation/operation.vue
+++ b/src/pages/operation/operation.vue
@@ -181,7 +181,7 @@
photoStateLabel(photo) { if (photo?.status === 'uploading') return '正在上传…'; if (photo?.status === 'error') return photo.errorMessage || (photo.fileId ? '重拍失败,已保留原图' : '上传失败,请重试'); return photo?.fileId ? '点击左侧照片可查看大图' : '请拍摄现场照片' },
startScanner() { this.stopScanner(); this._stopScanner = startScanBroadcast({ onScan: (value) => this.handleHardwareScan(value), onError: (error) => { this.error = error; this.showToast('硬件扫码接收器异常,请检查扫描服务', 'warning') } }) }, stopScanner() { if (typeof this._stopScanner === 'function') this._stopScanner(); this._stopScanner = null },
handleHardwareScan(value) { const scan = { value: String(value || '').trim(), target: this.activeTarget }; if (!scan.value) return; if (!this.inboundMode) return this.showToast('该功能缺少海外 PDA 专用接口,扫码未提交', 'warning'); if (this.phase === 'candidates') return this.showToast('请直接点击待卸货集装箱进入详情', 'warning'); if (this.phase !== 'receive') return; if (this.busy || this.activeTarget === 'quantity') { if (this.scanQueue.length >= 100) return this.showToast('扫码队列已满,请先处理当前托盘', 'warning'); this.scanQueue.push(scan); return this.showToast(`已排队 ${this.scanQueue.length} 个扫码`, 'warning') } return this.dispatchHardwareScan(scan) },
- dispatchHardwareScan(scan) { console.info('[Overseas PDA scan] dispatch', { value: scan.value, target: scan.target }); if (scan.target === 'pallet' && this.activeInbound) { this.palletInput = scan.value; return this.resolvePallet(scan.value) } this.cartonInput = scan.value; return this.resolveCarton(scan.value) },
+ dispatchHardwareScan(scan) { const palletWorkflowActive = Boolean(this.activeInbound || this.resolvedPallet); const target = palletWorkflowActive ? 'pallet' : scan.target; console.info('[Overseas PDA scan] dispatch', { value: scan.value, target, queuedTarget: scan.target }); if (target === 'pallet') { this.palletInput = scan.value; return this.resolvePallet(scan.value) } this.cartonInput = scan.value; return this.resolveCarton(scan.value) },
drainScanQueue() { if (this.busy || this.activeTarget === 'quantity' || !this.scanQueue.length || this.phase !== 'receive') return; const next = this.scanQueue.shift(); this.dispatchHardwareScan(next) },
async run(action, work) { this.busy = true; this.error = null; try { const result = await work(); this.showToast(`${action}成功`, 'success'); return result } catch (error) { const mappedError = toReceivingError(error, `${action}失败`); this.error = mappedError; playErrorSound(); this.showToast(mappedError.message, 'error'); throw mappedError } finally { this.busy = false; setTimeout(() => this.drainScanQueue(), 0) } },
setScanTargetForCurrent(value) { receiving.activeScanTarget = value?.status === 'WAIT_SIGN' ? 'container' : 'carton' },
diff --git a/src/pages/outbound/outbound.vue b/src/pages/outbound/outbound.vue
index a455740..b1dd1c8 100644
--- a/src/pages/outbound/outbound.vue
+++ b/src/pages/outbound/outbound.vue
@@ -4,6 +4,7 @@
出库任务一件代发扫描托盘码可整托出库;扫描排车业务号会转入“排车拣货”
+ 当前任务出库作业详情
@@ -14,15 +15,15 @@
{{ error.message }}错误码:{{ error.code }}requestId:{{ error.requestId }}traceId:{{ error.traceId }}网络连接失败,当前任务、扫码草稿和幂等键均已保留。
- 待处理出库任务
+ 待处理出库任务{{ tasks.length }} 个任务
-
+
暂无出库任务可刷新列表,或扫描任务号恢复未完成作业。
- {{ current.taskNo || '—' }}{{ statusLabel(current.status) }}出库单 {{ current.outboundNo || '—' }} · {{ taskType(current) }}{{ plannedPackages }}计划箱数{{ pickedPackages }}已拣箱数{{ remainingPackages }}待拣箱数
+ {{ current.taskNo || '—' }}{{ statusLabel(current.status) }}出库单 {{ current.outboundNo || '—' }} · {{ taskType(current) }}{{ plannedPackages }}计划箱数{{ pickedPackages }}已拣箱数{{ remainingPackages }}待拣箱数
@@ -57,8 +58,9 @@
export default {
components: { PortalHeader, ScanTargetInput },
- data() { return { user: session.currentUser, tasks: [], current: null, selectedItem: null, selectedPosition: null, scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '' } },
+ data() { return { user: session.currentUser, tasks: [], current: null, selectedItem: null, selectedPosition: null, scanCode: '', loading: false, busy: false, busyAction: '', error: null, successMessage: '', bootstrapped: false } },
computed: {
+ recentTaskNo() { return fulfillment.outbound.currentTaskNo || '' },
packages() { return fulfillment.outbound.packages },
plannedPackages() { return (this.current?.items || []).reduce((total, item) => total + item.plannedPackages, 0) },
pickedPackages() { return (this.current?.items || []).reduce((total, item) => total + item.pickedPackages, 0) },
@@ -67,7 +69,7 @@
canPick() { return this.current?.status !== 'COMPLETED' && this.selectedItem && this.selectedPosition && Number(this.packages) > 0 && Number(this.packages) <= this.selectedRemaining },
canComplete() { return this.plannedPackages > 0 && this.remainingPackages === 0 && this.current?.status !== 'COMPLETED' }
},
- onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreFulfillmentDraft(`${this.user?.id || ''}:${this.user?.warehouseId || ''}`); this.scanCode = fulfillment.outbound.scanCode; this.startScanner(); if (fulfillment.outbound.currentTaskNo) this.openTask(fulfillment.outbound.currentTaskNo); else this.loadTasks() },
+ onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreFulfillmentDraft(`${this.user?.id || ''}:${this.user?.warehouseId || ''}`); this.startScanner(); if (!this.bootstrapped) { this.bootstrapped = true; this.scanCode = ''; this.loadTasks(); return } if (this.current?.taskNo) this.openTask(this.current.taskNo); else this.loadTasks() },
onHide() { this.stopScanner() },
beforeUnmount() { this.stopScanner() },
methods: {
@@ -83,7 +85,7 @@
if (resolution?.workflow === 'DISPATCH' && resolution.dispatchNo) {
updateFulfillmentDraft('outbound', { currentTaskNo: '', scanCode: '', packages: 1 })
updateFulfillmentDraft('dispatch', { currentNo: resolution.dispatchNo, palletCode: '', packages: 1 })
- uni.navigateTo({ url: '/pages/dispatch/dispatch' })
+ uni.navigateTo({ url: '/pages/dispatch/dispatch?resume=1' })
return
}
if (resolution?.workflow !== 'OUTBOUND' || !resolution.taskNo) throw new Error('服务端未返回可执行的出库任务')
diff --git a/src/pages/relabel/relabel.vue b/src/pages/relabel/relabel.vue
index b68298c..f3923d0 100644
--- a/src/pages/relabel/relabel.vue
+++ b/src/pages/relabel/relabel.vue
@@ -3,13 +3,13 @@
{{ title }}{{ stageSubtitle }}
-
+ 当前任务换标执行详情
{{ candidateHeading }}
正在加载换标任务…
{{ emptyText }}
- {{ item.relabelNo }}客户订单:{{ item.customerOrderNo || '—' }}换标类型:{{ relabelTypeName(item.operationType) }}{{ statusLabel(item.taskStatus || item.applicationStatus) }}{{ item.expectedPackages }}应处理{{ item.relabelledPackages }}已换标{{ item.skippedPackages + item.exceptionPackages }}跳过/异常执行 ›
+ {{ item.relabelNo }}客户订单:{{ item.customerOrderNo || '—' }}换标类型:{{ relabelTypeName(item.operationType) }}{{ statusLabel(item.taskStatus || item.applicationStatus) }}{{ item.expectedPackages }}应处理{{ item.relabelledPackages }}已换标{{ item.skippedPackages + item.exceptionPackages }}跳过/异常查看并执行 ›
{{ item.relabelNo }}客户订单:{{ item.customerOrderNo || '—' }}换标类型:{{ relabelTypeName(item.operationType) }}{{ statusLabel(item.taskStatus || item.applicationStatus) }}{{ item.expectedPackages }}应处理{{ item.relabelledPackages }}已换标{{ item.skippedPackages + item.exceptionPackages }}跳过/异常
@@ -91,7 +91,7 @@
const isConflict = (error) => ['409', 'A0409', 'B0409'].includes(String(error?.code || ''))
export default {
components: { PortalHeader, ScanTargetInput, RelabelPalletWork },
- data() { return { user: session.currentUser, stage: 'receive', unitDrafts: {}, taskTab: 'sku', candidates: [], current: null, selectedItem: null, returnSkuCode: '', verifiedReturnSkuKey: '', bulkQuantities: {}, activeTarget: 'task', loading: false, busy: false, busyAction: '', error: null, successMessage: '', exceptionCode: 'OTHER', actionModes: [{ key: 'RELABEL', name: '正常换标' }, { key: 'SKIP', name: '跳过' }, { key: 'EXCEPTION', name: '异常' }], exceptionCodes: [{ key: 'DAMAGED', name: '破损' }, { key: 'MISSING_LABEL', name: '缺标签' }, { key: 'LABEL_MISMATCH', name: '标签不符' }, { key: 'UNREADABLE', name: '无法识别' }, { key: 'OTHER', name: '其他' }] } },
+ data() { return { user: session.currentUser, stage: 'receive', entryTaskNo: '', unitDrafts: {}, taskTab: 'sku', candidates: [], current: null, selectedItem: null, returnSkuCode: '', verifiedReturnSkuKey: '', bulkQuantities: {}, activeTarget: 'task', loading: false, busy: false, busyAction: '', error: null, successMessage: '', exceptionCode: 'OTHER', actionModes: [{ key: 'RELABEL', name: '正常换标' }, { key: 'SKIP', name: '跳过' }, { key: 'EXCEPTION', name: '异常' }], exceptionCodes: [{ key: 'DAMAGED', name: '破损' }, { key: 'MISSING_LABEL', name: '缺标签' }, { key: 'LABEL_MISMATCH', name: '标签不符' }, { key: 'UNREADABLE', name: '无法识别' }, { key: 'OTHER', name: '其他' }] } },
computed: {
isReturnBulk() { return this.current?.operationType === 'RETURN_BULK_SKU_LABEL' },
returnBulkSkuGroups() {
@@ -151,7 +151,7 @@
canSubmitUnit() { if (!this.selectedItem || !this.visibleItems.some((item) => item.itemId === this.selectedItem.itemId) || !this.oldLabel.trim()) return false; if (this.action === 'RELABEL') return this.cartonVerified && this.skuVerified; return Boolean(this.reason.trim()) },
unitActionText() { return ({ RELABEL: '确认本箱换标完成', SKIP: '确认跳过本箱', EXCEPTION: '登记本箱异常' })[this.action] }
},
- onLoad(options) { this.stage = STAGES[options.op] || 'receive' },
+ onLoad(options) { this.stage = STAGES[options.op] || 'receive'; this.entryTaskNo = String(options.task || '').trim() },
mixins: [{ data() { return { documents: [], quickCarton: '', lastCarton: null, quickSku: '', lastSku: '', skuChoices: [], photoUrls: {}, photoErrors: {} } } }],
watch: {
'current.reviewedAt'(value) { if (this.current?.reviewDecision === 'REWORK' && warehouseWork.relabel.reworkRound !== `${this.current.relabelNo}:${value}`) { this.unitDrafts = {}; this.selectedItem = null; this.lastCarton = null; this.quickCarton = ''; updateWarehouseWork('relabel', { reworkRound: `${this.current.relabelNo}:${value}`, photoFileIds: [], photoNames: [], remark: '' }) } },
@@ -262,7 +262,7 @@
handleHardwareScan(value) { if (this.activeTarget === 'return-sku') { this.verifyReturnSku(value); return } if (this.activeTarget === 'quick-sku') { this.quickSku = value; this.submitSku(value); return } if (this.activeTarget === 'old') { this.oldLabel = value; return } this.taskCode = value; this.openTask(value) },
handleOldLabel(value) { this.oldLabel = String(value?.detail?.value ?? value ?? '').trim(); if (this.action === 'RELABEL') this.activeTarget = this.taskTab === 'sku' ? 'sku-new-0' : 'new' },
handleNewLabel(value) { this.newLabel = String(value?.detail?.value ?? value ?? '').trim() },
- async bootstrap() { await this.loadCandidates(); if (this.error || this.stage !== 'execute') return; const owned = this.candidates.filter((item) => item.taskStatus === 'PROCESSING' && item.claimedByCurrentOperator === true); const target = owned.find((item) => item.relabelNo === this.taskCode) || (owned.length === 1 ? owned[0] : null); if (target) await this.openTask(target.relabelNo, false) },
+ async bootstrap() { await this.loadCandidates(); if (this.error || this.stage !== 'execute' || !this.entryTaskNo) return; const target = this.candidates.find((item) => item.relabelNo === this.entryTaskNo && item.taskStatus === 'PROCESSING' && item.claimedByCurrentOperator === true); this.entryTaskNo = ''; if (target) await this.openTask(target.relabelNo, false) },
async loadCandidates() { if (this.loading) return; this.loading = true; this.error = null; try { this.candidates = await listRelabelCandidates() } catch (error) { this.setError(error, '换标任务加载失败') } finally { this.loading = false } },
async openTask(raw, sound = true) { const no = String(raw?.detail?.value ?? raw ?? this.taskCode).trim(); if (!no || this.busy) return; this.taskCode = no; await this.run('detail', async () => { this.applyCurrent(await getRelabelTask(no)); if (sound) playSuccessSound() }, false) },
applyCurrent(value) { const changed = this.current?.relabelNo !== value.relabelNo; this.current = value; this.taskCode = value.relabelNo; if (changed) { this.unitDrafts = {}; this.documents = []; this.quickCarton = ''; this.lastCarton = null; this.returnSkuCode = ''; this.verifiedReturnSkuKey = ''; this.bulkQuantities = {}; if (warehouseWork.relabel.photoTaskNo !== value.relabelNo) updateWarehouseWork('relabel', { photoTaskNo: value.relabelNo, photoFileIds: [], photoNames: [], remark: '' }); this.changeTab(value.items.some((item) => item.operationType === 'SKU_LABEL') ? 'sku' : 'carton'); this.loadDocuments() } if (this.selectedItem) this.selectedItem = this.visibleItems.find((item) => item.itemId === this.selectedItem.itemId) || null },
@@ -286,7 +286,7 @@
}))
if (!confirmed) return
this.taskCode = active.relabelNo
- uni.redirectTo({ url: '/pages/relabel/relabel?op=relabelExecute' })
+ uni.redirectTo({ url: `/pages/relabel/relabel?op=relabelExecute&task=${encodeURIComponent(active.relabelNo)}` })
} finally { this.busy = false }
},
async openExecution(item) {
@@ -298,16 +298,17 @@
const active = this.candidates.find((candidate) => candidate.taskStatus === 'PROCESSING' && candidate.claimedByCurrentOperator === true)
if (active && active.relabelNo !== item.relabelNo) return this.promptActiveTask(active)
this.taskCode = item.relabelNo
- if (item.taskStatus === 'PROCESSING' && item.claimedByCurrentOperator) return uni.redirectTo({ url: '/pages/relabel/relabel?op=relabelExecute' })
+ if (item.taskStatus === 'PROCESSING' && item.claimedByCurrentOperator) return uni.redirectTo({ url: `/pages/relabel/relabel?op=relabelExecute&task=${encodeURIComponent(item.relabelNo)}` })
const payload = { relabelNo: item.relabelNo }; const key = warehouseIdempotencyKey('relabel', 'claim', payload); let claimed = false
await this.run('claim', async () => { this.applyCurrent(await claimRelabelTask(item.relabelNo, { idempotencyKey: key.value })); releaseWarehouseKey('relabel', key.signature); claimed = true }, true, key)
- if (claimed) uni.redirectTo({ url: '/pages/relabel/relabel?op=relabelExecute' })
+ if (claimed) uni.redirectTo({ url: `/pages/relabel/relabel?op=relabelExecute&task=${encodeURIComponent(item.relabelNo)}` })
},
async submitUnit() { if (!this.canSubmitUnit || this.busy) return; const base = { itemId: this.selectedItem.itemId, oldLabel: this.oldLabel.trim(), expectedVersion: this.current.version }; const payload = { relabelNo: this.current.relabelNo, action: this.action, ...base, newLabel: this.newLabel.trim(), skuVerifications: this.unitDraft.skuVerifications.map((sku) => ({ ...sku, skuCode: sku.skuCode.trim(), newSkuCode: sku.newSkuCode.trim() })), reason: this.reason.trim(), exceptionCode: this.exceptionCode }; const key = warehouseIdempotencyKey('relabel', 'unit', payload); await this.run('unit', async () => { let value; if (this.action === 'SKIP') value = await skipRelabelUnit(this.current.relabelNo, { ...base, reason: this.reason.trim(), idempotencyKey: key.value }); else if (this.action === 'EXCEPTION') value = await reportRelabelException(this.current.relabelNo, { ...base, exceptionCode: this.exceptionCode, reason: this.reason.trim(), idempotencyKey: key.value }); else value = await relabelUnit(this.current.relabelNo, { ...base, newLabel: this.newLabel.trim(), skuVerifications: payload.skuVerifications, idempotencyKey: key.value }); releaseWarehouseKey('relabel', key.signature); this.applyCurrent(value); this.oldLabel = ''; this.newLabel = ''; this.reason = ''; this.activeTarget = 'old'; this.successMessage = '本箱处理结果已保存' }, true, key) },
choosePhoto() { if (this.busy || !this.current || this.photoFileIds.length >= 20) return; const no = this.current.relabelNo; uni.chooseImage({ count: Math.min(9, 20 - this.photoFileIds.length), sourceType: ['camera', 'album'], success: async ({ tempFilePaths, tempFiles }) => { if (this.current?.relabelNo !== no) return; await this.run('photo', async () => { for (const [index, path] of (tempFilePaths || []).entries()) { const selected = tempFiles?.[index]?.file || tempFiles?.[index]; if (Number(tempFiles?.[index]?.size || 0) > 10 * 1024 * 1024) throw new Error('照片不能超过 10MB'); const uploadSource = typeof Blob !== 'undefined' && selected instanceof Blob ? selected : path; const file = await uploadRelabelPhoto(no, uploadSource); this.photoUrls[String(file.fileId)] = path; updateWarehouseWork('relabel', { photoTaskNo: no, photoFileIds: [...this.photoFileIds, String(file.fileId)], photoNames: [...this.photoNames, file.fileName || '换标现场照片'] }) } this.successMessage = '现场照片已上传' }, false) } }) },
async uploadPhoto(path) { await this.run('photo', async () => { const file = await uploadRelabelPhoto(this.current.relabelNo, path); updateWarehouseWork('relabel', { photoFileIds: [...this.photoFileIds, String(file.fileId)], photoNames: [...this.photoNames, file.fileName || '换标现场照片'] }); this.successMessage = '现场照片已上传' }, false) },
async completeTask() { if (this.busy || !this.photoFileIds.length || (!this.allProcessed && !this.remark.trim())) return; const payload = { photoFileIds: [...this.photoFileIds], expectedVersion: this.current.version, remark: this.remark.trim(), manualCompletion: !this.allProcessed }; const no = this.current.relabelNo; if (payload.manualCompletion) { const confirmed = await new Promise((resolve) => uni.showModal({ title: '手动完成换标', content: '未处理箱将登记异常,关联尾程订单将标为异常。确认提交?', success: (result) => resolve(result.confirm), fail: () => resolve(false) })); if (!confirmed || this.current?.relabelNo !== no || this.current.version !== payload.expectedVersion) return } const key = warehouseIdempotencyKey('relabel', 'complete', { relabelNo: no, ...payload }); let completed = false; await this.run('complete', async () => { await completeRelabelTask(no, { ...payload, idempotencyKey: key.value }); releaseWarehouseKey('relabel', key.signature); updateWarehouseWork('relabel', { currentNo: '', photoFileIds: [], photoNames: [], remark: '' }); completed = true }, true, key); if (completed) this.goToClaimList() },
goToClaimList() { this.taskCode = ''; uni.redirectTo({ url: '/pages/relabel/relabel?op=relabelReceive' }) },
+ closeCurrent() { this.current = null; this.selectedItem = null; this.error = null; this.successMessage = ''; this.activeTarget = 'task'; this.taskCode = '' },
async review(decision) { if (!this.current || this.current.taskStatus !== 'WAIT_REVIEW') return; const payload = { relabelNo: this.current.relabelNo, decision, expectedVersion: this.current.version, remark: this.remark.trim() }; const key = warehouseIdempotencyKey('relabel', 'review', payload); await this.run('review', async () => { this.applyCurrent(await reviewRelabelTask(this.current.relabelNo, { decision, expectedVersion: this.current.version, idempotencyKey: key.value, remark: this.remark.trim() || undefined })); releaseWarehouseKey('relabel', key.signature); this.remark = ''; this.successMessage = decision === 'APPROVE' ? '换标复核已通过' : '换标任务已退回重做'; await this.loadCandidates() }, true, key) },
async retry() { if (this.current?.relabelNo) await this.openTask(this.current.relabelNo, false); else await this.loadCandidates() },
async reloadCurrent() { if (this.current?.relabelNo) this.applyCurrent(await getRelabelTask(this.current.relabelNo)) },
@@ -319,6 +320,6 @@
}
-
diff --git a/tests/fulfillment-workflows.test.mjs b/tests/fulfillment-workflows.test.mjs
index 045796a..068f965 100644
--- a/tests/fulfillment-workflows.test.mjs
+++ b/tests/fulfillment-workflows.test.mjs
@@ -170,6 +170,20 @@ test('pages expose loading empty error offline and permission diagnostics with g
assert.doesNotMatch(dispatch + outbound, /DeliveryOrder|deliveryStatus|customerVisit|\/deliveries/)
})
+test('fulfillment workbench entries open task lists before details and keep explicit cross-workflow resume', async () => {
+ const [dispatch, outbound] = await Promise.all([read('../src/pages/dispatch/dispatch.vue'), read('../src/pages/outbound/outbound.vue')])
+ assert.match(dispatch, /resumeOnEntry: false/)
+ assert.match(dispatch, /onLoad\(options\) \{ this\.resumeOnEntry = options\?\.resume === '1' \}/)
+ assert.match(dispatch, /if \(this\.resumeOnEntry && fulfillment\.dispatch\.currentNo\) this\.openTask/)
+ assert.match(outbound, /if \(!this\.bootstrapped\) \{ this\.bootstrapped = true; this\.scanCode = ''; this\.loadTasks\(\); return \}/)
+ assert.match(outbound, /pages\/dispatch\/dispatch\?resume=1/)
+ for (const page of [dispatch, outbound]) {
+ assert.match(page, /detail-toolbar/)
+ assert.match(page, /上次作业/)
+ assert.match(page, /返回列表/)
+ }
+})
+
test('dispatch vehicle-plate conflict uses actionable Chinese copy and preserves diagnostics', () => {
const error = toDispatchError({
code: 'A0409',
diff --git a/tests/project-contract.test.mjs b/tests/project-contract.test.mjs
index 2b3b443..74a6c91 100644
--- a/tests/project-contract.test.mjs
+++ b/tests/project-contract.test.mjs
@@ -53,6 +53,8 @@ test('login has no committed demo credentials and inbound operation implements t
assert.match(runtime, /#ifdef H5[\s\S]*?VITE_H5_AUTH_API_BASE_URL/)
assert.match(runtime, /#ifdef H5[\s\S]*?VITE_H5_OVERSEAS_API_BASE_URL/)
assert.match(operation, /startScanBroadcast/)
+ assert.match(operation, /palletWorkflowActive = Boolean\(this\.activeInbound \|\| this\.resolvedPallet\)/)
+ assert.match(operation, /queuedTarget: scan\.target/)
for (const action of ['prepareReceivingContainer', 'getCurrentReceiving', 'resolveCurrentCarton', 'resolveCurrentPallet', 'receiveCurrentBulk', 'receiveCurrentCarton', 'receiveCurrentWholePallet', 'completeCurrentReceiving']) assert.match(operation, new RegExp(action))
for (const path of ['/candidates', '/prepare', '/unload-photos/files', '/sign', '/cartons/resolve', '/pallets/resolve', '/receipts/bulk', '/receipts/carton', '/receipts/pallet', '/complete']) assert.match(inbound, new RegExp(path.replaceAll('/', '\\/')))
assert.match(inbound, /current\('\/pallets\/resolve'\)/)
diff --git a/tests/relabel-active-task.test.mjs b/tests/relabel-active-task.test.mjs
index e2080f8..3c0486a 100644
--- a/tests/relabel-active-task.test.mjs
+++ b/tests/relabel-active-task.test.mjs
@@ -19,7 +19,7 @@ test('conflicting task opens modal and confirm resumes owned task without claimi
assert.equal(calls[0].cancelText, '取消')
assert.match(calls[0].content, /RL-ACTIVE/)
assert.equal(context.taskCode, 'RL-ACTIVE')
- assert.equal(calls[1].url, '/pages/relabel/relabel?op=relabelExecute')
+ assert.equal(calls[1].url, '/pages/relabel/relabel?op=relabelExecute&task=RL-ACTIVE')
assert.equal(context.error, null)
assert.equal(context.busy, false)
})
diff --git a/tests/warehouse-workflows.test.mjs b/tests/warehouse-workflows.test.mjs
index 391a12c..aa18a44 100644
--- a/tests/warehouse-workflows.test.mjs
+++ b/tests/warehouse-workflows.test.mjs
@@ -107,9 +107,10 @@ test('relabel list opens a row directly in execution and completion returns to t
assert.doesNotMatch(page, /name="relabelTask"/)
assert.match(page, /@tap="openExecution\(item\)"/)
assert.match(page, /async openExecution\(item\)/)
- assert.match(page, /uni\.redirectTo\(\{ url: '\/pages\/relabel\/relabel\?op=relabelExecute' \}\)/)
+ assert.match(page, /relabelExecute&task=\$\{encodeURIComponent\(item\.relabelNo\)\}/)
assert.match(page, /当前账号正在执行 \$\{active\.relabelNo\}/)
- assert.match(page, /v-if="stage === 'execute'" class="work-card">