v1.2.42 一尘看板基础版
This commit is contained in:
parent
b2b30cc54e
commit
7ff0ae5c9e
|
|
@ -0,0 +1,37 @@
|
||||||
|
import os
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.pool import QueuePool
|
||||||
|
|
||||||
|
COOLBOT_DB_URL = os.getenv(
|
||||||
|
"COOLBOT_DB_URL",
|
||||||
|
"postgresql://coolbot:Coolbot123@pgm-bp1t1008h019ez6cno.pg.rds.aliyuncs.com:5432/coolbot_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
coolbot_engine = create_engine(
|
||||||
|
COOLBOT_DB_URL,
|
||||||
|
poolclass=QueuePool,
|
||||||
|
pool_size=10,
|
||||||
|
max_overflow=20,
|
||||||
|
pool_timeout=30,
|
||||||
|
pool_recycle=1800,
|
||||||
|
pool_pre_ping=True,
|
||||||
|
echo=False,
|
||||||
|
connect_args={
|
||||||
|
"connect_timeout": 10,
|
||||||
|
"application_name": "zodiac-coolbot"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CoolbotSession = sessionmaker(autocommit=False, autoflush=False, bind=coolbot_engine)
|
||||||
|
|
||||||
|
def get_coolbot_db():
|
||||||
|
"""获取coolbot数据库会话"""
|
||||||
|
db = CoolbotSession()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
except Exception:
|
||||||
|
db.rollback()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
@ -16,6 +16,7 @@ from app.routers import auth, collections, operations
|
||||||
from app.routers import ocr as ocr_router
|
from app.routers import 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
|
from app.routers import information as information_router
|
||||||
|
from app.routers import yichens as yichens_router
|
||||||
|
|
||||||
# 版本信息 - 从 config/VERSION 文件读取
|
# 版本信息 - 从 config/VERSION 文件读取
|
||||||
def get_version():
|
def get_version():
|
||||||
|
|
@ -80,7 +81,8 @@ 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.include_router(information_router.router)
|
||||||
|
app.include_router(yichens_router.router) # 一尘看板
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ class Information(Base):
|
||||||
|
|
||||||
# 内容描述
|
# 内容描述
|
||||||
content = Column(Text, nullable=True)
|
content = Column(Text, nullable=True)
|
||||||
|
|
||||||
# 关联藏品ID
|
# 关联藏品ID
|
||||||
collection_id = Column(String(36), ForeignKey("collections.f99_90_id", ondelete="SET NULL"), nullable=True)
|
collection_id = Column(String(36), ForeignKey("collections.f99_90_id", ondelete="SET NULL"), nullable=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ router = APIRouter(prefix="/api/information", tags=["资讯"])
|
||||||
class InformationCreate(BaseModel):
|
class InformationCreate(BaseModel):
|
||||||
info_type: str # seek-寻配号, deal-成交数据, publish-发布
|
info_type: str # seek-寻配号, deal-成交数据, publish-发布
|
||||||
title: str
|
title: str
|
||||||
content: Optional[str] = None
|
content: Optional[str]
|
||||||
collection_id: Optional[str] = None
|
collection_id: Optional[str] = None
|
||||||
expect_category: Optional[str] = None
|
expect_category: Optional[str] = None
|
||||||
expect_version: Optional[str] = None
|
expect_version: Optional[str] = None
|
||||||
|
|
@ -30,7 +30,7 @@ class InformationCreate(BaseModel):
|
||||||
|
|
||||||
class InformationUpdate(BaseModel):
|
class InformationUpdate(BaseModel):
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
content: Optional[str] = None
|
content: Optional[str]
|
||||||
status: Optional[str] = None
|
status: Optional[str] = None
|
||||||
expect_category: Optional[str] = None
|
expect_category: Optional[str] = None
|
||||||
expect_version: Optional[str] = None
|
expect_version: Optional[str] = None
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy import func, text
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime, date
|
||||||
|
from app.core.coolbot_db import get_coolbot_db
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/yichens", tags=["一尘看板"])
|
||||||
|
|
||||||
|
# ============ 数据模型 ============
|
||||||
|
class YichensPostStats(BaseModel):
|
||||||
|
total_posts: int
|
||||||
|
total_deals: int # 出售
|
||||||
|
total_wants: int # 求购
|
||||||
|
total_replies: int
|
||||||
|
total_views: int
|
||||||
|
avg_price: Optional[float]
|
||||||
|
|
||||||
|
class CategoryStat(BaseModel):
|
||||||
|
category: str
|
||||||
|
count: int
|
||||||
|
|
||||||
|
class PostItem(BaseModel):
|
||||||
|
post_id: str
|
||||||
|
title: str
|
||||||
|
category: Optional[str]
|
||||||
|
post_type: str
|
||||||
|
price: Optional[float]
|
||||||
|
author_username: str
|
||||||
|
post_time: str
|
||||||
|
reply_count: int
|
||||||
|
view_count: int
|
||||||
|
url: Optional[str]
|
||||||
|
|
||||||
|
class UserStat(BaseModel):
|
||||||
|
total_users: int
|
||||||
|
new_users_today: int
|
||||||
|
sellers: int
|
||||||
|
|
||||||
|
class UserItem(BaseModel):
|
||||||
|
user_id: str
|
||||||
|
username: str
|
||||||
|
avatar_url: Optional[str]
|
||||||
|
credit_level: Optional[str]
|
||||||
|
credit_score: Optional[int]
|
||||||
|
post_count: int
|
||||||
|
is_seller: bool
|
||||||
|
registration_date: Optional[str]
|
||||||
|
|
||||||
|
# ============ 统计接口 ============
|
||||||
|
@router.get("/stats/posts", response_model=YichensPostStats)
|
||||||
|
def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
|
||||||
|
"""获取帖子统计"""
|
||||||
|
result = db.execute(text("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_posts,
|
||||||
|
COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals,
|
||||||
|
COUNT(*) FILTER (WHERE post_type = 'want') as total_wants,
|
||||||
|
COALESCE(SUM(reply_count), 0) as total_replies,
|
||||||
|
COALESCE(SUM(view_count), 0) as total_views,
|
||||||
|
AVG(price) as avg_price
|
||||||
|
FROM yichens_posts
|
||||||
|
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
|
||||||
|
"""), {"days": days}).fetchone()
|
||||||
|
|
||||||
|
return YichensPostStats(
|
||||||
|
total_posts=result[0] or 0,
|
||||||
|
total_deals=result[1] or 0,
|
||||||
|
total_wants=result[2] or 0,
|
||||||
|
total_replies=result[3] or 0,
|
||||||
|
total_views=result[4] or 0,
|
||||||
|
avg_price=float(result[5]) if result[5] else None
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/stats/categories", response_model=List[CategoryStat])
|
||||||
|
def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
|
||||||
|
"""按分类统计帖子数量"""
|
||||||
|
results = db.execute(text("""
|
||||||
|
SELECT category, COUNT(*) as count
|
||||||
|
FROM yichens_posts
|
||||||
|
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
|
||||||
|
GROUP BY category
|
||||||
|
ORDER BY count DESC
|
||||||
|
"""), {"days": days}).fetchall()
|
||||||
|
|
||||||
|
return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results]
|
||||||
|
|
||||||
|
@router.get("/stats/users", response_model=UserStat)
|
||||||
|
def get_user_stats(db: Session = Depends(get_coolbot_db)):
|
||||||
|
"""获取用户统计"""
|
||||||
|
result = db.execute(text("""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_users,
|
||||||
|
COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today,
|
||||||
|
COUNT(*) FILTER (WHERE is_seller = true) as sellers
|
||||||
|
FROM yichens_users
|
||||||
|
""")).fetchone()
|
||||||
|
|
||||||
|
return UserStat(
|
||||||
|
total_users=result[0] or 0,
|
||||||
|
new_users_today=result[1] or 0,
|
||||||
|
sellers=result[2] or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/posts", response_model=List[PostItem])
|
||||||
|
def get_posts(
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
offset: int = Query(0, ge=0),
|
||||||
|
category: Optional[str] = None,
|
||||||
|
post_type: Optional[str] = None,
|
||||||
|
db: Session = Depends(get_coolbot_db)
|
||||||
|
):
|
||||||
|
"""获取帖子列表"""
|
||||||
|
query = """
|
||||||
|
SELECT post_id, title, category, post_type, price,
|
||||||
|
author_username, post_time, reply_count, view_count, url
|
||||||
|
FROM yichens_posts
|
||||||
|
WHERE 1=1
|
||||||
|
"""
|
||||||
|
params = {"limit": limit, "offset": offset}
|
||||||
|
|
||||||
|
if category:
|
||||||
|
query += " AND category = :category"
|
||||||
|
params["category"] = category
|
||||||
|
|
||||||
|
if post_type:
|
||||||
|
query += " AND post_type = :post_type"
|
||||||
|
params["post_type"] = post_type
|
||||||
|
|
||||||
|
query += " ORDER BY post_time DESC LIMIT :limit OFFSET :offset"
|
||||||
|
|
||||||
|
results = db.execute(text(query), params).fetchall()
|
||||||
|
|
||||||
|
return [PostItem(
|
||||||
|
post_id=r[0],
|
||||||
|
title=r[1] or "",
|
||||||
|
category=r[2],
|
||||||
|
post_type=r[3] or "",
|
||||||
|
price=float(r[4]) if r[4] else None,
|
||||||
|
author_username=r[5] or "",
|
||||||
|
post_time=str(r[6]) if r[6] else "",
|
||||||
|
reply_count=r[7] or 0,
|
||||||
|
view_count=r[8] or 0,
|
||||||
|
url=r[9]
|
||||||
|
) for r in results]
|
||||||
|
|
||||||
|
@router.get("/users", response_model=List[UserItem])
|
||||||
|
def get_users(
|
||||||
|
limit: int = Query(20, ge=1, le=100),
|
||||||
|
offset: int = Query(0, ge=0),
|
||||||
|
is_seller: Optional[bool] = None,
|
||||||
|
db: Session = Depends(get_coolbot_db)
|
||||||
|
):
|
||||||
|
"""获取用户列表"""
|
||||||
|
query = """
|
||||||
|
SELECT user_id, username, avatar_url, credit_level, credit_score,
|
||||||
|
post_count, is_seller, registration_date
|
||||||
|
FROM yichens_users
|
||||||
|
WHERE 1=1
|
||||||
|
"""
|
||||||
|
params = {"limit": limit, "offset": offset}
|
||||||
|
|
||||||
|
if is_seller is not None:
|
||||||
|
query += " AND is_seller = :is_seller"
|
||||||
|
params["is_seller"] = is_seller
|
||||||
|
|
||||||
|
query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset"
|
||||||
|
|
||||||
|
results = db.execute(text(query), params).fetchall()
|
||||||
|
|
||||||
|
return [UserItem(
|
||||||
|
user_id=r[0],
|
||||||
|
username=r[1] or "",
|
||||||
|
avatar_url=r[2],
|
||||||
|
credit_level=r[3],
|
||||||
|
credit_score=r[4],
|
||||||
|
post_count=r[5] or 0,
|
||||||
|
is_seller=r[6] or False,
|
||||||
|
registration_date=str(r[7]) if r[7] else None
|
||||||
|
) for r in results]
|
||||||
|
|
@ -1 +1 @@
|
||||||
VERSION=1.2.41
|
VERSION=1.2.42
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default function Info() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 顶部tab:寻配号发布 / 发布管理
|
// 顶部tab:寻配号发布 / 发布管理
|
||||||
const [activeTab, setActiveTab] = useState('manage')
|
const [activeTab, setActiveTab] = useState('yichens')
|
||||||
const [showPublish, setShowPublish] = useState(false)
|
const [showPublish, setShowPublish] = useState(false)
|
||||||
const [publishType, setPublishType] = useState('deal') // 'seek' 或 'deal'
|
const [publishType, setPublishType] = useState('deal') // 'seek' 或 'deal'
|
||||||
const [myList, setMyList] = useState([])
|
const [myList, setMyList] = useState([])
|
||||||
|
|
@ -227,7 +227,8 @@ export default function Info() {
|
||||||
<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={{ 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' }}>
|
<div style={{ display: 'flex', gap: '8px', background: '#1e293b', borderRadius: '12px', padding: '4px' }}>
|
||||||
{[
|
{[
|
||||||
{ key: 'manage', label: '📋 发布管理' }
|
{ key: 'manage', label: '📋 发布管理' },
|
||||||
|
{ key: 'yichens', label: '📊 一尘看板' }
|
||||||
].map(tab => (
|
].map(tab => (
|
||||||
<div
|
<div
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
|
|
@ -672,6 +673,165 @@ export default function Info() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 一尘看板页 */}
|
||||||
|
{activeTab === 'yichens' && (
|
||||||
|
<YichensBoard />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ============ 一尘看板组件 ============
|
||||||
|
function YichensBoard() {
|
||||||
|
const [stats, setStats] = useState({ posts: null, categories: [], users: null })
|
||||||
|
const [posts, setPosts] = useState([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [expandedPosts, setExpandedPosts] = useState({})
|
||||||
|
const [postTypeFilter, setPostTypeFilter] = useState('all')
|
||||||
|
|
||||||
|
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStats()
|
||||||
|
fetchPosts()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
const [postsRes, catRes, usersRes] = await Promise.all([
|
||||||
|
fetch(`${API_BASE}/api/yichens/stats/posts?days=30`),
|
||||||
|
fetch(`${API_BASE}/api/yichens/stats/categories?days=30`),
|
||||||
|
fetch(`${API_BASE}/api/yichens/stats/users`)
|
||||||
|
])
|
||||||
|
const postsData = await postsRes.json()
|
||||||
|
const catData = await catRes.json()
|
||||||
|
const usersData = await usersRes.json()
|
||||||
|
setStats({ posts: postsData, categories: catData, users: usersData })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取统计失败:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPosts = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
let url = `${API_BASE}/api/yichens/posts?limit=50`
|
||||||
|
if (postTypeFilter !== 'all') url += `&post_type=${postTypeFilter}`
|
||||||
|
const res = await fetch(url)
|
||||||
|
const data = await res.json()
|
||||||
|
setPosts(data || [])
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取帖子失败:', e)
|
||||||
|
}
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePostExpand = (postId) => {
|
||||||
|
setExpandedPosts(prev => ({ ...prev, [postId]: !prev[postId] }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredPosts = postTypeFilter === 'all'
|
||||||
|
? posts
|
||||||
|
: posts.filter(p => p.post_type === postTypeFilter)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '0' }}>
|
||||||
|
{stats.posts && (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px', marginBottom: '20px' }}>
|
||||||
|
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', border: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#9ca3af', fontSize: '12px', marginBottom: '4px' }}>30天帖子</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}>{stats.posts.total_posts}</div>
|
||||||
|
<div style={{ color: '#6b7280', fontSize: '11px' }}>出售{stats.posts.total_deals} 求购{stats.posts.total_wants}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', border: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#9ca3af', fontSize: '12px', marginBottom: '4px' }}>浏览量</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}>{(stats.posts.total_views / 10000).toFixed(1)}万</div>
|
||||||
|
<div style={{ color: '#6b7280', fontSize: '11px' }}>回复{stats.posts.total_replies}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', border: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#9ca3af', fontSize: '12px', marginBottom: '4px' }}>用户数</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: '24px', fontWeight: 'bold' }}>{stats.users?.total_users || 0}</div>
|
||||||
|
<div style={{ color: '#6b7280', fontSize: '11px' }}>今日新增{stats.users?.new_users_today || 0}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', marginBottom: '20px', border: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#f9fafb', fontSize: '14px', fontWeight: '600', marginBottom: '12px' }}>📊 分类统计</div>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||||
|
{stats.categories.map(cat => (
|
||||||
|
<span key={cat.category} style={{ padding: '4px 12px', background: 'rgba(59,130,246,0.2)', borderRadius: '20px', color: '#93c5fd', fontSize: '12px' }}>
|
||||||
|
{cat.category} ({cat.count})
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
|
||||||
|
{[
|
||||||
|
{ key: 'all', label: '全部' },
|
||||||
|
{ key: 'deal', label: '出售' },
|
||||||
|
{ key: 'want', label: '求购' }
|
||||||
|
].map(ft => (
|
||||||
|
<button
|
||||||
|
key={ft.key}
|
||||||
|
onClick={() => { setPostTypeFilter(ft.key); fetchPosts() }}
|
||||||
|
style={{
|
||||||
|
padding: '8px 16px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
background: postTypeFilter === ft.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : '#374151',
|
||||||
|
color: '#fff',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '13px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ft.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div style={{ textAlign: 'center', color: '#9ca3af', padding: '40px' }}>加载中...</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
|
{filteredPosts.map(post => (
|
||||||
|
<div key={post.post_id} style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: '12px', padding: '16px', border: '1px solid #374151' }}>
|
||||||
|
<div onClick={() => togglePostExpand(post.post_id)} style={{ cursor: 'pointer' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
||||||
|
<span style={{ color: '#fff', fontSize: '15px', fontWeight: '500' }}>{post.title || '无标题'}</span>
|
||||||
|
<span style={{ color: post.post_type === 'deal' ? '#10b981' : post.post_type === 'want' ? '#f59e0b' : '#9ca3af', fontSize: '12px' }}>
|
||||||
|
{post.post_type === 'deal' ? '出售' : post.post_type === 'want' ? '求购' : '普通'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#9ca3af', fontSize: '12px' }}>
|
||||||
|
<span>{post.author_username || '未知'}</span>
|
||||||
|
<span>{post.post_time ? post.post_time.substring(0, 16) : ''}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expandedPosts[post.post_id] && (
|
||||||
|
<div style={{ marginTop: '12px', paddingTop: '12px', borderTop: '1px solid #374151' }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', fontSize: '13px' }}>
|
||||||
|
<div><span style={{ color: '#6b7280' }}>分类: </span><span style={{ color: '#fff' }}>{post.category || '-'}</span></div>
|
||||||
|
<div><span style={{ color: '#6b7280' }}>价格: </span><span style={{ color: '#10b981' }}>{post.price ? post.price.toLocaleString() + '元' : '待询'}</span></div>
|
||||||
|
<div><span style={{ color: '#6b7280' }}>浏览: </span><span style={{ color: '#fff' }}>{post.view_count || 0}</span></div>
|
||||||
|
<div><span style={{ color: '#6b7280' }}>回复: </span><span style={{ color: '#fff' }}>{post.reply_count || 0}</span></div>
|
||||||
|
</div>
|
||||||
|
{post.url && (
|
||||||
|
<a href={post.url} target="_blank" rel="noopener noreferrer" style={{ display: 'inline-block', marginTop: '12px', padding: '8px 16px', background: 'rgba(59,130,246,0.2)', borderRadius: '8px', color: '#60a5fa', fontSize: '13px', textDecoration: 'none' }}>
|
||||||
|
查看原帖
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import YichensBoard from './YichensBoard'
|
||||||
|
|
||||||
// 本地获取用户手机号 - 添加异常处理
|
// 本地获取用户手机号 - 添加异常处理
|
||||||
const getUserPhone = () => {
|
const getUserPhone = () => {
|
||||||
|
|
@ -22,7 +23,7 @@ const getStoredUserId = () => {
|
||||||
|
|
||||||
// 资讯页面 - 展示寻配号和行情信息(所有人可见)
|
// 资讯页面 - 展示寻配号和行情信息(所有人可见)
|
||||||
export default function News() {
|
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 [showSeekPublish, setShowSeekPublish] = useState(false)
|
||||||
const [showMySeeks, setShowMySeeks] = useState(false) // 我的寻号
|
const [showMySeeks, setShowMySeeks] = useState(false) // 我的寻号
|
||||||
const [showContactId, setShowContactId] = useState(null) // 显示联系方式的寻号ID
|
const [showContactId, setShowContactId] = useState(null) // 显示联系方式的寻号ID
|
||||||
|
|
@ -455,7 +456,7 @@ export default function News() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
{infoList.map(item => {
|
{activeTab === 'yichen' ? <YichensBoard /> : infoList.map(item => {
|
||||||
// 解析正文中的号码特征和联系方式
|
// 解析正文中的号码特征和联系方式
|
||||||
const content = item.content || ''
|
const content = item.content || ''
|
||||||
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
|
const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
export default function YichensBoard() {
|
||||||
|
const [stats, setStats] = useState({ posts: null, categories: [], users: null })
|
||||||
|
const [posts, setPosts] = useState([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [expandedPosts, setExpandedPosts] = useState({})
|
||||||
|
const [postTypeFilter, setPostTypeFilter] = useState('all')
|
||||||
|
const API_BASE = localStorage.getItem('API_BASE') || ''
|
||||||
|
|
||||||
|
useEffect(() => { fetchStats(); fetchPosts() }, [])
|
||||||
|
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
const [p, c, u] = await Promise.all([
|
||||||
|
fetch(`${API_BASE}/api/yichens/stats/posts?days=30`),
|
||||||
|
fetch(`${API_BASE}/api/yichens/stats/categories?days=30`),
|
||||||
|
fetch(`${API_BASE}/api/yichens/stats/users`)
|
||||||
|
])
|
||||||
|
setStats({ posts: await p.json(), categories: await c.json(), users: await u.json() })
|
||||||
|
} catch(e) { console.error(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPosts = async () => {
|
||||||
|
setLoading(true)
|
||||||
|
let url = `${API_BASE}/api/yichens/posts?limit=50`
|
||||||
|
if (postTypeFilter !== 'all') url += `&post_type=${postTypeFilter}`
|
||||||
|
try {
|
||||||
|
const res = await fetch(url)
|
||||||
|
setPosts((await res.json()) || [])
|
||||||
|
} catch { setPosts([]) }
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] }))
|
||||||
|
const filtered = postTypeFilter === 'all' ? posts : posts.filter(p => p.post_type === postTypeFilter)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '0' }}>
|
||||||
|
{stats.posts && (
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 12, marginBottom: 20 }}>
|
||||||
|
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#9ca3af', fontSize: 12, marginBottom: 4 }}>30天帖子</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: 24, fontWeight: 'bold' }}>{stats.posts.total_posts}</div>
|
||||||
|
<div style={{ color: '#6b7280', fontSize: 11 }}>出售{stats.posts.total_deals} 求购{stats.posts.total_wants}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#9ca3af', fontSize: 12, marginBottom: 4 }}>浏览量</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: 24, fontWeight: 'bold' }}>{(stats.posts.total_views/10000).toFixed(1)}万</div>
|
||||||
|
<div style={{ color: '#6b7280', fontSize: 11 }}>回复{stats.posts.total_replies}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#9ca3af', fontSize: 12, marginBottom: 4 }}>用户数</div>
|
||||||
|
<div style={{ color: '#fff', fontSize: 24, fontWeight: 'bold' }}>{stats.users?.total_users||0}</div>
|
||||||
|
<div style={{ color: '#6b7280', fontSize: 11 }}>今日新增{stats.users?.new_users_today||0}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, marginBottom: 20, border: '1px solid #374151' }}>
|
||||||
|
<div style={{ color: '#f9fafb', fontSize: 14, fontWeight: 600, marginBottom: 12 }}>📊 分类统计</div>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||||
|
{stats.categories.map(cat => (
|
||||||
|
<span key={cat.category} style={{ padding: '4px 12px', background: 'rgba(59,130,246,0.2)', borderRadius: 20, color: '#93c5fd', fontSize: 12 }}>
|
||||||
|
{cat.category} ({cat.count})
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
|
||||||
|
{[{key:'all',label:'全部'},{key:'deal',label:'出售'},{key:'want',label:'求购'}].map(k => (
|
||||||
|
<button key={k.key} onClick={() => { setPostTypeFilter(k.key); fetchPosts() }}
|
||||||
|
style={{ padding: '8px 16px', borderRadius: 8, border: 'none',
|
||||||
|
background: postTypeFilter===k.key ? 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)' : '#374151', color: '#fff', cursor: 'pointer', fontSize: 13 }}>
|
||||||
|
{k.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? <div style={{textAlign:'center',color:'#9ca3af',padding:40}}>加载中...</div> :
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
{filtered.map(post => (
|
||||||
|
<div key={post.post_id} style={{ background: 'linear-gradient(145deg, #1f2937 0%, #111827 100%)', borderRadius: 12, padding: 16, border: '1px solid #374151' }}>
|
||||||
|
<div onClick={() => togglePost(post.post_id)} style={{ cursor: 'pointer' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||||
|
<span style={{ color: '#fff', fontSize: 15, fontWeight: 500 }}>{post.title||'无标题'}</span>
|
||||||
|
<span style={{ color: post.post_type==='deal'?'#10b981':post.post_type==='want'?'#f59e0b':'#9ca3af', fontSize: 12 }}>
|
||||||
|
{post.post_type==='deal'?'出售':post.post_type==='want'?'求购':'普通'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', color: '#9ca3af', fontSize: 12 }}>
|
||||||
|
<span>{post.author_username||'未知'}</span>
|
||||||
|
<span>{post.post_time?.substring(0,16)||''}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{expandedPosts[post.post_id] && (
|
||||||
|
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #374151' }}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, fontSize: 13 }}>
|
||||||
|
<div><span style={{color:'#6b7280'}}>分类: </span><span style={{color:'#fff'}}>{post.category||'-'}</span></div>
|
||||||
|
<div><span style={{color:'#6b7280'}}>价格: </span><span style={{color:'#10b981'}}>{post.price?post.price.toLocaleString()+'元':'待询'}</span></div>
|
||||||
|
<div><span style={{color:'#6b7280'}}>浏览: </span><span style={{color:'#fff'}}>{post.view_count||0}</span></div>
|
||||||
|
<div><span style={{color:'#6b7280'}}>回复: </span><span style={{color:'#fff'}}>{post.reply_count||0}</span></div>
|
||||||
|
</div>
|
||||||
|
{post.url && <a href={post.url} target="_blank" rel="noopener noreferrer" style={{ display:'inline-block', marginTop:12, padding:'8px 16px', background:'rgba(59,130,246,0.2)', borderRadius:8, color:'#60a5fa', textDecoration:'none', fontSize:13 }}>查看原帖</a>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue