@tnotesjs/core 0.1.22 → 0.1.24

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 (101) hide show
  1. package/README.md +87 -87
  2. package/package.json +1 -1
  3. package/types/config.ts +60 -60
  4. package/types/index.ts +11 -11
  5. package/types/note.ts +33 -33
  6. package/vitepress/assets/icons/icon__check.svg +3 -3
  7. package/vitepress/assets/icons/icon__clipboard.svg +7 -7
  8. package/vitepress/assets/icons/icon__close.svg +1 -1
  9. package/vitepress/assets/icons/icon__collapse.svg +1 -1
  10. package/vitepress/assets/icons/icon__confirm.svg +1 -1
  11. package/vitepress/assets/icons/icon__copy.svg +4 -4
  12. package/vitepress/assets/icons/icon__focus.svg +1 -1
  13. package/vitepress/assets/icons/icon__fold.svg +3 -3
  14. package/vitepress/assets/icons/icon__folder.svg +1 -1
  15. package/vitepress/assets/icons/icon__fullscreen.svg +1 -1
  16. package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -1
  17. package/vitepress/assets/icons/icon__github.svg +3 -3
  18. package/vitepress/assets/icons/icon__mindmap.svg +1 -1
  19. package/vitepress/assets/icons/icon__next.svg +1 -1
  20. package/vitepress/assets/icons/icon__number_gray.svg +1 -1
  21. package/vitepress/assets/icons/icon__number_purple.svg +1 -1
  22. package/vitepress/assets/icons/icon__prev.svg +1 -1
  23. package/vitepress/assets/icons/icon__restore.svg +1 -1
  24. package/vitepress/assets/icons/icon__rotate.svg +4 -4
  25. package/vitepress/assets/icons/icon__search.svg +1 -1
  26. package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -1
  27. package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -1
  28. package/vitepress/assets/icons/icon__totop.svg +5 -5
  29. package/vitepress/assets/icons/icon__vscode.svg +5 -5
  30. package/vitepress/assets/icons/icon__zoom_fit.svg +1 -1
  31. package/vitepress/assets/icons/icon__zoom_in.svg +1 -1
  32. package/vitepress/assets/icons/icon__zoom_out.svg +1 -1
  33. package/vitepress/assets/icons/icon__zoom_reset.svg +1 -1
  34. package/vitepress/assets/icons/index.ts +39 -39
  35. package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -20
  36. package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +369 -369
  37. package/vitepress/components/CodeBlockFullscreen/index.ts +209 -209
  38. package/vitepress/components/CodeBlockFullscreen/styles.css +126 -126
  39. package/vitepress/components/Discussions/Discussions.vue +242 -242
  40. package/vitepress/components/EnWordList/EnWordList.vue +664 -664
  41. package/vitepress/components/EnWordList/RightClickMenu.vue +87 -87
  42. package/vitepress/components/Footprints/Footprints.vue +472 -472
  43. package/vitepress/components/Layout/AboutModal.vue +329 -329
  44. package/vitepress/components/Layout/AboutPanel.vue +485 -485
  45. package/vitepress/components/Layout/ContentCollapse.vue +591 -591
  46. package/vitepress/components/Layout/CustomSidebar.vue +817 -817
  47. package/vitepress/components/Layout/DocBeforeControls.vue +145 -145
  48. package/vitepress/components/Layout/DocFooter.vue +233 -233
  49. package/vitepress/components/Layout/ImagePreview.vue +485 -485
  50. package/vitepress/components/Layout/Layout.vue +744 -744
  51. package/vitepress/components/Layout/NoteStatus.vue +140 -140
  52. package/vitepress/components/Layout/SidebarItems.vue +1083 -1083
  53. package/vitepress/components/Layout/SidebarNavBefore.vue +170 -170
  54. package/vitepress/components/Layout/SidebarResizeHandle.vue +368 -311
  55. package/vitepress/components/Layout/Swiper.vue +167 -167
  56. package/vitepress/components/Layout/ToggleFullContent.vue +45 -45
  57. package/vitepress/components/Layout/ToggleSidebar.vue +35 -35
  58. package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -88
  59. package/vitepress/components/Layout/composables/useNoteConfig.ts +119 -119
  60. package/vitepress/components/Layout/composables/useNoteSave.ts +178 -178
  61. package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -85
  62. package/vitepress/components/Layout/composables/useRedirect.ts +113 -113
  63. package/vitepress/components/Layout/composables/useSidebarLayout.ts +122 -122
  64. package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -85
  65. package/vitepress/components/Layout/homeReadme.data.ts +124 -124
  66. package/vitepress/components/MarkMap/MarkMap.vue +562 -562
  67. package/vitepress/components/Mermaid/Mermaid.vue +644 -644
  68. package/vitepress/components/NotesTable/NotesTable.vue +175 -175
  69. package/vitepress/components/NotesTable/README.md +67 -67
  70. package/vitepress/components/Settings/Settings.vue +711 -711
  71. package/vitepress/components/SidebarCard/MindMapView.vue +484 -484
  72. package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -108
  73. package/vitepress/components/SidebarCard/SidebarCard.vue +945 -945
  74. package/vitepress/components/Tooltip/Tooltip.vue +70 -70
  75. package/vitepress/components/constants.ts +109 -109
  76. package/vitepress/components/notesConfig.data.ts +73 -73
  77. package/vitepress/components/sidebar.data.ts +59 -59
  78. package/vitepress/components/tnotes-config.data.ts +21 -21
  79. package/vitepress/components/utils.ts +26 -26
  80. package/vitepress/config/index.ts +169 -169
  81. package/vitepress/configs/constants.ts +26 -26
  82. package/vitepress/configs/head.config.ts +25 -25
  83. package/vitepress/configs/index.ts +9 -9
  84. package/vitepress/configs/markdown-it.d.ts +23 -23
  85. package/vitepress/configs/markdown.config.ts +368 -368
  86. package/vitepress/configs/theme.config.ts +108 -108
  87. package/vitepress/plugins/buildProgressPlugin.ts +434 -434
  88. package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -107
  89. package/vitepress/plugins/renameNotePlugin.ts +68 -68
  90. package/vitepress/plugins/sidebarStructurePlugin.ts +260 -260
  91. package/vitepress/plugins/updateConfigPlugin.ts +63 -63
  92. package/vitepress/theme/index.ts +137 -137
  93. package/vitepress/theme/styles/base.scss +66 -64
  94. package/vitepress/theme/styles/components/404.scss +31 -31
  95. package/vitepress/theme/styles/components/collapse.scss +172 -172
  96. package/vitepress/theme/styles/components/markmap.scss +101 -101
  97. package/vitepress/theme/styles/components/swiper.scss +255 -255
  98. package/vitepress/theme/styles/index.scss +25 -25
  99. package/vitepress/theme/styles/layout.scss +79 -78
  100. package/vitepress/theme/styles/utilities.scss +39 -39
  101. package/vitepress/theme/styles/vitepress-override.scss +25 -25
