@tnotesjs/ui 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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/package.json +68 -0
  4. package/src/components/BilibiliVideo/BilibiliVideo.vue +55 -0
  5. package/src/components/Footprints/Footprints.vue +339 -0
  6. package/src/components/Footprints/parse.ts +122 -0
  7. package/src/components/Mermaid/Mermaid.vue +553 -0
  8. package/src/components/Mermaid/icons/icon__center_off.svg +1 -0
  9. package/src/components/Mermaid/icons/icon__center_on.svg +1 -0
  10. package/src/components/Mermaid/icons/icon__check.svg +3 -0
  11. package/src/components/Mermaid/icons/icon__clipboard.svg +8 -0
  12. package/src/components/Mermaid/icons/icon__fullscreen.svg +1 -0
  13. package/src/components/Mermaid/icons/icon__fullscreen_exit.svg +1 -0
  14. package/src/components/Mindmap/FocusBreadcrumbs.test.ts +265 -0
  15. package/src/components/Mindmap/FocusBreadcrumbs.vue +436 -0
  16. package/src/components/Mindmap/InlineRuns.ts +25 -0
  17. package/src/components/Mindmap/Mindmap.vue +1210 -0
  18. package/src/components/Mindmap/MindmapOutlineNode.vue +62 -0
  19. package/src/components/Mindmap/MindmapViewIcon.vue +41 -0
  20. package/src/components/Mindmap/editor/AppIcon.vue +107 -0
  21. package/src/components/Mindmap/editor/CanvasContextMenu.vue +157 -0
  22. package/src/components/Mindmap/editor/LinkPopover.vue +83 -0
  23. package/src/components/Mindmap/editor/MarkdownView.vue +163 -0
  24. package/src/components/Mindmap/editor/MindmapView.vue +253 -0
  25. package/src/components/Mindmap/editor/OutlineView.vue +2494 -0
  26. package/src/components/Mindmap/editor/RichInlineEditor.vue +393 -0
  27. package/src/components/Mindmap/editor/SelectionToolbar.vue +191 -0
  28. package/src/components/Mindmap/editor/canvasClipboard.ts +6 -0
  29. package/src/components/Mindmap/editor/imagePaste.ts +32 -0
  30. package/src/components/Mindmap/editor/mindmapClipboard.ts +93 -0
  31. package/src/components/Mindmap/editor/outlineDrag.ts +29 -0
  32. package/src/components/Mindmap/editor/platform.ts +18 -0
  33. package/src/components/Mindmap/expandLevel.ts +28 -0
  34. package/src/components/Mindmap/icons/icon__fullscreen.svg +1 -0
  35. package/src/components/Mindmap/icons/icon__fullscreen_exit.svg +1 -0
  36. package/src/components/Mindmap/icons/icon__zoom_fit.svg +1 -0
  37. package/src/components/Mindmap/markdown.ts +83 -0
  38. package/src/components/Mindmap/wheelInteraction.ts +7 -0
  39. package/src/components/NotesTable/NotesTable.vue +119 -0
  40. package/src/components/NotesTable/types.ts +6 -0
  41. package/src/components/WordList/RightClickMenu.vue +106 -0
  42. package/src/components/WordList/WordList.vue +692 -0
  43. package/src/components/WordList/wordListFeatures.ts +38 -0
  44. package/src/index.ts +30 -0
  45. package/src/styles/tokens.css +35 -0
