使用 wangeditor 解析富文本并生成目录与代码块复制功能

在 Web 开发中,经常需要使用富文本编辑器来编辑和展示内容。wangeditor 是一个强大的富文本编辑器,提供了丰富的功能和灵活的配置,但是官方并没有提供目录导航和代码块的复制功能,所以我自己搞了一个

<template>
  <div class="editor" flex w-full>
    <!-- 文章内容 -->
    <div flex-grow overflow-hidden w-full>
      <slot/>
      <div>
        <Editor
          ref="editorContent" v-model="defaultHtml"
          :defaultConfig="editorConfig"
          :mode="mode"
          m-t-60px
          overflow-hidden
          w-auto
          @onChange="handleChange"
          @onCreated="handleCreated"
        />
      </div>
    </div>

    <div v-if="directory" class="flex-container" flex-none h-500px ml-20px p-t-160px relative w-300px>
      <el-affix :offset="10">
        <div border-b border-b-solid border-gray200 class="table-of-title" flex font-bold items-center p-10px w-300px>
          <el-icon>
            <Expand/>
          </el-icon>
          <span ml-5px>目录</span>
        </div>
        <!-- 目录 -->
        <div v-if="tableOfContents.length > 0" b-rd-5px class="table-of-contents " max-h-450px overflow-y-scroll p-10px relative
             w-300px>
          <!-- 目录内容 -->
          <ul list-none p-l-0>
            <li v-for="(item, index) in tableOfContents" :key="item.id" :style="{ paddingLeft: item.level * 20 + 'px' }" border-rd mb-5px
                py-3px>
              <a :class="{ active: activeIndex === index }" :href="`#${item.id}`" block decoration-none
                 @click="handleItemClick(index)">{{ item.text }}</a>
            </li>
          </ul>
        </div>
      </el-affix>

    </div>
  </div>
</template>

<script lang="ts" setup>
import {Editor} from "@wangeditor/editor-for-vue";
import {copy} from "@/utils";

// API 引用
import {upload} from "@/utils/request";

const props = defineProps({
  modelValue: {
    type: [String],
    default: "",
  },
  directory: {
    type: [Boolean],
    default: true,
  }
});

const emit = defineEmits(["update:modelValue"]);

const defaultHtml = useVModel(props, "modelValue", emit);

const editorRef = shallowRef(); // 编辑器实例,必须用 shallowRef
const mode = ref("default"); // 编辑器模式
// 在编辑器创建后生成目录
const tableOfContents = ref([]);
// 编辑器配置
const editorConfig = ref({
  placeholder: "请输入内容...",
  MENU_CONF: {
    uploadImage: {
      // 自定义图片上传
      async customUpload(file: any, insertFn: any) {
        const formData = new FormData();
        formData.set("file", file);
        upload(formData).then(({data: res}) => {
          insertFn(res.url);
        });
      },
    },
  },
  readOnly: true,
});

const handleCreated = (editor: any) => {
  editorRef.value = editor; // 记录 editor 实例,重要!
};

function handleChange(editor: any) {
  emit("update:modelValue", editor.getHtml());
}

// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
  const editor = editorRef.value;
  if (editor == null) return;
  editor.destroy();
});
watch(() => props.modelValue, (newVal) => {
  defaultHtml.value = newVal;
});
// 添加复制按钮的逻辑
const copyCode = () => {
  const codeBlocks = document.querySelectorAll(".editor pre > code");
  codeBlocks.forEach((codeBlock) => {
    // 创建复制按钮
    const copyButton = document.createElement("button");
    copyButton.innerText = "复制";
    copyButton.classList.add("copy-button");

    // 为复制按钮添加点击事件处理程序
    copyButton.addEventListener("click", () => {
      const codeText = codeBlock.querySelector("span").textContent;
      copy(codeText, "已复制", false);
      // 修改按钮文本为 "已复制"
      copyButton.innerText = "已复制";
      // 延迟一段时间后恢复按钮文本为 "复制"
      setTimeout(() => {
        copyButton.innerText = "复制";
      }, 3000); // 毫秒为单位,您可以调整时间长度
    });

    // 将复制按钮添加到代码块的父级元素中
    codeBlock.appendChild(copyButton);
  });
};

// 添加锚点
const addAnchorLinks = () => {
  const headings = document.querySelectorAll(".editor h1, .editor h2, .editor h3");
  headings.forEach((heading, index) => {
    const anchorLink = document.createElement("a");
    anchorLink.setAttribute("href", `#section-${index + 1}`);
    // anchorLink.textContent = heading.textContent; // 设置锚点文本为标题文本
    anchorLink.style.pointerEvents = "none"; // 设置 pointer-events 为 none,使链接不可点击

    // 设置标题的id属性
    heading.setAttribute("id", `section-${index + 1}`);

    // 将锚点链接插入到标题内
    heading.innerHTML = anchorLink.outerHTML + heading.innerHTML;
  });
};

// 更新目录项点击事件处理函数
const handleItemClick = (index: number) => {
  activeIndex.value = index;

  // 获取目标目录项的锚点链接 href 属性值
  const targetItem = document.querySelector(`.table-of-contents a[href="#section-${index + 1}"]`) as HTMLElement;

  // 滚动目录以确保当前点击的目录项可见
  if (targetItem) {
    const container = document.querySelector(".table-of-contents") as HTMLElement;
    const containerRect = container.getBoundingClientRect();
    const scrollTop = targetItem.offsetTop - containerRect.height / 2;
    container.scrollTop = scrollTop;
  }
};

// 生成目录
const generateTableOfContents = () => {
  const headings = document.querySelectorAll(".editor h1, .editor h2, .editor h3");
  const toc = [];
  headings.forEach((heading, index) => {
    const id = `section-${index + 1}`;
    const level = heading.tagName === "H1" ? 1 : heading.tagName === "H2" ? 2 : 3; // 根据标题等级设置目录项的缩进
    heading.setAttribute("id", id); // 设置标题的id属性
    toc.push({id: id, text: heading.textContent, level: level, index: index}); // 将标题文本、id和等级添加到目录项中
  });
  return toc;
};

const handleScroll = () => {
  requestAnimationFrame(() => {
    const sections = document.querySelectorAll(".editor h1, .editor h2, .editor h3");
    const scrollY = window.scrollY || window.pageYOffset;
    let currentIndex = 0;
    for (let i = 0; i < sections.length; i++) {
      const sectionTop = (sections[i] as HTMLElement).offsetTop;
      if (scrollY >= sectionTop) {
        currentIndex = i;
      }
    }

    // 检查当前视图中是否有标题元素,如果有,将其索引赋给 currentIndex
    const visibleSections = Array.from(sections).filter((section) => {
      const sectionTop = (section as HTMLElement).offsetTop;
      const sectionBottom = sectionTop + (section as HTMLElement).offsetHeight;
      return scrollY >= sectionTop && scrollY <= sectionBottom;
    });
    if (visibleSections.length > 0) {
      currentIndex = Array.from(sections).indexOf(visibleSections[visibleSections.length - 1]);
    }

    activeIndex.value = currentIndex;

    // 滚动目录以确保当前高亮的目录项可见
    const activeItem = document.querySelector(".table-of-contents .active") as HTMLElement;
    if (activeItem) {
      const container = document.querySelector(".table-of-contents");
      const containerRect = container.getBoundingClientRect();
      const activeRect = activeItem.getBoundingClientRect();
      const scrollTop = activeItem.offsetTop - containerRect.height / 2 + activeRect.height / 2;
      container.scrollTop = scrollTop;
    }
  });
};

// 在编辑器创建后添加复制按钮
onMounted(() => {
  tableOfContents.value = generateTableOfContents();
  addAnchorLinks();
  copyCode();
  window.addEventListener("scroll", handleScroll);
});

// 在组件销毁时移除滚动事件监听器
onBeforeUnmount(() => {
  window.removeEventListener("scroll", handleScroll);
});

// 当前高亮的目录项索引
const activeIndex = ref(0);
</script>

<style src="@wangeditor/editor/dist/css/style.css"></style>
<style lang="scss" scoped>
@import "@/assets/styles/variables.module";

html {
  scroll-behavior: smooth;
}

.editor {
  overflow: hidden;
  min-height: 250px;
  z-index: 999;
}

@media screen and (max-width: 900px) {
  .flex-container {
    display: none !important; /* 添加 !important 以确保覆盖其他样式 */
  }
}

