From d1cf2e1b5d3f56d14e5d75024ce2d054f9142b2e Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 06 八月 2026 17:54:54 +0800
Subject: [PATCH] refactor(components): 重构多个组件以提升代码质量和可维护性
---
src/views/srm/tender/detail.vue | 198 ++++++++++++++++++++++++++++++++++++++-----------
1 files changed, 152 insertions(+), 46 deletions(-)
diff --git a/src/views/srm/tender/detail.vue b/src/views/srm/tender/detail.vue
index 020af3c..4dfbeda 100644
--- a/src/views/srm/tender/detail.vue
+++ b/src/views/srm/tender/detail.vue
@@ -6,7 +6,7 @@
import { Page } from '@vben/common-ui';
-import { message } from 'ant-design-vue';
+import { message, Modal } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
import {
@@ -15,8 +15,8 @@
getBidListByProject, createBid, withdrawBid,
getQuoteList, createQuote, updateQuote,
createBidOpen, getBidOpen,
- getEvaluationList, createBidEvaluation, calculateRanking,
- createAward, getAward, getAwardByProject, approveAward, generatePurchaseOrder,
+ getEvaluationList, createBidEvaluation, updateBidEvaluation, calculateRanking,
+ createAward, deleteAward, getAward, getAwardByProject, approveAward, generatePurchaseOrder,
} from '#/api/srm/tender';
import { getSupplierSimpleList } from '#/api/srm/supplier';
import { getItemSimpleList } from '#/api/mdm/item';
@@ -64,6 +64,25 @@
const materialFormVisible = ref(false);
const materialForm = ref<SrmTenderApi.TenderMaterialVO>({ tenderProjectId: projectId });
+// 宸叉坊鍔犵殑鐗╂枡浜у搧ID闆嗗悎锛岄伩鍏嶉噸澶嶆坊鍔�
+const addedProductIds = computed(() => {
+ const ids = new Set(
+ materialList.value
+ .filter((m) => m.productId !== undefined)
+ .map((m) => m.productId!),
+ );
+ // 缂栬緫鏃跺厑璁镐繚鐣欏綋鍓嶇墿鏂欑殑 productId
+ if (materialForm.value.id && materialForm.value.productId) {
+ ids.delete(materialForm.value.productId);
+ }
+ return ids;
+});
+
+// 鐗╂枡涓嬫媺鍙�夊垪琛紙鎺掗櫎宸叉坊鍔犵殑锛�
+const availableMdmItems = computed(() =>
+ mdmItemList.value.filter((item) => !addedProductIds.value.has(item.id!)),
+);
+
async function loadProject() {
project.value = await getTenderProject(projectId);
}
@@ -107,7 +126,7 @@
const bidList = ref<SrmTenderApi.TenderBidVO[]>([]);
const bidLoading = ref(false);
const bidFormVisible = ref(false);
-const bidForm = ref<SrmTenderApi.TenderBidVO>({ tenderProjectId: projectId, supplierId: 0, bidNo: '' });
+const bidForm = ref<SrmTenderApi.TenderBidVO>({ tenderProjectId: projectId, supplierId: undefined!, bidNo: '' });
async function loadBids() {
bidLoading.value = true;
@@ -127,7 +146,7 @@
const quoteList = ref<SrmTenderApi.QuoteVO[]>([]);
const quoteLoading = ref(false);
const quoteFormVisible = ref(false);
-const quoteForm = ref<SrmTenderApi.QuoteVO>({ bidId: 0, tenderMaterialId: 0 });
+const quoteForm = ref<SrmTenderApi.QuoteVO>({ bidId: undefined!, tenderMaterialId: undefined! });
const selectedBidId = ref<number>();
const availableMaterials = computed(() => {
@@ -150,6 +169,27 @@
try { quoteList.value = await getQuoteList(bidId); } finally { quoteLoading.value = false; }
}
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 {
@@ -159,7 +199,7 @@
loadQuotes(selectedBidId.value);
}
function handleAddQuote() {
- quoteForm.value = { bidId: selectedBidId.value!, tenderMaterialId: 0 };
+ quoteForm.value = { bidId: selectedBidId.value!, tenderMaterialId: undefined! };
quoteFormVisible.value = true;
}
function handleEditQuote(row: SrmTenderApi.QuoteVO) {
@@ -186,14 +226,18 @@
const evaluationList = ref<SrmTenderApi.BidEvaluationVO[]>([]);
const evalLoading = ref(false);
const evalFormVisible = ref(false);
-const evalForm = ref<SrmTenderApi.BidEvaluationVO>({ tenderProjectId: projectId, supplierId: 0, bidId: 0 });
+const evalForm = ref<SrmTenderApi.BidEvaluationVO>({ tenderProjectId: projectId, supplierId: undefined!, bidId: undefined! });
async function loadEvaluations() {
evalLoading.value = true;
try { evaluationList.value = await getEvaluationList(projectId); } finally { evalLoading.value = false; }
}
async function handleCreateEvaluation() {
- await createBidEvaluation(evalForm.value);
+ if (evalForm.value.id) {
+ await updateBidEvaluation(evalForm.value);
+ } else {
+ await createBidEvaluation(evalForm.value);
+ }
evalFormVisible.value = false;
loadEvaluations();
loadProject();
@@ -207,7 +251,41 @@
// ========== 瀹氭爣 ==========
const award = ref<SrmTenderApi.BidAwardVO>();
const awardFormVisible = ref(false);
-const awardForm = ref<SrmTenderApi.BidAwardVO>({ tenderProjectId: projectId, supplierId: 0, bidId: 0, awardNo: '' });
+const awardForm = ref<SrmTenderApi.BidAwardVO>({ tenderProjectId: projectId, supplierId: undefined!, bidId: undefined!, awardNo: '' });
+
+/** 宸叉姇鏍囦笖鏈挙鏍囩殑渚涘簲鍟嗗垪琛紙浠庢姇鏍囪褰曚腑鎻愬彇锛� */
+const bidSuppliers = computed(() => {
+ const activeBids = bidList.value.filter((b) => b.bidStatus !== BID_STATUS.WITHDRAWN);
+ const seen = new Map<number, { id: number; name: string }>();
+ activeBids.forEach((b) => {
+ if (b.supplierId && !seen.has(b.supplierId)) {
+ seen.set(b.supplierId, { id: b.supplierId, name: b.supplierName || '' });
+ }
+ });
+ return [...seen.values()];
+});
+
+/** 鍙�夋姇鏍囪褰曪細鎺掗櫎鎾ゆ爣锛屼笖濡傛灉閫変簡渚涘簲鍟嗗垯鍙樉绀鸿渚涘簲鍟嗙殑鎶曟爣 */
+const activeBids = computed(() => {
+ return bidList.value.filter((b) => {
+ if (b.bidStatus === BID_STATUS.WITHDRAWN) return false;
+ if (awardForm.value.supplierId && b.supplierId !== awardForm.value.supplierId) return false;
+ return true;
+ });
+});
+
+/** 閫夋嫨渚涘簲鍟嗘椂娓呯┖宸查�夌殑鎶曟爣 */
+function onAwardSupplierChange() {
+ awardForm.value.bidId = undefined!;
+}
+
+/** 閫夋嫨鎶曟爣鏃惰嚜鍔ㄥ甫鍏ヨ鎶曟爣鐨勬姤浠烽噾棰� */
+function onAwardBidChange(bidId: number) {
+ const bid = bidList.value.find((b) => b.id === bidId);
+ if (bid?.bidTotalAmount != null) {
+ awardForm.value.awardAmount = bid.bidTotalAmount;
+ }
+}
async function loadAward() {
try { award.value = await getAwardByProject(projectId); } catch { award.value = undefined; }
@@ -225,6 +303,13 @@
loadAward();
loadProject();
}
+async function handleDeleteAward() {
+ if (!award.value?.id) return;
+ await deleteAward(award.value.id);
+ message.success('瀹氭爣宸插垹闄�');
+ award.value = undefined;
+}
+
async function handleGeneratePurchaseOrder() {
if (!award.value?.id) return;
await generatePurchaseOrder(award.value.id);
@@ -244,6 +329,20 @@
const statusLabel = computed(() => getLabel(TENDER_STATUS_CELLTAG.labels, project.value?.tenderStatus));
const statusColor = computed(() => TENDER_STATUS_COLORS[project.value?.tenderStatus!] || 'default');
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 isPublishedOrBidding = computed(() => {
+ const s = project.value?.tenderStatus;
+ return s === TENDER_STATUS.PUBLISHED || s === TENDER_STATUS.BIDDING;
+});
+const isBiddingOrEvaluating = computed(() => {
+ const s = project.value?.tenderStatus;
+ return s === TENDER_STATUS.BIDDING || s === TENDER_STATUS.EVALUATING;
+});
+const isEvaluating = computed(() => project.value?.tenderStatus === TENDER_STATUS.EVALUATING);
+const canDeleteAward = computed(() => award.value?.awardStatus === AWARD_STATUS.PENDING_APPROVAL);
+const canApproveAward = computed(() => award.value?.awardStatus === AWARD_STATUS.PENDING_APPROVAL);
+const canGeneratePO = computed(() => award.value?.awardStatus === AWARD_STATUS.AWARDED);
const stepItems = [
{ title: '鑽夌', status: TENDER_STATUS.DRAFT },
@@ -410,7 +509,7 @@
<a-space>
<span class="text-xs text-gray-400">鍏� <b class="text-gray-600">{{ materialList.length }}</b> 椤癸紝棰勪及鎬婚噾棰� <b class="text-blue-500">{{ materialTotalAmount.toLocaleString() }}</b> 鍏�</span>
</a-space>
- <a-button type="primary" size="small" @click="handleAddMaterial">
+ <a-button v-if="isDraft" type="primary" size="small" @click="handleAddMaterial">
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
娣诲姞鐗╂枡
</a-button>
@@ -421,7 +520,7 @@
{{ record.estimatedPrice ? record.estimatedPrice.toLocaleString() : '-' }}
</template>
<template v-if="column.key === 'actions'">
- <a-space>
+ <a-space v-if="isDraft">
<a-button size="small" type="link" @click="handleEditMaterial(record)"><IconifyIcon icon="ant-design:edit-outlined" />缂栬緫</a-button>
<a-popconfirm title="纭鍒犻櫎璇ョ墿鏂欙紵" @confirm="handleDeleteMaterial(record.id)">
<a-button size="small" type="link" danger><IconifyIcon icon="ant-design:delete-outlined" />鍒犻櫎</a-button>
@@ -440,7 +539,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 type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: 0, bidNo: '' }; bidFormVisible = true">
+ <a-button v-if="isPublishedOrBidding" type="primary" size="small" @click="bidForm = { tenderProjectId: projectId, supplierId: undefined!, bidNo: '' }; bidFormVisible = true">
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
鏂板鎶曟爣
</a-button>
@@ -458,7 +557,7 @@
<template v-if="column.key === 'actions'">
<a-space>
<a-button size="small" type="link" @click="loadQuotes(record.id); activeTab = 'quote'"><IconifyIcon icon="ant-design:dollar-outlined" />鎶ヤ环</a-button>
- <a-popconfirm v-if="record.bidStatus === BID_STATUS.REGISTERED" title="纭鎾ゆ爣锛�" @confirm="handleWithdrawBid(record.id)">
+ <a-popconfirm v-if="isPublishedOrBidding && record.bidStatus === BID_STATUS.REGISTERED" title="纭鎾ゆ爣锛�" @confirm="handleWithdrawBid(record.id)">
<a-button size="small" type="link" danger><IconifyIcon icon="ant-design:close-circle-outlined" />鎾ゆ爣</a-button>
</a-popconfirm>
</a-space>
@@ -481,7 +580,7 @@
<a-button size="small" type="link" class="ml-auto" @click="activeTab = 'bid'">鍒囨崲鎶曟爣</a-button>
</div>
<div class="text-right mb-3">
- <a-button v-if="selectedBid.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
+ <a-button v-if="isPublishedOrBidding && selectedBid.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
鏂板鎶ヤ环
</a-button>
@@ -501,7 +600,7 @@
{{ record.warrantyPeriod != null ? `${record.warrantyPeriod} 涓湀` : '-' }}
</template>
<template v-if="column.key === 'actions'">
- <a-button v-if="selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" size="small" type="link" @click="handleEditQuote(record)"><IconifyIcon icon="ant-design:edit-outlined" />缂栬緫</a-button>
+ <a-button v-if="isPublishedOrBidding && selectedBid?.bidStatus !== BID_STATUS.WITHDRAWN" size="small" type="link" @click="handleEditQuote(record)"><IconifyIcon icon="ant-design:edit-outlined" />缂栬緫</a-button>
</template>
</template>
</a-table>
@@ -529,14 +628,12 @@
</template>
<template v-else>
<div class="text-center py-8">
- <a-result status="info" title="灏氭湭寮�鏍�" sub-title="鎵ц寮�鏍囧悗椤圭洰杩涘叆鎶曟爣闃舵锛屼緵搴斿晢鍙寮忔姇鏍�">
- <template #extra>
- <a-button type="primary" @click="handleCreateBidOpen">
- <template #icon><IconifyIcon icon="ant-design:thunderbolt-outlined" /></template>
- 鎵ц寮�鏍�
- </a-button>
- </template>
- </a-result>
+ <a-result status="info" title="灏氭湭寮�鏍�" sub-title="鎵ц寮�鏍囧悗椤圭洰杩涘叆鎶曟爣闃舵锛屼緵搴斿晢鍙寮忔姇鏍�" />
+ <br />
+ <a-button v-if="isPublished" type="primary" @click="handleCreateBidOpen">
+ <template #icon><IconifyIcon icon="ant-design:thunderbolt-outlined" /></template>
+ 鎵ц寮�鏍�
+ </a-button>
</div>
</template>
</a-tab-pane>
@@ -548,12 +645,12 @@
</template>
<div class="flex items-center justify-between mb-3">
<span class="text-xs text-gray-400">鍏� <b class="text-gray-600">{{ evalCount }}</b> 鏉★紝宸叉帓鍚� <b class="text-green-500">{{ rankedCount }}</b> 鏉�</span>
- <a-space>
+ <a-space v-if="isBiddingOrEvaluating">
<a-button size="small" @click="handleCalculateRanking">
<template #icon><IconifyIcon icon="ant-design:bar-chart-outlined" /></template>
璁$畻鎺掑悕
</a-button>
- <a-button type="primary" size="small" @click="evalForm = { tenderProjectId: projectId, supplierId: 0, bidId: 0 }; evalFormVisible = true">
+ <a-button type="primary" size="small" @click="evalForm = { tenderProjectId: projectId, supplierId: undefined!, bidId: undefined! }; evalFormVisible = true">
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
鏂板璇勬爣
</a-button>
@@ -580,7 +677,7 @@
<span v-else class="text-gray-300">-</span>
</template>
<template v-if="column.key === 'actions'">
- <a-button size="small" type="link" @click="evalForm = { ...record }; evalFormVisible = true"><IconifyIcon icon="ant-design:edit-outlined" />缂栬緫</a-button>
+ <a-button v-if="isBiddingOrEvaluating" size="small" type="link" @click="evalForm = { ...record }; evalFormVisible = true"><IconifyIcon icon="ant-design:edit-outlined" />缂栬緫</a-button>
</template>
</template>
</a-table>
@@ -615,28 +712,39 @@
</a-col>
<a-col :span="8" class="flex items-center justify-center">
<template v-if="award.awardStatus === AWARD_STATUS.ORDER_GENERATED">
- <a-result status="success" title="璁㈠崟宸茬敓鎴�" class="p-0">
- <template #sub-title>
- <span class="text-sm">閲囪喘璁㈠崟 ID: {{ award.purchaseOrderId }}</span>
- </template>
- </a-result>
+ <div class="text-center">
+ <a-result status="success" title="璁㈠崟宸茬敓鎴�" class="p-0">
+ <template #sub-title>
+ <span class="text-sm">閲囪喘璁㈠崟 ID: {{ award.purchaseOrderId }}</span>
+ </template>
+ </a-result>
+ <a-popconfirm v-if="canDeleteAward" title="纭畾鍒犻櫎璇ュ畾鏍囪褰曪紵" ok-text="纭畾" cancel-text="鍙栨秷" @confirm="handleDeleteAward">
+ <a-button type="link" danger size="small" class="mt-2">鍒犻櫎瀹氭爣</a-button>
+ </a-popconfirm>
+ </div>
</template>
<template v-else-if="award.awardStatus === AWARD_STATUS.AWARDED">
<div class="text-center">
<a-result status="success" title="瀹氭爣瀹屾垚" class="p-0 mb-3" />
- <a-button type="primary" block @click="handleGeneratePurchaseOrder">
+ <a-button v-if="canGeneratePO" type="primary" block @click="handleGeneratePurchaseOrder">
<template #icon><IconifyIcon icon="ant-design:shopping-cart-outlined" /></template>
鐢熸垚閲囪喘璁㈠崟
</a-button>
+ <a-popconfirm v-if="canDeleteAward" title="纭畾鍒犻櫎璇ュ畾鏍囪褰曪紵" ok-text="纭畾" cancel-text="鍙栨秷" @confirm="handleDeleteAward">
+ <a-button type="link" danger size="small" class="mt-2">鍒犻櫎瀹氭爣</a-button>
+ </a-popconfirm>
</div>
</template>
<template v-else>
<div class="text-center">
<div class="text-gray-500 text-sm mb-3">瀹氭爣淇℃伅宸插垱寤猴紝璇峰鎵�</div>
- <a-button type="primary" block @click="handleApproveAward" style="background: #52c41a; border-color: #52c41a;">
+ <a-button v-if="canApproveAward" type="primary" block @click="handleApproveAward" style="background: #52c41a; border-color: #52c41a;">
<template #icon><IconifyIcon icon="ant-design:check-outlined" /></template>
瀹℃壒閫氳繃
</a-button>
+ <a-popconfirm v-if="canDeleteAward" title="纭畾鍒犻櫎璇ュ畾鏍囪褰曪紵" ok-text="纭畾" cancel-text="鍙栨秷" @confirm="handleDeleteAward">
+ <a-button type="link" danger size="small" class="mt-2">鍒犻櫎瀹氭爣</a-button>
+ </a-popconfirm>
</div>
</template>
</a-col>
@@ -644,14 +752,12 @@
</template>
<template v-else>
<div class="text-center py-8">
- <a-result status="info" title="灏氭湭瀹氭爣" sub-title="鍩轰簬璇勬爣鎺掑悕閫夋嫨涓爣渚涘簲鍟嗗苟鍒涘缓瀹氭爣">
- <template #extra>
- <a-button type="primary" @click="awardForm = { tenderProjectId: projectId, supplierId: 0, bidId: 0, awardNo: '' }; awardFormVisible = true">
- <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
- 鍒涘缓瀹氭爣
- </a-button>
- </template>
- </a-result>
+ <a-result status="info" title="灏氭湭瀹氭爣" sub-title="鍩轰簬璇勬爣鎺掑悕閫夋嫨涓爣渚涘簲鍟嗗苟鍒涘缓瀹氭爣" />
+ <br />
+ <a-button v-if="isEvaluating" type="primary" @click="awardForm = { tenderProjectId: projectId, supplierId: undefined!, bidId: undefined!, awardNo: '' }; awardFormVisible = true">
+ <template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
+ 鍒涘缓瀹氭爣
+ </a-button>
</div>
</template>
</a-tab-pane>
@@ -666,7 +772,7 @@
<a-form layout="vertical" class="mt-4">
<a-form-item label="MDM鐗╂枡" required>
<a-select v-model:value="materialForm.productId" show-search placeholder="鎼滅储骞堕�夋嫨MDM鐗╂枡" :filter-option="filterOption" option-label-prop="label" @change="onMaterialSelect">
- <a-select-option v-for="item in mdmItemList" :key="item.id" :value="item.id" :label="`${item.code} ${item.name}`">
+ <a-select-option v-for="item in availableMdmItems" :key="item.id" :value="item.id" :label="`${item.code} ${item.name}`">
<div class="flex flex-col">
<span class="font-medium">{{ item.code }}</span>
<span class="text-xs text-gray-400">{{ item.name }}{{ item.specification ? ` 路 ${item.specification}` : '' }}</span>
@@ -787,15 +893,15 @@
<a-row :gutter="12">
<a-col :span="12">
<a-form-item label="渚涘簲鍟�" required>
- <a-select v-model:value="awardForm.supplierId" show-search placeholder="閫夋嫨渚涘簲鍟�" :filter-option="filterOption">
- <a-select-option v-for="s in supplierList" :key="s.id" :value="s.id" :label="s.name">{{ s.name }}</a-select-option>
+ <a-select v-model:value="awardForm.supplierId" show-search placeholder="閫夋嫨渚涘簲鍟�" :filter-option="filterOption" @change="onAwardSupplierChange">
+ <a-select-option v-for="s in bidSuppliers" :key="s.id" :value="s.id" :label="s.name">{{ s.name }}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="鎶曟爣" required>
- <a-select v-model:value="awardForm.bidId" show-search placeholder="閫夋嫨鎶曟爣" :filter-option="filterOption">
- <a-select-option v-for="b in bidList" :key="b.id" :value="b.id" :label="`${b.bidNo} - ${b.supplierName}`">{{ b.bidNo }} - {{ b.supplierName }}</a-select-option>
+ <a-select v-model:value="awardForm.bidId" show-search placeholder="閫夋嫨鎶曟爣" :filter-option="filterOption" :disabled="!awardForm.supplierId" @change="onAwardBidChange">
+ <a-select-option v-for="b in activeBids" :key="b.id" :value="b.id" :label="`${b.bidNo} - ${b.supplierName}`">{{ b.bidNo }} - {{ b.supplierName }}</a-select-option>
</a-select>
</a-form-item>
</a-col>
--
Gitblit v1.9.3