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)
|
||||
- [@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
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
||||
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() {
|
||||
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 (
|
||||
<div>
|
||||
<h1>欢迎来到管理后台</h1>
|
||||
{loading ? (
|
||||
<p>加载中...</p>
|
||||
) : (
|
||||
<div style={styles.cards}>
|
||||
<div style={styles.card}>
|
||||
<h3>📊 数据统计</h3>
|
||||
<p style={styles.number}>1,234</p>
|
||||
<p style={styles.number}>{stats.totalUsers.toLocaleString()}</p>
|
||||
<p>总用户数</p>
|
||||
</div>
|
||||
<div style={styles.card}>
|
||||
<h3>📦 订单</h3>
|
||||
<p style={styles.number}>567</p>
|
||||
<p style={styles.number}>{stats.todayOrders.toLocaleString()}</p>
|
||||
<p>今日订单</p>
|
||||
</div>
|
||||
<div style={styles.card}>
|
||||
<h3>💰 收入</h3>
|
||||
<p style={styles.number}>¥8,888</p>
|
||||
<p style={styles.number}>¥{stats.todayRevenue.toLocaleString()}</p>
|
||||
<p>今日收入</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,21 +1,30 @@
|
||||
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 = () => {
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await authApi.logout()
|
||||
} catch (e) {
|
||||
console.error('Logout error:', e)
|
||||
} finally {
|
||||
api.setToken(null)
|
||||
onLogout()
|
||||
navigate('/login')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* 侧边栏 */}
|
||||
<div style={{ ...styles.sidebar, width: collapsed ? '80px' : '200px' }}>
|
||||
<div style={styles.logo}>
|
||||
{collapsed ? 'A' : 'Admin'}
|
||||
{collapsed ? 'A' : 'Atlas'}
|
||||
</div>
|
||||
<nav style={styles.nav}>
|
||||
<NavLink to="/" style={({ isActive }) => isActive ? { ...styles.navItem, ...styles.active } : styles.navItem}>
|
||||
|
||||
@ -1,16 +1,38 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { authApi } from '../api/modules/auth'
|
||||
import api from '../api'
|
||||
|
||||
function Login({ onLogin }) {
|
||||
const navigate = useNavigate()
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (username === 'admin' && password === 'admin123') {
|
||||
console.log('Login clicked', { username, password })
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
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('用户名或密码错误')
|
||||
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}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={styles.input}
|
||||
disabled={loading}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
@ -32,9 +55,12 @@ function Login({ onLogin }) {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={styles.input}
|
||||
disabled={loading}
|
||||
/>
|
||||
{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>
|
||||
<p style={styles.hint}>测试账号: admin / admin123</p>
|
||||
</div>
|
||||
|
||||
@ -1,14 +1,41 @@
|
||||
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 (
|
||||
<div>
|
||||
<h1>菜单1 - 项目管理</h1>
|
||||
{loading ? (
|
||||
<p>加载中...</p>
|
||||
) : (
|
||||
<div style={styles.table}>
|
||||
<div style={styles.header}>
|
||||
<div style={{...styles.cell, flex: 1}}>ID</div>
|
||||
@ -16,14 +43,14 @@ function Menu1() {
|
||||
<div style={{...styles.cell, flex: 1}}>状态</div>
|
||||
<div style={{...styles.cell, flex: 2}}>进度</div>
|
||||
</div>
|
||||
{data.map(item => (
|
||||
{projects.map(item => (
|
||||
<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: item.status === '已完成' ? '#52c41a' : item.status === '进行中' ? '#1890ff' : '#d9d9d9'
|
||||
backgroundColor: getStatusColor(item.status)
|
||||
}}>
|
||||
{item.status}
|
||||
</span>
|
||||
@ -37,6 +64,7 @@ function Menu1() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,14 +1,57 @@
|
||||
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 (
|
||||
<div>
|
||||
<h1>菜单2 - 系统设置</h1>
|
||||
{loading ? (
|
||||
<p>加载中...</p>
|
||||
) : (
|
||||
<div style={styles.card}>
|
||||
{settings.map(item => (
|
||||
<div key={item.key} style={styles.row}>
|
||||
@ -16,13 +59,27 @@ function Menu2() {
|
||||
<div style={styles.value}>
|
||||
{item.type === 'switch' ? (
|
||||
<label style={styles.switch}>
|
||||
<input type="checkbox" defaultChecked={item.value === '开启'} />
|
||||
<span style={styles.slider}></span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.value === true}
|
||||
onChange={(e) => updateSetting(item.key, e.target.checked)}
|
||||
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"
|
||||
defaultValue={item.value}
|
||||
value={item.value}
|
||||
onChange={(e) => updateSetting(item.key, e.target.value)}
|
||||
style={styles.input}
|
||||
/>
|
||||
)}
|
||||
@ -30,9 +87,21 @@ function Menu2() {
|
||||
</div>
|
||||
))}
|
||||
<div style={styles.actions}>
|
||||
<button style={styles.saveBtn}>保存设置</button>
|
||||
<button
|
||||
style={styles.saveBtn}
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? '保存中...' : '保存设置'}
|
||||
</button>
|
||||
{message && (
|
||||
<span style={message.includes('成功') ? styles.successMsg : styles.errorMsg}>
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
Loading…
Reference in New Issue
Block a user