feat: 集成基本功能
This commit is contained in:
parent
2d6bb0a096
commit
a746c26f94
204
README.md
204
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)
|
- 🔐 Login authentication
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
- 📱 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
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "micah",
|
"name": "atlas-console",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "micah",
|
"name": "atlas-console",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
|||||||
199
src/api/index.js
Normal file
199
src/api/index.js
Normal file
@ -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
|
||||||
31
src/api/modules/auth.js
Normal file
31
src/api/modules/auth.js
Normal file
@ -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
|
||||||
48
src/api/modules/project.js
Normal file
48
src/api/modules/project.js
Normal file
@ -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
|
||||||
23
src/api/modules/settings.js
Normal file
23
src/api/modules/settings.js
Normal file
@ -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
|
||||||
15
src/api/modules/stats.js
Normal file
15
src/api/modules/stats.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 统计数据相关 API
|
||||||
|
*/
|
||||||
|
import api from '../index'
|
||||||
|
|
||||||
|
export const statsApi = {
|
||||||
|
/**
|
||||||
|
* 获取首页统计数据
|
||||||
|
*/
|
||||||
|
getDashboardStats() {
|
||||||
|
return api.get('/stats')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default statsApi
|
||||||
@ -1,24 +1,55 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { statsApi } from '../api/modules/stats'
|
||||||
|
|
||||||
function Home() {
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>欢迎来到管理后台</h1>
|
<h1>欢迎来到管理后台</h1>
|
||||||
<div style={styles.cards}>
|
{loading ? (
|
||||||
<div style={styles.card}>
|
<p>加载中...</p>
|
||||||
<h3>📊 数据统计</h3>
|
) : (
|
||||||
<p style={styles.number}>1,234</p>
|
<div style={styles.cards}>
|
||||||
<p>总用户数</p>
|
<div style={styles.card}>
|
||||||
|
<h3>📊 数据统计</h3>
|
||||||
|
<p style={styles.number}>{stats.totalUsers.toLocaleString()}</p>
|
||||||
|
<p>总用户数</p>
|
||||||
|
</div>
|
||||||
|
<div style={styles.card}>
|
||||||
|
<h3>📦 订单</h3>
|
||||||
|
<p style={styles.number}>{stats.todayOrders.toLocaleString()}</p>
|
||||||
|
<p>今日订单</p>
|
||||||
|
</div>
|
||||||
|
<div style={styles.card}>
|
||||||
|
<h3>💰 收入</h3>
|
||||||
|
<p style={styles.number}>¥{stats.todayRevenue.toLocaleString()}</p>
|
||||||
|
<p>今日收入</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.card}>
|
)}
|
||||||
<h3>📦 订单</h3>
|
|
||||||
<p style={styles.number}>567</p>
|
|
||||||
<p>今日订单</p>
|
|
||||||
</div>
|
|
||||||
<div style={styles.card}>
|
|
||||||
<h3>💰 收入</h3>
|
|
||||||
<p style={styles.number}>¥8,888</p>
|
|
||||||
<p>今日收入</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,22 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom'
|
import { Outlet, NavLink, useNavigate } from 'react-router-dom'
|
||||||
|
import { authApi } from '../api/modules/auth'
|
||||||
|
import api from '../api'
|
||||||
|
|
||||||
function Layout({ onLogout }) {
|
function Layout({ onLogout }) {
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = async () => {
|
||||||
onLogout()
|
try {
|
||||||
navigate('/login')
|
await authApi.logout()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Logout error:', e)
|
||||||
|
} finally {
|
||||||
|
api.setToken(null)
|
||||||
|
onLogout()
|
||||||
|
navigate('/login')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -15,7 +24,7 @@ function Layout({ onLogout }) {
|
|||||||
{/* 侧边栏 */}
|
{/* 侧边栏 */}
|
||||||
<div style={{ ...styles.sidebar, width: collapsed ? '80px' : '200px' }}>
|
<div style={{ ...styles.sidebar, width: collapsed ? '80px' : '200px' }}>
|
||||||
<div style={styles.logo}>
|
<div style={styles.logo}>
|
||||||
{collapsed ? 'A' : 'Admin'}
|
{collapsed ? 'A' : 'Atlas'}
|
||||||
</div>
|
</div>
|
||||||
<nav style={styles.nav}>
|
<nav style={styles.nav}>
|
||||||
<NavLink to="/" style={({ isActive }) => isActive ? { ...styles.navItem, ...styles.active } : styles.navItem}>
|
<NavLink to="/" style={({ isActive }) => isActive ? { ...styles.navItem, ...styles.active } : styles.navItem}>
|
||||||
|
|||||||
@ -1,16 +1,38 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { authApi } from '../api/modules/auth'
|
||||||
|
import api from '../api'
|
||||||
|
|
||||||
function Login({ onLogin }) {
|
function Login({ onLogin }) {
|
||||||
|
const navigate = useNavigate()
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (username === 'admin' && password === 'admin123') {
|
console.log('Login clicked', { username, password })
|
||||||
onLogin()
|
setError('')
|
||||||
} else {
|
setLoading(true)
|
||||||
setError('用户名或密码错误')
|
|
||||||
|
try {
|
||||||
|
// 调用登录 API(使用 Mock 或真实接口)
|
||||||
|
const response = await authApi.login(username, password)
|
||||||
|
console.log('Login response:', response)
|
||||||
|
|
||||||
|
if (response.code === 0) {
|
||||||
|
api.setToken(response.data.token)
|
||||||
|
onLogin()
|
||||||
|
navigate('/')
|
||||||
|
} else {
|
||||||
|
setError(response.message || '登录失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login error:', err)
|
||||||
|
setError(err.message || '用户名或密码错误')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,6 +47,7 @@ function Login({ onLogin }) {
|
|||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
@ -32,9 +55,12 @@ function Login({ onLogin }) {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
{error && <p style={styles.error}>{error}</p>}
|
{error && <p style={styles.error}>{error}</p>}
|
||||||
<button type="submit" style={styles.button}>登录</button>
|
<button type="submit" style={styles.button} disabled={loading}>
|
||||||
|
{loading ? '登录中...' : '登录'}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<p style={styles.hint}>测试账号: admin / admin123</p>
|
<p style={styles.hint}>测试账号: admin / admin123</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,42 +1,70 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { projectApi } from '../api/modules/project'
|
||||||
|
|
||||||
function Menu1() {
|
function Menu1() {
|
||||||
const data = [
|
const [projects, setProjects] = useState([])
|
||||||
{ id: 1, name: '项目A', status: '进行中', progress: 75 },
|
const [loading, setLoading] = useState(true)
|
||||||
{ id: 2, name: '项目B', status: '已完成', progress: 100 },
|
|
||||||
{ id: 3, name: '项目C', status: '待开始', progress: 0 },
|
useEffect(() => {
|
||||||
{ id: 4, name: '项目D', status: '进行中', progress: 50 },
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>菜单1 - 项目管理</h1>
|
<h1>菜单1 - 项目管理</h1>
|
||||||
<div style={styles.table}>
|
{loading ? (
|
||||||
<div style={styles.header}>
|
<p>加载中...</p>
|
||||||
<div style={{...styles.cell, flex: 1}}>ID</div>
|
) : (
|
||||||
<div style={{...styles.cell, flex: 2}}>项目名称</div>
|
<div style={styles.table}>
|
||||||
<div style={{...styles.cell, flex: 1}}>状态</div>
|
<div style={styles.header}>
|
||||||
<div style={{...styles.cell, flex: 2}}>进度</div>
|
<div style={{...styles.cell, flex: 1}}>ID</div>
|
||||||
</div>
|
<div style={{...styles.cell, flex: 2}}>项目名称</div>
|
||||||
{data.map(item => (
|
<div style={{...styles.cell, flex: 1}}>状态</div>
|
||||||
<div key={item.id} style={styles.row}>
|
<div style={{...styles.cell, flex: 2}}>进度</div>
|
||||||
<div style={{...styles.cell, flex: 1}}>{item.id}</div>
|
|
||||||
<div style={{...styles.cell, flex: 2}}>{item.name}</div>
|
|
||||||
<div style={{...styles.cell, flex: 1}}>
|
|
||||||
<span style={{
|
|
||||||
...styles.badge,
|
|
||||||
backgroundColor: item.status === '已完成' ? '#52c41a' : item.status === '进行中' ? '#1890ff' : '#d9d9d9'
|
|
||||||
}}>
|
|
||||||
{item.status}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style={{...styles.cell, flex: 2}}>
|
|
||||||
<div style={styles.progressBg}>
|
|
||||||
<div style={{...styles.progressBar, width: `${item.progress}%`}}></div>
|
|
||||||
</div>
|
|
||||||
<span style={styles.progressText}>{item.progress}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
{projects.map(item => (
|
||||||
</div>
|
<div key={item.id} style={styles.row}>
|
||||||
|
<div style={{...styles.cell, flex: 1}}>{item.id}</div>
|
||||||
|
<div style={{...styles.cell, flex: 2}}>{item.name}</div>
|
||||||
|
<div style={{...styles.cell, flex: 1}}>
|
||||||
|
<span style={{
|
||||||
|
...styles.badge,
|
||||||
|
backgroundColor: getStatusColor(item.status)
|
||||||
|
}}>
|
||||||
|
{item.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{...styles.cell, flex: 2}}>
|
||||||
|
<div style={styles.progressBg}>
|
||||||
|
<div style={{...styles.progressBar, width: `${item.progress}%`}}></div>
|
||||||
|
</div>
|
||||||
|
<span style={styles.progressText}>{item.progress}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,38 +1,107 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { settingsApi } from '../api/modules/settings'
|
||||||
|
|
||||||
function Menu2() {
|
function Menu2() {
|
||||||
const settings = [
|
const [settings, setSettings] = useState([])
|
||||||
{ key: 'siteName', label: '网站名称', value: '我的管理系统' },
|
const [loading, setLoading] = useState(true)
|
||||||
{ key: 'siteDesc', label: '网站描述', value: '一个简单的React管理后台' },
|
const [saving, setSaving] = useState(false)
|
||||||
{ key: 'maintain', label: '维护模式', value: '关闭', type: 'switch' },
|
const [message, setMessage] = useState('')
|
||||||
{ key: 'maxUpload', label: '最大上传大小', value: '10MB' },
|
|
||||||
]
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>菜单2 - 系统设置</h1>
|
<h1>菜单2 - 系统设置</h1>
|
||||||
<div style={styles.card}>
|
{loading ? (
|
||||||
{settings.map(item => (
|
<p>加载中...</p>
|
||||||
<div key={item.key} style={styles.row}>
|
) : (
|
||||||
<div style={styles.label}>{item.label}</div>
|
<div style={styles.card}>
|
||||||
<div style={styles.value}>
|
{settings.map(item => (
|
||||||
{item.type === 'switch' ? (
|
<div key={item.key} style={styles.row}>
|
||||||
<label style={styles.switch}>
|
<div style={styles.label}>{item.label}</div>
|
||||||
<input type="checkbox" defaultChecked={item.value === '开启'} />
|
<div style={styles.value}>
|
||||||
<span style={styles.slider}></span>
|
{item.type === 'switch' ? (
|
||||||
</label>
|
<label style={styles.switch}>
|
||||||
) : (
|
<input
|
||||||
<input
|
type="checkbox"
|
||||||
type="text"
|
checked={item.value === true}
|
||||||
defaultValue={item.value}
|
onChange={(e) => updateSetting(item.key, e.target.checked)}
|
||||||
style={styles.input}
|
style={styles.switchInput}
|
||||||
/>
|
/>
|
||||||
)}
|
<span style={{
|
||||||
|
...styles.slider,
|
||||||
|
backgroundColor: item.value === true ? '#52c41a' : '#ccc',
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
...styles.sliderBefore,
|
||||||
|
transform: item.value === true ? 'translateX(22px)' : 'translateX(0)',
|
||||||
|
}}></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={item.value}
|
||||||
|
onChange={(e) => updateSetting(item.key, e.target.value)}
|
||||||
|
style={styles.input}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={styles.actions}>
|
||||||
|
<button
|
||||||
|
style={styles.saveBtn}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{saving ? '保存中...' : '保存设置'}
|
||||||
|
</button>
|
||||||
|
{message && (
|
||||||
|
<span style={message.includes('成功') ? styles.successMsg : styles.errorMsg}>
|
||||||
|
{message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
<div style={styles.actions}>
|
|
||||||
<button style={styles.saveBtn}>保存设置</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -73,6 +142,11 @@ const styles = {
|
|||||||
width: '44px',
|
width: '44px',
|
||||||
height: '22px',
|
height: '22px',
|
||||||
},
|
},
|
||||||
|
switchInput: {
|
||||||
|
opacity: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
slider: {
|
slider: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@ -84,9 +158,22 @@ const styles = {
|
|||||||
transition: '0.3s',
|
transition: '0.3s',
|
||||||
borderRadius: '22px',
|
borderRadius: '22px',
|
||||||
},
|
},
|
||||||
|
sliderBefore: {
|
||||||
|
position: 'absolute',
|
||||||
|
content: '""',
|
||||||
|
height: '18px',
|
||||||
|
width: '18px',
|
||||||
|
left: '2px',
|
||||||
|
bottom: '2px',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
transition: '0.3s',
|
||||||
|
borderRadius: '50%',
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
marginTop: '24px',
|
marginTop: '24px',
|
||||||
textAlign: 'right',
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '16px',
|
||||||
},
|
},
|
||||||
saveBtn: {
|
saveBtn: {
|
||||||
padding: '10px 24px',
|
padding: '10px 24px',
|
||||||
@ -97,6 +184,14 @@ const styles = {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
},
|
},
|
||||||
|
successMsg: {
|
||||||
|
color: '#52c41a',
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
|
errorMsg: {
|
||||||
|
color: '#ff4d4f',
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Menu2
|
export default Menu2
|
||||||
Loading…
Reference in New Issue
Block a user