Merge branch 'refs/heads/dev'
# Conflicts: # src/utils/index.ts
This commit is contained in:
24
components.d.ts
vendored
24
components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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>
|
||||
|
||||
12
src/App.vue
12
src/App.vue
@@ -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()
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import http from '@/utils/http.ts'
|
||||
import http from '@/utils/http'
|
||||
|
||||
export type LevelBenefits = {
|
||||
"level": {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useSettingStore } from '@/stores/setting'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
defineProps<{
|
||||
|
||||
@@ -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'
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
@@ -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";
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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/',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
// 先统一一些常见的“智能引号” -> 直引号,避免匹配问题
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
7
src/layout/empty.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot></slot>
|
||||
</template>
|
||||
@@ -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'
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
|
||||
// 状态管理
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -241,7 +241,7 @@ onMounted(init)
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.option:hover { background: var(--color-second); }
|
||||
@@ -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">
|
||||
@@ -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]
|
||||
@@ -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}"
|
||||
@@ -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>
|
||||
@@ -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'},
|
||||
]
|
||||
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DictType, PracticeArticleWordType } from '@/types/enum.ts'
|
||||
import { DictType, PracticeArticleWordType } from '@/types/enum'
|
||||
|
||||
export type Word = {
|
||||
id?: string
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user