From c5720973eee5049b759cefc195d5372b1a5af8e5 Mon Sep 17 00:00:00 2001 From: Zyronon Date: Tue, 6 Jan 2026 19:48:29 +0800 Subject: [PATCH] wip --- README.md | 9 - docs/README.en.md | 11 +- public/static-home.html | 5 +- src/assets/css/style.scss | 3 + src/hooks/dict.ts | 93 ++- src/pages/article/ArticlesPage.vue | 2 +- src/pages/article/BookDetail.vue | 529 ++++++++---------- .../article/components/TypingArticle.vue | 121 +--- src/pages/setting/Log.vue | 4 + src/pages/setting/Setting.vue | 43 +- src/pages/word/WordsPage.vue | 43 +- src/pages/word/components/GroupList.vue | 2 +- uno.config.ts | 4 + 13 files changed, 389 insertions(+), 480 deletions(-) diff --git a/README.md b/README.md index 3227bf6f..a2806dc1 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,6 @@ zyronon%2FTypeWords | Trendshift -

-
- License -
- 赞助: Skywork.AI: 10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left -
-
-

- 295shots_so 922shots_so diff --git a/docs/README.en.md b/docs/README.en.md index f4be4047..fe50007d 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -23,16 +23,7 @@ Practice English, one strike, one step forward
TypeWords | Trendshift
- - -

-
- License -
- Sponsor: Skywork.AI: 10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left -
-
-

+ 295shots_so 922shots_so diff --git a/public/static-home.html b/public/static-home.html index 4010a85b..cae8dcb4 100644 --- a/public/static-home.html +++ b/public/static-home.html @@ -311,6 +311,7 @@ width: 100%; margin-bottom: 1rem; color: var(--color-card-text); + display: none; } .container { @@ -714,9 +715,9 @@ - +
川公网安备51015602001426号
- +
蜀ICP备2025157466号-2
diff --git a/src/assets/css/style.scss b/src/assets/css/style.scss index 0e05da38..0e29da87 100644 --- a/src/assets/css/style.scss +++ b/src/assets/css/style.scss @@ -71,6 +71,9 @@ --bg-book: rgb(226 232 240); --color-line: rgb(226, 226, 226); + + --color-translate-main: black; + --color-translate-second: #818181; } html.dark { diff --git a/src/hooks/dict.ts b/src/hooks/dict.ts index f8b4b1db..ffad27ea 100644 --- a/src/hooks/dict.ts +++ b/src/hooks/dict.ts @@ -1,8 +1,13 @@ -import { Article, TaskWords, Word, WordPracticeMode } from '@/types/types.ts' +import { Article, Dict, DictId, DictType, TaskWords, Word } from '@/types/types.ts' import { useBaseStore } from '@/stores/base.ts' import { useSettingStore } from '@/stores/setting.ts' -import { getDefaultWord } from '@/types/func.ts' -import { cloneDeep, getRandomN, shuffle, splitIntoN } from '@/utils' +import { getDefaultDict, getDefaultWord } from '@/types/func.ts' +import { _getDictDataByUrl, cloneDeep, getRandomN, resourceWrap, shuffle, splitIntoN } from '@/utils' +import { onMounted, watch } from 'vue' +import { AppEnv, DICT_LIST } from '@/config/env.ts' +import { detail } from '@/apis' +import { useRuntimeStore } from '@/stores/runtime.ts' +import { useRoute, useRouter } from 'vue-router' export function useWordOptions() { const store = useBaseStore() @@ -12,9 +17,7 @@ export function useWordOptions() { } function toggleWordCollect(val: Word) { - let rIndex = store.collectWord.words.findIndex( - v => v.word.toLowerCase() === val.word.toLowerCase() - ) + let rIndex = store.collectWord.words.findIndex(v => v.word.toLowerCase() === val.word.toLowerCase()) if (rIndex > -1) { store.collectWord.words.splice(rIndex, 1) } else { @@ -114,9 +117,7 @@ export function getCurrentStudyWord(): TaskWords { if (complete && isEnd) { //复习比最小是1 let ratio = settingStore.wordReviewRatio || 1 - let ignoreList = [store.allIgnoreWords, store.knownWords][ - settingStore.ignoreSimpleWord ? 0 : 1 - ] + let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1] // 先将可用词表全部随机,再按需过滤忽略列表,只取到目标数量为止 let shuffled = shuffle(cloneDeep(dict.words)) let count = 0 @@ -204,7 +205,7 @@ export function getCurrentStudyWord(): TaskWords { } //如果已完成,那么合并写词和复习词 - if(complete){ + if (complete) { // data.new = [] // data.review = data.review.concat(data.write) // data.write = [] @@ -215,3 +216,75 @@ export function getCurrentStudyWord(): TaskWords { // console.log('data-write', data.write.map(v => v.word)) return data } + +export function useGetDict() { + const store = useBaseStore() + const runtimeStore = useRuntimeStore() + let loading = $ref(false) + const route = useRoute() + const router = useRouter() + + watch( + [() => store.load, () => loading], + ([a, b]) => { + if (a && b) loadDict() + }, + { immediate: true } + ) + + onMounted(() => { + if (!runtimeStore.editDict?.id) { + let dictId = route.params?.id + if (!dictId) { + return router.push('/articles') + } + loading = true + } else { + loadDict(runtimeStore.editDict) + } + }) + + async function loadDict(dict?: Dict) { + // console.log('load好了开始加载') + if (!dict) { + dict = getDefaultDict() + let dictId = route.query.id + //先在自己的词典列表里面找,如果没有再在资源列表里面找 + dict = store.article.bookList.find(v => v.id === dictId) + let r = await fetch(resourceWrap(DICT_LIST.WORD.ALL)) + let dict_list = await r.json() + if (!dict) dict = dict_list.flat().find(v => v.id === dictId) as Dict + } + if (dict && dict.id) { + if ( + !dict?.articles?.length && + !dict?.custom && + ![DictId.articleCollect].includes(dict.en_name || dict.id) && + !dict?.is_default + ) { + loading = true + let r = await _getDictDataByUrl(dict, DictType.article) + runtimeStore.editDict = r + } + if (store.article.bookList.find(book => book.id === runtimeStore.editDict.id)) { + if (AppEnv.CAN_REQUEST) { + let res = await detail({ id: runtimeStore.editDict.id }) + if (res.success) { + runtimeStore.editDict.statistics = res.data.statistics + if (res.data.articles.length) { + runtimeStore.editDict.articles = res.data.articles + } + } + } + } + loading = false + } else { + // router.push('/articles') + } + } + + return { + dict: runtimeStore.editDict, + loading, + } +} diff --git a/src/pages/article/ArticlesPage.vue b/src/pages/article/ArticlesPage.vue index a93fdbd8..90e1e515 100644 --- a/src/pages/article/ArticlesPage.vue +++ b/src/pages/article/ArticlesPage.vue @@ -163,7 +163,7 @@ function toggleSelect(item) { async function goBookDetail(val: DictResource) { runtimeStore.editDict = getDefaultDict(val) - nav('book-detail') + nav('book-detail',{id: val.id}) } const totalSpend = $computed(() => { diff --git a/src/pages/article/BookDetail.vue b/src/pages/article/BookDetail.vue index 8806d4c6..7ea4dea1 100644 --- a/src/pages/article/BookDetail.vue +++ b/src/pages/article/BookDetail.vue @@ -12,12 +12,12 @@ import { computed, onMounted, onUnmounted, watch } from 'vue' import { _dateFormat, _getDictDataByUrl, - isMobile, + _nextTick, + cloneDeep, msToHourMinute, resourceWrap, total, useNav, - _nextTick, } from '@/utils' import { getDefaultArticle, getDefaultDict } from '@/types/func.ts' import Toast from '@/components/base/toast/Toast.ts' @@ -29,31 +29,21 @@ import { AppEnv, DICT_LIST } from '@/config/env.ts' import { detail } from '@/apis' import BaseIcon from '@/components/BaseIcon.vue' import Switch from '@/components/base/Switch.vue' +import { useGetDict } from '@/hooks/dict.ts' const runtimeStore = useRuntimeStore() const settingStore = useSettingStore() -const base = useBaseStore() +const store = useBaseStore() const router = useRouter() const route = useRoute() const { nav } = useNav() let isEdit = $ref(false) let isAdd = $ref(false) -let loading = $ref(false) let studyLoading = $ref(false) let selectArticle: Article = $ref(getDefaultArticle({ id: -1 })) -// 计算当前选中文章的索引 -const currentArticleIndex = computed(() => { - return runtimeStore.editDict.articles.findIndex(article => article.id === selectArticle.id) -}) - -// 处理播放下一个音频 -const handlePlayNext = (nextArticle: Article) => { - selectArticle = nextArticle -} - function handleCheckedChange(val) { selectArticle = val.item } @@ -64,7 +54,7 @@ async function startPractice() { return Toast.warning('没有文章可学习!') } studyLoading = true - await base.changeBook(sbook) + await store.changeBook(sbook) studyLoading = false window.umami?.track('startStudyArticle', { @@ -80,63 +70,22 @@ const showBookDetail = computed(() => { return !(isAdd || isEdit) }) -async function init() { +const { dict, loading } = useGetDict() + +onMounted(() => { if (route.query?.isAdd) { isAdd = true runtimeStore.editDict = getDefaultDict() - } else { - if (!runtimeStore.editDict.id) { - await router.push('/articles') - } else { - if ( - !runtimeStore.editDict?.articles?.length && - !runtimeStore.editDict?.custom && - ![DictId.articleCollect].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id) && - !runtimeStore.editDict?.is_default - ) { - loading = true - let r = await _getDictDataByUrl(runtimeStore.editDict, DictType.article) - runtimeStore.editDict = r - } - - if (base.article.bookList.find(book => book.id === runtimeStore.editDict.id)) { - if (AppEnv.CAN_REQUEST) { - let res = await detail({ id: runtimeStore.editDict.id }) - if (res.success) { - runtimeStore.editDict.statistics = res.data.statistics - if (res.data.articles.length) { - runtimeStore.editDict.articles = res.data.articles - } - } - } - } - selectArticle = runtimeStore.editDict.articles[0] - loading = false - } } -} - -onMounted(() => { - init() - window.addEventListener('resize', handleResize) }) -watch( - () => selectArticle.id, - () => { - if (displayMode === 'typing-style') { - } - positionTranslations() - } -) - onUnmounted(() => { window.removeEventListener('resize', handleResize) }) function handleResize() { - if (displayMode === 'typing-style') { + if (displayMode === 'inline') { positionTranslations() } } @@ -156,9 +105,9 @@ function reset() { let dict = book_list.value.find(v => v.url === runtimeStore.editDict.url) as Dict if (dict && dict.id) { dict = await _getDictDataByUrl(dict, DictType.article) - let rIndex = base.article.bookList.findIndex(v => v.id === runtimeStore.editDict.id) + let rIndex = store.article.bookList.findIndex(v => v.id === runtimeStore.editDict.id) if (rIndex > -1) { - let item = base.article.bookList[rIndex] + let item = store.article.bookList[rIndex] item.custom = false item.id = dict.id item.articles = dict.articles @@ -211,9 +160,9 @@ const list = $computed(() => { let showTranslate = $ref(true) let startPlay = $ref(false) -let displayMode = $ref<'normal' | 'typing-style'>('normal') +let showDisplayMode = $ref(false) +let displayMode = $ref<'card' | 'inline' | 'line'>('inline') let articleWrapperRef = $ref() -const isMob = isMobile() const handleVolumeUpdate = (volume: number) => { settingStore.articleSoundVolume = volume @@ -223,66 +172,31 @@ const handleSpeedUpdate = (speed: number) => { settingStore.articleSoundSpeed = speed } -// 解析文本为段落和句子结构 -interface ParsedSentence { - text: string - translate: string -} +// 计算段落数量 +const paragraphCount = $computed(() => { + if (!selectArticle.text) return 0 + return selectArticle.text.split('\n\n').filter(p => p.trim()).length +}) -interface ParsedParagraph { - sentences: ParsedSentence[] -} - -function parseTextToSections(text: string, textTranslate: string): ParsedParagraph[] { - if (!text) return [] - - // 按段落分割(双换行) - const textParagraphs = text.split('\n\n').filter(p => p.trim()) - const translateParagraphs = textTranslate ? textTranslate.split('\n\n').filter(p => p.trim()) : [] - - // 句子分割正则:按句号、问号、感叹号分割,但保留标点 - const sentenceRegex = /([^.!?]+[.!?]+)/g - - return textParagraphs.map((para, paraIndex) => { - // 分割句子 - const sentences = para.match(sentenceRegex) || [para] - const translateSentences = translateParagraphs[paraIndex] - ? translateParagraphs[paraIndex].match(sentenceRegex) || [translateParagraphs[paraIndex]] - : [] - - return { - sentences: sentences.map((sent, sentIndex) => ({ - text: sent.trim(), - translate: translateSentences[sentIndex]?.trim() || '', - })), - } - }) -} - -// 计算解析后的文章结构 -const parsedArticle = $computed(() => { - if (!selectArticle.text || displayMode !== 'typing-style') return null - return parseTextToSections(selectArticle.text, selectArticle.textTranslate || '') +// 判断是否应该在段落下显示译文(card 模式且段落数 > 1) +const shouldShowInlineTranslation = $computed(() => { + return displayMode === 'card' && paragraphCount > 1 }) // 定位翻译到原文下方 function positionTranslations() { - // if ( isMob || !articleWrapperRef) return _nextTick(() => { const articleRect = articleWrapperRef.getBoundingClientRect() - console.log('articleRect',articleRect) selectArticle.textTranslate.split('\n\n').forEach((paragraph, paraIndex) => { paragraph.split('\n').forEach((sentence, sentIndex) => { - debugger const location = `${paraIndex}-${sentIndex}` - const sentenceClassName = `.sentence-${location}` + const sentenceClassName = `.word-${location}-0` const sentenceEl = articleWrapperRef?.querySelector(sentenceClassName) const translateClassName = `.translate-${location}` const translateEl = articleWrapperRef?.querySelector(translateClassName) as HTMLDivElement if (sentenceEl && translateEl && sentence) { const sentenceRect = sentenceEl.getBoundingClientRect() - console.log('sentenceRect',sentenceEl.innerText, sentenceRect) translateEl.style.opacity = '1' translateEl.style.top = sentenceRect.top - articleRect.top + 24 + 'px' const spaceEl = translateEl.firstElementChild as HTMLElement @@ -292,12 +206,12 @@ function positionTranslations() { } }) }) - }, 300) + }) } // 监听显示模式和文章变化,重新定位翻译 watch([() => displayMode, () => selectArticle.id, () => showTranslate], () => { - if (displayMode === 'typing-style') { + if (displayMode !== 'card') { positionTranslations() } }) @@ -305,194 +219,213 @@ watch([() => displayMode, () => selectArticle.id, () => showTranslate], () => {