diff --git a/README.md b/README.md index a36934d..e52ea1b 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,202 @@ -# React + Vite +# Atlas Console -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. +> A modern React admin dashboard template -Currently, two official plugins are available: +## Features -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) +- 🔐 Login authentication +- 📱 Responsive sidebar (collapsible) +- 📊 Dashboard with statistics +- ⚙️ Settings management +- 🎨 Clean UI design -## React Compiler +## Tech Stack -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). +- **Frontend**: React 19 + Vite +- **Routing**: React Router DOM +- **API**: Fetch API with mock support -## Expanding the ESLint configuration +## Quick Start -If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. +### Install Dependencies + +```bash +npm install +``` + +### Development + +```bash +npm run dev +``` + +Then open http://localhost:5173 in your browser. + +### Build for Production + +```bash +npm run build +``` + +The build output will be in the `dist` folder. + +### Preview Production Build + +```bash +npm run preview +``` + +## Demo Account + +- **Username**: admin +- **Password**: admin123 + +## API Configuration + +The project uses a centralized API service located at `src/api/index.js`. + +### Using Mock Data + +By default, the project uses mock data. To use real API: + +1. Open `src/api/index.js` +2. Find this line: + ```javascript + const USE_MOCK = true // 启用 Mock 数据 + // const USE_MOCK = false // 使用真实 API + ``` +3. Change to: + ```javascript + // const USE_MOCK = true // 启用 Mock 数据 + const USE_MOCK = false // 使用真实 API + ``` + +### API Endpoints + +When connecting to a real backend, ensure these endpoints are available: + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/auth/login` | Login | +| GET | `/api/auth/info` | Get user info | +| POST | `/api/auth/logout` | Logout | +| GET | `/api/projects` | Get project list | +| GET | `/api/settings` | Get settings | +| POST | `/api/settings` | Save settings | +| GET | `/api/stats` | Get dashboard stats | + +### Connecting to Real Backend + +1. Open `src/api/index.js` +2. Modify the `BASE_URL`: + ```javascript + const BASE_URL = 'http://your-api-server.com/api' + ``` +3. Enable real API mode: + ```javascript + const USE_MOCK = false + ``` + +## Project Structure + +``` +atlas-console/ +├── public/ # Static assets +├── src/ +│ ├── api/ # API service +│ │ ├── index.js # Core API with mock support +│ │ └── modules/ # API modules +│ │ ├── auth.js # Auth APIs +│ │ ├── project.js # Project APIs +│ │ ├── settings.js # Settings APIs +│ │ └── stats.js # Stats APIs +│ ├── assets/ # Images, fonts, etc. +│ ├── pages/ # Page components +│ │ ├── Login.jsx # Login page +│ │ ├── Layout.jsx # Main layout with sidebar +│ │ ├── Home.jsx # Dashboard +│ │ ├── Menu1.jsx # Menu 1 page +│ │ └── Menu2.jsx # Menu 2 page +│ ├── App.jsx # Root component +│ ├── main.jsx # Entry point +│ └── index.css # Global styles +├── index.html # HTML template +├── package.json # Dependencies +└── vite.config.js # Vite configuration +``` + +## Deployment + +### Static Hosting + +The project can be deployed to any static hosting service: + +#### Netlify + +```bash +# Install Netlify CLI +npm install -g netlify-cli + +# Deploy +netlify deploy --prod +``` + +#### Vercel + +```bash +# Install Vercel CLI +npm install -g vercel + +# Deploy +vercel --prod +``` + +#### Nginx + +1. Build the project: `npm run build` +2. Copy `dist` folder to Nginx web root +3. Configure Nginx: + +```nginx +server { + listen 80; + server_name your-domain.com; + root /path/to/dist; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} +``` + +#### Docker + +Create a `Dockerfile`: + +```dockerfile +FROM nginx:alpine +COPY dist/ /usr/share/nginx/html/ +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] +``` + +Build and run: + +```bash +docker build -t atlas-console . +docker run -d -p 80:80 atlas-console +``` + +### Backend Integration + +For production, you'll need a backend service that provides the API endpoints. The frontend expects: + +- RESTful API at `/api/*` +- JWT token authentication +- CORS configured for your domain + +## License + +MIT \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 07ad2e4..c07e45d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "micah", + "name": "atlas-console", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "micah", + "name": "atlas-console", "version": "0.0.0", "dependencies": { "react": "^19.2.4", diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 0000000..56f010d --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,199 @@ +/** + * API 统一出口 + * 使用方法:取消注释 USE_MOCK 即可启用 Mock 数据 + */ + +const USE_MOCK = true // 启用 Mock 数据 +// const USE_MOCK = false // 使用真实 API + +const BASE_URL = '/api' + +class ApiService { + constructor(baseUrl = BASE_URL) { + this.baseUrl = baseUrl + this.token = localStorage.getItem('token') + } + + setToken(token) { + this.token = token + if (token) { + localStorage.setItem('token', token) + } else { + localStorage.removeItem('token') + } + } + + getToken() { + return this.token || localStorage.getItem('token') + } + + async request(endpoint, options = {}) { + if (USE_MOCK) { + return this.mockRequest(endpoint, options) + } + + const url = `${this.baseUrl}${endpoint}` + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + } + + const token = this.getToken() + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + + try { + const response = await fetch(url, { + ...options, + headers, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({})) + throw new Error(error.message || `HTTP ${response.status}`) + } + + return await response.json() + } catch (error) { + console.error('API Error:', error) + throw error + } + } + + // ============ Mock 数据 ============ + mockData = { + user: { + id: 1, + username: 'admin', + nickname: '管理员', + avatar: '', + role: 'admin', + }, + projects: [ + { id: 1, name: '项目A', status: '进行中', progress: 75 }, + { id: 2, name: '项目B', status: '已完成', progress: 100 }, + { id: 3, name: '项目C', status: '待开始', progress: 0 }, + { id: 4, name: '项目D', status: '进行中', progress: 50 }, + ], + settings: [ + { key: 'siteName', label: '网站名称', value: 'Atlas Console' }, + { key: 'siteDesc', label: '网站描述', value: 'A powerful admin console' }, + { key: 'maintain', label: '维护模式', value: false, type: 'switch' }, + { key: 'maxUpload', label: '最大上传大小', value: '10MB' }, + ], + stats: { + totalUsers: 1234, + todayOrders: 567, + todayRevenue: 8888, + } + } + + mockRequest(endpoint, options) { + return new Promise((resolve, reject) => { + setTimeout(() => { + const { method = 'GET', body } = options + const data = body ? JSON.parse(body) : {} + + // 登录 + if (endpoint === '/auth/login' && method === 'POST') { + if (data.username === 'admin' && data.password === 'admin123') { + resolve({ + code: 0, + data: { + token: 'mock-token-' + Date.now(), + user: this.mockData.user, + }, + message: '登录成功' + }) + } else { + reject(new Error('用户名或密码错误')) + } + return + } + + // 获取用户信息 + if (endpoint === '/auth/info' && method === 'GET') { + resolve({ + code: 0, + data: this.mockData.user, + }) + return + } + + // 登出 + if (endpoint === '/auth/logout' && method === 'POST') { + resolve({ code: 0, message: '登出成功' }) + return + } + + // 项目列表 + if (endpoint === '/projects' && method === 'GET') { + resolve({ + code: 0, + data: this.mockData.projects, + }) + return + } + + // 系统设置 + if (endpoint === '/settings' && method === 'GET') { + resolve({ + code: 0, + data: this.mockData.settings, + }) + return + } + + // 保存设置 + if (endpoint === '/settings' && method === 'POST') { + resolve({ + code: 0, + message: '保存成功', + }) + return + } + + // 首页统计 + if (endpoint === '/stats' && method === 'GET') { + resolve({ + code: 0, + data: this.mockData.stats, + }) + return + } + + reject(new Error(`Mock: 未知接口 ${endpoint}`)) + }, 300) + }) + } + + // GET 请求 + get(endpoint) { + return this.request(endpoint, { method: 'GET' }) + } + + // POST 请求 + post(endpoint, data) { + return this.request(endpoint, { + method: 'POST', + body: JSON.stringify(data), + }) + } + + // PUT 请求 + put(endpoint, data) { + return this.request(endpoint, { + method: 'PUT', + body: JSON.stringify(data), + }) + } + + // DELETE 请求 + delete(endpoint) { + return this.request(endpoint, { method: 'DELETE' }) + } +} + +export const api = new ApiService() +export default api \ No newline at end of file diff --git a/src/api/modules/auth.js b/src/api/modules/auth.js new file mode 100644 index 0000000..00e777d --- /dev/null +++ b/src/api/modules/auth.js @@ -0,0 +1,31 @@ +/** + * 认证相关 API + */ +import api from '../index' + +export const authApi = { + /** + * 登录 + * @param {string} username 用户名 + * @param {string} password 密码 + */ + login(username, password) { + return api.post('/auth/login', { username, password }) + }, + + /** + * 获取用户信息 + */ + getUserInfo() { + return api.get('/auth/info') + }, + + /** + * 登出 + */ + logout() { + return api.post('/auth/logout') + }, +} + +export default authApi \ No newline at end of file diff --git a/src/api/modules/project.js b/src/api/modules/project.js new file mode 100644 index 0000000..2421beb --- /dev/null +++ b/src/api/modules/project.js @@ -0,0 +1,48 @@ +/** + * 项目相关 API + */ +import api from '../index' + +export const projectApi = { + /** + * 获取项目列表 + */ + getList() { + return api.get('/projects') + }, + + /** + * 获取项目详情 + * @param {number} id 项目ID + */ + getDetail(id) { + return api.get(`/projects/${id}`) + }, + + /** + * 创建项目 + * @param {object} data 项目数据 + */ + create(data) { + return api.post('/projects', data) + }, + + /** + * 更新项目 + * @param {number} id 项目ID + * @param {object} data 项目数据 + */ + update(id, data) { + return api.put(`/projects/${id}`, data) + }, + + /** + * 删除项目 + * @param {number} id 项目ID + */ + delete(id) { + return api.delete(`/projects/${id}`) + }, +} + +export default projectApi \ No newline at end of file diff --git a/src/api/modules/settings.js b/src/api/modules/settings.js new file mode 100644 index 0000000..16a3ba9 --- /dev/null +++ b/src/api/modules/settings.js @@ -0,0 +1,23 @@ +/** + * 系统设置相关 API + */ +import api from '../index' + +export const settingsApi = { + /** + * 获取系统设置 + */ + getList() { + return api.get('/settings') + }, + + /** + * 保存系统设置 + * @param {Array} settings 设置列表 + */ + save(settings) { + return api.post('/settings', { settings }) + }, +} + +export default settingsApi \ No newline at end of file diff --git a/src/api/modules/stats.js b/src/api/modules/stats.js new file mode 100644 index 0000000..9b3b57d --- /dev/null +++ b/src/api/modules/stats.js @@ -0,0 +1,15 @@ +/** + * 统计数据相关 API + */ +import api from '../index' + +export const statsApi = { + /** + * 获取首页统计数据 + */ + getDashboardStats() { + return api.get('/stats') + }, +} + +export default statsApi \ No newline at end of file diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx index 8d117fb..a9e8cfb 100644 --- a/src/pages/Home.jsx +++ b/src/pages/Home.jsx @@ -1,24 +1,55 @@ +import { useState, useEffect } from 'react' +import { statsApi } from '../api/modules/stats' + function Home() { + const [stats, setStats] = useState({ + totalUsers: 0, + todayOrders: 0, + todayRevenue: 0, + }) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchStats() + }, []) + + const fetchStats = async () => { + try { + const response = await statsApi.getDashboardStats() + if (response.code === 0) { + setStats(response.data) + } + } catch (error) { + console.error('Failed to fetch stats:', error) + } finally { + setLoading(false) + } + } + return (

