发布:将已验 TEST 功能更新至既有生产灰度(2026-10-23) #15

Merged
admin merged 69 commits from release/test into main 2026-10-23 10:51:31 +08:00
3 changed files with 59 additions and 17 deletions
Showing only changes of commit 4d7b6eb6dc - Show all commits
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { computed, onActivated, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ArrowLeft, ChevronDown, Plus } from '@lucide/vue'
import { useRoute, useRouter } from 'vue-router'
@@ -280,6 +280,11 @@ onMounted(() => {
else if (props.detailPage) void Promise.all([loadDetail(), loadOperationWarehouses()])
else void Promise.all([load(), loadOperationWarehouses()])
})
let activatedOnce = false
onActivated(() => {
if (activatedOnce && !props.createPage && !props.detailPage) void load()
activatedOnce = true
})
watch(() => [query.page, query.size] as const, ([nextPage, nextSize], [previousPage, previousSize]) => {
if (nextSize !== previousSize && nextPage !== 1) { query.page = 1; return }
if (!props.createPage && !props.detailPage) void load()
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { computed, nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, ChevronDown, Download, Plus, Printer, Trash2, Upload, X } from '@lucide/vue'
@@ -38,7 +38,7 @@ interface CargoRow { discardCartonIds?: string[]; id: number; included?: boolean
interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; fbaCartonMark: string; shipmentNo: string; destinationWarehouseId: string; remark: string }
interface ImportedRelabelBinding extends SkuRelabelRow { templateType: RelabelTemplateType; selected: boolean; shipmentNo: string }
interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] }
interface TransferCarton { cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string; transferItemId?: string | number; transferPalletId?: string | number; length?: number; width?: number; height?: number; weight?: number; quantity?: number; discrepancyReason?: string; countedAt?: string }
interface TransferCarton { items?: Array<{ transferItemId: string | number; skuCode: string; quantity: number }>; cartonMark: string; customerCartonMark?: string; cartonIndex: number; totalCartons: number; status: string; transferItemId?: string | number; transferPalletId?: string | number; length?: number; width?: number; height?: number; weight?: number; quantity?: number; discrepancyReason?: string; countedAt?: string }
const props = withDefaults(defineProps<{ service: ServiceType; createPage?: boolean; detailPage?: boolean; detailNo?: string }>(), { createPage: false, detailPage: false, detailNo: '' })
const auth = useAuthStore()
@@ -152,24 +152,28 @@ const showTransferCartons = ref(false)
function transferCartonItem(carton: TransferCarton) {
return selectedDetail.value?.items?.find((item) => carton.transferItemId != null && String(item.id) === String(carton.transferItemId))
}
function transferCartonLines(carton: TransferCarton) {
if (carton.items?.length) return carton.items.map(line => ({ ...line, productNameZh: selectedDetail.value?.items?.find(item => String(item.id) === String(line.transferItemId))?.productNameZh || '—' }))
const item = transferCartonItem(carton)
return [{ transferItemId: carton.transferItemId, skuCode: item?.skuCode || '—', productNameZh: item?.productNameZh || '—', quantity: Number(carton.quantity || 0) }]
}
function palletCartons(pallet: TransferPallet) {
return (selectedDetail.value?.cartons || []).filter((carton) => carton.status === 'COUNTED' && pallet.id != null && carton.transferPalletId != null && String(carton.transferPalletId) === String(pallet.id))
}
function palletSkuRows(pallet: TransferPallet) {
const summary = new Map<string, number>()
for (const carton of palletCartons(pallet)) {
const item = transferCartonItem(carton); const sku = item?.skuCode || '未标记 SKU'
summary.set(sku, (summary.get(sku) || 0) + Number(carton.quantity || 0))
for (const line of transferCartonLines(carton)) summary.set(line.skuCode, (summary.get(line.skuCode) || 0) + Number(line.quantity))
}
return [...summary.entries()].map(([sku, quantity]) => ({ sku, quantity }))
}
function palletQuantity(pallet: TransferPallet) { return palletCartons(pallet).reduce((sum, carton) => sum + Number(carton.quantity || 0), 0) }
function countedTransferCartons() { return (selectedDetail.value?.cartons || []).filter((carton) => carton.status === 'COUNTED') }
function receivedItemCartons(itemId: string | number | undefined) {
return countedTransferCartons().filter(carton => itemId != null && carton.transferItemId != null && String(carton.transferItemId) === String(itemId))
return countedTransferCartons().filter(carton => itemId != null && transferCartonLines(carton).some(line => String(line.transferItemId) === String(itemId)))
}
function receivedItemQuantity(itemId: string | number | undefined) {
return receivedItemCartons(itemId).reduce((sum, carton) => sum + Number(carton.quantity || 0), 0)
return receivedItemCartons(itemId).reduce((sum, carton) => sum + transferCartonLines(carton).filter(line => String(line.transferItemId) === String(itemId)).reduce((total, line) => total + Number(line.quantity), 0), 0)
}
const detailError = ref('')
@@ -418,6 +422,11 @@ onMounted(() => {
else void loadPage()
void nextTick(refreshWarehouseMarkLabels)
})
let activatedOnce = false
onActivated(() => {
if (activatedOnce && !props.createPage && !props.detailPage && mode.value === 'list') void loadPage()
activatedOnce = true
})
onBeforeUnmount(() => { clearCompletionPhotos(); clearMeasurementPhotos(); clearDiscardPhotos() })
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '', reviewStart: '', reviewEnd: '', completedStart: '', completedEnd: '', measuredStart: '', measuredEnd: '', outboundStart: '', outboundEnd: '' }) }
function resetForm() {
@@ -477,6 +486,14 @@ function returnToList() {
function serviceListRouteName(service: ServiceType) {
return ({ labeling: 'overseas-labeling', transfer: 'overseas-storage-transfer', disposal: 'overseas-cargo-disposal', relabel: 'overseas-return-relabel' } as Record<ServiceType, string>)[service]
}
async function goToServiceList() {
if (props.createPage || props.detailPage) {
await router.push({ name: serviceListRouteName(props.service) })
return
}
mode.value = 'list'
await loadPage()
}
async function openEdit(transferNo: string) {
try {
@@ -1191,13 +1208,12 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
if (shippingLabelFile.value) await uploadRelabelShippingLabel(created.application.relabelNo, shippingLabelFile.value)
} catch (error) {
// Creation already committed: return to the list even when an attachment still needs retrying.
if (props.createPage) await router.push({ name: serviceListRouteName(props.service) })
else { mode.value = 'list'; await load(); await openDetail({ '0': created.application.relabelNo }) }
await goToServiceList()
await ElMessageBox.alert(`申请 ${created.application.relabelNo} 已创建,部分附件上传失败:${error instanceof Error ? error.message : '上传失败'}。请在原单详情补传失败附件,不要重复下单。`, '申请已创建,附件待补传', { confirmButtonText: '知道了' })
return
}
ElMessage.success(`${config.value.title}提交成功`)
await router.push({ name: serviceListRouteName(props.service) })
await goToServiceList()
return
} else if (props.service === 'disposal') {
const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => {
@@ -1233,7 +1249,7 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
if (!relabelNo) {
ElMessage.success('转运单已提交审核;PDA 入库确认后将自动生成关联换标单')
await ElMessageBox.alert(`转运单 ${transferNo} 已提交。关联换标单会在 PDA 完成入库、客户确认实测库存后自动生成;届时请进入关联换标单详情上传贴标总文件。`, '转运单已提交', { confirmButtonText: '知道了' })
await router.push({ name: serviceListRouteName('transfer') })
await goToServiceList()
return
}
try {
@@ -1241,17 +1257,17 @@ async function submit(action: 'draft' | 'submit' = 'submit') {
if (skuTotalFile.value) await uploadRelabelFile(relabelNo, 'SKU_TOTAL', skuTotalFile.value)
} catch (error) {
await ElMessageBox.alert(`转运单 ${transferNo} 已提交,但换标总文件上传失败:${error instanceof Error ? error.message : '上传失败'}。请进入关联换标单详情补传,不要重复提交转运单。`, '转运单已提交,附件待补传', { confirmButtonText: '知道了' })
await openDetail({ '0': transferNo })
await goToServiceList()
return
}
}
ElMessage.success('转运单已提交审核')
await router.push({ name: serviceListRouteName('transfer') })
await goToServiceList()
return
}
}
ElMessage.success(props.service === 'transfer' ? '转运单草稿保存成功' : `${config.value.title}提交成功`)
await router.push({ name: serviceListRouteName(props.service) })
await goToServiceList()
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '提交失败') }
finally { saving.value = false }
}
@@ -1440,7 +1456,7 @@ async function resubmitRejectedRelabel() {
idempotencyKey: createIdempotencyKey(),
})
ElMessage.success('贴标单已重新提交审核')
await router.push({ name: serviceListRouteName('labeling') })
await goToServiceList()
} catch (error) {
if (error !== 'cancel' && error !== 'close') ElMessage.error(error instanceof Error ? error.message : '重新提交失败')
}
@@ -1777,10 +1793,10 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div class="warehouse-cartons-dialog-body">
<el-collapse>
<el-collapse-item v-for="carton in countedTransferCartons()" :key="carton.cartonMark" :name="carton.cartonMark">
<template #title><strong>{{ carton.cartonMark }}</strong><span class="warehouse-carton-summary">已确认 · {{ transferCartonItem(carton)?.skuCode ? 1 : 0 }} 种 SKU · {{ carton.quantity ?? '—' }} 件</span></template>
<template #title><strong>{{ carton.cartonMark }}</strong><span class="warehouse-carton-summary">已确认 · {{ transferCartonLines(carton).length }} 种 SKU · {{ carton.quantity ?? '—' }} 件</span></template>
<div class="warehouse-carton-measurements">所属托盘:{{ selectedDetail?.pallets?.find((pallet) => carton.transferPalletId != null && String(pallet.id) === String(carton.transferPalletId))?.palletNo || '散箱' }} · 清点时间:{{ formatDateTime(carton.countedAt) }}</div>
<div class="warehouse-carton-measurements">长 {{ carton.length ?? '—' }} cm · 宽 {{ carton.width ?? '—' }} cm · 高 {{ carton.height ?? '—' }} cm · 重量 {{ carton.weight ?? '—' }} kg · 体积 {{ [carton.length, carton.width, carton.height].every(value => value != null) ? (Number(carton.length) * Number(carton.width) * Number(carton.height) / 1000000).toFixed(6) : '—' }} m³</div>
<el-table :data="[{ skuCode: transferCartonItem(carton)?.skuCode || '—', productNameZh: transferCartonItem(carton)?.productNameZh || '—', quantity: carton.quantity ?? '—' }]" size="small" border>
<el-table :data="transferCartonLines(carton)" size="small" border>
<el-table-column prop="skuCode" label="SKU" min-width="220" />
<el-table-column prop="productNameZh" label="中文品名" min-width="180" />
<el-table-column prop="quantity" label="实际签收数量" width="130" />
@@ -0,0 +1,21 @@
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('./WarehouseServiceView.vue', import.meta.url), 'utf8')
const functions = source.slice(source.indexOf('function transferCartonItem('), source.indexOf("const detailError = ref('')"))
const code = ts.transpileModule(functions, { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText
const load = selectedDetail => new Function('selectedDetail', `${code}; return { transferCartonLines, palletSkuRows, palletQuantity, receivedItemCartons, receivedItemQuantity };`)(selectedDetail)
test('mixed carton detail and pallet summaries preserve every SKU without duplicating boxes', () => {
const mixed = { status: 'COUNTED', transferItemId: '1', transferPalletId: '9', quantity: 10, items: [{ transferItemId: '1', skuCode: 'A', quantity: 3 }, { transferItemId: '2', skuCode: 'B', quantity: 7 }] }
const display = load({ value: { items: [{ id: '1', skuCode: 'A', productNameZh: '产品A' }, { id: '2', skuCode: 'B', productNameZh: '产品B' }], cartons: [mixed, { ...mixed, status: 'NOT_RECEIVED', quantity: 999 }] } })
assert.equal(display.transferCartonLines(mixed).length, 2)
assert.equal(display.transferCartonLines(mixed)[1].productNameZh, '产品B')
assert.deepEqual(display.palletSkuRows({ id: '9' }), [{ sku: 'A', quantity: 3 }, { sku: 'B', quantity: 7 }])
assert.equal(display.palletQuantity({ id: '9' }), 10)
assert.equal(display.receivedItemQuantity('1'), 3)
assert.equal(display.receivedItemQuantity('2'), 7)
assert.equal(display.receivedItemCartons('2').length, 1)
})