2026-04-11 01:47:55 +08:00
|
|
|
|
// 添加藏品页面 - 支持 AI 识别/手工录入
|
2026-03-23 11:08:52 +08:00
|
|
|
|
import React, { useState, useRef } from 'react'
|
|
|
|
|
|
import { APP_VERSION } from '../config/version'
|
2026-04-11 01:47:55 +08:00
|
|
|
|
const API_BASE = localStorage.getItem('API_BASE') || ''
|
2026-04-11 18:38:03 +08:00
|
|
|
|
const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'带7号',label:'带7号'},{value:'带4号',label:'带4号'},{value:'其他',label:'其他'}];
|
2026-03-23 11:08:52 +08:00
|
|
|
|
|
|
|
|
|
|
// 字段转换函数
|
|
|
|
|
|
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',
|
|
|
|
|
|
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', numberCategory: 'f02_14_number_category',
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 通用 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%' }}>
|
|
|
|
|
|
<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 getDefaultForm = () => ({
|
|
|
|
|
|
name: '藏品', code: '', category: '自持', rarity: '通货', prefixSerial: '',
|
|
|
|
|
|
version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张',
|
|
|
|
|
|
material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false,
|
|
|
|
|
|
gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '',
|
|
|
|
|
|
serialFeature: '', numberCategory: '', issuer: '中国人民银行', issueYear: '2024',
|
|
|
|
|
|
costPrice: '', targetPrice: '', goalPrice: '', repairFee: '', gradingFee: '', remark: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
export default function Add() {
|
2026-04-11 01:47:55 +08:00
|
|
|
|
// 行情录入表单
|
|
|
|
|
|
const [dealForm, setDealForm] = useState({
|
|
|
|
|
|
serial: '',
|
|
|
|
|
|
category: '',
|
|
|
|
|
|
packaging: '标十',
|
|
|
|
|
|
price: '',
|
|
|
|
|
|
platform: '淘宝',
|
|
|
|
|
|
seller: '',
|
|
|
|
|
|
buyer: '',
|
2026-04-11 16:30:18 +08:00
|
|
|
|
date: new Date().toISOString().split('T')[0],
|
|
|
|
|
|
isGraded: false,
|
|
|
|
|
|
gradingCompany: '',
|
|
|
|
|
|
gradingScore: ''
|
2026-04-11 01:47:55 +08:00
|
|
|
|
})
|
2026-04-11 16:30:18 +08:00
|
|
|
|
const [batchDefaultPackaging, setBatchDefaultPackaging] = useState('')
|
|
|
|
|
|
const [batchDefaultDate, setBatchDefaultDate] = useState('')
|
|
|
|
|
|
const [batchDefaultPlatform, setBatchDefaultPlatform] = useState('')
|
|
|
|
|
|
const [dealMode, setDealMode] = useState('single') // single-单条录入, batch-批量录入
|
|
|
|
|
|
const [batchText, setBatchText] = useState('')
|
|
|
|
|
|
const [batchResult, setBatchResult] = useState([])
|
|
|
|
|
|
const [parsing, setParsing] = useState(false)
|
2026-04-11 01:47:55 +08:00
|
|
|
|
const [savingDeal, setSavingDeal] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
// 号码分类函数
|
|
|
|
|
|
const autoCategory = (serial) => {
|
|
|
|
|
|
const digits = serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
|
|
|
|
|
|
if (!digits) return ''
|
|
|
|
|
|
const d = digits
|
|
|
|
|
|
if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '圆圆号'
|
|
|
|
|
|
if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '倒置号'
|
|
|
|
|
|
if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马王'
|
|
|
|
|
|
if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马号'
|
|
|
|
|
|
if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山王'
|
|
|
|
|
|
if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马王'
|
|
|
|
|
|
if (!d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山号'
|
|
|
|
|
|
if (!d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马号'
|
|
|
|
|
|
if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧王'
|
|
|
|
|
|
if (!d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧号'
|
|
|
|
|
|
if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '如意号'
|
2026-04-11 18:38:03 +08:00
|
|
|
|
if (!d.includes('3') && !d.includes('4') && !d.includes('7')) return '钻石号'
|
|
|
|
|
|
if (!d.includes('4') && !d.includes('7')) return '永恒号'
|
|
|
|
|
|
if (!d.includes('4')) return '带7号'
|
|
|
|
|
|
return '带4号'
|
2026-04-11 01:47:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 11:08:52 +08:00
|
|
|
|
// 根据 URL 参数确定默认标签
|
|
|
|
|
|
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
|
|
|
|
|
|
const modeParam = params.get('mode')
|
2026-04-11 23:44:29 +08:00
|
|
|
|
const defaultTab = modeParam === 'manual' ? 'manual' : modeParam === 'deal' ? 'deal' : 'ai'
|
2026-03-23 11:08:52 +08:00
|
|
|
|
|
2026-04-11 01:47:55 +08:00
|
|
|
|
const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, deal
|
2026-03-23 11:08:52 +08:00
|
|
|
|
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 [recognizedImage, setRecognizedImage] = useState(null)
|
|
|
|
|
|
const fileInputRef = useRef(null)
|
|
|
|
|
|
const imageFileInputRef = useRef(null)
|
|
|
|
|
|
const [uploadImages, setUploadImages] = useState([])
|
|
|
|
|
|
const [tempImage, setTempImage] = useState(null) // OCR识别后的临时图片
|
|
|
|
|
|
const maxImages = 1
|
|
|
|
|
|
|
|
|
|
|
|
const handleUploadImage = (e) => {
|
|
|
|
|
|
const files = Array.from(e.target.files)
|
|
|
|
|
|
if (files.length === 0) return
|
|
|
|
|
|
if (files.length + uploadImages.length > maxImages) {
|
|
|
|
|
|
alert(`最多只能上传${maxImages}张图片`)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const newImages = files.map(file => ({
|
|
|
|
|
|
file,
|
|
|
|
|
|
preview: URL.createObjectURL(file),
|
|
|
|
|
|
name: file.name,
|
|
|
|
|
|
size: file.size
|
|
|
|
|
|
}))
|
|
|
|
|
|
setUploadImages([...uploadImages, ...newImages])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 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 })
|
|
|
|
|
|
// 只有设置了出售价(goalPrice)才自动设置为"已售"
|
|
|
|
|
|
if (key === 'goalPrice' && 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('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
|
|
|
|
|
|
else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47'
|
2026-04-11 18:38:03 +08:00
|
|
|
|
else if (digits.includes('7') && !digits.includes('4')) cat = '带7号'
|
|
|
|
|
|
else if (digits.includes('4')) cat = '带4号'
|
2026-03-23 11:08:52 +08:00
|
|
|
|
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 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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() }
|
|
|
|
|
|
// 直接对应 AI 返回的字段,不需要二次解析
|
|
|
|
|
|
if (data.fields.issuer) recognizedForm.issuer = data.fields.issuer
|
|
|
|
|
|
if (data.fields.version) recognizedForm.version = data.fields.version
|
|
|
|
|
|
if (data.fields.denomination) recognizedForm.denomination = data.fields.denomination
|
|
|
|
|
|
if (data.fields.prefix_serial) recognizedForm.prefixSerial = data.fields.prefix_serial
|
|
|
|
|
|
if (data.fields.packaging) recognizedForm.packaging = data.fields.packaging
|
|
|
|
|
|
if (data.fields.grading_company) recognizedForm.gradingCompany = data.fields.grading_company
|
|
|
|
|
|
if (data.fields.grading_score) recognizedForm.gradingScore = data.fields.grading_score
|
|
|
|
|
|
if (data.fields.special_mark && data.fields.special_mark !== '无') recognizedForm.specialMark = data.fields.special_mark
|
|
|
|
|
|
if (data.fields.serial_feature && data.fields.serial_feature !== '无') recognizedForm.serialFeature = data.fields.serial_feature
|
|
|
|
|
|
// 布尔字段
|
|
|
|
|
|
if (data.fields.is_graded !== undefined) recognizedForm.isGraded = data.fields.is_graded
|
|
|
|
|
|
if (data.fields.three_star !== undefined) recognizedForm.threeStar = data.fields.three_star
|
|
|
|
|
|
|
|
|
|
|
|
// 保存识别的图片信息(包含服务器临时路径)
|
|
|
|
|
|
const newImage = {
|
|
|
|
|
|
file: selectedImage,
|
|
|
|
|
|
preview: URL.createObjectURL(selectedImage),
|
|
|
|
|
|
name: selectedImage.name,
|
|
|
|
|
|
size: selectedImage.size,
|
|
|
|
|
|
temp_image: data.temp_image // 保存临时图片信息
|
|
|
|
|
|
}
|
|
|
|
|
|
setUploadImages([newImage])
|
|
|
|
|
|
setTempImage(data.temp_image) // 保存临时图片信息
|
|
|
|
|
|
|
|
|
|
|
|
setForm(recognizedForm)
|
|
|
|
|
|
setActiveTab('manual')
|
|
|
|
|
|
console.log('AI 识别结果:', recognizedForm)
|
|
|
|
|
|
console.log('识别图片:', newImage)
|
|
|
|
|
|
} else setError('识别结果为空')
|
|
|
|
|
|
} catch (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 formWithCategory = { ...form }
|
|
|
|
|
|
if (formWithCategory.prefixSerial && !formWithCategory.numberCategory) {
|
|
|
|
|
|
const serial = formWithCategory.prefixSerial
|
|
|
|
|
|
const match = serial.match(/J(\d{9})/)
|
|
|
|
|
|
const digits = match ? match[1] : serial.replace(/\D/g, '').slice(0, 9)
|
|
|
|
|
|
if (digits) {
|
|
|
|
|
|
let cat = ''
|
|
|
|
|
|
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('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
|
|
|
|
|
|
else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47'
|
2026-04-11 18:38:03 +08:00
|
|
|
|
else if (digits.includes('7') && !digits.includes('4')) cat = '带7号'
|
|
|
|
|
|
else if (digits.includes('4')) cat = '带4号'
|
2026-03-23 11:08:52 +08:00
|
|
|
|
else cat = '其他'
|
|
|
|
|
|
formWithCategory.numberCategory = cat
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formData = convertField(formWithCategory)
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 保存藏品信息(先检查是否重复)
|
|
|
|
|
|
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 || '保存失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
const result = await res.json()
|
|
|
|
|
|
let collectionId = result.f99_90_id || result.id
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有错误(重复编号等)
|
|
|
|
|
|
if (result.error) {
|
|
|
|
|
|
throw new Error(result.error.message || '保存失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否有重复警告
|
|
|
|
|
|
if (result.warning && result.warning.code === 'DUPLICATE_SERIAL') {
|
|
|
|
|
|
const { warning } = result
|
|
|
|
|
|
const confirmed = window.confirm(
|
|
|
|
|
|
`⚠️ 发现重复冠字号!\n\n` +
|
|
|
|
|
|
`冠字号:${warning.existing_collection.prefix_serial}\n` +
|
|
|
|
|
|
`已存在于:${warning.existing_collection.name} (编号:${warning.existing_collection.code})\n\n` +
|
|
|
|
|
|
`是否继续保存?`
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (!confirmed) {
|
|
|
|
|
|
setSaving(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 用户确认继续,再次调用 API(添加 force=true 参数)
|
|
|
|
|
|
const forceRes = await fetch('/api/collections?force=true', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
|
|
|
|
|
body: JSON.stringify(formData)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (!forceRes.ok) {
|
|
|
|
|
|
const forceData = await forceRes.json()
|
|
|
|
|
|
throw new Error(forceData.error?.message || '保存失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const forceResult = await forceRes.json()
|
|
|
|
|
|
collectionId = forceResult.f99_90_id || forceResult.id
|
|
|
|
|
|
console.log('藏品保存成功(确认重复),ID:', collectionId)
|
|
|
|
|
|
} else if (collectionId) {
|
|
|
|
|
|
console.log('藏品保存成功,ID:', collectionId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error('保存失败:未返回藏品 ID')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 上传图片(无论是否重复都执行)
|
|
|
|
|
|
let totalUploadCount = 0
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 处理识别的图片(如果有临时图片,直接认领;否则上传)
|
|
|
|
|
|
if (uploadImages.length > 0 && collectionId) {
|
|
|
|
|
|
console.log('处理图片,数量:', uploadImages.length)
|
|
|
|
|
|
|
|
|
|
|
|
for (const img of uploadImages) {
|
|
|
|
|
|
// 检查是否有临时图片(OCR识别后保存的)
|
|
|
|
|
|
if (img.temp_image && img.temp_image.id) {
|
|
|
|
|
|
// 认领临时图片
|
|
|
|
|
|
try {
|
|
|
|
|
|
const claimRes = await fetch(`/api/ocr/claim-temp-image?temp_id=${img.temp_image.id}&collection_id=${collectionId}`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
|
|
|
|
})
|
|
|
|
|
|
if (claimRes.ok) {
|
|
|
|
|
|
totalUploadCount++
|
|
|
|
|
|
console.log('✅ 临时图片认领成功')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('临时图片认领失败:', await claimRes.text())
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (claimErr) {
|
|
|
|
|
|
console.error('临时图片认领异常:', claimErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 普通上传
|
|
|
|
|
|
const imgFormData = new FormData()
|
|
|
|
|
|
imgFormData.append('file', img.file)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const uploadRes = await fetch(`/api/collections/upload-image?collection_id=${collectionId}`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Authorization': 'Bearer ' + token },
|
|
|
|
|
|
body: imgFormData
|
|
|
|
|
|
})
|
|
|
|
|
|
if (uploadRes.ok) {
|
|
|
|
|
|
totalUploadCount++
|
|
|
|
|
|
console.log(`图片上传成功`)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
console.error('图片上传失败:', await uploadRes.text())
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (uploadErr) {
|
|
|
|
|
|
console.error('图片上传异常:', uploadErr)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 共处理 ${totalUploadCount} 张图片`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
alert('保存成功!' + (totalUploadCount > 0 ? `已上传${totalUploadCount}张图片` : ''))
|
|
|
|
|
|
window.location.hash = '#/list'
|
|
|
|
|
|
window.refreshList?.()
|
|
|
|
|
|
window.refreshHome?.()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('保存失败:', e)
|
|
|
|
|
|
alert('保存失败:' + e.message)
|
|
|
|
|
|
}
|
|
|
|
|
|
finally { setSaving(false) }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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' }}>
|
2026-04-11 01:47:55 +08:00
|
|
|
|
AI 识别
|
2026-03-23 11:08:52 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<button onClick={() => setActiveTab('manual')}
|
|
|
|
|
|
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' }}>
|
2026-04-11 01:47:55 +08:00
|
|
|
|
手工录入
|
2026-03-23 11:08:52 +08:00
|
|
|
|
</button>
|
2026-04-11 01:47:55 +08:00
|
|
|
|
<button onClick={() => setActiveTab('deal')}
|
|
|
|
|
|
style={{ flex: 1, padding: '10px', background: activeTab === 'deal' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'deal' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>
|
|
|
|
|
|
行情录入
|
2026-03-23 11:08:52 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* AI 识别模式 */}
|
|
|
|
|
|
{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' }}>
|
2026-04-11 01:47:55 +08:00
|
|
|
|
{recognizing ? '🔍 识别中...' : '开始识别'}
|
2026-03-23 11:08:52 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div onClick={() => { fileInputRef.current.setAttribute('capture', 'environment'); 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.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>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 手工录入模式 - 完整表单 */}
|
|
|
|
|
|
{activeTab === 'manual' && (
|
|
|
|
|
|
<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>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<input ref={imageFileInputRef} type="file" accept="image/*" onChange={handleUploadImage} 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={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', background: 'rgba(0,0,0,0.3)' }} />
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
bottom: '4px',
|
|
|
|
|
|
left: '4px',
|
|
|
|
|
|
right: '4px',
|
|
|
|
|
|
background: 'rgba(0,0,0,0.7)',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
padding: '4px',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
fontSize: '10px',
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
|
|
whiteSpace: 'nowrap'
|
|
|
|
|
|
}}>{img.name}</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newImages = uploadImages.filter((_, i) => i !== index)
|
|
|
|
|
|
setUploadImages(newImages)
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: '8px',
|
|
|
|
|
|
right: '8px',
|
|
|
|
|
|
width: '28px',
|
|
|
|
|
|
height: '28px',
|
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
|
background: 'rgba(239, 68, 68, 0.9)',
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
fontSize: '16px',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center'
|
|
|
|
|
|
}}
|
|
|
|
|
|
>×</button>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{uploadImages.length < 1 && (
|
|
|
|
|
|
<div onClick={() => imageFileInputRef.current?.click()} style={{
|
|
|
|
|
|
aspectRatio: '1.5',
|
|
|
|
|
|
background: 'rgba(255,255,255,0.05)',
|
|
|
|
|
|
border: '2px dashed rgba(255,255,255,0.3)',
|
|
|
|
|
|
borderRadius: '12px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
color: '#94a3b8',
|
|
|
|
|
|
fontSize: '13px'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{ fontSize: '32px', marginBottom: '8px' }}>📷</div>
|
|
|
|
|
|
<div>添加图片</div>
|
|
|
|
|
|
<div style={{ fontSize: '11px', marginTop: '4px' }}>最多可上传 1 张</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div onClick={() => imageFileInputRef.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: '13px'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
<div style={{ fontSize: '48px', marginBottom: '8px' }}>📷</div>
|
|
|
|
|
|
<div>点击上传图片</div>
|
|
|
|
|
|
<div style={{ fontSize: '11px', marginTop: '4px', 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: '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={() => setActiveTab('ai')}
|
|
|
|
|
|
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' }}>
|
|
|
|
|
|
🔄 切换到 AI 识别
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-04-11 01:47:55 +08:00
|
|
|
|
|
2026-03-23 11:08:52 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 版本号 */}
|
2026-04-11 01:47:55 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 行情录入模式 */}
|
|
|
|
|
|
{activeTab === 'deal' && (
|
|
|
|
|
|
<div style={{ padding: '16px' }}>
|
|
|
|
|
|
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '20px', marginBottom: '16px' }}>
|
|
|
|
|
|
<div style={{ color: '#fbbf24', fontSize: '16px', fontWeight: 'bold', marginBottom: '16px' }}>成交行情录入</div>
|
|
|
|
|
|
|
2026-04-11 16:30:18 +08:00
|
|
|
|
{/* 单条/批量切换 */}
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
|
|
|
|
|
|
<button onClick={() => setDealMode('single')}
|
|
|
|
|
|
style={{ flex: 1, padding: '10px', background: dealMode === 'single' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: dealMode === 'single' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
|
|
|
|
|
|
单条录入
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button onClick={() => setDealMode('batch')}
|
|
|
|
|
|
style={{ flex: 1, padding: '10px', background: dealMode === 'batch' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: dealMode === 'batch' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
|
|
|
|
|
|
批量录入
|
|
|
|
|
|
</button>
|
2026-04-11 01:47:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-11 16:30:18 +08:00
|
|
|
|
{/* 单条录入 */}
|
|
|
|
|
|
{dealMode === 'single' && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div style={{ marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>冠字号 *</div>
|
|
|
|
|
|
<input value={dealForm.serial} onChange={(e) => setDealForm({...dealForm, serial: e.target.value.toUpperCase(), category: autoCategory(e.target.value)})}
|
|
|
|
|
|
placeholder="J0xxxxxxxx"
|
|
|
|
|
|
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{dealForm.category && (
|
|
|
|
|
|
<div style={{ background: 'rgba(251,191,36,0.2)', padding: '10px', borderRadius: '8px', textAlign: 'center', marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ fontSize: '12px', color: '#94a3b8' }}>号码分类</div>
|
|
|
|
|
|
<div style={{ fontSize: '14px', color: '#fbbf24', fontWeight: 'bold' }}>{dealForm.category}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>包装类型</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
|
|
|
|
{['单张', '标十', '标百'].map(p => (
|
|
|
|
|
|
<button key={p} onClick={() => setDealForm({...dealForm, packaging: p})}
|
|
|
|
|
|
style={{ flex: 1, padding: '10px', background: dealForm.packaging === p ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: dealForm.packaging === p ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
|
|
|
|
|
|
{p}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交价格 *</div>
|
|
|
|
|
|
<input type="number" value={dealForm.price} onChange={(e) => setDealForm({...dealForm, price: e.target.value})}
|
|
|
|
|
|
placeholder="请输入成交价格"
|
|
|
|
|
|
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交平台 *</div>
|
|
|
|
|
|
<select value={dealForm.platform} onChange={(e) => setDealForm({...dealForm, platform: e.target.value})}
|
|
|
|
|
|
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }}>
|
|
|
|
|
|
<option value="淘宝">淘宝</option>
|
|
|
|
|
|
<option value="咸鱼">咸鱼</option>
|
|
|
|
|
|
<option value="抖音">抖音</option>
|
|
|
|
|
|
<option value="快手">快手</option>
|
|
|
|
|
|
<option value="微拍堂">微拍堂</option>
|
|
|
|
|
|
<option value="拼多多">拼多多</option>
|
|
|
|
|
|
<option value="一尘">一尘</option>
|
|
|
|
|
|
<option value="爱藏">爱藏</option>
|
|
|
|
|
|
<option value="其他">其他</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '12px', marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ flex: 1 }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>出售者</div>
|
|
|
|
|
|
<input value={dealForm.seller} onChange={(e) => setDealForm({...dealForm, seller: e.target.value})}
|
|
|
|
|
|
placeholder="请输入出售者"
|
|
|
|
|
|
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: 1 }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>购买者</div>
|
|
|
|
|
|
<input value={dealForm.buyer} onChange={(e) => setDealForm({...dealForm, buyer: e.target.value})}
|
|
|
|
|
|
placeholder="请输入购买者"
|
|
|
|
|
|
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div style={{ marginBottom: '16px' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交日期 *</div>
|
|
|
|
|
|
<input type="date" value={dealForm.date} onChange={(e) => setDealForm({...dealForm, date: e.target.value})}
|
|
|
|
|
|
style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 评级字段 */}
|
|
|
|
|
|
<div style={{ marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>评级(可选)</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
|
|
|
|
<select value={dealForm.gradingCompany || ''} onChange={(e) => setDealForm({...dealForm, gradingCompany: e.target.value})}
|
|
|
|
|
|
style={{ flex: 1, padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '14px' }}>
|
|
|
|
|
|
<option value="">评级机构</option>
|
|
|
|
|
|
<option value="PCGS">PCGS</option>
|
|
|
|
|
|
<option value="PMG">PMG</option>
|
|
|
|
|
|
<option value="ACG">ACG</option>
|
|
|
|
|
|
<option value="爱藏">爱藏</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<input value={dealForm.gradingScore || ''} onChange={(e) => setDealForm({...dealForm, gradingScore: e.target.value})}
|
|
|
|
|
|
placeholder="评级分数"
|
|
|
|
|
|
style={{ flex: 1, padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '14px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button onClick={async () => {
|
|
|
|
|
|
if (!dealForm.serial || !dealForm.price || !dealForm.platform || !dealForm.date) {
|
|
|
|
|
|
alert('请填写所有必填项')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setSavingDeal(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const token = localStorage.getItem('token')
|
|
|
|
|
|
const digits = dealForm.serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
|
|
|
|
|
|
let tailNumber = '', sizeType = ''
|
|
|
|
|
|
if (dealForm.packaging === '标十' && digits.length >= 2) {
|
|
|
|
|
|
tailNumber = digits.slice(-2)
|
|
|
|
|
|
sizeType = ['01','11','21','31','41','51'].includes(tailNumber) ? '小号' : '大号'
|
|
|
|
|
|
} else if (dealForm.packaging === '标百' && digits.length >= 3) {
|
|
|
|
|
|
tailNumber = digits.slice(-3)
|
|
|
|
|
|
sizeType = ['101','201','301','401','501'].includes(tailNumber) ? '小号' : '大号'
|
|
|
|
|
|
}
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/api/information/`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
info_type: 'deal',
|
|
|
|
|
|
title: `J0${dealForm.serial.replace('J', '').slice(0, 8)}-¥${dealForm.price}`,
|
|
|
|
|
|
content: `分类: ${dealForm.category || '未分类'}\n尾号: ${tailNumber || '-'}\n大小号: ${sizeType || '-'}\n包装: ${dealForm.packaging}\n平台: ${dealForm.platform}\n价格: ¥${dealForm.price}\n出售者: ${dealForm.seller || '-'}\n购买者: ${dealForm.buyer || '-'}\n日期: ${dealForm.date}\n评级: ${dealForm.gradingCompany ? dealForm.gradingCompany + ' ' + dealForm.gradingScore : '未评级'}`,
|
|
|
|
|
|
deal_price: parseFloat(dealForm.price),
|
|
|
|
|
|
deal_date: dealForm.date,
|
|
|
|
|
|
packaging: dealForm.packaging,
|
|
|
|
|
|
category: dealForm.category,
|
|
|
|
|
|
is_graded: !!dealForm.gradingCompany,
|
|
|
|
|
|
grading_company: dealForm.gradingCompany || '',
|
|
|
|
|
|
grading_score: dealForm.gradingScore || ''
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
alert('行情录入成功!')
|
|
|
|
|
|
setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '淘宝', seller: '', buyer: '', date: new Date().toISOString().split('T')[0] })
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
alert('录入失败: ' + (data.detail || '未知错误'))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
alert('提交失败: ' + e.message)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSavingDeal(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
disabled={savingDeal}
|
|
|
|
|
|
style={{ width: '100%', padding: '14px', background: savingDeal ? '#64748b' : '#fbbf24', color: savingDeal ? '#94a3b8' : '#1e293b', border: 'none', borderRadius: '10px', fontSize: '16px', fontWeight: '600', cursor: savingDeal ? 'not-allowed' : 'pointer' }}>
|
|
|
|
|
|
{savingDeal ? '提交中...' : '提交行情'}
|
|
|
|
|
|
</button>
|
2026-04-11 01:47:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-04-11 16:30:18 +08:00
|
|
|
|
{/* 批量录入 */}
|
|
|
|
|
|
{dealMode === 'batch' && (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
{/* 日期和平台 */}
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ flex: 1 }}>
|
|
|
|
|
|
<div style={{ fontSize: '10px', color: '#64748b', marginBottom: '4px' }}>默认日期(可选)</div>
|
|
|
|
|
|
<input type="date" value={batchDefaultDate || ''} onChange={(e) => setBatchDefaultDate(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: '12px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: 1 }}>
|
|
|
|
|
|
<div style={{ fontSize: '10px', color: '#64748b', marginBottom: '4px' }}>成交平台(可选)</div>
|
|
|
|
|
|
<select value={batchDefaultPlatform || ''} onChange={(e) => setBatchDefaultPlatform(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: '12px' }}>
|
|
|
|
|
|
<option value="">请选择</option>
|
|
|
|
|
|
<option value="淘宝">淘宝</option>
|
|
|
|
|
|
<option value="咸鱼">咸鱼</option>
|
|
|
|
|
|
<option value="抖音">抖音</option>
|
|
|
|
|
|
<option value="快手">快手</option>
|
|
|
|
|
|
<option value="微拍堂">微拍堂</option>
|
|
|
|
|
|
<option value="拼多多">拼多多</option>
|
|
|
|
|
|
<option value="一尘">一尘</option>
|
|
|
|
|
|
<option value="爱藏">爱藏</option>
|
|
|
|
|
|
<option value="其他">其他</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 包装类型选择 */}
|
|
|
|
|
|
<div style={{ marginBottom: '12px' }}>
|
|
|
|
|
|
<div style={{ fontSize: '10px', color: '#64748b', marginBottom: '6px' }}>批量默认包装类型(可选)</div>
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '6px' }}>
|
|
|
|
|
|
<button onClick={() => setBatchDefaultPackaging(batchDefaultPackaging === '单张' ? '' : '单张')}
|
|
|
|
|
|
style={{ flex: 1, padding: '8px', background: batchDefaultPackaging === '单张' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: batchDefaultPackaging === '单张' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '12px', cursor: 'pointer' }}>
|
|
|
|
|
|
单张
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button onClick={() => setBatchDefaultPackaging(batchDefaultPackaging === '标十' ? '' : '标十')}
|
|
|
|
|
|
style={{ flex: 1, padding: '8px', background: batchDefaultPackaging === '标十' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: batchDefaultPackaging === '标十' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '12px', cursor: 'pointer' }}>
|
|
|
|
|
|
标十
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button onClick={() => setBatchDefaultPackaging(batchDefaultPackaging === '标百' ? '' : '标百')}
|
|
|
|
|
|
style={{ flex: 1, padding: '8px', background: batchDefaultPackaging === '标百' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: batchDefaultPackaging === '标百' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '12px', cursor: 'pointer' }}>
|
|
|
|
|
|
标百
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<textarea value={batchText} onChange={(e) => setBatchText(e.target.value)}
|
|
|
|
|
|
placeholder="粘贴批量行情文本..." style={{ width: '100%', minHeight: '120px', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '14px' }} />
|
|
|
|
|
|
<button onClick={async () => {
|
|
|
|
|
|
if (!batchText.trim()) { alert('请先粘贴行情文本'); return }
|
|
|
|
|
|
setParsing(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`${API_BASE}/api/information/batch-parse-local`, {
|
|
|
|
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({ text: batchText, defaultPackaging: batchDefaultPackaging, defaultDate: batchDefaultDate, defaultPlatform: batchDefaultPlatform })
|
|
|
|
|
|
})
|
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
if (data.success) { setBatchResult(data.data || []); alert(`解析成功${data.data.length}条`) }
|
|
|
|
|
|
else { alert('解析失败: ' + (data.error || '未知错误')) }
|
|
|
|
|
|
} catch (e) { alert('请求失败: ' + e.message) }
|
|
|
|
|
|
finally { setParsing(false) }
|
|
|
|
|
|
}}
|
|
|
|
|
|
disabled={parsing} style={{ width: '100%', marginTop: '8px', padding: '10px', background: parsing ? '#64748b' : '#22c55e', color: '#fff', border: 'none', borderRadius: '8px' }}>
|
|
|
|
|
|
{parsing ? '解析中...' : '解析文本'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
{batchResult.length > 0 && (
|
|
|
|
|
|
<div style={{ marginTop: '12px' }}>
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
|
|
|
|
|
<div style={{ color: '#94a3b8', fontSize: '12px' }}>解析结果 ({batchResult.length}条)</div>
|
|
|
|
|
|
<button onClick={() => { setBatchResult([]); setBatchText('') }} style={{ padding: '4px 8px', background: 'rgba(239,68,68,0.2)', color: '#ef4444', border: 'none', borderRadius: '4px', fontSize: '11px', cursor: 'pointer' }}>清空</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ maxHeight: '350px', overflowY: 'auto' }}>
|
|
|
|
|
|
{batchResult.map((item, idx) => (
|
|
|
|
|
|
<div key={idx} style={{ padding: '8px', background: 'rgba(255,255,255,0.05)', marginBottom: '6px', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.1)' }}>
|
|
|
|
|
|
{/* 第一行:冠字号 | 价格 | 分类 | 包装 */}
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '6px', marginBottom: '4px' }}>
|
|
|
|
|
|
<div style={{ flex: '0 0 35%' }}>
|
|
|
|
|
|
<input value={batchResult[idx].serial} onChange={(e) => {
|
|
|
|
|
|
const newResult = [...batchResult]
|
|
|
|
|
|
newResult[idx].serial = e.target.value
|
|
|
|
|
|
setBatchResult(newResult)
|
|
|
|
|
|
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fbbf24', fontSize: '12px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: '0 0 25%' }}>
|
|
|
|
|
|
<input type="number" value={batchResult[idx].price} onChange={(e) => {
|
|
|
|
|
|
const newResult = [...batchResult]
|
|
|
|
|
|
newResult[idx].price = parseFloat(e.target.value) || 0
|
|
|
|
|
|
setBatchResult(newResult)
|
|
|
|
|
|
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#22c55e', fontSize: '12px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: '0 0 20%' }}>
|
|
|
|
|
|
<select value={batchResult[idx].category} onChange={(e) => {
|
|
|
|
|
|
const newResult = [...batchResult]
|
|
|
|
|
|
newResult[idx].category = e.target.value
|
|
|
|
|
|
setBatchResult(newResult)
|
|
|
|
|
|
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fff', fontSize: '10px' }}>
|
2026-04-11 18:38:03 +08:00
|
|
|
|
{['圆圆号','倒置号','金马王','金马号','金山王','天马王','金山号','天马号','朦胧王','朦胧号','如意号','钻石号','永恒号','带7号','带4号'].map(c => <option key={c} value={c}>{c}</option>)}
|
2026-04-11 16:30:18 +08:00
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: '0 0 20%' }}>
|
|
|
|
|
|
<select value={batchResult[idx].packaging || '单张'} onChange={(e) => {
|
|
|
|
|
|
const newResult = [...batchResult]
|
|
|
|
|
|
newResult[idx].packaging = e.target.value
|
|
|
|
|
|
setBatchResult(newResult)
|
|
|
|
|
|
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fff', fontSize: '10px' }}>
|
|
|
|
|
|
<option value="单张">单张</option>
|
|
|
|
|
|
<option value="标十">标十</option>
|
|
|
|
|
|
<option value="标百">标百</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{/* 第二行:评级机构 | 分数 | 出售者 | 删除 */}
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '6px' }}>
|
|
|
|
|
|
<div style={{ flex: '0 0 22%' }}>
|
|
|
|
|
|
<select value={batchResult[idx].grading_company || ''} onChange={(e) => {
|
|
|
|
|
|
const newResult = [...batchResult]
|
|
|
|
|
|
newResult[idx].grading_company = e.target.value
|
|
|
|
|
|
newResult[idx].is_graded = !!e.target.value
|
|
|
|
|
|
setBatchResult(newResult)
|
|
|
|
|
|
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fff', fontSize: '10px' }}>
|
|
|
|
|
|
<option value="">未评级</option>
|
|
|
|
|
|
<option value="PCGS">PCGS</option>
|
|
|
|
|
|
<option value="PMG">PMG</option>
|
|
|
|
|
|
<option value="ACG">ACG</option>
|
|
|
|
|
|
<option value="爱藏">爱藏</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: '0 0 18%' }}>
|
|
|
|
|
|
<input value={batchResult[idx].grade || ''} onChange={(e) => {
|
|
|
|
|
|
const newResult = [...batchResult]
|
|
|
|
|
|
newResult[idx].grade = e.target.value
|
|
|
|
|
|
setBatchResult(newResult)
|
|
|
|
|
|
}} placeholder="分数" style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#06b6d4', fontSize: '11px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: '0 0 30%' }}>
|
|
|
|
|
|
<input value={batchResult[idx].seller || ''} onChange={(e) => {
|
|
|
|
|
|
const newResult = [...batchResult]
|
|
|
|
|
|
newResult[idx].seller = e.target.value
|
|
|
|
|
|
setBatchResult(newResult)
|
|
|
|
|
|
}} placeholder="出售者" style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fff', fontSize: '11px' }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={{ flex: '0 0 30%', textAlign: 'right' }}>
|
|
|
|
|
|
<button onClick={() => {
|
|
|
|
|
|
const newResult = batchResult.filter((_, i) => i !== idx)
|
|
|
|
|
|
setBatchResult(newResult)
|
|
|
|
|
|
}} style={{ padding: '4px 8px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', fontSize: '10px', cursor: 'pointer' }}>删除</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button onClick={async () => {
|
|
|
|
|
|
setSavingDeal(true)
|
|
|
|
|
|
let count = 0; const token = localStorage.getItem('token')
|
|
|
|
|
|
for (const item of batchResult) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 计算尾号和大小号
|
|
|
|
|
|
const digits = (item.serial || '').replace('J', '').replace(/[^0-9]/g, '')
|
|
|
|
|
|
let tail_number = '', size_type = ''
|
|
|
|
|
|
if ((item.packaging === '标十' || item.packaging === '标百') && digits.length >= 2) {
|
|
|
|
|
|
tail_number = item.packaging === '标十' ? digits.slice(-2) : digits.slice(-3)
|
|
|
|
|
|
size_type = (item.packaging === '标十' && ['01','11','21','31','41','51'].includes(tail_number)) || (item.packaging === '标百' && ['101','201','301','401','501'].includes(tail_number)) ? '小号' : '大号'
|
|
|
|
|
|
}
|
|
|
|
|
|
await fetch(`${API_BASE}/api/information/`, {
|
|
|
|
|
|
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
info_type: 'deal',
|
|
|
|
|
|
title: `${item.serial}-¥${item.price}`,
|
|
|
|
|
|
content: `分类: ${item.category || '-'}\n尾号: ${tail_number || '-'}\n大小号: ${size_type || '-'}\n包装: ${item.packaging || '单张'}\n平台: ${item.platform || '-'}\n价格: ¥${item.price}\n出售者: ${item.seller || '-'}\n购买者: ${item.buyer || '-'}\n日期: ${item.deal_date || new Date().toISOString().split('T')[0]}`,
|
|
|
|
|
|
deal_price: parseFloat(item.price),
|
|
|
|
|
|
deal_date: item.deal_date || new Date().toISOString().split('T')[0],
|
|
|
|
|
|
packaging: item.packaging || '单张',
|
|
|
|
|
|
is_graded: item.is_graded || false,
|
|
|
|
|
|
grading_company: item.grading_company || '',
|
|
|
|
|
|
grading_score: item.grade || '',
|
|
|
|
|
|
category: item.category || '',
|
|
|
|
|
|
tail_number: tail_number,
|
|
|
|
|
|
size_type: size_type
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
count++
|
|
|
|
|
|
} catch(e) { console.error(e) }
|
|
|
|
|
|
}
|
|
|
|
|
|
alert(`完成${count}条`); setBatchResult([]); setBatchText(''); setSavingDeal(false)
|
|
|
|
|
|
}} disabled={savingDeal} style={{ width: '100%', marginTop: '8px', padding: '10px', background: savingDeal?'#64748b':'#fbbf24', color: savingDeal?'#94a3b8':'#1e293b', border:'none', borderRadius:'8px' }}>
|
|
|
|
|
|
{savingDeal ? '提交中...' : `批量录入${batchResult.length}条`}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-04-11 01:47:55 +08:00
|
|
|
|
</div>
|
2026-04-11 16:30:18 +08:00
|
|
|
|
)}
|
2026-04-11 01:47:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-03-23 11:08:52 +08:00
|
|
|
|
<div style={{ position: 'fixed', bottom: '16px', right: '16px', color: 'rgba(255,255,255,0.2)', fontSize: '11px', zIndex: 100 }}>v{APP_VERSION}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|