From c5b6bf8931e80e5b097af0f93c95ee82164cf872 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 14 八月 2026 13:38:54 +0800
Subject: [PATCH] feat(srm): 添加供应商证书和招标管理附件功能

---
 src/api/srm/tender/index.ts                        |    4 +
 src/views/erp/purchase/invoice/index.vue           |   42 ----------
 src/views/bpm/model/index.vue                      |    7 +
 src/views/srm/statistics/index.vue                 |    9 ++
 src/views/crm/receivable/data.ts                   |   10 --
 src/views/erp/purchase/invoice/data.ts             |   22 -----
 src/views/srm/supplierCertificate/modules/form.vue |   11 ++
 src/components/form-create/helpers.ts              |    9 +
 src/views/srm/supplierCertificate/data.ts          |   20 +++++
 src/views/srm/tender/detail.vue                    |   28 +++++-
 src/views/srm/supplierCertificate/index.vue        |   17 ++++
 src/api/srm/supplier/index.ts                      |    5 +
 src/views/crm/receivable/components/data.ts        |   10 --
 src/api/srm/supplierCertificate/index.ts           |    3 
 src/api/erp/purchase/invoice/index.ts              |   17 ----
 src/views/srm/tender/data.ts                       |   13 +++
 src/views/srm/tender/modules/form.vue              |   12 +++
 17 files changed, 129 insertions(+), 110 deletions(-)

diff --git a/src/api/erp/purchase/invoice/index.ts b/src/api/erp/purchase/invoice/index.ts
index bf69aaa..305230c 100644
--- a/src/api/erp/purchase/invoice/index.ts
+++ b/src/api/erp/purchase/invoice/index.ts
@@ -26,8 +26,6 @@
     invoiceTitle?: string;
     price?: number;
     invoiceTime?: string;
-    auditStatus?: number;
-    processInstanceId?: string;
     remark?: string;
     attachmentList?: { id: number; name?: string; url?: string }[];
     blobIds?: number[];
@@ -48,7 +46,6 @@
     price?: number;
     supplierId?: number;
     purchaseOrderId?: number;
-    auditStatus?: number;
     hasAttachment?: boolean;
     remainingInvoicePrice?: number;
   }
@@ -89,19 +86,7 @@
   return requestClient.download('/erp/purchase-invoice/export-excel', { params });
 }
 
-/** 鏇存柊鏉ョエ鐨勭姸鎬侊紙瀹℃牳/鍙嶅鏍革級 */
-export function updatePurchaseInvoiceStatus(id: number, status: number) {
-  return requestClient.put('/erp/purchase-invoice/update-status', null, {
-    params: { id, status },
-  });
-}
-
-/** 鑾峰緱寰呭鏍告潵绁ㄦ暟閲� */
-export function getAuditPurchaseInvoiceCount() {
-  return requestClient.get<number>('/erp/purchase-invoice/audit-count');
-}
-
-/** 鏌ヨ鏉ョエ涓嬫媺鍒楄〃锛堟寜渚涘簲鍟嗗彲閫夋潵绁紝浠呭鏍搁�氳繃涓斿凡涓婁紶鍙戠エ闄勪欢锛� */
+/** 鏌ヨ鏉ョエ涓嬫媺鍒楄〃锛堟寜渚涘簲鍟嗗彲閫夋潵绁紝宸蹭笂浼犲彂绁ㄩ檮浠朵笖鍓╀綑鍙粯閲戦澶т簬 0锛� */
 export function getPurchaseInvoiceSimpleList(
   supplierId: number,
   purchaseOrderId?: number,
diff --git a/src/api/srm/supplier/index.ts b/src/api/srm/supplier/index.ts
index 60b176a..e75a362 100644
--- a/src/api/srm/supplier/index.ts
+++ b/src/api/srm/supplier/index.ts
@@ -31,6 +31,11 @@
   return requestClient.get<SrmSupplierApi.SupplierVO[]>('/srm/supplier/simple-list');
 }
 
+/** 鑾峰彇渚涘簲鍟嗘。妗堝垪琛紙涓嶈繃婊ゅ噯鍏ョ姸鎬侊級 */
+export function getSupplierList() {
+  return requestClient.get<SrmSupplierApi.SupplierVO[]>('/srm/supplier/list');
+}
+
 /** 鑾峰彇渚涘簲鍟嗚鎯� */
 export function getSupplier(id: number) {
   return requestClient.get<SrmSupplierApi.SupplierVO>(`/srm/supplier/get?id=${id}`);
diff --git a/src/api/srm/supplierCertificate/index.ts b/src/api/srm/supplierCertificate/index.ts
index 9908b1c..2504662 100644
--- a/src/api/srm/supplierCertificate/index.ts
+++ b/src/api/srm/supplierCertificate/index.ts
@@ -1,5 +1,6 @@
 import type { PageParam, PageResult } from '@vben/request';
 
+import type { SystemStorageApi } from '#/api/system/storage';
 import { requestClient } from '#/api/request';
 
 export namespace SrmSupplierCertificateApi {
@@ -16,6 +17,8 @@
     status?: number;
     remark?: string;
     createTime?: string;
+    blobIds?: number[];
+    attachmentList?: SystemStorageApi.StorageBlob[];
   }
 }
 
diff --git a/src/api/srm/tender/index.ts b/src/api/srm/tender/index.ts
index a44c5b9..cefefdb 100644
--- a/src/api/srm/tender/index.ts
+++ b/src/api/srm/tender/index.ts
@@ -1,5 +1,6 @@
 import type { PageParam, PageResult } from '@vben/request';
 
+import type { SystemStorageApi } from '#/api/system/storage';
 import { requestClient } from '#/api/request';
 
 export namespace SrmTenderApi {
@@ -23,6 +24,8 @@
     purchaseOrderNo?: string;
     biddingMode?: string;
     createTime?: string;
+    blobIds?: number[];
+    attachmentList?: SystemStorageApi.StorageBlob[];
   }
 
   export interface TenderMaterialVO {
@@ -105,6 +108,7 @@
     id: number;
     tenderDesc?: string;
     qualificationRequirements?: string;
+    blobIds?: number[];
   }
 }
 
diff --git a/src/components/form-create/helpers.ts b/src/components/form-create/helpers.ts
index 12d07a6..9e827de 100644
--- a/src/components/form-create/helpers.ts
+++ b/src/components/form-create/helpers.ts
@@ -28,7 +28,10 @@
 }
 
 /** 瑙g爜琛ㄥ崟 Conf */
-export function decodeConf(conf: string) {
+export function decodeConf(conf?: string | null) {
+  if (!conf) {
+    return {};
+  }
   return formCreate.parseJson(conf);
 }
 
@@ -43,9 +46,9 @@
 }
 
 /** 瑙g爜琛ㄥ崟 Fields */
