Initial commit

This commit is contained in:
2025-10-11 11:55:25 +08:00
parent 467dad8449
commit 8107dee8d3
2879 changed files with 610575 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Button,
Flex,
FormControl,
FormLabel,
HStack,
Icon,
Input,
Link,
Switch,
Text,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import BasicImage from "assets/img/BasicImage.png";
import React from "react";
import { FaApple, FaFacebook, FaGoogle } from "react-icons/fa";
import AuthBasic from "layouts/AuthBasic";
function LockBasic() {
// Chakra color mode
const textColor = useColorModeValue("gray.700", "white");
const bgForm = useColorModeValue("white", "navy.800");
return (
<AuthBasic
title="Welcome!"
description="Use these awesome forms to login or create new account in your project for free."
image={BasicImage}
>
<Flex
w="100%"
h="100%"
alignItems="center"
justifyContent="center"
mb="60px"
mt={{ base: "60px", md: "0px" }}
>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "20px", md: "100px" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
>
<Text
fontWeight="bold"
color={textColor}
textAlign="center"
mb="10px"
fontSize={{ base: "3xl", md: "4xl" }}
>
Mike Priesler
</Text>
<Text
fontWeight="regular"
textAlign="center"
color="gray.400"
mb="35px"
>
Enter your password to unlock your account.
</Text>
<FormControl>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Password
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="password"
placeholder="Your password"
mb="24px"
size="lg"
/>
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
UNLOCK
</Button>
</FormControl>
</Flex>
</Flex>
</AuthBasic>
);
}
export default LockBasic;

View File

@@ -0,0 +1,109 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Button,
Flex,
FormControl,
FormLabel,
Input,
Text,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import CoverImage from "assets/img/CoverImage.png";
import React from "react";
import AuthCover from "layouts/AuthCover";
function LockCover() {
// Chakra color mode
const textColor = useColorModeValue("gray.700", "white");
const bgForm = useColorModeValue("white", "navy.800");
return (
<AuthCover image={CoverImage}>
<Flex
w="100%"
h="100%"
alignItems="center"
justifyContent="center"
mb="60px"
mt={{ base: "60px", md: "30vh" }}
>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "20px", md: "100px" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
>
<Text
fontWeight="bold"
color={textColor}
textAlign="center"
mb="10px"
fontSize={{ base: "3xl", md: "4xl" }}
>
Mike Priesler
</Text>
<Text
fontWeight="regular"
textAlign="center"
color="gray.400"
mb="35px"
>
Enter your password to unlock your account.
</Text>
<FormControl>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Email
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="text"
placeholder="Your email address"
mb="24px"
size="lg"
/>
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
UNLOCK
</Button>
</FormControl>
</Flex>
</Flex>
</AuthCover>
);
}
export default LockCover;

View File

@@ -0,0 +1,81 @@
import React from "react";
// Chakra imports
import {
Button,
Flex,
FormControl,
FormLabel,
Input,
Text,
useColorModeValue,
LightMode,
} from "@chakra-ui/react";
// Assets
import illustration from "assets/img/illustration-auth.png";
import AuthIllustration from "layouts/AuthIllustration";
function LockIllustration() {
// Chakra color mode
const textColor = useColorModeValue("blue.500", "blue.500");
return (
<AuthIllustration
illustrationBackground='linear-gradient(180deg, #3182CE 0%, #63B3ED 100%)'
image={illustration}>
<Flex
w='100%'
h='100%'
alignItems='start'
justifyContent='start'
mb={{ base: "0px", md: "60px" }}
mt={{ base: "60px", md: "34vh" }}>
<Flex
zIndex='2'
direction='column'
w='445px'
background='transparent'
borderRadius='15px'
pe={{ base: "0px", md: "80px" }}
mx={{ base: "20px", md: "0px" }}
mb={{ base: "20px", md: "auto" }}>
<Text
fontWeight='bold'
color={textColor}
mb='10px'
fontSize={{ base: "3xl", md: "4xl" }}>
Mike Priesler
</Text>
<Text fontWeight='regular' color='gray.400' mb='35px'>
Enter your password to unlock your account.
</Text>
<FormControl>
<FormLabel ms='4px' fontSize='sm' fontWeight='normal'>
Password
</FormLabel>
<Input
variant='main'
fontSize='sm'
ms='4px'
type='password'
placeholder='Your password'
mb='24px'
size='lg'
/>
<LightMode>
<Button
fontSize='10px'
colorScheme='blue'
fontWeight='bold'
w='100%'
h='45'
mb='24px'>
UNLOCK
</Button>
</LightMode>
</FormControl>
</Flex>
</Flex>
</AuthIllustration>
);
}
export default LockIllustration;

View File

@@ -0,0 +1,125 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Button,
Flex,
FormControl,
FormLabel,
Input,
Text,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import BasicImage from "assets/img/BasicImage.png";
import React from "react";
import AuthBasic from "layouts/AuthBasic";
function ResetCover() {
// Chakra color mode
const textColor = useColorModeValue("gray.700", "white");
const bgForm = useColorModeValue("white", "navy.800");
return (
<AuthBasic
title="Welcome!"
description="Use these awesome forms to login or create new account in your project for free."
image={BasicImage}
>
<Flex
w="100%"
h="100%"
alignItems="center"
justifyContent="center"
mb="60px"
mt={{ base: "60px", md: "0px" }}
>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "20px", md: "100px" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
>
<Text
fontWeight="bold"
color={textColor}
textAlign="center"
mb="10px"
fontSize={{ base: "3xl", md: "4xl" }}
>
Reset password
</Text>
<Text
fontWeight="regular"
textAlign="center"
color="gray.400"
mb="35px"
>
You will receive an e-mail in maximum 60 seconds.
</Text>
<FormControl>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Email
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="text"
placeholder="Your email address"
mb="24px"
size="lg"
/>
{/* <FormLabel ms='4px' fontSize='sm' fontWeight='normal'>
Password
</FormLabel>
<Input
variant='auth'
fontSize='sm'
ms='4px'
type='password'
placeholder='Your password'
mb='24px'
size='lg'
/> */}
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
SEND
</Button>
</FormControl>
</Flex>
</Flex>
</AuthBasic>
);
}
export default ResetCover;

View File

@@ -0,0 +1,109 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Button,
Flex,
FormControl,
FormLabel,
Input,
Text,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import CoverImage from "assets/img/CoverImage.png";
import React from "react";
import AuthCover from "layouts/AuthCover";
function ResetCover() {
// Chakra color mode
const textColor = useColorModeValue("gray.700", "white");
const bgForm = useColorModeValue("white", "navy.800");
return (
<AuthCover image={CoverImage}>
<Flex
w="100%"
h="100%"
alignItems="center"
justifyContent="center"
mb="60px"
mt={{ base: "60px", md: "30vh" }}
>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "20px", md: "100px" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
>
<Text
fontWeight="bold"
color={textColor}
textAlign="center"
mb="10px"
fontSize={{ base: "3xl", md: "4xl" }}
>
Reset password
</Text>
<Text
fontWeight="regular"
textAlign="center"
color="gray.400"
mb="35px"
>
You will receive an e-mail in maximum 60 seconds.
</Text>
<FormControl>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Email
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="text"
placeholder="Your email address"
mb="24px"
size="lg"
/>
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
SEND
</Button>
</FormControl>
</Flex>
</Flex>
</AuthCover>
);
}
export default ResetCover;

View File

@@ -0,0 +1,81 @@
import React from "react";
// Chakra imports
import {
Button,
Flex,
FormControl,
FormLabel,
Input,
Text,
useColorModeValue,
LightMode,
} from "@chakra-ui/react";
// Assets
import illustration from "assets/img/illustration-auth.png";
import AuthIllustration from "layouts/AuthIllustration";
function ResetIllustration() {
// Chakra color mode
const textColor = useColorModeValue("blue.500", "blue.500");
return (
<AuthIllustration
illustrationBackground='linear-gradient(180deg, #3182CE 0%, #63B3ED 100%)'
image={illustration}>
<Flex
w='100%'
h='100%'
alignItems='start'
justifyContent='start'
mb={{ base: "0px", md: "60px" }}
mt={{ base: "60px", md: "34vh" }}>
<Flex
zIndex='2'
direction='column'
w='445px'
background='transparent'
borderRadius='15px'
pe={{ base: "0px", md: "80px" }}
mx={{ base: "20px", md: "0px" }}
mb={{ base: "20px", md: "auto" }}>
<Text
fontWeight='bold'
color={textColor}
mb='10px'
fontSize={{ base: "3xl", md: "4xl" }}>
Reset password
</Text>
<Text fontWeight='regular' color='gray.400' mb='35px'>
You will receive an e-mail in maximum 60 seconds.
</Text>
<FormControl>
<FormLabel ms='4px' fontSize='sm' fontWeight='normal'>
Email
</FormLabel>
<Input
variant='main'
fontSize='sm'
ms='4px'
type='text'
placeholder='Your email address'
mb='24px'
size='lg'
/>
<LightMode>
<Button
fontSize='10px'
colorScheme='blue'
fontWeight='bold'
w='100%'
h='45'
mb='24px'>
SEND
</Button>
</LightMode>
</FormControl>
</Flex>
</Flex>
</AuthIllustration>
);
}
export default ResetIllustration;

View File

