v1.2.14: 新增资讯功能页面(寻配号/成交数据/发布中心)

This commit is contained in:
甲辰生产 2026-03-25 10:58:00 +08:00
parent fadb0e5b4f
commit 0e7b5cbfdf
9 changed files with 1266 additions and 6 deletions

View File

@ -1 +1 @@
1.2.13 VERSION=1.2.14

View File

@ -15,6 +15,7 @@ from app.middleware.logging import logging_middleware
from app.routers import auth, collections, operations from app.routers import auth, collections, operations
from app.routers import ocr as ocr_router from app.routers import ocr as ocr_router
from app.routers import users as users_router from app.routers import users as users_router
from app.routers import information as information_router
# 版本信息 - 从 config/VERSION 文件读取 # 版本信息 - 从 config/VERSION 文件读取
def get_version(): def get_version():
@ -79,6 +80,7 @@ app.include_router(operations.router)
app.include_router(ocr_router.router) # OCR 识别 app.include_router(ocr_router.router) # OCR 识别
app.include_router(users_router.router) # 当前用户接口 app.include_router(users_router.router) # 当前用户接口
app.include_router(users_router.admin_router) # 管理员用户管理 app.include_router(users_router.admin_router) # 管理员用户管理
app.include_router(information_router.router) # 资讯
@app.get("/") @app.get("/")

View File

