增加招投标模块
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<div class="tender-announce" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">招标公告</h3>
|
||||
<el-button type="primary" :icon="Plus" :disabled="disabled" @click="openCreate">
|
||||
新建公告
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="list.length === 0" description="暂无公告" :image-size="80" />
|
||||
|
||||
<el-table v-else :data="list" stripe border>
|
||||
<el-table-column label="公告标题" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :underline="false" @click="openPreview(row)">
|
||||
{{ row.title }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ announceTypeText(row.announce_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.publish_status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.publish_status === 1 ? "已发布" : "草稿" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发布时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.publish_time) || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="浏览" prop="view_count" width="80" align="center" />
|
||||
<el-table-column label="创建人" prop="create_user_name" width="100" align="center" />
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.publish_status !== 1"
|
||||
link
|
||||
type="success"
|
||||
size="small"
|
||||
:disabled="disabled"
|
||||
@click="handlePublish(row)"
|
||||
>
|
||||
发布
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="openPreview(row)">预览</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="disabled || row.publish_status === 1"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 新建公告 -->
|
||||
<el-dialog
|
||||
v-model="createVisible"
|
||||
title="新建招标公告"
|
||||
width="760px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="公告标题" required>
|
||||
<el-input v-model="form.title" placeholder="请输入公告标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公告类型">
|
||||
<el-select v-model="form.announce_type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in tenderAnnounceTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="公告内容">
|
||||
<el-input
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="支持纯文本,可粘贴公告正文"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 公告预览 -->
|
||||
<el-dialog v-model="previewVisible" :title="previewRow?.title || '公告预览'" width="760px">
|
||||
<div class="preview-meta">
|
||||
<el-tag size="small">{{ announceTypeText(previewRow?.announce_type) }}</el-tag>
|
||||
<span>{{ formatDateTime(previewRow?.publish_time) || "未发布" }}</span>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="preview-content">{{ previewRow?.content || "(空)" }}</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getTenderAnnounceList,
|
||||
createTenderAnnounce,
|
||||
publishTenderAnnounce,
|
||||
deleteTenderAnnounce,
|
||||
} from "@/api/crmBidding";
|
||||
import { tenderAnnounceTypeOptions, formatDateTime } from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
tenderId: { type: [Number, String], required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(["count", "published"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
|
||||
const createVisible = ref(false);
|
||||
const previewVisible = ref(false);
|
||||
const previewRow = ref(null);
|
||||
const submitting = ref(false);
|
||||
|
||||
const form = reactive({ title: "", announce_type: "1", content: "" });
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.tenderId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenderAnnounceList({ tender_id: props.tenderId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
form.title = "";
|
||||
form.announce_type = "1";
|
||||
form.content = "";
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.title.trim()) {
|
||||
ElMessage.warning("请输入公告标题");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await createTenderAnnounce({
|
||||
tender_id: props.tenderId,
|
||||
title: form.title,
|
||||
announce_type: form.announce_type,
|
||||
content: form.content,
|
||||
});
|
||||
ElMessage.success("已创建");
|
||||
createVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "创建失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublish(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`发布后招标状态将转为「公告中」,确认发布「${row.title}」?`,
|
||||
"发布确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await publishTenderAnnounce(row.id);
|
||||
ElMessage.success("已发布");
|
||||
loadList();
|
||||
emit("published");
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "发布失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除公告「${row.title}」?`, "提示", { type: "warning" });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteTenderAnnounce(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
function openPreview(row) {
|
||||
previewRow.value = row;
|
||||
previewVisible.value = true;
|
||||
}
|
||||
|
||||
function announceTypeText(t) {
|
||||
const hit = tenderAnnounceTypeOptions.find((i) => String(i.value) === String(t));
|
||||
return hit ? hit.label : t || "-";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.preview-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
min-height: 120px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="tender-basic">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">基本信息</h3>
|
||||
<div class="section-actions">
|
||||
<el-button
|
||||
v-if="canEdit"
|
||||
type="primary"
|
||||
link
|
||||
:icon="EditPen"
|
||||
@click="emit('action', { type: 'edit', row: tender })"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canAward"
|
||||
type="success"
|
||||
link
|
||||
:icon="Trophy"
|
||||
@click="emit('action', { type: 'award', row: tender })"
|
||||
>
|
||||
定标
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canToContract"
|
||||
type="warning"
|
||||
link
|
||||
:icon="Document"
|
||||
@click="emit('action', { type: 'contract', row: tender })"
|
||||
>
|
||||
转合同
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="招标编号">{{ tender.tender_no || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="招标标题">{{ tender.title || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="招标类型">
|
||||
<el-tag size="small" :type="tenderTypeTag(tender.tender_type)">
|
||||
{{ tenderTypeText(tender.tender_type) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="招标状态">
|
||||
<el-tag size="small" :type="tenderStatusTag(tender.status)">
|
||||
{{ tenderStatusText(tender.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="评标方法">
|
||||
{{ evaluationMethodText(tender.evaluation_method) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">{{ tender.owner_user_name || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="招标方">{{ tender.customer_name || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发布单位">{{ tender.publish_org || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预算金额">
|
||||
{{ formatMoney(tender.budget_amount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="投标保证金">
|
||||
{{ formatMoney(tender.bid_deposit) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="公告发布日期">
|
||||
{{ formatDateOnly(tender.publish_date) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="报名起止">
|
||||
{{ formatBidTime(tender.register_start_date) || "-" }} ~
|
||||
{{ formatBidTime(tender.register_end_date) || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="投标起止">
|
||||
{{ formatBidTime(tender.bid_start_date) || "-" }} ~
|
||||
{{ formatBidTime(tender.bid_end_date) || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="开标时间">
|
||||
{{ formatBidTime(tender.open_bid_date) || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ tender.contact_person || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ tender.contact_phone || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系邮箱">{{ tender.contact_email || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="投标地点">{{ tender.bid_address || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开标地点">{{ tender.open_address || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中标单位">
|
||||
<el-tag v-if="tender.winner_supplier_name" type="success" size="small">
|
||||
{{ tender.winner_supplier_name }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="中标金额">
|
||||
{{ formatMoney(tender.winner_amount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="定标日期">
|
||||
{{ formatBidTime(tender.winner_date) || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="关联合同">
|
||||
<el-link v-if="tender.contract_id" type="primary" :underline="false">
|
||||
合同 #{{ tender.contract_id }}
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatDateTime(tender.create_time) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4 class="block-title">项目描述</h4>
|
||||
<div class="content-box">{{ tender.description || "暂无描述" }}</div>
|
||||
|
||||
<h4 class="block-title">资质要求</h4>
|
||||
<div class="content-box">{{ tender.requirement || "—" }}</div>
|
||||
|
||||
<h4 class="block-title">备注</h4>
|
||||
<div class="content-box">{{ tender.remark || "—" }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { EditPen, Trophy, Document } from "@element-plus/icons-vue";
|
||||
import {
|
||||
tenderStatusText,
|
||||
tenderStatusTag,
|
||||
tenderTypeText,
|
||||
tenderTypeTag,
|
||||
evaluationMethodText,
|
||||
formatMoney,
|
||||
formatDateOnly,
|
||||
formatDateTime,
|
||||
formatBidTime,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
tender: { type: Object, required: true },
|
||||
});
|
||||
const emit = defineEmits(["action"]);
|
||||
|
||||
const canEdit = computed(() => props.tender.locked !== 1);
|
||||
// 已开标(4)之后才允许定标
|
||||
const canAward = computed(() => {
|
||||
const s = Number(props.tender.status);
|
||||
return props.tender.locked !== 1 && s >= 4 && s < 6;
|
||||
});
|
||||
const canToContract = computed(
|
||||
() => props.tender.status === 6 && props.tender.winner_supplier_name && !props.tender.contract_id
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.block-title {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.content-box {
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-primary);
|
||||
min-height: 40px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div class="tender-evaluation" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">开标评标</h3>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Setting" @click="weightVisible = true">权重设置</el-button>
|
||||
<el-button type="primary" :icon="Plus" :disabled="disabled" @click="openCreate">
|
||||
新增评分
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="weightSum > 0 && Math.abs(weightSum - 100) > 0.01"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="`当前权重合计 ${weightSum.toFixed(2)}%,不等于 100%,请检查权重设置`"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
|
||||
<el-empty v-if="list.length === 0" description="暂无评分记录" :image-size="80" />
|
||||
|
||||
<el-table v-else :data="list" stripe border>
|
||||
<el-table-column label="排名" width="70" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.ranking === 1" type="success" size="small">1</el-tag>
|
||||
<span v-else>{{ row.ranking || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="投标方" min-width="160" show-overflow-tooltip prop="supplier_name" />
|
||||
<el-table-column label="技术分" width="90" align="center" prop="technical_score" />
|
||||
<el-table-column label="商务分" width="90" align="center" prop="business_score" />
|
||||
<el-table-column label="价格分" width="90" align="center" prop="price_score" />
|
||||
<el-table-column label="服务分" width="90" align="center" prop="service_score" />
|
||||
<el-table-column label="总分" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<strong>{{ Number(row.total_score || 0).toFixed(2) }}</strong>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结论" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="resultTag(row.result)" size="small">{{ resultText(row.result) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="评标人" prop="evaluation_user_name" width="100" align="center" />
|
||||
<el-table-column label="评标时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.evaluation_time) || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="评价" prop="comment" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="disabled"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 新增评分 -->
|
||||
<el-dialog v-model="dialogVisible" title="新增评标打分" width="620px" :close-on-click-modal="false">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="投标方" required>
|
||||
<el-select
|
||||
v-model="form.supplier_name"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入投标方"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="name in supplierNames"
|
||||
:key="name"
|
||||
:label="name"
|
||||
:value="name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="技术分">
|
||||
<el-input-number v-model="form.technical_score" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="商务分">
|
||||
<el-input-number v-model="form.business_score" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="价格分">
|
||||
<el-input-number v-model="form.price_score" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="服务分">
|
||||
<el-input-number v-model="form.service_score" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="折算总分">
|
||||
<el-input :model-value="previewTotal" readonly>
|
||||
<template #append>分</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="结论">
|
||||
<el-select v-model="form.result" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in evaluationResultOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="评价意见">
|
||||
<el-input v-model="form.comment" type="textarea" :rows="3" placeholder="评标意见(选填)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 权重设置 -->
|
||||
<el-dialog v-model="weightVisible" title="评标权重设置" width="460px" :close-on-click-modal="false">
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="权重为百分比数值,建议四项合计 100。留空或合计为 0 时,总分按四项直接求和。"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
<el-form label-width="110px">
|
||||
<el-form-item label="技术权重(%)">
|
||||
<el-input-number v-model="weights.technical" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="商务权重(%)">
|
||||
<el-input-number v-model="weights.business" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="价格权重(%)">
|
||||
<el-input-number v-model="weights.price" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务权重(%)">
|
||||
<el-input-number v-model="weights.service" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合计">
|
||||
<el-input :model-value="weightSum.toFixed(2)" readonly>
|
||||
<template #append>%</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="weightVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Setting } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getTenderEvaluationList,
|
||||
createTenderEvaluation,
|
||||
deleteTenderEvaluation,
|
||||
getTenderSupplierList,
|
||||
} from "@/api/crmBidding";
|
||||
import { evaluationResultOptions, formatDateTime } from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
tenderId: { type: [Number, String], required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(["count"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const supplierNames = ref([]);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const weightVisible = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
supplier_name: "",
|
||||
technical_score: 0,
|
||||
business_score: 0,
|
||||
price_score: 0,
|
||||
service_score: 0,
|
||||
result: 0,
|
||||
comment: "",
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
const weights = reactive({ technical: 25, business: 25, price: 30, service: 20 });
|
||||
|
||||
const weightSum = computed(
|
||||
() =>
|
||||
Number(weights.technical || 0) +
|
||||
Number(weights.business || 0) +
|
||||
Number(weights.price || 0) +
|
||||
Number(weights.service || 0)
|
||||
);
|
||||
|
||||
const previewTotal = computed(() => {
|
||||
const s = weightSum.value;
|
||||
if (s <= 0) {
|
||||
return (
|
||||
Number(form.technical_score || 0) +
|
||||
Number(form.business_score || 0) +
|
||||
Number(form.price_score || 0) +
|
||||
Number(form.service_score || 0)
|
||||
).toFixed(2);
|
||||
}
|
||||
return (
|
||||
(Number(form.technical_score || 0) * Number(weights.technical || 0)) / 100 +
|
||||
(Number(form.business_score || 0) * Number(weights.business || 0)) / 100 +
|
||||
(Number(form.price_score || 0) * Number(weights.price || 0)) / 100 +
|
||||
(Number(form.service_score || 0) * Number(weights.service || 0)) / 100
|
||||
).toFixed(2);
|
||||
});
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(() => {
|
||||
loadList();
|
||||
loadSuppliers();
|
||||
});
|
||||
|
||||
async function loadList() {
|
||||
if (!props.tenderId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenderEvaluationList({ tender_id: props.tenderId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSuppliers() {
|
||||
if (!props.tenderId) return;
|
||||
try {
|
||||
const res = await getTenderSupplierList({ tender_id: props.tenderId });
|
||||
supplierNames.value = (res?.data?.list || [])
|
||||
.map((i) => i.supplier_name)
|
||||
.filter(Boolean);
|
||||
} catch {
|
||||
supplierNames.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.supplier_name.trim()) {
|
||||
ElMessage.warning("请选择或输入投标方");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await createTenderEvaluation({
|
||||
tender_id: props.tenderId,
|
||||
supplier_name: form.supplier_name,
|
||||
technical_score: form.technical_score,
|
||||
business_score: form.business_score,
|
||||
price_score: form.price_score,
|
||||
service_score: form.service_score,
|
||||
weight_config: JSON.stringify(weights),
|
||||
comment: form.comment,
|
||||
result: form.result,
|
||||
});
|
||||
ElMessage.success("评分已保存,排名已自动重排");
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除「${row.supplier_name}」的评分记录?`, "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteTenderEvaluation(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
function resultText(v) {
|
||||
const hit = evaluationResultOptions.find((i) => Number(i.value) === Number(v));
|
||||
return hit ? hit.label : "-";
|
||||
}
|
||||
|
||||
function resultTag(v) {
|
||||
const map = { 0: "info", 1: "success", 2: "danger", 3: "warning" };
|
||||
return map[Number(v)] || "info";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="tender-file-list" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">招标文件</h3>
|
||||
<div class="header-actions">
|
||||
<el-select v-model="uploadType" size="small" style="width: 130px">
|
||||
<el-option
|
||||
v-for="opt in TENDER_FILE_TYPE_OPTIONS"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-upload
|
||||
:http-request="handleUpload"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeUpload"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<el-button type="primary" :icon="Plus" :disabled="disabled">上传文件</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="list.length === 0" description="暂无招标文件" :image-size="80" />
|
||||
|
||||
<el-table v-else :data="list" stripe border>
|
||||
<el-table-column label="文件名" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span style="margin-left: 8px">{{ row.file_name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ fileTypeText(row.file_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="大小" width="100" align="center">
|
||||
<template #default="{ row }">{{ formatFileSize(row.file_size) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上传人" prop="upload_user_name" width="100" align="center" />
|
||||
<el-table-column label="备注" prop="remark" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="上传时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="handleDownload(row)">下载</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="disabled"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Document } from "@element-plus/icons-vue";
|
||||
import { getTenderFileList, createTenderFile, deleteTenderFile } from "@/api/crmBidding";
|
||||
import { uploadFile } from "@/api/file";
|
||||
import { formatDateTime } from "@/views/apps/crm/dict";
|
||||
|
||||
// 招标文件类型:1招标文件 2技术规格书 3工程量清单 4合同模板 5答疑澄清 6其他
|
||||
const TENDER_FILE_TYPE_OPTIONS = [
|
||||
{ label: "招标文件", value: "1" },
|
||||
{ label: "技术规格书", value: "2" },
|
||||
{ label: "工程量清单", value: "3" },
|
||||
{ label: "合同模板", value: "4" },
|
||||
{ label: "答疑澄清", value: "5" },
|
||||
{ label: "其他", value: "6" },
|
||||
];
|
||||
|
||||
const props = defineProps({
|
||||
tenderId: { type: [Number, String], required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(["count"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const uploadType = ref("1");
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.tenderId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenderFileList({ tender_id: props.tenderId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function beforeUpload(file) {
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
ElMessage.warning("文件大小不能超过 100MB");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleUpload(option) {
|
||||
const file = option.file;
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
try {
|
||||
const upRes = await uploadFile(fd);
|
||||
// 秒传命中时返回 code=201,同样携带 url/id/name
|
||||
if (upRes?.code !== 200 && upRes?.code !== 201) {
|
||||
throw new Error(upRes?.msg || "上传失败");
|
||||
}
|
||||
const data = upRes?.data || {};
|
||||
const name = data.name || file.name;
|
||||
await createTenderFile({
|
||||
tender_id: props.tenderId,
|
||||
file_name: name,
|
||||
file_url: data.url || "",
|
||||
file_size: file.size || 0,
|
||||
file_ext: name.split(".").pop() || "",
|
||||
file_type: uploadType.value,
|
||||
});
|
||||
ElMessage.success("上传成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除文件「${row.file_name}」?`, "提示", { type: "warning" });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteTenderFile(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownload(row) {
|
||||
if (!row.file_url) {
|
||||
ElMessage.warning("文件 URL 为空");
|
||||
return;
|
||||
}
|
||||
window.open(row.file_url, "_blank");
|
||||
}
|
||||
|
||||
function fileTypeText(t) {
|
||||
const hit = TENDER_FILE_TYPE_OPTIONS.find((i) => String(i.value) === String(t));
|
||||
return hit ? hit.label : t || "-";
|
||||
}
|
||||
|
||||
function formatFileSize(size) {
|
||||
if (!size) return "-";
|
||||
if (size < 1024) return size + " B";
|
||||
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + " KB";
|
||||
return (size / 1024 / 1024).toFixed(2) + " MB";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<div class="tender-result" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">定标结果</h3>
|
||||
<el-button
|
||||
v-if="canAward"
|
||||
type="success"
|
||||
:icon="Trophy"
|
||||
@click="openAward"
|
||||
>
|
||||
定标
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="list.length === 0" description="尚未定标" :image-size="80" />
|
||||
|
||||
<div v-else>
|
||||
<el-table :data="list" stripe border>
|
||||
<el-table-column label="结果类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="resultTypeTag(row.result_type)" size="small">
|
||||
{{ resultTypeText(row.result_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中标单位" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.winner_supplier_name || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中标金额" width="140" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.winner_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="定标日期" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.result_date) || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关联合同" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.contract_id" type="primary" :underline="false">
|
||||
合同 #{{ row.contract_id }}
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 定标 -->
|
||||
<el-dialog v-model="awardVisible" title="定标" width="620px" :close-on-click-modal="false">
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="定标后招标状态将转为「已定标」并锁定,不可重复定标。"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
<el-form :model="awardForm" label-width="110px">
|
||||
<el-form-item label="结果类型">
|
||||
<el-radio-group v-model="awardForm.result_type">
|
||||
<el-radio :value="1">定标</el-radio>
|
||||
<el-radio :value="2">流标</el-radio>
|
||||
<el-radio :value="3">废标</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<template v-if="awardForm.result_type === 1">
|
||||
<el-form-item label="中标单位" required>
|
||||
<el-select
|
||||
v-model="awardForm.winner_supplier_name"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入中标单位"
|
||||
style="width: 100%"
|
||||
@change="onWinnerChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="s in suppliers"
|
||||
:key="s.id"
|
||||
:label="s.supplier_name"
|
||||
:value="s.supplier_name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="中标金额">
|
||||
<el-input v-model.number="awardForm.winner_amount" placeholder="0.00">
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="结果公告">
|
||||
<el-input v-model="awardForm.announcement" type="textarea" :rows="4" placeholder="定标公告内容(选填)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="awardVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleAward">确认定标</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 结果详情 -->
|
||||
<el-dialog v-model="detailVisible" title="定标结果详情" width="680px">
|
||||
<el-descriptions v-if="detailRow" :column="2" border>
|
||||
<el-descriptions-item label="结果类型">
|
||||
<el-tag :type="resultTypeTag(detailRow.result_type)" size="small">
|
||||
{{ resultTypeText(detailRow.result_type) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="中标单位">
|
||||
{{ detailRow.winner_supplier_name || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="中标金额">
|
||||
{{ formatMoney(detailRow.winner_amount) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="定标日期">
|
||||
{{ formatDateTime(detailRow.result_date) || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="关联合同">
|
||||
{{ detailRow.contract_id ? `合同 #${detailRow.contract_id}` : "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="关联项目">
|
||||
{{ detailRow.project_id ? `项目 #${detailRow.project_id}` : "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="记录人">
|
||||
{{ detailRow.create_user_name || "-" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider />
|
||||
<h4 class="block-title">结果公告</h4>
|
||||
<div class="content-box">{{ detailRow?.announcement || "—" }}</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Trophy } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getTenderResultList,
|
||||
getTenderResultDetail,
|
||||
awardTender,
|
||||
getTenderSupplierList,
|
||||
} from "@/api/crmBidding";
|
||||
import {
|
||||
tenderResultTypeOptions,
|
||||
formatMoney,
|
||||
formatDateTime,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
tenderId: { type: [Number, String], required: true },
|
||||
tender: { type: Object, default: null },
|
||||
});
|
||||
const emit = defineEmits(["count", "awarded"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const suppliers = ref([]);
|
||||
|
||||
const awardVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const detailRow = ref(null);
|
||||
const submitting = ref(false);
|
||||
|
||||
const awardForm = reactive({
|
||||
result_type: 1,
|
||||
winner_supplier_name: "",
|
||||
winner_supplier_id: 0,
|
||||
winner_amount: 0,
|
||||
announcement: "",
|
||||
});
|
||||
|
||||
const canAward = computed(() => {
|
||||
if (!props.tender) return true;
|
||||
const s = Number(props.tender.status);
|
||||
return props.tender.locked !== 1 && s >= 4 && s < 6;
|
||||
});
|
||||
|
||||
defineExpose({ reload: loadList, openAward });
|
||||
|
||||
onMounted(() => {
|
||||
loadList();
|
||||
loadSuppliers();
|
||||
});
|
||||
|
||||
async function loadList() {
|
||||
if (!props.tenderId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenderResultList({ tender_id: props.tenderId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSuppliers() {
|
||||
if (!props.tenderId) return;
|
||||
try {
|
||||
const res = await getTenderSupplierList({ tender_id: props.tenderId });
|
||||
suppliers.value = res?.data?.list || [];
|
||||
} catch {
|
||||
suppliers.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function onWinnerChange(name) {
|
||||
const hit = suppliers.value.find((s) => s.supplier_name === name);
|
||||
awardForm.winner_supplier_id = hit?.supplier_id || hit?.id || 0;
|
||||
if (hit && !awardForm.winner_amount) {
|
||||
awardForm.winner_amount = Number(hit.bid_amount) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
async function openAward() {
|
||||
awardForm.result_type = 1;
|
||||
awardForm.winner_supplier_name = props.tender?.winner_supplier_name || "";
|
||||
awardForm.winner_supplier_id = props.tender?.winner_supplier_id || 0;
|
||||
awardForm.winner_amount = Number(props.tender?.budget_amount) || 0;
|
||||
awardForm.announcement = "";
|
||||
await loadSuppliers();
|
||||
awardVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleAward() {
|
||||
if (awardForm.result_type === 1 && !awardForm.winner_supplier_name.trim()) {
|
||||
ElMessage.warning("请选择中标单位");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm("定标后不可撤销,确认继续?", "定标确认", { type: "warning" });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await awardTender(props.tenderId, { ...awardForm });
|
||||
ElMessage.success("定标成功");
|
||||
awardVisible.value = false;
|
||||
loadList();
|
||||
emit("awarded");
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "定标失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(row) {
|
||||
try {
|
||||
const res = await getTenderResultDetail(row.id);
|
||||
detailRow.value = res?.data || row;
|
||||
} catch {
|
||||
detailRow.value = row;
|
||||
}
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
function resultTypeText(v) {
|
||||
const hit = tenderResultTypeOptions.find((i) => Number(i.value) === Number(v));
|
||||
return hit ? hit.label : "-";
|
||||
}
|
||||
|
||||
function resultTypeTag(v) {
|
||||
const map = { 1: "success", 2: "warning", 3: "danger" };
|
||||
return map[Number(v)] || "info";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.block-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.content-box {
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
min-height: 40px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div class="tender-supplier" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">投标方报名</h3>
|
||||
<el-button type="primary" :icon="Plus" :disabled="disabled" @click="openCreate">
|
||||
登记投标方
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="list.length === 0" description="暂无投标方报名" :image-size="80" />
|
||||
|
||||
<el-table v-else :data="list" stripe border>
|
||||
<el-table-column label="投标方名称" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.supplier_name }}</span>
|
||||
<el-tag v-if="row.is_winner === 1" type="success" size="small" style="margin-left: 6px">
|
||||
中标
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报名状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="registerStatusTag(row.register_status)" size="small">
|
||||
{{ registerStatusText(row.register_status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="投标报价" width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.bid_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系人" prop="contact_person" width="100" align="center" />
|
||||
<el-table-column label="联系电话" prop="contact_phone" width="130" align="center" />
|
||||
<el-table-column label="排名" prop="ranking" width="70" align="center" />
|
||||
<el-table-column label="报名时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.register_time) || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="disabled"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.is_winner !== 1"
|
||||
link
|
||||
type="success"
|
||||
size="small"
|
||||
:disabled="disabled || row.register_status !== 1"
|
||||
@click="handleSetWinner(row)"
|
||||
>
|
||||
设为中标
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:disabled="disabled"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editing ? '编辑投标方' : '登记投标方'"
|
||||
width="620px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="投标方名称" required>
|
||||
<el-input v-model="form.supplier_name" placeholder="请输入投标方单位名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="报名状态">
|
||||
<el-select v-model="form.register_status" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in tenderSupplierStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="投标报价">
|
||||
<el-input v-model.number="form.bid_amount" placeholder="0.00">
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="form.contact_person" placeholder="联系人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="form.contact_phone" placeholder="联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注(选填)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getTenderSupplierList,
|
||||
createTenderSupplier,
|
||||
updateTenderSupplier,
|
||||
deleteTenderSupplier,
|
||||
} from "@/api/crmBidding";
|
||||
import {
|
||||
tenderSupplierStatusOptions,
|
||||
formatMoney,
|
||||
formatDateTime,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
tenderId: { type: [Number, String], required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(["count"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const editing = ref(null);
|
||||
const submitting = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
supplier_name: "",
|
||||
register_status: 1,
|
||||
bid_amount: 0,
|
||||
contact_person: "",
|
||||
contact_phone: "",
|
||||
remark: "",
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.tenderId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenderSupplierList({ tender_id: props.tenderId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editing.value = row;
|
||||
Object.assign(form, {
|
||||
supplier_name: row.supplier_name || "",
|
||||
register_status: Number(row.register_status) || 1,
|
||||
bid_amount: Number(row.bid_amount) || 0,
|
||||
contact_person: row.contact_person || "",
|
||||
contact_phone: row.contact_phone || "",
|
||||
remark: row.remark || "",
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.supplier_name.trim()) {
|
||||
ElMessage.warning("请输入投标方名称");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
tender_id: props.tenderId,
|
||||
supplier_name: form.supplier_name,
|
||||
register_status: form.register_status,
|
||||
bid_amount: form.bid_amount,
|
||||
contact_person: form.contact_person,
|
||||
contact_phone: form.contact_phone,
|
||||
remark: form.remark,
|
||||
};
|
||||
if (editing.value?.id) {
|
||||
await updateTenderSupplier(editing.value.id, payload);
|
||||
} else {
|
||||
await createTenderSupplier(payload);
|
||||
}
|
||||
ElMessage.success("保存成功");
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSetWinner(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将「${row.supplier_name}」标记为本招标的中标单位?`,
|
||||
"中标确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateTenderSupplier(row.id, {
|
||||
tender_id: props.tenderId,
|
||||
supplier_name: row.supplier_name,
|
||||
register_status: row.register_status,
|
||||
bid_amount: row.bid_amount,
|
||||
contact_person: row.contact_person,
|
||||
contact_phone: row.contact_phone,
|
||||
remark: row.remark,
|
||||
is_winner: 1,
|
||||
ranking: 1,
|
||||
});
|
||||
ElMessage.success("已标记中标");
|
||||
loadList();
|
||||
emit("count", list.value.length);
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除投标方「${row.supplier_name}」?`, "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteTenderSupplier(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
function registerStatusText(v) {
|
||||
const hit = tenderSupplierStatusOptions.find((i) => Number(i.value) === Number(v));
|
||||
return hit ? hit.label : "未知";
|
||||
}
|
||||
|
||||
function registerStatusTag(v) {
|
||||
const map = { 1: "success", 2: "danger", 3: "info" };
|
||||
return map[Number(v)] || "info";
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="编辑招标"
|
||||
width="860px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="handleClose"
|
||||
>
|
||||
<el-alert
|
||||
v-if="locked"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="该招标已定标归档,仅允许修改备注类信息。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="招标标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入招标标题" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="招标编号">
|
||||
<el-input v-model="form.tender_no" placeholder="招标编号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="招标类型">
|
||||
<el-select v-model="form.tender_type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in tenderTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="评标方法">
|
||||
<el-select v-model="form.evaluation_method" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in evaluationMethodOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="招标方" prop="customer_name">
|
||||
<el-input v-model="form.customer_name" placeholder="请输入招标方名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发布单位">
|
||||
<el-input v-model="form.publish_org" placeholder="发布单位" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="预算金额">
|
||||
<el-input v-model.number="form.budget_amount" placeholder="0.00">
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="投标保证金">
|
||||
<el-input v-model.number="form.bid_deposit" placeholder="0.00">
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="公告发布日期">
|
||||
<el-date-picker
|
||||
v-model="form.publish_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开标时间">
|
||||
<el-date-picker
|
||||
v-model="form.open_bid_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报名开始">
|
||||
<el-date-picker
|
||||
v-model="form.register_start_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报名截止">
|
||||
<el-date-picker
|
||||
v-model="form.register_end_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="投标开始">
|
||||
<el-date-picker
|
||||
v-model="form.bid_start_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="投标截止">
|
||||
<el-date-picker
|
||||
v-model="form.bid_end_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="form.contact_person" placeholder="联系人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="form.contact_phone" placeholder="联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系邮箱">
|
||||
<el-input v-model="form.contact_email" placeholder="联系邮箱" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="投标地点">
|
||||
<el-input v-model="form.bid_address" placeholder="递交投标文件的地点" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开标地点">
|
||||
<el-input v-model="form.open_address" placeholder="开标地点" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="项目描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="项目描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="资质要求">
|
||||
<el-input v-model="form.requirement" type="textarea" :rows="3" placeholder="资质要求" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { updateTender } from "@/api/crmBidding";
|
||||
import { tenderTypeOptions, evaluationMethodOptions } from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
tender: { type: Object, default: null },
|
||||
});
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
|
||||
const locked = computed(() => props.tender?.locked === 1);
|
||||
|
||||
const form = reactive({
|
||||
title: "",
|
||||
tender_no: "",
|
||||
tender_type: "1",
|
||||
evaluation_method: "1",
|
||||
customer_name: "",
|
||||
publish_org: "",
|
||||
budget_amount: 0,
|
||||
bid_deposit: 0,
|
||||
publish_date: "",
|
||||
register_start_date: "",
|
||||
register_end_date: "",
|
||||
bid_start_date: "",
|
||||
bid_end_date: "",
|
||||
open_bid_date: "",
|
||||
contact_person: "",
|
||||
contact_phone: "",
|
||||
contact_email: "",
|
||||
bid_address: "",
|
||||
open_address: "",
|
||||
description: "",
|
||||
requirement: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: "请输入招标标题", trigger: "blur" }],
|
||||
customer_name: [{ required: true, message: "请输入招标方名称", trigger: "blur" }],
|
||||
};
|
||||
|
||||
// 后端返回的时间字段可能是 "2026-01-01T00:00:00+08:00" 形式,统一裁剪
|
||||
function trimTime(val, len) {
|
||||
if (!val) return "";
|
||||
const s = String(val).replace("T", " ");
|
||||
return s.length > len ? s.slice(0, len) : s;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.tender],
|
||||
([val, tender]) => {
|
||||
if (!val || !tender) return;
|
||||
Object.assign(form, {
|
||||
title: tender.title || "",
|
||||
tender_no: tender.tender_no || "",
|
||||
tender_type: String(tender.tender_type ?? "1"),
|
||||
evaluation_method: String(tender.evaluation_method ?? "1"),
|
||||
customer_name: tender.customer_name || "",
|
||||
publish_org: tender.publish_org || "",
|
||||
budget_amount: Number(tender.budget_amount) || 0,
|
||||
bid_deposit: Number(tender.bid_deposit) || 0,
|
||||
publish_date: trimTime(tender.publish_date, 10),
|
||||
register_start_date: trimTime(tender.register_start_date, 19),
|
||||
register_end_date: trimTime(tender.register_end_date, 19),
|
||||
bid_start_date: trimTime(tender.bid_start_date, 19),
|
||||
bid_end_date: trimTime(tender.bid_end_date, 19),
|
||||
open_bid_date: trimTime(tender.open_bid_date, 19),
|
||||
contact_person: tender.contact_person || "",
|
||||
contact_phone: tender.contact_phone || "",
|
||||
contact_email: tender.contact_email || "",
|
||||
bid_address: tender.bid_address || "",
|
||||
open_address: tender.open_address || "",
|
||||
description: tender.description || "",
|
||||
requirement: tender.requirement || "",
|
||||
remark: tender.remark || "",
|
||||
});
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value || !props.tender?.id) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await updateTender(props.tender.id, { ...form });
|
||||
ElMessage.success("保存成功");
|
||||
emit("success");
|
||||
handleClose();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="新建招标"
|
||||
width="860px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="招标标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入招标标题" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="招标编号">
|
||||
<el-input v-model="form.tender_no" placeholder="可留空自动生成" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="招标类型">
|
||||
<el-select v-model="form.tender_type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in tenderTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="评标方法">
|
||||
<el-select v-model="form.evaluation_method" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in evaluationMethodOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="招标方" prop="customer_name">
|
||||
<el-input v-model="form.customer_name" placeholder="请输入招标方名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发布单位">
|
||||
<el-input v-model="form.publish_org" placeholder="留空时默认取招标方" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="预算金额">
|
||||
<el-input v-model.number="form.budget_amount" placeholder="0.00">
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="投标保证金">
|
||||
<el-input v-model.number="form.bid_deposit" placeholder="0.00">
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="公告发布日期">
|
||||
<el-date-picker
|
||||
v-model="form.publish_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开标时间">
|
||||
<el-date-picker
|
||||
v-model="form.open_bid_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报名开始">
|
||||
<el-date-picker
|
||||
v-model="form.register_start_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报名截止">
|
||||
<el-date-picker
|
||||
v-model="form.register_end_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="投标开始">
|
||||
<el-date-picker
|
||||
v-model="form.bid_start_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="投标截止">
|
||||
<el-date-picker
|
||||
v-model="form.bid_end_date"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="form.contact_person" placeholder="联系人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="form.contact_phone" placeholder="联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="联系邮箱">
|
||||
<el-input v-model="form.contact_email" placeholder="联系邮箱" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="投标地点">
|
||||
<el-input v-model="form.bid_address" placeholder="递交投标文件的地点" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开标地点">
|
||||
<el-input v-model="form.open_address" placeholder="开标地点" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="项目描述">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="项目背景与整体说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="资质要求">
|
||||
<el-input
|
||||
v-model="form.requirement"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="对投标方的资质、业绩、技术要求"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="其他备注(选填)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存为草稿</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { createTender } from "@/api/crmBidding";
|
||||
import { tenderTypeOptions, evaluationMethodOptions } from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
title: "",
|
||||
tender_no: "",
|
||||
tender_type: "1",
|
||||
evaluation_method: "1",
|
||||
customer_name: "",
|
||||
publish_org: "",
|
||||
budget_amount: 0,
|
||||
bid_deposit: 0,
|
||||
publish_date: "",
|
||||
register_start_date: "",
|
||||
register_end_date: "",
|
||||
bid_start_date: "",
|
||||
bid_end_date: "",
|
||||
open_bid_date: "",
|
||||
contact_person: "",
|
||||
contact_phone: "",
|
||||
contact_email: "",
|
||||
bid_address: "",
|
||||
open_address: "",
|
||||
description: "",
|
||||
requirement: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: "请输入招标标题", trigger: "blur" }],
|
||||
customer_name: [{ required: true, message: "请输入招标方名称", trigger: "blur" }],
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
Object.assign(form, emptyForm());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const res = await createTender({ ...form });
|
||||
ElMessage.success("创建成功");
|
||||
emit("success", res?.data);
|
||||
handleClose();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "创建失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="visible"
|
||||
:title="tender?.title || '招标详情'"
|
||||
direction="rtl"
|
||||
size="1000px"
|
||||
@update:model-value="handleClose"
|
||||
@opened="loadAll"
|
||||
>
|
||||
<div v-if="tender" class="tender-detail">
|
||||
<el-tabs v-model="activeTab" tab-position="left" class="detail-tabs">
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
<TenderBaseInfo :tender="tender" @action="handleAction" />
|
||||
<el-divider />
|
||||
<h4 class="block-title">流程进度</h4>
|
||||
<BidTimeline kind="tender" :status="tender.status" :times="tender" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`招标公告 (${counts.announce})`" name="announce">
|
||||
<TenderAnnounceList
|
||||
ref="announceRef"
|
||||
:tender-id="tender.id"
|
||||
:disabled="locked"
|
||||
@count="counts.announce = $event"
|
||||
@published="refreshSelf"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`招标文件 (${counts.file})`" name="file">
|
||||
<TenderFileList
|
||||
ref="fileRef"
|
||||
:tender-id="tender.id"
|
||||
:disabled="locked"
|
||||
@count="counts.file = $event"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`投标方 (${counts.supplier})`" name="supplier">
|
||||
<TenderSupplierList
|
||||
ref="supplierRef"
|
||||
:tender-id="tender.id"
|
||||
:disabled="locked"
|
||||
@count="counts.supplier = $event"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`开标评标 (${counts.evaluation})`" name="evaluation">
|
||||
<TenderEvaluation
|
||||
ref="evaluationRef"
|
||||
:tender-id="tender.id"
|
||||
:disabled="locked"
|
||||
@count="counts.evaluation = $event"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane :label="`定标结果 (${counts.result})`" name="result">
|
||||
<TenderResult
|
||||
ref="resultRef"
|
||||
:tender-id="tender.id"
|
||||
:tender="tender"
|
||||
@count="counts.result = $event"
|
||||
@awarded="refreshSelf"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { getTenderDetail, awardTenderToContract } from "@/api/crmBidding";
|
||||
|
||||
import BidTimeline from "../components/BidTimeline.vue";
|
||||
import TenderBaseInfo from "./components/TenderBaseInfo.vue";
|
||||
import TenderAnnounceList from "./components/TenderAnnounceList.vue";
|
||||
import TenderFileList from "./components/TenderFileList.vue";
|
||||
import TenderSupplierList from "./components/TenderSupplierList.vue";
|
||||
import TenderEvaluation from "./components/TenderEvaluation.vue";
|
||||
import TenderResult from "./components/TenderResult.vue";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
tender: { type: Object, default: null },
|
||||
});
|
||||
const emit = defineEmits(["update:visible", "refresh", "action"]);
|
||||
|
||||
const activeTab = ref("basic");
|
||||
const counts = reactive({
|
||||
announce: 0,
|
||||
file: 0,
|
||||
supplier: 0,
|
||||
evaluation: 0,
|
||||
result: 0,
|
||||
});
|
||||
|
||||
const announceRef = ref();
|
||||
const fileRef = ref();
|
||||
const supplierRef = ref();
|
||||
const evaluationRef = ref();
|
||||
const resultRef = ref();
|
||||
|
||||
const locked = computed(() => props.tender?.locked === 1);
|
||||
|
||||
function handleClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
async function refreshSelf() {
|
||||
if (!props.tender?.id) return;
|
||||
try {
|
||||
const res = await getTenderDetail(props.tender.id);
|
||||
if (res?.data) {
|
||||
Object.assign(props.tender, res.data);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
emit("refresh");
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
if (!props.tender?.id) return;
|
||||
await refreshSelf();
|
||||
counts.announce = 0;
|
||||
counts.file = 0;
|
||||
counts.supplier = 0;
|
||||
counts.evaluation = 0;
|
||||
counts.result = 0;
|
||||
announceRef.value?.reload?.();
|
||||
fileRef.value?.reload?.();
|
||||
supplierRef.value?.reload?.();
|
||||
evaluationRef.value?.reload?.();
|
||||
resultRef.value?.reload?.();
|
||||
}
|
||||
|
||||
async function handleAction({ type, row }) {
|
||||
if (type === "edit") {
|
||||
emit("action", { type: "edit", row });
|
||||
return;
|
||||
}
|
||||
if (type === "award") {
|
||||
activeTab.value = "result";
|
||||
resultRef.value?.openAward?.();
|
||||
return;
|
||||
}
|
||||
if (type === "contract") {
|
||||
await handleToContract(row);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleToContract(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将「${row.title}」的中标结果转为合同吗?`,
|
||||
"转合同确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await awardTenderToContract(row.id, {});
|
||||
ElMessage.success("已转合同");
|
||||
await refreshSelf();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "转合同失败");
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ reload: loadAll });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tender-detail {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
.detail-tabs {
|
||||
min-height: 640px;
|
||||
}
|
||||
.block-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="crm-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>我要招标</h2>
|
||||
<p>作为甲方发布招标、收集投标、评标定标,支持定标后一键转合同</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="fetchList">刷新</el-button>
|
||||
<el-button :icon="DataAnalysis" @click="goStatistics">统计</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openCreate">新建招标</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-form :inline="true" :model="filters" @submit.prevent>
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
clearable
|
||||
placeholder="搜索招标标题 / 编号 / 招标方"
|
||||
:prefix-icon="Search"
|
||||
style="width: 260px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select v-model="filters.status" clearable placeholder="状态" style="width: 140px">
|
||||
<el-option
|
||||
v-for="opt in tenderStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-select v-model="filters.tender_type" clearable placeholder="招标类型" style="width: 140px">
|
||||
<el-option
|
||||
v-for="opt in tenderTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button :icon="RefreshLeft" @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap" v-loading="loading">
|
||||
<el-table :data="tableData" stripe border>
|
||||
<el-table-column label="招标编号" prop="tender_no" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="招标标题" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" :underline="false" @click="openDetail(row)">
|
||||
{{ row.title }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="110" align="center">
|
||||
<template #default="{ row }"><TenderTypeTag :type="row.tender_type" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }"><StatusTag kind="tender" :status="row.status" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预算金额" width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.budget_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="投标截止" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatBidTime(row.bid_end_date) || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="中标单位" width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.winner_supplier_name || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.owner_user_name || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openDetail(row)">详情</el-button>
|
||||
<el-button
|
||||
link type="success" size="small"
|
||||
:disabled="row.status !== 6 || !!row.contract_id"
|
||||
@click="handleToContract(row)"
|
||||
>
|
||||
转合同
|
||||
</el-button>
|
||||
<el-button link type="warning" size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="暂无招标项目" :image-size="80" /></template>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSearch"
|
||||
@current-change="fetchList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TenderCreate v-model:visible="createVisible" @success="handleSaved" />
|
||||
<TenderEdit v-model:visible="editVisible" :tender="currentRow" @success="handleSaved" />
|
||||
<TenderDetail
|
||||
v-model:visible="detailVisible"
|
||||
:tender="currentRow"
|
||||
@refresh="fetchList"
|
||||
@action="handleDetailAction"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Search, Refresh, RefreshLeft, DataAnalysis } from "@element-plus/icons-vue";
|
||||
|
||||
import { getTenderList, deleteTender, awardTenderToContract } from "@/api/crmBidding";
|
||||
import {
|
||||
tenderStatusOptions,
|
||||
tenderTypeOptions,
|
||||
formatMoney,
|
||||
formatBidTime
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
import StatusTag from "../components/StatusTag.vue";
|
||||
import TenderTypeTag from "../components/TenderTypeTag.vue";
|
||||
import TenderCreate from "./create.vue";
|
||||
import TenderEdit from "./components/edit.vue";
|
||||
import TenderDetail from "./detail.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const filters = reactive({ keyword: "", status: "", tender_type: "" });
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
|
||||
const createVisible = ref(false);
|
||||
const editVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const currentRow = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
fetchList();
|
||||
// 从到期提醒跳转:携带 id 直接打开详情
|
||||
const qid = route.query.id;
|
||||
if (qid) {
|
||||
openDetail({ id: Number(qid) });
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenderList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
...filters
|
||||
});
|
||||
tableData.value = res?.data?.list || [];
|
||||
pagination.total = res?.data?.total || 0;
|
||||
} catch (e) {
|
||||
tableData.value = [];
|
||||
pagination.total = 0;
|
||||
ElMessage.error(e?.message || "查询失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = "";
|
||||
filters.status = "";
|
||||
filters.tender_type = "";
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
currentRow.value = null;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
currentRow.value = row;
|
||||
editVisible.value = true;
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
currentRow.value = row;
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
function goStatistics() {
|
||||
router.push("/apps/crm/bidding/statistics/tender");
|
||||
}
|
||||
|
||||
function handleSaved() {
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleDetailAction({ type, row }) {
|
||||
if (type === "edit" && row) {
|
||||
openEdit(row);
|
||||
}
|
||||
fetchList();
|
||||
}
|
||||
|
||||
async function handleToContract(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将招标「${row.title}」的中标结果转为合同吗?`,
|
||||
"转合同确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await awardTenderToContract(row.id, {});
|
||||
ElMessage.success("已转合同");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "转合同失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除招标「${row.title}」?`, "删除确认", {
|
||||
type: "warning"
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteTender(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped src="../../styles/crm-page.less"></style>
|
||||
|
||||
<style scoped>
|
||||
.filter-bar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user