Browse Source

feat:论坛增加回复回复的功能。

feature/unlimited-reply
zjz1993 5 years ago
parent
commit
609192b516
  1. 71
      models-built/reply.js
  2. 1
      models-built/reply.js.map
  3. 35
      models/reply.ts
  4. 89
      modules/discussion.js
  5. 88
      views/article.ejs

71
models-built/reply.js

@ -0,0 +1,71 @@
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
exports.__esModule = true;
var TypeORM = require("typeorm");
var common_1 = require("./common");
var Reply = /** @class */ (function (_super) {
__extends(Reply, _super);
function Reply() {
return _super !== null && _super.apply(this, arguments) || this;
}
Reply.cache = false;
__decorate([
TypeORM.PrimaryGeneratedColumn(),
__metadata("design:type", Number)
], Reply.prototype, "id");
__decorate([
TypeORM.Column({ nullable: true, type: "mediumtext" }),
__metadata("design:type", String)
], Reply.prototype, "content");
__decorate([
TypeORM.Index(),
TypeORM.Column({ nullable: true, type: "integer" }),
__metadata("design:type", Number)
], Reply.prototype, "user_id");
__decorate([
TypeORM.Index(),
TypeORM.Column({ nullable: true, type: "integer" }),
__metadata("design:type", Number)
], Reply.prototype, "comment_id");
__decorate([
TypeORM.Index(),
TypeORM.Column({ nullable: true, type: "integer" }),
__metadata("design:type", Number)
], Reply.prototype, "article_id");
__decorate([
TypeORM.Column({ nullable: true, type: "integer" }),
__metadata("design:type", Number)
], Reply.prototype, "public_time");
__decorate([
TypeORM.Column({ nullable: true, type: "integer" }),
__metadata("design:type", Number)
], Reply.prototype, "update_time");
Reply = __decorate([
TypeORM.Entity()
], Reply);
return Reply;
}(common_1["default"]));
exports["default"] = Reply;
;
//# sourceMappingURL=reply.js.map

1
models-built/reply.js.map

