diff --git a/backend/app/routers/yichens.py b/backend/app/routers/yichens.py index e163b4a..331be20 100644 --- a/backend/app/routers/yichens.py +++ b/backend/app/routers/yichens.py @@ -32,6 +32,7 @@ class PostItem(BaseModel): reply_count: int view_count: int url: Optional[str] + content: Optional[str] class UserStat(BaseModel): total_users: int @@ -42,6 +43,7 @@ class UserItem(BaseModel): user_id: str username: str avatar_url: Optional[str] + content: Optional[str] credit_level: Optional[str] credit_score: Optional[int] post_count: int @@ -105,7 +107,7 @@ def get_user_stats(db: Session = Depends(get_coolbot_db)): @router.get("/posts", response_model=List[PostItem]) def get_posts( - limit: int = Query(20, ge=1, le=100), + limit: int = Query(20, ge=1, le=500), offset: int = Query(0, ge=0), category: Optional[str] = None, post_type: Optional[str] = None, @@ -113,7 +115,7 @@ def get_posts( ): """获取帖子列表""" query = """ - SELECT post_id, title, category, post_type, price, + SELECT post_id, title, content, category, post_type, price, author_username, post_time, reply_count, view_count, url FROM yichens_posts WHERE 1=1 @@ -135,19 +137,20 @@ def get_posts( 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] + content=r[2] or "", + category=r[3], + post_type=r[4] or "", + price=float(r[5]) if r[5] else None, + author_username=r[6] or "", + post_time=str(r[7]) if r[7] else "", + reply_count=r[8] or 0, + view_count=r[9] or 0, + url=r[10] ) for r in results] @router.get("/users", response_model=List[UserItem]) def get_users( - limit: int = Query(20, ge=1, le=100), + limit: int = Query(20, ge=1, le=500), offset: int = Query(0, ge=0), is_seller: Optional[bool] = None, db: Session = Depends(get_coolbot_db) @@ -179,3 +182,69 @@ def get_users( is_seller=r[6] or False, registration_date=str(r[7]) if r[7] else None ) for r in results] + + +@router.get("/stats/today") +async def get_today_stats(db: Session = Depends(get_coolbot_db)): + """获取今日新增帖子统计""" + query = """ + SELECT + COUNT(*) as total, + SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, + SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants, + SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, + SUM(CASE WHEN category LIKE '%龙%' THEN 1 ELSE 0 END) as dragons, + SUM(CASE WHEN category LIKE '%马%' THEN 1 ELSE 0 END) as horses, + SUM(CASE WHEN category LIKE '%蛇%' THEN 1 ELSE 0 END) as snakes + FROM yichens_posts + WHERE post_time >= CURRENT_DATE + """ + result = db.execute(text(query)).fetchone() + return { + "total": result[0] or 0, + "deals": result[1] or 0, + "wants": result[2] or 0, + "others": result[3] or 0, + "dragons": result[3] or 0, + "horses": result[4] or 0, + "snakes": result[5] or 0 + } + +@router.get("/stats/hour") +async def get_hour_stats(db: Session = Depends(get_coolbot_db)): + """获取近一个小时新增帖子统计""" + query = """ + SELECT + COUNT(*) as total, + SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, + SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants, + SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, + SUM(CASE WHEN category LIKE '%龙%' THEN 1 ELSE 0 END) as dragons, + SUM(CASE WHEN category LIKE '%马%' THEN 1 ELSE 0 END) as horses, + SUM(CASE WHEN category LIKE '%蛇%' THEN 1 ELSE 0 END) as snakes + FROM yichens_posts + WHERE post_time >= NOW() - INTERVAL '1 hour' + """ + result = db.execute(text(query)).fetchone() + return { + "total": result[0] or 0, + "deals": result[1] or 0, + "wants": result[2] or 0, + "others": result[3] or 0, + "dragons": result[3] or 0, + "horses": result[4] or 0, + "snakes": result[5] or 0 + } + +@router.get("/stats/today-category") +async def get_today_category_stats(db: Session = Depends(get_coolbot_db)): + """获取今日帖子分类统计""" + query = """ + SELECT category, COUNT(*) as count + FROM yichens_posts + WHERE post_time >= CURRENT_DATE + GROUP BY category + ORDER BY count DESC + """ + results = db.execute(text(query)).fetchall() + return [{"category": r[0] or "未分类", "count": r[1]} for r in results] diff --git a/config/VERSION b/config/VERSION index 4aee9f9..79ee983 100644 --- a/config/VERSION +++ b/config/VERSION @@ -1 +1 @@ -VERSION=1.2.42 +VERSION=1.2.43 diff --git a/frontend/index.html b/frontend/index.html index bf5cc5a..42efbe9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v1.2.41 + 甲辰收藏 v1.2.42 diff --git a/frontend/src/pages/YichensBoard.jsx b/frontend/src/pages/YichensBoard.jsx index f105ce4..0785a51 100644 --- a/frontend/src/pages/YichensBoard.jsx +++ b/frontend/src/pages/YichensBoard.jsx @@ -1,66 +1,99 @@ import React, { useState, useEffect } from 'react' export default function YichensBoard() { - const [stats, setStats] = useState({ posts: null, categories: [], users: null }) + const [todayStats, setTodayStats] = useState({ total: 0, deals: 0, wants: 0, others: 0 }) + const [todayCategory, setTodayCategory] = useState([]) const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(false) const [expandedPosts, setExpandedPosts] = useState({}) const [postTypeFilter, setPostTypeFilter] = useState('all') + const [categoryFilter, setCategoryFilter] = useState('') + const [page, setPage] = useState(1) + const [totalPosts, setTotalPosts] = useState(0) const API_BASE = localStorage.getItem('API_BASE') || '' - useEffect(() => { fetchStats(); fetchPosts() }, []) + const today = new Date() + const dateStr = today.getFullYear() + '/' + (today.getMonth() + 1) + '/' + today.getDate() - const fetchStats = async () => { + useEffect(() => { fetchTodayStats(); fetchPosts(1, '') }, []) + + const fetchTodayStats = 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() }) + const res = await fetch(API_BASE + '/api/yichens/stats/today') + setTodayStats(await res.json()) } catch(e) { console.error(e) } } - const fetchPosts = async () => { + const fetchTodayCategory = async () => { + try { + const res = await fetch(API_BASE + '/api/yichens/stats/today-category') + setTodayCategory(await res.json()) + } catch(e) { console.error(e) } + } + + const fetchPosts = async (p, cat) => { setLoading(true) - let url = `${API_BASE}/api/yichens/posts?limit=50` - if (postTypeFilter !== 'all') url += `&post_type=${postTypeFilter}` + const currentPage = p !== undefined ? p : page + const currentCat = cat !== undefined ? cat : categoryFilter + + let url = API_BASE + '/api/yichens/posts?limit=390&offset=' + ((currentPage - 1) * 390) + if (postTypeFilter === 'deal') url += '&post_type=deal' + else if (postTypeFilter === 'want') url += '&post_type=want' + else if (postTypeFilter === 'other') url += '&post_type=normal' try { const res = await fetch(url) - setPosts((await res.json()) || []) + let data = await res.json() || [] + if (currentCat) { + if (currentCat === '龙') { + data = data.filter(p => p.category && (p.category.includes('龙') || p.category.includes('纪念钞'))) + } else if (currentCat === '蛇') { + data = data.filter(p => p.category && p.category.includes('蛇')) + } else if (currentCat === '马') { + data = data.filter(p => p.category && p.category.includes('马')) + } else if (currentCat === '其他') { + data = data.filter(p => p.category && !p.category.includes('龙') && !p.category.includes('纪念钞') && !p.category.includes('蛇') && !p.category.includes('马')) + } + } + setPosts(data) + setTotalPosts(todayStats.total || 0) } catch { setPosts([]) } setLoading(false) } + useEffect(() => { if (todayStats.total > 0) fetchTodayCategory() }, [todayStats]) + const togglePost = id => setExpandedPosts(p => ({ ...p, [id]: !p[id] })) - const filtered = postTypeFilter === 'all' ? posts : posts.filter(p => p.post_type === postTypeFilter) + + const StatCard = ({ label, value, color, onClick }) => ( +
+
{label}
+
{value}
+
+ ) return (
- {stats.posts && ( -
-
-
30天帖子
-
{stats.posts.total_posts}
-
出售{stats.posts.total_deals} 求购{stats.posts.total_wants}
-
-
-
浏览量
-
{(stats.posts.total_views/10000).toFixed(1)}万
-
回复{stats.posts.total_replies}
-
-
-
用户数
-
{stats.users?.total_users||0}
-
今日新增{stats.users?.new_users_today||0}
-
+
+
+ 📈 连体钞/纪念钞 {dateStr}
- )} +
+ { setPostTypeFilter('all'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} /> + { setPostTypeFilter('deal'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} /> + { setPostTypeFilter('want'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} /> + { setPostTypeFilter('other'); setCategoryFilter(''); setPage(1); fetchPosts(1, '') }} /> +
+
-
📊 分类统计
+
📊 今日分类统计
- {stats.categories.map(cat => ( + {todayCategory.map(cat => ( {cat.category} ({cat.count}) @@ -68,9 +101,9 @@ export default function YichensBoard() {
-
- {[{key:'all',label:'全部'},{key:'deal',label:'出售'},{key:'want',label:'求购'}].map(k => ( -
- {loading ?
加载中...
: -
- {filtered.map(post => ( -
-
togglePost(post.post_id)} style={{ cursor: 'pointer' }}> -
- {post.title||'无标题'} - - {post.post_type==='deal'?'出售':post.post_type==='want'?'求购':'普通'} - -
-
- {post.author_username||'未知'} - {post.post_time?.substring(0,16)||''} -
-
- {expandedPosts[post.post_id] && ( -
-
-
分类: {post.category||'-'}
-
价格: {post.price?post.price.toLocaleString()+'元':'待询'}
-
浏览: {post.view_count||0}
-
回复: {post.reply_count||0}
-
- {post.url && 查看原帖} -
+
+ {[{key:'',label:'全部'},{key:'龙',label:'龙钞'},{key:'蛇',label:'蛇钞'},{key:'马',label:'马钞'},{key:'其他',label:'其他'}].map(k => ( + + ))} +
+ + {loading ?
加载中...
: ( +
+
+
+ 共 {totalPosts} 条帖子,{Math.ceil(totalPosts / 390)} 页,当前第 {page} 页 +
+
+ {page > 1 ? ( + + ) : ( + 上一页 + )} + {posts.length >= 390 ? ( + + ) : ( + 下一页 )}
- ))} -
} +
+
+ {posts.map(post => ( +
+
togglePost(post.post_id)} style={{ cursor: 'pointer' }}> +
+ {post.title||'无标题'} +
+ + {post.post_type==='deal'?'出售':post.post_type==='want'?'求购':post.post_type==='normal'?'其他':'普通'} + + + {post.category || '-'} + +
+
+
+ {post.author_username||'未知'} + {post.post_time?.substring(0,16)||''} +
+
+ {expandedPosts[post.post_id] && post.content && ( +
+
+ {post.content} +
+ {post.url && 查看原帖} +
+ )} +
+ ))} +
+
+ )}
) }