@@ -0,0 +1,160 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import React, { useState } from "react";
import {
Box,
Button,
Flex,
FormControl,
FormLabel,
Heading,
Input,
Stack,
useColorModeValue,
Text,
Link,
InputGroup,
InputRightElement,
IconButton,
useToast,
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../../contexts/AuthContext";
export default function SignInBasic() {
const [showPassword, setShowPassword] = useState(false);
const [formData, setFormData] = useState({
email: "",
password: "",
});
const navigate = useNavigate();
const toast = useToast();
const { login, isLoading } = useAuth();
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!formData.email || !formData.password) {
toast({
title: "请填写完整信息",
description: "邮箱和密码都是必填项",
status: "warning",
duration: 3000,
isClosable: true,
});
return;
}
const result = await login(formData.email, formData.password, 'email');
if (result.success) {
// 登录成功,跳转到首页
navigate("/home");
}
};
return (
<Flex minH="100vh" align="center" justify="center" bg={useColorModeValue("gray.50", "gray.900")}>
<Stack spacing={8} mx="auto" maxW="lg" py={12} px={6}>
<Stack align="center">
<Heading fontSize="4xl" color="blue.600">
价小前投研
</Heading>
<Text fontSize="lg" color="gray.600">
登录您的账户
</Text>
</Stack>
<Box
rounded="lg"
bg={useColorModeValue("white", "gray.700")}
boxShadow="lg"
p={8}
>
<form onSubmit={handleSubmit}>
<Stack spacing={4}>
<FormControl id="email" isRequired>
<FormLabel>邮箱地址</FormLabel>
<Input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="请输入您的邮箱"
/>
</FormControl>
<FormControl id="password" isRequired>
<FormLabel>密码</FormLabel>
<InputGroup>
<Input
type={showPassword ? "text" : "password"}
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="请输入您的密码"
/>
<InputRightElement>
<IconButton
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
variant="ghost"
size="sm"
/>
</InputRightElement>
</InputGroup>
</FormControl>
<Stack spacing={10}>
<Button
type="submit"
bg="blue.600"
color="white"
_hover={{
bg: "blue.700",
}}
isLoading={isLoading}
loadingText="登录中..."
>
登录
</Button>
</Stack>
<Stack pt={6}>
<Text align="center">
还没有账户?{" "}
<Link color="blue.600" onClick={() => navigate("/auth/signup")}>
立即注册
</Link>
</Text>
</Stack>
</Stack>
</form>
</Box>
</Stack>
</Flex>
);
}

View File

@@ -0,0 +1,207 @@
import React, { useState } from "react";
import {
Box,
Button,
FormControl,
FormLabel,
Input,
VStack,
Heading,
Text,
Link,
useColorMode,
InputGroup,
InputRightElement,
IconButton,
Spinner,
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../../contexts/AuthContext";
export default function SignInCentered() {
const { colorMode } = useColorMode();
const navigate = useNavigate();
const { login, isLoading } = useAuth();
// 表单状态
const [formData, setFormData] = useState({
email: "",
password: "",
});
// UI状态
const [showPassword, setShowPassword] = useState(false);
const [errors, setErrors] = useState({});
// 处理输入变化
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// 清除对应字段的错误
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ""
}));
}
};
// 表单验证
const validateForm = () => {
const newErrors = {};
if (!formData.email) {
newErrors.email = "邮箱是必填项";
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = "请输入有效的邮箱地址";
}
if (!formData.password) {
newErrors.password = "密码是必填项";
} else if (formData.password.length < 6) {
newErrors.password = "密码至少需要6个字符";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理表单提交
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
const result = await login(formData.email, formData.password);
if (result.success) {
// 登录成功,跳转到首页
navigate("/home");
}
};
return (
<Box
minH="100vh"
display="flex"
alignItems="center"
justifyContent="center"
bg={colorMode === "dark" ? "gray.800" : "gray.50"}
p={4}
>
<Box
w="full"
maxW="md"
p={8}
bg={colorMode === "dark" ? "gray.700" : "white"}
borderRadius="lg"
shadow="xl"
>
<VStack spacing={6}>
<Box textAlign="center">
<Heading size="lg" mb={2}>欢迎回来</Heading>
<Text color="gray.500">请输入您的凭据登录</Text>
</Box>
<form onSubmit={handleSubmit} style={{ width: "100%" }}>
<VStack spacing={4}>
<FormControl isInvalid={!!errors.email}>
<FormLabel>邮箱地址</FormLabel>
<Input
name="email"
type="email"
placeholder="your@email.com"
value={formData.email}
onChange={handleInputChange}
size="lg"
/>
{errors.email && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.email}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.password}>
<FormLabel>密码</FormLabel>
<InputGroup size="lg">
<Input
name="password"
type={showPassword ? "text" : "password"}
placeholder="********"
value={formData.password}
onChange={handleInputChange}
/>
<InputRightElement>
<IconButton
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
variant="ghost"
onClick={() => setShowPassword(!showPassword)}
/>
</InputRightElement>
</InputGroup>
{errors.password && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.password}
</Text>
)}
</FormControl>
<Button
type="submit"
colorScheme="blue"
w="full"
size="lg"
isLoading={isLoading}
loadingText="登录中..."
>
{isLoading ? <Spinner size="sm" /> : "登录"}
</Button>
</VStack>
</form>
<VStack spacing={3}>
<Text fontSize="sm" textAlign="center">
还没有账户{" "}
<Link
color="blue.500"
onClick={() => navigate("/auth/signup")}
_hover={{ textDecoration: "underline" }}
>
立即注册
</Link>
</Text>
<Box textAlign="center">
<Link
color="gray.500"
fontSize="sm"
_hover={{ color: "blue.500" }}
>
忘记密码
</Link>
<Text color="gray.500" fontSize="sm" mt={2}>
还没有账户{" "}
<Link
color="blue.500"
fontWeight="medium"
_hover={{ textDecoration: "underline" }}
onClick={() => navigate('/auth/sign-up')}
>
立即注册
</Link>
</Text>
</Box>
</VStack>
</VStack>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,223 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Box,
Button,
Flex,
FormControl,
FormLabel,
HStack,
Icon,
Input,
Link,
Switch,
Text,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import CoverImage from "assets/img/CoverImage.png";
import React from "react";
import { FaApple, FaFacebook, FaGoogle } from "react-icons/fa";
import AuthCover from "layouts/AuthCover";
function SignInCover() {
// Chakra color mode
const textColor = useColorModeValue("gray.400", "white");
const bgForm = useColorModeValue("white", "navy.800");
const titleColor = useColorModeValue("gray.700", "blue.500");
const colorIcons = useColorModeValue("gray.700", "white");
const bgIcons = useColorModeValue("trasnparent", "navy.700");
const bgIconsHover = useColorModeValue("gray.50", "whiteAlpha.100");
return (
<AuthCover image={CoverImage}>
<Flex
w="100%"
h="100%"
alignItems="center"
justifyContent="center"
mb="60px"
mt={{ base: "60px", md: "160px" }}
>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "100px" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
>
<Text
fontSize="xl"
color={textColor}
fontWeight="bold"
textAlign="center"
mb="22px"
>
Sign In with
</Text>
<HStack spacing="15px" justify="center" mb="22px">
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon as={FaFacebook} color={colorIcons} w="30px" h="30px" />
</Link>
</Flex>
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon
as={FaApple}
color={colorIcons}
w="30px"
h="30px"
_hover={{ filter: "brightness(120%)" }}
/>
</Link>
</Flex>
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon
as={FaGoogle}
color={colorIcons}
w="30px"
h="30px"
_hover={{ filter: "brightness(120%)" }}
/>
</Link>
</Flex>
</HStack>
<Text
fontSize="lg"
color="gray.400"
fontWeight="bold"
textAlign="center"
mb="22px"
>
or
</Text>
<FormControl>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Name
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="text"
placeholder="Your full name"
mb="24px"
size="lg"
/>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Password
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="password"
placeholder="Your password"
mb="24px"
size="lg"
/>
<FormControl display="flex" alignItems="center" mb="24px">
<Switch id="remember-login" colorScheme="blue" me="10px" />
<FormLabel htmlFor="remember-login" mb="0" fontWeight="normal">
Remember me
</FormLabel>
</FormControl>
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
SIGN IN
</Button>
</FormControl>
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
maxW="100%"
mt="0px"
>
<Text color={textColor} fontWeight="medium">
Dont have an account?
<Link
color={titleColor}
as="span"
ms="5px"
href="#"
fontWeight="bold"
>
Sign up
</Link>
</Text>
</Flex>
</Flex>
</Flex>
</AuthCover>
);
}
export default SignInCover;

View File

@@ -0,0 +1,932 @@
// src/views/Authentication/SignIn/SignInIllustration.js - Session版本
import React, { useState, useEffect } from "react";
import {
Box,
Button,
Flex,
FormControl,
Input,
Stack,
Text,
Heading,
VStack,
HStack,
Checkbox,
useToast,
Icon,
InputGroup,
InputRightElement,
IconButton,
Tab,
TabList,
Tabs,
Link as ChakraLink,
Center,
Spinner,
Divider,
Alert,
AlertIcon,
useDisclosure
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { FaUser, FaEnvelope, FaMobile, FaWeixin, FaLock, FaQrcode, FaCode } from "react-icons/fa";
import { useNavigate, Link, useLocation } from "react-router-dom";
import { useAuth } from "../../../contexts/AuthContext";
import PrivacyPolicyModal from "../../../components/PrivacyPolicyModal";
import UserAgreementModal from "../../../components/UserAgreementModal";
// API配置
const isProduction = process.env.NODE_ENV === 'production';
const API_BASE_URL = isProduction ? "" : "http://49.232.185.254:5000";
export default function SignInIllustration() {
const [loginType, setLoginType] = useState(0); // 0: 微信, 1: 手机号
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [agreeToTerms, setAgreeToTerms] = useState(false);
// 传统登录表单数据
const [formData, setFormData] = useState({
username: "",
email: "",
phone: "",
password: "",
verificationCode: "", // 添加验证码字段
});
// 验证码登录相关状态
const [useVerificationCode, setUseVerificationCode] = useState(false);
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
const [countdown, setCountdown] = useState(0);
const [sendingCode, setSendingCode] = useState(false);
// 微信登录相关状态
const [wechatAuthUrl, setWechatAuthUrl] = useState("");
// 隐私政策弹窗状态
const {
isOpen: isPrivacyModalOpen,
onOpen: onPrivacyModalOpen,
onClose: onPrivacyModalClose
} = useDisclosure();
// 用户协议弹窗状态
const {
isOpen: isUserAgreementModalOpen,
onOpen: onUserAgreementModalOpen,
onClose: onUserAgreementModalClose
} = useDisclosure();
const navigate = useNavigate();
const location = useLocation();
const toast = useToast();
const { login, checkSession } = useAuth();
// 倒计时效果
useEffect(() => {
let timer;
if (countdown > 0) {
timer = setInterval(() => {
setCountdown(prev => prev - 1);
}, 1000);
} else if (countdown === 0) {
setVerificationCodeSent(false);
}
return () => clearInterval(timer);
}, [countdown]);
// 检查URL参数中的错误信息微信登录失败时
useEffect(() => {
const params = new URLSearchParams(location.search);
const error = params.get('error');
if (error) {
let errorMessage = '登录失败';
switch (error) {
case 'wechat_auth_failed':
errorMessage = '微信授权失败';
break;
case 'session_expired':
errorMessage = '会话已过期,请重新登录';
break;
case 'token_failed':
errorMessage = '获取微信授权失败';
break;
case 'userinfo_failed':
errorMessage = '获取用户信息失败';
break;
case 'login_failed':
errorMessage = '登录处理失败,请重试';
break;
default:
errorMessage = '登录失败,请重试';
}
toast({
title: "登录失败",
description: errorMessage,
status: "error",
duration: 5000,
isClosable: true,
});
// 清除URL参数
const newUrl = window.location.pathname;
window.history.replaceState({}, document.title, newUrl);
}
}, [location, toast]);
// 获取微信授权URL
const getWechatQRCode = async () => {
try {
setIsLoading(true);
const response = await fetch(`${API_BASE_URL}/api/auth/wechat/qrcode`);
if (!response.ok) {
throw new Error('获取二维码失败');
}
const data = await response.json();
setWechatAuthUrl(data.auth_url);
} catch (error) {
console.error('获取微信授权失败:', error);
toast({
title: "获取微信授权失败",
description: error.message || "请稍后重试",
status: "error",
duration: 3000,
});
} finally {
setIsLoading(false);
}
};
// 切换到微信登录时获取二维码
useEffect(() => {
if (loginType === 0) {
getWechatQRCode();
}
}, [loginType]);
// 打开微信登录窗口
const openWechatLogin = () => {
if (!agreeToTerms) {
toast({
title: "请先同意协议",
description: "请勾选同意用户协议和隐私政策后再使用微信登录",
status: "warning",
duration: 3000,
isClosable: true,
});
return;
}
if (wechatAuthUrl) {
// 方案1直接跳转推荐
window.location.href = wechatAuthUrl;
// 方案2新窗口打开备选
// const width = 600;
// const height = 600;
// const left = (window.innerWidth - width) / 2;
// const top = (window.innerHeight - height) / 2;
// window.open(
// wechatAuthUrl,
// 'wechat_login',
// `width=${width},height=${height},left=${left},top=${top}`
// );
}
};
// 发送验证码
const sendVerificationCode = async () => {
const credential = formData.phone;
const type = 'phone';
if (!credential) {
toast({
title: "请先输入手机号",
status: "warning",
duration: 3000,
});
return;
}
// 基本格式验证
if (!/^1[3-9]\d{9}$/.test(credential)) {
toast({
title: "请输入有效的手机号",
status: "warning",
duration: 3000,
});
return;
}
try {
setSendingCode(true);
const response = await fetch(`${API_BASE_URL}/api/auth/send-verification-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
credential,
type,
purpose: 'login'
}),
});
const data = await response.json();
if (response.ok && data.success) {
toast({
title: "验证码已发送",
description: "验证码已发送到您的手机号",
status: "success",
duration: 3000,
});
setVerificationCodeSent(true);
setCountdown(60); // 60秒倒计时
} else {
throw new Error(data.error || '发送验证码失败');
}
} catch (error) {
toast({
title: "发送验证码失败",
description: error.message || "请稍后重试",
status: "error",
duration: 3000,
});
} finally {
setSendingCode(false);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleTraditionalLogin = async (e) => {
e.preventDefault();
if (!agreeToTerms) {
toast({
title: "请先同意协议",
description: "请勾选同意用户协议和隐私政策后再登录",
status: "warning",
duration: 3000,
isClosable: true,
});
return;
}
setIsLoading(true);
try {
let credential = '';
let authLoginType = '';
if (loginType === 1) {
credential = formData.phone;
authLoginType = 'phone';
}
// 验证码登录
if (useVerificationCode && loginType === 1) {
if (!credential || !formData.verificationCode) {
toast({
title: "请填写完整信息",
description: "手机号和验证码不能为空",
status: "warning",
duration: 3000,
});
return;
}
const result = await loginWithVerificationCode(credential, formData.verificationCode, authLoginType);
if (result.success) {
navigate("/home");
}
} else {
// 传统密码登录
if (!credential || !formData.password) {
toast({
title: "请填写完整信息",
description: `${getCredentialName()}和密码不能为空`,
status: "warning",
duration: 3000,
});
return;
}
const result = await login(credential, formData.password, authLoginType);
if (result.success) {
navigate("/home");
}
}
} catch (error) {
console.error('Login error:', error);
} finally {
setIsLoading(false);
}
};
// 验证码登录函数
const loginWithVerificationCode = async (credential, verificationCode, authLoginType) => {
try {
const response = await fetch(`${API_BASE_URL}/api/auth/login-with-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
credential,
verification_code: verificationCode,
login_type: authLoginType
}),
});
const data = await response.json();
if (response.ok && data.success) {
// 更新认证状态
await checkSession();
toast({
title: "登录成功",
description: "欢迎回来!",
status: "success",
duration: 3000,
});
return { success: true };
} else {
throw new Error(data.error || '验证码登录失败');
}
} catch (error) {
toast({
title: "登录失败",
description: error.message || "请检查验证码是否正确",
status: "error",
duration: 3000,
});
return { success: false, error: error.message };
}
};
// 获取凭据名称
const getCredentialName = () => {
return "手机号";
};
const getInputPlaceholder = () => {
return "请输入手机号";
};
const getInputName = () => {
return "phone";
};
const getInputValue = () => {
return formData.phone;
};
return (
<Flex minH="100vh" position="relative" overflow="hidden">
{/* 流体波浪背景 */}
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
zIndex={0}
background={`
linear-gradient(45deg,
rgba(139, 69, 19, 0.9) 0%,
rgba(160, 82, 45, 0.8) 15%,
rgba(205, 133, 63, 0.7) 30%,
rgba(222, 184, 135, 0.8) 45%,
rgba(245, 222, 179, 0.6) 60%,
rgba(255, 228, 196, 0.7) 75%,
rgba(139, 69, 19, 0.8) 100%
)
`}
_before={{
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: `
conic-gradient(from 0deg at 30% 20%,
rgba(255, 140, 0, 0.6) 0deg,
rgba(255, 69, 0, 0.4) 60deg,
rgba(139, 69, 19, 0.5) 120deg,
rgba(160, 82, 45, 0.6) 180deg,
rgba(205, 133, 63, 0.4) 240deg,
rgba(255, 140, 0, 0.5) 300deg,
rgba(255, 140, 0, 0.6) 360deg
)
`,
mixBlendMode: 'multiply',
animation: 'fluid-rotate 20s linear infinite'
}}
_after={{
content: '""',
position: 'absolute',
top: '10%',
left: '20%',
width: '60%',
height: '80%',
borderRadius: '50%',
background: 'radial-gradient(ellipse at center, rgba(255, 165, 0, 0.3) 0%, rgba(255, 140, 0, 0.2) 50%, transparent 70%)',
filter: 'blur(40px)',
animation: 'wave-pulse 8s ease-in-out infinite'
}}
sx={{
'@keyframes fluid-rotate': {
'0%': { transform: 'rotate(0deg) scale(1)' },
'50%': { transform: 'rotate(180deg) scale(1.1)' },
'100%': { transform: 'rotate(360deg) scale(1)' }
},
'@keyframes wave-pulse': {
'0%, 100%': { opacity: 0.4, transform: 'scale(1)' },
'50%': { opacity: 0.8, transform: 'scale(1.2)' }
}
}}
/>
{/* 主要内容 */}
<Flex
width="100%"
align="center"
justify="center"
position="relative"
zIndex={1}
px={6}
py={12}
>
{/* 登录卡片 */}
<Box
bg="white"
borderRadius="2xl"
boxShadow="2xl"
p={8}
width="100%"
maxW="480px"
backdropFilter="blur(20px)"
border="1px solid rgba(255, 255, 255, 0.2)"
>
{/* 头部区域 */}
<VStack spacing={6} mb={8}>
<VStack spacing={2}>
<Heading size="xl" color="gray.800" fontWeight="bold">
欢迎回来
</Heading>
<Text color="gray.600" fontSize="md">
登录价值前沿继续您的投资之旅
</Text>
</VStack>
{/* 登录方式选择 */}
<Box width="100%">
<Tabs
index={loginType}
onChange={setLoginType}
variant="soft-rounded"
colorScheme="blue"
isFitted
>
<TabList bg="gray.100" borderRadius="xl" p={1}>
<Tab
borderRadius="lg"
_selected={{
bg: "linear-gradient(135deg, #00bcd4 0%, #2196f3 100%)",
color: "white",
transform: "scale(1.02)"
}}
transition="all 0.2s"
fontSize="sm"
>
<Icon as={FaWeixin} mr={1} />
微信
</Tab>
<Tab
borderRadius="lg"
_selected={{
bg: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
color: "white",
transform: "scale(1.02)"
}}
transition="all 0.2s"
fontSize="sm"
>
<Icon as={FaMobile} mr={1} />
手机号
</Tab>
</TabList>
</Tabs>
</Box>
</VStack>
{/* 登录内容 */}
{loginType === 0 ? (
// 微信登录 - 简化版
<VStack spacing={6}>
{/* 协议同意勾选框 - 微信登录 */}
<Box width="100%">
<Checkbox
isChecked={agreeToTerms}
onChange={(e) => setAgreeToTerms(e.target.checked)}
colorScheme="green"
size="sm"
>
<Text fontSize="sm" color="gray.600">
我已阅读并同意{" "}
<ChakraLink
color="green.500"
fontSize="sm"
onClick={onUserAgreementModalOpen}
textDecoration="underline"
_hover={{ color: "green.600" }}
>
用户协议
</ChakraLink>
{" "}{" "}
<ChakraLink
color="green.500"
fontSize="sm"
onClick={onPrivacyModalOpen}
textDecoration="underline"
_hover={{ color: "green.600" }}
>
隐私政策
</ChakraLink>
</Text>
</Checkbox>
{!agreeToTerms && (
<Text fontSize="xs" color="red.500" mt={1} ml={6}>
请先同意用户协议和隐私政策
</Text>
)}
</Box>
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
<VStack spacing={6}>
<Icon as={FaQrcode} w={20} h={20} color={agreeToTerms ? "green.500" : "gray.400"} />
<VStack spacing={2}>
<Text fontSize="lg" fontWeight="bold" color={agreeToTerms ? "gray.700" : "gray.400"}>
微信扫码登录
</Text>
<Text fontSize="sm" color={agreeToTerms ? "gray.500" : "gray.400"} textAlign="center">
{agreeToTerms ? "使用微信扫一扫,安全快速登录" : "请先同意协议后使用微信登录"}
</Text>
</VStack>
<Button
colorScheme="green"
size="lg"
leftIcon={<Icon as={FaWeixin} />}
onClick={openWechatLogin}
isLoading={isLoading || !wechatAuthUrl}
loadingText="准备中..."
isDisabled={!wechatAuthUrl || !agreeToTerms}
_hover={agreeToTerms ? {
transform: "translateY(-2px)",
boxShadow: "lg"
} : {}}
_active={agreeToTerms ? {
transform: "translateY(0)"
} : {}}
opacity={agreeToTerms ? 1 : 0.6}
>
立即扫码登录
</Button>
{!wechatAuthUrl && !isLoading && agreeToTerms && (
<Button
size="sm"
variant="link"
colorScheme="blue"
onClick={getWechatQRCode}
>
重新获取
</Button>
)}
</VStack>
</Center>
<Alert
status={agreeToTerms ? "info" : "warning"}
borderRadius="lg"
bg={agreeToTerms ? "blue.50" : "orange.50"}
border="1px solid"
borderColor={agreeToTerms ? "blue.200" : "orange.200"}
>
<AlertIcon color={agreeToTerms ? "blue.500" : "orange.500"} />
<Text fontSize="sm" color={agreeToTerms ? "blue.700" : "orange.700"}>
{agreeToTerms
? "点击按钮后将跳转到微信授权页面"
: "请先同意用户协议和隐私政策"}
</Text>
</Alert>
</VStack>
) : (
// 传统登录
<form onSubmit={handleTraditionalLogin}>
<VStack spacing={4}>
<FormControl isRequired>
<InputGroup>
<Input
name={getInputName()}
value={getInputValue()}
onChange={handleInputChange}
placeholder={getInputPlaceholder()}
size="lg"
borderRadius="lg"
bg="gray.50"
border="1px solid"
borderColor="gray.200"
_focus={{
borderColor: "blue.500",
boxShadow: "0 0 0 1px #667eea"
}}
/>
<InputRightElement pointerEvents="none">
<Icon
as={FaMobile}
color="gray.400"
/>
</InputRightElement>
</InputGroup>
</FormControl>
{/* 登录方式切换(仅手机号显示) */}
{loginType === 1 && (
<VStack spacing={2} align="stretch">
<HStack justify="center">
<Button
size="sm"
variant={useVerificationCode ? "ghost" : "solid"}
colorScheme="blue"
onClick={() => {
setUseVerificationCode(false);
setFormData(prev => ({ ...prev, verificationCode: "" }));
}}
>
密码登录
</Button>
<Button
size="sm"
variant={useVerificationCode ? "solid" : "ghost"}
colorScheme="green"
onClick={() => setUseVerificationCode(true)}
>
验证码登录
</Button>
</HStack>
</VStack>
)}
{/* 密码输入框 */}
{!useVerificationCode ? (
<FormControl isRequired>
<InputGroup size="lg">
<Input
name="password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={handleInputChange}
placeholder="请输入密码"
borderRadius="lg"
bg="gray.50"
border="1px solid"
borderColor="gray.200"
_focus={{
borderColor: "blue.500",
boxShadow: "0 0 0 1px #667eea"
}}
/>
<InputRightElement>
<IconButton
variant="ghost"
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
/>
</InputRightElement>
</InputGroup>
</FormControl>
) : (
// 验证码输入框
<VStack spacing={3} align="stretch">
<HStack>
<FormControl isRequired flex={2}>
<InputGroup size="lg">
<Input
name="verificationCode"
value={formData.verificationCode}
onChange={handleInputChange}
placeholder="请输入验证码"
borderRadius="lg"
bg="gray.50"
border="1px solid"
borderColor="gray.200"
_focus={{
borderColor: "green.500",
boxShadow: "0 0 0 1px #48bb78"
}}
maxLength={6}
/>
<InputRightElement>
<Icon as={FaCode} color="gray.400" />
</InputRightElement>
</InputGroup>
</FormControl>
<Button
flex={1}
size="lg"
colorScheme="green"
variant="outline"
onClick={sendVerificationCode}
isLoading={sendingCode}
isDisabled={verificationCodeSent && countdown > 0}
borderRadius="lg"
>
{sendingCode
? "发送中..."
: verificationCodeSent && countdown > 0
? `${countdown}s`
: "发送验证码"
}
</Button>
</HStack>
</VStack>
)}
<HStack justify="space-between" width="100%">
<Checkbox
isChecked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
colorScheme="blue"
>
<Text fontSize="sm" color="gray.600">记住我</Text>
</Checkbox>
<ChakraLink href="#" color="blue.500" fontSize="sm">
忘记密码
</ChakraLink>
</HStack>
{/* 协议同意勾选框 */}
<Box width="100%">
<Checkbox
isChecked={agreeToTerms}
onChange={(e) => setAgreeToTerms(e.target.checked)}
colorScheme="blue"
size="sm"
>
<Text fontSize="sm" color="gray.600">
我已阅读并同意{" "}
<ChakraLink
color="blue.500"
fontSize="sm"
onClick={onUserAgreementModalOpen}
textDecoration="underline"
_hover={{ color: "blue.600" }}
>
用户协议
</ChakraLink>
{" "}{" "}
<ChakraLink
color="blue.500"
fontSize="sm"
onClick={onPrivacyModalOpen}
textDecoration="underline"
_hover={{ color: "blue.600" }}
>
隐私政策
</ChakraLink>
</Text>
</Checkbox>
{!agreeToTerms && (
<Text fontSize="xs" color="red.500" mt={1} ml={6}>
请先同意用户协议和隐私政策
</Text>
)}
</Box>
<Button
type="submit"
width="100%"
size="lg"
background={agreeToTerms
? "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
: "gray.300"
}
color="white"
borderRadius="lg"
_hover={agreeToTerms ? {
transform: "translateY(-2px)",
boxShadow: "lg"
} : {}}
_active={agreeToTerms ? {
transform: "translateY(0)"
} : {}}
isLoading={isLoading}
loadingText="登录中..."
fontWeight="bold"
isDisabled={!agreeToTerms}
cursor={agreeToTerms ? "pointer" : "not-allowed"}
>
<Icon as={FaLock} mr={2} />
登录
</Button>
</VStack>
</form>
)}
{/* 底部链接 */}
<VStack spacing={4} mt={6}>
<HStack>
<Divider />
<Text fontSize="sm" color="gray.500" px={4}></Text>
<Divider />
</HStack>
<Button
as={Link}
to="/auth/sign-up"
width="100%"
size="lg"
bg="gray.800"
color="white"
borderRadius="lg"
_hover={{
bg: "gray.700",
transform: "translateY(-2px)",
boxShadow: "lg"
}}
_active={{
transform: "translateY(0)"
}}
fontWeight="bold"
>
没有账号立即注册
</Button>
</VStack>
</Box>
</Flex>
{/* 底部导航链接 */}
<Box
position="absolute"
bottom={8}
left="50%"
transform="translateX(-50%)"
zIndex={1}
>
<HStack spacing={6}>
<ChakraLink
color="white"
fontSize="sm"
opacity={0.8}
_hover={{ opacity: 1 }}
onClick={onUserAgreementModalOpen}
>
用户协议
</ChakraLink>
<ChakraLink
color="white"
fontSize="sm"
opacity={0.8}
_hover={{ opacity: 1 }}
onClick={onPrivacyModalOpen}
>
隐私政策
</ChakraLink>
</HStack>
</Box>
{/* 隐私政策弹窗 */}
<PrivacyPolicyModal
isOpen={isPrivacyModalOpen}
onClose={onPrivacyModalClose}
/>
{/* 用户协议弹窗 */}
<UserAgreementModal
isOpen={isUserAgreementModalOpen}
onClose={onUserAgreementModalClose}
/>
</Flex>
);
}

