feat: add new theme berry (#860)

* feat: add theme berry

* docs: add development notes

* fix: fix blank page

* chore: update implementation

* fix: fix package.json

* chore: update ui copy

---------

Co-authored-by: JustSong <songquanpeng@foxmail.com>
This commit is contained in:
Buer
2024-01-07 14:20:07 +08:00
committed by GitHub
parent 6227eee5bc
commit 48989d4a0b
157 changed files with 13979 additions and 5 deletions

View File

@@ -0,0 +1,68 @@
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Divider, Grid, Stack, Typography, useMediaQuery } from '@mui/material';
// project imports
import AuthWrapper from '../AuthWrapper';
import AuthCardWrapper from '../AuthCardWrapper';
import ForgetPasswordForm from '../AuthForms/ForgetPasswordForm';
import Logo from 'ui-component/Logo';
// assets
// ================================|| AUTH3 - LOGIN ||================================ //
const ForgetPassword = () => {
const theme = useTheme();
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
return (
<AuthWrapper>
<Grid container direction="column" justifyContent="flex-end">
<Grid item xs={12}>
<Grid container justifyContent="center" alignItems="center" sx={{ minHeight: 'calc(100vh - 136px)' }}>
<Grid item sx={{ m: { xs: 1, sm: 3 }, mb: 0 }}>
<AuthCardWrapper>
<Grid container spacing={2} alignItems="center" justifyContent="center">
<Grid item sx={{ mb: 3 }}>
<Link to="#">
<Logo />
</Link>
</Grid>
<Grid item xs={12}>
<Grid container direction={matchDownSM ? 'column-reverse' : 'row'} alignItems="center" justifyContent="center">
<Grid item>
<Stack alignItems="center" justifyContent="center" spacing={1}>
<Typography color={theme.palette.primary.main} gutterBottom variant={matchDownSM ? 'h3' : 'h2'}>
密码重置
</Typography>
</Stack>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<ForgetPasswordForm />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid item container direction="column" alignItems="center" xs={12}>
<Typography component={Link} to="/login" variant="subtitle1" sx={{ textDecoration: 'none' }}>
登录
</Typography>
</Grid>
</Grid>
</Grid>
</AuthCardWrapper>
</Grid>
</Grid>
</Grid>
</Grid>
</AuthWrapper>
);
};
export default ForgetPassword;

View File

@@ -0,0 +1,94 @@
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { showError } from 'utils/common';
import useLogin from 'hooks/useLogin';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Grid, Stack, Typography, useMediaQuery, CircularProgress } from '@mui/material';
// project imports
import AuthWrapper from '../AuthWrapper';
import AuthCardWrapper from '../AuthCardWrapper';
import Logo from 'ui-component/Logo';
// assets
// ================================|| AUTH3 - LOGIN ||================================ //
const GitHubOAuth = () => {
const theme = useTheme();
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
const [searchParams] = useSearchParams();
const [prompt, setPrompt] = useState('处理中...');
const { githubLogin } = useLogin();
let navigate = useNavigate();
const sendCode = async (code, state, count) => {
const { success, message } = await githubLogin(code, state);
if (!success) {
if (message) {
showError(message);
}
if (count === 0) {
setPrompt(`操作失败,重定向至登录界面中...`);
await new Promise((resolve) => setTimeout(resolve, 2000));
navigate('/login');
return;
}
count++;
setPrompt(`出现错误,第 ${count} 次重试中...`);
await new Promise((resolve) => setTimeout(resolve, 2000));
await sendCode(code, state, count);
}
};
useEffect(() => {
let code = searchParams.get('code');
let state = searchParams.get('state');
sendCode(code, state, 0).then();
}, []);
return (
<AuthWrapper>
<Grid container direction="column" justifyContent="flex-end">
<Grid item xs={12}>
<Grid container justifyContent="center" alignItems="center" sx={{ minHeight: 'calc(100vh - 136px)' }}>
<Grid item sx={{ m: { xs: 1, sm: 3 }, mb: 0 }}>
<AuthCardWrapper>
<Grid container spacing={2} alignItems="center" justifyContent="center">
<Grid item sx={{ mb: 3 }}>
<Link to="#">
<Logo />
</Link>
</Grid>
<Grid item xs={12}>
<Grid container direction={matchDownSM ? 'column-reverse' : 'row'} alignItems="center" justifyContent="center">
<Grid item>
<Stack alignItems="center" justifyContent="center" spacing={1}>
<Typography color={theme.palette.primary.main} gutterBottom variant={matchDownSM ? 'h3' : 'h2'}>
GitHub 登录
</Typography>
</Stack>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} container direction="column" justifyContent="center" alignItems="center" style={{ height: '200px' }}>
<CircularProgress />
<Typography variant="h3" paddingTop={'20px'}>
{prompt}
</Typography>
</Grid>
</Grid>
</AuthCardWrapper>
</Grid>
</Grid>
</Grid>
</Grid>
</AuthWrapper>
);
};
export default GitHubOAuth;

View File

@@ -0,0 +1,66 @@
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Divider, Grid, Stack, Typography, useMediaQuery } from '@mui/material';
// project imports
import AuthWrapper from '../AuthWrapper';
import AuthCardWrapper from '../AuthCardWrapper';
import AuthLogin from '../AuthForms/AuthLogin';
import Logo from 'ui-component/Logo';
// ================================|| AUTH3 - LOGIN ||================================ //
const Login = () => {
const theme = useTheme();
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
return (
<AuthWrapper>
<Grid container direction="column" justifyContent="flex-end">
<Grid item xs={12}>
<Grid container justifyContent="center" alignItems="center" sx={{ minHeight: 'calc(100vh - 136px)' }}>
<Grid item sx={{ m: { xs: 1, sm: 3 }, mb: 0 }}>
<AuthCardWrapper>
<Grid container spacing={2} alignItems="center" justifyContent="center">
<Grid item sx={{ mb: 3 }}>
<Link to="#">
<Logo />
</Link>
</Grid>
<Grid item xs={12}>
<Grid container direction={matchDownSM ? 'column-reverse' : 'row'} alignItems="center" justifyContent="center">
<Grid item>
<Stack alignItems="center" justifyContent="center" spacing={1}>
<Typography color={theme.palette.primary.main} gutterBottom variant={matchDownSM ? 'h3' : 'h2'}>
登录
</Typography>
</Stack>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<AuthLogin />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid item container direction="column" alignItems="center" xs={12}>
<Typography component={Link} to="/register" variant="subtitle1" sx={{ textDecoration: 'none' }}>
注册
</Typography>
</Grid>
</Grid>
</Grid>
</AuthCardWrapper>
</Grid>
</Grid>
</Grid>
</Grid>
</AuthWrapper>
);
};
export default Login;

View File

@@ -0,0 +1,71 @@
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Divider, Grid, Stack, Typography, useMediaQuery } from '@mui/material';
// project imports
import AuthWrapper from '../AuthWrapper';
import AuthCardWrapper from '../AuthCardWrapper';
import Logo from 'ui-component/Logo';
import AuthRegister from '../AuthForms/AuthRegister';
// assets
// ===============================|| AUTH3 - REGISTER ||=============================== //
const Register = () => {
const theme = useTheme();
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
return (
<AuthWrapper>
<Grid container direction="column" justifyContent="flex-end">
<Grid item xs={12}>
<Grid container justifyContent="center" alignItems="center" sx={{ minHeight: 'calc(100vh - 136px)' }}>
<Grid item sx={{ m: { xs: 1, sm: 3 }, mb: 0 }}>
<AuthCardWrapper>
<Grid container spacing={2} alignItems="center" justifyContent="center">
<Grid item sx={{ mb: 3 }}>
<Link to="#">
<Logo />
</Link>
</Grid>
<Grid item xs={12}>
<Grid container direction={matchDownSM ? 'column-reverse' : 'row'} alignItems="center" justifyContent="center">
<Grid item>
<Stack alignItems="center" justifyContent="center" spacing={1}>
<Typography color={theme.palette.primary.main} gutterBottom variant={matchDownSM ? 'h3' : 'h2'}>
注册
</Typography>
</Stack>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<AuthRegister />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid item container direction="column" alignItems="center" xs={12}>
<Typography component={Link} to="/login" variant="subtitle1" sx={{ textDecoration: 'none' }}>
已经有帐号了?点击登录
</Typography>
</Grid>
</Grid>
</Grid>
</AuthCardWrapper>
</Grid>
</Grid>
</Grid>
{/* <Grid item xs={12} sx={{ m: 3, mt: 1 }}>
<AuthFooter />
</Grid> */}
</Grid>
</AuthWrapper>
);
};
export default Register;

