@@ -13,7 +13,6 @@ import {
createPlatformTenantImpersonation ,
downloadTenantQualification ,
getPlatformTenant ,
getPlatformTenantDimensionInitializationStatus ,
listEnterpriseReviews ,
listPlatformTenantOrderCounts ,
listPlatformTenants ,
@@ -33,14 +32,6 @@ import {
type PlatformTenantResource ,
type PlatformTenantStatus ,
} from '@/modules/iam'
import {
getCustomerDimensionConfig ,
updateCustomerDimensionConfig ,
updateCustomerDimensionDefaultCalculationMode ,
type CustomerDimensionBoxType ,
type CustomerDimensionCalculationMode ,
} from '@/modules/dimension-config'
import { createCustomerChargeRule , setCustomerChargeRuleDefault } from '@/modules/fba-pricing'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import CopyButton from '@/shared/components/CopyButton.vue'
@@ -145,25 +136,6 @@ const queryFields: QueryField[] = [
]
type CustomerForm = CreatePlatformTenantRequest & { unifiedSocialCreditCode : string }
type CreationBillingRow = {
boxType : CustomerDimensionBoxType
boxTypeName : string
innerLengthCm : number
innerWidthCm : number
innerHeightCm : number
edgeAdditionCm : number
coefficient : number
weight : number
remark : string
}
const newCreationBillingRows = ( ) : CreationBillingRow [ ] => [
{ boxType : 'SMALL' , boxTypeName : '小箱' , innerLengthCm : 30 , innerWidthCm : 20 , innerHeightCm : 20 , edgeAdditionCm : 2 , coefficient : 1.1 , weight : 5 , remark : '标准小箱' } ,
{ boxType : 'MEDIUM' , boxTypeName : '中箱' , innerLengthCm : 40 , innerWidthCm : 30 , innerHeightCm : 30 , edgeAdditionCm : 2 , coefficient : 1.1 , weight : 8 , remark : '标准中箱' } ,
{ boxType : 'LARGE' , boxTypeName : '大箱' , innerLengthCm : 50 , innerWidthCm : 40 , innerHeightCm : 40 , edgeAdditionCm : 2 , coefficient : 1.1 , weight : 12 , remark : '标准大箱' } ,
{ boxType : 'EXTRA_LARGE' , boxTypeName : '特大箱' , innerLengthCm : 60 , innerWidthCm : 50 , innerHeightCm : 50 , edgeAdditionCm : 2 , coefficient : 1.1 , weight : 15 , remark : '特大箱' } ,
]
const emptyForm = ( ) : CustomerForm => ( {
tenantName : '' , legalName : '' , legalRepresentative : '' , unifiedSocialCreditCode : '' , contactName : '' , contactPhone : '' ,
contactEmail : '' , contactWechat : '' , servicePackage : '基础版' , salesOwnerName : '' , paymentTermDays : 30 ,
@@ -171,11 +143,6 @@ const emptyForm = (): CustomerForm => ({
registeredAddress : '' , internalRemark : '' ,
} )
const form = ref ( emptyForm ( ) )
const creationBillingEnabled = ref ( false )
const creationBillingDefaultMode = ref < Extract < CustomerDimensionCalculationMode , ' ADD ' | ' MULTIPLY ' > > ( 'ADD' )
const creationBillingRows = ref < CreationBillingRow [ ] > ( newCreationBillingRows ( ) )
const creationBillingSession = ref ( false )
const savedCreationBillingSnapshot = ref ( '' )
const rules : FormRules = {
tenantName : [ { required : true , message : '请输入公司名称' , trigger : 'blur' } ] ,
legalName : [ { required : true , message : '请输入营业执照上的企业名称' , trigger : 'blur' } ] ,
@@ -247,13 +214,9 @@ let loadSequence = 0
let reviewLoadSequence = 0
let editorLoadSequence = 0
const editorTitle = computed ( ( ) => editingId . value ? '编辑客户' : '新增客户' )
const editorDialogWidth = computed ( ( ) => creationBillingSession . value
? 'min(112 0px, calc(100vw - 28px))'
: 'min(64 0px, calc(100vw - 28px))' )
const canConfigureCreationBilling = computed ( ( ) => access . can ( 'platform:customer:list' )
&& access . can ( 'firstleg:dimension-config:view' )
&& access . can ( 'firstleg:dimension-config:edit' )
&& access . can ( 'firstleg:fba-pricing:rule:edit' ) )
const editorDialogWidth = computed ( ( ) => editingId . value
? 'min(64 0px, calc(100vw - 28px))'
: 'min(68 0px, calc(100vw - 28px))' )
const canUploadQualifications = computed ( ( ) => ! editorStatus . value || [ 'PROFILE_REQUIRED' , 'REJECTED' ] . includes ( editorStatus . value ) )
const canSubmitReview = computed ( ( ) => Boolean ( editingId . value ) && [ 'PROFILE_REQUIRED' , 'REJECTED' ] . includes ( editorStatus . value || '' ) )
const uploadingQualifications = computed ( ( ) => qualifications . value . some ( ( item ) => item . uploading ) )
@@ -423,110 +386,12 @@ function clearQualificationPreviews() {
function resetEditor ( ) {
clearQualificationPreviews ( )
form . value = emptyForm ( ) ; qualifications . value = newQualificationDrafts ( ) ; creditCodeEstablished . value = false ; qualificationLoadError . value = '' ; savedFormSnapshot . value = ''
creationBillingEnabled . value = false
creationBillingDefaultMode . value = 'ADD'
creationBillingRows . value = newCreationBillingRows ( )
creationBillingSession . value = false
savedCreationBillingSnapshot . value = ''
}
function openCreate ( ) {
editorLoadSequence += 1
editingId . value = null ; editorStatus . value = null ; resetEditor ( ) ; creationBillingSession . value = true ; editorOpen . value = true
editingId . value = null ; editorStatus . value = null ; resetEditor ( ) ; editorOpen . value = true
nextTick ( ( ) => formRef . value ? . clearValidate ( ) )
}
function creationBillingSnapshot ( ) {
return JSON . stringify ( {
enabled : creationBillingEnabled . value ,
defaultMode : creationBillingDefaultMode . value ,
rows : creationBillingRows . value ,
} )
}
function validateCreationBillingRules ( ) {
if ( ! creationBillingEnabled . value ) return true
if ( ! canConfigureCreationBilling . value ) {
ElMessage . error ( '当前账号缺少箱规尺寸或计费规则编辑权限,无法随客户一并保存' )
return false
}
if ( creationBillingRows . value . some ( ( row ) => ! [
row . innerLengthCm , row . innerWidthCm , row . innerHeightCm ,
] . every ( ( value ) => Number . isFinite ( value ) && value >= 0.001 ) ) ) {
ElMessage . error ( '四档箱规的内径长、宽、高必须大于 0' )
return false
}
if ( creationBillingRows . value . some ( ( row ) => ! Number . isFinite ( row . edgeAdditionCm )
|| row . edgeAdditionCm < 0.001 || row . edgeAdditionCm > 100 ) ) {
ElMessage . error ( '四档箱规的单边加宽必须在 0.001 至 100cm 之间' )
return false
}
if ( creationBillingRows . value . some ( ( row ) => ! Number . isFinite ( row . coefficient ) || row . coefficient < 0.001 ) ) {
ElMessage . error ( '四档箱规的权重系数必须大于 0' )
return false
}
if ( creationBillingRows . value . some ( ( row ) => ! Number . isFinite ( row . weight ) || row . weight < 0.001 ) ) {
ElMessage . error ( '四档箱规的重量必须大于 0' )
return false
}
return true
}
function wait ( milliseconds : number ) {
return new Promise ( ( resolve ) => window . setTimeout ( resolve , milliseconds ) )
}
async function waitForDimensionInitialization ( tenantId : string ) {
for ( let attempt = 0 ; attempt < 20 ; attempt += 1 ) {
const status = await getPlatformTenantDimensionInitializationStatus ( tenantId )
if ( status . status === 'READY' ) return
if ( status . status === 'FAILED' ) {
throw new Error ( status . lastErrorMessage || '客户默认箱规初始化失败' )
}
await wait ( 500 )
}
throw new Error ( '客户默认箱规仍在初始化,请稍后再次点击保存' )
}
async function saveCreationBillingRules ( tenantId : string , customerName : string ) {
if ( ! creationBillingEnabled . value || creationBillingSnapshot ( ) === savedCreationBillingSnapshot . value ) return
await waitForDimensionInitialization ( tenantId )
const current = await getCustomerDimensionConfig ( tenantId )
const versions = new Map ( current . rules . map ( ( rule ) => [ rule . boxType , rule . version ] ) )
await updateCustomerDimensionConfig ( tenantId , {
rules : creationBillingRows . value . map ( ( row ) => ( {
boxType : row . boxType ,
innerLengthCm : row . innerLengthCm ,
innerWidthCm : row . innerWidthCm ,
innerHeightCm : row . innerHeightCm ,
edgeAdditionCm : row . edgeAdditionCm ,
remark : row . remark || null ,
version : versions . get ( row . boxType ) ? ? 0 ,
} ) ) ,
} )
const firstCoefficient = creationBillingRows . value [ 0 ] . coefficient
const created = await createCustomerChargeRule ( tenantId , {
ruleCode : ` RULE- ${ Date . now ( ) } ` ,
customerId : tenantId ,
customerName ,
businessType : 'FBA' ,
chargeMode : 'CBM' ,
taxMode : 'TAX_INCLUDED' ,
conversionFactor : 167 ,
lengthFactor : firstCoefficient ,
widthFactor : firstCoefficient ,
heightFactor : firstCoefficient ,
defaultRule : false ,
roundingScale : 2 ,
roundingMode : 'HALF_UP' ,
currency : 'CNY' ,
effectiveFrom : new Date ( ) . toISOString ( ) . slice ( 0 , 19 ) ,
effectiveTo : null ,
remark : creationBillingRows . value . map ( ( row ) => ` ${ row . boxType } : ${ row . innerLengthCm } , ${ row . innerWidthCm } , ${ row . innerHeightCm } ;系数: ${ row . coefficient } ;重量: ${ row . weight } ; ${ row . boxTypeName } : ${ row . remark || '—' } ` ) . join ( '; ' ) ,
} )
await setCustomerChargeRuleDefault ( tenantId , created . id )
await updateCustomerDimensionDefaultCalculationMode ( tenantId , creationBillingDefaultMode . value )
savedCreationBillingSnapshot . value = creationBillingSnapshot ( )
}
function mapTenantToForm ( row : PlatformTenantResource ) : CustomerForm {
return {
tenantName : row . tenantName , legalName : row . legalName || '' , legalRepresentative : row . legalRepresentative || '' , unifiedSocialCreditCode : row . unifiedSocialCreditCode || '' ,
@@ -697,7 +562,6 @@ async function uploadSelectedQualifications(tenantId: string) {
async function saveCustomer ( ) {
if ( ! formRef . value || ! ( await formRef . value . validate ( ) . catch ( ( ) => false ) ) ) return
if ( ! validateCreationBillingRules ( ) ) return
if ( ! requiredQualificationPackageComplete . value ) {
ElMessage . warning ( ` 请完整保存必填业务项: ${ missingQualificationPackageItems . value . join ( '、' ) } ` )
return
@@ -728,15 +592,6 @@ async function saveCustomer() {
editorStatus . value = saved . status
savedFormSnapshot . value = JSON . stringify ( form . value )
const failures = hadSelectedQualifications ? await uploadSelectedQualifications ( String ( saved . id ) ) : [ ]
if ( creationBillingSession . value && creationBillingEnabled . value ) {
try {
await saveCreationBillingRules ( String ( saved . id ) , saved . tenantName )
} catch ( error ) {
ElMessage . error ( ` 客户资料已保存,但箱规尺寸和权重系数未完整保存: ${ getApiErrorMessage ( error , '请稍后再次点击保存' ) } ` )
await loadCustomers ( )
return
}
}
if ( failures . length ) {
ElMessage . warning ( ` 客户资料已保存,但 ${ failures . join ( '、' ) } 上传失败;失败文件已保留,请修正后再次保存 ` )
} else if ( hadBusinessLicense && hadLegalPersonId ) {
@@ -1235,26 +1090,6 @@ onMounted(() => { void loadCustomers() })
< el-form-item label = "账期(天)" prop = "paymentTermDays" required > < el-input-number v-model = "form.paymentTermDays" :min="0" :max="365" controls-position="right" placeholder="请输入账期天数" / > < / el-form-item >
< / div > < el-form-item label = "内部备注" prop = "internalRemark" > < el-input v-model = "form.internalRemark" type="textarea" :rows="3" placeholder="请输入仅平台人员可见的客户备注" maxlength="1000" show -word -limit / > < / el-form-item > < / FormSection >
< FormSection v-if = "creationBillingSession" class="creation-billing-section" title="客户计费规则" description="创建客户时一次配置箱规尺寸和权重系数,保存后无需再进入客户详情补录。" >
< div class = "creation-billing-heading" >
< el-checkbox v-model = "creationBillingEnabled" :disabled="!canConfigureCreationBilling" > 按箱规尺寸计算 < / el -checkbox >
< el-radio-group v-if = "creationBillingEnabled" v-model="creationBillingDefaultMode" aria-label="默认箱规计算方式" >
< el -radio -button value = "ADD" > 箱规尺寸 < / el-radio-button >
< el-radio-button value = "MULTIPLY" > 权重系数 < / el-radio-button >
< / el-radio-group >
< / div >
< p v-if = "!canConfigureCreationBilling" class="creation-billing-permission-hint" > 当前账号需同时具备箱规尺寸和客户计费规则编辑权限 , 才能在创建时配置 。 < / p >
< template v-if = "creationBillingEnabled" >
< p class = "creation-billing-hint" > 四档尺寸与权重系数会随客户资料一并保存 ; 所选计算方式将设为客户默认规则 。 < / p >
< div class = "creation-billing-table-wrap" >
< table class = "creation-billing-table" >
< thead > < tr > < th > 箱规类型 < / th > < th > 内径长 ( cm ) < / th > < th > 内径宽 ( cm ) < / th > < th > 内径高 ( cm ) < / th > < th > 单边加宽 ( cm ) < / th > < th > 权重系数 < / th > < th > 重量 ( kg ) < / th > < th > 备注 < / th > < / tr > < / thead >
< tbody > < tr v-for = "row in creationBillingRows" :key="row.boxType"><th scope="row">{{ row.boxTypeName }}</th><td><el-input-number v-model="row.innerLengthCm" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.innerWidthCm" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.innerHeightCm" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.edgeAdditionCm" :min="0.001" :max="100" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.coefficient" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.weight" :min="0.001" :precision="3" :controls="false" /></td><td><el-input v-model="row.remark" maxlength="500" placeholder="选填" / > < / td > < / tr > < / tbody >
< / table >
< / div >
< / template >
< / FormSection >
< FormSection class = "qualification-section" title = "企业认证资料" description = "营业执照和18位统一社会信用代码为必填业务项;法人身份证、货代资质证书和银行开户许可证均为选填。" > < div v-if = "editorStatus" class="qualification-heading"><el-tag :type="statusMeta(editorStatus).type" effect="light" > {{ statusMeta ( editorStatus ) .label }} < / el -tag > < / div >
< el-form-item label = "统一社会信用代码" prop = "unifiedSocialCreditCode" required class = "qualification-credit-code" > < el-input v-model = "form.unifiedSocialCreditCode" :disabled="creditCodeEstablished" placeholder="请输入18位大写字母或数字" maxlength="18" @input="form.unifiedSocialCreditCode = String($event).toUpperCase()" / > < small > { { creditCodeEstablished ? '已登记的统一社会信用代码不可直接修改。' : '必须与营业执照一致,并随营业执照共同保存;法人身份证为选填。' } } < / small > < / el-form-item >
< div v-if = "qualificationLoadError" class="qualification-load-error" role="alert">{{ qualificationLoadError }}<el-button v-if="editingId" link type="primary" @click="loadEditorQualifications(editingId)" > 重试 < / el -button > < / div >
@@ -1400,7 +1235,6 @@ onMounted(() => { void loadCustomers() })
: deep ( . customer - list - table . el - tag -- danger ) { background : linear - gradient ( 135 deg , rgba ( 239 , 68 , 68 , .15 ) , rgba ( 220 , 38 , 38 , .1 ) ) ; color : # dc2626 ; }
: deep ( . customer - list - table . el - tag -- info ) { background : linear - gradient ( 135 deg , rgba ( 102 , 126 , 234 , .15 ) , rgba ( 118 , 75 , 162 , .1 ) ) ; color : # 667 eea ; }
. capital - input { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) 130 px ; width : 100 % ; gap : 8 px ; } . capital - input : deep ( . el - input - number ) , . capital - input : deep ( . el - select ) { width : 100 % ; }
. creation - billing - heading { display : flex ; min - height : 36 px ; align - items : center ; justify - content : space - between ; flex - wrap : wrap ; gap : 12 px ; } . creation - billing - heading : deep ( . el - checkbox ) { margin - right : 0 ; font - weight : 650 ; } . creation - billing - hint , . creation - billing - permission - hint { margin : 8 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.6 ; } . creation - billing - permission - hint { color : var ( -- el - color - warning ) ; } . creation - billing - table - wrap { overflow - x : auto ; margin - top : 12 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; } . creation - billing - table { width : 100 % ; min - width : 1040 px ; border - collapse : collapse ; } . creation - billing - table th , . creation - billing - table td { padding : 9 px 10 px ; border - bottom : 1 px solid var ( -- cc - border ) ; text - align : left ; white - space : nowrap ; } . creation - billing - table thead th { background : # f7f8fc ; color : var ( -- cc - muted ) ; font - size : 12 px ; } . creation - billing - table tbody tr : last - child th , . creation - billing - table tbody tr : last - child td { border - bottom : 0 ; } . creation - billing - table tbody th { color : var ( -- cc - text ) ; font - size : 13 px ; } . creation - billing - table : deep ( . el - input - number ) { width : 112 px ; } . creation - billing - table : deep ( . el - input ) { width : 130 px ; }
. qualification - heading { display : flex ; justify - content : flex - end ; margin - bottom : 12 px ; } . qualification - hint { margin : 12 px 0 0 ; color : var ( -- cc - muted ) ; font - size : 12 px ; line - height : 1.6 ; } . qualification - hint span , . qualification - label > span { color : var ( -- el - color - danger ) ; }
. qualification - credit - code { max - width : 420 px ; margin - top : 16 px ; } . qualification - credit - code small , . field - hint { display : block ; margin - top : 6 px ; color : var ( -- cc - muted ) ; font - size : 11 px ; line - height : 1.5 ; }
. qualification - table { overflow : hidden ; margin - top : 14 px ; border : 1 px solid var ( -- cc - border ) ; border - radius : 8 px ; } . qualification - row { display : grid ; grid - template - columns : 170 px minmax ( 0 , 1 fr ) 220 px ; min - width : 0 ; border - top : 1 px solid var ( -- cc - border ) ; } . qualification - row : first - child { border - top : 0 ; } . qualification - row > * { display : flex ; min - width : 0 ; min - height : 68 px ; align - items : center ; padding : 12 px 14 px ; } . qualification - header { min - height : 44 px ; background : # f7f8fc ; color : var ( -- cc - muted ) ; font - size : 12 px ; font - weight : 600 ; } . qualification - header > * { min - height : 44 px ; }