v1.2.82 - 行情功能优化:批量录入增强、我的行情页面完善
- 批量录入增加默认日期、成交平台、默认包装类型选项 - 我的行情页面优化显示:冠字号金色、编号白色、大小号显示 - 编辑界面增加平台、出售者、购买者字段 - 导航栏优化:我的、行情 - 修复批量解析大小号计算问题 - 自动生成行情编号(日期+5位自然数)
This commit is contained in:
parent
bd22c20108
commit
74589883b6
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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 => (
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -794,13 +839,14 @@ export default function Add() {
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue