同步C环境最新代码 - v1.2.24 (包含寻号和行情区分)

This commit is contained in:
甲辰生产 2026-03-29 16:20:51 +08:00
parent 27e14270a3
commit 509e37cd98
6 changed files with 657 additions and 122 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>甲辰收藏 v1.2.19</title>
<title>甲辰收藏 v1.2.24</title>
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />

View File

@ -10,9 +10,12 @@ 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 (
@ -26,63 +29,131 @@ 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 }
if (!newUser.username || !newUser.password) {
alert('用户名和密码为必填项')
return
}
// email
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.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.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
role: editingUser.role
}
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>
@ -91,116 +162,257 @@ 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(u => (
<div key={u.id} style={{ background: 'rgba(30, 41, 59, 0.8)', borderRadius: '12px', padding: '16px', position: 'relative' }}>
{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' }}><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 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>
{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 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: '18px', fontWeight: 'bold' }}>¥{u.balance || 0}</div>
<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>
</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={{ 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({ ...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>}
<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>}
{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 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>
</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' }}>
<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: '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 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>
</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 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={{ 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>
)
}

View File

@ -0,0 +1,272 @@
import React, { useState, useEffect } from 'react'
// V2
export default function AdminV2() {
const [activeTab, setActiveTab] = useState('users')
const token = localStorage.getItem('token')
const API_BASE = localStorage.getItem('API_BASE') || ''
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>
)
}
return (
<div style={{ padding: '16px', paddingBottom: '80px', minHeight: '100vh', background: '#0f172a' }}>
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<h1 style={{ fontSize: '20px', color: '#fbbf24', margin: 0 }}> 管理后台</h1>
</div>
<div style={{ display: 'flex', background: '#1e293b', borderRadius: '8px', padding: '4px', marginBottom: '16px', flexWrap: 'wrap' }}>
{[
{ key: 'users', label: '👥 用户管理' },
{ key: 'stats', label: '📊 统计分析' },
{ key: 'publish', label: '📢 信息发布' },
{ key: 'infoManage', label: '📋 信息管理' }
].map(tab => (
<div key={tab.key} onClick={() => setActiveTab(tab.key)} style={{ flex: '1 1 45%', textAlign: 'center', padding: '10px 8px', margin: '2px', borderRadius: '6px', cursor: 'pointer', background: activeTab === tab.key ? '#3b82f6' : 'transparent', color: activeTab === tab.key ? '#fff' : '#94a3b8', fontSize: '13px', fontWeight: activeTab === tab.key ? 'bold' : 'normal' }}>
{tab.label}
</div>
))}
</div>
{activeTab === 'users' && <UserManagement token={token} API_BASE={API_BASE} />}
{activeTab === 'stats' && <Statistics token={token} API_BASE={API_BASE} />}
{activeTab === 'publish' && <InfoPublish token={token} API_BASE={API_BASE} />}
{activeTab === 'infoManage' && <InfoManage token={token} API_BASE={API_BASE} />}
</div>
)
}
function UserManagement({ token, API_BASE }) {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [editingUser, setEditingUser] = useState(null)
const [showAddModal, setShowAddModal] = useState(false)
const [newUser, setNewUser] = useState({ username: '', password: '', phone: '', role: 'user' })
useEffect(() => { fetchUsers() }, [])
const fetchUsers = async () => {
setLoading(true)
try {
const res = await fetch(`${API_BASE}/api/admin/users?page=1&limit=100`, { headers: { 'Authorization': `Bearer ${token}` } })
const data = await res.json()
setUsers(Array.isArray(data) ? data : [])
} catch (e) { console.error(e) }
setLoading(false)
}
const handleAddUser = async () => {
if (!newUser.username || !newUser.password) { alert('请填写用户名和密码'); return }
try {
const res = await fetch(`${API_BASE}/api/admin/users`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(newUser) })
if (res.ok) { alert('添加成功'); setShowAddModal(false); setNewUser({ username: '', password: '', phone: '', role: 'user' }); fetchUsers() }
} catch (e) { alert('添加失败') }
}
const handleUpdateUser = async () => {
try {
const res = await fetch(`${API_BASE}/api/admin/users/${editingUser.f99_90_id}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(editingUser) })
if (res.ok) { alert('更新成功'); setEditingUser(null); fetchUsers() }
} catch (e) { alert('更新失败') }
}
const handleDeleteUser = async (id) => {
if (!confirm('确定删除该用户?')) return
try { await fetch(`${API_BASE}/api/admin/users/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); fetchUsers() } catch (e) { console.error(e) }
}
const filteredUsers = users.filter(u => u.f01_01_name?.includes(search) || u.phone?.includes(search) || u.user_code?.includes(search))
return (
<div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
<input type="text" placeholder="搜索用户..." value={search} onChange={(e) => setSearch(e.target.value)} style={{ flex: 1, padding: '10px', background: '#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff' }} />
<button onClick={() => setShowAddModal(true)} style={{ padding: '10px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>+ 添加</button>
</div>
{loading ? <div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>加载中...</div> : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: '12px' }}>
{filteredUsers.map(u => (
<div key={u.f99_90_id} style={{ background: '#1e293b', borderRadius: '12px', padding: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ width: '40px', height: '40px', borderRadius: '50%', background: '#3b82f6', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: '16px' }}>{u.f01_01_name?.[0] || '?'}</div>
<div><div style={{ color: '#fff', fontWeight: 'bold' }}>{u.f01_01_name}</div><div style={{ color: '#64748b', fontSize: '12px' }}>{u.user_code || '-'}</div></div>
</div>
<span style={{ background: u.role === 'admin' ? '#f59e0b' : '#10b981', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '12px' }}>{u.role === 'admin' ? '管理员' : '用户'}</span>
</div>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '8px' }}>
<div>📱 {u.phone || '-'}</div><div>🏷 等级: {u.f99_94_level || '青铜'} | 藏品: {u.f99_97_collection_count || 0}</div>
<div>🔑 登录: {u.f99_98_login_count || 0} | AI: {u.f99_95_ai_count || 0}</div>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => setEditingUser(u)} style={{ flex: 1, padding: '6px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>编辑</button>
<button onClick={() => handleDeleteUser(u.f99_90_id)} style={{ flex: 1, padding: '6px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>删除</button>
</div>
</div>
))}
</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: '20px', width: '90%', maxWidth: '400px' }}>
<h3 style={{ color: '#fbbf24', marginTop: 0 }}>添加用户</h3>
<input type="text" placeholder="用户名" value={newUser.username} onChange={(e) => setNewUser({...newUser, username: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
<input type="password" placeholder="密码" value={newUser.password} onChange={(e) => setNewUser({...newUser, password: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
<input type="text" placeholder="手机号" value={newUser.phone} onChange={(e) => setNewUser({...newUser, phone: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '12px', boxSizing: 'border-box' }} />
<select value={newUser.role} onChange={(e) => setNewUser({...newUser, role: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '12px' }}><option value="user">普通用户</option><option value="admin">管理员</option></select>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={handleAddUser} style={{ flex: 1, padding: '10px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>添加</button>
<button onClick={() => setShowAddModal(false)} style={{ flex: 1, padding: '10px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '6px', 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: '20px', width: '90%', maxWidth: '400px' }}>
<h3 style={{ color: '#fbbf24', marginTop: 0 }}>编辑用户</h3>
<input type="text" placeholder="手机号" value={editingUser.phone || ''} onChange={(e) => setEditingUser({...editingUser, phone: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
<select value={editingUser.role} onChange={(e) => setEditingUser({...editingUser, role: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px' }}><option value="user">普通用户</option><option value="admin">管理员</option></select>
<input type="number" placeholder="积分" value={editingUser.f99_100_points || 0} onChange={(e) => setEditingUser({...editingUser, f99_100_points: parseInt(e.target.value)})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
<input type="number" placeholder="余额" value={editingUser.f01_11_balance || 0} onChange={(e) => setEditingUser({...editingUser, f01_11_balance: parseFloat(e.target.value)})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '12px', boxSizing: 'border-box' }} />
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={handleUpdateUser} style={{ flex: 1, padding: '10px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>保存</button>
<button onClick={() => setEditingUser(null)} style={{ flex: 1, padding: '10px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>取消</button>
</div>
</div>
</div>
)}
</div>
)
}
function Statistics({ token, API_BASE }) {
const [stats, setStats] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => { fetchStats() }, [])
const fetchStats = async () => {
setLoading(true)
try {
const collectionsRes = await fetch(`${API_BASE}/api/collections/stats`, { headers: { 'Authorization': `Bearer ${token}` } })
const collectionsData = await collectionsRes.json()
const usersRes = await fetch(`${API_BASE}/api/admin/users?page=1&limit=100`, { headers: { 'Authorization': `Bearer ${token}` } })
const usersData = await usersRes.json()
const totalUsers = Array.isArray(usersData) ? usersData.length : 0
const totalCollections = collectionsData.total || 0
const levelCount = {}
if (Array.isArray(usersData)) { usersData.forEach(u => { const level = u.f99_94_level || '青铜'; levelCount[level] = (levelCount[level] || 0) + 1 }) }
const statusCount = { in_collection: collectionsData.byStatus?.find(s => s.status === 'in_collection')?.count || 0, sold: collectionsData.byStatus?.find(s => s.status === 'sold')?.count || 0 }
const totalBalance = Array.isArray(usersData) ? usersData.reduce((sum, u) => sum + (u.f01_11_balance || 0), 0) : 0
setStats({ users: { total: totalUsers, levels: levelCount }, collections: { total: totalCollections, status: statusCount }, balance: { total: totalBalance } })
} catch (e) { console.error(e) }
setLoading(false)
}
if (loading) return <div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>加载中...</div>
return (
<div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px', marginBottom: '16px' }}>
<div style={{ background: 'linear-gradient(135deg, #3b82f6, #1d4ed8)', borderRadius: '12px', padding: '16px', textAlign: 'center' }}><div style={{ fontSize: '32px', fontWeight: 'bold', color: '#fff' }}>{stats?.users?.total || 0}</div><div style={{ color: 'rgba(255,255,255,0.8)', fontSize: '14px' }}>👥 用户总数</div></div>
<div style={{ background: 'linear-gradient(135deg, #10b981, #059669)', borderRadius: '12px', padding: '16px', textAlign: 'center' }}><div style={{ fontSize: '32px', fontWeight: 'bold', color: '#fff' }}>{stats?.collections?.total || 0}</div><div style={{ color: 'rgba(255,255,255,0.8)', fontSize: '14px' }}>📚 藏品总数</div></div>
<div style={{ background: 'linear-gradient(135deg, #f59e0b, #d97706)', borderRadius: '12px', padding: '16px', textAlign: 'center' }}><div style={{ fontSize: '32px', fontWeight: 'bold', color: '#fff' }}>¥{stats?.balance?.total?.toFixed(2) || '0'}</div><div style={{ color: 'rgba(255,255,255,0.8)', fontSize: '14px' }}>💰 账户总余额</div></div>
<div style={{ background: 'linear-gradient(135deg, #8b5cf6, #7c3aed)', borderRadius: '12px', padding: '16px', textAlign: 'center' }}><div style={{ fontSize: '32px', fontWeight: 'bold', color: '#fff' }}>{stats?.collections?.status?.in_collection || 0}</div><div style={{ color: 'rgba(255,255,255,0.8)', fontSize: '14px' }}> 收藏中</div></div>
</div>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}><h3 style={{ color: '#fbbf24', marginTop: 0, marginBottom: '12px', fontSize: '16px' }}>🏆 会员等级分布</h3><div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>{Object.entries(stats?.users?.levels || {}).map(([level, count]) => (<div key={level} style={{ background: '#0f172a', padding: '8px 12px', borderRadius: '8px' }}><span style={{ color: '#fff', fontWeight: 'bold' }}>{level}</span><span style={{ color: '#64748b', fontSize: '12px', marginLeft: '8px' }}>{count}</span></div>))}</div></div>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '16px' }}><h3 style={{ color: '#fbbf24', marginTop: 0, marginBottom: '12px', fontSize: '16px' }}>📦 藏品状态</h3><div style={{ display: 'flex', gap: '12px' }}><div style={{ flex: 1, background: '#10b98120', padding: '12px', borderRadius: '8px', textAlign: 'center' }}><div style={{ fontSize: '24px', fontWeight: 'bold', color: '#10b981' }}>{stats?.collections?.status?.in_collection || 0}</div><div style={{ color: '#94a3b8', fontSize: '12px' }}>收藏中</div></div><div style={{ flex: 1, background: '#ef444420', padding: '12px', borderRadius: '8px', textAlign: 'center' }}><div style={{ fontSize: '24px', fontWeight: 'bold', color: '#ef4444' }}>{stats?.collections?.status?.sold || 0}</div><div style={{ color: '#94a3b8', fontSize: '12px' }}>已售出</div></div></div></div>
</div>
)
}
function InfoPublish({ token, API_BASE }) {
const [publishType, setPublishType] = useState('system')
const [formData, setFormData] = useState({ title: '', content: '', expect_version: '', expect_packaging: '', deal_price: '' })
const [submitting, setSubmitting] = useState(false)
const handlePublish = async () => {
if (!formData.title) { alert('请输入标题'); return }
setSubmitting(true)
try {
const data = { title: formData.title, content: formData.content, info_type: publishType === 'system' ? 'publish' : 'deal', deal_price: publishType === 'deal' ? parseFloat(formData.deal_price) : null, expect_version: formData.expect_version, expect_packaging: formData.expect_packaging }
const res = await fetch(`${API_BASE}/api/information/`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
if (res.ok) { alert('发布成功!'); setFormData({ title: '', content: '', expect_version: '', expect_packaging: '', deal_price: '' }) }
} catch (e) { alert('发布失败') }
setSubmitting(false)
}
return (
<div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
<button onClick={() => setPublishType('system')} style={{ flex: 1, padding: '12px', background: publishType === 'system' ? '#3b82f6' : '#1e293b', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>📢 系统通知</button>
<button onClick={() => setPublishType('deal')} style={{ flex: 1, padding: '12px', background: publishType === 'deal' ? '#10b981' : '#1e293b', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>📈 成交数据</button>
</div>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '16px' }}>
<h3 style={{ color: '#fbbf24', marginTop: 0, marginBottom: '16px' }}>{publishType === 'system' ? '📢 发布系统通知' : '📈 发布成交数据'}</h3>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>标题 *</label><input type="text" value={formData.title} onChange={(e) => setFormData({...formData, title: e.target.value})} placeholder={publishType === 'system' ? '请输入通知标题' : '如2024版纪念钞成交'} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginTop: '4px', boxSizing: 'border-box' }} /></div>
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>内容</label><textarea value={formData.content} onChange={(e) => setFormData({...formData, content: e.target.value})} placeholder="请输入详细内容..." rows={4} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginTop: '4px', boxSizing: 'border-box', resize: 'vertical' }} /></div>
{publishType === 'deal' && (<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}><div style={{ flex: 1 }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>成交价()</label><input type="number" value={formData.deal_price} onChange={(e) => setFormData({...formData, deal_price: e.target.value})} placeholder="成交金额" style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginTop: '4px', boxSizing: 'border-box' }} /></div><div style={{ flex: 1 }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>版别</label><input type="text" value={formData.expect_version} onChange={(e) => setFormData({...formData, expect_version: e.target.value})} placeholder="如2024版" style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginTop: '4px', boxSizing: 'border-box' }} /></div><div style={{ flex: 1 }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>包装</label><input type="text" value={formData.expect_packaging} onChange={(e) => setFormData({...formData, expect_packaging: e.target.value})} placeholder="如:标十" style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginTop: '4px', boxSizing: 'border-box' }} /></div></div>)}
<button onClick={handlePublish} disabled={submitting} style={{ width: '100%', padding: '12px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '16px', fontWeight: 'bold', opacity: submitting ? 0.6 : 1 }}>{submitting ? '发布中...' : '发布'}</button>
</div>
</div>
)
}
function InfoManage({ token, API_BASE }) {
const [infoList, setInfoList] = useState([])
const [loading, setLoading] = useState(true)
const [filterType, setFilterType] = useState('all')
useEffect(() => { fetchInfoList() }, [])
const fetchInfoList = async () => {
setLoading(true)
try { const res = await fetch(`${API_BASE}/api/information/list?status=active`, { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); setInfoList(data || []) } catch (e) { console.error(e) }
setLoading(false)
}
const handleDelete = async (id) => { if (!confirm('确定删除该信息?')) return; try { await fetch(`${API_BASE}/api/information/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); fetchInfoList() } catch (e) { console.error(e) } }
const handleClose = async (id) => { try { await fetch(`${API_BASE}/api/information/${id}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'closed' }) }); fetchInfoList() } catch (e) { console.error(e) } }
const filteredList = filterType === 'all' ? infoList : infoList.filter(i => i.info_type === filterType)
const typeLabels = { seek: '🔍 寻配号', deal: '📈 成交', publish: '📝 发布' }
return (
<div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px', overflowX: 'auto' }}>{['all', 'seek', 'deal', 'publish'].map(type => (<button key={type} onClick={() => setFilterType(type)} style={{ padding: '8px 12px', background: filterType === type ? '#3b82f6' : '#1e293b', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', whiteSpace: 'nowrap', fontSize: '13px' }}>{type === 'all' ? '全部' : typeLabels[type]}</button>))}</div>
{loading ? <div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>加载中...</div> : filteredList.length === 0 ? <div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>暂无信息</div> : (
filteredList.map(item => (
<div key={item.id} style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<span style={{ background: item.info_type === 'seek' ? '#3b82f6' : item.info_type === 'deal' ? '#10b981' : '#f59e0b', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '12px' }}>{typeLabels[item.info_type]}</span>
<span style={{ color: '#64748b', fontSize: '12px' }}>{new Date(item.created_at).toLocaleDateString('zh-CN')}</span>
</div>
<h4 style={{ color: '#fff', margin: '0 0 8px 0' }}>{item.title}</h4>
{item.content && <p style={{ color: '#94a3b8', fontSize: '13px', margin: '0 0 8px 0' }}>{item.content}</p>}
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}>👤 {item.user_name} | 👁 {item.view_count} | 📞 {item.contact_count}</div>
<div style={{ display: 'flex', gap: '8px' }}><button onClick={() => handleClose(item.id)} style={{ flex: 1, padding: '6px', background: '#f59e0b', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>关闭</button><button onClick={() => handleDelete(item.id)} style={{ flex: 1, padding: '6px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>删除</button></div>
</div>
))
)}
</div>
)
}

View File

@ -1,6 +1,12 @@
import React, { useState, useEffect } from 'react'
export default function Info() {
//
if (!localStorage.getItem('token')) {
window.location.hash = '#/login'
return null
}
const [activeTab, setActiveTab] = useState('publish')
const [showPublish, setShowPublish] = useState(false)
const [myList, setMyList] = useState([])

View File

@ -3,13 +3,12 @@ import { APP_VERSION } from '../config/version'
export default function List() {
const [collections, setCollections] = useState([])
const [highlightId, setHighlightId] = useState('')
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState('')
const [filterType, setFilterType] = useState('')
const urlParams = new URLSearchParams(window.location.hash.split('?')[1] || '')
const urlId = urlParams.get('id') || ''
const [userIdFilter, setUserIdFilter] = useState(urlParams.get('userId') || '')
const urlUserId = urlParams.get('userId') || ''
const [userIdFilter, setUserIdFilter] = useState(urlUserId)
const [sortField, setSortField] = useState('createdAt')
const [sortOrder, setSortOrder] = useState('desc')
const [viewMode, setViewMode] = useState('list')
@ -32,9 +31,6 @@ export default function List() {
// hash
const handleHashChange = () => {
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
// id -
const idParam = params.get('id') || ''
setHighlightId(idParam)
// filter=category&value= filter=category=
let filterTypeParam = params.get('filter') || ''
let valueParam = params.get('value') || ''
@ -71,15 +67,11 @@ export default function List() {
try {
// URL
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
const urlId = params.get('id') || ''
const urlFilterType = params.get('filter') || ''
const urlFilterValue = params.get('value') ? decodeURIComponent(params.get('value')) : ''
let api = '/api/collections?page=' + page + '&limit=100&sortBy=' + sortField + '&sortOrder=' + sortOrder
// idid
if (urlId) {
api += '&id=' + encodeURIComponent(urlId)
} else if (urlFilterType && urlFilterValue) {
if (urlFilterType && urlFilterValue) {
api += '&' + urlFilterType + '=' + encodeURIComponent(urlFilterValue)
}
const res = await fetch(api, {
@ -109,7 +101,6 @@ export default function List() {
if (filterType && filter) {
const fieldMap = {
id: 'id',
status: 'status',
category: 'category',
packaging: 'packaging',
@ -415,8 +406,8 @@ export default function List() {
return { bg: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)', color: '#1e293b' }
}
const ListItem = ({ item, highlight }) => (
<div onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: highlight ? '2px solid #10b981' : '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '10px', marginBottom: '8px', cursor: 'pointer', boxShadow: highlight ? '0 0 10px rgba(16,185,129,0.3)' : 'none' }}>
const ListItem = ({ item }) => (
<div onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '10px', marginBottom: '8px', cursor: 'pointer' }}>
{/* 第1行编号 + 冠字号 + 包装(蓝色) | 状态 + 持仓类型 + 珍惜度(紫色) */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
@ -592,7 +583,7 @@ export default function List() {
) : viewMode === 'grid' ? (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}>
{filteredCollections.map(item => (
<div key={item.id} onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: item.id === highlightId ? '2px solid #10b981' : '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '12px', cursor: 'pointer', boxShadow: item.id === highlightId ? '0 0 10px rgba(16,185,129,0.3)' : 'none' }}>
<div key={item.id} onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '12px', cursor: 'pointer' }}>
<div style={{ height: '80px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', fontSize: '32px', marginBottom: '10px' }}>🐉</div>
<div style={{ color: '#fff', fontSize: '11px', fontWeight: 'normal' }}>{item.code || '-'}</div>
<div style={{ color: '#fbbf24', fontSize: '11px', fontFamily: 'monospace', marginTop: '2px' }}>{formatPrefixSerial(item.prefixSerial)}</div>
@ -604,7 +595,7 @@ export default function List() {
))}
</div>
) : (
filteredCollections.map(item => <ListItem key={item.id} item={item} highlight={item.id === highlightId} />)
filteredCollections.map(item => <ListItem key={item.id} item={item} />)
)}
</div>
</div>

View File

@ -1,4 +1,13 @@
import React, { useState, useEffect } from 'react'
//
const getUserPhone = () => {
try {
const user = JSON.parse(localStorage.getItem('user') || '{}')
console.log('getUserPhone user:', user)
// Try multiple possible field names
return user.phone || user.phoneNumber || user.mobile || user.tel || ''
} catch { return '' }
}
// -
export default function News() {
@ -12,8 +21,9 @@ export default function News() {
const [matchedStatus, setMatchedStatus] = useState({}) //
const [showMatchList, setShowMatchList] = useState(false)
const [matchCollections, setMatchCollections] = useState([])
const [customModal, setCustomModal] = useState({show: false, title: '', content: ''})
const [seekForm, setSeekForm] = useState({
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: (() => { try { const u = JSON.parse(localStorage.getItem('user') || '{}'); return u.phone || '' } catch { return '' } })() || ''
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || ''
})
const [infoList, setInfoList] = useState([])
const [viewMode, setViewMode] = useState('all')
@ -117,7 +127,7 @@ export default function News() {
if (data.message === '匹配成功,已通知发布者') {
setMatchedStatus(prev => ({...prev, [infoId]: 'matched'}))
setShowContactId(infoId) //
alert('匹配成功!')
setCustomModal({show: true, title: '匹配成功', content: '已通知发布者,请等待对方联系'})
fetchInfoList() //
}
} catch (e) {
@ -125,21 +135,45 @@ export default function News() {
}
}
const fetchMatchedUserInfo = async (infoId) => {
const token = localStorage.getItem('token')
if (!token) return
console.log('fetchMatchedUserInfo called with:', infoId)
let token = localStorage.getItem('token')
if (!token) {
// Try to refresh token
const username = localStorage.getItem('username')
const password = localStorage.getItem('password')
if (username && password) {
const form = new FormData()
form.append('username', username)
form.append('password', password)
const loginRes = await fetch(`${API_BASE}/api/auth/login`, { method: 'POST', body: form })
const loginData = await loginRes.json()
if (loginData.access_token) {
token = loginData.access_token
localStorage.setItem('token', token)
}
}
if (!token) { alert('请先登录'); return }
}
try {
const res = await fetch(`${API_BASE}/api/information/seek/matched-user/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
console.log('Matched user response:', res.status)
const data = await res.json()
if (data.matched_user_id) alert(`匹配者: ${data.user_name}, 联系方式: ${data.matched_contact || '未提供'}`)
console.log('Matched user data:', data)
if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.matched_contact || '未提供'}`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
const fetchPublisherInfo = async (infoId) => {
console.log('fetchPublisherInfo called with:', infoId)
const token = localStorage.getItem('token')
if (!token) return
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/seek/publisher/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
console.log('Publisher response:', res.status)
const data = await res.json()
if (data.user_id) alert(`发布者: ${data.user_name}, 联系方式: ${data.contact || '未提供'}`)
console.log('Publisher data:', data)
if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.contact || '未提供'}`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
const fetchMatchCollections = async (infoId) => {
@ -227,14 +261,13 @@ export default function News() {
} catch (e) { alert('更新失败') }
}
const handleSeekPublish = async () => { console.log('handleSeekPublish called')
const phone = (() => { try { const u = JSON.parse(localStorage.getItem('user') || '{}'); return u.phone || '' } catch { return '' } })()
const finalContact = seekForm.contact || phone
const handleSeekPublish = async () => {
const phone = getUserPhone()
const finalContact = seekForm.contact || phone || ''
if (!finalContact) { alert('请填写联系方式'); return }
//
const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + finalContact
try {
const token = localStorage.getItem('token')
const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + finalContact
// editioncategory
const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' }
const res = await fetch(`${API_BASE}/api/information/`, {
@ -251,7 +284,7 @@ export default function News() {
if (data.id || data.code === 0) {
alert('发布成功!')
setShowSeekPublish(false)
setSeekForm({ edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: (() => { try { const u = JSON.parse(localStorage.getItem('user') || '{}'); return u.phone || '' } catch { return '' } })() || '' })
setSeekForm({ edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || '' })
fetchInfoList()
} else { alert(data.message || '发布失败') }
} catch (e) { alert('发布失败: ' + e.message) }
@ -517,7 +550,7 @@ export default function News() {
if (item.matched_count > 0) {
matchAndContact(item.id)
} else {
alert('暂无匹配藏品,无法匹配')
setCustomModal({show: true, title: '提示', content: '暂无匹配藏品,无法匹配'})
}
}}
disabled={item.matched_count === 0}
@ -768,6 +801,27 @@ export default function News() {
</div>
</div>
)}
{customModal.show && (
<div style={{position:'fixed',top:0,left:0,right:0,bottom:0,background:'rgba(0,0,0,0.7)',zIndex:2000,display:'flex',alignItems:'center',justifyContent:'center'}}>
<div style={{background:'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)',borderRadius:'16px',padding:'24px',width:'85%',maxWidth:'380px',boxShadow:'0 20px 60px rgba(0,0,0,0.5)',border:'1px solid rgba(255,255,255,0.1)'}}>
<div style={{textAlign:'center',marginBottom:'20px'}}>
<div style={{width:'60px',height:'60px',background:'linear-gradient(135deg, #10b981 0%, #059669 100%)',borderRadius:'50%',display:'flex',alignItems:'center',justifyContent:'center',margin:'0 auto 12px'}}>
<span style={{fontSize:'28px'}}></span>
</div>
<h3 style={{color:'#10b981',fontSize:'20px',fontWeight:'bold',margin:'0'}}>{customModal.title}</h3>
</div>
<div style={{background:'rgba(0,0,0,0.3)',borderRadius:'12px',padding:'16px',marginBottom:'20px'}}>
{customModal.content.split('\n').map((line, i) => (
<div key={i} style={{color:'#e2e8f0',fontSize:'15px',lineHeight:'2',display:'flex',justifyContent:'space-between'}}>
<span style={{color:'#94a3b8'}}>{line.split(':')[0]}</span>
<span style={{color:'#fbbf24',fontWeight:'600'}}>{line.split(':')[1]}</span>
</div>
))}
</div>
<button onClick={()=>setCustomModal({...customModal,show:false})} style={{width:'100%',padding:'14px',background:'linear-gradient(135deg, #10b981 0%, #059669 100%)',border:'none',borderRadius:'10px',color:'#fff',fontSize:'16px',fontWeight:'bold',cursor:'pointer',boxShadow:'0 4px 15px rgba(16,185,129,0.4)'}}>知道了</button>
</div>
</div>
)}
</div>
)
}