md2ui 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +65 -20
  2. package/bin/build.js +13 -2
  3. package/bin/md2ui.js +25 -12
  4. package/package.json +4 -4
  5. package/public/docs/02-Markdown/346/270/262/346/237/223/00-/345/237/272/347/241/200/350/257/255/346/263/225.md +2 -0
  6. package/public/docs/02-Markdown/346/270/262/346/237/223/06-Mermaid/345/244/215/346/235/202/345/233/276/350/241/250/346/265/213/350/257/225.md +1376 -0
  7. package/public/docs/02-Markdown/346/270/262/346/237/223/assets/img-1777383093712.png +0 -0
  8. package/public/docs/03-/345/257/274/350/210/252/344/270/216/345/270/203/345/261/200/05-/345/244/247/347/272/262/345/216/213/345/212/233/346/265/213/350/257/225.md +340 -0
  9. package/public/docs/07-/347/247/273/345/212/250/347/253/257/351/200/202/351/205/215/00-/345/223/215/345/272/224/345/274/217/345/270/203/345/261/200.md +4 -4
  10. package/src/App.vue +36 -61
  11. package/src/components/ImageZoom.vue +9 -123
  12. package/src/components/MermaidNodeView.vue +10 -2
  13. package/src/components/MobileSearch.vue +97 -0
  14. package/src/components/TableOfContents.vue +42 -6
  15. package/src/composables/useDocManager.js +134 -44
  16. package/src/composables/useDocTree.js +26 -50
  17. package/src/composables/useMarkdown.js +51 -140
  18. package/src/composables/useMermaidCache.js +15 -0
  19. package/src/composables/useScroll.js +317 -32
  20. package/src/composables/useSearch.js +12 -11
  21. package/src/config.js +1 -4
  22. package/src/services/DocService.js +0 -16
  23. package/src/style.css +235 -10
  24. package/src/utils/imageConverter.js +129 -0
  25. package/vite-plugin-doc-api.js +158 -157
  26. package/vite.config.js +5 -1
  27. package/src/components/SearchPanel.vue +0 -90
  28. package/src/components/TableBubbleMenu.vue +0 -177
  29. package/src/composables/useExportPdf.js +0 -102