View File

@@ -0,0 +1,66 @@
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import { Divider, Grid, Stack, Typography, useMediaQuery } from '@mui/material';
// project imports
import AuthWrapper from '../AuthWrapper';
import AuthCardWrapper from '../AuthCardWrapper';
import ResetPasswordForm from '../AuthForms/ResetPasswordForm';
import Logo from 'ui-component/Logo';
// ================================|| AUTH3 - LOGIN ||================================ //
const ResetPassword = () => {
const theme = useTheme();
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
return (
<AuthWrapper>
<Grid container direction="column" justifyContent="flex-end">
<Grid item xs={12}>
<Grid container justifyContent="center" alignItems="center" sx={{ minHeight: 'calc(100vh - 136px)' }}>
<Grid item sx={{ m: { xs: 1, sm: 3 }, mb: 0 }}>
<AuthCardWrapper>
<Grid container spacing={2} alignItems="center" justifyContent="center">
<Grid item sx={{ mb: 3 }}>
<Link to="#">
<Logo />
</Link>
</Grid>
<Grid item xs={12}>
<Grid container direction={matchDownSM ? 'column-reverse' : 'row'} alignItems="center" justifyContent="center">
<Grid item>
<Stack alignItems="center" justifyContent="center" spacing={1}>
<Typography color={theme.palette.primary.main} gutterBottom variant={matchDownSM ? 'h3' : 'h2'}>
密码重置确认
</Typography>
</Stack>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<ResetPasswordForm />
</Grid>
<Grid item xs={12}>
<Divider />
</Grid>
<Grid item xs={12}>
<Grid item container direction="column" alignItems="center" xs={12}>
<Typography component={Link} to="/login" variant="subtitle1" sx={{ textDecoration: 'none' }}>
登录
</Typography>
</Grid>
</Grid>
</Grid>
</AuthCardWrapper>
</Grid>
</Grid>
</Grid>
</Grid>
</AuthWrapper>
);
};
export default ResetPassword;

View File

@@ -0,0 +1,32 @@
import PropTypes from 'prop-types';
// material-ui
import { Box } from '@mui/material';
// project import
import MainCard from 'ui-component/cards/MainCard';
// ==============================|| AUTHENTICATION CARD WRAPPER ||============================== //
const AuthCardWrapper = ({ children, ...other }) => (
<MainCard
sx={{
maxWidth: { xs: 400, lg: 475 },
margin: { xs: 2.5, md: 3 },
'& > *': {
flexGrow: 1,
flexBasis: '50%'
}
}}
content={false}
{...other}
>
<Box sx={{ p: { xs: 2, sm: 3, xl: 5 } }}>{children}</Box>
</MainCard>
);
AuthCardWrapper.propTypes = {
children: PropTypes.node
};
export default AuthCardWrapper;

View File

