版本更新: v1.2.15
This commit is contained in:
parent
bc23e95691
commit
a71697aed0
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import Users from './pages/Users'
|
||||
|
||||
const API_BASE = ''
|
||||
|
||||
|
|
@ -11,95 +12,33 @@ function Login({ onLogin }) {
|
|||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!username || !password) {
|
||||
setError('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError('')
|
||||
if (!username || !password) { setError('请输入用户名和密码'); return }
|
||||
setLoading(true); setError('')
|
||||
try {
|
||||
const res = await axios.post(`${API_BASE}/api/auth/login`,
|
||||
new URLSearchParams({ username, password }),
|
||||
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||||
)
|
||||
if (res.data.code === 0) {
|
||||
localStorage.setItem('adminToken', res.data.data.token)
|
||||
localStorage.setItem('adminUser', JSON.stringify(res.data.data.user))
|
||||
onLogin(res.data.data.user)
|
||||
} else {
|
||||
setError(res.data.message || '登录失败')
|
||||
}
|
||||
} catch (e) {
|
||||
setError('网络错误,请检查后端服务')
|
||||
}
|
||||
if (res.data.access_token) {
|
||||
localStorage.setItem('adminToken', res.data.access_token)
|
||||
localStorage.setItem('adminUser', JSON.stringify(res.data))
|
||||
onLogin(res.data)
|
||||
} else { setError(res.data.detail || '登录失败') }
|
||||
} catch (e) { setError('网络错误') }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)'
|
||||
}}>
|
||||
<div style={{
|
||||
width: '360px',
|
||||
padding: '40px 30px',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '16px',
|
||||
border: '1px solid rgba(255,255,255,0.1)'
|
||||
}}>
|
||||
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)' }}>
|
||||
<div style={{ width: '360px', padding: '40px 30px', background: 'rgba(255,255,255,0.05)', borderRadius: '16px', border: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
<h1 style={{ textAlign: 'center', marginBottom: '30px', fontSize: '24px' }}>甲辰管理后台</h1>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入管理员账号"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 16px',
|
||||
marginBottom: '16px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
color: '#fff',
|
||||
fontSize: '15px'
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onKeyPress={e => e.key === 'Enter' && handleLogin()}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 16px',
|
||||
marginBottom: '20px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
background: 'rgba(0,0,0,0.3)',
|
||||
color: '#fff',
|
||||
fontSize: '15px'
|
||||
}}
|
||||
/>
|
||||
<input type="text" placeholder="管理员账号" value={username} onChange={e => setUsername(e.target.value)}
|
||||
style={{ width: '100%', padding: '14px 16px', marginBottom: '16px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '15px' }} />
|
||||
<input type="password" placeholder="密码" value={password} onChange={e => setPassword(e.target.value)} onKeyPress={e => e.key === 'Enter' && handleLogin()}
|
||||
style={{ width: '100%', padding: '14px 16px', marginBottom: '20px', borderRadius: '8px', border: '1px solid rgba(255,255,255,0.2)', background: 'rgba(0,0,0,0.3)', color: '#fff', fontSize: '15px' }} />
|
||||
{error && <div style={{ color: '#f87171', marginBottom: '16px', textAlign: 'center' }}>{error}</div>}
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
disabled={loading}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
opacity: loading ? 0.7 : 1
|
||||
}}
|
||||
>
|
||||
<button onClick={handleLogin} disabled={loading}
|
||||
style={{ width: '100%', padding: '14px', borderRadius: '8px', border: 'none', background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', color: '#fff', fontSize: '16px', cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? 0.7 : 1 }}>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -109,6 +48,7 @@ function Login({ onLogin }) {
|
|||
|
||||
export default function App() {
|
||||
const [user, setUser] = useState(null)
|
||||
const [activeTab, setActiveTab] = useState('users')
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('adminToken')
|
||||
|
|
@ -126,11 +66,40 @@ export default function App() {
|
|||
|
||||
if (!user) return <Login onLogin={setUser} />
|
||||
|
||||
const tabs = [
|
||||
{ key: 'users', label: '👥 用户管理', icon: '👥' },
|
||||
{ key: 'stats', label: '📊 统计分析', icon: '📊' },
|
||||
{ key: 'info', label: '📝 资讯管理', icon: '📝' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#1a1a2e', color: '#fff', padding: '20px' }}>
|
||||
<h1>甲辰管理后台 - 登录成功</h1>
|
||||
<p>欢迎 {user.username || user.f01_01_name}</p>
|
||||
<button onClick={handleLogout} style={{ marginTop: '20px', padding: '10px 20px' }}>退出</button>
|
||||
<div style={{ minHeight: '100vh', background: '#0f172a', color: '#fff', paddingBottom: '80px' }}>
|
||||
{/* 顶部栏 */}
|
||||
<div style={{ background: '#1e293b', padding: '16px 20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderBottom: '1px solid #334155' }}>
|
||||
<h1 style={{ margin: 0, fontSize: '18px', fontWeight: '600' }}>甲辰管理后台</h1>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: '14px' }}>{user.username || user.name || '管理员'}</span>
|
||||
<button onClick={handleLogout} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #334155', background: 'transparent', color: '#94a3b8', cursor: 'pointer', fontSize: '13px' }}>退出</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 主内容 */}
|
||||
<div style={{ padding: '0' }}>
|
||||
{activeTab === 'users' && <Users />}
|
||||
{activeTab === 'stats' && <div style={{ padding: '40px', textAlign: 'center', color: '#94a3b8' }}>统计分析功能开发中...</div>}
|
||||
{activeTab === 'info' && <div style={{ padding: '40px', textAlign: 'center', color: '#94a3b8' }}>资讯管理功能开发中...</div>}
|
||||
</div>
|
||||
|
||||
{/* 底部导航 */}
|
||||
<div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, background: '#1e293b', borderTop: '1px solid #334155', padding: '8px 16px', display: 'flex', justifyContent: 'space-around', zIndex: 100 }}>
|
||||
{tabs.map(tab => (
|
||||
<div key={tab.key} onClick={() => setActiveTab(tab.key)}
|
||||
style={{ padding: '12px 20px', borderRadius: '10px', background: activeTab === tab.key ? '#3b82f6' : 'transparent',
|
||||
color: activeTab === tab.key ? '#fff' : '#94a3b8', cursor: 'pointer', fontSize: '14px', fontWeight: '500' }}>
|
||||
{tab.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
export default function Users() {
|
||||
const [users, setUsers] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [editingUser, setEditingUser] = useState(null)
|
||||
const [newUser, setNewUser] = useState({ username: '', password: '', email: '', role: 'user' })
|
||||
|
||||
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||
|
||||
useEffect(() => { fetchUsers() }, [])
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken')
|
||||
const res = await fetch(`${API_BASE}/api/admin/users?page=1&limit=100`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
if (!res.ok) throw new Error('获取用户列表失败')
|
||||
const data = await res.json()
|
||||
setUsers(data)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteUser = async (userId, username) => {
|
||||
if (!confirm(`确定要删除用户 "${username}" 吗?`)) return
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken')
|
||||
const res = await fetch(`${API_BASE}/api/admin/users/${userId}`, {
|
||||
method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
if (!res.ok) throw new Error('删除失败')
|
||||
fetchUsers()
|
||||
} catch (err) { alert(err.message) }
|
||||
}
|
||||
|
||||
const handleAddUser = async () => {
|
||||
if (!newUser.username || !newUser.password) { alert('用户名和密码为必填项'); return }
|
||||
const payload = { ...newUser }
|
||||
if (!payload.email) delete payload.email
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken')
|
||||
const res = await fetch(`${API_BASE}/api/auth/register`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
if (!res.ok) throw new Error('添加失败')
|
||||
alert('添加成功!')
|
||||
setShowAddModal(false)
|
||||
setNewUser({ username: '', password: '', email: '', role: 'user' })
|
||||
fetchUsers()
|
||||
} catch (err) { alert(err.message) }
|
||||
}
|
||||
|
||||
const handleEditUser = async () => {
|
||||
if (!editingUser.username) { alert('用户名不能为空'); return }
|
||||
if (editingUser.newPassword || editingUser.confirmPassword) {
|
||||
if (!editingUser.newPassword || !editingUser.confirmPassword) { alert('请填写完整密码信息'); return }
|
||||
if (editingUser.newPassword !== editingUser.confirmPassword) { alert('两次输入的密码不一致'); return }
|
||||
if (editingUser.newPassword.length < 6) { alert('密码至少 6 个字符'); return }
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('adminToken')
|
||||
const updateData = { username: editingUser.username, email: editingUser.email, role: editingUser.role }
|
||||
if (editingUser.newPassword && editingUser.newPassword.trim()) updateData.password = editingUser.newPassword
|
||||
const res = await fetch(`${API_BASE}/api/admin/users/${editingUser.id}`, {
|
||||
method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
||||
body: JSON.stringify(updateData)
|
||||
})
|
||||
if (!res.ok) throw new Error('更新失败')
|
||||
alert('更新成功!')
|
||||
setEditingUser(null)
|
||||
fetchUsers()
|
||||
} catch (err) { alert(err.message) }
|
||||
}
|
||||
|
||||
if (loading) return <div style={{ padding: '20px', color: '#fff' }}>加载中...</div>
|
||||
if (error) return <div style={{ padding: '20px', color: '#ef4444' }}>⚠️ {error}</div>
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', overflowY: 'auto', background: '#0f172a', padding: '20px', paddingBottom: '80px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h1 style={{ color: '#fbbf24', fontSize: '24px', fontWeight: '700' }}>⚙️ 用户管理</h1>
|
||||
<button onClick={() => setShowAddModal(true)} style={{ padding: '10px 20px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>➕ 添加用户</button>
|
||||
</div>
|
||||
|
||||
{/* 用户列表 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{users.map(user => (
|
||||
<div key={user.id} style={{ background: 'rgba(30, 41, 59, 0.8)', borderRadius: '12px', padding: '16px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '16px', fontWeight: 'bold' }}>{user.username}</div>
|
||||
<div style={{ padding: '2px 8px', borderRadius: '4px', background: user.role === 'admin' ? 'rgba(16, 185, 129, 0.2)' : 'rgba(148, 163, 184, 0.2)', color: user.role === 'admin' ? '#10b981' : '#94a3b8', fontSize: '12px' }}>
|
||||
{user.role === 'admin' ? '👑 管理员' : user.role === 'editor' ? '📝 信息员' : '👤 用户'}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ color: '#64748b', fontSize: '13px', marginTop: '4px' }}>📧 {user.email || '未设置'} | 📱 {user.phone || '未设置'}</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '12px' }}>藏品数</div>
|
||||
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: 'bold' }}>{user.collectionCount || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
<div style={{ color: '#64748b', fontSize: '12px' }}>注册时间:{user.created_at ? new Date(user.created_at).toLocaleDateString('zh-CN') : '-'}</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button onClick={() => setEditingUser({ ...user })} style={{ padding: '6px 12px', borderRadius: '6px', border: 'none', background: 'rgba(59, 130, 246, 0.2)', color: '#3b82f6', cursor: 'pointer', fontSize: '12px' }}>✏️ 编辑</button>
|
||||
{user.role !== 'admin' && (
|
||||
<button onClick={() => handleDeleteUser(user.id, user.username)} style={{ padding: '6px 12px', borderRadius: '6px', border: 'none', background: 'rgba(239, 68, 68, 0.2)', color: '#ef4444', cursor: 'pointer', fontSize: '12px' }}>🗑️ 删除</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{users.length === 0 && <div style={{ padding: '40px', textAlign: 'center', color: '#94a3b8' }}>暂无用户数据</div>}
|
||||
|
||||
{/* 添加用户模态框 */}
|
||||
{showAddModal && (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px', width: '90%', maxWidth: '400px' }}>
|
||||
<h2 style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold', marginBottom: '20px' }}>添加用户</h2>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>用户名 *</label>
|
||||
<input type="text" value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>密码 *</label>
|
||||
<input type="password" value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>邮箱</label>
|
||||
<input type="email" value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>角色</label>
|
||||
<select value={newUser.role} onChange={(e) => setNewUser({ ...newUser, role: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}>
|
||||
<option value="user">普通用户</option>
|
||||
<option value="editor">信息员</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button onClick={() => setShowAddModal(false)} style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>取消</button>
|
||||
<button onClick={handleAddUser} style={{ flex: 1, padding: '12px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', fontWeight: '600', cursor: 'pointer' }}>确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑用户模态框 */}
|
||||
{editingUser && (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px', width: '90%', maxWidth: '500px', maxHeight: '90vh', overflowY: 'auto' }}>
|
||||
<h2 style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold', marginBottom: '20px' }}>编辑用户</h2>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h3 style={{ color: '#fbbf24', fontSize: '15px', fontWeight: 'bold', marginBottom: '16px', paddingBottom: '8px', borderBottom: '1px solid rgba(251, 191, 36, 0.3)' }}>📋 基本信息</h3>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>用户名</label>
|
||||
<input type="text" value={editingUser.username} onChange={(e) => setEditingUser({ ...editingUser, username: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>邮箱</label>
|
||||
<input type="email" value={editingUser.email || ''} onChange={(e) => setEditingUser({ ...editingUser, email: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>角色</label>
|
||||
<select value={editingUser.role} onChange={(e) => setEditingUser({ ...editingUser, role: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}>
|
||||
<option value="user">普通用户</option>
|
||||
<option value="editor">信息员</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h3 style={{ color: '#fbbf24', fontSize: '15px', fontWeight: 'bold', marginBottom: '16px', paddingBottom: '8px', borderBottom: '1px solid rgba(251, 191, 36, 0.3)' }}>🔐 修改密码(可选)</h3>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>新密码 <span style={{ color: '#64748b', fontSize: '12px' }}>(留空则不修改)</span></label>
|
||||
<input type="password" value={editingUser.newPassword || ''} onChange={(e) => setEditingUser({ ...editingUser, newPassword: e.target.value })} placeholder="请输入新密码(至少 6 位)" style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>确认新密码</label>
|
||||
<input type="password" value={editingUser.confirmPassword || ''} onChange={(e) => setEditingUser({ ...editingUser, confirmPassword: e.target.value })} placeholder="请再次输入新密码" style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button onClick={() => setEditingUser(null)} style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>取消</button>
|
||||
<button onClick={handleEditUser} style={{ flex: 1, padding: '12px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', fontWeight: '600', cursor: 'pointer' }}>确定</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
VERSION=1.2.14
|
||||
VERSION=1.2.15
|
||||
|
|
|
|||
|
|
@ -10,12 +10,9 @@ export default function Admin() {
|
|||
const [newUser, setNewUser] = useState({ username: '', password: '', email: '', role: 'user' })
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
// 检查权限
|
||||
const userStr = localStorage.getItem('user')
|
||||
let user = null
|
||||
try {
|
||||
user = userStr ? JSON.parse(userStr) : null
|
||||
} catch (e) {}
|
||||
try { user = userStr ? JSON.parse(userStr) : null } catch (e) {}
|
||||
|
||||
if (!user || user.role !== 'admin') {
|
||||
return (
|
||||
|
|
@ -29,131 +26,63 @@ export default function Admin() {
|
|||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers()
|
||||
}, [])
|
||||
useEffect(() => { fetchUsers() }, [])
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/users?page=1&limit=100', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
const res = await fetch('/api/admin/users?page=1&limit=100', { headers: { 'Authorization': `Bearer ${token}` } })
|
||||
if (!res.ok) throw new Error('获取用户列表失败')
|
||||
const data = await res.json()
|
||||
setUsers(data)
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
} catch (err) { setError(err.message) } finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const handleDeleteUser = async (userId, username) => {
|
||||
if (!confirm(`确定要删除用户 "${username}" 吗?`)) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/admin/users/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
const res = await fetch(`/api/admin/users/${userId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } })
|
||||
if (!res.ok) throw new Error('删除失败')
|
||||
fetchUsers()
|
||||
} catch (err) {
|
||||
alert(err.message)
|
||||
}
|
||||
} catch (err) { alert(err.message) }
|
||||
}
|
||||
|
||||
const handleAddUser = async () => {
|
||||
if (!newUser.username || !newUser.password) {
|
||||
alert('用户名和密码为必填项')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果 email 为空,不传这个字段
|
||||
if (!newUser.username || !newUser.password) { alert('用户名和密码为必填项'); return }
|
||||
const payload = { ...newUser }
|
||||
if (!payload.email) {
|
||||
delete payload.email
|
||||
}
|
||||
|
||||
if (!payload.email) delete payload.email
|
||||
try {
|
||||
const res = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.detail || '添加失败')
|
||||
}
|
||||
|
||||
const res = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(payload) })
|
||||
if (!res.ok) { const data = await res.json(); throw new Error(data.detail || '添加失败') }
|
||||
alert('添加成功!')
|
||||
setShowAddModal(false)
|
||||
setNewUser({ username: '', password: '', email: '', role: 'user' })
|
||||
fetchUsers()
|
||||
} catch (err) {
|
||||
alert(err.message)
|
||||
}
|
||||
} catch (err) { alert(err.message) }
|
||||
}
|
||||
|
||||
const handleEditUser = async () => {
|
||||
if (!editingUser.username) {
|
||||
alert('用户名不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if (!editingUser.username) { alert('用户名不能为空'); return }
|
||||
if (editingUser.newPassword || editingUser.confirmPassword) {
|
||||
if (!editingUser.newPassword || !editingUser.confirmPassword) {
|
||||
alert('请填写完整密码信息')
|
||||
return
|
||||
}
|
||||
if (editingUser.newPassword !== editingUser.confirmPassword) {
|
||||
alert('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
if (editingUser.newPassword.length < 6) {
|
||||
alert('密码至少 6 个字符')
|
||||
return
|
||||
}
|
||||
if (!editingUser.newPassword || !editingUser.confirmPassword) { alert('请填写完整密码信息'); return }
|
||||
if (editingUser.newPassword !== editingUser.confirmPassword) { alert('两次输入的密码不一致'); return }
|
||||
if (editingUser.newPassword.length < 6) { alert('密码至少 6 个字符'); return }
|
||||
}
|
||||
|
||||
try {
|
||||
// 准备更新数据
|
||||
const updateData = {
|
||||
username: editingUser.username,
|
||||
email: editingUser.email,
|
||||
role: editingUser.role
|
||||
phone: editingUser.phone,
|
||||
role: editingUser.role,
|
||||
level: editingUser.level,
|
||||
points: editingUser.points
|
||||
}
|
||||
|
||||
// 如果有新密码,添加到更新数据
|
||||
if (editingUser.newPassword && editingUser.newPassword.trim()) {
|
||||
updateData.password = editingUser.newPassword
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/admin/users/${editingUser.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(updateData)
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.detail || '更新失败')
|
||||
}
|
||||
|
||||
if (editingUser.newPassword && editingUser.newPassword.trim()) updateData.password = editingUser.newPassword
|
||||
const res = await fetch(`/api/admin/users/${editingUser.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(updateData) })
|
||||
if (!res.ok) { const data = await res.json(); throw new Error(data.detail || '更新失败') }
|
||||
alert('更新成功!')
|
||||
setEditingUser(null)
|
||||
fetchUsers()
|
||||
} catch (err) {
|
||||
alert(err.message)
|
||||
}
|
||||
} catch (err) { alert(err.message) }
|
||||
}
|
||||
|
||||
if (loading) return <div style={{ padding: '20px', color: '#fff' }}>加载中...</div>
|
||||
|
|
@ -162,257 +91,116 @@ export default function Admin() {
|
|||
return (
|
||||
<div style={{ minHeight: '100vh', overflowY: 'auto', background: '#0f172a', padding: '20px', paddingBottom: '80px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h1 style={{ color: '#fbbf24', fontSize: '24px', fontWeight: '700' }}>
|
||||
⚙️ 用户管理
|
||||
</h1>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
background: '#fbbf24',
|
||||
color: '#1e293b',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
➕ 添加用户
|
||||
</button>
|
||||
<h1 style={{ color: '#fbbf24', fontSize: '24px', fontWeight: '700' }}>⚙️ 用户管理</h1>
|
||||
<button onClick={() => setShowAddModal(true)} style={{ padding: '10px 20px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>➕ 添加用户</button>
|
||||
</div>
|
||||
|
||||
{/* 用户列表 - 卡片式布局 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{users.map((user) => (
|
||||
<div key={user.id} style={{ background: 'rgba(30, 41, 59, 0.8)', borderRadius: '12px', padding: '16px' }}>
|
||||
{users.map(u => (
|
||||
<div key={u.id} style={{ background: 'rgba(30, 41, 59, 0.8)', borderRadius: '12px', padding: '16px', position: 'relative' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '16px', fontWeight: 'bold' }}>{user.username}</div>
|
||||
<div style={{
|
||||
padding: '2px 8px', borderRadius: '4px',
|
||||
background: user.role === 'admin' ? 'rgba(16, 185, 129, 0.2)' : 'rgba(148, 163, 184, 0.2)',
|
||||
color: user.role === 'admin' ? '#10b981' : '#94a3b8',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{user.role === 'admin' ? '👑 管理员' : '👤 用户'}
|
||||
<div style={{ color: '#fbbf24', fontSize: '16px', fontWeight: 'bold' }}><span style={{ color: '#64748b', fontSize: '12px' }}>#{u.user_code}</span> {u.username}</div>
|
||||
<div style={{ padding: '2px 8px', borderRadius: '4px', background: u.role === 'admin' ? 'rgba(16, 185, 129, 0.2)' : u.role === 'editor' ? 'rgba(59, 130, 246, 0.2)' : 'rgba(148, 163, 184, 0.2)', color: u.role === 'admin' ? '#10b981' : u.role === 'editor' ? '#3b82f6' : '#94a3b8', fontSize: '12px' }}>
|
||||
{u.role === 'admin' ? '👑 管理员' : u.role === 'editor' ? '📝 信息员' : '👤 用户'}
|
||||
</div>
|
||||
{u.level && <span style={{ color: '#f59e0b', fontSize: '14px' }}>{u.level === '青铜' ? '🥉' : u.level === '白银' ? '🥈' : u.level === '黄金' ? '🥇' : u.level === '钻石' ? '💎' : '👑'} {u.level}</span>}
|
||||
</div>
|
||||
<div style={{ color: '#64748b', fontSize: '13px', marginTop: '4px' }}>
|
||||
📧 {user.email || '未设置'} | 📱 {user.phone || '未设置'}
|
||||
<div style={{ color: '#64748b', fontSize: '13px', marginTop: '4px' }}>📧 {u.email || '未设置'} | 📱 {u.phone || '未设置'}</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px', marginTop: '10px' }}>
|
||||
<a href={`#/list?userId=${u.id}&username=${encodeURIComponent(u.username)}`} style={{ background: '#0f172a', padding: '8px', borderRadius: '6px', textAlign: 'center', textDecoration: 'none', cursor: 'pointer', transition: 'all 0.2s' }} title='点击查看用户藏品' onMouseOver={(e) => e.currentTarget.style.background = 'rgba(34,197,94,0.2)'} onMouseOut={(e) => e.currentTarget.style.background = '#0f172a'}><div style={{ color: '#64748b', fontSize: '10px' }}>藏品</div><div style={{ color: '#22c55e', fontSize: '14px', fontWeight: 'bold' }}>{u.collectionCount || 0}</div></a>
|
||||
<div style={{ background: '#0f172a', padding: '8px', borderRadius: '6px', textAlign: 'center' }}><div style={{ color: '#64748b', fontSize: '10px' }}>AI识别</div><div style={{ color: '#3b82f6', fontSize: '14px', fontWeight: 'bold' }}>{u.aiCount || 0}</div></div>
|
||||
<div style={{ background: '#0f172a', padding: '8px', borderRadius: '6px', textAlign: 'center' }}><div style={{ color: '#64748b', fontSize: '10px' }}>寻号</div><div style={{ color: '#f59e0b', fontSize: '14px', fontWeight: 'bold' }}>{u.searchCount || 0}</div></div>
|
||||
<div style={{ background: '#0f172a', padding: '8px', borderRadius: '6px', textAlign: 'center' }}><div style={{ color: '#64748b', fontSize: '10px' }}>积分</div><div style={{ color: '#8b5cf6', fontSize: '14px', fontWeight: 'bold' }}>{u.points || 0}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '12px' }}>藏品数</div>
|
||||
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => window.location.hash = '#/list?userId=' + user.id} title='点击查看'>{user.collection_count || 0}</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '12px' }}>余额</div>
|
||||
<div style={{ color: '#60a5fa', fontSize: '18px', fontWeight: 'bold' }}>¥{u.balance || 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
<div style={{ color: '#64748b', fontSize: '12px' }}>
|
||||
注册时间:{user.created_at ? new Date(user.created_at).toLocaleDateString('zh-CN') : '-'}
|
||||
</div>
|
||||
<div style={{ color: '#64748b', fontSize: '12px' }}>注册时间:{u.created_at ? new Date(u.created_at).toLocaleDateString('zh-CN') : '-'}</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => setEditingUser({ ...user })}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'rgba(59, 130, 246, 0.2)',
|
||||
color: '#3b82f6',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
✏️ 编辑
|
||||
</button>
|
||||
{user.role !== 'admin' && (
|
||||
<button
|
||||
onClick={() => handleDeleteUser(user.id, user.username)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: 'rgba(239, 68, 68, 0.2)',
|
||||
color: '#ef4444',
|
||||
cursor: 'pointer',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
🗑️ 删除
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => setEditingUser({ ...u })} style={{ padding: '6px 12px', borderRadius: '6px', border: 'none', background: 'rgba(59, 130, 246, 0.2)', color: '#3b82f6', cursor: 'pointer', fontSize: '12px' }}>✏️ 编辑</button>
|
||||
{u.role !== 'admin' && <button onClick={() => handleDeleteUser(u.id, u.username)} style={{ padding: '6px 12px', borderRadius: '6px', border: 'none', background: 'rgba(239, 68, 68, 0.2)', color: '#ef4444', cursor: 'pointer', fontSize: '12px' }}>🗑️ 删除</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{users.length === 0 && (
|
||||
<div style={{ padding: '40px', textAlign: 'center', color: '#94a3b8' }}>
|
||||
暂无用户数据
|
||||
</div>
|
||||
)}
|
||||
{users.length === 0 && <div style={{ padding: '40px', textAlign: 'center', color: '#94a3b8' }}>暂无用户数据</div>}
|
||||
|
||||
{/* 添加用户模态框 */}
|
||||
{/* 添加用户弹窗 */}
|
||||
{showAddModal && (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px', width: '90%', maxWidth: '400px' }}>
|
||||
<h2 style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold', marginBottom: '20px' }}>添加用户</h2>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>用户名 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newUser.username}
|
||||
onChange={(e) => setNewUser({ ...newUser, username: e.target.value })}
|
||||
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>密码 *</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newUser.password}
|
||||
onChange={(e) => setNewUser({ ...newUser, password: e.target.value })}
|
||||
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>邮箱</label>
|
||||
<input
|
||||
type="email"
|
||||
value={newUser.email}
|
||||
onChange={(e) => setNewUser({ ...newUser, email: e.target.value })}
|
||||
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>角色</label>
|
||||
<select
|
||||
value={newUser.role}
|
||||
onChange={(e) => setNewUser({ ...newUser, role: e.target.value })}
|
||||
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
|
||||
>
|
||||
<option value="user">普通用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button
|
||||
onClick={() => setShowAddModal(false)}
|
||||
style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddUser}
|
||||
style={{ flex: 1, padding: '12px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', fontWeight: '600', cursor: 'pointer' }}
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}><label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>用户名 *</label><input type="text" value={newUser.username} onChange={(e) => setNewUser({ ...newUser, username: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} /></div>
|
||||
<div style={{ marginBottom: '16px' }}><label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>密码 *</label><input type="password" value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} /></div>
|
||||
<div style={{ marginBottom: '16px' }}><label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>邮箱</label><input type="email" value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }} /></div>
|
||||
<div style={{ marginBottom: '20px' }}><label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>角色</label><select value={newUser.role} onChange={(e) => setNewUser({ ...newUser, role: e.target.value })} style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}><option value="user">普通用户</option><option value="editor">信息员</option><option value="admin">管理员</option></select></div>
|
||||
<div style={{ display: 'flex', gap: '12px' }}><button onClick={() => setShowAddModal(false)} style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>取消</button><button onClick={handleAddUser} style={{ flex: 1, padding: '12px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', fontWeight: '600', cursor: 'pointer' }}>确定</button></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑用户模态框 */}
|
||||
{/* 编辑用户弹窗 - 完整字段 */}
|
||||
{editingUser && (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px', width: '90%', maxWidth: '500px', maxHeight: '90vh', overflowY: 'auto' }}>
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px', width: '90%', maxWidth: '600px', maxHeight: '90vh', overflowY: 'auto' }}>
|
||||
<h2 style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold', marginBottom: '20px' }}>编辑用户</h2>
|
||||
|
||||
{/* 第一部分:基本信息 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h3 style={{ color: '#fbbf24', fontSize: '15px', fontWeight: 'bold', marginBottom: '16px', paddingBottom: '8px', borderBottom: '1px solid rgba(251, 191, 36, 0.3)' }}>📋 基本信息</h3>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>用户名</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingUser.username}
|
||||
onChange={(e) => setEditingUser({ ...editingUser, username: e.target.value })}
|
||||
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>邮箱</label>
|
||||
<input
|
||||
type="email"
|
||||
value={editingUser.email || ''}
|
||||
onChange={(e) => setEditingUser({ ...editingUser, email: e.target.value })}
|
||||
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>角色</label>
|
||||
<select
|
||||
value={editingUser.role}
|
||||
onChange={(e) => setEditingUser({ ...editingUser, role: e.target.value })}
|
||||
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
|
||||
>
|
||||
<option value="user">普通用户</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
{/* 基本信息 */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3 style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px', paddingBottom: '6px', borderBottom: '1px solid rgba(251, 191, 36, 0.3)' }}>📋 基本信息</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||
<div><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>用户码</label><input type="text" value={editingUser.user_code || ''} onChange={(e) => setEditingUser({ ...editingUser, user_code: e.target.value })} style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fbbf24', fontSize: '13px' }} /></div><div><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>用户名</label><input type="text" value={editingUser.username} onChange={(e) => setEditingUser({ ...editingUser, username: e.target.value })} style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} /></div>
|
||||
<div><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>手机号</label><input type="text" value={editingUser.phone || ''} onChange={(e) => setEditingUser({ ...editingUser, phone: e.target.value })} style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} /></div>
|
||||
<div style={{ gridColumn: 'span 2' }}><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>邮箱</label><input type="email" value={editingUser.email || ''} onChange={(e) => setEditingUser({ ...editingUser, email: e.target.value })} style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第二部分:修改密码 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<h3 style={{ color: '#fbbf24', fontSize: '15px', fontWeight: 'bold', marginBottom: '16px', paddingBottom: '8px', borderBottom: '1px solid rgba(251, 191, 36, 0.3)' }}>🔐 修改密码(可选)</h3>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>新密码 <span style={{ color: '#64748b', fontSize: '12px' }}>(留空则不修改)</span></label>
|
||||
<input
|
||||
type="password"
|
||||
value={editingUser.newPassword || ''}
|
||||
onChange={(e) => setEditingUser({ ...editingUser, newPassword: e.target.value })}
|
||||
placeholder="请输入新密码(至少 6 位)"
|
||||
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
|
||||
/>
|
||||
{/* 会员信息 */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3 style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px', paddingBottom: '6px', borderBottom: '1px solid rgba(251, 191, 36, 0.3)' }}>⭐ 会员信息</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||
<div><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>角色</label><select value={editingUser.role || 'user'} onChange={(e) => setEditingUser({ ...editingUser, role: e.target.value })} style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}><option value="user">普通用户</option><option value="editor">信息员</option><option value="admin">管理员</option></select></div>
|
||||
<div><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>会员等级</label><select value={editingUser.level || '青铜'} onChange={(e) => setEditingUser({ ...editingUser, level: e.target.value })} style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}><option value="青铜">🥉 青铜</option><option value="白银">🥈 白银</option><option value="黄金">🥇 黄金</option><option value="钻石">💎 钻石</option><option value="王者">👑 王者</option></select></div>
|
||||
<div><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>积分</label><input type="number" value={editingUser.points || 0} onChange={(e) => setEditingUser({ ...editingUser, points: parseInt(e.target.value) || 0 })} style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} /></div>
|
||||
<div><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>余额</label><input type="number" value={editingUser.balance || 0} onChange={(e) => setEditingUser({ ...editingUser, balance: parseFloat(e.target.value) || 0 })} style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>确认新密码</label>
|
||||
<input
|
||||
type="password"
|
||||
value={editingUser.confirmPassword || ''}
|
||||
onChange={(e) => setEditingUser({ ...editingUser, confirmPassword: e.target.value })}
|
||||
placeholder="请再次输入新密码"
|
||||
style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
|
||||
/>
|
||||
{/* 统计信息(只读) */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3 style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px', paddingBottom: '6px', borderBottom: '1px solid rgba(251, 191, 36, 0.3)' }}>📊 统计数据(仅展示)</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px' }}>
|
||||
<div style={{ background: '#0f172a', padding: '8px', borderRadius: '6px', textAlign: 'center' }}><div style={{ color: '#64748b', fontSize: '10px' }}>藏品</div><div style={{ color: '#22c55e', fontSize: '14px', fontWeight: 'bold' }}>{editingUser.collectionCount || 0}</div></div>
|
||||
<div style={{ background: '#0f172a', padding: '8px', borderRadius: '6px', textAlign: 'center' }}><div style={{ color: '#64748b', fontSize: '10px' }}>AI识别</div><div style={{ color: '#3b82f6', fontSize: '14px', fontWeight: 'bold' }}>{editingUser.aiCount || 0}</div></div>
|
||||
<div style={{ background: '#0f172a', padding: '8px', borderRadius: '6px', textAlign: 'center' }}><div style={{ color: '#64748b', fontSize: '10px' }}>寻号</div><div style={{ color: '#f59e0b', fontSize: '14px', fontWeight: 'bold' }}>{editingUser.searchCount || 0}</div></div>
|
||||
<div style={{ background: '#0f172a', padding: '8px', borderRadius: '6px', textAlign: 'center' }}><div style={{ color: '#64748b', fontSize: '10px' }}>登录</div><div style={{ color: '#8b5cf6', fontSize: '14px', fontWeight: 'bold' }}>{editingUser.loginCount || 0}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 修改密码 */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h3 style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px', paddingBottom: '6px', borderBottom: '1px solid rgba(251, 191, 36, 0.3)' }}>🔐 修改密码(可选)</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
|
||||
<div><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>新密码</label><input type="password" value={editingUser.newPassword || ''} onChange={(e) => setEditingUser({ ...editingUser, newPassword: e.target.value })} placeholder="留空则不修改" style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} /></div>
|
||||
<div><label style={{ display: 'block', color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>确认密码</label><input type="password" value={editingUser.confirmPassword || ''} onChange={(e) => setEditingUser({ ...editingUser, confirmPassword: e.target.value })} placeholder="再次输入" style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button
|
||||
onClick={() => setEditingUser(null)}
|
||||
style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEditUser}
|
||||
style={{ flex: 1, padding: '12px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', fontWeight: '600', cursor: 'pointer' }}
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
<button onClick={() => setEditingUser(null)} style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>取消</button>
|
||||
<button onClick={handleEditUser} style={{ flex: 1, padding: '12px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', fontWeight: '600', cursor: 'pointer' }}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 版本号 - 移到表格下方,避免被底部导航遮挡 */}
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: 'rgba(255,255,255,0.3)', fontSize: '12px' }}>
|
||||
v{APP_VERSION}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue