增加招投标模块
This commit is contained in:
Vendored
+1
@@ -65,6 +65,7 @@ declare module 'vue' {
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
ElStep: typeof import('element-plus/es')['ElStep']
|
||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* CRM 招投标管理接口
|
||||
*
|
||||
* 关联类型 related_type:4=投标 5=招标
|
||||
* 投标状态机:0草稿 -> 1立项审批中 -> 2投标中 -> 3已开标 -> (4中标 | 5落标) -> (6已转合同 | 7已归档)
|
||||
* 招标状态机:0草稿 -> 1公告中 -> 2报名中 -> 3投标中 -> 4开标中 -> 5评标中 -> 6已定标 -> 7已归档
|
||||
*/
|
||||
|
||||
/* ====================== 投标(Bid)主表 ====================== */
|
||||
|
||||
export function getBidList(params) {
|
||||
return request({ url: "/backend/crm/bidding/bid/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function getBidDetail(id) {
|
||||
return request({ url: `/backend/crm/bidding/bid/detail/${id}`, method: "get" });
|
||||
}
|
||||
|
||||
export function createBid(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid", method: "post", data });
|
||||
}
|
||||
|
||||
export function updateBid(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/bid/${id}`, method: "put", data });
|
||||
}
|
||||
|
||||
export function deleteBid(id) {
|
||||
return request({ url: `/backend/crm/bidding/bid/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/** 中标转合同 */
|
||||
export function convertBidToContract(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/bid/${id}/contract`, method: "post", data });
|
||||
}
|
||||
|
||||
/** 中标转项目 */
|
||||
export function convertBidToProject(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/bid/${id}/project`, method: "post", data });
|
||||
}
|
||||
|
||||
/** 提交开标结果 */
|
||||
export function submitBidResult(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/bid/${id}/result`, method: "post", data });
|
||||
}
|
||||
|
||||
/** 从商机创建投标 */
|
||||
export function createBidFromBusiness(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid/from-business", method: "post", data });
|
||||
}
|
||||
|
||||
/** 从项目创建投标 */
|
||||
export function createBidFromProject(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid/from-project", method: "post", data });
|
||||
}
|
||||
|
||||
/* ====================== 投标文件 ====================== */
|
||||
|
||||
export function getBidFileList(params) {
|
||||
return request({ url: "/backend/crm/bidding/bid-file/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createBidFile(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid-file", method: "post", data });
|
||||
}
|
||||
|
||||
export function deleteBidFile(id) {
|
||||
return request({ url: `/backend/crm/bidding/bid-file/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 投标任务 ====================== */
|
||||
|
||||
export function getBidTaskList(params) {
|
||||
return request({ url: "/backend/crm/bidding/bid-task/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createBidTask(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid-task", method: "post", data });
|
||||
}
|
||||
|
||||
export function updateBidTask(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/bid-task/${id}`, method: "put", data });
|
||||
}
|
||||
|
||||
export function deleteBidTask(id) {
|
||||
return request({ url: `/backend/crm/bidding/bid-task/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 投标结果 ====================== */
|
||||
|
||||
export function getBidResultList(params) {
|
||||
return request({ url: "/backend/crm/bidding/bid-result/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function getBidResultDetail(id) {
|
||||
return request({ url: `/backend/crm/bidding/bid-result/detail/${id}`, method: "get" });
|
||||
}
|
||||
|
||||
/* ====================== 投标报价 ====================== */
|
||||
|
||||
export function getBidQuoteList(params) {
|
||||
return request({ url: "/backend/crm/bidding/bid-quote/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createBidQuote(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid-quote", method: "post", data });
|
||||
}
|
||||
|
||||
export function deleteBidQuote(id) {
|
||||
return request({ url: `/backend/crm/bidding/bid-quote/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 投标竞争对手 ====================== */
|
||||
|
||||
export function getBidCompetitorList(params) {
|
||||
return request({ url: "/backend/crm/bidding/bid-competitor/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createBidCompetitor(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid-competitor", method: "post", data });
|
||||
}
|
||||
|
||||
export function deleteBidCompetitor(id) {
|
||||
return request({ url: `/backend/crm/bidding/bid-competitor/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 投标费用 ====================== */
|
||||
|
||||
export function getBidExpenseList(params) {
|
||||
return request({ url: "/backend/crm/bidding/bid-expense/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createBidExpense(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid-expense", method: "post", data });
|
||||
}
|
||||
|
||||
export function deleteBidExpense(id) {
|
||||
return request({ url: `/backend/crm/bidding/bid-expense/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 招标(Tender)主表 ====================== */
|
||||
|
||||
export function getTenderList(params) {
|
||||
return request({ url: "/backend/crm/bidding/tender/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function getTenderDetail(id) {
|
||||
return request({ url: `/backend/crm/bidding/tender/detail/${id}`, method: "get" });
|
||||
}
|
||||
|
||||
export function createTender(data) {
|
||||
return request({ url: "/backend/crm/bidding/tender", method: "post", data });
|
||||
}
|
||||
|
||||
export function updateTender(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/tender/${id}`, method: "put", data });
|
||||
}
|
||||
|
||||
export function deleteTender(id) {
|
||||
return request({ url: `/backend/crm/bidding/tender/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/** 定标 */
|
||||
export function awardTender(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/tender/${id}/award`, method: "post", data });
|
||||
}
|
||||
|
||||
/** 定标转合同 */
|
||||
export function awardTenderToContract(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/tender/${id}/contract`, method: "post", data });
|
||||
}
|
||||
|
||||
/* ====================== 招标文件 ====================== */
|
||||
|
||||
export function getTenderFileList(params) {
|
||||
return request({ url: "/backend/crm/bidding/tender-file/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createTenderFile(data) {
|
||||
return request({ url: "/backend/crm/bidding/tender-file", method: "post", data });
|
||||
}
|
||||
|
||||
export function deleteTenderFile(id) {
|
||||
return request({ url: `/backend/crm/bidding/tender-file/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 招标公告 ====================== */
|
||||
|
||||
export function getTenderAnnounceList(params) {
|
||||
return request({ url: "/backend/crm/bidding/tender-announce/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createTenderAnnounce(data) {
|
||||
return request({ url: "/backend/crm/bidding/tender-announce", method: "post", data });
|
||||
}
|
||||
|
||||
export function publishTenderAnnounce(id) {
|
||||
return request({ url: `/backend/crm/bidding/tender-announce/${id}/publish`, method: "post" });
|
||||
}
|
||||
|
||||
export function deleteTenderAnnounce(id) {
|
||||
return request({ url: `/backend/crm/bidding/tender-announce/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 投标方 / 供应商 ====================== */
|
||||
|
||||
export function getTenderSupplierList(params) {
|
||||
return request({ url: "/backend/crm/bidding/tender-supplier/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createTenderSupplier(data) {
|
||||
return request({ url: "/backend/crm/bidding/tender-supplier", method: "post", data });
|
||||
}
|
||||
|
||||
export function updateTenderSupplier(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/tender-supplier/${id}`, method: "put", data });
|
||||
}
|
||||
|
||||
export function deleteTenderSupplier(id) {
|
||||
return request({ url: `/backend/crm/bidding/tender-supplier/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 开标评标 ====================== */
|
||||
|
||||
export function getTenderEvaluationList(params) {
|
||||
return request({ url: "/backend/crm/bidding/tender-evaluation/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createTenderEvaluation(data) {
|
||||
return request({ url: "/backend/crm/bidding/tender-evaluation", method: "post", data });
|
||||
}
|
||||
|
||||
export function deleteTenderEvaluation(id) {
|
||||
return request({ url: `/backend/crm/bidding/tender-evaluation/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 定标结果 ====================== */
|
||||
|
||||
export function getTenderResultList(params) {
|
||||
return request({ url: "/backend/crm/bidding/tender-result/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function getTenderResultDetail(id) {
|
||||
return request({ url: `/backend/crm/bidding/tender-result/detail/${id}`, method: "get" });
|
||||
}
|
||||
|
||||
/* ====================== 投标立项审批 ====================== */
|
||||
|
||||
export function getBidApprovalList(params) {
|
||||
return request({ url: "/backend/crm/bidding/bid-approval/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createBidApproval(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid-approval", method: "post", data });
|
||||
}
|
||||
|
||||
export function approveBidApproval(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/bid-approval/${id}/approve`, method: "post", data });
|
||||
}
|
||||
|
||||
/* ====================== 标书版本 ====================== */
|
||||
|
||||
export function getBidDocumentList(params) {
|
||||
return request({ url: "/backend/crm/bidding/bid-document/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createBidDocument(data) {
|
||||
return request({ url: "/backend/crm/bidding/bid-document", method: "post", data });
|
||||
}
|
||||
|
||||
export function updateBidDocument(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/bid-document/${id}`, method: "put", data });
|
||||
}
|
||||
|
||||
export function deleteBidDocument(id) {
|
||||
return request({ url: `/backend/crm/bidding/bid-document/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 标书模板库 ====================== */
|
||||
|
||||
export function getTemplateList(params) {
|
||||
return request({ url: "/backend/crm/bidding/template/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createTemplate(data) {
|
||||
return request({ url: "/backend/crm/bidding/template", method: "post", data });
|
||||
}
|
||||
|
||||
export function updateTemplate(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/template/${id}`, method: "put", data });
|
||||
}
|
||||
|
||||
export function deleteTemplate(id) {
|
||||
return request({ url: `/backend/crm/bidding/template/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 资质文件库 ====================== */
|
||||
|
||||
export function getQualificationList(params) {
|
||||
return request({ url: "/backend/crm/bidding/qualification/list", method: "get", params });
|
||||
}
|
||||
|
||||
export function createQualification(data) {
|
||||
return request({ url: "/backend/crm/bidding/qualification", method: "post", data });
|
||||
}
|
||||
|
||||
export function updateQualification(id, data) {
|
||||
return request({ url: `/backend/crm/bidding/qualification/${id}`, method: "put", data });
|
||||
}
|
||||
|
||||
export function deleteQualification(id) {
|
||||
return request({ url: `/backend/crm/bidding/qualification/${id}`, method: "delete" });
|
||||
}
|
||||
|
||||
/* ====================== 统计 ====================== */
|
||||
|
||||
export function getTenderStatistics(params) {
|
||||
return request({ url: "/backend/crm/bidding/statistics/tender", method: "get", params });
|
||||
}
|
||||
|
||||
export function getBidStatistics(params) {
|
||||
return request({ url: "/backend/crm/bidding/statistics/bid", method: "get", params });
|
||||
}
|
||||
|
||||
/* ====================== 到期提醒(开发计划 4.1) ====================== */
|
||||
|
||||
export function getBiddingReminders(params) {
|
||||
return request({ url: "/backend/crm/bidding/reminders", method: "get", params });
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="bid-approval" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">立项审批</h3>
|
||||
<el-button type="primary" :icon="Promotion" :disabled="disabled" @click="openSubmit">
|
||||
提交立项审批
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="list.length === 0" description="暂无审批记录" :image-size="80" />
|
||||
|
||||
<el-timeline v-else class="approval-timeline">
|
||||
<el-timeline-item
|
||||
v-for="row in list"
|
||||
:key="row.id"
|
||||
:type="statusType(row.approval_status)"
|
||||
:timestamp="formatDateTime(row.submit_time)"
|
||||
placement="top"
|
||||
>
|
||||
<el-card shadow="never" class="approval-card">
|
||||
<div class="approval-row">
|
||||
<el-tag :type="approvalStatusTag(row.approval_status)" size="small">
|
||||
{{ approvalStatusText(row.approval_status) }}
|
||||
</el-tag>
|
||||
<span class="approval-type">{{ approvalTypeText(row.approval_type) }}</span>
|
||||
<span class="approval-user">提交人:{{ row.submit_user_name || "-" }}</span>
|
||||
</div>
|
||||
<div v-if="row.approval_users" class="approval-row muted">
|
||||
审批人:{{ row.approval_users }}
|
||||
</div>
|
||||
<div v-if="row.comment" class="approval-comment">{{ row.comment }}</div>
|
||||
<div v-if="row.complete_time" class="approval-row muted">
|
||||
完成时间:{{ formatDateTime(row.complete_time) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="row.approval_status === 1 && !disabled"
|
||||
class="approval-actions"
|
||||
>
|
||||
<el-button type="success" size="small" @click="handleApprove(row, true)">通过</el-button>
|
||||
<el-button type="danger" size="small" @click="handleApprove(row, false)">驳回</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="提交立项审批" width="560px" :close-on-click-modal="false">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="审批类型">
|
||||
<el-select v-model="form.approval_type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in approvalTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批人">
|
||||
<el-input v-model="form.approval_users" placeholder="多个审批人用逗号分隔" />
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Promotion } from "@element-plus/icons-vue";
|
||||
import { getBidApprovalList, createBidApproval, approveBidApproval } from "@/api/crmBidding";
|
||||
import {
|
||||
approvalStatusText,
|
||||
approvalStatusTag,
|
||||
approvalTypeOptions,
|
||||
formatDateTime,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
bidId: { type: [Number, String], required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(["count", "approved"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
const form = reactive({ approval_type: "1", approval_users: "", comment: "" });
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.bidId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidApprovalList({ bid_id: props.bidId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openSubmit() {
|
||||
form.approval_type = "1";
|
||||
form.approval_users = "";
|
||||
form.comment = "";
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
submitting.value = true;
|
||||
try {
|
||||
await createBidApproval({
|
||||
bid_id: props.bidId,
|
||||
approval_type: form.approval_type,
|
||||
approval_users: form.approval_users,
|
||||
comment: form.comment,
|
||||
});
|
||||
ElMessage.success("已提交,投标状态转为「立项审批中」");
|
||||
dialogVisible.value = false;
|
||||
loadList();
|
||||
emit("approved");
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "提交失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApprove(row, pass) {
|
||||
let comment = "";
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
pass ? "填写审批意见(可留空)" : "填写驳回原因",
|
||||
pass ? "审批通过" : "审批驳回",
|
||||
{ inputPlaceholder: "审批意见", inputType: "textarea" }
|
||||
);
|
||||
comment = value || "";
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await approveBidApproval(row.id, { pass, comment });
|
||||
ElMessage.success(pass ? "已通过,投标转为「投标中」" : "已驳回,投标退回草稿");
|
||||
loadList();
|
||||
emit("approved");
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "操作失败");
|
||||
}
|
||||
}
|
||||
|
||||
function approvalTypeText(v) {
|
||||
const hit = approvalTypeOptions.find((i) => String(i.value) === String(v));
|
||||
return hit ? hit.label : "-";
|
||||
}
|
||||
|
||||
function statusType(v) {
|
||||
const map = { 1: "primary", 2: "success", 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;
|
||||
}
|
||||
.approval-timeline {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.approval-card {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.approval-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.approval-row.muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
.approval-type {
|
||||
font-weight: 500;
|
||||
}
|
||||
.approval-user {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.approval-comment {
|
||||
margin: 6px 0;
|
||||
padding: 8px;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.approval-actions {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="bid-basic" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">基本信息</h3>
|
||||
<div class="section-actions">
|
||||
<el-button v-if="canEdit" type="primary" link :icon="EditPen" @click="handleEdit">编辑</el-button>
|
||||
<el-button v-if="canConvertContract" type="success" link :icon="Document" @click="handleConvert('contract')">
|
||||
中标转合同
|
||||
</el-button>
|
||||
<el-button v-if="canConvertProject" type="warning" link :icon="Box" @click="handleConvert('project')">
|
||||
中标转项目
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="投标编号">{{ bid.bid_no || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="投标标题">{{ bid.title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="投标类型">
|
||||
<el-tag size="small">{{ bidTypeText(bid.bid_type) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="投标状态">
|
||||
<el-tag :type="bidStatusTag(bid.status)" size="small">
|
||||
{{ bidStatusText(bid.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源">
|
||||
<el-tag v-if="bid.source_type === 1" size="small" type="warning">商机</el-tag>
|
||||
<el-tag v-else-if="bid.source_type === 2" size="small" type="primary">项目</el-tag>
|
||||
<el-tag v-else size="small" type="info" effect="plain">独立</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源名称">
|
||||
<span v-if="bid.business_name">{{ bid.business_name }}</span>
|
||||
<span v-else-if="bid.project_name">{{ bid.project_name }}</span>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="招标方">{{ bid.customer_name || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发布单位">{{ bid.publish_org || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="投标报价">{{ formatMoney(bid.bid_amount) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="保证金">{{ formatMoney(bid.bid_deposit) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="招标发布日期">{{ formatDateOnly(bid.publish_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="报名截止">{{ formatDateTime(bid.register_end_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="投标截止">{{ formatDateTime(bid.bid_end_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="开标时间">{{ formatDateTime(bid.open_bid_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="中标状态">
|
||||
<el-tag v-if="bid.win_status === 1" type="success" size="small">已中标</el-tag>
|
||||
<el-tag v-else-if="bid.win_status === 2" type="danger" size="small">未中标</el-tag>
|
||||
<el-tag v-else-if="bid.win_status === 3" type="warning" size="small">废标</el-tag>
|
||||
<el-tag v-else-if="bid.win_status === 4" type="info" size="small">流标</el-tag>
|
||||
<el-tag v-else type="info" size="small" effect="plain">未开标</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="中标日期">{{ formatDateTime(bid.win_date) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="我方排名">{{ bid.our_ranking || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="我方得分">{{ bid.total_score || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ bid.contact_person || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ bid.contact_phone || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">{{ bid.owner_user_name || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDateTime(bid.create_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关联合同">
|
||||
<el-link v-if="bid.contract_id" type="primary" :underline="false">合同 #{{ bid.contract_id }}</el-link>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="关联项目" v-if="bid.new_project_id || bid.project_id">
|
||||
<el-link type="primary" :underline="false">
|
||||
项目 #{{ bid.new_project_id || bid.project_id }}
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4 class="block-title">项目描述</h4>
|
||||
<div class="content-box">{{ bid.description || "暂无描述" }}</div>
|
||||
|
||||
<h4 class="block-title">备注</h4>
|
||||
<div class="content-box">{{ bid.remark || "—" }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from "vue";
|
||||
import { EditPen, Document, Box } from "@element-plus/icons-vue";
|
||||
import {
|
||||
bidStatusText,
|
||||
bidStatusTag,
|
||||
bidTypeText,
|
||||
formatMoney,
|
||||
formatDateOnly,
|
||||
formatDateTime,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
bid: { type: Object, required: true },
|
||||
});
|
||||
const emit = defineEmits(["action"]);
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const canEdit = computed(() => props.bid.locked !== 1 && props.bid.status !== 6);
|
||||
const canConvertContract = computed(
|
||||
() => props.bid.win_status === 1 && !props.bid.contract_id
|
||||
);
|
||||
const canConvertProject = computed(
|
||||
() => props.bid.win_status === 1 && !props.bid.new_project_id
|
||||
);
|
||||
|
||||
function handleEdit() {
|
||||
emit("action", { type: "edit", row: props.bid });
|
||||
}
|
||||
|
||||
function handleConvert(type) {
|
||||
emit("action", { type, row: props.bid });
|
||||
}
|
||||
</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,195 @@
|
||||
<template>
|
||||
<div class="bid-competitor" 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="160" show-overflow-tooltip prop="competitor_name" />
|
||||
<el-table-column label="类型" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ competitorTypeText(row.competitor_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="对手报价" width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.quote_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优势" prop="advantages" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="劣势" prop="weaknesses" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="信息来源" prop="source" width="130" 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="680px" :close-on-click-modal="false">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="对手名称" required>
|
||||
<el-input v-model="form.competitor_name" placeholder="请输入竞争对手名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="对手类型">
|
||||
<el-select v-model="form.competitor_type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in competitorTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="对手报价">
|
||||
<el-input-number v-model="form.quote_amount" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="对手优势">
|
||||
<el-input v-model="form.advantages" type="textarea" :rows="3" placeholder="对手的优势" />
|
||||
</el-form-item>
|
||||
<el-form-item label="对手劣势">
|
||||
<el-input v-model="form.weaknesses" type="textarea" :rows="3" placeholder="对手的劣势" />
|
||||
</el-form-item>
|
||||
<el-form-item label="信息来源">
|
||||
<el-input v-model="form.source" 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 { getBidCompetitorList, createBidCompetitor, deleteBidCompetitor } from "@/api/crmBidding";
|
||||
import { competitorTypeOptions, competitorTypeText, formatMoney } from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
bidId: { 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 submitting = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
competitor_name: "",
|
||||
competitor_type: 1,
|
||||
quote_amount: 0,
|
||||
advantages: "",
|
||||
weaknesses: "",
|
||||
source: "",
|
||||
remark: "",
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.bidId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidCompetitorList({ bid_id: props.bidId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.competitor_name.trim()) {
|
||||
ElMessage.warning("请输入竞争对手名称");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await createBidCompetitor({
|
||||
bid_id: props.bidId,
|
||||
competitor_name: form.competitor_name,
|
||||
competitor_type: form.competitor_type,
|
||||
quote_amount: form.quote_amount,
|
||||
advantages: form.advantages,
|
||||
weaknesses: form.weaknesses,
|
||||
source: form.source,
|
||||
remark: form.remark,
|
||||
});
|
||||
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.competitor_name}」?`, "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteBidCompetitor(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
</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,311 @@
|
||||
<template>
|
||||
<div class="bid-document" 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="版本" prop="version" width="90" align="center" />
|
||||
<el-table-column label="标题" min-width="180" show-overflow-tooltip prop="title" />
|
||||
<el-table-column label="引用模板" width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ templateName(row.template_id) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="documentStatusTag(row.status)" size="small">
|
||||
{{ documentStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核意见" prop="review_comment" 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="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openPreview(row)">查看</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
size="small"
|
||||
:disabled="disabled"
|
||||
@click="openEdit(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="820px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="版本号">
|
||||
<el-input v-model="form.version" placeholder="如 v1.0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="引用模板">
|
||||
<el-select
|
||||
v-model="form.template_id"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="可选,从标书模板库引用"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="t in templates"
|
||||
:key="t.id"
|
||||
:label="t.template_name"
|
||||
:value="t.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="标题" required>
|
||||
<el-input v-model="form.title" placeholder="标书标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in bidDocumentStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件链接">
|
||||
<el-input v-model="form.file_url" placeholder="上传后得到的附件地址(选填)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="正文内容">
|
||||
<el-input v-model="form.content" type="textarea" :rows="10" placeholder="标书正文" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见">
|
||||
<el-input v-model="form.review_comment" 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>
|
||||
|
||||
<el-dialog v-model="previewVisible" :title="previewRow?.title || '标书预览'" width="800px">
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="版本">{{ previewRow?.version || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="documentStatusTag(previewRow?.status)" size="small">
|
||||
{{ documentStatusText(previewRow?.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="模板">
|
||||
{{ templateName(previewRow?.template_id) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider />
|
||||
<div class="preview-content">{{ previewRow?.content || "(空)" }}</div>
|
||||
<template v-if="previewRow?.file_url">
|
||||
<el-divider />
|
||||
<el-link :href="previewRow.file_url" target="_blank" type="primary">打开附件</el-link>
|
||||
</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 {
|
||||
getBidDocumentList,
|
||||
createBidDocument,
|
||||
updateBidDocument,
|
||||
deleteBidDocument,
|
||||
getTemplateList,
|
||||
} from "@/api/crmBidding";
|
||||
import {
|
||||
bidDocumentStatusOptions,
|
||||
documentStatusText,
|
||||
documentStatusTag,
|
||||
formatDateTime,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
bidId: { type: [Number, String], required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
});
|
||||
const emit = defineEmits(["count"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const templates = ref([]);
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const previewVisible = ref(false);
|
||||
const previewRow = ref(null);
|
||||
const editing = ref(null);
|
||||
const submitting = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
version: "v1.0",
|
||||
title: "",
|
||||
template_id: null,
|
||||
status: 1,
|
||||
content: "",
|
||||
file_url: "",
|
||||
review_comment: "",
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(() => {
|
||||
loadList();
|
||||
loadTemplates();
|
||||
});
|
||||
|
||||
async function loadList() {
|
||||
if (!props.bidId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidDocumentList({ bid_id: props.bidId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTemplates() {
|
||||
try {
|
||||
const res = await getTemplateList({ page: 1, pageSize: 100 });
|
||||
templates.value = res?.data?.list || [];
|
||||
} catch {
|
||||
templates.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function templateName(id) {
|
||||
if (!id) return "-";
|
||||
const hit = templates.value.find((t) => Number(t.id) === Number(id));
|
||||
return hit ? hit.template_name : `模板 #${id}`;
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editing.value = row;
|
||||
Object.assign(form, {
|
||||
version: row.version || "v1.0",
|
||||
title: row.title || "",
|
||||
template_id: row.template_id || null,
|
||||
status: Number(row.status) || 1,
|
||||
content: row.content || "",
|
||||
file_url: row.file_url || "",
|
||||
review_comment: row.review_comment || "",
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.title.trim()) {
|
||||
ElMessage.warning("请输入标书标题");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
bid_id: props.bidId,
|
||||
version: form.version,
|
||||
title: form.title,
|
||||
template_id: form.template_id || 0,
|
||||
status: form.status,
|
||||
content: form.content,
|
||||
file_url: form.file_url,
|
||||
review_comment: form.review_comment,
|
||||
};
|
||||
if (editing.value?.id) {
|
||||
await updateBidDocument(editing.value.id, payload);
|
||||
} else {
|
||||
await createBidDocument(payload);
|
||||
}
|
||||
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.version}」?`, "提示", { type: "warning" });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteBidDocument(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
function openPreview(row) {
|
||||
previewRow.value = row;
|
||||
previewVisible.value = true;
|
||||
}
|
||||
</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-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
min-height: 120px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="bid-expense" 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" />
|
||||
<template v-else>
|
||||
<el-table :data="list" stripe border>
|
||||
<el-table-column label="费用名称" min-width="150" show-overflow-tooltip prop="expense_name" />
|
||||
<el-table-column label="类型" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ expenseTypeText(row.expense_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatMoney(row.amount) }} {{ row.currency || "" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="paymentStatusTag(row.payment_status)" size="small">
|
||||
{{ paymentStatusText(row.payment_status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付日期" width="120" align="center">
|
||||
<template #default="{ row }">{{ formatDateOnly(row.payment_date) || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="说明" prop="description" min-width="130" 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>
|
||||
|
||||
<div class="expense-summary">
|
||||
<span>
|
||||
合计:<strong>{{ formatMoney(totalAmount) }}</strong>
|
||||
</span>
|
||||
<span>
|
||||
已付:<strong class="paid">{{ formatMoney(paidAmount) }}</strong>
|
||||
</span>
|
||||
<span>
|
||||
未付:<strong class="unpaid">{{ formatMoney(unpaidAmount) }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="登记投标费用" width="640px" :close-on-click-modal="false">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="费用类型">
|
||||
<el-select v-model="form.expense_type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in bidExpenseTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="费用名称" required>
|
||||
<el-input v-model="form.expense_name" placeholder="如:标书制作费" />
|
||||
</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.amount" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="币种">
|
||||
<el-select v-model="form.currency" style="width: 100%">
|
||||
<el-option label="人民币 CNY" value="CNY" />
|
||||
<el-option label="美元 USD" value="USD" />
|
||||
<el-option label="欧元 EUR" value="EUR" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="支付状态">
|
||||
<el-select v-model="form.payment_status" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in paymentStatusOptions"
|
||||
: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-date-picker
|
||||
v-model="form.payment_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="凭证链接">
|
||||
<el-input v-model="form.voucher_url" placeholder="发票/凭证文件地址(选填)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="费用说明">
|
||||
<el-input v-model="form.description" 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { getBidExpenseList, createBidExpense, deleteBidExpense } from "@/api/crmBidding";
|
||||
import {
|
||||
bidExpenseTypeOptions,
|
||||
expenseTypeText,
|
||||
paymentStatusOptions,
|
||||
paymentStatusText,
|
||||
paymentStatusTag,
|
||||
formatMoney,
|
||||
formatDateOnly,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
bidId: { 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 submitting = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
expense_type: 1,
|
||||
expense_name: "",
|
||||
amount: 0,
|
||||
currency: "CNY",
|
||||
payment_status: 0,
|
||||
payment_date: "",
|
||||
voucher_url: "",
|
||||
description: "",
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
const totalAmount = computed(() =>
|
||||
list.value.reduce((s, r) => s + (Number(r.amount) || 0), 0)
|
||||
);
|
||||
const paidAmount = computed(() =>
|
||||
list.value
|
||||
.filter((r) => Number(r.payment_status) === 1)
|
||||
.reduce((s, r) => s + (Number(r.amount) || 0), 0)
|
||||
);
|
||||
const unpaidAmount = computed(() =>
|
||||
list.value
|
||||
.filter((r) => Number(r.payment_status) === 0)
|
||||
.reduce((s, r) => s + (Number(r.amount) || 0), 0)
|
||||
);
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.bidId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidExpenseList({ bid_id: props.bidId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.expense_name.trim()) {
|
||||
ElMessage.warning("请输入费用名称");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await createBidExpense({
|
||||
bid_id: props.bidId,
|
||||
expense_type: form.expense_type,
|
||||
expense_name: form.expense_name,
|
||||
amount: form.amount,
|
||||
currency: form.currency,
|
||||
payment_status: form.payment_status,
|
||||
payment_date: form.payment_date,
|
||||
voucher_url: form.voucher_url,
|
||||
description: form.description,
|
||||
});
|
||||
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.expense_name}」?`, "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteBidExpense(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
</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;
|
||||
}
|
||||
.expense-summary {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.expense-summary .paid {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.expense-summary .unpaid {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="bid-file-list" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">投标文件</h3>
|
||||
<el-upload
|
||||
:http-request="handleUpload"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<el-button type="primary" :icon="Plus">上传文件</el-button>
|
||||
</el-upload>
|
||||
</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="版本" prop="version" width="80" align="center" />
|
||||
<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="上传时间" 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" @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 {
|
||||
getBidFileList,
|
||||
createBidFile,
|
||||
deleteBidFile,
|
||||
} from "@/api/crmBidding";
|
||||
import { formatDateTime } from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
bidId: { type: [Number, String], required: true },
|
||||
});
|
||||
const emit = defineEmits(["count"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.bidId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidFileList({ bid_id: props.bidId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch (e) {
|
||||
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) {
|
||||
// 这里简化为只记录元数据;真实场景应先调用文件上传接口获取 URL
|
||||
ElMessage.info("请先通过文件服务上传文件,再录入文件信息(已预留 API)");
|
||||
// 模拟创建记录
|
||||
try {
|
||||
await createBidFile({
|
||||
bid_id: props.bidId,
|
||||
file_name: option.file.name,
|
||||
file_url: URL.createObjectURL(option.file),
|
||||
file_size: option.file.size,
|
||||
file_ext: option.file.name.split(".").pop() || "",
|
||||
file_type: "1",
|
||||
});
|
||||
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 deleteBidFile(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 map = { 1: "投标文件", 2: "技术方案", 3: "商务方案", 4: "报价单", 5: "保证金", 7: "其他" };
|
||||
return map[t] || 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;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="bid-quote" 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="160" show-overflow-tooltip prop="quote_name" />
|
||||
<el-table-column label="报价金额" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatMoney(row.total_amount) }} {{ row.currency || "" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税率" width="90" align="center">
|
||||
<template #default="{ row }">{{ taxText(row.tax_rate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="利润率" width="100" align="center">
|
||||
<template #default="{ row }">{{ percentText(row.profit_rate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期" width="120" align="center">
|
||||
<template #default="{ row }">{{ formatDateOnly(row.valid_date) || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="bidQuoteStatusTag(row.status)" size="small">
|
||||
{{ bidQuoteStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提交人" prop="submit_user_name" width="100" align="center" />
|
||||
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openDetail(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="新建报价" width="820px" :close-on-click-modal="false">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="报价名称" required>
|
||||
<el-input v-model="form.quote_name" placeholder="如:一期报价方案" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="币种">
|
||||
<el-select v-model="form.currency" style="width: 100%">
|
||||
<el-option label="人民币 CNY" value="CNY" />
|
||||
<el-option label="美元 USD" value="USD" />
|
||||
<el-option label="欧元 EUR" value="EUR" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="税率(%)">
|
||||
<el-input-number v-model="form.tax_rate" :min="0" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="利润率(%)">
|
||||
<el-input-number v-model="form.profit_rate" :min="-100" :max="100" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="有效期">
|
||||
<el-date-picker
|
||||
v-model="form.valid_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">产品明细</el-divider>
|
||||
<el-table :data="form.products" border size="small">
|
||||
<el-table-column label="产品名称" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.name" size="small" placeholder="产品名称" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.qty" :min="0" :precision="0" size="small" controls-position="right" style="width: 100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单价" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.price" :min="0" :precision="2" size="small" controls-position="right" style="width: 100%" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="小计" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ ((Number(row.qty) || 0) * (Number(row.price) || 0)).toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="70" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" size="small" @click="removeProduct($index)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="product-footer">
|
||||
<el-button size="small" :icon="Plus" @click="addProduct">添加一行</el-button>
|
||||
<span class="sum-text">合计:{{ productsSum.toFixed(2) }}</span>
|
||||
</div>
|
||||
|
||||
<el-form-item label="报价总额" style="margin-top: 12px">
|
||||
<el-input-number v-model="form.total_amount" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="成本分析">
|
||||
<el-input v-model="form.cost_analysis" type="textarea" :rows="3" placeholder="成本构成说明" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in bidQuoteStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</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>
|
||||
|
||||
<el-dialog v-model="detailVisible" :title="detailRow?.quote_name || '报价明细'" width="720px">
|
||||
<el-descriptions v-if="detailRow" :column="2" border size="small">
|
||||
<el-descriptions-item label="报价金额">
|
||||
{{ formatMoney(detailRow.total_amount) }} {{ detailRow.currency || "" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="税率">{{ taxText(detailRow.tax_rate) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="利润率">
|
||||
{{ percentText(detailRow.profit_rate) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="有效期">
|
||||
{{ formatDateOnly(detailRow.valid_date) || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="bidQuoteStatusTag(detailRow.status)" size="small">
|
||||
{{ bidQuoteStatusText(detailRow.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="提交人">
|
||||
{{ detailRow.submit_user_name || "-" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-divider content-position="left">产品明细</el-divider>
|
||||
<el-table :data="detailProducts" border size="small">
|
||||
<el-table-column label="产品名称" prop="name" min-width="180" />
|
||||
<el-table-column label="数量" prop="qty" width="90" align="center" />
|
||||
<el-table-column label="单价" width="120" align="right">
|
||||
<template #default="{ row }">{{ Number(row.price || 0).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="小计" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ ((Number(row.qty) || 0) * (Number(row.price) || 0)).toFixed(2) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty><el-empty description="无产品明细" :image-size="60" /></template>
|
||||
</el-table>
|
||||
|
||||
<el-divider content-position="left">成本分析</el-divider>
|
||||
<div class="content-box">{{ detailRow?.cost_analysis || "—" }}</div>
|
||||
<el-divider content-position="left">备注</el-divider>
|
||||
<div class="content-box">{{ detailRow?.remark || "—" }}</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { getBidQuoteList, createBidQuote, deleteBidQuote } from "@/api/crmBidding";
|
||||
import {
|
||||
bidQuoteStatusOptions,
|
||||
bidQuoteStatusText,
|
||||
bidQuoteStatusTag,
|
||||
formatMoney,
|
||||
formatDateOnly,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
bidId: { 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 detailVisible = ref(false);
|
||||
const detailRow = ref(null);
|
||||
const submitting = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
quote_name: "",
|
||||
currency: "CNY",
|
||||
tax_rate: 0,
|
||||
profit_rate: 0,
|
||||
valid_date: "",
|
||||
total_amount: 0,
|
||||
cost_analysis: "",
|
||||
status: 1,
|
||||
remark: "",
|
||||
products: [{ name: "", qty: 1, price: 0 }],
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
const productsSum = computed(() =>
|
||||
(form.products || []).reduce(
|
||||
(sum, p) => sum + (Number(p.qty) || 0) * (Number(p.price) || 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
const detailProducts = computed(() => {
|
||||
const raw = detailRow.value?.products;
|
||||
if (!raw) return [];
|
||||
if (Array.isArray(raw)) return raw;
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.bidId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidQuoteList({ bid_id: props.bidId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function addProduct() {
|
||||
form.products.push({ name: "", qty: 1, price: 0 });
|
||||
}
|
||||
|
||||
function removeProduct(index) {
|
||||
form.products.splice(index, 1);
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.quote_name.trim()) {
|
||||
ElMessage.warning("请输入报价名称");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await createBidQuote({
|
||||
bid_id: props.bidId,
|
||||
quote_name: form.quote_name,
|
||||
currency: form.currency,
|
||||
tax_rate: form.tax_rate,
|
||||
profit_rate: form.profit_rate,
|
||||
valid_date: form.valid_date,
|
||||
total_amount: form.total_amount,
|
||||
cost_analysis: form.cost_analysis,
|
||||
status: form.status,
|
||||
remark: form.remark,
|
||||
products: JSON.stringify(
|
||||
(form.products || []).filter((p) => (p.name || "").trim() !== "")
|
||||
),
|
||||
});
|
||||
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.quote_name}」?`, "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteBidQuote(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
detailRow.value = row;
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
function taxText(v) {
|
||||
if (v === null || v === undefined || v === "") return "-";
|
||||
return `${Number(v)}%`;
|
||||
}
|
||||
|
||||
function percentText(v) {
|
||||
if (v === null || v === undefined || v === "") return "-";
|
||||
return `${Number(v)}%`;
|
||||
}
|
||||
</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;
|
||||
}
|
||||
.product-footer {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.sum-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.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,215 @@
|
||||
<template>
|
||||
<div class="bid-result" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">开标结果</h3>
|
||||
<el-button
|
||||
v-if="canSubmit"
|
||||
type="primary"
|
||||
:icon="Plus"
|
||||
@click="openSubmit()"
|
||||
>
|
||||
录入开标结果
|
||||
</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="结果" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="bidResultTypeTag(row.result_type)" size="small">
|
||||
{{ bidResultTypeText(row.result_type) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="我方排名" prop="our_ranking" width="100" align="center" />
|
||||
<el-table-column label="我方得分" prop="total_score" width="100" align="center" />
|
||||
<el-table-column label="竞争对手数" prop="competitor_count" width="100" align="center" />
|
||||
<el-table-column label="公告日期" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.announce_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="公告链接" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-link v-if="row.announce_url" :href="row.announce_url" target="_blank" type="primary">
|
||||
{{ row.announce_url }}
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="原因分析" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.reason || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="经验教训" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.lessons || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="录入时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.create_time) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="录入人" width="100" align="center">
|
||||
<template #default="{ row }">{{ row.create_user_name || "-" }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 录入开标结果弹窗 -->
|
||||
<el-dialog
|
||||
v-model="submitVisible"
|
||||
title="录入开标结果"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="开标结果" prop="result_type">
|
||||
<el-select v-model="form.result_type" style="width: 100%">
|
||||
<el-option v-for="i in bidResultTypeOptions" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="我方排名">
|
||||
<el-input v-model.number="form.our_ranking" type="number" placeholder="如 1、2、3..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="我方得分">
|
||||
<el-input v-model.number="form.total_score" type="number" placeholder="0-100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="竞争对手数">
|
||||
<el-input v-model.number="form.competitor_count" type="number" placeholder="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公告日期">
|
||||
<el-date-picker v-model="form.announce_date" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公告链接">
|
||||
<el-input v-model="form.announce_url" placeholder="招标方公告URL" />
|
||||
</el-form-item>
|
||||
<el-form-item label="原因分析">
|
||||
<el-input v-model="form.reason" type="textarea" :rows="2" placeholder="中标/落标原因分析" />
|
||||
</el-form-item>
|
||||
<el-form-item label="经验教训">
|
||||
<el-input v-model="form.lessons" type="textarea" :rows="2" placeholder="可复用的经验教训" />
|
||||
</el-form-item>
|
||||
<el-form-item label="下一步行动">
|
||||
<el-input v-model="form.next_step" placeholder="如:发起转合同审批" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getBidResultList,
|
||||
submitBidResult,
|
||||
} from "@/api/crmBidding";
|
||||
import {
|
||||
bidResultTypeText,
|
||||
bidResultTypeTag,
|
||||
bidResultTypeOptions,
|
||||
formatDateTime,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
bid: { type: Object, required: true },
|
||||
});
|
||||
const emit = defineEmits(["count", "action"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const submitVisible = ref(false);
|
||||
const submitting = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
const form = reactive({
|
||||
result_type: 1,
|
||||
our_ranking: 0,
|
||||
total_score: 0,
|
||||
competitor_count: 0,
|
||||
announce_date: "",
|
||||
announce_url: "",
|
||||
reason: "",
|
||||
lessons: "",
|
||||
next_step: "",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
result_type: [{ required: true, message: "请选择开标结果", trigger: "change" }],
|
||||
};
|
||||
|
||||
const canSubmit = computed(
|
||||
() => props.bid.status !== 7 && props.bid.locked !== 1
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
reload: loadList,
|
||||
});
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.bid?.id) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidResultList({ bid_id: props.bid.id });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch (e) {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openSubmit() {
|
||||
Object.assign(form, {
|
||||
result_type: 1,
|
||||
our_ranking: 0,
|
||||
total_score: 0,
|
||||
competitor_count: 0,
|
||||
announce_date: "",
|
||||
announce_url: "",
|
||||
reason: "",
|
||||
lessons: "",
|
||||
next_step: "",
|
||||
});
|
||||
submitVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await submitBidResult(props.bid.id, { ...form });
|
||||
ElMessage.success("保存成功");
|
||||
submitVisible.value = false;
|
||||
loadList();
|
||||
emit("action", { type: "saved" });
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</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,257 @@
|
||||
<template>
|
||||
<div class="bid-task-list" v-loading="loading">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">任务分工</h3>
|
||||
<el-button type="primary" :icon="Plus" @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 prop="task_name" />
|
||||
<el-table-column label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ taskTypeText(row.task_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" prop="owner_user_name" width="100" align="center" />
|
||||
<el-table-column label="起止日期" width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateOnly(row.start_date) }} ~ {{ formatDateOnly(row.end_date) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.progress || 0" :status="progressStatus(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="bidTaskStatusTag(row.status)" size="small">
|
||||
{{ bidTaskStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</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="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="form.id ? '编辑任务' : '新增任务'"
|
||||
width="600px"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="任务名称" prop="task_name">
|
||||
<el-input v-model="form.task_name" placeholder="请输入任务名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型">
|
||||
<el-select v-model="form.task_type" style="width: 100%">
|
||||
<el-option v-for="i in bidTaskTypeOptions" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="owner_user_id">
|
||||
<el-input v-model="form.owner_user_name" placeholder="负责人姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开始日期">
|
||||
<el-date-picker v-model="form.start_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="截止日期">
|
||||
<el-date-picker v-model="form.end_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="进度">
|
||||
<el-slider v-model="form.progress" :max="100" show-stops />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width: 100%">
|
||||
<el-option v-for="i in bidTaskStatusOptions" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务内容">
|
||||
<el-input v-model="form.content" type="textarea" :rows="3" placeholder="任务说明" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</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 {
|
||||
getBidTaskList,
|
||||
createBidTask,
|
||||
updateBidTask,
|
||||
deleteBidTask,
|
||||
} from "@/api/crmBidding";
|
||||
import {
|
||||
bidTaskStatusText,
|
||||
bidTaskStatusTag,
|
||||
bidTaskStatusOptions,
|
||||
bidTaskTypeOptions,
|
||||
formatDateOnly,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
bidId: { type: [Number, String], required: true },
|
||||
});
|
||||
const emit = defineEmits(["count"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const list = ref([]);
|
||||
const dialogVisible = ref(false);
|
||||
const submitting = ref(false);
|
||||
const formRef = ref();
|
||||
|
||||
const form = reactive({
|
||||
id: null,
|
||||
task_name: "",
|
||||
task_type: "1",
|
||||
owner_user_id: "",
|
||||
owner_user_name: "",
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
status: 0,
|
||||
progress: 0,
|
||||
content: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
task_name: [{ required: true, message: "请输入任务名称", trigger: "blur" }],
|
||||
};
|
||||
|
||||
defineExpose({ reload: loadList });
|
||||
|
||||
onMounted(loadList);
|
||||
|
||||
async function loadList() {
|
||||
if (!props.bidId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidTaskList({ bid_id: props.bidId });
|
||||
list.value = res?.data?.list || [];
|
||||
emit("count", list.value.length);
|
||||
} catch (e) {
|
||||
list.value = [];
|
||||
emit("count", 0);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function taskTypeText(t) {
|
||||
return bidTaskTypeOptions.find((i) => String(i.value) === String(t))?.label || "-";
|
||||
}
|
||||
|
||||
function progressStatus(row) {
|
||||
if (row.status === 2) return "success";
|
||||
if (row.status === 3) return "exception";
|
||||
if (row.progress >= 100) return "success";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(form, {
|
||||
id: null,
|
||||
task_name: "",
|
||||
task_type: "1",
|
||||
owner_user_id: "",
|
||||
owner_user_name: "",
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
status: 0,
|
||||
progress: 0,
|
||||
content: "",
|
||||
remark: "",
|
||||
});
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
Object.assign(form, row);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
bid_id: props.bidId,
|
||||
task_name: form.task_name,
|
||||
task_type: form.task_type,
|
||||
owner_user_id: form.owner_user_id,
|
||||
owner_user_name: form.owner_user_name,
|
||||
start_date: form.start_date,
|
||||
end_date: form.end_date,
|
||||
status: form.status,
|
||||
progress: form.progress,
|
||||
content: form.content,
|
||||
remark: form.remark,
|
||||
};
|
||||
if (form.id) {
|
||||
await updateBidTask(form.id, payload);
|
||||
} else {
|
||||
await createBidTask(payload);
|
||||
}
|
||||
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.task_name}」?`, "提示", { type: "warning" });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteBidTask(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
loadList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
</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,103 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="中标转合同"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="handleClose"
|
||||
>
|
||||
<el-alert
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="恭喜中标!转化后将创建合同并锁定投标记录,相关信息会自动带入。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="合同名称" prop="contract_name">
|
||||
<el-input v-model="form.contract_name" placeholder="留空将使用投标标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同编号">
|
||||
<el-input v-model="form.contract_no" placeholder="可留空自动生成" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同分类">
|
||||
<el-select v-model="form.contract_category" style="width: 100%">
|
||||
<el-option v-for="i in contractCategoryOptions" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="签约日期">
|
||||
<el-date-picker v-model="form.sign_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</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 { convertBidToContract } from "@/api/crmBidding";
|
||||
import { contractCategoryOptions } from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
bid: { type: Object, default: null },
|
||||
});
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
contract_name: "",
|
||||
contract_no: "",
|
||||
contract_category: "2",
|
||||
sign_date: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
contract_name: [{ required: true, message: "请输入合同名称", trigger: "blur" }],
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.bid) {
|
||||
form.contract_name = props.bid.title || "";
|
||||
form.contract_no = "";
|
||||
form.contract_category = "2";
|
||||
form.sign_date = "";
|
||||
form.remark = "";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value || !props.bid) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await convertBidToContract(props.bid.id, { ...form });
|
||||
ElMessage.success("合同创建成功");
|
||||
emit("success");
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "创建失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="中标转项目"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="handleClose"
|
||||
>
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="中标后转为正式执行项目,可关联合同、记录回款、跟进交付。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="项目名称" prop="project_name">
|
||||
<el-input v-model="form.project_name" placeholder="留空将使用投标标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目编号">
|
||||
<el-input v-model="form.project_no" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目金额">
|
||||
<el-input v-model.number="form.amount" placeholder="0.00">
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始日期">
|
||||
<el-date-picker v-model="form.start_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="结束日期">
|
||||
<el-date-picker v-model="form.end_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</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 { convertBidToProject } from "@/api/crmBidding";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
bid: { type: Object, default: null },
|
||||
});
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
project_name: "",
|
||||
project_no: "",
|
||||
amount: 0,
|
||||
start_date: "",
|
||||
end_date: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
project_name: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.bid) {
|
||||
form.project_name = props.bid.title || "";
|
||||
form.project_no = "";
|
||||
form.amount = props.bid.bid_amount || 0;
|
||||
form.start_date = "";
|
||||
form.end_date = "";
|
||||
form.remark = "";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value || !props.bid) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await convertBidToProject(props.bid.id, { ...form });
|
||||
ElMessage.success("项目创建成功");
|
||||
emit("success");
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "创建失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
title="编辑投标"
|
||||
width="780px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="投标标题" prop="title">
|
||||
<el-input v-model="form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="投标编号">
|
||||
<el-input v-model="form.bid_no" />
|
||||
</el-form-item>
|
||||
<el-form-item label="投标类型">
|
||||
<el-select v-model="form.bid_type" style="width: 100%">
|
||||
<el-option v-for="i in bidTypeOptions" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="招标方" prop="customer_name">
|
||||
<el-input v-model="form.customer_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发布单位">
|
||||
<el-input v-model="form.publish_org" />
|
||||
</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.number="form.bid_deposit" placeholder="0.00">
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="招标发布日期">
|
||||
<el-date-picker v-model="form.publish_date" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="报名截止">
|
||||
<el-date-picker v-model="form.register_end_date" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="投标截止">
|
||||
<el-date-picker v-model="form.bid_end_date" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开标时间">
|
||||
<el-date-picker v-model="form.open_bid_date" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="form.contact_person" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="form.contact_phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</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 { updateBid } from "@/api/crmBidding";
|
||||
import { bidTypeOptions } from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
bid: { type: Object, default: null },
|
||||
});
|
||||
const emit = defineEmits(["update:visible", "success"]);
|
||||
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
const form = reactive({
|
||||
title: "",
|
||||
bid_no: "",
|
||||
bid_type: "1",
|
||||
customer_name: "",
|
||||
publish_org: "",
|
||||
bid_amount: 0,
|
||||
bid_deposit: 0,
|
||||
publish_date: "",
|
||||
register_end_date: "",
|
||||
bid_end_date: "",
|
||||
open_bid_date: "",
|
||||
contact_person: "",
|
||||
contact_phone: "",
|
||||
description: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: "请输入投标标题", trigger: "blur" }],
|
||||
customer_name: [{ required: true, message: "请输入招标方名称", trigger: "blur" }],
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val && props.bid) {
|
||||
const b = props.bid;
|
||||
form.title = b.title || "";
|
||||
form.bid_no = b.bid_no || "";
|
||||
form.bid_type = b.bid_type || "1";
|
||||
form.customer_name = b.customer_name || "";
|
||||
form.publish_org = b.publish_org || "";
|
||||
form.bid_amount = b.bid_amount || 0;
|
||||
form.bid_deposit = b.bid_deposit || 0;
|
||||
form.publish_date = b.publish_date || "";
|
||||
form.register_end_date = b.register_end_date || "";
|
||||
form.bid_end_date = b.bid_end_date || "";
|
||||
form.open_bid_date = b.open_bid_date || "";
|
||||
form.contact_person = b.contact_person || "";
|
||||
form.contact_phone = b.contact_phone || "";
|
||||
form.description = b.description || "";
|
||||
form.remark = b.remark || "";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value || !props.bid) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await updateBid(props.bid.id, { ...form });
|
||||
ElMessage.success("保存成功");
|
||||
emit("success");
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="dialogTitle"
|
||||
width="780px"
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="handleClose"
|
||||
>
|
||||
<el-alert
|
||||
v-if="mode !== 'independent'"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="modeTip"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<el-form-item label="投标标题" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入投标标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="投标编号">
|
||||
<el-input v-model="form.bid_no" placeholder="可留空自动生成" />
|
||||
</el-form-item>
|
||||
<el-form-item label="投标类型">
|
||||
<el-select v-model="form.bid_type" style="width: 100%">
|
||||
<el-option v-for="i in bidTypeOptions" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="招标方" prop="customer_name">
|
||||
<el-input v-model="form.customer_name" placeholder="请输入招标方名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发布单位">
|
||||
<el-input v-model="form.publish_org" placeholder="请输入招标信息发布单位" />
|
||||
</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.number="form.bid_deposit" placeholder="0.00">
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<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-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-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-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-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.description" 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 {
|
||||
createBid,
|
||||
createBidFromBusiness,
|
||||
createBidFromProject,
|
||||
} from "@/api/crmBidding";
|
||||
import { bidTypeOptions } from "@/views/apps/crm/dict";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
mode: { type: String, default: "independent" }, // independent | from-business | from-project
|
||||
prefill: { type: Object, default: null },
|
||||
});
|
||||
const emit = defineEmits(["update:visible", "success", "cancel"]);
|
||||
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (props.mode === "from-business") return "从商机发起投标";
|
||||
if (props.mode === "from-project") return "从项目发起投标";
|
||||
return "新建投标";
|
||||
});
|
||||
|
||||
const modeTip = computed(() => {
|
||||
if (props.mode === "from-business") {
|
||||
return "将基于商机自动带入招标方、客户信息,商机不会被锁定。";
|
||||
}
|
||||
if (props.mode === "from-project") {
|
||||
return "将基于项目自动带入项目名称、金额等信息,便于一站式跟进。";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
const form = reactive({
|
||||
title: "",
|
||||
bid_no: "",
|
||||
bid_type: "1",
|
||||
customer_name: "",
|
||||
publish_org: "",
|
||||
bid_amount: 0,
|
||||
bid_deposit: 0,
|
||||
publish_date: "",
|
||||
register_end_date: "",
|
||||
bid_end_date: "",
|
||||
open_bid_date: "",
|
||||
contact_person: "",
|
||||
contact_phone: "",
|
||||
description: "",
|
||||
remark: "",
|
||||
});
|
||||
|
||||
const rules = {
|
||||
title: [{ required: true, message: "请输入投标标题", trigger: "blur" }],
|
||||
customer_name: [{ required: true, message: "请输入招标方名称", trigger: "blur" }],
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
if (val) {
|
||||
// 重置
|
||||
Object.assign(form, {
|
||||
title: "",
|
||||
bid_no: "",
|
||||
bid_type: "1",
|
||||
customer_name: "",
|
||||
publish_org: "",
|
||||
bid_amount: 0,
|
||||
bid_deposit: 0,
|
||||
publish_date: "",
|
||||
register_end_date: "",
|
||||
bid_end_date: "",
|
||||
open_bid_date: "",
|
||||
contact_person: "",
|
||||
contact_phone: "",
|
||||
description: "",
|
||||
remark: "",
|
||||
});
|
||||
// 从 prefill 填充
|
||||
const p = props.prefill;
|
||||
if (props.mode === "from-business" && p?.business) {
|
||||
form.title = p.business.business_name || "";
|
||||
form.customer_name = p.business.customer_name || "";
|
||||
form.contact_person = p.business.contact_person || "";
|
||||
form.contact_phone = p.business.contact_phone || "";
|
||||
} else if (props.mode === "from-project" && p?.project) {
|
||||
form.title = p.project.project_name || "";
|
||||
form.bid_amount = p.project.amount || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function handleClose() {
|
||||
emit("update:visible", false);
|
||||
emit("cancel");
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value) return;
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
let res;
|
||||
if (props.mode === "from-business" && props.prefill?.business?.id) {
|
||||
res = await createBidFromBusiness({
|
||||
business_id: props.prefill.business.id,
|
||||
title: form.title,
|
||||
customer_name: form.customer_name,
|
||||
publish_org: form.publish_org,
|
||||
bid_type: form.bid_type,
|
||||
bid_amount: form.bid_amount,
|
||||
bid_end_date: form.bid_end_date,
|
||||
open_bid_date: form.open_bid_date,
|
||||
description: form.description,
|
||||
remark: form.remark,
|
||||
});
|
||||
} else if (props.mode === "from-project" && props.prefill?.project?.id) {
|
||||
res = await createBidFromProject({
|
||||
project_id: props.prefill.project.id,
|
||||
title: form.title,
|
||||
bid_type: form.bid_type,
|
||||
bid_amount: form.bid_amount,
|
||||
bid_end_date: form.bid_end_date,
|
||||
open_bid_date: form.open_bid_date,
|
||||
description: form.description,
|
||||
remark: form.remark,
|
||||
});
|
||||
} else {
|
||||
res = await createBid({ ...form });
|
||||
}
|
||||
ElMessage.success("创建成功");
|
||||
emit("success", res?.data);
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "创建失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-input-number,
|
||||
.el-date-picker {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="visible"
|
||||
:title="bid?.title || '投标详情'"
|
||||
direction="rtl"
|
||||
size="1000px"
|
||||
@update:model-value="handleClose"
|
||||
@opened="loadAll"
|
||||
>
|
||||
<div v-if="bid" class="bid-detail">
|
||||
<el-tabs v-model="activeTab" tab-position="left" class="detail-tabs">
|
||||
<!-- 基本信息 -->
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
<BidBaseInfo :bid="bid" @action="handleAction" />
|
||||
<el-divider />
|
||||
<h4 class="block-title">流程进度</h4>
|
||||
<BidTimeline kind="bid" :status="bid.status" :times="bid" />
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 立项审批 -->
|
||||
<el-tab-pane :label="`立项审批 (${counts.approval})`" name="approval">
|
||||
<BidApproval
|
||||
ref="approvalRef"
|
||||
:bid-id="bid.id"
|
||||
:disabled="locked"
|
||||
@count="counts.approval = $event"
|
||||
@approved="refreshSelf"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 标书版本 -->
|
||||
<el-tab-pane :label="`标书版本 (${counts.document})`" name="document">
|
||||
<BidDocumentList
|
||||
ref="documentRef"
|
||||
:bid-id="bid.id"
|
||||
:disabled="locked"
|
||||
@count="counts.document = $event"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 投标报价 -->
|
||||
<el-tab-pane :label="`投标报价 (${counts.quote})`" name="quote">
|
||||
<BidQuote
|
||||
ref="quoteRef"
|
||||
:bid-id="bid.id"
|
||||
:disabled="locked"
|
||||
@count="counts.quote = $event"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 竞争对手 -->
|
||||
<el-tab-pane :label="`竞争对手 (${counts.competitor})`" name="competitor">
|
||||
<BidCompetitor
|
||||
ref="competitorRef"
|
||||
:bid-id="bid.id"
|
||||
:disabled="locked"
|
||||
@count="counts.competitor = $event"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 投标费用 -->
|
||||
<el-tab-pane :label="`投标费用 (${counts.expense})`" name="expense">
|
||||
<BidExpenseList
|
||||
ref="expenseRef"
|
||||
:bid-id="bid.id"
|
||||
:disabled="locked"
|
||||
@count="counts.expense = $event"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 开标结果 -->
|
||||
<el-tab-pane :label="`开标结果 (${counts.result})`" name="result">
|
||||
<BidResult
|
||||
ref="resultRef"
|
||||
:bid="bid"
|
||||
@count="counts.result = $event"
|
||||
@action="handleAction"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 投标文件 -->
|
||||
<el-tab-pane :label="`投标文件 (${counts.file})`" name="file">
|
||||
<BidFileList
|
||||
ref="fileRef"
|
||||
:bid-id="bid.id"
|
||||
@count="counts.file = $event"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 任务分工 -->
|
||||
<el-tab-pane :label="`任务分工 (${counts.task})`" name="task">
|
||||
<BidTaskList
|
||||
ref="taskRef"
|
||||
:bid-id="bid.id"
|
||||
@count="counts.task = $event"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getBidDetail } from "@/api/crmBidding";
|
||||
import BidBaseInfo from "./components/BidBaseInfo.vue";
|
||||
import BidResult from "./components/BidResult.vue";
|
||||
import BidFileList from "./components/BidFileList.vue";
|
||||
import BidTaskList from "./components/BidTaskList.vue";
|
||||
import BidApproval from "./components/BidApproval.vue";
|
||||
import BidDocumentList from "./components/BidDocumentList.vue";
|
||||
import BidQuote from "./components/BidQuote.vue";
|
||||
import BidCompetitor from "./components/BidCompetitor.vue";
|
||||
import BidExpenseList from "./components/BidExpenseList.vue";
|
||||
import BidTimeline from "../components/BidTimeline.vue";
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
bid: { type: Object, default: null },
|
||||
});
|
||||
const emit = defineEmits(["update:visible", "refresh", "action"]);
|
||||
|
||||
const activeTab = ref("basic");
|
||||
const counts = reactive({
|
||||
approval: 0,
|
||||
document: 0,
|
||||
quote: 0,
|
||||
competitor: 0,
|
||||
expense: 0,
|
||||
result: 0,
|
||||
file: 0,
|
||||
task: 0,
|
||||
});
|
||||
|
||||
const approvalRef = ref();
|
||||
const documentRef = ref();
|
||||
const quoteRef = ref();
|
||||
const competitorRef = ref();
|
||||
const expenseRef = ref();
|
||||
const resultRef = ref();
|
||||
const fileRef = ref();
|
||||
const taskRef = ref();
|
||||
|
||||
const locked = computed(() => props.bid?.locked === 1 || Number(props.bid?.status) === 7);
|
||||
|
||||
function handleClose() {
|
||||
emit("update:visible", false);
|
||||
}
|
||||
|
||||
async function refreshSelf() {
|
||||
if (!props.bid?.id) return;
|
||||
// 重新拉取主表数据,确保看到最新状态
|
||||
try {
|
||||
const res = await getBidDetail(props.bid.id);
|
||||
if (res?.data) {
|
||||
Object.assign(props.bid, res.data);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
emit("refresh");
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
if (!props.bid?.id) return;
|
||||
await refreshSelf();
|
||||
Object.keys(counts).forEach((k) => {
|
||||
counts[k] = 0;
|
||||
});
|
||||
// 触发子组件 reload
|
||||
approvalRef.value?.reload?.();
|
||||
documentRef.value?.reload?.();
|
||||
quoteRef.value?.reload?.();
|
||||
competitorRef.value?.reload?.();
|
||||
expenseRef.value?.reload?.();
|
||||
resultRef.value?.reload?.();
|
||||
fileRef.value?.reload?.();
|
||||
taskRef.value?.reload?.();
|
||||
}
|
||||
|
||||
async function handleAction({ type, row }) {
|
||||
if (type === "saved") {
|
||||
await loadAll();
|
||||
emit("refresh");
|
||||
return;
|
||||
}
|
||||
emit("action", { type, row: props.bid });
|
||||
}
|
||||
|
||||
defineExpose({ reload: loadAll });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bid-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,350 @@
|
||||
<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="Folder" @click="goLibrary">资料库</el-button>
|
||||
<el-button :icon="DataAnalysis" @click="goStatistics">统计</el-button>
|
||||
<el-dropdown @command="handleCreateCommand">
|
||||
<el-button type="primary" :icon="Plus">
|
||||
新建投标<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="from-business">
|
||||
<el-icon><Aim /></el-icon>从商机发起
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="from-project">
|
||||
<el-icon><Box /></el-icon>从项目发起
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="independent" divided>
|
||||
<el-icon><EditPen /></el-icon>独立创建
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</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 label="投标状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 140px">
|
||||
<el-option v-for="i in bidStatusOptions" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="来源">
|
||||
<el-select v-model="filters.source_type" clearable placeholder="全部" style="width: 130px">
|
||||
<el-option v-for="i in bidSourceTypeOptions" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="中标状态">
|
||||
<el-select v-model="filters.win_status" clearable placeholder="全部" style="width: 140px">
|
||||
<el-option v-for="i in bidResultTypeOptions" :key="i.value" :label="i.label" :value="i.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="table-container" v-loading="loading">
|
||||
<el-table :data="tableData" stripe border row-key="id">
|
||||
<el-table-column label="状态" width="100" align="center" fixed>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="bidStatusTag(row.status)" size="small">
|
||||
{{ bidStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="投标标题" min-width="180" show-overflow-tooltip fixed>
|
||||
<template #default="{ row }">
|
||||
<span class="name-link" @click="openDetail(row)">{{ row.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bid_no" label="投标编号" width="140" show-overflow-tooltip />
|
||||
<el-table-column label="来源" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.source_type === 1" size="small" type="warning">商机</el-tag>
|
||||
<el-tag v-else-if="row.source_type === 2" size="small" type="primary">项目</el-tag>
|
||||
<el-tag v-else size="small" type="info" effect="plain">独立</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="customer_name" label="招标方" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="publish_org" label="发布单位" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="投标报价" width="130" align="right">
|
||||
<template #default="{ row }">{{ formatMoney(row.bid_amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="投标截止" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.bid_end_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开标时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ formatDateTime(row.open_bid_date) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" width="100">
|
||||
<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" :disabled="row.locked === 1" @click="openEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button link type="success" size="small" :disabled="row.win_status !== 1 || row.contract_id" @click="openConvertContract(row)">
|
||||
转合同
|
||||
</el-button>
|
||||
<el-button link type="warning" size="small" :disabled="row.win_status !== 1 || row.new_project_id" @click="openConvertProject(row)">
|
||||
转项目
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" :disabled="row.locked === 1" @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>
|
||||
|
||||
<!-- 弹窗组件 -->
|
||||
<BidCreate
|
||||
v-model:visible="createVisible"
|
||||
:mode="createMode"
|
||||
:prefill="createPrefill"
|
||||
@success="handleCreateSuccess"
|
||||
@cancel="createVisible = false"
|
||||
/>
|
||||
<BidEdit
|
||||
v-model:visible="editVisible"
|
||||
:bid="currentRow"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
<BidDetail
|
||||
v-model:visible="detailVisible"
|
||||
:bid="currentRow"
|
||||
@refresh="fetchList"
|
||||
@action="handleDetailAction"
|
||||
/>
|
||||
<BidConvertContract
|
||||
v-model:visible="convertContractVisible"
|
||||
:bid="currentRow"
|
||||
@success="handleConvertSuccess"
|
||||
/>
|
||||
<BidConvertProject
|
||||
v-model:visible="convertProjectVisible"
|
||||
:bid="currentRow"
|
||||
@success="handleConvertSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
Refresh,
|
||||
ArrowDown,
|
||||
Aim,
|
||||
Box,
|
||||
EditPen,
|
||||
Folder,
|
||||
DataAnalysis,
|
||||
} from "@element-plus/icons-vue";
|
||||
|
||||
import {
|
||||
getBidList,
|
||||
deleteBid,
|
||||
} from "@/api/crmBidding";
|
||||
import {
|
||||
bidStatusText,
|
||||
bidStatusTag,
|
||||
formatMoney,
|
||||
formatDateTime,
|
||||
bidStatusOptions,
|
||||
bidSourceTypeOptions,
|
||||
bidResultTypeOptions,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
import BidCreate from "./create.vue";
|
||||
import BidEdit from "./components/edit.vue";
|
||||
import BidDetail from "./detail.vue";
|
||||
import BidConvertContract from "./components/convert-contract.vue";
|
||||
import BidConvertProject from "./components/convert-project.vue";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const filters = reactive({
|
||||
keyword: "",
|
||||
status: "",
|
||||
source_type: "",
|
||||
win_status: "",
|
||||
});
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
|
||||
const createVisible = ref(false);
|
||||
const createMode = ref("independent"); // independent | from-business | from-project
|
||||
const createPrefill = ref(null);
|
||||
|
||||
const editVisible = ref(false);
|
||||
const detailVisible = ref(false);
|
||||
const convertContractVisible = ref(false);
|
||||
const convertProjectVisible = ref(false);
|
||||
const currentRow = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
// 从首页快速入口筛选
|
||||
const filter = route.query.filter;
|
||||
if (filter === "draft") filters.status = 0;
|
||||
else if (filter === "bidding") filters.status = 2;
|
||||
else if (filter === "won") filters.win_status = 1;
|
||||
fetchList();
|
||||
// 从到期提醒跳转:携带 id 直接打开详情
|
||||
const qid = route.query.id;
|
||||
if (qid) {
|
||||
openDetail({ id: Number(qid) });
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidList({
|
||||
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.source_type = "";
|
||||
filters.win_status = "";
|
||||
pagination.page = 1;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleCreateCommand(cmd) {
|
||||
createMode.value = cmd;
|
||||
createPrefill.value = null;
|
||||
createVisible.value = true;
|
||||
}
|
||||
|
||||
function openDetail(row) {
|
||||
currentRow.value = row;
|
||||
detailVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
currentRow.value = row;
|
||||
editVisible.value = true;
|
||||
}
|
||||
|
||||
function openConvertContract(row) {
|
||||
currentRow.value = row;
|
||||
convertContractVisible.value = true;
|
||||
}
|
||||
|
||||
function openConvertProject(row) {
|
||||
currentRow.value = row;
|
||||
convertProjectVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除投标「${row.title}」吗?删除后不可恢复。`,
|
||||
"删除确认",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteBid(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateSuccess() {
|
||||
createVisible.value = false;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleEditSuccess() {
|
||||
editVisible.value = false;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleConvertSuccess() {
|
||||
convertContractVisible.value = false;
|
||||
convertProjectVisible.value = false;
|
||||
fetchList();
|
||||
}
|
||||
|
||||
function handleDetailAction({ type, row }) {
|
||||
currentRow.value = row;
|
||||
if (type === "edit") editVisible.value = true;
|
||||
else if (type === "contract") convertContractVisible.value = true;
|
||||
else if (type === "project") convertProjectVisible.value = true;
|
||||
}
|
||||
|
||||
function goLibrary() {
|
||||
router.push("/apps/crm/bidding/library/template");
|
||||
}
|
||||
|
||||
function goStatistics() {
|
||||
router.push("/apps/crm/bidding/statistics/bid");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped src="../../styles/crm-page.less"></style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<el-timeline class="bid-timeline">
|
||||
<el-timeline-item
|
||||
v-for="node in nodes"
|
||||
:key="node.status"
|
||||
:type="node.type"
|
||||
:hollow="node.hollow"
|
||||
:timestamp="node.time"
|
||||
placement="top"
|
||||
>
|
||||
<div class="node-title">{{ node.label }}</div>
|
||||
<div v-if="node.desc" class="node-desc">{{ node.desc }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
bidStatusText,
|
||||
tenderStatusText,
|
||||
formatBidTime
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
/**
|
||||
* 招投标状态时间线
|
||||
* kind: bid | tender
|
||||
* 只展示已到达的状态节点,未到达的节点不渲染(避免时间线过长)。
|
||||
*/
|
||||
const props = defineProps({
|
||||
kind: { type: String, default: "bid" },
|
||||
status: { type: [Number, String], default: 0 },
|
||||
times: { type: Object, default: () => ({}) }
|
||||
});
|
||||
|
||||
// 投标节点顺序与对应时间字段
|
||||
const BID_NODES = [
|
||||
{ status: 0, label: "创建草稿", timeKey: "create_time" },
|
||||
{ status: 1, label: "提交立项审批", timeKey: "submit_time" },
|
||||
{ status: 2, label: "投标中", timeKey: "bid_start_date" },
|
||||
{ status: 3, label: "已开标", timeKey: "open_bid_date" },
|
||||
{ status: 4, label: "中标", timeKey: "result_time" },
|
||||
{ status: 5, label: "落标", timeKey: "result_time" },
|
||||
{ status: 6, label: "已转合同", timeKey: "convert_time" },
|
||||
{ status: 7, label: "已归档", timeKey: "archive_time" }
|
||||
];
|
||||
|
||||
const TENDER_NODES = [
|
||||
{ status: 0, label: "创建草稿", timeKey: "create_time" },
|
||||
{ status: 1, label: "发布公告", timeKey: "publish_date" },
|
||||
{ status: 2, label: "报名中", timeKey: "register_start_date" },
|
||||
{ status: 3, label: "投标中", timeKey: "bid_start_date" },
|
||||
{ status: 4, label: "开标", timeKey: "open_bid_date" },
|
||||
{ status: 5, label: "评标中", timeKey: "evaluation_time" },
|
||||
{ status: 6, label: "已定标", timeKey: "winner_date" },
|
||||
{ status: 7, label: "已归档", timeKey: "archive_time" }
|
||||
];
|
||||
|
||||
const nodes = computed(() => {
|
||||
const current = Number(props.status || 0);
|
||||
const table = props.kind === "tender" ? TENDER_NODES : BID_NODES;
|
||||
// 落标(5)时不应展示中标(4)节点
|
||||
const skipped = props.kind === "bid" && current === 5 ? [4] : [];
|
||||
return table
|
||||
.filter(n => !skipped.includes(n.status))
|
||||
.filter(n => n.status <= current || (current === 6 && n.status === 6))
|
||||
.map(n => {
|
||||
const reached = n.status <= current;
|
||||
return {
|
||||
status: n.status,
|
||||
label: props.kind === "tender"
|
||||
? tenderStatusText(n.status)
|
||||
: bidStatusText(n.status),
|
||||
time: formatBidTime(props.times?.[n.timeKey]),
|
||||
type: n.status === current ? "primary" : reached ? "success" : "info",
|
||||
hollow: !reached
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bid-timeline {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.node-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.node-desc {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<div class="source-select">
|
||||
<el-radio-group v-model="sourceType" class="source-radio">
|
||||
<el-radio-button :value="0">独立创建</el-radio-button>
|
||||
<el-radio-button :value="1">来自商机</el-radio-button>
|
||||
<el-radio-button :value="2">来自项目</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<div v-if="sourceType === 1" class="source-picker">
|
||||
<el-select
|
||||
v-model="sourceId"
|
||||
filterable
|
||||
remote
|
||||
clearable
|
||||
placeholder="输入商机名称搜索"
|
||||
:remote-method="searchBusiness"
|
||||
:loading="loading"
|
||||
style="width: 320px"
|
||||
@change="handlePick"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in businessOptions"
|
||||
:key="item.id"
|
||||
:label="item.business_name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-if="pickedLabel" class="picked-tip">
|
||||
已选:{{ pickedLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="sourceType === 2" class="source-picker">
|
||||
<el-input
|
||||
v-model.number="sourceId"
|
||||
placeholder="请输入项目 ID"
|
||||
clearable
|
||||
style="width: 320px"
|
||||
@change="handlePick"
|
||||
/>
|
||||
<span class="picked-tip muted">
|
||||
项目来源请填写项目 ID,提交后由后端带出项目信息
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { getBusinessList } from "@/api/crmPipeline";
|
||||
|
||||
/**
|
||||
* 投标来源选择:独立创建 / 来自商机 / 来自项目
|
||||
* 商机走远程搜索;项目按 ID 关联(项目模块未提供通用搜索接口)。
|
||||
*/
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, default: () => ({ source_type: 0, source_id: null }) }
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue", "pick"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const sourceType = ref(props.modelValue?.source_type ?? 0);
|
||||
const sourceId = ref(props.modelValue?.source_id ?? null);
|
||||
const businessOptions = ref([]);
|
||||
const pickedLabel = ref("");
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
v => {
|
||||
sourceType.value = v?.source_type ?? 0;
|
||||
sourceId.value = v?.source_id ?? null;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch([sourceType, sourceId], () => {
|
||||
emit("update:modelValue", {
|
||||
source_type: sourceType.value,
|
||||
source_id: sourceId.value || null
|
||||
});
|
||||
});
|
||||
|
||||
async function searchBusiness(keyword) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBusinessList({ keyword, page: 1, pageSize: 20 });
|
||||
businessOptions.value = res?.data?.list || [];
|
||||
} catch {
|
||||
businessOptions.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePick(val) {
|
||||
if (sourceType.value === 1) {
|
||||
const hit = businessOptions.value.find(b => b.id === val);
|
||||
pickedLabel.value = hit?.business_name || "";
|
||||
emit("pick", { type: 1, row: hit || null });
|
||||
} else {
|
||||
emit("pick", { type: 2, id: val });
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时拉一次商机列表,便于快速选择
|
||||
searchBusiness("");
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.source-select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.source-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.picked-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.picked-tip.muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<el-tag :type="tagType" size="small" effect="light">{{ text }}</el-tag>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import {
|
||||
bidStatusText,
|
||||
bidStatusTag,
|
||||
tenderStatusText,
|
||||
tenderStatusTag
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
/**
|
||||
* 招投标状态标签
|
||||
* kind: bid(投标)| tender(招标)
|
||||
*/
|
||||
const props = defineProps({
|
||||
kind: { type: String, default: "bid" },
|
||||
status: { type: [Number, String], default: 0 }
|
||||
});
|
||||
|
||||
const text = computed(() =>
|
||||
props.kind === "tender"
|
||||
? tenderStatusText(props.status)
|
||||
: bidStatusText(props.status)
|
||||
);
|
||||
|
||||
const tagType = computed(() =>
|
||||
props.kind === "tender"
|
||||
? tenderStatusTag(props.status)
|
||||
: bidStatusTag(props.status)
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-tag :type="tagType" size="small" effect="plain">{{ text }}</el-tag>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { tenderTypeText, tenderTypeTag } from "@/views/apps/crm/dict";
|
||||
|
||||
/** 招标类型标签:公开招标 / 邀请招标 / 竞争性谈判 / 询价 / 单一来源 */
|
||||
const props = defineProps({
|
||||
type: { type: [Number, String], default: "" }
|
||||
});
|
||||
|
||||
const text = computed(() => tenderTypeText(props.type));
|
||||
const tagType = computed(() => tenderTypeTag(props.type));
|
||||
</script>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<div class="bidding-home crm-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>招投标管理</h2>
|
||||
<p>支持「我要招标」与「我要投标」双场景,与商机/项目/合同无缝衔接</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="entry-cards">
|
||||
<div class="entry-card" @click="goBid">
|
||||
<div class="card-icon" :style="{ background: 'linear-gradient(135deg, #409EFF, #67C23A)' }">
|
||||
<el-icon :size="48"><Trophy /></el-icon>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3>我要投标</h3>
|
||||
<p class="card-desc">作为乙方参与,从商机或项目发起投标,沉淀标书与报价,中标可一键转合同/项目</p>
|
||||
<ul class="card-features">
|
||||
<li>支持从商机/项目快速发起</li>
|
||||
<li>任务分工 + 标书版本管理</li>
|
||||
<li>报价/竞争对手/费用全记录</li>
|
||||
<li>中标自动转合同/项目</li>
|
||||
</ul>
|
||||
</div>
|
||||
<el-button type="primary" :icon="ArrowRight">进入投标</el-button>
|
||||
</div>
|
||||
|
||||
<div class="entry-card" @click="goTender">
|
||||
<div class="card-icon" :style="{ background: 'linear-gradient(135deg, #E6A23C, #F56C6C)' }">
|
||||
<el-icon :size="48"><Document /></el-icon>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3>我要招标</h3>
|
||||
<p class="card-desc">作为甲方发布招标、收集投标、评标定标,支持流标/废标/转合同全流程</p>
|
||||
<ul class="card-features">
|
||||
<li>招标公告发布与版本管理</li>
|
||||
<li>投标方报名审核</li>
|
||||
<li>开标评标多维度打分</li>
|
||||
<li>定标自动转合同</li>
|
||||
</ul>
|
||||
</div>
|
||||
<el-button type="warning" :icon="ArrowRight">进入招标</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-stats">
|
||||
<h3 class="section-title">快速入口</h3>
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="hover" class="quick-card" @click="goBid('draft')">
|
||||
<div class="quick-content">
|
||||
<el-icon :size="28" color="#909399"><EditPen /></el-icon>
|
||||
<div class="quick-text">
|
||||
<div class="quick-title">草稿投标</div>
|
||||
<div class="quick-desc">继续未完成的投标</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="hover" class="quick-card" @click="goBid('bidding')">
|
||||
<div class="quick-content">
|
||||
<el-icon :size="28" color="#E6A23C"><Loading /></el-icon>
|
||||
<div class="quick-text">
|
||||
<div class="quick-title">进行中的投标</div>
|
||||
<div class="quick-desc">跟踪进度</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="hover" class="quick-card" @click="goBid('won')">
|
||||
<div class="quick-content">
|
||||
<el-icon :size="28" color="#67C23A"><CircleCheck /></el-icon>
|
||||
<div class="quick-text">
|
||||
<div class="quick-title">已中标</div>
|
||||
<div class="quick-desc">转合同/转项目</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="hover" class="quick-card" @click="goLibrary">
|
||||
<div class="quick-content">
|
||||
<el-icon :size="28" color="#409EFF"><Files /></el-icon>
|
||||
<div class="quick-text">
|
||||
<div class="quick-title">公共资料库</div>
|
||||
<div class="quick-desc">标书模板 + 资质文件</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="reminder-section">
|
||||
<div class="section-title-row">
|
||||
<h3 class="section-title">到期提醒</h3>
|
||||
<el-select v-model="reminderDays" size="small" style="width: 120px" @change="loadReminders">
|
||||
<el-option :value="3" label="近 3 天" />
|
||||
<el-option :value="7" label="近 7 天" />
|
||||
<el-option :value="15" label="近 15 天" />
|
||||
<el-option :value="30" label="近 30 天" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<el-card v-loading="reminderLoading" shadow="never" class="reminder-card">
|
||||
<el-empty v-if="!reminderList.length" :description="'近 ' + reminderDays + ' 天暂无到期事项'" :image-size="60" />
|
||||
<ul v-else class="reminder-list">
|
||||
<li
|
||||
v-for="item in reminderList"
|
||||
:key="item.biz_type + item.biz_id + item.kind"
|
||||
class="reminder-item"
|
||||
@click="goReminder(item)"
|
||||
>
|
||||
<el-tag :type="reminderTagType(item.level)" size="small" effect="dark">
|
||||
{{ item.level === 'danger' ? '紧急' : item.level === 'warning' ? '提醒' : '关注' }}
|
||||
</el-tag>
|
||||
<div class="reminder-main">
|
||||
<div class="reminder-title">
|
||||
<span class="reminder-biz">{{ bizTypeText(item.biz_type) }}</span>
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div class="reminder-meta">
|
||||
{{ item.kind_name }} · 到期 {{ item.due_date }} · 负责人 {{ item.owner_user_name || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="reminder-days" :class="'is-' + item.level">
|
||||
{{ item.remain_days < 0 ? '已逾期 ' + Math.abs(item.remain_days) + ' 天' : '剩 ' + item.remain_days + ' 天' }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Trophy, Document, ArrowRight, EditPen, Loading, CircleCheck, Files } from "@element-plus/icons-vue";
|
||||
import { getBiddingReminders } from "@/api/crmBidding";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function goBid(filter) {
|
||||
const query = filter ? { filter } : {};
|
||||
router.push({ path: "/apps/crm/bidding/bid", query });
|
||||
}
|
||||
|
||||
function goTender() {
|
||||
router.push("/apps/crm/bidding/tender");
|
||||
}
|
||||
|
||||
function goLibrary() {
|
||||
router.push("/apps/crm/bidding/library/template");
|
||||
}
|
||||
|
||||
/* ====================== 到期提醒(开发计划 4.1) ====================== */
|
||||
const reminderDays = ref(7);
|
||||
const reminderList = ref([]);
|
||||
const reminderLoading = ref(false);
|
||||
|
||||
async function loadReminders() {
|
||||
reminderLoading.value = true;
|
||||
try {
|
||||
const res = await getBiddingReminders({ days: reminderDays.value });
|
||||
reminderList.value = (res && res.data && res.data.list) || [];
|
||||
} catch (e) {
|
||||
reminderList.value = [];
|
||||
} finally {
|
||||
reminderLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reminderTagType(level) {
|
||||
if (level === "danger") return "danger";
|
||||
if (level === "warning") return "warning";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function bizTypeText(type) {
|
||||
if (type === "bid") return "投标";
|
||||
if (type === "tender") return "招标";
|
||||
return "资质";
|
||||
}
|
||||
|
||||
function goReminder(item) {
|
||||
if (item.biz_type === "bid") {
|
||||
router.push({ path: "/apps/crm/bidding/bid", query: { id: item.biz_id } });
|
||||
} else if (item.biz_type === "tender") {
|
||||
router.push({ path: "/apps/crm/bidding/tender", query: { id: item.biz_id } });
|
||||
} else {
|
||||
router.push({ path: "/apps/crm/bidding/library/qualification", query: { id: item.biz_id } });
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadReminders);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.bidding-home {
|
||||
.entry-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
background: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 20px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.card-desc {
|
||||
margin: 0 0 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.card-features {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
li {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-stats {
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
margin: 0 0 16px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.quick-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.quick-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.quick-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.quick-desc {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reminder-section {
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 16px;
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
margin: 0;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
.reminder-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.reminder-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
.reminder-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 8px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
.reminder-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.reminder-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
.reminder-biz {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--el-text-color-secondary);
|
||||
background: var(--el-fill-color);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
}
|
||||
.reminder-meta {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.reminder-days {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
&.is-danger {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
&.is-warning {
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
&.is-info {
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,436 @@
|
||||
<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 type="primary" :icon="Plus" @click="openCreate">新增资质</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="expiringCount > 0"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="`有 ${expiringCount} 项资质即将到期或已失效,请及时更新`"
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
|
||||
<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: 240px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filters.status" clearable placeholder="全部" style="width: 140px">
|
||||
<el-option
|
||||
v-for="opt in qualificationStatusOptions"
|
||||
: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-container" v-loading="loading">
|
||||
<el-table :data="tableData" stripe border>
|
||||
<el-table-column label="资质名称" min-width="160" show-overflow-tooltip prop="qualification_name" />
|
||||
<el-table-column label="类型" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ qualTypeText(row.qualification_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="证书编号" prop="qualification_no" width="160" show-overflow-tooltip />
|
||||
<el-table-column label="发证机关" prop="issuing_authority" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="有效期至" width="120" align="center">
|
||||
<template #default="{ row }">{{ formatDateOnly(row.expire_date) || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="qualificationStatusTag(row.status)" size="small">
|
||||
{{ qualificationStatusText(row.status) }}
|
||||
</el-tag>
|
||||
<div v-if="row.expiring_days !== undefined && row.expiring_days !== null" class="expire-tip">
|
||||
{{ expireTip(row) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" prop="owner_user_name" width="100" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openPreview(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>
|
||||
|
||||
<!-- 新建 / 编辑 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editing ? '编辑资质' : '新增资质'"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" label-width="110px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资质名称" required>
|
||||
<el-input v-model="form.qualification_name" placeholder="如:ISO9001 认证" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="资质类型">
|
||||
<el-select v-model="form.qualification_type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in qualificationTypeOptions"
|
||||
: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="证书编号">
|
||||
<el-input v-model="form.qualification_no" placeholder="证书编号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="发证机关">
|
||||
<el-input v-model="form.issuing_authority" placeholder="发证机关" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="发证日期">
|
||||
<el-date-picker
|
||||
v-model="form.issue_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="到期日期">
|
||||
<el-date-picker
|
||||
v-model="form.expire_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="选择日期"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="预警天数">
|
||||
<el-input-number v-model="form.alert_days" :min="0" :max="365" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="附件">
|
||||
<div class="file-row">
|
||||
<el-upload :http-request="handleUpload" :show-file-list="false">
|
||||
<el-button :icon="Upload">上传附件</el-button>
|
||||
</el-upload>
|
||||
<el-input v-model="form.file_url" placeholder="附件地址(也可直接填写)" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="说明">
|
||||
<el-input v-model="form.description" 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="previewVisible" :title="previewRow?.qualification_name || '资质详情'" width="680px">
|
||||
<el-descriptions v-if="previewRow" :column="2" border>
|
||||
<el-descriptions-item label="资质类型">
|
||||
{{ qualTypeText(previewRow.qualification_type) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="证书编号">
|
||||
{{ previewRow.qualification_no || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发证机关">
|
||||
{{ previewRow.issuing_authority || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发证日期">
|
||||
{{ formatDateOnly(previewRow.issue_date) || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="到期日期">
|
||||
{{ formatDateOnly(previewRow.expire_date) || "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="qualificationStatusTag(previewRow.status)" size="small">
|
||||
{{ qualificationStatusText(previewRow.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider />
|
||||
<h4 class="block-title">说明</h4>
|
||||
<div class="content-box">{{ previewRow?.description || "—" }}</div>
|
||||
<template v-if="previewRow?.file_url">
|
||||
<el-divider />
|
||||
<el-link :href="previewRow.file_url" target="_blank" type="primary">打开附件</el-link>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Search, Refresh, RefreshLeft, Upload } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getQualificationList,
|
||||
createQualification,
|
||||
updateQualification,
|
||||
deleteQualification,
|
||||
} from "@/api/crmBidding";
|
||||
import { uploadFile } from "@/api/file";
|
||||
import {
|
||||
qualificationTypeOptions,
|
||||
qualificationStatusOptions,
|
||||
qualificationStatusText,
|
||||
qualificationStatusTag,
|
||||
formatDateOnly,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const filters = reactive({ keyword: "", status: "" });
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const previewVisible = ref(false);
|
||||
const previewRow = ref(null);
|
||||
const editing = ref(null);
|
||||
const submitting = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
qualification_name: "",
|
||||
qualification_type: "1",
|
||||
qualification_no: "",
|
||||
issuing_authority: "",
|
||||
issue_date: "",
|
||||
expire_date: "",
|
||||
alert_days: 30,
|
||||
file_url: "",
|
||||
file_name: "",
|
||||
file_size: 0,
|
||||
description: "",
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
const expiringCount = computed(
|
||||
() => tableData.value.filter((r) => Number(r.status) !== 1).length
|
||||
);
|
||||
|
||||
onMounted(fetchList);
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getQualificationList({
|
||||
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 = "";
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editing.value = row;
|
||||
Object.assign(form, {
|
||||
qualification_name: row.qualification_name || "",
|
||||
qualification_type: String(row.qualification_type || "1"),
|
||||
qualification_no: row.qualification_no || "",
|
||||
issuing_authority: row.issuing_authority || "",
|
||||
issue_date: trimTime(row.issue_date, 10),
|
||||
expire_date: trimTime(row.expire_date, 10),
|
||||
alert_days: Number(row.alert_days) || 30,
|
||||
file_url: row.file_url || "",
|
||||
file_name: row.file_name || "",
|
||||
file_size: Number(row.file_size) || 0,
|
||||
description: row.description || "",
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function trimTime(val, len) {
|
||||
if (!val) return "";
|
||||
const s = String(val).replace("T", " ");
|
||||
return s.length > len ? s.slice(0, len) : s;
|
||||
}
|
||||
|
||||
async function handleUpload(option) {
|
||||
const file = option.file;
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
try {
|
||||
const upRes = await uploadFile(fd);
|
||||
if (upRes?.code !== 200 && upRes?.code !== 201) {
|
||||
throw new Error(upRes?.msg || "上传失败");
|
||||
}
|
||||
const data = upRes?.data || {};
|
||||
form.file_url = data.url || "";
|
||||
form.file_name = data.name || file.name;
|
||||
form.file_size = file.size || 0;
|
||||
ElMessage.success("上传成功");
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.qualification_name.trim()) {
|
||||
ElMessage.warning("请输入资质名称");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (editing.value?.id) {
|
||||
await updateQualification(editing.value.id, { ...form });
|
||||
} else {
|
||||
await createQualification({ ...form });
|
||||
}
|
||||
ElMessage.success("保存成功");
|
||||
dialogVisible.value = false;
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除资质「${row.qualification_name}」?`, "删除确认", {
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteQualification(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
function openPreview(row) {
|
||||
previewRow.value = row;
|
||||
previewVisible.value = true;
|
||||
}
|
||||
|
||||
function qualTypeText(v) {
|
||||
const hit = qualificationTypeOptions.find((i) => String(i.value) === String(v));
|
||||
return hit ? hit.label : "-";
|
||||
}
|
||||
|
||||
function expireTip(row) {
|
||||
const d = Number(row.expiring_days);
|
||||
if (Number.isNaN(d)) return "";
|
||||
if (d < 0) return `已过期 ${Math.abs(d)} 天`;
|
||||
return `剩余 ${d} 天`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped src="../../styles/crm-page.less"></style>
|
||||
|
||||
<style scoped>
|
||||
.expire-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.file-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.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,303 @@
|
||||
<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 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: 240px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板类型">
|
||||
<el-select v-model="filters.template_type" clearable placeholder="全部" style="width: 140px">
|
||||
<el-option
|
||||
v-for="opt in bidTemplateTypeOptions"
|
||||
: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-container" v-loading="loading">
|
||||
<el-table :data="tableData" stripe border>
|
||||
<el-table-column label="模板名称" min-width="180" show-overflow-tooltip prop="template_name" />
|
||||
<el-table-column label="类型" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ templateTypeText(row.template_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="适用行业" prop="industry" width="130" show-overflow-tooltip />
|
||||
<el-table-column label="描述" prop="description" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="引用次数" prop="use_count" width="100" align="center" />
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="负责人" prop="owner_user_name" width="100" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="openPreview(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>
|
||||
|
||||
<!-- 新建 / 编辑 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editing ? '编辑模板' : '新建模板'"
|
||||
width="820px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="模板名称" required>
|
||||
<el-input v-model="form.template_name" placeholder="请输入模板名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="模板类型">
|
||||
<el-select v-model="form.template_type" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in bidTemplateTypeOptions"
|
||||
: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="适用行业">
|
||||
<el-input v-model="form.industry" placeholder="如:政企 / 医疗 / 制造" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="文件链接">
|
||||
<el-input v-model="form.file_url" placeholder="模板文件地址(选填)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板描述">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="模板适用场景说明" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板正文">
|
||||
<el-input v-model="form.content" type="textarea" :rows="12" 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="previewVisible" :title="previewRow?.template_name || '模板预览'" width="800px">
|
||||
<el-descriptions :column="3" border size="small">
|
||||
<el-descriptions-item label="类型">
|
||||
{{ templateTypeText(previewRow?.template_type) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="行业">{{ previewRow?.industry || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="引用次数">{{ previewRow?.use_count || 0 }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider />
|
||||
<div class="preview-content">{{ previewRow?.content || "(空)" }}</div>
|
||||
<template v-if="previewRow?.file_url">
|
||||
<el-divider />
|
||||
<el-link :href="previewRow.file_url" target="_blank" type="primary">打开模板文件</el-link>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Search, Refresh, RefreshLeft } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getTemplateList,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
} from "@/api/crmBidding";
|
||||
import { bidTemplateTypeOptions, templateTypeText } from "@/views/apps/crm/dict";
|
||||
|
||||
const loading = ref(false);
|
||||
const tableData = ref([]);
|
||||
const filters = reactive({ keyword: "", template_type: "" });
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 });
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const previewVisible = ref(false);
|
||||
const previewRow = ref(null);
|
||||
const editing = ref(null);
|
||||
const submitting = ref(false);
|
||||
|
||||
const emptyForm = () => ({
|
||||
template_name: "",
|
||||
template_type: "1",
|
||||
industry: "",
|
||||
description: "",
|
||||
content: "",
|
||||
file_url: "",
|
||||
status: 1,
|
||||
});
|
||||
const form = reactive(emptyForm());
|
||||
|
||||
onMounted(fetchList);
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTemplateList({
|
||||
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.template_type = "";
|
||||
handleSearch();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null;
|
||||
Object.assign(form, emptyForm());
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editing.value = row;
|
||||
Object.assign(form, {
|
||||
template_name: row.template_name || "",
|
||||
template_type: String(row.template_type || "1"),
|
||||
industry: row.industry || "",
|
||||
description: row.description || "",
|
||||
content: row.content || "",
|
||||
file_url: row.file_url || "",
|
||||
status: Number(row.status) || 1,
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!form.template_name.trim()) {
|
||||
ElMessage.warning("请输入模板名称");
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (editing.value?.id) {
|
||||
await updateTemplate(editing.value.id, { ...form });
|
||||
} else {
|
||||
await createTemplate({ ...form });
|
||||
}
|
||||
ElMessage.success("保存成功");
|
||||
dialogVisible.value = false;
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除模板「${row.template_name}」?`, "删除确认", {
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteTemplate(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchList();
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
function openPreview(row) {
|
||||
previewRow.value = row;
|
||||
previewVisible.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped src="../../styles/crm-page.less"></style>
|
||||
|
||||
<style scoped>
|
||||
.preview-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
line-height: 1.8;
|
||||
font-size: 14px;
|
||||
min-height: 120px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="crm-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>投标统计</h2>
|
||||
<p>汇总投标数量、中标率、中标金额与费用投入,按月跟踪立项趋势</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="fetchData">刷新</el-button>
|
||||
<el-button :icon="ArrowLeft" @click="goBack">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading">
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">投标总数</div>
|
||||
<div class="stat-value">{{ data.total || 0 }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">中标数量</div>
|
||||
<div class="stat-value success">{{ data.won || 0 }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">中标率</div>
|
||||
<div class="stat-value">{{ winRateText }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">投标费用合计</div>
|
||||
<div class="stat-value money">{{ formatMoney(data.expense) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<template #header><span>按投标状态分布</span></template>
|
||||
<div ref="statusChartRef" class="chart-box" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<template #header><span>近 12 个月立项趋势</span></template>
|
||||
<div ref="monthlyChartRef" class="chart-box" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<template #header><span>投入产出</span></template>
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="中标项目金额">
|
||||
{{ formatMoney(data.amount_won) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="投标费用">
|
||||
{{ formatMoney(data.expense) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="费效比">
|
||||
{{ roiText }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Refresh, ArrowLeft } from "@element-plus/icons-vue";
|
||||
import * as echarts from "echarts";
|
||||
import { getBidStatistics } from "@/api/crmBidding";
|
||||
import { bidStatusText, formatMoney } from "@/views/apps/crm/dict";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const data = reactive({
|
||||
total: 0,
|
||||
won: 0,
|
||||
win_rate: 0,
|
||||
by_status: [],
|
||||
amount_won: 0,
|
||||
expense: 0,
|
||||
monthly: [],
|
||||
});
|
||||
|
||||
const statusChartRef = ref();
|
||||
const monthlyChartRef = ref();
|
||||
let statusChart = null;
|
||||
let monthlyChart = null;
|
||||
|
||||
const winRateText = computed(() => {
|
||||
const r = Number(data.win_rate);
|
||||
if (!r && r !== 0) return "-";
|
||||
return `${r.toFixed(2)}%`;
|
||||
});
|
||||
|
||||
const roiText = computed(() => {
|
||||
const expense = Number(data.expense) || 0;
|
||||
const amount = Number(data.amount_won) || 0;
|
||||
if (expense <= 0) return amount > 0 ? "—(无费用投入)" : "-";
|
||||
return `${(amount / expense).toFixed(2)} 倍`;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener("resize", handleResize);
|
||||
await fetchData();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
statusChart?.dispose();
|
||||
monthlyChart?.dispose();
|
||||
});
|
||||
|
||||
function handleResize() {
|
||||
statusChart?.resize();
|
||||
monthlyChart?.resize();
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBidStatistics();
|
||||
const d = res?.data || {};
|
||||
data.total = d.total || 0;
|
||||
data.won = d.won || 0;
|
||||
data.win_rate = d.win_rate || 0;
|
||||
data.by_status = d.by_status || [];
|
||||
data.amount_won = d.amount_won || 0;
|
||||
data.expense = d.expense || 0;
|
||||
data.monthly = d.monthly || [];
|
||||
await nextTick();
|
||||
renderCharts();
|
||||
} catch {
|
||||
data.by_status = [];
|
||||
data.monthly = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderCharts() {
|
||||
if (statusChartRef.value) {
|
||||
statusChart?.dispose();
|
||||
statusChart = echarts.init(statusChartRef.value);
|
||||
statusChart.setOption({
|
||||
tooltip: { trigger: "item" },
|
||||
legend: { bottom: 0 },
|
||||
series: [
|
||||
{
|
||||
type: "pie",
|
||||
radius: ["40%", "65%"],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: { borderRadius: 6 },
|
||||
label: { formatter: "{b}: {c}" },
|
||||
data: (data.by_status || []).map((r) => ({
|
||||
name: bidStatusText(r.name),
|
||||
value: Number(r.count) || 0,
|
||||
})),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (monthlyChartRef.value) {
|
||||
const rows = data.monthly || [];
|
||||
monthlyChart?.dispose();
|
||||
monthlyChart = echarts.init(monthlyChartRef.value);
|
||||
monthlyChart.setOption({
|
||||
tooltip: { trigger: "axis" },
|
||||
grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: rows.map((r) => r.month || ""),
|
||||
axisLabel: { rotate: 30 },
|
||||
},
|
||||
yAxis: { type: "value", minInterval: 1 },
|
||||
series: [
|
||||
{
|
||||
type: "bar",
|
||||
name: "立项数",
|
||||
data: rows.map((r) => Number(r.count) || 0),
|
||||
itemStyle: {
|
||||
borderRadius: [4, 4, 0, 0],
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: "#409eff" },
|
||||
{ offset: 1, color: "#a0cfff" },
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push("/apps/crm/bidding/bid");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped src="../../styles/crm-page.less"></style>
|
||||
|
||||
<style scoped>
|
||||
.stat-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.stat-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.stat-value {
|
||||
margin-top: 6px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.stat-value.success {
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
.stat-value.money {
|
||||
font-size: 20px;
|
||||
}
|
||||
.chart-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="crm-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h2>招标统计</h2>
|
||||
<p>按状态 / 类型统计招标项目分布,汇总预算与中标金额</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button :icon="Refresh" @click="fetchData">刷新</el-button>
|
||||
<el-button :icon="ArrowLeft" @click="goBack">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading">
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">招标总数</div>
|
||||
<div class="stat-value">{{ data.total || 0 }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">预算总额</div>
|
||||
<div class="stat-value money">{{ formatMoney(data.budget_total) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">中标总额</div>
|
||||
<div class="stat-value money">{{ formatMoney(data.award_total) }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
<div class="stat-label">节资率</div>
|
||||
<div class="stat-value">{{ saveRate }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<template #header><span>按状态分布</span></template>
|
||||
<div ref="statusChartRef" class="chart-box" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card shadow="never" class="chart-card">
|
||||
<template #header><span>按招标类型分布</span></template>
|
||||
<div ref="typeChartRef" class="chart-box" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { Refresh, ArrowLeft } from "@element-plus/icons-vue";
|
||||
import * as echarts from "echarts";
|
||||
import { getTenderStatistics } from "@/api/crmBidding";
|
||||
import {
|
||||
tenderStatusText,
|
||||
tenderTypeText,
|
||||
formatMoney,
|
||||
} from "@/views/apps/crm/dict";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
const data = reactive({ total: 0, by_status: [], by_type: [], budget_total: 0, award_total: 0 });
|
||||
|
||||
const statusChartRef = ref();
|
||||
const typeChartRef = ref();
|
||||
let statusChart = null;
|
||||
let typeChart = null;
|
||||
|
||||
const saveRate = computed(() => {
|
||||
const budget = Number(data.budget_total) || 0;
|
||||
const award = Number(data.award_total) || 0;
|
||||
if (budget <= 0) return "-";
|
||||
return `${(((budget - award) / budget) * 100).toFixed(2)}%`;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener("resize", handleResize);
|
||||
await fetchData();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
statusChart?.dispose();
|
||||
typeChart?.dispose();
|
||||
});
|
||||
|
||||
function handleResize() {
|
||||
statusChart?.resize();
|
||||
typeChart?.resize();
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getTenderStatistics();
|
||||
const d = res?.data || {};
|
||||
data.total = d.total || 0;
|
||||
data.by_status = d.by_status || [];
|
||||
data.by_type = d.by_type || [];
|
||||
data.budget_total = d.budget_total || 0;
|
||||
data.award_total = d.award_total || 0;
|
||||
await nextTick();
|
||||
renderCharts();
|
||||
} catch (e) {
|
||||
data.by_status = [];
|
||||
data.by_type = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toPieOption(rows, labelFn) {
|
||||
return {
|
||||
tooltip: { trigger: "item" },
|
||||
legend: { bottom: 0 },
|
||||
series: [
|
||||
{
|
||||
type: "pie",
|
||||
radius: ["40%", "65%"],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: { borderRadius: 6 },
|
||||
label: { formatter: "{b}: {c}" },
|
||||
data: (rows || []).map((r) => ({
|
||||
name: labelFn(r.name),
|
||||
value: Number(r.count) || 0,
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function renderCharts() {
|
||||
if (statusChartRef.value) {
|
||||
statusChart?.dispose();
|
||||
statusChart = echarts.init(statusChartRef.value);
|
||||
statusChart.setOption(toPieOption(data.by_status, tenderStatusText));
|
||||
}
|
||||
if (typeChartRef.value) {
|
||||
typeChart?.dispose();
|
||||
typeChart = echarts.init(typeChartRef.value);
|
||||
typeChart.setOption(toPieOption(data.by_type, tenderTypeText));
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push("/apps/crm/bidding/tender");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped src="../../styles/crm-page.less"></style>
|
||||
|
||||
<style scoped>
|
||||
.stat-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.stat-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.stat-value {
|
||||
margin-top: 6px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.stat-value.money {
|
||||
font-size: 20px;
|
||||
}
|
||||
.chart-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.chart-box {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -716,3 +716,416 @@ export const paybackStatusTag = (val) => PAYBACK_STATUS_TAG[normalize(val)] || "
|
||||
export const paybackItemStatusText = (val) =>
|
||||
PAYBACK_ITEM_STATUS_MAP[normalize(val)] || normalize(val) || "-";
|
||||
export const paybackItemStatusTag = (val) => PAYBACK_ITEM_STATUS_TAG[normalize(val)] || "info";
|
||||
|
||||
/* =====================================================================
|
||||
* 招投标管理(CRM-我要招标/我要投标)
|
||||
* ===================================================================== */
|
||||
|
||||
/** 招标类型:1公开 2邀请 3竞争性谈判 4询比 5单一来源 */
|
||||
export const TENDER_TYPE_OPTIONS = [
|
||||
{ label: "公开招标", value: "1" },
|
||||
{ label: "邀请招标", value: "2" },
|
||||
{ label: "竞争性谈判", value: "3" },
|
||||
{ label: "询比采购", value: "4" },
|
||||
{ label: "单一来源", value: "5" },
|
||||
];
|
||||
|
||||
/** 招标状态:0草稿 1公告中 2报名中 3投标中 4开标中 5评标中 6已定标 7已归档 */
|
||||
export const TENDER_STATUS_OPTIONS = [
|
||||
{ label: "草稿", value: 0 },
|
||||
{ label: "公告中", value: 1 },
|
||||
{ label: "报名中", value: 2 },
|
||||
{ label: "投标中", value: 3 },
|
||||
{ label: "开标中", value: 4 },
|
||||
{ label: "评标中", value: 5 },
|
||||
{ label: "已定标", value: 6 },
|
||||
{ label: "已归档", value: 7 },
|
||||
];
|
||||
|
||||
/** 评标方法:1综合评估 2最低价 3性价比 */
|
||||
export const EVALUATION_METHOD_OPTIONS = [
|
||||
{ label: "综合评估法", value: "1" },
|
||||
{ label: "最低价法", value: "2" },
|
||||
{ label: "性价比法", value: "3" },
|
||||
];
|
||||
|
||||
/** 定标类型:1定标 2流标 3废标 */
|
||||
export const TENDER_RESULT_TYPE_OPTIONS = [
|
||||
{ label: "定标", value: 1 },
|
||||
{ label: "流标", value: 2 },
|
||||
{ label: "废标", value: 3 },
|
||||
];
|
||||
|
||||
/** 投标类型(同招标类型) */
|
||||
export const BID_TYPE_OPTIONS = TENDER_TYPE_OPTIONS;
|
||||
|
||||
/** 投标状态:0草稿 1立项审批中 2投标中 3已开标 4中标 5落标 6已转合同 7已归档 */
|
||||
export const BID_STATUS_OPTIONS = [
|
||||
{ label: "草稿", value: 0 },
|
||||
{ label: "立项审批中", value: 1 },
|
||||
{ label: "投标中", value: 2 },
|
||||
{ label: "已开标", value: 3 },
|
||||
{ label: "中标", value: 4 },
|
||||
{ label: "落标", value: 5 },
|
||||
{ label: "已转合同", value: 6 },
|
||||
{ label: "已归档", value: 7 },
|
||||
];
|
||||
|
||||
/** 投标来源:0独立 1商机 2项目 */
|
||||
export const BID_SOURCE_TYPE_OPTIONS = [
|
||||
{ label: "独立创建", value: 0 },
|
||||
{ label: "商机", value: 1 },
|
||||
{ label: "项目", value: 2 },
|
||||
];
|
||||
|
||||
/** 投标结果:1中标 2未中标 3废标 4流标 */
|
||||
export const BID_RESULT_TYPE_OPTIONS = [
|
||||
{ label: "中标", value: 1 },
|
||||
{ label: "未中标", value: 2 },
|
||||
{ label: "废标", value: 3 },
|
||||
{ label: "流标", value: 4 },
|
||||
];
|
||||
|
||||
/** 投标方报名状态:1待审核 2已通过 3已拒绝 4已退标 */
|
||||
export const TENDER_SUPPLIER_STATUS_OPTIONS = [
|
||||
{ label: "待审核", value: 1 },
|
||||
{ label: "已通过", value: 2 },
|
||||
{ label: "已拒绝", value: 3 },
|
||||
{ label: "已退标", value: 4 },
|
||||
];
|
||||
|
||||
/** 招标公告类型:1招标公告 2变更公告 3中标公告 4终止公告 */
|
||||
export const TENDER_ANNOUNCE_TYPE_OPTIONS = [
|
||||
{ label: "招标公告", value: "1" },
|
||||
{ label: "变更公告", value: "2" },
|
||||
{ label: "中标公告", value: "3" },
|
||||
{ label: "终止公告", value: "4" },
|
||||
];
|
||||
|
||||
/** 评标结论:0待评 1合格 2不合格 3废标 */
|
||||
export const EVALUATION_RESULT_OPTIONS = [
|
||||
{ label: "待评", value: 0 },
|
||||
{ label: "合格", value: 1 },
|
||||
{ label: "不合格", value: 2 },
|
||||
{ label: "废标", value: 3 },
|
||||
];
|
||||
|
||||
/** 任务类型:1商务 2技术 3报价 4法务 5财务 6其他 */
|
||||
export const BID_TASK_TYPE_OPTIONS = [
|
||||
{ label: "商务", value: "1" },
|
||||
{ label: "技术", value: "2" },
|
||||
{ label: "报价", value: "3" },
|
||||
{ label: "法务", value: "4" },
|
||||
{ label: "财务", value: "5" },
|
||||
{ label: "其他", value: "6" },
|
||||
];
|
||||
|
||||
/** 任务状态:0未开始 1进行中 2已完成 3已逾期 4已取消 */
|
||||
export const BID_TASK_STATUS_OPTIONS = [
|
||||
{ label: "未开始", value: 0 },
|
||||
{ label: "进行中", value: 1 },
|
||||
{ label: "已完成", value: 2 },
|
||||
{ label: "已逾期", value: 3 },
|
||||
{ label: "已取消", value: 4 },
|
||||
];
|
||||
|
||||
/** 标书模板类型:1技术标 2商务标 3综合标 */
|
||||
export const BID_TEMPLATE_TYPE_OPTIONS = [
|
||||
{ label: "技术标", value: "1" },
|
||||
{ label: "商务标", value: "2" },
|
||||
{ label: "综合标", value: "3" },
|
||||
];
|
||||
|
||||
/** 资质类型:1营业执照 2资质证书 3业绩证明 4认证证书 5其他 */
|
||||
export const QUALIFICATION_TYPE_OPTIONS = [
|
||||
{ label: "营业执照", value: "1" },
|
||||
{ label: "资质证书", value: "2" },
|
||||
{ label: "业绩证明", value: "3" },
|
||||
{ label: "认证证书", value: "4" },
|
||||
{ label: "其他", value: "5" },
|
||||
];
|
||||
|
||||
/** 资质状态:1有效 2即将过期 0失效 */
|
||||
export const QUALIFICATION_STATUS_OPTIONS = [
|
||||
{ label: "有效", value: 1 },
|
||||
{ label: "即将过期", value: 2 },
|
||||
{ label: "失效", value: 0 },
|
||||
];
|
||||
|
||||
/** 报价单状态:1草稿 2待审批 3已审批 4已使用 */
|
||||
export const BID_QUOTE_STATUS_OPTIONS = [
|
||||
{ label: "草稿", value: 1 },
|
||||
{ label: "待审批", value: 2 },
|
||||
{ label: "已审批", value: 3 },
|
||||
{ label: "已使用", value: 4 },
|
||||
];
|
||||
|
||||
/** 竞争对手类型:1同类厂商 2关联企业 3其他 */
|
||||
export const COMPETITOR_TYPE_OPTIONS = [
|
||||
{ label: "同类厂商", value: 1 },
|
||||
{ label: "关联企业", value: 2 },
|
||||
{ label: "其他", value: 3 },
|
||||
];
|
||||
|
||||
/** 投标费用类型:1标书费 2保证金 3差旅费 4咨询费 5代理费 6其他 */
|
||||
export const BID_EXPENSE_TYPE_OPTIONS = [
|
||||
{ label: "标书费", value: 1 },
|
||||
{ label: "保证金", value: 2 },
|
||||
{ label: "差旅费", value: 3 },
|
||||
{ label: "咨询费", value: 4 },
|
||||
{ label: "代理费", value: 5 },
|
||||
{ label: "其他", value: 6 },
|
||||
];
|
||||
|
||||
/** 费用支付状态:0未付 1已付 2已退还 3已抵扣 */
|
||||
export const PAYMENT_STATUS_OPTIONS = [
|
||||
{ label: "未付", value: 0 },
|
||||
{ label: "已付", value: 1 },
|
||||
{ label: "已退还", value: 2 },
|
||||
{ label: "已抵扣", value: 3 },
|
||||
];
|
||||
|
||||
/** 标书版本状态:1编辑中 2待审核 3已定稿 4已提交 */
|
||||
export const BID_DOCUMENT_STATUS_OPTIONS = [
|
||||
{ label: "编辑中", value: 1 },
|
||||
{ label: "待审核", value: 2 },
|
||||
{ label: "已定稿", value: 3 },
|
||||
{ label: "已提交", value: 4 },
|
||||
];
|
||||
|
||||
/** 立项审批状态:0待审批 1已通过 2已拒绝 3已撤回 */
|
||||
export const APPROVAL_STATUS_OPTIONS = [
|
||||
{ label: "待审批", value: 0 },
|
||||
{ label: "已通过", value: 1 },
|
||||
{ label: "已拒绝", value: 2 },
|
||||
{ label: "已撤回", value: 3 },
|
||||
];
|
||||
|
||||
/** 审批类型:1立项审批 2报价审批 3合同审批 */
|
||||
export const APPROVAL_TYPE_OPTIONS = [
|
||||
{ label: "立项审批", value: "1" },
|
||||
{ label: "报价审批", value: "2" },
|
||||
{ label: "合同审批", value: "3" },
|
||||
];
|
||||
|
||||
/* ---------------- 派生 MAP / TAG / 工具函数 ---------------- */
|
||||
|
||||
const TENDER_STATUS_MAP = TENDER_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {});
|
||||
const TENDER_STATUS_TAG = {
|
||||
0: "info",
|
||||
1: "primary",
|
||||
2: "primary",
|
||||
3: "warning",
|
||||
4: "warning",
|
||||
5: "warning",
|
||||
6: "success",
|
||||
7: "info",
|
||||
};
|
||||
|
||||
const BID_STATUS_MAP = BID_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {});
|
||||
const BID_STATUS_TAG = {
|
||||
0: "info",
|
||||
1: "primary",
|
||||
2: "warning",
|
||||
3: "warning",
|
||||
4: "success",
|
||||
5: "danger",
|
||||
6: "success",
|
||||
7: "info",
|
||||
};
|
||||
|
||||
const BID_RESULT_TYPE_MAP = BID_RESULT_TYPE_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {});
|
||||
const BID_RESULT_TYPE_TAG = {
|
||||
1: "success",
|
||||
2: "danger",
|
||||
3: "warning",
|
||||
4: "info",
|
||||
};
|
||||
|
||||
const BID_SOURCE_TYPE_MAP = BID_SOURCE_TYPE_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {});
|
||||
|
||||
const BID_TASK_STATUS_MAP = BID_TASK_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {});
|
||||
const BID_TASK_STATUS_TAG = {
|
||||
0: "info",
|
||||
1: "primary",
|
||||
2: "success",
|
||||
3: "danger",
|
||||
4: "info",
|
||||
};
|
||||
|
||||
const QUALIFICATION_STATUS_MAP = QUALIFICATION_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {});
|
||||
const QUALIFICATION_STATUS_TAG = {
|
||||
1: "success",
|
||||
2: "warning",
|
||||
0: "info",
|
||||
};
|
||||
|
||||
const BID_QUOTE_STATUS_MAP = BID_QUOTE_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {});
|
||||
const BID_QUOTE_STATUS_TAG = {
|
||||
1: "info",
|
||||
2: "warning",
|
||||
3: "success",
|
||||
4: "primary",
|
||||
};
|
||||
|
||||
const PAYMENT_STATUS_MAP = PAYMENT_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {});
|
||||
const PAYMENT_STATUS_TAG = {
|
||||
0: "warning",
|
||||
1: "success",
|
||||
2: "primary",
|
||||
3: "info",
|
||||
};
|
||||
|
||||
const APPROVAL_STATUS_MAP = APPROVAL_STATUS_OPTIONS.reduce((m, i) => ((m[i.value] = i.label), m), {});
|
||||
const APPROVAL_STATUS_TAG = {
|
||||
0: "warning",
|
||||
1: "success",
|
||||
2: "danger",
|
||||
3: "info",
|
||||
};
|
||||
|
||||
/* 工具函数 */
|
||||
export const tenderStatusText = (val) => TENDER_STATUS_MAP[normalize(val)] || normalize(val) || "-";
|
||||
export const tenderStatusTag = (val) => TENDER_STATUS_TAG[normalize(val)] || "info";
|
||||
|
||||
export const bidStatusText = (val) => BID_STATUS_MAP[normalize(val)] || normalize(val) || "-";
|
||||
export const bidStatusTag = (val) => BID_STATUS_TAG[normalize(val)] || "info";
|
||||
|
||||
export const bidResultTypeText = (val) => BID_RESULT_TYPE_MAP[normalize(val)] || normalize(val) || "-";
|
||||
export const bidResultTypeTag = (val) => BID_RESULT_TYPE_TAG[normalize(val)] || "info";
|
||||
|
||||
export const bidSourceTypeText = (val) => BID_SOURCE_TYPE_MAP[normalize(val)] || normalize(val) || "-";
|
||||
|
||||
export const bidTaskStatusText = (val) => BID_TASK_STATUS_MAP[normalize(val)] || normalize(val) || "-";
|
||||
export const bidTaskStatusTag = (val) => BID_TASK_STATUS_TAG[normalize(val)] || "info";
|
||||
|
||||
export const qualificationStatusText = (val) => QUALIFICATION_STATUS_MAP[normalize(val)] || normalize(val) || "-";
|
||||
export const qualificationStatusTag = (val) => QUALIFICATION_STATUS_TAG[normalize(val)] || "info";
|
||||
|
||||
export const bidQuoteStatusText = (val) => BID_QUOTE_STATUS_MAP[normalize(val)] || normalize(val) || "-";
|
||||
export const bidQuoteStatusTag = (val) => BID_QUOTE_STATUS_TAG[normalize(val)] || "info";
|
||||
|
||||
export const paymentStatusText = (val) => PAYMENT_STATUS_MAP[normalize(val)] || normalize(val) || "-";
|
||||
export const paymentStatusTag = (val) => PAYMENT_STATUS_TAG[normalize(val)] || "info";
|
||||
|
||||
export const approvalStatusText = (val) => APPROVAL_STATUS_MAP[normalize(val)] || normalize(val) || "-";
|
||||
export const approvalStatusTag = (val) => APPROVAL_STATUS_TAG[normalize(val)] || "info";
|
||||
|
||||
/** 暴露给 el-select 遍历 */
|
||||
export const tenderTypeOptions = TENDER_TYPE_OPTIONS;
|
||||
export const tenderStatusOptions = TENDER_STATUS_OPTIONS;
|
||||
export const evaluationMethodOptions = EVALUATION_METHOD_OPTIONS;
|
||||
export const tenderResultTypeOptions = TENDER_RESULT_TYPE_OPTIONS;
|
||||
export const bidTypeOptions = BID_TYPE_OPTIONS;
|
||||
export const bidStatusOptions = BID_STATUS_OPTIONS;
|
||||
export const bidSourceTypeOptions = BID_SOURCE_TYPE_OPTIONS;
|
||||
export const bidResultTypeOptions = BID_RESULT_TYPE_OPTIONS;
|
||||
export const tenderSupplierStatusOptions = TENDER_SUPPLIER_STATUS_OPTIONS;
|
||||
export const tenderAnnounceTypeOptions = TENDER_ANNOUNCE_TYPE_OPTIONS;
|
||||
export const evaluationResultOptions = EVALUATION_RESULT_OPTIONS;
|
||||
export const bidTaskTypeOptions = BID_TASK_TYPE_OPTIONS;
|
||||
export const bidTaskStatusOptions = BID_TASK_STATUS_OPTIONS;
|
||||
export const bidTemplateTypeOptions = BID_TEMPLATE_TYPE_OPTIONS;
|
||||
export const qualificationTypeOptions = QUALIFICATION_TYPE_OPTIONS;
|
||||
export const qualificationStatusOptions = QUALIFICATION_STATUS_OPTIONS;
|
||||
export const bidQuoteStatusOptions = BID_QUOTE_STATUS_OPTIONS;
|
||||
export const competitorTypeOptions = COMPETITOR_TYPE_OPTIONS;
|
||||
export const bidExpenseTypeOptions = BID_EXPENSE_TYPE_OPTIONS;
|
||||
export const paymentStatusOptions = PAYMENT_STATUS_OPTIONS;
|
||||
export const bidDocumentStatusOptions = BID_DOCUMENT_STATUS_OPTIONS;
|
||||
export const approvalStatusOptions = APPROVAL_STATUS_OPTIONS;
|
||||
export const approvalTypeOptions = APPROVAL_TYPE_OPTIONS;
|
||||
|
||||
/* =====================================================================
|
||||
* 招投标:补齐的取值工具
|
||||
* 上面的 *_OPTIONS 已定义,这里只补 "值 -> 文案 / tag 类型" 的映射,
|
||||
* 与既有一 Style保持一致,供页面和 Tab 组件直接引用。
|
||||
* ===================================================================== */
|
||||
|
||||
const BIDDING_TEXT_MAP = {
|
||||
tenderType: TENDER_TYPE_OPTIONS,
|
||||
evaluationMethod: EVALUATION_METHOD_OPTIONS,
|
||||
bidResult: BID_RESULT_TYPE_OPTIONS,
|
||||
templateType: BID_TEMPLATE_TYPE_OPTIONS,
|
||||
competitorType: COMPETITOR_TYPE_OPTIONS,
|
||||
expenseType: BID_EXPENSE_TYPE_OPTIONS,
|
||||
documentStatus: BID_DOCUMENT_STATUS_OPTIONS
|
||||
};
|
||||
|
||||
const BIDDING_TAG_MAP = {
|
||||
tenderType: { 1: "primary", 2: "success", 3: "warning", 4: "info", 5: "danger" },
|
||||
bidResult: { 1: "success", 2: "danger", 3: "warning", 4: "info" },
|
||||
documentStatus: { 1: "info", 2: "warning", 3: "success", 4: "primary" }
|
||||
};
|
||||
|
||||
function biddingText(kind, val) {
|
||||
const opt = BIDDING_TEXT_MAP[kind];
|
||||
if (!opt) return "-";
|
||||
const hit = opt.find(i => String(i.value) === normalize(val));
|
||||
return hit ? hit.label : normalize(val) || "-";
|
||||
}
|
||||
|
||||
export const tenderTypeText = val => biddingText("tenderType", val);
|
||||
export const tenderTypeTag = val => BIDDING_TAG_MAP.tenderType[normalize(val)] || "info";
|
||||
|
||||
// 投标类型与招标类型共用同一套字典,保留 bidType* 别名避免调用方改字典名
|
||||
export const bidTypeText = val => biddingText("tenderType", val);
|
||||
export const bidTypeTag = val => BIDDING_TAG_MAP.tenderType[normalize(val)] || "info";
|
||||
|
||||
export const evaluationMethodText = val => biddingText("evaluationMethod", val);
|
||||
|
||||
export const bidResultText = val => biddingText("bidResult", val);
|
||||
export const bidResultTag = val => BIDDING_TAG_MAP.bidResult[normalize(val)] || "info";
|
||||
|
||||
export const templateTypeText = val => biddingText("templateType", val);
|
||||
|
||||
export const competitorTypeText = val => biddingText("competitorType", val);
|
||||
|
||||
export const expenseTypeText = val => biddingText("expenseType", val);
|
||||
|
||||
export const documentStatusText = val => biddingText("documentStatus", val);
|
||||
export const documentStatusTag = val =>
|
||||
BIDDING_TAG_MAP.documentStatus[normalize(val)] || "info";
|
||||
|
||||
/**
|
||||
* 时间线/详情用的时间格式化:空值、零值时间统一返回空串而不是 "-",
|
||||
* 避免时间线上出现一排 "-"。
|
||||
*/
|
||||
export function formatBidTime(val) {
|
||||
if (!val) return "";
|
||||
const s = String(val).trim();
|
||||
if (!s || s.startsWith("0001-")) return "";
|
||||
return s.length >= 16 ? s.slice(0, 16).replace("T", " ") : s.slice(0, 10);
|
||||
}
|
||||
|
||||
/* ====================== 状态流转合法性 ====================== */
|
||||
|
||||
// 允许流转到的下一状态,用于前端限制操作按钮(后端仍会二次校验)
|
||||
export const BID_NEXT_STATUS = {
|
||||
0: [1, 2], // 草稿 -> 审批中 / 直接投标中
|
||||
1: [2, 0], // 审批中 -> 投标中 / 驳回回草稿
|
||||
2: [3], // 投标中 -> 已开标
|
||||
3: [4, 5], // 已开标 -> 中标 / 落标
|
||||
4: [6, 7], // 中标 -> 已转合同 / 归档
|
||||
5: [7],
|
||||
6: [7],
|
||||
7: []
|
||||
};
|
||||
|
||||
export const TENDER_NEXT_STATUS = {
|
||||
0: [1],
|
||||
1: [2],
|
||||
2: [3],
|
||||
3: [4],
|
||||
4: [5],
|
||||
5: [6],
|
||||
6: [7],
|
||||
7: []
|
||||
};
|
||||
|
||||
/** 判断目标状态是否是合法流转 */
|
||||
export function canTransfer(kind, from, to) {
|
||||
const table = kind === "tender" ? TENDER_NEXT_STATUS : BID_NEXT_STATUS;
|
||||
const next = table[normalize(from)];
|
||||
return Array.isArray(next) && next.includes(Number(to));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,846 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// =============================================================
|
||||
// CRM 招投标管理 - 投标方(Bid)
|
||||
// =============================================================
|
||||
//
|
||||
// 状态机:0草稿 -> 1立项审批中 -> 2投标中 -> 3已开标 -> (4中标 | 5落标) -> (6已转合同 | 7已归档)
|
||||
// 关联:商机(1)/项目(2)/独立(0) -> 中标 -> 合同/项目
|
||||
// =============================================================
|
||||
|
||||
// BackendCrmBiddingBidController 投标方
|
||||
type BackendCrmBiddingBidController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// bidPayload 创建/更新请求体
|
||||
type bidPayload struct {
|
||||
BidNo string `json:"bid_no"`
|
||||
Title string `json:"title"`
|
||||
BidType string `json:"bid_type"`
|
||||
SourceType int8 `json:"source_type"`
|
||||
SourceID uint64 `json:"source_id"`
|
||||
BusinessID uint64 `json:"business_id"`
|
||||
BusinessName string `json:"business_name"`
|
||||
ProjectID uint64 `json:"project_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
TenderID uint64 `json:"tender_id"`
|
||||
TenderNo string `json:"tender_no"`
|
||||
CustomerID uint64 `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
PublishOrg string `json:"publish_org"`
|
||||
BidAmount float64 `json:"bid_amount"`
|
||||
BidDeposit float64 `json:"bid_deposit"`
|
||||
PublishDate string `json:"publish_date"`
|
||||
RegisterEndDate string `json:"register_end_date"`
|
||||
BidEndDate string `json:"bid_end_date"`
|
||||
OpenBidDate string `json:"open_bid_date"`
|
||||
ContactPerson string `json:"contact_person"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
BidAddress string `json:"bid_address"`
|
||||
OpenAddress string `json:"open_address"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
OwnerUserName string `json:"owner_user_name"`
|
||||
Description string `json:"description"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// List GET /backend/crm/bidding/bid/list
|
||||
func (c *BackendCrmBiddingBidController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
status := strings.TrimSpace(c.GetString("status"))
|
||||
winStatus := strings.TrimSpace(c.GetString("win_status"))
|
||||
sourceType := strings.TrimSpace(c.GetString("source_type"))
|
||||
ownerID := strings.TrimSpace(c.GetString("owner_user_id"))
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
|
||||
cond = crmApplyOwnerScope(cond, claims)
|
||||
|
||||
if keyword != "" {
|
||||
kw := orm.NewCondition().
|
||||
Or("title__contains", keyword).
|
||||
Or("bid_no__contains", keyword).
|
||||
Or("customer_name__contains", keyword).
|
||||
Or("business_name__contains", keyword).
|
||||
Or("contact_person__contains", keyword)
|
||||
cond = cond.AndCond(kw)
|
||||
}
|
||||
if status != "" {
|
||||
cond = cond.And("status", status)
|
||||
}
|
||||
if winStatus != "" {
|
||||
cond = cond.And("win_status", winStatus)
|
||||
}
|
||||
if sourceType != "" {
|
||||
if v, errParse := strconv.Atoi(sourceType); errParse == nil {
|
||||
cond = cond.And("source_type", int8(v))
|
||||
}
|
||||
}
|
||||
if ownerID != "" {
|
||||
cond = cond.And("owner_user_id", ownerID)
|
||||
}
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).SetCond(cond)
|
||||
total, _ := qs.Count()
|
||||
var list []models.TenantCrmBiddingBid
|
||||
if total > 0 {
|
||||
_, _ = qs.OrderBy("-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
|
||||
}
|
||||
pipelineOk(&c.Controller, map[string]interface{}{
|
||||
"list": list, "total": total, "page": page, "pageSize": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// Detail GET /backend/crm/bidding/bid/detail/:id
|
||||
func (c *BackendCrmBiddingBidController) Detail() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var row models.TenantCrmBiddingBid
|
||||
err = models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).
|
||||
Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).One(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "投标记录未找到")
|
||||
return
|
||||
}
|
||||
if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 403, 403, "无权访问该投标数据")
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, row)
|
||||
}
|
||||
|
||||
// Create POST /backend/crm/bidding/bid
|
||||
func (c *BackendCrmBiddingBidController) Create() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p bidPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.Title) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "投标标题不能为空")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.CustomerName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "请填写招标方名称")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
now := time.Now()
|
||||
row := models.TenantCrmBiddingBid{
|
||||
TenantID: tenantID,
|
||||
BidNo: strings.TrimSpace(p.BidNo),
|
||||
Title: strings.TrimSpace(p.Title),
|
||||
BidType: firstNonEmpty(p.BidType, "1"),
|
||||
Status: 0,
|
||||
SourceType: p.SourceType,
|
||||
BusinessID: nilOrID(p.BusinessID),
|
||||
BusinessName: strings.TrimSpace(p.BusinessName),
|
||||
ProjectID: nilOrID(p.ProjectID),
|
||||
ProjectName: strings.TrimSpace(p.ProjectName),
|
||||
TenderID: nilOrID(p.TenderID),
|
||||
TenderNo: strings.TrimSpace(p.TenderNo),
|
||||
CustomerID: nilOrID(p.CustomerID),
|
||||
CustomerName: strings.TrimSpace(p.CustomerName),
|
||||
PublishOrg: strings.TrimSpace(p.PublishOrg),
|
||||
BidAmount: p.BidAmount,
|
||||
BidDeposit: p.BidDeposit,
|
||||
PublishDate: parsePipelineDate(p.PublishDate),
|
||||
RegisterEndDate: parsePipelineDateTime(p.RegisterEndDate),
|
||||
BidEndDate: parsePipelineDateTime(p.BidEndDate),
|
||||
OpenBidDate: parsePipelineDateTime(p.OpenBidDate),
|
||||
ContactPerson: strings.TrimSpace(p.ContactPerson),
|
||||
ContactPhone: strings.TrimSpace(p.ContactPhone),
|
||||
BidAddress: strings.TrimSpace(p.BidAddress),
|
||||
OpenAddress: strings.TrimSpace(p.OpenAddress),
|
||||
OwnerUserID: firstNonEmpty(p.OwnerUserID, pipelineUID(claims)),
|
||||
OwnerUserName: firstNonEmpty(p.OwnerUserName, resolveUserName(claims)),
|
||||
Description: p.Description,
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateUserName: resolveUserName(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
// 唯一性校验:同租户内不允许重复创建同名投标
|
||||
if crmNameDuplicated(new(models.TenantCrmBiddingBid), tenantID, "title", row.Title) {
|
||||
pipelineErr(&c.Controller, 409, 409, "该投标标题已存在")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 4, uint64(id), "create", "创建投标:"+row.Title, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Update PUT /backend/crm/bidding/bid/:id
|
||||
func (c *BackendCrmBiddingBidController) Update() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p bidPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var row models.TenantCrmBiddingBid
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "投标记录未找到")
|
||||
return
|
||||
}
|
||||
if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "无权操作该投标数据")
|
||||
return
|
||||
}
|
||||
if row.Locked == 1 || row.Status == 6 {
|
||||
pipelineErr(&c.Controller, 400, 400, "投标记录已锁定或已转合同,不能编辑")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.Title) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "投标标题不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
row.BidNo = strings.TrimSpace(p.BidNo)
|
||||
row.Title = strings.TrimSpace(p.Title)
|
||||
if strings.TrimSpace(p.BidType) != "" {
|
||||
row.BidType = p.BidType
|
||||
}
|
||||
if p.SourceType > 0 {
|
||||
row.SourceType = p.SourceType
|
||||
}
|
||||
row.BusinessID = nilOrID(p.BusinessID)
|
||||
row.BusinessName = strings.TrimSpace(p.BusinessName)
|
||||
row.ProjectID = nilOrID(p.ProjectID)
|
||||
row.ProjectName = strings.TrimSpace(p.ProjectName)
|
||||
row.TenderID = nilOrID(p.TenderID)
|
||||
row.TenderNo = strings.TrimSpace(p.TenderNo)
|
||||
row.CustomerID = nilOrID(p.CustomerID)
|
||||
row.CustomerName = strings.TrimSpace(p.CustomerName)
|
||||
row.PublishOrg = strings.TrimSpace(p.PublishOrg)
|
||||
row.BidAmount = p.BidAmount
|
||||
row.BidDeposit = p.BidDeposit
|
||||
row.PublishDate = parsePipelineDate(p.PublishDate)
|
||||
row.RegisterEndDate = parsePipelineDateTime(p.RegisterEndDate)
|
||||
row.BidEndDate = parsePipelineDateTime(p.BidEndDate)
|
||||
row.OpenBidDate = parsePipelineDateTime(p.OpenBidDate)
|
||||
row.ContactPerson = strings.TrimSpace(p.ContactPerson)
|
||||
row.ContactPhone = strings.TrimSpace(p.ContactPhone)
|
||||
row.BidAddress = strings.TrimSpace(p.BidAddress)
|
||||
row.OpenAddress = strings.TrimSpace(p.OpenAddress)
|
||||
if strings.TrimSpace(p.OwnerUserID) != "" {
|
||||
row.OwnerUserID = strings.TrimSpace(p.OwnerUserID)
|
||||
}
|
||||
if strings.TrimSpace(p.OwnerUserName) != "" {
|
||||
row.OwnerUserName = strings.TrimSpace(p.OwnerUserName)
|
||||
}
|
||||
row.Description = p.Description
|
||||
row.Remark = p.Remark
|
||||
row.UpdateTime = time.Now()
|
||||
|
||||
if _, err := models.Orm.Update(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 4, row.ID, "update", "更新投标:"+row.Title, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID})
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/crm/bidding/bid/:id
|
||||
func (c *BackendCrmBiddingBidController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var row models.TenantCrmBiddingBid
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "投标记录未找到")
|
||||
return
|
||||
}
|
||||
if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "无权操作该投标数据")
|
||||
return
|
||||
}
|
||||
if row.Locked == 1 || row.Status == 6 {
|
||||
pipelineErr(&c.Controller, 400, 400, "投标记录已锁定或已转合同,不能删除")
|
||||
return
|
||||
}
|
||||
if !canDeleteCrmRecord(claims, row.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "只有创建人、租户管理员或平台管理员可以删除该投标")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// ConvertContract POST /backend/crm/bidding/bid/:id/contract 中标转合同
|
||||
func (c *BackendCrmBiddingBidController) ConvertContract() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ContractName string `json:"contract_name"`
|
||||
ContractNo string `json:"contract_no"`
|
||||
ContractCategory string `json:"contract_category"`
|
||||
SignDate string `json:"sign_date"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var bid models.TenantCrmBiddingBid
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&bid); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "投标记录未找到")
|
||||
return
|
||||
}
|
||||
if !canAccessCrmRecord(claims, bid.OwnerUserID, bid.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "无权操作该投标数据")
|
||||
return
|
||||
}
|
||||
if bid.WinStatus != 1 {
|
||||
pipelineErr(&c.Controller, 400, 400, "只有中标的投标才能转合同")
|
||||
return
|
||||
}
|
||||
if bid.ContractID != nil && *bid.ContractID > 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "该投标已转合同,不能重复转")
|
||||
return
|
||||
}
|
||||
|
||||
contractName := strings.TrimSpace(p.ContractName)
|
||||
if contractName == "" {
|
||||
contractName = bid.Title
|
||||
}
|
||||
category := strings.TrimSpace(p.ContractCategory)
|
||||
if category == "" {
|
||||
category = "2" // 默认:服务类
|
||||
}
|
||||
now := time.Now()
|
||||
bidIDVal := bid.ID
|
||||
|
||||
contract := models.TenantCrmContract{
|
||||
TenantID: tenantID,
|
||||
ContractNo: strings.TrimSpace(p.ContractNo),
|
||||
ContractName: contractName,
|
||||
ContractCategory: category,
|
||||
OurRole: 2, // 我方为乙方
|
||||
PartyCount: 2,
|
||||
ProjectID: bid.ProjectID, // 中标项目关联
|
||||
ProjectName: bid.ProjectName,
|
||||
OwnerUserID: bid.OwnerUserID,
|
||||
OwnerUserName: bid.OwnerUserName,
|
||||
SignDate: parsePipelineDate(p.SignDate),
|
||||
Step: 1,
|
||||
Status: 1,
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
contractID, err := models.Orm.Insert(&contract)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建合同失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新投标状态:状态置6已转合同,存 contract_id,加锁
|
||||
cid := uint64(contractID)
|
||||
bid.ContractID = &cid
|
||||
bid.Status = 6
|
||||
bid.Locked = 1
|
||||
bidIDForLog := bidIDVal
|
||||
bid.UpdateTime = now
|
||||
if _, err := models.Orm.Update(&bid, "contract_id", "status", "locked", "update_time"); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新投标状态失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
crmWriteLog(tenantID, 4, bidIDForLog, "convert-contract", "中标转合同:"+contractName, claims)
|
||||
crmWriteLog(tenantID, 1, cid, "create", "由投标「"+bid.Title+"」中标转化生成", claims)
|
||||
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"contract_id": contractID})
|
||||
}
|
||||
|
||||
// ConvertProject POST /backend/crm/bidding/bid/:id/project 中标转项目
|
||||
func (c *BackendCrmBiddingBidController) ConvertProject() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ProjectName string `json:"project_name"`
|
||||
ProjectNo string `json:"project_no"`
|
||||
Amount float64 `json:"amount"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var bid models.TenantCrmBiddingBid
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&bid); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "投标记录未找到")
|
||||
return
|
||||
}
|
||||
if !canAccessCrmRecord(claims, bid.OwnerUserID, bid.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "无权操作该投标数据")
|
||||
return
|
||||
}
|
||||
if bid.WinStatus != 1 {
|
||||
pipelineErr(&c.Controller, 400, 400, "只有中标的投标才能转项目")
|
||||
return
|
||||
}
|
||||
if bid.NewProjectID != nil && *bid.NewProjectID > 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "该投标已转项目,不能重复转")
|
||||
return
|
||||
}
|
||||
|
||||
projectName := strings.TrimSpace(p.ProjectName)
|
||||
if projectName == "" {
|
||||
projectName = bid.Title
|
||||
}
|
||||
amount := p.Amount
|
||||
if amount == 0 {
|
||||
amount = bid.BidAmount
|
||||
}
|
||||
now := time.Now()
|
||||
bidIDVal := bid.ID
|
||||
|
||||
proj := models.TenantCrmProject{
|
||||
TenantID: tenantID,
|
||||
ProjectName: projectName,
|
||||
ProjectNo: strings.TrimSpace(p.ProjectNo),
|
||||
ProjectType: 1, // 单一项目
|
||||
BusinessID: bid.BusinessID,
|
||||
CustomerID: bid.CustomerID,
|
||||
CustomerName: bid.CustomerName,
|
||||
OwnerUserID: bid.OwnerUserID,
|
||||
OwnerUserName: bid.OwnerUserName,
|
||||
UpstreamAmount: amount,
|
||||
Amount: amount,
|
||||
Status: 1,
|
||||
StartDate: parsePipelineDate(p.StartDate),
|
||||
EndDate: parsePipelineDate(p.EndDate),
|
||||
ContactPerson: bid.ContactPerson,
|
||||
ContactPhone: bid.ContactPhone,
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
projID, err := models.Orm.Insert(&proj)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建项目失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
pid := uint64(projID)
|
||||
bid.NewProjectID = &pid
|
||||
bid.ProjectID = &pid
|
||||
bid.ProjectName = projectName
|
||||
bid.UpdateTime = now
|
||||
if _, err := models.Orm.Update(&bid, "new_project_id", "project_id", "project_name", "update_time"); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新投标状态失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
crmWriteLog(tenantID, 4, bidIDVal, "convert-project", "中标转项目:"+projectName, claims)
|
||||
crmWriteLog(tenantID, 3, pid, "create", "由投标「"+bid.Title+"」中标转化生成", claims)
|
||||
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"project_id": projID})
|
||||
}
|
||||
|
||||
// SubmitResult POST /backend/crm/bidding/bid/:id/result 提交开标结果
|
||||
func (c *BackendCrmBiddingBidController) SubmitResult() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ResultType int8 `json:"result_type"`
|
||||
AnnounceDate string `json:"announce_date"`
|
||||
AnnounceURL string `json:"announce_url"`
|
||||
TotalScore float64 `json:"total_score"`
|
||||
OurRanking uint32 `json:"our_ranking"`
|
||||
CompetitorCount uint32 `json:"competitor_count"`
|
||||
Reason string `json:"reason"`
|
||||
Lessons string `json:"lessons"`
|
||||
NextStep string `json:"next_step"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var bid models.TenantCrmBiddingBid
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&bid); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "投标记录未找到")
|
||||
return
|
||||
}
|
||||
if !canAccessCrmRecord(claims, bid.OwnerUserID, bid.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "无权操作该投标数据")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
result := models.TenantCrmBiddingBidResult{
|
||||
TenantID: tenantID,
|
||||
BidID: bid.ID,
|
||||
ResultType: p.ResultType,
|
||||
AnnounceDate: parsePipelineDateTime(p.AnnounceDate),
|
||||
AnnounceURL: strings.TrimSpace(p.AnnounceURL),
|
||||
TotalScore: p.TotalScore,
|
||||
OurRanking: p.OurRanking,
|
||||
CompetitorCount: p.CompetitorCount,
|
||||
Reason: p.Reason,
|
||||
Lessons: p.Lessons,
|
||||
NextStep: strings.TrimSpace(p.NextStep),
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateUserName: resolveUserName(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
resultID, err := models.Orm.Insert(&result)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "保存投标结果失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 同步投标主表状态/得分
|
||||
bid.WinStatus = p.ResultType
|
||||
bid.WinDate = parsePipelineDateTime(p.AnnounceDate)
|
||||
bid.Status = 3 // 已开标
|
||||
bid.TotalScore = p.TotalScore
|
||||
bid.OurRanking = p.OurRanking
|
||||
bid.CompetitorCount = p.CompetitorCount
|
||||
if p.ResultType == 1 {
|
||||
bid.Status = 4 // 中标
|
||||
} else if p.ResultType == 2 {
|
||||
bid.Status = 5 // 落标
|
||||
}
|
||||
bid.UpdateTime = now
|
||||
if _, err := models.Orm.Update(&bid, "win_status", "win_date", "status", "total_score", "our_ranking", "competitor_count", "update_time"); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新投标状态失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
crmWriteLog(tenantID, 4, bid.ID, "submit-result",
|
||||
fmt.Sprintf("提交开标结果:%s(%s)", resultTypeText(p.ResultType), bid.Title), claims)
|
||||
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": resultID})
|
||||
}
|
||||
|
||||
// FromBusiness POST /backend/crm/bidding/bid/from-business 从商机创建投标
|
||||
func (c *BackendCrmBiddingBidController) FromBusiness() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
BusinessID uint64 `json:"business_id"`
|
||||
Title string `json:"title"`
|
||||
CustomerID uint64 `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
PublishOrg string `json:"publish_org"`
|
||||
BidType string `json:"bid_type"`
|
||||
BidAmount float64 `json:"bid_amount"`
|
||||
BidEndDate string `json:"bid_end_date"`
|
||||
OpenBidDate string `json:"open_bid_date"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
OwnerUserName string `json:"owner_user_name"`
|
||||
Description string `json:"description"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.BusinessID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "请选择商机")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
|
||||
var biz models.TenantCrmBusiness
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBusiness)).
|
||||
Filter("id", p.BusinessID).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&biz); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "商机未找到")
|
||||
return
|
||||
}
|
||||
if biz.Locked == 1 {
|
||||
pipelineErr(&c.Controller, 400, 400, "商机已转化并锁定,不能再发起投标")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
title = biz.BusinessName
|
||||
}
|
||||
bizID := biz.ID
|
||||
row := models.TenantCrmBiddingBid{
|
||||
TenantID: tenantID,
|
||||
Title: title,
|
||||
BidType: firstNonEmpty(p.BidType, "1"),
|
||||
Status: 0,
|
||||
SourceType: 1, // 1=商机
|
||||
SourceID: &bizID,
|
||||
BusinessID: &bizID,
|
||||
BusinessName: biz.BusinessName,
|
||||
CustomerID: biz.CustomerID,
|
||||
CustomerName: firstNonEmpty(p.CustomerName, biz.CustomerName),
|
||||
PublishOrg: firstNonEmpty(p.PublishOrg, biz.CustomerName),
|
||||
BidAmount: p.BidAmount,
|
||||
BidEndDate: parsePipelineDateTime(p.BidEndDate),
|
||||
OpenBidDate: parsePipelineDateTime(p.OpenBidDate),
|
||||
ContactPerson: biz.ContactPerson,
|
||||
ContactPhone: biz.ContactPhone,
|
||||
OwnerUserID: firstNonEmpty(p.OwnerUserID, biz.OwnerUserID, pipelineUID(claims)),
|
||||
OwnerUserName: firstNonEmpty(p.OwnerUserName, biz.OwnerUserName, resolveUserName(claims)),
|
||||
Description: p.Description,
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateUserName: resolveUserName(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
if crmNameDuplicated(new(models.TenantCrmBiddingBid), tenantID, "title", row.Title) {
|
||||
pipelineErr(&c.Controller, 409, 409, "该投标标题已存在")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 4, uint64(id), "from-business", "从商机「"+biz.BusinessName+"」创建投标", claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id, "business": biz})
|
||||
}
|
||||
|
||||
// FromProject POST /backend/crm/bidding/bid/from-project 从项目创建投标
|
||||
func (c *BackendCrmBiddingBidController) FromProject() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ProjectID uint64 `json:"project_id"`
|
||||
Title string `json:"title"`
|
||||
BidType string `json:"bid_type"`
|
||||
BidAmount float64 `json:"bid_amount"`
|
||||
BidEndDate string `json:"bid_end_date"`
|
||||
OpenBidDate string `json:"open_bid_date"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
OwnerUserName string `json:"owner_user_name"`
|
||||
Description string `json:"description"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.ProjectID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "请选择项目")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
|
||||
var proj models.TenantCrmProject
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmProject)).
|
||||
Filter("id", p.ProjectID).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&proj); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "项目未找到")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
title := strings.TrimSpace(p.Title)
|
||||
if title == "" {
|
||||
title = proj.ProjectName
|
||||
}
|
||||
projID := proj.ID
|
||||
row := models.TenantCrmBiddingBid{
|
||||
TenantID: tenantID,
|
||||
Title: title,
|
||||
BidType: firstNonEmpty(p.BidType, "1"),
|
||||
Status: 0,
|
||||
SourceType: 2, // 2=项目
|
||||
SourceID: &projID,
|
||||
ProjectID: &projID,
|
||||
ProjectName: proj.ProjectName,
|
||||
BusinessID: proj.BusinessID,
|
||||
BusinessName: "", // 需要回查商机名(不阻塞,简化处理)
|
||||
CustomerID: proj.CustomerID,
|
||||
CustomerName: proj.CustomerName,
|
||||
BidAmount: p.BidAmount,
|
||||
BidEndDate: parsePipelineDateTime(p.BidEndDate),
|
||||
OpenBidDate: parsePipelineDateTime(p.OpenBidDate),
|
||||
OwnerUserID: firstNonEmpty(p.OwnerUserID, proj.OwnerUserID, pipelineUID(claims)),
|
||||
OwnerUserName: firstNonEmpty(p.OwnerUserName, proj.OwnerUserName, resolveUserName(claims)),
|
||||
Description: p.Description,
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateUserName: resolveUserName(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
if crmNameDuplicated(new(models.TenantCrmBiddingBid), tenantID, "title", row.Title) {
|
||||
pipelineErr(&c.Controller, 409, 409, "该投标标题已存在")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 4, uint64(id), "from-project", "从项目「"+proj.ProjectName+"」创建投标", claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id, "project": proj})
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 辅助函数
|
||||
// =====================================================================
|
||||
|
||||
// nilOrID 当 id>0 时返回指针,否则返回 nil
|
||||
func nilOrID(id uint64) *uint64 {
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
v := id
|
||||
return &v
|
||||
}
|
||||
|
||||
// resultTypeText 投标结果文本
|
||||
func resultTypeText(t int8) string {
|
||||
switch t {
|
||||
case 1:
|
||||
return "中标"
|
||||
case 2:
|
||||
return "未中标"
|
||||
case 3:
|
||||
return "废标"
|
||||
case 4:
|
||||
return "流标"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,208 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// =============================================================
|
||||
// CRM 招投标管理 - 到期提醒(开发计划 4.1)
|
||||
// =============================================================
|
||||
//
|
||||
// 不新增表、不引入定时任务:所有提醒都是「按当前时间实时计算」的,
|
||||
// 避免定时任务没跑导致状态陈旧。前端在招投标首页拉取近 N 天的待办。
|
||||
//
|
||||
// 覆盖四类到期:
|
||||
// 1. 投标:报名截止 / 投标截止 / 开标时间
|
||||
// 2. 招标:报名截止 / 投标截止 / 开标时间
|
||||
// 3. 资质:证书到期
|
||||
//
|
||||
// GET /backend/crm/bidding/reminders?days=7
|
||||
// =============================================================
|
||||
|
||||
// BackendCrmBiddingReminderController 招投标到期提醒
|
||||
type BackendCrmBiddingReminderController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// biddingReminderItem 一条到期提醒
|
||||
type biddingReminderItem struct {
|
||||
BizType string `json:"biz_type"` // bid | tender | qualification
|
||||
BizID uint64 `json:"biz_id"` // 业务记录ID
|
||||
Kind string `json:"kind"` // register_end | bid_end | open_bid | expire
|
||||
KindName string `json:"kind_name"` // 提醒类型名称
|
||||
Title string `json:"title"` // 记录标题
|
||||
DueDate string `json:"due_date"` // 到期时间 yyyy-MM-dd HH:mm:ss
|
||||
RemainDays int `json:"remain_days"` // 剩余天数,负值表示已逾期
|
||||
Level string `json:"level"` // danger(<=1天/已逾期) | warning(<=3天) | info
|
||||
OwnerName string `json:"owner_user_name"`
|
||||
}
|
||||
|
||||
// List GET /backend/crm/bidding/reminders?days=7
|
||||
func (c *BackendCrmBiddingReminderController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tid := pipelineTenantID(claims)
|
||||
|
||||
days, _ := c.GetInt("days", 7)
|
||||
if days <= 0 || days > 365 {
|
||||
days = 7
|
||||
}
|
||||
now := time.Now()
|
||||
deadline := now.AddDate(0, 0, days)
|
||||
|
||||
items := make([]biddingReminderItem, 0, 32)
|
||||
|
||||
// ---------- 投标侧 ----------
|
||||
var bids []models.TenantCrmBiddingBid
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBid)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("status__in", 0, 1, 2, 3).
|
||||
All(&bids)
|
||||
for _, b := range bids {
|
||||
if !canAccessCrmRecord(claims, b.OwnerUserID, b.CreateUserID) {
|
||||
continue
|
||||
}
|
||||
items = append(items, pickBidReminders(b, now, deadline)...)
|
||||
}
|
||||
|
||||
// ---------- 招标侧 ----------
|
||||
var tenders []models.TenantCrmBiddingTender
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTender)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
Filter("status__in", 0, 1, 2, 3, 4, 5).
|
||||
All(&tenders)
|
||||
for _, t := range tenders {
|
||||
if !canAccessCrmRecord(claims, t.OwnerUserID, t.CreateUserID) {
|
||||
continue
|
||||
}
|
||||
items = append(items, pickTenderReminders(t, now, deadline)...)
|
||||
}
|
||||
|
||||
// ---------- 资质到期 ----------
|
||||
var quals []models.TenantCrmBiddingBidQualification
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQualification)).
|
||||
Filter("tenant_id", tid).
|
||||
Filter("delete_time__isnull", true).
|
||||
All(&quals)
|
||||
for _, q := range quals {
|
||||
if q.ExpireDate == nil {
|
||||
continue
|
||||
}
|
||||
due := *q.ExpireDate
|
||||
// 资质到期包含「已过期」,便于用户发现失效证书
|
||||
if due.After(deadline) {
|
||||
continue
|
||||
}
|
||||
items = append(items, biddingReminderItem{
|
||||
BizType: "qualification",
|
||||
BizID: q.ID,
|
||||
Kind: "expire",
|
||||
KindName: "资质到期",
|
||||
Title: q.QualificationName,
|
||||
DueDate: due.Format("2006-01-02"),
|
||||
RemainDays: daysBetween(now, due),
|
||||
Level: levelOf(daysBetween(now, due)),
|
||||
OwnerName: q.OwnerUserName,
|
||||
})
|
||||
}
|
||||
|
||||
// 按到期时间升序:最紧急的排在最前
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].RemainDays != items[j].RemainDays {
|
||||
return items[i].RemainDays < items[j].RemainDays
|
||||
}
|
||||
return items[i].DueDate < items[j].DueDate
|
||||
})
|
||||
|
||||
pipelineOk(&c.Controller, map[string]interface{}{
|
||||
"list": items,
|
||||
"total": len(items),
|
||||
"days": days,
|
||||
})
|
||||
}
|
||||
|
||||
// pickBidReminders 从一条投标记录中挑出落在窗口内的到期项
|
||||
func pickBidReminders(b models.TenantCrmBiddingBid, now, deadline time.Time) []biddingReminderItem {
|
||||
out := make([]biddingReminderItem, 0, 3)
|
||||
add := func(kind, kindName string, due *time.Time) {
|
||||
if due == nil {
|
||||
return
|
||||
}
|
||||
d := *due
|
||||
if d.Before(now) || d.After(deadline) {
|
||||
return
|
||||
}
|
||||
out = append(out, biddingReminderItem{
|
||||
BizType: "bid",
|
||||
BizID: b.ID,
|
||||
Kind: kind,
|
||||
KindName: kindName,
|
||||
Title: b.Title,
|
||||
DueDate: d.Format("2006-01-02 15:04:05"),
|
||||
RemainDays: daysBetween(now, d),
|
||||
Level: levelOf(daysBetween(now, d)),
|
||||
OwnerName: b.OwnerUserName,
|
||||
})
|
||||
}
|
||||
add("register_end", "报名截止", b.RegisterEndDate)
|
||||
add("bid_end", "投标截止", b.BidEndDate)
|
||||
add("open_bid", "开标时间", b.OpenBidDate)
|
||||
return out
|
||||
}
|
||||
|
||||
// pickTenderReminders 从一条招标记录中挑出落在窗口内的到期项
|
||||
func pickTenderReminders(t models.TenantCrmBiddingTender, now, deadline time.Time) []biddingReminderItem {
|
||||
out := make([]biddingReminderItem, 0, 3)
|
||||
add := func(kind, kindName string, due *time.Time) {
|
||||
if due == nil {
|
||||
return
|
||||
}
|
||||
d := *due
|
||||
if d.Before(now) || d.After(deadline) {
|
||||
return
|
||||
}
|
||||
out = append(out, biddingReminderItem{
|
||||
BizType: "tender",
|
||||
BizID: t.ID,
|
||||
Kind: kind,
|
||||
KindName: kindName,
|
||||
Title: t.Title,
|
||||
DueDate: d.Format("2006-01-02 15:04:05"),
|
||||
RemainDays: daysBetween(now, d),
|
||||
Level: levelOf(daysBetween(now, d)),
|
||||
OwnerName: t.OwnerUserName,
|
||||
})
|
||||
}
|
||||
add("register_end", "报名截止", t.RegisterEndDate)
|
||||
add("bid_end", "投标截止", t.BidEndDate)
|
||||
add("open_bid", "开标时间", t.OpenBidDate)
|
||||
return out
|
||||
}
|
||||
|
||||
// daysBetween 计算今天(0点)到目标时间的剩余天数
|
||||
func daysBetween(now, target time.Time) int {
|
||||
n := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
t := time.Date(target.Year(), target.Month(), target.Day(), 0, 0, 0, 0, target.Location())
|
||||
return int(t.Sub(n).Hours() / 24)
|
||||
}
|
||||
|
||||
// levelOf 剩余天数 -> 提醒等级
|
||||
func levelOf(days int) string {
|
||||
if days <= 1 {
|
||||
return "danger"
|
||||
}
|
||||
if days <= 3 {
|
||||
return "warning"
|
||||
}
|
||||
return "info"
|
||||
}
|
||||
@@ -0,0 +1,812 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// =============================================================
|
||||
// CRM 招投标管理 - 子表 CRUD(统一接口)
|
||||
// =============================================================
|
||||
//
|
||||
// 每个子表控制器提供统一的接口:
|
||||
// - List 按外键 bid_id / tender_id 列出
|
||||
// - Create 新增(关联外键由请求体传入)
|
||||
// - Delete 软删除
|
||||
// - Update 更新(用于文件改名、任务进度等)
|
||||
// =============================================================
|
||||
|
||||
// ------------------------------ 投标文件 ------------------------------
|
||||
|
||||
// BackendCrmBiddingBidFileController 投标文件
|
||||
type BackendCrmBiddingBidFileController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// bidFilePayload
|
||||
type bidFilePayload struct {
|
||||
BidID uint64 `json:"bid_id"`
|
||||
FileType string `json:"file_type"`
|
||||
FileName string `json:"file_name"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileSize uint64 `json:"file_size"`
|
||||
FileExt string `json:"file_ext"`
|
||||
Version string `json:"version"`
|
||||
UploadUserID string `json:"upload_user_id"`
|
||||
UploadUserName string `json:"upload_user_name"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// List GET /backend/crm/bidding/bid-file/list?bid_id=1
|
||||
func (c *BackendCrmBiddingBidFileController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
bidID, _ := c.GetInt64("bid_id")
|
||||
if bidID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "bid_id 必填")
|
||||
return
|
||||
}
|
||||
var list []models.TenantCrmBiddingBidFile
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidFile)).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("bid_id", bidID).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").All(&list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Create POST /backend/crm/bidding/bid-file
|
||||
func (c *BackendCrmBiddingBidFileController) Create() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p bidFilePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.BidID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "bid_id 必填")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.FileName) == "" || strings.TrimSpace(p.FileURL) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "文件信息不完整")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
row := models.TenantCrmBiddingBidFile{
|
||||
TenantID: pipelineTenantID(claims),
|
||||
BidID: p.BidID,
|
||||
FileType: firstNonEmpty(p.FileType, "1"),
|
||||
FileName: strings.TrimSpace(p.FileName),
|
||||
FileURL: strings.TrimSpace(p.FileURL),
|
||||
FileSize: p.FileSize,
|
||||
FileExt: strings.TrimSpace(p.FileExt),
|
||||
Version: firstNonEmpty(p.Version, "1.0"),
|
||||
UploadUserID: firstNonEmpty(p.UploadUserID, pipelineUID(claims)),
|
||||
UploadUserName: firstNonEmpty(p.UploadUserName, resolveUserName(claims)),
|
||||
Remark: p.Remark,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "上传失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "上传投标文件", claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/crm/bidding/bid-file/:id
|
||||
func (c *BackendCrmBiddingBidFileController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidFile)).
|
||||
Filter("id", id).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var delRow models.TenantCrmBiddingBidFile
|
||||
_ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidFile)).Filter("id", id).One(&delRow)
|
||||
crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除投标文件", claims)
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// ------------------------------ 投标任务 ------------------------------
|
||||
|
||||
// BackendCrmBiddingBidTaskController 投标任务
|
||||
type BackendCrmBiddingBidTaskController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type bidTaskPayload struct {
|
||||
BidID uint64 `json:"bid_id"`
|
||||
TaskName string `json:"task_name"`
|
||||
TaskType string `json:"task_type"`
|
||||
Content string `json:"content"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
OwnerUserName string `json:"owner_user_name"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
Status int8 `json:"status"`
|
||||
Progress int8 `json:"progress"`
|
||||
Files string `json:"files"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// List
|
||||
func (c *BackendCrmBiddingBidTaskController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
bidID, _ := c.GetInt64("bid_id")
|
||||
if bidID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "bid_id 必填")
|
||||
return
|
||||
}
|
||||
var list []models.TenantCrmBiddingBidTask
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidTask)).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("bid_id", bidID).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").All(&list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Create
|
||||
func (c *BackendCrmBiddingBidTaskController) Create() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p bidTaskPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.BidID == 0 || strings.TrimSpace(p.TaskName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "任务信息不完整")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
row := models.TenantCrmBiddingBidTask{
|
||||
TenantID: pipelineTenantID(claims),
|
||||
BidID: p.BidID,
|
||||
TaskName: strings.TrimSpace(p.TaskName),
|
||||
TaskType: firstNonEmpty(p.TaskType, "1"),
|
||||
Content: p.Content,
|
||||
OwnerUserID: firstNonEmpty(p.OwnerUserID, pipelineUID(claims)),
|
||||
OwnerUserName: firstNonEmpty(p.OwnerUserName, resolveUserName(claims)),
|
||||
StartDate: parsePipelineDate(p.StartDate),
|
||||
EndDate: parsePipelineDate(p.EndDate),
|
||||
Status: p.Status,
|
||||
Progress: p.Progress,
|
||||
Files: p.Files,
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateUserName: resolveUserName(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "新建投标任务", claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Update
|
||||
func (c *BackendCrmBiddingBidTaskController) Update() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p bidTaskPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
var row models.TenantCrmBiddingBidTask
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingBidTask)).
|
||||
Filter("id", id).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).One(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "任务未找到")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.TaskName) != "" {
|
||||
row.TaskName = strings.TrimSpace(p.TaskName)
|
||||
}
|
||||
if strings.TrimSpace(p.TaskType) != "" {
|
||||
row.TaskType = p.TaskType
|
||||
}
|
||||
row.Content = p.Content
|
||||
if strings.TrimSpace(p.OwnerUserID) != "" {
|
||||
row.OwnerUserID = strings.TrimSpace(p.OwnerUserID)
|
||||
row.OwnerUserName = strings.TrimSpace(p.OwnerUserName)
|
||||
}
|
||||
row.StartDate = parsePipelineDate(p.StartDate)
|
||||
row.EndDate = parsePipelineDate(p.EndDate)
|
||||
row.Status = p.Status
|
||||
row.Progress = p.Progress
|
||||
row.Files = p.Files
|
||||
row.Remark = p.Remark
|
||||
row.UpdateTime = time.Now()
|
||||
if _, err := models.Orm.Update(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(pipelineTenantID(claims), 5, row.BidID, "update", "更新投标任务", claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID})
|
||||
}
|
||||
|
||||
// Delete
|
||||
func (c *BackendCrmBiddingBidTaskController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidTask)).
|
||||
Filter("id", id).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var delRow models.TenantCrmBiddingBidTask
|
||||
_ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidTask)).Filter("id", id).One(&delRow)
|
||||
crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除投标任务", claims)
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// ------------------------------ 投标结果 ------------------------------
|
||||
|
||||
// BackendCrmBiddingBidResultController 投标结果
|
||||
type BackendCrmBiddingBidResultController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// List
|
||||
func (c *BackendCrmBiddingBidResultController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
bidID, _ := c.GetInt64("bid_id")
|
||||
if bidID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "bid_id 必填")
|
||||
return
|
||||
}
|
||||
var list []models.TenantCrmBiddingBidResult
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidResult)).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("bid_id", bidID).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").All(&list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Detail
|
||||
func (c *BackendCrmBiddingBidResultController) Detail() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var row models.TenantCrmBiddingBidResult
|
||||
err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidResult)).
|
||||
Filter("id", id).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).One(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "投标结果未找到")
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, row)
|
||||
}
|
||||
|
||||
// ------------------------------ 投标报价 ------------------------------
|
||||
|
||||
// BackendCrmBiddingBidQuoteController 投标报价
|
||||
type BackendCrmBiddingBidQuoteController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type bidQuotePayload struct {
|
||||
BidID uint64 `json:"bid_id"`
|
||||
QuoteName string `json:"quote_name"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
Currency string `json:"currency"`
|
||||
TaxRate float64 `json:"tax_rate"`
|
||||
ValidDate string `json:"valid_date"`
|
||||
Products string `json:"products"`
|
||||
CostAnalysis string `json:"cost_analysis"`
|
||||
ProfitRate float64 `json:"profit_rate"`
|
||||
Status int8 `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// List
|
||||
func (c *BackendCrmBiddingBidQuoteController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
bidID, _ := c.GetInt64("bid_id")
|
||||
if bidID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "bid_id 必填")
|
||||
return
|
||||
}
|
||||
var list []models.TenantCrmBiddingBidQuote
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQuote)).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("bid_id", bidID).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").All(&list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Create
|
||||
func (c *BackendCrmBiddingBidQuoteController) Create() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p bidQuotePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.BidID == 0 || strings.TrimSpace(p.QuoteName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "报价信息不完整")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
row := models.TenantCrmBiddingBidQuote{
|
||||
TenantID: pipelineTenantID(claims),
|
||||
BidID: p.BidID,
|
||||
QuoteName: strings.TrimSpace(p.QuoteName),
|
||||
TotalAmount: p.TotalAmount,
|
||||
Currency: firstNonEmpty(p.Currency, "CNY"),
|
||||
TaxRate: p.TaxRate,
|
||||
ValidDate: parsePipelineDate(p.ValidDate),
|
||||
Products: p.Products,
|
||||
CostAnalysis: p.CostAnalysis,
|
||||
ProfitRate: p.ProfitRate,
|
||||
Status: 1,
|
||||
SubmitUserID: pipelineUID(claims),
|
||||
SubmitUserName: resolveUserName(claims),
|
||||
SubmitTime: &now,
|
||||
Remark: p.Remark,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "新建投标报价", claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Delete
|
||||
func (c *BackendCrmBiddingBidQuoteController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQuote)).
|
||||
Filter("id", id).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var delRow models.TenantCrmBiddingBidQuote
|
||||
_ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidQuote)).Filter("id", id).One(&delRow)
|
||||
crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除投标报价", claims)
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// ------------------------------ 投标竞争对手 ------------------------------
|
||||
|
||||
// BackendCrmBiddingBidCompetitorController
|
||||
type BackendCrmBiddingBidCompetitorController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type bidCompetitorPayload struct {
|
||||
BidID uint64 `json:"bid_id"`
|
||||
CompetitorName string `json:"competitor_name"`
|
||||
CompetitorType int8 `json:"competitor_type"`
|
||||
QuoteAmount float64 `json:"quote_amount"`
|
||||
Advantages string `json:"advantages"`
|
||||
Weaknesses string `json:"weaknesses"`
|
||||
Source string `json:"source"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// List
|
||||
func (c *BackendCrmBiddingBidCompetitorController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
bidID, _ := c.GetInt64("bid_id")
|
||||
if bidID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "bid_id 必填")
|
||||
return
|
||||
}
|
||||
var list []models.TenantCrmBiddingBidCompetitor
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidCompetitor)).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("bid_id", bidID).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").All(&list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Create
|
||||
func (c *BackendCrmBiddingBidCompetitorController) Create() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p bidCompetitorPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.BidID == 0 || strings.TrimSpace(p.CompetitorName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "竞争对手信息不完整")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
row := models.TenantCrmBiddingBidCompetitor{
|
||||
TenantID: pipelineTenantID(claims),
|
||||
BidID: p.BidID,
|
||||
CompetitorName: strings.TrimSpace(p.CompetitorName),
|
||||
CompetitorType: firstNonEmptyInt8(p.CompetitorType, 1),
|
||||
QuoteAmount: p.QuoteAmount,
|
||||
Advantages: p.Advantages,
|
||||
Weaknesses: p.Weaknesses,
|
||||
Source: strings.TrimSpace(p.Source),
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateUserName: resolveUserName(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "新增竞争对手", claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Delete
|
||||
func (c *BackendCrmBiddingBidCompetitorController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidCompetitor)).
|
||||
Filter("id", id).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var delRow models.TenantCrmBiddingBidCompetitor
|
||||
_ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidCompetitor)).Filter("id", id).One(&delRow)
|
||||
crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除竞争对手", claims)
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// ------------------------------ 投标费用 ------------------------------
|
||||
|
||||
// BackendCrmBiddingBidExpenseController
|
||||
type BackendCrmBiddingBidExpenseController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type bidExpensePayload struct {
|
||||
BidID uint64 `json:"bid_id"`
|
||||
ExpenseType int8 `json:"expense_type"`
|
||||
ExpenseName string `json:"expense_name"`
|
||||
Amount float64 `json:"amount"`
|
||||
Currency string `json:"currency"`
|
||||
PaymentStatus int8 `json:"payment_status"`
|
||||
PaymentDate string `json:"payment_date"`
|
||||
Description string `json:"description"`
|
||||
VoucherURL string `json:"voucher_url"`
|
||||
}
|
||||
|
||||
// List
|
||||
func (c *BackendCrmBiddingBidExpenseController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
bidID, _ := c.GetInt64("bid_id")
|
||||
if bidID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "bid_id 必填")
|
||||
return
|
||||
}
|
||||
var list []models.TenantCrmBiddingBidExpense
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidExpense)).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("bid_id", bidID).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").All(&list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Create
|
||||
func (c *BackendCrmBiddingBidExpenseController) Create() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p bidExpensePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.BidID == 0 || strings.TrimSpace(p.ExpenseName) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "费用信息不完整")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
row := models.TenantCrmBiddingBidExpense{
|
||||
TenantID: pipelineTenantID(claims),
|
||||
BidID: p.BidID,
|
||||
ExpenseType: firstNonEmptyInt8(p.ExpenseType, 1),
|
||||
ExpenseName: strings.TrimSpace(p.ExpenseName),
|
||||
Amount: p.Amount,
|
||||
Currency: firstNonEmpty(p.Currency, "CNY"),
|
||||
PaymentStatus: p.PaymentStatus,
|
||||
PaymentDate: parsePipelineDateTime(p.PaymentDate),
|
||||
Description: p.Description,
|
||||
VoucherURL: strings.TrimSpace(p.VoucherURL),
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateUserName: resolveUserName(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(pipelineTenantID(claims), 5, p.BidID, "create", "登记投标费用", claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Delete
|
||||
func (c *BackendCrmBiddingBidExpenseController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmBiddingBidExpense)).
|
||||
Filter("id", id).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var delRow models.TenantCrmBiddingBidExpense
|
||||
_ = models.Orm.QueryTable(new(models.TenantCrmBiddingBidExpense)).Filter("id", id).One(&delRow)
|
||||
crmWriteLog(pipelineTenantID(claims), 5, delRow.BidID, "delete", "删除投标费用", claims)
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// ------------------------------ 招标文件 ------------------------------
|
||||
|
||||
// BackendCrmBiddingTenderFileController 招标文件
|
||||
type BackendCrmBiddingTenderFileController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
type tenderFilePayload struct {
|
||||
TenderID uint64 `json:"tender_id"`
|
||||
FileType string `json:"file_type"`
|
||||
FileName string `json:"file_name"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileSize uint64 `json:"file_size"`
|
||||
FileExt string `json:"file_ext"`
|
||||
UploadUserID string `json:"upload_user_id"`
|
||||
UploadUserName string `json:"upload_user_name"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// List
|
||||
func (c *BackendCrmBiddingTenderFileController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
tenderID, _ := c.GetInt64("tender_id")
|
||||
if tenderID == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "tender_id 必填")
|
||||
return
|
||||
}
|
||||
var list []models.TenantCrmBiddingTenderFile
|
||||
_, _ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderFile)).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("tender_id", tenderID).
|
||||
Filter("delete_time__isnull", true).
|
||||
OrderBy("-id").All(&list)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"list": list})
|
||||
}
|
||||
|
||||
// Create
|
||||
func (c *BackendCrmBiddingTenderFileController) Create() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p tenderFilePayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if p.TenderID == 0 || strings.TrimSpace(p.FileName) == "" || strings.TrimSpace(p.FileURL) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "文件信息不完整")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
row := models.TenantCrmBiddingTenderFile{
|
||||
TenantID: pipelineTenantID(claims),
|
||||
TenderID: p.TenderID,
|
||||
FileType: firstNonEmpty(p.FileType, "1"),
|
||||
FileName: strings.TrimSpace(p.FileName),
|
||||
FileURL: strings.TrimSpace(p.FileURL),
|
||||
FileSize: p.FileSize,
|
||||
FileExt: strings.TrimSpace(p.FileExt),
|
||||
UploadUserID: firstNonEmpty(p.UploadUserID, pipelineUID(claims)),
|
||||
UploadUserName: firstNonEmpty(p.UploadUserName, resolveUserName(claims)),
|
||||
Remark: p.Remark,
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "上传失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(pipelineTenantID(claims), 5, p.TenderID, "create", "上传招标文件", claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Delete
|
||||
func (c *BackendCrmBiddingTenderFileController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderFile)).
|
||||
Filter("id", id).
|
||||
Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
var delRow models.TenantCrmBiddingTenderFile
|
||||
_ = models.Orm.QueryTable(new(models.TenantCrmBiddingTenderFile)).Filter("id", id).One(&delRow)
|
||||
crmWriteLog(pipelineTenantID(claims), 5, delRow.TenderID, "delete", "删除招标文件", claims)
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 工具函数
|
||||
// =====================================================================
|
||||
|
||||
// firstNonEmptyInt8 返回第一个非零的 int8
|
||||
func firstNonEmptyInt8(v, def int8) int8 {
|
||||
if v == 0 {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// =============================================================
|
||||
// CRM 招投标管理 - 招标方(Tender)
|
||||
// =============================================================
|
||||
//
|
||||
// 状态机:0草稿 -> 1公告中 -> 2报名中 -> 3投标中 -> 4开标中 -> 5评标中 -> 6已定标 -> 7已归档
|
||||
// 关联:定标 -> 中标供应商 -> 转合同
|
||||
// =============================================================
|
||||
|
||||
// BackendCrmBiddingTenderController 招标方
|
||||
type BackendCrmBiddingTenderController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// tenderPayload 创建/更新请求体
|
||||
type tenderPayload struct {
|
||||
TenderNo string `json:"tender_no"`
|
||||
Title string `json:"title"`
|
||||
TenderType string `json:"tender_type"`
|
||||
CustomerID uint64 `json:"customer_id"`
|
||||
CustomerName string `json:"customer_name"`
|
||||
PublishOrg string `json:"publish_org"`
|
||||
BudgetAmount float64 `json:"budget_amount"`
|
||||
BidDeposit float64 `json:"bid_deposit"`
|
||||
PublishDate string `json:"publish_date"`
|
||||
RegisterStartDate string `json:"register_start_date"`
|
||||
RegisterEndDate string `json:"register_end_date"`
|
||||
BidStartDate string `json:"bid_start_date"`
|
||||
BidEndDate string `json:"bid_end_date"`
|
||||
OpenBidDate string `json:"open_bid_date"`
|
||||
ContactPerson string `json:"contact_person"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
BidAddress string `json:"bid_address"`
|
||||
OpenAddress string `json:"open_address"`
|
||||
Description string `json:"description"`
|
||||
Requirement string `json:"requirement"`
|
||||
EvaluationMethod string `json:"evaluation_method"`
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
OwnerUserName string `json:"owner_user_name"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// List GET /backend/crm/bidding/tender/list
|
||||
func (c *BackendCrmBiddingTenderController) List() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
page, _ := c.GetInt("page", 1)
|
||||
pageSize, _ := c.GetInt("pageSize", 20)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
keyword := strings.TrimSpace(c.GetString("keyword"))
|
||||
status := strings.TrimSpace(c.GetString("status"))
|
||||
ownerID := strings.TrimSpace(c.GetString("owner_user_id"))
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
cond := orm.NewCondition().And("tenant_id", tenantID).And("delete_time__isnull", true)
|
||||
cond = crmApplyOwnerScope(cond, claims)
|
||||
|
||||
if keyword != "" {
|
||||
kw := orm.NewCondition().
|
||||
Or("title__contains", keyword).
|
||||
Or("tender_no__contains", keyword).
|
||||
Or("customer_name__contains", keyword).
|
||||
Or("publish_org__contains", keyword)
|
||||
cond = cond.AndCond(kw)
|
||||
}
|
||||
if status != "" {
|
||||
if v, errParse := strconv.Atoi(status); errParse == nil {
|
||||
cond = cond.And("status", int8(v))
|
||||
}
|
||||
}
|
||||
if ownerID != "" {
|
||||
cond = cond.And("owner_user_id", ownerID)
|
||||
}
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)).SetCond(cond)
|
||||
total, _ := qs.Count()
|
||||
var list []models.TenantCrmBiddingTender
|
||||
if total > 0 {
|
||||
_, _ = qs.OrderBy("-id").Offset((page - 1) * pageSize).Limit(pageSize).All(&list)
|
||||
}
|
||||
pipelineOk(&c.Controller, map[string]interface{}{
|
||||
"list": list, "total": total, "page": page, "pageSize": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// Detail GET /backend/crm/bidding/tender/detail/:id
|
||||
func (c *BackendCrmBiddingTenderController) Detail() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var row models.TenantCrmBiddingTender
|
||||
err = models.Orm.QueryTable(new(models.TenantCrmBiddingTender)).
|
||||
Filter("id", id).Filter("tenant_id", pipelineTenantID(claims)).
|
||||
Filter("delete_time__isnull", true).One(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "招标记录未找到")
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, row)
|
||||
}
|
||||
|
||||
// Create POST /backend/crm/bidding/tender
|
||||
func (c *BackendCrmBiddingTenderController) Create() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p tenderPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.Title) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "招标标题不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
now := time.Now()
|
||||
row := models.TenantCrmBiddingTender{
|
||||
TenantID: tenantID,
|
||||
TenderNo: strings.TrimSpace(p.TenderNo),
|
||||
Title: strings.TrimSpace(p.Title),
|
||||
TenderType: firstNonEmpty(p.TenderType, "1"),
|
||||
Status: 0,
|
||||
CustomerID: nilOrID(p.CustomerID),
|
||||
CustomerName: strings.TrimSpace(p.CustomerName),
|
||||
PublishOrg: firstNonEmpty(p.PublishOrg, p.CustomerName),
|
||||
BudgetAmount: p.BudgetAmount,
|
||||
BidDeposit: p.BidDeposit,
|
||||
PublishDate: parsePipelineDate(p.PublishDate),
|
||||
RegisterStartDate: parsePipelineDateTime(p.RegisterStartDate),
|
||||
RegisterEndDate: parsePipelineDateTime(p.RegisterEndDate),
|
||||
BidStartDate: parsePipelineDateTime(p.BidStartDate),
|
||||
BidEndDate: parsePipelineDateTime(p.BidEndDate),
|
||||
OpenBidDate: parsePipelineDateTime(p.OpenBidDate),
|
||||
ContactPerson: strings.TrimSpace(p.ContactPerson),
|
||||
ContactPhone: strings.TrimSpace(p.ContactPhone),
|
||||
ContactEmail: strings.TrimSpace(p.ContactEmail),
|
||||
BidAddress: strings.TrimSpace(p.BidAddress),
|
||||
OpenAddress: strings.TrimSpace(p.OpenAddress),
|
||||
Description: p.Description,
|
||||
Requirement: p.Requirement,
|
||||
EvaluationMethod: firstNonEmpty(p.EvaluationMethod, "1"),
|
||||
OwnerUserID: firstNonEmpty(p.OwnerUserID, pipelineUID(claims)),
|
||||
OwnerUserName: firstNonEmpty(p.OwnerUserName, resolveUserName(claims)),
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateUserName: resolveUserName(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
|
||||
if crmNameDuplicated(new(models.TenantCrmBiddingTender), tenantID, "title", row.Title) {
|
||||
pipelineErr(&c.Controller, 409, 409, "该招标标题已存在")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := models.Orm.Insert(&row)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 5, uint64(id), "create", "创建招标:"+row.Title, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
// Update PUT /backend/crm/bidding/tender/:id
|
||||
func (c *BackendCrmBiddingTenderController) Update() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
var p tenderPayload
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var row models.TenantCrmBiddingTender
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "招标记录未找到")
|
||||
return
|
||||
}
|
||||
if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "无权操作该招标数据")
|
||||
return
|
||||
}
|
||||
if row.Locked == 1 || row.Status == 6 {
|
||||
pipelineErr(&c.Controller, 400, 400, "招标记录已定标锁定,不能编辑")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(p.Title) == "" {
|
||||
pipelineErr(&c.Controller, 400, 400, "招标标题不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
row.TenderNo = strings.TrimSpace(p.TenderNo)
|
||||
row.Title = strings.TrimSpace(p.Title)
|
||||
if strings.TrimSpace(p.TenderType) != "" {
|
||||
row.TenderType = p.TenderType
|
||||
}
|
||||
row.CustomerID = nilOrID(p.CustomerID)
|
||||
row.CustomerName = strings.TrimSpace(p.CustomerName)
|
||||
row.PublishOrg = strings.TrimSpace(p.PublishOrg)
|
||||
row.BudgetAmount = p.BudgetAmount
|
||||
row.BidDeposit = p.BidDeposit
|
||||
row.PublishDate = parsePipelineDate(p.PublishDate)
|
||||
row.RegisterStartDate = parsePipelineDateTime(p.RegisterStartDate)
|
||||
row.RegisterEndDate = parsePipelineDateTime(p.RegisterEndDate)
|
||||
row.BidStartDate = parsePipelineDateTime(p.BidStartDate)
|
||||
row.BidEndDate = parsePipelineDateTime(p.BidEndDate)
|
||||
row.OpenBidDate = parsePipelineDateTime(p.OpenBidDate)
|
||||
row.ContactPerson = strings.TrimSpace(p.ContactPerson)
|
||||
row.ContactPhone = strings.TrimSpace(p.ContactPhone)
|
||||
row.ContactEmail = strings.TrimSpace(p.ContactEmail)
|
||||
row.BidAddress = strings.TrimSpace(p.BidAddress)
|
||||
row.OpenAddress = strings.TrimSpace(p.OpenAddress)
|
||||
row.Description = p.Description
|
||||
row.Requirement = p.Requirement
|
||||
if strings.TrimSpace(p.EvaluationMethod) != "" {
|
||||
row.EvaluationMethod = p.EvaluationMethod
|
||||
}
|
||||
if strings.TrimSpace(p.OwnerUserID) != "" {
|
||||
row.OwnerUserID = strings.TrimSpace(p.OwnerUserID)
|
||||
}
|
||||
if strings.TrimSpace(p.OwnerUserName) != "" {
|
||||
row.OwnerUserName = strings.TrimSpace(p.OwnerUserName)
|
||||
}
|
||||
row.Remark = p.Remark
|
||||
row.UpdateTime = time.Now()
|
||||
|
||||
if _, err := models.Orm.Update(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
crmWriteLog(tenantID, 5, row.ID, "update", "更新招标:"+row.Title, claims)
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"id": row.ID})
|
||||
}
|
||||
|
||||
// Delete DELETE /backend/crm/bidding/tender/:id
|
||||
func (c *BackendCrmBiddingTenderController) Delete() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var row models.TenantCrmBiddingTender
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "招标记录未找到")
|
||||
return
|
||||
}
|
||||
if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "无权操作该招标数据")
|
||||
return
|
||||
}
|
||||
if row.Locked == 1 || row.Status == 6 {
|
||||
pipelineErr(&c.Controller, 400, 400, "招标记录已定标锁定,不能删除")
|
||||
return
|
||||
}
|
||||
if !canDeleteCrmRecord(claims, row.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "只有创建人、租户管理员或平台管理员可以删除该招标")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
_, err = models.Orm.QueryTable(new(models.TenantCrmBiddingTender)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Update(map[string]interface{}{"delete_time": now, "update_time": now})
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "删除失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
pipelineOk(&c.Controller, nil)
|
||||
}
|
||||
|
||||
// Award POST /backend/crm/bidding/tender/:id/award 定标
|
||||
func (c *BackendCrmBiddingTenderController) Award() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
WinnerSupplierID uint64 `json:"winner_supplier_id"`
|
||||
WinnerSupplierName string `json:"winner_supplier_name"`
|
||||
WinnerAmount float64 `json:"winner_amount"`
|
||||
Announcement string `json:"announcement"`
|
||||
ResultType int8 `json:"result_type"` // 1定标 2流标 3废标
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var row models.TenantCrmBiddingTender
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "招标记录未找到")
|
||||
return
|
||||
}
|
||||
if !canAccessCrmRecord(claims, row.OwnerUserID, row.CreateUserID) {
|
||||
pipelineErr(&c.Controller, 401, 401, "无权操作该招标数据")
|
||||
return
|
||||
}
|
||||
if row.Locked == 1 {
|
||||
pipelineErr(&c.Controller, 400, 400, "招标记录已定标,不能重复定标")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
resultType := p.ResultType
|
||||
if resultType == 0 {
|
||||
resultType = 1
|
||||
}
|
||||
|
||||
// 写定标结果
|
||||
result := models.TenantCrmBiddingTenderResult{
|
||||
TenantID: tenantID,
|
||||
TenderID: row.ID,
|
||||
ResultType: resultType,
|
||||
WinnerSupplierID: nilOrID(p.WinnerSupplierID),
|
||||
WinnerSupplierName: strings.TrimSpace(p.WinnerSupplierName),
|
||||
WinnerAmount: p.WinnerAmount,
|
||||
ResultDate: &now,
|
||||
Announcement: p.Announcement,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateUserName: resolveUserName(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
if _, err := models.Orm.Insert(&result); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "写入定标结果失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新招标主表:状态 6已定标 + 锁定
|
||||
row.Status = 6
|
||||
row.Locked = 1
|
||||
row.WinnerSupplierID = nilOrID(p.WinnerSupplierID)
|
||||
row.WinnerSupplierName = strings.TrimSpace(p.WinnerSupplierName)
|
||||
row.WinnerAmount = p.WinnerAmount
|
||||
row.WinnerDate = &now
|
||||
row.UpdateTime = now
|
||||
if _, err := models.Orm.Update(&row, "status", "locked", "winner_supplier_id", "winner_supplier_name", "winner_amount", "winner_date", "update_time"); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新招标状态失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
crmWriteLog(tenantID, 5, row.ID, "award",
|
||||
fmt.Sprintf("定标:%s 中标金额 %.2f", p.WinnerSupplierName, p.WinnerAmount), claims)
|
||||
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"result_id": result.ID})
|
||||
}
|
||||
|
||||
// AwardToContract POST /backend/crm/bidding/tender/:id/contract 定标转合同
|
||||
func (c *BackendCrmBiddingTenderController) AwardToContract() {
|
||||
claims, err := pipelineClaims(&c.Controller)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 401, 401, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if id == 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "无效的ID")
|
||||
return
|
||||
}
|
||||
var p struct {
|
||||
ContractName string `json:"contract_name"`
|
||||
ContractNo string `json:"contract_no"`
|
||||
ContractCategory string `json:"contract_category"`
|
||||
SignDate string `json:"sign_date"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
pipelineErr(&c.Controller, 400, 400, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := pipelineTenantID(claims)
|
||||
var row models.TenantCrmBiddingTender
|
||||
if err := models.Orm.QueryTable(new(models.TenantCrmBiddingTender)).
|
||||
Filter("id", id).Filter("tenant_id", tenantID).
|
||||
Filter("delete_time__isnull", true).One(&row); err != nil {
|
||||
pipelineErr(&c.Controller, 404, 404, "招标记录未找到")
|
||||
return
|
||||
}
|
||||
if row.Status != 6 || row.Locked != 1 {
|
||||
pipelineErr(&c.Controller, 400, 400, "招标未定标,无法转合同")
|
||||
return
|
||||
}
|
||||
if row.ContractID != nil && *row.ContractID > 0 {
|
||||
pipelineErr(&c.Controller, 400, 400, "该招标已转合同,不能重复转")
|
||||
return
|
||||
}
|
||||
|
||||
contractName := strings.TrimSpace(p.ContractName)
|
||||
if contractName == "" {
|
||||
contractName = row.Title
|
||||
}
|
||||
category := strings.TrimSpace(p.ContractCategory)
|
||||
if category == "" {
|
||||
category = "2" // 服务类
|
||||
}
|
||||
now := time.Now()
|
||||
tenderID := row.ID
|
||||
|
||||
contract := models.TenantCrmContract{
|
||||
TenantID: tenantID,
|
||||
ContractNo: strings.TrimSpace(p.ContractNo),
|
||||
ContractName: contractName,
|
||||
ContractCategory: category,
|
||||
OurRole: 1, // 我方为甲方
|
||||
PartyCount: 2,
|
||||
OwnerUserID: row.OwnerUserID,
|
||||
OwnerUserName: row.OwnerUserName,
|
||||
SignDate: parsePipelineDate(p.SignDate),
|
||||
Step: 1,
|
||||
Status: 1,
|
||||
Remark: p.Remark,
|
||||
CreateUserID: pipelineUID(claims),
|
||||
CreateTime: now,
|
||||
UpdateTime: now,
|
||||
}
|
||||
contractID, err := models.Orm.Insert(&contract)
|
||||
if err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "创建合同失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
cid := uint64(contractID)
|
||||
row.ContractID = &cid
|
||||
row.UpdateTime = now
|
||||
if _, err := models.Orm.Update(&row, "contract_id", "update_time"); err != nil {
|
||||
pipelineErr(&c.Controller, 500, 500, "更新招标合同ID失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
crmWriteLog(tenantID, 5, tenderID, "award-contract", "定标转合同:"+contractName, claims)
|
||||
crmWriteLog(tenantID, 1, cid, "create", "由招标「"+row.Title+"」定标转化生成", claims)
|
||||
|
||||
pipelineOk(&c.Controller, map[string]interface{}{"contract_id": contractID})
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
-- ============================================================
|
||||
-- CRM 招投标管理 —— 菜单配置(yz_system_menu)
|
||||
-- 幂等脚本:按 path 判重,重复执行不会产生重复菜单
|
||||
-- 执行:mysql -h <host> -P <port> -u <user> -p <db> < bidding_menus.sql
|
||||
-- ============================================================
|
||||
|
||||
-- 菜单表字段说明(yz_system_menu)
|
||||
-- type: 1=目录菜单,2=页面菜单
|
||||
-- pid : 父菜单ID
|
||||
-- views: 可见端标识,沿用父级 '[2]'(后台)
|
||||
-- is_visible / status: 1 表示显示并启用
|
||||
|
||||
-- 顶层入口「招投标」已存在:path = '/apps/crm/bidding'
|
||||
SET @bidding_id := (
|
||||
SELECT id FROM yz_system_menu
|
||||
WHERE path = '/apps/crm/bidding' AND delete_time IS NULL
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
-- ---------- 一级子页面 ----------
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @bidding_id, '我要投标', '/apps/crm/bidding/bid', 'apps/crm/bidding/bid/index.vue',
|
||||
'Trophy', 1, 1, 1, '[2]', 2, 'system', '投标方视角:从商机/项目发起投标,中标可转合同/项目'
|
||||
FROM DUAL
|
||||
WHERE @bidding_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/bid' AND delete_time IS NULL) x);
|
||||
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @bidding_id, '我要招标', '/apps/crm/bidding/tender', 'apps/crm/bidding/tender/index.vue',
|
||||
'Document', 2, 1, 1, '[2]', 2, 'system', '招标方视角:发布公告、报名、评标、定标'
|
||||
FROM DUAL
|
||||
WHERE @bidding_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/tender' AND delete_time IS NULL) x);
|
||||
|
||||
-- ---------- 公共资料库(目录 + 两个页面) ----------
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @bidding_id, '公共资料库', '/apps/crm/bidding/library', NULL,
|
||||
'Folder', 3, 1, 1, '[2]', 1, 'system', '标书模板 + 企业资质'
|
||||
FROM DUAL
|
||||
WHERE @bidding_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/library' AND delete_time IS NULL) x);
|
||||
|
||||
SET @library_id := (
|
||||
SELECT id FROM yz_system_menu
|
||||
WHERE path = '/apps/crm/bidding/library' AND delete_time IS NULL
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @library_id, '标书模板', '/apps/crm/bidding/library/template', 'apps/crm/bidding/library/template.vue',
|
||||
'Files', 1, 1, 1, '[2]', 2, 'system', '技术标/商务标/综合标模板库'
|
||||
FROM DUAL
|
||||
WHERE @library_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/library/template' AND delete_time IS NULL) x);
|
||||
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @library_id, '企业资质', '/apps/crm/bidding/library/qualification', 'apps/crm/bidding/library/qualification.vue',
|
||||
'Stamp', 2, 1, 1, '[2]', 2, 'system', '证照资质库,临期自动预警'
|
||||
FROM DUAL
|
||||
WHERE @library_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/library/qualification' AND delete_time IS NULL) x);
|
||||
|
||||
-- ---------- 统计报表(目录 + 两个页面) ----------
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @bidding_id, '统计报表', '/apps/crm/bidding/statistics', NULL,
|
||||
'DataAnalysis', 4, 1, 1, '[2]', 1, 'system', '招标/投标数据统计'
|
||||
FROM DUAL
|
||||
WHERE @bidding_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/statistics' AND delete_time IS NULL) x);
|
||||
|
||||
SET @stats_id := (
|
||||
SELECT id FROM yz_system_menu
|
||||
WHERE path = '/apps/crm/bidding/statistics' AND delete_time IS NULL
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @stats_id, '招标统计', '/apps/crm/bidding/statistics/tender', 'apps/crm/bidding/statistics/tender.vue',
|
||||
'PieChart', 1, 1, 1, '[2]', 2, 'system', '招标状态/类型分布、预算与中标金额'
|
||||
FROM DUAL
|
||||
WHERE @stats_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/statistics/tender' AND delete_time IS NULL) x);
|
||||
|
||||
INSERT INTO yz_system_menu
|
||||
(pid, title, path, component_path, icon, sort, status, is_visible, views, type, creater, remark)
|
||||
SELECT @stats_id, '投标统计', '/apps/crm/bidding/statistics/bid', 'apps/crm/bidding/statistics/bid.vue',
|
||||
'Histogram', 2, 1, 1, '[2]', 2, 'system', '投标数量、中标率、费用投入与月度趋势'
|
||||
FROM DUAL
|
||||
WHERE @stats_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM yz_system_menu WHERE path = '/apps/crm/bidding/statistics/bid' AND delete_time IS NULL) x);
|
||||
|
||||
-- ---------- 结果校验 ----------
|
||||
SELECT id, pid, title, path, component_path, type, sort
|
||||
FROM yz_system_menu
|
||||
WHERE delete_time IS NULL AND path LIKE '/apps/crm/bidding%'
|
||||
ORDER BY pid, sort;
|
||||
@@ -0,0 +1,526 @@
|
||||
-- =============================================================
|
||||
-- CRM 招投标管理模块表结构(17 张)
|
||||
--
|
||||
-- 模块说明:CRM 系统下的「招投标管理」子模块
|
||||
-- - 我要招标(甲方):6 张表
|
||||
-- - 我要投标(乙方):11 张表
|
||||
-- 设计原则:
|
||||
-- 1. 命名统一以 yz_backend_crm_bidding_ 为前缀
|
||||
-- 2. 软删除:delete_time
|
||||
-- 3. 多租户:tenant_id
|
||||
-- 4. 与现有 CRM 模块(business/contract/project)字段风格一致
|
||||
-- 5. 中标/定标后只存 ID 引用(contract_id、project_id),不冗余数据
|
||||
--
|
||||
-- 状态机:
|
||||
-- 招标:0草稿 -> 1公告中 -> 2报名中 -> 3投标中 -> 4开标中 -> 5评标中 -> 6已定标 -> 7已归档
|
||||
-- 投标:0草稿 -> 1立项审批中 -> 2投标中 -> 3已开标 -> (4中标 | 5落标) -> (6已转合同 | 7已归档)
|
||||
-- =============================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ============================================================
|
||||
-- 第一部分:我要招标(6 张)
|
||||
-- ============================================================
|
||||
|
||||
-- ---------------- 1. 招标项目主表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '租户ID',
|
||||
`tender_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '招标编号',
|
||||
`title` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '招标标题/项目名称',
|
||||
`tender_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '招标类型: 1公开招标 2邀请招标 3竞争性谈判 4询比 5单一来源',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1公告中 2报名中 3投标中 4开标中 5评标中 6已定标 7已归档',
|
||||
`customer_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '招标方客户ID(我方客户库)',
|
||||
`customer_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '招标方名称',
|
||||
`publish_org` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '发布单位',
|
||||
`budget_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '预算金额',
|
||||
`bid_deposit` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '保证金',
|
||||
`publish_date` DATE DEFAULT NULL COMMENT '发布日期',
|
||||
`register_start_date` DATETIME DEFAULT NULL COMMENT '报名开始',
|
||||
`register_end_date` DATETIME DEFAULT NULL COMMENT '报名截止',
|
||||
`bid_start_date` DATETIME DEFAULT NULL COMMENT '投标开始',
|
||||
`bid_end_date` DATETIME DEFAULT NULL COMMENT '投标截止',
|
||||
`open_bid_date` DATETIME DEFAULT NULL COMMENT '开标时间',
|
||||
`contact_person` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '联系人',
|
||||
`contact_phone` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '联系电话',
|
||||
`contact_email` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '联系邮箱',
|
||||
`bid_address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '投标地址',
|
||||
`open_address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '开标地址',
|
||||
`description` TEXT COMMENT '项目描述',
|
||||
`requirement` TEXT COMMENT '投标要求',
|
||||
`evaluation_method` VARCHAR(50) NOT NULL DEFAULT '1' COMMENT '评标方法: 1综合评估 2最低价 3性价比',
|
||||
`winner_supplier_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '中标供应商ID',
|
||||
`winner_supplier_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '中标供应商名称',
|
||||
`winner_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '中标金额',
|
||||
`winner_date` DATETIME DEFAULT NULL COMMENT '定标时间',
|
||||
`contract_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '转合同ID',
|
||||
`project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联项目ID',
|
||||
`owner_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '负责人ID',
|
||||
`owner_user_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '负责人姓名',
|
||||
`department_id` BIGINT UNSIGNED DEFAULT 0 COMMENT '部门ID',
|
||||
`locked` TINYINT NOT NULL DEFAULT 0 COMMENT '0否 1是(定标后锁定)',
|
||||
`remark` TEXT COMMENT '备注',
|
||||
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`create_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_status` (`tenant_id`, `status`),
|
||||
KEY `idx_tender_no` (`tenant_id`, `tender_no`),
|
||||
KEY `idx_owner` (`tenant_id`, `owner_user_id`),
|
||||
KEY `idx_customer` (`tenant_id`, `customer_id`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-招标项目主表';
|
||||
|
||||
-- ---------------- 2. 招标文件表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_file` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID',
|
||||
`file_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1招标文件 2补充文件 3答疑文件 4其他',
|
||||
`file_name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '文件名称',
|
||||
`file_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '文件URL',
|
||||
`file_size` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '文件大小(字节)',
|
||||
`file_ext` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '扩展名',
|
||||
`upload_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`upload_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_tender` (`tenant_id`, `tender_id`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-招标文件';
|
||||
|
||||
-- ---------------- 3. 招标公告表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_announce` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID',
|
||||
`announce_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1招标公告 2变更公告 3中标公告 4终止公告',
|
||||
`title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '公告标题',
|
||||
`content` LONGTEXT COMMENT '公告内容(富文本)',
|
||||
`publish_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1已发布',
|
||||
`publish_time` DATETIME DEFAULT NULL COMMENT '发布时间',
|
||||
`view_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '浏览次数',
|
||||
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`create_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_tender` (`tenant_id`, `tender_id`),
|
||||
KEY `idx_announce_type` (`tenant_id`, `announce_type`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-招标公告';
|
||||
|
||||
-- ---------------- 4. 投标方/供应商表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_supplier` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID',
|
||||
`supplier_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '供应商ID(ERP/CRM共享)',
|
||||
`supplier_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '投标方/供应商名称',
|
||||
`register_status` TINYINT NOT NULL DEFAULT 1 COMMENT '1待审核 2已通过 3已拒绝 4已退标',
|
||||
`register_time` DATETIME DEFAULT NULL COMMENT '报名时间',
|
||||
`bid_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '报价金额',
|
||||
`bid_files` TEXT COMMENT '投标文件JSON(多文件)',
|
||||
`contact_person` VARCHAR(50) NOT NULL DEFAULT '',
|
||||
`contact_phone` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`is_winner` TINYINT NOT NULL DEFAULT 0 COMMENT '0否 1是中标方',
|
||||
`ranking` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '排名',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`create_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_tender` (`tenant_id`, `tender_id`),
|
||||
KEY `idx_supplier` (`tenant_id`, `supplier_id`),
|
||||
KEY `idx_register_status` (`tenant_id`, `register_status`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标方/供应商';
|
||||
|
||||
-- ---------------- 5. 开标评标表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_evaluation` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID',
|
||||
`supplier_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '被评标供应商ID',
|
||||
`supplier_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '供应商名称',
|
||||
`technical_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '技术分',
|
||||
`business_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '商务分',
|
||||
`price_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '价格分',
|
||||
`service_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '服务分',
|
||||
`total_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '总分',
|
||||
`weight_config` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '权重配置JSON',
|
||||
`ranking` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '排名',
|
||||
`evaluation_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '评标人ID',
|
||||
`evaluation_user_name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '评标人姓名',
|
||||
`evaluation_time` DATETIME DEFAULT NULL COMMENT '评标时间',
|
||||
`comment` TEXT COMMENT '评语',
|
||||
`result` TINYINT NOT NULL DEFAULT 0 COMMENT '0待评 1合格 2不合格 3废标',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_tender` (`tenant_id`, `tender_id`),
|
||||
KEY `idx_supplier` (`tenant_id`, `supplier_id`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-开标评标';
|
||||
|
||||
-- ---------------- 6. 定标结果表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_tender_result` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`tender_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '招标项目ID',
|
||||
`result_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1定标 2流标 3废标',
|
||||
`winner_supplier_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '中标供应商ID',
|
||||
`winner_supplier_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '中标供应商名称',
|
||||
`winner_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '中标金额',
|
||||
`result_date` DATETIME DEFAULT NULL COMMENT '定标日期',
|
||||
`announcement` TEXT COMMENT '定标公告',
|
||||
`contract_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '转合同ID',
|
||||
`project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联项目ID',
|
||||
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`create_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_tender` (`tenant_id`, `tender_id`),
|
||||
KEY `idx_result_type` (`tenant_id`, `result_type`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-定标结果';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 第二部分:我要投标(11 张)
|
||||
-- ============================================================
|
||||
|
||||
-- ---------------- 7. 投标项目主表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`bid_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '投标编号',
|
||||
`title` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '投标标题/项目名称',
|
||||
`bid_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '投标类型: 1公开 2邀请 3竞争性谈判 4询比 5单一来源',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1立项审批中 2投标中 3已开标 4中标 5落标 6已转合同 7已归档',
|
||||
`source_type` TINYINT NOT NULL DEFAULT 0 COMMENT '0独立 1商机 2项目',
|
||||
`source_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '来源ID(商机/项目)',
|
||||
`business_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联商机ID',
|
||||
`business_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '商机名称',
|
||||
`project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联项目ID(中转)',
|
||||
`project_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '项目名称',
|
||||
`tender_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '关联招标项目ID(如果从招标信息创建)',
|
||||
`tender_no` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '招标编号',
|
||||
`customer_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '招标方ID',
|
||||
`customer_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '招标方名称',
|
||||
`publish_org` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '发布单位',
|
||||
`bid_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '投标报价',
|
||||
`bid_deposit` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '保证金',
|
||||
`publish_date` DATE DEFAULT NULL COMMENT '招标发布日期',
|
||||
`register_end_date` DATETIME DEFAULT NULL COMMENT '报名截止',
|
||||
`bid_end_date` DATETIME DEFAULT NULL COMMENT '投标截止',
|
||||
`open_bid_date` DATETIME DEFAULT NULL COMMENT '开标时间',
|
||||
`contact_person` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '联系人',
|
||||
`contact_phone` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`bid_address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '投标地址',
|
||||
`open_address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '开标地址',
|
||||
`win_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未开标 1中标 2未中标 3废标 4流标',
|
||||
`win_date` DATETIME DEFAULT NULL COMMENT '中标日期',
|
||||
`our_ranking` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '我方排名',
|
||||
`total_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '我方得分',
|
||||
`competitor_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '竞争对手数量',
|
||||
`contract_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '转合同ID',
|
||||
`new_project_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '中标后新建项目ID',
|
||||
`owner_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`owner_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`department_id` BIGINT UNSIGNED DEFAULT 0 COMMENT '部门ID',
|
||||
`description` TEXT COMMENT '项目描述',
|
||||
`remark` TEXT COMMENT '备注',
|
||||
`locked` TINYINT NOT NULL DEFAULT 0 COMMENT '0否 1是',
|
||||
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`create_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_status` (`tenant_id`, `status`),
|
||||
KEY `idx_bid_no` (`tenant_id`, `bid_no`),
|
||||
KEY `idx_owner` (`tenant_id`, `owner_user_id`),
|
||||
KEY `idx_business` (`tenant_id`, `business_id`),
|
||||
KEY `idx_project` (`tenant_id`, `project_id`),
|
||||
KEY `idx_tender` (`tenant_id`, `tender_id`),
|
||||
KEY `idx_customer` (`tenant_id`, `customer_id`),
|
||||
KEY `idx_win_status` (`tenant_id`, `win_status`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标项目主表';
|
||||
|
||||
-- ---------------- 8. 投标文件表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_file` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID',
|
||||
`file_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1投标文件 2技术方案 3商务方案 4报价单 5保证金 7其他',
|
||||
`file_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`file_url` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`file_size` BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`file_ext` VARCHAR(20) NOT NULL DEFAULT '',
|
||||
`version` VARCHAR(20) NOT NULL DEFAULT '1.0' COMMENT '版本号',
|
||||
`upload_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`upload_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_bid` (`tenant_id`, `bid_id`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标文件';
|
||||
|
||||
-- ---------------- 9. 投标立项审批表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_approval` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID',
|
||||
`approval_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1立项审批 2报价审批 3合同审批',
|
||||
`approval_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审批 1已通过 2已拒绝 3已撤回',
|
||||
`approval_flow_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '审批流ID(复用现有引擎)',
|
||||
`current_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '当前审批人',
|
||||
`current_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`approval_users` TEXT COMMENT '审批人列表JSON',
|
||||
`submit_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`submit_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`submit_time` DATETIME DEFAULT NULL,
|
||||
`complete_time` DATETIME DEFAULT NULL,
|
||||
`comment` TEXT COMMENT '审批意见',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_bid` (`tenant_id`, `bid_id`),
|
||||
KEY `idx_flow` (`approval_flow_id`),
|
||||
KEY `idx_status` (`tenant_id`, `approval_status`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标立项审批';
|
||||
|
||||
-- ---------------- 10. 投标任务表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_task` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID',
|
||||
`task_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '任务名称',
|
||||
`task_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1商务 2技术 3报价 4法务 5财务 6其他',
|
||||
`content` TEXT COMMENT '任务内容',
|
||||
`owner_user_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '负责人',
|
||||
`owner_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`start_date` DATE DEFAULT NULL,
|
||||
`end_date` DATE DEFAULT NULL,
|
||||
`complete_time` DATETIME DEFAULT NULL,
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未开始 1进行中 2已完成 3已逾期 4已取消',
|
||||
`progress` TINYINT NOT NULL DEFAULT 0 COMMENT '进度 0-100',
|
||||
`files` TEXT COMMENT '附件JSON',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`create_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_bid` (`tenant_id`, `bid_id`),
|
||||
KEY `idx_owner` (`tenant_id`, `owner_user_id`),
|
||||
KEY `idx_status` (`tenant_id`, `status`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标任务';
|
||||
|
||||
-- ---------------- 11. 标书模板表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_template` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`template_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '模板名称',
|
||||
`template_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1技术标 2商务标 3综合标',
|
||||
`industry` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '适用行业',
|
||||
`description` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`content` LONGTEXT COMMENT '模板内容',
|
||||
`file_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '模板文件URL',
|
||||
`use_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用次数',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0禁用',
|
||||
`owner_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`owner_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_type` (`tenant_id`, `template_type`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-标书模板';
|
||||
|
||||
-- ---------------- 12. 标书版本表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_document` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID',
|
||||
`template_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '基于模板',
|
||||
`version` VARCHAR(20) NOT NULL DEFAULT 'v1.0' COMMENT '版本号',
|
||||
`title` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '版本标题',
|
||||
`content` LONGTEXT COMMENT '标书内容',
|
||||
`file_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '标书文件URL',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1编辑中 2待审核 3已定稿 4已提交',
|
||||
`editor_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`editor_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`review_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`review_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`review_comment` TEXT COMMENT '审核意见',
|
||||
`review_time` DATETIME DEFAULT NULL,
|
||||
`submit_time` DATETIME DEFAULT NULL,
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_bid` (`tenant_id`, `bid_id`),
|
||||
KEY `idx_template` (`template_id`),
|
||||
KEY `idx_status` (`tenant_id`, `status`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-标书版本';
|
||||
|
||||
-- ---------------- 13. 资质文件库 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_qualification` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`qualification_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '资质名称',
|
||||
`qualification_type` VARCHAR(20) NOT NULL DEFAULT '1' COMMENT '1营业执照 2资质证书 3业绩证明 4认证证书 5其他',
|
||||
`qualification_no` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '证书编号',
|
||||
`issuing_authority` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '颁发机构',
|
||||
`issue_date` DATE DEFAULT NULL COMMENT '颁发日期',
|
||||
`expire_date` DATE DEFAULT NULL COMMENT '到期日期',
|
||||
`alert_days` INT UNSIGNED NOT NULL DEFAULT 30 COMMENT '提前提醒天数',
|
||||
`file_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '文件URL',
|
||||
`file_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`file_size` BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`description` TEXT COMMENT '备注',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1有效 2即将过期 0失效',
|
||||
`owner_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`owner_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_type` (`tenant_id`, `qualification_type`),
|
||||
KEY `idx_expire` (`tenant_id`, `expire_date`),
|
||||
KEY `idx_status` (`tenant_id`, `status`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-资质文件库';
|
||||
|
||||
-- ---------------- 14. 报价表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_quote` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID',
|
||||
`quote_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '报价单名称',
|
||||
`total_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '总报价',
|
||||
`currency` VARCHAR(10) NOT NULL DEFAULT 'CNY' COMMENT '币种',
|
||||
`tax_rate` DECIMAL(6,4) NOT NULL DEFAULT 0 COMMENT '税率',
|
||||
`valid_date` DATE DEFAULT NULL COMMENT '报价有效期',
|
||||
`products` LONGTEXT COMMENT '产品清单JSON',
|
||||
`cost_analysis` TEXT COMMENT '成本分析',
|
||||
`profit_rate` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '利润率%',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1草稿 2待审批 3已审批 4已使用',
|
||||
`approval_flow_id` BIGINT UNSIGNED DEFAULT NULL,
|
||||
`submit_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`submit_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`submit_time` DATETIME DEFAULT NULL,
|
||||
`approval_time` DATETIME DEFAULT NULL,
|
||||
`remark` TEXT COMMENT '备注',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_bid` (`tenant_id`, `bid_id`),
|
||||
KEY `idx_status` (`tenant_id`, `status`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-报价表';
|
||||
|
||||
-- ---------------- 15. 竞争对手报价表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_competitor` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID',
|
||||
`competitor_name` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '竞争对手名称',
|
||||
`competitor_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1同类厂商 2关联企业 3其他',
|
||||
`quote_amount` DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT '报价',
|
||||
`advantages` TEXT COMMENT '优势',
|
||||
`weaknesses` TEXT COMMENT '劣势',
|
||||
`source` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '信息来源',
|
||||
`remark` VARCHAR(500) NOT NULL DEFAULT '',
|
||||
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`create_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_bid` (`tenant_id`, `bid_id`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-竞争对手报价';
|
||||
|
||||
-- ---------------- 16. 投标结果表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_result` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID',
|
||||
`result_type` TINYINT NOT NULL DEFAULT 0 COMMENT '1中标 2未中标 3废标 4流标',
|
||||
`announce_date` DATETIME DEFAULT NULL COMMENT '公告日期',
|
||||
`announce_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '公告URL',
|
||||
`total_score` DECIMAL(6,2) NOT NULL DEFAULT 0 COMMENT '我方得分',
|
||||
`our_ranking` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '我方排名',
|
||||
`competitor_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '竞争对手数量',
|
||||
`reason` TEXT COMMENT '原因分析',
|
||||
`lessons` TEXT COMMENT '经验教训',
|
||||
`next_step` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '下一步行动',
|
||||
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`create_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_unique_bid` (`tenant_id`, `bid_id`),
|
||||
KEY `idx_result_type` (`tenant_id`, `result_type`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标结果';
|
||||
|
||||
-- ---------------- 17. 投标费用表 ----------------
|
||||
CREATE TABLE IF NOT EXISTS `yz_backend_crm_bidding_bid_expense` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tenant_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`bid_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '投标项目ID',
|
||||
`expense_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1标书费 2保证金 3差旅费 4咨询费 5代理费 6其他',
|
||||
`expense_name` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '费用名称',
|
||||
`amount` DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '金额',
|
||||
`currency` VARCHAR(10) NOT NULL DEFAULT 'CNY',
|
||||
`payment_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0未付 1已付 2已退还 3已抵扣',
|
||||
`payment_date` DATETIME DEFAULT NULL COMMENT '支付日期',
|
||||
`payment_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`payment_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`voucher_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '凭证URL',
|
||||
`description` TEXT COMMENT '说明',
|
||||
`create_user_id` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`create_user_name` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
`delete_time` DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tenant_bid` (`tenant_id`, `bid_id`),
|
||||
KEY `idx_payment_status` (`tenant_id`, `payment_status`),
|
||||
KEY `idx_delete_time` (`delete_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CRM-投标费用';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 菜单配置(投标管理)
|
||||
-- ============================================================
|
||||
-- 招标方菜单:path = 'apps/crm/bidding/tender/index.vue'
|
||||
-- 投标方菜单:path = 'apps/crm/bidding/bid/index.vue'
|
||||
-- 注意:菜单通常通过平台端 UI 配置,详见 docs/bidding_menus.sql
|
||||
@@ -97,6 +97,24 @@ func Init(_ string) {
|
||||
new(TenantCrmContract),
|
||||
new(TenantCrmPayback),
|
||||
new(TenantCrmPaybackRecord),
|
||||
// 招投标管理(docs/sql/create_crm_bidding.sql)
|
||||
new(TenantCrmBiddingTender),
|
||||
new(TenantCrmBiddingTenderFile),
|
||||
new(TenantCrmBiddingTenderAnnounce),
|
||||
new(TenantCrmBiddingTenderSupplier),
|
||||
new(TenantCrmBiddingTenderEvaluation),
|
||||
new(TenantCrmBiddingTenderResult),
|
||||
new(TenantCrmBiddingBid),
|
||||
new(TenantCrmBiddingBidFile),
|
||||
new(TenantCrmBiddingBidApproval),
|
||||
new(TenantCrmBiddingBidTask),
|
||||
new(TenantCrmBiddingBidTemplate),
|
||||
new(TenantCrmBiddingBidDocument),
|
||||
new(TenantCrmBiddingBidQualification),
|
||||
new(TenantCrmBiddingBidQuote),
|
||||
new(TenantCrmBiddingBidCompetitor),
|
||||
new(TenantCrmBiddingBidResult),
|
||||
new(TenantCrmBiddingBidExpense),
|
||||
// 平台更新通知(docs/sql/platform_upgrade.sql)
|
||||
new(PlatformUpgradeNotice),
|
||||
new(ErpAccountSet),
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// =============================================================
|
||||
// 投标方(我要投标):主表 + 文件 + 审批 + 任务(4 张)
|
||||
// =============================================================
|
||||
|
||||
// TenantCrmBiddingBid 投标项目主表: yz_backend_crm_bidding_bid
|
||||
//
|
||||
// 状态机:
|
||||
// 0草稿 -> 1立项审批中 -> 2投标中 -> 3已开标 -> (4中标 | 5落标) -> (6已转合同 | 7已归档)
|
||||
//
|
||||
// 来源关联:
|
||||
// - source_type: 0独立 1商机 2项目
|
||||
// - source_id: 来源ID(商机/项目)
|
||||
type TenantCrmBiddingBid struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BidNo string `orm:"column(bid_no);size(50)" json:"bid_no"` // 投标编号
|
||||
Title string `orm:"column(title);size(200)" json:"title"` // 投标标题
|
||||
BidType string `orm:"column(bid_type);size(20);default(1)" json:"bid_type"`
|
||||
Status int8 `orm:"column(status);default(0)" json:"status"` // 状态
|
||||
SourceType int8 `orm:"column(source_type);default(0)" json:"source_type"`
|
||||
SourceID *uint64 `orm:"column(source_id);null" json:"source_id"`
|
||||
BusinessID *uint64 `orm:"column(business_id);null" json:"business_id"` // 关联商机
|
||||
BusinessName string `orm:"column(business_name);size(200)" json:"business_name"`
|
||||
ProjectID *uint64 `orm:"column(project_id);null" json:"project_id"` // 关联项目(中转)
|
||||
ProjectName string `orm:"column(project_name);size(200)" json:"project_name"`
|
||||
TenderID *uint64 `orm:"column(tender_id);null" json:"tender_id"` // 关联招标项目
|
||||
TenderNo string `orm:"column(tender_no);size(50)" json:"tender_no"`
|
||||
CustomerID *uint64 `orm:"column(customer_id);null" json:"customer_id"` // 招标方
|
||||
CustomerName string `orm:"column(customer_name);size(200)" json:"customer_name"`
|
||||
PublishOrg string `orm:"column(publish_org);size(200)" json:"publish_org"`
|
||||
BidAmount float64 `orm:"column(bid_amount);digits(14);decimals(2);default(0)" json:"bid_amount"`
|
||||
BidDeposit float64 `orm:"column(bid_deposit);digits(14);decimals(2);default(0)" json:"bid_deposit"`
|
||||
PublishDate *time.Time `orm:"column(publish_date);type(date);null" json:"publish_date"`
|
||||
RegisterEndDate *time.Time `orm:"column(register_end_date);type(datetime);null" json:"register_end_date"`
|
||||
BidEndDate *time.Time `orm:"column(bid_end_date);type(datetime);null" json:"bid_end_date"`
|
||||
OpenBidDate *time.Time `orm:"column(open_bid_date);type(datetime);null" json:"open_bid_date"`
|
||||
ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"`
|
||||
ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"`
|
||||
BidAddress string `orm:"column(bid_address);size(255)" json:"bid_address"`
|
||||
OpenAddress string `orm:"column(open_address);size(255)" json:"open_address"`
|
||||
WinStatus int8 `orm:"column(win_status);default(0)" json:"win_status"` // 0未开标 1中标 2未中标 3废标 4流标
|
||||
WinDate *time.Time `orm:"column(win_date);type(datetime);null" json:"win_date"`
|
||||
OurRanking uint32 `orm:"column(our_ranking);default(0)" json:"our_ranking"`
|
||||
TotalScore float64 `orm:"column(total_score);digits(6);decimals(2);default(0)" json:"total_score"`
|
||||
CompetitorCount uint32 `orm:"column(competitor_count);default(0)" json:"competitor_count"`
|
||||
ContractID *uint64 `orm:"column(contract_id);null" json:"contract_id"` // 转合同ID
|
||||
NewProjectID *uint64 `orm:"column(new_project_id);null" json:"new_project_id"` // 中标后新建项目ID
|
||||
OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"`
|
||||
OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"`
|
||||
DepartmentID uint64 `orm:"column(department_id);default(0)" json:"department_id"`
|
||||
Description string `orm:"column(description);type(text);null" json:"description"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
Locked int8 `orm:"column(locked);default(0)" json:"locked"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBid) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingBidFile 投标文件: yz_backend_crm_bidding_bid_file
|
||||
type TenantCrmBiddingBidFile struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"`
|
||||
FileType string `orm:"column(file_type);size(20);default(1)" json:"file_type"`
|
||||
FileName string `orm:"column(file_name);size(255)" json:"file_name"`
|
||||
FileURL string `orm:"column(file_url);size(500)" json:"file_url"`
|
||||
FileSize uint64 `orm:"column(file_size);default(0)" json:"file_size"`
|
||||
FileExt string `orm:"column(file_ext);size(20)" json:"file_ext"`
|
||||
Version string `orm:"column(version);size(20);default(1.0)" json:"version"`
|
||||
UploadUserID string `orm:"column(upload_user_id);size(64)" json:"upload_user_id"`
|
||||
UploadUserName string `orm:"column(upload_user_name);size(128)" json:"upload_user_name"`
|
||||
Remark string `orm:"column(remark);size(500)" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidFile) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_file"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingBidApproval 投标立项审批: yz_backend_crm_bidding_bid_approval
|
||||
type TenantCrmBiddingBidApproval struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"`
|
||||
ApprovalType string `orm:"column(approval_type);size(20);default(1)" json:"approval_type"`
|
||||
ApprovalStatus int8 `orm:"column(approval_status);default(0)" json:"approval_status"`
|
||||
ApprovalFlowID *uint64 `orm:"column(approval_flow_id);null" json:"approval_flow_id"`
|
||||
CurrentUserID string `orm:"column(current_user_id);size(64)" json:"current_user_id"`
|
||||
CurrentUserName string `orm:"column(current_user_name);size(128)" json:"current_user_name"`
|
||||
ApprovalUsers string `orm:"column(approval_users);type(text);null" json:"approval_users"`
|
||||
SubmitUserID string `orm:"column(submit_user_id);size(64)" json:"submit_user_id"`
|
||||
SubmitUserName string `orm:"column(submit_user_name);size(128)" json:"submit_user_name"`
|
||||
SubmitTime *time.Time `orm:"column(submit_time);type(datetime);null" json:"submit_time"`
|
||||
CompleteTime *time.Time `orm:"column(complete_time);type(datetime);null" json:"complete_time"`
|
||||
Comment string `orm:"column(comment);type(text);null" json:"comment"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidApproval) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_approval"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingBidTask 投标任务: yz_backend_crm_bidding_bid_task
|
||||
type TenantCrmBiddingBidTask struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"`
|
||||
TaskName string `orm:"column(task_name);size(100)" json:"task_name"`
|
||||
TaskType string `orm:"column(task_type);size(20);default(1)" json:"task_type"`
|
||||
Content string `orm:"column(content);type(text);null" json:"content"`
|
||||
OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"`
|
||||
OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"`
|
||||
StartDate *time.Time `orm:"column(start_date);type(date);null" json:"start_date"`
|
||||
EndDate *time.Time `orm:"column(end_date);type(date);null" json:"end_date"`
|
||||
CompleteTime *time.Time `orm:"column(complete_time);type(datetime);null" json:"complete_time"`
|
||||
Status int8 `orm:"column(status);default(0)" json:"status"`
|
||||
Progress int8 `orm:"column(progress);default(0)" json:"progress"`
|
||||
Files string `orm:"column(files);type(text);null" json:"files"`
|
||||
Remark string `orm:"column(remark);size(500)" json:"remark"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidTask) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_task"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// =============================================================
|
||||
// 投标方 - 公共库:模板 + 标书版本 + 资质文件 + 报价(4 张)
|
||||
// =============================================================
|
||||
|
||||
// TenantCrmBiddingBidTemplate 标书模板: yz_backend_crm_bidding_bid_template
|
||||
type TenantCrmBiddingBidTemplate struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
TemplateName string `orm:"column(template_name);size(100)" json:"template_name"`
|
||||
TemplateType string `orm:"column(template_type);size(20);default(1)" json:"template_type"` // 1技术标 2商务标 3综合标
|
||||
Industry string `orm:"column(industry);size(50)" json:"industry"`
|
||||
Description string `orm:"column(description);size(500)" json:"description"`
|
||||
Content string `orm:"column(content);type(longtext);null" json:"content"`
|
||||
FileURL string `orm:"column(file_url);size(500)" json:"file_url"`
|
||||
UseCount uint32 `orm:"column(use_count);default(0)" json:"use_count"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 1启用 0禁用
|
||||
OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"`
|
||||
OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidTemplate) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_template"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingBidDocument 标书版本: yz_backend_crm_bidding_bid_document
|
||||
type TenantCrmBiddingBidDocument struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"`
|
||||
TemplateID *uint64 `orm:"column(template_id);null" json:"template_id"`
|
||||
Version string `orm:"column(version);size(20);default(v1.0)" json:"version"`
|
||||
Title string `orm:"column(title);size(200)" json:"title"`
|
||||
Content string `orm:"column(content);type(longtext);null" json:"content"`
|
||||
FileURL string `orm:"column(file_url);size(500)" json:"file_url"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 1编辑中 2待审核 3已定稿 4已提交
|
||||
EditorUserID string `orm:"column(editor_user_id);size(64)" json:"editor_user_id"`
|
||||
EditorUserName string `orm:"column(editor_user_name);size(128)" json:"editor_user_name"`
|
||||
ReviewUserID string `orm:"column(review_user_id);size(64)" json:"review_user_id"`
|
||||
ReviewUserName string `orm:"column(review_user_name);size(128)" json:"review_user_name"`
|
||||
ReviewComment string `orm:"column(review_comment);type(text);null" json:"review_comment"`
|
||||
ReviewTime *time.Time `orm:"column(review_time);type(datetime);null" json:"review_time"`
|
||||
SubmitTime *time.Time `orm:"column(submit_time);type(datetime);null" json:"submit_time"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidDocument) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_document"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingBidQualification 资质文件库: yz_backend_crm_bidding_bid_qualification
|
||||
type TenantCrmBiddingBidQualification struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
QualificationName string `orm:"column(qualification_name);size(100)" json:"qualification_name"`
|
||||
QualificationType string `orm:"column(qualification_type);size(20);default(1)" json:"qualification_type"`
|
||||
QualificationNo string `orm:"column(qualification_no);size(100)" json:"qualification_no"`
|
||||
IssuingAuthority string `orm:"column(issuing_authority);size(200)" json:"issuing_authority"`
|
||||
IssueDate *time.Time `orm:"column(issue_date);type(date);null" json:"issue_date"`
|
||||
ExpireDate *time.Time `orm:"column(expire_date);type(date);null" json:"expire_date"`
|
||||
AlertDays uint32 `orm:"column(alert_days);default(30)" json:"alert_days"`
|
||||
FileURL string `orm:"column(file_url);size(500)" json:"file_url"`
|
||||
FileName string `orm:"column(file_name);size(255)" json:"file_name"`
|
||||
FileSize uint64 `orm:"column(file_size);default(0)" json:"file_size"`
|
||||
Description string `orm:"column(description);type(text);null" json:"description"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"` // 1有效 2即将过期 0失效
|
||||
OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"`
|
||||
OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidQualification) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_qualification"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingBidQuote 报价表: yz_backend_crm_bidding_bid_quote
|
||||
type TenantCrmBiddingBidQuote struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"`
|
||||
QuoteName string `orm:"column(quote_name);size(100)" json:"quote_name"`
|
||||
TotalAmount float64 `orm:"column(total_amount);digits(14);decimals(2);default(0)" json:"total_amount"`
|
||||
Currency string `orm:"column(currency);size(10);default(CNY)" json:"currency"`
|
||||
TaxRate float64 `orm:"column(tax_rate);digits(6);decimals(4);default(0)" json:"tax_rate"`
|
||||
ValidDate *time.Time `orm:"column(valid_date);type(date);null" json:"valid_date"`
|
||||
Products string `orm:"column(products);type(longtext);null" json:"products"` // 产品清单JSON
|
||||
CostAnalysis string `orm:"column(cost_analysis);type(text);null" json:"cost_analysis"`
|
||||
ProfitRate float64 `orm:"column(profit_rate);digits(6);decimals(2);default(0)" json:"profit_rate"`
|
||||
Status int8 `orm:"column(status);default(1)" json:"status"`
|
||||
ApprovalFlowID *uint64 `orm:"column(approval_flow_id);null" json:"approval_flow_id"`
|
||||
SubmitUserID string `orm:"column(submit_user_id);size(64)" json:"submit_user_id"`
|
||||
SubmitUserName string `orm:"column(submit_user_name);size(128)" json:"submit_user_name"`
|
||||
SubmitTime *time.Time `orm:"column(submit_time);type(datetime);null" json:"submit_time"`
|
||||
ApprovalTime *time.Time `orm:"column(approval_time);type(datetime);null" json:"approval_time"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidQuote) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_quote"
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// =============================================================
|
||||
// 投标方 - 杂项:竞争对手 + 投标结果 + 投标费用(3 张)
|
||||
// =============================================================
|
||||
|
||||
// TenantCrmBiddingBidCompetitor 竞争对手报价: yz_backend_crm_bidding_bid_competitor
|
||||
type TenantCrmBiddingBidCompetitor struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"`
|
||||
CompetitorName string `orm:"column(competitor_name);size(200)" json:"competitor_name"`
|
||||
CompetitorType int8 `orm:"column(competitor_type);default(1)" json:"competitor_type"` // 1同类厂商 2关联企业 3其他
|
||||
QuoteAmount float64 `orm:"column(quote_amount);digits(14);decimals(2);default(0)" json:"quote_amount"`
|
||||
Advantages string `orm:"column(advantages);type(text);null" json:"advantages"`
|
||||
Weaknesses string `orm:"column(weaknesses);type(text);null" json:"weaknesses"`
|
||||
Source string `orm:"column(source);size(200)" json:"source"`
|
||||
Remark string `orm:"column(remark);size(500)" json:"remark"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidCompetitor) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_competitor"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingBidResult 投标结果: yz_backend_crm_bidding_bid_result
|
||||
type TenantCrmBiddingBidResult struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"`
|
||||
ResultType int8 `orm:"column(result_type);default(0)" json:"result_type"` // 1中标 2未中标 3废标 4流标
|
||||
AnnounceDate *time.Time `orm:"column(announce_date);type(datetime);null" json:"announce_date"`
|
||||
AnnounceURL string `orm:"column(announce_url);size(500)" json:"announce_url"`
|
||||
TotalScore float64 `orm:"column(total_score);digits(6);decimals(2);default(0)" json:"total_score"`
|
||||
OurRanking uint32 `orm:"column(our_ranking);default(0)" json:"our_ranking"`
|
||||
CompetitorCount uint32 `orm:"column(competitor_count);default(0)" json:"competitor_count"`
|
||||
Reason string `orm:"column(reason);type(text);null" json:"reason"`
|
||||
Lessons string `orm:"column(lessons);type(text);null" json:"lessons"`
|
||||
NextStep string `orm:"column(next_step);size(255)" json:"next_step"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidResult) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_result"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingBidExpense 投标费用: yz_backend_crm_bidding_bid_expense
|
||||
type TenantCrmBiddingBidExpense struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
BidID uint64 `orm:"column(bid_id);default(0)" json:"bid_id"`
|
||||
ExpenseType int8 `orm:"column(expense_type);default(1)" json:"expense_type"` // 1标书费 2保证金 3差旅费 4咨询费 5代理费 6其他
|
||||
ExpenseName string `orm:"column(expense_name);size(100)" json:"expense_name"`
|
||||
Amount float64 `orm:"column(amount);digits(12);decimals(2);default(0)" json:"amount"`
|
||||
Currency string `orm:"column(currency);size(10);default(CNY)" json:"currency"`
|
||||
PaymentStatus int8 `orm:"column(payment_status);default(0)" json:"payment_status"` // 0未付 1已付 2已退还 3已抵扣
|
||||
PaymentDate *time.Time `orm:"column(payment_date);type(datetime);null" json:"payment_date"`
|
||||
PaymentUserID string `orm:"column(payment_user_id);size(64)" json:"payment_user_id"`
|
||||
PaymentUserName string `orm:"column(payment_user_name);size(128)" json:"payment_user_name"`
|
||||
VoucherURL string `orm:"column(voucher_url);size(500)" json:"voucher_url"`
|
||||
Description string `orm:"column(description);type(text);null" json:"description"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingBidExpense) TableName() string {
|
||||
return "yz_backend_crm_bidding_bid_expense"
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// =============================================================
|
||||
// 招标方(我要招标):6 张表
|
||||
// =============================================================
|
||||
|
||||
// TenantCrmBiddingTender 招标项目主表: yz_backend_crm_bidding_tender
|
||||
//
|
||||
// 状态机:
|
||||
// 0草稿 -> 1公告中 -> 2报名中 -> 3投标中 -> 4开标中 -> 5评标中 -> 6已定标 -> 7已归档
|
||||
type TenantCrmBiddingTender struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
TenderNo string `orm:"column(tender_no);size(50)" json:"tender_no"` // 招标编号
|
||||
Title string `orm:"column(title);size(200)" json:"title"` // 招标标题
|
||||
TenderType string `orm:"column(tender_type);size(20);default(1)" json:"tender_type"` // 招标类型
|
||||
Status int8 `orm:"column(status);default(0)" json:"status"` // 状态
|
||||
CustomerID *uint64 `orm:"column(customer_id);null" json:"customer_id"` // 招标方客户ID
|
||||
CustomerName string `orm:"column(customer_name);size(200)" json:"customer_name"`
|
||||
PublishOrg string `orm:"column(publish_org);size(200)" json:"publish_org"`
|
||||
BudgetAmount float64 `orm:"column(budget_amount);digits(14);decimals(2);default(0)" json:"budget_amount"`
|
||||
BidDeposit float64 `orm:"column(bid_deposit);digits(14);decimals(2);default(0)" json:"bid_deposit"`
|
||||
PublishDate *time.Time `orm:"column(publish_date);type(date);null" json:"publish_date"`
|
||||
RegisterStartDate *time.Time `orm:"column(register_start_date);type(datetime);null" json:"register_start_date"`
|
||||
RegisterEndDate *time.Time `orm:"column(register_end_date);type(datetime);null" json:"register_end_date"`
|
||||
BidStartDate *time.Time `orm:"column(bid_start_date);type(datetime);null" json:"bid_start_date"`
|
||||
BidEndDate *time.Time `orm:"column(bid_end_date);type(datetime);null" json:"bid_end_date"`
|
||||
OpenBidDate *time.Time `orm:"column(open_bid_date);type(datetime);null" json:"open_bid_date"`
|
||||
ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"`
|
||||
ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"`
|
||||
ContactEmail string `orm:"column(contact_email);size(100)" json:"contact_email"`
|
||||
BidAddress string `orm:"column(bid_address);size(255)" json:"bid_address"`
|
||||
OpenAddress string `orm:"column(open_address);size(255)" json:"open_address"`
|
||||
Description string `orm:"column(description);type(text);null" json:"description"`
|
||||
Requirement string `orm:"column(requirement);type(text);null" json:"requirement"`
|
||||
EvaluationMethod string `orm:"column(evaluation_method);size(50);default(1)" json:"evaluation_method"`
|
||||
WinnerSupplierID *uint64 `orm:"column(winner_supplier_id);null" json:"winner_supplier_id"`
|
||||
WinnerSupplierName string `orm:"column(winner_supplier_name);size(200)" json:"winner_supplier_name"`
|
||||
WinnerAmount float64 `orm:"column(winner_amount);digits(14);decimals(2);default(0)" json:"winner_amount"`
|
||||
WinnerDate *time.Time `orm:"column(winner_date);type(datetime);null" json:"winner_date"`
|
||||
ContractID *uint64 `orm:"column(contract_id);null" json:"contract_id"` // 转合同ID
|
||||
ProjectID *uint64 `orm:"column(project_id);null" json:"project_id"` // 关联项目ID
|
||||
OwnerUserID string `orm:"column(owner_user_id);size(64)" json:"owner_user_id"`
|
||||
OwnerUserName string `orm:"column(owner_user_name);size(128)" json:"owner_user_name"`
|
||||
DepartmentID uint64 `orm:"column(department_id);default(0)" json:"department_id"`
|
||||
Locked int8 `orm:"column(locked);default(0)" json:"locked"`
|
||||
Remark string `orm:"column(remark);type(text);null" json:"remark"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingTender) TableName() string {
|
||||
return "yz_backend_crm_bidding_tender"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingTenderFile 招标文件: yz_backend_crm_bidding_tender_file
|
||||
type TenantCrmBiddingTenderFile struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"`
|
||||
FileType string `orm:"column(file_type);size(20);default(1)" json:"file_type"`
|
||||
FileName string `orm:"column(file_name);size(255)" json:"file_name"`
|
||||
FileURL string `orm:"column(file_url);size(500)" json:"file_url"`
|
||||
FileSize uint64 `orm:"column(file_size);default(0)" json:"file_size"`
|
||||
FileExt string `orm:"column(file_ext);size(20)" json:"file_ext"`
|
||||
UploadUserID string `orm:"column(upload_user_id);size(64)" json:"upload_user_id"`
|
||||
UploadUserName string `orm:"column(upload_user_name);size(128)" json:"upload_user_name"`
|
||||
Remark string `orm:"column(remark);size(500)" json:"remark"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingTenderFile) TableName() string {
|
||||
return "yz_backend_crm_bidding_tender_file"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingTenderAnnounce 招标公告: yz_backend_crm_bidding_tender_announce
|
||||
type TenantCrmBiddingTenderAnnounce struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"`
|
||||
AnnounceType string `orm:"column(announce_type);size(20);default(1)" json:"announce_type"`
|
||||
Title string `orm:"column(title);size(255)" json:"title"`
|
||||
Content string `orm:"column(content);type(longtext);null" json:"content"`
|
||||
PublishStatus int8 `orm:"column(publish_status);default(0)" json:"publish_status"`
|
||||
PublishTime *time.Time `orm:"column(publish_time);type(datetime);null" json:"publish_time"`
|
||||
ViewCount uint32 `orm:"column(view_count);default(0)" json:"view_count"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingTenderAnnounce) TableName() string {
|
||||
return "yz_backend_crm_bidding_tender_announce"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingTenderSupplier 投标方/供应商: yz_backend_crm_bidding_tender_supplier
|
||||
type TenantCrmBiddingTenderSupplier struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"`
|
||||
SupplierID *uint64 `orm:"column(supplier_id);null" json:"supplier_id"`
|
||||
SupplierName string `orm:"column(supplier_name);size(200)" json:"supplier_name"`
|
||||
RegisterStatus int8 `orm:"column(register_status);default(1)" json:"register_status"`
|
||||
RegisterTime *time.Time `orm:"column(register_time);type(datetime);null" json:"register_time"`
|
||||
BidAmount float64 `orm:"column(bid_amount);digits(14);decimals(2);default(0)" json:"bid_amount"`
|
||||
BidFiles string `orm:"column(bid_files);type(text);null" json:"bid_files"`
|
||||
ContactPerson string `orm:"column(contact_person);size(50)" json:"contact_person"`
|
||||
ContactPhone string `orm:"column(contact_phone);size(20)" json:"contact_phone"`
|
||||
IsWinner int8 `orm:"column(is_winner);default(0)" json:"is_winner"`
|
||||
Ranking uint32 `orm:"column(ranking);default(0)" json:"ranking"`
|
||||
Remark string `orm:"column(remark);size(500)" json:"remark"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingTenderSupplier) TableName() string {
|
||||
return "yz_backend_crm_bidding_tender_supplier"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingTenderEvaluation 开标评标: yz_backend_crm_bidding_tender_evaluation
|
||||
type TenantCrmBiddingTenderEvaluation struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"`
|
||||
SupplierID *uint64 `orm:"column(supplier_id);null" json:"supplier_id"`
|
||||
SupplierName string `orm:"column(supplier_name);size(200)" json:"supplier_name"`
|
||||
TechnicalScore float64 `orm:"column(technical_score);digits(6);decimals(2);default(0)" json:"technical_score"`
|
||||
BusinessScore float64 `orm:"column(business_score);digits(6);decimals(2);default(0)" json:"business_score"`
|
||||
PriceScore float64 `orm:"column(price_score);digits(6);decimals(2);default(0)" json:"price_score"`
|
||||
ServiceScore float64 `orm:"column(service_score);digits(6);decimals(2);default(0)" json:"service_score"`
|
||||
TotalScore float64 `orm:"column(total_score);digits(6);decimals(2);default(0)" json:"total_score"`
|
||||
WeightConfig string `orm:"column(weight_config);size(255)" json:"weight_config"`
|
||||
Ranking uint32 `orm:"column(ranking);default(0)" json:"ranking"`
|
||||
EvaluationUserID string `orm:"column(evaluation_user_id);size(64)" json:"evaluation_user_id"`
|
||||
EvaluationUserName string `orm:"column(evaluation_user_name);size(128)" json:"evaluation_user_name"`
|
||||
EvaluationTime *time.Time `orm:"column(evaluation_time);type(datetime);null" json:"evaluation_time"`
|
||||
Comment string `orm:"column(comment);type(text);null" json:"comment"`
|
||||
Result int8 `orm:"column(result);default(0)" json:"result"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingTenderEvaluation) TableName() string {
|
||||
return "yz_backend_crm_bidding_tender_evaluation"
|
||||
}
|
||||
|
||||
// TenantCrmBiddingTenderResult 定标结果: yz_backend_crm_bidding_tender_result
|
||||
type TenantCrmBiddingTenderResult struct {
|
||||
ID uint64 `orm:"column(id);pk;auto" json:"id"`
|
||||
TenantID string `orm:"column(tenant_id);size(64)" json:"tenant_id"`
|
||||
TenderID uint64 `orm:"column(tender_id);default(0)" json:"tender_id"`
|
||||
ResultType int8 `orm:"column(result_type);default(1)" json:"result_type"`
|
||||
WinnerSupplierID *uint64 `orm:"column(winner_supplier_id);null" json:"winner_supplier_id"`
|
||||
WinnerSupplierName string `orm:"column(winner_supplier_name);size(200)" json:"winner_supplier_name"`
|
||||
WinnerAmount float64 `orm:"column(winner_amount);digits(14);decimals(2);default(0)" json:"winner_amount"`
|
||||
ResultDate *time.Time `orm:"column(result_date);type(datetime);null" json:"result_date"`
|
||||
Announcement string `orm:"column(announcement);type(text);null" json:"announcement"`
|
||||
ContractID *uint64 `orm:"column(contract_id);null" json:"contract_id"`
|
||||
ProjectID *uint64 `orm:"column(project_id);null" json:"project_id"`
|
||||
CreateUserID string `orm:"column(create_user_id);size(64)" json:"create_user_id"`
|
||||
CreateUserName string `orm:"column(create_user_name);size(128)" json:"create_user_name"`
|
||||
CreateTime time.Time `orm:"column(create_time);auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"column(update_time);auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time"`
|
||||
}
|
||||
|
||||
func (m *TenantCrmBiddingTenderResult) TableName() string {
|
||||
return "yz_backend_crm_bidding_tender_result"
|
||||
}
|
||||
@@ -503,6 +503,102 @@ func registerOrganizationRoutes(module string) {
|
||||
// CRM操作日志
|
||||
beego.Router("/backend/crm/oplog/list", &controllers.BackendCrmOperateLogController{}, "get:List")
|
||||
|
||||
// ============================================================
|
||||
// CRM 招投标管理(我要招标 / 我要投标)
|
||||
// ============================================================
|
||||
// 投标方(Bid):从商机/项目发起 -> 投标 -> 中标 -> 转合同/项目
|
||||
beego.Router("/backend/crm/bidding/bid/list", &controllers.BackendCrmBiddingBidController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/bid/detail/:id", &controllers.BackendCrmBiddingBidController{}, "get:Detail")
|
||||
beego.Router("/backend/crm/bidding/bid", &controllers.BackendCrmBiddingBidController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/bid/:id", &controllers.BackendCrmBiddingBidController{}, "put:Update;delete:Delete")
|
||||
beego.Router("/backend/crm/bidding/bid/:id/contract", &controllers.BackendCrmBiddingBidController{}, "post:ConvertContract")
|
||||
beego.Router("/backend/crm/bidding/bid/:id/project", &controllers.BackendCrmBiddingBidController{}, "post:ConvertProject")
|
||||
beego.Router("/backend/crm/bidding/bid/:id/result", &controllers.BackendCrmBiddingBidController{}, "post:SubmitResult")
|
||||
beego.Router("/backend/crm/bidding/bid/from-business", &controllers.BackendCrmBiddingBidController{}, "post:FromBusiness")
|
||||
beego.Router("/backend/crm/bidding/bid/from-project", &controllers.BackendCrmBiddingBidController{}, "post:FromProject")
|
||||
// 投标文件
|
||||
beego.Router("/backend/crm/bidding/bid-file/list", &controllers.BackendCrmBiddingBidFileController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/bid-file", &controllers.BackendCrmBiddingBidFileController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/bid-file/:id", &controllers.BackendCrmBiddingBidFileController{}, "delete:Delete")
|
||||
// 投标任务
|
||||
beego.Router("/backend/crm/bidding/bid-task/list", &controllers.BackendCrmBiddingBidTaskController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/bid-task", &controllers.BackendCrmBiddingBidTaskController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/bid-task/:id", &controllers.BackendCrmBiddingBidTaskController{}, "put:Update;delete:Delete")
|
||||
// 投标结果
|
||||
beego.Router("/backend/crm/bidding/bid-result/list", &controllers.BackendCrmBiddingBidResultController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/bid-result/detail/:id", &controllers.BackendCrmBiddingBidResultController{}, "get:Detail")
|
||||
// 投标报价
|
||||
beego.Router("/backend/crm/bidding/bid-quote/list", &controllers.BackendCrmBiddingBidQuoteController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/bid-quote", &controllers.BackendCrmBiddingBidQuoteController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/bid-quote/:id", &controllers.BackendCrmBiddingBidQuoteController{}, "delete:Delete")
|
||||
// 投标竞争对手
|
||||
beego.Router("/backend/crm/bidding/bid-competitor/list", &controllers.BackendCrmBiddingBidCompetitorController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/bid-competitor", &controllers.BackendCrmBiddingBidCompetitorController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/bid-competitor/:id", &controllers.BackendCrmBiddingBidCompetitorController{}, "delete:Delete")
|
||||
// 投标费用
|
||||
beego.Router("/backend/crm/bidding/bid-expense/list", &controllers.BackendCrmBiddingBidExpenseController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/bid-expense", &controllers.BackendCrmBiddingBidExpenseController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/bid-expense/:id", &controllers.BackendCrmBiddingBidExpenseController{}, "delete:Delete")
|
||||
|
||||
// 招标方(Tender):发布招标 -> 投标方报名 -> 评标 -> 定标 -> 转合同
|
||||
beego.Router("/backend/crm/bidding/tender/list", &controllers.BackendCrmBiddingTenderController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/tender/detail/:id", &controllers.BackendCrmBiddingTenderController{}, "get:Detail")
|
||||
beego.Router("/backend/crm/bidding/tender", &controllers.BackendCrmBiddingTenderController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/tender/:id", &controllers.BackendCrmBiddingTenderController{}, "put:Update;delete:Delete")
|
||||
beego.Router("/backend/crm/bidding/tender/:id/award", &controllers.BackendCrmBiddingTenderController{}, "post:Award")
|
||||
beego.Router("/backend/crm/bidding/tender/:id/contract", &controllers.BackendCrmBiddingTenderController{}, "post:AwardToContract")
|
||||
// 招标文件
|
||||
beego.Router("/backend/crm/bidding/tender-file/list", &controllers.BackendCrmBiddingTenderFileController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/tender-file", &controllers.BackendCrmBiddingTenderFileController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/tender-file/:id", &controllers.BackendCrmBiddingTenderFileController{}, "delete:Delete")
|
||||
|
||||
// 招标公告
|
||||
beego.Router("/backend/crm/bidding/tender-announce/list", &controllers.BackendCrmBiddingTenderAnnounceController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/tender-announce", &controllers.BackendCrmBiddingTenderAnnounceController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/tender-announce/:id/publish", &controllers.BackendCrmBiddingTenderAnnounceController{}, "post:Publish")
|
||||
beego.Router("/backend/crm/bidding/tender-announce/:id", &controllers.BackendCrmBiddingTenderAnnounceController{}, "delete:Delete")
|
||||
|
||||
// 投标方/供应商
|
||||
beego.Router("/backend/crm/bidding/tender-supplier/list", &controllers.BackendCrmBiddingTenderSupplierController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/tender-supplier", &controllers.BackendCrmBiddingTenderSupplierController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/tender-supplier/:id", &controllers.BackendCrmBiddingTenderSupplierController{}, "put:Update;delete:Delete")
|
||||
|
||||
// 开标评标
|
||||
beego.Router("/backend/crm/bidding/tender-evaluation/list", &controllers.BackendCrmBiddingTenderEvaluationController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/tender-evaluation", &controllers.BackendCrmBiddingTenderEvaluationController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/tender-evaluation/:id", &controllers.BackendCrmBiddingTenderEvaluationController{}, "delete:Delete")
|
||||
|
||||
// 定标结果
|
||||
beego.Router("/backend/crm/bidding/tender-result/list", &controllers.BackendCrmBiddingTenderResultController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/tender-result/detail/:id", &controllers.BackendCrmBiddingTenderResultController{}, "get:Detail")
|
||||
|
||||
// 投标立项审批
|
||||
beego.Router("/backend/crm/bidding/bid-approval/list", &controllers.BackendCrmBiddingBidApprovalController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/bid-approval", &controllers.BackendCrmBiddingBidApprovalController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/bid-approval/:id/approve", &controllers.BackendCrmBiddingBidApprovalController{}, "post:Approve")
|
||||
|
||||
// 标书版本
|
||||
beego.Router("/backend/crm/bidding/bid-document/list", &controllers.BackendCrmBiddingBidDocumentController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/bid-document", &controllers.BackendCrmBiddingBidDocumentController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/bid-document/:id", &controllers.BackendCrmBiddingBidDocumentController{}, "put:Update;delete:Delete")
|
||||
|
||||
// 标书模板库
|
||||
beego.Router("/backend/crm/bidding/template/list", &controllers.BackendCrmBiddingBidTemplateController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/template", &controllers.BackendCrmBiddingBidTemplateController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/template/:id", &controllers.BackendCrmBiddingBidTemplateController{}, "put:Update;delete:Delete")
|
||||
|
||||
// 资质文件库
|
||||
beego.Router("/backend/crm/bidding/qualification/list", &controllers.BackendCrmBiddingBidQualificationController{}, "get:List")
|
||||
beego.Router("/backend/crm/bidding/qualification", &controllers.BackendCrmBiddingBidQualificationController{}, "post:Create")
|
||||
beego.Router("/backend/crm/bidding/qualification/:id", &controllers.BackendCrmBiddingBidQualificationController{}, "put:Update;delete:Delete")
|
||||
|
||||
// 招投标统计
|
||||
beego.Router("/backend/crm/bidding/statistics/tender", &controllers.BackendCrmBiddingStatisticsController{}, "get:Tender")
|
||||
beego.Router("/backend/crm/bidding/statistics/bid", &controllers.BackendCrmBiddingStatisticsController{}, "get:Bid")
|
||||
|
||||
// 招投标到期提醒(开发计划 4.1,实时计算,无定时任务)
|
||||
beego.Router("/backend/crm/bidding/reminders", &controllers.BackendCrmBiddingReminderController{}, "get:List")
|
||||
|
||||
// ERP供应商管理
|
||||
beego.Router("/backend/erp/supplier/list", &controllers.BackendErpSupplierController{}, "get:List")
|
||||
beego.Router("/backend/erp/supplier", &controllers.BackendErpSupplierController{}, "post:Create")
|
||||
|
||||
Reference in New Issue
Block a user