初步完成评论
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { BadRequestException, Controller, Get, Param, ParseUUIDPipe } from '@nestjs/common';
|
||||
import { BadRequestException, Body, Controller, Get, Param, ParseUUIDPipe, Post } from '@nestjs/common';
|
||||
import { BlogService } from './blog.service';
|
||||
|
||||
@Controller('blog')
|
||||
@@ -31,4 +31,31 @@ export class BlogController {
|
||||
content: blogContent,
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':id/comments')
|
||||
async getBlogComments(
|
||||
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
|
||||
) {
|
||||
const blog = await this.blogService.findById(id);
|
||||
if (!blog) throw new BadRequestException('文章不存在');
|
||||
|
||||
return await this.blogService.getComments(id);
|
||||
}
|
||||
|
||||
// TODO:鉴权,该接口允许匿名评论,但仍需验证userId合法性
|
||||
@Post(':id/comment')
|
||||
async createBlogComment(
|
||||
@Param('id', new ParseUUIDPipe({ version: '4' })) id: string,
|
||||
@Body() commentData: { content: string },
|
||||
) {
|
||||
const blog = await this.blogService.findById(id);
|
||||
if (!blog) throw new BadRequestException('文章不存在');
|
||||
|
||||
const comment = {
|
||||
...commentData,
|
||||
blogId: id,
|
||||
};
|
||||
|
||||
return await this.blogService.createComment(comment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { BlogController } from './blog.controller';
|
||||
import { BlogService } from './blog.service';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Blog } from './entity/Blog.entity';
|
||||
import { BlogComment } from './entity/BlogComment';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Blog])],
|
||||
imports: [TypeOrmModule.forFeature([Blog, BlogComment])],
|
||||
controllers: [BlogController],
|
||||
providers: [BlogService],
|
||||
exports: [BlogService],
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Blog } from './entity/Blog.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BlogComment } from './entity/BlogComment';
|
||||
|
||||
@Injectable()
|
||||
export class BlogService {
|
||||
@@ -9,6 +10,8 @@ export class BlogService {
|
||||
constructor(
|
||||
@InjectRepository(Blog)
|
||||
private readonly blogRepository: Repository<Blog>,
|
||||
@InjectRepository(BlogComment)
|
||||
private readonly blogCommentRepository: Repository<BlogComment>,
|
||||
) { }
|
||||
|
||||
async list() {
|
||||
@@ -43,4 +46,19 @@ export class BlogService {
|
||||
async incrementViewCount(id: string) {
|
||||
await this.blogRepository.increment({ id }, 'viewCount', 1);
|
||||
}
|
||||
|
||||
async getComments(id: string) {
|
||||
return this.blogCommentRepository.find({
|
||||
where: { blogId: id },
|
||||
relations: ['user'],
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async createComment(comment: Partial<BlogComment>) {
|
||||
const newComment = this.blogCommentRepository.create(comment);
|
||||
return this.blogCommentRepository.save(newComment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Column, CreateDateColumn, DeleteDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
|
||||
import { BlogComment } from "./BlogComment";
|
||||
|
||||
@Entity()
|
||||
export class Blog {
|
||||
@@ -27,4 +28,8 @@ export class Blog {
|
||||
deletedAt: Date;
|
||||
|
||||
// 权限关系 TODO
|
||||
|
||||
// 关系
|
||||
@OneToMany(() => BlogComment, blog => blog.id)
|
||||
comments: BlogComment[];
|
||||
}
|
||||
33
tone-page-server/src/blog/entity/BlogComment.ts
Normal file
33
tone-page-server/src/blog/entity/BlogComment.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { User } from "src/user/entities/user.entity";
|
||||
import { Column, CreateDateColumn, DeleteDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity()
|
||||
export class BlogComment {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
content: string;
|
||||
|
||||
// @Column()
|
||||
// ip: string;
|
||||
|
||||
// @Column()
|
||||
// address: string;
|
||||
|
||||
@CreateDateColumn({ precision: 3 })
|
||||
createdAt: Date;
|
||||
|
||||
@DeleteDateColumn({ precision: 3, nullable: true })
|
||||
deletedAt: Date;
|
||||
|
||||
@ManyToOne(() => User, { nullable: true })
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
blogId: string | null;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
parentId: string | null;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { BlogApi } from "@/lib/api";
|
||||
import { Send } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function BlogCommentTool({ blogId }: { blogId: string }) {
|
||||
|
||||
const [comment, setComment] = useState('');
|
||||
const submit = async () => {
|
||||
const res = await BlogApi.createComment(blogId, comment);
|
||||
if (res) {
|
||||
toast.success('发布成功');
|
||||
setComment('');
|
||||
// 提交界面刷新
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="my-3 flex items-end gap-2">
|
||||
<Textarea placeholder="评论" onChange={v => setComment(v.target.value)} value={comment} />
|
||||
<Button variant='outline' size='icon' onClick={() => submit()}>
|
||||
<Send />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import useSWR from "swr";
|
||||
import { BlogCommentTool } from "./BlogCommentTool";
|
||||
import { BlogApi } from "@/lib/api";
|
||||
|
||||
export function BlogComments({ blogId }: { blogId: string }) {
|
||||
const { data, isLoading, error } = useSWR(
|
||||
`/api/blog/${blogId}/comments`,
|
||||
() => BlogApi.getComments(blogId),
|
||||
)
|
||||
|
||||
return (
|
||||
data && <div className="" >
|
||||
<h1 className="px-2 border-l-4 border-zinc-300">评论 {data.length}</h1>
|
||||
<BlogCommentTool blogId={blogId} />
|
||||
<div className="flex flex-col gap-3">
|
||||
{
|
||||
data.filter(d => !d.parentId)
|
||||
.map(d => (
|
||||
<div key={d.id}>
|
||||
<h1 className="text-zinc-500">{d.user ? d.user : '匿名'}</h1>
|
||||
<div>{d.content}</div>
|
||||
<div className="text-xs text-zinc-500 flex gap-2">
|
||||
<p>{new Date(d.createdAt).toLocaleString()}</p>
|
||||
<p>未知</p>
|
||||
<p className="text-zinc-900 cursor-pointer">回复</p>
|
||||
</div>
|
||||
{
|
||||
data.filter(c => c.parentId === d.id).length > 0 && (
|
||||
<div className="flex flex-col gap-3 ml-5 my-1">
|
||||
{
|
||||
data.filter(c => c.parentId === d.id).map(c => (
|
||||
<div key={c.id}>
|
||||
<h1 className="text-zinc-500">{c.user ? c.user : '匿名'}</h1>
|
||||
<div>{c.content}</div>
|
||||
<p className="text-xs text-zinc-500 flex gap-2">
|
||||
<p>{new Date().toLocaleString()}</p>
|
||||
<p>未知</p>
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div >
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { PhotoProvider, PhotoView } from 'react-photo-view';
|
||||
import 'react-photo-view/dist/react-photo-view.css';
|
||||
import rehypeRaw from 'rehype-raw'
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { BlogComments } from "./components/BlogComments";
|
||||
|
||||
export default function Blog() {
|
||||
const params = useParams();
|
||||
@@ -74,6 +75,13 @@ export default function Blog() {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{data && (
|
||||
<>
|
||||
<div className="border my-5"></div>
|
||||
<BlogComments blogId={data.id} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
15
tone-page-web/lib/api/blog/createComment.ts
Normal file
15
tone-page-web/lib/api/blog/createComment.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import fetcher from "../fetcher";
|
||||
|
||||
export async function createComment(blogId: string, content: string) {
|
||||
return fetcher<{
|
||||
blogId: string;
|
||||
content: string;
|
||||
createdAt: string
|
||||
deletedAt: null; // 原则上能看到就是null
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
}>(`/api/blog/${blogId}/comment`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
}
|
||||
13
tone-page-web/lib/api/blog/getComments.ts
Normal file
13
tone-page-web/lib/api/blog/getComments.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import fetcher from "../fetcher";
|
||||
|
||||
export async function getComments(blogId: string) {
|
||||
return fetcher<{
|
||||
blogId: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
deletedAt: string | null;// 原则上能看到就是null,
|
||||
id: string;
|
||||
parentId: string | null; // 如果是回复,则有parentId
|
||||
user: null;// TODO需要完善
|
||||
}[]>(`/api/blog/${blogId}/comments`, { method: 'GET' });
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './list';
|
||||
export * from './get';
|
||||
export * from './get';
|
||||
export * from './getComments';
|
||||
export * from './createComment';
|
||||
@@ -3,4 +3,5 @@ export * as verificationApi from './verification/index';
|
||||
export * as AdminApi from './admin/index';
|
||||
export * as ResourceApi from './resource/index';
|
||||
export * as BlogApi from './blog/index';
|
||||
export * as UserApi from './user/index';
|
||||
export * as UserApi from './user/index';
|
||||
export * as OssApi from './oss/index';
|
||||
Reference in New Issue
Block a user