v1.2.41 清理废弃代码,准备开发一尘数据功能
This commit is contained in:
parent
c0260785c2
commit
5f6dffface
|
|
@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
DATABASE_URL = os.getenv(
|
||||
"DATABASE_URL",
|
||||
"postgresql://postgres:postgres@localhost:5432/zodiac"
|
||||
"postgresql://postgres:postgres@127.0.0.1:5432/zodiac"
|
||||
)
|
||||
|
||||
# 增强版数据库引擎配置
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
VERSION=v1.2.38
|
||||
v1.2.41
|
||||
|
|
|
|||
|
|
@ -1,272 +0,0 @@
|
|||
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.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.username?.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.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.username?.[0] || '?'}</div>
|
||||
<div><div style={{ color: '#fff', fontWeight: 'bold' }}>{u.username}</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.level || '青铜'} | 藏品: {u.collectionCount || 0}</div>
|
||||
<div>🔑 登录: {u.loginCount || 0}次 | AI: {u.aiCount || 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.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.points || 0} onChange={(e) => setEditingUser({...editingUser, 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.balance || 0} onChange={(e) => setEditingUser({...editingUser, 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.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.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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,513 +0,0 @@
|
|||
import React, { useState, useRef } from 'react'
|
||||
|
||||
// 字段名转换函数:驼峰转下划线
|
||||
const convertField = (obj) => {
|
||||
const map = {
|
||||
prefixSerial: 'prefix_serial',
|
||||
isGraded: 'is_graded',
|
||||
gradingCompany: 'grading_company',
|
||||
gradingScore: 'grading_score',
|
||||
threeStar: 'three_star',
|
||||
specialMark: 'special_mark',
|
||||
serialFeature: 'serial_feature',
|
||||
issueYear: 'issue_year',
|
||||
issueQuantity: 'issue_quantity',
|
||||
costPrice: 'cost_price',
|
||||
targetPrice: 'target_price',
|
||||
goalPrice: 'goal_price',
|
||||
repairFee: 'repair_fee',
|
||||
gradingFee: 'grading_fee'
|
||||
}
|
||||
const result = {}
|
||||
for (const key in obj) {
|
||||
result[map[key] || key] = obj[key]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export default function BatchMode() {
|
||||
const [images, setImages] = useState([])
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [result, setResult] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [savedCount, setSavedCount] = useState(0)
|
||||
const fileInputRef = useRef(null)
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'in_collection', label: '收藏中' },
|
||||
{ value: 'selling', label: '在售' },
|
||||
{ value: 'sold', label: '已售' },
|
||||
{ value: 'grading', label: '送评' },
|
||||
{ value: 'transit', label: '在途' },
|
||||
{ value: 'other', label: '其他' }
|
||||
]
|
||||
|
||||
const packagingOptions = [
|
||||
{ value: '单张', label: '单张' },
|
||||
{ value: '标十', label: '标十' },
|
||||
{ value: '标百', label: '标百' },
|
||||
{ value: '裸钞', label: '裸钞' }
|
||||
]
|
||||
|
||||
const categoryOptions = [
|
||||
{ value: '自持', label: '自持' },
|
||||
{ value: '寄存', label: '寄存' },
|
||||
{ value: '寄售', label: '寄售' },
|
||||
{ value: '共有', label: '共有' },
|
||||
{ value: '寻号', label: '寻号' },
|
||||
{ value: '其他', label: '其他' }
|
||||
]
|
||||
|
||||
const rarityOptions = [
|
||||
{ value: '通货', label: '通货' },
|
||||
{ value: '特色', label: '特色' },
|
||||
{ value: '少见', label: '少见' },
|
||||
{ value: '稀有', label: '稀有' },
|
||||
{ value: '珍品', label: '珍品' },
|
||||
{ value: '孤品', label: '孤品' }
|
||||
]
|
||||
|
||||
const handleChooseImages = (e) => {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (files.length > 0) {
|
||||
setImages(files.slice(0, 10))
|
||||
setCurrentIndex(0)
|
||||
setResult(null)
|
||||
setSavedCount(0)
|
||||
// Auto OCR first image
|
||||
processOCR(files[0])
|
||||
}
|
||||
}
|
||||
|
||||
const processOCR = async (file) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('image', file)
|
||||
|
||||
const res = await fetch('/api/ocr/recognize', { method: 'POST', body: formData })
|
||||
const data = await res.json()
|
||||
const text = data.result || ''
|
||||
|
||||
const parsed = {
|
||||
name: '生肖纪念钞',
|
||||
ownership_type: '自持',
|
||||
rarity: '通货',
|
||||
status: 'in_collection',
|
||||
version: '',
|
||||
prefixSerial: '',
|
||||
packaging: '单张',
|
||||
isGraded: false,
|
||||
gradingCompany: '',
|
||||
gradingScore: '',
|
||||
threeStar: false,
|
||||
specialMark: '',
|
||||
serialFeature: '',
|
||||
denomination: '贰拾圆',
|
||||
issuer: '中国人民银行',
|
||||
issueYear: '2024',
|
||||
material: '塑料',
|
||||
issueQuantity: '一亿',
|
||||
costPrice: '',
|
||||
goalPrice: '',
|
||||
targetPrice: '',
|
||||
remark: ''
|
||||
}
|
||||
|
||||
const v = text.match(/发行版别[::]\s*([^\n]+)/)
|
||||
if (v) {
|
||||
parsed.version = v[1].trim()
|
||||
const yearMatch = v[1].match(/20\d{2}/)
|
||||
if (yearMatch) parsed.issueYear = yearMatch[0]
|
||||
}
|
||||
// 自动识别发行机构
|
||||
const issuerMatch = text.match(/发行机构[::]\s*([^\n]+)/)
|
||||
if (issuerMatch) parsed.issuer = issuerMatch[1].trim()
|
||||
if (text.includes('三星') || text.includes('3星') || text.includes('★★★')) parsed.threeStar = true
|
||||
const d = text.match(/面额[::]\s*([^\n]+)/)
|
||||
if (d) parsed.denomination = d[1].trim()
|
||||
const p = text.match(/冠字序号[::]\s*([^\n]+)/)
|
||||
if (p) parsed.prefixSerial = p[1].trim()
|
||||
// 识别编号
|
||||
const codeMatch = text.match(/编号[::]\s*([^\n]+)/)
|
||||
if (codeMatch) parsed.code = codeMatch[1].trim()
|
||||
const pk = text.match(/封装类型[::]\s*([^\n]+)/)
|
||||
if (pk) {
|
||||
const pv = pk[1].trim()
|
||||
if (pv.includes('百')) parsed.packaging = '标百'
|
||||
else if (pv.includes('十')) parsed.packaging = '标十'
|
||||
else if (pv.includes('单')) parsed.packaging = '单张'
|
||||
else parsed.packaging = '裸钞'
|
||||
}
|
||||
const g = text.match(/是否评级[::]\s*([^\n]+)/)
|
||||
if (g) parsed.isGraded = g[1].trim() === '是'
|
||||
const gc = text.match(/评级机构[::]\s*([^\n]+)/)
|
||||
if (gc) parsed.gradingCompany = gc[1].trim()
|
||||
const gs = text.match(/评级分数[::]\s*([^\n]+)/)
|
||||
if (gs) parsed.gradingScore = gs[1].trim()
|
||||
const sm = text.match(/特殊标识[::]\s*([^\n]+)/)
|
||||
if (sm) parsed.specialMark = sm[1].trim()
|
||||
const sf = text.match(/号码特征[::]\s*([^\n]+)/)
|
||||
if (sf) parsed.serialFeature = sf[1].trim()
|
||||
|
||||
setResult(parsed)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
alert('识别失败,请重试')
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const updateResult = (field, value) => {
|
||||
setResult(prev => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
const handleSave = async (force = false) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
alert('请先登录')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const saveData = convertField({ ...result, _forceSave: force })
|
||||
const res = await fetch('/api/collections', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify(saveData)
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
setSavedCount(prev => prev + 1)
|
||||
alert('保存成功!')
|
||||
} else {
|
||||
const data = await res.json()
|
||||
const errMsg = data.error || data.message || data.detail || '保存失败'
|
||||
// 检查重复
|
||||
if (errMsg.includes('E002') || errMsg.includes('已存在') || errMsg.includes('禁止重复') || errMsg.includes('重复')) {
|
||||
const confirmed = confirm('发现重复:冠字号 ' + (result.prefixSerial || '') + ' 已存在,是否继续保存?')
|
||||
if (confirmed) {
|
||||
await handleSave(true) // 强制保存
|
||||
return
|
||||
}
|
||||
}
|
||||
alert(errMsg)
|
||||
}
|
||||
} catch (e) {
|
||||
alert('保存失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentIndex < images.length - 1) {
|
||||
setCurrentIndex(currentIndex + 1)
|
||||
setResult(null)
|
||||
processOCR(images[currentIndex + 1])
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrev = () => {
|
||||
if (currentIndex > 0) {
|
||||
setCurrentIndex(currentIndex - 1)
|
||||
setResult(null)
|
||||
processOCR(images[currentIndex - 1])
|
||||
}
|
||||
}
|
||||
|
||||
const handleReOCR = () => {
|
||||
processOCR(images[currentIndex])
|
||||
}
|
||||
|
||||
const handleReUpload = () => {
|
||||
setImages([])
|
||||
setCurrentIndex(0)
|
||||
setResult(null)
|
||||
setSavedCount(0)
|
||||
fileInputRef.current?.click()
|
||||
}
|
||||
|
||||
// No images selected
|
||||
if (images.length === 0) {
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<div
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
style={{
|
||||
minHeight: '50vh',
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
border: '2px dashed rgba(255,255,255,0.15)',
|
||||
borderRadius: '16px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '60px', opacity: 0.5 }}>📦</div>
|
||||
<div style={{ color: '#fff', fontSize: '16px', marginTop: '16px' }}>点击选择多张图片</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px', marginTop: '8px' }}>最多10张</div>
|
||||
</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={handleChooseImages}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const isSaved = savedCount > currentIndex
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px', paddingBottom: '120px' }}>
|
||||
{/* Sticky Header */}
|
||||
<div style={{ position: 'sticky', top: 0, background: '#0f172a', padding: '12px 0', marginBottom: '12px', zIndex: 10, borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
<div style={{ color: '#fff', fontSize: '14px', marginBottom: '8px' }}>
|
||||
已保存: {savedCount} / {images.length} | 当前第 {currentIndex + 1} 张
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
||||
{images.map((_, i) => (
|
||||
<div key={i} style={{
|
||||
width: '24px', height: '24px', borderRadius: '4px',
|
||||
background: i < savedCount ? '#22c55e' : (i === currentIndex ? '#fbbf24' : '#333'),
|
||||
color: '#fff', fontSize: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center'
|
||||
}}>
|
||||
{i + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{currentIndex === images.length - 1 && savedCount > 0 && (
|
||||
<button onClick={handleReUpload} style={{ marginTop: '12px', padding: '8px 16px', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '6px', fontSize: '13px', cursor: 'pointer' }}>
|
||||
重新上传
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Image */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<img
|
||||
src={URL.createObjectURL(images[currentIndex])}
|
||||
alt="preview"
|
||||
style={{ width: '100%', maxHeight: '200px', objectFit: 'contain', borderRadius: '8px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Buttons - moved here */}
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
|
||||
<button
|
||||
onClick={handlePrev}
|
||||
disabled={currentIndex === 0}
|
||||
style={{ flex: 1, padding: '12px', background: currentIndex === 0 ? '#333' : 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', cursor: currentIndex === 0 ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
上一张
|
||||
</button>
|
||||
|
||||
{currentIndex < images.length - 1 && (
|
||||
<button
|
||||
onClick={handleNext}
|
||||
style={{ flex: 1, padding: '12px', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}
|
||||
>
|
||||
下一张
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleReOCR}
|
||||
disabled={loading}
|
||||
style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '8px', cursor: loading ? 'not-allowed' : 'pointer' }}
|
||||
>
|
||||
重新识别
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!result || loading}
|
||||
style={{ flex: 1, padding: '12px', background: result && !loading ? '#22c55e' : '#333', color: '#fff', border: 'none', borderRadius: '8px', fontWeight: 'bold', cursor: result && !loading ? 'pointer' : 'not-allowed' }}
|
||||
>
|
||||
保存结果
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: '#fbbf24' }}>🤖 AI识别中...</div>
|
||||
)}
|
||||
|
||||
{result && !loading && (
|
||||
<>
|
||||
{/* 基本信息 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '12px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>基本信息</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>名称</div>
|
||||
<input type="text" value={result.name || ''} onChange={(e) => updateResult('name', 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: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>编号</div>
|
||||
<input type="text" value={result.code || ''} onChange={(e) => updateResult('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: '#fff', fontSize: '13px' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>持仓类型</div>
|
||||
<select value={result.ownership_type || '自持'} onChange={(e) => updateResult('ownership_type', 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' }}>
|
||||
{categoryOptions.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>珍惜度</div>
|
||||
<select value={result.rarity || '通货'} onChange={(e) => updateResult('rarity', 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' }}>
|
||||
{rarityOptions.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>冠字序号</div>
|
||||
<input type="text" value={result.prefixSerial || ''} onChange={(e) => updateResult('prefixSerial', 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', fontFamily: 'monospace' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>版别</div>
|
||||
<input type="text" value={result.version || ''} onChange={(e) => updateResult('version', 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: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>状态</div>
|
||||
<select value={result.status || 'in_collection'} onChange={(e) => updateResult('status', 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' }}>
|
||||
{statusOptions.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>封装</div>
|
||||
<select value={result.packaging || '单张'} onChange={(e) => updateResult('packaging', 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' }}>
|
||||
{packagingOptions.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 评级信息 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '12px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px', padding: '8px', background: result.isGraded ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255,255,255,0.03)', borderRadius: '6px', cursor: 'pointer' }}
|
||||
onClick={() => updateResult('isGraded', !result.isGraded)}>
|
||||
<div style={{ width: '22px', height: '22px', borderRadius: '4px', border: '2px solid', borderColor: result.isGraded ? '#22c55e' : '#64748b', backgroundColor: result.isGraded ? '#22c55e' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{result.isGraded && <span style={{color:'#fff',fontSize:'14px'}}>✓</span>}
|
||||
</div>
|
||||
<span style={{color:'#fff',fontSize:'13px'}}>已评级</span>
|
||||
</div>
|
||||
|
||||
{result.isGraded && (
|
||||
<>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>评级公司</div>
|
||||
<input type="text" value={result.gradingCompany || ''} onChange={(e) => updateResult('gradingCompany', 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: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>评级分数</div>
|
||||
<input type="text" value={result.gradingScore || ''} onChange={(e) => updateResult('gradingScore', 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 style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px', background: result.threeStar ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255,255,255,0.03)', borderRadius: '6px', cursor: 'pointer' }}
|
||||
onClick={() => updateResult('threeStar', !result.threeStar)}>
|
||||
<div style={{ width: '22px', height: '22px', borderRadius: '4px', border: '2px solid', borderColor: result.threeStar ? '#22c55e' : '#64748b', backgroundColor: result.threeStar ? '#22c55e' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{result.threeStar && <span style={{color:'#fff',fontSize:'14px'}}>✓</span>}
|
||||
</div>
|
||||
<span style={{color:'#fff',fontSize:'13px'}}>三星</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 特殊信息 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '12px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>特殊信息</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>发行机构</div>
|
||||
<input type="text" value={result.issuer || ''} onChange={(e) => updateResult('issuer', 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: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>年份</div>
|
||||
<input type="text" value={result.issueYear || ''} onChange={(e) => updateResult('issueYear', 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: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>特殊标识</div>
|
||||
<input type="text" value={result.specialMark || ''} onChange={(e) => updateResult('specialMark', 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: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>号码特征</div>
|
||||
<input type="text" value={result.serialFeature || ''} onChange={(e) => updateResult('serialFeature', 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: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>面额</div>
|
||||
<input type="text" value={result.denomination || ''} onChange={(e) => updateResult('denomination', 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 style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '16px' }}>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>成本价</div>
|
||||
<input type="number" value={result.costPrice || ''} onChange={(e) => updateResult('costPrice', 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: '10px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>出售价</div>
|
||||
<input type="number" value={result.targetPrice || ''} onChange={(e) => updateResult('targetPrice', 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>
|
||||
|
||||
{/* 备注 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '16px' }}>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>备注</div>
|
||||
<textarea value={result.remark || ''} onChange={(e) => updateResult('remark', e.target.value)} rows={2}
|
||||
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', resize: 'none' }} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
{savedCount > 0 && currentIndex === images.length - 1 && (
|
||||
<button onClick={handleReUpload} style={{ width: '100%', padding: '14px', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '15px', marginTop: '16px', cursor: 'pointer' }}>
|
||||
重新上传
|
||||
</button>
|
||||
)}
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={handleChooseImages}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,575 +0,0 @@
|
|||
import React, { useState, useEffect, useRef } from 'react'
|
||||
|
||||
|
||||
// 通用 Input 组件
|
||||
const Input = ({ form, handleChange, label, field, type = 'text', options = null }) => {
|
||||
const onChange = (e) => {
|
||||
const value = e.target.value
|
||||
if (type === 'number' && value !== '') {
|
||||
const num = parseFloat(value)
|
||||
handleChange(field, isNaN(num) ? '' : num)
|
||||
} else handleChange(field, value)
|
||||
}
|
||||
return (
|
||||
<div style={{ flex: 1, minWidth: '45%', marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>{label}</div>
|
||||
{options ? (
|
||||
<select value={form[field] || ''} onChange={onChange}
|
||||
style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff', padding: '8px', borderRadius: '6px', width: '100%', fontSize: '13px' }}>
|
||||
<option value="">请选择</option>
|
||||
{options.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<input type={type} value={form[field] ?? ''} onChange={onChange}
|
||||
style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff', padding: '8px', borderRadius: '6px', width: '100%', fontSize: '13px' }} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Edit() {
|
||||
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
|
||||
const id = params.get('id')
|
||||
|
||||
const [collection, setCollection] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [form, setForm] = useState({})
|
||||
const [showDelete, setShowDelete] = useState(false)
|
||||
const [collectionImages, setCollectionImages] = useState([])
|
||||
const fileInputRef = useRef(null)
|
||||
const [editingImageIndex, setEditingImageIndex] = useState(-1)
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchDetail()
|
||||
}
|
||||
}, [id])
|
||||
|
||||
// 图片上传处理
|
||||
const handleImageUpload = async (e) => {
|
||||
const files = Array.from(e.target.files)
|
||||
if (files.length === 0) return
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
const maxImages = 3
|
||||
|
||||
// 检查是否超过限制
|
||||
const currentCount = collection.images ? collection.images.length : 0
|
||||
if (editingImageIndex === -1 && currentCount + files.length > maxImages) {
|
||||
alert(`最多只能上传${maxImages}张图片`)
|
||||
return
|
||||
}
|
||||
|
||||
// 更换图片(点击已有图片)
|
||||
if (editingImageIndex >= 0) {
|
||||
const file = files[0]
|
||||
const oldImage = collection.images[editingImageIndex]
|
||||
|
||||
const imgFormData = new FormData()
|
||||
imgFormData.append('file', file)
|
||||
|
||||
try {
|
||||
// 先上传新图片
|
||||
const uploadRes = await fetch(`/api/collections/upload-image?collection_id=${id}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
body: imgFormData
|
||||
})
|
||||
|
||||
if (uploadRes.ok) {
|
||||
// 删除旧图片
|
||||
await fetch(`/api/collections/images/${oldImage.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
|
||||
// 重新加载详情
|
||||
await fetchDetail()
|
||||
alert('图片已更换')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('图片更换失败:', err)
|
||||
alert('更换失败,请重试')
|
||||
}
|
||||
|
||||
setEditingImageIndex(-1)
|
||||
return
|
||||
}
|
||||
|
||||
// 添加新图片
|
||||
for (const file of files) {
|
||||
const imgFormData = new FormData()
|
||||
imgFormData.append('file', file)
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/collections/upload-image?collection_id=${id}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
body: imgFormData
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
// 重新加载藏品详情
|
||||
await fetchDetail()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('图片上传失败:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteImage = async (imageId) => {
|
||||
if (!confirm('确定删除这张图片吗?')) return
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/collections/images/${imageId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
await fetchDetail()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('图片删除失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
setLoading(true)
|
||||
const token = localStorage.getItem('token')
|
||||
try {
|
||||
const res = await fetch(`/api/collections/${id}`, {
|
||||
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
|
||||
})
|
||||
const data = await res.json()
|
||||
const item = (data.code === 200 ? data.data : data)
|
||||
if (item && item.id) {
|
||||
setCollection(item)
|
||||
// 设置特殊信息默认值
|
||||
const yearMatch = item.version ? item.version.match(/(20\d{2})/) : null
|
||||
const defaultForm = {
|
||||
...item,
|
||||
issuer: item.issuer || '中国人民银行',
|
||||
issueYear: item.issueYear || (yearMatch ? yearMatch[1] : '2024'),
|
||||
material: item.material || '塑料钞',
|
||||
issueQuantity: item.issueQuantity || '1 亿'
|
||||
}
|
||||
setForm(defaultForm)
|
||||
// 加载图片
|
||||
if (item.images && Array.isArray(item.images)) {
|
||||
setCollectionImages(item.images)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载详情失败:', e)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
setForm({ ...form, [key]: value })
|
||||
// 填入出售价时自动把状态改为"已售"
|
||||
if (key === 'targetPrice' && value) {
|
||||
setForm(prev => ({ ...prev, status: 'sold' }))
|
||||
}
|
||||
// 版别变化时同步发行年份
|
||||
if (key === 'version') {
|
||||
const yearMatch = value.match(/(20\d{2})/)
|
||||
if (yearMatch) {
|
||||
setForm(prev => ({ ...prev, issueYear: yearMatch[1] }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setError('')
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
// 1. 先检查是否重复(同版别、同冠字号)
|
||||
if (form.prefixSerial) {
|
||||
const checkRes = await fetch(`/api/collections?search=${encodeURIComponent(form.prefixSerial)}`, {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
const checkData = await checkRes.json()
|
||||
const collections = checkData.data || checkData || []
|
||||
const duplicate = collections.find(c =>
|
||||
c.version === form.version &&
|
||||
c.prefixSerial === form.prefixSerial &&
|
||||
c.id !== id // 排除自己
|
||||
)
|
||||
if (duplicate) {
|
||||
setError('已经录入过同版同号藏品!')
|
||||
setSaving(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 转换字段名:camelCase 转字段编码
|
||||
const convertField = (obj) => {
|
||||
const map = {
|
||||
// f99 系统字段
|
||||
id: 'f99_90_id',
|
||||
userId: 'f99_91_user_id',
|
||||
createdAt: 'f99_92_created_at',
|
||||
updatedAt: 'f99_93_updated_at',
|
||||
// f01 基本信息
|
||||
name: 'f01_01_name',
|
||||
code: 'f01_02_code',
|
||||
category: 'f01_03_category',
|
||||
status: 'f01_04_status',
|
||||
remark: 'f01_05_remark',
|
||||
// f02 详细字段
|
||||
prefixSerial: 'f02_10_prefix_serial',
|
||||
version: 'f02_11_version',
|
||||
packaging: 'f02_12_packaging',
|
||||
rarity: 'f02_13_rarity',
|
||||
// f03 评级信息
|
||||
isGraded: 'f03_20_is_graded',
|
||||
gradingCompany: 'f03_21_grading_company',
|
||||
gradingScore: 'f03_22_grading_score',
|
||||
threeStar: 'f03_23_three_star',
|
||||
// f04 特殊信息
|
||||
specialMark: 'f04_30_special_mark',
|
||||
serialFeature: 'f04_31_serial_feature',
|
||||
issuer: 'f04_32_issuer',
|
||||
issueYear: 'f04_33_issue_year',
|
||||
material: 'f04_34_material',
|
||||
denomination: 'f04_35_denomination',
|
||||
issueQuantity: 'f04_36_issue_quantity',
|
||||
// f05 价格信息
|
||||
costPrice: 'f05_40_cost_price',
|
||||
targetPrice: 'f05_41_target_price',
|
||||
goalPrice: 'f05_42_goal_price',
|
||||
repairFee: 'f05_43_repair_fee',
|
||||
gradingFee: 'f05_44_grading_fee',
|
||||
// f06 其他信息
|
||||
purpose: 'f06_50_purpose'
|
||||
}
|
||||
const result = {}
|
||||
// 数字字段列表
|
||||
const numberFields = ['costPrice', 'targetPrice', 'goalPrice', 'repairFee', 'gradingFee']
|
||||
for (const key in obj) {
|
||||
let value = obj[key]
|
||||
// 过滤空字符串为 null
|
||||
if (value === '' || value === null) {
|
||||
value = null
|
||||
}
|
||||
// 数字字段转换为浮点数
|
||||
else if (numberFields.includes(key)) {
|
||||
value = parseFloat(value)
|
||||
if (isNaN(value)) value = null
|
||||
}
|
||||
result[map[key] || key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`/api/collections/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify(convertField(form))
|
||||
})
|
||||
if (res.ok) {
|
||||
alert('保存成功!')
|
||||
// 刷新首页统计数据
|
||||
if (window.refreshHome) {
|
||||
window.refreshHome()
|
||||
}
|
||||
window.location.hash = '#/detail?id=' + id
|
||||
} else {
|
||||
const data = await res.json()
|
||||
alert('保存失败: ' + (data.error || '未知错误'))
|
||||
}
|
||||
} catch (e) {
|
||||
alert('保存失败: ' + e.message)
|
||||
}
|
||||
setSaving(false)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
setShowDelete(true)
|
||||
}
|
||||
|
||||
const doDelete = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
try {
|
||||
const res = await fetch(`/api/collections/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
})
|
||||
if (res.ok) {
|
||||
alert('删除成功!')
|
||||
if (window.refreshList) window.refreshList()
|
||||
window.location.hash = '#/list'
|
||||
} else {
|
||||
alert('删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
alert('删除失败')
|
||||
}
|
||||
setShowDelete(false)
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
window.location.hash = '#/detail?id=' + id
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ value: 'in_collection', label: '收藏中' },
|
||||
{ value: 'selling', label: '出售中' },
|
||||
{ value: 'sold', label: '已售' },
|
||||
{ value: 'grading', label: '送评中' },
|
||||
{ value: 'repairing', label: '修复中' },
|
||||
{ value: 'transit', label: '在途中' },
|
||||
{ value: 'other', label: '其他' }
|
||||
]
|
||||
|
||||
const categoryOptions = [
|
||||
{ value: '自持', label: '自持' },
|
||||
{ value: '寄存', label: '寄存' },
|
||||
{ value: '寄售', label: '寄售' },
|
||||
{ value: '共有', label: '共有' },
|
||||
{ value: '寻号', label: '寻号' },
|
||||
{ value: '其他', label: '其他' }
|
||||
]
|
||||
|
||||
const rarityOptions = [
|
||||
{ value: '通货', label: '通货' },
|
||||
{ value: '特色', label: '特色' },
|
||||
{ value: '少见', label: '少见' },
|
||||
{ value: '稀有', label: '稀有' },
|
||||
{ value: '珍品', label: '珍品' },
|
||||
{ value: '孤品', label: '孤品' }
|
||||
]
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ padding: '40px', textAlign: 'center', color: '#94a3b8' }}>加载中...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ background: '#0f172a', minHeight: '100vh', overflowY: 'auto', paddingBottom: '120px' }}>
|
||||
{/* 顶部 */}
|
||||
<div style={{ padding: '16px', background: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', position: 'sticky', top: 0, zIndex: 100 }}>
|
||||
<button onClick={goBack} style={{ background: 'none', border: 'none', fontSize: '20px', cursor: 'pointer', padding: '8px', color: '#fff' }}>←</button>
|
||||
<span style={{ fontSize: '18px', fontWeight: 'bold', marginLeft: '8px', color: '#fff' }}>编辑藏品</span>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '16px' }}>
|
||||
{/* 图片预览区域 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '13px', fontWeight: 'bold' }}>藏品图片</div>
|
||||
{collectionImages.length > 0 && (
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: 'rgba(34, 197, 94, 0.2)',
|
||||
color: '#22c55e',
|
||||
border: '1px solid #22c55e',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
>
|
||||
📷 更换图片
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageUpload}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{collectionImages.length > 0 ? (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '12px' }}>
|
||||
{collectionImages.map((img, index) => (
|
||||
<div key={img.id || index} style={{ position: 'relative' }}>
|
||||
<img
|
||||
src={`http://120.26.133.10:3000/${img.path || `uploads/collections/${img.filename}`}`}
|
||||
alt={img.original_name || img.filename || '藏品图片'}
|
||||
style={{
|
||||
width: '100%',
|
||||
aspectRatio: '1.5',
|
||||
objectFit: 'contain',
|
||||
borderRadius: '12px',
|
||||
border: '2px solid #10b981',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
|
||||
background: 'rgba(0,0,0,0.3)'
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '8px',
|
||||
left: '8px',
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
color: '#fff',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}}>{index + 1}/{collectionImages.length}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
style={{
|
||||
aspectRatio: '1.5',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
border: '2px dashed #fbbf24',
|
||||
borderRadius: '12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
color: '#fbbf24',
|
||||
fontSize: '12px',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '48px', marginBottom: '8px' }}>📷</div>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>点击上传图片</div>
|
||||
<div style={{ fontSize: '11px', color: '#94a3b8' }}>
|
||||
最多可上传 1 张
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '13px', fontWeight: 'bold', marginBottom: '12px' }}>基本信息</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
|
||||
<Input form={form} handleChange={handleChange} label="名称 *" field="name" />
|
||||
<Input form={form} handleChange={handleChange} label="持仓类型" field="category" options={categoryOptions} />
|
||||
<Input form={form} handleChange={handleChange} label="珍惜度" field="rarity" options={rarityOptions} />
|
||||
<Input form={form} handleChange={handleChange} label="冠字序号" field="prefixSerial" />
|
||||
<Input form={form} handleChange={handleChange} label="版别" field="version" />
|
||||
<Input form={form} handleChange={handleChange} label="面值" field="denomination" />
|
||||
<Input form={form} handleChange={handleChange} label="状态" field="status" options={statusOptions} />
|
||||
<Input form={form} handleChange={handleChange} label="包装" field="packaging" options={packagingOptions} />
|
||||
</div>
|
||||
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<div style={{ display: 'flex', gap: '24px', flexWrap: 'wrap' }}>
|
||||
<div style={{ flex: 1, minWidth: '45%' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>编号</div>
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', color: '#fbbf24', padding: '8px', borderRadius: '6px', fontSize: '13px', fontFamily: 'monospace', fontWeight: 'bold' }}>
|
||||
{form.code || '(保存后自动生成)'}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: '45%' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>创建时间</div>
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', color: '#94a3b8', padding: '8px', borderRadius: '6px', fontSize: '13px' }}>
|
||||
{form.createdAt ? new Date(form.createdAt).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-') : '(保存后自动生成)'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 评级信息 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '13px', fontWeight: 'bold', marginBottom: '12px' }}>评级信息</div>
|
||||
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isGraded"
|
||||
checked={form.isGraded || false}
|
||||
onChange={(e) => handleChange('isGraded', e.target.checked)}
|
||||
style={{ width: '22px', height: '22px', cursor: 'pointer', backgroundColor: form.isGraded ? '#22c55e' : 'rgba(255,255,255,0.1)', border: '2px solid #22c55e', borderRadius: '4px', appearance: 'none', WebkitAppearance: 'none' }}
|
||||
/>
|
||||
<label htmlFor="isGraded" style={{ color: '#fff', cursor: 'pointer', fontSize: '15px' }}>是否评级</label>
|
||||
</div>
|
||||
<Input form={form} handleChange={handleChange} label="评级公司" field="gradingCompany" />
|
||||
<Input form={form} handleChange={handleChange} label="评级分数" field="gradingScore" />
|
||||
<Input form={form} handleChange={handleChange} label="特殊标识" field="specialMark" />
|
||||
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="threeStar"
|
||||
checked={form.threeStar || false}
|
||||
onChange={(e) => handleChange('threeStar', e.target.checked)}
|
||||
style={{ width: '22px', height: '22px', cursor: 'pointer', backgroundColor: form.threeStar ? '#22c55e' : 'rgba(255,255,255,0.1)', border: '2px solid #22c55e', borderRadius: '4px', appearance: 'none', WebkitAppearance: 'none' }}
|
||||
/>
|
||||
<label htmlFor="threeStar" style={{ color: '#fff', cursor: 'pointer', fontSize: '15px' }}>三星</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 特殊信息 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '13px', fontWeight: 'bold', marginBottom: '12px' }}>特殊信息</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
|
||||
<Input form={form} handleChange={handleChange} label="号码特征" field="serialFeature" />
|
||||
<Input form={form} handleChange={handleChange} label="发行方" field="issuer" />
|
||||
<Input form={form} handleChange={handleChange} label="发行年份" field="issueYear" />
|
||||
<Input form={form} handleChange={handleChange} label="材质" field="material" />
|
||||
<Input form={form} handleChange={handleChange} label="发行量" field="issueQuantity" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 价格信息 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '13px', fontWeight: 'bold', marginBottom: '12px' }}>价格信息</div>
|
||||
<Input form={form} handleChange={handleChange} label="成本价" field="costPrice" type="number" />
|
||||
<Input form={form} handleChange={handleChange} label="目标价" field="targetPrice" type="number" />
|
||||
<Input form={form} handleChange={handleChange} label="出售价" field="goalPrice" type="number" />
|
||||
<Input form={form} handleChange={handleChange} label="修复费" field="repairFee" type="number" />
|
||||
<Input form={form} handleChange={handleChange} label="评级费" field="gradingFee" type="number" />
|
||||
<Input form={form} handleChange={handleChange} label="用途" field="purpose" />
|
||||
</div>
|
||||
|
||||
{/* 备注 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>备注</div>
|
||||
<textarea
|
||||
value={form.remark || ''}
|
||||
onChange={(e) => handleChange('remark', e.target.value)}
|
||||
rows={3}
|
||||
style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff', padding: '8px', borderRadius: '8px', width: '100%', resize: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 按钮 */}
|
||||
<button onClick={saving ? null : handleSave} disabled={saving} style={{ width: '100%', padding: '14px', background: saving ? '#64748b' : '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '10px', fontSize: '16px', fontWeight: '600', marginBottom: '12px', cursor: saving ? 'not-allowed' : 'pointer' }}>
|
||||
{saving ? '保存中...' : '保存'}
|
||||
</button>
|
||||
|
||||
<button onClick={handleDelete} style={{ width: '100%', padding: '14px', background: 'rgba(239,68,68,0.1)', color: '#ef4444', border: '1px solid #ef4444', borderRadius: '10px', fontSize: '16px', cursor: 'pointer' }}>
|
||||
删除藏品
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 删除确认弹窗 */}
|
||||
{showDelete && (
|
||||
<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: '80%', maxWidth: '300px' }}>
|
||||
<div style={{ color: '#fff', fontSize: '16px', fontWeight: 'bold', marginBottom: '16px', textAlign: 'center' }}>确定删除此藏品?</div>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button onClick={() => setShowDelete(false)} style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '13px' }}>取消</button>
|
||||
<button onClick={doDelete} style={{ flex: 1, padding: '12px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '13px' }}>删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,554 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
// 资讯页面 - 展示寻配号和行情信息(所有人可见)
|
||||
export default function News() {
|
||||
const [activeTab, setActiveTab] = useState('deal')
|
||||
const [showSeekPublish, setShowSeekPublish] = useState(false)
|
||||
const [showMySeeks, setShowMySeeks] = useState(false) // 我的寻号
|
||||
const [showMatchList, setShowMatchList] = useState(false)
|
||||
const [matchCollections, setMatchCollections] = useState([])
|
||||
const [seekForm, setSeekForm] = useState({
|
||||
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: ''
|
||||
})
|
||||
const [infoList, setInfoList] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||
|
||||
// 获取资讯列表
|
||||
useEffect(() => {
|
||||
fetchInfoList()
|
||||
}, [activeTab])
|
||||
|
||||
// 自动生成寻号标题
|
||||
useEffect(() => {
|
||||
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} ${seekForm.type || '不限'} J0${seekForm.features || 'XXXXXXXX'} ${seekForm.matchType}」`
|
||||
setSeekForm(prev => ({...prev, title}))
|
||||
}, [seekForm.edition, seekForm.type, seekForm.features, seekForm.matchType])
|
||||
|
||||
const fetchInfoList = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 寻配号对所有人公开,无需登录即可查看
|
||||
const token = localStorage.getItem('token')
|
||||
// 根据tab获取不同类型的数据
|
||||
const type = activeTab === 'seek' ? 'seek' : 'deal'
|
||||
// 始终传递token,以便获取准确的matched_count
|
||||
const headers = token ? { Authorization: `Bearer ${token}` } : {}
|
||||
const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}`, { headers })
|
||||
const data = await res.json()
|
||||
console.log('资讯列表:', data)
|
||||
setInfoList(data || [])
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// 获取匹配藏品列表
|
||||
const fetchMatchCollections = async (infoId) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) { alert('请先登录'); return }
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/information/seek/match?info_id=${infoId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
const data = await res.json()
|
||||
console.log('匹配结果:', data)
|
||||
console.log('匹配数量:', data.matched_count)
|
||||
console.log('藏品列表:', data.collections)
|
||||
setMatchCollections(data.collections || [])
|
||||
setShowMatchList(true)
|
||||
} catch (e) {
|
||||
console.error('获取匹配藏品失败:', e)
|
||||
alert('获取匹配藏品失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取我的寻号列表
|
||||
const [mySeekList, setMySeekList] = useState([])
|
||||
const [editingSeek, setEditingSeek] = useState(null)
|
||||
const fetchMySeeks = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) { alert('请先登录'); return }
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/information/my/list`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
const data = await res.json()
|
||||
// 过滤出寻号类型的
|
||||
const seeks = Array.isArray(data) ? data.filter(item => item.info_type === 'seek') : []
|
||||
setMySeekList(seeks)
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
// 更新寻号
|
||||
const handleUpdateSeek = async () => {
|
||||
if (!editingSeek) return
|
||||
const token = localStorage.getItem('token')
|
||||
try {
|
||||
// 解析正文中的号码特征和联系方式
|
||||
const content = (editingSeek.content || '') + '\n联系方式: ' + editingSeek.contact
|
||||
await fetch(`${API_BASE}/api/information/${editingSeek.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: editingSeek.title,
|
||||
content,
|
||||
expect_category: editingSeek.edition,
|
||||
expect_number: editingSeek.features ? 'J0' + editingSeek.features : null
|
||||
})
|
||||
})
|
||||
setEditingSeek(null)
|
||||
fetchMySeeks()
|
||||
} catch (e) { alert('更新失败') }
|
||||
}
|
||||
|
||||
const handleSeekPublish = async () => {
|
||||
if (!seekForm.contact) { alert('请填写联系方式'); return }
|
||||
// 类型改为非必选
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + seekForm.contact
|
||||
// 从edition映射到category
|
||||
const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' }
|
||||
const res = await fetch(`${API_BASE}/api/information/`, {
|
||||
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: seekForm.title,
|
||||
content,
|
||||
info_type: 'seek',
|
||||
expect_category: seekForm.edition,
|
||||
expect_number: seekForm.features ? 'J0' + seekForm.features : None
|
||||
})
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.id || data.code === 0) {
|
||||
alert('发布成功!')
|
||||
setShowSeekPublish(false)
|
||||
setSeekForm({ edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: '' })
|
||||
fetchInfoList()
|
||||
} else { alert(data.message || '发布失败') }
|
||||
} catch (e) { alert('发布失败: ' + e.message) }
|
||||
}
|
||||
|
||||
// Tab切换
|
||||
const tabs = [
|
||||
{ key: 'seek', label: '🔍 寻配号' },
|
||||
{ key: 'deal', label: '💰 成交行情' }
|
||||
]
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: '#0f172a', paddingBottom: '60px' }}>
|
||||
{/* Tab导航 */}
|
||||
<div style={{ display: 'flex', padding: '12px 16px', background: '#1e293b', gap: '8px', overflowX: 'auto' }}>
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
borderRadius: '8px',
|
||||
background: activeTab === tab.key ? '#3b82f6' : 'transparent',
|
||||
color: '#fff',
|
||||
cursor: 'pointer',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 资讯列表 */}
|
||||
<div style={{ padding: '16px' }}>
|
||||
{showSeekPublish && activeTab === 'seek' && (
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', marginBottom: '16px' }}>
|
||||
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>版别</label>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
|
||||
{['龙钞','马钞','蛇钞'].map(item => {
|
||||
const isSelected = seekForm.edition ? seekForm.edition.split(',').includes(item) : false
|
||||
return (
|
||||
<button key={item} onClick={() => {
|
||||
const eds = seekForm.edition ? seekForm.edition.split(',').filter(x => x) : []
|
||||
if (isSelected) {
|
||||
const idx = eds.indexOf(item)
|
||||
if (idx > -1) eds.splice(idx, 1)
|
||||
} else {
|
||||
eds.push(item)
|
||||
}
|
||||
setSeekForm({...seekForm, edition: eds.join(',')})
|
||||
}}
|
||||
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: isSelected?'2px solid #10b981':'1px solid #334155', background: isSelected?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{item}</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>类型</label>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
|
||||
{['标百','标十','单张'].map(e => (
|
||||
<button key={e} onClick={() => setSeekForm({...seekForm, type: e})}
|
||||
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.type===e?'2px solid #10b981':'1px solid #334155', background: seekForm.type===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>分类</label>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
|
||||
{['求购','出售','寻号'].map(e => (
|
||||
<button key={e} onClick={() => setSeekForm({...seekForm, category: e})}
|
||||
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.category===e?'2px solid #10b981':'1px solid #334155', background: seekForm.category===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>价格(选填)</label>
|
||||
<input type="text" value={seekForm.price} onChange={(e) => setSeekForm({...seekForm, price: e.target.value})} placeholder="请输入价格"
|
||||
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>匹配度</label>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '6px' }}>
|
||||
{['精准','模糊'].map(e => (
|
||||
<button key={e} onClick={() => setSeekForm({...seekForm, matchType: e})}
|
||||
style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.matchType===e?'2px solid #10b981':'1px solid #334155', background: seekForm.matchType===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>号码特征(10位)</label>
|
||||
<div style={{ display: 'flex', gap: '4px', marginTop: '6px', flexWrap: 'wrap' }}>
|
||||
{[0,1,2,3,4,5,6,7,8,9].map(i => {
|
||||
const isFixed = i < 2
|
||||
const char = i === 0 ? 'J' : (i === 1 ? '0' : (seekForm.features[i - 2] || ''))
|
||||
const isDigit = /[0-9]/.test(char)
|
||||
const letterColor = isFixed ? '#10b981' : (isDigit ? '#fff' : ({ X: '#94a3b8', A: '#f472b6', B: '#60a5fa', C: '#34d399', D: '#fbbf24', E: '#a78bfa', F: '#fb923c', G: '#22d3ee' }[char] || '#fff'))
|
||||
return <input key={i} ref={el => { if (el) el.dataset.index = i }} type="text" maxLength={1}
|
||||
value={char}
|
||||
readOnly={isFixed}
|
||||
onChange={(e) => {
|
||||
if (i < 2) return
|
||||
const val = e.target.value.toUpperCase().replace(/[^0-9XABCFG]/g, '')
|
||||
const newFeatures = (seekForm.features || '').split('')
|
||||
while (newFeatures.length < 8) newFeatures.push('')
|
||||
newFeatures[i - 2] = val
|
||||
setSeekForm({...seekForm, features: newFeatures.join('')})
|
||||
// 自动跳转下一个
|
||||
if (val && i < 9) {
|
||||
setTimeout(() => {
|
||||
const nextInput = document.querySelector(`input[data-index="${i+1}"]`)
|
||||
if (nextInput) nextInput.focus()
|
||||
}, 50)
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// 按Delete或Backspace且当前为空时,跳回前一个
|
||||
if ((e.key === 'Backspace' || e.key === 'Delete') && !char && i > 2) {
|
||||
setTimeout(() => {
|
||||
const prevInput = document.querySelector(`input[data-index="${i-1}"]`)
|
||||
if (prevInput) prevInput.focus()
|
||||
}, 50)
|
||||
}
|
||||
}}
|
||||
style={{ width: '32px', height: '36px', textAlign: 'center', padding: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '4px', color: letterColor, fontSize: '14px', boxSizing: 'border-box' }} />
|
||||
})}
|
||||
</div>
|
||||
<div style={{ color: '#64748b', fontSize: '10px', marginTop: '6px' }}>
|
||||
备注说明:X=任意数字 A=非4 B=非47 C=非347 D=非247 E=非2347 F=非23457 G=非123457
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>正文</label>
|
||||
<textarea value={seekForm.content || ''} onChange={(e) => setSeekForm({...seekForm, content: e.target.value})} placeholder="请输入详细信息..."
|
||||
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box', resize: 'vertical', minHeight: '60px' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: '12px' }}><label style={{ color: '#ef4444', fontSize: '12px' }}>联系方式 *</label>
|
||||
<input type="text" value={seekForm.contact || ''} onChange={(e) => setSeekForm({...seekForm, contact: e.target.value})} placeholder="请输入手机号或微信"
|
||||
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: '12px' }}><label style={{ color: '#94a3b8', fontSize: '12px' }}>标题(自动生成)</label>
|
||||
<input type="text" value={seekForm.title} readOnly
|
||||
style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} onChange={(e) => setSeekForm({...seekForm, title: e.target.value})} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button onClick={handleSeekPublish} style={{ flex: 1, padding: '12px', background: '#10b981', border: 'none', borderRadius: '8px', color: '#fff', fontSize: '14px', fontWeight: 'bold', cursor: 'pointer' }}>发布寻号</button>
|
||||
<button onClick={() => setShowSeekPublish(false)} style={{ flex: 1, padding: '12px', background: 'transparent', border: '1px solid #334155', borderRadius: '8px', color: '#94a3b8', fontSize: '14px', cursor: 'pointer' }}>取消</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<h3 style={{ color: '#fff', marginBottom: '16px' }}>
|
||||
{activeTab === 'seek' ? '🔍 寻配号信息' : '💰 成交行情信息'}
|
||||
{activeTab === 'seek' && (
|
||||
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
|
||||
<button onClick={() => setShowSeekPublish(!showSeekPublish)} style={{ padding: '6px 12px', background: showSeekPublish ? '#6b7280' : '#10b981', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>
|
||||
'+ 发布寻号'
|
||||
</button>
|
||||
<button onClick={() => { setShowMySeeks(true); fetchMySeeks(); }} style={{ padding: '6px 12px', background: '#3b82f6', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>
|
||||
我的寻号
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</h3>
|
||||
|
||||
{loading ? (
|
||||
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>加载中...</div>
|
||||
) : infoList.length === 0 ? (
|
||||
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>
|
||||
暂无{activeTab === 'seek' ? '寻配号' : '成交行情'}信息
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{infoList.map(item => {
|
||||
// 解析正文中的号码特征和联系方式
|
||||
const content = item.content || ''
|
||||
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
|
||||
const contactMatch = content.match(/联系方式[:\s]*(.+?)(?:\n|$)/)
|
||||
const features = featuresMatch ? featuresMatch[1].trim() : ''
|
||||
const contact = contactMatch ? contactMatch[1].trim() : ''
|
||||
// 去除正文中的价格、号码特征、联系方式
|
||||
const cleanContent = content.replace(/价格[:\s]*.+?(\n|$)/g, '').replace(/号码特征[:\s]*.+?(\n|$)/g, '').replace(/联系方式[:\s]*.+?(\n|$)/g, '').trim()
|
||||
|
||||
return (
|
||||
<div key={item.id} style={{ background: '#1e293b', borderRadius: '12px', padding: '16px' }}>
|
||||
{/* 标题 */}
|
||||
<div style={{ color: '#fbbf24', fontSize: '15px', fontWeight: '600', marginBottom: '8px' }}>
|
||||
{item.title}
|
||||
</div>
|
||||
{/* 创建日期 + 用户名 */}
|
||||
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}>
|
||||
📅 {formatDate(item.created_at)} | 👤 {item.user_name || '匿名用户'}
|
||||
</div>
|
||||
{/* 号码特征 */}
|
||||
{features && (
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: '13px' }}>号码特征:</span>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '14px' }}>
|
||||
{/* 如果features已包含J0则不再重复添加 */}
|
||||
{features.startsWith('J0') ? (
|
||||
features.split('').map((char, i) => {
|
||||
const isDigit = /[0-9]/.test(char)
|
||||
const letterColor = isDigit ? '#fff' : ({ X: '#94a3b8', A: '#f472b6', B: '#60a5fa', C: '#34d399', D: '#fbbf24', E: '#a78bfa', F: '#fb923c', G: '#22d3ee' }[char] || '#fff')
|
||||
return <span key={i} style={{ color: i < 2 ? '#10b981' : letterColor }}>{char}</span>
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
{'J0'.split('').map((char, i) => (
|
||||
<span key={i} style={{ color: '#10b981' }}>{char}</span>
|
||||
))}
|
||||
{features.split('').map((char, i) => {
|
||||
const isDigit = /[0-9]/.test(char)
|
||||
const letterColor = isDigit ? '#fff' : ({ X: '#94a3b8', A: '#f472b6', B: '#60a5fa', C: '#34d399', D: '#fbbf24', E: '#a78bfa', F: '#fb923c', G: '#22d3ee' }[char] || '#fff')
|
||||
return <span key={i + 2} style={{ color: letterColor }}>{char}</span>
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
{/* 动态显示用到的字母说明 */}
|
||||
{features.match(/[XABCFG]/) && (
|
||||
<div style={{ color: '#64748b', fontSize: '10px', marginTop: '4px' }}>
|
||||
备注说明:{(() => {
|
||||
const used = []
|
||||
if (features.includes('X')) used.push('X=任意数字')
|
||||
if (features.includes('A')) used.push('A=非4')
|
||||
if (features.includes('B')) used.push('B=非47')
|
||||
if (features.includes('C')) used.push('C=非347')
|
||||
if (features.includes('D')) used.push('D=非247')
|
||||
if (features.includes('E')) used.push('E=非2347')
|
||||
if (features.includes('F')) used.push('F=非23457')
|
||||
if (features.includes('G')) used.push('G=非123457')
|
||||
return used.join(' | ')
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 配号结果 - 仅寻配号显示 */}
|
||||
{activeTab === 'seek' && (
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<span
|
||||
style={{ color: '#10b981', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer' }}
|
||||
onClick={() => fetchMatchCollections(item.id)}
|
||||
>
|
||||
配号结果: {item.matched_count || 0}条藏品匹配成功
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{/* 正文 */}
|
||||
{cleanContent && (
|
||||
<div style={{ color: '#e2e8f0', fontSize: '14px', lineHeight: '1.6', marginBottom: '8px' }}>
|
||||
{cleanContent}
|
||||
</div>
|
||||
)}
|
||||
{/* 联系方式 */}
|
||||
{contact && (
|
||||
<div style={{ color: '#f97316', fontSize: '13px', padding: '8px', background: 'rgba(249,115,22,0.1)', borderRadius: '6px' }}>
|
||||
联系方式:{contact}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 匹配藏品列表弹窗 */}
|
||||
{showMatchList && (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxHeight: '80vh', overflow: 'auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h3 style={{ color: '#fff', margin: 0 }}>匹配藏品清单</h3>
|
||||
<button onClick={() => setShowMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
{matchCollections.length === 0 ? (
|
||||
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '20px' }}>暂无匹配藏品</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{matchCollections.map(c => (
|
||||
<div
|
||||
key={c.id}
|
||||
style={{ background: '#0f172a', borderRadius: '8px', padding: '12px', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setShowMatchList(false)
|
||||
// 跳转到藏品列表页并定位到该藏品
|
||||
window.location.hash = `#/list?filter=id&value=${c.id}`
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#e2e8f0', fontSize: '13px' }}>
|
||||
<span style={{ color: '#94a3b8' }}>编号: </span>{c.code || c.id.substring(0,8)}
|
||||
</div>
|
||||
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
|
||||
<span style={{ color: '#94a3b8' }}>冠字号: </span>{c.number}
|
||||
</div>
|
||||
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
|
||||
<span style={{ color: '#94a3b8' }}>状态: </span>{c.status === 'in_collection' ? '持仓中' : c.status}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 我的寻号弹窗 */}
|
||||
{showMySeeks && (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxHeight: '80vh', overflow: 'auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h3 style={{ color: '#fff', margin: 0 }}>我发布的寻号</h3>
|
||||
<button onClick={() => setShowMySeeks(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
{mySeekList.length === 0 ? (
|
||||
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '20px' }}>暂无发布的寻号</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{mySeekList.map(item => (
|
||||
<div key={item.id} style={{ background: '#0f172a', borderRadius: '8px', padding: '12px' }}>
|
||||
{editingSeek && editingSeek.id === item.id ? (
|
||||
// 编辑模式
|
||||
<div>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<label style={{ color: '#94a3b8', fontSize: '12px' }}>版别</label>
|
||||
<select
|
||||
value={editingSeek.edition || '龙钞'}
|
||||
onChange={(e) => setEditingSeek({...editingSeek, edition: e.target.value})}
|
||||
style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', marginTop: '4px' }}
|
||||
>
|
||||
<option value="龙钞">龙钞</option>
|
||||
<option value="马钞">马钞</option>
|
||||
<option value="蛇钞">蛇钞</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<label style={{ color: '#94a3b8', fontSize: '12px' }}>类型</label>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '4px' }}>
|
||||
{['标百', '标十', '单张'].map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setEditingSeek({...editingSeek, type: t})}
|
||||
style={{ flex: 1, padding: '8px', background: editingSeek.type === t ? '#10b981' : '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', cursor: 'pointer' }}
|
||||
>{t}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<label style={{ color: '#94a3b8', fontSize: '12px' }}>匹配度</label>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '4px' }}>
|
||||
<button
|
||||
onClick={() => setEditingSeek({...editingSeek, matchType: '精准'})}
|
||||
style={{ flex: 1, padding: '8px', background: editingSeek.matchType === '精准' ? '#10b981' : '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', cursor: 'pointer' }}
|
||||
>精准</button>
|
||||
<button
|
||||
onClick={() => setEditingSeek({...editingSeek, matchType: '模糊'})}
|
||||
style={{ flex: 1, padding: '8px', background: editingSeek.matchType === '模糊' ? '#10b981' : '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', cursor: 'pointer' }}
|
||||
>模糊</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<label style={{ color: '#94a3b8', fontSize: '12px' }}>价格(选填)</label>
|
||||
<input
|
||||
value={editingSeek.price || ''}
|
||||
onChange={(e) => setEditingSeek({...editingSeek, price: e.target.value})}
|
||||
placeholder="期望价格"
|
||||
style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', marginTop: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<label style={{ color: '#94a3b8', fontSize: '12px' }}>正文</label>
|
||||
<textarea
|
||||
value={editingSeek.content || ''}
|
||||
onChange={(e) => setEditingSeek({...editingSeek, content: e.target.value})}
|
||||
style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', minHeight: '60px', marginTop: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<label style={{ color: '#94a3b8', fontSize: '12px' }}>联系方式(必填)</label>
|
||||
<input
|
||||
value={editingSeek.contact || ''}
|
||||
onChange={(e) => setEditingSeek({...editingSeek, contact: e.target.value})}
|
||||
placeholder="手机号或微信"
|
||||
style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', marginTop: '4px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button onClick={handleUpdateSeek} style={{ flex: 1, padding: '10px', background: '#10b981', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>保存</button>
|
||||
<button onClick={() => setEditingSeek(null)} style={{ flex: 1, padding: '10px', background: '#6b7280', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>取消</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// 显示模式
|
||||
<div>
|
||||
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: '500', marginBottom: '4px' }}>{item.title}</div>
|
||||
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '4px' }}>创建于: {new Date(item.created_at).toLocaleDateString()}</div>
|
||||
<div style={{ color: '#10b981', fontSize: '12px', marginBottom: '8px' }}>配号结果: {item.matched_count || 0}条藏品匹配成功</div>
|
||||
<button onClick={() => {
|
||||
// 解析内容提取字段
|
||||
const content = item.content || ''
|
||||
const contactMatch = content.match(/联系方式[:\s]*(.+?)$/m)
|
||||
const priceMatch = content.match(/价格[:\s]*(.+?)$/m)
|
||||
setEditingSeek({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
content: content.split('\n联系方式:')[0],
|
||||
edition: item.expect_category || '龙钞',
|
||||
type: item.title.match(/标百|标十|单张/)?.[0] || '标百',
|
||||
matchType: item.title.includes('精准') ? '精准' : '模糊',
|
||||
price: priceMatch ? priceMatch[1].trim() : '',
|
||||
contact: contactMatch ? contactMatch[1].trim() : ''
|
||||
})
|
||||
}} style={{ padding: '4px 8px', background: '#3b82f6', border: 'none', borderRadius: '4px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>编辑</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
// 资讯页面 - 展示寻配号和行情信息(所有人可见)
|
||||
export default function News() {
|
||||
const [activeTab, setActiveTab] = useState('deal')
|
||||
const [showSeekPublish, setShowSeekPublish] = useState(false)
|
||||
const [showMySeeks, setShowMySeeks] = useState(false)
|
||||
const [showMatchList, setShowMatchList] = useState(false)
|
||||
const [matchCollections, setMatchCollections] = useState([])
|
||||
const [listFilter, setListFilter] = useState('')
|
||||
const [currentUserId, setCurrentUserId] = useState('')
|
||||
const [matchedItems, setMatchedItems] = useState(() => {
|
||||
try { return JSON.parse(localStorage.getItem('matchedSeekItems') || '{}') } catch { return {} }
|
||||
})
|
||||
const [showCommentId, setShowCommentId] = useState(null)
|
||||
const [commentText, setCommentText] = useState('')
|
||||
const [comments, setComments] = useState({})
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); setCurrentUserId(payload.sub) } catch {} }
|
||||
}, [])
|
||||
|
||||
const API_BASE = 'http://47.103.9.192:3000'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>News Page TEST</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,654 +0,0 @@
|
|||
// AI OCR 识别页面 - 支持拍照识别和手工录入
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
|
||||
// 字段转换函数:camelCase → 字段编码
|
||||
const convertField = (obj) => {
|
||||
const map = {
|
||||
// f99 系统字段
|
||||
id: 'f99_90_id',
|
||||
userId: 'f99_91_user_id',
|
||||
// f01 基本信息
|
||||
name: 'f01_01_name',
|
||||
code: 'f01_02_code',
|
||||
category: 'f01_03_category',
|
||||
status: 'f01_04_status',
|
||||
remark: 'f01_05_remark',
|
||||
// f02 详细字段
|
||||
prefixSerial: 'f02_10_prefix_serial',
|
||||
version: 'f02_11_version',
|
||||
packaging: 'f02_12_packaging',
|
||||
rarity: 'f02_13_rarity',
|
||||
// f03 评级信息
|
||||
isGraded: 'f03_20_is_graded',
|
||||
gradingCompany: 'f03_21_grading_company',
|
||||
gradingScore: 'f03_22_grading_score',
|
||||
threeStar: 'f03_23_three_star',
|
||||
// f04 特殊信息
|
||||
specialMark: 'f04_30_special_mark',
|
||||
serialFeature: 'f04_31_serial_feature',
|
||||
issuer: 'f04_32_issuer',
|
||||
issueYear: 'f04_33_issue_year',
|
||||
material: 'f04_34_material',
|
||||
denomination: 'f04_35_denomination',
|
||||
issueQuantity: 'f04_36_issue_quantity',
|
||||
// f05 价格信息
|
||||
costPrice: 'f05_40_cost_price',
|
||||
targetPrice: 'f05_41_target_price',
|
||||
goalPrice: 'f05_42_goal_price',
|
||||
repairFee: 'f05_43_repair_fee',
|
||||
gradingFee: 'f05_44_grading_fee',
|
||||
// f06 其他信息
|
||||
purpose: 'f06_50_purpose'
|
||||
}
|
||||
const result = {}
|
||||
const numberFields = ['costPrice', 'targetPrice', 'goalPrice', 'repairFee', 'gradingFee']
|
||||
for (const key in obj) {
|
||||
let value = obj[key]
|
||||
if (value === '' || value === null) {
|
||||
value = null
|
||||
} else if (numberFields.includes(key)) {
|
||||
value = parseFloat(value)
|
||||
if (isNaN(value)) value = null
|
||||
}
|
||||
result[map[key] || key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 通用 Input 组件
|
||||
const Input = ({ form, handleChange, label, field, type = 'text', options = null, disabled = false }) => {
|
||||
const inputRef = useRef(null)
|
||||
const handleFocus = () => {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const onChange = (e) => {
|
||||
const value = e.target.value
|
||||
if (type === 'number' && value !== '') {
|
||||
const num = parseFloat(value)
|
||||
handleChange(field, isNaN(num) ? '' : num)
|
||||
} else {
|
||||
handleChange(field, value)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: '12px' }} ref={inputRef}>
|
||||
<div style={{ fontSize: '13px', color: '#94a3b8', marginBottom: '6px' }}>{label}</div>
|
||||
{options ? (
|
||||
<select
|
||||
value={form[field] || ''}
|
||||
onChange={onChange}
|
||||
onFocus={handleFocus}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
background: disabled ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.05)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
color: disabled ? '#64748b' : '#fff',
|
||||
padding: '10px',
|
||||
borderRadius: '8px',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<option value="">请选择</option>
|
||||
{options.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type={type}
|
||||
value={form[field] ?? ''}
|
||||
onChange={onChange}
|
||||
onFocus={handleFocus}
|
||||
disabled={disabled}
|
||||
style={{
|
||||
background: disabled ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.05)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
color: disabled ? '#64748b' : '#fff',
|
||||
padding: '10px',
|
||||
borderRadius: '8px',
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 默认表单数据
|
||||
const getDefaultForm = () => ({
|
||||
name: '龙钞',
|
||||
code: '',
|
||||
category: '自持',
|
||||
rarity: '通货',
|
||||
prefixSerial: '',
|
||||
version: '2024 龙',
|
||||
denomination: '',
|
||||
status: 'in_collection',
|
||||
packaging: '单张',
|
||||
material: '',
|
||||
issueQuantity: '',
|
||||
purpose: '收藏',
|
||||
isGraded: false,
|
||||
gradingCompany: '',
|
||||
gradingScore: '',
|
||||
threeStar: false,
|
||||
specialMark: '',
|
||||
serialFeature: '',
|
||||
issuer: '中国人民银行',
|
||||
issueYear: '',
|
||||
costPrice: '',
|
||||
targetPrice: '',
|
||||
goalPrice: '',
|
||||
repairFee: '',
|
||||
gradingFee: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
export default function OCR() {
|
||||
const [activeTab, setActiveTab] = useState('ai') // ai, manual, batch
|
||||
const [mode, setMode] = useState('camera') // camera, form
|
||||
const [form, setForm] = useState(getDefaultForm())
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [recognizing, setRecognizing] = useState(false)
|
||||
const [selectedImage, setSelectedImage] = useState(null)
|
||||
const [imagePreview, setImagePreview] = useState(null)
|
||||
const fileInputRef = useRef(null)
|
||||
const videoRef = useRef(null)
|
||||
const canvasRef = useRef(null)
|
||||
|
||||
// 选项定义
|
||||
const statusOptions = [
|
||||
{ value: 'in_collection', label: '收藏中' },
|
||||
{ value: 'selling', label: '出售中' },
|
||||
{ value: 'sold', label: '已售' },
|
||||
{ value: 'grading', label: '送评中' },
|
||||
{ value: 'repairing', label: '修复中' },
|
||||
{ value: 'transit', label: '在途中' },
|
||||
{ value: 'other', label: '其他' }
|
||||
]
|
||||
|
||||
const categoryOptions = [
|
||||
{ value: '自持', label: '自持' },
|
||||
{ value: '寄存', label: '寄存' },
|
||||
{ value: '寄售', label: '寄售' },
|
||||
{ value: '共有', label: '共有' },
|
||||
{ value: '寻号', label: '寻号' },
|
||||
{ value: '其他', label: '其他' }
|
||||
]
|
||||
|
||||
const rarityOptions = [
|
||||
{ value: '通货', label: '通货' },
|
||||
{ value: '特色', label: '特色' },
|
||||
{ value: '少见', label: '少见' },
|
||||
{ value: '稀有', label: '稀有' },
|
||||
{ value: '珍品', label: '珍品' },
|
||||
{ value: '孤品', label: '孤品' }
|
||||
]
|
||||
|
||||
const packagingOptions = [
|
||||
{ value: '标十', label: '标十' },
|
||||
{ value: '标百', label: '标百' },
|
||||
{ value: '单张', label: '单张' },
|
||||
{ value: '裸钞', label: '裸钞' }
|
||||
]
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
setForm({ ...form, [key]: value })
|
||||
if (key === 'targetPrice' && value) {
|
||||
setForm(prev => ({ ...prev, status: 'sold' }))
|
||||
}
|
||||
}
|
||||
|
||||
// 选择图片
|
||||
const handleSelectImage = (e) => {
|
||||
const file = e.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
setSelectedImage(file)
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
setImagePreview(e.target.result)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
// 调用 OCR 识别
|
||||
const handleRecognize = async () => {
|
||||
if (!selectedImage) {
|
||||
setError('请先选择图片')
|
||||
return
|
||||
}
|
||||
|
||||
setRecognizing(true)
|
||||
setError('')
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
const formData = new FormData()
|
||||
formData.append('image', selectedImage)
|
||||
|
||||
// 使用 XMLHttpRequest 替代 fetch,兼容性更好
|
||||
const data = await new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', '/api/ocr/recognize')
|
||||
xhr.setRequestHeader('Authorization', 'Bearer ' + token)
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
try {
|
||||
const data = JSON.parse(xhr.responseText)
|
||||
resolve(data)
|
||||
} catch (e) {
|
||||
reject(new Error('JSON解析失败: ' + xhr.responseText.substring(0, 100)))
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const data = JSON.parse(xhr.responseText)
|
||||
reject(new Error(data.error?.message || data.detail || '识别失败'))
|
||||
} catch (e) {
|
||||
reject(new Error('请求失败: ' + xhr.status))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onerror = function() {
|
||||
reject(new Error('网络错误'))
|
||||
}
|
||||
|
||||
xhr.send(formData)
|
||||
})
|
||||
|
||||
try {
|
||||
// 自动填充识别结果
|
||||
if (data.fields) {
|
||||
const recognizedForm = { ...getDefaultForm() }
|
||||
|
||||
// 映射识别字段到表单字段
|
||||
if (data.fields.name) recognizedForm.name = data.fields.name
|
||||
if (data.fields.code) recognizedForm.code = data.fields.code
|
||||
if (data.fields.version) recognizedForm.version = data.fields.version
|
||||
if (data.fields.prefix_serial) recognizedForm.prefixSerial = data.fields.prefix_serial
|
||||
if (data.fields.denomination) recognizedForm.denomination = data.fields.denomination
|
||||
if (data.fields.material) recognizedForm.material = data.fields.material
|
||||
if (data.fields.issue_year) recognizedForm.issueYear = data.fields.issue_year
|
||||
if (data.fields.issuer) recognizedForm.issuer = data.fields.issuer
|
||||
if (data.fields.cost_price) recognizedForm.costPrice = data.fields.cost_price
|
||||
if (data.fields.remark) recognizedForm.remark = data.fields.remark
|
||||
|
||||
setForm(recognizedForm)
|
||||
setMode('form')
|
||||
alert('识别成功!请检查并完善信息')
|
||||
} else {
|
||||
setError('识别结果为空')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('OCR 识别失败:', e)
|
||||
setError('识别失败:' + e.message)
|
||||
} finally {
|
||||
setRecognizing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存藏品
|
||||
const handleSave = async () => {
|
||||
if (!form.name || !form.version) {
|
||||
setError('名称和版别为必填项')
|
||||
return
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
setError('')
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
const formData = convertField(form)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/collections', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.error?.message || data.detail || '保存失败')
|
||||
}
|
||||
|
||||
alert('保存成功!')
|
||||
window.location.hash = '#/list'
|
||||
window.refreshList?.()
|
||||
window.refreshHome?.()
|
||||
} catch (e) {
|
||||
alert('保存失败:' + e.message)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 拍照模式界面
|
||||
if (mode === 'camera') {
|
||||
return (
|
||||
<div style={{ background: '#0f172a', minHeight: '100vh', paddingBottom: '80px' }}>
|
||||
{/* 顶部标签切换 */}
|
||||
<div style={{ padding: '12px 16px', background: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => setActiveTab('ai')}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '10px',
|
||||
background: activeTab === 'ai' ? '#fbbf24' : 'rgba(255,255,255,0.05)',
|
||||
color: activeTab === 'ai' ? '#1e293b' : '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
🤖 AI 识别
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('manual');
|
||||
setMode('form');
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '10px',
|
||||
background: activeTab === 'manual' ? '#fbbf24' : 'rgba(255,255,255,0.05)',
|
||||
color: activeTab === 'manual' ? '#1e293b' : '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
✍️ 手工录入
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('batch')}
|
||||
disabled
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '10px',
|
||||
background: 'rgba(255,255,255,0.02)',
|
||||
color: '#64748b',
|
||||
border: '1px solid rgba(255,255,255,0.05)',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: 'not-allowed'
|
||||
}}
|
||||
>
|
||||
📦 批量录入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
{activeTab === 'ai' && (
|
||||
|
||||
<div style={{ padding: '16px' }}>
|
||||
{/* 图片选择区域 */}
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
borderRadius: '12px',
|
||||
padding: '24px',
|
||||
textAlign: 'center',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
onChange={handleSelectImage}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{imagePreview ? (
|
||||
<div>
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="已选择图片"
|
||||
style={{ maxWidth: '100%', borderRadius: '8px', marginBottom: '16px' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
||||
<button
|
||||
onClick={() => { setSelectedImage(null); setImagePreview(null); }}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
background: 'rgba(239, 68, 68, 0.2)',
|
||||
color: '#ef4444',
|
||||
border: '1px solid #ef4444',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
🗑️ 重新选择
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRecognize}
|
||||
disabled={recognizing}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
background: recognizing ? '#64748b' : '#fbbf24',
|
||||
color: recognizing ? '#94a3b8' : '#1e293b',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
fontSize: '14px',
|
||||
fontWeight: '600',
|
||||
cursor: recognizing ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
{recognizing ? '🔍 识别中...' : '🤖 开始识别'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div
|
||||
onClick={() => {
|
||||
fileInputRef.current.setAttribute('capture', '');
|
||||
fileInputRef.current.setAttribute('accept', 'image/*');
|
||||
fileInputRef.current.click();
|
||||
}}
|
||||
style={{
|
||||
padding: '24px',
|
||||
background: 'rgba(59, 130, 246, 0.1)',
|
||||
border: '2px dashed rgba(59, 130, 246, 0.5)',
|
||||
borderRadius: '12px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '16px',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '40px', marginBottom: '12px' }}>📷</div>
|
||||
<div style={{ color: '#60a5fa', fontSize: '15px', fontWeight: 'bold', marginBottom: '4px' }}>
|
||||
拍照识别
|
||||
</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px' }}>
|
||||
使用相机拍照并识别
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => {
|
||||
fileInputRef.current.removeAttribute('capture');
|
||||
fileInputRef.current.setAttribute('accept', 'image/*');
|
||||
fileInputRef.current.click();
|
||||
}}
|
||||
style={{
|
||||
padding: '24px',
|
||||
background: 'rgba(34, 197, 94, 0.1)',
|
||||
border: '2px dashed rgba(34, 197, 94, 0.5)',
|
||||
borderRadius: '12px',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '40px', marginBottom: '12px' }}>🖼️</div>
|
||||
<div style={{ color: '#22c55e', fontSize: '15px', fontWeight: 'bold', marginBottom: '4px' }}>
|
||||
从相册选择
|
||||
</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px' }}>
|
||||
从相册选择已有图片
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div style={{
|
||||
background: 'rgba(239, 68, 68, 0.2)',
|
||||
border: '1px solid #ef4444',
|
||||
color: '#ef4444',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '12px'
|
||||
}}>
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 提示信息 */}
|
||||
<div style={{
|
||||
background: 'rgba(59, 130, 246, 0.1)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
borderRadius: '8px',
|
||||
padding: '16px',
|
||||
marginTop: '12px'
|
||||
}}>
|
||||
<div style={{ color: '#60a5fa', fontSize: '14px', fontWeight: 'bold', marginBottom: '8px' }}>💡 识别说明</div>
|
||||
<ul style={{ color: '#94a3b8', fontSize: '13px', paddingLeft: '20px', margin: 0 }}>
|
||||
<li>支持拍照或从相册选择图片</li>
|
||||
<li>自动识别名称、版别、冠字序号等字段</li>
|
||||
<li>识别结果可手动修改完善</li>
|
||||
<li>建议拍摄清晰、光线充足的正面照片</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 表单模式界面
|
||||
return (
|
||||
<div style={{ background: '#0f172a', minHeight: '100vh', paddingBottom: '80px' }}>
|
||||
{/* 顶部 */}
|
||||
<div style={{ padding: '16px', background: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<button
|
||||
onClick={() => setMode('camera')}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.1)',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
fontSize: '20px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<div>
|
||||
<div style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold' }}>确认信息</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px' }}>请检查并完善识别结果</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
margin: '16px',
|
||||
background: 'rgba(239, 68, 68, 0.2)',
|
||||
border: '1px solid #ef4444',
|
||||
color: '#ef4444',
|
||||
padding: '12px',
|
||||
borderRadius: '8px'
|
||||
}}>
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ padding: '16px' }}>
|
||||
{/* 基本信息 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>基本信息</div>
|
||||
<Input form={form} handleChange={handleChange} label="名称 *" field="name" />
|
||||
<Input form={form} handleChange={handleChange} label="版别 *" field="version" />
|
||||
<Input form={form} handleChange={handleChange} label="冠字序号" field="prefixSerial" />
|
||||
<Input form={form} handleChange={handleChange} label="面值" field="denomination" />
|
||||
<Input form={form} handleChange={handleChange} label="材质" field="material" />
|
||||
<Input form={form} handleChange={handleChange} label="发行方" field="issuer" />
|
||||
<Input form={form} handleChange={handleChange} label="发行年份" field="issueYear" />
|
||||
</div>
|
||||
|
||||
{/* 价格信息 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>价格信息</div>
|
||||
<Input form={form} handleChange={handleChange} label="成本价" field="costPrice" type="number" />
|
||||
<Input form={form} handleChange={handleChange} label="目标价" field="targetPrice" type="number" />
|
||||
<Input form={form} handleChange={handleChange} label="出售价" field="goalPrice" type="number" />
|
||||
</div>
|
||||
|
||||
{/* 备注 */}
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '13px', color: '#94a3b8', marginBottom: '6px' }}>备注</div>
|
||||
<textarea
|
||||
value={form.remark || ''}
|
||||
onChange={(e) => handleChange('remark', e.target.value)}
|
||||
rows={3}
|
||||
style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff', padding: '10px', borderRadius: '8px', width: '100%', resize: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<button
|
||||
onClick={saving ? null : handleSave}
|
||||
disabled={saving}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
background: saving ? '#64748b' : '#fbbf24',
|
||||
color: saving ? '#94a3b8' : '#1e293b',
|
||||
border: 'none',
|
||||
borderRadius: '10px',
|
||||
fontSize: '16px',
|
||||
fontWeight: '600',
|
||||
cursor: saving ? 'not-allowed' : 'pointer',
|
||||
marginBottom: '12px'
|
||||
}}
|
||||
>
|
||||
{saving ? '保存中...' : '✅ 保存藏品'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setMode('camera')}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
color: '#94a3b8',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: '10px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
🔄 重新识别
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue