jiachenlong/frontend/src/pages/Stats.jsx

290 lines
13 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'
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',
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', '无4': '#06b6d4', '带4': '#22c55e', '其他': '#64748b' },
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', '无4', '带4', '其他']
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).slice(0, 6).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(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" />
{/* 总统计 */}
<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>
<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>
)
}