-export function decodeFields(fields: string[]) {
+export function decodeFields(fields?: string[] | null) {
   const rule: Rule[] = [];
-  fields.forEach((item) => {
+  (fields || []).forEach((item) => {
     rule.push(formCreate.parseJson(item));
   });
   return rule;
diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue
index 91c504f..d7b2b7d 100644
--- a/src/views/bpm/model/index.vue
+++ b/src/views/bpm/model/index.vue
@@ -1,7 +1,7 @@
 <script lang="ts" setup>
 import type { ModelCategoryInfo } from '#/api/bpm/model';
 
-import { onActivated, reactive, ref, useTemplateRef, watch } from 'vue';
+import { onActivated, onMounted, reactive, ref, useTemplateRef, watch } from 'vue';
 
 import { Page, useVbenModal } from '@vben/common-ui';
 import { IconifyIcon } from '@vben/icons';
@@ -76,6 +76,11 @@
 }
 
 /** 鍒濆鍖� */
+onMounted(() => {
+  getList();
+});
+
+/** 婵�娲绘椂鍒锋柊锛坘eep-alive 缂撳瓨鍦烘櫙涓嬩粠缂栬緫椤佃繑鍥炴椂瑙﹀彂锛� */
 onActivated(() => {
   getList();
 });
diff --git a/src/views/crm/receivable/components/data.ts b/src/views/crm/receivable/components/data.ts
index 4f3f0e1..4aa273b 100644
--- a/src/views/crm/receivable/components/data.ts
+++ b/src/views/crm/receivable/components/data.ts
@@ -53,16 +53,6 @@
       minWidth: 150,
     },
     {
-      title: '鍥炴鐘舵��',
-      field: 'auditStatus',
-      minWidth: 100,
-      fixed: 'right',
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
-      },
-    },
-    {
       title: '鎿嶄綔',
       field: 'actions',
       width: 130,
diff --git a/src/views/crm/receivable/data.ts b/src/views/crm/receivable/data.ts
index 5dc6c1a..e401e1f 100644
--- a/src/views/crm/receivable/data.ts
+++ b/src/views/crm/receivable/data.ts
@@ -357,16 +357,6 @@
       minWidth: 150,
     },
     {
-      title: '鍥炴鐘舵��',
-      field: 'auditStatus',
-      minWidth: 100,
-      fixed: 'right',
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.CRM_AUDIT_STATUS },
-      },
-    },
-    {
       title: '鎿嶄綔',
       field: 'actions',
       minWidth: 200,
diff --git a/src/views/erp/purchase/invoice/data.ts b/src/views/erp/purchase/invoice/data.ts
index c70e185..a371839 100644
--- a/src/views/erp/purchase/invoice/data.ts
+++ b/src/views/erp/purchase/invoice/data.ts
@@ -148,19 +148,6 @@
         valueField: 'id',
       },
     },
-    {
-      fieldName: 'auditStatus',
-      label: '瀹℃牳鐘舵��',
-      component: 'Select',
-      componentProps: {
-        options: [
-          { label: '鏈鏍�', value: 10 },
-          { label: '宸插鏍�', value: 20 },
-        ],
-        placeholder: '璇烽�夋嫨瀹℃牳鐘舵��',
-        allowClear: true,
-      },
-    },
   ];
 }
 
@@ -221,16 +208,9 @@
       slots: { default: 'attachment' },
     },
     {
-      title: '瀹℃壒鐘舵��',
-      field: 'auditStatus',
-      minWidth: 100,
-      fixed: 'right',
-      slots: { default: 'auditStatus' },
-    },
-    {
       title: '鎿嶄綔',
       field: 'actions',
-      minWidth: 280,
+      minWidth: 200,
       fixed: 'right',
       slots: { default: 'actions' },
     },
diff --git a/src/views/erp/purchase/invoice/index.vue b/src/views/erp/purchase/invoice/index.vue
index c0cbd7b..38fc81b 100644
--- a/src/views/erp/purchase/invoice/index.vue
+++ b/src/views/erp/purchase/invoice/index.vue
@@ -12,7 +12,6 @@
   deletePurchaseInvoice,
   exportPurchaseInvoice,
   getPurchaseInvoicePage,
-  updatePurchaseInvoiceStatus,
 } from '#/api/erp/purchase/invoice';
 import { $t } from '#/locales';
 
