diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..80fe84f --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,87 @@ +# AnyNAT Frontend + +这是AnyNAT项目的React前端应用,提供用户登录注册和管理界面。 + +## 功能特性 + +- 🔐 用户登录/注册 +- 📱 响应式设计 +- 🎨 现代化UI界面 +- ⚡ TypeScript支持 +- 🛡️ 表单验证 + +## 快速开始 + +### 安装依赖 + +```bash +cd frontend +npm install +``` + +### 启动开发服务器 + +```bash +npm start +``` + +应用将在 [http://localhost:3000](http://localhost:3000) 启动。 + +### 构建生产版本 + +```bash +npm run build +``` + +## 演示账户 + +为了方便测试,应用提供了演示账户: + +- **邮箱**: admin@anynat.com +- **密码**: password + +## 项目结构 + +``` +frontend/ +├── public/ +│ └── index.html # HTML模板 +├── src/ +│ ├── components/ # 可复用组件 +│ │ └── AuthForm.tsx # 认证表单组件 +│ ├── pages/ # 页面组件 +│ │ ├── LoginPage.tsx # 登录页面 +│ │ ├── RegisterPage.tsx # 注册页面 +│ │ └── DashboardPage.tsx # 仪表板页面 +│ ├── styles/ # 样式文件 +│ │ ├── App.css # 全局样式 +│ │ ├── AuthPage.css # 认证页面样式 +│ │ └── Dashboard.css # 仪表板样式 +│ ├── App.tsx # 主应用组件 +│ └── index.tsx # 应用入口 +├── package.json # 项目配置 +└── tsconfig.json # TypeScript配置 +``` + +## 技术栈 + +- **React 18** - 用户界面库 +- **TypeScript** - 类型安全 +- **React Router** - 路由管理 +- **CSS3** - 样式和动画 + +## 开发说明 + +1. 登录和注册功能目前使用模拟数据 +2. 需要与后端API集成以实现真实的用户认证 +3. 可以根据需要添加更多功能页面 + +## API集成 + +要集成真实的后端API,请修改以下文件中的API调用: + +- `src/pages/LoginPage.tsx` - 登录API +- `src/pages/RegisterPage.tsx` - 注册API + +将模拟的API调用替换为实际的fetch请求。 + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..e74216f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,44 @@ +{ + "name": "anynat-frontend", + "version": "1.0.0", + "private": true, + "dependencies": { + "@types/node": "^16.18.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.0", + "react-scripts": "5.0.1", + "typescript": "^4.9.0", + "web-vitals": "^2.1.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@types/jest": "^27.5.0" + } +} + diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..01cb97d --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,19 @@ + + + + + + + + + AnyNAT - 登录 + + + +
+ + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..eee5361 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import LoginPage from './pages/LoginPage'; +import RegisterPage from './pages/RegisterPage'; +import DashboardPage from './pages/DashboardPage'; +import './styles/App.css'; + +function App() { + return ( + +
+ + } /> + } /> + } /> + } /> + +
+
+ ); +} + +export default App; + diff --git a/frontend/src/components/AuthForm.tsx b/frontend/src/components/AuthForm.tsx new file mode 100644 index 0000000..6bed07b --- /dev/null +++ b/frontend/src/components/AuthForm.tsx @@ -0,0 +1,83 @@ +import React, { useState } from 'react'; + +interface FormField { + name: string; + type: string; + placeholder: string; + label: string; + required?: boolean; +} + +interface AuthFormProps { + title: string; + fields: FormField[]; + onSubmit: (formData: any) => void; + loading: boolean; + error: string; + submitText: string; +} + +const AuthForm: React.FC = ({ + title, + fields, + onSubmit, + loading, + error, + submitText, +}) => { + const [formData, setFormData] = useState>({}); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value, + })); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(formData); + }; + + return ( +
+

{title}

+ + {error && ( +
+ {error} +
+ )} + +
+ {fields.map((field) => ( +
+ + +
+ ))} + + +
+
+ ); +}; + +export default AuthForm; + diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..5197965 --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement +); + +root.render( + + + +); + diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..765b83e --- /dev/null +++ b/frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import '../styles/Dashboard.css'; + +const DashboardPage: React.FC = () => { + const [user, setUser] = useState<{ email: string } | null>(null); + const navigate = useNavigate(); + + useEffect(() => { + // 检查用户是否已登录 + const token = localStorage.getItem('authToken'); + if (!token) { + navigate('/login'); + return; + } + + // 模拟获取用户信息 + setUser({ email: 'admin@anynat.com' }); + }, [navigate]); + + const handleLogout = () => { + localStorage.removeItem('authToken'); + navigate('/login'); + }; + + if (!user) { + return
加载中...
; + } + + return ( +
+
+
+

AnyNAT 控制台

+
+ 欢迎,{user.email} + +
+
+
+ +
+
+
+

欢迎使用 AnyNAT

+

您已成功登录到高性能内网穿透工具控制台

+
+ +
+
+

🚀 高性能

+

基于Node.js和MQTT协议,提供高效的内网穿透服务

+
+ +
+

🔒 安全可靠

+

采用加密传输,确保数据安全

+
+ +
+

⚡ 快速部署

+

支持Docker容器化部署,快速启动

+
+ +
+

📊 实时监控

+

提供实时连接状态和流量监控

+
+
+ +
+

快速操作

+
+ + + +
+
+
+
+
+ ); +}; + +export default DashboardPage; + diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..08e0e92 --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,102 @@ +import React, { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import AuthForm from '../components/AuthForm'; +import '../styles/AuthPage.css'; + +interface LoginFormData { + email: string; + password: string; +} + +const LoginPage: React.FC = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + const handleLogin = async (formData: LoginFormData) => { + setLoading(true); + setError(''); + + try { + // 这里应该调用实际的登录API + // const response = await fetch('/api/login', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify(formData), + // }); + + // 模拟API调用 + await new Promise(resolve => setTimeout(resolve, 1000)); + + // 模拟登录验证 + if (formData.email === 'admin@anynat.com' && formData.password === 'password') { + // 登录成功,保存token到localStorage + localStorage.setItem('authToken', 'mock-jwt-token'); + navigate('/dashboard'); + } else { + setError('邮箱或密码错误'); + } + } catch (err) { + setError('登录失败,请稍后重试'); + } finally { + setLoading(false); + } + }; + + const loginFields = [ + { + name: 'email', + type: 'email', + placeholder: '请输入邮箱', + label: '邮箱', + required: true, + }, + { + name: 'password', + type: 'password', + placeholder: '请输入密码', + label: '密码', + required: true, + }, + ]; + + return ( +
+
+
+

AnyNAT

+

高性能内网穿透工具

+
+ + + +
+

+ 还没有账户? + + 立即注册 + +

+
+ +
+

演示账户:

+

邮箱: admin@anynat.com

+

密码: password

+
+
+
+ ); +}; + +export default LoginPage; + diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx new file mode 100644 index 0000000..2ca922e --- /dev/null +++ b/frontend/src/pages/RegisterPage.tsx @@ -0,0 +1,125 @@ +import React, { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import AuthForm from '../components/AuthForm'; +import '../styles/AuthPage.css'; + +interface RegisterFormData { + username: string; + email: string; + password: string; + confirmPassword: string; +} + +const RegisterPage: React.FC = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + const handleRegister = async (formData: RegisterFormData) => { + setLoading(true); + setError(''); + + // 验证密码确认 + if (formData.password !== formData.confirmPassword) { + setError('两次输入的密码不一致'); + setLoading(false); + return; + } + + // 验证密码强度 + if (formData.password.length < 6) { + setError('密码长度至少为6位'); + setLoading(false); + return; + } + + try { + // 这里应该调用实际的注册API + // const response = await fetch('/api/register', { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // username: formData.username, + // email: formData.email, + // password: formData.password, + // }), + // }); + + // 模拟API调用 + await new Promise(resolve => setTimeout(resolve, 1500)); + + // 模拟注册成功 + alert('注册成功!请登录您的账户。'); + navigate('/login'); + } catch (err) { + setError('注册失败,请稍后重试'); + } finally { + setLoading(false); + } + }; + + const registerFields = [ + { + name: 'username', + type: 'text', + placeholder: '请输入用户名', + label: '用户名', + required: true, + }, + { + name: 'email', + type: 'email', + placeholder: '请输入邮箱', + label: '邮箱', + required: true, + }, + { + name: 'password', + type: 'password', + placeholder: '请输入密码(至少6位)', + label: '密码', + required: true, + }, + { + name: 'confirmPassword', + type: 'password', + placeholder: '请再次输入密码', + label: '确认密码', + required: true, + }, + ]; + + return ( +
+
+
+

AnyNAT

+

高性能内网穿透工具

+
+ + + +
+

+ 已有账户? + + 立即登录 + +

+
+
+
+ ); +}; + +export default RegisterPage; + diff --git a/frontend/src/styles/App.css b/frontend/src/styles/App.css new file mode 100644 index 0000000..77016ac --- /dev/null +++ b/frontend/src/styles/App.css @@ -0,0 +1,29 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +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; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; +} + +.App { + min-height: 100vh; +} + +.loading { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + font-size: 18px; + color: #666; +} + diff --git a/frontend/src/styles/AuthPage.css b/frontend/src/styles/AuthPage.css new file mode 100644 index 0000000..d09bb26 --- /dev/null +++ b/frontend/src/styles/AuthPage.css @@ -0,0 +1,181 @@ +.auth-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.auth-container { + background: white; + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); + padding: 40px; + width: 100%; + max-width: 400px; + animation: slideUp 0.5s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.auth-header { + text-align: center; + margin-bottom: 30px; +} + +.auth-header h1 { + color: #333; + font-size: 32px; + font-weight: 700; + margin-bottom: 8px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.auth-header p { + color: #666; + font-size: 14px; +} + +.auth-form h2 { + color: #333; + font-size: 24px; + font-weight: 600; + margin-bottom: 20px; + text-align: center; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 6px; + color: #333; + font-weight: 500; + font-size: 14px; +} + +.form-group input { + width: 100%; + padding: 12px 16px; + border: 2px solid #e1e5e9; + border-radius: 8px; + font-size: 16px; + transition: all 0.3s ease; + background: #f8f9fa; +} + +.form-group input:focus { + outline: none; + border-color: #667eea; + background: white; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.form-group input:disabled { + background: #f5f5f5; + cursor: not-allowed; +} + +.submit-btn { + width: 100%; + padding: 14px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + margin-top: 10px; +} + +.submit-btn:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); +} + +.submit-btn:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; +} + +.error-message { + background: #fee; + color: #c33; + padding: 12px; + border-radius: 6px; + margin-bottom: 20px; + border: 1px solid #fcc; + font-size: 14px; + text-align: center; +} + +.auth-footer { + text-align: center; + margin-top: 30px; + padding-top: 20px; + border-top: 1px solid #eee; +} + +.auth-footer p { + color: #666; + font-size: 14px; +} + +.auth-link { + color: #667eea; + text-decoration: none; + font-weight: 600; + margin-left: 5px; +} + +.auth-link:hover { + text-decoration: underline; +} + +.demo-info { + margin-top: 20px; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; + border-left: 4px solid #667eea; +} + +.demo-info p { + font-size: 12px; + color: #666; + margin-bottom: 4px; +} + +.demo-info p:first-child { + font-weight: 600; + color: #333; +} + +@media (max-width: 480px) { + .auth-container { + padding: 30px 20px; + margin: 10px; + } + + .auth-header h1 { + font-size: 28px; + } +} + diff --git a/frontend/src/styles/Dashboard.css b/frontend/src/styles/Dashboard.css new file mode 100644 index 0000000..1a886bb --- /dev/null +++ b/frontend/src/styles/Dashboard.css @@ -0,0 +1,199 @@ +.dashboard { + min-height: 100vh; + background: #f5f7fa; +} + +.dashboard-header { + background: white; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + position: sticky; + top: 0; + z-index: 100; +} + +.header-content { + max-width: 1200px; + margin: 0 auto; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.dashboard-header h1 { + color: #333; + font-size: 28px; + font-weight: 700; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.user-info { + display: flex; + align-items: center; + gap: 15px; +} + +.user-info span { + color: #666; + font-weight: 500; +} + +.logout-btn { + padding: 8px 16px; + background: #dc3545; + color: white; + border: none; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + transition: all 0.3s ease; +} + +.logout-btn:hover { + background: #c82333; + transform: translateY(-1px); +} + +.dashboard-main { + max-width: 1200px; + margin: 0 auto; + padding: 40px 20px; +} + +.welcome-section { + text-align: center; + margin-bottom: 50px; +} + +.welcome-section h2 { + color: #333; + font-size: 36px; + font-weight: 700; + margin-bottom: 10px; +} + +.welcome-section p { + color: #666; + font-size: 18px; +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 30px; + margin-bottom: 50px; +} + +.feature-card { + background: white; + padding: 30px; + border-radius: 12px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + text-align: center; +} + +.feature-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); +} + +.feature-card h3 { + color: #333; + font-size: 20px; + font-weight: 600; + margin-bottom: 15px; +} + +.feature-card p { + color: #666; + line-height: 1.6; +} + +.actions-section { + background: white; + padding: 40px; + border-radius: 12px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08); +} + +.actions-section h3 { + color: #333; + font-size: 24px; + font-weight: 600; + margin-bottom: 25px; + text-align: center; +} + +.action-buttons { + display: flex; + gap: 15px; + justify-content: center; + flex-wrap: wrap; +} + +.action-btn { + padding: 12px 24px; + border: 2px solid #e1e5e9; + border-radius: 8px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + background: white; + color: #333; +} + +.action-btn.primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-color: transparent; +} + +.action-btn:hover { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); +} + +.action-btn.primary:hover { + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3); +} + +@media (max-width: 768px) { + .header-content { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .dashboard-header h1 { + font-size: 24px; + } + + .welcome-section h2 { + font-size: 28px; + } + + .features-grid { + grid-template-columns: 1fr; + gap: 20px; + } + + .actions-section { + padding: 30px 20px; + } + + .action-buttons { + flex-direction: column; + align-items: center; + } + + .action-btn { + width: 100%; + max-width: 200px; + } +} + diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1b8e6eb --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "es6" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": [ + "src" + ] +} +