@ -1,5 +1,5 @@
# 数据库模型 - 使用字段编码 # 数据库模型 - 使用字段编码
from sqlalchemy import Column, String, Float, Boolean, DateTime, Integer, Text, ForeignKey, Date from sqlalchemy import Column, String, Float, Boolean, DateTime, Integer, Text, ForeignKey, Date, UniqueConstraint
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy.sql import func from sqlalchemy.sql import func
from app.core.database import Base from app.core.database import Base
@ -146,3 +146,62 @@ class CustomField(Base):
created_at = Column(DateTime(timezone=True), server_default=func.now()) created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="custom_fields") user = relationship("User", back_populates="custom_fields")
# 资讯模型
class Information(Base):
__tablename__ = "information"
id = Column(String(36), primary_key=True, default=generate_uuid)
user_id = Column(String(36), ForeignKey("users.f99_90_id", ondelete="CASCADE"), nullable=False, index=True)
# 信息类型: seek-寻配号, deal-成交数据, publish-发布
info_type = Column(String(20), nullable=False, index=True)
# 标题
title = Column(String(255), nullable=False)
# 内容描述
content = Column(Text, nullable=True)
# 关联藏品ID
collection_id = Column(String(36), ForeignKey("collections.f99_90_id", ondelete="SET NULL"), nullable=True)
# 期望条件 (寻配号用)
expect_category = Column(String(100), nullable=True) # 期望类别
expect_version = Column(String(100), nullable=True) # 期望版别
expect_packaging = Column(String(100), nullable=True) # 期望包装
expect_number = Column(String(50), nullable=True) # 期望号码
expect_price_min = Column(Float, nullable=True) # 期望价格区间
expect_price_max = Column(Float, nullable=True)
# 成交价格 (成交数据用)
deal_price = Column(Float, nullable=True)
deal_date = Column(Date, nullable=True)
# 状态: active-有效, closed-已关闭, expired-已过期
status = Column(String(20), default="active", index=True)
# 浏览/联系次数
view_count = Column(Integer, default=0)
contact_count = Column(Integer, default=0)
created_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
user = relationship("User")
collection = relationship("Collection")
# 资讯关联用户 (收藏/点赞)
class InformationLike(Base):
__tablename__ = "information_likes"
id = Column(String(36), primary_key=True, default=generate_uuid)
information_id = Column(String(36), ForeignKey("information.id", ondelete="CASCADE"), nullable=False, index=True)
user_id = Column(String(36), ForeignKey("users.f99_90_id", ondelete="CASCADE"), nullable=False, index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (
UniqueConstraint('information_id', 'user_id', name='uq_information_user'),
)

View File

@ -0,0 +1,496 @@
# 资讯API路由
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session, joinedload
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime, date
from app.core.database import get_db
from app.core.auth import get_current_user
from app.models.models import User, Information, Collection
router = APIRouter(prefix="/api/information", tags=["资讯"])
# Schema
class InformationCreate(BaseModel):
info_type: str # seek-寻配号, deal-成交数据, publish-发布
title: str
content: Optional[str] = None
collection_id: Optional[str] = None
expect_category: Optional[str] = None
expect_version: Optional[str] = None
expect_packaging: Optional[str] = None
expect_number: Optional[str] = None
expect_price_min: Optional[float] = None
expect_price_max: Optional[float] = None
deal_price: Optional[float] = None
deal_date: Optional[date] = None
class InformationUpdate(BaseModel):
title: Optional[str] = None
content: Optional[str] = None
status: Optional[str] = None
expect_category: Optional[str] = None
expect_version: Optional[str] = None
expect_packaging: Optional[str] = None
expect_number: Optional[str] = None
expect_price_min: Optional[float] = None
expect_price_max: Optional[float] = None
deal_price: Optional[float] = None
deal_date: Optional[date] = None
class InformationResponse(BaseModel):
id: str
user_id: str
info_type: str
title: str
content: Optional[str]
collection_id: Optional[str]
expect_category: Optional[str]
expect_version: Optional[str]
expect_packaging: Optional[str]
expect_number: Optional[str]
expect_price_min: Optional[float]
expect_price_max: Optional[float]
deal_price: Optional[float]
deal_date: Optional[date]
status: str
view_count: int
contact_count: int
created_at: datetime
# 用户信息
user_name: Optional[str] = None
user_avatar: Optional[str] = None
# 关联藏品信息
collection_name: Optional[str] = None
collection_category: Optional[str] = None
collection_version: Optional[str] = None
collection_number: Optional[str] = None
class Config:
from_attributes = True
# 资讯列表
@router.get("/list", response_model=List[InformationResponse])
def get_information_list(
info_type: Optional[str] = Query(None, description="类型: seek/deal/publish"),
status: str = Query("active", description="状态: active/closed/expired"),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db)
):
"""获取资讯列表"""
query = db.query(Information).options(
joinedload(Information.user),
joinedload(Information.collection)
).filter(Information.status == status)
if info_type:
query = query.filter(Information.info_type == info_type)
# 按创建时间倒序
query = query.order_by(Information.created_at.desc())
# 分页
offset = (page - 1) * page_size
items = query.offset(offset).limit(page_size).all()
# 转换结果
result = []
for item in items:
result.append(InformationResponse(
id=item.id,
user_id=item.user_id,
info_type=item.info_type,
title=item.title,
content=item.content,
collection_id=item.collection_id,
expect_category=item.expect_category,
expect_version=item.expect_version,
expect_packaging=item.expect_packaging,
expect_number=item.expect_number,
expect_price_min=item.expect_price_min,
expect_price_max=item.expect_price_max,
deal_price=item.deal_price,
deal_date=item.deal_date,
status=item.status,
view_count=item.view_count,
contact_count=item.contact_count,
created_at=item.created_at,
user_name=item.user.f01_01_name if item.user else None,
user_avatar=item.user.avatar if item.user else None,
collection_name=item.collection.f01_01_name if item.collection else None,
collection_category=item.collection.f01_03_category if item.collection else None,
collection_version=item.collection.f02_11_version if item.collection else None,
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
))
return result
# 获取单条资讯
@router.get("/{info_id}", response_model=InformationResponse)
def get_information(
info_id: str,
db: Session = Depends(get_db)
):
"""获取资讯详情"""
item = db.query(Information).options(
joinedload(Information.user),
joinedload(Information.collection)
).filter(Information.id == info_id).first()
if not item:
raise HTTPException(status_code=404, detail="资讯不存在")
# 增加浏览次数
item.view_count += 1
db.commit()
return InformationResponse(
id=item.id,
user_id=item.user_id,
info_type=item.info_type,
title=item.title,
content=item.content,
collection_id=item.collection_id,
expect_category=item.expect_category,
expect_version=item.expect_version,
expect_packaging=item.expect_packaging,
expect_number=item.expect_number,
expect_price_min=item.expect_price_min,
expect_price_max=item.expect_price_max,
deal_price=item.deal_price,
deal_date=item.deal_date,
status=item.status,
view_count=item.view_count,
contact_count=item.contact_count,
created_at=item.created_at,
user_name=item.user.f01_01_name if item.user else None,
user_avatar=item.user.avatar if item.user else None,
collection_name=item.collection.f01_01_name if item.collection else None,
collection_category=item.collection.f01_03_category if item.collection else None,
collection_version=item.collection.f02_11_version if item.collection else None,
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
)
# 发布资讯
@router.post("/", response_model=InformationResponse)
def create_information(
data: InformationCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""发布资讯"""
info = Information(
user_id=current_user.f99_90_id,
info_type=data.info_type,
title=data.title,
content=data.content,
collection_id=data.collection_id,
expect_category=data.expect_category,
expect_version=data.expect_version,
expect_packaging=data.expect_packaging,
expect_number=data.expect_number,
expect_price_min=data.expect_price_min,
expect_price_max=data.expect_price_max,
deal_price=data.deal_price,
deal_date=data.deal_date,
status="active"
)
db.add(info)
db.commit()
db.refresh(info)
return InformationResponse(
id=info.id,
user_id=info.user_id,
info_type=info.info_type,
title=info.title,
content=info.content,
collection_id=info.collection_id,
expect_category=info.expect_category,
expect_version=info.expect_version,
expect_packaging=info.expect_packaging,
expect_number=info.expect_number,
expect_price_min=info.expect_price_min,
expect_price_max=info.expect_price_max,
deal_price=info.deal_price,
deal_date=info.deal_date,
status=info.status,
view_count=info.view_count,
contact_count=info.contact_count,
created_at=info.created_at,
user_name=current_user.f01_01_name,
user_avatar=current_user.avatar,
collection_name=None,
collection_category=None,
collection_version=None,
collection_number=None,
)
# 更新资讯
@router.put("/{info_id}", response_model=InformationResponse)
def update_information(
info_id: str,
data: InformationUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""更新资讯"""
info = db.query(Information).filter(
Information.id == info_id,
Information.user_id == current_user.f99_90_id
).first()
if not info:
raise HTTPException(status_code=404, detail="资讯不存在或无权修改")
# 更新字段
if data.title is not None:
info.title = data.title
if data.content is not None:
info.content = data.content
if data.status is not None:
info.status = data.status
if data.expect_category is not None:
info.expect_category = data.expect_category
if data.expect_version is not None:
info.expect_version = data.expect_version
if data.expect_packaging is not None:
info.expect_packaging = data.expect_packaging
if data.expect_number is not None:
info.expect_number = data.expect_number
if data.expect_price_min is not None:
info.expect_price_min = data.expect_price_min
if data.expect_price_max is not None:
info.expect_price_max = data.expect_price_max
if data.deal_price is not None:
info.deal_price = data.deal_price
if data.deal_date is not None:
info.deal_date = data.deal_date
db.commit()
db.refresh(info)
return InformationResponse(
id=info.id,
user_id=info.user_id,
info_type=info.info_type,
title=info.title,
content=info.content,
collection_id=info.collection_id,
expect_category=info.expect_category,
expect_version=info.expect_version,
expect_packaging=info.expect_packaging,
expect_number=info.expect_number,
expect_price_min=info.expect_price_min,
expect_price_max=info.expect_price_max,
deal_price=info.deal_price,
deal_date=info.deal_date,
status=info.status,
view_count=info.view_count,
contact_count=info.contact_count,
created_at=info.created_at,
user_name=current_user.f01_01_name,
user_avatar=current_user.avatar,
collection_name=info.collection.f01_01_name if info.collection else None,
collection_category=info.collection.f01_03_category if info.collection else None,
collection_version=info.collection.f02_11_version if info.collection else None,
collection_number=info.collection.f02_10_prefix_serial if info.collection else None,
)
# 删除资讯
@router.delete("/{info_id}")
def delete_information(
info_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""删除资讯"""
info = db.query(Information).filter(
Information.id == info_id,
Information.user_id == current_user.f99_90_id
).first()
if not info:
raise HTTPException(status_code=404, detail="资讯不存在或无权删除")
db.delete(info)
db.commit()
return {"message": "删除成功"}
# 寻配号 - 自动匹配推荐藏品
@router.get("/seek/match")
def get_seek_match(
info_id: str,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取符合条件的我的藏品推荐"""
info = db.query(Information).filter(
Information.id == info_id,
Information.info_type == "seek"
).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 查询当前用户的藏品,匹配条件
query = db.query(Collection).filter(
Collection.f99_91_user_id == current_user.f99_90_id,
Collection.f01_04_status == "in_collection"
)
if info.expect_category:
query = query.filter(Collection.f01_03_category == info.expect_category)
if info.expect_version:
query = query.filter(Collection.f02_11_version == info.expect_version)
if info.expect_packaging:
query = query.filter(Collection.f02_12_packaging == info.expect_packaging)
if info.expect_number:
query = query.filter(Collection.f02_10_prefix_serial.contains(info.expect_number))
if info.expect_price_min:
query = query.filter(Collection.f05_40_cost_price >= info.expect_price_min)
if info.expect_price_max:
query = query.filter(Collection.f05_40_cost_price <= info.expect_price_max)
matched_collections = query.all()
return {
"info_id": info_id,
"matched_count": len(matched_collections),
"collections": [
{
"id": c.f99_90_id,
"name": c.f01_01_name,
"category": c.f01_03_category,
"version": c.f02_11_version,
"packaging": c.f02_12_packaging,
"number": c.f02_10_prefix_serial,
"cost_price": c.f05_40_cost_price,
}
for c in matched_collections
]
}
# 成交数据统计
@router.get("/deal/stats")
def get_deal_stats(
days: int = Query(7, ge=1, le=90, description="统计天数"),
db: Session = Depends(get_db)
):
"""获取成交数据统计"""
from sqlalchemy import func
from datetime import timedelta
start_date = datetime.now() - timedelta(days=days)
# 按版别统计
by_version = db.query(
Information.expect_version,
func.count(Information.id).label("count"),
func.avg(Information.deal_price).label("avg_price"),
func.max(Information.deal_price).label("max_price"),
func.min(Information.deal_price).label("min_price")
).filter(
Information.info_type == "deal",
Information.status == "active",
Information.created_at >= start_date
).group_by(Information.expect_version).all()
# 按包装统计
by_packaging = db.query(
Information.expect_packaging,
func.count(Information.id).label("count"),
func.avg(Information.deal_price).label("avg_price")
).filter(
Information.info_type == "deal",
Information.status == "active",
Information.created_at >= start_date
).group_by(Information.expect_packaging).all()
# 按号码分类统计
by_number = db.query(
Information.expect_number,
func.count(Information.id).label("count"),
func.avg(Information.deal_price).label("avg_price")
).filter(
Information.info_type == "deal",
Information.status == "active",
Information.expect_number.isnot(None),
Information.created_at >= start_date
).group_by(Information.expect_number).all()
return {
"days": days,
"by_version": [
{"version": r[0] or "未知", "count": r[1], "avg_price": float(r[2] or 0), "max_price": float(r[3] or 0), "min_price": float(r[4] or 0)}
for r in by_version if r[0]
],
"by_packaging": [
{"packaging": r[0] or "未知", "count": r[1], "avg_price": float(r[2] or 0)}
for r in by_packaging if r[0]
],
"by_number": [
{"number": r[0], "count": r[1], "avg_price": float(r[2] or 0)}
for r in by_number
]
}
# 获取我的发布列表
@router.get("/my/list", response_model=List[InformationResponse])
def get_my_information_list(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取我的发布列表"""
items = db.query(Information).options(
joinedload(Information.collection)
).filter(
Information.user_id == current_user.f99_90_id
).order_by(Information.created_at.desc()).offset((page-1)*page_size).limit(page_size).all()
result = []
for item in items:
result.append(InformationResponse(
id=item.id,
user_id=item.user_id,
info_type=item.info_type,
title=item.title,
content=item.content,
collection_id=item.collection_id,
expect_category=item.expect_category,
expect_version=item.expect_version,
expect_packaging=item.expect_packaging,
expect_number=item.expect_number,
expect_price_min=item.expect_price_min,
expect_price_max=item.expect_price_max,
deal_price=item.deal_price,
deal_date=item.deal_date,
status=item.status,
view_count=item.view_count,
contact_count=item.contact_count,
created_at=item.created_at,
user_name=current_user.f01_01_name,
user_avatar=current_user.avatar,
collection_name=item.collection.f01_01_name 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_number=item.collection.f02_10_prefix_serial if item.collection else None,
))
return result

View File

@ -1 +1 @@
1.2.13 VERSION=1.2.14

View File

@ -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>甲辰收藏 v1.2.11</title> <title>甲辰收藏 v0.0.0</title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/static/icons/favicon.ico" />

View File

@ -1,12 +1,12 @@
{ {
"name": "jiachenlong-frontend", "name": "jiachenlong-frontend",
"version": "1.0.1", "version": "1.2.12",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "jiachenlong-frontend", "name": "jiachenlong-frontend",
"version": "1.0.1", "version": "1.2.12",
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.7.9",
"react": "^18.3.1", "react": "^18.3.1",

View File

@ -4,6 +4,7 @@ import Settings from './pages/Settings'
import List from './pages/List' 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 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'
@ -28,6 +29,7 @@ export default function App() {
const getComponent = () => { const getComponent = () => {
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 === '/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 />
@ -82,6 +84,7 @@ export default function App() {
}}> }}>
{[ {[
{ path: '/', icon: '🏠', label: '首页' }, { path: '/', 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: '添加' },

700
frontend/src/pages/News.jsx Normal file
View File

@ -0,0 +1,700 @@
import React, { useState, useEffect } from 'react'
// -
export default function News() {
const [activeTab, setActiveTab] = useState('seek') // seek-, deal-, publish-
const [infoList, setInfoList] = useState([])
const [loading, setLoading] = useState(false)
const [showPublish, setShowPublish] = useState(false)
const [myList, setMyList] = useState([])
//
const [formData, setFormData] = useState({
info_type: 'seek',
title: '',
content: '',
collection_id: '',
expect_category: '',
expect_version: '',
expect_packaging: '',
expect_number: '',
expect_price_min: '',
expect_price_max: '',
deal_price: '',
deal_date: ''
})
//
const [myCollections, setMyCollections] = useState([])
const API_BASE = localStorage.getItem('API_BASE') || ''
useEffect(() => {
fetchInfoList()
fetchMyCollections()
fetchMyList()
}, [activeTab])
//
const fetchInfoList = async () => {
setLoading(true)
try {
const token = localStorage.getItem('token')
const res = await fetch(`${API_BASE}/api/information/list?info_type=${activeTab}`, {
headers: { 'Authorization': `Bearer ${token}` }
})
const data = await res.json()
setInfoList(data || [])
} catch (e) {
console.error(e)
}
setLoading(false)
}
//
const fetchMyCollections = async () => {
try {
const token = localStorage.getItem('token')
const res = await fetch(`${API_BASE}/api/collections?page=1&page_size=100`, {
headers: { 'Authorization': `Bearer ${token}` }
})
const data = await res.json()
setMyCollections(data.items || [])
} catch (e) {
console.error(e)
}
}
//
const fetchMyList = async () => {
try {
const token = localStorage.getItem('token')
const res = await fetch(`${API_BASE}/api/information/my/list`, {
headers: { 'Authorization': `Bearer ${token}` }
})
const data = await res.json()
setMyList(data || [])
} catch (e) {
console.error(e)
}
}
//
const handlePublish = async () => {
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({
...formData,
expect_price_min: formData.expect_price_min ? parseFloat(formData.expect_price_min) : null,
expect_price_max: formData.expect_price_max ? parseFloat(formData.expect_price_max) : null,
deal_price: formData.deal_price ? parseFloat(formData.deal_price) : null,
})
})
if (res.ok) {
alert('发布成功!')
setShowPublish(false)
setFormData({
info_type: 'seek',
title: '',
content: '',
collection_id: '',
expect_category: '',
expect_version: '',
expect_packaging: '',
expect_number: '',
expect_price_min: '',
expect_price_max: '',
deal_price: '',
deal_date: ''
})
fetchInfoList()
fetchMyList()
}
} catch (e) {
alert('发布失败')
}
}
//
const handleDelete = async (id) => {
if (!confirm('确定删除?')) return
try {
const token = localStorage.getItem('token')
await fetch(`${API_BASE}/api/information/${id}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
})
fetchMyList()
} catch (e) {
console.error(e)
}
}
//
const formatDate = (dateStr) => {
if (!dateStr) return ''
return new Date(dateStr).toLocaleDateString('zh-CN')
}
//
const typeMap = {
seek: '寻配号',
deal: '成交数据',
publish: '发布'
}
return (
<div style={{ padding: '16px', paddingBottom: '80px', minHeight: '100vh', background: '#0f172a' }}>
{/* 顶部标题 */}
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<h1 style={{ fontSize: '20px', color: '#fbbf24', margin: 0 }}>📰 资讯中心</h1>
</div>
{/* 二级标签切换 */}
<div style={{
display: 'flex',
background: '#1e293b',
borderRadius: '8px',
padding: '4px',
marginBottom: '16px'
}}>
{[
{ key: 'seek', label: '🔍 寻配号' },
{ key: 'deal', label: '📈 成交数据' },
{ key: 'publish', label: ' 发布' }
].map(tab => (
<div
key={tab.key}
onClick={() => setActiveTab(tab.key)}
style={{
flex: 1,
textAlign: 'center',
padding: '10px',
borderRadius: '6px',
cursor: 'pointer',
background: activeTab === tab.key ? '#3b82f6' : 'transparent',
color: activeTab === tab.key ? '#fff' : '#94a3b8',
fontSize: '14px',
fontWeight: activeTab === tab.key ? 'bold' : 'normal'
}}
>
{tab.label}
</div>
))}
</div>
{/* 发布按钮 */}
{activeTab !== 'publish' && (
<div style={{ marginBottom: '16px', textAlign: 'right' }}>
<button
onClick={() => setShowPublish(!showPublish)}
style={{
background: '#3b82f6',
color: '#fff',
border: 'none',
padding: '8px 16px',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px'
}}
>
{showPublish ? '取消发布' : ' 发布'}
</button>
</div>
)}
{/* 发布表单 */}
{showPublish && (
<div style={{
background: '#1e293b',
borderRadius: '12px',
padding: '16px',
marginBottom: '16px'
}}>
<h3 style={{ color: '#fbbf24', marginTop: 0 }}>发布信息</h3>
{/* 选择类型 */}
<div style={{ marginBottom: '12px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>类型</label>
<select
value={formData.info_type}
onChange={(e) => setFormData({...formData, info_type: e.target.value})}
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px'
}}
>
<option value="seek">🔍 寻配号</option>
<option value="deal">📈 成交数据</option>
<option value="publish">📝 普通发布</option>
</select>
</div>
{/* 标题 */}
<div style={{ marginBottom: '12px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>标题 *</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
placeholder="请输入标题"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
{/* 内容 */}
<div style={{ marginBottom: '12px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>详细描述</label>
<textarea
value={formData.content}
onChange={(e) => setFormData({...formData, content: e.target.value})}
placeholder="请输入详细描述..."
rows={3}
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box',
resize: 'vertical'
}}
/>
</div>
{/* 关联藏品 */}
{formData.info_type === 'seek' && (
<div style={{ marginBottom: '12px' }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>关联我的藏品可选</label>
<select
value={formData.collection_id}
onChange={(e) => setFormData({...formData, collection_id: e.target.value})}
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px'
}}
>
<option value="">不关联</option>
{myCollections.map(c => (
<option key={c.f99_90_id} value={c.f99_90_id}>
{c.f01_01_name} - {c.f01_03_category}
</option>
))}
</select>
</div>
)}
{/* 寻配号条件 */}
{formData.info_type === 'seek' && (
<>
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
<div style={{ flex: 1 }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>期望类别</label>
<input
type="text"
value={formData.expect_category}
onChange={(e) => setFormData({...formData, expect_category: e.target.value})}
placeholder="如:纪念钞"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>期望版别</label>
<input
type="text"
value={formData.expect_version}
onChange={(e) => setFormData({...formData, expect_version: e.target.value})}
placeholder="如2024版"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
</div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
<div style={{ flex: 1 }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>期望包装</label>
<input
type="text"
value={formData.expect_packaging}
onChange={(e) => setFormData({...formData, expect_packaging: e.target.value})}
placeholder="如:散钞"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>期望号码</label>
<input
type="text"
value={formData.expect_number}
onChange={(e) => setFormData({...formData, expect_number: e.target.value})}
placeholder="如888"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
</div>
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
<div style={{ flex: 1 }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>最低价格</label>
<input
type="number"
value={formData.expect_price_min}
onChange={(e) => setFormData({...formData, expect_price_min: e.target.value})}
placeholder="0"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>最高价格</label>
<input
type="number"
value={formData.expect_price_max}
onChange={(e) => setFormData({...formData, expect_price_max: e.target.value})}
placeholder="0"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
</div>
</>
)}
{/* 成交数据 */}
{formData.info_type === 'deal' && (
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
<div style={{ flex: 1 }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>成交价格</label>
<input
type="number"
value={formData.deal_price}
onChange={(e) => setFormData({...formData, deal_price: e.target.value})}
placeholder="成交金额"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>版别</label>
<input
type="text"
value={formData.expect_version}
onChange={(e) => setFormData({...formData, expect_version: e.target.value})}
placeholder="如2024版"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
<div style={{ flex: 1 }}>
<label style={{ color: '#94a3b8', fontSize: '12px' }}>包装</label>
<input
type="text"
value={formData.expect_packaging}
onChange={(e) => setFormData({...formData, expect_packaging: e.target.value})}
placeholder="如:标十"
style={{
width: '100%',
padding: '10px',
background: '#0f172a',
border: '1px solid #334155',
borderRadius: '6px',
color: '#fff',
marginTop: '4px',
boxSizing: 'border-box'
}}
/>
</div>
</div>
)}
<button
onClick={handlePublish}
style={{
width: '100%',
padding: '12px',
background: '#3b82f6',
color: '#fff',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '16px',
fontWeight: 'bold'
}}
>
发布
</button>
</div>
)}
{/* 资讯列表 */}
{activeTab === 'publish' ? (
//
<div>
<h3 style={{ color: '#fbbf24', marginBottom: '12px' }}>我的发布</h3>
{loading ? (
<div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>加载中...</div>
) : myList.length === 0 ? (
<div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>暂无发布</div>
) : (
myList.map(item => (
<div key={item.id} style={{
background: '#1e293b',
borderRadius: '12px',
padding: '16px',
marginBottom: '12px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{
background: item.info_type === 'seek' ? '#3b82f6' : item.info_type === 'deal' ? '#10b981' : '#f59e0b',
color: '#fff',
padding: '2px 8px',
borderRadius: '4px',
fontSize: '12px'
}}>
{typeMap[item.info_type]}
</span>
<button
onClick={() => handleDelete(item.id)}
style={{
background: '#ef4444',
color: '#fff',
border: 'none',
padding: '4px 8px',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
>
删除
</button>
</div>
<h4 style={{ color: '#fff', margin: '8px 0' }}>{item.title}</h4>
<p style={{ color: '#94a3b8', fontSize: '13px', margin: 0 }}>{item.content}</p>
<div style={{ color: '#64748b', fontSize: '12px', marginTop: '8px' }}>
{formatDate(item.created_at)} · 浏览 {item.view_count} · 联系 {item.contact_count}
</div>
</div>
))
)}
</div>
) : (
// /
<div>
{loading ? (
<div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>加载中...</div>
) : infoList.length === 0 ? (
<div style={{ textAlign: 'center', color: '#94a3b8', padding: '20px' }}>
暂无{activeTab === 'seek' ? '寻配号' : '成交数据'}信息
</div>
) : (
infoList.map(item => (
<div key={item.id} style={{
background: '#1e293b',
borderRadius: '12px',
padding: '16px',
marginBottom: '12px'
}}>
{/* 用户信息 */}
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '8px' }}>
<div style={{
width: '32px',
height: '32px',
borderRadius: '50%',
background: '#3b82f6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '14px',
marginRight: '8px'
}}>
{item.user_name ? item.user_name[0] : '?'}
</div>
<span style={{ color: '#94a3b8', fontSize: '13px' }}>{item.user_name}</span>
<span style={{ color: '#64748b', fontSize: '12px', marginLeft: 'auto' }}>
{formatDate(item.created_at)}
</span>
</div>
{/* 标题 */}
<h4 style={{ color: '#fff', margin: '0 0 8px 0', fontSize: '16px' }}>{item.title}</h4>
{/* 内容 */}
{item.content && (
<p style={{ color: '#94a3b8', fontSize: '14px', margin: '0 0 8px 0' }}>{item.content}</p>
)}
{/* 寻配号条件 */}
{item.info_type === 'seek' && (item.expect_category || item.expect_version || item.expect_number) && (
<div style={{
background: '#0f172a',
borderRadius: '8px',
padding: '12px',
marginBottom: '8px'
}}>
<div style={{ color: '#fbbf24', fontSize: '12px', marginBottom: '4px' }}>期望条件</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{item.expect_category && (
<span style={{ color: '#94a3b8', fontSize: '12px' }}>类别: {item.expect_category}</span>
)}
{item.expect_version && (
<span style={{ color: '#94a3b8', fontSize: '12px' }}>版别: {item.expect_version}</span>
)}
{item.expect_packaging && (
<span style={{ color: '#94a3b8', fontSize: '12px' }}>包装: {item.expect_packaging}</span>
)}
{item.expect_number && (
<span style={{ color: '#94a3b8', fontSize: '12px' }}>号码: {item.expect_number}</span>
)}
{(item.expect_price_min || item.expect_price_max) && (
<span style={{ color: '#94a3b8', fontSize: '12px' }}>
价格: {item.expect_price_min || '?'} - {item.expect_price_max || '?'}
</span>
)}
</div>
</div>
)}
{/* 成交数据 */}
{item.info_type === 'deal' && (
<div style={{
background: '#0f172a',
borderRadius: '8px',
padding: '12px',
marginBottom: '8px'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#10b981', fontSize: '18px', fontWeight: 'bold' }}>
¥{item.deal_price || '-'}
</span>
<span style={{ color: '#64748b', fontSize: '12px' }}>
{item.expect_version} / {item.expect_packaging}
</span>
</div>
</div>
)}
{/* 关联藏品 */}
{item.collection_name && (
<div style={{
background: '#3b82f620',
borderRadius: '8px',
padding: '8px 12px',
marginBottom: '8px',
borderLeft: '3px solid #3b82f6'
}}>
<span style={{ color: '#3b82f6', fontSize: '12px' }}>关联藏品: </span>
<span style={{ color: '#fff', fontSize: '13px' }}>{item.collection_name}</span>
</div>
)}
{/* 统计 */}
<div style={{ display: 'flex', gap: '16px', color: '#64748b', fontSize: '12px' }}>
<span>👁 {item.view_count}</span>
<span>📞 {item.contact_count}</span>
</div>
</div>
))
)}
</div>
)}
</div>
)
}