这是一次包含多个功能升级的提交: 1. 后端新增批量查询帖子最后回复的逻辑,在前端帖子列表项中展示最后回复信息 2. 重构点赞按钮,新增文章样式变体并统一样式逻辑 3. 重写工作台导航栏,精简默认展示的菜单选项 4. 新增文章目录组件,为帖子详情页添加文章目录导航 5. 重构帖子详情页,实现三栏响应式布局,新增板块卡片、作者信息栏与同板块热议推荐 6. 重构帖子列表行组件,优化排版与信息展示逻辑 7. 重构帖子管理操作组件,使用下拉菜单整合所有管理功能 8. 新增全局通用的滚动容器与点赞按钮样式
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useRouter, usePathname } from "next/navigation";
|
|
import { IconHeart } from "./Icons";
|
|
import { apiToggleLike } from "@/lib/api";
|
|
import { toast } from "@/lib/toast";
|
|
|
|
interface LikeButtonProps {
|
|
postId: string;
|
|
liked: boolean;
|
|
likeCount: number;
|
|
variant?: "default" | "compact" | "article";
|
|
}
|
|
|
|
export default function LikeButton({
|
|
postId,
|
|
liked: initialLiked,
|
|
likeCount: initialCount,
|
|
variant = "default",
|
|
}: LikeButtonProps) {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const [liked, setLiked] = useState(initialLiked);
|
|
const [count, setCount] = useState(initialCount);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// router.refresh()(登录/登出后)服务端会重新下发 liked 态,同步到本地,
|
|
// 避免客户端状态停留在刷新前的用户视角
|
|
useEffect(() => {
|
|
setLiked(initialLiked);
|
|
setCount(initialCount);
|
|
}, [initialLiked, initialCount]);
|
|
|
|
const handleClick = async () => {
|
|
if (loading) return;
|
|
setLoading(true);
|
|
// 乐观更新:先给反馈,失败再回滚
|
|
const wasLiked = liked;
|
|
setLiked(!wasLiked);
|
|
setCount((c) => c + (wasLiked ? -1 : 1));
|
|
try {
|
|
const res = await apiToggleLike(postId);
|
|
if (res.error === "未登录") {
|
|
router.push(`/login?redirect=${encodeURIComponent(pathname)}`);
|
|
return;
|
|
}
|
|
if (typeof res.liked === "boolean") {
|
|
setLiked(res.liked);
|
|
setCount(res.like_count ?? count);
|
|
} else {
|
|
throw new Error(res.error || "FAIL");
|
|
}
|
|
} catch {
|
|
setLiked(wasLiked);
|
|
setCount((c) => c + (wasLiked ? 1 : -1));
|
|
toast("操作失败,请稍后重试");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const sizeCls =
|
|
variant === "compact"
|
|
? "px-2.5 py-1.5 gap-1 text-[12.5px]"
|
|
: variant === "article"
|
|
? "px-7 py-3 gap-2 text-[15px] min-w-[148px]"
|
|
: "px-4 py-2 gap-1.5 text-sm";
|
|
const iconSize = variant === "compact" ? 13 : variant === "article" ? 18 : 15;
|
|
|
|
return (
|
|
<button
|
|
onClick={handleClick}
|
|
disabled={loading}
|
|
title={liked ? "取消点赞" : "点赞"}
|
|
aria-pressed={liked}
|
|
className={`j13-like inline-flex items-center justify-center font-semibold tabular-nums rounded-full border disabled:opacity-50 ${liked ? "is-liked" : ""} ${sizeCls}`}
|
|
>
|
|
<span key={String(liked)} className={liked ? "j13-heart-pop inline-flex" : "inline-flex"}>
|
|
<IconHeart size={iconSize} filled={liked} />
|
|
</span>
|
|
<span>{count}</span>
|
|
{variant === "article" && <span>{liked ? "已赞" : "点赞"}</span>}
|
|
</button>
|
|
);
|
|
}
|