@tnotesjs/core 0.1.0

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.

Potentially problematic release.


This version of @tnotesjs/core might be problematic. Click here for more details.

Files changed (117) hide show
  1. package/README.md +105 -0
  2. package/dist/chunk-K3X5OP3N.js +1532 -0
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.js +4199 -0
  5. package/dist/index.d.ts +138 -0
  6. package/dist/index.js +9 -0
  7. package/package.json +74 -0
  8. package/types/config.ts +61 -0
  9. package/types/index.ts +11 -0
  10. package/types/note.ts +33 -0
  11. package/vitepress/assets/icons/icon__check.svg +3 -0
  12. package/vitepress/assets/icons/icon__clipboard.svg +8 -0
  13. package/vitepress/assets/icons/icon__close.svg +1 -0
  14. package/vitepress/assets/icons/icon__collapse.svg +1 -0
  15. package/vitepress/assets/icons/icon__confirm.svg +1 -0
  16. package/vitepress/assets/icons/icon__copy.svg +4 -0
  17. package/vitepress/assets/icons/icon__focus.svg +1 -0
  18. package/vitepress/assets/icons/icon__fold.svg +3 -0
  19. package/vitepress/assets/icons/icon__folder.svg +1 -0
  20. package/vitepress/assets/icons/icon__fullscreen.svg +1 -0
  21. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -0
  22. package/vitepress/assets/icons/icon__github.svg +4 -0
  23. package/vitepress/assets/icons/icon__mindmap.svg +1 -0
  24. package/vitepress/assets/icons/icon__next.svg +1 -0
  25. package/vitepress/assets/icons/icon__number_gray.svg +1 -0
  26. package/vitepress/assets/icons/icon__number_purple.svg +1 -0
  27. package/vitepress/assets/icons/icon__prev.svg +1 -0
  28. package/vitepress/assets/icons/icon__restore.svg +1 -0
  29. package/vitepress/assets/icons/icon__rotate.svg +4 -0
  30. package/vitepress/assets/icons/icon__search.svg +1 -0
  31. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -0
  32. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -0
  33. package/vitepress/assets/icons/icon__totop.svg +6 -0
  34. package/vitepress/assets/icons/icon__vscode.svg +6 -0
  35. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -0
  36. package/vitepress/assets/icons/icon__zoom_in.svg +1 -0
  37. package/vitepress/assets/icons/icon__zoom_out.svg +1 -0
  38. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -0
  39. package/vitepress/assets/icons/index.ts +38 -0
  40. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -0
  41. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -0
  42. package/vitepress/components/CodeBlockFullscreen/index.ts +115 -0
  43. package/vitepress/components/CodeBlockFullscreen/styles.css +64 -0
  44. package/vitepress/components/Discussions/Discussions.module.scss +32 -0
  45. package/vitepress/components/Discussions/Discussions.vue +211 -0
  46. package/vitepress/components/EnWordList/EnWordList.module.scss +124 -0
  47. package/vitepress/components/EnWordList/EnWordList.vue +543 -0
  48. package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -0
  49. package/vitepress/components/EnWordList/RightClickMenu.vue +66 -0
  50. package/vitepress/components/Footprints/Footprints.module.scss +93 -0
  51. package/vitepress/components/Footprints/Footprints.vue +377 -0
  52. package/vitepress/components/Layout/AboutModal.module.scss +233 -0
  53. package/vitepress/components/Layout/AboutModal.vue +105 -0
  54. package/vitepress/components/Layout/AboutPanel.vue +266 -0
  55. package/vitepress/components/Layout/ContentCollapse.vue +603 -0
  56. package/vitepress/components/Layout/CustomSidebar.vue +605 -0
  57. package/vitepress/components/Layout/DocBeforeControls.vue +139 -0
  58. package/vitepress/components/Layout/DocFooter.vue +225 -0
  59. package/vitepress/components/Layout/ImagePreview.module.scss +201 -0
  60. package/vitepress/components/Layout/ImagePreview.vue +281 -0
  61. package/vitepress/components/Layout/Layout.module.scss +661 -0
  62. package/vitepress/components/Layout/Layout.vue +542 -0
  63. package/vitepress/components/Layout/NoteStatus.vue +140 -0
  64. package/vitepress/components/Layout/SidebarItems.vue +263 -0
  65. package/vitepress/components/Layout/SidebarNavBefore.vue +92 -0
  66. package/vitepress/components/Layout/Swiper.vue +167 -0
  67. package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -0
  68. package/vitepress/components/Layout/ToggleFullContent.vue +34 -0
  69. package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -0
  70. package/vitepress/components/Layout/ToggleSidebar.vue +35 -0
  71. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -0
  72. package/vitepress/components/Layout/composables/useNoteConfig.ts +121 -0
  73. package/vitepress/components/Layout/composables/useNoteSave.ts +173 -0
  74. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -0
  75. package/vitepress/components/Layout/composables/useRedirect.ts +110 -0
  76. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -0
  77. package/vitepress/components/Layout/homeReadme.data.ts +124 -0
  78. package/vitepress/components/LoadingPage/LoadingPage.vue +192 -0
  79. package/vitepress/components/MarkMap/MarkMap.module.scss +159 -0
  80. package/vitepress/components/MarkMap/MarkMap.vue +404 -0
  81. package/vitepress/components/Mermaid/Mermaid.module.scss +275 -0
  82. package/vitepress/components/Mermaid/Mermaid.vue +364 -0
  83. package/vitepress/components/NotesTable/NotesTable.module.scss +77 -0
  84. package/vitepress/components/NotesTable/NotesTable.vue +98 -0
  85. package/vitepress/components/NotesTable/README.md +67 -0
  86. package/vitepress/components/Settings/Settings.module.scss +433 -0
  87. package/vitepress/components/Settings/Settings.vue +306 -0
  88. package/vitepress/components/SidebarCard/MindMapView.vue +483 -0
  89. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -0
  90. package/vitepress/components/SidebarCard/SidebarCard.vue +948 -0
  91. package/vitepress/components/Tooltip/Tooltip.vue +70 -0
  92. package/vitepress/components/constants.ts +91 -0
  93. package/vitepress/components/notesConfig.data.ts +73 -0
  94. package/vitepress/components/sidebar.data.ts +59 -0
  95. package/vitepress/components/tnotes-config.data.ts +21 -0
  96. package/vitepress/components/utils.ts +26 -0
  97. package/vitepress/config/index.ts +126 -0
  98. package/vitepress/configs/constants.ts +26 -0
  99. package/vitepress/configs/head.config.ts +25 -0
  100. package/vitepress/configs/index.ts +9 -0
  101. package/vitepress/configs/markdown-it.d.ts +23 -0
  102. package/vitepress/configs/markdown.config.ts +366 -0
  103. package/vitepress/configs/theme.config.ts +108 -0
  104. package/vitepress/plugins/buildProgressPlugin.ts +390 -0
  105. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -0
  106. package/vitepress/plugins/renameNotePlugin.ts +60 -0
  107. package/vitepress/plugins/updateConfigPlugin.ts +63 -0
  108. package/vitepress/theme/index.ts +95 -0
  109. package/vitepress/theme/styles/base.scss +50 -0
  110. package/vitepress/theme/styles/components/404.scss +31 -0
  111. package/vitepress/theme/styles/components/collapse.scss +175 -0
  112. package/vitepress/theme/styles/components/markmap.scss +101 -0
  113. package/vitepress/theme/styles/components/swiper.scss +255 -0
  114. package/vitepress/theme/styles/index.scss +25 -0
  115. package/vitepress/theme/styles/layout.scss +62 -0
  116. package/vitepress/theme/styles/utilities.scss +39 -0
  117. package/vitepress/theme/styles/vitepress-override.scss +25 -0
