This commit is contained in:
Zyronon
2026-01-06 23:26:07 +08:00
parent e1555d7b46
commit b9f6d89d76
57 changed files with 718 additions and 803 deletions

4
components.d.ts vendored
View File

@@ -93,7 +93,6 @@ declare module 'vue' {
IconFluentMyLocation20Regular: typeof import('~icons/fluent/my-location20-regular')['default']
IconFluentNumberSymbol20Regular: typeof import('~icons/fluent/number-symbol20-regular')['default']
IconFluentPaddingLeft20Regular: typeof import('~icons/fluent/padding-left20-regular')['default']
IconFluentPen20Regular: typeof import('~icons/fluent/pen20-regular')['default']
IconFluentPerson20Regular: typeof import('~icons/fluent/person20-regular')['default']
IconFluentPhone20Regular: typeof import('~icons/fluent/phone20-regular')['default']
IconFluentPlay20Regular: typeof import('~icons/fluent/play20-regular')['default']
@@ -116,7 +115,6 @@ declare module 'vue' {
IconFluentTextBulletListSquare20Regular: typeof import('~icons/fluent/text-bullet-list-square20-regular')['default']
IconFluentTextEditStyle20Regular: typeof import('~icons/fluent/text-edit-style20-regular')['default']
IconFluentTextListAbcUppercaseLtr20Regular: typeof import('~icons/fluent/text-list-abc-uppercase-ltr20-regular')['default']
IconFluentTextParagraph16Regular: typeof import('~icons/fluent/text-paragraph16-regular')['default']
IconFluentTextPositionThrough20Regular: typeof import('~icons/fluent/text-position-through20-regular')['default']
IconFluentTextUnderlineDouble20Regular: typeof import('~icons/fluent/text-underline-double20-regular')['default']
IconFluentTranslate16Regular: typeof import('~icons/fluent/translate16-regular')['default']
@@ -124,7 +122,6 @@ declare module 'vue' {
IconFluentWeatherMoon16Regular: typeof import('~icons/fluent/weather-moon16-regular')['default']
IconFluentWeatherSunny16Regular: typeof import('~icons/fluent/weather-sunny16-regular')['default']
IconIconParkOutlineAddMusic: typeof import('~icons/icon-park-outline/add-music')['default']
IconIconParkOutlineVolumeNotice: typeof import('~icons/icon-park-outline/volume-notice')['default']
IconIxWechatLogo: typeof import('~icons/ix/wechat-logo')['default']
IconMaterialSymbolsMail: typeof import('~icons/material-symbols/mail')['default']
IconMdiSparkles: typeof import('~icons/mdi/sparkles')['default']
@@ -172,7 +169,6 @@ declare module 'vue' {
WeChat: typeof import('./src/components/ChannelIcons/WeChat.vue')['default']
WordItem: typeof import('./src/components/WordItem.vue')['default']
WordList: typeof import('./src/components/list/WordList.vue')['default']
WordList2: typeof import('./src/components/list/WordList2.vue')['default']
WordSetting: typeof import('./src/components/setting/WordSetting.vue')['default']
}
}

View File

@@ -1,24 +1,23 @@
<script setup lang="ts">
import {onMounted, watch} from "vue";
import {BaseState, useBaseStore} from "@/stores/base.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
import useTheme from "@/hooks/theme.ts";
import {loadJsLib, shakeCommonDict} from "@/utils";
import {get, set} from 'idb-keyval'
import { onMounted, watch } from 'vue'
import { BaseState, useBaseStore } from '@/stores/base.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useSettingStore } from '@/stores/setting.ts'
import useTheme from '@/hooks/theme.ts'
import { loadJsLib, shakeCommonDict } from '@/utils'
import { get, set } from 'idb-keyval'
import {useRoute} from "vue-router";
import {DictId} from "@/types/types.ts";
import {APP_VERSION, AppEnv, LOCAL_FILE_KEY, Origin, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/config/env.ts";
import {syncSetting} from "@/apis";
import {useUserStore} from "@/stores/user.ts";
import MigrateDialog from "@/components/MigrateDialog.vue";
import { useRoute } from 'vue-router'
import { APP_VERSION, AppEnv, DictId, LOCAL_FILE_KEY, Origin, SAVE_DICT_KEY, SAVE_SETTING_KEY } from '@/config/env.ts'
import { syncSetting } from '@/apis'
import { useUserStore } from '@/stores/user.ts'
import MigrateDialog from '@/components/MigrateDialog.vue'
const store = useBaseStore()
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
const userStore = useUserStore()
const {setTheme} = useTheme()
const { setTheme } = useTheme()
let lastAudioFileIdList = []
let isInitializing = true // 标记是否正在初始化
@@ -26,22 +25,24 @@ watch(store.$state, (n: BaseState) => {
// 如果正在初始化,不保存数据,避免覆盖
if (isInitializing) return
let data = shakeCommonDict(n)
set(SAVE_DICT_KEY.key, JSON.stringify({val: data, version: SAVE_DICT_KEY.version}))
set(SAVE_DICT_KEY.key, JSON.stringify({ val: data, version: SAVE_DICT_KEY.version }))
//筛选自定义和收藏
let bookList = data.article.bookList.filter(v => v.custom || [DictId.articleCollect].includes(v.id))
let audioFileIdList = []
bookList.forEach(v => {
//筛选 audioFileId 字体有值的
v.articles.filter(s => !s.audioSrc && s.audioFileId).forEach(a => {
//所有 id 存起来下次直接判断字符串是否相等因为这个watch会频繁调用
audioFileIdList.push(a.audioFileId)
})
v.articles
.filter(s => !s.audioSrc && s.audioFileId)
.forEach(a => {
//所有 id 存起来下次直接判断字符串是否相等因为这个watch会频繁调用
audioFileIdList.push(a.audioFileId)
})
})
if (audioFileIdList.toString() !== lastAudioFileIdList.toString()) {
let result = []
//删除未使用到的文件
get(LOCAL_FILE_KEY).then((fileList: Array<{ id: string, file: Blob }>) => {
get(LOCAL_FILE_KEY).then((fileList: Array<{ id: string; file: Blob }>) => {
if (fileList && fileList.length > 0) {
audioFileIdList.forEach(a => {
let item = fileList.find(b => b.id === a)
@@ -54,13 +55,17 @@ watch(store.$state, (n: BaseState) => {
}
})
watch(() => settingStore.$state, (n) => {
if (isInitializing) return
set(SAVE_SETTING_KEY.key, JSON.stringify({val: n, version: SAVE_SETTING_KEY.version}))
if (AppEnv.CAN_REQUEST) {
syncSetting(null, settingStore.$state)
}
}, {deep: true})
watch(
() => settingStore.$state,
n => {
if (isInitializing) return
set(SAVE_SETTING_KEY.key, JSON.stringify({ val: n, version: SAVE_SETTING_KEY.version }))
if (AppEnv.CAN_REQUEST) {
syncSetting(null, settingStore.$state)
}
},
{ deep: true }
)
async function init() {
isInitializing = true // 开始初始化
@@ -76,10 +81,10 @@ async function init() {
set(APP_VERSION.key, APP_VERSION.version)
} else {
get(APP_VERSION.key).then(r => {
runtimeStore.isNew = r ? (APP_VERSION.version > Number(r)) : true
runtimeStore.isNew = r ? APP_VERSION.version > Number(r) : true
})
}
window.umami?.track('host', {host: window.location.host})
window.umami?.track('host', { host: window.location.host })
}
onMounted(init)
@@ -88,7 +93,7 @@ onMounted(init)
let showTransfer = $ref(false)
onMounted(() => {
if (new URLSearchParams(window.location.search).get('from_old_site') === '1' && location.origin === Origin) {
if (localStorage.getItem('__migrated_from_2study_top__')) return;
if (localStorage.getItem('__migrated_from_2study_top__')) return
setTimeout(() => {
showTransfer = true
}, 1000)
@@ -127,8 +132,5 @@ onMounted(() => {
<!-- </transition>-->
<!-- </router-view>-->
<router-view></router-view>
<MigrateDialog
v-model="showTransfer"
@ok="init"
/>
<MigrateDialog v-model="showTransfer" @ok="init" />
</template>

View File

@@ -1,5 +1,5 @@
import http from '@/utils/http.ts'
import { Dict } from '@/types/types.ts'
import type { Dict } from '@/types/types.ts'
export function copyOfficialDict(params?, data?) {
return http<Dict>('dict/copyOfficialDict', data, params, 'post')

View File

@@ -1,5 +1,5 @@
import http, {axiosInstance, AxiosResponse} from "@/utils/http.ts";
import { Dict } from "@/types/types.ts";
import type { Dict } from "@/types/types.ts";
import { cloneDeep } from "@/utils";
function remove(data?: any) {

View File

@@ -1,5 +1,6 @@
import http from '@/utils/http.ts'
import { CodeType } from "@/types/types.ts";
import {CodeType} from "@/types/enum.ts";
// 用户登录接口
export interface LoginParams {

View File

@@ -1,5 +1,5 @@
import http from '@/utils/http.ts'
import { Dict } from '@/types/types.ts'
import type { Dict } from '@/types/types.ts'
export function wordDelete(params?, data?) {
return http<Dict>('word/delete', data, params, 'post')

View File

@@ -1,6 +1,5 @@
<script setup lang="tsx">
import { nextTick, onMounted, useSlots } from 'vue'
import { Sort } from '@/types/types.ts'
import MiniDialog from '@/components/dialog/MiniDialog.vue'
import BaseIcon from '@/components/BaseIcon.vue'
import BaseButton from '@/components/BaseButton.vue'
@@ -13,6 +12,7 @@ import DeleteIcon from '@/components/icon/DeleteIcon.vue'
import Dialog from '@/components/dialog/Dialog.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import { Host } from '@/config/env.ts'
import { Sort } from '@/types/enum.ts'
const props = withDefaults(
defineProps<{

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Dict } from "@/types/types.ts";
import type { Dict } from "@/types/types.ts";
import Progress from '@/components/base/Progress.vue'
import Checkbox from "@/components/base/checkbox/Checkbox.vue";

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import {computed, provide} from "vue"
import {ShortcutKey} from "@/types/types.ts"
import {useSettingStore} from "@/stores/setting.ts";
import Close from "@/components/icon/Close.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import {ShortcutKey} from "@/types/enum.ts";
const settingStore = useSettingStore()
let tabIndex = $ref(0)

View File

@@ -1,46 +1,41 @@
<script setup lang="ts">
import type { Word } from '@/types/types.ts'
import VolumeIcon from '@/components/icon/VolumeIcon.vue'
import { usePlayWordAudio } from '@/hooks/sound.ts'
import Tooltip from '@/components/base/Tooltip.vue'
import BaseIcon from '@/components/BaseIcon.vue'
import { useWordOptions } from '@/hooks/dict.ts'
import { Word } from "@/types/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import { usePlayWordAudio } from "@/hooks/sound.ts";
import Tooltip from "@/components/base/Tooltip.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import { useWordOptions } from "@/hooks/dict.ts";
withDefaults(defineProps<{
item: Word,
showTranslate?: boolean
showWord?: boolean
showTransPop?: boolean
showOption?: boolean
showCollectIcon?: boolean
showMarkIcon?: boolean
index?: number
active?: boolean
}>(), {
showTranslate: true,
showWord: true,
showTransPop: true,
showOption: true,
showCollectIcon: true,
showMarkIcon: true,
active: false,
})
withDefaults(
defineProps<{
item: Word
showTranslate?: boolean
showWord?: boolean
showTransPop?: boolean
showOption?: boolean
showCollectIcon?: boolean
showMarkIcon?: boolean
index?: number
active?: boolean
}>(),
{
showTranslate: true,
showWord: true,
showTransPop: true,
showOption: true,
showCollectIcon: true,
showMarkIcon: true,
active: false,
}
)
const playWordAudio = usePlayWordAudio()
const {
isWordCollect,
toggleWordCollect,
isWordSimple,
toggleWordSimple
} = useWordOptions()
const { isWordCollect, toggleWordCollect, isWordSimple, toggleWordSimple } = useWordOptions()
</script>
<template>
<div class="common-list-item"
:class="{active}"
>
<div class="common-list-item" :class="{ active }">
<div class="left">
<slot name="prefix" :item="item"></slot>
<div class="title-wrapper">
@@ -52,10 +47,7 @@ const {
</div>
<div class="item-sub-title flex flex-col gap-2" v-if="item.trans.length && showTranslate">
<div v-for="v in item.trans">
<Tooltip
v-if="v.cn.length > 30 && showTransPop"
:title="v.pos + ' ' + v.cn"
>
<Tooltip v-if="v.cn.length > 30 && showTransPop" :title="v.pos + ' ' + v.cn">
<span>{{ v.pos + ' ' + v.cn.slice(0, 30) + '...' }}</span>
</Tooltip>
<span v-else>{{ v.pos + ' ' + v.cn }}</span>
@@ -66,26 +58,26 @@ const {
<div class="right" v-if="showOption">
<slot name="suffix" :item="item"></slot>
<BaseIcon
v-if="showCollectIcon"
:class="!isWordCollect(item)?'collect':'fill'"
@click.stop="toggleWordCollect(item)"
:title="!isWordCollect(item) ? '收藏' : '取消收藏'">
<IconFluentStar16Regular v-if="!isWordCollect(item)"/>
<IconFluentStar16Filled v-else/>
v-if="showCollectIcon"
:class="!isWordCollect(item) ? 'collect' : 'fill'"
@click.stop="toggleWordCollect(item)"
:title="!isWordCollect(item) ? '收藏' : '取消收藏'"
>
<IconFluentStar16Regular v-if="!isWordCollect(item)" />
<IconFluentStar16Filled v-else />
</BaseIcon>
<BaseIcon
v-if="showMarkIcon"
:class="!isWordSimple(item)?'collect':'fill'"
@click.stop="toggleWordSimple(item)"
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'">
<IconFluentCheckmarkCircle16Regular v-if="!isWordSimple(item)"/>
<IconFluentCheckmarkCircle16Filled v-else/>
v-if="showMarkIcon"
:class="!isWordSimple(item) ? 'collect' : 'fill'"
@click.stop="toggleWordSimple(item)"
:title="!isWordSimple(item) ? '标记为已掌握' : '取消标记已掌握'"
>
<IconFluentCheckmarkCircle16Regular v-if="!isWordSimple(item)" />
<IconFluentCheckmarkCircle16Filled v-else />
</BaseIcon>
</div>
</div>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Article } from '@/types/types.ts'
import type { Article } from '@/types/types.ts'
import BaseList from '@/components/list/BaseList.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import { useArticleOptions } from '@/hooks/dict.ts'

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import {watch} from "vue";
import {DictResource} from "@/types/types.ts";
import type {DictResource} from "@/types/types.ts";
import DictList from "@/components/list/DictList.vue";
const props = defineProps<{
@@ -69,16 +69,16 @@ watch(() => props.groupByTag, () => {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
.category {
font-size: 1rem;
font-weight: bold;
}
.tags {
margin: 0.5rem 0;
gap: 0.3rem;
.tag {
padding: 0.3rem 0.8rem;
font-size: 0.9rem;
@@ -98,7 +98,7 @@ watch(() => props.groupByTag, () => {
.category {
font-size: 0.9rem;
}
.tags {
.tag {
padding: 0.2rem 0.6rem;

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import {Dict} from "@/types/types.ts";
import type {Dict} from "@/types/types.ts";
import Book from "@/components/Book.vue";
defineProps<{
@@ -38,7 +38,7 @@ const emit = defineEmits<{
@media (max-width: 768px) {
.flex.gap-4.flex-wrap {
gap: 0.5rem;
.book {
width: 5rem;
height: calc(5rem * 1.4);
@@ -46,33 +46,33 @@ const emit = defineEmits<{
cursor: pointer;
position: relative;
z-index: 10;
.text-base {
font-size: 0.8rem;
line-height: 1.2;
word-break: break-word;
margin-bottom: 0.2rem;
}
.text-sm {
font-size: 0.7rem;
line-height: 1.1;
margin-bottom: 0.3rem;
}
.absolute.bottom-4.right-3 {
bottom: 0.8rem;
right: 0.3rem;
font-size: 0.7rem;
line-height: 1;
}
.absolute.bottom-2.left-3.right-3 {
bottom: 0.2rem;
left: 0.3rem;
right: 0.3rem;
}
.absolute.left-3.bottom-3 {
left: 0.3rem;
bottom: 0.3rem;
@@ -85,22 +85,22 @@ const emit = defineEmits<{
@media (max-width: 480px) {
.flex.gap-4.flex-wrap {
gap: 0.3rem;
.book {
width: 4.5rem;
height: calc(4.5rem * 1.4);
padding: 0.4rem;
.text-base {
font-size: 0.7rem;
line-height: 1.1;
}
.text-sm {
font-size: 0.6rem;
line-height: 1;
}
.absolute.bottom-4.right-3 {
font-size: 0.6rem;
}

View File

@@ -2,7 +2,7 @@
import BaseIcon from "@/components/BaseIcon.vue";
import { cloneDeep, throttle } from "@/utils";
import { Article } from "@/types/types.ts";
import type { Article } from "@/types/types.ts";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import BaseInput from "@/components/base/BaseInput.vue";

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import BaseList from "@/components/list/BaseList.vue";
import { Word } from "@/types/types.ts";
import type { Word } from "@/types/types.ts";
import WordItem from "../WordItem.vue";
withDefaults(defineProps<{

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { ShortcutKey } from '@/types/types.ts'
import { SoundFileOptions } from '@/config/env.ts'
import { getAudioFileUrl, usePlayAudio } from '@/hooks/sound.ts'
import Switch from '@/components/base/Switch.vue'
@@ -10,6 +9,7 @@ import Slider from '@/components/base/Slider.vue'
import SettingItem from '@/pages/setting/SettingItem.vue'
import { useSettingStore } from '@/stores/setting.ts'
import { useBaseStore } from '@/stores/base.ts'
import {ShortcutKey} from "@/types/enum.ts";
const settingStore = useSettingStore()
const store = useBaseStore()

View File

@@ -9,7 +9,8 @@ import {
slideTouchMove,
slideTouchStart
} from "./common";
import {SlideType} from "@/types/types.ts";
import {SlideType} from "@/config/env";
const props = defineProps({
index: {

View File

@@ -1,7 +1,7 @@
import {emitter as bus} from "@/utils/eventBus.ts";
import Utils from '@/utils/gm.js'
import {SlideType} from "@/types/types.ts";
import GM from "@/utils/gm.js";
import {SlideType} from "@/config/env";
export function slideInit(el, state, type) {
state.wrapper.width = GM.$getCss(el, 'width')

View File

@@ -1,4 +1,5 @@
import { offset } from '@floating-ui/dom'
import { ShortcutKey, WordPracticeMode, WordPracticeStage } from '@/types/enum.ts'
export const GITHUB = 'https://github.com/zyronon/TypeWords'
export const Host = 'typewords.cc'
@@ -93,3 +94,103 @@ export const LIB_JS_URL = {
JSZIP: `${Origin}/libs/jszip.min.js`,
XLSX: `${Origin}/libs/xlsx.full.min.js`,
}
export const PronunciationApi = 'https://dict.youdao.com/dictvoice?audio='
export const DefaultShortcutKeyMap = {
[ShortcutKey.EditArticle]: 'Ctrl+E',
[ShortcutKey.ShowWord]: 'Escape',
[ShortcutKey.Previous]: 'Alt+⬅',
[ShortcutKey.Next]: 'Tab',
[ShortcutKey.ToggleSimple]: '`',
[ShortcutKey.ToggleCollect]: 'Enter',
[ShortcutKey.PreviousChapter]: 'Ctrl+⬅',
[ShortcutKey.NextChapter]: 'Ctrl+➡',
[ShortcutKey.RepeatChapter]: 'Ctrl+Enter',
[ShortcutKey.DictationChapter]: 'Alt+Enter',
[ShortcutKey.PlayWordPronunciation]: 'Ctrl+P',
[ShortcutKey.ToggleShowTranslate]: 'Ctrl+Z',
[ShortcutKey.ToggleDictation]: 'Ctrl+I',
[ShortcutKey.ToggleTheme]: 'Ctrl+Q',
[ShortcutKey.ToggleConciseMode]: 'Ctrl+M',
[ShortcutKey.TogglePanel]: 'Ctrl+L',
[ShortcutKey.RandomWrite]: 'Ctrl+R',
[ShortcutKey.KnowWord]: '1',
[ShortcutKey.UnknownWord]: '2',
}
export const SlideType = {
HORIZONTAL: 0,
VERTICAL: 1,
}
export const WordPracticeModeStageMap: Record<WordPracticeMode, WordPracticeStage[]> = {
[WordPracticeMode.Free]: [WordPracticeStage.FollowWriteNewWord, WordPracticeStage.Complete],
[WordPracticeMode.IdentifyOnly]: [
WordPracticeStage.IdentifyNewWord,
WordPracticeStage.IdentifyReview,
WordPracticeStage.IdentifyReviewAll,
WordPracticeStage.Complete,
],
[WordPracticeMode.DictationOnly]: [
WordPracticeStage.DictationNewWord,
WordPracticeStage.DictationReview,
WordPracticeStage.DictationReviewAll,
WordPracticeStage.Complete,
],
[WordPracticeMode.ListenOnly]: [
WordPracticeStage.ListenNewWord,
WordPracticeStage.ListenReview,
WordPracticeStage.ListenReviewAll,
WordPracticeStage.Complete,
],
[WordPracticeMode.System]: [
WordPracticeStage.FollowWriteNewWord,
WordPracticeStage.ListenNewWord,
WordPracticeStage.DictationNewWord,
WordPracticeStage.IdentifyReview,
WordPracticeStage.ListenReview,
WordPracticeStage.DictationReview,
WordPracticeStage.IdentifyReviewAll,
WordPracticeStage.ListenReviewAll,
WordPracticeStage.DictationReviewAll,
WordPracticeStage.Complete,
],
[WordPracticeMode.Shuffle]: [WordPracticeStage.Shuffle, WordPracticeStage.Complete],
[WordPracticeMode.Review]: [
WordPracticeStage.IdentifyReview,
WordPracticeStage.ListenReview,
WordPracticeStage.DictationReview,
WordPracticeStage.IdentifyReviewAll,
WordPracticeStage.ListenReviewAll,
WordPracticeStage.DictationReviewAll,
WordPracticeStage.Complete,
],
}
export const WordPracticeStageNameMap: Record<WordPracticeStage, string> = {
[WordPracticeStage.FollowWriteNewWord]: '跟写新词',
[WordPracticeStage.IdentifyNewWord]: '自测新词',
[WordPracticeStage.ListenNewWord]: '听写新词',
[WordPracticeStage.DictationNewWord]: '默写新词',
[WordPracticeStage.FollowWriteReview]: '跟写上次学习',
[WordPracticeStage.IdentifyReview]: '自测上次学习',
[WordPracticeStage.ListenReview]: '听写上次学习',
[WordPracticeStage.DictationReview]: '默写上次学习',
[WordPracticeStage.FollowWriteReviewAll]: '跟写之前学习',
[WordPracticeStage.IdentifyReviewAll]: '自测之前学习',
[WordPracticeStage.ListenReviewAll]: '听写之前学习',
[WordPracticeStage.DictationReviewAll]: '默写之前学习',
[WordPracticeStage.Complete]: '完成学习',
[WordPracticeStage.Shuffle]: '随机复习',
}
export const WordPracticeModeNameMap: Record<WordPracticeMode, string> = {
[WordPracticeMode.System]: '学习',
[WordPracticeMode.Free]: '自由练习',
[WordPracticeMode.IdentifyOnly]: '自测',
[WordPracticeMode.DictationOnly]: '默写',
[WordPracticeMode.ListenOnly]: '听写',
[WordPracticeMode.Shuffle]: '随机复习',
[WordPracticeMode.Review]: '复习',
}
export class DictId {
static wordCollect = 'wordCollect'
static wordWrong = 'wordWrong'
static wordKnown = 'wordKnown'
static articleCollect = 'articleCollect'
}

View File

@@ -1,4 +1,4 @@
import { Article, DictId, PracticeArticleWordType, Sentence } from "@/types/types.ts"
import type { Article, Sentence } from "@/types/types.ts"
import { _nextTick, cloneDeep } from "@/utils"
import { usePlayWordAudio } from "@/hooks/sound.ts"
import { getSentenceAllText, getSentenceAllTranslateText } from "@/hooks/translate.ts"
@@ -7,6 +7,8 @@ import { useSettingStore } from "@/stores/setting.ts"
import { useBaseStore } from "@/stores/base.ts"
import { useRuntimeStore } from "@/stores/runtime.ts"
import { nanoid } from 'nanoid'
import {PracticeArticleWordType} from "@/types/enum.ts";
import { DictId } from '@/config/env.ts'
function parseSentence(sentence: string) {
// 先统一一些常见的“智能引号” -> 直引号,避免匹配问题

View File

@@ -1,13 +1,14 @@
import { Article, Dict, DictId, DictType, TaskWords, Word } from '@/types/types.ts'
import type { Article, Dict, TaskWords, Word } from '@/types/types.ts'
import { useBaseStore } from '@/stores/base.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { getDefaultDict, getDefaultWord } from '@/types/func.ts'
import { _getDictDataByUrl, cloneDeep, getRandomN, resourceWrap, shuffle, sleep, splitIntoN } from '@/utils'
import { onMounted, ref, watch } from 'vue'
import { AppEnv, DICT_LIST } from '@/config/env.ts'
import { AppEnv, DICT_LIST, DictId } from '@/config/env.ts'
import { detail } from '@/apis'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useRoute, useRouter } from 'vue-router'
import { DictType } from '@/types/enum.ts'
export function useWordOptions() {
const store = useBaseStore()

View File

@@ -1,8 +1,7 @@
import {onMounted, watchEffect} from "vue"
import {useSettingStore} from "@/stores/setting.ts";
import {PronunciationApi} from "@/types/types.ts";
import {SoundFileOptions} from "@/config/env.ts";
import { PronunciationApi, SoundFileOptions } from '@/config/env.ts'
export function useSound(audioSrcList?: string[], audioFileLength?: number) {
let audioList: HTMLAudioElement[] = $ref([])
@@ -24,7 +23,7 @@ export function useSound(audioSrcList?: string[], audioFileLength?: number) {
}
function play(volume: number = 100) {
console.log('play',audioList)
console.log('play', audioList)
index++
if (audioList.length > 1 && audioList.length !== audioLength) {
audioList[index % audioList.length].volume = volume / 100
@@ -35,7 +34,7 @@ export function useSound(audioSrcList?: string[], audioFileLength?: number) {
}
}
return {play, setAudio}
return { play, setAudio }
}

View File

@@ -1,6 +1,7 @@
import {Article, Sentence, TranslateEngine} from "@/types/types.ts";
import type {Article, Sentence} from "@/types/types.ts";
import Baidu from "@/libs/translate/baidu";
import {Translator} from "@/libs/translate/translator/index.ts";
import {TranslateEngine} from "@/types/enum.ts";
export function getSentenceAllTranslateText(article: Article) {
return article.sections.map(v => v.map(s => s.translate.trim()).filter(v => v).join(' \n')).filter(v => v).join(' \n\n');

View File

@@ -13,7 +13,7 @@ import { useBaseStore } from '@/stores/base.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { getDefaultDict } from '@/types/func.ts'
import { DictResource, DictType } from '@/types/types.ts'
import type { DictResource } from '@/types/types.ts'
import {
_getDictDataByUrl,
_nextTick,
@@ -31,6 +31,7 @@ import isBetween from 'dayjs/plugin/isBetween'
import isoWeek from 'dayjs/plugin/isoWeek'
import { watch } from 'vue'
import { useRouter } from 'vue-router'
import { DictType } from '@/types/enum.ts'
dayjs.extend(isoWeek)
dayjs.extend(isBetween)

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Article, DictId } from '@/types/types.ts'
import type { Article } from '@/types/types.ts'
import BaseButton from '@/components/BaseButton.vue'
import { _nextTick, cloneDeep, loadJsLib } from '@/utils'
import { useBaseStore } from '@/stores/base.ts'
@@ -15,7 +15,7 @@ import { getDefaultArticle } from '@/types/func.ts'
import BackIcon from '@/components/BackIcon.vue'
import MiniDialog from '@/components/dialog/MiniDialog.vue'
import { onMounted } from 'vue'
import { LIB_JS_URL, Origin } from '@/config/env.ts'
import { DictId, LIB_JS_URL, Origin } from '@/config/env.ts'
import { syncBookInMyStudyList } from '@/hooks/article.ts'
const base = useBaseStore()
@@ -254,13 +254,13 @@ function updateList(e) {
@select-item="selectArticle"
>
<template v-slot="{ item, index }">
<div>
<div class="name">
<span class="text-sm text-gray-500" v-if="index != undefined"> {{ index + 1 }}. </span>
{{ item.title }}
</div>
<div class="translate-name">{{ ` ${item.titleTranslate}` }}</div>
</div>
<div>
<div class="name">
<span class="text-sm text-gray-500" v-if="index != undefined"> {{ index + 1 }}. </span>
{{ item.title }}
</div>
<div class="translate-name">{{ ` ${item.titleTranslate}` }}</div>
</div>
</template>
</List>
<div class="add" v-if="!article.title">正在添加新文章...</div>

View File

@@ -3,7 +3,7 @@ import BackIcon from '@/components/BackIcon.vue'
import Empty from '@/components/Empty.vue'
import ArticleList from '@/components/list/ArticleList.vue'
import { useBaseStore } from '@/stores/base.ts'
import { Article, Dict, DictType } from '@/types/types.ts'
import type { Article, Dict } from '@/types/types'
import { useRuntimeStore } from '@/stores/runtime.ts'
import BaseButton from '@/components/BaseButton.vue'
import { useRoute, useRouter } from 'vue-router'
@@ -20,6 +20,7 @@ import { DICT_LIST } from '@/config/env.ts'
import BaseIcon from '@/components/BaseIcon.vue'
import Switch from '@/components/base/Switch.vue'
import { useGetDict } from '@/hooks/dict.ts'
import { DictType } from '@/types/enum.ts'
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
@@ -147,7 +148,7 @@ const list = $computed(() => {
}),
].concat(runtimeStore.editDict.articles)
})
console.log('list',list)
console.log('list', list)
let showTranslate = $ref(true)
let startPlay = $ref(false)

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { resourceWrap, useNav } from "@/utils";
import BasePage from "@/components/BasePage.vue";
import { DictResource } from "@/types/types.ts";
import type { DictResource } from "@/types/types.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Empty from "@/components/Empty.vue";

View File

@@ -21,23 +21,14 @@ import { usePracticeStore } from '@/stores/practice.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { getDefaultArticle, getDefaultDict, getDefaultWord } from '@/types/func.ts'
import {
Article,
ArticleItem,
ArticleWord,
Dict,
DictType,
PracticeArticleWordType,
ShortcutKey,
Statistics,
Word,
} from '@/types/types.ts'
import type { Article, ArticleItem, ArticleWord, Dict, Statistics, Word } from '@/types/types.ts'
import { _getDictDataByUrl, _nextTick, cloneDeep, isMobile, loadJsLib, msToMinute, resourceWrap, total } from '@/utils'
import { getPracticeArticleCache, setPracticeArticleCache } from '@/utils/cache.ts'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import { computed, onMounted, onUnmounted, provide, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { nanoid } from 'nanoid'
import { DictType, PracticeArticleWordType, ShortcutKey } from '@/types/enum.ts'
const store = useBaseStore()
const runtimeStore = useRuntimeStore()

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Article } from '@/types/types.ts'
import type { Article } from '@/types/types.ts'
import { ref, watch } from 'vue'
import { get } from 'idb-keyval'
import Audio from '@/components/base/Audio.vue'

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { Article, Sentence, TranslateEngine } from '@/types/types.ts'
import type { Article, Sentence } from '@/types/types.ts'
import BaseButton from '@/components/BaseButton.vue'
import EditAbleText from '@/components/EditAbleText.vue'
import { getNetworkTranslate, getSentenceAllText, getSentenceAllTranslateText } from '@/hooks/translate.ts'
@@ -22,6 +22,7 @@ import BaseInput from '@/components/base/BaseInput.vue'
import Textarea from '@/components/base/Textarea.vue'
import { LOCAL_FILE_KEY } from '@/config/env.ts'
import PopConfirm from '@/components/PopConfirm.vue'
import {TranslateEngine} from "@/types/enum.ts";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))

View File

@@ -1,23 +1,23 @@
<script setup lang="ts">
import { Dict, DictId, DictType } from "@/types/types.ts";
import { cloneDeep } from "@/utils";
import type { Dict } from '@/types/types.ts'
import { cloneDeep } from '@/utils'
import Toast from '@/components/base/toast/Toast.ts'
import { onMounted, reactive } from "vue";
import { useRuntimeStore } from "@/stores/runtime.ts";
import { useBaseStore } from "@/stores/base.ts";
import BaseButton from "@/components/BaseButton.vue";
import { getDefaultDict } from "@/types/func.ts";
import { Option, Select } from "@/components/base/select";
import BaseInput from "@/components/base/BaseInput.vue";
import Form from "@/components/base/form/Form.vue";
import FormItem from "@/components/base/form/FormItem.vue";
import { addDict } from "@/apis";
import { AppEnv } from "@/config/env.ts";
import { onMounted, reactive } from 'vue'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useBaseStore } from '@/stores/base.ts'
import BaseButton from '@/components/BaseButton.vue'
import { getDefaultDict } from '@/types/func.ts'
import { Option, Select } from '@/components/base/select'
import BaseInput from '@/components/base/BaseInput.vue'
import Form from '@/components/base/form/Form.vue'
import FormItem from '@/components/base/form/FormItem.vue'
import { addDict } from '@/apis'
import { AppEnv, DictId } from '@/config/env.ts'
import { nanoid } from 'nanoid'
import { DictType } from '@/types/enum.ts'
const props = defineProps<{
isAdd: boolean,
isAdd: boolean
isBook: boolean
}>()
const emit = defineEmits<{
@@ -34,20 +34,20 @@ const DefaultDictForm = {
tags: [],
translateLanguage: 'zh-CN',
language: 'en',
type: DictType.article
type: DictType.article,
}
let dictForm: any = $ref(cloneDeep(DefaultDictForm))
const dictFormRef = $ref()
let loading = $ref(false)
const dictRules = reactive({
name: [
{required: true, message: '请输入名称', trigger: 'blur'},
{max: 20, message: '名称不能超过20个字符', trigger: 'blur'},
{ required: true, message: '请输入名称', trigger: 'blur' },
{ max: 20, message: '名称不能超过20个字符', trigger: 'blur' },
],
})
async function onSubmit() {
await dictFormRef.validate(async (valid) => {
await dictFormRef.validate(async valid => {
if (valid) {
let data: Dict = getDefaultDict(dictForm)
data.type = props.isBook ? DictType.article : DictType.word
@@ -78,10 +78,15 @@ async function onSubmit() {
} else {
let rIndex = source.bookList.findIndex(v => v.id === data.id)
//任意修改,都将其变为自定义词典
if (!data.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect, DictId.articleCollect].includes(data.en_name || data.id)) {
if (
!data.custom &&
![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect, DictId.articleCollect].includes(
data.en_name || data.id
)
) {
data.custom = true
if (!data.id.includes('_custom')) {
data.id +='_custom_' + nanoid(6)
data.id += '_custom_' + nanoid(6)
}
}
runtimeStore.editDict = data
@@ -106,36 +111,31 @@ onMounted(() => {
dictForm = cloneDeep(runtimeStore.editDict)
}
})
</script>
<template>
<div class="w-120 mt-4">
<Form
ref="dictFormRef"
:rules="dictRules"
:model="dictForm"
label-width="8rem">
<Form ref="dictFormRef" :rules="dictRules" :model="dictForm" label-width="8rem">
<FormItem label="名称" prop="name">
<BaseInput v-model="dictForm.name"/>
<BaseInput v-model="dictForm.name" />
</FormItem>
<FormItem label="描述">
<BaseInput v-model="dictForm.description" textarea/>
<BaseInput v-model="dictForm.description" textarea />
</FormItem>
<FormItem label="原文语言" v-if="false">
<Select v-model="dictForm.language" placeholder="请选择选项">
<Option label="英语" value="en"/>
<Option label="德语" value="de"/>
<Option label="日语" value="ja"/>
<Option label="代码" value="code"/>
<Option label="英语" value="en" />
<Option label="德语" value="de" />
<Option label="日语" value="ja" />
<Option label="代码" value="code" />
</Select>
</FormItem>
<FormItem label="译文语言" v-if="false">
<Select v-model="dictForm.translateLanguage" placeholder="请选择选项">
<Option label="中文" value="zh-CN"/>
<Option label="英语" value="en"/>
<Option label="德语" value="de"/>
<Option label="日语" value="ja"/>
<Option label="中文" value="zh-CN" />
<Option label="英语" value="en" />
<Option label="德语" value="de" />
<Option label="日语" value="ja" />
</Select>
</FormItem>
<div class="center">
@@ -146,7 +146,4 @@ onMounted(() => {
</div>
</template>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import {Article} from "@/types/types.ts";
import type {Article} from "@/types/types.ts";
import {useDisableEventListener} from "@/hooks/event.ts";
import EditArticle from "@/pages/article/components/EditArticle.vue";
import {getDefaultArticle} from "@/types/func.ts";

View File

@@ -10,7 +10,7 @@ import { useBaseStore } from '@/stores/base.ts'
import { usePracticeStore } from '@/stores/practice.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { getDefaultArticle, getDefaultWord } from '@/types/func.ts'
import { Article, ArticleWord, PracticeArticleWordType, Sentence, ShortcutKey, Word } from '@/types/types.ts'
import type { Article, ArticleWord, Sentence, Word } from '@/types/types.ts'
import { _dateFormat, _nextTick, isMobile, msToHourMinute, total } from '@/utils'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import ContextMenu from '@imengyu/vue3-context-menu'
@@ -20,6 +20,7 @@ import { nanoid } from 'nanoid'
import { inject, onMounted, onUnmounted, watch } from 'vue'
import { getPracticeArticleCache, setPracticeArticleCache } from '@/utils/cache.ts'
import { PracticeArticleWordType, ShortcutKey } from '@/types/enum.ts'
interface IProps {
article: Article
@@ -675,13 +676,15 @@ const currentPractice = inject('currentPractice', [])
<header class="pt-10 pb-6">
<div class="text-center">
<span class="text-3xl">{{ store.sbook.lastLearnIndex + 1 }}. </span>
<span class="text-3xl">{{ props.article?.title??'' }}</span>
<span class="text-3xl">{{ props.article?.title ?? '' }}</span>
<span class="ml-6 text-2xl" v-if="settingStore.translate">{{ props.article?.titleTranslate }}</span>
</div>
<div class="mt-2 text-2xl" v-if="props.article?.question?.text">
<div>Question: {{ props.article?.question?.text }}</div>
<div class="text-xl color-translate-second" v-if="settingStore.translate">问题: {{ props.article?.question?.translate }}</div>
<div class="text-xl color-translate-second" v-if="settingStore.translate">
问题: {{ props.article?.question?.translate }}
</div>
</div>
</header>

View File

@@ -1,12 +1,12 @@
<script setup lang="tsx">
import {useSettingStore} from "@/stores/setting.ts";
import Space from "@/pages/article/components/Space.vue";
import { PracticeArticleWordType } from "@/types/types.ts";
//引入这个编译就报错
// import {ArticleWord} from "@/types/types.ts";
import {PracticeArticleWordType} from "@/types/enum.ts";
import type {ArticleWord} from "@/types/types.ts";
const props = defineProps<{
word: any,
word: ArticleWord,
isTyping: boolean,
}>()
const settingStore = useSettingStore()

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import { ShortcutKey } from '@/types/types.ts'
import Logo from '@/components/Logo.vue'
import { useSettingStore } from '@/stores/setting.ts'
import { useRouter } from 'vue-router'
@@ -8,6 +7,7 @@ import BaseIcon from '@/components/BaseIcon.vue'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { jump2Feedback } from '@/utils'
import { watch } from 'vue'
import { ShortcutKey } from '@/types/enum.ts'
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()

View File

@@ -3,10 +3,18 @@ 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 { APP_NAME, APP_VERSION, AppEnv, Host, IS_DEV, LIB_JS_URL, LOCAL_FILE_KEY } from '@/config/env.ts'
import {
APP_NAME,
APP_VERSION,
AppEnv,
DefaultShortcutKeyMap,
Host,
IS_DEV,
LIB_JS_URL,
LOCAL_FILE_KEY,
} from '@/config/env.ts'
import BasePage from '@/components/BasePage.vue'
import Toast from '@/components/base/toast/Toast.ts'
import { set } from 'idb-keyval'

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import {CodeType} from "@/types/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import {sendCode} from "@/apis/user.ts";
import {PHONE_CONFIG} from "@/config/auth.ts";
import Toast from "@/components/base/toast/Toast.ts";
import {CodeType} from "@/types/enum.ts";
let isSendingCode = $ref(false)
let codeCountdown = $ref(0)
@@ -63,4 +63,4 @@ async function sendVerificationCode() {
<style scoped lang="scss">
</style>
</style>

View File

@@ -9,7 +9,6 @@ import BaseButton from "@/components/BaseButton.vue";
import {PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts";
import {changeEmailApi, changePhoneApi, setPassword, updateUserInfoApi, User} from "@/apis/user.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import {CodeType} from "@/types/types.ts";
import FormItem from "@/components/base/form/FormItem.vue";
import Form from "@/components/base/form/Form.vue";
import {FormInstance} from "@/components/base/form/types.ts";
@@ -18,6 +17,7 @@ import {_dateFormat, cloneDeep, jump2Feedback} from "@/utils";
import Toast from "@/components/base/toast/Toast.ts";
import Code from "@/pages/user/Code.vue";
import {MessageBox} from "@/utils/MessageBox.tsx";
import {CodeType} from "@/types/enum.ts";
const userStore = useUserStore()
const router = useRouter()
@@ -626,4 +626,4 @@ function onFileChange(e) {
.item {
@apply flex items-center justify-between min-h-14;
}
</style>
</style>

View File

@@ -1,26 +1,26 @@
<script setup lang="tsx">
import { onBeforeUnmount, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import BaseInput from "@/components/base/BaseInput.vue";
import BaseButton from "@/components/BaseButton.vue";
import { APP_NAME } from "@/config/env.ts";
import { useUserStore } from "@/stores/user.ts";
import { loginApi, LoginParams, registerApi, resetPasswordApi } from "@/apis/user.ts";
import { accountRules, codeRules, passwordRules, phoneRules } from "@/utils/validation.ts";
import Toast from "@/components/base/toast/Toast.ts";
import FormItem from "@/components/base/form/FormItem.vue";
import Form from "@/components/base/form/Form.vue";
import Notice from "@/pages/user/Notice.vue";
import { FormInstance } from "@/components/base/form/types.ts";
import { PASSWORD_CONFIG, PHONE_CONFIG } from "@/config/auth.ts";
import { CodeType, ImportStatus } from "@/types/types.ts";
import Code from "@/pages/user/Code.vue";
import { isNewUser, jump2Feedback, sleep, useNav } from "@/utils";
import Header from "@/components/Header.vue";
import PopConfirm from "@/components/PopConfirm.vue";
import { useExport } from "@/hooks/export.ts";
import { getProgress, upload, uploadImportData } from "@/apis";
import { Exception } from "sass";
import BaseInput from '@/components/base/BaseInput.vue'
import BaseButton from '@/components/BaseButton.vue'
import { APP_NAME } from '@/config/env.ts'
import { useUserStore } from '@/stores/user.ts'
import { loginApi, LoginParams, registerApi, resetPasswordApi } from '@/apis/user.ts'
import { accountRules, codeRules, passwordRules, phoneRules } from '@/utils/validation.ts'
import Toast from '@/components/base/toast/Toast.ts'
import FormItem from '@/components/base/form/FormItem.vue'
import Form from '@/components/base/form/Form.vue'
import Notice from '@/pages/user/Notice.vue'
import { FormInstance } from '@/components/base/form/types.ts'
import { PASSWORD_CONFIG, PHONE_CONFIG } from '@/config/auth.ts'
import Code from '@/pages/user/Code.vue'
import { isNewUser, jump2Feedback, sleep, useNav } from '@/utils'
import Header from '@/components/Header.vue'
import PopConfirm from '@/components/PopConfirm.vue'
import { useExport } from '@/hooks/export.ts'
import { getProgress, upload, uploadImportData } from '@/apis'
import { Exception } from 'sass'
import { CodeType, ImportStatus } from '@/types/enum.ts'
// 状态管理
const userStore = useUserStore()
@@ -41,28 +41,25 @@ let waitForImportConfirmation = $ref(true)
const QR_EXPIRE_TIME = 5 * 60 * 1000 // 5分钟过期
let phoneLoginForm = $ref({phone: '', code: ''})
let phoneLoginForm = $ref({ phone: '', code: '' })
let phoneLoginFormRef = $ref<FormInstance>()
let phoneLoginFormRules = {
phone: phoneRules,
code: codeRules
code: codeRules,
}
let loginForm2 = $ref({account: '', password: ''})
let loginForm2 = $ref({ account: '', password: '' })
let loginForm2Ref = $ref<FormInstance>()
let loginForm2Rules = {
account: accountRules,
password: passwordRules,
}
const registerForm = $ref({
account: '',
password: '',
confirmPassword: '',
code: ''
code: '',
})
let registerFormRef = $ref<FormInstance>()
// 注册表单规则和引用
@@ -71,23 +68,23 @@ let registerFormRules = {
code: codeRules,
password: passwordRules,
confirmPassword: [
{required: true, message: '请再次输入密码', trigger: 'blur'},
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
validator: (rule: any, value: any) => {
if (value !== registerForm.password) {
throw new Error('两次密码输入不一致')
}
}, trigger: 'blur'
},
trigger: 'blur',
},
],
}
const forgotForm = $ref({
account: '',
code: '',
newPassword: '',
confirmPassword: ''
confirmPassword: '',
})
let forgotFormRef = $ref<FormInstance>()
// 忘记密码表单规则和引用
@@ -96,13 +93,14 @@ let forgotFormRules = {
code: codeRules,
newPassword: passwordRules,
confirmPassword: [
{required: true, message: '请再次输入新密码', trigger: 'blur'},
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{
validator: (rule: any, value: any) => {
if (value !== forgotForm.newPassword) {
throw new Error('两次密码输入不一致')
}
}, trigger: 'blur'
},
trigger: 'blur',
},
],
}
@@ -122,17 +120,17 @@ function loginSuccess(token: string) {
// 统一登录处理
async function handleLogin() {
currentFormRef.validate(async (valid) => {
if (!valid) return;
currentFormRef.validate(async valid => {
if (!valid) return
try {
loading = true
let data = {}
//手机号登录
if (loginType === 'code') {
data = {...phoneLoginForm, type: 'code'}
data = { ...phoneLoginForm, type: 'code' }
} else {
//密码登录
data = {...loginForm2, type: 'pwd'}
data = { ...loginForm2, type: 'pwd' }
}
let res = await loginApi(data as LoginParams)
if (res.success) {
@@ -153,7 +151,7 @@ async function handleLogin() {
// 注册
async function handleRegister() {
registerFormRef.validate(async (valid) => {
registerFormRef.validate(async valid => {
if (!valid) return
try {
loading = true
@@ -175,7 +173,7 @@ async function handleRegister() {
// 忘记密码
async function handleForgotPassword() {
forgotFormRef.validate(async (valid) => {
forgotFormRef.validate(async valid => {
if (!valid) return
try {
loading = true
@@ -224,7 +222,8 @@ async function handleWechatLogin() {
// wechatQRUrl = response.qrUrl
// 暂时使用占位二维码
wechatQRUrl = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iI2Y1ZjVmNSIvPgogIDx0ZXh0IHg9IjUwJSIgeT0iNTAlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OTk5OSI+55So5o6l566h55CG6L295Lit6K+B77yBPC90ZXh0Pgo8L3N2Zz4K'
wechatQRUrl =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgZmlsbD0iI2Y1ZjVmNSIvPgogIDx0ZXh0IHg9IjUwJSIgeT0iNTAlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBkeT0iLjNlbSIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OTk5OSI+55So5o6l566h55CG6L295Lit6K+B77yBPC90ZXh0Pgo8L3N2Zz4K'
// 模拟轮询检查扫码状态
qrCheckInterval = setInterval(async () => {
@@ -246,7 +245,6 @@ async function handleWechatLogin() {
qrCheckInterval = null
Toast.info('二维码已过期,请点击刷新')
}, QR_EXPIRE_TIME)
} catch (error) {
console.error('Wechat login error:', error)
Toast.error('微信登录失败')
@@ -288,13 +286,13 @@ onBeforeUnmount(() => {
})
enum ImportStep {
CONFIRMATION,//等待确认
PROCESSING,//处理中
SUCCESS,//成功
FAIL,//失败
CONFIRMATION, //等待确认
PROCESSING, //处理中
SUCCESS, //成功
FAIL, //失败
}
const {exportData} = useExport()
const { exportData } = useExport()
let importStep = $ref<ImportStep>(ImportStep.CONFIRMATION)
let isImporting = $ref(false)
let reason = $ref('')
@@ -311,9 +309,9 @@ async function startSync() {
let res = await exportData('')
reason = '上传数据中'
let formData = new FormData()
formData.append('file', res, "example.zip")
formData.append('file', res, 'example.zip')
let result = await uploadImportData(formData, progressEvent => {
let percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
let percent = Math.round((progressEvent.loaded * 100) / progressEvent.total)
reason = `上传进度(${percent}%)`
})
if (result.success) {
@@ -342,7 +340,7 @@ async function startSync() {
}
}, 2000)
} else {
throw new Error(`同步失败,${result.msg ? ('原因: ' + result.msg) : ''},请联系管理员`)
throw new Error(`同步失败,${result.msg ? '原因: ' + result.msg : ''},请联系管理员`)
}
} catch (error) {
Toast.error(error.message || '同步失败')
@@ -356,13 +354,9 @@ function logout() {
waitForImportConfirmation = false
}
function forgetData() {
function forgetData() {}
}
function goHome(){
}
function goHome() {}
</script>
<template>
@@ -379,88 +373,83 @@ function goHome(){
<!-- Tab切换 -->
<div class="center gap-8 mb-6">
<div
class="center cp transition-colors"
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
@click="loginType = 'code'"
class="center cp transition-colors"
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
@click="loginType = 'code'"
>
<div>
<span>验证码登录</span>
<div
v-opacity="loginType === 'code'"
class="mt-1 h-0.5 bg-blue-600"
></div>
<div v-opacity="loginType === 'code'" class="mt-1 h-0.5 bg-blue-600"></div>
</div>
</div>
<div
class="center cp transition-colors"
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
@click="loginType = 'password'"
class="center cp transition-colors"
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
@click="loginType = 'password'"
>
<div>
<span>密码登录</span>
<div
v-opacity="loginType === 'password'"
class="mt-1 h-0.5 bg-blue-600"
></div>
<div v-opacity="loginType === 'password'" class="mt-1 h-0.5 bg-blue-600"></div>
</div>
</div>
</div>
<!-- 验证码登录表单 -->
<Form
v-if="loginType === 'code'"
ref="phoneLoginFormRef"
:rules="phoneLoginFormRules"
:model="phoneLoginForm">
v-if="loginType === 'code'"
ref="phoneLoginFormRef"
:rules="phoneLoginFormRules"
:model="phoneLoginForm"
>
<FormItem prop="phone">
<BaseInput v-model="phoneLoginForm.phone"
type="tel"
name="username"
autocomplete="tel"
size="large"
placeholder="请输入手机号"
<BaseInput
v-model="phoneLoginForm.phone"
type="tel"
name="username"
autocomplete="tel"
size="large"
placeholder="请输入手机号"
/>
</FormItem>
<FormItem prop="code">
<div class="flex gap-2">
<BaseInput
v-model="phoneLoginForm.code"
type="code"
size="large"
:max-length="PHONE_CONFIG.codeLength"
placeholder="请输入验证码"
v-model="phoneLoginForm.code"
type="code"
size="large"
:max-length="PHONE_CONFIG.codeLength"
placeholder="请输入验证码"
/>
<Code
:validate-field="() => phoneLoginFormRef.validateField('phone')"
:type="CodeType.Login"
:val="phoneLoginForm.phone"
/>
<Code :validate-field="() => phoneLoginFormRef.validateField('phone')"
:type="CodeType.Login"
:val="phoneLoginForm.phone"/>
</div>
</FormItem>
</Form>
<!-- 密码登录表单 -->
<Form
v-else
ref="loginForm2Ref"
:rules="loginForm2Rules"
:model="loginForm2">
<Form v-else ref="loginForm2Ref" :rules="loginForm2Rules" :model="loginForm2">
<FormItem prop="account">
<BaseInput v-model="loginForm2.account"
type="email"
name="username"
autocomplete="email"
size="large"
placeholder="请输入手机号/邮箱地址"
<BaseInput
v-model="loginForm2.account"
type="email"
name="username"
autocomplete="email"
size="large"
placeholder="请输入手机号/邮箱地址"
/>
</FormItem>
<FormItem prop="password">
<div class="flex gap-2">
<BaseInput
v-model="loginForm2.password"
type="password"
name="password"
autocomplete="current-password"
size="large"
placeholder="请输入密码"
v-model="loginForm2.password"
type="password"
name="password"
autocomplete="current-password"
size="large"
placeholder="请输入密码"
/>
</div>
</FormItem>
@@ -470,14 +459,7 @@ function goHome(){
<span v-if="loginType === 'code'">,未注册的手机号将自动注册</span>
</Notice>
<BaseButton
class="w-full"
size="large"
:loading="loading"
@click="handleLogin"
>
登录
</BaseButton>
<BaseButton class="w-full" size="large" :loading="loading" @click="handleLogin"> 登录 </BaseButton>
<!-- 底部操作链接 - 只在密码登录时显示 -->
<div class="mt-4 flex justify-between text-sm" v-opacity="loginType !== 'code'">
@@ -488,131 +470,116 @@ function goHome(){
<!-- 注册模式 -->
<div v-else-if="currentMode === 'register'">
<Header @click="switchMode('login')" title="注册新账号"/>
<Header @click="switchMode('login')" title="注册新账号" />
<Form
ref="registerFormRef"
:rules="registerFormRules"
:model="registerForm">
<Form ref="registerFormRef" :rules="registerFormRules" :model="registerForm">
<FormItem prop="account">
<BaseInput
v-model="registerForm.account"
type="tel"
name="username"
autocomplete="username"
size="large"
placeholder="请输入手机号/邮箱地址"
v-model="registerForm.account"
type="tel"
name="username"
autocomplete="username"
size="large"
placeholder="请输入手机号/邮箱地址"
/>
</FormItem>
<FormItem prop="code">
<div class="flex gap-2">
<BaseInput
v-model="registerForm.code"
type="code"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
v-model="registerForm.code"
type="code"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<Code
:validate-field="() => registerFormRef.validateField('account')"
:type="CodeType.Register"
:val="registerForm.account"
/>
<Code :validate-field="() => registerFormRef.validateField('account')"
:type="CodeType.Register"
:val="registerForm.account"/>
</div>
</FormItem>
<FormItem prop="password">
<BaseInput
v-model="registerForm.password"
type="password"
name="password"
autocomplete="current-password"
size="large"
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
v-model="registerForm.password"
type="password"
name="password"
autocomplete="current-password"
size="large"
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
/>
</FormItem>
<FormItem prop="confirmPassword">
<BaseInput
v-model="registerForm.confirmPassword"
type="password"
name="password"
autocomplete="new-password"
size="large"
placeholder="请再次输入密码"
v-model="registerForm.confirmPassword"
type="password"
name="password"
autocomplete="new-password"
size="large"
placeholder="请再次输入密码"
/>
</FormItem>
</Form>
<Notice/>
<BaseButton
class="w-full"
size="large"
:loading="loading"
@click="handleRegister"
>
注册
</BaseButton>
<Notice />
<BaseButton class="w-full" size="large" :loading="loading" @click="handleRegister"> 注册 </BaseButton>
</div>
<!-- 忘记密码模式 -->
<div v-else-if="currentMode === 'forgot'">
<Header @click="switchMode('login')" title="重置密码"/>
<Header @click="switchMode('login')" title="重置密码" />
<Form
ref="forgotFormRef"
:rules="forgotFormRules"
:model="forgotForm">
<Form ref="forgotFormRef" :rules="forgotFormRules" :model="forgotForm">
<FormItem prop="account">
<BaseInput
v-model="forgotForm.account"
type="tel"
name="username"
autocomplete="username"
size="large"
placeholder="请输入手机号/邮箱地址"
v-model="forgotForm.account"
type="tel"
name="username"
autocomplete="username"
size="large"
placeholder="请输入手机号/邮箱地址"
/>
</FormItem>
<FormItem prop="code">
<div class="flex gap-2">
<BaseInput
v-model="forgotForm.code"
type="code"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
v-model="forgotForm.code"
type="code"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<Code
:validate-field="() => forgotFormRef.validateField('account')"
:type="CodeType.ResetPwd"
:val="forgotForm.account"
/>
<Code :validate-field="() => forgotFormRef.validateField('account')"
:type="CodeType.ResetPwd"
:val="forgotForm.account"/>
</div>
</FormItem>
<FormItem prop="newPassword">
<BaseInput
v-model="forgotForm.newPassword"
type="password"
name="password"
autocomplete="new-password"
size="large"
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
v-model="forgotForm.newPassword"
type="password"
name="password"
autocomplete="new-password"
size="large"
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
/>
</FormItem>
<FormItem prop="confirmPassword">
<BaseInput
v-model="forgotForm.confirmPassword"
type="password"
name="password"
autocomplete="new-password"
size="large"
placeholder="请再次输入新密码"
v-model="forgotForm.confirmPassword"
type="password"
name="password"
autocomplete="new-password"
size="large"
placeholder="请再次输入新密码"
/>
</FormItem>
</Form>
<BaseButton
class="w-full mt-2"
size="large"
:loading="loading"
@click="handleForgotPassword"
>
<BaseButton class="w-full mt-2" size="large" :loading="loading" @click="handleForgotPassword">
重置密码
</BaseButton>
</div>
@@ -622,43 +589,42 @@ function goHome(){
<div v-if="currentMode === 'login'" class="center flex-col bg-gray-100 rounded-xl px-12">
<div class="relative w-40 h-40 bg-white rounded-xl overflow-hidden shadow-xl">
<img
v-if="showWechatQR"
:src="wechatQRUrl"
alt="微信登录二维码"
class="w-full h-full"
:class="{ 'opacity-30': qrStatus === 'expired' }"
v-if="showWechatQR"
:src="wechatQRUrl"
alt="微信登录二维码"
class="w-full h-full"
:class="{ 'opacity-30': qrStatus === 'expired' }"
/>
<!-- 扫描成功蒙层 -->
<div
v-if="qrStatus === 'scanned'"
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
v-if="qrStatus === 'scanned'"
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
>
<IconFluentCheckmarkCircle20Filled class="color-green text-4xl"/>
<IconFluentCheckmarkCircle20Filled class="color-green text-4xl" />
<div class="text-base text-gray-700 font-medium">扫描成功</div>
<div class="text-xs text-gray-600">微信中轻触允许即可登录</div>
</div>
<!-- 取消登录蒙层 -->
<div
v-if="qrStatus === 'cancelled'"
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
v-if="qrStatus === 'cancelled'"
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
>
<IconFluentErrorCircle20Regular class="color-red text-4xl"/>
<IconFluentErrorCircle20Regular class="color-red text-4xl" />
<div class="text-base text-gray-700 font-medium">你已取消此次登录</div>
<div class="text-xs text-gray-600">你可<span class="color-link" @click="refreshQRCode">再次登录</span>,或关闭窗口
<div class="text-xs text-gray-600">
你可<span class="color-link" @click="refreshQRCode">再次登录</span>,或关闭窗口
</div>
</div>
<!-- 过期蒙层 -->
<div
v-if=" qrStatus === 'expired'"
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
v-if="qrStatus === 'expired'"
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
>
<IconFluentArrowClockwise20Regular
@click="refreshQRCode"
class="cp text-4xl"/>
<IconFluentArrowClockwise20Regular @click="refreshQRCode" class="cp text-4xl" />
</div>
</div>
<p class="mt-4 center gap-space">
<IconIxWechatLogo class="text-xl color-green"/>
<IconIxWechatLogo class="text-xl color-green" />
<span class="text-sm text-gray-600">微信扫码登录</span>
</p>
</div>
@@ -675,12 +641,13 @@ function goHome(){
</div>
<div class="flex gap-space justify-end">
<template v-if="importStep === ImportStep.CONFIRMATION">
<PopConfirm :title="[
{text:'您的用户数据将以压缩包自动下载到您的电脑中,以便您随时恢复',type:'normal'},
{text:'随后网站的用户数据将被删除',type:'redBold'},
{text:'是否确认继续?',type:'normal'},
]"
@confirm="forgetData"
<PopConfirm
:title="[
{ text: '的用户数据将以压缩包自动下载到您的电脑中,以便您随时恢复', type: 'normal' },
{ text: '随后网站的用户数据将被删除', type: 'redBold' },
{ text: '是否确认继续?', type: 'normal' },
]"
@confirm="forgetData"
>
<BaseButton type="info">不同步</BaseButton>
</PopConfirm>
@@ -692,22 +659,14 @@ function goHome(){
<div>
<div class="title text-align-center">正在导入中</div>
<ol class="pl-4">
<li>
您的用户数据已自动下载到您的电脑中,以便随时恢复
</li>
<li>
随后将开始数据同步
</li>
<li>
如果您的数据量很大,这将是一个耗时操作
</li>
<li class="color-red-5 font-bold">
请耐心等待,请勿关闭此页面
</li>
<li>您的用户数据已自动下载到您的电脑中,以便随时恢复</li>
<li>随后将开始数据同步</li>
<li>如果您的数据量很大,这将是一个耗时操作</li>
<li class="color-red-5 font-bold">请耐心等待,请勿关闭此页面</li>
</ol>
<div class="flex items-center justify-between gap-2 mt-10">
<span>当前进度: {{ reason }}</span>
<IconEosIconsLoading class="text-xl"/>
<IconEosIconsLoading class="text-xl" />
</div>
</div>
</template>
@@ -737,4 +696,5 @@ function goHome(){
</div>
</div>
</div>
</template>]
</template>
]

View File

@@ -1,6 +1,4 @@
<script setup lang="tsx">
import { DictId, Sort } from '@/types/types.ts'
import { detail } from '@/apis'
import BackIcon from '@/components/BackIcon.vue'
import BaseButton from '@/components/BaseButton.vue'
@@ -15,7 +13,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, TourConfig } from '@/config/env.ts'
import { AppEnv, DictId, 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'
@@ -31,6 +29,7 @@ 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'
import { Sort } from '@/types/enum.ts'
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -610,9 +609,14 @@ defineRender(() => {
importLoading={importLoading}
>
{val => (
<WordItem showTransPop={false}
onClick={() => editWord(val.item)}
index={val.index} showCollectIcon={false} showMarkIcon={false} item={val.item}>
<WordItem
showTransPop={false}
onClick={() => editWord(val.item)}
index={val.index}
showCollectIcon={false}
showMarkIcon={false}
item={val.item}
>
{{
prefix: () => val.checkbox(val.item),
suffix: () => (

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { _nextTick, groupBy, isMobile, loadJsLib, resourceWrap, useNav } from "@/utils";
import BasePage from "@/components/BasePage.vue";
import { DictResource } from "@/types/types.ts";
import type { DictResource } from "@/types/types.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Empty from "@/components/Empty.vue";

View File

@@ -5,17 +5,7 @@ import Statistics from '@/pages/word/components/Statistics.vue'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import {
Dict,
PracticeData,
ShortcutKey,
TaskWords,
Word,
WordPracticeMode,
WordPracticeModeStageMap,
WordPracticeStage,
WordPracticeType,
} from '@/types/types.ts'
import type { Dict, PracticeData, TaskWords, Word } from '@/types/types.ts'
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from '@/hooks/event.ts'
import useTheme from '@/hooks/theme.ts'
import { getCurrentStudyWord, useWordOptions } from '@/hooks/dict.ts'
@@ -35,12 +25,13 @@ import { getDefaultDict, getDefaultWord } from '@/types/func.ts'
import ConflictNotice from '@/components/ConflictNotice.vue'
import PracticeLayout from '@/components/PracticeLayout.vue'
import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig } from '@/config/env.ts'
import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig, WordPracticeModeStageMap } from '@/config/env.ts'
import { ToastInstance } from '@/components/base/toast/type.ts'
import { watchOnce } from '@vueuse/core'
import { setUserDictProp } from '@/apis'
import GroupList from '@/pages/word/components/GroupList.vue'
import { getPracticeWordCache, setPracticeWordCache } from '@/utils/cache.ts'
import { ShortcutKey, WordPracticeMode, WordPracticeStage, WordPracticeType } from '@/types/enum.ts'
const { isWordCollect, toggleWordCollect, isWordSimple, toggleWordSimple } = useWordOptions()
const settingStore = useSettingStore()
@@ -735,7 +726,7 @@ useEvents([
<div class="practice-word mb-50">
<div
class="fixed z-1 top-4 w-full"
style="left: calc(50vw + var(--aside-width) / 2 - var(--toolbar-width) / 2);width:var(--toolbar-width)"
style="left: calc(50vw + var(--aside-width) / 2 - var(--toolbar-width) / 2); width: var(--toolbar-width)"
v-if="settingStore.showNearWord"
>
<div class="center gap-2 cursor-pointer float-left" @click="prev" v-if="prevWord">

View File

@@ -5,7 +5,7 @@ import BaseButton from '@/components/BaseButton.vue'
import VolumeIcon from '@/components/icon/VolumeIcon.vue'
import {useRoute, useRouter} from 'vue-router'
import {useBaseStore} from '@/stores/base.ts'
import {Dict, Word} from '@/types/types.ts'
import type {Dict, Word} from '@/types/types.ts'
import {_getDictDataByUrl, shuffle} from '@/utils'
import {useRuntimeStore} from '@/stores/runtime.ts'
import {usePlayBeep, usePlayCorrect, usePlayWordAudio} from '@/hooks/sound.ts'

View File

@@ -13,7 +13,7 @@ import {
useNav,
} from '@/utils'
import BasePage from '@/components/BasePage.vue'
import { DictResource, WordPracticeMode, WordPracticeModeNameMap } from '@/types/types.ts'
import type { DictResource } from '@/types/types.ts'
import { watch } from 'vue'
import { getCurrentStudyWord } from '@/hooks/dict.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
@@ -28,13 +28,14 @@ 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, Origin, TourConfig } from '@/config/env.ts'
import { AppEnv, DICT_LIST, Host, LIB_JS_URL, Origin, TourConfig, WordPracticeModeNameMap } 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'
import { WordPracticeMode } from '@/types/enum.ts'
const store = useBaseStore()
const settingStore = useSettingStore()

View File

@@ -2,24 +2,16 @@
import { inject, Ref } from 'vue'
import { usePracticeStore } from '@/stores/practice.ts'
import { useSettingStore } from '@/stores/setting.ts'
import {
PracticeData,
ShortcutKey,
TaskWords,
WordPracticeMode,
WordPracticeModeNameMap,
WordPracticeModeStageMap,
WordPracticeStage,
WordPracticeStageNameMap,
} from '@/types/types.ts'
import type { PracticeData, TaskWords } from '@/types/types.ts'
import BaseIcon from '@/components/BaseIcon.vue'
import Tooltip from '@/components/base/Tooltip.vue'
import Progress from '@/components/base/Progress.vue'
import SettingDialog from '@/components/setting/SettingDialog.vue'
import BaseButton from '@/components/BaseButton.vue'
import { useBaseStore } from '@/stores/base.ts'
import VolumeSettingMiniDialog from '@/pages/word/components/VolumeSettingMiniDialog.vue'
import StageProgress from '@/components/StageProgress.vue'
import { ShortcutKey, WordPracticeMode, WordPracticeStage } from '@/types/enum.ts'
import { WordPracticeModeNameMap, WordPracticeModeStageMap, WordPracticeStageNameMap } from '@/config/env.ts'
const statStore = usePracticeStore()
const store = useBaseStore()
@@ -97,9 +89,24 @@ const stages = $computed(() => {
)
) {
const stages = [
{ name: `新词:${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`, ratio: 33, percentage: 0, active: false },
{ name: `上次学习${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`, ratio: 33, percentage: 0, active: false },
{ name: `之前学习:${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`, ratio: 33, percentage: 0, active: false },
{
name: `新词${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`,
ratio: 33,
percentage: 0,
active: false,
},
{
name: `上次学习:${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`,
ratio: 33,
percentage: 0,
active: false,
},
{
name: `之前学习:${WordPracticeModeNameMap[settingStore.wordPracticeMode]}`,
ratio: 33,
percentage: 0,
active: false,
},
]
// 设置已完成阶段的百分比和比例
@@ -241,7 +248,7 @@ const stages = $computed(() => {
<div class="flex gap-2 justify-center items-center" id="toolbar-icons">
<SettingDialog type="word" />
<VolumeSettingMiniDialog/>
<VolumeSettingMiniDialog />
<BaseIcon
v-if="settingStore.wordPracticeMode !== WordPracticeMode.Free"

View File

@@ -3,7 +3,7 @@
import BaseTable from "@/components/BaseTable.vue";
import WordItem from "@/components/WordItem.vue";
import { defineAsyncComponent } from "vue";
import { TaskWords } from "@/types/types.ts";
import type { TaskWords } from "@/types/types.ts";
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useBaseStore } from '@/stores/base.ts'
import BaseButton from '@/components/BaseButton.vue'
import { ShortcutKey, Statistics, TaskWords, WordPracticeMode } from '@/types/types.ts'
import type { Statistics, TaskWords } from '@/types/types.ts'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { usePracticeStore } from '@/stores/practice.ts'
@@ -15,6 +15,7 @@ import ChannelIcons from '@/components/ChannelIcons/ChannelIcons.vue'
import { AppEnv } from '@/config/env.ts'
import { addStat } from '@/apis'
import Toast from '@/components/base/toast/Toast.ts'
import { ShortcutKey, WordPracticeMode } from '@/types/enum.ts'
dayjs.extend(isoWeek)
dayjs.extend(isBetween)
@@ -78,13 +79,9 @@ watch(model, async newVal => {
if (settingStore.wordPracticeMode !== WordPracticeMode.Shuffle) {
store.sdict.lastLearnIndex = store.sdict.lastLearnIndex + statStore.newWordNumber
// 检查已忽略的单词数量,是否全部完成
let ignoreList = [store.allIgnoreWords, store.knownWords][
settingStore.ignoreSimpleWord ? 0 : 1
]
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
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
@@ -156,13 +153,7 @@ calcWeekList() // 新增:计算本周学习记录
</script>
<template>
<Dialog
v-model="model"
:close-on-click-bg="false"
:header="false"
:keyboard="false"
:show-close="false"
>
<Dialog v-model="model" :close-on-click-bg="false" :header="false" :keyboard="false" :show-close="false">
<div class="p-8 pr-3 bg-[var(--bg-card-primary)] rounded-2xl space-y-6">
<!-- Header Section -->
<div class="text-center relative">

View File

@@ -1,13 +1,8 @@
<script setup lang="ts">
import { ShortcutKey, Word, WordPracticeStage, WordPracticeType } from '@/types/types.ts'
import type { Word } 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 { 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'
@@ -18,6 +13,7 @@ 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, WordPracticeStage, WordPracticeType } from '@/types/enum.ts'
interface IProps {
word: Word
@@ -308,18 +304,12 @@ async function onTyping(e: KeyboardEvent) {
wordCompletedTime = Date.now() // 记录单词完成的时间戳
playCorrect()
if (
[WordPracticeType.Listen, WordPracticeType.Identify].includes(
settingStore.wordPracticeType
) &&
[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) {
@@ -476,11 +466,9 @@ useEvents([
class="phonetic"
:class="
(settingStore.dictation ||
[
WordPracticeType.Spell,
WordPracticeType.Listen,
WordPracticeType.Dictation,
].includes(settingStore.wordPracticeType)) &&
[WordPracticeType.Spell, WordPracticeType.Listen, WordPracticeType.Dictation].includes(
settingStore.wordPracticeType
)) &&
!showFullWord &&
!showWordResult &&
'word-shadow'
@@ -499,9 +487,7 @@ useEvents([
<Tooltip
:title="
settingStore.dictation
? `可以按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`
: ''
settingStore.dictation ? `可以按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案` : ''
"
>
<div
@@ -526,12 +512,7 @@ useEvents([
>
<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>
@@ -605,10 +586,7 @@ useEvents([
:word="word.word"
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"
/>
<div
class="text-base anim"
v-opacity="settingStore.translate || showFullWord || showWordResult"
>
<div class="text-base anim" v-opacity="settingStore.translate || showFullWord || showWordResult">
{{ item.cn }}
</div>
</div>
@@ -627,10 +605,7 @@ useEvents([
:word="word.word"
:dictation="!(!settingStore.dictation || showFullWord || showWordResult)"
/>
<div
class="cn anim"
v-opacity="settingStore.translate || showFullWord || showWordResult"
>
<div class="cn anim" v-opacity="settingStore.translate || showFullWord || showWordResult">
{{ item.cn }}
</div>
</div>
@@ -647,16 +622,10 @@ useEvents([
<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"
>
<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>
@@ -670,9 +639,7 @@ useEvents([
<div
class="anim"
v-opacity="
(settingStore.translate && !settingStore.dictation) || showFullWord || showWordResult
"
v-opacity="(settingStore.translate && !settingStore.dictation) || showFullWord || showWordResult"
>
<template v-if="word?.etymology?.length">
<div class="line-white my-3"></div>

View File

@@ -1,10 +1,10 @@
import { defineStore } from 'pinia'
import { Dict, DictId, Word } from '../types/types.ts'
import { Dict, Word } from '../types/types.ts'
import { _getStudyProgress, checkAndUpgradeSaveDict, shakeCommonDict } from '@/utils'
import { shallowReactive } from 'vue'
import { getDefaultDict } from '@/types/func.ts'
import { get, set } from 'idb-keyval'
import { AppEnv, SAVE_DICT_KEY } from '@/config/env.ts'
import { AppEnv, DictId, SAVE_DICT_KEY } from '@/config/env.ts'
import { add2MyDict, dictListVersion, myDictList } from '@/apis'
import Toast from '@/components/base/toast/Toast.ts'

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { WordPracticeModeStageMap, WordPracticeStage, WordPracticeStageNameMap } from '@/types/types.ts'
import { useSettingStore } from './setting'
import {WordPracticeStage} from "@/types/enum.ts";
import { WordPracticeModeStageMap, WordPracticeStageNameMap } from '@/config/env.ts'
export interface PracticeState {
stage: WordPracticeStage

View File

@@ -1,5 +1,5 @@
import {defineStore} from "pinia"
import {Dict} from "@/types/types.ts";
import type {Dict} from "@/types/types.ts";
import {getDefaultDict} from "@/types/func.ts";
export interface RuntimeState {

View File

@@ -1,49 +1,49 @@
import { defineStore } from "pinia"
import { checkAndUpgradeSaveSetting, cloneDeep } from "@/utils";
import { DefaultShortcutKeyMap, WordPracticeMode, WordPracticeType } from "@/types/types.ts";
import { get } from "idb-keyval";
import { AppEnv, SAVE_SETTING_KEY } from "@/config/env.ts";
import { getSetting } from "@/apis";
import { AppEnv, DefaultShortcutKeyMap, SAVE_SETTING_KEY } from '@/config/env.ts'
import { getSetting } from '@/apis'
import { WordPracticeMode, WordPracticeType } from '@/types/enum.ts'
export interface SettingState {
soundType: string,
soundType: string
wordSound: boolean,
wordSoundVolume: number,
wordSoundSpeed: number,
wordReviewRatio:number //单词复习比例
wordSound: boolean
wordSoundVolume: number
wordSoundSpeed: number
wordReviewRatio: number //单词复习比例
articleSound: boolean,
articleAutoPlayNext: boolean,
articleSoundVolume: number,
articleSoundSpeed: number,
articleSound: boolean
articleAutoPlayNext: boolean
articleSoundVolume: number
articleSoundSpeed: number
keyboardSound: boolean,
keyboardSoundVolume: number,
keyboardSoundFile: string,
keyboardSound: boolean
keyboardSoundVolume: number
keyboardSoundFile: string
effectSound: boolean,
effectSoundVolume: number,
effectSound: boolean
effectSoundVolume: number
repeatCount: number, //重复次数
repeatCustomCount?: number, //自定义重复次数
dictation: boolean,//显示默写
translate: boolean, //显示翻译
repeatCount: number //重复次数
repeatCustomCount?: number //自定义重复次数
dictation: boolean //显示默写
translate: boolean //显示翻译
showNearWord: boolean //显示上/下一个词
ignoreCase: boolean //忽略大小写
allowWordTip: boolean //默写时时否允许查看提示
waitTimeForChangeWord: number // 切下一个词的等待时间
fontSize: {
articleForeignFontSize: number,
articleTranslateFontSize: number,
wordForeignFontSize: number,
wordTranslateFontSize: number,
},
showToolbar: boolean, //收起/展开工具栏
showPanel: boolean, // 收起/展开面板
sideExpand: boolean, //收起/展开左侧侧边栏
theme: string,
shortcutKeyMap: Record<string, string>,
articleForeignFontSize: number
articleTranslateFontSize: number
wordForeignFontSize: number
wordTranslateFontSize: number
}
showToolbar: boolean //收起/展开工具栏
showPanel: boolean // 收起/展开面板
sideExpand: boolean //收起/展开左侧侧边栏
theme: string
shortcutKeyMap: Record<string, string>
first: boolean
firstTime: number
load: boolean

103
src/types/enum.ts Normal file
View File

@@ -0,0 +1,103 @@
export enum DictType {
collect = 'collect',
simple = 'simple',
wrong = 'wrong',
known = 'known',
word = 'word',
article = 'article',
}
export enum Sort {
normal = 0,
random = 1,
reverse = 2,
reverseAll = 3,
randomAll = 4,
}
export enum ShortcutKey {
ShowWord = 'ShowWord',
EditArticle = 'EditArticle',
Next = 'Next',
Previous = 'Previous',
ToggleSimple = 'ToggleSimple',
ToggleCollect = 'ToggleCollect',
NextChapter = 'NextChapter',
PreviousChapter = 'PreviousChapter',
RepeatChapter = 'RepeatChapter',
DictationChapter = 'DictationChapter',
PlayWordPronunciation = 'PlayWordPronunciation',
ToggleShowTranslate = 'ToggleShowTranslate',
ToggleDictation = 'ToggleDictation',
ToggleTheme = 'ToggleTheme',
ToggleConciseMode = 'ToggleConciseMode',
TogglePanel = 'TogglePanel',
RandomWrite = 'RandomWrite',
KnowWord = 'KnowWord',
UnknownWord = 'UnknownWord',
}
export enum TranslateEngine {
Baidu = 0,
}
export enum PracticeArticleWordType {
Symbol,
Number,
Word,
}
//练习模式
export enum WordPracticeMode {
System = 0,
Free = 1,
IdentifyOnly = 2, // 独立自测模式
DictationOnly = 3, // 独立默写模式
ListenOnly = 4, // 独立听写模式
Shuffle = 5, // 随机复习模式
Review = 6, // 复习模式
}
//练习类型
export enum WordPracticeType {
FollowWrite, //跟写
Spell,
Identify,
Listen,
Dictation,
}
export enum CodeType {
Login = 0,
Register = 1,
ResetPwd = 2,
ChangeEmail = 3,
ChangePhoneNew = 4,
ChangePhoneOld = 5,
}
export enum ImportStatus {
Idle = 0,
Success = 1,
Fail = 2,
}
//练习阶段
export enum WordPracticeStage {
FollowWriteNewWord = 0,
IdentifyNewWord = 1,
ListenNewWord = 2,
DictationNewWord = 3,
FollowWriteReview = 4,
IdentifyReview = 5,
ListenReview = 6,
DictationReview = 7,
FollowWriteReviewAll = 8,
IdentifyReviewAll = 9,
ListenReviewAll = 10,
DictationReviewAll = 11,
Shuffle = 12,
Complete = 13,
}

View File

@@ -1,25 +1,26 @@
import { Article, ArticleWord, Dict, DictType, PracticeArticleWordType, Word } from "@/types/types.ts";
import type { Article, ArticleWord, Dict, Word } from '@/types/types.ts'
import { shallowReactive } from "vue";
import { cloneDeep } from "@/utils";
import { nanoid } from "nanoid";
import { DictType, PracticeArticleWordType } from '@/types/enum.ts'
export function getDefaultWord(val: Partial<Word> = {}): Word {
return {
custom: false,
id: nanoid(6),
"word": "",
"phonetic0": "",
"phonetic1": "",
"trans": [],
"sentences": [],
"phrases": [],
"synos": [],
"relWords": {
"root": "",
"rels": []
word: '',
phonetic0: '',
phonetic1: '',
trans: [],
sentences: [],
phrases: [],
synos: [],
relWords: {
root: '',
rels: [],
},
"etymology": [],
...val
etymology: [],
...val,
}
}

View File

@@ -1,3 +1,5 @@
import { DictType, PracticeArticleWordType } from '@/types/enum.ts'
export type Word = {
id?: string
custom?: boolean
@@ -37,20 +39,9 @@ export type Word = {
}[]
}
export const PronunciationApi = 'https://dict.youdao.com/dictvoice?audio='
export type TranslateLanguageType = 'en' | 'zh-CN' | 'ja' | 'de' | 'common' | ''
export type LanguageType = 'en' | 'ja' | 'de' | 'code'
export enum DictType {
collect = 'collect',
simple = 'simple',
wrong = 'wrong',
known = 'known',
word = 'word',
article = 'article',
}
export interface ArticleWord extends Word {
nextSpace: boolean
symbolPosition: 'start' | 'end' | ''
@@ -107,62 +98,6 @@ export interface Statistics {
title: string //文章标题
}
export enum Sort {
normal = 0,
random = 1,
reverse = 2,
reverseAll = 3,
randomAll = 4,
}
export enum ShortcutKey {
ShowWord = 'ShowWord',
EditArticle = 'EditArticle',
Next = 'Next',
Previous = 'Previous',
ToggleSimple = 'ToggleSimple',
ToggleCollect = 'ToggleCollect',
NextChapter = 'NextChapter',
PreviousChapter = 'PreviousChapter',
RepeatChapter = 'RepeatChapter',
DictationChapter = 'DictationChapter',
PlayWordPronunciation = 'PlayWordPronunciation',
ToggleShowTranslate = 'ToggleShowTranslate',
ToggleDictation = 'ToggleDictation',
ToggleTheme = 'ToggleTheme',
ToggleConciseMode = 'ToggleConciseMode',
TogglePanel = 'TogglePanel',
RandomWrite = 'RandomWrite',
KnowWord = 'KnowWord',
UnknownWord = 'UnknownWord',
}
export const DefaultShortcutKeyMap = {
[ShortcutKey.EditArticle]: 'Ctrl+E',
[ShortcutKey.ShowWord]: 'Escape',
[ShortcutKey.Previous]: 'Alt+⬅',
[ShortcutKey.Next]: 'Tab',
[ShortcutKey.ToggleSimple]: '`',
[ShortcutKey.ToggleCollect]: 'Enter',
[ShortcutKey.PreviousChapter]: 'Ctrl+⬅',
[ShortcutKey.NextChapter]: 'Ctrl+➡',
[ShortcutKey.RepeatChapter]: 'Ctrl+Enter',
[ShortcutKey.DictationChapter]: 'Alt+Enter',
[ShortcutKey.PlayWordPronunciation]: 'Ctrl+P',
[ShortcutKey.ToggleShowTranslate]: 'Ctrl+Z',
[ShortcutKey.ToggleDictation]: 'Ctrl+I',
[ShortcutKey.ToggleTheme]: 'Ctrl+Q',
[ShortcutKey.ToggleConciseMode]: 'Ctrl+M',
[ShortcutKey.TogglePanel]: 'Ctrl+L',
[ShortcutKey.RandomWrite]: 'Ctrl+R',
[ShortcutKey.KnowWord]: '1',
[ShortcutKey.UnknownWord]: '2',
}
export enum TranslateEngine {
Baidu = 0,
}
export type DictResource = {
id: string
name: string
@@ -202,11 +137,6 @@ export interface ArticleItem {
index: number
}
export const SlideType = {
HORIZONTAL: 0,
VERTICAL: 1,
}
export interface PracticeData {
index: number
words: Word[]
@@ -220,143 +150,3 @@ export interface TaskWords {
write: Word[]
shuffle: Word[]
}
export class DictId {
static wordCollect = 'wordCollect'
static wordWrong = 'wordWrong'
static wordKnown = 'wordKnown'
static articleCollect = 'articleCollect'
}
export enum PracticeArticleWordType {
Symbol,
Number,
Word,
}
//练习模式
export enum WordPracticeMode {
System = 0,
Free = 1,
IdentifyOnly = 2, // 独立自测模式
DictationOnly = 3, // 独立默写模式
ListenOnly = 4, // 独立听写模式
Shuffle = 5, // 随机复习模式
Review = 6, // 复习模式
}
//练习类型
export enum WordPracticeType {
FollowWrite, //跟写
Spell,
Identify,
Listen,
Dictation,
}
export enum CodeType {
Login = 0,
Register = 1,
ResetPwd = 2,
ChangeEmail = 3,
ChangePhoneNew = 4,
ChangePhoneOld = 5,
}
export enum ImportStatus {
Idle = 0,
Success = 1,
Fail = 2,
}
//练习阶段
export enum WordPracticeStage {
FollowWriteNewWord = 0,
IdentifyNewWord = 1,
ListenNewWord = 2,
DictationNewWord = 3,
FollowWriteReview = 4,
IdentifyReview = 5,
ListenReview = 6,
DictationReview = 7,
FollowWriteReviewAll = 8,
IdentifyReviewAll = 9,
ListenReviewAll = 10,
DictationReviewAll = 11,
Shuffle = 12,
Complete = 13,
}
export const WordPracticeModeStageMap: Record<WordPracticeMode, WordPracticeStage[]> = {
[WordPracticeMode.Free]: [WordPracticeStage.FollowWriteNewWord, WordPracticeStage.Complete],
[WordPracticeMode.IdentifyOnly]: [
WordPracticeStage.IdentifyNewWord,
WordPracticeStage.IdentifyReview,
WordPracticeStage.IdentifyReviewAll,
WordPracticeStage.Complete,
],
[WordPracticeMode.DictationOnly]: [
WordPracticeStage.DictationNewWord,
WordPracticeStage.DictationReview,
WordPracticeStage.DictationReviewAll,
WordPracticeStage.Complete,
],
[WordPracticeMode.ListenOnly]: [
WordPracticeStage.ListenNewWord,
WordPracticeStage.ListenReview,
WordPracticeStage.ListenReviewAll,
WordPracticeStage.Complete,
],
[WordPracticeMode.System]: [
WordPracticeStage.FollowWriteNewWord,
WordPracticeStage.ListenNewWord,
WordPracticeStage.DictationNewWord,
WordPracticeStage.IdentifyReview,
WordPracticeStage.ListenReview,
WordPracticeStage.DictationReview,
WordPracticeStage.IdentifyReviewAll,
WordPracticeStage.ListenReviewAll,
WordPracticeStage.DictationReviewAll,
WordPracticeStage.Complete,
],
[WordPracticeMode.Shuffle]: [WordPracticeStage.Shuffle, WordPracticeStage.Complete],
[WordPracticeMode.Review]: [
WordPracticeStage.IdentifyReview,
WordPracticeStage.ListenReview,
WordPracticeStage.DictationReview,
WordPracticeStage.IdentifyReviewAll,
WordPracticeStage.ListenReviewAll,
WordPracticeStage.DictationReviewAll,
WordPracticeStage.Complete,
],
}
export const WordPracticeStageNameMap: Record<WordPracticeStage, string> = {
[WordPracticeStage.FollowWriteNewWord]: '跟写新词',
[WordPracticeStage.IdentifyNewWord]: '自测新词',
[WordPracticeStage.ListenNewWord]: '听写新词',
[WordPracticeStage.DictationNewWord]: '默写新词',
[WordPracticeStage.FollowWriteReview]: '跟写上次学习',
[WordPracticeStage.IdentifyReview]: '自测上次学习',
[WordPracticeStage.ListenReview]: '听写上次学习',
[WordPracticeStage.DictationReview]: '默写上次学习',
[WordPracticeStage.FollowWriteReviewAll]: '跟写之前学习',
[WordPracticeStage.IdentifyReviewAll]: '自测之前学习',
[WordPracticeStage.ListenReviewAll]: '听写之前学习',
[WordPracticeStage.DictationReviewAll]: '默写之前学习',
[WordPracticeStage.Complete]: '完成学习',
[WordPracticeStage.Shuffle]: '随机复习',
}
export const WordPracticeModeNameMap: Record<WordPracticeMode, string> = {
[WordPracticeMode.System]: '学习',
[WordPracticeMode.Free]: '自由练习',
[WordPracticeMode.IdentifyOnly]: '自测',
[WordPracticeMode.DictationOnly]: '默写',
[WordPracticeMode.ListenOnly]: '听写',
[WordPracticeMode.Shuffle]: '随机复习',
[WordPracticeMode.Review]: '复习',
}

View File

@@ -1,4 +1,4 @@
import { PracticeData, TaskWords } from '@/types/types.ts'
import type { PracticeData, TaskWords } from '@/types/types.ts'
import { PracticeState } from '@/stores/practice.ts'
import { IS_DEV } from '@/config/env'

View File

@@ -1,14 +1,15 @@
import { BaseState, getDefaultBaseState, useBaseStore } from '@/stores/base.ts'
import { getDefaultSettingState, SettingState } from '@/stores/setting.ts'
import { Dict, DictId, DictResource, DictType } from '@/types/types.ts'
import type { Dict, DictResource } from '@/types/types.ts'
import { useRouter } from 'vue-router'
import { useRuntimeStore } from '@/stores/runtime.ts'
import dayjs from 'dayjs'
import { AppEnv, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY } from '@/config/env.ts'
import { AppEnv, DictId, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY } from '@/config/env.ts'
import { nextTick } from 'vue'
import Toast from '@/components/base/toast/Toast.ts'
import { getDefaultDict, getDefaultWord } from '@/types/func.ts'
import duration from 'dayjs/plugin/duration'
import {DictType} from "@/types/enum.ts";
dayjs.extend(duration)