diff --git a/apps/server/src/models/base/base.service.ts b/apps/server/src/models/base/base.service.ts index 1408bfa..346c314 100644 --- a/apps/server/src/models/base/base.service.ts +++ b/apps/server/src/models/base/base.service.ts @@ -570,6 +570,7 @@ export class BaseService< ): Promise { try { const result = await operationPromise; + return await transformFn(result); } catch (error) { throw error; // Re-throw the error to maintain existing error handling diff --git a/apps/server/src/models/post/post.router.ts b/apps/server/src/models/post/post.router.ts index bcbca1b..e1d1b33 100755 --- a/apps/server/src/models/post/post.router.ts +++ b/apps/server/src/models/post/post.router.ts @@ -70,8 +70,11 @@ export class PostRouter { }), findFirst: this.trpc.procedure .input(PostFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.postService.findFirst(input); + .query(async ({ input, ctx }) => { + const { staff, req } = ctx; + // 从请求中获取 IP + const ip = getClientIp(req); + return await this.postService.findFirst(input, staff, ip); }), deleteMany: this.trpc.protectProcedure .input(PostDeleteManyArgsSchema) @@ -88,8 +91,10 @@ export class PostRouter { }), ) .query(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.postService.findManyWithCursor(input, staff); + const { staff, req } = ctx; + // 从请求中获取 IP + const ip = getClientIp(req); + return await this.postService.findManyWithCursor(input, staff, ip); }), findManyWithPagination: this.trpc.procedure .input(z.object({ diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index ba06d94..5372060 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -61,28 +61,37 @@ export class PostService extends BaseService { }); return result; } - async findFirst(args?: Prisma.PostFindFirstArgs, staff?: UserProfile) { + async findFirst( + args?: Prisma.PostFindFirstArgs, + staff?: UserProfile, + clientIp?: string, + ) { const transDto = await this.wrapResult( super.findFirst(args), async (result) => { - await setPostRelation({ data: result, staff }); + await setPostRelation({ data: result, staff, clientIp }); await this.setPerms(result, staff); return result; }, ); return transDto; } - async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) { + async findManyWithCursor( + args: Prisma.PostFindManyArgs, + staff?: UserProfile, + clientIp?: string, + ) { if (!args.where) args.where = {}; args.where.OR = await this.preFilter(args.where.OR, staff); return this.wrapResult(super.findManyWithCursor(args), async (result) => { const { items } = result; await Promise.all( items.map(async (item) => { - await setPostRelation({ data: item, staff }); + await setPostRelation({ data: item, staff, clientIp }); await this.setPerms(item, staff); }), ); + return { ...result, items }; }); } diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 22dcc30..2f62a69 100644 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -1,6 +1,7 @@ import { db, Post, + PostDto, PostState, PostType, UserProfile, @@ -10,11 +11,12 @@ import { export async function setPostRelation(params: { data: Post; staff?: UserProfile; + clientIp?: string; }) { - const { data, staff } = params; + const { data, staff, clientIp } = params; // 在函数开始时计算一次时间 const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000); - const clientIp = (data?.meta as any)?.ip; + const commentsCount = await db.post.count({ where: { parentId: data.id, @@ -29,23 +31,25 @@ export async function setPostRelation(params: { visitorId: staff?.id, }, })) > 0; - const liked = await db.visit.count({ - where: { - postId: data.id, - type: VisitType?.LIKE, - ...(staff?.id - ? // 如果有 staff,查找对应的 visitorId - { visitorId: staff.id } - : // 如果没有 staff,查找相同 IP 且 visitorId 为 null 且 30 分钟内的记录 - { - visitorId: null, - meta: { path: ['ip'], equals: clientIp }, - updatedAt: { - gte: thirtyMinutesAgo, - }, - }), - }, - }); + + const liked = + (await db.visit.count({ + where: { + postId: data.id, + type: VisitType?.LIKE, + ...(staff?.id + ? // 如果有 staff,查找对应的 visitorId + { visitorId: staff.id } + : // 如果没有 staff,查找相同 IP 且 visitorId 为 null 且 30 分钟内的记录 + { + visitorId: null, + meta: { path: ['ip'], equals: clientIp }, + updatedAt: { + gte: thirtyMinutesAgo, + }, + }), + }, + })) > 0; const readedCount = await db.visit.count({ where: { postId: data.id, @@ -61,6 +65,8 @@ export async function setPostRelation(params: { commentsCount, // trouble }); + // console.log('data', data); + return data; // 明确返回修改后的数据 } export function getClientIp(req: any): string { diff --git a/apps/server/src/models/visit/visit.router.ts b/apps/server/src/models/visit/visit.router.ts index 3dbe053..edf00b9 100644 --- a/apps/server/src/models/visit/visit.router.ts +++ b/apps/server/src/models/visit/visit.router.ts @@ -6,6 +6,7 @@ import { VisitService } from './visit.service'; import { z, ZodType } from 'zod'; import { getClientIp } from './utils'; const VisitCreateArgsSchema: ZodType = z.any(); +const VisitDeleteArgsSchema: ZodType = z.any(); const VisitCreateManyInputSchema: ZodType = z.any(); const VisitDeleteManyArgsSchema: ZodType = z.any(); @@ -41,8 +42,27 @@ export class VisitRouter { }), deleteMany: this.trpc.procedure .input(VisitDeleteManyArgsSchema) - .mutation(async ({ input }) => { - return await this.visitService.deleteMany(input); + .mutation(async ({ ctx, input }) => { + const { staff, req } = ctx; + // 从请求中获取 IP + if (!input?.where?.visitorId) { + const ip = getClientIp(req); + // Create a new where clause + const newWhere = { + ...input.where, + meta: { + path: ['ip'], + equals: ip || '', + }, + }; + + // Update the input with the new where clause + input = { + ...input, + where: newWhere, + }; + } + return await this.visitService.deleteMany(input, staff); }), }); } diff --git a/apps/server/src/models/visit/visit.service.ts b/apps/server/src/models/visit/visit.service.ts index f03c852..9847639 100644 --- a/apps/server/src/models/visit/visit.service.ts +++ b/apps/server/src/models/visit/visit.service.ts @@ -123,4 +123,64 @@ export class VisitService extends BaseService { return { count: updatePromises.length }; // Return the number of updates if no new creates } + async deleteMany(args: Prisma.VisitDeleteManyArgs, staff?: UserProfile) { + // const where = Array.isArray(args.where) ? args.where : [args.where]; + // const updatePromises: any[] = []; + // const createData: Prisma.VisitCreateManyInput[] = []; + // super + // await Promise.all( + // data.map(async (item) => { + // if (staff && !item.visitorId) item.visitorId = staff.id; + // const { postId, messageId, visitorId } = item; + // const existingVisit = await db.visit.findFirst({ + // where: { + // visitorId, + // OR: [{ postId }, { messageId }], + // }, + // }); + + // if (existingVisit) { + // updatePromises.push( + // super.update({ + // where: { id: existingVisit.id }, + // data: { + // ...item, + // views: existingVisit.views + 1, + // }, + // }), + // ); + // } else { + // createData.push(item); + // } + // }), + // ); + // // Execute all updates in parallel + // await Promise.all(updatePromises); + // // Create new visits for those not existing + // if (createData.length > 0) { + // return super.createMany({ + // ...args, + // data: createData, + // }); + // } + // return { count: updatePromises.length }; // Return the number of updates if no new creates + const superDetele = super.deleteMany(args, staff); + if (args?.where?.postId) { + if (args.where.type === VisitType.READED) { + EventBus.emit('updateVisitCount', { + objectType: ObjectType.POST, + id: args?.where?.postId as string, + visitType: VisitType.READED, + }); + } + if (args.where.type === VisitType.LIKE) { + EventBus.emit('updateVisitCount', { + objectType: ObjectType.POST, + id: args?.where?.postId as string, + visitType: VisitType.LIKE, + }); + } + } + return superDetele; + } } diff --git a/apps/server/src/queue/models/post/post.queue.service.ts b/apps/server/src/queue/models/post/post.queue.service.ts index ebce878..5f38856 100644 --- a/apps/server/src/queue/models/post/post.queue.service.ts +++ b/apps/server/src/queue/models/post/post.queue.service.ts @@ -28,13 +28,15 @@ export class PostQueueService implements OnModuleInit { async addUpdateVisitCountJob(data: updateVisitCountJobData) { this.logger.log(`update post view count ${data.id}`); await this.generalQueue.add(QueueJobType.UPDATE_POST_VISIT_COUNT, data, { - debounce: { id: data.id }, + debounce: { + id: `${QueueJobType.UPDATE_POST_VISIT_COUNT}_${data.type}_${data.id}`, + }, }); } async addUpdatePostState(data: updatePostStateJobData) { this.logger.log(`update post state ${data.id}`); await this.generalQueue.add(QueueJobType.UPDATE_POST_STATE, data, { - debounce: { id: data.id }, + debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${data.id}` }, }); } } diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 06da647..6d43be2 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -12,16 +12,17 @@ export async function updatePostViewCount(id: string, type: VisitType) { if (type === VisitType.READED) { await db.post.update({ where: { - id, + id: id, }, data: { views: totalViews._sum.views || 0, // Use 0 if no visits exist }, }); } else if (type === VisitType.LIKE) { + console.log('totalViews._sum.view', totalViews._sum.views); await db.post.update({ where: { - id, + id: id, }, data: { likes: totalViews._sum.views || 0, // Use 0 if no visits exist diff --git a/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx b/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx index 3f489d5..9f44568 100644 --- a/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx +++ b/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx @@ -64,6 +64,7 @@ export function LetterFormProvider({ : undefined, }, }); + // console.log(123); navigate(`/${result.id}/detail`, { replace: true }); toast.success("发送成功!"); form.resetFields(); diff --git a/apps/web/src/components/models/post/detail/PostCommentCard.tsx b/apps/web/src/components/models/post/detail/PostCommentCard.tsx index 306a1fa..3cd2422 100644 --- a/apps/web/src/components/models/post/detail/PostCommentCard.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentCard.tsx @@ -1,30 +1,33 @@ import { PostDto, VisitType } from "@nice/common"; import { motion } from "framer-motion"; import dayjs from "dayjs"; -import { ChatBubbleLeftIcon, HeartIcon } from "@heroicons/react/24/outline"; -import { HeartIcon as HeartIconSolid } from "@heroicons/react/24/solid"; + import { Avatar } from "antd"; import { useVisitor } from "@nice/client"; -import { useContext } from "react"; +import { useContext, useState } from "react"; import { PostDetailContext } from "./context/PostDetailContext"; - +import { LikeFilled, LikeOutlined } from "@ant-design/icons"; export default function PostCommentCard({ post, index, - isReceiverComment + isReceiverComment, }: { post: PostDto; index: number; isReceiverComment: boolean; }) { const { user } = useContext(PostDetailContext); - const { like } = useVisitor(); + const { like, unLike } = useVisitor(); + const [liked, setLiked] = useState(post?.liked || false); + const [likeCount, setLikeCount] = useState(post?.likes || 0); async function likeThisPost() { - if (!post?.liked) { + if (!liked) { try { - await like.mutateAsync({ + setLikeCount((prev) => prev + 1); + setLiked(true); + like.mutateAsync({ data: { visitorId: user?.id || null, postId: post.id, @@ -33,7 +36,19 @@ export default function PostCommentCard({ }); } catch (error) { console.error("Failed to like post:", error); + setLikeCount((prev) => prev - 1); + setLiked(false); } + } else { + setLikeCount((prev) => prev - 1); + setLiked(false); + unLike.mutateAsync({ + where: { + visitorId: user?.id || null, + postId: post.id, + type: VisitType.LIKE, + }, + }); } } return ( @@ -43,27 +58,30 @@ export default function PostCommentCard({
- {!post.author?.avatar && (post.author?.showname || "匿名用户")} + size={40}> + {!post.author?.avatar && + (post.author?.showname || "匿名用户")}
- {isReceiverComment && ( - - 反馈解答 - - )} {post.author?.showname || "匿名用户"} {dayjs(post?.createdAt).format("YYYY-MM-DD HH:mm")} + {isReceiverComment && ( + + 官方回答 + + )}
- {post?.liked ? ( - - ) : ( - - )} - {post?.likes || 0} 有帮助 + ${ + liked + ? "bg-blue-50 text-blue-600" + : "hover:bg-slate-50 text-slate-600" + } transition-colors duration-200`}> + {liked ? : } + {likeCount} 有帮助
diff --git a/apps/web/src/components/models/post/detail/PostCommentList.tsx b/apps/web/src/components/models/post/detail/PostCommentList.tsx index 1da75b8..b0b7767 100644 --- a/apps/web/src/components/models/post/detail/PostCommentList.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentList.tsx @@ -12,12 +12,9 @@ import { motion, AnimatePresence } from "framer-motion"; import PostCommentCard from "./PostCommentCard"; import { useInView } from "react-intersection-observer"; import { LoadingCard } from "@web/src/components/models/post/detail/LoadingCard"; +import { Button } from "antd"; -export default function PostCommentList({ - official = true, -}: { - official?: boolean; -}) { +export default function PostCommentList() { const { post } = useContext(PostDetailContext); const { ref: loadMoreRef, inView } = useInView(); const { postParams } = useVisitor(); @@ -27,56 +24,117 @@ export default function PostCommentList({ [] ); }, [post]); - const params: Prisma.PostFindManyArgs = useMemo(() => { - return { + const officialParams: Prisma.PostFindManyArgs = useMemo( + () => ({ where: { parentId: post?.id, type: PostType.POST_COMMENT, - authorId: official - ? { in: receiverIds } - : { - notIn: receiverIds, - }, + authorId: { in: receiverIds }, }, select: postDetailSelect, - orderBy: [ - { - createdAt: "desc", - }, - ], + orderBy: [{ createdAt: "desc" }], take: 3, - }; - }, [post]); + }), + [post, receiverIds] + ); + + const nonOfficialParams: Prisma.PostFindManyArgs = useMemo( + () => ({ + where: { + parentId: post?.id, + type: PostType.POST_COMMENT, + authorId: { notIn: receiverIds }, + }, + select: postDetailSelect, + orderBy: [{ createdAt: "desc" }], + take: 3, + }), + [post, receiverIds] + ); + const { - data: queryData, - fetchNextPage, - refetch, - isPending, - isFetchingNextPage, - hasNextPage, - isLoading, - isRefetching, - } = api.post.findManyWithCursor.useInfiniteQuery(params, { + data: officialData, + fetchNextPage: fetchNextOfficialPage, + isFetchingNextPage: isFetchingNextOfficialPage, + hasNextPage: hasNextOfficialPage, + isLoading: isOfficialLoading, + } = api.post.findManyWithCursor.useInfiniteQuery(officialParams, { enabled: !!post?.id, getNextPageParam: (lastPage) => lastPage.nextCursor, }); + + const { + data: nonOfficialData, + fetchNextPage: fetchNextNonOfficialPage, + isFetchingNextPage: isFetchingNextNonOfficialPage, + hasNextPage: hasNextNonOfficialPage, + isLoading: isNonOfficialLoading, + } = api.post.findManyWithCursor.useInfiniteQuery(nonOfficialParams, { + enabled: !!post?.id, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }); + + const officialItems = useMemo( + () => + ((officialData?.pages as any)?.flatMap( + (page) => page.items + ) as any) || [], + [officialData] + ); + + const nonOfficialItems = useMemo( + () => + (nonOfficialData?.pages as any)?.flatMap((page) => page.items) || + [], + [nonOfficialData] + ); + + const isLoading = isOfficialLoading || isNonOfficialLoading; useEffect(() => { if (post?.id) { - postParams.addItem(params); + postParams.addItem(officialParams); + postParams.addItem(nonOfficialParams); return () => { - postParams.removeItem(params); + postParams.removeItem(officialParams); + postParams.removeItem(nonOfficialParams); }; } - }, [post, params]); - const items = useMemo(() => { - return queryData?.pages?.flatMap((page: any) => page.items) || []; - }, [queryData]); + }, [post, officialParams, nonOfficialParams]); + + const convertToPostDto = (item: any): PostDto => ({ + ...item, + createdAt: new Date(item.createdAt), + updatedAt: new Date(item.updatedAt), + deletedAt: item.deletedAt ? new Date(item.deletedAt) : new Date(0), + }); + + const items = useMemo(() => { + return [ + ...officialItems.map(convertToPostDto), + ...nonOfficialItems.map(convertToPostDto), + ]; + }, [officialItems, nonOfficialItems]); useEffect(() => { - if (inView && hasNextPage && !isFetchingNextPage) { - fetchNextPage(); + if (inView) { + if (hasNextOfficialPage && !isFetchingNextOfficialPage) { + fetchNextOfficialPage(); + } else if ( + hasNextNonOfficialPage && + !isFetchingNextNonOfficialPage + ) { + fetchNextNonOfficialPage(); + } } - }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + }, [ + inView, + hasNextOfficialPage, + hasNextNonOfficialPage, + isFetchingNextOfficialPage, + isFetchingNextNonOfficialPage, + fetchNextOfficialPage, + fetchNextNonOfficialPage, + ]); if (isLoading) { return ; @@ -119,13 +177,14 @@ export default function PostCommentList({ {/* 加载更多触发器 */}
- {isFetchingNextPage ? ( + {isFetchingNextOfficialPage || isFetchingNextNonOfficialPage ? (
) : ( items.length > 0 && - !hasNextPage && ( + !hasNextOfficialPage && + !hasNextNonOfficialPage && ( {/* Title Section */} - -
-

- {post?.title} -

- + +
{/* First Row - Basic Info */}
{/* Author Info Badge */} - - - - {post?.author?.showname || "匿名用户"} - - + {/* Date Info Badge */} {post?.createdAt && ( - - - - {format( - new Date(post?.createdAt), - "yyyy.MM.dd" - )} - - + )} - {/* Last Updated Badge */} {post?.updatedAt && post.updatedAt !== post.createdAt && ( - - - - 更新于:{" "} - {format( - new Date(post?.updatedAt), - "yyyy.MM.dd" - )} - - + )} - {/* Visibility Status Badge */} - - {post?.isPublic ? ( - - ) : ( - - )} - - {post?.isPublic ? "公开" : "私信"} - - +
- {/* Second Row - Term and Tags */}
{/* Term Badge */} {post?.term?.name && ( - - - - {post.term.name} - - + )} {/* Tags Badges */} - {post?.meta?.tags && post.meta.tags.length > 0 && ( - - {post.meta.tags.map((tag, index) => ( - - - #{tag} - - - ))} - - )} + {post?.meta?.tags && + post.meta.tags.length > 0 && + post.meta.tags.map((tag, index) => ( + + + #{tag} + + + ))}
@@ -176,44 +137,12 @@ export default function PostHeader() { {/* Stats Section */} - - - - - {post?.likes || 0} 有帮助 - - - - - - {post?.views || 0} 浏览 - - - - - - {post?.commentsCount || 0} 回复 - - - + ); } diff --git a/apps/web/src/components/models/post/detail/PostHeader/InfoBadge.tsx b/apps/web/src/components/models/post/detail/PostHeader/InfoBadge.tsx new file mode 100644 index 0000000..6a1931d --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostHeader/InfoBadge.tsx @@ -0,0 +1,85 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { + UserCircleIcon, + CalendarIcon, + ClockIcon, + LockClosedIcon, + LockOpenIcon, + StarIcon, +} from "@heroicons/react/24/outline"; +import dayjs from "dayjs"; + +interface InfoBadgeProps { + icon: React.ReactNode; + text: string; + delay?: number; +} + +export function InfoBadge({ icon, text, delay = 0 }: InfoBadgeProps) { + return ( + + {icon} + {text} + + ); +} + +export function AuthorBadge({ name }: { name: string }) { + return ( + } + text={name} + /> + ); +} + +export function DateBadge({ date, label }: { date: string; label: string }) { + return ( + } + text={`${label}: ${dayjs(date).format("YYYY-MM-DD")}`} + /> + ); +} + +export function UpdatedBadge({ date }: { date: string }) { + return ( + } + text={`更新于: ${dayjs(date).format("YYYY-MM-DD")}`} + delay={0.45} + /> + ); +} + +export function VisibilityBadge({ isPublic }: { isPublic: boolean }) { + return ( + + ) : ( + + ) + } + text={isPublic ? "公开" : "私信"} + delay={0.5} + /> + ); +} + +export function TermBadge({ term }: { term: string }) { + return ( + } + text={term} + delay={0.55} + /> + ); +} diff --git a/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx new file mode 100644 index 0000000..ecf9232 --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { LikeFilled, LikeOutlined, EyeOutlined, CommentOutlined } from "@ant-design/icons"; + +interface StatsButtonProps { + icon: React.ReactNode; + text: string; + onClick?: () => void; + isActive?: boolean; +} + +export function StatsButton({ icon, text, onClick, isActive }: StatsButtonProps) { + return ( + + {icon} + {text} + + ); +} + +interface StatsSectionProps { + likes: number; + views: number; + commentsCount: number; + liked: boolean; + onLikeClick: () => void; +} + +export function StatsSection({ + likes, + views, + commentsCount, + liked, + onLikeClick, +}: StatsSectionProps) { + return ( + + : } + text={`${likes} 有帮助`} + onClick={onLikeClick} + isActive={liked} + /> + } + text={`${views} 浏览`} + /> + } + text={`${commentsCount} 回复`} + /> + + ); +} diff --git a/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx b/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx new file mode 100644 index 0000000..e8d5d70 --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx @@ -0,0 +1,66 @@ +import { motion } from "framer-motion"; +import { Tag } from "antd"; +import { PostState } from "@nice/common"; +import { + ClockIcon, + CheckCircleIcon, + ExclamationCircleIcon, +} from "@heroicons/react/24/outline"; + +interface TitleSectionProps { + title: string; + state: PostState; +} + +const stateColors = { + [PostState.PENDING]: "orange", + [PostState.PROCESSING]: "blue", + [PostState.COMPLETED]: "green", +}; + +const stateLabels = { + [PostState.PENDING]: "待处理", + [PostState.PROCESSING]: "处理中", + [PostState.COMPLETED]: "已完成", +}; + +export function TitleSection({ title, state }: TitleSectionProps) { + return ( + + {/* Decorative Line */} +
+ + {/* Title */} +

+ {title} +

+ + {/* State Tag */} + {/* */} + + {state === PostState.PENDING && ( + + )} + {state === PostState.PROCESSING && ( + + )} + {state === PostState.COMPLETED && ( + + )} + {stateLabels[state]} + + {/* */} + + ); +} diff --git a/packages/client/src/api/hooks/useVisitor.ts b/packages/client/src/api/hooks/useVisitor.ts index 2730e68..2ff19ab 100644 --- a/packages/client/src/api/hooks/useVisitor.ts +++ b/packages/client/src/api/hooks/useVisitor.ts @@ -110,6 +110,13 @@ export function useVisitor() { liked: true, })) ); + const unLike = api.visitor.deleteMany.useMutation( + createOptimisticMutation((item) => ({ + ...item, + likes: item.likes - 1 || 0, + liked: false, + })) + ); const addStar = api.visitor.create.useMutation( createOptimisticMutation((item) => ({ @@ -148,5 +155,6 @@ export function useVisitor() { addStar, deleteStar, like, + unLike, }; } diff --git a/packages/common/src/select.ts b/packages/common/src/select.ts index 6a0692e..b9d2045 100644 --- a/packages/common/src/select.ts +++ b/packages/common/src/select.ts @@ -4,6 +4,7 @@ export const postDetailSelect: Prisma.PostSelect = { id: true, type: true, title: true, + state: true, content: true, views: true, likes: true, @@ -17,6 +18,7 @@ export const postDetailSelect: Prisma.PostSelect = { taxonomy: true, }, }, + authorId: true, author: { select: { id: true,