|
@@ -1,8 +1,8 @@
|
|
|
<template>
|
|
|
<div class="com-viewer">
|
|
|
+ <!-- getComponentType() -->
|
|
|
<div class="content" ref="content">
|
|
|
- <template v-for="(item, index) in coms"
|
|
|
- ><!-- getComponentType() -->
|
|
|
+ <template v-for="(item, index) in coms">
|
|
|
<component
|
|
|
:key="index"
|
|
|
:is="item.type"
|
|
@@ -16,12 +16,13 @@
|
|
|
</div>
|
|
|
<div class="export">
|
|
|
<el-button
|
|
|
- @click="onExport"
|
|
|
+ @click="onExports"
|
|
|
type="primary"
|
|
|
:loading="exporting"
|
|
|
:disabled="exporting"
|
|
|
>导出</el-button
|
|
|
>
|
|
|
+ <el-progress v-if="exporting" :percentage="exportProgress" />
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
@@ -30,10 +31,11 @@
|
|
|
import { exportDocument } from "@/api/document";
|
|
|
import TextArea from "./components/TextView";
|
|
|
import htmlDocx from "html-docx-js/dist/html-docx";
|
|
|
-import saveAs from "file-saver";
|
|
|
import jsPDF from "jspdf";
|
|
|
+import html2canvas from "html2canvas";
|
|
|
import { findData } from "@/api/sourceData";
|
|
|
-import { createDocument } from "html-to-docx";
|
|
|
+import HTMLtoDOCX from "html-to-docx";
|
|
|
+import { saveAs } from "file-saver";
|
|
|
|
|
|
export default {
|
|
|
name: "Viewer",
|
|
@@ -72,6 +74,7 @@ export default {
|
|
|
comList: [],
|
|
|
content: "",
|
|
|
exporting: false, // 新增:用于跟踪导出状态
|
|
|
+ exportProgress: 0,
|
|
|
};
|
|
|
},
|
|
|
methods: {
|
|
@@ -86,7 +89,7 @@ export default {
|
|
|
formual = await this.getRemote(formual);
|
|
|
el.content = eval(formual);
|
|
|
} catch (error) {}
|
|
|
- }else if(el.type == "variableNull"){
|
|
|
+ } else if (el.type == "variableNull") {
|
|
|
console.log(el);
|
|
|
}
|
|
|
}
|
|
@@ -361,8 +364,73 @@ export default {
|
|
|
});
|
|
|
|
|
|
let contentData = `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>
|
|
|
+ @page {
|
|
|
+ size: A4;
|
|
|
+ margin: 2cm;
|
|
|
+ }
|
|
|
+ body {
|
|
|
+ font-family: Arial, sans-serif;
|
|
|
+ font-size: 12pt;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+ table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ }
|
|
|
+ td, th {
|
|
|
+ border: 1px solid black;
|
|
|
+ padding: 5px;
|
|
|
+ }
|
|
|
+ .template-textarea {
|
|
|
+ page-break-after: always;
|
|
|
+ }
|
|
|
+ .toc-page {
|
|
|
+ page-break-before: always;
|
|
|
+ page-break-after: always;
|
|
|
+ }
|
|
|
+ .toc-content {
|
|
|
+ height: 100vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: flex-start;
|
|
|
+ padding-top: 100px;
|
|
|
+ }
|
|
|
+ .toc-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+ .toc-text {
|
|
|
+ margin-right: 10px;
|
|
|
+ }
|
|
|
+ .toc-page-num {
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .toc-item::after {
|
|
|
+ content: "";
|
|
|
+ border-bottom: 1px dotted black;
|
|
|
+ flex-grow: 1;
|
|
|
+ order: 2;
|
|
|
+ margin: 0 5px;
|
|
|
+ position: relative;
|
|
|
+ top: -5px;
|
|
|
+ }
|
|
|
+ .toc-text {
|
|
|
+ order: 1;
|
|
|
+ }
|
|
|
+ .toc-page-num {
|
|
|
+ order: 3;
|
|
|
+ }
|
|
|
+
|
|
|
+ .TOC {
|
|
|
+ display: none !important;
|
|
|
+ }
|
|
|
+ </style></head><body>${contentClone.innerHTML}</body></html>`; /*`<!DOCTYPE html><html><head><meta charset="UTF-8"><style>
|
|
|
@media print {
|
|
|
body {
|
|
|
+ width: 210mm;
|
|
|
+ height: 297mm;
|
|
|
+
|
|
|
margin: 0;
|
|
|
padding: 0;
|
|
|
}
|
|
@@ -410,12 +478,12 @@ export default {
|
|
|
.toc-page-num {
|
|
|
order: 3;
|
|
|
}
|
|
|
- /* 移除 WPS 自动添加的目录引用框 */
|
|
|
+
|
|
|
.TOC {
|
|
|
display: none !important;
|
|
|
}
|
|
|
}
|
|
|
- </style></head><body><p class="header1"></p>${contentClone.innerHTML}</body></html>`;
|
|
|
+ </style></head><body><p class="header1"></p>${contentClone.innerHTML}</body></html>` 移除 WPS 自动添加的目录引用框 */
|
|
|
contentData = contentData
|
|
|
.replaceAll("<table ", `<table style="border-collapse: collapse;" `)
|
|
|
.replaceAll("<td>", `<td style="border: 1px solid black;">`);
|
|
@@ -442,6 +510,234 @@ export default {
|
|
|
this.exporting = false; // 无论成功还是失败,都将导出状态设置回 false
|
|
|
}
|
|
|
},
|
|
|
+ /* 前端导出word */
|
|
|
+ async onExports() {
|
|
|
+ if (this.exporting) return;
|
|
|
+ this.exporting = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ let contentClone = this.$refs.content.cloneNode(true);
|
|
|
+ console.log(contentClone);
|
|
|
+
|
|
|
+ // 等待所有图片加载完成
|
|
|
+ await this.waitForImages(contentClone);
|
|
|
+
|
|
|
+ // 将图片转换为 Base64
|
|
|
+ const imgPromises = Array.from(
|
|
|
+ contentClone.querySelectorAll("img")
|
|
|
+ ).map((img) => this.convertImageToBase64(img));
|
|
|
+
|
|
|
+ await Promise.all(imgPromises);
|
|
|
+
|
|
|
+ // 处理输入框
|
|
|
+ contentClone.querySelectorAll('input[type="text"]').forEach((input) => {
|
|
|
+ const span = document.createElement("span");
|
|
|
+ span.textContent = input.value;
|
|
|
+ input.parentNode.replaceChild(span, input);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建HTML内容
|
|
|
+ let contentData = `
|
|
|
+ <!DOCTYPE html>
|
|
|
+ <html>
|
|
|
+ <head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <style>
|
|
|
+ @page {
|
|
|
+ size: A4;
|
|
|
+ margin: 2cm;
|
|
|
+ }
|
|
|
+ body {
|
|
|
+ font-family: Arial, sans-serif;
|
|
|
+ font-size: 12pt;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+ table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ }
|
|
|
+ td, th {
|
|
|
+ border: 1px solid black;
|
|
|
+ padding: 5px;
|
|
|
+ }
|
|
|
+ .template-textarea {
|
|
|
+ page-break-after: always;
|
|
|
+ }
|
|
|
+ img {
|
|
|
+ max-width: 100%;
|
|
|
+ height: auto;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>${contentClone.innerHTML}</body>
|
|
|
+ </html>
|
|
|
+ `;
|
|
|
+
|
|
|
+ // 创建文档选项
|
|
|
+ const options = {
|
|
|
+ margin: {
|
|
|
+ top: "2cm",
|
|
|
+ right: "2cm",
|
|
|
+ bottom: "2cm",
|
|
|
+ left: "2cm",
|
|
|
+ },
|
|
|
+ table: { row: { cantSplit: true } },
|
|
|
+ footer: {
|
|
|
+ default:
|
|
|
+ '<p style="color: #444; font-size: 10pt">页码: {page} / {pages}</p>',
|
|
|
+ },
|
|
|
+ pageNumber: true,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 创建文档
|
|
|
+ const docxBlob = await HTMLtoDOCX(contentData, null, options);
|
|
|
+
|
|
|
+ // 保存文件
|
|
|
+ saveAs(docxBlob, `${this.docAttr.title}.docx`);
|
|
|
+
|
|
|
+ this.$message.success("文档导出成功");
|
|
|
+ } catch (error) {
|
|
|
+ console.error("导出文档时发生错误:", error);
|
|
|
+ this.$message.error("导出文档时发生错误,请稍后重试");
|
|
|
+ } finally {
|
|
|
+ this.exporting = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 新增方法:等待所有图片加载完成
|
|
|
+ async waitForImages(element) {
|
|
|
+ const images = element.getElementsByTagName("img");
|
|
|
+ const imagePromises = Array.from(images).map((img) => {
|
|
|
+ if (img.complete) {
|
|
|
+ return Promise.resolve();
|
|
|
+ } else {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ img.onload = img.onerror = resolve;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ await Promise.all(imagePromises);
|
|
|
+ },
|
|
|
+ // 新增方法:将图片转换为Base64
|
|
|
+ async convertImageToBase64(img) {
|
|
|
+ if (img.src.startsWith("data:")) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const response = await fetch(img.src);
|
|
|
+ const blob = await response.blob();
|
|
|
+ const base64 = await new Promise((resolve) => {
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onloadend = () => resolve(reader.result);
|
|
|
+ reader.readAsDataURL(blob);
|
|
|
+ });
|
|
|
+ img.src = base64;
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Error converting image to Base64:", error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /* 终 */
|
|
|
+ /* pdf导出 */
|
|
|
+ async onExportPDF() {
|
|
|
+ if (this.exporting) return;
|
|
|
+ this.exporting = true;
|
|
|
+ this.exportProgress = 0;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const content = this.$refs.content;
|
|
|
+
|
|
|
+ // 保存原始样式
|
|
|
+ const originalStyle = {
|
|
|
+ height: content.style.height,
|
|
|
+ overflow: content.style.overflow,
|
|
|
+ position: content.style.position,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 临时修改样式以确保所有内容可见
|
|
|
+ content.style.height = "auto";
|
|
|
+ content.style.overflow = "visible";
|
|
|
+ content.style.position = "absolute";
|
|
|
+
|
|
|
+ // 等待图片加载
|
|
|
+ await this.waitForImages(content);
|
|
|
+
|
|
|
+ const pdf = new jsPDF("p", "pt", "a4");
|
|
|
+ const pageHeight = pdf.internal.pageSize.getHeight();
|
|
|
+ const pageWidth = pdf.internal.pageSize.getWidth();
|
|
|
+
|
|
|
+ // 设置超时
|
|
|
+ const timeout = setTimeout(() => {
|
|
|
+ throw new Error("PDF export timed out");
|
|
|
+ }, 60000); // 60秒超时
|
|
|
+
|
|
|
+ await this.addContentToPDF(content, pdf, pageWidth, pageHeight);
|
|
|
+
|
|
|
+ clearTimeout(timeout);
|
|
|
+
|
|
|
+ pdf.save(`${this.docAttr.title || "document"}.pdf`);
|
|
|
+
|
|
|
+ // 恢复原始样式
|
|
|
+ Object.assign(content.style, originalStyle);
|
|
|
+
|
|
|
+ this.$message.success("PDF导出成功");
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Export to PDF failed:", error);
|
|
|
+ this.$message.error(`PDF导出失败:${error.message}`);
|
|
|
+ } finally {
|
|
|
+ this.exporting = false;
|
|
|
+ this.exportProgress = 0;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async waitForImages(element) {
|
|
|
+ const images = element.getElementsByTagName("img");
|
|
|
+ const imagePromises = Array.from(images).map((img) => {
|
|
|
+ if (img.complete) return Promise.resolve();
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ img.onload = img.onerror = resolve;
|
|
|
+ });
|
|
|
+ });
|
|
|
+ await Promise.all(imagePromises);
|
|
|
+ },
|
|
|
+
|
|
|
+ async addContentToPDF(element, pdf, pageWidth, pageHeight, topOffset = 0) {
|
|
|
+ const totalHeight = element.scrollHeight;
|
|
|
+ const canvas = await html2canvas(element, {
|
|
|
+ scale: 2,
|
|
|
+ useCORS: true,
|
|
|
+ logging: false,
|
|
|
+ windowWidth: pageWidth,
|
|
|
+ windowHeight: pageHeight,
|
|
|
+ y: topOffset,
|
|
|
+ ignoreElements: (element) => {
|
|
|
+ // 忽略使用已弃用的 -ms-high-contrast 的元素
|
|
|
+ const style = window.getComputedStyle(element);
|
|
|
+ return style.getPropertyValue("-ms-high-contrast") !== "none";
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ const imgData = canvas.toDataURL("image/jpeg", 1.0);
|
|
|
+ const imgWidth = pageWidth;
|
|
|
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
|
|
+
|
|
|
+ pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
|
|
|
+
|
|
|
+ this.exportProgress = Math.min(
|
|
|
+ 100,
|
|
|
+ Math.round(((topOffset + pageHeight) / totalHeight) * 100)
|
|
|
+ );
|
|
|
+
|
|
|
+ if (canvas.height > pageHeight) {
|
|
|
+ pdf.addPage();
|
|
|
+ await this.addContentToPDF(
|
|
|
+ element,
|
|
|
+ pdf,
|
|
|
+ pageWidth,
|
|
|
+ pageHeight,
|
|
|
+ topOffset + pageHeight
|
|
|
+ );
|
|
|
+ }
|
|
|
+ },
|
|
|
|
|
|
/* onExport() {
|
|
|
let contentData = `<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><p class="header1"></p> ${this.$refs.content.innerHTML} </body></html>`;
|