jiachenlong/frontend/src/pages/Edit.jsx

492 lines
21 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.

import React, { useState, useRef, useEffect } from 'react'
// Input 组件(复用 Add.jsx 的定义)
const Input = ({ form, handleChange, label, field, type = 'text', options = null }) => {
const onChange = (e) => {
const value = e.target.value
if (type === 'number' && value !== '') {
const num = parseFloat(value)
handleChange(field, isNaN(num) ? '' : num)
} else handleChange(field, value)
}
return (
<div style={{ flex: 1, minWidth: '45%', marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>{label}</div>
{options ? (
<select value={form[field] || ''} onChange={onChange}
style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff', padding: '8px', borderRadius: '6px', width: '100%', fontSize: '13px' }}>
<option value="">请选择</option>
{options.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
</select>
) : (
<input type={type} value={form[field] ?? ''} onChange={onChange}
style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff', padding: '8px', borderRadius: '6px', width: '100%', fontSize: '13px' }} />
)}
</div>
)
}
// 选项定义
const statusOptions = [
{ value: 'in_collection', label: '收藏中' },
{ value: 'selling', label: '出售中' },
{ value: 'sold', label: '已售' },
{ value: 'grading', label: '送评中' },
{ value: 'repairing', label: '修复中' },
{ value: 'transit', label: '在途中' },
{ value: 'seeking', label: '寻号中' },
{ value: 'other', label: '其他' }
]
const categoryOptions = [
{ value: '自持', label: '自持' },
{ value: '寄存', label: '寄存' },
{ value: '寄售', label: '寄售' },
{ value: '共有', label: '共有' },
{ value: '其他', label: '其他' }
]
const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'无4',label:'无4'},{value:'带4',label:'带4'},{value:'其他',label:'其他'}];
const rarityOptions = [
{ value: '通货', label: '通货' },
{ value: '特色', label: '特色' },
{ value: '少见', label: '少见' },
{ value: '稀有', label: '稀有' },
{ value: '珍品', label: '珍品' },
{ value: '孤品', label: '孤品' }
]
const packagingOptions = [
{ value: '标十', label: '标十' },
{ value: '标百', label: '标百' },
{ value: '单张', label: '单张' },
{ value: '裸钞', label: '裸钞' }
]
export default function Edit() {
// 从 URL 获取藏品 ID
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
const editId = params.get('id')
const [form, setForm] = useState({
name: '', code: '', category: '自持', rarity: '通货', numberCategory: '', prefixSerial: '',
version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张',
material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false,
gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '',
serialFeature: '', issuer: '中国人民银行', issueYear: '2024',
costPrice: '', targetPrice: '', goalPrice: '', repairFee: '', gradingFee: '', remark: ''
})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [uploadImages, setUploadImages] = useState([])
const fileInputRef = useRef(null)
// 加载现有藏品数据
useEffect(() => {
if (editId) {
const token = localStorage.getItem('token')
fetch(`/api/collections/${editId}`, {
headers: { 'Authorization': 'Bearer ' + token }
})
.then(res => res.json())
.then(data => {
// 填充表单
setForm({
name: data.name || '',
code: data.code || '',
category: data.category || '自持',
rarity: data.rarity || '通货',
numberCategory: data.numberCategory || '',
prefixSerial: data.prefixSerial || '',
version: data.version || '2024 龙',
denomination: data.denomination || '',
status: data.status || 'in_collection',
packaging: data.packaging || '单张',
isGraded: data.isGraded || false,
gradingCompany: data.gradingCompany || '',
gradingScore: data.gradingScore || '',
threeStar: data.threeStar || false,
specialMark: data.specialMark || '',
serialFeature: data.serialFeature || '',
issuer: data.issuer || '中国人民银行',
issueYear: data.issueYear || '2024',
material: data.material || '塑料钞',
issueQuantity: data.issueQuantity || '1 亿',
costPrice: data.costPrice || '',
targetPrice: data.targetPrice || '',
goalPrice: data.goalPrice || '',
repairFee: data.repairFee || '',
gradingFee: data.gradingFee || '',
purpose: data.purpose || '收藏',
remark: data.remark || ''
})
// 加载图片
if (data.images && Array.isArray(data.images)) {
const images = data.images.map(img => ({
file: null,
preview: img.path?.startsWith('http') ? img.path : `/${img.path || `uploads/collections/${img.filename}`}`,
name: img.original_name || img.filename,
size: 0,
id: img.id
}))
setUploadImages(images)
}
setLoading(false)
})
.catch(err => {
console.error('加载藏品失败:', err)
setError('加载失败:' + err.message)
setLoading(false)
})
} else {
setError('未指定藏品 ID')
setLoading(false)
}
}, [editId])
const handleChange = (key, value) => {
setForm({ ...form, [key]: value })
if (key === 'targetPrice' && value) setForm(prev => ({ ...prev, status: 'sold' }))
// 冠字号变化时自动分类
if (key === 'prefixSerial') {
const serial = value
let cat = ''
const match = serial.match(/J(\d{9})/)
const digits = match ? match[1] : serial.replace(/\D/g, '').slice(0, 9)
if (digits) {
if (!digits.includes('2') && !digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无2347'
else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347'
else if (digits.includes('4')) cat = '带4'
else if (digits.includes('7')) cat = '无4'
else if (!digits.includes('4') && !digits.includes('7')) cat = '无47'
else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
else cat = '其他'
}
setForm(prev => ({ ...prev, numberCategory: cat }))
}
// 版别变化时同步发行年份
if (key === 'version') {
const yearMatch = value.match(/(20\d{2})/)
if (yearMatch) {
setForm(prev => ({ ...prev, issueYear: yearMatch[1] }))
}
}
}
// 图片上传处理
const handleImageUpload = async (e) => {
const files = Array.from(e.target.files)
if (files.length === 0 || !editId) return
const token = localStorage.getItem('token')
const file = files[0] // 只允许上传 1 张
const imgFormData = new FormData()
imgFormData.append('file', file)
try {
const res = await fetch(`/api/collections/upload-image?collection_id=${editId}`, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token },
body: imgFormData
})
if (res.ok) {
// 重新加载藏品数据
const dataRes = await fetch(`/api/collections/${editId}`, {
headers: { 'Authorization': 'Bearer ' + token }
})
const data = await dataRes.json()
if (data.images && Array.isArray(data.images)) {
const images = data.images.map(img => ({
file: null,
preview: img.path?.startsWith('http') ? img.path : `/${img.path || `uploads/collections/${img.filename}`}`,
name: img.original_name || img.filename,
size: 0,
id: img.id
}))
setUploadImages(images)
}
alert('图片上传成功!')
} else {
alert('图片上传失败')
}
} catch (err) {
console.error('图片上传失败:', err)
alert('图片上传失败:' + err.message)
}
}
const handleSave = async () => {
if (!form.name || !form.version) { setError('名称和版别为必填项'); return }
setSaving(true)
setError('')
const token = localStorage.getItem('token')
const formData = convertField(form)
try {
// 1. 更新藏品信息
const res = await fetch(`/api/collections/${editId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify(formData)
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.error?.message || data.detail || '更新失败')
}
alert('更新成功!')
window.location.hash = '#/list'
window.refreshList?.()
} catch (e) {
console.error('更新失败:', e)
alert('更新失败:' + e.message)
}
finally { setSaving(false) }
}
// 字段转换函数
const convertField = (obj) => {
const map = {
id: 'f99_90_id', userId: 'f99_91_user_id',
name: 'f01_01_name', code: 'f01_02_code', category: 'f01_03_category',
status: 'f01_04_status', remark: 'f01_05_remark',
prefixSerial: 'f02_10_prefix_serial', version: 'f02_11_version',
packaging: 'f02_12_packaging', rarity: 'f02_13_rarity',
numberCategory: 'f02_14_number_category',
isGraded: 'f03_20_is_graded', gradingCompany: 'f03_21_grading_company',
gradingScore: 'f03_22_grading_score', threeStar: 'f03_23_three_star',
specialMark: 'f04_30_special_mark', serialFeature: 'f04_31_serial_feature',
issuer: 'f04_32_issuer', issueYear: 'f04_33_issue_year',
material: 'f04_34_material', denomination: 'f04_35_denomination',
issueQuantity: 'f04_36_issue_quantity',
costPrice: 'f05_40_cost_price', targetPrice: 'f05_41_target_price',
goalPrice: 'f05_42_goal_price', repairFee: 'f05_43_repair_fee',
gradingFee: 'f05_44_grading_fee', purpose: 'f06_50_purpose'
}
const result = {}
const numberFields = ['costPrice', 'targetPrice', 'goalPrice', 'repairFee', 'gradingFee']
for (const key in obj) {
let value = obj[key]
if (value === '' || value === null) value = null
else if (numberFields.includes(key)) {
value = parseFloat(value)
if (isNaN(value)) value = null
}
result[map[key] || key] = value
}
return result
}
if (loading) {
return (
<div style={{ background: '#0f172a', minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ color: '#94a3b8', fontSize: '16px' }}>加载中...</div>
</div>
)
}
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 onClick={() => window.history.back()} style={{ color: '#60a5fa', fontSize: '24px', cursor: 'pointer', padding: '8px' }}></div>
</div>
{error && (
<div style={{ margin: '16px', padding: '12px', background: 'rgba(239, 68, 68, 0.2)', border: '1px solid #ef4444', color: '#ef4444', borderRadius: '8px' }}>
{error}
</div>
)}
<div style={{ padding: '16px' }}>
{/* 藏品图片 */}
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold' }}>藏品图片 ({uploadImages.length}/1)</div>
{uploadImages.length === 0 && (
<button
onClick={() => fileInputRef.current?.click()}
style={{
padding: '6px 12px',
background: 'rgba(34, 197, 94, 0.2)',
color: '#22c55e',
border: '1px solid #22c55e',
borderRadius: '6px',
fontSize: '12px',
cursor: 'pointer'
}}
>
📷 上传图片
</button>
)}
</div>
{/* 隐藏的文件输入框 */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
style={{ display: 'none' }}
/>
{uploadImages.length > 0 ? (
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${Math.min(uploadImages.length, 1)}, 1fr)`, gap: '12px' }}>
{uploadImages.map((img, index) => (
<div key={img.id || index} style={{ position: 'relative' }}>
<img
src={img.preview}
alt={img.name || '藏品图片'}
style={{
width: '100%',
aspectRatio: '1.5',
objectFit: 'contain',
borderRadius: '12px',
border: '2px solid #10b981',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
background: 'rgba(0,0,0,0.3)'
}}
/>
<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}/{uploadImages.length}</div>
</div>
))}
</div>
) : (
<div style={{ textAlign: 'center', color: '#94a3b8', padding: '24px' }}>
📷 暂无图片
</div>
)}
</div>
{/* 基本信息 */}
<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' }}>基本信息</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<Input form={form} handleChange={handleChange} label="名称 *" field="name" />
<Input form={form} handleChange={handleChange} label="编号" field="code" />
<Input form={form} handleChange={handleChange} label="持仓类型" field="category" options={categoryOptions} />
<Input form={form} handleChange={handleChange} label="珍惜度" field="rarity" options={rarityOptions} />
<Input form={form} handleChange={handleChange} label="冠字序号" field="prefixSerial" />
<Input form={form} handleChange={handleChange} label="版别 *" field="version" />
<Input form={form} handleChange={handleChange} label="面值" field="denomination" />
<Input form={form} handleChange={handleChange} label="状态" field="status" options={statusOptions} />
<Input form={form} handleChange={handleChange} label="包装" field="packaging" options={packagingOptions} />
</div>
</div>
{/* 评级信息 */}
<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' }}>评级信息</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px', alignItems: 'center', marginBottom: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input type="checkbox" id="isGraded" checked={form.isGraded || false} onChange={(e) => handleChange('isGraded', e.target.checked)}
style={{ width: '18px', height: '18px', cursor: 'pointer', accentColor: '#22c55e' }} />
<label htmlFor="isGraded" style={{ color: '#fff', cursor: 'pointer', fontSize: '13px' }}>是否评级</label>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<input type="checkbox" id="threeStar" checked={form.threeStar || false} onChange={(e) => handleChange('threeStar', e.target.checked)}
style={{ width: '18px', height: '18px', cursor: 'pointer', accentColor: '#22c55e' }} />
<label htmlFor="threeStar" style={{ color: '#fff', cursor: 'pointer', fontSize: '13px' }}>三星</label>
</div>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<Input form={form} handleChange={handleChange} label="评级公司" field="gradingCompany" />
<Input form={form} handleChange={handleChange} label="评级分数" field="gradingScore" />
</div>
</div>
{/* 特殊信息 */}
<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' }}>特殊信息</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<Input form={form} handleChange={handleChange} label="特殊标识" field="specialMark" />
<Input form={form} handleChange={handleChange} label="号码特征" field="serialFeature" />
<Input form={form} handleChange={handleChange} label="号码分类" field="numberCategory" options={numberCategoryOptions} />
<Input form={form} handleChange={handleChange} label="发行方" field="issuer" />
<Input form={form} handleChange={handleChange} label="发行年份" field="issueYear" />
<Input form={form} handleChange={handleChange} label="材质" field="material" />
<Input form={form} handleChange={handleChange} label="发行量" field="issueQuantity" />
</div>
</div>
{/* 价格信息 */}
<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' }}>价格信息</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<Input form={form} handleChange={handleChange} label="成本价" field="costPrice" type="number" />
<Input form={form} handleChange={handleChange} label="目标价" field="targetPrice" type="number" />
<Input form={form} handleChange={handleChange} label="出售价" field="goalPrice" type="number" />
<Input form={form} handleChange={handleChange} label="修复费" field="repairFee" type="number" />
<Input form={form} handleChange={handleChange} label="评级费" field="gradingFee" type="number" />
<Input form={form} handleChange={handleChange} label="用途" field="purpose" />
</div>
</div>
{/* 备注 */}
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ fontSize: '13px', color: '#94a3b8', marginBottom: '6px' }}>备注</div>
<textarea
value={form.remark || ''}
onChange={(e) => handleChange('remark', e.target.value)}
rows={3}
style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', color: '#fff', padding: '10px', borderRadius: '8px', width: '100%', resize: 'none' }}
/>
</div>
{/* 按钮 */}
<button
onClick={saving ? null : handleSave}
disabled={saving}
style={{
width: '100%',
padding: '14px',
background: saving ? '#64748b' : '#fbbf24',
color: saving ? '#94a3b8' : '#1e293b',
border: 'none',
borderRadius: '10px',
fontSize: '16px',
fontWeight: '600',
cursor: saving ? 'not-allowed' : 'pointer',
marginBottom: '12px'
}}
>
{saving ? '更新中...' : '✅ 更新藏品'}
</button>
<button
onClick={() => window.location.hash = '#/list'}
style={{
width: '100%',
padding: '14px',
background: 'rgba(255,255,255,0.05)',
color: '#94a3b8',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: '10px',
fontSize: '14px',
cursor: 'pointer'
}}
>
🔄 返回列表
</button>
</div>
</div>
)
}