feat: 帖子收藏与用户关注

- 新增收藏功能:favorite 服务与 handler、FavoriteButton 组件
- 新增关注功能:follow 服务与 handler、FollowButton 组件
- 用户主页适配关注/粉丝与收藏展示
- flash 消息工具、广告与侧边栏组件微调
This commit is contained in:
2026-09-26 03:02:40 +08:00
parent 31f5360c26
commit d7c6b3a961
40 changed files with 1528 additions and 268 deletions

View File

@@ -0,0 +1,80 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";
import { Bookmark } from "lucide-react";
import { apiToggleFavorite } from "@/lib/api";
import { toast } from "@/lib/toast";
interface FavoriteButtonProps {
postId: string;
favorited: boolean;
variant?: "default" | "compact" | "article";
}
export default function FavoriteButton({
postId,
favorited: initialFavorited,
variant = "default",
}: FavoriteButtonProps) {
const router = useRouter();
const pathname = usePathname();
const [favorited, setFavorited] = useState(initialFavorited);
const [loading, setLoading] = useState(false);
// router.refresh()(登录/登出后)服务端会重新下发 favorited 态,同步到本地
useEffect(() => {
setFavorited(initialFavorited);
}, [initialFavorited]);
const handleClick = async () => {
if (loading) return;
setLoading(true);
const wasFavorited = favorited;
setFavorited(!wasFavorited);
try {
const res = await apiToggleFavorite(postId);
if (res.error === "未登录") {
router.push(`/login?redirect=${encodeURIComponent(pathname)}`);
return;
}
if (typeof res.favorited === "boolean") {
setFavorited(res.favorited);
} else {
throw new Error(res.error || "FAIL");
}
} catch {
setFavorited(wasFavorited);
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-[132px]"
: "px-4 py-2 gap-1.5 text-sm";
const iconSize = variant === "compact" ? 13 : variant === "article" ? 18 : 15;
return (
<button
onClick={handleClick}
disabled={loading}
data-tip={favorited ? "取消收藏" : "收藏"}
aria-label={favorited ? "取消收藏" : "收藏"}
aria-pressed={favorited}
className={`inline-flex items-center justify-center tabular-nums rounded-full border transition-colors duration-150 disabled:opacity-50 ${
favorited
? "border-[var(--accent)] text-[var(--accent)] bg-[color-mix(in_srgb,var(--accent)_10%,var(--panel))]"
: "border-[var(--line-2)] text-[var(--ink-2)] hover:border-[var(--accent)] hover:text-[var(--accent)]"
} ${sizeCls}`}
style={!favorited ? { background: "var(--panel)" } : undefined}
>
<Bookmark size={iconSize} fill={favorited ? "currentColor" : "none"} />
{variant === "article" && <span>{favorited ? "已收藏" : "收藏"}</span>}
</button>
);
}