Files
erp-next/ruoyi-ui/src/views/bid/quotation/index.vue
王文昊 41b2e3e772 feat(bid): 完成批量业务优化与功能完善
1.  统一所有表格操作列样式,移除固定宽度避免布局溢出
2.  新增报价单自动编号与脏数据清理功能
3.  优化订单状态筛选与展示逻辑,新增closed状态支持
4.  完善操作日志管理,新增统计分析与详情查看功能
5.  优化报价单流程,调整提交审批逻辑与权限控制
6.  修复客户端订单查询SQL,优化关联查询逻辑
7.  新增报价单提交时自动更新提交时间的功能
2026-06-18 20:17:02 +08:00

742 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container quotation-page">
<!-- 顶部统计卡片 -->
<el-row :gutter="12" class="stat-row">
<el-col :span="6">
<div class="stat-card">
<div class="stat-body">
<div class="stat-num">{{ stats.total || 0 }}</div>
<div class="stat-lbl">全部报价</div>
</div>
<i class="el-icon-document stat-icon"></i>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-body">
<div class="stat-num">{{ stats.draft || 0 }}</div>
<div class="stat-lbl">草稿</div>
</div>
<i class="el-icon-edit-outline stat-icon"></i>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-body">
<div class="stat-num">{{ stats.submitted || 0 }}</div>
<div class="stat-lbl">待处理</div>
</div>
<i class="el-icon-time stat-icon"></i>
</div>
</el-col>
<el-col :span="6">
<div class="stat-card">
<div class="stat-body">
<div class="stat-num">{{ stats.accepted || 0 }}</div>
<div class="stat-lbl">已采纳</div>
</div>
<i class="el-icon-circle-check stat-icon"></i>
</div>
</el-col>
</el-row>
<!-- 搜索栏 -->
<div class="jd-filter-bar">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true">
<el-form-item label="报价单号">
<el-input v-model="queryParams.quoteNo" placeholder="报价单号" clearable style="width:150px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="RFQ单号">
<el-input v-model="queryParams.rfqNo" placeholder="询价单号" clearable style="width:150px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="供应商">
<el-input v-model="queryParams.supplierName" placeholder="供应商名称" clearable style="width:150px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width:110px">
<el-option label="草稿" value="draft" />
<el-option label="已提交" value="submitted" />
<el-option label="已采纳" value="accepted" />
<el-option label="已拒绝" value="rejected" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 工具栏 -->
<el-row :gutter="10" class="mb8" style="margin-top:12px">
<el-col :span="1.5">
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">新建报价</el-button>
</el-col>
<el-col :span="1.5" v-if="!isSupplier">
<el-button type="danger" plain icon="el-icon-delete" size="mini" @click="handleCleanNullQuote">清理无编号数据</el-button>
</el-col>
</el-row>
<!-- 报价列表 -->
<el-table v-loading="loading" :data="list" border stripe>
<el-table-column label="报价单号" width="155">
<template slot-scope="s">
<span :style="{ color: s.row.quoteNo ? '#333' : '#c0c4cc' }">
{{ s.row.quoteNo || '(待编号)' }}
</span>
</template>
</el-table-column>
<el-table-column label="关联询价单" width="200">
<template slot-scope="s">
<div style="font-weight:600;color:#333">{{ s.row.rfqNo }}</div>
<div style="font-size:12px;color:#999;margin-top:2px" v-if="s.row.rfqTitle">{{ s.row.rfqTitle }}</div>
</template>
</el-table-column>
<el-table-column label="供应商" prop="supplierName" min-width="150">
<template slot-scope="s">
<div style="display:flex;align-items:center;gap:6px">
<!-- <el-avatar :size="28" style="background:var(--brand-primary);flex-shrink:0">{{ (s.row.supplierName||'?').charAt(0) }}</el-avatar> -->
<span>{{ s.row.supplierName }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="金额" width="120" align="right">
<template slot-scope="s">
<strong style="color:#e4393c;font-size:15px">¥{{ s.row.totalAmount | money }}</strong>
</template>
</el-table-column>
<el-table-column label="交期" prop="deliveryDays" width="80" align="center">
<template slot-scope="s">{{ s.row.deliveryDays || '-' }} </template>
</el-table-column>
<el-table-column label="状态" width="105" align="center">
<template slot-scope="s">
<div class="status-chip" :class="'status-' + s.row.status">
<i :class="statusIcon(s.row.status)"></i>
{{ statusLabel(s.row.status) }}
</div>
</template>
</el-table-column>
<el-table-column label="提交时间" prop="submitTime" width="155" align="center">
<template slot-scope="s">
<span v-if="s.row.submitTime" style="font-size:12px">{{ s.row.submitTime }}</span>
<span v-else style="color:#c0c4cc;font-size:12px">未提交</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="260" fixed="right" class-name="col-ops">
<template slot-scope="s">
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(s.row)">查看</el-button>
<!-- 草稿提交审批是唯一送审入口 -->
<template v-if="s.row.status==='draft'">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(s.row)">编辑</el-button>
<el-button size="mini" type="text" style="color:#E6A23C" icon="el-icon-s-check"
@click="handleSubmitApproval(s.row)">提交审批</el-button>
<el-dropdown trigger="click" @command="c => handleDropdown(c, s.row)">
<el-button size="mini" type="text">更多<i class="el-icon-arrow-down el-icon--right" /></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="delete" icon="el-icon-delete">删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<!-- 已提交历史遗留允许补交审批 -->
<template v-if="s.row.status==='submitted'">
<el-dropdown trigger="click" @command="c => handleDropdown(c, s.row)">
<el-button size="mini" type="text">更多<i class="el-icon-arrow-down el-icon--right" /></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="submitApproval" icon="el-icon-s-check">提交审批</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<!-- 审批中审批人操作 -->
<template v-if="s.row.status==='10'">
<el-button size="mini" type="text" style="color:#67C23A" @click="handleApprove(s.row)">审批通过</el-button>
<el-button size="mini" type="text" style="color:#F56C6C" @click="handleApprovalReject(s.row)">审批驳回</el-button>
</template>
<!-- 已采纳生成发货单 -->
<el-button v-if="s.row.status==='accepted'" size="mini" type="text" style="color:#4A6FA5"
icon="el-icon-s-order" @click="handleCreateDelivery(s.row)">生成发货单</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize" @pagination="getList" />
<!-- 创建/编辑对话框 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogOpen" width="1000px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="90px" size="small">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="关联RFQ" prop="rfqId">
<el-select v-model="form.rfqId" placeholder="选择询价单" filterable style="width:100%" @change="onRfqChange">
<el-option v-for="r in rfqOptions" :key="r.rfqId"
:label="r.rfqNo + ' · ' + r.rfqTitle" :value="r.rfqId">
<div style="display:flex;justify-content:space-between">
<span style="font-weight:600">{{ r.rfqNo }}</span>
<span style="color:#909399;font-size:12px;margin-left:8px">{{ r.rfqTitle }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="供应商" prop="supplierId">
<el-select v-model="form.supplierId" placeholder="选择供应商" filterable style="width:100%">
<el-option v-for="s in supplierOptions" :key="s.supplierId"
:label="s.supplierName" :value="s.supplierId" />
</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.validDays" :min="1" :max="365" style="width:100%" />
<span style="margin-left:4px;color:#909399;font-size:12px"></span>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="整体交期">
<el-input-number v-model="form.deliveryDays" :min="0" style="width:100%" />
<span style="margin-left:4px;color:#909399;font-size:12px"></span>
</el-form-item>
</el-col>
<el-col :span="8">
<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-divider content-position="left">
<span style="font-weight:700;color:#333333">报价明细</span>
<el-tooltip content="选择RFQ后可自动加载物料需求" placement="top">
<i class="el-icon-question" style="margin-left:6px;color:#909399;cursor:pointer"></i>
</el-tooltip>
</el-divider>
<div v-if="rfqItemsLoading" style="text-align:center;padding:20px;color:#909399">
<i class="el-icon-loading"></i> 正在加载RFQ物料
</div>
<el-table v-else :data="form.items" border size="small" class="items-table">
<el-table-column type="index" width="44" label="#" />
<el-table-column label="物料名称" min-width="140">
<template slot-scope="s">
<el-input v-model="s.row.materialName" size="mini" placeholder="物料名称" />
</template>
</el-table-column>
<el-table-column label="规格" width="120">
<template slot-scope="s">
<el-input v-model="s.row.spec" size="mini" placeholder="规格" />
</template>
</el-table-column>
<el-table-column label="型号" width="120">
<template slot-scope="s">
<el-input v-model="s.row.modelNo" size="mini" placeholder="型号" />
</template>
</el-table-column>
<el-table-column label="单位" width="72">
<template slot-scope="s">
<el-input v-model="s.row.unit" size="mini" />
</template>
</el-table-column>
<el-table-column label="需求数量" width="90" align="right">
<template slot-scope="s">
<span style="color:#909399;font-size:13px">{{ s.row.quantity }}</span>
</template>
</el-table-column>
<el-table-column label="报价(元)" width="120">
<template slot-scope="s">
<el-input-number v-model="s.row.unitPrice" :min="0" :precision="4"
size="mini" controls-position="right" style="width:100%" @change="calcItem(s.row)" />
</template>
</el-table-column>
<el-table-column label="金额(元)" width="110" align="right">
<template slot-scope="s">
<strong style="color:#e4393c">¥{{ itemTotal(s.row) }}</strong>
</template>
</el-table-column>
<el-table-column label="交期(天)" width="85">
<template slot-scope="s">
<el-input-number v-model="s.row.deliveryDays" :min="0"
size="mini" controls-position="right" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="100">
<template slot-scope="s">
<el-input v-model="s.row.remark" size="mini" placeholder="备注" />
</template>
</el-table-column>
<el-table-column width="46" align="center">
<template slot="header">
<el-button type="text" icon="el-icon-plus" @click="addItem" />
</template>
<template slot-scope="s">
<el-button type="text" icon="el-icon-delete" style="color:#f56c6c"
@click="form.items.splice(s.$index, 1)" />
</template>
</el-table-column>
</el-table>
<div class="form-total-bar">
合计报价<strong>¥{{ formTotal }}</strong>
<span style="margin-left:16px;color:#909399;font-size:12px">{{ form.items.length }} 项物料</span>
</div>
<el-form-item label="备注" style="margin-top:12px">
<el-input v-model="form.note" type="textarea" :rows="2" placeholder="整体备注说明" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogOpen = false">取消</el-button>
<el-button type="success" @click="submitForm('draft')" :loading="saving">保存草稿</el-button>
<el-button type="primary" @click="submitForm('approval')" :loading="submitting">保存并提交审批</el-button>
</div>
</el-dialog>
<!-- 详情对话框 -->
<el-dialog title="报价单详情" :visible.sync="detailOpen" width="860px" append-to-body>
<div v-if="detailData">
<!-- 状态流程条 -->
<div class="detail-steps">
<div class="step-item" :class="{ active: ['draft','submitted','accepted','rejected'].includes(detailData.status) }">
<i class="el-icon-edit-outline"></i><span>草稿</span>
</div>
<div class="step-line" :class="{ active: ['submitted','accepted','rejected'].includes(detailData.status) }"></div>
<div class="step-item" :class="{ active: ['submitted','accepted','rejected'].includes(detailData.status) }">
<i class="el-icon-upload2"></i><span>已提交</span>
</div>
<div class="step-line" :class="{ active: ['accepted','rejected'].includes(detailData.status) }"></div>
<div class="step-item" :class="{ active: detailData.status === 'accepted', rejected: detailData.status === 'rejected' }">
<i :class="detailData.status === 'rejected' ? 'el-icon-circle-close' : 'el-icon-circle-check'"></i>
<span>{{ detailData.status === 'rejected' ? '已拒绝' : '已采纳' }}</span>
</div>
</div>
<!-- PDF 内容区 -->
<div id="quote-pdf-area" class="pdf-area">
<div class="pdf-header">
<img :src="logoSrc" class="pdf-logo" />
<div class="pdf-header-text">
<div class="pdf-company">福安德综合报价系统</div>
<div class="pdf-doc-type">供应商报价单</div>
</div>
<div class="pdf-header-no">{{ detailData.quoteNo || '(待编号)' }}</div>
</div>
<div class="pdf-divider"></div>
<table class="pdf-meta-table">
<tr>
<td class="meta-label">报价单号</td><td class="meta-val">{{ detailData.quoteNo || '(待编号)' }}</td>
<td class="meta-label">供应商</td><td class="meta-val"><strong>{{ detailData.supplierName }}</strong></td>
</tr>
<tr>
<td class="meta-label">关联询价</td><td class="meta-val">{{ detailData.rfqNo }}</td>
<td class="meta-label">状态</td>
<td class="meta-val">
<el-tag :type="statusType(detailData.status)" size="small">{{ statusLabel(detailData.status) }}</el-tag>
</td>
</tr>
<tr>
<td class="meta-label">报价总额</td>
<td class="meta-val amount">¥{{ detailData.totalAmount | money }}</td>
<td class="meta-label">整体交期</td>
<td class="meta-val">{{ detailData.deliveryDays || '-' }} </td>
</tr>
<tr>
<td class="meta-label">有效期</td><td class="meta-val">{{ detailData.validDays }} </td>
<td class="meta-label">币种</td><td class="meta-val">{{ detailData.currency }}</td>
</tr>
<tr v-if="detailData.note">
<td class="meta-label">备注</td><td class="meta-val" colspan="3">{{ detailData.note }}</td>
</tr>
</table>
<div class="pdf-section-title">报价明细</div>
<table class="pdf-items-table">
<thead>
<tr>
<th>#</th><th>物料名称</th><th>规格</th><th>型号</th>
<th>单位</th><th>数量</th><th>单价()</th><th>金额()</th><th>交期()</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, i) in detailData.items" :key="i">
<td>{{ i + 1 }}</td>
<td style="text-align:left;font-weight:500">{{ item.materialName }}</td>
<td>{{ item.spec || '-' }}</td>
<td>{{ item.modelNo || item.spec || '-' }}</td>
<td>{{ item.unit }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.unitPrice }}</td>
<td class="amount-cell">{{ item.totalPrice || itemTotal(item) }}</td>
<td>{{ item.deliveryDays || '-' }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="7" style="text-align:right;font-weight:700;padding:10px">合计金额</td>
<td class="amount-cell total-cell" colspan="2">¥{{ detailData.totalAmount | money }}</td>
</tr>
</tfoot>
</table>
<div class="pdf-footer">生成时间{{ new Date().toLocaleString('zh-CN') }}</div>
</div>
</div>
<div slot="footer">
<el-button @click="detailOpen = false">关闭</el-button>
<el-button type="primary" icon="el-icon-download" :loading="pdfLoading" @click="exportPdf">导出 PDF</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listQuotation, getQuotation, addQuotation, updateQuotation,
delQuotation, cleanNullQuoteNo } from "@/api/bid/quotation";
import { listRfq, getRfqItems } from "@/api/bid/rfq";
import { listSupplier } from "@/api/bid/supplier";
import { addDelivery } from "@/api/bid/delivery";
import { submitApproval, approveBiz, rejectBiz } from "@/api/bid/approvalAction";
import logoImg from "@/assets/logo/logo.svg";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
export default {
name: "Quotation",
filters: { money: v => v ? Number(v).toFixed(2) : "0.00" },
data() {
return {
loading: false, saving: false, submitting: false, pdfLoading: false,
total: 0, list: [],
stats: { total: 0, draft: 0, submitted: 0, accepted: 0 },
dialogOpen: false, dialogTitle: "",
detailOpen: false, detailData: null,
rfqItemsLoading: false,
rfqOptions: [], supplierOptions: [],
logoSrc: logoImg,
queryParams: { pageNum: 1, pageSize: 10, status: null, rfqNo: null, quoteNo: null, supplierName: null },
form: { items: [], currency: "CNY", validDays: 30, deliveryDays: null },
rules: {
rfqId: [{ required: true, message: "请选择询价单", trigger: "change" }],
supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }],
}
};
},
computed: {
/** 是否为供应商用户 */
isSupplier() {
return this.$store.getters.roles && this.$store.getters.roles.includes('supplier');
},
formTotal() {
return (this.form.items || []).reduce((s, i) => s + (parseFloat(i.quantity || 0) * parseFloat(i.unitPrice || 0)), 0).toFixed(2);
}
},
created() {
// 读取 URL 查询参数,实现跨页面跳转自动搜索
const query = this.$route.query;
if (query.rfqNo) this.queryParams.rfqNo = query.rfqNo;
if (query.supplierName) this.queryParams.supplierName = query.supplierName;
if (query.quoteNo) this.queryParams.quoteNo = query.quoteNo;
this.getList();
// 供应商只需加载自己可见的RFQ无需加载全部供应商列表
listRfq({ pageSize: 200 }).then(r => { this.rfqOptions = r.rows || []; });
if (!this.isSupplier) {
listSupplier({ pageSize: 200 }).then(r => { this.supplierOptions = r.rows || []; });
}
},
/** keep-alive 缓存激活时自动刷新(解决跨页面审批后状态不更新问题) */
activated() {
this.getList();
},
methods: {
getList() {
this.loading = true;
listQuotation(this.queryParams).then(r => {
this.list = r.rows || [];
this.total = r.total || 0;
// Compute stats
this.stats.total = this.total;
this.stats.draft = (r.rows || []).filter(x => x.status === "draft").length;
this.stats.submitted = (r.rows || []).filter(x => x.status === "submitted").length;
this.stats.accepted = (r.rows || []).filter(x => x.status === "accepted").length;
this.loading = false;
}).catch(() => { this.loading = false; });
},
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
resetQuery() {
this.resetForm("queryForm");
this.queryParams.quoteNo = null;
this.handleQuery();
},
handleAdd() {
this.form = { items: [], currency: "CNY", validDays: 30, deliveryDays: null, status: "draft" };
this.dialogTitle = "新建报价单";
this.dialogOpen = true;
},
/** 清理历史遗留的无编号报价单脏数据 */
handleCleanNullQuote() {
this.$modal.confirm("确认清理所有无报价单号的脏数据?此操作不可恢复!", "危险操作", { type: "warning" })
.then(() => cleanNullQuoteNo())
.then(r => { this.$modal.msgSuccess(r.msg || "清理成功"); this.getList(); })
.catch(() => {});
},
handleUpdate(row) {
getQuotation(row.quotationId).then(r => {
this.form = { ...r.data, items: r.data.items || [] };
this.dialogTitle = "编辑报价单";
this.dialogOpen = true;
});
},
handleView(row) {
getQuotation(row.quotationId).then(r => {
this.detailData = r.data;
this.detailOpen = true;
});
},
async onRfqChange(rfqId) {
if (!rfqId) return;
this.rfqItemsLoading = true;
try {
const res = await getRfqItems(rfqId);
const items = res.data || [];
this.form.items = items.map(i => ({
materialName: i.materialName || "",
spec: i.spec || "",
modelNo: i.modelNo || "",
unit: i.unit || "件",
quantity: i.quantity || 1,
unitPrice: 0,
deliveryDays: null,
remark: ""
}));
if (this.form.items.length > 0) {
this.$message.success(`已加载 ${this.form.items.length} 项物料需求`);
}
} catch(e) {
// If items API fails, just leave empty
}
this.rfqItemsLoading = false;
},
addItem() {
this.form.items.push({ materialName: "", spec: "", modelNo: "", unit: "件", quantity: 1, unitPrice: 0, deliveryDays: null, remark: "" });
},
calcItem(row) {
// trigger reactivity
this.$set(row, "unitPrice", row.unitPrice);
},
itemTotal(row) {
return ((parseFloat(row.quantity) || 0) * (parseFloat(row.unitPrice) || 0)).toFixed(2);
},
handleSubmitApproval(row) {
this.$modal.confirm("确认提交审批?").then(() => submitApproval("QUOTATION", row.quotationId))
.then(() => { this.$modal.msgSuccess("已提交审批"); this.getList(); });
},
handleApprove(row) {
this.$modal.confirm("确认通过该报价审批?").then(() => approveBiz("QUOTATION", row.quotationId))
.then(() => { this.$modal.msgSuccess("审批通过"); this.getList(); });
},
handleApprovalReject(row) {
this.$prompt("请输入驳回原因", "驳回", { inputPattern: /.+/, inputErrorMessage: "请填写原因" })
.then(({ value }) => rejectBiz("QUOTATION", row.quotationId, value))
.then(() => { this.$modal.msgSuccess("已驳回"); this.getList(); })
.catch(() => {});
},
handleDelete(row) {
this.$modal.confirm("确认删除?").then(() => delQuotation(row.quotationId))
.then(() => { this.$modal.msgSuccess("删除成功"); this.getList(); });
},
/** 「更多」下拉菜单统一路由 */
handleDropdown(command, row) {
const map = {
submitApproval:() => this.handleSubmitApproval(row),
delete: () => this.handleDelete(row),
};
if (map[command]) map[command]();
},
handleCreateDelivery(row) {
this.$modal.confirm("确认基于此报价单生成发货单?").then(() => {
return getQuotation(row.quotationId);
}).then(res => {
const q = res.data;
if (!q || !q.items || q.items.length === 0) {
throw new Error("该报价单无明细,无法生成发货单");
}
// 计算交货期 = 今天 + 报价单中的交期天数
let deliveryDate = ""
if (q.deliveryDays) {
const d = new Date()
d.setDate(d.getDate() + q.deliveryDays)
deliveryDate = d.toISOString().slice(0, 10)
}
const doPayload = {
rfqId: q.rfqId,
quotationId: q.quotationId,
supplierId: q.supplierId,
totalAmount: q.totalAmount,
deliveryDate: deliveryDate,
deliveryStatus: "pending",
items: q.items.map(it => ({
materialId: it.materialId || 0,
materialName: it.materialName,
spec: it.spec || "",
unit: it.unit || "",
quantity: it.quantity || 0,
unitPrice: it.unitPrice || 0,
totalPrice: it.totalPrice || ((it.quantity || 0) * (it.unitPrice || 0)),
remark: it.remark || ""
}))
};
return addDelivery(doPayload);
}).then(() => {
this.$modal.msgSuccess("发货单已生成");
this.getList();
}).catch(e => {
if (e.message) this.$modal.msgError(e.message);
});
},
submitForm(mode) {
this.$refs.form.validate(valid => {
if (!valid) return;
if (mode === "approval") this.submitting = true;
else this.saving = true;
const action = this.form.quotationId ? updateQuotation : addQuotation;
action(this.form).then(res => {
const id = (res.data && res.data.quotationId) || this.form.quotationId;
if (mode === "approval" && id) {
return submitApproval("QUOTATION", id).then(() => {
this.$modal.msgSuccess("已提交审批");
this.dialogOpen = false;
this.getList();
});
}
this.$modal.msgSuccess("保存成功");
this.dialogOpen = false;
this.getList();
}).finally(() => { this.saving = false; this.submitting = false; });
});
},
async exportPdf() {
this.pdfLoading = true;
await this.$nextTick();
try {
const el = document.getElementById("quote-pdf-area");
const canvas = await html2canvas(el, { scale: 2, useCORS: true, backgroundColor: "#ffffff" });
const imgData = canvas.toDataURL("image/png");
const pdf = new jsPDF({ orientation: "p", unit: "mm", format: "a4" });
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
const imgH = (canvas.height * pageW) / canvas.width;
let y = 0, rem = imgH, first = true;
while (rem > 0) {
if (!first) pdf.addPage();
pdf.addImage(imgData, "PNG", 0, y, pageW, imgH);
y -= pageH; rem -= pageH; first = false;
}
pdf.save("报价单_" + this.detailData.quoteNo + ".pdf");
this.$message.success("PDF已导出");
} catch(e) { this.$message.error("导出失败:" + e.message); }
finally { this.pdfLoading = false; }
},
statusType(s) { return { draft: "info", submitted: "warning", "10": "warning", accepted: "success", rejected: "danger" }[s] || ""; },
statusLabel(s) { return { draft: "草稿", submitted: "已提交", "10": "审批中", accepted: "已采纳", rejected: "已拒绝" }[s] || s; },
statusIcon(s) { return { draft: "el-icon-edit-outline", submitted: "el-icon-time", accepted: "el-icon-circle-check", rejected: "el-icon-circle-close" }[s] || "el-icon-document"; }
}
};
</script>
<style lang="scss" scoped>
.quotation-page { padding-bottom: 30px; }
/* ── 顶部统计卡片 ── */
.stat-row { margin-bottom: 12px !important; }
/* ── 搜索 ── */
.jd-filter-bar {
background: #ffffff;
padding: 12px 16px 4px;
border-radius: 2px;
::v-deep .el-form-item {
margin-bottom: 8px !important;
}
::v-deep .el-form-item__label {
font-size: 13px;
color: #666;
}
}
.search-card { ::v-deep .el-card__body { padding: 16px 20px 8px; } }
/* ── 状态芯片 ── */
.status-chip {
display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px;
border-radius: 12px; font-size: 12px; font-weight: 600;
i { font-size: 12px; }
}
.status-draft { background: #f4f4f5; color: #909399; }
.status-submitted { background: #fdf6ec; color: #e6a23c; border: 1px solid #faecd8; }
.status-accepted { background: #f0f9eb; color: #67c23a; border: 1px solid #c2e7b0; }
.status-rejected { background: #fef0f0; color: #f56c6c; border: 1px solid #fbc4c4; }
/* ── 表单合计 ── */
.items-table { margin-bottom: 0; }
.form-total-bar {
text-align: right; padding: 10px 16px;
background: #fafafa;
border: 1px solid #e5e5e5; border-top: none; border-radius: 0 0 2px 2px;
font-size: 14px; color: #666;
strong { font-size: 20px; color: #e4393c; margin-left: 6px; }
}
/* ── 详情 - 状态流程 ── */
.detail-steps {
display: flex; align-items: center; justify-content: center;
padding: 16px 0 20px; gap: 0;
}
.step-item {
display: flex; flex-direction: column; align-items: center; gap: 4px;
color: #c0c4cc; font-size: 12px;
i { font-size: 22px; }
&.active { color: var(--brand-primary); }
&.rejected { color: var(--color-danger); }
}
.step-line {
flex: 1; max-width: 80px; height: 2px; background: var(--silver-border); margin: 0 8px; margin-top: -12px;
&.active { background: var(--brand-primary); }
}
/* ── PDF ── */
.pdf-area { padding: 24px; background: #fff; font-family: "Microsoft YaHei", "Noto Sans SC", Arial, sans-serif; font-size: 13px; color: #222; }
.pdf-header { display: flex; align-items: center; padding-bottom: 14px; }
.pdf-logo { width: 48px; height: 48px; object-fit: contain; margin-right: 16px; }
.pdf-header-text { flex: 1; }
.pdf-company { font-size: 20px; font-weight: 700; color: var(--brand-primary); letter-spacing: 1px; }
.pdf-doc-type { font-size: 13px; color: var(--text-secondary); margin-top: 2px; }
.pdf-header-no { font-size: 13px; color: var(--text-muted); }
.pdf-divider { border-top: 2px solid var(--brand-primary); margin-bottom: 16px; }
.pdf-meta-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; td { padding: 7px 10px; border: 1px solid #e4e7ed; } }
.meta-label { background: var(--silver-bg); color: var(--text-secondary); font-weight: 600; width: 90px; }
.meta-val { color: var(--text-primary); }
.amount { color: var(--color-amount); font-weight: 700; font-size: 15px; }
.pdf-section-title { font-size: 14px; font-weight: 700; color: var(--text-primary); margin: 0 0 10px; padding-left: 8px; border-left: 4px solid var(--brand-primary); }
.pdf-items-table { width: 100%; border-collapse: collapse; margin-bottom: 20px;
th { background: var(--brand-primary); color: #fff; padding: 8px; text-align: center; font-weight: 600; font-size: 12px; }
td { border: 1px solid var(--silver-border); padding: 7px 8px; text-align: center; font-size: 12px; }
tbody tr:nth-child(even) td { background: #f9fbff; }
}
.amount-cell { color: #e4393c; font-weight: 600; }
.total-cell { font-size: 15px; background: #fafafa !important; font-weight: 700; }
.pdf-footer { text-align: right; font-size: 11px; color: #aaa; margin-top: 10px; border-top: 1px solid #f0f2f5; padding-top: 8px; }
/* 操作列:禁止溢出省略,确保所有按钮完整显示 */
::v-deep .col-ops .cell {
overflow: visible !important;
text-overflow: clip !important;
white-space: nowrap;
}
</style>