v1.2.82 - 行情功能优化:批量录入增强、我的行情页面完善

- 批量录入增加默认日期、成交平台、默认包装类型选项
- 我的行情页面优化显示:冠字号金色、编号白色、大小号显示
- 编辑界面增加平台、出售者、购买者字段
- 导航栏优化:我的、行情
- 修复批量解析大小号计算问题
- 自动生成行情编号(日期+5位自然数)
This commit is contained in:
甲辰生产 2026-04-11 16:30:18 +08:00
parent bd22c20108
commit 74589883b6
6 changed files with 1004 additions and 121 deletions

View File

@ -1 +1 @@
VERSION=1.2.79 1.2.82

View File

@ -180,6 +180,16 @@ class Information(Base):
deal_price = Column(Float, nullable=True) deal_price = Column(Float, nullable=True)
deal_date = Column(Date, nullable=True) deal_date = Column(Date, nullable=True)
# 评级相关字段
packaging = Column(String(50), nullable=True)
is_graded = Column(Boolean, default=False)
grading_company = Column(String(100), nullable=True)
grading_score = Column(String(50), nullable=True)
category = Column(String(100), nullable=True)
# 行情编号
deal_no = Column(String(50), nullable=True, index=True)
# 状态: active-有效, closed-已关闭, expired-已过期 # 状态: active-有效, closed-已关闭, expired-已过期
status = Column(String(20), default="active", index=True) status = Column(String(20), default="active", index=True)

View File

