diff --git a/admin-frontend/src/App.jsx b/admin-frontend/src/App.jsx index 7b3851f..28d4358 100644 --- a/admin-frontend/src/App.jsx +++ b/admin-frontend/src/App.jsx @@ -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 ( -
-
+
+

甲辰管理后台

- 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' - }} - /> - 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' - }} - /> + 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' }} /> + 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 &&
{error}
} -
@@ -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 + const tabs = [ + { key: 'users', label: '👥 用户管理', icon: '👥' }, + { key: 'stats', label: '📊 统计分析', icon: '📊' }, + { key: 'info', label: '📝 资讯管理', icon: '📝' }, + ] + return ( -
-

甲辰管理后台 - 登录成功

-

欢迎 {user.username || user.f01_01_name}

- +
+ {/* 顶部栏 */} +
+

甲辰管理后台

+
+ {user.username || user.name || '管理员'} + +
+
+ + {/* 主内容 */} +
+ {activeTab === 'users' && } + {activeTab === 'stats' &&
统计分析功能开发中...
} + {activeTab === 'info' &&
资讯管理功能开发中...
} +
+ + {/* 底部导航 */} +
+ {tabs.map(tab => ( +
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} +
+ ))} +
) } diff --git a/admin-frontend/src/pages/Users.jsx b/admin-frontend/src/pages/Users.jsx new file mode 100644 index 0000000..3075d7f --- /dev/null +++ b/admin-frontend/src/pages/Users.jsx @@ -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
加载中...
+ if (error) return
⚠️ {error}
+ + return ( +
+
+

⚙️ 用户管理

+ +
+ + {/* 用户列表 */} +
+ {users.map(user => ( +
+
+
+
+
{user.username}
+
+ {user.role === 'admin' ? '👑 管理员' : user.role === 'editor' ? '📝 信息员' : '👤 用户'} +
+
+
📧 {user.email || '未设置'} | 📱 {user.phone || '未设置'}
+
+
+
藏品数
+
{user.collectionCount || 0}
+
+
+
+
注册时间:{user.created_at ? new Date(user.created_at).toLocaleDateString('zh-CN') : '-'}
+
+ + {user.role !== 'admin' && ( + + )} +
+
+
+ ))} +
+ + {users.length === 0 &&
暂无用户数据
} + + {/* 添加用户模态框 */} + {showAddModal && ( +
+
+

添加用户

+
+ + 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' }} /> +
+
+ + 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' }} /> +
+
+ + 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' }} /> +
+
+ + +
+
+ + +
+
+
+ )} + + {/* 编辑用户模态框 */} + {editingUser && ( +
+
+

编辑用户

+ +
+

📋 基本信息

+
+ + 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' }} /> +
+
+ + 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' }} /> +
+
+ + +
+
+ +
+

🔐 修改密码(可选)

+
+ + 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' }} /> +
+
+ + 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' }} /> +
+
+ +
+ + +
+
+
+ )} +
+ ) +} diff --git a/config/VERSION b/config/VERSION index f9d405b..8f8608d 100644 --- a/config/VERSION +++ b/config/VERSION @@ -1 +1 @@ -VERSION=1.2.14 +VERSION=1.2.15 diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 653f65f..7e7953d 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -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
加载中...
@@ -162,257 +91,116 @@ export default function Admin() { return (
-

- ⚙️ 用户管理 -

- +

⚙️ 用户管理

+
- {/* 用户列表 - 卡片式布局 */}
- {users.map((user) => ( -
+ {users.map(u => ( +
-
{user.username}
-
- {user.role === 'admin' ? '👑 管理员' : '👤 用户'} +
#{u.user_code} {u.username}
+
+ {u.role === 'admin' ? '👑 管理员' : u.role === 'editor' ? '📝 信息员' : '👤 用户'}
+ {u.level && {u.level === '青铜' ? '🥉' : u.level === '白银' ? '🥈' : u.level === '黄金' ? '🥇' : u.level === '钻石' ? '💎' : '👑'} {u.level}}
-
- 📧 {user.email || '未设置'} | 📱 {user.phone || '未设置'} +
📧 {u.email || '未设置'} | 📱 {u.phone || '未设置'}
+
-
藏品数
-
window.location.hash = '#/list?userId=' + user.id} title='点击查看'>{user.collection_count || 0}
+
余额
+
¥{u.balance || 0}
-
- 注册时间:{user.created_at ? new Date(user.created_at).toLocaleDateString('zh-CN') : '-'} -
+
注册时间:{u.created_at ? new Date(u.created_at).toLocaleDateString('zh-CN') : '-'}
- - {user.role !== 'admin' && ( - - )} + + {u.role !== 'admin' && }
))}
- {users.length === 0 && ( -
- 暂无用户数据 -
- )} + {users.length === 0 &&
暂无用户数据
} - {/* 添加用户模态框 */} + {/* 添加用户弹窗 */} {showAddModal && (

添加用户

- -
- - 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' }} - /> -
- -
- - 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' }} - /> -
- -
- - 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' }} - /> -
- -
- - -
- -
- - -
+
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' }} />
+
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' }} />
+
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' }} />
+
+
)} - {/* 编辑用户模态框 */} + {/* 编辑用户弹窗 - 完整字段 */} {editingUser && (
-
+

编辑用户

- {/* 第一部分:基本信息 */} -
-

📋 基本信息

- -
- - 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' }} - /> -
- -
- - 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' }} - /> -
- -
- - + {/* 基本信息 */} +
+

📋 基本信息

+
+
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' }} />
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' }} />
+
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' }} />
+
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' }} />
- - {/* 第二部分:修改密码 */} -
-

🔐 修改密码(可选)

- -
- - 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' }} - /> -
- -
- - 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' }} - /> + + {/* 会员信息 */} +
+

⭐ 会员信息

+
+
+
+
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' }} />
+
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' }} />
- + + {/* 统计信息(只读) */} +
+

📊 统计数据(仅展示)

+
+
藏品
{editingUser.collectionCount || 0}
+
AI识别
{editingUser.aiCount || 0}
+
寻号
{editingUser.searchCount || 0}
+
登录
{editingUser.loginCount || 0}
+
+
+ + {/* 修改密码 */} +
+

🔐 修改密码(可选)

+
+
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' }} />
+
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' }} />
+
+
+
- - + +
)} - - {/* 版本号 - 移到表格下方,避免被底部导航遮挡 */} -
- v{APP_VERSION} -
) }