Browse Source

feat: 完成多级回复功能。

pull/27/head
zjz1993 5 years ago
parent
commit
3ce9dd139d
  1. 8
      models-built/new_reply.js
  2. 2
      models-built/new_reply.js.map
  3. 6
      models/new_reply.ts
  4. 175
      modules/discussion.js
  5. 155
      views/article.ejs

8
models-built/new_reply.js

@ -122,6 +122,10 @@ var NewReply = /** @class */ (function (_super) {
TypeORM.Column({ nullable: true, type: "integer" }),
__metadata("design:type", Number)
], NewReply.prototype, "parent_id");
__decorate([
TypeORM.Column({ nullable: true, type: "boolean", "default": true }),
__metadata("design:type", Boolean)
], NewReply.prototype, "is_show");
__decorate([
TypeORM.Column({ nullable: true, type: "integer" }),
__metadata("design:type", Number)
@ -130,10 +134,6 @@ var NewReply = /** @class */ (function (_super) {
TypeORM.Column({ nullable: true, type: "integer" }),
__metadata("design:type", Number)
], NewReply.prototype, "update_time");
__decorate([
TypeORM.Column({ "default": 0, type: "integer" }),
__metadata("design:type", Number)
], NewReply.prototype, "comments_num");
NewReply = __decorate([
TypeORM.Entity()
], NewReply);

2
models-built/new_reply.js.map

@ -1 +1 @@
{"version":3,"file":"new_reply.js","sourceRoot":"","sources":["../models/new_reply.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iCAAmC;AACnC,mCAA6B;AAC7B,+BAA0B;AAC1B,qCAAgC;AAIhC;IAAsC,4BAAK;IAA3C;;IA2CA,CAAC;IAVS,oCAAiB,GAAvB;;;;;;wBACI,KAAA,IAAI,CAAA;wBAAQ,qBAAM,iBAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,EAAA;;wBAAlD,GAAK,IAAI,GAAG,SAAsC,CAAC;wBACnD,KAAA,IAAI,CAAA;wBAAW,qBAAM,oBAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAA;;wBAAtD,GAAK,OAAO,GAAG,SAAuC,CAAC;;;;;KAC1D;IAEK,kCAAe,GAArB,UAAsB,IAAI;;;;4BACtB,qBAAM,IAAI,CAAC,iBAAiB,EAAE,EAAA;;wBAA9B,SAA8B,CAAC;wBAC/B,sBAAO,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAC;;;;KACvG;IAxCM,cAAK,GAAG,KAAK,CAAC;IAGrB;QADC,OAAO,CAAC,sBAAsB,EAAE;;gCACtB;IAGX;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;;qCACvC;IAGhB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;wCACjC;IAGnB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;0CAC/B;IAGrB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;wCACjC;IAGnB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;uCAClC;IAGlB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;yCAChC;IAGpB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;yCAChC;IAGpB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,SAAO,EAAE,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;0CAC3B;IA5BJ,QAAQ;QAD5B,OAAO,CAAC,MAAM,EAAE;OACI,QAAQ,CA2C5B;IAAD,eAAC;CAAA,AA3CD,CAAsC,mBAAK,GA2C1C;qBA3CoB,QAAQ;AA2C5B,CAAC"}
{"version":3,"file":"new_reply.js","sourceRoot":"","sources":["../models/new_reply.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,iCAAmC;AACnC,mCAA6B;AAC7B,+BAA0B;AAC1B,qCAAgC;AAIhC;IAAsC,4BAAK;IAA3C;;IA2CA,CAAC;IAVS,oCAAiB,GAAvB;;;;;;wBACI,KAAA,IAAI,CAAA;wBAAQ,qBAAM,iBAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,EAAA;;wBAAlD,GAAK,IAAI,GAAG,SAAsC,CAAC;wBACnD,KAAA,IAAI,CAAA;wBAAW,qBAAM,oBAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,EAAA;;wBAAtD,GAAK,OAAO,GAAG,SAAuC,CAAC;;;;;KAC1D;IAEK,kCAAe,GAArB,UAAsB,IAAI;;;;4BACtB,qBAAM,IAAI,CAAC,iBAAiB,EAAE,EAAA;;wBAA9B,SAA8B,CAAC;wBAC/B,sBAAO,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAC;;;;KACvG;IAxCM,cAAK,GAAG,KAAK,CAAC;IAGrB;QADC,OAAO,CAAC,sBAAsB,EAAE;;gCACtB;IAGX;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;;qCACvC;IAGhB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;wCACjC;IAGnB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;0CAC/B;IAGrB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;wCACjC;IAGnB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;uCAClC;IAGlB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAO,EAAE,IAAI,EAAE,CAAC;;qCAClD;IAGjB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;yCAChC;IAGpB;QADC,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;yCAChC;IA5BH,QAAQ;QAD5B,OAAO,CAAC,MAAM,EAAE;OACI,QAAQ,CA2C5B;IAAD,eAAC;CAAA,AA3CD,CAAsC,mBAAK,GA2C1C;qBA3CoB,QAAQ;AA2C5B,CAAC"}

6
models/new_reply.ts

@ -26,15 +26,15 @@ export default class NewReply extends Model {
@TypeORM.Column({ nullable: true, type: "integer" })
parent_id: number;
@TypeORM.Column({ nullable: true, type: "boolean", default: true })
is_show: boolean;
@TypeORM.Column({ nullable: true, type: "integer" })
public_time: number;
@TypeORM.Column({ nullable: true, type: "integer" })
update_time: number;
@TypeORM.Column({ default: 0, type: "integer" })
comments_num: number;
user?: User;
article?: Article;

175
modules/discussion.js

@ -22,7 +22,7 @@ app.get('/discussion/:type?', async (req, res) => {
let articles = await Article.queryPage(paginate, where, {
sort_time: 'DESC'
});
let newReplyQuery = await NewReply.createQueryBuilder();
for (let article of articles) {
await article.loadRelationships();
if (in_problems) {
@ -76,63 +76,6 @@ app.get('/discussion/problem/:pid', async (req, res) => {
});
app.get('/article/:id', async (req, res) => {
// try {
// let id = parseInt(req.params.id);
// let replyQuery = await Reply.createQueryBuilder();
// let article = await Article.findById(id);
// if (!article) throw new ErrorMessage('无此帖子。');
//
// await article.loadRelationships();
// article.allowedEdit = await article.isAllowedEditBy(res.locals.user);
// article.allowedComment = await article.isAllowedCommentBy(res.locals.user);
// article.content = await syzoj.utils.markdown(article.content);
//
// let where = { article_id: id };
// let commentsCount = await ArticleComment.countForPagination(where);
// let paginate = syzoj.utils.paginate(commentsCount, req.query.page, syzoj.config.page.article_comment);
//
// let comments = await ArticleComment.queryPage(paginate, where, {
// public_time: 'DESC'
// });
//
// 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.allowedEdit = await comment.isAllowedEditBy(res.locals.user);
// await comment.loadRelationships();
// }
//
// let problem = null;
// if (article.problem_id) {
// problem = await Problem.findById(article.problem_id);
// if (!await problem.isAllowedUseBy(res.locals.user)) {
// throw new ErrorMessage('您没有权限进行此操作。');
// }
// }
// res.render('article', {
// user: res.locals.user,
// article: article,
// comments: comments,
// paginate: paginate,
// problem: problem,
// commentsCount: commentsCount
// });
// } catch (e) {
// syzoj.log(e);
// res.render('error', {
// err: e
// });
// }
try {
let id = parseInt(req.params.id);
let article = await Article.findById(id);
@ -142,21 +85,57 @@ app.get('/article/:id', async (req, res) => {
article.allowedEdit = await article.isAllowedEditBy(res.locals.user);
article.allowedComment = await article.isAllowedCommentBy(res.locals.user);
article.content = await syzoj.utils.markdown(article.content);
let newReplyQuery = await NewReply.createQueryBuilder();
let where = { article_id: id };
let commentsCount = await NewReply.countForPagination(where);
let paginate = syzoj.utils.paginate(commentsCount, req.query.page, syzoj.config.page.article_comment);
let comments = await NewReply.queryPage(paginate, where, {
public_time: 'DESC'
});
console.log(comments);
for (let comment of comments) {
comment.content = await syzoj.utils.markdown(comment.content);
comment.allowedEdit = await comment.isAllowedEditBy(res.locals.user);
await comment.loadRelationships();
let comments = await newReplyQuery.where('article_id=:id', {id}).getMany();
function arrayToTree(data){
for(let i=0;i<data.length;i++){
if (data[i].parent_id){
var info = data.find(item=>item.id===data[i].parent_id);
if (info) {
if (!info.children) {
info.children=[data[i]];
} else {
info.children.push(data[i]);
}
}
}
}
return data.filter(item=>!item.parent_id && item.is_show)
}
const originData = comments.filter(item => item.is_show);
const datas = arrayToTree(comments.filter(item => item.is_show));
const rootIdArray = datas.map(item=>item.id);
const authorId = article.user_id;
let count = 0;
async function operate(data){
for(let i=0;i<data.length;i++){
count++;
data[i].user = await User.findById(data[i].from_user_id);
data[i].avatarUrl = syzoj.utils.gravatar(data[i].user.email, 120);
data[i].date = syzoj.utils.formatDate(data[i].public_time)
data[i].toUser = await User.findById(data[i].to_user_id)
data[i].allowedEdit = await data[i].isAllowedEditBy(res.locals.user);
if (rootIdArray.includes(data[i].parent_id)){
data[i].firstComment = true;
}
if (data[i].children) {
await operate(data[i].children);
}
}
}
await operate(datas);
let articleQuery = await Article.createQueryBuilder();
await articleQuery.update(Article).set({comments_num: count}).where("id = :id", { id }).execute();
datas.sort((a,b) => {
return b.public_time - a.public_time;
})
let problem = null;
if (article.problem_id) {
@ -168,10 +147,11 @@ app.get('/article/:id', async (req, res) => {
res.render('article', {
user: res.locals.user,
article: article,
comments: comments,
comments: datas,
paginate: paginate,
problem: problem,
commentsCount: commentsCount
commentsCount: count,
authorId
});
} catch (e) {
syzoj.log(e);
@ -260,15 +240,10 @@ app.post('/article/:id/delete', async (req, res) => {
} else {
if (!await article.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
}
await Promise.all((await ArticleComment.find({
article_id: article.id
})).map(comment => comment.destroy()));
let newReplyQuery = await NewReply.createQueryBuilder();
await Promise.all((await Reply.find({
article_id: article.id
})).map(reply => reply.destroy()));
await newReplyQuery.delete().from(NewReply).where("article_id = :id", { id }).execute();
await article.destroy();
res.redirect(syzoj.utils.makeUrl(['discussion', 'global']));
@ -300,7 +275,7 @@ app.post('/article/:id/comment', async (req, res) => {
to_user_id: to_id,
public_time: syzoj.utils.getCurrentDate(),
update_time: syzoj.utils.getCurrentDate(),
comments_num: 1,
is_show: true
});
await comment.save();
@ -324,31 +299,29 @@ app.post('/article/:id/comment/:commentId', async (req, res) => {
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 articleQuery = await Article.createQueryBuilder();
// await articleQuery.update(Article).set({comments_num: article.comments_num + 1}).where("id = :id", { id }).execute();
const toInfo = await NewReply.findById(commentId);
let newReply = await Reply.create({
let newReply = await NewReply.create({
content: req.body.comment,
article_id: id,
user_id: userId,
comment_id: commentId,
from_user_id: res.locals.user.id,
to_user_id: toInfo.from_user_id,
parent_id: commentId,
public_time: syzoj.utils.getCurrentDate(),
update_time: syzoj.utils.getCurrentDate()
update_time: syzoj.utils.getCurrentDate(),
is_show: true
});
const reply = await newReply.save();
await newReply.save();
await article.resetReplyCountAndTime();
//
res.redirect(syzoj.utils.makeUrl(['article', article.id]));
} catch (e) {
syzoj.log(e);
@ -358,27 +331,17 @@ app.post('/article/:id/comment/:commentId', async (req, res) => {
}
});
app.post('/article/:article_id/comment/:id/delete', async (req, res) => {
app.post('/article/:articleId/comment/: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 comment = await ArticleComment.findById(id);
let replyQuery = await Reply.createQueryBuilder();
if (!comment) {
throw new ErrorMessage('无此评论。');
} else {
if (!await comment.isAllowedEditBy(res.locals.user)) throw new ErrorMessage('您没有权限进行此操作。');
}
const article = await Article.findById(comment.article_id);
await comment.destroy();
await replyQuery.delete().from(Reply).where("comment_id = :id", { id }).execute();
await article.resetReplyCountAndTime();
let articleId = parseInt(req.params.articleId);
let replyQuery = await NewReply.createQueryBuilder();
await replyQuery.update(NewReply).set({is_show: 0}).where("id = :id", { id }).execute();
res.redirect(syzoj.utils.makeUrl(['article', comment.article_id]));
res.redirect(syzoj.utils.makeUrl(['article', articleId]));
} catch (e) {
syzoj.log(e);
res.render('error', {

155
views/article.ejs

@ -16,8 +16,16 @@
.article-comment:hover{
color: rgba(0,0,0,.8);
}
.border{
padding-bottom: 10px !important;
border-bottom: 1px solid #DEDEDF !important;
}
.border:last-child{
border-bottom: none !important;
}
</style>
<div class="padding">
<div class="padding" id="comment-zone">
<div class="ui breadcrumb">
<div class="section">讨论</div>
<i class="right angle icon divider"></i>
@ -69,51 +77,7 @@
<% if (comments.length) { %>
<div class="ui comments" style="max-width: none;">
<h3 class="ui dividing header">共 <%= commentsCount %> 条回复</h3>
<% for (let comment of comments) { %>
<div class="comment">
<a class="avatar">
<img src="<%= syzoj.utils.gravatar(comment.user.email, 120) %>">
</a>
<div class="content">
<a class="author" href="<%= syzoj.utils.makeUrl(['user', comment.user_id]) %>"><%= comment.user.username %></a><% if (comment.user.nameplate) { %><%- comment.user.nameplate %><% } %>
<div class="metadata">
<span class="date"><%= syzoj.utils.formatDate(comment.public_time) %></span>
</div>
<div class="text font-content" style="min-height: 19.5px; "><%- comment.content %></div>
<% if (comment.allowedEdit) { %>
<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 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, 'comment', comment.id, 'delete']) %>">
<i class="checkmark icon"></i>
</a>
</div>
</div>
<% } else { %>
<div class="actions">
<% if(user){%>
<a onclick="reply('reply', <%= comment.id %>)">回复</a>
<%}%>
</div>
<%}%>
</div>
</div>
<% } %>
</div>
<div style="margin-bottom: 50px; ">
<% include page %>
<comment-box v-for="(item, index) in comments" :key="index" :data="item" :index="index" :user="user"></comment-box>
</div>
<% } %>
<% if (article.allowedComment) { %>
@ -129,6 +93,26 @@
</button>
</div>
</form>
<div class="ui basic modal" id="modal-comment-delete">
<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="" id="modal-comment-delete-href">
<i class="checkmark icon"></i>
</a>
</div>
</div>
<script>
var editors = {
comment: { defaultValue: '' }
@ -147,6 +131,76 @@
</script>
<% } %>
</div>
<script type="text/x-template" id="test-comment">
<div class="comment" :class="{ reply: data.firstComment, border:!data.parent_id }">
<a class="avatar">
<img :src="data.avatarUrl">
</a>
<div class="content">
<a class="author" :href="userUrl(data.user.id)" >{{data.user.username}}</a>
<span v-if="data.user.id === authorId">(作者)</span>
回复给
<a class="author" :href="userUrl(data.toUser.id)" >{{data.toUser.username}}</a>
<span v-if="data.toUser.id === authorId">(作者)</span>
<div v-if="data.user.nameplate">{{data.user.nameplate}}</div>
<div class="metadata">
<span class="date">{{data.date}}</span>
</div>
<div class="text font-content" style="min-height: 19.5px; ">{{data.content}}</div>
<div class="actions">
<template v-if="data.allowedEdit">
<a @click="deleteComment(data.id)">删除</a>
</template>
<template v-if="user">
<a @click="reply('reply', data.id)">回复</a>
</template>
</div>
</div>
<node v-for="(item, index) in data.children" :key="index" :data="item" :index="index" :user="user"></node>
</div>
</script>
<script>
var comments = <%- serializejs(comments) %>
var authorId = <%= authorId %>
var user = <%- serializejs(user) %>
var testComponent = Vue.component("comment-box", {
template: "#test-comment",
name: "Node",
props: {
data: {
type: Object,
default() {
return {};
}
},
index:{
type:Number,
default(){
return 0
}
},
user: {
type:Object,
}
},
computed:{
userUrl() {
return function(id){
return `user/${id}`
}
}
}
})
new Vue({
el: '#comment-zone',
data:{
comments:comments,
authorId,
user
}
})
</script>
<% include footer %>
<script>
function reply(type, replyId){
@ -168,5 +222,14 @@
editorBtn.innerText=btnText;
editorDom.style.visibility='initial';
}
document.body.scrollTo({
top: document.body.scrollHeight,
behavior: "smooth"
});
}
function deleteComment(id){
const articleId = <%= article.id %>;
$('#modal-comment-delete').modal('show')
$('#modal-comment-delete-href').attr('href-post',`/article/${articleId}/comment/${id}/delete`)
}
</script>

Loading…
Cancel
Save