Files
erp-next/ruoyi-ui/src/views/bid/rfq/detail.vue
wangyu 7ffc140cf8 feat(approval): 新增业务审批流程及配置管理
- 新增审批配置主子表(biz_approval_config / biz_approval_config_user),支持或签
- 5 个业务模块接入审批: 采购订单/客户报价/供应商报价/发货单/订单异议
- 统一审批动作接口(提交/通过/驳回),status=10 表示审批中
- 新增"待我审批"聚合页面,按业务类型筛选
- 修复 logback 写本地路径报错,去除文件 appender
- 修复 Redis SSL 配置在 Spring Boot 4 下需对象格式
- 补齐部分业务表缺失的 update_by/update_time 审计列

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-16 11:14:46 +08:00

437 lines
15 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">
<div class="page-actions">
<el-button icon="el-icon-back" size="small" @click="$router.back()">返回</el-button>
<template v-if="!isEditing">
<el-button type="primary" icon="el-icon-edit" size="small" @click="startEdit"
v-if="rfq && rfq.status === 'draft'" :disabled="loading">编辑</el-button>
<el-button type="warning" icon="el-icon-download" size="small"
:loading="pdfLoading" @click="exportPdf">导出 PDF</el-button>
</template>
<template v-else>
<el-button @click="cancelEdit">取消</el-button>
<el-button type="primary" icon="el-icon-check" :loading="saving" @click="handleSave">保存</el-button>
</template>
</div>
<!-- ===================== 查看模式 ===================== -->
<div v-if="!isEditing" v-loading="loading">
<div v-if="rfq" id="rfq-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">询价单RFQ</div>
</div>
<div class="pdf-header-no">{{ rfq.rfqNo }}</div>
</div>
<div class="pdf-divider"></div>
<table class="pdf-meta-table">
<tr>
<td class="meta-label">RFQ编号</td><td class="meta-val">{{ rfq.rfqNo }}</td>
<td class="meta-label">标题</td><td class="meta-val">{{ rfq.rfqTitle }}</td>
</tr>
<tr>
<td class="meta-label">截止日期</td><td class="meta-val">{{ rfq.deadline }}</td>
<td class="meta-label">状态</td>
<td class="meta-val"><el-tag :type="statusType(rfq.status)" size="small">{{ statusLabel(rfq.status) }}</el-tag></td>
</tr>
<tr>
<td class="meta-label">交货地址</td><td class="meta-val" colspan="3">{{ rfq.deliveryAddr || '-' }}</td>
</tr>
<tr v-if="rfq.clientQuoteNo">
<td class="meta-label">关联甲方报价</td>
<td class="meta-val" colspan="3">
<el-button type="text" size="small" @click="viewClientQuote">
{{ rfq.clientQuoteNo }} - {{ rfq.clientName || '' }}
</el-button>
</td>
</tr>
<tr v-if="rfq.remark">
<td class="meta-label">备注</td><td class="meta-val" colspan="3">{{ rfq.remark }}</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 rfq.items" :key="i">
<td>{{ i + 1 }}</td>
<td>{{ item.materialName }}</td>
<td>{{ item.spec || '-' }}</td>
<td>{{ item.unit }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.expectedPrice != null ? '¥' + item.expectedPrice : '-' }}</td>
</tr>
</tbody>
</table>
<div v-if="quotations.length > 0">
<div class="pdf-section-title" style="margin-top:24px">收到报价汇总</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="q in quotations" :key="q.quotationId">
<td>{{ q.quoteNo }}</td>
<td>{{ q.supplierName }}</td>
<td class="amount-cell">¥{{ q.totalAmount }}</td>
<td>{{ q.deliveryDays }}</td>
<td><el-tag size="mini" :type="qStatusType(q.status)">{{ qStatusLabel(q.status) }}</el-tag></td>
<td>{{ q.submitTime }}</td>
</tr>
</tbody>
</table>
</div>
<div class="pdf-footer">生成时间{{ new Date().toLocaleString('zh-CN') }}</div>
</div>
</div>
<!-- ===================== 编辑模式 ===================== -->
<div v-else v-loading="loading">
<el-card shadow="never" class="edit-card">
<div slot="header"><strong>基本信息</strong></div>
<el-form ref="editForm" :model="editForm" :rules="editRules" label-width="110px" size="small">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="RFQ编号">
<el-input :value="editForm.rfqNo" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-tag :type="statusType(editForm.status)" size="small">{{ statusLabel(editForm.status) }}</el-tag>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="RFQ标题" prop="rfqTitle">
<el-input v-model="editForm.rfqTitle" placeholder="请输入标题" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="截止日期" prop="deadline">
<el-date-picker v-model="editForm.deadline" type="datetime" placeholder="选择截止日期"
value-format="yyyy-MM-dd HH:mm:ss" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="交货地址">
<el-input v-model="editForm.deliveryAddr" placeholder="请输入交货地址" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="editForm.clientQuoteNo">
<el-form-item label="关联甲方报价">
<el-button type="text" size="small" @click="viewClientQuote">
{{ editForm.clientQuoteNo }} - {{ editForm.clientName || '' }}
</el-button>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注">
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" class="edit-card" style="margin-top:12px">
<div slot="header">
<strong>物料明细</strong>
<el-button style="float:right" type="primary" size="mini" icon="el-icon-plus" @click="addEditItem">添加行</el-button>
</div>
<el-table :data="editForm.items" border size="small">
<el-table-column type="index" width="46" label="#" />
<el-table-column label="物料名称" min-width="160">
<template slot-scope="s">
<el-input v-model="s.row.materialName" size="mini" placeholder="物料名称" />
</template>
</el-table-column>
<el-table-column label="规格型号" min-width="130">
<template slot-scope="s">
<el-input v-model="s.row.spec" size="mini" />
</template>
</el-table-column>
<el-table-column label="单位" width="80">
<template slot-scope="s">
<el-input v-model="s.row.unit" size="mini" />
</template>
</el-table-column>
<el-table-column label="数量" width="110">
<template slot-scope="s">
<el-input-number v-model="s.row.quantity" :min="0" :precision="2" size="small" style="width:100%" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="预期单价" width="130">
<template slot-scope="s">
<el-input-number v-model="s.row.expectedPrice" :min="0" :precision="2" size="small" style="width:100%" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="参考来源" width="90" align="center">
<template slot-scope="s">
<span v-if="s.row._fromClient" style="color:#e4393c;font-size:12px">甲方报价</span>
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center">
<template slot-scope="s">
<el-button type="text" icon="el-icon-delete" style="color:#f56c6c" @click="removeEditItem(s.$index)" />
</template>
</el-table-column>
</el-table>
<div v-if="!editForm.items || editForm.items.length === 0"
style="text-align:center;padding:20px;color:#c0c4cc;font-size:13px">
暂无物料点击上方添加行按钮添加
</div>
</el-card>
</div>
</div>
</template>
<script>
import { getRfq, updateRfq } from "@/api/bid/rfq";
import { listQuotation } from "@/api/bid/quotation";
import logoImg from "@/assets/logo/logo.svg";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
export default {
name: "RfqDetail",
data() {
return {
loading: false, saving: false, pdfLoading: false,
rfq: null, quotations: [],
isEditing: false,
editForm: { items: [] },
editRules: {
rfqTitle: [{ required: true, message: "标题不能为空", trigger: "blur" }]
},
logoSrc: logoImg
};
},
created() {
const rfqId = this.$route.query.rfqId;
const editMode = this.$route.query.edit === '1';
if (rfqId) {
this.loadDetail(rfqId, editMode);
}
},
methods: {
loadDetail(rfqId, editMode) {
this.loading = true;
getRfq(rfqId).then(r => {
this.rfq = r.data;
if (editMode && this.rfq && this.rfq.status === 'draft') {
this.isEditing = true;
this.initEditForm();
}
this.loading = false;
}).catch(() => { this.loading = false; });
listQuotation({ rfqId, pageSize: 50 }).then(r => {
this.quotations = r.rows || [];
});
},
// ---- 编辑模式 ----
initEditForm() {
this.editForm = {
rfqId: this.rfq.rfqId,
rfqNo: this.rfq.rfqNo,
rfqTitle: this.rfq.rfqTitle,
deadline: this.rfq.deadline,
deliveryAddr: this.rfq.deliveryAddr || '',
clientQuoteId: this.rfq.clientQuoteId,
clientQuoteNo: this.rfq.clientQuoteNo,
clientName: this.rfq.clientName,
remark: this.rfq.remark || '',
status: this.rfq.status,
items: (this.rfq.items || []).map(item => ({ ...item }))
};
},
startEdit() {
if (this.rfq.status !== 'draft') {
this.$message.warning('只有草稿状态的RFQ可以编辑');
return;
}
this.initEditForm();
this.isEditing = true;
},
cancelEdit() {
this.isEditing = false;
this.editForm = { items: [] };
},
handleSave() {
this.$refs.editForm.validate(valid => {
if (!valid) return;
this.saving = true;
updateRfq(this.editForm).then(() => {
this.$modal.msgSuccess("保存成功");
this.isEditing = false;
// 重新加载数据刷新视图
return getRfq(this.editForm.rfqId);
}).then(r => {
this.rfq = r.data;
this.quotations = [];
listQuotation({ rfqId: this.editForm.rfqId, pageSize: 50 }).then(res => {
this.quotations = res.rows || [];
});
}).catch(() => {}).finally(() => { this.saving = false; });
});
},
addEditItem() {
if (!this.editForm.items) this.editForm.items = [];
this.editForm.items.push({
materialName: '', spec: '', unit: '', quantity: 1, expectedPrice: null
});
},
removeEditItem(idx) {
this.editForm.items.splice(idx, 1);
},
// ---- 关联跳转 ----
viewClientQuote() {
const quoteId = this.rfq ? this.rfq.clientQuoteId : this.editForm.clientQuoteId;
if (quoteId) {
this.$router.push({ path: '/bid/clientquote/detail', query: { quoteId } });
}
},
// ---- PDF导出 ----
async exportPdf() {
this.pdfLoading = true;
try {
const el = document.getElementById("rfq-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.rfq.rfqNo || "export") + ".pdf");
} finally {
this.pdfLoading = false;
}
},
statusType(s) { return { draft:"info", published:"warning", closed:"", completed:"success" }[s] || ""; },
statusLabel(s) { return { draft:"草稿", published:"已发布", closed:"已关闭", completed:"已完成" }[s] || s; },
qStatusType(s) { return { draft:"info", submitted:"warning", accepted:"success", rejected:"danger" }[s] || ""; },
qStatusLabel(s) { return { draft:"草稿", submitted:"已提交", accepted:"已采纳", rejected:"已拒绝" }[s] || s; }
}
};
</script>
<style scoped>
.page-actions {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.edit-card ::v-deep .el-card__body { padding: 20px; }
.pdf-area {
padding: 28px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
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: 52px;
height: 52px;
object-fit: contain;
margin-right: 16px;
}
.pdf-header-text { flex: 1; }
.pdf-company {
font-size: 22px;
font-weight: 700;
color: #e4393c;
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 #e4393c;
margin-bottom: 18px;
}
.pdf-meta-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 22px;
}
.pdf-meta-table td {
padding: 8px 12px;
border: 1px solid #e4e7ed;
}
.meta-label {
background: #f5f7fa;
color: #606266;
font-weight: 600;
width: 100px;
}
.meta-val { color: #303133; }
.pdf-section-title {
font-size: 14px;
font-weight: 700;
color: #333333;
margin: 0 0 12px;
padding-left: 8px;
border-left: 4px solid #e4393c;
}
.pdf-items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.pdf-items-table th {
background: #e4393c;
color: #fff;
padding: 9px 12px;
text-align: center;
font-weight: 600;
}
.pdf-items-table td {
border: 1px solid #e4e7ed;
padding: 8px 12px;
text-align: center;
}
.pdf-items-table tbody tr:nth-child(even) td { background: #f9fbff; }
.amount-cell { color: #e4393c; font-weight: 600; }
.pdf-footer {
text-align: right;
font-size: 11px;
color: #aaa;
margin-top: 16px;
}
</style>