Compare commits
71 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
a9a68de436 | |
|
|
36b467895c | |
|
|
591db03ee1 | |
|
|
fb987dc5b9 | |
|
|
d578289c7c | |
|
|
b0f28466c8 | |
|
|
57e51f6c28 | |
|
|
524601afc3 | |
|
|
58c2291e83 | |
|
|
4ae29eb4bf | |
|
|
257ca40079 | |
|
|
aec6ef7c79 | |
|
|
ea6528b7f3 | |
|
|
e381427138 | |
|
|
0ffc10ad9a | |
|
|
c1f3b9bd3c | |
|
|
d2c4d5bf5f | |
|
|
38f1da12b5 | |
|
|
2204e3a2af | |
|
|
e45899db0c | |
|
|
9fe3f77d45 | |
|
|
2306e35dec | |
|
|
5eb234443b | |
|
|
6d7ba6e097 | |
|
|
161ff51b72 | |
|
|
64eb4b921c | |
|
|
238383d3d8 | |
|
|
2b1044ddb2 | |
|
|
e1bed251fa | |
|
|
9faa2d8069 | |
|
|
810a36d757 | |
|
|
4f6de4d3ac | |
|
|
afa217e1b5 | |
|
|
73de6a1f85 | |
|
|
e8ac05decd | |
|
|
5484d12557 | |
|
|
dced540eb8 | |
|
|
926c12c787 | |
|
|
36527d033f | |
|
|
615be9805b | |
|
|
0093367452 | |
|
|
d5180d01b6 | |
|
|
285929024b | |
|
|
55f0c9aa23 | |
|
|
39e5d44604 | |
|
|
12c35e39db | |
|
|
f038a397cc | |
|
|
e2c0765623 | |
|
|
2b2b916f91 | |
|
|
99d34f86c4 | |
|
|
74589883b6 | |
|
|
bd22c20108 | |
|
|
acac7922af | |
|
|
abc8c7bd08 | |
|
|
550fc8f7da | |
|
|
b40f5c44a6 | |
|
|
448b3a1ba7 | |
|
|
dacd023523 | |
|
|
0dee8a9d53 | |
|
|
7e19e2a55d | |
|
|
6401fc57ea | |
|
|
49b68e802e | |
|
|
e5a6a5928e | |
|
|
e5a5a2c48c | |
|
|
a4be63512c | |
|
|
36f617266d | |
|
|
d6fd86bbfa | |
|
|
a46d63ab01 | |
|
|
022e81c800 | |
|
|
49795690af | |
|
|
723e5a2500 |
|
|
@ -32,6 +32,3 @@ Thumbs.db
|
|||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# 版本文件
|
||||
VERSION
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
VERSION=1.2.63
|
||||
1.2.98
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ 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():
|
||||
|
|
@ -84,6 +86,8 @@ app.include_router(users_router.router) # 当前用户接口
|
|||
app.include_router(users_router.admin_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("/")
|
||||
|
|
@ -106,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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -44,7 +44,8 @@ class User(Base):
|
|||
f99_100_points = Column(Integer, default=0) # 积分
|
||||
f01_11_balance = Column(Float, default=0) # 余额
|
||||
f01_12_total_amount = Column(Float, default=0) # 累计金额
|
||||
f01_13_invite_code = Column(String(20), nullable=True) # 邀请码
|
||||
f01_13_invite_code = Column(String(20), nullable=True) # 邀请码(自己的邀请码)
|
||||
f99_101_invited_count = Column(Integer, default=0) # 通过自己邀请码注册的用户数量
|
||||
|
||||
collections = relationship("Collection", back_populates="user", cascade="all, delete-orphan")
|
||||
operations = relationship("Operation", back_populates="user", cascade="all, delete-orphan")
|
||||
|
|
@ -179,6 +180,16 @@ class Information(Base):
|
|||
deal_price = Column(Float, nullable=True)
|
||||
deal_date = Column(Date, nullable=True)
|
||||
|
||||
# 评级相关字段
|
||||
packaging = Column(String(50), nullable=True)
|
||||
is_graded = Column(Boolean, default=False)
|
||||
grading_company = Column(String(100), nullable=True)
|
||||
grading_score = Column(String(50), nullable=True)
|
||||
category = Column(String(100), nullable=True)
|
||||
|
||||
# 行情编号
|
||||
deal_no = Column(String(50), nullable=True, index=True)
|
||||
|
||||
# 状态: active-有效, closed-已关闭, expired-已过期
|
||||
status = Column(String(20), default="active", index=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
@ -1,197 +1,489 @@
|
|||
# 认证路由 - 使用字段编码
|
||||
# auth - 认证路由
|
||||
# Version: 1.2.90
|
||||
# 更新:
|
||||
|
||||
from fastapi import APIRouter
|
||||
# Version: 1.2.x
|
||||
# 更新:
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Body
|
||||
# 更新:
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
# 更新:
|
||||
from sqlalchemy.orm import Session
|
||||
# 更新:
|
||||
from app.core.database import get_db
|
||||
# 更新:
|
||||
from app.core.auth import verify_password, create_access_token, get_password_hash, get_current_user
|
||||
# 更新:
|
||||
from app.models.models import User
|
||||
# 更新:
|
||||
from app.schemas.schemas import Token, UserCreate, UserResponse
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
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)):
|
||||
# 更新:
|
||||
"""用户注册"""
|
||||
# 更新:
|
||||
# 检查用户名是否已存在
|
||||
# 更新:
|
||||
existing_user = db.query(User).filter(User.f01_01_name == user_data.f01_01_name).first()
|
||||
# 更新:
|
||||
if existing_user:
|
||||
# 更新:
|
||||
raise HTTPException(
|
||||
# 更新:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
# 更新:
|
||||
detail="f01_01_name: 用户名已存在"
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 检查邮箱是否已存在
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 检查手机号是否已存在
|
||||
# 更新:
|
||||
if user_data.phone:
|
||||
# 更新:
|
||||
existing_phone = db.query(User).filter(User.phone == user_data.phone).first()
|
||||
# 更新:
|
||||
if existing_phone:
|
||||
# 更新:
|
||||
raise HTTPException(
|
||||
# 更新:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
# 更新:
|
||||
detail="E00040:该手机号已被注册,请更换手机号"
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
if user_data.email:
|
||||
# 更新:
|
||||
existing_email = db.query(User).filter(User.email == user_data.email).first()
|
||||
# 更新:
|
||||
if existing_email:
|
||||
# 更新:
|
||||
raise HTTPException(
|
||||
# 更新:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
# 更新:
|
||||
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,
|
||||
# 更新:
|
||||
avatar=user_data.avatar,
|
||||
# 更新:
|
||||
address=user_data.address,
|
||||
# 更新:
|
||||
bio=user_data.bio,
|
||||
# 更新:
|
||||
password=hashed_password,
|
||||
# 更新:
|
||||
role="user"
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
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
|
||||
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
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:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="E00011: 用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 验证密码
|
||||
if not verify_password(form_data.password, user.password):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="E00011: 用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 更新登录次数和最后登录时间
|
||||
from datetime import datetime
|
||||
user.f99_98_login_count = (user.f99_98_login_count or 0) + 1
|
||||
user.f99_99_last_login = datetime.now()
|
||||
db.commit()
|
||||
|
||||
# 生成 token
|
||||
access_token = create_access_token(data={"sub": user.f99_90_id})
|
||||
|
||||
# 更新:
|
||||
# 返回用户信息(避免Pydantic序列化问题)
|
||||
# 更新:
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer"
|
||||
# 更新:
|
||||
"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.get("/me", response_model=UserResponse)
|
||||
def get_current_user_info(
|
||||
current_user: User = Depends(lambda: None)
|
||||
):
|
||||
"""获取当前用户信息"""
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="请使用正确的依赖注入"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
def change_password(
|
||||
old_password: str = Body(...),
|
||||
new_password: str = Body(...),
|
||||
current_user: User = Depends(get_current_user),
|
||||
# 更新:
|
||||
@router.post("/login", response_model=Token)
|
||||
# 更新:
|
||||
def login(
|
||||
# 更新:
|
||||
form_data: OAuth2PasswordRequestForm = Depends(),
|
||||
# 更新:
|
||||
db: Session = Depends(get_db)
|
||||
# 更新:
|
||||
):
|
||||
"""修改当前用户密码"""
|
||||
from app.core.auth import verify_password, get_password_hash
|
||||
|
||||
# 在当前session中重新查询用户
|
||||
user = db.query(User).filter(User.f99_90_id == current_user.f99_90_id).first()
|
||||
# 更新:
|
||||
"""用户登录 - 支持用户名或用户编码登录"""
|
||||
# 更新:
|
||||
# 先尝试用户名登录
|
||||
# 更新:
|
||||
user = db.query(User).filter(User.f01_01_name == form_data.username).first()
|
||||
# 更新:
|
||||
# 如果用户名不存在,尝试用户编码登录
|
||||
# 更新:
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 验证旧密码
|
||||
if not verify_password(old_password, user.password):
|
||||
# 更新:
|
||||
user = db.query(User).filter(User.user_code == form_data.username).first()
|
||||
# 更新:
|
||||
if not user:
|
||||
# 更新:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="当前密码错误"
|
||||
# 更新:
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
# 更新:
|
||||
detail="E00011: 用户名或密码错误",
|
||||
# 更新:
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
|
||||
# 更新密码
|
||||
user.password = get_password_hash(new_password)
|
||||
# 更新:
|
||||
# 验证密码
|
||||
# 更新:
|
||||
if not verify_password(form_data.password, user.password):
|
||||
# 更新:
|
||||
raise HTTPException(
|
||||
# 更新:
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
# 更新:
|
||||
detail="E00011: 用户名或密码错误",
|
||||
# 更新:
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 更新登录次数和最后登录时间
|
||||
# 更新:
|
||||
from datetime import datetime
|
||||
# 更新:
|
||||
user.f99_98_login_count = (user.f99_98_login_count or 0) + 1
|
||||
# 更新:
|
||||
user.f99_99_last_login = datetime.now()
|
||||
# 更新:
|
||||
db.commit()
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 生成 token
|
||||
# 更新:
|
||||
access_token = create_access_token(data={"sub": user.f99_90_id})
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
return {
|
||||
# 更新:
|
||||
"access_token": access_token,
|
||||
# 更新:
|
||||
"token_type": "bearer"
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
# 更新:
|
||||
def get_current_user_info(
|
||||
# 更新:
|
||||
current_user: User = Depends(lambda: None)
|
||||
# 更新:
|
||||
):
|
||||
# 更新:
|
||||
"""获取当前用户信息"""
|
||||
# 更新:
|
||||
raise HTTPException(
|
||||
# 更新:
|
||||
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
# 更新:
|
||||
detail="请使用正确的依赖注入"
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
@router.post("/change-password")
|
||||
# 更新:
|
||||
def change_password(
|
||||
# 更新:
|
||||
old_password: str = Body(...),
|
||||
# 更新:
|
||||
new_password: str = Body(...),
|
||||
# 更新:
|
||||
current_user: User = Depends(get_current_user),
|
||||
# 更新:
|
||||
db: Session = Depends(get_db)
|
||||
# 更新:
|
||||
):
|
||||
# 更新:
|
||||
"""修改当前用户密码"""
|
||||
# 更新:
|
||||
from app.core.auth import verify_password, get_password_hash
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 在当前session中重新查询用户
|
||||
# 更新:
|
||||
user = db.query(User).filter(User.f99_90_id == current_user.f99_90_id).first()
|
||||
# 更新:
|
||||
if not user:
|
||||
# 更新:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 验证旧密码
|
||||
# 更新:
|
||||
if not verify_password(old_password, user.password):
|
||||
# 更新:
|
||||
raise HTTPException(
|
||||
# 更新:
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
# 更新:
|
||||
detail="当前密码错误"
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 更新密码
|
||||
# 更新:
|
||||
user.password = get_password_hash(new_password)
|
||||
# 更新:
|
||||
db.commit()
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
return {"message": "密码修改成功"}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# ============ 短信验证码接口 ============
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
@router.post("/send-verification-code")
|
||||
# 更新:
|
||||
def send_verification_code(
|
||||
# 更新:
|
||||
phone: str = Body(..., min_length=11, max_length=11),
|
||||
# 更新:
|
||||
purpose: str = Body("register") # register | login | reset_password
|
||||
# 更新:
|
||||
):
|
||||
# 更新:
|
||||
"""发送短信验证码"""
|
||||
# 更新:
|
||||
from app.services.sms import send_verification_code as send_sms
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 验证手机号格式
|
||||
# 更新:
|
||||
if not phone.startswith("1") or len(phone) != 11:
|
||||
# 更新:
|
||||
return {"success": False, "message": "手机号格式不正确"}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
result = send_sms(phone)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if result["success"]:
|
||||
# 更新:
|
||||
return {
|
||||
# 更新:
|
||||
"success": True,
|
||||
# 更新:
|
||||
"message": f"验证码已发送到 {phone[:3]}****{phone[7:]}",
|
||||
# 更新:
|
||||
"expire": result.get("expire", 300)
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
else:
|
||||
# 更新:
|
||||
return result
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
@router.post("/verify-code")
|
||||
# 更新:
|
||||
def verify_code(
|
||||
# 更新:
|
||||
phone: str = Body(...),
|
||||
# 更新:
|
||||
code: str = Body(..., min_length=6, max_length=6)
|
||||
# 更新:
|
||||
):
|
||||
# 更新:
|
||||
"""验证短信验证码(仅验证,不执行后续操作)"""
|
||||
# 更新:
|
||||
from app.services.sms import verify_code as check_code
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
is_valid = check_code(phone, code)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if is_valid:
|
||||
# 更新:
|
||||
return {"success": True, "message": "验证成功"}
|
||||
# 更新:
|
||||
else:
|
||||
# 更新:
|
||||
return {"success": False, "message": "验证码错误或已过期"}
|
||||
# 更新:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,502 @@
|
|||
# deal - 成交行情路由
|
||||
# Version: 1.2.85
|
||||
# 更新:
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
# 更新:
|
||||
# Version: 1.2.x
|
||||
# 更新:
|
||||
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": "删除成功"}
|
||||
# 更新:
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,128 +1,262 @@
|
|||
# news - 新闻路由
|
||||
# Version: 1.2.80
|
||||
# 更新:
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
# 更新:
|
||||
# Version: 1.2.x
|
||||
# 更新:
|
||||
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]
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
|
|
|||
|
|
@ -1,384 +1,770 @@
|
|||
# OCR 识别路由 - 专业人民币生肖纪念钞鉴定
|
||||
# ocr - OCR识别路由
|
||||
# Version: 1.2.75
|
||||
# 更新:
|
||||
|
||||
import os
|
||||
# 更新:
|
||||
import uuid
|
||||
# 更新:
|
||||
import base64
|
||||
# 更新:
|
||||
import httpx
|
||||
# 更新:
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||
# 更新:
|
||||
from sqlalchemy.orm import Session
|
||||
# 更新:
|
||||
from app.core.database import get_db
|
||||
# 更新:
|
||||
from app.core.auth import get_current_user
|
||||
# 更新:
|
||||
from app.models.models import User
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
router = APIRouter(prefix="/api/ocr", tags=["OCR 识别"])
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 阿里云 DashScope API 配置
|
||||
# 更新:
|
||||
DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY", "sk-9389024a37da4f7bb455ac9a6b28776f")
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 阿里云 OSS 配置
|
||||
# 更新:
|
||||
OSS_CONFIG = {
|
||||
# 更新:
|
||||
"access_key_id": os.getenv("OSS_ACCESS_KEY_ID", "LTAI5t6HUnpFBLEK9194kPVG"),
|
||||
# 更新:
|
||||
"access_key_secret": os.getenv("OSS_ACCESS_KEY_SECRET", "LEr4Q8yRxb8D5b24cKfCwlt4MMoke1"),
|
||||
# 更新:
|
||||
"bucket_name": "jiachenlong-oss",
|
||||
# 更新:
|
||||
"endpoint": "oss-cn-hangzhou.aliyuncs.com",
|
||||
# 更新:
|
||||
"public_url": "https://jiachenlong-oss.oss-cn-hangzhou.aliyuncs.com"
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 临时上传目录(用于OCR识别本地备选)
|
||||
# 更新:
|
||||
UPLOAD_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "uploads", "temp")
|
||||
# 更新:
|
||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
def get_oss_path(file_type: str, user_id: str = None, collection_id: str = None, filename: str = None):
|
||||
# 更新:
|
||||
"""生成OSS路径 - 按年/月/日分类"""
|
||||
# 更新:
|
||||
from datetime import datetime
|
||||
# 更新:
|
||||
now = datetime.now()
|
||||
# 更新:
|
||||
year = now.strftime("%Y")
|
||||
# 更新:
|
||||
month = now.strftime("%m")
|
||||
# 更新:
|
||||
day = now.strftime("%d")
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if file_type == "temp":
|
||||
# 更新:
|
||||
# 临时文件: temp/{year}/{month}/{day}/{uuid}.{ext}
|
||||
# 更新:
|
||||
import uuid
|
||||
# 更新:
|
||||
unique_id = str(uuid.uuid4())
|
||||
# 更新:
|
||||
ext = filename.split('.')[-1] if filename and '.' in filename else 'jpg'
|
||||
# 更新:
|
||||
return f"temp/{year}/{month}/{day}/{unique_id}.{ext}", unique_id
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
elif file_type == "collection":
|
||||
# 更新:
|
||||
# 藏品文件: collections/{user_id}/{year}/{collection_id}/{filename}
|
||||
# 更新:
|
||||
if not user_id or not collection_id:
|
||||
# 更新:
|
||||
raise ValueError("user_id and collection_id required for collection")
|
||||
# 更新:
|
||||
return f"collections/{user_id}/{year}/{collection_id}/{filename}"
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
elif file_type == "avatar":
|
||||
# 更新:
|
||||
# 头像: avatars/{user_id}/avatar.{ext}
|
||||
# 更新:
|
||||
if not user_id:
|
||||
# 更新:
|
||||
raise ValueError("user_id required for avatar")
|
||||
# 更新:
|
||||
ext = filename.split('.')[-1] if filename and '.' in filename else 'jpg'
|
||||
# 更新:
|
||||
return f"avatars/{user_id}/avatar.{ext}"
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
return None
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 上传图片到OSS - 使用服务层(带压缩)
|
||||
# 更新:
|
||||
from app.services.oss import upload_to_oss as oss_upload
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
def upload_to_oss(file_data, oss_key):
|
||||
# 更新:
|
||||
"""上传文件到阿里云OSS(带自动压缩)"""
|
||||
# 更新:
|
||||
return oss_upload(file_data, oss_key)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 专业提示词
|
||||
# 更新:
|
||||
PROFESSIONAL_PROMPT = """你是一名专业的人民币生肖纪念钞鉴定专家。请严格按照以下步骤分析这张图片,并提取准确信息。
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
【识别流程】
|
||||
# 更新:
|
||||
1. 判断类型是否评级钞:首先确认是否为裸钞还是评级钞(有封装盒和标签)
|
||||
# 更新:
|
||||
2. 验证纪念钞特征:对照生肖纪念钞特征进行确认
|
||||
# 更新:
|
||||
3. 验证评级类型:有'标十'字眼的为标十,有'百连'字眼的为标百,其他为单张
|
||||
# 更新:
|
||||
4. 提取信息:仔细阅读标签上的所有文字内容
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
【版别格式要求】
|
||||
# 更新:
|
||||
只需要:年份 + 属相,例如:
|
||||
# 更新:
|
||||
- 2024 龙
|
||||
# 更新:
|
||||
- 2025 蛇
|
||||
# 更新:
|
||||
- 2026 马
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
【输出要求】
|
||||
# 更新:
|
||||
严格按照以下格式输出,每个字段必须填写具体值:
|
||||
# 更新:
|
||||
✅ 1 发行机构:中国人民银行
|
||||
# 更新:
|
||||
✅ 2 发行版别:2024 龙
|
||||
# 更新:
|
||||
✅ 3 面额:贰拾圆
|
||||
# 更新:
|
||||
✅ 4 是否评级:是/否
|
||||
# 更新:
|
||||
✅ 5 封装类型:裸钞/单张/标十/标百
|
||||
# 更新:
|
||||
✅ 6 冠字序号:J0xxxxxxxx
|
||||
# 更新:
|
||||
✅ 7 评级机构:ACG/PCGS/PMG
|
||||
# 更新:
|
||||
✅ 8 评级分数:67/68/69
|
||||
# 更新:
|
||||
✅ 9 是否三星:是/否
|
||||
# 更新:
|
||||
✅ 10 特殊标识:金山标/天马标/红绳版等
|
||||
# 更新:
|
||||
✅ 11 号码特征:金山号 2 张,天马号 3 张等
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
现在请仔细分析提供的图片,按上述格式输出结果。"""
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
@router.post("/recognize")
|
||||
# 更新:
|
||||
async def recognize_image(
|
||||
# 更新:
|
||||
image: UploadFile = File(...),
|
||||
# 更新:
|
||||
current_user: User = Depends(get_current_user),
|
||||
# 更新:
|
||||
db: Session = Depends(get_db)
|
||||
# 更新:
|
||||
):
|
||||
# 更新:
|
||||
"""OCR 图片识别 - 识别后自动保存图片到OSS临时目录"""
|
||||
# 更新:
|
||||
try:
|
||||
# 更新:
|
||||
# 读取图片数据
|
||||
# 更新:
|
||||
image_data = await image.read()
|
||||
# 更新:
|
||||
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 生成OSS临时路径: temp/{year}/{month}/{day}/{uuid}.{ext}
|
||||
# 更新:
|
||||
oss_key, temp_id = get_oss_path("temp", filename=image.filename)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 初始化temp_path为空
|
||||
# 更新:
|
||||
temp_path = None
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 上传到OSS
|
||||
# 更新:
|
||||
try:
|
||||
# 更新:
|
||||
image_url = upload_to_oss(image_data, oss_key)
|
||||
# 更新:
|
||||
except Exception as oss_err:
|
||||
# 更新:
|
||||
# OSS失败时保存到本地作为备选
|
||||
# 更新:
|
||||
temp_path = os.path.join(UPLOAD_DIR, oss_key.split('/')[-1])
|
||||
# 更新:
|
||||
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
|
||||
# 更新:
|
||||
with open(temp_path, 'wb') as f:
|
||||
# 更新:
|
||||
f.write(image_data)
|
||||
# 更新:
|
||||
image_url = f"/uploads/temp/{oss_key.split('/')[-1]}"
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
headers = {
|
||||
# 更新:
|
||||
"Authorization": f"Bearer {DASHSCOPE_API_KEY}",
|
||||
# 更新:
|
||||
"Content-Type": "application/json"
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 阿里云 DashScope API 格式 (qwen-vl-plus 视觉模型)
|
||||
# 更新:
|
||||
payload = {
|
||||
# 更新:
|
||||
"model": "qwen-vl-plus",
|
||||
# 更新:
|
||||
"input": {
|
||||
# 更新:
|
||||
"messages": [{
|
||||
# 更新:
|
||||
"role": "user",
|
||||
# 更新:
|
||||
"content": [
|
||||
# 更新:
|
||||
{
|
||||
# 更新:
|
||||
"image": f"data:{image.content_type};base64,{image_base64}"
|
||||
# 更新:
|
||||
},
|
||||
# 更新:
|
||||
{
|
||||
# 更新:
|
||||
"text": PROFESSIONAL_PROMPT
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
]
|
||||
# 更新:
|
||||
}]
|
||||
# 更新:
|
||||
},
|
||||
# 更新:
|
||||
"parameters": {
|
||||
# 更新:
|
||||
"max_tokens": 1000
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
# 更新:
|
||||
response = await client.post(
|
||||
# 更新:
|
||||
"https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
|
||||
# 更新:
|
||||
json=payload,
|
||||
# 更新:
|
||||
headers=headers
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if response.status_code != 200:
|
||||
# 更新:
|
||||
# 识别失败,删除临时文件
|
||||
# 更新:
|
||||
if temp_path and os.path.exists(temp_path):
|
||||
# 更新:
|
||||
os.remove(temp_path)
|
||||
# 更新:
|
||||
raise HTTPException(status_code=500, detail=f"OCR API 调用失败:{response.text[:200]}")
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
ocr_result = response.json()
|
||||
# 更新:
|
||||
text_content = ""
|
||||
# 更新:
|
||||
# 新版API返回格式
|
||||
# 更新:
|
||||
if "output" in ocr_result and "choices" in ocr_result["output"]:
|
||||
# 更新:
|
||||
choices = ocr_result["output"]["choices"]
|
||||
# 更新:
|
||||
if choices and len(choices) > 0:
|
||||
# 更新:
|
||||
content = choices[0].get("message", {}).get("content", [])
|
||||
# 更新:
|
||||
if content and len(content) > 0:
|
||||
# 更新:
|
||||
text_content = content[0].get("text", "")
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
fields = extract_fields(text_content)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 更新用户AI识别次数
|
||||
# 更新:
|
||||
current_user.f99_95_ai_count = (current_user.f99_95_ai_count or 0) + 1
|
||||
# 更新:
|
||||
db.commit()
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 返回识别结果和临时图片路径
|
||||
# 更新:
|
||||
return {
|
||||
# 更新:
|
||||
"success": True,
|
||||
# 更新:
|
||||
"text": text_content,
|
||||
# 更新:
|
||||
"fields": fields,
|
||||
# 更新:
|
||||
"aiCount": current_user.f99_95_ai_count,
|
||||
# 更新:
|
||||
"temp_image": {
|
||||
# 更新:
|
||||
"id": temp_id,
|
||||
# 更新:
|
||||
"filename": oss_key.split('/')[-1],
|
||||
# 更新:
|
||||
"path": image_url,
|
||||
# 更新:
|
||||
"original_name": image.filename,
|
||||
# 更新:
|
||||
"is_oss": image_url.startswith("https://")
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
except Exception as e:
|
||||
# 更新:
|
||||
import traceback
|
||||
# 更新:
|
||||
error_detail = f"识别失败:{str(e)}\n{traceback.format_exc()}"
|
||||
# 更新:
|
||||
raise HTTPException(status_code=500, detail=error_detail)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
def extract_fields(text: str) -> dict:
|
||||
# 更新:
|
||||
"""从 OCR 文本中提取字段 - 直接返回 AI 识别结果"""
|
||||
# 更新:
|
||||
import re
|
||||
# 更新:
|
||||
fields = {}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 解析结构化输出
|
||||
# 更新:
|
||||
patterns = {
|
||||
# 更新:
|
||||
'issuer': r'✅.*?1.*?发行机构.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'version': r'✅.*?2.*?发行版别.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'denomination': r'✅.*?3.*?面额.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'is_graded_text': r'✅.*?4.*?是否评级.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'packaging': r'✅.*?5.*?封装类型.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'prefix_serial': r'✅.*?6.*?冠字序号.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'grading_company': r'✅.*?7.*?评级机构.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'grading_score': r'✅.*?8.*?评级分数.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'three_star_text': r'✅.*?9.*?是否三星.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'special_mark': r'✅.*?10.*?特殊标识.*?[::]\s*(.+?)(?:\n|$)',
|
||||
# 更新:
|
||||
'serial_feature': r'✅.*?11.*?号码特征.*?[::]\s*(.+?)(?:\n|$)'
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
for field, pattern in patterns.items():
|
||||
# 更新:
|
||||
match = re.search(pattern, text, re.IGNORECASE | re.DOTALL)
|
||||
# 更新:
|
||||
if match:
|
||||
# 更新:
|
||||
value = match.group(1).strip()
|
||||
# 更新:
|
||||
# 保留所有值,包括"无"和"未识别",让前端处理
|
||||
# 更新:
|
||||
fields[field] = value
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 处理是否评级
|
||||
# 更新:
|
||||
if 'is_graded_text' in fields:
|
||||
# 更新:
|
||||
fields['is_graded'] = '是' in fields.pop('is_graded_text')
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 处理是否三星
|
||||
# 更新:
|
||||
if 'three_star_text' in fields:
|
||||
# 更新:
|
||||
fields['three_star'] = '是' in fields.pop('three_star_text')
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 简化版别字段(2024 龙年贺岁纪念钞(标十) → 2024 龙)
|
||||
# 更新:
|
||||
if 'version' in fields:
|
||||
# 更新:
|
||||
version = fields['version']
|
||||
# 更新:
|
||||
# 提取年份和生肖
|
||||
# 更新:
|
||||
year_match = re.search(r'(20\d{2})', version)
|
||||
# 更新:
|
||||
animal = ''
|
||||
# 更新:
|
||||
if '龙' in version:
|
||||
# 更新:
|
||||
animal = '龙'
|
||||
# 更新:
|
||||
elif '蛇' in version:
|
||||
# 更新:
|
||||
animal = '蛇'
|
||||
# 更新:
|
||||
elif '马' in version:
|
||||
# 更新:
|
||||
animal = '马'
|
||||
# 更新:
|
||||
elif '羊' in version:
|
||||
# 更新:
|
||||
animal = '羊'
|
||||
# 更新:
|
||||
elif '猴' in version:
|
||||
# 更新:
|
||||
animal = '猴'
|
||||
# 更新:
|
||||
elif '鸡' in version:
|
||||
# 更新:
|
||||
animal = '鸡'
|
||||
# 更新:
|
||||
elif '狗' in version:
|
||||
# 更新:
|
||||
animal = '狗'
|
||||
# 更新:
|
||||
elif '猪' in version:
|
||||
# 更新:
|
||||
animal = '猪'
|
||||
# 更新:
|
||||
elif '鼠' in version:
|
||||
# 更新:
|
||||
animal = '鼠'
|
||||
# 更新:
|
||||
elif '牛' in version:
|
||||
# 更新:
|
||||
animal = '牛'
|
||||
# 更新:
|
||||
elif '虎' in version:
|
||||
# 更新:
|
||||
animal = '虎'
|
||||
# 更新:
|
||||
elif '兔' in version:
|
||||
# 更新:
|
||||
animal = '兔'
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if year_match and animal:
|
||||
# 更新:
|
||||
fields['version'] = f"{year_match.group(1)}{animal}"
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
return fields
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
@router.post("/claim-temp-image")
|
||||
# 更新:
|
||||
async def claim_temp_image(
|
||||
# 更新:
|
||||
temp_id: str,
|
||||
# 更新:
|
||||
collection_id: str,
|
||||
# 更新:
|
||||
current_user: User = Depends(get_current_user),
|
||||
# 更新:
|
||||
db: Session = Depends(get_db)
|
||||
# 更新:
|
||||
):
|
||||
# 更新:
|
||||
"""将临时图片移动到正式藏品目录 - 按用户/年/藏品ID分类"""
|
||||
# 更新:
|
||||
from app.models.models import Collection, CollectionImage
|
||||
# 更新:
|
||||
from datetime import datetime
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 验证藏品是否存在
|
||||
# 更新:
|
||||
collection = db.query(Collection).filter(
|
||||
# 更新:
|
||||
Collection.f99_90_id == collection_id,
|
||||
# 更新:
|
||||
Collection.f99_91_user_id == current_user.f99_90_id
|
||||
# 更新:
|
||||
).first()
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if not collection:
|
||||
# 更新:
|
||||
raise HTTPException(status_code=404, detail="藏品不存在")
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 生成正式文件名和OSS路径: collections/{user_id}/{year}/{collection_id}/{filename}
|
||||
# 更新:
|
||||
code = collection.f01_02_code or "0000"
|
||||
# 更新:
|
||||
prefix = collection.f02_10_prefix_serial or ""
|
||||
# 更新:
|
||||
username = current_user.f01_01_name
|
||||
# 更新:
|
||||
import time
|
||||
# 更新:
|
||||
final_filename = f"{username}-{code}-{prefix}-{int(time.time())}.jpg"
|
||||
# 更新:
|
||||
oss_key = get_oss_path("collection", user_id=current_user.f99_90_id, collection_id=collection_id, filename=final_filename)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 尝试从OSS获取临时图片 - 尝试多种扩展名和日期路径
|
||||
# 更新:
|
||||
temp_extensions = ['jpg', 'jpeg', 'png', 'gif', 'JPG', 'JPEG', 'PNG', 'GIF']
|
||||
# 更新:
|
||||
temp_content = None
|
||||
# 更新:
|
||||
found_key = None
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 尝试最近7天的路径
|
||||
# 更新:
|
||||
from datetime import timedelta
|
||||
# 更新:
|
||||
for i in range(7):
|
||||
# 更新:
|
||||
date = datetime.now() - timedelta(days=i)
|
||||
# 更新:
|
||||
year = date.strftime("%Y")
|
||||
# 更新:
|
||||
month = date.strftime("%m")
|
||||
# 更新:
|
||||
day = date.strftime("%d")
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
for ext in temp_extensions:
|
||||
# 更新:
|
||||
try:
|
||||
# 更新:
|
||||
temp_oss_key = f"temp/{year}/{month}/{day}/{temp_id}.{ext}"
|
||||
# 更新:
|
||||
import oss2
|
||||
# 更新:
|
||||
auth = oss2.Auth(OSS_CONFIG["access_key_id"], OSS_CONFIG["access_key_secret"])
|
||||
# 更新:
|
||||
bucket = oss2.Bucket(auth, OSS_CONFIG["endpoint"], OSS_CONFIG["bucket_name"])
|
||||
# 更新:
|
||||
temp_content = bucket.get_object(temp_oss_key).read()
|
||||
# 更新:
|
||||
found_key = temp_oss_key
|
||||
# 更新:
|
||||
break
|
||||
# 更新:
|
||||
except:
|
||||
# 更新:
|
||||
continue
|
||||
# 更新:
|
||||
if temp_content:
|
||||
# 更新:
|
||||
break
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if temp_content:
|
||||
# 更新:
|
||||
# 上传到正式目录
|
||||
# 更新:
|
||||
bucket.put_object(oss_key, temp_content)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 删除临时图片
|
||||
# 更新:
|
||||
try:
|
||||
# 更新:
|
||||
bucket.delete_object(found_key)
|
||||
# 更新:
|
||||
except:
|
||||
# 更新:
|
||||
pass
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# OSS URL
|
||||
# 更新:
|
||||
image_path = f"{OSS_CONFIG['public_url']}/{oss_key}"
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
else:
|
||||
# 更新:
|
||||
# OSS失败,使用本地文件
|
||||
# 更新:
|
||||
temp_path = None
|
||||
# 更新:
|
||||
for ext in temp_extensions:
|
||||
# 更新:
|
||||
temp_path = os.path.join(UPLOAD_DIR, f"{temp_id}.{ext}")
|
||||
# 更新:
|
||||
if os.path.exists(temp_path):
|
||||
# 更新:
|
||||
break
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if not temp_path or not os.path.exists(temp_path):
|
||||
# 更新:
|
||||
raise HTTPException(status_code=404, detail="临时图片不存在或已过期")
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 保存到本地
|
||||
# 更新:
|
||||
collection_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "uploads", "collections")
|
||||
# 更新:
|
||||
os.makedirs(collection_dir, exist_ok=True)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
new_path = os.path.join(collection_dir, final_filename)
|
||||
# 更新:
|
||||
import shutil
|
||||
# 更新:
|
||||
shutil.move(temp_path, new_path)
|
||||
# 更新:
|
||||
image_path = f"uploads/collections/{final_filename}"
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 创建图片记录
|
||||
# 更新:
|
||||
image_record = CollectionImage(
|
||||
# 更新:
|
||||
id=str(uuid.uuid4()),
|
||||
# 更新:
|
||||
collection_id=collection.f99_90_id,
|
||||
# 更新:
|
||||
filename=final_filename,
|
||||
# 更新:
|
||||
original_name=temp_id,
|
||||
# 更新:
|
||||
path=image_path
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
db.add(image_record)
|
||||
# 更新:
|
||||
db.commit()
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
return {
|
||||
# 更新:
|
||||
"success": True,
|
||||
# 更新:
|
||||
"image": {
|
||||
# 更新:
|
||||
"id": image_record.id,
|
||||
# 更新:
|
||||
"filename": image_record.filename,
|
||||
# 更新:
|
||||
"path": image_record.path
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
|
|
|||
|
|
@ -1,104 +1,210 @@
|
|||
# 操作路由
|
||||
# operations - 运营操作路由
|
||||
# Version: 1.2.70
|
||||
# 更新:
|
||||
|
||||
from typing import List, Optional
|
||||
# 更新:
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
# 更新:
|
||||
from sqlalchemy.orm import Session
|
||||
# 更新:
|
||||
from app.core.database import get_db
|
||||
# 更新:
|
||||
from app.core.auth import get_current_user
|
||||
# 更新:
|
||||
from app.models.models import User, Collection, Operation
|
||||
# 更新:
|
||||
from app.schemas.schemas import OperationCreate, OperationResponse
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
router = APIRouter(prefix="/api", tags=["操作"])
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
@router.get("/operations", response_model=List[OperationResponse])
|
||||
# 更新:
|
||||
def get_operations(
|
||||
# 更新:
|
||||
collection_id: Optional[str] = None,
|
||||
# 更新:
|
||||
page: int = Query(1, ge=1),
|
||||
# 更新:
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
# 更新:
|
||||
current_user: User = Depends(get_current_user),
|
||||
# 更新:
|
||||
db: Session = Depends(get_db)
|
||||
# 更新:
|
||||
):
|
||||
# 更新:
|
||||
"""获取操作历史"""
|
||||
# 更新:
|
||||
query = db.query(Operation).filter(Operation.user_id == current_user.id)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if collection_id:
|
||||
# 更新:
|
||||
query = query.filter(Operation.collection_id == collection_id)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
operations = query.order_by(Operation.created_at.desc()) \
|
||||
# 更新:
|
||||
.offset((page - 1) * limit) \
|
||||
# 更新:
|
||||
.limit(limit) \
|
||||
# 更新:
|
||||
.all()
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
return operations
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
@router.get("/operations/history")
|
||||
# 更新:
|
||||
def get_operation_history(
|
||||
# 更新:
|
||||
collection_id: Optional[str] = None,
|
||||
# 更新:
|
||||
type: Optional[str] = None,
|
||||
# 更新:
|
||||
start_date: Optional[str] = None,
|
||||
# 更新:
|
||||
end_date: Optional[str] = None,
|
||||
# 更新:
|
||||
page: int = Query(1, ge=1),
|
||||
# 更新:
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
# 更新:
|
||||
current_user: User = Depends(get_current_user),
|
||||
# 更新:
|
||||
db: Session = Depends(get_db)
|
||||
# 更新:
|
||||
):
|
||||
# 更新:
|
||||
"""获取操作历史(带统计)"""
|
||||
# 更新:
|
||||
query = db.query(Operation).filter(Operation.user_id == current_user.id)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if collection_id:
|
||||
# 更新:
|
||||
query = query.filter(Operation.collection_id == collection_id)
|
||||
# 更新:
|
||||
if type:
|
||||
# 更新:
|
||||
query = query.filter(Operation.type == type)
|
||||
# 更新:
|
||||
if start_date:
|
||||
# 更新:
|
||||
query = query.filter(Operation.created_at >= start_date)
|
||||
# 更新:
|
||||
if end_date:
|
||||
# 更新:
|
||||
query = query.filter(Operation.created_at <= end_date)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
total = query.count()
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
data = query.order_by(Operation.created_at.desc()) \
|
||||
# 更新:
|
||||
.offset((page - 1) * limit) \
|
||||
# 更新:
|
||||
.limit(limit) \
|
||||
# 更新:
|
||||
.all()
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
return {
|
||||
# 更新:
|
||||
"data": data,
|
||||
# 更新:
|
||||
"pagination": {
|
||||
# 更新:
|
||||
"page": page,
|
||||
# 更新:
|
||||
"limit": limit,
|
||||
# 更新:
|
||||
"total": total,
|
||||
# 更新:
|
||||
"pages": (total + limit - 1) // limit
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
@router.post("/operations", response_model=OperationResponse)
|
||||
# 更新:
|
||||
def create_operation(
|
||||
# 更新:
|
||||
operation_data: OperationCreate,
|
||||
# 更新:
|
||||
current_user: User = Depends(get_current_user),
|
||||
# 更新:
|
||||
db: Session = Depends(get_db)
|
||||
# 更新:
|
||||
):
|
||||
# 更新:
|
||||
"""创建操作记录"""
|
||||
# 更新:
|
||||
# 验证藏品存在
|
||||
# 更新:
|
||||
collection = db.query(Collection).filter(
|
||||
# 更新:
|
||||
Collection.id == operation_data.collection_id,
|
||||
# 更新:
|
||||
Collection.user_id == current_user.id
|
||||
# 更新:
|
||||
).first()
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if not collection:
|
||||
# 更新:
|
||||
raise HTTPException(status_code=404, detail="藏品不存在")
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
operation = Operation(
|
||||
# 更新:
|
||||
collection_id=operation_data.collection_id,
|
||||
# 更新:
|
||||
user_id=current_user.id,
|
||||
# 更新:
|
||||
type=operation_data.type,
|
||||
# 更新:
|
||||
price=operation_data.price,
|
||||
# 更新:
|
||||
note=operation_data.note
|
||||
# 更新:
|
||||
)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
db.add(operation)
|
||||
# 更新:
|
||||
db.commit()
|
||||
# 更新:
|
||||
db.refresh(operation)
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
return operation
|
||||
# 更新:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,384 @@
|
|||
# seek - 寻号匹配路由
|
||||
# Version: 1.2.70
|
||||
# 更新:
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
# 更新:
|
||||
# Version: 1.2.x
|
||||
# 更新:
|
||||
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": "删除成功"}
|
||||
# 更新:
|
||||
|
|
@ -1,4 +1,7 @@
|
|||
# 用户管理路由
|
||||
# users.py - 用户管理路由
|
||||
# Version: 1.2.98 (2026-04-19)
|
||||
# 更新:新增 dealCount 字段,从 Information 表统计用户发布的行情数量
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
|
||||
from sqlalchemy.orm import Session
|
||||
|
|
@ -11,7 +14,7 @@ router = APIRouter(prefix="/api", tags=["用户"])
|
|||
|
||||
# ============ 当前用户接口 ============
|
||||
|
||||
@router.get("/users/me", response_model=UserResponse)
|
||||
@router.get("/users/me") # 无 response_model,避免 Pydantic 序列化问题
|
||||
def get_current_user_info(
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
|
|
@ -47,7 +50,7 @@ def get_current_user_info(
|
|||
"f99_93_updated_at": current_user.f99_93_updated_at.isoformat() if current_user.f99_93_updated_at else None
|
||||
}
|
||||
|
||||
@router.put("/users/me", response_model=UserResponse)
|
||||
@router.put("/users/me") # 无 response_model,避免 Pydantic 序列化问题
|
||||
def update_current_user(
|
||||
user_update: UserUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
|
|
@ -114,6 +117,11 @@ def get_users(
|
|||
for u in users:
|
||||
# 统计每个用户的藏品数量
|
||||
count = db.query(Collection).filter(Collection.f99_91_user_id == u.f99_90_id).count()
|
||||
# 统计每个用户发布的行情数量(info_type='deal')
|
||||
deal_count = db.query(Information).filter(
|
||||
Information.user_id == u.f99_90_id,
|
||||
Information.info_type == "deal"
|
||||
).count()
|
||||
user_list.append({
|
||||
"id": u.f99_90_id,
|
||||
"username": u.f01_01_name,
|
||||
|
|
@ -123,6 +131,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,
|
||||
"dealCount": deal_count,
|
||||
"level": u.f99_94_level,
|
||||
"aiCount": u.f99_95_ai_count,
|
||||
"searchCount": u.f99_96_search_count,
|
||||
|
|
|
|||
|
|
@ -1,356 +1,760 @@
|
|||
# yichens - 一尘数据路由
|
||||
# Version: 1.2.70
|
||||
# 更新:
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
# 更新:
|
||||
# Version: 1.2.x
|
||||
# 更新:
|
||||
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", response_model=List[PostItem])
|
||||
# 更新:
|
||||
@router.get("/posts")
|
||||
# 更新:
|
||||
def get_posts(
|
||||
# 更新:
|
||||
limit: int = Query(20, ge=1, le=500),
|
||||
# 更新:
|
||||
offset: int = Query(0, ge=0),
|
||||
# 更新:
|
||||
category: Optional[str] = None,
|
||||
# 更新:
|
||||
post_type: Optional[str] = None,
|
||||
# 更新:
|
||||
keyword: Optional[str] = None,
|
||||
# 更新:
|
||||
db: Session = Depends(get_coolbot_db)
|
||||
# 更新:
|
||||
):
|
||||
"""获取帖子列表"""
|
||||
query = """
|
||||
SELECT post_id, title, content, category, post_type, price,
|
||||
author_username, post_time, reply_count, view_count, url
|
||||
FROM yichens_posts
|
||||
WHERE 1=1
|
||||
"""
|
||||
# 更新:
|
||||
"""获取帖子列表 - 支持全局搜索,返回总数和分页信息"""
|
||||
# 更新:
|
||||
# 构建WHERE条件
|
||||
# 更新:
|
||||
where_clauses = ["1=1"]
|
||||
# 更新:
|
||||
params = {"limit": limit, "offset": offset}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if category:
|
||||
query += " AND category = :category"
|
||||
# 更新:
|
||||
where_clauses.append("category = :category")
|
||||
# 更新:
|
||||
params["category"] = category
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
if post_type:
|
||||
query += " AND post_type = :post_type"
|
||||
# 更新:
|
||||
where_clauses.append("post_type = :post_type")
|
||||
# 更新:
|
||||
params["post_type"] = post_type
|
||||
# 更新:
|
||||
|
||||
query += " ORDER BY post_time DESC LIMIT :limit OFFSET :offset"
|
||||
# 更新:
|
||||
# 全局搜索
|
||||
# 更新:
|
||||
if keyword:
|
||||
# 更新:
|
||||
where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)")
|
||||
# 更新:
|
||||
params["keyword"] = f"%{keyword}%"
|
||||
# 更新:
|
||||
|
||||
results = db.execute(text(query), params).fetchall()
|
||||
# 更新:
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
# 更新:
|
||||
|
||||
return [PostItem(
|
||||
# 更新:
|
||||
# 查询总数
|
||||
# 更新:
|
||||
count_query = f"SELECT COUNT(*) FROM yichens_posts WHERE {where_sql}"
|
||||
# 更新:
|
||||
total_count = db.execute(text(count_query), {k: v for k, v in params.items() if k not in ['limit', 'offset']}).scalar() or 0
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
# 查询数据 - 有post_time时按post_time排序,没有时按crawled_at排序
|
||||
# 更新:
|
||||
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 COALESCE(post_time, crawled_at) 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}
|
||||
# 更新:
|
||||
}
|
||||
# 更新:
|
||||
|
||||
# 更新:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# 版本更新记录
|
||||
|
||||
## v1.2.98 (2026-04-19)
|
||||
|
||||
### 前端更新
|
||||
- **Admin.jsx**
|
||||
- 功能:修复管理员用户列表行情数显示问题
|
||||
- 改进:新增 dealCount 字段显示用户发布的成交行情数量
|
||||
- 代码:将"配号"改为"📈 行情: X条"
|
||||
|
||||
### 后端更新
|
||||
- **users.py**
|
||||
- 功能:修复管理员用户列表行情数API
|
||||
- 改进:新增 dealCount 字段,从 Information 表统计用户发布的 deal 数量
|
||||
- 代码:新增 Information 模型导入,统计查询
|
||||
|
||||
### Vite配置更新
|
||||
- **vite.config.js**
|
||||
- 功能:修复前端构建版本号不更新问题
|
||||
- 改进:将版本更新改为 closeBundle hook 执行
|
||||
- 代码:重构 updateHtmlTitle 函数
|
||||
|
||||
---
|
||||
|
||||
## v1.2.97 (2026-04-XX)
|
||||
|
||||
### 更新内容
|
||||
- (待记录)
|
||||
|
||||
---
|
||||
|
||||
## v1.2.96 (2026-04-XX)
|
||||
|
||||
### 更新内容
|
||||
- (待记录)
|
||||
|
|
@ -1 +1 @@
|
|||
VERSION=1.2.69
|
||||
1.2.98
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"version": "1.2.98",
|
||||
"updated": "2026-04-19",
|
||||
"modules": {
|
||||
"frontend": {
|
||||
"version": "1.2.98",
|
||||
"pages": {
|
||||
"Add": "1.2.90",
|
||||
"Admin": "1.2.98",
|
||||
"Detail": "1.2.90",
|
||||
"Edit": "1.2.90",
|
||||
"Home": "1.2.95",
|
||||
"List": "1.2.93",
|
||||
"Login": "1.2.85",
|
||||
"News": "1.2.80",
|
||||
"News_YichensBoard": "1.2.75",
|
||||
"Settings": "1.2.75",
|
||||
"Stats": "1.2.70",
|
||||
"YichensBoard": "1.2.70"
|
||||
},
|
||||
"config": {
|
||||
"version": "1.2.98"
|
||||
}
|
||||
},
|
||||
"backend": {
|
||||
"version": "1.2.98",
|
||||
"routers": {
|
||||
"auth": "1.2.90",
|
||||
"collections": "1.2.95",
|
||||
"deal": "1.2.85",
|
||||
"information": "1.2.92",
|
||||
"news": "1.2.80",
|
||||
"ocr": "1.2.75",
|
||||
"operations": "1.2.70",
|
||||
"seek": "1.2.70",
|
||||
"users": "1.2.98",
|
||||
"yichens": "1.2.70"
|
||||
},
|
||||
"app": {
|
||||
"core": "1.2.0",
|
||||
"models": "1.2.0",
|
||||
"schemas": "1.2.0",
|
||||
"services": "1.2.0",
|
||||
"utils": "1.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
VERSION=1.2.97
|
||||
|
|
@ -0,0 +1 @@
|
|||
1.2.98
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<title>甲辰收藏 v1.2.50</title>
|
||||
<title>甲辰收藏 v=1.2.97</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="/static/icons/favicon.png" />
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "jiachenlong-frontend",
|
||||
"version": "1.2.60",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
v=1.2.80
|
||||
|
|
@ -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 <Home />
|
||||
if (basePath === '/news') return <News />
|
||||
if (basePath === '/info') return <Info />
|
||||
if (basePath === '/stats') return <Stats />
|
||||
if (basePath === '/list') return <List />
|
||||
if (basePath === '/add') return <Add />
|
||||
|
|
@ -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 => (
|
||||
<div
|
||||
|
|
@ -116,3 +112,4 @@ export default function App() {
|
|||
</div>
|
||||
)
|
||||
}
|
||||
// Fri Apr 10 03:44:24 PM CST 2026
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
// 添加藏品页面 - 支持 AI 识别/手工录入/批量录入
|
||||
/**
|
||||
* Add - 添加藏品页面
|
||||
* Version: 1.2.90x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
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 +71,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 +209,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 +325,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 +456,15 @@ export default function Add() {
|
|||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button onClick={() => 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 识别
|
||||
AI 识别
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('manual')}
|
||||
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' }}>
|
||||
✍️ 手工录入
|
||||
手工录入
|
||||
</button>
|
||||
<button onClick={() => 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' }}>
|
||||
📦 批量录入
|
||||
<button onClick={() => setActiveTab('deal')}
|
||||
style={{ flex: 1, padding: '10px', background: activeTab === 'deal' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'deal' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>
|
||||
行情录入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -435,7 +486,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' }}>🗑️ 重新选择</button>
|
||||
<button onClick={handleRecognize} disabled={recognizing}
|
||||
style={{ padding: '12px 24px', background: recognizing ? '#64748b' : '#fbbf24', color: recognizing ? '#94a3b8' : '#1e293b', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: recognizing ? 'not-allowed' : 'pointer' }}>
|
||||
{recognizing ? '🔍 识别中...' : '🤖 开始识别'}
|
||||
{recognizing ? '🔍 识别中...' : '开始识别'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -657,16 +708,380 @@ export default function Add() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 批量录入模式 */}
|
||||
{activeTab === 'batch' && (
|
||||
<div style={{ padding: '48px 16px', textAlign: 'center', color: '#94a3b8' }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🚧</div>
|
||||
<div style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '8px' }}>批量录入开发中</div>
|
||||
<div style={{ fontSize: '13px' }}>敬请期待后续版本</div>
|
||||
|
||||
|
||||
{/* 版本号 */}
|
||||
|
||||
{/* 行情录入模式 */}
|
||||
{activeTab === 'deal' && (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<div style={{ background: 'rgba(255,255,255,0.03)', borderRadius: '12px', padding: '20px', marginBottom: '16px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '16px', fontWeight: 'bold', marginBottom: '16px' }}>成交行情录入</div>
|
||||
|
||||
{/* 单条/批量切换 */}
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
|
||||
<button onClick={() => setDealMode('single')}
|
||||
style={{ flex: 1, padding: '10px', background: dealMode === 'single' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: dealMode === 'single' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
|
||||
单条录入
|
||||
</button>
|
||||
<button onClick={() => setDealMode('batch')}
|
||||
style={{ flex: 1, padding: '10px', background: dealMode === 'batch' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: dealMode === 'batch' ? '#1e293b' : '#fff', border: 'none', borderRadius: '8px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
|
||||
批量录入
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 单条录入 */}
|
||||
{dealMode === 'single' && (
|
||||
<div>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>冠字号 *</div>
|
||||
<input value={dealForm.serial} onChange={(e) => setDealForm({...dealForm, serial: e.target.value.toUpperCase(), category: autoCategory(e.target.value)})}
|
||||
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' }} />
|
||||
</div>
|
||||
|
||||
{dealForm.category && (
|
||||
<div style={{ background: 'rgba(251,191,36,0.2)', padding: '10px', borderRadius: '8px', textAlign: 'center', marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8' }}>号码分类</div>
|
||||
<div style={{ fontSize: '14px', color: '#fbbf24', fontWeight: 'bold' }}>{dealForm.category}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>包装类型</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{['单张', '标十', '标百'].map(p => (
|
||||
<button key={p} onClick={() => 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交价格 *</div>
|
||||
<input type="number" value={dealForm.price} onChange={(e) => 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' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交平台 *</div>
|
||||
<select value={dealForm.platform} onChange={(e) => setDealForm({...dealForm, platform: 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' }}>
|
||||
<option value="淘宝">淘宝</option>
|
||||
<option value="咸鱼">咸鱼</option>
|
||||
<option value="抖音">抖音</option>
|
||||
<option value="快手">快手</option>
|
||||
<option value="微拍堂">微拍堂</option>
|
||||
<option value="拼多多">拼多多</option>
|
||||
<option value="一尘">一尘</option>
|
||||
<option value="爱藏">爱藏</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', marginBottom: '12px' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>出售者</div>
|
||||
<input value={dealForm.seller} onChange={(e) => 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' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>购买者</div>
|
||||
<input value={dealForm.buyer} onChange={(e) => 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' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>成交日期 *</div>
|
||||
<input type="date" value={dealForm.date} onChange={(e) => 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' }} />
|
||||
</div>
|
||||
|
||||
{/* 评级字段 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>评级(可选)</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<select value={dealForm.gradingCompany || ''} onChange={(e) => setDealForm({...dealForm, gradingCompany: e.target.value})}
|
||||
style={{ flex: 1, padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '14px' }}>
|
||||
<option value="">评级机构</option>
|
||||
<option value="PCGS">PCGS</option>
|
||||
<option value="PMG">PMG</option>
|
||||
<option value="ACG">ACG</option>
|
||||
<option value="爱藏">爱藏</option>
|
||||
</select>
|
||||
<input value={dealForm.gradingScore || ''} onChange={(e) => setDealForm({...dealForm, gradingScore: e.target.value})}
|
||||
placeholder="评级分数"
|
||||
style={{ flex: 1, padding: '10px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '14px' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onClick={async () => {
|
||||
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 ? '提交中...' : '提交行情'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 批量录入 */}
|
||||
{dealMode === 'batch' && (
|
||||
<div>
|
||||
{/* 日期和平台 */}
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '10px', color: '#64748b', marginBottom: '4px' }}>默认日期(可选)</div>
|
||||
<input type="date" value={batchDefaultDate || ''} onChange={(e) => setBatchDefaultDate(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '12px' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '10px', color: '#64748b', marginBottom: '4px' }}>成交平台(可选)</div>
|
||||
<select value={batchDefaultPlatform || ''} onChange={(e) => setBatchDefaultPlatform(e.target.value)}
|
||||
style={{ width: '100%', padding: '8px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>
|
||||
<option value="">请选择</option>
|
||||
<option value="淘宝">淘宝</option>
|
||||
<option value="咸鱼">咸鱼</option>
|
||||
<option value="抖音">抖音</option>
|
||||
<option value="快手">快手</option>
|
||||
<option value="微拍堂">微拍堂</option>
|
||||
<option value="拼多多">拼多多</option>
|
||||
<option value="一尘">一尘</option>
|
||||
<option value="爱藏">爱藏</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 包装类型选择 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{ fontSize: '10px', color: '#64748b', marginBottom: '6px' }}>批量默认包装类型(可选)</div>
|
||||
<div style={{ display: 'flex', gap: '6px' }}>
|
||||
<button onClick={() => setBatchDefaultPackaging(batchDefaultPackaging === '单张' ? '' : '单张')}
|
||||
style={{ flex: 1, padding: '8px', background: batchDefaultPackaging === '单张' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: batchDefaultPackaging === '单张' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '12px', cursor: 'pointer' }}>
|
||||
单张
|
||||
</button>
|
||||
<button onClick={() => setBatchDefaultPackaging(batchDefaultPackaging === '标十' ? '' : '标十')}
|
||||
style={{ flex: 1, padding: '8px', background: batchDefaultPackaging === '标十' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: batchDefaultPackaging === '标十' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '12px', cursor: 'pointer' }}>
|
||||
标十
|
||||
</button>
|
||||
<button onClick={() => setBatchDefaultPackaging(batchDefaultPackaging === '标百' ? '' : '标百')}
|
||||
style={{ flex: 1, padding: '8px', background: batchDefaultPackaging === '标百' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: batchDefaultPackaging === '标百' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '12px', cursor: 'pointer' }}>
|
||||
标百
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea value={batchText} onChange={(e) => setBatchText(e.target.value)}
|
||||
placeholder="粘贴批量行情文本..." style={{ width: '100%', minHeight: '120px', padding: '12px', background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', color: '#fff', fontSize: '14px' }} />
|
||||
<button onClick={async () => {
|
||||
if (!batchText.trim()) { alert('请先粘贴行情文本'); return }
|
||||
setParsing(true)
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/api/information/batch-parse-local`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: batchText, defaultPackaging: batchDefaultPackaging, defaultDate: batchDefaultDate, defaultPlatform: batchDefaultPlatform })
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.success) { setBatchResult(data.data || []); alert(`解析成功${data.data.length}条`) }
|
||||
else { alert('解析失败: ' + (data.error || '未知错误')) }
|
||||
} catch (e) { alert('请求失败: ' + e.message) }
|
||||
finally { setParsing(false) }
|
||||
}}
|
||||
disabled={parsing} style={{ width: '100%', marginTop: '8px', padding: '10px', background: parsing ? '#64748b' : '#22c55e', color: '#fff', border: 'none', borderRadius: '8px' }}>
|
||||
{parsing ? '解析中...' : '解析文本'}
|
||||
</button>
|
||||
|
||||
{batchResult.length > 0 && (
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
||||
<div style={{ color: '#94a3b8', fontSize: '12px' }}>解析结果 ({batchResult.length}条)</div>
|
||||
<button onClick={() => { setBatchResult([]); setBatchText('') }} style={{ padding: '4px 8px', background: 'rgba(239,68,68,0.2)', color: '#ef4444', border: 'none', borderRadius: '4px', fontSize: '11px', cursor: 'pointer' }}>清空</button>
|
||||
</div>
|
||||
<div style={{ maxHeight: '350px', overflowY: 'auto' }}>
|
||||
{batchResult.map((item, idx) => (
|
||||
<div key={idx} style={{ padding: '8px', background: 'rgba(255,255,255,0.05)', marginBottom: '6px', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
{/* 第一行:冠字号 | 价格 | 分类 | 包装 */}
|
||||
<div style={{ display: 'flex', gap: '6px', marginBottom: '4px' }}>
|
||||
<div style={{ flex: '0 0 35%' }}>
|
||||
<input value={batchResult[idx].serial} onChange={(e) => {
|
||||
const newResult = [...batchResult]
|
||||
newResult[idx].serial = e.target.value
|
||||
setBatchResult(newResult)
|
||||
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fbbf24', fontSize: '12px' }} />
|
||||
</div>
|
||||
<div style={{ flex: '0 0 25%' }}>
|
||||
<input type="number" value={batchResult[idx].price} onChange={(e) => {
|
||||
const newResult = [...batchResult]
|
||||
newResult[idx].price = parseFloat(e.target.value) || 0
|
||||
setBatchResult(newResult)
|
||||
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#22c55e', fontSize: '12px' }} />
|
||||
</div>
|
||||
<div style={{ flex: '0 0 20%' }}>
|
||||
<select value={batchResult[idx].category} onChange={(e) => {
|
||||
const newResult = [...batchResult]
|
||||
newResult[idx].category = e.target.value
|
||||
setBatchResult(newResult)
|
||||
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fff', fontSize: '10px' }}>
|
||||
{['圆圆号','倒置号','金马王','金马号','金山王','天马王','金山号','天马号','朦胧王','朦胧号','如意号','钻石号','永恒号','带7号','带4号'].map(c => <option key={c} value={c}>{c}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: '0 0 20%' }}>
|
||||
<select value={batchResult[idx].packaging || '单张'} onChange={(e) => {
|
||||
const newResult = [...batchResult]
|
||||
newResult[idx].packaging = e.target.value
|
||||
setBatchResult(newResult)
|
||||
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fff', fontSize: '10px' }}>
|
||||
<option value="单张">单张</option>
|
||||
<option value="标十">标十</option>
|
||||
<option value="标百">标百</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/* 第二行:评级机构 | 分数 | 出售者 | 删除 */}
|
||||
<div style={{ display: 'flex', gap: '6px' }}>
|
||||
<div style={{ flex: '0 0 22%' }}>
|
||||
<select value={batchResult[idx].grading_company || ''} onChange={(e) => {
|
||||
const newResult = [...batchResult]
|
||||
newResult[idx].grading_company = e.target.value
|
||||
newResult[idx].is_graded = !!e.target.value
|
||||
setBatchResult(newResult)
|
||||
}} style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fff', fontSize: '10px' }}>
|
||||
<option value="">未评级</option>
|
||||
<option value="PCGS">PCGS</option>
|
||||
<option value="PMG">PMG</option>
|
||||
<option value="ACG">ACG</option>
|
||||
<option value="爱藏">爱藏</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: '0 0 18%' }}>
|
||||
<input value={batchResult[idx].grade || ''} onChange={(e) => {
|
||||
const newResult = [...batchResult]
|
||||
newResult[idx].grade = e.target.value
|
||||
setBatchResult(newResult)
|
||||
}} placeholder="分数" style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#06b6d4', fontSize: '11px' }} />
|
||||
</div>
|
||||
<div style={{ flex: '0 0 30%' }}>
|
||||
<input value={batchResult[idx].seller || ''} onChange={(e) => {
|
||||
const newResult = [...batchResult]
|
||||
newResult[idx].seller = e.target.value
|
||||
setBatchResult(newResult)
|
||||
}} placeholder="出售者" style={{ width: '100%', padding: '4px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px', color: '#fff', fontSize: '11px' }} />
|
||||
</div>
|
||||
<div style={{ flex: '0 0 30%', textAlign: 'right' }}>
|
||||
<button onClick={() => {
|
||||
const newResult = batchResult.filter((_, i) => i !== idx)
|
||||
setBatchResult(newResult)
|
||||
}} style={{ padding: '4px 8px', background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', fontSize: '10px', cursor: 'pointer' }}>删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={async () => {
|
||||
setSavingDeal(true)
|
||||
let count = 0; const token = localStorage.getItem('token')
|
||||
for (const item of batchResult) {
|
||||
try {
|
||||
// 计算尾号和大小号
|
||||
const digits = (item.serial || '').replace('J', '').replace(/[^0-9]/g, '')
|
||||
let tail_number = '', size_type = ''
|
||||
if ((item.packaging === '标十' || item.packaging === '标百') && digits.length >= 2) {
|
||||
tail_number = item.packaging === '标十' ? digits.slice(-2) : digits.slice(-3)
|
||||
size_type = (item.packaging === '标十' && ['01','11','21','31','41','51'].includes(tail_number)) || (item.packaging === '标百' && ['101','201','301','401','501'].includes(tail_number)) ? '小号' : '大号'
|
||||
}
|
||||
await fetch(`${API_BASE}/api/deal`, {
|
||||
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: `${item.serial}-¥${item.price}`,
|
||||
content: `分类: ${item.category || '-'}\n尾号: ${tail_number || '-'}\n大小号: ${size_type || '-'}\n包装: ${item.packaging || '单张'}\n平台: ${item.platform || '-'}\n价格: ¥${item.price}\n出售者: ${item.seller || '-'}\n购买者: ${item.buyer || '-'}\n日期: ${item.deal_date || new Date().toISOString().split('T')[0]}`,
|
||||
deal_price: parseFloat(item.price),
|
||||
deal_date: item.deal_date || new Date().toISOString().split('T')[0],
|
||||
packaging: item.packaging || '单张',
|
||||
category: item.category || '',
|
||||
tail_number: tail_number,
|
||||
size_type: size_type,
|
||||
platform: item.platform || '-',
|
||||
seller: item.seller || '',
|
||||
buyer: item.buyer || ''
|
||||
})
|
||||
})
|
||||
count++
|
||||
} catch(e) { console.error(e) }
|
||||
}
|
||||
alert(`完成${count}条`); setBatchResult([]); setBatchText(''); setSavingDeal(false)
|
||||
}} disabled={savingDeal} style={{ width: '100%', marginTop: '8px', padding: '10px', background: savingDeal?'#64748b':'#fbbf24', color: savingDeal?'#94a3b8':'#1e293b', border:'none', borderRadius:'8px' }}>
|
||||
{savingDeal ? '提交中...' : `批量录入${batchResult.length}条`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 版本号 */}
|
||||
<div style={{ position: 'fixed', bottom: '16px', right: '16px', color: 'rgba(255,255,255,0.2)', fontSize: '11px', zIndex: 100 }}>v{APP_VERSION}</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* Admin - 管理员用户管理页面
|
||||
* Version: 1.2.98 (2026-04-19)
|
||||
* 更新:新增 dealCount 字段显示用户发布的行情数量
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { APP_VERSION } from '../config/version'
|
||||
|
||||
|
|
@ -126,6 +132,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,
|
||||
|
|
@ -232,7 +240,7 @@ export default function Admin() {
|
|||
🤖 AI识别: {user.aiCount || 0}次
|
||||
</div>
|
||||
<div style={{ background: 'rgba(20, 184, 166, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#14b8a6' }}>
|
||||
🎯 配号: {user.searchCount || 0}次
|
||||
📈 行情: {user.dealCount || 0}条
|
||||
</div>
|
||||
<div style={{ background: 'rgba(245, 158, 11, 0.1)', padding: '4px 10px', borderRadius: '6px', fontSize: '12px', color: '#f59e0b' }}>
|
||||
🔍 寻号: {user.searchCount || 0}次
|
||||
|
|
@ -253,7 +261,7 @@ export default function Admin() {
|
|||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => 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 +399,28 @@ export default function Admin() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>手机号</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingUser.phone || ''}
|
||||
onChange={(e) => 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' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', color: '#94a3b8', fontSize: '13px', marginBottom: '6px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingUser.phoneVerified === true}
|
||||
onChange={(e) => setEditingUser({ ...editingUser, phoneVerified: e.target.checked })}
|
||||
style={{ marginRight: '8px' }}
|
||||
/>
|
||||
手机号已验证
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', color: '#94a3b8', fontSize: '13px', marginBottom: '6px' }}>角色</label>
|
||||
<select
|
||||
|
|
@ -458,7 +488,10 @@ export default function Admin() {
|
|||
<option value="白银">白银</option>
|
||||
<option value="黄金">黄金</option>
|
||||
<option value="铂金">铂金</option>
|
||||
<option value="钻石">钻石</option>
|
||||
<option value="钻石号">钻石号</option>
|
||||
<option value="永恒号">永恒号</option>
|
||||
<option value="带7号">带7号</option>
|
||||
<option value="带4号">带4号</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -534,18 +567,6 @@ export default function Admin() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', color: '#94a3b8', fontSize: '13px', cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingUser.phoneVerified || false}
|
||||
onChange={(e) => setEditingUser({ ...editingUser, phoneVerified: e.target.checked })}
|
||||
style={{ marginRight: '8px' }}
|
||||
/>
|
||||
手机号已认证
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* Detail - 藏品详情页面
|
||||
* Version: 1.2.90x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
export default function Detail() {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* Edit - 编辑藏品页面
|
||||
* Version: 1.2.90x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
|
||||
// Input 组件(复用 Add.jsx 的定义)
|
||||
|
|
@ -46,7 +52,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 +163,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 = '其他'
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* Home - 首页
|
||||
* Version: 1.2.95x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { APP_VERSION } from '../config/version'
|
||||
|
||||
|
|
@ -6,7 +12,6 @@ export default function Home() {
|
|||
const [stats, setStats] = useState({ totalCount: 0, totalCost: 0, totalRevenue: 0, expectedProfit: 0, totalProfit: 0, gradedCount: 0 })
|
||||
const [recentCollections, setRecentCollections] = useState([])
|
||||
const [yichensStats, setYichensStats] = useState({ total: 0, deals: 0, wants: 0, dragons: 0, horses: 0, tianma: 0 })
|
||||
const [dragonStats, setDragonStats] = useState({})
|
||||
const [seekStats, setSeekStats] = useState({ seekCount: 0, matchedCount: 0 })
|
||||
const [recentPosts, setRecentPosts] = useState([])
|
||||
const [dragonStats, setDragonStats] = useState({})
|
||||
|
|
@ -74,8 +79,8 @@ export default function Home() {
|
|||
setDragonStats(data || {})
|
||||
}).catch(() => {})
|
||||
|
||||
fetch('/api/yichens/posts?limit=10&offset=0').then(res => res.json()).then(data => {
|
||||
setRecentPosts(Array.isArray(data) ? data : [])
|
||||
fetch('/api/yichens/posts?limit=20&offset=0').then(res => res.json()).then(data => {
|
||||
setRecentPosts(data.posts || data || [])
|
||||
}).catch(() => {})
|
||||
|
||||
// 获取寻配号统计数据
|
||||
|
|
@ -128,7 +133,7 @@ export default function Home() {
|
|||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<img src="/static/images/jiachenlong-logo.png" alt="甲辰收藏" style={{ width: '48px', height: '48px', borderRadius: '50%', objectFit: 'cover' }} />
|
||||
<div>
|
||||
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '600' }}>{user?.username || '用户'}</div>
|
||||
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '600' }}>{user?.username || '用户'} <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '11px', fontWeight: 'normal' }}>ID:{user?.user_code || '-'}</span></div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: '12px' }}>欢迎回来 👋</div>
|
||||
</div>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
|
|
@ -175,32 +180,58 @@ export default function Home() {
|
|||
<div onClick={() => 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'
|
||||
}}>
|
||||
<div style={{ fontSize: '24px' }}>📷</div>
|
||||
<div>
|
||||
<div style={{ color: '#fff', fontSize: '15px', fontWeight: '600' }}>AI识别</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '11px' }}>拍照识别藏品</div>
|
||||
</div>
|
||||
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>藏品录入</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>AI识别添加</div>
|
||||
</div>
|
||||
<div onClick={() => 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'
|
||||
}}>
|
||||
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>行情录入</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>录入成交行情</div>
|
||||
</div>
|
||||
<div onClick={() => 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'
|
||||
}}>
|
||||
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布寻号</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>发布寻配需求</div>
|
||||
</div>
|
||||
<div onClick={() => 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'
|
||||
}}>
|
||||
<div style={{ fontSize: '24px' }}>✏️</div>
|
||||
<div>
|
||||
<div style={{ color: '#fff', fontSize: '15px', fontWeight: '600' }}>手动录入</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '11px' }}>添加新藏品</div>
|
||||
</div>
|
||||
<div style={{ color: '#fff', fontSize: '14px', fontWeight: '600', whiteSpace: 'nowrap' }}>发布藏品</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '10px', marginTop: '2px' }}>手动发布</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -283,7 +314,7 @@ export default function Home() {
|
|||
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.dai4?.deals || 0) + (dragonStats.dai4?.wants || 0)}</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}>
|
||||
<div style={{ color: '#22c55e', fontSize: '14px', fontWeight: '500' }}>无4</div>
|
||||
<div style={{ color: '#06b6d4', fontSize: '14px', fontWeight: '500' }}>带7号</div>
|
||||
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu4?.deals || 0}</div>
|
||||
<div style={{ color: '#f97316', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu4?.wants || 0}</div>
|
||||
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu4?.deals || 0) + (dragonStats.wu4?.wants || 0)}</div>
|
||||
|
|
@ -368,13 +399,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) => (
|
||||
<div key={item.idx} onClick={() => window.location.hash = item.hash} style={{
|
||||
|
|
|
|||
|
|
@ -1,680 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
// 信息页面 - 寻配号发布和发布管理(包含寻号/行情区分)
|
||||
export default function Info() {
|
||||
// 检查登录状态,未登录则跳转到登录页
|
||||
if (!localStorage.getItem('token')) {
|
||||
window.location.hash = '#/login'
|
||||
return null
|
||||
}
|
||||
|
||||
// 顶部tab:寻配号发布 / 发布管理
|
||||
const [activeTab, setActiveTab] = useState('manage')
|
||||
const [showPublish, setShowPublish] = useState(false)
|
||||
const [publishType, setPublishType] = useState('deal') // 'seek' 或 'deal'
|
||||
const [myList, setMyList] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editingItem, setEditingItem] = useState(null)
|
||||
const [filterType, setFilterType] = useState('all') // all/seek/deal
|
||||
const [expandedItems, setExpandedItems] = useState({}) // 展开状态
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
edition: '龙钞', type: '标百', isGraded: true, category: '成交', source: '直播', title: ''
|
||||
})
|
||||
|
||||
// 寻配号发布表单
|
||||
const [seekForm, setSeekForm] = useState({
|
||||
edition: '龙钞', price: '', features: '', contact: '', content: '', title: ''
|
||||
})
|
||||
|
||||
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||
|
||||
// 切换展开/收起
|
||||
const toggleExpand = (itemId) => {
|
||||
setExpandedItems(prev => ({
|
||||
...prev,
|
||||
[itemId]: !prev[itemId]
|
||||
}))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'manage') fetchMyList()
|
||||
}, [activeTab])
|
||||
|
||||
useEffect(() => {
|
||||
// 自动生成行情标题
|
||||
const now = new Date()
|
||||
const date = `${now.getFullYear()}/${now.getMonth()+1}/${now.getDate()}`
|
||||
const grade = formData.isGraded ? '(评级币)' : '(裸钞)'
|
||||
const title = `「${date} ${formData.edition} ${formData.type} ${formData.category} 行情信息${grade}」`
|
||||
setFormData(prev => ({ ...prev, title }))
|
||||
}, [formData.edition, formData.type, formData.isGraded, formData.category])
|
||||
|
||||
useEffect(() => {
|
||||
// 自动生成寻配号标题
|
||||
if (seekForm.edition || seekForm.features) {
|
||||
const title = `「寻号 ${seekForm.edition} J0${seekForm.features || 'XXXXXXXX'}」`
|
||||
setSeekForm(prev => ({ ...prev, title }))
|
||||
}
|
||||
}, [seekForm.edition, seekForm.features])
|
||||
|
||||
const fetchMyList = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const res = await fetch(`${API_BASE}/api/information/list`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setMyList(data || [])
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// 发布寻配号
|
||||
const handlePublishSeek = async () => {
|
||||
if (!seekForm.contact) {
|
||||
alert('请填写联系方式')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const res = await fetch(`${API_BASE}/api/information/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: seekForm.title,
|
||||
content: document.getElementById('seekContent')?.value || seekForm.content,
|
||||
info_type: 'seek',
|
||||
expect_category: seekForm.edition,
|
||||
expect_number: seekForm.features ? `J0${seekForm.features}` : null
|
||||
})
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.id || data.code === 0) {
|
||||
alert('发布成功!')
|
||||
setShowPublish(false)
|
||||
setSeekForm({ edition: '龙钞', price: '', features: '', contact: '', content: '', title: '' })
|
||||
const contentEl = document.getElementById('seekContent')
|
||||
if (contentEl) contentEl.value = ''
|
||||
fetchMyList()
|
||||
} else {
|
||||
alert(data.message || '发布失败')
|
||||
}
|
||||
} catch (e) {
|
||||
alert('发布失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 发布行情(新增发布默认是行情)
|
||||
const handlePublishDeal = async () => {
|
||||
const content = document.getElementById('publishContent')?.value || ''
|
||||
if (!formData.title) {
|
||||
alert('请填写标题')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const res = await fetch(`${API_BASE}/api/information/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: formData.title,
|
||||
content: content,
|
||||
info_type: formData.category === '成交' ? 'deal' : 'seek'
|
||||
})
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.id || data.code === 0) {
|
||||
alert('发布成功!')
|
||||
setShowPublish(false)
|
||||
setFormData({ edition: '龙钞', type: '标百', isGraded: true, category: '成交', source: '直播', title: '' })
|
||||
const contentEl = document.getElementById('publishContent')
|
||||
if (contentEl) contentEl.value = ''
|
||||
fetchMyList()
|
||||
} else {
|
||||
alert(data.message || '发布失败')
|
||||
}
|
||||
} catch (e) {
|
||||
alert('发布失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('确定删除这条信息吗?')) return
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const res = await fetch(`${API_BASE}/api/information/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
if (res.ok) {
|
||||
alert('删除成功')
|
||||
fetchMyList()
|
||||
} else {
|
||||
alert('删除失败')
|
||||
}
|
||||
} catch (e) {
|
||||
alert('删除失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (item) => {
|
||||
setEditingItem({ id: item.id, title: item.title, content: item.content })
|
||||
}
|
||||
|
||||
const saveEdit = async () => {
|
||||
if (!editingItem) return
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const res = await fetch(`${API_BASE}/api/information/${editingItem.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ title: editingItem.title, content: editingItem.content })
|
||||
})
|
||||
if (res.ok) {
|
||||
alert('保存成功')
|
||||
setEditingItem(null)
|
||||
fetchMyList()
|
||||
} else {
|
||||
alert('保存失败')
|
||||
}
|
||||
} catch (e) {
|
||||
alert('保存失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const getUserPhone = () => {
|
||||
try {
|
||||
const user = JSON.parse(localStorage.getItem('user') || '{}')
|
||||
return user.phone || user.phoneNumber || user.mobile || user.tel || ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setSeekForm(prev => ({ ...prev, contact: getUserPhone() }))
|
||||
}, [])
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleString('zh-CN').slice(0, 16)
|
||||
}
|
||||
|
||||
// 过滤后的列表
|
||||
const filteredList = myList.filter(item => filterType === 'all' || item.info_type === filterType)
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: '100vh', background: 'linear-gradient(180deg, #0f172a 0%, #1e293b 100%)', paddingBottom: '80px' }}>
|
||||
{/* 顶部 Tab 切换 */}
|
||||
<div style={{ position: 'sticky', top: 0, background: 'rgba(15,23,42,0.95)', backdropFilter: 'blur(10px)', padding: '16px 20px', borderBottom: '1px solid #1e293b', zIndex: 100 }}>
|
||||
<div style={{ display: 'flex', gap: '8px', background: '#1e293b', borderRadius: '12px', padding: '4px' }}>
|
||||
{[
|
||||
{ key: 'manage', label: '📋 发布管理' }
|
||||
].map(tab => (
|
||||
<div
|
||||
key={tab.key}
|
||||
onClick={() => 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}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '20px' }}>
|
||||
{/* 寻配号发布页 - 已删除 */}
|
||||
{false && (
|
||||
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '16px', padding: '20px', border: '1px solid #374151', boxShadow: '0 4px 20px rgba(0,0,0,0.3)' }}>
|
||||
<h3 style={{ color: '#f9fafb', margin: '0 0 20px 0', fontSize: '18px', fontWeight: '600' }}>🔍 寻配号发布</h3>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||
<div>
|
||||
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>版别</label>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{['龙钞', '马钞', '蛇钞'].map(ed => {
|
||||
const selected = seekForm.edition === ed
|
||||
return (
|
||||
<button
|
||||
key={ed}
|
||||
onClick={() => 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}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>号码特征(8位)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={seekForm.features}
|
||||
onChange={e => 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' }}
|
||||
/>
|
||||
<div style={{ color: '#6b7280', fontSize: '11px', marginTop: '4px' }}>X=任意数字 A=非4 B=非47 C=非347 D=非247</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>标题(自动生成)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={seekForm.title}
|
||||
readOnly
|
||||
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#10b981', fontSize: '14px', boxSizing: 'border-box' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>正文</label>
|
||||
<textarea
|
||||
id="seekContent"
|
||||
placeholder="请输入详细信息..."
|
||||
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#fff', fontSize: '14px', boxSizing: 'border-box', resize: 'vertical', minHeight: '80px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ color: '#ef4444', fontSize: '13px', display: 'block', marginBottom: '8px' }}>联系方式 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={seekForm.contact}
|
||||
onChange={e => 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' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handlePublishSeek}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
border: 'none',
|
||||
borderRadius: '10px',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
🚀 发布寻配号
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 发布管理页 */}
|
||||
{activeTab === 'manage' && (
|
||||
<div>
|
||||
{/* 新增发布区域 - 发布行情信息 */}
|
||||
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '16px', padding: '20px', marginBottom: '16px', border: '1px solid #374151', boxShadow: '0 4px 20px rgba(0,0,0,0.3)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h3 style={{ color: '#f9fafb', margin: 0, fontSize: '16px', fontWeight: '600' }}>📋 发布管理</h3>
|
||||
<button
|
||||
onClick={() => 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 ? '✕ 收起' : '+ 💰 新增行情发布'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 行情发布表单 */}
|
||||
{showPublish && (
|
||||
<div style={{ background: '#0f172a', borderRadius: '12px', padding: '16px', marginBottom: '16px' }}>
|
||||
<div style={{ color: '#f59e0b', fontSize: '14px', fontWeight: '600', marginBottom: '16px' }}>💰 发布行情信息</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<div style={{ flex: 2 }}>
|
||||
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>版别</label>
|
||||
<div style={{ display: 'flex', gap: '6px' }}>
|
||||
{['龙钞', '马钞', '蛇钞', '其他'].map(ed => (
|
||||
<button
|
||||
key={ed}
|
||||
onClick={() => 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<div style={{ flex: 2 }}>
|
||||
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>类型</label>
|
||||
<div style={{ display: 'flex', gap: '6px' }}>
|
||||
{['标百', '标十', '单张'].map(t => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>是否评级</label>
|
||||
<button
|
||||
onClick={() => 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 ? '✓ 评级币' : '○ 裸钞'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>分类</label>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
{[{value:'成交',label:'💰 成交'},{value:'求购',label:'🔍 求购'},{value:'出售',label:'💵 出售'}].map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>标题(自动生成)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
readOnly
|
||||
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#10b981', fontSize: '14px', boxSizing: 'border-box' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ color: '#9ca3af', fontSize: '12px', display: 'block', marginBottom: '6px' }}>正文</label>
|
||||
<textarea
|
||||
id="publishContent"
|
||||
placeholder="请输入行情详细信息..."
|
||||
style={{ width: '100%', padding: '12px', background: '#1f2937', border: '1px solid #374151', borderRadius: '10px', color: '#fff', fontSize: '14px', boxSizing: 'border-box', resize: 'vertical', minHeight: '80px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handlePublishDeal}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px',
|
||||
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
border: 'none',
|
||||
borderRadius: '10px',
|
||||
color: '#fff',
|
||||
fontSize: '15px',
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
🚀 提交发布
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 列表过滤:全部 / 寻号 / 行情 */}
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
|
||||
<button
|
||||
onClick={() => setFilterType('all')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: filterType === 'all' ? '#3b82f6' : '#374151',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#fff',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilterType('seek')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: filterType === 'seek' ? '#10b981' : '#374151',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#fff',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
🔍 寻号
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilterType('deal')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: filterType === 'deal' ? '#f59e0b' : '#374151',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#fff',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
💰 行情
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 发布列表 */}
|
||||
{loading ? (
|
||||
<div style={{ color: '#9ca3af', textAlign: 'center', padding: '30px' }}>加载中...</div>
|
||||
) : filteredList.length === 0 ? (
|
||||
<div style={{ color: '#9ca3af', textAlign: 'center', padding: '30px', fontSize: '14px' }}>暂无发布记录</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{filteredList.map(item => (
|
||||
<div key={item.id} style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', border: '1px solid #334155' }}>
|
||||
{/* 标题行 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '8px' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '15px', fontWeight: '600', flex: 1 }}>{item.title}</div>
|
||||
<span style={{
|
||||
background: item.info_type === 'deal' ? 'rgba(245,158,11,0.2)' : 'rgba(16,185,129,0.2)',
|
||||
color: item.info_type === 'deal' ? '#f59e0b' : '#10b981',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '20px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
{item.info_type === 'deal' ? '💰 行情' : '🔍 寻号'}
|
||||
</span>
|
||||
</div>
|
||||
{/* 日期+用户名 */}
|
||||
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}>
|
||||
📅 {formatDate(item.created_at)} | 👤 {item.user_name || '匿名用户'}
|
||||
</div>
|
||||
{/* 正文 - 默认收起,点击展开 */}
|
||||
{item.content && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div
|
||||
onClick={() => 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)'
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '16px' }}>{expandedItems[item.id] ? '▼' : '▶'}</span>
|
||||
<span>{expandedItems[item.id] ? '收起详情' : '展开查看详情'}</span>
|
||||
</div>
|
||||
{expandedItems[item.id] && (
|
||||
<div style={{
|
||||
color: '#e2e8f0',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.8',
|
||||
marginTop: '12px',
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
borderLeft: '3px solid #10b981'
|
||||
}}>
|
||||
{item.content.split('\n').map((line, i) => (
|
||||
<div key={i} style={{ marginBottom: i < item.content.split('\n').length - 1 ? '6px' : 0 }}>
|
||||
{line || ' '}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', marginTop: '8px' }}>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button onClick={() => handleEdit(item)} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #3b82f6', background: 'transparent', color: '#3b82f6', cursor: 'pointer', fontSize: '12px' }}>✏️ 编辑</button>
|
||||
<button onClick={() => handleDelete(item.id)} style={{ padding: '6px 12px', borderRadius: '6px', border: '1px solid #ef4444', background: 'transparent', color: '#ef4444', cursor: 'pointer', fontSize: '12px' }}>🗑️ 删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 编辑弹窗 */}
|
||||
{editingItem && (
|
||||
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, padding: '20px' }}>
|
||||
<div style={{ background: '#1f2937', borderRadius: '16px', padding: '24px', width: '100%', maxWidth: '420px', border: '1px solid #374151' }}>
|
||||
<h3 style={{ color: '#f9fafb', marginBottom: '20px', fontSize: '18px', fontWeight: '600' }}>✏️ 编辑信息</h3>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>标题</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingItem.title}
|
||||
onChange={e => 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' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={{ color: '#9ca3af', fontSize: '13px', display: 'block', marginBottom: '8px' }}>内容</label>
|
||||
<textarea
|
||||
value={editingItem.content}
|
||||
onChange={e => setEditingItem({ ...editingItem, content: e.target.value })}
|
||||
rows={4}
|
||||
style={{ width: '100%', padding: '12px', background: '#0f172a', border: '1px solid #374151', borderRadius: '8px', color: '#fff', boxSizing: 'border-box', fontSize: '14px', resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button onClick={saveEdit} style={{ flex: 1, padding: '12px', background: '#10b981', border: 'none', borderRadius: '8px', color: '#fff', cursor: 'pointer', fontSize: '14px', fontWeight: '600' }}>💾 保存</button>
|
||||
<button onClick={() => setEditingItem(null)} style={{ flex: 1, padding: '12px', border: '1px solid #374151', borderRadius: '8px', background: 'transparent', color: '#9ca3af', cursor: 'pointer', fontSize: '14px' }}>取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
/**
|
||||
* List - 藏品列表页面
|
||||
* Version: 1.2.93x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
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 +24,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 +74,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 +371,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 +414,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 +518,18 @@ export default function List() {
|
|||
<div style={{ minHeight: '100vh', overflowY: 'auto', background: '#0f172a', paddingBottom: '70px' }}>
|
||||
<div style={{ padding: '20px', background: 'rgba(255,255,255,0.02)', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
|
||||
<div style={{ color: '#fff', fontSize: '20px', }}>我的藏品 <span style={{ fontSize: '14px', color: '#fbbf24' }}>({filteredCollections.length})</span></div>
|
||||
<div style={{ color: '#fff', fontSize: '20px' }}>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button onClick={() => setActiveTab('collections')}
|
||||
style={{ padding: '6px 16px', background: activeTab === 'collections' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'collections' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
|
||||
我的藏品 ({filteredCollections.length})
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('deals')}
|
||||
style={{ padding: '6px 16px', background: activeTab === 'deals' ? '#fbbf24' : 'rgba(255,255,255,0.05)', color: activeTab === 'deals' ? '#1e293b' : '#fff', border: 'none', borderRadius: '6px', fontSize: '13px', fontWeight: '600', cursor: 'pointer' }}>
|
||||
我的行情 ({filteredDeals.length})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
{filter && filterType && (
|
||||
<button
|
||||
|
|
@ -482,6 +556,8 @@ export default function List() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'collections' && (
|
||||
<>
|
||||
{/* 搜索框 - 全字段搜索 */}
|
||||
<div style={{ position: 'relative', marginBottom: '16px' }}>
|
||||
<input type="text"
|
||||
|
|
@ -557,9 +633,11 @@ export default function List() {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 分页组件 */}
|
||||
{pagination.pages > 1 && (
|
||||
{/* 分页组件 - 仅在藏品tab下显示 */}
|
||||
{activeTab === 'collections' && pagination.pages > 1 && (
|
||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center', justifyContent: 'flex-start', marginBottom: '12px', padding: '8px', background: 'rgba(255,255,255,0.03)', borderRadius: '8px' }}>
|
||||
<button onClick={() => { setPage(Math.max(1, page - 1)) }} disabled={page === 1} style={{ padding: '4px 10px', borderRadius: '6px', border: 'none', background: page === 1 ? 'rgba(255,255,255,0.1)' : '#3b82f6', color: '#fff', cursor: page === 1 ? 'not-allowed' : 'pointer', opacity: page === 1 ? 0.5 : 1 }}>上一页</button>
|
||||
|
||||
|
|
@ -576,6 +654,49 @@ export default function List() {
|
|||
</div>
|
||||
|
||||
<div style={{ padding: '16px' }}>
|
||||
{/* 行情tab内容 */}
|
||||
{activeTab === 'deals' && (
|
||||
<div>
|
||||
{/* 行情搜索框 */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<input type="text"
|
||||
placeholder="🔍 搜索行情..."
|
||||
value={dealSearch}
|
||||
onChange={e => 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'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{dealsLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>加载中...</div>
|
||||
) : filteredDeals.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '60px 0' }}>
|
||||
<div style={{ fontSize: '50px', opacity: 0.3 }}>📊</div>
|
||||
<div style={{ color: '#64748b', marginTop: '16px' }}>{dealSearch ? '没有匹配的行情' : '暂无行情记录'}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
{filteredDeals.map(deal => (
|
||||
<DealListItem key={deal.id} deal={deal} onRefresh={fetchMyDeals} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 藏品tab内容 */}
|
||||
{activeTab === 'collections' && (
|
||||
<>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>加载中...</div>
|
||||
) : filteredCollections.length === 0 ? (
|
||||
|
|
@ -597,7 +718,237 @@ export default function List() {
|
|||
) : (
|
||||
filteredCollections.map(item => <ListItem key={item.id} item={item} />)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 行情列表项组件
|
||||
function DealListItem({ deal, onRefresh }) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [editing, setEditing] = useState(false)
|
||||
const [editForm, setEditForm] = useState({})
|
||||
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||
|
||||
const openEdit = () => {
|
||||
// 从content中解析平台、出售者、购买者
|
||||
let platform = '', seller = '', buyer = ''
|
||||
if (deal.content) {
|
||||
const platformMatch = deal.content.match(/平台:\s*([^\n]+)/)
|
||||
const sellerMatch = deal.content.match(/出售者:\s*([^\n]+)/)
|
||||
const buyerMatch = deal.content.match(/购买者:\s*([^\n]+)/)
|
||||
if (platformMatch) platform = platformMatch[1].trim()
|
||||
if (sellerMatch) seller = sellerMatch[1].trim()
|
||||
if (buyerMatch) buyer = buyerMatch[1].trim()
|
||||
}
|
||||
setEditForm({
|
||||
title: deal.title,
|
||||
content: deal.content,
|
||||
deal_price: deal.deal_price,
|
||||
deal_date: deal.deal_date ? (typeof deal.deal_date === 'string' ? deal.deal_date.split('T')[0] : '') : '',
|
||||
packaging: deal.packaging || '单张',
|
||||
is_graded: deal.is_graded || false,
|
||||
grading_company: deal.grading_company || '',
|
||||
grading_score: deal.grading_score || '',
|
||||
category: deal.category || '',
|
||||
deal_no: deal.deal_no || '',
|
||||
platform: platform,
|
||||
seller: seller,
|
||||
buyer: buyer
|
||||
})
|
||||
setEditing(true)
|
||||
}
|
||||
|
||||
const saveEdit = async () => {
|
||||
const token = localStorage.getItem('token')
|
||||
try {
|
||||
await fetch(`${API_BASE}/api/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 (
|
||||
<div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: '10px', padding: '12px', border: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
{/* 简要展示 - 两行显示关键信息 */}
|
||||
<div onClick={() => setExpanded(!expanded)} style={{ cursor: 'pointer', padding: '10px', background: 'rgba(0,0,0,0.2)', borderRadius: '8px' }}>
|
||||
{/* 第一行:成交日期(月-日)+ 冠字号 + 包装 + 评级分数 + 价格 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px', flexWrap: 'wrap', gap: '4px' }}>
|
||||
{deal.deal_date && <span style={{ color: '#64748b', fontSize: '11px' }}>{deal.deal_date.slice(5)}</span>}
|
||||
<span style={{ color: '#fbbf24', fontSize: '13px', fontFamily: 'monospace' }}>{deal.title?.split('-')[0] || '-'}</span>
|
||||
{deal.packaging && <span style={{ background: 'rgba(139,92,246,0.15)', color: '#a78bfa', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{deal.packaging}</span>}
|
||||
{deal.grading_company && <span style={{ background: 'rgba(6,182,212,0.15)', color: '#06b6d4', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{deal.grading_score}</span>}
|
||||
<span style={{ color: '#22c55e', fontSize: '14px', fontWeight: 'bold' }}>¥{deal.deal_price?.toLocaleString()}</span>
|
||||
</div>
|
||||
{/* 第二行:编号 + 分类 + 大小号 + 展开箭头 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
|
||||
{deal.deal_no && <span style={{ color: '#fff', fontSize: '10px' }}>{deal.deal_no}</span>}
|
||||
{deal.category && <span style={{ background: 'rgba(59,130,246,0.15)', color: '#60a5fa', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{deal.category}</span>}
|
||||
{deal.content && (() => {
|
||||
const sizeMatch = deal.content.match(/大小号:\s*([^\n]+)/)
|
||||
return sizeMatch && <span style={{ background: 'rgba(34,197,94,0.15)', color: '#22c55e', fontSize: '10px', padding: '2px 6px', borderRadius: '4px' }}>{sizeMatch[1]}</span>
|
||||
})()}
|
||||
</div>
|
||||
<span style={{ color: '#64748b', fontSize: '12px' }}>{expanded ? '▲ 收起' : '▼展开'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 展开详情 */}
|
||||
{expanded && (
|
||||
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
{editing ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{/* 冠字号 */}
|
||||
<div>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>冠字号</div>
|
||||
<input value={editForm.title?.split('-')[0] || ''} onChange={e => setEditForm({...editForm, title: `${e.target.value}-¥${editForm.deal_price}`})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fbbf24', fontSize: '14px', fontFamily: 'monospace' }} />
|
||||
</div>
|
||||
|
||||
{/* 价格和日期 */}
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>价格</div>
|
||||
<input type="number" value={editForm.deal_price} onChange={e => {
|
||||
const val = parseFloat(e.target.value) || 0
|
||||
const serial = editForm.title?.split('-')[0] || ''
|
||||
setEditForm({...editForm, deal_price: val, title: `${serial}-¥${val}`})
|
||||
}} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#22c55e', fontSize: '14px' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>日期</div>
|
||||
<input type="date" value={editForm.deal_date} onChange={e => setEditForm({...editForm, deal_date: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '14px' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 包装和分类 */}
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>包装</div>
|
||||
<select value={editForm.packaging} onChange={e => setEditForm({...editForm, packaging: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
|
||||
<option value="单张">单张</option>
|
||||
<option value="标十">标十</option>
|
||||
<option value="标百">标百</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>分类</div>
|
||||
<input value={editForm.category || ''} onChange={e => setEditForm({...editForm, category: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 评级 */}
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>评级机构</div>
|
||||
<select value={editForm.grading_company || ''} onChange={e => setEditForm({...editForm, grading_company: e.target.value, is_graded: !!e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
|
||||
<option value="">未评级</option>
|
||||
<option value="PCGS">PCGS</option>
|
||||
<option value="PMG">PMG</option>
|
||||
<option value="ACG">ACG</option>
|
||||
<option value="爱藏">爱藏</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>评级分数</div>
|
||||
<input value={editForm.grading_score || ''} onChange={e => setEditForm({...editForm, grading_score: e.target.value})} placeholder="如: PC69, 67+" style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#06b6d4', fontSize: '13px' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 平台和出售者购买者 */}
|
||||
<div>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>平台</div>
|
||||
<select value={editForm.platform || ''} onChange={e => setEditForm({...editForm, platform: e.target.value})} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }}>
|
||||
<option value="">请选择</option>
|
||||
<option value="淘宝">淘宝</option>
|
||||
<option value="咸鱼">咸鱼</option>
|
||||
<option value="抖音">抖音</option>
|
||||
<option value="快手">快手</option>
|
||||
<option value="微拍堂">微拍堂</option>
|
||||
<option value="拼多多">拼多多</option>
|
||||
<option value="一尘">一尘</option>
|
||||
<option value="爱藏">爱藏</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>出售者</div>
|
||||
<input value={editForm.seller || ''} onChange={e => setEditForm({...editForm, seller: e.target.value})} placeholder="请输入出售者" style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>购买者</div>
|
||||
<input value={editForm.buyer || ''} onChange={e => setEditForm({...editForm, buyer: e.target.value})} placeholder="请输入购买者" style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 备注 */}
|
||||
<div>
|
||||
<div style={{ fontSize: '11px', color: '#64748b', marginBottom: '4px' }}>备注</div>
|
||||
<textarea value={editForm.content || ''} onChange={e => setEditForm({...editForm, content: e.target.value})} rows={2} style={{ width: '100%', padding: '10px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '6px', color: '#fff', fontSize: '13px', boxSizing: 'border-box' }} />
|
||||
</div>
|
||||
|
||||
{/* 保存取消按钮 */}
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button onClick={saveEdit} style={{ flex: 1, padding: '12px', background: '#22c55e', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>保存</button>
|
||||
<button onClick={() => setEditing(false)} style={{ flex: 1, padding: '12px', background: '#64748b', color: '#fff', border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '600', cursor: 'pointer' }}>取消</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '8px' }}>
|
||||
{deal.deal_no && <div>编号: <span style={{ color: '#fbbf24' }}>{deal.deal_no}</span></div>}
|
||||
<div>冠字号: <span style={{ color: '#fbbf24' }}>{deal.title?.split('-')[0] || '-'}</span></div>
|
||||
<div>价格: <span style={{ color: '#22c55e' }}>¥{deal.deal_price?.toLocaleString()}</span></div>
|
||||
<div>日期: {deal.deal_date}</div>
|
||||
<div>包装: {deal.packaging || '单张'}</div>
|
||||
<div>分类: {deal.category || '-'}</div>
|
||||
<div>评级: {deal.grading_company ? `${deal.grading_company} ${deal.grading_score || ''}` : '未评级'}</div>
|
||||
{deal.content && (() => {
|
||||
const platformMatch = deal.content.match(/平台:\s*([^\n]+)/)
|
||||
const sellerMatch = deal.content.match(/出售者:\s*([^\n]+)/)
|
||||
const buyerMatch = deal.content.match(/购买者:\s*([^\n]+)/)
|
||||
const sizeMatch = deal.content.match(/大小号:\s*([^\n]+)/)
|
||||
const tailMatch = deal.content.match(/尾号:\s*([^\n]+)/)
|
||||
return (
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
{platformMatch && <div>平台: {platformMatch[1]}</div>}
|
||||
{sellerMatch && <div>出售者: {sellerMatch[1]}</div>}
|
||||
{buyerMatch && buyerMatch[1].trim() !== '-' && <div>购买者: {buyerMatch[1]}</div>}
|
||||
{tailMatch && <div>尾号: {tailMatch[1]}</div>}
|
||||
{sizeMatch && <div>大小号: <span style={{ color: '#60a5fa' }}>{sizeMatch[1]}</span></div>}
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
{deal.content && <div style={{ marginTop: '8px', color: '#64748b', fontSize: '11px' }}>{deal.content}</div>}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '12px' }}>
|
||||
<button onClick={openEdit} style={{ flex: 1, padding: '8px', background: 'rgba(59,130,246,0.2)', color: '#60a5fa', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '12px' }}>编辑</button>
|
||||
<button onClick={deleteDeal} style={{ flex: 1, padding: '8px', background: 'rgba(239,68,68,0.2)', color: '#ef4444', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '12px' }}>删除</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* Login - 登录页面
|
||||
* Version: 1.2.85x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { APP_VERSION } from '../config/version'
|
||||
import { api } from '../utils/api'
|
||||
|
|
@ -18,7 +24,8 @@ export default function Login() {
|
|||
confirmPassword: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
verifyCode: ''
|
||||
verifyCode: '',
|
||||
inviteCode: ''
|
||||
})
|
||||
const [registerLoading, setRegisterLoading] = useState(false)
|
||||
const [registerError, setRegisterError] = useState('')
|
||||
|
|
@ -148,6 +155,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 +175,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 +594,7 @@ export default function Login() {
|
|||
</div>
|
||||
|
||||
{/* 确认密码 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<input
|
||||
type="password"
|
||||
value={registerData.confirmPassword}
|
||||
|
|
@ -603,6 +613,26 @@ export default function Login() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 邀请码(选填) */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={registerData.inviteCode}
|
||||
onChange={(e) => 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'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 用户协议勾选 */}
|
||||
<div style={{ marginBottom: '20px', display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
|
||||
|
|
@ -626,7 +656,7 @@ export default function Login() {
|
|||
<button
|
||||
onClick={() => {
|
||||
setShowRegister(false)
|
||||
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '' })
|
||||
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '', inviteCode: '' })
|
||||
setRegisterError('')
|
||||
setCodeSent(false)
|
||||
setCodeCountdown(0)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* News - 资讯列表页面
|
||||
* Version: 1.2.80x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import YichensBoard from './YichensBoard'
|
||||
|
||||
|
|
@ -32,6 +38,9 @@ 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)
|
||||
|
|
@ -60,7 +69,7 @@ export default function News() {
|
|||
// 获取资讯列表
|
||||
useEffect(() => {
|
||||
fetchInfoList()
|
||||
}, [activeTab])
|
||||
}, [activeTab, dealDate])
|
||||
|
||||
// 自动生成寻号标题
|
||||
useEffect(() => {
|
||||
|
|
@ -71,13 +80,29 @@ export default function News() {
|
|||
const fetchInfoList = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// 寻配号对所有人公开,无需登录即可查看
|
||||
const token = localStorage.getItem('token')
|
||||
// 根据tab获取不同类型的数据
|
||||
const type = activeTab === 'seek' ? 'seek' : activeTab === 'deal' ? 'deal' : 'yichen'
|
||||
// 始终传递token,以便获取准确的matched_count
|
||||
const headers = token ? { Authorization: `Bearer ${token}` } : {}
|
||||
const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}&page=${currentPage}&page_size=50`, { 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
|
||||
}
|
||||
} else if (activeTab === 'seek') {
|
||||
url += (url.includes("?") ? "&" : "?") + "page=" + currentPage + "&page_size=500"
|
||||
}
|
||||
|
||||
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))
|
||||
|
|
@ -90,7 +115,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([])
|
||||
|
|
@ -257,12 +296,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) }
|
||||
|
|
@ -273,7 +312,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()
|
||||
|
|
@ -337,9 +376,9 @@ export default function News() {
|
|||
|
||||
// Tab切换
|
||||
const tabs = [
|
||||
{ key: 'seek', label: '🔍 寻配号' },
|
||||
{ key: 'deal', label: '💰 成交行情' },
|
||||
{ key: 'yichen', label: '📊 一尘看板' }
|
||||
{ key: 'seek', label: '寻配号' },
|
||||
{ key: 'deal', label: '成交行情' },
|
||||
{ key: 'yichen', label: '一尘看板' }
|
||||
]
|
||||
|
||||
const formatDate = (dateStr) => {
|
||||
|
|
@ -459,7 +498,7 @@ export default function News() {
|
|||
</div>
|
||||
)}
|
||||
<h3 style={{ color: '#fff', marginBottom: '16px' }}>
|
||||
{activeTab === 'seek' ? '🔍 寻配号信息' : activeTab === 'deal' ? '💰 成交行情信息' : '📊 一尘看板'}
|
||||
{activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? `成交行情信息 (${dealDate})` : '一尘看板'}
|
||||
{activeTab === 'seek' && (
|
||||
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
|
||||
<button onClick={() => { setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部</button>
|
||||
|
|
@ -468,9 +507,18 @@ export default function News() {
|
|||
<button onClick={() => setShowSeekPublish(!showSeekPublish)} style={{ padding: '6px 12px', background: showSeekPublish ? '#6b7280' : '#10b981', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>发布寻号</button>
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'deal' && (
|
||||
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
|
||||
<input type="date" value={dealDate} onChange={e => setDealDate(e.target.value)}
|
||||
style={{ padding: '6px 12px', background: '#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }} />
|
||||
</div>
|
||||
)}
|
||||
</h3>
|
||||
|
||||
{loading ? (
|
||||
{/* 一尘看板独立渲染 */}
|
||||
{activeTab === 'yichen' ? (
|
||||
<YichensBoard />
|
||||
) : loading ? (
|
||||
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>加载中...</div>
|
||||
) : infoList.length === 0 ? (
|
||||
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>
|
||||
|
|
@ -478,7 +526,215 @@ export default function News() {
|
|||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{activeTab === 'yichen' ? <YichensBoard /> : infoList.map(item => {
|
||||
{/* 成交行情 - 价格统计表格 */}
|
||||
{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 (
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '16px', marginBottom: '12px' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginBottom: '12px' }}>
|
||||
{versions.map(v => (
|
||||
<button key={v} onClick={() => 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ padding: '8px', textAlign: 'left', color: '#94a3b8', borderBottom: '1px solid rgba(255,255,255,0.1)' }}></th>
|
||||
{packagings.map(p => (
|
||||
<th key={p} style={{ padding: '8px', textAlign: 'center', color: '#94a3b8', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>{p}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(() => {
|
||||
// 计算每个分类的平均价格
|
||||
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 (
|
||||
<tr key={cat}>
|
||||
<td style={{ padding: '8px', color: '#fbbf24', fontWeight: '500', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>{cat}</td>
|
||||
{rowData.map((d, i) => (
|
||||
<td key={i} style={{ padding: '8px', textAlign: 'center', borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
|
||||
{d ? (
|
||||
<div style={{ color: '#22c55e', fontWeight: '600', cursor: 'pointer' }}
|
||||
onClick={() => setDealDetailItems(d.items)}>
|
||||
¥{d.avg.toLocaleString()}
|
||||
<span style={{ color: '#64748b', fontSize: '11px', marginLeft: '4px' }}>({d.count})</span>
|
||||
</div>
|
||||
) : <span style={{ color: '#475569' }}>-</span>}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* 成交行情详情弹窗 */}
|
||||
{dealDetailItems && (
|
||||
<div style={{
|
||||
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
||||
background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: '20px'
|
||||
}}>
|
||||
<div style={{
|
||||
background: '#1e293b', borderRadius: '12px', padding: '16px',
|
||||
maxWidth: '600px', maxHeight: '80vh', overflow: 'auto', width: '100%'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<div style={{ color: '#fff', fontSize: '16px', fontWeight: '600' }}>成交详情列表</div>
|
||||
<button onClick={() => setDealDetailItems(null)}
|
||||
style={{ background: 'none', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>✕</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{([...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 (
|
||||
<div key={idx} style={{
|
||||
background: 'rgba(255,255,255,0.05)', borderRadius: '8px', padding: '12px',
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center'
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ color: '#fbbf24', fontWeight: '600', marginBottom: '4px' }}>
|
||||
{(item.title || '').split('-')[0]}
|
||||
{size && <span style={{ color: '#94a3b8', marginLeft: '8px' }}>| {size}</span>}
|
||||
{grade && <span style={{ color: '#06b6d4', marginLeft: '8px' }}>| {grade}</span>}
|
||||
</div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '12px' }}>
|
||||
{item.deal_date} | {item.category} | {item.packaging} | {platform}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '700' }}>
|
||||
¥{item.deal_price?.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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 (
|
||||
<div key={item.id} style={{ borderBottom: '1px solid #334155', padding: '12px 0' }}>
|
||||
{/* 标题行:冠字号 + 价格 */}
|
||||
<div style={{ fontSize: '15px', fontWeight: '500', marginBottom: '6px' }}>
|
||||
<span style={{ color: '#fbbf24' }}>{serial}</span>
|
||||
<span style={{ color: '#22c55e', marginLeft: '12px' }}>¥{item.deal_price?.toLocaleString()}</span>
|
||||
<span style={{ color: '#64748b', marginLeft: '12px', fontSize: '12px' }}>{item.deal_no}</span>
|
||||
</div>
|
||||
{/* 信息行:版别 | 包装 | 分类 | 评级 | 分数 | 平台 */}
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8' }}>
|
||||
<span style={{ color: '#fff' }}>{version}</span>
|
||||
<span style={{ margin: '0 8px' }}>|</span>
|
||||
<span style={{ color: '#3b82f6' }}>{packaging}</span>
|
||||
<span style={{ margin: '0 8px' }}>|</span>
|
||||
<span style={{ color: '#fbbf24' }}>{category}</span>
|
||||
{gradingCompany && <><span style={{ margin: '0 8px' }}>|</span><span>{gradingCompany}</span></>}
|
||||
{grade && <><span style={{ margin: '0 8px' }}>|</span><span>{grade}</span></>}
|
||||
<span style={{ margin: '0 8px' }}>|</span>
|
||||
<span>{platform}</span>
|
||||
<span style={{ margin: '0 8px' }}>|</span>
|
||||
<span>{dealDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 解析正文中的号码特征和联系方式
|
||||
const content = item.content || ''
|
||||
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
|
||||
|
|
@ -830,7 +1086,7 @@ export default function News() {
|
|||
)}
|
||||
|
||||
{/* 分页组件 */}
|
||||
{totalPages > 1 && (
|
||||
{infoList.length > 0 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px', padding: '16px', marginTop: '8px' }}>
|
||||
<button
|
||||
onClick={() => { if (currentPage > 1) { setCurrentPage(currentPage - 1); fetchInfoList() } }}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,224 @@
|
|||
/**
|
||||
* News_YichensBoard - 一尘帖子页面
|
||||
* Version: 1.2.75x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
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 }) => (
|
||||
<div 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'
|
||||
}}>
|
||||
<div style={{ color: '#9ca3af', fontSize: 11, marginBottom: 4 }}>{label}</div>
|
||||
<div style={{ color: color || '#fff', fontSize: 20, fontWeight: 'bold' }}>{value}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<div style={{ color: '#f9fafb', fontSize: 14, fontWeight: 600, marginBottom: 10 }}>
|
||||
📈 连体钞/纪念钞 <span style={{ color: '#9ca3af', fontWeight: 400 }}>{dateStr}</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
||||
<StatCard label='全部' value={todayStats.total} color='#3b82f6' onClick={() => { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||
<StatCard label='出售' value={todayStats.deals} color='#10b981' onClick={() => { setPostTypeFilter('deal'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||
<StatCard label='求购' value={todayStats.wants} color='#f59e0b' onClick={() => { setPostTypeFilter('want'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||
<StatCard label='其他' value={todayStats.others || 0} color='#8b5cf6' onClick={() => { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 12, marginBottom: 20, border: '1px solid #374151' }}>
|
||||
<div style={{ color: '#f9fafb', fontSize: 14, fontWeight: 600, marginBottom: 8 }}>📊 今日分类统计</div>
|
||||
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', paddingBottom: 4 }}>
|
||||
{todayCategory.map(cat => (
|
||||
<span key={cat.category} style={{ padding: '4px 10px', background: 'rgba(59,130,246,0.2)', borderRadius: 16, color: '#93c5fd', fontSize: 11, whiteSpace: 'nowrap' }}>
|
||||
{cat.category} ({cat.count})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索标题、内容、分类..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => { 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' }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => { setSearchKeyword(''); setPage(1); fetchPosts(1, '') }}
|
||||
style={{ padding: '10px 16px', borderRadius: 8, border: 'none', background: '#4b5563', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
{searchKeyword && <div style={{ color: '#9ca3af', fontSize: 12, marginTop: 6 }}>搜索: "{searchKeyword}",找到 {posts.length} 条结果</div>}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||
{[{key:'all',label:'全部'},{key:'deal',label:'出售'},{key:'want',label:'求购'},{key:'other',label:'其他'}].map(k => (
|
||||
<button key={k.key} onClick={() => { 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
{[{key:'',label:'全部'},{key:'龙',label:'龙钞'},{key:'蛇',label:'蛇钞'},{key:'马',label:'马钞'},{key:'其他',label:'其他'}].map(k => (
|
||||
<button key={k.key} onClick={() => { 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}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading ? <div style={{textAlign:'center',color:'#9ca3af',padding:40}}>加载中...</div> : (
|
||||
<div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{posts.map(post => (
|
||||
<div key={post.post_id} style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
|
||||
<div onClick={() => togglePost(post.post_id)} style={{ cursor: 'pointer' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ color: '#fff', fontSize: 15, fontWeight: 500, flex: 1 }}>{post.title||'无标题'}</span>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<span style={{ color: post.post_type==='deal'?'#10b981':post.post_type==='want'?'#f59e0b':post.post_type==='normal'?'#8b5cf6':'#9ca3af', fontSize: 12 }}>
|
||||
{post.post_type==='deal'?'出售':post.post_type==='want'?'求购':post.post_type==='normal'?'其他':'普通'}
|
||||
</span>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 11, background: post.category?.includes('龙') ? 'rgba(251,191,36,0.4)' : post.category?.includes('马') ? 'rgba(180,83,9,0.4)' : post.category?.includes('蛇') ? 'rgba(249,168,212,0.4)' : 'rgba(139,92,246,0.4)', color: post.category?.includes('龙') ? '#fde047' : post.category?.includes('马') ? '#d97706' : post.category?.includes('蛇') ? '#fbcfe8' : '#c4b5fd' }}>
|
||||
{post.category || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#9ca3af', fontSize: 12 }}>
|
||||
<span>{post.author_username||'未知'}</span>
|
||||
<span>{post.post_time?.substring(0,16)||''}</span>
|
||||
</div>
|
||||
</div>
|
||||
{expandedPosts[post.post_id] && post.content && (
|
||||
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #374151' }}>
|
||||
<div style={{ color: '#e2e8f0', fontSize: 13, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 300, overflowY: 'auto' }}>
|
||||
{post.content}
|
||||
</div>
|
||||
{post.url && <a href={post.url} target='_blank' rel='noopener noreferrer' style={{ display:'inline-block', marginTop:12, padding:'8px 16px', background:'rgba(59,130,246,0.2)', borderRadius:8, color:'#60a5fa', textDecoration:'none', fontSize:13 }}>查看原帖</a>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 分页按钮移到页面底部 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#1e293b', borderRadius: 12, marginTop: 16 }}>
|
||||
<div style={{ color: '#9ca3af', fontSize: 12 }}>
|
||||
共 {totalPosts} 条帖子,{Math.ceil(totalPosts / 390)} 页,当前第 {page} 页
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{page > 1 ? (
|
||||
<button onClick={() => { 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 }}>上一页</button>
|
||||
) : (
|
||||
<span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>上一页</span>
|
||||
)}
|
||||
{posts.length >= 390 ? (
|
||||
<button onClick={() => { 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 }}>下一页</button>
|
||||
) : (
|
||||
<span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>下一页</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* Settings - 设置页面
|
||||
* Version: 1.2.75x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
// 统计分析页面 - 支持点击跳转
|
||||
/**
|
||||
* Stats - 统计页面
|
||||
* Version: 1.2.70x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { APP_VERSION } from '../config/version'
|
||||
|
||||
|
|
@ -103,7 +108,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 +151,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) => {
|
||||
|
|
|
|||
|
|
@ -1,50 +1,70 @@
|
|||
/**
|
||||
* YichensBoard - 一尘看板页面
|
||||
* Version: 1.2.70x
|
||||
* 更新:
|
||||
*/
|
||||
|
||||
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 [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [expandedPosts, setExpandedPosts] = useState({})
|
||||
const [postTypeFilter, setPostTypeFilter] = useState('all')
|
||||
const [categoryFilter, setCategoryFilter] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [totalPosts, setTotalPosts] = useState(0)
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||
|
||||
const today = new Date()
|
||||
const dateStr = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate()
|
||||
|
||||
useEffect(() => { fetchTodayStats(); fetchPosts(1, '') }, [])
|
||||
useEffect(() => {
|
||||
console.log('YichensBoard: 开始加载数据')
|
||||
fetchTodayStats()
|
||||
}, [])
|
||||
|
||||
const fetchTodayStats = async () => {
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/api/yichens/stats/today')
|
||||
setTodayStats(await res.json())
|
||||
} catch(e) { console.error(e) }
|
||||
console.log('YichensBoard: 请求 /api/yichens/stats/today')
|
||||
const res = await fetch('/api/yichens/stats/today')
|
||||
if (!res.ok) throw new Error('stats API error: ' + res.status)
|
||||
const data = await res.json()
|
||||
console.log('YichensBoard: stats data', data)
|
||||
setTodayStats(data)
|
||||
} catch(e) {
|
||||
console.error('YichensBoard: fetchTodayStats error', e)
|
||||
setError(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTodayCategory = async () => {
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/api/yichens/stats/today-category')
|
||||
setTodayCategory(await res.json())
|
||||
} catch(e) { console.error(e) }
|
||||
}
|
||||
|
||||
const fetchPosts = async (p, cat) => {
|
||||
const fetchPosts = async (p, cat, kw) => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const currentPage = p !== undefined ? p : page
|
||||
const currentCat = cat !== undefined ? cat : categoryFilter
|
||||
const searchKw = kw !== undefined ? kw : searchKeyword
|
||||
|
||||
let url = API_BASE + '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390)
|
||||
let url = '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390)
|
||||
if (postTypeFilter === 'deal') url += '&post_type=deal'
|
||||
else if (postTypeFilter === 'want') url += '&post_type=want'
|
||||
else if (postTypeFilter === 'other') url += '&post_type=normal'
|
||||
if (searchKw && searchKw.trim()) url += '&keyword=' + encodeURIComponent(searchKw.trim())
|
||||
|
||||
try {
|
||||
console.log('YichensBoard: 请求 posts', url)
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) throw new Error('posts API error: ' + res.status)
|
||||
let data = await res.json() || []
|
||||
if (currentCat) {
|
||||
// 确保data是数组
|
||||
if (!Array.isArray(data)) {
|
||||
data = data.posts || data.data || []
|
||||
}
|
||||
console.log('YichensBoard: posts data count', data.length)
|
||||
if (currentCat && Array.isArray(data)) {
|
||||
if (currentCat === '龙') {
|
||||
data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
|
||||
} else if (currentCat === '蛇') {
|
||||
|
|
@ -55,36 +75,52 @@ export default function YichensBoard() {
|
|||
data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马'))
|
||||
}
|
||||
}
|
||||
// 搜索关键词过滤 - 强匹配(完全包含)
|
||||
if (searchKeyword) {
|
||||
const kw = searchKeyword.trim()
|
||||
if (kw) {
|
||||
data = data.filter(p =>
|
||||
(p.title && p.title.includes(kw)) ||
|
||||
(p.content && p.content.includes(kw)) ||
|
||||
(p.category && p.category.includes(kw)) ||
|
||||
(p.contact && p.contact.includes(kw))
|
||||
)
|
||||
// 确保data是数组
|
||||
if (!Array.isArray(data)) {
|
||||
data = data.posts || data.data || []
|
||||
}
|
||||
console.log('YichensBoard: posts data count', data.length)
|
||||
if (currentCat && Array.isArray(data)) {
|
||||
if (currentCat === '龙') {
|
||||
data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
|
||||
} else if (currentCat === '蛇') {
|
||||
data = data.filter(p => p.category && p.category.includes('蛇'))
|
||||
} else if (currentCat === '马') {
|
||||
data = data.filter(p => p.category && p.category.includes('马'))
|
||||
} else if (currentCat === '其他') {
|
||||
data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马'))
|
||||
}
|
||||
}
|
||||
setPosts(data)
|
||||
setTotalPosts(todayStats.total || 0)
|
||||
} catch { setPosts([]) }
|
||||
setLoading(false)
|
||||
// 支持新格式 {posts:[], total:xxx} 或旧格式 [{},{}]
|
||||
const postsArray = Array.isArray(data) ? data : (data.posts || [])
|
||||
const totalCount = data.total || todayStats.total || postsArray.length
|
||||
setPosts(postsArray)
|
||||
setTotalPosts(totalCount)
|
||||
} catch(e) {
|
||||
console.error('YichensBoard: fetchPosts error', e)
|
||||
setError(e.message)
|
||||
setPosts([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { if (todayStats.total > 0) fetchTodayCategory() }, [todayStats])
|
||||
useEffect(() => {
|
||||
if (todayStats.total > 0) {
|
||||
fetchPosts(1, categoryFilter, searchKeyword)
|
||||
}
|
||||
}, [todayStats, categoryFilter, postTypeFilter])
|
||||
|
||||
// 搜索关键词变化时重新获取数据
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
fetchPosts(1, '')
|
||||
fetchPosts(1, categoryFilter, searchKeyword)
|
||||
}, [searchKeyword])
|
||||
|
||||
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
|
||||
|
||||
const StatCard = ({ label, value, color, onClick }) => (
|
||||
<div onClick={onClick} style={{
|
||||
<div onClick={() => { setLoading(true); if (onClick) onClick(); }}
|
||||
style={{
|
||||
background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)',
|
||||
borderRadius: 12, padding: '12px 8px', border: '1px solid #374151',
|
||||
cursor: onClick ? 'pointer' : 'default',
|
||||
|
|
@ -95,6 +131,17 @@ export default function YichensBoard() {
|
|||
</div>
|
||||
)
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div style={{ padding: 20, textAlign: 'center', color: '#ef4444' }}>
|
||||
<div>加载失败: {error}</div>
|
||||
<button onClick={() => { setError(null); fetchTodayStats(); }} style={{ marginTop: 10, padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
|
|
@ -102,32 +149,25 @@ export default function YichensBoard() {
|
|||
📈 连体钞/纪念钞 <span style={{ color: '#9ca3af', fontWeight: 400 }}>{dateStr}</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
||||
<StatCard label='全部' value={todayStats.total} color='#3b82f6' onClick={() => { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||
<StatCard label='出售' value={todayStats.deals} color='#10b981' onClick={() => { setPostTypeFilter('deal'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||
<StatCard label='求购' value={todayStats.wants} color='#f59e0b' onClick={() => { setPostTypeFilter('want'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||
<StatCard label='其他' value={todayStats.others || 0} color='#8b5cf6' onClick={() => { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||
<StatCard label='全部' value={todayStats.total} color='#3b82f6' onClick={() => { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); }} />
|
||||
<StatCard label='出售' value={todayStats.deals} color='#10b981' onClick={() => { setPostTypeFilter('deal'); setCategoryFilter(''); setPage(1); }} />
|
||||
<StatCard label='求购' value={todayStats.wants} color='#f59e0b' onClick={() => { setPostTypeFilter('want'); setCategoryFilter(''); setPage(1); }} />
|
||||
<StatCard label='其他' value={todayStats.others || 0} color='#8b5cf6' onClick={() => { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 12, marginBottom: 20, border: '1px solid #374151' }}>
|
||||
<div style={{ color: '#f9fafb', fontSize: 14, fontWeight: 600, marginBottom: 8 }}>📊 今日分类统计</div>
|
||||
<div style={{ display: 'flex', gap: 6, overflowX: 'auto', paddingBottom: 4 }}>
|
||||
{todayCategory.map(cat => (
|
||||
<span key={cat.category} style={{ padding: '4px 10px', background: 'rgba(59,130,246,0.2)', borderRadius: 16, color: '#93c5fd', fontSize: 11, whiteSpace: 'nowrap' }}>
|
||||
{cat.category} ({cat.count})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ color: '#9ca3af', fontSize: 12 }}>龙: {todayStats.dragons || 0} | 马: {todayStats.horses || 0} | 蛇: {todayStats.snakes || 0} | 天马: {todayStats.tianma || 0}</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索标题、内容、分类..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => { setSearchKeyword(e.target.value); setPage(1); fetchPosts(1, '') }}
|
||||
onChange={(e) => { const kw = e.target.value; setSearchKeyword(kw); setPage(1); fetchPosts(1, '', kw) }}
|
||||
style={{ flex: 1, padding: '10px 14px', borderRadius: 8, border: '1px solid #374151', background: '#1f2937', color: '#fff', fontSize: 13, outline: 'none' }}
|
||||
/>
|
||||
<button
|
||||
|
|
@ -136,12 +176,12 @@ export default function YichensBoard() {
|
|||
清空
|
||||
</button>
|
||||
</div>
|
||||
{searchKeyword && <div style={{ color: '#9ca3af', fontSize: 12, marginTop: 6 }}>搜索: "{searchKeyword}",找到 {posts.length} 条结果</div>}
|
||||
{searchKeyword && <div style={{ color: '#9ca3af', fontSize: 12, marginTop: 6 }}>搜索: "{searchKeyword}",共找到 {totalPosts} 条结果</div>}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||
{[{key:'all',label:'全部'},{key:'deal',label:'出售'},{key:'want',label:'求购'},{key:'other',label:'其他'}].map(k => (
|
||||
<button key={k.key} onClick={() => { setPostTypeFilter(k.key==='other'?'other':k.key); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }}
|
||||
<button key={k.key} onClick={() => { setPostTypeFilter(k.key==='other'?'other':k.key); setCategoryFilter(''); setPage(1); }}
|
||||
style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
|
||||
background: postTypeFilter===k.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||
{k.label}
|
||||
|
|
@ -151,7 +191,7 @@ export default function YichensBoard() {
|
|||
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||
{[{key:'',label:'全部'},{key:'龙',label:'龙钞'},{key:'蛇',label:'蛇钞'},{key:'马',label:'马钞'},{key:'其他',label:'其他'}].map(k => (
|
||||
<button key={k.key} onClick={() => { setCategoryFilter(k.key); setPage(1); fetchPosts(1, k.key) }}
|
||||
<button key={k.key} onClick={() => { setCategoryFilter(k.key); setPage(1); }}
|
||||
style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
|
||||
background: categoryFilter===k.key ? 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||
{k.label}
|
||||
|
|
@ -162,50 +202,53 @@ export default function YichensBoard() {
|
|||
{loading ? <div style={{textAlign:'center',color:'#9ca3af',padding:40}}>加载中...</div> : (
|
||||
<div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{posts.map(post => (
|
||||
<div key={post.post_id} style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
|
||||
<div onClick={() => togglePost(post.post_id)} style={{ cursor: 'pointer' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ color: '#fff', fontSize: 15, fontWeight: 500, flex: 1 }}>{post.title||'无标题'}</span>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<span style={{ color: post.post_type==='deal'?'#10b981':post.post_type==='want'?'#f59e0b':post.post_type==='normal'?'#8b5cf6':'#9ca3af', fontSize: 12 }}>
|
||||
{post.post_type==='deal'?'出售':post.post_type==='want'?'求购':post.post_type==='normal'?'其他':'普通'}
|
||||
</span>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 11, background: post.category?.includes('龙') ? 'rgba(251,191,36,0.4)' : post.category?.includes('马') ? 'rgba(180,83,9,0.4)' : post.category?.includes('蛇') ? 'rgba(249,168,212,0.4)' : 'rgba(139,92,246,0.4)', color: post.category?.includes('龙') ? '#fde047' : post.category?.includes('马') ? '#d97706' : post.category?.includes('蛇') ? '#fbcfe8' : '#c4b5fd' }}>
|
||||
{post.category || '-'}
|
||||
</span>
|
||||
{posts.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', color: '#9ca3af', padding: 40 }}>暂无帖子数据</div>
|
||||
) : (
|
||||
posts.map(post => (
|
||||
<div key={post.post_id} style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
|
||||
<div onClick={() => togglePost(post.post_id)} style={{ cursor: 'pointer' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ color: '#fff', fontSize: 15, fontWeight: 500, flex: 1 }}>{post.title||'无标题'}</span>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<span style={{ color: post.post_type==='deal'?'#10b981':post.post_type==='want'?'#f59e0b':post.post_type==='normal'?'#8b5cf6':'#9ca3af', fontSize: 12 }}>
|
||||
{post.post_type==='deal'?'出售':post.post_type==='want'?'求购':post.post_type==='normal'?'其他':'普通'}
|
||||
</span>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 4, fontSize: 11, background: post.category?.includes('龙') ? 'rgba(251,191,36,0.4)' : post.category?.includes('马') ? 'rgba(180,83,9,0.4)' : post.category?.includes('蛇') ? 'rgba(249,168,212,0.4)' : 'rgba(139,92,246,0.4)', color: post.category?.includes('龙') ? '#fde047' : post.category?.includes('马') ? '#d97706' : post.category?.includes('蛇') ? '#fbcfe8' : '#c4b5fd' }}>
|
||||
{post.category || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#9ca3af', fontSize: 12 }}>
|
||||
<span>{post.author_username||'未知'}</span>
|
||||
<span>{post.post_time?.substring(0,16)||''}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#9ca3af', fontSize: 12 }}>
|
||||
<span>{post.author_username||'未知'}</span>
|
||||
<span>{post.post_time?.substring(0,16)||''}</span>
|
||||
</div>
|
||||
{expandedPosts[post.post_id] && post.content && (
|
||||
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #374151' }}>
|
||||
<div style={{ color: '#e2e8f0', fontSize: 13, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 300, overflowY: 'auto' }}>
|
||||
{post.content}
|
||||
</div>
|
||||
{post.url && <a href={post.url} target='_blank' rel='noopener noreferrer' style={{ display:'inline-block', marginTop:12, padding:'8px 16px', background:'rgba(59,130,246,0.2)', borderRadius:8, color:'#60a5fa', textDecoration:'none', fontSize:13 }}>查看原帖</a>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{expandedPosts[post.post_id] && post.content && (
|
||||
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #374151' }}>
|
||||
<div style={{ color: '#e2e8f0', fontSize: 13, lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 300, overflowY: 'auto' }}>
|
||||
{post.content}
|
||||
</div>
|
||||
{post.url && <a href={post.url} target='_blank' rel='noopener noreferrer' style={{ display:'inline-block', marginTop:12, padding:'8px 16px', background:'rgba(59,130,246,0.2)', borderRadius:8, color:'#60a5fa', textDecoration:'none', fontSize:13 }}>查看原帖</a>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页按钮移到页面底部 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', background: '#1e293b', borderRadius: 12, marginTop: 16 }}>
|
||||
<div style={{ color: '#9ca3af', fontSize: 12 }}>
|
||||
共 {totalPosts} 条帖子,{Math.ceil(totalPosts / 390)} 页,当前第 {page} 页
|
||||
共 {totalPosts} 条结果,{Math.ceil(totalPosts / 390)} 页,当前第 {page} 页
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{page > 1 ? (
|
||||
<button onClick={() => { 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 }}>上一页</button>
|
||||
<button onClick={() => { 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 }}>上一页</button>
|
||||
) : (
|
||||
<span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>上一页</span>
|
||||
)}
|
||||
{posts.length >= 390 ? (
|
||||
<button onClick={() => { 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 }}>下一页</button>
|
||||
{posts.length >= 390 && totalPosts > page * 390 ? (
|
||||
<button onClick={() => { 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 }}>下一页</button>
|
||||
) : (
|
||||
<span style={{ padding: '6px 12px', borderRadius: 6, background: '#374151', color: '#6b7280', fontSize: 12 }}>下一页</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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', // <20><><EFBFBD>
|
||||
'带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 }))
|
||||
|
|
@ -6,10 +6,13 @@ import { join } from 'path'
|
|||
// 从 config/VERSION 文件读取版本号
|
||||
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 versionFile = join(__dirname, 'config', 'VERSION')
|
||||
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,41 +20,42 @@ function getVersion() {
|
|||
}
|
||||
|
||||
const APP_VERSION = getVersion()
|
||||
console.log(`📦 构建版本:v${APP_VERSION}`)
|
||||
console.log('📦 构建版本:v' + APP_VERSION)
|
||||
|
||||
// 构建时自动更新 index.html 的 title
|
||||
// 构建后执行 - 更新 dist/index.html 的 title
|
||||
function updateHtmlTitle() {
|
||||
try {
|
||||
const htmlPath = join(__dirname, 'index.html')
|
||||
let htmlContent = readFileSync(htmlPath, 'utf-8')
|
||||
// 替换 <title>甲辰收藏 vXXX</title>
|
||||
htmlContent = htmlContent.replace(
|
||||
/<title>甲辰收藏 v[\d.]+<\/title>/,
|
||||
`<title>甲辰收藏 v${APP_VERSION}</title>`
|
||||
)
|
||||
writeFileSync(htmlPath, htmlContent, 'utf-8')
|
||||
console.log(`✅ 已更新 index.html title: 甲辰收藏 v${APP_VERSION}`)
|
||||
} catch (e) {
|
||||
console.error('更新 index.html 失败:', e.message)
|
||||
return {
|
||||
name: 'update-html-title',
|
||||
closeBundle() {
|
||||
try {
|
||||
const htmlPath = join(__dirname, 'dist', 'index.html')
|
||||
let htmlContent = readFileSync(htmlPath, 'utf-8')
|
||||
// 替换 <title>甲辰收藏 vXXX</title>
|
||||
htmlContent = htmlContent.replace(
|
||||
/<title>甲辰收藏 v[\d.]+<\/title>/,
|
||||
'<title>甲辰收藏 v' + APP_VERSION + '</title>'
|
||||
)
|
||||
writeFileSync(htmlPath, htmlContent, 'utf-8')
|
||||
console.log('✅ 已更新 dist/index.html title: 甲辰收藏 v' + APP_VERSION)
|
||||
} catch (e) {
|
||||
console.error('更新 dist/index.html 失败:', e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建前执行
|
||||
updateHtmlTitle()
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [react(), updateHtmlTitle()],
|
||||
define: {
|
||||
'import.meta.env.APP_VERSION': JSON.stringify(APP_VERSION)
|
||||
},
|
||||
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]'
|
||||
}
|
||||
},
|
||||
// 禁用缓存
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
# 版本一致性检查脚本
|
||||
# 用法: ./scripts/check_version.sh
|
||||
|
||||
echo "========================================="
|
||||
echo " 版本一致性检查"
|
||||
echo "========================================="
|
||||
|
||||
CONFIG_FILE="/root/.openclaw/workspace/jiachenlong/config/VERSION.json"
|
||||
VERSION=$(grep -o '"version": "[^"]*"' $CONFIG_FILE | cut -d'"' -f4)
|
||||
UPDATED=$(grep -o '"updated": "[^"]*"' $CONFIG_FILE | cut -d'"' -f4)
|
||||
|
||||
echo ""
|
||||
echo "当前版本: $VERSION"
|
||||
echo "更新时间: $UPDATED"
|
||||
echo ""
|
||||
|
||||
# 检查前端页面版本
|
||||
echo "--- 前端页面 (最后修改时间) ---"
|
||||
for file in /root/.openclaw/workspace/jiachenlong/frontend/src/pages/*.jsx; do
|
||||
name=$(basename $file .jsx)
|
||||
mtime=$(date -r "$file" "+%Y-%m-%d %H:%M")
|
||||
echo "$name: $mtime"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "--- 后端路由 (最后修改时间) ---"
|
||||
for file in /root/.openclaw/workspace/jiachenlong/backend/app/routers/*.py; do
|
||||
name=$(basename $file .py)
|
||||
if [ "$name" != "__init__" ]; then
|
||||
mtime=$(date -r "$file" "+%Y-%m-%d %H:%M")
|
||||
echo "$name: $mtime"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo " 检查完成"
|
||||
echo "========================================="
|
||||
|
|
@ -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后端服务已启动'
|
||||
Loading…
Reference in New Issue