jiachenlong/frontend/src/pages/List.jsx

949 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version'
const API_BASE = localStorage.getItem('API_BASE') || ''
export default function List() {
const [collections, setCollections] = useState([])
const [loading, setLoading] = useState(true)
const [filter, setFilter] = useState('')
const [filterType, setFilterType] = useState('')
const urlParams = new URLSearchParams(window.location.hash.split('?')[1] || '')
const urlUserId = urlParams.get('userId') || ''
const [userIdFilter, setUserIdFilter] = useState(urlUserId)
const [sortField, setSortField] = useState('createdAt')
const [sortOrder, setSortOrder] = useState('desc')
const [viewMode, setViewMode] = useState('list')
const [key, setKey] = useState(0)
const [search, setSearch] = useState('')
const [isAdmin, setIsAdmin] = useState(false)
const [page, setPage] = useState(1)
const [pagination, setPagination] = useState({ total: 0, pages: 1 })
const [activeTab, setActiveTab] = useState('collections') // collections-藏品, deals-行情
const [dealSearch, setDealSearch] = useState('')
const [dealSortField, setDealSortField] = useState('created_at')
const [dealSortOrder, setDealSortOrder] = useState('desc')
const [myDeals, setMyDeals] = useState([])
const [dealsLoading, setDealsLoading] = useState(false)
useEffect(() => {
// 检查是否管理员
const userStr = localStorage.getItem('user')
if (userStr) {
try {
const user = JSON.parse(userStr)
setIsAdmin(user.role === 'admin')
} catch (e) {}
}
// 监听hash变化重新读取筛选参数
const handleHashChange = () => {
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
// 支持两种格式filter=category&value=自持 或 filter=category=自持
let filterTypeParam = params.get('filter') || ''
let valueParam = params.get('value') || ''
if (filterTypeParam && valueParam) {
// 新格式filter=category&value=自持
setFilterType(filterTypeParam)
setFilter(decodeURIComponent(valueParam))
} else if (filterTypeParam && filterTypeParam.includes('=')) {
// 旧格式filter=category=自持
const [type, value] = filterTypeParam.split('=')
setFilterType(type)
setFilter(decodeURIComponent(value))
} else {
setFilter('')
setFilterType('')
}
fetchCollections()
}
handleHashChange()
window.addEventListener('hashchange', handleHashChange)
window.addEventListener('focus', fetchCollections)
return () => {
window.removeEventListener('hashchange', handleHashChange)
window.removeEventListener('focus', fetchCollections)
}
}, [])
// 获取当前用户录入的行情数据
const fetchMyDeals = async () => {
setDealsLoading(true)
const token = localStorage.getItem('token')
const userStr = localStorage.getItem('user')
if (!token || !userStr) {
setDealsLoading(false)
return
}
try {
const user = JSON.parse(userStr)
const res = await fetch(`${API_BASE}/api/deal/list?user_only=true&page_size=500`, {
headers: { 'Authorization': 'Bearer ' + token }
})
const data = await res.json()
const list = data.data || data || []
setMyDeals(Array.isArray(list) ? list : [])
} catch (e) {
console.error('获取行情失败:', e)
}
setDealsLoading(false)
}
// 切换tab时加载数据
useEffect(() => {
if (activeTab === 'deals') {
fetchMyDeals()
}
}, [activeTab])
// 行情过滤和排序
const filteredDeals = myDeals.filter(deal => {
if (!dealSearch) return true
const s = dealSearch.toLowerCase().trim()
const fields = [deal.title, deal.content, deal.category, deal.packaging, deal.grading_company, deal.grading_score, deal.deal_no, deal.seller, deal.platform, deal.buyer, deal.deal_price?.toString(), deal.deal_date].filter(v => v).map(v => v.toString().toLowerCase())
return fields.some(f => f.includes(s))
}).sort((a, b) => {
let aVal = a[dealSortField] || ''
let bVal = b[dealSortField] || ''
if (dealSortField === 'created_at') {
aVal = new Date(a.created_at).getTime()
bVal = new Date(b.created_at).getTime()
} else if (dealSortField === 'deal_price') {
aVal = a.deal_price || 0
bVal = b.deal_price || 0
}
if (dealSortOrder === 'asc') return aVal > bVal ? 1 : -1
return aVal < bVal ? 1 : -1
})
const fetchCollections = async () => {
setLoading(true)
const token = localStorage.getItem('token')
try {
// 从URL获取筛选参数
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
const urlFilterType = params.get('filter') || ''
const urlFilterValue = params.get('value') ? decodeURIComponent(params.get('value')) : ''
let api = '/api/collections?page=' + page + '&limit=100&sortBy=' + sortField + '&sortOrder=' + sortOrder
if (urlFilterType && urlFilterValue) {
api += '&' + urlFilterType + '=' + encodeURIComponent(urlFilterValue)
}
const res = await fetch(api, {
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
})
// 处理 401 未授权错误
if (res.status === 401) {
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.hash = '#/login'
return
}
const data = await res.json()
// 新格式直接返回数组或 {data: [], pagination: {}}
let list = data.data || data
if (!Array.isArray(list) && list && Array.isArray(list.items)) {
list = list.items
}
// 应用筛选条件
// 用户ID筛选从URL获取
if (userIdFilter) {
list = list.filter(item => item.userId === userIdFilter || item.userId === userIdFilter.replace(/-/g, ''))
}
if (filterType && filter) {
const fieldMap = {
status: 'status',
category: 'category',
packaging: 'packaging',
rarity: 'rarity',
numberCategory: 'numberCategory',
version: 'version',
gradingCompany: 'gradingCompany',
gradingScore: 'gradingScore',
specialMark: 'specialMark',
profitLoss: 'profitLoss'
}
const field = fieldMap[filterType] || filterType
if (filterType === 'profitLoss') {
// 盈亏筛选:需要计算出售价和总成本
list = list.filter(item => {
if (item.status !== 'sold') return false // 只筛选已售
const totalCost = (item.costPrice || 0) + (item.repairFee || 0) + (item.gradingFee || 0)
const isProfit = item.goalPrice > totalCost
return filter === 'profit' ? isProfit : !isProfit
})
} else {
list = list.filter(item => {
const value = item[field] || item[filterType]
return value === filter
})
}
console.log(`筛选:${filterType} = ${filter}, 结果:${list.length}`)
}
setCollections(list || [])
// 获取分页信息
if (data.pagination) {
setPagination({
total: data.pagination.total || 0,
pages: data.pagination.pages || 1
})
}
} catch (e) {
console.error('Fetch collections error:', e)
// 网络错误也跳转到登录页
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.hash = '#/login'
}
setLoading(false)
}
// 监听页码变化,重新获取数据
useEffect(() => {
if (page > 1) {
fetchCollections()
}
}, [page])
const refresh = () => {
setKey(k => k + 1)
}
useEffect(() => {
window.refreshList = refresh
return () => { delete window.refreshList }
}, [])
const goDetail = (id) => {
// 保存当前列表的URL包含筛选条件用于返回时恢复
sessionStorage.setItem('lastListUrl', window.location.hash.substring(1))
window.location.hash = '#/detail?id=' + id
}
// 获取筛选字段的中文标签
const getFilterLabel = (type) => {
const labels = {
status: '状态',
category: '持仓类型',
packaging: '包装',
rarity: '珍惜度',
version: '版别',
gradingCompany: '评级公司',
gradingScore: '评级分数',
specialMark: '特殊标识',
profitLoss: '盈亏'
}
return labels[type] || type
}
// 获取筛选条件值的中文显示
const getFilterValueLabel = (type, value) => {
const valueLabels = {
status: {
in_collection: '收藏中',
selling: '出售中',
sold: '已售',
grading: '送评中',
repairing: '修复中',
transit: '在途中',
seeking: '寻号中',
other: '其他'
},
category: {
自持: '自持',
寄存: '寄存',
寄售: '寄售',
共有: '共有',
寻号: '寻号',
其他: '其他'
},
packaging: {
标十: '标十',
标百: '标百',
单张: '单张',
裸钞: '裸钞'
},
rarity: {
通货: '通货',
特色: '特色',
少见: '少见',
稀有: '稀有',
珍品: '珍品',
孤品: '孤品'
},
profitLoss: {
profit: '盈利',
loss: '亏损'
},
isGraded: {
true: '已评级',
false: '未评级'
}
}
const typeLabels = valueLabels[type]
if (typeLabels) {
return typeLabels[value] || value
}
return value
}
const versions = collections && collections.length ? [...new Set(collections.map(c => c.version).filter(v => v))] : []
// 筛选和排序
const filteredCollections = collections.filter(c => {
// 筛选条件
if (filter && filterType) {
if (filterType === 'profitLoss') {
if (c.status !== 'sold') return false
if (filter === 'profit') return c.goalPrice > c.costPrice
return c.goalPrice <= c.costPrice
} else if (filterType === 'isGraded') {
if (c.isGraded !== (filter === 'true')) return false
} else if (c[filterType] !== filter) {
return false
}
}
// 搜索 - 全字段搜索
if (search) {
const s = search.toLowerCase().trim()
// 收集所有可搜索字段
const allFields = [
// 基本信息
c.name, c.code, c.prefixSerial, c.version,
c.status, c.category, c.packaging, c.rarity,
// 评级信息
c.gradingCompany, c.gradingScore, c.specialMark,
c.isGraded ? '已评级' : '未评级',
c.threeStar ? '三星' : '',
// 价格信息
c.targetPrice?.toString(), c.costPrice?.toString(), c.goalPrice?.toString(),
c.repairFee?.toString(), c.gradingFee?.toString(),
// 其他
c.remark, c.purpose, c.material, c.denomination,
c.issueYear, c.issueQuantity, c.serialFeature, c.issuer,
// 用户信息
c.username || '', c.userId || ''
].filter(v => v !== undefined && v !== null).map(v => v.toString().toLowerCase())
if (!allFields.some(f => f.includes(s))) {
return false
}
}
return true
}).sort((a, b) => {
let aVal = a[sortField]
let bVal = b[sortField]
if (sortField === 'createdAt') {
aVal = new Date(a.createdAt || 0).getTime()
bVal = new Date(b.createdAt || 0).getTime()
} else if (sortField === 'code') {
// 编号按数字排序,提取数字部分
aVal = parseInt(a.code?.replace(/\D/g, '') || '0', 10)
bVal = parseInt(b.code?.replace(/\D/g, '') || '0', 10)
} else if (sortField === 'prefixSerial') {
// 冠字号按字母排序
aVal = a.prefixSerial || ''
bVal = b.prefixSerial || ''
} else if (['costPrice', 'targetPrice', 'goalPrice', 'gradingScore'].includes(sortField)) {
// 价格和分数按数字排序
aVal = parseFloat(aVal) || 0
bVal = parseFloat(bVal) || 0
} else if (sortField === 'rarity') {
// 珍惜度按等级排序
const rarityOrder = { '通货': 1, '特色': 2, '少见': 3, '稀有': 4, '珍品': 5, '孤品': 6 }
aVal = rarityOrder[aVal] || 0
bVal = rarityOrder[bVal] || 0
} else if (sortField === 'numberCategory') {
// 号码分类按自定义顺序排序
const numberCategoryOrder = { '无2347': 1, '无347': 2, '无247': 3, '无47': 4, '带7号': 5, '带4号': 6, '其他': 7 }
aVal = numberCategoryOrder[aVal] || 99
bVal = numberCategoryOrder[bVal] || 99
}
if (aVal == null) return 1
if (bVal == null) return -1
if (sortOrder === 'asc') {
return aVal > bVal ? 1 : -1
}
return aVal < bVal ? 1 : -1
})
const clearFilter = () => {
setFilter('')
setFilterType('')
window.location.hash = '#/stats'
}
const getStatusText = (status) => {
const map = {
'in_collection': '收藏中',
'selling': '出售中',
'sold': '已售',
'grading': '送评中',
'repairing': '修复中',
'transit': '在途中',
'seeking': '寻号中',
'other': '其他'
}
return map[status] || status || '-'
}
const getCategoryText = (category) => {
const map = { '自持': '自持', '寄存': '寄存', '寄售': '寄售', '共有': '共有', '其他': '其他' }
return map[category] || category || '自持'
}
const getCategoryColor = (category) => {
const colors = { '自持': '#22c55e', '寄存': '#3b82f6', '寄售': '#f59e0b', '共有': '#8b5cf6', '其他': '#64748b' }
return colors[category] || '#64748b'
}
const getNumberCategoryColor = (cat) => {
const colors = { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '带7号': '#06b6d4', '带4号': '#22c55e', '其他': '#64748b' };
return colors[cat] || '#64748b';
};
const getRarityColor = (rarity) => {
const colors = { '通货': '#22c55e', '特色': '#06b6d4', '少见': '#3b82f6', '稀有': '#ec4899', '珍品': '#ef4444', '孤品': '#8b5cf6' }
return colors[rarity] || '#64748b'
}
const formatPrefixSerial = (serial) => {
if (!serial) return '-'
// 提取J开头的10位1位J + 9位数字
const match = serial.match(/J(\d{9})/)
if (match) return 'J' + match[1]
// 如果没有J开头取前10位
return serial.substring(0, 10)
}
const getPackagingColor = (packaging) => {
const colors = { '裸钞': '#22c55e', '单张': '#3b82f6', '标十': '#fbbf24', '标百': '#8b5cf6' }
return colors[packaging] || '#64748b'
}
const getStatusColor = (status) => {
const colors = {
'in_collection': '#22c55e',
'selling': '#f59e0b',
'sold': '#ef4444',
'grading': '#8b5cf6',
'repairing': '#f97316',
'transit': '#06b6d4',
'seeking': '#ec4899',
'other': '#64748b'
}
return colors[status] || '#64748b'
}
const getVersionColor = (version) => {
if (!version) return { bg: 'rgba(255,255,255,0.08)', color: '#94a3b8' }
const v = version.toLowerCase()
if (v.includes('龙')) return { bg: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)', color: '#1e293b' }
if (v.includes('蛇')) return { bg: 'linear-gradient(135deg, #ec4899 0%, #db2777 100%)', color: '#fff' }
if (v.includes('马')) return { bg: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', color: '#fff' }
if (v.includes('羊')) return { bg: 'linear-gradient(135deg, #f97316 0%, #ea580c 100%)', color: '#fff' }
if (v.includes('猴')) return { bg: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)', color: '#fff' }
if (v.includes('鸡')) return { bg: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', color: '#1e293b' }
if (v.includes('狗')) return { bg: 'linear-gradient(135deg, #6366f1 0%, #4f46e5 100%)', color: '#fff' }
if (v.includes('猪')) return { bg: 'linear-gradient(135deg, #ec4899 0%, #be185d 100%)', color: '#fff' }
if (v.includes('鼠')) return { bg: 'linear-gradient(135deg, #64748b 0%, #475569 100%)', color: '#fff' }
if (v.includes('牛')) return { bg: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)', color: '#fff' }
if (v.includes('虎')) return { bg: 'linear-gradient(135deg, #f97316 0%, #ea580c 100%)', color: '#fff' }
if (v.includes('兔')) return { bg: 'linear-gradient(135deg, #f43f5e 0%, #e11d48 100%)', color: '#fff' }
return { bg: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)', color: '#1e293b' }
}
const ListItem = ({ item }) => (
<div onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '10px', marginBottom: '8px', cursor: 'pointer' }}>
{/* 第1行编号 + 冠字号 + 包装(蓝色) | 状态 + 持仓类型 + 珍惜度(紫色) */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flexWrap: 'wrap' }}>
<span style={{ color: '#fff', fontSize: '11px', fontWeight: 'normal' }}>{item.code || '-'}</span>
<span style={{ color: '#fbbf24', fontSize: '14px', fontFamily: 'monospace', }}>{formatPrefixSerial(item.prefixSerial)}</span>
{item.packaging && <span style={{ background: getPackagingColor(item.packaging) + '20', color: getPackagingColor(item.packaging), fontSize: '10px', padding: '1px 5px', borderRadius: '3px' }}>{item.packaging}</span>}
{item.numberCategory && <span style={{ color: getNumberCategoryColor(item.numberCategory), fontSize: '9px', padding: '1px 4px', background: getNumberCategoryColor(item.numberCategory) + '20', borderRadius: '2px', marginLeft: '4px' }}>{item.numberCategory}</span>}
</div>
<div style={{ display: 'flex', gap: '3px' }}>
{item.status && <span style={{ color: getStatusColor(item.status), fontSize: '9px', padding: '1px 4px', background: getStatusColor(item.status) + '25', borderRadius: '2px' }}>{getStatusText(item.status)}</span>}
{item.category && <span style={{ color: getCategoryColor(item.category), fontSize: '9px', padding: '1px 4px', background: getCategoryColor(item.category) + '25', borderRadius: '2px' }}>{getCategoryText(item.category)}</span>}
</div>
</div>
{/* 第2行版本(彩色) + 已评级 + 评级公司 + 评级分数 + 三星 + 特殊标识 | 备注 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '2px', flexWrap: 'wrap', marginBottom: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '2px', flexWrap: 'wrap' }}>
{item.version && <span style={{ background: getVersionColor(item.version).bg, color: getVersionColor(item.version).color, fontSize: '10px', padding: '1px 5px', borderRadius: '3px' }}>{item.version}</span>}
{item.isGraded && <span style={{ background: 'rgba(6, 182, 212, 0.15)', color: '#06b6d4', fontSize: '9px', padding: '1px 4px', borderRadius: '2px' }}>已评级</span>}
{item.gradingCompany && <span style={{ background: 'rgba(6, 182, 212, 0.15)', color: '#06b6d4', fontSize: '9px', padding: '1px 4px', borderRadius: '2px' }}>{item.gradingCompany.substring(0,4)}</span>}
{item.gradingScore && <span style={{ background: 'rgba(249, 115, 22, 0.2)', color: '#f97316', fontSize: '9px', padding: '1px 4px', borderRadius: '2px', }}>{item.gradingScore}</span>}
{item.threeStar && <span style={{ background: 'rgba(6, 182, 212, 0.15)', color: '#06b6d4', fontSize: '9px', padding: '1px 4px', borderRadius: '2px' }}>三星</span>}
{item.specialMark && <span style={{ background: 'rgba(139, 92, 246, 0.15)', color: '#a78bfa', fontSize: '9px', padding: '1px 4px', borderRadius: '2px' }}>{item.specialMark}</span>}
</div>
<div style={{ display: 'flex', gap: '2px', marginLeft: 'auto' }}>
{item.rarity && <span style={{ color: getRarityColor(item.rarity), fontSize: '9px', padding: '1px 4px', background: getRarityColor(item.rarity) + '20', borderRadius: '2px' }}>{item.rarity}</span>}
{item.remark && <span style={{ background: 'rgba(100,116,139,0.2)', color: '#94a3b8', fontSize: '9px', padding: '1px 4px', borderRadius: '2px', maxWidth: '80px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{item.remark}</span>}
</div>
</div>
{/* 第3行成本 + 修复 + 评级 | 目标 + 出售 */}
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '10px', color: '#94a3b8', paddingTop: '4px', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'flex', gap: '8px' }}>
{item.costPrice && <span>成本: <span style={{ color: '#e2e8f0' }}>¥{item.costPrice}</span></span>}
{item.repairFee && <span>修复: <span style={{ color: '#e2e8f0' }}>¥{item.repairFee}</span></span>}
{item.gradingFee && <span>评级: <span style={{ color: '#e2e8f0' }}>¥{item.gradingFee}</span></span>}
</div>
<div style={{ display: 'flex', gap: '8px' }}>
{item.targetPrice && <span>目标: <span style={{ color: '#fbbf24' }}>¥{item.targetPrice}</span></span>}
{item.goalPrice && <span>出售: <span style={{ color: '#4ade80' }}>¥{item.goalPrice}</span></span>}
</div>
</div>
</div>
)
return (
<div style={{ minHeight: '100vh', overflowY: 'auto', background: '#0f172a', paddingBottom: '70px' }}>
<div style={{ padding: '20px', background: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style={{ color: '#fff', fontSize: '20px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => setActiveTab('collections')}
style={{ padding: '6px 16px', background: activeTab === 'collections' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'collections' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
我的藏品 ({filteredCollections.length})
</button>
<button onClick={() => setActiveTab('deals')}
style={{ padding: '6px 16px', background: activeTab === 'deals' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'deals' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
我的行情 ({filteredDeals.length})
</button>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{filter && filterType && (
<button
onClick={() => {
setFilter('')
setFilterType('')
window.location.hash = '#/list'
}}
style={{ background: 'rgba(239, 68, 68, 0.2)', color: '#ef4444', border: 'none', borderRadius: '6px', padding: '4px 10px', fontSize: '12px', cursor: 'pointer' }}
>
清除筛选
</button>
)}
<div style={{ color: 'rgba(255,255,255,0.3)', fontSize: '11px' }}>v{APP_VERSION}</div>
</div>
</div>
{filter && filterType && (
<div style={{ background: 'rgba(59, 130, 246, 0.1)', border: '1px solid rgba(59, 130, 246, 0.3)', borderRadius: '8px', padding: '12px', marginBottom: '16px' }}>
<div style={{ color: '#60a5fa', fontSize: '12px', }}>当前筛选</div>
<div style={{ color: '#fff', fontSize: '14px', marginTop: '4px' }}>
{getFilterLabel(filterType)} = <span style={{ color: '#fbbf24', }}>{getFilterValueLabel(filterType, filter)}</span>
</div>
</div>
)}
{activeTab === 'collections' && (
<>
{/* 搜索框 - 全字段搜索 */}
<div style={{ position: 'relative', marginBottom: '16px' }}>
<input type="text"
placeholder="🔍 搜索任意字段:名称、编号、冠字号、版别、评级公司、分数、价格..."
value={search}
onChange={e => setSearch(e.target.value)}
style={{
background: 'rgba(255,255,255,0.05)',
border: search ? '1px solid rgba(59, 130, 246, 0.5)' : '1px solid rgba(255,255,255,0.1)',
color: '#fff',
width: '100%',
boxSizing: 'border-box',
padding: '4px 40px 4px 8px',
borderRadius: '12px',
fontSize: '14px',
outline: 'none',
transition: 'border-color 0.2s'
}}
/>
{search && (
<button
onClick={() => setSearch('')}
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
background: 'rgba(255,255,255,0.1)',
border: 'none',
borderRadius: '50%',
width: '24px',
height: '24px',
color: '#fff',
fontSize: '16px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start'
}}
>
</button>
)}
</div>
{/* 排序表头按钮 */}
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap', marginBottom: '12px' }}>
<span style={{ color: '#94a3b8', fontSize: '12px', marginRight: '2px' }}>排序:</span>
{[
{ key: 'code', label: '编号' },
{ key: 'rarity', label: '珍惜度' },
{ key: 'numberCategory', label: '号码分类' },
{ key: 'packaging', label: '包装类型' },
].map(item => (
<div key={item.key} onClick={() => {
if (sortField === item.key) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
} else {
setSortField(item.key)
setSortOrder('desc')
}
}} style={{
padding: '4px 8px',
borderRadius: '8px',
fontSize: '12px',
cursor: 'pointer',
background: sortField === item.key ? (sortOrder === 'asc' ? '#22c55e' : '#fbbf24') : 'rgba(255,255,255,0.08)',
color: sortField === item.key ? '#fff' : '#94a3b8',
fontWeight: sortField === item.key ? 'bold' : 'normal',
border: sortField === item.key ? 'none' : '1px solid rgba(255,255,255,0.1)'
}}>
{item.label} {sortField === item.key && (sortOrder === 'asc' ? '↑' : '↓')}
</div>
))}
</div>
</>
)}
{/* 分页组件 - 仅在藏品tab下显示 */}
{activeTab === 'collections' && pagination.pages > 1 && (
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', justifyContent: 'flex-start', marginBottom: '12px', padding: '8px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px' }}>
<button onClick={() => { setPage(Math.max(1, page - 1)) }} disabled={page === 1} style={{ padding: '4px 10px', borderRadius: '6px', border: 'none', background: page === 1 ? 'rgba(255,255,255,0.1)' : '#3b82f6', color: '#fff', cursor: page === 1 ? 'not-allowed' : 'pointer', opacity: page === 1 ? 0.5 : 1 }}>上一页</button>
{Array.from({ length: Math.min(5, pagination.pages) }, (_, i) => {
let startPage = Math.max(1, page - 2)
return <button key={i} onClick={() => { setPage(startPage + i) }} style={{ padding: '4px 8px', borderRadius: '6px', border: 'none', background: page === startPage + i ? '#3b82f6' : 'rgba(255,255,255,0.08)', color: '#fff', cursor: 'pointer' }}>{startPage + i}</button>
})}
<button onClick={() => { setPage(Math.min(pagination.pages, page + 1)) }} disabled={page === pagination.pages} style={{ padding: '4px 10px', borderRadius: '6px', border: 'none', background: page === pagination.pages ? 'rgba(255,255,255,0.1)' : '#3b82f6', color: '#fff', cursor: page === pagination.pages ? 'not-allowed' : 'pointer', opacity: page === pagination.pages ? 0.5 : 1 }}>下一页</button>
<span style={{ color: '#94a3b8', fontSize: '11px', marginLeft: '8px' }}>{pagination.total}</span>
</div>
)}
</div>
<div style={{ padding: '16px' }}>
{/* 行情tab内容 */}
{activeTab === 'deals' && (
<div>
{/* 行情搜索框 */}
<div style={{ marginBottom: '12px' }}>
<input type="text"
placeholder="🔍 搜索行情..."
value={dealSearch}
onChange={e => setDealSearch(e.target.value)}
style={{
width: '100%',
background: 'rgba(255,255,255,0.05)',
border: '1px solid rgba(255,255,255,0.1)',
color: '#fff',
padding: '10px 12px',
borderRadius: '8px',
fontSize: '14px',
outline: 'none',
boxSizing: 'border-box'
}}
/>
</div>
{dealsLoading ? (
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>加载中...</div>
) : filteredDeals.length === 0 ? (
<div style={{ textAlign: 'center', padding: '60px 0' }}>
<div style={{ fontSize: '50px', opacity: 0.3 }}>📊</div>
<div style={{ color: '#64748b', marginTop: '16px' }}>{dealSearch ? '没有匹配的行情' : '暂无行情记录'}</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{filteredDeals.map(deal => (
<DealListItem key={deal.id} deal={deal} onRefresh={fetchMyDeals} />
))}
</div>
)}
</div>
)}
{/* 藏品tab内容 */}
{activeTab === 'collections' && (
<>
{loading ? (
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>加载中...</div>
) : filteredCollections.length === 0 ? (
<div style={{ textAlign: 'center', padding: '60px 0' }}><div style={{ fontSize: '50px', opacity: 0.3 }}>📭</div><div style={{ color: '#64748b', marginTop: '16px' }}>{filter ? '' : ''}</div></div>
) : viewMode === 'grid' ? (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}>
{filteredCollections.map(item => (
<div key={item.id} onClick={() => goDetail(item.id)} style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', borderRadius: '12px', padding: '12px', cursor: 'pointer' }}>
<div style={{ height: '80px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px', display: 'flex', alignItems: 'center', justifyContent: 'flex-start', fontSize: '32px', marginBottom: '10px' }}>🐉</div>
<div style={{ color: '#fff', fontSize: '11px', fontWeight: 'normal' }}>{item.code || '-'}</div>
<div style={{ color: '#fbbf24', fontSize: '11px', fontFamily: 'monospace', marginTop: '2px' }}>{formatPrefixSerial(item.prefixSerial)}</div>
<div style={{ display: 'flex', gap: '2px', marginTop: '6px', flexWrap: 'wrap' }}>
{item.gradingScore && <span style={{ color: '#fbbf24', fontSize: '11px', }}>{item.gradingScore}</span>}
{item.threeStar && <span style={{ color: '#fbbf24', fontSize: '10px' }}></span>}
</div>
</div>
))}
</div>
) : (
filteredCollections.map(item => <ListItem key={item.id} item={item} />)
)}
</>
)}
</div>
</div>
)
}
// 行情列表项组件
function DealListItem({ deal, onRefresh }) {
const [expanded, setExpanded] = useState(false)
const [editing, setEditing] = useState(false)
const [editForm, setEditForm] = useState({})
const API_BASE = localStorage.getItem('API_BASE') || ''
const openEdit = () => {
// 从content中解析平台、出售者、购买者
let platform = '', seller = '', buyer = ''
if (deal.content) {
const platformMatch = deal.content.match(/平台:\s*([^\n]+)/)
const sellerMatch = deal.content.match(/出售者:\s*([^\n]+)/)
const buyerMatch = deal.content.match(/购买者:\s*([^\n]+)/)
if (platformMatch) platform = platformMatch[1].trim()
if (sellerMatch) seller = sellerMatch[1].trim()
if (buyerMatch) buyer = buyerMatch[1].trim()
}
setEditForm({
title: deal.title,
content: deal.content,
deal_price: deal.deal_price,
deal_date: deal.deal_date ? (typeof deal.deal_date === 'string' ? deal.deal_date.split('T')[0] : '') : '',
packaging: deal.packaging || '单张',
is_graded: deal.is_graded || false,
grading_company: deal.grading_company || '',
grading_score: deal.grading_score || '',
category: deal.category || '',
deal_no: deal.deal_no || '',
platform: platform,
seller: seller,
buyer: buyer
})
setEditing(true)
}
const saveEdit = async () => {
const token = localStorage.getItem('token')
try {
await fetch(`${API_BASE}/api/deal/${deal.id}`, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(editForm)
})
setEditing(false)
onRefresh()
} catch(e) { alert('保存失败') }
}
const deleteDeal = async () => {
if (!confirm('确定删除这条行情?')) return
const token = localStorage.getItem('token')
try {
await fetch(`${API_BASE}/api/deal/${deal.id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
onRefresh()
} catch(e) { alert('删除失败') }
}
return (
<div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: '10px', padding: '12px', border: '1px solid rgba(255,255,255,0.1)' }}>
{/* 简要展示 - 两行显示关键信息 */}
<div onClick={() => setExpanded(!expanded)} style={{ cursor: 'pointer', padding: '10px', background: 'rgba(0,0,0,0.2)', borderRadius: '8px' }}>
{/* 第一行:成交日期(月-日)+ 冠字号 + 包装 + 评级分数 + 价格 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px', flexWrap: 'wrap', gap: '4px' }}>
{deal.deal_date && <span style={{ color: '#64748b', fontSize: '11px' }}>{deal.deal_date.slice(5)}</span>}
<span style={{ color: '#fbbf24', fontSize: '13px', fontFamily: 'monospace' }}>{deal.title?.split('-')[0] || '-'}</span>
{deal.packaging && <span style={{ background: 'rgba(139,92,246,0.15)', color: '#a78bfa', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{deal.packaging}</span>}
{deal.grading_company && <span style={{ background: 'rgba(6,182,212,0.15)', color: '#06b6d4', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{deal.grading_score}</span>}
<span style={{ color: '#22c55e', fontSize: '14px', fontWeight: 'bold' }}>¥{deal.deal_price?.toLocaleString()}</span>
</div>
{/* 第二行:编号 + 分类 + 大小号 + 展开箭头 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
{deal.deal_no && <span style={{ color: '#fff', fontSize: '10px' }}>{deal.deal_no}</span>}
{deal.category && <span style={{ background: 'rgba(59,130,246,0.15)', color: '#60a5fa', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{deal.category}</span>}
{deal.content && (() => {
const sizeMatch = deal.content.match(/大小号:\s*([^\n]+)/)
return sizeMatch && <span style={{ background: 'rgba(34,197,94,0.15)', color: '#22c55e', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{sizeMatch[1]}</span>
})()}
</div>
<span style={{ color: '#64748b', fontSize: '12px' }}>{expanded ? '▲ 收起' : '▼展开'}</span>
</div>
</div>
{/* 展开详情 */}
{expanded && (
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
{editing ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{/* 冠字号 */}
<div>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>冠字号</div>
<input value={editForm.title?.split('-')[0] || ''} onChange={e => setEditForm({...editForm, title: `${e.target.value}${editForm.deal_price}`})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fbbf24', fontSize: '14px', fontFamily: 'monospace' }} />
</div>
{/* 价格和日期 */}
<div style={{ display: 'flex', gap: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>价格</div>
<input type="number" value={editForm.deal_price} onChange={e => {
const val = parseFloat(e.target.value) || 0
const serial = editForm.title?.split('-')[0] || ''
setEditForm({...editForm, deal_price: val, title: `${serial}${val}`})
}} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#22c55e', fontSize: '14px' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>日期</div>
<input type="date" value={editForm.deal_date} onChange={e => setEditForm({...editForm, deal_date: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '14px' }} />
</div>
</div>
{/* 包装和分类 */}
<div style={{ display: 'flex', gap: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>包装</div>
<select value={editForm.packaging} onChange={e => setEditForm({...editForm, packaging: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
<option value="单张">单张</option>
<option value="标十">标十</option>
<option value="标百">标百</option>
</select>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>分类</div>
<input value={editForm.category || ''} onChange={e => setEditForm({...editForm, category: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
</div>
</div>
{/* 评级 */}
<div style={{ display: 'flex', gap: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>评级机构</div>
<select value={editForm.grading_company || ''} onChange={e => setEditForm({...editForm, grading_company: e.target.value, is_graded: !!e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
<option value="">未评级</option>
<option value="PCGS">PCGS</option>
<option value="PMG">PMG</option>
<option value="ACG">ACG</option>
<option value="爱藏">爱藏</option>
</select>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>评级分数</div>
<input value={editForm.grading_score || ''} onChange={e => setEditForm({...editForm, grading_score: e.target.value})} placeholder="如: PC69, 67+" style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#06b6d4', fontSize: '13px' }} />
</div>
</div>
{/* 平台和出售者购买者 */}
<div>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>平台</div>
<select value={editForm.platform || ''} onChange={e => setEditForm({...editForm, platform: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
<option value="">请选择</option>
<option value="淘宝">淘宝</option>
<option value="咸鱼">咸鱼</option>
<option value="抖音">抖音</option>
<option value="快手">快手</option>
<option value="微拍堂">微拍堂</option>
<option value="拼多多">拼多多</option>
<option value="一尘">一尘</option>
<option value="爱藏">爱藏</option>
<option value="其他">其他</option>
</select>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>出售者</div>
<input value={editForm.seller || ''} onChange={e => setEditForm({...editForm, seller: e.target.value})} placeholder="请输入出售者" style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>购买者</div>
<input value={editForm.buyer || ''} onChange={e => setEditForm({...editForm, buyer: e.target.value})} placeholder="请输入购买者" style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
</div>
</div>
{/* 备注 */}
<div>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>备注</div>
<textarea value={editForm.content || ''} onChange={e => setEditForm({...editForm, content: e.target.value})} rows={2} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
</div>
{/* 保存取消按钮 */}
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={saveEdit} style={{ flex: 1, padding: '12px', background: '#22c55e', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>保存</button>
<button onClick={() => setEditing(false)} style={{ flex: 1, padding: '12px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>取消</button>
</div>
</div>
) : (
<div>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '8px' }}>
{deal.deal_no && <div>编号: <span style={{ color: '#fbbf24' }}>{deal.deal_no}</span></div>}
<div>冠字号: <span style={{ color: '#fbbf24' }}>{deal.title?.split('-')[0] || '-'}</span></div>
<div>价格: <span style={{ color: '#22c55e' }}>¥{deal.deal_price?.toLocaleString()}</span></div>
<div>日期: {deal.deal_date}</div>
<div>包装: {deal.packaging || '单张'}</div>
<div>分类: {deal.category || '-'}</div>
<div>评级: {deal.grading_company ? `${deal.grading_company} ${deal.grading_score || ''}` : '未评级'}</div>
{deal.content && (() => {
const platformMatch = deal.content.match(/平台:\s*([^\n]+)/)
const sellerMatch = deal.content.match(/出售者:\s*([^\n]+)/)
const buyerMatch = deal.content.match(/购买者:\s*([^\n]+)/)
const sizeMatch = deal.content.match(/大小号:\s*([^\n]+)/)
const tailMatch = deal.content.match(/尾号:\s*([^\n]+)/)
return (
<div style={{ marginTop: '4px' }}>
{platformMatch && <div>平台: {platformMatch[1]}</div>}
{sellerMatch && <div>出售者: {sellerMatch[1]}</div>}
{buyerMatch && buyerMatch[1].trim() !== '-' && <div>购买者: {buyerMatch[1]}</div>}
{tailMatch && <div>尾号: {tailMatch[1]}</div>}
{sizeMatch && <div>大小号: <span style={{ color: '#60a5fa' }}>{sizeMatch[1]}</span></div>}
</div>
)
})()}
{deal.content && <div style={{ marginTop: '8px', color: '#64748b', fontSize: '11px' }}>{deal.content}</div>}
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
<button onClick={openEdit} style={{ flex: 1, padding: '8px', background: 'rgba(59,130,246,0.2)', color: '#60a5fa', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '12px' }}>编辑</button>
<button onClick={deleteDeal} style={{ flex: 1, padding: '8px', background: 'rgba(239,68,68,0.2)', color: '#ef4444', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '12px' }}>删除</button>
</div>
</div>
)}
</div>
)}
</div>
)
}