feat: 集成基本功能

This commit is contained in:
micah 2026-03-14 15:10:11 +08:00
parent a746c26f94
commit ba94d299e3
10 changed files with 1352 additions and 434 deletions

967
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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)
}) })
} }

View File

@ -3,7 +3,7 @@
*/ */
import api from '../index' import api from '../index'
export const authApi = { const authApi = {
/** /**
* 登录 * 登录
* @param {string} username 用户名 * @param {string} username 用户名

View File

@ -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>,
) )

View File

@ -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>
) : ( ) : (
<div style={styles.cards}> <Row gutter={[16, 16]}>
<div style={styles.card}> <Col xs={24} sm={8}>
<h3>📊 数据统计</h3> <Card>
<p style={styles.number}>{stats.totalUsers.toLocaleString()}</p> <Statistic
<p>总用户数</p> title="总用户数"
</div> value={stats.totalUsers}
<div style={styles.card}> prefix={<UserOutlined />}
<h3>📦 订单</h3> styles={{ value: { color: '#3f8600' } }}
<p style={styles.number}>{stats.todayOrders.toLocaleString()}</p> />
<p>今日订单</p> </Card>
</div> </Col>
<div style={styles.card}> <Col xs={24} sm={8}>
<h3>💰 收入</h3> <Card>
<p style={styles.number}>¥{stats.todayRevenue.toLocaleString()}</p> <Statistic
<p>今日收入</p> title="今日订单"
</div> value={stats.todayOrders}
</div> 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',
}, },
} }

View File

@ -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}>
<span style={styles.userInfo}>管理员</span>
<Button
type="text"
danger
icon={<LogoutOutlined />}
onClick={handleLogout}
>
退出登录
</Button>
</div> </div>
</Header>
{/* 主内容区 */} <Content style={styles.content(colorBgContainer, borderRadiusLG)}>
<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}>
<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

View File

@ -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
name="login"
onFinish={handleSubmit}
autoComplete="off"
size="large"
>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入用户名' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="用户名" placeholder="用户名"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={styles.input}
disabled={loading}
/> />
<input </Form.Item>
type="password"
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="密码" placeholder="密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={styles.input}
disabled={loading}
/> />
{error && <p style={styles.error}>{error}</p>} </Form.Item>
<button type="submit" style={styles.button} disabled={loading}>
{loading ? '登录中...' : '登录'} <Form.Item>
</button> <Button
</form> 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',

View File

@ -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',
}, },
} }

View File

@ -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>
))} ))}
</Form>
<div style={styles.actions}> <div style={styles.actions}>
<button <Button
style={styles.saveBtn} type="primary"
onClick={handleSave} onClick={handleSave}
disabled={saving} loading={saving}
> >
{saving ? '保存中...' : '保存设置'} 保存设置
</button> </Button>
{message && (
<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',
}, },
} }