Compare commits

...

11 Commits

9 changed files with 382 additions and 49 deletions

View File

@ -1 +1 @@
VERSION=1.2.59
VERSION=1.2.41

View File

@ -52,11 +52,10 @@ app = FastAPI(
# 设置全局错误处理器
setup_error_handlers(app)
# CORS 配置 - 生产环境限制域名
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://47.103.29.111,http://120.55.81.21,https://socoolbot.com").split(",")
# CORS 配置
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS, # 生产环境限制域名
allow_origins=["*"], # 生产环境应该限制域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@ -67,10 +66,10 @@ uploads_dir = "uploads"
os.makedirs(uploads_dir, exist_ok=True)
app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads")
# 挂载项目静态资源目录(可选,生产环境建议用 Nginx
# static_dir = Path(__file__).parent.parent.parent / "static"
# if static_dir.exists():
# app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
# 挂载项目静态资源目录
static_dir = Path(__file__).parent.parent / "static"
if static_dir.exists():
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
# 添加日志中间件
app.middleware("http")(logging_middleware)
@ -82,7 +81,7 @@ app.include_router(operations.router)
app.include_router(ocr_router.router) # OCR 识别
app.include_router(users_router.router) # 当前用户接口
app.include_router(users_router.admin_router) # 管理员用户管理
app.include_router(information_router.router) # 资讯
app.include_router(information_router.router)
app.include_router(yichens_router.router) # 一尘看板

View File

@ -163,7 +163,7 @@ class Information(Base):
# 内容描述
content = Column(Text, nullable=True)
# 关联藏品ID
collection_id = Column(String(36), ForeignKey("collections.f99_90_id", ondelete="SET NULL"), nullable=True)

View File

@ -1,12 +1,14 @@
# 资讯API路由
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import text
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime, date
from app.core.database import get_db
from app.core.auth import get_current_user
from app.core.coolbot_db import coolbot_engine
from app.models.models import User, Information, Collection
router = APIRouter(prefix="/api/information", tags=["资讯"])
@ -16,7 +18,7 @@ router = APIRouter(prefix="/api/information", tags=["资讯"])
class InformationCreate(BaseModel):
info_type: str # seek-寻配号, deal-成交数据, publish-发布
title: str
content: Optional[str] = None
content: Optional[str]
collection_id: Optional[str] = None
expect_category: Optional[str] = None
expect_version: Optional[str] = None
@ -30,7 +32,7 @@ class InformationCreate(BaseModel):
class InformationUpdate(BaseModel):
title: Optional[str] = None
content: Optional[str] = None
content: Optional[str]
status: Optional[str] = None
expect_category: Optional[str] = None
expect_version: Optional[str] = None
@ -74,6 +76,8 @@ class InformationResponse(BaseModel):
collection_number: Optional[str] = None
# 匹配数量(我的藏品中满足条件的数量)
matched_count: Optional[int] = 0
# 网络数据匹配数量coolbot_data数据库中满足条件的数量
network_matched_count: Optional[int] = 0
class Config:
from_attributes = True
@ -87,7 +91,8 @@ def get_information_list(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
current_user: Optional[User] = Depends(get_current_user),
db: Session = Depends(get_db)
db: Session = Depends(get_db),
response: Response = None
):
"""获取资讯列表(公开,无需登录)"""
query = db.query(Information).options(
@ -142,8 +147,21 @@ def get_information_list(
collection_version=item.collection.f02_11_version if item.collection else None,
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
matched_count=matched_count,
network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0,
))
# 获取总数并设置响应头
from fastapi import Response
total_query = db.query(Information).filter(Information.status == status)
if info_type:
total_query = total_query.filter(Information.info_type == info_type)
total_count = total_query.count()
total_pages = (total_count + page_size - 1) // page_size
# 设置响应头
response.headers['X-Total-Pages'] = str(total_pages)
response.headers['X-Total-Count'] = str(total_count)
return result
@ -182,6 +200,110 @@ def match_collections_count(db: Session, user_id: str, expect_number: str) -> in
return count
def match_collections_count_from_coolbot(expect_number: str) -> int:
"""根据号码特征计算匹配藏品数量从coolbot_data数据库"""
if not expect_number or len(expect_number) != 10:
return 0
# 固定前缀
if not expect_number.startswith('J0'):
return 0
pattern = expect_number[2:] # 后8位
if not pattern:
return 0
# 直接查询coolbot_data数据库
query = text("""
SELECT COUNT(*) FROM collections
WHERE crown_code IS NOT NULL
AND crown_code != ''
AND LENGTH(crown_code) >= 10
AND crown_code LIKE 'J0%'
""")
try:
with coolbot_engine.connect() as conn:
result = conn.execute(query)
total_count = result.scalar() or 0
# 遍历匹配
query_all = text("""
SELECT id, crown_code FROM collections
WHERE crown_code IS NOT NULL
AND crown_code != ''
AND LENGTH(crown_code) >= 10
AND crown_code LIKE 'J0%'
""")
result = conn.execute(query_all)
match_count = 0
for row in result:
crown_code = row[1]
if crown_code and len(crown_code) >= 10:
col_pattern = crown_code[2:10]
if match_pattern(col_pattern, pattern):
match_count += 1
return match_count
except Exception as e:
print(f"Error querying coolbot_data: {e}")
return 0
def match_collections_list_from_coolbot(expect_number: str, limit: int = 20) -> List[dict]:
"""获取匹配的藏品列表从coolbot_data数据库"""
if not expect_number or len(expect_number) != 10:
return []
# 固定前缀
if not expect_number.startswith('J0'):
return []
pattern = expect_number[2:] # 后8位
if not pattern:
return []
# 直接查询coolbot_data数据库
query = text("""
SELECT id, name, category, crown_code, price, post_title, post_url, author, post_crawled_at
FROM collections
WHERE crown_code IS NOT NULL
AND crown_code != ''
AND LENGTH(crown_code) >= 10
AND crown_code LIKE 'J0%'
""")
try:
with coolbot_engine.connect() as conn:
result = conn.execute(query)
matched = []
for row in result:
crown_code = row[3]
if crown_code and len(crown_code) >= 10:
col_pattern = crown_code[2:10]
if match_pattern(col_pattern, pattern):
matched.append({
"id": row[0],
"name": row[1],
"category": row[2],
"crown_code": crown_code,
"price": float(row[4]) if row[4] else None,
"post_title": row[5],
"post_url": row[6],
"author": row[7],
"post_crawled_at": row[8].isoformat() if row[8] else None
})
if len(matched) >= limit:
break
return matched
except Exception as e:
print(f"Error querying coolbot_data: {e}")
return []
def match_pattern(col_number: str, pattern: str) -> bool:
"""匹配号码特征模式"""
# X = 任意数字
@ -502,7 +624,36 @@ def get_seek_match(
"cost_price": c.f05_40_cost_price,
}
for c in matched
]
],
"network_matched_count": match_collections_count_from_coolbot(info.expect_number) if info.expect_number else 0,
"network_collections": match_collections_list_from_coolbot(info.expect_number, limit=20) if info.expect_number else []
}
# 获取网络数据匹配列表
@router.get("/seek/network-match/{info_id}")
def get_network_match(
info_id: str,
limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db)
):
"""获取一尘数据库中匹配的藏品列表"""
info = db.query(Information).filter(
Information.id == info_id,
Information.info_type == "seek"
).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
if not info.expect_number:
return {"matched_count": 0, "collections": []}
matched = match_collections_list_from_coolbot(info.expect_number, limit=limit)
return {
"matched_count": len(matched),
"collections": matched
}
@ -558,8 +709,21 @@ def get_my_seeks(
collection_version=item.collection.f02_11_version if item.collection else None,
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
matched_count=matched_count,
network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0,
))
# 获取总数并设置响应头
from fastapi import Response
total_query = db.query(Information).filter(Information.status == status)
if info_type:
total_query = total_query.filter(Information.info_type == info_type)
total_count = total_query.count()
total_pages = (total_count + page_size - 1) // page_size
# 设置响应头
response.headers['X-Total-Pages'] = str(total_pages)
response.headers['X-Total-Count'] = str(total_count)
return result
@ -891,3 +1055,18 @@ def get_publisher_info(
"contact": contact,
"created_at": info.created_at.isoformat() if info.created_at else None
}
@router.get("/yichen-posts")
def get_yichen_posts(category: str = None, search: str = None, page: int = 1, page_size: int = 20):
from app.models.models import Information
from sqlalchemy import desc
query = db.query(Information).filter(Information.info_type == 'yichen')
if category:
query = query.filter(Information.expect_category == category)
if search:
query = query.filter(Information.title.contains(search))
total = query.count()
offset = (page - 1) * page_size
items = query.order_by(desc(Information.created_at)).offset(offset).limit(page_size).all()
return {"data": items, "pagination": {"page": page, "limit": page_size, "total": total}}

