jiachenlong/frontend/src.bak/pages/Home.jsx

256 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Home() {
const [user, setUser] = useState(null)
const [stats, setStats] = useState({ totalCount: 0, totalCost: 0, totalRevenue: 0, expectedProfit: 0, totalProfit: 0, gradedCount: 0 })
const [recentCollections, setRecentCollections] = useState([])
const currentPath = window.location.hash.slice(1) || '/'
useEffect(() => {
const token = localStorage.getItem('token')
const userData = localStorage.getItem('user')
if (userData) {
try {
setUser(JSON.parse(userData))
} catch (e) {
console.error('Parse user error:', e)
}
}
if (token) {
fetch('/api/collections/stats', {
headers: { 'Authorization': 'Bearer ' + token }
}).then(res => {
if (!res.ok) throw new Error('Stats API failed')
return res.json()
}).then(data => {
// stats API直接返回对象不需要data.data包装
const info = data.totalCount !== undefined ? data : (data.data || data)
if (info && info.totalCount !== undefined) {
// 从byGrading计算评级数
const gradedCount = info.byGrading ? (info.byGrading.find(x => x.isGraded === true)?.count || 0) : 0
setStats({
totalCount: info.totalCount || 0,
totalCost: info.totalCost || 0,
totalRevenue: info.totalRevenue || 0,
expectedProfit: info.expectedProfit || 0,
totalProfit: info.totalProfit || 0,
gradedCount: gradedCount
})
}
})
fetch('/api/collections?limit=5&sort=createdAt&order=desc', {
headers: { 'Authorization': 'Bearer ' + token }
}).then(res => res.json()).then(data => {
// 新格式 {data:[], pagination:{}} 或老格式 {items:[]}
const list = data.data || data.items || data
if (Array.isArray(list)) {
setRecentCollections(list)
}
})
}
}, [])
// 检查是否为管理员
const isAdmin = user && user.role === 'admin'
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
window.location.hash = '#/login'
window.location.reload()
}
const formatMoney = (val) => {
if (!val || val === 0) return '0'
const v = val / 10000
return v.toFixed(4)
}
const statCards = [
{ label: '藏品数', value: stats.totalCount, color: '#3b82f6', icon: '📦' },
{ label: '总成本', value: formatMoney(stats.totalCost) + '万', color: '#22c55e', icon: '💰' },
{ label: '总收入', value: formatMoney(stats.totalRevenue) + '万', color: '#06b6d4', icon: '📈' },
{ label: '评级数', value: stats.gradedCount, color: '#8b5cf6', icon: '⭐' },
{ label: '预期利润', value: formatMoney(stats.expectedProfit) + '万', color: stats.expectedProfit >= 0 ? '#f59e0b' : '#ef4444', icon: '🎯' },
{ label: '总利润', value: formatMoney(stats.totalProfit) + '万', color: stats.totalProfit >= 0 ? '#10b981' : '#f43f5e', icon: '💵' }
]
return (
<div style={{
minHeight: '100vh', overflowY: 'auto',
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)',
padding: '20px',
fontFamily: '"Noto Sans SC", "PingFang SC", sans-serif'
}}>
{/* 顶部用户信息 */}
<div style={{
background: 'linear-gradient(135deg, rgba(59,130,246,0.2) 0%, rgba(139,92,246,0.2) 100%)',
borderRadius: '16px',
padding: '20px',
marginBottom: '20px',
border: '1px solid rgba(255,255,255,0.1)',
backdropFilter: 'blur(10px)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<img src="/static/images/jiachenlong-logo.png?v=1.2.4" alt="甲辰收藏" style={{ width: '48px', height: '48px', borderRadius: '50%', objectFit: 'cover' }} />
<div>
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '600' }}>{user?.username || '用户'}</div>
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: '12px' }}>欢迎回来 👋</div>
</div>
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ color: 'rgba(255,255,255,0.3)', fontSize: '10px' }}>v{APP_VERSION}</div>
<div onClick={() => window.location.hash = '#/settings'} style={{ color: 'rgba(255,255,255,0.5)', cursor: 'pointer', fontSize: '12px', padding: '4px 8px', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '4px' }}>设置</div>
<div onClick={handleLogout} style={{ color: 'rgba(255,255,255,0.5)', cursor: 'pointer', fontSize: '12px', padding: '4px 8px', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '4px' }}>退出</div>
</div>
</div>
</div>
{/* 统计卡片网格 */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '12px',
marginBottom: '20px'
}}>
{statCards.map((card, idx) => (
<div key={idx} style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)',
borderRadius: '16px',
padding: '16px 12px',
border: '1px solid rgba(255,255,255,0.08)',
backdropFilter: 'blur(10px)',
textAlign: 'center',
transition: 'transform 0.2s, box-shadow 0.2s',
cursor: 'pointer'
}}
onMouseOver={e => { e.currentTarget.style.transform = 'translateY(-2px)' }}
onMouseOut={e => { e.currentTarget.style.transform = 'translateY(0)' }}
onClick={() => window.location.hash = '#/stats'}
>
<div style={{ fontSize: '24px', marginBottom: '4px' }}>{card.icon}</div>
<div style={{ color: card.color, fontSize: '18px', fontWeight: '700', marginBottom: '4px' }}>{card.value}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '11px' }}>{card.label}</div>
</div>
))}
</div>
{/* 快捷操作 */}
<div style={{ marginBottom: '20px' }}>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>快捷操作</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '12px' }}>
<div onClick={() => window.location.hash = '#/add?mode=ocr'} style={{
background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
borderRadius: '12px',
padding: '16px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
<div style={{ fontSize: '24px' }}>📷</div>
<div>
<div style={{ color: '#fff', fontSize: '15px', fontWeight: '600' }}>AI识别</div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '11px' }}>拍照识别藏品</div>
</div>
</div>
<div onClick={() => window.location.hash = '#/add?mode=manual'} style={{
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
borderRadius: '12px',
padding: '16px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}>
<div style={{ fontSize: '24px' }}></div>
<div>
<div style={{ color: '#fff', fontSize: '15px', fontWeight: '600' }}>手动录入</div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '11px' }}>添加新藏品</div>
</div>
</div>
</div>
</div>
{/* 最近藏品 */}
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px', paddingLeft: '4px' }}>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px' }}>最近藏品</div>
<div onClick={() => window.location.hash = '#/list'} style={{ color: '#3b82f6', fontSize: '12px', cursor: 'pointer' }}>查看全部 </div>
</div>
<div style={{
background: 'rgba(255,255,255,0.03)',
borderRadius: '12px',
border: '1px solid rgba(255,255,255,0.05)',
overflow: 'hidden'
}}>
{recentCollections.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: 'rgba(255,255,255,0.3)' }}>暂无藏品</div>
) : (
recentCollections.map((item, idx) => (
<div key={item.id || idx} onClick={() => window.location.hash = '#/detail?id=' + item.id} style={{
padding: '12px 16px',
borderBottom: idx < recentCollections.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<div style={{ color: '#fff', fontSize: '14px' }}>{item.code || '-'} <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '14px', fontWeight: 'bold' }}>{item.prefixSerial || ''}</span></div>
<div style={{ color: 'rgba(255,255,255,0.4)', fontSize: '12px', marginTop: '2px' }}>{item.version || '-'} · {item.status === 'sold' ? '已售' : item.status === 'in_collection' ? '收藏中' : item.status}</div>
</div>
<div style={{ color: item.costPrice ? '#22c55e' : 'rgba(255,255,255,0.3)', fontSize: '13px' }}>
{item.costPrice ? '¥' + item.costPrice : '-'}
</div>
</div>
))
)}
</div>
</div>
{/* 底部导航 */}
<div style={{
position: 'fixed',
bottom: '0',
left: '0',
right: '0',
background: 'rgba(15, 23, 42, 0.95)',
borderTop: '1px solid rgba(255,255,255,0.1)',
display: 'flex',
justifyContent: 'space-around',
padding: '12px 0',
backdropFilter: 'blur(10px)'
}}>
{(isAdmin ? [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
{ icon: '', label: '添加', hash: '#/ocr', idx: 2 },
{ icon: '📊', label: '统计', hash: '#/stats', idx: 3 },
{ icon: '⚙️', label: '管理', hash: '#/admin', idx: 4 }
] : [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
{ icon: '', label: '添加', hash: '#/ocr', idx: 2 },
{ icon: '📊', label: '统计', hash: '#/stats', idx: 3 }
]).map((item) => (
<div key={item.idx} onClick={() => window.location.hash = item.hash} style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
cursor: 'pointer',
color: currentPath === item.hash ? '#fbbf24' : '#64748b'
}}>
<div style={{ fontSize: '18px', fontWeight: currentPath === item.hash ? 'bold' : 'normal' }}>{item.icon}</div>
<div style={{ fontSize: '10px', marginTop: '2px', fontWeight: currentPath === item.hash ? 'bold' : 'normal' }}>{item.label}</div>
</div>
))}
</div>
<div style={{ height: '70px' }}></div>
</div>
)
}