@@ -0,0 +1,268 @@
import { useState } from 'react';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
// material-ui
import { useTheme } from '@mui/material/styles';
import {
Box,
Button,
Divider,
FormControl,
FormHelperText,
Grid,
IconButton,
InputAdornment,
InputLabel,
OutlinedInput,
Stack,
Typography,
useMediaQuery
} from '@mui/material';
// third party
import * as Yup from 'yup';
import { Formik } from 'formik';
// project imports
import useLogin from 'hooks/useLogin';
import AnimateButton from 'ui-component/extended/AnimateButton';
import WechatModal from 'views/Authentication/AuthForms/WechatModal';
// assets
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import Github from 'assets/images/icons/github.svg';
import Wechat from 'assets/images/icons/wechat.svg';
import { onGitHubOAuthClicked } from 'utils/common';
// ============================|| FIREBASE - LOGIN ||============================ //
const LoginForm = ({ ...others }) => {
const theme = useTheme();
const { login, wechatLogin } = useLogin();
const [openWechat, setOpenWechat] = useState(false);
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
const customization = useSelector((state) => state.customization);
const siteInfo = useSelector((state) => state.siteInfo);
// const [checked, setChecked] = useState(true);
let tripartiteLogin = false;
if (siteInfo.github_oauth || siteInfo.wechat_login) {
tripartiteLogin = true;
}
const handleWechatOpen = () => {
setOpenWechat(true);
};
const handleWechatClose = () => {
setOpenWechat(false);
};
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};
const handleMouseDownPassword = (event) => {
event.preventDefault();
};
return (
<>
{tripartiteLogin && (
<Grid container direction="column" justifyContent="center" spacing={2}>
{siteInfo.github_oauth && (
<Grid item xs={12}>
<AnimateButton>
<Button
disableElevation
fullWidth
onClick={() => onGitHubOAuthClicked(siteInfo.github_client_id)}
size="large"
variant="outlined"
sx={{
color: 'grey.700',
backgroundColor: theme.palette.grey[50],
borderColor: theme.palette.grey[100]
}}
>
<Box sx={{ mr: { xs: 1, sm: 2, width: 20 }, display: 'flex', alignItems: 'center' }}>
<img src={Github} alt="github" width={25} height={25} style={{ marginRight: matchDownSM ? 8 : 16 }} />
</Box>
使用 Github 登录
</Button>
</AnimateButton>
</Grid>
)}
{siteInfo.wechat_login && (
<Grid item xs={12}>
<AnimateButton>
<Button
disableElevation
fullWidth
onClick={handleWechatOpen}
size="large"
variant="outlined"
sx={{
color: 'grey.700',
backgroundColor: theme.palette.grey[50],
borderColor: theme.palette.grey[100]
}}
>
<Box sx={{ mr: { xs: 1, sm: 2, width: 20 }, display: 'flex', alignItems: 'center' }}>
<img src={Wechat} alt="Wechat" width={25} height={25} style={{ marginRight: matchDownSM ? 8 : 16 }} />
</Box>
使用 Wechat 登录
</Button>
</AnimateButton>
<WechatModal open={openWechat} handleClose={handleWechatClose} wechatLogin={wechatLogin} qrCode={siteInfo.wechat_qrcode} />
</Grid>
)}
<Grid item xs={12}>
<Box
sx={{
alignItems: 'center',
display: 'flex'
}}
>
<Divider sx={{ flexGrow: 1 }} orientation="horizontal" />
<Button
variant="outlined"
sx={{
cursor: 'unset',
m: 2,
py: 0.5,
px: 7,
borderColor: `${theme.palette.grey[100]} !important`,
color: `${theme.palette.grey[900]}!important`,
fontWeight: 500,
borderRadius: `${customization.borderRadius}px`
}}
disableRipple
disabled
>
OR
</Button>
<Divider sx={{ flexGrow: 1 }} orientation="horizontal" />
</Box>
</Grid>
</Grid>
)}
<Formik
initialValues={{
username: '',
password: '',
submit: null
}}
validationSchema={Yup.object().shape({
username: Yup.string().max(255).required('Username is required'),
password: Yup.string().max(255).required('Password is required')
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
const { success, message } = await login(values.username, values.password);
if (success) {
setStatus({ success: true });
} else {
setStatus({ success: false });
if (message) {
setErrors({ submit: message });
}
}
setSubmitting(false);
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit} {...others}>
<FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.customInput }}>
<InputLabel htmlFor="outlined-adornment-username-login">用户名</InputLabel>
<OutlinedInput
id="outlined-adornment-username-login"
type="text"
value={values.username}
name="username"
onBlur={handleBlur}
onChange={handleChange}
label="用户名"
inputProps={{ autoComplete: 'username' }}
/>
{touched.username && errors.username && (
<FormHelperText error id="standard-weight-helper-text-username-login">
{errors.username}
</FormHelperText>
)}
</FormControl>
<FormControl fullWidth error={Boolean(touched.password && errors.password)} sx={{ ...theme.typography.customInput }}>
<InputLabel htmlFor="outlined-adornment-password-login">密码</InputLabel>
<OutlinedInput
id="outlined-adornment-password-login"
type={showPassword ? 'text' : 'password'}
value={values.password}
name="password"
onBlur={handleBlur}
onChange={handleChange}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
size="large"
>
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
}
label="Password"
/>
{touched.password && errors.password && (
<FormHelperText error id="standard-weight-helper-text-password-login">
{errors.password}
</FormHelperText>
)}
</FormControl>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
{/* <FormControlLabel
control={
<Checkbox checked={checked} onChange={(event) => setChecked(event.target.checked)} name="checked" color="primary" />
}
label="记住我"
/> */}
<Typography
component={Link}
to="/reset"
variant="subtitle1"
color="primary"
sx={{ textDecoration: 'none', cursor: 'pointer' }}
>
忘记密码?
</Typography>
</Stack>
{errors.submit && (
<Box sx={{ mt: 3 }}>
<FormHelperText error>{errors.submit}</FormHelperText>
</Box>
)}
<Box sx={{ mt: 2 }}>
<AnimateButton>
<Button disableElevation disabled={isSubmitting} fullWidth size="large" type="submit" variant="contained" color="primary">
登录
</Button>
</AnimateButton>
</Box>
</form>
)}
</Formik>
</>
);
};
export default LoginForm;

