This commit is contained in:
2025-08-20 22:02:42 +08:00
parent b40bb84468
commit 1a80726cd4
22 changed files with 1462 additions and 14993 deletions

View File

@ -1,7 +1,7 @@
# 开发环境配置
PORT=3001
REACT_APP_ENV=development
REACT_APP_API_URL=http://localhost:8080
REACT_APP_API_URL=http://localhost:3001
REACT_APP_API_TIMEOUT=10000
REACT_APP_DEBUG=true
GENERATE_SOURCEMAP=true

30
frontend/index.html Normal file
View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="UAdmin - 用户权限管理系统"
/>
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>UAdmin - 用户权限管理系统</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

15599
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,8 @@
"private": true,
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.126",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@types/react-router-dom": "^5.3.3",
@ -18,24 +14,30 @@
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.1",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"env-cmd": "^10.1.0"
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.7.0",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.3.0",
"@vitejs/plugin-react": "^4.7.0",
"env-cmd": "^10.1.0",
"typescript": "^5.9.2",
"vite": "^5.4.19"
},
"scripts": {
"start": "react-scripts start",
"start:dev": "env-cmd -f .env.development react-scripts start",
"start:prod": "env-cmd -f .env.production react-scripts start",
"start:test": "env-cmd -f .env.test react-scripts start",
"build": "env-cmd -f .env.production react-scripts build",
"build:dev": "env-cmd -f .env.development react-scripts build",
"build:prod": "env-cmd -f .env.production react-scripts build",
"build:test": "env-cmd -f .env.test react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"start": "vite",
"dev": "env-cmd -f .env.development vite",
"prod": "env-cmd -f .env.production vite",
"start:test": "env-cmd -f .env.test vite",
"build": "env-cmd -f .env.production vite build",
"build:dev": "env-cmd -f .env.development vite build",
"build:prod": "env-cmd -f .env.production vite build",
"build:test": "env-cmd -f .env.test vite build",
"test": "vitest",
"preview": "vite preview"
},
"eslintConfig": {
"extends": [

View File

@ -1,6 +1,6 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { ConfigProvider } from 'antd';
import { ConfigProvider, Spin, App as AntdApp } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { AuthProvider, useAuth } from './hooks/useAuth';
import Layout from './components/Layout';
@ -13,12 +13,30 @@ import Permissions from './pages/Permissions';
import Menus from './pages/Menus';
import './App.css';
// 全局message配置
import { message } from 'antd';
message.config({
top: 100,
duration: 2,
maxCount: 3,
});
// 受保护的路由组件
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { user, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: '#f0f2f5'
}}>
<Spin size="large" />
</div>
);
}
if (!user) {
@ -33,7 +51,17 @@ const PublicRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { user, loading } = useAuth();
if (loading) {
return <div>Loading...</div>;
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}}>
<Spin size="large" style={{ color: 'white' }} />
</div>
);
}
if (user) {
@ -46,40 +74,42 @@ const PublicRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
function App() {
return (
<ConfigProvider locale={zhCN}>
<AuthProvider>
<Router>
<Routes>
{/* 公共路由 */}
<Route path="/login" element={
<PublicRoute>
<Login />
</PublicRoute>
} />
<Route path="/register" element={
<PublicRoute>
<Register />
</PublicRoute>
} />
{/* 受保护的路由 */}
<Route path="/" element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}>
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="users" element={<Users />} />
<Route path="roles" element={<Roles />} />
<Route path="permissions" element={<Permissions />} />
<Route path="menus" element={<Menus />} />
</Route>
{/* 404 重定向 */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Router>
</AuthProvider>
<AntdApp>
<AuthProvider>
<Router>
<Routes>
{/* 公共路由 */}
<Route path="/login" element={
<PublicRoute>
<Login />
</PublicRoute>
} />
<Route path="/register" element={
<PublicRoute>
<Register />
</PublicRoute>
} />
{/* 受保护的路由 */}
<Route path="/" element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}>
<Route index element={<Navigate to="/dashboard" replace />} />
<Route path="dashboard" element={<Dashboard />} />
<Route path="users" element={<Users />} />
<Route path="roles" element={<Roles />} />
<Route path="permissions" element={<Permissions />} />
<Route path="menus" element={<Menus />} />
</Route>
{/* 404 重定向 */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Router>
</AuthProvider>
</AntdApp>
</ConfigProvider>
);
}

View File

@ -49,8 +49,9 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
const login = async (data: LoginRequest): Promise<boolean> => {
try {
setLoading(true);
console.log('useAuth: 开始登录请求', data);
const response = await apiService.login(data);
console.log('useAuth: 登录响应', response);
setToken(response.token);
setUser(response.user);
@ -60,19 +61,26 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
localStorage.setItem('user', JSON.stringify(response.user));
message.success('登录成功');
console.log('useAuth: 登录成功');
return true;
} catch (error: any) {
console.error('Login failed:', error);
message.error(error.response?.data?.message || '登录失败');
console.error('useAuth: 登录失败', error);
// 检查是否是apiService抛出的业务错误
if (error.response?.data?.code && error.response.data.code !== 200) {
message.error(error.response.data.message || '登录失败');
} else if (error.response?.status === 401) {
message.error('用户名或密码错误');
} else if (error.message) {
message.error(error.message);
} else {
message.error('登录失败');
}
return false;
} finally {
setLoading(false);
}
};
const register = async (data: RegisterRequest): Promise<boolean> => {
try {
setLoading(true);
await apiService.register(data);
message.success('注册成功,请登录');
return true;
@ -80,8 +88,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
console.error('Register failed:', error);
message.error(error.response?.data?.message || '注册失败');
return false;
} finally {
setLoading(false);
}
};

View File

@ -1,3 +1,4 @@
import '@ant-design/v5-patch-for-react-19';
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';

View File

@ -1,9 +1,10 @@
import React, { useState } from 'react';
import { Form, Input, Button, Card, Typography, message } from 'antd';
import { Form, Input, Button, Card, Typography } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { useAuth } from '../hooks/useAuth';
import { LoginRequest } from '../types';
import { useNavigate, Link } from 'react-router-dom';
import message from 'antd/es/message';
const { Title } = Typography;
@ -15,12 +16,18 @@ const Login: React.FC = () => {
const onFinish = async (values: LoginRequest) => {
setLoading(true);
try {
console.log('开始登录:', values);
const success = await login(values);
console.log('登录结果:', success);
if (success) {
console.log('登录成功跳转到dashboard');
navigate('/dashboard');
} else {
console.log('登录失败');
}
} catch (error) {
console.error('Login error:', error);
message.error('登录过程中发生错误');
} finally {
setLoading(false);
}

View File

@ -25,7 +25,7 @@ class ApiService {
constructor() {
this.api = axios.create({
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api',
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
@ -53,10 +53,13 @@ class ApiService {
},
(error) => {
if (error.response?.status === 401) {
// Token过期或无效清除本地存储并跳转到登录页
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/login';
// 如果不是登录请求才清除token并跳转
if (!error.config?.url?.includes('/auth/login')) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/login';
}
// 对于登录请求的401错误直接返回错误让组件处理
}
return Promise.reject(error);
}
@ -65,7 +68,13 @@ class ApiService {
// 认证相关API
async login(data: LoginRequest): Promise<LoginResponse> {
const response = await this.api.post<ApiResponse<LoginResponse>>('/auth/login', data);
const response = await this.api.post<ApiResponse<LoginResponse>>("/auth/login", data);
// 检查业务错误码
if (response.data.code !== 200) {
const error = new Error(response.data.message);
(error as any).response = { data: response.data };
throw error;
}
return response.data.data!;
}

View File

@ -0,0 +1,232 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import {
User,
Role,
Permission,
Menu,
CreateUserRequest,
UpdateUserRequest,
CreateRoleRequest,
UpdateRoleRequest,
CreatePermissionRequest,
UpdatePermissionRequest,
CreateMenuRequest,
UpdateMenuRequest,
LoginRequest,
LoginResponse,
RegisterRequest,
ApiResponse,
PaginatedResponse,
QueryParams,
} from '../types';
class ApiService {
private api: AxiosInstance;
constructor() {
this.api = axios.create({
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器 - 添加认证token
this.api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器 - 处理错误
this.api.interceptors.response.use(
(response: AxiosResponse) => {
return response;
},
(error) => {
if (error.response?.status === 401) {
// 如果不是登录请求才清除token并跳转
if (!error.config?.url?.includes('/auth/login')) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/login';
}
// 对于登录请求的401错误直接返回错误让组件处理
}
return Promise.reject(error);
}
);
}
// 认证相关API
async login(data: LoginRequest): Promise<LoginResponse> {
const response = await this.api.post<ApiResponse<LoginResponse>>('/auth/login', data);
return response.data.data!;
}
async register(data: RegisterRequest): Promise<User> {
const response = await this.api.post<ApiResponse<User>>('/auth/register', data);
return response.data.data!;
}
async logout(): Promise<void> {
await this.api.post('/auth/logout');
}
// 用户管理API
async getUsers(params?: QueryParams): Promise<PaginatedResponse<User>> {
const response = await this.api.get('/users', { params });
const backendData = response.data.data;
// 转换后端数据结构到前端期望的结构
return {
data: backendData.items.map((user: any) => ({
...user,
status: user.status === 1 ? 'active' : 'inactive'
})),
total: backendData.total,
page: backendData.page,
per_page: backendData.page_size,
total_pages: Math.ceil(backendData.total / backendData.page_size)
};
}
async getUser(id: number): Promise<User> {
const response = await this.api.get<ApiResponse<User>>(`/users/${id}`);
return response.data.data!;
}
async createUser(data: CreateUserRequest): Promise<User> {
const response = await this.api.post<ApiResponse<User>>('/users', data);
return response.data.data!;
}
async updateUser(id: number, data: UpdateUserRequest): Promise<User> {
// 转换前端的字符串状态为后端期望的数字格式
const backendData = {
...data,
status: data.status ? (data.status === 'active' ? 1 : 0) : undefined
};
const response = await this.api.put<ApiResponse<User>>(`/users/${id}`, backendData);
return response.data.data!;
}
async deleteUser(id: number): Promise<void> {
await this.api.delete(`/users/${id}`);
}
// 角色管理API
async getRoles(params?: QueryParams): Promise<PaginatedResponse<Role>> {
const response = await this.api.get<ApiResponse<PaginatedResponse<Role>>>('/roles', { params });
return response.data.data!;
}
async getAllRoles(): Promise<Role[]> {
const response = await this.api.get<ApiResponse<Role[]>>('/roles/all');
return response.data.data!;
}
async getRole(id: number): Promise<Role> {
const response = await this.api.get<ApiResponse<Role>>(`/roles/${id}`);
return response.data.data!;
}
async createRole(data: CreateRoleRequest): Promise<Role> {
const response = await this.api.post<ApiResponse<Role>>('/roles', data);
return response.data.data!;
}
async updateRole(id: number, data: UpdateRoleRequest): Promise<Role> {
// 转换前端的字符串状态为后端期望的数字格式
const backendData = {
...data,
status: data.status ? (data.status === 'active' ? 1 : 0) : undefined
};
const response = await this.api.put<ApiResponse<Role>>(`/roles/${id}`, backendData);
return response.data.data!;
}
async deleteRole(id: number): Promise<void> {
await this.api.delete(`/roles/${id}`);
}
// 权限管理API
async getPermissions(params?: QueryParams): Promise<PaginatedResponse<Permission>> {
const response = await this.api.get<ApiResponse<PaginatedResponse<Permission>>>('/permissions', { params });
return response.data.data!;
}
async getAllPermissions(): Promise<Permission[]> {
const response = await this.api.get<ApiResponse<Permission[]>>('/permissions/all');
return response.data.data!;
}
async getPermission(id: number): Promise<Permission> {
const response = await this.api.get<ApiResponse<Permission>>(`/permissions/${id}`);
return response.data.data!;
}
async createPermission(data: CreatePermissionRequest): Promise<Permission> {
const response = await this.api.post<ApiResponse<Permission>>('/permissions', data);
return response.data.data!;
}
async updatePermission(id: number, data: UpdatePermissionRequest): Promise<Permission> {
// 转换前端的字符串状态为后端期望的数字格式
const backendData = {
...data,
status: data.status ? (data.status === 'active' ? 1 : 0) : undefined
};
const response = await this.api.put<ApiResponse<Permission>>(`/permissions/${id}`, backendData);
return response.data.data!;
}
async deletePermission(id: number): Promise<void> {
await this.api.delete(`/permissions/${id}`);
}
// 菜单管理API
async getMenus(params?: QueryParams): Promise<PaginatedResponse<Menu>> {
const response = await this.api.get<ApiResponse<PaginatedResponse<Menu>>>('/menus', { params });
return response.data.data!;
}
async getAllMenus(): Promise<Menu[]> {
const response = await this.api.get<ApiResponse<Menu[]>>('/menus/all');
return response.data.data!;
}
async getMenu(id: number): Promise<Menu> {
const response = await this.api.get<ApiResponse<Menu>>(`/menus/${id}`);
return response.data.data!;
}
async createMenu(data: CreateMenuRequest): Promise<Menu> {
const response = await this.api.post<ApiResponse<Menu>>('/menus', data);
return response.data.data!;
}
async updateMenu(id: number, data: UpdateMenuRequest): Promise<Menu> {
// 转换前端的字符串状态为后端期望的数字格式
const backendData = {
...data,
status: data.status ? (data.status === 'active' ? 1 : 0) : undefined
};
const response = await this.api.put<ApiResponse<Menu>>(`/menus/${id}`, backendData);
return response.data.data!;
}
async deleteMenu(id: number): Promise<void> {
await this.api.delete(`/menus/${id}`);
}
}
export const apiService = new ApiService();
export default apiService;

30
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,30 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 3001,
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://127.0.0.1:8080',
changeOrigin: true,
secure: false,
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('Proxying request:', req.method, req.url);
console.log('Headers:', req.headers);
});
proxy.on('proxyRes', (proxyRes, req, res) => {
console.log('Proxy response:', proxyRes.statusCode, 'for', req.url);
});
proxy.on('error', (err, req, res) => {
console.log('Proxy error:', err.message);
});
}
}
}
}
})