View File

@@ -0,0 +1,254 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import React, { useState } from "react";
import {
Box,
Button,
Flex,
FormControl,
FormLabel,
Heading,
Input,
Stack,
useColorModeValue,
Text,
Link,
InputGroup,
InputRightElement,
IconButton,
useToast,
Checkbox,
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { useNavigate } from "react-router-dom";
export default function SignUpBasic() {
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
username: "",
email: "",
password: "",
confirmPassword: "",
agreeToTerms: false,
});
const navigate = useNavigate();
const toast = useToast();
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === "checkbox" ? checked : value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
if (formData.password !== formData.confirmPassword) {
toast({
title: "密码不匹配",
description: "请确保两次输入的密码相同",
status: "error",
duration: 3000,
isClosable: true,
});
return;
}
if (!formData.agreeToTerms) {
toast({
title: "请同意条款",
description: "请阅读并同意用户协议和隐私政策",
status: "error",
duration: 3000,
isClosable: true,
});
return;
}
setIsLoading(true);
// 模拟注册过程
setTimeout(() => {
setIsLoading(false);
toast({
title: "注册成功",
description: "欢迎加入价值前沿投资助手",
status: "success",
duration: 3000,
isClosable: true,
});
navigate("/home");
}, 1500);
};
return (
<Flex minH="100vh" align="center" justify="center" bg={useColorModeValue("gray.50", "gray.900")}>
<Stack spacing={8} mx="auto" maxW="lg" py={12} px={6}>
<Stack align="center">
<Heading fontSize="4xl" color="blue.600">
价小前投研
</Heading>
<Text fontSize="lg" color="gray.600">
创建您的账户
</Text>
</Stack>
<Box
rounded="lg"
bg={useColorModeValue("white", "gray.700")}
boxShadow="lg"
p={8}
>
<form onSubmit={handleSubmit}>
<Stack spacing={4}>
<FormControl id="username" isRequired>
<FormLabel>用户名</FormLabel>
<Input
type="text"
name="username"
value={formData.username}
onChange={handleInputChange}
placeholder="请输入您的用户名"
/>
</FormControl>
<FormControl id="email" isRequired>
<FormLabel>邮箱地址</FormLabel>
<Input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="请输入您的邮箱"
/>
</FormControl>
<FormControl id="password" isRequired>
<FormLabel>密码</FormLabel>
<InputGroup>
<Input
type={showPassword ? "text" : "password"}
name="password"
value={formData.password}
onChange={handleInputChange}
placeholder="请输入您的密码"
/>
<InputRightElement>
<IconButton
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
variant="ghost"
size="sm"
/>
</InputRightElement>
</InputGroup>
</FormControl>
<FormControl id="confirmPassword" isRequired>
<FormLabel>确认密码</FormLabel>
<InputGroup>
<Input
type={showConfirmPassword ? "text" : "password"}
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder="请再次输入您的密码"
/>
<InputRightElement>
<IconButton
aria-label={showConfirmPassword ? "隐藏密码" : "显示密码"}
icon={showConfirmPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
variant="ghost"
size="sm"
/>
</InputRightElement>
</InputGroup>
</FormControl>
<FormControl id="agreeToTerms">
<Checkbox
name="agreeToTerms"
isChecked={formData.agreeToTerms}
onChange={handleInputChange}
colorScheme="blue"
>
<Text fontSize="sm">
我已阅读并同意{" "}
<Link color="blue.600" href="#" isExternal>
用户协议
</Link>{" "}
{" "}
<Link color="blue.600" href="#" isExternal>
隐私政策
</Link>
</Text>
</Checkbox>
</FormControl>
<Stack spacing={10}>
<Button
type="submit"
bg="blue.600"
color="white"
_hover={{
bg: "blue.700",
}}
isLoading={isLoading}
loadingText="注册中..."
>
注册
</Button>
</Stack>
<Stack pt={6}>
<Text align="center">
已有账户?{" "}
<Link color="blue.600" onClick={() => navigate("/auth/signin")}>
立即登录
</Link>
</Text>
</Stack>
</Stack>
</form>
</Box>
</Stack>
</Flex>
);
}

View File

@@ -0,0 +1,282 @@
import React, { useState } from "react";
import {
Box,
Button,
FormControl,
FormLabel,
Input,
VStack,
Heading,
Text,
Link,
useColorMode,
InputGroup,
InputRightElement,
IconButton,
Spinner,
Checkbox,
HStack,
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../../contexts/AuthContext";
export default function SignUpCentered() {
const { colorMode } = useColorMode();
const navigate = useNavigate();
const { register, isLoading } = useAuth();
// 表单状态
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
confirmPassword: "",
});
// UI状态
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [errors, setErrors] = useState({});
const [agreedToTerms, setAgreedToTerms] = useState(false);
// 处理输入变化
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
// 清除对应字段的错误
if (errors[name]) {
setErrors(prev => ({
...prev,
[name]: ""
}));
}
};
// 表单验证
const validateForm = () => {
const newErrors = {};
if (!formData.name.trim()) {
newErrors.name = "姓名是必填项";
} else if (formData.name.trim().length < 2) {
newErrors.name = "姓名至少需要2个字符";
}
if (!formData.email) {
newErrors.email = "邮箱是必填项";
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = "请输入有效的邮箱地址";
}
if (!formData.password) {
newErrors.password = "密码是必填项";
} else if (formData.password.length < 6) {
newErrors.password = "密码至少需要6个字符";
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.password)) {
newErrors.password = "密码必须包含大小写字母和数字";
}
if (!formData.confirmPassword) {
newErrors.confirmPassword = "请确认密码";
} else if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "两次输入的密码不一致";
}
if (!agreedToTerms) {
newErrors.terms = "请同意服务条款和隐私政策";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理表单提交
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) {
return;
}
const result = await register(
formData.name, // username
formData.email,
formData.password
);
if (result.success) {
// 注册成功,跳转到首页
navigate("/home");
}
};
return (
<Box
minH="100vh"
display="flex"
alignItems="center"
justifyContent="center"
bg={colorMode === "dark" ? "gray.800" : "gray.50"}
p={4}
>
<Box
w="full"
maxW="md"
p={8}
bg={colorMode === "dark" ? "gray.700" : "white"}
borderRadius="lg"
shadow="xl"
>
<VStack spacing={6}>
<Box textAlign="center">
<Heading size="lg" mb={2}>创建账户</Heading>
<Text color="gray.500">加入价值前沿开启智能投资之旅</Text>
</Box>
<form onSubmit={handleSubmit} style={{ width: "100%" }}>
<VStack spacing={4}>
<FormControl isInvalid={!!errors.name}>
<FormLabel>姓名</FormLabel>
<Input
name="name"
placeholder="您的姓名"
value={formData.name}
onChange={handleInputChange}
size="lg"
/>
{errors.name && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.name}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.email}>
<FormLabel>邮箱地址</FormLabel>
<Input
name="email"
type="email"
placeholder="your@email.com"
value={formData.email}
onChange={handleInputChange}
size="lg"
/>
{errors.email && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.email}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.password}>
<FormLabel>密码</FormLabel>
<InputGroup size="lg">
<Input
name="password"
type={showPassword ? "text" : "password"}
placeholder="********"
value={formData.password}
onChange={handleInputChange}
/>
<InputRightElement>
<IconButton
aria-label={showPassword ? "隐藏密码" : "显示密码"}
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
variant="ghost"
onClick={() => setShowPassword(!showPassword)}
/>
</InputRightElement>
</InputGroup>
{errors.password && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.password}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.confirmPassword}>
<FormLabel>确认密码</FormLabel>
<InputGroup size="lg">
<Input
name="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
placeholder="********"
value={formData.confirmPassword}
onChange={handleInputChange}
/>
<InputRightElement>
<IconButton
aria-label={showConfirmPassword ? "隐藏密码" : "显示密码"}
icon={showConfirmPassword ? <ViewOffIcon /> : <ViewIcon />}
variant="ghost"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
/>
</InputRightElement>
</InputGroup>
{errors.confirmPassword && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.confirmPassword}
</Text>
)}
</FormControl>
<FormControl isInvalid={!!errors.terms}>
<HStack spacing={3}>
<Checkbox
isChecked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
colorScheme="blue"
>
<Text fontSize="sm">
我同意{" "}
<Link color="blue.500" _hover={{ textDecoration: "underline" }}>
服务条款
</Link>
{" "}{" "}
<Link color="blue.500" _hover={{ textDecoration: "underline" }}>
隐私政策
</Link>
</Text>
</Checkbox>
</HStack>
{errors.terms && (
<Text color="red.500" fontSize="sm" mt={1}>
{errors.terms}
</Text>
)}
</FormControl>
<Button
type="submit"
colorScheme="blue"
w="full"
size="lg"
isLoading={isLoading}
loadingText="注册中..."
>
{isLoading ? <Spinner size="sm" /> : "创建账户"}
</Button>
</VStack>
</form>
<VStack spacing={3}>
<Text fontSize="sm" textAlign="center">
已有账户{" "}
<Link
color="blue.500"
onClick={() => navigate("/auth/signin")}
_hover={{ textDecoration: "underline" }}
>
立即登录
</Link>
</Text>
</VStack>
</VStack>
</Box>
</Box>
);
}

View File

@@ -0,0 +1,234 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Button,
Flex,
FormControl,
FormLabel,
HStack,
Icon,
Input,
Link,
Switch,
Text,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import CoverImage from "assets/img/CoverImage.png";
import React from "react";
import { FaApple, FaFacebook, FaGoogle } from "react-icons/fa";
import AuthCover from "layouts/AuthCover";
function SignUpCover() {
// Chakra color mode
const textColor = useColorModeValue("gray.400", "white");
const bgForm = useColorModeValue("white", "navy.800");
const titleColor = useColorModeValue("gray.700", "blue.500");
const colorIcons = useColorModeValue("gray.700", "white");
const bgIcons = useColorModeValue("trasnparent", "navy.700");
const bgIconsHover = useColorModeValue("gray.50", "whiteAlpha.100");
return (
<AuthCover image={CoverImage}>
<Flex
w="100%"
h="100%"
alignItems="center"
justifyContent="center"
mb="60px"
mt={{ base: "60px", md: "160px" }}
>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "100px" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
>
<Text
fontSize="xl"
color={textColor}
fontWeight="bold"
textAlign="center"
mb="22px"
>
Sign In with
</Text>
<HStack spacing="15px" justify="center" mb="22px">
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon as={FaFacebook} color={colorIcons} w="30px" h="30px" />
</Link>
</Flex>
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon
as={FaApple}
color={colorIcons}
w="30px"
h="30px"
_hover={{ filter: "brightness(120%)" }}
/>
</Link>
</Flex>
<Flex
justify="center"
align="center"
w="75px"
h="75px"
borderRadius="8px"
border={useColorModeValue("1px solid", "0px")}
borderColor="gray.200"
cursor="pointer"
transition="all .25s ease"
bg={bgIcons}
_hover={{ bg: bgIconsHover }}
>
<Link href="#">
<Icon
as={FaGoogle}
color={colorIcons}
w="30px"
h="30px"
_hover={{ filter: "brightness(120%)" }}
/>
</Link>
</Flex>
</HStack>
<Text
fontSize="lg"
color="gray.400"
fontWeight="bold"
textAlign="center"
mb="22px"
>
or
</Text>
<FormControl>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Name
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="text"
placeholder="Your full name"
mb="24px"
size="lg"
/>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Email
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="email"
placeholder="Your full email adress"
mb="24px"
size="lg"
/>
<FormLabel ms="4px" fontSize="sm" fontWeight="normal">
Password
</FormLabel>
<Input
variant="auth"
fontSize="sm"
ms="4px"
type="password"
placeholder="Your password"
mb="24px"
size="lg"
/>
<FormControl display="flex" alignItems="center" mb="24px">
<Switch id="remember-login" colorScheme="blue" me="10px" />
<FormLabel htmlFor="remember-login" mb="0" fontWeight="normal">
Remember me
</FormLabel>
</FormControl>
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
SIGN IN
</Button>
</FormControl>
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
maxW="100%"
mt="0px"
>
<Text color={textColor} fontWeight="medium">
Dont have an account?
<Link
color={titleColor}
as="span"
ms="5px"
href="#"
fontWeight="bold"
>
Sign up
</Link>
</Text>
</Flex>
</Flex>
</Flex>
</AuthCover>
);
}
export default SignUpCover;

View File

@@ -0,0 +1,891 @@
// src\views\Authentication\SignUp/SignUpIllustration.js
import React, { useState, useEffect } from "react";
import {
Box,
Button,
Flex,
FormControl,
Input,
Stack,
Text,
Heading,
VStack,
HStack,
useToast,
Icon,
InputGroup,
InputRightElement,
IconButton,
Tab,
TabList,
Tabs,
Link as ChakraLink,
Image,
Center,
Spinner,
FormLabel,
FormErrorMessage,
Divider,
useDisclosure,
Checkbox
} from "@chakra-ui/react";
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
import { FaWeixin, FaMobile, FaEnvelope, FaUser, FaLock } from "react-icons/fa";
import { useNavigate, Link } from "react-router-dom";
import axios from "axios";
import { useAuth } from '../../../contexts/AuthContext'; // 假设AuthContext在这个路径
import PrivacyPolicyModal from '../../../components/PrivacyPolicyModal';
import UserAgreementModal from '../../../components/UserAgreementModal';
const isProduction = process.env.NODE_ENV === 'production';
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL;
export default function SignUpPage() {
const [registerType, setRegisterType] = useState(0); // 0: 微信, 1: 手机号
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [countdown, setCountdown] = useState(0);
const [wechatAuthUrl, setWechatAuthUrl] = useState("");
const [wechatSessionId, setWechatSessionId] = useState("");
const [wechatStatus, setWechatStatus] = useState("waiting");
const [errors, setErrors] = useState({});
const [checkInterval, setCheckInterval] = useState(null);
const [agreeToTerms, setAgreeToTerms] = useState(false);
const [formData, setFormData] = useState({
username: "",
email: "",
phone: "",
password: "",
confirmPassword: "",
verificationCode: ""
});
// 隐私政策弹窗状态
const {
isOpen: isPrivacyModalOpen,
onOpen: onPrivacyModalOpen,
onClose: onPrivacyModalClose
} = useDisclosure();
// 用户协议弹窗状态
const {
isOpen: isUserAgreementModalOpen,
onOpen: onUserAgreementModalOpen,
onClose: onUserAgreementModalClose
} = useDisclosure();
const navigate = useNavigate();
const toast = useToast();
const { loginWithWechat } = useAuth(); // 使用认证上下文
// 监听微信授权窗口的消息
useEffect(() => {
const handleMessage = (event) => {
console.log('收到消息:', event.data, 'from:', event.origin);
// 放宽来源验证,包含所有可能的域名
const allowedOrigins = [
window.location.origin,
'https://valuefrontier.cn',
'http://localhost:3000', // 开发环境
'http://127.0.0.1:3000' // 本地开发
];
if (!allowedOrigins.includes(event.origin)) {
console.warn('消息来源不受信任:', event.origin);
// 但仍然处理,因为可能是跨域问题
}
if (event.data && event.data.type === 'wechat_auth_success') {
console.log('收到微信授权成功消息');
// 授权成功,立即检查状态
if (wechatSessionId) {
console.log('开始检查微信状态, sessionId:', wechatSessionId);
checkWechatStatus();
} else {
console.error('wechatSessionId 为空');
}
}
};
window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, [wechatSessionId]);
// 获取微信授权URL
const getWechatQRCode = async () => {
try {
setIsLoading(true);
const response = await axios.get(`${API_BASE_URL}/api/auth/wechat/qrcode`);
setWechatAuthUrl(response.data.auth_url);
setWechatSessionId(response.data.session_id);
setWechatStatus("waiting");
// 开始轮询检查扫码状态
startWechatStatusCheck(response.data.session_id);
} catch (error) {
toast({
title: "获取授权失败",
description: error.response?.data?.error || "请稍后重试",
status: "error",
duration: 3000,
});
} finally {
setIsLoading(false);
}
};
// 立即检查微信状态(用于授权成功后)
const checkWechatStatus = async () => {
try {
console.log('检查微信状态, sessionId:', wechatSessionId);
const response = await axios.post(`${API_BASE_URL}/api/auth/wechat/check`, {
session_id: wechatSessionId
});
const { status, user_info } = response.data;
console.log('微信状态检查结果:', { status, user_info });
setWechatStatus(status);
if (status === "login_success" || status === "register_success") {
// 停止轮询
if (checkInterval) {
clearInterval(checkInterval);
setCheckInterval(null);
}
console.log('开始微信登录流程');
// 调用登录接口获取token
try {
let loginResult;
// 如果有 loginWithWechat 方法,使用它
if (loginWithWechat) {
console.log('使用 AuthContext 登录');
loginResult = await loginWithWechat(wechatSessionId);
} else {
console.log('使用传统登录方式');
// 否则使用原来的方式
const loginResponse = await axios.post(`${API_BASE_URL}/api/auth/login/wechat`, {
session_id: wechatSessionId
});
console.log('登录响应:', loginResponse.data);
// 保存登录信息(兼容旧方式)
if (loginResponse.data.token) {
localStorage.setItem('token', loginResponse.data.token);
}
if (loginResponse.data.user) {
localStorage.setItem('user', JSON.stringify(loginResponse.data.user));
}
loginResult = { success: true };
}
console.log('登录结果:', loginResult);
if (loginResult.success) {
toast({
title: status === "login_success" ? "登录成功" : "注册成功",
description: "正在跳转...",
status: "success",
duration: 2000,
});
// 跳转到首页
console.log('准备跳转到 /home');
setTimeout(() => {
navigate("/home"); // 修改为跳转到 /home
}, 1000);
} else {
throw new Error('登录失败');
}
} catch (loginError) {
console.error('登录失败:', loginError);
toast({
title: "登录失败",
description: loginError.response?.data?.error || loginError.message || "请重试",
status: "error",
duration: 3000,
});
}
}
} catch (error) {
console.error("检查微信状态失败:", error);
}
};
// 增加备用的状态检查机制
useEffect(() => {
if (registerType === 0 && wechatSessionId) {
// 每隔3秒检查一次状态备用机制
const backupCheck = setInterval(() => {
if (wechatStatus === "waiting") {
console.log('备用检查机制:检查微信状态');
checkWechatStatus();
}
}, 3000);
return () => clearInterval(backupCheck);
}
}, [registerType, wechatSessionId, wechatStatus]);
// 开始检查微信扫码状态
const startWechatStatusCheck = (sessionId) => {
// 清除之前的轮询
if (checkInterval) {
clearInterval(checkInterval);
}
const interval = setInterval(async () => {
try {
const response = await axios.post(`${API_BASE_URL}/api/auth/wechat/check`, {
session_id: sessionId
});
const { status, user_info } = response.data;
setWechatStatus(status);
if (status === "login_success" || status === "register_success") {
// 成功,停止轮询
clearInterval(interval);
setCheckInterval(null);
// 调用登录接口
let loginResult;
if (loginWithWechat) {
loginResult = await loginWithWechat(sessionId);
} else {
const loginResponse = await axios.post(`${API_BASE_URL}/api/auth/login/wechat`, {
session_id: sessionId
});
// 保存登录信息
if (loginResponse.data.token) {
localStorage.setItem('token', loginResponse.data.token);
}
if (loginResponse.data.user) {
localStorage.setItem('user', JSON.stringify(loginResponse.data.user));
}
loginResult = { success: true };
}
if (loginResult.success) {
toast({
title: status === "login_success" ? "登录成功" : "注册成功",
description: "正在跳转...",
status: "success",
duration: 2000,
});
setTimeout(() => {
navigate("/home"); // 跳转到首页
}, 1000);
}
} else if (status === "expired") {
clearInterval(interval);
setCheckInterval(null);
setWechatStatus("expired");
toast({
title: "授权已过期",
description: "请重新获取授权",
status: "warning",
duration: 3000,
});
}
} catch (error) {
console.error("检查微信状态失败:", error);
// 继续轮询,不中断
}
}, 2000); // 每2秒检查一次
setCheckInterval(interval);
// 5分钟后停止轮询
setTimeout(() => {
if (interval) {
clearInterval(interval);
setCheckInterval(null);
if (wechatStatus === "waiting") {
setWechatStatus("expired");
}
}
}, 300000);
};
// 组件卸载时清除轮询
useEffect(() => {
return () => {
if (checkInterval) {
clearInterval(checkInterval);
}
};
}, [checkInterval]);
// 初始化时如果选择了微信登录获取授权URL
useEffect(() => {
if (registerType === 0 && agreeToTerms) {
getWechatQRCode();
}
}, [registerType, agreeToTerms]);
// 发送验证码
const sendVerificationCode = async () => {
const contact = formData.phone;
const endpoint = "send-sms-code";
const fieldName = "phone";
if (!contact) {
toast({
title: "请输入手机号",
status: "warning",
duration: 2000,
});
return;
}
if (!/^1[3-9]\d{9}$/.test(contact)) {
toast({
title: "请输入正确的手机号",
status: "warning",
duration: 2000,
});
return;
}
try {
setIsLoading(true);
await axios.post(`${API_BASE_URL}/api/auth/${endpoint}`, {
[fieldName]: contact
});
toast({
title: "验证码已发送",
description: "请查收短信",
status: "success",
duration: 3000,
});
setCountdown(60);
} catch (error) {
toast({
title: "发送失败",
description: error.response?.data?.error || "请稍后重试",
status: "error",
duration: 3000,
});
} finally {
setIsLoading(false);
}
};
// 倒计时效果
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
// 表单验证
const validateForm = () => {
const newErrors = {};
if (!formData.username || formData.username.length < 3) {
newErrors.username = "用户名至少3个字符";
}
if (!/^[a-zA-Z0-9_]{3,20}$/.test(formData.username)) {
newErrors.username = "用户名只能包含字母、数字和下划线3-20个字符";
}
if (!formData.password || formData.password.length < 6) {
newErrors.password = "密码至少6个字符";
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = "两次密码不一致";
}
if (registerType === 1) {
if (!formData.phone) {
newErrors.phone = "请输入手机号";
} else if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
newErrors.phone = "请输入正确的手机号";
}
if (!formData.verificationCode) {
newErrors.verificationCode = "请输入验证码";
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// 处理注册提交
const handleSubmit = async (e) => {
e.preventDefault();
if (!agreeToTerms) {
toast({
title: "请先同意协议",
description: "请勾选同意用户协议和隐私政策后再注册",
status: "warning",
duration: 3000,
isClosable: true,
});
return;
}
if (!validateForm()) {
return;
}
setIsLoading(true);
try {
const endpoint = "/api/auth/register/phone";
const data = {
phone: formData.phone,
code: formData.verificationCode,
username: formData.username,
password: formData.password
};
await axios.post(`${API_BASE_URL}${endpoint}`, data);
toast({
title: "注册成功",
description: "即将跳转到登录页面",
status: "success",
duration: 2000,
});
setTimeout(() => {
navigate("/auth/sign-in");
}, 2000);
} catch (error) {
toast({
title: "注册失败",
description: error.response?.data?.error || "请稍后重试",
status: "error",
duration: 3000,
});
} finally {
setIsLoading(false);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: "" }));
}
};
// 切换注册方式时的处理
const handleRegisterTypeChange = (newType) => {
if (checkInterval) {
clearInterval(checkInterval);
setCheckInterval(null);
}
setRegisterType(newType);
setErrors({});
setAgreeToTerms(false); // 切换注册方式时重置协议同意状态
setFormData({
username: "",
email: "",
phone: "",
password: "",
confirmPassword: "",
verificationCode: ""
});
if (newType === 0) {
setWechatStatus("waiting");
setWechatAuthUrl("");
setWechatSessionId("");
// 不自动获取二维码,等用户同意协议后再获取
}
};
const getWechatStatusText = () => {
switch (wechatStatus) {
case "waiting": return "请使用微信扫描二维码";
case "login_success": return "✓ 登录成功,正在跳转...";
case "register_success": return "✓ 注册成功,正在跳转...";
case "expired": return "授权已过期";
default: return "请使用微信扫描二维码";
}
};
const getWechatStatusColor = () => {
switch (wechatStatus) {
case "login_success":
case "register_success": return "green.600";
case "expired": return "red.600";
default: return "gray.600";
}
};
// 公用的用户名和密码输入框组件
const commonAuthFields = (
<>
<FormControl isRequired isInvalid={!!errors.username}>
<FormLabel fontSize="sm">用户名</FormLabel>
<InputGroup>
<InputRightElement pointerEvents="none">
<Icon as={FaUser} color="gray.400" />
</InputRightElement>
<Input
name="username"
value={formData.username}
onChange={handleInputChange}
placeholder="设置用户名3-20个字符"
pr="2.5rem"
/>
</InputGroup>
<FormErrorMessage>{errors.username}</FormErrorMessage>
</FormControl>
<FormControl isRequired isInvalid={!!errors.password}>
<FormLabel fontSize="sm">密码</FormLabel>
<InputGroup>
<Input
name="password"
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={handleInputChange}
placeholder="设置密码至少6个字符"
pr="3rem"
/>
<InputRightElement width="3rem">
<IconButton
size="sm"
variant="ghost"
icon={showPassword ? <ViewOffIcon /> : <ViewIcon />}
onClick={() => setShowPassword(!showPassword)}
aria-label={showPassword ? "Hide password" : "Show password"}
/>
</InputRightElement>
</InputGroup>
<FormErrorMessage>{errors.password}</FormErrorMessage>
</FormControl>
<FormControl isRequired isInvalid={!!errors.confirmPassword}>
<FormLabel fontSize="sm">确认密码</FormLabel>
<Input
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleInputChange}
placeholder="再次输入密码"
/>
<FormErrorMessage>{errors.confirmPassword}</FormErrorMessage>
</FormControl>
</>
);
return (
<Flex minH="100vh" position="relative" overflow="hidden">
{/* 背景 */}
<Box
position="absolute" top={0} left={0} right={0} bottom={0} zIndex={0}
background={`linear-gradient(45deg, rgba(139, 69, 19, 0.9) 0%, rgba(160, 82, 45, 0.8) 15%, rgba(205, 133, 63, 0.7) 30%, rgba(222, 184, 135, 0.8) 45%, rgba(245, 222, 179, 0.6) 60%, rgba(255, 228, 196, 0.7) 75%, rgba(139, 69, 19, 0.8) 100%)`}
_before={{ content: '""', position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, background: `conic-gradient(from 0deg at 30% 20%, rgba(255, 140, 0, 0.6) 0deg, rgba(255, 69, 0, 0.4) 60deg, rgba(139, 69, 19, 0.5) 120deg, rgba(160, 82, 45, 0.6) 180deg, rgba(205, 133, 63, 0.4) 240deg, rgba(255, 140, 0, 0.5) 300deg, rgba(255, 140, 0, 0.6) 360deg)`, mixBlendMode: 'multiply', animation: 'fluid-rotate 20s linear infinite' }}
_after={{ content: '""', position: 'absolute', top: '10%', left: '20%', width: '60%', height: '80%', borderRadius: '50%', background: 'radial-gradient(ellipse at center, rgba(255, 165, 0, 0.3) 0%, rgba(255, 140, 0, 0.2) 50%, transparent 70%)', filter: 'blur(40px)', animation: 'wave-pulse 8s ease-in-out infinite' }}
sx={{ '@keyframes fluid-rotate': { '0%': { transform: 'rotate(0deg) scale(1)' }, '50%': { transform: 'rotate(180deg) scale(1.1)' }, '100%': { transform: 'rotate(360deg) scale(1)' } }, '@keyframes wave-pulse': { '0%, 100%': { opacity: 0.4, transform: 'scale(1)' }, '50%': { opacity: 0.8, transform: 'scale(1.2)' } } }}
/>
{/* 主要内容 */}
<Flex width="100%" align="center" justify="center" position="relative" zIndex={1} px={6} py={12}>
<Box bg="white" borderRadius="2xl" boxShadow="2xl" p={8} width="100%" maxW="480px" backdropFilter="blur(20px)" border="1px solid rgba(255, 255, 255, 0.2)">
<VStack spacing={6} mb={8}>
<VStack spacing={2}>
<Heading size="xl" color="gray.800" fontWeight="bold">创建账户</Heading>
<Text color="gray.600" fontSize="md">加入价值前沿开启投资新征程</Text>
</VStack>
<Box width="100%">
<Tabs index={registerType} onChange={handleRegisterTypeChange} variant="soft-rounded" colorScheme="orange" isFitted>
<TabList bg="gray.100" borderRadius="xl" p={1}>
<Tab borderRadius="lg" _selected={{ bg: "linear-gradient(135deg, #ff8c00 0%, #ff6347 100%)", color: "white", transform: "scale(1.02)" }} transition="all 0.2s">
<Icon as={FaWeixin} mr={2} /><Text fontSize="sm">微信扫码</Text>
</Tab>
<Tab borderRadius="lg" _selected={{ bg: "linear-gradient(135deg, #ff8c00 0%, #ff6347 100%)", color: "white", transform: "scale(1.02)" }} transition="all 0.2s">
<Icon as={FaMobile} mr={2} /><Text fontSize="sm">手机号</Text>
</Tab>
</TabList>
</Tabs>
</Box>
</VStack>
<form onSubmit={handleSubmit}>
<VStack spacing={4}>
{/* 微信注册 */}
{registerType === 0 && (
<>
{/* 协议同意勾选框 - 微信注册 */}
<Box width="100%" mb={4}>
<Checkbox
isChecked={agreeToTerms}
onChange={(e) => setAgreeToTerms(e.target.checked)}
colorScheme="orange"
size="sm"
>
<Text fontSize="sm" color="gray.600">
我已阅读并同意{" "}
<ChakraLink
color="orange.500"
fontSize="sm"
onClick={onUserAgreementModalOpen}
textDecoration="underline"
_hover={{ color: "orange.600" }}
>
用户协议
</ChakraLink>
{" "}{" "}
<ChakraLink
color="orange.500"
fontSize="sm"
onClick={onPrivacyModalOpen}
textDecoration="underline"
_hover={{ color: "orange.600" }}
>
隐私政策
</ChakraLink>
</Text>
</Checkbox>
{!agreeToTerms && (
<Text fontSize="xs" color="red.500" mt={1} ml={6}>
请先同意用户协议和隐私政策
</Text>
)}
</Box>
<Center width="100%" height="420px" bg="gray.50" borderRadius="lg" p={4} mb={4}>
{agreeToTerms ? (
wechatAuthUrl && wechatStatus !== "expired" ? (
<Box position="relative" width="100%" height="100%">
<iframe
src={wechatAuthUrl}
width="100%"
height="100%"
frameBorder="0"
scrolling="no"
style={{ borderRadius: '8px' }}
/>
{(wechatStatus === "login_success" || wechatStatus === "register_success") && (
<Box position="absolute" top={0} left={0} right={0} bottom={0}
bg="rgba(0,0,0,0.7)" display="flex" alignItems="center"
justifyContent="center" borderRadius="lg">
<VStack>
<Spinner color="white" />
<Text color="white" fontWeight="bold">
{wechatStatus === "login_success" ? "正在登录..." : "正在创建账号..."}
</Text>
</VStack>
</Box>
)}
</Box>
) : wechatStatus === "expired" ? (
<VStack>
<Text color="gray.500" fontWeight="bold" mb={4}>授权已过期</Text>
<Button colorScheme="orange" size="sm" onClick={getWechatQRCode}
isLoading={isLoading}>重新获取</Button>
</VStack>
) : (
<VStack>
<Spinner size="xl" color="orange.500" />
<Text color="gray.500" fontSize="sm">加载中...</Text>
</VStack>
)
) : (
<VStack spacing={4}>
<Icon as={FaWeixin} w={20} h={20} color="gray.400" />
<VStack spacing={2}>
<Text fontSize="lg" fontWeight="bold" color="gray.400">
微信扫码注册
</Text>
<Text fontSize="sm" color="gray.400" textAlign="center">
请先同意用户协议和隐私政策
</Text>
</VStack>
<Button
colorScheme="orange"
variant="outline"
size="lg"
isDisabled
opacity={0.6}
>
同意协议后显示二维码
</Button>
</VStack>
)}
</Center>
<Text textAlign="center" color={getWechatStatusColor()} fontSize="sm"
fontWeight={wechatStatus === "login_success" || wechatStatus === "register_success" ? "bold" : "normal"}>
{agreeToTerms ? getWechatStatusText() : "请先同意用户协议和隐私政策"}
</Text>
<Text fontSize="xs" color="gray.500" textAlign="center">
{agreeToTerms
? "扫码即表示同意创建账号,系统将使用您的微信昵称作为初始用户名"
: "同意协议后即可使用微信快速注册"}
</Text>
</>
)}
{/* 手机号注册 */}
{registerType === 1 && (
<>
<FormControl isRequired isInvalid={!!errors.phone}>
<FormLabel fontSize="sm">手机号</FormLabel>
<InputGroup>
<InputRightElement pointerEvents="none"><Icon as={FaMobile} color="gray.400" /></InputRightElement>
<Input name="phone" value={formData.phone} onChange={handleInputChange} placeholder="请输入11位手机号" pr="2.5rem" />
</InputGroup>
<FormErrorMessage>{errors.phone}</FormErrorMessage>
</FormControl>
<FormControl isRequired isInvalid={!!errors.verificationCode}>
<FormLabel fontSize="sm">验证码</FormLabel>
<HStack>
<Input name="verificationCode" value={formData.verificationCode} onChange={handleInputChange} placeholder="请输入6位验证码" />
<Button colorScheme="orange" onClick={sendVerificationCode} isDisabled={countdown > 0 || isLoading} isLoading={isLoading && countdown === 0} minW="120px">
{countdown > 0 ? `${countdown}秒后重试` : "获取验证码"}
</Button>
</HStack>
<FormErrorMessage>{errors.verificationCode}</FormErrorMessage>
</FormControl>
<Divider my={2} />
{commonAuthFields}
{/* 协议同意勾选框 - 手机号注册 */}
<Box width="100%" py={3}>
<Checkbox
isChecked={agreeToTerms}
onChange={(e) => setAgreeToTerms(e.target.checked)}
colorScheme="orange"
size="sm"
>
<Text fontSize="sm" color="gray.600">
我已阅读并同意{" "}
<ChakraLink
color="orange.500"
fontSize="sm"
onClick={onUserAgreementModalOpen}
textDecoration="underline"
_hover={{ color: "orange.600" }}
>
用户协议
</ChakraLink>
{" "}{" "}
<ChakraLink
color="orange.500"
fontSize="sm"
onClick={onPrivacyModalOpen}
textDecoration="underline"
_hover={{ color: "orange.600" }}
>
隐私政策
</ChakraLink>
</Text>
</Checkbox>
{!agreeToTerms && (
<Text fontSize="xs" color="red.500" mt={1} ml={6}>
请先同意用户协议和隐私政策
</Text>
)}
</Box>
</>
)}
{registerType !== 0 && (
<Button
type="submit"
width="100%"
size="lg"
background={agreeToTerms
? "linear-gradient(135deg, #ff8c00 0%, #ff6347 100%)"
: "gray.300"
}
color="white"
borderRadius="lg"
_hover={agreeToTerms ? {
transform: "translateY(-2px)",
boxShadow: "lg"
} : {}}
_active={agreeToTerms ? {
transform: "translateY(0)"
} : {}}
isLoading={isLoading}
loadingText="注册中..."
fontWeight="bold"
isDisabled={!agreeToTerms}
cursor={agreeToTerms ? "pointer" : "not-allowed"}
>
完成注册
</Button>
)}
<Text fontSize="sm" color="gray.600" textAlign="center">
已有账号{" "}
<ChakraLink as={Link} to="/auth/sign-in" color="orange.500" fontWeight="bold">
立即登录
</ChakraLink>
</Text>
</VStack>
</form>
</Box>
</Flex>
<Box position="absolute" bottom={8} left="50%" transform="translateX(-50%)" zIndex={1}>
<HStack spacing={6}>
<ChakraLink
color="white"
fontSize="sm"
opacity={0.8}
_hover={{ opacity: 1 }}
onClick={onUserAgreementModalOpen}
>
用户协议
</ChakraLink>
<ChakraLink
color="white"
fontSize="sm"
opacity={0.8}
_hover={{ opacity: 1 }}
onClick={onPrivacyModalOpen}
>
隐私政策
</ChakraLink>
</HStack>
</Box>
{/* 隐私政策弹窗 */}
<PrivacyPolicyModal
isOpen={isPrivacyModalOpen}
onClose={onPrivacyModalClose}
/>
{/* 用户协议弹窗 */}
<UserAgreementModal
isOpen={isUserAgreementModalOpen}
onClose={onUserAgreementModalClose}
/>
</Flex>
);
}

