提交
This commit is contained in:
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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user