diff --git a/VERSION b/VERSION
index 8a27477..aea1186 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-VERSION=1.2.79
+1.2.82
diff --git a/backend/app/models/models.py b/backend/app/models/models.py
index f218c60..6ea0838 100644
--- a/backend/app/models/models.py
+++ b/backend/app/models/models.py
@@ -180,6 +180,16 @@ class Information(Base):
deal_price = Column(Float, 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-已过期
status = Column(String(20), default="active", index=True)
diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py
index 9d14f85..e620732 100644
--- a/backend/app/routers/information.py
+++ b/backend/app/routers/information.py
@@ -1,10 +1,11 @@
# 资讯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 import text
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime, date
+import os
from app.core.database import get_db
from app.core.auth import get_current_user
@@ -28,6 +29,12 @@ class InformationCreate(BaseModel):
expect_price_max: Optional[float] = None
deal_price: Optional[float] = 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):
@@ -42,6 +49,10 @@ class InformationUpdate(BaseModel):
expect_price_max: Optional[float] = None
deal_price: Optional[float] = 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):
@@ -66,6 +77,13 @@ class InformationResponse(BaseModel):
view_count: int
contact_count: int
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_avatar: Optional[str] = None
@@ -89,7 +107,7 @@ def get_information_list(
info_type: Optional[str] = Query(None, description="类型: seek/deal/publish"),
status: str = Query("active", description="状态: active/closed/expired"),
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),
db: Session = Depends(get_db),
response: Response = None
@@ -146,6 +164,12 @@ def get_information_list(
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_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,
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,
contact_count=item.contact_count,
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_avatar=item.user.avatar if item.user 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)
):
"""发布资讯"""
+ # 生成行情编号:日期 + 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(
user_id=current_user.f99_90_id,
info_type=data.info_type,
@@ -425,6 +468,12 @@ def create_information(
expect_price_max=data.expect_price_max,
deal_price=data.deal_price,
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"
)
db.add(info)
@@ -499,6 +548,16 @@ def update_information(
info.deal_price = data.deal_price
if data.deal_date is not None:
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.refresh(info)
@@ -522,6 +581,11 @@ def update_information(
view_count=info.view_count,
contact_count=info.contact_count,
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_avatar=current_user.avatar,
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])
def get_my_information_list(
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),
db: Session = Depends(get_db)
):
@@ -1124,3 +1188,272 @@ def get_seek_stats(
"userMatchedCount": user_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}
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index a123fa5..34e17ce 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -85,9 +85,9 @@ export default function App() {
}}>
{[
{ path: '/', icon: '🏠', label: '首页' },
- { path: '/news', icon: '📰', label: '资讯' },
+ { path: '/news', icon: '📰', label: '行情' },
{ path: '/stats', icon: '📊', label: '统计' },
- { path: '/list', icon: '📚', label: '藏品' },
+ { path: '/list', icon: '📚', label: '我的' },
{ path: '/add', icon: '🎯', label: '录入' },
...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : [])
].map(tab => (
diff --git a/frontend/src/pages/Add.jsx b/frontend/src/pages/Add.jsx
index a4bc066..9ccd98f 100644
--- a/frontend/src/pages/Add.jsx
+++ b/frontend/src/pages/Add.jsx
@@ -81,8 +81,18 @@ export default function Add() {
platform: '淘宝',
seller: '',
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)
// 号码分类函数
@@ -703,123 +713,355 @@ export default function Add() {
成交行情录入
-
-
冠字号 *
-
setDealForm({...dealForm, serial: e.target.value.toUpperCase(), category: autoCategory(e.target.value)})}
- placeholder="J0xxxxxxxx"
- style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
+ {/* 单条/批量切换 */}
+
+
+
- {dealForm.category && (
-
-
号码分类
-
{dealForm.category}
+ {/* 单条录入 */}
+ {dealMode === 'single' && (
+
+
+
冠字号 *
+
setDealForm({...dealForm, serial: e.target.value.toUpperCase(), category: autoCategory(e.target.value)})}
+ placeholder="J0xxxxxxxx"
+ style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
+
+
+ {dealForm.category && (
+
+
号码分类
+
{dealForm.category}
+
+ )}
+
+
+
包装类型
+
+ {['单张', '标十', '标百'].map(p => (
+
+ ))}
+
+
+
+
+
成交价格 *
+
setDealForm({...dealForm, price: e.target.value})}
+ placeholder="请输入成交价格"
+ style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
+
+
+
+
成交平台 *
+
+
+
+
+
+
出售者
+
setDealForm({...dealForm, seller: e.target.value})}
+ placeholder="请输入出售者"
+ style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
+
+
+
购买者
+
setDealForm({...dealForm, buyer: e.target.value})}
+ placeholder="请输入购买者"
+ style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
+
+
+
+
+
成交日期 *
+
setDealForm({...dealForm, date: e.target.value})}
+ style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
+
+
+ {/* 评级字段 */}
+
+
评级(可选)
+
+
+ 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' }} />
+
+
+
+
)}
-
-
包装类型
-
- {['单张', '标十', '标百'].map(p => (
-
- ))}
+ {/* 批量录入 */}
+ {dealMode === 'batch' && (
+
+ {/* 日期和平台 */}
+
+
+
默认日期(可选)
+
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' }} />
+
+
+
成交平台(可选)
+
+
+
+
+ {/* 包装类型选择 */}
+
+
批量默认包装类型(可选)
+
+
+
+
+
+
+
+
-
-
-
-
成交价格 *
-
setDealForm({...dealForm, price: e.target.value})}
- placeholder="请输入成交价格"
- style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
-
-
-
-
成交平台 *
-
-
-
-
-
-
出售者
-
setDealForm({...dealForm, seller: e.target.value})}
- placeholder="请输入出售者"
- style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
-
-
-
购买者
-
setDealForm({...dealForm, buyer: e.target.value})}
- placeholder="请输入购买者"
- style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
-
-
-
-
-
成交日期 *
-
setDealForm({...dealForm, date: e.target.value})}
- style={{ width: '100%', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '15px' }} />
-
-
-
+ )}
)}
diff --git a/frontend/src/pages/List.jsx b/frontend/src/pages/List.jsx
index 0078943..83dca95 100644
--- a/frontend/src/pages/List.jsx
+++ b/frontend/src/pages/List.jsx
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'
import { APP_VERSION } from '../config/version'
+const API_BASE = localStorage.getItem('API_BASE') || ''
export default function List() {
const [collections, setCollections] = useState([])
@@ -17,6 +18,9 @@ export default function List() {
const [isAdmin, setIsAdmin] = useState(false)
const [page, setPage] = useState(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(() => {
// 检查是否管理员
@@ -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 () => {
setLoading(true)
const token = localStorage.getItem('token')
@@ -455,7 +489,18 @@ export default function List() {
-
我的藏品 ({filteredCollections.length})
+
+
+
+
+
+
{filter && filterType && (
)
}
+
+// 行情列表项组件
+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 (
+
+ {/* 简要展示 - 两行显示关键信息 */}
+
setExpanded(!expanded)} style={{ cursor: 'pointer', padding: '10px', background: 'rgba(0,0,0,0.2)', borderRadius: '8px' }}>
+ {/* 第一行:成交日期(月-日)+ 冠字号 + 包装 + 评级分数 + 价格 */}
+
+ {deal.deal_date && {deal.deal_date.slice(5)}}
+ {deal.title?.split('-')[0] || '-'}
+ {deal.packaging && {deal.packaging}}
+ {deal.grading_company && {deal.grading_score}}
+ ¥{deal.deal_price?.toLocaleString()}
+
+ {/* 第二行:编号 + 分类 + 大小号 + 展开箭头 */}
+
+
+ {deal.deal_no && {deal.deal_no}}
+ {deal.category && {deal.category}}
+ {deal.content && (() => {
+ const sizeMatch = deal.content.match(/大小号:\s*([^\n]+)/)
+ return sizeMatch && {sizeMatch[1]}
+ })()}
+
+
{expanded ? '▲ 收起' : '▼展开'}
+
+
+
+ {/* 展开详情 */}
+ {expanded && (
+
+ {editing ? (
+
+ {/* 冠字号 */}
+
+
冠字号
+
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' }} />
+
+
+ {/* 价格和日期 */}
+
+
+
价格
+
{
+ 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' }} />
+
+
+
日期
+
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' }} />
+
+
+
+ {/* 包装和分类 */}
+
+
+
包装
+
+
+
+
分类
+
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' }} />
+
+
+
+ {/* 评级 */}
+
+
+
评级机构
+
+
+
+
评级分数
+
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' }} />
+
+
+
+ {/* 平台和出售者购买者 */}
+
+
平台
+
+
+
+
+
+
出售者
+
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' }} />
+
+
+
购买者
+
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' }} />
+
+
+
+ {/* 备注 */}
+
+
+ {/* 保存取消按钮 */}
+
+ 保存
+ setEditing(false)} style={{ flex: 1, padding: '12px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>取消
+
+
+ ) : (
+
+
+ {deal.deal_no &&
编号: {deal.deal_no}
}
+
冠字号: {deal.title?.split('-')[0] || '-'}
+
价格: ¥{deal.deal_price?.toLocaleString()}
+
日期: {deal.deal_date}
+
包装: {deal.packaging || '单张'}
+
分类: {deal.category || '-'}
+
评级: {deal.grading_company ? `${deal.grading_company} ${deal.grading_score || ''}` : '未评级'}
+ {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 (
+
+ {platformMatch &&
平台: {platformMatch[1]}
}
+ {sellerMatch &&
出售者: {sellerMatch[1]}
}
+ {buyerMatch && buyerMatch[1].trim() !== '-' &&
购买者: {buyerMatch[1]}
}
+ {tailMatch &&
尾号: {tailMatch[1]}
}
+ {sizeMatch &&
大小号: {sizeMatch[1]}
}
+
+ )
+ })()}
+ {deal.content &&
{deal.content}
}
+
+
+ 编辑
+ 删除
+
+
+ )}
+
+ )}
+
+ )
+}