View File

@@ -0,0 +1,116 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Button,
Link,
Flex,
FormControl,
Text,
Icon,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import BasicImage from "assets/img/BasicImage.png";
import React from "react";
import AuthBasic from "layouts/AuthBasic";
import { PinInputLight } from "components/PinInput/PinInput";
import { IoIosRocket } from "react-icons/io";
function LockBasic() {
// Chakra color mode
const textColor = useColorModeValue("gray.700", "white");
const bgForm = useColorModeValue("white", "navy.800");
return (
<AuthBasic
title="Welcome!"
description="Use these awesome forms to login or create new account in your project for free."
image={BasicImage}
>
<Flex
w="100%"
h="100%"
alignItems="center"
justifyContent="center"
mb="60px"
mt={{ base: "60px", md: "0px" }}
>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "20px", md: "100px" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
>
<Flex
mx="auto"
borderRadius="50%"
bg="blue.500"
w={{ base: "100px" }}
h={{ base: "100px" }}
justify="center"
align="center"
mb="30px"
>
<Icon as={IoIosRocket} color="white" w="36px" h="36px" />
</Flex>
<Text
fontWeight="bold"
color={textColor}
textAlign="center"
mb="10px"
fontSize={{ base: "3xl", md: "4xl" }}
>
2-Step Verification
</Text>
<FormControl>
<Flex justify="center" align="center" mx="auto" mb="30px">
<PinInputLight />
</Flex>
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
UNLOCK
</Button>
</FormControl>
<Text color="gray.400" fontWeight="400" textAlign="center">
Haven't received it?{" "}
<Link color={textColor} as="span" fontWeight="700">
Resend a new code.
</Link>
</Text>
</Flex>
</Flex>
</AuthBasic>
);
}
export default LockBasic;

