优化
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { overseasRequest, overseasUpload } from './overseas.js'
|
||||
const base = '/pda/v1/last-leg/return-receipts/exceptions'
|
||||
const path = (number) => `${base}/${encodeURIComponent(number)}`
|
||||
export const listExceptionReturns = () => overseasRequest('GET', base)
|
||||
export const startExceptionReturn = (number) => overseasRequest('POST', `${path(number)}/start`, {})
|
||||
export const uploadExceptionReturnEvidence = (number, filePath) => overseasUpload(`${path(number)}/evidence`, filePath)
|
||||
export const receiveExceptionReturn = (number, body) => overseasRequest('POST', `${path(number)}/receive`, body)
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<scroll-view class="work-scroll" scroll-y><view class="portal-content work-content">
|
||||
<view v-if="error" class="work-card"><text role="alert">{{ error }}</text><button :disabled="busy" @tap="refresh">刷新任务</button></view>
|
||||
<template v-if="!current">
|
||||
<view class="row-head"><text class="card-title">异常退货入库</text><button :disabled="busy" @tap="refresh">刷新</button></view>
|
||||
<text v-if="!busy && !tasks.length" class="empty-copy">当前仓没有待验收异常退货任务</text>
|
||||
<view v-for="task in tasks" :key="task.exceptionNo" class="work-card">
|
||||
<text class="business-code">{{ task.exceptionNo }}</text>
|
||||
<text class="meta-line">车牌号:{{ task.vehiclePlate || '未登记' }}</text>
|
||||
<text class="meta-line">客户:{{ task.customerName || '—' }} · 待收 {{ task.packageCount - task.receivedPackages }} 箱</text>
|
||||
<button class="primary-button" :disabled="busy" @tap="start(task)">开始作业</button>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="work-card"><text class="business-code">{{ current.order.exceptionNo }}</text><text class="meta-line">车牌号:{{ selectedTask.vehiclePlate || '未登记' }}</text><button :disabled="busy" @tap="leave">返回任务列表</button></view>
|
||||
<view v-for="(item,index) in current.returnSources" :key="`${item.outboundPalletItemId}:${item.inventoryPositionId}:${item.returnCartonId}`" class="work-card">
|
||||
<text class="card-title">{{ item.skuCode || '货物' }}</text><text class="meta-line">原托盘 / 箱唛:{{ item.cartonMark || item.palletNo || '—' }}</text>
|
||||
<text class="meta-line">剩余可收 {{ item.maxReturnPackages }} 箱</text>
|
||||
<input v-model="quantities[index]" type="number" placeholder="本批实收箱数" :disabled="busy" />
|
||||
</view>
|
||||
<view class="work-card"><text class="card-title">上架位置</text><input v-model="palletCode" placeholder="目标托盘号" :disabled="busy" /><input v-model="locationCode" placeholder="目标库位号" :disabled="busy" /></view>
|
||||
<view class="work-card"><text class="card-title">实收照片</text><text v-for="photo in photos" :key="photo.fileId" class="meta-line">{{ photo.fileName }}</text><button :disabled="busy || photos.length >= 20" @tap="photo">拍照 / 选择照片</button></view>
|
||||
<button class="primary-button" :disabled="busy || !ready" @tap="receive">{{ busy ? '处理中…' : '确认本批入库' }}</button>
|
||||
</template>
|
||||
<text v-if="message" class="meta-line" role="status">{{ message }}</text>
|
||||
</view></scroll-view>
|
||||
</template>
|
||||
<script>
|
||||
import { listExceptionReturns, startExceptionReturn, uploadExceptionReturnEvidence, receiveExceptionReturn } from '@/api/exception-returns.js'
|
||||
export default {
|
||||
data() { return { tasks: [], current: null, selectedTask: null, quantities: [], palletCode: '', locationCode: '', photos: [], busy: false, error: '', message: '', revision: 0 } },
|
||||
computed: {
|
||||
ready() { return this.current && this.palletCode.trim() && this.locationCode.trim() && this.photos.length && this.quantities.some(value => Number(value) > 0) },
|
||||
},
|
||||
mounted() { this.refresh() },
|
||||
beforeUnmount() { this.revision++ },
|
||||
methods: {
|
||||
// 页面切换后不再用迟到响应覆盖另一张作业单。
|
||||
async run(action) { if (this.busy) return; const revision = this.revision; this.busy = true; this.error = ''; try { await action(() => revision === this.revision) } catch (error) { if (revision === this.revision) this.error = error.message || '操作失败,请刷新核对实际进度' } finally { if (revision === this.revision) this.busy = false } },
|
||||
async refresh() { await this.run(async valid => { const tasks = await listExceptionReturns(); if (valid()) this.tasks = tasks }) },
|
||||
async start(task) { await this.run(async valid => { const value = await startExceptionReturn(task.exceptionNo); if (valid()) { this.current = value; this.selectedTask = task; this.quantities = []; this.photos = []; this.palletCode = ''; this.locationCode = ''; this.message = '' } }) },
|
||||
async leave() { if (this.busy) return; this.current = null; this.photos = []; await this.refresh() },
|
||||
async photo() { await this.run(async valid => { const result = await new Promise((resolve,reject) => uni.chooseImage({ count: 1, sourceType: ['camera','album'], success: resolve, fail: reject })); const uploaded = await uploadExceptionReturnEvidence(this.current.order.exceptionNo,result.tempFilePaths[0]); if (valid()) this.photos.push(uploaded) }) },
|
||||
async receive() {
|
||||
if (!this.ready) return
|
||||
await this.run(async valid => {
|
||||
const items = this.current.returnSources.flatMap((item,index) => {
|
||||
const packages = Number(this.quantities[index] || 0)
|
||||
if (!Number.isSafeInteger(packages) || packages < 0 || packages > item.maxReturnPackages || item.returnCartonId && packages > 1) throw new Error('实收箱数必须为整数且不超过剩余可收数量')
|
||||
return packages ? [{ outboundPalletItemId: item.outboundPalletItemId, inventoryPositionId: item.inventoryPositionId, returnCartonId: item.returnCartonId, packages }] : []
|
||||
})
|
||||
const number = this.current.order.exceptionNo
|
||||
const result = await receiveExceptionReturn(number,{ expectedVersion: this.current.order.version, idempotencyKey: `exception-return-${Date.now()}-${Math.random().toString(36).slice(2)}`, palletCode: this.palletCode.trim(), locationCode: this.locationCode.trim(), items, evidenceFileIds: this.photos.map(photo => photo.fileId) })
|
||||
if (!valid()) return
|
||||
this.quantities = []; this.photos = []; this.message = `本批已入库 ${result.restoredPackages} 箱${result.status === 'COMPLETED' ? ',异常处理完成' : ',还有货物待收'}`
|
||||
if (result.status === 'COMPLETED') { this.current = null; this.tasks = await listExceptionReturns() }
|
||||
else this.current = await startExceptionReturn(number)
|
||||
})
|
||||
// 结果不明时重新读取权威版本,禁止使用旧快照重复累计数量。
|
||||
if (this.error && this.current) {
|
||||
try { this.current = await startExceptionReturn(this.current.order.exceptionNo); this.quantities = [] }
|
||||
catch (_) { this.current = null; this.tasks = []; }
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.work-card { margin: 12px 0; padding: 14px; border: 1px solid #dfe7ef; border-radius: 12px; background: #fff; }
|
||||
.business-code,.meta-line,.card-title { display: block; margin-bottom: 10px; }
|
||||
.business-code,.card-title { font-weight: 700; }.meta-line { color: #61758b; font-size: 13px; }
|
||||
input { height: 44px; margin: 8px 0; padding: 0 10px; border: 1px solid #cbd7e4; border-radius: 8px; }
|
||||
button { margin-top: 10px; font-size: 14px; }.primary-button { color: #fff; background: #1668dc; }.primary-button[disabled] { background: #a2b2c8; }
|
||||
</style>
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<view v-if="user" class="page-shell portal-page work-page">
|
||||
<PortalHeader title="退货入库" :user="user" />
|
||||
<scroll-view class="work-scroll" scroll-y><view class="portal-content work-content">
|
||||
<view class="row-head" style="padding: 10px"><button :disabled="busy" @tap="switchDomain('exception')">异常退货入库</button><button :disabled="busy" @tap="switchDomain('customer')">客户退货入库</button></view>
|
||||
<ExceptionReturnWork v-if="receiptDomain === 'exception'" />
|
||||
<scroll-view v-if="receiptDomain === 'customer'" class="work-scroll" scroll-y><view class="portal-content work-content">
|
||||
<view v-if="!current && !result" class="work-card">
|
||||
<text class="card-title">扫描 SKU 69 码</text>
|
||||
<ScanTargetInput name="lookup" v-model="lookupCode" label="SKU 69 码" placeholder="扫描商品 69 码,自动带出退货单号" :active="activeTarget === 'lookup'" @activate="activeTarget = 'lookup'" @submit="lookup" />
|
||||
@@ -59,7 +61,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PortalHeader from '@/components/PortalHeader.vue'
|
||||
import ExceptionReturnWork from '@/components/ExceptionReturnWork.vue'
|
||||
import PortalHeader from '@/components/PortalHeader.vue'
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import { checkReturnPallet, confirmReturnReceipt, createReturnPallet, downloadReturnPalletPhoto, findReturnSkuCandidates, manuallyCompleteReturnReceipt, startReturnReceipt, uploadReturnPalletPhoto } from '@/api/return-receipt.js'
|
||||
import { session, requireSession } from '@/stores/session.js'
|
||||
@@ -68,8 +71,8 @@
|
||||
import { playErrorSound, playSuccessSound } from '@/utils/audio-feedback.js'
|
||||
|
||||
export default {
|
||||
components: { PortalHeader, ScanTargetInput },
|
||||
data() { return { user: session.currentUser, current: null, candidates: null, result: null, receiptResults: [], selectedSku: '', palletCheck: null, newPalletHtml: '', unitType: 'PALLET', unitCode: '', boxRows: [{ items: {}, length: '', width: '', height: '', weight: '', originalPackaging: true, confirmed: false }], palletLength: '', palletWidth: '', palletHeight: '', palletWeight: '', unitOptions: [{ value: 'PALLET', label: '托盘' }, { value: 'BOX', label: '箱子' }, { value: 'PARCEL', label: '包裹' }], photoFileIds: [], photoNames: [], photoUrls: {}, activeTarget: 'lookup', busy: false, error: null } },
|
||||
components: { PortalHeader, ScanTargetInput, ExceptionReturnWork },
|
||||
data() { return { receiptDomain: 'exception', user: session.currentUser, current: null, candidates: null, result: null, receiptResults: [], selectedSku: '', palletCheck: null, newPalletHtml: '', unitType: 'PALLET', unitCode: '', boxRows: [{ items: {}, length: '', width: '', height: '', weight: '', originalPackaging: true, confirmed: false }], palletLength: '', palletWidth: '', palletHeight: '', palletWeight: '', unitOptions: [{ value: 'PALLET', label: '托盘' }, { value: 'BOX', label: '箱子' }, { value: 'PARCEL', label: '包裹' }], photoFileIds: [], photoNames: [], photoUrls: {}, activeTarget: 'lookup', busy: false, error: null } },
|
||||
computed: {
|
||||
entryMode: { get() { return warehouseWork.returnReceipt.entryMode }, set(value) { updateWarehouseWork('returnReceipt', { entryMode: value }) } },
|
||||
lookupCode: { get() { return warehouseWork.returnReceipt.lookupCode }, set(value) { updateWarehouseWork('returnReceipt', { lookupCode: value }) } },
|
||||
@@ -86,9 +89,10 @@
|
||||
unitPlaceholder() { return this.unitType === 'PALLET' ? '扫描托盘标签' : this.unitType === 'BOX' ? '扫描箱唛或录入箱号' : '扫描包裹面单或录入包裹码' }
|
||||
,palletVolume() { const l = Number(this.palletLength) || 0; const w = Number(this.palletWidth) || 0; const h = Number(this.palletHeight) || 0; return l > 0 && w > 0 && h > 0 ? (l * w * h).toFixed(2) : '—' }
|
||||
},
|
||||
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); this.entryMode = 'SKU'; this.startScanner(); if (warehouseWork.returnReceipt.currentReturnNo && !this.current && !this.busy) this.openOrder(warehouseWork.returnReceipt.currentReturnNo, 'SKU', false) },
|
||||
onShow() { if (!requireSession()) return; this.user = session.currentUser; restoreWarehouseWork(`${this.user.id}:${this.user.warehouseId}`); this.entryMode = 'SKU'; if (this.receiptDomain === 'customer') this.startScanner(); if (this.receiptDomain === 'customer' && warehouseWork.returnReceipt.currentReturnNo && !this.current && !this.busy) this.openOrder(warehouseWork.returnReceipt.currentReturnNo, 'SKU', false) },
|
||||
onHide() { this.stopScanner() }, onUnload() { this.stopScanner() }, beforeUnmount() { this.stopScanner() },
|
||||
methods: {
|
||||
switchDomain(value) { if (this.busy) return; this.stopScanner(); this.receiptDomain = value; if (value === 'customer') this.startScanner() },
|
||||
goCount() { const mark = this.result?.labels?.[0]?.cartonMark; if (mark) uni.navigateTo({ url: '/pages/return-counting/return-counting?cartonMark=' + encodeURIComponent(mark) }) },
|
||||
selectUnitType(type) { if (this.busy) return; this.unitType = type; this.unitCode = ''; this.boxRows = [{ items: {}, length: '', width: '', height: '', weight: '', originalPackaging: true, confirmed: false }]; this.palletCheck = null; this.activeTarget = type === 'PALLET' ? 'pallet' : 'unit' },
|
||||
resizeBoxRows(raw) { if (this.busy) return; const count = Math.floor(Number(raw) || 0); if (count < 1 || count > 100) return; this.boxRows = Array.from({ length: count }, (_, index) => this.boxRows[index] || { items: {}, length: '', width: '', height: '', weight: '', originalPackaging: true, confirmed: false }) },
|
||||
|
||||
@@ -111,7 +111,7 @@ test('editing the draft before the camera returns prevents late photo submission
|
||||
})
|
||||
|
||||
function setup(confirm, saved = new Map(), scope = 'operator:warehouse', runtime = {}) {
|
||||
const context = vm.createContext({ reactive: value => value, ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {},
|
||||
const context = vm.createContext({ reactive: value => value, ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {}, ExceptionReturnWork: {},
|
||||
playSuccessSound() {}, playErrorSound() {}, confirmReturnReceipt: confirm,
|
||||
...runtime,
|
||||
uni: { setStorageSync: (key, value) => saved.set(key, structuredClone(value)), getStorageSync: key => saved.get(key), removeStorageSync: key => saved.delete(key), ...runtime.uni } })
|
||||
@@ -135,7 +135,7 @@ function setup(confirm, saved = new Map(), scope = 'operator:warehouse', runtime
|
||||
test('SKU lookup displays all pending order lines while retaining scanned SKU for pallet generation', () => {
|
||||
const source = readFileSync(new URL('../src/pages/return-receipt/return-receipt.vue', import.meta.url), 'utf8')
|
||||
const script = source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1]
|
||||
const component = vm.runInNewContext(script.replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {} })
|
||||
const component = vm.runInNewContext(script.replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {}, ExceptionReturnWork: {} })
|
||||
const lines = [{ skuCode: 'SKU-A', expectedQuantity: 6 }, { skuCode: 'SKU-B', expectedQuantity: 4 }]
|
||||
const instance = { current: { expectedItems: lines }, selectedSku: 'SKU-A' }
|
||||
instance.selectedItems = component.computed.selectedItems.call(instance)
|
||||
@@ -168,7 +168,7 @@ test('PALLET page submits the required palletCode through the actual confirmatio
|
||||
|
||||
test('parcel signing allows an empty code but still rejects quantities above the remaining amount', async () => {
|
||||
const source = readFileSync(new URL('../src/pages/return-receipt/return-receipt.vue', import.meta.url), 'utf8')
|
||||
const component = vm.runInNewContext(source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {} })
|
||||
const component = vm.runInNewContext(source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {}, ExceptionReturnWork: {} })
|
||||
const state = { current: {}, selectedItems: [{ skuCode: 'A' }], receiptQuantity: 2, unitType: 'PARCEL', boxRows: [physicalRow({ A: 2 })], remaining: () => 3 }
|
||||
assert.equal(component.computed.canConfirm.call(state), true)
|
||||
state.boxRows[0].items.A = 4
|
||||
@@ -205,7 +205,7 @@ test('batch waybill scans stay on the single parcel waybill input', () => {
|
||||
|
||||
test('parcel rows must be nonempty and summed quantities cannot exceed the order remainder', () => {
|
||||
const source = readFileSync(new URL('../src/pages/return-receipt/return-receipt.vue', import.meta.url), 'utf8')
|
||||
const component = vm.runInNewContext(source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {} })
|
||||
const component = vm.runInNewContext(source.match(/<script[^>]*>([\s\S]*?)<\/script>/)[1].replace(/^\s*import .*$/mg, '').replace('export default', 'globalThis.component ='), { ReturnLabelConfirmation: {}, PortalHeader: {}, ScanTargetInput: {}, ExceptionReturnWork: {} })
|
||||
const state = { current: {}, selectedItems: [{ skuCode: 'A' }], receiptQuantity: 4, unitType: 'PARCEL', boxRows: [physicalRow({ A: 2 }), physicalRow({ A: 2 })], remaining: () => 3 }
|
||||
assert.equal(component.computed.canConfirm.call(state), false)
|
||||
state.remaining = () => 5
|
||||
|
||||
Reference in New Issue
Block a user