This commit is contained in:
Zyronon
2025-12-27 03:08:41 +08:00
parent e7e7b202bc
commit e09db8c22a
19 changed files with 762 additions and 513 deletions

1
components.d.ts vendored
View File

@@ -139,6 +139,7 @@ declare module 'vue' {
MigrateDialog: typeof import('./src/components/MigrateDialog.vue')['default']
MiniDialog: typeof import('./src/components/dialog/MiniDialog.vue')['default']
Option: typeof import('./src/components/base/select/Option.vue')['default']
OptionButton: typeof import('./src/components/base/OptionButton.vue')['default']
Pagination: typeof import('./src/components/base/Pagination.vue')['default']
Panel: typeof import('./src/components/Panel.vue')['default']
PopConfirm: typeof import('./src/components/PopConfirm.vue')['default']

View File

@@ -50,9 +50,6 @@
--en-article-family: Georgia, sans-serif;
--zh-article-family: "Songti SC", "SimSun", "Noto Serif CJK SC", serif;
--btn-primary: rgb(75, 85, 99);
--btn-info: white;
--btn-info-hover: #eaeaea;
--color-primary: #E6E8EB;
--color-second: rgb(247, 247, 247);
@@ -121,9 +118,6 @@ html.dark {
--color-sub-gray: #383737;
--color-scrollbar: rgb(92, 93, 94);
--btn-info: #1b1b1b;
--btn-info-hover: #3a3a3a;
--color-input-color: white;
--color-input-bg: rgba(14, 18, 23, 1);
--color-input-icon: #383737;

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import Tooltip from "@/components/base/Tooltip.vue";
import Tooltip from '@/components/base/Tooltip.vue'
interface IProps {
keyboard?: string,
keyboard?: string
active?: boolean
disabled?: boolean
loading?: boolean
size?: 'small' | 'normal' | 'large',
size?: 'small' | 'normal' | 'large'
type?: 'primary' | 'link' | 'info' | 'orange'
}
@@ -16,33 +16,45 @@ withDefaults(defineProps<IProps>(), {
})
defineEmits(['click'])
</script>
<template>
<Tooltip :disabled="!keyboard" :title="`${keyboard}`">
<div class="base-button"
v-bind="$attrs"
@click="e => (!disabled && !loading) && $emit('click',e)"
:class="[
active && 'active',
size,
type,
(disabled||loading) && 'disabled',
]">
<span :style="{opacity:loading?0:1}"><slot></slot></span>
<div
class="base-button"
v-bind="$attrs"
@click="e => !disabled && !loading && $emit('click', e)"
:class="[active && 'active', size, type, (disabled || loading) && 'disabled']"
>
<span :style="{ opacity: loading ? 0 : 1 }"><slot></slot></span>
<IconEosIconsLoading
v-if="loading"
class="loading"
width="18"
:color="type === 'info'?'#000000':'#ffffff'"
v-if="loading"
class="loading"
width="18"
:color="type === 'info' ? '#000000' : '#ffffff'"
/>
</div>
</Tooltip>
</template>
<style scoped lang="scss">
<style>
:root {
--btn-primary: rgb(75, 85, 99);
--btn-primary-disabled: #90969e;
--btn-primary-hover: rgb(105, 121, 143);
--btn-info: white;
--btn-info-hover: #eaeaea;
--btn-orange: #facc15;
--btn-orange-hover: #fbe27e;
}
html.dark {
--btn-info: #1b1b1b;
--btn-info-hover: #3a3a3a;
}
</style>
<style scoped lang="scss">
.base-button {
cursor: pointer;
box-sizing: border-box;
@@ -51,13 +63,13 @@ defineEmits(['click'])
justify-content: center;
outline: none;
text-align: center;
transition: all .3s;
transition: all 0.3s;
user-select: none;
vertical-align: middle;
white-space: nowrap;
border-radius: .3rem;
border-radius: 0.3rem;
padding: 0 0.9rem;
font-size: .9rem;
font-size: 0.9rem;
height: 2rem;
color: white;
@@ -65,28 +77,29 @@ defineEmits(['click'])
margin-left: 1rem;
}
.loading {
position: absolute;
}
&.disabled {
opacity: .6;
opacity: 0.6;
cursor: not-allowed;
user-select: none;
color: rgba(#fff, 0.4);
}
.loading {
position: absolute;
}
&.small {
border-radius: 0.3rem;
padding: 0 0.6rem;
height: 1.6rem;
font-size: .8rem;
font-size: 0.8rem;
}
&.large {
padding: 0 1.3rem;
height: 2.4rem;
font-size: 0.9rem;
border-radius: .5rem;
border-radius: 0.5rem;
}
& > span {
@@ -101,8 +114,13 @@ defineEmits(['click'])
&.primary {
background: var(--btn-primary);
&.disabled {
opacity: 1;
background: var(--btn-primary-disabled);
}
&:hover:not(.disabled) {
opacity: 0.6;
background: var(--btn-primary-hover);
}
}
@@ -126,17 +144,17 @@ defineEmits(['click'])
}
&.orange {
background: #FACC15;
background: var(--btn-orange);
color: black;
&:hover:not(.disabled) {
background: #fbe27e;
background: var(--btn-orange-hover);
color: rgba(0, 0, 0, 0.6);
}
}
&.active {
opacity: .4;
opacity: 0.4;
}
}
</style>

View File

@@ -1,13 +1,13 @@
<script setup lang="ts"></script>
<template>
<div class="w-full flex box-border cp color-white">
<div class="flex box-border cp color-white">
<div class="option-wrap">
<slot></slot>
</div>
<div class="relative group">
<div
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-1 border-l-gray/50 border-transparent box-border transition-all duration-300"
class="more w-10 rounded-r-lg h-full center border-solid border-1 border-l-gray/50 border-transparent box-border transition-all duration-300"
>
<IconFluentChevronDown20Regular />
</div>
@@ -26,8 +26,28 @@
display: flex;
:deep(.base-button) {
width: 100%;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
}
.primary-btn {
.more {
background: var(--btn-primary);
&:hover {
background: var(--btn-primary-hover);
}
}
}
.orange-btn {
.more {
background: var(--btn-orange);
color: black;
border-left-color: black;
&:hover {
background: var(--btn-orange-hover);
}
}
}
</style>

View File

@@ -87,7 +87,7 @@ watch(innerValue, () => {
}, {immediate: true})
</script>
<style>
<style scoped lang="scss">
.disabled {
opacity: 0.5;

View File

@@ -68,15 +68,6 @@ export const EXPORT_DATA_KEY = {
}
export const LOCAL_FILE_KEY = 'typing-word-files'
export const PracticeSaveWordKey = {
key: 'PracticeSaveWord',
version: 1,
}
export const PracticeSaveArticleKey = {
key: 'PracticeSaveArticle',
version: 1,
}
export const TourConfig = {
useModalOverlay: true,
defaultStepOptions: {

View File

@@ -1,22 +1,22 @@
import {loadJsLib, shakeCommonDict} from "@/utils";
import { loadJsLib, shakeCommonDict } from '@/utils'
import {
APP_NAME,
APP_VERSION,
EXPORT_DATA_KEY, LIB_JS_URL,
EXPORT_DATA_KEY,
LIB_JS_URL,
LOCAL_FILE_KEY,
Origin,
PracticeSaveArticleKey,
PracticeSaveWordKey,
SAVE_DICT_KEY,
SAVE_SETTING_KEY
} from "@/config/env.ts";
import {get} from "idb-keyval";
import {saveAs} from "file-saver";
import dayjs from "dayjs";
import Toast from "@/components/base/toast/Toast.ts";
import {useBaseStore} from "@/stores/base.ts";
import {useSettingStore} from "@/stores/setting.ts";
import {ref} from "vue";
SAVE_SETTING_KEY,
} from '@/config/env.ts'
import { get } from 'idb-keyval'
import { saveAs } from 'file-saver'
import dayjs from 'dayjs'
import Toast from '@/components/base/toast/Toast.ts'
import { useBaseStore } from '@/stores/base.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { ref } from 'vue'
import { PRACTICE_ARTICLE_CACHE, PRACTICE_WORD_CACHE } from '@/utils/cache.ts'
export function useExport() {
const store = useBaseStore()
@@ -24,60 +24,61 @@ export function useExport() {
let loading = ref(false)
async function exportData(notice = '导出成功!', fileName = `${APP_NAME}-User-Data-${dayjs().format('YYYY-MM-DD HH-mm-ss')}.zip`) {
async function exportData(
notice = '导出成功!',
fileName = `${APP_NAME}-User-Data-${dayjs().format('YYYY-MM-DD HH-mm-ss')}.zip`
) {
if (loading.value) return
loading.value = true
try {
const JSZip = await loadJsLib('JSZip', LIB_JS_URL.JSZIP);
const JSZip = await loadJsLib('JSZip', LIB_JS_URL.JSZIP)
let data = {
version: EXPORT_DATA_KEY.version,
val: {
setting: {
version: SAVE_SETTING_KEY.version,
val: settingStore.$state
val: settingStore.$state,
},
dict: {
version: SAVE_DICT_KEY.version,
val: shakeCommonDict(store.$state)
val: shakeCommonDict(store.$state),
},
[PracticeSaveWordKey.key]: {
version: PracticeSaveWordKey.version,
val: {}
[PRACTICE_WORD_CACHE.key]: {
version: PRACTICE_WORD_CACHE.version,
val: {},
},
[PracticeSaveArticleKey.key]: {
version: PracticeSaveArticleKey.version,
val: {}
[PRACTICE_ARTICLE_CACHE.key]: {
version: PRACTICE_ARTICLE_CACHE.version,
val: {},
},
[APP_VERSION.key]: -1
}
[APP_VERSION.key]: -1,
},
}
let d = localStorage.getItem(PracticeSaveWordKey.key)
let d = localStorage.getItem(PRACTICE_WORD_CACHE.key)
if (d) {
try {
data.val[PracticeSaveWordKey.key] = JSON.parse(d)
} catch (e) {
}
data.val[PRACTICE_WORD_CACHE.key] = JSON.parse(d)
} catch (e) {}
}
let d1 = localStorage.getItem(PracticeSaveArticleKey.key)
let d1 = localStorage.getItem(PRACTICE_ARTICLE_CACHE.key)
if (d1) {
try {
data.val[PracticeSaveArticleKey.key] = JSON.parse(d1)
} catch (e) {
}
data.val[PRACTICE_ARTICLE_CACHE.key] = JSON.parse(d1)
} catch (e) {}
}
let r = await get(APP_VERSION.key)
data.val[APP_VERSION.key] = r
const zip = new JSZip();
zip.file("data.json", JSON.stringify(data));
const zip = new JSZip()
zip.file('data.json', JSON.stringify(data))
const mp3 = zip.folder("mp3");
const allRecords = await get(LOCAL_FILE_KEY);
const mp3 = zip.folder('mp3')
const allRecords = await get(LOCAL_FILE_KEY)
for (const rec of allRecords ?? []) {
mp3.file(rec.id + ".mp3", rec.file);
mp3.file(rec.id + '.mp3', rec.file)
}
let content = await zip.generateAsync({type: "blob"})
saveAs(content, fileName);
let content = await zip.generateAsync({ type: 'blob' })
saveAs(content, fileName)
notice && Toast.success(notice)
return content
} catch (e) {
@@ -91,4 +92,4 @@ export function useExport() {
loading,
exportData,
}
}
}

View File

@@ -27,9 +27,10 @@ import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import isoWeek from 'dayjs/plugin/isoWeek'
import { useFetch } from "@vueuse/core";
import { AppEnv, DICT_LIST, Host, LIB_JS_URL, PracticeSaveArticleKey, TourConfig } from "@/config/env.ts";
import { AppEnv, DICT_LIST, Host, LIB_JS_URL, TourConfig } from "@/config/env.ts";
import { myDictList } from "@/apis";
import { useSettingStore } from "@/stores/setting.ts";
import {PRACTICE_ARTICLE_CACHE} from "@/utils/cache.ts";
dayjs.extend(isoWeek)
dayjs.extend(isBetween);
@@ -58,7 +59,7 @@ async function init() {
store.article.bookList[store.article.studyIndex] = await _getDictDataByUrl(store.sbook, DictType.article)
}
}
let d = localStorage.getItem(PracticeSaveArticleKey.key)
let d = localStorage.getItem(PRACTICE_ARTICLE_CACHE.key)
if (d) {
try {
let obj = JSON.parse(d)
@@ -73,7 +74,7 @@ async function init() {
}
isSaveData = true
} catch (e) {
localStorage.removeItem(PracticeSaveArticleKey.key)
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
}
}
}

View File

@@ -33,10 +33,11 @@ import ConflictNotice from "@/components/ConflictNotice.vue";
import { useRoute, useRouter } from "vue-router";
import PracticeLayout from "@/components/PracticeLayout.vue";
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
import { AppEnv, DICT_LIST, LIB_JS_URL, PracticeSaveArticleKey, TourConfig } from "@/config/env.ts";
import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig } from "@/config/env.ts";
import { addStat, setUserDictProp } from "@/apis";
import { useRuntimeStore } from "@/stores/runtime.ts";
import SettingDialog from "@/components/setting/SettingDialog.vue";
import {PRACTICE_ARTICLE_CACHE} from "@/utils/cache.ts";
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
@@ -222,7 +223,7 @@ useStartKeyboardEventListener()
useDisableEventListener(() => loading)
function savePracticeData(init = true, regenerate = true) {
let d = localStorage.getItem(PracticeSaveArticleKey.key)
let d = localStorage.getItem(PRACTICE_ARTICLE_CACHE.key)
if (d) {
try {
let obj = JSON.parse(d)
@@ -244,14 +245,14 @@ function savePracticeData(init = true, regenerate = true) {
}
obj.val.statStoreData = statStore.$state
localStorage.setItem(PracticeSaveArticleKey.key, JSON.stringify(obj))
localStorage.setItem(PRACTICE_ARTICLE_CACHE.key, JSON.stringify(obj))
} catch (e) {
localStorage.removeItem(PracticeSaveArticleKey.key)
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
regenerate && savePracticeData()
}
} else {
localStorage.setItem(PracticeSaveArticleKey.key, JSON.stringify({
version: PracticeSaveArticleKey.version,
localStorage.setItem(PRACTICE_ARTICLE_CACHE.key, JSON.stringify({
version: PRACTICE_ARTICLE_CACHE.version,
val: {
practiceData: {
sectionIndex: 0,
@@ -300,7 +301,7 @@ function setArticle(val: Article) {
async function complete() {
clearInterval(timer)
setTimeout(() => {
localStorage.removeItem(PracticeSaveArticleKey.key)
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
}, 1500)
//todo 有空了改成实时保存

View File

@@ -18,7 +18,8 @@ import {useWordOptions} from "@/hooks/dict.ts";
import nlp from "compromise/three";
import {nanoid} from "nanoid";
import {usePracticeStore} from "@/stores/practice.ts";
import {PracticeSaveArticleKey} from "@/config/env.ts";
import {PRACTICE_ARTICLE_CACHE} from "@/utils/cache.ts";
interface IProps {
article: Article,
@@ -89,8 +90,8 @@ const statStore = usePracticeStore()
const isMob = isMobile()
watch([() => sectionIndex, () => sentenceIndex, () => wordIndex, () => stringIndex], ([a, b, c,]) => {
localStorage.setItem(PracticeSaveArticleKey.key, JSON.stringify({
version: PracticeSaveArticleKey.version,
localStorage.setItem(PRACTICE_ARTICLE_CACHE.key, JSON.stringify({
version: PRACTICE_ARTICLE_CACHE.version,
val: {
practiceData: {
sectionIndex,
@@ -124,7 +125,7 @@ watch(() => isEnd, n => {
function init() {
if (!props.article.id) return
isSpace = isEnd = false
let d = localStorage.getItem(PracticeSaveArticleKey.key)
let d = localStorage.getItem(PRACTICE_ARTICLE_CACHE.key)
if (d) {
try {
let obj = JSON.parse(d)
@@ -132,7 +133,7 @@ function init() {
statStore.$patch(data.statStoreData)
jump(data.practiceData.sectionIndex, data.practiceData.sentenceIndex, data.practiceData.wordIndex)
} catch (e) {
localStorage.removeItem(PracticeSaveArticleKey.key)
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
init()
}
} else {
@@ -411,7 +412,7 @@ function onTyping(e: KeyboardEvent) {
e.preventDefault()
} catch (e) {
//todo 上报
localStorage.removeItem(PracticeSaveArticleKey.key)
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
init()
} finally {
isTyping = false

View File

@@ -1,31 +1,30 @@
<script setup lang="ts">
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 { DefaultShortcutKeyMap } from "@/types/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import { useBaseStore } from "@/stores/base.ts";
import { nextTick, ref, watch } from 'vue'
import { useSettingStore } from '@/stores/setting.ts'
import { getShortcutKey, useEventListener } from '@/hooks/event.ts'
import {
APP_NAME,
APP_VERSION,
Host,
LIB_JS_URL,
LOCAL_FILE_KEY,
PracticeSaveArticleKey,
PracticeSaveWordKey
} from "@/config/env.ts";
import BasePage from "@/components/BasePage.vue";
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 BasePage from '@/components/BasePage.vue'
import Toast from '@/components/base/toast/Toast.ts'
import { set } from "idb-keyval";
import { useRuntimeStore } from "@/stores/runtime.ts";
import { useExport } from "@/hooks/export.ts";
import MigrateDialog from "@/components/MigrateDialog.vue";
import Log from "@/pages/setting/Log.vue";
import About from "@/components/About.vue";
import CommonSetting from "@/components/setting/CommonSetting.vue";
import ArticleSettting from "@/components/setting/ArticleSettting.vue";
import WordSetting from "@/components/setting/WordSetting.vue";
import { set } from 'idb-keyval'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useExport } from '@/hooks/export.ts'
import MigrateDialog from '@/components/MigrateDialog.vue'
import Log from '@/pages/setting/Log.vue'
import About from '@/components/About.vue'
import CommonSetting from '@/components/setting/CommonSetting.vue'
import ArticleSettting from '@/components/setting/ArticleSettting.vue'
import WordSetting from '@/components/setting/WordSetting.vue'
import { PRACTICE_ARTICLE_CACHE, PRACTICE_WORD_CACHE } from '@/utils/cache.ts'
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
@@ -37,7 +36,7 @@ const runtimeStore = useRuntimeStore()
const store = useBaseStore()
//@ts-ignore
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
const gitLastCommitHash = ref(LATEST_COMMIT_HASH)
let editShortcutKey = $ref('')
@@ -45,19 +44,25 @@ const disabledDefaultKeyboardEvent = $computed(() => {
return editShortcutKey && tabIndex === 3
})
watch(() => disabledDefaultKeyboardEvent, v => {
emit('toggleDisabledDialogEscKey', !!v)
})
watch(
() => disabledDefaultKeyboardEvent,
v => {
emit('toggleDisabledDialogEscKey', !!v)
}
)
// 监听编辑快捷键状态变化,自动聚焦输入框
watch(() => editShortcutKey, (newVal) => {
if (newVal) {
// 使用nextTick确保DOM已更新
nextTick(() => {
focusShortcutInput()
})
watch(
() => editShortcutKey,
newVal => {
if (newVal) {
// 使用nextTick确保DOM已更新
nextTick(() => {
focusShortcutInput()
})
}
}
})
)
useEventListener('keydown', (e: KeyboardEvent) => {
if (!disabledDefaultKeyboardEvent) return
@@ -80,9 +85,15 @@ useEventListener('keydown', (e: KeyboardEvent) => {
settingStore.shortcutKeyMap[editShortcutKey] = ''
} else {
// 忽略单独的修饰键
if (shortcutKey === 'Ctrl+' || shortcutKey === 'Alt+' || shortcutKey === 'Shift+' ||
e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift') {
return;
if (
shortcutKey === 'Ctrl+' ||
shortcutKey === 'Alt+' ||
shortcutKey === 'Shift+' ||
e.key === 'Control' ||
e.key === 'Alt' ||
e.key === 'Shift'
) {
return
}
for (const [k, v] of Object.entries(settingStore.shortcutKeyMap)) {
@@ -120,26 +131,26 @@ function focusShortcutInput() {
// 快捷键中文名称映射
function getShortcutKeyName(key: string): string {
const shortcutKeyNameMap = {
'ShowWord': '显示单词',
'EditArticle': '编辑文章',
'Next': '下一个',
'Previous': '上一个',
'ToggleSimple': '切换已掌握状态',
'ToggleCollect': '切换收藏状态',
'NextChapter': '下一组',
'PreviousChapter': '上一组',
'RepeatChapter': '重复本组',
'DictationChapter': '默写本组',
'PlayWordPronunciation': '播放发音',
'ToggleShowTranslate': '切换显示翻译',
'ToggleDictation': '切换默写模式',
'ToggleTheme': '切换主题',
'ToggleConciseMode': '切换简洁模式',
'TogglePanel': '切换面板',
'RandomWrite': '随机默写',
'NextRandomWrite': '继续随机默写',
'KnowWord': '认识单词',
'UnknownWord': '不认识单词',
ShowWord: '显示单词',
EditArticle: '编辑文章',
Next: '下一个',
Previous: '上一个',
ToggleSimple: '切换已掌握状态',
ToggleCollect: '切换收藏状态',
NextChapter: '下一组',
PreviousChapter: '上一组',
RepeatChapter: '重复本组',
DictationChapter: '默写本组',
PlayWordPronunciation: '播放发音',
ToggleShowTranslate: '切换显示翻译',
ToggleDictation: '切换默写模式',
ToggleTheme: '切换主题',
ToggleConciseMode: '切换简洁模式',
TogglePanel: '切换面板',
RandomWrite: '随机默写',
NextRandomWrite: '继续随机默写',
KnowWord: '认识单词',
UnknownWord: '不认识单词',
}
return shortcutKeyNameMap[key] || key
@@ -162,10 +173,10 @@ function importJson(str: string, notice: boolean = true) {
val: {
setting: {},
dict: {},
[PracticeSaveWordKey.key]: {},
[PracticeSaveArticleKey.key]: {},
[PRACTICE_WORD_CACHE.key]: {},
[PRACTICE_ARTICLE_CACHE.key]: {},
[APP_VERSION.key]: {},
}
},
}
try {
obj = JSON.parse(str)
@@ -178,9 +189,12 @@ function importJson(str: string, notice: boolean = true) {
store.setState(baseState)
if (obj.version >= 3) {
try {
let save: any = obj.val[PracticeSaveWordKey.key] || {}
let save: any = obj.val[PRACTICE_WORD_CACHE.key] || {}
if (save.val && Object.keys(save.val).length > 0) {
localStorage.setItem(PracticeSaveWordKey.key, JSON.stringify(obj.val[PracticeSaveWordKey.key]))
localStorage.setItem(
PRACTICE_WORD_CACHE.key,
JSON.stringify(obj.val[PRACTICE_WORD_CACHE.key])
)
}
} catch (e) {
//todo 上报
@@ -188,9 +202,12 @@ function importJson(str: string, notice: boolean = true) {
}
if (obj.version >= 4) {
try {
let save: any = obj.val[PracticeSaveArticleKey.key] || {}
let save: any = obj.val[PRACTICE_ARTICLE_CACHE.key] || {}
if (save.val && Object.keys(save.val).length > 0) {
localStorage.setItem(PracticeSaveArticleKey.key, JSON.stringify(obj.val[PracticeSaveArticleKey.key]))
localStorage.setItem(
PRACTICE_ARTICLE_CACHE.key,
JSON.stringify(obj.val[PRACTICE_ARTICLE_CACHE.key])
)
}
} catch (e) {
//todo 上报
@@ -198,7 +215,7 @@ function importJson(str: string, notice: boolean = true) {
try {
let r: any = obj.val[APP_VERSION.key] || -1
set(APP_VERSION.key, r)
runtimeStore.isNew = r ? (APP_VERSION.version > Number(r)) : true
runtimeStore.isNew = r ? APP_VERSION.version > Number(r) : true
} catch (e) {
//todo 上报
}
@@ -218,59 +235,59 @@ async function beforeImport() {
await sleep(1500)
let d: HTMLDivElement = document.querySelector('#import')
d.click()
timer = setTimeout(()=>importLoading = false, 1000)
timer = setTimeout(() => (importLoading = false), 1000)
}
async function importData(e) {
clearTimeout(timer)
importLoading = true
let file = e.target.files[0]
if (!file) return importLoading = false
if (file.name.endsWith(".json")) {
let reader = new FileReader();
if (!file) return (importLoading = false)
if (file.name.endsWith('.json')) {
let reader = new FileReader()
reader.onload = function (v) {
let str: any = v.target.result;
let str: any = v.target.result
if (str) {
importJson(str)
}
}
reader.readAsText(file);
} else if (file.name.endsWith(".zip")) {
reader.readAsText(file)
} else if (file.name.endsWith('.zip')) {
try {
const JSZip = await loadJsLib('JSZip', LIB_JS_URL.JSZIP);
const zip = await JSZip.loadAsync(file);
const JSZip = await loadJsLib('JSZip', LIB_JS_URL.JSZIP)
const zip = await JSZip.loadAsync(file)
const dataFile = zip.file("data.json");
const dataFile = zip.file('data.json')
if (!dataFile) {
return Toast.error("缺少 data.json导入失败");
return Toast.error('缺少 data.json导入失败')
}
const mp3Folder = zip.folder("mp3");
const mp3Folder = zip.folder('mp3')
if (mp3Folder) {
const records: { id: string; file: Blob }[] = [];
const records: { id: string; file: Blob }[] = []
for (const filename in zip.files) {
if (filename.startsWith("mp3/") && filename.endsWith(".mp3")) {
const entry = zip.file(filename);
if (!entry) continue;
const blob = await entry.async("blob");
const id = filename.replace(/^mp3\//, "").replace(/\.mp3$/, "");
records.push({ id, file: blob });
if (filename.startsWith('mp3/') && filename.endsWith('.mp3')) {
const entry = zip.file(filename)
if (!entry) continue
const blob = await entry.async('blob')
const id = filename.replace(/^mp3\//, '').replace(/\.mp3$/, '')
records.push({ id, file: blob })
}
}
await set(LOCAL_FILE_KEY, records);
await set(LOCAL_FILE_KEY, records)
}
const str = await dataFile.async("string");
const str = await dataFile.async('string')
importJson(str, false)
Toast.success("导入成功!");
Toast.success('导入成功!')
} catch (e) {
Toast.error(e?.message || e || '导入失败')
} finally {
importLoading = false
}
} else {
Toast.error("不支持的文件类型");
Toast.error('不支持的文件类型')
}
importLoading = false
}
@@ -288,55 +305,59 @@ function transferOk() {
<template>
<BasePage>
<div class="setting text-md card flex flex-col" style="height: calc(100vh - 3rem);">
<div class="setting text-md card flex flex-col" style="height: calc(100vh - 3rem)">
<div class="page-title text-align-center">设置</div>
<div class="flex flex-1 overflow-hidden gap-4">
<div class="left">
<div class="tabs">
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
<IconFluentSettings20Regular width="20"/>
<IconFluentSettings20Regular width="20" />
<span>通用设置</span>
</div>
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
<IconFluentTextUnderlineDouble20Regular width="20"/>
<IconFluentTextUnderlineDouble20Regular width="20" />
<span>单词设置</span>
</div>
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
<IconFluentBookLetter20Regular width="20"/>
<IconFluentBookLetter20Regular width="20" />
<span>文章设置</span>
</div>
<div class="tab" :class="tabIndex === 4 && 'active'" @click="tabIndex = 4">
<IconFluentDatabasePerson20Regular width="20"/>
<IconFluentDatabasePerson20Regular width="20" />
<span>数据管理</span>
</div>
<div class="tab" :class="tabIndex === 3 && 'active'" @click="tabIndex = 3">
<IconFluentKeyboardLayoutFloat20Regular width="20"/>
<IconFluentKeyboardLayoutFloat20Regular width="20" />
<span>快捷键设置</span>
</div>
<div class="tab" :class="tabIndex === 5 && 'active'" @click="()=>{
tabIndex = 5
runtimeStore.isNew = false
set(APP_VERSION.key,APP_VERSION.version)
}">
<IconFluentTextBulletListSquare20Regular width="20"/>
<div
class="tab"
:class="tabIndex === 5 && 'active'"
@click="
() => {
tabIndex = 5
runtimeStore.isNew = false
set(APP_VERSION.key, APP_VERSION.version)
}
"
>
<IconFluentTextBulletListSquare20Regular width="20" />
<span>更新日志</span>
<div class="red-point" v-if="runtimeStore.isNew"></div>
</div>
<div class="tab" :class="tabIndex === 6 && 'active'" @click="tabIndex = 6">
<IconFluentPerson20Regular width="20"/>
<IconFluentPerson20Regular width="20" />
<span>关于</span>
</div>
</div>
</div>
<div class="col-line"></div>
<div class="flex-1 overflow-y-auto overflow-x-hidden pr-4 content">
<CommonSetting v-if="tabIndex === 0"/>
<WordSetting v-if="tabIndex === 1"/>
<ArticleSettting v-if="tabIndex === 2"/>
<div class="flex-1 overflow-y-auto overflow-x-hidden pr-4 content">
<CommonSetting v-if="tabIndex === 0" />
<WordSetting v-if="tabIndex === 1" />
<ArticleSettting v-if="tabIndex === 2" />
<div class="body" v-if="tabIndex === 3">
<div class="row">
@@ -348,10 +369,16 @@ function transferOk() {
<label class="item-title">{{ getShortcutKeyName(item[0]) }}</label>
<div class="wrapper" @click="editShortcutKey = item[0]">
<div class="set-key" v-if="editShortcutKey === item[0]">
<input ref="shortcutInput" :value="item[1]?item[1]:'未设置快捷键'" readonly type="text"
@blur="handleInputBlur">
<span @click.stop="editShortcutKey = ''">按键盘进行设置<span
class="text-red!">设置完成点击这里</span></span>
<input
ref="shortcutInput"
:value="item[1] ? item[1] : '未设置快捷键'"
readonly
type="text"
@blur="handleInputBlur"
/>
<span @click.stop="editShortcutKey = ''"
>按键盘进行设置<span class="text-red!">设置完成点击这里</span></span
>
</div>
<div v-else>
<div v-if="item[1]">{{ item[1] }}</div>
@@ -371,30 +398,41 @@ 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>
<BaseButton :loading="exportLoading" size="large" class="mt-3" @click="exportData()">导出数据备份(ZIP)</BaseButton>
<div class="text-gray text-sm mt-2">💾 导出的ZIP文件包含所有学习数据可在其他设备上导入恢复</div>
<div class="line mt-15 mb-3"></div>
<div>请注意导入数据将<b class="text-red"> 完全覆盖 </b>当前所有数据请谨慎操作执行导入操作时会先自动备份当前数据到您的电脑中供您随时恢复
<div>
请注意导入数据将<b class="text-red"> 完全覆盖 </b
>当前所有数据请谨慎操作执行导入操作时会先自动备份当前数据到您的电脑中供您随时恢复
</div>
<div class="flex gap-space mt-3">
<BaseButton size="large"
@click="beforeImport"
:loading="importLoading">导入数据恢复</BaseButton>
<input type="file"
id="import"
class="w-0 h-0 opacity-0"
accept="application/json,.zip,application/zip"
@change="importData">
<BaseButton size="large" @click="beforeImport" :loading="importLoading"
>导入数据恢复</BaseButton
>
<input
type="file"
id="import"
class="w-0 h-0 opacity-0"
accept="application/json,.zip,application/zip"
@change="importData"
/>
</div>
<template v-if="isNewHost">
<div class="line my-3"></div>
<div>请注意如果本地已有使用记录请先备份当前数据迁移数据后将<b class="text-red"> 完全覆盖 </b>当前所有数据请谨慎操作
<div>
请注意如果本地已有使用记录请先备份当前数据迁移数据后将<b class="text-red">
完全覆盖 </b
>当前所有数据请谨慎操作
</div>
<div class="flex gap-space mt-3">
<BaseButton @click="showTransfer = true">迁移 2study.top 网站数据</BaseButton>
@@ -403,33 +441,26 @@ function transferOk() {
</div>
<!-- 日志-->
<Log v-if="tabIndex === 5"/>
<Log v-if="tabIndex === 5" />
<div v-if="tabIndex === 6" class="center flex-col">
<About/>
<div class="text-md color-gray mt-10">
Build {{ gitLastCommitHash }}
</div>
<About />
<div class="text-md color-gray mt-10">Build {{ gitLastCommitHash }}</div>
</div>
</div>
</div>
</div>
</BasePage>
<MigrateDialog
v-model="showTransfer"
@ok="transferOk"
/>
<MigrateDialog v-model="showTransfer" @ok="transferOk" />
</template>
<style scoped lang="scss">
.col-line {
border-right: 2px solid gainsboro;
}
.setting {
.left {
display: flex;
flex-direction: column;
@@ -437,18 +468,18 @@ function transferOk() {
align-items: center;
.tabs {
padding: .6rem 0;
padding: 0.6rem 0;
display: flex;
flex-direction: column;
gap: .6rem;
gap: 0.6rem;
.tab {
@apply cursor-pointer flex items-center relative;
padding: .6rem .9rem;
border-radius: .5rem;
padding: 0.6rem 0.9rem;
border-radius: 0.5rem;
width: 10rem;
gap: .6rem;
transition: all .5s;
gap: 0.6rem;
transition: all 0.5s;
&:hover {
background: var(--btn-primary);
@@ -480,8 +511,6 @@ function transferOk() {
span {
text-align: right;
//width: 30rem;
font-size: .7rem;
color: gray;
}
@@ -491,17 +520,16 @@ function transferOk() {
input {
width: 9rem;
box-sizing: border-box;
margin-right: .6rem;
margin-right: 0.6rem;
height: 1.8rem;
outline: none;
font-size: 1rem;
border: 1px solid gray;
border-radius: .2rem;
padding: 0 .3rem;
border-radius: 0.2rem;
padding: 0 0.3rem;
background: var(--color-second);
color: var(--color-font-1);
}
}
}
@@ -515,7 +543,7 @@ function transferOk() {
}
.sub-title {
font-size: .9rem;
font-size: 0.9rem;
}
}
@@ -528,7 +556,7 @@ function transferOk() {
.scroll {
flex: 1;
padding-right: .6rem;
padding-right: 0.6rem;
overflow: auto;
}
@@ -590,7 +618,8 @@ function transferOk() {
}
// 补充:选择器和输入框优化
.base-select, .base-input {
.base-select,
.base-input {
width: 100% !important;
max-width: none;
}

View File

@@ -15,7 +15,7 @@ import Form from '@/components/base/form/Form.vue'
import FormItem from '@/components/base/form/FormItem.vue'
import Toast from '@/components/base/toast/Toast.ts'
import DeleteIcon from '@/components/icon/DeleteIcon.vue'
import { AppEnv, LIB_JS_URL, PracticeSaveWordKey, TourConfig } from '@/config/env.ts'
import { AppEnv, LIB_JS_URL, TourConfig } from '@/config/env.ts'
import { getCurrentStudyWord } from '@/hooks/dict.ts'
import EditBook from '@/pages/article/components/EditBook.vue'
import PracticeSettingDialog from '@/pages/word/components/PracticeSettingDialog.vue'
@@ -39,6 +39,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { wordDelete } from '@/apis/words.ts'
import { copyOfficialDict } from '@/apis/dict.ts'
import {PRACTICE_WORD_CACHE} from "@/utils/cache.ts";
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -289,7 +290,7 @@ const { nav } = useNav()
//todo 可以和首页合并
async function startPractice(query = {}) {
localStorage.removeItem(PracticeSaveWordKey.key)
localStorage.removeItem(PRACTICE_WORD_CACHE.key)
studyLoading = true
await base.changeDict(runtimeStore.editDict)
studyLoading = false

View File

@@ -47,14 +47,7 @@ import { getDefaultDict, getDefaultWord } from '@/types/func.ts'
import ConflictNotice from '@/components/ConflictNotice.vue'
import PracticeLayout from '@/components/PracticeLayout.vue'
import {
AppEnv,
DICT_LIST,
IS_DEV,
LIB_JS_URL,
PracticeSaveWordKey,
TourConfig,
} from '@/config/env.ts'
import { AppEnv, DICT_LIST, IS_DEV, LIB_JS_URL, TourConfig } from '@/config/env.ts'
import { ToastInstance } from '@/components/base/toast/type.ts'
import { watchOnce } from '@vueuse/core'
import { setUserDictProp } from '@/apis'
@@ -63,6 +56,7 @@ import OptionButton from '@/components/base/OptionButton.vue'
import Radio from '@/components/base/radio/Radio.vue'
import RadioGroup from '@/components/base/radio/RadioGroup.vue'
import GroupList from '@/pages/word/components/GroupList.vue'
import { getPracticeWordCache, PRACTICE_WORD_CACHE, setPracticeWordCache } from '@/utils/cache.ts'
const { isWordCollect, toggleWordCollect, isWordSimple, toggleWordSimple } = useWordOptions()
const settingStore = useSettingStore()
@@ -208,23 +202,12 @@ useStartKeyboardEventListener()
useDisableEventListener(() => loading)
function initData(initVal: TaskWords, init: boolean = false) {
let d = localStorage.getItem(PracticeSaveWordKey.key)
let d = getPracticeWordCache()
if (d && init) {
try {
//todo 记得删除
if (IS_DEV) {
throw new Error('开发环境,抛出错误跳过缓存')
}
let obj = JSON.parse(d)
let s = obj.val
taskWords = Object.assign(taskWords, s.taskWords)
//这里直接赋值的话provide后的inject获取不到最新值
data = Object.assign(data, s.practiceData)
statStore.$patch(s.statStoreData)
} catch (e) {
localStorage.removeItem(PracticeSaveWordKey.key)
initData(initVal, true)
}
taskWords = Object.assign(taskWords, d.taskWords)
//这里直接赋值的话provide后的inject获取不到最新值
data = Object.assign(data, d.practiceData)
statStore.$patch(d.statStoreData)
} else {
// taskWords = initVal
//不能直接赋值,会导致 inject 的数据为默认值
@@ -428,7 +411,7 @@ async function next(isTyping: boolean = true) {
console.log('自由模式,全完学完了')
showStatDialog = true
clearInterval(timer)
setTimeout(() => localStorage.removeItem(PracticeSaveWordKey.key), 300)
setTimeout(() => setPracticeWordCache(null), 300)
}
} else {
data.index++
@@ -466,7 +449,7 @@ async function next(isTyping: boolean = true) {
console.log('全完学完了')
showStatDialog = true
clearInterval(timer)
setTimeout(() => localStorage.removeItem(PracticeSaveWordKey.key), 300)
setTimeout(() => setPracticeWordCache(null), 300)
}
if (settingStore.wordPracticeMode === WordPracticeMode.System) {
@@ -557,17 +540,11 @@ function onTypeWrong() {
function savePracticeData() {
// console.log('savePracticeData')
localStorage.setItem(
PracticeSaveWordKey.key,
JSON.stringify({
version: PracticeSaveWordKey.version,
val: {
taskWords,
practiceData: data,
statStoreData: statStore.$state,
},
})
)
setPracticeWordCache({
taskWords,
practiceData: data,
statStoreData: statStore.$state,
})
}
watch(() => data.index, savePracticeData)

View File

@@ -75,10 +75,18 @@ watch(model, async newVal => {
})
//如果 shuffle 数组不为空,就说明是复习,不用修改 lastLearnIndex
//todo
if (settingStore.wordPracticeMode !== WordPracticeMode.Shuffle) {
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + store.sdict.perDayStudyNumber
if (store.sdict.lastLearnIndex >= store.sdict.length - 1) {
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
// 检查已忽略的单词数量,是否全部完成
let ignoreList = [store.allIgnoreWords, store.knownWords][
settingStore.ignoreSimpleWord ? 0 : 1
]
// 忽略单词数
const ignoreCount = ignoreList.filter(word =>
store.sdict.words.some(w => w.word.toLowerCase() === word)
).length
// 如果lastLearnIndex已经超过可学单词数则判定完成
if (store.sdict.lastLearnIndex + ignoreCount >= store.sdict.length) {
dictIsEnd = true
store.sdict.complete = true
store.sdict.lastLearnIndex = store.sdict.length

View File

@@ -29,19 +29,13 @@ import PracticeSettingDialog from '@/pages/word/components/PracticeSettingDialog
import ChangeLastPracticeIndexDialog from '@/pages/word/components/ChangeLastPracticeIndexDialog.vue'
import { useSettingStore } from '@/stores/setting.ts'
import { useFetch } from '@vueuse/core'
import {
AppEnv,
DICT_LIST,
Host,
LIB_JS_URL,
PracticeSaveWordKey,
TourConfig,
} from '@/config/env.ts'
import { AppEnv, DICT_LIST, Host, LIB_JS_URL, Origin, TourConfig } from '@/config/env.ts'
import { myDictList } from '@/apis'
import PracticeWordListDialog from '@/pages/word/components/PracticeWordListDialog.vue'
import ShufflePracticeSettingDialog from '@/pages/word/components/ShufflePracticeSettingDialog.vue'
import { deleteDict } from '@/apis/dict.ts'
import OptionButton from '@/components/base/OptionButton.vue'
import { getPracticeWordCache, setPracticeWordCache } from '@/utils/cache.ts'
const store = useBaseStore()
const settingStore = useSettingStore()
@@ -107,16 +101,10 @@ async function init() {
}
}
if (!currentStudy.new.length && store.sdict.words.length) {
let d = localStorage.getItem(PracticeSaveWordKey.key)
let d = getPracticeWordCache()
if (d) {
try {
let obj = JSON.parse(d)
currentStudy = obj.val.taskWords
isSaveData = true
} catch (e) {
localStorage.removeItem(PracticeSaveWordKey.key)
currentStudy = getCurrentStudyWord()
}
currentStudy = d.taskWords
isSaveData = true
} else {
currentStudy = getCurrentStudyWord()
}
@@ -124,19 +112,18 @@ async function init() {
loading = false
}
function startPractice(practiceMode?: WordPracticeMode): void {
function startPractice(practiceMode: WordPracticeMode, resetCache: boolean = false): void {
if (store.sdict.id) {
if (!store.sdict.words.length) {
Toast.warning('没有单词可学习!')
return
}
//todo 临时处理
localStorage.removeItem(PracticeSaveWordKey.key)
// 如果传入了独立模式,临时设置 wordPracticeMode
if (practiceMode !== undefined) {
settingStore.wordPracticeMode = practiceMode
if (resetCache) {
setPracticeWordCache(null)
}
settingStore.wordPracticeMode = practiceMode
window.umami?.track('startStudyWord', {
name: store.sdict.name,
index: store.sdict.lastLearnIndex,
@@ -154,6 +141,21 @@ function startPractice(practiceMode?: WordPracticeMode): void {
}
}
function freePractice() {
startPractice(
WordPracticeMode.Free,
settingStore.wordPracticeMode !== WordPracticeMode.Free && isSaveData
)
}
function systemPractice() {
startPractice(
settingStore.wordPracticeMode === WordPracticeMode.Free
? WordPracticeMode.System
: settingStore.wordPracticeMode,
settingStore.wordPracticeMode === WordPracticeMode.Free && isSaveData
)
}
let showPracticeSettingDialog = $ref(false)
let showShufflePracticeSettingDialog = $ref(false)
let showChangeLastPracticeIndexDialog = $ref(false)
@@ -220,7 +222,7 @@ function check(cb: Function) {
async function savePracticeSetting() {
Toast.success('修改成功')
isSaveData = false
localStorage.removeItem(PracticeSaveWordKey.key)
setPracticeWordCache(null)
await store.changeDict(runtimeStore.editDict)
currentStudy = getCurrentStudyWord()
}
@@ -235,7 +237,7 @@ async function onShufflePracticeSettingOk(total) {
complete: store.sdict.complete,
})
isSaveData = false
localStorage.removeItem(PracticeSaveWordKey.key)
setPracticeWordCache(null)
settingStore.wordPracticeMode = WordPracticeMode.Shuffle
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
currentStudy.shuffle = shuffle(
@@ -257,7 +259,7 @@ async function saveLastPracticeIndex(e) {
// runtimeStore.editDict.complete = e >= runtimeStore.editDict.length - 1
showChangeLastPracticeIndexDialog = false
isSaveData = false
localStorage.removeItem(PracticeSaveWordKey.key)
setPracticeWordCache(null)
await store.changeDict(runtimeStore.editDict)
currentStudy = getCurrentStudyWord()
}
@@ -267,14 +269,24 @@ const { data: recommendDictList, isFetching } = useFetch(
).json()
let isNewHost = $ref(window.location.host === Host)
const systemPracticeText = $computed(() => {
if (settingStore.wordPracticeMode === WordPracticeMode.Free) {
return '开始学习'
} else {
return isSaveData
? '继续' + WordPracticeModeNameMap[settingStore.wordPracticeMode]
: '开始' + WordPracticeModeNameMap[settingStore.wordPracticeMode]
}
})
</script>
<template>
<BasePage>
<div class="mb-4" v-if="!isNewHost">
新域名已启用后续请访问
<a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前 2study.top
域名将在不久后停止使用
<a class="mr-4" :href="`${Origin}/words?from_old_site=1`">{{ Origin }}</a
>当前 2study.top 域名将在不久后停止使用
</div>
<div class="card flex flex-col md:flex-row gap-4">
@@ -393,79 +405,91 @@ let isNewHost = $ref(window.location.host === Host)
</div>
</div>
<div class="flex items-end mt-4 gap-4 btn-no-margin">
<OptionButton class="flex-2">
<OptionButton
:class="
settingStore.wordPracticeMode !== WordPracticeMode.Free
? 'flex-1 orange-btn'
: 'primary-btn'
"
>
<BaseButton
size="large"
:type="settingStore.wordPracticeMode !== WordPracticeMode.Free ? 'orange' : 'primary'"
:disabled="!store.sdict.id"
:loading="loading"
@click="startPractice(settingStore.wordPracticeMode)"
@click="systemPractice"
>
<div class="flex items-center gap-2">
<span class="line-height-[2]">{{
isSaveData
? `继续${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`
: `开始${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`
}}</span>
<span class="line-height-[2]">{{ systemPracticeText }}</span>
<IconFluentArrowCircleRight16Regular class="text-xl" />
</div>
</BaseButton>
<template #options>
<BaseButton
class="w-23"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.System"
@click="startPractice(WordPracticeMode.System)"
class="w-full"
v-if="
settingStore.wordPracticeMode !== WordPracticeMode.System &&
settingStore.wordPracticeMode !== WordPracticeMode.Free
"
@click="startPractice(WordPracticeMode.System,true)"
>
{{ WordPracticeModeNameMap[WordPracticeMode.System] }}
智能学习
</BaseButton>
<BaseButton
class="w-full"
:disabled="!currentStudy.review.length && !currentStudy.write.length"
@click="startPractice(WordPracticeMode.Review,true)"
>
复习
</BaseButton>
<BaseButton
class="w-23"
class="w-full"
:disabled="store.sdict.lastLearnIndex < 10"
@click="check(() => (showShufflePracticeSettingDialog = true))"
>
随机复习
</BaseButton>
<BaseButton
class="w-full"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.IdentifyOnly"
@click="startPractice(WordPracticeMode.IdentifyOnly)"
@click="startPractice(WordPracticeMode.IdentifyOnly,true)"
>
{{ WordPracticeModeNameMap[WordPracticeMode.IdentifyOnly] }}
</BaseButton>
<BaseButton
class="w-23"
class="w-full"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.ListenOnly"
@click="startPractice(WordPracticeMode.ListenOnly)"
@click="startPractice(WordPracticeMode.ListenOnly,true)"
>
{{ WordPracticeModeNameMap[WordPracticeMode.ListenOnly] }}
</BaseButton>
<BaseButton
class="w-23"
class="w-full"
v-if="settingStore.wordPracticeMode !== WordPracticeMode.DictationOnly"
@click="startPractice(WordPracticeMode.DictationOnly)"
@click="startPractice(WordPracticeMode.DictationOnly,true)"
>
{{ WordPracticeModeNameMap[WordPracticeMode.DictationOnly] }}
</BaseButton>
</template>
</OptionButton>
<OptionButton class="flex-1" v-if="currentStudy.new.length">
<BaseButton
size="large"
:loading="loading"
@click="startPractice(WordPracticeMode.Review)"
>
复习
</BaseButton>
<template #options>
<BaseButton @click="check(() => (showShufflePracticeSettingDialog = true))">
随机复习
</BaseButton>
</template>
</OptionButton>
<BaseButton
v-else
:class="settingStore.wordPracticeMode === WordPracticeMode.Free ? 'flex-1' : ''"
:type="settingStore.wordPracticeMode === WordPracticeMode.Free ? 'orange' : 'primary'"
size="large"
@click="check(() => (showShufflePracticeSettingDialog = true))"
:loading="loading"
@click="freePractice()"
>
随机复习
</BaseButton>
<BaseButton size="large" :loading="loading" @click="startPractice(WordPracticeMode.Free)">
<div class="flex items-center gap-2">
<span class="line-height-[2]">自由练习</span>
<span class="line-height-[2]">
{{
settingStore.wordPracticeMode === WordPracticeMode.Free && isSaveData
? '继续自由练习'
: '自由练习'
}}
</span>
<IconStreamlineColorPenDrawFlat class="text-xl" />
</div>
</BaseButton>

View File

@@ -1,21 +1,26 @@
<script setup lang="ts">
import { ShortcutKey, Word, WordPracticeType } from "@/types/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.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 { onMounted, onUnmounted, 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, last } 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";
import { ShortcutKey, Word, WordPracticeStage, WordPracticeType } from '@/types/types.ts'
import VolumeIcon from '@/components/icon/VolumeIcon.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 { onMounted, onUnmounted, 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, last } 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,
word: Word
}
const props = withDefaults(defineProps<IProps>(), {
@@ -23,9 +28,9 @@ const props = withDefaults(defineProps<IProps>(), {
})
const emit = defineEmits<{
complete: [],
wrong: [],
know: [],
complete: []
wrong: []
know: []
}>()
let input = $ref('')
@@ -63,8 +68,8 @@ function updateCurrentWordInfo() {
word: props.word.word,
input: input,
inputLock: inputLock,
containsSpace: props.word.word.includes(' ')
};
containsSpace: props.word.word.includes(' '),
}
}
watch(() => props.word, reset, { deep: true })
@@ -73,25 +78,28 @@ function reset() {
wrong = input = ''
wordRepeatCount = 0
showWordResult = inputLock = false
wordCompletedTime = 0 // 重置时间戳
wordCompletedTime = 0 // 重置时间戳
if (settingStore.wordSound) {
if (settingStore.wordPracticeType !== WordPracticeType.Dictation) {
volumeIconRef?.play(400, true)
}
}
// 更新当前单词信息
updateCurrentWordInfo();
updateCurrentWordInfo()
checkCursorPosition()
}
// 监听输入变化,更新当前单词信息
watch(() => input, () => {
updateCurrentWordInfo();
})
watch(
() => input,
() => {
updateCurrentWordInfo()
}
)
onMounted(() => {
// 初始化当前单词信息
updateCurrentWordInfo();
updateCurrentWordInfo()
emitter.on(EventKey.resetWord, reset)
emitter.on(EventKey.onTyping, onTyping)
@@ -229,7 +237,7 @@ async function onTyping(e: KeyboardEvent) {
input += letter
wrong = ''
playKeyboardAudio()
updateCurrentWordInfo();
updateCurrentWordInfo()
inputLock = false
} else if (settingStore.wordPracticeType === WordPracticeType.Identify && !showWordResult) {
//当自测模式下按1和2会单独处理如果按其他键则自动默认为不认识
@@ -246,32 +254,34 @@ async function onTyping(e: KeyboardEvent) {
right = letter === word[input.length]
}
//针对中文的特殊判断
if (e.shiftKey && (
'' === word[input.length] && e.code === 'Digit1' ||
'' === word[input.length] && e.code === 'Digit4' ||
'' === word[input.length] && e.code === 'Digit6' ||
'' === word[input.length] && e.code === 'Digit9' ||
'' === word[input.length] && e.code === 'Minus' ||
'' === word[input.length] && e.code === 'Slash' ||
'' === word[input.length] && e.code === 'Period' ||
'' === word[input.length] && e.code === 'Comma' ||
'' === word[input.length] && e.code === 'Quote' ||
'' === word[input.length] && e.code === 'Semicolon' ||
'' === word[input.length] && e.code === 'Digit0')
if (
e.shiftKey &&
(('' === word[input.length] && e.code === 'Digit1') ||
('' === word[input.length] && e.code === 'Digit4') ||
('' === word[input.length] && e.code === 'Digit6') ||
('' === word[input.length] && e.code === 'Digit9') ||
('' === word[input.length] && e.code === 'Minus') ||
('' === word[input.length] && e.code === 'Slash') ||
('' === word[input.length] && e.code === 'Period') ||
('' === word[input.length] && e.code === 'Comma') ||
('' === word[input.length] && e.code === 'Quote') ||
('' === word[input.length] && e.code === 'Semicolon') ||
('' === word[input.length] && e.code === 'Digit0'))
) {
right = true
letter = word[input.length]
}
if (!e.shiftKey && (
'【' === word[input.length] && e.code === 'BracketLeft' ||
'' === word[input.length] && e.code === 'Slash' ||
'' === word[input.length] && e.code === 'Period' ||
'' === word[input.length] && e.code === 'Comma' ||
'' === word[input.length] && e.code === 'Quote' ||
'' === word[input.length] && e.code === 'Semicolon' ||
'' === word[input.length] && e.code === 'BracketLeft' ||
'' === word[input.length] && e.code === 'BracketRight'
)) {
if (
!e.shiftKey &&
(('' === word[input.length] && e.code === 'BracketLeft') ||
('' === word[input.length] && e.code === 'Slash') ||
('' === word[input.length] && e.code === 'Period') ||
('' === word[input.length] && e.code === 'Comma') ||
('' === word[input.length] && e.code === 'Quote') ||
('' === word[input.length] && e.code === 'Semicolon') ||
('' === word[input.length] && e.code === 'BracketLeft') ||
('】' === word[input.length] && e.code === 'BracketRight'))
) {
right = true
letter = word[input.length]
}
@@ -292,15 +302,24 @@ async function onTyping(e: KeyboardEvent) {
}, 500)
}
// 更新当前单词信息
updateCurrentWordInfo();
updateCurrentWordInfo()
//不需要把inputLock设为false输入完成不能再输入了只能删除删除会打开锁
if (input.toLowerCase() === word.toLowerCase()) {
wordCompletedTime = Date.now() // 记录单词完成的时间戳
wordCompletedTime = Date.now() // 记录单词完成的时间戳
playCorrect()
if ([WordPracticeType.Listen, WordPracticeType.Identify].includes(settingStore.wordPracticeType) && !showWordResult) {
if (
[WordPracticeType.Listen, WordPracticeType.Identify].includes(
settingStore.wordPracticeType
) &&
!showWordResult
) {
showWordResult = true
}
if ([WordPracticeType.FollowWrite, WordPracticeType.Spell].includes(settingStore.wordPracticeType)) {
if (
[WordPracticeType.FollowWrite, WordPracticeType.Spell].includes(
settingStore.wordPracticeType
)
) {
if (settingStore.autoNextWord) {
if (settingStore.repeatCount == 100) {
if (settingStore.repeatCustomCount <= wordRepeatCount + 1) {
@@ -337,7 +356,7 @@ function del() {
}
}
// 更新当前单词信息
updateCurrentWordInfo();
updateCurrentWordInfo()
}
function showWord() {
@@ -347,16 +366,8 @@ function showWord() {
}
showFullWord = true
//系统设定的默认模式情况下,如果看了单词统计到错词里面去
switch (statStore.step) {
case 1:
case 2:
case 4:
case 5:
case 7:
case 8:
case 10:
emit('wrong')
break
if (statStore.stage !== WordPracticeStage.FollowWriteNewWord) {
emit('wrong')
}
}
}
@@ -389,7 +400,7 @@ function hideWordInTranslation(text: string, word: string): string {
// 创建正则表达式,匹配单词本身及其常见变形(如复数、过去式等)
const wordBase = word.toLowerCase()
const patterns = [
`\\b${escapeRegExp(wordBase)}\\b`, // 单词本身
`\\b${escapeRegExp(wordBase)}\\b`, // 单词本身
`\\b${escapeRegExp(wordBase)}s\\b`, // 复数形式
`\\b${escapeRegExp(wordBase)}es\\b`, // 复数形式
`\\b${escapeRegExp(wordBase)}ed\\b`, // 过去式
@@ -416,32 +427,32 @@ watch([() => input, () => showFullWord, () => settingStore.dictation], checkCurs
function checkCursorPosition() {
_nextTick(() => {
// 选中目标元素
const cursorEl = document.querySelector(`.cursor`);
const inputList = document.querySelectorAll(`.l`);
if (!typingWordRef) return;
const typingWordRect = typingWordRef.getBoundingClientRect();
const cursorEl = document.querySelector(`.cursor`)
const inputList = document.querySelectorAll(`.l`)
if (!typingWordRef) return
const typingWordRect = typingWordRef.getBoundingClientRect()
if (inputList.length) {
let inputRect = last(Array.from(inputList)).getBoundingClientRect();
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 dictation = document.querySelector(`.dictation`);
const dictation = document.querySelector(`.dictation`)
let elRect
if (dictation) {
elRect = dictation.getBoundingClientRect();
elRect = dictation.getBoundingClientRect()
} else {
const letter = document.querySelector(`.letter`);
elRect = letter.getBoundingClientRect();
const letter = document.querySelector(`.letter`)
elRect = letter.getBoundingClientRect()
}
cursor = {
top: elRect.top + elRect.height - cursorEl.clientHeight - typingWordRect.top,
left: elRect.left - typingWordRect.left - 3,
};
}
}
},)
})
}
useEvents([
@@ -454,42 +465,73 @@ useEvents([
<div class="typing-word" ref="typingWordRef" v-if="word.word.length">
<div class="flex flex-col items-center">
<div class="flex gap-1 mt-30">
<div class="phonetic"
:class="!(!settingStore.dictation || showFullWord || showWordResult) && 'word-shadow'"
v-if="settingStore.soundType === 'uk' && word.phonetic0">[{{ word.phonetic0 }}]
<div
class="phonetic"
:class="!(!settingStore.dictation || showFullWord || showWordResult) && 'word-shadow'"
v-if="settingStore.soundType === 'uk' && word.phonetic0"
>
[{{ word.phonetic0 }}]
</div>
<div class="phonetic"
:class="((settingStore.dictation || [WordPracticeType.Spell,WordPracticeType.Listen,WordPracticeType.Dictation].includes(settingStore.wordPracticeType)) && !showFullWord && !showWordResult) && 'word-shadow'"
v-if="settingStore.soundType === 'us' && word.phonetic1">[{{ 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 === 'us' && 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>
<Tooltip
:title="(settingStore.dictation)
? `可以按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`
: ''
">
<div id="word" class="word my-1"
:class="wrong && 'is-wrong'"
:style="{fontSize: settingStore.fontSize.wordForeignFontSize +'px'}"
@mouseenter="showWord"
@mouseleave="mouseleave"
:title="
settingStore.dictation
? `可以按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`
: ''
"
>
<div
id="word"
class="word my-1"
:class="wrong && 'is-wrong'"
:style="{ fontSize: settingStore.fontSize.wordForeignFontSize + 'px' }"
@mouseenter="showWord"
@mouseleave="mouseleave"
>
<div v-if="settingStore.wordPracticeType === WordPracticeType.Dictation">
<div class="letter text-align-center w-full inline-block"
v-opacity="!settingStore.dictation || showWordResult || showFullWord">
<div
class="letter text-align-center w-full inline-block"
v-opacity="!settingStore.dictation || showWordResult || showFullWord"
>
{{ word.word }}
</div>
<div
class="mt-2 w-120 dictation"
:style="{minHeight: settingStore.fontSize.wordForeignFontSize +'px'}"
:class="showWordResult ? (right ? 'right' : 'wrong') : ''">
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"/>
<Space
class="l"
v-else
:is-wrong="showWordResult ? !right : false"
:is-wait="!showWordResult"
/>
</template>
</div>
</div>
@@ -497,47 +539,76 @@ useEvents([
<span class="input" v-if="input">{{ input }}</span>
<span class="wrong" v-if="wrong">{{ wrong }}</span>
<span class="letter" v-if="settingStore.dictation && !showFullWord">
{{ displayWord.split('').map((v) => (v === ' ' ? '&nbsp;' : '_')).join('') }}
</span>
{{
displayWord
.split('')
.map(v => (v === ' ' ? '&nbsp;' : '_'))
.join('')
}}
</span>
<span class="letter" v-else>{{ displayWord }}</span>
</template>
</div>
</Tooltip>
<div class="mt-4 flex gap-4"
v-if="settingStore.wordPracticeType === WordPracticeType.Identify && !showWordResult">
<div
class="mt-4 flex gap-4"
v-if="settingStore.wordPracticeType === WordPracticeType.Identify && !showWordResult"
>
<BaseButton
:keyboard="`快捷键(${settingStore.shortcutKeyMap[ShortcutKey.KnowWord]})`"
size="large" @click="know">我认识
:keyboard="`快捷键(${settingStore.shortcutKeyMap[ShortcutKey.KnowWord]})`"
size="large"
@click="know"
>我认识
</BaseButton>
<BaseButton
:keyboard="`快捷键(${settingStore.shortcutKeyMap[ShortcutKey.UnknownWord]})`"
size="large" @click="unknown">不认识
: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 || showWordResult || showFullWord"
:style="{
fontSize: settingStore.fontSize.wordTranslateFontSize +'px',
}"
<div
class="translate flex flex-col gap-2 my-3"
v-opacity="settingStore.translate || showWordResult || showFullWord"
:style="{
fontSize: settingStore.fontSize.wordTranslateFontSize + 'px',
}"
>
<div class="flex" v-for="v in word.trans">
<div class="shrink-0" :class="v.pos ? 'w-12 en-article-family' : '-ml-3'">{{ v.pos }}</div>
<div class="shrink-0" :class="v.pos ? 'w-12 en-article-family' : '-ml-3'">
{{ v.pos }}
</div>
<span v-if="!settingStore.dictation || showWordResult || showFullWord">{{ v.cn }}</span>
<span v-else v-html="hideWordInTranslation(v.cn, word.word)"></span>
</div>
</div>
</div>
<div class="other anim"
v-opacity="![WordPracticeType.Listen,WordPracticeType.Dictation,WordPracticeType.Identify].includes(settingStore.wordPracticeType) || showFullWord || showWordResult">
<div
class="other anim"
v-opacity="
![WordPracticeType.Listen, WordPracticeType.Dictation, WordPracticeType.Identify].includes(
settingStore.wordPracticeType
) ||
showFullWord ||
showWordResult
"
>
<div class="line-white my-3"></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 || showWordResult)"/>
<div class="text-base anim" v-opacity="settingStore.translate || showFullWord || showWordResult">
<SentenceHightLightWord
class="text-xl"
:text="item.c"
:word="word.word"
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"
/>
<div
class="text-base anim"
v-opacity="settingStore.translate || showFullWord || showWordResult"
>
{{ item.cn }}
</div>
</div>
@@ -550,9 +621,16 @@ useEvents([
<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 || showWordResult)"/>
<div class="cn anim" v-opacity="settingStore.translate || showFullWord || showWordResult">
<SentenceHightLightWord
class="en"
:text="item.c"
:word="word.word"
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"
/>
<div
class="cn anim"
v-opacity="settingStore.translate || showFullWord || showWordResult"
>
{{ item.cn }}
</div>
</div>
@@ -560,20 +638,26 @@ useEvents([
</div>
</template>
<template v-if="(settingStore.translate || !settingStore.dictation)">
<template v-if="settingStore.translate || !settingStore.dictation">
<template v-if="word?.synos?.length">
<div class="line-white my-3"></div>
<div class="flex">
<div class='label'>同近义词</div>
<div class="label">同近义词</div>
<div class="flex flex-col gap-3">
<div class="flex" v-for="item in word.synos">
<div class="pos line-height-1.4rem!">{{ item.pos }}</div>
<div>
<div class="cn anim" v-opacity="settingStore.translate || showFullWord || showWordResult">
<div
class="cn anim"
v-opacity="settingStore.translate || showFullWord || showWordResult"
>
{{ item.cn }}
</div>
<div class="anim" v-opacity="!settingStore.dictation || showFullWord || showWordResult">
<span class="en" v-for="(i,j) in item.ws">
<div
class="anim"
v-opacity="!settingStore.dictation || showFullWord || showWordResult"
>
<span class="en" v-for="(i, j) in item.ws">
{{ i }} {{ j !== item.ws.length - 1 ? ' / ' : '' }}
</span>
</div>
@@ -584,8 +668,12 @@ useEvents([
</template>
</template>
<div class="anim"
v-opacity="(settingStore.translate && !settingStore.dictation) || showFullWord || showWordResult">
<div
class="anim"
v-opacity="
(settingStore.translate && !settingStore.dictation) || showFullWord || showWordResult
"
>
<template v-if="word?.etymology?.length">
<div class="line-white my-3"></div>
@@ -622,8 +710,14 @@ useEvents([
</template>
</div>
</div>
<div class="cursor"
:style="{top:cursor.top+'px',left:cursor.left+'px',height: settingStore.fontSize.wordForeignFontSize +'px'}"></div>
<div
class="cursor"
:style="{
top: cursor.top + 'px',
left: cursor.left + 'px',
height: settingStore.fontSize.wordForeignFontSize + 'px',
}"
></div>
</div>
</template>
@@ -641,7 +735,8 @@ useEvents([
color: var(--color-font-2);
padding-bottom: 8rem;
.phonetic, .translate {
.phonetic,
.translate {
font-size: 1.2rem;
}
@@ -654,10 +749,10 @@ useEvents([
font-size: 3rem;
line-height: 1;
font-family: var(--en-article-family);
letter-spacing: .3rem;
letter-spacing: 0.3rem;
.input, .right {
.input,
.right {
color: rgb(22, 163, 74);
}
@@ -706,7 +801,6 @@ useEvents([
// 移动端适配
@media (max-width: 768px) {
.typing-word {
padding: 0 0.5rem 12rem;
@@ -716,7 +810,8 @@ useEvents([
margin: 0.5rem 0;
}
.phonetic, .translate {
.phonetic,
.translate {
font-size: 1rem;
}
@@ -792,7 +887,8 @@ useEvents([
margin: 0.3rem 0;
}
.phonetic, .translate {
.phonetic,
.translate {
font-size: 0.9rem;
}

View File

@@ -3,7 +3,6 @@ import { WordPracticeModeStageMap, WordPracticeStage, WordPracticeStageNameMap }
import { useSettingStore } from './setting'
export interface PracticeState {
step: number
stage: WordPracticeStage
startDate: number
spend: number
@@ -18,7 +17,6 @@ export interface PracticeState {
export const usePracticeStore = defineStore('practice', {
state: (): PracticeState => {
return {
step: 0,
stage: WordPracticeStage.FollowWriteNewWord,
spend: 0,
startDate: Date.now(),

View File

@@ -351,8 +351,8 @@ export const WordPracticeStageNameMap: Record<WordPracticeStage, string> = {
}
export const WordPracticeModeNameMap: Record<WordPracticeMode, string> = {
[WordPracticeMode.System]: '智能学习',
[WordPracticeMode.Free]: '自由',
[WordPracticeMode.System]: '学习',
[WordPracticeMode.Free]: '自由练习',
[WordPracticeMode.IdentifyOnly]: '自测',
[WordPracticeMode.DictationOnly]: '默写',
[WordPracticeMode.ListenOnly]: '听写',

88
src/utils/cache.ts Normal file
View File

@@ -0,0 +1,88 @@
import { Article, PracticeData, TaskWords } from '@/types/types.ts'
import { PracticeState } from '@/stores/practice.ts'
import { IS_DEV } from '@/config/env'
export const PRACTICE_WORD_CACHE = {
key: 'PracticeSaveWord',
version: 1,
}
export const PRACTICE_ARTICLE_CACHE = {
key: 'PracticeSaveArticle',
version: 1,
}
export type PracticeWordCache = {
taskWords: TaskWords
practiceData: PracticeData
statStoreData: PracticeState
}
export type PracticeArticleCache = {
article: Article
practiceData: PracticeData
statStoreData: PracticeState
}
export function getPracticeWordCache(): PracticeWordCache | null {
let d = localStorage.getItem(PRACTICE_WORD_CACHE.key)
if (d) {
try {
//todo 记得删除
if (IS_DEV) {
// throw new Error('开发环境,抛出错误跳过缓存')
}
let obj = JSON.parse(d)
if (obj.version !== PRACTICE_WORD_CACHE.version) {
throw new Error()
}
return obj.val
} catch (e) {
localStorage.removeItem(PRACTICE_WORD_CACHE.key)
}
}
return null
}
export function getPracticeArticleCache(): PracticeArticleCache | null {
let d = localStorage.getItem(PRACTICE_ARTICLE_CACHE.key)
if (d) {
try {
let obj = JSON.parse(d)
if (obj.version !== PRACTICE_ARTICLE_CACHE.version) {
throw new Error()
}
return obj.val
} catch (e) {
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
}
}
return null
}
export function setPracticeWordCache(cache: PracticeWordCache | null) {
if (cache) {
localStorage.setItem(
PRACTICE_WORD_CACHE.key,
JSON.stringify({
version: PRACTICE_WORD_CACHE.version,
val: cache,
})
)
} else {
localStorage.removeItem(PRACTICE_WORD_CACHE.key)
}
}
export function setPracticeArticleCache(cache: PracticeArticleCache | null) {
if (cache) {
localStorage.setItem(
PRACTICE_ARTICLE_CACHE.key,
JSON.stringify({
version: PRACTICE_ARTICLE_CACHE.version,
val: cache,
})
)
} else {
localStorage.removeItem(PRACTICE_ARTICLE_CACHE.key)
}
}