@ -0,0 +1 @@
{"version":3,"file":"reply.js","sourceRoot":"","sources":["../models/reply.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA,iCAAmC;AACnC,mCAA6B;AAO7B;IAAmC,yBAAK;IAAxC;;IA0BA,CAAC;IAzBU,WAAK,GAAG,KAAK,CAAC;IAGrB;QADC,OAAO,CAAC,sBAAsB,EAAE;;6BACtB;IAGX;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;;kCACvC;IAIhB;QAFC,OAAO,CAAC,KAAK,EAAE;QACf,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;kCACpC;IAIhB;QAFC,OAAO,CAAC,KAAK,EAAE;QACf,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;qCACjC;IAInB;QAFC,OAAO,CAAC,KAAK,EAAE;QACf,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;qCACjC;IAGnB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;sCAChC;IAGpB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;sCAChC;IAzBH,KAAK;QADzB,OAAO,CAAC,MAAM,EAAE;OACI,KAAK,CA0BzB;IAAD,YAAC;CAAA,AA1BD,CAAmC,mBAAK,GA0BvC;qBA1BoB,KAAK;AA0BzB,CAAC"}

35
models/reply.ts

@ -0,0 +1,35 @@
import * as TypeORM from "typeorm";
import Model from "./common";
import User from "./user";
import Article from "./article";
declare var syzoj: any;
@TypeORM.Entity()
export default class Reply extends Model {
static cache = false;
@TypeORM.PrimaryGeneratedColumn()
id: number;
@TypeORM.Column({ nullable: true, type: "mediumtext" })
content: string;
@TypeORM.Index()
@TypeORM.Column({ nullable: true, type: "integer" })
user_id: number;
@TypeORM.Index()
@TypeORM.Column({ nullable: true, type: "integer" })
comment_id: number;
@TypeORM.Index()
@TypeORM.Column({ nullable: true, type: "integer" })
article_id: number;
@TypeORM.Column({ nullable: true, type: "integer" })
public_time: number;
@TypeORM.Column({ nullable: true, type: "integer" })
update_time: number;
};

89
modules/discussion.js

@ -2,6 +2,7 @@ let Problem = syzoj.model('problem');
let Article = syzoj.model('article'); let Article = syzoj.model('article');
let ArticleComment = syzoj.model('article-comment'); let ArticleComment = syzoj.model('article-comment');
let User = syzoj.model('user'); let User = syzoj.model('user');
let Reply = syzoj.model('reply');
app.get('/discussion/:type?', async (req, res) => { app.get('/discussion/:type?', async (req, res) => {
try { try {
@ -76,6 +77,7 @@ app.get('/discussion/problem/:pid', async (req, res) => {
app.get('/article/:id', async (req, res) => { app.get('/article/:id', async (req, res) => {
try { try {
let id = parseInt(req.params.id); let id = parseInt(req.params.id);
let replyQuery = await Reply.createQueryBuilder();
let article = await Article.findById(id); let article = await Article.findById(id);
if (!article) throw new ErrorMessage('无此帖子。'); if (!article) throw new ErrorMessage('无此帖子。');
@ -93,6 +95,17 @@ app.get('/article/:id', async (req, res) => {
}); });
for (let comment of comments) { for (let comment of comments) {
const commentId = comment.id;
const reply = await replyQuery.where("comment_id = :commentId", { commentId }).getMany();
if (reply.length === 0) {
comment.reply = [];
} else {
comment.reply = await reply.mapAsync(async (item) => {
const replyUserId = item.user_id;
item.reply_user = await User.findById(replyUserId);
return item
});
}
comment.content = await syzoj.utils.markdown(comment.content); comment.content = await syzoj.utils.markdown(comment.content);
comment.allowedEdit = await comment.isAllowedEditBy(res.locals.user); comment.allowedEdit = await comment.isAllowedEditBy(res.locals.user);
await comment.loadRelationships(); await comment.loadRelationships();
@ -105,8 +118,8 @@ app.get('/article/:id', async (req, res) => {
throw new ErrorMessage('您没有权限进行此操作。'); throw new ErrorMessage('您没有权限进行此操作。');
} }
} }
res.render('article', { res.render('article', {
user: res.locals.user,
article: article, article: article,
comments: comments, comments: comments,
paginate: paginate, paginate: paginate,
@ -203,7 +216,11 @@ app.post('/article/:id/delete', async (req, res) => {
await Promise.all((await ArticleComment.find({ await Promise.all((await ArticleComment.find({
article_id: article.id article_id: article.id
})).map(comment => comment.destroy())) })).map(comment => comment.destroy()));
await Promise.all((await Reply.find({
article_id: article.id
})).map(reply => reply.destroy()));
await article.destroy(); await article.destroy();
@ -249,13 +266,55 @@ app.post('/article/:id/comment', async (req, res) => {
} }
}); });
app.post('/article/:id/comment/:commentId', async (req, res) => {
try {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
let id = parseInt(req.params.id);
let commentId = parseInt(req.params.commentId);
let userId = parseInt(res.locals.user.id);
let article = await Article.findById(id);
let articleComment = await ArticleComment.findById(commentId);
if (!articleComment) {
throw new ErrorMessage('无此回复。');
}
if (!article) {
throw new ErrorMessage('无此帖子。');
} else {
if (!await article.isAllowedCommentBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
}
let newReply = await Reply.create({
content: req.body.comment,
article_id: id,
user_id: userId,
comment_id: commentId,
public_time: syzoj.utils.getCurrentDate(),
update_time: syzoj.utils.getCurrentDate()
});
const reply = await newReply.save();
await article.resetReplyCountAndTime();
//
res.redirect(syzoj.utils.makeUrl(['article', article.id]));
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
});
}
});
app.post('/article/:article_id/comment/:id/delete', async (req, res) => { app.post('/article/:article_id/comment/:id/delete', async (req, res) => {
try { try {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) }); if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
let id = parseInt(req.params.id); let id = parseInt(req.params.id);
let comment = await ArticleComment.findById(id); let comment = await ArticleComment.findById(id);
let replyQuery = await Reply.createQueryBuilder();
if (!comment) { if (!comment) {
throw new ErrorMessage('无此评论。'); throw new ErrorMessage('无此评论。');
} else { } else {
@ -265,6 +324,7 @@ app.post('/article/:article_id/comment/:id/delete', async (req, res) => {
const article = await Article.findById(comment.article_id); const article = await Article.findById(comment.article_id);
await comment.destroy(); await comment.destroy();
await replyQuery.delete().from(Reply).where("comment_id = :id", { id }).execute();
await article.resetReplyCountAndTime(); await article.resetReplyCountAndTime();
@ -276,3 +336,26 @@ app.post('/article/:article_id/comment/:id/delete', async (req, res) => {
}); });
} }
}); });
app.post('/article/:articleId/reply/:id/delete', async (req, res) => {
try {
if (!res.locals.user) throw new ErrorMessage('请登录后继续。', { '登录': syzoj.utils.makeUrl(['login'], { 'url': req.originalUrl }) });
let id = parseInt(req.params.id);
let reply = await Reply.findById(id);
let articleId = parseInt(req.params.articleId);
let replyQuery = await Reply.createQueryBuilder();
if (!reply) {
throw new ErrorMessage('无此回复。');
}
await reply.destroy();
res.redirect(`/article/${articleId}`);
} catch (e) {
syzoj.log(e);
res.render('error', {
err: e
});
}
});

88
views/article.ejs

@ -4,6 +4,18 @@
.small{ .small{
font-size: 0.7em; font-size: 0.7em;
} }
.reply{
margin-left: 45px !important;
}
.article-comment{
cursor: pointer;
display: inline-block;
margin: 0 .75em 0 0;
color: rgba(0,0,0,.4);
}
.article-comment:hover{
color: rgba(0,0,0,.8);
}
</style> </style>
<div class="padding"> <div class="padding">
<div class="ui breadcrumb"> <div class="ui breadcrumb">
@ -17,11 +29,15 @@
<a href="<%= syzoj.utils.makeUrl(['discussion', 'global']) %>" class="section">全局板块</a> <a href="<%= syzoj.utils.makeUrl(['discussion', 'global']) %>" class="section">全局板块</a>
<% } %> <% } %>
</div> </div>
<span id="user-info" style="display: none"><%= user %></span>
<h1><%= article.title %></h1> <h1><%= article.title %></h1>
<p style="margin-bottom: -5px; "> <p style="margin-bottom: -5px; ">
<img style="vertical-align: middle; margin-bottom: 2px; margin-right: 2px; " src="<%= syzoj.utils.gravatar(article.user.email, 34) %>" width="17" height="17"> <img style="vertical-align: middle; margin-bottom: 2px; margin-right: 2px; " src="<%= syzoj.utils.gravatar(article.user.email, 34) %>" width="17" height="17">
<b style="margin-right: 30px; "><a class="black-link" href="<%= syzoj.utils.makeUrl(['user', article.user_id]) %>"><%= article.user.username %></a><% if (article.user.nameplate) { %><%- article.user.nameplate %><% } %></b> <b style="margin-right: 30px; "><a class="black-link" href="<%= syzoj.utils.makeUrl(['user', article.user_id]) %>"><%= article.user.username %></a><% if (article.user.nameplate) { %><%- article.user.nameplate %><% } %></b>
<b style="margin-right: 30px; "><i class="calendar icon"></i> <%= syzoj.utils.formatDate(article.public_time) %></b> <b style="margin-right: 30px; "><i class="calendar icon"></i> <%= syzoj.utils.formatDate(article.public_time) %></b>
<% if(user) {%>
<span class="article-comment" onclick="reply('comment')">回复</span>
<%}%>
<% if (article.public_time !== article.update_time) { %><b style="margin-right: 30px; "><i class="edit icon"></i> <%= syzoj.utils.formatDate(article.update_time) %></b><% } %> <% if (article.public_time !== article.update_time) { %><b style="margin-right: 30px; "><i class="edit icon"></i> <%= syzoj.utils.formatDate(article.update_time) %></b><% } %>
<% if (article.allowedEdit) { %> <% if (article.allowedEdit) { %>
<a style="margin-top: -4px; " class="ui mini right floated labeled icon button" href="<%= syzoj.utils.makeUrl(['article', article.id, 'edit']) %>"><i class="ui edit icon"></i>编辑</a> <a style="margin-top: -4px; " class="ui mini right floated labeled icon button" href="<%= syzoj.utils.makeUrl(['article', article.id, 'edit']) %>"><i class="ui edit icon"></i>编辑</a>
@ -65,14 +81,14 @@
</div> </div>
<div class="text font-content" style="min-height: 19.5px; "><%- comment.content %></div> <div class="text font-content" style="min-height: 19.5px; "><%- comment.content %></div>
<% if (comment.allowedEdit) { %> <% if (comment.allowedEdit) { %>
<div class="actions"><a onclick="$('#modal-delete-<%= comment.id %>').modal('show')">删除</a></div> <div class="actions"><a onclick="$('#modal-delete-<%= comment.id %>').modal('show')">删除</a><a onclick="reply('reply', <%= comment.id %>)">回复</a></div>
<div class="ui basic modal" id="modal-delete-<%= comment.id %>"> <div class="ui basic modal" id="modal-delete-<%= comment.id %>">
<div class="ui icon header"> <div class="ui icon header">
<i class="trash icon"></i> <i class="trash icon"></i>
<p style="margin-top: 15px; ">删除评论</p> <p style="margin-top: 15px; ">删除评论</p>
</div> </div>
<div class="content" style="text-align: center; "> <div class="content" style="text-align: center; ">
<p>确认删除这条评论吗?</p> <p>确认删除这条评论吗?评论下所有回复也会被删除</p>
</div> </div>
<div class="actions"> <div class="actions">
<div class="ui red basic cancel inverted button"> <div class="ui red basic cancel inverted button">
@ -85,10 +101,52 @@
</a> </a>
</div> </div>
</div> </div>
<% } else { %>
<div class="actions">
<% if(user){%>
<a onclick="reply('reply', <%= comment.id %>)">回复</a>
<%}%>
</div>
<%}%>
</div>
</div>
<% for (let reply of comment.reply) { %>
<div class="comment reply">
<a class="avatar">
<img src="<%= syzoj.utils.gravatar(reply.reply_user.email, 120) %>" alt="">
</a>
<div class="content">
<a class="author" href="<%= syzoj.utils.makeUrl(['user', reply.user_id]) %>"><%= reply.reply_user.username %></a><% if (reply.reply_user.nameplate) { %><%- reply.reply_user.nameplate %><% } %>
<div class="metadata">
<span class="date"><%= syzoj.utils.formatDate(reply.public_time) %></span>
</div>
<div class="text font-content" style="min-height: 19.5px; "><%- reply.content %></div>
<% if (comment.allowedEdit) { %>
<div class="actions"><a onclick="$('#modal-delete-reply-<%= reply.id %>').modal('show')">删除</a></div>
<div class="ui basic modal" id="modal-delete-reply-<%= reply.id %>">
<div class="ui icon header">
<i class="trash icon"></i>
<p style="margin-top: 15px; ">删除回复</p>
</div>
<div class="content" style="text-align: center; ">
<p>确认删除这条回复吗?</p>
</div>
<div class="actions">
<div class="ui red basic cancel inverted button">
<i class="remove icon"></i>
</div>
<a class="ui green ok inverted button" href-post="<%= syzoj.utils.makeUrl(['article', article.id, 'reply', reply.id, 'delete']) %>">
<i class="checkmark icon"></i>
</a>
</div>
</div>
<% } %> <% } %>
</div> </div>
</div> </div>
<% } %> <% } %>
<% } %>
</div> </div>
<div style="margin-bottom: 50px; "> <div style="margin-bottom: 50px; ">
<% include page %> <% include page %>
@ -96,14 +154,14 @@
<% } %> <% } %>
<% if (article.allowedComment) { %> <% if (article.allowedComment) { %>
<% include monaco-editor %> <% include monaco-editor %>
<form class="ui reply form" method="post" action="<%= syzoj.utils.makeUrl(['article', article.id, 'comment']) %>"> <form id="comment-box" class="ui reply form" method="post" action="" style="visibility:hidden">
<div id="comment" class="editor editor-with-border" style="height: 200px; width: 100%; margin-bottom: 1em; "> <div id="comment" class="editor editor-with-border" style="height: 200px; width: 100%; margin-bottom: 1em; ">
<%- this.showLoadingEditor(); %> <%- this.showLoadingEditor(); %>
</div> </div>
<input type="hidden" name="comment"> <input type="hidden" name="comment">
<div style="text-align: center; "> <div style="text-align: center; ">
<button id="submit_button" type="submit" class="ui disabled labeled submit icon button"> <button id="submit_button" type="submit" class="ui disabled labeled submit icon button">
<i class="icon edit"></i> 回复 <i class="icon edit"></i> <span id="reply-text">回复</span>
</button> </button>
</div> </div>
</form> </form>
@ -126,3 +184,25 @@
<% } %> <% } %>
</div> </div>
<% include footer %> <% include footer %>
<script>
function reply(type, replyId){
const id = document.getElementById('user-info').innerHTML;
if (id) {
const editorDom = document.getElementById('comment-box');
const editorBtn = document.getElementById('reply-text');
const articleId = <%= article.id %>;
let actionUrl = null;
let btnText = null;
if (type === 'comment') {
actionUrl = `/article/${articleId}/comment`;
btnText='回复帖子';
} else if (type === 'reply' && replyId) {
actionUrl = `/article/${articleId}/comment/${replyId}`;
btnText='回复评论';
}
editorDom.setAttribute('action', actionUrl);
editorBtn.innerText=btnText;
editorDom.style.visibility='initial';
}
}
</script>

Loading…
Cancel
Save