Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/nuxt/app/pages/(words)/practice-words/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
3 changes: 2 additions & 1 deletion apps/nuxt/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: ",
Expand Down Expand Up @@ -531,4 +532,4 @@
"self_assessment": "自我评估",
"word_test": "单词测试",
"identify_method": "自测方式"
}
}
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -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フィードバック:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 피드백:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/th.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
"uncollect": "取消收藏",
"mark_mastered": "標記為已掌握",
"unmark_mastered": "取消標記已掌握",
"missing_dict_hint": "需要下載 {dictName} 以顯示完整內容",
"about_thanks": "感謝使用本项目!本项目是開源项目,免費使用,如果觉得有帮助,請在 GitHub 点個 Star,您的支持是我持續改進的動力!",
"github_address": "GitHub地址:",
"about_wechat_feedback": "微信反馈:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
1 change: 1 addition & 0 deletions apps/nuxt/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
"uncollect": "取消收藏",
"mark_mastered": "标记为已掌握",
"unmark_mastered": "取消标记已掌握",
"missing_dict_hint": "需要下载 {dictName} 以显示完整内容",
"about_thanks": "感谢使用本项目!本项目是开源项目,免费使用,如果觉得有帮助,请在 GitHub 点个 Star,您的支持是我持续改进的动力!",
"github_address": "GitHub地址:",
"about_wechat_feedback": "微信反馈:",
Expand Down
3 changes: 2 additions & 1 deletion apps/vscode-web/src/pages/(words)/practice-words/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down
78 changes: 75 additions & 3 deletions packages/core/src/components/word/WordItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Comment on lines +62 to +73
}
)
Comment on lines +47 to +75

watch(
() => props.showTranslate,
val => {
if (val) {
doHydrate(props.item)
return
}
hydrateToken++
hydrateFailed = false
missingDictName = ''
}
)
</script>

<template>
Expand All @@ -46,7 +99,10 @@ const { isWordCollect, toggleWordCollect, isWordSimple, toggleWordSimple } = use
<span class="phonetic text-gray" :class="!showWord && 'word-shadow'">{{ item.phonetic0 }}</span>
<VolumeIcon class="volume" @click="playWordAudio(item.word)"></VolumeIcon>
</div>
<TranslationList :pos-space="false" :word="item" :showFull="showWord" v-if="showTranslate" />
<TranslationList :pos-space="false" :word="item" :showFull="showWord" v-if="showTranslate && !hydrateFailed" />
<button v-else-if="showTranslate && hydrateFailed" class="missing-dict-hint" @click="goDictList">
{{ $t('missing_dict_hint', { dictName: missingDictName }) }}
</button>
</div>
</div>
<div class="right" v-if="showOption">
Expand Down Expand Up @@ -74,4 +130,20 @@ const { isWordCollect, toggleWordCollect, isWordSimple, toggleWordSimple } = use
</div>
</template>

<style scoped lang="scss"></style>
<style scoped lang="scss">
.missing-dict-hint {
color: var(--color-sub-text);
font-size: 0.82rem;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
transition: color 0.15s;
background: none;
border: none;
padding: 0;
font-family: inherit;
}
.missing-dict-hint:hover {
color: var(--color-icon-hightlight);
}
</style>
13 changes: 13 additions & 0 deletions packages/core/src/hooks/dict.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand Down
63 changes: 63 additions & 0 deletions packages/core/src/hooks/useWordHydrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Word } from '../types'
import { useBaseStore } from '../stores/base'

type DictIndexCache = {
signature: string
index: Map<string, Word>
}

const dictIndex: Record<string, DictIndexCache> = {}

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<string, Word>()
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 }
}
1 change: 1 addition & 0 deletions packages/core/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading