jiachenlong/frontend/src/pages/Admin.jsx

207 lines
21 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version'
export default function Admin() {
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 token = localStorage.getItem('token')
const userStr = localStorage.getItem('user')
let user = null
try { user = userStr ? JSON.parse(userStr) : null } catch (e) {}
if (!user || user.role !== 'admin') {
return (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#0f172a', color: '#ef4444', padding: '20px' }}>
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🚫</div>
<div style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '8px' }}>无权访问</div>
<div style={{ color: '#94a3b8', fontSize: '14px' }}>仅管理员可以访问用户管理界面</div>
</div>
</div>
)
}
useEffect(() => { fetchUsers() }, [])
const fetchUsers = async () => {
try {
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) }
}
const handleDeleteUser = async (userId, username) => {
if (!confirm(`确定要删除用户 "${username}" 吗?`)) return
try {
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) }
}
const handleAddUser = async () => {
if (!newUser.username || !newUser.password) { alert('用户名和密码为必填项'); return }
const payload = { ...newUser }
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 || '添加失败') }
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 updateData = {
username: editingUser.username,
email: editingUser.email,
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 || '更新失败') }
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(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' }}><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' }}>📧 {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: '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' }}>注册时间{u.created_at ? new Date(u.created_at).toLocaleDateString('zh-CN') : '-'}</div>
<div style={{ display: 'flex', gap: '8px' }}>
<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>}
{/* 添加用户弹窗 */}
{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: '600px', maxHeight: '90vh', overflowY: 'auto' }}>
<h2 style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold', marginBottom: '20px' }}>编辑用户</h2>
{/* 基本信息 */}
<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: '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: '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>
</div>
</div>
</div>
)}
</div>
)
}