需求:从接口获取到一个完整的html代码数据渲染到页面展示
实现:
- 使用vue3的v-html来渲染数据,发现完整的html数据中有一些外链标签未能加载
- 使用iframe嵌套实现渲染
- 文本修改使用属性
contentEditable = true
- 导出页面为图片:使用
html2canvas
实现
代码
html
<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-neutral
const 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元素转换为canvas
const 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转换为图片URL
const 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字符串,这里使用假数据模拟。
js
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>
<button
class="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>
<button
class="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">
<img
src="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>
`
