<script lang="ts" setup>
|
import type { SrmTenderApi } from '#/api/srm/tender';
|
|
import { computed, onMounted, ref } from 'vue';
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { Page } from '@vben/common-ui';
|
|
import { message } from 'ant-design-vue';
|
import { IconifyIcon } from '@vben/icons';
|
|
import {
|
getTenderProject,
|
getMaterialList, createMaterial, updateMaterial, deleteMaterial,
|
getBidListByProject, createBid, withdrawBid,
|
getQuoteList, createQuote, updateQuote,
|
createBidOpen, getBidOpen,
|
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';
|
import type { MdmItemApi } from '#/api/mdm/item';
|
import {
|
TENDER_STATUS,
|
TENDER_STATUS_CELLTAG,
|
TENDER_STATUS_COLORS,
|
TENDER_TYPE_CELLTAG,
|
BID_STATUS,
|
BID_STATUS_MAP,
|
AWARD_STATUS,
|
AWARD_STATUS_MAP,
|
getLabel,
|
} from '../enums';
|
|
defineOptions({ name: 'SrmTenderDetail' });
|
|
const route = useRoute();
|
const router = useRouter();
|
const projectId = Number(route.query.id);
|
|
const project = ref<SrmTenderApi.TenderProjectVO>();
|
const activeTab = ref('info');
|
|
// ========== 下拉选项数据 ==========
|
const mdmItemList = ref<MdmItemApi.Item[]>([]);
|
const supplierList = ref<{ id: number; name: string }[]>([]);
|
|
onMounted(async () => {
|
const [items, suppliers] = await Promise.all([
|
getItemSimpleList(),
|
getSupplierSimpleList(),
|
]);
|
mdmItemList.value = items;
|
supplierList.value = suppliers;
|
});
|
|
const filterOption = (input: string, option: any) =>
|
option.label?.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
|
// ========== 基本信息 ==========
|
const materialList = ref<SrmTenderApi.TenderMaterialVO[]>([]);
|
const materialLoading = ref(false);
|
const materialFormVisible = ref(false);
|
const materialForm = ref<SrmTenderApi.TenderMaterialVO>({ tenderProjectId: projectId });
|
|
async function loadProject() {
|
project.value = await getTenderProject(projectId);
|
}
|
async function loadMaterials() {
|
materialLoading.value = true;
|
try { materialList.value = await getMaterialList(projectId); } finally { materialLoading.value = false; }
|
}
|
async function handleSaveMaterial() {
|
if (materialForm.value.id) {
|
await updateMaterial(materialForm.value);
|
} else {
|
await createMaterial(materialForm.value);
|
}
|
materialFormVisible.value = false;
|
loadMaterials();
|
}
|
function onMaterialSelect(itemId: number) {
|
const item = mdmItemList.value.find((i) => i.id === itemId);
|
if (item) {
|
materialForm.value.productCode = item.code;
|
materialForm.value.productName = item.name;
|
materialForm.value.productSpec = item.specification;
|
materialForm.value.unit = item.unitMeasureName;
|
}
|
}
|
|
function handleAddMaterial() {
|
materialForm.value = { tenderProjectId: projectId };
|
materialFormVisible.value = true;
|
}
|
function handleEditMaterial(row: SrmTenderApi.TenderMaterialVO) {
|
materialForm.value = { ...row };
|
materialFormVisible.value = true;
|
}
|
async function handleDeleteMaterial(id: number) {
|
await deleteMaterial(id);
|
loadMaterials();
|
}
|
|
// ========== 投标管理 ==========
|
const bidList = ref<SrmTenderApi.TenderBidVO[]>([]);
|
const bidLoading = ref(false);
|
const bidFormVisible = ref(false);
|
const bidForm = ref<SrmTenderApi.TenderBidVO>({ tenderProjectId: projectId, supplierId: 0, bidNo: '' });
|
|
async function loadBids() {
|
bidLoading.value = true;
|
try { bidList.value = await getBidListByProject(projectId); } finally { bidLoading.value = false; }
|
}
|
async function handleCreateBid() {
|
await createBid(bidForm.value);
|
bidFormVisible.value = false;
|
loadBids();
|
}
|
async function handleWithdrawBid(id: number) {
|
await withdrawBid(id);
|
loadBids();
|
}
|
|
// ========== 报价管理 ==========
|
const quoteList = ref<SrmTenderApi.QuoteVO[]>([]);
|
const quoteLoading = ref(false);
|
const quoteFormVisible = ref(false);
|
const quoteForm = ref<SrmTenderApi.QuoteVO>({ bidId: 0, tenderMaterialId: 0 });
|
const selectedBidId = ref<number>();
|
|
const availableMaterials = computed(() => {
|
const quotedIds = new Set(
|
quoteList.value
|
.filter((q) => q.tenderMaterialId !== undefined)
|
.map((q) => q.tenderMaterialId),
|
);
|
// 编辑时允许保留当前物料
|
if (quoteForm.value.id && quoteForm.value.tenderMaterialId) {
|
quotedIds.delete(quoteForm.value.tenderMaterialId);
|
}
|
return materialList.value.filter((m) => !quotedIds.has(m.id));
|
});
|
|
async function loadQuotes(bidId?: number) {
|
if (!bidId) return;
|
selectedBidId.value = bidId;
|
quoteLoading.value = true;
|
try { quoteList.value = await getQuoteList(bidId); } finally { quoteLoading.value = false; }
|
}
|
async function handleSaveQuote() {
|
if (quoteForm.value.id) {
|
await updateQuote(quoteForm.value);
|
} else {
|
await createQuote(quoteForm.value);
|
}
|
quoteFormVisible.value = false;
|
loadQuotes(selectedBidId.value);
|
}
|
function handleAddQuote() {
|
quoteForm.value = { bidId: selectedBidId.value!, tenderMaterialId: 0 };
|
quoteFormVisible.value = true;
|
}
|
function handleEditQuote(row: SrmTenderApi.QuoteVO) {
|
quoteForm.value = { ...row };
|
quoteFormVisible.value = true;
|
}
|
|
// ========== 开标 ==========
|
const bidOpen = ref<any>(null);
|
|
async function loadBidOpen() {
|
try {
|
bidOpen.value = null;
|
} catch { /* 暂无开标记录 */ }
|
}
|
async function handleCreateBidOpen() {
|
await createBidOpen(projectId);
|
message.success('开标成功');
|
loadBidOpen();
|
loadProject();
|
}
|
|
// ========== 评标管理 ==========
|
const evaluationList = ref<SrmTenderApi.BidEvaluationVO[]>([]);
|
const evalLoading = ref(false);
|
const evalFormVisible = ref(false);
|
const evalForm = ref<SrmTenderApi.BidEvaluationVO>({ tenderProjectId: projectId, supplierId: 0, bidId: 0 });
|
|
async function loadEvaluations() {
|
evalLoading.value = true;
|
try { evaluationList.value = await getEvaluationList(projectId); } finally { evalLoading.value = false; }
|
}
|
async function handleCreateEvaluation() {
|
if (evalForm.value.id) {
|
await updateBidEvaluation(evalForm.value);
|
} else {
|
await createBidEvaluation(evalForm.value);
|
}
|
evalFormVisible.value = false;
|
loadEvaluations();
|
loadProject();
|
}
|
async function handleCalculateRanking() {
|
await calculateRanking(projectId);
|
message.success('排名计算完成');
|
loadEvaluations();
|
}
|
|
// ========== 定标 ==========
|
const award = ref<SrmTenderApi.BidAwardVO>();
|
const awardFormVisible = ref(false);
|
const awardForm = ref<SrmTenderApi.BidAwardVO>({ tenderProjectId: projectId, supplierId: 0, bidId: 0, awardNo: '' });
|
|
async function loadAward() {
|
try { award.value = await getAwardByProject(projectId); } catch { award.value = undefined; }
|
}
|
async function handleCreateAward() {
|
await createAward(awardForm.value);
|
awardFormVisible.value = false;
|
loadAward();
|
loadProject();
|
}
|
async function handleApproveAward() {
|
if (!award.value?.id) return;
|
await approveAward(award.value.id);
|
message.success('定标审批通过');
|
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);
|
message.success('采购订单已生成');
|
loadAward();
|
loadProject();
|
}
|
|
// ========== 初始化 ==========
|
loadProject();
|
loadMaterials();
|
loadBids();
|
loadEvaluations();
|
loadAward();
|
|
// ========== 状态判断 ==========
|
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 stepItems = [
|
{ title: '草稿', status: TENDER_STATUS.DRAFT },
|
{ title: '发布', status: TENDER_STATUS.PUBLISHED },
|
{ title: '投标中', status: TENDER_STATUS.BIDDING },
|
{ title: '评标中', status: TENDER_STATUS.EVALUATING },
|
{ title: '已定标', status: TENDER_STATUS.AWARDED },
|
];
|
const currentStep = computed(() => {
|
const s = project.value?.tenderStatus ?? 0;
|
if (s === TENDER_STATUS.CLOSED) return 4;
|
const idx = stepItems.findIndex((item) => item.status >= s);
|
return idx === -1 ? 0 : idx;
|
});
|
const stepStatus = computed(() => {
|
const s = project.value?.tenderStatus;
|
if (s === TENDER_STATUS.CLOSED) return 'error';
|
if (s === TENDER_STATUS.AWARDED) return 'finish';
|
return 'process';
|
});
|
|
const materialTotalAmount = computed(() =>
|
materialList.value.reduce((sum, m) => sum + (m.estimatedPrice ?? 0) * (m.quantity ?? 0), 0),
|
);
|
const bidCount = computed(() => bidList.value.length);
|
const bidTotalAmount = computed(() =>
|
bidList.value.reduce((sum, b) => sum + (b.bidTotalAmount ?? 0), 0),
|
);
|
const evalCount = computed(() => evaluationList.value.length);
|
const rankedCount = computed(() => evaluationList.value.filter((e) => e.rank != null).length);
|
|
const selectedBid = computed(() =>
|
bidList.value.find((b) => b.id === selectedBidId.value),
|
);
|
|
const materialColumns = [
|
{ title: '物料编码', dataIndex: 'productCode', width: 120 },
|
{ title: '物料名称', dataIndex: 'productName', width: 150 },
|
{ title: '规格型号', dataIndex: 'productSpec', width: 120 },
|
{ title: '单位', dataIndex: 'unit', width: 80 },
|
{ title: '数量', dataIndex: 'quantity', width: 80 },
|
{ title: '技术要求', dataIndex: 'techRequirement', width: 150 },
|
{ title: '预估单价', dataIndex: 'estimatedPrice', width: 100 },
|
{ title: '操作', key: 'actions', width: 150 },
|
];
|
|
const bidColumns = [
|
{ title: '投标编号', dataIndex: 'bidNo', width: 140 },
|
{ title: '供应商', dataIndex: 'supplierName', width: 200 },
|
{ 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: 180 },
|
];
|
|
const quoteColumns = [
|
{ title: '物料', dataIndex: 'materialName', width: 120 },
|
{ title: '报价价格', dataIndex: 'quotePrice', width: 100 },
|
{ title: '税率(%)', dataIndex: 'taxRate', width: 80 },
|
{ title: '交期(天)', dataIndex: 'deliveryCycle', width: 80 },
|
{ title: '付款条件', dataIndex: 'paymentTerms', width: 120 },
|
{ title: '质保期(月)', dataIndex: 'warrantyPeriod', width: 80 },
|
{ title: '备注', dataIndex: 'remark', width: 150 },
|
{ title: '操作', key: 'actions', width: 120 },
|
];
|
|
const evalColumns = [
|
{ title: '投标ID', dataIndex: 'bidId', width: 80 },
|
{ title: '供应商', dataIndex: 'supplierName', width: 150 },
|
{ title: '价格评分', dataIndex: 'priceScore', width: 100 },
|
{ title: '技术评分', dataIndex: 'techScore', width: 100 },
|
{ title: '交付评分', dataIndex: 'deliveryScore', width: 100 },
|
{ title: '服务评分', dataIndex: 'serviceScore', width: 100 },
|
{ title: '综合评分', dataIndex: 'compositeScore', width: 100 },
|
{ title: '排名', dataIndex: 'rank', width: 80 },
|
{ title: '评委', dataIndex: 'evaluatorName', width: 100 },
|
{ title: '评语', dataIndex: 'evaluationOpinion', width: 150 },
|
{ title: '操作', key: 'actions', width: 120 },
|
];
|
</script>
|
|
<template>
|
<Page auto-content-height>
|
<!-- 加载中 -->
|
<a-skeleton v-if="!project" active class="p-4" />
|
<template v-else>
|
<!-- 顶部标题栏 -->
|
<div class="flex items-center gap-2 px-4 pt-3 pb-2">
|
<a-button type="text" size="small" @click="router.back()">
|
<template #icon><IconifyIcon icon="ant-design:arrow-left-outlined" /></template>
|
</a-button>
|
<span class="text-lg font-bold truncate max-w-md">{{ project.tenderName }}</span>
|
<span class="text-gray-400 text-xs">{{ project.tenderNo }}</span>
|
<a-tag :color="statusColor">{{ statusLabel }}</a-tag>
|
<a-tag color="blue">{{ typeLabel }}</a-tag>
|
<div class="flex-1" />
|
<a-button size="small" @click="loadProject(); loadMaterials(); loadBids(); loadEvaluations(); loadAward();">
|
<template #icon><IconifyIcon icon="ant-design:reload-outlined" /></template>
|
刷新
|
</a-button>
|
</div>
|
|
<!-- 工作流进度 + 关键信息 -->
|
<div class="px-4 mb-3">
|
<a-card size="small">
|
<!-- 流程步骤 -->
|
<a-steps :current="currentStep" :status="stepStatus" size="small" class="mb-3">
|
<a-step v-for="item in stepItems" :key="item.status" :title="item.title" />
|
</a-steps>
|
|
<!-- 关键字段 -->
|
<div class="grid grid-cols-6 gap-3 text-xs">
|
<div>
|
<div class="text-gray-400 mb-0.5">预算金额</div>
|
<div class="font-semibold text-blue-500">{{ project.budgetAmount?.toLocaleString() ?? '-' }} 元</div>
|
</div>
|
<div>
|
<div class="text-gray-400 mb-0.5">负责人</div>
|
<div class="font-semibold">{{ project.projectLeaderName || '-' }}</div>
|
</div>
|
<div>
|
<div class="text-gray-400 mb-0.5">采购组织</div>
|
<div class="font-semibold">{{ project.purchaseOrg || '-' }}</div>
|
</div>
|
<div>
|
<div class="text-gray-400 mb-0.5">开标时间</div>
|
<div class="font-semibold">{{ project.bidOpenTime || '待定' }}</div>
|
</div>
|
<div>
|
<div class="text-gray-400 mb-0.5">开始时间</div>
|
<div class="font-semibold">{{ project.startTime || '-' }}</div>
|
</div>
|
<div>
|
<div class="text-gray-400 mb-0.5">截止时间</div>
|
<div class="font-semibold">{{ project.endTime || '-' }}</div>
|
</div>
|
</div>
|
|
<!-- 招标说明 / 资质要求 -->
|
<template v-if="project.tenderDesc || project.qualificationRequirements">
|
<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>
|
</a-collapse>
|
</template>
|
</a-card>
|
</div>
|
|
<!-- Tab 页签 -->
|
<div class="px-4 pb-4">
|
<a-card size="small">
|
<a-tabs v-model:activeKey="activeTab" size="small">
|
<!-- Tab1: 物料清单 -->
|
<a-tab-pane key="info">
|
<template #tab>
|
<a-space :size="4"><IconifyIcon icon="ant-design:unordered-list-outlined" />物料清单</a-space>
|
</template>
|
<div class="flex items-center justify-between mb-3">
|
<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">
|
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
|
添加物料
|
</a-button>
|
</div>
|
<a-table :columns="materialColumns" :data-source="materialList" :loading="materialLoading" :pagination="false" row-key="id" size="small">
|
<template #bodyCell="{ column, record }">
|
<template v-if="column.dataIndex === 'estimatedPrice'">
|
{{ record.estimatedPrice ? record.estimatedPrice.toLocaleString() : '-' }}
|
</template>
|
<template v-if="column.key === 'actions'">
|
<a-space>
|
<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>
|
</a-popconfirm>
|
</a-space>
|
</template>
|
</template>
|
</a-table>
|
<a-empty v-if="!materialLoading && materialList.length === 0" description="暂无物料" class="mt-8" />
|
</a-tab-pane>
|
|
<!-- Tab2: 投标管理 -->
|
<a-tab-pane key="bid">
|
<template #tab>
|
<a-space :size="4"><IconifyIcon icon="ant-design:team-outlined" />投标管理</a-space>
|
</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">
|
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
|
新增投标
|
</a-button>
|
</div>
|
<a-table :columns="bidColumns" :data-source="bidList" :loading="bidLoading" :pagination="false" row-key="id" size="small">
|
<template #bodyCell="{ column, record }">
|
<template v-if="column.dataIndex === 'bidTotalAmount'">
|
<span class="font-medium">{{ record.bidTotalAmount?.toLocaleString() }}</span>
|
</template>
|
<template v-if="column.dataIndex === 'bidStatus'">
|
<a-tag :color="record.bidStatus === BID_STATUS.WIN ? 'green' : record.bidStatus === BID_STATUS.LOSE ? 'red' : 'default'">
|
{{ BID_STATUS_MAP[record.bidStatus] }}
|
</a-tag>
|
</template>
|
<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-button size="small" type="link" danger><IconifyIcon icon="ant-design:close-circle-outlined" />撤标</a-button>
|
</a-popconfirm>
|
</a-space>
|
</template>
|
</template>
|
</a-table>
|
<a-empty v-if="!bidLoading && bidList.length === 0" description="暂无投标记录" class="mt-8" />
|
</a-tab-pane>
|
|
<!-- Tab3: 报价管理 -->
|
<a-tab-pane key="quote">
|
<template #tab>
|
<a-space :size="4"><IconifyIcon icon="ant-design:dollar-outlined" />报价管理</a-space>
|
</template>
|
<template v-if="selectedBidId && selectedBid">
|
<div class="flex items-center gap-3 mb-3 px-3 py-2 bg-gray-50 rounded text-xs">
|
<span class="text-gray-400">投标:<b class="text-gray-700">{{ selectedBid.bidNo }}</b></span>
|
<a-divider type="vertical" />
|
<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">
|
<a-button v-if="selectedBid.bidStatus !== BID_STATUS.WITHDRAWN" type="primary" size="small" @click="handleAddQuote">
|
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
|
新增报价
|
</a-button>
|
</div>
|
<a-table :columns="quoteColumns" :data-source="quoteList" :loading="quoteLoading" :pagination="false" row-key="id" size="small">
|
<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 === 'taxRate'">
|
{{ record.taxRate != null ? `${record.taxRate}%` : '-' }}
|
</template>
|
<template v-if="column.dataIndex === 'deliveryCycle'">
|
{{ record.deliveryCycle != null ? `${record.deliveryCycle} 天` : '-' }}
|
</template>
|
<template v-if="column.dataIndex === 'warrantyPeriod'">
|
{{ 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>
|
</template>
|
</template>
|
</a-table>
|
<a-empty v-if="!quoteLoading && quoteList.length === 0" description="暂无报价明细" class="mt-8" />
|
</template>
|
<a-empty v-else description="请先在「投标管理」中选择一个投标查看报价">
|
<a-button type="primary" @click="activeTab = 'bid'">前往投标管理</a-button>
|
</a-empty>
|
</a-tab-pane>
|
|
<!-- Tab4: 开标 -->
|
<a-tab-pane key="bidOpen">
|
<template #tab>
|
<a-space :size="4"><IconifyIcon icon="ant-design:unlock-outlined" />开标</a-space>
|
</template>
|
<template v-if="bidOpen">
|
<a-result status="success" title="已开标">
|
<template #sub-title>
|
<a-descriptions :column="2" size="small" class="mt-4">
|
<a-descriptions-item label="开标编号">{{ bidOpen.id }}</a-descriptions-item>
|
<a-descriptions-item label="开标时间">{{ bidOpen.openTime }}</a-descriptions-item>
|
</a-descriptions>
|
</template>
|
</a-result>
|
</template>
|
<template v-else>
|
<div class="text-center py-8">
|
<a-result status="info" title="尚未开标" sub-title="执行开标后项目进入投标阶段,供应商可正式投标" />
|
<br />
|
<a-button type="primary" @click="handleCreateBidOpen">
|
<template #icon><IconifyIcon icon="ant-design:thunderbolt-outlined" /></template>
|
执行开标
|
</a-button>
|
</div>
|
</template>
|
</a-tab-pane>
|
|
<!-- Tab5: 评标 -->
|
<a-tab-pane key="evaluation">
|
<template #tab>
|
<a-space :size="4"><IconifyIcon icon="ant-design:trophy-outlined" />评标</a-space>
|
</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-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">
|
<template #icon><IconifyIcon icon="ant-design:plus-outlined" /></template>
|
新增评标
|
</a-button>
|
</a-space>
|
</div>
|
<a-table :columns="evalColumns" :data-source="evaluationList" :loading="evalLoading" :pagination="false" row-key="id" size="small">
|
<template #bodyCell="{ column, record }">
|
<template v-if="['priceScore', 'techScore', 'deliveryScore', 'serviceScore'].includes(column.dataIndex)">
|
<div class="flex items-center gap-2">
|
<a-progress :percent="record[column.dataIndex] || 0" :show-info="false" size="small"
|
:stroke-color="record[column.dataIndex] >= 80 ? '#52c41a' : record[column.dataIndex] >= 60 ? '#faad14' : '#ff4d4f'"
|
class="flex-1" style="min-width: 50px;" />
|
<span class="text-xs w-8 text-right">{{ record[column.dataIndex] ?? '-' }}</span>
|
</div>
|
</template>
|
<template v-if="column.dataIndex === 'compositeScore'">
|
<span class="font-bold" :class="record.compositeScore >= 80 ? 'text-green-500' : record.compositeScore >= 60 ? 'text-orange-500' : 'text-red-500'">
|
{{ record.compositeScore ?? '-' }}
|
</span>
|
</template>
|
<template v-if="column.dataIndex === 'rank'">
|
<a-tag v-if="record.rank === 1" color="gold"><IconifyIcon icon="ant-design:trophy-outlined" /> 第1名</a-tag>
|
<a-tag v-else-if="record.rank" color="default">第{{ record.rank }}名</a-tag>
|
<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>
|
</template>
|
</template>
|
</a-table>
|
<a-empty v-if="!evalLoading && evaluationList.length === 0" description="暂无评标记录" class="mt-8" />
|
</a-tab-pane>
|
|
<!-- Tab6: 定标 -->
|
<a-tab-pane key="award">
|
<template #tab>
|
<a-space :size="4"><IconifyIcon icon="ant-design:check-circle-outlined" />定标</a-space>
|
</template>
|
<template v-if="award">
|
<a-row :gutter="16">
|
<a-col :span="16">
|
<a-descriptions :column="2" bordered size="small">
|
<a-descriptions-item label="定标编号">
|
<span class="font-mono">{{ award.awardNo }}</span>
|
</a-descriptions-item>
|
<a-descriptions-item label="中标供应商">
|
<a-tag color="blue">{{ award.supplierName || award.supplierId }}</a-tag>
|
</a-descriptions-item>
|
<a-descriptions-item label="定标金额">
|
<span class="font-bold text-blue-600">{{ award.awardAmount?.toLocaleString() }}</span> 元
|
</a-descriptions-item>
|
<a-descriptions-item label="定标时间">{{ award.awardTime || '-' }}</a-descriptions-item>
|
<a-descriptions-item label="状态">
|
<a-tag :color="award.awardStatus === AWARD_STATUS.AWARDED ? 'green' : 'orange'">{{ AWARD_STATUS_MAP[award.awardStatus] }}</a-tag>
|
</a-descriptions-item>
|
<a-descriptions-item label="采购订单">{{ award.purchaseOrderId || '待生成' }}</a-descriptions-item>
|
<a-descriptions-item label="备注" :span="2">{{ award.remark || '-' }}</a-descriptions-item>
|
</a-descriptions>
|
</a-col>
|
<a-col :span="8" class="flex items-center justify-center">
|
<template v-if="award.awardStatus === AWARD_STATUS.ORDER_GENERATED">
|
<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 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">
|
<template #icon><IconifyIcon icon="ant-design:shopping-cart-outlined" /></template>
|
生成采购订单
|
</a-button>
|
<a-popconfirm 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;">
|
<template #icon><IconifyIcon icon="ant-design:check-outlined" /></template>
|
审批通过
|
</a-button>
|
<a-popconfirm 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>
|
</a-row>
|
</template>
|
<template v-else>
|
<div class="text-center py-8">
|
<a-result status="info" title="尚未定标" sub-title="基于评标排名选择中标供应商并创建定标" />
|
<br />
|
<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>
|
</div>
|
</template>
|
</a-tab-pane>
|
</a-tabs>
|
</a-card>
|
</div>
|
|
<!-- ========== 弹窗 ========== -->
|
|
<!-- 物料表单 -->
|
<a-modal v-model:open="materialFormVisible" :title="materialForm.id ? '编辑物料' : '添加物料'" :width="560" @ok="handleSaveMaterial">
|
<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}`">
|
<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>
|
</div>
|
</a-select-option>
|
</a-select>
|
</a-form-item>
|
<a-row :gutter="12">
|
<a-col :span="12">
|
<a-form-item label="物料名称" required><a-input v-model:value="materialForm.productName" placeholder="自动带入" /></a-form-item>
|
</a-col>
|
<a-col :span="12">
|
<a-form-item label="规格型号"><a-input v-model:value="materialForm.productSpec" placeholder="自动带入" /></a-form-item>
|
</a-col>
|
</a-row>
|
<a-row :gutter="12">
|
<a-col :span="8">
|
<a-form-item label="单位"><a-input v-model:value="materialForm.unit" placeholder="自动带入" /></a-form-item>
|
</a-col>
|
<a-col :span="8">
|
<a-form-item label="数量" required><a-input-number v-model:value="materialForm.quantity" :min="0" class="!w-full" placeholder="数量" /></a-form-item>
|
</a-col>
|
<a-col :span="8">
|
<a-form-item label="预估单价"><a-input-number v-model:value="materialForm.estimatedPrice" :min="0" class="!w-full" placeholder="单价" /></a-form-item>
|
</a-col>
|
</a-row>
|
<a-form-item label="技术要求">
|
<a-textarea v-model:value="materialForm.techRequirement" placeholder="如:符合GB/T标准" :rows="2" />
|
</a-form-item>
|
</a-form>
|
</a-modal>
|
|
<!-- 投标表单 -->
|
<a-modal v-model:open="bidFormVisible" title="新增投标" :width="460" @ok="handleCreateBid">
|
<a-form layout="vertical" class="mt-4">
|
<a-form-item label="投标编号" required><a-input v-model:value="bidForm.bidNo" placeholder="如 BID2024010001" /></a-form-item>
|
<a-form-item label="供应商" required>
|
<a-select v-model:value="bidForm.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>
|
</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>
|
</a-form-item>
|
</a-form>
|
</a-modal>
|
|
<!-- 报价表单 -->
|
<a-modal v-model:open="quoteFormVisible" :title="quoteForm.id ? '编辑报价' : '新增报价'" :width="560" @ok="handleSaveQuote">
|
<a-form layout="vertical" class="mt-4">
|
<a-form-item label="招标物料" required>
|
<a-select v-model:value="quoteForm.tenderMaterialId" show-search placeholder="搜索并选择招标物料" :filter-option="filterOption">
|
<a-select-option v-for="m in availableMaterials" :key="m.id" :value="m.id" :label="m.productName">{{ m.productCode }} - {{ m.productName }}</a-select-option>
|
</a-select>
|
</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-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>
|
</a-col>
|
<a-col :span="8">
|
<a-form-item label="交期(天)"><a-input-number v-model:value="quoteForm.deliveryCycle" :min="0" class="!w-full" placeholder="天数" /></a-form-item>
|
</a-col>
|
</a-row>
|
<a-row :gutter="12">
|
<a-col :span="12">
|
<a-form-item label="付款条件"><a-input v-model:value="quoteForm.paymentTerms" placeholder="如:货到30天付款" /></a-form-item>
|
</a-col>
|
<a-col :span="12">
|
<a-form-item label="质保期(月)"><a-input-number v-model:value="quoteForm.warrantyPeriod" :min="0" class="!w-full" placeholder="月数" /></a-form-item>
|
</a-col>
|
</a-row>
|
<a-form-item label="备注"><a-textarea v-model:value="quoteForm.remark" placeholder="如含运费" :rows="2" /></a-form-item>
|
</a-form>
|
</a-modal>
|
|
<!-- 评标表单 -->
|
<a-modal v-model:open="evalFormVisible" :title="evalForm.id ? '编辑评标' : '新增评标'" :width="600" @ok="handleCreateEvaluation">
|
<a-form layout="vertical" class="mt-4">
|
<a-row :gutter="12">
|
<a-col :span="12">
|
<a-form-item label="投标" required>
|
<a-select v-model:value="evalForm.bidId" show-search placeholder="选择投标" :filter-option="filterOption"
|
@change="(val: number) => { const b = bidList.find(i => i.id === val); if (b) { evalForm.supplierId = b.supplierId; evalForm.supplierName = b.supplierName; } }">
|
<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>
|
</a-form-item>
|
</a-col>
|
<a-col :span="12">
|
<a-form-item label="供应商">
|
<a-select v-model:value="evalForm.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>
|
</a-form-item>
|
</a-col>
|
</a-row>
|
<a-divider class="!my-2">评分项(百分制)</a-divider>
|
<a-row :gutter="12">
|
<a-col :span="12"><a-form-item label="价格评分"><a-input-number v-model:value="evalForm.priceScore" :min="0" :max="100" class="!w-full" placeholder="0-100" /></a-form-item></a-col>
|
<a-col :span="12"><a-form-item label="技术评分"><a-input-number v-model:value="evalForm.techScore" :min="0" :max="100" class="!w-full" placeholder="0-100" /></a-form-item></a-col>
|
<a-col :span="12"><a-form-item label="交付评分"><a-input-number v-model:value="evalForm.deliveryScore" :min="0" :max="100" class="!w-full" placeholder="0-100" /></a-form-item></a-col>
|
<a-col :span="12"><a-form-item label="服务评分"><a-input-number v-model:value="evalForm.serviceScore" :min="0" :max="100" class="!w-full" placeholder="0-100" /></a-form-item></a-col>
|
</a-row>
|
<a-divider class="!my-2" />
|
<a-row :gutter="12">
|
<a-col :span="12"><a-form-item label="评委"><a-input v-model:value="evalForm.evaluatorName" placeholder="评委姓名" /></a-form-item></a-col>
|
</a-row>
|
<a-form-item label="评标意见"><a-textarea v-model:value="evalForm.evaluationOpinion" placeholder="供应商的技术方案、交付能力等综合评价" :rows="3" /></a-form-item>
|
</a-form>
|
</a-modal>
|
|
<!-- 定标表单 -->
|
<a-modal v-model:open="awardFormVisible" title="创建定标" :width="500" @ok="handleCreateAward">
|
<a-form layout="vertical" class="mt-4">
|
<a-form-item label="定标编号" required><a-input v-model:value="awardForm.awardNo" placeholder="如 AWARD2024010001" /></a-form-item>
|
<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>
|
</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>
|
</a-form-item>
|
</a-col>
|
</a-row>
|
<a-form-item label="定标金额" required>
|
<a-input-number v-model:value="awardForm.awardAmount" :min="0" class="!w-full" placeholder="定标金额"><template #addonAfter>元</template></a-input-number>
|
</a-form-item>
|
<a-form-item label="备注"><a-textarea v-model:value="awardForm.remark" placeholder="如:综合评标第一名" :rows="2" /></a-form-item>
|
</a-form>
|
</a-modal>
|
</template>
|
</Page>
|
</template>
|