From fde86b90d2d1aa31473edb69898481d8c287b2b7 Mon Sep 17 00:00:00 2001 From: bwstudio Date: Thu, 11 Jun 2026 19:17:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=82=B9=E8=B8=A9?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=88=97=E8=A1=A8=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E5=92=8C=E8=AF=A6=E6=83=85=E9=A1=B5=E6=94=AF=E6=8C=81=20?= =?UTF-8?q?=F0=9F=91=8D/=F0=9F=91=8E=20=E6=8A=95=E7=A5=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: 新增 dislike_count 字段(模型/Schema/数据库迁移) - 后端: 新增 POST /api/jokes/{id}/dislike 端点 - 后端: 管理后台统计新增 total_dislikes - 前端: 新增 useVote 组合函数(localStorage 持久化防重复) - 前端: JokeCard 列表卡片新增 👍/👎 可点击投票按钮 - 前端: 详情页 ❤️ 改为 👍 赞一下 / 👎 踩一脚 双按钮 --- api/app/models/joke.py | 1 + api/app/routers/admin.py | 3 + api/app/routers/jokes.py | 25 +++++-- api/app/schemas/joke.py | 1 + api/main.py | 12 ++++ web/src/api/joke.js | 1 + web/src/composables/useVote.js | 67 ++++++++++++++++++ web/src/views/detail/index.vue | 82 +++++++++++++--------- web/src/views/home/components/JokeCard.vue | 44 ++++++++++-- 9 files changed, 191 insertions(+), 45 deletions(-) create mode 100644 web/src/composables/useVote.js diff --git a/api/app/models/joke.py b/api/app/models/joke.py index 16cc986..3bc197b 100644 --- a/api/app/models/joke.py +++ b/api/app/models/joke.py @@ -25,6 +25,7 @@ class Joke(Base): status = Column(String(20), default="pending") view_count = Column(Integer, default=0) like_count = Column(Integer, default=0) + dislike_count = Column(Integer, default=0) created_at = Column(DateTime, default=func.now()) updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) diff --git a/api/app/routers/admin.py b/api/app/routers/admin.py index 7c0354c..9c3bef2 100644 --- a/api/app/routers/admin.py +++ b/api/app/routers/admin.py @@ -87,6 +87,7 @@ def joke_to_response(joke: Joke, db: Session = None) -> JokeResponse: status=joke.status, view_count=joke.view_count, like_count=joke.like_count, + dislike_count=joke.dislike_count, created_at=joke.created_at, updated_at=joke.updated_at, type_names=type_names, @@ -224,6 +225,7 @@ def admin_stats( rejected_jokes = 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_jokes, "approved_jokes": approved_jokes, @@ -231,6 +233,7 @@ def admin_stats( "rejected_jokes": rejected_jokes, "total_views": total_views, "total_likes": total_likes, + "total_dislikes": total_dislikes, } diff --git a/api/app/routers/jokes.py b/api/app/routers/jokes.py index 1d40f00..2f54a0f 100644 --- a/api/app/routers/jokes.py +++ b/api/app/routers/jokes.py @@ -76,6 +76,7 @@ def joke_to_response(joke: Joke, type_map: dict = None, crowd_map: dict = None) status=joke.status, view_count=joke.view_count, like_count=joke.like_count, + dislike_count=joke.dislike_count, created_at=joke.created_at, updated_at=joke.updated_at, type_names=type_names, @@ -207,10 +208,8 @@ def get_random_joke(db: Session = Depends(get_db)): return joke_to_response(joke, type_map, crowd_map) -@router.post("/{joke_id}/like") -def like_joke(joke_id: int, db: Session = Depends(get_db)): - """为笑话点赞(仅允许已审核通过的笑话)""" - # 先检查笑话是否存在且已审核 +def _vote_joke(joke_id: int, field: str, db: Session): + """通用投票逻辑:点赞或点踩(仅允许已审核通过的笑话)""" joke = db.query(Joke).filter( Joke.id == joke_id, Joke.status == "approved" @@ -218,8 +217,20 @@ def like_joke(joke_id: int, db: Session = Depends(get_db)): if not joke: raise HTTPException(status_code=404, detail="笑话不存在") # 使用原子更新避免并发竞态 - db.query(Joke).filter(Joke.id == joke_id).update({Joke.like_count: Joke.like_count + 1}) + update_data = {getattr(Joke, field): getattr(Joke, field) + 1} + db.query(Joke).filter(Joke.id == joke_id).update(update_data) db.commit() - # 获取更新后的值 db.refresh(joke) - return {"message": "点赞成功", "like_count": joke.like_count} \ No newline at end of file + return {"message": "操作成功", field: getattr(joke, field)} + + +@router.post("/{joke_id}/like") +def like_joke(joke_id: int, db: Session = Depends(get_db)): + """为笑话点赞(仅允许已审核通过的笑话)""" + return _vote_joke(joke_id, "like_count", db) + + +@router.post("/{joke_id}/dislike") +def dislike_joke(joke_id: int, db: Session = Depends(get_db)): + """为笑话点踩(仅允许已审核通过的笑话)""" + return _vote_joke(joke_id, "dislike_count", db) \ No newline at end of file diff --git a/api/app/schemas/joke.py b/api/app/schemas/joke.py index 1457aa5..40a1409 100644 --- a/api/app/schemas/joke.py +++ b/api/app/schemas/joke.py @@ -34,6 +34,7 @@ class JokeResponse(JokeBase): ai_level: str | None = None view_count: int like_count: int + dislike_count: int created_at: datetime updated_at: datetime type_names: list[str] | None = None diff --git a/api/main.py b/api/main.py index 841dfa8..110153b 100644 --- a/api/main.py +++ b/api/main.py @@ -12,6 +12,18 @@ from app.models.feedback import Feedback # 注册模型,确保 create_all 能 # 创建新表(如果不存在) Base.metadata.create_all(bind=engine) +# 对已有表新增字段的兼容迁移(SQLite 不支持 ALTER TABLE ADD COLUMN IF NOT EXISTS) +def _migrate_db(): + from sqlalchemy import inspect, text + inspector = inspect(engine) + columns = [c["name"] for c in inspector.get_columns("jokes")] + if "dislike_count" not in columns: + with engine.connect() as conn: + conn.execute(text("ALTER TABLE jokes ADD COLUMN dislike_count INTEGER DEFAULT 0")) + conn.commit() + +_migrate_db() + # Create FastAPI application app = FastAPI(title=API_TITLE, version=API_VERSION) diff --git a/web/src/api/joke.js b/web/src/api/joke.js index 77dede3..32549b6 100644 --- a/web/src/api/joke.js +++ b/web/src/api/joke.js @@ -4,4 +4,5 @@ export const getJokes = params => request.get('/jokes/', { params }) export const getJoke = id => request.get(`/jokes/${id}`) 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 }) diff --git a/web/src/composables/useVote.js b/web/src/composables/useVote.js new file mode 100644 index 0000000..453aaf9 --- /dev/null +++ b/web/src/composables/useVote.js @@ -0,0 +1,67 @@ +/** + * 点赞/点踩 投票状态管理 (localStorage 持久化) + * 键名格式: joke_vote_{id} → { liked: bool, disliked: bool } + * + * @param {import('vue').Ref} jokeIdRef — 响应式的笑话 ID + * @returns {{ state: { liked: boolean, disliked: boolean }, handleLike: () => Promise, handleDislike: () => Promise }} + */ +import { reactive, watchEffect, isRef, ref, toRef } from 'vue' +import { likeJoke, dislikeJoke } from '@/api/joke' + +const STORAGE_PREFIX = 'joke_vote_' + +function loadState(jokeId) { + if (!jokeId) return { liked: false, disliked: false } + try { + return JSON.parse(localStorage.getItem(STORAGE_PREFIX + jokeId)) || {} + } catch { + return {} + } +} + +function persistState(jokeId, key) { + if (!jokeId) return + const prev = loadState(jokeId) + prev[key] = true + localStorage.setItem(STORAGE_PREFIX + jokeId, JSON.stringify(prev)) +} + +export function useVote(jokeIdRef) { + const state = reactive({ liked: false, disliked: false }) + + // 当 jokeId 变化时重新从 localStorage 读取 + const _id = isRef(jokeIdRef) ? jokeIdRef : ref(jokeIdRef) + watchEffect(() => { + const saved = loadState(_id.value) + state.liked = !!saved.liked + state.disliked = !!saved.disliked + }) + + const handleLike = async () => { + const id = _id.value + if (!id || state.liked) return + try { + const res = await likeJoke(id) + state.liked = true + persistState(id, 'liked') + return res.like_count + } catch { + // 静默失败 + } + } + + const handleDislike = async () => { + const id = _id.value + if (!id || state.disliked) return + try { + const res = await dislikeJoke(id) + state.disliked = true + persistState(id, 'disliked') + return res.dislike_count + } catch { + // 静默失败 + } + } + + return { state, handleLike, handleDislike } +} \ No newline at end of file diff --git a/web/src/views/detail/index.vue b/web/src/views/detail/index.vue index 5127bdd..07ceb0f 100644 --- a/web/src/views/detail/index.vue +++ b/web/src/views/detail/index.vue @@ -44,9 +44,14 @@
浏览 {{ joke.view_count || 0 }} | - + | + | {{ formatDate(joke.created_at) }} @@ -63,50 +68,52 @@