View File

@@ -0,0 +1,310 @@
import { useState, useEffect } from 'react';
import { useSelector } from 'react-redux';
import useRegister from 'hooks/useRegister';
import Turnstile from 'react-turnstile';
import { useSearchParams } from 'react-router-dom';
// import { useSelector } from 'react-redux';
// material-ui
import { useTheme } from '@mui/material/styles';
import {
Box,
Button,
FormControl,
FormHelperText,
Grid,
IconButton,
InputAdornment,
InputLabel,
OutlinedInput,
Typography
} from '@mui/material';
// third party
import * as Yup from 'yup';
import { Formik } from 'formik';
// project imports
import AnimateButton from 'ui-component/extended/AnimateButton';
import { strengthColor, strengthIndicator } from 'utils/password-strength';
// assets
import Visibility from '@mui/icons-material/Visibility';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import { showError, showInfo } from 'utils/common';
// ===========================|| FIREBASE - REGISTER ||=========================== //
const RegisterForm = ({ ...others }) => {
const theme = useTheme();
const { register, sendVerificationCode } = useRegister();
const siteInfo = useSelector((state) => state.siteInfo);
const [showPassword, setShowPassword] = useState(false);
const [searchParams] = useSearchParams();
const [showEmailVerification, setShowEmailVerification] = useState(false);
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState('');
const [strength, setStrength] = useState(0);
const [level, setLevel] = useState();
const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};
const handleMouseDownPassword = (event) => {
event.preventDefault();
};
const changePassword = (value) => {
const temp = strengthIndicator(value);
setStrength(temp);
setLevel(strengthColor(temp));
};
const handleSendCode = async (email) => {
if (email === '') {
showError('请输入邮箱');
return;
}
if (turnstileEnabled && turnstileToken === '') {
showError('请稍后几秒重试Turnstile 正在检查用户环境!');
return;
}
const { success, message } = await sendVerificationCode(email, turnstileToken);
if (!success) {
showError(message);
return;
}
};
useEffect(() => {
let affCode = searchParams.get('aff');
if (affCode) {
localStorage.setItem('aff', affCode);
}
setShowEmailVerification(siteInfo.email_verification);
if (siteInfo.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(siteInfo.turnstile_site_key);
}
}, [siteInfo]);
return (
<>
<Formik
initialValues={{
username: '',
password: '',
confirmPassword: '',
email: showEmailVerification ? '' : undefined,
verification_code: showEmailVerification ? '' : undefined,
submit: null
}}
validationSchema={Yup.object().shape({
username: Yup.string().max(255).required('用户名是必填项'),
password: Yup.string().max(255).required('密码是必填项'),
confirmPassword: Yup.string()
.required('确认密码是必填项')
.oneOf([Yup.ref('password'), null], '两次输入的密码不一致'),
email: showEmailVerification ? Yup.string().email('必须是有效的Email地址').max(255).required('Email是必填项') : Yup.mixed(),
verification_code: showEmailVerification ? Yup.string().max(255).required('验证码是必填项') : Yup.mixed()
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试Turnstile 正在检查用户环境!');
setSubmitting(false);
return;
}
const { success, message } = await register(values, turnstileToken);
if (success) {
setStatus({ success: true });
} else {
setStatus({ success: false });
if (message) {
setErrors({ submit: message });
}
}
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit} {...others}>
<FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.customInput }}>
<InputLabel htmlFor="outlined-adornment-username-register">用户名</InputLabel>
<OutlinedInput
id="outlined-adornment-username-register"
type="text"
value={values.username}
name="username"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{ autoComplete: 'username' }}
/>
{touched.username && errors.username && (
<FormHelperText error id="standard-weight-helper-text--register">
{errors.username}
</FormHelperText>
)}
</FormControl>
<FormControl fullWidth error={Boolean(touched.password && errors.password)} sx={{ ...theme.typography.customInput }}>
<InputLabel htmlFor="outlined-adornment-password-register">密码</InputLabel>
<OutlinedInput
id="outlined-adornment-password-register"
type={showPassword ? 'text' : 'password'}
value={values.password}
name="password"
label="Password"
onBlur={handleBlur}
onChange={(e) => {
handleChange(e);
changePassword(e.target.value);
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
size="large"
color={'primary'}
>
{showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
}
inputProps={{}}
/>
{touched.password && errors.password && (
<FormHelperText error id="standard-weight-helper-text-password-register">
{errors.password}
</FormHelperText>
)}
</FormControl>
<FormControl
fullWidth
error={Boolean(touched.confirmPassword && errors.confirmPassword)}
sx={{ ...theme.typography.customInput }}
>
<InputLabel htmlFor="outlined-adornment-confirm-password-register">确认密码</InputLabel>
<OutlinedInput
id="outlined-adornment-confirm-password-register"
type={showPassword ? 'text' : 'password'}
value={values.confirmPassword}
name="confirmPassword"
label="Confirm Password"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{}}
/>
{touched.confirmPassword && errors.confirmPassword && (
<FormHelperText error id="standard-weight-helper-text-confirm-password-register">
{errors.confirmPassword}
</FormHelperText>
)}
</FormControl>
{strength !== 0 && (
<FormControl fullWidth>
<Box sx={{ mb: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Box style={{ backgroundColor: level?.color }} sx={{ width: 85, height: 8, borderRadius: '7px' }} />
</Grid>
<Grid item>
<Typography variant="subtitle1" fontSize="0.75rem">
{level?.label}
</Typography>
</Grid>
</Grid>
</Box>
</FormControl>
)}
{showEmailVerification && (
<>
<FormControl fullWidth error={Boolean(touched.email && errors.email)} sx={{ ...theme.typography.customInput }}>
<InputLabel htmlFor="outlined-adornment-email-register">Email</InputLabel>
<OutlinedInput
id="outlined-adornment-email-register"
type="text"
value={values.email}
name="email"
onBlur={handleBlur}
onChange={handleChange}
endAdornment={
<InputAdornment position="end">
<Button variant="contained" color="primary" onClick={() => handleSendCode(values.email)}>
发送验证码
</Button>
</InputAdornment>
}
inputProps={{}}
/>
{touched.email && errors.email && (
<FormHelperText error id="standard-weight-helper-text--register">
{errors.email}
</FormHelperText>
)}
</FormControl>
<FormControl
fullWidth
error={Boolean(touched.verification_code && errors.verification_code)}
sx={{ ...theme.typography.customInput }}
>
<InputLabel htmlFor="outlined-adornment-verification_code-register">验证码</InputLabel>
<OutlinedInput
id="outlined-adornment-verification_code-register"
type="text"
value={values.verification_code}
name="verification_code"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{}}
/>
{touched.verification_code && errors.verification_code && (
<FormHelperText error id="standard-weight-helper-text--register">
{errors.verification_code}
</FormHelperText>
)}
</FormControl>
</>
)}
{errors.submit && (
<Box sx={{ mt: 3 }}>
<FormHelperText error>{errors.submit}</FormHelperText>
</Box>
)}
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
<Box sx={{ mt: 2 }}>
<AnimateButton>
<Button disableElevation disabled={isSubmitting} fullWidth size="large" type="submit" variant="contained" color="primary">
Sign up
</Button>
</AnimateButton>
</Box>
</form>
)}
</Formik>
</>
);
};
export default RegisterForm;

