feat(crm): sync contract number date segment with sign date

Align standard contract nos on create/update (keep seq, bump on collision),
preview the change in contract forms, and sync name parentheses.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-03 11:46:11 +08:00
1 parent f01057d902
commit 594e4ce38e
5 files changed
+260 -202

No files matched your search

@@ -19,7 +19,12 @@
<el-input v-model="form.contractNo" placeholder="系统自动生成,格式:SX/X-JT20250926002" maxlength="50" />
</el-form-item>
<el-form-item label="合同名称" prop="contractName">
<el-input v-model="form.contractName" placeholder="可不填,系统将自动生成" maxlength="100" show-word-limit />
<el-input
v-model="form.contractName"
placeholder="可不填,自动生成:供方简称-需方简称(合同编号)"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item label="合同类型" prop="contractType" required>
<el-select v-model="form.contractType" placeholder="请选择合同类型" style="width: 100%" @change="handleContractTypeChange">
@@ -242,7 +247,7 @@
</div>
</div>
<div class="list-body list-wrap">
<el-table :data="form.products" class="list-table" stripe @selection-change="handleProductSelectionChange">
<ListTable table-key="crm-contract-components-contract-form-dialog" :data="form.products" class="list-table" stripe @selection-change="handleProductSelectionChange">
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column label="序号" type="index" min-width="60" align="center" header-align="center" />
<el-table-column label="产品名称" min-width="160" :show-overflow-tooltip="true">
@@ -410,7 +415,7 @@
</el-button>
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
@@ -427,7 +432,7 @@
</el-button>
<div class="panel dialog-list-panel mb-3">
<div class="list-body list-wrap dialog-list-body-only">
<el-table :data="prod.priceTiers || []" class="list-table" stripe empty-text="暂无调价区间">
<ListTable table-key="crm-contract-components-contract-form-dialog-2" :data="prod.priceTiers || []" class="list-table" stripe empty-text="暂无调价区间">
<el-table-column type="index" label="#" min-width="50" align="center" header-align="center" />
<el-table-column label="开始日期" min-width="150">
<template #default="scope">
@@ -468,7 +473,7 @@
</el-button>
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
</div>
@@ -601,6 +606,7 @@ import {
} from '@/utils/crm-product-brand'
import { fetchProductDetail } from '@/api/crm/product'
import ApprovalDialog from './ApprovalDialog.vue'
import { omitEmptyExceptPartyFields } from '../partyFields'
import { getContactByCustomerId } from '@/api/crm/customer'
import type { CustomerVO, ContactVO } from '@/api/crm/customer/types'
import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo'
@@ -1348,6 +1354,31 @@ const updateContractNoBySupplier = (newSupplierCode: string | undefined) => {
}
}
const CONTRACT_NO_STD_RE = /^([A-Z]+)\/X-JT(\d{8})(\d{3})$/
/** 签订日变更时:标准编号只换日期段,保留序号;并同步名称括号内编号 */
const syncContractNoDateFromSignDate = (signDateVal: string | null) => {
if (!signDateVal || !form.value.contractNo) return
const m = String(form.value.contractNo).match(CONTRACT_NO_STD_RE)
if (!m) return
const dateStr = signDateVal.replace(/-/g, '')
if (dateStr.length !== 8) return
if (m[2] === dateStr) return
const nextNo = `${m[1]}/X-JT${dateStr}${m[3]}`
form.value.contractNo = nextNo
if (form.value.contractName) {
const replaced = form.value.contractName.replace(/[((][^))]*[))]$/, `(${nextNo})`)
if (replaced !== form.value.contractName) {
form.value.contractName = replaced
} else {
rebuildContractName()
}
}
}
const COMMON_SUPPLIER_SHORT_NAMES = ['山峡', 'SX']
const extractContractNameParts = (contractName: string) => {
@@ -1366,7 +1397,7 @@ const isSupplierShortNamePart = (part: string, supplierShort: string) => {
return part === supplierShort || COMMON_SUPPLIER_SHORT_NAMES.some((name) => part.includes(name))
}
/** 从现有合同名称推断客户简称(切换合同类型时不能依赖新类型解析旧格式) */
/** 从现有合同名称推断客户简称(统一格式:供方简称-需方简称) */
const inferCustomerShortNameFromContractName = (): string => {
if (customerShortName.value) return customerShortName.value
if (!form.value.contractName) return ''
@@ -1383,14 +1414,13 @@ const inferCustomerShortNameFromContractName = (): string => {
return isSupplierShortNamePart(parts[0], supplierShort) ? '' : parts[0]
}
// 统一格式:供方(交易主体)-需方(客户);供方在前则取第二段为客户简称
if (isSupplierShortNamePart(parts[0], supplierShort)) return parts[1]
if (isSupplierShortNamePart(parts[1], supplierShort)) return parts[0]
if (form.value.contractType === 'purchase') return parts[0]
return parts[1]
}
/** 按合同类型重建合同名称:销售/框架=供方简称-客户简称,采购=客户简称-供方简称 */
/** 重建合同名称:统一为 {供方简称}-{需方简称}({合同编号}),不因采购反序 */
const rebuildContractName = () => {
if (!form.value.contractName) return
@@ -1400,10 +1430,7 @@ const rebuildContractName = () => {
const { contractNoInName } = extractContractNameParts(form.value.contractName)
const contractNo = form.value.contractNo || contractNoInName
const isPurchase = form.value.contractType === 'purchase'
const nameBody = isPurchase
? [customerShort, supplierShort].filter(Boolean).join('-')
: [supplierShort, customerShort].filter(Boolean).join('-')
const nameBody = [supplierShort, customerShort].filter(Boolean).join('-')
if (!nameBody) return
@@ -1534,6 +1561,7 @@ const handleSignDateChange = (val: string | null) => {
form.value.startDate = ''
form.value.endDate = ''
}
syncContractNoDateFromSignDate(val)
nextTick(() => {
formRef.value?.validateField('startDate')
})
@@ -1919,11 +1947,7 @@ const handleSaveDraft = async () => {
data.attachmentFiles = attachmentJson
}
Object.keys(data).forEach((key) => {
if (data[key] === undefined || data[key] === '') {
delete data[key]
}
})
omitEmptyExceptPartyFields(data)
if (data.contactsName) {
data.contactName = data.contactsName
@@ -2086,11 +2110,7 @@ const handleSubmit = async () => {
data.attachmentFiles = attachmentJson
}
Object.keys(data).forEach((key) => {
if (data[key] === undefined || data[key] === '') {
delete data[key]
}
})
omitEmptyExceptPartyFields(data)
if (data.contactsName) {
data.contactName = data.contactsName
+63 -178
View File
@@ -19,7 +19,12 @@
</el-col>
<el-col :span="6">
<el-form-item label="合同名称" prop="contractName">
<el-input v-model="form.contractName" placeholder="可不填,系统将自动生成" maxlength="100" show-word-limit />
<el-input
v-model="form.contractName"
placeholder="可不填,自动生成:供方简称-需方简称(合同编号)"
maxlength="100"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="6">
@@ -600,6 +605,7 @@ import ContactSelector from '@/components/ContactSelector.vue';
import QuoteSelector from '@/components/QuoteSelector.vue';
import UserSelector from '@/components/UserSelector.vue';
import ApprovalDialog from './components/ApprovalDialog.vue';
import { omitEmptyExceptPartyFields } from './partyFields';
import { getContactByCustomerId } from '@/api/crm/customer';
import type { CustomerVO, ContactVO } from '@/api/crm/customer/types';
import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo';
@@ -1324,11 +1330,29 @@ const updateContractNoBySupplier = (newSupplierCode: string | undefined) => {
}
};
const CONTRACT_NO_STD_RE = /^([A-Z]+)\/X-JT(\d{8})(\d{3})$/;
/** 签订日变更时:标准编号只换日期段,保留序号;并同步名称括号内编号 */
const syncContractNoDateFromSignDate = (signDateVal: string | null) => {
if (!signDateVal || !form.value.contractNo) return;
const m = String(form.value.contractNo).match(CONTRACT_NO_STD_RE);
if (!m) return;
const dateStr = signDateVal.replace(/-/g, '');
if (dateStr.length !== 8) return;
if (m[2] === dateStr) return;
const nextNo = `${m[1]}/X-JT${dateStr}${m[3]}`;
form.value.contractNo = nextNo;
if (form.value.contractName) {
form.value.contractName = form.value.contractName.replace(/[((][^))]*[))]$/, `(${nextNo})`);
}
};
/**
* 更新合同名称中的供方简称
* 合同名称格式:
* - 销售合同、框架合同:供方简称-客户简称(合同编号)
* - 采购合同:客户简称-供方简称(合同编号)
* 统一格式:{供方简称}-{需方简称}({合同编号})
*/
const updateContractNameBySupplier = (newSupplierShortName: string | undefined) => {
if (!form.value.contractName || !newSupplierShortName) {
@@ -1336,7 +1360,6 @@ const updateContractNameBySupplier = (newSupplierShortName: string | undefined)
}
const contractName = form.value.contractName;
const contractType = form.value.contractType;
// 提取合同编号(在括号中,支持中文和英文括号)
const contractNoMatch = contractName.match(/[((](.+?)[))]$/);
@@ -1348,78 +1371,21 @@ const updateContractNameBySupplier = (newSupplierShortName: string | undefined)
// 常见的供方简称列表(用于识别和替换)
const commonSupplierNames = ['山峡', 'SX'];
// 根据合同类型解析和替换
if (contractType === 'sale' || contractType === 'framework') {
// 销售/框架合同格式:供方简称-客户简称(合同编号)
// 例如:山峡-客户A(SX/X-JT20250926002)
const parts = nameBody.split('-').map((p) => p.trim());
if (parts.length >= 2) {
// 替换第一个部分(供方简称)
parts[0] = newSupplierShortName;
const newNameBody = parts.join('-');
form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody;
} else if (parts.length === 1) {
// 如果只有一个部分,判断是否是供方简称
const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name));
if (isSupplierName) {
// 是供方简称,替换它
form.value.contractName = contractNo ? `${newSupplierShortName}(${contractNo})` : newSupplierShortName;
} else {
// 可能是客户简称,保持原样,在前面添加供方简称
form.value.contractName = contractNo ? `${newSupplierShortName}-${parts[0]}(${contractNo})` : `${newSupplierShortName}-${parts[0]}`;
}
// 统一格式:供方简称-需方简称(合同编号)
const parts = nameBody.split('-').map((p) => p.trim());
if (parts.length >= 2) {
parts[0] = newSupplierShortName;
const newNameBody = parts.join('-');
form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody;
} else if (parts.length === 1) {
const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name));
if (isSupplierName) {
form.value.contractName = contractNo ? `${newSupplierShortName}(${contractNo})` : newSupplierShortName;
} else {
form.value.contractName = contractNo
? `${newSupplierShortName}-${parts[0]}(${contractNo})`
: `${newSupplierShortName}-${parts[0]}`;
}
} else if (contractType === 'purchase') {
// 采购合同格式:客户简称-供方简称(合同编号)
// 例如:客户A-山峡(SX/X-JT20250926002)
const parts = nameBody.split('-').map((p) => p.trim());
if (parts.length >= 2) {
// 替换最后一个部分(供方简称)
parts[parts.length - 1] = newSupplierShortName;
const newNameBody = parts.join('-');
form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody;
} else if (parts.length === 1) {
// 如果只有一个部分,判断是否是供方简称
const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name));
if (isSupplierName) {
// 是供方简称,替换它
form.value.contractName = contractNo ? `${newSupplierShortName}(${contractNo})` : newSupplierShortName;
} else {
// 可能是客户简称,保持原样,在后面添加供方简称
form.value.contractName = contractNo ? `${parts[0]}-${newSupplierShortName}(${contractNo})` : `${parts[0]}-${newSupplierShortName}`;
}
}
} else {
// 其他类型,尝试替换所有可能的供方简称
let updatedName = contractName;
let replaced = false;
for (const oldName of commonSupplierNames) {
if (updatedName.includes(oldName)) {
// 使用正则替换,保持格式
updatedName = updatedName.replace(new RegExp(oldName, 'g'), newSupplierShortName);
replaced = true;
break;
}
}
// 如果没有找到已知的供方简称,尝试在名称开头或结尾查找
if (!replaced && nameBody) {
// 检查是否以常见供方简称开头或结尾
for (const oldName of commonSupplierNames) {
if (nameBody.startsWith(oldName)) {
updatedName = contractName.replace(oldName, newSupplierShortName);
replaced = true;
break;
} else if (nameBody.endsWith(oldName)) {
updatedName = contractName.replace(oldName, newSupplierShortName);
replaced = true;
break;
}
}
}
form.value.contractName = updatedName;
}
// 如果合同编号已更新,同步更新合同名称中的合同编号
@@ -1430,10 +1396,8 @@ const updateContractNameBySupplier = (newSupplierShortName: string | undefined)
};
/**
* 更新合同名称中的客户简称
* 合同名称格式:
* - 销售合同、框架合同:供方简称-客户简称(合同编号)
* - 采购合同:客户简称-供方简称(合同编号)
* 更新合同名称中的客户简称(需方)
* 统一格式:{供方简称}-{需方简称}({合同编号})
*/
const updateContractNameByCustomer = (newCustomerShortName: string | undefined) => {
if (!form.value.contractName || !newCustomerShortName) {
@@ -1441,7 +1405,6 @@ const updateContractNameByCustomer = (newCustomerShortName: string | undefined)
}
const contractName = form.value.contractName;
const contractType = form.value.contractType;
// 提取合同编号(在括号中,支持中文和英文括号)
const contractNoMatch = contractName.match(/[((](.+?)[))]$/);
@@ -1453,92 +1416,21 @@ const updateContractNameByCustomer = (newCustomerShortName: string | undefined)
// 常见的供方简称列表(用于识别,避免误替换)
const commonSupplierNames = ['山峡', 'SX'];
// 根据合同类型解析和替换
if (contractType === 'sale' || contractType === 'framework') {
// 销售/框架合同格式:供方简称-客户简称(合同编号)
// 例如:山峡-客户A(SX/X-JT20250926002)
const parts = nameBody.split('-').map((p) => p.trim());
if (parts.length >= 2) {
// 替换第二个部分(客户简称)
parts[1] = newCustomerShortName;
const newNameBody = parts.join('-');
form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody;
} else if (parts.length === 1) {
// 如果只有一个部分,判断是否是供方简称
const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name));
if (isSupplierName) {
// 是供方简称,在后面添加客户简称
form.value.contractName = contractNo ? `${parts[0]}-${newCustomerShortName}(${contractNo})` : `${parts[0]}-${newCustomerShortName}`;
} else {
// 可能是客户简称,替换它,但需要添加供方简称
// 这里保持原样,只替换客户简称部分(如果当前没有供方简称,则只更新客户简称)
form.value.contractName = contractNo ? `${newCustomerShortName}(${contractNo})` : newCustomerShortName;
}
// 统一格式:供方简称-需方简称(合同编号)
const parts = nameBody.split('-').map((p) => p.trim());
if (parts.length >= 2) {
parts[1] = newCustomerShortName;
const newNameBody = parts.join('-');
form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody;
} else if (parts.length === 1) {
const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name));
if (isSupplierName) {
form.value.contractName = contractNo
? `${parts[0]}-${newCustomerShortName}(${contractNo})`
: `${parts[0]}-${newCustomerShortName}`;
} else {
form.value.contractName = contractNo ? `${newCustomerShortName}(${contractNo})` : newCustomerShortName;
}
} else if (contractType === 'purchase') {
// 采购合同格式:客户简称-供方简称(合同编号)
// 例如:客户A-山峡(SX/X-JT20250926002)
const parts = nameBody.split('-').map((p) => p.trim());
if (parts.length >= 2) {
// 替换第一个部分(客户简称)
parts[0] = newCustomerShortName;
const newNameBody = parts.join('-');
form.value.contractName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody;
} else if (parts.length === 1) {
// 如果只有一个部分,判断是否是供方简称
const isSupplierName = commonSupplierNames.some((name) => parts[0].includes(name));
if (isSupplierName) {
// 是供方简称,在前面添加客户简称
form.value.contractName = contractNo ? `${newCustomerShortName}-${parts[0]}(${contractNo})` : `${newCustomerShortName}-${parts[0]}`;
} else {
// 可能是客户简称,替换它
form.value.contractName = contractNo ? `${newCustomerShortName}(${contractNo})` : newCustomerShortName;
}
}
} else {
// 其他类型,尝试智能替换客户简称
// 策略:如果名称中包含已知的供方简称,则替换另一个部分;否则尝试替换非供方简称的部分
let updatedName = contractName;
let replaced = false;
// 先检查是否包含供方简称
let supplierNameInBody = '';
for (const supplierName of commonSupplierNames) {
if (nameBody.includes(supplierName)) {
supplierNameInBody = supplierName;
break;
}
}
if (supplierNameInBody) {
// 如果包含供方简称,替换另一个部分
const parts = nameBody.split('-').map((p) => p.trim());
if (parts.length >= 2) {
// 找到供方简称的位置并替换另一个部分
const supplierIndex = parts.findIndex((p) => p.includes(supplierNameInBody));
const customerIndex = supplierIndex === 0 ? 1 : 0;
parts[customerIndex] = newCustomerShortName;
const newNameBody = parts.join('-');
updatedName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody;
replaced = true;
}
}
// 如果没有替换成功,尝试直接替换非供方简称的部分
if (!replaced && nameBody) {
const parts = nameBody.split('-').map((p) => p.trim());
if (parts.length >= 2) {
// 替换第一个部分(假设是客户简称)
parts[0] = newCustomerShortName;
const newNameBody = parts.join('-');
updatedName = contractNo ? `${newNameBody}(${contractNo})` : newNameBody;
} else if (parts.length === 1 && !commonSupplierNames.some((name) => parts[0].includes(name))) {
// 如果只有一个部分且不是供方简称,则替换它
updatedName = contractNo ? `${newCustomerShortName}(${contractNo})` : newCustomerShortName;
}
}
form.value.contractName = updatedName;
}
// 如果合同编号已更新,同步更新合同名称中的合同编号
@@ -1679,6 +1571,7 @@ const handleSignDateChange = (val: string | null) => {
form.value.startDate = '';
form.value.endDate = '';
}
syncContractNoDateFromSignDate(val);
// 触发合同期限字段的验证
nextTick(() => {
formRef.value?.validateField('startDate');
@@ -2184,12 +2077,8 @@ const handleSaveDraft = async () => {
data.attachmentFiles = JSON.stringify(attachmentsWithCategory);
}
// 清理未填写的字段,避免发送 undefined 到后端
Object.keys(data).forEach((key) => {
if (data[key] === undefined || data[key] === '') {
delete data[key];
}
});
// 清理未填写的字段;供/需方空字符串保留,以便清空落库
omitEmptyExceptPartyFields(data);
// 联系人字段映射(前端contactsName/contactsPhone映射回后端contactName/contactMobile)
if (data.contactsName) {
@@ -2366,12 +2255,8 @@ const handleSubmit = async () => {
data.attachmentFiles = JSON.stringify(attachmentsWithCategory);
}
// 清理未填写的字段,避免发送 undefined 到后端
Object.keys(data).forEach((key) => {
if (data[key] === undefined || data[key] === '') {
delete data[key];
}
});
// 清理未填写的字段;供/需方空字符串保留,以便清空落库
omitEmptyExceptPartyFields(data);
// 联系人字段映射(前端contactsName/contactsPhone映射回后端contactName/contactMobile)
if (data.contactsName) {