@@ -0,0 +1,340 @@
1
+ # 大纲压力测试
2
+
3
+ 本文档用于测试右侧文档大纲(TOC)在大量标题锚点场景下的显示效果、滚动跟随和点击跳转功能。文档包含从 h1 到 h6 各级标题,总计超过 40 个锚点。
4
+
5
+ ## 第一章 项目概述
6
+
7
+ 本章介绍项目的基本背景和目标定位,帮助读者快速了解项目全貌。
8
+
9
+ 项目启动于 2024 年初,旨在构建一套轻量级的文档站点生成工具。核心理念是让开发者专注于内容创作,而非繁琐的站点配置。我们希望通过简洁的设计和强大的功能,降低文档维护的门槛。
10
+
11
+ ### 1.1 背景介绍
12
+
13
+ 随着开源生态的蓬勃发展,越来越多的项目需要高质量的文档支持。然而现有的文档工具要么过于复杂,要么功能不足,难以满足中小型项目的需求。
14
+
15
+ 在调研了市面上主流的文档工具后,我们发现大多数工具存在以下问题:配置繁琐、构建缓慢、定制困难。因此我们决定从零开始,打造一款真正好用的文档工具。
16
+
17
+ #### 1.1.1 行业现状
18
+
19
+ 当前文档工具市场呈现两极分化的趋势。一方面是功能全面但学习曲线陡峭的重型方案,如 Docusaurus、GitBook 等;另一方面是轻量但功能有限的简易方案,如纯静态 Markdown 渲染器。
20
+
21
+ 我们的目标是在两者之间找到平衡点,提供开箱即用的体验,同时保留足够的扩展能力。
22
+
23
+ ##### 1.1.1.1 国内生态
24
+
25
+ 国内开发者对中文文档的支持有着特殊需求,包括中文搜索、中文排版优化、中文锚点生成等。这些细节往往被国外工具忽略,导致使用体验不佳。
26
+
27
+ 我们在设计之初就将中文支持作为一等公民,确保所有功能在中文环境下都能完美运行。
28
+
29
+ ###### 1.1.1.1.1 社区反馈
30
+
31
+ 通过社区调研,我们收集到了大量用户反馈。超过 70% 的用户表示希望有一款支持中文的轻量级文档工具。这坚定了我们的开发方向。
32
+
33
+ ###### 1.1.1.1.2 竞品分析
34
+
35
+ 我们对比了 VitePress、Docusaurus、Nextra、Rspress 等主流方案,分析了各自的优缺点,最终确定了差异化的产品定位。
36
+
37
+ ##### 1.1.1.2 国际趋势
38
+
39
+ 从全球范围来看,文档即代码(Docs as Code)的理念正在被越来越多的团队接受。Markdown 作为文档格式的事实标准,其生态也在不断完善。
40
+
41
+ #### 1.1.2 技术选型
42
+
43
+ 经过多轮评估,我们最终选择了以下技术栈:
44
+
45
+ - 前端框架:Vue 3 + Composition API
46
+ - 构建工具:Vite
47
+ - Markdown 解析:markdown-it
48
+ - 编辑器:Tiptap
49
+ - 样式方案:Tailwind CSS
50
+
51
+ ### 1.2 目标定位
52
+
53
+ 我们的目标是打造一款面向中小型项目的文档工具,核心特点包括:零配置启动、实时预览、全文搜索、多端适配。
54
+
55
+ #### 1.2.1 核心用户
56
+
57
+ 主要面向以下用户群体:
58
+
59
+ 1. 开源项目维护者
60
+ 2. 技术团队内部文档管理
61
+ 3. 个人知识库搭建
62
+ 4. 产品帮助文档编写
63
+
64
+ #### 1.2.2 使用场景
65
+
66
+ 典型的使用场景包括 API 文档、使用指南、架构设计文档、团队规范文档等。无论是公开的开源文档还是私有的内部文档,都能很好地支持。
67
+
68
+ ## 第二章 架构设计
69
+
70
+ 本章深入介绍系统的整体架构设计,包括前端架构、数据流和模块划分。
71
+
72
+ ### 2.1 整体架构
73
+
74
+ 系统采用经典的三层架构:展示层、逻辑层、数据层。展示层负责 UI 渲染和用户交互,逻辑层处理业务规则和状态管理,数据层负责文档的读取和持久化。
75
+
76
+ 整体架构遵循单向数据流的原则,所有状态变更都通过明确的路径传递,便于调试和维护。
77
+
78
+ #### 2.1.1 前端架构
79
+
80
+ 前端采用 Vue 3 的 Composition API 进行开发,通过 composables 模式组织可复用逻辑。主要模块包括:
81
+
82
+ - 文档树管理(useDocTree)
83
+ - 文档内容管理(useDocManager)
84
+ - 搜索功能(useSearch)
85
+ - 滚动控制(useScroll)
86
+ - 移动端适配(useMobile)
87
+
88
+ 每个 composable 职责单一,通过组合的方式构建复杂功能,避免了传统 Options API 中逻辑分散的问题。
89
+
90
+ ##### 2.1.1.1 组件设计
91
+
92
+ 组件按照功能域划分,遵循单一职责原则。核心组件包括 AppSidebar(侧边栏)、DocContent(文档内容)、TableOfContents(目录大纲)、SearchPanel(搜索面板)等。
93
+
94
+ 每个组件都有清晰的 props 和 events 接口定义,组件间通过 provide/inject 或事件总线进行通信。
95
+
96
+ ###### 2.1.1.1.1 渲染组件
97
+
98
+ 渲染组件负责将 Markdown 内容转换为可视化的 HTML。支持代码高亮、数学公式、Mermaid 图表等扩展语法。渲染过程采用增量更新策略,避免不必要的 DOM 操作。
99
+
100
+ ###### 2.1.1.1.2 交互组件
101
+
102
+ 交互组件处理用户操作,如搜索、导航、编辑等。所有交互都经过防抖和节流处理,确保流畅的用户体验。
103
+
104
+ ##### 2.1.1.2 状态管理
105
+
106
+ 项目没有引入 Vuex 或 Pinia 等状态管理库,而是通过 Vue 3 的 reactive API 和 composables 模式实现轻量级状态管理。这种方式更加灵活,也减少了不必要的依赖。
107
+
108
+ #### 2.1.2 数据流设计
109
+
110
+ 数据流遵循单向流动原则:用户操作 -> 状态更新 -> 视图渲染。文档数据通过 API 层获取,经过解析和转换后存入响应式状态,最终驱动视图更新。
111
+
112
+ ### 2.2 模块划分
113
+
114
+ 系统按照功能域划分为以下核心模块,每个模块独立开发、独立测试。
115
+
116
+ #### 2.2.1 文档解析模块
117
+
118
+ 负责将 Markdown 源文件解析为结构化数据。支持标准 Markdown 语法以及多种扩展语法,包括 Frontmatter、数学公式、Mermaid 图表等。
119
+
120
+ 解析过程分为两个阶段:词法分析和语法分析。词法分析将源文本拆分为 token 序列,语法分析将 token 序列转换为 AST(抽象语法树)。
121
+
122
+ #### 2.2.2 路由模块
123
+
124
+ 路由模块基于 hash 模式实现,将 URL 路径映射到对应的文档文件。支持多级目录结构和中文路径,确保文档链接的稳定性和可分享性。
125
+
126
+ #### 2.2.3 搜索模块
127
+
128
+ 搜索模块实现了全文搜索功能,支持中文分词和关键词高亮。搜索索引在文档加载时构建,采用倒排索引结构,确保搜索响应速度。
129
+
130
+ #### 2.2.4 编辑模块
131
+
132
+ 编辑模块基于 Tiptap 编辑器实现,支持所见即所得的 Markdown 编辑体验。编辑器内置了丰富的工具栏和快捷键,降低了 Markdown 的学习门槛。
133
+
134
+ ### 2.3 性能优化
135
+
136
+ 性能是我们始终关注的重点。通过多种优化手段,确保文档站点在各种设备上都能流畅运行。
137
+
138
+ #### 2.3.1 懒加载策略
139
+
140
+ 文档内容采用按需加载策略,只有当用户导航到某个文档时才会请求其内容。同时对图片和代码块实现了视口内懒加载,减少初始加载时间。
141
+
142
+ #### 2.3.2 缓存机制
143
+
144
+ 多层缓存机制确保重复访问的文档能够快速展示。包括内存缓存、浏览器缓存和 Service Worker 缓存三个层级,根据数据的时效性选择合适的缓存策略。
145
+
146
+ ## 第三章 功能实现
147
+
148
+ 本章详细介绍各核心功能的实现细节和技术方案。
149
+
150
+ ### 3.1 Markdown 渲染
151
+
152
+ Markdown 渲染是文档工具的核心能力。我们基于 markdown-it 实现了完整的渲染管线,支持标准语法和多种扩展。
153
+
154
+ 渲染管线的设计遵循插件化原则,每种扩展语法都通过独立的插件实现,便于维护和扩展。
155
+
156
+ #### 3.1.1 代码高亮
157
+
158
+ 代码高亮基于 highlight.js 实现,支持超过 180 种编程语言的语法高亮。同时提供了行号显示、代码复制、语言标签等增强功能。
159
+
160
+ ##### 3.1.1.1 主题定制
161
+
162
+ 支持自定义代码高亮主题,内置了多套配色方案。用户可以通过 CSS 变量轻松切换主题,也可以完全自定义配色。
163
+
164
+ ##### 3.1.1.2 性能优化
165
+
166
+ 对于超长代码块,采用虚拟滚动技术,只渲染视口内的代码行,避免大量 DOM 节点导致的性能问题。
167
+
168
+ #### 3.1.2 数学公式
169
+
170
+ 数学公式渲染基于 KaTeX 实现,支持行内公式和块级公式。KaTeX 相比 MathJax 有更快的渲染速度,适合文档场景。
171
+
172
+ 行内公式示例:$E = mc^2$,块级公式支持复杂的数学表达式。
173
+
174
+ #### 3.1.3 图表渲染
175
+
176
+ Mermaid 图表渲染支持流程图、时序图、甘特图、类图等多种图表类型。图表在文档加载时异步渲染,不阻塞主内容的展示。
177
+
178
+ ### 3.2 导航系统
179
+
180
+ 导航系统是文档站点的骨架,决定了用户浏览文档的效率和体验。
181
+
182
+ #### 3.2.1 目录树
183
+
184
+ 左侧目录树支持多级嵌套,自动从文件系统结构生成。支持展开/折叠、当前文档高亮、拖拽排序等交互功能。
185
+
186
+ 目录树的数据结构采用递归树形结构,每个节点包含标题、路径、子节点等信息。
187
+
188
+ ##### 3.2.1.1 排序规则
189
+
190
+ 文档排序支持两种模式:文件名前缀排序(如 00-、01-)和自然排序。前缀排序适合需要精确控制顺序的场景,自然排序适合随意组织的文档。
191
+
192
+ ##### 3.2.1.2 图标系统
193
+
194
+ 目录树中的图标根据节点类型自动匹配:文件夹使用目录图标,文档使用文件图标。展开/折叠状态也有对应的图标变化。
195
+
196
+ #### 3.2.2 文档大纲
197
+
198
+ 右侧文档大纲从当前文档内容中提取各级标题,生成可点击的导航列表。支持滚动跟随高亮和点击平滑跳转。
199
+
200
+ 这正是本文档重点测试的功能。当标题数量很多时,大纲列表需要自身支持滚动,并且保持良好的可读性。
201
+
202
+ #### 3.2.3 面包屑导航
203
+
204
+ 顶部面包屑显示当前文档在目录树中的完整路径,方便用户了解当前位置和快速跳转到上级目录。
205
+
206
+ #### 3.2.4 上下篇导航
207
+
208
+ 文档底部提供上一篇和下一篇的导航链接,方便用户按顺序阅读文档。导航顺序与目录树的排列顺序一致。
209
+
210
+ ### 3.3 搜索功能
211
+
212
+ 全文搜索是提升文档可用性的关键功能。用户可以通过关键词快速定位到目标内容。
213
+
214
+ #### 3.3.1 索引构建
215
+
216
+ 搜索索引在文档首次加载时构建,采用倒排索引结构。索引数据存储在内存中,支持增量更新。对于大型文档站点,索引构建过程在 Web Worker 中异步执行,不阻塞主线程。
217
+
218
+ #### 3.3.2 中文分词
219
+
220
+ 中文搜索的核心挑战是分词。我们采用了基于字典的正向最大匹配算法,结合 N-gram 模型,实现了较好的中文分词效果。
221
+
222
+ #### 3.3.3 结果排序
223
+
224
+ 搜索结果按照相关度排序,综合考虑关键词出现频率、位置权重(标题 > 正文)、文档权重等因素。排序算法参考了 BM25 模型。
225
+
226
+ ### 3.4 编辑功能
227
+
228
+ 内置编辑器让用户可以直接在浏览器中编辑文档,无需切换到代码编辑器。
229
+
230
+ #### 3.4.1 所见即所得
231
+
232
+ 基于 Tiptap 实现的所见即所得编辑器,用户在编辑时就能看到最终的渲染效果。支持常用的格式化操作,如加粗、斜体、列表、链接等。
233
+
234
+ #### 3.4.2 自动保存
235
+
236
+ 编辑器支持自动保存功能,在用户停止输入一段时间后自动将内容保存到服务端。同时提供手动保存的快捷键(Ctrl+S)。
237
+
238
+ #### 3.4.3 协同编辑
239
+
240
+ 未来计划支持多人协同编辑,基于 CRDT 算法实现冲突解决。目前已预留了协同编辑的接口设计。
241
+
242
+ ## 第四章 部署方案
243
+
244
+ 本章介绍项目的部署方式和配置选项。
245
+
246
+ ### 4.1 CLI 工具
247
+
248
+ 提供命令行工具 `markdoc` 用于项目初始化、开发预览和生产构建。
249
+
250
+ #### 4.1.1 初始化命令
251
+
252
+ 通过 `markdoc init` 命令可以快速创建一个新的文档项目,自动生成目录结构和配置文件。
253
+
254
+ #### 4.1.2 开发模式
255
+
256
+ `markdoc dev` 启动本地开发服务器,支持热更新和实时预览。开发服务器基于 Vite 构建,启动速度极快。
257
+
258
+ #### 4.1.3 生产构建
259
+
260
+ `markdoc build` 执行生产构建,生成优化后的静态文件。构建过程包括代码压缩、资源优化、预渲染等步骤。
261
+
262
+ ### 4.2 静态部署
263
+
264
+ 构建产物为纯静态文件,可以部署到任何静态文件托管服务。
265
+
266
+ #### 4.2.1 GitHub Pages
267
+
268
+ 支持一键部署到 GitHub Pages,通过 GitHub Actions 实现自动化构建和部署。只需在仓库中添加工作流配置文件即可。
269
+
270
+ #### 4.2.2 Vercel
271
+
272
+ Vercel 部署同样简单,连接 Git 仓库后自动检测项目类型并完成部署。支持自定义域名和 HTTPS。
273
+
274
+ #### 4.2.3 Nginx
275
+
276
+ 对于自建服务器的场景,提供了 Nginx 配置模板。包括 gzip 压缩、缓存策略、SPA 路由回退等最佳实践配置。
277
+
278
+ ### 4.3 自定义配置
279
+
280
+ 通过配置文件可以自定义站点的各项参数。
281
+
282
+ #### 4.3.1 站点信息
283
+
284
+ 配置站点标题、描述、Logo、favicon 等基本信息。这些信息会显示在页面标题栏和浏览器标签页中。
285
+
286
+ #### 4.3.2 主题配置
287
+
288
+ 支持自定义主题色、字体、间距等样式参数。通过 CSS 变量实现,修改简单且不影响功能。
289
+
290
+ #### 4.3.3 插件系统
291
+
292
+ 预留了插件系统的扩展接口,允许用户通过插件扩展文档站点的功能。插件可以注册新的 Markdown 语法、添加页面组件、扩展构建流程等。
293
+
294
+ ## 第五章 测试与质量
295
+
296
+ 本章介绍项目的测试策略和质量保障措施。
297
+
298
+ ### 5.1 单元测试
299
+
300
+ 核心模块都有对应的单元测试,覆盖率目标为 80% 以上。测试框架使用 Vitest,与 Vite 生态无缝集成。
301
+
302
+ #### 5.1.1 工具函数测试
303
+
304
+ 工具函数的测试最为直接,输入输出明确,易于编写和维护。包括路径处理、字符串转换、数据格式化等函数的测试。
305
+
306
+ #### 5.1.2 Composable 测试
307
+
308
+ Composable 的测试需要模拟 Vue 的响应式环境。通过 @vue/test-utils 提供的工具函数,可以在测试中创建响应式上下文。
309
+
310
+ ### 5.2 集成测试
311
+
312
+ 集成测试验证多个模块协同工作的正确性,重点关注数据流和状态同步。
313
+
314
+ ### 5.3 E2E 测试
315
+
316
+ 端到端测试基于 Playwright 实现,模拟真实用户操作验证完整的功能流程。测试覆盖了文档浏览、搜索、编辑、导航等核心场景。
317
+
318
+ ## 第六章 未来规划
319
+
320
+ 本章展望项目的未来发展方向和计划中的新功能。
321
+
322
+ ### 6.1 国际化支持
323
+
324
+ 计划支持多语言文档,允许同一文档提供不同语言的版本。语言切换通过 URL 前缀或下拉菜单实现。
325
+
326
+ ### 6.2 版本管理
327
+
328
+ 支持文档的多版本管理,用户可以查看不同版本的文档内容。版本切换不影响当前的浏览位置。
329
+
330
+ ### 6.3 AI 辅助
331
+
332
+ 探索 AI 技术在文档领域的应用,包括智能搜索、自动摘要、翻译辅助、写作建议等功能。
333
+
334
+ ### 6.4 性能监控
335
+
336
+ 内置性能监控面板,实时展示页面加载时间、渲染性能、内存占用等指标,帮助开发者优化文档站点的性能。
337
+
338
+ ---
339
+
340
+ 以上内容包含了从 h1 到 h6 共六个层级、超过 40 个标题锚点,用于验证右侧文档大纲在大量标题场景下的显示效果、滚动行为和交互体验。
@@ -5,9 +5,9 @@
5
5
  ## 断点设计
