jiachenlong/frontend/src/pages/Edit.jsx.dual_column_bak

576 lines
24 KiB
Plaintext
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, useRef } from 'react'
// 通用 Input 组件
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>
)
}
export default function Edit() {
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 [saving, setSaving] = useState(false)
const [form, setForm] = useState({})
const [showDelete, setShowDelete] = useState(false)
const [collectionImages, setCollectionImages] = useState([])
const fileInputRef = useRef(null)
const [editingImageIndex, setEditingImageIndex] = useState(-1)
useEffect(() => {
if (id) {
fetchDetail()
}
}, [id])
// 图片上传处理
const handleImageUpload = async (e) => {
const files = Array.from(e.target.files)
if (files.length === 0) return
const token = localStorage.getItem('token')
const maxImages = 3
// 检查是否超过限制
const currentCount = collection.images ? collection.images.length : 0
if (editingImageIndex === -1 && currentCount + files.length > maxImages) {
alert(`最多只能上传${maxImages}张图片`)
return
}
// 更换图片(点击已有图片)
if (editingImageIndex >= 0) {
const file = files[0]
const oldImage = collection.images[editingImageIndex]
const imgFormData = new FormData()
imgFormData.append('file', file)
try {
// 先上传新图片
const uploadRes = await fetch(`/api/collections/upload-image?collection_id=${id}`, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token },
body: imgFormData
})
if (uploadRes.ok) {
// 删除旧图片
await fetch(`/api/collections/images/${oldImage.id}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
})
// 重新加载详情
await fetchDetail()
alert('图片已更换')
}
} catch (err) {
console.error('图片更换失败:', err)
alert('更换失败,请重试')
}
setEditingImageIndex(-1)
return
}
// 添加新图片
for (const file of files) {
const imgFormData = new FormData()
imgFormData.append('file', file)
try {
const res = await fetch(`/api/collections/upload-image?collection_id=${id}`, {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + token },
body: imgFormData
})
if (res.ok) {
// 重新加载藏品详情
await fetchDetail()
}
} catch (err) {
console.error('图片上传失败:', err)
}
}
}
const handleDeleteImage = async (imageId) => {
if (!confirm('确定删除这张图片吗?')) return
const token = localStorage.getItem('token')
try {
const res = await fetch(`/api/collections/images/${imageId}`, {
method: 'DELETE',
headers: { 'Authorization': 'Bearer ' + token }
})
if (res.ok) {
await fetchDetail()
}
} catch (err) {
console.error('图片删除失败:', err)
}
}
const fetchDetail = async () => {
setLoading(true)
const token = localStorage.getItem('token')
try {
const res = await fetch(`/api/collections/${id}`, {
headers: token ? { 'Authorization': 'Bearer ' + token } : {}
})
const data = await res.json()
const item = (data.code === 200 ? data.data : data)
if (item && item.id) {
setCollection(item)
// 设置特殊信息默认值
const yearMatch = item.version ? item.version.match(/(20\d{2})/) : null
const defaultForm = {
...item,
issuer: item.issuer || '中国人民银行',
issueYear: item.issueYear || (yearMatch ? yearMatch[1] : '2024'),
material: item.material || '塑料钞',
issueQuantity: item.issueQuantity || '1 亿'
}
setForm(defaultForm)
// 加载图片
if (item.images && Array.isArray(item.images)) {
setCollectionImages(item.images)
}
}
} catch (e) {
console.error('加载详情失败:', e)
}
setLoading(false)
}
const handleChange = (key, value) => {
setForm({ ...form, [key]: value })
// 填入出售价时自动把状态改为"已售"
if (key === 'targetPrice' && value) {
setForm(prev => ({ ...prev, status: 'sold' }))
}
// 版别变化时同步发行年份
if (key === 'version') {
const yearMatch = value.match(/(20\d{2})/)
if (yearMatch) {
setForm(prev => ({ ...prev, issueYear: yearMatch[1] }))
}
}
}
const handleSave = async () => {
setSaving(true)
setError('')
const token = localStorage.getItem('token')
// 1. 先检查是否重复(同版别、同冠字号)
if (form.prefixSerial) {
const checkRes = await fetch(`/api/collections?search=${encodeURIComponent(form.prefixSerial)}`, {
headers: { 'Authorization': 'Bearer ' + token }
})
const checkData = await checkRes.json()
const collections = checkData.data || checkData || []
const duplicate = collections.find(c =>
c.version === form.version &&
c.prefixSerial === form.prefixSerial &&
c.id !== id // 排除自己
)
if (duplicate) {
setError('已经录入过同版同号藏品!')
setSaving(false)
return
}
}
// 转换字段名camelCase 转字段编码
const convertField = (obj) => {
const map = {
// f99 系统字段
id: 'f99_90_id',
userId: 'f99_91_user_id',
createdAt: 'f99_92_created_at',
updatedAt: 'f99_93_updated_at',
// f01 基本信息
name: 'f01_01_name',
code: 'f01_02_code',
category: 'f01_03_category',
status: 'f01_04_status',
remark: 'f01_05_remark',
// f02 详细字段
prefixSerial: 'f02_10_prefix_serial',
version: 'f02_11_version',
packaging: 'f02_12_packaging',
rarity: 'f02_13_rarity',
// f03 评级信息
isGraded: 'f03_20_is_graded',
gradingCompany: 'f03_21_grading_company',
gradingScore: 'f03_22_grading_score',
threeStar: 'f03_23_three_star',
// f04 特殊信息
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',
// f05 价格信息
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',
// f06 其他信息
purpose: 'f06_50_purpose'
}
const result = {}
// 数字字段列表
const numberFields = ['costPrice', 'targetPrice', 'goalPrice', 'repairFee', 'gradingFee']
for (const key in obj) {
let value = obj[key]
// 过滤空字符串为 null
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
}
try {
const res = await fetch(`/api/collections/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify(convertField(form))
})
if (res.ok) {
alert('保存成功!')
// 刷新首页统计数据
if (window.refreshHome) {
window.refreshHome()
}
window.location.hash = '#/detail?id=' + id
} else {
const data = await res.json()
alert('保存失败: ' + (data.error || '未知错误'))
}
} catch (e) {
alert('保存失败: ' + e.message)
}
setSaving(false)
}
const handleDelete = () => {
setShowDelete(true)
}
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 goBack = () => {
window.location.hash = '#/detail?id=' + id
}
const statusOptions = [
{ value: 'in_collection', label: '收藏中' },
{ value: 'selling', label: '出售中' },
{ value: 'sold', label: '已售' },
{ value: 'grading', label: '送评中' },
{ value: 'repairing', label: '修复中' },
{ value: 'transit', label: '在途中' },
{ value: 'other', label: '其他' }
]
const categoryOptions = [
{ value: '自持', label: '自持' },
{ value: '寄存', label: '寄存' },
{ value: '寄售', label: '寄售' },
{ value: '共有', label: '共有' },
{ value: '寻号', label: '寻号' },
{ value: '其他', label: '其他' }
]
const rarityOptions = [
{ value: '通货', label: '通货' },
{ value: '特色', label: '特色' },
{ value: '少见', label: '少见' },
{ value: '稀有', label: '稀有' },
{ value: '珍品', label: '珍品' },
{ value: '孤品', label: '孤品' }
]
if (loading) {
return <div style={{ padding: '40px', textAlign: 'center', color: '#94a3b8' }}>加载中...</div>
}
return (
<div style={{ background: '#0f172a', minHeight: '100vh', overflowY: 'auto', paddingBottom: '120px' }}>
{/* 顶部 */}
<div style={{ padding: '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 }}>
<button onClick={goBack} style={{ background: 'none', border: 'none', fontSize: '20px', cursor: 'pointer', padding: '8px', color: '#fff' }}>←</button>
<span style={{ fontSize: '18px', fontWeight: 'bold', marginLeft: '8px', color: '#fff' }}>编辑藏品</span>
</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: '13px', fontWeight: 'bold' }}>藏品图片</div>
{collectionImages.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',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}
>
📷 更换图片
</button>
)}
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
style={{ display: 'none' }}
/>
{collectionImages.length > 0 ? (
<div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '12px' }}>
{collectionImages.map((img, index) => (
<div key={img.id || index} style={{ position: 'relative' }}>
<img
src={`http://120.26.133.10:3000/${img.path || `uploads/collections/${img.filename}`}`}
alt={img.original_name || img.filename || '藏品图片'}
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}/{collectionImages.length}</div>
</div>
))}
</div>
) : (
<div
onClick={() => fileInputRef.current?.click()}
style={{
aspectRatio: '1.5',
background: 'rgba(255,255,255,0.05)',
border: '2px dashed #fbbf24',
borderRadius: '12px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
color: '#fbbf24',
fontSize: '12px',
transition: 'all 0.2s'
}}
>
<div style={{ fontSize: '48px', marginBottom: '8px' }}>📷</div>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>点击上传图片</div>
<div style={{ fontSize: '11px', color: '#94a3b8' }}>
最多可上传 1 张
</div>
</div>
)}
</div>
{/* 基本信息 */}
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ color: '#fbbf24', fontSize: '13px', 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="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 style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'flex', gap: '24px', flexWrap: 'wrap' }}>
<div style={{ flex: 1, minWidth: '45%' }}>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>编号</div>
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', color: '#fbbf24', padding: '8px', borderRadius: '6px', fontSize: '13px', fontFamily: 'monospace', fontWeight: 'bold' }}>
{form.code || '(保存后自动生成)'}
</div>
</div>
<div style={{ flex: 1, minWidth: '45%' }}>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>创建时间</div>
<div style={{ background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.05)', color: '#94a3b8', padding: '8px', borderRadius: '6px', fontSize: '13px' }}>
{form.createdAt ? new Date(form.createdAt).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }).replace(/\//g, '-') : '(保存后自动生成)'}
</div>
</div>
</div>
</div>
</div>
{/* 评级信息 */}
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ color: '#fbbf24', fontSize: '13px', fontWeight: 'bold', marginBottom: '12px' }}>评级信息</div>
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '10px' }}>
<input
type="checkbox"
id="isGraded"
checked={form.isGraded || false}
onChange={(e) => handleChange('isGraded', e.target.checked)}
style={{ width: '22px', height: '22px', cursor: 'pointer', backgroundColor: form.isGraded ? '#22c55e' : 'rgba(255,255,255,0.1)', border: '2px solid #22c55e', borderRadius: '4px', appearance: 'none', WebkitAppearance: 'none' }}
/>
<label htmlFor="isGraded" style={{ color: '#fff', cursor: 'pointer', fontSize: '15px' }}>是否评级</label>
</div>
<Input form={form} handleChange={handleChange} label="评级公司" field="gradingCompany" />
<Input form={form} handleChange={handleChange} label="评级分数" field="gradingScore" />
<Input form={form} handleChange={handleChange} label="特殊标识" field="specialMark" />
<div style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '10px' }}>
<input
type="checkbox"
id="threeStar"
checked={form.threeStar || false}
onChange={(e) => handleChange('threeStar', e.target.checked)}
style={{ width: '22px', height: '22px', cursor: 'pointer', backgroundColor: form.threeStar ? '#22c55e' : 'rgba(255,255,255,0.1)', border: '2px solid #22c55e', borderRadius: '4px', appearance: 'none', WebkitAppearance: 'none' }}
/>
<label htmlFor="threeStar" style={{ color: '#fff', cursor: 'pointer', fontSize: '15px' }}>三星</label>
</div>
</div>
{/* 特殊信息 */}
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ color: '#fbbf24', fontSize: '13px', fontWeight: 'bold', marginBottom: '12px' }}>特殊信息</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
<Input form={form} handleChange={handleChange} label="号码特征" field="serialFeature" />
<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: '13px', fontWeight: 'bold', marginBottom: '12px' }}>价格信息</div>
<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 style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>备注</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: '8px', borderRadius: '8px', width: '100%', resize: 'none' }}
/>
</div>
{/* 按钮 */}
<button onClick={saving ? null : handleSave} disabled={saving} style={{ width: '100%', padding: '14px', background: saving ? '#64748b' : '#fbbf24', color: '#1e293b', border: 'none', borderRadius: '10px', fontSize: '16px', fontWeight: '600', marginBottom: '12px', cursor: saving ? 'not-allowed' : 'pointer' }}>
{saving ? '保存中...' : '保存'}
</button>
<button onClick={handleDelete} style={{ width: '100%', padding: '14px', background: 'rgba(239,68,68,0.1)', color: '#ef4444', border: '1px solid #ef4444', borderRadius: '10px', fontSize: '16px', cursor: 'pointer' }}>
删除藏品
</button>
</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: '13px' }}>取消</button>
<button onClick={doDelete} style={{ flex: 1, padding: '12px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '13px' }}>删除</button>
</div>
</div>
</div>
)}
</div>
)
}