feat: 集成基本功能
This commit is contained in:
parent
a746c26f94
commit
ba94d299e3
967
package-lock.json
generated
967
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,8 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"antd": "^6.3.2",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-router-dom": "^7.13.1"
|
||||
|
||||
@ -8,6 +8,13 @@ const USE_MOCK = true // 启用 Mock 数据
|
||||
|
||||
const BASE_URL = '/api'
|
||||
|
||||
// 全局 Message API 实例
|
||||
let messageApi = null
|
||||
|
||||
export const setMessageApi = (api) => {
|
||||
messageApi = api
|
||||
}
|
||||
|
||||
class ApiService {
|
||||
constructor(baseUrl = BASE_URL) {
|
||||
this.baseUrl = baseUrl
|
||||
@ -27,6 +34,15 @@ class ApiService {
|
||||
return this.token || localStorage.getItem('token')
|
||||
}
|
||||
|
||||
// 显示错误消息
|
||||
showError(message) {
|
||||
if (messageApi) {
|
||||
messageApi.error(message)
|
||||
} else {
|
||||
console.error('Message API not initialized:', message)
|
||||
}
|
||||
}
|
||||
|
||||
async request(endpoint, options = {}) {
|
||||
if (USE_MOCK) {
|
||||
return this.mockRequest(endpoint, options)
|
||||
@ -51,13 +67,16 @@ class ApiService {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}))
|
||||
throw new Error(error.message || `HTTP ${response.status}`)
|
||||
const errMsg = error.message || `HTTP ${response.status}`
|
||||
this.showError(errMsg)
|
||||
return { code: -1, message: errMsg }
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} catch (error) {
|
||||
console.error('API Error:', error)
|
||||
throw error
|
||||
this.showError(error.message || '网络错误')
|
||||
return { code: -1, message: error.message || '网络错误' }
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,7 +109,7 @@ class ApiService {
|
||||
}
|
||||
|
||||
mockRequest(endpoint, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const { method = 'GET', body } = options
|
||||
const data = body ? JSON.parse(body) : {}
|
||||
@ -107,7 +126,8 @@ class ApiService {
|
||||
message: '登录成功'
|
||||
})
|
||||
} else {
|
||||
reject(new Error('用户名或密码错误'))
|
||||
this.showError('用户名或密码错误')
|
||||
resolve({ code: -1, message: '用户名或密码错误' })
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -163,7 +183,9 @@ class ApiService {
|
||||
return
|
||||
}
|
||||
|
||||
reject(new Error(`Mock: 未知接口 ${endpoint}`))
|
||||
const errMsg = `Mock: 未知接口 ${endpoint}`
|
||||
this.showError(errMsg)
|
||||
resolve({ code: -1, message: errMsg })
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
*/
|
||||
import api from '../index'
|
||||
|
||||
export const authApi = {
|
||||
const authApi = {
|
||||
/**
|
||||
* 登录
|
||||
* @param {string} username 用户名
|
||||
|
||||
23
src/main.jsx
23
src/main.jsx
@ -1,10 +1,31 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { ConfigProvider, App as AntApp } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import 'antd/dist/reset.css'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
import { setMessageApi } from './api'
|
||||
|
||||
// App 组件容器,用于获取 message API
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
function AppWrapper() {
|
||||
const { message } = AntApp.useApp()
|
||||
|
||||
// 初始化时设置 message API
|
||||
if (message) {
|
||||
setMessageApi(message)
|
||||
}
|
||||
|
||||
return <App />
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<AntApp>
|
||||
<AppWrapper />
|
||||
</AntApp>
|
||||
</ConfigProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
@ -1,5 +1,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { statsApi } from '../api/modules/stats'
|
||||
import { Card, Row, Col, Statistic, Spin } from 'antd'
|
||||
import { UserOutlined, ShoppingOutlined, DollarOutlined } from '@ant-design/icons'
|
||||
import statsApi from '../api/modules/stats'
|
||||
|
||||
function Home() {
|
||||
const [stats, setStats] = useState({
|
||||
@ -28,50 +30,60 @@ function Home() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>欢迎来到管理后台</h1>
|
||||
<h2 style={styles.title}>欢迎来到 Atlas Console</h2>
|
||||
|
||||
{loading ? (
|
||||
<p>加载中...</p>
|
||||
<div style={styles.loading}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
) : (
|
||||
<div style={styles.cards}>
|
||||
<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>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总用户数"
|
||||
value={stats.totalUsers}
|
||||
prefix={<UserOutlined />}
|
||||
styles={{ value: { color: '#3f8600' } }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="今日订单"
|
||||
value={stats.todayOrders}
|
||||
prefix={<ShoppingOutlined />}
|
||||
styles={{ value: { color: '#1890ff' } }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="今日收入"
|
||||
value={stats.todayRevenue}
|
||||
prefix={<DollarOutlined />}
|
||||
precision={2}
|
||||
suffix="元"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
cards: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 1fr)',
|
||||
gap: '24px',
|
||||
marginTop: '24px',
|
||||
title: {
|
||||
marginBottom: '24px',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
padding: '24px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
},
|
||||
number: {
|
||||
fontSize: '32px',
|
||||
fontWeight: 'bold',
|
||||
color: '#1890ff',
|
||||
margin: '16px 0',
|
||||
loading: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '200px',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,25 @@
|
||||
import { useState } from 'react'
|
||||
import { Outlet, NavLink, useNavigate } from 'react-router-dom'
|
||||
import { authApi } from '../api/modules/auth'
|
||||
import { Layout as AntLayout, Menu, Button, theme } from 'antd'
|
||||
import {
|
||||
HomeOutlined,
|
||||
AppstoreOutlined,
|
||||
SettingOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
LogoutOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import authApi from '../api/modules/auth'
|
||||
import api from '../api'
|
||||
|
||||
const { Header, Sider, Content } = AntLayout
|
||||
|
||||
function Layout({ onLogout }) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
const {
|
||||
token: { colorBgContainer, borderRadiusLG },
|
||||
} = theme.useToken()
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
@ -19,131 +33,114 @@ function Layout({ onLogout }) {
|
||||
}
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
key: '/',
|
||||
icon: <HomeOutlined />,
|
||||
label: <NavLink to="/">首页</NavLink>,
|
||||
},
|
||||
{
|
||||
key: '/menu1',
|
||||
icon: <AppstoreOutlined />,
|
||||
label: <NavLink to="/menu1">菜单1</NavLink>,
|
||||
},
|
||||
{
|
||||
key: '/menu2',
|
||||
icon: <SettingOutlined />,
|
||||
label: <NavLink to="/menu2">菜单2</NavLink>,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* 侧边栏 */}
|
||||
<div style={{ ...styles.sidebar, width: collapsed ? '80px' : '200px' }}>
|
||||
<AntLayout style={{ minHeight: '100vh' }}>
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
theme="dark"
|
||||
width={200}
|
||||
collapsedWidth={80}
|
||||
>
|
||||
<div style={styles.logo}>
|
||||
{collapsed ? 'A' : 'Atlas'}
|
||||
{collapsed ? 'A' : 'Atlas Console'}
|
||||
</div>
|
||||
<nav style={styles.nav}>
|
||||
<NavLink to="/" style={({ isActive }) => isActive ? { ...styles.navItem, ...styles.active } : styles.navItem}>
|
||||
<span style={styles.icon}>🏠</span>
|
||||
{!collapsed && <span>首页</span>}
|
||||
</NavLink>
|
||||
<NavLink to="/menu1" style={({ isActive }) => isActive ? { ...styles.navItem, ...styles.active } : styles.navItem}>
|
||||
<span style={styles.icon}>📦</span>
|
||||
{!collapsed && <span>菜单1</span>}
|
||||
</NavLink>
|
||||
<NavLink to="/menu2" style={({ isActive }) => isActive ? { ...styles.navItem, ...styles.active } : styles.navItem}>
|
||||
<span style={styles.icon}>⚙️</span>
|
||||
{!collapsed && <span>菜单2</span>}
|
||||
</NavLink>
|
||||
</nav>
|
||||
<button onClick={() => setCollapsed(!collapsed)} style={styles.collapseBtn}>
|
||||
{collapsed ? '→' : '←'}
|
||||
</button>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
defaultSelectedKeys={['/']}
|
||||
items={menuItems}
|
||||
style={styles.menu}
|
||||
/>
|
||||
</Sider>
|
||||
<AntLayout>
|
||||
<Header style={styles.header(colorBgContainer)}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
style={styles.trigger}
|
||||
/>
|
||||
<div style={styles.headerRight}>
|
||||
<span style={styles.userInfo}>管理员</span>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<LogoutOutlined />}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<div style={styles.main}>
|
||||
{/* 顶部栏 */}
|
||||
<div style={styles.header}>
|
||||
<span style={styles.headerTitle}>Atlas Console</span>
|
||||
<button onClick={handleLogout} style={styles.logoutBtn}>退出登录</button>
|
||||
</div>
|
||||
|
||||
{/* 内容区 */}
|
||||
<div style={styles.content}>
|
||||
</Header>
|
||||
<Content style={styles.content(colorBgContainer, borderRadiusLG)}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Content>
|
||||
</AntLayout>
|
||||
</AntLayout>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
},
|
||||
sidebar: {
|
||||
backgroundColor: '#001529',
|
||||
color: '#fff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'width 0.3s',
|
||||
},
|
||||
logo: {
|
||||
height: '64px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
borderBottom: '1px solid #ffffff20',
|
||||
},
|
||||
nav: {
|
||||
flex: 1,
|
||||
padding: '16px 0',
|
||||
},
|
||||
navItem: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '12px 24px',
|
||||
color: '#ffffff80',
|
||||
textDecoration: 'none',
|
||||
transition: 'all 0.2s',
|
||||
},
|
||||
active: {
|
||||
backgroundColor: '#1890ff',
|
||||
color: '#fff',
|
||||
},
|
||||
icon: {
|
||||
fontSize: '18px',
|
||||
},
|
||||
collapseBtn: {
|
||||
padding: '12px',
|
||||
backgroundColor: '#ffffff10',
|
||||
border: 'none',
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
main: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
menu: {
|
||||
borderRight: 'none',
|
||||
},
|
||||
header: {
|
||||
height: '64px',
|
||||
backgroundColor: '#fff',
|
||||
header: (colorBgContainer) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 24px',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.1)',
|
||||
},
|
||||
headerTitle: {
|
||||
padding: '0 16px',
|
||||
background: colorBgContainer,
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
}),
|
||||
trigger: {
|
||||
fontSize: '18px',
|
||||
fontWeight: 'bold',
|
||||
color: '#333',
|
||||
padding: '0 12px',
|
||||
},
|
||||
logoutBtn: {
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#ff4d4f',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
headerRight: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
userInfo: {
|
||||
color: '#666',
|
||||
},
|
||||
content: (colorBgContainer, borderRadiusLG) => ({
|
||||
margin: '24px',
|
||||
padding: '24px',
|
||||
overflow: 'auto',
|
||||
backgroundColor: '#f5f5f5',
|
||||
},
|
||||
minHeight: '280px',
|
||||
background: colorBgContainer,
|
||||
borderRadius: borderRadiusLG,
|
||||
}),
|
||||
}
|
||||
|
||||
export default Layout
|
||||
@ -1,36 +1,30 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { authApi } from '../api/modules/auth'
|
||||
import { Form, Input, Button, message } from 'antd'
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
||||
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 = async (e) => {
|
||||
e.preventDefault()
|
||||
console.log('Login clicked', { username, password })
|
||||
setError('')
|
||||
const handleSubmit = async (values) => {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
// 调用登录 API(使用 Mock 或真实接口)
|
||||
const response = await authApi.login(username, password)
|
||||
console.log('Login response:', response)
|
||||
const response = await authApi.login(values.username, values.password)
|
||||
|
||||
if (response.code === 0) {
|
||||
api.setToken(response.data.token)
|
||||
message.success('登录成功')
|
||||
onLogin()
|
||||
navigate('/')
|
||||
} else {
|
||||
setError(response.message || '登录失败')
|
||||
message.error(response.message || '登录失败')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Login error:', err)
|
||||
setError(err.message || '用户名或密码错误')
|
||||
message.error(err.message || '用户名或密码错误')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@ -39,29 +33,47 @@ function Login({ onLogin }) {
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.card}>
|
||||
<h2 style={styles.title}>登录</h2>
|
||||
<form onSubmit={handleSubmit} style={styles.form}>
|
||||
<input
|
||||
type="text"
|
||||
<h2 style={styles.title}>Atlas Console</h2>
|
||||
<p style={styles.subtitle}>登录您的账户</p>
|
||||
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={handleSubmit}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined />}
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={styles.input}
|
||||
disabled={loading}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined />}
|
||||
placeholder="密码"
|
||||
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} disabled={loading}>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</button>
|
||||
</form>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<p style={styles.hint}>测试账号: admin / admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -73,45 +85,26 @@ const styles = {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
backgroundColor: '#f5f5f5',
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
padding: '40px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||
width: '320px',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
|
||||
width: '360px',
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
marginBottom: '24px',
|
||||
marginBottom: '8px',
|
||||
color: '#333',
|
||||
},
|
||||
form: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '16px',
|
||||
},
|
||||
input: {
|
||||
padding: '12px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
},
|
||||
button: {
|
||||
padding: '12px',
|
||||
backgroundColor: '#1890ff',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
error: {
|
||||
color: '#ff4d4f',
|
||||
fontSize: '14px',
|
||||
subtitle: {
|
||||
textAlign: 'center',
|
||||
marginBottom: '24px',
|
||||
color: '#666',
|
||||
fontSize: '14px',
|
||||
},
|
||||
hint: {
|
||||
marginTop: '16px',
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { projectApi } from '../api/modules/project'
|
||||
import { Table, Tag, Progress, Card } from 'antd'
|
||||
import projectApi from '../api/modules/project'
|
||||
|
||||
function Menu1() {
|
||||
const [projects, setProjects] = useState([])
|
||||
@ -24,98 +25,61 @@ function Menu1() {
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
switch (status) {
|
||||
case '已完成': return '#52c41a'
|
||||
case '进行中': return '#1890ff'
|
||||
default: return '#d9d9d9'
|
||||
case '已完成': return 'success'
|
||||
case '进行中': return 'processing'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status) => (
|
||||
<Tag color={getStatusColor(status)}>{status}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
render: (progress) => (
|
||||
<Progress percent={progress} size="small" />
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>菜单1 - 项目管理</h1>
|
||||
{loading ? (
|
||||
<p>加载中...</p>
|
||||
) : (
|
||||
<div style={styles.table}>
|
||||
<div style={styles.header}>
|
||||
<div style={{...styles.cell, flex: 1}}>ID</div>
|
||||
<div style={{...styles.cell, flex: 2}}>项目名称</div>
|
||||
<div style={{...styles.cell, flex: 1}}>状态</div>
|
||||
<div style={{...styles.cell, flex: 2}}>进度</div>
|
||||
</div>
|
||||
{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: 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>
|
||||
)}
|
||||
<h2 style={styles.title}>项目管理</h2>
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={projects}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
table: {
|
||||
marginTop: '24px',
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
backgroundColor: '#fafafa',
|
||||
padding: '16px',
|
||||
fontWeight: 'bold',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
},
|
||||
row: {
|
||||
display: 'flex',
|
||||
padding: '16px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
},
|
||||
cell: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
badge: {
|
||||
padding: '4px 12px',
|
||||
borderRadius: '4px',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
},
|
||||
progressBg: {
|
||||
flex: 1,
|
||||
height: '8px',
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: '4px',
|
||||
marginRight: '8px',
|
||||
},
|
||||
progressBar: {
|
||||
height: '100%',
|
||||
backgroundColor: '#1890ff',
|
||||
borderRadius: '4px',
|
||||
transition: 'width 0.3s',
|
||||
},
|
||||
progressText: {
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
width: '40px',
|
||||
title: {
|
||||
marginBottom: '24px',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,196 +1,138 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { settingsApi } from '../api/modules/settings'
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { Card, Form, Input, Switch, Button, message } from 'antd'
|
||||
import settingsApi from '../api/modules/settings'
|
||||
|
||||
function Menu2() {
|
||||
const [settings, setSettings] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [form] = Form.useForm()
|
||||
const formRef = useRef(form)
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings()
|
||||
}, [])
|
||||
|
||||
const fetchSettings = async () => {
|
||||
const fetchSettings = useCallback(async () => {
|
||||
try {
|
||||
const response = await settingsApi.getList()
|
||||
if (response.code === 0) {
|
||||
setSettings(response.data)
|
||||
// 转换为表单格式
|
||||
const formData = {}
|
||||
response.data.forEach(item => {
|
||||
formData[item.key] = item.value
|
||||
})
|
||||
formRef.current.setFieldsValue(formData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings()
|
||||
}, [fetchSettings])
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setMessage('')
|
||||
try {
|
||||
const response = await settingsApi.save(settings)
|
||||
const values = form.getFieldsValue()
|
||||
// 转换回设置格式
|
||||
const updatedSettings = settings.map(item => ({
|
||||
...item,
|
||||
value: values[item.key]
|
||||
}))
|
||||
|
||||
setSaving(true)
|
||||
const response = await settingsApi.save(updatedSettings)
|
||||
if (response.code === 0) {
|
||||
setMessage('保存成功')
|
||||
setTimeout(() => setMessage(''), 3000)
|
||||
message.success('保存成功')
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage('保存失败: ' + error.message)
|
||||
message.error('保存失败: ' + error.message)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const updateSetting = (key, value) => {
|
||||
setSettings(settings.map(s =>
|
||||
s.key === key ? { ...s, value } : s
|
||||
))
|
||||
const renderField = (item) => {
|
||||
if (item.type === 'switch') {
|
||||
return (
|
||||
<Form.Item
|
||||
key={item.key}
|
||||
name={item.key}
|
||||
valuePropName="checked"
|
||||
style={styles.formItem}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={item.key}
|
||||
name={item.key}
|
||||
style={styles.formItem}
|
||||
>
|
||||
<Input style={{ maxWidth: 300 }} />
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>菜单2 - 系统设置</h1>
|
||||
{loading ? (
|
||||
<p>加载中...</p>
|
||||
) : (
|
||||
<div style={styles.card}>
|
||||
<h2 style={styles.title}>系统设置</h2>
|
||||
<Card loading={loading}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
style={styles.form}
|
||||
>
|
||||
{settings.map(item => (
|
||||
<div key={item.key} style={styles.row}>
|
||||
<div style={styles.label}>{item.label}</div>
|
||||
<div style={styles.value}>
|
||||
{item.type === 'switch' ? (
|
||||
<label style={styles.switch}>
|
||||
<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"
|
||||
value={item.value}
|
||||
onChange={(e) => updateSetting(item.key, e.target.value)}
|
||||
style={styles.input}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span style={styles.label}>{item.label}</span>
|
||||
{renderField(item)}
|
||||
</div>
|
||||
))}
|
||||
</Form>
|
||||
<div style={styles.actions}>
|
||||
<button
|
||||
style={styles.saveBtn}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
loading={saving}
|
||||
>
|
||||
{saving ? '保存中...' : '保存设置'}
|
||||
</button>
|
||||
{message && (
|
||||
<span style={message.includes('成功') ? styles.successMsg : styles.errorMsg}>
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
保存设置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
card: {
|
||||
marginTop: '24px',
|
||||
backgroundColor: '#fff',
|
||||
padding: '24px',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
title: {
|
||||
marginBottom: '24px',
|
||||
},
|
||||
form: {
|
||||
maxWidth: 600,
|
||||
},
|
||||
row: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '16px 0',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
marginBottom: '8px',
|
||||
},
|
||||
label: {
|
||||
width: '150px',
|
||||
fontWeight: 'bold',
|
||||
fontWeight: '500',
|
||||
color: '#333',
|
||||
},
|
||||
value: {
|
||||
formItem: {
|
||||
marginBottom: '0',
|
||||
flex: 1,
|
||||
},
|
||||
input: {
|
||||
width: '100%',
|
||||
maxWidth: '400px',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
fontSize: '14px',
|
||||
},
|
||||
switch: {
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
width: '44px',
|
||||
height: '22px',
|
||||
},
|
||||
switchInput: {
|
||||
opacity: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
slider: {
|
||||
position: 'absolute',
|
||||
cursor: 'pointer',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: '#ccc',
|
||||
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',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
},
|
||||
saveBtn: {
|
||||
padding: '10px 24px',
|
||||
backgroundColor: '#1890ff',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
},
|
||||
successMsg: {
|
||||
color: '#52c41a',
|
||||
fontSize: '14px',
|
||||
},
|
||||
errorMsg: {
|
||||
color: '#ff4d4f',
|
||||
fontSize: '14px',
|
||||
paddingTop: '24px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user