feat: bugfix
This commit is contained in:
@@ -132,7 +132,6 @@
|
||||
"prettier": "2.2.1",
|
||||
"react-error-overlay": "6.0.9",
|
||||
"sharp": "^0.34.4",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"ts-node": "^10.9.2",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"yn": "^5.1.0"
|
||||
|
||||
@@ -69,9 +69,10 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
>
|
||||
高频跟踪
|
||||
</MenuButton>
|
||||
<MenuList minW="260px" p={2} onMouseEnter={onHighFreqOpen} onMouseLeave={onHighFreqClose}>
|
||||
<MenuList minW="260px" p={2} onMouseEnter={onHighFreqOpen}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onHighFreqClose(); // 先关闭菜单
|
||||
// 🎯 追踪菜单项点击
|
||||
navEvents.trackMenuItemClicked('事件中心', 'dropdown', '/community');
|
||||
navigate('/community');
|
||||
@@ -92,6 +93,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onHighFreqClose(); // 先关闭菜单
|
||||
// 🎯 追踪菜单项点击
|
||||
navEvents.trackMenuItemClicked('概念中心', 'dropdown', '/concepts');
|
||||
navigate('/concepts');
|
||||
@@ -127,9 +129,12 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
>
|
||||
行情复盘
|
||||
</MenuButton>
|
||||
<MenuList minW="260px" p={2} onMouseEnter={onMarketReviewOpen} onMouseLeave={onMarketReviewClose}>
|
||||
<MenuList minW="260px" p={2} onMouseEnter={onMarketReviewOpen}>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/limit-analyse')}
|
||||
onClick={() => {
|
||||
onMarketReviewClose(); // 先关闭菜单
|
||||
navigate('/limit-analyse');
|
||||
}}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'}
|
||||
borderLeft={location.pathname.includes('/limit-analyse') ? '3px solid' : 'none'}
|
||||
@@ -142,7 +147,10 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/stocks')}
|
||||
onClick={() => {
|
||||
onMarketReviewClose(); // 先关闭菜单
|
||||
navigate('/stocks');
|
||||
}}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'}
|
||||
borderLeft={location.pathname.includes('/stocks') ? '3px solid' : 'none'}
|
||||
@@ -155,7 +163,10 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/trading-simulation')}
|
||||
onClick={() => {
|
||||
onMarketReviewClose(); // 先关闭菜单
|
||||
navigate('/trading-simulation');
|
||||
}}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'}
|
||||
borderLeft={location.pathname.includes('/trading-simulation') ? '3px solid' : 'none'}
|
||||
@@ -181,7 +192,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
>
|
||||
AGENT社群
|
||||
</MenuButton>
|
||||
<MenuList minW="300px" p={4} onMouseEnter={onAgentCommunityOpen} onMouseLeave={onAgentCommunityClose}>
|
||||
<MenuList minW="300px" p={4} onMouseEnter={onAgentCommunityOpen}>
|
||||
<MenuItem
|
||||
isDisabled
|
||||
cursor="not-allowed"
|
||||
@@ -210,7 +221,7 @@ const DesktopNav = memo(({ isAuthenticated, user }) => {
|
||||
>
|
||||
联系我们
|
||||
</MenuButton>
|
||||
<MenuList minW="260px" p={4} onMouseEnter={onContactUsOpen} onMouseLeave={onContactUsClose}>
|
||||
<MenuList minW="260px" p={4} onMouseEnter={onContactUsOpen}>
|
||||
<Text fontSize="sm" color={contactTextColor}>敬请期待</Text>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||
@@ -52,11 +52,14 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
||||
>
|
||||
更多
|
||||
</MenuButton>
|
||||
<MenuList minW="300px" p={2} onMouseEnter={onOpen} onMouseLeave={onClose}>
|
||||
<MenuList minW="300px" p={2} onMouseEnter={onOpen}>
|
||||
{/* 高频跟踪组 */}
|
||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">高频跟踪</Text>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/community')}
|
||||
onClick={() => {
|
||||
onClose(); // 先关闭菜单
|
||||
navigate('/community');
|
||||
}}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/community') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
@@ -69,7 +72,10 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/concepts')}
|
||||
onClick={() => {
|
||||
onClose(); // 先关闭菜单
|
||||
navigate('/concepts');
|
||||
}}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/concepts') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
@@ -84,7 +90,10 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
||||
{/* 行情复盘组 */}
|
||||
<Text fontSize="xs" fontWeight="bold" px={3} py={2} color="gray.500">行情复盘</Text>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/limit-analyse')}
|
||||
onClick={() => {
|
||||
onClose(); // 先关闭菜单
|
||||
navigate('/limit-analyse');
|
||||
}}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/limit-analyse') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
@@ -94,7 +103,10 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/stocks')}
|
||||
onClick={() => {
|
||||
onClose(); // 先关闭菜单
|
||||
navigate('/stocks');
|
||||
}}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/stocks') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
@@ -104,7 +116,10 @@ const MoreMenu = memo(({ isAuthenticated, user }) => {
|
||||
</Flex>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => navigate('/trading-simulation')}
|
||||
onClick={() => {
|
||||
onClose(); // 先关闭菜单
|
||||
navigate('/trading-simulation');
|
||||
}}
|
||||
borderRadius="md"
|
||||
bg={location.pathname.includes('/trading-simulation') ? 'blue.50' : 'transparent'}
|
||||
>
|
||||
|
||||
@@ -57,7 +57,7 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => {
|
||||
>
|
||||
个人中心
|
||||
</MenuButton>
|
||||
<MenuList onMouseEnter={onOpen} onMouseLeave={onClose}>
|
||||
<MenuList onMouseEnter={onOpen}>
|
||||
{/* 用户信息区 */}
|
||||
<Box px={3} py={2} borderBottom="1px" borderColor="gray.200">
|
||||
<Text fontSize="sm" fontWeight="bold">{getDisplayName()}</Text>
|
||||
@@ -71,24 +71,36 @@ const PersonalCenterMenu = memo(({ user, handleLogout }) => {
|
||||
</Box>
|
||||
|
||||
{/* 前往个人中心 */}
|
||||
<MenuItem icon={<FiHome />} onClick={() => navigate('/home/center')}>
|
||||
<MenuItem icon={<FiHome />} onClick={() => {
|
||||
onClose(); // 先关闭菜单
|
||||
navigate('/home/center');
|
||||
}}>
|
||||
前往个人中心
|
||||
</MenuItem>
|
||||
|
||||
<MenuDivider />
|
||||
|
||||
{/* 账户管理组 */}
|
||||
<MenuItem icon={<FiUser />} onClick={() => navigate('/home/profile')}>
|
||||
<MenuItem icon={<FiUser />} onClick={() => {
|
||||
onClose(); // 先关闭菜单
|
||||
navigate('/home/profile');
|
||||
}}>
|
||||
个人资料
|
||||
</MenuItem>
|
||||
<MenuItem icon={<FiSettings />} onClick={() => navigate('/home/settings')}>
|
||||
<MenuItem icon={<FiSettings />} onClick={() => {
|
||||
onClose(); // 先关闭菜单
|
||||
navigate('/home/settings');
|
||||
}}>
|
||||
账户设置
|
||||
</MenuItem>
|
||||
|
||||
<MenuDivider />
|
||||
|
||||
{/* 功能入口组 */}
|
||||
<MenuItem icon={<FaCrown />} onClick={() => navigate('/home/pages/account/subscription')}>
|
||||
<MenuItem icon={<FaCrown />} onClick={() => {
|
||||
onClose(); // 先关闭菜单
|
||||
navigate('/home/pages/account/subscription');
|
||||
}}>
|
||||
订阅管理
|
||||
</MenuItem>
|
||||
|
||||
|
||||
@@ -25,7 +25,12 @@ async function startApp() {
|
||||
// Render the app with Router wrapper
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Router>
|
||||
<Router
|
||||
future={{
|
||||
v7_startTransition: true,
|
||||
v7_relativeSplatPath: true,
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</Router>
|
||||
</React.StrictMode>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
// src/layouts/Auth.js
|
||||
import React from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ErrorBoundary from '../components/ErrorBoundary';
|
||||
|
||||
// 导入认证相关页面
|
||||
import SignInIllustration from '../views/Authentication/SignIn/SignInIllustration';
|
||||
import SignUpIllustration from '../views/Authentication/SignUp/SignUpIllustration';
|
||||
|
||||
// 认证路由组件 - 已登录用户不能访问登录页
|
||||
const AuthRoute = ({ children }) => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
// 加载中不做跳转
|
||||
if (isLoading) {
|
||||
return children;
|
||||
}
|
||||
|
||||
// 已登录用户跳转到首页
|
||||
if (isAuthenticated) {
|
||||
// 检查是否有记录的重定向路径
|
||||
const redirectPath = localStorage.getItem('redirectPath');
|
||||
if (redirectPath && redirectPath !== '/auth/signin' && redirectPath !== '/auth/sign-up') {
|
||||
localStorage.removeItem('redirectPath');
|
||||
return <Navigate to={redirectPath} replace />;
|
||||
}
|
||||
return <Navigate to="/home" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default function Auth() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Box minH="100vh">
|
||||
<Routes>
|
||||
{/* 登录页面 */}
|
||||
<Route
|
||||
path="/signin"
|
||||
element={
|
||||
<AuthRoute>
|
||||
<SignInIllustration />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 注册页面 */}
|
||||
<Route
|
||||
path="/sign-up"
|
||||
element={
|
||||
<AuthRoute>
|
||||
<SignUpIllustration />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 默认重定向到登录页 */}
|
||||
<Route path="/" element={<Navigate to="/auth/signin" replace />} />
|
||||
<Route path="*" element={<Navigate to="/auth/signin" replace />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// src/layouts/Home.js
|
||||
import React from "react";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
// 导航栏已由 MainLayout 提供,此处不再导入
|
||||
// import HomeNavbar from "../components/Navbars/HomeNavbar";
|
||||
|
||||
// 导入页面组件
|
||||
import HomePage from "views/Home/HomePage";
|
||||
import ProfilePage from "views/Profile/ProfilePage";
|
||||
import SettingsPage from "views/Settings/SettingsPage";
|
||||
import CenterDashboard from "views/Dashboard/Center";
|
||||
import Subscription from "views/Pages/Account/Subscription";
|
||||
|
||||
// 懒加载隐私政策、用户协议、微信回调和模拟交易页面
|
||||
const PrivacyPolicy = React.lazy(() => import("views/Pages/PrivacyPolicy"));
|
||||
const UserAgreement = React.lazy(() => import("views/Pages/UserAgreement"));
|
||||
const WechatCallback = React.lazy(() => import("views/Pages/WechatCallback"));
|
||||
const TradingSimulation = React.lazy(() => import("views/TradingSimulation"));
|
||||
|
||||
// 导入保护路由组件
|
||||
import ProtectedRoute from "../components/ProtectedRoute";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Box minH="100vh">
|
||||
{/* 导航栏已由 MainLayout 提供,此处不再渲染 */}
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<Box>
|
||||
<Routes>
|
||||
{/* 首页默认路由 */}
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route
|
||||
path="/center"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<CenterDashboard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 需要登录保护的页面 */}
|
||||
<Route
|
||||
path="/profile"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<ProfilePage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<SettingsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 订阅管理页面 */}
|
||||
<Route
|
||||
path="/pages/account/subscription"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Subscription />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 模拟盘交易页面 */}
|
||||
<Route
|
||||
path="/trading-simulation"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<TradingSimulation />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 隐私政策页面 - 无需登录 */}
|
||||
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
|
||||
|
||||
{/* 用户协议页面 - 无需登录 */}
|
||||
<Route path="/user-agreement" element={<UserAgreement />} />
|
||||
|
||||
{/* 微信授权回调页面 - 无需登录 */}
|
||||
<Route path="/wechat-callback" element={<WechatCallback />} />
|
||||
|
||||
{/* 其他可能的路由 */}
|
||||
<Route path="*" element={<HomePage />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
// src/layouts/MainLayout.js
|
||||
// 主布局组件 - 为所有带导航栏的页面提供统一布局
|
||||
import React, { memo } from "react";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import React, { memo, Suspense } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import HomeNavbar from "../components/Navbars/HomeNavbar";
|
||||
import AppFooter from "./AppFooter";
|
||||
import BackToTopButton from "./components/BackToTopButton";
|
||||
import PageTransitionWrapper from "./components/PageTransitionWrapper";
|
||||
import { ANIMATION_CONFIG, BACK_TO_TOP_CONFIG } from "./config/layoutConfig";
|
||||
import ErrorBoundary from "../components/ErrorBoundary";
|
||||
import PageLoader from "../components/Loading/PageLoader";
|
||||
import { BACK_TO_TOP_CONFIG } from "./config/layoutConfig";
|
||||
|
||||
// ✅ P0 性能优化:缓存静态组件,避免路由切换时不必要的重新渲染
|
||||
// HomeNavbar (1623行) 和 AppFooter 不依赖路由参数,使用 memo 可大幅减少渲染次数
|
||||
@@ -20,38 +21,27 @@ const MemoizedAppFooter = memo(AppFooter);
|
||||
* 使用 <Outlet /> 渲染子路由,确保导航栏只渲染一次
|
||||
* 页面切换时只有 Outlet 内的内容会更新,导航栏保持不变
|
||||
*
|
||||
* 架构优化(2024-10-30):
|
||||
* - ✅ P0: 组件拆分 - BackToTopButton 独立复用(37行 → 独立文件)
|
||||
* - ✅ P0: 组件拆分 - PageTransitionWrapper 封装复杂逻辑(18行 → 独立文件)
|
||||
* - ✅ P0: 性能优化 - 使用 memo 避免导航栏和页脚重新渲染(性能提升 50%+)
|
||||
* - ✅ P1: 性能优化 - 使用 RAF 节流滚动事件(性能提升 80%)
|
||||
* - ✅ P1: 错误隔离 - ErrorBoundary 包裹页面内容,确保导航栏可用
|
||||
* - ✅ P2: 用户体验 - 页面过渡动画(framer-motion)
|
||||
* - ✅ P2: 配置集中 - layoutConfig 统一管理配置常量
|
||||
* - ✅ P3: 用户体验 - 返回顶部按钮(滚动 > 300px 显示)
|
||||
*
|
||||
* 代码优化成果:
|
||||
* - 代码量:115 行 → 42 行(减少 63%)
|
||||
* - 复杂度:内联组件 → 独立模块
|
||||
* - 可维护性:配置分散 → 集中管理
|
||||
* - 可复用性:耦合 → 解耦
|
||||
* 架构优化:
|
||||
* - ✅ 组件拆分 - BackToTopButton 独立复用
|
||||
* - ✅ 性能优化 - 使用 memo 避免导航栏和页脚重新渲染
|
||||
* - ✅ 错误隔离 - ErrorBoundary 包裹页面内容,确保导航栏可用
|
||||
* - ✅ 懒加载支持 - Suspense 统一处理懒加载
|
||||
* - ✅ 布局简化 - 直接内联容器逻辑,减少嵌套层级
|
||||
*/
|
||||
export default function MainLayout() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Box minH="100vh" display="flex" flexDirection="column">
|
||||
{/* 导航栏 - 在所有页面间共享,memo 后不会在路由切换时重新渲染 */}
|
||||
<MemoizedHomeNavbar />
|
||||
|
||||
{/* 页面内容区域 - 包含动画、错误边界、懒加载 */}
|
||||
<PageTransitionWrapper
|
||||
location={location}
|
||||
animationConfig={ANIMATION_CONFIG.default}
|
||||
loaderMessage="页面加载中..."
|
||||
>
|
||||
{/* 页面内容区域 - flex: 1 占据剩余空间,包含错误边界、懒加载 */}
|
||||
<Box flex="1" w="100%" position="relative" overflow="hidden">
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<PageLoader message="页面加载中..." />}>
|
||||
<Outlet />
|
||||
</PageTransitionWrapper>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
|
||||
{/* 页脚 - 在所有页面间共享,memo 后不会在路由切换时重新渲染 */}
|
||||
<MemoizedAppFooter />
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
// src/layouts/components/PageTransitionWrapper.js
|
||||
import React, { Suspense, memo } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import ErrorBoundary from '../../components/ErrorBoundary';
|
||||
import PageLoader from '../../components/Loading/PageLoader';
|
||||
|
||||
// 创建 motion 包裹的 Box 组件
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
/**
|
||||
* 页面过渡动画包裹组件
|
||||
*
|
||||
* 功能:
|
||||
* - 页面切换时的过渡动画(AnimatePresence)
|
||||
* - 错误边界隔离(ErrorBoundary)
|
||||
* - 懒加载支持(Suspense)
|
||||
*
|
||||
* 优化:
|
||||
* - ✅ 使用 memo 避免不必要的重新渲染
|
||||
* - ✅ 支持自定义动画配置
|
||||
* - ✅ 错误隔离,确保导航栏不受影响
|
||||
*
|
||||
* @param {React.ReactNode} children - 要渲染的子组件(通常是 <Outlet />)
|
||||
* @param {object} location - 路由位置对象(用于动画 key)
|
||||
* @param {object} animationConfig - 自定义动画配置
|
||||
* @param {string} loaderMessage - 加载时显示的消息
|
||||
*/
|
||||
const PageTransitionWrapper = memo(({
|
||||
children,
|
||||
location,
|
||||
animationConfig = {
|
||||
initial: { opacity: 0, y: 20 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
exit: { opacity: 0, y: -20 },
|
||||
transition: { duration: 0.2 }
|
||||
},
|
||||
loaderMessage = '页面加载中...'
|
||||
}) => {
|
||||
return (
|
||||
<Box flex="1" position="relative" overflow="hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
<MotionBox
|
||||
key={location.pathname}
|
||||
initial={animationConfig.initial}
|
||||
animate={animationConfig.animate}
|
||||
exit={animationConfig.exit}
|
||||
transition={animationConfig.transition}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
{/* 错误边界:隔离页面错误,确保导航栏仍可用 */}
|
||||
<ErrorBoundary>
|
||||
{/* Suspense:支持 React.lazy() 懒加载 */}
|
||||
<Suspense fallback={<PageLoader message={loaderMessage} />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</MotionBox>
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
PageTransitionWrapper.displayName = 'PageTransitionWrapper';
|
||||
|
||||
export default PageTransitionWrapper;
|
||||
@@ -1,53 +0,0 @@
|
||||
// src/routes/components/RouteContainer.js
|
||||
// 路由容器组件 - 提供统一的错误边界、加载状态和主题背景
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
import { Box, useColorMode } from '@chakra-ui/react';
|
||||
import ErrorBoundary from '@components/ErrorBoundary';
|
||||
import PageLoader from '@components/Loading/PageLoader';
|
||||
|
||||
/**
|
||||
* RouteContainer - 路由容器组件
|
||||
*
|
||||
* 为路由系统提供统一的外层包装,包含:
|
||||
* 1. 主题感知的背景色(深色/浅色模式)
|
||||
* 2. Suspense 懒加载边界(显示加载提示)
|
||||
* 3. ErrorBoundary 错误边界(隔离路由错误)
|
||||
*
|
||||
* 这个组件确保:
|
||||
* - 所有路由页面都有一致的背景色
|
||||
* - 懒加载组件有统一的加载提示
|
||||
* - 单个路由的错误不会导致整个应用崩溃
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {React.ReactNode} props.children - 子组件(通常是 Routes)
|
||||
* @param {string} [props.loadingMessage='加载页面中...'] - 加载提示文本
|
||||
*
|
||||
* @example
|
||||
* <RouteContainer>
|
||||
* <Routes>
|
||||
* <Route path="/" element={<Home />} />
|
||||
* </Routes>
|
||||
* </RouteContainer>
|
||||
*/
|
||||
export function RouteContainer({
|
||||
children,
|
||||
loadingMessage = "加载页面中..."
|
||||
}) {
|
||||
const { colorMode } = useColorMode();
|
||||
|
||||
return (
|
||||
<Box
|
||||
minH="100vh"
|
||||
bg={colorMode === 'dark' ? 'gray.800' : 'white'}
|
||||
>
|
||||
{/* Suspense 统一处理懒加载组件的加载状态 */}
|
||||
<Suspense fallback={<PageLoader message={loadingMessage} />}>
|
||||
{/* ErrorBoundary 隔离路由错误,防止整个应用崩溃 */}
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// src/routes/components/index.js
|
||||
// 统一导出所有路由组件
|
||||
|
||||
export { RouteContainer } from './RouteContainer';
|
||||
@@ -1,5 +1,5 @@
|
||||
// src/routes/constants/index.js
|
||||
// 统一导出所有路由常量
|
||||
|
||||
export { LAYOUT_COMPONENTS } from './layoutComponents';
|
||||
export { PROTECTION_WRAPPER_MAP } from './protectionWrappers';
|
||||
export { PROTECTION_MODES } from './protectionModes';
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// src/routes/constants/layoutComponents.js
|
||||
// 布局组件映射表
|
||||
|
||||
import Auth from '@layouts/Auth';
|
||||
import HomeLayout from '@layouts/Home';
|
||||
|
||||
/**
|
||||
* 特殊布局组件映射表
|
||||
*
|
||||
* 用于将字符串标识符映射到实际的组件。
|
||||
* 这些是非懒加载的布局组件,在 routeConfig.js 中通过字符串引用。
|
||||
*
|
||||
* @example
|
||||
* // 在 routeConfig.js 中:
|
||||
* {
|
||||
* path: 'auth/*',
|
||||
* component: 'Auth', // 字符串标识符
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* // 通过 LAYOUT_COMPONENTS['Auth'] 获取实际组件
|
||||
*/
|
||||
export const LAYOUT_COMPONENTS = {
|
||||
Auth,
|
||||
HomeLayout,
|
||||
};
|
||||
14
src/routes/constants/protectionModes.js
Normal file
14
src/routes/constants/protectionModes.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// src/routes/constants/protectionModes.js
|
||||
// 路由保护模式常量
|
||||
|
||||
/**
|
||||
* 路由保护模式
|
||||
* - 'modal': 使用 ProtectedRoute (弹窗模式登录)
|
||||
* - 'redirect': 使用 ProtectedRouteRedirect (跳转模式登录)
|
||||
* - 'public': 公开访问,无需登录
|
||||
*/
|
||||
export const PROTECTION_MODES = {
|
||||
MODAL: 'modal',
|
||||
REDIRECT: 'redirect',
|
||||
PUBLIC: 'public',
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import ProtectedRoute from '@components/ProtectedRoute';
|
||||
import ProtectedRouteRedirect from '@components/ProtectedRouteRedirect';
|
||||
import { PROTECTION_MODES } from '../routeConfig';
|
||||
import { PROTECTION_MODES } from './protectionModes';
|
||||
|
||||
/**
|
||||
* 保护模式包装器映射表
|
||||
|
||||
115
src/routes/homeRoutes.js
Normal file
115
src/routes/homeRoutes.js
Normal file
@@ -0,0 +1,115 @@
|
||||
// src/routes/homeRoutes.js
|
||||
// Home 模块子路由配置
|
||||
|
||||
import { lazyComponents } from './lazy-components';
|
||||
import { PROTECTION_MODES } from './constants/protectionModes';
|
||||
|
||||
/**
|
||||
* Home 模块的子路由配置
|
||||
* 这些路由将作为 /home/* 的嵌套路由
|
||||
*
|
||||
* 注意:
|
||||
* - 使用相对路径(不带前导斜杠)
|
||||
* - 空字符串 '' 表示索引路由,匹配 /home
|
||||
* - 这些路由将通过 Outlet 渲染到父路由中
|
||||
*/
|
||||
export const homeRoutes = [
|
||||
// 首页 - /home
|
||||
{
|
||||
path: '',
|
||||
component: lazyComponents.HomePage,
|
||||
protection: PROTECTION_MODES.PUBLIC,
|
||||
meta: {
|
||||
title: '首页',
|
||||
description: '价值前沿首页'
|
||||
}
|
||||
},
|
||||
|
||||
// 个人中心 - /home/center
|
||||
{
|
||||
path: 'center',
|
||||
component: lazyComponents.CenterDashboard,
|
||||
protection: PROTECTION_MODES.MODAL,
|
||||
meta: {
|
||||
title: '个人中心',
|
||||
description: '用户个人中心'
|
||||
}
|
||||
},
|
||||
|
||||
// 个人资料 - /home/profile
|
||||
{
|
||||
path: 'profile',
|
||||
component: lazyComponents.ProfilePage,
|
||||
protection: PROTECTION_MODES.MODAL,
|
||||
meta: {
|
||||
title: '个人资料',
|
||||
description: '用户个人资料'
|
||||
}
|
||||
},
|
||||
|
||||
// 账户设置 - /home/settings
|
||||
{
|
||||
path: 'settings',
|
||||
component: lazyComponents.SettingsPage,
|
||||
protection: PROTECTION_MODES.MODAL,
|
||||
meta: {
|
||||
title: '账户设置',
|
||||
description: '用户账户设置'
|
||||
}
|
||||
},
|
||||
|
||||
// 订阅管理 - /home/pages/account/subscription
|
||||
{
|
||||
path: 'pages/account/subscription',
|
||||
component: lazyComponents.Subscription,
|
||||
protection: PROTECTION_MODES.MODAL,
|
||||
meta: {
|
||||
title: '订阅管理',
|
||||
description: '管理订阅服务'
|
||||
}
|
||||
},
|
||||
|
||||
// 隐私政策 - /home/privacy-policy
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
component: lazyComponents.PrivacyPolicy,
|
||||
protection: PROTECTION_MODES.PUBLIC,
|
||||
meta: {
|
||||
title: '隐私政策',
|
||||
description: '隐私保护政策'
|
||||
}
|
||||
},
|
||||
|
||||
// 用户协议 - /home/user-agreement
|
||||
{
|
||||
path: 'user-agreement',
|
||||
component: lazyComponents.UserAgreement,
|
||||
protection: PROTECTION_MODES.PUBLIC,
|
||||
meta: {
|
||||
title: '用户协议',
|
||||
description: '用户使用协议'
|
||||
}
|
||||
},
|
||||
|
||||
// 微信授权回调 - /home/wechat-callback
|
||||
{
|
||||
path: 'wechat-callback',
|
||||
component: lazyComponents.WechatCallback,
|
||||
protection: PROTECTION_MODES.PUBLIC,
|
||||
meta: {
|
||||
title: '微信授权',
|
||||
description: '微信授权回调页面'
|
||||
}
|
||||
},
|
||||
|
||||
// 回退路由 - 匹配任何未定义的 /home/* 路径
|
||||
{
|
||||
path: '*',
|
||||
component: lazyComponents.HomePage,
|
||||
protection: PROTECTION_MODES.PUBLIC,
|
||||
meta: {
|
||||
title: '首页',
|
||||
description: '价值前沿首页'
|
||||
}
|
||||
},
|
||||
];
|
||||
@@ -10,9 +10,8 @@ import { getMainLayoutRoutes, getStandaloneRoutes } from './routeConfig';
|
||||
// 布局组件
|
||||
import MainLayout from '@layouts/MainLayout';
|
||||
|
||||
// 路由工具和组件
|
||||
// 路由工具
|
||||
import { renderRoute } from './utils';
|
||||
import { RouteContainer } from './components';
|
||||
|
||||
/**
|
||||
* AppRoutes - 应用路由组件
|
||||
@@ -31,7 +30,11 @@ import { RouteContainer } from './components';
|
||||
* 目录结构:
|
||||
* - constants/ - 常量配置(布局映射、保护包装器)
|
||||
* - utils/ - 工具函数(renderRoute, wrapWithProtection)
|
||||
* - components/ - 路由组件(RouteContainer)
|
||||
* - components/ - 路由相关组件
|
||||
*
|
||||
* 注意:
|
||||
* - Suspense/ErrorBoundary 由 PageTransitionWrapper 统一处理
|
||||
* - 全屏容器由 MainLayout 提供(minH="100vh")
|
||||
*/
|
||||
export function AppRoutes() {
|
||||
// 🎯 性能优化:使用 useMemo 缓存路由计算结果
|
||||
@@ -39,7 +42,6 @@ export function AppRoutes() {
|
||||
const standaloneRoutes = useMemo(() => getStandaloneRoutes(), []);
|
||||
|
||||
return (
|
||||
<RouteContainer>
|
||||
<Routes>
|
||||
{/* 主布局路由 - 带导航栏和页脚 */}
|
||||
<Route element={<MainLayout />}>
|
||||
@@ -55,7 +57,6 @@ export function AppRoutes() {
|
||||
{/* 404 页面 - 捕获所有未匹配的路由 */}
|
||||
<Route path="*" element={<Navigate to="/home" replace />} />
|
||||
</Routes>
|
||||
</RouteContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,16 @@ import React from 'react';
|
||||
* 使用 React.lazy() 实现路由懒加载,大幅减少初始 JS 包大小
|
||||
*/
|
||||
export const lazyComponents = {
|
||||
// Home 模块
|
||||
HomePage: React.lazy(() => import('../views/Home/HomePage')),
|
||||
CenterDashboard: React.lazy(() => import('../views/Dashboard/Center')),
|
||||
ProfilePage: React.lazy(() => import('../views/Profile/ProfilePage')),
|
||||
SettingsPage: React.lazy(() => import('../views/Settings/SettingsPage')),
|
||||
Subscription: React.lazy(() => import('../views/Pages/Account/Subscription')),
|
||||
PrivacyPolicy: React.lazy(() => import('../views/Pages/PrivacyPolicy')),
|
||||
UserAgreement: React.lazy(() => import('../views/Pages/UserAgreement')),
|
||||
WechatCallback: React.lazy(() => import('../views/Pages/WechatCallback')),
|
||||
|
||||
// 社区/内容模块
|
||||
Community: React.lazy(() => import('../views/Community')),
|
||||
ConceptCenter: React.lazy(() => import('../views/Concept')),
|
||||
@@ -31,6 +41,14 @@ export const lazyComponents = {
|
||||
* 按需导出单个组件(可选)
|
||||
*/
|
||||
export const {
|
||||
HomePage,
|
||||
CenterDashboard,
|
||||
ProfilePage,
|
||||
SettingsPage,
|
||||
Subscription,
|
||||
PrivacyPolicy,
|
||||
UserAgreement,
|
||||
WechatCallback,
|
||||
Community,
|
||||
ConceptCenter,
|
||||
StockOverview,
|
||||
|
||||
@@ -2,35 +2,30 @@
|
||||
// 声明式路由配置
|
||||
|
||||
import { lazyComponents } from './lazy-components';
|
||||
import { homeRoutes } from './homeRoutes';
|
||||
import { PROTECTION_MODES } from './constants/protectionModes';
|
||||
|
||||
/**
|
||||
* 路由保护模式
|
||||
* - 'modal': 使用 ProtectedRoute (弹窗模式登录)
|
||||
* - 'redirect': 使用 ProtectedRouteRedirect (跳转模式登录)
|
||||
* - 'public': 公开访问,无需登录
|
||||
*/
|
||||
export const PROTECTION_MODES = {
|
||||
MODAL: 'modal',
|
||||
REDIRECT: 'redirect',
|
||||
PUBLIC: 'public',
|
||||
};
|
||||
// 重新导出 PROTECTION_MODES 以保持向后兼容
|
||||
export { PROTECTION_MODES };
|
||||
|
||||
/**
|
||||
* 路由配置
|
||||
* 每个路由对象包含:
|
||||
* - path: 路由路径
|
||||
* - component: 组件(从 lazyComponents 引用)
|
||||
* - component: 组件(从 lazyComponents 引用,或设为 null 使用 Outlet)
|
||||
* - protection: 保护模式 (modal/redirect/public)
|
||||
* - layout: 布局类型 (main/auth/none)
|
||||
* - children: 子路由配置数组(可选,用于嵌套路由)
|
||||
* - meta: 路由元数据(可选,用于面包屑、标题等)
|
||||
*/
|
||||
export const routeConfig = [
|
||||
// ==================== 首页 ====================
|
||||
{
|
||||
path: 'home/*',
|
||||
component: 'HomeLayout', // 非懒加载,直接在 App.js 导入
|
||||
path: 'home',
|
||||
component: null, // 使用 Outlet 渲染子路由
|
||||
protection: PROTECTION_MODES.PUBLIC,
|
||||
layout: 'main',
|
||||
children: homeRoutes, // 子路由配置
|
||||
meta: {
|
||||
title: '首页',
|
||||
description: '价值前沿首页'
|
||||
@@ -107,7 +102,7 @@ export const routeConfig = [
|
||||
{
|
||||
path: 'forecast-report',
|
||||
component: lazyComponents.ForecastReport,
|
||||
protection: PROTECTION_MODES.REDIRECT,
|
||||
protection: PROTECTION_MODES.MODAL,
|
||||
layout: 'main',
|
||||
meta: {
|
||||
title: '财报预测',
|
||||
@@ -115,7 +110,7 @@ export const routeConfig = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'Financial',
|
||||
path: 'financial',
|
||||
component: lazyComponents.FinancialPanorama,
|
||||
protection: PROTECTION_MODES.MODAL,
|
||||
layout: 'main',
|
||||
@@ -154,18 +149,6 @@ export const routeConfig = [
|
||||
description: '实时市场数据'
|
||||
}
|
||||
},
|
||||
|
||||
// ==================== 认证模块 ====================
|
||||
{
|
||||
path: 'auth/*',
|
||||
component: 'Auth', // 非懒加载,直接在 App.js 导入
|
||||
protection: PROTECTION_MODES.PUBLIC,
|
||||
layout: 'none',
|
||||
meta: {
|
||||
title: '登录/注册',
|
||||
description: '用户认证'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// 路由渲染工具函数
|
||||
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { LAYOUT_COMPONENTS } from '../constants';
|
||||
import { Route, Outlet } from 'react-router-dom';
|
||||
import { wrapWithProtection } from './wrapWithProtection';
|
||||
|
||||
/**
|
||||
@@ -11,14 +10,16 @@ import { wrapWithProtection } from './wrapWithProtection';
|
||||
*
|
||||
* 根据路由配置项生成 React Router 的 Route 组件。
|
||||
* 处理以下逻辑:
|
||||
* 1. 解析组件(特殊布局组件 vs 懒加载组件)
|
||||
* 1. 解析组件(懒加载组件 or Outlet)
|
||||
* 2. 应用路由保护(根据 protection 字段)
|
||||
* 3. 生成唯一 key
|
||||
* 3. 处理嵌套路由(children 数组)
|
||||
* 4. 生成唯一 key
|
||||
*
|
||||
* @param {Object} routeItem - 路由配置项(来自 routeConfig.js)
|
||||
* @param {string} routeItem.path - 路由路径
|
||||
* @param {React.ComponentType|string} routeItem.component - 组件或组件标识符
|
||||
* @param {React.ComponentType|null} routeItem.component - 懒加载组件或 null(null 表示使用 Outlet)
|
||||
* @param {string} routeItem.protection - 保护模式 (modal/redirect/public)
|
||||
* @param {Array} [routeItem.children] - 子路由配置数组(可选)
|
||||
* @param {number} index - 路由索引,用于生成唯一 key
|
||||
*
|
||||
* @returns {React.ReactElement} Route 组件
|
||||
@@ -27,19 +28,41 @@ import { wrapWithProtection } from './wrapWithProtection';
|
||||
* // 使用示例
|
||||
* const routes = [
|
||||
* { path: 'community', component: CommunityComponent, protection: 'modal' },
|
||||
* { path: 'auth/*', component: 'Auth', protection: 'public' },
|
||||
* { path: 'home', component: null, protection: 'public', children: [...] },
|
||||
* ];
|
||||
*
|
||||
* routes.map((route, index) => renderRoute(route, index));
|
||||
*/
|
||||
export function renderRoute(routeItem, index) {
|
||||
const { path, component, protection } = routeItem;
|
||||
const { path, component, protection, children } = routeItem;
|
||||
|
||||
// 解析组件:
|
||||
// - 如果是字符串(如 'Auth', 'HomeLayout'),从 LAYOUT_COMPONENTS 映射表查找
|
||||
// - 如果是 null,使用 <Outlet /> 用于嵌套路由
|
||||
// - 否则直接使用(懒加载组件)
|
||||
const Component = LAYOUT_COMPONENTS[component] || component;
|
||||
let Component;
|
||||
let isOutletRoute = false;
|
||||
|
||||
if (component === null) {
|
||||
Component = Outlet; // 用于嵌套路由
|
||||
isOutletRoute = true;
|
||||
} else {
|
||||
Component = component; // 直接使用懒加载组件
|
||||
}
|
||||
|
||||
// 如果有子路由,递归渲染
|
||||
if (children && children.length > 0) {
|
||||
return (
|
||||
<Route
|
||||
key={`${path}-${index}`}
|
||||
path={path}
|
||||
element={isOutletRoute ? <Outlet /> : wrapWithProtection(Component, protection)}
|
||||
>
|
||||
{children.map((childRoute, childIndex) => renderRoute(childRoute, childIndex))}
|
||||
</Route>
|
||||
);
|
||||
}
|
||||
|
||||
// 没有子路由,渲染单个路由
|
||||
return (
|
||||
<Route
|
||||
key={`${path}-${index}`}
|
||||
|
||||
@@ -47,37 +47,3 @@
|
||||
.bg-gradient-to-br {
|
||||
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)) !important;
|
||||
}
|
||||
|
||||
.from-n-8 { --tw-gradient-from: var(--color-n-8); --tw-gradient-to: transparent; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); }
|
||||
.via-n-7 { --tw-gradient-to: transparent; --tw-gradient-stops: var(--tw-gradient-from), var(--color-n-7), var(--tw-gradient-to); }
|
||||
.to-n-6 { --tw-gradient-to: var(--color-n-6); }
|
||||
|
||||
/* 文字渐变 */
|
||||
.bg-gradient-to-r { background-image: linear-gradient(to right, var(--tw-gradient-stops)) !important; }
|
||||
.from-color-1 { --tw-gradient-from: var(--color-1); --tw-gradient-to: transparent; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); }
|
||||
.to-color-2 { --tw-gradient-to: var(--color-2); }
|
||||
.from-color-2 { --tw-gradient-from: var(--color-2); --tw-gradient-to: transparent; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); }
|
||||
.to-color-1 { --tw-gradient-to: var(--color-1); }
|
||||
|
||||
.bg-clip-text {
|
||||
-webkit-background-clip: text !important;
|
||||
background-clip: text !important;
|
||||
}
|
||||
.text-transparent {
|
||||
color: transparent !important;
|
||||
}
|
||||
|
||||
/* 其他样式增强 */
|
||||
.backdrop-blur-sm { backdrop-filter: blur(8px) !important; }
|
||||
.backdrop-blur { backdrop-filter: blur(16px) !important; }
|
||||
|
||||
/* 确保body有深色背景 */
|
||||
body {
|
||||
background-color: var(--color-n-8) !important;
|
||||
}
|
||||
|
||||
/* z-index 修复 */
|
||||
.z-50 { z-index: 50 !important; }
|
||||
.z-10 { z-index: 10 !important; }
|
||||
.z-2 { z-index: 2 !important; }
|
||||
.z-1 { z-index: 1 !important; }
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* 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 style={{minWidth: '140px'}} 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>
|
||||
);
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
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}>欢迎回来1</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>
|
||||
);
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* 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">
|
||||
Don’t have an account?
|
||||
<Link
|
||||
color={titleColor}
|
||||
as="span"
|
||||
ms="5px"
|
||||
href="#"
|
||||
fontWeight="bold"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AuthCover>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignInCover;
|
||||
@@ -1,538 +0,0 @@
|
||||
// src/views/Authentication/SignIn/SignInIllustration.js - Session版本
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
Input,
|
||||
Text,
|
||||
Heading,
|
||||
VStack,
|
||||
HStack,
|
||||
useToast,
|
||||
Icon,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
IconButton,
|
||||
Link as ChakraLink,
|
||||
Center,
|
||||
useDisclosure,
|
||||
FormErrorMessage
|
||||
} from "@chakra-ui/react";
|
||||
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
|
||||
import { FaMobile, FaLock } from "react-icons/fa";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "../../../contexts/AuthContext";
|
||||
import PrivacyPolicyModal from "../../../components/PrivacyPolicyModal";
|
||||
import UserAgreementModal from "../../../components/UserAgreementModal";
|
||||
import AuthBackground from "../../../components/Auth/AuthBackground";
|
||||
import AuthHeader from "../../../components/Auth/AuthHeader";
|
||||
import AuthFooter from "../../../components/Auth/AuthFooter";
|
||||
import VerificationCodeInput from "../../../components/Auth/VerificationCodeInput";
|
||||
import WechatRegister from "../../../components/Auth/WechatRegister";
|
||||
import { logger } from "../../../utils/logger";
|
||||
|
||||
export default function SignInIllustration() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const toast = useToast();
|
||||
const { login, checkSession } = useAuth();
|
||||
|
||||
// 追踪组件挂载状态,防止内存泄漏
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
// 页面状态
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
// 检查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]);
|
||||
|
||||
// 传统登录数据
|
||||
// 表单数据初始化
|
||||
const [formData, setFormData] = useState({
|
||||
username: "", // 用户名称
|
||||
email: "", // 邮箱
|
||||
phone: "", // 电话
|
||||
password: "", // 密码
|
||||
verificationCode: "", // 添加验证码字段
|
||||
});
|
||||
|
||||
// 验证码登录状态 是否开启验证码
|
||||
const [useVerificationCode, setUseVerificationCode] = useState(false);
|
||||
// 密码展示状态
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
|
||||
const [verificationCodeSent, setVerificationCodeSent] = useState(false); // 验证码发送状态
|
||||
const [sendingCode, setSendingCode] = useState(false); // 发送验证码状态
|
||||
|
||||
|
||||
// 隐私政策弹窗状态
|
||||
const { isOpen: isPrivacyModalOpen, onOpen: onPrivacyModalOpen, onClose: onPrivacyModalClose } = useDisclosure();
|
||||
|
||||
// 用户协议弹窗状态
|
||||
const { isOpen: isUserAgreementModalOpen, onOpen: onUserAgreementModalOpen, onClose: onUserAgreementModalClose } = useDisclosure();
|
||||
|
||||
// 输入框输入
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
|
||||
// ========== 发送验证码逻辑 =============
|
||||
// 倒计时效果
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
let isMounted = true;
|
||||
|
||||
if (countdown > 0) {
|
||||
timer = setInterval(() => {
|
||||
if (isMounted) {
|
||||
setCountdown(prev => prev - 1);
|
||||
}
|
||||
}, 1000);
|
||||
} else if (countdown === 0 && isMounted) {
|
||||
setVerificationCodeSent(false);
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (timer) clearInterval(timer);
|
||||
};
|
||||
}, [countdown]);
|
||||
|
||||
// 发送验证码
|
||||
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/auth/send-verification-code', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
credential,
|
||||
type,
|
||||
purpose: 'login'
|
||||
}),
|
||||
});
|
||||
|
||||
// ✅ 安全检查:验证 response 存在
|
||||
if (!response) {
|
||||
throw new Error('网络请求失败,请检查网络连接');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 组件卸载后不再执行后续操作
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// ✅ 安全检查:验证 data 存在
|
||||
if (!data) {
|
||||
throw new Error('服务器响应为空');
|
||||
}
|
||||
|
||||
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) {
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "发送验证码失败",
|
||||
description: error.message || "请稍后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setSendingCode(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 验证码登录函数
|
||||
const loginWithVerificationCode = async (credential, verificationCode, authLoginType) => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/login-with-code', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
credential,
|
||||
verification_code: verificationCode,
|
||||
login_type: authLoginType
|
||||
}),
|
||||
});
|
||||
|
||||
// ✅ 安全检查:验证 response 存在
|
||||
if (!response) {
|
||||
throw new Error('网络请求失败,请检查网络连接');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 组件卸载后不再执行后续操作
|
||||
if (!isMountedRef.current) {
|
||||
return { success: false, error: '操作已取消' };
|
||||
}
|
||||
|
||||
// ✅ 安全检查:验证 data 存在
|
||||
if (!data) {
|
||||
throw new Error('服务器响应为空');
|
||||
}
|
||||
|
||||
if (response.ok && data.success) {
|
||||
// 更新认证状态
|
||||
await checkSession();
|
||||
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "登录成功",
|
||||
description: "欢迎回来!",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
return { success: true };
|
||||
} else {
|
||||
throw new Error(data.error || '验证码登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "登录失败",
|
||||
description: error.message || "请检查验证码是否正确",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 传统行业登陆
|
||||
const handleTraditionalLogin = async (e) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const credential = formData.phone;
|
||||
const authLoginType = 'phone';
|
||||
|
||||
if (useVerificationCode) { // 验证码登陆
|
||||
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: `手机号和密码不能为空`,
|
||||
status: "warning",
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await login(credential, formData.password, authLoginType);
|
||||
|
||||
if (result.success) {
|
||||
// ✅ 显示成功提示
|
||||
toast({
|
||||
title: "登录成功",
|
||||
description: "欢迎回来!",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
navigate("/home");
|
||||
} else {
|
||||
// ❌ 显示错误提示
|
||||
toast({
|
||||
title: "登录失败",
|
||||
description: result.error || "请检查您的登录信息",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('SignInIllustration', 'handleTraditionalLogin', error, {
|
||||
phone: formData.phone ? formData.phone.substring(0, 3) + '****' + formData.phone.substring(7) : 'N/A',
|
||||
useVerificationCode,
|
||||
loginType: 'phone'
|
||||
});
|
||||
toast({
|
||||
title: "登录失败",
|
||||
description: error.message || "发生未预期的错误,请重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
isClosable: true,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换登录方式
|
||||
const handleChangeMethod = () => {
|
||||
setUseVerificationCode(!useVerificationCode);
|
||||
// 切换到密码模式时清空验证码
|
||||
if (useVerificationCode) {
|
||||
setFormData(prev => ({ ...prev, verificationCode: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex minH="100vh" position="relative" overflow="hidden">
|
||||
{/* 背景 */}
|
||||
<AuthBackground />
|
||||
|
||||
{/* 主要内容 */}
|
||||
<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="800px" backdropFilter="blur(20px)" border="1px solid rgba(255, 255, 255, 0.2)">
|
||||
{/* 头部区域 */}
|
||||
<AuthHeader title="欢迎回来" subtitle="登录价值前沿,继续您的投资之旅" />
|
||||
{/* 左右布局 */}
|
||||
<HStack spacing={8} align="stretch">
|
||||
{/* 左侧:手机号登陆 - 80% 宽度 */}
|
||||
<Box flex="4">
|
||||
<form onSubmit={handleTraditionalLogin}>
|
||||
<VStack spacing={4}>
|
||||
<Heading size="md" color="gray.700" alignSelf="flex-start">
|
||||
手机号登陆
|
||||
</Heading>
|
||||
<FormControl isRequired isInvalid={!!errors.phone}>
|
||||
<Input
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入11位手机号"
|
||||
pr="2.5rem"
|
||||
/>
|
||||
<FormErrorMessage>{errors.phone}</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
{/* 密码/验证码输入框 */}
|
||||
{useVerificationCode ? (
|
||||
<VerificationCodeInput
|
||||
value={formData.verificationCode}
|
||||
onChange={handleInputChange}
|
||||
onSendCode={sendVerificationCode}
|
||||
countdown={countdown}
|
||||
isLoading={isLoading}
|
||||
isSending={sendingCode}
|
||||
error={errors.verificationCode}
|
||||
colorScheme="green"
|
||||
/>
|
||||
) : (
|
||||
<FormControl isRequired isInvalid={!!errors.password}>
|
||||
<InputGroup>
|
||||
<Input
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
pr="3rem"
|
||||
placeholder="请输入密码"
|
||||
_focus={{
|
||||
borderColor: "blue.500",
|
||||
boxShadow: "0 0 0 1px #667eea"
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<AuthFooter
|
||||
linkText="还没有账号,"
|
||||
linkLabel="去注册"
|
||||
linkTo="/auth/sign-up"
|
||||
useVerificationCode={useVerificationCode}
|
||||
onSwitchMethod={handleChangeMethod}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
width="100%"
|
||||
size="lg"
|
||||
colorScheme="green"
|
||||
color="white"
|
||||
borderRadius="lg"
|
||||
_hover={{
|
||||
transform: "translateY(-2px)",
|
||||
boxShadow: "lg"
|
||||
}}
|
||||
_active={{ transform: "translateY(0)" }}
|
||||
isLoading={isLoading}
|
||||
loadingText="登录中..."
|
||||
fontWeight="bold"
|
||||
cursor={"pointer"}
|
||||
>
|
||||
<Icon as={FaLock} mr={2} />登录
|
||||
</Button>
|
||||
</VStack>
|
||||
</form>
|
||||
</Box>
|
||||
{/* 右侧:微信登陆 - 20% 宽度 */}
|
||||
<Box flex="1">
|
||||
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
|
||||
<WechatRegister />
|
||||
</Center>
|
||||
</Box>
|
||||
</HStack>
|
||||
|
||||
{/* 底部链接 */}
|
||||
<VStack spacing={4} mt={6}>
|
||||
{/* 协议同意勾选框 */}
|
||||
<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>
|
||||
</VStack>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
|
||||
{/* 隐私政策弹窗 */}
|
||||
<PrivacyPolicyModal isOpen={isPrivacyModalOpen} onClose={onPrivacyModalClose} />
|
||||
|
||||
{/* 用户协议弹窗 */}
|
||||
<UserAgreementModal isOpen={isUserAgreementModalOpen} onClose={onUserAgreementModalClose} />
|
||||
</Flex >
|
||||
);
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* 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">
|
||||
Don’t have an account?
|
||||
<Link
|
||||
color={titleColor}
|
||||
as="span"
|
||||
ms="5px"
|
||||
href="#"
|
||||
fontWeight="bold"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</AuthCover>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignUpCover;
|
||||
@@ -1,445 +0,0 @@
|
||||
// src\views\Authentication\SignUp/SignUpIllustration.js
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { getApiBase } from '../../../utils/apiConfig';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
Input,
|
||||
Text,
|
||||
Heading,
|
||||
VStack,
|
||||
HStack,
|
||||
useToast,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
IconButton,
|
||||
Center,
|
||||
FormErrorMessage,
|
||||
Link as ChakraLink,
|
||||
useDisclosure
|
||||
} from "@chakra-ui/react";
|
||||
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import axios from "axios";
|
||||
import AuthBackground from '../../../components/Auth/AuthBackground';
|
||||
import AuthHeader from '../../../components/Auth/AuthHeader';
|
||||
import AuthFooter from '../../../components/Auth/AuthFooter';
|
||||
import VerificationCodeInput from '../../../components/Auth/VerificationCodeInput';
|
||||
import WechatRegister from '../../../components/Auth/WechatRegister';
|
||||
import PrivacyPolicyModal from '../../../components/PrivacyPolicyModal';
|
||||
import UserAgreementModal from '../../../components/UserAgreementModal';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const API_BASE_URL = getApiBase();
|
||||
|
||||
export default function SignUpPage() {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
username: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
verificationCode: ""
|
||||
});
|
||||
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
|
||||
// 追踪组件挂载状态,防止内存泄漏
|
||||
const isMountedRef = useRef(true);
|
||||
|
||||
// 隐私政策弹窗状态
|
||||
const { isOpen: isPrivacyModalOpen, onOpen: onPrivacyModalOpen, onClose: onPrivacyModalClose } = useDisclosure();
|
||||
|
||||
// 用户协议弹窗状态
|
||||
const { isOpen: isUserAgreementModalOpen, onOpen: onUserAgreementModalOpen, onClose: onUserAgreementModalClose } = useDisclosure();
|
||||
|
||||
// 验证码登录状态 是否开启验证码
|
||||
const [useVerificationCode, setUseVerificationCode] = useState(false);
|
||||
|
||||
// 切换注册方式
|
||||
const handleChangeMethod = () => {
|
||||
setUseVerificationCode(!useVerificationCode);
|
||||
// 切换到密码模式时清空验证码
|
||||
if (useVerificationCode) {
|
||||
setFormData(prev => ({ ...prev, verificationCode: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
// 发送验证码
|
||||
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);
|
||||
const response = await axios.post(`${API_BASE_URL}/api/auth/${endpoint}`, {
|
||||
[fieldName]: contact
|
||||
}, {
|
||||
timeout: 10000 // 添加10秒超时
|
||||
});
|
||||
|
||||
// 组件卸载后不再执行后续操作
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// ✅ 安全检查:验证 response 和 data 存在
|
||||
if (!response || !response.data) {
|
||||
throw new Error('服务器响应为空');
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "验证码已发送",
|
||||
description: "请查收短信",
|
||||
status: "success",
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
setCountdown(60);
|
||||
} catch (error) {
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "发送失败",
|
||||
description: error.response?.data?.error || error.message || "请稍后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 倒计时效果
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
|
||||
if (countdown > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
if (isMounted) {
|
||||
setCountdown(countdown - 1);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
// 表单验证
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
// 手机号验证(两种方式都需要)
|
||||
if (!formData.phone) {
|
||||
newErrors.phone = "请输入手机号";
|
||||
} else if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
|
||||
newErrors.phone = "请输入正确的手机号";
|
||||
}
|
||||
|
||||
if (useVerificationCode) {
|
||||
// 验证码注册方式:只验证手机号和验证码
|
||||
if (!formData.verificationCode) {
|
||||
newErrors.verificationCode = "请输入验证码";
|
||||
}
|
||||
} else {
|
||||
// 密码注册方式:验证用户名、密码和确认密码
|
||||
if (!formData.password || formData.password.length < 6) {
|
||||
newErrors.password = "密码至少6个字符";
|
||||
}
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = "两次密码不一致";
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
// 处理注册提交
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
let endpoint, data;
|
||||
|
||||
if (useVerificationCode) {
|
||||
// 验证码注册:只发送手机号和验证码
|
||||
endpoint = "/api/auth/register/phone-code";
|
||||
data = {
|
||||
phone: formData.phone,
|
||||
code: formData.verificationCode
|
||||
};
|
||||
} else {
|
||||
// 密码注册:发送手机号、用户名和密码
|
||||
endpoint = "/api/auth/register/phone";
|
||||
data = {
|
||||
phone: formData.phone,
|
||||
username: formData.username,
|
||||
password: formData.password
|
||||
};
|
||||
}
|
||||
|
||||
const response = await axios.post(`${API_BASE_URL}${endpoint}`, data, {
|
||||
timeout: 10000 // 添加10秒超时
|
||||
});
|
||||
|
||||
// 组件卸载后不再执行后续操作
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// ✅ 安全检查:验证 response 和 data 存在
|
||||
if (!response || !response.data) {
|
||||
throw new Error('注册请求失败:服务器响应为空');
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "注册成功",
|
||||
description: "即将跳转到登录页面",
|
||||
status: "success",
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (isMountedRef.current) {
|
||||
navigate("/auth/sign-in");
|
||||
}
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
if (isMountedRef.current) {
|
||||
toast({
|
||||
title: "注册失败",
|
||||
description: error.response?.data?.error || error.message || "请稍后重试",
|
||||
status: "error",
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
if (errors[name]) {
|
||||
setErrors(prev => ({ ...prev, [name]: "" }));
|
||||
}
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 公用的用户名和密码输入框组件
|
||||
const commonAuthFields = (
|
||||
<VStack spacing={4} width="100%">
|
||||
<FormControl isRequired isInvalid={!!errors.password}>
|
||||
<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}>
|
||||
<Input
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
placeholder="再次输入密码"
|
||||
/>
|
||||
<FormErrorMessage>{errors.confirmPassword}</FormErrorMessage>
|
||||
</FormControl>
|
||||
</VStack>
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex minH="100vh" position="relative" overflow="hidden">
|
||||
{/* 背景 */}
|
||||
<AuthBackground />
|
||||
|
||||
{/* 主要内容 */}
|
||||
<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="800px" backdropFilter="blur(20px)" border="1px solid rgba(255, 255, 255, 0.2)">
|
||||
{/* 头部区域 */}
|
||||
<AuthHeader title="创建账户" subtitle="加入价值前沿,开启投资新征程" />
|
||||
{/* 左右布局 */}
|
||||
<HStack spacing={8} align="stretch">
|
||||
{/* 左侧:手机号注册 - 80% 宽度 */}
|
||||
<Box flex="4">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<VStack spacing={4}>
|
||||
<Heading size="md" color="gray.700" alignSelf="flex-start">
|
||||
注册
|
||||
</Heading>
|
||||
<FormControl isRequired isInvalid={!!errors.phone}>
|
||||
<Input
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handleInputChange}
|
||||
placeholder="请输入11位手机号"
|
||||
pr="2.5rem"
|
||||
/>
|
||||
<FormErrorMessage>{errors.phone}</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
{/* 表单字段区域 */}
|
||||
<Box width="100%">
|
||||
{
|
||||
useVerificationCode ? (
|
||||
<VStack spacing={4} width="100%">
|
||||
<VerificationCodeInput
|
||||
value={formData.verificationCode}
|
||||
onChange={handleInputChange}
|
||||
onSendCode={sendVerificationCode}
|
||||
countdown={countdown}
|
||||
isLoading={isLoading}
|
||||
isSending={isLoading && countdown === 0}
|
||||
error={errors.verificationCode}
|
||||
colorScheme="green"
|
||||
/>
|
||||
{/* 隐藏的占位元素,保持与密码模式相同的高度 */}
|
||||
<Box height="40px" width="100%" visibility="hidden" />
|
||||
</VStack>
|
||||
) : (
|
||||
<>
|
||||
{commonAuthFields}
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Box>
|
||||
|
||||
<AuthFooter
|
||||
linkText="已有账号?"
|
||||
linkLabel="去登录"
|
||||
linkTo="/auth/sign-in"
|
||||
useVerificationCode={useVerificationCode}
|
||||
onSwitchMethod={handleChangeMethod}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
width="100%"
|
||||
size="lg"
|
||||
colorScheme="green"
|
||||
color="white"
|
||||
borderRadius="lg"
|
||||
_hover={{
|
||||
transform: "translateY(-2px)",
|
||||
boxShadow: "lg"
|
||||
}}
|
||||
_active={{ transform: "translateY(0)" }}
|
||||
isLoading={isLoading}
|
||||
loadingText="注册中..."
|
||||
fontWeight="bold"
|
||||
cursor="pointer"
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
|
||||
{/* 协议同意文本 */}
|
||||
<Text fontSize="sm" color="gray.600" textAlign="center" width="100%">
|
||||
注册登录即表示阅读并同意{" "}
|
||||
<ChakraLink
|
||||
color="blue.500"
|
||||
fontSize="sm"
|
||||
onClick={onUserAgreementModalOpen}
|
||||
textDecoration="underline"
|
||||
_hover={{ color: "blue.600" }}
|
||||
cursor="pointer"
|
||||
>
|
||||
《用户协议》
|
||||
</ChakraLink>
|
||||
{" "}和{" "}
|
||||
<ChakraLink
|
||||
color="blue.500"
|
||||
fontSize="sm"
|
||||
onClick={onPrivacyModalOpen}
|
||||
textDecoration="underline"
|
||||
_hover={{ color: "blue.600" }}
|
||||
cursor="pointer"
|
||||
>
|
||||
《隐私政策》
|
||||
</ChakraLink>
|
||||
</Text>
|
||||
</VStack>
|
||||
</form>
|
||||
</Box>
|
||||
|
||||
{/* 右侧:微信注册 - 20% 宽度 */}
|
||||
<Box flex="1">
|
||||
<Center width="100%" bg="gray.50" borderRadius="lg" p={8}>
|
||||
<WechatRegister />
|
||||
</Center>
|
||||
</Box>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* 隐私政策弹窗 */}
|
||||
<PrivacyPolicyModal isOpen={isPrivacyModalOpen} onClose={onPrivacyModalClose} />
|
||||
|
||||
{/* 用户协议弹窗 */}
|
||||
<UserAgreementModal isOpen={isUserAgreementModalOpen} onClose={onUserAgreementModalClose} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -349,12 +349,13 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
|
||||
<Container maxW="container.xl">
|
||||
<Flex justify="space-between" align="center">
|
||||
{/* 左侧占位 */}
|
||||
<Box flex="1" />
|
||||
<Box key="left-spacer" flex="1" />
|
||||
|
||||
{/* 中间:分页器 */}
|
||||
{pagination.total > 0 && localEvents.length > 0 ? (
|
||||
<Flex align="center" gap={2}>
|
||||
<Flex key="pagination-controls" align="center" gap={2}>
|
||||
<Button
|
||||
key="prev-page"
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => onPageChange(pagination.current - 1)}
|
||||
@@ -362,10 +363,11 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
<Text fontSize="xs" color={mutedColor} px={2} whiteSpace="nowrap">
|
||||
<Text key="page-info" fontSize="xs" color={mutedColor} px={2} whiteSpace="nowrap">
|
||||
第 {pagination.current} / {Math.ceil(pagination.total / pagination.pageSize)} 页
|
||||
</Text>
|
||||
<Button
|
||||
key="next-page"
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => onPageChange(pagination.current + 1)}
|
||||
@@ -373,18 +375,19 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
<Text fontSize="xs" color={mutedColor} ml={2} whiteSpace="nowrap">
|
||||
<Text key="total-count" fontSize="xs" color={mutedColor} ml={2} whiteSpace="nowrap">
|
||||
共 {pagination.total} 条
|
||||
</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box flex="1" />
|
||||
<Box key="center-spacer" flex="1" />
|
||||
)}
|
||||
|
||||
{/* 右侧:控制按钮 */}
|
||||
<Flex align="center" gap={3} flex="1" justify="flex-end">
|
||||
<Flex key="right-controls" align="center" gap={3} flex="1" justify="flex-end">
|
||||
{/* WebSocket 连接状态 */}
|
||||
<Badge
|
||||
key="websocket-status"
|
||||
colorScheme={isConnected ? 'green' : 'red'}
|
||||
fontSize="xs"
|
||||
px={2}
|
||||
@@ -395,7 +398,7 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
|
||||
</Badge>
|
||||
|
||||
{/* 桌面推送开关 */}
|
||||
<FormControl display="flex" alignItems="center" w="auto">
|
||||
<FormControl key="push-notification" display="flex" alignItems="center" w="auto">
|
||||
<FormLabel htmlFor="push-notification" mb="0" fontSize="xs" color={textColor} mr={2}>
|
||||
推送
|
||||
</FormLabel>
|
||||
@@ -420,7 +423,7 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
|
||||
</FormControl>
|
||||
|
||||
{/* 视图切换控制 */}
|
||||
<FormControl display="flex" alignItems="center" w="auto">
|
||||
<FormControl key="compact-mode" display="flex" alignItems="center" w="auto">
|
||||
<FormLabel htmlFor="compact-mode" mb="0" fontSize="xs" color={textColor} mr={2}>
|
||||
精简
|
||||
</FormLabel>
|
||||
@@ -440,7 +443,7 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
|
||||
{/* 事件列表内容 */}
|
||||
<Container maxW="container.xl">
|
||||
{localEvents.length > 0 ? (
|
||||
<VStack align="stretch" spacing={0}>
|
||||
<VStack key="event-list" align="stretch" spacing={0}>
|
||||
{localEvents.map((event, index) => (
|
||||
<Box key={event.id} position="relative">
|
||||
<EventCard
|
||||
@@ -460,10 +463,10 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
|
||||
))}
|
||||
</VStack>
|
||||
) : (
|
||||
<Center h="300px">
|
||||
<Center key="empty-state" h="300px">
|
||||
<VStack spacing={4}>
|
||||
<InfoIcon boxSize={12} color={mutedColor} />
|
||||
<Text color={mutedColor} fontSize="lg">
|
||||
<InfoIcon key="empty-icon" boxSize={12} color={mutedColor} />
|
||||
<Text key="empty-text" color={mutedColor} fontSize="lg">
|
||||
暂无事件数据
|
||||
</Text>
|
||||
</VStack>
|
||||
@@ -472,6 +475,7 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai
|
||||
|
||||
{pagination.total > 0 && (
|
||||
<Pagination
|
||||
key="bottom-pagination"
|
||||
current={pagination.current}
|
||||
total={pagination.total}
|
||||
pageSize={pagination.pageSize}
|
||||
|
||||
@@ -283,7 +283,7 @@ export default function CenterDashboard() {
|
||||
icon={<FiPlus />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate('/stock-analysis/overview')}
|
||||
onClick={() => navigate('/stocks')}
|
||||
aria-label="添加自选股"
|
||||
/>
|
||||
</Flex>
|
||||
@@ -300,7 +300,7 @@ export default function CenterDashboard() {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
colorScheme="blue"
|
||||
onClick={() => navigate('/stock-analysis/overview')}
|
||||
onClick={() => navigate('/stocks')}
|
||||
>
|
||||
添加自选股
|
||||
</Button>
|
||||
@@ -321,7 +321,7 @@ export default function CenterDashboard() {
|
||||
<VStack align="start" spacing={0}>
|
||||
<LinkOverlay
|
||||
as={Link}
|
||||
to={`/stock-analysis/company?scode=${stock.stock_code}`}
|
||||
to={`/company/${stock.stock_code}`}
|
||||
>
|
||||
<Text fontWeight="medium" fontSize="sm">
|
||||
{stock.stock_name || stock.stock_code}
|
||||
@@ -365,7 +365,7 @@ export default function CenterDashboard() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => navigate('/stock-analysis/overview')}
|
||||
onClick={() => navigate('/stocks')}
|
||||
>
|
||||
查看全部 ({watchlist.length})
|
||||
</Button>
|
||||
|
||||
@@ -1,629 +0,0 @@
|
||||
// src/views/Home/HomePage.js
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Text,
|
||||
Button,
|
||||
Container,
|
||||
VStack,
|
||||
HStack,
|
||||
Icon,
|
||||
Heading,
|
||||
useBreakpointValue,
|
||||
Link,
|
||||
SimpleGrid,
|
||||
Divider
|
||||
} from '@chakra-ui/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext'; // 添加这个导入来调试
|
||||
import heroBg from 'assets/img/BackgroundCard1.png';
|
||||
import teamWorkingImg from 'assets/img/background-card-reports.png';
|
||||
|
||||
export default function HomePage() {
|
||||
const navigate = useNavigate();
|
||||
const { user, isAuthenticated, isLoading } = useAuth(); // 添加这行来调试
|
||||
|
||||
// 添加调试信息
|
||||
useEffect(() => {
|
||||
console.log('🏠 HomePage AuthContext 状态:', {
|
||||
user,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
hasUser: !!user,
|
||||
userInfo: user ? {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
nickname: user.nickname
|
||||
} : null
|
||||
});
|
||||
}, [user?.id, isAuthenticated, isLoading]); // 只依赖 user.id,避免无限循环
|
||||
|
||||
// 统计数据动画
|
||||
const [stats, setStats] = useState({
|
||||
dataSize: 0,
|
||||
dataSources: 0,
|
||||
researchTargets: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const targetStats = {
|
||||
dataSize: 17,
|
||||
dataSources: 300,
|
||||
researchTargets: 45646
|
||||
};
|
||||
|
||||
// 动画效果
|
||||
const animateStats = () => {
|
||||
const duration = 2000; // 2秒动画
|
||||
const startTime = Date.now();
|
||||
|
||||
const animate = () => {
|
||||
const elapsed = Date.now() - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
setStats({
|
||||
dataSize: Math.floor(targetStats.dataSize * progress),
|
||||
dataSources: Math.floor(targetStats.dataSources * progress),
|
||||
researchTargets: Math.floor(targetStats.researchTargets * progress)
|
||||
});
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
animate();
|
||||
};
|
||||
|
||||
const timer = setTimeout(animateStats, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* 临时调试信息栏 - 完成调试后可以删除 */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<Box bg="yellow.100" p={2} fontSize="sm" borderBottom="1px solid" borderColor="yellow.300">
|
||||
<Text fontWeight="bold">🐛 调试信息:</Text>
|
||||
<Text>认证状态: {isAuthenticated ? '✅ 已登录' : '❌ 未登录'}</Text>
|
||||
<Text>加载状态: {isLoading ? '⏳ 加载中' : '✅ 加载完成'}</Text>
|
||||
<Text>用户信息: {user ? `👤 ${user.nickname || user.username} (ID: ${user.id})` : '❌ 无用户信息'}</Text>
|
||||
<Text>localStorage: {localStorage.getItem('user') ? '✅ 有数据' : '❌ 无数据'}</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Hero Section - Brainwave风格 */}
|
||||
<Box
|
||||
position="relative"
|
||||
minH="100vh"
|
||||
bg="linear-gradient(135deg, #0E0C15 0%, #15131D 50%, #252134 100%)"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* 同心圆背景装饰 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translate(-50%, -50%)"
|
||||
w="78rem"
|
||||
h="78rem"
|
||||
border="1px solid"
|
||||
borderColor="brainwave.n6"
|
||||
borderRadius="50%"
|
||||
opacity={0.3}
|
||||
zIndex={0}
|
||||
>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translate(-50%, -50%)"
|
||||
w="65rem"
|
||||
h="65rem"
|
||||
border="1px solid"
|
||||
borderColor="brainwave.n6"
|
||||
borderRadius="50%"
|
||||
opacity={0.2}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translate(-50%, -50%)"
|
||||
w="51rem"
|
||||
h="51rem"
|
||||
border="1px solid"
|
||||
borderColor="brainwave.n6"
|
||||
borderRadius="50%"
|
||||
opacity={0.1}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="50%"
|
||||
left="50%"
|
||||
transform="translate(-50%, -50%)"
|
||||
w="36rem"
|
||||
h="36rem"
|
||||
border="1px solid"
|
||||
borderColor="brainwave.n6"
|
||||
borderRadius="50%"
|
||||
opacity={0.1}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 动态装饰点 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="30%"
|
||||
right="20%"
|
||||
w="4"
|
||||
h="4"
|
||||
bg="linear-gradient(135deg, #AC6AFF, #1A1A32)"
|
||||
borderRadius="50%"
|
||||
animation="float 3s ease-in-out infinite"
|
||||
zIndex={1}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="40%"
|
||||
left="15%"
|
||||
w="6"
|
||||
h="6"
|
||||
bg="linear-gradient(135deg, #7ADB78, #1A1A32)"
|
||||
borderRadius="50%"
|
||||
animation="float 4s ease-in-out infinite 0.5s"
|
||||
zIndex={1}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="20%"
|
||||
left="25%"
|
||||
w="3"
|
||||
h="3"
|
||||
bg="linear-gradient(135deg, #FFC876, #1A1A32)"
|
||||
borderRadius="50%"
|
||||
animation="float 2.5s ease-in-out infinite 1s"
|
||||
zIndex={1}
|
||||
/>
|
||||
|
||||
{/* 主要内容 */}
|
||||
<Container maxW="container.lg" position="relative" zIndex={2}>
|
||||
<VStack spacing={8} textAlign="center" color="brainwave.n1">
|
||||
<Heading
|
||||
as="h1"
|
||||
fontSize={{ base: "3xl", md: "5xl", lg: "6xl" }}
|
||||
fontWeight="bold"
|
||||
lineHeight={1.1}
|
||||
letterSpacing="tight"
|
||||
>
|
||||
探索
|
||||
<Text as="span" bgGradient="linear(to-r, brainwave.purple, brainwave.orange)" bgClip="text">
|
||||
人工智能
|
||||
</Text>
|
||||
的无限可能
|
||||
<br />
|
||||
<Text as="span" fontSize={{ base: "2xl", md: "4xl", lg: "5xl" }} color="brainwave.n2">
|
||||
价值前沿 AI 投研助手
|
||||
</Text>
|
||||
</Heading>
|
||||
<Text
|
||||
fontSize={{ base: "lg", md: "xl" }}
|
||||
color="brainwave.n2"
|
||||
maxW="600px"
|
||||
lineHeight={1.6}
|
||||
>
|
||||
释放AI的力量,升级您的投研效率。
|
||||
体验超越ChatGPT的专业投资分析平台。
|
||||
</Text>
|
||||
<Button
|
||||
size="lg"
|
||||
bg="linear-gradient(135deg, #AC6AFF, #FFC876)"
|
||||
color="white"
|
||||
_hover={{
|
||||
transform: "translateY(-2px)",
|
||||
boxShadow: "0 8px 32px rgba(172, 106, 255, 0.3)"
|
||||
}}
|
||||
_active={{ transform: "translateY(0)" }}
|
||||
transition="all 0.2s"
|
||||
borderRadius="full"
|
||||
px={8}
|
||||
py={6}
|
||||
fontSize="md"
|
||||
fontWeight="semibold"
|
||||
onClick={() => navigate('/community')}
|
||||
>
|
||||
开始体验
|
||||
</Button>
|
||||
</VStack>
|
||||
</Container>
|
||||
|
||||
{/* 渐变底部 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom={0}
|
||||
left={0}
|
||||
right={0}
|
||||
height="100px"
|
||||
zIndex={2}
|
||||
bg="linear-gradient(to top, white 0%, transparent 100%)"
|
||||
/>
|
||||
|
||||
{/* CSS动画定义 */}
|
||||
<style jsx>{`
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-20px); }
|
||||
}
|
||||
`}</style>
|
||||
</Box>
|
||||
|
||||
{/* 统计数据区域 - 玻璃拟态效果 */}
|
||||
<Box py={12} position="relative" mt={-20} zIndex={3}>
|
||||
<Container maxW="container.lg">
|
||||
<Box
|
||||
bg="rgba(255, 255, 255, 0.1)"
|
||||
backdropFilter="blur(20px)"
|
||||
borderRadius="xl"
|
||||
border="1px solid"
|
||||
borderColor="rgba(255, 255, 255, 0.2)"
|
||||
boxShadow="0 25px 50px rgba(0, 0, 0, 0.25)"
|
||||
p={8}
|
||||
position="relative"
|
||||
_before={{
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: "xl",
|
||||
background: "linear-gradient(135deg, rgba(172, 106, 255, 0.1) 0%, rgba(255, 200, 118, 0.1) 100%)",
|
||||
zIndex: -1
|
||||
}}
|
||||
>
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={8}>
|
||||
<VStack textAlign="center" spacing={4}>
|
||||
<Heading
|
||||
size="2xl"
|
||||
bgGradient="linear(to-r, brainwave.purple, brainwave.orange)"
|
||||
bgClip="text"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{stats.dataSize}TB
|
||||
</Heading>
|
||||
<Heading size="lg" color="brainwave.n1" fontWeight="semibold">基础数据</Heading>
|
||||
<Text color="brainwave.n2" fontSize="sm" lineHeight={1.6}>
|
||||
我们收集来自全世界的各类数据,打造您的专属智能投资助手
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<VStack textAlign="center" spacing={4}>
|
||||
<Heading
|
||||
size="2xl"
|
||||
bgGradient="linear(to-r, brainwave.green, brainwave.blue)"
|
||||
bgClip="text"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{stats.dataSources}+
|
||||
</Heading>
|
||||
<Heading size="lg" color="brainwave.n1" fontWeight="semibold">数据源</Heading>
|
||||
<Text color="brainwave.n2" fontSize="sm" lineHeight={1.6}>
|
||||
我们即时采集来自300多家数据源的实时数据,随时满足您的投研需求
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<VStack textAlign="center" spacing={4}>
|
||||
<Heading
|
||||
size="2xl"
|
||||
bgGradient="linear(to-r, brainwave.pink, brainwave.red)"
|
||||
bgClip="text"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{stats.researchTargets.toLocaleString()}
|
||||
</Heading>
|
||||
<Heading size="lg" color="brainwave.n1" fontWeight="semibold">研究标的</Heading>
|
||||
<Text color="brainwave.n2" fontSize="sm" lineHeight={1.6}>
|
||||
我们的研究范围涵盖全球主流市场,包括股票、外汇、大宗等交易类型,给您足够宏观的视角
|
||||
</Text>
|
||||
</VStack>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* 特色功能介绍 - Brainwave深色风格 */}
|
||||
<Box as="section" py={20} bg="brainwave.n8" position="relative" overflow="hidden">
|
||||
{/* 背景装饰几何图形 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top="10%"
|
||||
right="-5%"
|
||||
w="300px"
|
||||
h="300px"
|
||||
border="1px solid"
|
||||
borderColor="brainwave.n6"
|
||||
borderRadius="50%"
|
||||
opacity={0.1}
|
||||
zIndex={0}
|
||||
/>
|
||||
<Box
|
||||
position="absolute"
|
||||
bottom="10%"
|
||||
left="-5%"
|
||||
w="200px"
|
||||
h="200px"
|
||||
border="1px solid"
|
||||
borderColor="brainwave.n6"
|
||||
borderRadius="50%"
|
||||
opacity={0.1}
|
||||
zIndex={0}
|
||||
/>
|
||||
|
||||
<Container maxW="container.xl" position="relative" zIndex={1}>
|
||||
<Flex align="center" gap={16}>
|
||||
{/* 左侧功能介绍 - 深色主题版本 */}
|
||||
<Box flex="1" ml="auto">
|
||||
{/* 第一行 */}
|
||||
<SimpleGrid columns={2} spacing={8} mb={12}>
|
||||
<Box>
|
||||
<VStack align="start" spacing={4}>
|
||||
<Box className="icon icon-sm">
|
||||
<Icon viewBox="0 0 40 44" w="25px" h="25px" color="brainwave.purple">
|
||||
<path fill="currentColor" d="M40,40 L36.3636364,40 L36.3636364,3.63636364 L5.45454545,3.63636364 L5.45454545,0 L38.1818182,0 C39.1854545,0 40,0.814545455 40,1.81818182 L40,40 Z" opacity="0.603585379"/>
|
||||
<path fill="currentColor" d="M30.9090909,7.27272727 L1.81818182,7.27272727 C0.814545455,7.27272727 0,8.08727273 0,9.09090909 L0,41.8181818 C0,42.8218182 0.814545455,43.6363636 1.81818182,43.6363636 L30.9090909,43.6363636 C31.9127273,43.6363636 32.7272727,42.8218182 32.7272727,41.8181818 L32.7272727,9.09090909 C32.7272727,8.08727273 31.9127273,7.27272727 30.9090909,7.27272727 Z M18.1818182,34.5454545 L7.27272727,34.5454545 L7.27272727,30.9090909 L18.1818182,30.9090909 L18.1818182,34.5454545 Z M25.4545455,27.2727273 L7.27272727,27.2727273 L7.27272727,23.6363636 L25.4545455,23.6363636 L25.4545455,27.2727273 Z M25.4545455,20 L7.27272727,20 L7.27272727,16.3636364 L25.4545455,16.3636364 L25.4545455,20 Z"/>
|
||||
</Icon>
|
||||
</Box>
|
||||
<Heading size="md" fontWeight="bold" mt={3} color="brainwave.n1">人工智能驱动</Heading>
|
||||
<Text color="brainwave.n2" fontSize="sm" pr="5" lineHeight={1.8}>
|
||||
• 收集海量投研资料和数据,确保信息全面丰富<br/>
|
||||
• 训练专注于投研的大语言模型,专业度领先<br/>
|
||||
• 在金融投资领域表现卓越,优于市面其他模型
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<VStack align="start" spacing={4}>
|
||||
<Box className="icon icon-sm">
|
||||
<Icon viewBox="0 0 45 40" w="25px" h="25px" color="brainwave.orange">
|
||||
<path fill="currentColor" d="M46.7199583,10.7414583 L40.8449583,0.949791667 C40.4909749,0.360605034 39.8540131,0 39.1666667,0 L7.83333333,0 C7.1459869,0 6.50902508,0.360605034 6.15504167,0.949791667 L0.280041667,10.7414583 C0.0969176761,11.0460037 -1.23209662e-05,11.3946378 -1.23209662e-05,11.75 C-0.00758042603,16.0663731 3.48367543,19.5725301 7.80004167,19.5833333 L7.81570833,19.5833333 C9.75003686,19.5882688 11.6168794,18.8726691 13.0522917,17.5760417 C16.0171492,20.2556967 20.5292675,20.2556967 23.494125,17.5760417 C26.4604562,20.2616016 30.9794188,20.2616016 33.94575,17.5760417 C36.2421905,19.6477597 39.5441143,20.1708521 42.3684437,18.9103691 C45.1927731,17.649886 47.0084685,14.8428276 47.0000295,11.75 C47.0000295,11.3946378 46.9030823,11.0460037 46.7199583,10.7414583 Z" opacity="0.598981585"/>
|
||||
<path fill="currentColor" d="M39.198,22.4912623 C37.3776246,22.4928106 35.5817531,22.0149171 33.951625,21.0951667 L33.92225,21.1107282 C31.1430221,22.6838032 27.9255001,22.9318916 24.9844167,21.7998837 C24.4750389,21.605469 23.9777983,21.3722567 23.4960833,21.1018359 L23.4745417,21.1129513 C20.6961809,22.6871153 17.4786145,22.9344611 14.5386667,21.7998837 C14.029926,21.6054643 13.533337,21.3722507 13.0522917,21.1018359 C11.4250962,22.0190609 9.63246555,22.4947009 7.81570833,22.4912623 C7.16510551,22.4842162 6.51607673,22.4173045 5.875,22.2911849 L5.875,44.7220845 C5.875,45.9498589 6.7517757,46.9451667 7.83333333,46.9451667 L19.5833333,46.9451667 L19.5833333,33.6066734 L27.4166667,33.6066734 L27.4166667,46.9451667 L39.1666667,46.9451667 C40.2482243,46.9451667 41.125,45.9498589 41.125,44.7220845 L41.125,22.2822926 C40.4887822,22.4116582 39.8442868,22.4815492 39.198,22.4912623 Z"/>
|
||||
</Icon>
|
||||
</Box>
|
||||
<Heading size="md" fontWeight="bold" mt={3} color="brainwave.n1">投研数据湖</Heading>
|
||||
<Text color="brainwave.n2" fontSize="sm" pr="3" lineHeight={1.8}>
|
||||
• AI Agent 24/7 全天候采集全球数据,确保实时更新<br/>
|
||||
• 整合多种数据源,覆盖范围广泛、信息丰富<br/>
|
||||
• 构建独特数据湖,提供业内无可比拟的数据深度
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* 第二行 */}
|
||||
<SimpleGrid columns={2} spacing={8}>
|
||||
<Box mt={3}>
|
||||
<VStack align="start" spacing={4}>
|
||||
<Box className="icon icon-sm">
|
||||
<Icon viewBox="0 0 42 44" w="25px" h="25px" color="brainwave.green">
|
||||
<path fill="currentColor" d="M18.8086957,4.70034783 C15.3814926,0.343541521 9.0713063,-0.410050841 4.7145,3.01715217 C0.357693695,6.44435519 -0.395898667,12.7545415 3.03130435,17.1113478 C5.53738466,10.3360568 11.6337901,5.54042955 18.8086957,4.70034783 L18.8086957,4.70034783 Z" opacity="0.6"/>
|
||||
<path fill="currentColor" d="M38.9686957,17.1113478 C42.3958987,12.7545415 41.6423063,6.44435519 37.2855,3.01715217 C32.9286937,-0.410050841 26.6185074,0.343541521 23.1913043,4.70034783 C30.3662099,5.54042955 36.4626153,10.3360568 38.9686957,17.1113478 Z" opacity="0.6"/>
|
||||
<path fill="currentColor" d="M34.3815652,34.7668696 C40.2057958,27.7073059 39.5440671,17.3375603 32.869743,11.0755718 C26.1954189,4.81358341 15.8045811,4.81358341 9.13025701,11.0755718 C2.45593289,17.3375603 1.79420418,27.7073059 7.61843478,34.7668696 L3.9753913,40.0506522 C3.58549114,40.5871271 3.51710058,41.2928217 3.79673036,41.8941824 C4.07636014,42.4955431 4.66004722,42.8980248 5.32153275,42.9456105 C5.98301828,42.9931963 6.61830436,42.6784048 6.98113043,42.1232609 L10.2744783,37.3434783 C16.5555112,42.3298213 25.4444888,42.3298213 31.7255217,37.3434783 L35.0188696,42.1196087 C35.6014207,42.9211577 36.7169135,43.1118605 37.53266,42.5493622 C38.3484064,41.9868639 38.5667083,40.8764423 38.0246087,40.047 L34.3815652,34.7668696 Z M30.1304348,25.5652174 L21,25.5652174 C20.49574,25.5652174 20.0869565,25.1564339 20.0869565,24.6521739 L20.0869565,15.5217391 C20.0869565,15.0174791 20.49574,14.6086957 21,14.6086957 C21.50426,14.6086957 21.9130435,15.0174791 21.9130435,15.5217391 L21.9130435,23.7391304 L30.1304348,23.7391304 C30.6346948,23.7391304 31.0434783,24.1479139 31.0434783,24.6521739 C31.0434783,25.1564339 30.6346948,25.5652174 30.1304348,25.5652174 Z"/>
|
||||
</Icon>
|
||||
</Box>
|
||||
<Heading size="md" fontWeight="bold" mt={3} color="brainwave.n1">投研Agent</Heading>
|
||||
<Text color="brainwave.n2" fontSize="sm" pr="5" lineHeight={1.8}>
|
||||
• 采用 AI 模拟人类分析师,智能化程度高<br/>
|
||||
• 具备独特的全球视角,全面审视各类资产<br/>
|
||||
• 提供最佳投资建议,支持科学决策
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
<Box mt={3}>
|
||||
<VStack align="start" spacing={4}>
|
||||
<Box className="icon icon-sm">
|
||||
<Icon viewBox="0 0 42 42" w="25px" h="25px" color="brainwave.blue">
|
||||
<path fill="currentColor" d="M12.25,17.5 L8.75,17.5 L8.75,1.75 C8.75,0.78225 9.53225,0 10.5,0 L31.5,0 C32.46775,0 33.25,0.78225 33.25,1.75 L33.25,12.25 L29.75,12.25 L29.75,3.5 L12.25,3.5 L12.25,17.5 Z" opacity="0.6"/>
|
||||
<path fill="currentColor" d="M40.25,14 L24.5,14 C23.53225,14 22.75,14.78225 22.75,15.75 L22.75,38.5 L19.25,38.5 L19.25,22.75 C19.25,21.78225 18.46775,21 17.5,21 L1.75,21 C0.78225,21 0,21.78225 0,22.75 L0,40.25 C0,41.21775 0.78225,42 1.75,42 L40.25,42 C41.21775,42 42,41.21775 42,40.25 L42,15.75 C42,14.78225 41.21775,14 40.25,14 Z M12.25,36.75 L7,36.75 L7,33.25 L12.25,33.25 L12.25,36.75 Z M12.25,29.75 L7,29.75 L7,26.25 L12.25,26.25 L12.25,29.75 Z M35,36.75 L29.75,36.75 L29.75,33.25 L35,33.25 L35,36.75 Z M35,29.75 L29.75,29.75 L29.75,26.25 L35,26.25 L35,29.75 Z M35,22.75 L29.75,22.75 L29.75,19.25 L35,19.25 L35,22.75 Z"/>
|
||||
</Icon>
|
||||
</Box>
|
||||
<Heading size="md" fontWeight="bold" mt={3} color="brainwave.n1">新闻事件驱动</Heading>
|
||||
<Text color="brainwave.n2" fontSize="sm" pr="3" lineHeight={1.8}>
|
||||
• 基于AI的信息挖掘技术<br/>
|
||||
• Agent 赋能的未来推演和数据关联<br/>
|
||||
• 自由交流,我们相信集体的力量
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 深研系统 → 盈利预测报表 入口 */}
|
||||
<Box mt={3}>
|
||||
<VStack align="start" spacing={3}>
|
||||
<Heading size="md" fontWeight="bold" mt={1} color="brainwave.n1">深研系统</Heading>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="purple"
|
||||
variant="ghost"
|
||||
onClick={() => navigate('/admin/stock-analysis/forecast-report')}
|
||||
>
|
||||
盈利预测报表
|
||||
</Button>
|
||||
</VStack>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
{/* 右侧卡片 - 完全按照原网站设计 */}
|
||||
<Box flex="0 0 auto" w="400px" p={4}>
|
||||
<Box
|
||||
position="relative"
|
||||
borderRadius="xl"
|
||||
overflow="hidden"
|
||||
transform="perspective(1000px) rotateY(-5deg)"
|
||||
boxShadow="2xl"
|
||||
bgImage={`url(${teamWorkingImg})`}
|
||||
bgSize="cover"
|
||||
bgPosition="center"
|
||||
h="400px"
|
||||
>
|
||||
{/* 黑色遮罩 */}
|
||||
<Box
|
||||
position="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
bg="blackAlpha.600"
|
||||
/>
|
||||
|
||||
{/* 内容 */}
|
||||
<Flex
|
||||
direction="column"
|
||||
align="center"
|
||||
justify="center"
|
||||
h="full"
|
||||
position="relative"
|
||||
zIndex={1}
|
||||
color="white"
|
||||
textAlign="center"
|
||||
pt={7}
|
||||
>
|
||||
{/* 3D盒子图标 */}
|
||||
<Box className="icon icon-lg up" mb={3} mt={3}>
|
||||
<Icon viewBox="0 0 42 42" w="50px" h="50px" color="white">
|
||||
<path fill="currentColor" d="M22.7597136,19.3090182 L38.8987031,11.2395234 C39.3926816,10.9925342 39.592906,10.3918611 39.3459167,9.89788265 C39.249157,9.70436312 39.0922432,9.5474453 38.8987261,9.45068056 L20.2741875,0.1378125 L20.2741875,0.1378125 C19.905375,-0.04725 19.469625,-0.04725 19.0995,0.1378125 L3.1011696,8.13815822 C2.60720568,8.38517662 2.40701679,8.98586148 2.6540352,9.4798254 C2.75080129,9.67332903 2.90771305,9.83023153 3.10122239,9.9269862 L21.8652864,19.3090182 C22.1468139,19.4497819 22.4781861,19.4497819 22.7597136,19.3090182 Z"/>
|
||||
<path fill="currentColor" d="M23.625,22.429159 L23.625,39.8805372 C23.625,40.4328219 24.0727153,40.8805372 24.625,40.8805372 C24.7802551,40.8805372 24.9333778,40.8443874 25.0722402,40.7749511 L41.2741875,32.673375 L41.2741875,32.673375 C41.719125,32.4515625 42,31.9974375 42,31.5 L42,14.241659 C42,13.6893742 41.5522847,13.241659 41,13.241659 C40.8447549,13.241659 40.6916418,13.2778041 40.5527864,13.3472318 L24.1777864,21.5347318 C23.8390024,21.7041238 23.625,22.0503869 23.625,22.429159 Z" opacity="0.7"/>
|
||||
<path fill="currentColor" d="M20.4472136,21.5347318 L1.4472136,12.0347318 C0.953235098,11.7877425 0.352562058,11.9879669 0.105572809,12.4819454 C0.0361450918,12.6208008 6.47121774e-16,12.7739139 0,12.929159 L0,30.1875 L0,30.1875 C0,30.6849375 0.280875,31.1390625 0.7258125,31.3621875 L19.5528096,40.7750766 C20.0467945,41.0220531 20.6474623,40.8218132 20.8944388,40.3278283 C20.963859,40.1889789 21,40.0358742 21,39.8806379 L21,22.429159 C21,22.0503869 20.7859976,21.7041238 20.4472136,21.5347318 Z" opacity="0.7"/>
|
||||
</Icon>
|
||||
</Box>
|
||||
|
||||
<Heading size="xl" color="brainwave.n1" className="up" mb={0} lineHeight="1.2">
|
||||
<Text as="span" bgGradient="linear(to-r, brainwave.purple, brainwave.orange)" bgClip="text">
|
||||
事件催化
|
||||
</Text><br />
|
||||
让成功有迹可循
|
||||
</Heading>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate('/community')}
|
||||
bg="linear-gradient(135deg, #AC6AFF, #FFC876)"
|
||||
color="white"
|
||||
size="lg"
|
||||
mt={3}
|
||||
className="up btn-round"
|
||||
borderRadius="full"
|
||||
border="none"
|
||||
_hover={{
|
||||
transform: "translateY(-2px)",
|
||||
boxShadow: "0 8px 32px rgba(172, 106, 255, 0.3)"
|
||||
}}
|
||||
_active={{ transform: "translateY(0)" }}
|
||||
transition="all 0.2s"
|
||||
>
|
||||
访问新闻催化分析
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* AI投研专题应用区域 - Brainwave风格 */}
|
||||
<Box as="section" py={20} bg="brainwave.n7" position="relative">
|
||||
<Container maxW="container.xl">
|
||||
<VStack spacing={12} textAlign="center">
|
||||
<VStack spacing={2}>
|
||||
<Heading size="xl" color="brainwave.n1" mb={0}>
|
||||
AI投研专题应用
|
||||
</Heading>
|
||||
<Heading
|
||||
size="xl"
|
||||
bgGradient="linear(to-r, brainwave.orange, brainwave.purple)"
|
||||
bgClip="text"
|
||||
fontWeight="bold"
|
||||
>
|
||||
By 价小前投研
|
||||
</Heading>
|
||||
<Text fontSize="lg" color="brainwave.n2" fontWeight="medium">
|
||||
人工智能+专业投研流程—最强投资AI助手
|
||||
</Text>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
{/* 页脚 - Brainwave深色主题 */}
|
||||
<Box as="footer" bg="brainwave.n8" color="brainwave.n1" py={16}>
|
||||
<Container maxW="container.xl">
|
||||
<SimpleGrid columns={{ base: 2, md: 5 }} spacing={8}>
|
||||
{/* 价值前沿 */}
|
||||
<VStack align="start" spacing={4}>
|
||||
<Heading size="md" bgGradient="linear(to-r, brainwave.purple, brainwave.orange)" bgClip="text">价值前沿</Heading>
|
||||
<Text fontSize="sm" color="brainwave.n2" fontWeight="bold">
|
||||
更懂投资者的AI投研平台
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
{/* 关于我们 */}
|
||||
<VStack align="start" spacing={4}>
|
||||
<Heading size="sm" color="brainwave.orange">关于我们</Heading>
|
||||
<VStack align="start" spacing={2}>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.orange" }}>公司介绍</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.orange" }}>团队架构</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.orange" }}>联系方式</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.orange" }}>反馈评价</Link>
|
||||
</VStack>
|
||||
</VStack>
|
||||
|
||||
{/* 免费资源 */}
|
||||
<VStack align="start" spacing={4}>
|
||||
<Heading size="sm" color="brainwave.green">免费资源</Heading>
|
||||
<VStack align="start" spacing={2}>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.green" }}>投研日报</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.green" }}>资讯速递</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.green" }}>免费试用</Link>
|
||||
</VStack>
|
||||
</VStack>
|
||||
|
||||
{/* 产品介绍 */}
|
||||
<VStack align="start" spacing={4}>
|
||||
<Heading size="sm" color="brainwave.blue">产品介绍</Heading>
|
||||
<VStack align="start" spacing={2}>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.blue" }}>行情复盘</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.blue" }}>高频跟踪</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.blue" }}>深研系统</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.blue" }}>了解更多</Link>
|
||||
</VStack>
|
||||
</VStack>
|
||||
|
||||
{/* 产品下载 */}
|
||||
<VStack align="start" spacing={4}>
|
||||
<Heading size="sm" color="brainwave.pink">产品下载</Heading>
|
||||
<VStack align="start" spacing={2}>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.pink" }}>手机APP</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.pink" }}>Win终端</Link>
|
||||
<Link fontSize="sm" color="brainwave.n3" _hover={{ color: "brainwave.pink" }}>Mac终端</Link>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</SimpleGrid>
|
||||
|
||||
{/* 版权信息 */}
|
||||
<Divider my={8} />
|
||||
<Text textAlign="center" fontSize="sm" color="brainwave.n4">
|
||||
All rights reserved. Copyright © {new Date().getFullYear()} 投研系统 by{' '}
|
||||
<Link color="brainwave.orange" _hover={{ textDecoration: "underline" }}>
|
||||
价值前沿
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
import plugin from "tailwindcss/plugin";
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/templates/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/components/**/*.{js,jsx,ts,tsx}",
|
||||
"./src/views/**/*.{js,jsx,ts,tsx}",
|
||||
"./public/index.html",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
color: {
|
||||
1: "#AC6AFF",
|
||||
2: "#FFC876",
|
||||
3: "#FF776F",
|
||||
4: "#7ADB78",
|
||||
5: "#858DFF",
|
||||
6: "#FF98E2",
|
||||
},
|
||||
stroke: {
|
||||
1: "#26242C",
|
||||
},
|
||||
n: {
|
||||
1: "#FFFFFF",
|
||||
2: "#CAC6DD",
|
||||
3: "#ADA8C3",
|
||||
4: "#757185",
|
||||
5: "#3F3A52",
|
||||
6: "#252134",
|
||||
7: "#15131D",
|
||||
8: "#0E0C15",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sora)", ...fontFamily.sans],
|
||||
code: "var(--font-code)",
|
||||
grotesk: "var(--font-grotesk)",
|
||||
},
|
||||
letterSpacing: {
|
||||
tagline: ".15em",
|
||||
},
|
||||
spacing: {
|
||||
0.25: "0.0625rem",
|
||||
7.5: "1.875rem",
|
||||
15: "3.75rem",
|
||||
},
|
||||
opacity: {
|
||||
15: ".15",
|
||||
},
|
||||
transitionDuration: {
|
||||
DEFAULT: "200ms",
|
||||
},
|
||||
transitionTimingFunction: {
|
||||
DEFAULT: "linear",
|
||||
},
|
||||
zIndex: {
|
||||
1: "1",
|
||||
2: "2",
|
||||
3: "3",
|
||||
4: "4",
|
||||
5: "5",
|
||||
},
|
||||
borderWidth: {
|
||||
DEFAULT: "0.0625rem",
|
||||
},
|
||||
backgroundImage: {
|
||||
"radial-gradient": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"conic-gradient":
|
||||
"conic-gradient(from 225deg, #FFC876, #79FFF7, #9F53FF, #FF98E2, #FFC876)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
plugin(function ({ addBase, addComponents, addUtilities }) {
|
||||
addBase({});
|
||||
addComponents({
|
||||
".container": {
|
||||
"@apply max-w-[77.5rem] mx-auto px-5 md:px-10 lg:px-15 xl:max-w-[87.5rem]":
|
||||
{},
|
||||
},
|
||||
".h1": {
|
||||
"@apply font-semibold text-[2.5rem] leading-[3.25rem] md:text-[2.75rem] md:leading-[3.75rem] lg:text-[3.25rem] lg:leading-[4.0625rem] xl:text-[3.75rem] xl:leading-[4.5rem]":
|
||||
{},
|
||||
},
|
||||
".h2": {
|
||||
"@apply text-[1.75rem] leading-[2.5rem] md:text-[2rem] md:leading-[2.5rem] lg:text-[2.5rem] lg:leading-[3.5rem] xl:text-[3rem] xl:leading-tight":
|
||||
{},
|
||||
},
|
||||
".h3": {
|
||||
"@apply text-[2rem] leading-normal md:text-[2.5rem]": {},
|
||||
},
|
||||
".h4": {
|
||||
"@apply text-[2rem] leading-normal": {},
|
||||
},
|
||||
".h5": {
|
||||
"@apply text-2xl leading-normal": {},
|
||||
},
|
||||
".h6": {
|
||||
"@apply font-semibold text-lg leading-8": {},
|
||||
},
|
||||
".body-1": {
|
||||
"@apply text-[0.875rem] leading-[1.5rem] md:text-[1rem] md:leading-[1.75rem] lg:text-[1.25rem] lg:leading-8":
|
||||
{},
|
||||
},
|
||||
".body-2": {
|
||||
"@apply font-light text-[0.875rem] leading-6 md:text-base":
|
||||
{},
|
||||
},
|
||||
".caption": {
|
||||
"@apply text-sm": {},
|
||||
},
|
||||
".tagline": {
|
||||
"@apply font-grotesk font-light text-xs tracking-tagline uppercase":
|
||||
{},
|
||||
},
|
||||
".quote": {
|
||||
"@apply font-code text-lg leading-normal": {},
|
||||
},
|
||||
".button": {
|
||||
"@apply font-code text-xs font-bold uppercase tracking-wider":
|
||||
{},
|
||||
},
|
||||
});
|
||||
addUtilities({
|
||||
".tap-highlight-color": {
|
||||
"-webkit-tap-highlight-color": "rgba(0, 0, 0, 0)",
|
||||
},
|
||||
});
|
||||
}),
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user