60sapi接口搭建完毕,数据库连接测试成功,登录注册部分简单完成
This commit is contained in:
88
frontend/react-app/src/App.js
vendored
Normal file
88
frontend/react-app/src/App.js
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import styled from 'styled-components';
|
||||
|
||||
// 页面组件
|
||||
import HomePage from './pages/HomePage';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import Api60sPage from './pages/Api60sPage';
|
||||
import SmallGamePage from './pages/SmallGamePage';
|
||||
import AiModelPage from './pages/AiModelPage';
|
||||
|
||||
// 公共组件
|
||||
import Header from './components/Header';
|
||||
import Navigation from './components/Navigation';
|
||||
import Footer from './components/Footer';
|
||||
|
||||
// 上下文
|
||||
import { UserProvider } from './contexts/UserContext';
|
||||
|
||||
// 样式
|
||||
import './styles/global.css';
|
||||
|
||||
const AppContainer = styled.div`
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #a8e6cf 0%, #dcedc1 50%, #ffd3a5 100%);
|
||||
`;
|
||||
|
||||
const MainContent = styled.main`
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<UserProvider>
|
||||
<Router>
|
||||
<AppContainer>
|
||||
<Header />
|
||||
<MainContent>
|
||||
<Routes>
|
||||
{/* 主要页面 */}
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/60sapi" element={<Api60sPage />} />
|
||||
<Route path="/smallgame" element={<SmallGamePage />} />
|
||||
<Route path="/aimodel" element={<AiModelPage />} />
|
||||
</Routes>
|
||||
</MainContent>
|
||||
<Navigation />
|
||||
<Footer />
|
||||
|
||||
{/* 全局提示组件 */}
|
||||
<Toaster
|
||||
position="top-center"
|
||||
toastOptions={{
|
||||
duration: 3000,
|
||||
style: {
|
||||
background: '#333',
|
||||
color: '#fff',
|
||||
borderRadius: '10px',
|
||||
padding: '16px',
|
||||
fontSize: '14px'
|
||||
},
|
||||
success: {
|
||||
iconTheme: {
|
||||
primary: '#4ade80',
|
||||
secondary: '#fff'
|
||||
}
|
||||
},
|
||||
error: {
|
||||
iconTheme: {
|
||||
primary: '#ef4444',
|
||||
secondary: '#fff'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</AppContainer>
|
||||
</Router>
|
||||
</UserProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
115
frontend/react-app/src/components/Footer.js
vendored
Normal file
115
frontend/react-app/src/components/Footer.js
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FooterContainer = styled.footer`
|
||||
background: #1f2937;
|
||||
color: #d1d5db;
|
||||
padding: 40px 0 80px; /* 底部留出导航栏空间 */
|
||||
margin-top: 60px;
|
||||
|
||||
@media (min-width: 769px) {
|
||||
padding: 40px 0 20px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterContent = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
const FooterInfo = styled.div`
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const FooterTitle = styled.h3`
|
||||
color: white;
|
||||
margin-bottom: 12px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const FooterDescription = styled.p`
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
color: #9ca3af;
|
||||
`;
|
||||
|
||||
const FooterLinks = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
gap: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterLink = styled.a`
|
||||
color: #d1d5db;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
`;
|
||||
|
||||
const FooterDivider = styled.div`
|
||||
height: 1px;
|
||||
background: #374151;
|
||||
margin: 24px 0;
|
||||
`;
|
||||
|
||||
const FooterBottom = styled.div`
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
const Copyright = styled.p`
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const ICP = styled.p`
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const Footer = () => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<FooterContainer>
|
||||
<FooterContent>
|
||||
<FooterInfo>
|
||||
<FooterTitle>✨ 神奇万事通 ✨</FooterTitle>
|
||||
<FooterDescription>
|
||||
🎨 一个多功能的聚合软件应用 💬
|
||||
</FooterDescription>
|
||||
</FooterInfo>
|
||||
|
||||
<FooterLinks>
|
||||
<FooterLink href="/60sapi">📡聚合应用</FooterLink>
|
||||
<FooterLink href="/smallgame">🎮小游戏</FooterLink>
|
||||
<FooterLink href="/aimodel">🤖AI工具</FooterLink>
|
||||
</FooterLinks>
|
||||
|
||||
<FooterDivider />
|
||||
|
||||
<FooterBottom>
|
||||
<Copyright>
|
||||
📄 蜀ICP备2025151694号 | Copyright © 2025-{currentYear} ✨
|
||||
</Copyright>
|
||||
</FooterBottom>
|
||||
</FooterContent>
|
||||
</FooterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
349
frontend/react-app/src/components/Header.js
vendored
Normal file
349
frontend/react-app/src/components/Header.js
vendored
Normal file
@@ -0,0 +1,349 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { FiUser, FiMenu, FiX, FiLogOut } from 'react-icons/fi';
|
||||
import { useUser } from '../contexts/UserContext';
|
||||
|
||||
const HeaderContainer = styled.header`
|
||||
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
||||
color: white;
|
||||
padding: 12px 0;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
const HeaderContent = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const Logo = styled(Link)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
|
||||
.logo-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 18px;
|
||||
|
||||
.logo-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Nav = styled.nav`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavLink = styled(Link)`
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
`;
|
||||
|
||||
const UserSection = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
`;
|
||||
|
||||
const UserButton = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 8px 12px;
|
||||
|
||||
.user-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const UserAvatar = styled.img`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
object-fit: cover;
|
||||
`;
|
||||
|
||||
const UserInfo = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.user-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenuButton = styled.button`
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenu = styled.div.withConfig({
|
||||
shouldForwardProp: (prop) => prop !== 'isOpen'
|
||||
})`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2000;
|
||||
display: ${props => props.isOpen ? 'block' : 'none'};
|
||||
|
||||
@media (min-width: 769px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const MobileMenuContent = styled.div.withConfig({
|
||||
shouldForwardProp: (prop) => prop !== 'isOpen'
|
||||
})`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
background: white;
|
||||
transform: translateX(${props => props.isOpen ? '0' : '100%'});
|
||||
transition: transform 0.3s ease;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const MobileMenuHeader = styled.div`
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
`;
|
||||
|
||||
const MobileMenuTitle = styled.h3`
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const MobileNavLink = styled(Link)`
|
||||
display: block;
|
||||
color: #374151;
|
||||
text-decoration: none;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const Header = () => {
|
||||
const { user, isLoggedIn, logout, getQQAvatar } = useUser();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
setIsMenuOpen(false);
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const handleMenuToggle = () => {
|
||||
setIsMenuOpen(!isMenuOpen);
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderContainer>
|
||||
<HeaderContent>
|
||||
<Logo to="/">
|
||||
<span className="logo-icon">✨</span>
|
||||
神奇万事通
|
||||
</Logo>
|
||||
|
||||
<Nav>
|
||||
<NavLink to="/60sapi">60s API</NavLink>
|
||||
<NavLink to="/smallgame">小游戏</NavLink>
|
||||
<NavLink to="/aimodel">AI模型</NavLink>
|
||||
</Nav>
|
||||
|
||||
<UserSection>
|
||||
{isLoggedIn && user ? (
|
||||
<>
|
||||
<UserInfo>
|
||||
{getQQAvatar(user.account) ? (
|
||||
<UserAvatar
|
||||
src={getQQAvatar(user.account)}
|
||||
alt="QQ头像"
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
e.target.nextSibling.style.display = 'inline';
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<FiUser style={{ display: getQQAvatar(user.account) ? 'none' : 'inline' }} />
|
||||
<span className="user-name">{user.account}</span>
|
||||
</UserInfo>
|
||||
<UserButton onClick={handleLogout}>
|
||||
<FiLogOut size={16} />
|
||||
<span className="user-text">退出</span>
|
||||
</UserButton>
|
||||
</>
|
||||
) : (
|
||||
<UserButton as={Link} to="/login">
|
||||
<FiUser />
|
||||
<span className="user-text">登录</span>
|
||||
</UserButton>
|
||||
)}
|
||||
|
||||
<MobileMenuButton onClick={handleMenuToggle}>
|
||||
<FiMenu />
|
||||
</MobileMenuButton>
|
||||
</UserSection>
|
||||
</HeaderContent>
|
||||
</HeaderContainer>
|
||||
|
||||
<MobileMenu isOpen={isMenuOpen} onClick={handleMenuClose}>
|
||||
<MobileMenuContent isOpen={isMenuOpen} onClick={(e) => e.stopPropagation()}>
|
||||
<MobileMenuHeader>
|
||||
<MobileMenuTitle>菜单</MobileMenuTitle>
|
||||
<CloseButton onClick={handleMenuClose}>
|
||||
<FiX />
|
||||
</CloseButton>
|
||||
</MobileMenuHeader>
|
||||
|
||||
<MobileNavLink to="/" onClick={handleMenuClose}>
|
||||
🏠 首页
|
||||
</MobileNavLink>
|
||||
<MobileNavLink to="/60sapi" onClick={handleMenuClose}>
|
||||
📡 60s API
|
||||
</MobileNavLink>
|
||||
<MobileNavLink to="/smallgame" onClick={handleMenuClose}>
|
||||
🎮 小游戏
|
||||
</MobileNavLink>
|
||||
<MobileNavLink to="/aimodel" onClick={handleMenuClose}>
|
||||
🤖 AI模型
|
||||
</MobileNavLink>
|
||||
|
||||
{isLoggedIn && user ? (
|
||||
<>
|
||||
<div style={{
|
||||
padding: '12px 0',
|
||||
borderBottom: '1px solid #f3f4f6',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}>
|
||||
{getQQAvatar(user.account) ? (
|
||||
<UserAvatar
|
||||
src={getQQAvatar(user.account)}
|
||||
alt="QQ头像"
|
||||
style={{ width: '32px', height: '32px' }}
|
||||
/>
|
||||
) : (
|
||||
<FiUser size={24} color="#666" />
|
||||
)}
|
||||
<span style={{ color: '#374151', fontWeight: '500' }}>{user.account}</span>
|
||||
</div>
|
||||
<MobileNavLink as="button"
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
color: '#ef4444'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleLogout();
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
🚪 退出登录
|
||||
</MobileNavLink>
|
||||
</>
|
||||
) : (
|
||||
<MobileNavLink to="/login" onClick={handleMenuClose}>
|
||||
👤 登录注册
|
||||
</MobileNavLink>
|
||||
)}
|
||||
</MobileMenuContent>
|
||||
</MobileMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
126
frontend/react-app/src/components/Navigation.js
vendored
Normal file
126
frontend/react-app/src/components/Navigation.js
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { FiHome, FiActivity, FiGrid, FiCpu } from 'react-icons/fi';
|
||||
|
||||
const NavigationContainer = styled.nav`
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-top: 1px solid rgba(168, 230, 207, 0.3);
|
||||
padding: 8px 0 calc(8px + env(safe-area-inset-bottom));
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -4px 20px rgba(168, 230, 207, 0.2);
|
||||
backdrop-filter: blur(15px);
|
||||
|
||||
@media (min-width: 769px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavList = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
const NavItem = styled(Link).withConfig({
|
||||
shouldForwardProp: (prop) => prop !== 'isActive'
|
||||
})`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: ${props => props.isActive ? '#66bb6a' : '#6b7280'};
|
||||
transition: all 0.2s ease;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
min-width: 60px;
|
||||
|
||||
&:hover {
|
||||
color: #66bb6a;
|
||||
background: rgba(129, 199, 132, 0.1);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-text {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
${props => props.isActive && `
|
||||
.nav-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Navigation = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
path: '/',
|
||||
icon: FiHome,
|
||||
text: '首页',
|
||||
exact: true
|
||||
},
|
||||
{
|
||||
path: '/60sapi',
|
||||
icon: FiActivity,
|
||||
text: '60s API'
|
||||
},
|
||||
{
|
||||
path: '/smallgame',
|
||||
icon: FiGrid,
|
||||
text: '小游戏'
|
||||
},
|
||||
{
|
||||
path: '/aimodel',
|
||||
icon: FiCpu,
|
||||
text: 'AI模型'
|
||||
}
|
||||
];
|
||||
|
||||
const isActive = (path, exact = false) => {
|
||||
if (exact) {
|
||||
return location.pathname === path;
|
||||
}
|
||||
return location.pathname.startsWith(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<NavList>
|
||||
{navItems.map((item) => {
|
||||
const IconComponent = item.icon;
|
||||
const active = isActive(item.path, item.exact);
|
||||
|
||||
return (
|
||||
<NavItem
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
isActive={active}
|
||||
>
|
||||
<IconComponent className="nav-icon" />
|
||||
<span className="nav-text">{item.text}</span>
|
||||
</NavItem>
|
||||
);
|
||||
})}
|
||||
</NavList>
|
||||
</NavigationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navigation;
|
||||
118
frontend/react-app/src/contexts/UserContext.js
vendored
Normal file
118
frontend/react-app/src/contexts/UserContext.js
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { authAPI } from '../utils/api';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const UserContext = createContext();
|
||||
|
||||
export const useUser = () => {
|
||||
const context = useContext(UserContext);
|
||||
if (!context) {
|
||||
throw new Error('useUser must be used within a UserProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const UserProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
|
||||
// 检查登录状态
|
||||
const checkLoginStatus = async () => {
|
||||
try {
|
||||
const response = await authAPI.checkLogin();
|
||||
if (response.data.success && response.data.logged_in) {
|
||||
const userData = response.data.user;
|
||||
setUser(userData);
|
||||
setIsLoggedIn(true);
|
||||
} else {
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查登录状态失败:', error);
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 登录
|
||||
const login = async (loginData) => {
|
||||
try {
|
||||
const response = await authAPI.login(loginData);
|
||||
if (response.data.success) {
|
||||
const userData = response.data.user;
|
||||
setUser(userData);
|
||||
setIsLoggedIn(true);
|
||||
toast.success('登录成功!');
|
||||
return { success: true };
|
||||
} else {
|
||||
toast.error(response.data.message || '登录失败');
|
||||
return { success: false, message: response.data.message };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
const message = error.response?.data?.message || '登录失败,请重试';
|
||||
toast.error(message);
|
||||
return { success: false, message };
|
||||
}
|
||||
};
|
||||
|
||||
// 登出
|
||||
const logout = async () => {
|
||||
try {
|
||||
await authAPI.logout();
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
toast.success('已成功登出');
|
||||
} catch (error) {
|
||||
console.error('登出失败:', error);
|
||||
// 即使登出请求失败,也清除本地状态
|
||||
setUser(null);
|
||||
setIsLoggedIn(false);
|
||||
toast.error('登出失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 获取QQ头像URL
|
||||
const getQQAvatar = (email) => {
|
||||
if (!email) return null;
|
||||
|
||||
const qqDomains = ['qq.com', 'vip.qq.com', 'foxmail.com'];
|
||||
const domain = email.split('@')[1]?.toLowerCase();
|
||||
|
||||
if (qqDomains.includes(domain)) {
|
||||
const qqNumber = email.split('@')[0];
|
||||
if (/^\d+$/.test(qqNumber)) {
|
||||
return `http://q1.qlogo.cn/g?b=qq&nk=${qqNumber}&s=100`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// 组件挂载时检查登录状态
|
||||
useEffect(() => {
|
||||
checkLoginStatus();
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
user,
|
||||
isLoading,
|
||||
isLoggedIn,
|
||||
login,
|
||||
logout,
|
||||
checkLoginStatus,
|
||||
getQQAvatar
|
||||
};
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={value}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserContext;
|
||||
11
frontend/react-app/src/index.js
vendored
Normal file
11
frontend/react-app/src/index.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './styles/index.css';
|
||||
import App from './App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
118
frontend/react-app/src/md/前端邮件功能测试指南.md
Normal file
118
frontend/react-app/src/md/前端邮件功能测试指南.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 前端邮件功能测试指南
|
||||
|
||||
## 问题修复说明
|
||||
|
||||
### 修复的问题
|
||||
- **响应拦截器问题**:修复了 `api.js` 中响应拦截器直接返回 `response.data` 导致前端无法正确访问 `response.data.success` 的问题
|
||||
- **API响应格式不匹配**:现在前端代码可以正确处理后端返回的响应格式
|
||||
|
||||
### 修复内容
|
||||
在 `src/utils/api.js` 文件中:
|
||||
```javascript
|
||||
// 修复前
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data; // 这里直接返回了data,导致前端无法访问response.data.success
|
||||
},
|
||||
// ...
|
||||
);
|
||||
|
||||
// 修复后
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response; // 现在返回完整的response对象
|
||||
},
|
||||
// ...
|
||||
);
|
||||
```
|
||||
|
||||
## 测试步骤
|
||||
|
||||
### 1. 启动服务
|
||||
确保以下服务正在运行:
|
||||
- **后端服务**:`http://localhost:5000`
|
||||
- **前端服务**:`http://localhost:3001`
|
||||
|
||||
### 2. 测试注册功能
|
||||
1. 打开浏览器访问 `http://localhost:3001`
|
||||
2. 点击登录按钮或直接访问 `/login` 页面
|
||||
3. 切换到「注册」标签
|
||||
4. 填写以下信息:
|
||||
- **邮箱**:输入有效的QQ邮箱(如:your_qq@qq.com)
|
||||
- **用户名**:输入用户名
|
||||
- **密码**:输入密码(至少6位)
|
||||
- **确认密码**:再次输入相同密码
|
||||
5. 点击「发送验证码」按钮
|
||||
6. 检查是否显示成功提示:"验证码已发送到您的邮箱"
|
||||
7. 检查邮箱是否收到验证码邮件
|
||||
8. 输入收到的验证码
|
||||
9. 点击「注册」按钮完成注册
|
||||
|
||||
### 3. 测试登录功能(验证码登录)
|
||||
1. 在登录页面选择「验证码登录」
|
||||
2. 输入已注册的QQ邮箱
|
||||
3. 点击「发送验证码」按钮
|
||||
4. 检查是否显示成功提示
|
||||
5. 检查邮箱是否收到登录验证码
|
||||
6. 输入验证码并点击「登录」
|
||||
|
||||
### 4. 测试登录功能(密码登录)
|
||||
1. 在登录页面选择「密码登录」
|
||||
2. 输入邮箱和密码
|
||||
3. 点击「登录」按钮
|
||||
|
||||
## 预期结果
|
||||
|
||||
### 成功的表现
|
||||
- ✅ 点击「发送验证码」后显示绿色成功提示
|
||||
- ✅ 倒计时正常显示(60秒)
|
||||
- ✅ 邮箱收到格式正确的验证码邮件
|
||||
- ✅ 后端日志显示:"验证码邮件发送成功: your_email@qq.com"
|
||||
- ✅ 验证码验证成功,注册/登录流程完整
|
||||
|
||||
### 失败的表现
|
||||
- ❌ 显示红色错误提示
|
||||
- ❌ 邮箱未收到验证码
|
||||
- ❌ 后端日志显示SMTP错误
|
||||
|
||||
## 技术细节
|
||||
|
||||
### API调用流程
|
||||
1. 前端调用 `authAPI.sendVerification(data)`
|
||||
2. 请求发送到 `/api/auth/send-verification`
|
||||
3. 后端处理邮件发送
|
||||
4. 返回响应格式:`{ success: true/false, message: "...", data: {...} }`
|
||||
5. 前端通过 `response.data.success` 判断是否成功
|
||||
|
||||
### 环境变量要求
|
||||
确保后端设置了正确的环境变量:
|
||||
```bash
|
||||
MAIL_USERNAME=your_qq_email@qq.com
|
||||
MAIL_PASSWORD=your_qq_auth_code
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 如果仍然无法发送邮件
|
||||
1. 检查后端环境变量是否正确设置
|
||||
2. 确认QQ邮箱已开启SMTP服务并获取授权码
|
||||
3. 检查网络连接是否正常
|
||||
4. 查看浏览器开发者工具的Network标签,确认API请求状态
|
||||
5. 查看后端控制台日志,确认具体错误信息
|
||||
|
||||
### 常见错误
|
||||
- **535 Authentication failed**:QQ邮箱授权码错误
|
||||
- **Network Error**:前后端连接问题
|
||||
- **Timeout**:网络超时或SMTP服务器响应慢
|
||||
|
||||
## 注意事项
|
||||
- 仅支持QQ邮箱系列(qq.com、vip.qq.com、foxmail.com)
|
||||
- 验证码有效期为10分钟
|
||||
- 同一邮箱60秒内只能发送一次验证码
|
||||
- 验证码最多尝试5次
|
||||
|
||||
---
|
||||
|
||||
**修复完成时间**:2025年9月2日
|
||||
**修复内容**:API响应拦截器格式问题
|
||||
**测试状态**:✅ 后端功能正常,前端API调用已修复
|
||||
287
frontend/react-app/src/pages/AiModelPage.js
vendored
Normal file
287
frontend/react-app/src/pages/AiModelPage.js
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { FiCpu, FiLock, FiMessageCircle, FiImage, FiFileText, FiUser } from 'react-icons/fi';
|
||||
import { useUser } from '../contexts/UserContext';
|
||||
|
||||
const AiContainer = styled.div`
|
||||
min-height: calc(100vh - 140px);
|
||||
padding: 20px 0;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
const PageHeader = styled.div`
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 40px 20px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||
border-radius: 16px;
|
||||
`;
|
||||
|
||||
const PageTitle = styled.h1`
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-emoji {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const PageDescription = styled.p`
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
line-height: 1.6;
|
||||
`;
|
||||
|
||||
const LoginPrompt = styled.div`
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
const LoginIcon = styled.div`
|
||||
font-size: 64px;
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const LoginTitle = styled.h2`
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const LoginText = styled.p`
|
||||
color: #6b7280;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const LoginButton = styled.button`
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 14px 32px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
`;
|
||||
|
||||
const FeatureGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
const FeatureCard = styled.div`
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
`;
|
||||
|
||||
const FeatureIcon = styled.div`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const FeatureTitle = styled.h3`
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const FeatureDescription = styled.p`
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
const FeatureStatus = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #f59e0b;
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
const LockOverlay = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(2px);
|
||||
`;
|
||||
|
||||
const LockIcon = styled.div`
|
||||
font-size: 32px;
|
||||
color: #9ca3af;
|
||||
`;
|
||||
|
||||
const AiModelPage = () => {
|
||||
const { isLoggedIn, isLoading } = useUser();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogin = () => {
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const aiFeatures = [
|
||||
{
|
||||
icon: <FiMessageCircle />,
|
||||
title: 'AI对话助手',
|
||||
description: '智能对话机器人,回答问题、提供建议、进行闲聊',
|
||||
status: '开发中'
|
||||
},
|
||||
{
|
||||
icon: <FiFileText />,
|
||||
title: '智能文本生成',
|
||||
description: '根据提示生成文章、总结、翻译等文本内容',
|
||||
status: '开发中'
|
||||
},
|
||||
{
|
||||
icon: <FiImage />,
|
||||
title: '图像识别分析',
|
||||
description: '上传图片进行内容识别、文字提取、场景分析',
|
||||
status: '规划中'
|
||||
},
|
||||
{
|
||||
icon: <FiCpu />,
|
||||
title: '数据智能处理',
|
||||
description: '自动化数据分析、图表生成、趋势预测',
|
||||
status: '规划中'
|
||||
}
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<AiContainer>
|
||||
<Container>
|
||||
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
||||
<div className="spinner"></div>
|
||||
<p style={{ marginTop: '16px', color: '#6b7280' }}>加载中...</p>
|
||||
</div>
|
||||
</Container>
|
||||
</AiContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AiContainer>
|
||||
<Container>
|
||||
<PageHeader>
|
||||
<PageTitle>
|
||||
<span className="title-emoji">🤖</span>
|
||||
AI模型
|
||||
<span className="title-emoji">🤖</span>
|
||||
</PageTitle>
|
||||
<PageDescription>
|
||||
智能AI工具和模型应用,提供对话、文本生成、图像识别等功能
|
||||
</PageDescription>
|
||||
</PageHeader>
|
||||
|
||||
{!isLoggedIn ? (
|
||||
<LoginPrompt>
|
||||
<LoginIcon>🔒</LoginIcon>
|
||||
<LoginTitle>需要登录访问</LoginTitle>
|
||||
<LoginText>
|
||||
AI模型功能需要登录后才能使用,请先登录您的账户。
|
||||
<br />
|
||||
登录后即可体验强大的AI工具和服务。
|
||||
</LoginText>
|
||||
<LoginButton onClick={handleLogin}>
|
||||
<FiUser />
|
||||
立即登录
|
||||
</LoginButton>
|
||||
</LoginPrompt>
|
||||
) : (
|
||||
<LoginPrompt>
|
||||
<LoginIcon>🚧</LoginIcon>
|
||||
<LoginTitle>功能开发中</LoginTitle>
|
||||
<LoginText>
|
||||
AI模型功能正在紧张开发中,即将为您带来强大的人工智能体验。
|
||||
<br />
|
||||
感谢您的耐心等待!
|
||||
</LoginText>
|
||||
</LoginPrompt>
|
||||
)}
|
||||
|
||||
<FeatureGrid>
|
||||
{aiFeatures.map((feature, index) => (
|
||||
<FeatureCard key={index}>
|
||||
<FeatureIcon>
|
||||
{feature.icon}
|
||||
</FeatureIcon>
|
||||
<FeatureTitle>{feature.title}</FeatureTitle>
|
||||
<FeatureDescription>{feature.description}</FeatureDescription>
|
||||
<FeatureStatus>
|
||||
<span>⏳</span>
|
||||
{feature.status}
|
||||
</FeatureStatus>
|
||||
|
||||
{!isLoggedIn && (
|
||||
<LockOverlay>
|
||||
<LockIcon>
|
||||
<FiLock />
|
||||
</LockIcon>
|
||||
</LockOverlay>
|
||||
)}
|
||||
</FeatureCard>
|
||||
))}
|
||||
</FeatureGrid>
|
||||
</Container>
|
||||
</AiContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AiModelPage;
|
||||
427
frontend/react-app/src/pages/Api60sPage.js
vendored
Normal file
427
frontend/react-app/src/pages/Api60sPage.js
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FiActivity, FiStar, FiExternalLink, FiArrowLeft } from 'react-icons/fi';
|
||||
|
||||
const Api60sContainer = styled.div`
|
||||
min-height: calc(100vh - 140px);
|
||||
padding: 20px 0;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
`;
|
||||
|
||||
const Subtitle = styled.p`
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 18px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const CategorySection = styled.div`
|
||||
margin-bottom: 50px;
|
||||
`;
|
||||
|
||||
const CategoryTitle = styled.h2`
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
`;
|
||||
|
||||
const CategoryGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ApiCard = styled.div`
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: ${props => props.color || 'linear-gradient(135deg, #81c784 0%, #a5d6a7 100%)'};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`;
|
||||
|
||||
const CardHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const CardIcon = styled.div`
|
||||
font-size: 20px;
|
||||
color: ${props => props.color || '#66bb6a'};
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
const CardTitle = styled.h3`
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #2e7d32;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
line-height: 1.3;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ExternalIcon = styled.div`
|
||||
font-size: 14px;
|
||||
color: #81c784;
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const EmbeddedContainer = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const EmbeddedContent = styled.div`
|
||||
background: white;
|
||||
border-radius: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
`;
|
||||
|
||||
const EmbeddedHeader = styled.div`
|
||||
background: linear-gradient(135deg, #4caf50, #66bb6a);
|
||||
color: white;
|
||||
padding: 15px 20px;
|
||||
padding-top: max(15px, env(safe-area-inset-top));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
z-index: 1001;
|
||||
`;
|
||||
|
||||
const BackButton = styled.button`
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
`;
|
||||
|
||||
const EmbeddedFrame = styled.iframe`
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
border: none;
|
||||
background: white;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
const Api60sPage = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [apiCategories, setApiCategories] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [embeddedApi, setEmbeddedApi] = useState(null);
|
||||
|
||||
// 动态扫描60sapi文件夹
|
||||
const scanApiModules = async () => {
|
||||
try {
|
||||
// 定义分类配置
|
||||
const categoryConfig = {
|
||||
'热搜榜单': {
|
||||
icon: <FiStar />,
|
||||
color: '#66bb6a'
|
||||
},
|
||||
'日更资讯': {
|
||||
icon: <FiStar />,
|
||||
color: '#4caf50'
|
||||
},
|
||||
'实用功能': {
|
||||
icon: <FiStar />,
|
||||
color: '#388e3c'
|
||||
},
|
||||
'娱乐消遣': {
|
||||
icon: <FiActivity />,
|
||||
color: '#66bb6a'
|
||||
}
|
||||
};
|
||||
|
||||
// 颜色渐变配置
|
||||
const gradientColors = [
|
||||
'linear-gradient(135deg, #81c784 0%, #66bb6a 100%)',
|
||||
'linear-gradient(135deg, #a5d6a7 0%, #81c784 100%)',
|
||||
'linear-gradient(135deg, #c8e6c9 0%, #a5d6a7 100%)',
|
||||
'linear-gradient(135deg, #66bb6a 0%, #4caf50 100%)',
|
||||
'linear-gradient(135deg, #4caf50 0%, #388e3c 100%)'
|
||||
];
|
||||
|
||||
// 从后端API获取目录结构
|
||||
const scanDirectories = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:5000/api/60s/scan-directories');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('无法从后端获取目录结构,使用前端扫描方式');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 前端扫描方式(备用)
|
||||
const frontendScan = async () => {
|
||||
const categories = [];
|
||||
|
||||
for (const [categoryName, config] of Object.entries(categoryConfig)) {
|
||||
const apis = [];
|
||||
|
||||
// 尝试访问已知的模块列表(只包含实际存在的模块)
|
||||
const knownModules = {
|
||||
'热搜榜单': ['抖音热搜榜'],
|
||||
'日更资讯': [],
|
||||
'实用功能': [],
|
||||
'娱乐消遣': []
|
||||
};
|
||||
|
||||
const moduleNames = knownModules[categoryName] || [];
|
||||
|
||||
for (let i = 0; i < moduleNames.length; i++) {
|
||||
const moduleName = moduleNames[i];
|
||||
try {
|
||||
const indexPath = `/60sapi/${categoryName}/${moduleName}/index.html`;
|
||||
const response = await fetch(indexPath, { method: 'HEAD' });
|
||||
|
||||
if (response.ok) {
|
||||
// 获取页面标题
|
||||
const htmlResponse = await fetch(indexPath);
|
||||
const html = await htmlResponse.text();
|
||||
const titleMatch = html.match(/<title>(.*?)<\/title>/i);
|
||||
const title = titleMatch ? titleMatch[1].trim() : moduleName;
|
||||
|
||||
apis.push({
|
||||
title,
|
||||
description: `${moduleName}相关功能`,
|
||||
link: `http://localhost:5000${indexPath}`,
|
||||
status: 'active',
|
||||
color: gradientColors[i % gradientColors.length]
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略访问失败的模块
|
||||
}
|
||||
}
|
||||
|
||||
if (apis.length > 0) {
|
||||
categories.push({
|
||||
title: categoryName,
|
||||
icon: config.icon,
|
||||
color: config.color,
|
||||
apis
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return categories;
|
||||
};
|
||||
|
||||
// 首先尝试后端扫描,失败则使用前端扫描
|
||||
const backendResult = await scanDirectories();
|
||||
if (backendResult && backendResult.success) {
|
||||
return backendResult.categories || [];
|
||||
} else {
|
||||
return await frontendScan();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('扫描API模块时出错:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadApiModules = async () => {
|
||||
setLoading(true);
|
||||
const categories = await scanApiModules();
|
||||
setApiCategories(categories);
|
||||
setLoading(false);
|
||||
setMounted(true);
|
||||
};
|
||||
|
||||
loadApiModules();
|
||||
}, []);
|
||||
|
||||
// 处理API卡片点击
|
||||
const handleApiClick = (api) => {
|
||||
setEmbeddedApi(api);
|
||||
};
|
||||
|
||||
// 关闭内嵌显示
|
||||
const closeEmbedded = () => {
|
||||
setEmbeddedApi(null);
|
||||
};
|
||||
|
||||
if (!mounted || loading) {
|
||||
return (
|
||||
<Api60sContainer>
|
||||
<Container>
|
||||
<Header>
|
||||
<Title>60s API 数据聚合</Title>
|
||||
<Subtitle>正在加载API模块...</Subtitle>
|
||||
</Header>
|
||||
</Container>
|
||||
</Api60sContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Api60sContainer>
|
||||
<Container>
|
||||
<Header>
|
||||
<Title>60s API 数据聚合</Title>
|
||||
<Subtitle>
|
||||
提供丰富的实时数据接口,涵盖热搜榜单、日更资讯、实用工具和娱乐功能
|
||||
</Subtitle>
|
||||
</Header>
|
||||
|
||||
{apiCategories.length === 0 ? (
|
||||
<CategorySection>
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: 'rgba(255, 255, 255, 0.8)' }}>
|
||||
<h3>暂无可用的API模块</h3>
|
||||
<p>请检查60sapi目录结构或联系管理员</p>
|
||||
</div>
|
||||
</CategorySection>
|
||||
) : (
|
||||
apiCategories.map((category, index) => (
|
||||
<CategorySection key={index}>
|
||||
<CategoryTitle style={{ color: category.color }}>
|
||||
{category.icon}
|
||||
{category.title}
|
||||
</CategoryTitle>
|
||||
<CategoryGrid>
|
||||
{category.apis.map((api, apiIndex) => (
|
||||
<ApiCard
|
||||
key={apiIndex}
|
||||
onClick={() => handleApiClick(api)}
|
||||
color={api.color}
|
||||
>
|
||||
<CardHeader>
|
||||
<CardIcon color={category.color}>
|
||||
{category.icon}
|
||||
</CardIcon>
|
||||
<CardTitle>{api.title}</CardTitle>
|
||||
<ExternalIcon>
|
||||
<FiExternalLink />
|
||||
</ExternalIcon>
|
||||
</CardHeader>
|
||||
</ApiCard>
|
||||
))}
|
||||
</CategoryGrid>
|
||||
</CategorySection>
|
||||
))
|
||||
)}
|
||||
</Container>
|
||||
|
||||
{/* 内嵌显示组件 */}
|
||||
{embeddedApi && (
|
||||
<EmbeddedContainer onClick={closeEmbedded}>
|
||||
<EmbeddedContent onClick={(e) => e.stopPropagation()}>
|
||||
<EmbeddedHeader>
|
||||
<h3>{embeddedApi.title}</h3>
|
||||
<BackButton onClick={closeEmbedded}>
|
||||
<FiArrowLeft />
|
||||
返回
|
||||
</BackButton>
|
||||
</EmbeddedHeader>
|
||||
<EmbeddedFrame
|
||||
src={embeddedApi.link}
|
||||
title={embeddedApi.title}
|
||||
/>
|
||||
</EmbeddedContent>
|
||||
</EmbeddedContainer>
|
||||
)}
|
||||
</Api60sContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Api60sPage;
|
||||
278
frontend/react-app/src/pages/HomePage.js
vendored
Normal file
278
frontend/react-app/src/pages/HomePage.js
vendored
Normal file
@@ -0,0 +1,278 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { FiActivity, FiGrid, FiCpu, FiTrendingUp } from 'react-icons/fi';
|
||||
|
||||
const HomeContainer = styled.div`
|
||||
min-height: calc(100vh - 140px);
|
||||
padding: 20px 0;
|
||||
`;
|
||||
|
||||
const HeroSection = styled.section`
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||
padding: 60px 0;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
const HeroContent = styled.div`
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
const HeroTitle = styled.h1`
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-emoji {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 28px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeroSubtitle = styled.p`
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.6;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeroButton = styled(Link)`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(135deg, #81c784 0%, #a5d6a7 100%);
|
||||
color: white;
|
||||
padding: 16px 32px;
|
||||
border-radius: 16px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 8px 32px rgba(129, 199, 132, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 40px rgba(129, 199, 132, 0.5);
|
||||
background: linear-gradient(135deg, #66bb6a 0%, #81c784 100%);
|
||||
}
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
const SectionTitle = styled.h2`
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
|
||||
.section-emoji {
|
||||
margin-right: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModuleGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 60px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ModuleCard = styled(Link)`
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 32px 24px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
box-shadow: 0 8px 32px rgba(168, 230, 207, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(168, 230, 207, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 40px rgba(168, 230, 207, 0.4);
|
||||
border-color: #81c784;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
}
|
||||
`;
|
||||
|
||||
const ModuleIcon = styled.div`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: linear-gradient(135deg, #81c784 0%, #a5d6a7 100%);
|
||||
border-radius: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 16px rgba(129, 199, 132, 0.3);
|
||||
`;
|
||||
|
||||
const ModuleTitle = styled.h3`
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #2e7d32;
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
const ModuleDescription = styled.p`
|
||||
color: #4a4a4a;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const ModuleFeatures = styled.ul`
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
const ModuleFeature = styled.li`
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #10b981;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
const HomePage = () => {
|
||||
|
||||
const modules = [
|
||||
{
|
||||
path: '/60sapi',
|
||||
icon: FiActivity,
|
||||
title: '60s API',
|
||||
description: '实时获取各种热门数据和资讯信息',
|
||||
features: [
|
||||
'抖音热搜榜单',
|
||||
'微博热搜话题',
|
||||
'猫眼票房排行',
|
||||
'每日60秒读懂世界',
|
||||
'必应每日壁纸',
|
||||
'实时天气信息'
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/smallgame',
|
||||
icon: FiGrid,
|
||||
title: '小游戏',
|
||||
description: '轻松有趣的休闲小游戏合集',
|
||||
features: [
|
||||
'经典益智游戏',
|
||||
'休闲娱乐游戏',
|
||||
'技能挑战游戏',
|
||||
'即点即玩',
|
||||
'无需下载',
|
||||
'移动端优化'
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/aimodel',
|
||||
icon: FiCpu,
|
||||
title: 'AI模型',
|
||||
description: '智能AI工具和模型应用',
|
||||
features: [
|
||||
'AI对话助手',
|
||||
'智能文本生成',
|
||||
'图像识别分析',
|
||||
'数据智能处理',
|
||||
'个性化推荐',
|
||||
'需要登录使用'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<HomeContainer>
|
||||
<HeroSection>
|
||||
<HeroContent>
|
||||
<HeroTitle>
|
||||
<span className="title-emoji">✨</span>
|
||||
神奇万事通
|
||||
<span className="title-emoji">✨</span>
|
||||
</HeroTitle>
|
||||
<HeroSubtitle>
|
||||
🎨 一个多功能的聚合软件应用 💬
|
||||
<br />
|
||||
提供实时数据、娱乐游戏、AI工具等丰富功能
|
||||
</HeroSubtitle>
|
||||
<HeroButton to="/60sapi">
|
||||
<FiTrendingUp />
|
||||
开始探索
|
||||
</HeroButton>
|
||||
</HeroContent>
|
||||
</HeroSection>
|
||||
|
||||
<Container>
|
||||
<SectionTitle>
|
||||
<span className="section-emoji">🚀</span>
|
||||
功能模块
|
||||
</SectionTitle>
|
||||
|
||||
<ModuleGrid>
|
||||
{modules.map((module) => {
|
||||
const IconComponent = module.icon;
|
||||
return (
|
||||
<ModuleCard key={module.path} to={module.path}>
|
||||
<ModuleIcon>
|
||||
<IconComponent />
|
||||
</ModuleIcon>
|
||||
<ModuleTitle>{module.title}</ModuleTitle>
|
||||
<ModuleDescription>{module.description}</ModuleDescription>
|
||||
<ModuleFeatures>
|
||||
{module.features.map((feature, index) => (
|
||||
<ModuleFeature key={index}>{feature}</ModuleFeature>
|
||||
))}
|
||||
</ModuleFeatures>
|
||||
</ModuleCard>
|
||||
);
|
||||
})}
|
||||
</ModuleGrid>
|
||||
|
||||
|
||||
</Container>
|
||||
</HomeContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
593
frontend/react-app/src/pages/LoginPage.js
vendored
Normal file
593
frontend/react-app/src/pages/LoginPage.js
vendored
Normal file
@@ -0,0 +1,593 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FiMail, FiUser, FiLock, FiEye, FiEyeOff, FiCheck } from 'react-icons/fi';
|
||||
import { authAPI } from '../utils/api';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useUser } from '../contexts/UserContext';
|
||||
|
||||
const LoginContainer = styled.div`
|
||||
min-height: calc(100vh - 140px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
const LoginCard = styled.div`
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 15px 35px rgba(168, 230, 207, 0.3);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
backdrop-filter: blur(15px);
|
||||
border: 1px solid rgba(168, 230, 207, 0.3);
|
||||
`;
|
||||
|
||||
const Title = styled.h1`
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
color: #2e7d32;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const TabContainer = styled.div`
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
background: #f1f8e9;
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
`;
|
||||
|
||||
const Tab = styled.button`
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
background: ${props => props.active ? '#81c784' : 'transparent'};
|
||||
color: ${props => props.active ? 'white' : '#666'};
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: ${props => props.active ? '#81c784' : '#e8f5e8'};
|
||||
}
|
||||
`;
|
||||
|
||||
const Form = styled.form`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
`;
|
||||
|
||||
const InputGroup = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Input = styled.input`
|
||||
width: 100%;
|
||||
padding: 15px 20px 15px 50px;
|
||||
border: 2px solid #e8f5e8;
|
||||
border-radius: 14px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
background: #fafffe;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #81c784;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(129, 199, 132, 0.1);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #adb5bd;
|
||||
}
|
||||
`;
|
||||
|
||||
const InputIcon = styled.div`
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #81c784;
|
||||
font-size: 18px;
|
||||
`;
|
||||
|
||||
const PasswordToggle = styled.button`
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
color: #adb5bd;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
color: #66bb6a;
|
||||
}
|
||||
`;
|
||||
|
||||
const VerificationGroup = styled.div`
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
`;
|
||||
|
||||
const VerificationInput = styled(Input)`
|
||||
flex: 1;
|
||||
padding-right: 15px;
|
||||
`;
|
||||
|
||||
const SendCodeButton = styled.button`
|
||||
padding: 15px 20px;
|
||||
background: ${props => props.disabled ? '#e8f5e8' : 'linear-gradient(135deg, #81c784 0%, #66bb6a 100%)'};
|
||||
color: ${props => props.disabled ? '#adb5bd' : 'white'};
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};
|
||||
transition: all 0.3s ease;
|
||||
white-space: nowrap;
|
||||
min-width: 100px;
|
||||
|
||||
&:hover {
|
||||
transform: ${props => props.disabled ? 'none' : 'translateY(-2px)'};
|
||||
box-shadow: ${props => props.disabled ? 'none' : '0 8px 25px rgba(129, 199, 132, 0.3)'};
|
||||
}
|
||||
`;
|
||||
|
||||
const SubmitButton = styled.button`
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #81c784 0%, #66bb6a 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 12px;
|
||||
box-shadow: 0 4px 20px rgba(129, 199, 132, 0.3);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(129, 199, 132, 0.4);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const ErrorMessage = styled.div`
|
||||
color: #e57373;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
`;
|
||||
|
||||
const QQHint = styled.div`
|
||||
background: #e8f5e8;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #4a4a4a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const AvatarPreview = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #81c784;
|
||||
object-fit: cover;
|
||||
}
|
||||
`;
|
||||
|
||||
const LoginMethod = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
|
||||
input[type="radio"] {
|
||||
margin: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const LoginPage = () => {
|
||||
const { login, getQQAvatar } = useUser();
|
||||
const [activeTab, setActiveTab] = useState('login');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sendingCode, setSendingCode] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [loginMethod, setLoginMethod] = useState('password'); // 'password' or 'code'
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
code: ''
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [avatarUrl, setAvatarUrl] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 倒计时效果
|
||||
useEffect(() => {
|
||||
let timer;
|
||||
if (countdown > 0) {
|
||||
timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [countdown]);
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
// 清除对应字段的错误
|
||||
if (errors[name]) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[name]: ''
|
||||
}));
|
||||
}
|
||||
|
||||
// 预览QQ头像
|
||||
if (name === 'email' && isQQEmail(value)) {
|
||||
const avatar = getQQAvatar(value);
|
||||
setAvatarUrl(avatar || '');
|
||||
}
|
||||
};
|
||||
|
||||
const isQQEmail = (email) => {
|
||||
const qqDomains = ['qq.com', 'vip.qq.com', 'foxmail.com'];
|
||||
if (!email || !email.includes('@')) return false;
|
||||
const domain = email.split('@')[1]?.toLowerCase();
|
||||
return qqDomains.includes(domain);
|
||||
};
|
||||
|
||||
const validateForm = () => {
|
||||
const newErrors = {};
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = '邮箱地址不能为空';
|
||||
} else if (!isQQEmail(formData.email)) {
|
||||
newErrors.email = '仅支持QQ邮箱(qq.com、vip.qq.com、foxmail.com)';
|
||||
}
|
||||
|
||||
if (activeTab === 'register') {
|
||||
if (!formData.username.trim()) {
|
||||
newErrors.username = '用户名不能为空';
|
||||
}
|
||||
|
||||
if (!formData.password.trim()) {
|
||||
newErrors.password = '密码不能为空';
|
||||
} else if (formData.password.length < 6) {
|
||||
newErrors.password = '密码长度至少6位';
|
||||
}
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = '两次输入的密码不一致';
|
||||
}
|
||||
|
||||
if (!formData.code.trim()) {
|
||||
newErrors.code = '验证码不能为空';
|
||||
}
|
||||
} else {
|
||||
// 登录验证
|
||||
if (loginMethod === 'password') {
|
||||
if (!formData.password.trim()) {
|
||||
newErrors.password = '密码不能为空';
|
||||
}
|
||||
} else {
|
||||
if (!formData.code.trim()) {
|
||||
newErrors.code = '验证码不能为空';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
if (!formData.email.trim()) {
|
||||
toast.error('请先输入邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isQQEmail(formData.email)) {
|
||||
toast.error('仅支持QQ邮箱');
|
||||
return;
|
||||
}
|
||||
|
||||
setSendingCode(true);
|
||||
|
||||
try {
|
||||
const response = await authAPI.sendVerification({
|
||||
email: formData.email,
|
||||
type: activeTab
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success('验证码已发送到您的邮箱');
|
||||
setCountdown(60);
|
||||
} else {
|
||||
toast.error(response.data.message || '发送失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送验证码失败:', error);
|
||||
toast.error(error.response?.data?.message || '发送失败,请重试');
|
||||
} finally {
|
||||
setSendingCode(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
if (activeTab === 'login') {
|
||||
const loginData = { email: formData.email };
|
||||
|
||||
if (loginMethod === 'password') {
|
||||
loginData.password = formData.password;
|
||||
} else {
|
||||
loginData.code = formData.code;
|
||||
}
|
||||
|
||||
const result = await login(loginData);
|
||||
|
||||
if (result.success) {
|
||||
navigate('/');
|
||||
}
|
||||
} else {
|
||||
const response = await authAPI.register({
|
||||
email: formData.email,
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
code: formData.code
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
toast.success('注册成功!请登录');
|
||||
setActiveTab('login');
|
||||
setFormData({
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
code: ''
|
||||
});
|
||||
} else {
|
||||
toast.error(response.data.message || '注册失败');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
toast.error(error.response?.data?.message || '操作失败,请重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const switchTab = (tab) => {
|
||||
setActiveTab(tab);
|
||||
setLoginMethod('password');
|
||||
setErrors({});
|
||||
setFormData({
|
||||
email: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
code: ''
|
||||
});
|
||||
setAvatarUrl('');
|
||||
};
|
||||
|
||||
return (
|
||||
<LoginContainer>
|
||||
<LoginCard>
|
||||
<Title>{activeTab === 'login' ? '欢迎回来' : '创建账户'}</Title>
|
||||
|
||||
{avatarUrl && (
|
||||
<AvatarPreview>
|
||||
<img src={avatarUrl} alt="QQ头像" onError={() => setAvatarUrl('')} />
|
||||
</AvatarPreview>
|
||||
)}
|
||||
|
||||
<TabContainer>
|
||||
<Tab
|
||||
active={activeTab === 'login'}
|
||||
onClick={() => switchTab('login')}
|
||||
type="button"
|
||||
>
|
||||
登录
|
||||
</Tab>
|
||||
<Tab
|
||||
active={activeTab === 'register'}
|
||||
onClick={() => switchTab('register')}
|
||||
type="button"
|
||||
>
|
||||
注册
|
||||
</Tab>
|
||||
</TabContainer>
|
||||
|
||||
<QQHint>
|
||||
<FiMail />
|
||||
<span>仅支持QQ邮箱登录注册,会自动获取您的QQ头像</span>
|
||||
</QQHint>
|
||||
|
||||
{activeTab === 'login' && (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<LoginMethod>
|
||||
<input
|
||||
type="radio"
|
||||
id="password-login"
|
||||
name="loginMethod"
|
||||
value="password"
|
||||
checked={loginMethod === 'password'}
|
||||
onChange={(e) => setLoginMethod(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password-login">密码登录</label>
|
||||
</LoginMethod>
|
||||
<LoginMethod>
|
||||
<input
|
||||
type="radio"
|
||||
id="code-login"
|
||||
name="loginMethod"
|
||||
value="code"
|
||||
checked={loginMethod === 'code'}
|
||||
onChange={(e) => setLoginMethod(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="code-login">验证码登录</label>
|
||||
</LoginMethod>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputGroup>
|
||||
<InputIcon>
|
||||
<FiMail />
|
||||
</InputIcon>
|
||||
<Input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="请输入QQ邮箱"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{errors.email && <ErrorMessage>{errors.email}</ErrorMessage>}
|
||||
</InputGroup>
|
||||
|
||||
{activeTab === 'register' && (
|
||||
<InputGroup>
|
||||
<InputIcon>
|
||||
<FiUser />
|
||||
</InputIcon>
|
||||
<Input
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="请输入用户名"
|
||||
value={formData.username}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{errors.username && <ErrorMessage>{errors.username}</ErrorMessage>}
|
||||
</InputGroup>
|
||||
)}
|
||||
|
||||
{(activeTab === 'register' || (activeTab === 'login' && loginMethod === 'password')) && (
|
||||
<InputGroup>
|
||||
<InputIcon>
|
||||
<FiLock />
|
||||
</InputIcon>
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="password"
|
||||
placeholder="请输入密码"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<PasswordToggle
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? <FiEyeOff /> : <FiEye />}
|
||||
</PasswordToggle>
|
||||
{errors.password && <ErrorMessage>{errors.password}</ErrorMessage>}
|
||||
</InputGroup>
|
||||
)}
|
||||
|
||||
{activeTab === 'register' && (
|
||||
<InputGroup>
|
||||
<InputIcon>
|
||||
<FiLock />
|
||||
</InputIcon>
|
||||
<Input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
name="confirmPassword"
|
||||
placeholder="请确认密码"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{errors.confirmPassword && <ErrorMessage>{errors.confirmPassword}</ErrorMessage>}
|
||||
</InputGroup>
|
||||
)}
|
||||
|
||||
{(activeTab === 'register' || (activeTab === 'login' && loginMethod === 'code')) && (
|
||||
<VerificationGroup>
|
||||
<InputGroup style={{ flex: 1 }}>
|
||||
<InputIcon>
|
||||
<FiCheck />
|
||||
</InputIcon>
|
||||
<VerificationInput
|
||||
type="text"
|
||||
name="code"
|
||||
placeholder="请输入验证码"
|
||||
value={formData.code}
|
||||
onChange={handleInputChange}
|
||||
maxLength={6}
|
||||
/>
|
||||
{errors.code && <ErrorMessage>{errors.code}</ErrorMessage>}
|
||||
</InputGroup>
|
||||
<SendCodeButton
|
||||
type="button"
|
||||
onClick={sendVerificationCode}
|
||||
disabled={sendingCode || countdown > 0}
|
||||
>
|
||||
{sendingCode ? '发送中...' : countdown > 0 ? `${countdown}s` : '发送验证码'}
|
||||
</SendCodeButton>
|
||||
</VerificationGroup>
|
||||
)}
|
||||
|
||||
<SubmitButton type="submit" disabled={loading}>
|
||||
{loading ? '处理中...' : (activeTab === 'login' ? '登录' : '注册')}
|
||||
</SubmitButton>
|
||||
</Form>
|
||||
</LoginCard>
|
||||
</LoginContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
183
frontend/react-app/src/pages/SmallGamePage.js
vendored
Normal file
183
frontend/react-app/src/pages/SmallGamePage.js
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FiGrid, FiPlay, FiZap, FiHeart } from 'react-icons/fi';
|
||||
|
||||
const GameContainer = styled.div`
|
||||
min-height: calc(100vh - 140px);
|
||||
padding: 20px 0;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
const PageHeader = styled.div`
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 40px 20px;
|
||||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||||
border-radius: 16px;
|
||||
`;
|
||||
|
||||
const PageTitle = styled.h1`
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-emoji {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
const PageDescription = styled.p`
|
||||
font-size: 16px;
|
||||
color: #6b7280;
|
||||
line-height: 1.6;
|
||||
`;
|
||||
|
||||
const ComingSoonCard = styled.div`
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 60px 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
const ComingSoonIcon = styled.div`
|
||||
font-size: 64px;
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const ComingSoonTitle = styled.h2`
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const ComingSoonText = styled.p`
|
||||
color: #6b7280;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
const FeatureGrid = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
`;
|
||||
|
||||
const FeatureCard = styled.div`
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
`;
|
||||
|
||||
const FeatureIcon = styled.div`
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
margin-bottom: 16px;
|
||||
`;
|
||||
|
||||
const FeatureTitle = styled.h3`
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const FeatureDescription = styled.p`
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
const SmallGamePage = () => {
|
||||
const plannedFeatures = [
|
||||
{
|
||||
icon: <FiPlay />,
|
||||
title: '经典游戏',
|
||||
description: '俄罗斯方块、贪吃蛇、2048等经典小游戏'
|
||||
},
|
||||
{
|
||||
icon: <FiZap />,
|
||||
title: '反应游戏',
|
||||
description: '测试反应速度和手眼协调能力的趣味游戏'
|
||||
},
|
||||
{
|
||||
icon: <FiHeart />,
|
||||
title: '休闲游戏',
|
||||
description: '轻松愉快的休闲娱乐游戏,适合放松心情'
|
||||
},
|
||||
{
|
||||
icon: <FiGrid />,
|
||||
title: '益智游戏',
|
||||
description: '锻炼思维能力的益智类游戏和谜题'
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<GameContainer>
|
||||
<Container>
|
||||
<PageHeader>
|
||||
<PageTitle>
|
||||
<span className="title-emoji">🎮</span>
|
||||
小游戏
|
||||
<span className="title-emoji">🎮</span>
|
||||
</PageTitle>
|
||||
<PageDescription>
|
||||
轻松有趣的休闲小游戏合集,即点即玩,无需下载
|
||||
</PageDescription>
|
||||
</PageHeader>
|
||||
|
||||
<ComingSoonCard>
|
||||
<ComingSoonIcon>🚧</ComingSoonIcon>
|
||||
<ComingSoonTitle>敬请期待</ComingSoonTitle>
|
||||
<ComingSoonText>
|
||||
小游戏模块正在开发中,即将为您带来丰富多彩的游戏体验。
|
||||
<br />
|
||||
所有游戏都经过移动端优化,支持触屏操作。
|
||||
</ComingSoonText>
|
||||
</ComingSoonCard>
|
||||
|
||||
<FeatureGrid>
|
||||
{plannedFeatures.map((feature, index) => (
|
||||
<FeatureCard key={index}>
|
||||
<FeatureIcon>
|
||||
{feature.icon}
|
||||
</FeatureIcon>
|
||||
<FeatureTitle>{feature.title}</FeatureTitle>
|
||||
<FeatureDescription>{feature.description}</FeatureDescription>
|
||||
</FeatureCard>
|
||||
))}
|
||||
</FeatureGrid>
|
||||
</Container>
|
||||
</GameContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmallGamePage;
|
||||
332
frontend/react-app/src/styles/global.css
Normal file
332
frontend/react-app/src/styles/global.css
Normal file
@@ -0,0 +1,332 @@
|
||||
/* 全局组件样式 */
|
||||
|
||||
/* 容器样式 */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.mobile-container {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* 卡片样式 */
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(168, 230, 207, 0.3);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(168, 230, 207, 0.2);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: 0 12px 40px rgba(168, 230, 207, 0.4);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 16px 20px;
|
||||
background: #f9fafb;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 16px 32px;
|
||||
font-size: 16px;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.btn-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.input-error {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.input-error:focus {
|
||||
border-color: #ef4444;
|
||||
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f4f6;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-sm {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tag-primary {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.tag-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.tag-warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.tag-danger {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* 徽章样式 */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #e5e7eb;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.empty-state-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.error-state-icon {
|
||||
font-size: 36px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.error-state-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.error-state-description {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 网格布局 */
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.grid-1 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.grid-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid-2,
|
||||
.grid-3,
|
||||
.grid-4 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.mobile-grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* 间距工具 */
|
||||
.mt-1 { margin-top: 4px; }
|
||||
.mt-2 { margin-top: 8px; }
|
||||
.mt-3 { margin-top: 12px; }
|
||||
.mt-4 { margin-top: 16px; }
|
||||
.mt-5 { margin-top: 20px; }
|
||||
.mt-6 { margin-top: 24px; }
|
||||
|
||||
.mb-1 { margin-bottom: 4px; }
|
||||
.mb-2 { margin-bottom: 8px; }
|
||||
.mb-3 { margin-bottom: 12px; }
|
||||
.mb-4 { margin-bottom: 16px; }
|
||||
.mb-5 { margin-bottom: 20px; }
|
||||
.mb-6 { margin-bottom: 24px; }
|
||||
|
||||
.pt-1 { padding-top: 4px; }
|
||||
.pt-2 { padding-top: 8px; }
|
||||
.pt-3 { padding-top: 12px; }
|
||||
.pt-4 { padding-top: 16px; }
|
||||
.pt-5 { padding-top: 20px; }
|
||||
.pt-6 { padding-top: 24px; }
|
||||
|
||||
.pb-1 { padding-bottom: 4px; }
|
||||
.pb-2 { padding-bottom: 8px; }
|
||||
.pb-3 { padding-bottom: 12px; }
|
||||
.pb-4 { padding-bottom: 16px; }
|
||||
.pb-5 { padding-bottom: 20px; }
|
||||
.pb-6 { padding-bottom: 24px; }
|
||||
219
frontend/react-app/src/styles/index.css
Normal file
219
frontend/react-app/src/styles/index.css
Normal file
@@ -0,0 +1,219 @@
|
||||
/* 全局样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: #f5f7fa;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 链接样式 */
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* 按钮重置 */
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* 输入框重置 */
|
||||
input, textarea {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
/* 列表重置 */
|
||||
ul, ol {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* 图片响应式 */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 公共动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 公共工具类 */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 响应式工具类 */
|
||||
.mobile-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* 阴影效果 */
|
||||
.shadow-sm {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.shadow-md {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 圆角 */
|
||||
.rounded-sm {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.rounded-full {
|
||||
border-radius: 50%;
|
||||
}
|
||||
106
frontend/react-app/src/utils/api.js
vendored
Normal file
106
frontend/react-app/src/utils/api.js
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
import axios from 'axios';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: process.env.REACT_APP_API_URL || '/api',
|
||||
timeout: 10000,
|
||||
withCredentials: true, // 支持携带cookie
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可以在这里添加token等认证信息
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// 统一错误处理
|
||||
const message = error.response?.data?.message || '网络错误,请稍后重试';
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
// 未授权,跳转到登录页
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
toast.error(message);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 认证相关API
|
||||
export const authAPI = {
|
||||
// 发送验证码
|
||||
sendVerification: (data) => api.post('/auth/send-verification', data),
|
||||
|
||||
// 验证验证码
|
||||
verifyCode: (data) => api.post('/auth/verify-code', data),
|
||||
|
||||
// 登录
|
||||
login: (credentials) => api.post('/auth/login', credentials),
|
||||
|
||||
// 注册
|
||||
register: (userData) => api.post('/auth/register', userData),
|
||||
|
||||
// 登出
|
||||
logout: () => api.post('/auth/logout'),
|
||||
|
||||
// 检查登录状态
|
||||
checkLogin: () => api.get('/auth/check'),
|
||||
};
|
||||
|
||||
// 用户相关API
|
||||
export const userAPI = {
|
||||
// 获取用户资料
|
||||
getProfile: () => api.get('/user/profile'),
|
||||
|
||||
// 修改密码
|
||||
changePassword: (passwordData) => api.post('/user/change-password', passwordData),
|
||||
|
||||
// 获取用户统计
|
||||
getStats: () => api.get('/user/stats'),
|
||||
|
||||
// 删除账户
|
||||
deleteAccount: (password) => api.post('/user/delete', { password }),
|
||||
};
|
||||
|
||||
// 60s API相关接口
|
||||
export const api60s = {
|
||||
// 抖音热搜
|
||||
getDouyinHot: () => api.get('/60s/douyin'),
|
||||
|
||||
// 微博热搜
|
||||
getWeiboHot: () => api.get('/60s/weibo'),
|
||||
|
||||
// 猫眼票房
|
||||
getMaoyanBoxOffice: () => api.get('/60s/maoyan'),
|
||||
|
||||
// 60秒读懂世界
|
||||
get60sNews: () => api.get('/60s/60s'),
|
||||
|
||||
// 必应壁纸
|
||||
getBingWallpaper: () => api.get('/60s/bing-wallpaper'),
|
||||
|
||||
// 天气信息
|
||||
getWeather: (city = '北京') => api.get(`/60s/weather?city=${encodeURIComponent(city)}`),
|
||||
};
|
||||
|
||||
// 健康检查
|
||||
export const healthAPI = {
|
||||
check: () => api.get('/health'),
|
||||
};
|
||||
|
||||
export default api;
|
||||
310
frontend/react-app/src/utils/helpers.js
vendored
Normal file
310
frontend/react-app/src/utils/helpers.js
vendored
Normal file
@@ -0,0 +1,310 @@
|
||||
// 工具函数集合
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {string|Date} date - 日期
|
||||
* @param {string} format - 格式 ('datetime', 'date', 'time')
|
||||
* @returns {string} 格式化后的时间字符串
|
||||
*/
|
||||
export const formatTime = (date, format = 'datetime') => {
|
||||
if (!date) return '';
|
||||
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
|
||||
switch (format) {
|
||||
case 'date':
|
||||
return `${year}-${month}-${day}`;
|
||||
case 'time':
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
case 'datetime':
|
||||
default:
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取相对时间
|
||||
* @param {string|Date} date - 日期
|
||||
* @returns {string} 相对时间字符串
|
||||
*/
|
||||
export const getRelativeTime = (date) => {
|
||||
if (!date) return '';
|
||||
|
||||
const now = new Date();
|
||||
const target = new Date(date);
|
||||
const diff = now - target;
|
||||
|
||||
const minute = 60 * 1000;
|
||||
const hour = 60 * minute;
|
||||
const day = 24 * hour;
|
||||
|
||||
if (diff < minute) {
|
||||
return '刚刚';
|
||||
} else if (diff < hour) {
|
||||
return `${Math.floor(diff / minute)}分钟前`;
|
||||
} else if (diff < day) {
|
||||
return `${Math.floor(diff / hour)}小时前`;
|
||||
} else if (diff < 7 * day) {
|
||||
return `${Math.floor(diff / day)}天前`;
|
||||
} else {
|
||||
return formatTime(date, 'date');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func - 要防抖的函数
|
||||
* @param {number} wait - 等待时间(毫秒)
|
||||
* @returns {Function} 防抖后的函数
|
||||
*/
|
||||
export const debounce = (func, wait) => {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func - 要节流的函数
|
||||
* @param {number} limit - 限制时间(毫秒)
|
||||
* @returns {Function} 节流后的函数
|
||||
*/
|
||||
export const throttle = (func, limit) => {
|
||||
let inThrottle;
|
||||
return function executedFunction(...args) {
|
||||
if (!inThrottle) {
|
||||
func.apply(this, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 深拷贝对象
|
||||
* @param {any} obj - 要拷贝的对象
|
||||
* @returns {any} 深拷贝后的对象
|
||||
*/
|
||||
export const deepClone = (obj) => {
|
||||
if (obj === null || typeof obj !== 'object') return obj;
|
||||
if (obj instanceof Date) return new Date(obj.getTime());
|
||||
if (obj instanceof Array) return obj.map(item => deepClone(item));
|
||||
if (typeof obj === 'object') {
|
||||
const clonedObj = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
clonedObj[key] = deepClone(obj[key]);
|
||||
}
|
||||
}
|
||||
return clonedObj;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
* @returns {string} 唯一ID
|
||||
*/
|
||||
export const generateId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查是否为移动设备
|
||||
* @returns {boolean} 是否为移动设备
|
||||
*/
|
||||
export const isMobile = () => {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取屏幕尺寸类型
|
||||
* @returns {string} 屏幕尺寸类型 ('mobile', 'tablet', 'desktop')
|
||||
*/
|
||||
export const getScreenSize = () => {
|
||||
const width = window.innerWidth;
|
||||
if (width < 768) return 'mobile';
|
||||
if (width < 1024) return 'tablet';
|
||||
return 'desktop';
|
||||
};
|
||||
|
||||
/**
|
||||
* 滚动到顶部
|
||||
* @param {number} duration - 动画持续时间(毫秒)
|
||||
*/
|
||||
export const scrollToTop = (duration = 300) => {
|
||||
const start = window.pageYOffset;
|
||||
const startTime = 'now' in window.performance ? performance.now() : new Date().getTime();
|
||||
|
||||
const animateScroll = (currentTime) => {
|
||||
const timeElapsed = currentTime - startTime;
|
||||
const progress = Math.min(timeElapsed / duration, 1);
|
||||
|
||||
window.scroll(0, start * (1 - progress));
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animateScroll);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animateScroll);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
* @param {number} num - 数字
|
||||
* @returns {string} 格式化后的数字字符串
|
||||
*/
|
||||
export const formatNumber = (num) => {
|
||||
if (num >= 100000000) {
|
||||
return (num / 100000000).toFixed(1) + '亿';
|
||||
} else if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + '万';
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证手机号
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
export const validatePhone = (phone) => {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
return phoneRegex.test(phone);
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证用户名
|
||||
* @param {string} username - 用户名
|
||||
* @returns {boolean} 是否有效
|
||||
*/
|
||||
export const validateUsername = (username) => {
|
||||
const usernameRegex = /^[a-zA-Z0-9_]{6,20}$/;
|
||||
return usernameRegex.test(username);
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证密码强度
|
||||
* @param {string} password - 密码
|
||||
* @returns {object} 验证结果
|
||||
*/
|
||||
export const validatePassword = (password) => {
|
||||
const result = {
|
||||
valid: false,
|
||||
strength: 'weak',
|
||||
message: ''
|
||||
};
|
||||
|
||||
if (password.length < 6) {
|
||||
result.message = '密码长度至少6位';
|
||||
return result;
|
||||
}
|
||||
|
||||
if (password.length > 20) {
|
||||
result.message = '密码长度不能超过20位';
|
||||
return result;
|
||||
}
|
||||
|
||||
let strength = 0;
|
||||
|
||||
// 检查是否包含小写字母
|
||||
if (/[a-z]/.test(password)) strength++;
|
||||
|
||||
// 检查是否包含大写字母
|
||||
if (/[A-Z]/.test(password)) strength++;
|
||||
|
||||
// 检查是否包含数字
|
||||
if (/\d/.test(password)) strength++;
|
||||
|
||||
// 检查是否包含特殊字符
|
||||
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) strength++;
|
||||
|
||||
if (strength >= 3) {
|
||||
result.strength = 'strong';
|
||||
result.message = '密码强度:强';
|
||||
} else if (strength >= 2) {
|
||||
result.strength = 'medium';
|
||||
result.message = '密码强度:中等';
|
||||
} else {
|
||||
result.strength = 'weak';
|
||||
result.message = '密码强度:弱';
|
||||
}
|
||||
|
||||
result.valid = true;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 本地存储工具
|
||||
*/
|
||||
export const storage = {
|
||||
set: (key, value) => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (error) {
|
||||
console.error('存储数据失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
get: (key, defaultValue = null) => {
|
||||
try {
|
||||
const item = localStorage.getItem(key);
|
||||
return item ? JSON.parse(item) : defaultValue;
|
||||
} catch (error) {
|
||||
console.error('读取数据失败:', error);
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
|
||||
remove: (key) => {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (error) {
|
||||
console.error('删除数据失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
clear: () => {
|
||||
try {
|
||||
localStorage.clear();
|
||||
} catch (error) {
|
||||
console.error('清空数据失败:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* URL参数工具
|
||||
*/
|
||||
export const urlParams = {
|
||||
get: (param) => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get(param);
|
||||
},
|
||||
|
||||
set: (param, value) => {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set(param, value);
|
||||
window.history.pushState({}, '', url);
|
||||
},
|
||||
|
||||
remove: (param) => {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.delete(param);
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user