- 新增审批配置主子表(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>
437 lines
15 KiB
Vue
437 lines
15 KiB
Vue
<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>
|