feat: 当月热门笑话 + 统计面板,删除首页清除筛选按钮

- 后端新增 GET /api/jokes/hot-monthly(当月浏览量 top 10)
- 后端新增 GET /api/jokes/stats(公开统计:总数/审核/待审/浏览等)
- 右侧栏热门改为当月热门笑话
- 右侧栏新增网站统计区(笑话总数/已审核/待审核/总浏览)
- 首页删除清除筛选按钮
This commit is contained in:
bwstudio
2026-06-12 05:48:32 +08:00
parent 93b1d725e3
commit 076812f1aa
5 changed files with 139 additions and 34 deletions
+61 -20
View File
@@ -1,5 +1,6 @@
import json
import random
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import func, text
@@ -161,26 +162,6 @@ def list_jokes(
)
@router.get("/{joke_id}", response_model=JokeResponse)
def get_joke(joke_id: int, db: Session = Depends(get_db)):
"""获取单条笑话详情"""
# 只返回已审核通过的笑话
joke = db.query(Joke).filter(
Joke.id == joke_id,
Joke.status == "approved"
).first()
if not joke:
raise HTTPException(status_code=404, detail="笑话不存在")
# 使用原子更新避免并发竞态
db.query(Joke).filter(Joke.id == joke_id).update({Joke.view_count: Joke.view_count + 1})
db.commit()
# 重新查询获取更新后的数据
db.refresh(joke)
# 批量加载类型和人群名称
type_map, crowd_map = _batch_load_categories(db, [joke])
return joke_to_response(joke, type_map, crowd_map)
@router.get("/random", response_model=JokeResponse)
def get_random_joke(db: Session = Depends(get_db)):
"""随机获取一条已审核通过的笑话"""
@@ -208,6 +189,66 @@ def get_random_joke(db: Session = Depends(get_db)):
return joke_to_response(joke, type_map, crowd_map)
@router.get("/hot-monthly")
def hot_monthly_jokes(db: Session = Depends(get_db)):
"""当月热门笑话:本月浏览量最高的前 10 条"""
now = datetime.now()
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
jokes = (
db.query(Joke)
.filter(
Joke.status == "approved",
Joke.created_at >= start_of_month,
)
.order_by(Joke.view_count.desc())
.limit(10)
.all()
)
type_map, crowd_map = _batch_load_categories(db, jokes)
return [joke_to_response(j, type_map, crowd_map) for j in jokes]
@router.get("/stats")
def public_stats(db: Session = Depends(get_db)):
"""公开统计数据:笑话总数、审核/待审/拒绝数量、总浏览/赞/踩"""
total = db.query(Joke).count()
approved = db.query(Joke).filter(Joke.status == "approved").count()
pending = db.query(Joke).filter(Joke.status == "pending").count()
rejected = db.query(Joke).filter(Joke.status == "rejected").count()
total_views = db.query(Joke).with_entities(func.sum(Joke.view_count)).scalar() or 0
total_likes = db.query(Joke).with_entities(func.sum(Joke.like_count)).scalar() or 0
total_dislikes = db.query(Joke).with_entities(func.sum(Joke.dislike_count)).scalar() or 0
return {
"total_jokes": total,
"approved_jokes": approved,
"pending_jokes": pending,
"rejected_jokes": rejected,
"total_views": total_views,
"total_likes": total_likes,
"total_dislikes": total_dislikes,
}
@router.get("/{joke_id}", response_model=JokeResponse)
def get_joke(joke_id: int, db: Session = Depends(get_db)):
"""获取单条笑话详情"""
# 只返回已审核通过的笑话
joke = db.query(Joke).filter(
Joke.id == joke_id,
Joke.status == "approved"
).first()
if not joke:
raise HTTPException(status_code=404, detail="笑话不存在")
# 使用原子更新避免并发竞态
db.query(Joke).filter(Joke.id == joke_id).update({Joke.view_count: Joke.view_count + 1})
db.commit()
# 重新查询获取更新后的数据
db.refresh(joke)
# 批量加载类型和人群名称
type_map, crowd_map = _batch_load_categories(db, [joke])
return joke_to_response(joke, type_map, crowd_map)
def _vote_joke(joke_id: int, field: str, db: Session):
"""通用投票逻辑:点赞或点踩(仅允许已审核通过的笑话)"""
joke = db.query(Joke).filter(
+13 -5
View File
@@ -58,6 +58,7 @@
:hot-jokes="hotJokes"
:types="allTypes"
:crowds="allCrowds"
:stats="stats"
/>
</div>
@@ -75,7 +76,7 @@ import AppSubNav from '@/components/AppSubNav.vue'
import AppRightbar from '@/components/AppRightbar.vue'
import Footer from '@/components/Footer.vue'
import { getTypes, getCrowds } from '@/api/category'
import { getJokes } from '@/api/joke'
import { getJokes, getHotMonthly, getStats } from '@/api/joke'
import { useThemeStore } from '@/stores/theme'
const themeStore = useThemeStore()
@@ -98,6 +99,8 @@ const filterCrowdIds = ref([])
// 热门笑话(右侧栏)
const hotJokes = ref([])
// 网站统计
const stats = ref(null)
// 右侧栏:仅首页显示
@@ -180,14 +183,19 @@ onMounted(async () => {
}
try {
const res = await getJokes({ page: 1, page_size: 30 })
console.log('[APP] jokes loaded:', res.items?.length, 'total:', res.total)
const sorted = ((res.items || []).sort((a, b) => (b.view_count || 0) - (a.view_count || 0)))
hotJokes.value = sorted.slice(0, 5)
hotJokes.value = await getHotMonthly()
console.log('[APP] monthly hot jokes loaded:', hotJokes.value.length)
} catch (e) {
console.warn('[APP] Failed to load hot jokes:', e)
hotJokes.value = []
}
try {
stats.value = await getStats()
console.log('[APP] stats loaded:', stats.value)
} catch (e) {
console.warn('[APP] Failed to load stats:', e)
}
})
</script>
+2
View File
@@ -6,3 +6,5 @@ export const getRandomJoke = () => request.get('/jokes/random')
export const likeJoke = id => request.post(`/jokes/${id}/like`)
export const dislikeJoke = id => request.post(`/jokes/${id}/dislike`)
export const searchJokes = params => request.get('/jokes/', { params })
export const getHotMonthly = () => request.get('/jokes/hot-monthly')
export const getStats = () => request.get('/jokes/stats')
+63 -3
View File
@@ -1,10 +1,10 @@
<template>
<aside class="rightbar">
<!-- 热门笑话 -->
<!-- 当月热门笑话 -->
<div class="rightbar-section">
<div class="section-title">
<span class="title-icon">&#x1F525;</span>
热门笑话
当月热门笑话
</div>
<div class="hot-list" v-if="hotJokes.length">
<div
@@ -23,6 +23,32 @@
<div v-else class="empty-hint">暂无数据</div>
</div>
<!-- 网站统计 -->
<div class="rightbar-section stats-section" v-if="stats">
<div class="section-title">
<span class="title-icon">&#x1F4CA;</span>
网站统计
</div>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-num">{{ stats.total_jokes || 0 }}</span>
<span class="stat-label">笑话总数</span>
</div>
<div class="stat-item">
<span class="stat-num">{{ stats.approved_jokes || 0 }}</span>
<span class="stat-label">已审核</span>
</div>
<div class="stat-item">
<span class="stat-num">{{ stats.pending_jokes || 0 }}</span>
<span class="stat-label">待审核</span>
</div>
<div class="stat-item">
<span class="stat-num">{{ stats.total_views || 0 }}</span>
<span class="stat-label">总浏览</span>
</div>
</div>
</div>
<!-- 类型快捷 -->
<div class="rightbar-section">
<div class="section-title">
@@ -74,7 +100,8 @@
defineProps({
hotJokes: { type: Array, default: () => [] },
types: { type: Array, default: () => [] },
crowds: { type: Array, default: () => [] }
crowds: { type: Array, default: () => [] },
stats: { type: Object, default: null }
})
</script>
@@ -157,6 +184,32 @@ defineProps({
}
.quick-tag.crowd:hover { background: #2196F3; color: white; }
/* 统计区域 */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.stat-item {
text-align: center;
padding: 10px 6px;
background: var(--bg-page);
border-radius: 8px;
transition: background 0.3s;
}
.stat-num {
display: block;
font-size: 20px;
font-weight: 800;
color: var(--primary);
line-height: 1.2;
}
.stat-label {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
/* 每日一笑 */
.daily-section { background: var(--tag-type-bg); }
.daily-text {
@@ -166,4 +219,11 @@ defineProps({
font-style: italic;
transition: color 0.3s;
}
/* 移动端隐藏右侧栏 */
@media (max-width: 1199px) {
.rightbar {
display: none;
}
}
</style>
-6
View File
@@ -4,7 +4,6 @@
<div class="filter-bar" v-if="filterText">
<span class="filter-text">{{ filterText }}</span>
<span class="filter-count"> {{ jokeStore.total }} </span>
<el-button link type="primary" @click="clearFilter" v-if="jokeStore.total > 0">清除筛选</el-button>
</div>
<!-- 笑话网格 -->
@@ -84,11 +83,6 @@ const loadJokes = async (p = 1) => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const clearFilter = () => {
window.history.pushState({}, '', '/')
loadJokes(1)
}
onMounted(async () => {
types.value = allTypes.value?.length ? allTypes.value : await getTypes()
crowds.value = allCrowds.value?.length ? allCrowds.value : await getCrowds()