From f8246c32557e3a20c7b1c42b081f928e8d4ebc35 Mon Sep 17 00:00:00 2001 From: Zyronon Date: Thu, 20 Nov 2025 01:25:16 +0800 Subject: [PATCH] wip --- components.d.ts | 1 + pnpm-lock.yaml | 10 ++ public/migrate.html | 2 +- src/App.vue | 2 +- src/apis/index.ts | 13 ++ src/hooks/article.ts | 23 ++- src/hooks/export.ts | 89 ++++++++++ src/hooks/theme.ts | 41 ++--- src/hooks/useMobile.ts | 6 - src/pages/setting/Setting.vue | 131 +++++--------- src/pages/user/User.vue | 2 +- src/pages/user/VipIntro.vue | 2 +- src/pages/user/login.vue | 302 +++++++++++++++++++------------- src/router.ts | 2 +- src/stores/{auth.ts => user.ts} | 0 src/types/types.ts | 6 + 16 files changed, 372 insertions(+), 260 deletions(-) create mode 100644 src/hooks/export.ts delete mode 100644 src/hooks/useMobile.ts rename src/stores/{auth.ts => user.ts} (100%) diff --git a/components.d.ts b/components.d.ts index ecc78d1b..a8d1aa90 100644 --- a/components.d.ts +++ b/components.d.ts @@ -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'] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 057e3871..bdc7e481 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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': diff --git a/public/migrate.html b/public/migrate.html index 72bc2c29..aba8ed0f 100644 --- a/public/migrate.html +++ b/public/migrate.html @@ -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); diff --git a/src/App.vue b/src/App.vue index 8a93dde9..01a1b2ae 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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() diff --git a/src/apis/index.ts b/src/apis/index.ts index e2f9666f..8e8142a1 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -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 }) diff --git a/src/hooks/article.ts b/src/hooks/article.ts index 1f82ae81..df6cfe80 100644 --- a/src/hooks/article.ts +++ b/src/hooks/article.ts @@ -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() diff --git a/src/hooks/export.ts b/src/hooks/export.ts new file mode 100644 index 00000000..3a2fc2fe --- /dev/null +++ b/src/hooks/export.ts @@ -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, + } +} \ No newline at end of file diff --git a/src/hooks/theme.ts b/src/hooks/theme.ts index 8a7cace5..2443057f 100644 --- a/src/hooks/theme.ts +++ b/src/hooks/theme.ts @@ -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; } diff --git a/src/hooks/useMobile.ts b/src/hooks/useMobile.ts deleted file mode 100644 index 4667e169..00000000 --- a/src/hooks/useMobile.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default function useMobile() { - if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) { - return true - } - return false -} \ No newline at end of file diff --git a/src/pages/setting/Setting.vue b/src/pages/setting/Setting.vue index 472397d0..dff289f3 100644 --- a/src/pages/setting/Setting.vue +++ b/src/pages/setting/Setting.vue @@ -1,14 +1,14 @@