View File

@@ -0,0 +1,174 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import Turnstile from "react-turnstile";
import { API } from "utils/api";
// material-ui
import { useTheme } from "@mui/material/styles";
import {
Box,
Button,
FormControl,
FormHelperText,
InputLabel,
OutlinedInput,
Typography,
} from "@mui/material";
// third party
import * as Yup from "yup";
import { Formik } from "formik";
// project imports
import AnimateButton from "ui-component/extended/AnimateButton";
// assets
import { showError, showInfo, showSuccess } from "utils/common";
// ===========================|| FIREBASE - REGISTER ||=========================== //
const ForgetPasswordForm = ({ ...others }) => {
const theme = useTheme();
const siteInfo = useSelector((state) => state.siteInfo);
const [sendEmail, setSendEmail] = useState(false);
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState("");
const [turnstileToken, setTurnstileToken] = useState("");
const [disableButton, setDisableButton] = useState(false);
const [countdown, setCountdown] = useState(30);
const submit = async (values, { setSubmitting }) => {
setDisableButton(true);
setSubmitting(true);
if (turnstileEnabled && turnstileToken === "") {
showInfo("请稍后几秒重试Turnstile 正在检查用户环境!");
setSubmitting(false);
return;
}
const res = await API.get(
`/api/reset_password?email=${values.email}&turnstile=${turnstileToken}`
);
const { success, message } = res.data;
if (success) {
showSuccess("重置邮件发送成功,请检查邮箱!");
setSendEmail(true);
} else {
showError(message);
setDisableButton(false);
setCountdown(30);
}
setSubmitting(false);
};
useEffect(() => {
let countdownInterval = null;
if (disableButton && countdown > 0) {
countdownInterval = setInterval(() => {
setCountdown(countdown - 1);
}, 1000);
} else if (countdown === 0) {
setDisableButton(false);
setCountdown(30);
}
return () => clearInterval(countdownInterval);
}, [disableButton, countdown]);
useEffect(() => {
if (siteInfo.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(siteInfo.turnstile_site_key);
}
}, [siteInfo]);
return (
<>
{sendEmail ? (
<Typography variant="h3" padding={"20px"}>
重置邮件发送成功请检查邮箱
</Typography>
) : (
<Formik
initialValues={{
email: "",
}}
validationSchema={Yup.object().shape({
email: Yup.string()
.email("必须是有效的Email地址")
.max(255)
.required("Email是必填项"),
})}
onSubmit={submit}
>
{({
errors,
handleBlur,
handleChange,
handleSubmit,
isSubmitting,
touched,
values,
}) => (
<form noValidate onSubmit={handleSubmit} {...others}>
<FormControl
fullWidth
error={Boolean(touched.email && errors.email)}
sx={{ ...theme.typography.customInput }}
>
<InputLabel htmlFor="outlined-adornment-email-register">
Email
</InputLabel>
<OutlinedInput
id="outlined-adornment-email-register"
type="text"
value={values.email}
name="email"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{}}
/>
{touched.email && errors.email && (
<FormHelperText
error
id="standard-weight-helper-text--register"
>
{errors.email}
</FormHelperText>
)}
</FormControl>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
<Box sx={{ mt: 2 }}>
<AnimateButton>
<Button
disableElevation
disabled={isSubmitting || disableButton}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
{disableButton ? `重试 (${countdown})` : "提交"}
</Button>
</AnimateButton>
</Box>
</form>
)}
</Formik>
)}
</>
);
};
export default ForgetPasswordForm;