@@ -0,0 +1,692 @@
1
+ <script setup>
2
+ import { marked } from 'marked'
3
+ import { computed, ref, onMounted, onUnmounted, nextTick } from 'vue'
4
+
5
+ import RightClickMenu from './RightClickMenu.vue'
6
+ import {
7
+ WORD_LIST_FEATURES_FULL,
8
+ resolveWordListFeatures
9
+ } from './wordListFeatures'
10
+
11
+ const DEFAULT_WORDS_BASE_URL = 'https://github.com/tnotesjs/en-words/blob/main/'
12
+ const DEFAULT_WORDS_RAW_BASE_URL =
13
+ 'https://raw.githubusercontent.com/tnotesjs/en-words/refs/heads/main/'
14
+
15
+ const props = defineProps({
16
+ words: {
17
+ type: Array,
18
+ default: () => [],
19
+ },
20
+ needSort: {
21
+ type: Boolean,
22
+ default: false,
23
+ },
24
+ wordsBaseUrl: {
25
+ type: String,
26
+ default: DEFAULT_WORDS_BASE_URL,
27
+ },
28
+ wordsRawBaseUrl: {
29
+ type: String,
30
+ default: DEFAULT_WORDS_RAW_BASE_URL,
31
+ },
32
+ /** Capability overrides; omit for full web/core behavior. */
33
+ features: {
34
+ type: Object,
35
+ default: () => ({ ...WORD_LIST_FEATURES_FULL }),
36
+ },
37
+ })
38
+
39
+ const features = computed(() => resolveWordListFeatures(props.features))
40
+
41
+ const isMobile = computed(() => {
42
+ if (typeof navigator === 'undefined') return false
43
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
44
+ navigator.userAgent
45
+ )
46
+ })
47
+
48
+ // checkbox ---------------------------------------------------
49
+
50
+ const pathname = typeof window !== 'undefined' ? window.location.pathname : ''
51
+ const sortedWords = computed(() => {
52
+ const unique = [...new Set(props.words ?? [])]
53
+ if (!props.needSort) return unique
54
+ return unique.sort(
55
+ (a, b) => a.toLowerCase().charCodeAt(0) - b.toLowerCase().charCodeAt(0)
56
+ )
57
+ })
58
+ const checkedStates = ref({})
59
+
60
+ const updateCheckedState = (word, isChecked) => {
61
+ const key = `${pathname}-${word}`
62
+ checkedStates.value[word] = isChecked
63
+ localStorage.setItem(key, isChecked)
64
+ }
65
+
66
+ const checkAll = () => {
67
+ Object.keys(checkedStates.value).forEach((word) => {
68
+ updateCheckedState(word, true)
69
+ })
70
+ hideContextMenu()
71
+ }
72
+
73
+ const reset = () => {
74
+ sortedWords.value.forEach((word) => {
75
+ const key = `${pathname}-${word}`
76
+ localStorage.removeItem(key)
77
+ checkedStates.value[word] = false
78
+ })
79
+ hideContextMenu()
80
+ }
81
+
82
+ // word card ---------------------------------------------------
83
+
84
+ const topZIndex = ref(10000)
85
+
86
+ const isAutoShowCard = ref(false)
87
+
88
+ // 卡片状态
89
+ const showCard = ref(false)
90
+ const cardX = ref(0)
91
+ const cardY = ref(0)
92
+ const cardContent = ref('')
93
+ const wordCache = ref({})
94
+
95
+ // 加载失败的词汇(也就是词库中不存在的词汇)
96
+ const failedWords = ref({})
97
+
98
+ // pinnedCards: { id, word, x, y, isDragging }
99
+ const pinnedCards = ref([])
100
+ let draggingCard = null
101
+ let offsetX = 0
102
+ let offsetY = 0
103
+
104
+ const CARD_DEFAULT_WIDTH = 400
105
+ const CARD_DEFAULT_HEIGHT = 500
106
+
107
+ let resizingCard = null
108
+ let startX = 0
109
+ let startY = 0
110
+ let startWidth = 0
111
+ let startHeight = 0
112
+
113
+ // 右键菜单状态
114
+ const contextMenuVisible = ref(false)
115
+ const contextMenuX = ref(0)
116
+ const contextMenuY = ref(0)
117
+ let currentWordForContextMenu = null
118
+
119
+ // 防抖计时器
120
+ let hoverTimer = null
121
+
122
+ /**
123
+ * 显示单词卡片
124
+ */
125
+ const showWordCard = async (e, word) => {
126
+ if (!features.value.enableCards || !features.value.enableWordData) {
127
+ return Promise.resolve()
128
+ }
129
+ cardContent.value = '<em>加载中……</em>'
130
+ return new Promise((resolve) => {
131
+ clearTimeout(hoverTimer)
132
+ hoverTimer = setTimeout(async () => {
133
+ const { clientX, clientY } = e
134
+ cardX.value = clientX + 10
135
+ cardY.value = clientY + 10
136
+ showCard.value = true
137
+
138
+ if (wordCache.value[word]) {
139
+ cardContent.value = wordCache.value[word]
140
+ resolve()
141
+ return
142
+ }
143
+
144
+ const url = `${props.wordsRawBaseUrl}${encodeURIComponent(
145
+ word.toLowerCase().replaceAll(/\s/g, '_')
146
+ )}.md`
147
+ try {
148
+ const res = await fetch(url)
149
+ if (res.ok) {
150
+ let text = await res.text()
151
+ text = marked.parse(text)
152
+ wordCache.value[word] = text
153
+ cardContent.value = text
154
+ } else {
155
+ cardContent.value = `<em>无法加载单词内容</em>`
156
+ }
157
+ } catch (err) {
158
+ console.error(err)
159
+ cardContent.value = `<em>加载失败</em>`
160
+ }
161
+ resolve()
162
+ }, 300)
163
+ })
164
+ }
165
+
166
+ // const convertMarkdownToHTML = (text) => {
167
+ // const lines = text.trim().split('\n')
168
+ // let stack = [{ level: -1, html: [] }]
169
+
170
+ // for (let line of lines) {
171
+ // const match = line.match(/^(\s*)-\s(.*)/)
172
+ // if (!match) continue
173
+
174
+ // const indent = match[1].length
175
+ // const content = match[2]
176
+ // const currentLevel = stack[stack.length - 1]
177
+
178
+ // const imageMatch = content.match(/^!\$$(.+?)$$/)
179
+ // const processedContent = imageMatch
180
+ // ? `<img src="${imageMatch[1]}" alt="" />`
181
+ // : content
182
+
183
+ // // 1. 深了:如果缩进比上一级更深,开启新子列表
184
+ // if (indent > currentLevel.level) {
185
+ // stack.push({ level: indent, html: [] })
186
+ // }
187
+ // // 2. 浅了:如果缩进更浅,关闭之前的列表直到匹配层级
188
+ // else if (indent < currentLevel.level) {
189
+ // while (stack.length > 1 && stack[stack.length - 2].level >= indent) {
190
+ // const closed = stack.pop()
191
+ // const innerHTML = closed.html.join('')
192
+ // stack[stack.length - 1].html.push(`<ul>${innerHTML}</ul>`)
193
+ // }
194
+ // }
195
+ // // 3. 一致:stack[-1] 是与当前 indent 层级一致的节点,添加当前 li 内容。
196
+ // stack[stack.length - 1].html.push(`<li>${processedContent}</li>`)
197
+ // }
198
+
199
+ // // 清理栈中剩余的 ul
200
+ // while (stack.length > 1) {
201
+ // const closed = stack.pop()
202
+ // const innerHTML = closed.html.join('')
203
+ // stack[stack.length - 1].html.push(`<ul>${innerHTML}</ul>`)
204
+ // }
205
+ // console.log(stack)
206
+
207
+ // return stack[0].html.join('')
208
+ // }
209
+
210
+ const preloadWords = async () => {
211
+ if (!features.value.enableWordData) return
212
+ const wordsToPreload = sortedWords.value
213
+ if (!wordsToPreload.length) return
214
+
215
+ for (let i = 0; i < wordsToPreload.length; i++) {
216
+ const word = wordsToPreload[i]
217
+
218
+ // 如果已经缓存过,跳过
219
+ if (wordCache.value[word]) continue
220
+
221
+ const url = `${props.wordsRawBaseUrl}${encodeURIComponent(
222
+ word.toLowerCase().replaceAll(/\s/g, '_')
223
+ )}.md`
224
+ try {
225
+ const res = await fetch(url)
226
+ if (res.ok) {
227
+ let text = await res.text()
228
+ text = marked.parse(text)
229
+ wordCache.value[word] = text
230
+ console.log(`✅ 预加载完成: ${word}`)
231
+ } else {
232
+ wordCache.value[word] = `<em>无法加载单词内容</em>`
233
+ failedWords.value[word] = true
234
+ }
235
+ } catch (err) {
236
+ console.error(`❌ 加载失败: ${word}`, err)
237
+ wordCache.value[word] = `<em>加载失败</em>`
238
+ failedWords.value[word] = true
239
+ }
240
+
241
+ // 可选:加个延迟避免并发请求过多
242
+ await new Promise((resolve) => setTimeout(resolve, 200))
243
+ }
244
+ }
245
+
246
+ const pinCard = (word) => {
247
+ if (!features.value.enableCards) return
248
+ // 如果已存在该卡片则不再重复添加
249
+ if (pinnedCards.value.some((card) => card.word === word)) return
250
+
251
+ pinnedCards.value.push({
252
+ id: Date.now(),
253
+ word,
254
+ x: cardX.value,
255
+ y: cardY.value,
256
+ content: cardContent.value,
257
+ width: CARD_DEFAULT_WIDTH,
258
+ height: CARD_DEFAULT_HEIGHT,
259
+ zIndex: topZIndex.value++,
260
+ })
261
+ }
262
+
263
+ const bringToFront = (card) => {
264
+ const index = pinnedCards.value.indexOf(card)
265
+ if (index > -1) {
266
+ pinnedCards.value = [
267
+ ...pinnedCards.value.slice(0, index),
268
+ ...pinnedCards.value.slice(index + 1),
269
+ { ...card, zIndex: topZIndex.value++ },
270
+ ]
271
+ }
272
+ }
273
+
274
+ const removeCard = (id) => {
275
+ pinnedCards.value = pinnedCards.value.filter((card) => card.id !== id)
276
+ }
277
+
278
+ const startDrag = (card, e) => {
279
+ draggingCard = card
280
+ offsetX = e.clientX - card.x
281
+ offsetY = e.clientY - card.y
282
+ document.addEventListener('mousemove', onDragging)
283
+ document.addEventListener('mouseup', stopDrag)
284
+ }
285
+
286
+ const onDragging = (e) => {
287
+ if (!draggingCard) return
288
+ draggingCard.x = e.clientX - offsetX
289
+ draggingCard.y = e.clientY - offsetY
290
+ }
291
+
292
+ const stopDrag = () => {
293
+ draggingCard = null
294
+ document.removeEventListener('mousemove', onDragging)
295
+ document.removeEventListener('mouseup', stopDrag)
296
+ }
297
+
298
+ const showContextMenu = (e, word) => {
299
+ e.preventDefault()
300
+ currentWordForContextMenu = word
301
+ contextMenuX.value = e.clientX
302
+ contextMenuY.value = e.clientY
303
+ contextMenuVisible.value = true
304
+ }
305
+
306
+ const hideContextMenu = () => {
307
+ contextMenuVisible.value = false
308
+ }
309
+
310
+ const handleContextMenuPin = () => {
311
+ if (!features.value.enableContextMenuPin || !features.value.enableCards) {
312
+ hideContextMenu()
313
+ return
314
+ }
315
+ if (currentWordForContextMenu) {
316
+ const word = currentWordForContextMenu
317
+ // 提前加载内容
318
+ showWordCard(
319
+ { clientX: contextMenuX.value, clientY: contextMenuY.value },
320
+ word
321
+ ).then(() => {
322
+ pinCard(word)
323
+ showCard.value = false
324
+ })
325
+ hideContextMenu()
326
+ }
327
+ }
328
+
329
+ const startResize = (card, e) => {
330
+ resizingCard = card
331
+ startX = e.clientX
332
+ startY = e.clientY
333
+ startWidth = card.width
334
+ startHeight = card.height
335
+
336
+ document.addEventListener('mousemove', onResizing)
337
+ document.addEventListener('mouseup', stopResize)
338
+ }
339
+
340
+ const onResizing = (e) => {
341
+ if (!resizingCard) return
342
+
343
+ const newWidth = startWidth + (e.clientX - startX)
344
+ const newHeight = startHeight + (e.clientY - startY)
345
+
346
+ // 设置最小尺寸
347
+ if (newWidth > 200) resizingCard.width = newWidth
348
+ if (newHeight > 100) resizingCard.height = newHeight
349
+ }
350
+
351
+ const stopResize = () => {
352
+ resizingCard = null
353
+ document.removeEventListener('mousemove', onResizing)
354
+ document.removeEventListener('mouseup', stopResize)
355
+ }
356
+
357
+ /**
358
+ * 处理鼠标离开事件
359
+ */
360
+ const handleMouseLeave = () => {
361
+ setTimeout(() => {
362
+ hideWordCard()
363
+ }, 100)
364
+ }
365
+
366
+ /**
367
+ * 隐藏单词卡片
368
+ */
369
+ const hideWordCard = () => {
370
+ showCard.value = false
371
+ }
372
+
373
+ // pronounce ----------------------------------------------------------
374
+
375
+ let currentPronounceAllIndex = ref(0)
376
+ let isPronouncingAll = ref(false)
377
+ let pronounceAllInterval = null
378
+
379
+ const handlePronounceAll = (lang) => {
380
+ if (isPronouncingAll.value) {
381
+ // 如果正在播放,就停止
382
+ stopPronounceAll()
383
+ return
384
+ }
385
+
386
+ const wordsToSpeak = sortedWords.value
387
+ if (!wordsToSpeak.length) return
388
+
389
+ currentPronounceAllIndex.value = 0
390
+ isPronouncingAll.value = true
391
+
392
+ const speakNext = async () => {
393
+ if (
394
+ !isPronouncingAll.value ||
395
+ currentPronounceAllIndex.value >= wordsToSpeak.length
396
+ ) {
397
+ stopPronounceAll()
398
+ return
399
+ }
400
+
401
+ const word = wordsToSpeak[currentPronounceAllIndex.value]
402
+ const utterance = new SpeechSynthesisUtterance(word)
403
+ utterance.lang = lang
404
+ speechSynthesis.speak(utterance)
405
+
406
+ await nextTick()
407
+ currentPronounceAllIndex.value++
408
+ }
409
+
410
+ speakNext()
411
+
412
+ // 每隔 1.5 秒读一个词
413
+ pronounceAllInterval = setInterval(speakNext, 1500)
414
+
415
+ hideContextMenu()
416
+ }
417
+
418
+ const stopPronounceAll = () => {
419
+ isPronouncingAll.value = false
420
+ if (pronounceAllInterval) {
421
+ clearInterval(pronounceAllInterval)
422
+ pronounceAllInterval = null
423
+ }
424
+ speechSynthesis.cancel() // 停止所有未完成的语音
425
+ }
426
+
427
+ const handlePronounce = (word, lang = 'en-GB') => {
428
+ if ('speechSynthesis' in window) {
429
+ stopPronounceAll()
430
+
431
+ const utterance = new SpeechSynthesisUtterance(word)
432
+ utterance.lang = lang
433
+ speechSynthesis.speak(utterance)
434
+ hideContextMenu()
435
+ } else {
436
+ alert('您的浏览器不支持语音功能,请尝试使用 Chrome 或 Edge 浏览器。')
437
+ }
438
+ }
439
+
440
+ // hooks ----------------------------------------------------------
441
+
442
+ onMounted(() => {
443
+ sortedWords.value.forEach((word) => {
444
+ const key = `${pathname}-${word}`
445
+ const storedState = localStorage.getItem(key)
446
+ checkedStates.value[word] = storedState === 'true'
447
+ })
448
+
449
+ if (!isMobile.value && features.value.enableWordData) preloadWords()
450
+
451
+ // 添加点击事件监听以隐藏右键菜单
452
+ if (typeof document !== 'undefined') {
453
+ document.body.addEventListener('click', hideContextMenu)
454
+ }
455
+ })
456
+
457
+ /**
458
+ * 销毁时清理定时器
459
+ */
460
+ onUnmounted(() => {
461
+ clearTimeout(hoverTimer)
462
+ if (typeof document !== 'undefined') {
463
+ document.removeEventListener('mousemove', onDragging)
464
+ document.removeEventListener('mouseup', stopDrag)
465
+ document.body.removeEventListener('click', hideContextMenu)
466
+ }
467
+ })
468
+ </script>
469
+
470
+ <template>
471
+ <div class="tn-word-list">
472
+ <ol>
473
+ <li
474
+ v-for="(word, index) in sortedWords"
475
+ :key="word"
476
+ :class="{
477
+ pronounced: isPronouncingAll && currentPronounceAllIndex === index + 1,
478
+ }"
479
+ >
480
+ <span class="index">{{ index + 1 }}.</span>
481
+ <input
482
+ type="checkbox"
483
+ :id="word"
484
+ :checked="checkedStates[word]"
485
+ @change="(e) => updateCheckedState(word, e.target.checked)"
486
+ />
487
+ <label :for="word">
488
+ <a
489
+ :href="`${props.wordsBaseUrl}${encodeURIComponent(
490
+ word.toLowerCase().replaceAll(/\s/g, '_')
491
+ )}.md`"
492
+ :class="{
493
+ lineThrough: checkedStates[word],
494
+ textRed: failedWords[word],
495
+ }"
496
+ @mouseenter="
497
+ (e) =>
498
+ features.enableCards &&
499
+ isAutoShowCard &&
500
+ showWordCard(e, word)
501
+ "
502
+ @mouseleave="handleMouseLeave"
503
+ @contextmenu="(e) => showContextMenu(e, word)"
504
+ @click.ctrl.exact="(e) => handlePronounce(word)"
505
+ >
506
+ {{ word }}
507
+ </a>
508
+ </label>
509
+ </li>
510
+ </ol>
511
+
512
+ <div
513
+ class="wordCard"
514
+ :style="{ left: cardX + 'px', top: cardY + 'px' }"
515
+ v-if="features.enableCards && showCard"
516
+ >
517
+ <div class="wordCardContent" v-html="cardContent"></div>
518
+ </div>
519
+
520
+ <!-- pinned cards -->
521
+ <template v-if="features.enableCards">
522
+ <div
523
+ v-for="card in pinnedCards"
524
+ :key="card.id"
525
+ class="wordCard"
526
+ :style="{
527
+ left: card.x + 'px',
528
+ top: card.y + 'px',
529
+ width: card.width + 'px',
530
+ height: card.height + 'px',
531
+ zIndex: card.zIndex,
532
+ }"
533
+ @mousedown="(e) => startDrag(card, e)"
534
+ @click="bringToFront(card)"
535
+ >
536
+ <div class="wordCardContentWrapper">
537
+ <div class="wordCardContent" v-html="card.content"></div>
538
+ </div>
539
+ <button class="closeBtn" @click.stop="removeCard(card.id)">
540
+
541
+ </button>
542
+ <div
543
+ class="resizeHandle"
544
+ @mousedown.stop="startResize(card, $event)"
545
+ ></div>
546
+ </div>
547
+ </template>
548
+ </div>
549
+
550
+ <RightClickMenu
551
+ v-if="!isMobile"
552
+ :show="contextMenuVisible"
553
+ :x="contextMenuX"
554
+ :y="contextMenuY"
555
+ :isAutoShowCard="isAutoShowCard"
556
+ :showPin="features.enableContextMenuPin"
557
+ :showAutoShowCard="features.enableContextMenuAutoShowCard"
558
+ @pin="handleContextMenuPin"
559
+ @pronounce="(lang) => handlePronounce(currentWordForContextMenu, lang)"
560
+ @pronounceAll="(lang) => handlePronounceAll(lang)"
561
+ @autoShowCard="
562
+ () => {
563
+ if (!features.enableContextMenuAutoShowCard) return
564
+ isAutoShowCard = !isAutoShowCard
565
+ hideContextMenu()
566
+ }
567
+ "
568
+ @checkAll="checkAll"
569
+ @reset="reset"
570
+ />
571
+ </template>
572
+
573
+ <style scoped lang="scss">
574
+ .tn-word-list {
575
+ // Checkbox 样式
576
+ input[type='checkbox'] {
577
+ margin: 8px;
578
+ transform: scale(1.3);
579
+ cursor: pointer;
580
+ }
581
+
582
+ // 链接样式
583
+ a {
584
+ text-decoration: none;
585
+ color: #4fc3f7;
586
+
587
+ &:hover {
588
+ text-decoration: underline !important;
589
+ }
590
+
591
+ &.lineThrough {
592
+ color: #999;
593
+ text-decoration: line-through;
594
+ }
595
+
596
+ &.textRed {
597
+ color: #f40 !important;
598
+ }
599
+ }
600
+
601
+ // 单词列表样式
602
+ ol {
603
+ list-style-type: decimal;
604
+ padding-left: 20px;
605
+
606
+ li {
607
+ display: flex;
608
+ align-items: center;
609
+ margin-bottom: 8px;
610
+ transition: all 0.3s ease;
611
+
612
+ &.pronounced {
613
+ background-color: rgba(255, 255, 0, 0.1);
614
+ }
615
+ }
616
+ }
617
+
618
+ // 序号样式
619
+ .index {
620
+ margin-right: 10px;
621
+ color: #aaa;
622
+ }
623
+ }
624
+
625
+ // 单词卡片样式(🌑 暗色悬浮卡片)
626
+ .wordCard {
627
+ position: fixed;
628
+ z-index: 9999;
629
+ background: #1e1e1e;
630
+ border: 1px solid #333;
631
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
632
+ padding: 12px 16px;
633
+ max-width: 600px;
634
+ min-width: 200px;
635
+ min-height: 100px;
636
+ font-size: 14px;
637
+ line-height: 1.4;
638
+ border-radius: 8px;
639
+ color: #eee;
640
+ pointer-events: auto;
641
+ font-family: sans-serif;
642
+ cursor: move;
643
+
644
+ // 关闭按钮
645
+ .closeBtn {
646
+ position: absolute;
647
+ right: 5px;
648
+ top: 5px;
649
+ background: none;
650
+ border: none;
651
+ font-size: 16px;
652
+ cursor: pointer;
653
+ color: #ccc;
654
+
655
+ &:hover {
656
+ color: white;
657
+ }
658
+ }
659
+ }
660
+
661
+ // 卡片内容包裹器
662
+ .wordCardContentWrapper {
663
+ width: 100%;
664
+ height: 100%;
665
+ overflow: auto;
666
+ }
667
+
668
+ // 卡片内容样式
669
+ .wordCardContent {
670
+ :deep(ul) {
671
+ margin: 0.5rem 0;
672
+ padding-left: 1rem;
673
+ }
674
+ }
675
+
676
+ // 调整大小手柄
677
+ .resizeHandle {
678
+ position: absolute;
679
+ right: 0;
680
+ bottom: 0;
681
+ width: 12px;
682
+ height: 12px;
683
+ background-color: #666;
684
+ cursor: nwse-resize;
685
+ z-index: 2;
686
+ border-radius: 50%;
687
+
688
+ &:hover {
689
+ background-color: #aaa;
690
+ }
691
+ }
692
+ </style>