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 json
|
||||||
import random
|
import random
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy import func, text
|
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)
|
@router.get("/random", response_model=JokeResponse)
|
||||||
def get_random_joke(db: Session = Depends(get_db)):
|
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)
|
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):
|
def _vote_joke(joke_id: int, field: str, db: Session):
|
||||||
"""通用投票逻辑:点赞或点踩(仅允许已审核通过的笑话)"""
|
"""通用投票逻辑:点赞或点踩(仅允许已审核通过的笑话)"""
|
||||||
joke = db.query(Joke).filter(
|
joke = db.query(Joke).filter(
|
||||||
|
|||||||
+13
-5
@@ -58,6 +58,7 @@
|
|||||||
:hot-jokes="hotJokes"
|
:hot-jokes="hotJokes"
|
||||||
:types="allTypes"
|
:types="allTypes"
|
||||||
:crowds="allCrowds"
|
:crowds="allCrowds"
|
||||||
|
:stats="stats"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ import AppSubNav from '@/components/AppSubNav.vue'
|
|||||||
import AppRightbar from '@/components/AppRightbar.vue'
|
import AppRightbar from '@/components/AppRightbar.vue'
|
||||||
import Footer from '@/components/Footer.vue'
|
import Footer from '@/components/Footer.vue'
|
||||||
import { getTypes, getCrowds } from '@/api/category'
|
import { getTypes, getCrowds } from '@/api/category'
|
||||||
import { getJokes } from '@/api/joke'
|
import { getJokes, getHotMonthly, getStats } from '@/api/joke'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
@@ -98,6 +99,8 @@ const filterCrowdIds = ref([])
|
|||||||
|
|
||||||
// 热门笑话(右侧栏)
|
// 热门笑话(右侧栏)
|
||||||
const hotJokes = ref([])
|
const hotJokes = ref([])
|
||||||
|
// 网站统计
|
||||||
|
const stats = ref(null)
|
||||||
|
|
||||||
|
|
||||||
// 右侧栏:仅首页显示
|
// 右侧栏:仅首页显示
|
||||||
@@ -180,14 +183,19 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getJokes({ page: 1, page_size: 30 })
|
hotJokes.value = await getHotMonthly()
|
||||||
console.log('[APP] jokes loaded:', res.items?.length, 'total:', res.total)
|
console.log('[APP] monthly hot jokes loaded:', hotJokes.value.length)
|
||||||
const sorted = ((res.items || []).sort((a, b) => (b.view_count || 0) - (a.view_count || 0)))
|
|
||||||
hotJokes.value = sorted.slice(0, 5)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[APP] Failed to load hot jokes:', e)
|
console.warn('[APP] Failed to load hot jokes:', e)
|
||||||
hotJokes.value = []
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ export const getRandomJoke = () => request.get('/jokes/random')
|
|||||||
export const likeJoke = id => request.post(`/jokes/${id}/like`)
|
export const likeJoke = id => request.post(`/jokes/${id}/like`)
|
||||||
export const dislikeJoke = id => request.post(`/jokes/${id}/dislike`)
|
export const dislikeJoke = id => request.post(`/jokes/${id}/dislike`)
|
||||||
export const searchJokes = params => request.get('/jokes/', { params })
|
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>
|
<template>
|
||||||
<aside class="rightbar">
|
<aside class="rightbar">
|
||||||
<!-- 热门笑话 -->
|
<!-- 当月热门笑话 -->
|
||||||
<div class="rightbar-section">
|
<div class="rightbar-section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
<span class="title-icon">🔥</span>
|
<span class="title-icon">🔥</span>
|
||||||
热门笑话
|
当月热门笑话
|
||||||
</div>
|
</div>
|
||||||
<div class="hot-list" v-if="hotJokes.length">
|
<div class="hot-list" v-if="hotJokes.length">
|
||||||
<div
|
<div
|
||||||
@@ -23,6 +23,32 @@
|
|||||||
<div v-else class="empty-hint">暂无数据</div>
|
<div v-else class="empty-hint">暂无数据</div>
|
||||||
</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="rightbar-section">
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
@@ -74,7 +100,8 @@
|
|||||||
defineProps({
|
defineProps({
|
||||||
hotJokes: { type: Array, default: () => [] },
|
hotJokes: { type: Array, default: () => [] },
|
||||||
types: { type: Array, default: () => [] },
|
types: { type: Array, default: () => [] },
|
||||||
crowds: { type: Array, default: () => [] }
|
crowds: { type: Array, default: () => [] },
|
||||||
|
stats: { type: Object, default: null }
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -157,6 +184,32 @@ defineProps({
|
|||||||
}
|
}
|
||||||
.quick-tag.crowd:hover { background: #2196F3; color: white; }
|
.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-section { background: var(--tag-type-bg); }
|
||||||
.daily-text {
|
.daily-text {
|
||||||
@@ -166,4 +219,11 @@ defineProps({
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
transition: color 0.3s;
|
transition: color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 移动端隐藏右侧栏 */
|
||||||
|
@media (max-width: 1199px) {
|
||||||
|
.rightbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
<div class="filter-bar" v-if="filterText">
|
<div class="filter-bar" v-if="filterText">
|
||||||
<span class="filter-text">{{ filterText }}</span>
|
<span class="filter-text">{{ filterText }}</span>
|
||||||
<span class="filter-count">共 {{ jokeStore.total }} 条</span>
|
<span class="filter-count">共 {{ jokeStore.total }} 条</span>
|
||||||
<el-button link type="primary" @click="clearFilter" v-if="jokeStore.total > 0">清除筛选</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 笑话网格 -->
|
<!-- 笑话网格 -->
|
||||||
@@ -84,11 +83,6 @@ const loadJokes = async (p = 1) => {
|
|||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearFilter = () => {
|
|
||||||
window.history.pushState({}, '', '/')
|
|
||||||
loadJokes(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
types.value = allTypes.value?.length ? allTypes.value : await getTypes()
|
types.value = allTypes.value?.length ? allTypes.value : await getTypes()
|
||||||
crowds.value = allCrowds.value?.length ? allCrowds.value : await getCrowds()
|
crowds.value = allCrowds.value?.length ? allCrowds.value : await getCrowds()
|
||||||
|
|||||||
Reference in New Issue
Block a user