diff --git a/app/(main)/post/[id]/page.tsx b/app/(main)/post/[id]/page.tsx
index 251041d..628d5e8 100644
--- a/app/(main)/post/[id]/page.tsx
+++ b/app/(main)/post/[id]/page.tsx
@@ -1,14 +1,15 @@
-import { ArrowLeft, MessageCircle, Pin, Star } from "lucide-react"
+import { ArrowLeft, Pin, Star } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { notFound } from "next/navigation"
+import { PostCommentCount } from "@/components/post-comment-count"
import { PostComments } from "@/components/post-comments"
import { PostLikeButton } from "@/components/post-like-button"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
-import { getOrderById, getPlayerById, getPostById, listCommentsByPost } from "@/lib/api"
+import { getOrderById, getPlayerById, getPostById } from "@/lib/api"
import { roleLabels } from "@/lib/constants"
export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
@@ -16,7 +17,6 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
const post = getPostById(id)
if (!post) notFound()
- const comments = listCommentsByPost(id)
const linkedOrder = post.linkedOrderId ? getOrderById(post.linkedOrderId) : null
const linkedPlayer = linkedOrder ? getPlayerById(linkedOrder.playerId) : null
@@ -91,18 +91,15 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
)}
@@ -32,20 +33,8 @@ export function PostComments({ initialComments, postId }: PostCommentsProps) {
requireAuth(() => {
const nextContent = content.trim()
if (!nextContent) return
- if (!user) return
- const createdAt = new Date().toISOString()
- setComments((prev) => [
- ...prev,
- {
- id: generateId(`comment-${postId}`),
- postId,
- author: user,
- content: nextContent,
- likeCount: 0,
- liked: false,
- createdAt,
- },
- ])
+ const decision = addComment(postId, nextContent)
+ if (!decision.ok) return
setContent("")
})
}}
@@ -85,19 +74,7 @@ export function PostComments({ initialComments, postId }: PostCommentsProps) {
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground mt-1 transition-colors"
onClick={() =>
requireAuth(() => {
- setComments((prev) =>
- prev.map((item) => {
- if (item.id !== comment.id) return item
- const nextLiked = !item.liked
- return {
- ...item,
- liked: nextLiked,
- likeCount: nextLiked
- ? item.likeCount + 1
- : Math.max(0, item.likeCount - 1),
- }
- }),
- )
+ toggleCommentLike(comment.id)
})
}
>
diff --git a/components/post-like-button.tsx b/components/post-like-button.tsx
index 2d7a4d4..efd7345 100644
--- a/components/post-like-button.tsx
+++ b/components/post-like-button.tsx
@@ -1,18 +1,21 @@
"use client"
import { Heart } from "lucide-react"
-import { useState } from "react"
+import { togglePostLike } from "@/lib/api/posts"
import { useRequireAuth } from "@/lib/use-require-auth"
+import { usePostStore } from "@/store/posts"
interface PostLikeButtonProps {
- initialLiked: boolean
- initialCount: number
+ postId: string
}
-export function PostLikeButton({ initialLiked, initialCount }: PostLikeButtonProps) {
+export function PostLikeButton({ postId }: PostLikeButtonProps) {
const { requireAuth } = useRequireAuth()
- const [liked, setLiked] = useState(initialLiked)
- const [count, setCount] = useState(initialCount)
+ const post = usePostStore((state) => state.posts.find((item) => item.id === postId))
+
+ if (!post) {
+ return null
+ }
return (
)
}
diff --git a/lib/api/comments.ts b/lib/api/comments.ts
index e077791..a7dce89 100644
--- a/lib/api/comments.ts
+++ b/lib/api/comments.ts
@@ -1,9 +1,56 @@
-import { mockComments } from "@/lib/mock"
+import { allow, deny } from "@/lib/policy/assert"
+import { addNotification } from "@/lib/api/notifications"
+import { useAuthStore } from "@/store/auth"
+import { useCommentStore } from "@/store/comments"
+import { usePostStore } from "@/store/posts"
export function listComments() {
- return mockComments
+ return useCommentStore.getState().comments
}
export function listCommentsByPost(postId: string) {
- return mockComments.filter((comment) => comment.postId === postId)
+ return useCommentStore.getState().comments.filter((comment) => comment.postId === postId)
+}
+
+export function addComment(postId: string, content: string) {
+ const user = useAuthStore.getState().user
+ if (!user) {
+ return deny("AUTH_REQUIRED", "请先登录")
+ }
+
+ const post = usePostStore.getState().posts.find((item) => item.id === postId)
+ if (!post) {
+ return deny("NOT_FOUND", "帖子不存在")
+ }
+
+ const comment = useCommentStore.getState().addComment(postId, user, content)
+ if (!comment) {
+ return deny("VALIDATION_FAILED", "评论内容不能为空")
+ }
+
+ usePostStore.getState().incrementCommentCount(postId)
+
+ addNotification({
+ type: "community",
+ title: "帖子收到新评论",
+ content: `《${post.title}》有新的评论`,
+ link: `/post/${post.id}`,
+ })
+
+ return allow()
+}
+
+export function toggleCommentLike(commentId: string) {
+ const user = useAuthStore.getState().user
+ if (!user) {
+ return deny("AUTH_REQUIRED", "请先登录")
+ }
+
+ const comment = useCommentStore.getState().comments.find((item) => item.id === commentId)
+ if (!comment) {
+ return deny("NOT_FOUND", "评论不存在")
+ }
+
+ useCommentStore.getState().toggleCommentLike(commentId)
+ return allow()
}
diff --git a/lib/api/index.ts b/lib/api/index.ts
index 017f43e..4dffa3a 100644
--- a/lib/api/index.ts
+++ b/lib/api/index.ts
@@ -1,13 +1,13 @@
export { getChatSessionById, listChatMessages, listChatSessions } from "./chat"
export { requestWithAuth } from "./client"
-export { listComments, listCommentsByPost } from "./comments"
+export { addComment, listComments, listCommentsByPost, toggleCommentLike } from "./comments"
export { getDisputeByOrderId, listDisputes } from "./disputes"
export { isFavorited, listFavorites, listFavoritesByUser } from "./favorites"
export { getGameById, listGames } from "./games"
-export { listNotifications } from "./notifications"
+export { addNotification, listNotifications, markNotificationAsRead } from "./notifications"
export { getOrderById, listOrders, listOrdersByConsumer } from "./orders"
export { getPlayerById, listPlayers, listPlayersByShop } from "./players"
-export { getPostById, listPosts, listPostsByAuthor } from "./posts"
+export { getPostById, listPosts, listPostsByAuthor, togglePostLike } from "./posts"
export { listReviews, listReviewsByOrder, listReviewsByTargetUser } from "./reviews"
export { getServiceById, listServices, listServicesByPlayer } from "./services"
export { getShopById, getShopByOwnerId, listShops } from "./shops"
diff --git a/lib/api/posts.ts b/lib/api/posts.ts
index 7319158..fe1649b 100644
--- a/lib/api/posts.ts
+++ b/lib/api/posts.ts
@@ -1,3 +1,6 @@
+import { allow, deny } from "@/lib/policy/assert"
+import { addNotification } from "@/lib/api/notifications"
+import { useAuthStore } from "@/store/auth"
import { usePostStore } from "@/store/posts"
export function listPosts() {
@@ -11,3 +14,30 @@ export function getPostById(postId: string) {
export function listPostsByAuthor(userId: string) {
return usePostStore.getState().posts.filter((post) => post.author.id === userId)
}
+
+export function togglePostLike(postId: string) {
+ const user = useAuthStore.getState().user
+ if (!user) {
+ return deny("AUTH_REQUIRED", "请先登录")
+ }
+
+ const post = usePostStore.getState().posts.find((item) => item.id === postId)
+ if (!post) {
+ return deny("NOT_FOUND", "帖子不存在")
+ }
+
+ const shouldNotify = !post.liked
+
+ usePostStore.getState().togglePostLike(postId)
+
+ if (shouldNotify) {
+ addNotification({
+ type: "community",
+ title: "帖子收到点赞",
+ content: `《${post.title}》有新的点赞`,
+ link: `/post/${post.id}`,
+ })
+ }
+
+ return allow()
+}
diff --git a/store/comments.ts b/store/comments.ts
new file mode 100644
index 0000000..72b4af2
--- /dev/null
+++ b/store/comments.ts
@@ -0,0 +1,47 @@
+import { create } from "zustand"
+import { generateId } from "@/lib/id"
+import { mockComments } from "@/lib/mock"
+import type { Comment, User } from "@/lib/types"
+
+interface CommentState {
+ comments: Comment[]
+ addComment: (postId: string, author: User, content: string) => Comment | null
+ toggleCommentLike: (commentId: string) => void
+}
+
+export const useCommentStore = create
((set) => ({
+ comments: mockComments,
+ addComment: (postId, author, content) => {
+ const normalizedContent = content.trim()
+ if (!normalizedContent) return null
+
+ const comment: Comment = {
+ id: generateId("comment"),
+ postId,
+ author,
+ content: normalizedContent,
+ likeCount: 0,
+ liked: false,
+ createdAt: new Date().toISOString(),
+ }
+
+ set((state) => ({
+ comments: [...state.comments, comment],
+ }))
+
+ return comment
+ },
+ toggleCommentLike: (commentId) => {
+ set((state) => ({
+ comments: state.comments.map((comment) => {
+ if (comment.id !== commentId) return comment
+ const liked = !comment.liked
+ return {
+ ...comment,
+ liked,
+ likeCount: liked ? comment.likeCount + 1 : Math.max(0, comment.likeCount - 1),
+ }
+ }),
+ }))
+ },
+}))
diff --git a/store/posts.ts b/store/posts.ts
index c1357a1..b414383 100644
--- a/store/posts.ts
+++ b/store/posts.ts
@@ -17,6 +17,8 @@ interface CreatePostInput {
interface PostState {
posts: Post[]
createPost: (input: CreatePostInput) => Post
+ togglePostLike: (postId: string) => void
+ incrementCommentCount: (postId: string) => void
}
export const usePostStore = create((set) => ({
@@ -45,4 +47,22 @@ export const usePostStore = create((set) => ({
return post
},
+ togglePostLike: (postId) =>
+ set((state) => ({
+ posts: state.posts.map((post) => {
+ if (post.id !== postId) return post
+ const liked = !post.liked
+ return {
+ ...post,
+ liked,
+ likeCount: liked ? post.likeCount + 1 : Math.max(0, post.likeCount - 1),
+ }
+ }),
+ })),
+ incrementCommentCount: (postId) =>
+ set((state) => ({
+ posts: state.posts.map((post) =>
+ post.id === postId ? { ...post, commentCount: post.commentCount + 1 } : post,
+ ),
+ })),
}))