@ -1,10 +1,11 @@
# 资讯API路由 # 资讯API路由
from fastapi import APIRouter, Depends, HTTPException, Query, Response from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from sqlalchemy import text from sqlalchemy import text
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
from datetime import datetime, date from datetime import datetime, date
import os
from app.core.database import get_db from app.core.database import get_db
from app.core.auth import get_current_user from app.core.auth import get_current_user
@ -28,6 +29,12 @@ class InformationCreate(BaseModel):
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
deal_price: Optional[float] = None deal_price: Optional[float] = None
deal_date: Optional[date] = None deal_date: Optional[date] = None
packaging: Optional[str] = None
is_graded: Optional[bool] = False
grading_company: Optional[str] = None
grading_score: Optional[str] = None
category: Optional[str] = None
deal_no: Optional[str] = None
class InformationUpdate(BaseModel): class InformationUpdate(BaseModel):
@ -42,6 +49,10 @@ class InformationUpdate(BaseModel):
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
deal_price: Optional[float] = None deal_price: Optional[float] = None
deal_date: Optional[date] = None deal_date: Optional[date] = None
packaging: Optional[str] = None
is_graded: Optional[bool] = None
grading_company: Optional[str] = None
grading_score: Optional[str] = None
class InformationResponse(BaseModel): class InformationResponse(BaseModel):
@ -66,6 +77,13 @@ class InformationResponse(BaseModel):
view_count: int view_count: int
contact_count: int contact_count: int
created_at: datetime created_at: datetime
# 评级相关字段
packaging: Optional[str] = None
is_graded: Optional[bool] = False
grading_company: Optional[str] = None
grading_score: Optional[str] = None
category: Optional[str] = None
deal_no: Optional[str] = None
# 用户信息 # 用户信息
user_name: Optional[str] = None user_name: Optional[str] = None
user_avatar: Optional[str] = None user_avatar: Optional[str] = None
@ -89,7 +107,7 @@ def get_information_list(
info_type: Optional[str] = Query(None, description="类型: seek/deal/publish"), info_type: Optional[str] = Query(None, description="类型: seek/deal/publish"),
status: str = Query("active", description="状态: active/closed/expired"), status: str = Query("active", description="状态: active/closed/expired"),
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100), page_size: int = Query(20, ge=1, le=500),
current_user: Optional[User] = Depends(get_current_user), current_user: Optional[User] = Depends(get_current_user),
db: Session = Depends(get_db), db: Session = Depends(get_db),
response: Response = None response: Response = None
@ -146,6 +164,12 @@ def get_information_list(
collection_category=item.collection.f01_03_category if item.collection else None, collection_category=item.collection.f01_03_category if item.collection else None,
collection_version=item.collection.f02_11_version if item.collection else None, collection_version=item.collection.f02_11_version if item.collection else None,
collection_number=item.collection.f02_10_prefix_serial if item.collection else None, collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
packaging=item.packaging,
is_graded=item.is_graded or False,
grading_company=item.grading_company,
grading_score=item.grading_score,
category=item.category,
deal_no=item.deal_no,
matched_count=matched_count, matched_count=matched_count,
network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0, network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0,
)) ))
@ -394,6 +418,11 @@ def get_information(
view_count=item.view_count, view_count=item.view_count,
contact_count=item.contact_count, contact_count=item.contact_count,
created_at=item.created_at, created_at=item.created_at,
packaging=item.packaging,
is_graded=item.is_graded or False,
grading_company=item.grading_company,
grading_score=item.grading_score,
category=item.category,
user_name=item.user.f01_01_name if item.user else None, user_name=item.user.f01_01_name if item.user else None,
user_avatar=item.user.avatar if item.user else None, user_avatar=item.user.avatar if item.user else None,
collection_name=item.collection.f01_01_name if item.collection else None, collection_name=item.collection.f01_01_name if item.collection else None,
@ -411,6 +440,20 @@ def create_information(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""发布资讯""" """发布资讯"""
# 生成行情编号:日期 + 5位自然数从00001开始
deal_no = None
if data.info_type == 'deal':
today = datetime.now().strftime('%Y%m%d')
# 查询当天已有行情数量
from app.models.models import Information
count_today = db.query(Information).filter(
Information.info_type == 'deal',
Information.deal_no.like(f'DJ{today}%')
).count()
# 编号 = 日期 + 5位自然数如 DJ2026041100001
seq = count_today + 1
deal_no = f'DJ{today}{seq:05d}'
info = Information( info = Information(
user_id=current_user.f99_90_id, user_id=current_user.f99_90_id,
info_type=data.info_type, info_type=data.info_type,
@ -425,6 +468,12 @@ def create_information(
expect_price_max=data.expect_price_max, expect_price_max=data.expect_price_max,
deal_price=data.deal_price, deal_price=data.deal_price,
deal_date=data.deal_date, deal_date=data.deal_date,
packaging=data.packaging,
is_graded=data.is_graded or False,
grading_company=data.grading_company,
grading_score=data.grading_score,
category=data.category,
deal_no=deal_no,
status="active" status="active"
) )
db.add(info) db.add(info)
@ -499,6 +548,16 @@ def update_information(
info.deal_price = data.deal_price info.deal_price = data.deal_price
if data.deal_date is not None: if data.deal_date is not None:
info.deal_date = data.deal_date info.deal_date = data.deal_date
if data.packaging is not None:
info.packaging = data.packaging
if data.is_graded is not None:
info.is_graded = data.is_graded
if data.grading_company is not None:
info.grading_company = data.grading_company
if data.grading_score is not None:
info.grading_score = data.grading_score
if data.category is not None:
info.category = data.category
db.commit() db.commit()
db.refresh(info) db.refresh(info)
@ -522,6 +581,11 @@ def update_information(
view_count=info.view_count, view_count=info.view_count,
contact_count=info.contact_count, contact_count=info.contact_count,
created_at=info.created_at, created_at=info.created_at,
packaging=info.packaging,
is_graded=info.is_graded or False,
grading_company=info.grading_company,
grading_score=info.grading_score,
category=info.category,
user_name=current_user.f01_01_name, user_name=current_user.f01_01_name,
user_avatar=current_user.avatar, user_avatar=current_user.avatar,
collection_name=info.collection.f01_01_name if info.collection else None, collection_name=info.collection.f01_01_name if info.collection else None,
@ -796,7 +860,7 @@ def get_deal_stats(
@router.get("/my/list", response_model=List[InformationResponse]) @router.get("/my/list", response_model=List[InformationResponse])
def get_my_information_list( def get_my_information_list(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100), page_size: int = Query(20, ge=1, le=500),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
@ -1124,3 +1188,272 @@ def get_seek_stats(
"userMatchedCount": user_matched_count, "userMatchedCount": user_matched_count,
"totalMatchedCount": total_matched_count "totalMatchedCount": total_matched_count
} }
# 批量解析行情数据API
@router.post("/batch-parse")
async def batch_parse_deals(text: str = Body(..., embed=True)):
"""使用AI智能解析批量行情文本"""
import httpx
import json
import re
# 使用阿里云百炼Coding Plan API
api_key = "sk-sp-d5ce68bb203e48ca857c2aea25255b26"
base_url = "https://coding.dashscope.aliyuncs.com/v1"
# 更详细的解析提示词
prompt = f"""你是一个专业的龙钞行情数据提取助手。请从以下文本中提取所有龙钞行情记录。
解析规则
1. 每条记录格式冠字号 价格 评级/包装 出售者
2. 冠字号J0开头的9位数字如J0298810101
3. 价格¥xxx,xxx 格式去掉逗号转为数字
4. 评级/包装PC69/PMG68/爱藏67+/爱藏67 标十 标百 单张
5. 出售者人名
6. 号码分类根据冠字号数字特征判断圆圆号/倒置号/金马王/金马号/金山王/天马王/金山号/天马号/朦胧王/朦胧号/如意号/钻石/永恒/无4/通货
输出格式
返回JSON数组每条记录包含
- serial: 冠字号完整9位如J0298810101
- price: 价格数字
- grade: 评级如PC69, PMG68, 爱藏67+, 爱藏67
- packaging: 包装类型标十/标百/单张
- category: 号码分类
- seller: 出售者
- date: 交易日从文本中提取日期如2026-03-29
只返回JSON数组不要其他内容
文本
{text}"""
try:
async with httpx.AsyncClient(timeout=120.0) as client:
response = await client.post(
f"{base_url}/chat/completions",
json={
"model": "qwen3.6-plus",
"messages": [
{"role": "system", "content": "你是一个专业的收藏品行情数据提取助手擅长从文本中提取结构化的交易数据。只返回JSON数组。"},
{"role": "user", "content": prompt}
],
"temperature": 0.1
},
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
)
if response.status_code != 200:
return {"success": False, "error": f"API错误: {response.status_code}, {response.text[:200]}"}
result = response.json()
# 阿里云百炼OpenAI兼容格式
choices = result.get("choices", [])
content = ""
if choices and len(choices) > 0:
content = choices[0].get("message", {}).get("content", "")
# 解析JSON
try:
# 尝试提取JSON
if "```json" in content:
content = content.split("```json")[1].split("```")[0]
elif "```" in content:
content = content.split("```")[1].split("```")[0]
# 尝试直接解析
data = json.loads(content.strip())
return {"success": True, "data": data}
except json.JSONDecodeError:
# 尝试用正则提取
match = re.search(r'\[.*\]', content, re.DOTALL)
if match:
try:
data = json.loads(match.group())
return {"success": True, "data": data}
except:
pass
return {"success": False, "error": "解析失败", "raw": content[:500]}
except Exception as e:
return {"success": False, "error": str(e)}
# 本地正则解析函数
def parse_deals_locally(text: str, default_packaging: str = '', default_date: str = '', default_platform: str = ''):
"""本地正则解析批量行情文本"""
import re
from datetime import datetime
results = []
# 尝试从文本中提取日期(可能出现在标题或时间戳中)
# 格式如: 3月29日, 2026年3月29日, 2026-03-29
date_patterns = [
r'(\d{1,2})月(\d{1,2})日',
r'(\d{4})年(\d{1,2})月(\d{1,2})日',
r'(\d{4})-(\d{1,2})-(\d{1,2})'
]
extracted_date = None
for pattern in date_patterns:
match = re.search(pattern, text)
if match:
try:
if len(match.groups()) == 2:
# 3月29日 - 使用当前年份
extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}"
elif len(match.groups()) == 3:
if int(match.group(1)) > 2000:
# 2026年3月29日
extracted_date = f"{match.group(1)}-{int(match.group(2)):02d}-{int(match.group(3)):02d}"
else:
# 3月29日格式
extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}"
break
except:
pass
# 默认使用今天
default_date = datetime.now().strftime('%Y-%m-%d')
deal_date = extracted_date or default_date
lines = text.strip().split('\n')
for line in lines:
line = line.strip()
if not line or 'J0' not in line:
continue
# 提取冠字号 J0 + 8-9位数字
serial_match = re.search(r'J0(\d{8,9})', line)
if not serial_match:
continue
serial_num = serial_match.group(1)
if len(serial_num) == 9:
serial_num = serial_num[:8]
serial = 'J0' + serial_num
# 提取价格 ¥xxx,xxx 或 xxx,xxx必须在J0之后
serial_pos = line.find(serial)
after_serial = line[serial_pos + len(serial):]
price_match = re.search(r'[¥¥]?\s*([1-9][\d,]{2,})', after_serial)
if not price_match:
continue
price = int(price_match.group(1).replace(',', ''))
# 提取卖家(价格后面的中文字符)
after_price_pos = after_serial.find(price_match.group(0)) + len(price_match.group(0))
after_price = after_serial[after_price_pos:]
seller_match = re.search(r'([^\d\s¥¥]+?)(?:\s*$|\s*\n)', after_price)
seller = seller_match.group(1).strip() if seller_match else ''
# 分类判断
digits = serial_num
d = digits
category = '通货'
if not any(c in d for c in ['1','2','3','4','5','7']): category = '圆圆号'
elif not any(c in d for c in ['2','3','4','5','7']): category = '倒置号'
elif not any(c in d for c in ['1','2','3','4','7']): category = '金马王'
elif not any(c in d for c in ['2','3','4','7']): category = '金马号'
elif not any(c in d for c in ['1','2','4','5','7']): category = '金山王'
elif not any(c in d for c in ['1','2','4','7']): category = '天马王'
elif not any(c in d for c in ['2','4','5','7']): category = '金山号'
elif not any(c in d for c in ['2','4','7']): category = '天马号'
elif not any(c in d for c in ['1','3','4','5','7']): category = '朦胧王'
elif not any(c in d for c in ['3','4','5','7']): category = '朦胧号'
elif not any(c in d for c in ['1','3','4','7']): category = '如意号'
elif not any(c in d for c in ['3','4','7']): category = '钻石'
elif not any(c in d for c in ['4','7']): category = '永恒'
elif '4' not in d: category = '无4'
# 提取评级机构 PCGS/PMG/ACG/爱藏
grade = ''
grading_company = ''
packaging = '单张'
if 'PC69' in line or 'PC68' in line or 'PC67' in line:
grade_match = re.search(r'PC(6[789]|5\d?)', line)
grade = 'PC' + grade_match.group(1) if grade_match else ''
grading_company = 'PCGS'
elif 'PMG68' in line or 'PMG67' in line:
grade_match = re.search(r'PMG(6[789]|5\d?)', line)
grade = 'PMG' + grade_match.group(1) if grade_match else ''
grading_company = 'PMG'
elif 'ACG' in line:
grade_match = re.search(r'ACG(6[789]|5\d?)', line)
grade = 'ACG' + grade_match.group(1) if grade_match else ''
grading_company = 'ACG'
elif '爱藏67+' in line:
grade = '67+'
grading_company = '爱藏'
elif '爱藏67' in line:
grade = '67'
grading_company = '爱藏'
# 判断包装类型
packaging = '单张'
# 如果传入了默认包装类型,先使用默认
if default_packaging:
packaging = default_packaging
# 简化识别:带"刀"字=标百,带"标"字=标十
if '' in line:
packaging = '标百'
elif '' in line:
packaging = '标十'
# 尾号判断如果冠字号尾号是01/11/21/31/41/51/61/71/81/91且有刀/标字样,基本确认是标百
if len(serial_num) >= 2:
tail = serial_num[-2:]
if tail in ['01', '11', '21', '31', '41', '51', '61', '71', '81', '91']:
if '' in line or ('' in line and packaging == '单张'):
packaging = '标百'
packaging = '标百'
# 如果没有刀/标字样但尾号是01且没有其他特征可能是标百
if packaging == '单张' and len(serial_num) >= 2:
tail = serial_num[-2:]
if tail == '01':
# 检查是否在特定语境下
packaging = '标百'
# 计算尾号和大小号
tail_number = ''
size_type = ''
if packaging == '标十' and len(serial_num) >= 2:
tail_number = serial_num[-2:]
size_type = tail_number in ['01','11','21','31','41','51'] and '小号' or '大号'
elif packaging == '标百' and len(serial_num) >= 3:
tail_number = serial_num[-3:]
size_type = tail_number in ['101','201','301','401','501'] and '小号' or '大号'
results.append({
'serial': serial,
'price': price,
'category': category,
'seller': seller,
'packaging': packaging,
'grade': grade,
'grading_company': grading_company,
'deal_date': deal_date or default_date, # 成交时间
'entry_date': default_date, # 录入时间
'is_graded': bool(grade),
'tail_number': tail_number, # 尾号
'size_type': size_type, # 大小号
'platform': default_platform # 平台
})
return results
@router.post("/batch-parse-local")
async def batch_parse_deals_local(request: dict = Body(...)):
"""本地正则解析批量行情文本无需AI"""
text = request.get('text', '')
default_packaging = request.get('defaultPackaging', '')
default_date = request.get('defaultDate', '')
default_platform = request.get('defaultPlatform', '')
results = parse_deals_locally(text, default_packaging, default_date, default_platform)
return {"success": True, "data": results}

View File

@ -85,9 +85,9 @@ export default function App() {
}}> }}>
{[ {[
{ path: '/', icon: '🏠', label: '首页' }, { path: '/', icon: '🏠', label: '首页' },
{ path: '/news', icon: '📰', label: '资讯' }, { path: '/news', icon: '📰', label: '行情' },
{ path: '/stats', icon: '📊', label: '统计' }, { path: '/stats', icon: '📊', label: '统计' },
{ path: '/list', icon: '📚', label: '藏品' }, { path: '/list', icon: '📚', label: '我的' },
{ path: '/add', icon: '🎯', label: '录入' }, { path: '/add', icon: '🎯', label: '录入' },
...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : []) ...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : [])
].map(tab => ( ].map(tab => (

View File

@ -81,8 +81,18 @@ export default function Add() {
platform: '淘宝', platform: '淘宝',
seller: '', seller: '',
buyer: '', buyer: '',
date: new Date().toISOString().split('T')[0] date: new Date().toISOString().split('T')[0],
isGraded: false,
gradingCompany: '',
gradingScore: ''
}) })
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)
const [savingDeal, setSavingDeal] = useState(false) const [savingDeal, setSavingDeal] = useState(false)
// //
@ -703,6 +713,21 @@ export default function Add() {
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '20px', marginBottom: '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> <div style={{ color: '#fbbf24', fontSize: '16px', fontWeight: 'bold', marginBottom: '16px' }}>成交行情录入</div>
{/* 单条/批量切换 */}
<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>
</div>
{/* 单条录入 */}
{dealMode === 'single' && (
<div>
<div style={{ marginBottom: '12px' }}> <div style={{ marginBottom: '12px' }}>
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>冠字号 *</div> <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)})} <input value={dealForm.serial} onChange={(e) => setDealForm({...dealForm, serial: e.target.value.toUpperCase(), category: autoCategory(e.target.value)})}
@ -743,8 +768,11 @@ export default function Add() {
<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> <option value="拼多多">拼多多</option>
<option value="一尘">一尘</option>
<option value="爱藏">爱藏</option>
<option value="其他">其他</option> <option value="其他">其他</option>
</select> </select>
</div> </div>
@ -770,6 +798,24 @@ export default function Add() {
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' }} /> 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: '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 () => { <button onClick={async () => {
if (!dealForm.serial || !dealForm.price || !dealForm.platform || !dealForm.date) { if (!dealForm.serial || !dealForm.price || !dealForm.platform || !dealForm.date) {
alert('请填写所有必填项') alert('请填写所有必填项')
@ -778,7 +824,6 @@ export default function Add() {
setSavingDeal(true) setSavingDeal(true)
try { try {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
//
const digits = dealForm.serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9) const digits = dealForm.serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
let tailNumber = '', sizeType = '' let tailNumber = '', sizeType = ''
if (dealForm.packaging === '标十' && digits.length >= 2) { if (dealForm.packaging === '标十' && digits.length >= 2) {
@ -793,14 +838,15 @@ export default function Add() {
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
info_type: 'deal', info_type: 'deal',
title: `J0${dealForm.serial.replace('J', '').slice(0, 8)} - ¥${dealForm.price}`, 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}`, 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_price: parseFloat(dealForm.price),
deal_date: dealForm.date, deal_date: dealForm.date,
number_category: dealForm.category,
tail_number: tailNumber,
packaging: dealForm.packaging, packaging: dealForm.packaging,
size_type: sizeType category: dealForm.category,
is_graded: !!dealForm.gradingCompany,
grading_company: dealForm.gradingCompany || '',
grading_score: dealForm.gradingScore || ''
}) })
}) })
if (response.ok) { if (response.ok) {
@ -821,6 +867,202 @@ export default function Add() {
{savingDeal ? '提交中...' : '提交行情'} {savingDeal ? '提交中...' : '提交行情'}
</button> </button>
</div> </div>
)}
{/* 批量录入 */}
{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' }}>
{['圆圆号','倒置号','金马王','金马号','金山王','天马王','金山号','天马号','朦胧王','朦胧号','如意号','钻石','永恒','无4','通货'].map(c => <option key={c} value={c}>{c}</option>)}
</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>
)}
</div>
)}
</div>
</div> </div>
)} )}

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version' import { APP_VERSION } from '../config/version'
const API_BASE = localStorage.getItem('API_BASE') || ''
export default function List() { export default function List() {
const [collections, setCollections] = useState([]) const [collections, setCollections] = useState([])
@ -17,6 +18,9 @@ export default function List() {
const [isAdmin, setIsAdmin] = useState(false) const [isAdmin, setIsAdmin] = useState(false)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [pagination, setPagination] = useState({ total: 0, pages: 1 }) const [pagination, setPagination] = useState({ total: 0, pages: 1 })
const [activeTab, setActiveTab] = useState('collections') // collections-, deals-
const [myDeals, setMyDeals] = useState([])
const [dealsLoading, setDealsLoading] = useState(false)
useEffect(() => { useEffect(() => {
// //
@ -61,6 +65,36 @@ export default function List() {
} }
}, []) }, [])
//
const fetchMyDeals = async () => {
setDealsLoading(true)
const token = localStorage.getItem('token')
const userStr = localStorage.getItem('user')
if (!token || !userStr) {
setDealsLoading(false)
return
}
try {
const user = JSON.parse(userStr)
const res = await fetch(`${API_BASE}/api/information/list?info_type=deal&user_id=${user.f99_90_id}&page_size=500`, {
headers: { 'Authorization': 'Bearer ' + token }
})
const data = await res.json()
const list = data.data || data || []
setMyDeals(Array.isArray(list) ? list : [])
} catch (e) {
console.error('获取行情失败:', e)
}
setDealsLoading(false)
}
// tab
useEffect(() => {
if (activeTab === 'deals') {
fetchMyDeals()
}
}, [activeTab])
const fetchCollections = async () => { const fetchCollections = async () => {
setLoading(true) setLoading(true)
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
@ -455,7 +489,18 @@ export default function List() {
<div style={{ minHeight: '100vh', overflowY: 'auto', background: '#0f172a', paddingBottom: '70px' }}> <div style={{ minHeight: '100vh', overflowY: 'auto', background: '#0f172a', paddingBottom: '70px' }}>
<div style={{ padding: '20px', background: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)' }}> <div style={{ padding: '20px', background: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
<div style={{ color: '#fff', fontSize: '20px', }}>我的藏品 <span style={{ fontSize: '14px', color: '#fbbf24' }}>({filteredCollections.length})</span></div> <div style={{ color: '#fff', fontSize: '20px' }}>
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={() => setActiveTab('collections')}
style={{ padding: '6px 16px', background: activeTab === 'collections' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'collections' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
我的藏品 ({filteredCollections.length})
</button>
<button onClick={() => setActiveTab('deals')}
style={{ padding: '6px 16px', background: activeTab === 'deals' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'deals' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
我的行情 ({myDeals.length})
</button>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{filter && filterType && ( {filter && filterType && (
<button <button
@ -558,8 +603,8 @@ export default function List() {
))} ))}
</div> </div>
{/* 分页组件 */} {/* 分页组件 - 仅在藏品tab下显示 */}
{pagination.pages > 1 && ( {activeTab === 'collections' && pagination.pages > 1 && (
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', justifyContent: 'flex-start', marginBottom: '12px', padding: '8px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px' }}> <div style={{ display: 'flex', gap: '6px', alignItems: 'center', justifyContent: 'flex-start', marginBottom: '12px', padding: '8px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px' }}>
<button onClick={() => { setPage(Math.max(1, page - 1)) }} disabled={page === 1} style={{ padding: '4px 10px', borderRadius: '6px', border: 'none', background: page === 1 ? 'rgba(255,255,255,0.1)' : '#3b82f6', color: '#fff', cursor: page === 1 ? 'not-allowed' : 'pointer', opacity: page === 1 ? 0.5 : 1 }}>上一页</button> <button onClick={() => { setPage(Math.max(1, page - 1)) }} disabled={page === 1} style={{ padding: '4px 10px', borderRadius: '6px', border: 'none', background: page === 1 ? 'rgba(255,255,255,0.1)' : '#3b82f6', color: '#fff', cursor: page === 1 ? 'not-allowed' : 'pointer', opacity: page === 1 ? 0.5 : 1 }}>上一页</button>
@ -576,6 +621,29 @@ export default function List() {
</div> </div>
<div style={{ padding: '16px' }}> <div style={{ padding: '16px' }}>
{/* 行情tab内容 */}
{activeTab === 'deals' && (
<div>
{dealsLoading ? (
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>加载中...</div>
) : myDeals.length === 0 ? (
<div style={{ textAlign: 'center', padding: '60px 0' }}>
<div style={{ fontSize: '50px', opacity: 0.3 }}>📊</div>
<div style={{ color: '#64748b', marginTop: '16px' }}>暂无行情记录</div>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
{myDeals.map(deal => (
<DealListItem key={deal.id} deal={deal} onRefresh={fetchMyDeals} />
))}
</div>
)}
</div>
)}
{/* 藏品tab内容 */}
{activeTab === 'collections' && (
<>
{loading ? ( {loading ? (
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>加载中...</div> <div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>加载中...</div>
) : filteredCollections.length === 0 ? ( ) : filteredCollections.length === 0 ? (
@ -597,7 +665,237 @@ export default function List() {
) : ( ) : (
filteredCollections.map(item => <ListItem key={item.id} item={item} />) filteredCollections.map(item => <ListItem key={item.id} item={item} />)
)} )}
</>
)}
</div> </div>
</div> </div>
) )
} }
//
function DealListItem({ deal, onRefresh }) {
const [expanded, setExpanded] = useState(false)
const [editing, setEditing] = useState(false)
const [editForm, setEditForm] = useState({})
const API_BASE = localStorage.getItem('API_BASE') || ''
const openEdit = () => {
// content
let platform = '', seller = '', buyer = ''
if (deal.content) {
const platformMatch = deal.content.match(/平台:\s*([^\n]+)/)
const sellerMatch = deal.content.match(/出售者:\s*([^\n]+)/)
const buyerMatch = deal.content.match(/购买者:\s*([^\n]+)/)
if (platformMatch) platform = platformMatch[1].trim()
if (sellerMatch) seller = sellerMatch[1].trim()
if (buyerMatch) buyer = buyerMatch[1].trim()
}
setEditForm({
title: deal.title,
content: deal.content,
deal_price: deal.deal_price,
deal_date: deal.deal_date ? (typeof deal.deal_date === 'string' ? deal.deal_date.split('T')[0] : '') : '',
packaging: deal.packaging || '单张',
is_graded: deal.is_graded || false,
grading_company: deal.grading_company || '',
grading_score: deal.grading_score || '',
category: deal.category || '',
deal_no: deal.deal_no || '',
platform: platform,
seller: seller,
buyer: buyer
})
setEditing(true)
}
const saveEdit = async () => {
const token = localStorage.getItem('token')
try {
await fetch(`${API_BASE}/api/information/${deal.id}`, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(editForm)
})
setEditing(false)
onRefresh()
} catch(e) { alert('保存失败') }
}
const deleteDeal = async () => {
if (!confirm('确定删除这条行情?')) return
const token = localStorage.getItem('token')
try {
await fetch(`${API_BASE}/api/information/${deal.id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
onRefresh()
} catch(e) { alert('删除失败') }
}
return (
<div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: '10px', padding: '12px', border: '1px solid rgba(255,255,255,0.1)' }}>
{/* 简要展示 - 两行显示关键信息 */}
<div onClick={() => setExpanded(!expanded)} style={{ cursor: 'pointer', padding: '10px', background: 'rgba(0,0,0,0.2)', borderRadius: '8px' }}>
{/* 第一行:成交日期(月-日)+ 冠字号 + 包装 + 评级分数 + 价格 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px', flexWrap: 'wrap', gap: '4px' }}>
{deal.deal_date && <span style={{ color: '#64748b', fontSize: '11px' }}>{deal.deal_date.slice(5)}</span>}
<span style={{ color: '#fbbf24', fontSize: '13px', fontFamily: 'monospace' }}>{deal.title?.split('-')[0] || '-'}</span>
{deal.packaging && <span style={{ background: 'rgba(139,92,246,0.15)', color: '#a78bfa', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{deal.packaging}</span>}
{deal.grading_company && <span style={{ background: 'rgba(6,182,212,0.15)', color: '#06b6d4', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{deal.grading_score}</span>}
<span style={{ color: '#22c55e', fontSize: '14px', fontWeight: 'bold' }}>¥{deal.deal_price?.toLocaleString()}</span>
</div>
{/* 第二行:编号 + 分类 + 大小号 + 展开箭头 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
{deal.deal_no && <span style={{ color: '#fff', fontSize: '10px' }}>{deal.deal_no}</span>}
{deal.category && <span style={{ background: 'rgba(59,130,246,0.15)', color: '#60a5fa', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{deal.category}</span>}
{deal.content && (() => {
const sizeMatch = deal.content.match(/大小号:\s*([^\n]+)/)
return sizeMatch && <span style={{ background: 'rgba(34,197,94,0.15)', color: '#22c55e', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{sizeMatch[1]}</span>
})()}
</div>
<span style={{ color: '#64748b', fontSize: '12px' }}>{expanded ? '▲ 收起' : '▼展开'}</span>
</div>
</div>
{/* 展开详情 */}
{expanded && (
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
{editing ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{/* 冠字号 */}
<div>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>冠字号</div>
<input value={editForm.title?.split('-')[0] || ''} onChange={e => setEditForm({...editForm, title: `${e.target.value}${editForm.deal_price}`})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fbbf24', fontSize: '14px', fontFamily: 'monospace' }} />
</div>
{/* 价格和日期 */}
<div style={{ display: 'flex', gap: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>价格</div>
<input type="number" value={editForm.deal_price} onChange={e => {
const val = parseFloat(e.target.value) || 0
const serial = editForm.title?.split('-')[0] || ''
setEditForm({...editForm, deal_price: val, title: `${serial}${val}`})
}} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#22c55e', fontSize: '14px' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>日期</div>
<input type="date" value={editForm.deal_date} onChange={e => setEditForm({...editForm, deal_date: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '14px' }} />
</div>
</div>
{/* 包装和分类 */}
<div style={{ display: 'flex', gap: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>包装</div>
<select value={editForm.packaging} onChange={e => setEditForm({...editForm, packaging: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
<option value="单张">单张</option>
<option value="标十">标十</option>
<option value="标百">标百</option>
</select>
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>分类</div>
<input value={editForm.category || ''} onChange={e => setEditForm({...editForm, category: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
</div>
</div>
{/* 评级 */}
<div style={{ display: 'flex', gap: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>评级机构</div>
<select value={editForm.grading_company || ''} onChange={e => setEditForm({...editForm, grading_company: e.target.value, is_graded: !!e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
<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: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>评级分数</div>
<input value={editForm.grading_score || ''} onChange={e => setEditForm({...editForm, grading_score: e.target.value})} placeholder="如: PC69, 67+" style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#06b6d4', fontSize: '13px' }} />
</div>
</div>
{/* 平台和出售者购买者 */}
<div>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>平台</div>
<select value={editForm.platform || ''} onChange={e => setEditForm({...editForm, platform: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
<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 style={{ display: 'flex', gap: '8px' }}>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>出售者</div>
<input value={editForm.seller || ''} onChange={e => setEditForm({...editForm, seller: e.target.value})} placeholder="请输入出售者" style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>购买者</div>
<input value={editForm.buyer || ''} onChange={e => setEditForm({...editForm, buyer: e.target.value})} placeholder="请输入购买者" style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
</div>
</div>
{/* 备注 */}
<div>
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>备注</div>
<textarea value={editForm.content || ''} onChange={e => setEditForm({...editForm, content: e.target.value})} rows={2} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
</div>
{/* 保存取消按钮 */}
<div style={{ display: 'flex', gap: '8px' }}>
<button onClick={saveEdit} style={{ flex: 1, padding: '12px', background: '#22c55e', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>保存</button>
<button onClick={() => setEditing(false)} style={{ flex: 1, padding: '12px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>取消</button>
</div>
</div>
) : (
<div>
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '8px' }}>
{deal.deal_no && <div>编号: <span style={{ color: '#fbbf24' }}>{deal.deal_no}</span></div>}
<div>冠字号: <span style={{ color: '#fbbf24' }}>{deal.title?.split('-')[0] || '-'}</span></div>
<div>价格: <span style={{ color: '#22c55e' }}>¥{deal.deal_price?.toLocaleString()}</span></div>
<div>日期: {deal.deal_date}</div>
<div>包装: {deal.packaging || '单张'}</div>
<div>分类: {deal.category || '-'}</div>
<div>评级: {deal.grading_company ? `${deal.grading_company} ${deal.grading_score || ''}` : '未评级'}</div>
{deal.content && (() => {
const platformMatch = deal.content.match(/平台:\s*([^\n]+)/)
const sellerMatch = deal.content.match(/出售者:\s*([^\n]+)/)
const buyerMatch = deal.content.match(/购买者:\s*([^\n]+)/)
const sizeMatch = deal.content.match(/大小号:\s*([^\n]+)/)
const tailMatch = deal.content.match(/尾号:\s*([^\n]+)/)
return (
<div style={{ marginTop: '4px' }}>
{platformMatch && <div>平台: {platformMatch[1]}</div>}
{sellerMatch && <div>出售者: {sellerMatch[1]}</div>}
{buyerMatch && buyerMatch[1].trim() !== '-' && <div>购买者: {buyerMatch[1]}</div>}
{tailMatch && <div>尾号: {tailMatch[1]}</div>}
{sizeMatch && <div>大小号: <span style={{ color: '#60a5fa' }}>{sizeMatch[1]}</span></div>}
</div>
)
})()}
{deal.content && <div style={{ marginTop: '8px', color: '#64748b', fontSize: '11px' }}>{deal.content}</div>}
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
<button onClick={openEdit} style={{ flex: 1, padding: '8px', background: 'rgba(59,130,246,0.2)', color: '#60a5fa', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '12px' }}>编辑</button>
<button onClick={deleteDeal} style={{ flex: 1, padding: '8px', background: 'rgba(239,68,68,0.2)', color: '#ef4444', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '12px' }}>删除</button>
</div>
</div>
)}
</div>
)}
</div>
)
}