欢迎来到管理后台

-
-
-

📊 数据统计

-

1,234

-

总用户数

+ {loading ? ( +

加载中...

+ ) : ( +
+
+

📊 数据统计

+

{stats.totalUsers.toLocaleString()}

+

总用户数

+
+
+

📦 订单

+

{stats.todayOrders.toLocaleString()}

+

今日订单

+
+
+

💰 收入

+

¥{stats.todayRevenue.toLocaleString()}

+

今日收入

+
-
-

📦 订单

-

567

-

今日订单

-
-
-

💰 收入

-

¥8,888

-

今日收入

-
-
+ )}
) } diff --git a/src/pages/Layout.jsx b/src/pages/Layout.jsx index 05fa438..93d3ef9 100644 --- a/src/pages/Layout.jsx +++ b/src/pages/Layout.jsx @@ -1,13 +1,22 @@ import { useState } from 'react' import { Outlet, NavLink, useNavigate } from 'react-router-dom' +import { authApi } from '../api/modules/auth' +import api from '../api' function Layout({ onLogout }) { const [collapsed, setCollapsed] = useState(false) const navigate = useNavigate() - const handleLogout = () => { - onLogout() - navigate('/login') + const handleLogout = async () => { + try { + await authApi.logout() + } catch (e) { + console.error('Logout error:', e) + } finally { + api.setToken(null) + onLogout() + navigate('/login') + } } return ( @@ -15,7 +24,7 @@ function Layout({ onLogout }) { {/* 侧边栏 */}
- {collapsed ? 'A' : 'Admin'} + {collapsed ? 'A' : 'Atlas'}
diff --git a/src/pages/Menu1.jsx b/src/pages/Menu1.jsx index fc0efb5..59f9e0c 100644 --- a/src/pages/Menu1.jsx +++ b/src/pages/Menu1.jsx @@ -1,42 +1,70 @@ +import { useState, useEffect } from 'react' +import { projectApi } from '../api/modules/project' + function Menu1() { - const data = [ - { id: 1, name: '项目A', status: '进行中', progress: 75 }, - { id: 2, name: '项目B', status: '已完成', progress: 100 }, - { id: 3, name: '项目C', status: '待开始', progress: 0 }, - { id: 4, name: '项目D', status: '进行中', progress: 50 }, - ] + const [projects, setProjects] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchProjects() + }, []) + + const fetchProjects = async () => { + try { + const response = await projectApi.getList() + if (response.code === 0) { + setProjects(response.data) + } + } catch (error) { + console.error('Failed to fetch projects:', error) + } finally { + setLoading(false) + } + } + + const getStatusColor = (status) => { + switch (status) { + case '已完成': return '#52c41a' + case '进行中': return '#1890ff' + default: return '#d9d9d9' + } + } return (

菜单1 - 项目管理

-
-
-
ID
-
项目名称
-
状态
-
进度
-
- {data.map(item => ( -
-
{item.id}
-
{item.name}
-
- - {item.status} - -
-
-
-
-
- {item.progress}% -
+ {loading ? ( +

加载中...

+ ) : ( +
+
+
ID
+
项目名称
+
状态
+
进度
- ))} -
+ {projects.map(item => ( +
+
{item.id}
+
{item.name}
+
+ + {item.status} + +
+
+
+
+
+ {item.progress}% +
+
+ ))} +
+ )}
) } diff --git a/src/pages/Menu2.jsx b/src/pages/Menu2.jsx index 293a7fa..b552e32 100644 --- a/src/pages/Menu2.jsx +++ b/src/pages/Menu2.jsx @@ -1,38 +1,107 @@ +import { useState, useEffect } from 'react' +import { settingsApi } from '../api/modules/settings' + function Menu2() { - const settings = [ - { key: 'siteName', label: '网站名称', value: '我的管理系统' }, - { key: 'siteDesc', label: '网站描述', value: '一个简单的React管理后台' }, - { key: 'maintain', label: '维护模式', value: '关闭', type: 'switch' }, - { key: 'maxUpload', label: '最大上传大小', value: '10MB' }, - ] + const [settings, setSettings] = useState([]) + const [loading, setLoading] = useState(true) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState('') + + useEffect(() => { + fetchSettings() + }, []) + + const fetchSettings = async () => { + try { + const response = await settingsApi.getList() + if (response.code === 0) { + setSettings(response.data) + } + } catch (error) { + console.error('Failed to fetch settings:', error) + } finally { + setLoading(false) + } + } + + const handleSave = async () => { + setSaving(true) + setMessage('') + try { + const response = await settingsApi.save(settings) + if (response.code === 0) { + setMessage('保存成功') + setTimeout(() => setMessage(''), 3000) + } + } catch (error) { + setMessage('保存失败: ' + error.message) + } finally { + setSaving(false) + } + } + + const updateSetting = (key, value) => { + setSettings(settings.map(s => + s.key === key ? { ...s, value } : s + )) + } return (

菜单2 - 系统设置

-
- {settings.map(item => ( -
-
{item.label}
-
- {item.type === 'switch' ? ( - - ) : ( - - )} + {loading ? ( +

加载中...

+ ) : ( +
+ {settings.map(item => ( +
+
{item.label}
+
+ {item.type === 'switch' ? ( + + ) : ( + updateSetting(item.key, e.target.value)} + style={styles.input} + /> + )} +
+ ))} +
+ + {message && ( + + {message} + + )}
- ))} -
-
-
+ )}
) } @@ -73,6 +142,11 @@ const styles = { width: '44px', height: '22px', }, + switchInput: { + opacity: 0, + width: 0, + height: 0, + }, slider: { position: 'absolute', cursor: 'pointer', @@ -84,9 +158,22 @@ const styles = { transition: '0.3s', borderRadius: '22px', }, + sliderBefore: { + position: 'absolute', + content: '""', + height: '18px', + width: '18px', + left: '2px', + bottom: '2px', + backgroundColor: 'white', + transition: '0.3s', + borderRadius: '50%', + }, actions: { marginTop: '24px', - textAlign: 'right', + display: 'flex', + alignItems: 'center', + gap: '16px', }, saveBtn: { padding: '10px 24px', @@ -97,6 +184,14 @@ const styles = { cursor: 'pointer', fontSize: '14px', }, + successMsg: { + color: '#52c41a', + fontSize: '14px', + }, + errorMsg: { + color: '#ff4d4f', + fontSize: '14px', + }, } export default Menu2 \ No newline at end of file