zlwu 11 kuukautta sitten
vanhempi
commit
2f6cab5b24

BIN
.DS_Store


+ 2 - 3
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/domain/bo/CrmContractProductBo.java

@@ -25,14 +25,13 @@ public class CrmContractProductBo extends BaseEntity {
 
     /**
      * 主键ID
+     * 注意:编辑合同时,后端会先删除所有旧产品再重新插入,所以不需要校验id
      */
-    @NotNull(message = "主键不能为空", groups = { EditGroup.class })
     private Long id;
 
     /**
-     * 合同ID - 新增时由后端自动填充
+     * 合同ID - 新增时由后端自动填充,编辑时也会自动填充
      */
-    @NotNull(message = "合同ID不能为空", groups = { EditGroup.class })
     private Long contractId;
 
     /**

+ 58 - 46
web/src/views/crm/contract/form.vue

@@ -1932,29 +1932,35 @@ const handleSaveDraft = async () => {
 
     // 清理产品列表中的扩展字段并映射回后端字段
     if (data.products && Array.isArray(data.products) && data.products.length > 0) {
-      data.products = data.products.map((product: any) => ({
-        id: product.id,
-        contractId: product.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
-      }));
+      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
+      })
     } else {
       // 如果产品列表为空,删除该字段,避免后端校验
       delete data.products;
@@ -2120,29 +2126,35 @@ const handleSubmit = async () => {
 
     // 清理产品列表中的扩展字段并映射回后端字段
     if (data.products && Array.isArray(data.products) && data.products.length > 0) {
-      data.products = data.products.map((product: any) => ({
-        id: product.id,
-        contractId: product.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
-      }));
+      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
+      })
     } else {
       // 如果产品列表为空,删除该字段,避免后端校验
       delete data.products;

+ 27 - 25
web/src/views/crm/contract/index.vue

@@ -364,14 +364,7 @@
                 @click.stop="handleViewAttachment(scope.row, file)"
               >
                 <!-- 图片缩略图 -->
-                <ImagePreview
-                  v-if="isListImageFile(file)"
-                  :src="file.ossId"
-                  class="attachment-thumb-image"
-                  :isOssId="true"
-                  width="50"
-                  height="50"
-                />
+                <ImagePreview v-if="isListImageFile(file)" :src="file.ossId" class="attachment-thumb-image" :isOssId="true" width="50" height="50" />
                 <!-- 非图片文件图标 -->
                 <div v-else class="attachment-file-icon">
                   <el-icon><Document /></el-icon>
@@ -1668,14 +1661,14 @@ const loadAllAttachmentInfos = () => {
   contractList.value.forEach((row) => {
     if (row.attachmentFiles) {
       const attachments = parseListAttachments(row.attachmentFiles);
-      attachments.forEach(att => {
+      attachments.forEach((att) => {
         if (att.ossId && !allOssIds.includes(att.ossId)) {
           allOssIds.push(att.ossId);
         }
       });
     }
   });
-  
+
   if (allOssIds.length > 0) {
     loadAttachmentInfos(allOssIds);
   }
@@ -1708,7 +1701,7 @@ const parseListAttachments = (fileStr: string): Array<{ ossId: string; category?
       if (Array.isArray(parsed)) {
         return parsed.map((item: any) => ({
           ossId: typeof item === 'string' ? item : item.ossId || item,
-          category: typeof item === 'object' ? (item.category || 'other') : 'other'
+          category: typeof item === 'object' ? item.category || 'other' : 'other'
         }));
       }
     } catch (e) {
@@ -1721,7 +1714,7 @@ const parseListAttachments = (fileStr: string): Array<{ ossId: string; category?
     .split(',')
     .map((id: string) => id.trim())
     .filter((id: string) => id)
-    .map(ossId => ({ ossId, category: 'other' }));
+    .map((ossId) => ({ ossId, category: 'other' }));
 };
 
 /** 获取附件数量 */
@@ -1739,7 +1732,7 @@ const loadAttachmentInfos = async (ossIds: string[]) => {
   if (ossIds.length === 0) return;
 
   // 过滤出未缓存的ossId
-  const uncachedIds = ossIds.filter(id => !attachmentInfoCache.value.has(id));
+  const uncachedIds = ossIds.filter((id) => !attachmentInfoCache.value.has(id));
   if (uncachedIds.length === 0) return;
 
   try {
@@ -1765,7 +1758,7 @@ const loadAttachmentInfos = async (ossIds: string[]) => {
 const getAttachmentList = (row: ContractVO): Array<{ ossId: string; name: string; url?: string }> => {
   if (!row.attachmentFiles) return [];
   const attachments = parseListAttachments(row.attachmentFiles);
-  
+
   // 返回所有附件,优先使用缓存的文件名
   return attachments.map((att, index) => {
     const cachedInfo = attachmentInfoCache.value.get(att.ossId);
@@ -1795,19 +1788,19 @@ const getListFileExt = (fileName: string): string => {
 /** 获取短文件名(用于显示) */
 const getShortFileName = (fileName: string): string => {
   if (!fileName) return '';
-  
+
   const lastDotIndex = fileName.lastIndexOf('.');
   if (lastDotIndex === -1) {
     return fileName.length > 10 ? fileName.substring(0, 10) + '...' : fileName;
   }
-  
+
   const name = fileName.substring(0, lastDotIndex);
   const ext = fileName.substring(lastDotIndex);
-  
+
   if (name.length <= 8) {
     return fileName;
   }
-  
+
   return name.substring(0, 8) + '...' + ext;
 };
 
@@ -2827,6 +2820,13 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
     return '/src/assets/images/yz.png';
   };
 
+  // 获取单位字典标签的辅助函数
+  const getUnitLabel = (unitValue: string | undefined): string => {
+    if (!unitValue) return '';
+    const unitDict = crm_product_unit.value?.find((item: any) => item.value === unitValue);
+    return unitDict?.label || unitValue || '';
+  };
+
   // 构建产品明细表格行
   const productRows =
     contractData.products
@@ -2839,6 +2839,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
     <tr>
       <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;">${getUnitLabel(product.unit)}</td>
       <td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.quantity || 0}</td>
       <td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${formatNumber(product.price)}</td>
       <td style="padding: 4px 6px; border: 1px solid #000; text-align: right; font-size: 12px;">${formatNumber(beforeTax)}</td>
@@ -2848,7 +2849,7 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
     </tr>`;
       })
       .join('') ||
-    '<tr><td colspan="8" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
+    '<tr><td colspan="9" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
 
   return `
     <div style="width: 210mm; margin: 0 auto; padding: 0 15mm 20mm 15mm; font-family: '宋体', SimSun, serif; line-height: 1.6; box-sizing: border-box;">
@@ -2890,8 +2891,9 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
             <tr style="background-color: #fff;">
               <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 20%;">产品名称</th>
               <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 10%;">规格</th>
-              <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 8%;">用量(吨)</th>
-              <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 12%;">含税单价(元/吨)</th>
+              <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 8%;">单位</th>
+              <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 8%;">用量</th>
+              <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 12%;">含税单价(元)</th>
               <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 12%;">未含税金额(元)</th>
               <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 10%;">税费(元)</th>
               <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 13%;">含税金额(元)</th>
@@ -2902,12 +2904,12 @@ const generateContractTemplate = (contractData: ContractVO | undefined): string
             ${productRows}
             <!-- 合计行 -->
             <tr style="background-color: #fff; font-weight: bold;">
-              <td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">合计(人民币)</td>
+              <td colspan="5" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">合计(人民币)</td>
               <td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${productTotalChinese}(¥${formatNumber(productTotal)})</td>
             </tr>
             <!-- 提示行 -->
             <tr>
-              <td colspan="8" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 11px; background-color: #fff;">
+              <td colspan="9" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 11px; background-color: #fff;">
                 含税、含运费、装卸费(一票制)&nbsp;&nbsp;&nbsp;&nbsp;结算以实际到货吨位为准
               </td>
             </tr>
@@ -3690,11 +3692,11 @@ onMounted(() => {
   overflow-x: auto;
   overflow-y: hidden;
   white-space: nowrap;
-  
+
   /* 隐藏滚动条但保持滚动功能 */
   scrollbar-width: none; /* Firefox */
   -ms-overflow-style: none; /* IE and Edge */
-  
+
   &::-webkit-scrollbar {
     display: none; /* Chrome, Safari, Opera */
   }