diff --git a/README.md b/README.md
deleted file mode 100644
index 4143811..0000000
--- a/README.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# 甲辰藏品管理系统
-
-> 生肖纪念钞收藏管理系统
-
-## 版本信息
-
-| 项目 | 内容 |
-|------|------|
-| **版本** | v1.2.2 |
-| **代号** | 列表优化版本 |
-| **发布日期** | 2026-03-20 |
-
-## 技术栈
-
-- **后端**: FastAPI + PostgreSQL + 阿里云OSS + 阿里云短信
-- **前端**: React + Vite + Tailwind CSS
-- **部署**: Nginx
-
-## 核心功能
-
-- 用户管理(管理员/普通用户)
-- 藏品管理(CRUD、多条件筛选)
-- OCR识别(阿里云视觉智能)
-- 统计分析
-- 图片上传(阿里云OSS)
-- 短信验证码
-- 用户协议
-- 密码强度验证
-
-## 项目结构
-
-```
-jiachenlong/
-├── backend/ # FastAPI后端
-│ ├── app/
-│ │ ├── routers/ # API路由
-│ │ ├── services/ # 业务服务
-│ │ └── modules/ # 业务模块
-│ └── config/
-├── frontend/ # React前端
-│ ├── src/pages/ # 页面组件
-│ └── dist/ # 构建产物
-└── config/ # 版本配置
-```
-
-## 版本历史
-
-| 版本 | 日期 | 说明 |
-|------|------|------|
-| v1.2.2 | 2026-03-20 | 列表布局优化 |
-| v1.2.1 | 2026-03-20 | 密码强度验证、用户协议 |
-| v1.2.0 | 2026-03-20 | 管理员查看用户藏品 |
-| v1.1.21 | 2026-03-19 | Nginx优化、OSS集成 |
-| v1.1.0 | 2026-03-18 | 用户设置功能 |
-
-## 快速开始
-
-```bash
-# 后端
-cd backend && pip install -r requirements.txt
-python -m uvicorn app.main:app --reload
-
-# 前端
-cd frontend && npm install && npm run dev
-```
-
-## API接口
-
-### 认证
-- POST /api/auth/register - 注册
-- POST /api/auth/login - 登录
-- GET /api/auth/me - 当前用户
-
-### 藏品
-- GET /api/collections - 列表
-- POST /api/collections - 创建
-- PUT /api/collections/{id} - 更新
-- DELETE /api/collections/{id} - 删除
-
-## 开发团队
-
-| 角色 | 姓名 | 职责 |
-|------|------|------|
-| 产品负责人 | 酷博特 | 项目总负责 |
-| 项目经理 | 龙大 | 项目整体管理 |
-| 开发 | 龙二 | 本地开发 |
-| 测试 | 龙A | 生产A环境负责人 |
-| 测试 | 龙B | 生产B环境负责人 |
-| 运维 | 龙运 | 运维负责人 |
-| 运维 | 龙镜 | 镜像版本管理负责人 |
-
-## 文档
-
-- [部署手册](https://qcn9r1i8r51d.feishu.cn/docx/ZvEadXZ7XoxUtYxGLoIcMCfgn7g)
-- [Git仓库](http://47.253.189.47:3000/coolbot/jiachenlong)
-
----
-
-© 2026 甲辰收藏
diff --git a/VERSION b/VERSION
index 1f8d37f..dd91a8b 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.2.38
+1.2.98
diff --git a/backend/VERSION b/backend/VERSION
index 0b1f1ed..dd91a8b 100644
--- a/backend/VERSION
+++ b/backend/VERSION
@@ -1 +1 @@
-1.2.13
+1.2.98
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/core/coolbot_db.py b/backend/app/core/coolbot_db.py
new file mode 100644
index 0000000..a0b9014
--- /dev/null
+++ b/backend/app/core/coolbot_db.py
@@ -0,0 +1,37 @@
+import os
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.pool import QueuePool
+
+COOLBOT_DB_URL = os.getenv(
+ "COOLBOT_DB_URL",
+ "postgresql://coolbot:Coolbot123@pgm-bp1t1008h019ez6cno.pg.rds.aliyuncs.com:5432/coolbot_data"
+)
+
+coolbot_engine = create_engine(
+ COOLBOT_DB_URL,
+ poolclass=QueuePool,
+ pool_size=10,
+ max_overflow=20,
+ pool_timeout=30,
+ pool_recycle=1800,
+ pool_pre_ping=True,
+ echo=False,
+ connect_args={
+ "connect_timeout": 10,
+ "application_name": "zodiac-coolbot"
+ }
+)
+
+CoolbotSession = sessionmaker(autocommit=False, autoflush=False, bind=coolbot_engine)
+
+def get_coolbot_db():
+ """获取coolbot数据库会话"""
+ db = CoolbotSession()
+ try:
+ yield db
+ except Exception:
+ db.rollback()
+ raise
+ finally:
+ db.close()
diff --git a/backend/app/core/database.py b/backend/app/core/database.py
index 2218c71..8683b0b 100644
--- a/backend/app/core/database.py
+++ b/backend/app/core/database.py
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
DATABASE_URL = os.getenv(
"DATABASE_URL",
- "postgresql://postgres:postgres@localhost:5432/zodiac"
+ "postgresql://postgres:postgres@127.0.0.1:5432/zodiac"
)
# 增强版数据库引擎配置
diff --git a/backend/app/main.py b/backend/app/main.py
index 7624c39..0587121 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -16,6 +16,9 @@ from app.routers import auth, collections, operations
from app.routers import ocr as ocr_router
from app.routers import users as users_router
from app.routers import information as information_router
+from app.routers import yichens as yichens_router
+from app.routers import seek as seek_router
+from app.routers import deal as deal_router
# 版本信息 - 从 config/VERSION 文件读取
def get_version():
@@ -51,10 +54,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=["*"],
@@ -65,10 +69,10 @@ uploads_dir = "uploads"
os.makedirs(uploads_dir, exist_ok=True)
app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads")
-# 挂载项目静态资源目录(可选,生产环境建议用 Nginx)
-# static_dir = Path(__file__).parent.parent.parent / "static"
-# if static_dir.exists():
-# app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
+# 挂载项目静态资源目录
+static_dir = Path(__file__).parent.parent / "static"
+if static_dir.exists():
+ app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
# 添加日志中间件
app.middleware("http")(logging_middleware)
@@ -80,7 +84,10 @@ app.include_router(operations.router)
app.include_router(ocr_router.router) # OCR 识别
app.include_router(users_router.router) # 当前用户接口
app.include_router(users_router.admin_router) # 管理员用户管理
-app.include_router(information_router.router) # 资讯
+app.include_router(information_router.router)
+app.include_router(yichens_router.router) # 一尘看板
+app.include_router(seek_router.router) # 寻配号
+app.include_router(deal_router.router) # 成交行情
@app.get("/")
@@ -103,3 +110,6 @@ if __name__ == "__main__":
import uvicorn
port = int(os.getenv("PORT", "3000"))
uvicorn.run(app, host="0.0.0.0", port=port)
+
+from app.routers import seek as seek_router
+from app.routers import deal as deal_router
diff --git a/backend/app/models/deal_info.py b/backend/app/models/deal_info.py
new file mode 100644
index 0000000..82e3206
--- /dev/null
+++ b/backend/app/models/deal_info.py
@@ -0,0 +1,54 @@
+from sqlalchemy import Column, String, Text, Float, Date, Boolean, Integer, DateTime
+from sqlalchemy.sql import func
+from app.core.database import Base
+import uuid
+
+def generate_uuid():
+ return str(uuid.uuid4())
+
+class DealInfo(Base):
+ __tablename__ = "deal_info"
+
+ id = Column(String(36), primary_key=True, default=generate_uuid)
+ user_id = Column(String(36), nullable=True, index=True)
+
+ # 标题和内容
+ title = Column(String(255), nullable=False)
+ content = Column(Text, nullable=True)
+
+ # 成交信息
+ deal_price = Column(Float, nullable=True) # 成交价格
+ deal_date = Column(Date, nullable=True) # 成交日期
+ deal_no = Column(String(20), nullable=True, index=True) # 行情编号(从A000001开始递增)
+
+ # 包装和分类
+ packaging = Column(String(50), nullable=True) # 包装(标百/标十/单张)
+ category = Column(String(100), nullable=True) # 分类
+
+ # 评级相关
+ is_graded = Column(Boolean, default=False) # 是否评级
+ grading_company = Column(String(100), nullable=True) # 评级机构
+ grading_score = Column(String(50), nullable=True) # 评级分数
+
+ # 号码特征
+ tail_number = Column(String(10), nullable=True) # 尾号
+ size_type = Column(String(20), nullable=True) # 大小号
+
+ # 版别
+ version = Column(String(50), nullable=True) # 版别
+
+ # 交易信息
+ platform = Column(String(50), nullable=True) # 成交平台
+ seller = Column(String(100), nullable=True) # 出售者
+ buyer = Column(String(100), nullable=True) # 购买者
+
+ # 状态
+ status = Column(String(20), default="active")
+
+ # 统计
+ view_count = Column(Integer, default=0)
+ contact_count = Column(Integer, default=0)
+
+ # 时间
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
\ No newline at end of file
diff --git a/backend/app/models/models.py b/backend/app/models/models.py
index 40811e7..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")
@@ -163,7 +164,7 @@ class Information(Base):
# 内容描述
content = Column(Text, nullable=True)
-
+
# 关联藏品ID
collection_id = Column(String(36), ForeignKey("collections.f99_90_id", ondelete="SET NULL"), nullable=True)
@@ -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/models/seek_info.py b/backend/app/models/seek_info.py
new file mode 100644
index 0000000..eb3fd6c
--- /dev/null
+++ b/backend/app/models/seek_info.py
@@ -0,0 +1,39 @@
+from sqlalchemy import Column, String, Text, Float, Date, Boolean, Integer, DateTime
+from sqlalchemy.sql import func
+from app.core.database import Base
+import uuid
+
+def generate_uuid():
+ return str(uuid.uuid4())
+
+class SeekInfo(Base):
+ __tablename__ = "seek_info"
+
+ id = Column(String(36), primary_key=True, default=generate_uuid)
+ user_id = Column(String(36), nullable=False, index=True)
+
+ # 标题和内容
+ title = Column(String(255), nullable=False)
+ content = Column(Text, nullable=True)
+
+ # 期望条件(求购条件)
+ expect_category = Column(String(100), nullable=True) # 期望类别
+ expect_version = Column(String(100), nullable=True) # 期望版别
+ expect_packaging = Column(String(100), nullable=True) # 期望包装
+ expect_number = Column(String(50), nullable=True) # 期望号码
+ expect_price_min = Column(Float, nullable=True) # 期望最低价
+ expect_price_max = Column(Float, nullable=True) # 期望最高价
+
+ # 匹配状态
+ status = Column(String(20), default="active") # active/closed/expired
+ is_matched = Column(String(10), default="false") # 是否已匹配
+ matched_user_id = Column(String(36), nullable=True) # 匹配的用户ID
+ matched_contact = Column(String(100), nullable=True) # 匹配的联系方式
+
+ # 统计
+ view_count = Column(Integer, default=0)
+ contact_count = Column(Integer, default=0)
+
+ # 时间
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
\ No newline at end of file
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/deal.py b/backend/app/routers/deal.py
new file mode 100644
index 0000000..314f3ca
--- /dev/null
+++ b/backend/app/routers/deal.py
@@ -0,0 +1,248 @@
+from fastapi import APIRouter, Depends, Query, HTTPException
+from sqlalchemy.orm import Session
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime, date
+from app.core.database import get_db
+from app.core.auth import get_current_user
+from app.models.deal_info import DealInfo
+
+router = APIRouter(prefix="/api/deal", tags=["成交行情"])
+
+# ============ Schema ============
+class DealInfoCreate(BaseModel):
+ title: str
+ content: Optional[str] = None
+ deal_price: Optional[float] = None
+ deal_date: Optional[str] = None # YYYY-MM-DD
+ packaging: Optional[str] = None
+ category: Optional[str] = None
+ is_graded: Optional[bool] = False
+ grading_company: Optional[str] = None
+ grading_score: Optional[str] = None
+ tail_number: Optional[str] = None
+ size_type: Optional[str] = None
+ version: Optional[str] = None
+ platform: Optional[str] = None
+ seller: Optional[str] = None
+ buyer: Optional[str] = None
+
+class DealInfoUpdate(BaseModel):
+ title: Optional[str] = None
+ content: Optional[str] = None
+ deal_price: Optional[float] = None
+ deal_date: Optional[str] = None
+ packaging: Optional[str] = None
+ category: Optional[str] = None
+ is_graded: Optional[bool] = None
+ grading_company: Optional[str] = None
+ grading_score: Optional[str] = None
+ tail_number: Optional[str] = None
+ size_type: Optional[str] = None
+ version: Optional[str] = None
+ platform: Optional[str] = None
+ seller: Optional[str] = None
+ buyer: Optional[str] = None
+ status: Optional[str] = None
+
+class DealInfoResponse(BaseModel):
+ id: str
+ user_id: Optional[str]
+ title: str
+ content: Optional[str]
+ deal_price: Optional[float]
+ deal_date: Optional[date]
+ deal_no: Optional[str]
+ packaging: Optional[str]
+ category: Optional[str]
+ is_graded: Optional[bool]
+ grading_company: Optional[str]
+ grading_score: Optional[str]
+ tail_number: Optional[str]
+ size_type: Optional[str]
+ version: Optional[str]
+ platform: Optional[str]
+ seller: Optional[str]
+ buyer: Optional[str]
+ status: str
+ view_count: int
+ contact_count: int
+ created_at: Optional[datetime]
+ updated_at: Optional[datetime]
+
+ class Config:
+ from_attributes = True
+
+# 生成行情编号
+def generate_deal_no(db: Session):
+ """生成行情编号,从A000001开始递增"""
+ last = db.query(DealInfo).order_by(DealInfo.deal_no.desc()).first()
+ if last and last.deal_no:
+ # 例如 A000001 -> 2 -> A000002
+ num = int(last.deal_no[1:]) + 1
+ return f"A{num:06d}"
+ return "A000001"
+
+# ============ API ============
+@router.get("/list", response_model=list[DealInfoResponse])
+def get_deal_list(
+ status: str = Query("active"),
+ deal_date: Optional[str] = Query(None),
+ page: int = Query(1, ge=1),
+ page_size: int = Query(20, ge=1, le=1000),
+ user_only: bool = Query(False), # 是否只查看自己的
+ current_user: Optional = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取成交行情列表"""
+ query = db.query(DealInfo).filter(DealInfo.status == status)
+
+ # 我的行情:只查看自己的(管理员也只看自己的)
+ if user_only and current_user:
+ query = query.filter(DealInfo.user_id == current_user.f99_90_id)
+
+ # 成交日期过滤
+ if deal_date:
+ query = query.filter(DealInfo.deal_date == deal_date)
+
+ # 排序:优先成交日期倒序,同日按编号倒序
+ query = query.order_by(DealInfo.deal_date.desc().nullslast(), DealInfo.deal_no.desc().nullslast())
+
+ # 分页
+ offset = (page - 1) * page_size
+ items = query.offset(offset).limit(page_size).all()
+
+ return items
+
+@router.get("/stats")
+def get_deal_stats(
+ db: Session = Depends(get_db)
+):
+ """获取成交行情统计"""
+ total = db.query(DealInfo).filter(DealInfo.status == "active").count()
+
+ # 按日期统计
+ from sqlalchemy import func
+ date_stats = db.query(
+ DealInfo.deal_date,
+ func.count(DealInfo.id).label('count')
+ ).filter(
+ DealInfo.status == "active",
+ DealInfo.deal_date.isnot(None)
+ ).group_by(DealInfo.deal_date).order_by(DealInfo.deal_date.desc()).limit(10).all()
+
+ return {
+ "total": total,
+ "by_date": [{"date": str(d.deal_date), "count": d.count} for d in date_stats]
+ }
+
+@router.post("", response_model=DealInfoResponse)
+def create_deal(
+ data: DealInfoCreate,
+ current_user = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """创建成交行情"""
+ if not current_user:
+ raise HTTPException(status_code=401, detail="请先登录")
+
+ # 生成行情编号
+ deal_no = generate_deal_no(db)
+
+ # 解析日期
+ deal_date = None
+ if data.deal_date:
+ try:
+ deal_date = datetime.strptime(data.deal_date, "%Y-%m-%d").date()
+ except:
+ pass
+
+ deal = DealInfo(
+ user_id=current_user.f99_90_id if current_user else None,
+ title=data.title,
+ content=data.content,
+ deal_price=data.deal_price,
+ deal_date=deal_date,
+ deal_no=deal_no,
+ packaging=data.packaging,
+ category=data.category,
+ is_graded=data.is_graded or False,
+ grading_company=data.grading_company,
+ grading_score=data.grading_score,
+ tail_number=data.tail_number,
+ size_type=data.size_type,
+ version=data.version,
+ platform=data.platform,
+ seller=data.seller,
+ buyer=data.buyer,
+ status="active"
+ )
+ db.add(deal)
+ db.commit()
+ db.refresh(deal)
+ return deal
+
+@router.get("/{deal_id}", response_model=DealInfoResponse)
+def get_deal(
+ deal_id: str,
+ current_user = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取成交行情详情"""
+ deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first()
+ if not deal:
+ raise HTTPException(status_code=404, detail="成交行情不存在")
+
+ # 增加浏览数
+ deal.view_count += 1
+ db.commit()
+
+ return deal
+
+@router.put("/{deal_id}", response_model=DealInfoResponse)
+def update_deal(
+ deal_id: str,
+ data: DealInfoUpdate,
+ current_user = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """更新成交行情"""
+ if not current_user:
+ raise HTTPException(status_code=401, detail="请先登录")
+
+ deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first()
+ if not deal:
+ raise HTTPException(status_code=404, detail="成交行情不存在")
+
+ # 处理日期
+ if data.deal_date:
+ try:
+ data.deal_date = datetime.strptime(data.deal_date, "%Y-%m-%d").date()
+ except:
+ data.deal_date = None
+
+ for key, value in data.model_dump(exclude_unset=True).items():
+ setattr(deal, key, value)
+
+ db.commit()
+ db.refresh(deal)
+ return deal
+
+@router.delete("/{deal_id}")
+def delete_deal(
+ deal_id: str,
+ current_user = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """删除成交行情"""
+ if not current_user:
+ raise HTTPException(status_code=401, detail="请先登录")
+
+ deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first()
+ if not deal:
+ raise HTTPException(status_code=404, detail="成交行情不存在")
+
+ deal.status = "deleted"
+ db.commit()
+
+ return {"message": "删除成功"}
\ No newline at end of file
diff --git a/backend/app/routers/seek.py b/backend/app/routers/seek.py
new file mode 100644
index 0000000..76ff298
--- /dev/null
+++ b/backend/app/routers/seek.py
@@ -0,0 +1,189 @@
+from fastapi import APIRouter, Depends, Query, HTTPException
+from sqlalchemy.orm import Session
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+from app.core.database import get_db
+from app.core.auth import get_current_user
+from app.models.seek_info import SeekInfo
+
+router = APIRouter(prefix="/api/seek", tags=["寻配号"])
+
+# ============ Schema ============
+class SeekInfoCreate(BaseModel):
+ title: str
+ content: Optional[str] = None
+ expect_category: Optional[str] = None
+ expect_version: Optional[str] = None
+ expect_packaging: Optional[str] = None
+ expect_number: Optional[str] = None
+ expect_price_min: Optional[float] = None
+ expect_price_max: Optional[float] = None
+
+class SeekInfoUpdate(BaseModel):
+ title: Optional[str] = None
+ content: Optional[str] = None
+ expect_category: Optional[str] = None
+ expect_version: Optional[str] = None
+ expect_packaging: Optional[str] = None
+ expect_number: Optional[str] = None
+ expect_price_min: Optional[float] = None
+ expect_price_max: Optional[float] = None
+ status: Optional[str] = None
+
+class SeekInfoResponse(BaseModel):
+ id: str
+ user_id: str
+ title: str
+ content: Optional[str]
+ expect_category: Optional[str]
+ expect_version: Optional[str]
+ expect_packaging: Optional[str]
+ expect_number: Optional[str]
+ expect_price_min: Optional[float]
+ expect_price_max: Optional[float]
+ status: str
+ is_matched: Optional[str]
+ matched_user_id: Optional[str]
+ matched_contact: Optional[str]
+ view_count: int
+ contact_count: int
+ created_at: Optional[datetime]
+ updated_at: Optional[datetime]
+
+ class Config:
+ from_attributes = True
+
+# ============ API ============
+@router.get("/list", response_model=list[SeekInfoResponse])
+def get_seek_list(
+ status: str = Query("active"),
+ page: int = Query(1, ge=1),
+ page_size: int = Query(20, ge=1, le=1000),
+ user_only: bool = Query(False),
+ current_user: Optional = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取寻配号列表"""
+ query = db.query(SeekInfo).filter(SeekInfo.status == status)
+
+ # 我的寻配号:只查看自己的
+ if user_only and current_user:
+ query = query.filter(SeekInfo.user_id == current_user.f99_90_id)
+
+ # 排序
+ query = query.order_by(SeekInfo.created_at.desc())
+
+ # 分页
+ offset = (page - 1) * page_size
+ items = query.offset(offset).limit(page_size).all()
+
+ return items
+
+@router.get("/stats")
+def get_seek_stats(
+ current_user = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取寻配号统计"""
+ total = db.query(SeekInfo).filter(SeekInfo.status == "active").count()
+ matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count()
+
+ return {
+ "total": total,
+ "matched": matched,
+ "unmatched": total - matched
+ }
+
+@router.post("", response_model=SeekInfoResponse)
+def create_seek(
+ data: SeekInfoCreate,
+ current_user = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """创建寻配号"""
+ if not current_user:
+ raise HTTPException(status_code=401, detail="请先登录")
+
+ seek = SeekInfo(
+ user_id=current_user.f99_90_id,
+ title=data.title,
+ content=data.content,
+ expect_category=data.expect_category,
+ expect_version=data.expect_version,
+ expect_packaging=data.expect_packaging,
+ expect_number=data.expect_number,
+ expect_price_min=data.expect_price_min,
+ expect_price_max=data.expect_price_max,
+ status="active"
+ )
+ db.add(seek)
+ db.commit()
+ db.refresh(seek)
+ return seek
+
+@router.get("/{seek_id}", response_model=SeekInfoResponse)
+def get_seek(
+ seek_id: str,
+ current_user = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """获取寻配号详情"""
+ seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first()
+ if not seek:
+ raise HTTPException(status_code=404, detail="寻配号不存在")
+
+ # 增加浏览数
+ seek.view_count += 1
+ db.commit()
+
+ return seek
+
+@router.put("/{seek_id}", response_model=SeekInfoResponse)
+def update_seek(
+ seek_id: str,
+ data: SeekInfoUpdate,
+ current_user = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """更新寻配号"""
+ if not current_user:
+ raise HTTPException(status_code=401, detail="请先登录")
+
+ seek = db.query(SeekInfo).filter(
+ SeekInfo.id == seek_id,
+ SeekInfo.user_id == current_user.f99_90_id
+ ).first()
+
+ if not seek:
+ raise HTTPException(status_code=404, detail="寻配号不存在")
+
+ for key, value in data.model_dump(exclude_unset=True).items():
+ setattr(seek, key, value)
+
+ db.commit()
+ db.refresh(seek)
+ return seek
+
+@router.delete("/{seek_id}")
+def delete_seek(
+ seek_id: str,
+ current_user = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ """删除寻配号"""
+ if not current_user:
+ raise HTTPException(status_code=401, detail="请先登录")
+
+ seek = db.query(SeekInfo).filter(
+ SeekInfo.id == seek_id,
+ SeekInfo.user_id == current_user.f99_90_id
+ ).first()
+
+ if not seek:
+ raise HTTPException(status_code=404, detail="寻配号不存在")
+
+ seek.status = "deleted"
+ db.commit()
+
+ return {"message": "删除成功"}
\ No newline at end of file
diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py
index b0c9a00..c7e0fa2 100644
--- a/backend/app/routers/users.py
+++ b/backend/app/routers/users.py
@@ -2,6 +2,7 @@
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
from sqlalchemy.orm import Session
+from sqlalchemy import text
from app.core.database import get_db
from app.core.auth import get_current_user
from app.models.models import User, Collection
@@ -11,12 +12,14 @@ 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)
):
"""获取当前登录用户信息"""
return {
+ "id": current_user.f99_90_id,
+ "username": current_user.f01_01_name,
"f99_90_id": current_user.f99_90_id,
"f01_01_name": current_user.f01_01_name,
"email": current_user.email,
@@ -25,11 +28,27 @@ def get_current_user_info(
"address": current_user.address,
"bio": current_user.bio,
"role": current_user.role,
+ "level": current_user.f99_94_level,
+ "aiCount": current_user.f99_95_ai_count or 0,
+ "searchCount": current_user.f99_96_search_count or 0,
+ "collectionCount": current_user.f99_97_collection_count or 0,
+ "phoneVerified": current_user.f01_06_phone_verified or False,
+ "loginCount": current_user.f99_98_login_count or 0,
+ "lastLogin": current_user.f99_99_last_login.isoformat() if current_user.f99_99_last_login else None,
+ "gender": current_user.f01_07_gender,
+ "birthday": current_user.f01_08_birthday.isoformat() if current_user.f01_08_birthday else None,
+ "region": current_user.f01_09_region,
+ "realnameVerified": current_user.f01_10_realname_verified or False,
+ "points": current_user.f99_100_points or 0,
+ "balance": float(current_user.f01_11_balance) if current_user.f01_11_balance else 0,
+ "totalAmount": float(current_user.f01_12_total_amount) if current_user.f01_12_total_amount else 0,
+ "inviteCode": current_user.f01_13_invite_code,
+ "user_code": current_user.user_code,
"f99_92_created_at": current_user.f99_92_created_at.isoformat() if current_user.f99_92_created_at else None,
"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),
@@ -96,6 +115,11 @@ def get_users(
for u in users:
# 统计每个用户的藏品数量
count = db.query(Collection).filter(Collection.f99_91_user_id == u.f99_90_id).count()
+ # 统计每个用户的行情信息数量
+ info_count = db.execute(text(
+ "SELECT COUNT(*) FROM information WHERE user_id = :user_id"
+ ), {"user_id": str(u.f99_90_id)}).fetchone()[0]
+
user_list.append({
"id": u.f99_90_id,
"username": u.f01_01_name,
@@ -105,6 +129,7 @@ def get_users(
"user_code": u.user_code,
"created_at": u.f99_92_created_at.isoformat() if u.f99_92_created_at else None,
"collectionCount": count,
+ "infoCount": info_count,
"level": u.f99_94_level,
"aiCount": u.f99_95_ai_count,
"searchCount": u.f99_96_search_count,
diff --git a/backend/app/routers/yichens.py b/backend/app/routers/yichens.py
new file mode 100644
index 0000000..de8e152
--- /dev/null
+++ b/backend/app/routers/yichens.py
@@ -0,0 +1,377 @@
+from fastapi import APIRouter, Depends, Query
+from sqlalchemy import func, text
+from sqlalchemy.orm import Session
+from pydantic import BaseModel
+from typing import Optional, List
+from datetime import datetime, date
+from app.core.coolbot_db import get_coolbot_db
+
+router = APIRouter(prefix="/api/yichens", tags=["一尘看板"])
+
+# ============ 数据模型 ============
+class YichensPostStats(BaseModel):
+ total_posts: int
+ total_deals: int # 出售
+ total_wants: int # 求购
+ total_replies: int
+ total_views: int
+ avg_price: Optional[float]
+
+class CategoryStat(BaseModel):
+ category: str
+ count: int
+
+class PostItem(BaseModel):
+ post_id: str
+ title: str
+ category: Optional[str]
+ post_type: str
+ price: Optional[float]
+ author_username: str
+ post_time: str
+ reply_count: int
+ view_count: int
+ url: Optional[str]
+ content: Optional[str]
+
+class UserStat(BaseModel):
+ total_users: int
+ new_users_today: int
+ sellers: int
+
+class UserItem(BaseModel):
+ user_id: str
+ username: str
+ avatar_url: Optional[str]
+ content: Optional[str]
+ credit_level: Optional[str]
+ credit_score: Optional[int]
+ post_count: int
+ is_seller: bool
+ registration_date: Optional[str]
+
+# ============ 统计接口 ============
+@router.get("/stats/posts", response_model=YichensPostStats)
+def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
+ """获取帖子统计"""
+ result = db.execute(text("""
+ SELECT
+ COUNT(*) as total_posts,
+ COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals,
+ COUNT(*) FILTER (WHERE post_type = 'want') as total_wants,
+ COALESCE(SUM(reply_count), 0) as total_replies,
+ COALESCE(SUM(view_count), 0) as total_views,
+ AVG(price) as avg_price
+ FROM yichens_posts
+ WHERE post_time >= NOW() - INTERVAL '1 day' * :days
+ """), {"days": days}).fetchone()
+
+ return YichensPostStats(
+ total_posts=result[0] or 0,
+ total_deals=result[1] or 0,
+ total_wants=result[2] or 0,
+ total_replies=result[3] or 0,
+ total_views=result[4] or 0,
+ avg_price=float(result[5]) if result[5] else None
+ )
+
+@router.get("/stats/categories", response_model=List[CategoryStat])
+def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
+ """按分类统计帖子数量"""
+ results = db.execute(text("""
+ SELECT category, COUNT(*) as count
+ FROM yichens_posts
+ WHERE post_time >= NOW() - INTERVAL '1 day' * :days
+ GROUP BY category
+ ORDER BY count DESC
+ """), {"days": days}).fetchall()
+
+ return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results]
+
+@router.get("/stats/users", response_model=UserStat)
+def get_user_stats(db: Session = Depends(get_coolbot_db)):
+ """获取用户统计"""
+ result = db.execute(text("""
+ SELECT
+ COUNT(*) as total_users,
+ COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today,
+ COUNT(*) FILTER (WHERE is_seller = true) as sellers
+ FROM yichens_users
+ """)).fetchone()
+
+ return UserStat(
+ total_users=result[0] or 0,
+ new_users_today=result[1] or 0,
+ sellers=result[2] or 0
+ )
+
+@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)
+):
+ """获取帖子列表 - 支持全局搜索,返回总数和分页信息"""
+ # 构建WHERE条件
+ where_clauses = ["1=1"]
+ params = {"limit": limit, "offset": offset}
+
+ if category:
+ where_clauses.append("category = :category")
+ params["category"] = category
+
+ if post_type:
+ where_clauses.append("post_type = :post_type")
+ params["post_type"] = post_type
+
+ # 全局搜索
+ 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}%"
+
+ where_sql = " AND ".join(where_clauses)
+
+ # 查询总数
+ 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 "",
+ category=r[3],
+ post_type=r[4] or "",
+ price=float(r[5]) if r[5] else None,
+ author_username=r[6] or "",
+ post_time=str(r[7]) if r[7] else "",
+ reply_count=r[8] or 0,
+ 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(
+ limit: int = Query(20, ge=1, le=500),
+ offset: int = Query(0, ge=0),
+ is_seller: Optional[bool] = None,
+ db: Session = Depends(get_coolbot_db)
+):
+ """获取用户列表"""
+ query = """
+ SELECT user_id, username, avatar_url, credit_level, credit_score,
+ post_count, is_seller, registration_date
+ FROM yichens_users
+ WHERE 1=1
+ """
+ params = {"limit": limit, "offset": offset}
+
+ if is_seller is not None:
+ query += " AND is_seller = :is_seller"
+ params["is_seller"] = is_seller
+
+ query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset"
+
+ results = db.execute(text(query), params).fetchall()
+
+ return [UserItem(
+ user_id=r[0],
+ username=r[1] or "",
+ avatar_url=r[2],
+ credit_level=r[3],
+ credit_score=r[4],
+ post_count=r[5] or 0,
+ is_seller=r[6] or False,
+ registration_date=str(r[7]) if r[7] else None
+ ) for r in results]
+
+
+@router.get("/stats/today")
+async def get_today_stats(db: Session = Depends(get_coolbot_db)):
+ """获取今日新增帖子统计"""
+ query = """
+ 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,
+ SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
+ SUM(CASE WHEN category LIKE '%龙%' THEN 1 ELSE 0 END) as dragons,
+ SUM(CASE WHEN category LIKE '%马%' THEN 1 ELSE 0 END) as horses,
+ SUM(CASE WHEN category LIKE '%蛇%' THEN 1 ELSE 0 END) as snakes,
+ SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma
+ FROM yichens_posts
+ WHERE post_time >= CURRENT_DATE
+ """
+ result = db.execute(text(query)).fetchone()
+ return {
+ "total": result[0] or 0,
+ "deals": result[1] or 0,
+ "wants": result[2] or 0,
+ "others": result[3] or 0,
+ "dragons": result[4] or 0,
+ "horses": result[5] or 0,
+ "snakes": result[6] or 0,
+ "tianma": result[7] or 0
+ }
+
+@router.get("/stats/hour")
+async def get_hour_stats(db: Session = Depends(get_coolbot_db)):
+ """获取近一个小时新增帖子统计"""
+ query = """
+ 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,
+ SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
+ SUM(CASE WHEN category LIKE '%龙%' THEN 1 ELSE 0 END) as dragons,
+ SUM(CASE WHEN category LIKE '%马%' THEN 1 ELSE 0 END) as horses,
+ SUM(CASE WHEN category LIKE '%蛇%' THEN 1 ELSE 0 END) as snakes
+ FROM yichens_posts
+ WHERE post_time >= NOW() - INTERVAL '1 hour'
+ """
+ result = db.execute(text(query)).fetchone()
+ return {
+ "total": result[0] or 0,
+ "deals": result[1] or 0,
+ "wants": result[2] or 0,
+ "others": result[3] or 0,
+ "dragons": result[3] or 0,
+ "horses": result[4] or 0,
+ "snakes": result[5] or 0
+ }
+
+@router.get("/stats/today-category")
+async def get_today_category_stats(db: Session = Depends(get_coolbot_db)):
+ """获取今日帖子分类统计"""
+ query = """
+ SELECT category, COUNT(*) as count
+ FROM yichens_posts
+ WHERE post_time >= CURRENT_DATE
+ GROUP BY category
+ ORDER BY count DESC
+ """
+ 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 86aee77..dd91a8b 100644
--- a/config/VERSION
+++ b/config/VERSION
@@ -1 +1 @@
-VERSION=1.2.30
+1.2.98
diff --git a/env.conf b/env.conf
new file mode 100644
index 0000000..3f35852
--- /dev/null
+++ b/env.conf
@@ -0,0 +1,8 @@
+DATABASE_URL=postgresql://jiachenlong:Passwd1%403@pgm-bp1t5w248t7s1pvr.pg.rds.aliyuncs.com:5432/jiachenlong
+SECRET_KEY=production-secret-key-b-env
+OSS_ACCESS_KEY_ID=LTAI5t6HUnpFBLEK9194kPVG
+OSS_ACCESS_KEY_SECRET=LEr4Q8yRxb8D5b24cKfCwlt4MMoke1
+SMS_ACCESS_KEY_ID=LTAI5tQAx5niD7JQVqGE5acE
+SMS_ACCESS_KEY_SECRET=QsQFAEKBkaNynIoKyvdIi3BUyWVZu1
+SMS_SIGN_NAME=苏州算力
+SMS_TEMPLATE_CODE=SMS_501590956
diff --git a/frontend/VERSION b/frontend/VERSION
new file mode 100644
index 0000000..dd91a8b
--- /dev/null
+++ b/frontend/VERSION
@@ -0,0 +1 @@
+1.2.98
diff --git a/frontend/index.html b/frontend/index.html
index 1c7f88f..7335504 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
-
甲辰收藏 v1.2.30
+ 甲辰收藏 v=1.2.98
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 156d83c..f22a1fc 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.82",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jiachenlong-frontend",
- "version": "1.2.12",
+ "version": "1.2.82",
"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..1826e55 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "jiachenlong-frontend",
- "version": "1.2.12",
+ "version": "1.2.82",
"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..09a9208 100644
--- a/frontend/src/pages/Add.jsx
+++ b/frontend/src/pages/Add.jsx
@@ -1,7 +1,8 @@
-// 添加藏品页面 - 支持 AI 识别/手工录入/批量录入
+// 添加藏品页面 - 支持 AI 识别/手工录入
import React, { useState, useRef } from 'react'
import { APP_VERSION } from '../config/version'
-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:'其他'}];
+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:'带7号',label:'带7号'},{value:'带4号',label:'带4号'},{value:'其他',label:'其他'}];
// 字段转换函数
const convertField = (obj) => {
@@ -65,18 +66,63 @@ const getDefaultForm = () => ({
name: '藏品', code: '', category: '自持', rarity: '通货', prefixSerial: '',
version: '2024 龙', denomination: '', status: 'in_collection', packaging: '单张',
material: '塑料钞', issueQuantity: '1 亿', purpose: '收藏', isGraded: false,
- gradingCompany: '', gradingScore: '', threeStar: false, specialMark: '',
+ gradingCompany: '爱藏', gradingScore: '67+', threeStar: false, specialMark: '',
serialFeature: '', numberCategory: '', issuer: '中国人民银行', issueYear: '2024',
costPrice: '', targetPrice: '', goalPrice: '', repairFee: '', gradingFee: '', remark: ''
})
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: '67+'
+ })
+ 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 '带7号'
+ return '带4号'
+ }
+
// 根据 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('')
@@ -158,8 +204,8 @@ export default function Add() {
else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347'
else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47'
- else if (digits.includes('7') && !digits.includes('4')) cat = '无4'
- else if (digits.includes('4')) cat = '带4'
+ else if (digits.includes('7') && !digits.includes('4')) cat = '带7号'
+ else if (digits.includes('4')) cat = '带4号'
else cat = '其他'
}
setForm(prev => ({ ...prev, numberCategory: cat }))
@@ -274,8 +320,8 @@ export default function Add() {
else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347'
else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
else if (!digits.includes('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47'
- else if (digits.includes('7') && !digits.includes('4')) cat = '无4'
- else if (digits.includes('4')) cat = '带4'
+ else if (digits.includes('7') && !digits.includes('4')) cat = '带7号'
+ else if (digits.includes('4')) cat = '带4号'
else cat = '其他'
formWithCategory.numberCategory = cat
}
@@ -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,380 @@ 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')
+ // 冠字号矫正:用户输入的数字位数不同,生成规则不同
+ // 7位 → J0 + 0 + 数字 (如 J00123456)
+ // 8位 → J0 + 数字 (如 J01234567)
+ // 9位 → J0 + 取后8位数字 (如 J02345678)
+ const rawSerial = dealForm.serial.replace('J', '').replace(/\D/g, '')
+ let normalizedSerial = ''
+ if (rawSerial.length === 7) {
+ normalizedSerial = 'J0' + '0' + rawSerial
+ } else if (rawSerial.length === 8) {
+ normalizedSerial = 'J0' + rawSerial
+ } else if (rawSerial.length >= 9) {
+ normalizedSerial = 'J0' + rawSerial.slice(1, 9) // 取后8位
+ } else {
+ normalizedSerial = 'J0' + rawSerial
+ }
+
+ const tailNumber = rawSerial.slice(-2)
+ const sizeType = (dealForm.packaging === '标十' && ['01','11','21','31','41','51','61','71','81','91'].includes(tailNumber)) ? '小号' :
+ (dealForm.packaging === '标百' && ['001','011','021','031','041','051','061','071','081','091'].includes(rawSerial.slice(-3))) ? '小号' : '大号'
+
+ const response = await fetch(`${API_BASE}/api/deal`, {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ title: `${normalizedSerial}-¥${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,
+ tail_number: tailNumber,
+ size_type: sizeType,
+ platform: dealForm.platform,
+ seller: dealForm.seller || '',
+ buyer: dealForm.buyer || ''
+ })
+ })
+ 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..559b13e 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,
@@ -219,8 +221,16 @@ export default function Admin() {
-
藏品数
-
window.location.hash = '#/list?userId=' + user.id} title='点击查看'>{user.collectionCount || 0}
+
+
+
藏品
+
window.location.hash = '#/list?userId=' + user.id} title='点击查看藏品'>{user.collectionCount || 0}
+
+
+
行情
+
window.location.hash = '#/news?userId=' + user.id} title='点击查看行情'>{user.infoCount || 0}
+
+
{/* 更多字段 */}
@@ -253,7 +263,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 +401,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' }}
+ />
+
+
+
+
+
+
@@ -534,18 +569,6 @@ export default function Admin() {
/>
-
-
-
-
diff --git a/frontend/src/pages/AdminV2.jsx b/frontend/src/pages/AdminV2.jsx
deleted file mode 100644
index 940ff94..0000000
--- a/frontend/src/pages/AdminV2.jsx
+++ /dev/null
@@ -1,272 +0,0 @@
-import React, { useState, useEffect } from 'react'
-
-// 管理后台V2
-export default function AdminV2() {
- const [activeTab, setActiveTab] = useState('users')
- const token = localStorage.getItem('token')
- const API_BASE = localStorage.getItem('API_BASE') || ''
-
- const userStr = localStorage.getItem('user')
- let user = null
- try { user = userStr ? JSON.parse(userStr) : null } catch (e) {}
-
- if (!user || user.role !== 'admin') {
- return (
-
-
-
🚫
-
无权访问
-
仅管理员可以访问管理后台
-
-
- )
- }
-
- return (
-
-
-
⚙️ 管理后台
-
-
-
- {[
- { key: 'users', label: '👥 用户管理' },
- { key: 'stats', label: '📊 统计分析' },
- { key: 'publish', label: '📢 信息发布' },
- { key: 'infoManage', label: '📋 信息管理' }
- ].map(tab => (
-
setActiveTab(tab.key)} style={{ flex: '1 1 45%', textAlign: 'center', padding: '10px 8px', margin: '2px', borderRadius: '6px', cursor: 'pointer', background: activeTab === tab.key ? '#3b82f6' : 'transparent', color: activeTab === tab.key ? '#fff' : '#94a3b8', fontSize: '13px', fontWeight: activeTab === tab.key ? 'bold' : 'normal' }}>
- {tab.label}
-
- ))}
-
-
- {activeTab === 'users' &&
}
- {activeTab === 'stats' &&
}
- {activeTab === 'publish' &&
}
- {activeTab === 'infoManage' &&
}
-
- )
-}
-
-function UserManagement({ token, API_BASE }) {
- const [users, setUsers] = useState([])
- const [loading, setLoading] = useState(true)
- const [search, setSearch] = useState('')
- const [editingUser, setEditingUser] = useState(null)
- const [showAddModal, setShowAddModal] = useState(false)
- const [newUser, setNewUser] = useState({ username: '', password: '', phone: '', role: 'user' })
-
- useEffect(() => { fetchUsers() }, [])
-
- const fetchUsers = async () => {
- setLoading(true)
- try {
- const res = await fetch(`${API_BASE}/api/admin/users?page=1&limit=100`, { headers: { 'Authorization': `Bearer ${token}` } })
- const data = await res.json()
- setUsers(Array.isArray(data) ? data : [])
- } catch (e) { console.error(e) }
- setLoading(false)
- }
-
- const handleAddUser = async () => {
- if (!newUser.username || !newUser.password) { alert('请填写用户名和密码'); return }
- try {
- const res = await fetch(`${API_BASE}/api/admin/users`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(newUser) })
- if (res.ok) { alert('添加成功'); setShowAddModal(false); setNewUser({ username: '', password: '', phone: '', role: 'user' }); fetchUsers() }
- } catch (e) { alert('添加失败') }
- }
-
- const handleUpdateUser = async () => {
- try {
- const res = await fetch(`${API_BASE}/api/admin/users/${editingUser.id}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(editingUser) })
- if (res.ok) { alert('更新成功'); setEditingUser(null); fetchUsers() }
- } catch (e) { alert('更新失败') }
- }
-
- const handleDeleteUser = async (id) => {
- if (!confirm('确定删除该用户?')) return
- try { await fetch(`${API_BASE}/api/admin/users/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); fetchUsers() } catch (e) { console.error(e) }
- }
-
- const filteredUsers = users.filter(u => u.username?.includes(search) || u.phone?.includes(search) || u.user_code?.includes(search))
-
- return (
-
-
- setSearch(e.target.value)} style={{ flex: 1, padding: '10px', background: '#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff' }} />
- setShowAddModal(true)} style={{ padding: '10px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>+ 添加
-
-
- {loading ?
加载中...
: (
-
- {filteredUsers.map(u => (
-
-
-
-
{u.username?.[0] || '?'}
-
{u.username}
{u.user_code || '-'}
-
-
{u.role === 'admin' ? '管理员' : '用户'}
-
-
-
📱 {u.phone || '-'}
🏷️ 等级: {u.level || '青铜'} | 藏品: {u.collectionCount || 0}
-
🔑 登录: {u.loginCount || 0}次 | AI: {u.aiCount || 0}次
-
-
- setEditingUser(u)} style={{ flex: 1, padding: '6px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>编辑
- handleDeleteUser(u.id)} style={{ flex: 1, padding: '6px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>删除
-
-
- ))}
-
- )}
-
- {showAddModal && (
-
-
-
添加用户
-
setNewUser({...newUser, username: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
-
setNewUser({...newUser, password: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
-
setNewUser({...newUser, phone: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '12px', boxSizing: 'border-box' }} />
-
-
- 添加
- setShowAddModal(false)} style={{ flex: 1, padding: '10px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>取消
-
-
-
- )}
-
- {editingUser && (
-
-
-
编辑用户
-
setEditingUser({...editingUser, phone: e.target.value})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
-
-
setEditingUser({...editingUser, points: parseInt(e.target.value)})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '8px', boxSizing: 'border-box' }} />
-
setEditingUser({...editingUser, balance: parseFloat(e.target.value)})} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginBottom: '12px', boxSizing: 'border-box' }} />
-
- 保存
- setEditingUser(null)} style={{ flex: 1, padding: '10px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer' }}>取消
-
-
-
- )}
-
- )
-}
-
-function Statistics({ token, API_BASE }) {
- const [stats, setStats] = useState(null)
- const [loading, setLoading] = useState(true)
-
- useEffect(() => { fetchStats() }, [])
-
- const fetchStats = async () => {
- setLoading(true)
- try {
- const collectionsRes = await fetch(`${API_BASE}/api/collections/stats`, { headers: { 'Authorization': `Bearer ${token}` } })
- const collectionsData = await collectionsRes.json()
- const usersRes = await fetch(`${API_BASE}/api/admin/users?page=1&limit=100`, { headers: { 'Authorization': `Bearer ${token}` } })
- const usersData = await usersRes.json()
- const totalUsers = Array.isArray(usersData) ? usersData.length : 0
- const totalCollections = collectionsData.total || 0
- const levelCount = {}
- if (Array.isArray(usersData)) { usersData.forEach(u => { const level = u.level || '青铜'; levelCount[level] = (levelCount[level] || 0) + 1 }) }
- const statusCount = { in_collection: collectionsData.byStatus?.find(s => s.status === 'in_collection')?.count || 0, sold: collectionsData.byStatus?.find(s => s.status === 'sold')?.count || 0 }
- const totalBalance = Array.isArray(usersData) ? usersData.reduce((sum, u) => sum + (u.balance || 0), 0) : 0
- setStats({ users: { total: totalUsers, levels: levelCount }, collections: { total: totalCollections, status: statusCount }, balance: { total: totalBalance } })
- } catch (e) { console.error(e) }
- setLoading(false)
- }
-
- if (loading) return
加载中...
-
- return (
-
-
-
{stats?.users?.total || 0}
👥 用户总数
-
{stats?.collections?.total || 0}
📚 藏品总数
-
¥{stats?.balance?.total?.toFixed(2) || '0'}
💰 账户总余额
-
{stats?.collections?.status?.in_collection || 0}
✨ 收藏中
-
-
🏆 会员等级分布
{Object.entries(stats?.users?.levels || {}).map(([level, count]) => (
{level}{count}人
))}
-
📦 藏品状态
{stats?.collections?.status?.in_collection || 0}
收藏中
{stats?.collections?.status?.sold || 0}
已售出
-
- )
-}
-
-function InfoPublish({ token, API_BASE }) {
- const [publishType, setPublishType] = useState('system')
- const [formData, setFormData] = useState({ title: '', content: '', expect_version: '', expect_packaging: '', deal_price: '' })
- const [submitting, setSubmitting] = useState(false)
-
- const handlePublish = async () => {
- if (!formData.title) { alert('请输入标题'); return }
- setSubmitting(true)
- try {
- const data = { title: formData.title, content: formData.content, info_type: publishType === 'system' ? 'publish' : 'deal', deal_price: publishType === 'deal' ? parseFloat(formData.deal_price) : null, expect_version: formData.expect_version, expect_packaging: formData.expect_packaging }
- const res = await fetch(`${API_BASE}/api/information/`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
- if (res.ok) { alert('发布成功!'); setFormData({ title: '', content: '', expect_version: '', expect_packaging: '', deal_price: '' }) }
- } catch (e) { alert('发布失败') }
- setSubmitting(false)
- }
-
- return (
-
-
- setPublishType('system')} style={{ flex: 1, padding: '12px', background: publishType === 'system' ? '#3b82f6' : '#1e293b', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>📢 系统通知
- setPublishType('deal')} style={{ flex: 1, padding: '12px', background: publishType === 'deal' ? '#10b981' : '#1e293b', color: '#fff', border: 'none', borderRadius: '8px', cursor: 'pointer' }}>📈 成交数据
-
-
-
{publishType === 'system' ? '📢 发布系统通知' : '📈 发布成交数据'}
-
setFormData({...formData, title: e.target.value})} placeholder={publishType === 'system' ? '请输入通知标题' : '如:2024版纪念钞成交'} style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginTop: '4px', boxSizing: 'border-box' }} />
-
- {publishType === 'deal' && (
setFormData({...formData, deal_price: e.target.value})} placeholder="成交金额" style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginTop: '4px', boxSizing: 'border-box' }} />
setFormData({...formData, expect_version: e.target.value})} placeholder="如:2024版" style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginTop: '4px', boxSizing: 'border-box' }} />
setFormData({...formData, expect_packaging: e.target.value})} placeholder="如:标十" style={{ width: '100%', padding: '10px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', marginTop: '4px', boxSizing: 'border-box' }} />
)}
-
{submitting ? '发布中...' : '发布'}
-
-
- )
-}
-
-function InfoManage({ token, API_BASE }) {
- const [infoList, setInfoList] = useState([])
- const [loading, setLoading] = useState(true)
- const [filterType, setFilterType] = useState('all')
-
- useEffect(() => { fetchInfoList() }, [])
-
- const fetchInfoList = async () => {
- setLoading(true)
- try { const res = await fetch(`${API_BASE}/api/information/list?status=active`, { headers: { 'Authorization': `Bearer ${token}` } }); const data = await res.json(); setInfoList(data || []) } catch (e) { console.error(e) }
- setLoading(false)
- }
-
- const handleDelete = async (id) => { if (!confirm('确定删除该信息?')) return; try { await fetch(`${API_BASE}/api/information/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); fetchInfoList() } catch (e) { console.error(e) } }
-
- const handleClose = async (id) => { try { await fetch(`${API_BASE}/api/information/${id}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ status: 'closed' }) }); fetchInfoList() } catch (e) { console.error(e) } }
-
- const filteredList = filterType === 'all' ? infoList : infoList.filter(i => i.info_type === filterType)
- const typeLabels = { seek: '🔍 寻配号', deal: '📈 成交', publish: '📝 发布' }
-
- return (
-
-
{['all', 'seek', 'deal', 'publish'].map(type => ( setFilterType(type)} style={{ padding: '8px 12px', background: filterType === type ? '#3b82f6' : '#1e293b', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', whiteSpace: 'nowrap', fontSize: '13px' }}>{type === 'all' ? '全部' : typeLabels[type]}))}
- {loading ?
加载中...
: filteredList.length === 0 ?
暂无信息
: (
- filteredList.map(item => (
-
-
- {typeLabels[item.info_type]}
- {new Date(item.created_at).toLocaleDateString('zh-CN')}
-
-
{item.title}
- {item.content &&
{item.content}
}
-
👤 {item.user_name} | 👁 {item.view_count} | 📞 {item.contact_count}
-
handleClose(item.id)} style={{ flex: 1, padding: '6px', background: '#f59e0b', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>关闭 handleDelete(item.id)} style={{ flex: 1, padding: '6px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px' }}>删除
-
- ))
- )}
-
- )
-}
diff --git a/frontend/src/pages/BatchMode.jsx b/frontend/src/pages/BatchMode.jsx
deleted file mode 100644
index a8fcef7..0000000
--- a/frontend/src/pages/BatchMode.jsx
+++ /dev/null
@@ -1,513 +0,0 @@
-import React, { useState, useRef } from 'react'
-
-// 字段名转换函数:驼峰转下划线
-const convertField = (obj) => {
- const map = {
- prefixSerial: 'prefix_serial',
- isGraded: 'is_graded',
- gradingCompany: 'grading_company',
- gradingScore: 'grading_score',
- threeStar: 'three_star',
- specialMark: 'special_mark',
- serialFeature: 'serial_feature',
- issueYear: 'issue_year',
- issueQuantity: 'issue_quantity',
- costPrice: 'cost_price',
- targetPrice: 'target_price',
- goalPrice: 'goal_price',
- repairFee: 'repair_fee',
- gradingFee: 'grading_fee'
- }
- const result = {}
- for (const key in obj) {
- result[map[key] || key] = obj[key]
- }
- return result
-}
-
-export default function BatchMode() {
- const [images, setImages] = useState([])
- const [currentIndex, setCurrentIndex] = useState(0)
- const [result, setResult] = useState(null)
- const [loading, setLoading] = useState(false)
- const [savedCount, setSavedCount] = useState(0)
- const fileInputRef = useRef(null)
-
- const statusOptions = [
- { value: 'in_collection', label: '收藏中' },
- { value: 'selling', label: '在售' },
- { value: 'sold', label: '已售' },
- { value: 'grading', label: '送评' },
- { value: 'transit', label: '在途' },
- { value: 'other', label: '其他' }
- ]
-
- const packagingOptions = [
- { value: '单张', label: '单张' },
- { value: '标十', label: '标十' },
- { value: '标百', label: '标百' },
- { value: '裸钞', label: '裸钞' }
- ]
-
- const categoryOptions = [
- { value: '自持', label: '自持' },
- { value: '寄存', label: '寄存' },
- { value: '寄售', label: '寄售' },
- { value: '共有', label: '共有' },
- { value: '寻号', label: '寻号' },
- { value: '其他', label: '其他' }
- ]
-
- const rarityOptions = [
- { value: '通货', label: '通货' },
- { value: '特色', label: '特色' },
- { value: '少见', label: '少见' },
- { value: '稀有', label: '稀有' },
- { value: '珍品', label: '珍品' },
- { value: '孤品', label: '孤品' }
- ]
-
- const handleChooseImages = (e) => {
- const files = Array.from(e.target.files || [])
- if (files.length > 0) {
- setImages(files.slice(0, 10))
- setCurrentIndex(0)
- setResult(null)
- setSavedCount(0)
- // Auto OCR first image
- processOCR(files[0])
- }
- }
-
- const processOCR = async (file) => {
- setLoading(true)
- try {
- const formData = new FormData()
- formData.append('image', file)
-
- const res = await fetch('/api/ocr/recognize', { method: 'POST', body: formData })
- const data = await res.json()
- const text = data.result || ''
-
- const parsed = {
- name: '生肖纪念钞',
- ownership_type: '自持',
- rarity: '通货',
- status: 'in_collection',
- version: '',
- prefixSerial: '',
- packaging: '单张',
- isGraded: false,
- gradingCompany: '',
- gradingScore: '',
- threeStar: false,
- specialMark: '',
- serialFeature: '',
- denomination: '贰拾圆',
- issuer: '中国人民银行',
- issueYear: '2024',
- material: '塑料',
- issueQuantity: '一亿',
- costPrice: '',
- goalPrice: '',
- targetPrice: '',
- remark: ''
- }
-
- const v = text.match(/发行版别[::]\s*([^\n]+)/)
- if (v) {
- parsed.version = v[1].trim()
- const yearMatch = v[1].match(/20\d{2}/)
- if (yearMatch) parsed.issueYear = yearMatch[0]
- }
- // 自动识别发行机构
- const issuerMatch = text.match(/发行机构[::]\s*([^\n]+)/)
- if (issuerMatch) parsed.issuer = issuerMatch[1].trim()
- if (text.includes('三星') || text.includes('3星') || text.includes('★★★')) parsed.threeStar = true
- const d = text.match(/面额[::]\s*([^\n]+)/)
- if (d) parsed.denomination = d[1].trim()
- const p = text.match(/冠字序号[::]\s*([^\n]+)/)
- if (p) parsed.prefixSerial = p[1].trim()
- // 识别编号
- const codeMatch = text.match(/编号[::]\s*([^\n]+)/)
- if (codeMatch) parsed.code = codeMatch[1].trim()
- const pk = text.match(/封装类型[::]\s*([^\n]+)/)
- if (pk) {
- const pv = pk[1].trim()
- if (pv.includes('百')) parsed.packaging = '标百'
- else if (pv.includes('十')) parsed.packaging = '标十'
- else if (pv.includes('单')) parsed.packaging = '单张'
- else parsed.packaging = '裸钞'
- }
- const g = text.match(/是否评级[::]\s*([^\n]+)/)
- if (g) parsed.isGraded = g[1].trim() === '是'
- const gc = text.match(/评级机构[::]\s*([^\n]+)/)
- if (gc) parsed.gradingCompany = gc[1].trim()
- const gs = text.match(/评级分数[::]\s*([^\n]+)/)
- if (gs) parsed.gradingScore = gs[1].trim()
- const sm = text.match(/特殊标识[::]\s*([^\n]+)/)
- if (sm) parsed.specialMark = sm[1].trim()
- const sf = text.match(/号码特征[::]\s*([^\n]+)/)
- if (sf) parsed.serialFeature = sf[1].trim()
-
- setResult(parsed)
- } catch (e) {
- console.error(e)
- alert('识别失败,请重试')
- }
- setLoading(false)
- }
-
- const updateResult = (field, value) => {
- setResult(prev => ({ ...prev, [field]: value }))
- }
-
- const handleSave = async (force = false) => {
- const token = localStorage.getItem('token')
- if (!token) {
- alert('请先登录')
- return
- }
-
- try {
- const saveData = convertField({ ...result, _forceSave: force })
- const res = await fetch('/api/collections', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
- body: JSON.stringify(saveData)
- })
-
- if (res.ok) {
- setSavedCount(prev => prev + 1)
- alert('保存成功!')
- } else {
- const data = await res.json()
- const errMsg = data.error || data.message || data.detail || '保存失败'
- // 检查重复
- if (errMsg.includes('E002') || errMsg.includes('已存在') || errMsg.includes('禁止重复') || errMsg.includes('重复')) {
- const confirmed = confirm('发现重复:冠字号 ' + (result.prefixSerial || '') + ' 已存在,是否继续保存?')
- if (confirmed) {
- await handleSave(true) // 强制保存
- return
- }
- }
- alert(errMsg)
- }
- } catch (e) {
- alert('保存失败: ' + e.message)
- }
- }
-
- const handleNext = () => {
- if (currentIndex < images.length - 1) {
- setCurrentIndex(currentIndex + 1)
- setResult(null)
- processOCR(images[currentIndex + 1])
- }
- }
-
- const handlePrev = () => {
- if (currentIndex > 0) {
- setCurrentIndex(currentIndex - 1)
- setResult(null)
- processOCR(images[currentIndex - 1])
- }
- }
-
- const handleReOCR = () => {
- processOCR(images[currentIndex])
- }
-
- const handleReUpload = () => {
- setImages([])
- setCurrentIndex(0)
- setResult(null)
- setSavedCount(0)
- fileInputRef.current?.click()
- }
-
- // No images selected
- if (images.length === 0) {
- return (
-
-
fileInputRef.current?.click()}
- style={{
- minHeight: '50vh',
- background: 'rgba(255,255,255,0.03)',
- border: '2px dashed rgba(255,255,255,0.15)',
- borderRadius: '16px',
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- justifyContent: 'center',
- cursor: 'pointer'
- }}
- >
-
📦
-
点击选择多张图片
-
最多10张
-
-
-
- )
- }
-
- const isSaved = savedCount > currentIndex
-
- return (
-
- {/* Sticky Header */}
-
-
- 已保存: {savedCount} / {images.length} | 当前第 {currentIndex + 1} 张
-
-
- {images.map((_, i) => (
-
- {i + 1}
-
- ))}
-
- {currentIndex === images.length - 1 && savedCount > 0 && (
-
- 重新上传
-
- )}
-
-
- {/* Image */}
-
-
})
-
-
- {/* Buttons - moved here */}
-
-
- 上一张
-
-
- {currentIndex < images.length - 1 && (
-
- 下一张
-
- )}
-
-
- 重新识别
-
-
- 保存结果
-
-
-
- {loading && (
-
🤖 AI识别中...
- )}
-
- {result && !loading && (
- <>
- {/* 基本信息 */}
-
-
基本信息
-
-
-
名称
-
updateResult('name', 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: '13px' }} />
-
-
-
-
编号
-
updateResult('code', 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: '13px' }} />
-
-
-
-
持仓类型
-
-
-
-
-
珍惜度
-
-
-
-
-
冠字序号
-
updateResult('prefixSerial', 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: '#fbbf24', fontSize: '13px', fontFamily: 'monospace' }} />
-
-
-
-
版别
-
updateResult('version', 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: '13px' }} />
-
-
-
-
状态
-
-
-
-
-
封装
-
-
-
-
- {/* 评级信息 */}
-
-
updateResult('isGraded', !result.isGraded)}>
-
- {result.isGraded && ✓}
-
-
已评级
-
-
- {result.isGraded && (
- <>
-
-
评级公司
-
updateResult('gradingCompany', 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: '13px' }} />
-
-
-
评级分数
-
updateResult('gradingScore', 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: '#fbbf24', fontSize: '13px' }} />
-
- >
- )}
-
-
updateResult('threeStar', !result.threeStar)}>
-
- {result.threeStar && ✓}
-
-
三星
-
-
-
- {/* 特殊信息 */}
-
-
特殊信息
-
-
-
发行机构
-
updateResult('issuer', 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: '13px' }} />
-
-
-
-
年份
-
updateResult('issueYear', 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: '13px' }} />
-
-
-
-
特殊标识
-
updateResult('specialMark', 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: '13px' }} />
-
-
-
-
号码特征
-
updateResult('serialFeature', 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: '13px' }} />
-
-
-
-
面额
-
updateResult('denomination', 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: '13px' }} />
-
-
-
- {/* 价格信息 */}
-
-
-
成本价
-
updateResult('costPrice', 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: '13px' }} />
-
-
-
-
出售价
-
updateResult('targetPrice', 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: '#fbbf24', fontSize: '13px' }} />
-
-
-
- {/* 备注 */}
-
- >
- )}
-
- {/* Footer */}
- {savedCount > 0 && currentIndex === images.length - 1 && (
-
- 重新上传
-
- )}
-
-
-
- )
-}
diff --git a/frontend/src/pages/Edit.jsx b/frontend/src/pages/Edit.jsx
index 802fc66..118fc6e 100644
--- a/frontend/src/pages/Edit.jsx
+++ b/frontend/src/pages/Edit.jsx
@@ -46,7 +46,7 @@ const categoryOptions = [
{ value: '其他', label: '其他' }
]
-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:'其他'}];
+const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'带7号',label:'带7号'},{value:'带4号',label:'带4号'},{value:'其他',label:'其他'}];
const rarityOptions = [
{ value: '通货', label: '通货' },
{ value: '特色', label: '特色' },
@@ -157,8 +157,8 @@ export default function Edit() {
if (digits) {
if (!digits.includes('2') && !digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无2347'
else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347'
- else if (digits.includes('4')) cat = '带4'
- else if (digits.includes('7')) cat = '无4'
+ else if (digits.includes('4')) cat = '带4号'
+ else if (digits.includes('7')) cat = '带7号'
else if (!digits.includes('4') && !digits.includes('7')) cat = '无47'
else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
else cat = '其他'
diff --git a/frontend/src/pages/Edit.jsx.dual_column_bak b/frontend/src/pages/Edit.jsx.dual_column_bak
deleted file mode 100644
index 337cba3..0000000
--- a/frontend/src/pages/Edit.jsx.dual_column_bak
+++ /dev/null
@@ -1,575 +0,0 @@
-import React, { useState, useEffect, useRef } from 'react'
-
-
-// 通用 Input 组件
-const Input = ({ form, handleChange, label, field, type = 'text', options = null }) => {
- const onChange = (e) => {
- const value = e.target.value
- if (type === 'number' && value !== '') {
- const num = parseFloat(value)
- handleChange(field, isNaN(num) ? '' : num)
- } else handleChange(field, value)
- }
- return (
-
-
{label}
- {options ? (
-
- ) : (
-
- )}
-
- )
-}
-
-export default function Edit() {
- const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
- const id = params.get('id')
-
- const [collection, setCollection] = useState(null)
- const [loading, setLoading] = useState(true)
- const [saving, setSaving] = useState(false)
- const [form, setForm] = useState({})
- const [showDelete, setShowDelete] = useState(false)
- const [collectionImages, setCollectionImages] = useState([])
- const fileInputRef = useRef(null)
- const [editingImageIndex, setEditingImageIndex] = useState(-1)
-
- useEffect(() => {
- if (id) {
- fetchDetail()
- }
- }, [id])
-
- // 图片上传处理
- const handleImageUpload = async (e) => {
- const files = Array.from(e.target.files)
- if (files.length === 0) return
-
- const token = localStorage.getItem('token')
- const maxImages = 3
-
- // 检查是否超过限制
- const currentCount = collection.images ? collection.images.length : 0
- if (editingImageIndex === -1 && currentCount + files.length > maxImages) {
- alert(`最多只能上传${maxImages}张图片`)
- return
- }
-
- // 更换图片(点击已有图片)
- if (editingImageIndex >= 0) {
- const file = files[0]
- const oldImage = collection.images[editingImageIndex]
-
- const imgFormData = new FormData()
- imgFormData.append('file', file)
-
- try {
- // 先上传新图片
- const uploadRes = await fetch(`/api/collections/upload-image?collection_id=${id}`, {
- method: 'POST',
- headers: { 'Authorization': 'Bearer ' + token },
- body: imgFormData
- })
-
- if (uploadRes.ok) {
- // 删除旧图片
- await fetch(`/api/collections/images/${oldImage.id}`, {
- method: 'DELETE',
- headers: { 'Authorization': 'Bearer ' + token }
- })
-
- // 重新加载详情
- await fetchDetail()
- alert('图片已更换')
- }
- } catch (err) {
- console.error('图片更换失败:', err)
- alert('更换失败,请重试')
- }
-
- setEditingImageIndex(-1)
- return
- }
-
- // 添加新图片
- for (const file of files) {
- const imgFormData = new FormData()
- imgFormData.append('file', file)
-
- try {
- const res = await fetch(`/api/collections/upload-image?collection_id=${id}`, {
- method: 'POST',
- headers: { 'Authorization': 'Bearer ' + token },
- body: imgFormData
- })
-
- if (res.ok) {
- // 重新加载藏品详情
- await fetchDetail()
- }
- } catch (err) {
- console.error('图片上传失败:', err)
- }
- }
- }
-
- const handleDeleteImage = async (imageId) => {
- if (!confirm('确定删除这张图片吗?')) return
-
- const token = localStorage.getItem('token')
-
- try {
- const res = await fetch(`/api/collections/images/${imageId}`, {
- method: 'DELETE',
- headers: { 'Authorization': 'Bearer ' + token }
- })
-
- if (res.ok) {
- await fetchDetail()
- }
- } catch (err) {
- console.error('图片删除失败:', err)
- }
- }
-
- const fetchDetail = async () => {
- setLoading(true)
- const token = localStorage.getItem('token')
- try {
- const res = await fetch(`/api/collections/${id}`, {
- headers: token ? { 'Authorization': 'Bearer ' + token } : {}
- })
- const data = await res.json()
- const item = (data.code === 200 ? data.data : data)
- if (item && item.id) {
- setCollection(item)
- // 设置特殊信息默认值
- const yearMatch = item.version ? item.version.match(/(20\d{2})/) : null
- const defaultForm = {
- ...item,
- issuer: item.issuer || '中国人民银行',
- issueYear: item.issueYear || (yearMatch ? yearMatch[1] : '2024'),
- material: item.material || '塑料钞',
- issueQuantity: item.issueQuantity || '1 亿'
- }
- setForm(defaultForm)
- // 加载图片
- if (item.images && Array.isArray(item.images)) {
- setCollectionImages(item.images)
- }
- }
- } catch (e) {
- console.error('加载详情失败:', e)
- }
- setLoading(false)
- }
-
- const handleChange = (key, value) => {
- setForm({ ...form, [key]: value })
- // 填入出售价时自动把状态改为"已售"
- if (key === 'targetPrice' && value) {
- setForm(prev => ({ ...prev, status: 'sold' }))
- }
- // 版别变化时同步发行年份
- if (key === 'version') {
- const yearMatch = value.match(/(20\d{2})/)
- if (yearMatch) {
- setForm(prev => ({ ...prev, issueYear: yearMatch[1] }))
- }
- }
- }
-
- const handleSave = async () => {
- setSaving(true)
- setError('')
- const token = localStorage.getItem('token')
-
- // 1. 先检查是否重复(同版别、同冠字号)
- if (form.prefixSerial) {
- const checkRes = await fetch(`/api/collections?search=${encodeURIComponent(form.prefixSerial)}`, {
- headers: { 'Authorization': 'Bearer ' + token }
- })
- const checkData = await checkRes.json()
- const collections = checkData.data || checkData || []
- const duplicate = collections.find(c =>
- c.version === form.version &&
- c.prefixSerial === form.prefixSerial &&
- c.id !== id // 排除自己
- )
- if (duplicate) {
- setError('已经录入过同版同号藏品!')
- setSaving(false)
- return
- }
- }
-
- // 转换字段名:camelCase 转字段编码
- const convertField = (obj) => {
- const map = {
- // f99 系统字段
- id: 'f99_90_id',
- userId: 'f99_91_user_id',
- createdAt: 'f99_92_created_at',
- updatedAt: 'f99_93_updated_at',
- // f01 基本信息
- name: 'f01_01_name',
- code: 'f01_02_code',
- category: 'f01_03_category',
- status: 'f01_04_status',
- remark: 'f01_05_remark',
- // f02 详细字段
- prefixSerial: 'f02_10_prefix_serial',
- version: 'f02_11_version',
- packaging: 'f02_12_packaging',
- rarity: 'f02_13_rarity',
- // f03 评级信息
- isGraded: 'f03_20_is_graded',
- gradingCompany: 'f03_21_grading_company',
- gradingScore: 'f03_22_grading_score',
- threeStar: 'f03_23_three_star',
- // f04 特殊信息
- specialMark: 'f04_30_special_mark',
- serialFeature: 'f04_31_serial_feature',
- issuer: 'f04_32_issuer',
- issueYear: 'f04_33_issue_year',
- material: 'f04_34_material',
- denomination: 'f04_35_denomination',
- issueQuantity: 'f04_36_issue_quantity',
- // f05 价格信息
- costPrice: 'f05_40_cost_price',
- targetPrice: 'f05_41_target_price',
- goalPrice: 'f05_42_goal_price',
- repairFee: 'f05_43_repair_fee',
- gradingFee: 'f05_44_grading_fee',
- // f06 其他信息
- purpose: 'f06_50_purpose'
- }
- const result = {}
- // 数字字段列表
- const numberFields = ['costPrice', 'targetPrice', 'goalPrice', 'repairFee', 'gradingFee']
- for (const key in obj) {
- let value = obj[key]
- // 过滤空字符串为 null
- if (value === '' || value === null) {
- value = null
- }
- // 数字字段转换为浮点数
- else if (numberFields.includes(key)) {
- value = parseFloat(value)
- if (isNaN(value)) value = null
- }
- result[map[key] || key] = value
- }
- return result
- }
- try {
- const res = await fetch(`/api/collections/${id}`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': 'Bearer ' + token
- },
- body: JSON.stringify(convertField(form))
- })
- if (res.ok) {
- alert('保存成功!')
- // 刷新首页统计数据
- if (window.refreshHome) {
- window.refreshHome()
- }
- window.location.hash = '#/detail?id=' + id
- } else {
- const data = await res.json()
- alert('保存失败: ' + (data.error || '未知错误'))
- }
- } catch (e) {
- alert('保存失败: ' + e.message)
- }
- setSaving(false)
- }
-
- const handleDelete = () => {
- setShowDelete(true)
- }
-
- const doDelete = async () => {
- const token = localStorage.getItem('token')
- try {
- const res = await fetch(`/api/collections/${id}`, {
- method: 'DELETE',
- headers: {
- 'Authorization': 'Bearer ' + token
- }
- })
- if (res.ok) {
- alert('删除成功!')
- if (window.refreshList) window.refreshList()
- window.location.hash = '#/list'
- } else {
- alert('删除失败')
- }
- } catch (e) {
- alert('删除失败')
- }
- setShowDelete(false)
- }
-
- const goBack = () => {
- window.location.hash = '#/detail?id=' + id
- }
-
- const statusOptions = [
- { value: 'in_collection', label: '收藏中' },
- { value: 'selling', label: '出售中' },
- { value: 'sold', label: '已售' },
- { value: 'grading', label: '送评中' },
- { value: 'repairing', label: '修复中' },
- { value: 'transit', label: '在途中' },
- { value: 'other', label: '其他' }
- ]
-
- const categoryOptions = [
- { value: '自持', label: '自持' },
- { value: '寄存', label: '寄存' },
- { value: '寄售', label: '寄售' },
- { value: '共有', label: '共有' },
- { value: '寻号', label: '寻号' },
- { value: '其他', label: '其他' }
- ]
-
- const rarityOptions = [
- { value: '通货', label: '通货' },
- { value: '特色', label: '特色' },
- { value: '少见', label: '少见' },
- { value: '稀有', label: '稀有' },
- { value: '珍品', label: '珍品' },
- { value: '孤品', label: '孤品' }
- ]
-
- if (loading) {
- return
加载中...
- }
-
- return (
-
- {/* 顶部 */}
-
- ←
- 编辑藏品
-
-
-
- {/* 图片预览区域 */}
-
-
-
藏品图片
- {collectionImages.length > 0 && (
-
fileInputRef.current?.click()}
- style={{
- padding: '6px 12px',
- background: 'rgba(34, 197, 94, 0.2)',
- color: '#22c55e',
- border: '1px solid #22c55e',
- borderRadius: '6px',
- fontSize: '12px',
- cursor: 'pointer',
- display: 'flex',
- alignItems: 'center',
- gap: '4px'
- }}
- >
- 📷 更换图片
-
- )}
-
-
-
-
- {collectionImages.length > 0 ? (
-
- {collectionImages.map((img, index) => (
-
-

-
{index + 1}/{collectionImages.length}
-
- ))}
-
- ) : (
-
fileInputRef.current?.click()}
- style={{
- aspectRatio: '1.5',
- background: 'rgba(255,255,255,0.05)',
- border: '2px dashed #fbbf24',
- borderRadius: '12px',
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- justifyContent: 'center',
- cursor: 'pointer',
- color: '#fbbf24',
- fontSize: '12px',
- transition: 'all 0.2s'
- }}
- >
-
📷
-
点击上传图片
-
- 最多可上传 1 张
-
-
- )}
-
-
- {/* 基本信息 */}
-
-
- {/* 评级信息 */}
-
-
评级信息
-
- handleChange('isGraded', e.target.checked)}
- style={{ width: '22px', height: '22px', cursor: 'pointer', backgroundColor: form.isGraded ? '#22c55e' : 'rgba(255,255,255,0.1)', border: '2px solid #22c55e', borderRadius: '4px', appearance: 'none', WebkitAppearance: 'none' }}
- />
-
-
-
-
-
-
- handleChange('threeStar', e.target.checked)}
- style={{ width: '22px', height: '22px', cursor: 'pointer', backgroundColor: form.threeStar ? '#22c55e' : 'rgba(255,255,255,0.1)', border: '2px solid #22c55e', borderRadius: '4px', appearance: 'none', WebkitAppearance: 'none' }}
- />
-
-
-
-
- {/* 特殊信息 */}
-
-
- {/* 价格信息 */}
-
-
- {/* 备注 */}
-
-
- {/* 按钮 */}
-
- {saving ? '保存中...' : '保存'}
-
-
-
- 删除藏品
-
-
-
- {/* 删除确认弹窗 */}
- {showDelete && (
-
-
-
确定删除此藏品?
-
- setShowDelete(false)} style={{ flex: 1, padding: '12px', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '13px' }}>取消
- 删除
-
-
-
- )}
-
- )
-}
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx
index dc0262b..f6912a9 100644
--- a/frontend/src/pages/Home.jsx
+++ b/frontend/src/pages/Home.jsx
@@ -5,6 +5,10 @@ export default function Home() {
const [user, setUser] = useState(null)
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(() => {
@@ -54,6 +58,31 @@ 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/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(() => {})
+ }, [])
+
// 检查是否为管理员
const isAdmin = user && user.role === 'admin'
@@ -67,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 (
@@ -96,9 +125,9 @@ export default function Home() {
backdropFilter: 'blur(10px)'
}}>
-

+
-
{user?.username || '用户'}
+
{user?.username || '用户'} ID:{user?.user_code || '-'}
欢迎回来 👋
@@ -131,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}
@@ -145,41 +174,171 @@ export default function Home() {
window.location.hash = '#/add?mode=ocr'} style={{
background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
borderRadius: '12px',
- padding: '16px',
+ padding: '12px 16px',
cursor: 'pointer',
display: 'flex',
+ flexDirection: 'column',
alignItems: 'center',
- gap: '12px'
+ justifyContent: 'center',
+ textAlign: 'center'
}}>
-
📷
-
+
藏品录入
+
AI识别添加
+
+
window.location.hash = '#/add?mode=deal'} style={{
+ background: 'linear-gradient(135deg, #ec4899 0%, #db2777 100%)',
+ borderRadius: '12px',
+ padding: '12px 16px',
+ cursor: 'pointer',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ textAlign: 'center'
+ }}>
+
行情录入
+
录入成交行情
+
+
window.location.hash = '#/news?type=seek'} style={{
+ background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
+ borderRadius: '12px',
+ padding: '12px 16px',
+ cursor: 'pointer',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ textAlign: 'center'
+ }}>
+
发布寻号
+
发布寻配需求
window.location.hash = '#/add?mode=manual'} style={{
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
borderRadius: '12px',
- padding: '16px',
+ padding: '12px 16px',
cursor: 'pointer',
display: 'flex',
+ flexDirection: 'column',
alignItems: 'center',
- gap: '12px'
+ justifyContent: 'center',
+ textAlign: 'center'
}}>
-
✏️
-
+
发布藏品
+
手动发布
- {/* 最近藏品 */}
+ {/* 寻配号数据 */}
+
+
🔍 寻配号数据
+
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}
+
总帖子
+
+
+
{yichensStats.deals || 0}
+
出售
+
+
+
{yichensStats.wants || 0}
+
求购
+
+
+
{yichensStats.dragons || 0}
+
龙钞
+
+
+
{yichensStats.horses || 0}
+
马钞
+
+
+
{yichensStats.tianma || 0}
+
天马
+
+
+
+
+ {/* 今日龙钞帖子数据统计 */}
+
+
🐉 今日龙钞帖子数据统计
+
+ {/* 表头 */}
+
+ {/* 数据行 */}
+
+
带4
+
{dragonStats.dai4?.deals || 0}
+
{dragonStats.dai4?.wants || 0}
+
{(dragonStats.dai4?.deals || 0) + (dragonStats.dai4?.wants || 0)}
+
+
+
带7号
+
{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)}
+
+
+
+
+ {/* 最新一尘发帖 */}
-
最近藏品
-
window.location.hash = '#/list'} style={{ color: '#3b82f6', fontSize: '12px', cursor: 'pointer' }}>查看全部 →
+
📝 最新一尘发帖
+
window.location.hash = '#/news'} style={{ color: '#3b82f6', fontSize: '12px', cursor: 'pointer' }}>查看全部 →
- {recentCollections.length === 0 ? (
-
暂无藏品
+ {recentPosts.length === 0 ? (
+
暂无帖子
) : (
- recentCollections.map((item, idx) => (
-
window.location.hash = '#/detail?id=' + item.id} style={{
+ recentPosts.map((item, idx) => (
+
window.open(item.url, '_blank')} style={{
padding: '12px 16px',
- borderBottom: idx < recentCollections.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none',
+ borderBottom: idx < recentPosts.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
-
-
{item.code || '-'} {item.prefixSerial || ''}
-
{item.version || '-'} · {item.status === 'sold' ? '已售' : item.status === 'in_collection' ? '收藏中' : item.status}
+
+
{item.title || '无标题'}
+
{item.category || '-'} · {item.author_username || '未知'} · {item.post_time?.substring(0, 16) || ''}
-
- {item.costPrice ? '¥' + item.costPrice : '-'}
+
+ {item.post_type === 'deal' ? '出售' : item.post_type === 'want' ? '求购' : '其他'}
))
@@ -228,13 +393,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 390bdb8..0000000
--- a/frontend/src/pages/Info.jsx
+++ /dev/null
@@ -1,677 +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..6979f44 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/deal/list?user_only=true&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')
@@ -308,7 +365,7 @@ export default function List() {
bVal = rarityOrder[bVal] || 0
} else if (sortField === 'numberCategory') {
// 号码分类按自定义顺序排序
- const numberCategoryOrder = { '无2347': 1, '无347': 2, '无247': 3, '无47': 4, '无4': 5, '带4': 6, '其他': 7 }
+ const numberCategoryOrder = { '无2347': 1, '无347': 2, '无247': 3, '无47': 4, '带7号': 5, '带4号': 6, '其他': 7 }
aVal = numberCategoryOrder[aVal] || 99
bVal = numberCategoryOrder[bVal] || 99
}
@@ -351,7 +408,7 @@ export default function List() {
}
const getNumberCategoryColor = (cat) => {
- const colors = { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '无4': '#06b6d4', '带4': '#22c55e', '其他': '#64748b' };
+ const colors = { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '带7号': '#06b6d4', '带4号': '#22c55e', '其他': '#64748b' };
return colors[cat] || '#64748b';
};
@@ -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' }}>
+ 我的行情 ({filteredDeals.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,49 @@ export default function List() {
+ {/* 行情tab内容 */}
+ {activeTab === 'deals' && (
+
+ {/* 行情搜索框 */}
+
+ setDealSearch(e.target.value)}
+ style={{
+ width: '100%',
+ background: 'rgba(255,255,255,0.05)',
+ border: '1px solid rgba(255,255,255,0.1)',
+ color: '#fff',
+ padding: '10px 12px',
+ borderRadius: '8px',
+ fontSize: '14px',
+ outline: 'none',
+ boxSizing: 'border-box'
+ }}
+ />
+
+
+ {dealsLoading ? (
+
加载中...
+ ) : filteredDeals.length === 0 ? (
+
+
📊
+
{dealSearch ? '没有匹配的行情' : '暂无行情记录'}
+
+ ) : (
+
+ {filteredDeals.map(deal => (
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* 藏品tab内容 */}
+ {activeTab === 'collections' && (
+ <>
{loading ? (
加载中...
) : filteredCollections.length === 0 ? (
@@ -597,7 +712,254 @@ 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,
+ // 从冠字号推断版别
+ version: (deal.title || '').startsWith('J0') ? '龙钞' : (deal.title || '').startsWith('J1') ? '马钞' : (deal.title || '').startsWith('J3') ? '蛇钞' : '其他'
+ })
+ setEditing(true)
+ }
+
+ const saveEdit = async () => {
+ const token = localStorage.getItem('token')
+ try {
+ await fetch(`${API_BASE}/api/deal/${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/deal/${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.version || (deal.title?.startsWith('J0') ? '龙钞' : deal.title?.startsWith('J1') ? '马钞' : deal.title?.startsWith('J3') ? '蛇钞' : '其他')}
+ {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.version || (deal.title?.startsWith('J0') ? '龙钞' : deal.title?.startsWith('J1') ? '马钞' : deal.title?.startsWith('J3') ? '蛇钞' : '其他')}
+
价格: ¥{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 04cb53f..e54e7c9 100644
--- a/frontend/src/pages/News.jsx
+++ b/frontend/src/pages/News.jsx
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'
+import YichensBoard from './YichensBoard'
// 本地获取用户手机号 - 添加异常处理
const getUserPhone = () => {
@@ -22,7 +23,7 @@ const getStoredUserId = () => {
// 资讯页面 - 展示寻配号和行情信息(所有人可见)
export default function News() {
- const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'seek')
+ const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'yichen')
const [showSeekPublish, setShowSeekPublish] = useState(false)
const [showMySeeks, setShowMySeeks] = useState(false) // 我的寻号
const [showContactId, setShowContactId] = useState(null) // 显示联系方式的寻号ID
@@ -31,7 +32,12 @@ export default function News() {
const [comments, setComments] = useState({}) // 存储各寻号的评论
const [matchedStatus, setMatchedStatus] = useState({}) // 存储各寻号的匹配状态
const [showMatchList, setShowMatchList] = useState(false)
+ const [dealVersion, setDealVersion] = useState('龙钞')
+ const [dealDate, setDealDate] = useState('')
+ const [dealDetailItems, setDealDetailItems] = useState(null) // 弹窗显示的详情列表
const [matchCollections, setMatchCollections] = useState([])
+ const [networkMatchCollections, setNetworkMatchCollections] = useState([])
+ const [showNetworkMatchList, setShowNetworkMatchList] = useState(false)
const [customModal, setCustomModal] = useState({show: false, title: '', content: ''})
const [seekForm, setSeekForm] = useState({
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || ''
@@ -40,6 +46,12 @@ export default function News() {
const [viewMode, setViewMode] = useState('all')
const [expandedItems, setExpandedItems] = useState({}) // 展开状态
const [loading, setLoading] = useState(false)
+ const [currentPage, setCurrentPage] = useState(1)
+ const [totalPages, setTotalPages] = useState(1)
+
+ // 从URL获取userId参数(用于管理员查看指定用户的行情)
+ const urlParams = new URLSearchParams(window.location.hash.split('?')[1] || '')
+ const filterUserId = urlParams.get('userId')
const API_BASE = localStorage.getItem('API_BASE') || ''
const currentUser = localStorage.getItem('token') ? { id: 'logged_in' } : null
@@ -55,7 +67,7 @@ export default function News() {
// 获取资讯列表
useEffect(() => {
fetchInfoList()
- }, [activeTab])
+ }, [activeTab, dealDate])
// 自动生成寻号标题
useEffect(() => {
@@ -66,13 +78,41 @@ export default function News() {
const fetchInfoList = async () => {
setLoading(true)
try {
- // 寻配号对所有人公开,无需登录即可查看
const token = localStorage.getItem('token')
- // 根据tab获取不同类型的数据
- const type = activeTab === 'seek' ? 'seek' : 'deal'
- // 始终传递token,以便获取准确的matched_count
+ const type = activeTab === 'seek' ? 'seek' : activeTab === 'deal' ? 'deal' : 'yichen'
const headers = token ? { Authorization: `Bearer ${token}` } : {}
- const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}`, { headers })
+
+ // 构建URL参数
+ // 使用新的独立API(seek和deal已拆分)
+ let url = activeTab === 'yichen'
+ ? `${API_BASE}/api/information/list?info_type=${activeTab}`
+ : activeTab === 'seek'
+ ? `${API_BASE}/api/seek/list`
+ : `${API_BASE}/api/deal/list`
+
+ // 成交行情和寻配号每页500条
+ if (activeTab === 'deal') {
+ url += (url.includes('?') ? '&' : '?') + 'page_size=500'
+ if (dealDate) {
+ url += '&deal_date=' + dealDate
+ }
+ if (filterUserId) {
+ url += '&user_id=' + filterUserId
+ }
+ } else if (activeTab === 'seek') {
+ url += (url.includes("?") ? "&" : "?") + "page=" + currentPage + "&page_size=500"
+ if (filterUserId) {
+ url += '&user_id=' + filterUserId
+ }
+ } else if (activeTab === 'yichen' && filterUserId) {
+ // 行情tab也需要支持user_id过滤
+ url += (url.includes('?') ? '&' : '?') + 'user_id=' + filterUserId
+ }
+
+ const res = await fetch(url, { headers })
+ // 从响应头获取总页数
+ const total = res.headers.get('X-Total-Pages') || res.headers.get('x-total-pages')
+ if (total) setTotalPages(parseInt(total))
// 检查响应状态
if (!res.ok) {
console.error('获取资讯列表失败:', res.status)
@@ -82,7 +122,21 @@ export default function News() {
}
const data = await res.json()
console.log('资讯列表:', data)
- setInfoList(data || [])
+
+ // 成交行情按分类排序
+ let sortedData = data || []
+ if (activeTab === 'deal' || activeTab === 'seek') {
+ // 优先按日期倒序,同日按编号倒序
+ sortedData = [...(data || [])].sort((a, b) => {
+ const dateA = a.deal_date || a.created_at?.split('T')[0] || ""
+ const dateB = b.deal_date || b.created_at?.split('T')[0] || ""
+ if (dateA !== dateB) return dateB.localeCompare(dateA)
+ const noA = a.deal_no || ""
+ const noB = b.deal_no || ""
+ return noB.localeCompare(noA)
+ })
+ }
+ setInfoList(sortedData)
} catch (e) {
console.error(e)
setInfoList([])
@@ -146,10 +200,11 @@ export default function News() {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
+ const phone = getUserPhone()
const res = await fetch(`${API_BASE}/api/information/seek/match-confirm`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
- body: JSON.stringify({ info_id: infoId })
+ body: JSON.stringify({ info_id: infoId, contact: phone || '用户已同意' })
})
const data = await res.json()
if (data.message === '匹配成功,已通知发布者') {
@@ -187,7 +242,7 @@ export default function News() {
console.log('Matched user response:', res.status)
const data = await res.json()
console.log('Matched user data:', data)
- if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.matched_contact || '未提供'}`})
+ if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示:所有用户信息仅作参考,交易请走正规平台,如产生经济损失与本站无关,后果自负。`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
@@ -200,7 +255,7 @@ export default function News() {
console.log('Publisher response:', res.status)
const data = await res.json()
console.log('Publisher data:', data)
- if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.contact || '未提供'}`})
+ if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示:所有用户信息仅作参考,交易请走正规平台,如产生经济损失与本站无关,后果自负。`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
@@ -223,6 +278,20 @@ export default function News() {
}
}
+ // 获取网络数据匹配列表
+ const fetchNetworkMatchCollections = async (infoId) => {
+ try {
+ const res = await fetch(`${API_BASE}/api/information/seek/network-match/${infoId}`)
+ const data = await res.json()
+ console.log('网络数据匹配结果:', data)
+ setNetworkMatchCollections(data.collections || [])
+ setShowNetworkMatchList(true)
+ } catch (e) {
+ console.error('获取网络匹配藏品失败:', e)
+ alert('获取网络匹配藏品失败: ' + e.message)
+ }
+ }
+
// 获取当前用户ID
const currentUserId = getStoredUserId()
@@ -234,12 +303,12 @@ export default function News() {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
- const res = await fetch(`${API_BASE}/api/information/my/list`, {
+ const res = await fetch(`${API_BASE}/api/seek/list`, {
headers: { Authorization: `Bearer ${token}` }
})
const data = await res.json()
- // 过滤出寻号类型的并筛选当前用户
- const seeks = Array.isArray(data) ? data.filter(item => item.info_type === 'seek' && item.user_id === currentUserId) : []
+ // 筛选当前用户
+ const seeks = Array.isArray(data) ? data.filter(item => item.user_id === currentUserId) : []
setMySeekList(seeks)
setInfoList(seeks) // 在主列表显示
} catch (e) { console.error(e) }
@@ -250,7 +319,7 @@ export default function News() {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
- const res = await fetch(`${API_BASE}/api/information/list?info_type=seek`, {
+ const res = await fetch(`${API_BASE}/api/seek/list`, {
headers: { Authorization: `Bearer ${token}` }
})
const data = await res.json()
@@ -314,8 +383,9 @@ export default function News() {
// Tab切换
const tabs = [
- { key: 'seek', label: '🔍 寻配号' },
- { key: 'deal', label: '💰 成交行情' }
+ { key: 'seek', label: '寻配号' },
+ { key: 'deal', label: '成交行情' },
+ { key: 'yichen', label: '一尘看板' }
]
const formatDate = (dateStr) => {
@@ -387,7 +457,7 @@ export default function News() {
readOnly={isFixed}
onChange={(e) => {
if (i < 2) return
- const val = e.target.value.toUpperCase().replace(/[^0-9XABCFG]/g, '')
+ const val = e.target.value.toUpperCase().replace(/[^0-9XABCDEFG]/g, '')
const newFeatures = (seekForm.features || '').split('')
while (newFeatures.length < 8) newFeatures.push('')
newFeatures[i - 2] = val
@@ -435,7 +505,7 @@ export default function News() {
)}
- {activeTab === 'seek' ? '🔍 寻配号信息' : '💰 成交行情信息'}
+ {activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? `成交行情信息 (${dealDate})` : '一尘看板'}
{activeTab === 'seek' && (
{ setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部
@@ -444,17 +514,234 @@ export default function News() {
setShowSeekPublish(!showSeekPublish)} style={{ padding: '6px 12px', background: showSeekPublish ? '#6b7280' : '#10b981', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>发布寻号
)}
+ {activeTab === 'deal' && (
+
+ setDealDate(e.target.value)}
+ style={{ padding: '6px 12px', background: '#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }} />
+
+ )}
- {loading ? (
+ {/* 一尘看板独立渲染 */}
+ {activeTab === 'yichen' ? (
+
+ ) : loading ? (
加载中...
) : infoList.length === 0 ? (
- 暂无{activeTab === 'seek' ? '寻配号' : '成交行情'}信息
+ 暂无{activeTab === 'seek' ? '寻配号' : activeTab === 'deal' ? '成交行情' : '一尘看板'}信息
) : (
+ {/* 成交行情 - 价格统计表格 */}
+ {activeTab === 'deal' && (() => {
+ const versions = ['龙钞', '马钞', '蛇钞', '其他']
+ const packagings = ['标百', '标十', '单张']
+ const categories = ['带4号', '带7号', '永恒号', '永恒', '钻石号', '钻石', '如意号', '朦胧号', '朦胧王', '天马号', '金山号', '金马号', '天马王', '金山王', '金马王', '倒置号', '圆圆号', '通货', '无4']
+
+ // 分类名称标准化
+ const normalizeCat = (c) => {
+ if (c === '通货') return '带4号'
+ if (c === '无4') return '带7号'
+ return c
+ }
+
+ const filteredData = infoList.filter(item => {
+ const serial = (item.title || '').split('-')[0] || ''
+ let version = '其他'
+ if (serial.startsWith('J0')) version = '龙钞'
+ else if (serial.startsWith('J1')) version = '马钞'
+ else if (serial.startsWith('J3')) version = '蛇钞'
+ if (version !== dealVersion) return false
+ return true
+ })
+
+ const calcAvg = (pkg, cat) => {
+ const items = filteredData.filter(item => {
+ const content = item.content || ''
+ const p = content.includes('包装:') ? content.split('包装:')[1].split('\n')[0].trim() : (item.packaging || '')
+ let c = content.includes('分类:') ? content.split('分类:')[1].split('\n')[0].trim() : (item.category || '')
+ c = normalizeCat(c)
+ return p === pkg && c === cat
+ })
+ if (items.length === 0) return null
+ const sum = items.reduce((a, b) => a + (b.deal_price || 0), 0)
+ return { avg: Math.round(sum / items.length), count: items.length, items }
+ }
+
+ return (
+
+
+
+ {versions.map(v => (
+ setDealVersion(v)}
+ style={{ padding: '6px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
+ background: dealVersion === v ? '#3b82f6' : 'rgba(255,255,255,0.1)',
+ color: '#fff', fontSize: '13px', fontWeight: dealVersion === v ? '600' : '400' }}>
+ {v}
+
+ ))}
+
+
+
+
+
+
+
+ |
+ {packagings.map(p => (
+ {p} |
+ ))}
+
+
+
+ {(() => {
+ // 计算每个分类的平均价格
+ const catAvg = categories.map(cat => {
+ const prices = []
+ packagings.forEach(pkg => {
+ const d = calcAvg(pkg, cat)
+ if (d) prices.push(d.avg)
+ })
+ const avg = prices.length > 0 ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0
+ return { cat, avg }
+ }).filter(c => c.avg > 0)
+ // 分类名称标准化并按固定顺序排序
+ catAvg.sort((a, b) => a.avg - b.avg)
+ const categoryOrder = ['带4号', '带7号', '永恒号', '天马号', '天马王', '金山号', '金山王', '钻石号', '朦胧号', '朦胧王', '金马号', '金马王', '倒置号', '圆圆号']
+ const sortedCats = categoryOrder.filter(cat => catAvg.some(c => c.cat === cat))
+ .concat(catAvg.filter(c => !categoryOrder.includes(c.cat)).map(c => c.cat))
+
+ return sortedCats.map(cat => {
+ const rowData = packagings.map(pkg => calcAvg(pkg, cat))
+ const hasData = rowData.some(d => d !== null)
+ if (!hasData) return null
+ return (
+
+ | {cat} |
+ {rowData.map((d, i) => (
+
+ {d ? (
+ setDealDetailItems(d.items)}>
+ ¥{d.avg.toLocaleString()}
+ ({d.count})
+
+ ) : -}
+ |
+ ))}
+
+ )
+ })
+ })()}
+
+
+
+
+ )
+ })()}
+
+ {/* 成交行情详情弹窗 */}
+ {dealDetailItems && (
+
+
+
+
成交详情列表
+
setDealDetailItems(null)}
+ style={{ background: 'none', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>✕
+
+
+ {([...dealDetailItems].sort((a, b) => (a.deal_price || 0) - (b.deal_price || 0))).map((item, idx) => {
+ const content = item.content || ''
+ const grade = content.includes('评级:') ? content.split('评级:')[1].split('\n')[0].trim() : (item.grading_score || '')
+ const sizeMatch = content.match(/大小号:\s*(.+?)(?:\n|$)/)
+ const size = sizeMatch ? sizeMatch[1].trim() : ''
+ const platformMatch = content.match(/平台:\s*(.+?)(?:\n|$)/)
+ const platform = platformMatch ? platformMatch[1].trim() : '-'
+ return (
+
+
+
+ {(item.title || '').split('-')[0]}
+ {size && | {size}}
+ {grade && | {grade}}
+
+
+ {item.deal_date} | {item.category} | {item.packaging} | {platform}
+
+
+
+ ¥{item.deal_price?.toLocaleString()}
+
+
+ )
+ })}
+
+
+
+ )}
+
{infoList.map(item => {
+ // 成交行情 tab - 按维度汇总展示
+ if (activeTab === 'deal') {
+ const content = item.content || ''
+ const categoryMatch = content.match(/分类:\s*(.+?)(?:\n|$)/)
+ const packagingMatch = content.match(/包装:\s*(.+?)(?:\n|$)/)
+ const platformMatch = content.match(/平台:\s*(.+?)(?:\n|$)/)
+ const sellerMatch = content.match(/出售者:\s*(.+?)(?:\n|$)/)
+ const dateMatch = content.match(/日期:\s*(.+?)(?:\n|$)/)
+ const gradeMatch = content.match(/评级:\s*(.+?)(?:\n|$)/)
+
+ const category = categoryMatch ? categoryMatch[1].trim() : item.category || '-'
+ const packaging = packagingMatch ? packagingMatch[1].trim() : item.packaging || '-'
+ const platform = platformMatch ? platformMatch[1].trim() : '-'
+ const seller = sellerMatch ? sellerMatch[1].trim() : '-'
+ const dealDate = dateMatch ? dateMatch[1].trim() : item.deal_date || '-'
+ const grade = gradeMatch ? gradeMatch[1].trim() : (item.grading_score || '')
+ const gradingCompany = item.grading_company || ''
+
+ const serial = (item.title || '').split('-')[0] || ''
+ let version = '其他'
+ if (serial.startsWith('J0')) version = '龙钞'
+ else if (serial.startsWith('J1')) version = '马钞'
+ else if (serial.startsWith('J3')) version = '蛇钞'
+
+ return (
+
+ {/* 标题行:冠字号 + 价格 */}
+
+ {serial}
+ ¥{item.deal_price?.toLocaleString()}
+ {item.deal_no}
+
+ {/* 信息行:版别 | 包装 | 分类 | 评级 | 分数 | 平台 */}
+
+ {version}
+ |
+ {packaging}
+ |
+ {category}
+ {gradingCompany && <>|{gradingCompany}>}
+ {grade && <>|{grade}>}
+ |
+ {platform}
+ |
+ {dealDate}
+
+
+ )
+ }
+
// 解析正文中的号码特征和联系方式
const content = item.content || ''
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
@@ -472,7 +759,7 @@ export default function News() {
{/* 创建日期 + 用户名 */}
- 📅 {formatDate(item.created_at)} | 👤 {item.user_name || '匿名用户'}
+ 📅 {formatDate(item.created_at)} | 👤 {(item.content && item.content.match(/发布者: (.+)/)?.[1]) || item.user_name || '匿名用户'}
{/* 号码特征 */}
{features && (
@@ -521,12 +808,20 @@ export default function News() {
{/* 配号结果 - 仅寻配号显示 */}
{activeTab === 'seek' && (
- fetchMatchCollections(item.id)}
>
- 配号结果: {item.matched_count || 0}条藏品匹配成功
+ 自有{item.matched_count || 0}条藏品匹配成功
+ {item.network_matched_count !== undefined && (
+ fetchNetworkMatchCollections(item.id)}
+ >
+ 网络数据{item.network_matched_count}条匹配成功
+
+ )}
)}
{/* 正文 - 默认收起,点击展开 */}
@@ -613,23 +908,24 @@ export default function News() {
{
+ if (item.is_matched === 'matched') return
if (item.matched_count > 0) {
matchAndContact(item.id)
} else {
setCustomModal({show: true, title: '提示', content: '暂无匹配藏品,无法匹配'})
}
}}
- disabled={item.matched_count === 0}
+ disabled={item.matched_count === 0 || item.is_matched === 'matched'}
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '6px',
border: 'none',
fontSize: '12px',
- background: item.matched_count > 0 ? '#3b82f6' : '#4b5563',
+ background: (item.matched_count > 0 && item.is_matched !== 'matched') ? '#3b82f6' : '#4b5563',
color: '#fff',
- cursor: item.matched_count > 0 ? 'pointer' : 'not-allowed',
- opacity: item.matched_count > 0 ? 1 : 0.5
+ cursor: (item.matched_count > 0 && item.is_matched !== 'matched') ? 'pointer' : 'not-allowed',
+ opacity: (item.matched_count > 0 && item.is_matched !== 'matched') ? 1 : 0.5
}}
>
匹配并联系藏友
@@ -751,6 +1047,74 @@ export default function News() {
)}
+ {/* 网络数据匹配藏品列表弹窗 */}
+ {showNetworkMatchList && (
+
+
+
+
匹配藏品清单(网络数据)
+ setShowNetworkMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×
+
+ {networkMatchCollections.length === 0 ? (
+
暂无匹配藏品
+ ) : (
+
+ {networkMatchCollections.map(c => (
+
{
+ if (c.post_url) {
+ window.open(c.post_url, '_blank')
+ }
+ setShowNetworkMatchList(false)
+ }}
+ >
+
+ 名称: {c.name || '-'}
+
+
+ 冠字号: {c.crown_code}
+
+ {c.price && (
+
+ 价格: ¥{c.price}
+
+ )}
+
+ 点击查看原帖 →
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* 分页组件 */}
+ {infoList.length > 0 && (
+
+ { if (currentPage > 1) { setCurrentPage(currentPage - 1); fetchInfoList() } }}
+ disabled={currentPage === 1}
+ style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage === 1 ? '#374151' : '#3b82f6', color: currentPage === 1 ? '#6b7280' : '#fff', cursor: currentPage === 1 ? 'not-allowed' : 'pointer', fontSize: '13px' }}
+ >
+ 上一页
+
+
+ 第 {currentPage} / {totalPages} 页
+
+ { if (currentPage < totalPages) { setCurrentPage(currentPage + 1); fetchInfoList() } }}
+ disabled={currentPage >= totalPages}
+ style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage >= totalPages ? '#374151' : '#3b82f6', color: currentPage >= totalPages ? '#6b7280' : '#fff', cursor: currentPage >= totalPages ? 'not-allowed' : 'pointer', fontSize: '13px' }}
+ >
+ 下一页
+
+
+ )}
+
{/* 我的寻号弹窗 */}
{showMySeeks && (
diff --git a/frontend/src/pages/News.jsx.backup_20260328 b/frontend/src/pages/News.jsx.backup_20260328
deleted file mode 100644
index 231066a..0000000
--- a/frontend/src/pages/News.jsx.backup_20260328
+++ /dev/null
@@ -1,554 +0,0 @@
-import React, { useState, useEffect } from 'react'
-
-// 资讯页面 - 展示寻配号和行情信息(所有人可见)
-export default function News() {
- const [activeTab, setActiveTab] = useState('deal')
- const [showSeekPublish, setShowSeekPublish] = useState(false)
- const [showMySeeks, setShowMySeeks] = useState(false) // 我的寻号
- const [showMatchList, setShowMatchList] = useState(false)
- const [matchCollections, setMatchCollections] = useState([])
- const [seekForm, setSeekForm] = useState({
- edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: ''
- })
- const [infoList, setInfoList] = useState([])
- const [loading, setLoading] = useState(false)
-
- const API_BASE = localStorage.getItem('API_BASE') || ''
-
- // 获取资讯列表
- useEffect(() => {
- fetchInfoList()
- }, [activeTab])
-
- // 自动生成寻号标题
- useEffect(() => {
- const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} ${seekForm.type || '不限'} J0${seekForm.features || 'XXXXXXXX'} ${seekForm.matchType}」`
- setSeekForm(prev => ({...prev, title}))
- }, [seekForm.edition, seekForm.type, seekForm.features, seekForm.matchType])
-
- const fetchInfoList = async () => {
- setLoading(true)
- try {
- // 寻配号对所有人公开,无需登录即可查看
- const token = localStorage.getItem('token')
- // 根据tab获取不同类型的数据
- const type = activeTab === 'seek' ? 'seek' : 'deal'
- // 始终传递token,以便获取准确的matched_count
- const headers = token ? { Authorization: `Bearer ${token}` } : {}
- const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}`, { headers })
- const data = await res.json()
- console.log('资讯列表:', data)
- setInfoList(data || [])
- } catch (e) {
- console.error(e)
- }
- setLoading(false)
- }
-
- // 获取匹配藏品列表
- const fetchMatchCollections = async (infoId) => {
- const token = localStorage.getItem('token')
- if (!token) { alert('请先登录'); return }
- try {
- const res = await fetch(`${API_BASE}/api/information/seek/match?info_id=${infoId}`, {
- headers: { Authorization: `Bearer ${token}` }
- })
- const data = await res.json()
- console.log('匹配结果:', data)
- console.log('匹配数量:', data.matched_count)
- console.log('藏品列表:', data.collections)
- setMatchCollections(data.collections || [])
- setShowMatchList(true)
- } catch (e) {
- console.error('获取匹配藏品失败:', e)
- alert('获取匹配藏品失败: ' + e.message)
- }
- }
-
- // 获取我的寻号列表
- const [mySeekList, setMySeekList] = useState([])
- const [editingSeek, setEditingSeek] = useState(null)
- const fetchMySeeks = async () => {
- const token = localStorage.getItem('token')
- if (!token) { alert('请先登录'); return }
- try {
- const res = await fetch(`${API_BASE}/api/information/my/list`, {
- headers: { Authorization: `Bearer ${token}` }
- })
- const data = await res.json()
- // 过滤出寻号类型的
- const seeks = Array.isArray(data) ? data.filter(item => item.info_type === 'seek') : []
- setMySeekList(seeks)
- } catch (e) { console.error(e) }
- }
-
- // 更新寻号
- const handleUpdateSeek = async () => {
- if (!editingSeek) return
- const token = localStorage.getItem('token')
- try {
- // 解析正文中的号码特征和联系方式
- const content = (editingSeek.content || '') + '\n联系方式: ' + editingSeek.contact
- await fetch(`${API_BASE}/api/information/${editingSeek.id}`, {
- method: 'PUT',
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
- body: JSON.stringify({
- title: editingSeek.title,
- content,
- expect_category: editingSeek.edition,
- expect_number: editingSeek.features ? 'J0' + editingSeek.features : null
- })
- })
- setEditingSeek(null)
- fetchMySeeks()
- } catch (e) { alert('更新失败') }
- }
-
- const handleSeekPublish = async () => {
- if (!seekForm.contact) { alert('请填写联系方式'); return }
- // 类型改为非必选
- try {
- const token = localStorage.getItem('token')
- const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + seekForm.contact
- // 从edition映射到category
- const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' }
- 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,
- info_type: 'seek',
- expect_category: seekForm.edition,
- expect_number: seekForm.features ? 'J0' + seekForm.features : None
- })
- })
- const data = await res.json()
- if (data.id || data.code === 0) {
- alert('发布成功!')
- setShowSeekPublish(false)
- setSeekForm({ edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: '' })
- fetchInfoList()
- } else { alert(data.message || '发布失败') }
- } catch (e) { alert('发布失败: ' + e.message) }
- }
-
- // Tab切换
- const tabs = [
- { key: 'seek', label: '🔍 寻配号' },
- { key: 'deal', label: '💰 成交行情' }
- ]
-
- const formatDate = (dateStr) => {
- if (!dateStr) return '-'
- const date = new Date(dateStr)
- return `${date.getMonth() + 1}/${date.getDate()}`
- }
-
- return (
-
- {/* Tab导航 */}
-
- {tabs.map(tab => (
-
setActiveTab(tab.key)}
- style={{
- padding: '10px 20px',
- borderRadius: '8px',
- background: activeTab === tab.key ? '#3b82f6' : 'transparent',
- color: '#fff',
- cursor: 'pointer',
- whiteSpace: 'nowrap',
- fontSize: '14px'
- }}
- >
- {tab.label}
-
- ))}
-
-
- {/* 资讯列表 */}
-
- {showSeekPublish && activeTab === 'seek' && (
-
-
-
- {['龙钞','马钞','蛇钞'].map(item => {
- const isSelected = seekForm.edition ? seekForm.edition.split(',').includes(item) : false
- return (
- {
- const eds = seekForm.edition ? seekForm.edition.split(',').filter(x => x) : []
- if (isSelected) {
- const idx = eds.indexOf(item)
- if (idx > -1) eds.splice(idx, 1)
- } else {
- eds.push(item)
- }
- setSeekForm({...seekForm, edition: eds.join(',')})
- }}
- style={{ flex: 1, padding: '8px', borderRadius: '6px', border: isSelected?'2px solid #10b981':'1px solid #334155', background: isSelected?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{item}
- )
- })}
-
-
-
-
- {['标百','标十','单张'].map(e => (
- setSeekForm({...seekForm, type: e})}
- style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.type===e?'2px solid #10b981':'1px solid #334155', background: seekForm.type===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}
- ))}
-
-
-
-
- {['求购','出售','寻号'].map(e => (
- setSeekForm({...seekForm, category: e})}
- style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.category===e?'2px solid #10b981':'1px solid #334155', background: seekForm.category===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}
- ))}
-
-
-
- setSeekForm({...seekForm, price: e.target.value})} placeholder="请输入价格"
- style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
-
-
-
- {['精准','模糊'].map(e => (
- setSeekForm({...seekForm, matchType: e})}
- style={{ flex: 1, padding: '8px', borderRadius: '6px', border: seekForm.matchType===e?'2px solid #10b981':'1px solid #334155', background: seekForm.matchType===e?'rgba(16,185,129,0.2)':'#0f172a', color: '#fff', fontSize: '12px' }}>{e}
- ))}
-
-
-
-
- {[0,1,2,3,4,5,6,7,8,9].map(i => {
- const isFixed = i < 2
- const char = i === 0 ? 'J' : (i === 1 ? '0' : (seekForm.features[i - 2] || ''))
- const isDigit = /[0-9]/.test(char)
- const letterColor = isFixed ? '#10b981' : (isDigit ? '#fff' : ({ X: '#94a3b8', A: '#f472b6', B: '#60a5fa', C: '#34d399', D: '#fbbf24', E: '#a78bfa', F: '#fb923c', G: '#22d3ee' }[char] || '#fff'))
- return { if (el) el.dataset.index = i }} type="text" maxLength={1}
- value={char}
- readOnly={isFixed}
- onChange={(e) => {
- if (i < 2) return
- const val = e.target.value.toUpperCase().replace(/[^0-9XABCFG]/g, '')
- const newFeatures = (seekForm.features || '').split('')
- while (newFeatures.length < 8) newFeatures.push('')
- newFeatures[i - 2] = val
- setSeekForm({...seekForm, features: newFeatures.join('')})
- // 自动跳转下一个
- if (val && i < 9) {
- setTimeout(() => {
- const nextInput = document.querySelector(`input[data-index="${i+1}"]`)
- if (nextInput) nextInput.focus()
- }, 50)
- }
- }}
- onKeyDown={(e) => {
- // 按Delete或Backspace且当前为空时,跳回前一个
- if ((e.key === 'Backspace' || e.key === 'Delete') && !char && i > 2) {
- setTimeout(() => {
- const prevInput = document.querySelector(`input[data-index="${i-1}"]`)
- if (prevInput) prevInput.focus()
- }, 50)
- }
- }}
- style={{ width: '32px', height: '36px', textAlign: 'center', padding: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '4px', color: letterColor, fontSize: '14px', boxSizing: 'border-box' }} />
- })}
-
-
- 备注说明:X=任意数字 A=非4 B=非47 C=非347 D=非247 E=非2347 F=非23457 G=非123457
-
-
-
-
-
- setSeekForm({...seekForm, contact: e.target.value})} placeholder="请输入手机号或微信"
- style={{ width: '100%', padding: '8px', marginTop: '6px', background: '#0f172a', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
-
-
- setSeekForm({...seekForm, title: e.target.value})} />
-
-
- 发布寻号
- setShowSeekPublish(false)} style={{ flex: 1, padding: '12px', background: 'transparent', border: '1px solid #334155', borderRadius: '8px', color: '#94a3b8', fontSize: '14px', cursor: 'pointer' }}>取消
-
-
- )}
-
- {activeTab === 'seek' ? '🔍 寻配号信息' : '💰 成交行情信息'}
- {activeTab === 'seek' && (
-
- setShowSeekPublish(!showSeekPublish)} style={{ padding: '6px 12px', background: showSeekPublish ? '#6b7280' : '#10b981', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>
- '+ 发布寻号'
-
- { setShowMySeeks(true); fetchMySeeks(); }} style={{ padding: '6px 12px', background: '#3b82f6', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>
- 我的寻号
-
-
- )}
-
-
- {loading ? (
-
加载中...
- ) : infoList.length === 0 ? (
-
- 暂无{activeTab === 'seek' ? '寻配号' : '成交行情'}信息
-
- ) : (
-
- {infoList.map(item => {
- // 解析正文中的号码特征和联系方式
- const content = item.content || ''
- const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
- const contactMatch = content.match(/联系方式[:\s]*(.+?)(?:\n|$)/)
- const features = featuresMatch ? featuresMatch[1].trim() : ''
- const contact = contactMatch ? contactMatch[1].trim() : ''
- // 去除正文中的价格、号码特征、联系方式
- const cleanContent = content.replace(/价格[:\s]*.+?(\n|$)/g, '').replace(/号码特征[:\s]*.+?(\n|$)/g, '').replace(/联系方式[:\s]*.+?(\n|$)/g, '').trim()
-
- return (
-
- {/* 标题 */}
-
- {item.title}
-
- {/* 创建日期 + 用户名 */}
-
- 📅 {formatDate(item.created_at)} | 👤 {item.user_name || '匿名用户'}
-
- {/* 号码特征 */}
- {features && (
-
-
号码特征:
-
- {/* 如果features已包含J0则不再重复添加 */}
- {features.startsWith('J0') ? (
- features.split('').map((char, i) => {
- const isDigit = /[0-9]/.test(char)
- const letterColor = isDigit ? '#fff' : ({ X: '#94a3b8', A: '#f472b6', B: '#60a5fa', C: '#34d399', D: '#fbbf24', E: '#a78bfa', F: '#fb923c', G: '#22d3ee' }[char] || '#fff')
- return {char}
- })
- ) : (
- <>
- {'J0'.split('').map((char, i) => (
- {char}
- ))}
- {features.split('').map((char, i) => {
- const isDigit = /[0-9]/.test(char)
- const letterColor = isDigit ? '#fff' : ({ X: '#94a3b8', A: '#f472b6', B: '#60a5fa', C: '#34d399', D: '#fbbf24', E: '#a78bfa', F: '#fb923c', G: '#22d3ee' }[char] || '#fff')
- return {char}
- })}
- >
- )}
-
- {/* 动态显示用到的字母说明 */}
- {features.match(/[XABCFG]/) && (
-
- 备注说明:{(() => {
- const used = []
- if (features.includes('X')) used.push('X=任意数字')
- if (features.includes('A')) used.push('A=非4')
- if (features.includes('B')) used.push('B=非47')
- if (features.includes('C')) used.push('C=非347')
- if (features.includes('D')) used.push('D=非247')
- if (features.includes('E')) used.push('E=非2347')
- if (features.includes('F')) used.push('F=非23457')
- if (features.includes('G')) used.push('G=非123457')
- return used.join(' | ')
- })()}
-
- )}
-
- )}
- {/* 配号结果 - 仅寻配号显示 */}
- {activeTab === 'seek' && (
-
- fetchMatchCollections(item.id)}
- >
- 配号结果: {item.matched_count || 0}条藏品匹配成功
-
-
- )}
- {/* 正文 */}
- {cleanContent && (
-
- {cleanContent}
-
- )}
- {/* 联系方式 */}
- {contact && (
-
- 联系方式:{contact}
-
- )}
-
- )
- })}
-
- )}
-
-
- {/* 匹配藏品列表弹窗 */}
- {showMatchList && (
-
-
-
-
匹配藏品清单
- setShowMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×
-
- {matchCollections.length === 0 ? (
-
暂无匹配藏品
- ) : (
-
- {matchCollections.map(c => (
-
{
- setShowMatchList(false)
- // 跳转到藏品列表页并定位到该藏品
- window.location.hash = `#/list?filter=id&value=${c.id}`
- }}
- >
-
- 编号: {c.code || c.id.substring(0,8)}
-
-
- 冠字号: {c.number}
-
-
- 状态: {c.status === 'in_collection' ? '持仓中' : c.status}
-
-
- ))}
-
- )}
-
-
- )}
-
- {/* 我的寻号弹窗 */}
- {showMySeeks && (
-
-
-
-
我发布的寻号
- setShowMySeeks(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×
-
- {mySeekList.length === 0 ? (
-
暂无发布的寻号
- ) : (
-
- {mySeekList.map(item => (
-
- {editingSeek && editingSeek.id === item.id ? (
- // 编辑模式
-
-
-
-
-
-
-
-
- {['标百', '标十', '单张'].map(t => (
- setEditingSeek({...editingSeek, type: t})}
- style={{ flex: 1, padding: '8px', background: editingSeek.type === t ? '#10b981' : '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', cursor: 'pointer' }}
- >{t}
- ))}
-
-
-
-
-
- setEditingSeek({...editingSeek, matchType: '精准'})}
- style={{ flex: 1, padding: '8px', background: editingSeek.matchType === '精准' ? '#10b981' : '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', cursor: 'pointer' }}
- >精准
- setEditingSeek({...editingSeek, matchType: '模糊'})}
- style={{ flex: 1, padding: '8px', background: editingSeek.matchType === '模糊' ? '#10b981' : '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', cursor: 'pointer' }}
- >模糊
-
-
-
-
- setEditingSeek({...editingSeek, price: e.target.value})}
- placeholder="期望价格"
- style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', marginTop: '4px' }}
- />
-
-
-
-
-
-
- setEditingSeek({...editingSeek, contact: e.target.value})}
- placeholder="手机号或微信"
- style={{ width: '100%', padding: '8px', background: '#1e293b', border: '1px solid #334155', borderRadius: '4px', color: '#fff', marginTop: '4px' }}
- />
-
-
- 保存
- setEditingSeek(null)} style={{ flex: 1, padding: '10px', background: '#6b7280', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>取消
-
-
- ) : (
- // 显示模式
-
-
{item.title}
-
创建于: {new Date(item.created_at).toLocaleDateString()}
-
配号结果: {item.matched_count || 0}条藏品匹配成功
-
{
- // 解析内容提取字段
- const content = item.content || ''
- const contactMatch = content.match(/联系方式[:\s]*(.+?)$/m)
- const priceMatch = content.match(/价格[:\s]*(.+?)$/m)
- setEditingSeek({
- id: item.id,
- title: item.title,
- content: content.split('\n联系方式:')[0],
- edition: item.expect_category || '龙钞',
- type: item.title.match(/标百|标十|单张/)?.[0] || '标百',
- matchType: item.title.includes('精准') ? '精准' : '模糊',
- price: priceMatch ? priceMatch[1].trim() : '',
- contact: contactMatch ? contactMatch[1].trim() : ''
- })
- }} style={{ padding: '4px 8px', background: '#3b82f6', border: 'none', borderRadius: '4px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>编辑
-
- )}
-
- ))}
-
- )}
-
-
- )}
-
- )
-}
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/News_original_backup.jsx b/frontend/src/pages/News_original_backup.jsx
deleted file mode 100644
index bef5194..0000000
--- a/frontend/src/pages/News_original_backup.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React, { useState, useEffect } from 'react'
-
-// 资讯页面 - 展示寻配号和行情信息(所有人可见)
-export default function News() {
- const [activeTab, setActiveTab] = useState('deal')
- const [showSeekPublish, setShowSeekPublish] = useState(false)
- const [showMySeeks, setShowMySeeks] = useState(false)
- const [showMatchList, setShowMatchList] = useState(false)
- const [matchCollections, setMatchCollections] = useState([])
- const [listFilter, setListFilter] = useState('')
- const [currentUserId, setCurrentUserId] = useState('')
- const [matchedItems, setMatchedItems] = useState(() => {
- try { return JSON.parse(localStorage.getItem('matchedSeekItems') || '{}') } catch { return {} }
- })
- const [showCommentId, setShowCommentId] = useState(null)
- const [commentText, setCommentText] = useState('')
- const [comments, setComments] = useState({})
-
- useEffect(() => {
- const token = localStorage.getItem('token')
- if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); setCurrentUserId(payload.sub) } catch {} }
- }, [])
-
- const API_BASE = 'http://47.103.9.192:3000'
-
- return (
-
-
News Page TEST
-
- )
-}
diff --git a/frontend/src/pages/OCR.jsx b/frontend/src/pages/OCR.jsx
deleted file mode 100644
index 0b1229f..0000000
--- a/frontend/src/pages/OCR.jsx
+++ /dev/null
@@ -1,654 +0,0 @@
-// AI OCR 识别页面 - 支持拍照识别和手工录入
-import React, { useState, useEffect, useRef } from 'react'
-
-// 字段转换函数:camelCase → 字段编码
-const convertField = (obj) => {
- const map = {
- // f99 系统字段
- id: 'f99_90_id',
- userId: 'f99_91_user_id',
- // f01 基本信息
- name: 'f01_01_name',
- code: 'f01_02_code',
- category: 'f01_03_category',
- status: 'f01_04_status',
- remark: 'f01_05_remark',
- // f02 详细字段
- prefixSerial: 'f02_10_prefix_serial',
- version: 'f02_11_version',
- packaging: 'f02_12_packaging',
- rarity: 'f02_13_rarity',
- // f03 评级信息
- isGraded: 'f03_20_is_graded',
- gradingCompany: 'f03_21_grading_company',
- gradingScore: 'f03_22_grading_score',
- threeStar: 'f03_23_three_star',
- // f04 特殊信息
- specialMark: 'f04_30_special_mark',
- serialFeature: 'f04_31_serial_feature',
- issuer: 'f04_32_issuer',
- issueYear: 'f04_33_issue_year',
- material: 'f04_34_material',
- denomination: 'f04_35_denomination',
- issueQuantity: 'f04_36_issue_quantity',
- // f05 价格信息
- costPrice: 'f05_40_cost_price',
- targetPrice: 'f05_41_target_price',
- goalPrice: 'f05_42_goal_price',
- repairFee: 'f05_43_repair_fee',
- gradingFee: 'f05_44_grading_fee',
- // f06 其他信息
- purpose: 'f06_50_purpose'
- }
- const result = {}
- const numberFields = ['costPrice', 'targetPrice', 'goalPrice', 'repairFee', 'gradingFee']
- for (const key in obj) {
- let value = obj[key]
- if (value === '' || value === null) {
- value = null
- } else if (numberFields.includes(key)) {
- value = parseFloat(value)
- if (isNaN(value)) value = null
- }
- result[map[key] || key] = value
- }
- return result
-}
-
-// 通用 Input 组件
-const Input = ({ form, handleChange, label, field, type = 'text', options = null, disabled = false }) => {
- const inputRef = useRef(null)
- const handleFocus = () => {
- setTimeout(() => {
- inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
- }, 100)
- }
-
- const onChange = (e) => {
- const value = e.target.value
- if (type === 'number' && value !== '') {
- const num = parseFloat(value)
- handleChange(field, isNaN(num) ? '' : num)
- } else {
- handleChange(field, value)
- }
- }
-
- return (
-
-
{label}
- {options ? (
-
- ) : (
-
- )}
-
- )
-}
-
-// 默认表单数据
-const getDefaultForm = () => ({
- name: '龙钞',
- code: '',
- category: '自持',
- rarity: '通货',
- prefixSerial: '',
- version: '2024 龙',
- denomination: '',
- status: 'in_collection',
- packaging: '单张',
- material: '',
- issueQuantity: '',
- purpose: '收藏',
- isGraded: false,
- gradingCompany: '',
- gradingScore: '',
- threeStar: false,
- specialMark: '',
- serialFeature: '',
- issuer: '中国人民银行',
- issueYear: '',
- costPrice: '',
- targetPrice: '',
- goalPrice: '',
- repairFee: '',
- gradingFee: '',
- remark: ''
-})
-
-export default function OCR() {
- const [activeTab, setActiveTab] = useState('ai') // ai, manual, batch
- const [mode, setMode] = useState('camera') // camera, form
- const [form, setForm] = useState(getDefaultForm())
- const [saving, setSaving] = useState(false)
- const [error, setError] = useState('')
- const [recognizing, setRecognizing] = useState(false)
- const [selectedImage, setSelectedImage] = useState(null)
- const [imagePreview, setImagePreview] = useState(null)
- const fileInputRef = useRef(null)
- const videoRef = useRef(null)
- const canvasRef = useRef(null)
-
- // 选项定义
- const statusOptions = [
- { value: 'in_collection', label: '收藏中' },
- { value: 'selling', label: '出售中' },
- { value: 'sold', label: '已售' },
- { value: 'grading', label: '送评中' },
- { value: 'repairing', label: '修复中' },
- { value: 'transit', label: '在途中' },
- { value: 'other', label: '其他' }
- ]
-
- const categoryOptions = [
- { value: '自持', label: '自持' },
- { value: '寄存', label: '寄存' },
- { value: '寄售', label: '寄售' },
- { value: '共有', label: '共有' },
- { value: '寻号', label: '寻号' },
- { value: '其他', label: '其他' }
- ]
-
- const rarityOptions = [
- { value: '通货', label: '通货' },
- { value: '特色', label: '特色' },
- { value: '少见', label: '少见' },
- { value: '稀有', label: '稀有' },
- { value: '珍品', label: '珍品' },
- { value: '孤品', label: '孤品' }
- ]
-
- const packagingOptions = [
- { value: '标十', label: '标十' },
- { value: '标百', label: '标百' },
- { value: '单张', label: '单张' },
- { value: '裸钞', label: '裸钞' }
- ]
-
- const handleChange = (key, value) => {
- setForm({ ...form, [key]: value })
- if (key === 'targetPrice' && value) {
- setForm(prev => ({ ...prev, status: 'sold' }))
- }
- }
-
- // 选择图片
- const handleSelectImage = (e) => {
- const file = e.target.files[0]
- if (!file) return
-
- setSelectedImage(file)
- const reader = new FileReader()
- reader.onload = (e) => {
- setImagePreview(e.target.result)
- }
- reader.readAsDataURL(file)
- }
-
- // 调用 OCR 识别
- const handleRecognize = async () => {
- if (!selectedImage) {
- setError('请先选择图片')
- return
- }
-
- setRecognizing(true)
- setError('')
-
- const token = localStorage.getItem('token')
- const formData = new FormData()
- formData.append('image', selectedImage)
-
- // 使用 XMLHttpRequest 替代 fetch,兼容性更好
- const data = await new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest()
- xhr.open('POST', '/api/ocr/recognize')
- xhr.setRequestHeader('Authorization', 'Bearer ' + token)
-
- xhr.onload = function() {
- if (xhr.status >= 200 && xhr.status < 300) {
- try {
- const data = JSON.parse(xhr.responseText)
- resolve(data)
- } catch (e) {
- reject(new Error('JSON解析失败: ' + xhr.responseText.substring(0, 100)))
- }
- } else {
- try {
- const data = JSON.parse(xhr.responseText)
- reject(new Error(data.error?.message || data.detail || '识别失败'))
- } catch (e) {
- reject(new Error('请求失败: ' + xhr.status))
- }
- }
- }
-
- xhr.onerror = function() {
- reject(new Error('网络错误'))
- }
-
- xhr.send(formData)
- })
-
- try {
- // 自动填充识别结果
- if (data.fields) {
- const recognizedForm = { ...getDefaultForm() }
-
- // 映射识别字段到表单字段
- if (data.fields.name) recognizedForm.name = data.fields.name
- if (data.fields.code) recognizedForm.code = data.fields.code
- if (data.fields.version) recognizedForm.version = data.fields.version
- if (data.fields.prefix_serial) recognizedForm.prefixSerial = data.fields.prefix_serial
- if (data.fields.denomination) recognizedForm.denomination = data.fields.denomination
- if (data.fields.material) recognizedForm.material = data.fields.material
- if (data.fields.issue_year) recognizedForm.issueYear = data.fields.issue_year
- if (data.fields.issuer) recognizedForm.issuer = data.fields.issuer
- if (data.fields.cost_price) recognizedForm.costPrice = data.fields.cost_price
- if (data.fields.remark) recognizedForm.remark = data.fields.remark
-
- setForm(recognizedForm)
- setMode('form')
- alert('识别成功!请检查并完善信息')
- } else {
- setError('识别结果为空')
- }
- } catch (e) {
- console.error('OCR 识别失败:', e)
- setError('识别失败:' + e.message)
- } finally {
- setRecognizing(false)
- }
- }
-
- // 保存藏品
- const handleSave = async () => {
- if (!form.name || !form.version) {
- setError('名称和版别为必填项')
- return
- }
-
- setSaving(true)
- setError('')
-
- const token = localStorage.getItem('token')
- const formData = convertField(form)
-
- try {
- const res = await fetch('/api/collections', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': 'Bearer ' + token
- },
- body: JSON.stringify(formData)
- })
-
- if (!res.ok) {
- const data = await res.json()
- throw new Error(data.error?.message || data.detail || '保存失败')
- }
-
- alert('保存成功!')
- window.location.hash = '#/list'
- window.refreshList?.()
- window.refreshHome?.()
- } catch (e) {
- alert('保存失败:' + e.message)
- } finally {
- setSaving(false)
- }
- }
-
- // 拍照模式界面
- if (mode === 'camera') {
- return (
-
- {/* 顶部标签切换 */}
-
-
- setActiveTab('ai')}
- style={{
- flex: 1,
- padding: '10px',
- background: activeTab === 'ai' ? '#fbbf24' : 'rgba(255,255,255,0.05)',
- color: activeTab === 'ai' ? '#1e293b' : '#fff',
- border: 'none',
- borderRadius: '8px',
- fontSize: '14px',
- fontWeight: '600',
- cursor: 'pointer'
- }}
- >
- 🤖 AI 识别
-
- {
- setActiveTab('manual');
- setMode('form');
- }}
- style={{
- flex: 1,
- padding: '10px',
- background: activeTab === 'manual' ? '#fbbf24' : 'rgba(255,255,255,0.05)',
- color: activeTab === 'manual' ? '#1e293b' : '#fff',
- border: 'none',
- borderRadius: '8px',
- fontSize: '14px',
- fontWeight: '600',
- cursor: 'pointer'
- }}
- >
- ✍️ 手工录入
-
- setActiveTab('batch')}
- disabled
- style={{
- flex: 1,
- padding: '10px',
- background: 'rgba(255,255,255,0.02)',
- color: '#64748b',
- border: '1px solid rgba(255,255,255,0.05)',
- borderRadius: '8px',
- fontSize: '14px',
- fontWeight: '600',
- cursor: 'not-allowed'
- }}
- >
- 📦 批量录入
-
-
-
-
- {/* 内容区域 */}
- {activeTab === 'ai' && (
-
-
- {/* 图片选择区域 */}
-
-
-
- {imagePreview ? (
-
-

-
- { setSelectedImage(null); setImagePreview(null); }}
- style={{
- padding: '12px 24px',
- background: 'rgba(239, 68, 68, 0.2)',
- color: '#ef4444',
- border: '1px solid #ef4444',
- borderRadius: '8px',
- fontSize: '14px',
- cursor: 'pointer'
- }}
- >
- 🗑️ 重新选择
-
-
- {recognizing ? '🔍 识别中...' : '🤖 开始识别'}
-
-
-
- ) : (
-
-
{
- fileInputRef.current.setAttribute('capture', '');
- fileInputRef.current.setAttribute('accept', 'image/*');
- fileInputRef.current.click();
- }}
- style={{
- padding: '24px',
- background: 'rgba(59, 130, 246, 0.1)',
- border: '2px dashed rgba(59, 130, 246, 0.5)',
- borderRadius: '12px',
- cursor: 'pointer',
- marginBottom: '16px',
- textAlign: 'center'
- }}
- >
-
📷
-
- 拍照识别
-
-
- 使用相机拍照并识别
-
-
-
-
{
- fileInputRef.current.removeAttribute('capture');
- fileInputRef.current.setAttribute('accept', 'image/*');
- fileInputRef.current.click();
- }}
- style={{
- padding: '24px',
- background: 'rgba(34, 197, 94, 0.1)',
- border: '2px dashed rgba(34, 197, 94, 0.5)',
- borderRadius: '12px',
- cursor: 'pointer',
- textAlign: 'center'
- }}
- >
-
🖼️
-
- 从相册选择
-
-
- 从相册选择已有图片
-
-
-
- )}
-
-
- {/* 错误提示 */}
- {error && (
-
- ⚠️ {error}
-
- )}
-
- {/* 提示信息 */}
-
-
💡 识别说明
-
- - 支持拍照或从相册选择图片
- - 自动识别名称、版别、冠字序号等字段
- - 识别结果可手动修改完善
- - 建议拍摄清晰、光线充足的正面照片
-
-
-
-
- )
- }
-
- // 表单模式界面
- return (
-
- {/* 顶部 */}
-
-
setMode('camera')}
- style={{
- background: 'rgba(255,255,255,0.1)',
- color: '#fff',
- border: 'none',
- borderRadius: '6px',
- width: '36px',
- height: '36px',
- fontSize: '20px',
- cursor: 'pointer'
- }}
- >
- ←
-
-
-
-
- {error && (
-
- ⚠️ {error}
-
- )}
-
-
- {/* 基本信息 */}
-
-
- {/* 价格信息 */}
-
-
- {/* 备注 */}
-
-
- {/* 保存按钮 */}
-
- {saving ? '保存中...' : '✅ 保存藏品'}
-
-
-
setMode('camera')}
- style={{
- width: '100%',
- padding: '14px',
- background: 'rgba(255,255,255,0.05)',
- color: '#94a3b8',
- border: '1px solid rgba(255,255,255,0.1)',
- borderRadius: '10px',
- fontSize: '14px',
- cursor: 'pointer'
- }}
- >
- 🔄 重新识别
-
-
-
- )
-}
diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx
index 97ee369..666341d 100644
--- a/frontend/src/pages/Stats.jsx
+++ b/frontend/src/pages/Stats.jsx
@@ -103,7 +103,7 @@ export default function Stats() {
status: { 'in_collection': '#22c55e', 'selling': '#f59e0b', 'sold': '#ef4444', 'grading': '#8b5cf6', 'repairing': '#f97316', 'transit': '#06b6d4', 'seeking': '#ec4899' },
category: { '自持': '#22c55e', '寄存': '#3b82f6', '寄售': '#f59e0b', '共有': '#8b5cf6', '寻号': '#06b6d4', '生肖钞': '#22c55e', '其他': '#64748b' },
profitLoss: { 'profit': '#22c55e', 'loss': '#ef4444' },
- numberCategory: { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '无4': '#06b6d4', '带4': '#22c55e', '其他': '#64748b' },
+ numberCategory: { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '带7号': '#06b6d4', '带4号': '#22c55e', '其他': '#64748b' },
version: {},
gradingCompany: {},
gradingScore: {},
@@ -146,7 +146,7 @@ export default function Stats() {
return Number(val).toLocaleString('zh-CN')
}
- const numberCategoryOrder = ['无2347', '无347', '无247', '无47', '无4', '带4', '其他']
+ const numberCategoryOrder = ['无2347', '无347', '无247', '无47', '带7号', '带4号', '其他']
const getSortedData = (data, type) => {
if (type === 'numberCategory') {
return [...data].sort((a, b) => {
diff --git a/frontend/src/pages/YichensBoard.jsx b/frontend/src/pages/YichensBoard.jsx
new file mode 100644
index 0000000..85d80e0
--- /dev/null
+++ b/frontend/src/pages/YichensBoard.jsx
@@ -0,0 +1,255 @@
+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(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 today = new Date()
+ const dateStr = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate()
+
+ useEffect(() => {
+ console.log('YichensBoard: 开始加载数据')
+ fetchTodayStats()
+ }, [])
+
+ const fetchTodayStats = async () => {
+ try {
+ 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 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/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() || []
+ // 确保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('马'))
+ }
+ }
+ // 确保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('马'))
+ }
+ }
+ // 支持新格式 {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) {
+ fetchPosts(1, categoryFilter, searchKeyword)
+ }
+ }, [todayStats, categoryFilter, postTypeFilter])
+
+ useEffect(() => {
+ setPage(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',
+ textAlign: 'center'
+ }}>
+
{label}
+
{value}
+
+ )
+
+ if (error) {
+ return (
+
+
加载失败: {error}
+
{ setError(null); fetchTodayStats(); }} style={{ marginTop: 10, padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>
+ 重试
+
+
+ )
+ }
+
+ return (
+
+
+
+ 📈 连体钞/纪念钞 {dateStr}
+
+
+ { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); }} />
+ { setPostTypeFilter('deal'); setCategoryFilter(''); setPage(1); }} />
+ { setPostTypeFilter('want'); setCategoryFilter(''); setPage(1); }} />
+ { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); }} />
+
+
+
+
+
📊 今日分类统计
+
龙: {todayStats.dragons || 0} | 马: {todayStats.horses || 0} | 蛇: {todayStats.snakes || 0} | 天马: {todayStats.tianma || 0}
+
+
+
+
+ { 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' }}
+ />
+ { setSearchKeyword(''); setPage(1); fetchPosts(1, '') }}
+ style={{ padding: '10px 16px', borderRadius: 8, border: 'none', background: '#4b5563', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
+ 清空
+
+
+ {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); }}
+ 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); }}
+ 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.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)||''}
+
+
+ {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, searchKeyword) }} 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/pages/news.py b/frontend/src/pages/news.py
new file mode 100644
index 0000000..c969522
--- /dev/null
+++ b/frontend/src/pages/news.py
@@ -0,0 +1,128 @@
+from fastapi import APIRouter, Depends, HTTPException, Query
+from sqlalchemy import Table, MetaData
+from sqlalchemy.orm import Session
+from pydantic import BaseModel
+from typing import Optional, List
+from datetime import datetime, date
+from app.core.database import get_db, engine
+from app.models.models import User
+from app.routers.auth import get_current_user
+
+router = APIRouter(prefix="/api/news", tags=["资讯"])
+metadata = MetaData()
+
+# 分类表
+categories_table = Table('news_categories', metadata, autoload_with=engine)
+news_table = Table('news', metadata, autoload_with=engine)
+user_posts_table = Table('user_posts', metadata, autoload_with=engine)
+users_table = Table('users', metadata, autoload_with=engine)
+deals_table = Table('deals', metadata, autoload_with=engine)
+notifications_table = Table('notifications', metadata, autoload_with=engine)
+
+# ============ 获取分类 ============
+@router.get("/categories")
+def get_categories(db: Session = Depends(get_db)):
+ results = db.query(categories_table).order_by(categories_table.c.sort_order).all()
+ return [dict(r._mapping) for r in results]
+
+# ============ 获取资讯 ============
+@router.get("")
+def get_news(
+ category_id: Optional[int] = None,
+ page: int = 1,
+ limit: int = 20,
+ db: Session = Depends(get_db)
+):
+ query = db.query(news_table)
+ if category_id:
+ query = query.filter(news_table.c.category_id == category_id)
+ offset = (page - 1) * limit
+ results = query.order_by(news_table.c.created_at.desc()).offset(offset).limit(limit).all()
+ return [dict(r._mapping) for r in results]
+
+# ============ 获取用户发布 ============
+@router.get("/posts")
+def get_posts(
+ post_type: Optional[str] = None,
+ status: str = "active",
+ page: int = 1,
+ limit: int = 20,
+ db: Session = Depends(get_db)
+):
+ query = db.query(user_posts_table).filter(user_posts_table.c.status == status)
+ if post_type:
+ query = query.filter(user_posts_table.c.post_type == post_type)
+ offset = (page - 1) * limit
+ results = query.order_by(user_posts_table.c.created_at.desc()).offset(offset).limit(limit).all()
+ return [dict(r._mapping) for r in results]
+
+# ============ 创建发布 ============
+class PostCreate(BaseModel):
+ post_type: str
+ title: str
+ content: Optional[str] = None
+ zodiac_type: Optional[str] = None
+ packaging: Optional[str] = None
+
+@router.post("/posts")
+def create_post(
+ post: PostCreate,
+ current_user: User = Depends(get_current_user),
+ db: Session = Depends(get_db)
+):
+ result = db.execute(user_posts_table.insert().values(
+ user_id=current_user.f99_90_id,
+ post_type=post.post_type,
+ title=post.title,
+ content=post.content,
+ zodiac_type=post.zodiac_type,
+ packaging=post.packaging,
+ status="pending"
+ ))
+ db.commit()
+ return {"success": True, "id": result.inserted_primary_key[0]}
+
+# ============ 成交数据 ============
+@router.get("/deals")
+def get_deals(
+ zodiac_type: Optional[str] = None,
+ limit: int = 20,
+ db: Session = Depends(get_db)
+):
+ query = db.query(deals_table)
+ if zodiac_type:
+ query = query.filter(deals_table.c.zodiac_type == zodiac_type)
+ results = query.order_by(deals_table.c.deal_date.desc()).limit(limit).all()
+ return [dict(r._mapping) for r in results]
+
+# ============ 通知 ============
+@router.get("/notifications")
+def get_notifications(limit: int = 10, db: Session = Depends(get_db)):
+ results = db.query(notifications_table).filter(
+ notifications_table.c.is_published == True
+ ).order_by(notifications_table.c.created_at.desc()).limit(limit).all()
+ return [dict(r._mapping) for r in results]
+
+# ============ 首页数据 ============
+@router.get("/home")
+def get_home(db: Session = Depends(get_db)):
+ # 推荐发布
+ posts = db.query(user_posts_table).filter(
+ user_posts_table.c.status == "active"
+ ).order_by(user_posts_table.c.created_at.desc()).limit(10).all()
+
+ # 成交
+ deals = db.query(deals_table).order_by(
+ deals_table.c.deal_date.desc()
+ ).limit(10).all()
+
+ # 通知
+ notices = db.query(notifications_table).filter(
+ notifications_table.c.is_published == True
+ ).order_by(notifications_table.c.created_at.desc()).limit(5).all()
+
+ return {
+ "posts": [dict(p._mapping) for p in posts],
+ "deals": [dict(d._mapping) for d in deals],
+ "notices": [dict(n._mapping) for n in notices]
+ }
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
diff --git a/start.sh b/start.sh
new file mode 100755
index 0000000..70bca6b
--- /dev/null
+++ b/start.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+cd /root/jiachenlong/backend
+export DATABASE_URL='postgresql://jiachenlong:Passwd1%403@pgm-bp1t5w248t7s1pvr.pg.rds.aliyuncs.com:5432/jiachenlong'
+export SECRET_KEY=production-secret-key-b-env-20260401
+export OSS_ACCESS_KEY_ID=LTAI5t6HUnpFBLEK9194kPVG
+export OSS_ACCESS_KEY_SECRET=LEr4Q8yRxb8D5b24cKfCwlt4MMoke1
+export SMS_ACCESS_KEY_ID=LTAI5t86bc1nNKVNyYv4Af6x
+export SMS_ACCESS_KEY_SECRET=92EVAIE3GECr214c9UaSF6TSYJvDLY
+export SMS_SIGN_NAME=苏州双人旁
+export SMS_TEMPLATE_CODE=SMS_505015231
+export DASHSCOPE_API_KEY=sk-9389024a37da4f7bb455ac9a6b28776f
+nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 3000 --workers 1 > /tmp/uvicorn.log 2>&1 &
+echo 'B后端服务已启动'