:deep() {
  .w-e-text-container {
    overflow: hidden;
    width: 100%;
    color: $base-text-color;
    border-bottom-right-radius: 8px;
    border-bottom-left-radius: 8px;
    background-color: $base-bg-box;

    .w-e-scroll {
      width: 100%;
      overflow: hidden !important;
    }

    [data-slate-editor] code {
      position: relative;
      background-color: $base-code-color;

      span {
        background-color: $base-code-color;
      }
    }

    pre > code {
      background-color: $base-code-color;
      // 防止花眼
      text-shadow: none;
    }

    iframe {
      width: 80%;
      height: 640px;
      display: block;
      border-radius: 8px;
      margin: 0 auto; /* 让图片水平居中 */
    }

    p {
      text-align: left; /* 保持文字左对齐 */
      line-height: 1.5rem;
      font-size: 0.875rem;
      font-family: "PingFang SC", sans-serif;
    }

    img {
      display: block;
      margin: 0 auto; /* 让图片水平居中 */
      max-width: 80%;
      //max-width: 80vw !important; /* 设置图片最大宽度为父元素宽度 */
      height: auto; /* 保持宽高比 */
      transform: scale(0.8); /* 设置缩放比例,这里是缩小为原来的80% */
    }

    [data-slate-editor] blockquote {
      background-color: $base-code-color;
      color: $base-text-color;
      border-radius: 2px;
    }

    [data-slate-editor] pre .copy-button {
      position: absolute;
      top: 6px;
      right: 6px;
      color: $base-text-color;
      border: none;
      padding: 0 5px;
      border-radius: 4px;
      cursor: pointer;
    }
  }

  .table-of-title {
    background-color: $base-bg-box;
  }

  .table-of-contents {
    color: $base-text-color;
    background-color: $base-bg-box;

    li:hover {
      color: #95c92c;
    }

    .active {
      font-weight: bold;
      color: #95c92c;
    }
  }
}
</style>
export const copy = (text: string, message: string, showSuccess: boolean = true) => {
  navigator.clipboard.writeText(text)
    .then(function () {
      if (showSuccess) {
        ElMessage({message: message, type: 'success', duration: 1500});
      }
    })
    .catch(function (err) {
      console.error('Unable to copy text to clipboard', err);
    });
}

实现功能如图:
目录功能:在这里插入图片描述
代码块复制功能:
在这里插入图片描述

具体实现效果可在平台https://web.yujky.cn/登录体验

租户:体验租户
用户名:cxks
密码: cxks123

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/530838.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

安卓的认证测试

1 CTS CTS 是 Android 兼容性测试套件&#xff0c;用于验证设备是否符合 Android 平台的兼容性标准。它包含一系列测试用例&#xff0c;涵盖了设备的各个方面&#xff0c;如硬件功能、软件功能、API 的正确实现等。通过 CTS 测试&#xff0c;设备厂商可以确保其设备符合 Andro…

华为S5735S核心交换配置实例

以下脚本实现创建vlan2,3&#xff0c;IP划分&#xff0c;DHCP启用&#xff0c;接口划分&#xff0c;ssh,telnet,http,远程登录启用 默认用户创建admin/admin123提示首次登录需要更改用户密码S5735产品手册更多功能配置&#xff0c;移步官网参考手册配置 sysname test-Hxvlan …

Linux入门攻坚——18、SELinux、Bash脚本编程续

SELinux——Secure Enhanced Linux&#xff08;安全加强的Linux&#xff09;&#xff0c;工作于Linux内核中。 SELinux 主要作用就是最大限度地减小系统中服务进程可访问的资源&#xff08;最小权限原则&#xff09;。采用委任式存取控制&#xff0c;是在进行程序、文件等细节权…

如何在Flutter应用中配置ipa Guard进行混淆

在移动应用开发中&#xff0c;保护应用代码安全至关重要。Flutter 提供了简单易用的混淆工具&#xff0c;帮助开发者在构建 release 版本应用时有效保护代码。本文将介绍如何在 Flutter 应用中使用混淆&#xff0c;并提供了相关的操作步骤和注意事项。 &#x1f4dd; 摘要 本…

EDM营销:常见的邮件模板制作方法

在EDM邮件营销中&#xff0c;邮件模板的制作是一个关键步骤&#xff0c;邮件模板的质量直接影响到邮件的打开率、阅读率和转化率。邮件内容可能是简单的文字&#xff0c;只需要进行基本的排版&#xff1b;而有些则涉及文字、图片以及超链接等多种元素&#xff0c;这就需要借助工…

spring02:DI(依赖注入)

spring02&#xff1a;DI&#xff08;依赖注入&#xff09; 文章目录 spring02&#xff1a;DI&#xff08;依赖注入&#xff09;前言&#xff1a;一、构造器注入二、set注入&#xff1a;1. Student类&#xff1a;2. Address类&#xff1a;3. beans.xml&#xff1a;4. MyTest&…

想拥有健康体魄?学会中医气血调理秘籍!

《素问调经论》所述&#xff0c;人的生理机能主要依赖于血与气。这两者构成了我们身体生命的基石&#xff0c;其他所有生理活动都是围绕这一核心进行的。因此&#xff0c;各种健康问题&#xff0c;其根源往往可以追溯到气血的失调。 气虚会表现为畏寒怕冷&#xff0c;头晕耳鸣、…

反射(Reflection) --Java学习笔记

反射 反射就是:加载类&#xff0c;并允许以编程的方式解剖类中的各种成分(成员变量、方法、构造器等) 反射学什么? 学习获取类的信息、操作它们 反射第一步:加载类&#xff0c;获取类的字节码:Class对象获取类的构造器:Constructor对象获取类的成员变量:Field对象获取类的成…

SpringBoot集成Skywalking日志收集