View File

@ -1 +1 @@
VERSION=1.2.59
VERSION=1.2.50

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>甲辰收藏 v1.2.53</title>
<title>甲辰收藏 v1.2.50</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="/static/icons/favicon.png" />

View File

@ -5,6 +5,8 @@ export default function Home() {
const [user, setUser] = useState(null)
const [stats, setStats] = useState({ totalCount: 0, totalCost: 0, totalRevenue: 0, expectedProfit: 0, totalProfit: 0, gradedCount: 0 })
const [recentCollections, setRecentCollections] = useState([])
const [yichensStats, setYichensStats] = useState({ total: 0, deals: 0, wants: 0, dragons: 0, horses: 0, tianma: 0 })
const [recentPosts, setRecentPosts] = useState([])
const currentPath = window.location.hash.slice(1) || '/'
useEffect(() => {
@ -54,6 +56,18 @@ export default function Home() {
}
}, [])
//
useEffect(() => {
fetch('/api/yichens/stats/today').then(res => res.json()).then(data => {
setYichensStats(data || {})
}).catch(() => {})
//
fetch('/api/yichens/posts?limit=10&offset=0').then(res => res.json()).then(data => {
setRecentPosts(Array.isArray(data) ? data : [])
}).catch(() => {})
}, [])
//
const isAdmin = user && user.role === 'admin'
@ -96,7 +110,7 @@ export default function Home() {
backdropFilter: 'blur(10px)'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<img src="/static/images/jiachenlong-logo.png?v=1.2.4" alt="甲辰收藏" style={{ width: '48px', height: '48px', borderRadius: '50%', objectFit: 'cover' }} />
<img src="/static/images/jiachenlong-logo.png" alt="甲辰收藏" style={{ width: '48px', height: '48px', borderRadius: '50%', objectFit: 'cover' }} />
<div>
<div style={{ color: '#fff', fontSize: '18px', fontWeight: '600' }}>{user?.username || '用户'}</div>
<div style={{ color: 'rgba(255,255,255,0.6)', fontSize: '12px' }}>欢迎回来 👋</div>
@ -175,11 +189,42 @@ export default function Home() {
</div>
</div>
{/* 最近藏品 */}
{/* 一尘今日数据 */}
<div style={{ marginBottom: '20px' }}>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px', marginBottom: '12px', paddingLeft: '4px' }}>📊 一尘今日数据</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
<div style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.15) 0%, rgba(59,130,246,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(59,130,246,0.2)', textAlign: 'center' }}>
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: '700' }}>{yichensStats.total || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>总帖子</div>
</div>
<div style={{ background: 'linear-gradient(135deg, rgba(16,185,129,0.15) 0%, rgba(16,185,129,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(16,185,129,0.2)', textAlign: 'center' }}>
<div style={{ color: '#34d399', fontSize: '20px', fontWeight: '700' }}>{yichensStats.deals || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>出售</div>
</div>
<div style={{ background: 'linear-gradient(135deg, rgba(245,158,11,0.15) 0%, rgba(245,158,11,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(245,158,11,0.2)', textAlign: 'center' }}>
<div style={{ color: '#fbbf24', fontSize: '20px', fontWeight: '700' }}>{yichensStats.wants || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>求购</div>
</div>
<div style={{ background: 'linear-gradient(135deg, rgba(139,92,246,0.15) 0%, rgba(139,92,246,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(139,92,246,0.2)', textAlign: 'center' }}>
<div style={{ color: '#a78bfa', fontSize: '20px', fontWeight: '700' }}>{yichensStats.dragons || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>龙钞</div>
</div>
<div style={{ background: 'linear-gradient(135deg, rgba(236,72,153,0.15) 0%, rgba(236,72,153,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(236,72,153,0.2)', textAlign: 'center' }}>
<div style={{ color: '#f472b6', fontSize: '20px', fontWeight: '700' }}>{yichensStats.horses || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>马钞</div>
</div>
<div style={{ background: 'linear-gradient(135deg, rgba(20,184,166,0.15) 0%, rgba(20,184,166,0.05) 100%)', borderRadius: '12px', padding: '12px 8px', border: '1px solid rgba(20,184,166,0.2)', textAlign: 'center' }}>
<div style={{ color: '#2dd4bf', fontSize: '20px', fontWeight: '700' }}>{yichensStats.tianma || 0}</div>
<div style={{ color: 'rgba(255,255,255,0.5)', fontSize: '10px' }}>天马</div>
</div>
</div>
</div>
{/* 最新一尘发帖 */}
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px', paddingLeft: '4px' }}>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px' }}>最近藏品</div>
<div onClick={() => window.location.hash = '#/list'} style={{ color: '#3b82f6', fontSize: '12px', cursor: 'pointer' }}>查看全部 </div>
<div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '14px' }}>📝 最新一尘发帖</div>
<div onClick={() => window.location.hash = '#/news'} style={{ color: '#3b82f6', fontSize: '12px', cursor: 'pointer' }}>查看全部 </div>
</div>
<div style={{
background: 'rgba(255,255,255,0.03)',
@ -187,24 +232,30 @@ export default function Home() {
border: '1px solid rgba(255,255,255,0.05)',
overflow: 'hidden'
}}>
{recentCollections.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: 'rgba(255,255,255,0.3)' }}>暂无藏品</div>
{recentPosts.length === 0 ? (
<div style={{ padding: '40px', textAlign: 'center', color: 'rgba(255,255,255,0.3)' }}>暂无帖子</div>
) : (
recentCollections.map((item, idx) => (
<div key={item.id || idx} onClick={() => window.location.hash = '#/detail?id=' + item.id} style={{
recentPosts.map((item, idx) => (
<div key={item.post_id || idx} onClick={() => window.location.hash = '#/news'} style={{
padding: '12px 16px',
borderBottom: idx < recentCollections.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none',
borderBottom: idx < recentPosts.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<div>
<div style={{ color: '#fff', fontSize: '14px' }}>{item.code || '-'} <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '14px', fontWeight: 'bold' }}>{item.prefixSerial || ''}</span></div>
<div style={{ color: 'rgba(255,255,255,0.4)', fontSize: '12px', marginTop: '2px' }}>{item.version || '-'} · {item.status === 'sold' ? '已售' : item.status === 'in_collection' ? '收藏中' : item.status}</div>
<div style={{ flex: 1 }}>
<div style={{ color: '#fff', fontSize: '14px' }}>{item.title || '无标题'}</div>
<div style={{ color: 'rgba(255,255,255,0.4)', fontSize: '12px', marginTop: '2px' }}>{item.category || '-'} · {item.author_username || '未知'} · {item.post_time?.substring(0, 16) || ''}</div>
</div>
<div style={{ color: item.costPrice ? '#22c55e' : 'rgba(255,255,255,0.3)', fontSize: '13px' }}>
{item.costPrice ? '¥' + item.costPrice : '-'}
<div style={{
color: item.post_type === 'deal' ? '#10b981' : item.post_type === 'want' ? '#f59e0b' : '#8b5cf6',
fontSize: '12px',
padding: '2px 8px',
borderRadius: '4px',
background: item.post_type === 'deal' ? 'rgba(16,185,129,0.2)' : item.post_type === 'want' ? 'rgba(245,158,11,0.2)' : 'rgba(139,92,246,0.2)'
}}>
{item.post_type === 'deal' ? '出售' : item.post_type === 'want' ? '求购' : '其他'}
</div>
</div>
))

View File

@ -672,6 +672,9 @@ export default function Info() {
</div>
</div>
)}
</div>
</div>
)
}

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'
import YichensBoard from './YichensBoard'
// -
const getUserPhone = () => {
@ -22,7 +23,7 @@ const getStoredUserId = () => {
// -
export default function News() {
const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'seek')
const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'yichen')
const [showSeekPublish, setShowSeekPublish] = useState(false)
const [showMySeeks, setShowMySeeks] = useState(false) //
const [showContactId, setShowContactId] = useState(null) // ID
@ -32,6 +33,8 @@ export default function News() {
const [matchedStatus, setMatchedStatus] = useState({}) //
const [showMatchList, setShowMatchList] = useState(false)
const [matchCollections, setMatchCollections] = useState([])
const [networkMatchCollections, setNetworkMatchCollections] = useState([])
const [showNetworkMatchList, setShowNetworkMatchList] = useState(false)
const [customModal, setCustomModal] = useState({show: false, title: '', content: ''})
const [seekForm, setSeekForm] = useState({
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || ''
@ -40,6 +43,8 @@ export default function News() {
const [viewMode, setViewMode] = useState('all')
const [expandedItems, setExpandedItems] = useState({}) //
const [loading, setLoading] = useState(false)
const [currentPage, setCurrentPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const API_BASE = localStorage.getItem('API_BASE') || ''
const currentUser = localStorage.getItem('token') ? { id: 'logged_in' } : null
@ -69,10 +74,13 @@ export default function News() {
//
const token = localStorage.getItem('token')
// tab
const type = activeTab === 'seek' ? 'seek' : 'deal'
const type = activeTab === 'seek' ? 'seek' : activeTab === 'deal' ? 'deal' : 'yichen'
// token便matched_count
const headers = token ? { Authorization: `Bearer ${token}` } : {}
const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}`, { headers })
const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}&page=${currentPage}&page_size=50`, { headers })
//
const total = res.headers.get('X-Total-Pages') || res.headers.get('x-total-pages')
if (total) setTotalPages(parseInt(total))
//
if (!res.ok) {
console.error('获取资讯列表失败:', res.status)
@ -146,10 +154,11 @@ export default function News() {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
const phone = getUserPhone()
const res = await fetch(`${API_BASE}/api/information/seek/match-confirm`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ info_id: infoId })
body: JSON.stringify({ info_id: infoId, contact: phone || '用户已同意' })
})
const data = await res.json()
if (data.message === '匹配成功,已通知发布者') {
@ -187,7 +196,7 @@ export default function News() {
console.log('Matched user response:', res.status)
const data = await res.json()
console.log('Matched user data:', data)
if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.matched_contact || '未提供'}`})
if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示所有用户信息仅作参考交易请走正规平台如产生经济损失与本站无关后果自负。`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
@ -200,7 +209,7 @@ export default function News() {
console.log('Publisher response:', res.status)
const data = await res.json()
console.log('Publisher data:', data)
if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.contact || '未提供'}`})
if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示所有用户信息仅作参考交易请走正规平台如产生经济损失与本站无关后果自负。`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
@ -223,6 +232,20 @@ export default function News() {
}
}
//
const fetchNetworkMatchCollections = async (infoId) => {
try {
const res = await fetch(`${API_BASE}/api/information/seek/network-match/${infoId}`)
const data = await res.json()
console.log('网络数据匹配结果:', data)
setNetworkMatchCollections(data.collections || [])
setShowNetworkMatchList(true)
} catch (e) {
console.error('获取网络匹配藏品失败:', e)
alert('获取网络匹配藏品失败: ' + e.message)
}
}
// ID
const currentUserId = getStoredUserId()
@ -315,7 +338,8 @@ export default function News() {
// Tab
const tabs = [
{ key: 'seek', label: '🔍 寻配号' },
{ key: 'deal', label: '💰 成交行情' }
{ key: 'deal', label: '💰 成交行情' },
{ key: 'yichen', label: '📊 一尘看板' }
]
const formatDate = (dateStr) => {
@ -387,7 +411,7 @@ export default function News() {
readOnly={isFixed}
onChange={(e) => {
if (i < 2) return
const val = e.target.value.toUpperCase().replace(/[^0-9XABCFG]/g, '')
const val = e.target.value.toUpperCase().replace(/[^0-9XABCDEFG]/g, '')
const newFeatures = (seekForm.features || '').split('')
while (newFeatures.length < 8) newFeatures.push('')
newFeatures[i - 2] = val
@ -435,7 +459,7 @@ export default function News() {
</div>
)}
<h3 style={{ color: '#fff', marginBottom: '16px' }}>
{activeTab === 'seek' ? '🔍 寻配号信息' : '💰 成交行情信息'}
{activeTab === 'seek' ? '🔍 寻配号信息' : activeTab === 'deal' ? '💰 成交行情信息' : '📊 一尘看板'}
{activeTab === 'seek' && (
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
<button onClick={() => { setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部</button>
@ -450,11 +474,11 @@ export default function News() {
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>加载中...</div>
) : infoList.length === 0 ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '40px' }}>
暂无{activeTab === 'seek' ? '寻配号' : '成交行情'}信息
暂无{activeTab === 'seek' ? '寻配号' : activeTab === 'deal' ? '成交行情' : '一尘看板'}信息
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{infoList.map(item => {
{activeTab === 'yichen' ? <YichensBoard /> : infoList.map(item => {
//
const content = item.content || ''
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
@ -472,7 +496,7 @@ export default function News() {
</div>
{/* 创建日期 + 用户名 */}
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}>
📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {item.user_name || '匿名用户'}
📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {(item.content && item.content.match(/发布者: (.+)/)?.[1]) || item.user_name || '匿名用户'}
</div>
{/* 号码特征 */}
{features && (
@ -521,12 +545,20 @@ export default function News() {
{/* 配号结果 - 仅寻配号显示 */}
{activeTab === 'seek' && (
<div style={{ marginBottom: '8px' }}>
<span
<span
style={{ color: '#10b981', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer' }}
onClick={() => fetchMatchCollections(item.id)}
>
配号结果: {item.matched_count || 0}条藏品匹配成功
自有{item.matched_count || 0}条藏品匹配成功
</span>
{item.network_matched_count !== undefined && (
<span
style={{ color: '#3b82f6', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer', marginLeft: '12px' }}
onClick={() => fetchNetworkMatchCollections(item.id)}
>
网络数据{item.network_matched_count}条匹配成功
</span>
)}
</div>
)}
{/* 正文 - 默认收起,点击展开 */}
@ -613,23 +645,24 @@ export default function News() {
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button
onClick={() => {
if (item.is_matched === 'matched') return
if (item.matched_count > 0) {
matchAndContact(item.id)
} else {
setCustomModal({show: true, title: '提示', content: '暂无匹配藏品,无法匹配'})
}
}}
disabled={item.matched_count === 0}
disabled={item.matched_count === 0 || item.is_matched === 'matched'}
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '6px',
border: 'none',
fontSize: '12px',
background: item.matched_count > 0 ? '#3b82f6' : '#4b5563',
background: (item.matched_count > 0 && item.is_matched !== 'matched') ? '#3b82f6' : '#4b5563',
color: '#fff',
cursor: item.matched_count > 0 ? 'pointer' : 'not-allowed',
opacity: item.matched_count > 0 ? 1 : 0.5
cursor: (item.matched_count > 0 && item.is_matched !== 'matched') ? 'pointer' : 'not-allowed',
opacity: (item.matched_count > 0 && item.is_matched !== 'matched') ? 1 : 0.5
}}
>
匹配并联系藏友
@ -751,6 +784,74 @@ export default function News() {
</div>
)}
{/* 网络数据匹配藏品列表弹窗 */}
{showNetworkMatchList && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxHeight: '80vh', overflow: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h3 style={{ color: '#fff', margin: 0 }}>匹配藏品清单网络数据</h3>
<button onClick={() => setShowNetworkMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button>
</div>
{networkMatchCollections.length === 0 ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '20px' }}>暂无匹配藏品</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{networkMatchCollections.map(c => (
<div
key={c.id}
style={{ background: '#0f172a', borderRadius: '8px', padding: '12px', cursor: 'pointer' }}
onClick={() => {
if (c.post_url) {
window.open(c.post_url, '_blank')
}
setShowNetworkMatchList(false)
}}
>
<div style={{ color: '#e2e8f0', fontSize: '13px' }}>
<span style={{ color: '#94a3b8' }}>名称: </span>{c.name || '-'}
</div>
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
<span style={{ color: '#94a3b8' }}>冠字号: </span>{c.crown_code}
</div>
{c.price && (
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
<span style={{ color: '#94a3b8' }}>价格: </span>¥{c.price}
</div>
)}
<div style={{ color: '#3b82f6', fontSize: '12px', marginTop: '6px' }}>
点击查看原帖
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* 分页组件 */}
{totalPages > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px', padding: '16px', marginTop: '8px' }}>
<button
onClick={() => { if (currentPage > 1) { setCurrentPage(currentPage - 1); fetchInfoList() } }}
disabled={currentPage === 1}
style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage === 1 ? '#374151' : '#3b82f6', color: currentPage === 1 ? '#6b7280' : '#fff', cursor: currentPage === 1 ? 'not-allowed' : 'pointer', fontSize: '13px' }}
>
上一页
</button>
<span style={{ color: '#94a3b8', fontSize: '13px' }}>
{currentPage} / {totalPages}
</span>
<button
onClick={() => { if (currentPage < totalPages) { setCurrentPage(currentPage + 1); fetchInfoList() } }}
disabled={currentPage >= totalPages}
style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage >= totalPages ? '#374151' : '#3b82f6', color: currentPage >= totalPages ? '#6b7280' : '#fff', cursor: currentPage >= totalPages ? 'not-allowed' : 'pointer', fontSize: '13px' }}
>
下一页
</button>
</div>
)}
{/* 我的寻号弹窗 */}
{showMySeeks && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>