diff --git a/VERSION b/VERSION
index 1ce7217..aea1186 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-VERSION=1.2.41
+1.2.82
diff --git a/backend/VERSION b/backend/VERSION
index 1f8d37f..8a27477 100644
--- a/backend/VERSION
+++ b/backend/VERSION
@@ -1 +1 @@
-1.2.38
+VERSION=1.2.79
diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py
index 602723a..2fe7a71 100644
--- a/backend/app/core/auth.py
+++ b/backend/app/core/auth.py
@@ -11,7 +11,9 @@ from app.core.database import SessionLocal
from app.models.models import User
# 配置
-SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
+SECRET_KEY = os.getenv("SECRET_KEY")
+if not SECRET_KEY:
+ raise ValueError("SECRET_KEY environment variable is not set. Please configure it in production!")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080")) # 7天
diff --git a/backend/app/main.py b/backend/app/main.py
index 2ec184f..73ade76 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -52,10 +52,11 @@ app = FastAPI(
# 设置全局错误处理器
setup_error_handlers(app)
-# CORS 配置
+# CORS 配置 - 生产环境限制域名
+ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://47.103.29.111,http://120.55.81.21,https://socoolbot.com").split(",")
app.add_middleware(
CORSMiddleware,
- allow_origins=["*"], # 生产环境应该限制域名
+ allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
diff --git a/backend/app/models/models.py b/backend/app/models/models.py
index 387f670..6ea0838 100644
--- a/backend/app/models/models.py
+++ b/backend/app/models/models.py
@@ -44,7 +44,8 @@ class User(Base):
f99_100_points = Column(Integer, default=0) # 积分
f01_11_balance = Column(Float, default=0) # 余额
f01_12_total_amount = Column(Float, default=0) # 累计金额
- f01_13_invite_code = Column(String(20), nullable=True) # 邀请码
+ f01_13_invite_code = Column(String(20), nullable=True) # 邀请码(自己的邀请码)
+ f99_101_invited_count = Column(Integer, default=0) # 通过自己邀请码注册的用户数量
collections = relationship("Collection", back_populates="user", cascade="all, delete-orphan")
operations = relationship("Operation", back_populates="user", cascade="all, delete-orphan")
@@ -179,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/auth.py b/backend/app/routers/auth.py
index 750a671..0c6fe73 100644
--- a/backend/app/routers/auth.py
+++ b/backend/app/routers/auth.py
@@ -13,16 +13,21 @@ router = APIRouter(prefix="/api/auth", tags=["认证"])
def generate_user_code(db):
- """生成用户编码,从0001开始"""
+ """生成用户编码,从201开始,按自然数顺序递增,跳过已存在的"""
# 查找最大的user_code
max_code = db.query(User.user_code).filter(User.user_code != None).order_by(User.user_code.desc()).first()
if max_code and max_code[0]:
try:
num = int(max_code[0]) + 1
- return f"{num:04d}"
+ if num < 201:
+ num = 201
+ # 检查是否已存在,如果存在则继续递增
+ while db.query(User).filter(User.user_code == str(num)).first():
+ num += 1
+ return str(num)
except:
pass
- return "000501"
+ return "201"
@router.post("/register", response_model=UserResponse)
def register(user_data: UserCreate, db: Session = Depends(get_db)):
@@ -53,13 +58,25 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)):
detail="E00041:该邮箱已被注册,请更换邮箱"
)
+ # 处理邀请码
+ invited_by_user = None
+ if user_data.invite_code:
+ # 查找邀请人
+ invited_by_user = db.query(User).filter(User.user_code == user_data.invite_code).first()
+ if not invited_by_user:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail="E00042:邀请码无效"
+ )
+
# 创建用户
import uuid
hashed_password = get_password_hash(user_data.password)
+ generated_code = generate_user_code(db)
user = User(
f99_90_id=str(uuid.uuid4()),
f99_91_user_id=str(uuid.uuid4()),
- user_code=generate_user_code(db),
+ user_code=generated_code,
f01_01_name=user_data.f01_01_name,
email=user_data.email,
phone=user_data.phone,
@@ -71,10 +88,35 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)):
)
db.add(user)
+ db.flush() # 确保获取user ID
+
+ # 更新邀请人、被邀请人的关联关系
+ if invited_by_user:
+ # 记录是被谁邀请的
+ user.f01_13_invite_code = invited_by_user.user_code
+ # 增加邀请人的邀请计数
+ invited_by_user.f99_101_invited_count = (invited_by_user.f99_101_invited_count or 0) + 1
+
+ # 生成自己的邀请码(用自己的user_code)
+ user.f01_13_invite_code = generated_code
+
db.commit()
db.refresh(user)
- return user
+ # 返回用户信息(避免Pydantic序列化问题)
+ return {
+ "id": user.f99_90_id,
+ "username": user.f01_01_name,
+ "user_code": user.user_code,
+ "email": user.email,
+ "phone": user.phone,
+ "avatar": user.avatar,
+ "role": user.role,
+ "level": user.f99_94_level,
+ "aiCount": user.f99_95_ai_count or 0,
+ "searchCount": user.f99_96_search_count or 0,
+ "collectionCount": user.f99_97_collection_count or 0
+ }
@router.post("/login", response_model=Token)
@@ -82,9 +124,12 @@ def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
- """用户登录"""
- # 查找用户
+ """用户登录 - 支持用户名或用户编码登录"""
+ # 先尝试用户名登录
user = db.query(User).filter(User.f01_01_name == form_data.username).first()
+ # 如果用户名不存在,尝试用户编码登录
+ if not user:
+ user = db.query(User).filter(User.user_code == form_data.username).first()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
diff --git a/backend/app/routers/collections.py b/backend/app/routers/collections.py
index bf48d73..d04bb8f 100644
--- a/backend/app/routers/collections.py
+++ b/backend/app/routers/collections.py
@@ -56,6 +56,7 @@ def to_camel_case(data: dict) -> dict:
'f05_43_repair_fee': 'repairFee',
'f05_44_grading_fee': 'gradingFee',
'f06_50_purpose': 'purpose',
+ 'images': 'images',
}
return {mapping.get(k, k): v for k, v in data.items()}
@@ -66,11 +67,11 @@ def generate_code(version: str, user_id: str, db: Session) -> str:
"""自动生成藏品编号 - 按用户独立编码"""
import re
- # 查询当前用户的非空编码(不与其他用户混算)
+ # 查询当前用户的非空编码(不与其他用户混算)- 使用行锁防止并发
user_codes = db.query(Collection.f01_02_code).filter(
Collection.f01_02_code.isnot(None),
Collection.f99_91_user_id == user_id
- ).all()
+ ).with_for_update().all()
max_num = 0
for (code,) in user_codes:
@@ -131,14 +132,17 @@ def get_collections(
# 如果指定 all_users=true,则返回所有用户藏品
from sqlalchemy.orm import joinedload
- # 管理员默认查看全库,普通用户只看自己
- if current_user.role == "admin":
- # 联表查询获取用户名
+ # 管理员默认查看全库,普通用户只看自己,未登录返回空列表
+ if current_user is None or current_user.role != "admin":
+ # 非管理员或未登录用户只能查看自己的藏品(如果没有登录则返回空)
+ if current_user is None:
+ return {"data": [], "total": 0, "page": 1, "limit": 20}
+ query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id)
+ else:
+ # 管理员查看所有藏品
query = db.query(Collection, User.f01_01_name.label('owner_name')).join(
User, Collection.f99_91_user_id == User.f99_90_id, isouter=True
)
- else:
- query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id)
# 如果指定了user_id参数,则只返回该用户的藏品
if user_id:
@@ -201,7 +205,7 @@ def get_collections(
data_list = []
for item in data:
# 处理联表查询结果
- if current_user.role == "admin":
+ if current_user is not None and current_user.role == "admin":
collection_item, owner_name = item
else:
collection_item = item
@@ -277,12 +281,12 @@ def get_stats(
):
"""获取藏品统计"""
# 获取所有藏品
- if current_user.role == "admin":
- all_collections = db.query(Collection).all()
- else:
+ if current_user is None or current_user.role != "admin":
all_collections = db.query(Collection).filter(
Collection.f99_91_user_id == current_user.f99_90_id
).all()
+ else:
+ all_collections = db.query(Collection).all()
# 总数
total_count = len(all_collections)
@@ -381,7 +385,7 @@ def get_collection(
collection = dict(result._mapping)
# 非管理员只能查看自己的藏品
- if current_user.role != "admin" and collection.get('f99_91_user_id') != current_user.f99_90_id:
+ if (current_user is None or current_user.role != "admin") and collection.get('f99_91_user_id') != current_user.f99_90_id:
raise HTTPException(status_code=403, detail="无权访问")
result_dict = {
@@ -495,6 +499,11 @@ def create_collection(
}
}
+ # 自动分类:如果未提供号码分类,则根据冠字号自动分类
+ if not collection_data.f02_14_number_category and collection_data.f02_10_prefix_serial:
+ from app.utils.number_category import get_number_category
+ collection_data.f02_14_number_category = get_number_category(collection_data.f02_10_prefix_serial)
+
collection = Collection(
f99_91_user_id=current_user.f99_90_id,
f01_01_name=collection_data.f01_01_name,
@@ -708,7 +717,7 @@ async def delete_image(
Collection.f99_90_id == image.collection_id
).first()
- if collection and current_user.role != "admin" and collection.f99_91_user_id != current_user.f99_90_id:
+ if collection and (current_user is None or current_user.role != "admin") and collection.f99_91_user_id != current_user.f99_90_id:
raise HTTPException(status_code=403, detail="E00014: 无权删除此图片")
# 删除OSS文件(如果path是OSS URL)
diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py
index a249fbb..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
+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)
):
@@ -1070,3 +1134,326 @@ def get_yichen_posts(category: str = None, search: str = None, page: int = 1, pa
offset = (page - 1) * page_size
items = query.order_by(desc(Information.created_at)).offset(offset).limit(page_size).all()
return {"data": items, "pagination": {"page": page, "limit": page_size, "total": total}}
+
+
+@router.get("/seek/stats")
+def get_seek_stats(
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取寻配号统计数据"""
+ # 寻号需求数(seek类型且expect_number不为空的总数)
+ seek_count = db.query(Information).filter(
+ Information.info_type == 'seek',
+ Information.expect_number.isnot(None),
+ Information.expect_number != ''
+ ).count()
+
+ # 我的匹配:自有藏品匹配成功的寻号帖子数量
+ # 即 is_matched = 'confirmed' 的记录,用户ID等于当前用户
+ user_matched_count = 0
+ if current_user:
+ user_matched_count = db.query(Information).filter(
+ Information.info_type == 'seek',
+ Information.expect_number.isnot(None),
+ Information.expect_number != '',
+ Information.matched_user_id == current_user.f99_90_id,
+ Information.is_matched == 'confirmed'
+ ).count()
+
+ # 总共匹配:自有匹配成功 + 网络数据匹配成功
+ # 自有匹配成功:is_matched = 'confirmed'
+ # 网络数据匹配成功:查询每个帖子的network_matched_count并求和
+ seeks = db.query(Information).filter(
+ Information.info_type == 'seek',
+ Information.expect_number.isnot(None),
+ Information.expect_number != ''
+ ).all()
+
+ total_self_matched = 0
+ total_network_matched = 0
+ for seek in seeks:
+ # 自身匹配成功
+ if seek.is_matched == 'confirmed':
+ total_self_matched += 1
+ # 网络数据匹配成功(通过coolbot数据库查询)
+ if seek.expect_number:
+ network_count = match_collections_count_from_coolbot(seek.expect_number)
+ total_network_matched += network_count
+
+ total_matched_count = total_self_matched + total_network_matched
+
+ return {
+ "seekCount": seek_count,
+ "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/backend/app/routers/users.py b/backend/app/routers/users.py
index 079be6e..c8cf2da 100644
--- a/backend/app/routers/users.py
+++ b/backend/app/routers/users.py
@@ -11,7 +11,7 @@ router = APIRouter(prefix="/api", tags=["用户"])
# ============ 当前用户接口 ============
-@router.get("/users/me", response_model=UserResponse)
+@router.get("/users/me") # 无 response_model,避免 Pydantic 序列化问题
def get_current_user_info(
current_user: User = Depends(get_current_user)
):
@@ -47,7 +47,7 @@ def get_current_user_info(
"f99_93_updated_at": current_user.f99_93_updated_at.isoformat() if current_user.f99_93_updated_at else None
}
-@router.put("/users/me", response_model=UserResponse)
+@router.put("/users/me") # 无 response_model,避免 Pydantic 序列化问题
def update_current_user(
user_update: UserUpdate,
current_user: User = Depends(get_current_user),
diff --git a/backend/app/routers/yichens.py b/backend/app/routers/yichens.py
index 9b55df7..de8e152 100644
--- a/backend/app/routers/yichens.py
+++ b/backend/app/routers/yichens.py
@@ -105,36 +105,50 @@ def get_user_stats(db: Session = Depends(get_coolbot_db)):
sellers=result[2] or 0
)
-@router.get("/posts", response_model=List[PostItem])
+@router.get("/posts")
def get_posts(
limit: int = Query(20, ge=1, le=500),
offset: int = Query(0, ge=0),
category: Optional[str] = None,
post_type: Optional[str] = None,
+ keyword: Optional[str] = None,
db: Session = Depends(get_coolbot_db)
):
- """获取帖子列表"""
- query = """
- SELECT post_id, title, content, category, post_type, price,
- author_username, post_time, reply_count, view_count, url
- FROM yichens_posts
- WHERE 1=1
- """
+ """获取帖子列表 - 支持全局搜索,返回总数和分页信息"""
+ # 构建WHERE条件
+ where_clauses = ["1=1"]
params = {"limit": limit, "offset": offset}
if category:
- query += " AND category = :category"
+ where_clauses.append("category = :category")
params["category"] = category
if post_type:
- query += " AND post_type = :post_type"
+ where_clauses.append("post_type = :post_type")
params["post_type"] = post_type
- query += " ORDER BY post_time DESC LIMIT :limit OFFSET :offset"
+ # 全局搜索
+ if keyword:
+ where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)")
+ params["keyword"] = f"%{keyword}%"
- results = db.execute(text(query), params).fetchall()
+ where_sql = " AND ".join(where_clauses)
- return [PostItem(
+ # 查询总数
+ count_query = f"SELECT COUNT(*) FROM yichens_posts WHERE {where_sql}"
+ total_count = db.execute(text(count_query), {k: v for k, v in params.items() if k not in ['limit', 'offset']}).scalar() or 0
+
+ # 查询数据
+ data_query = f"""
+ SELECT post_id, title, content, category, post_type, price,
+ author_username, post_time, reply_count, view_count, url
+ FROM yichens_posts
+ WHERE {where_sql}
+ ORDER BY post_time DESC LIMIT :limit OFFSET :offset
+ """
+ results = db.execute(text(data_query), params).fetchall()
+
+ posts = [PostItem(
post_id=r[0],
title=r[1] or "",
content=r[2] or "",
@@ -147,6 +161,13 @@ def get_posts(
view_count=r[9] or 0,
url=r[10]
) for r in results]
+
+ return {
+ "posts": posts,
+ "total": total_count,
+ "page": offset // limit + 1,
+ "page_size": limit
+ }
@router.get("/users", response_model=List[UserItem])
def get_users(
@@ -250,3 +271,107 @@ async def get_today_category_stats(db: Session = Depends(get_coolbot_db)):
"""
results = db.execute(text(query)).fetchall()
return [{"category": r[0] or "未分类", "count": r[1]} for r in results]
+
+
+@router.get("/stats/dragons-today")
+def get_dragons_stats_today(
+ db: Session = Depends(get_coolbot_db)
+):
+ """获取今日龙钞详细统计数据(按号码分类)- 就高不就低"""
+ from sqlalchemy import text
+
+ # 1. 带4:包含"带4"、"带四"、"通货"
+ dai4 = db.execute(text("""
+ SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
+ SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
+ FROM yichens_posts
+ WHERE post_time >= CURRENT_DATE
+ AND category LIKE '%龙%'
+ AND (
+ content LIKE '%带4%' OR title LIKE '%带4%'
+ OR content LIKE '%带四%' OR title LIKE '%带四%'
+ OR content LIKE '%通货%' OR title LIKE '%通货%'
+ )
+ """)).fetchone()
+
+ # 2. 无4:包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒"
+ wu4 = db.execute(text("""
+ SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
+ SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
+ FROM yichens_posts
+ WHERE post_time >= CURRENT_DATE
+ AND category LIKE '%龙%'
+ AND (
+ content LIKE '%无4%' OR title LIKE '%无4%'
+ OR content LIKE '%无四%' OR title LIKE '%无四%'
+ )
+ AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%'
+ AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%'
+ AND content NOT LIKE '%永恒%' AND title NOT LIKE '%永恒%'
+ AND content NOT LIKE '%无四七%' AND title NOT LIKE '%无四七%'
+ """)).fetchone()
+
+ # 3. 无47:包含"无47"、"永恒"、"无四七",排除"无247"
+ wu47 = db.execute(text("""
+ SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
+ SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
+ FROM yichens_posts
+ WHERE post_time >= CURRENT_DATE
+ AND category LIKE '%龙%'
+ AND (
+ content LIKE '%无47%' OR title LIKE '%无47%'
+ OR content LIKE '%永恒%' OR title LIKE '%永恒%'
+ OR content LIKE '%无四七%' OR title LIKE '%无四七%'
+ )
+ AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%'
+ """)).fetchone()
+
+ # 4. 无247:包含"无247"、"天马"、"金山",排除"无347"
+ wu247 = db.execute(text("""
+ SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
+ SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
+ FROM yichens_posts
+ WHERE post_time >= CURRENT_DATE
+ AND category LIKE '%龙%'
+ AND (
+ content LIKE '%无247%' OR title LIKE '%无247%'
+ OR content LIKE '%天马%' OR title LIKE '%天马%'
+ OR content LIKE '%金山%' OR title LIKE '%金山%'
+ )
+ AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%'
+ """)).fetchone()
+
+ # 5. 无347:包含"无347"、"钻石"、"金马"、"魅力"、"朦胧"
+ wu347 = db.execute(text("""
+ SELECT
+ COUNT(*) as total,
+ SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
+ SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
+ FROM yichens_posts
+ WHERE post_time >= CURRENT_DATE
+ AND category LIKE '%龙%'
+ AND (
+ content LIKE '%无347%' OR title LIKE '%无347%'
+ OR content LIKE '%钻石%' OR title LIKE '%钻石%'
+ OR content LIKE '%金马%' OR title LIKE '%金马%'
+ OR content LIKE '%魅力%' OR title LIKE '%魅力%'
+ OR content LIKE '%朦胧%' OR title LIKE '%朦胧%'
+ )
+ """)).fetchone()
+
+ return {
+ "dai4": {"total": dai4[0] or 0, "deals": dai4[1] or 0, "wants": dai4[2] or 0},
+ "wu4": {"total": wu4[0] or 0, "deals": wu4[1] or 0, "wants": wu4[2] or 0},
+ "wu47": {"total": wu47[0] or 0, "deals": wu47[1] or 0, "wants": wu47[2] or 0},
+ "wu247": {"total": wu247[0] or 0, "deals": wu247[1] or 0, "wants": wu247[2] or 0},
+ "wu347": {"total": wu347[0] or 0, "deals": wu347[1] or 0, "wants": wu347[2] or 0}
+ }
+
diff --git a/backend/app/schemas/schemas.py b/backend/app/schemas/schemas.py
index fe79fe6..a8497b5 100644
--- a/backend/app/schemas/schemas.py
+++ b/backend/app/schemas/schemas.py
@@ -19,6 +19,7 @@ class UserBase(BaseModel):
class UserCreate(UserBase):
password: str = Field(..., min_length=6)
+ invite_code: Optional[str] = Field(None, alias="inviteCode") # 填写的邀请码(选填)
class UserUpdate(BaseModel):
@@ -50,6 +51,7 @@ class UserResponse(UserBase):
f01_11_balance: Optional[float] = Field(0, alias="balance")
f01_12_total_amount: Optional[float] = Field(0, alias="totalAmount")
f01_13_invite_code: Optional[str] = Field(None, alias="inviteCode")
+ f99_101_invited_count: Optional[int] = Field(0, alias="invitedCount")
f99_90_id: str = Field(..., alias="id")
f01_01_name: str = Field(..., alias="username")
role: str
diff --git a/backend/app/utils/number_category.py b/backend/app/utils/number_category.py
new file mode 100644
index 0000000..340e9f2
--- /dev/null
+++ b/backend/app/utils/number_category.py
@@ -0,0 +1,132 @@
+# 号码分类工具
+# 按MEMORY.md最新规则 (2026-04-02)
+# 优先级:数字越小越有价值
+
+CATEGORIES = [
+ # 1. 圆圆号:无.123457,只能用0689
+ {"name": "圆圆号", "cannot_use": ".123457", "must_have": ""},
+ # 2. 倒置号:无23457,可用01689必须有1
+ {"name": "倒置号", "cannot_use": "23457", "must_have": "1"},
+ # 3. 金马王:无12347,可用05689必须有5
+ {"name": "金马王", "cannot_use": "12347", "must_have": "5"},
+ # 4. 金马号:无2347,可用015689必须有1和5
+ {"name": "金马号", "cannot_use": "2347", "must_have": "15"},
+ # 5. 金山王:无12457,可用03689必须有3
+ {"name": "金山王", "cannot_use": "12457", "must_have": "3"},
+ # 6. 天马王:无1247,可用035689必须有3和5
+ {"name": "天马王", "cannot_use": "1247", "must_have": "35"},
+ # 7. 金山号:无2457,可用013689必须有1和3
+ {"name": "金山号", "cannot_use": "2457", "must_have": "13"},
+ # 8. 天马号:无247,可用0135689必须有1、3和5
+ {"name": "天马号", "cannot_use": "247", "must_have": "135"},
+ # 9. 朦胧王:无13457
+ {"name": "朦胧王", "cannot_use": "13457", "must_have": ""},
+ # 10. 朦胧号:无3457
+ {"name": "朦胧号", "cannot_use": "3457", "must_have": ""},
+ # 11. 如意号:无1347
+ {"name": "如意号", "cannot_use": "1347", "must_have": ""},
+ # 12. 钻石号:无347
+ {"name": "钻石号", "cannot_use": "347", "must_have": ""},
+ # 13. 永恒号:无47
+ {"name": "永恒号", "cannot_use": "47", "must_have": ""},
+ # 14. 无4号:不包含4
+ {"name": "无4号", "cannot_use": "4", "must_have": ""},
+ # 15. 通货:含4
+ {"name": "通货", "cannot_use": "", "must_have": "4"},
+]
+
+
+def extract_digits(serial: str) -> dict:
+ """提取冠字号中的数字部分"""
+ if not serial:
+ return {"digits": "", "type": "single"}
+
+ nums = serial.replace("J", "").replace(",", "").replace(".", "").strip()
+ nums = "".join(c for c in nums if c.isdigit())
+
+ if nums.endswith("01"):
+ return {"digits": nums[:-2], "type": "hundred"}
+ elif nums.endswith("1"):
+ return {"digits": nums[:-1], "type": "ten"}
+ else:
+ return {"digits": nums, "type": "single"}
+
+
+def matches_category(digits: str, category: dict) -> bool:
+ cannot_use = category.get("cannot_use", "")
+ must_have = category.get("must_have", "")
+
+ # 检查不能用的数字
+ for n in cannot_use:
+ if n in digits:
+ return False
+
+ # 检查必须有的数字
+ if must_have:
+ for n in must_have:
+ if n not in digits:
+ return False
+
+ return True
+
+
+def get_number_category(serial: str) -> str:
+ """号码分类函数"""
+ if not serial:
+ return ""
+
+ # 提取数字
+ info = extract_digits(serial)
+ digits = info["digits"]
+
+ if not digits:
+ return ""
+
+ # 根据类型取对应位数
+ digits_type = info["type"]
+ if digits_type == "hundred":
+ # 标百看后6位
+ check_digits = digits[-6:] if len(digits) >= 6 else digits
+ elif digits_type == "ten":
+ # 标十看后7位
+ check_digits = digits[-7:] if len(digits) >= 7 else digits
+ else:
+ # 散钞看全部
+ check_digits = digits
+
+ # 按优先级匹配分类
+ for category in CATEGORIES:
+ if matches_category(check_digits, category):
+ return category["name"]
+
+ return "通货"
+
+
+def get_number_category_color(category: str) -> str:
+ """获取分类颜色"""
+ colors = {
+ "圆圆号": "#ef4444", # 红
+ "倒置号": "#f97316", # 橙
+ "金马王": "#eab308", # 黄
+ "金马号": "#84cc16", # 绿
+ "金山王": "#22c55e", # 深绿
+ "天马王": "#14b8a6", # 青
+ "金山号": "#06b6d4", # 蓝
+ "天马号": "#0ea5e9", # 浅蓝
+ "朦胧王": "#6366f1", # 靛蓝
+ "朦胧号": "#8b5cf6", # 紫
+ "如意号": "#a855f7", # 深紫
+ "钻石号": "#d946ef", # 品红
+ "永恒号": "#ec4899", # 粉红
+ "无4号": "#64748b", # 灰
+ "通货": "#9ca3af", # 浅灰
+ }
+ return colors.get(category, "#9ca3af")
+
+
+def get_category_priority(category: str) -> int:
+ """获取分类优先级(数字越小越高级)"""
+ for i, cat in enumerate(CATEGORIES, 1):
+ if cat["name"] == category:
+ return i
+ return 999
\ No newline at end of file
diff --git a/config/VERSION b/config/VERSION
index 532db09..f73f4c0 100644
--- a/config/VERSION
+++ b/config/VERSION
@@ -1 +1 @@
-VERSION=1.2.50
+VERSION=1.2.81
diff --git a/frontend/index.html b/frontend/index.html
index 6525875..e3a4d95 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
-
甲辰收藏 v1.2.50
+ 甲辰收藏 v=1.2.81
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 156d83c..79e65c8 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "jiachenlong-frontend",
- "version": "1.2.12",
+ "version": "1.2.81",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jiachenlong-frontend",
- "version": "1.2.12",
+ "version": "1.2.81",
"dependencies": {
"axios": "^1.7.9",
"react": "^18.3.1",
@@ -15,7 +15,7 @@
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
- "vite": "^6.0.7"
+ "vite": "^6.4.2"
}
},
"node_modules/@babel/code-frame": {
@@ -2047,9 +2047,9 @@
}
},
"node_modules/vite": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
- "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "version": "6.4.2",
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/frontend/package.json b/frontend/package.json
index ad0ec71..c37ff78 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "jiachenlong-frontend",
- "version": "1.2.12",
+ "version": "1.2.81",
"private": true,
"description": "甲辰藏品管理系统 - 移动端前端",
"scripts": {
@@ -9,13 +9,13 @@
"preview": "vite preview"
},
"dependencies": {
+ "axios": "^1.7.9",
"react": "^18.3.1",
"react-dom": "^18.3.1",
- "react-router-dom": "^7.1.0",
- "axios": "^1.7.9"
+ "react-router-dom": "^7.1.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
- "vite": "^6.0.7"
+ "vite": "^6.4.2"
}
}
diff --git a/frontend/package.txt b/frontend/package.txt
new file mode 100644
index 0000000..1d01bc1
--- /dev/null
+++ b/frontend/package.txt
@@ -0,0 +1 @@
+v=1.2.80
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index ae35683..34e17ce 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -5,7 +5,6 @@ import List from './pages/List'
import Add from './pages/Add'
import Stats from './pages/Stats'
import News from './pages/News'
-import Info from './pages/Info'
import Login from './pages/Login'
import Detail from './pages/Detail'
import Edit from './pages/Edit'
@@ -31,7 +30,6 @@ export default function App() {
const basePath = path.split('?')[0]
if (basePath === '/') return
if (basePath === '/news') return
- if (basePath === '/info') return
if (basePath === '/stats') return
if (basePath === '/list') return
if (basePath === '/add') return
@@ -53,7 +51,6 @@ export default function App() {
}
const isAdmin = user && user.role === 'admin'
const isEditor = user && user.role === 'editor'
- const canAccessInfo = isAdmin || isEditor
// 登录页面独立渲染,不显示底部导航
if (!token) {
@@ -88,11 +85,10 @@ export default function App() {
}}>
{[
{ path: '/', icon: '🏠', label: '首页' },
- { path: '/news', icon: '📰', label: '资讯' },
+ { path: '/news', icon: '📰', label: '行情' },
{ path: '/stats', icon: '📊', label: '统计' },
- { path: '/list', icon: '📚', label: '藏品' },
- { path: '/add', icon: '🎯', label: '添加' },
- ...(canAccessInfo ? [{ path: '/info', icon: '📝', label: '信息' }] : []),
+ { path: '/list', icon: '📚', label: '我的' },
+ { path: '/add', icon: '🎯', label: '录入' },
...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : [])
].map(tab => (
)
}
+// Fri Apr 10 03:44:24 PM CST 2026
diff --git a/frontend/src/config/version.js b/frontend/src/config/version.js
index cf4abcc..e808cde 100644
--- a/frontend/src/config/version.js
+++ b/frontend/src/config/version.js
@@ -3,7 +3,7 @@
// 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx
// 从环境变量读取(vite.config.js 注入)
-export const APP_VERSION = import.meta.env.APP_VERSION || '1.0.0'
+export const APP_VERSION = import.meta.env.APP_VERSION || '1.2.82'
// 版本信息
export const VERSION_INFO = {
diff --git a/frontend/src/pages/Add.jsx b/frontend/src/pages/Add.jsx
index f09e0cb..9ccd98f 100644
--- a/frontend/src/pages/Add.jsx
+++ b/frontend/src/pages/Add.jsx
@@ -1,6 +1,7 @@
-// 添加藏品页面 - 支持 AI 识别/手工录入/批量录入
+// 添加藏品页面 - 支持 AI 识别/手工录入
import React, { useState, useRef } from 'react'
import { APP_VERSION } from '../config/version'
+const API_BASE = localStorage.getItem('API_BASE') || ''
const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'无4',label:'无4'},{value:'带4',label:'带4'},{value:'其他',label:'其他'}];
// 字段转换函数
@@ -71,12 +72,57 @@ const getDefaultForm = () => ({
})
export default function Add() {
+ // 行情录入表单
+ const [dealForm, setDealForm] = useState({
+ serial: '',
+ category: '',
+ packaging: '标十',
+ price: '',
+ platform: '淘宝',
+ seller: '',
+ buyer: '',
+ 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 autoCategory = (serial) => {
+ const digits = serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
+ if (!digits) return ''
+ const d = digits
+ if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '圆圆号'
+ if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '倒置号'
+ if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马王'
+ if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马号'
+ if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山王'
+ if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马王'
+ if (!d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山号'
+ if (!d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马号'
+ if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧王'
+ if (!d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧号'
+ if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '如意号'
+ if (!d.includes('3') && !d.includes('4') && !d.includes('7')) return '钻石'
+ if (!d.includes('4') && !d.includes('7')) return '永恒'
+ if (!d.includes('4')) return '无4'
+ return '通货'
+ }
+
// 根据 URL 参数确定默认标签
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
const modeParam = params.get('mode')
const defaultTab = modeParam === 'manual' ? 'manual' : 'ai'
- const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, batch
+ const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, deal
const [form, setForm] = useState(getDefaultForm())
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
@@ -405,15 +451,15 @@ export default function Add() {
-
@@ -435,7 +481,7 @@ export default function Add() {
style={{ padding: '12px 24px', background: 'rgba(239, 68, 68, 0.2)', color: '#ef4444', border: '1px solid #ef4444', borderRadius: '8px', fontSize: '14px', cursor: 'pointer' }}>🗑️ 重新选择
- {recognizing ? '🔍 识别中...' : '🤖 开始识别'}
+ {recognizing ? '🔍 识别中...' : '开始识别'}
@@ -657,16 +703,369 @@ export default function Add() {
)}
- {/* 批量录入模式 */}
- {activeTab === 'batch' && (
-
-
🚧
-
批量录入开发中
-
敬请期待后续版本
-
- )}
+
{/* 版本号 */}
+
+ {/* 行情录入模式 */}
+ {activeTab === 'deal' && (
+
+
+
成交行情录入
+
+ {/* 单条/批量切换 */}
+
+ 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' }}>
+ 单条录入
+
+ 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' }}>
+ 批量录入
+
+
+
+ {/* 单条录入 */}
+ {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, packaging: p})}
+ style={{ flex: 1, padding: '10px', background: dealForm.packaging === p ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: dealForm.packaging === p ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
+ {p}
+
+ ))}
+
+
+
+
+
成交价格 *
+
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' }} />
+
+
+
+
{
+ if (!dealForm.serial || !dealForm.price || !dealForm.platform || !dealForm.date) {
+ alert('请填写所有必填项')
+ return
+ }
+ setSavingDeal(true)
+ try {
+ const token = localStorage.getItem('token')
+ const digits = dealForm.serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
+ let tailNumber = '', sizeType = ''
+ if (dealForm.packaging === '标十' && digits.length >= 2) {
+ tailNumber = digits.slice(-2)
+ sizeType = ['01','11','21','31','41','51'].includes(tailNumber) ? '小号' : '大号'
+ } else if (dealForm.packaging === '标百' && digits.length >= 3) {
+ tailNumber = digits.slice(-3)
+ sizeType = ['101','201','301','401','501'].includes(tailNumber) ? '小号' : '大号'
+ }
+ const response = await fetch(`${API_BASE}/api/information/`, {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ info_type: 'deal',
+ title: `J0${dealForm.serial.replace('J', '').slice(0, 8)}-¥${dealForm.price}`,
+ content: `分类: ${dealForm.category || '未分类'}\n尾号: ${tailNumber || '-'}\n大小号: ${sizeType || '-'}\n包装: ${dealForm.packaging}\n平台: ${dealForm.platform}\n价格: ¥${dealForm.price}\n出售者: ${dealForm.seller || '-'}\n购买者: ${dealForm.buyer || '-'}\n日期: ${dealForm.date}\n评级: ${dealForm.gradingCompany ? dealForm.gradingCompany + ' ' + dealForm.gradingScore : '未评级'}`,
+ deal_price: parseFloat(dealForm.price),
+ deal_date: dealForm.date,
+ packaging: dealForm.packaging,
+ category: dealForm.category,
+ is_graded: !!dealForm.gradingCompany,
+ grading_company: dealForm.gradingCompany || '',
+ grading_score: dealForm.gradingScore || ''
+ })
+ })
+ if (response.ok) {
+ alert('行情录入成功!')
+ setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '淘宝', seller: '', buyer: '', date: new Date().toISOString().split('T')[0] })
+ } else {
+ const data = await response.json()
+ alert('录入失败: ' + (data.detail || '未知错误'))
+ }
+ } catch (e) {
+ alert('提交失败: ' + e.message)
+ } finally {
+ setSavingDeal(false)
+ }
+ }}
+ disabled={savingDeal}
+ style={{ width: '100%', padding: '14px', background: savingDeal ? '#64748b' : '#fbbf24', color: savingDeal ? '#94a3b8' : '#1e293b', border: 'none', borderRadius: '10px', fontSize: '16px', fontWeight: '600', cursor: savingDeal ? 'not-allowed' : 'pointer' }}>
+ {savingDeal ? '提交中...' : '提交行情'}
+
+
+ )}
+
+ {/* 批量录入 */}
+ {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' }} />
+
+
+
成交平台(可选)
+
+
+
+
+ {/* 包装类型选择 */}
+
+
批量默认包装类型(可选)
+
+ 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' }}>
+ 单张
+
+ 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' }}>
+ 标十
+
+ 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' }}>
+ 标百
+
+
+
+
+
+ )}
+
+
+ )}
+
v{APP_VERSION}
)
diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx
index 56ed116..84695fd 100644
--- a/frontend/src/pages/Admin.jsx
+++ b/frontend/src/pages/Admin.jsx
@@ -126,6 +126,8 @@ export default function Admin() {
const updateData = {
username: editingUser.username,
email: editingUser.email,
+ phone: editingUser.phone,
+ phoneVerified: editingUser.phoneVerified,
role: editingUser.role,
user_code: editingUser.user_code,
level: editingUser.level,
@@ -253,7 +255,7 @@ export default function Admin() {
setEditingUser({ ...user })}
+ onClick={() => setEditingUser({ ...user, email: user.email || '', phone: user.phone || '', phoneVerified: user.phoneVerified !== false, newPassword: '', confirmPassword: '' })}
style={{
padding: '6px 12px',
borderRadius: '6px',
@@ -391,6 +393,28 @@ export default function Admin() {
/>
+
+
+ setEditingUser({ ...editingUser, phone: e.target.value })}
+ style={{ width: '100%', padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff' }}
+ />
+
+
+
+
+
+
-
-
-
-
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx
index 1a044ca..1476144 100644
--- a/frontend/src/pages/Home.jsx
+++ b/frontend/src/pages/Home.jsx
@@ -6,7 +6,9 @@ export default function Home() {
const [stats, setStats] = useState({ totalCount: 0, totalCost: 0, totalRevenue: 0, expectedProfit: 0, totalProfit: 0, gradedCount: 0 })
const [recentCollections, setRecentCollections] = useState([])
const [yichensStats, setYichensStats] = useState({ total: 0, deals: 0, wants: 0, dragons: 0, horses: 0, tianma: 0 })
+ const [seekStats, setSeekStats] = useState({ seekCount: 0, matchedCount: 0 })
const [recentPosts, setRecentPosts] = useState([])
+ const [dragonStats, setDragonStats] = useState({})
const currentPath = window.location.hash.slice(1) || '/'
useEffect(() => {
@@ -58,13 +60,26 @@ export default function Home() {
// 获取一尘看板数据
useEffect(() => {
+ fetch('/api/yichens/stats/dragons-today').then(res => res.json()).then(data => {
+ setDragonStats(data || {})
+ }).catch(() => {})
+
fetch('/api/yichens/stats/today').then(res => res.json()).then(data => {
setYichensStats(data || {})
}).catch(() => {})
// 获取最新一尘帖子
- fetch('/api/yichens/posts?limit=10&offset=0').then(res => res.json()).then(data => {
- setRecentPosts(Array.isArray(data) ? data : [])
+ fetch('/api/yichens/stats/dragons-today').then(res => res.json()).then(data => {
+ setDragonStats(data || {})
+ }).catch(() => {})
+
+ fetch('/api/yichens/posts?limit=20&offset=0').then(res => res.json()).then(data => {
+ setRecentPosts(data.posts || data || [])
+ }).catch(() => {})
+
+ // 获取寻配号统计数据
+ fetch('/api/information/seek/stats').then(res => res.json()).then(data => {
+ setSeekStats(data || {})
}).catch(() => {})
}, [])
@@ -81,16 +96,16 @@ export default function Home() {
const formatMoney = (val) => {
if (!val || val === 0) return '0'
const v = val / 10000
- return v.toFixed(4)
+ return v.toFixed(1)
}
const statCards = [
- { label: '藏品数', value: stats.totalCount, color: '#3b82f6', icon: '📦' },
- { label: '总成本', value: formatMoney(stats.totalCost) + '万', color: '#22c55e', icon: '💰' },
- { label: '总收入', value: formatMoney(stats.totalRevenue) + '万', color: '#06b6d4', icon: '📈' },
- { label: '评级数', value: stats.gradedCount, color: '#8b5cf6', icon: '⭐' },
- { label: '预期利润', value: formatMoney(stats.expectedProfit) + '万', color: stats.expectedProfit >= 0 ? '#f59e0b' : '#ef4444', icon: '🎯' },
- { label: '总利润', value: formatMoney(stats.totalProfit) + '万', color: stats.totalProfit >= 0 ? '#10b981' : '#f43f5e', icon: '💵' }
+ { label: '藏品数', value: stats.totalCount, color: '#3b82f6' },
+ { label: '总成本', value: formatMoney(stats.totalCost) + '万', color: '#22c55e' },
+ { label: '总收入', value: formatMoney(stats.totalRevenue) + '万', color: '#06b6d4' },
+ { label: '评级数', value: stats.gradedCount, color: '#8b5cf6' },
+ { label: '预期利润', value: formatMoney(stats.expectedProfit) + '万', color: stats.expectedProfit >= 0 ? '#f59e0b' : '#ef4444' },
+ { label: '总利润', value: formatMoney(stats.totalProfit) + '万', color: stats.totalProfit >= 0 ? '#10b981' : '#f43f5e' }
]
return (
@@ -112,7 +127,7 @@ export default function Home() {
-
{user?.username || '用户'}
+
{user?.username || '用户'} ID:{user?.user_code || '-'}
欢迎回来 👋
@@ -145,7 +160,7 @@ export default function Home() {
onMouseOut={e => { e.currentTarget.style.transform = 'translateY(0)' }}
onClick={() => window.location.hash = '#/stats'}
>
-
{card.icon}
+
{card.value}
{card.label}
@@ -189,9 +204,37 @@ export default function Home() {
+ {/* 寻配号数据 */}
+
+
🔍 寻配号数据
+
window.location.hash = '#/news?type=seek'} style={{
+ background: 'linear-gradient(135deg, rgba(245,158,11,0.15) 0%, rgba(245,158,11,0.05) 100%)',
+ borderRadius: '12px',
+ padding: '16px',
+ border: '1px solid rgba(245,158,11,0.2)',
+ cursor: 'pointer'
+ }}>
+
+
+
{seekStats.seekCount || 0}
+
寻号需求
+
+
+
{seekStats.userMatchedCount || 0}
+
我的匹配
+
+
+
{seekStats.totalMatchedCount || 0}
+
总共匹配
+
+
+
+
+
+
{/* 一尘今日数据 */}
-
📊 一尘今日数据
+
📊 一尘今日连体纪念钞数据
{yichensStats.total || 0}
@@ -220,6 +263,51 @@ export default function Home() {
+ {/* 今日龙钞帖子数据统计 */}
+
+
🐉 今日龙钞帖子数据统计
+
+ {/* 表头 */}
+
+ {/* 数据行 */}
+
+
带4
+
{dragonStats.dai4?.deals || 0}
+
{dragonStats.dai4?.wants || 0}
+
{(dragonStats.dai4?.deals || 0) + (dragonStats.dai4?.wants || 0)}
+
+
+
无4
+
{dragonStats.wu4?.deals || 0}
+
{dragonStats.wu4?.wants || 0}
+
{(dragonStats.wu4?.deals || 0) + (dragonStats.wu4?.wants || 0)}
+
+
+
无47
+
{dragonStats.wu47?.deals || 0}
+
{dragonStats.wu47?.wants || 0}
+
{(dragonStats.wu47?.deals || 0) + (dragonStats.wu47?.wants || 0)}
+
+
+
无247
+
{dragonStats.wu247?.deals || 0}
+
{dragonStats.wu247?.wants || 0}
+
{(dragonStats.wu247?.deals || 0) + (dragonStats.wu247?.wants || 0)}
+
+
+
无347
+
{dragonStats.wu347?.deals || 0}
+
{dragonStats.wu347?.wants || 0}
+
{(dragonStats.wu347?.deals || 0) + (dragonStats.wu347?.wants || 0)}
+
+
+
+
{/* 最新一尘发帖 */}
@@ -236,7 +324,7 @@ export default function Home() {
暂无帖子
) : (
recentPosts.map((item, idx) => (
-
window.location.hash = '#/news'} style={{
+
window.open(item.url, '_blank')} style={{
padding: '12px 16px',
borderBottom: idx < recentPosts.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none',
cursor: 'pointer',
@@ -279,13 +367,13 @@ export default function Home() {
{(isAdmin ? [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
- { icon: '➕', label: '添加', hash: '#/ocr', idx: 2 },
+ { icon: '➕', label: '录入', hash: '#/ocr', idx: 2 },
{ icon: '📊', label: '统计', hash: '#/stats', idx: 3 },
{ icon: '⚙️', label: '管理', hash: '#/admin', idx: 4 }
] : [
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
- { icon: '➕', label: '添加', hash: '#/ocr', idx: 2 },
+ { icon: '➕', label: '录入', hash: '#/ocr', idx: 2 },
{ icon: '📊', label: '统计', hash: '#/stats', idx: 3 }
]).map((item) => (
window.location.hash = item.hash} style={{
diff --git a/frontend/src/pages/Info.jsx b/frontend/src/pages/Info.jsx
deleted file mode 100644
index 17c7baf..0000000
--- a/frontend/src/pages/Info.jsx
+++ /dev/null
@@ -1,680 +0,0 @@
-import React, { useState, useEffect } from 'react'
-
-// 信息页面 - 寻配号发布和发布管理(包含寻号/行情区分)
-export default function Info() {
- // 检查登录状态,未登录则跳转到登录页
- if (!localStorage.getItem('token')) {
- window.location.hash = '#/login'
- return null
- }
-
- // 顶部tab:寻配号发布 / 发布管理
- const [activeTab, setActiveTab] = useState('manage')
- const [showPublish, setShowPublish] = useState(false)
- const [publishType, setPublishType] = useState('deal') // 'seek' 或 'deal'
- const [myList, setMyList] = useState([])
- const [loading, setLoading] = useState(false)
- const [editingItem, setEditingItem] = useState(null)
- const [filterType, setFilterType] = useState('all') // all/seek/deal
- const [expandedItems, setExpandedItems] = useState({}) // 展开状态
-
- const [formData, setFormData] = useState({
- edition: '龙钞', type: '标百', isGraded: true, category: '成交', source: '直播', title: ''
- })
-
- // 寻配号发布表单
- const [seekForm, setSeekForm] = useState({
- edition: '龙钞', price: '', features: '', contact: '', content: '', title: ''
- })
-
- const API_BASE = localStorage.getItem('API_BASE') || ''
-
- // 切换展开/收起
- const toggleExpand = (itemId) => {
- setExpandedItems(prev => ({
- ...prev,
- [itemId]: !prev[itemId]
- }))
- }
-
- useEffect(() => {
- if (activeTab === 'manage') fetchMyList()
- }, [activeTab])
-
- useEffect(() => {
- // 自动生成行情标题
- const now = new Date()
- const date = `${now.getFullYear()}/${now.getMonth()+1}/${now.getDate()}`
- const grade = formData.isGraded ? '(评级币)' : '(裸钞)'
- const title = `「${date} ${formData.edition} ${formData.type} ${formData.category} 行情信息${grade}」`
- setFormData(prev => ({ ...prev, title }))
- }, [formData.edition, formData.type, formData.isGraded, formData.category])
-
- useEffect(() => {
- // 自动生成寻配号标题
- if (seekForm.edition || seekForm.features) {
- const title = `「寻号 ${seekForm.edition} J0${seekForm.features || 'XXXXXXXX'}」`
- setSeekForm(prev => ({ ...prev, title }))
- }
- }, [seekForm.edition, seekForm.features])
-
- const fetchMyList = async () => {
- setLoading(true)
- try {
- const token = localStorage.getItem('token')
- const res = await fetch(`${API_BASE}/api/information/list`, {
- headers: { Authorization: `Bearer ${token}` }
- })
- if (res.ok) {
- const data = await res.json()
- setMyList(data || [])
- }
- } catch (e) {
- console.error(e)
- }
- setLoading(false)
- }
-
- // 发布寻配号
- const handlePublishSeek = async () => {
- if (!seekForm.contact) {
- alert('请填写联系方式')
- return
- }
- try {
- const token = localStorage.getItem('token')
- const res = await fetch(`${API_BASE}/api/information/`, {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- title: seekForm.title,
- content: document.getElementById('seekContent')?.value || seekForm.content,
- info_type: 'seek',
- expect_category: seekForm.edition,
- expect_number: seekForm.features ? `J0${seekForm.features}` : null
- })
- })
- const data = await res.json()
- if (data.id || data.code === 0) {
- alert('发布成功!')
- setShowPublish(false)
- setSeekForm({ edition: '龙钞', price: '', features: '', contact: '', content: '', title: '' })
- const contentEl = document.getElementById('seekContent')
- if (contentEl) contentEl.value = ''
- fetchMyList()
- } else {
- alert(data.message || '发布失败')
- }
- } catch (e) {
- alert('发布失败: ' + e.message)
- }
- }
-
- // 发布行情(新增发布默认是行情)
- const handlePublishDeal = async () => {
- const content = document.getElementById('publishContent')?.value || ''
- if (!formData.title) {
- alert('请填写标题')
- return
- }
- try {
- const token = localStorage.getItem('token')
- const res = await fetch(`${API_BASE}/api/information/`, {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- title: formData.title,
- content: content,
- info_type: formData.category === '成交' ? 'deal' : 'seek'
- })
- })
- const data = await res.json()
- if (data.id || data.code === 0) {
- alert('发布成功!')
- setShowPublish(false)
- setFormData({ edition: '龙钞', type: '标百', isGraded: true, category: '成交', source: '直播', title: '' })
- const contentEl = document.getElementById('publishContent')
- if (contentEl) contentEl.value = ''
- fetchMyList()
- } else {
- alert(data.message || '发布失败')
- }
- } catch (e) {
- alert('发布失败: ' + e.message)
- }
- }
-
- // 删除
- const handleDelete = async (id) => {
- if (!confirm('确定删除这条信息吗?')) return
- try {
- const token = localStorage.getItem('token')
- const res = await fetch(`${API_BASE}/api/information/${id}`, {
- method: 'DELETE',
- headers: { Authorization: `Bearer ${token}` }
- })
- if (res.ok) {
- alert('删除成功')
- fetchMyList()
- } else {
- alert('删除失败')
- }
- } catch (e) {
- alert('删除失败: ' + e.message)
- }
- }
-
- // 编辑
- const handleEdit = (item) => {
- setEditingItem({ id: item.id, title: item.title, content: item.content })
- }
-
- const saveEdit = async () => {
- if (!editingItem) return
- try {
- const token = localStorage.getItem('token')
- const res = await fetch(`${API_BASE}/api/information/${editingItem.id}`, {
- method: 'PUT',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ title: editingItem.title, content: editingItem.content })
- })
- if (res.ok) {
- alert('保存成功')
- setEditingItem(null)
- fetchMyList()
- } else {
- alert('保存失败')
- }
- } catch (e) {
- alert('保存失败: ' + e.message)
- }
- }
-
- const getUserPhone = () => {
- try {
- const user = JSON.parse(localStorage.getItem('user') || '{}')
- return user.phone || user.phoneNumber || user.mobile || user.tel || ''
- } catch {
- return ''
- }
- }
-
- useEffect(() => {
- setSeekForm(prev => ({ ...prev, contact: getUserPhone() }))
- }, [])
-
- // 格式化日期
- const formatDate = (date) => {
- if (!date) return '-'
- return new Date(date).toLocaleString('zh-CN').slice(0, 16)
- }
-
- // 过滤后的列表
- const filteredList = myList.filter(item => filterType === 'all' || item.info_type === filterType)
-
- return (
-
- {/* 顶部 Tab 切换 */}
-
-
- {[
- { key: 'manage', label: '📋 发布管理' }
- ].map(tab => (
-
setActiveTab(tab.key)}
- style={{
- flex: 1,
- padding: '12px 16px',
- borderRadius: '10px',
- background: activeTab === tab.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : 'transparent',
- color: activeTab === tab.key ? '#fff' : '#9ca3af',
- cursor: 'pointer',
- textAlign: 'center',
- fontSize: '14px',
- fontWeight: '600',
- transition: 'all 0.3s'
- }}
- >
- {tab.label}
-
- ))}
-
-
-
-
- {/* 寻配号发布页 - 已删除 */}
- {false && (
-
-
🔍 寻配号发布
-
-
-
-
-
- {['龙钞', '马钞', '蛇钞'].map(ed => {
- const selected = seekForm.edition === ed
- return (
- setSeekForm({ ...seekForm, edition: ed })}
- style={{
- flex: 1,
- padding: '10px',
- borderRadius: '8px',
- border: selected ? '2px solid #10b981' : '1px solid #374151',
- background: selected ? 'rgba(16,185,129,0.15)' : '#1f2937',
- color: selected ? '#10b981' : '#d1d5db',
- cursor: 'pointer',
- fontSize: '14px',
- fontWeight: '500'
- }}
- >
- {ed}
-
- )
- })}
-
-
-
-
-
-
setSeekForm({ ...seekForm, features: e.target.value.toUpperCase().slice(0, 8) })}
- placeholder="输入号码特征,如:12345678"
- maxLength={8}
- style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#fff', fontSize: '14px', boxSizing: 'border-box' }}
- />
-
X=任意数字 A=非4 B=非47 C=非347 D=非247
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- setSeekForm({ ...seekForm, contact: e.target.value })}
- placeholder="请输入手机号或微信"
- style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#fff', fontSize: '14px', boxSizing: 'border-box' }}
- />
-
-
-
- 🚀 发布寻配号
-
-
-
- )}
-
- {/* 发布管理页 */}
- {activeTab === 'manage' && (
-
- {/* 新增发布区域 - 发布行情信息 */}
-
-
-
📋 发布管理
- setShowPublish(!showPublish)}
- style={{
- padding: '10px 20px',
- background: showPublish ? 'rgba(108,114,132,0.5)' : 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
- border: 'none',
- borderRadius: '10px',
- color: '#fff',
- cursor: 'pointer',
- fontSize: '14px',
- fontWeight: '600'
- }}
- >
- {showPublish ? '✕ 收起' : '+ 💰 新增行情发布'}
-
-
-
- {/* 行情发布表单 */}
- {showPublish && (
-
-
💰 发布行情信息
-
-
-
-
-
-
- {['龙钞', '马钞', '蛇钞', '其他'].map(ed => (
- setFormData({ ...formData, edition: ed })}
- style={{
- flex: 1,
- padding: '8px',
- borderRadius: '6px',
- border: formData.edition === ed ? '2px solid #f59e0b' : '1px solid #374151',
- background: formData.edition === ed ? 'rgba(245,158,11,0.15)' : '#1f2937',
- color: formData.edition === ed ? '#f59e0b' : '#d1d5db',
- cursor: 'pointer',
- fontSize: '12px'
- }}
- >
- {ed}
-
- ))}
-
-
-
-
-
-
-
-
- {['标百', '标十', '单张'].map(t => (
- setFormData({ ...formData, type: t })}
- style={{
- flex: 1,
- padding: '8px',
- borderRadius: '6px',
- border: formData.type === t ? '2px solid #f59e0b' : '1px solid #374151',
- background: formData.type === t ? 'rgba(245,158,11,0.15)' : '#1f2937',
- color: formData.type === t ? '#f59e0b' : '#d1d5db',
- cursor: 'pointer',
- fontSize: '12px'
- }}
- >
- {t}
-
- ))}
-
-
-
-
- setFormData({ ...formData, isGraded: !formData.isGraded })}
- style={{
- width: '100%',
- padding: '8px',
- borderRadius: '6px',
- border: formData.isGraded ? '2px solid #f59e0b' : '1px solid #374151',
- background: formData.isGraded ? 'rgba(245,158,11,0.15)' : '#1f2937',
- color: formData.isGraded ? '#f59e0b' : '#d1d5db',
- cursor: 'pointer',
- fontSize: '12px'
- }}
- >
- {formData.isGraded ? '✓ 评级币' : '○ 裸钞'}
-
-
-
-
-
-
-
- {[{value:'成交',label:'💰 成交'},{value:'求购',label:'🔍 求购'},{value:'出售',label:'💵 出售'}].map(opt => (
- setFormData({ ...formData, category: opt.value })}
- style={{
- flex: 1,
- padding: '10px',
- borderRadius: '8px',
- border: formData.category === opt.value ? '2px solid #f59e0b' : '1px solid #374151',
- background: formData.category === opt.value ? 'rgba(245,158,11,0.15)' : '#1f2937',
- color: formData.category === opt.value ? '#f59e0b' : '#d1d5db',
- cursor: 'pointer',
- fontSize: '13px',
- fontWeight: '500'
- }}
- >
- {opt.label}
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 🚀 提交发布
-
-
-
- )}
-
-
- {/* 列表过滤:全部 / 寻号 / 行情 */}
-
- setFilterType('all')}
- style={{
- padding: '8px 16px',
- background: filterType === 'all' ? '#3b82f6' : '#374151',
- border: 'none',
- borderRadius: '6px',
- color: '#fff',
- fontSize: '13px',
- cursor: 'pointer'
- }}
- >
- 全部
-
- setFilterType('seek')}
- style={{
- padding: '8px 16px',
- background: filterType === 'seek' ? '#10b981' : '#374151',
- border: 'none',
- borderRadius: '6px',
- color: '#fff',
- fontSize: '13px',
- cursor: 'pointer'
- }}
- >
- 🔍 寻号
-
- setFilterType('deal')}
- style={{
- padding: '8px 16px',
- background: filterType === 'deal' ? '#f59e0b' : '#374151',
- border: 'none',
- borderRadius: '6px',
- color: '#fff',
- fontSize: '13px',
- cursor: 'pointer'
- }}
- >
- 💰 行情
-
-
-
- {/* 发布列表 */}
- {loading ? (
-
加载中...
- ) : filteredList.length === 0 ? (
-
暂无发布记录
- ) : (
-
- {filteredList.map(item => (
-
- {/* 标题行 */}
-
-
{item.title}
-
- {item.info_type === 'deal' ? '💰 行情' : '🔍 寻号'}
-
-
- {/* 日期+用户名 */}
-
- 📅 {formatDate(item.created_at)} | 👤 {item.user_name || '匿名用户'}
-
- {/* 正文 - 默认收起,点击展开 */}
- {item.content && (
-
-
toggleExpand(item.id)}
- style={{
- color: '#10b981',
- fontSize: '13px',
- cursor: 'pointer',
- display: 'flex',
- alignItems: 'center',
- gap: '6px',
- padding: '8px 12px',
- background: 'rgba(16,185,129,0.1)',
- borderRadius: '6px',
- border: '1px solid rgba(16,185,129,0.2)'
- }}
- >
- {expandedItems[item.id] ? '▼' : '▶'}
- {expandedItems[item.id] ? '收起详情' : '展开查看详情'}
-
- {expandedItems[item.id] && (
-
- {item.content.split('\n').map((line, i) => (
-
- {line || ' '}
-
- ))}
-
- )}
-
- )}
- {/* 操作按钮 */}
-
-
- handleEdit(item)} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #3b82f6', background: 'transparent', color: '#3b82f6', cursor: 'pointer', fontSize: '12px' }}>✏️ 编辑
- handleDelete(item.id)} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #ef4444', background: 'transparent', color: '#ef4444', cursor: 'pointer', fontSize: '12px' }}>🗑️ 删除
-
-
-
- ))}
-
- )}
-
- )}
-
-
- {/* 编辑弹窗 */}
- {editingItem && (
-
-
-
✏️ 编辑信息
-
-
- setEditingItem({ ...editingItem, title: e.target.value })}
- style={{ width: '100%', padding: '12px', background: '#0f172a', border: '1px solid #374151', borderRadius: '8px', color: '#fff', boxSizing: 'border-box', fontSize: '14px' }}
- />
-
-
-
-
-
- 💾 保存
- setEditingItem(null)} style={{ flex: 1, padding: '12px', border: '1px solid #374151', borderRadius: '8px', background: 'transparent', color: '#9ca3af', cursor: 'pointer', fontSize: '14px' }}>取消
-
-
-
- )}
-
- )
-}
-
-
-
diff --git a/frontend/src/pages/List.jsx b/frontend/src/pages/List.jsx
index 0078943..03a758f 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,12 @@ 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 [dealSearch, setDealSearch] = useState('')
+ const [dealSortField, setDealSortField] = useState('created_at')
+ const [dealSortOrder, setDealSortOrder] = useState('desc')
+ const [myDeals, setMyDeals] = useState([])
+ const [dealsLoading, setDealsLoading] = useState(false)
useEffect(() => {
// 检查是否管理员
@@ -61,6 +68,56 @@ 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 filteredDeals = myDeals.filter(deal => {
+ if (!dealSearch) return true
+ const s = dealSearch.toLowerCase().trim()
+ const fields = [deal.title, deal.content, deal.category, deal.packaging, deal.grading_company, deal.grading_score, deal.deal_no, deal.seller, deal.platform, deal.buyer, deal.deal_price?.toString(), deal.deal_date].filter(v => v).map(v => v.toString().toLowerCase())
+ return fields.some(f => f.includes(s))
+ }).sort((a, b) => {
+ let aVal = a[dealSortField] || ''
+ let bVal = b[dealSortField] || ''
+ if (dealSortField === 'created_at') {
+ aVal = new Date(a.created_at).getTime()
+ bVal = new Date(b.created_at).getTime()
+ } else if (dealSortField === 'deal_price') {
+ aVal = a.deal_price || 0
+ bVal = b.deal_price || 0
+ }
+ if (dealSortOrder === 'asc') return aVal > bVal ? 1 : -1
+ return aVal < bVal ? 1 : -1
+ })
+
const fetchCollections = async () => {
setLoading(true)
const token = localStorage.getItem('token')
@@ -455,7 +512,18 @@ export default function List() {
-
我的藏品 ({filteredCollections.length})
+
+
+ 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})
+
+ 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})
+
+
+
{filter && filterType && (
)}
+ {activeTab === 'collections' && (
+ <>
{/* 搜索框 - 全字段搜索 */}
))}
+ >
+ )}
- {/* 分页组件 */}
- {pagination.pages > 1 && (
+ {/* 分页组件 - 仅在藏品tab下显示 */}
+ {activeTab === 'collections' && pagination.pages > 1 && (
{ 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 }}>上一页
@@ -576,6 +648,70 @@ export default function List() {
+ {/* 行情tab内容 */}
+ {activeTab === 'deals' && (
+
+ {/* 行情搜索和排序 */}
+
+ setDealSearch(e.target.value)}
+ style={{
+ flex: 1,
+ background: 'rgba(255,255,255,0.05)',
+ border: '1px solid rgba(255,255,255,0.1)',
+ color: '#fff',
+ padding: '8px 12px',
+ borderRadius: '8px',
+ fontSize: '14px',
+ outline: 'none'
+ }}
+ />
+
+
+
+ {dealsLoading ? (
+
加载中...
+ ) : filteredDeals.length === 0 ? (
+
+
📊
+
{dealSearch ? '没有匹配的行情' : '暂无行情记录'}
+
+ ) : (
+
+ {filteredDeals.map(deal => (
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* 藏品tab内容 */}
+ {activeTab === 'collections' && (
+ <>
{loading ? (
加载中...
) : filteredCollections.length === 0 ? (
@@ -597,7 +733,237 @@ export default function List() {
) : (
filteredCollections.map(item =>
)
)}
+ >
+ )}
)
}
+
+// 行情列表项组件
+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}
}
+
+
+ 编辑
+ 删除
+
+
+ )}
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx
index 47f3198..8c8dfc1 100644
--- a/frontend/src/pages/Login.jsx
+++ b/frontend/src/pages/Login.jsx
@@ -18,7 +18,8 @@ export default function Login() {
confirmPassword: '',
email: '',
phone: '',
- verifyCode: ''
+ verifyCode: '',
+ inviteCode: ''
})
const [registerLoading, setRegisterLoading] = useState(false)
const [registerError, setRegisterError] = useState('')
@@ -148,6 +149,9 @@ export default function Login() {
if (registerData.email) {
payload.email = registerData.email
}
+ if (registerData.inviteCode) {
+ payload.inviteCode = registerData.inviteCode
+ }
const res = await fetch('/api/auth/register', {
method: 'POST',
@@ -165,7 +169,7 @@ export default function Login() {
alert('注册成功!请登录')
setShowRegister(false)
- setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '' })
+ setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '', inviteCode: '' })
setCodeSent(false)
setCodeCountdown(0)
generateCaptcha()
@@ -584,7 +588,7 @@ export default function Login() {
{/* 确认密码 */}
-
+
+ {/* 邀请码(选填) */}
+
+ setRegisterData(prev => ({ ...prev, inviteCode: e.target.value }))}
+ placeholder="邀请码(选填)"
+ style={{
+ width: '100%',
+ padding: '14px',
+ borderRadius: '8px',
+ border: '1px solid rgba(255,255,255,0.2)',
+ background: 'rgba(255,255,255,0.05)',
+ color: '#fff',
+ fontSize: '16px',
+ outline: 'none'
+ }}
+ />
+
+
{/* 用户协议勾选 */}
@@ -626,7 +650,7 @@ export default function Login() {
{
setShowRegister(false)
- setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '' })
+ setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '', inviteCode: '' })
setRegisterError('')
setCodeSent(false)
setCodeCountdown(0)
diff --git a/frontend/src/pages/News.jsx b/frontend/src/pages/News.jsx
index b316cc6..7d2472b 100644
--- a/frontend/src/pages/News.jsx
+++ b/frontend/src/pages/News.jsx
@@ -337,9 +337,9 @@ export default function News() {
// Tab切换
const tabs = [
- { key: 'seek', label: '🔍 寻配号' },
- { key: 'deal', label: '💰 成交行情' },
- { key: 'yichen', label: '📊 一尘看板' }
+ { key: 'seek', label: '寻配号' },
+ { key: 'deal', label: '成交行情' },
+ { key: 'yichen', label: '一尘看板' }
]
const formatDate = (dateStr) => {
@@ -459,7 +459,7 @@ export default function News() {
)}
- {activeTab === 'seek' ? '🔍 寻配号信息' : activeTab === 'deal' ? '💰 成交行情信息' : '📊 一尘看板'}
+ {activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? '成交行情信息' : '一尘看板'}
{activeTab === 'seek' && (
{ setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部
@@ -470,7 +470,10 @@ export default function News() {
)}
- {loading ? (
+ {/* 一尘看板独立渲染 */}
+ {activeTab === 'yichen' ? (
+
+ ) : loading ? (
加载中...
) : infoList.length === 0 ? (
@@ -478,7 +481,7 @@ export default function News() {
) : (
- {activeTab === 'yichen' ?
: infoList.map(item => {
+ {infoList.map(item => {
// 解析正文中的号码特征和联系方式
const content = item.content || ''
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
diff --git a/frontend/src/pages/News_YichensBoard.jsx b/frontend/src/pages/News_YichensBoard.jsx
new file mode 100644
index 0000000..7af8ba1
--- /dev/null
+++ b/frontend/src/pages/News_YichensBoard.jsx
@@ -0,0 +1,218 @@
+import React, { useState, useEffect } from 'react'
+
+export default function YichensBoard() {
+ const [todayStats, setTodayStats] = useState({ total: 0, deals: 0, wants: 0, others: 0 })
+ const [todayCategory, setTodayCategory] = useState([])
+ const [posts, setPosts] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [expandedPosts, setExpandedPosts] = useState({})
+ const [postTypeFilter, setPostTypeFilter] = useState('all')
+ const [categoryFilter, setCategoryFilter] = useState('')
+ const [page, setPage] = useState(1)
+ const [totalPosts, setTotalPosts] = useState(0)
+ const [searchKeyword, setSearchKeyword] = useState('')
+ const API_BASE = localStorage.getItem('API_BASE') || ''
+
+ const today = new Date()
+ const dateStr = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate()
+
+ useEffect(() => { fetchTodayStats(); fetchPosts(1, '') }, [])
+
+ const fetchTodayStats = async () => {
+ try {
+ const res = await fetch(API_BASE + '/api/yichens/stats/today')
+ setTodayStats(await res.json())
+ } catch(e) { console.error(e) }
+ }
+
+ const fetchTodayCategory = async () => {
+ try {
+ const res = await fetch(API_BASE + '/api/yichens/stats/today-category')
+ setTodayCategory(await res.json())
+ } catch(e) { console.error(e) }
+ }
+
+ const fetchPosts = async (p, cat) => {
+ setLoading(true)
+ const currentPage = p !== undefined ? p : page
+ const currentCat = cat !== undefined ? cat : categoryFilter
+
+ let url = API_BASE + '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390)
+ if (postTypeFilter === 'deal') url += '&post_type=deal'
+ else if (postTypeFilter === 'want') url += '&post_type=want'
+ else if (postTypeFilter === 'other') url += '&post_type=normal'
+ try {
+ const res = await fetch(url)
+ let data = await res.json() || []
+ if (currentCat) {
+ if (currentCat === '龙') {
+ data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
+ } else if (currentCat === '蛇') {
+ data = data.filter(p => p.category && p.category.includes('蛇'))
+ } else if (currentCat === '马') {
+ data = data.filter(p => p.category && p.category.includes('马'))
+ } else if (currentCat === '其他') {
+ data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马'))
+ }
+ }
+ // 搜索关键词过滤 - 强匹配(完全包含)
+ if (searchKeyword) {
+ const kw = searchKeyword.trim()
+ if (kw) {
+ data = data.filter(p =>
+ (p.title && p.title.includes(kw)) ||
+ (p.content && p.content.includes(kw)) ||
+ (p.category && p.category.includes(kw)) ||
+ (p.contact && p.contact.includes(kw))
+ )
+ }
+ }
+ setPosts(data)
+ setTotalPosts(todayStats.total || 0)
+ } catch { setPosts([]) }
+ setLoading(false)
+ }
+
+ useEffect(() => { if (todayStats.total > 0) fetchTodayCategory() }, [todayStats])
+
+ // 搜索关键词变化时重新获取数据
+ useEffect(() => {
+ setPage(1)
+ fetchPosts(1, '')
+ }, [searchKeyword])
+
+ const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
+
+ const StatCard = ({ label, value, color, onClick }) => (
+
+ )
+
+ return (
+
+
+
+ 📈 连体钞/纪念钞 {dateStr}
+
+
+ { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
+ { setPostTypeFilter('deal'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
+ { setPostTypeFilter('want'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
+ { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
+
+
+
+
+
📊 今日分类统计
+
+ {todayCategory.map(cat => (
+
+ {cat.category} ({cat.count})
+
+ ))}
+
+
+
+ {/* 搜索框 */}
+
+
+ { setSearchKeyword(e.target.value); setPage(1); fetchPosts(1, '') }}
+ style={{ flex: 1, padding: '10px 14px', borderRadius: 8, border: '1px solid #374151', background: '#1f2937', color: '#fff', fontSize: 13, outline: 'none' }}
+ />
+ { setSearchKeyword(''); setPage(1); fetchPosts(1, '') }}
+ style={{ padding: '10px 16px', borderRadius: 8, border: 'none', background: '#4b5563', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
+ 清空
+
+
+ {searchKeyword &&
搜索: "{searchKeyword}",找到 {posts.length} 条结果
}
+
+
+
+ {[{key:'all',label:'全部'},{key:'deal',label:'出售'},{key:'want',label:'求购'},{key:'other',label:'其他'}].map(k => (
+ { setPostTypeFilter(k.key==='other'?'other':k.key); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }}
+ style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
+ background: postTypeFilter===k.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
+ {k.label}
+
+ ))}
+
+
+
+ {[{key:'',label:'全部'},{key:'龙',label:'龙钞'},{key:'蛇',label:'蛇钞'},{key:'马',label:'马钞'},{key:'其他',label:'其他'}].map(k => (
+ { setCategoryFilter(k.key); setPage(1); fetchPosts(1, k.key) }}
+ style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
+ background: categoryFilter===k.key ? 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
+ {k.label}
+
+ ))}
+
+
+ {loading ?
加载中...
: (
+
+
+ {posts.map(post => (
+
+
togglePost(post.post_id)} style={{ cursor: 'pointer' }}>
+
+
{post.title||'无标题'}
+
+
+ {post.post_type==='deal'?'出售':post.post_type==='want'?'求购':post.post_type==='normal'?'其他':'普通'}
+
+
+ {post.category || '-'}
+
+
+
+
+ {post.author_username||'未知'}
+ {post.post_time?.substring(0,16)||''}
+
+
+ {expandedPosts[post.post_id] && post.content && (
+
+
+ {post.content}
+
+ {post.url &&
查看原帖}
+
+ )}
+
+ ))}
+
+
+ {/* 分页按钮移到页面底部 */}
+
+
+ 共 {totalPosts} 条帖子,{Math.ceil(totalPosts / 390)} 页,当前第 {page} 页
+
+
+ {page > 1 ? (
+ { const newPage = page - 1; setPage(newPage); fetchPosts(newPage, categoryFilter) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>上一页
+ ) : (
+ 上一页
+ )}
+ {posts.length >= 390 ? (
+ { const newPage = page + 1; setPage(newPage); fetchPosts(newPage, categoryFilter) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>下一页
+ ) : (
+ 下一页
+ )}
+
+
+
+ )}
+
+ )
+}
diff --git a/frontend/src/pages/YichensBoard.jsx b/frontend/src/pages/YichensBoard.jsx
index 7af8ba1..85d80e0 100644
--- a/frontend/src/pages/YichensBoard.jsx
+++ b/frontend/src/pages/YichensBoard.jsx
@@ -4,47 +4,61 @@ export default function YichensBoard() {
const [todayStats, setTodayStats] = useState({ total: 0, deals: 0, wants: 0, others: 0 })
const [todayCategory, setTodayCategory] = useState([])
const [posts, setPosts] = useState([])
- const [loading, setLoading] = useState(false)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
const [expandedPosts, setExpandedPosts] = useState({})
const [postTypeFilter, setPostTypeFilter] = useState('all')
const [categoryFilter, setCategoryFilter] = useState('')
const [page, setPage] = useState(1)
const [totalPosts, setTotalPosts] = useState(0)
const [searchKeyword, setSearchKeyword] = useState('')
- const API_BASE = localStorage.getItem('API_BASE') || ''
const today = new Date()
const dateStr = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate()
- useEffect(() => { fetchTodayStats(); fetchPosts(1, '') }, [])
+ useEffect(() => {
+ console.log('YichensBoard: 开始加载数据')
+ fetchTodayStats()
+ }, [])
const fetchTodayStats = async () => {
try {
- const res = await fetch(API_BASE + '/api/yichens/stats/today')
- setTodayStats(await res.json())
- } catch(e) { console.error(e) }
+ console.log('YichensBoard: 请求 /api/yichens/stats/today')
+ const res = await fetch('/api/yichens/stats/today')
+ if (!res.ok) throw new Error('stats API error: ' + res.status)
+ const data = await res.json()
+ console.log('YichensBoard: stats data', data)
+ setTodayStats(data)
+ } catch(e) {
+ console.error('YichensBoard: fetchTodayStats error', e)
+ setError(e.message)
+ }
}
- const fetchTodayCategory = async () => {
- try {
- const res = await fetch(API_BASE + '/api/yichens/stats/today-category')
- setTodayCategory(await res.json())
- } catch(e) { console.error(e) }
- }
-
- const fetchPosts = async (p, cat) => {
+ const fetchPosts = async (p, cat, kw) => {
setLoading(true)
+ setError(null)
const currentPage = p !== undefined ? p : page
const currentCat = cat !== undefined ? cat : categoryFilter
+ const searchKw = kw !== undefined ? kw : searchKeyword
- let url = API_BASE + '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390)
+ let url = '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390)
if (postTypeFilter === 'deal') url += '&post_type=deal'
else if (postTypeFilter === 'want') url += '&post_type=want'
else if (postTypeFilter === 'other') url += '&post_type=normal'
+ if (searchKw && searchKw.trim()) url += '&keyword=' + encodeURIComponent(searchKw.trim())
+
try {
+ console.log('YichensBoard: 请求 posts', url)
const res = await fetch(url)
+ if (!res.ok) throw new Error('posts API error: ' + res.status)
let data = await res.json() || []
- if (currentCat) {
+ // 确保data是数组
+ if (!Array.isArray(data)) {
+ data = data.posts || data.data || []
+ }
+ console.log('YichensBoard: posts data count', data.length)
+ if (currentCat && Array.isArray(data)) {
if (currentCat === '龙') {
data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
} else if (currentCat === '蛇') {
@@ -55,36 +69,52 @@ export default function YichensBoard() {
data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马'))
}
}
- // 搜索关键词过滤 - 强匹配(完全包含)
- if (searchKeyword) {
- const kw = searchKeyword.trim()
- if (kw) {
- data = data.filter(p =>
- (p.title && p.title.includes(kw)) ||
- (p.content && p.content.includes(kw)) ||
- (p.category && p.category.includes(kw)) ||
- (p.contact && p.contact.includes(kw))
- )
+ // 确保data是数组
+ if (!Array.isArray(data)) {
+ data = data.posts || data.data || []
+ }
+ console.log('YichensBoard: posts data count', data.length)
+ if (currentCat && Array.isArray(data)) {
+ if (currentCat === '龙') {
+ data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
+ } else if (currentCat === '蛇') {
+ data = data.filter(p => p.category && p.category.includes('蛇'))
+ } else if (currentCat === '马') {
+ data = data.filter(p => p.category && p.category.includes('马'))
+ } else if (currentCat === '其他') {
+ data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马'))
}
}
- setPosts(data)
- setTotalPosts(todayStats.total || 0)
- } catch { setPosts([]) }
- setLoading(false)
+ // 支持新格式 {posts:[], total:xxx} 或旧格式 [{},{}]
+ const postsArray = Array.isArray(data) ? data : (data.posts || [])
+ const totalCount = data.total || todayStats.total || postsArray.length
+ setPosts(postsArray)
+ setTotalPosts(totalCount)
+ } catch(e) {
+ console.error('YichensBoard: fetchPosts error', e)
+ setError(e.message)
+ setPosts([])
+ } finally {
+ setLoading(false)
+ }
}
- useEffect(() => { if (todayStats.total > 0) fetchTodayCategory() }, [todayStats])
+ useEffect(() => {
+ if (todayStats.total > 0) {
+ fetchPosts(1, categoryFilter, searchKeyword)
+ }
+ }, [todayStats, categoryFilter, postTypeFilter])
- // 搜索关键词变化时重新获取数据
useEffect(() => {
setPage(1)
- fetchPosts(1, '')
+ fetchPosts(1, categoryFilter, searchKeyword)
}, [searchKeyword])
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
const StatCard = ({ label, value, color, onClick }) => (
-
{ setLoading(true); if (onClick) onClick(); }}
+ style={{
background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)',
borderRadius: 12, padding: '12px 8px', border: '1px solid #374151',
cursor: onClick ? 'pointer' : 'default',
@@ -95,6 +125,17 @@ export default function YichensBoard() {
)
+ if (error) {
+ return (
+
+
加载失败: {error}
+
{ setError(null); fetchTodayStats(); }} style={{ marginTop: 10, padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>
+ 重试
+
+
+ )
+ }
+
return (
@@ -102,32 +143,25 @@ export default function YichensBoard() {
📈 连体钞/纪念钞 {dateStr}
- { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
- { setPostTypeFilter('deal'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
- { setPostTypeFilter('want'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
- { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
+ { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); }} />
+ { setPostTypeFilter('deal'); setCategoryFilter(''); setPage(1); }} />
+ { setPostTypeFilter('want'); setCategoryFilter(''); setPage(1); }} />
+ { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); }} />
📊 今日分类统计
-
- {todayCategory.map(cat => (
-
- {cat.category} ({cat.count})
-
- ))}
-
+
龙: {todayStats.dragons || 0} | 马: {todayStats.horses || 0} | 蛇: {todayStats.snakes || 0} | 天马: {todayStats.tianma || 0}
- {/* 搜索框 */}
{ setSearchKeyword(e.target.value); setPage(1); fetchPosts(1, '') }}
+ onChange={(e) => { const kw = e.target.value; setSearchKeyword(kw); setPage(1); fetchPosts(1, '', kw) }}
style={{ flex: 1, padding: '10px 14px', borderRadius: 8, border: '1px solid #374151', background: '#1f2937', color: '#fff', fontSize: 13, outline: 'none' }}
/>
- {searchKeyword &&
搜索: "{searchKeyword}",找到 {posts.length} 条结果
}
+ {searchKeyword &&
搜索: "{searchKeyword}",共找到 {totalPosts} 条结果
}
{[{key:'all',label:'全部'},{key:'deal',label:'出售'},{key:'want',label:'求购'},{key:'other',label:'其他'}].map(k => (
-
{ setPostTypeFilter(k.key==='other'?'other':k.key); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }}
+ { setPostTypeFilter(k.key==='other'?'other':k.key); setCategoryFilter(''); setPage(1); }}
style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
background: postTypeFilter===k.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
{k.label}
@@ -151,7 +185,7 @@ export default function YichensBoard() {
{[{key:'',label:'全部'},{key:'龙',label:'龙钞'},{key:'蛇',label:'蛇钞'},{key:'马',label:'马钞'},{key:'其他',label:'其他'}].map(k => (
-
{ setCategoryFilter(k.key); setPage(1); fetchPosts(1, k.key) }}
+ { setCategoryFilter(k.key); setPage(1); }}
style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
background: categoryFilter===k.key ? 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
{k.label}
@@ -162,50 +196,53 @@ export default function YichensBoard() {
{loading ? 加载中...
: (
- {posts.map(post => (
-
-
togglePost(post.post_id)} style={{ cursor: 'pointer' }}>
-
-
{post.title||'无标题'}
-
-
- {post.post_type==='deal'?'出售':post.post_type==='want'?'求购':post.post_type==='normal'?'其他':'普通'}
-
-
- {post.category || '-'}
-
+ {posts.length === 0 ? (
+
暂无帖子数据
+ ) : (
+ posts.map(post => (
+
+
togglePost(post.post_id)} style={{ cursor: 'pointer' }}>
+
+
{post.title||'无标题'}
+
+
+ {post.post_type==='deal'?'出售':post.post_type==='want'?'求购':post.post_type==='normal'?'其他':'普通'}
+
+
+ {post.category || '-'}
+
+
+
+
+ {post.author_username||'未知'}
+ {post.post_time?.substring(0,16)||''}
-
- {post.author_username||'未知'}
- {post.post_time?.substring(0,16)||''}
-
+ {expandedPosts[post.post_id] && post.content && (
+
+
+ {post.content}
+
+ {post.url &&
查看原帖}
+
+ )}
- {expandedPosts[post.post_id] && post.content && (
-
-
- {post.content}
-
- {post.url &&
查看原帖}
-
- )}
-
- ))}
+ ))
+ )}
- {/* 分页按钮移到页面底部 */}
- 共 {totalPosts} 条帖子,{Math.ceil(totalPosts / 390)} 页,当前第 {page} 页
+ 共 {totalPosts} 条结果,{Math.ceil(totalPosts / 390)} 页,当前第 {page} 页
{page > 1 ? (
- { const newPage = page - 1; setPage(newPage); fetchPosts(newPage, categoryFilter) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>上一页
+ { const newPage = page - 1; setPage(newPage); fetchPosts(newPage, categoryFilter, searchKeyword) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>上一页
) : (
上一页
)}
- {posts.length >= 390 ? (
- { const newPage = page + 1; setPage(newPage); fetchPosts(newPage, categoryFilter) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>下一页
+ {posts.length >= 390 && totalPosts > page * 390 ? (
+ { const newPage = page + 1; setPage(newPage); fetchPosts(newPage, categoryFilter, searchKeyword) }} style={{ padding: '6px 12px', borderRadius: 6, border: 'none', background: '#3b82f6', color: '#fff', cursor: 'pointer', fontSize: 12 }}>下一页
) : (
下一页
)}
diff --git a/frontend/src/utils/numberCategory.js b/frontend/src/utils/numberCategory.js
new file mode 100644
index 0000000..abe12df
--- /dev/null
+++ b/frontend/src/utils/numberCategory.js
@@ -0,0 +1,129 @@
+/**
+ * 号码分类工具
+ * 根据冠字号自动分类为:圆圆号、倒置号、金马王、金马号、金山王、天马王、金山号、天马号、朦胧号、如意号、钻石号、永恒号、带7号、带4号
+ */
+
+// 分类定义(按优先级排序)
+const CATEGORIES = [
+ { name: '圆圆号', mustNot: ['1','2','3','4','5','7'], mustHave: [] },
+ { name: '倒置号', mustNot: ['2','3','4','5','7'], mustHave: ['1'] },
+ { name: '金马王', mustNot: ['1','2','3','4','7'], mustHave: ['5'] },
+ { name: '金马号', mustNot: ['2','3','4','7'], mustHave: ['1','5'] },
+ { name: '金山王', mustNot: ['1','2','4','5','7'], mustHave: ['3'] },
+ { name: '天马王', mustNot: ['1','2','4','7'], mustHave: ['3','5'] },
+ { name: '金山号', mustNot: ['2','4','5','7'], mustHave: ['1','3'] },
+ { name: '天马号', mustNot: ['2','4','7'], mustHave: ['1','3','5'] },
+ { name: '朦胧号', mustNot: ['3','4','5','7'], mustHave: [] },
+ { name: '如意号', mustNot: ['1','3','4','7'], mustHave: [] },
+ { name: '钻石号', mustNot: ['3','4','7'], mustHave: [] },
+ { name: '永恒号', mustNot: ['4','7'], mustHave: [] },
+ { name: '带7号', mustNot: ['4'], mustHave: ['7'] },
+ { name: '带4号', mustNot: [], mustHave: ['4'] },
+]
+
+/**
+ * 提取冠字号中的数字部分
+ * @param {string} serial - 冠字号,如 J0123456789 或 J0123456781 或 J0123456701
+ * @returns {object} - { digits: 数字串, type: 'single'|'ten'|'hundred' }
+ */
+export function extractDigits(serial) {
+ if (!serial) return { digits: '', type: 'single' }
+
+ // 去掉J,取数字部分
+ const nums = serial.replace(/J/g, '').replace(/\D/g, '')
+
+ // 判断类型
+ if (nums.endsWith('01')) {
+ // 标百:去掉最后2位
+ return { digits: nums.slice(0, -2), type: 'hundred' }
+ } else if (nums.endsWith('1')) {
+ // 标十:去掉最后1位
+ return { digits: nums.slice(0, -1), type: 'ten' }
+ } else {
+ // 单张:全部数字
+ return { digits: nums, type: 'single' }
+ }
+}
+
+/**
+ * 号码分类函数
+ * @param {string} serial - 冠字号
+ * @returns {string} - 分类名称
+ */
+export function getNumberCategory(serial) {
+ const { digits } = extractDigits(serial)
+
+ if (!digits || digits.length < 7) {
+ return '其他'
+ }
+
+ // 按优先级匹配
+ for (const cat of CATEGORIES) {
+ if (matchesCategory(digits, cat)) {
+ return cat.name
+ }
+ }
+
+ return '其他'
+}
+
+/**
+ * 检查数字是否匹配分类条件
+ * @param {string} digits - 数字串
+ * @param {object} category - 分类定义
+ * @returns {boolean}
+ */
+function matchesCategory(digits, category) {
+ const mustNot = category.mustNot
+ const mustHave = category.mustHave
+
+ // 1. 检查必须不含的数字
+ for (const n of mustNot) {
+ if (digits.includes(n)) {
+ return false
+ }
+ }
+
+ // 2. 检查必须含有的数字
+ for (const n of mustHave) {
+ if (!digits.includes(n)) {
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * 获取分类颜色
+ * @param {string} category - 分类名称
+ * @returns {string} - 颜色 hex
+ */
+export function getNumberCategoryColor(category) {
+ const colors = {
+ '圆圆号': '#8b5cf6', // 紫
+ '倒置号': '#ec4899', // 粉
+ '金马王': '#f59e0b', // 金
+ '金马号': '#ef4444', // 红
+ '金山王': '#14b8a6', // 青
+ '天马王': '#06b6d4', // 蓝
+ '金山号': '#0d9488', // 绿松石
+ '天马号': '#22c55e', // 绿
+ '朦胧号': '#6366f1', // 靛蓝
+ '如意号': '#a855f7', // 紫红
+ '钻石号': '#eab308', // 黄
+ '永恒号': '#3b82f6', // ���
+ '带7号': '#f97316', // 橙
+ '带4号': '#64748b', // 灰
+ '其他': '#94a3b8',
+ }
+ return colors[category] || colors['其他']
+}
+
+/**
+ * 分类优先级(用于排序)
+ */
+export const NUMBER_CATEGORY_ORDER = CATEGORIES.map(c => c.name)
+
+// 可选值列表(用于下拉框)
+export const NUMBER_CATEGORY_OPTIONS = CATEGORIES.map(c => ({ value: c.name, label: c.name }))
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index c167f55..a2b9451 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -7,9 +7,12 @@ import { join } from 'path'
function getVersion() {
try {
const versionFile = join(__dirname, '..', 'config', 'VERSION')
- const content = readFileSync(versionFile, 'utf-8')
- const match = content.match(/^VERSION=(.*)$/m)
- return match ? match[1].trim() : '0.0.0'
+ const content = readFileSync(versionFile, 'utf-8').trim()
+ // 移除 VERSION= 前缀
+ if (content.startsWith('VERSION=')) {
+ return content.substring(7).trim()
+ }
+ return content || '0.0.0'
} catch (e) {
console.error('读取 VERSION 文件失败:', e.message)
return '0.0.0'
@@ -17,7 +20,7 @@ function getVersion() {
}
const APP_VERSION = getVersion()
-console.log(`📦 构建版本:v${APP_VERSION}`)
+console.log('📦 构建版本:v' + APP_VERSION)
// 构建时自动更新 index.html 的 title
function updateHtmlTitle() {
@@ -27,10 +30,10 @@ function updateHtmlTitle() {
// 替换
甲辰收藏 vXXX
htmlContent = htmlContent.replace(
/甲辰收藏 v[\d.]+<\/title>/,
- `甲辰收藏 v${APP_VERSION}`
+ '甲辰收藏 v' + APP_VERSION + ''
)
writeFileSync(htmlPath, htmlContent, 'utf-8')
- console.log(`✅ 已更新 index.html title: 甲辰收藏 v${APP_VERSION}`)
+ console.log('✅ 已更新 index.html title: 甲辰收藏 v' + APP_VERSION)
} catch (e) {
console.error('更新 index.html 失败:', e.message)
}
@@ -47,11 +50,10 @@ export default defineConfig({
build: {
rollupOptions: {
output: {
- entryFileNames: `assets/[name]-[hash]-[name].js`,
- chunkFileNames: `assets/[name]-[hash].js`,
- assetFileNames: `assets/[name]-[hash].[ext]`
+ entryFileNames: 'assets/[name]-[hash]-[name].js',
+ chunkFileNames: 'assets/[name]-[hash].js',
+ assetFileNames: 'assets/[name]-[hash].[ext]'
}
- },
- // 禁用缓存
+ }
}
-})
+})
\ No newline at end of file