@@ -62,24 +61,6 @@
   try {
     await deletePurchaseInvoice(row.id!);
     message.success($t('ui.actionMessage.deleteSuccess', [row.no]));
-    handleRefresh();
-  } finally {
-    hideLoading();
-  }
-}
-
-/** 鏇存柊鏉ョエ鐘舵�侊紙瀹℃牳/鍙嶅鏍革級 */
-async function handleUpdateStatus(
-  row: ErpPurchaseInvoiceApi.PurchaseInvoice,
-  status: number,
-) {
-  const hideLoading = message.loading({
-    content: '鏇存柊鐘舵�佷腑...',
-    duration: 0,
-  });
-  try {
-    await updatePurchaseInvoiceStatus(row.id!, status);
-    message.success('鏇存柊鐘舵�佹垚鍔�');
     handleRefresh();
   } finally {
     hideLoading();
@@ -153,11 +134,6 @@
         <Tag v-if="!row.hasAttachment" color="warning">鏈笂浼�</Tag>
         <Tag v-else color="success">宸蹭笂浼�</Tag>
       </template>
-      <template #auditStatus="{ row }">
-        <Tag v-if="row.auditStatus === 10" color="warning">鏈鏍�</Tag>
-        <Tag v-else-if="row.auditStatus === 20" color="success">宸插鏍�</Tag>
-        <Tag v-else color="default">--</Tag>
-      </template>
       <template #actions="{ row }">
         <TableAction
           :actions="[
@@ -173,24 +149,7 @@
               type: 'link',
               icon: ACTION_ICON.EDIT,
               auth: ['erp:purchase-invoice:update'],
-              ifShow: row.auditStatus === 10,
               onClick: handleEdit.bind(null, row),
-            },
-            {
-              label: '瀹℃牳',
-              type: 'link',
-              icon: ACTION_ICON.AUDIT,
-              auth: ['erp:purchase-invoice:update-status'],
-              ifShow: row.auditStatus === 10,
-              onClick: handleUpdateStatus.bind(null, row, 20),
-            },
-            {
-              label: '鍙嶅鏍�',
-              type: 'link',
-              icon: ACTION_ICON.VIEW,
-              auth: ['erp:purchase-invoice:update-status'],
-              ifShow: row.auditStatus === 20,
-              onClick: handleUpdateStatus.bind(null, row, 10),
             },
             {
               label: $t('common.delete'),
@@ -198,7 +157,6 @@
               danger: true,
               icon: ACTION_ICON.DELETE,
               auth: ['erp:purchase-invoice:delete'],
-              ifShow: row.auditStatus !== 20,
               popConfirm: {
                 title: $t('ui.actionMessage.deleteConfirm', [row.no]),
                 confirm: handleDelete.bind(null, row),
diff --git a/src/views/srm/statistics/index.vue b/src/views/srm/statistics/index.vue
index 439ae78..5f9652a 100644
--- a/src/views/srm/statistics/index.vue
+++ b/src/views/srm/statistics/index.vue
@@ -96,7 +96,14 @@
         </div>
         <div class="rounded-lg bg-purple-50 p-6 text-center shadow-sm">
           <div class="text-3xl font-bold text-purple-600">{{ stats.savedAmount?.toLocaleString() ?? 0 }}</div>
-          <div class="mt-2 text-gray-500">鑺傜害閲戦(鍏�)</div>
+          <div class="mt-2 flex items-center justify-center gap-1 text-gray-500">
+            鑺傜害閲戦(鍏�)
+            <a-tooltip title="鑺傜害閲戦 = 鎷涙爣鎬婚噾棰�(棰勭畻閲戦) 鈭� 瀹氭爣鎬婚噾棰�(涓爣閲戦)">
+              <span
+                class="inline-flex h-3.5 w-3.5 cursor-help items-center justify-center rounded-full bg-gray-400 text-[10px] leading-none text-white"
+              >?</span>
+            </a-tooltip>
+          </div>
         </div>
         <div class="rounded-lg bg-teal-50 p-6 text-center shadow-sm">
           <div class="text-3xl font-bold text-teal-600">{{ stats.winRate ?? 0 }}%</div>
diff --git a/src/views/srm/supplierCertificate/data.ts b/src/views/srm/supplierCertificate/data.ts
index 1472c53..c956f2d 100644
--- a/src/views/srm/supplierCertificate/data.ts
+++ b/src/views/srm/supplierCertificate/data.ts
@@ -83,6 +83,19 @@
       rules: 'required',
     },
     {
+      fieldName: 'blobIds',
+      label: '璇佷功闄勪欢',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        accept: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
+        helpText: '鏀寔jpg銆乯peg銆乸ng銆乸df銆乨oc銆乨ocx鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃2MB',
+      },
+      formItemClass: 'md:col-span-2',
+    },
+    {
       fieldName: 'remark',
       label: '澶囨敞',
       component: 'Textarea',
@@ -144,6 +157,13 @@
     { field: 'validEndDate', title: '鍒版湡鏃ユ湡', minWidth: 120 },
     { field: 'createTime', title: '鍒涘缓鏃堕棿', minWidth: 160 },
     {
+      field: 'attachmentList',
+      title: '闄勪欢',
+      minWidth: 100,
+      fixed: 'right',
+      slots: { default: 'attachment' },
+    },
+    {
       title: '鎿嶄綔',
       width: 130,
       fixed: 'right',
diff --git a/src/views/srm/supplierCertificate/index.vue b/src/views/srm/supplierCertificate/index.vue
index 29ea03f..a26aa75 100644
--- a/src/views/srm/supplierCertificate/index.vue
+++ b/src/views/srm/supplierCertificate/index.vue
@@ -4,7 +4,7 @@
 
 import { Page, useVbenModal } from '@vben/common-ui';
 
-import { message } from 'ant-design-vue';
+import { message, Tag } from 'ant-design-vue';
 
 import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
@@ -112,6 +112,21 @@
           ]"
         />
       </template>
+      <template #attachment="{ row }">
+        <div v-if="row.attachmentList && row.attachmentList.length" class="flex flex-col gap-1">
+          <a
+            v-for="file in row.attachmentList"
+            :key="file.id"
+            :href="file.url"
+            target="_blank"
+            rel="noopener noreferrer"
+            class="text-blue-500 hover:underline"
+          >
+            {{ file.name || '闄勪欢-' + file.id }}
+          </a>
+        </div>
+        <Tag v-else color="default">--</Tag>
+      </template>
       <template #actions="{ row }">
         <TableAction
           :actions="[
diff --git a/src/views/srm/supplierCertificate/modules/form.vue b/src/views/srm/supplierCertificate/modules/form.vue
index d0bc05b..041e5e0 100644
--- a/src/views/srm/supplierCertificate/modules/form.vue
+++ b/src/views/srm/supplierCertificate/modules/form.vue
@@ -65,6 +65,17 @@
     modalApi.lock();
     try {
       formData.value = await getCertificate(data.id);
+      if (formData.value?.attachmentList?.length) {
+        // 杞负涓婁紶缁勪欢鐨勬枃浠跺璞★紱鎻愪氦鏃� formApi 浼氭寜 valueKey 鎻愬彇闄勪欢 ID
+        (formData.value as Record<string, unknown>).blobIds =
+          formData.value.attachmentList.map((item) => ({
+            uid: String(item.id),
+            name: item.name || '',
+            url: item.url || '',
+            status: 'done',
+            id: item.id,
+          }));
+      }
       await formApi.setValues(formData.value);
     } finally {
       modalApi.unlock();
diff --git a/src/views/srm/tender/data.ts b/src/views/srm/tender/data.ts
index a3b1005..475eae7 100644
--- a/src/views/srm/tender/data.ts
+++ b/src/views/srm/tender/data.ts
@@ -180,6 +180,19 @@
       componentProps: { placeholder: '璇疯緭鍏ヨ祫璐ㄨ姹�', rows: 3 },
       formItemClass: 'col-span-2',
     },
+    {
+      fieldName: 'blobIds',
+      label: '璧勮川瑕佹眰闄勪欢',
+      component: 'FileUpload',
+      componentProps: {
+        valueKey: 'id',
+        maxNumber: 10,
+        multiple: true,
+        accept: ['jpg', 'jpeg', 'png', 'pdf', 'doc', 'docx'],
+        helpText: '鏀寔jpg銆乯peg銆乸ng銆乸df銆乨oc銆乨ocx鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃2MB',
+      },
+      formItemClass: 'col-span-2',
+    },
   ];
 }
 
