Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -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请求。

44 changes: 44 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}

19 changes: 19 additions & 0 deletions frontend/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="AnyNAT - 高性能内网穿透工具"
/>
<title>AnyNAT - 登录</title>
</head>
<body>
<noscript>您需要启用JavaScript才能运行此应用程序。</noscript>
<div id="root"></div>
</body>
</html>

24 changes: 24 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Router>
<div className="App">
<Routes>
<Route path="/" element={<Navigate to="/login" replace />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</div>
</Router>
);
}

export default App;

83 changes: 83 additions & 0 deletions frontend/src/components/AuthForm.tsx
Original file line number Diff line number Diff line change
@@ -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<AuthFormProps> = ({
title,
fields,
onSubmit,
loading,
error,
submitText,
}) => {
const [formData, setFormData] = useState<Record<string, string>>({});

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value,
}));
};

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(formData);
};

return (
<div className="auth-form">
<h2>{title}</h2>

{error && (
<div className="error-message">
{error}
</div>
)}

<form onSubmit={handleSubmit}>
{fields.map((field) => (
<div key={field.name} className="form-group">
<label htmlFor={field.name}>{field.label}</label>
<input
type={field.type}
id={field.name}
name={field.name}
placeholder={field.placeholder}
value={formData[field.name] || ''}
onChange={handleChange}
required={field.required}
disabled={loading}
/>
</div>
))}

<button
type="submit"
className="submit-btn"
disabled={loading}
>
{loading ? '处理中...' : submitText}
</button>
</form>
</div>
);
};

export default AuthForm;

14 changes: 14 additions & 0 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<App />
</React.StrictMode>
);

88 changes: 88 additions & 0 deletions frontend/src/pages/DashboardPage.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className="loading">加载中...</div>;
}

return (
<div className="dashboard">
<header className="dashboard-header">
<div className="header-content">
<h1>AnyNAT 控制台</h1>
<div className="user-info">
<span>欢迎,{user.email}</span>
<button onClick={handleLogout} className="logout-btn">
退出登录
</button>
</div>
</div>
</header>

<main className="dashboard-main">
<div className="dashboard-content">
<div className="welcome-section">
<h2>欢迎使用 AnyNAT</h2>
<p>您已成功登录到高性能内网穿透工具控制台</p>
</div>

<div className="features-grid">
<div className="feature-card">
<h3>🚀 高性能</h3>
<p>基于Node.js和MQTT协议,提供高效的内网穿透服务</p>
</div>

<div className="feature-card">
<h3>🔒 安全可靠</h3>
<p>采用加密传输,确保数据安全</p>
</div>

<div className="feature-card">
<h3>⚡ 快速部署</h3>
<p>支持Docker容器化部署,快速启动</p>
</div>

<div className="feature-card">
<h3>📊 实时监控</h3>
<p>提供实时连接状态和流量监控</p>
</div>
</div>

<div className="actions-section">
<h3>快速操作</h3>
<div className="action-buttons">
<button className="action-btn primary">创建隧道</button>
<button className="action-btn">查看日志</button>
<button className="action-btn">系统设置</button>
</div>
</div>
</div>
</main>
</div>
);
};

export default DashboardPage;

Loading