2026-03-16 11:39:39 +08:00
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
|
|
|
|
import { APP_VERSION } from '../config/version'
|
|
|
|
|
|
|
|
|
|
|
|
export default function List() {
|
|
|
|
|
|
const [collections, setCollections] = useState([])
|
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
|
const [filter, setFilter] = useState('')
|
|
|
|
|
|
const [filterType, setFilterType] = useState('')
|
2026-03-20 15:03:26 +08:00
|
|
|
|
const urlParams = new URLSearchParams(window.location.hash.split('?')[1] || '')
|
|
|
|
|
|
const urlUserId = urlParams.get('userId') || ''
|
|
|
|
|
|
const [userIdFilter, setUserIdFilter] = useState(urlUserId)
|
2026-03-16 11:39:39 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
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 fetchCollections = async () => {
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
const token = localStorage.getItem('token')
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch('/api/collections?limit=100', {
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 应用筛选条件
|
2026-03-20 15:03:26 +08:00
|
|
|
|
// 用户ID筛选(从URL获取)
|
|
|
|
|
|
if (userIdFilter) {
|
|
|
|
|
|
list = list.filter(item => item.userId === userIdFilter || item.userId === userIdFilter.replace(/-/g, ''))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 11:39:39 +08:00
|
|
|
|
if (filterType && filter) {
|
|
|
|
|
|
const fieldMap = {
|
|
|
|
|
|
status: 'status',
|
|
|
|
|
|
category: 'category',
|
|
|
|
|
|
packaging: 'packaging',
|
|
|
|
|
|
rarity: 'rarity',
|
|
|
|
|
|
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 || [])
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('Fetch collections error:', e)
|
|
|
|
|
|
// 网络错误也跳转到登录页
|
|
|
|
|
|
localStorage.removeItem('token')
|
|
|
|
|
|
localStorage.removeItem('user')
|
|
|
|
|
|
window.location.hash = '#/login'
|
|
|
|
|
|
}
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const refresh = () => {
|
|
|
|
|
|
setKey(k => k + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
window.refreshList = refresh
|
|
|
|
|
|
return () => { delete window.refreshList }
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const goDetail = (id) => {
|
2026-03-18 17:14:24 +08:00
|
|
|
|
// 保存当前列表的URL(包含筛选条件),用于返回时恢复
|
|
|
|
|
|
sessionStorage.setItem('lastListUrl', window.location.hash.substring(1))
|
2026-03-16 11:39:39 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
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'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 18:26:21 +08:00
|
|
|
|
const getRarityColor = (rarity) => {
|
|
|
|
|
|
const colors = { '通货': '#22c55e', '特色': '#06b6d4', '少见': '#3b82f6', '稀有': '#ec4899', '珍品': '#ef4444', '孤品': '#8b5cf6' }
|
|
|
|
|
|
return colors[rarity] || '#64748b'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getPackagingColor = (packaging) => {
|
|
|
|
|
|
const colors = { '裸钞': '#22c55e', '单张': '#3b82f6', '标十': '#fbbf24', '标百': '#8b5cf6' }
|
|
|
|
|
|
return colors[packaging] || '#64748b'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 11:39:39 +08:00
|
|
|
|
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 }) => (
|
2026-03-20 18:26:21 +08:00
|
|
|
|
<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' }}>
|
2026-03-20 22:02:12 +08:00
|
|
|
|
<span style={{ color: '#fff', fontSize: '13px', fontWeight: '500' }}>{item.code || '-'}</span>
|
|
|
|
|
|
<span style={{ color: '#fbbf24', fontSize: '11px', fontFamily: 'monospace' }}>{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>}
|
2026-03-16 11:39:39 +08:00
|
|
|
|
</div>
|
2026-03-20 18:26:21 +08:00
|
|
|
|
<div style={{ display: 'flex', gap: '3px' }}>
|
2026-03-20 22:02:12 +08:00
|
|
|
|
{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>}
|
|
|
|
|
|
{item.rarity && <span style={{ color: getRarityColor(item.rarity), fontSize: '9px', padding: '1px 4px', background: getRarityColor(item.rarity) + '20', borderRadius: '2px' }}>{item.rarity}</span>}
|
2026-03-16 11:39:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-20 18:26:21 +08:00
|
|
|
|
{/* 第2行:版本(彩色) + 已评级 + 评级公司 + 评级分数 + 三星 + 特殊标识 | 备注 */}
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '4px', flexWrap: 'wrap', marginBottom: '4px' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'wrap' }}>
|
2026-03-20 22:02:12 +08:00
|
|
|
|
{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', fontWeight: 'bold' }}>{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>}
|
2026-03-20 18:26:21 +08:00
|
|
|
|
</div>
|
2026-03-20 22:02:12 +08:00
|
|
|
|
{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>}
|
2026-03-16 11:39:39 +08:00
|
|
|
|
</div>
|
2026-03-20 18:26:21 +08:00
|
|
|
|
{/* 第3行:成本 + 修复 + 评级 | 目标 + 出售 */}
|
2026-03-20 22:02:12 +08:00
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '10px', color: '#94a3b8', paddingTop: '4px', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
|
2026-03-20 18:26:21 +08:00
|
|
|
|
<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>
|
2026-03-16 11:39:39 +08:00
|
|
|
|
</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', fontWeight: 'bold' }}>我的藏品 <span style={{ fontSize: '14px', color: '#fbbf24' }}>({filteredCollections.length})</span></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: '6px 12px', fontSize: '13px', 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: '13px', fontWeight: 'bold' }}>当前筛选:</div>
|
|
|
|
|
|
<div style={{ color: '#fff', fontSize: '14px', marginTop: '4px' }}>
|
|
|
|
|
|
{getFilterLabel(filterType)} = <span style={{ color: '#fbbf24', fontWeight: 'bold' }}>{getFilterValueLabel(filterType, filter)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 搜索框 - 全字段搜索 */}
|
|
|
|
|
|
<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: '12px 40px 12px 12px',
|
|
|
|
|
|
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: 'center'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
✕
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 排序表头按钮 */}
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap', marginBottom: '12px' }}>
|
|
|
|
|
|
<span style={{ color: '#94a3b8', fontSize: '13px', marginRight: '4px' }}>排序:</span>
|
|
|
|
|
|
{[
|
|
|
|
|
|
{ key: 'code', label: '编号' },
|
|
|
|
|
|
{ key: 'prefixSerial', label: '冠字号' },
|
|
|
|
|
|
{ key: 'costPrice', label: '成本' },
|
|
|
|
|
|
{ key: 'goalPrice', label: '售价' },
|
|
|
|
|
|
{ key: 'category', label: '持仓类型' },
|
|
|
|
|
|
{ key: 'rarity', label: '珍惜度' },
|
|
|
|
|
|
{ key: 'gradingScore', 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: '8px 14px',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ padding: '16px' }}>
|
|
|
|
|
|
{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: 'center', fontSize: '32px', marginBottom: '10px' }}>🐉</div>
|
|
|
|
|
|
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '500' }}>{item.code || '-'}</div>
|
|
|
|
|
|
<div style={{ color: '#fbbf24', fontSize: '12px', fontFamily: 'monospace', marginTop: '2px' }}>{item.prefixSerial || '-'}</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '4px', marginTop: '6px', flexWrap: 'wrap' }}>
|
|
|
|
|
|
{item.gradingScore && <span style={{ color: '#fbbf24', fontSize: '12px', fontWeight: 'bold' }}>{item.gradingScore}</span>}
|
2026-03-20 22:02:12 +08:00
|
|
|
|
{item.threeStar && <span style={{ color: '#fbbf24', fontSize: '10px' }}>⭐⭐⭐</span>}
|
2026-03-16 11:39:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
filteredCollections.map(item => <ListItem key={item.id} item={item} />)
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|