在实际项目中&#xff0c;为了方便线上排查问题&#xff0c;尤其是微服务之间调用链路比较复杂的系统中&#xff0c;通过可视化日志的手段仍然是最直接也很方便的排查定位问题的手段&#xff0c;比如大家熟悉的ELK就是一种比较成熟的可视化日志展现方式&#xff0c;在skywalkin…

语音情感识别调研

语音情感识别调研 1、情绪识别综述2、语音情感识别算法3、语音特征提取4、相关项目1、用 LSTM、CNN、SVM、MLP 进行语音情感识别2、DST&#xff1a;基于Transformer的可变形语音情感识别模型3、语音情感基座模型emotion2vec4、IEEE ICME 2023论文&#xff5c;基于交互式注意力的…

康姿百德床垫官网价格公道,为你带来健康与舒适的睡眠享受

我们一生中有很长一段时间在睡眠度过&#xff0c;睡眠之于我们来说十分重要。良好的睡眠质量不仅能够帮助我们更好地恢复体力和精神&#xff0c;还能提高我们的生活质量。因此选择一款优质的床垫变得尤为重要。作为床垫行业的领导品牌&#xff0c;康姿百德床垫一直以提升人们睡…

stm32 之SPI通信协议

本文为大家介绍 SPI 通信协议的基础知识。 文章目录 前言一、SPI协议的概念二、SPI总线架构三、SPI通讯时序1. 起始&#xff0c;停止 信号2.CPOL&#xff08;时钟极性&#xff09;/CPHA&#xff08;时钟相位&#xff09; 四&#xff0c; I2C 总线 和SPI 总线比较相同点&#xf…

二叉树的前序遍历、中序遍历、后序遍历

二叉树的前序遍历、中序遍历、后序遍历 一、递归算法的三个要素二、144. 二叉树的前序遍历三、94. 二叉树的中序遍历四、145. 二叉树的后序遍历 一、递归算法的三个要素 1、确定递归函数的参数和返回值&#xff1a; 确定哪些参数是递归的过程中需要处理的&#xff0c;那么就在…

【单片机】PMS5003,PM2.5传感器数据读取处理

文章目录 传感器介绍数据处理解析pm2.5的代码帮助、问询 传感器介绍 PMS5003是一款基于激光散射原理的数字式通用颗粒物浓度传感器,可连续采集 并计算单位体积内空气中不同粒径的悬浮颗粒物个数,即颗粒物浓度分布,进而 换算成为质量浓度,并以通用数字接口形式输出。本传感器可…

LangChain-15 Manage Prompt Size 管理上下文大小,用Agent的方式询问问题,并去百科检索内容,总结后返回

背景描述 这一节内容比较复杂&#xff1a; 涉及到使用工具进行百科的检索&#xff08;有现成的插件&#xff09;有AgentExecutor来帮助我们执行后续由于上下文过大&#xff0c; 我们通过计算num_tokens&#xff0c;来控制我们的上下文 安装依赖 pip install --upgrade --qu…

SpringBoot整合RabbitMQ-应答模式

一、应答模式 RabbitMQ 中的消息应答模式主要包括两种&#xff1a;自动应答&#xff08;Automatic Acknowledgement&#xff09;和手动应答&#xff08;Manual Acknowledgement&#xff09;。&#xff08;一般交换机发送消息&#xff0c;RabbitMQ只有在接收到消费者的确认后才…

常见性能测试工具对比

在性能测试工作中&#xff0c;我们常常会遇到好几个工具&#xff0c;但是每一个工具都有自己的优势&#xff0c;一时间不知道怎么选择。 今天我们就将性能测试常用的工具进行对比&#xff0c;这样大家在选择工具的时候心里就有底啦&#xff01; 阿里云PTS 性能测试PTS&#xff…

基于springboot实现常州地方旅游管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现旅游管理系统演示 摘要 随着旅游业的迅速发展&#xff0c;传统的旅游信息查询方式&#xff0c;已经无法满足用户需求&#xff0c;因此&#xff0c;结合计算机技术的优势和普及&#xff0c;针对常州旅游&#xff0c;特开发了本基于Bootstrap的常州地方旅游管…

C++初阶:6.string类

string类 string不属于STL,早于STL出现 看文档 C非官网(建议用这个) C官网 文章目录 string类一.为什么学习string类&#xff1f;1.C语言中的字符串2. 两个面试题(暂不做讲解) 二.标准库中的string类1. string类(了解)2. string类的常用接口说明&#xff08;注意下面我只讲解…

ONNX系列: ONNX模型修改

ONNX 模型修改 当我们熟悉了ONNX模型各个层级的结构后&#xff0c;我们便可以针对各个结构来对模型进行修改&#xff0c;从而使其更好的适配后端运行时或者特定硬件平台的编译器。对模型的修改通常可以概括为"增删改查"的操作。"增"是增加相应结构&#xf…