当前位置: 首页 > news >正文

Electron-vite【实战】MD 编辑器 -- 大纲区(含自动生成大纲,大纲缩进,折叠大纲,滚动同步高亮大纲,点击大纲滚动等)

最终效果

在这里插入图片描述

页面

    <!-- 大纲 --><div v-if="currentFilePath && outlineList.length" class="outlinePanel"><div class="panelTitle">大纲</div><div class="searchTitleBox"><Icon class="searchTitleInputIcon" icon="material-symbols-light:search" /><inputv-model="searchTitleKeyWord"class="searchTitleInput"type="text"placeholder="请输入标题"/><Iconv-show="searchTitleKeyWord"class="clearSearchTitleInputBtn"icon="codex:cross"@click="clearSearchTitleInput"/></div><div class="outlineListBox"><template v-for="(item, index) in outlineList_filtered"><divv-if="!item.hide":key="index":title="item.text"class="outlineItem":style="{ color: currentOutLineID === item.id ? 'red' : '' }"@click="handle_clickOutLine(item)"><div v-for="i in item.level" :key="i"><Iconv-if="index < outlineList_filtered.length - 1 &&item.level < outlineList_filtered[index + 1].level &&i === item.level":icon="item.collapsed ? 'iconamoon:arrow-right-2-light' : 'iconamoon:arrow-down-2-light'"@click.stop="toggleCollapse(item, index)"/><Icon v-else icon="" /></div><div style="margin-left: 4px">{{ item.text }}</div></div></template></div></div>

不同级别标题的缩进和折叠图标的实现

            <div v-for="i in item.level" :key="i"><Iconv-if="index < outlineList_filtered.length - 1 &&item.level < outlineList_filtered[index + 1].level &&i === item.level":icon="item.collapsed ? 'iconamoon:arrow-right-2-light' : 'iconamoon:arrow-down-2-light'"@click.stop="toggleCollapse(item, index)"/><Icon v-else icon="" /></div>
  • 按 level 值,渲染空白图标, level 值越大,缩进越多。
  • 仅当下一行标题的 level 更大,即有子标题时,显示大纲折叠图标

相关样式

/* 大纲的样式 */
.outlinePanel {width: 200px;border: 1px solid gray;border-left: none;display: flex;flex-direction: column;font-size: 14px;
}
.searchTitleBox {display: flex;align-items: center;justify-content: center;padding: 10px;
}
.searchTitleInput {display: block;font-size: 12px;padding: 4px 20px;
}
.searchTitleInputIcon {position: absolute;font-size: 16px;transform: translateX(-80px);
}
.clearSearchTitleInputBtn {position: absolute;cursor: pointer;font-size: 16px;transform: translateX(77px);
}
.outlineListBox {padding: 10px;line-height: 1.5;flex: 1;overflow-y: auto;
}
.outlineItem {cursor: pointer;text-wrap: nowrap;display: flex;align-items: center;color: #787474;margin-bottom: 6px;
}

相关变量

// 大纲相关变量
const searchTitleKeyWord = ref('') // 搜索大纲的关键词
const currentOutLineID = ref('0') // 当前选中的大纲项的id
const ifClickOutLine = ref(false) // 是否点击了大纲项
// 计算属性:根据markdownContent生成大纲列表
const outlineList = computed(() => {const originalList = MarkdownParser(markdownContent.value).outline || []// 使用 reactive 让大纲项的每个属性都是响应式return originalList.map((item) => reactive({ ...item, collapsed: false }))
})
// 计算属性:根据searchTitleKeyWord过滤大纲列表
const outlineList_filtered = computed(() => {let result = outlineList.value.filter((outline) => {return outline.text.toLowerCase().includes(searchTitleKeyWord.value.toLowerCase())})return result
})

核心方法

根据 markdown 内容生成大纲

const originalList = MarkdownParser(markdownContent.value).outline || []

在 MarkdownParser 方法中,遍历每一行时,将标题行转换为目标格式,存入 outlineList 中

    // 标题if (line.startsWith('#')) {const resultTemp = parseMarkdownHeadings(line, index)if (resultTemp) {line = resultTemp.contentoutlineList.push({...resultTemp.outline,index})}}

最终返回

  return {content: result,outline: outlineList}

其中格式解析的方法 parseMarkdownHeadings 如下:

/*** 将 Markdown 标题(# 开头)转换为 HTML 标题标签* @param markdown - 输入的 Markdown 文本* @returns 转换后的 HTML 文本*/
function parseMarkdownHeadings(markdown: string,index: number
): {content: stringoutline: outlineItemType
} | void {// 正则表达式匹配 Markdown 标题const headingRegex = /^(#+)\s+(.*)$/gmconst match = headingRegex.exec(markdown)if (match) {const level = match[1].length // 标题等级由#的数量决定const text = match[2].trim()const id = index.toString() // 生成锚点IDreturn {content: `<h${level}  id=${id}  >${escapeHTML(text)}</h${level}>`,outline: {text,id,level}}}
}

