优化
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="work-card">
|
||||
<text class="card-title">箱与箱合并</text>
|
||||
<text class="card-title">包裹/箱合箱</text>
|
||||
<text v-if="plan" class="business-code">{{ plan.planNo }}</text>
|
||||
<text v-if="plan && plan.remark" class="hint plan-remark">合箱备注:{{ plan.remark }}</text>
|
||||
<view class="choice-row"><button class="secondary" @tap="choose('NEW')">{{ mode === 'NEW' ? '✓ ' : '' }}新增箱子</button><button class="secondary" @tap="choose('EXISTING')">{{ mode === 'EXISTING' ? '✓ ' : '' }}使用已有箱子</button></view>
|
||||
<view class="choice-row"><button class="secondary" @tap="choose('NEW')">{{ mode === 'NEW' ? '✓ ' : '' }}新增箱子</button><button v-if="!plan || plan.mode !== 'SKU'" class="secondary" @tap="choose('EXISTING')">{{ mode === 'EXISTING' ? '✓ ' : '' }}使用已有箱子</button></view>
|
||||
<text class="hint">{{ mode === 'NEW' ? '从下方来源箱选择 SKU 和扣除数量,完成后生成新箱唛。' : '扫描来源箱,再扫描另一只箱作为目标;目标箱原有库存不扣除。' }}</text>
|
||||
<template v-if="mode === 'EXISTING' || (mode === 'NEW' && !plan)">
|
||||
<template v-if="mode">
|
||||
<ScanTargetInput name="source" v-model="sourceCode" label="来源箱唛" placeholder="扫描来源箱唛" :active="active === 'source'" @activate="active = 'source'" @submit="scanSource" />
|
||||
<button class="secondary" :disabled="busy || !mode || !sourceCode.trim()" @tap="scanSource(sourceCode)">{{ mode === 'EXISTING' && sources.length ? '确认更换来源箱' : '确认来源箱' }}</button>
|
||||
<view v-if="mode === 'EXISTING'" class="confirmed-sources">
|
||||
@@ -36,7 +36,8 @@
|
||||
</view>
|
||||
<view v-for="box in sources" :key="box.cartonMark" class="item-card section-gap">
|
||||
<view class="row-head"><text class="business-code">{{ box.cartonMark }}</text><button v-if="!plan" class="link-button" @tap="remove(box.cartonMark)">移除</button></view>
|
||||
<view v-for="item in box.items" :key="item.skuCode" class="source-quantity"><text class="meta-line">{{ item.skuCode }} · 可扣 {{ item.quantity }} 件</text><input class="small-input" type="number" :value="deduction(box.cartonMark,item.skuCode)" placeholder="扣除数量" @input="setDeduction(box.cartonMark,item.skuCode,$event.detail.value)" /></view>
|
||||
<template v-if="!plan"><button class="secondary" @tap="printSource(box.cartonMark)">补打包裹/箱标签</button><ReturnLabelConfirmation ref="sourceLabels" :carton-mark="box.cartonMark" /></template>
|
||||
<view v-for="item in box.items" :key="item.skuCode" class="source-quantity"><text class="meta-line">{{ item.skuCode }} · 可扣 {{ item.quantity }} 件</text><input class="small-input" type="number" :disabled="plan?.mode === 'SKU'" :value="deduction(box.cartonMark,item.skuCode)" placeholder="扣除数量" @input="setDeduction(box.cartonMark,item.skuCode,$event.detail.value)" /></view>
|
||||
</view>
|
||||
<button class="primary" :disabled="!canComplete" @tap="complete">{{ busy ? '处理中…' : '确认合箱' }}</button>
|
||||
</view>
|
||||
@@ -45,16 +46,18 @@
|
||||
|
||||
<script>
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import { scanReturnCarton, completePackingPlan, mergeReturnBoxes } from '@/api/return-packing.js'
|
||||
import ReturnLabelConfirmation from '@/components/ReturnLabelConfirmation.vue'
|
||||
import { scanReturnCarton, completePackingPlan, mergeReturnBoxes, getReturnCartonLabel } from '@/api/return-packing.js'
|
||||
import { updateWarehouseWork, warehouseWork, warehouseIdempotencyKey, releaseWarehouseKey } from '@/stores/warehouse-work.js'
|
||||
export default {
|
||||
components: { ScanTargetInput },
|
||||
components: { ScanTargetInput, ReturnLabelConfirmation },
|
||||
props: { plan: { type: Object, default: null }, initialSource: { type: Object, default: null } },
|
||||
emits: ['completed', 'error', 'busy-change'],
|
||||
data() { return { mode:'', dimensions:{length:'',width:'',height:'',weight:''}, fields:[{key:'length',label:'长 cm'},{key:'width',label:'宽 cm'},{key:'height',label:'高 cm'},{key:'weight',label:'重量 kg'}], sourceCode: '', targetCode: '', sources: [], target: null, deductions: {}, active: 'source', busy: false } },
|
||||
computed: { volume() { const value = Number(this.dimensions.length) * Number(this.dimensions.width) * Number(this.dimensions.height) / 1000000; return Number.isFinite(value) && value > 0 ? value.toFixed(6) : '0.000000' }, canComplete() { return !this.busy && this.plannedItems().length > 0 && (this.mode === 'NEW' ? this.fields.every(f => Number(this.dimensions[f.key]) > 0) : !!this.target && this.target.cartonMark === this.targetCode.trim() && this.sources[0]?.cartonMark === this.sourceCode.trim()) } },
|
||||
computed: { volume() { const value = Number(this.dimensions.length) * Number(this.dimensions.width) * Number(this.dimensions.height) / 1000000; return Number.isFinite(value) && value > 0 ? value.toFixed(6) : '0.000000' }, canComplete() { return !this.busy && (!this.plan || this.plan.mode !== 'SKU' || this.plan.sourceCartonMarks.every(mark => this.sources.some(box => box.cartonMark === mark))) && this.plannedItems().length > 0 && (this.mode === 'NEW' ? this.fields.every(f => Number(this.dimensions[f.key]) > 0) : !!this.target && this.target.cartonMark === this.targetCode.trim() && this.sources[0]?.cartonMark === this.sourceCode.trim()) } },
|
||||
async mounted() {
|
||||
if (!this.plan && this.initialSource) { this.choose('EXISTING'); this.sources = [this.initialSource]; this.sourceCode = this.initialSource.cartonMark; this.active = 'target'; return }
|
||||
if (!this.plan && this.initialSource) { this.choose('NEW'); this.sources = [this.initialSource]; this.sourceCode = ''; this.active = 'source'; return }
|
||||
this.choose('NEW')
|
||||
this.targetCode = warehouseWork.returnPacking.targetCode || ''
|
||||
const marks = this.plan?.sourceCartonMarks || warehouseWork.returnPacking.sourceCartonMarks || []
|
||||
if (this.plan) {
|
||||
@@ -65,7 +68,7 @@ export default {
|
||||
} else for (const mark of marks) await this.scanSource(mark)
|
||||
},
|
||||
methods: {
|
||||
choose(mode) { if(this.busy || this.mode === mode) return; const current = !this.plan ? (this.sources[0] || this.initialSource) : null; this.mode=mode; this.sources=mode === 'NEW' && this.plan ? (this.plan.sources || []).map(box => ({...box,customerId:this.plan.customerId})) : current ? [current] : []; this.target=null; this.targetCode=''; this.sourceCode=mode === 'EXISTING' && current ? current.cartonMark : ''; this.deductions={}; this.active=mode === 'NEW' && this.plan ? '' : mode === 'EXISTING' && current ? 'target' : 'source' },
|
||||
choose(mode) { if(this.busy || this.mode === mode) return; const current = !this.plan ? (this.sources[0] || this.initialSource) : null; this.mode=mode; this.sources=current ? [current] : []; this.target=null; this.targetCode=''; this.sourceCode=mode === 'EXISTING' && current ? current.cartonMark : ''; this.deductions={}; this.active=mode === 'EXISTING' && current ? 'target' : 'source' },
|
||||
save() { updateWarehouseWork('returnPacking', { sourceCartonMarks: this.sources.map(box => box.cartonMark), targetCode: this.targetCode }) },
|
||||
remove(mark) { this.sources = this.sources.filter(box => box.cartonMark !== mark); this.save() },
|
||||
async scanSource(raw) {
|
||||
@@ -85,6 +88,7 @@ export default {
|
||||
if (this.mode === 'EXISTING') {
|
||||
this.sources = [box]; this.sourceCode = mark; this.deductions = {}; this.target = null; this.targetCode = ''
|
||||
} else { this.sources.push(box); this.sourceCode = '' }
|
||||
if (this.plan?.mode === 'SKU') this.deductions = { ...this.deductions, [mark]: Object.fromEntries((box.items || []).map(item => [item.skuCode, Number(item.quantity)])) }
|
||||
this.save(); this.active = this.mode === 'NEW' ? 'source' : 'target'
|
||||
})
|
||||
},
|
||||
@@ -111,7 +115,18 @@ export default {
|
||||
this.target = box; this.save()
|
||||
})
|
||||
},
|
||||
acceptScan(value) { if (!this.active) return; return this.active === 'source' ? this.scanSource(value) : this.scanTarget(value) },
|
||||
acceptScan(value) { if (!this.plan && this.sources.some(box => box.cartonMark === String(value).trim())) { for (const label of [].concat(this.$refs?.sourceLabels || [])) label.acceptScan(value); return } if (!this.active) return; return this.active === 'source' ? this.scanSource(value) : this.scanTarget(value) },
|
||||
async printSource(mark) {
|
||||
// #ifdef H5
|
||||
const tab = window.open('', '_blank')
|
||||
if (!tab) { this.$emit('error', new Error('请允许打开标签打印窗口')); return }
|
||||
try { const label = await getReturnCartonLabel(mark); tab.document.write(label.printableHtml); tab.document.close(); tab.focus(); tab.print() }
|
||||
catch (error) { tab.close(); this.$emit('error', error) }
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
this.$emit('error', new Error('当前设备未配置打印服务'))
|
||||
// #endif
|
||||
},
|
||||
deduction(mark, sku) { return this.deductions?.[mark]?.[sku] || '' },
|
||||
setDeduction(mark, sku, raw) {
|
||||
const box = this.sources.find(item => item.cartonMark === mark)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<view class="label-confirmation">
|
||||
<text v-if="confirmed">标签已扫码确认,可按该包裹/箱号出库</text>
|
||||
<template v-else>
|
||||
<ScanTargetInput v-model="code" label="确认已贴标签" placeholder="贴好后扫描此包裹/箱标签" @submit="confirm" />
|
||||
<button class="secondary" :disabled="busy || !code.trim()" @tap="confirm(code)">确认贴标</button>
|
||||
<text v-if="error">{{ error }}</text>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import ScanTargetInput from '@/components/ScanTargetInput.vue'
|
||||
import { confirmReturnLabel } from '@/api/return-packing.js'
|
||||
export default {
|
||||
components: { ScanTargetInput },
|
||||
props: { cartonMark: { type: String, required: true } },
|
||||
data() { return { code: '', confirmed: false, busy: false, error: '' } },
|
||||
methods: {
|
||||
acceptScan(value) { if (String(value).trim() === this.cartonMark) return this.confirm(value) },
|
||||
async confirm(raw) {
|
||||
if (this.busy || this.confirmed) return
|
||||
const mark = String(raw?.detail?.value ?? raw ?? this.code).trim()
|
||||
if (mark !== this.cartonMark) { this.error = '请扫描当前包裹/箱的标签'; return }
|
||||
this.busy = true; this.error = ''
|
||||
try { await confirmReturnLabel(mark); this.confirmed = true }
|
||||
catch (error) { this.error = error?.message || '贴标确认失败,请重试' }
|
||||
finally { this.busy = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<view class="section-gap">
|
||||
<text class="card-title">换标商品独立合箱</text><text class="hint">部分换标须装入独立新箱;来源余量留在原包裹/箱。所有来源登记成功后才可生成新箱。</text>
|
||||
<view v-for="row in deductions" :key="`${row.cartonMark}:${row.skuCode}`" class="meta-line">{{ row.cartonMark }} · {{ row.skuCode }} · {{ row.quantity }} 件</view>
|
||||
<template v-if="!facts.length"><view class="dimensions"><label v-for="field in fields" :key="field.key"><text>{{ field.label }}</text><input v-model="dimensions[field.key]" type="digit" placeholder="请输入" /></label></view><button class="primary" :disabled="!ready" @tap="pack">{{ busy ? '生成中…' : '生成独立新箱及标签' }}</button></template>
|
||||
<view v-for="fact in facts" :key="fact.cartonMark"><text class="business-code">新箱:{{ fact.cartonMark }} · {{ fact.quantity }} 件</text><button class="secondary" @tap="print(fact.label)">查看 / 打印新箱识别标签</button><text class="hint">此标签用于识别仓库包裹/箱;商品目标 SKU 标签请按上方换标明细核对。</text></view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
import { packReturnRelabelCartons } from '@/api/relabel.js'
|
||||
import { requiresReturnPack, returnDeductions, returnPackFacts, returnSelections } from '@/utils/return-relabel.js'
|
||||
import { warehouseIdempotencyKey, releaseWarehouseKey } from '@/stores/warehouse-work.js'
|
||||
export default {
|
||||
props: { task: { type: Object, required: true }, locked: { type: Boolean, default: false } }, emits: ['saved', 'error', 'busy', 'refresh'],
|
||||
data() { return { busy: false, dimensions: { length: '', width: '', height: '', weight: '' }, fields: [{ key: 'length', label: '长 cm' }, { key: 'width', label: '宽 cm' }, { key: 'height', label: '高 cm' }, { key: 'weight', label: '重量 kg' }] } },
|
||||
computed: {
|
||||
deductions() { return returnDeductions(this.task) }, facts() { return returnPackFacts(this.task) },
|
||||
ready() { const items = returnSelections(this.task); return !this.locked && !this.busy && requiresReturnPack(this.task) && !this.facts.length && items.length === this.task.items.length && items.length > 0 && items.every(item => Number(item.relabelledPackages) === Number(item.expectedPackages) && !Number(item.skippedPackages) && !Number(item.exceptionPackages)) && this.deductions.every(row => row.cartonMark && row.skuCode && Number.isSafeInteger(row.quantity) && row.quantity > 0) && this.fields.every(field => Number.isFinite(Number(this.dimensions[field.key])) && Number(this.dimensions[field.key]) > 0) }
|
||||
},
|
||||
methods: {
|
||||
async pack() {
|
||||
if (!this.ready) return
|
||||
const payload = { sourceCartonMarks: [...new Set(this.deductions.map(row => row.cartonMark))], targetCartonMark: null, sourceDeductions: this.deductions, ...Object.fromEntries(this.fields.map(field => [field.key, Number(this.dimensions[field.key])])) }
|
||||
const key = warehouseIdempotencyKey('relabel', 'return-pack', { relabelNo: this.task.relabelNo, round: this.task.reviewedAt || 'INITIAL', ...payload })
|
||||
this.busy = true; this.$emit('busy', true)
|
||||
try { const result = await packReturnRelabelCartons(this.task.relabelNo, { ...payload, idempotencyKey: key.value }); releaseWarehouseKey('relabel', key.signature); this.$emit('saved', result) } catch (error) { if (['409', 'A0409', 'B0409'].includes(String(error?.code || ''))) { releaseWarehouseKey('relabel', key.signature); this.$emit('refresh') } this.$emit('error', error) } finally { this.busy = false; this.$emit('busy', false) }
|
||||
},
|
||||
print(label) {
|
||||
// #ifdef H5
|
||||
if (!label?.printableHtml) return this.$emit('error', new Error('服务端未返回可打印的新箱标签'))
|
||||
const tab = window.open('', '_blank'); if (!tab) return this.$emit('error', new Error('请允许打开标签打印窗口'))
|
||||
tab.document.write(label.printableHtml); tab.document.close(); tab.focus(); tab.print()
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
this.$emit('error', new Error('当前设备未配置打印服务,生成标签不代表打印完成'))
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>.dimensions{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin:12px 0}.dimensions input{border:1px solid #cbd8e8;padding:8px}.hint,.meta-line{display:block;margin:8px 0}</style>
|
||||
Reference in New Issue
Block a user