wip
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -87,7 +87,7 @@ watch(innerValue, () => {
|
||||
}, {immediate: true})
|
||||
|
||||
</script>
|
||||
<style>
|
||||
<style scoped lang="scss">
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 有空了改成实时保存
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 === ' ' ? ' ' : '_')).join('') }}
|
||||
</span>
|
||||
{{
|
||||
displayWord
|
||||
.split('')
|
||||
.map(v => (v === ' ' ? ' ' : '_'))
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
88
src/utils/cache.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user