feat(order): add PDF export functionality and readonly order display

- Implemented PDF export feature using html2canvas and jspdf
- Added export button with loading state and disabled UI during export
- Created OrderInfoDisplay component for readonly order information
- Updated dependencies to include html2canvas and jspdf
- Modified webpack config to transpile html2canvas related packages
- Replaced OrderInfo component with OrderInfoDisplay in approval views
- Adjusted form labels and layout for better readability
- Fixed data loading state management
- Improved UI styling for PDF export mode
dev_1.0.0
chenhao 2025-11-21 09:04:13 +08:00
parent 7a07fae8f9
commit 99cc370025
7 changed files with 488 additions and 12 deletions

View File

@ -38,6 +38,8 @@
"jsencrypt": "3.0.0-rc.1", "jsencrypt": "3.0.0-rc.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"quill": "2.0.2", "quill": "2.0.2",
"html2canvas": "^1.4.1",
"jspdf": "^2.5.1",
"screenfull": "5.0.2", "screenfull": "5.0.2",
"sortablejs": "1.10.2", "sortablejs": "1.10.2",
"splitpanes": "2.4.1", "splitpanes": "2.4.1",

View File

@ -0,0 +1,364 @@
<template>
<el-row class="order-info-display">
<el-col :span="16">
<div class="col-item-border">
<el-form-item label="项目名称" prop="projectName">
<span>{{ displayValue(localOrderData.projectName) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="版本号" prop="versionCode">
<span>{{ displayValue(localOrderData.versionCode) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="项目编号" prop="projectCode">
<span>{{ displayValue(localOrderData.projectCode) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="16">
<div class="col-item-border">
<el-form-item label="最终客户" prop="customerName">
<span>{{ displayValue(localOrderData.customerName) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="BG" prop="bgProperty">
<span>{{ displayValue(getDictLabel(bgOptions, localOrderData.bgProperty)) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="行业" prop="industryType">
<span>{{ displayValue(getDictLabel(industryOptions, localOrderData.industryType)) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="代表处" prop="agentName">
<span>{{ displayValue(localOrderData.agentName) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="进货商接口人" prop="businessPerson">
<span>{{ displayValue(localOrderData.businessPerson) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="Email" prop="businessEmail">
<span>{{ displayValue(localOrderData.businessEmail) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="联系方式" prop="businessPhone">
<span>{{ displayValue(localOrderData.businessPhone) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="合同编号" prop="orderCode">
<span>{{ displayValue(localOrderData.orderCode) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="16">
<div class="col-item-border">
<el-form-item label="执行单截止时间" prop="orderEndTime">
<span>{{ displayValue(localOrderData.orderEndTime) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="币种" prop="currencyType">
<span>{{ displayValue(getDictLabel(currencyOptions, localOrderData.currencyType)) }}</span>
</el-form-item>
</div>
</el-col>
<el-col
v-if="(localOrderData.processTemplate=='1' ||(localOrderData.processTemplate!='1' &&( localOrderData.orderStatus=='1'||localOrderData.orderStatus=='2')))"
:span="8">
<div class="col-item-border">
<el-form-item label="总代进货金额" prop="actualPurchaseAmount">
<span>{{ displayValue(localOrderData.actualPurchaseAmount) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="(localOrderData.processTemplate=='1' ||(localOrderData.processTemplate!='1' &&( localOrderData.orderStatus=='1'||localOrderData.orderStatus=='2')))?8:16">
<div class="col-item-border">
<el-form-item label="总代出货金额" prop="shipmentAmount">
<span>{{ displayValue(localOrderData.shipmentAmount) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="要求到货时间" prop="deliveryTime">
<span>{{ displayValue(localOrderData.deliveryTime) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="16">
<div class="col-item-border">
<el-form-item label="公司直发" prop="companyDelivery">
<span>{{ displayValue(getDictLabel(companyDeliveryOptions, localOrderData.companyDelivery)) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="下单通路" prop="orderChannel">
<span>{{ displayValue(localOrderData.orderChannel === '1' ? '总代' : '直签') }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8" v-if="localOrderData.orderChannel == '1'">
<div class="col-item-border">
<el-form-item label="总代" prop="zd">
<span>广州佳都技术有限公司</span>
</el-form-item>
</div>
</el-col>
<el-col :span="localOrderData.orderChannel == '2' ? 16 : 8">
<div class="col-item-border">
<el-form-item label="供货商" prop="supplier">
<span>{{ displayValue(localOrderData.supplier) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="汇智责任人" prop="dutyName">
<span>{{ displayValue(localOrderData.dutyName) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="Email" prop="dutyEmail">
<span>{{ displayValue(localOrderData.dutyEmail) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="联系方式" prop="dutyPhone">
<span>{{ displayValue(localOrderData.dutyPhone) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="16">
<div class="col-item-border">
<el-form-item label="进货商" prop="partnerName">
<span>{{ displayValue(localOrderData.partnerName) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="进货商类型" prop="level">
<span>{{ displayValue(getDictLabel(partnerLevelOptions, localOrderData.level)) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="进货商联系人" prop="partnerUserName">
<span>{{ displayValue(localOrderData.partnerUserName) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="Email" prop="partnerEmail">
<span>{{ displayValue(localOrderData.partnerEmail) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="联系方式" prop="partnerPhone">
<span>{{ displayValue(localOrderData.partnerPhone) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="24">
<div class="col-item-border">
<el-form-item label="收货地址" prop="notifierAddress">
<span>{{ displayValue(localOrderData.notifierAddress) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="收货人" prop="notifier">
<span>{{ displayValue(localOrderData.notifier) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="Email" prop="notifierEmail">
<span>{{ displayValue(localOrderData.notifierEmail) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="联系方式" prop="notifierPhone">
<span>{{ displayValue(localOrderData.notifierPhone) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="付款方式" prop="paymentMethod">
<span>{{ displayValue(getPaymentMethodLabel(localOrderData.paymentMethod)) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="8">
<div class="col-item-border">
<el-form-item label="付款比例" prop="paymentRatio">
<span>{{ displayValue(localOrderData.paymentRatio, true) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="24">
<div class="col-item-border">
<el-form-item label="付款条件" prop="paymentDescription">
<span style="white-space: pre-wrap;">{{ displayValue(localOrderData.paymentDescription) }}</span>
</el-form-item>
</div>
</el-col>
<el-col :span="24">
<div class="col-item-border">
<el-form-item label="其他特别说明" prop="remark">
<span>{{ displayValue(localOrderData.remark) }}</span>
</el-form-item>
</div>
</el-col>
</el-row>
</template>
<script>
import { getDicts } from "@/api/system/dict/data";
export default {
name: "OrderInfoDisplay",
props: {
orderData: {
type: Object,
required: true
}
},
data() {
return {
localOrderData: {},
bgOptions: [],
industryOptions: [],
currencyOptions: [],
companyDeliveryOptions: [],
partnerLevelOptions: [],
paymentMethodOptions: [],
};
},
watch: {
orderData: {
handler(newValue) {
this.localOrderData = newValue;
if (newValue.bgProperty) {
this.fetchIndustryOptions(newValue.bgProperty);
}
if (newValue.orderChannel) {
this.updatePaymentMethodOptions(newValue.orderChannel);
}
},
immediate: true,
deep: true
}
},
created() {
this.getDicts("bg_type").then(response => { this.bgOptions = response.data; });
this.getDicts("currency_type").then(response => { this.currencyOptions = response.data; });
this.getDicts("company_delivery").then(response => { this.companyDeliveryOptions = response.data; });
this.getDicts("identify_level").then(response => { this.partnerLevelOptions = response.data; });
},
methods: {
getDicts,
fetchIndustryOptions(bgValue) {
const dictType = bgValue === 'YYS' ? 'bg_yys' : 'bg_hysy';
this.getDicts(dictType).then(response => {
this.industryOptions = response.data;
});
},
updatePaymentMethodOptions(channel) {
if (channel === '1') { //
this.paymentMethodOptions = [
{ label: '全款支付,无需预付款', value: '1-1' },
{ label: '全款支付,需单独备货生产,预付订单总货款百分比', value: '1-2' }
];
} else if (channel === '2') { //
this.paymentMethodOptions = [
{ label: '全款支付', value: '2-1' },
{ label: '全款支付,需单独备货生产,预付订单总货款百分比', value: '2-2' },
{ label: '商业汇票支付,预付订单总货款百分比', value: '2-3' }
];
} else {
this.paymentMethodOptions = [];
}
},
getDictLabel(options, value) {
if (!options || !value) return '';
const match = options.find(opt => opt.dictValue === value);
return match ? match.dictLabel : value;
},
getPaymentMethodLabel(value) {
if (!value) return '';
const match = this.paymentMethodOptions.find(opt => opt.value === value);
return match ? match.label : value;
},
displayValue(value, isPercentage = false) {
if (value === null || value === undefined || value === '') {
return '-';
}
return isPercentage ? `${value}%` : value;
}
}
};
</script>
<style scoped>
.order-info-display .el-form-item {
margin-bottom: 5px;
}
.order-info-display span {
color: #606266;
line-height: 36px;
}
.col-item-border {
border: 1px solid ;
padding: 3px;
margin-bottom: -1px; /* 消除边框重叠 */
margin-right: -1px; /* 消除边框重叠 */
}
.el-row {
margin-bottom: -1px; /* 消除el-col之间的间隙 */
}
</style>

View File

@ -1,17 +1,27 @@
<template> <template>
<div class="approve-container"> <div>
<ApproveLayout title="紫光汇智信息技术有限公司"> <div style="display: flex;flex-direction: row-reverse;">
<el-button
v-if="dataLoaded"
type="primary"
size="small"
icon="el-icon-download"
@click="exportPDF"
:loading="pdfExporting"
>导出PDF</el-button>
</div>
<div class="approve-container" :class="{ 'exporting-pdf': pdfExporting }">
<ApproveLayout ref="approveLayout" title="紫光汇智信息技术有限公司">
<template #default> <template #default>
<div style="display: flex;align-items: center;justify-content: center;"> <div style="display: flex;align-items: center;justify-content: center;">
<span id="projectNameBox" style="margin-left: 10px;color: black;font-size: 30px"> <span id="projectNameBox" style="margin-left: 10px;color: black;font-size: 30px">
紫光汇智信息技术有限公司进货供货订单 紫光汇智信息技术有限公司进货供货订单
</span> </span>
</div> </div>
<el-form ref="orderForm" :model="order" label-width="120px" class="mb20"> <el-form ref="orderForm" :model="order" label-width="120px" class="mb20">
<h3 class="section-title">订单信息</h3> <h3 class="section-title">订单信息</h3>
<order-info-view <order-info-display
:order-data.sync="order" :order-data.sync="order"
:is-readonly="true"
/> />
</el-form> </el-form>
@ -118,18 +128,23 @@
</span> </span>
</el-dialog> </el-dialog>
</div> </div>
</div>
</template> </template>
<script> <script>
import { approveOrder,getOrder } from "@/api/approve/order"; import { approveOrder,getOrder } from "@/api/approve/order";
import ConfigInfo from './ConfigInfo.vue'; import ConfigInfo from './ConfigInfo.vue';
import ApproveLayout from '@/views/approve/ApproveLayout.vue'; import ApproveLayout from '@/views/approve/ApproveLayout.vue';
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
import OrderInfoDisplay from '@/components/order/OrderInfoDisplay.vue';
export default { export default {
name: "Approve", name: "Approve",
components: { components: {
ApproveLayout, ApproveLayout,
OrderInfoView: () => import('@/components/order/OrderInfo.vue'), OrderInfoDisplay,
ConfigInfo ConfigInfo
}, },
props: { props: {
@ -150,6 +165,8 @@ export default {
dialogTitle: '', dialogTitle: '',
approveType: '', // 'approve' 'reject' approveType: '', // 'approve' 'reject'
submitLoading: false, // loading submitLoading: false, // loading
dataLoaded: false, //
pdfExporting: false, // PDF
opinionForm: { opinionForm: {
approveOpinion: '', approveOpinion: '',
}, },
@ -222,11 +239,15 @@ export default {
if (this.uniqueVersions.length > 0) { if (this.uniqueVersions.length > 0) {
this.activeVersionTab = String(this.uniqueVersions[0]); this.activeVersionTab = String(this.uniqueVersions[0]);
} }
//
this.dataLoaded = true;
}); });
}, },
// //
handleDiscountChange(value) { handleDiscountChange(value) {
this.selectedDiscount = value; this.selectedDiscount = value;
//todo
}, },
// //
handleTaxRateChange(product) { handleTaxRateChange(product) {
@ -443,6 +464,71 @@ export default {
downloadFile(file) { downloadFile(file) {
const url = `${process.env.VUE_APP_BASE_API}/project/order/file/download?filePath=${encodeURIComponent(file.filePath)}&fileName=${encodeURIComponent(file.fileName)}`; const url = `${process.env.VUE_APP_BASE_API}/project/order/file/download?filePath=${encodeURIComponent(file.filePath)}&fileName=${encodeURIComponent(file.fileName)}`;
window.location.href = url; window.location.href = url;
},
// PDF
async exportPDF() {
this.pdfExporting = true;
const disabledElements = [];
try {
// ApproveLayoutDOM
const element = this.$refs.approveLayout.$el;
// disabled 便PDF
element.querySelectorAll('input:disabled, textarea:disabled').forEach(el => {
disabledElements.push(el);
el.disabled = false;
});
// 使html2canvas
const canvas = await html2canvas(element, {
scale: 2, //
useCORS: true, //
logging: false, //
backgroundColor: '#F8F5F0' //
});
// PDF
const imgWidth = 210; // A4mm
const pageHeight = 297; // A4mm
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
// PDF
const pdf = new jsPDF('p', 'mm', 'a4');
let position = 0;
// canvas
const imgData = canvas.toDataURL('image/jpeg');
//
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
//
while (heightLeft > 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'JPEG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
//
const fileName = `${this.order.projectCode || '订单'}-${this.order.orderCode || ''}-Rev.${this.order.versionCode || '1'}.pdf`;
// PDF
pdf.save(fileName);
this.$modal.msgSuccess('PDF导出成功');
} catch (error) {
console.error('PDF导出失败:', error);
this.$modal.msgError('PDF导出失败请稍后重试');
} finally {
// disabled
disabledElements.forEach(el => {
el.disabled = true;
});
this.pdfExporting = false;
}
} }
}, },
}; };
@ -453,6 +539,22 @@ export default {
position: relative; position: relative;
} }
/* 导出PDF时的特殊样式 */
.approve-container.exporting-pdf ::v-deep .el-button--primary {
display: none;
}
.approve-container.exporting-pdf ::v-deep .el-input__inner,
.approve-container.exporting-pdf ::v-deep .el-textarea__inner {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
resize: none !important;
padding: 0 !important;
}
.approve-container.exporting-pdf ::v-deep .el-input__suffix {
display: none;
}
.section-title { .section-title {
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="合同编号" prop="orderCode"> <el-form-item label="合同编号" prop="orderCode">
<el-input <el-input
v-model="queryParams.orderCode" v-model="queryParams.orderCode"

View File

@ -134,7 +134,7 @@
<script> <script>
import { getOrder } from "@/api/project/order"; import { getOrder } from "@/api/project/order";
import ProductConfig from '@/views/project/info/ProductConfig.vue'; import ProductConfig from '@/views/project/info/ProductConfig.vue';
import OrderInfo from '@/components/order/OrderInfo.vue'; import OrderInfo from '@/components/order/OrderInfoDisplay.vue';
export default { export default {
name: "OrderDetailDrawer", name: "OrderDetailDrawer",

View File

@ -1,4 +1,4 @@
<template> <template xmlns="http://www.w3.org/1999/html">
<div class="app-container"> <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px"> <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="100px">
<el-form-item label="项目编号" prop="projectCode"> <el-form-item label="项目编号" prop="projectCode">
@ -73,6 +73,7 @@
<el-option label="已完结" value="1" /> <el-option label="已完结" value="1" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<br/>
<el-form-item label="时间选择" prop="timeType"> <el-form-item label="时间选择" prop="timeType">
<el-select v-model="queryParams.timeType" @change="changeTimeType"> <el-select v-model="queryParams.timeType" @change="changeTimeType">
<el-option label="到货时间" value="0" /> <el-option label="到货时间" value="0" />
@ -225,7 +226,7 @@ export default {
agentName: null, agentName: null,
dutyName: null, dutyName: null,
partnerName: null, partnerName: null,
financeStatus: '0', financeStatus: null,
timeType: '0', timeType: '0',
deliveryTimeStart: null, deliveryTimeStart: null,
deliveryTimeEnd: null, deliveryTimeEnd: null,

View File

@ -27,7 +27,14 @@ module.exports = {
assetsDir: 'static', assetsDir: 'static',
// 如果你不需要生产环境的 source map可以将其设置为 false 以加速生产环境构建。 // 如果你不需要生产环境的 source map可以将其设置为 false 以加速生产环境构建。
productionSourceMap: false, productionSourceMap: false,
transpileDependencies: ['quill'], transpileDependencies: [
'quill',
'html2canvas',
/html2canvas/, // 匹配所有 html2canvas 相关的包
'fast-png',
'iobuffer',
'pako'
],
// webpack-dev-server 相关配置 // webpack-dev-server 相关配置
devServer: { devServer: {
host: '0.0.0.0', host: '0.0.0.0',