diff --git a/src/api/email.api.ts b/src/api/email.api.ts new file mode 100644 index 0000000..b391580 --- /dev/null +++ b/src/api/email.api.ts @@ -0,0 +1,6 @@ +import axios from '@/utils/axios' +import { EmailSendDto, EmailSendResponse } from './email.interface' + +export async function sendEmailVerifyCode(params: EmailSendDto) { + return axios.get('/api/email/verifyCode', { params }) +} diff --git a/src/api/email.interface.ts b/src/api/email.interface.ts new file mode 100644 index 0000000..e069eaa --- /dev/null +++ b/src/api/email.interface.ts @@ -0,0 +1,28 @@ +/** 邮箱验证码场景 */ +export enum EmailVerifyCodeScene { + register = 'register', + forgetPassword = 'forgetPassword', + changeEmail = 'changeEmail', + deleteUser = 'deleteUser', +} + +/** 验证码邮件发送入参 */ +export interface EmailSendDto { + email: string + scene: EmailVerifyCodeScene +} + +/** 验证码邮件发送回参 */ +export interface EmailSendResponse { + token: string + userId?: string +} + +/** 邮箱验证入参 */ +export interface EmailVerifyDto { + email: string + /** @description 发送邮箱接口返回的Token */ + token: string + /** @description 邮箱验证码 */ + verifyCode: string +} diff --git a/src/api/index.ts b/src/api/index.ts index 0764d36..355ca1b 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1 +1,2 @@ -export * as user from './user' +export * as user from './user.api' +export * as email from './email.api' diff --git a/src/api/user.ts b/src/api/user.api.ts similarity index 89% rename from src/api/user.ts rename to src/api/user.api.ts index c9dc044..c2e9730 100644 --- a/src/api/user.ts +++ b/src/api/user.api.ts @@ -2,13 +2,13 @@ import axios from '@/utils/axios' import { type AxiosResponse } from 'axios' import { LoginInputDto, - CreateUserDto, + RegisterInputDto, Token, TokenRefreshPayload, User, } from './user.interface' -export async function register(data: CreateUserDto) { +export async function register(data: RegisterInputDto) { return axios.post('/api/users', data) } diff --git a/src/api/user.interface.ts b/src/api/user.interface.ts index 9d3f221..684727b 100644 --- a/src/api/user.interface.ts +++ b/src/api/user.interface.ts @@ -1,10 +1,13 @@ +import { EmailVerifyDto } from './email.interface' + +/** 登录入参 */ export interface LoginInputDto { email: string password: string } -export interface CreateUserDto { - email: string +/** 注册入参 */ +export interface RegisterInputDto extends EmailVerifyDto { password: string username?: string } @@ -18,6 +21,7 @@ export interface TokenRefreshPayload { refreshToken: string } +/** 用户 */ export interface User { id: string email: string diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 30cbcba..97a5c8b 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -5,6 +5,7 @@ import NextLink, { type LinkProps as NextLinkProps } from 'next/link' import { LinkProps } from '@mui/material/Link' import { ThemeProvider, createTheme } from '@mui/material' import { ToastContainer } from 'react-toastify' +import styled from '@emotion/styled' import 'react-toastify/dist/ReactToastify.css' import '@fontsource/roboto/300.css' import '@fontsource/roboto/400.css' @@ -32,12 +33,18 @@ const theme = createTheme({ }, }) +const StyledToastContainer = styled(ToastContainer)` + .Toastify__toast-body { + font-size: 0.9rem; + } +` + export default function App({ Component, pageProps }: AppProps) { return ( - + - - + + 登录 @@ -89,9 +89,10 @@ export default function Login() { - - 注册 - + + 没有账号? + + 注册 diff --git a/src/pages/register.tsx b/src/pages/register.tsx index a93cf3b..da29533 100644 --- a/src/pages/register.tsx +++ b/src/pages/register.tsx @@ -2,28 +2,35 @@ import Avatar from '@mui/material/Avatar' import Button from '@mui/material/Button' import TextField from '@mui/material/TextField' import Link from '@mui/material/Link' -import Grid from '@mui/material/Grid' +import Grid from '@mui/material/Unstable_Grid2' import Box from '@mui/material/Box' -import LockOutlinedIcon from '@mui/icons-material/LockOutlined' +import AppRegistrationIcon from '@mui/icons-material/AppRegistration' import Typography from '@mui/material/Typography' import Container from '@mui/material/Container' import { useFormik } from 'formik' import { useRouter } from 'next/router' import * as yup from '@/utils/validation' import * as api from '@/api' +import { RegisterInputDto } from '@/api/user.interface' +import { EmailVerifyCodeScene } from '@/api/email.interface' +import { useCountdown, COUNTDOWN_SECONDS } from '@/utils/useCountdown' export default function Register() { const router = useRouter() + const { countdown, setCountdown } = useCountdown() - const formik = useFormik({ + const formik = useFormik({ initialValues: { email: '', password: '', + verifyCode: '', + token: '', }, validateOnChange: false, validationSchema: yup.object({ email: yup.emailSchema, password: yup.passwordSchema, + verifyCode: yup.verifyCodeSchema, }), onSubmit: async (values) => { const res = await api.user.register(values) @@ -33,8 +40,23 @@ export default function Register() { }, }) + async function sendVerifyCode() { + await formik.validateField('email') + if (formik.errors.email) return + setCountdown(COUNTDOWN_SECONDS) + try { + const res = await api.email.sendEmailVerifyCode({ + email: formik.values.email, + scene: EmailVerifyCodeScene.register, + }) + formik.setFieldValue('token', res.data.token) + } catch (err) { + setCountdown(0) + } + } + return ( - + - - + + 注册 @@ -53,7 +75,7 @@ export default function Register() { component="form" onSubmit={formik.handleSubmit} noValidate - sx={{ mt: 1 }} + sx={{ mt: 1, width: '100%' }} > + + + + + + + + - - - 登录 - + + + 已有账号? + + 登录 diff --git a/src/utils/useCountdown.ts b/src/utils/useCountdown.ts new file mode 100644 index 0000000..4000043 --- /dev/null +++ b/src/utils/useCountdown.ts @@ -0,0 +1,27 @@ +import { useState, useRef, useEffect } from 'react' + +/** 倒计时 */ +export function useCountdown() { + const [countdown, setCountdown] = useState(0) + const previousCountdownRef = useRef(countdown) + useEffect(() => { + previousCountdownRef.current = countdown + }, [countdown]) + + const intervalRef = useRef() + + useEffect(() => { + intervalRef.current = window.setInterval(() => { + const previousCountdown = previousCountdownRef.current + if (previousCountdown > 0) { + const newCountdown = previousCountdown - 1 + setCountdown(newCountdown) + } + }, 1000) + return () => window.clearInterval(intervalRef.current) + }, []) + + return { countdown, setCountdown } +} + +export const COUNTDOWN_SECONDS = 60 diff --git a/src/utils/validation.ts b/src/utils/validation.ts index e925cc1..a5f42b6 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -14,3 +14,8 @@ export const passwordSchema = yup .matches(/^(.*)?\d+(.*)?$/, '至少一个数字') .matches(/^(?=.*[a-z])(?=.*[A-Z])[^]*$/, '大写字母和小写字母') .matches(/^[^\s].*[^\s]$/, '首尾字符不能是空格') + +export const verifyCodeSchema = yup + .string() + .required('请输入验证码') + .length(6, '验证码为6位数')