2.13 博客留言

130 阅读2分钟

留言页面

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文件:

image.png

留言接口

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:

image.png

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方法,使用评论:

image.png

验证

点开一篇博客,能看到下面的留言框:

image.png

留言后:

image.png

鼠标移到这条留言上,会浮现删除按钮:

image.png

点击删除后:

image.png

作业

现在已经有了留言,但需要在浏览的右侧展示该博客的留言数量。你可以尝试处理下。