2026-04-01 10:44:51 +08:00
|
|
|
|
<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;">
|
2026-05-16 14:20:30 +08:00
|
|
|
|
<div style="display: flex; align-items: center; gap: 4px; flex-wrap: wrap;">
|
2026-04-07 11:47:41 +08:00
|
|
|
|
<el-input v-model="queryParams.keyword" placeholder="请输入关键字" clearable
|
2026-05-16 14:20:30 +08:00
|
|
|
|
@keyup.enter.native="handleQuery" style="width: 160px;" />
|
|
|
|
|
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">筛选</el-button>
|
2026-04-02 16:49:07 +08:00
|
|
|
|
<el-button icon="el-icon-sort" size="mini" @click="toggleMoreFilter"
|
|
|
|
|
|
:type="showMoreFilter ? 'primary' : 'default'">
|
2026-04-01 10:44:51 +08:00
|
|
|
|
</el-button>
|
2026-04-02 16:49:07 +08:00
|
|
|
|
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="$emit('add')"></el-button>
|
2026-04-01 10:44:51 +08:00
|
|
|
|
</div>
|
2026-05-16 14:20:30 +08:00
|
|
|
|
<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>
|
2026-04-01 10:44:51 +08:00
|
|
|
|
</div>
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-04-01 10:44:51 +08:00
|
|
|
|
<!-- 更多筛选条件 -->
|
2026-04-02 16:49:07 +08:00
|
|
|
|
<div v-show="showMoreFilter" class="more-filter"
|
2026-05-16 14:20:30 +08:00
|
|
|
|
style="margin-top: 8px; padding-top: 10px; border-top: 1px dashed #e4e7ed;">
|
2026-04-01 10:44:51 +08:00
|
|
|
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="80px">
|
2026-04-07 11:47:41 +08:00
|
|
|
|
<el-form-item label="合同名称" prop="contractName">
|
|
|
|
|
|
<el-input v-model="queryParams.contractName" placeholder="请输入合同名称" clearable
|
|
|
|
|
|
@keyup.enter.native="handleQuery" />
|
|
|
|
|
|
</el-form-item>
|
2026-04-13 17:48:19 +08:00
|
|
|
|
<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
|
2026-04-02 16:49:07 +08:00
|
|
|
|
@keyup.enter.native="handleQuery" />
|
2026-04-01 10:44:51 +08:00
|
|
|
|
</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>
|
2026-04-16 15:50:54 +08:00
|
|
|
|
<el-form-item label="合同状态" prop="status">
|
|
|
|
|
|
<el-select v-model="queryParams.status" placeholder="请选择合同状态">
|
2026-04-01 10:44:51 +08:00
|
|
|
|
<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">
|
2026-04-02 16:49:07 +08:00
|
|
|
|
<div v-for="row in contractList" :key="row.contractId" class="list-item"
|
2026-05-05 17:42:14 +08:00
|
|
|
|
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>
|
2026-04-01 10:44:51 +08:00
|
|
|
|
<div style="font-weight: bold;">{{ row.contractName }}</div>
|
|
|
|
|
|
</div>
|
2026-05-05 17:42:14 +08:00
|
|
|
|
<div style="font-size: 12px; color: #606266;">{{ row.contractCode }}</div>
|
|
|
|
|
|
</div>
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-04-01 10:44:51 +08:00
|
|
|
|
<!-- 供方和需方 -->
|
|
|
|
|
|
<div style="font-size: 12px; color: #909399; margin-bottom: 6px;">
|
|
|
|
|
|
<span>供方: {{ row.supplier }}</span>
|
|
|
|
|
|
<span style="margin-left: 20px;">需方: {{ row.customer }}</span>
|
|
|
|
|
|
</div>
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-04-01 10:44:51 +08:00
|
|
|
|
<!-- 签订时间和交货日期 -->
|
|
|
|
|
|
<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>
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-04-01 10:44:51 +08:00
|
|
|
|
<!-- 签订地点和状态 -->
|
|
|
|
|
|
<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>
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-04-01 10:44:51 +08:00
|
|
|
|
<!-- 操作按钮独占一行 -->
|
2026-05-05 17:42:14 +08:00
|
|
|
|
<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>
|
2026-05-16 14:20:30 +08:00
|
|
|
|
<el-button size="mini" type="text" icon="el-icon-document" @click.stop="$emit('detailEdit', row)">详情编辑</el-button>
|
2026-05-05 17:42:14 +08:00
|
|
|
|
<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>
|
2026-04-01 10:44:51 +08:00
|
|
|
|
</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;" />
|
2026-06-02 08:56:31 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 导出预览对话框 -->
|
|
|
|
|
|
<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>
|
2026-04-01 10:44:51 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-04-13 17:48:19 +08:00
|
|
|
|
import { listOrder, updateOrder } from "@/api/crm/order";
|
2026-06-02 08:56:31 +08:00
|
|
|
|
import html2canvas from 'html2canvas';
|
|
|
|
|
|
import jsPDF from 'jspdf';
|
|
|
|
|
|
import contractLogo from '@/assets/images/contractLogo.png';
|
2026-05-11 10:38:29 +08:00
|
|
|
|
import {
|
|
|
|
|
|
parseProductContent,
|
|
|
|
|
|
convertToChinese
|
|
|
|
|
|
} from '@/utils/productContent';
|
2026-04-01 10:44:51 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: "ContractList",
|
2026-04-13 17:48:19 +08:00
|
|
|
|
dicts: ['wip_pack_saleman'],
|
2026-04-01 10:44:51 +08:00
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
// 合同信息表格数据
|
|
|
|
|
|
contractList: [],
|
|
|
|
|
|
// 遮罩层
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
// 总条数
|
|
|
|
|
|
total: 0,
|
|
|
|
|
|
// 选中的行
|
|
|
|
|
|
selectedRow: null,
|
|
|
|
|
|
// 是否显示更多筛选
|
|
|
|
|
|
showMoreFilter: false,
|
|
|
|
|
|
// 查询参数
|
|
|
|
|
|
queryParams: {
|
|
|
|
|
|
pageNum: 1,
|
|
|
|
|
|
pageSize: 10,
|
|
|
|
|
|
contractName: undefined,
|
2026-04-14 10:03:16 +08:00
|
|
|
|
contractCode: undefined,
|
2026-04-01 10:44:51 +08:00
|
|
|
|
supplier: undefined,
|
|
|
|
|
|
customer: undefined,
|
2026-05-16 14:20:30 +08:00
|
|
|
|
signDateStart: undefined,
|
|
|
|
|
|
signDateEnd: undefined,
|
|
|
|
|
|
deliveryDateStart: undefined,
|
|
|
|
|
|
deliveryDateEnd: undefined,
|
2026-04-01 10:44:51 +08:00
|
|
|
|
signLocation: undefined,
|
|
|
|
|
|
status: undefined,
|
|
|
|
|
|
},
|
2026-06-02 08:56:31 +08:00
|
|
|
|
// 导出预览
|
|
|
|
|
|
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 },
|
|
|
|
|
|
],
|
2026-04-01 10:44:51 +08:00
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
created() {
|
|
|
|
|
|
this.getList();
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
/** 查询合同信息列表 */
|
|
|
|
|
|
getList() {
|
|
|
|
|
|
this.loading = true;
|
2026-04-13 17:48:19 +08:00
|
|
|
|
listOrder(this.queryParams).then(response => {
|
2026-04-01 10:44:51 +08:00
|
|
|
|
this.contractList = response.rows;
|
|
|
|
|
|
this.total = response.total;
|
|
|
|
|
|
this.loading = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
2026-05-05 17:42:14 +08:00
|
|
|
|
/** 切换置顶状态 */
|
|
|
|
|
|
handleToggleTop(row) {
|
|
|
|
|
|
const newTopStatus = row.isTop ? 0 : 1;
|
|
|
|
|
|
updateOrder({ ...row, isTop: newTopStatus }).then(response => {
|
|
|
|
|
|
this.$message({
|
|
|
|
|
|
message: newTopStatus ? "置顶成功" : "取消置顶成功",
|
|
|
|
|
|
type: "success"
|
|
|
|
|
|
});
|
|
|
|
|
|
this.getList();
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
2026-04-01 10:44:51 +08:00
|
|
|
|
/** 状态变更 */
|
|
|
|
|
|
handleChangeStatus(row) {
|
2026-04-13 17:48:19 +08:00
|
|
|
|
updateOrder(row).then(response => {
|
2026-04-01 10:44:51 +08:00
|
|
|
|
this.$message({
|
|
|
|
|
|
message: "状态变更成功",
|
|
|
|
|
|
type: "success"
|
|
|
|
|
|
});
|
|
|
|
|
|
this.getList();
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 搜索按钮操作 */
|
|
|
|
|
|
handleQuery() {
|
|
|
|
|
|
this.queryParams.pageNum = 1;
|
|
|
|
|
|
this.getList();
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 重置按钮操作 */
|
|
|
|
|
|
resetQuery() {
|
|
|
|
|
|
this.$refs["queryForm"].resetFields();
|
2026-05-16 14:20:30 +08:00
|
|
|
|
this.queryParams.signDateStart = undefined;
|
|
|
|
|
|
this.queryParams.signDateEnd = undefined;
|
|
|
|
|
|
this.queryParams.deliveryDateStart = undefined;
|
|
|
|
|
|
this.queryParams.deliveryDateEnd = undefined;
|
2026-04-01 10:44:51 +08:00
|
|
|
|
this.handleQuery();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 行点击事件
|
|
|
|
|
|
handleRowClick(row) {
|
|
|
|
|
|
this.selectedRow = row;
|
|
|
|
|
|
this.$emit('rowClick', row);
|
|
|
|
|
|
},
|
|
|
|
|
|
// 切换更多筛选显示/隐藏
|
|
|
|
|
|
toggleMoreFilter() {
|
|
|
|
|
|
this.showMoreFilter = !this.showMoreFilter;
|
2026-04-02 16:49:07 +08:00
|
|
|
|
},
|
2026-06-02 08:56:31 +08:00
|
|
|
|
/** 导出合同 - 打开预览对话框 */
|
|
|
|
|
|
handleExport(row) {
|
|
|
|
|
|
this.exportRow = row;
|
|
|
|
|
|
this.exportDialogVisible = true;
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 全选/取消全选 */
|
|
|
|
|
|
handleSelectAllColumns(val) {
|
|
|
|
|
|
this.columnConfigs.forEach(col => { col.checked = val; });
|
|
|
|
|
|
this.columnIndeterminate = false;
|
|
|
|
|
|
this.generatePreviewHtml();
|
2026-04-21 09:38:42 +08:00
|
|
|
|
},
|
2026-06-02 08:56:31 +08:00
|
|
|
|
/** 单个列勾选变化 */
|
|
|
|
|
|
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>`;
|
2026-06-05 10:41:33 +08:00
|
|
|
|
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>`;
|
2026-06-02 08:56:31 +08:00
|
|
|
|
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>`;
|
2026-04-02 16:49:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-02 08:56:31 +08:00
|
|
|
|
// 合计行
|
|
|
|
|
|
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 = '';
|
2026-06-05 10:41:33 +08:00
|
|
|
|
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);
|
2026-06-02 08:56:31 +08:00
|
|
|
|
totalCells += `<td style="border:1px solid #000;padding:3px 4px;text-align:center;">${val}</td>`;
|
2026-04-02 16:49:07 +08:00
|
|
|
|
}
|
2026-06-02 08:56:31 +08:00
|
|
|
|
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>
|
2026-06-02 15:40:47 +08:00
|
|
|
|
<tr>${headerCells}</tr>
|
2026-06-02 08:56:31 +08:00
|
|
|
|
${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>
|
2026-06-02 15:40:47 +08:00
|
|
|
|
${productData.remark ? `<tr><td colspan="${colCount}" style="border:1px solid #000;padding:4px 6px;text-align:left;">备注:${productData.remark}</td></tr>` : ''}
|
2026-06-02 08:56:31 +08:00
|
|
|
|
</table>`;
|
|
|
|
|
|
return html;
|
|
|
|
|
|
},
|
|
|
|
|
|
/** 生成预览HTML并更新iframe */
|
|
|
|
|
|
generatePreviewHtml() {
|
|
|
|
|
|
const row = this.exportRow;
|
|
|
|
|
|
if (!row) return;
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-05-11 10:38:29 +08:00
|
|
|
|
let productData = parseProductContent(row.productContent);
|
|
|
|
|
|
if (!productData.productName && row.productName) {
|
|
|
|
|
|
productData.productName = row.productName;
|
2026-04-02 16:49:07 +08:00
|
|
|
|
}
|
2026-06-02 08:56:31 +08:00
|
|
|
|
const products = productData.products && productData.products.length > 0 ? productData.products : [];
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-06-02 08:56:31 +08:00
|
|
|
|
const productTableHtml = this.buildProductTableHtml(productData, products);
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-06-02 08:56:31 +08:00
|
|
|
|
// 解析合同内容
|
|
|
|
|
|
let contractContentHtml = '';
|
2026-04-02 16:49:07 +08:00
|
|
|
|
if (row.contractContent) {
|
|
|
|
|
|
let htmlContent = row.contractContent;
|
|
|
|
|
|
const pTagRegex = /<p[^>]*>([\s\S]*?)<\/p>/g;
|
|
|
|
|
|
let match;
|
|
|
|
|
|
const pContents = [];
|
|
|
|
|
|
while ((match = pTagRegex.exec(htmlContent)) !== null) {
|
2026-06-02 08:56:31 +08:00
|
|
|
|
let content = match[1].replace(/<[^>]*>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, "'").trim();
|
2026-04-02 16:49:07 +08:00
|
|
|
|
if (content) {
|
2026-04-03 11:30:19 +08:00
|
|
|
|
const chineseNumberRegex = /^[一二三四五六七八九十]+、/;
|
2026-06-02 08:56:31 +08:00
|
|
|
|
pContents.push(chineseNumberRegex.test(content) ? content : ' ' + content);
|
2026-04-02 16:49:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pContents.length === 0) {
|
2026-06-02 08:56:31 +08:00
|
|
|
|
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>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-07 11:47:41 +08:00
|
|
|
|
|
2026-06-02 08:56:31 +08:00
|
|
|
|
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; }
|
2026-06-02 15:40:47 +08:00
|
|
|
|
th { font-weight: bold; }
|
2026-06-02 08:56:31 +08:00
|
|
|
|
.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;">
|
2026-06-02 15:40:47 +08:00
|
|
|
|
<img src="${contractLogo}" style="position:absolute;left:40px;top:40px;height:80px;" crossorigin="anonymous" />
|
2026-06-02 08:56:31 +08:00
|
|
|
|
<div style="text-align:center;padding-top:10px;">
|
|
|
|
|
|
<div style="font-size:20px;font-weight:bold;letter-spacing:2px;">嘉祥科伦普重工有限公司</div>
|
|
|
|
|
|
</div>
|
2026-06-02 15:40:47 +08:00
|
|
|
|
<div style="position:relative;margin:12px 0 8px 0;text-align:center;">
|
2026-06-02 08:56:31 +08:00
|
|
|
|
<div style="font-size:22px;font-weight:bold;letter-spacing:6px;">产 品 销 售 合 同</div>
|
2026-06-02 15:40:47 +08:00
|
|
|
|
<div style="position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:11px;">合同编号:${row.contractCode || ''}</div>
|
2026-06-02 08:56:31 +08:00
|
|
|
|
</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);
|
2026-04-03 11:30:19 +08:00
|
|
|
|
}
|
2026-06-02 08:56:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
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>';
|
2026-04-02 16:49:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-07 11:47:41 +08:00
|
|
|
|
|
2026-06-02 08:56:31 +08:00
|
|
|
|
const totalTax = products.reduce((a, p) => a + (parseFloat(p.taxTotal) || 0), 0);
|
|
|
|
|
|
const amountWords = productData.totalAmountInWords || convertToChinese(totalTax) || '零元整';
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-06-02 08:56:31 +08:00
|
|
|
|
// 构建完整的合同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>
|
2026-06-02 15:40:47 +08:00
|
|
|
|
<div style="position:relative;margin:12px 0 8px 0;text-align:center;">
|
2026-06-02 08:56:31 +08:00
|
|
|
|
<div style="font-size:22px;font-weight:bold;letter-spacing:6px;">产 品 销 售 合 同</div>
|
2026-06-02 15:40:47 +08:00
|
|
|
|
<div style="position:absolute;right:0;top:50%;transform:translateY(-50%);font-size:11px;">合同编号:${row.contractCode || ''}</div>
|
2026-06-02 08:56:31 +08:00
|
|
|
|
</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
|
|
|
|
|
|
});
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-06-02 08:56:31 +08:00
|
|
|
|
document.body.removeChild(container);
|
|
|
|
|
|
|
2026-06-05 10:41:33 +08:00
|
|
|
|
const pdf = new jsPDF('p', 'mm', 'a4');
|
2026-06-02 08:56:31 +08:00
|
|
|
|
const pdfWidth = 210;
|
|
|
|
|
|
const pdfHeight = 297;
|
2026-06-05 10:41:33 +08:00
|
|
|
|
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;
|
2026-04-02 16:49:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 08:56:31 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-04-02 16:49:07 +08:00
|
|
|
|
},
|
2026-04-01 10:44:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
</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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-05 17:42:14 +08:00
|
|
|
|
.list-item-top {
|
|
|
|
|
|
background-color: #fff7e6;
|
|
|
|
|
|
border-left: 3px solid #e6a23c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.list-item-top:hover {
|
|
|
|
|
|
background-color: #ffebcc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 10:44:51 +08:00
|
|
|
|
@media screen and (max-width: 1200px) {
|
|
|
|
|
|
.list-header {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-04-01 10:44:51 +08:00
|
|
|
|
.list-item {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
2026-04-02 16:49:07 +08:00
|
|
|
|
|
2026-04-01 10:44:51 +08:00
|
|
|
|
.list-item .el-button {
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|