Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/components/articles/article-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,25 @@ import { estimateReadTime } from "#/lib/utils/article-content"
import { Button } from "#/components/ui/button"
import { cn } from "#/lib/utils"
import { Route } from "#/routes/articles.$slug"
import { useQuery } from "@tanstack/react-query"
import { articleKeys } from "#/hooks/keys/article-keys"
import { useBookmarkStatus, useLikeStatus } from "#/hooks/queries"
import { useBookmarkArticle, useUnbookmarkArticle, useLikeArticle, useUnlikeArticle } from "#/hooks/mutations"
import { authClient } from "#/lib/auth-client"

export function ArticleDetailPage() {
const { article } = Route.useLoaderData()
const loaderData = Route.useLoaderData()
const navigate = useNavigate()
const { data: session } = authClient.useSession()

// 使用 useQuery 获取文章数据,以响应实时更新(如点赞计数)
// loader 已将数据放入缓存,所以这里不会重复请求
const { data: article } = useQuery({
queryKey: articleKeys.detail(loaderData.article.id),
initialData: loaderData.article,
staleTime: 1000 * 60 * 5, // 5 分钟内不重新请求
})

// 获取收藏和点赞状态
const { data: bookmarkStatus } = useBookmarkStatus(article.id)
const { data: likeStatus } = useLikeStatus(article.id)
Expand Down
191 changes: 165 additions & 26 deletions src/hooks/mutations/use-like-mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,143 @@ import { likeKeys } from '#/hooks/keys/like-keys'
import { articleKeys } from '#/hooks/keys/article-keys'
import { likeArticleFn, unlikeArticleFn } from '#/data/like'
import type { UseMutationOptions } from '#/hooks/types'
import type { Article } from '#/types/article'
import {
createToggleOnMutate,
createToggleOnError,
createToggleOnSuccess,
createToggleOnSettled,
} from '#/hooks/utils/optimistic-update'

/**
* 乐观更新文章详情中的 likeCount
*/
function updateArticleLikeCount(
queryClient: ReturnType<typeof useQueryClient>,
articleId: string,
delta: number
) {
const articleData = queryClient.getQueryData<Article>(articleKeys.detail(articleId))
if (articleData) {
queryClient.setQueryData(articleKeys.detail(articleId), {
...articleData,
likeCount: articleData.likeCount + delta,
})
}
}

/**
* 乐观更新无限列表中的文章 likeCount
*/
function updateInfiniteArticleLikeCount(
queryClient: ReturnType<typeof useQueryClient>,
articleId: string,
delta: number
) {
const infiniteData = queryClient.getQueryData<{ pages: Array<{ articles: Article[] }> }>(
articleKeys.infinite()
)
if (!infiniteData) return

const updatedPages = infiniteData.pages.map((page) => ({
...page,
articles: page.articles.map((article) =>
article.id === articleId ? { ...article, likeCount: article.likeCount + delta } : article
),
}))

queryClient.setQueryData(articleKeys.infinite(), { ...infiniteData, pages: updatedPages })
}

/**
* 回滚文章详情中的 likeCount
*/
function rollbackArticleLikeCount(
queryClient: ReturnType<typeof useQueryClient>,
articleId: string,
previousCount: number
) {
const articleData = queryClient.getQueryData<Article>(articleKeys.detail(articleId))
if (articleData) {
queryClient.setQueryData(articleKeys.detail(articleId), {
...articleData,
likeCount: previousCount,
})
}
}

/**
* 回滚无限列表中的文章 likeCount
*/
function rollbackInfiniteArticleLikeCount(
queryClient: ReturnType<typeof useQueryClient>,
articleId: string,
previousCount: number
) {
const infiniteData = queryClient.getQueryData<{ pages: Array<{ articles: Article[] }> }>(
articleKeys.infinite()
)
if (!infiniteData) return

const updatedPages = infiniteData.pages.map((page) => ({
...page,
articles: page.articles.map((article) =>
article.id === articleId ? { ...article, likeCount: previousCount } : article
),
}))

queryClient.setQueryData(articleKeys.infinite(), { ...infiniteData, pages: updatedPages })
}

export function useLikeArticle(options?: UseMutationOptions) {
const queryClient = useQueryClient()

return useMutation({
mutationFn: (articleId: string) => likeArticleFn({ data: { articleId } }),
onMutate: createToggleOnMutate<{ isLiked: boolean }>(queryClient, {
statusKey: likeKeys.status,
batchBaseKey: likeKeys.multipleStatusBase,
statusField: 'isLiked',
newStatus: true,
}),
onError: createToggleOnError(queryClient, {
statusKey: likeKeys.status,
batchBaseKey: likeKeys.multipleStatusBase,
statusField: 'isLiked',
options,
}),
onMutate: (articleId: string) => {
// 先执行原有的点赞状态乐观更新
const context = createToggleOnMutate<{ isLiked: boolean }>(queryClient, {
statusKey: likeKeys.status,
batchBaseKey: likeKeys.multipleStatusBase,
statusField: 'isLiked',
newStatus: true,
})(articleId)

// 保存当前文章的 likeCount 用于回滚
const articleData = queryClient.getQueryData<Article>(articleKeys.detail(articleId))
const previousLikeCount = articleData?.likeCount ?? 0

// 乐观增加 likeCount(详情页和无限列表)
updateArticleLikeCount(queryClient, articleId, 1)
updateInfiniteArticleLikeCount(queryClient, articleId, 1)

return { ...context, previousLikeCount }
},
onError: (error: unknown, articleId: string, context: unknown) => {
const ctx = context as { previousStatus?: { isLiked: boolean }; previousLikeCount?: number }

// 回滚点赞状态
createToggleOnError(queryClient, {
statusKey: likeKeys.status,
batchBaseKey: likeKeys.multipleStatusBase,
statusField: 'isLiked',
options,
})(error, articleId, { previousStatus: ctx?.previousStatus })

// 回滚 likeCount(详情页和无限列表)
if (ctx?.previousLikeCount !== undefined) {
rollbackArticleLikeCount(queryClient, articleId, ctx.previousLikeCount)
rollbackInfiniteArticleLikeCount(queryClient, articleId, ctx.previousLikeCount)
}
},
onSuccess: createToggleOnSuccess(queryClient, {
successMessage: '已点赞',
invalidateKeys: (id) => [likeKeys.myLikes(), articleKeys.detail(id), articleKeys.lists()],
invalidateKeys: (id) => [
likeKeys.myLikes(),
articleKeys.detail(id),
articleKeys.lists(),
articleKeys.infinite(),
],
options,
}),
onSettled: createToggleOnSettled(queryClient, {
Expand All @@ -44,21 +154,50 @@ export function useUnlikeArticle(options?: UseMutationOptions) {

return useMutation({
mutationFn: (articleId: string) => unlikeArticleFn({ data: { articleId } }),
onMutate: createToggleOnMutate<{ isLiked: boolean }>(queryClient, {
statusKey: likeKeys.status,
batchBaseKey: likeKeys.multipleStatusBase,
statusField: 'isLiked',
newStatus: false,
}),
onError: createToggleOnError(queryClient, {
statusKey: likeKeys.status,
batchBaseKey: likeKeys.multipleStatusBase,
statusField: 'isLiked',
options,
}),
onMutate: (articleId: string) => {
// 先执行原有的点赞状态乐观更新
const context = createToggleOnMutate<{ isLiked: boolean }>(queryClient, {
statusKey: likeKeys.status,
batchBaseKey: likeKeys.multipleStatusBase,
statusField: 'isLiked',
newStatus: false,
})(articleId)

// 保存当前文章的 likeCount 用于回滚
const articleData = queryClient.getQueryData<Article>(articleKeys.detail(articleId))
const previousLikeCount = articleData?.likeCount ?? 0

// 乐观减少 likeCount(详情页和无限列表)
updateArticleLikeCount(queryClient, articleId, -1)
updateInfiniteArticleLikeCount(queryClient, articleId, -1)

return { ...context, previousLikeCount }
},
onError: (error: unknown, articleId: string, context: unknown) => {
const ctx = context as { previousStatus?: { isLiked: boolean }; previousLikeCount?: number }

// 回滚点赞状态
createToggleOnError(queryClient, {
statusKey: likeKeys.status,
batchBaseKey: likeKeys.multipleStatusBase,
statusField: 'isLiked',
options,
})(error, articleId, { previousStatus: ctx?.previousStatus })

// 回滚 likeCount(详情页和无限列表)
if (ctx?.previousLikeCount !== undefined) {
rollbackArticleLikeCount(queryClient, articleId, ctx.previousLikeCount)
rollbackInfiniteArticleLikeCount(queryClient, articleId, ctx.previousLikeCount)
}
},
onSuccess: createToggleOnSuccess(queryClient, {
successMessage: '已取消点赞',
invalidateKeys: (id) => [likeKeys.myLikes(), articleKeys.detail(id), articleKeys.lists()],
invalidateKeys: (id) => [
likeKeys.myLikes(),
articleKeys.detail(id),
articleKeys.lists(),
articleKeys.infinite(),
],
options,
}),
onSettled: createToggleOnSettled(queryClient, {
Expand Down
8 changes: 7 additions & 1 deletion src/routes/articles.$slug.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { createFileRoute, notFound } from "@tanstack/react-router"
import { getArticleByIdFn } from "#/data/articles"
import { ArticleDetailPage } from "#/components/articles"
import { articleKeys } from "#/hooks/keys/article-keys"

export const Route = createFileRoute("/articles/$slug")({
loader: async ({ params }) => {
loader: async ({ params, context }) => {
const article = await getArticleByIdFn({ data: { id: params.slug } })
if (!article) {
throw notFound()
}

// 将文章数据放入 Query 缓存,以便组件能响应实时更新
const queryClient = context.queryClient
queryClient.setQueryData(articleKeys.detail(article.id), article)

return { article }
},
component: ArticleDetailPage,
Expand Down
Loading