@tnotesjs/core 0.0.1
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.
- package/README.md +84 -0
- package/dist/chunk-CSSIRO3I.js +4288 -0
- package/dist/chunk-NASIL5FY.js +203 -0
- package/dist/cli/index.js +694 -0
- package/dist/index.js +8 -0
- package/dist/vitepress/config/index.js +904 -0
- package/package.json +77 -0
- package/types/config.ts +61 -0
- package/types/index.ts +11 -0
- package/types/note.ts +33 -0
- package/types/shims.d.ts +10 -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 +141 -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>
|