View File

@@ -0,0 +1,75 @@
import { useState, useEffect } from "react";
import { useSearchParams } from "react-router-dom";
// material-ui
import { Button, Stack, Typography, Alert } from "@mui/material";
// assets
import { showError, showInfo } from "utils/common";
import { API } from "utils/api";
// ===========================|| FIREBASE - REGISTER ||=========================== //
const ResetPasswordForm = () => {
const [searchParams] = useSearchParams();
const [inputs, setInputs] = useState({
email: "",
token: "",
});
const [newPassword, setNewPassword] = useState("");
const submit = async () => {
const res = await API.post(`/api/user/reset`, inputs);
const { success, message } = res.data;
if (success) {
let password = res.data.data;
setNewPassword(password);
navigator.clipboard.writeText(password);
showInfo(`新密码已复制到剪贴板:${password}`);
} else {
showError(message);
}
};
useEffect(() => {
let email = searchParams.get("email");
let token = searchParams.get("token");
setInputs({
token,
email,
});
}, []);
return (
<Stack
spacing={3}
padding={"24px"}
justifyContent={"center"}
alignItems={"center"}
>
{!inputs.email || !inputs.token ? (
<Typography variant="h3" sx={{ textDecoration: "none" }}>
无效的链接
</Typography>
) : newPassword ? (
<Alert severity="error">
你的新密码是: <b>{newPassword}</b> <br />
请登录后及时修改密码
</Alert>
) : (
<Button
fullWidth
onClick={submit}
size="large"
type="submit"
variant="contained"
color="primary"
>
点击重置密码
</Button>
)}
</Stack>
);
};
export default ResetPasswordForm;

