514 lines
24 KiB
React
514 lines
24 KiB
React
|
|
import React, { useState, useRef } from 'react'
|
|||
|
|
|
|||
|
|
// 字段名转换函数:驼峰转下划线
|
|||
|
|
const convertField = (obj) => {
|
|||
|
|
const map = {
|
|||
|
|
prefixSerial: 'prefix_serial',
|
|||
|
|
isGraded: 'is_graded',
|
|||
|
|
gradingCompany: 'grading_company',
|
|||
|
|
gradingScore: 'grading_score',
|
|||
|
|
threeStar: 'three_star',
|
|||
|
|
specialMark: 'special_mark',
|
|||
|
|
serialFeature: 'serial_feature',
|
|||
|
|
issueYear: 'issue_year',
|
|||
|
|
issueQuantity: 'issue_quantity',
|
|||
|
|
costPrice: 'cost_price',
|
|||
|
|
targetPrice: 'target_price',
|
|||
|
|
goalPrice: 'goal_price',
|
|||
|
|
repairFee: 'repair_fee',
|
|||
|
|
gradingFee: 'grading_fee'
|
|||
|
|
}
|
|||
|
|
const result = {}
|
|||
|
|
for (const key in obj) {
|
|||
|
|
result[map[key] || key] = obj[key]
|
|||
|
|
}
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default function BatchMode() {
|
|||
|
|
const [images, setImages] = useState([])
|
|||
|
|
const [currentIndex, setCurrentIndex] = useState(0)
|
|||
|
|
const [result, setResult] = useState(null)
|
|||
|
|
const [loading, setLoading] = useState(false)
|
|||
|
|
const [savedCount, setSavedCount] = useState(0)
|
|||
|
|
const fileInputRef = useRef(null)
|
|||
|
|
|
|||
|
|
const statusOptions = [
|
|||
|
|
{ value: 'in_collection', label: '收藏中' },
|
|||
|
|
{ value: 'selling', label: '在售' },
|
|||
|
|
{ value: 'sold', label: '已售' },
|
|||
|
|
{ value: 'grading', label: '送评' },
|
|||
|
|
{ value: 'transit', label: '在途' },
|
|||
|
|
{ value: 'other', label: '其他' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const packagingOptions = [
|
|||
|
|
{ value: '单张', label: '单张' },
|
|||
|
|
{ value: '标十', label: '标十' },
|
|||
|
|
{ value: '标百', label: '标百' },
|
|||
|
|
{ value: '裸钞', 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: '孤品' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const handleChooseImages = (e) => {
|
|||
|
|
const files = Array.from(e.target.files || [])
|
|||
|
|
if (files.length > 0) {
|
|||
|
|
setImages(files.slice(0, 10))
|
|||
|
|
setCurrentIndex(0)
|
|||
|
|
setResult(null)
|
|||
|
|
setSavedCount(0)
|
|||
|
|
// Auto OCR first image
|
|||
|
|
processOCR(files[0])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const processOCR = async (file) => {
|
|||
|
|
setLoading(true)
|
|||
|
|
try {
|
|||
|
|
const formData = new FormData()
|
|||
|
|
formData.append('image', file)
|
|||
|
|
|
|||
|
|
const res = await fetch('/api/ocr', { method: 'POST', body: formData })
|
|||
|
|
const data = await res.json()
|
|||
|
|
const text = data.result || ''
|
|||
|
|
|
|||
|
|
const parsed = {
|
|||
|
|
name: '生肖纪念钞',
|
|||
|
|
ownership_type: '自持',
|
|||
|
|
rarity: '通货',
|
|||
|
|
status: 'in_collection',
|
|||
|
|
version: '',
|
|||
|
|
prefixSerial: '',
|
|||
|
|
packaging: '单张',
|
|||
|
|
isGraded: false,
|
|||
|
|
gradingCompany: '',
|
|||
|
|
gradingScore: '',
|
|||
|
|
threeStar: false,
|
|||
|
|
specialMark: '',
|
|||
|
|
serialFeature: '',
|
|||
|
|
denomination: '贰拾圆',
|
|||
|
|
issuer: '中国人民银行',
|
|||
|
|
issueYear: '2024',
|
|||
|
|
material: '塑料',
|
|||
|
|
issueQuantity: '一亿',
|
|||
|
|
costPrice: '',
|
|||
|
|
goalPrice: '',
|
|||
|
|
targetPrice: '',
|
|||
|
|
remark: ''
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const v = text.match(/发行版别[::]\s*([^\n]+)/)
|
|||
|
|
if (v) {
|
|||
|
|
parsed.version = v[1].trim()
|
|||
|
|
const yearMatch = v[1].match(/20\d{2}/)
|
|||
|
|
if (yearMatch) parsed.issueYear = yearMatch[0]
|
|||
|
|
}
|
|||
|
|
// 自动识别发行机构
|
|||
|
|
const issuerMatch = text.match(/发行机构[::]\s*([^\n]+)/)
|
|||
|
|
if (issuerMatch) parsed.issuer = issuerMatch[1].trim()
|
|||
|
|
if (text.includes('三星') || text.includes('3星') || text.includes('★★★')) parsed.threeStar = true
|
|||
|
|
const d = text.match(/面额[::]\s*([^\n]+)/)
|
|||
|
|
if (d) parsed.denomination = d[1].trim()
|
|||
|
|
const p = text.match(/冠字序号[::]\s*([^\n]+)/)
|
|||
|
|
if (p) parsed.prefixSerial = p[1].trim()
|
|||
|
|
// 识别编号
|
|||
|
|
const codeMatch = text.match(/编号[::]\s*([^\n]+)/)
|
|||
|
|
if (codeMatch) parsed.code = codeMatch[1].trim()
|
|||
|
|
const pk = text.match(/封装类型[::]\s*([^\n]+)/)
|
|||
|
|
if (pk) {
|
|||
|
|
const pv = pk[1].trim()
|
|||
|
|
if (pv.includes('百')) parsed.packaging = '标百'
|
|||
|
|
else if (pv.includes('十')) parsed.packaging = '标十'
|
|||
|
|
else if (pv.includes('单')) parsed.packaging = '单张'
|
|||
|
|
else parsed.packaging = '裸钞'
|
|||
|
|
}
|
|||
|
|
const g = text.match(/是否评级[::]\s*([^\n]+)/)
|
|||
|
|
if (g) parsed.isGraded = g[1].trim() === '是'
|
|||
|
|
const gc = text.match(/评级机构[::]\s*([^\n]+)/)
|
|||
|
|
if (gc) parsed.gradingCompany = gc[1].trim()
|
|||
|
|
const gs = text.match(/评级分数[::]\s*([^\n]+)/)
|
|||
|
|
if (gs) parsed.gradingScore = gs[1].trim()
|
|||
|
|
const sm = text.match(/特殊标识[::]\s*([^\n]+)/)
|
|||
|
|
if (sm) parsed.specialMark = sm[1].trim()
|
|||
|
|
const sf = text.match(/号码特征[::]\s*([^\n]+)/)
|
|||
|
|
if (sf) parsed.serialFeature = sf[1].trim()
|
|||
|
|
|
|||
|
|
setResult(parsed)
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error(e)
|
|||
|
|
alert('识别失败,请重试')
|
|||
|
|
}
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const updateResult = (field, value) => {
|
|||
|
|
setResult(prev => ({ ...prev, [field]: value }))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleSave = async (force = false) => {
|
|||
|
|
const token = localStorage.getItem('token')
|
|||
|
|
if (!token) {
|
|||
|
|
alert('请先登录')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const saveData = convertField({ ...result, _forceSave: force })
|
|||
|
|
const res = await fetch('/api/collections', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
|||
|
|
body: JSON.stringify(saveData)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (res.ok) {
|
|||
|
|
setSavedCount(prev => prev + 1)
|
|||
|
|
alert('保存成功!')
|
|||
|
|
} else {
|
|||
|
|
const data = await res.json()
|
|||
|
|
const errMsg = data.error || data.message || data.detail || '保存失败'
|
|||
|
|
// 检查重复
|
|||
|
|
if (errMsg.includes('E002') || errMsg.includes('已存在') || errMsg.includes('禁止重复') || errMsg.includes('重复')) {
|
|||
|
|
const confirmed = confirm('发现重复:冠字号 ' + (result.prefixSerial || '') + ' 已存在,是否继续保存?')
|
|||
|
|
if (confirmed) {
|
|||
|
|
await handleSave(true) // 强制保存
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
alert(errMsg)
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
alert('保存失败: ' + e.message)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleNext = () => {
|
|||
|
|
if (currentIndex < images.length - 1) {
|
|||
|
|
setCurrentIndex(currentIndex + 1)
|
|||
|
|
setResult(null)
|
|||
|
|
processOCR(images[currentIndex + 1])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handlePrev = () => {
|
|||
|
|
if (currentIndex > 0) {
|
|||
|
|
setCurrentIndex(currentIndex - 1)
|
|||
|
|
setResult(null)
|
|||
|
|
processOCR(images[currentIndex - 1])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleReOCR = () => {
|
|||
|
|
processOCR(images[currentIndex])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleReUpload = () => {
|
|||
|
|
setImages([])
|
|||
|
|
setCurrentIndex(0)
|
|||
|
|
setResult(null)
|
|||
|
|
setSavedCount(0)
|
|||
|
|
fileInputRef.current?.click()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// No images selected
|
|||
|
|
if (images.length === 0) {
|
|||
|
|
return (
|
|||
|
|
<div style={{ padding: '20px' }}>
|
|||
|
|
<div
|
|||
|
|
onClick={() => fileInputRef.current?.click()}
|
|||
|
|
style={{
|
|||
|
|
minHeight: '50vh',
|
|||
|
|
background: 'rgba(255,255,255,0.03)',
|
|||
|
|
border: '2px dashed rgba(255,255,255,0.15)',
|
|||
|
|
borderRadius: '16px',
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
cursor: 'pointer'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{ fontSize: '60px', opacity: 0.5 }}>📦</div>
|
|||
|
|
<div style={{ color: '#fff', fontSize: '16px', marginTop: '16px' }}>点击选择多张图片</div>
|
|||
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginTop: '8px' }}>最多10张</div>
|
|||
|
|
</div>
|
|||
|
|
<input
|
|||
|
|
ref={fileInputRef}
|
|||
|
|
type="file"
|
|||
|
|
accept="image/*"
|
|||
|
|
multiple
|
|||
|
|
onChange={handleChooseImages}
|
|||
|
|
style={{ display: 'none' }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isSaved = savedCount > currentIndex
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div style={{ padding: '16px', paddingBottom: '120px' }}>
|
|||
|
|
{/* Sticky Header */}
|
|||
|
|
<div style={{ position: 'sticky', top: 0, background: '#0f172a', padding: '12px 0', marginBottom: '12px', zIndex: 10, borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
|||
|
|
<div style={{ color: '#fff', fontSize: '14px', marginBottom: '8px' }}>
|
|||
|
|
已保存: {savedCount} / {images.length} | 当前第 {currentIndex + 1} 张
|
|||
|
|
</div>
|
|||
|
|
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
|
|||
|
|
{images.map((_, i) => (
|
|||
|
|
<div key={i} style={{
|
|||
|
|
width: '24px', height: '24px', borderRadius: '4px',
|
|||
|
|
background: i < savedCount ? '#22c55e' : (i === currentIndex ? '#fbbf24' : '#333'),
|
|||
|
|
color: '#fff', fontSize: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center'
|
|||
|
|
}}>
|
|||
|
|
{i + 1}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
{currentIndex === images.length - 1 && savedCount > 0 && (
|
|||
|
|
<button onClick={handleReUpload} style={{ marginTop: '12px', padding: '8px 16px', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '6px', fontSize: '13px', cursor: 'pointer' }}>
|
|||
|
|
重新上传
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Image */}
|
|||
|
|
<div style={{ marginBottom: '16px' }}>
|
|||
|
|
<img
|
|||
|
|
src={URL.createObjectURL(images[currentIndex])}
|
|||
|
|
alt="preview"
|
|||
|
|
style={{ width: '100%', maxHeight: '200px', objectFit: 'contain', borderRadius: '8px' }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Buttons - moved here */}
|
|||
|
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
|
|||
|
|
<button
|
|||
|
|
onClick={handlePrev}
|
|||
|
|
disabled={currentIndex === 0}
|
|||
|
|
style={{ flex: 1, padding: '12px', background: currentIndex === 0 ? '#333' : 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', cursor: currentIndex === 0 ? 'not-allowed' : 'pointer' }}
|
|||
|
|
>
|
|||
|
|
上一张
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
{currentIndex < images.length - 1 && (
|
|||
|
|
<button
|
|||
|
|
onClick={handleNext}
|
|||
|
|
style={{ flex: 1, padding: '12px', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}
|
|||
|
|
>
|
|||
|
|
下一张
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
onClick={handleReOCR}
|
|||
|
|
disabled={loading}
|
|||
|
|
style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: '1px solid rgba(255,255,255,0.2)', borderRadius: '8px', cursor: loading ? 'not-allowed' : 'pointer' }}
|
|||
|
|
>
|
|||
|
|
重新识别
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleSave}
|
|||
|
|
disabled={!result || loading}
|
|||
|
|
style={{ flex: 1, padding: '12px', background: result && !loading ? '#22c55e' : '#333', color: '#fff', border: 'none', borderRadius: '8px', fontWeight: 'bold', cursor: result && !loading ? 'pointer' : 'not-allowed' }}
|
|||
|
|
>
|
|||
|
|
保存结果
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{loading && (
|
|||
|
|
<div style={{ textAlign: 'center', padding: '20px', color: '#fbbf24' }}>🤖 AI识别中...</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{result && !loading && (
|
|||
|
|
<>
|
|||
|
|
{/* 基本信息 */}
|
|||
|
|
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '12px' }}>
|
|||
|
|
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>基本信息</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>名称</div>
|
|||
|
|
<input type="text" value={result.name || ''} onChange={(e) => updateResult('name', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>编号</div>
|
|||
|
|
<input type="text" value={result.code || ''} onChange={(e) => updateResult('code', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>持仓类型</div>
|
|||
|
|
<select value={result.ownership_type || '自持'} onChange={(e) => updateResult('ownership_type', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
|
|||
|
|
{categoryOptions.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>珍惜度</div>
|
|||
|
|
<select value={result.rarity || '通货'} onChange={(e) => updateResult('rarity', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
|
|||
|
|
{rarityOptions.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>冠字序号</div>
|
|||
|
|
<input type="text" value={result.prefixSerial || ''} onChange={(e) => updateResult('prefixSerial', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fbbf24', fontSize: '13px', fontFamily: 'monospace' }} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>版别</div>
|
|||
|
|
<input type="text" value={result.version || ''} onChange={(e) => updateResult('version', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>状态</div>
|
|||
|
|
<select value={result.status || 'in_collection'} onChange={(e) => updateResult('status', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
|
|||
|
|
{statusOptions.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>封装</div>
|
|||
|
|
<select value={result.packaging || '单张'} onChange={(e) => updateResult('packaging', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
|
|||
|
|
{packagingOptions.map(o => <option key={o.value} value={o.value} style={{color:'#000'}}>{o.label}</option>)}
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 评级信息 */}
|
|||
|
|
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '12px' }}>
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px', padding: '8px', background: result.isGraded ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255,255,255,0.03)', borderRadius: '6px', cursor: 'pointer' }}
|
|||
|
|
onClick={() => updateResult('isGraded', !result.isGraded)}>
|
|||
|
|
<div style={{ width: '22px', height: '22px', borderRadius: '4px', border: '2px solid', borderColor: result.isGraded ? '#22c55e' : '#64748b', backgroundColor: result.isGraded ? '#22c55e' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|||
|
|
{result.isGraded && <span style={{color:'#fff',fontSize:'14px'}}>✓</span>}
|
|||
|
|
</div>
|
|||
|
|
<span style={{color:'#fff',fontSize:'13px'}}>已评级</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{result.isGraded && (
|
|||
|
|
<>
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>评级公司</div>
|
|||
|
|
<input type="text" value={result.gradingCompany || ''} onChange={(e) => updateResult('gradingCompany', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>评级分数</div>
|
|||
|
|
<input type="text" value={result.gradingScore || ''} onChange={(e) => updateResult('gradingScore', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fbbf24', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px', background: result.threeStar ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255,255,255,0.03)', borderRadius: '6px', cursor: 'pointer' }}
|
|||
|
|
onClick={() => updateResult('threeStar', !result.threeStar)}>
|
|||
|
|
<div style={{ width: '22px', height: '22px', borderRadius: '4px', border: '2px solid', borderColor: result.threeStar ? '#22c55e' : '#64748b', backgroundColor: result.threeStar ? '#22c55e' : 'transparent', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|||
|
|
{result.threeStar && <span style={{color:'#fff',fontSize:'14px'}}>✓</span>}
|
|||
|
|
</div>
|
|||
|
|
<span style={{color:'#fff',fontSize:'13px'}}>三星</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 特殊信息 */}
|
|||
|
|
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '12px' }}>
|
|||
|
|
<div style={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>特殊信息</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>发行机构</div>
|
|||
|
|
<input type="text" value={result.issuer || ''} onChange={(e) => updateResult('issuer', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>年份</div>
|
|||
|
|
<input type="text" value={result.issueYear || ''} onChange={(e) => updateResult('issueYear', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>特殊标识</div>
|
|||
|
|
<input type="text" value={result.specialMark || ''} onChange={(e) => updateResult('specialMark', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>号码特征</div>
|
|||
|
|
<input type="text" value={result.serialFeature || ''} onChange={(e) => updateResult('serialFeature', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>面额</div>
|
|||
|
|
<input type="text" value={result.denomination || ''} onChange={(e) => updateResult('denomination', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 价格信息 */}
|
|||
|
|
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '16px' }}>
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>成本价</div>
|
|||
|
|
<input type="number" value={result.costPrice || ''} onChange={(e) => updateResult('costPrice', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ marginBottom: '10px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>出售价</div>
|
|||
|
|
<input type="number" value={result.targetPrice || ''} onChange={(e) => updateResult('targetPrice', e.target.value)}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fbbf24', fontSize: '13px' }} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 备注 */}
|
|||
|
|
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '14px', marginBottom: '16px' }}>
|
|||
|
|
<div style={{ marginBottom: '8px' }}>
|
|||
|
|
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '4px' }}>备注</div>
|
|||
|
|
<textarea value={result.remark || ''} onChange={(e) => updateResult('remark', e.target.value)} rows={2}
|
|||
|
|
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px', resize: 'none' }} />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* Footer */}
|
|||
|
|
{savedCount > 0 && currentIndex === images.length - 1 && (
|
|||
|
|
<button onClick={handleReUpload} style={{ width: '100%', padding: '14px', background: '#6366f1', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '15px', marginTop: '16px', cursor: 'pointer' }}>
|
|||
|
|
重新上传
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
<input
|
|||
|
|
ref={fileInputRef}
|
|||
|
|
type="file"
|
|||
|
|
accept="image/*"
|
|||
|
|
multiple
|
|||
|
|
onChange={handleChooseImages}
|
|||
|
|
style={{ display: 'none' }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|