@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.
- package/README.md +105 -0
- package/dist/chunk-K3X5OP3N.js +1532 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +4199 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.js +9 -0
- package/package.json +74 -0
- package/types/config.ts +61 -0
- package/types/index.ts +11 -0
- package/types/note.ts +33 -0
- package/vitepress/assets/icons/icon__check.svg +3 -0
- package/vitepress/assets/icons/icon__clipboard.svg +8 -0
- package/vitepress/assets/icons/icon__close.svg +1 -0
- package/vitepress/assets/icons/icon__collapse.svg +1 -0
- package/vitepress/assets/icons/icon__confirm.svg +1 -0
- package/vitepress/assets/icons/icon__copy.svg +4 -0
- package/vitepress/assets/icons/icon__focus.svg +1 -0
- package/vitepress/assets/icons/icon__fold.svg +3 -0
- package/vitepress/assets/icons/icon__folder.svg +1 -0
- package/vitepress/assets/icons/icon__fullscreen.svg +1 -0
- package/vitepress/assets/icons/icon__fullscreen_exit.svg +1 -0
- package/vitepress/assets/icons/icon__github.svg +4 -0
- package/vitepress/assets/icons/icon__mindmap.svg +1 -0
- package/vitepress/assets/icons/icon__next.svg +1 -0
- package/vitepress/assets/icons/icon__number_gray.svg +1 -0
- package/vitepress/assets/icons/icon__number_purple.svg +1 -0
- package/vitepress/assets/icons/icon__prev.svg +1 -0
- package/vitepress/assets/icons/icon__restore.svg +1 -0
- package/vitepress/assets/icons/icon__rotate.svg +4 -0
- package/vitepress/assets/icons/icon__search.svg +1 -0
- package/vitepress/assets/icons/icon__sidebar_collapsed.svg +1 -0
- package/vitepress/assets/icons/icon__sidebar_opened.svg +1 -0
- package/vitepress/assets/icons/icon__totop.svg +6 -0
- package/vitepress/assets/icons/icon__vscode.svg +6 -0
- package/vitepress/assets/icons/icon__zoom_fit.svg +1 -0
- package/vitepress/assets/icons/icon__zoom_in.svg +1 -0
- package/vitepress/assets/icons/icon__zoom_out.svg +1 -0
- package/vitepress/assets/icons/icon__zoom_reset.svg +1 -0
- package/vitepress/assets/icons/index.ts +38 -0
- package/vitepress/components/BilibiliOutsidePlayer/BilibiliOutsidePlayer.vue +20 -0
- package/vitepress/components/CodeBlockFullscreen/CodeBlockFullscreen.vue +373 -0
- package/vitepress/components/CodeBlockFullscreen/index.ts +115 -0
- package/vitepress/components/CodeBlockFullscreen/styles.css +64 -0
- package/vitepress/components/Discussions/Discussions.module.scss +32 -0
- package/vitepress/components/Discussions/Discussions.vue +211 -0
- package/vitepress/components/EnWordList/EnWordList.module.scss +124 -0
- package/vitepress/components/EnWordList/EnWordList.vue +543 -0
- package/vitepress/components/EnWordList/RightClickMenu.module.scss +22 -0
- package/vitepress/components/EnWordList/RightClickMenu.vue +66 -0
- package/vitepress/components/Footprints/Footprints.module.scss +93 -0
- package/vitepress/components/Footprints/Footprints.vue +377 -0
- package/vitepress/components/Layout/AboutModal.module.scss +233 -0
- package/vitepress/components/Layout/AboutModal.vue +105 -0
- package/vitepress/components/Layout/AboutPanel.vue +266 -0
- package/vitepress/components/Layout/ContentCollapse.vue +603 -0
- package/vitepress/components/Layout/CustomSidebar.vue +605 -0
- package/vitepress/components/Layout/DocBeforeControls.vue +139 -0
- package/vitepress/components/Layout/DocFooter.vue +225 -0
- package/vitepress/components/Layout/ImagePreview.module.scss +201 -0
- package/vitepress/components/Layout/ImagePreview.vue +281 -0
- package/vitepress/components/Layout/Layout.module.scss +661 -0
- package/vitepress/components/Layout/Layout.vue +542 -0
- package/vitepress/components/Layout/NoteStatus.vue +140 -0
- package/vitepress/components/Layout/SidebarItems.vue +263 -0
- package/vitepress/components/Layout/SidebarNavBefore.vue +92 -0
- package/vitepress/components/Layout/Swiper.vue +167 -0
- package/vitepress/components/Layout/ToggleFullContent.module.scss +11 -0
- package/vitepress/components/Layout/ToggleFullContent.vue +34 -0
- package/vitepress/components/Layout/ToggleSidebar.module.scss +11 -0
- package/vitepress/components/Layout/ToggleSidebar.vue +35 -0
- package/vitepress/components/Layout/composables/useCollapseControl.ts +88 -0
- package/vitepress/components/Layout/composables/useNoteConfig.ts +121 -0
- package/vitepress/components/Layout/composables/useNoteSave.ts +173 -0
- package/vitepress/components/Layout/composables/useNoteValidation.ts +85 -0
- package/vitepress/components/Layout/composables/useRedirect.ts +110 -0
- package/vitepress/components/Layout/composables/useVSCodeIntegration.ts +85 -0
- package/vitepress/components/Layout/homeReadme.data.ts +124 -0
- package/vitepress/components/LoadingPage/LoadingPage.vue +192 -0
- package/vitepress/components/MarkMap/MarkMap.module.scss +159 -0
- package/vitepress/components/MarkMap/MarkMap.vue +404 -0
- package/vitepress/components/Mermaid/Mermaid.module.scss +275 -0
- package/vitepress/components/Mermaid/Mermaid.vue +364 -0
- package/vitepress/components/NotesTable/NotesTable.module.scss +77 -0
- package/vitepress/components/NotesTable/NotesTable.vue +98 -0
- package/vitepress/components/NotesTable/README.md +67 -0
- package/vitepress/components/Settings/Settings.module.scss +433 -0
- package/vitepress/components/Settings/Settings.vue +306 -0
- package/vitepress/components/SidebarCard/MindMapView.vue +483 -0
- package/vitepress/components/SidebarCard/NotesTrendChart.vue +108 -0
- package/vitepress/components/SidebarCard/SidebarCard.vue +948 -0
- package/vitepress/components/Tooltip/Tooltip.vue +70 -0
- package/vitepress/components/constants.ts +91 -0
- package/vitepress/components/notesConfig.data.ts +73 -0
- package/vitepress/components/sidebar.data.ts +59 -0
- package/vitepress/components/tnotes-config.data.ts +21 -0
- package/vitepress/components/utils.ts +26 -0
- package/vitepress/config/index.ts +126 -0
- package/vitepress/configs/constants.ts +26 -0
- package/vitepress/configs/head.config.ts +25 -0
- package/vitepress/configs/index.ts +9 -0
- package/vitepress/configs/markdown-it.d.ts +23 -0
- package/vitepress/configs/markdown.config.ts +366 -0
- package/vitepress/configs/theme.config.ts +108 -0
- package/vitepress/plugins/buildProgressPlugin.ts +390 -0
- package/vitepress/plugins/getNoteByConfigIdPlugin.ts +107 -0
- package/vitepress/plugins/renameNotePlugin.ts +60 -0
- package/vitepress/plugins/updateConfigPlugin.ts +63 -0
- package/vitepress/theme/index.ts +95 -0
- package/vitepress/theme/styles/base.scss +50 -0
- package/vitepress/theme/styles/components/404.scss +31 -0
- package/vitepress/theme/styles/components/collapse.scss +175 -0
- package/vitepress/theme/styles/components/markmap.scss +101 -0
- package/vitepress/theme/styles/components/swiper.scss +255 -0
- package/vitepress/theme/styles/index.scss +25 -0
- package/vitepress/theme/styles/layout.scss +62 -0
- package/vitepress/theme/styles/utilities.scss +39 -0
- 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>
|