wip
This commit is contained in:
1
components.d.ts
vendored
1
components.d.ts
vendored
@@ -109,6 +109,7 @@ declare module 'vue' {
|
||||
IconSimpleIconsWechat: typeof import('~icons/simple-icons/wechat')['default']
|
||||
IconStreamlineDiscountPercentCoupon: typeof import('~icons/streamline/discount-percent-coupon')['default']
|
||||
IconSystemUiconsImport: typeof import('~icons/system-uicons/import')['default']
|
||||
IconUiwAlipay: typeof import('~icons/uiw/alipay')['default']
|
||||
InputNumber: typeof import('./src/components/base/InputNumber.vue')['default']
|
||||
List: typeof import('./src/components/list/List.vue')['default']
|
||||
Logo: typeof import('./src/components/Logo.vue')['default']
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -102,6 +102,9 @@ importers:
|
||||
'@iconify-json/system-uicons':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4
|
||||
'@iconify-json/uiw':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
'@types/file-saver':
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7
|
||||
@@ -547,6 +550,9 @@ packages:
|
||||
'@iconify-json/system-uicons@1.2.4':
|
||||
resolution: {integrity: sha512-9WB9dmEm+TRCXI5Ml2IY8zQAPZES8euKxY0VOaf8D6E6ZaEr7ztO6DChMlGg7qWECs3m3FjFUqNgBx8ZpB+djw==}
|
||||
|
||||
'@iconify-json/uiw@1.2.3':
|
||||
resolution: {integrity: sha512-6ThTlo2tXfgHiN6fOMMENnBLDjJc0H0cDdsjvxSqiFWsB24G+ES0/a+ousVd+/wB0KoUCNAKPNws33CU7m+zzA==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
@@ -4193,6 +4199,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/uiw@1.2.3':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@iconify/utils@2.3.0':
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
function loadIDBKeyval() {
|
||||
return new Promise((resolve) => {
|
||||
let script = document.createElement("script");
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/idb-keyval@6.2.2/dist/umd.js';
|
||||
script.src = '/libs/idb-keyval.js';
|
||||
script.onload = function () {
|
||||
log("idb-keyval 加载完成");
|
||||
resolve(window.idbKeyval);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {useRoute} from "vue-router";
|
||||
import {DictId} from "@/types/types.ts";
|
||||
import {APP_VERSION, AppEnv, LOCAL_FILE_KEY, Origin, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/config/env.ts";
|
||||
import {syncSetting} from "@/apis";
|
||||
import {useUserStore} from "@/stores/auth.ts";
|
||||
import {useUserStore} from "@/stores/user.ts";
|
||||
import MigrateDialog from "@/pages/MigrateDialog.vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
|
||||
@@ -55,6 +55,19 @@ export function uploadImportData(data, onUploadProgress) {
|
||||
headers: {
|
||||
contentType: 'formdata',
|
||||
},
|
||||
timeout: 1000000000,
|
||||
data,
|
||||
onUploadProgress
|
||||
})
|
||||
}
|
||||
|
||||
export function upload(data, onUploadProgress) {
|
||||
return axiosInstance({
|
||||
url: 'file/upload',
|
||||
method: 'post',
|
||||
headers: {
|
||||
contentType: 'formdata',
|
||||
},
|
||||
data,
|
||||
onUploadProgress
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ function parseSentence(sentence: string) {
|
||||
// 1) 货币 + 数字($1,000.50 或 ¥200 或 €100.5)
|
||||
let m = rest.match(/^[\$¥€£]\d{1,3}(?:,\d{3})*(?:\.\d+)?%?/)
|
||||
if (m) {
|
||||
tokens.push({ word: m[0], start: i, end: i + m[0].length, type: PracticeArticleWordType.Number })
|
||||
tokens.push({word: m[0], start: i, end: i + m[0].length, type: PracticeArticleWordType.Number})
|
||||
i += m[0].length
|
||||
continue
|
||||
}
|
||||
@@ -39,7 +39,7 @@ function parseSentence(sentence: string) {
|
||||
// 2) 数字/小数/百分比(100% 3.14 1,000.00)
|
||||
m = rest.match(/^\d{1,3}(?:,\d{3})*(?:\.\d+)?%?/)
|
||||
if (m) {
|
||||
tokens.push({ word: m[0], start: i, end: i + m[0].length, type: PracticeArticleWordType.Number })
|
||||
tokens.push({word: m[0], start: i, end: i + m[0].length, type: PracticeArticleWordType.Number})
|
||||
i += m[0].length
|
||||
continue
|
||||
}
|
||||
@@ -47,7 +47,7 @@ function parseSentence(sentence: string) {
|
||||
// 3) 带点缩写或多段缩写(U.S. U.S.A. e.g. i.e. Ph.D.)
|
||||
m = rest.match(/^[A-Za-z]+(?:\.[A-Za-z]+)+\.?/)
|
||||
if (m) {
|
||||
tokens.push({ word: m[0], start: i, end: i + m[0].length, type: PracticeArticleWordType.Word })
|
||||
tokens.push({word: m[0], start: i, end: i + m[0].length, type: PracticeArticleWordType.Word})
|
||||
i += m[0].length
|
||||
continue
|
||||
}
|
||||
@@ -55,7 +55,7 @@ function parseSentence(sentence: string) {
|
||||
// 4) 单词(包含撇号/连字符,如 it's, o'clock, we'll, mother-in-law)
|
||||
m = rest.match(/^[A-Za-z0-9]+(?:[\'\-][A-Za-z0-9]+)*/)
|
||||
if (m) {
|
||||
tokens.push({ word: m[0], start: i, end: i + m[0].length, type: PracticeArticleWordType.Word })
|
||||
tokens.push({word: m[0], start: i, end: i + m[0].length, type: PracticeArticleWordType.Word})
|
||||
i += m[0].length
|
||||
continue
|
||||
}
|
||||
@@ -63,13 +63,13 @@ function parseSentence(sentence: string) {
|
||||
// 5) 其它可视符号(标点)——单字符处理(连续标点会被循环拆为单字符)
|
||||
// 包括:.,!?;:"'()-[]{}<>/\\@#%^&*~`等非单词非空白字符
|
||||
if (/[^\w\s]/.test(ch)) {
|
||||
tokens.push({ word: ch, start: i, end: i + 1, type: PracticeArticleWordType.Symbol })
|
||||
tokens.push({word: ch, start: i, end: i + 1, type: PracticeArticleWordType.Symbol})
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
||||
// 6) 回退方案:把当前字符当作一个 token(防止意外丢失)
|
||||
tokens.push({ word: ch, start: i, end: i + 1, type: PracticeArticleWordType.Symbol })
|
||||
tokens.push({word: ch, start: i, end: i + 1, type: PracticeArticleWordType.Symbol})
|
||||
i += 1
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ function parseSentence(sentence: string) {
|
||||
const next = tokens[idx + 1]
|
||||
const between = next ? sentence.slice(t.end, next.start) : sentence.slice(t.end)
|
||||
const nextSpace = /\s/.test(between)
|
||||
return getDefaultArticleWord({ word: t.word, nextSpace, type: t.type })
|
||||
return getDefaultArticleWord({word: t.word, nextSpace, type: t.type})
|
||||
})
|
||||
|
||||
return result
|
||||
@@ -125,7 +125,8 @@ export function genArticleSectionData(article: Article): number {
|
||||
try {
|
||||
let s = translateList[i]
|
||||
sList = s.split("\n")
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
for (let j = 0; j < v.length; j++) {
|
||||
let sentence = v[j]
|
||||
@@ -294,7 +295,7 @@ export function splitCNArticle2(text: string): string {
|
||||
// text = `上星期我去看戏。我的座位很好,戏很有意思,但我却无法欣赏。一青年男子与一青年女子坐在我的身后,大声地说着话。我非常生气,因为我听不见演员在说什么。我回过头去怒视着那一男一女,他们却毫不理会。最后,我忍不住了,又一次回过头去,生气地说:“我一个字也听不见了!”
|
||||
// “不关你的事,”那男的毫不客气地说,“这是私人间的谈话!”`
|
||||
}
|
||||
const segmenterJa = new Intl.Segmenter("zh-CN", { granularity: "sentence" })
|
||||
const segmenterJa = new Intl.Segmenter("zh-CN", {granularity: "sentence"})
|
||||
|
||||
let sectionTextList = text.replaceAll("\n\n", "`^`").replaceAll("\n", "").split("`^`")
|
||||
|
||||
@@ -323,10 +324,6 @@ export function splitCNArticle2(text: string): string {
|
||||
return s
|
||||
}
|
||||
|
||||
export function getTranslateText(article: Article) {
|
||||
return article.textTranslate.split("\n\n").filter((v) => v)
|
||||
}
|
||||
|
||||
export function usePlaySentenceAudio() {
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
89
src/hooks/export.ts
Normal file
89
src/hooks/export.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { loadJsLib, shakeCommonDict } from "@/utils";
|
||||
import {
|
||||
APP_NAME,
|
||||
APP_VERSION,
|
||||
EXPORT_DATA_KEY,
|
||||
LOCAL_FILE_KEY,
|
||||
Origin,
|
||||
PracticeSaveArticleKey,
|
||||
PracticeSaveWordKey,
|
||||
SAVE_DICT_KEY,
|
||||
SAVE_SETTING_KEY
|
||||
} from "@/config/env.ts";
|
||||
import { get } from "idb-keyval";
|
||||
import { saveAs } from "file-saver";
|
||||
import dayjs from "dayjs";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { ref } from "vue";
|
||||
|
||||
export function useExport() {
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
let loading = ref(false)
|
||||
|
||||
async function exportData(notice = '导出成功!') {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`);
|
||||
let data = {
|
||||
version: EXPORT_DATA_KEY.version,
|
||||
val: {
|
||||
setting: {
|
||||
version: SAVE_SETTING_KEY.version,
|
||||
val: settingStore.$state
|
||||
},
|
||||
dict: {
|
||||
version: SAVE_DICT_KEY.version,
|
||||
val: shakeCommonDict(store.$state)
|
||||
},
|
||||
[PracticeSaveWordKey.key]: {
|
||||
version: PracticeSaveWordKey.version,
|
||||
val: {}
|
||||
},
|
||||
[PracticeSaveArticleKey.key]: {
|
||||
version: PracticeSaveArticleKey.version,
|
||||
val: {}
|
||||
},
|
||||
[APP_VERSION.key]: -1
|
||||
}
|
||||
}
|
||||
let d = localStorage.getItem(PracticeSaveWordKey.key)
|
||||
if (d) {
|
||||
try {
|
||||
data.val[PracticeSaveWordKey.key] = JSON.parse(d)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
let d1 = localStorage.getItem(PracticeSaveArticleKey.key)
|
||||
if (d1) {
|
||||
try {
|
||||
data.val[PracticeSaveArticleKey.key] = JSON.parse(d1)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
let r = await get(APP_VERSION.key)
|
||||
data.val[APP_VERSION.key] = r
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file("data.json", JSON.stringify(data));
|
||||
|
||||
const mp3 = zip.folder("mp3");
|
||||
const allRecords = await get(LOCAL_FILE_KEY);
|
||||
for (const rec of allRecords ?? []) {
|
||||
mp3.file(rec.id + ".mp3", rec.file);
|
||||
}
|
||||
let content = await zip.generateAsync({type: "blob"})
|
||||
saveAs(content, `${APP_NAME}-User-Data-${dayjs().format('YYYY-MM-DD HH-mm-ss')}.zip`);
|
||||
notice && Toast.success(notice)
|
||||
loading.value = false
|
||||
return content
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
exportData,
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
// 获取系统主题
|
||||
function getSystemTheme(): Theme {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
return 'dark';
|
||||
} else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
||||
return 'light';
|
||||
return 'light';
|
||||
}
|
||||
return 'light'; // 默认浅色模式
|
||||
}
|
||||
@@ -18,18 +19,18 @@ function swapTheme(theme: Theme): Theme {
|
||||
|
||||
// 监听系统主题变化
|
||||
function listenToSystemThemeChange(call: (theme: Theme) => void) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||
if (e.matches) {
|
||||
// console.log('系统已切换到深色模式');
|
||||
call('dark');
|
||||
}
|
||||
});
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', e => {
|
||||
if (e.matches) {
|
||||
// console.log('系统已切换到浅色模式');
|
||||
call('light');
|
||||
}
|
||||
});
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||
if (e.matches) {
|
||||
// console.log('系统已切换到深色模式');
|
||||
call('dark');
|
||||
}
|
||||
});
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', e => {
|
||||
if (e.matches) {
|
||||
// console.log('系统已切换到浅色模式');
|
||||
call('light');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default function useTheme() {
|
||||
@@ -38,10 +39,10 @@ export default function useTheme() {
|
||||
// 开启监听系统主题变更,后期可以通过用户配置来决定是否开启
|
||||
listenToSystemThemeChange((theme: Theme) => {
|
||||
// 如果系统主题变更后和当前的主题一致,则不需要再重新切换
|
||||
if(settingStore.theme === theme){
|
||||
return;
|
||||
if (settingStore.theme === theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
settingStore.theme = theme;
|
||||
setTheme(theme);
|
||||
})
|
||||
@@ -52,13 +53,13 @@ export default function useTheme() {
|
||||
setTheme(settingStore.theme);
|
||||
}
|
||||
|
||||
function setTheme(val:string) {
|
||||
function setTheme(val: string) {
|
||||
// auto模式下,则通过查询系统主题来设置主题名称
|
||||
document.documentElement.className = val === 'auto' ? getSystemTheme() : val;
|
||||
}
|
||||
|
||||
// 获取当前具体的主题名称
|
||||
function getTheme():Theme{
|
||||
function getTheme(): Theme {
|
||||
// auto模式下,则通过查询系统主题来获取当前具体的主题名称
|
||||
return settingStore.theme === 'auto' ? getSystemTheme() : settingStore.theme as Theme;
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
export default function useMobile() {
|
||||
if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import {nextTick, ref, watch} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {getAudioFileUrl, usePlayAudio} from "@/hooks/sound.ts";
|
||||
import {getShortcutKey, useEventListener} from "@/hooks/event.ts";
|
||||
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, shakeCommonDict} from "@/utils";
|
||||
import {DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode} from "@/types/types.ts";
|
||||
import { nextTick, ref, watch } from "vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { getAudioFileUrl, usePlayAudio } from "@/hooks/sound.ts";
|
||||
import { getShortcutKey, useEventListener } from "@/hooks/event.ts";
|
||||
import { checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, shakeCommonDict } from "@/utils";
|
||||
import { DefaultShortcutKeyMap, ShortcutKey, WordPracticeMode } from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {saveAs} from "file-saver";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { saveAs } from "file-saver";
|
||||
import {
|
||||
APP_NAME, APP_VERSION, EMAIL,
|
||||
EXPORT_DATA_KEY, GITHUB,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import dayjs from "dayjs";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import {Option, Select} from "@/components/base/select";
|
||||
import { Option, Select } from "@/components/base/select";
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
|
||||
@@ -29,9 +29,10 @@ import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import {get, set} from "idb-keyval";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useUserStore} from "@/stores/auth.ts";
|
||||
import { get, set } from "idb-keyval";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import { useUserStore } from "@/stores/user.ts";
|
||||
import { useExport } from "@/hooks/export.ts";
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleDisabledDialogEscKey: [val: boolean]
|
||||
@@ -98,7 +99,7 @@ useEventListener('keydown', (e: KeyboardEvent) => {
|
||||
} else {
|
||||
// 忽略单独的修饰键
|
||||
if (shortcutKey === 'Ctrl+' || shortcutKey === 'Alt+' || shortcutKey === 'Shift+' ||
|
||||
e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift') {
|
||||
e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -168,65 +169,9 @@ function resetShortcutKeyMap() {
|
||||
Toast.success('恢复成功')
|
||||
}
|
||||
|
||||
let exportLoading = $ref(false)
|
||||
let importLoading = $ref(false)
|
||||
|
||||
async function exportData(notice = '导出成功!') {
|
||||
exportLoading = true
|
||||
const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`);
|
||||
let data = {
|
||||
version: EXPORT_DATA_KEY.version,
|
||||
val: {
|
||||
setting: {
|
||||
version: SAVE_SETTING_KEY.version,
|
||||
val: settingStore.$state
|
||||
},
|
||||
dict: {
|
||||
version: SAVE_DICT_KEY.version,
|
||||
val: shakeCommonDict(store.$state)
|
||||
},
|
||||
[PracticeSaveWordKey.key]: {
|
||||
version: PracticeSaveWordKey.version,
|
||||
val: {}
|
||||
},
|
||||
[PracticeSaveArticleKey.key]: {
|
||||
version: PracticeSaveArticleKey.version,
|
||||
val: {}
|
||||
},
|
||||
[APP_VERSION.key]: -1
|
||||
}
|
||||
}
|
||||
let d = localStorage.getItem(PracticeSaveWordKey.key)
|
||||
if (d) {
|
||||
try {
|
||||
data.val[PracticeSaveWordKey.key] = JSON.parse(d)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
let d1 = localStorage.getItem(PracticeSaveArticleKey.key)
|
||||
if (d1) {
|
||||
try {
|
||||
data.val[PracticeSaveArticleKey.key] = JSON.parse(d1)
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
let r = await get(APP_VERSION.key)
|
||||
data.val[APP_VERSION.key] = r
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file("data.json", JSON.stringify(data));
|
||||
|
||||
const mp3 = zip.folder("mp3");
|
||||
const allRecords = await get(LOCAL_FILE_KEY);
|
||||
for (const rec of allRecords ?? []) {
|
||||
mp3.file(rec.id + ".mp3", rec.file);
|
||||
}
|
||||
exportLoading = false
|
||||
zip.generateAsync({type: "blob"}).then(function (content) {
|
||||
saveAs(content, `${APP_NAME}-User-Data-${dayjs().format('YYYY-MM-DD HH-mm-ss')}.zip`);
|
||||
});
|
||||
Toast.success(notice)
|
||||
}
|
||||
const {loading: exportLoading, exportData} = useExport()
|
||||
|
||||
function importJson(str: string, notice: boolean = true) {
|
||||
let obj = {
|
||||
@@ -427,8 +372,8 @@ function importOldData() {
|
||||
v-if="settingStore.ignoreSimpleWord"
|
||||
>
|
||||
<Textarea
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 音效-->
|
||||
@@ -456,16 +401,16 @@ function importOldData() {
|
||||
class="w-50!"
|
||||
>
|
||||
<Option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
@@ -583,16 +528,16 @@ function importOldData() {
|
||||
<SettingItem mainTitle="字体设置"/>
|
||||
<SettingItem title="外语字体">
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordForeignFontSize"/>
|
||||
<span class="w-10 pl-5">{{ settingStore.fontSize.wordForeignFontSize }}px</span>
|
||||
</SettingItem>
|
||||
<SettingItem title="中文字体">
|
||||
<Slider
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
:min="10"
|
||||
:max="100"
|
||||
v-model="settingStore.fontSize.wordTranslateFontSize"/>
|
||||
<span class="w-10 pl-5">{{ settingStore.fontSize.wordTranslateFontSize }}px</span>
|
||||
</SettingItem>
|
||||
</div>
|
||||
@@ -641,7 +586,7 @@ function importOldData() {
|
||||
<input ref="shortcutInput" :value="item[1]?item[1]:'未设置快捷键'" readonly type="text"
|
||||
@blur="handleInputBlur">
|
||||
<span @click.stop="editShortcutKey = ''">按键盘进行设置,<span
|
||||
class="text-red!">设置完成点击这里</span></span>
|
||||
class="text-red!">设置完成点击这里</span></span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="item[1]">{{ item[1] }}</div>
|
||||
@@ -678,8 +623,8 @@ function importOldData() {
|
||||
@change="importData">
|
||||
</div>
|
||||
<PopConfirm
|
||||
title="导入老版本数据前,请先备份当前数据,确定要导入老版本数据吗?"
|
||||
@confirm="importOldData">
|
||||
title="导入老版本数据前,请先备份当前数据,确定要导入老版本数据吗?"
|
||||
@confirm="importOldData">
|
||||
<BaseButton>老版本数据导入</BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
@@ -706,7 +651,9 @@ function importOldData() {
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<div>日期:2025/11/14</div>
|
||||
<div>内容:新增文章练习时可跳过空格:如果在单词的最后一位上,不按空格直接输入下一个字母的话,自动跳下一个单词, 按空格也自动跳下一个单词</div>
|
||||
<div>内容:新增文章练习时可跳过空格:如果在单词的最后一位上,不按空格直接输入下一个字母的话,自动跳下一个单词,
|
||||
按空格也自动跳下一个单词
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -841,10 +788,10 @@ function importOldData() {
|
||||
<p v-if="userStore.user.phone" class="text-sm color-gray">{{ userStore.user.phone }}</p>
|
||||
|
||||
<BaseButton
|
||||
@click="userStore.logout"
|
||||
type="info"
|
||||
class="mt-4"
|
||||
:loading="userStore.isLoading"
|
||||
@click="userStore.logout"
|
||||
type="info"
|
||||
class="mt-4"
|
||||
:loading="userStore.isLoading"
|
||||
>
|
||||
退出登录
|
||||
</BaseButton>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted} from 'vue'
|
||||
import {useUserStore} from '@/stores/auth.ts'
|
||||
import {useUserStore} from '@/stores/user.ts'
|
||||
import {useRouter} from 'vue-router'
|
||||
import BaseInput from '@/components/base/BaseInput.vue'
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useUserStore} from '@/stores/auth.ts'
|
||||
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";
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
<script setup lang="tsx">
|
||||
import {onBeforeUnmount, onMounted} from 'vue'
|
||||
import {useRoute} from 'vue-router'
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {APP_NAME} from "@/config/env.ts";
|
||||
import {useUserStore} from "@/stores/auth.ts";
|
||||
import {loginApi, LoginParams, registerApi, resetPasswordApi} from "@/apis/user.ts";
|
||||
import {accountRules, codeRules, passwordRules, phoneRules} from "@/utils/validation.ts";
|
||||
import { APP_NAME } from "@/config/env.ts";
|
||||
import { useUserStore } from "@/stores/user.ts";
|
||||
import { loginApi, LoginParams, registerApi, resetPasswordApi } from "@/apis/user.ts";
|
||||
import { accountRules, codeRules, passwordRules, phoneRules } from "@/utils/validation.ts";
|
||||
import Toast from "@/components/base/toast/Toast.ts";
|
||||
import FormItem from "@/components/base/form/FormItem.vue";
|
||||
import Form from "@/components/base/form/Form.vue";
|
||||
import Notice from "@/pages/user/Notice.vue";
|
||||
import {FormInstance} from "@/components/base/form/types.ts";
|
||||
import {PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts";
|
||||
import {CodeType} from "@/types/types.ts";
|
||||
import { FormInstance } from "@/components/base/form/types.ts";
|
||||
import { PASSWORD_CONFIG, PHONE_CONFIG } from "@/config/auth.ts";
|
||||
import { CodeType, ImportStatus } from "@/types/types.ts";
|
||||
import Code from "@/pages/user/Code.vue";
|
||||
import {isNewUser, useNav} from "@/utils";
|
||||
import { isNewUser, 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";
|
||||
|
||||
// 状态管理
|
||||
const userStore = useUserStore()
|
||||
@@ -34,8 +37,7 @@ let wechatQRUrl = $ref('https://open.weixin.qq.com/connect/qrcode/041GmMJM2wfM0w
|
||||
let qrStatus = $ref<'idle' | 'scanned' | 'expired' | 'cancelled'>('idle')
|
||||
let qrExpireTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let qrCheckInterval: ReturnType<typeof setInterval> | null = null
|
||||
let waitForImportConfirmation = $ref(true)
|
||||
let isImporting = $ref(true)
|
||||
|
||||
const QR_EXPIRE_TIME = 5 * 60 * 1000 // 5分钟过期
|
||||
|
||||
|
||||
@@ -271,7 +273,7 @@ function cancelWechatLogin() {
|
||||
|
||||
// 初始化页面
|
||||
onMounted(() => {
|
||||
console.log('route.query', route.query)
|
||||
// console.log('route.query', route.query)
|
||||
if (route.query?.register) {
|
||||
currentMode = 'register'
|
||||
}
|
||||
@@ -280,7 +282,52 @@ onMounted(() => {
|
||||
// 组件卸载时清理定时器
|
||||
onBeforeUnmount(() => {
|
||||
clearQRTimers()
|
||||
clearInterval(timer)
|
||||
})
|
||||
|
||||
const {exportData} = useExport()
|
||||
let waitForImportConfirmation = $ref(true)
|
||||
let isImporting = $ref(false)
|
||||
let reason = $ref('')
|
||||
let timer = $ref(-1)
|
||||
|
||||
async function startSync() {
|
||||
isImporting = true
|
||||
reason = '导出数据中'
|
||||
let res = await exportData('')
|
||||
reason = '上传数据中'
|
||||
let formData = new FormData()
|
||||
formData.append('file', res, "example.zip")
|
||||
uploadImportData(formData, progressEvent => {
|
||||
let percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
reason = `上传进度(${percent}%)`
|
||||
}).then((result: any) => {
|
||||
if (result.success) {
|
||||
reason = `上传完成; 正在解析中`
|
||||
clearInterval(timer)
|
||||
timer = setInterval(() => {
|
||||
getProgress().then(r => {
|
||||
if (r.success) {
|
||||
if (r.data.status === ImportStatus.Success) {
|
||||
reason = '同步完成'
|
||||
clearInterval(timer)
|
||||
} else if (r.data.status === ImportStatus.Success) {
|
||||
reason = '同步失败'
|
||||
} else {
|
||||
reason = r.data.reason
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 2000)
|
||||
} else {
|
||||
throw new Error('同步失败')
|
||||
}
|
||||
}).catch(error => {
|
||||
Toast.error(error.message || '同步失败')
|
||||
reason = error.message || '同步失败'
|
||||
isImporting = false
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -297,28 +344,28 @@ onBeforeUnmount(() => {
|
||||
<!-- Tab切换 -->
|
||||
<div class="center gap-8 mb-6">
|
||||
<div
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'code'"
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'code' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'code'"
|
||||
>
|
||||
<div>
|
||||
<span>验证码登录</span>
|
||||
<div
|
||||
v-opacity="loginType === 'code'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
v-opacity="loginType === 'code'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'password'"
|
||||
class="center cp transition-colors"
|
||||
:class="loginType === 'password' ? 'link font-medium' : 'text-gray-600'"
|
||||
@click="loginType = 'password'"
|
||||
>
|
||||
<div>
|
||||
<span>密码登录</span>
|
||||
<div
|
||||
v-opacity="loginType === 'password'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
v-opacity="loginType === 'password'"
|
||||
class="mt-1 h-0.5 bg-blue-600"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,10 +373,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 验证码登录表单 -->
|
||||
<Form
|
||||
v-if="loginType === 'code'"
|
||||
ref="phoneLoginFormRef"
|
||||
:rules="phoneLoginFormRules"
|
||||
:model="phoneLoginForm">
|
||||
v-if="loginType === 'code'"
|
||||
ref="phoneLoginFormRef"
|
||||
:rules="phoneLoginFormRules"
|
||||
:model="phoneLoginForm">
|
||||
<FormItem prop="phone">
|
||||
<BaseInput v-model="phoneLoginForm.phone"
|
||||
type="tel"
|
||||
@@ -342,11 +389,11 @@ onBeforeUnmount(() => {
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="phoneLoginForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
placeholder="请输入验证码"
|
||||
v-model="phoneLoginForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
placeholder="请输入验证码"
|
||||
/>
|
||||
<Code :validate-field="() => phoneLoginFormRef.validateField('phone')"
|
||||
:type="CodeType.Login"
|
||||
@@ -357,10 +404,10 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- 密码登录表单 -->
|
||||
<Form
|
||||
v-else
|
||||
ref="loginForm2Ref"
|
||||
:rules="loginForm2Rules"
|
||||
:model="loginForm2">
|
||||
v-else
|
||||
ref="loginForm2Ref"
|
||||
:rules="loginForm2Rules"
|
||||
:model="loginForm2">
|
||||
<FormItem prop="account">
|
||||
<BaseInput v-model="loginForm2.account"
|
||||
type="email"
|
||||
@@ -373,12 +420,12 @@ onBeforeUnmount(() => {
|
||||
<FormItem prop="password">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="loginForm2.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
v-model="loginForm2.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
@@ -389,10 +436,10 @@ onBeforeUnmount(() => {
|
||||
</Notice>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</BaseButton>
|
||||
@@ -409,27 +456,27 @@ onBeforeUnmount(() => {
|
||||
<Header @click="switchMode('login')" title="注册新账号"/>
|
||||
|
||||
<Form
|
||||
ref="registerFormRef"
|
||||
:rules="registerFormRules"
|
||||
:model="registerForm">
|
||||
ref="registerFormRef"
|
||||
:rules="registerFormRules"
|
||||
:model="registerForm">
|
||||
<FormItem prop="account">
|
||||
<BaseInput
|
||||
v-model="registerForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
v-model="registerForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="registerForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
v-model="registerForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => registerFormRef.validateField('account')"
|
||||
:type="CodeType.Register"
|
||||
@@ -438,22 +485,22 @@ onBeforeUnmount(() => {
|
||||
</FormItem>
|
||||
<FormItem prop="password">
|
||||
<BaseInput
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
size="large"
|
||||
:placeholder="`请设置密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="confirmPassword">
|
||||
<BaseInput
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入密码"
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入密码"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
@@ -461,10 +508,10 @@ onBeforeUnmount(() => {
|
||||
<Notice/>
|
||||
|
||||
<BaseButton
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleRegister"
|
||||
class="w-full"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleRegister"
|
||||
>
|
||||
注册
|
||||
</BaseButton>
|
||||
@@ -476,27 +523,27 @@ onBeforeUnmount(() => {
|
||||
<Header @click="switchMode('login')" title="重置密码"/>
|
||||
|
||||
<Form
|
||||
ref="forgotFormRef"
|
||||
:rules="forgotFormRules"
|
||||
:model="forgotForm">
|
||||
ref="forgotFormRef"
|
||||
:rules="forgotFormRules"
|
||||
:model="forgotForm">
|
||||
<FormItem prop="account">
|
||||
<BaseInput
|
||||
v-model="forgotForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
v-model="forgotForm.account"
|
||||
type="tel"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
size="large"
|
||||
placeholder="请输入手机号/邮箱地址"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="code">
|
||||
<div class="flex gap-2">
|
||||
<BaseInput
|
||||
v-model="forgotForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
v-model="forgotForm.code"
|
||||
type="code"
|
||||
size="large"
|
||||
placeholder="请输入验证码"
|
||||
:max-length="PHONE_CONFIG.codeLength"
|
||||
/>
|
||||
<Code :validate-field="() => forgotFormRef.validateField('account')"
|
||||
:type="CodeType.ResetPwd"
|
||||
@@ -505,31 +552,31 @@ onBeforeUnmount(() => {
|
||||
</FormItem>
|
||||
<FormItem prop="newPassword">
|
||||
<BaseInput
|
||||
v-model="forgotForm.newPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
v-model="forgotForm.newPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位)`"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem prop="confirmPassword">
|
||||
<BaseInput
|
||||
v-model="forgotForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入新密码"
|
||||
v-model="forgotForm.confirmPassword"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
size="large"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<BaseButton
|
||||
class="w-full mt-2"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleForgotPassword"
|
||||
class="w-full mt-2"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
@click="handleForgotPassword"
|
||||
>
|
||||
重置密码
|
||||
</BaseButton>
|
||||
@@ -540,16 +587,16 @@ onBeforeUnmount(() => {
|
||||
<div v-if="currentMode === 'login'" class="center flex-col bg-gray-100 rounded-xl px-12">
|
||||
<div class="relative w-40 h-40 bg-white rounded-xl overflow-hidden shadow-xl">
|
||||
<img
|
||||
v-if="showWechatQR"
|
||||
:src="wechatQRUrl"
|
||||
alt="微信登录二维码"
|
||||
class="w-full h-full"
|
||||
:class="{ 'opacity-30': qrStatus === 'expired' }"
|
||||
v-if="showWechatQR"
|
||||
:src="wechatQRUrl"
|
||||
alt="微信登录二维码"
|
||||
class="w-full h-full"
|
||||
:class="{ 'opacity-30': qrStatus === 'expired' }"
|
||||
/>
|
||||
<!-- 扫描成功蒙层 -->
|
||||
<div
|
||||
v-if="qrStatus === 'scanned'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
v-if="qrStatus === 'scanned'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
>
|
||||
<IconFluentCheckmarkCircle20Filled class="color-green text-4xl"/>
|
||||
<div class="text-base text-gray-700 font-medium">扫描成功</div>
|
||||
@@ -557,8 +604,8 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<!-- 取消登录蒙层 -->
|
||||
<div
|
||||
v-if="qrStatus === 'cancelled'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
v-if="qrStatus === 'cancelled'"
|
||||
class="absolute left-0 top-0 w-full h-full center flex-col gap-space bg-white"
|
||||
>
|
||||
<IconFluentErrorCircle20Regular class="color-red text-4xl"/>
|
||||
<div class="text-base text-gray-700 font-medium">你已取消此次登录</div>
|
||||
@@ -567,12 +614,12 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<!-- 过期蒙层 -->
|
||||
<div
|
||||
v-if=" qrStatus === 'expired'"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
|
||||
v-if=" qrStatus === 'expired'"
|
||||
class="absolute top-0 left-0 right-0 bottom-0 bg-opacity-95 center backdrop-blur-sm"
|
||||
>
|
||||
<IconFluentArrowClockwise20Regular
|
||||
@click="refreshQRCode"
|
||||
class="cp text-4xl"/>
|
||||
@click="refreshQRCode"
|
||||
class="cp text-4xl"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-4 center gap-space">
|
||||
@@ -590,7 +637,7 @@ onBeforeUnmount(() => {
|
||||
<h2>检测到您本地存在使用记录</h2>
|
||||
<h3>是否需要同步到账户中?</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div v-else>
|
||||
<h3 class="text-align-center">正在导入中</h3>
|
||||
<ol class="pl-4">
|
||||
<li>
|
||||
@@ -606,17 +653,24 @@ onBeforeUnmount(() => {
|
||||
请耐心等待,请勿关闭此页面
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div class="flex items-center justify-between gap-2 mt-10">
|
||||
<span>当前进度: {{ reason }}</span>
|
||||
<IconEosIconsLoading class="text-xl"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-space justify-end">
|
||||
<div class="flex gap-space justify-end" v-if="!isImporting">
|
||||
<BaseButton type="info" @click="waitForImportConfirmation = false">退出登录</BaseButton>
|
||||
|
||||
<PopConfirm :title="[
|
||||
{text:'您的用户数据将以压缩包自动下载到您的电脑中',type:'normal'},
|
||||
{text:'随后用户数据将被移除',type:'redBold'},
|
||||
{text:'您的用户数据将以压缩包自动下载到您的电脑中,以便您随时恢复',type:'normal'},
|
||||
{text:'随后网站的用户数据将被删除',type:'redBold'},
|
||||
{text:'是否确认继续?',type:'normal'},
|
||||
]">
|
||||
<BaseButton type="info">放弃数据</BaseButton>
|
||||
</PopConfirm>
|
||||
|
||||
<BaseButton>确认同步</BaseButton>
|
||||
<BaseButton @click="startSync">确认同步</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ 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 { useAuthStore } from "@/stores/auth.ts";
|
||||
// import { useAuthStore } from "@/stores/user.ts";
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
|
||||
@@ -239,3 +239,9 @@ export enum CodeType {
|
||||
ChangePhoneNew = 4,
|
||||
ChangePhoneOld = 5
|
||||
}
|
||||
|
||||
export enum ImportStatus {
|
||||
Idle = 0,
|
||||
Success = 1,
|
||||
Fail = 2
|
||||
}
|
||||
Reference in New Issue
Block a user