725 lines
33 KiB
Vue
725 lines
33 KiB
Vue
<template>
|
||
<div class="contract-list">
|
||
<!-- 筛选区和按钮操作区合并 -->
|
||
<div class="filter-section" style="padding: 10px; border-bottom: 1px solid #e4e7ed;">
|
||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
|
||
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
|
||
<el-input v-model="queryParams.keyword" placeholder="请输入关键字" clearable
|
||
@keyup.enter.native="handleQuery" style="width: 160px;" />
|
||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">筛选</el-button>
|
||
<el-button icon="el-icon-sort" size="mini" @click="toggleMoreFilter"
|
||
:type="showMoreFilter ? 'primary' : 'default'">
|
||
</el-button>
|
||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="$emit('add')"></el-button>
|
||
</div>
|
||
<div style="font-size: 12px; color: #909399;">
|
||
共 <span style="color: #409eff; font-weight: bold;">{{ total }}</span> 条记录
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 日期范围筛选行 -->
|
||
<div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap; margin-bottom: 4px;">
|
||
<div style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: #606266;">
|
||
<span style="white-space: nowrap;">签订日期:</span>
|
||
<el-date-picker clearable v-model="queryParams.signDateStart" type="date" value-format="yyyy-MM-dd"
|
||
placeholder="签订日期开始" style="width: 180px;" />
|
||
<span>~</span>
|
||
<el-date-picker clearable v-model="queryParams.signDateEnd" type="date" value-format="yyyy-MM-dd"
|
||
placeholder="签订日期结束" style="width: 180px;" />
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: #606266;">
|
||
<span style="white-space: nowrap;">交货日期:</span>
|
||
<el-date-picker clearable v-model="queryParams.deliveryDateStart" type="date" value-format="yyyy-MM-dd"
|
||
placeholder="交货开始日期" style="width: 180px;" />
|
||
<span>~</span>
|
||
<el-date-picker clearable v-model="queryParams.deliveryDateEnd" type="date" value-format="yyyy-MM-dd"
|
||
placeholder="交货结束日期" style="width: 180px;" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 更多筛选条件 -->
|
||
<div v-show="showMoreFilter" class="more-filter"
|
||
style="margin-top: 8px; padding-top: 10px; border-top: 1px dashed #e4e7ed;">
|
||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
|
||
<el-form-item label="合同名称" prop="contractName">
|
||
<el-input v-model="queryParams.contractName" placeholder="请输入合同名称" clearable
|
||
@keyup.enter.native="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="销售员" prop="salesman">
|
||
<el-input v-model="queryParams.salesman" placeholder="请输入销售员" clearable
|
||
@keyup.enter.native="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="合同编号" prop="contractCode">
|
||
<el-input v-model="queryParams.contractCode" placeholder="请输入合同编号" clearable
|
||
@keyup.enter.native="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="供方" prop="supplier">
|
||
<el-input v-model="queryParams.supplier" placeholder="请输入供方" clearable @keyup.enter.native="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="需方" prop="customer">
|
||
<el-input v-model="queryParams.customer" placeholder="请输入需方" clearable @keyup.enter.native="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="签订地点" prop="signLocation">
|
||
<el-input v-model="queryParams.signLocation" placeholder="请输入签订地点" clearable
|
||
@keyup.enter.native="handleQuery" />
|
||
</el-form-item>
|
||
<el-form-item label="合同状态" prop="status">
|
||
<el-select v-model="queryParams.status" placeholder="请选择合同状态">
|
||
<el-option label="草稿" value="0" />
|
||
<el-option label="已生效" value="1" />
|
||
<el-option label="已作废" value="2" />
|
||
<el-option label="已完成" value="3" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="custom-list" v-loading="loading">
|
||
<div class="list-body">
|
||
<div v-for="row in contractList" :key="row.contractId" class="list-item"
|
||
style="padding: 10px; border-bottom: 2px solid #dddddd; cursor: pointer;"
|
||
:class="{ 'list-item-active': selectedRow === row, 'list-item-top': row.isTop }" @click="handleRowClick(row)">
|
||
<!-- 合同名称和编号 -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<el-tag v-if="row.isTop" type="warning" size="mini" effect="dark">置顶</el-tag>
|
||
<div style="font-weight: bold;">{{ row.contractName }}</div>
|
||
</div>
|
||
<div style="font-size: 12px; color: #606266;">{{ row.contractCode }}</div>
|
||
</div>
|
||
|
||
<!-- 供方和需方 -->
|
||
<div style="font-size: 12px; color: #909399; margin-bottom: 6px;">
|
||
<span>供方: {{ row.supplier }}</span>
|
||
<span style="margin-left: 20px;">需方: {{ row.customer }}</span>
|
||
</div>
|
||
|
||
<!-- 签订时间和交货日期 -->
|
||
<div style="font-size: 12px; color: #909399; margin-bottom: 6px;">
|
||
<span>签订时间: {{ parseTime(row.signTime, '{y}-{m}-{d}') }}</span>
|
||
<span style="margin-left: 20px;">交货日期: {{ parseTime(row.deliveryDate, '{y}-{m}-{d}') }}</span>
|
||
</div>
|
||
|
||
<!-- 签订地点和状态 -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||
<div style="font-size: 12px; color: #909399;">
|
||
签订地点: {{ row.signLocation }}
|
||
</div>
|
||
<el-tag
|
||
:type="row.status == 0 ? 'info' : row.status == 1 ? 'success' : row.status == 2 ? 'danger' : 'primary'"
|
||
size="small">
|
||
{{ row.status == 0 ? '草稿' : row.status == 1 ? '已生效' : row.status == 2 ? '已作废' : '已完成' }}
|
||
</el-tag>
|
||
</div>
|
||
|
||
<!-- 操作按钮独占一行 -->
|
||
<div style="display: flex; gap: 10px; padding-top: 8px; border-top: 1px dashed #f0f0f0;">
|
||
<el-button size="mini" type="text" :icon="row.isTop ? 'el-icon-top' : 'el-icon-s-operation'" @click.stop="handleToggleTop(row)">{{ row.isTop ? '取消置顶' : '置顶' }}</el-button>
|
||
<el-button size="mini" type="text" icon="el-icon-document" @click.stop="$emit('detailEdit', row)">详情编辑</el-button>
|
||
<el-button size="mini" type="text" icon="el-icon-download" @click.stop="handleExport(row)">导出</el-button>
|
||
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="$emit('update', row)">修改</el-button>
|
||
<el-button size="mini" type="text" icon="el-icon-delete" @click.stop="$emit('delete', row)">删除</el-button>
|
||
</div>
|
||
</div>
|
||
<div v-if="contractList.length === 0" style="padding: 40px; text-align: center; color: #909399;">
|
||
暂无合同数据
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||
@pagination="getList" style="padding: 10px; margin-bottom: 10px !important;" />
|
||
|
||
<!-- 导出预览对话框 -->
|
||
<el-dialog title="导出预览" :visible.sync="exportDialogVisible" width="95%" top="20px" append-to-body
|
||
:close-on-click-modal="false" @open="generatePreviewHtml">
|
||
<div style="display: flex; gap: 16px; height: calc(100vh - 200px);">
|
||
<!-- 左侧:列配置 -->
|
||
<div style="width: 200px; flex-shrink: 0; border: 1px solid #e4e7ed; border-radius: 4px; padding: 12px; overflow-y: auto;">
|
||
<div style="font-weight: bold; margin-bottom: 10px; font-size: 14px;">产品表列配置</div>
|
||
<el-checkbox v-model="selectAllColumns" :indeterminate="columnIndeterminate" @change="handleSelectAllColumns"
|
||
style="margin-bottom: 8px;">全选</el-checkbox>
|
||
<div v-for="col in columnConfigs" :key="col.key" style="margin-bottom: 6px;">
|
||
<el-checkbox v-model="col.checked" @change="onColumnChange">{{ col.label }}</el-checkbox>
|
||
</div>
|
||
<el-divider />
|
||
<div style="font-size: 12px; color: #909399;">勾选的列将显示在导出的产品表中</div>
|
||
</div>
|
||
<!-- 右侧:预览 -->
|
||
<div style="flex: 1; border: 1px solid #e4e7ed; border-radius: 4px; overflow: hidden; background: #f0f0f0;">
|
||
<iframe ref="previewFrame" style="width: 100%; height: 100%; border: none; background: #fff;"></iframe>
|
||
</div>
|
||
</div>
|
||
<span slot="footer">
|
||
<el-button @click="exportDialogVisible = false">取 消</el-button>
|
||
<el-button type="primary" :loading="exportLoading" @click="confirmExport">确认导出</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { listOrder, updateOrder } from "@/api/crm/order";
|
||
import html2canvas from 'html2canvas';
|
||
import jsPDF from 'jspdf';
|
||
import contractLogo from '@/assets/images/contractLogo.png';
|
||
import {
|
||
parseProductContent,
|
||
convertToChinese
|
||
} from '@/utils/productContent';
|
||
|
||
export default {
|
||
name: "ContractList",
|
||
dicts: ['wip_pack_saleman'],
|
||
data() {
|
||
return {
|
||
// 合同信息表格数据
|
||
contractList: [],
|
||
// 遮罩层
|
||
loading: false,
|
||
// 总条数
|
||
total: 0,
|
||
// 选中的行
|
||
selectedRow: null,
|
||
// 是否显示更多筛选
|
||
showMoreFilter: false,
|
||
// 查询参数
|
||
queryParams: {
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
contractName: undefined,
|
||
contractCode: undefined,
|
||
supplier: undefined,
|
||
customer: undefined,
|
||
signDateStart: undefined,
|
||
signDateEnd: undefined,
|
||
deliveryDateStart: undefined,
|
||
deliveryDateEnd: undefined,
|
||
signLocation: undefined,
|
||
status: undefined,
|
||
},
|
||
// 导出预览
|
||
exportDialogVisible: false,
|
||
exportLoading: false,
|
||
exportRow: null,
|
||
selectAllColumns: true,
|
||
columnIndeterminate: false,
|
||
columnConfigs: [
|
||
{ key: 'spec', label: '规格(mm)', checked: true },
|
||
{ key: 'material', label: '材质', checked: true },
|
||
{ key: 'quantity', label: '数量(吨)', checked: true },
|
||
{ key: 'taxPrice', label: '含税单价(元/吨)', checked: true },
|
||
{ key: 'taxDivisor', label: '税率除数', checked: true },
|
||
{ key: 'noTaxPrice', label: '无税单价(元/吨)', checked: true },
|
||
{ key: 'taxTotal', label: '含税总额(元)', checked: true },
|
||
{ key: 'noTaxTotal', label: '无税总额(元)', checked: true },
|
||
{ key: 'taxAmount', label: '税额(元)', checked: true },
|
||
{ key: 'remark', label: '备注', checked: true },
|
||
],
|
||
};
|
||
},
|
||
created() {
|
||
this.getList();
|
||
},
|
||
methods: {
|
||
/** 查询合同信息列表 */
|
||
getList() {
|
||
this.loading = true;
|
||
listOrder(this.queryParams).then(response => {
|
||
this.contractList = response.rows;
|
||
this.total = response.total;
|
||
this.loading = false;
|
||
});
|
||
},
|
||
/** 切换置顶状态 */
|
||
handleToggleTop(row) {
|
||
const newTopStatus = row.isTop ? 0 : 1;
|
||
updateOrder({ ...row, isTop: newTopStatus }).then(response => {
|
||
this.$message({
|
||
message: newTopStatus ? "置顶成功" : "取消置顶成功",
|
||
type: "success"
|
||
});
|
||
this.getList();
|
||
})
|
||
},
|
||
/** 状态变更 */
|
||
handleChangeStatus(row) {
|
||
updateOrder(row).then(response => {
|
||
this.$message({
|
||
message: "状态变更成功",
|
||
type: "success"
|
||
});
|
||
this.getList();
|
||
})
|
||
},
|
||
/** 搜索按钮操作 */
|
||
handleQuery() {
|
||
this.queryParams.pageNum = 1;
|
||
this.getList();
|
||
},
|
||
/** 重置按钮操作 */
|
||
resetQuery() {
|
||
this.$refs["queryForm"].resetFields();
|
||
this.queryParams.signDateStart = undefined;
|
||
this.queryParams.signDateEnd = undefined;
|
||
this.queryParams.deliveryDateStart = undefined;
|
||
this.queryParams.deliveryDateEnd = undefined;
|
||
this.handleQuery();
|
||
},
|
||
|
||
// 行点击事件
|
||
handleRowClick(row) {
|
||
this.selectedRow = row;
|
||
this.$emit('rowClick', row);
|
||
},
|
||
// 切换更多筛选显示/隐藏
|
||
toggleMoreFilter() {
|
||
this.showMoreFilter = !this.showMoreFilter;
|
||
},
|
||
/** 导出合同 - 打开预览对话框 */
|
||
handleExport(row) {
|
||
this.exportRow = row;
|
||
this.exportDialogVisible = true;
|
||
},
|
||
/** 全选/取消全选 */
|
||
handleSelectAllColumns(val) {
|
||
this.columnConfigs.forEach(col => { col.checked = val; });
|
||
this.columnIndeterminate = false;
|
||
this.generatePreviewHtml();
|
||
},
|
||
/** 单个列勾选变化 */
|
||
onColumnChange() {
|
||
const checkedCount = this.columnConfigs.filter(c => c.checked).length;
|
||
this.selectAllColumns = checkedCount === this.columnConfigs.length;
|
||
this.columnIndeterminate = checkedCount > 0 && checkedCount < this.columnConfigs.length;
|
||
this.generatePreviewHtml();
|
||
},
|
||
/** 根据列配置生成产品表HTMl */
|
||
buildProductTableHtml(productData, products) {
|
||
const activeCols = this.columnConfigs.filter(c => c.checked);
|
||
const hasCol = (key) => activeCols.some(c => c.key === key);
|
||
|
||
// 表头
|
||
let headerCells = '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:30px;">序号</th>';
|
||
if (hasCol('spec')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:80px;">规格(mm)</th>';
|
||
if (hasCol('material')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:60px;">材质</th>';
|
||
if (hasCol('quantity')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:55px;">数量(吨)</th>';
|
||
if (hasCol('taxPrice')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:70px;">含税单价(元/吨)</th>';
|
||
if (hasCol('taxDivisor')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:45px;">税率除数</th>';
|
||
if (hasCol('noTaxPrice')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:70px;">无税单价(元/吨)</th>';
|
||
if (hasCol('taxTotal')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:70px;">含税总额(元)</th>';
|
||
if (hasCol('noTaxTotal')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:70px;">无税总额(元)</th>';
|
||
if (hasCol('taxAmount')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:55px;">税额(元)</th>';
|
||
if (hasCol('remark')) headerCells += '<th style="border:1px solid #000;padding:4px 4px;font-weight:bold;width:80px;">备注</th>';
|
||
const colCount = activeCols.length + 1; // +1 for 序号
|
||
|
||
// 数据行
|
||
let bodyRows = '';
|
||
products.forEach((product, index) => {
|
||
let cells = `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${index + 1}</td>`;
|
||
if (hasCol('spec')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.spec || ''}</td>`;
|
||
if (hasCol('material')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.material || ''}</td>`;
|
||
if (hasCol('quantity')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.quantity || ''}</td>`;
|
||
if (hasCol('taxPrice')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.taxPrice || ''}</td>`;
|
||
if (hasCol('taxDivisor')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${product.taxDivisor || '1.13'}</td>`;
|
||
if (hasCol('noTaxPrice')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.noTaxPrice || 0).toFixed(3)}</td>`;
|
||
if (hasCol('taxTotal')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.taxTotal || 0).toFixed(3)}</td>`;
|
||
if (hasCol('noTaxTotal')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.noTaxTotal || 0).toFixed(3)}</td>`;
|
||
if (hasCol('taxAmount')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${(product.taxAmount || 0).toFixed(3)}</td>`;
|
||
if (hasCol('remark')) cells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;max-width:100px;word-wrap:break-word;">${product.remark || ''}</td>`;
|
||
bodyRows += `<tr>${cells}</tr>`;
|
||
});
|
||
|
||
// 合计行
|
||
const totalQty = products.reduce((a, p) => a + (parseFloat(p.quantity) || 0), 0);
|
||
const totalTax = products.reduce((a, p) => a + (parseFloat(p.taxTotal) || 0), 0);
|
||
const totalNoTax = products.reduce((a, p) => a + (parseFloat(p.noTaxTotal) || 0), 0);
|
||
const totalTaxAmt = products.reduce((a, p) => a + (parseFloat(p.taxAmount) || 0), 0);
|
||
const totalAmountInWords = productData.totalAmountInWords || convertToChinese(totalTax) || '零元整';
|
||
|
||
// 合计行单元格(按列顺序填充)
|
||
let totalCells = '';
|
||
let cellIdx = 0;
|
||
activeCols.forEach((col) => {
|
||
if (cellIdx < 2) {
|
||
// 前 2 个活动列(spec、material)与序号一起被"合计" colspan=3 覆盖
|
||
if (cellIdx === 0) {
|
||
totalCells += `<td colspan="3" style="border:1px solid #000;padding:4px 4px;font-weight:bold;text-align:center;">合 计</td>`;
|
||
}
|
||
} else {
|
||
let val = '';
|
||
if (col.key === 'quantity') val = totalQty.toFixed(3);
|
||
else if (col.key === 'taxTotal') val = totalTax.toFixed(3);
|
||
else if (col.key === 'noTaxTotal') val = totalNoTax.toFixed(3);
|
||
else if (col.key === 'taxAmount') val = totalTaxAmt.toFixed(3);
|
||
totalCells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${val}</td>`;
|
||
}
|
||
cellIdx++;
|
||
});
|
||
|
||
const html = `
|
||
<table style="width:100%;border-collapse:collapse;font-size:11px;margin-bottom:6px;">
|
||
<tr>
|
||
<td colspan="4" style="border:1px solid #000;padding:3px 6px;font-weight:bold;text-align:left;">产品名称:${productData.productName || ''}</td>
|
||
<td colspan="${colCount - 4 > 0 ? colCount - 4 : 1}" style="border:1px solid #000;padding:3px 6px;font-weight:bold;text-align:left;">生产厂家:嘉祥科伦普重工有限公司</td>
|
||
</tr>
|
||
<tr>${headerCells}</tr>
|
||
${bodyRows}
|
||
<tr>${totalCells}</tr>
|
||
<tr>
|
||
<td colspan="${colCount}" style="border:1px solid #000;padding:4px 6px;font-weight:bold;text-align:left;">合计人民币(大写):${totalAmountInWords}</td>
|
||
</tr>
|
||
${productData.remark ? `<tr><td colspan="${colCount}" style="border:1px solid #000;padding:4px 6px;text-align:left;">备注:${productData.remark}</td></tr>` : ''}
|
||
</table>`;
|
||
return html;
|
||
},
|
||
/** 生成预览HTML并更新iframe */
|
||
generatePreviewHtml() {
|
||
const row = this.exportRow;
|
||
if (!row) return;
|
||
|
||
let productData = parseProductContent(row.productContent);
|
||
if (!productData.productName && row.productName) {
|
||
productData.productName = row.productName;
|
||
}
|
||
const products = productData.products && productData.products.length > 0 ? productData.products : [];
|
||
|
||
const productTableHtml = this.buildProductTableHtml(productData, products);
|
||
|
||
// 解析合同内容
|
||
let contractContentHtml = '';
|
||
if (row.contractContent) {
|
||
let htmlContent = row.contractContent;
|
||
const pTagRegex = /<p[^>]*>([\s\S]*?)<\/p>/g;
|
||
let match;
|
||
const pContents = [];
|
||
while ((match = pTagRegex.exec(htmlContent)) !== null) {
|
||
let content = match[1].replace(/<[^>]*>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").trim();
|
||
if (content) {
|
||
const chineseNumberRegex = /^[一二三四五六七八九十]+、/;
|
||
pContents.push(chineseNumberRegex.test(content) ? content : ' ' + content);
|
||
}
|
||
}
|
||
if (pContents.length === 0) {
|
||
const textContent = htmlContent.replace(/<[^>]*>/g, '').replace(/ /g, ' ').trim();
|
||
if (textContent) pContents.push(textContent);
|
||
}
|
||
if (pContents.length > 0) {
|
||
contractContentHtml = '<div style="margin-top:8px;font-size:12px;line-height:1.8;">' + pContents.join('<br/>') + '</div>';
|
||
}
|
||
}
|
||
|
||
const totalTax = products.reduce((a, p) => a + (parseFloat(p.taxTotal) || 0), 0);
|
||
const amountWords = productData.totalAmountInWords || convertToChinese(totalTax) || '零元整';
|
||
|
||
const fullHtml = `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="utf-8"><title>合同预览</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body { font-family: 'SimSun','宋体',serif; color: #000; background: #e8e8e8; font-size: 12px; line-height: 1.6; display: flex; flex-direction: column; align-items: center; padding: 20px 0; }
|
||
.a4-page { width: 794px; min-height: 1123px; padding: 30px 40px; background: #fff; margin-bottom: 20px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); }
|
||
.company-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 6px; }
|
||
.contract-title { text-align: center; font-size: 20px; font-weight: bold; letter-spacing: 6px; margin-bottom: 14px; }
|
||
.info-row { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 13px; line-height: 2; }
|
||
.section-title { font-size: 13px; font-weight: bold; margin-bottom: 4px; margin-top: 8px; }
|
||
table { width: 100%; border-collapse: collapse; }
|
||
th, td { border: 1px solid #000; padding: 3px 4px; text-align: center; font-size: 11px; }
|
||
th { font-weight: bold; }
|
||
.sign-section { margin-top: 24px; font-size: 12px; line-height: 2.2; }
|
||
.sign-section .col { width: 48%; }
|
||
.sign-row { display: flex; justify-content: space-between; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="a4-page" style="position:relative;">
|
||
<img src="${contractLogo}" style="position:absolute;left:40px;top:40px;height:80px;" crossorigin="anonymous" />
|
||
<div style="text-align:center;padding-top:10px;">
|
||
<div style="font-size:20px;font-weight:bold;letter-spacing:2px;">嘉祥科伦普重工有限公司</div>
|
||
</div>
|
||
<div style="position:relative;margin:12px 0 8px 0;text-align:center;">
|
||
<div style="font-size:22px;font-weight:bold;letter-spacing:6px;">产 品 销 售 合 同</div>
|
||
<div style="position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:11px;">合同编号:${row.contractCode || ''}</div>
|
||
</div>
|
||
<div class="info-row">
|
||
<div style="width:55%;">
|
||
<div>供方(甲方):${row.supplier || ''}</div>
|
||
<div>需方(乙方):${row.customer || ''}</div>
|
||
</div>
|
||
<div style="width:40%;">
|
||
<div>签订时间:${row.signTime ? this.parseTime(row.signTime, '{y}年{m}月{d}日') : ''}</div>
|
||
<div>签订地点:${row.signLocation || ''}</div>
|
||
</div>
|
||
</div>
|
||
<div class="section-title">一、产品内容</div>
|
||
${productTableHtml}
|
||
${contractContentHtml}
|
||
<div class="sign-section">
|
||
<div class="sign-row">
|
||
<div class="col">
|
||
<div>供方(甲方):${row.supplier || ''}</div>
|
||
<div>地址:${row.supplierAddress || ''}</div>
|
||
<div>电话:${row.supplierPhone || ''}</div>
|
||
<div>开户行:${row.supplierBank || ''}</div>
|
||
<div>账号:${row.supplierAccount || ''}</div>
|
||
<div>税号:${row.supplierTaxNo || ''}</div>
|
||
</div>
|
||
<div class="col">
|
||
<div>需方(乙方):${row.customer || ''}</div>
|
||
<div>地址:${row.customerAddress || ''}</div>
|
||
<div>电话:${row.customerPhone || ''}</div>
|
||
<div>开户行:${row.customerBank || ''}</div>
|
||
<div>账号:${row.customerAccount || ''}</div>
|
||
<div>税号:${row.customerTaxNo || ''}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
|
||
this.$nextTick(() => {
|
||
const iframe = this.$refs.previewFrame;
|
||
if (iframe) {
|
||
iframe.srcdoc = fullHtml;
|
||
}
|
||
});
|
||
},
|
||
/** 确认导出PDF */
|
||
async confirmExport() {
|
||
const row = this.exportRow;
|
||
if (!row) return;
|
||
|
||
this.exportLoading = true;
|
||
const loading = this.$loading({
|
||
lock: true,
|
||
text: '正在生成PDF,请稍候...',
|
||
background: 'rgba(0, 0, 0, 0.7)'
|
||
});
|
||
|
||
try {
|
||
let productData = parseProductContent(row.productContent);
|
||
if (!productData.productName && row.productName) {
|
||
productData.productName = row.productName;
|
||
}
|
||
const products = productData.products && productData.products.length > 0 ? productData.products : [];
|
||
|
||
const productTableHtml = this.buildProductTableHtml(productData, products);
|
||
|
||
// 解析合同内容
|
||
let contractContentHtml = '';
|
||
if (row.contractContent) {
|
||
let htmlContent = row.contractContent;
|
||
const pTagRegex = /<p[^>]*>([\s\S]*?)<\/p>/g;
|
||
let match;
|
||
const pContents = [];
|
||
while ((match = pTagRegex.exec(htmlContent)) !== null) {
|
||
let content = match[1].replace(/<[^>]*>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").trim();
|
||
if (content) {
|
||
const chineseNumberRegex = /^[一二三四五六七八九十]+、/;
|
||
pContents.push(chineseNumberRegex.test(content) ? content : ' ' + content);
|
||
}
|
||
}
|
||
if (pContents.length === 0) {
|
||
const textContent = htmlContent.replace(/<[^>]*>/g, '').replace(/ /g, ' ').trim();
|
||
if (textContent) pContents.push(textContent);
|
||
}
|
||
if (pContents.length > 0) {
|
||
contractContentHtml = '<div style="margin-top:10px;font-size:12px;line-height:1.8;">' + pContents.join('<br/>') + '</div>';
|
||
}
|
||
}
|
||
|
||
const totalTax = products.reduce((a, p) => a + (parseFloat(p.taxTotal) || 0), 0);
|
||
const amountWords = productData.totalAmountInWords || convertToChinese(totalTax) || '零元整';
|
||
|
||
// 构建完整的合同HTML(794px 宽,适合A4)
|
||
const contractHtml = `
|
||
<div id="contract-pdf-content" style="width:794px;padding:30px 40px;font-family:'SimSun','宋体',serif;color:#000;background:#fff;box-sizing:border-box;position:relative;">
|
||
<img src="${contractLogo}" style="position:absolute;left:20px;top:20px;height:80px;" crossorigin="anonymous" />
|
||
<div style="text-align:center;padding-top:10px;">
|
||
<div style="font-size:20px;font-weight:bold;letter-spacing:2px;">嘉祥科伦普重工有限公司</div>
|
||
</div>
|
||
<div style="position:relative;margin:12px 0 8px 0;text-align:center;">
|
||
<div style="font-size:22px;font-weight:bold;letter-spacing:6px;">产 品 销 售 合 同</div>
|
||
<div style="position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:11px;">合同编号:${row.contractCode || ''}</div>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:8px;font-size:13px;line-height:2;">
|
||
<div style="width:55%;">
|
||
<div>供方(甲方):${row.supplier || ''}</div>
|
||
<div>需方(乙方):${row.customer || ''}</div>
|
||
</div>
|
||
<div style="width:40%;text-align:left;">
|
||
<div>签订时间:${row.signTime ? this.parseTime(row.signTime, '{y}年{m}月{d}日') : ''}</div>
|
||
<div>签订地点:${row.signLocation || ''}</div>
|
||
</div>
|
||
</div>
|
||
<div style="font-size:13px;font-weight:bold;margin-bottom:5px;">一、产品内容</div>
|
||
${productTableHtml}
|
||
${contractContentHtml}
|
||
<div style="margin-top:40px;font-size:12px;line-height:2.2;">
|
||
<div style="display:flex;justify-content:space-between;">
|
||
<div style="width:48%;">
|
||
<div>供方(甲方):${row.supplier || ''}</div>
|
||
<div>地址:${row.supplierAddress || ''}</div>
|
||
<div>电话:${row.supplierPhone || ''}</div>
|
||
<div>开户行:${row.supplierBank || ''}</div>
|
||
<div>账号:${row.supplierAccount || ''}</div>
|
||
<div>税号:${row.supplierTaxNo || ''}</div>
|
||
</div>
|
||
<div style="width:48%;">
|
||
<div>需方(乙方):${row.customer || ''}</div>
|
||
<div>地址:${row.customerAddress || ''}</div>
|
||
<div>电话:${row.customerPhone || ''}</div>
|
||
<div>开户行:${row.customerBank || ''}</div>
|
||
<div>账号:${row.customerAccount || ''}</div>
|
||
<div>税号:${row.customerTaxNo || ''}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const container = document.createElement('div');
|
||
container.style.position = 'fixed';
|
||
container.style.left = '-9999px';
|
||
container.style.top = '0';
|
||
container.innerHTML = contractHtml;
|
||
document.body.appendChild(container);
|
||
|
||
const element = document.getElementById('contract-pdf-content');
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
||
const canvas = await html2canvas(element, {
|
||
scale: 2,
|
||
useCORS: true,
|
||
allowTaint: true,
|
||
logging: false,
|
||
backgroundColor: '#ffffff',
|
||
imageTimeout: 5000
|
||
});
|
||
|
||
document.body.removeChild(container);
|
||
|
||
const pdf = new jsPDF('p', 'mm', 'a4');
|
||
const pdfWidth = 210;
|
||
const pdfHeight = 297;
|
||
const margin = 5;
|
||
const contentWidth = pdfWidth - margin * 2;
|
||
const contentHeight = pdfHeight - margin * 2;
|
||
const scale = contentWidth / canvas.width;
|
||
|
||
function isBlankRow(canvas, y) {
|
||
const ctx = canvas.getContext('2d');
|
||
const row = ctx.getImageData(0, y, canvas.width, 1).data;
|
||
let whiteCount = 0;
|
||
const total = row.length / 4;
|
||
for (let i = 0; i < row.length; i += 4) {
|
||
if (row[i] > 250 && row[i + 1] > 250 && row[i + 2] > 250) whiteCount++;
|
||
}
|
||
return whiteCount / total >= 0.95;
|
||
}
|
||
|
||
function findBreakY(canvas, targetY, range) {
|
||
range = range || 40;
|
||
for (let offset = 0; offset < range; offset++) {
|
||
if (targetY + offset < canvas.height && isBlankRow(canvas, targetY + offset)) return targetY + offset;
|
||
if (targetY - offset > 0 && isBlankRow(canvas, targetY - offset)) return targetY - offset;
|
||
}
|
||
return targetY;
|
||
}
|
||
|
||
let currentY = 0;
|
||
let firstPage = true;
|
||
|
||
while (currentY < canvas.height) {
|
||
const pageHeightPx = Math.round(contentHeight / scale);
|
||
let endY = Math.min(currentY + pageHeightPx, canvas.height);
|
||
|
||
if (endY < canvas.height) {
|
||
endY = findBreakY(canvas, endY);
|
||
}
|
||
|
||
const sliceHeight = endY - currentY;
|
||
const pageCanvas = document.createElement('canvas');
|
||
pageCanvas.width = canvas.width;
|
||
pageCanvas.height = sliceHeight;
|
||
pageCanvas.getContext('2d').drawImage(canvas, 0, currentY, canvas.width, sliceHeight, 0, 0, canvas.width, sliceHeight);
|
||
|
||
const imgData = pageCanvas.toDataURL('image/jpeg', 0.95);
|
||
const pageImgHeight = sliceHeight * scale;
|
||
|
||
if (firstPage) {
|
||
pdf.addImage(imgData, 'JPEG', margin, margin, contentWidth, pageImgHeight);
|
||
firstPage = false;
|
||
} else {
|
||
pdf.addPage();
|
||
pdf.addImage(imgData, 'JPEG', margin, margin, contentWidth, pageImgHeight);
|
||
}
|
||
|
||
currentY = endY;
|
||
}
|
||
|
||
pdf.save(`合同_${row.contractCode || row.contractName || '未命名'}.pdf`);
|
||
this.$message.success('PDF导出成功');
|
||
this.exportDialogVisible = false;
|
||
} catch (error) {
|
||
console.error('PDF导出失败:', error);
|
||
this.$message.error('PDF导出失败,请重试');
|
||
} finally {
|
||
loading.close();
|
||
this.exportLoading = false;
|
||
}
|
||
},
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.contract-list {
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.custom-list {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.list-item:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.list-item-active {
|
||
background-color: #ecf5ff;
|
||
border-left: 3px solid #409eff;
|
||
}
|
||
|
||
.list-item-top {
|
||
background-color: #fff7e6;
|
||
border-left: 3px solid #e6a23c;
|
||
}
|
||
|
||
.list-item-top:hover {
|
||
background-color: #ffebcc;
|
||
}
|
||
|
||
@media screen and (max-width: 1200px) {
|
||
.list-header {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.list-item {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.list-item .el-button {
|
||
font-size: 10px;
|
||
}
|
||
}
|
||
</style> |