Merge branch 'refs/heads/dev'

# Conflicts:
#	src/utils/index.ts
This commit is contained in:
Zyronon
2026-01-08 23:39:28 +08:00
69 changed files with 491 additions and 616 deletions

24
components.d.ts vendored
View File

@@ -9,6 +9,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
About: typeof import('./src/components/About.vue')['default']
ArticleAudio: typeof import('./src/components/article/components/ArticleAudio.vue')['default']
ArticleList: typeof import('./src/components/list/ArticleList.vue')['default']
ArticleSetting: typeof import('./src/components/setting/ArticleSetting.vue')['default']
Audio: typeof import('./src/components/base/Audio.vue')['default']
@@ -20,9 +21,11 @@ declare module 'vue' {
BasePage: typeof import('./src/components/BasePage.vue')['default']
BaseTable: typeof import('./src/components/BaseTable.vue')['default']
Book: typeof import('./src/components/Book.vue')['default']
ChangeLastPracticeIndexDialog: typeof import('./src/components/word/components/ChangeLastPracticeIndexDialog.vue')['default']
ChannelIcons: typeof import('./src/components/ChannelIcons/ChannelIcons.vue')['default']
Checkbox: typeof import('./src/components/base/checkbox/Checkbox.vue')['default']
Close: typeof import('./src/components/icon/Close.vue')['default']
Code: typeof import('./src/components/user/Code.vue')['default']
Collapse: typeof import('./src/components/base/Collapse.vue')['default']
CommonSetting: typeof import('./src/components/setting/CommonSetting.vue')['default']
ConflictNotice: typeof import('./src/components/ConflictNotice.vue')['default']
@@ -32,10 +35,15 @@ declare module 'vue' {
DictGroup: typeof import('./src/components/list/DictGroup.vue')['default']
DictList: typeof import('./src/components/list/DictList.vue')['default']
EditAbleText: typeof import('./src/components/EditAbleText.vue')['default']
EditArticle: typeof import('./src/components/article/components/EditArticle.vue')['default']
EditBook: typeof import('./src/components/article/components/EditBook.vue')['default']
EditSingleArticleModal: typeof import('./src/components/article/components/EditSingleArticleModal.vue')['default']
Empty: typeof import('./src/components/Empty.vue')['default']
Footer: typeof import('./src/components/word/components/Footer.vue')['default']
Form: typeof import('./src/components/base/form/Form.vue')['default']
FormItem: typeof import('./src/components/base/form/FormItem.vue')['default']
Github: typeof import('./src/components/ChannelIcons/Github.vue')['default']
GroupList: typeof import('./src/components/word/components/GroupList.vue')['default']
Header: typeof import('./src/components/Header.vue')['default']
IconBxVolume: typeof import('~icons/bx/volume')['default']
IconBxVolumeFull: typeof import('~icons/bx/volume-full')['default']
@@ -52,7 +60,6 @@ declare module 'vue' {
IconFluentArrowClockwise20Regular: typeof import('~icons/fluent/arrow-clockwise20-regular')['default']
IconFluentArrowDownload20Regular: typeof import('~icons/fluent/arrow-download20-regular')['default']
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
IconFluentArrowRepeatAll20Regular: typeof import('~icons/fluent/arrow-repeat-all20-regular')['default']
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
@@ -138,34 +145,49 @@ declare module 'vue' {
IconUiwQq: typeof import('~icons/uiw/qq')['default']
InputNumber: typeof import('./src/components/base/InputNumber.vue')['default']
List: typeof import('./src/components/list/List.vue')['default']
Log: typeof import('./src/components/setting/Log.vue')['default']
Logo: typeof import('./src/components/Logo.vue')['default']
MigrateDialog: typeof import('./src/components/MigrateDialog.vue')['default']
MiniDialog: typeof import('./src/components/dialog/MiniDialog.vue')['default']
Notice: typeof import('./src/components/user/Notice.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']
PracticeLayout: typeof import('./src/components/PracticeLayout.vue')['default']
PracticeSettingDialog: typeof import('./src/components/word/components/PracticeSettingDialog.vue')['default']
PracticeWordListDialog: typeof import('./src/components/word/components/PracticeWordListDialog.vue')['default']
Progress: typeof import('./src/components/base/Progress.vue')['default']
QuestionForm: typeof import('./src/components/article/components/QuestionForm.vue')['default']
QuestionItem: typeof import('./src/components/article/components/QuestionItem.vue')['default']
Radio: typeof import('./src/components/base/radio/Radio.vue')['default']
RadioGroup: typeof import('./src/components/base/radio/RadioGroup.vue')['default']
ResourceCard: typeof import('./src/components/ResourceCard.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('./src/components/base/select/Select.vue')['default']
SentenceHightLightWord: typeof import('./src/components/word/components/SentenceHightLightWord.vue')['default']
SettingDialog: typeof import('./src/components/setting/SettingDialog.vue')['default']
SettingItem: typeof import('./src/components/setting/SettingItem.vue')['default']
ShareIcon: typeof import('./src/components/ChannelIcons/ShareIcon.vue')['default']
ShufflePracticeSettingDialog: typeof import('./src/components/word/components/ShufflePracticeSettingDialog.vue')['default']
Slide: typeof import('./src/components/Slide.vue')['default']
SlideHorizontal: typeof import('./src/components/slide/SlideHorizontal.vue')['default']
SlideItem: typeof import('./src/components/slide/SlideItem.vue')['default']
Slider: typeof import('./src/components/base/Slider.vue')['default']
Space: typeof import('./src/components/article/components/Space.vue')['default']
StageProgress: typeof import('./src/components/StageProgress.vue')['default']
Statistics: typeof import('./src/components/word/components/Statistics.vue')['default']
Switch: typeof import('./src/components/base/Switch.vue')['default']
Textarea: typeof import('./src/components/base/Textarea.vue')['default']
Toast: typeof import('./src/components/base/toast/Toast.vue')['default']
Tooltip: typeof import('./src/components/base/Tooltip.vue')['default']
TypeWord: typeof import('./src/components/word/components/TypeWord.vue')['default']
TypingArticle: typeof import('./src/components/article/components/TypingArticle.vue')['default']
TypingWord: typeof import('./src/components/article/components/TypingWord.vue')['default']
VolumeIcon: typeof import('./src/components/icon/VolumeIcon.vue')['default']
VolumeSettingMiniDialog: typeof import('./src/components/word/components/VolumeSettingMiniDialog.vue')['default']
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']

View File

@@ -178,6 +178,6 @@
document.body.appendChild(dialog)
})()
</script>
<script type="module" src="/src/main.ts"></script>
<script type="module" src="/src/main"></script>
</body>
</html>

View File

@@ -1,16 +1,16 @@
<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 { BaseState, useBaseStore } from '@/stores/base'
import { useRuntimeStore } from '@/stores/runtime'
import { useSettingStore } from '@/stores/setting'
import useTheme from '@/hooks/theme'
import { loadJsLib, shakeCommonDict } from '@/utils'
import { get, set } from 'idb-keyval'
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 { APP_VERSION, AppEnv, DictId, LOCAL_FILE_KEY, Origin, SAVE_DICT_KEY, SAVE_SETTING_KEY } from '@/config/env'
import { syncSetting } from '@/apis'
import { useUserStore } from '@/stores/user.ts'
import { useUserStore } from '@/stores/user'
import MigrateDialog from '@/components/MigrateDialog.vue'
const store = useBaseStore()

View File

@@ -1,4 +1,4 @@
import http, {axiosInstance, AxiosResponse} from "@/utils/http.ts";
import http, {axiosInstance, AxiosResponse} from "@/utils/http";
import type { Dict } from "@/types/types.ts";
import { cloneDeep } from "@/utils";

View File

@@ -1,4 +1,4 @@
import http from '@/utils/http.ts'
import http from '@/utils/http'
export type LevelBenefits = {
"level": {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { Dict } from "@/types/types.ts";
import type { Dict } from "@/types/types";
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 {useSettingStore} from "@/stores/setting.ts";
import {useSettingStore} from "@/stores/setting";
import Close from "@/components/icon/Close.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import {ShortcutKey} from "@/types/enum.ts";
import {ShortcutKey} from "@/types/enum";
const settingStore = useSettingStore()
let tabIndex = $ref(0)

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { useSettingStore } from '@/stores/setting.ts'
import { useSettingStore } from '@/stores/setting'
const settingStore = useSettingStore()
defineProps<{

View File

@@ -17,7 +17,7 @@ import Tooltip from '@/components/base/Tooltip.vue'
import InputNumber from '@/components/base/InputNumber.vue'
import { nanoid } from 'nanoid'
import { update } from 'idb-keyval'
import ArticleAudio from '@/pages/article/components/ArticleAudio.vue'
import ArticleAudio from '@/components/article/components/ArticleAudio.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import Textarea from '@/components/base/Textarea.vue'
import { LOCAL_FILE_KEY } from '@/config/env.ts'

View File

@@ -2,7 +2,7 @@
import type {Article} from "@/types/types.ts";
import {useDisableEventListener} from "@/hooks/event.ts";
import EditArticle from "@/pages/article/components/EditArticle.vue";
import EditArticle from "@/components/article/components/EditArticle.vue";
import {getDefaultArticle} from "@/types/func.ts";
import {defineAsyncComponent} from "vue";

View File

@@ -1,26 +1,26 @@
<script setup lang="ts">
import Toast from '@/components/base/toast/Toast.ts'
import Toast from '@/components/base/toast/Toast'
import BaseButton from '@/components/BaseButton.vue'
import { useWordOptions } from '@/hooks/dict.ts'
import { usePlayBeep, usePlayKeyboardAudio, usePlayWordAudio } from '@/hooks/sound.ts'
import QuestionForm from '@/pages/article/components/QuestionForm.vue'
import Space from '@/pages/article/components/Space.vue'
import TypingWord from '@/pages/article/components/TypingWord.vue'
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 type { Article, ArticleWord, Sentence, Word } from '@/types/types.ts'
import { useWordOptions } from '@/hooks/dict'
import { usePlayBeep, usePlayKeyboardAudio, usePlayWordAudio } from '@/hooks/sound'
import QuestionForm from '@/components/article/components/QuestionForm.vue'
import Space from '@/components/article/components/Space.vue'
import TypingWord from '@/components/article/components/TypingWord.vue'
import { useBaseStore } from '@/stores/base'
import { usePracticeStore } from '@/stores/practice'
import { useSettingStore } from '@/stores/setting'
import { getDefaultArticle, getDefaultWord } from '@/types/func'
import type { Article, ArticleWord, Sentence, Word } from '@/types/types'
import { _dateFormat, _nextTick, isMobile, msToHourMinute, total } from '@/utils'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import { emitter, EventKey, useEvents } from '@/utils/eventBus'
import ContextMenu from '@imengyu/vue3-context-menu'
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
import nlp from 'compromise/three'
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'
import { getPracticeArticleCache, setPracticeArticleCache } from '@/utils/cache'
import { PracticeArticleWordType, ShortcutKey } from '@/types/enum'
interface IProps {
article: Article

View File

@@ -1,6 +1,6 @@
<script setup lang="tsx">
import {useSettingStore} from "@/stores/setting.ts";
import Space from "@/pages/article/components/Space.vue";
import Space from "@/components/article/components/Space.vue";
import {PracticeArticleWordType} from "@/types/enum.ts";
import type {ArticleWord} from "@/types/types.ts";

View File

@@ -1,5 +1,5 @@
import {createVNode, render} from 'vue'
import ToastComponent from '@/components/base/toast/Toast.vue'
import ToastComponent from '@/components/base/toast/ToastComponent.vue'
import type {ToastOptions, ToastInstance, ToastService} from '@/components/base/toast/type.ts'
interface ToastContainer {

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import { onMounted, onUnmounted, watch } from 'vue'
import Tooltip from '@/components/base/Tooltip.vue'
import { useEventListener } from '@/hooks/event.ts'
import { useEventListener } from '@/hooks/event'
import BaseButton from '@/components/BaseButton.vue'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useRuntimeStore } from '@/stores/runtime'
export interface ModalProps {
modelValue?: boolean

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import Switch from '@/components/base/Switch.vue'
import Slider from '@/components/base/Slider.vue'
import SettingItem from '@/pages/setting/SettingItem.vue'
import SettingItem from '@/components/setting/SettingItem.vue'
import { useSettingStore } from '@/stores/setting.ts'
const settingStore = useSettingStore()

View File

@@ -6,7 +6,7 @@ import { Option, Select } from '@/components/base/select'
import Textarea from '@/components/base/Textarea.vue'
import VolumeIcon from '@/components/icon/VolumeIcon.vue'
import Slider from '@/components/base/Slider.vue'
import SettingItem from '@/pages/setting/SettingItem.vue'
import SettingItem from '@/components/setting/SettingItem.vue'
import { useSettingStore } from '@/stores/setting.ts'
import { useBaseStore } from '@/stores/base.ts'
import {ShortcutKey} from "@/types/enum.ts";

View File

@@ -4,7 +4,7 @@ import Switch from "@/components/base/Switch.vue";
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
import InputNumber from "@/components/base/InputNumber.vue";
import Slider from "@/components/base/Slider.vue";
import SettingItem from "@/pages/setting/SettingItem.vue";
import SettingItem from "@/components/setting/SettingItem.vue";
import Radio from "@/components/base/radio/Radio.vue";
import { useSettingStore } from "@/stores/setting.ts";
const settingStore = useSettingStore()

View File

@@ -1,17 +1,17 @@
<script setup lang="ts">
import { inject, Ref } from 'vue'
import { usePracticeStore } from '@/stores/practice.ts'
import { useSettingStore } from '@/stores/setting.ts'
import type { PracticeData, TaskWords } from '@/types/types.ts'
import { usePracticeStore } from '@/stores/practice'
import { useSettingStore } from '@/stores/setting'
import type { PracticeData, TaskWords } from '@/types/types'
import BaseIcon from '@/components/BaseIcon.vue'
import Tooltip from '@/components/base/Tooltip.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 { useBaseStore } from '@/stores/base'
import VolumeSettingMiniDialog from '@/components/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'
import { ShortcutKey, WordPracticeMode, WordPracticeStage } from '@/types/enum'
import { WordPracticeModeNameMap, WordPracticeModeStageMap, WordPracticeStageNameMap } from '@/config/env'
const statStore = usePracticeStore()
const store = useBaseStore()

View File

@@ -4,11 +4,11 @@ import BaseButton from '@/components/BaseButton.vue'
import Checkbox from '@/components/base/checkbox/Checkbox.vue'
import Slider from '@/components/base/Slider.vue'
import { defineAsyncComponent, watch } from 'vue'
import { useSettingStore } from '@/stores/setting.ts'
import Toast from '@/components/base/toast/Toast.ts'
import ChangeLastPracticeIndexDialog from '@/pages/word/components/ChangeLastPracticeIndexDialog.vue'
import { useSettingStore } from '@/stores/setting'
import Toast from '@/components/base/toast/Toast'
import ChangeLastPracticeIndexDialog from '@/components/word/components/ChangeLastPracticeIndexDialog.vue'
import Tooltip from '@/components/base/Tooltip.vue'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useRuntimeStore } from '@/stores/runtime'
import BaseInput from '@/components/base/BaseInput.vue'
import InputNumber from '@/components/base/InputNumber.vue'

View File

@@ -1,19 +1,19 @@
<script setup lang="ts">
import type { Word } from '@/types/types.ts'
import type { Word } from '@/types/types'
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 { useSettingStore } from '@/stores/setting'
import { usePlayBeep, usePlayCorrect, usePlayKeyboardAudio, usePlayWordAudio } from '@/hooks/sound'
import { emitter, EventKey, useEvents } from '@/utils/eventBus'
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 SentenceHightLightWord from '@/components/word/components/SentenceHightLightWord.vue'
import { usePracticeStore } from '@/stores/practice'
import { getDefaultWord } from '@/types/func'
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 Space from '@/components/article/components/Space.vue'
import Toast from '@/components/base/toast/Toast'
import Tooltip from '@/components/base/Tooltip.vue'
import { ShortcutKey, WordPracticeStage, WordPracticeType } from '@/types/enum.ts'
import { ShortcutKey, WordPracticeStage, WordPracticeType } from '@/types/enum'
interface IProps {
word: Word

View File

@@ -1,5 +1,5 @@
import { offset } from '@floating-ui/dom'
import { ShortcutKey, WordPracticeMode, WordPracticeStage } from '@/types/enum.ts'
import { ShortcutKey, WordPracticeMode, WordPracticeStage } from '@/types/enum'
export const GITHUB = 'https://github.com/zyronon/TypeWords'
export const Host = 'typewords.cc'
@@ -13,6 +13,7 @@ const common = {
const map = {
DEV: {
API: 'http://localhost/',
RESOURCE_URL: 'https://dicts.2study.top/',
},
}

View File

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

View File

@@ -8,15 +8,15 @@ import {
Origin,
SAVE_DICT_KEY,
SAVE_SETTING_KEY,
} from '@/config/env.ts'
} from '@/config/env'
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 Toast from '@/components/base/toast/Toast'
import { useBaseStore } from '@/stores/base'
import { useSettingStore } from '@/stores/setting'
import { ref } from 'vue'
import { PRACTICE_ARTICLE_CACHE, PRACTICE_WORD_CACHE } from '@/utils/cache.ts'
import { PRACTICE_ARTICLE_CACHE, PRACTICE_WORD_CACHE } from '@/utils/cache'
export function useExport() {
const store = useBaseStore()

View File

@@ -1,7 +1,7 @@
import {onMounted, watchEffect} from "vue"
import {useSettingStore} from "@/stores/setting.ts";
import {useSettingStore} from "@/stores/setting";
import { PronunciationApi, SoundFileOptions } from '@/config/env.ts'
import { PronunciationApi, SoundFileOptions } from '@/config/env'
export function useSound(audioSrcList?: string[], audioFileLength?: number) {
let audioList: HTMLAudioElement[] = $ref([])

7
src/layout/empty.vue Normal file
View File

@@ -0,0 +1,7 @@
<script setup lang="ts">
</script>
<template>
<slot></slot>
</template>

View File

@@ -1,12 +1,12 @@
import {createApp} from 'vue'
import './assets/css/style.scss'
import './assets/css/main.scss'
import 'virtual:uno.css';
import App from './App.vue'
import {createPinia} from "pinia"
import router from "@/router.ts";
import router from "@/router";
import VueVirtualScroller from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import './types/global.d.ts'
import './types/global.d'
import loadingDirective from './directives/loading.tsx'

View File

@@ -9,7 +9,7 @@ import { useWindowClick } from '@/hooks/event.ts'
import { MessageBox } from '@/utils/MessageBox.tsx'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { nanoid } from 'nanoid'
import EditArticle from '@/pages/article/components/EditArticle.vue'
import EditArticle from '@/components/article/components/EditArticle.vue'
import Toast from '@/components/base/toast/Toast.ts'
import { getDefaultArticle } from '@/types/func.ts'
import BackIcon from '@/components/BackIcon.vue'
@@ -283,7 +283,7 @@ function updateList(e) {
:loading="exportLoading"
:disabled="!article.id"
@click="exportData({ type: 'item', data: article })"
>当前
>当前
</BaseButton>
</div>
</MiniDialog>

View File

@@ -7,12 +7,12 @@ 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'
import EditBook from '@/pages/article/components/EditBook.vue'
import EditBook from '@/components/article/components/EditBook.vue'
import { computed, onMounted, onUnmounted, watch } from 'vue'
import { _dateFormat, _getDictDataByUrl, _nextTick, msToHourMinute, resourceWrap, total, useNav } from '@/utils'
import { getDefaultArticle, getDefaultDict } from '@/types/func.ts'
import Toast from '@/components/base/toast/Toast.ts'
import ArticleAudio from '@/pages/article/components/ArticleAudio.vue'
import ArticleAudio from '@/components/article/components/ArticleAudio.vue'
import { MessageBox } from '@/utils/MessageBox.tsx'
import { useSettingStore } from '@/stores/setting.ts'
import { useFetch } from '@vueuse/core'
@@ -319,7 +319,7 @@ watch([() => displayMode, () => selectArticle.id, () => showTranslate], () => {
v-for="(s, n) in w.split(' ').filter(Boolean)"
:class="`inline-block word-${i}-${j}-${n}`"
:key="`${i}-${j}-${n}`"
><span>{{ s }}</span>
><span>{{ s }}</span>
<span class="space"></span>
</span>
</span>

View File

@@ -38,10 +38,10 @@ const searchList = computed<any[]>(() => {
let s = searchKey.toLowerCase()
return bookList.value.filter((item) => {
return item.id.toLowerCase().includes(s)
|| item.name.toLowerCase().includes(s)
|| item.category.toLowerCase().includes(s)
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|| item?.url?.toLowerCase?.().includes?.(s)
|| item.name.toLowerCase().includes(s)
|| item.category.toLowerCase().includes(s)
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|| item?.url?.toLowerCase?.().includes?.(s)
})
}
return []
@@ -68,20 +68,20 @@ const searchList = computed<any[]>(() => {
</div>
<div class="mt-4" v-if="searchKey">
<DictList
v-if="searchList.length "
@selectDict="selectDict"
:list="searchList"
quantifier="篇"
:select-id="'-1'"/>
v-if="searchList.length "
@selectDict="selectDict"
:list="searchList"
quantifier="篇"
:select-id="'-1'"/>
<Empty v-else text="没有相关书籍"/>
</div>
<div class="w-full mt-2" v-else>
<DictList
v-if="bookList?.length "
@selectDict="selectDict"
:list="bookList"
quantifier="篇"
:select-id="'-1'"/>
v-if="bookList?.length "
@selectDict="selectDict"
:list="bookList"
quantifier="篇"
:select-id="'-1'"/>
</div>
</div>
</BasePage>

View File

@@ -13,9 +13,9 @@ import { genArticleSectionData, usePlaySentenceAudio } from '@/hooks/article.ts'
import { useArticleOptions } from '@/hooks/dict.ts'
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from '@/hooks/event.ts'
import useTheme from '@/hooks/theme.ts'
import ArticleAudio from '@/pages/article/components/ArticleAudio.vue'
import EditSingleArticleModal from '@/pages/article/components/EditSingleArticleModal.vue'
import TypingArticle from '@/pages/article/components/TypingArticle.vue'
import ArticleAudio from '@/components/article/components/ArticleAudio.vue'
import EditSingleArticleModal from '@/components/article/components/EditSingleArticleModal.vue'
import TypingArticle from '@/components/article/components/TypingArticle.vue'
import { useBaseStore } from '@/stores/base.ts'
import { usePracticeStore } from '@/stores/practice.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'

View File

@@ -10,16 +10,15 @@ import { accountRules, codeRules, passwordRules, phoneRules } from '@/utils/vali
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 Notice from '@/components/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 Code from '@/components/user/Code.vue'
import { 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 { getProgress, uploadImportData } from '@/apis'
import { CodeType, ImportStatus } from '@/types/enum.ts'
//

View File

@@ -1,23 +1,23 @@
<script setup lang="ts">
import {onMounted} from 'vue'
import {useUserStore} from '@/stores/user.ts'
import {useRouter} from 'vue-router'
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/user.ts'
import { useRouter } from 'vue-router'
import BaseInput from '@/components/base/BaseInput.vue'
import BasePage from "@/components/BasePage.vue";
import {APP_NAME, EMAIL, GITHUB} from "@/config/env.ts";
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 FormItem from "@/components/base/form/FormItem.vue";
import Form from "@/components/base/form/Form.vue";
import {FormInstance} from "@/components/base/form/types.ts";
import {codeRules, emailRules, passwordRules, phoneRules} from "@/utils/validation.ts";
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";
import BasePage from '@/components/BasePage.vue'
import { APP_NAME, EMAIL } from '@/config/env.ts'
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 FormItem from '@/components/base/form/FormItem.vue'
import Form from '@/components/base/form/Form.vue'
import { FormInstance } from '@/components/base/form/types.ts'
import { codeRules, emailRules, passwordRules, phoneRules } from '@/utils/validation.ts'
import { cloneDeep, jump2Feedback } from '@/utils'
import Toast from '@/components/base/toast/Toast.ts'
import Code from '@/components/user/Code.vue'
import { MessageBox } from '@/utils/MessageBox.tsx'
import { CodeType } from '@/types/enum.ts'
const userStore = useUserStore()
const router = useRouter()
@@ -45,19 +45,23 @@ onMounted(() => {
//
//
let changePhoneFormRef = $ref<FormInstance>()
let defaultFrom = {oldCode: '', phone: '', code: '', pwd: '',}
let defaultFrom = { oldCode: '', phone: '', code: '', pwd: '' }
let changePhoneForm = $ref(cloneDeep(defaultFrom))
let changePhoneFormRules = {
oldCode: codeRules,
phone: [...phoneRules, {
validator: (rule: any, value: any) => {
if (userStore.user?.phone && value === userStore.user?.phone) {
throw new Error('新手机号与原手机号一致')
}
}, trigger: 'blur'
},],
phone: [
...phoneRules,
{
validator: (rule: any, value: any) => {
if (userStore.user?.phone && value === userStore.user?.phone) {
throw new Error('新手机号与原手机号一致')
}
},
trigger: 'blur',
},
],
code: codeRules,
pwd: passwordRules
pwd: passwordRules,
}
function showChangePhoneForm() {
@@ -92,15 +96,15 @@ function changePhone() {
//
//
let changeUsernameFormRef = $ref<FormInstance>()
let changeUsernameForm = $ref({username: ''})
let changeUsernameForm = $ref({ username: '' })
let changeUsernameFormRules = {
username: [{required: true, message: '请输入用户名', trigger: 'blur'}],
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
}
function showChangeUsernameForm() {
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
showChangeUsername = true
changeUsernameForm = cloneDeep({username: userStore.user?.username ?? '',})
changeUsernameForm = cloneDeep({ username: userStore.user?.username ?? '' })
}
function changeUsername() {
@@ -137,13 +141,15 @@ let changeEmailForm = $ref({
})
let changeEmailFormRules = {
email: [
...emailRules, {
...emailRules,
{
validator: (rule: any, value: any) => {
if (userStore.user?.email && value === userStore.user?.email) {
throw new Error('该邮箱与当前一致')
}
}, trigger: 'blur'
}
},
trigger: 'blur',
},
],
pwd: passwordRules,
code: codeRules,
@@ -152,7 +158,7 @@ let changeEmailFormRules = {
function showChangeEmailForm() {
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
showChangeEmail = true
changeEmailForm = cloneDeep({email: userStore.user?.email ?? '', pwd: '', code: '',})
changeEmailForm = cloneDeep({ email: userStore.user?.email ?? '', pwd: '', code: '' })
}
function changeEmail() {
@@ -191,13 +197,14 @@ let changePwdFormRules = {
oldPwd: passwordRules,
newPwd: passwordRules,
confirmPwd: [
{required: true, message: '请再次输入新密码', trigger: 'blur'},
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{
validator: (rule: any, value: any) => {
if (value !== changePwdForm.newPwd) {
throw new Error('两次密码输入不一致')
}
}, trigger: 'blur'
},
trigger: 'blur',
},
],
}
@@ -230,7 +237,7 @@ function changePwd() {
})
}
const member = $computed<User['member']>(() => userStore.user?.member ?? {} as any)
const member = $computed<User['member']>(() => userStore.user?.member ?? ({} as any))
const memberEndDate = $computed(() => {
if (member?.endDate === null) return '永久'
@@ -239,12 +246,10 @@ const memberEndDate = $computed(() => {
function subscribe() {
router.push('/vip')
}
function onFileChange(e) {
console.log('e', e)
}
</script>
@@ -254,21 +259,15 @@ function onFileChange(e) {
<div v-if="!userStore.isLogin" class="center h-screen">
<div class="card-white text-center flex-col gap-6 w-110">
<div class="w-20 h-20 bg-blue-100 rounded-full center mx-auto">
<IconFluentPerson20Regular class="text-3xl text-blue-600"/>
<IconFluentPerson20Regular class="text-3xl text-blue-600" />
</div>
<h1 class="text-2xl font-bold">
<IconFluentHandWave20Regular class="text-xl translate-y-1 mr-2 shrink-0"/>
<IconFluentHandWave20Regular class="text-xl translate-y-1 mr-2 shrink-0" />
<span>欢迎使用</span>
</h1>
<p class="">登录开启您的学习之旅</p>
<div>保存进度同步数据解锁个性化内容</div>
<BaseButton
@click="router.push('/login')"
size="large"
class="w-full mt-4"
>
登录
</BaseButton>
<BaseButton @click="router.push('/login')" size="large" class="w-full mt-4"> 登录 </BaseButton>
<p class="text-sm text-gray-500">
还没有账户
<router-link to="/login?register=1" class="line">立即注册</router-link>
@@ -287,20 +286,17 @@ function onFileChange(e) {
<div class="flex-1">
<div class="mb-2">用户名</div>
<div class="flex items-center gap-2" v-if="userStore.user?.username">
<IconFluentPerson20Regular class="text-base"/>
<IconFluentPerson20Regular class="text-base" />
<span>{{ userStore.user?.username }}</span>
</div>
<div v-else class="text-xs">在此设置用户名</div>
</div>
<BaseIcon @click="showChangeUsernameForm">
<IconFluentTextEditStyle20Regular/>
<IconFluentTextEditStyle20Regular />
</BaseIcon>
</div>
<div v-if="showChangeUsername">
<Form
ref="changeUsernameFormRef"
:rules="changeUsernameFormRules"
:model="changeUsernameForm">
<Form ref="changeUsernameFormRef" :rules="changeUsernameFormRules" :model="changeUsernameForm">
<FormItem prop="username">
<BaseInput
v-model="changeUsernameForm.username"
@@ -310,7 +306,7 @@ function onFileChange(e) {
autofocus
>
<template #preIcon>
<IconFluentPerson20Regular class="text-base"/>
<IconFluentPerson20Regular class="text-base" />
</template>
</BaseInput>
</FormItem>
@@ -327,20 +323,17 @@ function onFileChange(e) {
<div class="flex-1">
<div class="mb-2">手机号</div>
<div class="flex items-center gap-2" v-if="userStore.user?.phone">
<IconFluentMail20Regular class="text-base"/>
<IconFluentMail20Regular class="text-base" />
<span>{{ userStore.user?.phone }}</span>
</div>
<div v-else class="text-xs">在此设置手机号</div>
</div>
<BaseIcon @click="showChangePhoneForm">
<IconFluentTextEditStyle20Regular/>
<IconFluentTextEditStyle20Regular />
</BaseIcon>
</div>
<div v-if="showChangePhone">
<Form
ref="changePhoneFormRef"
:rules="changePhoneFormRules"
:model="changePhoneForm">
<Form ref="changePhoneFormRef" :rules="changePhoneFormRules" :model="changePhoneForm">
<FormItem prop="oldCode" v-if="userStore.user?.phone">
<div class="flex gap-2">
<BaseInput
@@ -350,18 +343,11 @@ function onFileChange(e) {
placeholder="请输入原手机号验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<Code :validate-field="() => true"
:type="CodeType.ChangePhoneOld"
:val="userStore.user.phone"/>
<Code :validate-field="() => true" :type="CodeType.ChangePhoneOld" :val="userStore.user.phone" />
</div>
</FormItem>
<FormItem prop="phone">
<BaseInput
v-model="changePhoneForm.phone"
type="tel"
size="large"
placeholder="请输入新手机号"
/>
<BaseInput v-model="changePhoneForm.phone" type="tel" size="large" placeholder="请输入新手机号" />
</FormItem>
<FormItem prop="code">
<div class="flex gap-2">
@@ -371,24 +357,24 @@ function onFileChange(e) {
placeholder="请输入新手机号验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<Code :validate-field="() => changePhoneFormRef.validateField('phone')"
:type="CodeType.ChangePhoneNew"
:val="changePhoneForm.phone"/>
<Code
:validate-field="() => changePhoneFormRef.validateField('phone')"
:type="CodeType.ChangePhoneNew"
:val="changePhoneForm.phone"
/>
</div>
</FormItem>
<FormItem prop="pwd" v-if="!userStore.user?.phone">
<BaseInput
v-model="changePhoneForm.pwd"
type="password"
size="large"
placeholder="请输入原密码"
/>
<BaseInput v-model="changePhoneForm.pwd" type="password" size="large" placeholder="请输入原密码" />
</FormItem>
</Form>
<div class="flex justify-between items-end mb-2">
<span class="link text-sm cp"
@click="MessageBox.notice(`请提供证明信息发送邮件到 ${EMAIL} 进行申诉`,'人工申诉')"
v-if="userStore.user?.phone">原手机号不可用点此申诉</span>
<span
class="link text-sm cp"
@click="MessageBox.notice(`请提供证明信息发送邮件到 ${EMAIL} 进行申诉`, '人工申诉')"
v-if="userStore.user?.phone"
>原手机号不可用点此申诉</span
>
<span v-else></span>
<div>
<BaseButton type="info" @click="showChangePhone = false">取消</BaseButton>
@@ -403,20 +389,17 @@ function onFileChange(e) {
<div class="flex-1">
<div class="mb-2">电子邮箱</div>
<div class="flex items-center gap-2" v-if="userStore.user?.email">
<IconFluentMail20Regular class="text-base"/>
<IconFluentMail20Regular class="text-base" />
<span>{{ userStore.user?.email }}</span>
</div>
<div v-else class="text-xs">在此设置邮箱</div>
</div>
<BaseIcon @click="showChangeEmailForm">
<IconFluentTextEditStyle20Regular/>
<IconFluentTextEditStyle20Regular />
</BaseIcon>
</div>
<div v-if="showChangeEmail">
<Form
ref="changeEmailFormRef"
:rules="changeEmailFormRules"
:model="changeEmailForm">
<Form ref="changeEmailFormRef" :rules="changeEmailFormRules" :model="changeEmailForm">
<FormItem prop="email">
<BaseInput
v-model="changeEmailForm.email"
@@ -434,18 +417,15 @@ function onFileChange(e) {
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<Code :validate-field="() => changeEmailFormRef.validateField('email')"
:type="CodeType.ChangeEmail"
:val="changeEmailForm.email"/>
<Code
:validate-field="() => changeEmailFormRef.validateField('email')"
:type="CodeType.ChangeEmail"
:val="changeEmailForm.email"
/>
</div>
</FormItem>
<FormItem prop="pwd" v-if="userStore.user?.hasPwd">
<BaseInput
v-model="changePwdForm.pwd"
type="password"
size="large"
placeholder="请输入密码"
/>
<BaseInput v-model="changePwdForm.pwd" type="password" size="large" placeholder="请输入密码" />
</FormItem>
</Form>
<div class="text-align-end mb-2">
@@ -455,7 +435,6 @@ function onFileChange(e) {
</div>
<div class="line"></div>
<!-- Password Section -->
<div class="item">
<div class="flex-1">
@@ -463,22 +442,13 @@ function onFileChange(e) {
<div class="text-xs">在此输入密码</div>
</div>
<BaseIcon @click="showChangePwdForm">
<IconFluentTextEditStyle20Regular/>
<IconFluentTextEditStyle20Regular />
</BaseIcon>
</div>
<div v-if="showChangePwd">
<Form
ref="changePwdFormRef"
:rules="changePwdFormRules"
:model="changePwdForm">
<Form ref="changePwdFormRef" :rules="changePwdFormRules" :model="changePwdForm">
<FormItem prop="oldPwd" v-if="userStore.user.hasPwd">
<BaseInput
v-model="changePwdForm.oldPwd"
placeholder="旧密码"
type="password"
size="large"
autofocus
/>
<BaseInput v-model="changePwdForm.oldPwd" placeholder="旧密码" type="password" size="large" autofocus />
</FormItem>
<FormItem prop="newPwd">
@@ -510,15 +480,10 @@ function onFileChange(e) {
</div>
<div class="line"></div>
<!-- Contact Support -->
<div class="item cp"
v-if="false"
@click="contactSupport">
<div class="flex-1">
联系 {{ APP_NAME }} 客服
</div>
<IconFluentChevronLeft28Filled class="rotate-180"/>
<div class="item cp" v-if="false" @click="contactSupport">
<div class="flex-1">联系 {{ APP_NAME }} 客服</div>
<IconFluentChevronLeft28Filled class="rotate-180" />
</div>
<!-- <div class="line"></div>-->
@@ -528,32 +493,26 @@ function onFileChange(e) {
<div class="">同步进度</div>
<!-- <div class="text-xs mt-2">在此输入密码</div>-->
</div>
<IconFluentChevronLeft28Filled class="rotate-180"/>
<input type="file" accept=".json,.zip,application/json,application/zip"
@change="onFileChange"
class="absolute left-0 top-0 w-full h-full bg-red cp opacity-0"/>
<IconFluentChevronLeft28Filled class="rotate-180" />
<input
type="file"
accept=".json,.zip,application/json,application/zip"
@change="onFileChange"
class="absolute left-0 top-0 w-full h-full bg-red cp opacity-0"
/>
</div>
<div class="line"></div>
<!-- 去github issue-->
<div class="item cp"
@click="jump2Feedback()">
<div class="flex-1">
{{ APP_NAME }} 提交意见
</div>
<IconFluentChevronLeft28Filled class="rotate-180"/>
<div class="item cp" @click="jump2Feedback()">
<div class="flex-1"> {{ APP_NAME }} 提交意见</div>
<IconFluentChevronLeft28Filled class="rotate-180" />
</div>
<div class="line"></div>
<!-- Logout Button -->
<div class="center w-full mt-4">
<BaseButton
@click="handleLogout"
size="large"
class="w-[40%]"
>
登出
</BaseButton>
<BaseButton @click="handleLogout" size="large" class="w-[40%]"> 登出 </BaseButton>
</div>
<div class="text-xs text-center mt-2">
@@ -566,12 +525,11 @@ function onFileChange(e) {
<!-- Subscription Information -->
<div class="card-white w-80">
<div class="flex items-center gap-3 mb-4">
<IconFluentCrown20Regular class="text-2xl text-yellow-500"/>
<IconFluentCrown20Regular class="text-2xl text-yellow-500" />
<div class="text-lg font-bold">订阅信息</div>
</div>
<div class="space-y-4">
<template v-if="userStore.user?.member">
<div>
<div class="mb-1">当前计划</div>
@@ -581,17 +539,17 @@ function onFileChange(e) {
<div>
<div class="mb-1">状态</div>
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full" :class="member?.active ?'bg-green-500':'bg-red-500'"></div>
<span class="text-base font-medium" :class="member?.active ?'text-green-700':'text-red-700'">
{{ member?.status }}
</span>
<div class="w-2 h-2 rounded-full" :class="member?.active ? 'bg-green-500' : 'bg-red-500'"></div>
<span class="text-base font-medium" :class="member?.active ? 'text-green-700' : 'text-red-700'">
{{ member?.status }}
</span>
</div>
</div>
<div>
<div class="mb-1">到期时间</div>
<div class="flex items-center gap-2">
<IconFluentCalendarDate20Regular class="text-lg"/>
<IconFluentCalendarDate20Regular class="text-lg" />
<span class="text-base font-medium">{{ memberEndDate }}</span>
</div>
</div>
@@ -599,22 +557,18 @@ function onFileChange(e) {
<div>
<div class="mb-1">自动续费</div>
<div class="flex items-center gap-2">
<div class="w-2 h-2 rounded-full"
:class="member?.autoRenew ? 'bg-blue-500' : 'bg-gray-400'"
></div>
<span class="text-base font-medium"
:class="member?.autoRenew ? 'text-blue-700' : 'text-gray-600'">
{{ member?.autoRenew ? '已开启' : '已关闭' }}
</span>
<div class="w-2 h-2 rounded-full" :class="member?.autoRenew ? 'bg-blue-500' : 'bg-gray-400'"></div>
<span class="text-base font-medium" :class="member?.autoRenew ? 'text-blue-700' : 'text-gray-600'">
{{ member?.autoRenew ? '已开启' : '已关闭' }}
</span>
</div>
</div>
</template>
<div class="text-base" v-else>当前无订阅</div>
<BaseButton class="w-full" size="large" @click="subscribe">{{
userStore.user?.member ? '管理订阅' : '会员介绍'
}}
<BaseButton class="w-full" size="large" @click="subscribe"
>{{ userStore.user?.member ? '管理订阅' : '会员介绍' }}
</BaseButton>
</div>
</div>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts">
import BasePage from '@/components/BasePage.vue'
import BaseButton from '@/components/BaseButton.vue'
import {useRouter} from 'vue-router'
import {useUserStore} from '@/stores/user.ts'
import {User} from "@/apis/user.ts";
import {computed, onMounted, onUnmounted, ref, watch} from "vue";
import Header from "@/components/Header.vue";
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user.ts'
import { User } from '@/apis/user.ts'
import { onMounted, onUnmounted, watch } from 'vue'
import Header from '@/components/Header.vue'
import {
alipayQuery,
CouponInfo,
@@ -14,17 +14,15 @@ import {
levelBenefits,
orderCreate,
orderStatus,
setAutoRenewApi, testPay
} from "@/apis/member.ts";
import Radio from "@/components/base/radio/Radio.vue";
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
import {APP_NAME} from "@/config/env.ts";
import Toast from "@/components/base/toast/Toast.ts";
import {_dateFormat, _nextTick} from "@/utils";
import InputNumber from "@/components/base/InputNumber.vue";
import dayjs from "dayjs";
import BaseInput from "@/components/base/BaseInput.vue";
import PopConfirm from "@/components/PopConfirm.vue";
setAutoRenewApi,
} from '@/apis/member.ts'
import Radio from '@/components/base/radio/Radio.vue'
import RadioGroup from '@/components/base/radio/RadioGroup.vue'
import Toast from '@/components/base/toast/Toast.ts'
import { _dateFormat, _nextTick } from '@/utils'
import InputNumber from '@/components/base/InputNumber.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import PopConfirm from '@/components/PopConfirm.vue'
const router = useRouter()
const userStore = useUserStore()
@@ -38,11 +36,11 @@ interface Plan {
autoRenew?: boolean
}
let loading = $ref(false);
let loading = $ref(false)
let selectedPaymentMethod = $ref('alipay')
let selectedPlanId = $ref('')
let duration = $ref(1)
const member = $computed<User['member']>(() => userStore.user?.member ?? {} as any)
const member = $computed<User['member']>(() => userStore.user?.member ?? ({} as any))
const memberEndDate = $computed(() => {
if (member?.endDate === null) return '永久'
@@ -58,7 +56,7 @@ const plans: Plan[] = $computed(() => {
name: '月付',
price: data.level.price,
unit: '月',
},)
})
list.push({
id: 'month_auto',
name: '连续包月',
@@ -66,14 +64,14 @@ const plans: Plan[] = $computed(() => {
unit: '月',
highlight: '性价比更高',
autoRenew: true,
},)
})
list.push({
id: 'year',
name: '年度会员',
price: data.level.yearly_price,
unit: '年',
highlight: '年度优惠',
},)
})
}
return list
})
@@ -88,8 +86,8 @@ const paymentMethods = [
{
id: 'alipay',
name: '支付宝',
description: '使用支付宝支付'
}
description: '使用支付宝支付',
},
]
const currentPlan = $computed(() => {
@@ -118,36 +116,35 @@ const enoughDiscount = $computed(() => {
})
const endPrice = $computed(() => {
if (!coupon.is_valid) {
return Number(originalPrice.toFixed(2))
}
if (coupon.type === 'free_trial') return 0
if (!enoughDiscount) {
return Number(originalPrice.toFixed(2))
}
let discountAmount = 0
if (coupon.type === 'discount') {
// Discount coupon: e.g., 0.8 means 20% off
const discountRate = Number(coupon.value)
discountAmount = originalPrice * (1 - discountRate)
// Apply max_discount limit if available
if (coupon.max_discount) {
const maxDiscount = Number(coupon.max_discount)
discountAmount = Math.min(discountAmount, maxDiscount)
}
} else if (coupon.type === 'amount') {
// Amount coupon: fixed amount off
discountAmount = Number(coupon.value)
}
const finalPrice = Math.max(originalPrice - discountAmount, 0)
return finalPrice.toFixed(2)
if (!coupon.is_valid) {
return Number(originalPrice.toFixed(2))
}
)
if (coupon.type === 'free_trial') return 0
if (!enoughDiscount) {
return Number(originalPrice.toFixed(2))
}
let discountAmount = 0
if (coupon.type === 'discount') {
// Discount coupon: e.g., 0.8 means 20% off
const discountRate = Number(coupon.value)
discountAmount = originalPrice * (1 - discountRate)
// Apply max_discount limit if available
if (coupon.max_discount) {
const maxDiscount = Number(coupon.max_discount)
discountAmount = Math.min(discountAmount, maxDiscount)
}
} else if (coupon.type === 'amount') {
// Amount coupon: fixed amount off
discountAmount = Number(coupon.value)
}
const finalPrice = Math.max(originalPrice - discountAmount, 0)
return finalPrice.toFixed(2)
})
const startDate = $computed(() => {
if (member?.active) {
@@ -158,18 +155,18 @@ const startDate = $computed(() => {
})
onMounted(async () => {
let res = await levelBenefits({levelCode: 'basic'})
let res = await levelBenefits({ levelCode: 'basic' })
if (res.success) {
data = res.data
}
})
let loading2 = $ref(false);
let loading2 = $ref(false)
async function toggleAutoRenew() {
if (loading2) return
loading2 = true
let res = await setAutoRenewApi({autoRenew: false})
let res = await setAutoRenewApi({ autoRenew: false })
if (res.success) {
Toast.success('取消成功')
userStore.init()
@@ -188,13 +185,13 @@ function getPlanButtonText(plan: Plan) {
function goPurchase(plan: Plan) {
if (!userStore.isLogin) {
router.push({path: '/login', query: {redirect: '/vip'}})
router.push({ path: '/login', query: { redirect: '/vip' } })
return
}
selectedPlanId = plan.id
_nextTick(() => {
let el = document.getElementById('pay')
el.scrollIntoView({behavior: "smooth"})
el.scrollIntoView({ behavior: 'smooth' })
})
}
@@ -202,36 +199,39 @@ let startLoop = $ref(false)
let orderNo = $ref('')
let timer: number = $ref()
let showCouponInput = $ref(false)
let coupon = $ref<CouponInfo>({code: ''} as CouponInfo)
let coupon = $ref<CouponInfo>({ code: '' } as CouponInfo)
let checkLoading = $ref(false)
let showCheckBtn = $ref(false)
watch(() => startLoop, (n) => {
if (n) {
clearInterval(timer)
timer = setInterval(() => {
orderStatus({orderNo}).then(res => {
if (res?.success) {
if (res.data?.payment_status === 'paid') {
Toast.success('付款成功')
userStore.init()
watch(
() => startLoop,
n => {
if (n) {
clearInterval(timer)
timer = setInterval(() => {
orderStatus({ orderNo }).then(res => {
if (res?.success) {
if (res.data?.payment_status === 'paid') {
Toast.success('付款成功')
userStore.init()
startLoop = false
selectedPlanId = undefined
}
} else {
startLoop = false
selectedPlanId = undefined
Toast.error(res.msg || '付款失败')
}
} else {
startLoop = false
Toast.error(res.msg || '付款失败')
}
})
}, 1000)
setTimeout(() => {
showCheckBtn = true
}, 3000)
} else {
clearInterval(timer)
showCheckBtn = false
})
}, 1000)
setTimeout(() => {
showCheckBtn = true
}, 3000)
} else {
clearInterval(timer)
showCheckBtn = false
}
}
})
)
onUnmounted(() => {
startLoop = false
@@ -252,21 +252,21 @@ async function handlePayment() {
plan: selectedPlanId,
duration: Number(duration),
payment_method: selectedPaymentMethod,
couponCode: coupon.is_valid ? coupon.code : undefined
couponCode: coupon.is_valid ? coupon.code : undefined,
}
let res = await orderCreate(data)
console.log('res', res)
if (res.success) {
_nextTick(() => {
const iframe = document.getElementById('payFrame');
const iframe = document.getElementById('payFrame')
// about:blank document
iframe.src = 'about:blank';
iframe.src = 'about:blank'
iframe.onload = () => {
const doc = iframe.contentWindow.document;
doc.open();
doc.write(res.data.result); // form
doc.close(); // form
};
const doc = iframe.contentWindow.document
doc.open()
doc.write(res.data.result) // form
doc.close() // form
}
startLoop = true
})
orderNo = res.data.orderNo
@@ -279,7 +279,7 @@ async function handlePayment() {
async function checkOrderStatus() {
if (checkLoading) return
checkLoading = true
let res = await alipayQuery({orderNo})
let res = await alipayQuery({ orderNo })
if (!res.success) {
Toast.info(res.msg || '未付款')
}
@@ -298,11 +298,11 @@ async function getCouponInfo() {
if (res.data.is_valid) {
coupon = res.data
} else {
coupon = {code: coupon.code} as CouponInfo
coupon = { code: coupon.code } as CouponInfo
Toast.info('优惠券已失效')
}
} else {
coupon = {code: coupon.code} as CouponInfo
coupon = { code: coupon.code } as CouponInfo
Toast.error(res.msg || '优惠券无效')
}
couponLoading = false
@@ -310,7 +310,6 @@ async function getCouponInfo() {
showCouponInput = true
}
}
</script>
<template>
@@ -320,7 +319,7 @@ async function getCouponInfo() {
<Header title="会员介绍"></Header>
<div class="grid grid-cols-3 grid-rows-3 gap-3">
<div class="text-lg flex items-center" v-for="f in data.benefits" :key="f.name">
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600" />
<span>
<span>{{ f.name }}</span>
<span v-if="f.value !== 'true'">{{ `(${f.value}${f.unit ?? ''})` }}</span>
@@ -329,27 +328,22 @@ async function getCouponInfo() {
</div>
</div>
<div v-if="member?.active" class="card-white bg-green-50 dark:bg-item border border-green-200 mt-3 mb-6">
<div v-if="member?.active" class="card-white bg-green-50 dark:bg-item border border-green-200 mt-3 mb-6">
<div class="flex items-center justify-between">
<div class="flex items-center">
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600"/>
<IconFluentCheckmarkCircle20Regular class="mr-2 text-green-600" />
<div>
<div class="font-semibold text-green-800">当前计划{{ currentPlan?.name }}</div>
<div class="text-sm text-green-600">
到期时间{{ memberEndDate }}
</div>
<div class="text-sm text-green-600">到期时间{{ memberEndDate }}</div>
</div>
</div>
<div class="text-align-end space-y-2">
<div v-if="member.autoRenew" class="flex items-center gap-space">
<div class="flex items-center text-sm text-gray-600">
<IconFluentArrowRepeatAll20Regular class="mr-1"/>
<IconFluentArrowRepeatAll20Regular class="mr-1" />
<span>自动续费已开启</span>
</div>
<PopConfirm
title="确认取消?"
@confirm="toggleAutoRenew"
>
<PopConfirm title="确认取消?" @confirm="toggleAutoRenew">
<BaseButton size="small" type="info" :loading="loading2">关闭</BaseButton>
</PopConfirm>
</div>
@@ -363,8 +357,7 @@ async function getCouponInfo() {
</div>
<div class="plans">
<div v-for="p in plans" :key="p.id"
class="card-white p-0 overflow-hidden flex flex-col">
<div v-for="p in plans" :key="p.id" class="card-white p-0 overflow-hidden flex flex-col">
<div class="text-2xl font-bold bg-gray-300 dark:bg-third px-6 py-4">{{ p.name }}</div>
<div class="p-6 flex flex-col justify-between flex-1">
<div class="plan-head">
@@ -375,12 +368,16 @@ async function getCouponInfo() {
<div v-if="p.highlight" class="tag">{{ p.highlight }}</div>
</div>
<div v-if="p.autoRenew" class="text-sm flex items-center mt-4">
<IconFluentArrowRepeatAll20Regular class="mr-2"/>
<IconFluentArrowRepeatAll20Regular class="mr-2" />
开启自动续费可随时关闭
</div>
<BaseButton class="w-full mt-4" size="large"
:type="(p.id === currentPlan?.id || p.id === selectedPlanId) ? 'primary' : 'info'"
:disabled="p.id === currentPlan?.id" @click="goPurchase(p)">
<BaseButton
class="w-full mt-4"
size="large"
:type="p.id === currentPlan?.id || p.id === selectedPlanId ? 'primary' : 'info'"
:disabled="p.id === currentPlan?.id"
@click="goPurchase(p)"
>
{{ getPlanButtonText(p) }}
</BaseButton>
</div>
@@ -397,25 +394,25 @@ async function getCouponInfo() {
<div class="center">
<div class="card-white w-5/10">
<div class="flex items-center justify-between gap-6 ">
<div class="flex items-center justify-between gap-6">
<div class="center gap-2" v-if="!showCouponInput">
<IconStreamlineDiscountPercentCoupon/>
<IconStreamlineDiscountPercentCoupon />
<span>有优惠券</span>
</div>
<BaseInput v-else v-model="coupon.code"
placeholder="请输入优惠券"
autofocus
clearable
@enter="getCouponInfo"
<BaseInput
v-else
v-model="coupon.code"
placeholder="请输入优惠券"
autofocus
clearable
@enter="getCouponInfo"
/>
<BaseButton size="large"
:loading="couponLoading"
@click="getCouponInfo">{{ showCouponInput ? '确定' : '在此兑换!' }}
<BaseButton size="large" :loading="couponLoading" @click="getCouponInfo"
>{{ showCouponInput ? '确定' : '在此兑换!' }}
</BaseButton>
</div>
<div class="bg-green-50 border border-green-200 rounded-lg px-4 py-3 mt-4"
v-if="coupon.is_valid">
<div class="bg-green-50 border border-green-200 rounded-lg px-4 py-3 mt-4" v-if="coupon.is_valid">
<div class="font-medium">优惠券: {{ coupon.name }}</div>
<div class="flex justify-between w-full mt-2">
<span v-if="coupon.type === 'discount'">折扣券{{ (Number(coupon.value) * 10).toFixed(1) }}</span>
@@ -426,8 +423,8 @@ async function getCouponInfo() {
<div v-if="coupon.min_amount || coupon.max_discount">
<span v-if="coupon.min_amount">{{ Number(coupon.min_amount).toFixed(2) }}元可用</span>
<span v-if="coupon.max_discount && coupon.type === 'discount'">
· 最高减{{ Number(coupon.max_discount).toFixed(2) }}
</span>
· 最高减{{ Number(coupon.max_discount).toFixed(2) }}
</span>
</div>
</div>
</div>
@@ -441,13 +438,16 @@ async function getCouponInfo() {
<div class="text-lg font-medium mb-4">选择支付方式</div>
<RadioGroup v-model="selectedPaymentMethod">
<div class="space-y-3 w-full">
<div v-for="method in paymentMethods" :key="method.id"
@click=" selectedPaymentMethod = method.id"
class="flex p-4 border rounded-lg cp transition-all duration-200 hover:bg-item"
:class="selectedPaymentMethod === method.id && 'bg-item'">
<div
v-for="method in paymentMethods"
:key="method.id"
@click="selectedPaymentMethod = method.id"
class="flex p-4 border rounded-lg cp transition-all duration-200 hover:bg-item"
:class="selectedPaymentMethod === method.id && 'bg-item'"
>
<div class="flex items-center flex-1 gap-4">
<IconSimpleIconsWechat class="text-xl color-green-500" v-if="method.id === 'wechat'"/>
<IconUiwAlipay class="text-xl color-blue" v-else/>
<IconSimpleIconsWechat class="text-xl color-green-500" v-if="method.id === 'wechat'" />
<IconUiwAlipay class="text-xl color-blue" v-else />
<div>
<div class="font-medium color-main">{{ method.name }}</div>
<div class="text-sm text-gray-500">{{ method.description }}</div>
@@ -472,14 +472,13 @@ async function getCouponInfo() {
<div class="flex justify-between items-center mb-4">
<!-- Price -->
<div class="flex items-baseline">
<span class="font-semibold"
:class="selectPlan?.id === 'month_auto' ? 'text-3xl' : 'text-xl'">
{{ selectPlan?.price }}
</span>
<span class="font-semibold" :class="selectPlan?.id === 'month_auto' ? 'text-3xl' : 'text-xl'">
{{ selectPlan?.price }}
</span>
<span class="ml-2">/ {{ selectPlan?.unit }}</span>
</div>
<div v-if="selectPlan?.id !== 'month_auto'">
<InputNumber :min="1" v-model="duration"/>
<InputNumber :min="1" v-model="duration" />
</div>
</div>
@@ -490,7 +489,7 @@ async function getCouponInfo() {
</div>
<div class="text-sm">
<div v-if="enoughDiscount" class="text-green-600 flex items-center">
<IconStreamlineDiscountPercentCoupon class="mr-2"/>
<IconStreamlineDiscountPercentCoupon class="mr-2" />
<span>已优惠{{ (Number(originalPrice) - Number(endPrice)).toFixed(2) }}</span>
</div>
<span v-else>优惠券不可用未满足条件</span>
@@ -503,14 +502,19 @@ async function getCouponInfo() {
<span class="text-3xl font-semibold">{{ endPrice }}</span>
</div>
<div class="bg-second text-sm px-4 py-3 rounded-lg mb-4 text-gray-600">
<div class="bg-second text-sm px-4 py-3 rounded-lg mb-4 text-gray-600">
会员属于虚拟服务一经购买激活后不支持退款请在购买前仔细阅读权益说明确认符合您的需求再进行支付
</div>
<!-- Payment Button -->
<BaseButton class="w-full" size="large" :loading="loading || startLoop"
:type="!!selectedPaymentMethod ? 'primary' : 'info'" :disabled="!selectedPaymentMethod"
@click="handlePayment">
<BaseButton
class="w-full"
size="large"
:loading="loading || startLoop"
:type="!!selectedPaymentMethod ? 'primary' : 'info'"
:disabled="!selectedPaymentMethod"
@click="handlePayment"
>
付款
</BaseButton>
</div>
@@ -520,31 +524,22 @@ async function getCouponInfo() {
<div class="text-lg font-semibold mb-4">扫码支付</div>
<div class="center flex-col relative flex-1">
<div class="center h-full w-full absolute left-0 top-0 bg-white z-2" v-if="!startLoop">
<div class="w-5/10">
请点击左侧付款按钮后支付二维码将自动显示
</div>
<div class="w-5/10">请点击左侧付款按钮后支付二维码将自动显示</div>
</div>
<iframe id="payFrame" class="w-[205px] h-[205px] center border-none"></iframe>
<div class="text-center my-4">
请使用支付宝扫码支付
</div>
<BaseButton size="large"
v-if="showCheckBtn"
:loading="checkLoading"
@click="checkOrderStatus">
<div class="text-center my-4">请使用支付宝扫码支付</div>
<BaseButton size="large" v-if="showCheckBtn" :loading="checkLoading" @click="checkOrderStatus">
我已付款
</BaseButton>
</div>
</div>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
.pay-dialog {
position: fixed;
left: 0;
@@ -564,7 +559,6 @@ async function getCouponInfo() {
@apply flex flex-col gap-2;
}
.price {
@apply flex items-end gap-1;
}

View File

@@ -15,8 +15,8 @@ import Toast from '@/components/base/toast/Toast.ts'
import DeleteIcon from '@/components/icon/DeleteIcon.vue'
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'
import EditBook from '@/components/article/components/EditBook.vue'
import PracticeSettingDialog from '@/components/word/components/PracticeSettingDialog.vue'
import { useBaseStore } from '@/stores/base.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useSettingStore } from '@/stores/setting.ts'
@@ -183,11 +183,11 @@ function word2Str(word) {
res.synos = word.synos.map(v => (v.pos + v.cn + '\n' + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
res.relWords = word.relWords.root
? '词根:' +
word.relWords.root +
'\n\n' +
word.relWords.rels
.map(v => (v.pos + '\n' + v.words.map(v => v.c + ':' + v.cn).join('\n')).replaceAll('"', ''))
.join('\n\n')
word.relWords.root +
'\n\n' +
word.relWords.rels
.map(v => (v.pos + '\n' + v.words.map(v => v.c + ':' + v.cn).join('\n')).replaceAll('"', ''))
.join('\n\n')
: ''
res.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
return res

View File

@@ -70,10 +70,10 @@ const searchList = computed<any[]>(() => {
let s = searchKey.toLowerCase()
return dict_list.value.filter((item) => {
return item.id.toLowerCase().includes(s)
|| item.name.toLowerCase().includes(s)
|| item.category.toLowerCase().includes(s)
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|| item?.url?.toLowerCase?.().includes?.(s)
|| item.name.toLowerCase().includes(s)
|| item.category.toLowerCase().includes(s)
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|| item?.url?.toLowerCase?.().includes?.(s)
})
}
return []
@@ -125,31 +125,31 @@ watch(dict_list, (val) => {
<div class="py-1 flex flex-1 justify-end" v-else>
<span class="page-title absolute w-full center">词典列表</span>
<BaseIcon
title="搜索"
@click="showSearchInput = true"
class="z-1"
icon="fluent:search-24-regular">
title="搜索"
@click="showSearchInput = true"
class="z-1"
icon="fluent:search-24-regular">
<IconFluentSearch24Regular/>
</BaseIcon>
</div>
</div>
<div class="mt-4" v-if="searchKey">
<DictList
v-if="searchList.length "
@selectDict="selectDict"
:list="searchList"
quantifier="词"
:select-id="'-1'"/>
v-if="searchList.length "
@selectDict="selectDict"
:list="searchList"
quantifier="词"
:select-id="'-1'"/>
<Empty v-else text="没有相关词典"/>
</div>
<div class="w-full" v-else>
<DictGroup
v-for="item in groupedByCategoryAndTag"
:select-id="store.sdict.id"
@selectDict="selectDict"
quantifier="词"
:groupByTag="item[1]"
:category="item[0]"
v-for="item in groupedByCategoryAndTag"
:select-id="store.sdict.id"
@selectDict="selectDict"
quantifier="词"
:groupByTag="item[1]"
:category="item[0]"
/>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { onMounted, onUnmounted, provide, ref, watch } from 'vue'
import Statistics from '@/pages/word/components/Statistics.vue'
import Statistics from '@/components/word/components/Statistics.vue'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
@@ -11,12 +11,12 @@ import useTheme from '@/hooks/theme.ts'
import { getCurrentStudyWord, useWordOptions } from '@/hooks/dict.ts'
import { _getDictDataByUrl, _nextTick, cloneDeep, isMobile, loadJsLib, resourceWrap, shuffle } from '@/utils'
import { useRoute, useRouter } from 'vue-router'
import Footer from '@/pages/word/components/Footer.vue'
import Footer from '@/components/word/components/Footer.vue'
import Panel from '@/components/Panel.vue'
import BaseIcon from '@/components/BaseIcon.vue'
import Tooltip from '@/components/base/Tooltip.vue'
import WordList from '@/components/list/WordList.vue'
import TypeWord from '@/pages/word/components/TypeWord.vue'
import TypeWord from '@/components/word/components/TypeWord.vue'
import Empty from '@/components/Empty.vue'
import { useBaseStore } from '@/stores/base.ts'
import { usePracticeStore } from '@/stores/practice.ts'
@@ -29,7 +29,7 @@ import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig, WordPracticeModeStageMap } f
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 GroupList from '@/components/word/components/GroupList.vue'
import { getPracticeWordCache, setPracticeWordCache } from '@/utils/cache.ts'
import { ShortcutKey, WordPracticeMode, WordPracticeStage, WordPracticeType } from '@/types/enum.ts'

View File

@@ -241,7 +241,7 @@ onMounted(init)
</div>
</div>
</BasePage>
</template>
</template>
<style scoped>
.option:hover { background: var(--color-second); }

View File

@@ -24,14 +24,14 @@ import Toast from '@/components/base/toast/Toast.ts'
import BaseButton from '@/components/BaseButton.vue'
import { getDefaultDict } from '@/types/func.ts'
import DeleteIcon from '@/components/icon/DeleteIcon.vue'
import PracticeSettingDialog from '@/pages/word/components/PracticeSettingDialog.vue'
import ChangeLastPracticeIndexDialog from '@/pages/word/components/ChangeLastPracticeIndexDialog.vue'
import PracticeSettingDialog from '@/components/word/components/PracticeSettingDialog.vue'
import ChangeLastPracticeIndexDialog from '@/components/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, 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 PracticeWordListDialog from '@/components/word/components/PracticeWordListDialog.vue'
import ShufflePracticeSettingDialog from '@/components/word/components/ShufflePracticeSettingDialog.vue'
import { deleteDict } from '@/apis/dict.ts'
import OptionButton from '@/components/base/OptionButton.vue'
import { getPracticeWordCache, setPracticeWordCache } from '@/utils/cache.ts'
@@ -355,7 +355,7 @@ const systemPracticeText = $computed(() => {
{{ isSaveData ? '上次任务' : '今日任务' }}
</div>
<span class="color-link cursor-pointer" v-if="store.sdict.id" @click="showPracticeWordListDialog = true"
>词表</span
>词表</span
>
</div>
<div class="flex gap-1 items-center" v-if="store.sdict.id">

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import { nextTick, ref, watch } from 'vue'
import { useSettingStore } from '@/stores/setting.ts'
import { getShortcutKey, useEventListener } from '@/hooks/event.ts'
import { useSettingStore } from '@/stores/setting'
import { getShortcutKey, useEventListener } from '@/hooks/event'
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, sleep } from '@/utils'
import BaseButton from '@/components/BaseButton.vue'
import { useBaseStore } from '@/stores/base.ts'
import { useBaseStore } from '@/stores/base'
import {
APP_NAME,
APP_VERSION,
@@ -14,19 +14,19 @@ import {
IS_DEV,
LIB_JS_URL,
LOCAL_FILE_KEY,
} from '@/config/env.ts'
} from '@/config/env'
import BasePage from '@/components/BasePage.vue'
import Toast from '@/components/base/toast/Toast.ts'
import Toast from '@/components/base/toast/Toast'
import { set } from 'idb-keyval'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useExport } from '@/hooks/export.ts'
import { useRuntimeStore } from '@/stores/runtime'
import { useExport } from '@/hooks/export'
import MigrateDialog from '@/components/MigrateDialog.vue'
import Log from '@/pages/setting/Log.vue'
import Log from '@/components/setting/Log.vue'
import About from '@/components/About.vue'
import CommonSetting from '@/components/setting/CommonSetting.vue'
import ArticleSetting from '@/components/setting/ArticleSetting.vue'
import WordSetting from '@/components/setting/WordSetting.vue'
import { PRACTICE_ARTICLE_CACHE, PRACTICE_WORD_CACHE } from '@/utils/cache.ts'
import { PRACTICE_ARTICLE_CACHE, PRACTICE_WORD_CACHE } from '@/utils/cache'
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]

View File

@@ -1 +0,0 @@
"{\"val\":{\"myDictList\":[{\"id\":\"collect\",\"name\":\"收藏\",\"description\":\"\",\"sort\":0,\"originWords\":[],\"words\":[],\"chapterWordNumber\":30,\"chapterWords\":[],\"residueWords\":[],\"chapterIndex\":0,\"wordIndex\":0,\"articles\":[],\"statistics\":[],\"isCustom\":true,\"length\":0,\"resourceId\":\"\",\"url\":\"\",\"category\":\"自带字典\",\"tags\":[\"自带\"],\"translateLanguage\":\"common\",\"type\":\"collect\",\"language\":\"en\"},{\"id\":\"skip\",\"name\":\"简单词\",\"description\":\"\",\"sort\":0,\"originWords\":[],\"words\":[],\"chapterWordNumber\":30,\"chapterWords\":[],\"residueWords\":[],\"chapterIndex\":0,\"wordIndex\":0,\"articles\":[],\"statistics\":[],\"isCustom\":true,\"length\":0,\"resourceId\":\"\",\"url\":\"\",\"category\":\"自带字典\",\"tags\":[],\"translateLanguage\":\"common\",\"type\":\"simple\",\"language\":\"en\"},{\"id\":\"wrong\",\"name\":\"错词本\",\"description\":\"\",\"sort\":0,\"originWords\":[],\"words\":[],\"chapterWordNumber\":30,\"chapterWords\":[],\"residueWords\":[],\"chapterIndex\":0,\"wordIndex\":0,\"articles\":[],\"statistics\":[],\"isCustom\":true,\"length\":0,\"resourceId\":\"\",\"url\":\"\",\"category\":\"自带字典\",\"tags\":[],\"translateLanguage\":\"common\",\"type\":\"wrong\",\"language\":\"en\"},{\"id\":\"cet4\",\"name\":\"CET-4\",\"description\":\"大学英语四级词库\",\"sort\":0,\"originWords\":[],\"words\":[],\"chapterWordNumber\":30,\"chapterWords\":[],\"residueWords\":[],\"chapterIndex\":0,\"wordIndex\":0,\"articles\":[],\"statistics\":[],\"isCustom\":false,\"length\":2607,\"resourceId\":\"\",\"url\":\"CET4_T.json\",\"category\":\"中国考试\",\"tags\":[\"大学英语\"],\"translateLanguage\":\"common\",\"type\":\"word\",\"language\":\"en\"}],\"collectDictIds\":[],\"current\":{\"index\":3,\"practiceType\":\"word\"},\"simpleWords\":[\"a\",\"an\",\"i\",\"my\",\"you\",\"your\",\"me\",\"it\",\"what\",\"who\",\"where\",\"how\",\"when\",\"which\",\"be\",\"am\",\"is\",\"do\",\"are\",\"did\",\"were\",\"was\",\"can\",\"could\",\"will\",\"would\",\"the\",\"that\",\"this\",\"to\",\"of\",\"for\",\"and\",\"at\",\"not\",\"no\",\"yes\"],\"load\":true},\"version\":3}"

View File

@@ -1,93 +0,0 @@
<script setup lang="ts">
// import origin from './data.json'
import BaseButton from "@/components/BaseButton.vue";
import {checkAndUpgradeSaveDict} from "@/utils";
import str from './data.json'
import {get} from 'idb-keyval'
import { SAVE_DICT_KEY } from "@/config/env.ts";
let data = {}
let origin = {}
async function look() {
let configStr: string = await get(SAVE_DICT_KEY.key)
let obj = JSON.parse(configStr)
console.log('local', obj)
}
function set1() {
// localforage.setItem(SAVE_DICT_KEY.key, JSON.stringify({val: shakeCommonDict(origin.val as any), version: 3}))
}
async function check() {
// let configStr: string = await localforage.getItem(SAVE_DICT_KEY.key)
// console.log('local', configStr)
// console.log('or',origin)
// let configStr: string = await localforage.getItem(SAVE_DICT_KEY.key)
console.parse(str)
// console.log(str)
let data = checkAndUpgradeSaveDict(str)
// console.log('data', data)
// this.setState(data)
}
const generateColumns = (length = 10, prefix = 'column-', props?: any) =>
Array.from({length}).map((_, columnIndex) => ({
...props,
key: `${prefix}${columnIndex}`,
dataKey: `${prefix}${columnIndex}`,
title: `Column ${columnIndex}`,
width: 150,
}))
const generateData = (
columns: ReturnType<typeof generateColumns>,
length = 200,
prefix = 'row-'
) =>
Array.from({length}).map((_, rowIndex) => {
return columns.reduce(
(rowData, column, columnIndex) => {
rowData[column.dataKey] = `Row ${rowIndex} - Col ${columnIndex}`
return rowData
},
{
id: `${prefix}${rowIndex}`,
parentId: null,
}
)
})
const columns = generateColumns(10)
const data1 = generateData(columns, 1000)
</script>
<template>
<div class="page">
<div class="data">
<p>数据升级检测</p>
<BaseButton @click="look">获取保存到localforage的数据</BaseButton>
<BaseButton @click="set">设置data.json的数据到localforage</BaseButton>
<BaseButton @click="check">检测升级逻辑</BaseButton>
</div>
</div>
</template>
<style scoped lang="scss">
.page {
position: relative;
z-index: 1;
font-size: 1rem;
color: black;
.data {
display: flex;
flex-direction: column;
gap: 1rem;
width: 30rem;
}
}
</style>

View File

@@ -1,23 +1,25 @@
import * as VueRouter from 'vue-router'
import {RouteRecordRaw} from 'vue-router'
import WordsPage from "@/pages/word/WordsPage.vue";
import Layout from "@/pages/layout.vue";
import ArticlesPage from "@/pages/article/ArticlesPage.vue";
import PracticeArticles from "@/pages/article/PracticeArticles.vue";
import DictDetail from "@/pages/word/DictDetail.vue";
import PracticeWords from "@/pages/word/PracticeWords.vue";
import WordTest from "@/pages/word/WordTest.vue";
import BookDetail from "@/pages/article/BookDetail.vue";
import DictList from "@/pages/word/DictList.vue";
import BookList from "@/pages/article/BookList.vue";
import Setting from "@/pages/setting/Setting.vue";
import Login from "@/pages/user/login.vue";
import User from "@/pages/user/User.vue";
import VipIntro from "@/pages/user/VipIntro.vue";
import Feedback from "@/pages/feedback.vue";
import Qa from "@/pages/qa.vue";
import Doc from "@/pages/doc.vue";
// import { useAuthStore } from "@/stores/user.ts";
import Layout from "@/layout/default.vue";
import words from "@/pages/(words)/words.vue";
import DictDetail from "@/pages/(words)/dict-detail.vue";
import DictList from "@/pages/(words)/dict-list.vue";
import PracticeWords from "@/pages/(words)/practice-words/[id].vue";
import WordTest from "@/pages/(words)/words-test/[id].vue";
import articles from "@/pages/(articles)/articles.vue";
import BookDetail from "@/pages/(articles)/book-detail.vue";
import BookList from "@/pages/(articles)/book-list.vue";
import PracticeArticles from "@/pages/(articles)/practice-articles/[id].vue";
import setting from "@/pages/setting.vue";
import login from "@/pages/(user)/login.vue";
import user from "@/pages/(user)/user.vue";
import vip from "@/pages/(user)/vip.vue";
import feedback from "@/pages/feedback.vue";
import qa from "@/pages/qa.vue";
import doc from "@/pages/doc.vue";
// import { useAuthStore } from "@/stores/user";
export const routes: RouteRecordRaw[] = [
{
@@ -25,7 +27,7 @@ export const routes: RouteRecordRaw[] = [
component: Layout,
children: [
{path: '/', redirect: '/words'},
{path: 'words', component: WordsPage},
{path: 'words', component: words},
{path: 'word', redirect: '/words'},
{path: 'practice-words/:id', component: PracticeWords},
{path: 'word-test/:id', component: WordTest},
@@ -33,25 +35,24 @@ export const routes: RouteRecordRaw[] = [
{path: 'dict-list', component: DictList},
{path: 'dict-detail', component: DictDetail},
{path: 'articles', component: ArticlesPage},
{path: 'articles', component: articles},
{path: 'article', redirect: '/articles'},
{path: 'practice-articles/:id', component: PracticeArticles},
{path: 'study-article', redirect: '/articles'},
{path: 'book-detail', component: BookDetail},
{path: 'book-list', component: BookList},
{path: 'login', component: Login},
{path: 'user', component: User},
{path: 'vip', component: VipIntro},
{path: 'login', component: login},
{path: 'user', component: user},
{path: 'vip', component: vip},
{path: 'setting', component: Setting},
{path: 'feedback', component: Feedback},
{path: 'qa', component: Qa},
{path: 'doc', component: Doc},
{path: 'setting', component: setting},
{path: 'feedback', component: feedback},
{path: 'qa', component: qa},
{path: 'doc', component: doc},
]
},
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},
{path: '/test', component: () => import("@/pages/test/test.vue")},
{path: '/batch-edit-article', component: () => import("@/pages/(articles)/batch-edit-article.vue")},
{path: '/:pathMatch(.*)*', redirect: '/words'},
]

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import {defineStore} from 'pinia'
import {ref} from 'vue'
import {getUserInfo, User} from '@/apis/user.ts'
import {AppEnv} from "@/config/env.ts";
import Toast from "@/components/base/toast/Toast.ts";
import {getUserInfo, User} from '@/apis/user'
import {AppEnv} from "@/config/env";
import Toast from "@/components/base/toast/Toast";
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)

View File

@@ -1,8 +1,8 @@
import type { Article, ArticleWord, Dict, Word } from '@/types/types.ts'
import type { Article, ArticleWord, Dict, Word } from '@/types/types'
import { shallowReactive } from "vue";
import { cloneDeep } from "@/utils";
import { nanoid } from "nanoid";
import { DictType, PracticeArticleWordType } from '@/types/enum.ts'
import { DictType, PracticeArticleWordType } from '@/types/enum'
export function getDefaultWord(val: Partial<Word> = {}): Word {
return {

View File

@@ -1,4 +1,4 @@
import { DictType, PracticeArticleWordType } from '@/types/enum.ts'
import { DictType, PracticeArticleWordType } from '@/types/enum'
export type Word = {
id?: string

View File

@@ -1,7 +1,7 @@
import axios, {AxiosInstance} from 'axios'
import axios from 'axios'
import type {AxiosInstance} from 'axios'
import {AppEnv, ENV} from "@/config/env.ts";
import Toast from "@/components/base/toast/Toast.ts";
import App from "@/App.vue";
export const axiosInstance: AxiosInstance = axios.create({
baseURL: ENV.API,

View File

@@ -1,15 +1,15 @@
import { BaseState, getDefaultBaseState, useBaseStore } from '@/stores/base.ts'
import { getDefaultSettingState, SettingState } from '@/stores/setting.ts'
import type { Dict, DictResource } from '@/types/types.ts'
import { BaseState, getDefaultBaseState, useBaseStore } from '@/stores/base'
import { getDefaultSettingState, SettingState } from '@/stores/setting'
import type { Dict, DictResource } from '@/types/types'
import { useRouter } from 'vue-router'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useRuntimeStore } from '@/stores/runtime'
import dayjs from 'dayjs'
import { AppEnv, DictId, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY } from '@/config/env.ts'
import { AppEnv, DictId, ENV, RESOURCE_PATH, SAVE_DICT_KEY, SAVE_SETTING_KEY } from '@/config/env'
import { nextTick } from 'vue'
import Toast from '@/components/base/toast/Toast.ts'
import { getDefaultDict, getDefaultWord } from '@/types/func.ts'
import Toast from '@/components/base/toast/Toast'
import { getDefaultDict, getDefaultWord } from '@/types/func'
import duration from 'dayjs/plugin/duration'
import {DictType} from "@/types/enum.ts";
import { DictType } from '@/types/enum'
dayjs.extend(duration)
@@ -61,9 +61,7 @@ export function checkAndUpgradeSaveDict(val: any) {
return defaultState
} else {
// 版本不匹配时,尽量保留数据而不是直接返回默认状态
console.warn(
`数据版本不匹配: 当前版本 ${version}, 期望版本 ${SAVE_DICT_KEY.version},尝试保留数据`
)
console.warn(`数据版本不匹配: 当前版本 ${version}, 期望版本 ${SAVE_DICT_KEY.version},尝试保留数据`)
try {
checkRiskKey(defaultState, state)
// 尝试保留 bookList 数据
@@ -136,8 +134,7 @@ export function checkAndUpgradeSaveSetting(val: any) {
export function shakeCommonDict(n: BaseState): BaseState {
let data: BaseState = cloneDeep(n)
data.word.bookList.map((v: Dict) => {
if (!v.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(v.id))
v.words = []
if (!v.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(v.id)) v.words = []
})
data.article.bookList.map((v: Dict) => {
if (!v.custom && ![DictId.articleCollect].includes(v.id)) v.articles = []
@@ -248,14 +245,11 @@ export async function sleep(time: number) {
return new Promise(resolve => setTimeout(resolve, time))
}
export async function _getDictDataByUrl(
val: DictResource,
type: DictType = DictType.word
): Promise<Dict> {
export async function _getDictDataByUrl(val: DictResource, type: DictType = DictType.word): Promise<Dict> {
// await sleep(2000);
let dictResourceUrl = `https://dicts.2study.top/dicts/${val.language}/word/${val.url}`
let dictResourceUrl = ENV.RESOURCE_URL + `dicts/${val.language}/word/${val.url}`
if (type === DictType.article) {
dictResourceUrl = `https://dicts.2study.top/dicts/${val.language}/article/${val.url}`
dictResourceUrl = ENV.RESOURCE_URL + `dicts/${val.language}/article/${val.url}`
}
let s = await fetch(resourceWrap(dictResourceUrl, val.version)).then(r => r.json())
if (s) {
@@ -271,8 +265,7 @@ export async function _getDictDataByUrl(
//从字符串里面转换为Word格式
export function convertToWord(raw: any) {
const safeString = str => (typeof str === 'string' ? str.trim() : '')
const safeSplit = (str, sep) =>
safeString(str) ? safeString(str).split(sep).filter(Boolean) : []
const safeSplit = (str, sep) => (safeString(str) ? safeString(str).split(sep).filter(Boolean) : [])
// 1. trans
const trans = safeSplit(raw.trans, '\n').map(line => {
@@ -508,9 +501,7 @@ export async function isNewUser() {
let base = useBaseStore()
console.log(JSON.stringify(base.$state))
console.log(JSON.stringify(getDefaultBaseState()))
return (
JSON.stringify(base.$state) === JSON.stringify({ ...getDefaultBaseState(), ...{ load: true } })
)
return JSON.stringify(base.$state) === JSON.stringify({ ...getDefaultBaseState(), ...{ load: true } })
}
export function jump2Feedback() {