This commit is contained in:
2026-10-15 18:25:46 +08:00
parent 2a38288e20
commit 55b41709f2
4 changed files with 174 additions and 48 deletions
+3 -3
View File
@@ -1,9 +1,9 @@
<template><view><view class="measure-grid"><label><text>长(cm)</text><input v-model="box.length" type="digit" class="measure-input" :disabled="disabled" placeholder="长度" /></label><label><text>宽(cm)</text><input v-model="box.width" type="digit" class="measure-input" :disabled="disabled" placeholder="宽度" /></label><label><text>高(cm)</text><input v-model="box.height" type="digit" class="measure-input" :disabled="disabled" placeholder="高度" /></label><label><text>重量(kg)</text><input v-model="box.weight" type="digit" class="measure-input" :disabled="disabled" placeholder="重量" /></label></view><view class="volume-chip"><text>体积</text><strong>{{ boxVolume(box) }} cm³</strong></view>
<view v-for="(line, lineIndex) in box.items" :key="lineIndex" class="count-line"><picker class="sku-picker" :disabled="disabled || !allowSkuChange" :range="skuCodes" :value="Math.max(0, skuCodes.indexOf(line.skuCode))" @change="line.skuCode = skuCodes[Number($event.detail.value)]"><text>{{ line.skuCode || '选择 SKU' }} ▾</text></picker><input v-model="line.quantity" type="number" class="number-input quantity" :disabled="disabled" placeholder="数量" /><button v-if="!disabled && allowSkuChange && box.items.length > 1" class="link-button" :disabled="disabled" @tap="box.items.splice(lineIndex, 1)">删除</button></view></view></template>
<view v-for="(line, lineIndex) in box.items" :key="lineIndex" class="count-line"><picker class="sku-picker" :disabled="disabled || !allowSkuChange || (lockFirstSku && lineIndex === 0)" :range="lineSkuCodes(lineIndex)" :value="Math.max(0, lineSkuCodes(lineIndex).indexOf(line.skuCode))" @change="line.skuCode = lineSkuCodes(lineIndex)[Number($event.detail.value)]"><text>{{ line.skuCode || '选择 SKU' }} ▾</text></picker><input v-model="line.quantity" type="number" class="number-input quantity" :disabled="disabled" placeholder="数量" /><button v-if="!disabled && allowSkuChange && box.items.length > 1 && (!lockFirstSku || lineIndex > 0)" class="link-button" :disabled="disabled" @tap="box.items.splice(lineIndex, 1)">删除</button></view></view></template>
<script>
export default {
props: { box: { type: Object, required: true }, skuCodes: { type: Array, default: () => [] }, disabled: Boolean, allowSkuChange: { type: Boolean, default: true } },
methods: { boxVolume(box) { const value = Number(box.length) * Number(box.width) * Number(box.height); return Number.isFinite(value) && value > 0 ? value.toFixed(2) : '—' } }
props: { box: { type: Object, required: true }, skuCodes: { type: Array, default: () => [] }, disabled: Boolean, uniqueSkus: Boolean, lockFirstSku: Boolean, allowSkuChange: { type: Boolean, default: true } },
methods: { lineSkuCodes(index) { return this.uniqueSkus ? this.skuCodes.filter(sku => !this.box.items.some((line, other) => other !== index && line.skuCode === sku)) : this.skuCodes }, boxVolume(box) { const value = Number(box.length) * Number(box.width) * Number(box.height); return Number.isFinite(value) && value > 0 ? value.toFixed(2) : '—' } }
}
</script>
<style scoped>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -36,7 +36,7 @@ test('warehouse transfer page loads detail without exposing internal identifiers
assert.match(page, /entryMode === 'work' && taskNo/)
assert.match(page, /mode=work&task=\$\{encodeURIComponent\(String\(item\.transferNo\)\)\}/)
assert.match(page, /v-if="!selected && entryMode === 'list'"/)
assert.match(page, /作业详情/)
assert.match(page, /返回列表/)
assert.match(page, /backToCandidates\(\)/)
assert.match(page, /backToCandidates\(\) \{ if \(this\.submitting.*this\.entryMode = 'list'/)
assert.match(page, /存在未完结的测量单/)
@@ -0,0 +1,117 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
const source = await readFile(new URL('../src/pages/warehouse-transfer/warehouse-transfer.vue', import.meta.url), 'utf8')
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1].replace(/^import .+$/gm, '').replace('export default', 'return')
const page = new Function('CartonCountFields', 'PortalHeader', 'ScanTargetInput', 'session', script)({}, {}, {}, { currentUser: null })
function context() {
const ctx = { ...page.data(), selected: { transferNo: 'WT1', packageCount: 2, items: [{ id: '1', skuCode: 'A' }, { id: '2', skuCode: 'B' }], cartons: [] }, saveCartonDraft() {} }
for (const [name, method] of Object.entries(page.methods)) ctx[name] = method.bind(ctx)
for (const [name, getter] of Object.entries(page.computed)) Object.defineProperty(ctx, name, { get: getter.bind(ctx), configurable: true })
ctx.saveCartonDraft = () => {}
return ctx
}
const box = (sku = 'A', quantity = 3) => ({ received: true, length: 10, width: 20, height: 30, weight: 2, items: [{ skuCode: sku, quantity }] })
test('one carton can add distinct forecast SKUs and validates every quantity', () => {
const ctx = context(), current = box()
ctx.addCartonSku(current)
assert.deepEqual(current.items, [{ skuCode: 'A', quantity: 3 }, { skuCode: 'B', quantity: '' }])
assert.equal(ctx.validCartonSkuLines(current), false)
current.items[1].quantity = 7
assert.equal(ctx.validCartonSkuLines(current), true)
ctx.addCartonSku(current)
assert.equal(current.items.length, 2)
current.items[1].skuCode = 'A'
assert.equal(ctx.validCartonSkuLines(current), false)
current.items[1].skuCode = 'FOREIGN'
assert.equal(ctx.validCartonSkuLines(current), false)
})
test('single SKU batch changes never overwrite mixed carton contents', () => {
const ctx = context(), single = box(), mixed = box()
mixed.items.push({ skuCode: 'B', quantity: 7 })
ctx.cartonBoxes = [single, mixed]; ctx.batchSku = 'A'
ctx.selectBatchCartons(true)
assert.deepEqual(ctx.batchSelectedIndexes, [0])
ctx.toggleBatchCarton(1)
assert.deepEqual(ctx.batchSelectedIndexes, [0])
assert.equal(ctx.previousMatchingCarton(1), null)
assert.equal(ctx.cartonQuantity, 13)
})
test('delete only changes actual draft and preserves forecast; additions keep their own SKU rows', () => {
const ctx = context()
ctx.cartonBoxes = [box(), box('B')]; ctx.batchSelectedIndexes = [1]
ctx.removeCarton(0)
assert.equal(ctx.cartonBoxes.length, 1)
assert.equal(ctx.selected.packageCount, 2)
assert.deepEqual(ctx.batchSelectedIndexes, [])
ctx.addExtraCarton()
assert.equal(ctx.cartonBoxes.length, 2)
assert.equal(ctx.cartonBoxes[1].items[0].skuCode, 'A')
ctx.cartonBoxes[1].items[0].quantity = 8
assert.equal(ctx.cartonBoxes[0].items[0].quantity, 3)
})
test('draft reload preserves deletions, including an empty actual list', () => {
const ctx = context()
ctx.selected.cartons = [{ cartonMark: 'BOX1', transferItemId: '1', status: 'EXPECTED' }, { cartonMark: 'BOX2', transferItemId: '2', status: 'EXPECTED' }]
let saved = { boxes: [{ ...box('B'), cartonMark: 'BOX2', transferItemId: '2' }], discrepancyReason: '少到一箱' }
globalThis.uni = { getStorageSync: () => saved }
try {
ctx.restoreCartons()
assert.equal(ctx.cartonBoxes.length, 1)
assert.equal(ctx.cartonBoxes[0].cartonMark, 'BOX2')
assert.equal(ctx.countDiscrepancyReason, '少到一箱')
saved = { boxes: [], discrepancyReason: '重新录入' }
ctx.restoreCartons()
assert.equal(ctx.cartonBoxes.length, 0)
assert.equal(ctx.selected.cartons.length, 2)
} finally { delete globalThis.uni }
})
test('100 same SKU cartons accept two distinct specification groups and independent exceptions', () => {
const ctx = context()
ctx.selected.packageCount = 100
ctx.pallets = [{ generated: true, draftId: 'P1' }, { generated: true, draftId: 'P2' }]
ctx.cartonBoxes = Array.from({ length: 100 }, (_, index) => ({ ...box('A', ''), localKey: `BOX-${index}`, transferItemId: '1', length: '', width: '', height: '', weight: '' }))
ctx.batchSku = 'A'; ctx.batchPalletId = 'P1'; ctx.batchBox = { ...box('A', 12), length: 40, width: 30, height: 20, weight: 5 }
ctx.selectBatchCartons(true)
for (let index = 60; index < 100; index++) ctx.toggleBatchCarton(index)
assert.equal(ctx.batchTargetBoxes.length, 60)
assert.equal(ctx.batchTotalQuantity, 720)
ctx.applyBatchCartons()
assert.equal(ctx.cartonBoxes[59].items[0].quantity, 12)
assert.equal(ctx.cartonBoxes[60].items[0].quantity, '')
ctx.selectBatchCartons(true)
for (let index = 0; index < 60; index++) ctx.toggleBatchCarton(index)
ctx.batchPalletId = 'P2'; ctx.batchBox = { ...box('A', 8), length: 50, width: 40, height: 30, weight: 7 }
ctx.applyBatchCartons()
assert.equal(ctx.palletSummary(ctx.pallets[0]).cartonCount, 60)
assert.equal(ctx.palletSummary(ctx.pallets[1]).cartonCount, 40)
assert.equal(ctx.cartonQuantity, 1040)
assert.equal(ctx.totalWeight, '580.00')
assert.equal(ctx.totalVolume, '3.840000')
assert.equal(ctx.cartonCountReady, true)
ctx.cartonBoxes[0].items[0].quantity = 11
ctx.cartonBoxes[0].weight = 6
assert.equal(ctx.cartonBoxes[1].items[0].quantity, 12)
assert.equal(ctx.cartonBoxes[1].weight, 5)
assert.equal(ctx.batchBox.items[0].quantity, 8)
assert.equal(ctx.cartonQuantity, 1039)
assert.equal(ctx.totalWeight, '581.00')
})
test('copy previous matching SKU skips other SKUs and preserves the target pallet', () => {
const ctx = context()
ctx.cartonBoxes = [{ ...box('A', 12), length: 40, width: 30, height: 20, weight: 5, transferPalletId: 'P1' }, box('B', 99), { ...box('A', ''), transferPalletId: 'P2' }]
ctx.copyPreviousCarton(2)
assert.equal(ctx.cartonBoxes[2].length, 40)
assert.equal(ctx.cartonBoxes[2].weight, 5)
assert.equal(ctx.cartonBoxes[2].items[0].quantity, 12)
assert.equal(ctx.cartonBoxes[2].transferPalletId, 'P2')
ctx.cartonBoxes[2].items[0].quantity = 9
assert.equal(ctx.cartonBoxes[0].items[0].quantity, 12)
})