在 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