feat(web-admin): TinyMCE 表格类 Excel 调宽调高,并优化出入库/分装/送货单排版

支持合同、报价与入库/分装/送(提)货单在富文本中拖拽调整列宽行高;单据侧加宽物料编号与名称、收窄规格单位、放大 logo、加宽条款区,并弱化调整手柄绿色阴影。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-25 18:02:22 +08:00
1 parent 2380b01a4f
commit 3d5866aa14
13 files changed
+31815 -30418

No files matched your search

@@ -782,6 +782,9 @@
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
>
<p v-if="!isPreviewMode" class="generate-contract-edit-hint">
编辑模式:鼠标移入表格后,列/行边界会出现绿色调整条,按住拖动即可调列宽或行高;工具栏可合并拆分、插删行列、对齐与换行。调整后请点「保存模板」。
</p>
<TinyMceEditor
v-model="contractContent"
:height="600"
@@ -886,6 +889,7 @@ import ImagePreview from '@/components/ImagePreview/index.vue';
import TinyMceEditor from '@/components/TinyMceEditor/index.vue';
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
import { fitRichTextTablesToMaxWidth } from '@/utils/tinymce-table-excel-resize';
import {
downloadCrmAttachment,
getCrmAttachmentCategoryLabel,
@@ -2698,12 +2702,14 @@ const handleGeneratePdfFromDialog = async () => {
htmlContent = replacePlaceholders(htmlContent, res.data, quoteFreight);
}
// 1. 创建临时容器
// 1. 创建临时容器(border-box:794 含 padding)
const tempDiv = document.createElement('div');
tempDiv.style.position = 'absolute';
tempDiv.style.left = '-9999px';
tempDiv.style.top = '0';
tempDiv.style.width = '794px'; // A4纸宽度
tempDiv.style.boxSizing = 'border-box';
tempDiv.style.overflow = 'hidden';
tempDiv.style.background = 'white';
tempDiv.style.padding = '20px';
@@ -2711,6 +2717,8 @@ const handleGeneratePdfFromDialog = async () => {
const watermarkDiv = document.createElement('div');
watermarkDiv.style.position = 'relative';
watermarkDiv.style.width = '100%';
watermarkDiv.style.maxWidth = '100%';
watermarkDiv.style.boxSizing = 'border-box';
// 根据供方编码决定是否添加水印背景层
if (isSXSupplier) {
@@ -2734,7 +2742,7 @@ const handleGeneratePdfFromDialog = async () => {
// 添加内容层
const contentDiv = document.createElement('div');
contentDiv.style.cssText = 'position: relative; z-index: 1;';
contentDiv.style.cssText = 'position: relative; z-index: 1; width: 100%; max-width: 100%; box-sizing: border-box; overflow: hidden;';
// 如果不是SX供方编码,在设置HTML之前移除logo
let contentHtml = htmlContent;
@@ -2753,6 +2761,12 @@ const handleGeneratePdfFromDialog = async () => {
}
contentDiv.innerHTML = contentHtml;
const fitStyle = document.createElement('style');
fitStyle.textContent = `
table { max-width: 100% !important; width: 100% !important; table-layout: fixed !important; box-sizing: border-box !important; }
table td, table th { box-sizing: border-box !important; min-width: 0 !important; word-break: break-word; overflow-wrap: anywhere; }
`;
contentDiv.prepend(fitStyle);
watermarkDiv.appendChild(contentDiv);
tempDiv.appendChild(watermarkDiv);
@@ -2772,6 +2786,10 @@ const handleGeneratePdfFromDialog = async () => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
// 编辑器拖拽后表宽常大于 A4 内容区;截图前等比缩进,避免 PDF 右侧裁切
const pdfContentMaxWidth = Math.max(1, tempDiv.clientWidth - 40);
fitRichTextTablesToMaxWidth(contentDiv, pdfContentMaxWidth);
// 如果不是SX供方编码,再次检查并隐藏可能遗漏的logo
if (!isSXSupplier) {
const logoImages = contentDiv.querySelectorAll('img');
@@ -2794,7 +2812,9 @@ const handleGeneratePdfFromDialog = async () => {
scale: getContractPdfCanvasScale(),
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff'
backgroundColor: '#ffffff',
width: 794,
windowWidth: 794
});
// 3. 移除临时容器
@@ -2950,6 +2970,17 @@ onMounted(async () => {
line-height: 1.5;
}
.generate-contract-edit-hint {
margin: 0 0 12px;
padding: 8px 12px;
font-size: 12px;
line-height: 1.5;
color: #047857;
background: #ecfdf5;
border: 1px solid #a7f3d0;
border-radius: 8px;
}
.parties-section {
margin: 8px 0 20px;
}
+33 -2
View File
@@ -984,6 +984,9 @@
append-to-body
:close-on-click-modal="false"
>
<p v-if="!isPreviewMode" class="generate-contract-edit-hint">
编辑模式:鼠标移入表格后,列/行边界会出现绿色调整条,按住拖动即可调列宽或行高;工具栏可合并拆分、插删行列、对齐与换行。调整后请点「保存模板」。
</p>
<TinyMceEditor v-model="contractContent" :height="600" :disabled="isPreviewMode" :toolbar="!isPreviewMode" :contractNo="contract?.contractNo" />
<template #footer>
<div class="dialog-footer">
@@ -1029,6 +1032,7 @@ import { useContractPerm } from '@/hooks/business/useContractPerm';
import { canDeleteByCreator } from '@/utils/creator-delete';
import html2canvas from 'html2canvas';
import { jsPDF } from 'jspdf';
import { fitRichTextTablesToMaxWidth } from '@/utils/tinymce-table-excel-resize';
const router = useRouter();
const route = useRoute();
@@ -2541,6 +2545,8 @@ const handleGeneratePdfFile = async () => {
tempDiv.style.left = '-9999px';
tempDiv.style.top = '0';
tempDiv.style.width = '794px'; // A4纸宽度的像素值(210mm * 96dpi / 25.4)
tempDiv.style.boxSizing = 'border-box';
tempDiv.style.overflow = 'hidden';
tempDiv.style.background = 'white';
tempDiv.style.padding = '20px';
@@ -2548,6 +2554,8 @@ const handleGeneratePdfFile = async () => {
const watermarkDiv = document.createElement('div');
watermarkDiv.style.position = 'relative';
watermarkDiv.style.width = '100%';
watermarkDiv.style.maxWidth = '100%';
watermarkDiv.style.boxSizing = 'border-box';
// 添加水印背景层
const watermarkBg = document.createElement('div');
@@ -2568,8 +2576,14 @@ const handleGeneratePdfFile = async () => {
// 添加内容层
const contentDiv = document.createElement('div');
contentDiv.style.cssText = 'position: relative; z-index: 1;';
contentDiv.style.cssText = 'position: relative; z-index: 1; width: 100%; max-width: 100%; box-sizing: border-box; overflow: hidden;';
contentDiv.innerHTML = contractContent.value;
const fitStyle = document.createElement('style');
fitStyle.textContent = `
table { max-width: 100% !important; width: 100% !important; table-layout: fixed !important; box-sizing: border-box !important; }
table td, table th { box-sizing: border-box !important; min-width: 0 !important; word-break: break-word; overflow-wrap: anywhere; }
`;
contentDiv.prepend(fitStyle);
watermarkDiv.appendChild(watermarkBg);
watermarkDiv.appendChild(contentDiv);
@@ -2590,12 +2604,18 @@ const handleGeneratePdfFile = async () => {
});
await Promise.all(imagePromises);
// 编辑器拖拽后表宽常大于 A4 内容区;截图前等比缩进,避免 PDF 右侧裁切
const pdfContentMaxWidth = Math.max(1, tempDiv.clientWidth - 40);
fitRichTextTablesToMaxWidth(contentDiv, pdfContentMaxWidth);
// 使用html2canvas将HTML转换为canvas
const canvas = await html2canvas(tempDiv, {
scale: 2, // 提高清晰度
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff'
backgroundColor: '#ffffff',
width: 794,
windowWidth: 794
});
// 移除临时div
@@ -3014,4 +3034,15 @@ const handleGeneratePdfFile = async () => {
.tier-letter-download-link {
font-size: 12px;
}
.generate-contract-edit-hint {
margin: 0 0 12px;
padding: 8px 12px;
font-size: 12px;
line-height: 1.5;
color: #047857;
background: #ecfdf5;
border: 1px solid #a7f3d0;
border-radius: 8px;
}
</style>
+36 -4
View File
@@ -769,6 +769,7 @@ import yzImg from '@/assets/images/yz.png';
import ydImg from '@/assets/images/yd.png';
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
import { fitRichTextTablesToMaxWidth } from '@/utils/tinymce-table-excel-resize';
import { useQuotePerm } from '@/hooks/business/useQuotePerm'
import { filterDeletableByCreator, canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
@@ -2162,7 +2163,10 @@ const handlePrint = () => {
margin: 0;
padding: 20px;
position: relative;
box-sizing: border-box;
}
table { max-width: 100% !important; width: 100% !important; table-layout: fixed !important; box-sizing: border-box !important; }
table td, table th { box-sizing: border-box !important; min-width: 0 !important; word-break: break-word; overflow-wrap: anywhere; }
${backgroundStyle}
@media print {
body {
@@ -2179,8 +2183,15 @@ const handlePrint = () => {
`);
printWindow.document.close();
// 等待图片和内容加载完成后打印
// 等待图片和内容加载完成后打印;先把超宽表缩进打印页
setTimeout(() => {
try {
const body = printWindow.document.body;
const maxW = Math.max(1, (body?.clientWidth || 794) - 40);
fitRichTextTablesToMaxWidth(body, maxW);
} catch {
/* ignore */
}
printWindow.print();
}, 1000);
}
@@ -2259,12 +2270,14 @@ const handleGeneratePdf = async () => {
// 根据供应商编码决定是否显示背景图片
const showBackground = currentSupplierCode.value === 'SX';
// 创建临时容器
// 创建临时容器(border-box:794 含 padding,与 A4 截图像素一致)
const tempDiv = document.createElement('div');
tempDiv.style.position = 'absolute';
tempDiv.style.left = '-9999px';
tempDiv.style.top = '0';
tempDiv.style.width = '794px'; // A4纸宽度
tempDiv.style.boxSizing = 'border-box';
tempDiv.style.overflow = 'hidden';
tempDiv.style.background = 'white';
tempDiv.style.padding = '20px';
@@ -2272,6 +2285,8 @@ const handleGeneratePdf = async () => {
const containerDiv = document.createElement('div');
containerDiv.style.position = 'relative';
containerDiv.style.width = '100%';
containerDiv.style.maxWidth = '100%';
containerDiv.style.boxSizing = 'border-box';
// 如果是SX编码,添加水印背景层
if (showBackground) {
@@ -2295,19 +2310,36 @@ const handleGeneratePdf = async () => {
// 添加内容层
const contentDiv = document.createElement('div');
contentDiv.style.cssText = 'position: relative; z-index: 1;';
contentDiv.style.cssText = 'position: relative; z-index: 1; width: 100%; max-width: 100%; box-sizing: border-box; overflow: hidden;';
contentDiv.innerHTML = quoteContent.value;
// PDF 环境无 TinyMCE content_style,补齐表格钳制,防止声明像素宽撑破
const fitStyle = document.createElement('style');
fitStyle.textContent = `
table { max-width: 100% !important; width: 100% !important; table-layout: fixed !important; box-sizing: border-box !important; }
table td, table th { box-sizing: border-box !important; min-width: 0 !important; word-break: break-word; overflow-wrap: anywhere; }
`;
contentDiv.prepend(fitStyle);
containerDiv.appendChild(contentDiv);
tempDiv.appendChild(containerDiv);
document.body.appendChild(tempDiv);
// 按声明列宽重写为百分比(编辑器视觉被 max-width 钳制时 getBoundingClientRect 会误判为已适配)
const pdfContentMaxWidth = Math.max(1, tempDiv.clientWidth - 40);
fitRichTextTablesToMaxWidth(contentDiv, pdfContentMaxWidth);
await new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
});
// 使用html2canvas转换为canvas
const canvas = await html2canvas(tempDiv, {
scale: 2, // 提高清晰度
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff'
backgroundColor: '#ffffff',
width: 794,
windowWidth: 794
});
// 移除临时容器