diff --git a/README.md b/README.md index 544c9c3f..8d965814 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ 3. 在项目根目录下,打开命令行,运行`npm install`来下载依赖。 4. 执行`npm run dev`来启动项目,项目默认地址为[`http://localhost:3000`](http://localhost:3000) 5. 在浏览器中打开[`http://localhost:3000`](http://localhost:3000) 来访问项目。 -6. 执行`npm run build-nocdn`打包项目文件 +6. 执行`npm run build`打包项目文件 ## 功能与建议 diff --git a/components.d.ts b/components.d.ts index 59240612..6dd7be23 100644 --- a/components.d.ts +++ b/components.d.ts @@ -29,6 +29,7 @@ declare module 'vue' { Empty: typeof import('./src/components/Empty.vue')['default'] Form: typeof import('./src/components/base/form/Form.vue')['default'] FormItem: typeof import('./src/components/base/form/FormItem.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'] IconBxVolumeLow: typeof import('~icons/bx/volume-low')['default'] @@ -39,8 +40,10 @@ declare module 'vue' { IconFluentAddSquare20Regular: typeof import('~icons/fluent/add-square20-regular')['default'] IconFluentArrowBounce20Regular: typeof import('~icons/fluent/arrow-bounce20-regular')['default'] IconFluentArrowCircleRight16Regular: typeof import('~icons/fluent/arrow-circle-right16-regular')['default'] + IconFluentArrowClockwise20Regular: typeof import('~icons/fluent/arrow-clockwise20-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'] IconFluentArrowShuffle20Filled: typeof import('~icons/fluent/arrow-shuffle20-filled')['default'] @@ -48,25 +51,36 @@ declare module 'vue' { IconFluentArrowSwap20Regular: typeof import('~icons/fluent/arrow-swap20-regular')['default'] IconFluentBookLetter20Regular: typeof import('~icons/fluent/book-letter20-regular')['default'] IconFluentBookNumber20Filled: typeof import('~icons/fluent/book-number20-filled')['default'] + IconFluentCalendarDate20Regular: typeof import('~icons/fluent/calendar-date20-regular')['default'] IconFluentCheckmark20Regular: typeof import('~icons/fluent/checkmark20-regular')['default'] IconFluentCheckmarkCircle16Filled: typeof import('~icons/fluent/checkmark-circle16-filled')['default'] IconFluentCheckmarkCircle16Regular: typeof import('~icons/fluent/checkmark-circle16-regular')['default'] IconFluentCheckmarkCircle20Filled: typeof import('~icons/fluent/checkmark-circle20-filled')['default'] + IconFluentCheckmarkCircle20Regular: typeof import('~icons/fluent/checkmark-circle20-regular')['default'] + IconFluentChevronDown20Regular: typeof import('~icons/fluent/chevron-down20-regular')['default'] IconFluentChevronLeft20Filled: typeof import('~icons/fluent/chevron-left20-filled')['default'] IconFluentChevronLeft28Filled: typeof import('~icons/fluent/chevron-left28-filled')['default'] + IconFluentCrown20Regular: typeof import('~icons/fluent/crown20-regular')['default'] IconFluentDatabasePerson20Regular: typeof import('~icons/fluent/database-person20-regular')['default'] IconFluentDelete20Regular: typeof import('~icons/fluent/delete20-regular')['default'] IconFluentDismiss20Regular: typeof import('~icons/fluent/dismiss20-regular')['default'] IconFluentDismissCircle16Regular: typeof import('~icons/fluent/dismiss-circle16-regular')['default'] IconFluentDismissCircle20Filled: typeof import('~icons/fluent/dismiss-circle20-filled')['default'] IconFluentErrorCircle20Filled: typeof import('~icons/fluent/error-circle20-filled')['default'] + IconFluentErrorCircle20Regular: typeof import('~icons/fluent/error-circle20-regular')['default'] IconFluentEye16Regular: typeof import('~icons/fluent/eye16-regular')['default'] IconFluentEyeOff16Regular: typeof import('~icons/fluent/eye-off16-regular')['default'] + IconFluentHandWave20Regular: typeof import('~icons/fluent/hand-wave20-regular')['default'] IconFluentHome20Regular: typeof import('~icons/fluent/home20-regular')['default'] IconFluentKeyboardLayoutFloat20Regular: typeof import('~icons/fluent/keyboard-layout-float20-regular')['default'] + IconFluentLockClosed20Regular: typeof import('~icons/fluent/lock-closed20-regular')['default'] + IconFluentMail20Regular: typeof import('~icons/fluent/mail20-regular')['default'] IconFluentMyLocation20Regular: typeof import('~icons/fluent/my-location20-regular')['default'] + IconFluentNumberSymbol20Regular: typeof import('~icons/fluent/number-symbol20-regular')['default'] IconFluentPaddingLeft20Regular: typeof import('~icons/fluent/padding-left20-regular')['default'] + IconFluentPayment20Regular: typeof import('~icons/fluent/payment20-regular')['default'] IconFluentPerson20Regular: typeof import('~icons/fluent/person20-regular')['default'] + IconFluentPhone20Regular: typeof import('~icons/fluent/phone20-regular')['default'] IconFluentPlay20Regular: typeof import('~icons/fluent/play20-regular')['default'] IconFluentQuestionCircle20Regular: typeof import('~icons/fluent/question-circle20-regular')['default'] IconFluentReplay20Regular: typeof import('~icons/fluent/replay20-regular')['default'] @@ -90,7 +104,9 @@ declare module 'vue' { IconFluentWeatherMoon16Regular: typeof import('~icons/fluent/weather-moon16-regular')['default'] IconFluentWeatherSunny16Regular: typeof import('~icons/fluent/weather-sunny16-regular')['default'] IconIconParkOutlineAddMusic: typeof import('~icons/icon-park-outline/add-music')['default'] + IconIxWechatLogo: typeof import('~icons/ix/wechat-logo')['default'] IconPhExportLight: typeof import('~icons/ph/export-light')['default'] + IconSimpleIconsWechat: typeof import('~icons/simple-icons/wechat')['default'] IconSystemUiconsImport: typeof import('~icons/system-uicons/import')['default'] InputNumber: typeof import('./src/components/base/InputNumber.vue')['default'] List: typeof import('./src/components/list/List.vue')['default'] diff --git a/package.json b/package.json index ba6bd345..13438bf0 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@iconify-json/fluent": "^1.2.28", "@iconify-json/icon-park-outline": "^1.2.4", "@iconify-json/icon-park-solid": "^1.2.4", + "@iconify-json/ix": "^1.2.10", "@iconify-json/material-symbols": "^1.2.33", "@iconify-json/oui": "^1.2.6", "@iconify-json/ph": "^1.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acaf46fb..1b46c643 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: '@iconify-json/icon-park-solid': specifier: ^1.2.4 version: 1.2.4 + '@iconify-json/ix': + specifier: ^1.2.10 + version: 1.2.10 '@iconify-json/material-symbols': specifier: ^1.2.33 version: 1.2.33 @@ -514,6 +517,9 @@ packages: '@iconify-json/icon-park-solid@1.2.4': resolution: {integrity: sha512-030MChSP6lCY7N+U5J5R7YguHTGcm7qQEI/ivBjk77El/i8yJatoj568cwwXGM8c6HEU/kIxEE4m3O/6w0WBGg==} + '@iconify-json/ix@1.2.10': + resolution: {integrity: sha512-2NMqsW+sMyH+cpRnRW6mVqJM/q3Mbb7UVY9NWJJEJfHGn1SbzZde/jpgEmTZe5jMJMPQGWhaCzbGsTMrFim+3Q==} + '@iconify-json/material-symbols@1.2.33': resolution: {integrity: sha512-Bs0X1+/vpJydW63olrGh60zkR8/Y70sI14AIWaP7Z6YQXukzWANH4q3I0sIPklbIn1oL6uwLvh0zQyd6Vh79LQ==} @@ -4145,6 +4151,10 @@ snapshots: dependencies: '@iconify/types': 2.0.0 + '@iconify-json/ix@1.2.10': + dependencies: + '@iconify/types': 2.0.0 + '@iconify-json/material-symbols@1.2.33': dependencies: '@iconify/types': 2.0.0 diff --git a/public/privacy-policy.html b/public/privacy-policy.html new file mode 100644 index 00000000..78c80663 --- /dev/null +++ b/public/privacy-policy.html @@ -0,0 +1,100 @@ + + + + + + 隐私政策 + + +
+

隐私政策

+
+
+

一、引言

+

+ 我们非常重视您的隐私保护。本隐私政策说明了我们如何收集、使用、存储和保护您的个人信息。在使用本应用之前,请您仔细阅读本隐私政策。

+
+ +
+

二、信息收集

+

我们可能收集以下信息:

+

1. 账户信息:当您注册账户时,我们会收集您的手机号、邮箱地址、密码等信息。

+

2. 学习数据:我们会记录您的学习进度、学习记录、练习数据等信息,以便为您提供个性化的学习服务。 +

+

3. 设备信息:我们可能收集您的设备型号、操作系统版本、唯一设备标识符等信息,用于改善服务质量和安全性。 +

+

4. 日志信息:当您使用本应用时,我们可能自动收集某些信息,包括IP地址、访问时间、访问页面等。 +

+
+ +
+

三、信息使用

+

我们使用收集的信息用于以下目的:

+

1. 提供、维护和改进我们的服务;

+

2. 处理您的注册、登录、学习记录等请求;

+

3. 向您发送服务通知、更新和安全提醒;

+

4. 进行数据分析,以改善用户体验和服务质量;

+

5. 检测、预防和解决技术问题;

+

6. 遵守法律法规要求。

+
+ +
+

四、信息存储

+

1. 我们采用行业标准的安全措施来保护您的个人信息,防止未经授权的访问、使用或泄露。

+

2. 您的个人信息将存储在安全的服务器上,我们会对数据进行加密处理。

+

3. 我们仅在为实现本隐私政策所述目的所必需的期间内保留您的个人信息。

+
+ +
+

五、信息共享

+

我们不会向第三方出售、交易或转让您的个人信息,除非:

+

1. 获得您的明确同意;

+

2. 法律法规要求或司法机关、行政机关依法要求提供;

+

3. 为履行我们的服务协议或本隐私政策,我们可能需要与我们的服务提供商共享某些信息。

+
+ +
+

六、Cookie和类似技术

+

+ 我们可能使用Cookie和类似技术来收集信息、改善用户体验和分析服务使用情况。您可以通过浏览器设置管理Cookie,但这可能影响某些功能的正常使用。

+
+ +
+

七、您的权利

+

根据相关法律法规,您对自己的个人信息享有以下权利:

+

1. 访问权:您有权访问我们持有的关于您的个人信息;

+

2. 更正权:您有权要求更正不准确的个人信息;

+

3. 删除权:在特定情况下,您有权要求删除您的个人信息;

+

4. 撤回同意:您有权随时撤回您之前给予的同意;

+

5. 投诉权:如果您认为我们对您个人信息的处理违反了相关法律法规,您有权向相关监管部门投诉。 +

+
+ +
+

八、未成年人保护

+

+ 我们非常重视未成年人的个人信息保护。如果您是未成年人,建议您请您的父母或监护人仔细阅读本隐私政策,并在征得您的父母或监护人同意的前提下使用我们的服务。

+
+ +
+

九、隐私政策更新

+

+ 我们可能会不时更新本隐私政策。我们会在本页面上发布新的隐私政策,并通过适当方式通知您。如果您不同意更新后的隐私政策,您可以选择停止使用我们的服务。

+
+ +
+

十、联系我们

+

如果您对本隐私政策有任何疑问、意见或建议,或需要行使您的相关权利,请通过以下方式联系我们:

+

邮箱:zyronon@163.com

+
+ +
+

最后更新时间:2025年11月11日

+
+
+
+ + \ No newline at end of file diff --git a/public/user-agreement.html b/public/user-agreement.html new file mode 100644 index 00000000..20fd737a --- /dev/null +++ b/public/user-agreement.html @@ -0,0 +1,83 @@ + + + + + + 用户协议 + + +
+

用户协议

+
+
+

一、总则

+

欢迎使用本应用!在使用本应用之前,请您仔细阅读本用户协议(以下简称"本协议")。当您注册、登录、使用(以下统称"使用")本应用时,即表示您已阅读、理解并同意接受本协议的全部内容。

+
+ +
+

二、服务内容

+

本应用为用户提供单词学习、文章阅读等在线教育服务。我们保留随时修改或中断服务而不需通知用户的权利,我们行使修改或中断服务的权利,不需对用户或第三方负责。

+
+ +
+

三、用户账户

+

1. 用户在使用本应用前需要注册一个账户。用户应当使用真实、准确、完整的信息注册账户。

+

2. 用户有责任维护账户信息的安全,对账户下的所有活动负责。

+

3. 用户不得将账户转让、出售或以其他方式提供给第三方使用。

+
+ +
+

四、用户行为规范

+

用户在使用本应用时,应当遵守相关法律法规,不得从事以下行为:

+

1. 发布、传播违法、有害、威胁、辱骂、骚扰、侵权、诽谤、淫秽、暴力或其他不当内容;

+

2. 侵犯他人知识产权、隐私权或其他合法权益;

+

3. 干扰或破坏本应用的正常运行;

+

4. 使用自动化工具或脚本进行数据采集、批量操作等;

+

5. 其他违反法律法规或本协议的行为。

+
+ +
+

五、知识产权

+

1. 本应用的所有内容,包括但不限于文字、图片、音频、视频、软件、程序、版面设计等,均受知识产权法保护。

+

2. 未经我们书面许可,用户不得复制、传播、展示、镜像、上传、下载本应用的任何内容。

+
+ +
+

六、隐私保护

+

我们重视用户的隐私保护。关于我们如何收集、使用、存储和保护您的个人信息,请详见《隐私政策》。

+
+ +
+

七、免责声明

+

1. 用户明确同意使用本应用的风险由用户个人承担。

+

2. 我们不对因不可抗力或非我们原因造成的服务中断或终止承担责任。

+

3. 我们不对用户在使用本应用过程中产生的任何直接、间接、偶然、特殊及后续的损害承担责任。

+
+ +
+

八、协议修改

+

我们有权随时修改本协议的任何条款。一旦本协议的内容发生变动,我们将会通过适当方式向用户提示修改内容。如果用户不同意我们对本协议相关条款所做的修改,用户有权停止使用本应用。如果用户继续使用本应用,则视为用户接受我们对本协议相关条款所做的修改。

+
+ +
+

九、法律适用与争议解决

+

1. 本协议的订立、执行和解释及争议的解决均应适用中华人民共和国法律。

+

2. 如双方就本协议内容或其执行发生任何争议,双方应尽量友好协商解决;协商不成时,任何一方均可向我们所在地的人民法院提起诉讼。

+
+ +
+

十、其他

+

1. 本协议构成双方对本协议之约定事项及其他有关事宜的完整协议,除本协议规定的之外,未赋予本协议各方其他权利。

+

2. 如本协议中的任何条款无论因何种原因完全或部分无效或不具有执行力,本协议的其余条款仍应有效并且有约束力。

+
+ +
+

最后更新时间:2025年11月11日

+
+
+
+ + \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 57d53c11..13a08c93 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,17 +5,18 @@ import { useRuntimeStore } from "@/stores/runtime.ts"; import { useSettingStore } from "@/stores/setting.ts"; import useTheme from "@/hooks/theme.ts"; import { shakeCommonDict } from "@/utils"; -import { routes } from "@/router.ts"; import { get, set } from 'idb-keyval' import { useRoute } from "vue-router"; import { DictId } from "@/types/types.ts"; -import { APP_VERSION, CAN_REQUEST, LOCAL_FILE_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY } from "@/config/env.ts"; +import { APP_VERSION, AppEnv, LOCAL_FILE_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY } from "@/config/env.ts"; import { syncSetting } from "@/apis"; +import { useUserStore } from "@/stores/auth.ts"; const store = useBaseStore() const runtimeStore = useRuntimeStore() const settingStore = useSettingStore() +const userStore = useUserStore() const { setTheme } = useTheme() let lastAudioFileIdList = [] @@ -51,15 +52,17 @@ watch(store.$state, (n: BaseState) => { watch(() => settingStore.$state, (n) => { set(SAVE_SETTING_KEY.key, JSON.stringify({ val: n, version: SAVE_SETTING_KEY.version })) - if (CAN_REQUEST) { + if (AppEnv.CAN_REQUEST) { syncSetting(null, settingStore.$state) } }, { deep: true }) async function init() { + await userStore.init() await store.init() await settingStore.init() store.load = true + setTheme(settingStore.theme) if (settingStore.first) { @@ -80,19 +83,19 @@ watch(() => route.path, (to, from) => { return transitionName = '' // console.log('watch', to, from) // //footer下面的5个按钮,对跳不要用动画 - let noAnimation = [ - '/pc/practice', - '/pc/dict', - '/mobile', - '/' - ] - if (noAnimation.indexOf(from) !== -1 && noAnimation.indexOf(to) !== -1) { - return transitionName = '' - } - - const toDepth = routes.findIndex(v => v.path === to) - const fromDepth = routes.findIndex(v => v.path === from) - transitionName = toDepth > fromDepth ? 'go' : 'back' + // let noAnimation = [ + // '/pc/practice', + // '/pc/dict', + // '/mobile', + // '/' + // ] + // if (noAnimation.indexOf(from) !== -1 && noAnimation.indexOf(to) !== -1) { + // return transitionName = '' + // } + // + // const toDepth = routes.findIndex(v => v.path === to) + // const fromDepth = routes.findIndex(v => v.path === from) + // transitionName = toDepth > fromDepth ? 'go' : 'back' // console.log('transitionName', transitionName, toDepth, fromDepth) }) @@ -106,6 +109,4 @@ watch(() => route.path, (to, from) => { - - - + \ No newline at end of file diff --git a/src/apis/index.ts b/src/apis/index.ts index 4215fc31..e2f9666f 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -48,7 +48,7 @@ export function addDict(params?, data?) { return http('dict/addDict', remove(data), remove(params), 'post') } -export function uploadImportData(data,onUploadProgress) { +export function uploadImportData(data, onUploadProgress) { return axiosInstance({ url: 'dict/uploadImportData', method: 'post', @@ -59,3 +59,7 @@ export function uploadImportData(data,onUploadProgress) { onUploadProgress }) } + +export function getProgress() { + return http<{ status: number; reason: string }>('dict/getProgress', null, null, 'get') +} diff --git a/src/apis/member.ts b/src/apis/member.ts new file mode 100644 index 00000000..0403c257 --- /dev/null +++ b/src/apis/member.ts @@ -0,0 +1,30 @@ +import http from '@/utils/http.ts' + +export type LevelBenefits = { + "level": { + "id": number, + "name": string, + "code": string, + "level": number, + "price": string, + "price_auto": string, + "yearly_price": string, + "description": string, + "color": string, + "icon": string, + "is_active": number, + "created_at": string, + "updated_at": string + }, + "benefits": { + "code": string, + "name": string, + "type": boolean, + "unit": null, + "value": string + }[] +} + +export function levelBenefits(params) { + return http('member/levelBenefits', null, params, 'get') +} diff --git a/src/apis/user.ts b/src/apis/user.ts new file mode 100644 index 00000000..29bfdc0f --- /dev/null +++ b/src/apis/user.ts @@ -0,0 +1,115 @@ +import http from '@/utils/http.ts' +import { CodeType } from "@/types/types.ts"; + +// 用户登录接口 +export interface LoginParams { + account?: string + password?: string + phone?: string + code?: string + type: 'code' | 'pwd' +} + +export interface User { + id: string + email?: string + phone?: string + username?: string + avatar?: string, + hasPwd?: boolean, + member: { + levelDesc: string, + status: string, + active: boolean, + endDate: number, + autoRenew: boolean, + plan: string, + } +} + + +// 用户注册接口 +export interface RegisterParams { + account: string + password: string + code: string +} + +export interface RegisterResponse { + token: string + user: { + id: string + email?: string + phone: string + nickname?: string + avatar?: string + } +} + +// 发送验证码接口 +export interface SendCodeParams { + val: string + type: CodeType +} + +// 重置密码接口 +export interface ResetPasswordParams { + account: string + code: string + newPassword: string +} + +// 微信登录接口 +export interface WechatLoginParams { + code: string + state?: string +} + +export function loginApi(params: LoginParams) { + return http<{ token:string }>('user/login', params, null, 'post') +} + +export function registerApi(params: RegisterParams) { + return http('user/register', params, null, 'post') +} + +export function sendCode(params: SendCodeParams) { + return http('user/sendCode', null, params, 'get') +} + +export function resetPasswordApi(params: ResetPasswordParams) { + return http('user/resetPassword', params, null, 'post') +} + +export function wechatLogin(params: WechatLoginParams) { + return http('user/wechatLogin', params, null, 'post') +} + +export function refreshToken() { + return http<{ token: string }>('user/refreshToken', null, null, 'post') +} + +// 获取用户信息 +export function getUserInfo() { + return http('user/userInfo', null, null, 'get') +} + +// 设置密码 +export function setPassword(data) { + return http('user/setPassword', data, null, 'post') +} + +// 修改邮箱 +export function changeEmailApi(data) { + return http('user/changeEmail', data, null, 'post') +} + +// 修改手机号 +export function changePhoneApi(data) { + return http('user/changePhone', data, null, 'post') +} + +// 修改用户信息 +export function updateUserInfoApi(data) { + return http('user/updateUserInfo', data, null, 'post') +} diff --git a/src/assets/css/anim.scss b/src/assets/css/anim.scss index 3ef6c727..7c07f336 100644 --- a/src/assets/css/anim.scss +++ b/src/assets/css/anim.scss @@ -27,6 +27,7 @@ } + @keyframes shake { 10%, 90% { diff --git a/src/assets/css/style.scss b/src/assets/css/style.scss index bc2ea52f..a9283b4e 100644 --- a/src/assets/css/style.scss +++ b/src/assets/css/style.scss @@ -15,7 +15,8 @@ --color-font-2: rgb(46, 46, 46); --color-font-3: rgb(75, 85, 99); --color-font-active-1: white; - --color-scrollbar: rgb(147, 173, 227); + --color-scrollbar: #c1c1c1; + --color-sub-gray: #c0bfbf; --article-width: 50vw; @@ -71,7 +72,7 @@ --color-progress-bar: #d1d5df !important; --color-label-bg: whitesmoke; - --color-link: rgb(64, 158, 255) + --color-link: #2563EB; } .footer { @@ -183,7 +184,7 @@ html, body { z-index: 1; height: 100%; width: 100%; - font-size: .9rem; + font-size: 1rem; display: flex; flex-direction: column; } @@ -218,6 +219,14 @@ a { text-decoration: none; } +.link { + color: var(--color-link); + @apply hover:opacity-80; +} + +.cp { + @apply cursor-pointer; +} @supports selector(::-webkit-scrollbar) { ::-webkit-scrollbar { @@ -390,7 +399,7 @@ a { } .card { - @apply rounded-xl p-4 mb-5 box-border relative; + @apply rounded-xl p-4 mb-8 shadow-lg box-border relative; background: var(--color-second); } @@ -413,6 +422,8 @@ a { .line { width: 100%; border-bottom: 1px solid var(--color-item-border); + @apply hover:text-blue-700; + } .line-white { diff --git a/src/components/BackIcon.vue b/src/components/BackIcon.vue index 494d7cc1..376eb9e2 100644 --- a/src/components/BackIcon.vue +++ b/src/components/BackIcon.vue @@ -1,10 +1,11 @@ + + + + \ No newline at end of file diff --git a/src/components/PopConfirm.vue b/src/components/PopConfirm.vue index bcbc8817..ed06eca5 100644 --- a/src/components/PopConfirm.vue +++ b/src/components/PopConfirm.vue @@ -1,11 +1,13 @@ diff --git a/src/components/base/BaseInput.vue b/src/components/base/BaseInput.vue index c08d6e37..d57853b1 100644 --- a/src/components/base/BaseInput.vue +++ b/src/components/base/BaseInput.vue @@ -1,13 +1,18 @@ diff --git a/src/components/base/form/Form.vue b/src/components/base/form/Form.vue index 94095f90..7f6ac7ea 100644 --- a/src/components/base/form/Form.vue +++ b/src/components/base/form/Form.vue @@ -5,7 +5,8 @@ diff --git a/src/components/base/form/FormItem.vue b/src/components/base/form/FormItem.vue index 0247c43f..d8a1dd4e 100644 --- a/src/components/base/form/FormItem.vue +++ b/src/components/base/form/FormItem.vue @@ -11,7 +11,7 @@ let error = $ref('') // 拿到 form 的 model 和注册函数 const formModel = inject('formModel') -const registerField = inject('registerField') +const registerField = inject('registerField') const formRules = inject('formRules', {}) const myRules = $computed(() => { @@ -19,9 +19,13 @@ const myRules = $computed(() => { }) // 校验函数 -const validate = (rules) => { +const validate = (rules, isBlur = false) => { error = '' const val = formModel.value[props.prop] + //为空并且是非主动触发检验的情况下,不检验 + if (isBlur && val.trim() === '') { + return true + } for (const rule of rules) { if (rule.required && (!val || !val.toString().trim())) { error = rule.message @@ -31,43 +35,93 @@ const validate = (rules) => { error = rule.message return false } + if (rule.min && val && val.toString().length < rule.min) { + error = rule.message + return false + } + if (rule.max && val && val.toString().length > rule.max) { + error = rule.message + return false + } + if (rule.validator) { + try { + rule.validator(rule, val) + } catch (e) { + error = e.message + return false + } + } } return true } // 自动触发 blur 校验 -const handleBlur = () => { +function handleBlur() { const blurRules = myRules.filter((r) => r.trigger === 'blur') - if (blurRules.length) validate(blurRules) + if (blurRules.length) validate(blurRules, true) +} + +function handChange() { + error = '' } // 注册到 Form onMounted(() => { registerField && registerField({prop: props.prop, modelValue: value, validate}) }) + let slot = useSlots() + +function patchVNode(vnode, patchFn) { + if (!vnode) return vnode + + // 如果当前节点就是我们要找的 BaseInput + if (vnode.type && vnode.type.name) { + return patchFn(vnode) + } + + // 如果有子节点,则递归修改 + if (Array.isArray(vnode.children)) { + vnode.children = vnode.children.map(child => patchVNode(child, patchFn)) + } + + return vnode +} + + defineRender(() => { - let DefaultNode = slot.default()[0] - return
+ let DefaultNode: any = slot.default()[0] + + // 对 DefaultNode 深度查找 BaseInput 并加上 onBlur / error + DefaultNode = patchVNode(DefaultNode, vnode => { + return { + ...vnode, + props: { + ...vnode.props, + error: !!error, + onBlur: handleBlur, + onChange: handChange + }, + } + }) + + return
{props.label && - } + }
- -
{error}
+ +
{error}  
}) diff --git a/src/components/base/form/types.ts b/src/components/base/form/types.ts new file mode 100644 index 00000000..f3834630 --- /dev/null +++ b/src/components/base/form/types.ts @@ -0,0 +1,65 @@ +// Form 组件的 TypeScript 类型定义 + +// 表单字段接口 +export interface FormField { + prop: string + modelValue: any + validate: (rules: FormRule[]) => boolean +} + +// 表单规则接口 +export interface FormRule { + required?: boolean + message?: string + pattern?: RegExp + validator?: (rule: FormRule, value: any, callback: (error?: Error) => void) => void + min?: number + max?: number + len?: number + type?: string +} + +// 表单规则对象类型 +export type FormRules = Record + +// 表单模型对象类型 +export type FormModel = Record + +// Form 组件的 Props 接口 +export interface FormProps { + model?: FormModel + rules?: FormRules +} + +// Form 组件的实例接口 +export interface FormInstance { + /** + * 校验整个表单 + * @param callback 校验完成后的回调函数,接收校验结果 + */ + validate: (callback: (valid: boolean) => void) => void + + /** + * 校验指定字段 + * @param fieldName 要校验的字段名称 + * @param callback 可选的回调函数,接收校验结果 + * @returns 校验是否通过 + */ + validateField: (fieldName: string, callback?: (valid: boolean) => void) => boolean +} + +// 注入的上下文类型 +export interface FormContext { + registerField: (field: FormField) => void + formModel: FormModel + formValidate: (callback: (valid: boolean) => void) => void + formRules: FormRules +} + +// 验证状态枚举 +export enum ValidateStatus { + Success = 'success', + Error = 'error', + Validating = 'validating', + Pending = 'pending' +} \ No newline at end of file diff --git a/src/components/dialog/Dialog.vue b/src/components/dialog/Dialog.vue index 84646d84..458d14cc 100644 --- a/src/components/dialog/Dialog.vue +++ b/src/components/dialog/Dialog.vue @@ -188,7 +188,7 @@ async function cancel() { \ No newline at end of file diff --git a/src/pages/user/Notice.vue b/src/pages/user/Notice.vue new file mode 100644 index 00000000..6364751e --- /dev/null +++ b/src/pages/user/Notice.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/pages/user/Pay.vue b/src/pages/user/Pay.vue new file mode 100644 index 00000000..9b22bf36 --- /dev/null +++ b/src/pages/user/Pay.vue @@ -0,0 +1,217 @@ + + + + + \ No newline at end of file diff --git a/src/pages/user/User.vue b/src/pages/user/User.vue new file mode 100644 index 00000000..29b5da90 --- /dev/null +++ b/src/pages/user/User.vue @@ -0,0 +1,616 @@ + + + + \ No newline at end of file diff --git a/src/pages/user/VipIntro.vue b/src/pages/user/VipIntro.vue new file mode 100644 index 00000000..42f78152 --- /dev/null +++ b/src/pages/user/VipIntro.vue @@ -0,0 +1,311 @@ + + + + + + diff --git a/src/pages/user/index.vue b/src/pages/user/index.vue deleted file mode 100644 index ee0701d7..00000000 --- a/src/pages/user/index.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - - - diff --git a/src/pages/user/login.vue b/src/pages/user/login.vue index f55ec21f..27c1d3be 100644 --- a/src/pages/user/login.vue +++ b/src/pages/user/login.vue @@ -1,66 +1,588 @@ - - diff --git a/src/pages/word/DictDetail.vue b/src/pages/word/DictDetail.vue index 345960ed..5cbc62fb 100644 --- a/src/pages/word/DictDetail.vue +++ b/src/pages/word/DictDetail.vue @@ -26,7 +26,7 @@ import { getCurrentStudyWord } from "@/hooks/dict.ts"; import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue"; import { useSettingStore } from "@/stores/setting.ts"; import { MessageBox } from "@/utils/MessageBox.tsx"; -import { CAN_REQUEST, Origin, PracticeSaveWordKey } from "@/config/env.ts"; +import { AppEnv, Origin, PracticeSaveWordKey } from "@/config/env.ts"; import { detail } from "@/apis"; const runtimeStore = useRuntimeStore() @@ -196,7 +196,7 @@ onMounted(async () => { } if (base.word.bookList.find(book => book.id === runtimeStore.editDict.id)) { - if (CAN_REQUEST) { + if (AppEnv.CAN_REQUEST) { let res = await detail({id: runtimeStore.editDict.id}) if (res.success) { runtimeStore.editDict.statistics = res.data.statistics diff --git a/src/pages/word/PracticeWords.vue b/src/pages/word/PracticeWords.vue index 0839bbd5..06ad9763 100644 --- a/src/pages/word/PracticeWords.vue +++ b/src/pages/word/PracticeWords.vue @@ -248,9 +248,7 @@ function goNextStep(originList, mode, msg) { } async function next(isTyping: boolean = true) { - if (isTyping) { - statStore.inputWordNumber++ - } + if (isTyping) statStore.inputWordNumber++ if (settingStore.wordPracticeMode === WordPracticeMode.Free) { if (data.index === data.words.length - 1) { data.wrongWords = data.wrongWords.filter(v => (!data.excludeWords.includes(v.word))) @@ -310,9 +308,9 @@ async function next(isTyping: boolean = true) { return goNextStep(shuffle(taskWords.write), WordPracticeType.Listen, '开始听写之前') } - //开始复写之前 + //开始辨认之前 if (statStore.step === 5) { - return goNextStep(taskWords.write, WordPracticeType.Identify, '开始复写之前') + return goNextStep(taskWords.write, WordPracticeType.Identify, '开始辨认之前') } //开始默写上次 @@ -325,9 +323,9 @@ async function next(isTyping: boolean = true) { return goNextStep(shuffle(taskWords.review), WordPracticeType.Listen, '开始听写上次') } - //开始复写昨日 + //开始辨认昨日 if (statStore.step === 2) { - return goNextStep(taskWords.review, WordPracticeType.Identify, '开始复写昨日') + return goNextStep(taskWords.review, WordPracticeType.Identify, '开始辨认昨日') } //开始默写新词 @@ -352,6 +350,13 @@ async function next(isTyping: boolean = true) { savePracticeData() } +function skipStep(){ + data.index = data.words.length - 1 + settingStore.wordPracticeType = WordPracticeType.Spell + data.wrongWords = [] + next(false) +} + function onWordKnow() { //标记模式时,用户认识的单词加入到排除里面,后续不再复习 let rIndex = data.excludeWords.findIndex(v => v === word.word) @@ -649,6 +654,7 @@ useEvents([ :is-collect="isWordCollect(word)" @toggle-collect="toggleWordCollect(word)" @skip="next(false)" + @skipStep="skipStep" /> diff --git a/src/pages/word/WordsPage.vue b/src/pages/word/WordsPage.vue index 5441f55e..9765ccd4 100644 --- a/src/pages/word/WordsPage.vue +++ b/src/pages/word/WordsPage.vue @@ -1,26 +1,26 @@