jiachenlong/frontend/src/pages/Detail.jsx

395 lines
17 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.

/**
* Detail - 藏品详情页面
* Version: 1.2.90x
* 更新:
*/
import React, { useState, useEffect } from 'react'
export default function Detail() {
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
const id = params.get('id')
const [collection, setCollection] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [showDelete, setShowDelete] = useState(false)
const [showImage, setShowImage] = useState(false)
const [currentImageIndex, setCurrentImageIndex] = useState(0)
useEffect(() => {
if (id) {
fetchDetail()
}
}, [id])
const fetchDetail = async () => {
setLoading(true)
const token = localStorage.getItem('token')
try {
const res = await fetch(`/api/collections/${id}`, {
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
})
if (!res.ok) {
const errorData = await res.json()
const errorCode = errorData.error?.code || 'E00000'
const errorMessage = errorData.error?.message || '加载失败'
// 标准错误码处理
if (res.status === 404) {
setError('E00033: 藏品不存在')
} else if (res.status === 401) {
setError('E00010: 未登录或登录已过期')
} else if (res.status === 403) {
setError('E00014: 无权访问此藏品')
} else {
setError(`${errorCode}: ${errorMessage}`)
}
setLoading(false)
return
}
const data = await res.json()
// 后端已返回 camelCase 格式,直接使用
const collectionData = data.data || data
if (collectionData && collectionData.id) {
setCollection(collectionData)
} else {
setError('E00000: 数据格式错误')
}
} catch (e) {
console.error(e)
setError('E00001: 网络连接失败')
}
setLoading(false)
}
const handleEdit = () => {
window.location.hash = '#/edit?id=' + id
}
const goBack = () => {
// 尝试返回到上次筛选的列表页面
const lastUrl = sessionStorage.getItem('lastListUrl')
if (lastUrl) {
sessionStorage.removeItem('lastListUrl')
window.location.hash = '#' + lastUrl
} else {
window.location.hash = '#/list'
}
}
const doDelete = async () => {
const token = localStorage.getItem('token')
try {
const res = await fetch(`/api/collections/${id}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
})
if (res.ok) {
alert('删除成功!')
if (window.refreshList) window.refreshList()
window.location.hash = '#/list'
} else {
alert('删除失败')
}
} catch (e) {
alert('删除失败')
}
setShowDelete(false)
}
const getStatusText = (status) => {
const map = { 'in_collection': '收藏中', 'selling': '出售中', 'sold': '已售', 'grading': '送评中', 'repairing': '修复中', 'transit': '在途中', 'other': '其他' }
return map[status] || status || '-'
}
const getCategoryText = (category) => {
const map = { '自持': '自持', '寄存': '寄存', '寄售': '寄售', '共有': '共有', '寻号': '寻号', '其他': '其他' }
return map[category] || category || '-'
}
const Field = ({ label, value, green }) => (
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '6px 0', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<span style={{ color: '#94a3b8', fontSize: '13px' }}>{label}</span>
<span style={{ color: green ? '#22c55e' : '#fff', fontSize: '13px', textAlign: 'right', maxWidth: '60%' }}>{value || '-'}</span>
</div>
)
const Section = ({ title, children }) => (
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '10px', padding: '12px', marginBottom: '10px' }}>
<div style={{ color: '#fbbf24', fontSize: '13px', fontWeight: 'bold', marginBottom: '8px' }}>{title}</div>
{children}
</div>
)
if (loading) {
return <div style={{ padding: '40px', textAlign: 'center', color: '#94a3b8' }}>加载中...</div>
}
if (error) {
return (
<div style={{ padding: '40px', textAlign: 'center' }}>
<div style={{ fontSize: '60px' }}></div>
<div style={{ color: '#ef4444', marginTop: '16px', fontSize: '16px', fontWeight: 'bold' }}>{error}</div>
<button onClick={goBack} style={{ marginTop: '20px', padding: '12px 24px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>返回列表</button>
</div>
)
}
if (!collection) {
return (
<div style={{ padding: '40px', textAlign: 'center' }}>
<div style={{ fontSize: '60px' }}></div>
<div style={{ color: '#94a3b8', marginTop: '16px' }}>藏品不存在</div>
<button onClick={goBack} style={{ marginTop: '20px', padding: '12px 24px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>返回列表</button>
</div>
)
}
return (
<div style={{ background: '#0f172a', minHeight: '100vh', paddingBottom: '80px' }}>
{/* 顶部 */}
<div style={{ padding: '12px 16px', background: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', position: 'sticky', top: 0, zIndex: 100 }}>
<span style={{ fontSize: '16px', fontWeight: 'bold', color: '#fff' }}>藏品详情</span>
<div style={{ marginLeft: 'auto', display: 'flex', gap: '8px' }}>
<button onClick={goBack} style={{ padding: '6px 12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', fontSize: '13px' }}>返回</button>
<button onClick={() => setShowDelete(true)} style={{ padding: '6px 12px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '6px', fontSize: '13px' }}>删除</button>
<button onClick={handleEdit} style={{ padding: '6px 12px', background: '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '6px', fontSize: '13px', fontWeight: '600' }}>编辑</button>
</div>
</div>
{/* 主信息 */}
<div style={{ padding: '16px' }}>
{/* 图片显示 */}
{collection.images && collection.images.length > 0 && (
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${Math.min(collection.images.length, 1)}, 1fr)`, gap: '12px' }}>
{collection.images.map((img, index) => (
<div key={img.id || index} style={{ position: 'relative' }}>
<img
src={img.path?.startsWith('http') ? img.path : `/${img.path || `uploads/collections/${img.filename}`}`}
alt={img.originalName || '藏品图片'}
onClick={() => { setCurrentImageIndex(index); setShowImage(true); }}
style={{
width: '100%',
aspectRatio: '1.5',
objectFit: 'contain',
borderRadius: '12px',
border: '2px solid #10b981',
cursor: 'pointer',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
background: '#0f172a'
}}
onError={(e) => {
// 图片加载失败时显示灰色占位符,不显示 logo
e.target.style.display = 'none';
e.target.parentElement.innerHTML = '<div style="width:100%;aspectRatio:1.5;background:rgba(255,255,255,0.05);border-radius:12px;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,0.3);font-size:14px;">无图片</div>';
}}
/>
{collection.images.length > 1 && (
<div style={{
position: 'absolute',
top: '8px',
left: '8px',
background: 'rgba(0,0,0,0.6)',
color: '#fff',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px'
}}>
{index + 1}/{collection.images.length}
</div>
)}
</div>
))}
</div>
</div>
)}
<div style={{ fontSize: '18px', fontWeight: 'bold', textAlign: 'center', color: '#fff', marginBottom: '8px' }}>{collection.name}</div>
<div style={{ textAlign: 'center', color: '#fbbf24', fontSize: '14px', fontFamily: 'monospace', fontWeight: 'bold' }}>{collection.prefixSerial || '-'}</div>
</div>
{/* 信息区块 */}
<div style={{ padding: '0 16px' }}>
<Section title="基本信息">
<Field label="编号" value={collection.code} />
<Field label="持仓类型" value={getCategoryText(collection.category)} />
<Field label="珍惜度" value={collection.rarity || '-'} green={collection.rarity && collection.rarity !== '通货'} />
<Field label="版别" value={collection.version} />
<Field label="面值" value={collection.denomination} />
<Field label="状态" value={getStatusText(collection.status)} green={collection.status === 'in_collection'} />
<Field label="包装" value={collection.packaging} />
<Field label="创建时间" value={collection.createdAt ? new Date(collection.createdAt).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-') : '-'} />
</Section>
<Section title="评级信息">
<Field label="是否评级" value={collection.isGraded ? '已评级' : '未评级'} />
<Field label="评级公司" value={collection.gradingCompany} />
<Field label="评级分数" value={collection.gradingScore} green={collection.gradingScore} />
<Field label="三星" value={collection.threeStar ? '是' : '否'} green={collection.threeStar} />
</Section>
<Section title="特殊信息">
<Field label="特殊标识" value={collection.specialMark} />
<Field label="号码特征" value={collection.serialFeature} />
<Field label="号码分类" value={collection.numberCategory} />
<Field label="发行方" value={collection.issuer} />
<Field label="发行年份" value={collection.issueYear} />
<Field label="材质" value={collection.material} />
<Field label="发行量" value={collection.issueQuantity} />
</Section>
<Section title="价格信息">
<Field label="成本价" value={collection.costPrice ? '¥' + collection.costPrice : '-'} />
<Field label="目标价" value={collection.targetPrice ? '¥' + collection.targetPrice : '-'} />
<Field label="出售价" value={collection.goalPrice ? '¥' + collection.goalPrice : '-'} green={collection.goalPrice} />
<Field label="修复费" value={collection.repairFee ? '¥' + collection.repairFee : '-'} />
<Field label="评级费" value={collection.gradingFee ? '¥' + collection.gradingFee : '-'} />
<Field label="用途" value={collection.purpose} />
</Section>
{collection.remark && (
<Section title="备注">
<div style={{ color: '#fff', fontSize: '13px', lineHeight: '1.5' }}>{collection.remark}</div>
</Section>
)}
</div>
{/* 删除确认弹窗 */}
{showDelete && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '24px', width: '80%', maxWidth: '300px' }}>
<div style={{ color: '#fff', fontSize: '16px', fontWeight: 'bold', marginBottom: '16px', textAlign: 'center' }}>确定删除此藏品</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button onClick={() => setShowDelete(false)} style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '14px' }}>取消</button>
<button onClick={doDelete} style={{ flex: 1, padding: '12px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '14px' }}>删除</button>
</div>
</div>
</div>
)}
{/* 图片查看器 */}
{showImage && collection.images && collection.images.length > 0 && (
<div
style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.95)', zIndex: 9999, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px' }}
onClick={() => setShowImage(false)}
>
{/* 左侧切换按钮 */}
{collection.images.length > 1 && (
<button
onClick={(e) => { e.stopPropagation(); setCurrentImageIndex((currentImageIndex - 1 + collection.images.length) % collection.images.length); }}
style={{
position: 'absolute',
left: '20px',
background: 'rgba(255,255,255,0.2)',
color: '#fff',
border: 'none',
borderRadius: '50%',
width: '50px',
height: '50px',
fontSize: '24px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
</button>
)}
{/* 图片 */}
<img
src={collection.images[currentImageIndex].path?.startsWith('http') ? collection.images[currentImageIndex].path : `/${collection.images[currentImageIndex].path || `uploads/collections/${collection.images[currentImageIndex].filename}`}`}
alt={collection.images[currentImageIndex].originalName || '藏品图片'}
onClick={(e) => e.stopPropagation()}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }}
/>
{/* 右侧切换按钮 */}
{collection.images.length > 1 && (
<button
onClick={(e) => { e.stopPropagation(); setCurrentImageIndex((currentImageIndex + 1) % collection.images.length); }}
style={{
position: 'absolute',
right: '20px',
background: 'rgba(255,255,255,0.2)',
color: '#fff',
border: 'none',
borderRadius: '50%',
width: '50px',
height: '50px',
fontSize: '24px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
</button>
)}
{/* 图片指示器 */}
{collection.images.length > 1 && (
<div
style={{
position: 'absolute',
bottom: '40px',
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
gap: '10px'
}}
onClick={(e) => e.stopPropagation()}
>
{collection.images.map((_, index) => (
<div
key={index}
onClick={() => setCurrentImageIndex(index)}
style={{
width: '12px',
height: '12px',
borderRadius: '50%',
background: index === currentImageIndex ? '#fbbf24' : 'rgba(255,255,255,0.3)',
cursor: 'pointer',
border: '2px solid #fff'
}}
/>
))}
</div>
)}
{/* 关闭提示 */}
<div
style={{
position: 'absolute',
top: '20px',
right: '20px',
background: 'rgba(255,255,255,0.2)',
color: '#fff',
border: 'none',
borderRadius: '50%',
width: '40px',
height: '40px',
fontSize: '24px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
onClick={() => setShowImage(false)}
>
×
</div>
</div>
)}
</div>
)
}
// v2.7.1