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