|
|
@@ -1146,6 +1146,25 @@
|
|
|
|
|
|
<!-- 客户详情抽屉 -->
|
|
|
<CustomerDetail ref="customerDetailRef" />
|
|
|
+
|
|
|
+ <!-- 生成合同对话框 -->
|
|
|
+ <el-dialog
|
|
|
+ :title="isPreviewMode ? '预览合同' : '编辑合同'"
|
|
|
+ v-model="generateContractDialog.visible"
|
|
|
+ width="1200px"
|
|
|
+ append-to-body
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ >
|
|
|
+ <TinyMceEditor v-model="contractContent" :height="600" :disabled="isPreviewMode" :toolbar="!isPreviewMode" />
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="generateContractDialog.visible = false">关 闭</el-button>
|
|
|
+ <el-button type="info" icon="View" @click="togglePreviewMode">{{ isPreviewMode ? '编辑模式' : '预览模式' }}</el-button>
|
|
|
+ <el-button type="success" icon="Document" @click="handleGeneratePdfFromDialog">生成PDF</el-button>
|
|
|
+ <el-button type="primary" icon="Printer" @click="handlePrintContract">打印</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -1162,7 +1181,6 @@ import {
|
|
|
executeContract,
|
|
|
completeContract,
|
|
|
invalidContract,
|
|
|
- generatePdf,
|
|
|
listContractApproval,
|
|
|
type ContractQuery,
|
|
|
type ContractVO
|
|
|
@@ -1176,8 +1194,11 @@ import InvalidDialog from './components/InvalidDialog.vue';
|
|
|
import UploadDialog from './components/UploadDialog.vue';
|
|
|
import CustomerDetail from '@/views/crm/customer/detail.vue';
|
|
|
import ImagePreview from '@/components/ImagePreview/index.vue';
|
|
|
+import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
|
|
|
import { CircleCheck, CircleClose, ChatLineSquare, Clock, InfoFilled, VideoPlay, Document, Paperclip, Download } from '@element-plus/icons-vue';
|
|
|
import request from '@/utils/request';
|
|
|
+import html2canvas from 'html2canvas';
|
|
|
+import jsPDF from 'jspdf';
|
|
|
|
|
|
const router = useRouter();
|
|
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
|
|
@@ -1248,6 +1269,14 @@ const detailApprovalList = ref<any[]>([]);
|
|
|
const detailContractFileList = ref<any[]>([]);
|
|
|
const detailAttachmentFileList = ref<any[]>([]);
|
|
|
|
|
|
+// 打印相关
|
|
|
+const generateContractDialog = reactive({
|
|
|
+ visible: false
|
|
|
+});
|
|
|
+const contractContent = ref('');
|
|
|
+const isPreviewMode = ref(false);
|
|
|
+const pdfGenerating = ref(false);
|
|
|
+
|
|
|
/** 查询列表 */
|
|
|
const getList = async () => {
|
|
|
loading.value = true;
|
|
|
@@ -1453,14 +1482,80 @@ const handleUpload = (row: ContractVO) => {
|
|
|
uploadDialogRef.value?.open(row.id);
|
|
|
};
|
|
|
|
|
|
-/** 生成PDF */
|
|
|
+/** 生成PDF(前端实现) */
|
|
|
const handleGeneratePdf = async (row: ContractVO) => {
|
|
|
+ pdfGenerating.value = true;
|
|
|
try {
|
|
|
- const pdfUrl = await generatePdf(row.id);
|
|
|
+ proxy?.$modal.loading('正在生成PDF...');
|
|
|
+
|
|
|
+ // 1. 获取合同详情
|
|
|
+ const res = await getContract(row.id);
|
|
|
+ const contract = res.data;
|
|
|
+
|
|
|
+ // 2. 生成HTML内容
|
|
|
+ const htmlContent = generateContractTemplate(contract);
|
|
|
+
|
|
|
+ // 3. 创建临时容器
|
|
|
+ const tempDiv = document.createElement('div');
|
|
|
+ tempDiv.style.position = 'absolute';
|
|
|
+ tempDiv.style.left = '-9999px';
|
|
|
+ tempDiv.style.top = '0';
|
|
|
+ tempDiv.style.width = '794px'; // A4纸宽度
|
|
|
+ tempDiv.style.background = 'white';
|
|
|
+ tempDiv.innerHTML = htmlContent;
|
|
|
+ document.body.appendChild(tempDiv);
|
|
|
+
|
|
|
+ // 4. 使用html2canvas转换为canvas
|
|
|
+ const canvas = await html2canvas(tempDiv, {
|
|
|
+ scale: 2, // 提高清晰度
|
|
|
+ useCORS: true,
|
|
|
+ allowTaint: true,
|
|
|
+ backgroundColor: '#ffffff'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 5. 移除临时容器
|
|
|
+ document.body.removeChild(tempDiv);
|
|
|
+
|
|
|
+ // 6. 计算PDF尺寸
|
|
|
+ const imgWidth = 210; // A4纸宽度(mm)
|
|
|
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
|
|
+
|
|
|
+ // 7. 创建PDF
|
|
|
+ const pdf = new jsPDF({
|
|
|
+ orientation: 'portrait',
|
|
|
+ unit: 'mm',
|
|
|
+ format: 'a4'
|
|
|
+ });
|
|
|
+
|
|
|
+ const imgData = canvas.toDataURL('image/jpeg', 1.0);
|
|
|
+
|
|
|
+ // 8. 如果内容高度超过一页,需要分页
|
|
|
+ const pageHeight = 297; // A4纸高度(mm)
|
|
|
+ let heightLeft = imgHeight;
|
|
|
+ let position = 0;
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 9. 下载PDF
|
|
|
+ const safeContractNo = contract.contractNo.replace(/[/\\:*?"<>|]/g, '-');
|
|
|
+ pdf.save(`合同-${safeContractNo}.pdf`);
|
|
|
+
|
|
|
+ proxy?.$modal.closeLoading();
|
|
|
proxy?.$modal.msgSuccess('PDF生成成功');
|
|
|
- window.open(pdfUrl, '_blank');
|
|
|
} catch (error) {
|
|
|
console.error('生成PDF失败:', error);
|
|
|
+ proxy?.$modal.closeLoading();
|
|
|
+ proxy?.$modal.msgError('生成PDF失败,请重试');
|
|
|
+ } finally {
|
|
|
+ pdfGenerating.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -1675,12 +1770,27 @@ const handleDetailInvalid = () => {
|
|
|
};
|
|
|
|
|
|
/** 详情抽屉 - 打印 */
|
|
|
-const handleDetailPrint = () => {
|
|
|
- // 打开新窗口打印
|
|
|
- router.push({
|
|
|
- path: '/crm/contract/detail',
|
|
|
- query: { id: detailContract.value?.id }
|
|
|
- });
|
|
|
+const handleDetailPrint = async () => {
|
|
|
+ try {
|
|
|
+ // 显示加载提示
|
|
|
+ proxy?.$modal.loading('正在生成合同...');
|
|
|
+
|
|
|
+ // 生成HTML内容
|
|
|
+ const htmlContent = generateContractTemplate(detailContract.value);
|
|
|
+
|
|
|
+ // 先设置内容,再打开对话框
|
|
|
+ contractContent.value = htmlContent;
|
|
|
+
|
|
|
+ // 关闭加载提示
|
|
|
+ proxy?.$modal.closeLoading();
|
|
|
+
|
|
|
+ // 打开对话框
|
|
|
+ generateContractDialog.visible = true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成合同失败:', error);
|
|
|
+ proxy?.$modal.closeLoading();
|
|
|
+ proxy?.$modal.msgError('生成合同失败');
|
|
|
+ }
|
|
|
};
|
|
|
|
|
|
/** 详情抽屉 - 查看客户 */
|
|
|
@@ -1905,6 +2015,397 @@ watch(detailActiveTab, (newTab) => {
|
|
|
// 目前回款和开票记录在详情中已经包含,暂不需要额外加载
|
|
|
});
|
|
|
|
|
|
+/** 生成合同HTML模版 */
|
|
|
+const generateContractTemplate = (contractData: ContractVO | undefined): string => {
|
|
|
+ if (!contractData) return '';
|
|
|
+
|
|
|
+ // 辅助函数:安全地将值转换为数字并格式化
|
|
|
+ const formatNumber = (value: any, decimals: number = 2): string => {
|
|
|
+ const num = parseFloat(value || 0);
|
|
|
+ return isNaN(num) ? '0.00' : num.toFixed(decimals);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 数字转中文大写
|
|
|
+ const numberToChinese = (num: number): string => {
|
|
|
+ const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
|
|
|
+ const units = ['', '拾', '佰', '仟', '万', '拾', '佰', '仟', '亿'];
|
|
|
+ const decimalUnits = ['角', '分'];
|
|
|
+
|
|
|
+ if (num === 0) return '零元整';
|
|
|
+
|
|
|
+ // 分离整数和小数部分
|
|
|
+ const parts = num.toFixed(2).split('.');
|
|
|
+ const integerPart = parseInt(parts[0]);
|
|
|
+ const decimalPart = parts[1];
|
|
|
+
|
|
|
+ let result = '';
|
|
|
+
|
|
|
+ // 处理整数部分
|
|
|
+ if (integerPart === 0) {
|
|
|
+ result = '零';
|
|
|
+ } else {
|
|
|
+ const intStr = integerPart.toString();
|
|
|
+ const len = intStr.length;
|
|
|
+ let zeroCount = 0;
|
|
|
+
|
|
|
+ for (let i = 0; i < len; i++) {
|
|
|
+ const digit = parseInt(intStr[i]);
|
|
|
+ const unit = units[len - i - 1];
|
|
|
+
|
|
|
+ if (digit === 0) {
|
|
|
+ zeroCount++;
|
|
|
+ } else {
|
|
|
+ if (zeroCount > 0) {
|
|
|
+ result += '零';
|
|
|
+ }
|
|
|
+ result += digits[digit] + unit;
|
|
|
+ zeroCount = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ result += '元';
|
|
|
+
|
|
|
+ // 处理小数部分
|
|
|
+ if (decimalPart === '00') {
|
|
|
+ result += '整';
|
|
|
+ } else {
|
|
|
+ const jiao = parseInt(decimalPart[0]);
|
|
|
+ const fen = parseInt(decimalPart[1]);
|
|
|
+
|
|
|
+ if (jiao > 0) {
|
|
|
+ result += digits[jiao] + decimalUnits[0];
|
|
|
+ }
|
|
|
+ if (fen > 0) {
|
|
|
+ if (jiao === 0) {
|
|
|
+ result += '零';
|
|
|
+ }
|
|
|
+ result += digits[fen] + decimalUnits[1];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 格式化日期(用于顶部信息,格式:2025.09.26)
|
|
|
+ const formatDate = (dateStr: string): string => {
|
|
|
+ if (!dateStr) return '';
|
|
|
+ const date = new Date(dateStr);
|
|
|
+ const year = date.getFullYear();
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(date.getDate()).padStart(2, '0');
|
|
|
+ return `${year}.${month}.${day}`;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 格式化日期(用于合同条款,格式:2025 年 09 月 26 日)
|
|
|
+ const formatDateChinese = (dateStr: string): string => {
|
|
|
+ if (!dateStr) return '';
|
|
|
+ const date = new Date(dateStr);
|
|
|
+ const year = date.getFullYear();
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(date.getDate()).padStart(2, '0');
|
|
|
+ return `${year} 年 ${month} 月 ${day} 日`;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 计算产品总金额(未含税)
|
|
|
+ const productTotalBeforeTax =
|
|
|
+ contractData.products?.reduce((sum, item) => {
|
|
|
+ const subtotal = parseFloat(String(item.subtotal || 0));
|
|
|
+ const taxAmount = parseFloat(String(item.taxAmount || 0));
|
|
|
+ return sum + (isNaN(subtotal) ? 0 : subtotal) - (isNaN(taxAmount) ? 0 : taxAmount);
|
|
|
+ }, 0) || 0;
|
|
|
+
|
|
|
+ // 计算总税费
|
|
|
+ const totalTax =
|
|
|
+ contractData.products?.reduce((sum, item) => {
|
|
|
+ const taxAmount = parseFloat(String(item.taxAmount || 0));
|
|
|
+ return sum + (isNaN(taxAmount) ? 0 : taxAmount);
|
|
|
+ }, 0) || 0;
|
|
|
+
|
|
|
+ // 计算产品总金额(含税)
|
|
|
+ const productTotal = parseFloat(String(contractData.amount || 0));
|
|
|
+ const productTotalChinese = numberToChinese(productTotal);
|
|
|
+
|
|
|
+ // 构建产品明细表格行
|
|
|
+ const productRows =
|
|
|
+ contractData.products
|
|
|
+ ?.map((product, index) => {
|
|
|
+ const subtotal = parseFloat(String(product.subtotal || 0));
|
|
|
+ const taxAmount = parseFloat(String(product.taxAmount || 0));
|
|
|
+ const beforeTax = subtotal - taxAmount;
|
|
|
+
|
|
|
+ return `
|
|
|
+ <tr>
|
|
|
+ <td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.productName || ''}</td>
|
|
|
+ <td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.model || product.productModel || ''}</td>
|
|
|
+ <td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.quantity || 0}</td>
|
|
|
+ <td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${formatNumber(product.price)}</td>
|
|
|
+ <td style="padding: 4px 6px; border: 1px solid #000; text-align: right; font-size: 12px;">${formatNumber(beforeTax)}</td>
|
|
|
+ <td style="padding: 4px 6px; border: 1px solid #000; text-align: right; font-size: 12px;">${formatNumber(taxAmount)}</td>
|
|
|
+ <td style="padding: 4px 6px; border: 1px solid #000; text-align: right; font-weight: bold; font-size: 12px;">${formatNumber(subtotal)}</td>
|
|
|
+ <td style="padding: 4px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">${product.remark || ''}</td>
|
|
|
+ </tr>`;
|
|
|
+ })
|
|
|
+ .join('') ||
|
|
|
+ '<tr><td colspan="8" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">暂无产品信息</td></tr>';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <div style="width: 210mm; margin: 0 auto; padding: 0 15mm 20mm 15mm; font-family: '宋体', SimSun, serif; line-height: 1.6; box-sizing: border-box;">
|
|
|
+ <!-- Logo -->
|
|
|
+ <div style="text-align: right; margin-top: 15px; margin-bottom: 10px;">
|
|
|
+ <img src="/src/assets/images/logo.gif" alt="logo" style="height: 30px;" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 标题 -->
|
|
|
+ <h1 style="text-align: center; font-size: 28px; margin: 0; font-weight: bold; line-height: 1.5;">山峡化工产品销售合同</h1>
|
|
|
+
|
|
|
+ <!-- 合同基本信息 -->
|
|
|
+ <div style="margin-bottom: 0; font-size: 12px; line-height: 1.5;">
|
|
|
+ <div style="display: flex; justify-content: space-between; margin-bottom: 0;">
|
|
|
+ <div>签订日期:${formatDate(contractData.signDate)}</div>
|
|
|
+ <div style="flex: 1;"></div>
|
|
|
+ </div>
|
|
|
+ <div style="display: flex; justify-content: space-between; margin-bottom: 0;">
|
|
|
+ <div>供方:${contractData.partyA || '安庆市山峡化工有限公司'}</div>
|
|
|
+ <div>合同编号:${contractData.contractNo || ''}</div>
|
|
|
+ </div>
|
|
|
+ <div style="display: flex; justify-content: space-between; margin-bottom: 0;">
|
|
|
+ <div>需方:${contractData.partyB || contractData.customerName || ''}</div>
|
|
|
+ <div>签订地点:安徽安庆</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 合同前言 -->
|
|
|
+ <div style="margin-bottom: 0; font-size: 12px; text-indent: 2em; line-height: 1.5;">
|
|
|
+ 经供需本着平等互利、协调一致的原则,根据中华人民共和国民法典的相关规定,订立合同。
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第一条 -->
|
|
|
+ <div style="margin-bottom: 0;">
|
|
|
+ <div style="font-weight: bold; margin-bottom: 10px; font-size: 12px;">第一条、标的、数量、价格及其他</div>
|
|
|
+
|
|
|
+ <!-- 产品表格 -->
|
|
|
+ <table style="width: 100%; border-collapse: collapse; margin-bottom: 0; border: 1px solid #000;">
|
|
|
+ <thead>
|
|
|
+ <tr style="background-color: #fff;">
|
|
|
+ <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 20%;">产品名称</th>
|
|
|
+ <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 10%;">规格</th>
|
|
|
+ <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 8%;">用量(吨)</th>
|
|
|
+ <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 12%;">含税单价(元/吨)</th>
|
|
|
+ <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 12%;">未含税金额(元)</th>
|
|
|
+ <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 10%;">税费(元)</th>
|
|
|
+ <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 13%;">含税金额(元)</th>
|
|
|
+ <th style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px; width: 15%;">备注</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ ${productRows}
|
|
|
+ <!-- 合计行 -->
|
|
|
+ <tr style="background-color: #fff; font-weight: bold;">
|
|
|
+ <td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 12px;">合计(人民币)</td>
|
|
|
+ <td colspan="4" style="padding: 5px 6px; border: 1px solid #000; text-align: right; font-size: 12px; padding-right: 15px;">${productTotalChinese}(¥${formatNumber(productTotal)})</td>
|
|
|
+ </tr>
|
|
|
+ <!-- 提示行 -->
|
|
|
+ <tr>
|
|
|
+ <td colspan="8" style="padding: 5px 6px; border: 1px solid #000; text-align: center; font-size: 11px; background-color: #fff;">
|
|
|
+ 含税、含运费、装卸费(一票制) 结算以实际到货吨位为准
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第二条 -->
|
|
|
+ <div style="margin-bottom: 0; line-height: 1.5; font-size: 12px;">
|
|
|
+ <div style="font-weight: bold; margin-bottom: 0;">第二条、产品质量及包装要求</div>
|
|
|
+ <div style="line-height: 1.8;">
|
|
|
+ <div style="margin: 0;">1、产品质量要求:产品质量以双方认可的产品质量标准为验收标准。</div>
|
|
|
+ <div style="margin: 0;">2、产品包装规格:散水包装。</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第三条 -->
|
|
|
+ <div style="margin-bottom: 0; line-height: 1.5; font-size: 12px;">
|
|
|
+ <div style="font-weight: bold; margin-bottom: 0;">第三条、交货方式及运输方式</div>
|
|
|
+ <div style="line-height: 1.8;">
|
|
|
+ <div style="margin: 0;">1、交货方式:供方送货,供方需提供需方要求产品,送达至需方要求地点。</div>
|
|
|
+ <div style="margin: 0;">2、运输:在运输过程中发生的一切费用由供方承担,到达本公司卸货过程中所发生的一些费用由供方承担。</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第四条 -->
|
|
|
+ <div style="margin-bottom: 0; line-height: 1.5; font-size: 12px;">
|
|
|
+ <div style="font-weight: bold; margin-bottom: 0;">第四条、验收标准</div>
|
|
|
+ <div style="line-height: 1.8; margin: 0;">
|
|
|
+ 按合同第一条规定,货物到达交货地点后,需方应在卸车前检验。如检验结果达到合同约定的质量标准,需方应无条件卸货。如检验结果未达到标准双方协商折价、换货或终止合同;需方验收合格应当日内安排卸货。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第五条 -->
|
|
|
+ <div style="margin-bottom: 0; line-height: 1.5; font-size: 12px;">
|
|
|
+ <div style="font-weight: bold; margin-bottom: 0;">第五条、结算方式及期限</div>
|
|
|
+ <div style="line-height: 1.8; margin: 0;">
|
|
|
+ 货到付款,需方收到供方货共13%增值税专用发票,无异议后,尽快以电汇方式付款。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第六条 -->
|
|
|
+ <div style="margin-bottom: 0; line-height: 1.5; font-size: 12px;">
|
|
|
+ <div style="font-weight: bold; margin-bottom: 0;">第六条、合同有效期限</div>
|
|
|
+ <div style="line-height: 1.8; margin: 0;">
|
|
|
+ 合同于${formatDateChinese(contractData.startDate)}生效,有效期至${formatDateChinese(contractData.endDate)}。本合同一式两份,经双方盖章后生效,双方盖章后扫描件、传真件与合同原件具有同等效力。
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 第七条 -->
|
|
|
+ <div style="margin-bottom: 0; line-height: 1.5; font-size: 12px;">
|
|
|
+ <div style="font-weight: bold; margin-bottom: 0;">第七条、其他补充及规定</div>
|
|
|
+ <div style="line-height: 1.8; margin: 0;">
|
|
|
+ 其他:本合同如有未尽事宜,须经双方共同协商,作出补充规定。如发生纠纷,由双方当事人协商解决,协商或调解不成的,可合同签订地的人民法院提出诉讼。若涉及易制毒化学品,需方须办理易制毒购买备案证明
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 双方信息 -->
|
|
|
+ <div style="display: flex; justify-content: space-between; margin-top: 10px;">
|
|
|
+ <div style="flex: 1; padding-right: 10px; border: 1px solid #000; padding: 8px; line-height: 1.3; font-size: 12px; position: relative;">
|
|
|
+ <div style="font-weight: bold; margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid #000;">供方</div>
|
|
|
+ <!-- 印章图片 -->
|
|
|
+ <img src="/src/assets/images/yz.png" alt="印章" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 140px; height: 140px; opacity: 0.85; z-index: 10;" />
|
|
|
+ <div style="margin-bottom: 0;">单位名称(章):${contractData.partyA || '安庆市山峡化工有限公司'}</div>
|
|
|
+ <div style="margin-bottom: 0;">单位地址:${contractData.partyAAddress || '安徽省安庆市迎江区皖江大道5号'}</div>
|
|
|
+ <div style="margin-bottom: 0;">法定代表人:${contractData.partyALegalPerson || ''}</div>
|
|
|
+ <div style="margin-bottom: 0;">代理人:${contractData.partyAAgent || ''}</div>
|
|
|
+ <div style="margin-bottom: 0;">电话/传真:${contractData.partyAPhoneFax || '0556-5396799'}</div>
|
|
|
+ <div style="margin-bottom: 0;">开户银行:${contractData.partyABank || '中国建设银行安庆市城北支行'}</div>
|
|
|
+ <div style="margin-bottom: 0;">账号:${contractData.partyAAccount || '34050168680800000121'}</div>
|
|
|
+ </div>
|
|
|
+ <div style="flex: 1; padding-left: 10px; border: 1px solid #000; padding: 8px; line-height: 1.3; font-size: 12px;">
|
|
|
+ <div style="font-weight: bold; margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid #000;">需方</div>
|
|
|
+ <div style="margin-bottom: 0;">单位名称(章):${contractData.partyB || contractData.customerName || ''}</div>
|
|
|
+ <div style="margin-bottom: 0;">单位地址:${contractData.partyBAddress || ''}</div>
|
|
|
+ <div style="margin-bottom: 0;">法定代表人:${contractData.partyBLegalPerson || ''}</div>
|
|
|
+ <div style="margin-bottom: 0;">代理人:${contractData.partyBAgent || ''}</div>
|
|
|
+ <div style="margin-bottom: 0;">电话/传真:${contractData.partyBPhoneFax || ''}</div>
|
|
|
+ <div style="margin-bottom: 0;">开户银行:${contractData.partyBBank || ''}</div>
|
|
|
+ <div style="margin-bottom: 0;">账号:${contractData.partyBAccount || ''}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+};
|
|
|
+
|
|
|
+/** 打印合同 */
|
|
|
+const handlePrintContract = () => {
|
|
|
+ // 创建打印窗口
|
|
|
+ const printWindow = window.open('', '_blank');
|
|
|
+ if (printWindow) {
|
|
|
+ printWindow.document.write(`
|
|
|
+ <!DOCTYPE html>
|
|
|
+ <html>
|
|
|
+ <head>
|
|
|
+ <meta charset="utf-8">
|
|
|
+ <title>打印合同</title>
|
|
|
+ <style>
|
|
|
+ body { margin: 0; padding: 20px; }
|
|
|
+ @media print {
|
|
|
+ body { margin: 0; }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>
|
|
|
+ ${contractContent.value}
|
|
|
+ </body>
|
|
|
+ </html>
|
|
|
+ `);
|
|
|
+ printWindow.document.close();
|
|
|
+
|
|
|
+ // 等待内容加载完成后打印
|
|
|
+ setTimeout(() => {
|
|
|
+ printWindow.print();
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/** 切换预览/编辑模式 */
|
|
|
+const togglePreviewMode = () => {
|
|
|
+ isPreviewMode.value = !isPreviewMode.value;
|
|
|
+};
|
|
|
+
|
|
|
+/** 从打印弹窗生成PDF(前端实现) */
|
|
|
+const handleGeneratePdfFromDialog = async () => {
|
|
|
+ if (!detailContract.value?.id) {
|
|
|
+ proxy?.$modal.msgError('合同信息不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ pdfGenerating.value = true;
|
|
|
+ try {
|
|
|
+ proxy?.$modal.loading('正在生成PDF...');
|
|
|
+
|
|
|
+ // 1. 创建临时容器
|
|
|
+ const tempDiv = document.createElement('div');
|
|
|
+ tempDiv.style.position = 'absolute';
|
|
|
+ tempDiv.style.left = '-9999px';
|
|
|
+ tempDiv.style.top = '0';
|
|
|
+ tempDiv.style.width = '794px'; // A4纸宽度
|
|
|
+ tempDiv.style.background = 'white';
|
|
|
+ tempDiv.innerHTML = contractContent.value;
|
|
|
+ document.body.appendChild(tempDiv);
|
|
|
+
|
|
|
+ // 2. 使用html2canvas转换为canvas
|
|
|
+ const canvas = await html2canvas(tempDiv, {
|
|
|
+ scale: 2, // 提高清晰度
|
|
|
+ useCORS: true,
|
|
|
+ allowTaint: true,
|
|
|
+ backgroundColor: '#ffffff'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3. 移除临时容器
|
|
|
+ document.body.removeChild(tempDiv);
|
|
|
+
|
|
|
+ // 4. 计算PDF尺寸
|
|
|
+ const imgWidth = 210; // A4纸宽度(mm)
|
|
|
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
|
|
+
|
|
|
+ // 5. 创建PDF
|
|
|
+ const pdf = new jsPDF({
|
|
|
+ orientation: 'portrait',
|
|
|
+ unit: 'mm',
|
|
|
+ format: 'a4'
|
|
|
+ });
|
|
|
+
|
|
|
+ const imgData = canvas.toDataURL('image/jpeg', 1.0);
|
|
|
+
|
|
|
+ // 6. 如果内容高度超过一页,需要分页
|
|
|
+ const pageHeight = 297; // A4纸高度(mm)
|
|
|
+ let heightLeft = imgHeight;
|
|
|
+ let position = 0;
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 下载PDF
|
|
|
+ const safeContractNo = detailContract.value.contractNo.replace(/[/\\:*?"<>|]/g, '-');
|
|
|
+ pdf.save(`合同-${safeContractNo}.pdf`);
|
|
|
+
|
|
|
+ proxy?.$modal.closeLoading();
|
|
|
+ proxy?.$modal.msgSuccess('PDF生成成功');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成PDF失败:', error);
|
|
|
+ proxy?.$modal.closeLoading();
|
|
|
+ proxy?.$modal.msgError('生成PDF失败,请重试');
|
|
|
+ } finally {
|
|
|
+ pdfGenerating.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
getList();
|
|
|
});
|