v1.2.15: 新增管理后台V2(用户管理/统计分析/信息发布/信息管理)
This commit is contained in:
parent
1c2e836593
commit
1833d6e002
|
|
@ -1 +1 @@
|
||||||
VERSION=1.2.14
|
VERSION=1.2.15
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<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">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<title>甲辰收藏 v1.2.14</title>
|
<title>甲辰收藏 v1.2.15</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import Login from './pages/Login'
|
||||||
import Detail from './pages/Detail'
|
import Detail from './pages/Detail'
|
||||||
import Edit from './pages/Edit'
|
import Edit from './pages/Edit'
|
||||||
import Admin from './pages/Admin'
|
import Admin from './pages/Admin'
|
||||||
|
import AdminV2 from './pages/AdminV2'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [path, setPath] = useState(window.location.hash.slice(1) || '/')
|
const [path, setPath] = useState(window.location.hash.slice(1) || '/')
|
||||||
|
|
@ -35,7 +36,7 @@ export default function App() {
|
||||||
if (basePath === '/add') return <Add />
|
if (basePath === '/add') return <Add />
|
||||||
if (basePath === '/login') return <Login />
|
if (basePath === '/login') return <Login />
|
||||||
if (basePath === '/settings') return <Settings />
|
if (basePath === '/settings') return <Settings />
|
||||||
if (basePath === '/admin') return <Admin />
|
if (basePath === '/admin') return <AdminV2 />
|
||||||
if (basePath.startsWith('/edit')) return <Edit />
|
if (basePath.startsWith('/edit')) return <Edit />
|
||||||
if (basePath.startsWith('/detail')) return <Detail />
|
if (basePath.startsWith('/detail')) return <Detail />
|
||||||
return <Home />
|
return <Home />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue