@@ -11,7 +11,7 @@ import CompletionConfirmationDialog from '../components/CompletionConfirmationDi
import { returnRelabelCandidate } from '../utils/returnRelabelCandidates'
import {
createDiscard , createRelabel , createTransfer , getDiscard , getRelabel , getTransfer , resubmitRelabel ,
getTransferCartonLabel , submitTransfer , updateTransfer , getTransferSourceCartons , type TransferSourceCarton ,
getTransferCartonLabel , submitTransfer , updateTransfer , increaseTransferForecastCartons , getTransferSourceCartons , type TransferSourceCarton ,
downloadRelabelCompletionPhoto , downloadTransferMeasurementPhoto , downloadDiscardPhoto , type DiscardPhoto , uploadRelabelShippingLabel ,
uploadRelabelFile ,
listDiscards , listDiscardCandidates , listRelabelInventoryCandidates , listRelabelHistoricalOrders , listRelabels , listTransfers , cancelDiscard ,
@@ -148,6 +148,34 @@ interface ServiceDetail extends Record<string, unknown> {
cartons ? : TransferCarton [ ] ; pallets ? : TransferPallet [ ] ; measurementPhotos ? : TransferMeasurementPhoto [ ] ; customerConfirmedAt ? : string
}
const selectedDetail = ref < ServiceDetail | null > ( null )
// 预报箱唛在提交后即可打印,与PDA实收清点明细分别展示。
const showForecastCartons = ref ( false )
const increaseForecastOpen = ref ( false )
const increasingForecast = ref ( false )
const forecastSnapshot = ref < ServiceDetail | null > ( null )
const increaseForecastForm = reactive ( { itemId : '' , additionalCount : 1 } )
const canIncreaseForecast = computed ( ( ) => canCreate . value && selectedDetail . value ? . transferNo === selectedDetail . value ? . inboundCode && [ 'PENDING_REVIEW' , 'WAIT_PDA_RECEIVING' ] . includes ( selectedDetail . value ? . status || '' ) )
/** 保存原预报快照,防止弹窗打开后其他设备的修改被覆盖。 */
function openIncreaseForecast ( ) {
if ( ! canIncreaseForecast . value || ! selectedDetail . value ) return
forecastSnapshot . value = selectedDetail . value
increaseForecastForm . itemId = String ( selectedDetail . value . items ? . [ 0 ] ? . id || '' )
increaseForecastForm . additionalCount = 1 ; increaseForecastOpen . value = true
}
/** 只补箱唛,SKU预报件数不随包装箱数自动改变。 */
async function saveIncreaseForecast ( ) {
if ( increasingForecast . value || ! forecastSnapshot . value ? . transferNo ) return
const snapshot = forecastSnapshot . value
const item = snapshot . items ? . find ( row => String ( row . id ) === increaseForecastForm . itemId )
if ( ! item || ! Number . isSafeInteger ( increaseForecastForm . additionalCount ) || increaseForecastForm . additionalCount < 1 ) { ElMessage . warning ( '请选择SKU并填写新增箱数' ) ; return }
increasingForecast . value = true
try {
const result = await increaseTransferForecastCartons ( String ( snapshot . transferNo ) , { expectedPackageCount : Number ( snapshot . packageCount ) , transferItemId : String ( item . id ) , expectedItemPackageCount : Number ( item . packageCount ) , packageCount : Number ( item . packageCount ) + increaseForecastForm . additionalCount } )
selectedDetail . value = result as ServiceDetail ; increaseForecastOpen . value = false
ElMessage . success ( '预报箱数已增加,新增箱唛可打印;请通知PDA刷新后继续清点' )
} catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '增加预报箱数失败' ) }
finally { increasingForecast . value = false }
}
const showTransferCartons = ref ( false )
function transferCartonItem ( carton : TransferCarton ) {
return selectedDetail . value ? . items ? . find ( ( item ) => carton . transferItemId != null && String ( item . id ) === String ( carton . transferItemId ) )
@@ -612,36 +640,6 @@ async function selectDiscardWarehouse() {
}
function createCargoRow ( ) : CargoRow { return { id : ++ cargoSequence , sku : '' , name : '' , quantity : 1 , expectedPackageCount : 1 , fnsku : '' , shipmentNo : '' , newSkuCode : '' , shippingMark : '' , customerCartonMarks : '' , cartonMark : '' , fbaCartonMark : '' , destinationWarehouseId : '' , remark : '' } }
function addCargoRow ( ) { cargoRows . value . push ( createCargoRow ( ) ) }
function escapePrintHtml ( value : string ) {
return value . replaceAll ( '&' , '&' ) . replaceAll ( '<' , '<' ) . replaceAll ( '>' , '>' ) . replaceAll ( '"' , '"' ) . replaceAll ( "'" , ''' )
}
function printTransferDraftCartonMarks ( ) {
if ( props . service !== 'transfer' ) return
const validRows = cargoRows . value . filter ( ( row ) => row . sku . trim ( ) && Number ( row . expectedPackageCount ) > 0 )
if ( ! validRows . length || validRows . some ( ( row ) => ! Number . isInteger ( Number ( row . expectedPackageCount ) ) ) ) {
ElMessage . warning ( '请先填写商品 SKU 和对应的整数箱数' ) ; return
}
const totalCartons = validRows . reduce ( ( sum , row ) => sum + Number ( row . expectedPackageCount ) , 0 )
if ( totalCartons > 500 ) { ElMessage . warning ( '单次最多打印 500 张箱唛' ) ; return }
const batch = new Date ( ) . toISOString ( ) . replace ( /\D/g , '' ) . slice ( 2 , 14 )
const labels : Array < { mark : string ; sku : string ; index : number } > = [ ]
let cartonIndex = 0
for ( const [ rowIndex , row ] of validRows . entries ( ) ) {
let marks = transferCustomerMarks ( row )
if ( marks . length !== Number ( row . expectedPackageCount ) ) {
marks = Array . from ( { length : Number ( row . expectedPackageCount ) } , ( _ , index ) => ` CT- ${ batch } - ${ String ( rowIndex + 1 ) . padStart ( 2 , '0' ) } - ${ String ( index + 1 ) . padStart ( 3 , '0' ) } ` )
row . customerCartonMarks = marks . join ( '\n' )
}
for ( const mark of marks ) labels . push ( { mark , sku : row . sku . trim ( ) , index : ++ cartonIndex } )
}
const printWindow = window . open ( '' , '_blank' )
if ( ! printWindow ) { ElMessage . warning ( '浏览器阻止了打印窗口,请允许弹窗后重试' ) ; return }
printWindow . opener = null
const sections = labels . map ( ( label ) => ` <section class="label"><div class="title">仓储转运客户箱唛</div><div class="mark"> ${ escapePrintHtml ( label . mark ) } </div><div class="sku">SKU: ${ escapePrintHtml ( label . sku ) } </div><div class="meta"> ${ label . index } / ${ labels . length } </div></section> ` ) . join ( '' )
printWindow . document . open ( )
printWindow . document . write ( ` <!doctype html><html><head><meta charset="utf-8"><title>仓储转运客户箱唛</title><style>@page{size:100mm 100mm;margin:8mm}body{margin:0;font-family:Arial,'Microsoft YaHei',sans-serif;color:#111}.label{height:84mm;display:flex;flex-direction:column;align-items:center;justify-content:center;page-break-after:always;border:1px solid #222;box-sizing:border-box;padding:8mm}.title{font-size:18px;font-weight:700}.mark{margin:11mm 0 6mm;font-size:26px;font-weight:700;letter-spacing:1px;word-break:break-all;text-align:center}.sku{font-size:17px;font-weight:650}.meta{font-size:12px;margin-top:4mm}</style></head><body> ${ sections } </body></html> ` )
printWindow . document . close ( ) ; printWindow . focus ( ) ; printWindow . print ( )
}
function addTransferRelabelRow ( ) {
const source = cargoRows . value [ Math . min ( transferRelabelRows . value . length , Math . max ( 0 , cargoRows . value . length - 1 ) ) ]
transferRelabelRows . value . push ( { ... createCargoRow ( ) , sku : source ? . sku || '' } )
@@ -1787,7 +1785,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
</tbody></table></div>
<div v-if=" ! transferSourceLoading && ! transferSourceError && ! transferSourceCartons . length " class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>商品 SKU</th><th>中文品名</th><th>库存箱数</th><th>原目的仓</th></tr></thead><tbody><tr v-for=" row in cargoRows " :key=" row . id "><td>{{ row.sku || '—' }}</td><td>{{ row.name || '—' }}</td><td>{{ row.expectedPackageCount }}</td><td>{{ transferInventoryCandidates.filter(item => item.inboundCode === selectedTransferInventory && item.skuCode === row.sku).map(item => item.destinationFbaWarehouseCode || item.destinationFbaWarehouseName).filter(Boolean).join(' / ') || '未指定' }}</td></tr></tbody></table></div>
</section>
<div v-if=" ! inventoryTransfer && ( props . service !== 'disposal' || ! discardSelectedCandidate ? . cartonSelectionSupported ) " class=" warehouse - section - heading " :class=" { 'warehouse-step-heading' : props . service === 'transfer' } "><div><h3><span v-if=" props . service === 'transfer' " class=" warehouse - step - number ">1</span>货物明细</h3></div><div v-if=" props . service !== 'disposal' " class=" warehouse - heading - actions "><el-button v-if=" props . service === 'transfer' " size=" small " :disabled=" ! cargoRows . some ( ( row ) => row . sku . trim ( ) && Number ( row . expectedPackageCount ) > 0 ) " @click=" printTransferDraftCartonMarks "><Printer :size=" 15 " />打印唛头</el-button><el-button type="primary " size=" small " @click=" addCargoRow "><Plus :size=" 15 " />添加行</el-button><el-button size=" small " @click=" cargoRows = [ ] ">清空</el-button></div></div>
<div v-if=" ! inventoryTransfer && ( props . service !== 'disposal' || ! discardSelectedCandidate ? . cartonSelectionSupported ) " class=" warehouse - section - heading " :class=" { 'warehouse-step-heading' : props . service === 'transfer' } "><div><h3><span v-if=" props . service === 'transfer' " class=" warehouse - step - number ">1</span>货物明细</h3></div><div v-if=" props . service !== 'disposal' " class=" warehouse - heading - actions "><el-button type=" primary " size=" small " @click=" addCargoRow "><Plus :size=" 15 " />添加行</el-button><el-button size=" small " @click=" cargoRows = [ ] ">清空</el-button></div></div>
<div v-if=" ! inventoryTransfer && ( props . service !== 'disposal' || ! discardSelectedCandidate ? . cartonSelectionSupported ) " class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>商品 SKU{{ props.service === 'transfer' ? '' : ' *' }}</th><th>中文品名{{ props.service === 'transfer' ? '' : ' *' }}</th><th v-if=" props . service === 'disposal' ">可用箱数</th><th v-if=" props . service === 'relabel' ">FNSKU / 箱唛</th><th v-if=" props . service === 'transfer' ">SKU 数量(件)*</th><th>{{ props.service === 'disposal' ? '本次废弃箱数' : '预计箱数' }} *</th><th v-if=" props . service !== 'disposal' ">操作</th></tr></thead><tbody><tr v-for=" row in cargoRows " :key=" row . id "><td><el-input v-model=" row . sku " :readonly=" props . service === 'disposal' " :placeholder=" props . service === 'transfer' ? '选填' : '请输入商品 SKU' " /></td><td><el-input v-model=" row . name " :readonly=" props . service === 'disposal' " :placeholder=" props . service === 'transfer' ? '选填' : '请输入中文品名' " /></td><td v-if=" props . service === 'disposal' "><strong>{{ row.availableQuantity }} 箱</strong></td><td v-if=" props . service === 'relabel' "><el-input v-model=" row . shippingMark " placeholder=" 请输入FNSKU或箱唛 " /></td><td v-if=" props . service === 'transfer' "><el-input-number v-model=" row . quantity " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder=" 请输入件数 " /></td><td v-if=" props . service === 'transfer' "><el-input-number v-model=" row . expectedPackageCount " :min=" 1 " :precision=" 0 " controls-position=" right " placeholder=" 请输入预计箱数 " /></td><td v-else><el-input-number v-model=" row . quantity " :disabled=" props . service === 'disposal' " :min=" props . service === 'disposal' ? 0 : 1 " :max=" row . availableQuantity " controls-position=" right " placeholder=" 请输入箱数 " /></td><td v-if=" props . service !== 'disposal' "><button type=" button " class=" warehouse - delete " title=" 删除 " @click=" removeCargoRow ( row . id ) "><Trash2 :size=" 16 " />删除</button></td></tr><tr v-if=" cargoRows . length === 0 "><td :colspan=" props . service === 'disposal' ? 4 : props . service === 'relabel' || props . service === 'transfer' ? 5 : 4 "><div class=" warehouse - empty ">{{ props.service === 'disposal' ? '请先选择入库单,系统将自动带出 SKU 和可用箱数' : '暂无货物明细,请点击“添加行”' }}</div></td></tr></tbody></table></div>
</template>
</div>
@@ -1833,6 +1831,14 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<h3>本次废弃箱子</h3><div class=" warehouse - table - scroll "><table class=" warehouse - edit - table "><thead><tr><th>入仓码</th><th>SKU</th><th>品名</th><th>申请数量</th><th>所选箱唛</th></tr></thead><tbody><tr v-for=" ( item , index ) in selectedDetail ? . items || [ ] " :key=" index "><td>{{ item.inboundCode || '—' }}</td><td>{{ item.skuCode || '—' }}</td><td>{{ item.productName || '—' }}</td><td>{{ item.discardPackages ?? '—' }}</td><td>{{ item.cartonMarks?.join('、') || '全部库存(历史记录无箱唛快照)' }}</td></tr></tbody></table></div>
<h3>废弃现场照片</h3><div v-if=" selectedDetail ? . photos ? . length " class=" warehouse - photo - grid "><div v-for=" photo in selectedDetail . photos " :key=" photo . fileId " class=" warehouse - photo - item "><el-image v-if=" discardPhotoUrls [ photo . fileId ] " :src=" discardPhotoUrls [ photo . fileId ] " :preview-src-list=" discardPhotoPreviewUrls " :initial-index=" discardPhotoPreviewUrls . indexOf ( discardPhotoUrls [ photo . fileId ] ) " fit=" cover " preview-teleported /><div v-else-if=" discardPhotoLoading [ photo . fileId ] " class=" warehouse - photo - placeholder ">照片加载中…</div><button v-else class=" warehouse - photo - placeholder warehouse - photo - retry " type=" button " @click=" loadDiscardPhoto ( photo . fileId ) ">{{ discardPhotoErrors[photo.fileId] ? '加载失败,点击重试' : '加载照片' }}</button><small>{{ formatDateTime(photo.submittedAt) }}</small></div></div><div v-else class=" warehouse - empty ">暂无已提交的废弃现场照片</div>
</div>
<div v-if=" props . service === 'transfer' && selectedDetail ? . status !== 'DRAFT' " class=" warehouse - card warehouse - form - section ">
<div class=" warehouse - section - heading warehouse - transfer - receipt - heading "><h3>预报箱唛({{ selectedDetail?.packageCount || 0 }} 箱)</h3><div class=" warehouse - transfer - receipt - actions "><el-button v-if=" canIncreaseForecast " type=" primary " size=" small " @click=" openIncreaseForecast "><Plus :size=" 14 " />增加预报箱数</el-button><el-button type=" primary " size=" small " @click=" printTransferCartons ( String ( selectedDetail ? . transferNo || '' ) ) "><Printer :size=" 14 " />打印预报箱唛</el-button><el-button size=" small " @click=" showForecastCartons = true ">查看预报箱唛</el-button></div></div>
<el-dialog v-model=" showForecastCartons " title=" 预报箱唛明细 " width=" 900 px " align-center destroy-on-close append-to-body><el-table :data=" selectedDetail ? . cartons || [ ] " size=" small " border><el-table-column prop=" cartonMark " label=" 仓库唛头 " min-width=" 300 " /><el-table-column label=" 预报SKU " min-width=" 240 "><template #default=" { row } ">{{ selectedDetail?.items?.find(item => String(item.id) === String(row.transferItemId))?.skuCode || '—' }}</template></el-table-column><el-table-column label=" 清点状态 " width=" 130 "><template #default=" { row } ">{{ row.status === 'COUNTED' ? '已清点' : row.status === 'NOT_RECEIVED' ? '未收到' : '待清点' }}</template></el-table-column></el-table><template #footer><el-button @click=" showForecastCartons = false ">关闭</el-button></template></el-dialog>
<el-dialog v-model=" increaseForecastOpen " title=" 增加预报箱数 " width=" 560 px " align-center destroy-on-close append-to-body :close-on-click-modal=" ! increasingForecast " :show-close=" ! increasingForecast ">
<el-form label-width=" 110 px "><el-form-item label=" 预报SKU "><el-select v-model=" increaseForecastForm . itemId " :disabled=" increasingForecast " style=" width : 100 % "><el-option v-for=" item in forecastSnapshot ? . items || [ ] " :key=" String ( item . id ) " :value=" String ( item . id ) " :label=" item . skuCode || item . productNameZh || '未命名SKU' " /></el-select></el-form-item><el-form-item label=" 新增箱数 "><el-input-number v-model=" increaseForecastForm . additionalCount " :disabled=" increasingForecast " :min=" 1 " :max=" Math . max ( 1 , 5000 - Number ( forecastSnapshot ? . packageCount || 0 ) ) " :precision=" 0 " /></el-form-item></el-form><p class=" warehouse - form - hint ">保留原箱唛,仅补生成新增箱唛;SKU预报件数保持不变。</p>
<template #footer><el-button :disabled=" increasingForecast " @click=" increaseForecastOpen = false ">取消</el-button><el-button type=" primary " :loading=" increasingForecast " @click=" saveIncreaseForecast ">确认增加</el-button></template>
</el-dialog>
</div>
<div v-if=" props . service === 'transfer' " class=" warehouse - card warehouse - form - section warehouse - transfer - cargo - card ">
<div class=" warehouse - section - heading "><h3>预报与实际签收</h3></div>
<div class=" warehouse - table - scroll "><table class=" warehouse - edit - table warehouse - transfer - comparison - table "><colgroup><col style=" width : 500 px " /><col style=" width : 140 px " /><col style=" width : 130 px " /><col style=" width : 100 px " /><col style=" width : 130 px " /><col style=" width : 130 px " /></colgroup><thead><tr><th>商品 SKU</th><th>中文品名</th><th>预报数量(件)</th><th>预计箱数</th><th>实际签收件数</th><th>实际签收箱数</th></tr></thead><tbody><tr v-for=" ( item , index ) in selectedDetail ? . items || [ ] " :key=" String ( item . id || index ) "><td>{{ item.skuCode || '—' }}</td><td>{{ item.productNameZh || '—' }}</td><td>{{ item.quantity ?? '—' }}</td><td>{{ item.packageCount ?? '—' }}</td><td>{{ receivedItemQuantity(item.id) }}</td><td>{{ receivedItemCartons(item.id).length }}</td></tr><tr v-if=" ! selectedDetail ? . items ? . length "><td colspan=" 6 "><div class=" warehouse - empty ">暂无货物明细</div></td></tr></tbody></table></div>
@@ -1855,7 +1861,7 @@ function addReturnBoxBinding(source: RelabelInventoryCandidate) {
<div class=" warehouse - evidence warehouse - measurement - evidence "><span>测量打托现场照片</span><div v-if=" selectedDetail ? . measurementPhotos ? . length " class=" warehouse - photo - grid warehouse - measurement - photo - grid "><div v-for=" ( photo , index ) in selectedDetail . measurementPhotos " :key=" photo . fileId " class=" warehouse - photo - item warehouse - measurement - photo - item " :title=" photo . fileName || ` 测量照片 ${ index + 1 } ` "><el-image v-if=" measurementPhotoUrls [ photo . fileId ] " :src=" measurementPhotoUrls [ photo . fileId ] " :preview-src-list=" measurementPhotoPreviewUrls " :initial-index=" measurementPhotoPreviewUrls . indexOf ( measurementPhotoUrls [ photo . fileId ] ) " :alt=" photo . fileName || ` 测量照片 ${ index + 1 } ` " fit=" cover " preview-teleported /><div v-else-if=" measurementPhotoLoading [ photo . fileId ] " class=" warehouse - photo - placeholder ">照片 {{ index + 1 }} 加载中…</div><button v-else class=" warehouse - photo - placeholder warehouse - photo - retry " type=" button " @click=" loadMeasurementPhoto ( photo . fileId ) ">{{ measurementPhotoErrors[photo.fileId] ? `照片 ${index + 1} 加载失败,点击重试` : `加载照片 ${index + 1}` }}</button></div></div><strong v-else>仓库尚未上传测量打托照片</strong></div>
</div>
<div v-if=" props . service === 'transfer' " class=" warehouse - card warehouse - form - section warehouse - transfer - received - card ">
<div class=" warehouse - section - heading warehouse - transfer - receipt - heading "><h3>签收箱号明细({{ countedTransferCartons().length }} 箱)</h3><div class=" warehouse - transfer - receipt - actions "><el-button v-if=" selectedDetail ? . status !== 'DRAFT' " native-type=" button " type=" primary " size=" small " @click=" printTransferCartons ( String ( selectedDetail ? . transferNo || selectedDetail ? . [ '0' ] || '' ) ) "><Printer :size=" 14 " />打印箱唛</el-button><el-button type="primary " size=" small " :disabled=" ! countedTransferCartons ( ) . length " @click=" showTransferCartons = true ">查看箱号明细</el-button></div></div>
<div class=" warehouse - section - heading warehouse - transfer - receipt - heading "><h3>签收箱号明细({{ countedTransferCartons().length }} 箱)</h3><div class=" warehouse - transfer - receipt - actions "><el-button type=" primary " size=" small " :disabled=" ! countedTransferCartons ( ) . length " @click=" showTransferCartons = true ">查看箱号明细</el-button></div></div>
<div v-if=" ! countedTransferCartons ( ) . length " class=" warehouse - empty ">暂无实际签收箱,等待 PDA 清点。</div>
<el-dialog v-model=" showTransferCartons " title=" 签收箱号明细 " width=" 1120 px " align-center destroy-on-close append-to-body>
<div class=" warehouse - cartons - dialog - body ">