2026-04-19 21:34:14 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Stats - 统计页面
|
2026-04-19 21:58:28 +08:00
|
|
|
|
* Version: 1.2.70x
|
2026-04-19 21:34:14 +08:00
|
|
|
|
* 更新:
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-23 11:08:52 +08:00
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
|
|
|
|
import { APP_VERSION } from '../config/version'
|
|
|
|
|
|
|
|
|
|
|
|
export default function Stats() {
|
|
|
|
|
|
const [stats, setStats] = useState({
|
|
|
|
|
|
totalCount: 0,
|
|
|
|
|
|
byCategory: [],
|
|
|
|
|
|
byStatus: [],
|
|
|
|
|
|
byGrading: [],
|
|
|
|
|
|
byPackaging: [],
|
|
|
|
|
|
byRarity: [],
|
|
|
|
|
|
byNumberCategory: [],
|
|
|
|
|
|
byVersion: [],
|
|
|
|
|
|
byGradingCompany: [],
|
|
|
|
|
|
byGradingScore: [],
|
|
|
|
|
|
bySpecialMark: [],
|
|
|
|
|
|
byProfitLoss: [],
|
|
|
|
|
|
totalCost: 0,
|
|
|
|
|
|
totalTarget: 0,
|
|
|
|
|
|
expectedProfit: 0,
|
|
|
|
|
|
totalRevenue: 0,
|
|
|
|
|
|
totalProfit: 0
|
|
|
|
|
|
})
|
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchStats()
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
const fetchStats = async () => {
|
|
|
|
|
|
setLoading(true)
|
|
|
|
|
|
const token = localStorage.getItem('token')
|
|
|
|
|
|
try {
|
|
|
|
|
|
const statsRes = await fetch('/api/collections/stats', {
|
|
|
|
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!statsRes.ok) {
|
|
|
|
|
|
throw new Error(`HTTP ${statsRes.status}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await statsRes.json()
|
|
|
|
|
|
console.log('统计数据:', data)
|
|
|
|
|
|
|
|
|
|
|
|
setStats({
|
|
|
|
|
|
totalCount: data.totalCount || 0,
|
|
|
|
|
|
byCategory: data.byCategory || [],
|
|
|
|
|
|
byStatus: data.byStatus || [],
|
|
|
|
|
|
byGrading: data.byGrading || [],
|
|
|
|
|
|
byPackaging: data.byPackaging || [],
|
|
|
|
|
|
byRarity: data.byRarity || [],
|
|
|
|
|
|
byNumberCategory: data.byNumberCategory || [],
|
|
|
|
|
|
byVersion: data.byVersion || [],
|
|
|
|
|
|
byGradingCompany: data.byGradingCompany || [],
|
|
|
|
|
|
byGradingScore: data.byGradingScore || [],
|
|
|
|
|
|
bySpecialMark: data.bySpecialMark || [],
|
|
|
|
|
|
byProfitLoss: data.byProfitLoss || [],
|
|
|
|
|
|
totalCost: data.totalCost || 0,
|
|
|
|
|
|
totalTarget: data.totalTarget || 0,
|
|
|
|
|
|
expectedProfit: data.expectedProfit || 0,
|
|
|
|
|
|
totalRevenue: data.totalRevenue || 0,
|
|
|
|
|
|
totalProfit: data.totalProfit || 0
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('统计加载失败:', e)
|
|
|
|
|
|
alert('加载失败:' + e.message)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 点击统计项跳转到列表页
|
|
|
|
|
|
const handleItemClick = (type, value) => {
|
|
|
|
|
|
const filterKey = getFilterKey(type)
|
|
|
|
|
|
// 跳转到列表页并带上筛选条件
|
|
|
|
|
|
window.location.hash = `#/list?filter=${filterKey}&value=${encodeURIComponent(value)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 根据统计类型获取对应的筛选字段名
|
|
|
|
|
|
const getFilterKey = (type) => {
|
|
|
|
|
|
const map = {
|
|
|
|
|
|
status: 'status',
|
|
|
|
|
|
category: 'category',
|
|
|
|
|
|
packaging: 'packaging',
|
|
|
|
|
|
rarity: 'rarity',
|
|
|
|
|
|
version: 'version',
|
|
|
|
|
|
gradingCompany: 'gradingCompany',
|
|
|
|
|
|
gradingScore: 'gradingScore',
|
|
|
|
|
|
specialMark: 'specialMark',
|
|
|
|
|
|
numberCategory: 'numberCategory',
|
|
|
|
|
|
gradingCompany: 'gradingCompany',
|
|
|
|
|
|
gradingScore: 'gradingScore',
|
|
|
|
|
|
profitLoss: 'profitLoss'
|
|
|
|
|
|
}
|
|
|
|
|
|
return map[type] || type
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 颜色配置
|
|
|
|
|
|
const colors = {
|
|
|
|
|
|
packaging: { '标十': '#22c55e', '标百': '#3b82f6', '单张': '#f59e0b', '裸钞': '#64748b' },
|
|
|
|
|
|
rarity: { '通货': '#64748b', '特色': '#22c55e', '少见': '#3b82f6', '稀有': '#8b5cf6', '珍品': '#ef4444', '孤品': '#fbbf24' },
|
|
|
|
|
|
status: { 'in_collection': '#22c55e', 'selling': '#f59e0b', 'sold': '#ef4444', 'grading': '#8b5cf6', 'repairing': '#f97316', 'transit': '#06b6d4', 'seeking': '#ec4899' },
|
|
|
|
|
|
category: { '自持': '#22c55e', '寄存': '#3b82f6', '寄售': '#f59e0b', '共有': '#8b5cf6', '寻号': '#06b6d4', '生肖钞': '#22c55e', '其他': '#64748b' },
|
|
|
|
|
|
profitLoss: { 'profit': '#22c55e', 'loss': '#ef4444' },
|
2026-04-11 18:38:03 +08:00
|
|
|
|
numberCategory: { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '带7号': '#06b6d4', '带4号': '#22c55e', '其他': '#64748b' },
|
2026-03-23 11:08:52 +08:00
|
|
|
|
version: {},
|
|
|
|
|
|
gradingCompany: {},
|
|
|
|
|
|
gradingScore: {},
|
|
|
|
|
|
specialMark: {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标签映射
|
|
|
|
|
|
const labels = {
|
|
|
|
|
|
status: {
|
|
|
|
|
|
'in_collection': '收藏中',
|
|
|
|
|
|
'selling': '出售中',
|
|
|
|
|
|
'sold': '已售',
|
|
|
|
|
|
'grading': '送评中',
|
|
|
|
|
|
'repairing': '修复中',
|
|
|
|
|
|
'transit': '在途中',
|
|
|
|
|
|
'seeking': '寻号中'
|
|
|
|
|
|
},
|
|
|
|
|
|
profitLoss: {
|
|
|
|
|
|
'profit': '盈利',
|
|
|
|
|
|
'loss': '亏损'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const colorPalette = ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#f97316', '#14b8a6', '#a855f7']
|
|
|
|
|
|
const getColor = (type, value) => {
|
|
|
|
|
|
if (colors[type]?.[value]) return colors[type][value]
|
|
|
|
|
|
// 动态生成颜色
|
|
|
|
|
|
const key = String(value)
|
|
|
|
|
|
let hash = 0
|
|
|
|
|
|
for (let i = 0; i < key.length; i++) hash = key.charCodeAt(i) + ((hash << 5) - hash)
|
|
|
|
|
|
return colorPalette[Math.abs(hash) % colorPalette.length]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getLabel = (type, value) => {
|
|
|
|
|
|
return labels[type]?.[value] || value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formatMoney = (val) => {
|
|
|
|
|
|
if (val === null || val === undefined) return '0'
|
|
|
|
|
|
return Number(val).toLocaleString('zh-CN')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-11 18:38:03 +08:00
|
|
|
|
const numberCategoryOrder = ['无2347', '无347', '无247', '无47', '带7号', '带4号', '其他']
|
2026-03-23 11:08:52 +08:00
|
|
|
|
const getSortedData = (data, type) => {
|
|
|
|
|
|
if (type === 'numberCategory') {
|
|
|
|
|
|
return [...data].sort((a, b) => {
|
|
|
|
|
|
const order = numberCategoryOrder.indexOf(a.numberCategory)
|
|
|
|
|
|
const order2 = numberCategoryOrder.indexOf(b.numberCategory)
|
|
|
|
|
|
return order - order2
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return data
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const DistributionCard = ({ title, data, type, valueKey, labelKey }) => (
|
|
|
|
|
|
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>{title}</div>
|
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
|
|
|
|
|
|
{getSortedData(data, type).map((item, index) => {
|
|
|
|
|
|
const value = item[valueKey]
|
|
|
|
|
|
const label = getLabel(type, item[labelKey] || value)
|
|
|
|
|
|
const color = getColor(type, value)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
onClick={() => handleItemClick(type, value)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'rgba(255,255,255,0.05)',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
padding: '10px',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
border: '1px solid rgba(255,255,255,0.05)',
|
|
|
|
|
|
transition: 'all 0.2s'
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseEnter={(e) => {
|
|
|
|
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.1)'
|
|
|
|
|
|
e.currentTarget.style.borderColor = 'rgba(251, 191, 36, 0.3)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
onMouseLeave={(e) => {
|
|
|
|
|
|
e.currentTarget.style.background = 'rgba(255,255,255,0.05)'
|
|
|
|
|
|
e.currentTarget.style.borderColor = 'rgba(255,255,255,0.05)'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', flex: 1, overflow: 'hidden' }}>
|
|
|
|
|
|
<div style={{ width: '8px', height: '8px', borderRadius: '2px', background: color, flexShrink: 0 }} />
|
|
|
|
|
|
<div style={{ color: color, fontSize: '12px', fontWeight: '500', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
|
|
|
|
{label}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ color: '#fff', fontSize: '14px', fontWeight: 'bold', marginLeft: '8px', flexShrink: 0 }}>
|
|
|
|
|
|
{item.count}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{data.length > 6 && (
|
|
|
|
|
|
<div style={{ textAlign: 'center', color: '#94a3b8', fontSize: '12px', marginTop: '8px' }}>
|
|
|
|
|
|
共{data.length}项,显示前 6 项
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ background: '#0f172a', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '16px' }}>加载中...</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const gradedCount = stats.byGrading.find(g => g.isGraded === true)?.count || 0
|
|
|
|
|
|
const ungradedCount = stats.byGrading.find(g => g.isGraded === false)?.count || 0
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ background: '#0f172a', minHeight: '100vh', paddingBottom: '80px' }}>
|
|
|
|
|
|
{/* 顶部 */}
|
|
|
|
|
|
<div style={{ padding: '16px', background: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold' }}>统计分析</div>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginTop: '4px' }}>点击统计项查看明细</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ color: 'rgba(255,255,255,0.3)', fontSize: '11px' }}>v{APP_VERSION}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ padding: '16px' }}>
|
|
|
|
|
|
{/* 总统计 */}
|
|
|
|
|
|
<div style={{ background: 'linear-gradient(135deg, rgba(251, 191, 36, 0.2) 0%, rgba(251, 191, 36, 0.05) 100%)', borderRadius: '12px', padding: '20px', marginBottom: '16px', border: '1px solid rgba(251, 191, 36, 0.3)' }}>
|
|
|
|
|
|
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '16px' }}>📊 总统计</div>
|
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px' }}>
|
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>总数量</div>
|
|
|
|
|
|
<div style={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}>{stats.totalCount}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>已评级</div>
|
|
|
|
|
|
<div style={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}>{gradedCount}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>未评级</div>
|
|
|
|
|
|
<div style={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}>{ungradedCount}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 财务统计 */}
|
|
|
|
|
|
<div style={{ background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.2) 0%, rgba(34, 197, 94, 0.05) 100%)', borderRadius: '12px', padding: '20px', marginBottom: '16px', border: '1px solid rgba(34, 197, 94, 0.3)' }}>
|
|
|
|
|
|
<div style={{ color: '#22c55e', fontSize: '14px', fontWeight: 'bold', marginBottom: '16px' }}>💰 财务统计</div>
|
|
|
|
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}>
|
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>总成本</div>
|
|
|
|
|
|
<div style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold' }}>¥{formatMoney(stats.totalCost)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>总收入</div>
|
|
|
|
|
|
<div style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold' }}>¥{formatMoney(stats.totalRevenue)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>预期利润</div>
|
|
|
|
|
|
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: 'bold' }}>+¥{formatMoney(stats.expectedProfit)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ textAlign: 'center' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '12px', marginBottom: '4px' }}>已实现利润</div>
|
|
|
|
|
|
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: 'bold' }}>+¥{formatMoney(stats.totalProfit)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 盈亏统计移到顶部 */}
|
|
|
|
|
|
<DistributionCard title="💰 盈亏统计" data={stats.byProfitLoss} type="profitLoss" valueKey="type" labelKey="label" />
|
|
|
|
|
|
|
|
|
|
|
|
<DistributionCard title="📦 包装分布" data={stats.byPackaging} type="packaging" valueKey="packaging" />
|
|
|
|
|
|
<DistributionCard title="🔢 号码分类分布" data={stats.byNumberCategory} type="numberCategory" valueKey="numberCategory" />
|
|
|
|
|
|
<DistributionCard title="📋 状态分布" data={stats.byStatus} type="status" valueKey="status" />
|
|
|
|
|
|
<DistributionCard title="⭐ 珍惜度分布" data={stats.byRarity} type="rarity" valueKey="rarity" />
|
|
|
|
|
|
<DistributionCard title="🏷️ 版别分布" data={stats.byVersion} type="version" valueKey="version" />
|
|
|
|
|
|
<DistributionCard title="🏅 评级机构分布" data={stats.byGradingCompany} type="gradingCompany" valueKey="company" />
|
|
|
|
|
|
<DistributionCard title="📈 评级分数分布" data={stats.byGradingScore} type="gradingScore" valueKey="score" />
|
|
|
|
|
|
<DistributionCard title="✨ 特殊标识分布" data={stats.bySpecialMark} type="specialMark" valueKey="mark" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|