切换大纲项的折叠状态

// 切换大纲项的折叠状态
const toggleCollapse = (item: outlineItemType, index: number): void => {item.collapsed = !item.collapsedfor (let i = index + 1, len = outlineList_filtered.value.length; i < len; i++) {const outline = outlineList_filtered.value[i]if (outline.level > item.level) {outline.hide = item.collapsed} else {break}}
}

点击大纲项滚动编辑区和预览区

在这里插入图片描述

 @click="handle_clickOutLine(item)"
// 点击大纲项
const handle_clickOutLine = (item): void => {currentOutLineID.value = item.idconst editor = editorRef.valueif (editor) {ifClickOutLine.value = true// 编辑区滚动滚动到指定行const lineHeight = parseInt(getComputedStyle(editor).lineHeight)const paddingTop = parseInt(getComputedStyle(editor).paddingTop)editor.scrollTo({top: paddingTop + item.index * lineHeight,behavior: 'smooth'})}const preview = previewRef.valueif (preview) {const targetDom = document.getElementById(item.id)if (targetDom) {targetDom.scrollIntoView({behavior: 'smooth',block: 'start'})setTimeout(() => {// 延迟1s后,将 ifClickOutLine 置为 false,防止点击大纲项时,在滚动编辑区时,同步滚动预览区ifClickOutLine.value = false}, 1000)}}
}

点击大纲项时,禁止编辑区的滚动导致预览区同步滚动

// 同步预览区滚动
const syncPreviewScroll = (): void => {// 点击大纲项时,不触发同步预览区滚动if (editorRef.value && previewRef.value && !ifClickOutLine.value) {const editor = editorRef.valueconst preview = previewRef.valueconst editorScrollRatio = editor.scrollTop / (editor.scrollHeight - editor.clientHeight)const previewScrollTop = editorScrollRatio * (preview.scrollHeight - preview.clientHeight)preview.scrollTop = previewScrollTop}
}

滚动预览区时,当前高亮大纲同步变化

onMounted(() => {if (previewRef.value) {// 监听滚动事件以更新当前大纲项IDpreviewRef.value.addEventListener('scroll', update_currentOutlineId)}
// 更新当前高亮大纲
const update_currentOutlineId = (): void => {if (ifClickOutLine.value) {return}if (!previewRef.value) returnconst headings = previewRef.value.querySelectorAll('h1, h2, h3, h4, h5, h6')let lastId = ''for (const heading of headings) {// 当标题即将超出预览区域时,停止查找更新if ((heading as HTMLElement).offsetTop - 60 >= previewRef.value.scrollTop) {break}lastId = heading.id}currentOutLineID.value = lastId
}

onBeforeUnmount 中

  if (previewRef.value) {previewRef.value.removeEventListener('click', handle_preview_click)previewRef.value.removeEventListener('scroll', update_currentOutlineId)}

相关文章:

  • PyCharm Python IDE
  • 【沉浸式解决问题】Idea运行Junit测试中scanner无法获取控制台的输入内容
  • 创建型设计模式
  • 25-Oracle 23ai DBMS_SEARCH — Ubiquitous Search(无处不在的搜索)
  • 软件架构期末复习
  • WinForm中实现Adobe PDF Reader实现旋转PDF功能
  • 从零打造前沿Web聊天组件:从设计到交互
  • PHP性能提升方案
  • 深入理解 Go 中的字节序(Endianness)检测代码
  • Qt::QueuedConnection详解
  • 前端框架vue3的变化之处
  • 2025 年 MQTT 技术趋势:驱动 AI 与物联网未来发展的关键动力
  • 基于YOLOv12的电力高空作业安全检测:为电力作业“保驾护航”,告别安全隐患!
  • Oracle 的 FORCE_LOGGING 特性
  • HTTP 响应状态码
  • DevSecOps实践:CI/CD流水线集成动态安全测试(DAST)工具
  • 什么是 Solana 上的 MEV?一键狙击是如何保护你的代币启动的?
  • 混合型交易所架构:CEX+DEX融合与Layer2扩展方案
  • Solidity 开发从入门到精通:语法特性与实战指南
  • 跨链/Layer2交易所架构:全链互操作与ZK-Rollup优化
  • 昆明网站开发/营销策略4p
  • 成都企业做网站/做一个网站需要多少钱
  • 青岛市崂山区建设局网站/新产品推广方案怎么写
  • 网站建设课设心得体会/网络优化培训骗局
  • 微信服务号菜单链接网站怎么做/郑州搜索引擎优化公司
  • 深圳网站建设网站设计软文推广/新冠疫情最新数据