This commit is contained in:
2026-10-16 17:43:42 +08:00
parent af3cacd2db
commit fbaa48337d
5 changed files with 96 additions and 23 deletions
@@ -0,0 +1,47 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import ts from 'typescript'
const source = readFileSync(new URL('./transferCartonPrinting.ts', import.meta.url), 'utf8')
const js = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS } }).outputText
const exports = {}
new Function('exports', js)(exports)
const prepare = exports.prepareTransferCartonLabels
test('prints forecast counts independently of quantities or order fields and escapes cargo', () => {
const rows = [{ sku: 'A<script>', name: '货物 & 品名', expectedPackageCount: 12 }]
let next = 0
const html = prepare(rows, () => `BX-${++next}`)
assert.equal((html.match(/<section /g) || []).length, 12)
assert.match(html, /12 \/ 12/)
assert.match(html, /A&lt;script&gt;/)
assert.equal(rows[0].generatedCartonMarks.length, 12)
assert.equal(prepare(rows, () => { throw new Error('must retain labels') }), html)
})
test('adding boxes or rows keeps previously printed identities', () => {
const rows = [{ sku: 'A', name: '', expectedPackageCount: 2 }]
let next = 0
prepare(rows, () => `BX-${++next}`)
rows[0].expectedPackageCount = 3
rows.push({ sku: 'B', name: '', expectedPackageCount: 1 })
const html = prepare(rows, () => `BX-${++next}`)
assert.deepEqual(rows[0].generatedCartonMarks, ['BX-1', 'BX-2', 'BX-3'])
assert.match(html, /BX-4/)
})
test('invalid or excessive forecasts do not generate any labels', () => {
for (const count of [0, -1, 1.5, undefined, 5001]) {
const row = { sku: '', name: '', expectedPackageCount: count }
assert.throws(() => prepare([row]), /预计箱数/)
assert.equal(row.generatedCartonMarks, undefined)
}
})
test('print button cannot create, update or submit an order', () => {
const view = readFileSync(new URL('../views/WarehouseServiceView.vue', import.meta.url), 'utf8')
const handler = view.slice(view.indexOf('function printForecastCartons()'), view.indexOf('async function submit('))
assert.match(handler, /prepareTransferCartonLabels\(cargoRows.value\)/)
assert.doesNotMatch(handler, /createTransfer|updateTransfer|submitTransfer|getTransferCartonLabel|form\./)
assert.match(view, /cartonMarks: item.generatedCartonMarks/)
})
@@ -0,0 +1,27 @@
export interface PrintableCargoRow {
sku: string
name: string
expectedPackageCount?: number
generatedCartonMarks?: string[]
}
const escapeHtml = (value: string) => value.replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char]!)
/** Generate labels in memory only. The later order request carries these same identities. */
export function prepareTransferCartonLabels(rows: PrintableCargoRow[], newMark: () => string = () => {
const bytes = crypto.getRandomValues(new Uint8Array(16))
return `BX-${Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('').toUpperCase()}`
}) {
const total = rows.reduce((sum, row) => sum + Number(row.expectedPackageCount || 0), 0)
if (!rows.length || rows.some(row => !Number.isSafeInteger(row.expectedPackageCount) || Number(row.expectedPackageCount) < 1) || total > 5000) {
throw new Error('请填写货物的预计箱数(正整数),单次最多打印 5000 箱')
}
let index = 0
const labels = rows.flatMap(row => {
const count = Number(row.expectedPackageCount)
const marks = row.generatedCartonMarks ||= []
while (marks.length < count) marks.push(newMark())
return marks.slice(0, count).map(mark => `<section class="label"><div class="title">仓储转运箱唛</div><div class="mark">${escapeHtml(mark)}</div><div>${++index} / ${total}</div><div>SKU:${escapeHtml(row.sku)}</div><div>${escapeHtml(row.name)}</div></section>`)
})
return `<!doctype html><html><head><meta charset="utf-8"><title>仓储转运箱唛</title><style>@page{size:100mm 100mm;margin:0}body{margin:0;font-family:Arial,sans-serif}.label{box-sizing:border-box;width:100mm;height:100mm;padding:10mm;break-after:page;display:flex;flex-direction:column;justify-content:center;gap:6mm;text-align:center;overflow-wrap:anywhere}.label:last-child{break-after:auto}.title{font-size:22px;font-weight:bold}.mark{font-size:23px;font-weight:bold}</style></head><body>${labels.join('')}</body></html>`
}