diff --git a/src/views/srm/tender/detail.vue b/src/views/srm/tender/detail.vue
index b7f5e2c..83cd7b8 100644
--- a/src/views/srm/tender/detail.vue
+++ b/src/views/srm/tender/detail.vue
@@ -19,7 +19,7 @@
   createAward, deleteAward, getAward, getAwardByProject, approveAward, generatePurchaseOrder,
   confirmTenderProject,
 } from '#/api/srm/tender';
-import { getSupplierSimpleList } from '#/api/srm/supplier';
+import { getSupplierList } from '#/api/srm/supplier';
 import { getItemSimpleList } from '#/api/mdm/item';
 import type { MdmItemApi } from '#/api/mdm/item';
 import {
@@ -51,7 +51,7 @@
 onMounted(async () => {
   const [items, suppliers] = await Promise.all([
     getItemSimpleList(),
-    getSupplierSimpleList(),
+    getSupplierList(),
   ]);
   mdmItemList.value = items;
   supplierList.value = suppliers;
@@ -538,14 +538,32 @@
           </div>
 
           <!-- 鎷涙爣璇存槑 / 璧勮川瑕佹眰 -->
-          <template v-if="project.tenderDesc || project.qualificationRequirements">
+          <template
+            v-if="project.tenderDesc || project.qualificationRequirements || (project.attachmentList && project.attachmentList.length)"
+          >
             <a-divider class="!my-2" />
             <a-collapse :bordered="false" ghost expand-icon-position="end">
               <a-collapse-panel v-if="project.tenderDesc" key="desc" header="鎷涙爣璇存槑">
                 <p class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.tenderDesc }}</p>
               </a-collapse-panel>
-              <a-collapse-panel v-if="project.qualificationRequirements" key="req" header="璧勮川瑕佹眰">
-                <p class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.qualificationRequirements }}</p>
+              <a-collapse-panel
+                v-if="project.qualificationRequirements || (project.attachmentList && project.attachmentList.length)"
+                key="req"
+                header="璧勮川瑕佹眰"
+              >
+                <p v-if="project.qualificationRequirements" class="text-gray-500 text-xs whitespace-pre-wrap">{{ project.qualificationRequirements }}</p>
+                <div v-if="project.attachmentList && project.attachmentList.length" class="mt-2 flex flex-col gap-1">
+                  <a
+                    v-for="file in project.attachmentList"
+                    :key="file.id"
+                    :href="file.url"
+                    target="_blank"
+                    rel="noopener noreferrer"
+                    class="text-blue-500 text-xs hover:underline"
+                  >
+                    {{ file.name || '闄勪欢-' + file.id }}
+                  </a>
+                </div>
               </a-collapse-panel>
             </a-collapse>
           </template>
