655 lines
22 KiB
React
655 lines
22 KiB
React
|
|
// AI OCR 识别页面 - 支持拍照识别和手工录入
|
|||
|
|
import React, { useState, useEffect, useRef } from 'react'
|
|||
|
|
|
|||
|
|
// 字段转换函数:camelCase → 字段编码
|
|||
|
|
const convertField = (obj) => {
|
|||
|
|
const map = {
|
|||
|
|
// f99 系统字段
|
|||
|
|
id: 'f99_90_id',
|
|||
|
|
userId: 'f99_91_user_id',
|
|||
|
|
// 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]
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 通用 Input 组件
|
|||
|
|
const Input = ({ form, handleChange, label, field, type = 'text', options = null, disabled = false }) => {
|
|||
|
|
const inputRef = useRef(null)
|
|||
|
|
const handleFocus = () => {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|||
|
|
}, 100)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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={{ marginBottom: '12px' }} ref={inputRef}>
|
|||
|
|
<div style={{ fontSize: '13px', color: '#94a3b8', marginBottom: '6px' }}>{label}</div>
|
|||
|
|
{options ? (
|
|||
|
|
<select
|
|||
|
|
value={form[field] || ''}
|
|||
|
|
onChange={onChange}
|
|||
|
|
onFocus={handleFocus}
|
|||
|
|
disabled={disabled}
|
|||
|
|
style={{
|
|||
|
|
background: disabled ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.05)',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|||
|
|
color: disabled ? '#64748b' : '#fff',
|
|||
|
|
padding: '10px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
width: '100%'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<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}
|
|||
|
|
onFocus={handleFocus}
|
|||
|
|
disabled={disabled}
|
|||
|
|
style={{
|
|||
|
|
background: disabled ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.05)',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|||
|
|
color: disabled ? '#64748b' : '#fff',
|
|||
|
|
padding: '10px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
width: '100%'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 默认表单数据
|
|||
|
|
const getDefaultForm = () => ({
|
|||
|
|
name: '龙钞',
|
|||
|
|
code: '',
|
|||
|
|
category: '自持',
|
|||
|
|
rarity: '通货',
|
|||
|
|
prefixSerial: '',
|
|||
|
|
version: '2024 龙',
|
|||
|
|
denomination: '',
|
|||
|
|
status: 'in_collection',
|
|||
|
|
packaging: '单张',
|
|||
|
|
material: '',
|
|||
|
|
issueQuantity: '',
|
|||
|
|
purpose: '收藏',
|
|||
|
|
isGraded: false,
|
|||
|
|
gradingCompany: '',
|
|||
|
|
gradingScore: '',
|
|||
|
|
threeStar: false,
|
|||
|
|
specialMark: '',
|
|||
|
|
serialFeature: '',
|
|||
|
|
issuer: '中国人民银行',
|
|||
|
|
issueYear: '',
|
|||
|
|
costPrice: '',
|
|||
|
|
targetPrice: '',
|
|||
|
|
goalPrice: '',
|
|||
|
|
repairFee: '',
|
|||
|
|
gradingFee: '',
|
|||
|
|
remark: ''
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
export default function OCR() {
|
|||
|
|
const [activeTab, setActiveTab] = useState('ai') // ai, manual, batch
|
|||
|
|
const [mode, setMode] = useState('camera') // camera, form
|
|||
|
|
const [form, setForm] = useState(getDefaultForm())
|
|||
|
|
const [saving, setSaving] = useState(false)
|
|||
|
|
const [error, setError] = useState('')
|
|||
|
|
const [recognizing, setRecognizing] = useState(false)
|
|||
|
|
const [selectedImage, setSelectedImage] = useState(null)
|
|||
|
|
const [imagePreview, setImagePreview] = useState(null)
|
|||
|
|
const fileInputRef = useRef(null)
|
|||
|
|
const videoRef = useRef(null)
|
|||
|
|
const canvasRef = useRef(null)
|
|||
|
|
|
|||
|
|
// 选项定义
|
|||
|
|
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: '孤品' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const packagingOptions = [
|
|||
|
|
{ value: '标十', label: '标十' },
|
|||
|
|
{ value: '标百', label: '标百' },
|
|||
|
|
{ value: '单张', label: '单张' },
|
|||
|
|
{ value: '裸钞', label: '裸钞' }
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const handleChange = (key, value) => {
|
|||
|
|
setForm({ ...form, [key]: value })
|
|||
|
|
if (key === 'targetPrice' && value) {
|
|||
|
|
setForm(prev => ({ ...prev, status: 'sold' }))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 选择图片
|
|||
|
|
const handleSelectImage = (e) => {
|
|||
|
|
const file = e.target.files[0]
|
|||
|
|
if (!file) return
|
|||
|
|
|
|||
|
|
setSelectedImage(file)
|
|||
|
|
const reader = new FileReader()
|
|||
|
|
reader.onload = (e) => {
|
|||
|
|
setImagePreview(e.target.result)
|
|||
|
|
}
|
|||
|
|
reader.readAsDataURL(file)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调用 OCR 识别
|
|||
|
|
const handleRecognize = async () => {
|
|||
|
|
if (!selectedImage) {
|
|||
|
|
setError('请先选择图片')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setRecognizing(true)
|
|||
|
|
setError('')
|
|||
|
|
|
|||
|
|
const token = localStorage.getItem('token')
|
|||
|
|
const formData = new FormData()
|
|||
|
|
formData.append('image', selectedImage)
|
|||
|
|
|
|||
|
|
// 使用 XMLHttpRequest 替代 fetch,兼容性更好
|
|||
|
|
const data = await new Promise((resolve, reject) => {
|
|||
|
|
const xhr = new XMLHttpRequest()
|
|||
|
|
xhr.open('POST', '/api/ocr/recognize')
|
|||
|
|
xhr.setRequestHeader('Authorization', 'Bearer ' + token)
|
|||
|
|
|
|||
|
|
xhr.onload = function() {
|
|||
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|||
|
|
try {
|
|||
|
|
const data = JSON.parse(xhr.responseText)
|
|||
|
|
resolve(data)
|
|||
|
|
} catch (e) {
|
|||
|
|
reject(new Error('JSON解析失败: ' + xhr.responseText.substring(0, 100)))
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
try {
|
|||
|
|
const data = JSON.parse(xhr.responseText)
|
|||
|
|
reject(new Error(data.error?.message || data.detail || '识别失败'))
|
|||
|
|
} catch (e) {
|
|||
|
|
reject(new Error('请求失败: ' + xhr.status))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
xhr.onerror = function() {
|
|||
|
|
reject(new Error('网络错误'))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
xhr.send(formData)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 自动填充识别结果
|
|||
|
|
if (data.fields) {
|
|||
|
|
const recognizedForm = { ...getDefaultForm() }
|
|||
|
|
|
|||
|
|
// 映射识别字段到表单字段
|
|||
|
|
if (data.fields.name) recognizedForm.name = data.fields.name
|
|||
|
|
if (data.fields.code) recognizedForm.code = data.fields.code
|
|||
|
|
if (data.fields.version) recognizedForm.version = data.fields.version
|
|||
|
|
if (data.fields.prefix_serial) recognizedForm.prefixSerial = data.fields.prefix_serial
|
|||
|
|
if (data.fields.denomination) recognizedForm.denomination = data.fields.denomination
|
|||
|
|
if (data.fields.material) recognizedForm.material = data.fields.material
|
|||
|
|
if (data.fields.issue_year) recognizedForm.issueYear = data.fields.issue_year
|
|||
|
|
if (data.fields.issuer) recognizedForm.issuer = data.fields.issuer
|
|||
|
|
if (data.fields.cost_price) recognizedForm.costPrice = data.fields.cost_price
|
|||
|
|
if (data.fields.remark) recognizedForm.remark = data.fields.remark
|
|||
|
|
|
|||
|
|
setForm(recognizedForm)
|
|||
|
|
setMode('form')
|
|||
|
|
alert('识别成功!请检查并完善信息')
|
|||
|
|
} else {
|
|||
|
|
setError('识别结果为空')
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('OCR 识别失败:', e)
|
|||
|
|
setError('识别失败:' + e.message)
|
|||
|
|
} finally {
|
|||
|
|
setRecognizing(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 保存藏品
|
|||
|
|
const handleSave = async () => {
|
|||
|
|
if (!form.name || !form.version) {
|
|||
|
|
setError('名称和版别为必填项')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setSaving(true)
|
|||
|
|
setError('')
|
|||
|
|
|
|||
|
|
const token = localStorage.getItem('token')
|
|||
|
|
const formData = convertField(form)
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/collections', {
|
|||
|
|
method: 'POST',
|
|||
|
|
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?.()
|
|||
|
|
window.refreshHome?.()
|
|||
|
|
} catch (e) {
|
|||
|
|
alert('保存失败:' + e.message)
|
|||
|
|
} finally {
|
|||
|
|
setSaving(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 拍照模式界面
|
|||
|
|
if (mode === 'camera') {
|
|||
|
|
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)' }}>
|
|||
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setActiveTab('ai')}
|
|||
|
|
style={{
|
|||
|
|
flex: 1,
|
|||
|
|
padding: '10px',
|
|||
|
|
background: activeTab === 'ai' ? '#fbbf24' : 'rgba(255,255,255,0.05)',
|
|||
|
|
color: activeTab === 'ai' ? '#1e293b' : '#fff',
|
|||
|
|
border: 'none',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
fontWeight: '600',
|
|||
|
|
cursor: 'pointer'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
🤖 AI 识别
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
setActiveTab('manual');
|
|||
|
|
setMode('form');
|
|||
|
|
}}
|
|||
|
|
style={{
|
|||
|
|
flex: 1,
|
|||
|
|
padding: '10px',
|
|||
|
|
background: activeTab === 'manual' ? '#fbbf24' : 'rgba(255,255,255,0.05)',
|
|||
|
|
color: activeTab === 'manual' ? '#1e293b' : '#fff',
|
|||
|
|
border: 'none',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
fontWeight: '600',
|
|||
|
|
cursor: 'pointer'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
✍️ 手工录入
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setActiveTab('batch')}
|
|||
|
|
disabled
|
|||
|
|
style={{
|
|||
|
|
flex: 1,
|
|||
|
|
padding: '10px',
|
|||
|
|
background: 'rgba(255,255,255,0.02)',
|
|||
|
|
color: '#64748b',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.05)',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
fontWeight: '600',
|
|||
|
|
cursor: 'not-allowed'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
📦 批量录入
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 内容区域 */}
|
|||
|
|
{activeTab === 'ai' && (
|
|||
|
|
|
|||
|
|
<div style={{ padding: '16px' }}>
|
|||
|
|
{/* 图片选择区域 */}
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(255,255,255,0.03)',
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
padding: '24px',
|
|||
|
|
textAlign: 'center',
|
|||
|
|
marginBottom: '12px'
|
|||
|
|
}}>
|
|||
|
|
<input
|
|||
|
|
ref={fileInputRef}
|
|||
|
|
type="file"
|
|||
|
|
accept="image/*"
|
|||
|
|
capture="environment"
|
|||
|
|
onChange={handleSelectImage}
|
|||
|
|
style={{ display: 'none' }}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
{imagePreview ? (
|
|||
|
|
<div>
|
|||
|
|
<img
|
|||
|
|
src={imagePreview}
|
|||
|
|
alt="已选择图片"
|
|||
|
|
style={{ maxWidth: '100%', borderRadius: '8px', marginBottom: '16px' }}
|
|||
|
|
/>
|
|||
|
|
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
|
|||
|
|
<button
|
|||
|
|
onClick={() => { setSelectedImage(null); setImagePreview(null); }}
|
|||
|
|
style={{
|
|||
|
|
padding: '12px 24px',
|
|||
|
|
background: 'rgba(239, 68, 68, 0.2)',
|
|||
|
|
color: '#ef4444',
|
|||
|
|
border: '1px solid #ef4444',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
cursor: 'pointer'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
🗑️ 重新选择
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleRecognize}
|
|||
|
|
disabled={recognizing}
|
|||
|
|
style={{
|
|||
|
|
padding: '12px 24px',
|
|||
|
|
background: recognizing ? '#64748b' : '#fbbf24',
|
|||
|
|
color: recognizing ? '#94a3b8' : '#1e293b',
|
|||
|
|
border: 'none',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
fontWeight: '600',
|
|||
|
|
cursor: recognizing ? 'not-allowed' : 'pointer'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{recognizing ? '🔍 识别中...' : '🤖 开始识别'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div>
|
|||
|
|
<div
|
|||
|
|
onClick={() => {
|
|||
|
|
fileInputRef.current.setAttribute('capture', '');
|
|||
|
|
fileInputRef.current.setAttribute('accept', 'image/*');
|
|||
|
|
fileInputRef.current.click();
|
|||
|
|
}}
|
|||
|
|
style={{
|
|||
|
|
padding: '24px',
|
|||
|
|
background: 'rgba(59, 130, 246, 0.1)',
|
|||
|
|
border: '2px dashed rgba(59, 130, 246, 0.5)',
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
marginBottom: '16px',
|
|||
|
|
textAlign: 'center'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{ fontSize: '40px', marginBottom: '12px' }}>📷</div>
|
|||
|
|
<div style={{ color: '#60a5fa', fontSize: '15px', fontWeight: 'bold', marginBottom: '4px' }}>
|
|||
|
|
拍照识别
|
|||
|
|
</div>
|
|||
|
|
<div style={{ color: '#94a3b8', fontSize: '13px' }}>
|
|||
|
|
使用相机拍照并识别
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div
|
|||
|
|
onClick={() => {
|
|||
|
|
fileInputRef.current.removeAttribute('capture');
|
|||
|
|
fileInputRef.current.setAttribute('accept', 'image/*');
|
|||
|
|
fileInputRef.current.click();
|
|||
|
|
}}
|
|||
|
|
style={{
|
|||
|
|
padding: '24px',
|
|||
|
|
background: 'rgba(34, 197, 94, 0.1)',
|
|||
|
|
border: '2px dashed rgba(34, 197, 94, 0.5)',
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
textAlign: 'center'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div style={{ fontSize: '40px', marginBottom: '12px' }}>🖼️</div>
|
|||
|
|
<div style={{ color: '#22c55e', fontSize: '15px', fontWeight: 'bold', marginBottom: '4px' }}>
|
|||
|
|
从相册选择
|
|||
|
|
</div>
|
|||
|
|
<div style={{ color: '#94a3b8', fontSize: '13px' }}>
|
|||
|
|
从相册选择已有图片
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 错误提示 */}
|
|||
|
|
{error && (
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(239, 68, 68, 0.2)',
|
|||
|
|
border: '1px solid #ef4444',
|
|||
|
|
color: '#ef4444',
|
|||
|
|
padding: '12px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
marginBottom: '12px'
|
|||
|
|
}}>
|
|||
|
|
⚠️ {error}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 提示信息 */}
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(59, 130, 246, 0.1)',
|
|||
|
|
border: '1px solid rgba(59, 130, 246, 0.3)',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
padding: '16px',
|
|||
|
|
marginTop: '12px'
|
|||
|
|
}}>
|
|||
|
|
<div style={{ color: '#60a5fa', fontSize: '14px', fontWeight: 'bold', marginBottom: '8px' }}>💡 识别说明</div>
|
|||
|
|
<ul style={{ color: '#94a3b8', fontSize: '13px', paddingLeft: '20px', margin: 0 }}>
|
|||
|
|
<li>支持拍照或从相册选择图片</li>
|
|||
|
|
<li>自动识别名称、版别、冠字序号等字段</li>
|
|||
|
|
<li>识别结果可手动修改完善</li>
|
|||
|
|
<li>建议拍摄清晰、光线充足的正面照片</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</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', alignItems: 'center', gap: '12px' }}>
|
|||
|
|
<button
|
|||
|
|
onClick={() => setMode('camera')}
|
|||
|
|
style={{
|
|||
|
|
background: 'rgba(255,255,255,0.1)',
|
|||
|
|
color: '#fff',
|
|||
|
|
border: 'none',
|
|||
|
|
borderRadius: '6px',
|
|||
|
|
width: '36px',
|
|||
|
|
height: '36px',
|
|||
|
|
fontSize: '20px',
|
|||
|
|
cursor: 'pointer'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
←
|
|||
|
|
</button>
|
|||
|
|
<div>
|
|||
|
|
<div style={{ color: '#fff', fontSize: '18px', fontWeight: 'bold' }}>确认信息</div>
|
|||
|
|
<div style={{ color: '#94a3b8', fontSize: '13px' }}>请检查并完善识别结果</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<div style={{
|
|||
|
|
margin: '16px',
|
|||
|
|
background: 'rgba(239, 68, 68, 0.2)',
|
|||
|
|
border: '1px solid #ef4444',
|
|||
|
|
color: '#ef4444',
|
|||
|
|
padding: '12px',
|
|||
|
|
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={{ color: '#fbbf24', fontSize: '14px', fontWeight: 'bold', marginBottom: '12px' }}>基本信息</div>
|
|||
|
|
<Input form={form} handleChange={handleChange} label="名称 *" field="name" />
|
|||
|
|
<Input form={form} handleChange={handleChange} label="版别 *" field="version" />
|
|||
|
|
<Input form={form} handleChange={handleChange} label="冠字序号" field="prefixSerial" />
|
|||
|
|
<Input form={form} handleChange={handleChange} label="面值" field="denomination" />
|
|||
|
|
<Input form={form} handleChange={handleChange} label="材质" field="material" />
|
|||
|
|
<Input form={form} handleChange={handleChange} label="发行方" field="issuer" />
|
|||
|
|
<Input form={form} handleChange={handleChange} label="发行年份" field="issueYear" />
|
|||
|
|
</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>
|
|||
|
|
<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" />
|
|||
|
|
</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={() => setMode('camera')}
|
|||
|
|
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>
|
|||
|
|
)
|
|||
|
|
}
|