Files
jiang13-bbs/frontend/components/FollowButton.tsx
freefire d7c6b3a961 feat: 帖子收藏与用户关注
- 新增收藏功能:favorite 服务与 handler、FavoriteButton 组件
- 新增关注功能:follow 服务与 handler、FollowButton 组件
- 用户主页适配关注/粉丝与收藏展示
- flash 消息工具、广告与侧边栏组件微调
2026-09-26 03:02:40 +08:00

78 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { useRouter, usePathname } from "next/navigation";
import { UserPlus, UserCheck } from "lucide-react";
import { apiToggleFollow } from "@/lib/api";
import { toast } from "@/lib/toast";
interface FollowButtonProps {
userId: string | number;
following: boolean;
size?: "default" | "sm";
}
export default function FollowButton({
userId,
following: initialFollowing,
size = "default",
}: FollowButtonProps) {
const router = useRouter();
const pathname = usePathname();
const [following, setFollowing] = useState(initialFollowing);
const [loading, setLoading] = useState(false);
// router.refresh()(登录/登出后)服务端会重新下发关注态,同步到本地
useEffect(() => {
setFollowing(initialFollowing);
}, [initialFollowing]);
const handleClick = async () => {
if (loading) return;
setLoading(true);
const wasFollowing = following;
setFollowing(!wasFollowing);
try {
const res = await apiToggleFollow(userId);
if (res.error === "未登录") {
router.push(`/login?redirect=${encodeURIComponent(pathname)}`);
return;
}
if (typeof res.following === "boolean") {
setFollowing(res.following);
// 关注数变化后刷新页面统计(导航计数、Hero 统计)
router.refresh();
} else {
throw new Error(res.error || "FAIL");
}
} catch {
setFollowing(wasFollowing);
toast("操作失败,请稍后重试");
} finally {
setLoading(false);
}
};
const sizeCls = size === "sm" ? "px-2.5 py-1 gap-1 text-[12px]" : "btn-sm";
const iconSize = size === "sm" ? 12 : 14;
return (
<button
onClick={handleClick}
disabled={loading}
data-tip={following ? "取消关注" : "关注"}
aria-label={following ? "取消关注" : "关注"}
aria-pressed={following}
className={`inline-flex items-center justify-center gap-1.5 rounded-full border font-semibold transition-colors duration-150 disabled:opacity-50 ${
following
? "border-[var(--line-2)] text-[var(--ink-2)] hover:border-[var(--danger)] hover:text-[var(--danger)]"
: "border-[var(--accent)] text-[var(--accent)] hover:bg-[color-mix(in_srgb,var(--accent)_10%,var(--panel))]"
} ${sizeCls}`}
style={{ background: "var(--panel)" }}
>
{following ? <UserCheck size={iconSize} /> : <UserPlus size={iconSize} />}
{following ? "已关注" : "关注"}
</button>
);
}