From e646b204568645edd2aa7a95419d9f2bed03215e Mon Sep 17 00:00:00 2001 From: tone <3341154833@qq.com> Date: Sat, 7 Jun 2025 15:19:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=8D=9A=E5=AE=A2=E8=AF=84?= =?UTF-8?q?=E8=AE=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tone-page-server/src/auth/auth.module.ts | 3 ++ .../src/auth/strategies/OptionalAuthGuard.ts | 22 ++++++++ tone-page-server/src/blog/blog.controller.ts | 16 ++++-- tone-page-server/src/blog/blog.module.ts | 4 +- tone-page-server/src/blog/blog.service.ts | 1 + .../src/blog/dto/create.blogcomment.dto.ts | 10 ++++ .../blog/[id]/components/BlogCommentTool.tsx | 52 ++++++++++++++++--- .../blog/[id]/components/BlogComments.tsx | 31 +++++++---- tone-page-web/lib/api/blog/createComment.ts | 7 ++- 9 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 tone-page-server/src/auth/strategies/OptionalAuthGuard.ts create mode 100644 tone-page-server/src/blog/dto/create.blogcomment.dto.ts diff --git a/tone-page-server/src/auth/auth.module.ts b/tone-page-server/src/auth/auth.module.ts index d061946..1ab0df8 100644 --- a/tone-page-server/src/auth/auth.module.ts +++ b/tone-page-server/src/auth/auth.module.ts @@ -9,6 +9,7 @@ import { PassportModule } from '@nestjs/passport'; import { JwtStrategy } from './strategies/jwt.strategy'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { VerificationModule } from 'src/verification/verification.module'; +import { OptionalAuthGuard } from './strategies/OptionalAuthGuard'; @Module({ imports: [ @@ -32,11 +33,13 @@ import { VerificationModule } from 'src/verification/verification.module'; providers: [ AuthService, JwtStrategy, + OptionalAuthGuard, ], exports: [ PassportModule, JwtStrategy, AuthService, + OptionalAuthGuard, ] }) export class AuthModule { } diff --git a/tone-page-server/src/auth/strategies/OptionalAuthGuard.ts b/tone-page-server/src/auth/strategies/OptionalAuthGuard.ts new file mode 100644 index 0000000..863232e --- /dev/null +++ b/tone-page-server/src/auth/strategies/OptionalAuthGuard.ts @@ -0,0 +1,22 @@ +import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common"; +import { AuthGuard } from "@nestjs/passport"; +import { Observable, retry } from "rxjs"; + +@Injectable() +export class OptionalAuthGuard extends AuthGuard('jwt') implements CanActivate { + async canActivate(context: ExecutionContext): Promise { + try { + await super.canActivate(context); + return true; + } catch (error) { + return true;// 如果验证失败,仍然允许访问 + } + } + + handleRequest(err: any, user: any, info: any, context: ExecutionContext, status?: any): TUser { + if (err || !user) { + return null; // 如果没有用户信息,返回null + } + return user; // 如果有用户信息,返回用户对象 + } +} \ No newline at end of file diff --git a/tone-page-server/src/blog/blog.controller.ts b/tone-page-server/src/blog/blog.controller.ts index da867ca..f7a0c95 100644 --- a/tone-page-server/src/blog/blog.controller.ts +++ b/tone-page-server/src/blog/blog.controller.ts @@ -1,11 +1,15 @@ -import { BadRequestException, Body, Controller, Get, Param, ParseUUIDPipe, Post } from '@nestjs/common'; +import { BadRequestException, Body, Controller, Get, Param, ParseUUIDPipe, Post, Req, Request, UseGuards } from '@nestjs/common'; import { BlogService } from './blog.service'; +import { OptionalAuthGuard } from 'src/auth/strategies/OptionalAuthGuard'; +import { UserService } from 'src/user/user.service'; +import { createBlogCommentDto } from './dto/create.blogcomment.dto'; @Controller('blog') export class BlogController { constructor( private readonly blogService: BlogService, + private readonly userService: UserService, ) { } @Get() @@ -42,18 +46,24 @@ export class BlogController { return await this.blogService.getComments(id); } - // TODO:鉴权,该接口允许匿名评论,但仍需验证userId合法性 + // 该接口允许匿名评论,但仍需验证userId合法性 + @UseGuards(OptionalAuthGuard) @Post(':id/comment') async createBlogComment( @Param('id', new ParseUUIDPipe({ version: '4' })) id: string, - @Body() commentData: { content: string }, + @Body() commentData: createBlogCommentDto, + @Request() req, ) { + const { userId } = req.user || {}; const blog = await this.blogService.findById(id); if (!blog) throw new BadRequestException('文章不存在'); + let user = userId ? await this.userService.findOne({ userId }) : null; + const comment = { ...commentData, blogId: id, + user: user, }; return await this.blogService.createComment(comment); diff --git a/tone-page-server/src/blog/blog.module.ts b/tone-page-server/src/blog/blog.module.ts index 65620cd..00f2eda 100644 --- a/tone-page-server/src/blog/blog.module.ts +++ b/tone-page-server/src/blog/blog.module.ts @@ -4,9 +4,11 @@ import { BlogService } from './blog.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Blog } from './entity/Blog.entity'; import { BlogComment } from './entity/BlogComment'; +import { AuthModule } from 'src/auth/auth.module'; +import { UserModule } from 'src/user/user.module'; @Module({ - imports: [TypeOrmModule.forFeature([Blog, BlogComment])], + imports: [TypeOrmModule.forFeature([Blog, BlogComment]), AuthModule, UserModule], controllers: [BlogController], providers: [BlogService], exports: [BlogService], diff --git a/tone-page-server/src/blog/blog.service.ts b/tone-page-server/src/blog/blog.service.ts index d8cbb8f..189a488 100644 --- a/tone-page-server/src/blog/blog.service.ts +++ b/tone-page-server/src/blog/blog.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Blog } from './entity/Blog.entity'; import { Repository } from 'typeorm'; import { BlogComment } from './entity/BlogComment'; +import { UserService } from 'src/user/user.service'; @Injectable() export class BlogService { diff --git a/tone-page-server/src/blog/dto/create.blogcomment.dto.ts b/tone-page-server/src/blog/dto/create.blogcomment.dto.ts new file mode 100644 index 0000000..b2c65e8 --- /dev/null +++ b/tone-page-server/src/blog/dto/create.blogcomment.dto.ts @@ -0,0 +1,10 @@ +import { IsOptional, IsString, IsUUID } from "class-validator"; + +export class createBlogCommentDto { + @IsString({ message: '评论内容不能为空' }) + content: string; + + @IsOptional() + @IsUUID('4', { message: '父评论ID格式错误' }) + parentId?: string; +} \ No newline at end of file diff --git a/tone-page-web/app/(with-header-footer)/blog/[id]/components/BlogCommentTool.tsx b/tone-page-web/app/(with-header-footer)/blog/[id]/components/BlogCommentTool.tsx index 46f01e3..02e6b77 100644 --- a/tone-page-web/app/(with-header-footer)/blog/[id]/components/BlogCommentTool.tsx +++ b/tone-page-web/app/(with-header-footer)/blog/[id]/components/BlogCommentTool.tsx @@ -4,29 +4,69 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { BlogApi } from "@/lib/api"; import { BlogComment } from "@/lib/types/blogComment"; -import { Send } from "lucide-react"; -import { useState } from "react"; +import { Send, Undo2 } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -export function BlogCommentTool({ blogId, onInsertComment }: { blogId: string, onInsertComment: (b: BlogComment) => void }) { +interface BlogCommentToolProps { + blogId: string; + onInsertComment: (b: BlogComment) => void; + replayTarget: BlogComment | null; + handleClearReplayTarget: () => void; +} +export function BlogCommentTool({ blogId, onInsertComment, replayTarget, handleClearReplayTarget }: BlogCommentToolProps) { const [comment, setComment] = useState(''); + const textareaRef = useRef(null); + + useEffect(() => { + if (replayTarget && textareaRef.current) { + textareaRef.current.focus(); + textareaRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'start' + }) + } + }, [replayTarget]); + const submit = async () => { if (comment.trim().length === 0) return; - const res = await BlogApi.createComment(blogId, comment); + const res = await BlogApi.createComment(blogId, comment, replayTarget ? replayTarget.id : undefined); if (res) { toast.success('发布成功'); setComment(''); onInsertComment(res); + handleClearReplayTarget(); } } + const getPlaceHolderText = () => { + if (!replayTarget) return '评论'; + + let replayComment = replayTarget.content.trim(); + if (replayComment.length > 8) { + replayComment = replayComment.slice(0, 8) + '...'; + } + + const replayUser = replayTarget.user ? replayTarget.user.nickname : '匿名'; + + return `回复 ${replayUser} 的 ${replayComment}`; + } + return (
-