<template>
|
<div>
|
<h3>前端 Word 编辑器(TinyMCE)</h3>
|
<editor v-model="content" :init="editorInit" :api-key="apiKey"></editor>
|
<div class="actions">
|
<button @click="exportToWord">导出 Word</button>
|
<button @click="exportToPDF">导出 PDF</button>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import Editor from '@tinymce/tinymce-vue';
|
import { Document, Packer, Paragraph, TextRun, ImageRun, Table as DocxTable, TableRow as DocxTableRow, TableCell as DocxTableCell } from 'docx';
|
import { saveAs } from 'file-saver';
|
import jsPDF from 'jspdf';
|
import html2canvas from 'html2canvas';
|
|
export default {
|
components: { Editor },
|
data() {
|
return {
|
apiKey: '3ceaxd5ckw4te35xj38vj3p5rmmeyv0x8pq2yrr92rwdiqzp', // 您的 TinyMCE API 密钥
|
content: `
|
|
`,
|
editorInit: {
|
height: 500,
|
menubar: true,
|
plugins: [
|
'advlist autolink lists link image charmap print preview anchor',
|
'searchreplace visualblocks code fullscreen',
|
'insertdatetime media table paste code help wordcount'
|
],
|
toolbar:
|
'undo redo | formatselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image table | removeformat | help',
|
images_upload_handler: (blobInfo, success, failure) => {
|
const reader = new FileReader();
|
reader.onload = () => success(reader.result);
|
reader.onerror = () => failure('图片上传失败');
|
reader.readAsDataURL(blobInfo.blob());
|
},
|
content_style: 'body { font-family: SimSun, Arial; font-size: 14px; }',
|
table_default_attributes: { border: 1 },
|
readonly: false,
|
language: 'zh_CN', // 尝试 zh-CN
|
},
|
};
|
},
|
methods: {
|
exportToWord() {
|
this.parseHtmlToDocx(this.content).then((elements) => {
|
const doc = new Document({
|
sections: [{ properties: {}, children: elements }],
|
});
|
|
Packer.toBlob(doc).then((blob) => {
|
saveAs(blob, '文档.docx');
|
}).catch((err) => {
|
console.error('生成 Word 失败:', err);
|
});
|
}).catch((err) => {
|
console.error('解析 HTML 失败:', err);
|
});
|
},
|
parseHtmlToDocx(html) {
|
return new Promise((resolve, reject) => {
|
const parser = new DOMParser();
|
const doc = parser.parseFromString(html, 'text/html');
|
const elements = [];
|
|
const processNode = (node, callback) => {
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
if (node.tagName === 'P') {
|
const textRuns = Array.from(node.childNodes).map((child) => {
|
if (child.nodeType === Node.TEXT_NODE) {
|
return new TextRun({ text: child.textContent || '' });
|
} else if (child.tagName === 'B') {
|
return new TextRun({ text: child.textContent || '', bold: true });
|
} else if (child.tagName === 'I') {
|
return new TextRun({ text: child.textContent || '', italics: true });
|
} else if (child.tagName === 'IMG') {
|
this.getImageBase64FromUrl(child.src).then((base64) => {
|
callback(null, new ImageRun({
|
data: base64,
|
transformation: { width: 200, height: 200 },
|
}));
|
}).catch((err) => {
|
console.error('图片加载失败:', err);
|
callback(null, new TextRun(`[图片: ${child.alt || '图像'}]`));
|
});
|
return null;
|
}
|
return new TextRun({ text: child.textContent || '' });
|
}).filter(Boolean);
|
if (textRuns.length) {
|
elements.push(new Paragraph({ children: textRuns }));
|
}
|
} else if (node.tagName === 'H1') {
|
elements.push(new Paragraph({ text: node.textContent || '', heading: 'Heading1' }));
|
} else if (node.tagName === 'UL' || node.tagName === 'OL') {
|
Array.from(node.children).forEach((li) => {
|
elements.push(
|
new Paragraph({
|
text: li.textContent || '',
|
bullet: node.tagName === 'UL' ? { level: 0 } : { level: 0, number: true },
|
})
|
);
|
});
|
} else if (node.tagName === 'TABLE') {
|
const rows = Array.from(node.querySelectorAll('tr')).map((tr) => {
|
const cells = Array.from(tr.querySelectorAll('td, th')).map((cell) => {
|
return new DocxTableCell({
|
children: [new Paragraph(cell.textContent || '')],
|
});
|
});
|
return new DocxTableRow({ children: cells });
|
});
|
elements.push(new DocxTable({ rows }));
|
}
|
Array.from(node.children).forEach((child) => processNode(child, callback));
|
}
|
callback(null);
|
};
|
|
const nodes = Array.from(doc.body.childNodes);
|
let completed = 0;
|
const checkCompletion = () => {
|
completed++;
|
if (completed === nodes.length) {
|
resolve(elements);
|
}
|
};
|
|
if (nodes.length === 0) {
|
resolve(elements);
|
} else {
|
nodes.forEach((node) => {
|
processNode(node, checkCompletion);
|
});
|
}
|
});
|
},
|
getImageBase64FromUrl(url) {
|
return new Promise((resolve, reject) => {
|
fetch(url, { mode: 'cors' })
|
.then((response) => {
|
if (!response.ok) throw new Error('网络图片加载失败');
|
return response.blob();
|
})
|
.then((blob) => {
|
const reader = new FileReader();
|
reader.onloadend = () => resolve(reader.result.split(',')[1]);
|
reader.onerror = () => reject(new Error('图片读取失败'));
|
reader.readAsDataURL(blob);
|
})
|
.catch((err) => reject(err));
|
});
|
},
|
exportToPDF() {
|
const contentElement = document.createElement('div');
|
contentElement.innerHTML = this.content;
|
contentElement.style.padding = '10px';
|
document.body.appendChild(contentElement);
|
|
html2canvas(contentElement, { scale: 2 })
|
.then((canvas) => {
|
const doc = new jsPDF();
|
const imgData = canvas.toDataURL('image/png');
|
doc.addImage(imgData, 'PNG', 10, 10, 190, 0);
|
doc.save('文档.pdf');
|
})
|
.catch((err) => {
|
console.error('生成 PDF 失败:', err);
|
document.body.removeChild(contentElement);
|
});
|
},
|
},
|
};
|
</script>
|
|
<style>
|
.actions {
|
margin-top: 10px;
|
}
|
.actions button {
|
padding: 8px 16px;
|
margin-right: 10px;
|
background-color: #409eff;
|
color: white;
|
border: none;
|
border-radius: 4px;
|
cursor: pointer;
|
}
|
.actions button:hover {
|
background-color: #66b1ff;
|
}
|
</style>
|