jiachenlong/frontend/src/pages/Stats.jsx

293 lines
13 KiB
React
Raw Normal View History

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' },
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')
}
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>
)
}