This commit is contained in:
zlwu committed 2026-08-26 11:10:40 +08:00
1 parent dd1acdc787
commit 4222eaa593
47 files changed
+1054350 -9663

No files matched your search

@@ -103,6 +103,9 @@
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增
</el-button>
<el-button class="btn-ghost" :disabled="single" @click="handleCopy()">
<el-icon><DocumentCopy /></el-icon>合同复制
</el-button>
<el-button class="btn-ghost" :disabled="!canSubmit" @click="handleSubmitBatch">
<el-icon><Document /></el-icon>提交审批
</el-button>
@@ -516,13 +519,14 @@
v-if="isManageMode"
v-model:visible="formDialogVisible"
:edit-id="formDialogEditId"
:copy-source="formDialogCopySource"
@success="handleFormSuccess"
/>
</div>
</template>
<script setup lang="ts" name="ContractListPage">
import { ref, reactive, computed, onMounted } from 'vue';
import { ref, reactive, computed, onMounted, watch } from 'vue';
import {
listContract,
getContract as getContractDetail,
@@ -536,6 +540,7 @@ import {
Clock,
Close,
Document,
DocumentCopy,
Download,
Plus,
RefreshLeft,
@@ -606,6 +611,7 @@ const resolveCurrentUserId = (): string => {
// 合同表单对话框状态
const formDialogVisible = ref(false)
const formDialogEditId = ref<string | null>(null)
const formDialogCopySource = ref<ContractVO | null>(null)
// 列配置
const listColumnDefs = [
@@ -1087,15 +1093,50 @@ const handleExpandChange = async (row: ContractVO, expandedRows: ContractVO[]) =
}
};
const resetFormDialogState = () => {
formDialogEditId.value = null
formDialogCopySource.value = null
}
/** 新增按钮操作 */
const handleAdd = () => {
formDialogEditId.value = null
resetFormDialogState()
formDialogVisible.value = true
};
}
/** 解析当前唯一选中的合同行(优先列表数据,保证含 products) */
const resolveSingleSelectedContract = (row?: ContractVO): ContractVO | undefined => {
if (row?.id != null && String(row.id) !== '') {
return contractList.value.find((item) => String(item.id) === String(row.id)) || row
}
if (ids.value.length !== 1) return undefined
const selectedId = ids.value[0]
return (
contractList.value.find((item) => String(item.id) === String(selectedId))
|| selectedRows.value.find((item) => String(item.id) === String(selectedId))
)
}
/** 合同复制:选中一条后打开表单,仅带入产品明细 */
const handleCopy = (row?: ContractVO) => {
if (!row && ids.value.length !== 1) {
$modal.msgWarning('请选择一条数据进行合同复制')
return
}
const targetRow = resolveSingleSelectedContract(row)
if (!targetRow?.id) {
$modal.msgWarning('未找到选中的数据')
return
}
formDialogEditId.value = null
formDialogCopySource.value = targetRow
formDialogVisible.value = true
}
/** 表单对话框保存/提交成功回调 */
const handleFormSuccess = () => {
formDialogVisible.value = false
resetFormDialogState()
getList()
}
@@ -1300,7 +1341,13 @@ onMounted(() => {
} else {
handleViewChange()
}
});
})
watch(formDialogVisible, (visible) => {
if (!visible) {
resetFormDialogState()
}
})
</script>
<style scoped lang="scss">
@@ -24,6 +24,13 @@
>{{ dictStore.getLabel('crm_contract_status', detailContract?.status) }}</el-tag>
</div>
<div class="detail-actions">
<el-button
v-if="manageActions"
class="btn-ghost"
@click="handleDetailCopy()"
>
<el-icon><DocumentCopy /></el-icon>合同复制
</el-button>
<el-button
v-if="manageActions && isContractEditableStatus(detailContract?.status)"
class="btn-ghost"
@@ -844,6 +851,7 @@
<ContractFormDialog
v-model:visible="formDialogVisible"
:edit-id="formDialogEditId"
:copy-source="formDialogCopySource"
@success="handleFormSuccess"
/>
<CustomerDetailDialog v-if="customerDialogActive" ref="customerDetailRef" />
@@ -853,10 +861,14 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, defineAsyncComponent, nextTick } from 'vue';
import { ref, reactive, computed, onMounted, defineAsyncComponent, nextTick, watch } from 'vue';
import { useRouter } from 'vue-router';
import { isFrameworkContractType, isContractEditableStatus, getContractReeditConfirmMessage } from '../contractType'
import { displayFrameworkMoney, displayFrameworkQuantity } from '../frameworkProductPricing'
import {
displayFrameworkMoney,
displayFrameworkQuantity,
formatFrameworkQuantityForPrint
} from '../frameworkProductPricing'
import {
CircleCheck,
CircleClose,
@@ -865,6 +877,7 @@ import {
Close,
Delete,
Document,
DocumentCopy,
DocumentChecked,
Download,
Edit,
@@ -984,6 +997,18 @@ const currentContractId = ref<string | number>();
const formDialogVisible = ref(false);
const formDialogEditId = ref<string | null>(null);
const formDialogCopySource = ref<ContractVO | null>(null);
const resetFormDialogState = () => {
formDialogEditId.value = null
formDialogCopySource.value = null
}
watch(formDialogVisible, (open) => {
if (!open) {
resetFormDialogState()
}
})
const approvalDialogRef = ref();
const executeDialogRef = ref();
@@ -1050,6 +1075,7 @@ function handleActionSuccess() {
function handleFormSuccess() {
formDialogVisible.value = false;
resetFormDialogState();
loadDetailData();
emit('success');
}
@@ -1258,6 +1284,18 @@ const handleDetailDrawerClose = () => {
detailAttachmentFileList.value = [];
};
/** 详情抽屉 - 合同复制 */
const handleDetailCopy = () => {
if (!detailContract.value?.id) {
$modal.msgWarning('未找到合同数据')
return
}
visible.value = false
formDialogEditId.value = null
formDialogCopySource.value = detailContract.value
formDialogVisible.value = true
}
/** 详情抽屉 - 编辑 */
const handleDetailEdit = async () => {
if (!detailContract.value?.id) return
@@ -1274,7 +1312,8 @@ const handleDetailEdit = async () => {
}
}
visible.value = false
formDialogEditId.value = detailContract.value.id
resetFormDialogState()
formDialogEditId.value = String(detailContract.value.id)
formDialogVisible.value = true
}
@@ -1874,7 +1913,7 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${getUnitLabel(product.unit)}</td>
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatFrameworkQuantityForPrint(product, isFrameworkContract)}</td>
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
${pricingCells}
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${product.packageSpec || ''}</td>
@@ -2252,6 +2291,8 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
return unitLabel || unitValue || '';
};
const isFrameworkContract = isFrameworkContractType(contractData.contractType);
// 构建产品明细表格行(使用 0.3px 边框)
const productRows =
contractData.products
@@ -2268,7 +2309,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
<td style="padding: 4px 6px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${getUnitLabel(product.unit)}</td>
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatFrameworkQuantityForPrint(product, isFrameworkContract)}</td>
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(product.price)}</td>
<td style="padding: 4px 3px; border: 0.3px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(beforeTax)}</td>
@@ -824,7 +824,8 @@ import {
updateContract,
type ContractForm,
type ContractProductForm,
type ContractProductPriceTierForm
type ContractProductPriceTierForm,
type ContractVO
} from '@/api/crm/contract/index'
import { isFrameworkContractType, isPurchaseLikeContractType } from '../contractType'
import {
@@ -936,6 +937,8 @@ type ExtendedContractProductForm = ContractProductForm & {
const props = defineProps<{
visible: boolean
editId: string | null
/** 复制来源合同(列表选中行,含 products) */
copySource?: ContractVO | null
}>()
const emit = defineEmits<{
@@ -953,7 +956,9 @@ const userStore = useUserStore()
// 表单标题
const formTitle = computed(() => {
return form.value.id ? '编辑合同' : '新增合同'
if (props.editId) return '编辑合同'
if (props.copySource) return '复制合同'
return '新增合同'
})
// 合同期限日期范围
@@ -1077,7 +1082,7 @@ const submitLoading = ref(false)
watch(
() => props.visible,
(visible) => {
if (visible && props.editId) {
if (visible && (props.editId || props.copySource)) {
formLoading.value = true
} else if (!visible) {
formLoading.value = false
@@ -1346,7 +1351,8 @@ interface QuoteVO {
const handleOpened = async () => {
resetPartyExpand()
const isEdit = !!props.editId
if (isEdit) {
const isCopy = !!props.copySource
if (isEdit || isCopy) {
formLoading.value = true
}
try {
@@ -1354,23 +1360,15 @@ const handleOpened = async () => {
if (props.editId) {
await loadContractDetail(props.editId)
} else if (props.copySource) {
await loadContractForCopy(props.copySource)
} else {
resetForm()
form.value.signDate = new Date().toISOString().split('T')[0]
handleSignDateChange(form.value.signDate)
applyDefaultOwnerUser()
// 设置默认供方主体为"SX"
const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
if (defaultSupplier) {
form.value.supplierId = defaultSupplier.id
await handleSupplierChange(defaultSupplier.id)
}
await initNewContractForm()
}
} catch (error: any) {
console.error('合同表单初始化失败:', error)
$modal.msgError(error?.message || '加载合同数据失败')
if (isEdit) {
if (isEdit || isCopy) {
dialogVisible.value = false
}
} finally {
@@ -1378,6 +1376,20 @@ const handleOpened = async () => {
}
}
/** 新增合同表单默认值 */
const initNewContractForm = async () => {
resetForm()
form.value.signDate = new Date().toISOString().split('T')[0]
handleSignDateChange(form.value.signDate)
applyDefaultOwnerUser()
const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
if (defaultSupplier) {
form.value.supplierId = defaultSupplier.id
await handleSupplierChange(defaultSupplier.id)
}
}
/** 重置表单 */
const resetForm = () => {
form.value = {
@@ -1446,6 +1458,104 @@ const resetForm = () => {
selectedProducts.value = []
}
/** 将源合同产品明细映射为表单行(复制时清除主键及数量/单价/备注) */
const mapContractProductsFromSource = (products: any[], forCopy = false) =>
products.map((product: any) => {
const mapped: ExtendedContractProductForm = {
...product,
id: forCopy ? undefined : product.id,
contractId: forCopy ? undefined : product.contractId,
productModel: product.model || product.productModel || '',
isToxic: Number(product.isToxic || 0),
isExplosive: Number(product.isExplosive || 0),
brandMode: product.brandMode || undefined,
brandId: product.brandId || undefined,
brandName: product.brandName || undefined,
brandOptions: [],
priceTiers: (product.priceTiers || []).map((t: any) => {
let ids: string[] = []
if (Array.isArray(t.letterOssIds) && t.letterOssIds.length) {
ids = t.letterOssIds.map((id: any) => String(id))
} else if (t.letterOssId != null && t.letterOssId !== '') {
ids = [String(t.letterOssId)]
}
return {
...t,
id: forCopy ? undefined : t.id,
contractId: forCopy ? undefined : t.contractId,
contractProductId: forCopy ? undefined : t.contractProductId,
letterOssIds: ids,
...(forCopy ? { price: undefined } : {})
}
}),
taxRate: product.taxRate || 6,
taxAmount: forCopy ? 0 : (product.taxAmount ?? 0),
packageSpec: product.packageSpec || '',
deliveryMethod: product.deliveryMethod || ''
}
if (forCopy) {
mapped.quantity = 0
mapped.price = 0
mapped.unitPrice = 0
mapped.subtotal = 0
mapped.remark = ''
}
return mapped
})
/** 写入产品明细并刷新金额 */
const applyContractProductsToForm = async (products: ExtendedContractProductForm[], contractType: string) => {
form.value.products = products
await enrichContractProductBrandMeta(form.value.products)
const isFw = isFrameworkContractType(contractType)
form.value.products.forEach((product: any) => {
normalizeFrameworkProductPricing(product, isFw)
if (!isFw || !isFrameworkPriceMarket(product)) {
calculateProductAmount(product)
}
})
if (isFw) {
form.value.amount = 0
} else {
calculateTotalAmount()
}
}
/** 复制合同:仅带入产品明细,客户等信息由用户重新填写 */
const loadContractForCopy = async (source: ContractVO) => {
await initNewContractForm()
const contractType = source.contractType || 'sale'
form.value.contractType = contractType
let products = Array.isArray(source.products) ? source.products : []
// 列表 includeProducts 不含价阶;框架合同尝试拉详情补全,失败则回退列表明细
if (isFrameworkContractType(contractType)) {
try {
const res = await getContract(source.id)
if (res.data?.products?.length) {
products = res.data.products
form.value.contractType = res.data.contractType || contractType
}
} catch (error) {
console.warn('框架合同复制:加载详情价阶失败,使用列表产品明细', error)
}
}
if (!products.length) {
const res = await getContract(source.id)
products = res.data?.products || []
form.value.contractType = res.data?.contractType || contractType
}
if (products.length) {
const mapped = mapContractProductsFromSource(products, true)
await applyContractProductsToForm(mapped, form.value.contractType)
}
}
/** 加载合同详情 */
const loadContractDetail = async (id: number | string) => {
const res = await getContract(id)
@@ -1492,47 +1602,8 @@ const loadContractDetail = async (id: number | string) => {
}
if (contractData.products && Array.isArray(contractData.products)) {
form.value.products = contractData.products.map((product: any) => ({
...product,
productModel: product.model || '',
isToxic: Number(product.isToxic || 0),
isExplosive: Number(product.isExplosive || 0),
brandMode: product.brandMode || undefined,
brandId: product.brandId || undefined,
brandName: product.brandName || undefined,
brandOptions: [],
priceTiers: (product.priceTiers || []).map((t: any) => {
let ids: string[] = []
if (Array.isArray(t.letterOssIds) && t.letterOssIds.length) {
ids = t.letterOssIds.map((id: any) => String(id))
} else if (t.letterOssId != null && t.letterOssId !== '') {
ids = [String(t.letterOssId)]
}
return {
...t,
letterOssIds: ids
}
}),
taxRate: product.taxRate || 6,
taxAmount: product.taxAmount ?? 0,
packageSpec: product.packageSpec || '',
deliveryMethod: product.deliveryMethod || ''
}))
await enrichContractProductBrandMeta(form.value.products)
const isFw = isFrameworkContractType(contractData.contractType)
form.value.products.forEach((product: any) => {
normalizeFrameworkProductPricing(product, isFw)
if (!isFw || !isFrameworkPriceMarket(product)) {
calculateProductAmount(product)
}
})
if (isFw) {
form.value.amount = 0
} else {
calculateTotalAmount()
}
const products = mapContractProductsFromSource(contractData.products)
await applyContractProductsToForm(products, contractData.contractType)
}
if (form.value.startDate && form.value.endDate) {
+9 -4
View File
@@ -1098,7 +1098,11 @@ import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
import { getOpportunity } from '@/api/crm/opportunity';
import { getQuote } from '@/api/crm/quote';
import { isFrameworkContractType, isContractEditableStatus, getContractReeditConfirmMessage } from './contractType';
import { displayFrameworkMoney, displayFrameworkQuantity } from './frameworkProductPricing';
import {
displayFrameworkMoney,
displayFrameworkQuantity,
formatFrameworkQuantityForPrint
} from './frameworkProductPricing';
import {
parseSettlementClauseDrafts,
resolveSettlementClauseContent
@@ -1594,6 +1598,8 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
return dictStore.getLabel('crm_product_unit', unitValue) || unitValue || '';
};
const isFrameworkContract = isFrameworkContractType(contractData.contractType);
// 构建产品明细表格行
const productRows =
contractData.products
@@ -1610,7 +1616,7 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${getUnitLabel(product.unit)}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatFrameworkQuantityForPrint(product, isFrameworkContract)}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(product.price)}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(beforeTax)}</td>
@@ -1699,7 +1705,6 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
const totalWithExtrasChinese = numberToChinese(totalWithExtras);
const freightText = freightAmount > 0 ? `¥${formatNumber(freightAmount)}` : '-';
const barrelDepositText = barrelDepositAmount > 0 ? `¥${formatNumber(barrelDepositAmount)}` : '-';
const isFrameworkContract = isFrameworkContractType(contractData.contractType);
const totalWithExtrasText = isFrameworkContract
? '随行就市'
: `${totalWithExtrasChinese}(¥${formatNumber(totalWithExtras)})`;
@@ -1962,7 +1967,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.productName || ''}</td>
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
<td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${formatBrandForPrint(product.brandMode, product.brandName)}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${product.quantity || 0}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatFrameworkQuantityForPrint(product, isFrameworkContract)}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px;">${taxRateText}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: center; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(product.price)}</td>
<td style="padding: 4px 3px; border: 1px solid #000; text-align: right; font-size: 12px; overflow: hidden; word-break: break-all;">${formatNumber(beforeTax)}</td>
@@ -18,6 +18,8 @@ export type FrameworkPriceMode =
(typeof FRAMEWORK_PRICE_MODE)[keyof typeof FRAMEWORK_PRICE_MODE]
export const FRAMEWORK_QUANTITY_LABEL = '按客户需求'
/** 打印/PDF 富文本中「按客户需求」对应的用量文案 */
export const FRAMEWORK_QUANTITY_PRINT_LABEL = '按实际到货数量'
export const FRAMEWORK_PRICE_LABEL = '随行就市'
export type FrameworkPricingProduct = {
@@ -160,6 +162,20 @@ export function displayFrameworkQuantity(product: FrameworkPricingProduct, isFra
return formatFrameworkQuantityDisplay(normalizeFrameworkProductPricing({ ...product }, true))
}
/** 打印/PDF 富文本用量列:按客户需求时显示「按实际到货数量」,否则与原先一致输出数量 */
export function formatFrameworkQuantityForPrint(
product: FrameworkPricingProduct,
isFramework: boolean
): string {
if (isFramework) {
const normalized = normalizeFrameworkProductPricing({ ...product }, true)
if (isFrameworkQuantityCustomerDemand(normalized)) {
return FRAMEWORK_QUANTITY_PRINT_LABEL
}
}
return String(product.quantity || 0)
}
export function displayFrameworkMoney(
product: FrameworkPricingProduct,
field: 'price' | 'subtotal' | 'taxAmount',
@@ -320,7 +320,8 @@ import { ElMessage } from 'element-plus'
import {
fetchCustomerDetail,
fetchCreateCustomer,
fetchUpdateCustomer
fetchUpdateCustomer,
fetchCheckDuplicate
} from '@/api/crm/customer'
import type { CustomerForm as CustomerFormType } from '@/api/crm/customerTypes'
import { addInvoiceInfo } from '@/api/crm/invoiceInfo'
@@ -377,8 +378,40 @@ function unwrapApiData<T>(res: T | { data?: T }): T {
}
// 表单校验规则
const validateCustomerNameUnique = (
_rule: unknown,
value: string,
callback: (error?: Error) => void
) => {
const name = (value || '').trim()
if (!name) {
callback()
return
}
fetchCheckDuplicate({
customerName: name,
excludeId: form.value.id != null ? String(form.value.id) : undefined
})
.then((res) => {
const data = unwrapApiData(res as Record<string, unknown> | { data?: Record<string, unknown> })
if (data?.isDuplicate && Array.isArray(data.duplicates) && data.duplicates.length) {
const dup = data.duplicates[0] as Record<string, unknown>
const customerNo = (dup.customerNo || dup.customer_no || '') as string
callback(
new Error(customerNo ? `客户名称已存在(编号:${customerNo})` : '客户名称已存在')
)
return
}
callback()
})
.catch(() => callback())
}
const rules = ref<Record<string, FormItemRule[]>>({
customerName: [{ required: true, message: '客户名称不能为空', trigger: 'blur' }],
customerName: [
{ required: true, message: '客户名称不能为空', trigger: 'blur' },
{ validator: validateCustomerNameUnique, trigger: 'blur' }
],
shortName: [{ required: true, message: '客户简称不能为空', trigger: 'blur' }],
customerType: [{ required: true, message: '客户类型不能为空', trigger: 'change' }],
source: [{ required: true, message: '客户来源不能为空', trigger: 'change' }],
@@ -592,6 +625,7 @@ function buildSubmitPayload() {
delete payload.businessLicenseImg
// 显式传空串:后端按「重新生成」处理;有值则手改入库
payload.namePinyinAbbr = (form.value.namePinyinAbbr || '').trim()
payload.customerName = (form.value.customerName || '').trim()
return payload
}