From 4692f2fa6514c74843b3a6ec0bc9b266c2baf65d Mon Sep 17 00:00:00 2001 From: Zyronon Date: Fri, 7 Nov 2025 10:52:40 +0000 Subject: [PATCH 01/17] save --- README.md | 2 +- src/assets/css/style.scss | 3 ++- src/pages/word/components/TypeWord.vue | 23 +++++++++++++++-------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4a98af12..3e51542a 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/src/assets/css/style.scss b/src/assets/css/style.scss index 244218c6..8688040f 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; diff --git a/src/pages/word/components/TypeWord.vue b/src/pages/word/components/TypeWord.vue index 899b20be..743d85d0 100644 --- a/src/pages/word/components/TypeWord.vue +++ b/src/pages/word/components/TypeWord.vue @@ -293,14 +293,18 @@ function del() { function showWord() { if (settingStore.allowWordTip) { showFullWord = true - } - //系统设定的默认模式情况下,如果看了单词统计到错词里面去 - switch (statStore.step) { - case 1: - case 3: - case 4: - emit('wrong') - break + //系统设定的默认模式情况下,如果看了单词统计到错词里面去 + switch (statStore.step) { + case 1: + case 2: + case 4: + case 5: + case 7: + case 8: + case 10: + emit('wrong') + break + } } } @@ -309,6 +313,9 @@ function hideWord() { } function play() { + if (settingStore.wordPracticeType === WordPracticeType.Dictation) { + emit('wrong') + } volumeIconRef?.play() } From 882d80f6d44392e8c8a2ce50ac98d306d77e51e4 Mon Sep 17 00:00:00 2001 From: Zyronon Date: Mon, 10 Nov 2025 16:57:28 +0000 Subject: [PATCH 02/17] save --- src/apis/auth.ts | 95 ++++ src/apis/index.ts | 3 + src/assets/css/style.scss | 3 + src/components/base/BaseInput.vue | 27 +- src/config/auth.ts | 52 ++ src/main.ts | 7 +- src/pages/setting/Setting.vue | 94 ++++ src/pages/user/index.vue | 138 ++++- src/pages/user/login.vue | 801 +++++++++++++++++++++++++++++- src/router.ts | 26 +- src/stores/auth.ts | 168 +++++++ src/utils/validation.ts | 166 +++++++ 12 files changed, 1538 insertions(+), 42 deletions(-) create mode 100644 src/apis/auth.ts create mode 100644 src/config/auth.ts create mode 100644 src/stores/auth.ts create mode 100644 src/utils/validation.ts diff --git a/src/apis/auth.ts b/src/apis/auth.ts new file mode 100644 index 00000000..f557cd50 --- /dev/null +++ b/src/apis/auth.ts @@ -0,0 +1,95 @@ +import http from '@/utils/http.ts' +// 用户登录接口 +export interface LoginParams { + email?: string + phone?: string + password?: string + code?: string + type: 'email' | 'phone' | 'wechat' +} + +export interface LoginResponse { + token: string + user: { + id: string + email?: string + phone?: string + nickname?: string + avatar?: string + } +} + +// 用户注册接口 +export interface RegisterParams { + email?: string + phone: string + password: string + code: string + nickname?: string +} + +export interface RegisterResponse { + token: string + user: { + id: string + email?: string + phone: string + nickname?: string + avatar?: string + } +} + +// 发送验证码接口 +export interface SendCodeParams { + email?: string + phone: string + type: 'login' | 'register' | 'reset_password' +} + +// 重置密码接口 +export interface ResetPasswordParams { + email?: string + phone: string + code: string + newPassword: string +} + +// 微信登录接口 +export interface WechatLoginParams { + code: string + state?: string +} + +// API 函数定义 +export function login(params: LoginParams) { + return http('auth/login', params, null, 'post') +} + +export function register(params: RegisterParams) { + return http('auth/register', params, null, 'post') +} + +export function sendCode(params: SendCodeParams) { + return http('auth/sendCode', params, null, 'post') +} + +export function resetPassword(params: ResetPasswordParams) { + return http('auth/resetPassword', params, null, 'post') +} + +export function wechatLogin(params: WechatLoginParams) { + return http('auth/wechatLogin', params, null, 'post') +} + +export function logout() { + return http('auth/logout', null, null, 'post') +} + +export function refreshToken() { + return http<{ token: string }>('auth/refreshToken', null, null, 'post') +} + +// 获取用户信息 +export function getUserInfo() { + return http('auth/userInfo', null, null, 'get') +} diff --git a/src/apis/index.ts b/src/apis/index.ts index 4215fc31..6b61335d 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -59,3 +59,6 @@ export function uploadImportData(data,onUploadProgress) { onUploadProgress }) } + +// 导出认证相关API +export * from './auth' diff --git a/src/assets/css/style.scss b/src/assets/css/style.scss index b78ab1de..6f357ea5 100644 --- a/src/assets/css/style.scss +++ b/src/assets/css/style.scss @@ -219,6 +219,9 @@ a { text-decoration: none; } +.cp{ + @apply cursor-pointer; +} @supports selector(::-webkit-scrollbar) { ::-webkit-scrollbar { diff --git a/src/components/base/BaseInput.vue b/src/components/base/BaseInput.vue index c08d6e37..5088bf0f 100644 --- a/src/components/base/BaseInput.vue +++ b/src/components/base/BaseInput.vue @@ -21,6 +21,11 @@ const props = defineProps({ default: false, }, maxLength: Number, + size: { + type: String, + default: 'normal', + validator: (value: string) => ['normal', 'large'].includes(value) + }, }); const emit = defineEmits(['update:modelValue', 'input', 'change', 'focus', 'blur', 'validation']); @@ -96,7 +101,7 @@ const vFocus = { diff --git a/src/components/base/form/FormItem.vue b/src/components/base/form/FormItem.vue index 0247c43f..8f6ba091 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(() => { @@ -31,43 +31,94 @@ 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) + return true + } 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) } +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/config/auth.ts b/src/config/auth.ts index 69b957c2..4590b319 100644 --- a/src/config/auth.ts +++ b/src/config/auth.ts @@ -30,7 +30,7 @@ export const PHONE_CONFIG = { sendInterval: 60, // 手机号正则表达式(中国大陆) - phoneRegex: /^1[3-9]\d{9}$/ + phoneRegex: /^1[2-9]\d{9}$/ } // 邮箱配置 @@ -45,7 +45,7 @@ export const EMAIL_CONFIG = { // 密码配置 export const PASSWORD_CONFIG = { // 密码最小长度 - minLength: 6, + minLength: 9, // 密码最大长度 maxLength: 20 diff --git a/src/pages/layout.vue b/src/pages/layout.vue index 52bcf7cc..ba5cdf60 100644 --- a/src/pages/layout.vue +++ b/src/pages/layout.vue @@ -44,10 +44,10 @@ function goHome() { 设置
- - - - +
+ + 用户 +
+ + + + diff --git a/src/pages/user/login.vue b/src/pages/user/login.vue index 8c03e966..c7ba312b 100644 --- a/src/pages/user/login.vue +++ b/src/pages/user/login.vue @@ -1,14 +1,18 @@ - - - - - - diff --git a/src/pages/user/UserAgreement.vue b/src/pages/user/UserAgreement.vue deleted file mode 100644 index 5d2f55d0..00000000 --- a/src/pages/user/UserAgreement.vue +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - diff --git a/src/pages/user/index.vue b/src/pages/user/index.vue index e3453d29..d9f661d5 100644 --- a/src/pages/user/index.vue +++ b/src/pages/user/index.vue @@ -4,94 +4,219 @@ import { useAuthStore } from "@/stores/auth.ts"; import { useRouter } from "vue-router"; import BaseButton from "@/components/BaseButton.vue"; import Toast from "@/components/base/toast/Toast.ts"; +import { uploadImportData, getProgress } from "@/apis/index.ts"; -const authStore = useAuthStore() -const router = useRouter() +const authStore = useAuthStore(); +const router = useRouter(); // 页面状态 -const isLoading = ref(false) +const isLoading = ref(false); + +// 同步数据状态 +const isSyncing = ref(false); +const uploadPercent = ref(0); +const progressText = ref("等待上传..."); +const syncStatus = ref(null); // 0=导入中,1=完成,2=失败 +const syncReason = ref(""); +const fileInputRef = ref(null); // 退出登录 const handleLogout = async () => { - await authStore.logout() -} + isLoading.value = true; + try { + await authStore.logout(); + } finally { + isLoading.value = false; + } +}; // 跳转到设置页面 const goToSettings = () => { - router.push('/setting') -} + router.push("/setting"); +}; onMounted(() => { - // 如果用户未登录,跳转到登录页 if (!authStore.isLoggedIn) { - router.push({path: "/login"}); - return + return; } - - // 获取用户信息 if (!authStore.user) { - authStore.fetchUserInfo() + authStore.fetchUserInfo(); } -}) +}); + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +const resetSync = () => { + isSyncing.value = false; + uploadPercent.value = 0; + progressText.value = "等待上传..."; + syncStatus.value = null; + syncReason.value = ""; +}; + +const handleSyncClick = () => { + fileInputRef.value?.click(); +}; + +const onFileSelected = async (e: Event) => { + const input = e.target as HTMLInputElement; + const file = input.files?.[0]; + input.value = ""; // 重置,便于重复选择同一文件 + if (!file) return; + + const ext = file.name.split(".").pop()?.toLowerCase(); + if (!ext || (ext !== "zip" && ext !== "json")) { + Toast.warning("仅支持上传 zip 或 json 文件"); + return; + } + + try { + isSyncing.value = true; + progressText.value = "上传中..."; + + const formData = new FormData(); + formData.append("file", file); + + await uploadImportData(formData, (event: ProgressEvent) => { + if (event.total) { + uploadPercent.value = Math.round((event.loaded / event.total) * 100); + } + }); + + progressText.value = "导入中..."; + + // 轮询导入进度,直到 status != 0 + while (true) { + const res = await getProgress(); + const { status, reason } = res as any; // http 封装返回结构按实际为准 + syncStatus.value = status; + syncReason.value = reason || ""; + + if (status !== 0) break; + await sleep(1000); + } + + if (syncStatus.value === 1) { + uploadPercent.value = 100; + progressText.value = "导入完成"; + Toast.success("数据同步成功"); + } else if (syncStatus.value === 2) { + progressText.value = "导入失败"; + Toast.error(syncReason.value || "导入失败"); + } + } catch (err: any) { + progressText.value = "上传或导入失败"; + Toast.error(err?.message || "上传失败"); + } finally { + // 保留结果展示片刻,再复位 + setTimeout(() => resetSync(), 1500); + } +}; diff --git a/src/pages/user/login.vue b/src/pages/user/login.vue index c7ba312b..4c5f2cb6 100644 --- a/src/pages/user/login.vue +++ b/src/pages/user/login.vue @@ -1,18 +1,18 @@ @@ -120,7 +147,7 @@ const onFileSelected = async (e: Event) => {
- 头像 + 头像
{{ authStore.user?.nickname?.charAt(0) || "U" }}
@@ -135,52 +162,76 @@ const onFileSelected = async (e: Event) => {
同步数据 + + + 查看同步进度 + + 系统设置 退出登录
-
+ +
- {{ progressText }} - {{ syncReason }} + 上传中({{ uploadPercent }}%)
+ + +
+
+
+ 后台正在处理... + 已用时:{{ Math.floor(processElapsedSec / 60) }}分{{ processElapsedSec % 60 }}秒 + {{ processReason }} +
+
+ + +
导入完成
+
导入失败:{{ processReason }}
- diff --git a/src/pages/user/login.vue b/src/pages/user/login.vue index 4c5f2cb6..59a922f2 100644 --- a/src/pages/user/login.vue +++ b/src/pages/user/login.vue @@ -1,18 +1,20 @@ + \ No newline at end of file diff --git a/src/pages/user/login.vue b/src/pages/user/login.vue index 59a922f2..106f5f21 100644 --- a/src/pages/user/login.vue +++ b/src/pages/user/login.vue @@ -6,7 +6,7 @@ import BaseButton from "@/components/BaseButton.vue"; import {APP_NAME} from "@/config/env.ts"; import {useAuthStore} from "@/stores/auth.ts"; import {loginApi, LoginParams, registerApi, resetPasswordApi, sendCode} from "@/apis/user.ts"; -import {validateEmail, validatePhone} from "@/utils/validation.ts"; +import {accountRules, codeRules, passwordRules, phoneRules, validateEmail, validatePhone} 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"; @@ -15,6 +15,7 @@ import {FormInstance} from "@/components/base/form/types.ts"; import {PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts"; import {CodeType} from "@/types/types.ts"; import router from "@/router.ts"; +import Code from "@/pages/user/Code.vue"; // 状态管理 const authStore = useAuthStore() @@ -34,40 +35,6 @@ let qrExpireTimer: ReturnType | null = null let qrCheckInterval: ReturnType | null = null const QR_EXPIRE_TIME = 5 * 60 * 1000 // 5分钟过期 -const codeRules = [ - {required: true, message: '请输入验证码', trigger: 'blur'}, - {min: PHONE_CONFIG.codeLength, message: `请输入 ${PHONE_CONFIG.codeLength} 位验证码`, trigger: 'blur'}, -] -const accountRules = [ - {required: true, message: '请输入手机号/邮箱地址', trigger: 'blur'}, - { - validator: (rule: any, value: any) => { - if (!validatePhone(value) && !validateEmail(value)) { - throw new Error('请输入有效的手机号或邮箱地址') - } - }, trigger: 'blur' - }, -] -const phoneRules = [ - {required: true, message: '请输入手机号', trigger: 'blur'}, - { - validator: (rule: any, value: any) => { - if (!validatePhone(value)) { - throw new Error('请输入有效的手机号') - } - }, trigger: 'blur' - }, -] -const passwordRules = [ - {required: true, message: '请输入密码', trigger: 'blur'}, - { - min: PASSWORD_CONFIG.minLength, - max: PASSWORD_CONFIG.maxLength, - message: `密码长度为 ${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位`, - trigger: 'blur' - }, -] - let phoneLoginForm = $ref({phone: '', code: ''}) let phoneLoginFormRef = $ref() @@ -178,18 +145,10 @@ async function handleLogin() { let data = {} //手机号登录 if (loginType === 'code') { - data = { - phone: phoneLoginForm.phone, - code: phoneLoginForm.code, - type: 'code' - } + data = {...phoneLoginForm, type: 'code'} } else { //密码登录 - data = { - account: loginForm2.account, - password: loginForm2.password, - type: 'pwd' - } + data = {...loginForm2, type: 'pwd'} } let res = await loginApi(data as LoginParams) if (res.success) { @@ -215,11 +174,7 @@ async function handleRegister() { if (!valid) return try { loading = true - let res = await registerApi({ - account: registerForm.account, - password: registerForm.password, - code: registerForm.code, - }) + let res = await registerApi(registerForm) if (res.success) { authStore.setToken(res.data.token) authStore.setUser(res.data.user) @@ -243,11 +198,7 @@ async function handleForgotPassword() { if (!valid) return try { loading = true - const res = await resetPasswordApi({ - account: forgotForm.account, - code: forgotForm.code, - newPassword: forgotForm.newPassword - }) + const res = await resetPasswordApi(forgotForm) if (res.success) { Toast.success('密码重置成功,请重新登录') switchMode('login') @@ -405,6 +356,8 @@ onBeforeUnmount(() => { @@ -413,20 +366,14 @@ onBeforeUnmount(() => {
- - {{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '发送验证码') }} - +
@@ -439,7 +386,9 @@ onBeforeUnmount(() => { :model="loginForm2"> @@ -449,6 +398,8 @@ onBeforeUnmount(() => { @@ -487,6 +438,8 @@ onBeforeUnmount(() => { @@ -495,26 +448,22 @@ onBeforeUnmount(() => {
- - {{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '获取验证码') }} - +
@@ -523,6 +472,8 @@ onBeforeUnmount(() => { @@ -557,6 +508,8 @@ onBeforeUnmount(() => { @@ -565,26 +518,22 @@ onBeforeUnmount(() => {
- - {{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '获取验证码') }} - +
@@ -593,6 +542,8 @@ onBeforeUnmount(() => { diff --git a/src/stores/auth.ts b/src/stores/auth.ts index efeb3324..1adf6b3b 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -9,8 +9,9 @@ export interface User { id: string email?: string phone?: string - nickname?: string - avatar?: string + username?: string + avatar?: string, + hasPwd?: boolean } export const useAuthStore = defineStore('auth', () => { @@ -37,15 +38,15 @@ export const useAuthStore = defineStore('auth', () => { } // 登出 - const logout = async () => { + function logout() { clearToken() - Toast.success('已退出登录') + // Toast.success('已退出登录') //这行会引起hrm失效 // router.push('/') } // 获取用户信息 - const fetchUserInfo = async () => { + async function fetchUserInfo() { try { const res = await getUserInfo() if (res.success) { @@ -61,7 +62,7 @@ export const useAuthStore = defineStore('auth', () => { // 初始化用户状态 - const init = async () => { + async function init() { if (AppEnv.CAN_REQUEST) { const success = await fetchUserInfo() if (!success) { diff --git a/src/types/types.ts b/src/types/types.ts index a846c744..5ab35aca 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -235,4 +235,7 @@ export enum CodeType { Login = 0, Register = 1, ResetPwd = 2, + ChangeEmail = 3, + ChangePhoneNew = 4, + ChangePhoneOld = 5 } diff --git a/src/utils/validation.ts b/src/utils/validation.ts index ffff054e..c89bf941 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1,5 +1,5 @@ // 邮箱验证 -import {EMAIL_CONFIG, PHONE_CONFIG} from "@/config/auth.ts"; +import {EMAIL_CONFIG, PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts"; export const validateEmail = (email: string): boolean => { return EMAIL_CONFIG.emailRegex.test(email) @@ -8,3 +8,47 @@ export const validateEmail = (email: string): boolean => { export const validatePhone = (phone: string): boolean => { return PHONE_CONFIG.phoneRegex.test(phone) } + +export const codeRules = [ + {required: true, message: '请输入验证码', trigger: 'blur'}, + {min: PHONE_CONFIG.codeLength, message: `请输入 ${PHONE_CONFIG.codeLength} 位验证码`, trigger: 'blur'}, +] +export const accountRules = [ + {required: true, message: '请输入手机号/邮箱地址', trigger: 'blur'}, + { + validator: (rule: any, value: any) => { + if (!validatePhone(value) && !validateEmail(value)) { + throw new Error('请输入有效的手机号或邮箱地址') + } + }, trigger: 'blur' + }, +] +export const emailRules = [ + {required: true, message: '请输入邮箱地址', trigger: 'blur'}, + { + validator: (rule: any, value: any) => { + if (!validateEmail(value)) { + throw new Error('请输入有效的邮箱地址') + } + }, trigger: 'blur' + }, +] +export const phoneRules = [ + {required: true, message: '请输入手机号', trigger: 'blur'}, + { + validator: (rule: any, value: any) => { + if (!validatePhone(value)) { + throw new Error('请输入有效的手机号') + } + }, trigger: 'blur' + }, +] +export const passwordRules = [ + {required: true, message: '请输入密码', trigger: 'blur'}, + { + min: PASSWORD_CONFIG.minLength, + max: PASSWORD_CONFIG.maxLength, + message: `密码长度为 ${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength} 位`, + trigger: 'blur' + }, +] \ No newline at end of file From 066686f024e36ed8e898d4b70bf441fbf767e003 Mon Sep 17 00:00:00 2001 From: Zyronon Date: Fri, 14 Nov 2025 02:00:21 +0800 Subject: [PATCH 10/17] save --- components.d.ts | 15 + src/App.vue | 6 +- src/apis/user.ts | 32 +- src/components/BackIcon.vue | 5 +- src/components/Header.vue | 21 ++ src/pages/article/BookDetail.vue | 2 +- .../article/components/TypingArticle.vue | 2 +- src/pages/setting/Setting.vue | 20 +- src/pages/user/User.vue | 351 +++++++++--------- src/pages/user/VipIntro.vue | 169 +++++++++ src/pages/user/login.vue | 301 +++++++-------- src/router.ts | 8 +- src/stores/auth.ts | 18 +- src/utils/index.ts | 20 +- 14 files changed, 581 insertions(+), 389 deletions(-) create mode 100644 src/components/Header.vue create mode 100644 src/pages/user/VipIntro.vue diff --git a/components.d.ts b/components.d.ts index 7834607a..c8c320ca 100644 --- a/components.d.ts +++ b/components.d.ts @@ -29,11 +29,13 @@ 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'] IconBxVolumeMute: typeof import('~icons/bx/volume-mute')['default'] IconEosIconsLoading: typeof import('~icons/eos-icons/loading')['default'] + IconFluentAccessibilityQuestionMark20Regular: typeof import('~icons/fluent/accessibility-question-mark20-regular')['default'] IconFluentAdd16Regular: typeof import('~icons/fluent/add16-regular')['default'] IconFluentAdd20Regular: typeof import('~icons/fluent/add20-regular')['default'] IconFluentAddSquare20Regular: typeof import('~icons/fluent/add-square20-regular')['default'] @@ -41,6 +43,8 @@ declare module 'vue' { 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,27 +52,38 @@ 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'] + IconFluentCalendarEmpty20Regular: typeof import('~icons/fluent/calendar-empty20-regular')['default'] + IconFluentCardUi20Regular: typeof import('~icons/fluent/card-ui20-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'] 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'] + IconFluentDocumentSparkle20Regular: typeof import('~icons/fluent/document-sparkle20-regular')['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'] diff --git a/src/App.vue b/src/App.vue index a0699b92..877aca5c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,12 +12,12 @@ 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 { syncSetting } from "@/apis"; -import {useAuthStore} from "@/stores/auth.ts"; +import {useUserStore} from "@/stores/auth.ts"; const store = useBaseStore() const runtimeStore = useRuntimeStore() const settingStore = useSettingStore() -const authStore = useAuthStore() +const userStore = useUserStore() const {setTheme} = useTheme() let lastAudioFileIdList = [] @@ -59,10 +59,10 @@ watch(settingStore.$state, (n) => { }) async function init() { + await userStore.init() await store.init() await settingStore.init() store.load = true - await authStore.init() setTheme(settingStore.theme) diff --git a/src/apis/user.ts b/src/apis/user.ts index 04349fc2..a0b3fdbe 100644 --- a/src/apis/user.ts +++ b/src/apis/user.ts @@ -1,5 +1,5 @@ import http from '@/utils/http.ts' -import {CodeType} from "@/types/types.ts"; +import { CodeType } from "@/types/types.ts"; // 用户登录接口 export interface LoginParams { @@ -10,17 +10,25 @@ export interface LoginParams { type: 'code' | 'pwd' } -export interface LoginResponse { - token: string - user: { - id: string - email?: string - phone?: string - nickname?: string - avatar?: string +export interface User { + id: string + email?: string + phone?: string + username?: string + avatar?: string, + hasPwd?: boolean, + member: { + level: number, + levelDesc: string, + active: boolean, + endTime: number, + autoRenew: boolean, + payMethod: number, + payMethodDesc: string, } } + // 用户注册接口 export interface RegisterParams { account: string @@ -59,7 +67,7 @@ export interface WechatLoginParams { } export function loginApi(params: LoginParams) { - return http('user/login', params, null, 'post') + return http('user/login', params, null, 'post') } export function registerApi(params: RegisterParams) { @@ -75,7 +83,7 @@ export function resetPasswordApi(params: ResetPasswordParams) { } export function wechatLogin(params: WechatLoginParams) { - return http('user/wechatLogin', params, null, 'post') + return http('user/wechatLogin', params, null, 'post') } export function refreshToken() { @@ -84,7 +92,7 @@ export function refreshToken() { // 获取用户信息 export function getUserInfo() { - return http('user/userInfo', null, null, 'get') + return http('user/userInfo', null, null, 'get') } // 设置密码 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/pages/article/BookDetail.vue b/src/pages/article/BookDetail.vue index 92e646d2..e6fd7e1b 100644 --- a/src/pages/article/BookDetail.vue +++ b/src/pages/article/BookDetail.vue @@ -225,7 +225,7 @@ function next() {
- {{ _dateFormat(i.startDate, 'YYYY/MM/DD HH:mm') }} + {{ _dateFormat(i.startDate) }} {{ msToHourMinute(i.spend) }}
diff --git a/src/pages/article/components/TypingArticle.vue b/src/pages/article/components/TypingArticle.vue index ad6932b2..456587a6 100644 --- a/src/pages/article/components/TypingArticle.vue +++ b/src/pages/article/components/TypingArticle.vue @@ -636,7 +636,7 @@ const currentPractice = inject('currentPractice', []) {{ i === currentPractice.length - 1 ? '当前' : i + 1 - }}.  {{ _dateFormat(item.startDate, 'YYYY/MM/DD HH:mm') }} + }}.  {{ _dateFormat(item.startDate) }} {{ msToHourMinute(item.spend) }}
diff --git a/src/pages/setting/Setting.vue b/src/pages/setting/Setting.vue index d6124324..a38b62b3 100644 --- a/src/pages/setting/Setting.vue +++ b/src/pages/setting/Setting.vue @@ -31,7 +31,7 @@ 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 {useAuthStore} from "@/stores/auth.ts"; +import {useUserStore} from "@/stores/auth.ts"; const emit = defineEmits<{ toggleDisabledDialogEscKey: [val: boolean] @@ -41,7 +41,7 @@ const tabIndex = $ref(0) const settingStore = useSettingStore() const runtimeStore = useRuntimeStore() const store = useBaseStore() -const authStore = useAuthStore() +const userStore = useUserStore() //@ts-ignore const gitLastCommitHash = ref(LATEST_COMMIT_HASH); @@ -793,22 +793,22 @@ function importOldData() {

Type Words

-