6
6
 
7
7
  | 屏幕宽度 | 模式 | 布局变化 |
8
- |----------|------|----------|
9
- | > 768px | 桌面端 | 三栏布局,侧边栏常驻 |
10
- | <= 768px | 移动端 | 单栏布局,抽屉式导航 |
8
+ | --- | --- | --- |
9
+ | &gt; 768px | 桌面端 | 三栏布局,侧边栏常驻 |
10
+ | &lt;= 768px | 移动端 | 单栏布局,抽屉式导航 |
11
11
 
12
12
  ## 移动端特性
13
13
 
@@ -34,4 +34,4 @@
34
34
  2. 点击汉堡菜单按钮,侧边栏应以抽屉形式弹出
35
35
  3. 点击遮罩应关闭抽屉
36
36
  4. 底部应显示 TOC 浮动按钮
37
- 5. 恢复窗口宽度后应自动切回桌面端布局
37
+ 5. 恢复窗口宽度后应自动切回桌面端布局
package/src/App.vue CHANGED
@@ -5,7 +5,7 @@
5
5
  v-if="isMobile"
6
6
  @open-drawer="mobileDrawerOpen = true"
7
7
  @go-home="goHome"
8
- @open-search="openSearch"
8
+ @open-search="mobileSearchOpen = true"
9
9
  />