View File

@@ -0,0 +1,104 @@
/*!
=========================================================
* Argon Dashboard Chakra PRO - v1.0.0
=========================================================
* Product Page: https://www.creative-tim.com/product/argon-dashboard-chakra-pro
* Copyright 2022 Creative Tim (https://www.creative-tim.com/)
* Designed and Coded by Simmmple & Creative Tim
=========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/
// Chakra imports
import {
Button,
Flex,
Link,
Icon,
FormControl,
Text,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import { PinInputLight } from "components/PinInput/PinInput";
import { IoIosRocket } from "react-icons/io";
import CoverImage from "assets/img/CoverImage.png";
import React from "react";
import AuthCover from "layouts/AuthCover";
function LockCover() {
// Chakra color mode
const textColor = useColorModeValue("gray.700", "white");
const bgForm = useColorModeValue("white", "navy.800");
return (
<AuthCover image={CoverImage}>
<Flex
zIndex="2"
direction="column"
w="445px"
background="transparent"
borderRadius="15px"
p="40px"
mx={{ base: "20px", md: "auto" }}
mb={{ base: "20px", md: "auto" }}
bg={bgForm}
boxShadow={useColorModeValue(
"0px 5px 14px rgba(0, 0, 0, 0.05)",
"unset"
)}
mt="25vh"
>
<Flex
mx="auto"
borderRadius="50%"
bg="blue.500"
w={{ base: "100px" }}
h={{ base: "100px" }}
justify="center"
align="center"
mb="30px"
>
<Icon as={IoIosRocket} color="white" w="36px" h="36px" />
</Flex>
<Text
fontWeight="bold"
color={textColor}
textAlign="center"
mb="10px"
fontSize={{ base: "3xl", md: "4xl" }}
>
2-Step Verification
</Text>
<FormControl>
<Flex justify="center" align="center" mx="auto" mb="30px">
<PinInputLight />
</Flex>
<Button
fontSize="10px"
variant="dark"
fontWeight="bold"
w="100%"
h="45"
mb="24px"
>
UNLOCK
</Button>
</FormControl>
<Text color="gray.400" fontWeight="400" textAlign="center">
Haven't received it?{" "}
<Link color={textColor} as="span" fontWeight="700">
Resend a new code.
</Link>
</Text>
</Flex>
</AuthCover>
);
}
export default LockCover;

View File

@@ -0,0 +1,93 @@
import React from "react";
// Chakra imports
import {
Button,
Flex,
FormControl,
Link,
Text,
Icon,
useColorModeValue,
} from "@chakra-ui/react";
// Assets
import illustration from "assets/img/illustration-auth.png";
import AuthIllustration from "layouts/AuthIllustration";
import { PinInputDark } from "components/PinInput/PinInput";
import { IoIosRocket } from "react-icons/io";
function LockIllustration() {
// Chakra color mode
const textColor = useColorModeValue("blue.500", "blue.500");
const inputBg = useColorModeValue(
{ background: "white !important" },
{ background: "red !important" }
);
return (
<AuthIllustration
illustrationBackground='linear-gradient(180deg, #3182CE 0%, #63B3ED 100%)'
image={illustration}>
<Flex
w='100%'
h='100%'
alignItems='start'
justifyContent='start'
mb={{ base: "0px", md: "60px" }}
mt={{ base: "60px", md: "30vh" }}>
<Flex
zIndex='2'
direction='column'
w='445px'
background='transparent'
borderRadius='15px'
pe={{ base: "0px", md: "80px" }}
mx={{ base: "20px", md: "0px" }}
mb={{ base: "20px", md: "auto" }}>
<Flex
mx={{ base: "auto", md: "0px" }}
borderRadius='50%'
bg='blue.500'
w={{ base: "100px" }}
h={{ base: "100px" }}
justify='center'
align='center'
mb='30px'>
<Icon as={IoIosRocket} color='white' w='36px' h='36px' />
</Flex>
<Text
fontWeight='bold'
color={textColor}
textAlign='start'
mb='10px'
fontSize={{ base: "3xl", md: "4xl" }}>
2-Step Verification
</Text>
<FormControl>
<Flex mx={{ base: "auto", md: "0px" }} mb='30px'>
<PinInputDark />
</Flex>
<Button
fontSize='10px'
variant='dark'
fontWeight='bold'
w='100%'
h='45'
mb='24px'>
UNLOCK
</Button>
</FormControl>
<Text
color='gray.400'
fontWeight='400'
textAlign={{ base: "center", md: "start" }}>
Haven't received it?{" "}
<Link color={textColor} as='span' fontWeight='700'>
Resend a new code.
</Link>
</Text>
</Flex>
</Flex>
</AuthIllustration>
);
}
export default LockIllustration;