Merge branch 'refs/heads/dev'
# Conflicts: # src/components/BaseTable.vue
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -42,7 +42,6 @@ declare module 'vue' {
|
||||
IconFluentArrowBounce20Regular: typeof import('~icons/fluent/arrow-bounce20-regular')['default']
|
||||
IconFluentArrowCircleRight16Regular: typeof import('~icons/fluent/arrow-circle-right16-regular')['default']
|
||||
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
|
||||
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
|
||||
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
|
||||
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
|
||||
IconFluentArrowSort20Regular: typeof import('~icons/fluent/arrow-sort20-regular')['default']
|
||||
|
||||
9502
pnpm-lock.yaml
generated
9502
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ let list = defineModel('list')
|
||||
const props = withDefaults(defineProps<{
|
||||
loading?: boolean
|
||||
showToolbar?: boolean
|
||||
showPagination?: boolean
|
||||
exportLoading?: boolean
|
||||
importLoading?: boolean
|
||||
del?: Function
|
||||
@@ -28,6 +29,7 @@ const props = withDefaults(defineProps<{
|
||||
}>(), {
|
||||
loading: true,
|
||||
showToolbar: true,
|
||||
showPagination: true,
|
||||
exportLoading: false,
|
||||
importLoading: false,
|
||||
del: () => void 0,
|
||||
@@ -71,6 +73,7 @@ let currentList = $computed(() => {
|
||||
if (searchKey) {
|
||||
return list.value.filter(v => v.word.includes(searchKey))
|
||||
}
|
||||
if (!props.showPagination) return list.value
|
||||
return list.value.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
|
||||
})
|
||||
|
||||
@@ -171,134 +174,136 @@ defineRender(
|
||||
<span>{selectIds.length} / {list.value.length}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 relative">
|
||||
{
|
||||
selectIds.length ?
|
||||
<PopConfirm title="确认删除所有选中数据?"
|
||||
onConfirm={handleBatchDel}
|
||||
>
|
||||
<BaseIcon
|
||||
class="del"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
: null
|
||||
}
|
||||
<BaseIcon
|
||||
onClick={() => showImportDialog = true}
|
||||
title="导入">
|
||||
<IconSystemUiconsImport/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={() => emit('exportData')}
|
||||
title="导出">
|
||||
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={props.add}
|
||||
title="添加单词">
|
||||
<IconFluentAdd20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!currentList.length}
|
||||
title="改变顺序"
|
||||
onClick={() => showSortDialog = !showSortDialog}
|
||||
>
|
||||
<IconFluentArrowSort20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!currentList.length}
|
||||
onClick={() => showSearchInput = !showSearchInput}
|
||||
title="搜索">
|
||||
<IconFluentSearch20Regular/>
|
||||
</BaseIcon>
|
||||
<MiniDialog
|
||||
modelValue={showSortDialog}
|
||||
onUpdate:modelValue={e => showSortDialog = e}
|
||||
style="width: 8rem;"
|
||||
>
|
||||
<div class="mini-row-title">
|
||||
列表顺序设置
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" onClick={() => sort(Sort.reverse)}>翻转
|
||||
</BaseButton>
|
||||
<BaseButton size="small" onClick={() => sort(Sort.random)}>随机</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
props.loading ?
|
||||
<div class="h-full w-full center text-4xl">
|
||||
<IconEosIconsLoading color="gray"/>
|
||||
<div class="flex gap-2 relative">
|
||||
{
|
||||
selectIds.length ?
|
||||
<PopConfirm title="确认删除所有选中数据?"
|
||||
onConfirm={handleBatchDel}
|
||||
>
|
||||
<BaseIcon
|
||||
class="del"
|
||||
title="删除">
|
||||
<DeleteIcon/>
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
: null
|
||||
}
|
||||
<BaseIcon
|
||||
onClick={() => showImportDialog = true}
|
||||
title="导入">
|
||||
<IconSystemUiconsImport/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={() => emit('exportData')}
|
||||
title="导出">
|
||||
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={props.add}
|
||||
title="添加单词">
|
||||
<IconFluentAdd20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!currentList.length}
|
||||
title="改变顺序"
|
||||
onClick={() => showSortDialog = !showSortDialog}
|
||||
>
|
||||
<IconFluentArrowSort20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!currentList.length}
|
||||
onClick={() => showSearchInput = !showSearchInput}
|
||||
title="搜索">
|
||||
<IconFluentSearch20Regular/>
|
||||
</BaseIcon>
|
||||
<MiniDialog
|
||||
modelValue={showSortDialog}
|
||||
onUpdate:modelValue={e => showSortDialog = e}
|
||||
style="width: 8rem;"
|
||||
>
|
||||
<div class="mini-row-title">
|
||||
列表顺序设置
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" onClick={() => sort(Sort.reverse)}>翻转
|
||||
</BaseButton>
|
||||
<BaseButton size="small" onClick={() => sort(Sort.random)}>随机</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
: currentList.length ? (
|
||||
<>
|
||||
<div class="flex-1 overflow-auto"
|
||||
ref={e => listRef = e}>
|
||||
{currentList.map((item, index) => {
|
||||
return (
|
||||
<div class="list-item-wrapper"
|
||||
key={item.word}
|
||||
>
|
||||
{s.default({checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Pagination
|
||||
currentPage={pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={pageSize}
|
||||
onUpdate:page-size={(e) => pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="prev, pager, next"
|
||||
total={list.value.length}/>
|
||||
</div>
|
||||
</>
|
||||
) : <Empty/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<Dialog modelValue={showImportDialog}
|
||||
onUpdate:modelValue={closeImportDialog}
|
||||
title="导入教程"
|
||||
>
|
||||
<div className="w-100 p-4 pt-0">
|
||||
<div>请按照模板的格式来填写数据</div>
|
||||
<div class="color-red">单词项为必填,其他项可不填</div>
|
||||
<div>翻译:一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行</div>
|
||||
<div>例句:一行原文,一行译文;多个请换<span class="color-red">两</span>行</div>
|
||||
<div>短语:一行原文,一行译文;多个请换<span class="color-red">两</span>行</div>
|
||||
<div>同义词、同根词、词源:请前往官方字典,然后编辑其中某个单词,参考其格式</div>
|
||||
<div class="mt-6">
|
||||
模板下载地址:<a href="https://2study.top/libs/单词导入模板.xlsx">单词导入模板</a>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<BaseButton
|
||||
onClick={() => {
|
||||
let d: HTMLDivElement = document.querySelector('#upload-trigger')
|
||||
d.click()
|
||||
}}
|
||||
loading={props.importLoading}>导入</BaseButton>
|
||||
<input
|
||||
id="upload-trigger"
|
||||
type="file"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
onChange={e => emit('importData', e)}
|
||||
class="w-0 h-0 opacity-0"/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
{
|
||||
props.loading ?
|
||||
<div class="h-full w-full center text-4xl">
|
||||
<IconEosIconsLoading color="gray"/>
|
||||
</div>
|
||||
: currentList.length ? (
|
||||
<>
|
||||
<div class="flex-1 overflow-auto"
|
||||
ref={e => listRef = e}>
|
||||
{currentList.map((item, index) => {
|
||||
return (
|
||||
<div class="list-item-wrapper"
|
||||
key={item.word}
|
||||
>
|
||||
{s.default({checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{
|
||||
props.showPagination && <div class="flex justify-end">
|
||||
<Pagination
|
||||
currentPage={pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={pageSize}
|
||||
onUpdate:page-size={(e) => pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="prev, pager, next"
|
||||
total={list.value.length}/>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
) : <Empty/>
|
||||
}
|
||||
|
||||
<Dialog modelValue={showImportDialog}
|
||||
onUpdate:modelValue={closeImportDialog}
|
||||
title="导入教程"
|
||||
>
|
||||
<div className="w-100 p-4 pt-0">
|
||||
<div>请按照模板的格式来填写数据</div>
|
||||
<div class="color-red">单词项为必填,其他项可不填</div>
|
||||
<div>翻译:一行一个翻译,前面词性,后面内容(如n.取消);多个翻译请换行</div>
|
||||
<div>例句:一行原文,一行译文;多个请换<span class="color-red">两</span>行</div>
|
||||
<div>短语:一行原文,一行译文;多个请换<span class="color-red">两</span>行</div>
|
||||
<div>同义词、同根词、词源:请前往官方字典,然后编辑其中某个单词,参考其格式</div>
|
||||
<div class="mt-6">
|
||||
模板下载地址:<a href="https://2study.top/libs/单词导入模板.xlsx">单词导入模板</a>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<BaseButton
|
||||
onClick={() => {
|
||||
let d: HTMLDivElement = document.querySelector('#upload-trigger')
|
||||
d.click()
|
||||
}}
|
||||
loading={props.importLoading}>导入</BaseButton>
|
||||
<input
|
||||
id="upload-trigger"
|
||||
type="file"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
onChange={e => emit('importData', e)}
|
||||
class="w-0 h-0 opacity-0"/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -5,7 +5,7 @@ import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
withDefaults(defineProps<{
|
||||
item: Word,
|
||||
showTranslate?: boolean
|
||||
showWord?: boolean
|
||||
@@ -37,8 +37,8 @@ const playWordAudio = usePlayWordAudio()
|
||||
<div class="item-sub-title flex flex-col gap-2" v-if="item.trans.length && showTranslate">
|
||||
<div v-for="v in item.trans">
|
||||
<Tooltip
|
||||
v-if="v.cn.length > 30 && showTransPop"
|
||||
:title="v.pos + ' ' + v.cn"
|
||||
v-if="v.cn.length > 30 && showTransPop"
|
||||
:title="v.pos + ' ' + v.cn"
|
||||
>
|
||||
<span>{{ v.pos + ' ' + v.cn.slice(0, 30) + '...' }}</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -126,7 +126,6 @@ const vFocus = {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background: var(--color-input-bg);
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
@@ -173,6 +172,7 @@ const vFocus = {
|
||||
transition: all .3s;
|
||||
height: 1.5rem;
|
||||
color: var(--color-input-color);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
showText: true,
|
||||
textInside: false,
|
||||
strokeWidth: 6,
|
||||
color: '#409eff',
|
||||
color: '#93ADE3',
|
||||
format: (percentage) => `${percentage}%`,
|
||||
});
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export const SoundFileOptions = [
|
||||
]
|
||||
export const APP_VERSION = {
|
||||
key: 'type-words-app-version',
|
||||
version: 1
|
||||
version: 2
|
||||
}
|
||||
export const SAVE_DICT_KEY = {
|
||||
key: 'typing-word-dict',
|
||||
@@ -51,7 +51,7 @@ export const SAVE_DICT_KEY = {
|
||||
}
|
||||
export const SAVE_SETTING_KEY = {
|
||||
key: 'typing-word-setting',
|
||||
version: 15
|
||||
version: 16
|
||||
}
|
||||
export const EXPORT_DATA_KEY = {
|
||||
key: 'typing-word-export',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Article, TaskWords, Word } from "@/types/types.ts";
|
||||
import {Article, TaskWords, Word, WordPracticeMode} from "@/types/types.ts";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { getDefaultWord } from "@/types/func.ts";
|
||||
@@ -144,7 +144,7 @@ export function getCurrentStudyWord(): TaskWords {
|
||||
}
|
||||
|
||||
//如果是自由模式,那么统统设置到new字段里面去
|
||||
if (settingStore.wordPracticeMode === 1) {
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
|
||||
data.new = data.new.length ? data.new : data.review
|
||||
data.review = []
|
||||
return data
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { getAudioFileUrl, usePlayAudio } from "@/hooks/sound.ts";
|
||||
import { getShortcutKey, useEventListener } from "@/hooks/event.ts";
|
||||
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, shakeCommonDict } from "@/utils";
|
||||
import { DefaultShortcutKeyMap, ShortcutKey } from "@/types/types.ts";
|
||||
import {DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode} from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
@@ -151,7 +151,9 @@ function getShortcutKeyName(key: string): string {
|
||||
'ToggleConciseMode': '切换简洁模式',
|
||||
'TogglePanel': '切换面板',
|
||||
'RandomWrite': '随机默写',
|
||||
'NextRandomWrite': '继续随机默写'
|
||||
'NextRandomWrite': '继续随机默写',
|
||||
'KnowWord': '认识单词',
|
||||
'UnknownWord': '不认识单词',
|
||||
}
|
||||
|
||||
return shortcutKeyNameMap[key] || key
|
||||
@@ -487,8 +489,8 @@ function importOldData() {
|
||||
<div v-if="tabIndex === 1">
|
||||
<SettingItem title="练习模式">
|
||||
<RadioGroup v-model="settingStore.wordPracticeMode" class="flex-col gap-0!">
|
||||
<Radio :value="0" label="智能模式,系统自动计算复习单词与默写单词"/>
|
||||
<Radio :value="1" label="自由模式,系统不强制复习与默写"/>
|
||||
<Radio :value="WordPracticeMode.System" label="智能模式,系统自动计算复习单词与默写单词"/>
|
||||
<Radio :value="WordPracticeMode.Free" label="自由模式,系统不强制复习与默写"/>
|
||||
</RadioGroup>
|
||||
</SettingItem>
|
||||
|
||||
@@ -552,7 +554,7 @@ function importOldData() {
|
||||
<div class="line"></div>
|
||||
<SettingItem mainTitle="自动切换"/>
|
||||
<SettingItem title="自动切换下一个单词"
|
||||
desc="未开启自动切换时,当输入完成后请使用空格键切换下一个"
|
||||
desc="未开启自动切换时,当输入完成后请使用 **空格键** 切换下一个"
|
||||
>
|
||||
<Switch v-model="settingStore.autoNextWord"/>
|
||||
</SettingItem>
|
||||
@@ -680,8 +682,61 @@ function importOldData() {
|
||||
<div class="item p-2">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<span>2025/9/14:</span>
|
||||
<span>完善文章编辑、导入、导出等功能</span>
|
||||
<div>更新日期:2025/10/26</div>
|
||||
<div>更新内容:进一步完善单词练习,解决复习数量太多的问题</div>
|
||||
</div>
|
||||
<div class="text-base mt-1">
|
||||
<ol>
|
||||
<li>
|
||||
<div class="title"><b>智能模式优化</b></div>
|
||||
<div class="desc">练习时新增四种练习模式:学习、复习、听写、默写。</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title"><b>学习模式</b></div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>仅在练习新词时出现。</li>
|
||||
<li>采用「跟写 / 拼写」方式进行学习。</li>
|
||||
<li>每 7 个单词会 <b>强制进行听写</b>,解决原来“一次练太多,听写时已忘记”的问题。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title"><b>复习模式(新增)</b></div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>仅在复习已学单词时出现。</li>
|
||||
<li>不再强制拼写,提供「我认识」与「不认识」选项。</li>
|
||||
<li>选择「我认识」后,该单词在后续听写或默写中将不再出现,<b>显著减少复习数量</b>。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title"><b>听写模式</b></div>
|
||||
<div class="desc">原有逻辑保持不变。</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="title"><b>默写模式(新增)</b></div>
|
||||
<div class="desc">
|
||||
<ul>
|
||||
<li>仅显示释义,不自动发音,不显示单词长度。</li>
|
||||
<li>适合强化拼写记忆的场景。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<b>说明:</b>
|
||||
<div>本次更新重点解决了“复习单词数量过多、效率偏低”的问题。</div>
|
||||
<div>通过引入「复习」与「默写」两种模式,使复习流程更加灵活、高效。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<div class="item p-2">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>更新日期:2025/9/14</div>
|
||||
<div>更新内容:完善文章编辑、导入、导出等功能</div>
|
||||
</div>
|
||||
<div class="text-base mt-1">
|
||||
<div>1、文章的音频管理功能,目前已可添加音频、设置句子与音频的对应位置</div>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { onMounted, provide, watch } from "vue";
|
||||
import {onMounted, provide, ref, watch} from "vue";
|
||||
|
||||
import Statistics from "@/pages/word/Statistics.vue";
|
||||
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { Dict, PracticeData, ShortcutKey, TaskWords, Word } from "@/types/types.ts";
|
||||
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts";
|
||||
import {emitter, EventKey, useEvents} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {Dict, PracticeData, WordPracticeType, ShortcutKey, TaskWords, Word, WordPracticeMode} from "@/types/types.ts";
|
||||
import {useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener} from "@/hooks/event.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import { getCurrentStudyWord, useWordOptions } from "@/hooks/dict.ts";
|
||||
import { _getDictDataByUrl, cloneDeep, resourceWrap, shuffle } from "@/utils";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {getCurrentStudyWord, useWordOptions} from "@/hooks/dict.ts";
|
||||
import {_getDictDataByUrl, cloneDeep, resourceWrap, shuffle} from "@/utils";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import Footer from "@/pages/word/components/Footer.vue";
|
||||
import Panel from "@/components/Panel.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
@@ -19,14 +19,15 @@ import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import WordList from "@/components/list/WordList.vue";
|
||||
import TypeWord from "@/pages/word/components/TypeWord.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import { getDefaultDict, getDefaultWord } from "@/types/func.ts";
|
||||
import {getDefaultDict, getDefaultWord} from "@/types/func.ts";
|
||||
import ConflictNotice from "@/components/ConflictNotice.vue";
|
||||
import PracticeLayout from "@/components/PracticeLayout.vue";
|
||||
|
||||
import { DICT_LIST, PracticeSaveWordKey } from "@/config/env.ts";
|
||||
import {DICT_LIST, PracticeSaveWordKey} from "@/config/env.ts";
|
||||
import {ToastInstance} from "@/components/base/toast/type.ts";
|
||||
|
||||
const {
|
||||
isWordCollect,
|
||||
@@ -48,14 +49,21 @@ let loading = $ref(false)
|
||||
let taskWords = $ref<TaskWords>({
|
||||
new: [],
|
||||
review: [],
|
||||
write: []
|
||||
write: [],
|
||||
})
|
||||
|
||||
let data = $ref<PracticeData>({
|
||||
index: 0,
|
||||
words: [],
|
||||
wrongWords: [],
|
||||
excludeWords: [],
|
||||
})
|
||||
let isTypingWrongWord = ref(false)
|
||||
|
||||
let practiceMode = ref(WordPracticeType.FollowWrite)
|
||||
provide('isTypingWrongWord', isTypingWrongWord)
|
||||
provide('practiceData', data)
|
||||
provide('practiceMode', practiceMode)
|
||||
|
||||
async function loadDict() {
|
||||
// console.log('load好了开始加载')
|
||||
@@ -89,7 +97,6 @@ watch(() => store.load, (n) => {
|
||||
if (n && loading) loadDict()
|
||||
}, {immediate: true})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
//如果是从单词学习主页过来的,就直接使用;否则等待加载
|
||||
if (runtimeStore.routeData) {
|
||||
@@ -120,26 +127,27 @@ function initData(initVal: TaskWords, init: boolean = false) {
|
||||
taskWords = initVal
|
||||
if (taskWords.new.length === 0) {
|
||||
if (taskWords.review.length) {
|
||||
settingStore.dictation = false
|
||||
statStore.step = 2
|
||||
settingStore.wordPracticeType = WordPracticeType.Identify
|
||||
statStore.step = 3
|
||||
data.words = taskWords.review
|
||||
} else {
|
||||
if (taskWords.write.length) {
|
||||
settingStore.dictation = true
|
||||
settingStore.wordPracticeType = WordPracticeType.Identify
|
||||
data.words = taskWords.write
|
||||
statStore.step = 4
|
||||
statStore.step = 6
|
||||
} else {
|
||||
Toast.warning('没有可学习的单词!')
|
||||
router.push('/word')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
settingStore.dictation = false
|
||||
settingStore.wordPracticeType = WordPracticeType.FollowWrite
|
||||
data.words = taskWords.new
|
||||
statStore.step = 0
|
||||
}
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
data.excludeWords = []
|
||||
allWrongWords.clear()
|
||||
statStore.startDate = Date.now()
|
||||
statStore.inputWordNumber = 0
|
||||
@@ -149,12 +157,11 @@ function initData(initVal: TaskWords, init: boolean = false) {
|
||||
statStore.reviewWordNumber = taskWords.review.length
|
||||
statStore.writeWordNumber = taskWords.write.length
|
||||
statStore.index = 0
|
||||
isTypingWrongWord.value = false
|
||||
}
|
||||
}
|
||||
|
||||
provide('practiceData', data)
|
||||
|
||||
const word = $computed(() => {
|
||||
const word = $computed<Word>(() => {
|
||||
return data.words[data.index] ?? getDefaultWord()
|
||||
})
|
||||
const prevWord: Word = $computed(() => {
|
||||
@@ -164,94 +171,176 @@ const nextWord: Word = $computed(() => {
|
||||
return data.words?.[data.index + 1] ?? undefined
|
||||
})
|
||||
|
||||
function next(isTyping: boolean = true) {
|
||||
// showStatDialog = true
|
||||
// return
|
||||
if (data.index === data.words.length - 1) {
|
||||
if (data.wrongWords.length) {
|
||||
console.log('当前学完了,但还有错词')
|
||||
data.words = shuffle(cloneDeep(data.wrongWords))
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
watch(() => settingStore.wordPracticeType, (n) => {
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.Free) return
|
||||
switch (n) {
|
||||
case WordPracticeType.Spell:
|
||||
case WordPracticeType.Dictation:
|
||||
case WordPracticeType.Listen:
|
||||
settingStore.dictation = true;
|
||||
settingStore.translate = false;
|
||||
break
|
||||
case WordPracticeType.FollowWrite:
|
||||
settingStore.dictation = false;
|
||||
settingStore.translate = true;
|
||||
break
|
||||
case WordPracticeType.Identify:
|
||||
settingStore.dictation = false;
|
||||
settingStore.translate = false;
|
||||
break
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
function wordLoop() {
|
||||
// return data.index++
|
||||
let d = Math.floor(data.index / 6) - 1
|
||||
if (data.index > 0 && data.index % 6 === (d < 0 ? 0 : d)) {
|
||||
if (settingStore.wordPracticeType === WordPracticeType.FollowWrite) {
|
||||
settingStore.wordPracticeType = WordPracticeType.Spell
|
||||
data.index -= 6
|
||||
} else {
|
||||
console.log('当前学完了,没错词', statStore.total, statStore.step, data.index)
|
||||
if (isTyping) statStore.inputWordNumber++
|
||||
|
||||
//学完了
|
||||
if (statStore.step === 4) {
|
||||
statStore.spend = Date.now() - statStore.startDate
|
||||
console.log('全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
return;
|
||||
// emit('complete', {})
|
||||
}
|
||||
|
||||
//开始默认所有单词
|
||||
if (statStore.step === 3) {
|
||||
statStore.step++
|
||||
if (taskWords.write.length) {
|
||||
console.log('开始默认所有单词')
|
||||
settingStore.dictation = true
|
||||
data.words = shuffle(taskWords.write)
|
||||
data.index = 0
|
||||
} else {
|
||||
console.log('开始默认所有单词-无单词略过')
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
//开始默写昨日
|
||||
if (statStore.step === 2) {
|
||||
statStore.step++
|
||||
if (taskWords.review.length) {
|
||||
console.log('开始默写昨日')
|
||||
settingStore.dictation = true
|
||||
data.words = shuffle(taskWords.review)
|
||||
data.index = 0
|
||||
} else {
|
||||
console.log('开始默写昨日-无单词略过')
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
//开始复习昨日
|
||||
if (statStore.step === 1) {
|
||||
statStore.step++
|
||||
if (taskWords.review.length) {
|
||||
console.log('开始复习昨日')
|
||||
settingStore.dictation = false
|
||||
data.words = shuffle(taskWords.review)
|
||||
data.index = 0
|
||||
} else {
|
||||
console.log('开始复习昨日-无单词略过')
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
//开始默写新词
|
||||
if (statStore.step === 0) {
|
||||
if (settingStore.wordPracticeMode === 1) {
|
||||
console.log('自由模式,全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
return
|
||||
}
|
||||
statStore.step++
|
||||
console.log('开始默写新词')
|
||||
settingStore.dictation = true
|
||||
data.words = shuffle(taskWords.new)
|
||||
data.index = 0
|
||||
}
|
||||
settingStore.wordPracticeType = WordPracticeType.FollowWrite
|
||||
data.index++
|
||||
}
|
||||
} else {
|
||||
data.index++
|
||||
isTyping && statStore.inputWordNumber++
|
||||
// console.log('这个词完了')
|
||||
}
|
||||
}
|
||||
|
||||
function goNextStep(originList, mode, msg) {
|
||||
//每次都判断,因为每次都可能新增已掌握的单词
|
||||
let list = originList.filter(v => (!data.excludeWords.includes(v.word)))
|
||||
console.log(msg)
|
||||
if (list.length) {
|
||||
if (toastInstance) toastInstance.close()
|
||||
toastInstance = Toast.info('输入完成后按空格键切换下一个', {duration: 5000})
|
||||
data.words = list
|
||||
settingStore.wordPracticeType = mode
|
||||
data.index = 0
|
||||
statStore.step++
|
||||
} else {
|
||||
console.log(msg + ':无单词略过')
|
||||
statStore.step += 3
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
let toastInstance: ToastInstance = null
|
||||
|
||||
async function next(isTyping: boolean = true) {
|
||||
if (isTyping) {
|
||||
statStore.inputWordNumber++
|
||||
}
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
|
||||
if (data.index === data.words.length - 1) {
|
||||
console.log('自由模式,全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
} else {
|
||||
data.index++
|
||||
}
|
||||
} else {
|
||||
if (data.index === data.words.length - 1) {
|
||||
if (statStore.step === 0 || isTypingWrongWord.value) {
|
||||
if (settingStore.wordPracticeType !== WordPracticeType.Spell) {
|
||||
let i = data.index
|
||||
i--
|
||||
let d = Math.floor(i / 6) - 1
|
||||
while (i % 6 !== (d < 0 ? 0 : d)) {
|
||||
i--
|
||||
d = Math.floor(i / 6) - 1
|
||||
}
|
||||
console.log('i', i)
|
||||
if (i <= 0) i = -1
|
||||
if (i + 1 == data.index) {
|
||||
data.index = 0
|
||||
}
|
||||
data.index = i + 1
|
||||
emitter.emit(EventKey.resetWord)
|
||||
settingStore.wordPracticeType = WordPracticeType.Spell
|
||||
return
|
||||
}
|
||||
}
|
||||
data.wrongWords = data.wrongWords.filter(v => (!data.excludeWords.includes(v.word)))
|
||||
if (data.wrongWords.length) {
|
||||
isTypingWrongWord.value = true
|
||||
settingStore.wordPracticeType = WordPracticeType.FollowWrite
|
||||
console.log('当前学完了,但还有错词')
|
||||
data.words = shuffle(cloneDeep(data.wrongWords))
|
||||
data.index = 0
|
||||
data.wrongWords = []
|
||||
} else {
|
||||
isTypingWrongWord.value = false
|
||||
console.log('当前学完了,没错词', statStore.total, statStore.step, data.index)
|
||||
//学完了
|
||||
if (statStore.step === 8) {
|
||||
statStore.spend = Date.now() - statStore.startDate
|
||||
console.log('全完学完了')
|
||||
showStatDialog = true
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
return;
|
||||
}
|
||||
|
||||
//开始默写之前
|
||||
if (statStore.step === 7) {
|
||||
return goNextStep(shuffle(taskWords.write), WordPracticeType.Dictation, '开始默写之前')
|
||||
}
|
||||
|
||||
//开始听写之前
|
||||
if (statStore.step === 6) {
|
||||
return goNextStep(shuffle(taskWords.write), WordPracticeType.Listen, '开始听写之前')
|
||||
}
|
||||
|
||||
//开始复写之前
|
||||
if (statStore.step === 5) {
|
||||
return goNextStep(taskWords.write, WordPracticeType.Identify, '开始复写之前')
|
||||
}
|
||||
|
||||
//开始默写上次
|
||||
if (statStore.step === 4) {
|
||||
return goNextStep(shuffle(taskWords.review), WordPracticeType.Dictation, '开始默写上次')
|
||||
}
|
||||
|
||||
//开始听写上次
|
||||
if (statStore.step === 3) {
|
||||
return goNextStep(shuffle(taskWords.review), WordPracticeType.Listen, '开始听写上次')
|
||||
}
|
||||
|
||||
//开始复写昨日
|
||||
if (statStore.step === 2) {
|
||||
return goNextStep(taskWords.review, WordPracticeType.Identify, '开始复写昨日')
|
||||
}
|
||||
|
||||
//开始默写新词
|
||||
if (statStore.step === 1) {
|
||||
return goNextStep(shuffle(taskWords.new), WordPracticeType.Dictation, '开始默写新词')
|
||||
}
|
||||
|
||||
//开始听写新词
|
||||
if (statStore.step === 0) {
|
||||
return goNextStep(shuffle(taskWords.new), WordPracticeType.Listen, '开始听写新词')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (statStore.step === 0) {
|
||||
wordLoop()
|
||||
} else {
|
||||
if (isTypingWrongWord.value) wordLoop()
|
||||
else data.index++
|
||||
}
|
||||
}
|
||||
}
|
||||
savePracticeData()
|
||||
}
|
||||
|
||||
function onWordKnow() {
|
||||
//标记模式时,用户认识的单词加入到排除里面,后续不再复习
|
||||
let rIndex = data.excludeWords.findIndex(v => v === word.word)
|
||||
if (rIndex < 0) {
|
||||
data.excludeWords.push(word.word)
|
||||
}
|
||||
}
|
||||
|
||||
function onTypeWrong() {
|
||||
let temp = word.word.toLowerCase()
|
||||
if (!allWrongWords.has(word.word.toLowerCase())) {
|
||||
@@ -299,7 +388,7 @@ useOnKeyboardEventListener(onKeyDown, onKeyUp)
|
||||
|
||||
function repeat() {
|
||||
console.log('重学一遍')
|
||||
if (settingStore.wordPracticeMode === 0) settingStore.dictation = false
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
|
||||
if (store.sdict.lastLearnIndex === 0 && store.sdict.complete) {
|
||||
//如果是刚刚完成,那么学习进度要从length减回去,因为lastLearnIndex为0了,同时改complete为false
|
||||
store.sdict.lastLearnIndex = store.sdict.length - statStore.newWordNumber
|
||||
@@ -331,6 +420,7 @@ function skip(e: KeyboardEvent) {
|
||||
}
|
||||
|
||||
function show(e: KeyboardEvent) {
|
||||
if (![WordPracticeType.FollowWrite].includes(settingStore.wordPracticeType)) onTypeWrong()
|
||||
typingRef.showWord()
|
||||
}
|
||||
|
||||
@@ -344,12 +434,16 @@ function play() {
|
||||
|
||||
function toggleWordSimpleWrapper() {
|
||||
if (!isWordSimple(word)) {
|
||||
toggleWordSimple(word)
|
||||
//延迟一下,不知道为什么不延迟会导致当前条目不自动定位到列表中间
|
||||
setTimeout(() => next(false))
|
||||
} else {
|
||||
toggleWordSimple(word)
|
||||
}
|
||||
let rIndex = data.excludeWords.findIndex(v => v === word.word)
|
||||
if (rIndex > -1) {
|
||||
data.excludeWords.splice(rIndex, 1)
|
||||
} else {
|
||||
data.excludeWords.push(word.word)
|
||||
}
|
||||
toggleWordSimple(word)
|
||||
}
|
||||
|
||||
function toggleTranslate() {
|
||||
@@ -370,7 +464,7 @@ function togglePanel() {
|
||||
}
|
||||
|
||||
function continueStudy() {
|
||||
if (settingStore.wordPracticeMode === 0) settingStore.dictation = false
|
||||
if (settingStore.wordPracticeMode === WordPracticeMode.System) settingStore.dictation = false
|
||||
//这里判断是否显示结算弹框,如果显示了结算弹框的话,就不用加进度了
|
||||
if (!showStatDialog) {
|
||||
console.log('没学完,强行跳过')
|
||||
@@ -388,6 +482,7 @@ function randomWrite() {
|
||||
data.index = 0
|
||||
settingStore.dictation = true
|
||||
}
|
||||
|
||||
function nextRandomWrite() {
|
||||
console.log('继续随机默写')
|
||||
initData(getCurrentStudyWord())
|
||||
@@ -456,6 +551,7 @@ useEvents([
|
||||
:word="word"
|
||||
@wrong="onTypeWrong"
|
||||
@complete="next"
|
||||
@know="onWordKnow"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -89,8 +89,8 @@ useEvents([
|
||||
])
|
||||
|
||||
function options(emitType: string) {
|
||||
close()
|
||||
emitter.emit(EventKey[emitType])
|
||||
close()
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -111,11 +111,11 @@ function options(emitType: string) {
|
||||
<div class="text-4xl font-bold">{{ statStore.newWordNumber }}</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">复习数</div>
|
||||
<div class="text-sm color-gray">复习上次</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.reviewWordNumber }}</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-sm color-gray">默写数</div>
|
||||
<div class="text-sm color-gray">复习之前</div>
|
||||
<div class="text-4xl font-bold">{{ statStore.writeWordNumber }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "vue-router";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import { _getAccomplishDate, _getDictDataByUrl, resourceWrap, useNav } from "@/utils";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { DictResource } from "@/types/types.ts";
|
||||
import {DictResource, WordPracticeMode} from "@/types/types.ts";
|
||||
import { watch } from "vue";
|
||||
import { getCurrentStudyWord } from "@/hooks/dict.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
@@ -22,6 +22,7 @@ import CollectNotice from "@/components/CollectNotice.vue";
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { CAN_REQUEST, DICT_LIST, PracticeSaveWordKey } from "@/config/env.ts";
|
||||
import { myDictList } from "@/apis";
|
||||
import PracticeWordListDialog from "@/pages/word/components/PracticeWordListDialog.vue";
|
||||
|
||||
|
||||
const store = useBaseStore()
|
||||
@@ -93,6 +94,7 @@ function startPractice() {
|
||||
|
||||
let showPracticeSettingDialog = $ref(false)
|
||||
let showChangeLastPracticeIndexDialog = $ref(false)
|
||||
let showPracticeWordListDialog = $ref(false)
|
||||
|
||||
async function goDictDetail(val: DictResource) {
|
||||
runtimeStore.editDict = getDefaultDict(val)
|
||||
@@ -182,7 +184,6 @@ const {
|
||||
<BaseIcon title="切换词典"
|
||||
class="ml-4"
|
||||
@click="router.push('/dict-list')"
|
||||
|
||||
>
|
||||
<IconFluentArrowSort20Regular v-if="store.sdict.name"/>
|
||||
<IconFluentAdd20Filled v-else/>
|
||||
@@ -211,21 +212,24 @@ const {
|
||||
</div>
|
||||
|
||||
<div class="w-3/10 flex flex-col justify-evenly">
|
||||
<div class="center text-xl">{{ isSaveData ? '上次学习任务' : '今日任务' }}</div>
|
||||
<div class="center gap-2">
|
||||
<span class="text-xl">{{ isSaveData ? '上次学习任务' : '今日任务' }}</span>
|
||||
<span class="color-blue cursor-pointer" @click="showPracticeWordListDialog = true">词表</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-4xl font-bold">{{ currentStudy.new.length }}</div>
|
||||
<div class="text">新词</div>
|
||||
</div>
|
||||
<template v-if="settingStore.wordPracticeMode === 0">
|
||||
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-4xl font-bold">{{ currentStudy.review.length }}</div>
|
||||
<div class="text">复习</div>
|
||||
<div class="text">复习上次</div>
|
||||
</div>
|
||||
<div class="flex-1 flex flex-col items-center">
|
||||
<div class="text-4xl font-bold">{{ currentStudy.write.length }}
|
||||
</div>
|
||||
<div class="text">默写</div>
|
||||
<div class="text">复习之前</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -234,8 +238,8 @@ const {
|
||||
<div class="flex flex-col items-end justify-around ">
|
||||
<div class="flex gap-1 items-center">
|
||||
每日目标
|
||||
<div style="color:#ac6ed1;" @click="check(()=>showPracticeSettingDialog = true)"
|
||||
class="bg-third px-2 h-10 flex center text-2xl rounded cursor-pointer">
|
||||
<div style="color:#ac6ed1;"
|
||||
class="bg-third px-2 h-10 flex center text-2xl rounded">
|
||||
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
|
||||
</div>
|
||||
个单词
|
||||
@@ -308,6 +312,11 @@ const {
|
||||
@ok="saveLastPracticeIndex"
|
||||
/>
|
||||
|
||||
<PracticeWordListDialog
|
||||
:data="currentStudy"
|
||||
v-model="showPracticeWordListDialog"
|
||||
/>
|
||||
|
||||
<CollectNotice/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { inject, watch } from "vue"
|
||||
import { inject, Ref, watch } from "vue"
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { ShortcutKey, PracticeData } from "@/types/types.ts";
|
||||
import { PracticeData, WordPracticeType, ShortcutKey } from "@/types/types.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import Progress from '@/components/base/Progress.vue'
|
||||
@@ -25,28 +25,42 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
let practiceData = inject<PracticeData>('practiceData')
|
||||
let isTypingWrongWord = inject<Ref<boolean>>('isTypingWrongWord')
|
||||
|
||||
function format(val: number, suffix: string = '', check: number = -1) {
|
||||
return val === check ? '-' : (val + suffix)
|
||||
}
|
||||
|
||||
const status = $computed(() => {
|
||||
let str = '正在'
|
||||
if (isTypingWrongWord.value) return '复习错词'
|
||||
let str = ''
|
||||
switch (statisticsStore.step) {
|
||||
case 0:
|
||||
str += `学习新词`
|
||||
break
|
||||
case 1:
|
||||
str += `默写新词`
|
||||
str += `听写新词`
|
||||
break
|
||||
case 2:
|
||||
str += `复习上次`
|
||||
str += `默写新词`
|
||||
break
|
||||
case 3:
|
||||
str += `默写上次`
|
||||
str += `复习上次学习`
|
||||
break
|
||||
case 4:
|
||||
str += '默写之前'
|
||||
str += '听写上次学习'
|
||||
break
|
||||
case 5:
|
||||
str += '默写上次学习'
|
||||
break
|
||||
case 6:
|
||||
str += '复习之前学习'
|
||||
break
|
||||
case 7:
|
||||
str += '听写之前学习'
|
||||
break
|
||||
case 8:
|
||||
str += '默写之前学习'
|
||||
break
|
||||
}
|
||||
return str
|
||||
@@ -77,7 +91,7 @@ const progress = $computed(() => {
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="stat">
|
||||
<div class="row">
|
||||
<div class="num">{{ `${practiceData.index}/${practiceData.words.length}` }}</div>
|
||||
<div class="num">{{ `${practiceData.index + 1}/${practiceData.words.length}` }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">{{ status }}</div>
|
||||
</div>
|
||||
|
||||
@@ -12,11 +12,10 @@ import {useSettingStore} from "@/stores/setting.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
const store = useBaseStore()
|
||||
const settings = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
@@ -63,11 +62,24 @@ watch(() => model.value, (n) => {
|
||||
<Dialog v-model="model" title="学习设置" :footer="true"
|
||||
@ok="changePerDayStudyNumber">
|
||||
<div class="target-modal color-main">
|
||||
<div class="center">
|
||||
<div class="flex gap-4 text-center h-30 w-85">
|
||||
<div class="mode-item" :class="temPracticeMode == 0 && 'active'" @click=" temPracticeMode = 0">
|
||||
<div class="title text-align-center">智能模式</div>
|
||||
<div class="desc mt-2">自动规划学习、复习、听写、默写</div>
|
||||
</div>
|
||||
<div class="mode-item" :class="temPracticeMode == 1 && 'active'" @click=" temPracticeMode = 1">
|
||||
<div class="title">自由模式</div>
|
||||
<div class="desc mt-2">自由练习,系统不强制复习与默写</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-2 mb-8">
|
||||
<span>从<span class="text-3xl mx-2 lh">{{ tempLastLearnIndex }}</span>个开始,</span>
|
||||
<span>从第<span class="text-3xl mx-2 lh">{{ tempLastLearnIndex }}</span>个开始,</span>
|
||||
<span>每日<span class="text-3xl mx-2 lh">{{ tempPerDayStudyNumber }}</span>个,</span>
|
||||
<span>预计<span
|
||||
class="text-3xl mx-2 lh">{{
|
||||
class="text-3xl mx-2 lh">{{
|
||||
_getAccomplishDays(runtimeStore.editDict.length - tempLastLearnIndex, tempPerDayStudyNumber)
|
||||
}}</span>天完成</span>
|
||||
</div>
|
||||
@@ -90,13 +102,6 @@ watch(() => model.value, (n) => {
|
||||
<BaseButton @click="show = true">从词典选起始位置</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gap-space">
|
||||
<RadioGroup v-model="temPracticeMode" class="flex-col gap-0!">
|
||||
<Radio :value="0" label="智能模式,系统自动计算复习单词与默写单词"/>
|
||||
<Radio :value="1" label="自由模式,系统不强制复习与默写"/>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
<template v-slot:footer-left v-if="showLeftOption">
|
||||
<div class="flex items-center">
|
||||
@@ -108,8 +113,8 @@ watch(() => model.value, (n) => {
|
||||
</template>
|
||||
</Dialog>
|
||||
<ChangeLastPracticeIndexDialog
|
||||
v-model="show"
|
||||
@ok="e => {
|
||||
v-model="show"
|
||||
@ok="e => {
|
||||
tempLastLearnIndex = e
|
||||
show = false
|
||||
}"
|
||||
@@ -125,5 +130,13 @@ watch(() => model.value, (n) => {
|
||||
.lh {
|
||||
color: rgb(176, 116, 211)
|
||||
}
|
||||
|
||||
.mode-item{
|
||||
@apply w-50% border border-blue border-solid p-2 rounded-lg cursor-pointer;
|
||||
}
|
||||
|
||||
.active{
|
||||
@apply bg-blue color-white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
96
src/pages/word/components/PracticeWordListDialog.vue
Normal file
96
src/pages/word/components/PracticeWordListDialog.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseTable from "@/components/BaseTable.vue";
|
||||
import WordItem from "@/components/WordItem.vue";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
import {TaskWords} from "@/types/types.ts";
|
||||
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
const model = defineModel()
|
||||
defineProps<{
|
||||
data: TaskWords
|
||||
}>()
|
||||
|
||||
let showTranslate = $ref(false)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model="model" title="任务">
|
||||
<div class="px-4 pb-4 h-80vh flex gap-4">
|
||||
<div class="h-full flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="title">新词 {{data.new.length}}</span>
|
||||
</div>
|
||||
<BaseTable
|
||||
class="overflow-auto flex-1 w-85"
|
||||
:list='data.new'
|
||||
:loading='false'
|
||||
:show-toolbar="false"
|
||||
:showPagination="false"
|
||||
>
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
:item="item.item"
|
||||
:show-translate="showTranslate">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
<div class="h-full flex flex-col gap-2" v-if="data.review.length">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="title">复习上次 {{data.review.length}}</span>
|
||||
</div>
|
||||
<BaseTable
|
||||
class="overflow-auto flex-1 w-85"
|
||||
:list='data.review'
|
||||
:loading='false'
|
||||
:show-toolbar="false"
|
||||
:showPagination="false"
|
||||
>
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
:item="item.item"
|
||||
:show-translate="showTranslate">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
<div class="h-full flex flex-col gap-2" v-if="data.write.length">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="title">复习之前 {{data.write.length}}</span>
|
||||
<Checkbox v-model="showTranslate">翻译</Checkbox>
|
||||
</div>
|
||||
<BaseTable
|
||||
class="overflow-auto flex-1 w-85"
|
||||
:list='data.write'
|
||||
:loading='false'
|
||||
:show-toolbar="false"
|
||||
:showPagination="false"
|
||||
>
|
||||
<template v-slot="item">
|
||||
<WordItem
|
||||
:item="item.item"
|
||||
:show-translate="showTranslate">
|
||||
<template v-slot:prefix>
|
||||
{{ item.index }}
|
||||
</template>
|
||||
</WordItem>
|
||||
</template>
|
||||
</BaseTable>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,15 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import {ShortcutKey, Word} from "@/types/types.ts";
|
||||
import { WordPracticeType, ShortcutKey, Word, WordPracticeMode } from "@/types/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio, useTTsPlayAudio} from "@/hooks/sound.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {nextTick, onMounted, onUnmounted, watch} from "vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio } from "@/hooks/sound.ts";
|
||||
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
|
||||
import { inject, onMounted, onUnmounted, Ref, watch } from "vue";
|
||||
import SentenceHightLightWord from "@/pages/word/components/SentenceHightLightWord.vue";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {getDefaultWord} from "@/types/func.ts";
|
||||
import {_nextTick, sleep} from "@/utils";
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import { getDefaultWord } from "@/types/func.ts";
|
||||
import { _nextTick, last, sleep } from "@/utils";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import Space from "@/pages/article/components/Space.vue";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
|
||||
interface IProps {
|
||||
word: Word,
|
||||
@@ -21,7 +24,8 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
|
||||
const emit = defineEmits<{
|
||||
complete: [],
|
||||
wrong: []
|
||||
wrong: [],
|
||||
know: [],
|
||||
}>()
|
||||
|
||||
let input = $ref('')
|
||||
@@ -60,17 +64,21 @@ function updateCurrentWordInfo() {
|
||||
};
|
||||
}
|
||||
|
||||
watch(() => props.word, () => {
|
||||
watch(() => props.word, reset, {deep: true})
|
||||
|
||||
function reset() {
|
||||
wrong = input = ''
|
||||
wordRepeatCount = 0
|
||||
inputLock = false
|
||||
showWordResult = inputLock = false
|
||||
if (settingStore.wordSound) {
|
||||
volumeIconRef?.play(400, true)
|
||||
if (settingStore.wordPracticeType !== WordPracticeType.Dictation) {
|
||||
volumeIconRef?.play(400, true)
|
||||
}
|
||||
}
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
checkCursorPosition()
|
||||
}, {deep: true})
|
||||
}
|
||||
|
||||
// 监听输入变化,更新当前单词信息
|
||||
watch(() => input, () => {
|
||||
@@ -81,11 +89,7 @@ onMounted(() => {
|
||||
// 初始化当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
|
||||
emitter.on(EventKey.resetWord, () => {
|
||||
wrong = input = ''
|
||||
updateCurrentWordInfo();
|
||||
})
|
||||
|
||||
emitter.on(EventKey.resetWord, reset)
|
||||
emitter.on(EventKey.onTyping, onTyping)
|
||||
})
|
||||
|
||||
@@ -106,95 +110,168 @@ function repeat() {
|
||||
}, settingStore.waitTimeForChangeWord)
|
||||
}
|
||||
|
||||
async function onTyping(e: KeyboardEvent) {
|
||||
if (inputLock) {
|
||||
//如果是锁定状态,说明要么输入太快;要么就是设置了不自动跳转,然后输入完了
|
||||
//当单词全部输入完成后,空格键用于切换到下一个单词
|
||||
if (e.code === 'Space' && input.toLowerCase() === props.word.word.toLowerCase()) {
|
||||
return emit('complete')
|
||||
}
|
||||
return
|
||||
}
|
||||
let letter = e.key
|
||||
inputLock = true
|
||||
let showWordResult = $ref(false)
|
||||
let pressNumber = 0
|
||||
|
||||
// 检查当前单词是否包含空格
|
||||
const wordContainsSpace = props.word.word.includes(' ')
|
||||
|
||||
// 如果是空格键,需要判断是作为输入还是切换单词
|
||||
if (letter === ' ' || e.code === 'Space') {
|
||||
// 如果当前单词包含空格
|
||||
if (wordContainsSpace && props.word.word[input.length] === ' ') {
|
||||
letter = ' '
|
||||
}
|
||||
// 如果当前单词不包含空格,且已经输入完成,则视为切换单词的信号
|
||||
else if (!wordContainsSpace && input.toLowerCase() === props.word.word.toLowerCase()) {
|
||||
return emit('complete')
|
||||
}
|
||||
}
|
||||
|
||||
let isTypingRight = false
|
||||
const right = $computed(() => {
|
||||
if (settingStore.ignoreCase) {
|
||||
isTypingRight = letter.toLowerCase() === props.word.word[input.length].toLowerCase()
|
||||
return input.toLowerCase() === props.word.word.toLowerCase()
|
||||
} else {
|
||||
isTypingRight = letter === props.word.word[input.length]
|
||||
}
|
||||
if (isTypingRight) {
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
} else {
|
||||
emit('wrong')
|
||||
wrong = letter
|
||||
playBeep()
|
||||
volumeIconRef?.play()
|
||||
await sleep(500)
|
||||
if (settingStore.inputWrongClear) input = ''
|
||||
wrong = ''
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
return input === props.word.word
|
||||
}
|
||||
})
|
||||
|
||||
if (input.toLowerCase() === props.word.word.toLowerCase()) {
|
||||
playCorrect()
|
||||
//不需要把inputLock设为false,输入完成不能再输入了,只能删除,删除会打开锁
|
||||
if (settingStore.autoNextWord) {
|
||||
if (settingStore.repeatCount == 100) {
|
||||
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
} else {
|
||||
if (settingStore.repeatCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
let showNotice = false
|
||||
|
||||
function know(e) {
|
||||
if (settingStore.wordPracticeType === WordPracticeType.Identify) {
|
||||
if (!showWordResult) {
|
||||
inputLock = showWordResult = true
|
||||
input = props.word.word
|
||||
emit('know')
|
||||
if (!showNotice) {
|
||||
Toast.info('若误选“我认识”,可按删除键重新选择!', {duration: 5000})
|
||||
showNotice = true
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
onTyping(e)
|
||||
}
|
||||
|
||||
function unknown(e) {
|
||||
if (settingStore.wordPracticeType === WordPracticeType.Identify) {
|
||||
if (!showWordResult) {
|
||||
showWordResult = true
|
||||
emit('wrong')
|
||||
return
|
||||
}
|
||||
}
|
||||
onTyping(e)
|
||||
}
|
||||
|
||||
async function onTyping(e: KeyboardEvent) {
|
||||
debugger
|
||||
let word = props.word.word
|
||||
if (inputLock) {
|
||||
// 因为输入完成会锁死不能再输入,所以在这里判断空格键切换到下一个单词
|
||||
if (e.code === 'Space' && input.toLowerCase() === word.toLowerCase()) {
|
||||
showWordResult = inputLock = false
|
||||
emit('complete')
|
||||
} else {
|
||||
//当显示单词时,提示用户正确按键
|
||||
if (showWordResult) {
|
||||
pressNumber++
|
||||
if (pressNumber >= 3) {
|
||||
Toast.info(right ? '请按空格键切换' : '请按删除键重新输入', {duration: 2000})
|
||||
pressNumber = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
inputLock = true
|
||||
let letter = e.key
|
||||
//默写特殊逻辑
|
||||
if (settingStore.wordPracticeType === WordPracticeType.Dictation) {
|
||||
if (e.code === 'Space') {
|
||||
//如果输入长度大于单词长度/单词不包含空格,并且输入不为空(开始直接输入空格不行),则显示单词;
|
||||
// 这里inputLock 不设为 false,不能再输入了,只能删除(删除会重置 inputLock)或按空格切下一格
|
||||
if (input.length && (input.length >= word.length || !word.includes(' '))) {
|
||||
//比对是否一致
|
||||
if (input.toLowerCase() === word.toLowerCase()) {
|
||||
//如果已显示单词,则发射完成事件,并 return
|
||||
if (showWordResult) {
|
||||
return emit('complete')
|
||||
} else {
|
||||
//未显示单词,则播放正确音乐,并在后面设置为 showWordResult 为 true 来显示单词
|
||||
playCorrect()
|
||||
volumeIconRef?.play()
|
||||
}
|
||||
} else {
|
||||
//错误处理
|
||||
playBeep()
|
||||
volumeIconRef?.play()
|
||||
emit('wrong')
|
||||
}
|
||||
showWordResult = true
|
||||
return
|
||||
}
|
||||
}
|
||||
//默写途中不判断是否正确,在按空格再判断
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
updateCurrentWordInfo();
|
||||
inputLock = false
|
||||
} else {
|
||||
let right = false
|
||||
if (settingStore.ignoreCase) {
|
||||
right = letter.toLowerCase() === word[input.length].toLowerCase()
|
||||
} else {
|
||||
right = letter === props.word.word[input.length]
|
||||
}
|
||||
if (right) {
|
||||
input += letter
|
||||
wrong = ''
|
||||
playKeyboardAudio()
|
||||
} else {
|
||||
emit('wrong')
|
||||
wrong = letter
|
||||
playBeep()
|
||||
volumeIconRef?.play()
|
||||
await sleep(500)
|
||||
if (settingStore.inputWrongClear) input = ''
|
||||
wrong = ''
|
||||
}
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
//不需要把inputLock设为false,输入完成不能再输入了,只能删除,删除会打开锁
|
||||
if (input.toLowerCase() === word.toLowerCase()) {
|
||||
playCorrect()
|
||||
if ([WordPracticeType.Listen, WordPracticeType.Identify].includes(settingStore.wordPracticeType) && !showWordResult) {
|
||||
showWordResult = true
|
||||
}
|
||||
if ([WordPracticeType.FollowWrite, WordPracticeType.Spell].includes(settingStore.wordPracticeType)) {
|
||||
if (settingStore.autoNextWord) {
|
||||
if (settingStore.repeatCount == 100) {
|
||||
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
} else {
|
||||
if (settingStore.repeatCount <= wordRepeatCount + 1) {
|
||||
setTimeout(() => emit('complete'), settingStore.waitTimeForChangeWord)
|
||||
} else {
|
||||
repeat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inputLock = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function del() {
|
||||
playKeyboardAudio()
|
||||
inputLock = false
|
||||
|
||||
if (wrong) {
|
||||
wrong = ''
|
||||
if (showWordResult) {
|
||||
input = ''
|
||||
showWordResult = false
|
||||
} else {
|
||||
input = input.slice(0, -1)
|
||||
if (wrong) {
|
||||
wrong = ''
|
||||
} else {
|
||||
input = input.slice(0, -1)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前单词信息
|
||||
updateCurrentWordInfo();
|
||||
}
|
||||
|
||||
|
||||
function showWord() {
|
||||
if (settingStore.allowWordTip) {
|
||||
showFullWord = true
|
||||
@@ -262,43 +339,53 @@ function checkCursorPosition() {
|
||||
_nextTick(() => {
|
||||
// 选中目标元素
|
||||
const cursorEl = document.querySelector(`.cursor`);
|
||||
const input = document.querySelector(`.input`);
|
||||
const inputList = document.querySelectorAll(`.l`);
|
||||
const typingWordRect = typingWordRef.getBoundingClientRect();
|
||||
|
||||
if (input) {
|
||||
let inputRect = input.getBoundingClientRect();
|
||||
if (inputList.length) {
|
||||
let inputRect = last(Array.from(inputList)).getBoundingClientRect();
|
||||
cursor = {
|
||||
top: inputRect.top + inputRect.height - cursorEl.clientHeight - typingWordRect.top,
|
||||
left: inputRect.right - typingWordRect.left - 3,
|
||||
};
|
||||
} else {
|
||||
const letter = document.querySelector(`.letter`);
|
||||
let letterRect = letter.getBoundingClientRect();
|
||||
const dictation = document.querySelector(`.dictation`);
|
||||
let elRect
|
||||
if (dictation) {
|
||||
elRect = dictation.getBoundingClientRect();
|
||||
} else {
|
||||
const letter = document.querySelector(`.letter`);
|
||||
elRect = letter.getBoundingClientRect();
|
||||
}
|
||||
cursor = {
|
||||
top: letterRect.top + letterRect.height - cursorEl.clientHeight - typingWordRect.top,
|
||||
left: letterRect.left - typingWordRect.left - 3,
|
||||
top: elRect.top + elRect.height - cursorEl.clientHeight - typingWordRect.top,
|
||||
left: elRect.left - typingWordRect.left - 3,
|
||||
};
|
||||
}
|
||||
},)
|
||||
}
|
||||
|
||||
useEvents([
|
||||
[ShortcutKey.KnowWord, know],
|
||||
[ShortcutKey.UnknownWord, unknown],
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="typing-word" ref="typingWordRef" v-if="props.word.word.length">
|
||||
<div class="typing-word" ref="typingWordRef" v-if="word.word.length">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex gap-1 mt-26">
|
||||
<div class="phonetic" v-if="settingStore.soundType === 'us' && word.phonetic0">[{{
|
||||
(settingStore.dictation && !showFullWord) ? '_'.repeat(word.phonetic0.length) : word.phonetic0
|
||||
}}]
|
||||
<div class="phonetic"
|
||||
:class="!(!settingStore.dictation || showFullWord || showWordResult) && 'word-shadow'"
|
||||
v-if="settingStore.soundType === 'us' && word.phonetic0">[{{ word.phonetic0 }}]
|
||||
</div>
|
||||
<div class="phonetic" v-if="settingStore.soundType === 'uk' && word.phonetic1">[{{
|
||||
(settingStore.dictation && !showFullWord) ? '_'.repeat(word.phonetic1.length) : word.phonetic1
|
||||
}}]
|
||||
<div class="phonetic"
|
||||
:class="((settingStore.dictation || [WordPracticeType.Spell,WordPracticeType.Listen,WordPracticeType.Dictation].includes(settingStore.wordPracticeType)) && !showFullWord && !showWordResult) && 'word-shadow'"
|
||||
v-if="settingStore.soundType === 'uk' && word.phonetic1">[{{ word.phonetic1 }}]
|
||||
</div>
|
||||
<VolumeIcon
|
||||
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
|
||||
:title="`发音(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
ref="volumeIconRef" :simple="true" :cb="() => playWordAudio(word.word)"/>
|
||||
</div>
|
||||
|
||||
<div class="word my-1"
|
||||
@@ -307,52 +394,101 @@ function checkCursorPosition() {
|
||||
@mouseenter="showWord"
|
||||
@mouseleave="mouseleave"
|
||||
>
|
||||
<span class="input" v-if="input">{{ input }}</span>
|
||||
<span class="wrong" v-if="wrong">{{ wrong }}</span>
|
||||
<template v-if="settingStore.dictation">
|
||||
<span class="letter" v-if="!showFullWord">{{ displayWord.split('').map(() => '_').join('') }}</span>
|
||||
<span class="letter" v-else>{{ displayWord }}</span>
|
||||
<div v-if="settingStore.wordPracticeType === WordPracticeType.Dictation">
|
||||
<div class="letter text-align-center w-full inline-block"
|
||||
v-opacity="showWordResult || showFullWord">
|
||||
{{ word.word }}
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 w-120 dictation"
|
||||
:style="{minHeight: settingStore.fontSize.wordForeignFontSize +'px'}"
|
||||
:class="showWordResult ? (right ? 'right' : 'wrong') : ''">
|
||||
<template v-for="i in input">
|
||||
<span class="l" v-if="i !== ' '">{{ i }}</span>
|
||||
<Space class="l" v-else :is-wrong="showWordResult ? (!right) : false" :is-wait="!showWordResult"/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<span class="input" v-if="input">{{ input }}</span>
|
||||
<span class="wrong" v-if="wrong">{{ wrong }}</span>
|
||||
<template v-if="settingStore.wordPracticeMode === WordPracticeMode.System">
|
||||
<template
|
||||
v-if="[WordPracticeType.Spell,WordPracticeType.Listen,WordPracticeType.Dictation].includes(settingStore.wordPracticeType)">
|
||||
<span class="letter" v-if="!showFullWord">{{
|
||||
displayWord.split('').map(() => (WordPracticeType.Dictation === settingStore.wordPracticeType ? ' ' : '_')).join('')
|
||||
}}</span>
|
||||
<span class="letter" v-else>{{ displayWord }}</span>
|
||||
</template>
|
||||
<span class="letter" v-else>{{ displayWord }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="letter" v-if="(settingStore.dictation && !showFullWord)">
|
||||
{{ displayWord.split('').map(() => '_').join('') }}
|
||||
</span>
|
||||
<span class="letter" v-else>{{ displayWord }}</span>
|
||||
</template>
|
||||
</template>
|
||||
<span class="letter" v-else>{{ displayWord }}</span>
|
||||
</div>
|
||||
|
||||
<div class="translate anim flex flex-col gap-2 my-3"
|
||||
v-opacity="settingStore.translate || showFullWord"
|
||||
<div class="mt-4 flex gap-4"
|
||||
v-if="settingStore.wordPracticeType === WordPracticeType.Identify && !showWordResult">
|
||||
<BaseButton
|
||||
:keyboard="`快捷键(${settingStore.shortcutKeyMap[ShortcutKey.KnowWord]})`"
|
||||
size="large" @click="know">我认识
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
:keyboard="`快捷键(${settingStore.shortcutKeyMap[ShortcutKey.UnknownWord]})`"
|
||||
size="large" @click="unknown">不认识
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<div class="translate flex flex-col gap-2 my-3"
|
||||
v-opacity="settingStore.translate || ![WordPracticeType.Listen,WordPracticeType.Identify].includes(settingStore.wordPracticeType) || showWordResult || showFullWord"
|
||||
:style="{
|
||||
fontSize: settingStore.fontSize.wordTranslateFontSize +'px',
|
||||
}"
|
||||
>
|
||||
<div class="flex" v-for="(v,i) in word.trans">
|
||||
<div class="shrink-0" :class="v.pos ? 'w-12 en-article-family' : '-ml-3'">{{ v.pos }}</div>
|
||||
<span v-if="settingStore.dictation && !showFullWord" v-html="hideWordInTranslation(v.cn, word.word)"></span>
|
||||
<span
|
||||
v-if="([WordPracticeType.Listen,WordPracticeType.Identify].includes(settingStore.wordPracticeType) || settingStore.dictation) && !(showWordResult || showFullWord)"
|
||||
v-html="hideWordInTranslation(v.cn, word.word)"></span>
|
||||
<span v-else>{{ v.cn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="other">
|
||||
<div class="other anim"
|
||||
v-opacity="![WordPracticeType.Listen,WordPracticeType.Dictation,WordPracticeType.Identify].includes(settingStore.wordPracticeType) || showFullWord || showWordResult">
|
||||
<div class="line-white my-2"></div>
|
||||
|
||||
<template v-if="word?.sentences?.length">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="sentence" v-for="item in word.sentences">
|
||||
<SentenceHightLightWord class="text-xl" :text="item.c" :word="word.word"
|
||||
:dictation="(settingStore.dictation && !showFullWord)"/>
|
||||
<div class="text-base anim" v-opacity="settingStore.translate || showFullWord">{{ item.cn }}</div>
|
||||
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"/>
|
||||
<div class="text-base anim"
|
||||
v-opacity="settingStore.translate || showFullWord || showWordResult">
|
||||
{{ item.cn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="line-white my-2 mb-5 anim" v-opacity="settingStore.translate || showFullWord"></div>
|
||||
<div class="line-white my-2 mb-5 anim"
|
||||
v-opacity="settingStore.translate || showFullWord || showWordResult"></div>
|
||||
</template>
|
||||
|
||||
|
||||
<div class="anim" v-opacity="settingStore.translate || showFullWord">
|
||||
<div class="anim"
|
||||
v-opacity="settingStore.translate || showFullWord || showWordResult">
|
||||
<template v-if="word?.phrases?.length">
|
||||
<div class="flex">
|
||||
<div class="label">短语</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-4" v-for="item in word.phrases">
|
||||
<SentenceHightLightWord class="en" :text="item.c" :word="word.word"
|
||||
:dictation="(settingStore.dictation && !showFullWord)"/>
|
||||
<div class="cn anim" v-opacity="settingStore.translate">{{ item.cn }}</div>
|
||||
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"/>
|
||||
<div class="cn anim" v-opacity="settingStore.translate || showFullWord || showWordResult">{{
|
||||
item.cn
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -419,6 +555,10 @@ function checkCursorPosition() {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dictation {
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
|
||||
.typing-word {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
@@ -442,7 +582,8 @@ function checkCursorPosition() {
|
||||
font-family: var(--en-article-family);
|
||||
letter-spacing: .3rem;
|
||||
|
||||
.input {
|
||||
|
||||
.input, .right {
|
||||
color: rgb(22, 163, 74);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { checkAndUpgradeSaveSetting, cloneDeep } from "@/utils";
|
||||
import { DefaultShortcutKeyMap } from "@/types/types.ts";
|
||||
import {DefaultShortcutKeyMap, WordPracticeMode, WordPracticeType} from "@/types/types.ts";
|
||||
import { get } from "idb-keyval";
|
||||
import { CAN_REQUEST, SAVE_SETTING_KEY } from "@/config/env.ts";
|
||||
import { getSetting } from "@/apis";
|
||||
@@ -48,7 +48,8 @@ export interface SettingState {
|
||||
load: boolean
|
||||
conflictNotice: boolean // 其他脚本/插件冲突提示
|
||||
ignoreSimpleWord: boolean // 忽略简单词
|
||||
wordPracticeMode: number // 单词练习模式,0:智能模式,1:自由模式
|
||||
wordPracticeMode: WordPracticeMode // 单词练习模式
|
||||
wordPracticeType: WordPracticeType // 单词练习类型
|
||||
disableShowPracticeSettingDialog: boolean // 不默认显示练习设置弹框
|
||||
autoNextWord: boolean //自动切换下一个单词
|
||||
inputWrongClear: boolean //单词输入错误,清空已输入内容
|
||||
@@ -97,7 +98,8 @@ export const getDefaultSettingState = (): SettingState => ({
|
||||
load: false,
|
||||
conflictNotice: true,
|
||||
ignoreSimpleWord: false,
|
||||
wordPracticeMode: 0,
|
||||
wordPracticeMode: WordPracticeMode.System,
|
||||
wordPracticeType: WordPracticeType.FollowWrite,
|
||||
disableShowPracticeSettingDialog: false,
|
||||
autoNextWord: true,
|
||||
inputWrongClear: false,
|
||||
|
||||
@@ -117,7 +117,9 @@ export enum ShortcutKey {
|
||||
ToggleConciseMode = 'ToggleConciseMode',
|
||||
TogglePanel = 'TogglePanel',
|
||||
RandomWrite = 'RandomWrite',
|
||||
NextRandomWrite = 'NextRandomWrite'
|
||||
NextRandomWrite = 'NextRandomWrite',
|
||||
KnowWord = 'KnowWord',
|
||||
UnknownWord = 'UnknownWord',
|
||||
}
|
||||
|
||||
export const DefaultShortcutKeyMap = {
|
||||
@@ -139,6 +141,8 @@ export const DefaultShortcutKeyMap = {
|
||||
[ShortcutKey.TogglePanel]: 'Ctrl+L',
|
||||
[ShortcutKey.RandomWrite]: 'Ctrl+R',
|
||||
[ShortcutKey.NextRandomWrite]: 'Ctrl+Shift+R',
|
||||
[ShortcutKey.KnowWord]: '1',
|
||||
[ShortcutKey.UnknownWord]: '2',
|
||||
}
|
||||
|
||||
export enum TranslateEngine {
|
||||
@@ -187,8 +191,9 @@ export const SlideType = {
|
||||
|
||||
export interface PracticeData {
|
||||
index: number,
|
||||
words: any[],
|
||||
wrongWords: any[],
|
||||
words: Word[],
|
||||
wrongWords: Word[],
|
||||
excludeWords: string[],
|
||||
}
|
||||
|
||||
export interface TaskWords {
|
||||
@@ -208,4 +213,19 @@ export enum PracticeArticleWordType {
|
||||
Symbol,
|
||||
Number,
|
||||
Word
|
||||
}
|
||||
|
||||
//练习模式
|
||||
export enum WordPracticeMode {
|
||||
System = 0,
|
||||
Free = 1
|
||||
}
|
||||
|
||||
//练习类型
|
||||
export enum WordPracticeType {
|
||||
FollowWrite,//跟写
|
||||
Spell,
|
||||
Identify,
|
||||
Listen,
|
||||
Dictation
|
||||
}
|
||||
Reference in New Issue
Block a user