This commit is contained in:
zlwu committed 2026-08-04 15:33:41 +08:00
1 parent 5fe53c5481
commit 5cf67f60d1
39 files changed
+18999 -15713

No files matched your search

@@ -419,7 +419,6 @@ body {
flex-wrap: wrap;
gap: 8px;
padding: 10px 16px;
border-bottom: 1px solid var(--mint-border);
h2,
h3 {
@@ -218,7 +218,7 @@
align="right"
>
<template #default="scope">
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
<span v-if="isFrameworkContractType(detailContract.contractType)" class="text-muted">随行就市</span>
<span v-else class="money-display">¥{{ parseFloat(scope.row.price || 0).toFixed(2) }}</span>
</template>
</el-table-column>
@@ -230,7 +230,7 @@
align="right"
>
<template #default="scope">
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
<span v-if="isFrameworkContractType(detailContract.contractType)" class="text-muted">随行就市</span>
<span v-else class="money-display">¥{{ parseFloat(scope.row.subtotal || 0).toFixed(2) }}</span>
</template>
</el-table-column>
@@ -242,7 +242,7 @@
align="right"
>
<template #default="scope">
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
<span v-if="isFrameworkContractType(detailContract.contractType)" class="text-muted">随行就市</span>
<span v-else class="money-display">¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
</template>
</el-table-column>
@@ -265,7 +265,7 @@
</div>
</div>
<template v-if="detailContract.contractType === 'framework' && !hideProductPrice">
<template v-if="isFrameworkContractType(detailContract.contractType) && !hideProductPrice">
<el-divider content-position="left">框架合同调价明细</el-divider>
<p class="text-muted" style="font-size: 13px; margin-bottom: 12px">按生效区间记录含税单价及调价函;后续结算可据此核对。</p>
<div
@@ -850,6 +850,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, defineAsyncComponent, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { isFrameworkContractType } from '../contractType'
import {
CircleCheck,
CircleClose,
@@ -1771,7 +1772,7 @@ const replacePlaceholders = (htmlContent: string, contractData: ContractVO | und
const typeLabel = dictStore.getLabel('crm_contract_type', contractData.contractType);
return typeLabel || '销售合同';
};
const isFrameworkContract = getContractTypeName() === '框架合同';
const isFrameworkContract = isFrameworkContractType(contractData.contractType);
// 计算产品相关数据
const productTotal = parseFloat(String(contractData.amount || 0));
@@ -11,8 +11,20 @@
:close-on-click-modal="false"
@opened="handleOpened"
>
<div class="contract-form-body">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<div
class="contract-form-body"
v-loading="formLoading"
element-loading-text="正在加载合同数据..."
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="120px"
class="crm-dialog-form"
:disabled="formLoading"
>
<!-- 合同基础信息(一行 4 列) -->
<div class="contract-basic-info">
<el-form-item label="合同编号" prop="contractNo">
@@ -420,7 +432,7 @@
</div>
<!-- 框架合同调价区间 -->
<template v-if="form.contractType === 'framework'">
<template v-if="isFrameworkContractType(form.contractType)">
<el-divider content-position="left">框架合同调价区间</el-divider>
<p class="text-muted mb-3" style="font-size: 13px">
按产品维护生效日期区间与含税单价(可选);若填写区间则须完整(日期、单价、调价函)。合同总金额不与此联动,仍为 0。
@@ -511,11 +523,18 @@
<el-button
class="btn-ghost"
:loading="saveDraftLoading"
:disabled="formLoading"
@click="handleSaveDraft"
>
<el-icon><FolderChecked /></el-icon>保存草稿
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
<el-button
class="btn-main"
type="primary"
:loading="submitLoading"
:disabled="formLoading"
@click="handleSubmit"
>
<el-icon><Promotion /></el-icon>提交审批
</el-button>
</div>
@@ -556,7 +575,7 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue'
import { ref, reactive, computed, nextTick, watch } from 'vue'
import type { FormInstance, UploadRequestOptions } from 'element-plus'
import { ElMessage } from 'element-plus'
import {
@@ -567,6 +586,7 @@ import {
type ContractProductForm,
type ContractProductPriceTierForm
} from '@/api/crm/contract/index'
import { isFrameworkContractType } from '../contractType'
import { useDictStore } from '@/store/modules/dict'
import { useUserStore } from '@/store/modules/user'
import { $modal } from '@/utils/modal'
@@ -607,7 +627,7 @@ import {
import { fetchProductDetail } from '@/api/crm/product'
import ApprovalDialog from './ApprovalDialog.vue'
import { omitEmptyExceptPartyFields } from '../partyFields'
import { getContactByCustomerId } from '@/api/crm/customer'
import { getContactByCustomerId, fetchCustomerDetail } from '@/api/crm/customer'
import type { CustomerVO, ContactVO } from '@/api/crm/customer/types'
import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo'
import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo'
@@ -744,8 +764,9 @@ const originalTemplateContent = ref<string>('')
// 供方信息列表
const supplierList = ref<SupplierInfoVO[]>([])
// 客户简称(用于按合同类型重建合同名称)
// 客户简称 / 开票公司名(用于按供需方面板匹配简称)
const customerShortName = ref('')
const customerInvoiceName = ref('')
// 表单校验规则
const rules = ref({
@@ -773,12 +794,25 @@ const rules = ref({
// 附件列表
const attachmentList = ref<ContractAttachmentItem[]>([])
const isFrameworkContract = computed(() => form.value.contractType === 'framework')
const isFrameworkContract = computed(() => isFrameworkContractType(form.value.contractType))
// 加载状态
const formLoading = ref(false)
const saveDraftLoading = ref(false)
const submitLoading = ref(false)
/** 编辑打开时立即进入加载态,避免空白表单可被误改 */
watch(
() => props.visible,
(visible) => {
if (visible && props.editId) {
formLoading.value = true
} else if (!visible) {
formLoading.value = false
}
}
)
// 组件引用
const formRef = ref<FormInstance>()
const approvalDialogRef = ref()
@@ -820,21 +854,35 @@ interface QuoteVO {
/** 对话框打开后的初始化 */
const handleOpened = async () => {
resetPartyExpand()
await Promise.all([loadSupplierList(), loadUserList()])
const isEdit = !!props.editId
if (isEdit) {
formLoading.value = true
}
try {
await Promise.all([loadSupplierList(), loadUserList()])
if (props.editId) {
await loadContractDetail(props.editId)
} else {
resetForm()
form.value.signDate = new Date().toISOString().split('T')[0]
handleSignDateChange(form.value.signDate)
if (props.editId) {
await loadContractDetail(props.editId)
} else {
resetForm()
form.value.signDate = new Date().toISOString().split('T')[0]
handleSignDateChange(form.value.signDate)
// 设置默认供方主体为"SX"
const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
if (defaultSupplier) {
form.value.supplierId = defaultSupplier.id
await handleSupplierChange(defaultSupplier.id)
// 设置默认供方主体为"SX"
const defaultSupplier = supplierList.value.find((supplier) => supplier.supplierCode === 'SX')
if (defaultSupplier) {
form.value.supplierId = defaultSupplier.id
await handleSupplierChange(defaultSupplier.id)
}
}
} catch (error: any) {
console.error('合同表单初始化失败:', error)
$modal.msgError(error?.message || '加载合同数据失败')
if (isEdit) {
dialogVisible.value = false
}
} finally {
formLoading.value = false
}
}
@@ -891,6 +939,7 @@ const resetForm = () => {
resetPartyExpand()
originalTemplateContent.value = ''
customerShortName.value = ''
customerInvoiceName.value = ''
contractDateRange.value = [] as any
attachmentList.value = []
selectedProducts.value = []
@@ -972,7 +1021,7 @@ const loadContractDetail = async (id: number | string) => {
await enrichContractProductBrandMeta(form.value.products)
if (contractData.contractType === 'framework') {
if (isFrameworkContractType(contractData.contractType)) {
form.value.products.forEach((product: any) => {
product.price = 0
product.subtotal = 0
@@ -995,7 +1044,25 @@ const loadContractDetail = async (id: number | string) => {
await handleAttachmentData()
}
customerShortName.value = inferCustomerShortNameFromContractName()
customerShortName.value = ''
customerInvoiceName.value = ''
if (form.value.customerId) {
try {
const [invoiceRes, customerRes] = await Promise.all([
getDefaultInvoiceInfoByCustomerId(form.value.customerId),
fetchCustomerDetail({ id: String(form.value.customerId) })
])
customerInvoiceName.value = invoiceRes?.data?.companyName || ''
customerShortName.value = customerRes?.data?.shortName || ''
if (!form.value.customerName && customerRes?.data?.customerName) {
form.value.customerName = customerRes.data.customerName
}
} catch {
customerInvoiceName.value = ''
}
}
syncPartiesSwappedFlagFromPanels()
rebuildContractName()
}
/** 选择客户 */
@@ -1023,9 +1090,19 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
form.value.quoteNo = ''
try {
const invoiceRes = await getDefaultInvoiceInfoByCustomerId(customer.id)
const [invoiceRes, customerRes] = await Promise.all([
getDefaultInvoiceInfoByCustomerId(customer.id),
fetchCustomerDetail({ id: String(customer.id) })
])
const invoiceData = invoiceRes?.data
// 简称以客户主数据为准(列表/选择器可能缺字段)
customerShortName.value =
(customerRes?.data?.shortName || customer.shortName || '').trim()
if (customerRes?.data?.customerName) {
form.value.customerName = customerRes.data.customerName
}
customerInvoiceName.value = invoiceData?.companyName || ''
if (invoiceData && invoiceData.companyName) {
if (isPartiesSwapped.value) {
form.value.partyA = invoiceData.companyName || ''
@@ -1065,6 +1142,7 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
}
} catch (error) {
console.error('获取客户开票信息失败:', error)
customerInvoiceName.value = ''
if (isPartiesSwapped.value) {
form.value.partyA = customer.customerName
form.value.partyAAddress = fullAddress
@@ -1116,7 +1194,9 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
console.error('获取客户联系人失败:', error)
}
customerShortName.value = customer.shortName || ''
if (!customerShortName.value) {
customerShortName.value = (customer.shortName || '').trim()
}
rebuildContractName()
}
}
@@ -1198,7 +1278,7 @@ const handleQuoteConfirm = (quote: QuoteVO) => {
$modal
.confirm('是否要从报价单中带入产品明细?')
.then(async () => {
const isFw = form.value.contractType === 'framework'
const isFw = isFrameworkContractType(form.value.contractType)
form.value.products = quote.productList!.map((item: any) => ({
productId: item.productId,
productName: item.productName,
@@ -1363,82 +1443,79 @@ const syncContractNoDateFromSignDate = (signDateVal: string | null) => {
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()
}
}
rebuildContractName()
}
const COMMON_SUPPLIER_SHORT_NAMES = ['山峡', 'SX']
/** 面板单位全称 → 匹配交易主体/客户主数据取简称;对不上用全称 */
const resolvePartyShortName = (partyName?: string): string => {
const party = (partyName || '').trim()
if (!party) return ''
const extractContractNameParts = (contractName: string) => {
const contractNoMatch = contractName.match(/[((](.+?)[))]$/)
const contractNoInName = contractNoMatch ? contractNoMatch[1] : ''
const nameBody = contractNoMatch ? contractName.substring(0, contractNoMatch.index).trim() : contractName.trim()
return { contractNoInName, nameBody }
}
const getSupplierShortName = (): string => {
const supplier = form.value.supplierId ? supplierList.value.find((s) => s.id === form.value.supplierId) : undefined
return supplier?.shortName || supplier?.supplierName || '山峡'
const supplierName = (supplier?.supplierName || '').trim()
const supplierShort = (supplier?.shortName || supplierName || '').trim()
if (supplierName && party === supplierName) return supplierShort || party
if (supplierShort && party === supplierShort) return supplierShort
const customerName = (form.value.customerName || '').trim()
const customerShort = (customerShortName.value || '').trim()
const invoiceName = (customerInvoiceName.value || '').trim()
if (customerName && party === customerName) return customerShort || customerName
if (customerShort && party === customerShort) return customerShort
if (invoiceName && party === invoiceName) return customerShort || invoiceName
return party
}
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 ''
const { nameBody } = extractContractNameParts(form.value.contractName)
const parts = nameBody
.split('-')
.map((p) => p.trim())
.filter(Boolean)
if (parts.length === 0) return ''
const supplierShort = getSupplierShortName()
if (parts.length === 1) {
return isSupplierShortNamePart(parts[0], supplierShort) ? '' : parts[0]
}
// 统一格式:供方(交易主体)-需方(客户);供方在前则取第二段为客户简称
if (isSupplierShortNamePart(parts[0], supplierShort)) return parts[1]
if (isSupplierShortNamePart(parts[1], supplierShort)) return parts[0]
return parts[1]
}
/** 重建合同名称:统一为 {供方简称}-{需方简称}({合同编号}),不因采购反序 */
/** 重建合同名称:始终 {供方简称}-{需方简称}({合同编号}),随面板调换 */
const rebuildContractName = () => {
if (!form.value.contractName) return
const customerShort = inferCustomerShortNameFromContractName()
const supplierShort = getSupplierShortName()
if (!customerShort && !supplierShort) return
const { contractNoInName } = extractContractNameParts(form.value.contractName)
const contractNo = form.value.contractNo || contractNoInName
const nameBody = [supplierShort, customerShort].filter(Boolean).join('-')
const partyAShort = resolvePartyShortName(form.value.partyA)
const partyBShort = resolvePartyShortName(form.value.partyB)
const nameBody = [partyAShort, partyBShort].filter(Boolean).join('-')
if (!nameBody) return
const contractNo = (form.value.contractNo || '').trim()
form.value.contractName = contractNo ? `${nameBody}(${contractNo})` : nameBody
if (customerShort) {
customerShortName.value = customerShort
}
/** 根据面板与交易主体/客户推断是否已调换(编辑回显用) */
const syncPartiesSwappedFlagFromPanels = () => {
const partyA = (form.value.partyA || '').trim()
const partyB = (form.value.partyB || '').trim()
if (!partyA && !partyB) {
isPartiesSwapped.value = false
return
}
const supplier = form.value.supplierId
? supplierList.value.find((s) => s.id === form.value.supplierId)
: undefined
const supplierName = (supplier?.supplierName || '').trim()
if (supplierName) {
if (partyB === supplierName && partyA !== supplierName) {
isPartiesSwapped.value = true
return
}
if (partyA === supplierName) {
isPartiesSwapped.value = false
return
}
}
const customerName = (form.value.customerName || '').trim()
const invoiceName = (customerInvoiceName.value || '').trim()
if ((customerName && partyA === customerName) || (invoiceName && partyA === invoiceName)) {
isPartiesSwapped.value = true
return
}
if ((customerName && partyB === customerName) || (invoiceName && partyB === invoiceName)) {
isPartiesSwapped.value = false
}
}
/** 供需方调换 */
/** 供需方调换:仅交换面板字段,勿再调 handleSupplierChange(会按交易主体覆盖一侧,导致两边都变成交易主体) */
const handleSwapParties = async () => {
const currentSupplierId = form.value.supplierId
const temp = {
partyA: form.value.partyA,
partyAAddress: form.value.partyAAddress,
@@ -1471,17 +1548,14 @@ const handleSwapParties = async () => {
form.value.partyBAccount = temp.partyAAccount
isPartiesSwapped.value = !isPartiesSwapped.value
if (currentSupplierId) {
await handleSupplierChange(currentSupplierId)
}
rebuildContractName()
$modal.msgSuccess('供需方信息已调换')
}
/** 合同类型变化处理 */
const handleContractTypeChange = (value: string) => {
if (value === 'framework') {
if (isFrameworkContractType(value)) {
form.value.amount = 0
form.value.products?.forEach((p) => {
p.price = 0
@@ -1573,7 +1647,7 @@ const handleAddProduct = () => {
/** 产品选择确认 */
const handleProductSelected = (products: any[]) => {
const isFw = form.value.contractType === 'framework'
const isFw = isFrameworkContractType(form.value.contractType)
products.forEach((product) => {
const brandMeta = initLineBrandMetaFromProduct(product)
form.value.products.push({
@@ -1718,7 +1792,7 @@ const handlePackageSpecChange = (product: ExtendedContractProductForm, value: st
/** 计算产品金额 */
const calculateProductAmount = (product: ExtendedContractProductForm) => {
if (form.value.contractType === 'framework') {
if (isFrameworkContractType(form.value.contractType)) {
product.price = 0
product.subtotal = 0
product.taxAmount = 0
@@ -1742,7 +1816,7 @@ const calculateProductAmount = (product: ExtendedContractProductForm) => {
/** 计算合计金额 */
const calculateTotalAmount = () => {
if (form.value.contractType === 'framework') {
if (isFrameworkContractType(form.value.contractType)) {
form.value.amount = 0
return
}
@@ -1822,7 +1896,7 @@ const buildProductsPayload = () => {
if (product.id) {
base.id = product.id
}
if (form.value.contractType === 'framework') {
if (isFrameworkContractType(form.value.contractType)) {
base.price = 0
base.subtotal = 0
base.taxAmount = 0
@@ -1931,6 +2005,7 @@ const handleSaveDraft = async () => {
return
}
rebuildContractName()
saveDraftLoading.value = true
try {
const attachmentJson = serializeContractAttachments(attachmentList.value)
@@ -2012,7 +2087,7 @@ const handleSubmit = async () => {
}
}
const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || form.value.contractType === 'framework'
const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || isFrameworkContractType(form.value.contractType)
const validateProductRowCommon = (product: any, rowNum: number): boolean => {
if (!product.productName || String(product.productName).trim() === '') {
@@ -2044,7 +2119,7 @@ const handleSubmit = async () => {
const product = form.value.products[i]
const rowNum = i + 1
if (!validateProductRowCommon(product, rowNum)) return
const isFw = form.value.contractType === 'framework'
const isFw = isFrameworkContractType(form.value.contractType)
if (!isFw) {
if (!product.price || Number(product.price) <= 0) {
$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空且必须大于0`)
@@ -2098,6 +2173,7 @@ const handleSubmit = async () => {
submitLoading.value = true
try {
rebuildContractName()
const attachmentJson = serializeContractAttachments(attachmentList.value)
const data: any = { ...form.value }
@@ -2215,6 +2291,8 @@ const loadSupplierList = async () => {
}
.contract-form-body {
position: relative;
min-height: 280px;
padding: 0 4px;
}
@@ -0,0 +1,12 @@
/** 框架类合同类型(含历史 framework,兼容未刷完的数据) */
export const FRAMEWORK_CONTRACT_TYPES = [
'framework',
'framework_sale',
'framework_purchase'
] as const
export type FrameworkContractType = (typeof FRAMEWORK_CONTRACT_TYPES)[number]
export function isFrameworkContractType(type?: string | null): boolean {
return FRAMEWORK_CONTRACT_TYPES.includes((type || '').trim() as FrameworkContractType)
}
+5 -4
View File
@@ -308,19 +308,19 @@
<el-table-column label="数量" prop="quantity" width="110" align="right" />
<el-table-column label="含税单价/元" prop="price" width="130" align="right">
<template #default="scope">
<span v-if="contract.contractType === 'framework'" class="text-muted">随行就市</span>
<span v-if="isFrameworkContractType(contract.contractType)" class="text-muted">随行就市</span>
<template v-else>¥{{ parseFloat(scope.row.price || 0).toFixed(2) }}</template>
</template>
</el-table-column>
<el-table-column label="含税总价/元" prop="subtotal" width="130" align="right">
<template #default="scope">
<span v-if="contract.contractType === 'framework'" class="text-muted">随行就市</span>
<span v-if="isFrameworkContractType(contract.contractType)" class="text-muted">随行就市</span>
<span v-else class="text-success font-weight-bold">¥{{ parseFloat(scope.row.subtotal || 0).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="税费/元" prop="taxAmount" width="120" align="right">
<template #default="scope">
<span v-if="contract.contractType === 'framework'" class="text-muted">随行就市</span>
<span v-if="isFrameworkContractType(contract.contractType)" class="text-muted">随行就市</span>
<span v-else>¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
</template>
</el-table-column>
@@ -337,7 +337,7 @@
</el-table>
<!-- 框架合同:各产品调价明细(仅详情页展示,不参与打印模板 contractContent) -->
<template v-if="contract.contractType === 'framework'">
<template v-if="isFrameworkContractType(contract.contractType)">
<el-divider content-position="left">框架合同调价明细</el-divider>
<p class="text-muted" style="font-size: 13px; margin-bottom: 12px">按生效区间记录含税单价及调价函;后续结算可据此核对。</p>
<div
@@ -1033,6 +1033,7 @@ import ImagePreview from '@/components/ImagePreview/index.vue';
import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
import { getOpportunity } from '@/api/crm/opportunity';
import { getQuote } from '@/api/crm/quote';
import { isFrameworkContractType } from './contractType';
import {
parseCrmAttachmentField,
resolveCrmAttachmentFiles,
+60 -108
View File
@@ -374,7 +374,7 @@
</el-table-column>
</el-table>
<template v-if="form.contractType === 'framework'">
<template v-if="isFrameworkContractType(form.contractType)">
<el-divider content-position="left">框架合同调价区间</el-divider>
<p class="text-muted mb-3" style="font-size: 13px">
按产品维护生效日期区间与含税单价(可选);若填写区间则须完整(日期、单价、调价函)。合同总金额不与此联动,仍为 0。
@@ -587,6 +587,7 @@ import {
type ContractProductForm,
type ContractProductPriceTierForm
} from '@/api/crm/contract/index';
import { isFrameworkContractType } from './contractType';
import { useDictStore } from '@/store/modules/dict';
import { useUserStore } from '@/store/modules/user';
import { $modal } from '@/utils/modal';
@@ -606,7 +607,7 @@ 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 { getContactByCustomerId, fetchCustomerDetail } from '@/api/crm/customer';
import type { CustomerVO, ContactVO } from '@/api/crm/customer/types';
import { getDefaultInvoiceInfoByCustomerId } from '@/api/crm/invoiceInfo';
import { listAllSupplierInfo, type SupplierInfoVO } from '@/api/crm/supplierInfo';
@@ -714,6 +715,10 @@ const originalTemplateContent = ref<string>('');
// 供方信息列表
const supplierList = ref<SupplierInfoVO[]>([]);
// 客户简称 / 开票公司名(用于按供需方面板匹配简称)
const customerShortName = ref('');
const customerInvoiceName = ref('');
// 表单校验规则
const rules = ref({
contractType: [{ required: true, message: '合同类型不能为空', trigger: 'change' }],
@@ -758,7 +763,7 @@ const uploadDataTierLetter = computed(() => ({
bizType: 'crm_contract_price_letter'
}));
const isFrameworkContract = computed(() => form.value.contractType === 'framework');
const isFrameworkContract = computed(() => isFrameworkContractType(form.value.contractType));
// 加载状态
const saveDraftLoading = ref(false);
@@ -877,7 +882,7 @@ const loadContractDetail = async (id: number | string) => {
deliveryMethod: product.deliveryMethod || ''
}));
if (contractData.contractType === 'framework') {
if (isFrameworkContractType(contractData.contractType)) {
form.value.products.forEach((product: any) => {
product.price = 0;
product.subtotal = 0;
@@ -932,7 +937,10 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
// 自动获取客户的默认开票信息并填充到需方信息(乙方)
try {
// 注意:不要使用 Number() 转换,避免大整数精度丢失
const invoiceRes = await getDefaultInvoiceInfoByCustomerId(customer.id);
const [invoiceRes, customerRes] = await Promise.all([
getDefaultInvoiceInfoByCustomerId(customer.id),
fetchCustomerDetail({ id: String(customer.id) })
]);
console.log('========== 开票信息调试 ==========');
console.log('客户ID(原始):', customer.id, '类型:', typeof customer.id);
console.log('1. API返回完整数据:', JSON.stringify(invoiceRes, null, 2));
@@ -940,6 +948,11 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
// 判断是否有开票信息数据(data可能为null或空对象)
const invoiceData = invoiceRes?.data;
console.log('2. 开票信息data:', invoiceData);
customerShortName.value = (customerRes?.data?.shortName || customer.shortName || '').trim();
if (customerRes?.data?.customerName) {
form.value.customerName = customerRes.data.customerName;
}
customerInvoiceName.value = invoiceData?.companyName || '';
if (invoiceData && invoiceData.companyName) {
console.log('3. 开票信息字段详情:');
@@ -1017,6 +1030,7 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
console.log('====================================');
} catch (error) {
console.error('❌ 获取客户开票信息失败:', error);
customerInvoiceName.value = '';
// 如果获取开票信息失败,使用客户基本信息
if (isPartiesSwapped.value) {
// 已调换:客户名称对应供方信息区域(partyA)
@@ -1082,8 +1096,11 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
// 即使获取联系人失败,也不影响客户选择流程
}
// 更新合同名称中的客户简称
updateContractNameByCustomer(customer.shortName);
// 更新合同名称(按供需方面板);简称以客户主数据为准
if (!customerShortName.value) {
customerShortName.value = (customer.shortName || '').trim();
}
rebuildContractName();
}
};
@@ -1175,7 +1192,7 @@ const handleQuoteConfirm = (quote: QuoteVO) => {
$modal
.confirm('是否要从报价单中带入产品明细?')
.then(() => {
const isFw = form.value.contractType === 'framework';
const isFw = isFrameworkContractType(form.value.contractType);
form.value.products = quote.productList.map((item: any) => ({
productId: item.productId,
productName: item.productName,
@@ -1272,8 +1289,8 @@ const handleSupplierChange = async (supplierId: number | undefined) => {
// 更新合同编号中的供方编码
updateContractNoBySupplier(supplier.supplierCode);
// 更新合同名称中的供方简称
updateContractNameBySupplier(supplier.shortName);
// 更新合同名称(按供需方面板)
rebuildContractName();
const allowToxic = Number((supplier as any).allowToxic || 0);
const allowExplosive = Number((supplier as any).allowExplosive || 0);
@@ -1344,101 +1361,41 @@ const syncContractNoDateFromSignDate = (signDateVal: string | null) => {
};
/**
* 更新合同名称中的供方简称
* 统一格式:{供方简称}-{需方简称}({合同编号})
* 面板单位全称 → 匹配交易主体/客户主数据取简称;对不上用全称
*/
const updateContractNameBySupplier = (newSupplierShortName: string | undefined) => {
if (!form.value.contractName || !newSupplierShortName) {
return;
}
const resolvePartyShortName = (partyName?: string): string => {
const party = (partyName || '').trim();
if (!party) return '';
const contractName = form.value.contractName;
const supplier = form.value.supplierId ? supplierList.value.find((s) => s.id === form.value.supplierId) : undefined;
const supplierName = (supplier?.supplierName || '').trim();
const supplierShort = (supplier?.shortName || supplierName || '').trim();
if (supplierName && party === supplierName) return supplierShort || party;
if (supplierShort && party === supplierShort) return supplierShort;
// 提取合同编号(在括号中,支持中文和英文括号)
const contractNoMatch = contractName.match(/[((](.+?)[))]$/);
const contractNo = contractNoMatch ? contractNoMatch[1] : '';
const customerName = (form.value.customerName || '').trim();
const customerShort = (customerShortName.value || '').trim();
const invoiceName = (customerInvoiceName.value || '').trim();
if (customerName && party === customerName) return customerShort || customerName;
if (customerShort && party === customerShort) return customerShort;
if (invoiceName && party === invoiceName) return customerShort || invoiceName;
// 提取合同名称主体部分(去掉括号和合同编号)
const nameBody = contractNoMatch ? contractName.substring(0, contractNoMatch.index).trim() : contractName.trim();
// 常见的供方简称列表(用于识别和替换)
const commonSupplierNames = ['山峡', 'SX'];
// 统一格式:供方简称-需方简称(合同编号)
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]}`;
}
}
// 如果合同编号已更新,同步更新合同名称中的合同编号
if (contractNo && form.value.contractNo && contractNo !== form.value.contractNo) {
// 替换合同名称中的旧合同编号
form.value.contractName = form.value.contractName.replace(/[((].+?[))]$/, `(${form.value.contractNo})`);
}
return party;
};
/**
* 更新合同名称中的客户简称(需方)
* 统一格式:{供方简称}-{需方简称}({合同编号})
*/
const updateContractNameByCustomer = (newCustomerShortName: string | undefined) => {
if (!form.value.contractName || !newCustomerShortName) {
return;
}
/** 重建合同名称:始终 {供方简称}-{需方简称}({合同编号}) */
const rebuildContractName = () => {
const partyAShort = resolvePartyShortName(form.value.partyA);
const partyBShort = resolvePartyShortName(form.value.partyB);
const nameBody = [partyAShort, partyBShort].filter(Boolean).join('-');
if (!nameBody) return;
const contractName = form.value.contractName;
// 提取合同编号(在括号中,支持中文和英文括号)
const contractNoMatch = contractName.match(/[((](.+?)[))]$/);
const contractNo = contractNoMatch ? contractNoMatch[1] : '';
// 提取合同名称主体部分(去掉括号和合同编号)
const nameBody = contractNoMatch ? contractName.substring(0, contractNoMatch.index).trim() : contractName.trim();
// 常见的供方简称列表(用于识别,避免误替换)
const commonSupplierNames = ['山峡', 'SX'];
// 统一格式:供方简称-需方简称(合同编号)
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;
}
}
// 如果合同编号已更新,同步更新合同名称中的合同编号
if (contractNo && form.value.contractNo && contractNo !== form.value.contractNo) {
// 替换合同名称中的旧合同编号
form.value.contractName = form.value.contractName.replace(/[((].+?[))]$/, `(${form.value.contractNo})`);
}
const contractNo = (form.value.contractNo || '').trim();
form.value.contractName = contractNo ? `${nameBody}(${contractNo})` : nameBody;
};
/** 供需方调换 */
/** 供需方调换:仅交换面板字段,勿再调 handleSupplierChange(会按交易主体覆盖一侧) */
const handleSwapParties = async () => {
// 保存当前的供方主体ID(调换时保持不变)
const currentSupplierId = form.value.supplierId;
// 交换供方和需方的所有信息(不包括supplierId)
const temp = {
partyA: form.value.partyA,
partyAAddress: form.value.partyAAddress,
@@ -1470,20 +1427,15 @@ const handleSwapParties = async () => {
form.value.partyBBank = temp.partyABank;
form.value.partyBAccount = temp.partyAAccount;
// 切换调换状态
isPartiesSwapped.value = !isPartiesSwapped.value;
// 如果存在供方主体,根据新的调换状态重新填充对应的信息区域
if (currentSupplierId) {
await handleSupplierChange(currentSupplierId);
}
rebuildContractName();
$modal.msgSuccess('供需方信息已调换');
};
/** 合同类型变化处理 */
const handleContractTypeChange = (value: string) => {
if (value === 'framework') {
if (isFrameworkContractType(value)) {
form.value.amount = 0;
form.value.products?.forEach((p) => {
p.price = 0;
@@ -1582,7 +1534,7 @@ const handleAddProduct = () => {
/** 产品选择确认 */
const handleProductSelected = (products: any[]) => {
const isFw = form.value.contractType === 'framework';
const isFw = isFrameworkContractType(form.value.contractType);
products.forEach((product) => {
// 允许重复添加相同产品:直接追加到 products 列表
form.value.products.push({
@@ -1697,7 +1649,7 @@ const handlePackageSpecChange = (product: ExtendedContractProductForm, value: st
/** 计算产品金额(参考报价单逻辑) */
const calculateProductAmount = (product: ExtendedContractProductForm) => {
if (form.value.contractType === 'framework') {
if (isFrameworkContractType(form.value.contractType)) {
product.price = 0;
product.subtotal = 0;
product.taxAmount = 0;
@@ -1725,7 +1677,7 @@ const calculateProductAmount = (product: ExtendedContractProductForm) => {
/** 计算合计金额 */
const calculateTotalAmount = () => {
if (form.value.contractType === 'framework') {
if (isFrameworkContractType(form.value.contractType)) {
form.value.amount = 0;
return;
}
@@ -1801,7 +1753,7 @@ const buildProductsPayload = () => {
if (product.id) {
base.id = product.id;
}
if (form.value.contractType === 'framework') {
if (isFrameworkContractType(form.value.contractType)) {
base.price = 0;
base.subtotal = 0;
base.taxAmount = 0;
@@ -2145,7 +2097,7 @@ const handleSubmit = async () => {
}
}
const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || form.value.contractType === 'framework';
const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || isFrameworkContractType(form.value.contractType);
const validateProductRowCommon = (product: any, rowNum: number): boolean => {
if (!product.productName || String(product.productName).trim() === '') {
@@ -2178,7 +2130,7 @@ const handleSubmit = async () => {
if (!validateProductRowCommon(product, rowNum)) {
return;
}
const isFw = form.value.contractType === 'framework';
const isFw = isFrameworkContractType(form.value.contractType);
if (!isFw) {
if (!product.price || Number(product.price) <= 0) {
$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空且必须大于0`);