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

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

<template><div class="editor" flex w-full><!-- 文章内容 --><div flex-grow overflow-hidden w-full><slot/><div><Editorref="editorContent" v-model="defaultHtml":defaultConfig="editorConfig":mode="mode"m-t-60pxoverflow-hiddenw-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 relativew-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-5pxpy-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;}}// 检查当前视图中是否有标题元素,如果有,将其索引赋给 currentIndexconst 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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

5个超好用的Python工具,赶紧码住!

Python开发软件可根据其用途不同分为两种&#xff0c;Python代码编辑器和Python集成开发工具&#xff0c;两者配合使用极大的提高Python开发人员的编程效率。掌握调试、语法高亮、Project管理、代码跳转、智能提示、自动完成、单元测试、版本控制等操作。 Python常用工具&…

小白新手学习 Python 使用哪个 Linux 系统更好?

对于小白新手学习Python&#xff0c;选择哪个Linux系统是一个很重要的问题&#xff0c;因为不同的Linux发行版&#xff08;distribution&#xff09;有着不同的特点、优势和适用场景。在选择时&#xff0c;需要考虑到易用性、学习曲线、社区支持等因素。 Ubuntu Ubuntu 是一个…

分布式系统中的唯一ID生成方法

通常在分布式系统中&#xff0c;有生成唯一ID的需求&#xff0c;唯一ID有多种实现方式。我们选择其中几种&#xff0c;简单阐述一下实现原理、适用场景、优缺点等信息。 目录 数据库多主复制UUID工单服务器雪花算法总结 数据库多主复制 数据库通常有自增属性&#xff0c;在单机…

CSS 实现无限波浪边框卡片

CSS 实现无限波浪边框卡片 效果展示 鼠标悬停效果&#xff0c;底部色块的边框是无限滚动的波浪 鼠标没有悬停效果 CSS 知识点 CSS 基础知识回顾使用 radial-gradient 实现波浪边框使用 anumate 属性实现波浪边框动画和控制动画运动 波浪实现原理 波浪边框的实现思路其…

dayjs 判断是否今天、本周内、本年内、本年外显示周几、月份等

效果: 判断是否今天需从 dayjs 中引入 isToday 插件&#xff1b; 判断是否两个日期之间需从 dayjs 中引入 isBetween 插件 import dayjs from dayjs import isToday from dayjs/plugin/isToday import isBetween from dayjs/plugin/isBetween// 注册插件 dayjs.extend(isBet…

DP:子数组模型

一、最大子数组和 . - 力扣&#xff08;LeetCode&#xff09; 二、环形子数组的最大和 . - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int maxSubarraySumCircular(vector<int>& nums) {//动态规划思想解决 //环形数组问题&#xff0c;尝试转…

01-Git 快速入门

https://learngitbranching.js.org/?localezh_CN在线练习git 1. Git 安装好Git以后, 先检查是否已经绑定了用户名和邮箱 git config --list再检查C:\Users\xxx.ssh 下是否存在 id_rsa.pub , 存在的话复制其内容到 GitHub 的 SSH KEY 中 没有这一步, PUSH操作的时候会报错:…

土壤墒情监测系统:洞察土壤水分奥秘

TH-TS400土壤墒情监测系统&#xff0c;作为现代农业科技的重要组成部分&#xff0c;已经成为农业生产过程中不可或缺的一环。该系统通过先进的传感器技术和数据处理能力&#xff0c;能够实时监测土壤的水分状况&#xff0c;为农业生产提供精准、可靠的数据支持。本文将从系统构…

算法打卡day29

今日任务&#xff1a; 1&#xff09;1005.K次取反后最大化的数组和 2&#xff09;134.加油站 3&#xff09;135.分发糖果 1005.K次取反后最大化的数组和 题目链接&#xff1a;1005. K 次取反后最大化的数组和 - 力扣&#xff08;LeetCode&#xff09; 给定一个整数数组 A&…

Java Web-分层解耦

三层架构 当我们所有代码都写在一起时&#xff0c;代码的复用性差&#xff0c;并且难以维护。就像我们要修改一下服务端获取数据的方式&#xff0c;从文本文档获取改为到数据库中获取&#xff0c;就难以修改&#xff0c;而使用三层架构能很好的解决这个问题。 controller: 控…

