2
0

fix:注册界面及相关功能

This commit is contained in:
zhongzm
2024-11-29 09:04:06 +08:00
parent a3a6aeb40e
commit 650a180778
10 changed files with 222 additions and 33 deletions

View File

@@ -1,12 +1,13 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useCountDown, useLoading } from '@sa/hooks'; import { useCountDown, useLoading } from '@sa/hooks';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { REG_PHONE } from '@/constants/reg'; import {REG_EMAIL} from '@/constants/reg';
import {useAuthStore} from "@/store/modules/auth";
export function useCaptcha() { export function useCaptcha() {
const { loading, startLoading, endLoading } = useLoading(); const { loading, startLoading, endLoading } = useLoading();
const { count, start, stop, isCounting } = useCountDown(10); const { count, start, stop, isCounting } = useCountDown(10);
const authStore = useAuthStore();
const label = computed(() => { const label = computed(() => {
let text = $t('page.login.codeLogin.getCode'); let text = $t('page.login.codeLogin.getCode');
@@ -23,14 +24,14 @@ export function useCaptcha() {
return text; return text;
}); });
function isPhoneValid(phone: string) { function isEmailValid(email: string) {
if (phone.trim() === '') { if (email.trim() === '') {
$message?.error?.($t('form.phone.required')); $message?.error?.($t('form.phone.required'));
return false; return false;
} }
if (!REG_PHONE.test(phone)) { if (!REG_EMAIL.test(email)) {
$message?.error?.($t('form.phone.invalid')); $message?.error?.($t('form.phone.invalid'));
return false; return false;
@@ -38,17 +39,20 @@ export function useCaptcha() {
return true; return true;
} }
//获取验证码方法
async function getCaptcha(phone: string) { async function getCaptcha(email: string) {
const valid = isPhoneValid(phone); console.log(email)
//const valid = isPhoneValid(phone);
const valid = isEmailValid(email);
if (!valid || loading.value) { if (!valid || loading.value) {
return; return;
} }
startLoading(); startLoading();
// request await authStore.captcha(
email,
);
await new Promise(resolve => { await new Promise(resolve => {
setTimeout(resolve, 500); setTimeout(resolve, 500);
}); });

View File

@@ -178,6 +178,7 @@ const local: any = {
back: 'Back', back: 'Back',
validateSuccess: 'Verification passed', validateSuccess: 'Verification passed',
loginSuccess: 'Login successfully', loginSuccess: 'Login successfully',
registerSuccess:'Register successfully',
welcomeBack: 'Welcome back, {username} !', welcomeBack: 'Welcome back, {username} !',
checkCode: 'Please check the verification code', checkCode: 'Please check the verification code',
emailPlaceholder:'Please enter the email' emailPlaceholder:'Please enter the email'
@@ -224,6 +225,9 @@ const local: any = {
address: 'Address', address: 'Address',
next: 'Next', next: 'Next',
prev: 'Prev', prev: 'Prev',
birthDate: 'Birth Date',
birthDatePlaceholder: 'Please select birth date',
birthDateRequired: 'Please select birth date',
}, },
resetPwd: { resetPwd: {
title: 'Reset Password' title: 'Reset Password'

View File

@@ -178,6 +178,7 @@ const local:any = {
back: '返回', back: '返回',
validateSuccess: '验证成功', validateSuccess: '验证成功',
loginSuccess: '登录成功', loginSuccess: '登录成功',
registerSuccess:'注册成功',
welcomeBack: '欢迎回来,{username} ', welcomeBack: '欢迎回来,{username} ',
checkCode: '请输入验证码', checkCode: '请输入验证码',
emailPlaceholder:'请输入邮箱' emailPlaceholder:'请输入邮箱'
@@ -224,6 +225,9 @@ const local:any = {
address: '地址', address: '地址',
next: '下一步', next: '下一步',
prev: '上一步', prev: '上一步',
birthDate: '出生日期',
birthDatePlaceholder: '请选择出生日期',
birthDateRequired: '请选择出生日期',
}, },
resetPwd: { resetPwd: {
title: '重置密码' title: '重置密码'

View File

@@ -13,7 +13,23 @@ export function fetchLogin(body: Api.Auth.LoginBody) {
data: body data: body
}); });
} }
//邮箱验证码接口
export function sendCaptcha(body:Api.Auth.EmailCaptcha){
return request({
url:`/system/email/code?email=${body.email}`,
method:'get',
})
}
//验证注册
//添加注册
export function fetchRegister(body: Api.Auth.RegisterBody) {
return request({
url: '/auth/register',
method: 'post',
data: body
});
}
/** logout */ /** logout */
export function doDeleteLogout() { export function doDeleteLogout() {
return request<App.Service.Response<null>>({ return request<App.Service.Response<null>>({

View File

@@ -7,6 +7,7 @@ import { localStg } from '@/utils/storage';
import { $t } from '@/locales'; import { $t } from '@/locales';
import { useRouteStore } from '../route'; import { useRouteStore } from '../route';
import { clearAuthStorage, emptyInfo, getToken } from './shared'; import { clearAuthStorage, emptyInfo, getToken } from './shared';
import {sendCaptcha} from "@/service/api/auth";
export const useAuthStore = defineStore(SetupStoreId.Auth, () => { export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
const routeStore = useRouteStore(); const routeStore = useRouteStore();
@@ -121,6 +122,34 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
} }
return false; return false;
} }
/**
* Register new user
*/
async function register(registerForm: Api.Auth.RegisterBody) {
startLoading();
const { error } = await fetchRegister(registerForm);
if (!error) {
$message?.success($t('page.login.common.registerSuccess'));
// 注册成功后跳转到登录页
await toLogin();
}
endLoading();
return !error;
}
async function captcha(email:string){
if (!email) {
return;
}
try {
await sendCaptcha({ email }); // 这里调用后端接口发送验证码
} catch (error) {
}
}
return { return {
token, token,
@@ -131,6 +160,8 @@ export const useAuthStore = defineStore(SetupStoreId.Auth, () => {
resetStore, resetStore,
permissions, permissions,
login, login,
refreshUserInfo refreshUserInfo,
register,
captcha,
}; };
}); });

14
src/typings/api.d.ts vendored
View File

@@ -138,6 +138,20 @@ declare namespace Api {
uuid: string; uuid: string;
authType: string; authType: string;
} }
interface RegisterBody{
username: string;
password: string;
authType:string;
email: string;
fullName: string;
age: number;
address: string;
sex: string;
phonenumber: string;
}
interface EmailCaptcha{
email:string;
}
} }
/** /**

View File

@@ -358,6 +358,7 @@ declare namespace App {
back: string; back: string;
validateSuccess: string; validateSuccess: string;
loginSuccess: string; loginSuccess: string;
registerSuccess: string;
welcomeBack: string; welcomeBack: string;
checkCode: string; checkCode: string;
}; };

View File

@@ -20,6 +20,7 @@ declare global {
const beforeAll: typeof import('vitest')['beforeAll'] const beforeAll: typeof import('vitest')['beforeAll']
const beforeEach: typeof import('vitest')['beforeEach'] const beforeEach: typeof import('vitest')['beforeEach']
const chai: typeof import('vitest')['chai'] const chai: typeof import('vitest')['chai']
const checkReport: typeof import('../service/api/auth')['checkReport']
const clearAuthStorage: typeof import('../store/modules/auth/shared')['clearAuthStorage'] const clearAuthStorage: typeof import('../store/modules/auth/shared')['clearAuthStorage']
const cloneDeep: typeof import('lodash-es')['cloneDeep'] const cloneDeep: typeof import('lodash-es')['cloneDeep']
const computed: typeof import('vue')['computed'] const computed: typeof import('vue')['computed']
@@ -96,6 +97,7 @@ declare global {
const fetchIsRouteExist: typeof import('../service/api/route')['fetchIsRouteExist'] const fetchIsRouteExist: typeof import('../service/api/route')['fetchIsRouteExist']
const fetchLogin: typeof import('../service/api/auth')['fetchLogin'] const fetchLogin: typeof import('../service/api/auth')['fetchLogin']
const fetchRefreshToken: typeof import('../service/api/auth')['fetchRefreshToken'] const fetchRefreshToken: typeof import('../service/api/auth')['fetchRefreshToken']
const fetchRegister: typeof import('../service/api/auth')['fetchRegister']
const filterAuthRoutesByRoles: typeof import('../store/modules/route/shared')['filterAuthRoutesByRoles'] const filterAuthRoutesByRoles: typeof import('../store/modules/route/shared')['filterAuthRoutesByRoles']
const filterTabsById: typeof import('../store/modules/tab/shared')['filterTabsById'] const filterTabsById: typeof import('../store/modules/tab/shared')['filterTabsById']
const filterTabsByIds: typeof import('../store/modules/tab/shared')['filterTabsByIds'] const filterTabsByIds: typeof import('../store/modules/tab/shared')['filterTabsByIds']
@@ -182,6 +184,7 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent'] const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const sendCaptcha: typeof import('../service/api/auth')['sendCaptcha']
const sessionStg: typeof import('../utils/storage')['sessionStg'] const sessionStg: typeof import('../utils/storage')['sessionStg']
const setActivePinia: typeof import('pinia')['setActivePinia'] const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']

View File

@@ -13,6 +13,7 @@ declare module 'vue' {
ACard: typeof import('ant-design-vue/es')['Card'] ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACol: typeof import('ant-design-vue/es')['Col'] ACol: typeof import('ant-design-vue/es')['Col']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions'] ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem'] ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider'] ADivider: typeof import('ant-design-vue/es')['Divider']

View File

@@ -1,17 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref } from 'vue'; import {computed, reactive, ref} from 'vue';
import { useI18n } from 'vue-i18n'; // 添加这行 import {useI18n} from 'vue-i18n'; // 添加这行
import { useRouterPush } from '@/hooks/common/router'; import {useAuthStore} from '@/store/modules/auth';
import { useAntdForm, useFormRules } from '@/hooks/common/form'; import {useRouterPush} from '@/hooks/common/router';
import { useCaptcha } from '@/hooks/business/captcha'; import {useAntdForm, useFormRules} from '@/hooks/common/form';
import { useWindowSize } from '@vueuse/core'; import {useCaptcha} from '@/hooks/business/captcha';
import { registerTerms } from '@/views/_builtin/login/modules/terms'; import {useWindowSize} from '@vueuse/core';
import {registerTerms} from '@/views/_builtin/login/modules/terms';
import dayjs from 'dayjs';
import type {Rule} from 'ant-design-vue/es/form';
const { t } = useI18n();
const authStore = useAuthStore();
defineOptions({ defineOptions({
name: 'Register' name: 'Register'
}); });
const { t } = useI18n();
const { toggleLoginModule } = useRouterPush(); const { toggleLoginModule } = useRouterPush();
const { formRef, validate } = useAntdForm(); const { formRef, validate } = useAntdForm();
const { label, isCounting, loading, getCaptcha } = useCaptcha(); const { label, isCounting, loading, getCaptcha } = useCaptcha();
@@ -24,12 +29,39 @@ const currentStep = ref(0);
// 是否同意协议 // 是否同意协议
const agreeTerms = ref(false); const agreeTerms = ref(false);
// 定义一个统一的数据模型
interface RegisterModel {
username: string;
password: string;
email: string;
fullName: string;
age:0,
gender: string;
phone: string;
address: string;
code: string;
authType: string;
}
// 使用一个统一的 model
const model = reactive<RegisterModel>({
username: '',
password: '',
email: '',
fullName: '',
age:0,
gender: '',
phone: '',
address: '',
code: '',
authType: 'u'
});
// 第一步表单数据 // 第一步表单数据
interface BasicFormModel { interface BasicFormModel {
username: string; username: string;
fullName: string; fullName: string;
age: number | undefined; birthDate: string;
gender: string; gender: string;
phone: string; phone: string;
email: string; email: string;
@@ -39,7 +71,7 @@ interface BasicFormModel {
const basicModel = reactive<BasicFormModel>({ const basicModel = reactive<BasicFormModel>({
username: '', username: '',
fullName: '', fullName: '',
age: undefined, birthDate: '',
gender: '', gender: '',
phone: '', phone: '',
email: '', email: '',
@@ -62,20 +94,20 @@ const securityModel = reactive<SecurityFormModel>({
}); });
// 第一步表单验证规则 // 第一步表单验证规则
const basicRules = computed(() => { const basicRules = computed<Record<string, Rule | Rule[]>>(() => {
const { formRules } = useFormRules(); const { formRules } = useFormRules();
return { return {
username: formRules.username, username: formRules.username,
phone: formRules.phone, email: formRules.email,
email: formRules.email birthDate: [{ required: true, message: t('page.login.register.birthDateRequired') }],
phone: [{ pattern: /^1[3-9]\d{9}$/, message: t('form.phone.invalid'), trigger: 'blur' }]
}; };
}); });
// 第三步表单验证规则 // 第三步表单验证规则
const securityRules = computed(() => { const securityRules = computed<Record<string, Rule | Rule[]>>(() => {
const { formRules, createConfirmPwdRule } = useFormRules(); const { formRules, createConfirmPwdRule } = useFormRules();
return { return {
phone: formRules.phone,
code: formRules.code, code: formRules.code,
password: formRules.pwd, password: formRules.pwd,
confirmPassword: createConfirmPwdRule(securityModel.password) confirmPassword: createConfirmPwdRule(securityModel.password)
@@ -92,7 +124,7 @@ const terms = computed(() => {
async function nextStep() { async function nextStep() {
if (currentStep.value === 0) { if (currentStep.value === 0) {
await validate(); await validate();
// 复制邮箱到第三步 // 复制电话号码到第三步
securityModel.email = basicModel.email; securityModel.email = basicModel.email;
} }
if (currentStep.value === 1) { if (currentStep.value === 1) {
@@ -107,11 +139,35 @@ async function nextStep() {
function prevStep() { function prevStep() {
currentStep.value -= 1; currentStep.value -= 1;
} }
//注册按钮
async function handleSubmit() { async function handleSubmit() {
await validate(); try {
// request to register await validate();
$message?.success(t('page.login.common.validateSuccess'));
// 整合表单数据
model.username = basicModel.username;
model.password = securityModel.password;
model.email = securityModel.email;
model.fullName = basicModel.fullName;
model.gender = basicModel.gender; // 直接使用 gender 值
model.phone = basicModel.phone;
model.address = basicModel.address;
model.code = securityModel.code;
const success = await authStore.register({
...model,
age: dayjs().diff(dayjs(basicModel.birthDate), 'year'),
sex: model.gender, // 直接使用 gender 值,不需要转换
phonenumber: model.phone,
});
if (success) {
window.$message?.success(t('page.login.register.registerSuccess'));
toggleLoginModule('pwd-login');
}
} catch (error) {
console.error('Form validation failed:', error);
}
} }
// 在script setup部分添加一个新的计算属性 // 在script setup部分添加一个新的计算属性
@@ -165,8 +221,13 @@ const showSteps = computed(() => !isMobile.value);
</AFormItem> </AFormItem>
</ACol> </ACol>
<ACol :xs="12" :sm="12" :lg="24"> <ACol :xs="12" :sm="12" :lg="24">
<AFormItem name="age" :label="t('page.login.register.age')"> <AFormItem name="birthDate" :label="t('page.login.register.birthDate')">
<AInputNumber v-model:value="basicModel.age" class="!w-full" /> <ADatePicker
v-model:value="basicModel.birthDate"
class="!w-full birth-date-picker"
:placeholder="t('page.login.register.birthDatePlaceholder')"
:disabled-date="(current: dayjs.Dayjs) => current && current.isAfter(dayjs())"
/>
</AFormItem> </AFormItem>
</ACol> </ACol>
<ACol :xs="12" :sm="12" :lg="24"> <ACol :xs="12" :sm="12" :lg="24">
@@ -296,6 +357,52 @@ const showSteps = computed(() => !isMobile.value);
<style scoped> <style scoped>
@media (max-width: 640px) { @media (max-width: 640px) {
:deep(.ant-form) {
.ant-form-item {
margin-bottom: 4px !important;
}
.ant-form-item-label {
padding: 0 !important;
line-height: 28px !important;
> label {
font-size: 13px !important;
height: 28px !important;
padding-bottom: 0 !important;
}
}
.ant-input,
.ant-input-password,
.ant-select,
.ant-input-number,
.ant-btn {
font-size: 13px !important;
height: 28px !important;
line-height: 28px !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.ant-select-selector {
height: 28px !important;
padding: 0 8px !important;
.ant-select-selection-item {
line-height: 26px !important;
}
}
.ant-input-number {
height: 32px !important;
input {
height: 30px !important;
padding: 0 8px !important;
}
}
}
.mobile-step-indicator { .mobile-step-indicator {
font-size: 13px; font-size: 13px;
@@ -331,4 +438,8 @@ const showSteps = computed(() => !isMobile.value);
padding-bottom: 4px; padding-bottom: 4px;
} }
} }
.birth-date-picker {
margin-top: 2px !important;
}
</style> </style>