123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768 |
- <template>
- <div class="com-viewer">
- <!-- getComponentType() -->
- <div class="content" ref="content">
- <template v-for="(item, index) in coms">
- <component
- :key="index"
- :is="item.type"
- :com="item"
- :coms="comList"
- :currentIndex="index"
- :isActive="false"
- @onUpdateData="onUpdateData"
- />
- </template>
- </div>
- <div class="export">
- <el-button
- @click="onExports"
- type="primary"
- :loading="exporting"
- :disabled="exporting"
- >导出</el-button
- >
- <el-progress v-if="exporting" :percentage="exportProgress" />
- </div>
- </div>
- </template>
- <script>
- import { exportDocument } from "@/api/document";
- import TextArea from "./components/TextView";
- import htmlDocx from "html-docx-js/dist/html-docx";
- import jsPDF from "jspdf";
- import html2canvas from "html2canvas";
- import { findData } from "@/api/sourceData";
- import HTMLtoDOCX from "html-to-docx";
- import { saveAs } from "file-saver";
- export default {
- name: "Viewer",
- components: {
- TextArea,
- },
- props: {
- id: {
- type: Number,
- default: 0,
- },
- coms: {
- type: Array,
- default: null,
- },
- docAttr: {
- type: Object,
- default: () => ({
- title: "",
- }),
- },
- },
- watch: {
- coms: {
- handler(val) {
- if (val == null) return;
- this.comList = JSON.parse(JSON.stringify(val));
- this.replaceData(val);
- },
- immediate: true,
- deep: true,
- },
- },
- data() {
- return {
- comList: [],
- content: "",
- exporting: false, // 新增:用于跟踪导出状态
- exportProgress: 0,
- };
- },
- methods: {
- async replaceData(data) {
- for (let item of this.comList) {
- for (let el of item.attrs) {
- let attrId = el.id;
- if (el.type == "formual") {
- try {
- let formual = await this.analysisFormual(el);
- formual = await this.getRemote(formual);
- el.content = eval(formual);
- } catch (error) {}
- } else if (el.type == "variableNull") {
- console.log(el);
- }
- }
- }
- //this.$emit('update:coms', [...this.comList])
- return data;
- },
- //分析公式
- async analysisFormual(attrs) {
- let _this = this;
- // console.log("开始分析公式:", attrs.formula);
- var pattern = /(\[.*?\]){3}/;
- var formual = attrs.formula;
- var reg = new RegExp(pattern);
- while (true) {
- var items = formual.match(reg);
- if (items == null) break;
- let itemName = items[0];
- // console.log("处理项:", itemName);
- try {
- let data = await _this.getFormualData(itemName);
- // console.log(`获取到的数据: ${itemName} = ${data}`);
- if (data === null || data === undefined || isNaN(data)) {
- console.warn(`获取到的数据无效: ${itemName}`);
- formual = formual.replace(itemName, "(0)");
- } else {
- formual = formual.replace(itemName, `(${parseFloat(data)})`);
- }
- } catch (error) {
- console.error(`处理 ${itemName} 时出错:`, error);
- formual = formual.replace(itemName, "(0)");
- }
- }
- return formual;
- },
- async getFormualData(formualItem) {
- let _this = this;
- // console.log("开始获取公式数据:", formualItem);
- var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
- var reg = new RegExp(pattern);
- let items = formualItem.match(reg);
- // console.log("解析结果:", items);
- let calResult = 0;
- if (items && items[1] == "T") {
- calResult = await _this.getModuleData(items[2], items[3]);
- } else {
- console.warn("无法解析公式项:", formualItem);
- }
- // console.log("计算结果:", calResult);
- return calResult;
- },
- //获取本地文档模块数据
- async getModuleData(code, attrName) {
- let _this = this;
- // console.log(`尝试获取模块数据: code=${code}, attrName=${attrName}`);
- // console.log("当前 coms:", _this.coms);
- let item = _this.comList.filter((o) => o.name == code);
- // console.log(`找到的模块:`, item);
- if (item.length > 0) {
- let attr = item[0].attrs.filter((o) => o.name == attrName);
- // console.log(`找到的属性:`, attr);
- if (attr.length > 0) {
- // console.log(`返回的值:`, attr[0].content);
- return parseFloat(attr[0].content);
- }
- }
- // console.log("未找到匹配的模块或属性,返回 0");
- return 0;
- },
- async getRemote(formual) {
- let _this = this;
- var pattern = /\[(.*?)\]\[(.*?)\]\[(.*?)\]\[(.*?)\]/;
- var reg = new RegExp(pattern);
- while (true) {
- let items = formual.match(reg);
- if (items == null) break;
- if (items[1] == "R") {
- //表示获取远程数据
- var dataIndex = items[4];
- dataIndex = items[4].split(",");
- let calResult = await _this.getRemoteData(
- items[2],
- items[3],
- dataIndex[0],
- dataIndex[1]
- );
- let itemName = items[0];
- // 检查calResult是否为数字
- if (!isNaN(calResult)) {
- formual = formual.replace(
- itemName,
- "(" + parseFloat(calResult) + ")"
- );
- } else {
- formual = formual.replace(itemName, `"${calResult}"`);
- }
- formual = formual.replace(
- itemName,
- "(" + parseFloat(calResult) + ")"
- );
- }
- }
- return formual;
- },
- async getRemoteData(code, sheet, row, col) {
- let _this = this;
- let result = 0;
- let par = {
- code: code,
- sheetName: sheet,
- row: row,
- col: col,
- };
- /* if (!this.isEditing) {
- this.loading = true;
- this.progress = 0;
- try {
- const updateProgress = setInterval(() => {
- if (this.progress < 90) {
- this.progress += 10;
- }
- }, 100);
- let res = await findData(par);
- if (res.status == 200) {
- result = res.data.result;
- }
- this.progress = 100;
- clearInterval(updateProgress);
- } catch (error) {
- console.error("Error fetching remote data:", error);
- } finally {
- setTimeout(() => {
- this.loading = false;
- this.progress = 0;
- }, 1000);
- }
- } else { */
- // 如果正在编辑,直接获取数据而不显示进度条
- try {
- let res = await findData(par);
- if (res.status == 200) {
- result = res.data.result;
- // 添加数据类型检查和转换
- if (typeof result === "string" && !isNaN(result)) {
- result = parseFloat(result);
- }
- }
- } catch (error) {
- console.error("获取远程数据错误:", error);
- }
- /* } */
- return result;
- },
- getComponentType(type) {
- if (type === "textarea") {
- return TextArea;
- }
- // Add other component types if needed
- return type;
- },
- onUpdateData(index, data) {
- this.comList[index].content = data;
- this.rebuildContent();
- //this.$emit('update:coms', [...this.comList])
- },
- rebuildContent() {
- this.content = this.comList.map((item) => item.content).join("");
- },
- async onExport() {
- if (this.exporting) return; // 如果正在导出,直接返回
- this.exporting = true; // 设置导出状态为 true
- try {
- let contentClone = this.$refs.content.cloneNode(true);
- /* const templateTextareas =
- contentClone.querySelectorAll(".template-textarea");
- if (templateTextareas.length > 0) {
- // 创建目录页
- const tocPage = document.createElement("div");
- tocPage.className = "toc-page";
- tocPage.innerHTML = `
- <div style="page-break-before: always; page-break-after: always; height: 100vh; display: flex; flex-direction: column; justify-content: center;">
- <h1 style="text-align: center; margin-bottom: 40px;">目录</h1>
- <div id="toc" style="margin: 0 auto; min-width: 600px;"></div>
- </div>
- `;
- // 在第一个 template-textarea 之后插入目录页
- templateTextareas[0].parentNode.insertBefore(
- tocPage,
- templateTextareas[0].nextSibling
- );
- // 生成目录内容
- let tocHtml = '<div style="column-count: 1; column-gap: 40px;">';
- templateTextareas.forEach((textarea, index) => {
- const title = textarea.querySelector("h1, h2, h3, h4, h5, h6");
- if (title) {
- tocHtml += `
- <p style="margin: 0; padding: 5px 0;">
- <span style="float: left;">${title.textContent}</span>
- <span style="float: right;">${index + 1}</span>
- <span style="display: block; overflow: hidden; height: 1.2em; border-bottom: 1px dotted black;"></span>
- </p>
- `;
- title.id = `section-${index + 1}`;
- }
- });
- tocHtml += "</div>";
- // 将目录内容插入到目录页中
- contentClone.querySelector("#toc").innerHTML = tocHtml;
- } */
- const templateTextareas =
- contentClone.querySelectorAll(".template-textarea");
- if (templateTextareas.length > 0) {
- // 创建空白页和目录页
- const blankPage = document.createElement("div");
- blankPage.className = "blank-page";
- blankPage.innerHTML = '<div style="page-break-after: always;"></div>';
- const tocPage = document.createElement("div");
- tocPage.className = "toc-page";
- tocPage.innerHTML = `
- <div class="toc-content">
- <h1 style="text-align: center; margin-bottom: 40px;">目录</h1>
- <div id="toc"></div>
- </div>
- `;
- // 在第一个 template-textarea 之后插入空白页和目录页
- templateTextareas[0].parentNode.insertBefore(
- tocPage,
- templateTextareas[0].nextSibling
- );
- templateTextareas[0].parentNode.insertBefore(blankPage, tocPage);
- // 生成目录内容
- let tocHtml = "";
- templateTextareas.forEach((textarea, index) => {
- const title = textarea.querySelector("h1, h2, h3, h4, h5, h6");
- if (title) {
- tocHtml += `
- <div class="toc-item">
- <span class="toc-text">${title.textContent}</span>
- <span class="toc-page-num">${index + 2}</span>
- </div>
- `;
- title.id = `section-${index + 2}`;
- }
- });
- // 将目录内容插入到目录页中
- contentClone.querySelector("#toc").innerHTML = tocHtml;
- }
- contentClone.querySelectorAll('input[type="text"]').forEach((input) => {
- const span = document.createElement("span");
- span.textContent = input.value;
- input.parentNode.replaceChild(span, input);
- });
- 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;
- }
- .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 {
- margin: 0 auto;
- max-width: 600px;
- }
- .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><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;">`);
- const res = await exportDocument({
- content: contentData,
- title: this.docAttr.title,
- });
- if (res.status != 200) {
- this.$alert(res.errMsg);
- return;
- }
- let a = document.createElement("a");
- a.href = res.data.file_path;
- a.download = res.data.file_name;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- } catch (error) {
- this.$alert("导出文档时发生错误,请稍后重试。");
- } finally {
- 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>`;
- contentData = contentData
- .replaceAll("<table ", `<table style="border-collapse: collapse;" `)
- .replaceAll("<td>", `<td style="border: 1px solid black;">`);
- exportDocument({ content: contentData, title: this.docAttr.title }).then(
- (res) => {
- if (res.status != 200) {
- this.$alert(res.errMsg);
- return;
- }
- let a = document.createElement("a");
- a.href = res.data.file_path; // Use the file_path from the response
- a.download = res.data.file_name; // Set the download attribute to the file_name
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- }
- );
- }, */
- },
- };
- </script>
- <style lang="scss">
- @import "./view.scss";
- </style>
|