feat: 帖子收藏与用户关注
- 新增收藏功能:favorite 服务与 handler、FavoriteButton 组件 - 新增关注功能:follow 服务与 handler、FollowButton 组件 - 用户主页适配关注/粉丝与收藏展示 - flash 消息工具、广告与侧边栏组件微调
This commit is contained in:
@@ -295,6 +295,7 @@ export interface PublicAdItem {
|
||||
title?: string;
|
||||
text_color?: string;
|
||||
bg_color?: string;
|
||||
ends_at?: string | null;
|
||||
}
|
||||
|
||||
export interface PublicSponsorItem {
|
||||
@@ -392,6 +393,8 @@ export interface Post {
|
||||
view_count: number;
|
||||
comment_count: number;
|
||||
liked: boolean;
|
||||
/** 详情接口返回:当前查看者是否已收藏 */
|
||||
favorited?: boolean;
|
||||
/** 详情接口返回:是否存在编辑历史快照(列表接口无此字段) */
|
||||
edited?: boolean;
|
||||
created_at: string;
|
||||
@@ -584,12 +587,18 @@ export interface UserProfile {
|
||||
total_points?: number;
|
||||
// 徽章墙(全量,含授予时间)
|
||||
badges?: UserBadgeView[];
|
||||
/** 访客(登录态)与该用户的关注关系;本人访问恒 false */
|
||||
is_following?: boolean;
|
||||
/** 仅本人访问时下发的打码邮箱(j***@163.com) */
|
||||
email?: string;
|
||||
};
|
||||
stats: {
|
||||
post_count: number;
|
||||
comment_count: number;
|
||||
points: number;
|
||||
streak: number;
|
||||
favorite_count: number;
|
||||
following_count: number;
|
||||
};
|
||||
posts: PostListItem[];
|
||||
posts_total: number;
|
||||
@@ -612,11 +621,34 @@ export interface UserCommentsResponse {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface UserFavoritesResponse {
|
||||
posts: PostListItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface UserFollowingItem {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
level?: number;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface UserFollowingResponse {
|
||||
users: UserFollowingItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface NotificationItem {
|
||||
id: number;
|
||||
user_id: number;
|
||||
actor_id: number;
|
||||
type: "comment" | "reply" | "like" | "approved" | "rejected" | "mention" | "pending_review" | "deleted" | "badge" | "necro_reply";
|
||||
type: "comment" | "reply" | "like" | "approved" | "rejected" | "mention" | "pending_review" | "deleted" | "badge" | "necro_reply" | "follow";
|
||||
post_id: number;
|
||||
comment_id: number;
|
||||
room_id?: number;
|
||||
@@ -1286,8 +1318,25 @@ export async function fetchComments(
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchUserProfile(id: string, page = 1, cookieHeader?: string): Promise<UserProfile> {
|
||||
const params = new URLSearchParams({ page: String(page), size: "20" });
|
||||
// 评论定位:返回楼层号(与楼层分页口径一致),0 = 不存在。
|
||||
// 通知/主页评论深链 #comment-{id} 跨页时用它换到正确页码。
|
||||
export async function fetchCommentLocation(
|
||||
postId: string | number,
|
||||
commentId: number
|
||||
): Promise<number> {
|
||||
const res = await fetch(`${API_BASE}/api/posts/${postId}/comments/${commentId}`);
|
||||
if (!res.ok) return 0;
|
||||
const data = (await res.json()) as { floor?: number };
|
||||
return data.floor || 0;
|
||||
}
|
||||
|
||||
export async function fetchUserProfile(
|
||||
id: string,
|
||||
page = 1,
|
||||
cookieHeader?: string,
|
||||
sort: string = "new"
|
||||
): Promise<UserProfile> {
|
||||
const params = new URLSearchParams({ page: String(page), size: "20", sort });
|
||||
const res = await fetch(`${API_BASE}/api/users/${id}?${params}`, ssrInit(cookieHeaders(cookieHeader)));
|
||||
if (res.status === 404) throw new Error("用户不存在");
|
||||
if (!res.ok) throw new Error("获取用户资料失败");
|
||||
@@ -1307,9 +1356,11 @@ export async function fetchMe(cookieHeader?: string): Promise<MeResponse> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchUserComments(id: string, page = 1): Promise<UserCommentsResponse> {
|
||||
// cookieHeader:SSR 时必须转发访问者 cookie——后端开启「评论需登录」时,
|
||||
// 匿名请求会拿到空列表(表现为所有人主页回帖都显示暂无评论)
|
||||
export async function fetchUserComments(id: string, page = 1, cookieHeader?: string): Promise<UserCommentsResponse> {
|
||||
const params = new URLSearchParams({ page: String(page), size: "20" });
|
||||
const res = await fetch(`${API_BASE}/api/users/${id}/comments?${params}`, ssrInit());
|
||||
const res = await fetch(`${API_BASE}/api/users/${id}/comments?${params}`, ssrInit(cookieHeaders(cookieHeader)));
|
||||
if (res.status === 404) throw new Error("用户不存在");
|
||||
if (!res.ok) throw new Error("获取评论失败");
|
||||
const data = (await res.json()) as UserCommentsResponse;
|
||||
@@ -1317,6 +1368,24 @@ export async function fetchUserComments(id: string, page = 1): Promise<UserComme
|
||||
return { ...data, comments: data.comments ?? [] };
|
||||
}
|
||||
|
||||
export async function fetchUserFavorites(id: string, page = 1): Promise<UserFavoritesResponse> {
|
||||
const params = new URLSearchParams({ page: String(page), size: "20" });
|
||||
const res = await fetch(`${API_BASE}/api/users/${id}/favorites?${params}`, ssrInit());
|
||||
if (res.status === 404) throw new Error("用户不存在");
|
||||
if (!res.ok) throw new Error("获取收藏失败");
|
||||
const data = (await res.json()) as UserFavoritesResponse;
|
||||
return { ...data, posts: data.posts ?? [] };
|
||||
}
|
||||
|
||||
export async function fetchUserFollowing(id: string, page = 1): Promise<UserFollowingResponse> {
|
||||
const params = new URLSearchParams({ page: String(page), size: "20" });
|
||||
const res = await fetch(`${API_BASE}/api/users/${id}/following?${params}`, ssrInit());
|
||||
if (res.status === 404) throw new Error("用户不存在");
|
||||
if (!res.ok) throw new Error("获取关注列表失败");
|
||||
const data = (await res.json()) as UserFollowingResponse;
|
||||
return { ...data, users: data.users ?? [] };
|
||||
}
|
||||
|
||||
// SSR 直出通知列表(仅登录用户;401 由页面层转为登录引导)
|
||||
export async function fetchNotifications(page = 1, cookieHeader?: string): Promise<NotificationsResponse> {
|
||||
const params = new URLSearchParams({ page: String(page), size: "20" });
|
||||
@@ -2242,6 +2311,22 @@ export async function apiToggleLike(postId: string) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiToggleFavorite(postId: string) {
|
||||
const res = await fetchWithRefresh(`/api/posts/${postId}/favorite`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiToggleFollow(userId: string | number) {
|
||||
const res = await fetchWithRefresh(`/api/users/${userId}/follow`, {
|
||||
method: "POST",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function apiDeleteComment(
|
||||
postId: string,
|
||||
commentId: number,
|
||||
|
||||
Reference in New Issue
Block a user