|
|
@@ -89,7 +89,14 @@
|
|
|
</el-col>
|
|
|
<el-col :span="6">
|
|
|
<el-form-item label="签订日期" prop="signDate">
|
|
|
- <el-date-picker v-model="form.signDate" type="date" placeholder="选择签订日期" value-format="YYYY-MM-DD" style="width: 100%" @change="handleSignDateChange" />
|
|
|
+ <el-date-picker
|
|
|
+ v-model="form.signDate"
|
|
|
+ type="date"
|
|
|
+ placeholder="选择签订日期"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ style="width: 100%"
|
|
|
+ @change="handleSignDateChange"
|
|
|
+ />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="6">
|
|
|
@@ -290,12 +297,7 @@
|
|
|
style="width: 100%"
|
|
|
@change="handleUnitChange(scope.row)"
|
|
|
>
|
|
|
- <el-option
|
|
|
- v-for="dict in crm_product_unit"
|
|
|
- :key="dict.value"
|
|
|
- :label="dict.label"
|
|
|
- :value="dict.value"
|
|
|
- />
|
|
|
+ <el-option v-for="dict in crm_product_unit" :key="dict.value" :label="dict.label" :value="dict.value" />
|
|
|
</el-select>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
@@ -306,20 +308,29 @@
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="price" width="130">
|
|
|
- <template #header> <span style="color: #f56c6c">*</span> 含税单价/元</template>
|
|
|
+ <template #header>
|
|
|
+ <span v-if="isFrameworkContract">含税单价/元</span>
|
|
|
+ <span v-else><span style="color: #f56c6c">*</span> 含税单价/元</span>
|
|
|
+ </template>
|
|
|
<template #default="scope">
|
|
|
- <el-input v-model="scope.row.price" placeholder="含税单价" @input="handlePriceChange(scope.row)" clearable size="small" />
|
|
|
+ <span v-if="isFrameworkContract" class="text-muted">随行就市</span>
|
|
|
+ <el-input v-else v-model="scope.row.price" placeholder="含税单价" @input="handlePriceChange(scope.row)" clearable size="small" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="subtotal" width="130" align="right">
|
|
|
- <template #header> <span style="color: #f56c6c">*</span> 含税总价/元</template>
|
|
|
+ <template #header>
|
|
|
+ <span v-if="isFrameworkContract">含税总价/元</span>
|
|
|
+ <span v-else><span style="color: #f56c6c">*</span> 含税总价/元</span>
|
|
|
+ </template>
|
|
|
<template #default="scope">
|
|
|
- <span class="text-success font-weight-bold">¥{{ parseFloat(scope.row.subtotal || 0).toFixed(2) }}</span>
|
|
|
+ <span v-if="isFrameworkContract" 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>¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
|
|
|
+ <span v-if="isFrameworkContract" class="text-muted">随行就市</span>
|
|
|
+ <span v-else>¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="packageSpec" min-width="150">
|
|
|
@@ -350,7 +361,16 @@
|
|
|
</el-table-column>
|
|
|
<el-table-column prop="remark" label="备注" min-width="160">
|
|
|
<template #default="scope">
|
|
|
- <el-input v-model="scope.row.remark" type="textarea" :rows="2" placeholder="请输入备注" maxlength="500" show-word-limit clearable size="small" />
|
|
|
+ <el-input
|
|
|
+ v-model="scope.row.remark"
|
|
|
+ type="textarea"
|
|
|
+ :rows="2"
|
|
|
+ placeholder="请输入备注"
|
|
|
+ maxlength="500"
|
|
|
+ show-word-limit
|
|
|
+ clearable
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" width="80" align="center" fixed="right">
|
|
|
@@ -360,6 +380,60 @@
|
|
|
</el-table-column>
|
|
|
</el-table>
|
|
|
|
|
|
+ <template v-if="form.contractType === 'framework'">
|
|
|
+ <el-divider content-position="left">框架合同调价区间</el-divider>
|
|
|
+ <p class="text-muted mb-3" style="font-size: 13px">
|
|
|
+ 按产品维护生效日期区间与含税单价;每条区间须上传调价函。合同总金额不与此联动,仍为 0。提交审批前将校验每条产品至少一条完整区间。
|
|
|
+ </p>
|
|
|
+ <div v-for="(prod, pIdx) in form.products" :key="'fw-tier-' + pIdx" class="framework-tier-block mb-4">
|
|
|
+ <div class="fw-tier-title mb-2">{{ prod.productName || '产品' + (pIdx + 1) }}</div>
|
|
|
+ <el-button type="primary" link class="mb-2 fw-tier-letter-add-btn" icon="Plus" @click="addPriceTierRow(pIdx)">调价函</el-button>
|
|
|
+ <el-table :data="prod.priceTiers || []" border size="small">
|
|
|
+ <el-table-column type="index" label="#" width="50" align="center" />
|
|
|
+ <el-table-column label="开始日期" min-width="150">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-date-picker v-model="scope.row.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" style="width: 100%" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="结束日期" min-width="150">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-date-picker v-model="scope.row.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" style="width: 100%" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="含税单价/元" width="140">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-input v-model="scope.row.price" placeholder="含税单价" clearable size="small" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="调价函" min-width="300">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-upload
|
|
|
+ :action="uploadAction"
|
|
|
+ :headers="uploadHeaders"
|
|
|
+ :data="uploadDataTierLetter"
|
|
|
+ list-type="picture-card"
|
|
|
+ :limit="20"
|
|
|
+ multiple
|
|
|
+ accept="image/*"
|
|
|
+ :file-list="scope.row._letterFiles || []"
|
|
|
+ :on-success="(res, file) => handleTierLetterUploadSuccess(res, file, pIdx, scope.$index)"
|
|
|
+ :on-remove="(file) => handleTierLetterRemove(file, pIdx, scope.$index)"
|
|
|
+ class="tier-letter-upload"
|
|
|
+ >
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ </el-upload>
|
|
|
+ <div class="text-muted" style="font-size: 11px; margin-top: 4px">支持多张图片,单图建议不超过 5MB</div>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="70" align="center" fixed="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button link type="danger" icon="Delete" @click="removePriceTierRow(pIdx, scope.$index)" />
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
<el-divider />
|
|
|
<div class="total-section">
|
|
|
<el-row :gutter="20">
|
|
|
@@ -510,10 +584,17 @@ import { ref, reactive, toRefs, computed, onMounted, nextTick, getCurrentInstanc
|
|
|
import { useRouter, useRoute } from 'vue-router';
|
|
|
import type { FormInstance } from 'element-plus';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
-import { getContract, addContract, updateContract, type ContractForm, type ContractProductForm } from '@/api/crm/contract/index';
|
|
|
+import {
|
|
|
+ getContract,
|
|
|
+ addContract,
|
|
|
+ updateContract,
|
|
|
+ type ContractForm,
|
|
|
+ type ContractProductForm,
|
|
|
+ type ContractProductPriceTierForm
|
|
|
+} from '@/api/crm/contract/index';
|
|
|
import { getToken } from '@/utils/auth';
|
|
|
import { useDict } from '@/utils/dict';
|
|
|
-import { UploadFilled, Document, Loading, Download, Delete, ZoomIn, Sort } from '@element-plus/icons-vue';
|
|
|
+import { UploadFilled, Document, Loading, Download, Delete, ZoomIn, Sort, Plus } from '@element-plus/icons-vue';
|
|
|
import ImagePreview from '@/components/ImagePreview/index.vue';
|
|
|
import request from '@/utils/request';
|
|
|
import ProductSelector from './components/ProductSelector.vue';
|
|
|
@@ -543,12 +624,15 @@ type ExtendedContractForm = ContractForm & {
|
|
|
};
|
|
|
|
|
|
// 扩展产品类型,添加显示字段
|
|
|
+type PriceTierRow = ContractProductPriceTierForm & { _letterFiles?: any[] };
|
|
|
+
|
|
|
type ExtendedContractProductForm = ContractProductForm & {
|
|
|
productName?: string;
|
|
|
productModel?: string;
|
|
|
unit?: string;
|
|
|
unitPrice?: number;
|
|
|
subtotal?: number;
|
|
|
+ priceTiers?: PriceTierRow[];
|
|
|
};
|
|
|
|
|
|
const router = useRouter();
|
|
|
@@ -671,6 +755,12 @@ const uploadData = computed(() => {
|
|
|
};
|
|
|
});
|
|
|
|
|
|
+const uploadDataTierLetter = computed(() => ({
|
|
|
+ bizType: 'crm_contract_price_letter'
|
|
|
+}));
|
|
|
+
|
|
|
+const isFrameworkContract = computed(() => form.value.contractType === 'framework');
|
|
|
+
|
|
|
// 加载状态
|
|
|
const saveDraftLoading = ref(false);
|
|
|
const submitLoading = ref(false);
|
|
|
@@ -772,19 +862,39 @@ const loadContractDetail = async (id: number | string) => {
|
|
|
if (contractData.products && Array.isArray(contractData.products)) {
|
|
|
form.value.products = contractData.products.map((product: any) => ({
|
|
|
...product,
|
|
|
- // 字段映射:后端model -> 前端productModel
|
|
|
productModel: product.model || '',
|
|
|
- // 初始化前端需要的计算字段
|
|
|
- taxRate: product.taxRate || 6, // 默认税率6%
|
|
|
+ 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,
|
|
|
+ _letterFiles: [] as any[]
|
|
|
+ };
|
|
|
+ }),
|
|
|
+ taxRate: product.taxRate || 6,
|
|
|
taxAmount: 0,
|
|
|
packageSpec: product.packageSpec || '',
|
|
|
deliveryMethod: product.deliveryMethod || ''
|
|
|
}));
|
|
|
|
|
|
- // 重新计算每个产品的税费
|
|
|
- form.value.products.forEach((product: any) => {
|
|
|
- calculateProductAmount(product);
|
|
|
- });
|
|
|
+ if (contractData.contractType === 'framework') {
|
|
|
+ form.value.products.forEach((product: any) => {
|
|
|
+ product.price = 0;
|
|
|
+ product.subtotal = 0;
|
|
|
+ product.taxAmount = 0;
|
|
|
+ });
|
|
|
+ form.value.amount = 0;
|
|
|
+ await enrichFrameworkTierLetterFiles();
|
|
|
+ } else {
|
|
|
+ form.value.products.forEach((product: any) => {
|
|
|
+ calculateProductAmount(product);
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 6. 设置合同期限日期范围
|
|
|
@@ -1077,6 +1187,7 @@ const handleQuoteConfirm = (quote: QuoteVO) => {
|
|
|
proxy?.$modal
|
|
|
.confirm('是否要从报价单中带入产品明细?')
|
|
|
.then(() => {
|
|
|
+ const isFw = form.value.contractType === 'framework';
|
|
|
form.value.products = quote.productList.map((item: any) => ({
|
|
|
productId: item.productId,
|
|
|
productName: item.productName,
|
|
|
@@ -1085,18 +1196,19 @@ const handleQuoteConfirm = (quote: QuoteVO) => {
|
|
|
categoryName: item.categoryName || '',
|
|
|
unit: item.unit || '个',
|
|
|
quantity: item.quantity || 1,
|
|
|
- price: item.price || 0,
|
|
|
- unitPrice: item.price || 0,
|
|
|
+ price: isFw ? 0 : item.price || 0,
|
|
|
+ unitPrice: isFw ? 0 : item.price || 0,
|
|
|
taxRate: item.taxRate || 13, // 税率(%)
|
|
|
- taxAmount: item.taxAmount || 0, // 税费/元
|
|
|
+ taxAmount: isFw ? 0 : item.taxAmount || 0, // 税费/元
|
|
|
discount: item.discount || 0,
|
|
|
- subtotal: item.subtotal || (item.quantity || 1) * (item.price || 0) * (1 - (item.discount || 0) / 100),
|
|
|
+ subtotal: isFw ? 0 : item.subtotal || (item.quantity || 1) * (item.price || 0) * (1 - (item.discount || 0) / 100),
|
|
|
packageSpec: item.packageSpec || '', // 包装规格
|
|
|
deliveryMethod: item.deliveryMethod || '', // 送货方式
|
|
|
deliveryDate: '',
|
|
|
description: item.description || '',
|
|
|
remark: item.remark || '',
|
|
|
- sort: item.sort // 排序号,保持与报价单一致
|
|
|
+ sort: item.sort, // 排序号,保持与报价单一致
|
|
|
+ priceTiers: isFw ? [] : undefined
|
|
|
}));
|
|
|
calculateTotalAmount();
|
|
|
})
|
|
|
@@ -1212,7 +1324,7 @@ const updateContractNoBySupplier = (newSupplierCode: string | undefined) => {
|
|
|
/**
|
|
|
* 更新合同名称中的供方简称
|
|
|
* 合同名称格式:
|
|
|
- * - 销售合同:供方简称-客户简称(合同编号)
|
|
|
+ * - 销售合同、框架合同:供方简称-客户简称(合同编号)
|
|
|
* - 采购合同:客户简称-供方简称(合同编号)
|
|
|
*/
|
|
|
const updateContractNameBySupplier = (newSupplierShortName: string | undefined) => {
|
|
|
@@ -1234,8 +1346,8 @@ const updateContractNameBySupplier = (newSupplierShortName: string | undefined)
|
|
|
const commonSupplierNames = ['山峡', 'SX'];
|
|
|
|
|
|
// 根据合同类型解析和替换
|
|
|
- if (contractType === 'sale') {
|
|
|
- // 销售合同格式:供方简称-客户简称(合同编号)
|
|
|
+ if (contractType === 'sale' || contractType === 'framework') {
|
|
|
+ // 销售/框架合同格式:供方简称-客户简称(合同编号)
|
|
|
// 例如:山峡-客户A(SX/X-JT20250926002)
|
|
|
const parts = nameBody.split('-').map((p) => p.trim());
|
|
|
if (parts.length >= 2) {
|
|
|
@@ -1317,7 +1429,7 @@ const updateContractNameBySupplier = (newSupplierShortName: string | undefined)
|
|
|
/**
|
|
|
* 更新合同名称中的客户简称
|
|
|
* 合同名称格式:
|
|
|
- * - 销售合同:供方简称-客户简称(合同编号)
|
|
|
+ * - 销售合同、框架合同:供方简称-客户简称(合同编号)
|
|
|
* - 采购合同:客户简称-供方简称(合同编号)
|
|
|
*/
|
|
|
const updateContractNameByCustomer = (newCustomerShortName: string | undefined) => {
|
|
|
@@ -1339,8 +1451,8 @@ const updateContractNameByCustomer = (newCustomerShortName: string | undefined)
|
|
|
const commonSupplierNames = ['山峡', 'SX'];
|
|
|
|
|
|
// 根据合同类型解析和替换
|
|
|
- if (contractType === 'sale') {
|
|
|
- // 销售合同格式:供方简称-客户简称(合同编号)
|
|
|
+ if (contractType === 'sale' || contractType === 'framework') {
|
|
|
+ // 销售/框架合同格式:供方简称-客户简称(合同编号)
|
|
|
// 例如:山峡-客户A(SX/X-JT20250926002)
|
|
|
const parts = nameBody.split('-').map((p) => p.trim());
|
|
|
if (parts.length >= 2) {
|
|
|
@@ -1483,6 +1595,17 @@ const handleSwapParties = async () => {
|
|
|
|
|
|
/** 合同类型变化处理 */
|
|
|
const handleContractTypeChange = (value: string) => {
|
|
|
+ if (value === 'framework') {
|
|
|
+ form.value.amount = 0;
|
|
|
+ form.value.products?.forEach((p) => {
|
|
|
+ p.price = 0;
|
|
|
+ p.subtotal = 0;
|
|
|
+ p.taxAmount = 0;
|
|
|
+ if (!p.priceTiers) {
|
|
|
+ p.priceTiers = [];
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
// 当选择采购合同时,自动调换供需方
|
|
|
if (value === 'purchase' && !isPartiesSwapped.value) {
|
|
|
proxy?.$modal
|
|
|
@@ -1566,6 +1689,7 @@ const handleAddProduct = () => {
|
|
|
|
|
|
/** 产品选择确认 */
|
|
|
const handleProductSelected = (products: any[]) => {
|
|
|
+ const isFw = form.value.contractType === 'framework';
|
|
|
products.forEach((product) => {
|
|
|
// 允许重复添加相同产品:直接追加到 products 列表
|
|
|
form.value.products.push({
|
|
|
@@ -1576,14 +1700,15 @@ const handleProductSelected = (products: any[]) => {
|
|
|
categoryName: product.categoryName || '',
|
|
|
unit: product.unit || '个',
|
|
|
quantity: 1,
|
|
|
- price: product.price || 0,
|
|
|
+ price: isFw ? 0 : product.price || 0,
|
|
|
taxRate: normalizeTaxRateDisplay(product.taxRate, 13), // 税率:整数不显示小数位
|
|
|
taxAmount: 0,
|
|
|
- subtotal: product.price || 0,
|
|
|
+ subtotal: isFw ? 0 : product.price || 0,
|
|
|
packageSpec: '',
|
|
|
deliveryMethod: '',
|
|
|
description: product.description || '',
|
|
|
- remark: ''
|
|
|
+ remark: '',
|
|
|
+ priceTiers: isFw ? [] : undefined
|
|
|
});
|
|
|
});
|
|
|
// 计算所有产品金额
|
|
|
@@ -1677,6 +1802,12 @@ const handlePackageSpecChange = (product: ExtendedContractProductForm, value: st
|
|
|
|
|
|
/** 计算产品金额(参考报价单逻辑) */
|
|
|
const calculateProductAmount = (product: ExtendedContractProductForm) => {
|
|
|
+ if (form.value.contractType === 'framework') {
|
|
|
+ product.price = 0;
|
|
|
+ product.subtotal = 0;
|
|
|
+ product.taxAmount = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
// 获取输入值并转换为数字
|
|
|
const price = parseFloat(product.price?.toString() || '0');
|
|
|
const quantity = parseFloat(product.quantity?.toString() || '0');
|
|
|
@@ -1699,6 +1830,10 @@ const calculateProductAmount = (product: ExtendedContractProductForm) => {
|
|
|
|
|
|
/** 计算合计金额 */
|
|
|
const calculateTotalAmount = () => {
|
|
|
+ if (form.value.contractType === 'framework') {
|
|
|
+ form.value.amount = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
let total = 0;
|
|
|
form.value.products.forEach((item) => {
|
|
|
const quantity = item.quantity || 0;
|
|
|
@@ -1711,6 +1846,146 @@ const calculateTotalAmount = () => {
|
|
|
form.value.amount = total;
|
|
|
};
|
|
|
|
|
|
+/** 框架合同:批量拉取调价函 OSS URL,用于上传组件缩略图回显 */
|
|
|
+const enrichFrameworkTierLetterFiles = async () => {
|
|
|
+ const products = form.value.products || [];
|
|
|
+ const idSet = new Set<string>();
|
|
|
+ products.forEach((p: any) => {
|
|
|
+ (p.priceTiers || []).forEach((t: any) => {
|
|
|
+ (t.letterOssIds || []).forEach((id: string) => idSet.add(String(id)));
|
|
|
+ });
|
|
|
+ });
|
|
|
+ if (idSet.size === 0) return;
|
|
|
+ try {
|
|
|
+ const response = await request({
|
|
|
+ url: `/resource/oss/listByIds/${[...idSet].join(',')}`,
|
|
|
+ method: 'get'
|
|
|
+ });
|
|
|
+ if (response.code !== 200 || !response.data?.length) return;
|
|
|
+ const urlMap = new Map<string, string>();
|
|
|
+ response.data.forEach((item: any) => {
|
|
|
+ urlMap.set(String(item.ossId), item.url);
|
|
|
+ });
|
|
|
+ products.forEach((p: any) => {
|
|
|
+ (p.priceTiers || []).forEach((t: any) => {
|
|
|
+ const ids = t.letterOssIds || [];
|
|
|
+ t._letterFiles = ids.map((ossId: string, i: number) => ({
|
|
|
+ name: `调价函${i + 1}`,
|
|
|
+ ossId: String(ossId),
|
|
|
+ url: urlMap.get(String(ossId)),
|
|
|
+ uid: `tier-prev-${t.id ?? 'n'}-${i}-${ossId}`,
|
|
|
+ status: 'success'
|
|
|
+ }));
|
|
|
+ });
|
|
|
+ });
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('调价函缩略图加载失败', e);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 组装提交/保存的产品明细(含框架合同调价区间) */
|
|
|
+const buildProductsPayload = () => {
|
|
|
+ const list = form.value.products || [];
|
|
|
+ return list.map((product: any, idx: number) => {
|
|
|
+ const base: any = {
|
|
|
+ contractId: product.contractId || form.value.id,
|
|
|
+ productId: product.productId,
|
|
|
+ productName: product.productName,
|
|
|
+ productNo: product.productNo,
|
|
|
+ categoryId: product.categoryId,
|
|
|
+ categoryName: product.categoryName,
|
|
|
+ model: product.productModel || product.model,
|
|
|
+ unit: product.unit,
|
|
|
+ quantity: product.quantity,
|
|
|
+ price: product.price,
|
|
|
+ discount: product.discount,
|
|
|
+ taxRate: product.taxRate,
|
|
|
+ taxAmount: product.taxAmount,
|
|
|
+ subtotal: product.subtotal,
|
|
|
+ deliveryDate: product.deliveryDate,
|
|
|
+ description: product.description,
|
|
|
+ packageSpec: product.packageSpec,
|
|
|
+ deliveryMethod: product.deliveryMethod,
|
|
|
+ sort: product.sort ?? idx + 1,
|
|
|
+ remark: product.remark
|
|
|
+ };
|
|
|
+ if (product.id) {
|
|
|
+ base.id = product.id;
|
|
|
+ }
|
|
|
+ if (form.value.contractType === 'framework') {
|
|
|
+ base.price = 0;
|
|
|
+ base.subtotal = 0;
|
|
|
+ base.taxAmount = 0;
|
|
|
+ base.priceTiers = (product.priceTiers || [])
|
|
|
+ .filter(
|
|
|
+ (t: any) => t.startDate && t.endDate && t.price != null && t.price !== '' && Array.isArray(t.letterOssIds) && t.letterOssIds.length > 0
|
|
|
+ )
|
|
|
+ .map((t: any, i: number) => ({
|
|
|
+ id: t.id,
|
|
|
+ startDate: t.startDate,
|
|
|
+ endDate: t.endDate,
|
|
|
+ price: Number(t.price),
|
|
|
+ letterOssIds: (t.letterOssIds || []).map((id: string) => String(id).trim()).filter(Boolean),
|
|
|
+ sort: t.sort ?? i + 1
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ return base;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const addPriceTierRow = (pIdx: number) => {
|
|
|
+ const p = form.value.products[pIdx];
|
|
|
+ if (!p) return;
|
|
|
+ if (!p.priceTiers) {
|
|
|
+ p.priceTiers = [];
|
|
|
+ }
|
|
|
+ p.priceTiers.push({ startDate: '', endDate: '', price: undefined, letterOssIds: [], _letterFiles: [] });
|
|
|
+};
|
|
|
+
|
|
|
+const removePriceTierRow = (pIdx: number, tIdx: number) => {
|
|
|
+ const p = form.value.products[pIdx];
|
|
|
+ if (!p?.priceTiers) return;
|
|
|
+ p.priceTiers.splice(tIdx, 1);
|
|
|
+};
|
|
|
+
|
|
|
+const handleTierLetterUploadSuccess = (res: any, file: any, pIdx: number, tIdx: number) => {
|
|
|
+ if (res.code !== 200 || res.data?.ossId == null) {
|
|
|
+ ElMessage.error(res.msg || '上传失败');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const p = form.value.products[pIdx];
|
|
|
+ const tiers = p?.priceTiers;
|
|
|
+ if (!tiers || !tiers[tIdx]) return;
|
|
|
+ const row = tiers[tIdx];
|
|
|
+ const oid = String(res.data.ossId);
|
|
|
+ row.letterOssIds = row.letterOssIds || [];
|
|
|
+ row.letterOssIds.push(oid);
|
|
|
+ row._letterFiles = row._letterFiles || [];
|
|
|
+ row._letterFiles.push({
|
|
|
+ name: file.name || `调价函${row._letterFiles.length + 1}`,
|
|
|
+ ossId: oid,
|
|
|
+ url: res.data.url,
|
|
|
+ uid: file.uid,
|
|
|
+ status: 'success'
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const handleTierLetterRemove = (file: any, pIdx: number, tIdx: number) => {
|
|
|
+ const p = form.value.products[pIdx];
|
|
|
+ const tiers = p?.priceTiers;
|
|
|
+ if (!tiers || !tiers[tIdx]) return;
|
|
|
+ const row = tiers[tIdx];
|
|
|
+ const uid = file.uid;
|
|
|
+ const idx = (row._letterFiles || []).findIndex((f: any) => f.uid === uid);
|
|
|
+ if (idx >= 0) {
|
|
|
+ const removed = row._letterFiles[idx];
|
|
|
+ row._letterFiles.splice(idx, 1);
|
|
|
+ if (removed?.ossId) {
|
|
|
+ row.letterOssIds = (row.letterOssIds || []).filter((id: string) => String(id) !== String(removed.ossId));
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
/** 处理附件数据(支持分类) */
|
|
|
const handleAttachmentData = async () => {
|
|
|
console.log('[附件回显] 开始处理附件数据');
|
|
|
@@ -2056,35 +2331,7 @@ const handleSaveDraft = async () => {
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
- data.products = data.products.map((product: any) => {
|
|
|
- const productData: any = {
|
|
|
- contractId: product.contractId || form.value.id, // 新增产品时,使用合同ID填充contractId
|
|
|
- productId: product.productId,
|
|
|
- productName: product.productName,
|
|
|
- productNo: product.productNo,
|
|
|
- categoryId: product.categoryId,
|
|
|
- categoryName: product.categoryName,
|
|
|
- model: product.productModel || product.model, // 前端productModel映射回后端model
|
|
|
- unit: product.unit,
|
|
|
- quantity: product.quantity,
|
|
|
- price: product.price,
|
|
|
- discount: product.discount,
|
|
|
- taxRate: product.taxRate, // 税率
|
|
|
- taxAmount: product.taxAmount, // 税费
|
|
|
- subtotal: product.subtotal,
|
|
|
- deliveryDate: product.deliveryDate,
|
|
|
- description: product.description,
|
|
|
- packageSpec: product.packageSpec, // 包装规格
|
|
|
- deliveryMethod: product.deliveryMethod, // 送货方式
|
|
|
- sort: product.sort,
|
|
|
- remark: product.remark
|
|
|
- };
|
|
|
- // 只有已存在的产品才包含id字段(新增产品没有id)
|
|
|
- if (product.id) {
|
|
|
- productData.id = product.id;
|
|
|
- }
|
|
|
- return productData;
|
|
|
- });
|
|
|
+ data.products = buildProductsPayload();
|
|
|
} else {
|
|
|
// 如果产品列表为空,删除该字段,避免后端校验
|
|
|
delete data.products;
|
|
|
@@ -2129,93 +2376,94 @@ const handleSubmit = async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 根据合同类型决定是否需要校验产品信息
|
|
|
- console.log('当前合同类型:', form.value.contractType);
|
|
|
- console.log('合同类型判断:', form.value.contractType === 'sale', form.value.contractType === 'purchase');
|
|
|
+ const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase' || form.value.contractType === 'framework';
|
|
|
|
|
|
- const requiresProducts = form.value.contractType === 'sale' || form.value.contractType === 'purchase';
|
|
|
- console.log('是否需要强制产品:', requiresProducts);
|
|
|
+ const validateProductRowCommon = (product: any, rowNum: number): boolean => {
|
|
|
+ if (!product.productName || String(product.productName).trim() === '') {
|
|
|
+ proxy?.$modal.msgWarning(`第${rowNum}行产品的产品名称不能为空`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!product.quantity || Number(product.quantity) <= 0) {
|
|
|
+ proxy?.$modal.msgWarning(`第${rowNum}行产品的数量不能为空且必须大于0`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!product.packageSpec || String(product.packageSpec).trim() === '') {
|
|
|
+ proxy?.$modal.msgWarning(`第${rowNum}行产品的包装规格不能为空`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (!product.deliveryMethod || String(product.deliveryMethod).trim() === '') {
|
|
|
+ proxy?.$modal.msgWarning(`第${rowNum}行产品的送货方式不能为空`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ };
|
|
|
|
|
|
if (requiresProducts) {
|
|
|
- // 销售合同和采购合同必须至少添加一个产品
|
|
|
if (!form.value.products || form.value.products.length === 0) {
|
|
|
- proxy?.$modal.msgWarning('销售合同和采购合同必须至少添加一个产品');
|
|
|
+ proxy?.$modal.msgWarning('销售合同、采购合同或框架合同必须至少添加一个产品');
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- // 验证产品必填字段
|
|
|
for (let i = 0; i < form.value.products.length; i++) {
|
|
|
const product = form.value.products[i];
|
|
|
const rowNum = i + 1;
|
|
|
-
|
|
|
- if (!product.productName || product.productName.trim() === '') {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的产品名称不能为空`);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!product.quantity || product.quantity <= 0) {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的数量不能为空且必须大于0`);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!product.price || product.price <= 0) {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空且必须大于0`);
|
|
|
+ if (!validateProductRowCommon(product, rowNum)) {
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- if (!product.subtotal || product.subtotal <= 0) {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的含税总价不能为空且必须大于0`);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!product.packageSpec || product.packageSpec.trim() === '') {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的包装规格不能为空`);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!product.deliveryMethod || product.deliveryMethod.trim() === '') {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的送货方式不能为空`);
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 其他合同类型不强制要求产品,但如果已填写产品,则验证产品字段的完整性
|
|
|
- if (form.value.products && form.value.products.length > 0) {
|
|
|
- for (let i = 0; i < form.value.products.length; i++) {
|
|
|
- const product = form.value.products[i];
|
|
|
- const rowNum = i + 1;
|
|
|
-
|
|
|
- if (!product.productName || product.productName.trim() === '') {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的产品名称不能为空`);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!product.quantity || product.quantity <= 0) {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的数量不能为空且必须大于0`);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (!product.price || product.price <= 0) {
|
|
|
+ const isFw = form.value.contractType === 'framework';
|
|
|
+ if (!isFw) {
|
|
|
+ if (!product.price || Number(product.price) <= 0) {
|
|
|
proxy?.$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空且必须大于0`);
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- if (!product.subtotal || product.subtotal <= 0) {
|
|
|
+ if (!product.subtotal || Number(product.subtotal) <= 0) {
|
|
|
proxy?.$modal.msgWarning(`第${rowNum}行产品的含税总价不能为空且必须大于0`);
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- if (!product.packageSpec || product.packageSpec.trim() === '') {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的包装规格不能为空`);
|
|
|
+ } else {
|
|
|
+ const tiers = product.priceTiers || [];
|
|
|
+ if (tiers.length === 0) {
|
|
|
+ proxy?.$modal.msgWarning(`框架合同产品「${product.productName || rowNum}」至少添加一条调价区间`);
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- if (!product.deliveryMethod || product.deliveryMethod.trim() === '') {
|
|
|
- proxy?.$modal.msgWarning(`第${rowNum}行产品的送货方式不能为空`);
|
|
|
- return;
|
|
|
+ for (let j = 0; j < tiers.length; j++) {
|
|
|
+ const t = tiers[j];
|
|
|
+ const tRow = j + 1;
|
|
|
+ if (!t.startDate || !t.endDate) {
|
|
|
+ proxy?.$modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:请选择开始与结束日期`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (String(t.startDate) > String(t.endDate)) {
|
|
|
+ proxy?.$modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:开始日期不能晚于结束日期`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const pVal = t.price === null || t.price === '' ? NaN : Number(t.price);
|
|
|
+ if (Number.isNaN(pVal) || pVal <= 0) {
|
|
|
+ proxy?.$modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:含税单价必须大于0`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!(t.letterOssIds?.length > 0)) {
|
|
|
+ proxy?.$modal.msgWarning(`产品「${product.productName}」调价区间第${tRow}行:请上传调价函`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ } else if (form.value.products && form.value.products.length > 0) {
|
|
|
+ for (let i = 0; i < form.value.products.length; i++) {
|
|
|
+ const product = form.value.products[i];
|
|
|
+ const rowNum = i + 1;
|
|
|
+ if (!validateProductRowCommon(product, rowNum)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!product.price || Number(product.price) <= 0) {
|
|
|
+ proxy?.$modal.msgWarning(`第${rowNum}行产品的含税单价不能为空且必须大于0`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!product.subtotal || Number(product.subtotal) <= 0) {
|
|
|
+ proxy?.$modal.msgWarning(`第${rowNum}行产品的含税总价不能为空且必须大于0`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
submitLoading.value = true;
|
|
|
@@ -2261,35 +2509,7 @@ const handleSubmit = async () => {
|
|
|
|
|
|
// 清理产品列表中的扩展字段并映射回后端字段
|
|
|
if (data.products && Array.isArray(data.products) && data.products.length > 0) {
|
|
|
- data.products = data.products.map((product: any) => {
|
|
|
- const productData: any = {
|
|
|
- contractId: product.contractId || form.value.id, // 新增产品时,使用合同ID填充contractId
|
|
|
- productId: product.productId,
|
|
|
- productName: product.productName,
|
|
|
- productNo: product.productNo,
|
|
|
- categoryId: product.categoryId,
|
|
|
- categoryName: product.categoryName,
|
|
|
- model: product.productModel || product.model, // 前端productModel映射回后端model
|
|
|
- unit: product.unit,
|
|
|
- quantity: product.quantity,
|
|
|
- price: product.price,
|
|
|
- discount: product.discount,
|
|
|
- taxRate: product.taxRate, // 税率
|
|
|
- taxAmount: product.taxAmount, // 税费
|
|
|
- subtotal: product.subtotal,
|
|
|
- deliveryDate: product.deliveryDate,
|
|
|
- description: product.description,
|
|
|
- packageSpec: product.packageSpec, // 包装规格
|
|
|
- deliveryMethod: product.deliveryMethod, // 送货方式
|
|
|
- sort: product.sort,
|
|
|
- remark: product.remark
|
|
|
- };
|
|
|
- // 只有已存在的产品才包含id字段(新增产品没有id)
|
|
|
- if (product.id) {
|
|
|
- productData.id = product.id;
|
|
|
- }
|
|
|
- return productData;
|
|
|
- });
|
|
|
+ data.products = buildProductsPayload();
|
|
|
} else {
|
|
|
// 如果产品列表为空,删除该字段,避免后端校验
|
|
|
delete data.products;
|
|
|
@@ -2338,10 +2558,10 @@ const checkTemplateContentDeleted = () => {
|
|
|
// 原始有模板内容,现在没有了,提示用户
|
|
|
proxy?.$modal.alert(
|
|
|
'<div style="color: #e6a23c; font-weight: bold;">⚠️ 提醒:合同模板内容已被清除</div>' +
|
|
|
- '<div style="margin-top: 10px; line-height: 1.8;">' +
|
|
|
- '您在编辑合同表单时,之前的富文本模板内容已被意外清除。<br/>' +
|
|
|
- '下次打印合同时,需要重新编辑模板内容。' +
|
|
|
- '</div>',
|
|
|
+ '<div style="margin-top: 10px; line-height: 1.8;">' +
|
|
|
+ '您在编辑合同表单时,之前的富文本模板内容已被意外清除。<br/>' +
|
|
|
+ '下次打印合同时,需要重新编辑模板内容。' +
|
|
|
+ '</div>',
|
|
|
'温馨提示',
|
|
|
{
|
|
|
confirmButtonText: '知道了',
|
|
|
@@ -2718,4 +2938,29 @@ onMounted(async () => {
|
|
|
background: transparent !important;
|
|
|
border: none !important;
|
|
|
}
|
|
|
+
|
|
|
+/* 框架合同调价函:表格内多图卡片上传 */
|
|
|
+.tier-letter-upload :deep(.el-upload-list--picture-card) {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.tier-letter-upload :deep(.el-upload--picture-card) {
|
|
|
+ width: 72px;
|
|
|
+ height: 72px;
|
|
|
+ line-height: 72px;
|
|
|
+}
|
|
|
+.tier-letter-upload :deep(.el-upload-list--picture-card .el-upload-list__item) {
|
|
|
+ width: 72px;
|
|
|
+ height: 72px;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 框架合同:增加「调价函」行内按钮可点区域宽度 */
|
|
|
+.fw-tier-letter-add-btn.el-button--primary.is-link {
|
|
|
+ min-width: 108px;
|
|
|
+ padding-left: 12px;
|
|
|
+ padding-right: 12px;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
</style>
|