马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
媒介
本次功能的引出是由于博主所做的功能业务为统计分析页面,须要将图表及分析数据导出到word文档,使体系利用职员可以在页面表现的底子上举行修改。按理说页面导出的数据是不允许修改的,以是博主向产物司理提出直接将页面导出为图片,在博主的据理力图下,乐成的被否决了,那就来好好研究一下吧
正文
在通过多方搜索以及实践之下,终于找到了办理方案,原理就是先将页面上的元素样式转换为行内样式,然后再加上可以被word文档辨认的标头,然后通过html-docx-js将html文档转为blob流,如许就可以通过点击a标签的情势将该文件导出
将页面元素样式转化为行内样式
起首,须要天生一个空iframe元素 我们利用的数据都在这个元素内,如许就不会影响页面上的数据
- window.contIframe = document.createElement('iframe');
- window.contIframe.style = 'display: none; width:100%;';
- document.body.appendChild(window.contIframe);
复制代码 然后将须要导出的元素插入到这个iframe中
- // contEl 需要导出的元素
- let cloneEl = contEl.cloneNode(true);
- cloneEl.style.width = getComputedStyle(contEl).width;
- window.contIframe.contentDocument.body.appendChild(cloneEl);
复制代码 删除埋伏元素并将元素样式转换为行内样式
- [/code] [code]let domWrap = cloneEl.cloneNode(true)
- // 1. 删除隐藏的元素, 并且将元素样式设置为行内样式, 方便word识别
- Array.from(domWrap.querySelectorAll('*')).forEach(item => {
- // 这个是需要特殊处理的元素 这个是需要将这个元素转换为canvas然后再导出,这里只提供这种思路 如若需要可以按我这个方法走,不然就直接删掉这个if分支
- if (item.className.includes('fishbone_main') && canvas) {
- item.childNodes[0].remove();
- item.appendChild(canvas);
- }
- let attr = item.getAttribute('data-toword');
- let originItem = contEl.querySelector('[data-toword="' + attr + '"]');
- if (originItem) {
- let sty = getComputedStyle(originItem);
- if (sty.display == 'none ') return item.remove();
- if (sty.opacity === '0') return item.remove();
- setStyle(item, sty);
- }
- // 当页面中存在表格时 需要稍微处理一下,因为直接导出会导致单元格过宽,本次使用的是element的table表格
- if (item.className.includes('department_content_table')) {
- const table = convertElTableToHtmlTableWord(item.childNodes[0], '');
- item.childNodes[0].remove();
- item.innerHTML = table;
- }
- });
-
- function setStyle(ele, sty) {
- if (ele.nodeName.toLowerCase() != 'img') {
- // let sty = getComputedStyle(ele)
- ele.setAttribute(
- 'style',
- (ele.getAttribute('style') || '') +
- `;font-size: ${sty.fontSize};color: ${sty.color};font-style: ${sty.fontStyle};line-height: ${sty.lineHeight};font-weight: ${sty.fontWeight};
- font-family: ${sty.fontFamily};text-align: ${sty.textAlign};text-indent: ${sty.textIndent}; margin: ${sty.margin}; padding: ${sty.padding};width: ${sty.width}; height: ${sty.height};
- white-space:${sty.whiteSpace};word-break:${sty.wordBreak};display:${sty.display}`
- );
- }
- }
- // 处理table转换为html
- function convertElTableToHtmlTableWord(elTable, title, infoLeft = '', infoRight = '') {
- if (!elTable) return '';
- // 获取 el-table 的表头数据,包括多级表头
- const theadRows = elTable.querySelectorAll('thead tr');
- // 获取 el-table 的数据行
- const tbodyRows = elTable.querySelectorAll('tbody tr');
- let length = getTotalColumnCount(theadRows[0]);
- // 开始构建 HTML 表格的字符串,设置表格整体样式和边框样式
- let htmlTable = '<table style="border-collapse: collapse; border: 1px solid black;"><thead>';
- htmlTable += '<tr>';
- if (infoRight != '') {
- htmlTable += `<th colspan="${Math.floor(length / 2)}" style="width:306.2000pt;padding:0.0000pt 5.4000pt 0.0000pt 5.4000pt ;border-left:1.0000pt solid rgb(255,255,255);
- mso-border-left-alt:0.5000pt solid rgb(255,255,255);border-right:1.0000pt solid rgb(255,255,255);mso-border-right-alt:0.5000pt solid rgb(255,255,255);
- border-top:1.0000pt solid rgb(255,255,255);mso-border-top-alt:0.5000pt solid rgb(255,255,255);border-bottom:none;
- mso-border-bottom-alt:none;background:rgb(255,255,255);text-align: right;">单位:${infoRight}</th>`;
- } else {
- htmlTable += `<th colspan="${Math.floor(length / 2)}" style="width:306.2000pt;padding:0.0000pt 5.4000pt 0.0000pt 5.4000pt ;border-left:1.0000pt solid rgb(255,255,255);
- mso-border-left-alt:0.5000pt solid rgb(255,255,255);border-right:1.0000pt solid rgb(255,255,255);mso-border-right-alt:0.5000pt solid rgb(255,255,255);
- border-top:1.0000pt solid rgb(255,255,255);mso-border-top-alt:0.5000pt solid rgb(255,255,255);border-bottom:none;
- mso-border-bottom-alt:none;background:rgb(255,255,255);text-align: right;"></th>`;
- }
- htmlTable += '</tr>';
- // 处理多级表头
- theadRows.forEach(row => {
- htmlTable += '<tr>';
- const columns = row.querySelectorAll('th');
- columns.forEach(column => {
- const colspan = column.getAttribute('colspan') || '1';
- const rowspan = column.getAttribute('rowspan') || '1';
- htmlTable += `<th colspan="${colspan}" rowspan="${rowspan}" style="border: 1px solid black;">${column.innerText}</th>`;
- });
- htmlTable += '</tr>';
- });
- htmlTable += '</thead><tbody>';
- // 构建数据行
- tbodyRows.forEach(row => {
- htmlTable += '<tr>';
- const cells = row.querySelectorAll('td');
- cells.forEach(cell => {
- if (cell.querySelector('div')) {
- htmlTable += `<td style="border: 1px solid black;">${cell.querySelector('div').innerHTML}</td>`;
- } else {
- htmlTable += `<td style="border: 1px solid black;">${cell.innerText}</td>`;
- }
- });
- htmlTable += '</tr>';
- });
- htmlTable += '</tbody></table>';
- return htmlTable;
- }
复制代码
如许整个html文档的样式就转换为行内样式了
下面就须要将图片转换为base64用于导出
- let imgList = domWrap.querySelectorAll('img');
- console.log('加载图片数量: ', imgList.length);
- await Promise.all(
- Array.from(imgList)
- .filter(x => !x.src.startsWith('data'))
- .map(tempimg => {
- let img = new Image();
- img.setAttribute('crossOrigin', 'anonymous');
- img.src = options.proxyHost ? tempimg.src.replace(location.host, options.proxyHost) : tempimg.src;
- return new Promise((resolve, reject) => {
- try {
- img.onload = function() {
- img.onload = null;
- const cw = Math.min(img.width, options.maxWidth);
- const ch = img.height * (cw / img.width);
- const canvas = document.createElement('CANVAS');
- canvas.width = cw;
- canvas.height = ch;
- const context = canvas.getContext('2d');
- context?.drawImage(img, 0, 0, cw, ch);
- const uri = canvas.toDataURL('image/jpg', 0.8);
- tempimg.src = uri;
- const w = Math.min(img.width, 550, options.maxWidth); // word图片最大宽度
- tempimg.width = w;
- tempimg.height = img.height * (w / img.width);
- console.log('img onload...', options.fileName, img.src, img.width, img.height, cw, ch, w, tempimg.height);
- canvas.remove();
- resolve(img.src);
- };
- img.onerror = function() {
- console.log('img load error, ', img.src);
- resolve('');
- };
- } catch (e) {
- console.log(e);
- resolve('');
- }
- });
- })
- );
复制代码 还须要将canvas转换为 base64,实在可以利用这一步将页面中不想让用户更改的元素先转化为canvas,然后再导出
- [/code] [code] let canvasList = domWrap.querySelectorAll('canvas');
- console.log('加载canvas数量: ', canvasList.length);
- await Promise.all(
- Array.from(canvasList).map(tempCanvas => {
- let img = new Image();
- img.setAttribute('crossOrigin', 'anonymous');
- return new Promise((resolve, reject) => {
- try {
- let attr = tempCanvas.getAttribute('data-toword');
- let cvs = contEl.querySelector('[data-toword="' + attr + '"]');
- // 由于该canvas是我再导出之前生成,所以需要单独处理一下
- if (!cvs && tempCanvas.className === 'fishbone_canvas') {
- cvs = tempCanvas;
- }
- if (!cvs) return resolve();
- img.src = cvs.toDataURL('image/jpg', 0.8);
- const w = Math.min(cvs.width, options.maxWidth);
- const h = cvs.height * (w / cvs.width);
- img.width = w;
- img.height = h;
- const parent = tempCanvas.parentNode;
- if (tempCanvas.nextSibling) {
- parent.insertBefore(img, tempCanvas.nextSibling);
- } else {
- parent.appendChild(img);
- }
- tempCanvas.remove();
- resolve('');
- } catch (e) {
- console.log(e);
- resolve('');
- }
- });
- })
- );
复制代码 至此准备工作完全竣事 下面就须要将文件导出为word文档 起首须要引入html-docx-js包
- [/code] [code] const htmlContent = domWrap.innerHTML;
- // 将html数据转化为blob
- const docxBlob = htmlDocx.asBlob(htmlContent, { tableCellMargin: 0 });
- console.log('即将生成文件大小: ', docxBlob.size, (docxBlob.size / 1024 / 1024).toFixed(2) + 'M');
- // 移除iframe内部元素, 方便下次导出
- if (!window.devs) domWrap.remove();
- saveAs(docxBlob, options.fileName + '.docx');
-
- //--------------
-
- function saveAs(blob, fileName) {
- var URL = window.URL || window.webkitURL;
- var a = document.createElement('a');
- fileName = fileName || blob.name || 'download';
- a.download = fileName;
- a.rel = 'noopener';
- a.target = '_blank';
- if (typeof blob === 'string') {
- a.href = blob;
- a.click();
- } else {
- a.href = URL.createObjectURL(blob);
- setTimeout(() => a.click(), 0);
- setTimeout(() => URL.revokeObjectURL(a.href), 2e4); // 20s
- }
- }
复制代码
报错题目
- With statements cannot be used with the "esm" output format due to strict mode
复制代码 博主在利用html-docx-js包时发现了报错题目导致整个流程卡在了末了一步,在颠末长达一天的研究之后,发现可以通过script方式引入 第一步 先将包下载到当地 npm install html-docx-js 将包内关键js文件拿到当地
博主是拿出之后并修改了名字, 然后在index.html文件中导入该js文件 肯定要在head标签内导入 否则是不收效的
自此导出完毕
效果展示
完备代码
toWord.js完备代码
- import { formatDate, setStyle, saveAs, convertElTableToHtmlTableWord } from './util';// maxWidth?: number, title?: string, fileName?: string, time?: string, proxyHost?: string, exclude?: arrayexport async function toWord(contEl, option, canvas) { let options = Object.assign( { fileName: `word_${formatDate('yyyy-MM-dd hh:mm:ss')}`, // 导出文件名 maxWidth: 550, // 图片最大宽度, title: '', // 导出添加一级标题 time: '', // 导出添加文章时间 blob: false, // 返回效果为blob exclude: [] // 清除元素选择器 }, option || {} ); if (!contEl) return console.warn('未传入导出元素'); if (typeof contEl === 'string') contEl = document.getElementById(contEl) || document.querySelector(contEl); // 设置标记, 方便复制样式 Array.from(contEl.querySelectorAll('*')).forEach(item => { item.setAttribute( 'data-toword', Math.random() .toString(32) .slice(-5) ); }); if (!window.contIframe) { window.contIframe = document.createElement('iframe');
- window.contIframe.style = 'display: none; width:100%;';
- document.body.appendChild(window.contIframe);
- } let cloneEl = contEl.cloneNode(true); cloneEl.style.width = getComputedStyle(contEl).width; window.contIframe.contentDocument.body.appendChild(cloneEl); let domWrap = cloneEl; // .cloneNode(true) // 1. 删除埋伏的元素, 而且将元素样式设置为行内样式, 方便word辨认 Array.from(domWrap.querySelectorAll('*')).forEach(item => { if (item.className.includes('fishbone_main') && canvas) { item.childNodes[0].remove(); item.appendChild(canvas); } let attr = item.getAttribute('data-toword'); let originItem = contEl.querySelector('[data-toword="' + attr + '"]'); if (originItem) { let sty = getComputedStyle(originItem); if (sty.display == 'none ') return item.remove(); if (sty.opacity === '0') return item.remove(); setStyle(item, sty); } if (item.className.includes('department_content_table')) { const table = convertElTableToHtmlTableWord(item.childNodes[0], ''); item.childNodes[0].remove(); item.innerHTML = table; } }); // // 1.1 删除清除的元素 if (Array.isArray(options.exclude) && options.exclude.length) { options.exclude.forEach(ext => { Array.from(domWrap.querySelectorAll(ext)).forEach(item => item.remove()); }); } // 2. 将图片转为Base64编码, 方便word生存 let imgList = domWrap.querySelectorAll('img');
- console.log('加载图片数量: ', imgList.length);
- await Promise.all(
- Array.from(imgList)
- .filter(x => !x.src.startsWith('data'))
- .map(tempimg => {
- let img = new Image();
- img.setAttribute('crossOrigin', 'anonymous');
- img.src = options.proxyHost ? tempimg.src.replace(location.host, options.proxyHost) : tempimg.src;
- return new Promise((resolve, reject) => {
- try {
- img.onload = function() {
- img.onload = null;
- const cw = Math.min(img.width, options.maxWidth);
- const ch = img.height * (cw / img.width);
- const canvas = document.createElement('CANVAS');
- canvas.width = cw;
- canvas.height = ch;
- const context = canvas.getContext('2d');
- context?.drawImage(img, 0, 0, cw, ch);
- const uri = canvas.toDataURL('image/jpg', 0.8);
- tempimg.src = uri;
- const w = Math.min(img.width, 550, options.maxWidth); // word图片最大宽度
- tempimg.width = w;
- tempimg.height = img.height * (w / img.width);
- console.log('img onload...', options.fileName, img.src, img.width, img.height, cw, ch, w, tempimg.height);
- canvas.remove();
- resolve(img.src);
- };
- img.onerror = function() {
- console.log('img load error, ', img.src);
- resolve('');
- };
- } catch (e) {
- console.log(e);
- resolve('');
- }
- });
- })
- );
- // 3. 将canvas转为Base64编码, 方便word生存 let canvasList = domWrap.querySelectorAll('canvas'); console.log('加载canvas数目: ', canvasList.length); await Promise.all( Array.from(canvasList).map(tempCanvas => { let img = new Image(); img.setAttribute('crossOrigin', 'anonymous'); return new Promise((resolve, reject) => { try { let attr = tempCanvas.getAttribute('data-toword'); let cvs = contEl.querySelector('[data-toword="' + attr + '"]'); if (!cvs && tempCanvas.className === 'fishbone_canvas') { cvs = tempCanvas; } if (!cvs) return resolve(); img.src = cvs.toDataURL('image/jpg', 0.8); const w = Math.min(cvs.width, options.maxWidth); const h = cvs.height * (w / cvs.width); img.width = w; img.height = h; const parent = tempCanvas.parentNode; if (tempCanvas.nextSibling) { parent.insertBefore(img, tempCanvas.nextSibling); } else { parent.appendChild(img); } tempCanvas.remove(); resolve(''); } catch (e) { console.log(e); resolve(''); } }); }) ); Array.from(contEl.querySelectorAll('*')).forEach(item => { item.removeAttribute('data-toword'); }); const htmlContent = domWrap.innerHTML; const docxBlob = htmlDocx.asBlob(htmlContent, { tableCellMargin: 0 }); console.log('即将天生文件巨细: ', docxBlob.size, (docxBlob.size / 1024 / 1024).toFixed(2) + 'M'); // 移除iframe内部元素, 方便下次导出 if (!window.devs) domWrap.remove(); saveAs(docxBlob, options.fileName + '.docx');}
复制代码
util完备代码
- [/code] [code]export function formatDate(fmt, date) {
- if (!date) date = new Date();
- if (!fmt) fmt = 'yyyy-MM-dd hh:mm:ss';
- if (/(y+)/.test(fmt)) {
- fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
- }
- const o = {
- 'M+': date.getMonth() + 1,
- 'd+': date.getDate(),
- 'h+': date.getHours(),
- 'm+': date.getMinutes(),
- 's+': date.getSeconds()
- };
- for (const k in o) {
- if (new RegExp(`(${k})`).test(fmt)) {
- const str = o[k] + '';
- fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? str : ('00' + str).substr(str.length));
- }
- }
- return fmt;
- }
- export function setStyle(ele, sty) {
- if (ele.nodeName.toLowerCase() != 'img') {
- // let sty = getComputedStyle(ele)
- ele.setAttribute(
- 'style',
- (ele.getAttribute('style') || '') +
- `;font-size: ${sty.fontSize};color: ${sty.color};font-style: ${sty.fontStyle};line-height: ${sty.lineHeight};font-weight: ${sty.fontWeight};
- font-family: ${sty.fontFamily};text-align: ${sty.textAlign};text-indent: ${sty.textIndent}; margin: ${sty.margin}; padding: ${sty.padding};width: ${sty.width}; height: ${sty.height};
- white-space:${sty.whiteSpace};word-break:${sty.wordBreak};display:${sty.display}`
- );
- }
- }
- export function saveAs(blob, fileName) {
- var URL = window.URL || window.webkitURL;
- var a = document.createElement('a');
- fileName = fileName || blob.name || 'download';
- a.download = fileName;
- a.rel = 'noopener';
- a.target = '_blank';
- if (typeof blob === 'string') {
- a.href = blob;
- a.click();
- } else {
- a.href = URL.createObjectURL(blob);
- setTimeout(() => a.click(), 0);
- setTimeout(() => URL.revokeObjectURL(a.href), 2e4); // 20s
- }
- }
- //导出word e-table转html
- export function convertElTableToHtmlTableWord(elTable, title, infoLeft = '', infoRight = '') {
- if (!elTable) return '';
- // 获取 el-table 的表头数据,包括多级表头
- const theadRows = elTable.querySelectorAll('thead tr');
- // 获取 el-table 的数据行
- const tbodyRows = elTable.querySelectorAll('tbody tr');
- let length = getTotalColumnCount(theadRows[0]);
- // 开始构建 HTML 表格的字符串,设置表格整体样式和边框样式
- let htmlTable = '<table style="border-collapse: collapse; border: 1px solid black;"><thead>';
- htmlTable += '<tr>';
- if (infoRight != '') {
- htmlTable += `<th colspan="${Math.floor(length / 2)}" style="width:306.2000pt;padding:0.0000pt 5.4000pt 0.0000pt 5.4000pt ;border-left:1.0000pt solid rgb(255,255,255);
- mso-border-left-alt:0.5000pt solid rgb(255,255,255);border-right:1.0000pt solid rgb(255,255,255);mso-border-right-alt:0.5000pt solid rgb(255,255,255);
- border-top:1.0000pt solid rgb(255,255,255);mso-border-top-alt:0.5000pt solid rgb(255,255,255);border-bottom:none;
- mso-border-bottom-alt:none;background:rgb(255,255,255);text-align: right;">单位:${infoRight}</th>`;
- } else {
- htmlTable += `<th colspan="${Math.floor(length / 2)}" style="width:306.2000pt;padding:0.0000pt 5.4000pt 0.0000pt 5.4000pt ;border-left:1.0000pt solid rgb(255,255,255);
- mso-border-left-alt:0.5000pt solid rgb(255,255,255);border-right:1.0000pt solid rgb(255,255,255);mso-border-right-alt:0.5000pt solid rgb(255,255,255);
- border-top:1.0000pt solid rgb(255,255,255);mso-border-top-alt:0.5000pt solid rgb(255,255,255);border-bottom:none;
- mso-border-bottom-alt:none;background:rgb(255,255,255);text-align: right;"></th>`;
- }
- htmlTable += '</tr>';
- // 处理多级表头
- theadRows.forEach(row => {
- htmlTable += '<tr>';
- const columns = row.querySelectorAll('th');
- columns.forEach(column => {
- const colspan = column.getAttribute('colspan') || '1';
- const rowspan = column.getAttribute('rowspan') || '1';
- htmlTable += `<th colspan="${colspan}" rowspan="${rowspan}" style="border: 1px solid black;">${column.innerText}</th>`;
- });
- htmlTable += '</tr>';
- });
- htmlTable += '</thead><tbody>';
- // 构建数据行
- tbodyRows.forEach(row => {
- htmlTable += '<tr>';
- const cells = row.querySelectorAll('td');
- cells.forEach(cell => {
- if (cell.querySelector('div')) {
- htmlTable += `<td style="border: 1px solid black;">${cell.querySelector('div').innerHTML}</td>`;
- } else {
- htmlTable += `<td style="border: 1px solid black;">${cell.innerText}</td>`;
- }
- });
- htmlTable += '</tr>';
- });
- htmlTable += '</tbody></table>';
- return htmlTable;
- }
- //获取总列数
- function getTotalColumnCount(theadRows) {
- let rowColumns = 0;
- const columns = theadRows.querySelectorAll('th');
- columns.forEach(column => {
- const colspan = column.getAttribute('colspan') || 1;
- rowColumns += Number(colspan);
- });
- return rowColumns;
- }
复制代码
结语
本文竣事,如果有更好的导出方式接待批评区讨论
原文:
|