10
10
  <!-- 桌面端顶栏 -->
11
11
  <TopBar
@@ -49,9 +49,15 @@
49
49
  <button v-if="!isMobile && sidebarCollapsed" class="expand-btn expand-btn-left" @click="sidebarCollapsed = false" title="展开导航">
50
50
  <ChevronRight :size="14" />
51
51
  </button>
52
+ <!-- 移动端搜索页(替换内容区) -->
53
+ <MobileSearch
54
+ v-if="isMobile && mobileSearchOpen"
55
+ @close="mobileSearchOpen = false"
56
+ @select="(key) => { mobileSearchOpen = false; handleSearchSelect(key) }"
57
+ />
52
58
  <!-- 内容区:查看模式 / 编辑模式 -->
53
59
  <DocContent
54
- v-if="!editMode"
60
+ v-else-if="!editMode"
55
61
  :showWelcome="showWelcome"
56
62
  :htmlContent="htmlContent"
57
63
  :prevDoc="prevDoc"
@@ -110,7 +116,7 @@
110
116
  </template>
111
117
 
112
118
  <script setup>
113
- import { ref, onMounted, nextTick, watch } from 'vue'
119
+ import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
114
120
  import { ArrowUp, ChevronRight, ChevronLeft } from 'lucide-vue-next'
