From c77086ad2d7949ab36dd0c4e0b59907e672dd64f Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 18 九月 2026 15:04:42 +0800
Subject: [PATCH] Merge remote-tracking branch 'refs/remotes/origin/dev_pro2.0' into dev_宁夏_九泓化工

---
 src/views/srm/tender/detail.vue |  161 ++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 126 insertions(+), 35 deletions(-)

diff --git a/src/views/srm/tender/detail.vue b/src/views/srm/tender/detail.vue
index 7b2f1de..fda47ec 100644
--- a/src/views/srm/tender/detail.vue
+++ b/src/views/srm/tender/detail.vue
@@ -6,20 +6,20 @@
 
 import { Page } from '@vben/common-ui';
 
-import { message, Modal } from 'ant-design-vue';
+import { message } from 'ant-design-vue';
 import { IconifyIcon } from '@vben/icons';
 
 import {
   getTenderProject,
   getMaterialList, createMaterial, updateMaterial, deleteMaterial,
-  getBidListByProject, createBid, withdrawBid,
+  getBidListByProject, createBid, withdrawBid, updateBidTotal,
   getQuoteList, createQuote, updateQuote,
   createBidOpen, getBidOpen,
   getEvaluationList, createBidEvaluation, updateBidEvaluation, calculateRanking,
   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;
@@ -150,6 +150,8 @@
 const quoteFormVisible = ref(false);
 const quoteForm = ref<SrmTenderApi.QuoteVO>({ bidId: undefined!, tenderMaterialId: undefined! });
 const selectedBidId = ref<number>();
+/** 鎶曟爣鎬婚噾棰濊崏绋匡細杩涘叆鎶ヤ环椤垫椂甯﹀嚭宸蹭繚瀛樻�讳环锛屽彲鑷姩甯﹀嚭鎶ヤ环鍚堣鍚庝汉宸ュ井璋� */
+const bidTotalDraft = ref<number>();
 
 const availableMaterials = computed(() => {
   const quotedIds = new Set(
@@ -169,29 +171,11 @@
   selectedBidId.value = bidId;
   quoteLoading.value = true;
   try { quoteList.value = await getQuoteList(bidId); } finally { quoteLoading.value = false; }
+  // 甯﹀嚭璇ユ姇鏍囧凡淇濆瓨鐨勬姇鏍囨�婚噾棰濓紙涓虹┖鍒欒鐢ㄦ埛褰曞叆/鑷姩甯﹀嚭锛�
+  const bid = bidList.value.find((b) => b.id === bidId);
+  bidTotalDraft.value = bid?.bidTotalAmount ?? undefined;
 }
 async function handleSaveQuote() {
-  // 鏍¢獙锛氭姤浠烽噾棰濅笌鎶曟爣閲戦涓�鑷存��
-  const bid = bidList.value.find((b) => b.id === quoteForm.value.bidId);
-  if (bid?.bidTotalAmount != null) {
-    const otherQuotesTotal = quoteList.value
-      .filter((q) => q.id !== quoteForm.value.id)
-      .reduce((sum, q) => sum + (q.quotePrice ?? 0), 0);
-    const newTotal = otherQuotesTotal + (quoteForm.value.quotePrice ?? 0);
-    if (newTotal !== bid.bidTotalAmount) {
-      await new Promise<void>((resolve, reject) => {
-        Modal.confirm({
-          title: '閲戦涓嶄竴鑷存彁绀�',
-          content: `鎶ヤ环鎬婚噾棰濓紙${newTotal.toLocaleString()}锛変笌鎶曟爣閲戦锛�${bid.bidTotalAmount!.toLocaleString()}锛変笉涓�鑷达紝鏄惁缁х画鎻愪氦锛焋,
-          okText: '缁х画鎻愪氦',
-          cancelText: '鍙栨秷',
-          onOk: () => { resolve(); },
-          onCancel: () => { reject(new Error('cancel')); },
-        });
-      });
-    }
-  }
-
   if (quoteForm.value.id) {
     await updateQuote(quoteForm.value);
   } else {
@@ -208,6 +192,62 @@
   quoteForm.value = { ...row };
   quoteFormVisible.value = true;
 }
+
+/** 鎷涙爣鐗╂枡 Map锛氭姤浠疯鎸� tenderMaterialId 鍙嶆煡鐗╂枡鏁伴噺 */
+const materialMap = computed(() => {
+  const map = new Map<number, SrmTenderApi.TenderMaterialVO>();
+  materialList.value.forEach((m) => {
+    if (m.id != null) map.set(m.id, m);
+  });
+  return map;
+});
+
+/** 鎶ヤ环鏄庣粏瀵瑰簲鐨勬嫑鏍囩墿鏂欐暟閲� */
+function materialQuantityOf(quote: SrmTenderApi.QuoteVO): number {
+  return materialMap.value.get(quote.tenderMaterialId!)?.quantity ?? 0;
+}
+
+/** 鎶ヤ环琛岄噾棰� = 鎶ヤ环鍗曚环 脳 鐗╂枡鏁伴噺 */
+function quoteAmountOf(quote: SrmTenderApi.QuoteVO): number {
+  return (quote.quotePrice ?? 0) * materialQuantityOf(quote);
+}
+
+/** 閫変腑鎶曟爣涓嬪凡褰曟姤浠风殑鍚堣閲戦 */
+const selectedQuoteTotalAmount = computed(() =>
+  quoteList.value.reduce((sum, q) => sum + quoteAmountOf(q), 0),
+);
+
+/** 缁存姢锛堟暣鍗曪級鎶曟爣鎬婚噾棰濓細鍙嚜鍔ㄥ甫鍑烘姤浠峰悎璁″悗浜哄伐寰皟锛堢敤浜庢暣鍗曡鍒╃瓑锛� */
+async function handleSaveBidTotal() {
+  if (!selectedBidId.value) return;
+  if (bidTotalDraft.value == null) {
+    message.warning('璇峰~鍐欐姇鏍囨�婚噾棰�');
+    return;
+  }
+  await updateBidTotal(selectedBidId.value, bidTotalDraft.value);
+  message.success('鎶曟爣鎬婚噾棰濆凡淇濆瓨');
+  loadBids();
+}
+
+/** 宸蹭繚瀛樼殑鎶曟爣鎬婚噾棰� 涓� 鎶ヤ环鍚堣 鐨勫樊寮傛彁绀猴紙鍙彁绀猴紝涓嶆嫤鎴級 */
+const bidTotalCompareHint = computed<{ text: string; tone: 'success' | 'warning' } | null>(() => {
+  const sum = selectedQuoteTotalAmount.value;
+  if (sum <= 0) {
+    return null; // 灏氭湭褰曟姤浠凤紝鏆備笉姣旇緝
+  }
+  const stored = selectedBid.value?.bidTotalAmount;
+  if (stored == null) {
+    return { text: '璇ユ姇鏍囧皻鏈淮鎶ゆ�婚噾棰濓細鍙偣銆岃嚜鍔ㄥ甫鍑烘姤浠峰悎璁°�嶅~鍏ュ悗淇濆瓨', tone: 'warning' };
+  }
+  const diff = Math.abs(stored - sum);
+  if (diff < 0.01) {
+    return { text: `鎶曟爣鎬婚噾棰� 楼${stored.toLocaleString()} 涓庢姤浠峰悎璁′竴鑷碻, tone: 'success' };
+  }
+  return {
+    text: `鎶曟爣鎬婚噾棰� 楼${stored.toLocaleString()} 涓庢姤浠峰悎璁� 楼${sum.toLocaleString()} 涓嶄竴鑷达紙宸 楼${diff.toLocaleString()}锛夈�傚闇�鏁村崟璁╁埄/璋冧环锛岀洿鎺ュ湪鎶曟爣鎬婚噾棰濆淇敼骞朵繚瀛樸�俙,
+    tone: 'warning',
+  };
+});
 
 // ========== 寮�鏍� ==========
 const bidOpen = ref<any>(null);
@@ -363,6 +403,7 @@
 const typeLabel = computed(() => getLabel(TENDER_TYPE_CELLTAG.labels, project.value?.tenderType));
 const isDraft = computed(() => project.value?.tenderStatus === TENDER_STATUS.DRAFT);
 const isPublished = computed(() => project.value?.tenderStatus === TENDER_STATUS.PUBLISHED);
+const isBidding = computed(() => project.value?.tenderStatus === TENDER_STATUS.BIDDING);
 const isPublishedOrBidding = computed(() => {
   const s = project.value?.tenderStatus;
   return s === TENDER_STATUS.PUBLISHED || s === TENDER_STATUS.BIDDING;
@@ -438,7 +479,7 @@
 const bidColumns = [
   { title: '鎶曟爣缂栧彿', dataIndex: 'bidNo', width: 140 },
   { title: '渚涘簲鍟�', dataIndex: 'supplierName', width: 200 },
-  { title: '鎶曟爣閲戦', dataIndex: 'bidTotalAmount', width: 120 },
+  { title: '鎶曟爣鎬婚噾棰�', dataIndex: 'bidTotalAmount', width: 120 },
   { title: '鎶曟爣鏃堕棿', dataIndex: 'bidTime', width: 160 },
   { title: '鐘舵��', dataIndex: 'bidStatus', width: 100, customRender: ({ text }: any) => BID_STATUS_MAP[text] || text },
   { title: '鎿嶄綔', key: 'actions', width: 240 },
@@ -446,7 +487,9 @@
 
 const quoteColumns = [
   { title: '鐗╂枡', dataIndex: 'materialName', width: 120 },
-  { title: '鎶ヤ环浠锋牸', dataIndex: 'quotePrice', width: 100 },
+  { title: '鎶ヤ环鍗曚环', dataIndex: 'quotePrice', width: 100 },
+  { title: '鏁伴噺', dataIndex: 'quantity', width: 70 },
+  { title: '鎶ヤ环閲戦', dataIndex: 'quoteAmount', width: 110 },
   { title: '绋庣巼(%)', dataIndex: 'taxRate', width: 80 },
   { title: '浜ゆ湡(澶�)', dataIndex: 'deliveryCycle', width: 80 },
   { title: '浠樻鏉′欢', dataIndex: 'paymentTerms', width: 120 },
@@ -537,14 +580,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>
@@ -594,7 +655,7 @@
               </template>
               <div class="flex items-center justify-between mb-3">
                 <span class="text-xs text-gray-400">鍏� <b class="text-gray-600">{{ bidCount }}</b> 涓姇鏍�</span>
-                <a-button v-if="isPublishedOrBidding || isDraft || isPriceComparing" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: undefined!, bidNo: '' }; bidFormVisible = true">
+                <a-button v-if="isBidding || isDraft || isPriceComparing" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: undefined!, bidNo: '' }; bidFormVisible = true">
                   <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                   鏂板鎶曟爣
                 </a-button>
@@ -637,7 +698,29 @@
                   <span class="text-gray-400">渚涘簲鍟嗭細<b class="text-gray-700">{{ selectedBid.supplierName }}</b></span>
                   <a-button size="small" type="link" class="ml-auto" @click="activeTab = 'bid'">鍒囨崲鎶曟爣</a-button>
                 </div>
-                <div class="text-right mb-3">
+                <!-- 鎶曟爣鎬婚噾棰濓紙鏁村崟鎬讳环锛夛細鍙笌鎶ヤ环鍚堣鑷姩甯﹀嚭骞朵汉宸ヨ皟鏁� -->
+                <div v-if="(isPublishedOrBidding || isDraft || isPriceComparing) && selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" class="flex flex-wrap items-center gap-x-3 gap-y-2 mb-2 px-3 py-2 bg-blue-50/70 rounded text-xs">
+                  <span class="text-gray-600 font-medium">鎶曟爣鎬婚噾棰�</span>
+                  <a-input-number v-model:value="bidTotalDraft" :min="0" :precision="2" class="!w-40" placeholder="鏁村崟鎬讳环">
+                    <template #addonAfter>鍏�</template>
+                  </a-input-number>
+                  <a-button size="small" type="link" :disabled="selectedQuoteTotalAmount <= 0" @click="bidTotalDraft = selectedQuoteTotalAmount">
+                    鑷姩甯﹀嚭鎶ヤ环鍚堣 楼{{ selectedQuoteTotalAmount.toLocaleString() }}
+                  </a-button>
+                  <a-button size="small" type="primary" @click="handleSaveBidTotal">淇濆瓨鎬讳环</a-button>
+                </div>
+                <a-alert
+                  v-if="bidTotalCompareHint"
+                  :type="bidTotalCompareHint.tone"
+                  :show-icon="true"
+                  class="mb-3"
+                  :message="bidTotalCompareHint.text"
+                />
+                <div class="flex items-center justify-between mb-3">
+                  <span class="text-xs text-gray-400">
+                    宸叉姤浠� <b class="text-gray-600">{{ quoteList.length }}</b> / <b class="text-gray-600">{{ materialList.length }}</b> 椤圭墿鏂欙紝鎶ヤ环鍚堣
+                    <b class="text-blue-500">{{ selectedQuoteTotalAmount.toLocaleString() }}</b> 鍏�
+                  </span>
                   <a-button v-if="(isPublishedOrBidding || isDraft || isPriceComparing) && selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
                     <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
                     鏂板鎶ヤ环
@@ -647,6 +730,12 @@
                   <template #bodyCell="{ column, record }">
                     <template v-if="column.dataIndex === 'quotePrice'">
                       <span class="font-medium text-blue-600">{{ record.quotePrice?.toLocaleString() }}</span>
+                    </template>
+                    <template v-if="column.dataIndex === 'quantity'">
+                      {{ materialQuantityOf(record) || '-' }}
+                    </template>
+                    <template v-if="column.dataIndex === 'quoteAmount'">
+                      <span class="font-medium text-blue-600">{{ quoteAmountOf(record).toLocaleString() }}</span>
                     </template>
                     <template v-if="column.dataIndex === 'taxRate'">
                       {{ record.taxRate != null ? `${record.taxRate}%` : '-' }}
@@ -873,7 +962,9 @@
             </a-select>
           </a-form-item>
           <a-form-item label="鎶曟爣鎬婚噾棰�">
-            <a-input-number v-model:value="bidForm.bidTotalAmount" :min="0" class="!w-full" placeholder="鎶曟爣鎬婚噾棰�"><template #addonAfter>鍏�</template></a-input-number>
+            <div class="text-xs text-gray-400 leading-5">
+              鍙殏涓嶅~锛氭姤浠锋槑缁嗗綍瀹屽悗锛屽湪銆屾姤浠风鐞嗐�嶉〉鎸夋姤浠峰悎璁¤嚜鍔ㄥ甫鍑恒�佸啀浜哄伐寰皟淇濆瓨銆�
+            </div>
           </a-form-item>
         </a-form>
       </a-modal>
@@ -888,7 +979,7 @@
           </a-form-item>
           <a-row :gutter="12">
             <a-col :span="8">
-              <a-form-item label="鎶ヤ环浠锋牸" required><a-input-number v-model:value="quoteForm.quotePrice" :min="0" class="!w-full" placeholder="鎶ヤ环"><template #addonAfter>鍏�</template></a-input-number></a-form-item>
+              <a-form-item label="鎶ヤ环鍗曚环" required><a-input-number v-model:value="quoteForm.quotePrice" :min="0" class="!w-full" placeholder="鎶ヤ环"><template #addonAfter>鍏�</template></a-input-number></a-form-item>
             </a-col>
             <a-col :span="8">
               <a-form-item label="绋庣巼(%)"><a-input-number v-model:value="quoteForm.taxRate" :min="0" :max="100" class="!w-full" placeholder="濡� 13" /></a-form-item>
@@ -998,4 +1089,4 @@
       </a-modal>
     </template>
   </Page>
-</template>
+</template>
\ No newline at end of file

--
Gitblit v1.9.3