@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,543 @@
1
+ <script setup>
2
+ import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
3
+ import { marked } from 'marked'
4
+ import {
5
+ EN_WORDS_REPO_BASE_URL,
6
+ EN_WORDS_REPO_BASE_RAW_URL,
7
+ EN_WORD_LIST_COMP_IS_AUTO_SHOW_CARD,
8
+ } from '../constants.ts'
9
+ import RightClickMenu from './RightClickMenu.vue'
10
+
11
+ const props = defineProps({
12
+ words: {
13
+ type: Array,
14
+ default: () => [],
15
+ },
16
+ needSort: {
17
+ type: Boolean,
18
+ default: false,
19
+ },
20
+ })
21
+
22
+ const isMobile = computed(() => {
23
+ if (typeof navigator === 'undefined') return false
24
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
25
+ navigator.userAgent
26
+ )
27
+ })
28
+
29
+ // checkbox ---------------------------------------------------
30
+
31
+ const pathname = typeof window !== 'undefined' ? window.location.pathname : ''
32
+ const sortedWords = props.needSort
33
+ ? computed(() =>
34
+ [...new Set(props.words)].sort(
35
+ (a, b) =>
36
+ a.toLowerCase()[0].charCodeAt() - b.toLowerCase()[0].charCodeAt()
37
+ )
38
+ )
39
+ : computed(() => [...new Set(props.words)])
40
+ const checkedStates = ref({})
41
+
42
+ const updateCheckedState = (word, isChecked) => {
43
+ const key = `${pathname}-${word}`
44
+ checkedStates.value[word] = isChecked
45
+ localStorage.setItem(key, isChecked)
46
+ }
47
+
48
+ const checkAll = () => {
49
+ Object.keys(checkedStates.value).forEach((word) => {
50
+ updateCheckedState(word, true)
51
+ })
52
+ hideContextMenu()
53
+ }
54
+
55
+ const reset = () => {
56
+ sortedWords.value.forEach((word) => {
57
+ const key = `${pathname}-${word}`
58
+ localStorage.removeItem(key)
59
+ checkedStates.value[word] = false
60
+ })
61
+ hideContextMenu()
62
+ }
63
+
64
+ // word card ---------------------------------------------------
65
+
66
+ const topZIndex = ref(10000)
67
+
68
+ const isAutoShowCard = ref(false)
69
+
70
+ // 卡片状态
71
+ const showCard = ref(false)
72
+ const cardX = ref(0)
73
+ const cardY = ref(0)
74
+ const cardContent = ref('')
75
+ const wordCache = ref({})
76
+
77
+ // 加载失败的词汇(也就是词库中不存在的词汇)
78
+ const failedWords = ref({})
79
+
80
+ // pinnedCards: { id, word, x, y, isDragging }
81
+ const pinnedCards = ref([])
82
+ let draggingCard = null
83
+ let offsetX = 0
84
+ let offsetY = 0
85
+
86
+ const CARD_DEFAULT_WIDTH = 400
87
+ const CARD_DEFAULT_HEIGHT = 500
88
+
89
+ let resizingCard = null
90
+ let startX = 0
91
+ let startY = 0
92
+ let startWidth = 0
93
+ let startHeight = 0
94
+
95
+ // 右键菜单状态
96
+ const contextMenuVisible = ref(false)
97
+ const contextMenuX = ref(0)
98
+ const contextMenuY = ref(0)
99
+ let currentWordForContextMenu = null
100
+
101
+ // 防抖计时器
102
+ let hoverTimer = null
103
+
104
+ /**
105
+ * 显示单词卡片
106
+ */
107
+ const showWordCard = async (e, word) => {
108
+ cardContent.value = '<em>加载中……</em>'
109
+ return new Promise((resolve) => {
110
+ clearTimeout(hoverTimer)
111
+ hoverTimer = setTimeout(async () => {
112
+ const { clientX, clientY } = e
113
+ cardX.value = clientX + 10
114
+ cardY.value = clientY + 10
115
+ showCard.value = true
116
+
117
+ if (wordCache.value[word]) {
118
+ cardContent.value = wordCache.value[word]
119
+ resolve()
120
+ return
121
+ }
122
+
123
+ const url = `${EN_WORDS_REPO_BASE_RAW_URL}${encodeURIComponent(
124
+ word.toLowerCase().replaceAll(/\s/g, '_')
125
+ )}.md`
126
+ try {
127
+ const res = await fetch(url)
128
+ if (res.ok) {
129
+ let text = await res.text()
130
+ text = marked.parse(text)
131
+ wordCache.value[word] = text
132
+ cardContent.value = text
133
+ } else {
134
+ cardContent.value = `<em>无法加载单词内容</em>`
135
+ }
136
+ } catch (err) {
137
+ console.error(err)
138
+ cardContent.value = `<em>加载失败</em>`
139
+ }
140
+ resolve()
141
+ }, 300)
142
+ })
143
+ }
144
+
145
+ // const convertMarkdownToHTML = (text) => {
146
+ // const lines = text.trim().split('\n')
147
+ // let stack = [{ level: -1, html: [] }]
148
+
149
+ // for (let line of lines) {
150
+ // const match = line.match(/^(\s*)-\s(.*)/)
151
+ // if (!match) continue
152
+
153
+ // const indent = match[1].length
154
+ // const content = match[2]
155
+ // const currentLevel = stack[stack.length - 1]
156
+
157
+ // const imageMatch = content.match(/^!\$$(.+?)$$/)
158
+ // const processedContent = imageMatch
159
+ // ? `<img src="${imageMatch[1]}" alt="" />`
160
+ // : content
161
+
162
+ // // 1. 深了:如果缩进比上一级更深,开启新子列表
163
+ // if (indent > currentLevel.level) {
164
+ // stack.push({ level: indent, html: [] })
165
+ // }
166
+ // // 2. 浅了:如果缩进更浅,关闭之前的列表直到匹配层级
167
+ // else if (indent < currentLevel.level) {
168
+ // while (stack.length > 1 && stack[stack.length - 2].level >= indent) {
169
+ // const closed = stack.pop()
170
+ // const innerHTML = closed.html.join('')
171
+ // stack[stack.length - 1].html.push(`<ul>${innerHTML}</ul>`)
172
+ // }
173
+ // }
174
+ // // 3. 一致:stack[-1] 是与当前 indent 层级一致的节点,添加当前 li 内容。
175
+ // stack[stack.length - 1].html.push(`<li>${processedContent}</li>`)
176
+ // }
177
+
178
+ // // 清理栈中剩余的 ul
179
+ // while (stack.length > 1) {
180
+ // const closed = stack.pop()
181
+ // const innerHTML = closed.html.join('')
182
+ // stack[stack.length - 1].html.push(`<ul>${innerHTML}</ul>`)
183
+ // }
184
+ // console.log(stack)
185
+
186
+ // return stack[0].html.join('')
187
+ // }
188
+
189
+ const preloadWords = async () => {
190
+ const wordsToPreload = sortedWords.value
191
+ if (!wordsToPreload.length) return
192
+
193
+ for (let i = 0; i < wordsToPreload.length; i++) {
194
+ const word = wordsToPreload[i]
195
+
196
+ // 如果已经缓存过,跳过
197
+ if (wordCache.value[word]) continue
198
+
199
+ const url = `${EN_WORDS_REPO_BASE_RAW_URL}${encodeURIComponent(
200
+ word.toLowerCase().replaceAll(/\s/g, '_')
201
+ )}.md`
202
+ try {
203
+ const res = await fetch(url)
204
+ if (res.ok) {
205
+ let text = await res.text()
206
+ text = marked.parse(text)
207
+ wordCache.value[word] = text
208
+ console.log(`✅ 预加载完成: ${word}`)
209
+ } else {
210
+ wordCache.value[word] = `<em>无法加载单词内容</em>`
211
+ failedWords.value[word] = true
212
+ }
213
+ } catch (err) {
214
+ console.error(`❌ 加载失败: ${word}`, err)
215
+ wordCache.value[word] = `<em>加载失败</em>`
216
+ failedWords.value[word] = true
217
+ }
218
+
219
+ // 可选:加个延迟避免并发请求过多
220
+ await new Promise((resolve) => setTimeout(resolve, 200))
221
+ }
222
+ }
223
+
224
+ const pinCard = (word) => {
225
+ // const key = `${pathname}-${word}`
226
+ // const storedState = localStorage.getItem(key)
227
+ // if (!storedState || storedState !== 'true') {
228
+ // updateCheckedState(word, true)
229
+ // }
230
+
231
+ // 如果已存在该卡片则不再重复添加
232
+ if (pinnedCards.value.some((card) => card.word === word)) return
233
+
234
+ pinnedCards.value.push({
235
+ id: Date.now(),
236
+ word,
237
+ x: cardX.value,
238
+ y: cardY.value,
239
+ content: cardContent.value,
240
+ width: CARD_DEFAULT_WIDTH,
241
+ height: CARD_DEFAULT_HEIGHT,
242
+ zIndex: topZIndex.value++,
243
+ })
244
+ }
245
+
246
+ const bringToFront = (card) => {
247
+ const index = pinnedCards.value.indexOf(card)
248
+ if (index > -1) {
249
+ pinnedCards.value = [
250
+ ...pinnedCards.value.slice(0, index),
251
+ ...pinnedCards.value.slice(index + 1),
252
+ { ...card, zIndex: topZIndex.value++ },
253
+ ]
254
+ }
255
+ }
256
+
257
+ const removeCard = (id) => {
258
+ pinnedCards.value = pinnedCards.value.filter((card) => card.id !== id)
259
+ }
260
+
261
+ const startDrag = (card, e) => {
262
+ draggingCard = card
263
+ offsetX = e.clientX - card.x
264
+ offsetY = e.clientY - card.y
265
+ document.addEventListener('mousemove', onDragging)
266
+ document.addEventListener('mouseup', stopDrag)
267
+ }
268
+
269
+ const onDragging = (e) => {
270
+ if (!draggingCard) return
271
+ draggingCard.x = e.clientX - offsetX
272
+ draggingCard.y = e.clientY - offsetY
273
+ }
274
+
275
+ const stopDrag = () => {
276
+ draggingCard = null
277
+ document.removeEventListener('mousemove', onDragging)
278
+ document.removeEventListener('mouseup', stopDrag)
279
+ }
280
+
281
+ const showContextMenu = (e, word) => {
282
+ e.preventDefault()
283
+ currentWordForContextMenu = word
284
+ contextMenuX.value = e.clientX
285
+ contextMenuY.value = e.clientY
286
+ contextMenuVisible.value = true
287
+ }
288
+
289
+ const hideContextMenu = () => {
290
+ contextMenuVisible.value = false
291
+ }
292
+
293
+ const handleContextMenuPin = () => {
294
+ if (currentWordForContextMenu) {
295
+ const word = currentWordForContextMenu
296
+ // 提前加载内容
297
+ showWordCard(
298
+ { clientX: contextMenuX.value, clientY: contextMenuY.value },
299
+ word
300
+ ).then(() => {
301
+ pinCard(word)
302
+ showCard.value = false
303
+ })
304
+ hideContextMenu()
305
+ }
306
+ }
307
+
308
+ const startResize = (card, e) => {
309
+ resizingCard = card
310
+ startX = e.clientX
311
+ startY = e.clientY
312
+ startWidth = card.width
313
+ startHeight = card.height
314
+
315
+ document.addEventListener('mousemove', onResizing)
316
+ document.addEventListener('mouseup', stopResize)
317
+ }
318
+
319
+ const onResizing = (e) => {
320
+ if (!resizingCard) return
321
+
322
+ const newWidth = startWidth + (e.clientX - startX)
323
+ const newHeight = startHeight + (e.clientY - startY)
324
+
325
+ // 设置最小尺寸
326
+ if (newWidth > 200) resizingCard.width = newWidth
327
+ if (newHeight > 100) resizingCard.height = newHeight
328
+ }
329
+
330
+ const stopResize = () => {
331
+ resizingCard = null
332
+ document.removeEventListener('mousemove', onResizing)
333
+ document.removeEventListener('mouseup', stopResize)
334
+ }
335
+
336
+ /**
337
+ * 处理鼠标离开事件
338
+ */
339
+ const handleMouseLeave = () => {
340
+ setTimeout(() => {
341
+ hideWordCard()
342
+ }, 100)
343
+ }
344
+
345
+ /**
346
+ * 隐藏单词卡片
347
+ */
348
+ const hideWordCard = () => {
349
+ showCard.value = false
350
+ }
351
+
352
+ // pronounce ----------------------------------------------------------
353
+
354
+ let currentPronounceAllIndex = ref(0)
355
+ let isPronouncingAll = ref(false)
356
+ let pronounceAllInterval = null
357
+
358
+ const handlePronounceAll = (lang) => {
359
+ if (isPronouncingAll.value) {
360
+ // 如果正在播放,就停止
361
+ stopPronounceAll()
362
+ return
363
+ }
364
+
365
+ const wordsToSpeak = sortedWords.value
366
+ if (!wordsToSpeak.length) return
367
+
368
+ currentPronounceAllIndex.value = 0
369
+ isPronouncingAll.value = true
370
+
371
+ const speakNext = async () => {
372
+ if (
373
+ !isPronouncingAll.value ||
374
+ currentPronounceAllIndex.value >= wordsToSpeak.length
375
+ ) {
376
+ stopPronounceAll()
377
+ return
378
+ }
379
+
380
+ const word = wordsToSpeak[currentPronounceAllIndex.value]
381
+ const utterance = new SpeechSynthesisUtterance(word)
382
+ utterance.lang = lang
383
+ speechSynthesis.speak(utterance)
384
+
385
+ await nextTick()
386
+ currentPronounceAllIndex.value++
387
+ }
388
+
389
+ speakNext()
390
+
391
+ // 每隔 1.5 秒读一个词
392
+ pronounceAllInterval = setInterval(speakNext, 1500)
393
+
394
+ hideContextMenu()
395
+ }
396
+
397
+ const stopPronounceAll = () => {
398
+ isPronouncingAll.value = false
399
+ if (pronounceAllInterval) {
400
+ clearInterval(pronounceAllInterval)
401
+ pronounceAllInterval = null
402
+ }
403
+ speechSynthesis.cancel() // 停止所有未完成的语音
404
+ }
405
+
406
+ const handlePronounce = (word, lang = 'en-GB') => {
407
+ if ('speechSynthesis' in window) {
408
+ stopPronounceAll()
409
+
410
+ const utterance = new SpeechSynthesisUtterance(word)
411
+ utterance.lang = lang
412
+ speechSynthesis.speak(utterance)
413
+ hideContextMenu()
414
+ } else {
415
+ alert('您的浏览器不支持语音功能,请尝试使用 Chrome 或 Edge 浏览器。')
416
+ }
417
+ }
418
+
419
+ // hooks ----------------------------------------------------------
420
+
421
+ onMounted(() => {
422
+ sortedWords.value.forEach((word) => {
423
+ const key = `${pathname}-${word}`
424
+ const storedState = localStorage.getItem(key)
425
+ checkedStates.value[word] = storedState === 'true'
426
+ })
427
+
428
+ if (!isMobile.value) preloadWords()
429
+
430
+ // 添加点击事件监听以隐藏右键菜单
431
+ if (typeof document !== 'undefined') {
432
+ document.body.addEventListener('click', hideContextMenu)
433
+ }
434
+ })
435
+
436
+ /**
437
+ * 销毁时清理定时器
438
+ */
439
+ onUnmounted(() => {
440
+ clearTimeout(hoverTimer)
441
+ if (typeof document !== 'undefined') {
442
+ document.removeEventListener('mousemove', onDragging)
443
+ document.removeEventListener('mouseup', stopDrag)
444
+ document.body.removeEventListener('click', hideContextMenu)
445
+ }
446
+ })
447
+ </script>
448
+
449
+ <template>
450
+ <div :class="$style.enWordList">
451
+ <ol>
452
+ <li
453
+ v-for="(word, index) in sortedWords"
454
+ :key="word"
455
+ :class="{
456
+ [$style.pronounced]:
457
+ isPronouncingAll && currentPronounceAllIndex === index + 1,
458
+ }"
459
+ >
460
+ <span :class="$style.index">{{ index + 1 }}.</span>
461
+ <input
462
+ type="checkbox"
463
+ :id="word"
464
+ :checked="checkedStates[word]"
465
+ @change="(e) => updateCheckedState(word, e.target.checked)"
466
+ />
467
+ <label :for="word">
468
+ <a
469
+ :href="`${EN_WORDS_REPO_BASE_URL}${encodeURIComponent(
470
+ word.toLowerCase().replaceAll(/\s/g, '_')
471
+ )}.md`"
472
+ :class="{
473
+ [$style.lineThrough]: checkedStates[word],
474
+ [$style.textRed]: failedWords[word],
475
+ }"
476
+ @mouseenter="(e) => isAutoShowCard && showWordCard(e, word)"
477
+ @mouseleave="handleMouseLeave"
478
+ @contextmenu="(e) => showContextMenu(e, word)"
479
+ @click.ctrl.exact="(e) => handlePronounce(word)"
480
+ >
481
+ {{ word }}
482
+ </a>
483
+ </label>
484
+ </li>
485
+ </ol>
486
+
487
+ <div
488
+ :class="$style.wordCard"
489
+ :style="{ left: cardX + 'px', top: cardY + 'px' }"
490
+ v-if="showCard"
491
+ >
492
+ <div :class="$style.wordCardContent" v-html="cardContent"></div>
493
+ </div>
494
+
495
+ <!-- pinned cards -->
496
+ <div
497
+ v-for="card in pinnedCards"
498
+ :key="card.id"
499
+ :class="$style.wordCard"
500
+ :style="{
501
+ left: card.x + 'px',
502
+ top: card.y + 'px',
503
+ width: card.width + 'px',
504
+ height: card.height + 'px',
505
+ zIndex: card.zIndex,
506
+ }"
507
+ @mousedown="(e) => startDrag(card, e)"
508
+ @click="bringToFront(card)"
509
+ >
510
+ <div :class="$style.wordCardContentWrapper">
511
+ <div :class="$style.wordCardContent" v-html="card.content"></div>
512
+ </div>
513
+ <button :class="$style.closeBtn" @click.stop="removeCard(card.id)">
514
+
515
+ </button>
516
+ <div
517
+ :class="$style.resizeHandle"
518
+ @mousedown.stop="startResize(card, $event)"
519
+ ></div>
520
+ </div>
521
+ </div>
522
+
523
+ <RightClickMenu
524
+ v-if="!isMobile"
525
+ :show="contextMenuVisible"
526
+ :x="contextMenuX"
527
+ :y="contextMenuY"
528
+ :isAutoShowCard="isAutoShowCard"
529
+ @pin="handleContextMenuPin"
530
+ @pronounce="(lang) => handlePronounce(currentWordForContextMenu, lang)"
531
+ @pronounceAll="(lang) => handlePronounceAll(lang)"
532
+ @autoShowCard="
533
+ () => {
534
+ isAutoShowCard = !isAutoShowCard
535
+ hideContextMenu()
536
+ }
537
+ "
538
+ @checkAll="checkAll"
539
+ @reset="reset"
540
+ />
541
+ </template>
542
+
543
+ <style module src="./EnWordList.module.scss"></style>
@@ -0,0 +1,22 @@
1
+ // RightClickMenu 组件样式
2
+
3
+ .rightClickMenu {
4
+ position: fixed;
5
+ z-index: 99999;
6
+ background: #2c2c2c;
7
+ border: 1px solid #444;
8
+ border-radius: 6px;
9
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
10
+ font-size: 13px;
11
+ color: #eee;
12
+ cursor: pointer;
13
+ user-select: none;
14
+ }
15
+
16
+ .menuItem {
17
+ padding: 8px 16px;
18
+
19
+ &:hover {
20
+ background-color: #444;
21
+ }
22
+ }
@@ -0,0 +1,66 @@
1
+ <template>
2
+ <div
3
+ v-if="show"
4
+ :class="$style.rightClickMenu"
5
+ :style="{ left: x + 'px', top: y + 'px' }"
6
+ >
7
+ <div :class="$style.menuItem" @click="handlePin">📌 Pin</div>
8
+ <div :class="$style.menuItem" @click="(e) => handlePronounce(e, 'en-GB')">
9
+ 📢 Pronounce(英)
10
+ </div>
11
+ <div :class="$style.menuItem" @click="(e) => handlePronounce(e, 'en-US')">
12
+ 📢 Pronounce(美)
13
+ </div>
14
+ <div
15
+ :class="$style.menuItem"
16
+ @click="(e) => handlePronounceAll(e, 'en-GB')"
17
+ >
18
+ 📢 Pronounce All(英)
19
+ </div>
20
+ <div
21
+ :class="$style.menuItem"
22
+ @click="(e) => handlePronounceAll(e, 'en-US')"
23
+ >
24
+ 📢 Pronounce All(美)
25
+ </div>
26
+ <div :class="$style.menuItem" @click="handleAutoShowCard">
27
+ 🔍 Auto Show Card({{ isAutoShowCard ? '关' : '开' }})
28
+ </div>
29
+ <div :class="$style.menuItem" @click="handleCheckAll">✅ Check All</div>
30
+ <div :class="$style.menuItem" @click="handleReset">❌ Reset</div>
31
+ </div>
32
+ </template>
33
+
34
+ <script setup>
35
+ const props = defineProps({
36
+ show: Boolean,
37
+ isAutoShowCard: Boolean,
38
+ x: Number,
39
+ y: Number,
40
+ })
41
+
42
+ const emit = defineEmits(['pin', 'pronounce'])
43
+ const handlePin = (e) => {
44
+ emit('pin')
45
+ e.preventDefault()
46
+ }
47
+ const handleAutoShowCard = () => {
48
+ emit('autoShowCard')
49
+ }
50
+ const handlePronounce = (e, lang) => {
51
+ emit('pronounce', lang)
52
+ e.preventDefault()
53
+ }
54
+ const handlePronounceAll = (e, lang) => {
55
+ emit('pronounceAll', lang)
56
+ e.preventDefault()
57
+ }
58
+ const handleCheckAll = () => {
59
+ emit('checkAll')
60
+ }
61
+ const handleReset = () => {
62
+ emit('reset')
63
+ }
64
+ </script>
65
+
66
+ <style module src="./RightClickMenu.module.scss"></style>