Compare commits
8 Commits
b2b30cc54e
...
e8c3069bfd
| Author | SHA1 | Date |
|---|---|---|
|
|
e8c3069bfd | |
|
|
693e13bbab | |
|
|
195d779e79 | |
|
|
97cc727547 | |
|
|
3c3072159a | |
|
|
f11c4a0be0 | |
|
|
9d1b59a421 | |
|
|
7ff0ae5c9e |
|
|
@ -0,0 +1,37 @@
|
|||
import os
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import QueuePool
|
||||
|
||||
COOLBOT_DB_URL = os.getenv(
|
||||
"COOLBOT_DB_URL",
|
||||
"postgresql://coolbot:Coolbot123@pgm-bp1t1008h019ez6cno.pg.rds.aliyuncs.com:5432/coolbot_data"
|
||||
)
|
||||
|
||||
coolbot_engine = create_engine(
|
||||
COOLBOT_DB_URL,
|
||||
poolclass=QueuePool,
|
||||
pool_size=10,
|
||||
max_overflow=20,
|
||||
pool_timeout=30,
|
||||
pool_recycle=1800,
|
||||
pool_pre_ping=True,
|
||||
echo=False,
|
||||
connect_args={
|
||||
"connect_timeout": 10,
|
||||
"application_name": "zodiac-coolbot"
|
||||
}
|
||||
)
|
||||
|
||||
CoolbotSession = sessionmaker(autocommit=False, autoflush=False, bind=coolbot_engine)
|
||||
|
||||
def get_coolbot_db():
|
||||
"""获取coolbot数据库会话"""
|
||||
db = CoolbotSession()
|
||||
try:
|
||||
yield db
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
|
@ -16,6 +16,7 @@ from app.routers import auth, collections, operations
|
|||
from app.routers import ocr as ocr_router
|
||||
from app.routers import users as users_router
|
||||
from app.routers import information as information_router
|
||||
from app.routers import yichens as yichens_router
|
||||
|
||||
# 版本信息 - 从 config/VERSION 文件读取
|
||||
def get_version():
|
||||
|
|
@ -65,10 +66,10 @@ uploads_dir = "uploads"
|
|||
os.makedirs(uploads_dir, exist_ok=True)
|
||||
app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads")
|
||||
|
||||
# 挂载项目静态资源目录(可选,生产环境建议用 Nginx)
|
||||
# static_dir = Path(__file__).parent.parent.parent / "static"
|
||||
# if static_dir.exists():
|
||||
# app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
||||
# 挂载项目静态资源目录
|
||||
static_dir = Path(__file__).parent.parent / "static"
|
||||
if static_dir.exists():
|
||||
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
||||
|
||||
# 添加日志中间件
|
||||
app.middleware("http")(logging_middleware)
|
||||
|
|
@ -80,7 +81,8 @@ app.include_router(operations.router)
|
|||
app.include_router(ocr_router.router) # OCR 识别
|
||||
app.include_router(users_router.router) # 当前用户接口
|
||||
app.include_router(users_router.admin_router) # 管理员用户管理
|
||||
app.include_router(information_router.router) # 资讯
|
||||
app.include_router(information_router.router)
|
||||
app.include_router(yichens_router.router) # 一尘看板
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ class Information(Base):
|
|||
|
||||
# 内容描述
|
||||
content = Column(Text, nullable=True)
|
||||
|
||||
|
||||
# 关联藏品ID
|
||||
collection_id = Column(String(36), ForeignKey("collections.f99_90_id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
# 资讯API路由
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import text
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime, date
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.auth import get_current_user
|
||||
from app.core.coolbot_db import coolbot_engine
|
||||
from app.models.models import User, Information, Collection
|
||||
|
||||
router = APIRouter(prefix="/api/information", tags=["资讯"])
|
||||
|
|
@ -16,7 +18,7 @@ router = APIRouter(prefix="/api/information", tags=["资讯"])
|
|||
class InformationCreate(BaseModel):
|
||||
info_type: str # seek-寻配号, deal-成交数据, publish-发布
|
||||
title: str
|
||||
content: Optional[str] = None
|
||||
content: Optional[str]
|
||||
collection_id: Optional[str] = None
|
||||
expect_category: Optional[str] = None
|
||||
expect_version: Optional[str] = None
|
||||
|
|
@ -30,7 +32,7 @@ class InformationCreate(BaseModel):
|
|||
|
||||
class InformationUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
content: Optional[str]
|
||||
status: Optional[str] = None
|
||||
expect_category: Optional[str] = None
|
||||
expect_version: Optional[str] = None
|
||||
|
|
@ -74,6 +76,8 @@ class InformationResponse(BaseModel):
|
|||
collection_number: Optional[str] = None
|
||||
# 匹配数量(我的藏品中满足条件的数量)
|
||||
matched_count: Optional[int] = 0
|
||||
# 网络数据匹配数量(coolbot_data数据库中满足条件的数量)
|
||||
network_matched_count: Optional[int] = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
|
@ -87,7 +91,8 @@ def get_information_list(
|
|||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
current_user: Optional[User] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
db: Session = Depends(get_db),
|
||||
response: Response = None
|
||||
):
|
||||
"""获取资讯列表(公开,无需登录)"""
|
||||
query = db.query(Information).options(
|
||||
|
|
@ -142,8 +147,21 @@ def get_information_list(
|
|||
collection_version=item.collection.f02_11_version if item.collection else None,
|
||||
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
|
||||
matched_count=matched_count,
|
||||
network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0,
|
||||
))
|
||||
|
||||
# 获取总数并设置响应头
|
||||
from fastapi import Response
|
||||
total_query = db.query(Information).filter(Information.status == status)
|
||||
if info_type:
|
||||
total_query = total_query.filter(Information.info_type == info_type)
|
||||
total_count = total_query.count()
|
||||
total_pages = (total_count + page_size - 1) // page_size
|
||||
|
||||
# 设置响应头
|
||||
response.headers['X-Total-Pages'] = str(total_pages)
|
||||
response.headers['X-Total-Count'] = str(total_count)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -182,6 +200,110 @@ def match_collections_count(db: Session, user_id: str, expect_number: str) -> in
|
|||
return count
|
||||
|
||||
|
||||
def match_collections_count_from_coolbot(expect_number: str) -> int:
|
||||
"""根据号码特征计算匹配藏品数量(从coolbot_data数据库)"""
|
||||
if not expect_number or len(expect_number) != 10:
|
||||
return 0
|
||||
|
||||
# 固定前缀
|
||||
if not expect_number.startswith('J0'):
|
||||
return 0
|
||||
|
||||
pattern = expect_number[2:] # 后8位
|
||||
if not pattern:
|
||||
return 0
|
||||
|
||||
# 直接查询coolbot_data数据库
|
||||
query = text("""
|
||||
SELECT COUNT(*) FROM collections
|
||||
WHERE crown_code IS NOT NULL
|
||||
AND crown_code != ''
|
||||
AND LENGTH(crown_code) >= 10
|
||||
AND crown_code LIKE 'J0%'
|
||||
""")
|
||||
|
||||
try:
|
||||
with coolbot_engine.connect() as conn:
|
||||
result = conn.execute(query)
|
||||
total_count = result.scalar() or 0
|
||||
|
||||
# 遍历匹配
|
||||
query_all = text("""
|
||||
SELECT id, crown_code FROM collections
|
||||
WHERE crown_code IS NOT NULL
|
||||
AND crown_code != ''
|
||||
AND LENGTH(crown_code) >= 10
|
||||
AND crown_code LIKE 'J0%'
|
||||
""")
|
||||
result = conn.execute(query_all)
|
||||
|
||||
match_count = 0
|
||||
for row in result:
|
||||
crown_code = row[1]
|
||||
if crown_code and len(crown_code) >= 10:
|
||||
col_pattern = crown_code[2:10]
|
||||
if match_pattern(col_pattern, pattern):
|
||||
match_count += 1
|
||||
|
||||
return match_count
|
||||
except Exception as e:
|
||||
print(f"Error querying coolbot_data: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def match_collections_list_from_coolbot(expect_number: str, limit: int = 20) -> List[dict]:
|
||||
"""获取匹配的藏品列表(从coolbot_data数据库)"""
|
||||
if not expect_number or len(expect_number) != 10:
|
||||
return []
|
||||
|
||||
# 固定前缀
|
||||
if not expect_number.startswith('J0'):
|
||||
return []
|
||||
|
||||
pattern = expect_number[2:] # 后8位
|
||||
if not pattern:
|
||||
return []
|
||||
|
||||
# 直接查询coolbot_data数据库
|
||||
query = text("""
|
||||
SELECT id, name, category, crown_code, price, post_title, post_url, author, post_crawled_at
|
||||
FROM collections
|
||||
WHERE crown_code IS NOT NULL
|
||||
AND crown_code != ''
|
||||
AND LENGTH(crown_code) >= 10
|
||||
AND crown_code LIKE 'J0%'
|
||||
""")
|
||||
|
||||
try:
|
||||
with coolbot_engine.connect() as conn:
|
||||
result = conn.execute(query)
|
||||
|
||||
matched = []
|
||||
for row in result:
|
||||
crown_code = row[3]
|
||||
if crown_code and len(crown_code) >= 10:
|
||||
col_pattern = crown_code[2:10]
|
||||
if match_pattern(col_pattern, pattern):
|
||||
matched.append({
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"category": row[2],
|
||||
"crown_code": crown_code,
|
||||
"price": float(row[4]) if row[4] else None,
|
||||
"post_title": row[5],
|
||||
"post_url": row[6],
|
||||
"author": row[7],
|
||||
"post_crawled_at": row[8].isoformat() if row[8] else None
|
||||
})
|
||||
if len(matched) >= limit:
|
||||
break
|
||||
|
||||
return matched
|
||||
except Exception as e:
|
||||
print(f"Error querying coolbot_data: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def match_pattern(col_number: str, pattern: str) -> bool:
|
||||
"""匹配号码特征模式"""
|
||||
# X = 任意数字
|
||||
|
|
@ -502,7 +624,36 @@ def get_seek_match(
|
|||
"cost_price": c.f05_40_cost_price,
|
||||
}
|
||||
for c in matched
|
||||
]
|
||||
],
|
||||
"network_matched_count": match_collections_count_from_coolbot(info.expect_number) if info.expect_number else 0,
|
||||
"network_collections": match_collections_list_from_coolbot(info.expect_number, limit=20) if info.expect_number else []
|
||||
}
|
||||
|
||||
|
||||
# 获取网络数据匹配列表
|
||||
@router.get("/seek/network-match/{info_id}")
|
||||
def get_network_match(
|
||||
info_id: str,
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""获取一尘数据库中匹配的藏品列表"""
|
||||
info = db.query(Information).filter(
|
||||
Information.id == info_id,
|
||||
Information.info_type == "seek"
|
||||
).first()
|
||||
|
||||
if not info:
|
||||
raise HTTPException(status_code=404, detail="寻配号信息不存在")
|
||||
|
||||
if not info.expect_number:
|
||||
return {"matched_count": 0, "collections": []}
|
||||
|
||||
matched = match_collections_list_from_coolbot(info.expect_number, limit=limit)
|
||||
|
||||
return {
|
||||
"matched_count": len(matched),
|
||||
"collections": matched
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -558,8 +709,21 @@ def get_my_seeks(
|
|||
collection_version=item.collection.f02_11_version if item.collection else None,
|
||||
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
|
||||
matched_count=matched_count,
|
||||
network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0,
|
||||
))
|
||||
|
||||
# 获取总数并设置响应头
|
||||
from fastapi import Response
|
||||
total_query = db.query(Information).filter(Information.status == status)
|
||||
if info_type:
|
||||
total_query = total_query.filter(Information.info_type == info_type)
|
||||
total_count = total_query.count()
|
||||
total_pages = (total_count + page_size - 1) // page_size
|
||||
|
||||
# 设置响应头
|
||||
response.headers['X-Total-Pages'] = str(total_pages)
|
||||
response.headers['X-Total-Count'] = str(total_count)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,252 @@
|
|||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import func, text
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, date
|
||||
from app.core.coolbot_db import get_coolbot_db
|
||||
|
||||
router = APIRouter(prefix="/api/yichens", tags=["一尘看板"])
|
||||
|
||||
# ============ 数据模型 ============
|
||||
class YichensPostStats(BaseModel):
|
||||
total_posts: int
|
||||
total_deals: int # 出售
|
||||
total_wants: int # 求购
|
||||
total_replies: int
|
||||
total_views: int
|
||||
avg_price: Optional[float]
|
||||
|
||||
class CategoryStat(BaseModel):
|
||||
category: str
|
||||
count: int
|
||||
|
||||
class PostItem(BaseModel):
|
||||
post_id: str
|
||||
title: str
|
||||
category: Optional[str]
|
||||
post_type: str
|
||||
price: Optional[float]
|
||||
author_username: str
|
||||
post_time: str
|
||||
reply_count: int
|
||||
view_count: int
|
||||
url: Optional[str]
|
||||
content: Optional[str]
|
||||
|
||||
class UserStat(BaseModel):
|
||||
total_users: int
|
||||
new_users_today: int
|
||||
sellers: int
|
||||
|
||||
class UserItem(BaseModel):
|
||||
user_id: str
|
||||
username: str
|
||||
avatar_url: Optional[str]
|
||||
content: Optional[str]
|
||||
credit_level: Optional[str]
|
||||
credit_score: Optional[int]
|
||||
post_count: int
|
||||
is_seller: bool
|
||||
registration_date: Optional[str]
|
||||
|
||||
# ============ 统计接口 ============
|
||||
@router.get("/stats/posts", response_model=YichensPostStats)
|
||||
def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
|
||||
"""获取帖子统计"""
|
||||
result = db.execute(text("""
|
||||
SELECT
|
||||
COUNT(*) as total_posts,
|
||||
COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals,
|
||||
COUNT(*) FILTER (WHERE post_type = 'want') as total_wants,
|
||||
COALESCE(SUM(reply_count), 0) as total_replies,
|
||||
COALESCE(SUM(view_count), 0) as total_views,
|
||||
AVG(price) as avg_price
|
||||
FROM yichens_posts
|
||||
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
|
||||
"""), {"days": days}).fetchone()
|
||||
|
||||
return YichensPostStats(
|
||||
total_posts=result[0] or 0,
|
||||
total_deals=result[1] or 0,
|
||||
total_wants=result[2] or 0,
|
||||
total_replies=result[3] or 0,
|
||||
total_views=result[4] or 0,
|
||||
avg_price=float(result[5]) if result[5] else None
|
||||
)
|
||||
|
||||
@router.get("/stats/categories", response_model=List[CategoryStat])
|
||||
def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
|
||||
"""按分类统计帖子数量"""
|
||||
results = db.execute(text("""
|
||||
SELECT category, COUNT(*) as count
|
||||
FROM yichens_posts
|
||||
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
|
||||
GROUP BY category
|
||||
ORDER BY count DESC
|
||||
"""), {"days": days}).fetchall()
|
||||
|
||||
return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results]
|
||||
|
||||
@router.get("/stats/users", response_model=UserStat)
|
||||
def get_user_stats(db: Session = Depends(get_coolbot_db)):
|
||||
"""获取用户统计"""
|
||||
result = db.execute(text("""
|
||||
SELECT
|
||||
COUNT(*) as total_users,
|
||||
COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today,
|
||||
COUNT(*) FILTER (WHERE is_seller = true) as sellers
|
||||
FROM yichens_users
|
||||
""")).fetchone()
|
||||
|
||||
return UserStat(
|
||||
total_users=result[0] or 0,
|
||||
new_users_today=result[1] or 0,
|
||||
sellers=result[2] or 0
|
||||
)
|
||||
|
||||
@router.get("/posts", response_model=List[PostItem])
|
||||
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,
|
||||
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
|
||||
"""
|
||||
params = {"limit": limit, "offset": offset}
|
||||
|
||||
if category:
|
||||
query += " AND category = :category"
|
||||
params["category"] = category
|
||||
|
||||
if post_type:
|
||||
query += " AND post_type = :post_type"
|
||||
params["post_type"] = post_type
|
||||
|
||||
query += " ORDER BY post_time DESC LIMIT :limit OFFSET :offset"
|
||||
|
||||
results = db.execute(text(query), params).fetchall()
|
||||
|
||||
return [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]
|
||||
|
||||
@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]
|
||||
|
|
@ -1 +1 @@
|
|||
VERSION=1.2.41
|
||||
VERSION=1.2.50
|
||||
|
|
|
|||
|
|
@ -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.41</title>
|
||||
<title>甲辰收藏 v1.2.50</title>
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="/static/icons/favicon.png" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ export default function Home() {
|
|||
const [user, setUser] = useState(null)
|
||||
const [stats, setStats] = useState({ totalCount: 0, totalCost: 0, totalRevenue: 0, expectedProfit: 0, totalProfit: 0, gradedCount: 0 })
|
||||
const [recentCollections, setRecentCollections] = useState([])
|
||||
const [yichensStats, setYichensStats] = useState({ total: 0, deals: 0, wants: 0, dragons: 0, horses: 0, tianma: 0 })
|
||||
const [recentPosts, setRecentPosts] = useState([])
|
||||
const currentPath = window.location.hash.slice(1) || '/'
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -54,6 +56,18 @@ export default function Home() {
|
|||
}
|
||||
}, [])
|
||||
|
||||
// 获取一尘看板数据
|
||||
useEffect(() => {
|
||||
fetch('/api/yichens/stats/today').then(res => res.json()).then(data => {
|
||||
setYichensStats(data || {})
|
||||
}).catch(() => {})
|
||||
|
||||
// 获取最新一尘帖子
|
||||
fetch('/api/yichens/posts?limit=10&offset=0').then(res => res.json()).then(data => {
|
||||
setRecentPosts(Array.isArray(data) ? data : [])
|
||||
}).catch(() => {})
|
||||
}, [])
|
||||
|
||||
// 检查是否为管理员
|
||||
const isAdmin = user && user.role === 'admin'
|
||||
|
||||
|
|
@ -96,7 +110,7 @@ export default function Home() {
|
|||
backdropFilter: 'blur(10px)'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<img src="/static/images/jiachenlong-logo.png?v=1.2.4" alt="甲辰收藏" style={{ width: '48px', height: '48px', borderRadius: '50%', objectFit: 'cover' }} />
|
||||
<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: 'rgba(255,255,255,0.6)', fontSize: '12px' }}>欢迎回来 👋</div>
|
||||
|
|
@ -175,11 +189,42 @@ export default function Home() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最近藏品 */}
|
||||
{/* 一尘今日数据 */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>📊 一尘今日数据</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
|
||||
<div style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.15) 0%, rgba(59,130,246,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(59,130,246,0.2)', textAlign: 'center' }}>
|
||||
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: '700' }}>{yichensStats.total || 0}</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>总帖子</div>
|
||||
</div>
|
||||
<div style={{ background: 'linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(16,185,129,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(16,185,129,0.2)', textAlign: 'center' }}>
|
||||
<div style={{ color: '#34d399', fontSize: '20px', fontWeight: '700' }}>{yichensStats.deals || 0}</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>出售</div>
|
||||
</div>
|
||||
<div style={{ background: 'linear-gradient(135deg, rgba(245,158,11,0.15) 0%, rgba(245,158,11,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(245,158,11,0.2)', textAlign: 'center' }}>
|
||||
<div style={{ color: '#fbbf24', fontSize: '20px', fontWeight: '700' }}>{yichensStats.wants || 0}</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>求购</div>
|
||||
</div>
|
||||
<div style={{ background: 'linear-gradient(135deg, rgba(139,92,246,0.15) 0%, rgba(139,92,246,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(139,92,246,0.2)', textAlign: 'center' }}>
|
||||
<div style={{ color: '#a78bfa', fontSize: '20px', fontWeight: '700' }}>{yichensStats.dragons || 0}</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>龙钞</div>
|
||||
</div>
|
||||
<div style={{ background: 'linear-gradient(135deg, rgba(236,72,153,0.15) 0%, rgba(236,72,153,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(236,72,153,0.2)', textAlign: 'center' }}>
|
||||
<div style={{ color: '#f472b6', fontSize: '20px', fontWeight: '700' }}>{yichensStats.horses || 0}</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>马钞</div>
|
||||
</div>
|
||||
<div style={{ background: 'linear-gradient(135deg, rgba(20,184,166,0.15) 0%, rgba(20,184,166,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(20,184,166,0.2)', textAlign: 'center' }}>
|
||||
<div style={{ color: '#2dd4bf', fontSize: '20px', fontWeight: '700' }}>{yichensStats.tianma || 0}</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>天马</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 最新一尘发帖 */}
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px', paddingLeft: '4px' }}>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px' }}>最近藏品</div>
|
||||
<div onClick={() => window.location.hash = '#/list'} style={{ color: '#3b82f6', fontSize: '12px', cursor: 'pointer' }}>查看全部 →</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px' }}>📝 最新一尘发帖</div>
|
||||
<div onClick={() => window.location.hash = '#/news'} style={{ color: '#3b82f6', fontSize: '12px', cursor: 'pointer' }}>查看全部 →</div>
|
||||
</div>
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
|
|
@ -187,24 +232,30 @@ export default function Home() {
|
|||
border: '1px solid rgba(255,255,255,0.05)',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{recentCollections.length === 0 ? (
|
||||
<div style={{ padding: '40px', textAlign: 'center', color: 'rgba(255,255,255,0.3)' }}>暂无藏品</div>
|
||||
{recentPosts.length === 0 ? (
|
||||
<div style={{ padding: '40px', textAlign: 'center', color: 'rgba(255,255,255,0.3)' }}>暂无帖子</div>
|
||||
) : (
|
||||
recentCollections.map((item, idx) => (
|
||||
<div key={item.id || idx} onClick={() => window.location.hash = '#/detail?id=' + item.id} style={{
|
||||
recentPosts.map((item, idx) => (
|
||||
<div key={item.post_id || idx} onClick={() => window.location.hash = '#/news'} style={{
|
||||
padding: '12px 16px',
|
||||
borderBottom: idx < recentCollections.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none',
|
||||
borderBottom: idx < recentPosts.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ color: '#fff', fontSize: '14px' }}>{item.code || '-'} <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '14px', fontWeight: 'bold' }}>{item.prefixSerial || ''}</span></div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.4)', fontSize: '12px', marginTop: '2px' }}>{item.version || '-'} · {item.status === 'sold' ? '已售' : item.status === 'in_collection' ? '收藏中' : item.status}</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ color: '#fff', fontSize: '14px' }}>{item.title || '无标题'}</div>
|
||||
<div style={{ color: 'rgba(255,255,255,0.4)', fontSize: '12px', marginTop: '2px' }}>{item.category || '-'} · {item.author_username || '未知'} · {item.post_time?.substring(0, 16) || ''}</div>
|
||||
</div>
|
||||
<div style={{ color: item.costPrice ? '#22c55e' : 'rgba(255,255,255,0.3)', fontSize: '13px' }}>
|
||||
{item.costPrice ? '¥' + item.costPrice : '-'}
|
||||
<div style={{
|
||||
color: item.post_type === 'deal' ? '#10b981' : item.post_type === 'want' ? '#f59e0b' : '#8b5cf6',
|
||||
fontSize: '12px',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
background: item.post_type === 'deal' ? 'rgba(16,185,129,0.2)' : item.post_type === 'want' ? 'rgba(245,158,11,0.2)' : 'rgba(139,92,246,0.2)'
|
||||
}}>
|
||||
{item.post_type === 'deal' ? '出售' : item.post_type === 'want' ? '求购' : '其他'}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -672,6 +672,9 @@ export default function Info() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import YichensBoard from './YichensBoard'
|
||||
|
||||
// 本地获取用户手机号 - 添加异常处理
|
||||
const getUserPhone = () => {
|
||||
|
|
@ -22,7 +23,7 @@ const getStoredUserId = () => {
|
|||
|
||||
// 资讯页面 - 展示寻配号和行情信息(所有人可见)
|
||||
export default function News() {
|
||||
const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'seek')
|
||||
const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'yichen')
|
||||
const [showSeekPublish, setShowSeekPublish] = useState(false)
|
||||
const [showMySeeks, setShowMySeeks] = useState(false) // 我的寻号
|
||||
const [showContactId, setShowContactId] = useState(null) // 显示联系方式的寻号ID
|
||||
|
|
@ -32,6 +33,8 @@ export default function News() {
|
|||
const [matchedStatus, setMatchedStatus] = useState({}) // 存储各寻号的匹配状态
|
||||
const [showMatchList, setShowMatchList] = useState(false)
|
||||
const [matchCollections, setMatchCollections] = useState([])
|
||||
const [networkMatchCollections, setNetworkMatchCollections] = useState([])
|
||||
const [showNetworkMatchList, setShowNetworkMatchList] = useState(false)
|
||||
const [customModal, setCustomModal] = useState({show: false, title: '', content: ''})
|
||||
const [seekForm, setSeekForm] = useState({
|
||||
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || ''
|
||||
|
|
@ -40,6 +43,8 @@ export default function News() {
|
|||
const [viewMode, setViewMode] = useState('all')
|
||||
const [expandedItems, setExpandedItems] = useState({}) // 展开状态
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||
const currentUser = localStorage.getItem('token') ? { id: 'logged_in' } : null
|
||||
|
|
@ -72,7 +77,10 @@ export default function News() {
|
|||
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}`, { headers })
|
||||
const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}&page=${currentPage}&page_size=50`, { headers })
|
||||
// 从响应头获取总页数
|
||||
const total = res.headers.get('X-Total-Pages') || res.headers.get('x-total-pages')
|
||||
if (total) setTotalPages(parseInt(total))
|
||||
// 检查响应状态
|
||||
if (!res.ok) {
|
||||
console.error('获取资讯列表失败:', res.status)
|
||||
|
|
@ -146,10 +154,11 @@ export default function News() {
|
|||
const token = localStorage.getItem('token')
|
||||
if (!token) { alert('请先登录'); return }
|
||||
try {
|
||||
const phone = getUserPhone()
|
||||
const res = await fetch(`${API_BASE}/api/information/seek/match-confirm`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ info_id: infoId })
|
||||
body: JSON.stringify({ info_id: infoId, contact: phone || '用户已同意' })
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.message === '匹配成功,已通知发布者') {
|
||||
|
|
@ -187,7 +196,7 @@ export default function News() {
|
|||
console.log('Matched user response:', res.status)
|
||||
const data = await res.json()
|
||||
console.log('Matched user data:', data)
|
||||
if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.matched_contact || '未提供'}`})
|
||||
if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示:所有用户信息仅作参考,交易请走正规平台,如产生经济损失与本站无关,后果自负。`})
|
||||
else if (data.detail) alert(data.detail)
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
|
@ -200,7 +209,7 @@ export default function News() {
|
|||
console.log('Publisher response:', res.status)
|
||||
const data = await res.json()
|
||||
console.log('Publisher data:', data)
|
||||
if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.contact || '未提供'}`})
|
||||
if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示:所有用户信息仅作参考,交易请走正规平台,如产生经济损失与本站无关,后果自负。`})
|
||||
else if (data.detail) alert(data.detail)
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
|
@ -223,6 +232,20 @@ export default function News() {
|
|||
}
|
||||
}
|
||||
|
||||
// 获取网络数据匹配列表
|
||||
const fetchNetworkMatchCollections = async (infoId) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/information/seek/network-match/${infoId}`)
|
||||
const data = await res.json()
|
||||
console.log('网络数据匹配结果:', data)
|
||||
setNetworkMatchCollections(data.collections || [])
|
||||
setShowNetworkMatchList(true)
|
||||
} catch (e) {
|
||||
console.error('获取网络匹配藏品失败:', e)
|
||||
alert('获取网络匹配藏品失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
const currentUserId = getStoredUserId()
|
||||
|
||||
|
|
@ -388,7 +411,7 @@ export default function News() {
|
|||
readOnly={isFixed}
|
||||
onChange={(e) => {
|
||||
if (i < 2) return
|
||||
const val = e.target.value.toUpperCase().replace(/[^0-9XABCFG]/g, '')
|
||||
const val = e.target.value.toUpperCase().replace(/[^0-9XABCDEFG]/g, '')
|
||||
const newFeatures = (seekForm.features || '').split('')
|
||||
while (newFeatures.length < 8) newFeatures.push('')
|
||||
newFeatures[i - 2] = val
|
||||
|
|
@ -455,7 +478,7 @@ export default function News() {
|
|||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{infoList.map(item => {
|
||||
{activeTab === 'yichen' ? <YichensBoard /> : infoList.map(item => {
|
||||
// 解析正文中的号码特征和联系方式
|
||||
const content = item.content || ''
|
||||
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
|
||||
|
|
@ -522,12 +545,20 @@ export default function News() {
|
|||
{/* 配号结果 - 仅寻配号显示 */}
|
||||
{activeTab === 'seek' && (
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<span
|
||||
<span
|
||||
style={{ color: '#10b981', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer' }}
|
||||
onClick={() => fetchMatchCollections(item.id)}
|
||||
>
|
||||
配号结果: {item.matched_count || 0}条藏品匹配成功
|
||||
自有{item.matched_count || 0}条藏品匹配成功
|
||||
</span>
|
||||
{item.network_matched_count !== undefined && (
|
||||
<span
|
||||
style={{ color: '#3b82f6', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer', marginLeft: '12px' }}
|
||||
onClick={() => fetchNetworkMatchCollections(item.id)}
|
||||
>
|
||||
网络数据{item.network_matched_count}条匹配成功
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 正文 - 默认收起,点击展开 */}
|
||||
|
|
@ -614,23 +645,24 @@ export default function News() {
|
|||
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (item.is_matched === 'matched') return
|
||||
if (item.matched_count > 0) {
|
||||
matchAndContact(item.id)
|
||||
} else {
|
||||
setCustomModal({show: true, title: '提示', content: '暂无匹配藏品,无法匹配'})
|
||||
}
|
||||
}}
|
||||
disabled={item.matched_count === 0}
|
||||
disabled={item.matched_count === 0 || item.is_matched === 'matched'}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
fontSize: '12px',
|
||||
background: item.matched_count > 0 ? '#3b82f6' : '#4b5563',
|
||||
background: (item.matched_count > 0 && item.is_matched !== 'matched') ? '#3b82f6' : '#4b5563',
|
||||
color: '#fff',
|
||||
cursor: item.matched_count > 0 ? 'pointer' : 'not-allowed',
|
||||
opacity: item.matched_count > 0 ? 1 : 0.5
|
||||
cursor: (item.matched_count > 0 && item.is_matched !== 'matched') ? 'pointer' : 'not-allowed',
|
||||
opacity: (item.matched_count > 0 && item.is_matched !== 'matched') ? 1 : 0.5
|
||||
}}
|
||||
>
|
||||
匹配并联系藏友
|
||||
|
|
@ -752,6 +784,74 @@ export default function News() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* 网络数据匹配藏品列表弹窗 */}
|
||||
{showNetworkMatchList && (
|
||||
<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' }}>
|
||||
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxHeight: '80vh', overflow: 'auto' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h3 style={{ color: '#fff', margin: 0 }}>匹配藏品清单(网络数据)</h3>
|
||||
<button onClick={() => setShowNetworkMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
{networkMatchCollections.length === 0 ? (
|
||||
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '20px' }}>暂无匹配藏品</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{networkMatchCollections.map(c => (
|
||||
<div
|
||||
key={c.id}
|
||||
style={{ background: '#0f172a', borderRadius: '8px', padding: '12px', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
if (c.post_url) {
|
||||
window.open(c.post_url, '_blank')
|
||||
}
|
||||
setShowNetworkMatchList(false)
|
||||
}}
|
||||
>
|
||||
<div style={{ color: '#e2e8f0', fontSize: '13px' }}>
|
||||
<span style={{ color: '#94a3b8' }}>名称: </span>{c.name || '-'}
|
||||
</div>
|
||||
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
|
||||
<span style={{ color: '#94a3b8' }}>冠字号: </span>{c.crown_code}
|
||||
</div>
|
||||
{c.price && (
|
||||
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
|
||||
<span style={{ color: '#94a3b8' }}>价格: </span>¥{c.price}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ color: '#3b82f6', fontSize: '12px', marginTop: '6px' }}>
|
||||
点击查看原帖 →
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分页组件 */}
|
||||
{totalPages > 1 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px', padding: '16px', marginTop: '8px' }}>
|
||||
<button
|
||||
onClick={() => { if (currentPage > 1) { setCurrentPage(currentPage - 1); fetchInfoList() } }}
|
||||
disabled={currentPage === 1}
|
||||
style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage === 1 ? '#374151' : '#3b82f6', color: currentPage === 1 ? '#6b7280' : '#fff', cursor: currentPage === 1 ? 'not-allowed' : 'pointer', fontSize: '13px' }}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span style={{ color: '#94a3b8', fontSize: '13px' }}>
|
||||
第 {currentPage} / {totalPages} 页
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { if (currentPage < totalPages) { setCurrentPage(currentPage + 1); fetchInfoList() } }}
|
||||
disabled={currentPage >= totalPages}
|
||||
style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage >= totalPages ? '#374151' : '#3b82f6', color: currentPage >= totalPages ? '#6b7280' : '#fff', cursor: currentPage >= totalPages ? 'not-allowed' : 'pointer', fontSize: '13px' }}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 我的寻号弹窗 */}
|
||||
{showMySeeks && (
|
||||
<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' }}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
export default function YichensBoard() {
|
||||
const [todayStats, setTodayStats] = useState({ total: 0, deals: 0, wants: 0, others: 0 })
|
||||
const [todayCategory, setTodayCategory] = useState([])
|
||||
const [posts, setPosts] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [expandedPosts, setExpandedPosts] = useState({})
|
||||
const [postTypeFilter, setPostTypeFilter] = useState('all')
|
||||
const [categoryFilter, setCategoryFilter] = useState('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [totalPosts, setTotalPosts] = useState(0)
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||
|
||||
const today = new Date()
|
||||
const dateStr = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate()
|
||||
|
||||
useEffect(() => { fetchTodayStats(); fetchPosts(1, '') }, [])
|
||||
|
||||
const fetchTodayStats = async () => {
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/api/yichens/stats/today')
|
||||
setTodayStats(await res.json())
|
||||
} catch(e) { console.error(e) }
|
||||
}
|
||||
|
||||
const fetchTodayCategory = async () => {
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/api/yichens/stats/today-category')
|
||||
setTodayCategory(await res.json())
|
||||
} catch(e) { console.error(e) }
|
||||
}
|
||||
|
||||
const fetchPosts = async (p, cat) => {
|
||||
setLoading(true)
|
||||
const currentPage = p !== undefined ? p : page
|
||||
const currentCat = cat !== undefined ? cat : categoryFilter
|
||||
|
||||
let url = API_BASE + '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390)
|
||||
if (postTypeFilter === 'deal') url += '&post_type=deal'
|
||||
else if (postTypeFilter === 'want') url += '&post_type=want'
|
||||
else if (postTypeFilter === 'other') url += '&post_type=normal'
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
let data = await res.json() || []
|
||||
if (currentCat) {
|
||||
if (currentCat === '龙') {
|
||||
data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
|
||||
} else if (currentCat === '蛇') {
|
||||
data = data.filter(p => p.category && p.category.includes('蛇'))
|
||||
} else if (currentCat === '马') {
|
||||
data = data.filter(p => p.category && p.category.includes('马'))
|
||||
} else if (currentCat === '其他') {
|
||||
data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马'))
|
||||
}
|
||||
}
|
||||
// 搜索关键词过滤 - 强匹配(完全包含)
|
||||
if (searchKeyword) {
|
||||
const kw = searchKeyword.trim()
|
||||
if (kw) {
|
||||
data = data.filter(p =>
|
||||
(p.title && p.title.includes(kw)) ||
|
||||
(p.content && p.content.includes(kw)) ||
|
||||
(p.category && p.category.includes(kw)) ||
|
||||
(p.contact && p.contact.includes(kw))
|
||||
)
|
||||
}
|
||||
}
|
||||
setPosts(data)
|
||||
setTotalPosts(todayStats.total || 0)
|
||||
} catch { setPosts([]) }
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => { if (todayStats.total > 0) fetchTodayCategory() }, [todayStats])
|
||||
|
||||
// 搜索关键词变化时重新获取数据
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
fetchPosts(1, '')
|
||||
}, [searchKeyword])
|
||||
|
||||
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
|
||||
|
||||
const StatCard = ({ label, value, color, onClick }) => (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue