Files
klp-oa/klp-ui/src/views/mes/qc/certificate/book.vue
砂糖 56b306d301 feat(mes/qc): add quality certificate management function
- 新增质量证明书主、明细的CRUD接口
- 新增质量证明书列表页、明细编辑页
- 新增打印预览组件和PDF导出打印功能
- 添加配套的静态资源和路由依赖
- 优化路由菜单处理逻辑
2026-05-16 17:23:20 +08:00

658 lines
20 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" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="证明书号" prop="certificateNo">
<el-input
v-model="queryParams.certificateNo"
placeholder="请输入证明书号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="合同号" prop="contractNo">
<el-input
v-model="queryParams.contractNo"
placeholder="请输入合同号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input
v-model="queryParams.productName"
placeholder="请输入产品名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="执行标准" prop="standard">
<el-input
v-model="queryParams.standard"
placeholder="请输入执行标准"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="收货单位" prop="consignee">
<el-input
v-model="queryParams.consignee"
placeholder="请输入收货单位"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="生产厂家" prop="manufacturer">
<el-input
v-model="queryParams.manufacturer"
placeholder="请输入生产厂家"
clearable
@keyup.enter.native="handleQuery"
/>
</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-col :span="1.5">
<el-button
type="success"
plain
icon="el-icon-edit"
size="mini"
:disabled="single"
@click="handleUpdate"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<div v-loading="loading" class="certificate-list">
<div
v-for="item in certificateList"
:key="item.certificateId"
class="certificate-card"
:class="{ 'certificate-card-selected': selectedIds.includes(item.certificateId) }"
@click="handleCardClick(item)"
>
<div class="card-top-bar"></div>
<div class="card-title-row">
<span class="card-doc-type">质量保证书</span>
<span class="card-sep">|</span>
<span class="card-env">MES-QC-{{ item.certificateNo || '' }}</span>
</div>
<div class="card-cert-no">
<span class="card-cert-no-label">No.</span>
<span class="card-cert-no-value">{{ item.certificateNo }}</span>
</div>
<div class="card-info-grid">
<div class="card-info-item">
<span class="card-info-label">合同号</span>
<span class="card-info-value">{{ item.contractNo || '-' }}</span>
</div>
<div class="card-info-item">
<span class="card-info-label">产品名称</span>
<span class="card-info-value">{{ item.productName || '-' }}</span>
</div>
<div class="card-info-item">
<span class="card-info-label">执行标准</span>
<span class="card-info-value">{{ item.standard || '-' }}</span>
</div>
<div class="card-info-item">
<span class="card-info-label">收货单位</span>
<span class="card-info-value">{{ item.consignee || '-' }}</span>
</div>
<div class="card-info-item">
<span class="card-info-label">生产厂家</span>
<span class="card-info-value">{{ item.manufacturer || '-' }}</span>
</div>
<div class="card-info-item">
<span class="card-info-label">签发日期</span>
<span class="card-info-value">{{ parseTime(item.issueDate, '{y}-{m}-{d}') }}</span>
</div>
</div>
<div class="card-footer">
<div class="card-stamp-placeholder">质检专用</div>
<div class="card-actions">
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleUpdate(item)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDelete(item)">删除</el-button>
<el-button size="mini" type="text" icon="el-icon-view" @click.stop="handlePreview(item)">预览</el-button>
<el-button size="mini" type="text" icon="el-icon-printer" @click.stop="handlePrint(item)">打印</el-button>
</div>
</div>
</div>
</div>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改质量证明书主对话框 -->
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="证明书号" prop="certificateNo">
<el-input v-model="form.certificateNo" placeholder="请输入证明书号" />
</el-form-item>
<el-form-item label="合同号" prop="contractNo">
<el-input v-model="form.contractNo" placeholder="请输入合同号" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input v-model="form.productName" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="执行标准" prop="standard">
<el-input v-model="form.standard" placeholder="请输入执行标准" />
</el-form-item>
<el-form-item label="收货单位" prop="consignee">
<el-input v-model="form.consignee" placeholder="请输入收货单位" />
</el-form-item>
<el-form-item label="生产厂家" prop="manufacturer">
<el-input v-model="form.manufacturer" placeholder="请输入生产厂家" />
</el-form-item>
<el-form-item label="签发日期" prop="issueDate">
<el-date-picker clearable
v-model="form.issueDate"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
placeholder="请选择签发日期">
</el-date-picker>
</el-form-item>
<el-form-item label="质保证明说明" prop="note">
<el-input v-model="form.note" type="textarea" placeholder="请输入内容" auto-height />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" auto-height />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<!-- 预览对话框 -->
<el-dialog :title="previewTitle" :visible.sync="previewVisible" width="1200px" append-to-body fullscreen>
<div class="preview-content-wrapper">
<CertificatePrintPreview
ref="printComponent"
:visible="true"
:preview="true"
:certificate="previewData"
:items="previewItems"
/>
</div>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleExportPdfFromPreview">导出PDF</el-button>
<el-button type="success" @click="handlePrintFromPreview">打印</el-button>
<el-button @click="previewVisible = false">关闭</el-button>
</div>
</el-dialog>
<!-- 隐藏的打印组件 -->
<CertificatePrintPreview
v-show="false"
ref="hiddenPrintComponent"
:certificate="printCertificateData"
:items="printItemsData"
/>
</div>
</template>
<script>
import { listCertificate, getCertificate, delCertificate, addCertificate, updateCertificate } from "@/api/mes/qc/certificate";
import { listCertificateItem } from "@/api/mes/qc/certificateItem";
import CertificatePrintPreview from "./components/CertificatePrintPreview.vue";
import { print as printPdf, downloadPdf } from "./lib/printUtils";
export default {
name: "Certificate",
components: {
CertificatePrintPreview
},
data() {
return {
buttonLoading: false,
loading: true,
ids: [],
selectedIds: [],
single: true,
multiple: true,
showSearch: true,
total: 0,
certificateList: [],
title: "",
open: false,
previewVisible: false,
previewTitle: "",
previewData: {},
previewItems: [],
printComponentVisible: false,
printCertificateData: {},
printItemsData: [],
queryParams: {
pageNum: 1,
pageSize: 10,
certificateNo: undefined,
contractNo: undefined,
productName: undefined,
standard: undefined,
consignee: undefined,
manufacturer: undefined,
},
form: {},
rules: {
}
};
},
created() {
this.getList();
},
methods: {
/** 查询质量证明书主列表 */
getList() {
this.loading = true;
listCertificate(this.queryParams).then(response => {
this.certificateList = response.rows;
this.total = response.total;
this.loading = false;
});
},
// 取消按钮
cancel() {
this.open = false;
this.reset();
},
// 表单重置
reset() {
this.form = {
certificateId: undefined,
certificateNo: undefined,
contractNo: undefined,
productName: undefined,
standard: undefined,
consignee: undefined,
manufacturer: undefined,
issueDate: undefined,
note: 'D.T=Denu_Test \t T.S=Tensile Strength \t D=弯心直径MandrelDiameter \n Strength \t G.L=拉伸标距GaugeLength \t EL=Percentage Elongation After Fracture',
remark: '1.本产品经检验满足订货标准要求。The material has been tested with satisfactory resultsin accordance with the speciicatication \n 2.本质量证明书中空白项目均不作为交货条件。The blank items shouldntbe regarded as delivery conditions. \n 3.盖章后生效。The quality certificate willcome into force with a valid stamp.',
delFlag: undefined,
createTime: undefined,
createBy: undefined,
updateTime: undefined,
updateBy: undefined
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
this.getList();
},
/** 重置按钮操作 */
resetQuery() {
this.resetForm("queryForm");
this.handleQuery();
},
handleCardClick(item) {
const index = this.selectedIds.indexOf(item.certificateId);
if (index > -1) {
this.selectedIds.splice(index, 1);
} else {
this.selectedIds.push(item.certificateId);
}
this.updateSelectionStatus();
},
handleCheckboxChange(certificateId, event) {
if (event.target.checked) {
if (!this.selectedIds.includes(certificateId)) {
this.selectedIds.push(certificateId);
}
} else {
const index = this.selectedIds.indexOf(certificateId);
if (index > -1) {
this.selectedIds.splice(index, 1);
}
}
this.updateSelectionStatus();
},
updateSelectionStatus() {
this.ids = [...this.selectedIds];
this.single = this.selectedIds.length !== 1;
this.multiple = this.selectedIds.length === 0;
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加质量证明书主";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.loading = true;
this.reset();
const certificateId = row.certificateId || this.ids
getCertificate(certificateId).then(response => {
this.loading = false;
this.form = response.data;
this.open = true;
this.title = "修改质量证明书主";
});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
this.buttonLoading = true;
if (this.form.certificateId != null) {
updateCertificate(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
} else {
addCertificate(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
}).finally(() => {
this.buttonLoading = false;
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const certificateIds = row.certificateId || this.ids;
this.$modal.confirm('是否确认删除质量证明书主编号为"' + certificateIds + '"的数据项?').then(() => {
this.loading = true;
return delCertificate(certificateIds);
}).then(() => {
this.loading = false;
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {
}).finally(() => {
this.loading = false;
});
},
/** 预览按钮操作 */
async handlePreview(row) {
const certificateId = row.certificateId;
this.loading = true;
try {
const [certRes, itemsRes] = await Promise.all([
getCertificate(certificateId),
listCertificateItem({ certificateId, pageSize: 100 })
]);
this.previewData = certRes.data;
this.previewItems = itemsRes.rows || [];
this.previewTitle = `预览 - ${certRes.data.certificateNo}`;
this.previewVisible = true;
} catch (error) {
this.$message.error('获取数据失败');
} finally {
this.loading = false;
}
},
/** 打印按钮操作 */
async handlePrint(row) {
const certificateId = row.certificateId;
await this.preparePrintComponent(certificateId);
await this.$nextTick();
const el = this.$refs.hiddenPrintComponent.$refs.certificateContent;
await printPdf(el);
},
/** 从预览打印 */
async handlePrintFromPreview() {
await this.$nextTick();
const el = this.$refs.printComponent.$refs.certificateContent;
await printPdf(el);
},
/** 导出PDF按钮操作 */
async handleExportPdf() {
if (this.ids.length !== 1) {
this.$message.warning('请选择一条质保书进行导出');
return;
}
await this.preparePrintComponent(this.ids[0]);
await this.$nextTick();
const fileName = `${this.printCertificateData.certificateNo || '质量证明书'}_${new Date().getTime()}.pdf`;
const el = this.$refs.hiddenPrintComponent.$refs.certificateContent;
await downloadPdf(el, fileName);
this.$message.success('导出成功');
},
/** 从预览导出PDF */
async handleExportPdfFromPreview() {
await this.$nextTick();
const fileName = `${this.previewData.certificateNo || '质量证明书'}_${new Date().getTime()}.pdf`;
const el = this.$refs.printComponent.$refs.certificateContent;
await downloadPdf(el, fileName);
this.$message.success('导出成功');
},
async preparePrintComponent(certificateId) {
this.loading = true;
try {
const [certRes, itemsRes] = await Promise.all([
getCertificate(certificateId),
listCertificateItem({ certificateId, pageSize: 100 })
]);
this.printCertificateData = certRes.data;
this.printItemsData = itemsRes.rows || [];
this.printComponentVisible = true;
await this.$nextTick();
} catch (error) {
this.$message.error('获取数据失败');
} finally {
this.loading = false;
}
}
}
};
</script>
<style scoped>
.certificate-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
gap: 18px;
padding: 10px;
}
.preview-content-wrapper {
display: flex;
justify-content: center;
padding: 20px;
}
.certificate-card {
background: #fcf9f2;
border: 1px solid #d6cfc0;
border-radius: 2px;
padding: 18px 20px 14px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.card-top-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #3a3a3a 0%, #7a7a7a 50%, #3a3a3a 100%);
}
.certificate-card:hover {
border-color: #b8ad98;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.1);
}
.certificate-card-selected {
border-color: #7a6f5e;
border-width: 2px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.08);
}
.card-checkbox {
position: absolute;
top: 14px;
right: 14px;
z-index: 1;
}
.card-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
.card-doc-type {
font-size: 15px;
font-weight: 700;
color: #2a2a2a;
letter-spacing: 2px;
font-family: "SimSun", "Songti SC", "Noto Serif SC", serif;
}
.card-sep {
color: #c0b8a8;
font-size: 14px;
font-weight: 200;
}
.card-env {
font-size: 10px;
color: #9a9080;
letter-spacing: 0.5px;
font-family: "Courier New", monospace;
}
.card-cert-no {
display: flex;
align-items: baseline;
gap: 4px;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px solid #ddd6c8;
}
.card-cert-no-label {
font-size: 11px;
color: #8a8070;
font-style: italic;
font-family: "Times New Roman", serif;
}
.card-cert-no-value {
font-size: 20px;
font-weight: 700;
color: #1a1a1a;
letter-spacing: 1px;
font-family: "Courier New", "Times New Roman", monospace;
}
.card-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2px 16px;
}
.card-info-item {
display: flex;
align-items: baseline;
padding: 5px 0;
border-bottom: 1px dotted #e6dfd0;
}
.card-info-item:nth-last-child(-n+2) {
border-bottom: none;
}
.card-info-label {
font-size: 11px;
color: #8a8270;
min-width: 58px;
flex-shrink: 0;
}
.card-info-value {
font-size: 13px;
color: #222;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 10px;
border-top: 1px solid #ddd6c8;
}
.card-stamp-placeholder {
width: 44px;
height: 44px;
border: 1.5px solid #9a8a7a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #9a8a7a;
font-size: 9px;
font-weight: bold;
text-align: center;
line-height: 1.2;
font-family: "SimSun", serif;
}
.card-actions {
display: flex;
gap: 6px;
}
.card-actions .el-button {
padding: 3px 6px;
font-size: 12px;
}
</style>