View File

@@ -0,0 +1,70 @@
// WechatModal.js
import PropTypes from 'prop-types';
import React from 'react';
import { Dialog, DialogTitle, DialogContent, TextField, Button, Typography, Grid } from '@mui/material';
import { Formik, Form, Field } from 'formik';
import { showError } from 'utils/common';
import * as Yup from 'yup';
const validationSchema = Yup.object().shape({
code: Yup.string().required('验证码不能为空')
});
const WechatModal = ({ open, handleClose, wechatLogin, qrCode }) => {
const handleSubmit = (values) => {
const { success, message } = wechatLogin(values.code);
if (success) {
handleClose();
} else {
showError(message || '未知错误');
}
};
return (
<Dialog open={open} onClose={handleClose}>
<DialogTitle>微信验证码登录</DialogTitle>
<DialogContent>
<Grid container direction="column" alignItems="center">
<img src={qrCode} alt="二维码" style={{ maxWidth: '300px', maxHeight: '300px', width: 'auto', height: 'auto' }} />
<Typography
variant="body2"
color="text.secondary"
style={{ marginTop: '10px', textAlign: 'center', wordWrap: 'break-word', maxWidth: '300px' }}
>
请使用微信扫描二维码关注公众号输入验证码获取验证码三分钟内有效
</Typography>
<Formik initialValues={{ code: '' }} validationSchema={validationSchema} onSubmit={handleSubmit}>
{({ errors, touched }) => (
<Form style={{ width: '100%' }}>
<Grid item xs={12}>
<Field
as={TextField}
name="code"
label="验证码"
error={touched.code && Boolean(errors.code)}
helperText={touched.code && errors.code}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<Button type="submit" fullWidth>
提交
</Button>
</Grid>
</Form>
)}
</Formik>
</Grid>
</DialogContent>
</Dialog>
);
};
export default WechatModal;
WechatModal.propTypes = {
open: PropTypes.bool,
handleClose: PropTypes.func,
wechatLogin: PropTypes.func,
qrCode: PropTypes.string
};

View File

@@ -0,0 +1,28 @@
// material-ui
import { styled } from '@mui/material/styles';
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router';
import { useEffect, useContext } from 'react';
import { UserContext } from 'contexts/UserContext';
// ==============================|| AUTHENTICATION 1 WRAPPER ||============================== //
const AuthStyle = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.primary.light
}));
// eslint-disable-next-line
const AuthWrapper = ({ children }) => {
const account = useSelector((state) => state.account);
const { isUserLoaded } = useContext(UserContext);
const navigate = useNavigate();
useEffect(() => {
if (isUserLoaded && account.user) {
navigate('/panel');
}
}, [account, navigate, isUserLoaded]);
return <AuthStyle> {children} </AuthStyle>;
};
export default AuthWrapper;