diff --git a/apps/nuxt/app/pages/(words)/practice-words/[id].vue b/apps/nuxt/app/pages/(words)/practice-words/[id].vue index 9d3ca9c2..c93e2f56 100644 --- a/apps/nuxt/app/pages/(words)/practice-words/[id].vue +++ b/apps/nuxt/app/pages/(words)/practice-words/[id].vue @@ -11,7 +11,7 @@ import { useStartKeyboardEventListener, } from '@typewords/core/hooks/event.ts' import useTheme from '@typewords/core/hooks/theme.ts' -import { getCurrentStudyWord, useWordOptions } from '@typewords/core/hooks/dict.ts' +import { ensureWordSourceDictId, getCurrentStudyWord, useWordOptions } from '@typewords/core/hooks/dict.ts' import { _getDictDataByUrl, _nextTick, @@ -719,6 +719,7 @@ function onTypeWrong() { statStore.wrong++ } if (!store.wrong.words.find((v: Word) => v.word.toLowerCase() === temp)) { + ensureWordSourceDictId(word, store.sdict.id) store.wrong.words.push(word) store.wrong.length = store.wrong.words.length } diff --git a/apps/nuxt/i18n/locales/de.json b/apps/nuxt/i18n/locales/de.json index c5553cd1..e38e5de0 100644 --- a/apps/nuxt/i18n/locales/de.json +++ b/apps/nuxt/i18n/locales/de.json @@ -195,6 +195,7 @@ "uncollect": "Favorit entfernen", "mark_mastered": "Als gemeistert markieren", "unmark_mastered": "Gemeistert-Markierung entfernen", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "Danke für die Nutzung dieses Projekts! Es ist ein Open-Source-Projekt, kostenlos nutzbar. Wenn es hilfreich war, geben Sie uns einen Stern auf GitHub!", "github_address": "GitHub:", "about_wechat_feedback": "WeChat-Feedback:", diff --git a/apps/nuxt/i18n/locales/en.json b/apps/nuxt/i18n/locales/en.json index e1d7c05b..e26280f6 100644 --- a/apps/nuxt/i18n/locales/en.json +++ b/apps/nuxt/i18n/locales/en.json @@ -195,6 +195,7 @@ "uncollect": "Unfavorite", "mark_mastered": "Mark as Mastered", "unmark_mastered": "Unmark Mastered", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "Thank you for using this project! It is open source and free. If helpful, please star us on GitHub. Your support motivates our improvement!", "github_address": "GitHub: ", "about_wechat_feedback": "WeChat Feedback: ", @@ -531,4 +532,4 @@ "self_assessment": "自我评估", "word_test": "单词测试", "identify_method": "自测方式" -} \ No newline at end of file +} diff --git a/apps/nuxt/i18n/locales/es.json b/apps/nuxt/i18n/locales/es.json index 75331ed6..3488adb4 100644 --- a/apps/nuxt/i18n/locales/es.json +++ b/apps/nuxt/i18n/locales/es.json @@ -195,6 +195,7 @@ "uncollect": "Quitar favorito", "mark_mastered": "Marcar como dominado", "unmark_mastered": "Desmarcar dominado", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "¡Gracias por usar este proyecto! Es un proyecto de código abierto, de uso gratuito. Si le resulta útil, ¡déjenos una estrella en GitHub!", "github_address": "GitHub:", "about_wechat_feedback": "Comentarios de WeChat:", diff --git a/apps/nuxt/i18n/locales/fr.json b/apps/nuxt/i18n/locales/fr.json index 2dfccd70..a2bb5c66 100644 --- a/apps/nuxt/i18n/locales/fr.json +++ b/apps/nuxt/i18n/locales/fr.json @@ -195,6 +195,7 @@ "uncollect": "Retirer des favoris", "mark_mastered": "Marquer comme maîtrisé", "unmark_mastered": "Démarquer comme maîtrisé", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "Merci d'utiliser ce projet ! C'est un projet open source, gratuit. Si vous le trouvez utile, mettez-nous une étoile sur GitHub !", "github_address": "GitHub:", "about_wechat_feedback": "Retour WeChat:", diff --git a/apps/nuxt/i18n/locales/id.json b/apps/nuxt/i18n/locales/id.json index 0c943d72..aeb677b6 100644 --- a/apps/nuxt/i18n/locales/id.json +++ b/apps/nuxt/i18n/locales/id.json @@ -195,6 +195,7 @@ "uncollect": "Batal favorit", "mark_mastered": "Tandai sebagai dikuasai", "unmark_mastered": "Hapus tanda dikuasai", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "Terima kasih telah menggunakan proyek ini! Ini adalah proyek open source, gratis digunakan. Jika bermanfaat, beri kami bintang di GitHub!", "github_address": "GitHub:", "about_wechat_feedback": "Umpan Balik WeChat:", diff --git a/apps/nuxt/i18n/locales/ja.json b/apps/nuxt/i18n/locales/ja.json index 6cbda22b..3b0c97e2 100644 --- a/apps/nuxt/i18n/locales/ja.json +++ b/apps/nuxt/i18n/locales/ja.json @@ -195,6 +195,7 @@ "uncollect": "お気に入り解除", "mark_mastered": "習得済みとしてマーク", "unmark_mastered": "習得済みマーク解除", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "ご利用ありがとうございます!これはオープンソースプロジェクトで、無料でご利用いただけます。役に立った場合は、GitHubでスターをお願いします!", "github_address": "GitHub:", "about_wechat_feedback": "WeChatフィードバック:", diff --git a/apps/nuxt/i18n/locales/ko.json b/apps/nuxt/i18n/locales/ko.json index 1663befd..210b3a27 100644 --- a/apps/nuxt/i18n/locales/ko.json +++ b/apps/nuxt/i18n/locales/ko.json @@ -195,6 +195,7 @@ "uncollect": "즐겨찾기 해제", "mark_mastered": "마스터로 표시", "unmark_mastered": "마스터 표시 해제", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "이 프로젝트를 사용해 주셔서 감사합니다! 이것은 무료로 사용할 수 있는 오픈소스 프로젝트입니다. 도움이 되셨다면 GitHub에서 스타를 눌러주세요!", "github_address": "GitHub:", "about_wechat_feedback": "WeChat 피드백:", diff --git a/apps/nuxt/i18n/locales/pt.json b/apps/nuxt/i18n/locales/pt.json index f7d717e2..d3f5c40f 100644 --- a/apps/nuxt/i18n/locales/pt.json +++ b/apps/nuxt/i18n/locales/pt.json @@ -195,6 +195,7 @@ "uncollect": "Desfavoritar", "mark_mastered": "Marcar como dominado", "unmark_mastered": "Desmarcar dominado", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "Obrigado por usar este projeto! É um projeto de código aberto, gratuito. Se achar útil, deixe uma estrela no GitHub!", "github_address": "GitHub:", "about_wechat_feedback": "Feedback do WeChat:", diff --git a/apps/nuxt/i18n/locales/ru.json b/apps/nuxt/i18n/locales/ru.json index 355a0459..30315091 100644 --- a/apps/nuxt/i18n/locales/ru.json +++ b/apps/nuxt/i18n/locales/ru.json @@ -195,6 +195,7 @@ "uncollect": "Убрать из избранного", "mark_mastered": "Отметить как освоенное", "unmark_mastered": "Снять отметку освоенного", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "Спасибо за использование проекта! Это проект с открытым кодом, бесплатный. Если нашли полезным, поставьте звезду на GitHub!", "github_address": "GitHub:", "about_wechat_feedback": "Отзыв в WeChat:", diff --git a/apps/nuxt/i18n/locales/th.json b/apps/nuxt/i18n/locales/th.json index a8c74269..97ea8277 100644 --- a/apps/nuxt/i18n/locales/th.json +++ b/apps/nuxt/i18n/locales/th.json @@ -195,6 +195,7 @@ "uncollect": "ยกเลิกรายการโปรด", "mark_mastered": "ทำเครื่องหมายว่าเชี่ยวชาญ", "unmark_mastered": "ยกเลิกเครื่องหมายเชี่ยวชาญ", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "ขอบคุณที่ใช้โปรเจกต์นี้! นี่คือโปรเจกต์โอเพนซอร์ส ใช้งานฟรี หากเป็นประโยชน์ กรุณาให้ดาวบน GitHub!", "github_address": "GitHub:", "about_wechat_feedback": "ติดต่อ WeChat:", diff --git a/apps/nuxt/i18n/locales/tw.json b/apps/nuxt/i18n/locales/tw.json index c014bde0..c93a4325 100644 --- a/apps/nuxt/i18n/locales/tw.json +++ b/apps/nuxt/i18n/locales/tw.json @@ -195,6 +195,7 @@ "uncollect": "取消收藏", "mark_mastered": "標記為已掌握", "unmark_mastered": "取消標記已掌握", + "missing_dict_hint": "需要下載 {dictName} 以顯示完整內容", "about_thanks": "感謝使用本项目!本项目是開源项目,免費使用,如果觉得有帮助,請在 GitHub 点個 Star,您的支持是我持續改進的動力!", "github_address": "GitHub地址:", "about_wechat_feedback": "微信反馈:", diff --git a/apps/nuxt/i18n/locales/uk.json b/apps/nuxt/i18n/locales/uk.json index fddb1d37..96e302cb 100644 --- a/apps/nuxt/i18n/locales/uk.json +++ b/apps/nuxt/i18n/locales/uk.json @@ -195,6 +195,7 @@ "uncollect": "Видалити з обраного", "mark_mastered": "Позначити як освоєне", "unmark_mastered": "Зняти позначку освоєного", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "Дякуємо за використання проекту! Це проект з відкритим кодом, безкоштовний. Якщо знайшли корисним, поставте зірку на GitHub!", "github_address": "GitHub:", "about_wechat_feedback": "Відгук у WeChat:", diff --git a/apps/nuxt/i18n/locales/vi.json b/apps/nuxt/i18n/locales/vi.json index 1017a9f4..7dd1c886 100644 --- a/apps/nuxt/i18n/locales/vi.json +++ b/apps/nuxt/i18n/locales/vi.json @@ -195,6 +195,7 @@ "uncollect": "Bỏ yêu thích", "mark_mastered": "Đánh dấu đã thành thạo", "unmark_mastered": "Bỏ đánh dấu thành thạo", + "missing_dict_hint": "Download {dictName} to view full definitions", "about_thanks": "Cảm ơn bạn đã sử dụng dự án này! Đây là dự án mã nguồn mở, miễn phí. Nếu thấy hữu ích, hãy cho chúng tôi một sao trên GitHub!", "github_address": "GitHub:", "about_wechat_feedback": "Phản hồi WeChat:", diff --git a/apps/nuxt/i18n/locales/zh.json b/apps/nuxt/i18n/locales/zh.json index 9f321db0..445dfdf2 100644 --- a/apps/nuxt/i18n/locales/zh.json +++ b/apps/nuxt/i18n/locales/zh.json @@ -195,6 +195,7 @@ "uncollect": "取消收藏", "mark_mastered": "标记为已掌握", "unmark_mastered": "取消标记已掌握", + "missing_dict_hint": "需要下载 {dictName} 以显示完整内容", "about_thanks": "感谢使用本项目!本项目是开源项目,免费使用,如果觉得有帮助,请在 GitHub 点个 Star,您的支持是我持续改进的动力!", "github_address": "GitHub地址:", "about_wechat_feedback": "微信反馈:", diff --git a/apps/vscode-web/src/pages/(words)/practice-words/[id].vue b/apps/vscode-web/src/pages/(words)/practice-words/[id].vue index 9a159ce3..a5282064 100644 --- a/apps/vscode-web/src/pages/(words)/practice-words/[id].vue +++ b/apps/vscode-web/src/pages/(words)/practice-words/[id].vue @@ -11,7 +11,7 @@ import { useStartKeyboardEventListener, } from '@typewords/core/hooks/event.ts' import useTheme from '@typewords/core/hooks/theme.ts' -import { getCurrentStudyWord, useWordOptions } from '@typewords/core/hooks/dict.ts' +import { ensureWordSourceDictId, getCurrentStudyWord, useWordOptions } from '@typewords/core/hooks/dict.ts' import { _getDictDataByUrl, _nextTick, @@ -760,6 +760,7 @@ function onTypeWrong() { statStore.wrong++ } if (!store.wrong.words.find((v: Word) => v.word.toLowerCase() === temp)) { + ensureWordSourceDictId(word, store.sdict.id) store.wrong.words.push(word) store.wrong.length = store.wrong.words.length } diff --git a/packages/core/src/components/word/WordItem.vue b/packages/core/src/components/word/WordItem.vue index 52d00630..f28398f6 100644 --- a/packages/core/src/components/word/WordItem.vue +++ b/packages/core/src/components/word/WordItem.vue @@ -3,9 +3,12 @@ import type { Word } from '../../types' import { usePlayWordAudio } from '../../hooks/sound.ts' import { BaseIcon, Tooltip, VolumeIcon } from '@typewords/base' import { useWordOptions } from '../../hooks/dict.ts' +import { useWordHydrator } from '../../hooks/useWordHydrator' import TranslationList from './TranslationList.vue' +import { onMounted, watch } from 'vue' +import { useRouter } from 'vue-router' -withDefaults( +const props = withDefaults( defineProps<{ item: Word showTranslate?: boolean @@ -33,6 +36,56 @@ withDefaults( const playWordAudio = usePlayWordAudio() const { isWordCollect, toggleWordCollect, isWordSimple, toggleWordSimple } = useWordOptions() + +const { hydrate } = useWordHydrator() +const router = useRouter() + +let hydrateFailed = $ref(false) +let missingDictName = $ref('') +let hydrateToken = 0 + +async function doHydrate(item: Word) { + if (!props.showTranslate) return + const token = ++hydrateToken + const result = await hydrate(item) + // 仅当 token 未过期时更新状态,避免竞态导致旧结果覆盖当前单词 + if (token !== hydrateToken) return + if (!result.hydrated && result.dictName) { + hydrateFailed = true + missingDictName = result.dictName + } else if (result.hydrated) { + hydrateFailed = false + missingDictName = '' + } +} + +function goDictList() { + router.push('/dict-list') +} + +onMounted(() => { + doHydrate(props.item) +}) + +watch( + () => props.item, + val => { + doHydrate(val) + } +) + +watch( + () => props.showTranslate, + val => { + if (val) { + doHydrate(props.item) + return + } + hydrateToken++ + hydrateFailed = false + missingDictName = '' + } +) - + diff --git a/packages/core/src/hooks/dict.ts b/packages/core/src/hooks/dict.ts index b15d5ef7..e4ad21c0 100644 --- a/packages/core/src/hooks/dict.ts +++ b/packages/core/src/hooks/dict.ts @@ -14,6 +14,10 @@ import { computed } from 'vue' export function useWordOptions() { const store = useBaseStore() + function ensureCurrentWordSource(val: Word) { + ensureWordSourceDictId(val, store.sdict.id) + } + function isWordCollect(val: Word) { return !!store.collectWord.words.find(v => v.word.toLowerCase() === val.word.toLowerCase()) } @@ -23,6 +27,7 @@ export function useWordOptions() { if (rIndex > -1) { store.collectWord.words.splice(rIndex, 1) } else { + ensureCurrentWordSource(val) store.collectWord.words.push(val) } store.collectWord.length = store.collectWord.words.length @@ -37,6 +42,7 @@ export function useWordOptions() { if (rIndex > -1) { store.known.words.splice(rIndex, 1) } else { + ensureCurrentWordSource(val) store.known.words.push(val) } store.known.length = store.known.words.length @@ -68,6 +74,13 @@ export function useWordOptions() { } } +export function ensureWordSourceDictId(val: Word, sourceDictId?: string): Word { + if (!val.sourceDictId && sourceDictId) { + val.sourceDictId = sourceDictId + } + return val +} + export function useArticleOptions() { const store = useBaseStore() diff --git a/packages/core/src/hooks/useWordHydrator.ts b/packages/core/src/hooks/useWordHydrator.ts new file mode 100644 index 00000000..0a74df77 --- /dev/null +++ b/packages/core/src/hooks/useWordHydrator.ts @@ -0,0 +1,63 @@ +import { Word } from '../types' +import { useBaseStore } from '../stores/base' + +type DictIndexCache = { + signature: string + index: Map +} + +const dictIndex: Record = {} + +function getDictSignature(words: Word[]): string { + const length = words.length + const firstWord = words[0]?.word ?? '' + const lastWord = words[length - 1]?.word ?? '' + return `${length}:${firstWord}:${lastWord}` +} + +export function useWordHydrator() { + async function hydrate( + word: Word, + force = false + ): Promise<{ hydrated: boolean; dictName?: string }> { + if (!word.sourceDictId || (!force && word.trans && word.trans.length > 0)) { + return { hydrated: true } + } + + const dictId = word.sourceDictId + + // 每次 hydrate 时检查缓存索引是否仍有效(词典是否仍在 bookList 中且有单词) + const store = useBaseStore() + const userDict = store.word.bookList.find(v => v.id === dictId) + + if (!userDict?.words?.length) { + delete dictIndex[dictId] + return { hydrated: false, dictName: userDict?.name || dictId } + } + + const signature = getDictSignature(userDict.words) + + // 按需构建 O(1) 查找索引 + if (!dictIndex[dictId] || dictIndex[dictId].signature !== signature) { + const index = new Map() + for (const w of userDict.words) { + index.set(w.word.toLowerCase(), w) + } + dictIndex[dictId] = { + signature, + index, + } + } + + const sourceWord = dictIndex[dictId].index.get(word.word.toLowerCase()) + if (!sourceWord) { + return { hydrated: false, dictName: userDict.name || dictId } + } + + const { id, sourceDictId, custom, ...details } = sourceWord + Object.assign(word, details) + return { hydrated: true } + } + + return { hydrate } +} diff --git a/packages/core/src/types/types.ts b/packages/core/src/types/types.ts index 6eabbe81..d2cdfe6b 100644 --- a/packages/core/src/types/types.ts +++ b/packages/core/src/types/types.ts @@ -6,6 +6,7 @@ import { APP_VERSION } from '../config/env' export type Word = { id?: string custom?: boolean + sourceDictId?: string word: string phonetic0: string phonetic1: string diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index ad5cb1b8..c0f2d8eb 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -224,7 +224,39 @@ export async function checkAndUpgradeSaveSetting(val: any) { export function shakeCommonDict(n: BaseState): BaseState { let data: BaseState = cloneDeep(n) data.word.bookList.map((v: Dict) => { - if (!v.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(v.id)) v.words = [] + if (!v.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(v.id)) { + v.words = [] + } else if ([DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(v.id)) { + v.words.forEach(word => { + // 极致瘦身:如果有来源 ID,且不是自定义词典中的词,则在同步时删除几乎所有详情 + // 详情将由前端通过 sourceDictId 动态加载 + if (word.sourceDictId && !word.custom) { + word.trans = [] + word.phonetic0 = '' + word.phonetic1 = '' + word.sentences = [] + word.phrases = [] + word.synos = [] + word.relWords = { root: '', rels: [] } + word.etymology = [] + } else { + // 平衡瘦身:针对自定义单词或旧有无来源数据 + // 保留核心显示字段,剔除超重详情 + word.phrases = [] + word.synos = [] + word.relWords = { root: '', rels: [] } + word.etymology = [] + + // 对于“已掌握”单词,即使无来源也进一步剔除释义,仅保留标识用于过滤 + if (v.id === DictId.wordKnown) { + word.trans = [] + word.sentences = [] + word.phonetic0 = '' + word.phonetic1 = '' + } + } + }) + } }) data.article.bookList.map((v: Dict) => { if (!v.custom && ![DictId.articleCollect].includes(v.id)) v.articles = []