HJ61 放苹果(递归,苹果多的情况+盘子多的情况)

当苹果数 < 盘子数&#xff0c;有空盘&#xff0c;则忽略一个盘子&#xff0c;在n-1个放苹果&#xff0c;一直递推到n1&#xff0c;有一种摆法苹果数 > 盘子数&#xff0c;可以看作没有空盘。则可以选择忽略一个盘子&#xff0c;如上边做法。还可以选择每个盘子放一个苹果…

GlusterFS分布式存储

目录 前言 一、GlusterFS分布式存储概述 1、GFS概念 2、GFS特点 3、GFS术语 4、GFS构成 5、GFS工作流程 6、后端存储如何定位文件 7、GlusterFs的卷类型 7.1 Distributed Volume&#xff08;分布式卷&#xff09; 7.2 Striped Volume&#xff08;条带卷&#xff09…

线性变换在人工智能领域的深度实践与应用探索

线性变换&#xff0c;作为数学中的一种基本工具&#xff0c;在人工智能领域中发挥着举足轻重的作用。其强大的表示能力和灵活的运算特性使得线性变换成为机器学习、深度学习等多个子领域的核心组成部分。本文将详细探讨线性变换在人工智能领域中的实践应用&#xff0c;旨在揭示…

自动化测试selenium

目录 什么是自动化测试 什么是selenium selenium工作原理 selenium环境搭建 1.查看chrome浏览器版本 2.下载chrome浏览器驱动 3.配置系统环境变量PATH 4.验证环境是否搭建成功 selenium相关API 1.定位元素 CSS选择器定位 xpath定位元素 标签定位元素 2.操作测试对…

浏览器工作原理与实践--渐进式网页应用(PWA):它究竟解决了Web应用的哪些问题

在专栏开篇词中&#xff0c;我们提到过浏览器的三大进化路线&#xff1a; 第一个是应用程序Web化&#xff1b; 第二个是Web应用移动化&#xff1b; 第三个是Web操作系统化&#xff1b; 其中&#xff0c;第二个Web应用移动化是Google梦寐以求而又一直在发力的一件事&#xf…

【免费SSL】免费一年的SSL证书

一、SSL证书行业政策 目前市面上绝大多数的免费SSL证书有效期都在3个月左右&#xff0c;而不是一年。例如&#xff0c;腾讯云在2024年4月后不再提供有效期为一年的免费证书&#xff0c;改为提供有效期为3个月的免费证书。同样&#xff0c;阿里云在2023年11月后也不再提供有效期…

javaScript常见对象方法总结

1&#xff0c;object.assign() 用于合并对象的属性。它可以将一个或多个源对象的属性复制到目标对象中&#xff0c;实现属性的合并。 语法 Object.assign(target, ...sources); 1,target&#xff1a;目标对象&#xff0c;将属性复制到该对象中。 2,sources&#xff1a;一个…

51单片机学习笔记16 小型直流电机和五线四相电机控制

51单片机学习笔记16 小型直流电机和五线四相电机控制 一、电机分类二、小型直流电机控制1. 简介2. 驱动芯片ULN2003D3. 代码实现dc_motor_utils.cmain.c 三、五线四相步进电机控制1. 步进电机工作原理2. 构造3. 极性区分4. 驱动方式5. 28BYJ-48步进电机&#xff08;1&#xff0…

3D可视化技术亮相高铁站,引领智慧出行新潮流

在科技飞速发展的今天&#xff0c;我们的生活正经历着前所未有的变革。高铁站作为现代交通的重要枢纽&#xff0c;也在不断地创新和进步。 3D可视化技术通过三维立体的方式&#xff0c;将高铁站内部和外部的结构、设施、流线等以更加直观、生动的形式呈现出来。乘客们只需通过手…

Docker容器嵌入式开发:MySQL表的外键约束及其解决方法

本文内容涵盖了使用MySQL创建数据库和表、添加数据、处理字符集错误、解决外键约束问题以及使用SQL查询数据的过程。通过创建表、插入数据和调整字符集等操作&#xff0c;成功解决了数据库表中的字符集问题&#xff0c;并使用INSERT语句向各个表中添加了示例数据。同时&#xf…