115
121
  import MobileHeader from './components/MobileHeader.vue'
116
122
  import TopBar from './components/TopBar.vue'
@@ -120,12 +126,12 @@ import EditorContentVue from './components/EditorContent.vue'
120
126
  import TableOfContents from './components/TableOfContents.vue'
121
127
  import MobileToc from './components/MobileToc.vue'
122
128
  import ImageZoom from './components/ImageZoom.vue'
129
+ import MobileSearch from './components/MobileSearch.vue'
123
130
  import { useDocManager } from './composables/useDocManager.js'
124
131
  import { useResize } from './composables/useResize.js'
125
132
  import { useFileWatcher } from './composables/useFileWatcher.js'
126
133
  import { resetContentEtag } from './services/DocService.js'
127
134
 
128
- import { useSearch } from './composables/useSearch.js'
129
135
  import { useMobile } from './composables/useMobile.js'
130
136
 
131
137
 
@@ -133,9 +139,9 @@ import { useMobile } from './composables/useMobile.js'
133
139
  const sidebarCollapsed = ref(sessionStorage.getItem('sidebarCollapsed') === 'true')
134
140
  const tocCollapsed = ref(sessionStorage.getItem('tocCollapsed') === 'true')
135
141
  const zoomVisible = ref(false)
136
- const zoomContent = ref('')
137
142
  const zoomImages = ref([])
