vue3渲染html数据并实现文本修改
需求:从接口获取到一个完整的html代码数据渲染到页面展示
实现:
- 使用vue3的v-html来渲染数据,发现完整的html数据中有一些外链标签未能加载
- 使用iframe嵌套实现渲染
- 文本修改使用属性
contentEditable = true
- 导出页面为图片:使用
html2canvas
实现
代码
<template><div class="doubang-editor-page"><div class="html-preview"><iframe ref="previewIframe" class="preview-iframe" sandbox="allow-same-origin allow-scripts allow-forms"></iframe></div><div class="actions"><el-button type="success" @click="getHtml">获取修改后的HTML</el-button><!-- @click="exportAsImage" --><el-button type="primary" :loading="isExporting">{{ isExporting ? '导出中...' : '导出为图片' }}</el-button></div></div>
</template><script setup lang="ts">
import { ref, reactive, watch, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { htmlData } from './data'
import html2canvas from 'html2canvas';const html = ref(htmlData);
const previewIframe = ref<HTMLIFrameElement | null>(null);
const isExporting = ref(false);// 更新iframe内容的函数
const updateIframeContent = () => {if (previewIframe.value) {const iframe = previewIframe.value;const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;if (iframeDoc) {// 添加编辑样式const editableStyle = `<style>*{cursor: default;}*[contenteditable=true] {outline: none;box-sizing: border-box;border: 1px dashed #ccc;}</style>`;// border: none;// 将样式添加到HTML内容中const htmlWithStyle = html.value.replace('</head>', `${editableStyle}</head>`);iframeDoc.open();iframeDoc.write(htmlWithStyle);iframeDoc.close();// 等待iframe内容加载完成后添加事件监听器setTimeout(() => {// 获取顶级节点bg-neutralconst bgNeutral = iframeDoc.querySelector('main.container');if (bgNeutral) {bgNeutral.addEventListener('click', function (e: any) {e.preventDefault();console.log('点击节点标签:', e.target.localName);if (e.target.localName !== 'img') {// 设置可编辑e.target.contentEditable = true;e.target.focus();// 失焦时e.target.addEventListener('blur', function () {e.target.contentEditable = false; // 设置不可编辑// 删除contentEditable属性e.target.removeAttribute('contenteditable');});} else {// 获取随机数获取图片e.target.src = 'https://picsum.photos/200/200?random=' + Math.floor(Math.random() * 1000);}if (e.target.classList.contains('child')) {console.log('子元素被点击:', e.target.textContent);}});} else {console.error('未找到 main.container 元素');}}, 500); // 给予足够的时间让iframe内容加载完成}}
};// 在组件挂载后更新iframe内容
onMounted(() => {updateIframeContent();
});// 当html内容变化时更新iframe
watch(html, () => {updateIframeContent();
});// 保存修改后的HTML内容
const getHtml = () => {if (previewIframe.value) {const iframe = previewIframe.value;const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;if (iframeDoc) {// 获取修改后的HTML内容const modifiedHtml = iframeDoc.documentElement.outerHTML;console.log('修改后的HTML内容:', modifiedHtml);// 更新html引用html.value = modifiedHtml;}}
};// 导出main标签内容为图片
const exportAsImage = async () => {if (previewIframe.value) {const iframe = previewIframe.value;const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;if (iframeDoc) {try {isExporting.value = true;ElMessage.info('正在导出图片,请稍候...');// 获取main.container元素const mainElement:any = iframeDoc.querySelector('main.container');if (!mainElement) {ElMessage.error('未找到main.container元素');isExporting.value = false;return;}// 保存原始样式const originalStyles = window.getComputedStyle(mainElement);const originalBackgroundColor = originalStyles.backgroundColor;// 确保背景色被正确应用const parentElement = mainElement.parentElement;const parentBackgroundColor = parentElement ? window.getComputedStyle(parentElement).backgroundColor : null;// 使用html2canvas将main元素转换为canvasconst canvas = await html2canvas(mainElement, {allowTaint: true,useCORS: true,scale: 2, // 提高图片质量backgroundColor: originalBackgroundColor !== 'rgba(0, 0, 0, 0)' ? originalBackgroundColor : parentBackgroundColor,logging: false,onclone: (clonedDoc) => {// 在克隆的文档中确保所有元素都保留其背景色const clonedMain:any = clonedDoc.querySelector('main.container');if (clonedMain) {// 确保背景色被保留if (originalBackgroundColor === 'rgba(0, 0, 0, 0)' || originalBackgroundColor === 'transparent') {clonedMain.style.backgroundColor = parentBackgroundColor || '#ffffff';}// 递归设置所有子元素的背景色,如果它们是透明的const applyBackgroundToTransparentElements = (element:any) => {const children = element.children;for (let i = 0; i < children.length; i++) {const child = children[i];const childStyle = window.getComputedStyle(child);if (childStyle.backgroundColor === 'rgba(0, 0, 0, 0)' || childStyle.backgroundColor === 'transparent') {// 只有当元素背景是透明的时候才设置背景色child.style.backgroundColor = window.getComputedStyle(child.parentElement).backgroundColor || '#ffffff';}if (child.children.length > 0) {applyBackgroundToTransparentElements(child);}}};applyBackgroundToTransparentElements(clonedMain);}}});// 将canvas转换为图片URLconst imageUrl = canvas.toDataURL('image/png');// 创建下载链接const downloadLink = document.createElement('a');downloadLink.href = imageUrl;downloadLink.download = 'page-content.png';// 触发下载document.body.appendChild(downloadLink);downloadLink.click();document.body.removeChild(downloadLink);ElMessage.success('图片导出成功');} catch (error) {console.error('导出图片失败:', error);ElMessage.error('导出图片失败: ' + (error as Error).message);} finally {isExporting.value = false;}}}
};
</script><style scoped>
.doubang-editor-page {display: flex;flex-direction: column;height: calc(100vh - 100px);padding: 20px;box-sizing: border-box;border: 1px solid #eee;
}.html-preview {flex: 1;margin-bottom: 20px;border: 1px solid #dcdfe6;border-radius: 4px;overflow: hidden;
}.preview-iframe {width: 100%;height: 100%;border: none;
}.actions {display: flex;justify-content: flex-end;padding: 10px 0;
}
</style>
data.ts文件
接口返回的就是一个完整的html字符串,这里使用假数据模拟。
export const htmlData = `
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>白居易的小红书</title><script src="https://cdn.tailwindcss.com"></script><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"><script>tailwind.config = {theme: {extend: {colors: {primary: '#FF2442',secondary: '#FFD8CC',neutral: '#F8F8F8',dark: '#333333',},fontFamily: {sans: ['Inter', 'system-ui', 'sans-serif'],serif: ['Noto Serif SC', 'serif'],},},}}</script><style type="text/tailwindcss">@layer utilities {.content-auto {content-visibility: auto;}.text-shadow {text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);}.bg-blur {backdrop-filter: blur(8px);}}</style>
</head><body class="bg-neutral font-sans text-dark"><!-- 顶部导航栏 --><header class="sticky top-0 bg-white/80 bg-blur z-50 border-b border-gray-200"><div class="container mx-auto px-4 py-3 flex items-center justify-between"><div class="flex items-center space-x-2"><i class="fa fa-arrow-left text-lg"></i><h1 class="font-bold text-lg">发现</h1></div><div class="flex items-center space-x-4"><i class="fa fa-search text-lg"></i><i class="fa fa-share-alt text-lg"></i></div></div></header><main class="container mx-auto px-4 py-6"><!-- 文章内容区 --><article class="bg-white rounded-xl shadow-sm overflow-hidden mb-6"><!-- 文章标题区 --><div class="p-5"><div class="flex items-center space-x-3 mb-4"><img src="https://picsum.photos/seed/baijuyi/200/200" alt="白居易头像"class="w-12 h-12 rounded-full object-cover border-2 border-primary"><div><h2 class="font-bold text-lg">白居易</h2><p class="text-gray-500 text-sm">唐代现实主义诗人</p></div><buttonclass="ml-auto bg-primary text-white px-4 py-1.5 rounded-full text-sm font-medium hover:bg-primary/90 transition">关注</button></div><h3 class="text-2xl font-bold mb-4">《赋得古原草送别》背后的故事</h3><p class="text-gray-700 mb-4 leading-relaxed">离离原上草,一岁一枯荣。<br>野火烧不尽,春风吹又生。<br>远芳侵古道,晴翠接荒城。<br>又送王孙去,萋萋满别情。</p><p class="text-gray-700 mb-4 leading-relaxed">今天想和大家分享这首我十六岁时写的《赋得古原草送别》背后的故事。当年我初到长安,拿着这首诗去拜见顾况大人,他看到"野火烧不尽,春风吹又生"时,不禁赞叹:"有句如此,居天下亦不难"。</p><p class="text-gray-700 mb-4 leading-relaxed">其实这首诗是我在郊外看到草原的景象,有感而发。草的生命力如此顽强,即使被野火焚烧,来年春天依旧能焕发生机。这让我想到人生,无论遇到多少挫折,只要心中有希望,就一定能重新站起来。</p><p class="text-gray-700 mb-4 leading-relaxed">诗的最后两句"又送王孙去,萋萋满别情",则表达了我对友人的不舍之情。就像这草原上的草一样,虽然我们暂时分离,但友情永远不会断绝。</p><div class="flex flex-wrap gap-2 mb-4"><span class="bg-secondary/50 text-primary px-3 py-1 rounded-full text-sm">#唐诗</span><span class="bg-secondary/50 text-primary px-3 py-1 rounded-full text-sm">#白居易</span><span class="bg-secondary/50 text-primary px-3 py-1 rounded-full text-sm">#123</span><span class="bg-secondary/50 text-primary px-3 py-1 rounded-full text-sm">#送别</span></div></div><!-- 文章图片区 --><div class="grid grid-cols-2 gap-1"><img src="https://picsum.photos/seed/grass1/800/800" alt="草原春景" class="w-full h-64 object-cover"><img src="https://picsum.photos/seed/grass2/800/800" alt="草原秋景" class="w-full h-64 object-cover"><img src="https://picsum.photos/seed/grass3/800/800" alt="草原雪景" class="w-full h-64 object-cover"><img src="https://picsum.photos/seed/grass4/800/800" alt="古道边的草原" class="w-full h-64 object-cover"></div><!-- 文章互动区 --><div class="p-4 flex items-center justify-between border-t border-gray-100"><div class="flex items-center space-x-6"><button class="flex items-center space-x-1 text-gray-500 hover:text-primary transition"><i class="fa fa-heart-o text-xl"></i><span>1.2w</span></button><button class="flex items-center space-x-1 text-gray-500 hover:text-primary transition"><i class="fa fa-comment-o text-xl"></i><span>328</span></button></div><div class="flex items-center space-x-6"><button class="flex items-center space-x-1 text-gray-500 hover:text-primary transition"><i class="fa fa-bookmark-o text-xl"></i></button><button class="flex items-center space-x-1 text-gray-500 hover:text-primary transition"><i class="fa fa-share text-xl"></i></button></div></div><!-- 评论预览区 --><div class="p-4 bg-gray-50"><h4 class="font-bold mb-3">精选评论</h4><div class="space-y-4"><div class="flex space-x-3"><img src="https://picsum.photos/seed/user1/200/200" alt="李白头像" class="w-8 h-8 rounded-full object-cover"><div class="flex-1"><div class="flex items-center justify-between"><h5 class="font-medium">李白</h5><span class="text-xs text-gray-500">1小时前</span></div><p class="text-sm mt-1">野火烧不尽,春风吹又生。真乃千古名句!</p><div class="flex items-center space-x-4 mt-2"><button class="text-xs text-gray-500 hover:text-primary transition"><i class="fa fa-heart-o"></i> 89</button><button class="text-xs text-gray-500 hover:text-primary transition">回复</button></div></div></div><div class="flex space-x-3"><img src="https://picsum.photos/seed/user2/200/200" alt="杜甫头像" class="w-8 h-8 rounded-full object-cover"><div class="flex-1"><div class="flex items-center justify-between"><h5 class="font-medium">杜甫</h5><span class="text-xs text-gray-500">3小时前</span></div><p class="text-sm mt-1">乐天兄的诗,总能以景喻情,意境深远。这首送别诗更是感人至深。</p><div class="flex items-center space-x-4 mt-2"><button class="text-xs text-gray-500 hover:text-primary transition"><i class="fa fa-heart-o"></i> 124</button><button class="text-xs text-gray-500 hover:text-primary transition">回复</button></div></div></div></div><buttonclass="w-full mt-4 py-2 text-center text-primary text-sm border border-primary/30 rounded-lg hover:bg-primary/5 transition">查看全部1222222条评论</button></div></article><!-- 推荐文章区 --><section class="mb-8"><h3 class="font-bold text-xl mb-4">推荐阅读2</h3><div class="grid grid-cols-1 md:grid-cols-2 gap-4"><a href="#" class="bg-white rounded-xl shadow-sm overflow-hidden hover:shadow-md transition"><img src="https://picsum.photos/seed/poem1/800/500" alt="《长恨歌》赏析" class="w-full h-48 object-cover"><div class="p-4"><h4 class="font-bold text-lg mb-2">《长恨歌》背后的爱情故事</h4><p class="text-gray-600 text-sm line-clamp-2">杨家有女初长成,养在深闺人未识。天生丽质难自弃,一朝选在君王侧...</p><div class="flex items-center justify-between mt-3"><div class="flex items-center space-x-2"><img src="https://picsum.photos/seed/baijuyi/200/200" alt="白居易头像"class="w-6 h-6 rounded-full object-cover"><span class="text-xs text-gray-500">白居易</span></div><div class="flex items-center space-x-3 text-xs text-gray-500"><span><i class="fa fa-heart-o"></i> 8.5k</span><span><i class="fa fa-comment-o"></i> 215</span></div></div></div></a><a href="#" class="bg-white rounded-xl shadow-sm overflow-hidden hover:shadow-md transition"><imgsrc="https://p9-flow-imagex-sign.byteimg.com/tos-cn-i-a9rns2rl98/rc/pc/code_assistant/24f7bcfe7e854ba99dc87248c7c5d50d~tplv-a9rns2rl98-image.image?rcl=202508011519593DB1652542B0563479E9&rk3s=8e244e95&rrcfp=e75484ac&x-expires=1754637600&x-signature=V9q2W3TWzqi3ONsIxsoZzG1tvPA%3D"alt="《琵琶行》创作背景" class="w-full h-48 object-cover"><div class="p-4"><h4 class="font-bold text-lg mb-2">《琵琶行》创作背后的心酸</h4><p class="text-gray-600 text-sm line-clamp-2">浔阳江头夜送客,枫叶荻花秋瑟瑟。主人下马客在船,举酒欲饮无管弦...</p><div class="flex items-center justify-between mt-3"><div class="flex items-center space-x-2"><img src="https://picsum.photos/seed/baijuyi/200/200" alt="白居易头像"class="w-6 h-6 rounded-full object-cover"><span class="text-xs text-gray-500">白居易</span></div><div class="flex items-center space-x-3 text-xs text-gray-500"><span><i class="fa fa-heart-o"></i> 6.3k</span><span><i class="fa fa-comment-o"></i> 187</span></div></div></div></a></div></section></main><!-- 底部评论区 --><footer class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 p-3"><div class="flex items-center space-x-3"><img src="https://picsum.photos/seed/user/200/200" alt="当前用户头像" class="w-8 h-8 rounded-full object-cover"><div class="flex-1 relative"><input type="text" placeholder="写下你的评论..."class="w-full bg-gray-100 rounded-full py-2 px-4 pr-10 text-sm focus:outline-none focus:ring-2 focus:ring-primary/30"><button class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-primary transition"><i class="fa fa-paper-plane-o"></i></button></div><div class="flex items-center space-x-3"><button class="text-gray-400 hover:text-primary transition"><i class="fa fa-smile-o text-xl"></i></button><button class="text-gray-400 hover:text-primary transition"><i class="fa fa-camera-o text-xl"></i></button></div></div></footer>
</body></html>
`