vue3渲染html数据并实现文本修改

发布于:2025-08-06 ⋅ 阅读:(21) ⋅ 点赞:(0)

需求:从接口获取到一个完整的html代码数据渲染到页面展示

实现:

  1. 使用vue3的v-html来渲染数据,发现完整的html数据中有一些外链标签未能加载
  2. 使用iframe嵌套实现渲染
  3. 文本修改使用属性contentEditable = true
  4. 导出页面为图片:使用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-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字符串,这里使用假数据模拟。

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&amp;rk3s=8e244e95&amp;rrcfp=e75484ac&amp;x-expires=1754637600&amp;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>
`

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到