60sapi接口搭建完毕,数据库连接测试成功,登录注册部分简单完成

This commit is contained in:
2025-09-02 19:45:50 +08:00
parent b139fb14d9
commit e1f8885c6c
150 changed files with 53045 additions and 8 deletions

88
frontend/react-app/src/App.js vendored Normal file
View 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;

View 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;

View 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;

View 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;

View 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
View 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>
);

View 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调用已修复

View 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;

View 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
View 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;

View 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;

View 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;

View 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; }

View 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
View 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
View 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);
}
};