feat(mes/qc): add quality certificate management function

- 新增质量证明书主、明细的CRUD接口
- 新增质量证明书列表页、明细编辑页
- 新增打印预览组件和PDF导出打印功能
- 添加配套的静态资源和路由依赖
- 优化路由菜单处理逻辑
This commit is contained in:
2026-05-16 17:23:20 +08:00
parent 5c2910987e
commit 56b306d301
11 changed files with 2136 additions and 2 deletions

1
klp-ui/.gitignore vendored
View File

@@ -12,6 +12,7 @@ selenium-debug.log
# Editor directories and files
.idea
.trae
.vscode
*.suo
*.ntvs*

View File

@@ -1,9 +1,29 @@
import request from '@/utils/request'
// 获取路由
const addParentPrefix = (menuList, parentName = '') => {
return menuList.map(menu => {
const newName = parentName ? `${parentName}-${menu.name}` : menu.name
const newMenu = { ...menu, name: newName }
if (menu.children && menu.children.length > 0) {
newMenu.children = addParentPrefix(menu.children, newName)
}
return newMenu
})
}
export const getRouters = () => {
return request({
url: '/getRouters',
method: 'get'
}).then(res => {
if (res && Array.isArray(res.data)) {
return {
data: addParentPrefix(res.data),
code: 200,
msg: 'success'
}
}
console.log(res, 'getRouters res')
return res
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询质量证明书主列表
export function listCertificate(query) {
return request({
url: '/qc/certificate/list',
method: 'get',
params: query
})
}
// 查询质量证明书主详细
export function getCertificate(certificateId) {
return request({
url: '/qc/certificate/' + certificateId,
method: 'get'
})
}
// 新增质量证明书主
export function addCertificate(data) {
return request({
url: '/qc/certificate',
method: 'post',
data: data
})
}
// 修改质量证明书主
export function updateCertificate(data) {
return request({
url: '/qc/certificate',
method: 'put',
data: data
})
}
// 删除质量证明书主
export function delCertificate(certificateId) {
return request({
url: '/qc/certificate/' + certificateId,
method: 'delete'
})
}

View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
// 查询质量证明书明细列表
export function listCertificateItem(query) {
return request({
url: '/qc/certificateItem/list',
method: 'get',
params: query
})
}
// 查询质量证明书明细详细
export function getCertificateItem(itemId) {
return request({
url: '/qc/certificateItem/' + itemId,
method: 'get'
})
}
// 新增质量证明书明细
export function addCertificateItem(data) {
return request({
url: '/qc/certificateItem',
method: 'post',
data: data
})
}
// 修改质量证明书明细
export function updateCertificateItem(data) {
return request({
url: '/qc/certificateItem',
method: 'put',
data: data
})
}
// 删除质量证明书明细
export function delCertificateItem(itemId) {
return request({
url: '/qc/certificateItem/' + itemId,
method: 'delete'
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,4 +1,5 @@
export { default as CheckItemSelect } from './CheckItemSelect/index.vue';
export { default as SchemeSelect } from './SchemeSelect/index.vue';
export { default as ProductSelect } from './ProductSelect/index.vue';
export { default as RawMaterialSelect } from './RawMaterialSelect/index.vue';
export { default as UserSelect } from './UserSelect/index.vue';

View File

@@ -0,0 +1,657 @@
<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>

View File

@@ -0,0 +1,384 @@
<template>
<div class="certificate-print-container" :class="{ 'is-hidden': !preview }">
<div ref="certificateContent" class="certificate-pdf-content" :class="{ 'is-preview': preview }">
<div class="pdf-header">
<div class="header-left">
<img src="@/assets/logo/logo.png" class="company-logo" alt="logo" />
<div class="company-name-cn">科伦普</div>
<div class="company-name-en">KE LUN PU</div>
</div>
<div class="header-center">
<div class="main-title">产品质量证明书</div>
<div class="main-title-en">PRODUCT QUALITY CERTIFICATE</div>
</div>
<div class="header-right">
<div class="company-full-name">嘉祥科伦普重工有限公司</div>
<div class="company-address">山东省济宁市嘉祥县化工园区</div>
</div>
</div>
<div class="pdf-body">
<table class="info-table">
<tr>
<td class="label-cell">执行标准(SPECIFICATION):</td>
<td class="value-cell">{{ certificate.standard || '/' }}</td>
<td class="label-cell">证明书号(CERTIFICATE NO.):</td>
<td class="value-cell">{{ certificate.certificateNo || '' }}</td>
</tr>
<tr>
<td class="label-cell">收货单位(CONSIGNEE):</td>
<td class="value-cell">{{ certificate.consignee || '-' }}</td>
<td class="label-cell">合同号(CONTRACT NO.):</td>
<td class="value-cell">{{ certificate.contractNo || '' }}</td>
</tr>
<tr>
<td class="label-cell">产品名称(PRODUCT):</td>
<td class="value-cell">{{ certificate.productName || '' }}</td>
<td class="label-cell">签发日期(DATE OF ISSUE):</td>
<td class="value-cell">{{ formatDate(certificate.issueDate) || '' }}</td>
</tr>
</table>
<table class="data-table">
<thead>
<tr>
<th rowspan="2">序号</th>
<th rowspan="2">钢卷号<br>Coil No.</th>
<th rowspan="2">炉号<br>Heat No.</th>
<th rowspan="2">材质<br>Type</th>
<th rowspan="2">规格 Size<br>(mm)</th>
<th rowspan="2">件数<br>Pieces</th>
<th rowspan="2">重量<br>Weight(t)</th>
<th colspan="6">化学成分(%)<br>Chemical Composition</th>
<th colspan="3">拉伸试验 Tensile Test<br>(G.L=80mm)</th>
<th rowspan="2">硬度试验<br>Hardness Test</th>
<th rowspan="2">弯曲试验<br>180° B.T<br>D=0a</th>
<th rowspan="2">表面质量</th>
<th rowspan="2">表面结构</th>
<th rowspan="2">边缘状态</th>
</tr>
<tr>
<th>C</th>
<th>Si</th>
<th>Mn</th>
<th>P</th>
<th>S</th>
<th>Als</th>
<th>屈服 RP0.2<br>Y.S(MPa)</th>
<th>抗拉 T.S<br>(MPa)</th>
<th>伸长率 EL<br>(%)</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in items" :key="index">
<td>{{ index + 1 }}</td>
<td>{{ item.coilNo || '' }}</td>
<td>{{ item.heatNo || '' }}</td>
<td>{{ item.materialType || '' }}</td>
<td>{{ item.size || '' }}</td>
<td>{{ item.pieces || '' }}</td>
<td>{{ item.weight || '' }}</td>
<td>{{ item.c || '/' }}</td>
<td>{{ item.si || '/' }}</td>
<td>{{ item.mn || '/' }}</td>
<td>{{ item.p || '/' }}</td>
<td>{{ item.s || '/' }}</td>
<td>{{ item.als || '/' }}</td>
<td>{{ item.yieldStrength || '/' }}</td>
<td>{{ item.tensileStrength || '/' }}</td>
<td>{{ item.elongation || '/' }}</td>
<td>{{ item.hardness || '/' }}</td>
<td>{{ item.bendingTest || '/' }}</td>
<td>{{ item.surfaceQuality || '/' }}</td>
<td>{{ item.surfaceStructure || '/' }}</td>
<td>{{ item.edgeStatus || '/' }}</td>
</tr>
<tr v-if="items.length === 0">
<td colspan="21" class="empty-cell">暂无数据</td>
</tr>
</tbody>
</table>
<table class="footer-table">
<tr>
<td class="footer-label">注释<br>NOTE</td>
<td class="footer-content">
<pre>{{ certificate.note || '/' }}</pre>
</td>
</tr>
<tr>
<td class="footer-label">备注<br>REMARK</td>
<td class="footer-content footer-remark-cell">
<pre class="remark-text">{{ certificate.remark || '/' }}</pre>
<div class="signature-area">
<img src="@/assets/images/zhijian.png" class="stamp-image" alt="质检专用章" />
<div class="signature-block">
<div class="signature-label">质量负责人:</div>
<img src="@/assets/images/yanghongyan.png" class="signature-image" alt="签名" />
</div>
</div>
</td>
</tr>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CertificatePrintPreview',
props: {
certificate: {
type: Object,
default: () => ({})
},
items: {
type: Array,
default: () => []
},
preview: {
type: Boolean,
default: false
}
},
methods: {
formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
}
}
};
</script>
<style scoped>
.certificate-print-container.is-hidden {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
opacity: 0;
pointer-events: none;
}
.certificate-print-container {
display: flex;
justify-content: center;
align-items: flex-start;
}
.certificate-pdf-content {
width: 297mm;
min-height: 210mm;
background: white;
padding: 8mm 10mm;
font-family: "SimSun", "宋体", Arial, sans-serif;
color: #000;
}
.pdf-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
padding-bottom: 6px;
}
.header-left {
text-align: center;
width: 90px;
flex-shrink: 0;
}
.company-logo {
width: 70px;
height: auto;
margin-bottom: 2px;
}
.company-name-cn {
font-size: 14px;
font-weight: bold;
color: #000;
line-height: 1.2;
}
.company-name-en {
font-size: 9px;
color: #333;
letter-spacing: 1px;
}
.header-center {
flex: 1;
text-align: center;
padding: 0 10px;
}
.main-title {
font-size: 22px;
font-weight: bold;
color: #000;
letter-spacing: 4px;
line-height: 1.3;
}
.main-title-en {
font-size: 13px;
color: #000;
font-family: "Times New Roman", serif;
letter-spacing: 1px;
margin-top: 2px;
}
.header-right {
text-align: right;
width: 160px;
flex-shrink: 0;
}
.company-full-name {
font-size: 13px;
font-weight: bold;
color: #000;
line-height: 1.4;
}
.company-address {
font-size: 10px;
color: #333;
margin-top: 2px;
}
.pdf-body {
margin-top: 5px;
}
.info-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 0;
font-size: 11px;
}
.info-table td {
border: 1px solid #000;
padding: 3px 6px;
height: 22px;
}
.info-table .label-cell {
font-weight: bold;
width: 22%;
white-space: nowrap;
background: #fafafa;
}
.info-table .value-cell {
width: 28%;
text-align: center;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 0;
font-size: 9px;
}
.data-table th,
.data-table td {
border: 1px solid #000;
padding: 2px 3px;
text-align: center;
vertical-align: middle;
line-height: 1.3;
}
.data-table th {
background-color: #f0f0f0;
font-weight: bold;
font-size: 9px;
}
.data-table td {
font-size: 9px;
}
.empty-cell {
text-align: center;
padding: 20px;
color: #999;
font-size: 12px;
}
.footer-table {
width: 100%;
border-collapse: collapse;
margin-top: 0;
font-size: 9px;
}
.footer-table td {
border: 1px solid #000;
padding: 4px 6px;
vertical-align: top;
}
.footer-label {
font-weight: bold;
width: 60px;
text-align: center;
vertical-align: middle;
font-size: 9px;
line-height: 1.4;
}
.footer-content {
font-size: 9px;
line-height: 1.5;
}
.footer-remark-cell {
position: relative;
padding-right: 140px;
}
.remark-text {
font-size: 9px;
line-height: 1.6;
}
.signature-area {
position: absolute;
right: 8px;
bottom: 8px;
display: flex;
align-items: flex-end;
gap: 8px;
}
.stamp-image {
width: 80px;
height: auto;
}
.signature-block {
text-align: center;
}
.signature-label {
font-size: 9px;
margin-bottom: 2px;
white-space: nowrap;
}
.signature-image {
height: 35px;
width: auto;
}
</style>

View File

@@ -0,0 +1,888 @@
<template>
<div class="app-container">
<div class="tab-container">
<div class="select-button" @click="openCertificateDialog">
<i class="el-icon-setting"></i>
</div>
<div class="custom-tabs">
<div class="tab-nav">
<button v-if="certificateList.length > 0 && queryParams.pageNum > 1" class="nav-btn prev" @click="prevPage">
<i class="el-icon-arrow-left"></i>
</button>
<div class="tab-header">
<div v-for="cert in certificateList" :key="cert.certificateId" class="tab-item"
:class="{ active: activeTab === cert.certificateId.toString() }" @click="handleTabClick(cert)">
<div class="tab-title">{{ cert.certificateNo }}</div>
<div class="tab-info">
<span>{{ cert.productName }}</span>
<span class="date">{{ parseTime(cert.issueDate, '{y}-{m}-{d}') }}</span>
</div>
</div>
<div v-if="certificateList.length === 0" class="tab-item disabled">
<div class="tab-title">暂无质保书</div>
<div class="tab-info">请点击齿轮图标选择质保书</div>
</div>
</div>
<button v-if="certificateList.length > 0 && queryParams.pageNum < totalPage" class="nav-btn next" @click="nextPage">
<i class="el-icon-arrow-right"></i>
</button>
</div>
</div>
</div>
<div v-if="currentCertificateId" class="certificate-detail-container">
<div class="card-header">
<h3>{{ currentCertificateInfo.certificateNo || '质保书明细' }}</h3>
<div>
<el-button type="primary" plain @click="handleAdd">新增明细</el-button>
<el-button type="success" plain @click="openBatchAddDialog">批量新增</el-button>
<el-button type="danger" plain @click="handleBatchDelete"
:disabled="selectedRows.length === 0">批量删除</el-button>
<el-button type="warning" plain icon="el-icon-printer" @click="handlePrint">打印</el-button>
<el-button type="info" plain @click="getList">刷新</el-button>
</div>
</div>
<el-form class="certificate-info" :model="currentCertificateInfo" label-width="80px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="证明书号">
<el-input v-model="currentCertificateInfo.certificateNo" @change="handleCertificateInfoChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="合同号">
<el-input v-model="currentCertificateInfo.contractNo" @change="handleCertificateInfoChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="产品名称">
<el-input v-model="currentCertificateInfo.productName" @change="handleCertificateInfoChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="签发日期">
<el-date-picker v-model="currentCertificateInfo.issueDate" type="date" @change="handleCertificateInfoChange" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="执行标准">
<el-input v-model="currentCertificateInfo.standard" @change="handleCertificateInfoChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="收货单位">
<el-input v-model="currentCertificateInfo.consignee" @change="handleCertificateInfoChange" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="生产厂家">
<el-input v-model="currentCertificateInfo.manufacturer" @change="handleCertificateInfoChange" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-table v-loading="loading" :data="certificateItemList" style="width: 100%" border
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" fixed="left" />
<el-table-column label="操作" align="center" width="140" fixed="left">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="handleSave(scope.row)">保存</el-button>
<el-button type="danger" size="mini" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
<el-table-column label="钢卷号" align="center" prop="coilNo" width="180">
<template slot-scope="scope">
<el-input v-model="scope.row.coilNo" style="background-color: #fff3e6;" placeholder="点击选择钢卷">
<template slot="append">
<el-button size="mini" icon="el-icon-search" @click="openCoilSelectDialog(scope.row)"></el-button>
</template>
</el-input>
</template>
</el-table-column>
<el-table-column label="炉号" align="center" prop="heatNo" width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.heatNo" style="background-color: #e6f7ff;" />
</template>
</el-table-column>
<el-table-column label="材质" align="center" prop="materialType" width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.materialType" style="background-color: #fff3e6;" />
</template>
</el-table-column>
<el-table-column label="规格(mm)" align="center" prop="size" width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.size" style="background-color: #fff3e6;" />
</template>
</el-table-column>
<el-table-column label="件数" align="center" prop="pieces" width="80">
<template slot-scope="scope">
<el-input v-model.number="scope.row.pieces" style="background-color: #fff3e6;" />
</template>
</el-table-column>
<el-table-column label="重量(t)" align="center" prop="weight" width="100">
<template slot-scope="scope">
<el-input v-model.number="scope.row.weight" style="background-color: #fff3e6;" />
</template>
</el-table-column>
<el-table-column label="C(%)" align="center" prop="c" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.c" style="background-color: #e6f7ff;" />
</template>
</el-table-column>
<el-table-column label="Si(%)" align="center" prop="si" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.si" style="background-color: #e6f7ff;" />
</template>
</el-table-column>
<el-table-column label="Mn(%)" align="center" prop="mn" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.mn" style="background-color: #e6f7ff;" />
</template>
</el-table-column>
<el-table-column label="P(%)" align="center" prop="p" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.p" style="background-color: #e6f7ff;" />
</template>
</el-table-column>
<el-table-column label="S(%)" align="center" prop="s" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.s" style="background-color: #e6f7ff;" />
</template>
</el-table-column>
<el-table-column label="Als(%)" align="center" prop="als" width="90">
<template slot-scope="scope">
<el-input v-model="scope.row.als" style="background-color: #e6f7ff;" />
</template>
</el-table-column>
<el-table-column label="屈服强度(MPa)" align="center" prop="yieldStrength" width="130">
<template slot-scope="scope">
<el-input v-model.number="scope.row.yieldStrength" style="background-color: #f6ffed;" />
</template>
</el-table-column>
<el-table-column label="抗拉强度(MPa)" align="center" prop="tensileStrength" width="130">
<template slot-scope="scope">
<el-input v-model.number="scope.row.tensileStrength" style="background-color: #f6ffed;" />
</template>
</el-table-column>
<el-table-column label="伸长率(%)" align="center" prop="elongation" width="110">
<template slot-scope="scope">
<el-input v-model="scope.row.elongation" style="background-color: #f6ffed;" />
</template>
</el-table-column>
<el-table-column label="硬度(HRB)" align="center" prop="hardness" width="100">
<template slot-scope="scope">
<el-input v-model.number="scope.row.hardness" style="background-color: #f6ffed;" />
</template>
</el-table-column>
<el-table-column label="弯曲试验" align="center" prop="bendingTest" width="100">
<template slot-scope="scope">
<el-input v-model="scope.row.bendingTest" style="background-color: #f6ffed;" />
</template>
</el-table-column>
<el-table-column label="表面质量" align="center" prop="surfaceQuality" width="100">
<template slot-scope="scope">
<el-input v-model="scope.row.surfaceQuality" style="background-color: #f6ffed;" />
</template>
</el-table-column>
<el-table-column label="表面结构" align="center" prop="surfaceStructure" width="100">
<template slot-scope="scope">
<el-input v-model="scope.row.surfaceStructure" style="background-color: #f6ffed;" />
</template>
</el-table-column>
<el-table-column label="边缘状态" align="center" prop="edgeStatus" width="100">
<template slot-scope="scope">
<el-input v-model="scope.row.edgeStatus" style="background-color: #f6ffed;" />
</template>
</el-table-column>
<!-- <el-table-column label="备注" align="center" prop="remark">
<template slot-scope="scope">
<el-input v-model="scope.row.remark" style="background-color: #fff0f6;" />
</template>
</el-table-column> -->
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
@pagination="getList" />
</div>
<el-empty v-else class="appempty" description="选择质保书查看明细" />
<el-dialog :title="'选择钢卷'" :visible.sync="coilDialogVisible" width="900px" append-to-body>
<div class="dialog-toolbar">
<el-form :model="coilQueryParams" ref="coilQueryForm" size="small" :inline="true" label-width="68px">
<el-form-item label="钢卷号" prop="coilNo">
<el-input v-model="coilQueryParams.coilNo" placeholder="请输入钢卷号" clearable
@keyup.enter.native="handleCoilQuery" />
</el-form-item>
<el-form-item label="材质" prop="material">
<el-input v-model="coilQueryParams.material" placeholder="请输入材质" clearable
@keyup.enter.native="handleCoilQuery" />
</el-form-item>
<el-form-item label="规格" prop="specification">
<el-input v-model="coilQueryParams.specification" placeholder="请输入规格" clearable
@keyup.enter.native="handleCoilQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleCoilQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetCoilQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table v-loading="coilLoading" :data="coilList" height="350px" @row-click="handleCoilSelect">
<el-table-column label="钢卷号" align="center" prop="currentCoilNo" />
<el-table-column label="材质" align="center" prop="material" />
<el-table-column label="规格" align="center" prop="specification" />
<el-table-column label="重量(t)" align="center" prop="netWeight" />
</el-table>
<pagination v-show="coilTotal > 0" :total="coilTotal" :page.sync="coilQueryParams.pageNum"
:limit.sync="coilQueryParams.pageSize" @pagination="getCoilList" />
</el-dialog>
<el-dialog :title="'选择质保书'" :visible.sync="certificateDialogVisible" width="900px" append-to-body>
<div class="dialog-toolbar">
<el-form :model="certificateQueryParams" ref="certificateQueryForm" size="small" :inline="true" label-width="68px">
<el-form-item label="证明书号" prop="certificateNo">
<el-input v-model="certificateQueryParams.certificateNo" placeholder="请输入证明书号" clearable
@keyup.enter.native="handleCertificateQuery" />
</el-form-item>
<el-form-item label="合同号" prop="contractNo">
<el-input v-model="certificateQueryParams.contractNo" placeholder="请输入合同号" clearable
@keyup.enter.native="handleCertificateQuery" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input v-model="certificateQueryParams.productName" placeholder="请输入产品名称" clearable
@keyup.enter.native="handleCertificateQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleCertificateQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetCertificateQuery">重置</el-button>
<el-button type="success" icon="el-icon-plus" size="mini" @click="handleAddCertificate">新增质保书</el-button>
</el-form-item>
</el-form>
</div>
<el-table v-loading="certificateLoading" :data="allCertificateList" height="350px" @row-click="handleTabClick">
<el-table-column label="证明书号" align="center" prop="certificateNo" />
<el-table-column label="合同号" align="center" prop="contractNo" />
<el-table-column label="产品名称" align="center" prop="productName" />
<el-table-column label="执行标准" align="center" prop="standard" />
<el-table-column label="收货单位" align="center" prop="consignee" />
<el-table-column label="签发日期" align="center" prop="issueDate" width="140">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.issueDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
</el-table>
<pagination v-show="certificateTotal > 0" :total="certificateTotal" :page.sync="certificateQueryParams.pageNum"
:limit.sync="certificateQueryParams.pageSize" @pagination="getCertificateList" />
</el-dialog>
<el-dialog :title="'新增质保书'" :visible.sync="addCertificateDialogVisible" width="800px" append-to-body>
<el-form :model="certificateForm" ref="certificateForm" label-width="100px">
<el-form-item label="证明书号" prop="certificateNo">
<el-input v-model="certificateForm.certificateNo" placeholder="请输入证明书号" />
</el-form-item>
<el-form-item label="合同号" prop="contractNo">
<el-input v-model="certificateForm.contractNo" placeholder="请输入合同号" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input v-model="certificateForm.productName" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="执行标准" prop="standard">
<el-input v-model="certificateForm.standard" placeholder="请输入执行标准" />
</el-form-item>
<el-form-item label="收货单位" prop="consignee">
<el-input v-model="certificateForm.consignee" placeholder="请输入收货单位" />
</el-form-item>
<el-form-item label="生产厂家" prop="manufacturer">
<el-input v-model="certificateForm.manufacturer" placeholder="请输入生产厂家" />
</el-form-item>
<el-form-item label="签发日期" prop="issueDate">
<el-date-picker v-model="certificateForm.issueDate" value-format="yyyy-MM-dd HH:mm:ss" type="date" placeholder="请选择签发日期" />
</el-form-item>
<el-form-item label="质保证明说明" prop="note">
<el-input v-model="certificateForm.note" type="textarea" placeholder="请输入内容" auto-height />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="certificateForm.remark" type="textarea" placeholder="请输入内容" auto-height />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="addCertificateDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitCertificateForm">确定</el-button>
</div>
</el-dialog>
<CoilSelector
:visible.sync="batchAddDialogVisible"
:use-trigger="false"
:multiple="true"
@confirm="handleBatchAddConfirm"
/>
<!-- 隐藏的打印组件 -->
<CertificatePrintPreview
ref="certificatePrint"
v-show="false"
:certificate="printCertificateData"
:items="printItemsData"
/>
</div>
</template>
<script>
import { listCertificateItem, getCertificateItem, delCertificateItem, addCertificateItem, updateCertificateItem } from "@/api/mes/qc/certificateItem";
import { listCertificate, getCertificate, updateCertificate, addCertificate } from "@/api/mes/qc/certificate";
import { listMaterialCoil } from "@/api/wms/coil";
import CoilSelector from "@/components/CoilSelector/index.vue";
import CertificatePrintPreview from "./components/CertificatePrintPreview.vue";
import { print as printPdf } from "./lib/printUtils";
export default {
components: {
CoilSelector,
CertificatePrintPreview
},
name: "CertificateItem",
computed: {
totalPage() {
return Math.ceil(this.certificateTotal / this.certificateQueryParams.pageSize)
}
},
data() {
return {
buttonLoading: false,
loading: false,
certificateLoading: false,
total: 0,
certificateTotal: 0,
certificateList: [],
allCertificateList: [],
certificateItemList: [],
currentCertificateId: undefined,
currentCertificateInfo: {},
activeTab: '',
certificateDialogVisible: false,
coilDialogVisible: false,
batchAddDialogVisible: false,
addCertificateDialogVisible: false,
printComponentVisible: false,
printCertificateData: {},
printItemsData: [],
certificateForm: {
certificateNo: '',
contractNo: '',
productName: '',
standard: '',
consignee: '',
manufacturer: '',
issueDate: '',
remark: ''
},
coilLoading: false,
coilTotal: 0,
coilList: [],
currentEditRow: null,
selectedRows: [],
queryParams: {
pageNum: 1,
pageSize: 50,
certificateId: undefined
},
certificateQueryParams: {
pageNum: 1,
pageSize: 10,
certificateNo: undefined,
contractNo: undefined,
productName: undefined
},
coilQueryParams: {
pageNum: 1,
pageSize: 10,
coilNo: undefined,
material: undefined,
specification: undefined
}
};
},
created() {
this.getCertificateList();
},
methods: {
parseTime(time, pattern) {
if (!time) return '';
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
const date = new Date(time);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const second = date.getSeconds();
return format.replace('{y}', year)
.replace('{m}', month.toString().padStart(2, '0'))
.replace('{d}', day.toString().padStart(2, '0'))
.replace('{h}', hour.toString().padStart(2, '0'))
.replace('{i}', minute.toString().padStart(2, '0'))
.replace('{s}', second.toString().padStart(2, '0'));
},
getCertificateList() {
this.certificateLoading = true;
listCertificate(this.certificateQueryParams).then(response => {
this.allCertificateList = response.rows;
this.certificateTotal = response.total;
this.certificateList = response.rows;
this.certificateLoading = false;
});
},
handleCertificateQuery() {
this.certificateQueryParams.pageNum = 1;
this.getCertificateList();
},
resetCertificateQuery() {
this.certificateQueryParams = {
pageNum: 1,
pageSize: 10,
certificateNo: undefined,
contractNo: undefined,
productName: undefined
};
this.getCertificateList();
},
handleTabClick(certificate) {
this.activeTab = certificate.certificateId.toString();
this.certificateDialogVisible = false;
this.handleRowClick(certificate);
},
handleRowClick(row) {
this.currentCertificateId = row.certificateId;
this.currentCertificateInfo = row;
this.queryParams.certificateId = row.certificateId;
this.getList();
},
getCertificateDetail(certificateId) {
getCertificate(certificateId).then(response => {
this.currentCertificateInfo = response.data;
});
},
prevPage() {
if (this.certificateQueryParams.pageNum > 1) {
this.certificateQueryParams.pageNum--;
this.getCertificateList();
}
},
nextPage() {
if (this.certificateQueryParams.pageNum < this.totalPage) {
this.certificateQueryParams.pageNum++;
this.getCertificateList();
}
},
openCertificateDialog() {
this.getCertificateList();
this.certificateDialogVisible = true;
},
openCoilSelectDialog(row) {
this.currentEditRow = row;
this.coilDialogVisible = true;
this.getCoilList();
},
getCoilList() {
this.coilLoading = true;
listMaterialCoil(this.coilQueryParams).then(response => {
this.coilList = response.rows;
this.coilTotal = response.total;
this.coilLoading = false;
});
},
handleCoilQuery() {
this.coilQueryParams.pageNum = 1;
this.getCoilList();
},
resetCoilQuery() {
this.coilQueryParams = {
pageNum: 1,
pageSize: 10,
coilNo: undefined,
material: undefined,
specification: undefined
};
this.getCoilList();
},
handleCoilSelect(row) {
if (this.currentEditRow) {
this.currentEditRow.coilNo = row.currentCoilNo;
this.currentEditRow.materialType = row.material;
this.currentEditRow.size = row.specification;
this.currentEditRow.weight = row.netWeight;
this.currentEditRow.pieces = 1;
}
this.coilDialogVisible = false;
this.currentEditRow = null;
},
getList() {
this.loading = true;
listCertificateItem({
...this.queryParams,
certificateId: this.currentCertificateId,
}).then(response => {
this.certificateItemList = response.rows.sort((a, b) => {
return parseInt(a.itemSeqNo) - parseInt(b.itemSeqNo);
});
this.total = response.total;
this.loading = false;
});
},
handleAdd() {
const newRow = {
certificateId: this.currentCertificateId,
itemSeqNo: this.certificateItemList.length + 1
};
addCertificateItem(newRow).then(response => {
this.$message({
message: "新增成功",
type: "success"
});
this.getList();
});
},
openBatchAddDialog() {
this.batchAddDialogVisible = true;
},
handleBatchAddConfirm(coils) {
if (!coils || coils.length === 0) {
this.$message.warning('请选择钢卷');
return;
}
const totalCount = coils.length;
let successCount = 0;
let failCount = 0;
coils.forEach((coil, index) => {
const newRow = {
certificateId: this.currentCertificateId,
itemSeqNo: this.certificateItemList.length + index + 1,
coilNo: coil.currentCoilNo,
materialType: coil.material,
size: coil.specification,
weight: coil.netWeight,
pieces: 1
};
addCertificateItem(newRow).then(() => {
successCount++;
if (successCount + failCount === totalCount) {
this.batchAddDialogVisible = false;
if (successCount === totalCount) {
this.$message.success(`批量新增成功,共新增 ${successCount} 条明细`);
} else {
this.$message.warning(`批量新增完成,成功 ${successCount} 条,失败 ${failCount}`);
}
this.getList();
}
}).catch(() => {
failCount++;
if (successCount + failCount === totalCount) {
this.batchAddDialogVisible = false;
if (failCount === totalCount) {
this.$message.error('批量新增失败');
} else {
this.$message.warning(`批量新增完成,成功 ${successCount} 条,失败 ${failCount}`);
}
this.getList();
}
});
});
},
handleAddCertificate() {
this.certificateForm = {
certificateNo: '',
contractNo: '',
productName: '',
standard: '',
consignee: '',
manufacturer: '',
issueDate: '',
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.',
};
this.addCertificateDialogVisible = true;
},
submitCertificateForm() {
addCertificate(this.certificateForm).then(response => {
this.$message({
message: "新增成功",
type: "success"
});
this.addCertificateDialogVisible = false;
this.getCertificateList();
}).catch(error => {
this.$message({
message: "新增失败",
type: "error"
});
});
},
handleCertificateInfoChange() {
if (!this.currentCertificateId) return;
updateCertificate(this.currentCertificateInfo).then(response => {
this.$message({
message: "保存成功",
type: "success"
});
}).catch(error => {
this.$message({
message: "保存失败",
type: "error"
});
this.getCertificateDetail(this.currentCertificateId);
});
},
handleSave(row) {
updateCertificateItem(row).then(response => {
this.$message({
message: "保存成功",
type: "success"
});
}).catch(error => {
this.$message({
message: "保存失败,正在重新获取数据",
type: "error"
});
this.getList();
});
},
handleDelete(row) {
this.$confirm('是否确认删除该质保书明细?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.buttonLoading = true;
delCertificateItem(row.itemId).then(response => {
this.$message({
message: "删除成功",
type: "success"
});
this.buttonLoading = false;
this.getList();
});
});
},
handleSelectionChange(selection) {
this.selectedRows = selection;
},
handleBatchDelete() {
if (this.selectedRows.length === 0) {
this.$message.warning('请至少选择一条明细');
return;
}
this.$confirm(`确认删除选中的 ${this.selectedRows.length} 条明细?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const ids = this.selectedRows.map(row => row.itemId).join(',');
this.buttonLoading = true;
delCertificateItem(ids).then(() => {
this.$message.success('批量删除成功');
this.getList();
this.buttonLoading = false;
}).catch(error => {
this.$message.error('批量删除失败');
this.buttonLoading = false;
});
});
},
async handlePrint() {
this.printCertificateData = this.currentCertificateInfo;
this.printItemsData = this.certificateItemList || [];
this.$nextTick(() => {
const el = this.$refs.certificatePrint.$refs.certificateContent;
console.log(el);
printPdf(el);
});
}
}
};
</script>
<style scoped>
.tab-container {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.select-button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border: 1px solid #e4e7ed;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
background-color: #f9f9f9;
color: #606266;
font-size: 14px;
user-select: none;
margin-right: 10px;
color: #409eff;
}
.select-button:hover {
background-color: #ecf5ff;
border-color: #c6e2ff;
color: #409eff;
}
.custom-tabs {
flex: 1;
border-bottom: 1px solid #e4e7ed;
}
.tab-nav {
display: flex;
align-items: flex-end;
gap: 4px;
}
.nav-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 52px;
border: 1px solid #e4e7ed;
border-bottom: none;
border-radius: 4px 4px 0 0;
background-color: #f9f9f9;
cursor: pointer;
transition: all 0.3s ease;
color: #606266;
}
.nav-btn:hover {
background-color: #ecf5ff;
border-color: #c6e2ff;
color: #409eff;
}
.tab-header {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 1;
overflow: hidden;
}
.tab-item {
padding: 4px 4px;
border: 1px solid #e4e7ed;
border-bottom: none;
border-radius: 4px 4px 0 0;
cursor: pointer;
transition: all 0.3s ease;
background-color: #f9f9f9;
min-width: 200px;
}
.tab-item:hover {
background-color: #ecf5ff;
border-color: #c6e2ff;
}
.tab-item.active {
background-color: #ffffff;
border-color: #409eff;
border-bottom-color: #ffffff;
color: #409eff;
box-shadow: 0 -2px 0 0 #409eff inset;
}
.tab-item.disabled {
cursor: not-allowed;
background-color: #f5f7fa;
color: #c0c4cc;
border-color: #ebeef5;
}
.tab-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-info {
font-size: 12px;
color: #909399;
display: flex;
justify-content: space-between;
align-items: center;
}
.tab-info .date {
color: #606266;
font-weight: 400;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.card-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.certificate-info {
margin-bottom: 20px;
padding: 15px;
background-color: #fafafa;
border-radius: 8px;
}
.certificate-detail-container {
margin-top: 20px;
}
.el-table {
margin-top: 10px;
}
.el-table .cell {
white-space: normal;
word-break: break-word;
}
.dialog-toolbar {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.appempty {
margin-top: 20px;
}
::v-deep .el-table .cell {
padding: 0;
}
</style>

View File

@@ -0,0 +1,95 @@
import html2canvas from 'html2canvas';
import { PDFDocument } from 'pdf-lib';
async function generatePdf(domElement) {
const originalParent = domElement.parentNode;
const originalNext = domElement.nextSibling;
const paperWidthMm = 297;
const paperHeightMm = 210;
const wrapper = document.createElement('div');
wrapper.style.cssText = `position:fixed;left:-100000px;top:0;width:${paperWidthMm}mm;height:${paperHeightMm}mm;box-sizing:border-box;background-color:#ffffff;overflow:hidden;`;
wrapper.appendChild(domElement);
document.body.appendChild(wrapper);
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 300));
try {
const containerWidth = domElement.offsetWidth || domElement.scrollWidth;
const containerHeight = domElement.offsetHeight || domElement.scrollHeight;
const canvas = await html2canvas(domElement, {
scale: 2,
useCORS: true,
backgroundColor: '#ffffff',
logging: false,
width: containerWidth,
height: containerHeight,
windowWidth: containerWidth,
windowHeight: containerHeight
});
const pdfDoc = await PDFDocument.create();
const mmToPt = 72 / 25.4;
const pageWidthPt = paperWidthMm * mmToPt;
const pageHeightPt = paperHeightMm * mmToPt;
const page = pdfDoc.addPage([pageWidthPt, pageHeightPt]);
const pngImage = await pdfDoc.embedPng(canvas.toDataURL('image/png'));
const imgWidth = page.getWidth();
const imgHeight = (pngImage.height * imgWidth) / pngImage.width;
if (imgHeight > page.getHeight()) {
const scale = page.getHeight() / imgHeight;
page.drawImage(pngImage, {
x: (page.getWidth() - imgWidth * scale) / 2,
y: 0,
width: imgWidth * scale,
height: page.getHeight()
});
} else {
page.drawImage(pngImage, {
x: 0,
y: (page.getHeight() - imgHeight) / 2,
width: imgWidth,
height: imgHeight
});
}
const pdfBytes = await pdfDoc.save();
return pdfBytes;
} finally {
if (originalParent) {
if (originalNext) {
originalParent.insertBefore(domElement, originalNext);
} else {
originalParent.appendChild(domElement);
}
}
if (wrapper.parentNode) {
document.body.removeChild(wrapper);
}
}
}
export async function downloadPdf(domElement, fileName) {
const pdfBytes = await generatePdf(domElement);
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName || `certificate_${new Date().getTime()}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
export async function print(domElement) {
const pdfBytes = await generatePdf(domElement);
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const printWindow = window.open(url, '_blank');
}