@@ -1,472 +1,472 @@
1
- <script setup>
2
- import { ref, onMounted, onBeforeUnmount, computed, nextTick } from 'vue'
3
-
4
- // ----------------------------------------------------------
5
- // #region - 图片处理
6
- // ----------------------------------------------------------
7
- /**
8
- * 获取图片容器的 DOM 引用
9
- */
10
- const imageContainer = ref(null)
11
-
12
- /**
13
- * 动态存储图片路径的列表
14
- */
15
- const images = ref([])
16
-
17
- /**
18
- * 控制 modal 显示
19
- */
20
- const isModalVisible = ref(false)
21
-
22
- /**
23
- * 当前预览的图片索引
24
- */
25
- const currentIndex = ref(0)
26
- const currentImage = computed(() => images.value[currentIndex.value])
27
-
28
- /**
29
- * 唯一标识符,用于区分不同的组件实例。
30
- * 解决被多次复用的时候,直接操作 DOM 的相关逻辑出现 bug。
31
- */
32
- const instanceId = Math.random().toString(36).substr(2, 9)
33
-
34
- /**
35
- * 打开模态框
36
- * @param index 需要展示的图片的索引
37
- */
38
- const openModal = (index) => {
39
- currentIndex.value = index
40
- isModalVisible.value = true
41
- }
42
- /**
43
- * 关闭模态框
44
- */
45
- const closeModal = () => (isModalVisible.value = false)
46
- /**
47
- * 键盘事件处理
48
- * @param event 事件对象
49
- */
50
- const handleKeyDown = (event) => {
51
- if (!isModalVisible.value) return
52
- switch (event.key) {
53
- case 'ArrowLeft':
54
- if (currentIndex.value > 0) currentIndex.value -= 1
55
- break
56
- case 'ArrowRight':
57
- if (currentIndex.value < images.value.length - 1) currentIndex.value += 1
58
- break
59
- case 'Escape':
60
- closeModal()
61
- break
62
- }
63
- }
64
- /**
65
- * 手指按下的初始 X 坐标
66
- */
67
- let touchStartX = 0
68
- /**
69
- * 手指抬起的最终 X 坐标
70
- */
71
- let touchEndX = 0
72
-
73
- /**
74
- * 处理触摸开始事件
75
- * @param event 事件对象
76
- */
77
- const handleTouchStart = (event) => {
78
- event.preventDefault() // 阻止默认行为
79
- touchStartX = event.touches[0].clientX // 记录手指按下的初始位置
80
- // console.log(`Touch start at: ${touchStartX}`);
81
- }
82
-
83
- /**
84
- * 处理触摸移动事件
85
- * @param event 事件对象
86
- */
87
- const handleTouchMove = (event) => {
88
- event.preventDefault() // 阻止默认行为
89
- const currentX = event.touches[0].clientX
90
- const moveDistance = currentX - touchStartX
91
-
92
- // 可选:添加视觉反馈(例如轻微移动图片的位置)
93
- // console.log(`Move distance: ${moveDistance}`);
94
- }
95
-
96
- /**
97
- * 处理触摸结束事件
98
- * - 向右滑动:切换到上一张图片
99
- * - 向左滑动:切换到下一张图片
100
- */
101
- const handleTouchEnd = () => {
102
- const swipeThreshold = 50 // 滑动阈值,单位为像素
103
- const swipeDistance = touchEndX - touchStartX // 计算滑动距离
104
- console.log(`Touch end at: ${touchEndX}, Distance: ${swipeDistance}`)
105
-
106
- if (swipeDistance > swipeThreshold && currentIndex.value > 0) {
107
- currentIndex.value--
108
- } else if (
109
- swipeDistance < -swipeThreshold &&
110
- currentIndex.value < images.value.length - 1
111
- ) {
112
- currentIndex.value++
113
- }
114
- }
115
- /**
116
- * 鼠标按下的初始 X 坐标
117
- */
118
- let mouseStartX = 0
119
- /**
120
- * 处理鼠标按下事件
121
- * @param event 事件对象
122
- */
123
- const handleMouseDown = (event) => {
124
- event.preventDefault()
125
- mouseStartX = event.clientX // 记录鼠标按下的初始位置
126
- window.addEventListener('mousemove', handleMouseMove)
127
- window.addEventListener('mouseup', handleMouseUp)
128
- }
129
- /**
130
- * 处理鼠标移动事件
131
- * @param event 事件对象
132
- */
133
- const handleMouseMove = (event) => {
134
- event.preventDefault()
135
- const currentX = event.clientX
136
- const moveDistance = currentX - mouseStartX
137
- // 可以在这里添加一些视觉反馈,比如轻微移动图片的位置
138
- // console.log(`Move distance: ${moveDistance}`);
139
- }
140
- /**
141
- * 处理鼠标松开事件
142
- */
143
- const handleMouseUp = () => {
144
- const swipeThreshold = 50 // 滑动阈值,单位为像素
145
- const modalContent = document.querySelector('.__dynamic__modal-content')
146
- if (!modalContent) return
147
-
148
- // 使用鼠标松开时的位置
149
- const currentX = event.clientX // 直接从事件对象中获取
150
- const moveDistance = currentX - mouseStartX
151
-
152
- if (moveDistance > swipeThreshold && currentIndex.value > 0) {
153
- currentIndex.value-- // 向右滑动,切换到上一张图片
154
- } else if (
155
- moveDistance < -swipeThreshold &&
156
- currentIndex.value < images.value.length - 1
157
- ) {
158
- currentIndex.value++ // 向左滑动,切换到下一张图片
159
- }
160
-
161
- // 清理变量和事件监听器
162
- mouseStartX = 0
163
- window.removeEventListener('mousemove', handleMouseMove)
164
- window.removeEventListener('mouseup', handleMouseUp)
165
- }
166
-
167
- onMounted(() => {
168
- const imgElements = imageContainer.value.querySelectorAll('img')
169
- images.value = Array.from(imgElements).map((img) => img.src)
170
-
171
- // 添加键盘事件监听器
172
- window.addEventListener('keydown', handleKeyDown)
173
-
174
- // 添加触摸事件监听器
175
- const modalContent = document.querySelector(
176
- `.__dynamic__modal-content-${instanceId}`
177
- )
178
- if (modalContent) {
179
- modalContent.addEventListener('touchstart', handleTouchStart)
180
- modalContent.addEventListener('touchmove', handleTouchMove) // 绑定 touchmove 事件
181
- modalContent.addEventListener('touchend', (event) => {
182
- touchEndX = event.changedTouches[0].clientX // 记录手指抬起的最终位置
183
- handleTouchEnd()
184
- })
185
-
186
- // 添加鼠标事件监听器
187
- modalContent.addEventListener('mousedown', handleMouseDown)
188
- }
189
- })
190
-
191
- onBeforeUnmount(() => {
192
- window.removeEventListener('keydown', handleKeyDown)
193
-
194
- // 移除触摸事件监听器
195
- const modalContent = document.querySelector(
196
- `.__dynamic__modal-content-${instanceId}`
197
- )
198
- if (modalContent) {
199
- modalContent.removeEventListener('touchstart', handleTouchStart)
200
- modalContent.removeEventListener('touchmove', handleTouchMove) // 移除 touchmove 事件
201
- modalContent.removeEventListener('touchend', handleTouchEnd)
202
- modalContent.removeEventListener('mousedown', handleMouseDown)
203
- }
204
- })
205
- // ----------------------------------------------------------
206
- // #endregion - 图片处理
207
- // ----------------------------------------------------------
208
-
209
- // ----------------------------------------------------------
210
- // #region - 文案处理
211
- // ----------------------------------------------------------
212
-
213
- /**
214
- * 文本容器的 DOM 引用
215
- */
216
- const textContainer = ref(null)
217
-
218
- /**
219
- * 控制文本是否折叠
220
- */
221
- const isCollapsed = ref(true)
222
-
223
- /**
224
- * 是否需要显示“全文”按钮
225
- */
226
- const isExpandable = ref(false)
227
-
228
- /**
229
- * 计算一级子元素数量并判断是否需要折叠
230
- * 1. 获取所有一级子元素
231
- * 2. 如果一级子元素数量大于 2,则可以展开
232
- */
233
- const checkChildElementCount = () => {
234
- if (textContainer.value) {
235
- const childElements = textContainer.value.children
236
- isExpandable.value = childElements.length > 2
237
- }
238
- }
239
-
240
- /**
241
- * 切换折叠/展开状态
242
- */
243
- const toggleCollapse = () => {
244
- isCollapsed.value = !isCollapsed.value
245
- }
246
-
247
- /**
248
- * 在 DOM 渲染后再做计算
249
- */
250
- onMounted(() => {
251
- nextTick(checkChildElementCount)
252
- })
253
- // ----------------------------------------------------------
254
- // #endregion - 文案处理
255
- // ----------------------------------------------------------
256
-
257
- // ----------------------------------------------------------
258
- // #region - 时间信息处理
259
- // ----------------------------------------------------------
260
- const props = defineProps({
261
- times: {
262
- type: Array,
263
- default: () => [],
264
- validator: (value) => {
265
- // 验证数组长度和内容类型
266
- return (
267
- value.length >= 2 &&
268
- value.length <= 6 &&
269
- value.every((item, index) => {
270
- if (index === 0) return typeof item === 'number' && item >= 0 // 年
271
- if (index === 1)
272
- return typeof item === 'number' && item >= 1 && item <= 12 // 月
273
- if (index === 2)
274
- return typeof item === 'number' && item >= 1 && item <= 31 // 日
275
- if (index === 3)
276
- return typeof item === 'number' && item >= 0 && item <= 23 // 小时
277
- if (index === 4)
278
- return typeof item === 'number' && item >= 0 && item <= 59 // 分钟
279
- if (index === 5)
280
- return typeof item === 'number' && item >= 0 && item <= 59 // 秒
281
- return false
282
- })
283
- )
284
- },
285
- },
286
- })
287
-
288
- const formattedTime = computed(() => {
289
- if (props.times.length < 2) return ''
290
-
291
- const [year, month, day, hour, minute, second] = props.times
292
-
293
- // 格式化年月日
294
- const datePart = `${String(year).padStart(4, '0')}-${String(month).padStart(
295
- 2,
296
- '0'
297
- )}`
298
- const dayPart = day !== undefined ? `-${String(day).padStart(2, '0')}` : ''
299
-
300
- // 格式化时分秒
301
- let timePart = ''
302
- if (hour !== undefined && minute !== undefined && second !== undefined) {
303
- timePart = ` ${String(hour).padStart(2, '0')}:${String(minute).padStart(
304
- 2,
305
- '0'
306
- )}:${String(second).padStart(2, '0')}`
307
- } else if (hour !== undefined && minute !== undefined) {
308
- timePart = ` ${String(hour).padStart(2, '0')}:${String(minute).padStart(
309
- 2,
310
- '0'
311
- )}`
312
- }
313
-
314
- // 👣 天数
315
- let daysSinceBirthday = ''
316
- if (day !== undefined) {
317
- // 确保时间部分为 00:00:00,避免时区和时间部分的影响
318
- const birthday = new Date(Date.UTC(1999, 5, 29)) // 注意:月份从 0 开始计数
319
- const currentDate = new Date(Date.UTC(year, month - 1, day)) // 同样使用 UTC 时间
320
- const diffInMilliseconds = currentDate - birthday
321
- const diffInDays =
322
- Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24)) + 1
323
- daysSinceBirthday = `👣 ${diffInDays} | `
324
- }
325
-
326
- return daysSinceBirthday + datePart + dayPart + timePart
327
- })
328
- // ----------------------------------------------------------
329
- // #endregion - 时间信息处理
330
- // ----------------------------------------------------------
331
- </script>
332
-
333
- <template>
334
- <div class="textContainer">
335
- <div
336
- class="textContent"
337
- :class="{ collapsed: isCollapsed }"
338
- ref="textContainer"
339
- >
340
- <slot name="text-area"></slot>
341
- </div>
342
- <button
343
- v-if="isExpandable"
344
- class="toggleButton"
345
- @click="toggleCollapse"
346
- >
347
- {{ isCollapsed ? '全文' : '收起' }}
348
- </button>
349
- </div>
350
-
351
- <div class="imageContainer" ref="imageContainer">
352
- <slot
353
- name="image-list"
354
- :openModal="openModal"
355
- :closeModal="closeModal"
356
- :currentImage="currentImage"
357
- :isModalVisible="isModalVisible"
358
- ></slot>
359
- </div>
360
- <div class="modal" v-show="isModalVisible" @click.self="closeModal">
361
- <span class="close" @click="closeModal">&times;</span>
362
- <img
363
- :class="['modalContent', `__dynamic__modal-content-${instanceId}`]"
364
- :src="currentImage"
365
- alt="Preview"
366
- @mousedown="handleMouseDown"
367
- />
368
- </div>
369
-
370
- <div class="timeContainer">
371
- <p>{{ formattedTime }}</p>
372
- </div>
373
- <div class="otherInfoContainer">
374
- <slot name="other-info"></slot>
375
- </div>
376
- </template>
377
-
378
- <style scoped lang="scss">
379
- // Footprints 组件样式
380
-
381
- // 其他信息容器和时间容器
382
- .otherInfoContainer,
383
- .timeContainer {
384
- font-size: 0.8rem;
385
- color: gray;
386
- }
387
-
388
- // 文本容器
389
- .textContainer {
390
- position: relative;
391
- margin-bottom: 1rem;
392
- }
393
-
394
- // 文本内容
395
- .textContent {
396
- overflow: hidden;
397
- transition: max-height 0.3s ease;
398
-
399
- // 折叠状态:隐藏从第三个开始的所有一级子元素
400
- &.collapsed > :nth-child(n + 3) {
401
- display: none;
402
- }
403
- }
404
-
405
- // 切换按钮
406
- .toggleButton {
407
- display: block;
408
- margin-top: 0.5rem;
409
- background-color: transparent;
410
- color: #007bff;
411
- border: none;
412
- cursor: pointer;
413
- text-align: left;
414
- font-size: 1rem;
415
-
416
- &:hover {
417
- text-decoration: underline;
418
- }
419
- }
420
-
421
- // 图片容器
422
- .imageContainer {
423
- display: flex;
424
- width: 100%;
425
- flex-wrap: wrap;
426
-
427
- img {
428
- width: calc(33.33% - 10px);
429
- aspect-ratio: 1; // 设置宽高比为 1:1
430
- object-fit: cover;
431
- cursor: pointer;
432
- margin: 0.3rem;
433
- }
434
- }
435
-
436
- // 模态框
437
- .modal {
438
- display: flex;
439
- position: fixed;
440
- z-index: 1000;
441
- left: 0;
442
- top: 0;
443
- width: 100%;
444
- height: 100%;
445
- overflow: auto;
446
- background-color: rgba(0, 0, 0, 1);
447
- align-items: center;
448
- justify-content: center;
449
- }
450
-
451
- // 模态框内容(图片)
452
- .modalContent {
453
- position: absolute;
454
- top: 50%;
455
- left: 50%;
456
- transform: translate(-50%, -50%);
457
- max-width: 90%;
458
- max-height: 90%;
459
- cursor: grab;
460
- }
461
-
462
- // 关闭按钮
463
- .close {
464
- position: absolute;
465
- top: 20px;
466
- right: 30px;
467
- color: white;
468
- font-size: 30px;
469
- font-weight: bold;
470
- cursor: pointer;
471
- }
472
- </style>
1
+ <script setup>
2
+ import { ref, onMounted, onBeforeUnmount, computed, nextTick } from 'vue'
3
+
4
+ // ----------------------------------------------------------
5
+ // #region - 图片处理
6
+ // ----------------------------------------------------------
7
+ /**
8
+ * 获取图片容器的 DOM 引用
9
+ */
10
+ const imageContainer = ref(null)
11
+
12
+ /**
13
+ * 动态存储图片路径的列表
14
+ */
15
+ const images = ref([])
16
+
17
+ /**
18
+ * 控制 modal 显示
19
+ */
20
+ const isModalVisible = ref(false)
21
+
22
+ /**
23
+ * 当前预览的图片索引
24
+ */
25
+ const currentIndex = ref(0)
26
+ const currentImage = computed(() => images.value[currentIndex.value])
27
+
28
+ /**
29
+ * 唯一标识符,用于区分不同的组件实例。
30
+ * 解决被多次复用的时候,直接操作 DOM 的相关逻辑出现 bug。
31
+ */
32
+ const instanceId = Math.random().toString(36).substr(2, 9)
33
+
34
+ /**
35
+ * 打开模态框
36
+ * @param index 需要展示的图片的索引
37
+ */
38
+ const openModal = (index) => {
39
+ currentIndex.value = index
40
+ isModalVisible.value = true
41
+ }
42
+ /**
43
+ * 关闭模态框
44
+ */
45
+ const closeModal = () => (isModalVisible.value = false)
46
+ /**
47
+ * 键盘事件处理
48
+ * @param event 事件对象
49
+ */
50
+ const handleKeyDown = (event) => {
51
+ if (!isModalVisible.value) return
52
+ switch (event.key) {
53
+ case 'ArrowLeft':
54
+ if (currentIndex.value > 0) currentIndex.value -= 1
55
+ break
56
+ case 'ArrowRight':
57
+ if (currentIndex.value < images.value.length - 1) currentIndex.value += 1
58
+ break
59
+ case 'Escape':
60
+ closeModal()
61
+ break
62
+ }
63
+ }
64
+ /**
65
+ * 手指按下的初始 X 坐标
66
+ */
67
+ let touchStartX = 0
68
+ /**
69
+ * 手指抬起的最终 X 坐标
70
+ */
71
+ let touchEndX = 0
72
+
73
+ /**
74
+ * 处理触摸开始事件
75
+ * @param event 事件对象
76
+ */
77
+ const handleTouchStart = (event) => {
78
+ event.preventDefault() // 阻止默认行为
79
+ touchStartX = event.touches[0].clientX // 记录手指按下的初始位置
80
+ // console.log(`Touch start at: ${touchStartX}`);
81
+ }
82
+
83
+ /**
84
+ * 处理触摸移动事件
85
+ * @param event 事件对象
86
+ */
87
+ const handleTouchMove = (event) => {
88
+ event.preventDefault() // 阻止默认行为
89
+ const currentX = event.touches[0].clientX
90
+ const moveDistance = currentX - touchStartX
91
+
92
+ // 可选:添加视觉反馈(例如轻微移动图片的位置)
93
+ // console.log(`Move distance: ${moveDistance}`);
94
+ }
95
+
96
+ /**
97
+ * 处理触摸结束事件
98
+ * - 向右滑动:切换到上一张图片
99
+ * - 向左滑动:切换到下一张图片
100
+ */
101
+ const handleTouchEnd = () => {
102
+ const swipeThreshold = 50 // 滑动阈值,单位为像素
103
+ const swipeDistance = touchEndX - touchStartX // 计算滑动距离
104
+ console.log(`Touch end at: ${touchEndX}, Distance: ${swipeDistance}`)
105
+
106
+ if (swipeDistance > swipeThreshold && currentIndex.value > 0) {
107
+ currentIndex.value--
108
+ } else if (
109
+ swipeDistance < -swipeThreshold &&
110
+ currentIndex.value < images.value.length - 1
111
+ ) {
112
+ currentIndex.value++
113
+ }
114
+ }
115
+ /**
116
+ * 鼠标按下的初始 X 坐标
117
+ */
118
+ let mouseStartX = 0
119
+ /**
120
+ * 处理鼠标按下事件
121
+ * @param event 事件对象
122
+ */
123
+ const handleMouseDown = (event) => {
124
+ event.preventDefault()
125
+ mouseStartX = event.clientX // 记录鼠标按下的初始位置
126
+ window.addEventListener('mousemove', handleMouseMove)
127
+ window.addEventListener('mouseup', handleMouseUp)
128
+ }
129
+ /**
130
+ * 处理鼠标移动事件
131
+ * @param event 事件对象
132
+ */
133
+ const handleMouseMove = (event) => {
134
+ event.preventDefault()
135
+ const currentX = event.clientX
136
+ const moveDistance = currentX - mouseStartX
137
+ // 可以在这里添加一些视觉反馈,比如轻微移动图片的位置
138
+ // console.log(`Move distance: ${moveDistance}`);
139
+ }
140
+ /**
141
+ * 处理鼠标松开事件
142
+ */
143
+ const handleMouseUp = () => {
144
+ const swipeThreshold = 50 // 滑动阈值,单位为像素
145
+ const modalContent = document.querySelector('.__dynamic__modal-content')
146
+ if (!modalContent) return
147
+
148
+ // 使用鼠标松开时的位置
149
+ const currentX = event.clientX // 直接从事件对象中获取
150
+ const moveDistance = currentX - mouseStartX
151
+
152
+ if (moveDistance > swipeThreshold && currentIndex.value > 0) {
153
+ currentIndex.value-- // 向右滑动,切换到上一张图片
154
+ } else if (
155
+ moveDistance < -swipeThreshold &&
156
+ currentIndex.value < images.value.length - 1
157
+ ) {
158
+ currentIndex.value++ // 向左滑动,切换到下一张图片
159
+ }
160
+
161
+ // 清理变量和事件监听器
162
+ mouseStartX = 0
163
+ window.removeEventListener('mousemove', handleMouseMove)
164
+ window.removeEventListener('mouseup', handleMouseUp)
165
+ }
166
+
167
+ onMounted(() => {
168
+ const imgElements = imageContainer.value.querySelectorAll('img')
169
+ images.value = Array.from(imgElements).map((img) => img.src)
170
+
171
+ // 添加键盘事件监听器
172
+ window.addEventListener('keydown', handleKeyDown)
173
+
174
+ // 添加触摸事件监听器
175
+ const modalContent = document.querySelector(
176
+ `.__dynamic__modal-content-${instanceId}`
177
+ )
178
+ if (modalContent) {
179
+ modalContent.addEventListener('touchstart', handleTouchStart)
180
+ modalContent.addEventListener('touchmove', handleTouchMove) // 绑定 touchmove 事件
181
+ modalContent.addEventListener('touchend', (event) => {
182
+ touchEndX = event.changedTouches[0].clientX // 记录手指抬起的最终位置
183
+ handleTouchEnd()
184
+ })
185
+
186
+ // 添加鼠标事件监听器
187
+ modalContent.addEventListener('mousedown', handleMouseDown)
188
+ }
189
+ })
190
+
191
+ onBeforeUnmount(() => {
192
+ window.removeEventListener('keydown', handleKeyDown)
193
+
194
+ // 移除触摸事件监听器
195
+ const modalContent = document.querySelector(
196
+ `.__dynamic__modal-content-${instanceId}`
197
+ )
198
+ if (modalContent) {
199
+ modalContent.removeEventListener('touchstart', handleTouchStart)
200
+ modalContent.removeEventListener('touchmove', handleTouchMove) // 移除 touchmove 事件
201
+ modalContent.removeEventListener('touchend', handleTouchEnd)
202
+ modalContent.removeEventListener('mousedown', handleMouseDown)
203
+ }
204
+ })
205
+ // ----------------------------------------------------------
206
+ // #endregion - 图片处理
207
+ // ----------------------------------------------------------
208
+
209
+ // ----------------------------------------------------------
210
+ // #region - 文案处理
211
+ // ----------------------------------------------------------
212
+
213
+ /**
214
+ * 文本容器的 DOM 引用
215
+ */
216
+ const textContainer = ref(null)
217
+
218
+ /**
219
+ * 控制文本是否折叠
220
+ */
221
+ const isCollapsed = ref(true)
222
+
223
+ /**
224
+ * 是否需要显示“全文”按钮
225
+ */
226
+ const isExpandable = ref(false)
227
+
228
+ /**
229
+ * 计算一级子元素数量并判断是否需要折叠
230
+ * 1. 获取所有一级子元素
231
+ * 2. 如果一级子元素数量大于 2,则可以展开
232
+ */
233
+ const checkChildElementCount = () => {
234
+ if (textContainer.value) {
235
+ const childElements = textContainer.value.children
236
+ isExpandable.value = childElements.length > 2
237
+ }
238
+ }
239
+
240
+ /**
241
+ * 切换折叠/展开状态
242
+ */
243
+ const toggleCollapse = () => {
244
+ isCollapsed.value = !isCollapsed.value
245
+ }
246
+
247
+ /**
248
+ * 在 DOM 渲染后再做计算
249
+ */
250
+ onMounted(() => {
251
+ nextTick(checkChildElementCount)
252
+ })
253
+ // ----------------------------------------------------------
254
+ // #endregion - 文案处理
255
+ // ----------------------------------------------------------
256
+
257
+ // ----------------------------------------------------------
258
+ // #region - 时间信息处理
259
+ // ----------------------------------------------------------
260
+ const props = defineProps({
261
+ times: {
262
+ type: Array,
263
+ default: () => [],
264
+ validator: (value) => {
265
+ // 验证数组长度和内容类型
266
+ return (
267
+ value.length >= 2 &&
268
+ value.length <= 6 &&
269
+ value.every((item, index) => {
270
+ if (index === 0) return typeof item === 'number' && item >= 0 // 年
271
+ if (index === 1)
272
+ return typeof item === 'number' && item >= 1 && item <= 12 // 月
273
+ if (index === 2)
274
+ return typeof item === 'number' && item >= 1 && item <= 31 // 日
275
+ if (index === 3)
276
+ return typeof item === 'number' && item >= 0 && item <= 23 // 小时
277
+ if (index === 4)
278
+ return typeof item === 'number' && item >= 0 && item <= 59 // 分钟
279
+ if (index === 5)
280
+ return typeof item === 'number' && item >= 0 && item <= 59 // 秒
281
+ return false
282
+ })
283
+ )
284
+ },
285
+ },
286
+ })
287
+
288
+ const formattedTime = computed(() => {
289
+ if (props.times.length < 2) return ''
290
+
291
+ const [year, month, day, hour, minute, second] = props.times
292
+
293
+ // 格式化年月日
294
+ const datePart = `${String(year).padStart(4, '0')}-${String(month).padStart(
295
+ 2,
296
+ '0'
297
+ )}`
298
+ const dayPart = day !== undefined ? `-${String(day).padStart(2, '0')}` : ''
299
+
300
+ // 格式化时分秒
301
+ let timePart = ''
302
+ if (hour !== undefined && minute !== undefined && second !== undefined) {
303
+ timePart = ` ${String(hour).padStart(2, '0')}:${String(minute).padStart(
304
+ 2,
305
+ '0'
306
+ )}:${String(second).padStart(2, '0')}`
307
+ } else if (hour !== undefined && minute !== undefined) {
308
+ timePart = ` ${String(hour).padStart(2, '0')}:${String(minute).padStart(
309
+ 2,
310
+ '0'
311
+ )}`
312
+ }
313
+
314
+ // 👣 天数
315
+ let daysSinceBirthday = ''
316
+ if (day !== undefined) {
317
+ // 确保时间部分为 00:00:00,避免时区和时间部分的影响
318
+ const birthday = new Date(Date.UTC(1999, 5, 29)) // 注意:月份从 0 开始计数
319
+ const currentDate = new Date(Date.UTC(year, month - 1, day)) // 同样使用 UTC 时间
320
+ const diffInMilliseconds = currentDate - birthday
321
+ const diffInDays =
322
+ Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24)) + 1
323
+ daysSinceBirthday = `👣 ${diffInDays} | `
324
+ }
325
+
326
+ return daysSinceBirthday + datePart + dayPart + timePart
327
+ })
328
+ // ----------------------------------------------------------
329
+ // #endregion - 时间信息处理
330
+ // ----------------------------------------------------------
331
+ </script>
332
+
333
+ <template>
334
+ <div class="textContainer">
335
+ <div
336
+ class="textContent"
337
+ :class="{ collapsed: isCollapsed }"
338
+ ref="textContainer"
339
+ >
340
+ <slot name="text-area"></slot>
341
+ </div>
342
+ <button
343
+ v-if="isExpandable"
344
+ class="toggleButton"
345
+ @click="toggleCollapse"
346
+ >
347
+ {{ isCollapsed ? '全文' : '收起' }}
348
+ </button>
349
+ </div>
350
+
351
+ <div class="imageContainer" ref="imageContainer">
352
+ <slot
353
+ name="image-list"
354
+ :openModal="openModal"
355
+ :closeModal="closeModal"
356
+ :currentImage="currentImage"
357
+ :isModalVisible="isModalVisible"
358
+ ></slot>
359
+ </div>
360
+ <div class="modal" v-show="isModalVisible" @click.self="closeModal">
361
+ <span class="close" @click="closeModal">&times;</span>
362
+ <img
363
+ :class="['modalContent', `__dynamic__modal-content-${instanceId}`]"
364
+ :src="currentImage"
365
+ alt="Preview"
366
+ @mousedown="handleMouseDown"
367
+ />
368
+ </div>
369
+
370
+ <div class="timeContainer">
371
+ <p>{{ formattedTime }}</p>
372
+ </div>
373
+ <div class="otherInfoContainer">
374
+ <slot name="other-info"></slot>
375
+ </div>
376
+ </template>
377
+
378
+ <style scoped lang="scss">
379
+ // Footprints 组件样式
380
+
381
+ // 其他信息容器和时间容器
382
+ .otherInfoContainer,
383
+ .timeContainer {
384
+ font-size: 0.8rem;
385
+ color: gray;
386
+ }
387
+
388
+ // 文本容器
389
+ .textContainer {
390
+ position: relative;
391
+ margin-bottom: 1rem;
392
+ }
393
+
394
+ // 文本内容
395
+ .textContent {
396
+ overflow: hidden;
397
+ transition: max-height 0.3s ease;
398
+
399
+ // 折叠状态:隐藏从第三个开始的所有一级子元素
400
+ &.collapsed > :nth-child(n + 3) {
401
+ display: none;
402
+ }
403
+ }
404
+
405
+ // 切换按钮
406
+ .toggleButton {
407
+ display: block;
408
+ margin-top: 0.5rem;
409
+ background-color: transparent;
410
+ color: #007bff;
411
+ border: none;
412
+ cursor: pointer;
413
+ text-align: left;
414
+ font-size: 1rem;
415
+
416
+ &:hover {
417
+ text-decoration: underline;
418
+ }
419
+ }
420
+
421
+ // 图片容器
422
+ .imageContainer {
423
+ display: flex;
424
+ width: 100%;
425
+ flex-wrap: wrap;
426
+
427
+ img {
428
+ width: calc(33.33% - 10px);
429
+ aspect-ratio: 1; // 设置宽高比为 1:1
430
+ object-fit: cover;
431
+ cursor: pointer;
432
+ margin: 0.3rem;
433
+ }
434
+ }
435
+
436
+ // 模态框
437
+ .modal {
438
+ display: flex;
439
+ position: fixed;
440
+ z-index: 1000;
441
+ left: 0;
442
+ top: 0;
443
+ width: 100%;
444
+ height: 100%;
445
+ overflow: auto;
446
+ background-color: rgba(0, 0, 0, 1);
447
+ align-items: center;
448
+ justify-content: center;
449
+ }
450
+
451
+ // 模态框内容(图片)
452
+ .modalContent {
453
+ position: absolute;
454
+ top: 50%;
455
+ left: 50%;
456
+ transform: translate(-50%, -50%);
457
+ max-width: 90%;
458
+ max-height: 90%;
459
+ cursor: grab;
460
+ }
461
+
462
+ // 关闭按钮
463
+ .close {
464
+ position: absolute;
465
+ top: 20px;
466
+ right: 30px;
467
+ color: white;
468
+ font-size: 30px;
469
+ font-weight: bold;
470
+ cursor: pointer;
471
+ }
472
+ </style>