// 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 (
{label}
{options ? ( ) : ( )}
) } // 默认表单数据 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 (
{/* 顶部标签切换 */}
{/* 内容区域 */} {activeTab === 'ai' && (
{/* 图片选择区域 */}
{imagePreview ? (
已选择图片
) : (
{ 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' }} >
📷
拍照识别
使用相机拍照并识别
{ 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' }} >
🖼️
从相册选择
从相册选择已有图片
)}
{/* 错误提示 */} {error && (
⚠️ {error}
)} {/* 提示信息 */}
💡 识别说明
  • 支持拍照或从相册选择图片
  • 自动识别名称、版别、冠字序号等字段
  • 识别结果可手动修改完善
  • 建议拍摄清晰、光线充足的正面照片
) } // 表单模式界面 return (
{/* 顶部 */}
确认信息
请检查并完善识别结果
{error && (
⚠️ {error}
)}
{/* 基本信息 */}
基本信息
{/* 价格信息 */}
价格信息
{/* 备注 */}
备注