jiachenlong/frontend/src/pages/News.jsx

1246 lines
70 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

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

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

import React, { useState, useEffect } from 'react'
import YichensBoard from './YichensBoard'
// 本地获取用户手机号 - 添加异常处理
const getUserPhone = () => {
try {
const userStr = localStorage.getItem('user')
if (!userStr) return ''
const user = JSON.parse(userStr)
return user.phone || user.phoneNumber || user.mobile || user.tel || ''
} catch { return '' }
}
// 本地获取用户ID - 添加异常处理
const getStoredUserId = () => {
try {
const userStr = localStorage.getItem('user')
if (!userStr) return null
const user = JSON.parse(userStr)
return user.id || user.user_id || user.f99_90_id || null
} catch { return null }
}
// 资讯页面 - 展示寻配号和行情信息(所有人可见)
export default function News() {
const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'yichen')
const [showSeekPublish, setShowSeekPublish] = useState(false)
const [showMySeeks, setShowMySeeks] = useState(false) // 我的寻号
const [showContactId, setShowContactId] = useState(null) // 显示联系方式的寻号ID
const [showCommentId, setShowCommentId] = useState(null) // 显示评论输入的寻号ID
const [commentText, setCommentText] = useState('') // 评论内容
const [comments, setComments] = useState({}) // 存储各寻号的评论
const [matchedStatus, setMatchedStatus] = useState({}) // 存储各寻号的匹配状态
const [showMatchList, setShowMatchList] = useState(false)
const [dealVersion, setDealVersion] = useState('龙钞')
const [dealDate, setDealDate] = useState(() => {
const today = new Date()
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
})
const [dealDetailItems, setDealDetailItems] = useState(null) // 弹窗显示的详情列表
const [matchCollections, setMatchCollections] = useState([])
const [networkMatchCollections, setNetworkMatchCollections] = useState([])
const [showNetworkMatchList, setShowNetworkMatchList] = useState(false)
const [customModal, setCustomModal] = useState({show: false, title: '', content: ''})
const [seekForm, setSeekForm] = useState({
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || ''
})
const [infoList, setInfoList] = useState([])
const [viewMode, setViewMode] = useState('all')
const [expandedItems, setExpandedItems] = useState({}) // 展开状态
const [loading, setLoading] = useState(false)
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const API_BASE = localStorage.getItem('API_BASE') || ''
const currentUser = localStorage.getItem('token') ? { id: 'logged_in' } : null
// 切换展开/收起
const toggleExpand = (itemId) => {
setExpandedItems(prev => ({
...prev,
[itemId]: !prev[itemId]
}))
}
// 获取资讯列表
useEffect(() => {
fetchInfoList()
}, [activeTab, dealDate])
// 自动生成寻号标题
useEffect(() => {
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${seekForm.features || 'XXXXXXXX'}`
setSeekForm(prev => ({...prev, title}))
}, [seekForm.edition, seekForm.features])
const fetchInfoList = async () => {
setLoading(true)
try {
const token = localStorage.getItem('token')
const type = activeTab === 'seek' ? 'seek' : activeTab === 'deal' ? 'deal' : 'yichen'
const headers = token ? { Authorization: `Bearer ${token}` } : {}
// 构建URL参数
let url = `${API_BASE}/api/information/list?info_type=${type}`
// 如果是成交行情tab获取当天所有数据不分页
if (activeTab === 'deal' && dealDate) {
url += `&deal_date=${dealDate}&page_size=500`
} else {
url += `&page=${currentPage}&page_size=50`
}
const res = await fetch(url, { headers })
// 从响应头获取总页数
const total = res.headers.get('X-Total-Pages') || res.headers.get('x-total-pages')
if (total) setTotalPages(parseInt(total))
// 检查响应状态
if (!res.ok) {
console.error('获取资讯列表失败:', res.status)
setInfoList([])
setLoading(false)
return
}
const data = await res.json()
console.log('资讯列表:', data)
// 成交行情按分类排序
let sortedData = data || []
if (activeTab === 'deal') {
const categoryOrder = ['带4号', '带7号', '永恒号', '天马号', '天马王', '金山号', '金山王', '钻石号', '朦胧号', '朦胧王', '金马号', '金马王', '倒置号', '圆圆号']
// 标准化分类名称(通货->带4号无4->带7号
const normalizeCat = (c) => {
if (c === '通货') return '带4号'
if (c === '无4') return '带7号'
return c
}
sortedData = [...(data || [])].sort((a, b) => {
const contentA = a.content || ''
const contentB = b.content || ''
const catA = normalizeCat(contentA.match(/分类:\s*(.+?)(?:\n|$)/)?.[1]?.trim() || a.category || '')
const catB = normalizeCat(contentB.match(/分类:\s*(.+?)(?:\n|$)/)?.[1]?.trim() || b.category || '')
const idxA = categoryOrder.indexOf(catA)
const idxB = categoryOrder.indexOf(catB)
return (idxA === -1 ? 999 : idxA) - (idxB === -1 ? 999 : idxB)
})
}
setInfoList(sortedData)
} catch (e) {
console.error(e)
setInfoList([])
}
setLoading(false)
}
// 获取匹配藏品列表
// 获取寻号评论
const fetchComments = async (infoId) => {
// 每次都重新获取评论
try {
const res = await fetch(`${API_BASE}/api/information/comments/${infoId}`)
const data = await res.json()
setComments(prev => ({...prev, [infoId]: data || []}))
} catch (e) {
console.error('获取评论失败:', e)
}
}
// 添加评论
const addComment = async (infoId) => {
if (!commentText.trim()) { alert('请输入评论内容'); return }
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/comment`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ information_id: infoId, content: commentText })
})
const data = await res.json()
if (data.message === '留言成功' || data.message === '评论成功') {
setCommentText('')
// 强制刷新评论列表,先清空缓存
setComments(prev => {
const newComments = {...prev}
delete newComments[infoId]
return newComments
})
// 然后重新获取
setTimeout(() => fetchComments(infoId), 100)
// 确保停留在寻号 tab
if (activeTab !== 'seek') {
setActiveTab('seek')
}
alert('留言成功!')
} else {
alert(data.detail || '留言失败')
}
} catch (e) {
console.error('留言失败:', e)
alert('留言失败,请重试')
}
}
// 匹配并联系藏友
const matchAndContact = async (infoId) => {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const phone = getUserPhone()
const res = await fetch(`${API_BASE}/api/information/seek/match-confirm`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ info_id: infoId, contact: phone || '用户已同意' })
})
const data = await res.json()
if (data.message === '匹配成功,已通知发布者') {
setMatchedStatus(prev => ({...prev, [infoId]: 'matched'}))
setShowContactId(infoId) // 显示联系方式
setCustomModal({show: true, title: '匹配成功', content: '已通知发布者,请等待对方联系'})
fetchInfoList() // 刷新列表
}
} catch (e) {
alert('操作失败: ' + e.message)
}
}
const fetchMatchedUserInfo = async (infoId) => {
console.log('fetchMatchedUserInfo called with:', infoId)
let token = localStorage.getItem('token')
if (!token) {
// Try to refresh token
const username = localStorage.getItem('username')
const password = localStorage.getItem('password')
if (username && password) {
const form = new FormData()
form.append('username', username)
form.append('password', password)
const loginRes = await fetch(`${API_BASE}/api/auth/login`, { method: 'POST', body: form })
const loginData = await loginRes.json()
if (loginData.access_token) {
token = loginData.access_token
localStorage.setItem('token', token)
}
}
if (!token) { alert('请先登录'); return }
}
try {
const res = await fetch(`${API_BASE}/api/information/seek/matched-user/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
console.log('Matched user response:', res.status)
const data = await res.json()
console.log('Matched user data:', data)
if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示:所有用户信息仅作参考,交易请走正规平台,如产生经济损失与本站无关,后果自负。`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
const fetchPublisherInfo = async (infoId) => {
console.log('fetchPublisherInfo called with:', infoId)
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/seek/publisher/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
console.log('Publisher response:', res.status)
const data = await res.json()
console.log('Publisher data:', data)
if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示:所有用户信息仅作参考,交易请走正规平台,如产生经济损失与本站无关,后果自负。`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
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 fetchNetworkMatchCollections = async (infoId) => {
try {
const res = await fetch(`${API_BASE}/api/information/seek/network-match/${infoId}`)
const data = await res.json()
console.log('网络数据匹配结果:', data)
setNetworkMatchCollections(data.collections || [])
setShowNetworkMatchList(true)
} catch (e) {
console.error('获取网络匹配藏品失败:', e)
alert('获取网络匹配藏品失败: ' + e.message)
}
}
// 获取当前用户ID
const currentUserId = getStoredUserId()
// 获取我的寻号列表
const [mySeekList, setMySeekList] = useState([])
const [myMatchedList, setMyMatchedList] = 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' && item.user_id === currentUserId) : []
setMySeekList(seeks)
setInfoList(seeks) // 在主列表显示
} catch (e) { console.error(e) }
}
// 获取我已匹配的寻号列表
const fetchMyMatches = async () => {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const res = await fetch(`${API_BASE}/api/information/list?info_type=seek`, {
headers: { Authorization: `Bearer ${token}` }
})
const data = await res.json()
// 筛选我已匹配的matched_user_id等于当前用户ID
const matched = data.filter(item => item.is_matched === 'matched' && item.matched_user_id === currentUserId)
setMyMatchedList(matched)
setInfoList(matched) // 在主列表显示
} 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 () => {
const phone = getUserPhone()
const finalContact = seekForm.contact || phone || ''
if (!finalContact) { alert('请填写联系方式'); return }
const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + finalContact
try {
const token = localStorage.getItem('token')
// 从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: getUserPhone() || '' })
fetchInfoList()
} else { alert(data.message || '发布失败') }
} catch (e) { alert('发布失败: ' + e.message) }
}
// Tab切换
const tabs = [
{ key: 'seek', label: '寻配号' },
{ key: 'deal', label: '成交行情' },
{ key: 'yichen', 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); localStorage.setItem('news_activeTab', 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>
<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: '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-9XABCDEFG]/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>
<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={{ 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={{ 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 === 'deal' ? `成交行情信息 (${dealDate})` : '一尘看板'}
{activeTab === 'seek' && (
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
<button onClick={() => { setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部</button>
<button onClick={() => { setViewMode('mySeeks'); fetchMySeeks(); }} style={{ padding: '6px 12px', background: viewMode==='mySeeks'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>我的寻号</button>
<button onClick={() => { setViewMode('myMatches'); fetchMyMatches(); }} style={{ padding: '6px 12px', background: viewMode==='myMatches'?'#8b5cf6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>我的配号</button>
<button onClick={() => setShowSeekPublish(!showSeekPublish)} style={{ padding: '6px 12px', background: showSeekPublish ? '#6b7280' : '#10b981', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>发布寻号</button>
</div>
)}
{activeTab === 'deal' && (
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
<input type="date" value={dealDate} onChange={e => setDealDate(e.target.value)}
style={{ padding: '6px 12px', background: '#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }} />
</div>
)}
</h3>
{/* 一尘看板独立渲染 */}
{activeTab === 'yichen' ? (
<YichensBoard />
) : loading ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>加载中...</div>
) : infoList.length === 0 ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>
暂无{activeTab === 'seek' ? '寻配号' : activeTab === 'deal' ? '成交行情' : '一尘看板'}信息
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{/* 成交行情 - 价格统计表格 */}
{activeTab === 'deal' && (() => {
const versions = ['龙钞', '马钞', '蛇钞', '其他']
const packagings = ['标百', '标十', '单张']
const categories = ['带4号', '带7号', '永恒号', '永恒', '钻石号', '钻石', '如意号', '朦胧号', '朦胧王', '天马号', '金山号', '金马号', '天马王', '金山王', '金马王', '倒置号', '圆圆号', '通货', '无4']
// 分类名称标准化
const normalizeCat = (c) => {
if (c === '通货') return '带4号'
if (c === '无4') return '带7号'
return c
}
const filteredData = infoList.filter(item => {
const serial = (item.title || '').split('-')[0] || ''
let version = '其他'
if (serial.startsWith('J0')) version = '龙钞'
else if (serial.startsWith('J1')) version = '马钞'
else if (serial.startsWith('J3')) version = '蛇钞'
if (version !== dealVersion) return false
return true
})
const calcAvg = (pkg, cat) => {
const items = filteredData.filter(item => {
const content = item.content || ''
const p = content.includes('包装:') ? content.split('包装:')[1].split('\n')[0].trim() : (item.packaging || '')
let c = content.includes('分类:') ? content.split('分类:')[1].split('\n')[0].trim() : (item.category || '')
c = normalizeCat(c)
return p === pkg && c === cat
})
if (items.length === 0) return null
const sum = items.reduce((a, b) => a + (b.deal_price || 0), 0)
return { avg: Math.round(sum / items.length), count: items.length, items }
}
return (
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '12px' }}>
{versions.map(v => (
<button key={v} onClick={() => setDealVersion(v)}
style={{ padding: '6px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
background: dealVersion === v ? '#3b82f6' : 'rgba(255,255,255,0.1)',
color: '#fff', fontSize: '13px', fontWeight: dealVersion === v ? '600' : '400' }}>
{v}
</button>
))}
</div>
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead>
<tr>
<th style={{ padding: '8px', textAlign: 'left', color: '#94a3b8', borderBottom: '1px solid rgba(255,255,255,0.1)' }}></th>
{packagings.map(p => (
<th key={p} style={{ padding: '8px', textAlign: 'center', color: '#94a3b8', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>{p}</th>
))}
</tr>
</thead>
<tbody>
{(() => {
// 计算每个分类的平均价格
const catAvg = categories.map(cat => {
const prices = []
packagings.forEach(pkg => {
const d = calcAvg(pkg, cat)
if (d) prices.push(d.avg)
})
const avg = prices.length > 0 ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0
return { cat, avg }
}).filter(c => c.avg > 0)
// 分类名称标准化并按固定顺序排序
catAvg.sort((a, b) => a.avg - b.avg)
const categoryOrder = ['带4号', '带7号', '永恒号', '天马号', '天马王', '金山号', '金山王', '钻石号', '朦胧号', '朦胧王', '金马号', '金马王', '倒置号', '圆圆号']
const sortedCats = categoryOrder.filter(cat => catAvg.some(c => c.cat === cat))
.concat(catAvg.filter(c => !categoryOrder.includes(c.cat)).map(c => c.cat))
return sortedCats.map(cat => {
const rowData = packagings.map(pkg => calcAvg(pkg, cat))
const hasData = rowData.some(d => d !== null)
if (!hasData) return null
return (
<tr key={cat}>
<td style={{ padding: '8px', color: '#fbbf24', fontWeight: '500', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{cat}</td>
{rowData.map((d, i) => (
<td key={i} style={{ padding: '8px', textAlign: 'center', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
{d ? (
<div style={{ color: '#22c55e', fontWeight: '600', cursor: 'pointer' }}
onClick={() => setDealDetailItems(d.items)}>
¥{d.avg.toLocaleString()}
<span style={{ color: '#64748b', fontSize: '11px', marginLeft: '4px' }}>({d.count})</span>
</div>
) : <span style={{ color: '#475569' }}>-</span>}
</td>
))}
</tr>
)
})
})()}
</tbody>
</table>
</div>
</div>
)
})()}
{/* 成交行情详情弹窗 */}
{dealDetailItems && (
<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',
padding: '20px'
}}>
<div style={{
background: '#1e293b', borderRadius: '12px', padding: '16px',
maxWidth: '600px', maxHeight: '80vh', overflow: 'auto', width: '100%'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<div style={{ color: '#fff', fontSize: '16px', fontWeight: '600' }}>成交详情列表</div>
<button onClick={() => setDealDetailItems(null)}
style={{ background: 'none', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}></button>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{dealDetailItems.map((item, idx) => {
const content = item.content || ''
const grade = content.includes('评级:') ? content.split('评级:')[1].split('\n')[0].trim() : ''
return (
<div key={idx} style={{
background: 'rgba(255,255,255,0.05)', borderRadius: '8px', padding: '12px',
display: 'flex', justifyContent: 'space-between', alignItems: 'center'
}}>
<div>
<div style={{ color: '#fbbf24', fontWeight: '600' }}>{(item.title || '').split('-')[0]}</div>
<div style={{ color: '#94a3b8', fontSize: '12px', marginTop: '4px' }}>
{item.deal_date} | {item.category} | {item.packaging}
{grade && ` | ${grade}`}
</div>
</div>
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '700' }}>
¥{item.deal_price?.toLocaleString()}
</div>
</div>
)
})}
</div>
</div>
</div>
)}
{infoList.map(item => {
// 成交行情 tab - 按维度汇总展示
if (activeTab === 'deal') {
const content = item.content || ''
const categoryMatch = content.match(/分类:\s*(.+?)(?:\n|$)/)
const packagingMatch = content.match(/包装:\s*(.+?)(?:\n|$)/)
const platformMatch = content.match(/平台:\s*(.+?)(?:\n|$)/)
const sellerMatch = content.match(/出售者:\s*(.+?)(?:\n|$)/)
const dateMatch = content.match(/日期:\s*(.+?)(?:\n|$)/)
const gradeMatch = content.match(/评级:\s*(.+?)(?:\n|$)/)
const category = categoryMatch ? categoryMatch[1].trim() : item.category || '-'
const packaging = packagingMatch ? packagingMatch[1].trim() : item.packaging || '-'
const platform = platformMatch ? platformMatch[1].trim() : '-'
const seller = sellerMatch ? sellerMatch[1].trim() : '-'
const dealDate = dateMatch ? dateMatch[1].trim() : item.deal_date || '-'
const grade = gradeMatch ? gradeMatch[1].trim() : (item.grading_score || '')
const gradingCompany = item.grading_company || ''
const serial = (item.title || '').split('-')[0] || ''
let version = '其他'
if (serial.startsWith('J0')) version = '龙钞'
else if (serial.startsWith('J1')) version = '马钞'
else if (serial.startsWith('J3')) version = '蛇钞'
return (
<div key={item.id} style={{ background: '#1e293b', borderRadius: '12px', padding: '12px', marginBottom: '10px' }}>
{/* 第一行:冠字号 | 版别 | 包装 | 分数 + 价格 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ color: '#fbbf24', fontSize: '15px', fontWeight: '600' }}>{serial}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#3b82f6', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{version}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#8b5cf6', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{packaging}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#06b6d4', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{grade}</span>
</div>
<div style={{ color: '#22c55e', fontSize: '16px', fontWeight: '700' }}>¥{item.deal_price?.toLocaleString()}</div>
</div>
{/* 第二行:编码 | 分类 | 评级机构 | 平台 | 日期 */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
<span style={{ color: '#64748b', fontSize: '11px' }}>{item.deal_no}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#f97316', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{category}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
{gradingCompany && <span style={{ background: '#ec4899', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{gradingCompany}</span>}
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#10b981', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{platform}</span>
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
<span style={{ background: '#64748b', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{dealDate}</span>
</div>
</div>
)
}
// 解析正文中的号码特征和联系方式
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)} &nbsp;|&nbsp; 👤 {(item.content && item.content.match(/发布者: (.+)/)?.[1]) || 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>
{item.network_matched_count !== undefined && (
<span
style={{ color: '#3b82f6', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer', marginLeft: '12px' }}
onClick={() => fetchNetworkMatchCollections(item.id)}
>
网络数据{item.network_matched_count}条匹配成功
</span>
)}
</div>
)}
{/* 正文 - 默认收起,点击展开 */}
{cleanContent && (
<div style={{ marginTop: '8px' }}>
{/* 展开收起按钮 */}
<div
onClick={() => toggleExpand(item.id)}
style={{
color: '#10b981',
fontSize: '13px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '8px 12px',
background: 'rgba(16,185,129,0.1)',
borderRadius: '6px',
border: '1px solid rgba(16,185,129,0.2)'
}}
>
<span style={{ fontSize: '16px' }}>{expandedItems[item.id] ? '▼' : '▶'}</span>
<span>{expandedItems[item.id] ? '收起详情' : '展开查看详情'}</span>
</div>
{/* 展开后的内容 */}
{expandedItems[item.id] && (
<div style={{
color: '#e2e8f0',
fontSize: '14px',
lineHeight: '1.8',
marginTop: '12px',
background: 'rgba(255,255,255,0.03)',
padding: '12px',
borderRadius: '8px',
borderLeft: '3px solid #10b981'
}}>
{cleanContent.split('\n').map((line, i) => {
const isHighlight = line.includes('涨价') || line.includes('下跌') || line.includes('稀缺') || line.includes('热门')
return (
<div key={i} style={{
marginBottom: i < cleanContent.split('\n').length - 1 ? '8px' : 0,
color: isHighlight ? '#fbbf24' : '#e2e8f0',
fontWeight: isHighlight ? 'bold' : 'normal'
}}>
{line || ' '}
</div>
)
})}
</div>
)}
</div>
)}
{/* 联系方式 - 默认隐藏,显示*** */}
{contact && (
<div style={{ marginBottom: '8px' }}>
<div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>联系方式</div>
<div style={{ color: '#f97316', fontSize: '13px', padding: '8px', background: 'rgba(249,115,22,0.1)', borderRadius: '6px' }}>
联系方式{showContactId === item.id ? contact : '匹配成功后可查看'}
</div>
{/* 是否匹配按钮 - 仅对已登录用户显示 */}
{currentUser && (
<div style={{ marginTop: '8px' }}>
<span style={{
padding: '5px 14px',
borderRadius: '4px',
fontSize: '12px',
background: (item.is_matched === 'matched' || matchedStatus[item.id] === 'matched')
? ((item.user_id === currentUserId || item.matched_user_id === currentUserId) ? '#10b981' : '#6b7280')
: '#6b7280',
color: '#fff',
opacity: (item.is_matched === 'matched' || matchedStatus[item.id] === 'matched')
? ((item.user_id === currentUserId || item.matched_user_id === currentUserId) ? 1 : 0.5)
: 1
}}>
{(item.is_matched === 'matched' || matchedStatus[item.id] === 'matched') ? '已经匹配' : '尚未匹配'}
{(item.is_matched === 'matched' || matchedStatus[item.id] === 'matched') && <button onClick={() => item.user_id === currentUserId ? fetchMatchedUserInfo(item.id) : fetchPublisherInfo(item.id)} style={{padding:'4px 10px',borderRadius:'4px',fontSize:'12px',background:(item.user_id === currentUserId || item.matched_user_id === currentUserId) ? '#10b981' : '#6b7280',border:'none',color:(item.user_id === currentUserId || item.matched_user_id === currentUserId) ? '#fb923c' : '#9ca3af',fontWeight:'bold',cursor:(item.user_id === currentUserId || item.matched_user_id === currentUserId) ? 'pointer' : 'not-allowed',marginLeft:'4px'}}>{item.user_id === currentUserId ? '匹配者信息' : '发布者信息'}</button>}
</span>
</div>
)}
{/* 匹配并联系藏友 / 留言藏友 按钮 - 有匹配藏品才可点击 */}
{currentUser && (
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button
onClick={() => {
if (item.is_matched === 'matched') return
if (item.matched_count > 0) {
matchAndContact(item.id)
} else {
setCustomModal({show: true, title: '提示', content: '暂无匹配藏品,无法匹配'})
}
}}
disabled={item.matched_count === 0 || item.is_matched === 'matched'}
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '6px',
border: 'none',
fontSize: '12px',
background: (item.matched_count > 0 && item.is_matched !== 'matched') ? '#3b82f6' : '#4b5563',
color: '#fff',
cursor: (item.matched_count > 0 && item.is_matched !== 'matched') ? 'pointer' : 'not-allowed',
opacity: (item.matched_count > 0 && item.is_matched !== 'matched') ? 1 : 0.5
}}
>
匹配并联系藏友
</button>
<button
onClick={() => {
setShowCommentId(showCommentId === item.id ? null : item.id)
fetchComments(item.id)
}}
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid #334155',
fontSize: '12px',
background: '#1e293b',
color: '#fff',
cursor: 'pointer'
}}
>
留言藏友
</button>
</div>
)}
{/* 留言区域 */}
{showCommentId === item.id && (
<div style={{ marginTop: '8px', padding: '12px', background: '#0f172a', borderRadius: '6px' }}>
{/* 评论列表 */}
{comments[item.id] && comments[item.id].length > 0 && (
<div style={{ marginBottom: '8px' }}>
{comments[item.id].map((c, idx) => (
<div key={c.id || idx} style={{ padding: '8px 0', borderBottom: '1px solid #334155' }}>
<div style={{ color: '#3b82f6', fontSize: '12px', marginBottom: '4px' }}>{c.user_name || '匿名用户'}</div>
<div style={{ color: '#e2e8f0', fontSize: '13px' }}>{c.content}</div>
</div>
))}
</div>
)}
{/* 评论输入框 */}
<div style={{ display: 'flex', gap: '8px' }}>
<input
type="text"
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="输入留言..."
style={{
flex: 1,
padding: '8px',
background: '#1e293b',
border: '1px solid #334155',
borderRadius: '4px',
color: '#fff',
fontSize: '13px'
}}
/>
<button
onClick={() => addComment(item.id)}
style={{
padding: '8px 16px',
background: '#10b981',
border: 'none',
borderRadius: '4px',
color: '#fff',
fontSize: '13px',
cursor: 'pointer'
}}
>
发送
</button>
</div>
</div>
)}
</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>
)}
{/* 网络数据匹配藏品列表弹窗 */}
{showNetworkMatchList && (
<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={() => setShowNetworkMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button>
</div>
{networkMatchCollections.length === 0 ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '20px' }}>暂无匹配藏品</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{networkMatchCollections.map(c => (
<div
key={c.id}
style={{ background: '#0f172a', borderRadius: '8px', padding: '12px', cursor: 'pointer' }}
onClick={() => {
if (c.post_url) {
window.open(c.post_url, '_blank')
}
setShowNetworkMatchList(false)
}}
>
<div style={{ color: '#e2e8f0', fontSize: '13px' }}>
<span style={{ color: '#94a3b8' }}>名称: </span>{c.name || '-'}
</div>
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
<span style={{ color: '#94a3b8' }}>冠字号: </span>{c.crown_code}
</div>
{c.price && (
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
<span style={{ color: '#94a3b8' }}>价格: </span>¥{c.price}
</div>
)}
<div style={{ color: '#3b82f6', fontSize: '12px', marginTop: '6px' }}>
点击查看原帖
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* 分页组件 */}
{totalPages > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px', padding: '16px', marginTop: '8px' }}>
<button
onClick={() => { if (currentPage > 1) { setCurrentPage(currentPage - 1); fetchInfoList() } }}
disabled={currentPage === 1}
style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage === 1 ? '#374151' : '#3b82f6', color: currentPage === 1 ? '#6b7280' : '#fff', cursor: currentPage === 1 ? 'not-allowed' : 'pointer', fontSize: '13px' }}
>
上一页
</button>
<span style={{ color: '#94a3b8', fontSize: '13px' }}>
{currentPage} / {totalPages}
</span>
<button
onClick={() => { if (currentPage < totalPages) { setCurrentPage(currentPage + 1); fetchInfoList() } }}
disabled={currentPage >= totalPages}
style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage >= totalPages ? '#374151' : '#3b82f6', color: currentPage >= totalPages ? '#6b7280' : '#fff', cursor: currentPage >= totalPages ? 'not-allowed' : 'pointer', fontSize: '13px' }}
>
下一页
</button>
</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>
)}
{customModal.show && (
<div style={{position:'fixed',top:0,left:0,right:0,bottom:0,background:'rgba(0,0,0,0.7)',zIndex:2000,display:'flex',alignItems:'center',justifyContent:'center'}}>
<div style={{background:'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)',borderRadius:'16px',padding:'24px',width:'85%',maxWidth:'380px',boxShadow:'0 20px 60px rgba(0,0,0,0.5)',border:'1px solid rgba(255,255,255,0.1)'}}>
<div style={{textAlign:'center',marginBottom:'20px'}}>
<div style={{width:'60px',height:'60px',background:'linear-gradient(135deg, #10b981 0%, #059669 100%)',borderRadius:'50%',display:'flex',alignItems:'center',justifyContent:'center',margin:'0 auto 12px'}}>
<span style={{fontSize:'28px'}}></span>
</div>
<h3 style={{color:'#10b981',fontSize:'20px',fontWeight:'bold',margin:'0'}}>{customModal.title}</h3>
</div>
<div style={{background:'rgba(0,0,0,0.3)',borderRadius:'12px',padding:'16px',marginBottom:'20px'}}>
{customModal.content.split('\n').map((line, i) => (
<div key={i} style={{color:'#e2e8f0',fontSize:'15px',lineHeight:'2',display:'flex',justifyContent:'space-between'}}>
<span style={{color:'#94a3b8'}}>{line.split(':')[0]}</span>
<span style={{color:'#fbbf24',fontWeight:'600'}}>{line.split(':')[1]}</span>
</div>
))}
</div>
<button onClick={()=>setCustomModal({...customModal,show:false})} style={{width:'100%',padding:'14px',background:'linear-gradient(135deg, #10b981 0%, #059669 100%)',border:'none',borderRadius:'10px',color:'#fff',fontSize:'16px',fontWeight:'bold',cursor:'pointer',boxShadow:'0 4px 15px rgba(16,185,129,0.4)'}}>知道了</button>
</div>
</div>
)}
</div>
)
}