This commit is contained in:
Zyronon
2025-11-13 11:09:33 +00:00
parent 04136db975
commit 0441302f88
12 changed files with 647 additions and 225 deletions

View File

@@ -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 {
@@ -88,6 +88,21 @@ export function getUserInfo() {
}
// 设置密码
export function setPassword(password: string) {
return http('user/setPassword', {password}, null, 'post')
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')
}

View File

@@ -184,7 +184,7 @@ html, body {
z-index: 1;
height: 100%;
width: 100%;
font-size: .9rem;
font-size: 1rem;
display: flex;
flex-direction: column;
}

View File

@@ -106,6 +106,15 @@ const vFocus = {
ref="inputEl"
:class="{ 'is-disabled': disabled, 'error': props.error, focus, [`base-input--${size}`]: true }">
<slot name="subfix"></slot>
<!-- PreIcon slot -->
<div v-if="$slots.preIcon" class="pre-icon">
<slot name="preIcon"></slot>
</div>
<IconFluentLockClosed20Regular class="pre-icon" v-if="type === 'password'"/>
<IconFluentMail20Regular class="pre-icon" v-if="type === 'email'"/>
<IconFluentPhone20Regular class="pre-icon" v-if="type === 'tel'"/>
<IconFluentNumberSymbol20Regular class="pre-icon" v-if="type === 'code'"/>
<input
v-bind="attrs"
:type="inputType"
@@ -195,6 +204,24 @@ const vFocus = {
cursor: not-allowed;
}
// PreIcon styling
&.has-preicon {
.inner {
padding-left: 2rem;
}
}
.pre-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--color-input-color);
opacity: 0.6;
z-index: 1;
pointer-events: none;
margin-right: 0.2rem;
}
.inner {
flex: 1;
font-size: 1rem;

View File

@@ -46,7 +46,6 @@ const validate = (rules, isBlur = false) => {
if (rule.validator) {
try {
rule.validator(rule, val)
return true
} catch (e) {
error = e.message
return false

View File

@@ -1,5 +1,6 @@
export const GITHUB = 'https://github.com/zyronon/TypeWords'
export const Host = '2study.top'
export const EMAIL = 'zyronon@163.com'
export const Origin = `https://${Host}`
export const APP_NAME = 'Type Words'

View File

@@ -10,7 +10,7 @@ import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useBaseStore} from "@/stores/base.ts";
import {saveAs} from "file-saver";
import {
APP_NAME, APP_VERSION,
APP_NAME, APP_VERSION, EMAIL,
EXPORT_DATA_KEY, GITHUB,
LOCAL_FILE_KEY,
Origin,
@@ -824,7 +824,7 @@ function importOldData() {
反馈<a :href="`${GITHUB}/issues`" target="_blank">{{ GITHUB }}/issues</a>
</p>
<p>
作者邮箱<a href="mailto:zyronon@163.com">zyronon@163.com</a>
作者邮箱<a :href="`mailto:${EMAIL}`">{{ EMAIL }}</a>
</p>
<div class="text-md color-gray mt-10">
Build {{ gitLastCommitHash }}

66
src/pages/user/Code.vue Normal file
View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import {CodeType} from "@/types/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import {sendCode} from "@/apis/user.ts";
import {PHONE_CONFIG} from "@/config/auth.ts";
import Toast from "@/components/base/toast/Toast.ts";
let isSendingCode = $ref(false)
let codeCountdown = $ref(0)
interface IProps {
validateField: Function,
type: CodeType
val: any
size?: any
}
const props = withDefaults(defineProps<IProps>(), {
size: 'large',
})
// 发送验证码
async function sendVerificationCode() {
let res = props.validateField()
if (res) {
try {
isSendingCode = true
const res = await sendCode({val: props.val, type: props.type})
if (res.success) {
codeCountdown = PHONE_CONFIG.sendInterval
const timer = setInterval(() => {
codeCountdown--
if (codeCountdown <= 0) {
clearInterval(timer)
}
}, 1000)
} else {
Toast.error(res.msg || '发送失败')
}
} catch (error) {
console.error('Send code error:', error)
Toast.error('发送验证码失败')
} finally {
isSendingCode = false
}
}
}
</script>
<template>
<BaseButton
@click="sendVerificationCode"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
:size="props.size"
style="border: 1px solid var(--color-input-border)"
>
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '发送验证码') }}
</BaseButton>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,14 +1,24 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Calendar, CreditCard, Crown } from 'lucide-vue-next'
import { useAuthStore } from '@/stores/auth.ts'
import { useRouter } from 'vue-router'
import {computed, ref} from 'vue'
import {Calendar, CreditCard, Crown} from 'lucide-vue-next'
import {useAuthStore} from '@/stores/auth.ts'
import {useRouter} from 'vue-router'
import BaseInput from '@/components/base/BaseInput.vue'
import BasePage from "@/components/BasePage.vue";
import { APP_NAME, GITHUB } from "@/config/env.ts";
import {APP_NAME, EMAIL, GITHUB} from "@/config/env.ts";
import BaseButton from "@/components/BaseButton.vue";
import { PASSWORD_CONFIG } from "@/config/auth.ts";
import { setPassword } from "@/apis/user.ts";
import {PASSWORD_CONFIG, PHONE_CONFIG} from "@/config/auth.ts";
import {changeEmailApi, changePhoneApi, setPassword, updateUserInfoApi} from "@/apis/user.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import {CodeType} from "@/types/types.ts";
import FormItem from "@/components/base/form/FormItem.vue";
import Form from "@/components/base/form/Form.vue";
import {FormInstance} from "@/components/base/form/types.ts";
import {codeRules, emailRules, passwordRules, phoneRules, validatePhone} from "@/utils/validation.ts";
import {cloneDeep} from "@/utils";
import Toast from "@/components/base/toast/Toast.ts";
import Code from "@/pages/user/Code.vue";
import {MessageBox} from "@/utils/MessageBox.tsx";
const authStore = useAuthStore()
const router = useRouter()
@@ -16,11 +26,6 @@ const router = useRouter()
// Check login state
const isLoggedIn = computed(() => authStore.isLogin)
// Form data
const username = ref('Brian W')
const email = ref('ttentau@gmail.com')
const receiveNotifications = ref(false)
// Mock subscription data (you can replace with real data from your API)
const subscriptionData = ref({
plan: 'Premium',
@@ -31,47 +36,17 @@ const subscriptionData = ref({
})
// UI state
const isEditingUsername = ref(false)
const isEditingEmail = ref(false)
const showPasswordSection = ref(false)
let showChangePwd = $ref(false)
let showChangeEmail = $ref(false)
let showChangeUsername = $ref(false)
let showChangePhone = $ref(false)
let loading = $ref(false)
const handleLogout = () => {
authStore.logout()
router.push('/login')
}
const editUsername = () => {
isEditingUsername.value = true
}
const saveUsername = () => {
isEditingUsername.value = false
// Here you would typically save to backend
}
const editEmail = () => {
isEditingEmail.value = true
}
const saveEmail = () => {
isEditingEmail.value = false
// Here you would typically save to backend
}
const toggleNotifications = () => {
receiveNotifications.value = !receiveNotifications.value
}
const downloadPersonalInfo = () => {
console.log('Download personal info')
}
const deleteAccount = () => {
if (confirm('确定要删除您的账户吗?此操作无法撤销。')) {
console.log('Delete account')
}
}
const contactSupport = () => {
console.log('Contact support')
}
@@ -80,18 +55,202 @@ const leaveTrustpilotReview = () => {
window.open(GITHUB + '/issues', '_blank')
}
async function changePassword(e) {
let res = await setPassword(e.target.value)
//todo
// 修改手机号
// 修改手机号
// 修改手机号
let changePhoneFormRef = $ref<FormInstance>()
let defaultFrom = {oldCode: '', phone: '', code: '', pwd: '',}
let changePhoneForm = $ref(cloneDeep(defaultFrom))
let changePhoneFormRules = {
oldCode: codeRules,
phone: [...phoneRules, {
validator: (rule: any, value: any) => {
if (authStore.user?.phone && value === authStore.user?.phone) {
throw new Error('新手机号与原手机号一致')
}
}, trigger: 'blur'
},],
code: codeRules,
pwd: passwordRules
}
function showChangePhoneForm() {
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
showChangePhone = true
changePhoneForm = cloneDeep(defaultFrom)
}
function changePhone() {
changePhoneFormRef.validate(async valid => {
if (valid) {
try {
loading = true
const res = await changePhoneApi(changePhoneForm)
if (res.success) {
Toast.success('修改成功')
await authStore.fetchUserInfo()
showChangePhone = false
} else {
Toast.error(res.msg || '修改失败')
}
} catch (error) {
Toast.error(error || '修改失败,请重试')
} finally {
loading = false
}
}
})
}
// 修改用户名
// 修改用户名
// 修改用户名
let changeUsernameFormRef = $ref<FormInstance>()
let changeUsernameForm = $ref({username: ''})
let changeUsernameFormRules = {
username: [{required: true, message: '请输入用户名', trigger: 'blur'}],
}
function showChangeUsernameForm() {
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
showChangeUsername = true
changeUsernameForm = cloneDeep({username: authStore.user?.username ?? '',})
}
function changeUsername() {
changeUsernameFormRef.validate(async valid => {
if (valid) {
try {
loading = true
const res = await updateUserInfoApi(changeUsernameForm)
if (res.success) {
Toast.success('修改成功')
await authStore.fetchUserInfo()
showChangeUsername = false
} else {
Toast.error(res.msg || '修改失败')
}
} catch (error) {
Toast.error(error || '修改失败,请重试')
} finally {
loading = false
}
}
})
}
// 修改邮箱
// 修改邮箱
// 修改邮箱
let changeEmailFormRef = $ref<FormInstance>()
let changeEmailForm = $ref({
email: '',
pwd: '',
code: '',
})
let changeEmailFormRules = {
email: [
...emailRules, {
validator: (rule: any, value: any) => {
if (authStore.user?.email && value === authStore.user?.email) {
throw new Error('该邮箱与当前一致')
}
}, trigger: 'blur'
}
],
pwd: passwordRules,
code: codeRules,
}
function showChangeEmailForm() {
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
showChangeEmail = true
changeEmailForm = cloneDeep({email: authStore.user?.email ?? '', pwd: '', code: '',})
}
function changeEmail() {
changeEmailFormRef.validate(async valid => {
if (valid) {
try {
loading = true
const res = await changeEmailApi(changeEmailForm)
if (res.success) {
Toast.success('修改成功')
await authStore.fetchUserInfo()
showChangeEmail = false
} else {
Toast.error(res.msg || '修改失败')
}
} catch (error) {
Toast.error(error || '修改失败,请重试')
} finally {
loading = false
}
}
})
}
// 修改密码
// 修改密码
// 修改密码
let changePwdFormRef = $ref<FormInstance>()
const defaultChangePwdForm = {
oldPwd: '',
newPwd: '',
confirmPwd: '',
}
let changePwdForm = $ref(cloneDeep(defaultChangePwdForm))
let changePwdFormRules = {
oldPwd: passwordRules,
newPwd: passwordRules,
confirmPwd: [
{required: true, message: '请再次输入新密码', trigger: 'blur'},
{
validator: (rule: any, value: any) => {
if (value !== changePwdForm.newPwd) {
throw new Error('两次密码输入不一致')
}
}, trigger: 'blur'
},
],
}
function showChangePwdForm() {
showChangePhone = showChangeUsername = showChangeEmail = showChangePwd = false
showChangePwd = true
changePwdForm = cloneDeep(defaultChangePwdForm)
}
function changePwd() {
changePwdFormRef.validate(async valid => {
if (valid) {
try {
loading = true
const res = await setPassword(changePwdForm)
if (res.success) {
Toast.success('密码设置成功,请重新登录')
showChangePwd = false
authStore.logout()
} else {
Toast.error(res.msg || '设置失败')
}
} catch (error) {
Toast.error(error || '设置密码失败,请重试')
} finally {
loading = false
}
}
})
}
</script>
<template>
<BasePage>
<!-- Unauthenticated View -->
<div v-if="isLoggedIn" class="center h-screen">
<div v-if="!isLoggedIn" class="center h-screen">
<div class="card shadow-lg text-center flex-col gap-6 w-100 ">
<div class="w-20 h-20 bg-blue-100 rounded-full center mx-auto">
<IconFluentPerson20Regular class="text-3xl text-blue-600"/>
@@ -99,9 +258,9 @@ async function changePassword(e) {
<h1 class="text-2xl font-bold">欢迎使用</h1>
<p class="">请登录以管理您的账户</p>
<BaseButton
@click="router.push('/login')"
size="large"
class="w-full mt-4"
@click="router.push('/login')"
size="large"
class="w-full mt-4"
>
登录
</BaseButton>
@@ -113,120 +272,271 @@ async function changePassword(e) {
</div>
<!-- Authenticated View -->
<div v-else class="w-full flex items-start gap-4">
<div v-else class="w-full flex gap-4">
<!-- Main Account Settings -->
<div class="card flex-1 flex flex-col gap-2 px-8">
<h1 class="text-xl font-bold">帐户</h1>
<!-- Username Section -->
<div class="flex items-center justify-between ">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">用户名</div>
<div class="flex items-center gap-2">
<IconFluentPerson20Regular class="text-base text-gray-500"/>
<BaseInput
v-if="isEditingUsername"
v-model="username"
type="text"
size="normal"
@blur="saveUsername"
@keyup.enter="saveUsername"
class="flex-1 max-w-xs"
autofocus
/>
<span v-else class="text-gray-900">{{ username }}</span>
</div>
</div>
<div class="card flex-1 flex flex-col gap-2 px-6">
<h1 class="text-2xl font-bold mt-0">帐户</h1>
<IconFluentTextEditStyle20Regular
@click="isEditingUsername ? saveUsername() : editUsername()"
class="text-xl"/>
<!-- 用户名-->
<div class="item">
<div class="flex-1">
<div class="mb-2">用户名</div>
<div class="flex items-center gap-2" v-if="authStore.user?.username">
<IconFluentPerson20Regular class="text-base"/>
<span>{{ authStore.user?.username }}</span>
</div>
<div v-else class="text-xs">在此设置用户名</div>
</div>
<BaseIcon @click="showChangeUsernameForm">
<IconFluentTextEditStyle20Regular/>
</BaseIcon>
</div>
<div v-if="showChangeUsername">
<Form
ref="changeUsernameFormRef"
:rules="changeUsernameFormRules"
:model="changeUsernameForm">
<FormItem prop="username">
<BaseInput
v-model="changeUsernameForm.username"
type="text"
size="large"
placeholder="请输入用户名"
autofocus
>
<template #preIcon>
<IconFluentPerson20Regular class="text-base"/>
</template>
</BaseInput>
</FormItem>
</Form>
<div class="text-align-end mb-2">
<BaseButton type="info" @click="showChangeUsername = false">取消</BaseButton>
<BaseButton @click="changeUsername">保存</BaseButton>
</div>
</div>
<div class="line"></div>
<!-- 手机号-->
<div class="item">
<div class="flex-1">
<div class="mb-2">手机号</div>
<div class="flex items-center gap-2" v-if="authStore.user?.phone">
<IconFluentMail20Regular class="text-base"/>
<span>{{ authStore.user?.phone }}</span>
</div>
<div v-else class="text-xs">在此设置手机号</div>
</div>
<BaseIcon @click="showChangePhoneForm">
<IconFluentTextEditStyle20Regular/>
</BaseIcon>
</div>
<div v-if="showChangePhone">
<Form
ref="changePhoneFormRef"
:rules="changePhoneFormRules"
:model="changePhoneForm">
<FormItem prop="oldCode" v-if="authStore.user?.phone">
<div class="flex gap-2">
<BaseInput
v-model="changePhoneForm.oldCode"
type="code"
autofocus
placeholder="请输入原手机号验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<Code :validate-field="() => true"
:type="CodeType.ChangePhoneOld"
:val="authStore.user.phone"/>
</div>
</FormItem>
<FormItem prop="phone">
<BaseInput
v-model="changePhoneForm.phone"
type="tel"
size="large"
placeholder="请输入新手机号"
/>
</FormItem>
<FormItem prop="code">
<div class="flex gap-2">
<BaseInput
v-model="changePhoneForm.code"
type="code"
placeholder="请输入新手机号验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<Code :validate-field="() => changePhoneFormRef.validateField('phone')"
:type="CodeType.ChangePhoneNew"
:val="changePhoneForm.phone"/>
</div>
</FormItem>
<FormItem prop="pwd" v-if="!authStore.user?.phone">
<BaseInput
v-model="changePhoneForm.pwd"
type="password"
size="large"
placeholder="请输入原密码"
/>
</FormItem>
</Form>
<div class="flex justify-between items-end mb-2">
<span class="link text-sm cp"
@click="MessageBox.notice(`请提供证明信息发送邮件到 ${EMAIL} 进行申诉`,'人工申诉')"
v-if="authStore.user?.phone">原手机号不可用,点此申诉</span>
<span v-else></span>
<div>
<BaseButton type="info" @click="showChangePhone = false">取消</BaseButton>
<BaseButton @click="changePhone">保存</BaseButton>
</div>
</div>
</div>
<div class="line"></div>
<!-- Email Section -->
<div class="flex items-center justify-between ">
<div class="item">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">电子邮箱</div>
<div class="flex items-center gap-2">
<IconFluentMail20Regular class="text-base text-gray-500"/>
<BaseInput
v-if="isEditingEmail"
v-model="email"
type="email"
size="normal"
@blur="saveEmail"
@keyup.enter="saveEmail"
class="flex-1 max-w-xs"
autofocus
/>
<span v-else class="text-gray-900">{{ email }}</span>
<div class="mb-2">电子邮箱</div>
<div class="flex items-center gap-2" v-if="authStore.user?.email">
<IconFluentMail20Regular class="text-base"/>
<span>{{ authStore.user?.email }}</span>
</div>
<div v-else class="text-xs">在此设置邮箱</div>
</div>
<BaseIcon @click="showChangeEmailForm">
<IconFluentTextEditStyle20Regular/>
</BaseIcon>
</div>
<div v-if="showChangeEmail">
<Form
ref="changeEmailFormRef"
:rules="changeEmailFormRules"
:model="changeEmailForm">
<FormItem prop="email">
<BaseInput
v-model="changeEmailForm.email"
type="email"
size="large"
placeholder="请输入邮箱地址"
autofocus
/>
</FormItem>
<FormItem prop="code">
<div class="flex gap-2">
<BaseInput
v-model="changeEmailForm.code"
type="code"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<Code :validate-field="() => changeEmailFormRef.validateField('email')"
:type="CodeType.ChangeEmail"
:val="changeEmailForm.email"/>
</div>
</FormItem>
<FormItem prop="pwd" v-if="authStore.user?.hasPwd">
<BaseInput
v-model="changePwdForm.pwd"
type="password"
size="large"
placeholder="请输入密码"
/>
</FormItem>
</Form>
<div class="text-align-end mb-2">
<BaseButton type="info" @click="showChangeEmail = false">取消</BaseButton>
<BaseButton @click="changeEmail">保存</BaseButton>
</div>
<IconFluentTextEditStyle20Regular
@click="isEditingEmail ? saveEmail() : editEmail()"
class="text-xl"/>
</div>
<div class="line"></div>
<!-- Password Section -->
<div class="flex items-center justify-between cp"
@click="showPasswordSection = !showPasswordSection"
>
<div class="item cp" @click="showChangePwdForm">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">设置密码</div>
<div class="text-xs text-gray-500">在此输入密码</div>
<div class="mb-2">设置密码</div>
<div class="text-xs">在此输入密码</div>
</div>
<IconFluentChevronLeft28Filled
class="transition-transform"
:class="['rotate-270','rotate-180'][showPasswordSection?0:1]"/>
class="transition-transform"
:class="['rotate-270','rotate-180'][showChangePwd?0:1]"/>
</div>
<div v-if="showPasswordSection">
<BaseInput placeholder="新密码"
type="password"
autofocus
:min="PASSWORD_CONFIG.minLength"
:max="PASSWORD_CONFIG.maxLength"/>
<div class="text-align-end mt-4">
<BaseButton type="info" @click="showPasswordSection = !showPasswordSection">取消</BaseButton>
<BaseButton @click="changePassword">保存</BaseButton>
<div v-if="showChangePwd">
<Form
ref="changePwdFormRef"
:rules="changePwdFormRules"
:model="changePwdForm">
<FormItem prop="oldPwd" v-if="authStore.user.hasPwd">
<BaseInput
v-model="changePwdForm.oldPwd"
placeholder="旧密码"
type="password"
size="large"
autofocus
/>
</FormItem>
<FormItem prop="newPwd">
<BaseInput
v-model="changePwdForm.newPwd"
type="password"
size="large"
:placeholder="`请输入新密码(${PASSWORD_CONFIG.minLength}-${PASSWORD_CONFIG.maxLength}位)`"
:min="PASSWORD_CONFIG.minLength"
:max="PASSWORD_CONFIG.maxLength"
/>
</FormItem>
<FormItem prop="confirmPwd">
<BaseInput
v-model="changePwdForm.confirmPwd"
type="password"
size="large"
placeholder="请再次输入新密码"
:min="PASSWORD_CONFIG.minLength"
:max="PASSWORD_CONFIG.maxLength"
/>
</FormItem>
</Form>
<div class="text-align-end mb-2">
<BaseButton type="info" @click="showChangePwd = false">取消</BaseButton>
<BaseButton :loading="loading" @click="changePwd">保存</BaseButton>
</div>
</div>
<div class="line"></div>
<!-- Contact Support -->
<div class="flex py-2 items-center justify-between cp"
<div class="item cp"
@click="contactSupport">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1">联系 {{ APP_NAME }} 客服</div>
联系 {{ APP_NAME }} 客服
</div>
<IconFluentChevronLeft28Filled class="rotate-180"/>
</div>
<div class="line"></div>
<!-- Trustpilot Review -->
<div class="flex py-2 items-center justify-between cp"
<div class="item cp"
@click="leaveTrustpilotReview">
<div class="flex-1">
<div class="text-sm font-medium text-gray-700 mb-1"> {{ APP_NAME }} 上留下评论</div>
给 {{ APP_NAME }} 提交意见
</div>
<IconFluentChevronLeft28Filled class="rotate-180"/>
</div>
<div class="line"></div>
<!-- Logout Button -->
<div class="center w-full">
<div class="center w-full mt-4">
<BaseButton
@click="handleLogout"
size="large"
class="w-[80%]"
@click="handleLogout"
size="large"
class="w-[80%]"
>
登出
</BaseButton>
</div>
<div class="text-xs text-center">
<div class="text-xs text-center mt-2">
<a href="/user-agreement.html" target="_blank" class="text-gray-500 hover:text-gray-700">用户协议</a>
<a href="/privacy-policy.html" target="_blank" class="text-gray-500 hover:text-gray-700">隐私政策</a>
@@ -291,4 +601,9 @@ async function changePassword(e) {
</div>
</div>
</BasePage>
</template>
</template>
<style scoped lang="scss">
.item {
@apply flex items-center justify-between min-h-14;
}
</style>

View File

@@ -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<typeof setTimeout> | null = null
let qrCheckInterval: ReturnType<typeof setInterval> | 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<FormInstance>()
@@ -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(() => {
<FormItem prop="phone">
<BaseInput v-model="phoneLoginForm.phone"
type="tel"
name="username"
autocomplete="tel"
size="large"
placeholder="请输入手机号"
/>
@@ -413,20 +366,14 @@ onBeforeUnmount(() => {
<div class="flex gap-2">
<BaseInput
v-model="phoneLoginForm.code"
type="text"
type="code"
size="large"
:max-length="PHONE_CONFIG.codeLength"
placeholder="请输入验证码"
/>
<BaseButton
@click="sendVerificationCode(phoneLoginForm.phone, CodeType.Login,'phone')"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
size="large"
style="border: 1px solid var(--color-input-border)"
>
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '发送验证码') }}
</BaseButton>
<Code :validate-field="() => phoneLoginFormRef.validateField('phone')"
:type="CodeType.Login"
:val="phoneLoginForm.phone"/>
</div>
</FormItem>
</Form>
@@ -439,7 +386,9 @@ onBeforeUnmount(() => {
:model="loginForm2">
<FormItem prop="account">
<BaseInput v-model="loginForm2.account"
type="text"
type="email"
name="username"
autocomplete="email"
size="large"
placeholder="请输入手机号/邮箱地址"
/>
@@ -449,6 +398,8 @@ onBeforeUnmount(() => {
<BaseInput
v-model="loginForm2.password"
type="password"
name="password"
autocomplete="current-password"
size="large"
placeholder="请输入密码"
/>
@@ -487,6 +438,8 @@ onBeforeUnmount(() => {
<BaseInput
v-model="registerForm.account"
type="tel"
name="username"
autocomplete="username"
size="large"
placeholder="请输入手机号/邮箱地址"
/>
@@ -495,26 +448,22 @@ onBeforeUnmount(() => {
<div class="flex gap-2">
<BaseInput
v-model="registerForm.code"
type="text"
type="code"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<BaseButton
@click="sendVerificationCode(registerForm.account, CodeType.Register,'phone')"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
size="large"
style="border: 1px solid var(--color-input-border)"
>
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '获取验证码') }}
</BaseButton>
<Code :validate-field="() => registerFormRef.validateField('account')"
:type="CodeType.Register"
:val="registerForm.account"/>
</div>
</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} 位)`"
/>
@@ -523,6 +472,8 @@ onBeforeUnmount(() => {
<BaseInput
v-model="registerForm.confirmPassword"
type="password"
name="password"
autocomplete="new-password"
size="large"
placeholder="请再次输入密码"
/>
@@ -557,6 +508,8 @@ onBeforeUnmount(() => {
<BaseInput
v-model="forgotForm.account"
type="tel"
name="username"
autocomplete="username"
size="large"
placeholder="请输入手机号/邮箱地址"
/>
@@ -565,26 +518,22 @@ onBeforeUnmount(() => {
<div class="flex gap-2">
<BaseInput
v-model="forgotForm.code"
type="text"
type="code"
size="large"
placeholder="请输入验证码"
:max-length="PHONE_CONFIG.codeLength"
/>
<BaseButton
@click="sendVerificationCode(forgotForm.account, CodeType.ResetPwd,'account')"
:disabled="isSendingCode || codeCountdown > 0"
type="info"
size="large"
style="border: 1px solid var(--color-input-border)"
>
{{ codeCountdown > 0 ? `${codeCountdown}s` : (isSendingCode ? '发送中' : '获取验证码') }}
</BaseButton>
<Code :validate-field="() => forgotFormRef.validateField('account')"
:type="CodeType.ResetPwd"
:val="forgotForm.account"/>
</div>
</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} 位)`"
/>
@@ -593,6 +542,8 @@ onBeforeUnmount(() => {
<BaseInput
v-model="forgotForm.confirmPassword"
type="password"
name="password"
autocomplete="new-password"
size="large"
placeholder="请再次输入新密码"
/>

View File

@@ -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) {

View File

@@ -235,4 +235,7 @@ export enum CodeType {
Login = 0,
Register = 1,
ResetPwd = 2,
ChangeEmail = 3,
ChangePhoneNew = 4,
ChangePhoneOld = 5
}

View File

@@ -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'
},
]