留言页面
comments.ejs
新建views/components/comments.ejs
<div class="ui grid">
<div class="four wide column"></div>
<div class="eight wide column">
<div class="ui segment">
<div class="ui minimal comments">
<h3 class="ui dividing header">留言</h3>
<% post.comments.forEach(function (comment) { %>
<div class="comment">
<span class="avatar">
<img src="/static/img/<%= comment.author.avatar %>">
</span>
<div class="content">
<a class="author" href="/posts?userId=<%= comment.author.id %>">
<%= comment.author.name %>
</a>
<div class="metadata">
<span class="date">
<%= comment.createdAt %>
</span>
</div>
<div class="text">
<%- comment.contentHtml %>
</div>
<% if (user && comment.author.id && user.id===comment.author.id) { %>
<div class="actions">
<a class="reply" href="/posts/<%= post.id %>/comment/<%= comment.id %>/remove">删除</a>
</div>
<% } %>
</div>
</div>
<% }) %>
<% if (user) { %>
<form class="ui reply form" method="post" action="/posts/<%= post.id %>/comment">
<div class="field">
<textarea name="content"></textarea>
</div>
<input type="submit" class="ui icon button" value="留言" />
</form>
<% } %>
</div>
</div>
</div>
</div>
post.ejs
修改views/post.ejs,引入刚才的ejs文件:
留言接口
comments.schema.ts
新建src/comments/comments.schema.ts
import { Prop } from "../schema.ts";
import { UserInfo } from "../user/user.schema.ts";
export class Comment {
@Prop()
userId: string;
@Prop({
required: true,
})
content: string;
@Prop({
required: true,
})
postId: string;
@Prop()
createTime: Date;
@Prop()
updateTime: Date;
createdAt?: string;
contentHtml?: string;
author?: UserInfo | null;
}
内容与post.schema.ts大同小异。
comments.dto.ts
新建src/comments/comments.dto.ts
import { IsString } from "deno_class_validator";
export class CreateCommentDto {
@IsString()
content: string;
userId: string;
postId: string;
}
comments.service.ts
新建src/comments/comments.service.ts
import { Injectable } from "oak_nest";
import { InjectModel, Model } from "../model.ts";
import { UserService } from "../user/user.service.ts";
import { Comment } from "./comments.schema.ts";
import { format } from "timeago";
import { Marked } from "markdown";
import { CreateCommentDto } from "./comments.dto.ts";
@Injectable()
export class CommentsService {
constructor(
@InjectModel(Comment) private readonly model: Model<Comment>,
private readonly userService: UserService,
) {
}
create(params: CreateCommentDto) {
const now = new Date();
return this.model.insertOne({
...params,
createTime: now,
updateTime: now,
});
}
deleteById(id: string) {
return this.model.findByIdAndDelete(id);
}
async findByPostId(postId: string) {
const arr = await this.model.findMany({
postId,
});
const userIds = arr.map((item) => item.userId);
const users = await this.userService.getUsersByIds(userIds);
arr.forEach((comment) => {
comment.createdAt = format(comment.createTime, "zh_CN");
const html = Marked.parse(comment.content).content;
comment.contentHtml = html;
comment.author = users.find((user) => user.id === comment.userId);
});
return arr;
}
findById(id: string) {
return this.model.findById(id);
}
}
comments.controller.ts
新建src/comments/comments.controller.ts
import { BadRequestException, ForbiddenException } from "oak_exception";
import {
Controller,
Form,
Get,
Params,
Post,
REDIRECT_BACK,
Res,
Response,
UseGuards,
} from "oak_nest";
import { SSOGuard } from "../guards/sso.guard.ts";
import { Flash, UserParam } from "../session/session.decorator.ts";
import { Logger } from "../tools/log.ts";
import type { UserInfo } from "../user/user.schema.ts";
import { CreateCommentDto } from "./comments.dto.ts";
import { CommentsService } from "./comments.service.ts";
@Controller("/posts")
@UseGuards(SSOGuard)
export class CommentsController {
constructor(
private readonly commentsService: CommentsService,
private readonly logger: Logger,
) {}
@Post("/:postId/comment")
async createComment(
@Params("postId") postId: string,
@Form() params: CreateCommentDto,
@Res() res: Response,
@UserParam() userInfo: UserInfo,
@Flash() flash: Flash,
) {
const id = await this.commentsService.create({
postId,
userId: userInfo.id,
content: params.content,
});
this.logger.info(`用户${userInfo.id}创建了博客${postId}的留言: ${id}`);
flash("success", "留言成功");
// 留言成功后跳转到上一页
res.redirect(REDIRECT_BACK);
}
@Get("/:postId/comment/:commentId/remove")
async removeComment(
@Params("postId") postId: string,
@Params("commentId") commentId: string,
@UserParam() user: UserInfo,
@Res() res: Response,
@Flash() flash: Flash,
) {
const comment = await this.commentsService.findById(commentId);
if (!comment) {
throw new BadRequestException(`未找到id为${commentId}的留言`);
}
if (comment.userId !== user.id) {
throw new ForbiddenException(`您没有权限删除该留言`);
}
await this.commentsService.deleteById(commentId);
this.logger.info(
`用户${user.id}删除了博客${postId}的留言:${commentId}`,
);
flash("success", "删除留言成功");
res.redirect(REDIRECT_BACK);
}
}
总共两个接口,都需要安全守卫,所以将UseGuards装饰器放在Controller上。
comments.module.ts
新建src/comments/comments.module.ts,把刚才的CommentsController引入进来。
import { Module } from "oak_nest";
import { CommentsController } from "./comments.controller.ts";
@Module({
controllers: [
CommentsController,
],
})
export class CommentsModule {
}
app.module.ts
修改src/app.module.ts,引入CommentsModule:
posts.schema.ts
修改src/posts/posts.schema.ts,给Post添加一个属性comments:
comments?: Comment[] | null;
posts.service.ts
修改src/posts/posts.service.ts的findById:
import { CommentsService } from "../comments/comments.service.ts";
interface PopulateOptions {
isWithUserInfo?: boolean;
isWithComments?: boolean; // 增加评论
isIncrementPv?: boolean;
}
@Injectable()
export class PostsService {
constructor(
private readonly commentsService: CommentsService, // 增加
) {}
async findById(id: string, options: PopulateOptions = {}) {
...
if (options.isWithComments) {
post.comments = await this.commentsService.findByPostId(id);
}
...
return post;
}
}
posts.controller.ts
修改src/posts/posts.controller.ts的findPostById方法,使用评论:
验证
点开一篇博客,能看到下面的留言框:
留言后:
鼠标移到这条留言上,会浮现删除按钮:
点击删除后:
作业
现在已经有了留言,但需要在浏览的右侧展示该博客的留言数量。你可以尝试处理下。