diff --git a/src/views/srm/tender/modules/form.vue b/src/views/srm/tender/modules/form.vue
index bcdc8d2..652d017 100644
--- a/src/views/srm/tender/modules/form.vue
+++ b/src/views/srm/tender/modules/form.vue
@@ -58,6 +58,7 @@
           id: data.id!,
           tenderDesc: data.tenderDesc,
           qualificationRequirements: data.qualificationRequirements,
+          blobIds: data.blobIds,
         });
       } else {
         await (formData.value?.id
@@ -92,6 +93,17 @@
     modalApi.lock();
     try {
       formData.value = await getTenderProject(data.id);
+      if (formData.value?.attachmentList?.length) {
+        // 杞负涓婁紶缁勪欢鐨勬枃浠跺璞★紱鎻愪氦鏃� formApi 浼氭寜 valueKey 鎻愬彇闄勪欢 ID
+        (formData.value as Record<string, unknown>).blobIds =
+          formData.value.attachmentList.map((item) => ({
+            uid: String(item.id),
+            name: item.name || '',
+            url: item.url || '',
+            status: 'done',
+            id: item.id,
+          }));
+      }
       const partial = formData.value.tenderStatus !== undefined
         && formData.value.tenderStatus !== TENDER_STATUS.DRAFT;
       await formApi.setValues({ ...formData.value, _partial: partial || undefined });

--
Gitblit v1.9.3