@@ -215,6 +215,7 @@ const destinationWarehousesLoading = ref(false)
const pageDataLoading = computed ( ( ) => supportingDataLoading . value || ( ! isCreatePage . value && store . loading ) )
const editorBaseline = ref ( '' )
let cargoSequence = 0
let packingGroupSequence = 0
let inboundBillingRequestSequence = 0
let channelRequestSequence = 0
let estimateRequestSequence = 0
@@ -1332,6 +1333,71 @@ function addCargoItem() {
resetEditorValidation ( )
}
const sharedPackingFields = [
'packageType' , 'packageCount' , 'lengthCm' , 'widthCm' , 'heightCm' ,
'grossWeightKg' , 'netWeightKg' , 'volumeCbm' ,
] as const
function packingGroupFor ( item : FirstLegCargoItem ) {
return item . packingGroupNo . trim ( )
}
function mixedSkuCount ( item : FirstLegCargoItem ) {
const group = packingGroupFor ( item )
return group ? form . items . filter ( ( candidate ) => packingGroupFor ( candidate ) === group ) . length : 1
}
function isAdditionalMixedSku ( item : FirstLegCargoItem , index : number ) {
const group = packingGroupFor ( item )
return Boolean ( group && form . items . slice ( 0 , index ) . some ( ( candidate ) => packingGroupFor ( candidate ) === group ) )
}
function syncSkuQuantity ( item : FirstLegCargoItem ) {
if ( Number . isInteger ( item . unitsPerPackage ) && item . unitsPerPackage > 0
&& Number . isInteger ( item . packageCount ) && item . packageCount > 0 ) {
item . pieces = item . unitsPerPackage * item . packageCount
}
}
function syncPackingGroupFields ( source : FirstLegCargoItem ) {
if ( form . fulfillmentType !== 'ONE_CLICK_DROPSHIP' ) return
syncSkuQuantity ( source )
const group = packingGroupFor ( source )
if ( ! group ) return
form . items . forEach ( ( item ) => {
if ( item === source || packingGroupFor ( item ) !== group ) return
sharedPackingFields . forEach ( ( field ) => { item [ field ] = source [ field ] as never } )
syncSkuQuantity ( item )
} )
}
function normalizeSingleSkuGroups ( ) {
const counts = new Map < string , number > ( )
form . items . forEach ( ( item ) => {
const group = packingGroupFor ( item )
if ( group ) counts . set ( group , ( counts . get ( group ) ? ? 0 ) + 1 )
} )
form . items . forEach ( ( item ) => {
const group = packingGroupFor ( item )
if ( group && counts . get ( group ) === 1 ) item . packingGroupNo = ''
} )
}
function addSkuToPackingGroup ( index : number ) {
const source = form . items [ index ]
if ( ! source ) return
const group = packingGroupFor ( source ) || ` PG- ${ Date . now ( ) . toString ( 36 ) } - ${ ++ packingGroupSequence } `
source . packingGroupNo = group
const added = createEmptyCargoItem ( ` new-cargo- ${ ++ cargoSequence } ` )
added . packingGroupNo = group
sharedPackingFields . forEach ( ( field ) => { added [ field ] = source [ field ] as never } )
syncSkuQuantity ( added )
let insertAt = index + 1
while ( insertAt < form . items . length && packingGroupFor ( form . items [ insertAt ] ! ) === group ) insertAt ++
form . items . splice ( insertAt , 0 , added )
resetEditorValidation ( )
}
function removeCargoItem ( index : number ) {
if ( form . items . length === 1 ) {
ElMessage . warning ( '订单至少需要保留一条货物明细' )
@@ -1339,6 +1405,7 @@ function removeCargoItem(index: number) {
}
const [ removed ] = form . items . splice ( index , 1 )
if ( removed ) releaseProductImagePreviews ( [ removed ] )
normalizeSingleSkuGroups ( )
resetEditorValidation ( )
}
@@ -1503,6 +1570,7 @@ function removeSelectedCargoItems() {
const selectedIds = new Set ( selectedCargoIds . value )
const removedItems = form . items . filter ( ( item ) => selectedIds . has ( item . id ) )
form . items . splice ( 0 , form . items . length , ... form . items . filter ( ( item ) => ! selectedIds . has ( item . id ) ) )
normalizeSingleSkuGroups ( )
releaseProductImagePreviews ( removedItems )
selectedCargoIds . value = [ ]
resetEditorValidation ( )
@@ -1969,32 +2037,31 @@ async function searchOrders(model: QueryModel) {
< span v-else class = "cargo-table-hint" > 可逐行维护货物明细 。 < / span >
< div class = "cargo-table-actions" > < el-button @click ="addCargoItem" > < Plus :size = "16" / > 添加行 < / el-button > < el-button type = "danger" plain :disabled = "!selectedCargoIds.length" @click ="removeSelectedCargoItems" > < Trash2 :size = "15" / > 删除所选行 < / el-button > < / div >
< / div >
< div class = "cargo-table-wrap" >
< div class = "cargo-table-wrap" : class = "{ 'is-dropship': form.fulfillmentType === 'ONE_CLICK_DROPSHIP' }" >
< table class = "cargo-edit-table" >
< thead > < tr >
< th class = "cargo-check-cell" > < el-checkbox :model-value = "allCargoSelected" :indeterminate = "someCargoSelected" aria -label = " 全选货物 " @change ="toggleAllCargoItems" / > < / th > < th class = "cargo-index-cell" > # < / th >
< th > SKU NO . < small > SKU必填 < / small > < / th > < th v-if = "form.fulfillmentType === 'ONE_CLICK_DROPSHIP'">混箱组合<small>同号为同一批箱</small></th><th>中文品名</th><th class="cargo-image-column">图片</th><th>英文品名</th><th>报关 HS CODE</th><th>清关 HS CODE</th><th>包装类型</th><th>材质</th><th class="cargo-switch-column">危险品</th><th class= "cargo-switch-column"> 木制品 < small > 勾选后须商检 < / small > < / th >
< th > SKU NO . < small > SKU必填 < / small > < / th > < th > 中文品名 < / th > < th class = "cargo-image-column" > 图片 < / th > < th > 英文品名 < / th > < th > 报关 HS CODE < / th > < th > 清关 HS CODE < / th > < th > 包装类型 < / th > < th > 材质 < / th > < th class = "cargo-switch-column" > 危险品 < / th > < th class = "cargo-switch-column" > 木制品 < small > 勾选后须商检 < / small > < / th >
< th class = "cargo-number-column" > 箱规长 cm < / th > < th class = "cargo-number-column" > 箱规宽 cm < / th > < th class = "cargo-number-column" > 箱规高 cm < / th > < th class = "cargo-number-column" > 每箱数量 < / th > < th class = "cargo-number-column" > 箱数 < / th > < th class = "cargo-number-column" > < span class = "required-field-mark" aria -hidden = " true " > * < / span > 总毛重 < / th > < th class = "cargo-number-column" > < span class = "required-field-mark" aria -hidden = " true " > * < / span > 总体积 < / th > < th class = "cargo-number-column" > 总数量 < / th > < th class = "cargo-number-column" > 总净重 < / th >
< th > 清关申报单价 USD < / th > < th > 清关申报金额 USD < / th > < th > 用途 < / th > < th > 型号 < / th > < th > 品牌 < / th > < th class = "cargo-dimensions-column" > 产品规格 长 × 宽 × 高 < / th > < th > 货件编号 < small > 货件编号必填 < / small > < / th > < th > 跟踪编号 < small > 跟踪编号必填 < / small > < / th > < th > UPC NO . < / th > < th > 备注 < / th > < th class = "cargo-operation-column" > 操作 < / th >
< / tr > < / thead >
< tbody > < tr v-for = "(item, index) in form.items" :key="item.id" >
< tbody > < tr v-for = "(item, index) in form.items" :key="item.id" :class="{ 'mixed-sku-detail-row': isAdditionalMixedSku(item, index) }" >
< td class = "cargo-check-cell" > < el-checkbox :model-value = "selectedCargoIds.includes(item.id)" : aria -label = " ` 选择货物 $ { index + 1 } ` " @change ="toggleCargoItem(item.id, $event)" / > < / td > < td class = "cargo-index-cell" > { { index + 1 } } < / td >
< td > < el-form-item :prop = "`items.${index}.sku`" : required = "form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" : rules = "form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? requiredTextRules('请输入 SKU NO.') : undefined" > < el-input v-model = "item.sku" maxlength="64" placeholder="SKU编号" / > < / el-form-item > < / td >
< td v-if = "form.fulfillmentType === 'ONE_CLICK_DROPSHIP'"><el-input v-model="item.packingGroupNo" maxlength="32" placeholder="如 A(选填)" / > < / td >
< td > < el-form-item :prop = "`items.${index}.sku`" : required = "form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" : rules = "form.destinationType === 'FBA' || form.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? requiredTextRules('请输入 SKU NO.') : undefined" > < div class = "cargo-sku-input" > < el-input v-model = "item.sku" maxlength="64" placeholder="SKU编号" /><small v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP' && mixedSkuCount(item) > 1" class="mixed-sku-badge" > 同箱 < / small > < / div > < / el -form -item > < / td >
< td > < el-form-item :prop = "`items.${index}.cargoNameCn`" required :rules = "requiredTextRules('请输入中文品名')" > < el-input v-model = "item.cargoNameCn" maxlength="128" placeholder="中文品名" / > < / el-form-item > < / td >
< td class = "cargo-image-column" > < div class = "cargo-table-image" > < img v-if = "item.productImagePreviewUrl" :src="item.productImagePreviewUrl" :alt="`${item.cargoNameCn || `货物 ${index + 1}`}产品图片`" /><el-upload v-else :show-file-list="false" accept=".jpg,.jpeg,.png,image/jpeg,image/png" :disabled="item.productImageUploading" :http-request="(options: UploadRequestOptions) => uploadCargoProductImage(item, options)"><el-button text :loading="item.productImageUploading" aria-label="上传图片"><ImagePlus :size="17" /></el-button></el-upload><el-button v-if="item.productImagePreviewUrl" class="cargo-image-remove" text type="danger" aria-label="移除图片" @click="removeCargoProductImage(item)"><Trash2 :size="12" / > < / el-button > < / div > < / td >
< td > < el-input v-model = "item.cargoNameEn" maxlength="128" placeholder="English Name" /></td><td><el-input v-model="item.hsCode" maxlength="20" placeholder="HS Code" /></td><td><el-input v-model="item.customsHsCode" maxlength="20" placeholder="HS Code" / > < / td >
< td > < el-form-item :prop = "`items.${index}.packageType`" required :rules = "requiredSelectRules('请选择包装类型')" > < el-select v-model = "item.packageType "><el-option label="纸箱" value="CARTON" /><el-option label="托盘" value="PALLET" /><el-option label="木箱" value="WOODEN_CASE" /></el-select></el-form-item></td><td><el-input v-model="item.material" maxlength="128" placeholder="材质" / > < / td >
< td > < span v-if = "isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span><el-form-item v-else :prop= "`items.${index}.packageType`" required :rules= "requiredSelectRules('请选择包装类型')"><el-select v-model="item.packageType" @change="syncPackingGroupFields(item) "><el-option label="纸箱" value="CARTON" /><el-option label="托盘" value="PALLET" /><el-option label="木箱" value="WOODEN_CASE" /></el-select></el-form-item></td><td><el-input v-model="item.material" maxlength="128" placeholder="材质" / > < / td >
< td class = "cargo-switch-column" > < el-switch v-model = "item.dangerousGoods" :aria-label="`第 ${index + 1} 条货物是否危险品`" /></td><td class="cargo-switch-column"><div class="cargo-switch-cell"><el-switch v-model="item.woodenProduct" :aria-label="`第 ${index + 1} 条货物是否木制品`" /><small v-if="item.woodenProduct && form.customsInspection !== true" > 第 {{ index + 1 }} 条须商检 < / small > < / div > < / td >
< td > < el-input-number v-model = "item.lengthCm" :min="0" :precision="2" :controls="false" placeholder="长" /></td><td ><el-input-number v-model="item.widthCm" :min="0" :precision="2" :controls="false" placeholder="宽" /></td><td ><el-input-number v-model="item.heightCm" :min="0" :precision="2" :controls="false" placeholder="高" / > < / td >
< td > < el-form-item :prop = "`items.${index}.unitsPerPackage`" required : rules = "positiveNumberRules('每箱数量必须大于 0', true)" > < el-input-number v-model = "item.unitsPerPackage" :min="1" :controls="false" placeholder="每箱数量" /></el-form-item></td><td ><el-form-item :prop="`items.${index}.packageCount`" required :rules="positiveNumberRules('箱数必须大于 0', true)"><el-input-number v-model="item.packageCount" :min="1" :controls="false" placeholder="箱数" /></el-form-item></td><td ><el-form-item :prop="`items.${index}.grossWeightKg`" required :rules="positiveNumberRules('总毛重必须大于 0')"><el-input-number v-model="item.grossWeightKg" :min="0" :precision="3" :controls="false" placeholder="总毛重" /></el-form-item></td><td ><el-form-item :prop="`items.${index}.volumeCbm`" required :rules="positiveNumberRules('总体积必须大于 0')"><el-input-number v-model="item.volumeCbm" :min="0" :precision="4" :controls="false" placeholder="总体积" /></el-form-item></td><td ><el-form-item :prop="`items.${index}.pieces`" required :rules="positiveNumberRules('总数量必须大于 0', true)"><el-input-number v-model="item.pieces" :min="1" :controls="false" placeholder="总数量" /></el-form-item></td><td><el-input-number v-model="item.netWeightKg" :min="0" :precision="3" :controls="false" placeholder="净重" / > < / td >
< td > < span v-if = "isAdditionalMixedSku(item, index)" class="shared-carton-value">同上</span><el-input-number v-else v-model="item.lengthCm" :min="0" :precision="2" :controls="false" placeholder="长" @change="syncPackingGroupFields(item)" /></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同上</span ><el-input-number v-else v-model="item.widthCm" :min="0" :precision="2" :controls="false" placeholder="宽" @change="syncPackingGroupFields(item)" /></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同上</span ><el-input-number v-else v-model="item.heightCm" :min="0" :precision="2" :controls="false" placeholder="高" @change="syncPackingGroupFields(item)" / > < / td >
< td > < el-form-item :prop = "`items.${index}.unitsPerPackage`" required : rules = "positiveNumberRules('每箱数量必须大于 0', true)" > < el-input-number v-model = "item.unitsPerPackage" :min="1" :controls="false" placeholder="每箱数量" @change="syncSkuQuantity(item)" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span ><el-form-item v-else :prop="`items.${index}.packageCount`" required :rules="positiveNumberRules('箱数必须大于 0', true)"><el-input-number v-model="item.packageCount" :min="1" :controls="false" placeholder="箱数" @change="syncPackingGroupFields(item)" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span ><el-form-item v-else :prop="`items.${index}.grossWeightKg`" required :rules="positiveNumberRules('总毛重必须大于 0')"><el-input-number v-model="item.grossWeightKg" :min="0" :precision="3" :controls="false" placeholder="总毛重" @change="syncPackingGroupFields(item)" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span ><el-form-item v-else :prop="`items.${index}.volumeCbm`" required :rules="positiveNumberRules('总体积必须大于 0')"><el-input-number v-model="item.volumeCbm" :min="0" :precision="4" :controls="false" placeholder="总体积" @change="syncPackingGroupFields(item)" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="derived-sku-value">{{ item.pieces }}</span ><el-form-item v-else :prop="`items.${index}.pieces`" required :rules="positiveNumberRules('总数量必须大于 0', true)"><el-input-number v-model="item.pieces" :min="1" :controls="false" placeholder="总数量" /></el-form-item></td><td><span v-if="isAdditionalMixedSku(item, index)" class="shared-carton-value">同箱</span>< el-input-number v-else v-model="item.netWeightKg" :min="0" :precision="3" :controls="false" placeholder="净重" @change="syncPackingGroupFields(item)" / > < / td >
< td > < el-input-number v-model = "item.unitDeclaredValue" :min="0" :precision="2" :controls="false" placeholder="申报单价" /></td><td><el-input-number v-model="item.declaredValue" :min="0" :precision="2" :controls="false" placeholder="申报总价" /></td><td><el-input v-model="item.usage" maxlength="128" placeholder="用途" /></td><td><el-input v-model="item.model" maxlength="64" placeholder="型号" /></td><td><el-input v-model="item.brand" maxlength="64" placeholder="品牌" / > < / td >
< td class = "cargo-dimensions-column" > < div class = "cargo-table-dimensions" > < el-input-number v-model = "item.productLengthCm" :min="0" :precision="2" :controls="false" placeholder="长" /><span>× </span><el-input-number v-model="item.productWidthCm" :min="0" :precision="2" :controls="false" placeholder="宽" /><span>× </span><el-input-number v-model="item.productHeightCm" :min="0" :precision="2" :controls="false" placeholder="高" / > < / div > < / td >
< td > < el-form-item :prop = "`items.${index}.shipmentId`" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入货件编号') : undefined" > < el-input v-model = "item.shipmentId" maxlength="64" placeholder="货件编号" /></el-form-item></td><td><el-form-item :prop="`items.${index}.referenceId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入跟踪编号') : undefined"><el-input v-model="item.referenceId" maxlength="64" placeholder="跟踪编号" /></el-form-item></td><td><el-form-item :prop="`items.${index}.upc`"><el-input v-model="item.upc" maxlength="64" placeholder="UPC NO.(选填)" /></el-form-item></td><td><el-input v-model="item.remark" maxlength="500" placeholder="备注" /></td><td class="cargo-operation-column"><el-button link type="danger" :aria-label="`删除货物 ${index + 1}`" @click="removeCargoItem(index)"><Trash2 :size="15" / > 删除 < / el-button > < / td >
< td > < el-form-item :prop = "`items.${index}.shipmentId`" : required = "form.destinationType === 'FBA'" : rules = "form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入货件编号') : undefined" > < el-input v-model = "item.shipmentId" maxlength="64" placeholder="货件编号" /></el-form-item></td><td><el-form-item :prop="`items.${index}.referenceId`" :required="form.destinationType === 'FBA'" :rules="form.destinationType === 'FBA' ? requiredTextRules('FBA 货物请输入跟踪编号') : undefined"><el-input v-model="item.referenceId" maxlength="64" placeholder="跟踪编号" /></el-form-item></td><td><el-form-item :prop="`items.${index}.upc`"><el-input v-model="item.upc" maxlength="64" placeholder="UPC NO.(选填)" /></el-form-item></td><td><el-input v-model="item.remark" maxlength="500" placeholder="备注" /></td><td class="cargo-operation-column"><el-button v-if="form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" link type="primary" :aria-label="`为货物 ${index + 1} 添加同箱 SKU`" @click="addSkuToPackingGroup(index)"><Plus :size="15" />添加 SKU</el-button><el-button link type="danger" :aria-label="`删除货物 ${index + 1}`" @click="removeCargoItem(index)"><Trash2 :size="15" / > 删除 < / el-button > < / td >
< / tr > < / tbody >
< / table >
< / div >
< div class = "cargo-table-hint" > < span v-if = "form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" > 相同混箱组合号表示相同一批物理箱 ; 组内箱数 、 包装类型和箱规必须一致 , 物理箱数只计算 一次。 < / span > < span v-else > SKU NO. 、 货件编号 、 跟踪编号为 FBA 货物必填项 , UPC NO. 选填 < / span > < / div >
< div class = "cargo-table-hint" > < span v-if = "form.fulfillmentType === 'ONE_CLICK_DROPSHIP'" > 每个箱组默认装一个 SKU ; 同一箱内有多个 SKU 时点击 “ 添加 SKU ” , 箱数 、 箱规 、 重量和体积只填写 一次。 < / span > < span v-else > SKU NO. 、 货件编号 、 跟踪编号为 FBA 货物必填项 , UPC NO. 选填 < / span > < / div >
< / section >
< section class = "order-form-section prototype-section cost-estimate-section" aria -label = " 费用预估 " >
@@ -2430,6 +2497,12 @@ async function searchOrders(model: QueryModel) {
. required - field - mark { margin - right : 2 px ; color : var ( -- el - color - danger ) ; }
. cargo - dimensions - column { width : 240 px ! important ; }
. cargo - operation - column { width : 76 px ! important ; text - align : center ! important ; }
. cargo - table - wrap . is - dropship . cargo - operation - column { width : 148 px ! important ; }
. cargo - sku - input { position : relative ; width : 100 % ; }
. mixed - sku - badge { position : absolute ; top : - 10 px ; right : 2 px ; padding : 1 px 5 px ; border - radius : 999 px ; background : # eaf6f2 ; color : var ( -- primary ) ; font - size : 9 px ; font - weight : 650 ; line - height : 16 px ; white - space : nowrap ; }
. mixed - sku - detail - row { background : # f8fbff ; }
. shared - carton - value { display : inline - flex ; min - height : 30 px ; align - items : center ; color : # 8 a97a8 ; font - size : 12 px ; white - space : nowrap ; }
. derived - sku - value { display : inline - flex ; min - height : 30 px ; align - items : center ; color : # 172 b4d ; font - weight : 650 ; }
. cargo - switch - column { width : 112 px ! important ; text - align : center ! important ; vertical - align : middle ! important ; }
. cargo - switch - cell { display : flex ; align - items : center ; justify - content : center ; flex - direction : column ; gap : 4 px ; }
. cargo - switch - cell small { color : # b54708 ; font - size : 9 px ; line - height : 1.25 ; white - space : normal ; }