feat: 当月热门笑话 + 统计面板,删除首页清除筛选按钮
- 后端新增 GET /api/jokes/hot-monthly(当月浏览量 top 10) - 后端新增 GET /api/jokes/stats(公开统计:总数/审核/待审/浏览等) - 右侧栏热门改为当月热门笑话 - 右侧栏新增网站统计区(笑话总数/已审核/待审核/总浏览) - 首页删除清除筛选按钮
This commit is contained in:
+61
-20
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<aside class="rightbar">
|
||||
<!-- 热门笑话 -->
|
||||
<!-- 当月热门笑话 -->
|
||||
<div class="rightbar-section">
|
||||
<div class="section-title">
|
||||
<span class="title-icon">🔥</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">📊</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>
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user