@@ -0,0 +1,263 @@
1
+ <template>
2
+ <template v-for="item in items" :key="getItemKey(item)">
3
+ <!-- 只有在不超过最大层级时才渲染组 -->
4
+ <div
5
+ v-if="hasChildren(item) && depth < maxDepth - 1"
6
+ class="group"
7
+ :style="getGroupStyle(depth)"
8
+ >
9
+ <button
10
+ class="group-title"
11
+ :class="`group-title-level-${depth}`"
12
+ @click="toggleItem(item)"
13
+ >
14
+ <span>{{ item.text }}</span>
15
+
16
+ <span class="arrow" :class="{ collapsed: item.collapsed }">
17
+ <img
18
+ :src="
19
+ item.collapsed ? icon__sidebar_collapsed : icon__sidebar_opened
20
+ "
21
+ alt=""
22
+ />
23
+ </span>
24
+ </button>
25
+
26
+ <div v-show="!item.collapsed" class="group-items">
27
+ <SidebarItems
28
+ :items="item.items"
29
+ :depth="depth + 1"
30
+ :max-depth="maxDepth"
31
+ :show-note-id="showNoteId"
32
+ :base="base"
33
+ :current-path="currentPath"
34
+ :item-depth="depth"
35
+ />
36
+ </div>
37
+ </div>
38
+
39
+ <!-- 如果是链接项,或者是超过最大层级的组,都不渲染 -->
40
+ <a
41
+ v-else-if="!hasChildren(item)"
42
+ :href="getFullLink(item.link)"
43
+ :class="[
44
+ 'nav-item',
45
+ { active: isActive(item.link) },
46
+ `nav-item-${extractNoteIdFromLink(item.link)}`,
47
+ `nav-item-level-${actualItemDepth + 1}`,
48
+ ]"
49
+ :data-note-id="extractNoteIdFromLink(item.link)"
50
+ :style="getItemStyle(actualItemDepth)"
51
+ >
52
+ {{ getNoteDisplayText(item.text, item.link) }}
53
+ </a>
54
+ </template>
55
+ </template>
56
+
57
+ <script setup lang="ts">
58
+ import { computed } from 'vue'
59
+ import { icon__sidebar_opened, icon__sidebar_collapsed } from '../../assets/icons'
60
+
61
+ interface SidebarItem {
62
+ text: string
63
+ link?: string
64
+ items?: SidebarItem[]
65
+ collapsed?: boolean
66
+ }
67
+
68
+ interface Props {
69
+ items: SidebarItem[]
70
+ depth: number
71
+ maxDepth: number
72
+ showNoteId: boolean
73
+ base: string
74
+ currentPath: string
75
+ itemDepth?: number // 用于计算链接项的缩进,默认等于 depth
76
+ }
77
+
78
+ const props = withDefaults(defineProps<Props>(), {
79
+ depth: 0,
80
+ maxDepth: 3,
81
+ showNoteId: false,
82
+ base: '/',
83
+ currentPath: '',
84
+ itemDepth: undefined,
85
+ })
86
+
87
+ // 获取实际的 item depth(用于链接项的缩进)
88
+ const actualItemDepth = computed(() => props.itemDepth ?? props.depth)
89
+
90
+ // 判断项是否有子项
91
+ function hasChildren(item: SidebarItem): boolean {
92
+ return !!(item.items && item.items.length > 0)
93
+ }
94
+
95
+ // 获取项的唯一 key
96
+ function getItemKey(item: SidebarItem): string {
97
+ return item.link || item.text
98
+ }
99
+
100
+ // 切换项的展开/折叠状态
101
+ function toggleItem(item: SidebarItem) {
102
+ item.collapsed = !item.collapsed
103
+ }
104
+
105
+ // 根据深度获取组的缩进样式
106
+ function getGroupStyle(depth: number) {
107
+ // 从第二层组开始才需要缩进(depth >= 1)
108
+ if (depth === 0) {
109
+ return {}
110
+ }
111
+ return {
112
+ paddingLeft: `${depth * 16}px`,
113
+ }
114
+ }
115
+
116
+ // 根据深度获取项的缩进样式
117
+ function getItemStyle(depth: number) {
118
+ // 项的缩进基于其父组的depth
119
+ // 第一层组(depth=0)的子项不需要额外缩进
120
+ // 第二层组(depth=1)的子项需要缩进 16px
121
+ if (depth === 0) {
122
+ return {}
123
+ }
124
+ return {
125
+ paddingLeft: `${depth * 16}px`,
126
+ }
127
+ }
128
+
129
+ // 获取完整链接(包含 base)
130
+ function getFullLink(link?: string) {
131
+ if (!link) return '#'
132
+ const cleanLink = link.startsWith('/') ? link.slice(1) : link
133
+ return props.base + cleanLink
134
+ }
135
+
136
+ // 判断链接是否激活
137
+ function isActive(link?: string) {
138
+ if (!link) return false
139
+ const fullLink = getFullLink(link)
140
+ const decodedRoutePath = decodeURIComponent(props.currentPath)
141
+ const decodedFullLink = decodeURIComponent(fullLink)
142
+
143
+ return (
144
+ decodedRoutePath === decodedFullLink ||
145
+ decodedRoutePath === decodedFullLink + '.html'
146
+ )
147
+ }
148
+
149
+ // 从链接中提取笔记 ID
150
+ function extractNoteIdFromLink(link?: string): string | null {
151
+ if (!link) return null
152
+ const match = link.match(/\/notes\/(\d{4})\./)
153
+ return match ? match[1] : null
154
+ }
155
+
156
+ // 提取文本开头的 emoji
157
+ function extractEmoji(text: string): { emoji: string; rest: string } {
158
+ const emojiMatch = text.match(
159
+ /^([\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}✅❌⏰]+)\s*/u
160
+ )
161
+
162
+ if (emojiMatch) {
163
+ return {
164
+ emoji: emojiMatch[1],
165
+ rest: text.slice(emojiMatch[0].length),
166
+ }
167
+ }
168
+
169
+ return { emoji: '', rest: text }
170
+ }
171
+
172
+ // 获取笔记显示文本
173
+ function getNoteDisplayText(text: string, link?: string): string {
174
+ const { emoji, rest } = extractEmoji(text)
175
+
176
+ if (props.showNoteId) {
177
+ if (/^\d{4}\./.test(rest)) {
178
+ return emoji ? `${emoji} ${rest}` : rest
179
+ }
180
+
181
+ const noteId = extractNoteIdFromLink(link)
182
+ if (noteId) {
183
+ return emoji ? `${emoji} ${noteId}. ${rest}` : `${noteId}. ${rest}`
184
+ }
185
+
186
+ return text
187
+ } else {
188
+ const cleanRest = rest.replace(/^\d{4}\.\s*/, '')
189
+ return emoji ? `${emoji} ${cleanRest}` : cleanRest
190
+ }
191
+ }
192
+ </script>
193
+
194
+ <style scoped>
195
+ .group {
196
+ margin-bottom: 16px;
197
+ }
198
+
199
+ .group-title {
200
+ display: flex;
201
+ justify-content: space-between;
202
+ align-items: center;
203
+ width: 100%;
204
+ padding: 6px 0;
205
+ font-weight: 600;
206
+ color: var(--vp-c-text-1);
207
+ background: none;
208
+ border: none;
209
+ cursor: pointer;
210
+ text-align: left;
211
+ transition: color 0.25s;
212
+ }
213
+
214
+ .group-title:hover {
215
+ color: var(--vp-c-brand-1);
216
+ }
217
+
218
+ .arrow {
219
+ font-size: 10px;
220
+ transform: rotate(90deg);
221
+ transition: transform 0.25s;
222
+ }
223
+
224
+ .arrow.collapsed {
225
+ transform: rotate(0deg);
226
+ }
227
+
228
+ .nav-item {
229
+ display: block;
230
+ padding: 4px;
231
+ color: var(--vp-c-text-2);
232
+ text-decoration: none;
233
+ border-radius: 4px;
234
+ font-size: 14px;
235
+ line-height: 24px;
236
+ transition: all 0.25s;
237
+ }
238
+
239
+ .nav-item:hover {
240
+ color: var(--vp-c-brand-1);
241
+ background-color: var(--vp-c-default-soft);
242
+ }
243
+
244
+ .nav-item.active {
245
+ color: var(--vp-c-brand-1);
246
+ font-weight: 600;
247
+ }
248
+
249
+ /* 聚焦高亮动画 */
250
+ .nav-item.focus-highlight {
251
+ animation: focusPulse 1s ease-in-out;
252
+ }
253
+
254
+ @keyframes focusPulse {
255
+ 0%,
256
+ 100% {
257
+ background-color: transparent;
258
+ }
259
+ 50% {
260
+ background-color: var(--vp-c-brand-soft);
261
+ }
262
+ }
263
+ </style>
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <div class="sidebar-toggle-wrapper">
3
+ <!-- 笔记编号显示切换按钮 -->
4
+ <button
5
+ class="toggle-btn"
6
+ @click="$emit('toggle-note-id')"
7
+ :title="showNoteId ? '隐藏笔记编号' : '显示笔记编号'"
8
+ >
9
+ <img
10
+ :src="showNoteId ? icon__number_purple : icon__number_gray"
11
+ class="toggle-icon"
12
+ alt="切换笔记编号"
13
+ />
14
+ </button>
15
+
16
+ <!-- 展开/折叠全部按钮 -->
17
+ <button
18
+ class="toggle-btn"
19
+ @click="$emit('toggle-expand')"
20
+ :title="isExpanded ? '全部折叠' : '全部展开'"
21
+ >
22
+ <img :src="icon__fold" class="toggle-icon" alt="切换展开折叠" />
23
+ </button>
24
+
25
+ <!-- 聚焦到当前笔记按钮 -->
26
+ <button
27
+ class="toggle-btn"
28
+ @click="$emit('focus-current')"
29
+ title="聚焦到当前笔记(点击切换多个位置)"
30
+ >
31
+ <img :src="icon__focus" class="toggle-icon" alt="聚焦当前笔记" />
32
+ </button>
33
+ </div>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import { icon__fold, icon__number_purple, icon__number_gray, icon__focus } from '../../assets/icons'
38
+
39
+ defineProps<{
40
+ isExpanded: boolean
41
+ showNoteId: boolean
42
+ }>()
43
+
44
+ defineEmits<{
45
+ 'toggle-expand': []
46
+ 'toggle-note-id': []
47
+ 'focus-current': []
48
+ }>()
49
+ </script>
50
+
51
+ <style scoped>
52
+ .sidebar-toggle-wrapper {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ gap: 8px;
57
+ padding: 12px 0 16px 0;
58
+ border-bottom: 1px solid var(--vp-c-divider);
59
+ margin-bottom: 16px;
60
+ }
61
+
62
+ .toggle-btn {
63
+ display: inline-flex;
64
+ align-items: center;
65
+ justify-content: center;
66
+ padding: 8px;
67
+ cursor: pointer;
68
+ background: transparent;
69
+ border: none;
70
+ border-radius: 6px;
71
+ transition: all 0.2s ease;
72
+ }
73
+
74
+ .toggle-btn:hover {
75
+ background: var(--vp-c-bg-soft);
76
+ }
77
+
78
+ .toggle-btn:active .toggle-icon {
79
+ transform: scale(0.95);
80
+ }
81
+
82
+ .toggle-icon {
83
+ width: 20px;
84
+ height: 20px;
85
+ display: block;
86
+ transition: transform 0.2s ease;
87
+ }
88
+
89
+ .toggle-btn:hover .toggle-icon {
90
+ transform: scale(1.1);
91
+ }
92
+ </style>
@@ -0,0 +1,167 @@
1
+ <template></template>
2
+
3
+ <script setup>
4
+ import { useData, onContentUpdated } from 'vitepress'
5
+ import { ref, onMounted, onBeforeUnmount } from 'vue'
6
+
7
+ import 'swiper/css'
8
+
9
+ const vpData = useData()
10
+
11
+ const swiperInstances = ref([])
12
+
13
+ const initSwiper = () => {
14
+ // 先清理旧实例
15
+ destroySwiper()
16
+
17
+ // 只在客户端动态加载
18
+ import('swiper').then(({ default: Swiper }) => {
19
+ import('swiper/modules').then(({ Navigation, Pagination }) => {
20
+ const wrappers = document.querySelectorAll('.tn-swiper')
21
+ wrappers.forEach((wrap) => {
22
+ const container = wrap.querySelector('.swiper-container')
23
+ const tabsEl = wrap.querySelector('.tn-swiper-tabs')
24
+ if (!container || !tabsEl) return
25
+
26
+ const instance = new Swiper(container, {
27
+ // loop: false,
28
+ // TODO 可配置
29
+ // effect: 'fade',
30
+ speed: 0, // 禁用动画
31
+ on: {
32
+ slideChange: () => {
33
+ updateActiveTab(wrap, instance.activeIndex)
34
+ updateContainerHeight(container, instance.activeIndex)
35
+ },
36
+ // 在初始化时也设置一次高度
37
+ afterInit: () => {
38
+ setTimeout(() => {
39
+ updateContainerHeight(container, instance.activeIndex)
40
+ }, 0)
41
+ },
42
+ },
43
+ })
44
+
45
+ // 生成 tabs(文案来自 data-title;为空则为 'img')
46
+ const slides = wrap.querySelectorAll('.swiper-slide')
47
+ const isTnTabNavVisible = slides.length >= 2
48
+ tabsEl.innerHTML = ''
49
+
50
+ // 微调 tabs 容器样式
51
+ if (isTnTabNavVisible) {
52
+ tabsEl.style.padding = '0 0.8rem 0 3rem'
53
+ } else {
54
+ tabsEl.style.padding = '0 0.8rem'
55
+ }
56
+
57
+ // 左按钮 - 上一页
58
+ if (isTnTabNavVisible) {
59
+ const prevBtn = document.createElement('button')
60
+ prevBtn.type = 'button'
61
+ prevBtn.className = 'tn-tab-nav tn-tab-prev'
62
+ prevBtn.textContent = '<'
63
+ prevBtn.title = '上一页'
64
+ prevBtn.addEventListener('click', () => {
65
+ if (instance.activeIndex === 0) {
66
+ // 到头 -> 跳到最后一张
67
+ instance.slideTo(slides.length - 1)
68
+ } else {
69
+ instance.slidePrev()
70
+ }
71
+ setTimeout(() => {
72
+ updateContainerHeight(container, instance.activeIndex)
73
+ }, 0)
74
+ })
75
+ tabsEl.appendChild(prevBtn)
76
+
77
+ const line = document.createElement('span')
78
+ line.className = 'tn-tab-nav tab-tab-line'
79
+ line.textContent = '/'
80
+ tabsEl.appendChild(line)
81
+ }
82
+
83
+ slides.forEach((slide, i) => {
84
+ const label = slide.getAttribute('data-title') || 'img'
85
+ const btn = document.createElement('button')
86
+ btn.type = 'button'
87
+ btn.className = 'tn-tab' + (i === 0 ? ' active' : '')
88
+ btn.textContent = label
89
+ btn.addEventListener('click', () => {
90
+ instance.slideTo(i)
91
+ // 点击tab时更新高度
92
+ setTimeout(() => {
93
+ updateContainerHeight(container, i)
94
+ }, 0)
95
+ })
96
+ tabsEl.appendChild(btn)
97
+ })
98
+
99
+ // 右按钮 - 下一页
100
+ if (isTnTabNavVisible) {
101
+ const nextBtn = document.createElement('button')
102
+ nextBtn.type = 'button'
103
+ nextBtn.className = 'tn-tab-nav tn-tab-next'
104
+ nextBtn.textContent = '>'
105
+ nextBtn.title = '下一页'
106
+ nextBtn.addEventListener('click', () => {
107
+ if (instance.activeIndex === slides.length - 1) {
108
+ // 到头 -> 跳到第一张
109
+ instance.slideTo(0)
110
+ } else {
111
+ instance.slideNext()
112
+ }
113
+ setTimeout(() => {
114
+ updateContainerHeight(container, instance.activeIndex)
115
+ }, 0)
116
+ })
117
+ tabsEl.appendChild(nextBtn)
118
+ }
119
+
120
+ swiperInstances.value.push(instance)
121
+ })
122
+ })
123
+ })
124
+ }
125
+
126
+ function updateActiveTab(wrap, activeIndex) {
127
+ const btns = wrap.querySelectorAll('.tn-swiper-tabs .tn-tab')
128
+ btns.forEach((b, i) => b.classList.toggle('active', i === activeIndex))
129
+ }
130
+
131
+ // 新增函数:根据当前slide中的图片高度更新容器高度
132
+ function updateContainerHeight(container, activeIndex) {
133
+ const slides = container.querySelectorAll('.swiper-slide')
134
+ if (slides[activeIndex]) {
135
+ const img = slides[activeIndex].querySelector('img')
136
+ if (img && img.complete) {
137
+ // 如果图片已加载完成,直接设置高度
138
+ container.style.height = img.offsetHeight + 'px'
139
+ } else if (img) {
140
+ // 如果图片未加载完成,等待加载完成后设置高度
141
+ img.onload = () => {
142
+ container.style.height = img.offsetHeight + 'px'
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ function destroySwiper() {
149
+ swiperInstances.value.forEach((inst) => {
150
+ try {
151
+ inst.destroy(true, true)
152
+ } catch {}
153
+ })
154
+ swiperInstances.value = []
155
+ }
156
+
157
+ onBeforeUnmount(destroySwiper)
158
+
159
+ onMounted(() => {
160
+ initSwiper()
161
+ })
162
+
163
+ // 监听内容更新(包括 HMR 和路由变化)
164
+ onContentUpdated(() => {
165
+ initSwiper()
166
+ })
167
+ </script>
@@ -0,0 +1,11 @@
1
+ .contentToggleBtn {
2
+ width: 1.5rem;
3
+ height: 1.5rem;
4
+ padding: 3px;
5
+ cursor: pointer;
6
+ transition: all 0.2s;
7
+
8
+ &:hover {
9
+ background: var(--vp-c-bg-alt);
10
+ }
11
+ }
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <img @click="toggle" :class="$style.contentToggleBtn" :aria-pressed="full.toString()" :title="full ? '退出全屏内容区' : '全屏显示内容区'" :src="full ? icon__fullscreen_exit : icon__fullscreen" alt=""></img>
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { onMounted, ref } from 'vue'
7
+ import { icon__fullscreen, icon__fullscreen_exit } from '../../assets/icons'
8
+
9
+ const KEY = 'vp:content:fullscreen'
10
+ const full = ref(false)
11
+
12
+ onMounted(() => {
13
+ try {
14
+ full.value = localStorage.getItem(KEY) === '1'
15
+ } catch {}
16
+ apply(full.value)
17
+ })
18
+
19
+ function apply(val: boolean) {
20
+ const root = document.documentElement
21
+ if (val) root.classList.add('content-fullscreen')
22
+ else root.classList.remove('content-fullscreen')
23
+ }
24
+
25
+ function toggle() {
26
+ full.value = !full.value
27
+ apply(full.value)
28
+ try {
29
+ localStorage.setItem(KEY, full.value ? '1' : '0')
30
+ } catch {}
31
+ }
32
+ </script>
33
+
34
+ <style module src="./ToggleFullContent.module.scss"></style>
@@ -0,0 +1,11 @@
1
+ .sidebarToggleBtn {
2
+ width: 1.5rem;
3
+ height: 1.5rem;
4
+ padding: 3px;
5
+ cursor: pointer;
6
+ transition: all 0.2s;
7
+
8
+ &:hover {
9
+ background: var(--vp-c-bg-alt);
10
+ }
11
+ }
@@ -0,0 +1,35 @@
1
+ <!-- .vitepress\components\Layout\ToggleSidebar.vue -->
2
+ <template>
3
+ <img @click="toggle" :class="$style.sidebarToggleBtn" :aria-pressed="hidden.toString()" :title="hidden ? '显示侧边栏' : '隐藏侧边栏'" :src="hidden ? icon__next : icon__prev" alt=""></img>
4
+ </template>
5
+
6
+ <script setup lang="ts">
7
+ import { onMounted, ref } from 'vue'
8
+ import { icon__next, icon__prev } from '../../assets/icons'
9
+
10
+ const KEY = 'vp:sidebar:hidden'
11
+ const hidden = ref(false)
12
+
13
+ onMounted(() => {
14
+ try {
15
+ hidden.value = localStorage.getItem(KEY) === '1'
16
+ } catch {}
17
+ apply(hidden.value)
18
+ })
19
+
20
+ function apply(val: boolean) {
21
+ const root = document.documentElement
22
+ if (val) root.classList.add('hide-sidebar')
23
+ else root.classList.remove('hide-sidebar')
24
+ }
25
+
26
+ function toggle() {
27
+ hidden.value = !hidden.value
28
+ apply(hidden.value)
29
+ try {
30
+ localStorage.setItem(KEY, hidden.value ? '1' : '0')
31
+ } catch {}
32
+ }
33
+ </script>
34
+
35
+ <style module src="./ToggleSidebar.module.scss"></style>
@@ -0,0 +1,88 @@
1
+ import { ref, watch } from 'vue'
2
+ import { useRoute } from 'vitepress'
3
+
4
+ /**
5
+ * 全局折叠/展开功能
6
+ */
7
+ export function useCollapseControl() {
8
+ const route = useRoute()
9
+ const allCollapsed = ref(false)
10
+
11
+ function toggleAllCollapse() {
12
+ if (typeof document === 'undefined') return
13
+
14
+ allCollapsed.value = !allCollapsed.value
15
+
16
+ // 获取所有折叠区域
17
+ const tocHeaders = document.querySelectorAll('.toc-collapse-header')
18
+ const h2Elements = document.querySelectorAll('.vp-doc h2.collapsible-h2')
19
+
20
+ // 切换 TOC 区域
21
+ tocHeaders.forEach((header) => {
22
+ const content = header.nextElementSibling
23
+ if (!content) return
24
+
25
+ const isCollapsed = content.classList.contains('collapsed')
26
+
27
+ // 根据目标状态决定是否需要切换
28
+ if (allCollapsed.value && !isCollapsed) {
29
+ // 需要折叠 - 直接操作 DOM
30
+ content.classList.add('collapsed')
31
+ header.classList.add('collapsed')
32
+ // 保存状态到 localStorage
33
+ const noteKey = route.path.replace(/\//g, '_')
34
+ const storageKey = `tnotes_collapse_state_${noteKey}_toc`
35
+ localStorage.setItem(storageKey, '1')
36
+ } else if (!allCollapsed.value && isCollapsed) {
37
+ // 需要展开 - 直接操作 DOM
38
+ content.classList.remove('collapsed')
39
+ header.classList.remove('collapsed')
40
+ // 保存状态到 localStorage
41
+ const noteKey = route.path.replace(/\//g, '_')
42
+ const storageKey = `tnotes_collapse_state_${noteKey}_toc`
43
+ localStorage.setItem(storageKey, '0')
44
+ }
45
+ })
46
+
47
+ // 切换 H2 区域
48
+ h2Elements.forEach((h2) => {
49
+ const content = h2.nextElementSibling
50
+ if (!content || !content.classList.contains('h2-collapse-content')) return
51
+
52
+ const isCollapsed = content.classList.contains('collapsed')
53
+ const h2Id = h2.id || `h2_${Array.from(h2Elements).indexOf(h2)}`
54
+
55
+ // 根据目标状态决定是否需要切换
56
+ if (allCollapsed.value && !isCollapsed) {
57
+ // 需要折叠 - 直接操作 DOM
58
+ content.classList.add('collapsed')
59
+ h2.classList.add('collapsed')
60
+ // 保存状态到 localStorage
61
+ const noteKey = route.path.replace(/\//g, '_')
62
+ const storageKey = `tnotes_collapse_state_${noteKey}_h2_${h2Id}`
63
+ localStorage.setItem(storageKey, '1')
64
+ } else if (!allCollapsed.value && isCollapsed) {
65
+ // 需要展开 - 直接操作 DOM
66
+ content.classList.remove('collapsed')
67
+ h2.classList.remove('collapsed')
68
+ // 保存状态到 localStorage
69
+ const noteKey = route.path.replace(/\//g, '_')
70
+ const storageKey = `tnotes_collapse_state_${noteKey}_h2_${h2Id}`
71
+ localStorage.setItem(storageKey, '0')
72
+ }
73
+ })
74
+ }
75
+
76
+ // 监听路由变化,重置折叠状态
77
+ watch(
78
+ () => route.path,
79
+ () => {
80
+ allCollapsed.value = false
81
+ }
82
+ )
83
+
84
+ return {
85
+ allCollapsed,
86
+ toggleAllCollapse,
87
+ }
88
+ }