Files
erp-next/ruoyi-ui/src/views/bid/quotation/index.vue
wangyu 1bc99dcc7c feat: replace detail drawers with dialogs, add PDF export
- purchaseorder: el-drawer replaced with el-dialog + PDF export
- quotation: el-drawer replaced with el-dialog + PDF export
- rfq: add route-based detail page (/bid/rfq/detail) with PDF export
- PDF header: logo + 福安德综合报价系统 + doc type + item tables
- Add jspdf 2.5.2 + html2canvas for PDF generation
2026-05-22 10:17:38 +08:00

377 lines
16 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">
<el-form :model="queryParams" size="small" :inline="true" ref="queryForm">
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable style="width:120px">
<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>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增报价</el-button>
</el-col>
</el-row>
<el-table v-loading="loading" :data="list">
<el-table-column label="报价单号" prop="quoteNo" width="140" />
<el-table-column label="RFQ编号" prop="rfqNo" width="140" />
<el-table-column label="RFQ标题" prop="rfqTitle" :show-overflow-tooltip="true" />
<el-table-column label="供应商" prop="supplierName" width="150" />
<el-table-column label="总金额" prop="totalAmount" width="120" align="right">
<template slot-scope="scope">
<span style="color:#409EFF;font-weight:bold">¥{{ scope.row.totalAmount | numFormat }}</span>
</template>
</el-table-column>
<el-table-column label="交期(天)" prop="deliveryDays" width="90" align="center" />
<el-table-column label="状态" width="100">
<template slot-scope="scope">
<el-tag :type="statusType(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="提交时间" prop="submitTime" width="160" />
<el-table-column label="操作" align="center" width="220">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-if="scope.row.status==='draft'">编辑</el-button>
<el-button size="mini" type="text" @click="handleSubmit(scope.row)" v-if="scope.row.status==='draft'" style="color:#67C23A">提交</el-button>
<el-button size="mini" type="text" style="color:#67C23A" @click="handleAccept(scope.row)" v-if="scope.row.status==='submitted'">采纳</el-button>
<el-button size="mini" type="text" style="color:#F56C6C" @click="handleReject(scope.row)" v-if="scope.row.status==='submitted'">拒绝</el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click="handleView(scope.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" />
<!-- Edit / Create Dialog -->
<el-dialog :title="title" :visible.sync="open" width="960px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="关联RFQ" prop="rfqId">
<el-select v-model="form.rfqId" placeholder="选择RFQ" filterable style="width:100%">
<el-option v-for="r in rfqOptions" :key="r.rfqId" :label="r.rfqNo+' - '+r.rfqTitle" :value="r.rfqId" />
</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>
<el-col :span="8">
<el-form-item label="有效期(天)" prop="validDays">
<el-input-number v-model="form.validDays" :min="1" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="交货期(天)" prop="deliveryDays">
<el-input-number v-model="form.deliveryDays" :min="0" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="币种" prop="currency">
<el-select v-model="form.currency" style="width:100%">
<el-option label="人民币 CNY" value="CNY"/>
<el-option label="美元 USD" value="USD"/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">报价明细</el-divider>
<el-table :data="form.items || []" border size="small">
<el-table-column label="物料名称" min-width="150">
<template slot-scope="scope"><el-input v-model="scope.row.materialName" size="small" /></template>
</el-table-column>
<el-table-column label="规格" min-width="120">
<template slot-scope="scope"><el-input v-model="scope.row.spec" size="small" /></template>
</el-table-column>
<el-table-column label="单位" width="80">
<template slot-scope="scope"><el-input v-model="scope.row.unit" size="small" /></template>
</el-table-column>
<el-table-column label="数量" width="100">
<template slot-scope="scope">
<el-input-number v-model="scope.row.quantity" :min="0" :precision="2" size="small" style="width:90px" />
</template>
</el-table-column>
<el-table-column label="单价" width="130">
<template slot-scope="scope">
<el-input-number v-model="scope.row.unitPrice" :min="0" :precision="4" size="small" style="width:120px" />
</template>
</el-table-column>
<el-table-column label="金额" width="120" align="right">
<template slot-scope="scope">
<span style="color:#409EFF">{{ ((scope.row.quantity||0)*(scope.row.unitPrice||0)).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center">
<template slot-scope="scope">
<el-button type="text" icon="el-icon-delete" @click="form.items.splice(scope.$index,1)" style="color:#f56c6c" />
</template>
</el-table-column>
</el-table>
<el-button size="small" style="margin-top:8px" @click="form.items.push({materialName:'',spec:'',unit:'',quantity:1,unitPrice:0})" icon="el-icon-plus">添加行</el-button>
<el-form-item label="备注" style="margin-top:16px"><el-input v-model="form.note" type="textarea" rows="2" /></el-form-item>
</el-form>
<div slot="footer">
<el-button @click="open=false">取消</el-button>
<el-button type="primary" @click="submitForm">保存</el-button>
</div>
</el-dialog>
<!-- Detail Dialog -->
<el-dialog title="报价单详情" :visible.sync="detailOpen" width="820px" append-to-body>
<div v-if="detailData">
<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">{{ detailData.supplierName }}</td>
</tr>
<tr>
<td class="meta-label">关联RFQ</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 }}</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></tr>
</thead>
<tbody>
<tr v-for="(item, i) in detailData.items" :key="i">
<td>{{ item.materialName }}</td>
<td>{{ item.spec }}</td>
<td>{{ item.unit }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.unitPrice }}</td>
<td class="amount-cell">{{ item.totalPrice || ((item.quantity||0)*(item.unitPrice||0)).toFixed(2) }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5" style="text-align:right;font-weight:bold;padding:8px">合计金额</td>
<td class="amount-cell total-cell">¥{{ detailData.totalAmount }}</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, submitQuotation, acceptQuotation, rejectQuotation } from "@/api/bid/quotation";
import { listRfq } from "@/api/bid/rfq";
import { listSupplier } from "@/api/bid/supplier";
import logoImg from "@/assets/logo/logo.png";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
export default {
name: "Quotation",
filters: { numFormat: v => v ? Number(v).toFixed(2) : "0.00" },
data() {
return {
loading: false, total: 0, list: [],
open: false, title: "",
detailOpen: false, detailData: null, pdfLoading: false,
logoSrc: logoImg,
rfqOptions: [], supplierOptions: [],
queryParams: { pageNum: 1, pageSize: 10, status: null },
form: { items: [], currency: "CNY", validDays: 30 },
rules: {
rfqId: [{ required: true, message: "请选择RFQ", trigger: "change" }],
supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }]
}
};
},
created() { this.getList(); this.loadOptions(); },
methods: {
getList() {
this.loading = true;
listQuotation(this.queryParams).then(r => { this.list = r.rows; this.total = r.total; this.loading = false; });
},
loadOptions() {
listRfq({ pageSize: 200 }).then(r => { this.rfqOptions = r.rows || []; });
listSupplier({ pageSize: 200 }).then(r => { this.supplierOptions = r.rows || []; });
},
handleQuery() { this.queryParams.pageNum = 1; this.getList(); },
resetQuery() { this.resetForm("queryForm"); this.handleQuery(); },
handleAdd() { this.form = { items: [], currency: "CNY", validDays: 30, status: "draft" }; this.open = true; this.title = "新增报价"; },
handleUpdate(row) { getQuotation(row.quotationId).then(r => { this.form = r.data; this.open = true; this.title = "编辑报价"; }); },
handleView(row) { getQuotation(row.quotationId).then(r => { this.detailData = r.data; this.detailOpen = true; }); },
handleSubmit(row) {
this.$modal.confirm("确认提交报价?提交后不可修改").then(() => submitQuotation(row.quotationId))
.then(() => { this.$modal.msgSuccess("提交成功"); this.getList(); });
},
handleAccept(row) { acceptQuotation(row.quotationId).then(() => { this.$modal.msgSuccess("已采纳"); this.getList(); }); },
handleReject(row) { rejectQuotation(row.quotationId).then(() => { this.$modal.msgSuccess("已拒绝"); this.getList(); }); },
submitForm() {
this.$refs["form"].validate(valid => {
if (!valid) return;
const action = this.form.quotationId ? updateQuotation : addQuotation;
action(this.form).then(() => { this.$modal.msgSuccess("保存成功"); this.open = false; this.getList(); });
});
},
async exportPdf() {
this.pdfLoading = true;
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;
let remain = imgH;
while (remain > 0) {
pdf.addImage(imgData, "PNG", 0, y, pageW, imgH);
remain -= pageH;
if (remain > 0) { pdf.addPage(); y -= pageH; }
}
pdf.save("报价单_" + (this.detailData.quoteNo || "export") + ".pdf");
} finally {
this.pdfLoading = false;
}
},
statusType(s) { return { draft:"info", submitted:"warning", accepted:"success", rejected:"danger" }[s] || ""; },
statusLabel(s) { return { draft:"草稿", submitted:"已提交", accepted:"已采纳", rejected:"已拒绝" }[s] || s; }
}
};
</script>
<style scoped>
.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: #1171c4;
letter-spacing: 1px;
}
.pdf-doc-type {
font-size: 13px;
color: #666;
margin-top: 2px;
}
.pdf-header-no {
font-size: 13px;
color: #888;
}
.pdf-divider {
border-top: 2px solid #1171c4;
margin-bottom: 16px;
}
.pdf-meta-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.pdf-meta-table td {
padding: 7px 10px;
border: 1px solid #e4e7ed;
}
.meta-label {
background: #f5f7fa;
color: #606266;
font-weight: 600;
width: 90px;
}
.meta-val { color: #303133; }
.amount {
color: #409EFF;
font-weight: 700;
font-size: 15px;
}
.pdf-section-title {
font-size: 14px;
font-weight: 700;
color: #1a2c4e;
margin: 0 0 10px;
padding-left: 8px;
border-left: 4px solid #1171c4;
}
.pdf-items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.pdf-items-table th {
background: #1171c4;
color: #fff;
padding: 8px 10px;
text-align: center;
font-weight: 600;
}
.pdf-items-table td {
border: 1px solid #e4e7ed;
padding: 7px 10px;
text-align: center;
}
.pdf-items-table tbody tr:nth-child(even) td { background: #f9fbff; }
.amount-cell { color: #409EFF; font-weight: 600; }
.total-cell { font-size: 15px; background: #f0f7ff !important; }
.pdf-footer {
text-align: right;
font-size: 11px;
color: #aaa;
margin-top: 10px;
}
</style>