md2ui 1.0.8 → 1.0.9

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.
package/bin/build.js CHANGED
@@ -232,7 +232,7 @@ async function renderMarkdownToHtml(markdown, currentDocKey, docsList) {
232
232
 
233
233
  // 纯锚点
234
234
  if (decoded.startsWith('#')) {
235
- const anchor = slugger.slug(decoded.slice(1), false)
235
+ const anchor = decoded.slice(1)
236
236
  return `<a href="#${anchor}"${titleAttr}>${text}</a>`
237
237
  }
238
238
 
@@ -243,9 +243,8 @@ async function renderMarkdownToHtml(markdown, currentDocKey, docsList) {
243
243
  const doc = findDocInTree(docsList, targetKey)
244
244
  if (doc) {
245
245
  const hash = docHash(doc.key)
246
- const anchorSlug = anchor ? slugger.slug(anchor, false) : ''
247
- const url = anchorSlug ? `/${hash}.html#${anchorSlug}` : `/${hash}.html`
248
- return `<a href="${url}" data-doc-key="${doc.key}"${anchorSlug ? ` data-anchor="${anchorSlug}"` : ''}${titleAttr}>${text}</a>`
246
+ const url = anchor ? `/${hash}.html#${anchor}` : `/${hash}.html`
247
+ return `<a href="${url}" data-doc-key="${doc.key}"${anchor ? ` data-anchor="${anchor}"` : ''}${titleAttr}>${text}</a>`
249
248
  }
250
249
  return `<a href="javascript:void(0)" class="broken-link" title="文档未找到: ${decoded}">${text}</a>`
251
250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "md2ui",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "type": "module",
5
5
  "description": "将本地 Markdown 文档转换为美观的 HTML 页面",
6
6
  "author": "",
package/src/App.vue CHANGED
@@ -53,7 +53,7 @@
53
53
  />
54
54
  <!-- 桌面端 TOC -->
55
55
  <div v-if="!isMobile && !tocCollapsed && tocItems.length > 0" class="resizer resizer-right" @mousedown="startResize('right', $event)"></div>
56
- <TableOfContents v-if="!isMobile" :tocItems="tocItems" :activeHeading="activeHeading" :collapsed="tocCollapsed" :width="tocWidth" @toggle="tocCollapsed = !tocCollapsed" @scroll-to="scrollToHeading" />
56
+ <TableOfContents v-if="!isMobile" :tocItems="tocItems" :activeHeading="activeHeading" :collapsed="tocCollapsed" :width="tocWidth" @toggle="tocCollapsed = !tocCollapsed" @scroll-to="(id) => scrollToHeading(id, { push: true })" />
57
57
  <button v-if="!isMobile && tocCollapsed && tocItems.length > 0" class="expand-btn expand-btn-right" @click="tocCollapsed = false" title="展开目录">
58
58
  <ChevronLeft :size="14" />
59
59
  </button>
@@ -67,7 +67,7 @@
67
67
  :showWelcome="showWelcome"
68
68
  @toggle="mobileTocOpen = !mobileTocOpen"
69
69
  @close="mobileTocOpen = false"
70
- @scroll-to="(id) => { scrollToHeading(id); mobileTocOpen = false }"
70
+ @scroll-to="(id) => { scrollToHeading(id, { push: true }); mobileTocOpen = false }"
71
71
  />
72
72
  <!-- 返回顶部 -->
73
73
  <transition name="fade">
@@ -4,7 +4,7 @@ import { useMarkdown } from './useMarkdown.js'
4
4
  import { useSearch } from './useSearch.js'
5
5
  import { useScroll } from './useScroll.js'
6
6
  import { useMobile } from './useMobile.js'
7
- import { findDoc, findFirstDoc, findDocByHash, expandParents, flattenDocsList, expandAll, collapseAll } from './useDocTree.js'
7
+ import { findDoc, findFirstDoc, findReadmeDoc, findDocByHash, expandParents, flattenDocsList, expandAll, collapseAll } from './useDocTree.js'
8
8
 
9
9
  // 等待内容区域图片加载完成
10
10
  async function waitForContentImages(timeoutMs = 3000) {
@@ -36,23 +36,38 @@ export function useDocManager() {
36
36
  } = useScroll()
37
37
  const { isMobile, mobileDrawerOpen } = useMobile()
38
38
 
39
- // 滚动处理,同步锚点到 URL
39
+ // 获取当前滚动位置
40
+ function getScrollTop() {
41
+ const el = document.querySelector('.content')
42
+ return el ? el.scrollTop : 0
43
+ }
44
+
45
+ // 构造 history state,保留滚动位置
46
+ function makeState(scrollTop) {
47
+ return { scrollTop: scrollTop ?? getScrollTop() }
48
+ }
49
+
50
+ // 滚动处理,同步锚点到 URL(replaceState 保留滚动位置)
40
51
  function handleScroll(e) {
41
52
  _handleScroll(e)
42
53
  if (activeHeading.value && currentDoc.value) {
43
- history.replaceState(null, '', `/${docHash(currentDoc.value)}#${activeHeading.value}`)
54
+ history.replaceState(makeState(), '', `/${docHash(currentDoc.value)}#${activeHeading.value}`)
44
55
  }
45
56
  }
46
57
 
47
58
  // push: true 表示用户主动点击锚点,产生可回退的历史条目
48
59
  function scrollToHeading(id, { push = false } = {}) {
60
+ if (push && currentDoc.value) {
61
+ // 先把当前滚动位置写入即将被覆盖的历史条目
62
+ history.replaceState(makeState(), '', window.location.href)
63
+ }
49
64
  _scrollToHeading(id)
50
65
  if (currentDoc.value) {
51
66
  const url = `/${docHash(currentDoc.value)}#${id}`
52
67
  if (push) {
53
- history.pushState(null, '', url)
68
+ history.pushState(makeState(), '', url)
54
69
  } else {
55
- history.replaceState(null, '', url)
70
+ history.replaceState(makeState(), '', url)
56
71
  }
57
72
  }
58
73
  }
@@ -63,26 +78,33 @@ export function useDocManager() {
63
78
  buildIndex(docsList.value)
64
79
  }
65
80
 
66
- // 回到欢迎页
67
- function goHome() {
81
+ // 回到欢迎页(isPopstate: true 表示由浏览器回退触发,不操作 history)
82
+ function goHome({ isPopstate = false } = {}) {
68
83
  currentDoc.value = ''
69
84
  showWelcome.value = true
70
85
  htmlContent.value = ''
71
86
  tocItems.value = []
72
- history.pushState(null, '', '/')
87
+ if (!isPopstate) {
88
+ // 用户主动点击,保存旧条目滚动位置后 push
89
+ history.replaceState(makeState(), '', window.location.href)
90
+ history.pushState(makeState(0), '', '/')
91
+ }
73
92
  if (isMobile.value) mobileDrawerOpen.value = false
74
93
  }
75
94
 
76
95
  // 加载文档
77
- async function loadDoc(key, { replace = false, anchor = '' } = {}) {
96
+ async function loadDoc(key, { replace = false, anchor = '', keepState = false } = {}) {
78
97
  currentDoc.value = key
79
98
  showWelcome.value = false
80
99
  const hash = docHash(key)
81
100
  const url = anchor ? `/${hash}#${anchor}` : `/${hash}`
82
101
  if (replace) {
83
- history.replaceState(null, '', url)
102
+ // keepState: popstate 回退时保留浏览器已有的 state(含 scrollTop)
103
+ if (!keepState) history.replaceState(makeState(0), '', url)
84
104
  } else {
85
- history.pushState(null, '', url)
105
+ // push 前先保存当前滚动位置到旧条目
106
+ history.replaceState(makeState(), '', window.location.href)
107
+ history.pushState(makeState(0), '', url)
86
108
  }
87
109
  const doc = findDoc(docsList.value, key)
88
110
  if (!doc) return
@@ -175,22 +197,35 @@ export function useDocManager() {
175
197
  async function loadFromUrl() {
176
198
  const pathname = window.location.pathname.replace(/^\//, '')
177
199
  const anchor = window.location.hash.replace('#', '')
200
+ const savedScroll = history.state?.scrollTop
178
201
  if (!pathname) {
179
202
  if (currentDoc.value) {
180
- // 从文档页回退到首页
181
- goHome()
203
+ // 浏览器回退到首页,不操作 history
204
+ goHome({ isPopstate: true })
182
205
  } else if (docsList.value.length === 0) {
183
206
  showWelcome.value = false
184
207
  renderMarkdown('# 当前目录没有 Markdown 文档\n\n请在当前目录下添加 `.md` 文件,然后刷新页面。')
208
+ } else {
209
+ // 优先定位到 README,没有则定位到第一篇文档
210
+ const readme = findReadmeDoc(docsList.value)
211
+ const target = readme || findFirstDoc(docsList.value)
212
+ if (target) {
213
+ expandParents(docsList.value, target.key)
214
+ await loadDoc(target.key, { replace: true })
215
+ }
185
216
  }
186
217
  return
187
218
  }
188
219
  const doc = findDocByHash(docsList.value, pathname, docHash)
189
220
  if (!doc) return
190
- // 同一文档内的锚点变化,只需滚动,无需重新加载
221
+ // 同一文档内的锚点变化(含回退)
191
222
  if (doc.key === currentDoc.value) {
192
- if (anchor) {
193
- await nextTick()
223
+ await nextTick()
224
+ if (savedScroll != null) {
225
+ // 有保存的滚动位置,直接恢复(回退场景)
226
+ const contentEl = document.querySelector('.content')
227
+ if (contentEl) contentEl.scrollTo({ top: savedScroll, behavior: 'smooth' })
228
+ } else if (anchor) {
194
229
  _scrollToHeading(decodeURIComponent(anchor))
195
230
  } else {
196
231
  const contentEl = document.querySelector('.content')
@@ -199,8 +234,14 @@ export function useDocManager() {
199
234
  return
200
235
  }
201
236
  expandParents(docsList.value, doc.key)
202
- await loadDoc(doc.key, { replace: true, anchor: anchor ? decodeURIComponent(anchor) : '' })
203
- if (anchor) { await nextTick(); await waitForContentImages(); _scrollToHeading(decodeURIComponent(anchor)) }
237
+ await loadDoc(doc.key, { replace: true, keepState: savedScroll != null, anchor: anchor ? decodeURIComponent(anchor) : '' })
238
+ if (savedScroll != null) {
239
+ await nextTick()
240
+ const contentEl = document.querySelector('.content')
241
+ if (contentEl) contentEl.scrollTo({ top: savedScroll })
242
+ } else if (anchor) {
243
+ await nextTick(); await waitForContentImages(); _scrollToHeading(decodeURIComponent(anchor))
244
+ }
204
245
  }
205
246
 
206
247
  return {
@@ -24,6 +24,22 @@ export function findFirstDoc(items) {
24
24
  return null
25
25
  }
26
26
 
27
+ // 查找 README 文档(不区分大小写,优先根目录)
28
+ export function findReadmeDoc(items) {
29
+ // 先在当前层级找
30
+ for (const item of items) {
31
+ if (item.type === 'file' && /^readme$/i.test(item.key?.split('/').pop() || '')) return item
32
+ }
33
+ // 再递归子目录找
34
+ for (const item of items) {
35
+ if (item.type === 'folder' && item.children) {
36
+ const found = findReadmeDoc(item.children)
37
+ if (found) return found
38
+ }
39
+ }
40
+ return null
41
+ }
42
+
27
43
  // 根据 hash 查找文档
28
44
  export function findDocByHash(items, hash, docHash) {
29
45
  for (const item of items) {
package/src/style.css CHANGED
@@ -7,8 +7,8 @@
7
7
  --color-text: #1a1e1b;
8
8
  --color-text-secondary: #505856;
9
9
  --color-text-tertiary: #848e89;
10
- --color-border: #d4dbd6;
11
- --color-border-light: #e4e9e5;
10
+ --color-border: rgba(66, 184, 131, 0.25);
11
+ --color-border-light: rgba(66, 184, 131, 0.15);
12
12
  --color-accent: #42b883;
13
13
  --color-accent-hover: #359d6e;
14
14
  --color-accent-bg: rgba(66, 184, 131, 0.1);
@@ -18,7 +18,7 @@
18
18
  --color-code-border: transparent;
19
19
  --color-pre-bg: #ffffff;
20
20
  --color-pre-header: #f7f9f8;
21
- --color-pre-border: #d4dbd6;
21
+ --color-pre-border: rgba(66, 184, 131, 0.25);
22
22
  --color-pre-text: #1a1e1b;
23
23
  --color-blockquote-border: #42b883;
24
24
  --color-blockquote-bg: rgba(66, 184, 131, 0.03);
@@ -1074,7 +1074,7 @@ body {
1074
1074
  align-items: center;
1075
1075
  padding: 8px 16px;
1076
1076
  background: var(--color-bg);
1077
- border-bottom: 2px solid var(--color-accent);
1077
+ border-bottom: 1px solid var(--color-border);
1078
1078
  flex-shrink: 0;
1079
1079
  gap: 12px;
1080
1080
  z-index: 100;