feat: 集成基本功能

This commit is contained in:
micah 2026-03-14 14:16:13 +08:00
parent 2d6bb0a096
commit a746c26f94
12 changed files with 789 additions and 98 deletions

204
README.md
View File

@ -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
View File

@ -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
View 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
View 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

View 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

View 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
View File

@ -0,0 +1,15 @@
/**
* 统计数据相关 API
*/
import api from '../index'
export const statsApi = {
/**
* 获取首页统计数据
*/
getDashboardStats() {
return api.get('/stats')
},
}
export default statsApi

View File

@ -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>
{loading ? (
<p>加载中...</p>
) : (
<div style={styles.cards}> <div style={styles.cards}>
<div style={styles.card}> <div style={styles.card}>
<h3>📊 数据统计</h3> <h3>📊 数据统计</h3>
<p style={styles.number}>1,234</p> <p style={styles.number}>{stats.totalUsers.toLocaleString()}</p>
<p>总用户数</p> <p>总用户数</p>
</div> </div>
<div style={styles.card}> <div style={styles.card}>
<h3>📦 订单</h3> <h3>📦 订单</h3>
<p style={styles.number}>567</p> <p style={styles.number}>{stats.todayOrders.toLocaleString()}</p>
<p>今日订单</p> <p>今日订单</p>
</div> </div>
<div style={styles.card}> <div style={styles.card}>
<h3>💰 收入</h3> <h3>💰 收入</h3>
<p style={styles.number}>¥8,888</p> <p style={styles.number}>¥{stats.todayRevenue.toLocaleString()}</p>
<p>今日收入</p> <p>今日收入</p>
</div> </div>
</div> </div>
)}
</div> </div>
) )
} }

View File

@ -1,21 +1,30 @@
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 () => {
try {
await authApi.logout()
} catch (e) {
console.error('Logout error:', e)
} finally {
api.setToken(null)
onLogout() onLogout()
navigate('/login') navigate('/login')
} }
}
return ( return (
<div style={styles.container}> <div style={styles.container}>
{/* 侧边栏 */} {/* 侧边栏 */}
<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}>

View File

@ -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 })
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() onLogin()
navigate('/')
} else { } 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} 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>

View File

@ -1,14 +1,41 @@
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>
{loading ? (
<p>加载中...</p>
) : (
<div style={styles.table}> <div style={styles.table}>
<div style={styles.header}> <div style={styles.header}>
<div style={{...styles.cell, flex: 1}}>ID</div> <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: 1}}>状态</div>
<div style={{...styles.cell, flex: 2}}>进度</div> <div style={{...styles.cell, flex: 2}}>进度</div>
</div> </div>
{data.map(item => ( {projects.map(item => (
<div key={item.id} style={styles.row}> <div key={item.id} style={styles.row}>
<div style={{...styles.cell, flex: 1}}>{item.id}</div> <div style={{...styles.cell, flex: 1}}>{item.id}</div>
<div style={{...styles.cell, flex: 2}}>{item.name}</div> <div style={{...styles.cell, flex: 2}}>{item.name}</div>
<div style={{...styles.cell, flex: 1}}> <div style={{...styles.cell, flex: 1}}>
<span style={{ <span style={{
...styles.badge, ...styles.badge,
backgroundColor: item.status === '已完成' ? '#52c41a' : item.status === '进行中' ? '#1890ff' : '#d9d9d9' backgroundColor: getStatusColor(item.status)
}}> }}>
{item.status} {item.status}
</span> </span>
@ -37,6 +64,7 @@ function Menu1() {
</div> </div>
))} ))}
</div> </div>
)}
</div> </div>
) )
} }

View File

@ -1,14 +1,57 @@
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>
{loading ? (
<p>加载中...</p>
) : (
<div style={styles.card}> <div style={styles.card}>
{settings.map(item => ( {settings.map(item => (
<div key={item.key} style={styles.row}> <div key={item.key} style={styles.row}>
@ -16,13 +59,27 @@ function Menu2() {
<div style={styles.value}> <div style={styles.value}>
{item.type === 'switch' ? ( {item.type === 'switch' ? (
<label style={styles.switch}> <label style={styles.switch}>
<input type="checkbox" defaultChecked={item.value === '开启'} /> <input
<span style={styles.slider}></span> 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> </label>
) : ( ) : (
<input <input
type="text" type="text"
defaultValue={item.value} value={item.value}
onChange={(e) => updateSetting(item.key, e.target.value)}
style={styles.input} style={styles.input}
/> />
)} )}
@ -30,9 +87,21 @@ function Menu2() {
</div> </div>
))} ))}
<div style={styles.actions}> <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> </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