138
143
  const zoomIndex = ref(0)
144
+ const mobileSearchOpen = ref(false)
139
145
 
140
146
  // composables
141
147
  const {
@@ -153,7 +159,6 @@ const {
153
159
 
154
160
  const { sidebarWidth, tocWidth, startResize } = useResize()
155
161
 
156
- const { openSearch } = useSearch()
157
162
  const { isMobile, mobileDrawerOpen, mobileTocOpen } = useMobile()
158
163
 
159
164
  // 文件监听(DocService 统一处理模式检测和 ETag)
@@ -163,72 +168,37 @@ useFileWatcher({
163
168
  onDocContentChange: (content) => reloadCurrentDoc(content)
164
169
  })
165
170
 
166
- // 切换编辑模式(基于锚点/标题文本保持阅读位置)
171
+ // 切换编辑模式(记住当前锚点,切换后复用 scrollToHeading 恢复位置)
167
172
  function onToggleEdit(isEdit) {
173
+ // 记住当前激活的锚点
174
+ const savedAnchor = activeHeading.value
175
+ // 记录滚动比例作为兜底
168
176
  const contentEl = document.querySelector('.content')
169
- let anchorId = ''
170
- let headingText = ''
171
177
  let scrollRatio = 0
172
-
173
178
  if (contentEl) {
174
- const headings = contentEl.querySelectorAll('.markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6')
175
- const contentRect = contentEl.getBoundingClientRect()
176
- const scrollTop = contentEl.scrollTop
177
-
178
- // 找到当前视口中最近的标题(最后一个已滚过顶部的)
179
- for (const heading of headings) {
180
- const rect = heading.getBoundingClientRect()
181
- const offsetFromTop = rect.top - contentRect.top
182
- if (offsetFromTop <= 80) {
183
- anchorId = heading.id || ''
184
- // 提取纯文本(去掉锚点图标等)
185
- const clone = heading.cloneNode(true)
186
- clone.querySelectorAll('.heading-anchor').forEach(a => a.remove())
187
- headingText = clone.textContent.trim()
188
- }
189
- }
190
-
191
- // 记录滚动比例作为兜底
192
179
  const scrollHeight = contentEl.scrollHeight - contentEl.clientHeight
193
- scrollRatio = scrollHeight > 0 ? scrollTop / scrollHeight : 0
180
+ scrollRatio = scrollHeight > 0 ? contentEl.scrollTop / scrollHeight : 0
194
181
  }
195
182
 
196
183
  editMode.value = isEdit
197
184
  sessionStorage.setItem('editMode', isEdit)
198
185
 
186
+ // 等待 DOM 渲染完成后恢复位置
199
187
  nextTick(() => {
200
- const newContentEl = document.querySelector('.content')
201
- if (!newContentEl) return
202
-
203
- // 策略1:通过 id 定位(查看模式有 id)
204
- if (anchorId) {
205
- const target = document.getElementById(anchorId)
206
- if (target) {
207
- target.scrollIntoView({ block: 'start' })
208
- activeHeading.value = anchorId
209
- return
210
- }
211
- }
212
-
213
- // 策略2:通过标题文本匹配(编辑模式无 id,但文本一致)
214
- if (headingText) {
215
- const headings = newContentEl.querySelectorAll('h1, h2, h3, h4, h5, h6')
216
- for (const heading of headings) {
217
- const clone = heading.cloneNode(true)
218
- clone.querySelectorAll('.heading-anchor').forEach(a => a.remove())
219
- if (clone.textContent.trim() === headingText) {
220
- heading.scrollIntoView({ block: 'start' })
221
- if (heading.id) activeHeading.value = heading.id
222
- return
188
+ // 再等一帧,确保 tiptap 编辑器完成渲染
189
+ requestAnimationFrame(() => {
190
+ const newContentEl = document.querySelector('.content')
191
+ if (!newContentEl) return
192
+
193
+ if (savedAnchor) {
194
+ scrollToHeading(savedAnchor)
195
+ } else if (scrollRatio > 0) {
196
+ const scrollHeight = newContentEl.scrollHeight - newContentEl.clientHeight
197
+ if (scrollHeight > 0) {
198
+ newContentEl.scrollTop = scrollRatio * scrollHeight
223
199
  }
224
200
  }
225
- }
226
-
227
- // 策略3:按滚动比例恢复
228
- const scrollHeight = newContentEl.scrollHeight - newContentEl.clientHeight
229
- if (scrollHeight > 0) {
230
- newContentEl.scrollTop = scrollRatio * scrollHeight
231
- }
201
+ })
232
202
  })
233
203
  }
234
204
 
@@ -256,15 +226,20 @@ function onContentClick(event) {
256
226
  })
257
227
  }
258
228
 
259
- // 全局快捷键
260
- window.addEventListener('popstate', () => loadFromUrl())
229
+ // popstate 事件监听(生命周期管理)
230
+ function onPopstate() { loadFromUrl() }
261
231
 
262
232
  // 持久化 UI 状态
263
233
  watch(sidebarCollapsed, (v) => sessionStorage.setItem('sidebarCollapsed', v))
264
234
  watch(tocCollapsed, (v) => sessionStorage.setItem('tocCollapsed', v))
265
235
 
266
236
  onMounted(async () => {
237
+ window.addEventListener('popstate', onPopstate)
267
238
  await loadDocsList()
268
239
  await loadFromUrl()
269
240
  })
241
+
242
+ onBeforeUnmount(() => {
243
+ window.removeEventListener('popstate', onPopstate)
244
+ })
270
245
  </script>