Compare commits
No commits in common. "c7e8052194061c0005ff73adaba796281af6d704" and "550fc8f7da99236868900725bb51c836b16a7759" have entirely different histories.
c7e8052194
...
550fc8f7da
|
|
@ -1 +1 @@
|
||||||
1.2.85
|
VERSION=1.2.72
|
||||||
|
|
|
||||||
|
|
@ -180,16 +180,6 @@ class Information(Base):
|
||||||
deal_price = Column(Float, nullable=True)
|
deal_price = Column(Float, nullable=True)
|
||||||
deal_date = Column(Date, 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-已过期
|
# 状态: active-有效, closed-已关闭, expired-已过期
|
||||||
status = Column(String(20), default="active", index=True)
|
status = Column(String(20), default="active", index=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ router = APIRouter(prefix="/api/auth", tags=["认证"])
|
||||||
|
|
||||||
|
|
||||||
def generate_user_code(db):
|
def generate_user_code(db):
|
||||||
"""生成用户编码,从201开始,按自然数顺序递增,跳过已存在的"""
|
"""生成用户编码,从201开始,按自然数顺序递增"""
|
||||||
# 查找最大的user_code
|
# 查找最大的user_code
|
||||||
max_code = db.query(User.user_code).filter(User.user_code != None).order_by(User.user_code.desc()).first()
|
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]:
|
if max_code and max_code[0]:
|
||||||
|
|
@ -21,9 +21,6 @@ def generate_user_code(db):
|
||||||
num = int(max_code[0]) + 1
|
num = int(max_code[0]) + 1
|
||||||
if num < 201:
|
if num < 201:
|
||||||
num = 201
|
num = 201
|
||||||
# 检查是否已存在,如果存在则继续递增
|
|
||||||
while db.query(User).filter(User.user_code == str(num)).first():
|
|
||||||
num += 1
|
|
||||||
return str(num)
|
return str(num)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
@ -103,20 +100,7 @@ def register(user_data: UserCreate, db: Session = Depends(get_db)):
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
|
|
||||||
# 返回用户信息(避免Pydantic序列化问题)
|
return user
|
||||||
return {
|
|
||||||
"id": user.f99_90_id,
|
|
||||||
"username": user.f01_01_name,
|
|
||||||
"user_code": user.user_code,
|
|
||||||
"email": user.email,
|
|
||||||
"phone": user.phone,
|
|
||||||
"avatar": user.avatar,
|
|
||||||
"role": user.role,
|
|
||||||
"level": user.f99_94_level,
|
|
||||||
"aiCount": user.f99_95_ai_count or 0,
|
|
||||||
"searchCount": user.f99_96_search_count or 0,
|
|
||||||
"collectionCount": user.f99_97_collection_count or 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=Token)
|
@router.post("/login", response_model=Token)
|
||||||
|
|
|
||||||
|
|
@ -132,17 +132,14 @@ def get_collections(
|
||||||
# 如果指定 all_users=true,则返回所有用户藏品
|
# 如果指定 all_users=true,则返回所有用户藏品
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
# 管理员默认查看全库,普通用户只看自己,未登录返回空列表
|
# 管理员默认查看全库,普通用户只看自己
|
||||||
if current_user is None or current_user.role != "admin":
|
if current_user.role == "admin":
|
||||||
# 非管理员或未登录用户只能查看自己的藏品(如果没有登录则返回空)
|
# 联表查询获取用户名
|
||||||
if current_user is None:
|
|
||||||
return {"data": [], "total": 0, "page": 1, "limit": 20}
|
|
||||||
query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id)
|
|
||||||
else:
|
|
||||||
# 管理员查看所有藏品
|
|
||||||
query = db.query(Collection, User.f01_01_name.label('owner_name')).join(
|
query = db.query(Collection, User.f01_01_name.label('owner_name')).join(
|
||||||
User, Collection.f99_91_user_id == User.f99_90_id, isouter=True
|
User, Collection.f99_91_user_id == User.f99_90_id, isouter=True
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id)
|
||||||
|
|
||||||
# 如果指定了user_id参数,则只返回该用户的藏品
|
# 如果指定了user_id参数,则只返回该用户的藏品
|
||||||
if user_id:
|
if user_id:
|
||||||
|
|
@ -205,7 +202,7 @@ def get_collections(
|
||||||
data_list = []
|
data_list = []
|
||||||
for item in data:
|
for item in data:
|
||||||
# 处理联表查询结果
|
# 处理联表查询结果
|
||||||
if current_user is not None and current_user.role == "admin":
|
if current_user.role == "admin":
|
||||||
collection_item, owner_name = item
|
collection_item, owner_name = item
|
||||||
else:
|
else:
|
||||||
collection_item = item
|
collection_item = item
|
||||||
|
|
@ -281,12 +278,12 @@ def get_stats(
|
||||||
):
|
):
|
||||||
"""获取藏品统计"""
|
"""获取藏品统计"""
|
||||||
# 获取所有藏品
|
# 获取所有藏品
|
||||||
if current_user is None or current_user.role != "admin":
|
if current_user.role == "admin":
|
||||||
|
all_collections = db.query(Collection).all()
|
||||||
|
else:
|
||||||
all_collections = db.query(Collection).filter(
|
all_collections = db.query(Collection).filter(
|
||||||
Collection.f99_91_user_id == current_user.f99_90_id
|
Collection.f99_91_user_id == current_user.f99_90_id
|
||||||
).all()
|
).all()
|
||||||
else:
|
|
||||||
all_collections = db.query(Collection).all()
|
|
||||||
|
|
||||||
# 总数
|
# 总数
|
||||||
total_count = len(all_collections)
|
total_count = len(all_collections)
|
||||||
|
|
@ -385,7 +382,7 @@ def get_collection(
|
||||||
collection = dict(result._mapping)
|
collection = dict(result._mapping)
|
||||||
|
|
||||||
# 非管理员只能查看自己的藏品
|
# 非管理员只能查看自己的藏品
|
||||||
if (current_user is None or current_user.role != "admin") and collection.get('f99_91_user_id') != current_user.f99_90_id:
|
if current_user.role != "admin" and collection.get('f99_91_user_id') != current_user.f99_90_id:
|
||||||
raise HTTPException(status_code=403, detail="无权访问")
|
raise HTTPException(status_code=403, detail="无权访问")
|
||||||
|
|
||||||
result_dict = {
|
result_dict = {
|
||||||
|
|
@ -499,11 +496,6 @@ def create_collection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# 自动分类:如果未提供号码分类,则根据冠字号自动分类
|
|
||||||
if not collection_data.f02_14_number_category and collection_data.f02_10_prefix_serial:
|
|
||||||
from app.utils.number_category import get_number_category
|
|
||||||
collection_data.f02_14_number_category = get_number_category(collection_data.f02_10_prefix_serial)
|
|
||||||
|
|
||||||
collection = Collection(
|
collection = Collection(
|
||||||
f99_91_user_id=current_user.f99_90_id,
|
f99_91_user_id=current_user.f99_90_id,
|
||||||
f01_01_name=collection_data.f01_01_name,
|
f01_01_name=collection_data.f01_01_name,
|
||||||
|
|
@ -717,7 +709,7 @@ async def delete_image(
|
||||||
Collection.f99_90_id == image.collection_id
|
Collection.f99_90_id == image.collection_id
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if collection and (current_user is None or current_user.role != "admin") and collection.f99_91_user_id != current_user.f99_90_id:
|
if collection and current_user.role != "admin" and collection.f99_91_user_id != current_user.f99_90_id:
|
||||||
raise HTTPException(status_code=403, detail="E00014: 无权删除此图片")
|
raise HTTPException(status_code=403, detail="E00014: 无权删除此图片")
|
||||||
|
|
||||||
# 删除OSS文件(如果path是OSS URL)
|
# 删除OSS文件(如果path是OSS URL)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
# 资讯API路由
|
# 资讯API路由
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session, joinedload
|
from sqlalchemy.orm import Session, joinedload
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
import os
|
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.auth import get_current_user
|
from app.core.auth import get_current_user
|
||||||
|
|
@ -29,12 +28,6 @@ class InformationCreate(BaseModel):
|
||||||
expect_price_max: Optional[float] = None
|
expect_price_max: Optional[float] = None
|
||||||
deal_price: Optional[float] = None
|
deal_price: Optional[float] = None
|
||||||
deal_date: Optional[date] = None
|
deal_date: Optional[date] = None
|
||||||
packaging: Optional[str] = None
|
|
||||||
is_graded: Optional[bool] = False
|
|
||||||
grading_company: Optional[str] = None
|
|
||||||
grading_score: Optional[str] = None
|
|
||||||
category: Optional[str] = None
|
|
||||||
deal_no: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class InformationUpdate(BaseModel):
|
class InformationUpdate(BaseModel):
|
||||||
|
|
@ -49,10 +42,6 @@ class InformationUpdate(BaseModel):
|
||||||
expect_price_max: Optional[float] = None
|
expect_price_max: Optional[float] = None
|
||||||
deal_price: Optional[float] = None
|
deal_price: Optional[float] = None
|
||||||
deal_date: Optional[date] = None
|
deal_date: Optional[date] = None
|
||||||
packaging: Optional[str] = None
|
|
||||||
is_graded: Optional[bool] = None
|
|
||||||
grading_company: Optional[str] = None
|
|
||||||
grading_score: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class InformationResponse(BaseModel):
|
class InformationResponse(BaseModel):
|
||||||
|
|
@ -77,13 +66,6 @@ class InformationResponse(BaseModel):
|
||||||
view_count: int
|
view_count: int
|
||||||
contact_count: int
|
contact_count: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
# 评级相关字段
|
|
||||||
packaging: Optional[str] = None
|
|
||||||
is_graded: Optional[bool] = False
|
|
||||||
grading_company: Optional[str] = None
|
|
||||||
grading_score: Optional[str] = None
|
|
||||||
category: Optional[str] = None
|
|
||||||
deal_no: Optional[str] = None
|
|
||||||
# 用户信息
|
# 用户信息
|
||||||
user_name: Optional[str] = None
|
user_name: Optional[str] = None
|
||||||
user_avatar: Optional[str] = None
|
user_avatar: Optional[str] = None
|
||||||
|
|
@ -106,10 +88,8 @@ class InformationResponse(BaseModel):
|
||||||
def get_information_list(
|
def get_information_list(
|
||||||
info_type: Optional[str] = Query(None, description="类型: seek/deal/publish"),
|
info_type: Optional[str] = Query(None, description="类型: seek/deal/publish"),
|
||||||
status: str = Query("active", description="状态: active/closed/expired"),
|
status: str = Query("active", description="状态: active/closed/expired"),
|
||||||
user_id: Optional[str] = Query(None, description="用户ID,用于获取该用户的行情"),
|
|
||||||
deal_date: Optional[str] = Query(None, description="成交日期过滤,格式YYYY-MM-DD"),
|
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=500),
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
current_user: Optional[User] = Depends(get_current_user),
|
current_user: Optional[User] = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
response: Response = None
|
response: Response = None
|
||||||
|
|
@ -123,16 +103,6 @@ def get_information_list(
|
||||||
if info_type:
|
if info_type:
|
||||||
query = query.filter(Information.info_type == info_type)
|
query = query.filter(Information.info_type == info_type)
|
||||||
|
|
||||||
# 如果传入了user_id,只返回该用户的行情
|
|
||||||
if user_id:
|
|
||||||
query = query.filter(Information.user_id == user_id)
|
|
||||||
|
|
||||||
# 成交日期过滤
|
|
||||||
if deal_date:
|
|
||||||
from datetime import date
|
|
||||||
deal_date_obj = date.fromisoformat(deal_date)
|
|
||||||
query = query.filter(Information.deal_date == deal_date_obj)
|
|
||||||
|
|
||||||
# 按创建时间倒序
|
# 按创建时间倒序
|
||||||
query = query.order_by(Information.created_at.desc())
|
query = query.order_by(Information.created_at.desc())
|
||||||
|
|
||||||
|
|
@ -176,12 +146,6 @@ def get_information_list(
|
||||||
collection_category=item.collection.f01_03_category if item.collection else None,
|
collection_category=item.collection.f01_03_category if item.collection else None,
|
||||||
collection_version=item.collection.f02_11_version if item.collection else None,
|
collection_version=item.collection.f02_11_version if item.collection else None,
|
||||||
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
|
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
|
||||||
packaging=item.packaging,
|
|
||||||
is_graded=item.is_graded or False,
|
|
||||||
grading_company=item.grading_company,
|
|
||||||
grading_score=item.grading_score,
|
|
||||||
category=item.category,
|
|
||||||
deal_no=item.deal_no,
|
|
||||||
matched_count=matched_count,
|
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,
|
network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0,
|
||||||
))
|
))
|
||||||
|
|
@ -430,11 +394,6 @@ def get_information(
|
||||||
view_count=item.view_count,
|
view_count=item.view_count,
|
||||||
contact_count=item.contact_count,
|
contact_count=item.contact_count,
|
||||||
created_at=item.created_at,
|
created_at=item.created_at,
|
||||||
packaging=item.packaging,
|
|
||||||
is_graded=item.is_graded or False,
|
|
||||||
grading_company=item.grading_company,
|
|
||||||
grading_score=item.grading_score,
|
|
||||||
category=item.category,
|
|
||||||
user_name=item.user.f01_01_name if item.user else None,
|
user_name=item.user.f01_01_name if item.user else None,
|
||||||
user_avatar=item.user.avatar if item.user else None,
|
user_avatar=item.user.avatar if item.user else None,
|
||||||
collection_name=item.collection.f01_01_name if item.collection else None,
|
collection_name=item.collection.f01_01_name if item.collection else None,
|
||||||
|
|
@ -452,20 +411,6 @@ def create_information(
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""发布资讯"""
|
"""发布资讯"""
|
||||||
# 生成行情编号:日期 + 5位自然数(从00001开始)
|
|
||||||
deal_no = None
|
|
||||||
if data.info_type == 'deal':
|
|
||||||
today = datetime.now().strftime('%Y%m%d')
|
|
||||||
# 查询当天已有行情数量
|
|
||||||
from app.models.models import Information
|
|
||||||
count_today = db.query(Information).filter(
|
|
||||||
Information.info_type == 'deal',
|
|
||||||
Information.deal_no.like(f'DJ{today}%')
|
|
||||||
).count()
|
|
||||||
# 编号 = 日期 + 5位自然数(如 DJ2026041100001)
|
|
||||||
seq = count_today + 1
|
|
||||||
deal_no = f'DJ{today}{seq:05d}'
|
|
||||||
|
|
||||||
info = Information(
|
info = Information(
|
||||||
user_id=current_user.f99_90_id,
|
user_id=current_user.f99_90_id,
|
||||||
info_type=data.info_type,
|
info_type=data.info_type,
|
||||||
|
|
@ -480,12 +425,6 @@ def create_information(
|
||||||
expect_price_max=data.expect_price_max,
|
expect_price_max=data.expect_price_max,
|
||||||
deal_price=data.deal_price,
|
deal_price=data.deal_price,
|
||||||
deal_date=data.deal_date,
|
deal_date=data.deal_date,
|
||||||
packaging=data.packaging,
|
|
||||||
is_graded=data.is_graded or False,
|
|
||||||
grading_company=data.grading_company,
|
|
||||||
grading_score=data.grading_score,
|
|
||||||
category=data.category,
|
|
||||||
deal_no=deal_no,
|
|
||||||
status="active"
|
status="active"
|
||||||
)
|
)
|
||||||
db.add(info)
|
db.add(info)
|
||||||
|
|
@ -560,16 +499,6 @@ def update_information(
|
||||||
info.deal_price = data.deal_price
|
info.deal_price = data.deal_price
|
||||||
if data.deal_date is not None:
|
if data.deal_date is not None:
|
||||||
info.deal_date = data.deal_date
|
info.deal_date = data.deal_date
|
||||||
if data.packaging is not None:
|
|
||||||
info.packaging = data.packaging
|
|
||||||
if data.is_graded is not None:
|
|
||||||
info.is_graded = data.is_graded
|
|
||||||
if data.grading_company is not None:
|
|
||||||
info.grading_company = data.grading_company
|
|
||||||
if data.grading_score is not None:
|
|
||||||
info.grading_score = data.grading_score
|
|
||||||
if data.category is not None:
|
|
||||||
info.category = data.category
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(info)
|
db.refresh(info)
|
||||||
|
|
@ -593,11 +522,6 @@ def update_information(
|
||||||
view_count=info.view_count,
|
view_count=info.view_count,
|
||||||
contact_count=info.contact_count,
|
contact_count=info.contact_count,
|
||||||
created_at=info.created_at,
|
created_at=info.created_at,
|
||||||
packaging=info.packaging,
|
|
||||||
is_graded=info.is_graded or False,
|
|
||||||
grading_company=info.grading_company,
|
|
||||||
grading_score=info.grading_score,
|
|
||||||
category=info.category,
|
|
||||||
user_name=current_user.f01_01_name,
|
user_name=current_user.f01_01_name,
|
||||||
user_avatar=current_user.avatar,
|
user_avatar=current_user.avatar,
|
||||||
collection_name=info.collection.f01_01_name if info.collection else None,
|
collection_name=info.collection.f01_01_name if info.collection else None,
|
||||||
|
|
@ -872,7 +796,7 @@ def get_deal_stats(
|
||||||
@router.get("/my/list", response_model=List[InformationResponse])
|
@router.get("/my/list", response_model=List[InformationResponse])
|
||||||
def get_my_information_list(
|
def get_my_information_list(
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=500),
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
|
|
@ -1200,272 +1124,3 @@ def get_seek_stats(
|
||||||
"userMatchedCount": user_matched_count,
|
"userMatchedCount": user_matched_count,
|
||||||
"totalMatchedCount": total_matched_count
|
"totalMatchedCount": total_matched_count
|
||||||
}
|
}
|
||||||
|
|
||||||
# 批量解析行情数据API
|
|
||||||
@router.post("/batch-parse")
|
|
||||||
async def batch_parse_deals(text: str = Body(..., embed=True)):
|
|
||||||
"""使用AI智能解析批量行情文本"""
|
|
||||||
import httpx
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
# 使用阿里云百炼Coding Plan API
|
|
||||||
api_key = "sk-sp-d5ce68bb203e48ca857c2aea25255b26"
|
|
||||||
base_url = "https://coding.dashscope.aliyuncs.com/v1"
|
|
||||||
|
|
||||||
# 更详细的解析提示词
|
|
||||||
prompt = f"""你是一个专业的龙钞行情数据提取助手。请从以下文本中提取所有龙钞行情记录。
|
|
||||||
|
|
||||||
【解析规则】
|
|
||||||
1. 每条记录格式:冠字号 价格 评级/包装 出售者
|
|
||||||
2. 冠字号:J0开头的9位数字(如J0298810101)
|
|
||||||
3. 价格:¥xxx,xxx 格式,去掉逗号转为数字
|
|
||||||
4. 评级/包装:PC69/PMG68/爱藏67+/爱藏67 标十 标百 单张
|
|
||||||
5. 出售者:人名
|
|
||||||
6. 号码分类:根据冠字号数字特征判断(圆圆号/倒置号/金马王/金马号/金山王/天马王/金山号/天马号/朦胧王/朦胧号/如意号/钻石号/永恒号/带7号/带4号)
|
|
||||||
|
|
||||||
【输出格式】
|
|
||||||
返回JSON数组,每条记录包含:
|
|
||||||
- serial: 冠字号(完整9位,如J0298810101)
|
|
||||||
- price: 价格(数字)
|
|
||||||
- grade: 评级(如PC69, PMG68, 爱藏67+, 爱藏67)
|
|
||||||
- packaging: 包装类型(标十/标百/单张)
|
|
||||||
- category: 号码分类
|
|
||||||
- seller: 出售者
|
|
||||||
- date: 交易日(从文本中提取日期,如2026-03-29)
|
|
||||||
|
|
||||||
只返回JSON数组,不要其他内容。
|
|
||||||
|
|
||||||
文本:
|
|
||||||
{text}"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient(timeout=120.0) as client:
|
|
||||||
response = await client.post(
|
|
||||||
f"{base_url}/chat/completions",
|
|
||||||
json={
|
|
||||||
"model": "qwen3.6-plus",
|
|
||||||
"messages": [
|
|
||||||
{"role": "system", "content": "你是一个专业的收藏品行情数据提取助手,擅长从文本中提取结构化的交易数据。只返回JSON数组。"},
|
|
||||||
{"role": "user", "content": prompt}
|
|
||||||
],
|
|
||||||
"temperature": 0.1
|
|
||||||
},
|
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
return {"success": False, "error": f"API错误: {response.status_code}, {response.text[:200]}"}
|
|
||||||
|
|
||||||
result = response.json()
|
|
||||||
# 阿里云百炼OpenAI兼容格式
|
|
||||||
choices = result.get("choices", [])
|
|
||||||
content = ""
|
|
||||||
if choices and len(choices) > 0:
|
|
||||||
content = choices[0].get("message", {}).get("content", "")
|
|
||||||
|
|
||||||
# 解析JSON
|
|
||||||
try:
|
|
||||||
# 尝试提取JSON
|
|
||||||
if "```json" in content:
|
|
||||||
content = content.split("```json")[1].split("```")[0]
|
|
||||||
elif "```" in content:
|
|
||||||
content = content.split("```")[1].split("```")[0]
|
|
||||||
|
|
||||||
# 尝试直接解析
|
|
||||||
data = json.loads(content.strip())
|
|
||||||
return {"success": True, "data": data}
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# 尝试用正则提取
|
|
||||||
match = re.search(r'\[.*\]', content, re.DOTALL)
|
|
||||||
if match:
|
|
||||||
try:
|
|
||||||
data = json.loads(match.group())
|
|
||||||
return {"success": True, "data": data}
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return {"success": False, "error": "解析失败", "raw": content[:500]}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {"success": False, "error": str(e)}
|
|
||||||
|
|
||||||
# 本地正则解析函数
|
|
||||||
def parse_deals_locally(text: str, default_packaging: str = '', default_date: str = '', default_platform: str = ''):
|
|
||||||
"""本地正则解析批量行情文本"""
|
|
||||||
import re
|
|
||||||
from datetime import datetime
|
|
||||||
results = []
|
|
||||||
|
|
||||||
# 尝试从文本中提取日期(可能出现在标题或时间戳中)
|
|
||||||
# 格式如: 3月29日, 2026年3月29日, 2026-03-29
|
|
||||||
date_patterns = [
|
|
||||||
r'(\d{1,2})月(\d{1,2})日',
|
|
||||||
r'(\d{4})年(\d{1,2})月(\d{1,2})日',
|
|
||||||
r'(\d{4})-(\d{1,2})-(\d{1,2})'
|
|
||||||
]
|
|
||||||
|
|
||||||
extracted_date = None
|
|
||||||
for pattern in date_patterns:
|
|
||||||
match = re.search(pattern, text)
|
|
||||||
if match:
|
|
||||||
try:
|
|
||||||
if len(match.groups()) == 2:
|
|
||||||
# 3月29日 - 使用当前年份
|
|
||||||
extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}"
|
|
||||||
elif len(match.groups()) == 3:
|
|
||||||
if int(match.group(1)) > 2000:
|
|
||||||
# 2026年3月29日
|
|
||||||
extracted_date = f"{match.group(1)}-{int(match.group(2)):02d}-{int(match.group(3)):02d}"
|
|
||||||
else:
|
|
||||||
# 3月29日格式
|
|
||||||
extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}"
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 默认使用今天
|
|
||||||
default_date = datetime.now().strftime('%Y-%m-%d')
|
|
||||||
deal_date = extracted_date or default_date
|
|
||||||
|
|
||||||
lines = text.strip().split('\n')
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
line = line.strip()
|
|
||||||
if not line or 'J0' not in line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 提取冠字号 J0 + 8-9位数字
|
|
||||||
serial_match = re.search(r'J0(\d{8,9})', line)
|
|
||||||
if not serial_match:
|
|
||||||
continue
|
|
||||||
|
|
||||||
serial_num = serial_match.group(1)
|
|
||||||
if len(serial_num) == 9:
|
|
||||||
serial_num = serial_num[:8]
|
|
||||||
serial = 'J0' + serial_num
|
|
||||||
|
|
||||||
# 提取价格 ¥xxx,xxx 或 xxx,xxx(必须在J0之后)
|
|
||||||
serial_pos = line.find(serial)
|
|
||||||
after_serial = line[serial_pos + len(serial):]
|
|
||||||
price_match = re.search(r'[¥¥]?\s*([1-9][\d,]{2,})', after_serial)
|
|
||||||
if not price_match:
|
|
||||||
continue
|
|
||||||
price = int(price_match.group(1).replace(',', ''))
|
|
||||||
|
|
||||||
# 提取卖家(价格后面的中文字符)
|
|
||||||
after_price_pos = after_serial.find(price_match.group(0)) + len(price_match.group(0))
|
|
||||||
after_price = after_serial[after_price_pos:]
|
|
||||||
seller_match = re.search(r'([^\d\s¥¥]+?)(?:\s*$|\s*\n)', after_price)
|
|
||||||
seller = seller_match.group(1).strip() if seller_match else ''
|
|
||||||
|
|
||||||
# 分类判断
|
|
||||||
digits = serial_num
|
|
||||||
d = digits
|
|
||||||
category = '通货'
|
|
||||||
if not any(c in d for c in ['1','2','3','4','5','7']): category = '圆圆号'
|
|
||||||
elif not any(c in d for c in ['2','3','4','5','7']): category = '倒置号'
|
|
||||||
elif not any(c in d for c in ['1','2','3','4','7']): category = '金马王'
|
|
||||||
elif not any(c in d for c in ['2','3','4','7']): category = '金马号'
|
|
||||||
elif not any(c in d for c in ['1','2','4','5','7']): category = '金山王'
|
|
||||||
elif not any(c in d for c in ['1','2','4','7']): category = '天马王'
|
|
||||||
elif not any(c in d for c in ['2','4','5','7']): category = '金山号'
|
|
||||||
elif not any(c in d for c in ['2','4','7']): category = '天马号'
|
|
||||||
elif not any(c in d for c in ['1','3','4','5','7']): category = '朦胧王'
|
|
||||||
elif not any(c in d for c in ['3','4','5','7']): category = '朦胧号'
|
|
||||||
elif not any(c in d for c in ['1','3','4','7']): category = '如意号'
|
|
||||||
elif not any(c in d for c in ['3','4','7']): category = '钻石号'
|
|
||||||
elif not any(c in d for c in ['4','7']): category = '永恒号'
|
|
||||||
elif '4' not in d: category = '带7号'
|
|
||||||
|
|
||||||
# 提取评级机构 PCGS/PMG/ACG/爱藏
|
|
||||||
grade = ''
|
|
||||||
grading_company = ''
|
|
||||||
packaging = '单张'
|
|
||||||
|
|
||||||
if 'PC69' in line or 'PC68' in line or 'PC67' in line:
|
|
||||||
grade_match = re.search(r'PC(6[789]|5\d?)', line)
|
|
||||||
grade = 'PC' + grade_match.group(1) if grade_match else ''
|
|
||||||
grading_company = 'PCGS'
|
|
||||||
elif 'PMG68' in line or 'PMG67' in line:
|
|
||||||
grade_match = re.search(r'PMG(6[789]|5\d?)', line)
|
|
||||||
grade = 'PMG' + grade_match.group(1) if grade_match else ''
|
|
||||||
grading_company = 'PMG'
|
|
||||||
elif 'ACG' in line:
|
|
||||||
grade_match = re.search(r'ACG(6[789]|5\d?)', line)
|
|
||||||
grade = 'ACG' + grade_match.group(1) if grade_match else ''
|
|
||||||
grading_company = 'ACG'
|
|
||||||
elif '爱藏67+' in line:
|
|
||||||
grade = '67+'
|
|
||||||
grading_company = '爱藏'
|
|
||||||
elif '爱藏67' in line:
|
|
||||||
grade = '67'
|
|
||||||
grading_company = '爱藏'
|
|
||||||
|
|
||||||
# 判断包装类型
|
|
||||||
packaging = '单张'
|
|
||||||
|
|
||||||
# 如果传入了默认包装类型,先使用默认
|
|
||||||
if default_packaging:
|
|
||||||
packaging = default_packaging
|
|
||||||
|
|
||||||
# 简化识别:带"刀"字=标百,带"标"字=标十
|
|
||||||
if '刀' in line:
|
|
||||||
packaging = '标百'
|
|
||||||
elif '标' in line:
|
|
||||||
packaging = '标十'
|
|
||||||
|
|
||||||
# 尾号判断:如果冠字号尾号是01/11/21/31/41/51/61/71/81/91,且有刀/标字样,基本确认是标百
|
|
||||||
if len(serial_num) >= 2:
|
|
||||||
tail = serial_num[-2:]
|
|
||||||
if tail in ['01', '11', '21', '31', '41', '51', '61', '71', '81', '91']:
|
|
||||||
if '刀' in line or ('标' in line and packaging == '单张'):
|
|
||||||
packaging = '标百'
|
|
||||||
packaging = '标百'
|
|
||||||
|
|
||||||
# 如果没有刀/标字样,但尾号是01且没有其他特征,可能是标百
|
|
||||||
if packaging == '单张' and len(serial_num) >= 2:
|
|
||||||
tail = serial_num[-2:]
|
|
||||||
if tail == '01':
|
|
||||||
# 检查是否在特定语境下
|
|
||||||
packaging = '标百'
|
|
||||||
|
|
||||||
# 计算尾号和大小号
|
|
||||||
tail_number = ''
|
|
||||||
size_type = ''
|
|
||||||
if packaging == '标十' and len(serial_num) >= 2:
|
|
||||||
tail_number = serial_num[-2:]
|
|
||||||
size_type = tail_number in ['01','11','21','31','41','51'] and '小号' or '大号'
|
|
||||||
elif packaging == '标百' and len(serial_num) >= 3:
|
|
||||||
tail_number = serial_num[-3:]
|
|
||||||
size_type = tail_number in ['101','201','301','401','501'] and '小号' or '大号'
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'serial': serial,
|
|
||||||
'price': price,
|
|
||||||
'category': category,
|
|
||||||
'seller': seller,
|
|
||||||
'packaging': packaging,
|
|
||||||
'grade': grade,
|
|
||||||
'grading_company': grading_company,
|
|
||||||
'deal_date': deal_date or default_date, # 成交时间
|
|
||||||
'entry_date': default_date, # 录入时间
|
|
||||||
'is_graded': bool(grade),
|
|
||||||
'tail_number': tail_number, # 尾号
|
|
||||||
'size_type': size_type, # 大小号
|
|
||||||
'platform': default_platform # 平台
|
|
||||||
})
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
@router.post("/batch-parse-local")
|
|
||||||
async def batch_parse_deals_local(request: dict = Body(...)):
|
|
||||||
"""本地正则解析批量行情文本(无需AI)"""
|
|
||||||
text = request.get('text', '')
|
|
||||||
default_packaging = request.get('defaultPackaging', '')
|
|
||||||
default_date = request.get('defaultDate', '')
|
|
||||||
default_platform = request.get('defaultPlatform', '')
|
|
||||||
results = parse_deals_locally(text, default_packaging, default_date, default_platform)
|
|
||||||
return {"success": True, "data": results}
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ router = APIRouter(prefix="/api", tags=["用户"])
|
||||||
|
|
||||||
# ============ 当前用户接口 ============
|
# ============ 当前用户接口 ============
|
||||||
|
|
||||||
@router.get("/users/me") # 无 response_model,避免 Pydantic 序列化问题
|
@router.get("/users/me", response_model=UserResponse)
|
||||||
def get_current_user_info(
|
def get_current_user_info(
|
||||||
current_user: User = Depends(get_current_user)
|
current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
|
|
@ -47,7 +47,7 @@ def get_current_user_info(
|
||||||
"f99_93_updated_at": current_user.f99_93_updated_at.isoformat() if current_user.f99_93_updated_at else None
|
"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,避免 Pydantic 序列化问题
|
@router.put("/users/me", response_model=UserResponse)
|
||||||
def update_current_user(
|
def update_current_user(
|
||||||
user_update: UserUpdate,
|
user_update: UserUpdate,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
|
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
# 号码分类工具
|
|
||||||
# 按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
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
1.2.85
|
VERSION=1.2.77
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
<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">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<title>甲辰收藏 v=1.2.81</title>
|
<title>甲辰收藏 v=1.2.72</title>
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="/static/icons/favicon.png" />
|
<link rel="icon" type="image/png" href="/static/icons/favicon.png" />
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "jiachenlong-frontend",
|
"name": "jiachenlong-frontend",
|
||||||
"version": "1.2.81",
|
"version": "1.2.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "jiachenlong-frontend",
|
"name": "jiachenlong-frontend",
|
||||||
"version": "1.2.81",
|
"version": "1.2.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"vite": "^6.4.2"
|
"vite": "^6.0.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
|
|
@ -2047,9 +2047,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.4.2",
|
"version": "6.4.1",
|
||||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "jiachenlong-frontend",
|
"name": "jiachenlong-frontend",
|
||||||
"version": "1.2.85",
|
"version": "1.2.77",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "甲辰藏品管理系统 - 移动端前端",
|
"description": "甲辰藏品管理系统 - 移动端前端",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -9,13 +9,13 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.9",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^7.1.0"
|
"react-router-dom": "^7.1.0",
|
||||||
|
"axios": "^1.7.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"vite": "^6.4.2"
|
"vite": "^6.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
v=1.2.80
|
|
||||||
|
|
@ -5,6 +5,7 @@ import List from './pages/List'
|
||||||
import Add from './pages/Add'
|
import Add from './pages/Add'
|
||||||
import Stats from './pages/Stats'
|
import Stats from './pages/Stats'
|
||||||
import News from './pages/News'
|
import News from './pages/News'
|
||||||
|
import Info from './pages/Info'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import Detail from './pages/Detail'
|
import Detail from './pages/Detail'
|
||||||
import Edit from './pages/Edit'
|
import Edit from './pages/Edit'
|
||||||
|
|
@ -30,6 +31,7 @@ export default function App() {
|
||||||
const basePath = path.split('?')[0]
|
const basePath = path.split('?')[0]
|
||||||
if (basePath === '/') return <Home />
|
if (basePath === '/') return <Home />
|
||||||
if (basePath === '/news') return <News />
|
if (basePath === '/news') return <News />
|
||||||
|
if (basePath === '/info') return <Info />
|
||||||
if (basePath === '/stats') return <Stats />
|
if (basePath === '/stats') return <Stats />
|
||||||
if (basePath === '/list') return <List />
|
if (basePath === '/list') return <List />
|
||||||
if (basePath === '/add') return <Add />
|
if (basePath === '/add') return <Add />
|
||||||
|
|
@ -51,6 +53,7 @@ export default function App() {
|
||||||
}
|
}
|
||||||
const isAdmin = user && user.role === 'admin'
|
const isAdmin = user && user.role === 'admin'
|
||||||
const isEditor = user && user.role === 'editor'
|
const isEditor = user && user.role === 'editor'
|
||||||
|
const canAccessInfo = isAdmin || isEditor
|
||||||
|
|
||||||
// 登录页面独立渲染,不显示底部导航
|
// 登录页面独立渲染,不显示底部导航
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|
@ -85,10 +88,11 @@ export default function App() {
|
||||||
}}>
|
}}>
|
||||||
{[
|
{[
|
||||||
{ path: '/', icon: '🏠', label: '首页' },
|
{ path: '/', icon: '🏠', label: '首页' },
|
||||||
{ path: '/news', icon: '📰', label: '行情' },
|
{ path: '/news', icon: '📰', label: '资讯' },
|
||||||
{ path: '/stats', icon: '📊', label: '统计' },
|
{ path: '/stats', icon: '📊', label: '统计' },
|
||||||
{ path: '/list', icon: '📚', label: '我的' },
|
{ path: '/list', icon: '📚', label: '藏品' },
|
||||||
{ path: '/add', icon: '🎯', label: '录入' },
|
{ path: '/add', icon: '🎯', label: '添加' },
|
||||||
|
...(canAccessInfo ? [{ path: '/info', icon: '📝', label: '信息' }] : []),
|
||||||
...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : [])
|
...(isAdmin ? [{ path: '/admin', icon: '⚙️', label: '管理' }] : [])
|
||||||
].map(tab => (
|
].map(tab => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -112,4 +116,3 @@ export default function App() {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Fri Apr 10 03:44:24 PM CST 2026
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx
|
// 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx
|
||||||
|
|
||||||
// 从环境变量读取(vite.config.js 注入)
|
// 从环境变量读取(vite.config.js 注入)
|
||||||
export const APP_VERSION = import.meta.env.APP_VERSION || '1.2.82'
|
export const APP_VERSION = import.meta.env.APP_VERSION || '1.0.0'
|
||||||
|
|
||||||
// 版本信息
|
// 版本信息
|
||||||
export const VERSION_INFO = {
|
export const VERSION_INFO = {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
// 添加藏品页面 - 支持 AI 识别/手工录入
|
// 添加藏品页面 - 支持 AI 识别/手工录入/批量录入
|
||||||
import React, { useState, useRef } from 'react'
|
import React, { useState, useRef } from 'react'
|
||||||
import { APP_VERSION } from '../config/version'
|
import { APP_VERSION } from '../config/version'
|
||||||
const API_BASE = localStorage.getItem('API_BASE') || ''
|
const numberCategoryOptions = [{value:'无2347',label:'无2347'},{value:'无347',label:'无347'},{value:'无247',label:'无247'},{value:'无47',label:'无47'},{value:'无4',label:'无4'},{value:'带4',label:'带4'},{value:'其他',label:'其他'}];
|
||||||
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) => {
|
const convertField = (obj) => {
|
||||||
|
|
@ -72,57 +71,12 @@ const getDefaultForm = () => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
export default function Add() {
|
export default function Add() {
|
||||||
// 行情录入表单
|
|
||||||
const [dealForm, setDealForm] = useState({
|
|
||||||
serial: '',
|
|
||||||
category: '',
|
|
||||||
packaging: '标十',
|
|
||||||
price: '',
|
|
||||||
platform: '淘宝',
|
|
||||||
seller: '',
|
|
||||||
buyer: '',
|
|
||||||
date: new Date().toISOString().split('T')[0],
|
|
||||||
isGraded: false,
|
|
||||||
gradingCompany: '',
|
|
||||||
gradingScore: ''
|
|
||||||
})
|
|
||||||
const [batchDefaultPackaging, setBatchDefaultPackaging] = useState('')
|
|
||||||
const [batchDefaultDate, setBatchDefaultDate] = useState('')
|
|
||||||
const [batchDefaultPlatform, setBatchDefaultPlatform] = useState('')
|
|
||||||
const [dealMode, setDealMode] = useState('single') // single-单条录入, batch-批量录入
|
|
||||||
const [batchText, setBatchText] = useState('')
|
|
||||||
const [batchResult, setBatchResult] = useState([])
|
|
||||||
const [parsing, setParsing] = useState(false)
|
|
||||||
const [savingDeal, setSavingDeal] = useState(false)
|
|
||||||
|
|
||||||
// 号码分类函数
|
|
||||||
const autoCategory = (serial) => {
|
|
||||||
const digits = serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
|
|
||||||
if (!digits) return ''
|
|
||||||
const d = digits
|
|
||||||
if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '圆圆号'
|
|
||||||
if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '倒置号'
|
|
||||||
if (!d.includes('1') && !d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马王'
|
|
||||||
if (!d.includes('2') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '金马号'
|
|
||||||
if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山王'
|
|
||||||
if (!d.includes('1') && !d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马王'
|
|
||||||
if (!d.includes('2') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '金山号'
|
|
||||||
if (!d.includes('2') && !d.includes('4') && !d.includes('7')) return '天马号'
|
|
||||||
if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧王'
|
|
||||||
if (!d.includes('3') && !d.includes('4') && !d.includes('5') && !d.includes('7')) return '朦胧号'
|
|
||||||
if (!d.includes('1') && !d.includes('3') && !d.includes('4') && !d.includes('7')) return '如意号'
|
|
||||||
if (!d.includes('3') && !d.includes('4') && !d.includes('7')) return '钻石号'
|
|
||||||
if (!d.includes('4') && !d.includes('7')) return '永恒号'
|
|
||||||
if (!d.includes('4')) return '带7号'
|
|
||||||
return '带4号'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据 URL 参数确定默认标签
|
// 根据 URL 参数确定默认标签
|
||||||
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
|
const params = new URLSearchParams(window.location.hash.split('?')[1] || '')
|
||||||
const modeParam = params.get('mode')
|
const modeParam = params.get('mode')
|
||||||
const defaultTab = modeParam === 'manual' ? 'manual' : 'ai'
|
const defaultTab = modeParam === 'manual' ? 'manual' : 'ai'
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, deal
|
const [activeTab, setActiveTab] = useState(defaultTab) // ai, manual, batch
|
||||||
const [form, setForm] = useState(getDefaultForm())
|
const [form, setForm] = useState(getDefaultForm())
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
@ -204,8 +158,8 @@ export default function Add() {
|
||||||
else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347'
|
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('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('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47'
|
||||||
else if (digits.includes('7') && !digits.includes('4')) cat = '带7号'
|
else if (digits.includes('7') && !digits.includes('4')) cat = '无4'
|
||||||
else if (digits.includes('4')) cat = '带4号'
|
else if (digits.includes('4')) cat = '带4'
|
||||||
else cat = '其他'
|
else cat = '其他'
|
||||||
}
|
}
|
||||||
setForm(prev => ({ ...prev, numberCategory: cat }))
|
setForm(prev => ({ ...prev, numberCategory: cat }))
|
||||||
|
|
@ -320,8 +274,8 @@ export default function Add() {
|
||||||
else if (!digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347'
|
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('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('4') && !digits.includes('7') && digits.includes('2') && digits.includes('3')) cat = '无47'
|
||||||
else if (digits.includes('7') && !digits.includes('4')) cat = '带7号'
|
else if (digits.includes('7') && !digits.includes('4')) cat = '无4'
|
||||||
else if (digits.includes('4')) cat = '带4号'
|
else if (digits.includes('4')) cat = '带4'
|
||||||
else cat = '其他'
|
else cat = '其他'
|
||||||
formWithCategory.numberCategory = cat
|
formWithCategory.numberCategory = cat
|
||||||
}
|
}
|
||||||
|
|
@ -451,15 +405,15 @@ export default function Add() {
|
||||||
<div style={{ display: 'flex', gap: '8px' }}>
|
<div style={{ display: 'flex', gap: '8px' }}>
|
||||||
<button onClick={() => setActiveTab('ai')}
|
<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' }}>
|
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>
|
||||||
<button onClick={() => setActiveTab('manual')}
|
<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' }}>
|
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>
|
||||||
<button onClick={() => setActiveTab('deal')}
|
<button onClick={() => setActiveTab('batch')} disabled
|
||||||
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' }}>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -481,7 +435,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>
|
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}
|
<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' }}>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -703,369 +657,16 @@ export default function Add() {
|
||||||
</div>
|
</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>
|
||||||
{activeTab === 'deal' && (
|
<div style={{ fontSize: '13px' }}>敬请期待后续版本</div>
|
||||||
<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')
|
|
||||||
const digits = dealForm.serial.replace('J', '').replace(/[^0-9]/g, '').slice(0, 9)
|
|
||||||
let tailNumber = '', sizeType = ''
|
|
||||||
if (dealForm.packaging === '标十' && digits.length >= 2) {
|
|
||||||
tailNumber = digits.slice(-2)
|
|
||||||
sizeType = ['01','11','21','31','41','51'].includes(tailNumber) ? '小号' : '大号'
|
|
||||||
} else if (dealForm.packaging === '标百' && digits.length >= 3) {
|
|
||||||
tailNumber = digits.slice(-3)
|
|
||||||
sizeType = ['101','201','301','401','501'].includes(tailNumber) ? '小号' : '大号'
|
|
||||||
}
|
|
||||||
const response = await fetch(`${API_BASE}/api/information/`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
info_type: 'deal',
|
|
||||||
title: `J0${dealForm.serial.replace('J', '').slice(0, 8)}-¥${dealForm.price}`,
|
|
||||||
content: `分类: ${dealForm.category || '未分类'}\n尾号: ${tailNumber || '-'}\n大小号: ${sizeType || '-'}\n包装: ${dealForm.packaging}\n平台: ${dealForm.platform}\n价格: ¥${dealForm.price}\n出售者: ${dealForm.seller || '-'}\n购买者: ${dealForm.buyer || '-'}\n日期: ${dealForm.date}\n评级: ${dealForm.gradingCompany ? dealForm.gradingCompany + ' ' + dealForm.gradingScore : '未评级'}`,
|
|
||||||
deal_price: parseFloat(dealForm.price),
|
|
||||||
deal_date: dealForm.date,
|
|
||||||
packaging: dealForm.packaging,
|
|
||||||
category: dealForm.category,
|
|
||||||
is_graded: !!dealForm.gradingCompany,
|
|
||||||
grading_company: dealForm.gradingCompany || '',
|
|
||||||
grading_score: dealForm.gradingScore || ''
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if (response.ok) {
|
|
||||||
alert('行情录入成功!')
|
|
||||||
setDealForm({ serial: '', category: '', packaging: '标十', price: '', platform: '淘宝', seller: '', buyer: '', date: new Date().toISOString().split('T')[0] })
|
|
||||||
} else {
|
|
||||||
const data = await response.json()
|
|
||||||
alert('录入失败: ' + (data.detail || '未知错误'))
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
alert('提交失败: ' + e.message)
|
|
||||||
} finally {
|
|
||||||
setSavingDeal(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={savingDeal}
|
|
||||||
style={{ width: '100%', padding: '14px', background: savingDeal ? '#64748b' : '#fbbf24', color: savingDeal ? '#94a3b8' : '#1e293b', border: 'none', borderRadius: '10px', fontSize: '16px', fontWeight: '600', cursor: savingDeal ? 'not-allowed' : 'pointer' }}>
|
|
||||||
{savingDeal ? '提交中...' : '提交行情'}
|
|
||||||
</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/information/`, {
|
|
||||||
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
info_type: 'deal',
|
|
||||||
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 || '单张',
|
|
||||||
is_graded: item.is_graded || false,
|
|
||||||
grading_company: item.grading_company || '',
|
|
||||||
grading_score: item.grade || '',
|
|
||||||
category: item.category || '',
|
|
||||||
tail_number: tail_number,
|
|
||||||
size_type: size_type
|
|
||||||
})
|
|
||||||
})
|
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 版本号 */}
|
||||||
<div style={{ position: 'fixed', bottom: '16px', right: '16px', color: 'rgba(255,255,255,0.2)', fontSize: '11px', zIndex: 100 }}>v{APP_VERSION}</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>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -482,10 +482,7 @@ export default function Admin() {
|
||||||
<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="带7号">带7号</option>
|
|
||||||
<option value="带4号">带4号</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const categoryOptions = [
|
||||||
{ value: '其他', label: '其他' }
|
{ 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 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 rarityOptions = [
|
const rarityOptions = [
|
||||||
{ value: '通货', label: '通货' },
|
{ value: '通货', label: '通货' },
|
||||||
{ value: '特色', label: '特色' },
|
{ value: '特色', label: '特色' },
|
||||||
|
|
@ -157,8 +157,8 @@ export default function Edit() {
|
||||||
if (digits) {
|
if (digits) {
|
||||||
if (!digits.includes('2') && !digits.includes('3') && !digits.includes('4') && !digits.includes('7')) cat = '无2347'
|
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('3') && !digits.includes('4') && !digits.includes('7')) cat = '无347'
|
||||||
else if (digits.includes('4')) cat = '带4号'
|
else if (digits.includes('4')) cat = '带4'
|
||||||
else if (digits.includes('7')) cat = '带7号'
|
else if (digits.includes('7')) cat = '无4'
|
||||||
else if (!digits.includes('4') && !digits.includes('7')) cat = '无47'
|
else if (!digits.includes('4') && !digits.includes('7')) cat = '无47'
|
||||||
else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
|
else if (!digits.includes('2') && !digits.includes('4') && !digits.includes('7')) cat = '无247'
|
||||||
else cat = '其他'
|
else cat = '其他'
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ export default function Home() {
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<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' }} />
|
<img src="/static/images/jiachenlong-logo.png" alt="甲辰收藏" style={{ width: '48px', height: '48px', borderRadius: '50%', objectFit: 'cover' }} />
|
||||||
<div>
|
<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: '#fff', fontSize: '18px', fontWeight: '600' }}>{user?.username || '用户'}</div>
|
||||||
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: '12px' }}>欢迎回来 👋</div>
|
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: '12px' }}>欢迎回来 👋</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
|
|
@ -282,7 +282,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 style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.dai4?.deals || 0) + (dragonStats.dai4?.wants || 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '60px 1fr 1fr 1fr', gap: '8px', marginBottom: '6px' }}>
|
||||||
<div style={{ color: '#06b6d4', fontSize: '14px', fontWeight: '500' }}>带7号</div>
|
<div style={{ color: '#22c55e', fontSize: '14px', fontWeight: '500' }}>无4</div>
|
||||||
<div style={{ color: '#22c55e', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{dragonStats.wu4?.deals || 0}</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: '#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>
|
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '700', textAlign: 'center' }}>{(dragonStats.wu4?.deals || 0) + (dragonStats.wu4?.wants || 0)}</div>
|
||||||
|
|
@ -367,13 +367,13 @@ export default function Home() {
|
||||||
{(isAdmin ? [
|
{(isAdmin ? [
|
||||||
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
|
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
|
||||||
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
|
{ 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: '#/stats', idx: 3 },
|
||||||
{ icon: '⚙️', label: '管理', hash: '#/admin', idx: 4 }
|
{ icon: '⚙️', label: '管理', hash: '#/admin', idx: 4 }
|
||||||
] : [
|
] : [
|
||||||
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
|
{ icon: '🏠', label: '首页', hash: '#/', idx: 0 },
|
||||||
{ icon: '📚', label: '藏品', hash: '#/list', idx: 1 },
|
{ 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: '#/stats', idx: 3 }
|
||||||
]).map((item) => (
|
]).map((item) => (
|
||||||
<div key={item.idx} onClick={() => window.location.hash = item.hash} style={{
|
<div key={item.idx} onClick={() => window.location.hash = item.hash} style={{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,680 @@
|
||||||
|
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,6 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { APP_VERSION } from '../config/version'
|
import { APP_VERSION } from '../config/version'
|
||||||
const API_BASE = localStorage.getItem('API_BASE') || ''
|
|
||||||
|
|
||||||
export default function List() {
|
export default function List() {
|
||||||
const [collections, setCollections] = useState([])
|
const [collections, setCollections] = useState([])
|
||||||
|
|
@ -18,12 +17,6 @@ export default function List() {
|
||||||
const [isAdmin, setIsAdmin] = useState(false)
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pagination, setPagination] = useState({ total: 0, pages: 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(() => {
|
useEffect(() => {
|
||||||
// 检查是否管理员
|
// 检查是否管理员
|
||||||
|
|
@ -68,56 +61,6 @@ export default function List() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 获取当前用户录入的行情数据
|
|
||||||
const fetchMyDeals = async () => {
|
|
||||||
setDealsLoading(true)
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
const userStr = localStorage.getItem('user')
|
|
||||||
if (!token || !userStr) {
|
|
||||||
setDealsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const user = JSON.parse(userStr)
|
|
||||||
const res = await fetch(`${API_BASE}/api/information/list?info_type=deal&user_id=${user.f99_90_id}&page_size=500`, {
|
|
||||||
headers: { 'Authorization': 'Bearer ' + token }
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
const list = data.data || data || []
|
|
||||||
setMyDeals(Array.isArray(list) ? list : [])
|
|
||||||
} catch (e) {
|
|
||||||
console.error('获取行情失败:', e)
|
|
||||||
}
|
|
||||||
setDealsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换tab时加载数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeTab === 'deals') {
|
|
||||||
fetchMyDeals()
|
|
||||||
}
|
|
||||||
}, [activeTab])
|
|
||||||
|
|
||||||
// 行情过滤和排序
|
|
||||||
const filteredDeals = myDeals.filter(deal => {
|
|
||||||
if (!dealSearch) return true
|
|
||||||
const s = dealSearch.toLowerCase().trim()
|
|
||||||
const fields = [deal.title, deal.content, deal.category, deal.packaging, deal.grading_company, deal.grading_score, deal.deal_no, deal.seller, deal.platform, deal.buyer, deal.deal_price?.toString(), deal.deal_date].filter(v => v).map(v => v.toString().toLowerCase())
|
|
||||||
return fields.some(f => f.includes(s))
|
|
||||||
}).sort((a, b) => {
|
|
||||||
let aVal = a[dealSortField] || ''
|
|
||||||
let bVal = b[dealSortField] || ''
|
|
||||||
if (dealSortField === 'created_at') {
|
|
||||||
aVal = new Date(a.created_at).getTime()
|
|
||||||
bVal = new Date(b.created_at).getTime()
|
|
||||||
} else if (dealSortField === 'deal_price') {
|
|
||||||
aVal = a.deal_price || 0
|
|
||||||
bVal = b.deal_price || 0
|
|
||||||
}
|
|
||||||
if (dealSortOrder === 'asc') return aVal > bVal ? 1 : -1
|
|
||||||
return aVal < bVal ? 1 : -1
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchCollections = async () => {
|
const fetchCollections = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
|
|
@ -365,7 +308,7 @@ export default function List() {
|
||||||
bVal = rarityOrder[bVal] || 0
|
bVal = rarityOrder[bVal] || 0
|
||||||
} else if (sortField === 'numberCategory') {
|
} else if (sortField === 'numberCategory') {
|
||||||
// 号码分类按自定义顺序排序
|
// 号码分类按自定义顺序排序
|
||||||
const numberCategoryOrder = { '无2347': 1, '无347': 2, '无247': 3, '无47': 4, '带7号': 5, '带4号': 6, '其他': 7 }
|
const numberCategoryOrder = { '无2347': 1, '无347': 2, '无247': 3, '无47': 4, '无4': 5, '带4': 6, '其他': 7 }
|
||||||
aVal = numberCategoryOrder[aVal] || 99
|
aVal = numberCategoryOrder[aVal] || 99
|
||||||
bVal = numberCategoryOrder[bVal] || 99
|
bVal = numberCategoryOrder[bVal] || 99
|
||||||
}
|
}
|
||||||
|
|
@ -408,7 +351,7 @@ export default function List() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNumberCategoryColor = (cat) => {
|
const getNumberCategoryColor = (cat) => {
|
||||||
const colors = { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '带7号': '#06b6d4', '带4号': '#22c55e', '其他': '#64748b' };
|
const colors = { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '无4': '#06b6d4', '带4': '#22c55e', '其他': '#64748b' };
|
||||||
return colors[cat] || '#64748b';
|
return colors[cat] || '#64748b';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -512,18 +455,7 @@ export default function List() {
|
||||||
<div style={{ minHeight: '100vh', overflowY: 'auto', background: '#0f172a', paddingBottom: '70px' }}>
|
<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={{ 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={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '16px' }}>
|
||||||
<div style={{ color: '#fff', fontSize: '20px' }}>
|
<div style={{ color: '#fff', fontSize: '20px', }}>我的藏品 <span style={{ fontSize: '14px', color: '#fbbf24' }}>({filteredCollections.length})</span></div>
|
||||||
<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' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||||
{filter && filterType && (
|
{filter && filterType && (
|
||||||
<button
|
<button
|
||||||
|
|
@ -550,8 +482,6 @@ export default function List() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'collections' && (
|
|
||||||
<>
|
|
||||||
{/* 搜索框 - 全字段搜索 */}
|
{/* 搜索框 - 全字段搜索 */}
|
||||||
<div style={{ position: 'relative', marginBottom: '16px' }}>
|
<div style={{ position: 'relative', marginBottom: '16px' }}>
|
||||||
<input type="text"
|
<input type="text"
|
||||||
|
|
@ -627,11 +557,9 @@ export default function List() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 分页组件 - 仅在藏品tab下显示 */}
|
{/* 分页组件 */}
|
||||||
{activeTab === 'collections' && pagination.pages > 1 && (
|
{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' }}>
|
<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>
|
<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>
|
||||||
|
|
||||||
|
|
@ -648,49 +576,6 @@ export default function List() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ padding: '16px' }}>
|
<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 ? (
|
{loading ? (
|
||||||
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>加载中...</div>
|
<div style={{ textAlign: 'center', padding: '60px', color: '#64748b' }}>加载中...</div>
|
||||||
) : filteredCollections.length === 0 ? (
|
) : filteredCollections.length === 0 ? (
|
||||||
|
|
@ -712,237 +597,7 @@ export default function List() {
|
||||||
) : (
|
) : (
|
||||||
filteredCollections.map(item => <ListItem key={item.id} item={item} />)
|
filteredCollections.map(item => <ListItem key={item.id} item={item} />)
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</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/information/${deal.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(editForm)
|
|
||||||
})
|
|
||||||
setEditing(false)
|
|
||||||
onRefresh()
|
|
||||||
} catch(e) { alert('保存失败') }
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteDeal = async () => {
|
|
||||||
if (!confirm('确定删除这条行情?')) return
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
try {
|
|
||||||
await fetch(`${API_BASE}/api/information/${deal.id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
|
||||||
})
|
|
||||||
onRefresh()
|
|
||||||
} catch(e) { alert('删除失败') }
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -32,12 +32,6 @@ export default function News() {
|
||||||
const [comments, setComments] = useState({}) // 存储各寻号的评论
|
const [comments, setComments] = useState({}) // 存储各寻号的评论
|
||||||
const [matchedStatus, setMatchedStatus] = useState({}) // 存储各寻号的匹配状态
|
const [matchedStatus, setMatchedStatus] = useState({}) // 存储各寻号的匹配状态
|
||||||
const [showMatchList, setShowMatchList] = useState(false)
|
const [showMatchList, setShowMatchList] = useState(false)
|
||||||
const [dealVersion, setDealVersion] = useState('龙钞')
|
|
||||||
const [dealDate, setDealDate] = useState(() => {
|
|
||||||
const today = new Date()
|
|
||||||
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
|
|
||||||
})
|
|
||||||
const [dealDetailItems, setDealDetailItems] = useState(null) // 弹窗显示的详情列表
|
|
||||||
const [matchCollections, setMatchCollections] = useState([])
|
const [matchCollections, setMatchCollections] = useState([])
|
||||||
const [networkMatchCollections, setNetworkMatchCollections] = useState([])
|
const [networkMatchCollections, setNetworkMatchCollections] = useState([])
|
||||||
const [showNetworkMatchList, setShowNetworkMatchList] = useState(false)
|
const [showNetworkMatchList, setShowNetworkMatchList] = useState(false)
|
||||||
|
|
@ -66,7 +60,7 @@ export default function News() {
|
||||||
// 获取资讯列表
|
// 获取资讯列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInfoList()
|
fetchInfoList()
|
||||||
}, [activeTab, dealDate])
|
}, [activeTab])
|
||||||
|
|
||||||
// 自动生成寻号标题
|
// 自动生成寻号标题
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -77,21 +71,13 @@ export default function News() {
|
||||||
const fetchInfoList = async () => {
|
const fetchInfoList = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
|
// 寻配号对所有人公开,无需登录即可查看
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
|
// 根据tab获取不同类型的数据
|
||||||
const type = activeTab === 'seek' ? 'seek' : activeTab === 'deal' ? 'deal' : 'yichen'
|
const type = activeTab === 'seek' ? 'seek' : activeTab === 'deal' ? 'deal' : 'yichen'
|
||||||
|
// 始终传递token,以便获取准确的matched_count
|
||||||
const headers = token ? { Authorization: `Bearer ${token}` } : {}
|
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参数
|
|
||||||
let url = `${API_BASE}/api/information/list?info_type=${type}`
|
|
||||||
|
|
||||||
// 如果是成交行情tab,获取当天所有数据(不分页)
|
|
||||||
if (activeTab === 'deal' && dealDate) {
|
|
||||||
url += `&deal_date=${dealDate}&page_size=500`
|
|
||||||
} else {
|
|
||||||
url += `&page=${currentPage}&page_size=50`
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(url, { headers })
|
|
||||||
// 从响应头获取总页数
|
// 从响应头获取总页数
|
||||||
const total = res.headers.get('X-Total-Pages') || res.headers.get('x-total-pages')
|
const total = res.headers.get('X-Total-Pages') || res.headers.get('x-total-pages')
|
||||||
if (total) setTotalPages(parseInt(total))
|
if (total) setTotalPages(parseInt(total))
|
||||||
|
|
@ -104,29 +90,7 @@ export default function News() {
|
||||||
}
|
}
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
console.log('资讯列表:', data)
|
console.log('资讯列表:', data)
|
||||||
|
setInfoList(data || [])
|
||||||
// 成交行情按分类排序
|
|
||||||
let sortedData = data || []
|
|
||||||
if (activeTab === 'deal') {
|
|
||||||
const categoryOrder = ['带4号', '带7号', '永恒号', '天马号', '天马王', '金山号', '金山王', '钻石号', '朦胧号', '朦胧王', '金马号', '金马王', '倒置号', '圆圆号']
|
|
||||||
// 标准化分类名称(通货->带4号,无4->带7号)
|
|
||||||
const normalizeCat = (c) => {
|
|
||||||
if (c === '通货') return '带4号'
|
|
||||||
if (c === '无4') return '带7号'
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
sortedData = [...(data || [])].sort((a, b) => {
|
|
||||||
const contentA = a.content || ''
|
|
||||||
const contentB = b.content || ''
|
|
||||||
const catA = normalizeCat(contentA.match(/分类:\s*(.+?)(?:\n|$)/)?.[1]?.trim() || a.category || '')
|
|
||||||
const catB = normalizeCat(contentB.match(/分类:\s*(.+?)(?:\n|$)/)?.[1]?.trim() || b.category || '')
|
|
||||||
const idxA = categoryOrder.indexOf(catA)
|
|
||||||
const idxB = categoryOrder.indexOf(catB)
|
|
||||||
return (idxA === -1 ? 999 : idxA) - (idxB === -1 ? 999 : idxB)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setInfoList(sortedData)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
setInfoList([])
|
setInfoList([])
|
||||||
|
|
@ -373,9 +337,9 @@ export default function News() {
|
||||||
|
|
||||||
// Tab切换
|
// Tab切换
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'seek', label: '寻配号' },
|
{ key: 'seek', label: '🔍 寻配号' },
|
||||||
{ key: 'deal', label: '成交行情' },
|
{ key: 'deal', label: '💰 成交行情' },
|
||||||
{ key: 'yichen', label: '一尘看板' }
|
{ key: 'yichen', label: '📊 一尘看板' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
|
|
@ -495,7 +459,7 @@ export default function News() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h3 style={{ color: '#fff', marginBottom: '16px' }}>
|
<h3 style={{ color: '#fff', marginBottom: '16px' }}>
|
||||||
{activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? `成交行情信息 (${dealDate})` : '一尘看板'}
|
{activeTab === 'seek' ? '🔍 寻配号信息' : activeTab === 'deal' ? '💰 成交行情信息' : '📊 一尘看板'}
|
||||||
{activeTab === 'seek' && (
|
{activeTab === 'seek' && (
|
||||||
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
|
<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>
|
<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>
|
||||||
|
|
@ -504,12 +468,6 @@ 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>
|
<button onClick={() => setShowSeekPublish(!showSeekPublish)} style={{ padding: '6px 12px', background: showSeekPublish ? '#6b7280' : '#10b981', border: 'none', borderRadius: '6px', color: '#fff', fontSize: '12px', cursor: 'pointer' }}>发布寻号</button>
|
||||||
</div>
|
</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>
|
</h3>
|
||||||
|
|
||||||
{/* 一尘看板独立渲染 */}
|
{/* 一尘看板独立渲染 */}
|
||||||
|
|
@ -523,220 +481,7 @@ export default function News() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
{/* 成交行情 - 价格统计表格 */}
|
|
||||||
{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 => {
|
{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={{ background: '#1e293b', borderRadius: '12px', padding: '12px', marginBottom: '10px' }}>
|
|
||||||
{/* 第一行:冠字号 | 版别 | 包装 | 分数 + 价格 */}
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
||||||
<span style={{ color: '#fbbf24', fontSize: '15px', fontWeight: '600' }}>{serial}</span>
|
|
||||||
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
|
|
||||||
<span style={{ background: '#3b82f6', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{version}</span>
|
|
||||||
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
|
|
||||||
<span style={{ background: '#8b5cf6', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{packaging}</span>
|
|
||||||
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
|
|
||||||
<span style={{ background: '#06b6d4', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{grade}</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ color: '#22c55e', fontSize: '16px', fontWeight: '700' }}>¥{item.deal_price?.toLocaleString()}</div>
|
|
||||||
</div>
|
|
||||||
{/* 第二行:编码 | 分类 | 评级机构 | 平台 | 日期 */}
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center' }}>
|
|
||||||
<span style={{ color: '#64748b', fontSize: '11px' }}>{item.deal_no}</span>
|
|
||||||
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
|
|
||||||
<span style={{ background: '#f97316', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{category}</span>
|
|
||||||
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
|
|
||||||
{gradingCompany && <span style={{ background: '#ec4899', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{gradingCompany}</span>}
|
|
||||||
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
|
|
||||||
<span style={{ background: '#10b981', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{platform}</span>
|
|
||||||
<span style={{ color: '#94a3b8', fontSize: '12px' }}>|</span>
|
|
||||||
<span style={{ background: '#64748b', color: '#fff', padding: '2px 8px', borderRadius: '4px', fontSize: '11px' }}>{dealDate}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析正文中的号码特征和联系方式
|
// 解析正文中的号码特征和联系方式
|
||||||
const content = item.content || ''
|
const content = item.content || ''
|
||||||
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
|
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export default function Stats() {
|
||||||
status: { 'in_collection': '#22c55e', 'selling': '#f59e0b', 'sold': '#ef4444', 'grading': '#8b5cf6', 'repairing': '#f97316', 'transit': '#06b6d4', 'seeking': '#ec4899' },
|
status: { 'in_collection': '#22c55e', 'selling': '#f59e0b', 'sold': '#ef4444', 'grading': '#8b5cf6', 'repairing': '#f97316', 'transit': '#06b6d4', 'seeking': '#ec4899' },
|
||||||
category: { '自持': '#22c55e', '寄存': '#3b82f6', '寄售': '#f59e0b', '共有': '#8b5cf6', '寻号': '#06b6d4', '生肖钞': '#22c55e', '其他': '#64748b' },
|
category: { '自持': '#22c55e', '寄存': '#3b82f6', '寄售': '#f59e0b', '共有': '#8b5cf6', '寻号': '#06b6d4', '生肖钞': '#22c55e', '其他': '#64748b' },
|
||||||
profitLoss: { 'profit': '#22c55e', 'loss': '#ef4444' },
|
profitLoss: { 'profit': '#22c55e', 'loss': '#ef4444' },
|
||||||
numberCategory: { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '带7号': '#06b6d4', '带4号': '#22c55e', '其他': '#64748b' },
|
numberCategory: { '无2347': '#8b5cf6', '无347': '#ef4444', '无247': '#fbbf24', '无47': '#3b82f6', '无4': '#06b6d4', '带4': '#22c55e', '其他': '#64748b' },
|
||||||
version: {},
|
version: {},
|
||||||
gradingCompany: {},
|
gradingCompany: {},
|
||||||
gradingScore: {},
|
gradingScore: {},
|
||||||
|
|
@ -146,7 +146,7 @@ export default function Stats() {
|
||||||
return Number(val).toLocaleString('zh-CN')
|
return Number(val).toLocaleString('zh-CN')
|
||||||
}
|
}
|
||||||
|
|
||||||
const numberCategoryOrder = ['无2347', '无347', '无247', '无47', '带7号', '带4号', '其他']
|
const numberCategoryOrder = ['无2347', '无347', '无247', '无47', '无4', '带4', '其他']
|
||||||
const getSortedData = (data, type) => {
|
const getSortedData = (data, type) => {
|
||||||
if (type === 'numberCategory') {
|
if (type === 'numberCategory') {
|
||||||
return [...data].sort((a, b) => {
|
return [...data].sort((a, b) => {
|
||||||
|
|
|
||||||
|
|
@ -53,28 +53,8 @@ export default function YichensBoard() {
|
||||||
const res = await fetch(url)
|
const res = await fetch(url)
|
||||||
if (!res.ok) throw new Error('posts API error: ' + res.status)
|
if (!res.ok) throw new Error('posts API error: ' + res.status)
|
||||||
let data = await res.json() || []
|
let data = await res.json() || []
|
||||||
// 确保data是数组
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
data = data.posts || data.data || []
|
|
||||||
}
|
|
||||||
console.log('YichensBoard: posts data count', data.length)
|
console.log('YichensBoard: posts data count', data.length)
|
||||||
if (currentCat && Array.isArray(data)) {
|
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('马'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 确保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 === '龙') {
|
if (currentCat === '龙') {
|
||||||
data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
|
data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞')))
|
||||||
} else if (currentCat === '蛇') {
|
} else if (currentCat === '蛇') {
|
||||||
|
|
@ -86,7 +66,7 @@ export default function YichensBoard() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 支持新格式 {posts:[], total:xxx} 或旧格式 [{},{}]
|
// 支持新格式 {posts:[], total:xxx} 或旧格式 [{},{}]
|
||||||
const postsArray = Array.isArray(data) ? data : (data.posts || [])
|
const postsArray = data.posts || data
|
||||||
const totalCount = data.total || todayStats.total || postsArray.length
|
const totalCount = data.total || todayStats.total || postsArray.length
|
||||||
setPosts(postsArray)
|
setPosts(postsArray)
|
||||||
setTotalPosts(totalCount)
|
setTotalPosts(totalCount)
|
||||||
|
|
@ -94,27 +74,26 @@ export default function YichensBoard() {
|
||||||
console.error('YichensBoard: fetchPosts error', e)
|
console.error('YichensBoard: fetchPosts error', e)
|
||||||
setError(e.message)
|
setError(e.message)
|
||||||
setPosts([])
|
setPosts([])
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (todayStats.total > 0) {
|
if (todayStats.total > 0) {
|
||||||
fetchPosts(1, categoryFilter, searchKeyword)
|
// fetchTodayCategory()
|
||||||
|
fetchPosts(1, '')
|
||||||
}
|
}
|
||||||
}, [todayStats, categoryFilter, postTypeFilter])
|
}, [todayStats])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(1)
|
setPage(1)
|
||||||
fetchPosts(1, categoryFilter, searchKeyword)
|
fetchPosts(1, '')
|
||||||
}, [searchKeyword])
|
}, [searchKeyword])
|
||||||
|
|
||||||
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
|
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
|
||||||
|
|
||||||
const StatCard = ({ label, value, color, onClick }) => (
|
const StatCard = ({ label, value, color, onClick }) => (
|
||||||
<div onClick={() => { setLoading(true); if (onClick) onClick(); }}
|
<div onClick={onClick} style={{
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)',
|
background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)',
|
||||||
borderRadius: 12, padding: '12px 8px', border: '1px solid #374151',
|
borderRadius: 12, padding: '12px 8px', border: '1px solid #374151',
|
||||||
cursor: onClick ? 'pointer' : 'default',
|
cursor: onClick ? 'pointer' : 'default',
|
||||||
|
|
@ -143,10 +122,10 @@ export default function YichensBoard() {
|
||||||
📈 连体钞/纪念钞 <span style={{ color: '#9ca3af', fontWeight: 400 }}>{dateStr}</span>
|
📈 连体钞/纪念钞 <span style={{ color: '#9ca3af', fontWeight: 400 }}>{dateStr}</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
|
||||||
<StatCard label='全部' value={todayStats.total} color='#3b82f6' onClick={() => { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); }} />
|
<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); }} />
|
<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); }} />
|
<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); }} />
|
<StatCard label='其他' value={todayStats.others || 0} color='#8b5cf6' onClick={() => { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -175,7 +154,7 @@ export default function YichensBoard() {
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||||
{[{key:'all',label:'全部'},{key:'deal',label:'出售'},{key:'want',label:'求购'},{key:'other',label:'其他'}].map(k => (
|
{[{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); }}
|
<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',
|
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 }}>
|
background: postTypeFilter===k.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||||
{k.label}
|
{k.label}
|
||||||
|
|
@ -185,7 +164,7 @@ export default function YichensBoard() {
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||||
{[{key:'',label:'全部'},{key:'龙',label:'龙钞'},{key:'蛇',label:'蛇钞'},{key:'马',label:'马钞'},{key:'其他',label:'其他'}].map(k => (
|
{[{key:'',label:'全部'},{key:'龙',label:'龙钞'},{key:'蛇',label:'蛇钞'},{key:'马',label:'马钞'},{key:'其他',label:'其他'}].map(k => (
|
||||||
<button key={k.key} onClick={() => { setCategoryFilter(k.key); setPage(1); }}
|
<button key={k.key} onClick={() => { setCategoryFilter(k.key); setPage(1); fetchPosts(1, k.key) }}
|
||||||
style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
|
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 }}>
|
background: categoryFilter===k.key ? 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||||
{k.label}
|
{k.label}
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
/**
|
|
||||||
* 号码分类工具
|
|
||||||
* 根据冠字号自动分类为:圆圆号、倒置号、金马王、金马号、金山王、天马王、金山号、天马号、朦胧号、如意号、钻石号、永恒号、带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 }))
|
|
||||||
Loading…
Reference in New Issue