wip
This commit is contained in:
@@ -21,15 +21,6 @@
|
||||
<a href="https://trendshift.io/repositories/15226" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15226" alt="zyronon%2FTypeWords | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<br>
|
||||
<a href="https://skywork.ai/p/GrXQb4"><img src="/public/skywork-ai.png" alt="License" style="width: 650px;"></a>
|
||||
<br>
|
||||
赞助: <a href="https://skywork.ai/p/GrXQb4" target="_blank">Skywork.AI: 10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left</a>
|
||||
<br/>
|
||||
<br/>
|
||||
</p>
|
||||
|
||||
<img width="1920" height="1440" alt="295shots_so" src="https://github.com/user-attachments/assets/383ed437-856e-48fe-92b0-9619babb49be" />
|
||||
<img width="1920" height="1440" alt="922shots_so" src="https://github.com/user-attachments/assets/5b5fa13f-747c-4368-ae21-3c9d7d30fbc7" />
|
||||
|
||||
|
||||
@@ -23,16 +23,7 @@ Practice English, one strike, one step forward
|
||||
<div align=center>
|
||||
<a href="https://trendshift.io/repositories/14139" target="_blank" class="trendshift-badge"><img src="https://trendshift.io/api/badge/repositories/14139" alt="TypeWords | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</div>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<br>
|
||||
<a href="https://skywork.ai/p/GrXQb4"><img src="/public/skywork-ai.png" alt="License" style="width: 650px;"></a>
|
||||
<br>
|
||||
Sponsor: <a href="https://skywork.ai/p/GrXQb4" target="_blank">Skywork.AI: 10 tasks in 1 hour, not 10 hours →Limited free spots: 127 left</a>
|
||||
<br/>
|
||||
<br/>
|
||||
</p>
|
||||
|
||||
|
||||
<img width="1920" height="1440" alt="295shots_so" src="https://github.com/user-attachments/assets/383ed437-856e-48fe-92b0-9619babb49be" />
|
||||
<img width="1920" height="1440" alt="922shots_so" src="https://github.com/user-attachments/assets/5b5fa13f-747c-4368-ae21-3c9d7d30fbc7" />
|
||||
|
||||
@@ -311,6 +311,7 @@
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-card-text);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -714,9 +715,9 @@
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<div><a href="https://beian.mps.gov.cn/#/query/webSearch?code=51015602001426" target="_blank">川公网安备51015602001426号 </a></div>
|
||||
|
||||
|
||||
<div><a href="https://beian.miit.gov.cn/" target="_blank">蜀ICP备2025157466号-2</a></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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<HTMLElement>()
|
||||
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], () => {
|
||||
|
||||
<template>
|
||||
<div class="center h-screen">
|
||||
<div
|
||||
class="mb-0 flex p-space box-border flex-col bg-second w-full 3xl:w-7/10 2xl:w-8/10 xl:w-full 2xl:card 2xl:h-[97vh] h-full"
|
||||
v-if="showBookDetail"
|
||||
>
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<div class="flex gap-space">
|
||||
<BackIcon class="dict-back z-2" />
|
||||
<div class="dict-title text-2xl text-align-center">{{ runtimeStore.editDict.name }}</div>
|
||||
<div class="bg-second w-full 3xl:w-7/10 2xl:w-8/10 xl:w-full 2xl:card 2xl:h-[97vh] h-full overflow-hidden mb-0">
|
||||
<div class="flex p-space box-border flex-col h-full" v-if="showBookDetail">
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<div class="flex gap-space">
|
||||
<BackIcon class="dict-back z-2" />
|
||||
<div class="dict-title text-2xl text-align-center">{{ runtimeStore.editDict.name }}</div>
|
||||
</div>
|
||||
<div class="dict-actions flex">
|
||||
<BaseButton v-if="runtimeStore.editDict.custom && runtimeStore.editDict.url" type="info" @click="reset">
|
||||
恢复默认
|
||||
</BaseButton>
|
||||
<BaseButton :loading="studyLoading || loading" type="info" @click="isEdit = true">编辑</BaseButton>
|
||||
<BaseButton type="info" @click="router.push('batch-edit-article')">文章管理</BaseButton>
|
||||
<BaseButton :loading="studyLoading || loading" @click="startPractice">学习</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dict-actions flex">
|
||||
<BaseButton v-if="runtimeStore.editDict.custom && runtimeStore.editDict.url" type="info" @click="reset">
|
||||
恢复默认
|
||||
</BaseButton>
|
||||
<BaseButton :loading="studyLoading || loading" type="info" @click="isEdit = true">编辑</BaseButton>
|
||||
<BaseButton type="info" @click="router.push('batch-edit-article')">文章管理</BaseButton>
|
||||
<BaseButton :loading="studyLoading || loading" @click="startPractice">学习</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 overflow-hidden mt-3">
|
||||
<div class="3xl:w-80 2xl:w-60 xl:w-55 lg:w-50 overflow-auto">
|
||||
<ArticleList
|
||||
:show-desc="true"
|
||||
v-if="runtimeStore.editDict.length"
|
||||
@click="handleCheckedChange"
|
||||
:list="list"
|
||||
:active-id="selectArticle.id"
|
||||
>
|
||||
</ArticleList>
|
||||
<Empty v-else />
|
||||
</div>
|
||||
<div class="flex-1 shrink-0 pl-4 flex flex-col overflow-hidden">
|
||||
<template v-if="selectArticle.id">
|
||||
<template v-if="selectArticle.id === -1">
|
||||
<div class="flex gap-4 mt-2">
|
||||
<img
|
||||
:src="runtimeStore.editDict?.cover"
|
||||
class="w-30 rounded-md"
|
||||
v-if="runtimeStore.editDict?.cover"
|
||||
alt=""
|
||||
/>
|
||||
<div class="text-lg">介绍:{{ runtimeStore.editDict.description }}</div>
|
||||
</div>
|
||||
<div class="text-base" v-if="totalSpend">总学习时长:{{ totalSpend }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex-1 space-y-10 overflow-auto pb-30">
|
||||
<div>
|
||||
<div class="flex justify-between items-center relative">
|
||||
<span class="text-3xl">
|
||||
<span class="font-bold">{{ selectArticle.title }}</span>
|
||||
<span class="ml-6 text-2xl" v-if="showTranslate">{{ selectArticle.titleTranslate }}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2 mr-4">
|
||||
<BaseIcon
|
||||
:title="`切换显示模式`"
|
||||
@click="displayMode = displayMode === 'normal' ? 'typing-style' : 'normal'"
|
||||
>
|
||||
<IconFluentTextParagraph16Regular v-if="displayMode === 'normal'" />
|
||||
<IconFluentTextAlignLeft16Regular v-else />
|
||||
<div class="flex flex-1 overflow-hidden mt-3">
|
||||
<div class="3xl:w-80 2xl:w-60 xl:w-55 lg:w-50 overflow-auto">
|
||||
<ArticleList
|
||||
:show-desc="true"
|
||||
v-if="runtimeStore.editDict.length"
|
||||
@click="handleCheckedChange"
|
||||
:list="list"
|
||||
:active-id="selectArticle.id"
|
||||
>
|
||||
</ArticleList>
|
||||
<Empty v-else />
|
||||
</div>
|
||||
<div class="flex-1 shrink-0 pl-4 flex flex-col overflow-hidden">
|
||||
<template v-if="selectArticle.id">
|
||||
<template v-if="selectArticle.id === -1">
|
||||
<div class="flex gap-4 mt-2">
|
||||
<img
|
||||
:src="runtimeStore.editDict?.cover"
|
||||
class="w-30 rounded-md"
|
||||
v-if="runtimeStore.editDict?.cover"
|
||||
alt=""
|
||||
/>
|
||||
<div class="text-lg">介绍:{{ runtimeStore.editDict.description }}</div>
|
||||
</div>
|
||||
<div class="text-base" v-if="totalSpend">总学习时长:{{ totalSpend }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex-1 overflow-auto pb-30">
|
||||
<div>
|
||||
<div class="flex justify-between items-center relative">
|
||||
<span>
|
||||
<span class="text-4xl">{{ selectArticle.title }}</span>
|
||||
<span class="ml-6 text-2xl" v-if="showTranslate">{{ selectArticle.titleTranslate }}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2 mr-4">
|
||||
<BaseIcon :title="`开关释义显示`" @click="showTranslate = !showTranslate">
|
||||
<IconFluentTranslate16Regular v-if="showTranslate" />
|
||||
<IconFluentTranslateOff16Regular v-else />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
:disabled="!showTranslate"
|
||||
:title="`切换显示模式`"
|
||||
@click="showDisplayMode = !showDisplayMode"
|
||||
>
|
||||
<IconFluentTextAlignLeft16Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1 mr-4 justify-end" v-if="showDisplayMode">
|
||||
<BaseIcon title="逐行显示" @click="displayMode = 'inline'">
|
||||
<IconFluentTextPositionThrough20Regular />
|
||||
</BaseIcon>
|
||||
<BaseIcon :title="`开关释义显示`" @click="showTranslate = !showTranslate">
|
||||
<IconFluentTranslate16Regular v-if="showTranslate" />
|
||||
<IconFluentTranslateOff16Regular v-else />
|
||||
<BaseIcon title="单行显示" @click="displayMode = 'line'">
|
||||
<IconFluentTextAlignLeft16Regular />
|
||||
</BaseIcon>
|
||||
<BaseIcon title="对照显示" @click="displayMode = 'card'">
|
||||
<IconFluentAlignSpaceFitVertical20Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-2xl" v-if="selectArticle?.question?.text">
|
||||
Question: {{ selectArticle?.question?.text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="false">
|
||||
<!-- 原文-->
|
||||
<div class="text-2xl en-article-family space-y-5" v-if="selectArticle.text">
|
||||
<!-- <div class="break-words w-full" v-for="(t, i) in selectArticle.text.split('\n\n')">-->
|
||||
<!-- <span v-for="(w, j) in t.split('\n')" :class="`sentence-${i}-${j}`" :key="`${i}-${j}`">-->
|
||||
<!-- <!– <span v-for="(s,n) in w.split(' ')">{{s}}</span>–>-->
|
||||
<!-- {{ w }}-->
|
||||
<!-- </span>-->
|
||||
<!-- </div>-->
|
||||
<div v-for="t in selectArticle.text.split('\n\n')">{{ t }}</div>
|
||||
<div class="text-right italic">{{ selectArticle?.quote?.text }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 译文-->
|
||||
<template v-if="showTranslate">
|
||||
<div class="line"></div>
|
||||
<div class="text-xl line-height-normal space-y-5" v-if="selectArticle.textTranslate">
|
||||
<div class="mt-2" v-if="selectArticle?.question?.translate">
|
||||
<div class="mt-2 text-2xl" v-if="selectArticle?.question?.text">
|
||||
<div>Question: {{ selectArticle?.question?.text }}</div>
|
||||
<div
|
||||
class="text-xl color-translate-second"
|
||||
v-if="showTranslate && (displayMode !== 'card' || shouldShowInlineTranslation)"
|
||||
>
|
||||
问题: {{ selectArticle?.question?.translate }}
|
||||
</div>
|
||||
<!-- <div class="break-words w-full" v-for="(t, i) in selectArticle.textTranslate.split('\n\n')">-->
|
||||
<!-- <span v-for="(w, j) in t.split('\n')" :class="`translate-${i}-${j}`" :key="`${i}-${j}`">-->
|
||||
<!-- <!– <span v-for="(s,n) in w.split(' ')">{{s}}</span>–>-->
|
||||
<!-- {{ w }}-->
|
||||
<!-- </span>-->
|
||||
<!-- </div>-->
|
||||
<div v-for="t in selectArticle.textTranslate.split('\n\n')">{{ t }}</div>
|
||||
<div class="text-right italic">{{ selectArticle?.quote?.translate }}</div>
|
||||
</div>
|
||||
<Empty v-else />
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 打字式显示模式 -->
|
||||
<template v-if="true">
|
||||
<div class="article-content" :class="[showTranslate && 'tall']" ref="articleWrapperRef">
|
||||
<div
|
||||
class="article-content mt-6"
|
||||
:class="[showTranslate && displayMode !== 'card' && 'tall']"
|
||||
ref="articleWrapperRef"
|
||||
>
|
||||
<article>
|
||||
<div class="break-words w-full section" v-for="(t, i) in selectArticle.text.split('\n\n')">
|
||||
<span v-for="(w, j) in t.split('\n')" :class="`sentence-${i}-${j}`" :key="`${i}-${j}`"
|
||||
>{{ w }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-right italic" v-if="selectArticle?.quote?.text">
|
||||
{{ selectArticle?.quote?.text }}
|
||||
</div>
|
||||
</article>
|
||||
<div class="translate" v-show="showTranslate">
|
||||
<div
|
||||
class="break-words w-full section"
|
||||
v-for="(t, i) in selectArticle.textTranslate.split('\n\n')"
|
||||
>
|
||||
<div v-for="(w, j) in t.split('\n')" :class="`row translate-${i}-${j}`" :key="`${i}-${j}`">
|
||||
<span class="space"></span>
|
||||
<span>{{ w }}</span>
|
||||
<template v-for="(t, i) in selectArticle.text.split('\n\n')" :key="`para-${i}`">
|
||||
<div class="article-row w-full mb-10">
|
||||
<span
|
||||
:class="displayMode === 'line' && 'block'"
|
||||
v-for="(w, j) in t.split('\n')"
|
||||
:key="`${i}-${j}`"
|
||||
>
|
||||
<span
|
||||
v-for="(s, n) in w.split(' ').filter(Boolean)"
|
||||
:class="`inline-block word-${i}-${j}-${n}`"
|
||||
:key="`${i}-${j}-${n}`"
|
||||
><span>{{ s }}</span>
|
||||
<span class="space"></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 当 card 模式且段落数 > 1 时,在每个段落下显示对应译文 -->
|
||||
<div
|
||||
v-if="shouldShowInlineTranslation && showTranslate && selectArticle.textTranslate"
|
||||
class="trans-row text-xl color-translate-second -mt-7 mb-10"
|
||||
>
|
||||
<div v-if="selectArticle.textTranslate.split('\n\n')[i]">
|
||||
{{ selectArticle.textTranslate.split('\n\n')[i] }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="text-right italic">
|
||||
<div class="text-2xl" v-if="selectArticle?.quote?.text">{{ selectArticle?.quote?.text }}</div>
|
||||
<div
|
||||
class="trans-row text-xl color-translate-second"
|
||||
v-if="
|
||||
selectArticle?.quote?.translate &&
|
||||
showTranslate &&
|
||||
(displayMode !== 'card' || shouldShowInlineTranslation)
|
||||
"
|
||||
>
|
||||
{{ selectArticle?.quote?.translate }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right italic" v-if="selectArticle?.quote?.translate">
|
||||
{{ selectArticle?.quote?.translate }}
|
||||
</article>
|
||||
|
||||
<template v-if="showTranslate && selectArticle.textTranslate">
|
||||
<div class="translate color-translate-second" v-if="displayMode !== 'card'">
|
||||
<div
|
||||
class="break-words w-full section"
|
||||
v-for="(t, i) in selectArticle.textTranslate.split('\n\n')"
|
||||
>
|
||||
<div v-for="(w, j) in t.split('\n')" :class="`row translate-${i}-${j}`" :key="`${i}-${j}`">
|
||||
<span class="space"></span>
|
||||
<span>{{ w }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<!-- 当段落数 <= 1 时,保持原样在文章末尾显示译文 -->
|
||||
<template v-if="!shouldShowInlineTranslation">
|
||||
<div class="line my-10"></div>
|
||||
<div class="text-xl line-height-normal space-y-5">
|
||||
<div class="mt-2" v-if="selectArticle?.question?.translate">
|
||||
问题: {{ selectArticle?.question?.translate }}
|
||||
</div>
|
||||
<div class="trans-row" v-for="t in selectArticle.textTranslate.split('\n\n')">{{ t }}</div>
|
||||
<div class="trans-row text-right italic">{{ selectArticle?.quote?.translate }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 移动端显示翻译 -->
|
||||
<template v-if="isMob && showTranslate">
|
||||
<div
|
||||
class="sentence-translate-mobile"
|
||||
v-for="(paragraph, paraIndex) in parsedArticle"
|
||||
:key="`m-${paraIndex}`"
|
||||
>
|
||||
<div v-for="(sentence, sentIndex) in paragraph.sentences" :key="`${paraIndex}-${sentIndex}`">
|
||||
<div v-if="sentence.translate" class="mt-2">{{ sentence.translate }}</div>
|
||||
<template v-if="currentPractice.length">
|
||||
<div class="line my-10"></div>
|
||||
<div class="font-family text-base pr-2">
|
||||
<div class="text-2xl font-bold">学习记录</div>
|
||||
<div class="mt-1 mb-3">总学习时长:{{ msToHourMinute(total(currentPractice, 'spend')) }}</div>
|
||||
<div
|
||||
class="item border border-item border-solid mt-2 p-2 bg-[var(--bg-history)] rounded-md flex justify-between"
|
||||
v-for="i in currentPractice"
|
||||
>
|
||||
<span class="color-gray">{{ _dateFormat(i.startDate) }}</span>
|
||||
<span>{{ msToHourMinute(i.spend) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="currentPractice.length">
|
||||
<div class="line"></div>
|
||||
<div class="font-family text-base pr-2">
|
||||
<div class="text-2xl font-bold">学习记录</div>
|
||||
<div class="mt-1 mb-3">总学习时长:{{ msToHourMinute(total(currentPractice, 'spend')) }}</div>
|
||||
<div
|
||||
class="item border border-item border-solid mt-2 p-2 bg-[var(--bg-history)] rounded-md flex justify-between"
|
||||
v-for="i in currentPractice"
|
||||
>
|
||||
<span class="color-gray">{{ _dateFormat(i.startDate) }}</span>
|
||||
<span>{{ msToHourMinute(i.spend) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="border-t-1 border-t-gray-300 border-solid border-0 center gap-2 pt-4">
|
||||
<ArticleAudio
|
||||
:article="selectArticle"
|
||||
@update-speed="handleSpeedUpdate"
|
||||
@update-volume="handleVolumeUpdate"
|
||||
:autoplay="settingStore.articleAutoPlayNext && startPlay"
|
||||
@ended="next"
|
||||
/>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>结束后播放下一篇</span>
|
||||
<Switch v-model="settingStore.articleAutoPlayNext" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t-1 border-t-gray-300 border-solid border-0 center gap-2 pt-4">
|
||||
<ArticleAudio
|
||||
:article="selectArticle"
|
||||
@update-speed="handleSpeedUpdate"
|
||||
@update-volume="handleVolumeUpdate"
|
||||
:autoplay="settingStore.articleAutoPlayNext && startPlay"
|
||||
@ended="next"
|
||||
/>
|
||||
<div class="flex items-center gap-1">
|
||||
<span>结束后播放下一篇</span>
|
||||
<Switch v-model="settingStore.articleAutoPlayNext" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<Empty v-else />
|
||||
<Empty v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-0 dict-detail-card" v-else>
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" @click="isAdd ? $router.back() : (isEdit = false)" />
|
||||
<div class="dict-title absolute text-2xl text-align-center w-full">
|
||||
{{ runtimeStore.editDict.id ? '修改' : '创建' }}书籍
|
||||
<div class="card mb-0 dict-detail-card" v-else>
|
||||
<div class="dict-header flex justify-between items-center relative">
|
||||
<BackIcon class="dict-back z-2" @click="isAdd ? $router.back() : (isEdit = false)" />
|
||||
<div class="dict-title absolute text-2xl text-align-center w-full">
|
||||
{{ runtimeStore.editDict.id ? '修改' : '创建' }}书籍
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<EditBook :is-add="isAdd" :is-book="true" @close="formClose" @submit="isEdit = isAdd = false" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="center">
|
||||
<EditBook :is-add="isAdd" :is-book="true" @close="formClose" @submit="isEdit = isAdd = false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -517,60 +450,44 @@ $article-lh: 2.4;
|
||||
|
||||
.article-content {
|
||||
position: relative;
|
||||
color: var(--color-article);
|
||||
font-size: 1.6rem;
|
||||
|
||||
&.tall {
|
||||
article {
|
||||
line-height: $article-lh;
|
||||
color: var(--color-article);
|
||||
}
|
||||
}
|
||||
|
||||
article {
|
||||
.article-row {
|
||||
word-break: keep-all;
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
font-family: var(--en-article-family);
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 1.5rem;
|
||||
.trans-row {
|
||||
@apply cn-article-family font-bold;
|
||||
}
|
||||
|
||||
.sentence {
|
||||
transition: all 0.3s;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
article {
|
||||
@apply en-article-family;
|
||||
}
|
||||
|
||||
.translate {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: 1.2rem;
|
||||
@apply absolute top-0 left-0 h-full w-full text-xl pointer-events-none font-bold cn-article-family;
|
||||
line-height: $translate-lh;
|
||||
letter-spacing: 0.2rem;
|
||||
font-family: var(--zh-article-family);
|
||||
font-weight: bold;
|
||||
color: #818181;
|
||||
|
||||
.row {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
|
||||
.space {
|
||||
transition: all 0.3s;
|
||||
display: inline-block;
|
||||
}
|
||||
@apply absolute left-0 w-full opacity-0 transition-all duration-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.space {
|
||||
@apply inline-block w-2 transition-all duration-300;
|
||||
}
|
||||
|
||||
.sentence-translate-mobile {
|
||||
display: none;
|
||||
margin-top: 0.4rem;
|
||||
|
||||
@@ -10,14 +10,7 @@ import { useBaseStore } from '@/stores/base.ts'
|
||||
import { usePracticeStore } from '@/stores/practice.ts'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { getDefaultArticle, getDefaultWord } from '@/types/func.ts'
|
||||
import {
|
||||
Article,
|
||||
ArticleWord,
|
||||
PracticeArticleWordType,
|
||||
Sentence,
|
||||
ShortcutKey,
|
||||
Word,
|
||||
} from '@/types/types.ts'
|
||||
import { Article, ArticleWord, PracticeArticleWordType, Sentence, ShortcutKey, Word } from '@/types/types.ts'
|
||||
import { _dateFormat, _nextTick, isMobile, msToHourMinute, total } from '@/utils'
|
||||
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
|
||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||
@@ -96,22 +89,19 @@ const settingStore = useSettingStore()
|
||||
const statStore = usePracticeStore()
|
||||
const isMob = isMobile()
|
||||
|
||||
watch(
|
||||
[() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex],
|
||||
([a, b, c]) => {
|
||||
if (a !== 0 || b !== 0 || c !== 0) {
|
||||
setPracticeArticleCache({
|
||||
practiceData: {
|
||||
sectionIndex,
|
||||
sentenceIndex,
|
||||
wordIndex,
|
||||
},
|
||||
statStoreData: statStore.$state,
|
||||
})
|
||||
}
|
||||
checkCursorPosition(a, b, c)
|
||||
watch([() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex], ([a, b, c]) => {
|
||||
if (a !== 0 || b !== 0 || c !== 0) {
|
||||
setPracticeArticleCache({
|
||||
practiceData: {
|
||||
sectionIndex,
|
||||
sentenceIndex,
|
||||
wordIndex,
|
||||
},
|
||||
statStoreData: statStore.$state,
|
||||
})
|
||||
}
|
||||
)
|
||||
checkCursorPosition(a, b, c)
|
||||
})
|
||||
|
||||
// watch(() => props.article.id, init, {immediate: true})
|
||||
|
||||
@@ -286,11 +276,7 @@ const isNameWord = () => {
|
||||
let currentSection = props.article.sections[sectionIndex]
|
||||
let currentSentence = currentSection[sentenceIndex]
|
||||
let w: ArticleWord = currentSentence.words[wordIndex]
|
||||
return (
|
||||
w?.type === PracticeArticleWordType.Word &&
|
||||
namePatterns.length > 0 &&
|
||||
namePatterns.includes(normalize(w.word))
|
||||
)
|
||||
return w?.type === PracticeArticleWordType.Word && namePatterns.length > 0 && namePatterns.includes(normalize(w.word))
|
||||
}
|
||||
|
||||
let isTyping = false
|
||||
@@ -497,11 +483,7 @@ function del() {
|
||||
}
|
||||
}
|
||||
|
||||
function showSentence(
|
||||
i1: number = sectionIndex,
|
||||
i2: number = sentenceIndex,
|
||||
i3: number = wordIndex
|
||||
) {
|
||||
function showSentence(i1: number = sectionIndex, i2: number = sentenceIndex, i3: number = wordIndex) {
|
||||
hoverIndex = { sectionIndex: i1, sentenceIndex: i2, wordIndex: i3 }
|
||||
}
|
||||
|
||||
@@ -690,13 +672,16 @@ const currentPractice = inject('currentPractice', [])
|
||||
@beforeinput="handleMobileBeforeInput"
|
||||
@input="handleMobileInput"
|
||||
/>
|
||||
<header class="mb-4">
|
||||
<div class="title">
|
||||
<span class="font-family text-3xl">{{ store.sbook.lastLearnIndex + 1 }}. </span
|
||||
>{{ props.article?.title ?? '' }}
|
||||
<header class="pt-10 pb-6">
|
||||
<div class="text-center">
|
||||
<span class="text-3xl">{{ store.sbook.lastLearnIndex + 1 }}. </span>
|
||||
<span class="text-4xl">{{ props.article?.title??'' }}</span>
|
||||
<span class="ml-6 text-2xl" v-if="settingStore.translate">{{ props.article?.titleTranslate }}</span>
|
||||
</div>
|
||||
<div class="titleTranslate" v-if="settingStore.translate">
|
||||
{{ props.article.titleTranslate }}
|
||||
|
||||
<div class="mt-2 text-2xl" v-if="props.article?.question?.text">
|
||||
<div>Question: {{ props.article?.question?.text }}</div>
|
||||
<div class="text-xl color-translate-second" v-if="settingStore.translate">问题: {{ props.article?.question?.translate }}</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -742,11 +727,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
]"
|
||||
@click="playWordAudio(word.word)"
|
||||
>
|
||||
<TypingWord
|
||||
:word="word"
|
||||
:is-typing="true"
|
||||
v-if="isCurrent(indexI, indexJ, indexW) && !isSpace"
|
||||
/>
|
||||
<TypingWord :word="word" :is-typing="true" v-if="isCurrent(indexI, indexJ, indexW) && !isSpace" />
|
||||
<TypingWord :word="word" :is-typing="false" v-else />
|
||||
<span class="border-bottom" v-if="settingStore.dictation"></span>
|
||||
</span>
|
||||
@@ -758,10 +739,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
:is-shake="isCurrent(indexI, indexJ, indexW) && isSpace && wrong !== ''"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="sentence-translate-mobile"
|
||||
v-if="isMob && settingStore.translate && sentence.translate"
|
||||
>
|
||||
<span class="sentence-translate-mobile" v-if="isMob && settingStore.translate && sentence.translate">
|
||||
{{ sentence.translate }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -773,11 +751,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
class="row"
|
||||
:class="[
|
||||
`translate${indexI + '-' + indexJ}`,
|
||||
sectionIndex > indexI
|
||||
? 'wrote'
|
||||
: sectionIndex >= indexI && sentenceIndex > indexJ
|
||||
? 'wrote'
|
||||
: '',
|
||||
sectionIndex > indexI ? 'wrote' : sectionIndex >= indexI && sentenceIndex > indexJ ? 'wrote' : '',
|
||||
]"
|
||||
v-for="(item, indexJ) in v"
|
||||
>
|
||||
@@ -788,18 +762,12 @@ const currentPractice = inject('currentPractice', [])
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
class="cursor"
|
||||
v-if="!isEnd"
|
||||
:style="{ top: cursor.top + 'px', left: cursor.left + 'px' }"
|
||||
></div>
|
||||
<div class="cursor" v-if="!isEnd" :style="{ top: cursor.top + 'px', left: cursor.left + 'px' }"></div>
|
||||
</div>
|
||||
|
||||
<div class="options flex justify-center" v-if="isEnd">
|
||||
<BaseButton @click="emit('replay')">重新练习 </BaseButton>
|
||||
<BaseButton
|
||||
v-if="store.sbook.lastLearnIndex < store.sbook.articles.length - 1"
|
||||
@click="emit('next')"
|
||||
<BaseButton v-if="store.sbook.lastLearnIndex < store.sbook.articles.length - 1" @click="emit('next')"
|
||||
>下一篇
|
||||
</BaseButton>
|
||||
</div>
|
||||
@@ -813,9 +781,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
v-for="(item, i) in currentPractice"
|
||||
>
|
||||
<span :class="i === currentPractice.length - 1 ? 'color-red' : 'color-gray'"
|
||||
>{{ i === currentPractice.length - 1 ? '当前' : i + 1 }}. {{
|
||||
_dateFormat(item.startDate)
|
||||
}}</span
|
||||
>{{ i === currentPractice.length - 1 ? '当前' : i + 1 }}. {{ _dateFormat(item.startDate) }}</span
|
||||
>
|
||||
<span>{{ msToHourMinute(item.spend) }}</span>
|
||||
</div>
|
||||
@@ -826,12 +792,7 @@ const currentPractice = inject('currentPractice', [])
|
||||
<BaseButton @click="showQuestions = !showQuestions">显示题目</BaseButton>
|
||||
</div>
|
||||
<div class="toggle" v-if="showQuestions">
|
||||
<QuestionForm
|
||||
:questions="article.questions"
|
||||
:duration="300"
|
||||
:immediateFeedback="false"
|
||||
:randomize="true"
|
||||
/>
|
||||
<QuestionForm :questions="article.questions" :duration="300" :immediateFeedback="false" :randomize="true" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -851,26 +812,6 @@ $article-lh: 2.4;
|
||||
font-size: 1.6rem;
|
||||
margin-bottom: 10rem;
|
||||
|
||||
header {
|
||||
word-wrap: break-word;
|
||||
position: relative;
|
||||
padding-top: 3rem;
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
font-size: 2.2rem;
|
||||
font-family: var(--en-article-family);
|
||||
}
|
||||
|
||||
.titleTranslate {
|
||||
@extend .title;
|
||||
font-size: 1.2rem;
|
||||
margin-top: 0.5rem;
|
||||
font-family: var(--zh-article-family);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
let logList = [
|
||||
{
|
||||
date: '2026/01/06',
|
||||
content: '优化书籍详情页面',
|
||||
},
|
||||
{
|
||||
date: '2025/12/30',
|
||||
content: '移除“继续默写”选项',
|
||||
|
||||
@@ -2,17 +2,11 @@
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { getShortcutKey, useEventListener } from '@/hooks/event.ts'
|
||||
import {
|
||||
checkAndUpgradeSaveDict,
|
||||
checkAndUpgradeSaveSetting,
|
||||
cloneDeep,
|
||||
loadJsLib,
|
||||
sleep,
|
||||
} from '@/utils'
|
||||
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, sleep } from '@/utils'
|
||||
import { DefaultShortcutKeyMap } from '@/types/types.ts'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import { useBaseStore } from '@/stores/base.ts'
|
||||
import { APP_NAME, APP_VERSION, Host, LIB_JS_URL, LOCAL_FILE_KEY } from '@/config/env.ts'
|
||||
import { APP_NAME, APP_VERSION, AppEnv, Host, IS_DEV, LIB_JS_URL, LOCAL_FILE_KEY } from '@/config/env.ts'
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import { set } from 'idb-keyval'
|
||||
@@ -191,10 +185,7 @@ function importJson(str: string, notice: boolean = true) {
|
||||
try {
|
||||
let save: any = obj.val[PRACTICE_WORD_CACHE.key] || {}
|
||||
if (save.val && Object.keys(save.val).length > 0) {
|
||||
localStorage.setItem(
|
||||
PRACTICE_WORD_CACHE.key,
|
||||
JSON.stringify(obj.val[PRACTICE_WORD_CACHE.key])
|
||||
)
|
||||
localStorage.setItem(PRACTICE_WORD_CACHE.key, JSON.stringify(obj.val[PRACTICE_WORD_CACHE.key]))
|
||||
}
|
||||
} catch (e) {
|
||||
//todo 上报
|
||||
@@ -204,10 +195,7 @@ function importJson(str: string, notice: boolean = true) {
|
||||
try {
|
||||
let save: any = obj.val[PRACTICE_ARTICLE_CACHE.key] || {}
|
||||
if (save.val && Object.keys(save.val).length > 0) {
|
||||
localStorage.setItem(
|
||||
PRACTICE_ARTICLE_CACHE.key,
|
||||
JSON.stringify(obj.val[PRACTICE_ARTICLE_CACHE.key])
|
||||
)
|
||||
localStorage.setItem(PRACTICE_ARTICLE_CACHE.key, JSON.stringify(obj.val[PRACTICE_ARTICLE_CACHE.key]))
|
||||
}
|
||||
} catch (e) {
|
||||
//todo 上报
|
||||
@@ -230,9 +218,11 @@ function importJson(str: string, notice: boolean = true) {
|
||||
|
||||
let timer = -1
|
||||
async function beforeImport() {
|
||||
importLoading = true
|
||||
await exportData('已自动备份数据', 'TypeWords数据备份.zip')
|
||||
await sleep(1500)
|
||||
if (!IS_DEV) {
|
||||
importLoading = true
|
||||
await exportData('已自动备份数据', 'TypeWords数据备份.zip')
|
||||
await sleep(1500)
|
||||
}
|
||||
let d: HTMLDivElement = document.querySelector('#import')
|
||||
d.click()
|
||||
timer = setTimeout(() => (importLoading = false), 1000)
|
||||
@@ -398,15 +388,13 @@ function transferOk() {
|
||||
<div v-if="tabIndex === 4">
|
||||
<div>
|
||||
所有用户数据
|
||||
<b class="text-red">保存在本地浏览器中</b>。如果您需要在不同的设备、浏览器上使用
|
||||
{{ APP_NAME }}, 您需要手动进行数据导出和导入
|
||||
<b class="text-red">保存在本地浏览器中</b>。如果您需要在不同的设备、浏览器上使用 {{ APP_NAME }},
|
||||
您需要手动进行数据导出和导入
|
||||
</div>
|
||||
<BaseButton :loading="exportLoading" size="large" class="mt-3" @click="exportData()"
|
||||
>导出数据备份(ZIP)</BaseButton
|
||||
>
|
||||
<div class="text-gray text-sm mt-2">
|
||||
💾 导出的ZIP文件包含所有学习数据,可在其他设备上导入恢复
|
||||
</div>
|
||||
<div class="text-gray text-sm mt-2">💾 导出的ZIP文件包含所有学习数据,可在其他设备上导入恢复</div>
|
||||
|
||||
<div class="line mt-15 mb-3"></div>
|
||||
|
||||
@@ -415,9 +403,7 @@ function transferOk() {
|
||||
>当前所有数据,请谨慎操作。执行导入操作时,会先自动备份当前数据到您的电脑中,供您随时恢复
|
||||
</div>
|
||||
<div class="flex gap-space mt-3">
|
||||
<BaseButton size="large" @click="beforeImport" :loading="importLoading"
|
||||
>导入数据恢复</BaseButton
|
||||
>
|
||||
<BaseButton size="large" @click="beforeImport" :loading="importLoading">导入数据恢复</BaseButton>
|
||||
<input
|
||||
type="file"
|
||||
id="import"
|
||||
@@ -430,8 +416,7 @@ function transferOk() {
|
||||
<template v-if="isNewHost">
|
||||
<div class="line my-3"></div>
|
||||
<div>
|
||||
请注意,如果本地已有使用记录,请先备份当前数据,迁移数据后将<b class="text-red">
|
||||
完全覆盖 </b
|
||||
请注意,如果本地已有使用记录,请先备份当前数据,迁移数据后将<b class="text-red"> 完全覆盖 </b
|
||||
>当前所有数据,请谨慎操作。
|
||||
</div>
|
||||
<div class="flex gap-space mt-3">
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
_getAccomplishDate,
|
||||
_getDictDataByUrl,
|
||||
_nextTick,
|
||||
cloneDeep,
|
||||
isMobile,
|
||||
loadJsLib,
|
||||
resourceWrap,
|
||||
@@ -432,27 +431,27 @@ const systemPracticeText = $computed(() => {
|
||||
随机复习
|
||||
</BaseButton>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
v-if="settingStore.wordPracticeMode !== WordPracticeMode.IdentifyOnly"
|
||||
@click="startPractice(WordPracticeMode.IdentifyOnly, true)"
|
||||
>
|
||||
{{ WordPracticeModeNameMap[WordPracticeMode.IdentifyOnly] }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
v-if="settingStore.wordPracticeMode !== WordPracticeMode.ListenOnly"
|
||||
@click="startPractice(WordPracticeMode.ListenOnly, true)"
|
||||
>
|
||||
{{ WordPracticeModeNameMap[WordPracticeMode.ListenOnly] }}
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
v-if="settingStore.wordPracticeMode !== WordPracticeMode.DictationOnly"
|
||||
@click="startPractice(WordPracticeMode.DictationOnly, true)"
|
||||
>
|
||||
{{ WordPracticeModeNameMap[WordPracticeMode.DictationOnly] }}
|
||||
</BaseButton>
|
||||
<!-- <BaseButton-->
|
||||
<!-- class="w-full"-->
|
||||
<!-- v-if="settingStore.wordPracticeMode !== WordPracticeMode.IdentifyOnly"-->
|
||||
<!-- @click="startPractice(WordPracticeMode.IdentifyOnly, true)"-->
|
||||
<!-- >-->
|
||||
<!-- {{ WordPracticeModeNameMap[WordPracticeMode.IdentifyOnly] }}-->
|
||||
<!-- </BaseButton>-->
|
||||
<!-- <BaseButton-->
|
||||
<!-- class="w-full"-->
|
||||
<!-- v-if="settingStore.wordPracticeMode !== WordPracticeMode.ListenOnly"-->
|
||||
<!-- @click="startPractice(WordPracticeMode.ListenOnly, true)"-->
|
||||
<!-- >-->
|
||||
<!-- {{ WordPracticeModeNameMap[WordPracticeMode.ListenOnly] }}-->
|
||||
<!-- </BaseButton>-->
|
||||
<!-- <BaseButton-->
|
||||
<!-- class="w-full"-->
|
||||
<!-- v-if="settingStore.wordPracticeMode !== WordPracticeMode.DictationOnly"-->
|
||||
<!-- @click="startPractice(WordPracticeMode.DictationOnly, true)"-->
|
||||
<!-- >-->
|
||||
<!-- {{ WordPracticeModeNameMap[WordPracticeMode.DictationOnly] }}-->
|
||||
<!-- </BaseButton>-->
|
||||
</template>
|
||||
</OptionButton>
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ const emit = defineEmits<{
|
||||
>
|
||||
<RadioGroup :model-value="store.currentGroup">
|
||||
<div class="card-white">
|
||||
<div ref="scrollContainer" class="h-70 overflow-y-auto space-y-2">
|
||||
<div ref="scrollContainer" class="max-h-70 overflow-y-auto space-y-2">
|
||||
<div
|
||||
:ref="el => setItemRef(el as HTMLElement, value - 1)"
|
||||
class="break-keep flex bg-primary px-3 py-1 rounded-md hover:bg-card-active anim border border-solid border-item"
|
||||
|
||||
@@ -21,6 +21,10 @@ export default defineConfig({
|
||||
'border-item': 'border-[var(--color-item-border)]',
|
||||
'border-item-solid': 'border-1 border-solid border-[var(--color-item-border)]',
|
||||
card: 'rounded-xl p-4 mb-8 shadow-lg box-border relative bg-second',
|
||||
'color-translate-main':'color-[var(--color-translate-main)]',
|
||||
'color-translate-second':'color-[var(--color-translate-second)]',
|
||||
'en-article-family':'font-[var(--en-article-family)]',
|
||||
'cn-article-family':'font-[var(--zh-article-family)]',
|
||||
},
|
||||
presets: [presetWind3()],
|
||||
// 自定义断点
|
||||
|
||||
Reference in New Issue
Block a user