关于博客评论功能实现遇到的问题
-
评论功能
这方面很容易实现,做一个comment的model。使用modelform进行表单验证,即可实现添加评论的功能。
难点是利用ajax发送请求,实现动态添加评论。不刷新页面即可剪刀评论。在这方面对跟评论的添加很容易,使用ajax发请求向后端拿数据,找到评论的div,进行append即可。关键是子评论的添加。首先判断父评论的id值pid,在append的父评论中的div标签添加动态的评论的主键pk值为comment_d值,通过comment_d=pid找到这个标签,然后在这个标签进行append子评论,同时子评论也需要添加一个comment_d=pk,用于子子评论的动态添加。
-
评论树展示
第一种方法是使用递归的方式实现。在views.py中拿到所有评论数据render给页面渲染。页面渲染处用自定义标签中的方法对渲染的comment_dict进行递归生成评论的HTML标签.
第二种就是发送ajax请求,不绑定时间,直接写在body标签后(很重要,因为卸载body中由于文件过多可能会导致
<script>
标签不执行的问题),下边就和动态加载append的父评论标签和子评论标签一样啦。
JS代码
models.py
class Comment(models.Model):
"""
文章评论
"""
content = models.TextField(verbose_name='评论内容')
username = models.CharField(max_length=30, verbose_name='用户名')
add_time = models.DateTimeField(auto_now_add=True, verbose_name='发布时间')
article = models.ForeignKey(Article, verbose_name='文章', on_delete=models.CASCADE)
qq_email = models.CharField(max_length=100, verbose_name='qq邮箱')
web_site = models.CharField(max_length=100, blank=True, null=True, verbose_name='网站')
pid = models.ForeignKey('self', blank=True, null=True, verbose_name='父级评论', on_delete=models.CASCADE)
class Meta:
verbose_name = '评论'
verbose_name_plural = verbose_name
def __str__(self):
return self.content[:20]
views.py
class CommentForm(forms.ModelForm):
class Meta:
model = models.Comment
fields = "__all__"
# exclude = ['pid', ]
class CommentView(View):
def post(self, request):
msg = {
}
error = {
}
data = {
}
form = CommentForm(request.POST)
pid = request.POST.get('pid')
if form.is_valid():
msg['success'] = True
# print(form.cleaned_data)
comment_obj = form.save()
data['pk'] = comment_obj.pk
data['content'] = comment_obj.content
data['username'] = comment_obj.username
data['add_time'] = comment_obj.add_time.strftime('%Y-%m-%d %H:%M:%S')
# print(comment_obj.pid)
if comment_obj.pid != None:
# pid = int(comment_obj.pid)
fu = models.Comment.objects.get(pk=pid).username
# print(fu)
data['fu_username'] = fu
else:
data['fu_username'] = 0
msg['data'] = data
else:
print(form.errors)
msg['success'] = False
for field in form.fields.keys():
if form.has_error(field):
error[field] = 'valied'
else:
error[field] = 0
msg['error'] = error
# print(msg) # 发给AjaxForm的数据
return JsonResponse(msg)
class CommentTreeView(View):
def get(self, request):
msg = []
article_id = request.GET.get('article_id')
# 该文章的所有评论
comment_obj = models.Comment.objects.filter(article_id=article_id)
for comment in comment_obj:
data = {
}
if comment.pid:
data['pid'] = comment.pid.id
data['fu_username'] = models.Comment.objects.get(pk=comment.pid.id).username
else:
data['pid'] = None
data['fu_username'] = None
data['pk'] = comment.pk
data['content'] = comment.content
data['username'] = comment.username
data['add_time'] = comment.add_time.strftime('%Y-%m-%d %H:%M:%S')
msg.append(data)
# print(msg) # print(msg) # 发给Ajax的数据
return JsonResponse(msg, safe=False)
js代码
$("#comment-submit").click(function () {
$("#comment-submit").html("正在提交评论...");
$("#comment-form").ajaxSubmit(function (data) {
console.log(data);
// console.log(data.error.qq_email);
// console.log(data.error.article);
if (data.success == false) {
if (data.error.username) {
$("#username").addClass("is-invalid");
$("#username-feedback").html(data.error.username);
}
if (data.error.qq_email) {
$("#qq_email").addClass("is-invalid");
$("#qq_email-feedback").html(data.error.qq_email);
}
if (data.error.web_site) {
$("#web_site").addClass("is-invalid");
}
if (data.error.content) {
$("#content").addClass("is-invalid");
$("#content-feedback").html(data.error.content);
}
if (data.error.article) {
Swal.fire({
text: data.error.article, type: 'error'});
//layer.msg(data.error.article_id.join(","),{icon:2,time:3000});
}
$("#comment-submit").html("提交评论");
// if (data.error.category_id) {
// Swal.fire({text: data.error.category_id.join(","), type: 'error'});
// //layer.msg(data.error.category_id.join(","),{icon:2,time:3000});
// }
// if (data.error.p_id) {
// Swal.fire({text: data.error.parent_id, type: 'error'});
// }
// if (data.error.msg) { join(",")
// Swal.fire({text: data.error.msg, type: 'error'});
// }
} else {
var content = data.data.content
var add_time = data.data.add_time
var username = data.data.username
var fu_username = data.data.fu_username
var pk = data.data.pk
// ajax动态加载评论
var gen_comment = `
<li class="list-group-item comment-${
pk} mt-3 px-2 pt-3 pb-2 depth-0" comment_id=${
pk}>
<div class="clearfix" id="div-comment-${
pk}">
<div class="media">
<img src="/static/picture/g-sdk_cFeAJq3pic4ekYTaQMJSx4Q_10.jpg"
class="mr-3 rounded-circle" width="50" height="50"
οnerrοr="javascript:this.src='/static/image/unknow.png';">
<div class="media-body">
<div class="comment-info">
<cite class="c3">
${
username}
</cite>
</div>
<div class="comment-meta"><span
class="font-weight-light text-muted">${
add_time}</span>
</div>
</div>
</div>
<p class="text-break mt-2">${
content}</p>
<a class="btn btn-sm btn-secondary float-right"
οnclick="reply('div-comment-${
pk}','${
pk}')">回复</a>
</div> </li>
`
var zi_comment = `
<li class="list-group-item comment-${
pk} mt-3 px-2 pt-3 pb-2 depth-0" comment_id=${
pk}>
<div class="clearfix" id="div-comment-${
pk}">
<div class="media">
<img src="/static/picture/g-sdk_cFeAJq3pic4ekYTaQMJSx4Q_10.jpg"
class="mr-3 rounded-circle" width="50" height="50"
οnerrοr="javascript:this.src='/static/image/unknow.png';">
<div class="media-body">
<div class="comment-info">
<cite class="c3">
${
username}
</cite>
<i class="fa fa-share fa-fw fa-1x mr-2 c1" aria-hidden="true"></i>
<cite class="c3"><a href="#div-comment-48" class="text-reset">${
fu_username}</a></cite>
</div>
<div class="comment-meta"><span class="font-weight-light text-muted">${
add_time}</span>
</div>
</div>
</div>
<p class="text-break mt-2">${
content}</p>
<a class="btn btn-sm btn-secondary float-right"
οnclick="reply('div-comment-${
pk}','${
pk}')">回复</a>
</div> </li>
`
if ($("input[name='pid']").val()) {
// console.log($("input[name='pid']").val())
$('#respond').before('<ol class="children">' + zi_comment + '</ol>');
} else {
$(".comment-list").append(gen_comment);
}
// 清空pid的值,不然下次的就不是根评论啦
$("input[name='pid']").val('');
// js改变了输入框的值但是页面不显示,用prop()
// $("#content").text(" ");//清空评论内容
$("#content").prop('value','');//清空评论内容
//触发取消评论按钮点击事件即恢复评论输入框位置,同时隐藏取消评论按钮
$("#cancel-reply").trigger("click").addClass("d-none");
//页面反馈信息
Swal.fire("提交成功");
}
//恢复提交按钮内容
$("#comment-submit").html("提交评论");
})
})
})
{
# js代码要写在body后,否则容易不执行 评论树展示#}
<script>
{
#$(".dianji").click(function (){
#}
$.ajax({
url: "/comment/tree/",
type: "get",
data: {
article_id: "{
{ article.id }}",
},
success:function (data){
{
#console.log(data)#}
$.each(data, function (index, coment_obj){
var pid = coment_obj.pid
var add_time = coment_obj.add_time
var content = coment_obj.content
var pk = coment_obj.pk
var username = coment_obj.username
var fu_username = coment_obj.fu_username
if(!pid){
var gen_comment =
`
<li class="list-group-item comment-${
pk} mt-3 px-2 pt-3 pb-2 depth-0" comment_id=${
pk}>
<div class="clearfix" id="div-comment-${
pk}">
<div class="media">
<img src="/static/picture/g-sdk_cFeAJq3pic4ekYTaQMJSx4Q_10.jpg"
class="mr-3 rounded-circle" width="50" height="50"
οnerrοr="javascript:this.src='/static/image/unknow.png';">
<div class="media-body">
<div class="comment-info">
<cite class="c3">
${
username}
</cite>
</div>
<div class="comment-meta"><span
class="font-weight-light text-muted">${
add_time}</span>
</div>
</div>
</div>
<p class="text-break mt-2">${
content}</p>
<a class="btn btn-sm btn-secondary float-right"
οnclick="reply('div-comment-${
pk}','${
pk}')">回复</a>
</div> </li>`
$(".comment-list").append(gen_comment);
}else {
var zi_comment = `
<li class="list-group-item comment-${
pk} mt-3 px-2 pt-3 pb-2 depth-0" comment_id=${
pk}>
<div class="clearfix" id="div-comment-${
pk}">
<div class="media">
<img src="/static/picture/g-sdk_cFeAJq3pic4ekYTaQMJSx4Q_10.jpg"
class="mr-3 rounded-circle" width="50" height="50"
οnerrοr="javascript:this.src='/static/image/unknow.png';">
<div class="media-body">
<div class="comment-info">
<cite class="c3">
${
username}
</cite>
<i class="fa fa-share fa-fw fa-1x mr-2 c1" aria-hidden="true"></i>
<cite class="c3"><a href="#div-comment-48" class="text-reset">${
fu_username}</a></cite>
</div>
<div class="comment-meta"><span
class="font-weight-light text-muted">${
add_time}</span>
</div>
</div>
</div>
<p class="text-break mt-2">${
content}</p>
<a class="btn btn-sm btn-secondary float-right"
οnclick="reply('div-comment-${
pk}','${
pk}')">回复</a>
</div> </li>`
{
#console.log(pid)#}
{
#console.log(fu_username)#}
$("[comment_id="+pid+"]").append('<ol class="children">' + zi_comment + '</ol>');
}
})
}
})
{
# })#}
</script>
html
<div id="comments" class="" style="opacity:0.85;">
<!--评论列表-start-->
<ol class="list-group comment-list" id="comments"></ol>
<!--评论表单-start-->
<div id="respond" class="bg-white mt-3 px-3 pt-3 pb-2">
<a name="comment"></a>
<h4>发表评论
<a class="btn btn-sm btn-info d-none text-white" id="cancel-reply"
onclick="cancelReply();">取消回复</a>
</h4>
<p class="text-muted">电子邮件地址不会被公开。</p>
<form id="comment-form" onsubmit="return false;" method="post" action="{% url 'comment' %}" novalidate>
{% csrf_token %}
<div class="form-group" id="article_content">
<textarea class="form-control OwO-textarea" id="content" name="content" rows="5"
placeholder="居然什么都不说,哼!" autocomplete="off" aria-label="content"
aria-describedby="addon-wrapping" required=""></textarea>
<div class="invalid-feedback" id="content-feedback">
</div>
<div class="OwO">
<a href="javascript: void(0);" class="btn btn-sm btn-warning OwO-logo"
rel="external nofollow"><i class="fa fa-smile-o"
aria-hidden="true"></i>表情</a>
</div>
</div>
<!--评论人信息--->
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text text-center"><i class="fa fa-user"></i></span>
</div>
<input name="username" id="username" type="text" class="form-control"
placeholder="昵称(必填)" aria-label="username"
aria-describedby="addon-wrapping" required="">
<div class="invalid-feedback" id="username-feedback">
</div>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text text-center"><i class="fa fa-envelope-o"></i></span>
</div>
<input name="qq_email" type="email" id="qq_email" class="form-control"
placeholder="QQ邮箱(必填)" aria-label="email"
aria-describedby="addon-wrapping" required="">
<div class="invalid-feedback" id="qq_email-feedback">
请输入正确格式的qq邮箱
</div>
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text text-center"><i class="fa fa-link"></i></span>
</div>
<input name="link" id="web_site" type="text" class="form-control"
placeholder="网站(选填)" aria-label="link" aria-describedby="addon-wrapping">
<div class="invalid-feedback">
请输入以http或https开头的URL,格式如:https://libo_sober.top
</div>
</div>
<button type="submit" class="btn btn-info btn-block" id="comment-submit"
data-placement="top" data-toggle="tooltip" title="评论需博主审核才会显示">提交评论
</button>
<input type="hidden" name="article" value="{
{ article.id }}" id="comment_post_ID">
{# <input type="hidden" name="category_id" value="7" id="comment_post_type">#}
{# <input type="hidden" name="pid" id="pid" value="{
{ }}"><!--为0代表新评论-->#}
{# <input type="hidden" name="csrfmiddlewaretoken"#}
{# value="13fuPwPJplNcsC9wTDaYPs3KK5qS6W6u9HTA2NaMmjdDkHlqCcSdMO92BzDF0BIs">#}
</form>
</div>
<!--评论表单-end-->
</div>
递归部分
把数据造成这样传给html页面渲染
ret = [{
'pid': None,
'fu_username': None,
'pk': 21,
'content': '哈哈哈',
'username': 'libo',
'add_time': '2021-03-11 12:53:14',
'children': [{
'pid': 21,
'fu_username': 'libo',
'pk': 29,
'content': '笑你妈',
'username': 'h8fanc6o',
'add_time': '2021-03-11 13:34:41',
'children': [{
'pid': 29,
'fu_username': 'h8fanc6o',
'pk': 30,
'content': '你管人家',
'username': 'gzjuq2rh',
'add_time': '2021-03-11 13:35:29',
'children': []
}]
}, {
'pid': 21,
'fu_username': 'libo',
'pk': 32,
'content': '开心吗',
'username': 'taibai666',
'add_time': '2021-03-11 13:36:20',
'children': []
}, {
'pid': 21,
'fu_username': 'libo',
'pk': 34,
'content': '<img src="/static/picture/aini_org.png">',
'username': 'libo',
'add_time': '2021-03-13 10:52:43',
'children': []
}]
}, {
'pid': None,
'fu_username': None,
'pk': 31,
'content': '我来了',
'username': 'taibai',
'add_time': '2021-03-11 13:35:49',
'children': []
}]
造数据方法
class ArticleView(View):
def get(self, request, article_id=None):
article = models.Article.objects.get(pk=article_id)
article.viewed() # 增加阅读数P
# 为甚么刷新页面会产生两次访问ArticleView
# 已经解决,因为文章中的请求js或者csss图片等路径为空或出错的,就会自动请求当前路径
mk = mistune.Markdown()
output = mk(article.content)
# 文章分类
categories = models.Category.objects.all()
# 该文章的所有评论
comment_obj = models.Comment.objects.filter(article_id=article_id)
comment_list = self.build_msg(comment_obj)
ret = self.get_comment_list(comment_list)
return render(request, 'datail.html', {
'article': article, 'detail_html': output, 'categories': categories, 'ret': ret})
def get_comment_list(self, comment_list):
# 把msg增加一个chirld键值对,存放它的儿子们
ret = []
comment_dic = {
}
for comment_obj in comment_list:
comment_obj['children'] = []
comment_dic[comment_obj['pk']] = comment_obj
for comment in comment_list:
p_obj = comment_dic.get(comment['pid'])
if not p_obj:
ret.append(comment)
else:
p_obj['children'].append(comment)
return ret
def build_msg(self, comment_obj):
# 把数据造成列表里边套字典的形式
msg = []
for comment in comment_obj:
data = {
}
if comment.pid:
data['pid'] = comment.pid.id
data['fu_username'] = models.Comment.objects.get(pk=comment.pid.id).username
else:
data['pid'] = None
data['fu_username'] = None
data['pk'] = comment.pk
data['content'] = comment.content
data['username'] = comment.username
data['add_time'] = comment.add_time.strftime('%Y-%m-%d %H:%M:%S')
msg.append(data)
return msg
render给html页面后,页面使用自定义的过滤器
{% load custom_tag %}
<!--文章内容区域-start-->
<div id="article-content" class="article-content">
{
{ detail_html | custom_markdown | safe }}}
</div>
<!--文章内容区域-end-->
自定义过滤器进行递归处理
def tree_son(comment):
zi_com = ''
for com in comment:
pk = com['pk']
pid = com['pid']
username = com['username']
add_time = com['add_time']
content = com['content']
fu_username = com['fu_username']
zi_com += f"""
<li class="list-group-item comment-{pk} mt-3 px-2 pt-3 pb-2 depth-0" comment_id={pk}>
<div class="clearfix" id="div-comment-{pk}">
<div class="media">
<img src="/static/picture/g-sdk_cFeAJq3pic4ekYTaQMJSx4Q_10.jpg"
class="mr-3 rounded-circle" width="50" height="50"
οnerrοr="javascript:this.src='/static/image/unknow.png';">
<div class="media-body">
<div class="comment-info">
<cite class="c3">
{username}
</cite>
<i class="fa fa-share fa-fw fa-1x mr-2 c1" aria-hidden="true"></i>
<cite class="c3"><a href="#div-comment-{pid}" class="text-reset">{fu_username}</a></cite>
</div>
<div class="comment-meta"><span
class="font-weight-light text-muted">{add_time}</span>
</div>
</div>
</div>
<p class="text-break mt-2">{content}</p>
<a class="btn btn-sm btn-secondary float-right"
οnclick="reply('div-comment-{pk}','{pk}')">回复</a>
</div> </li>
"""
if com['children'] != []:
zi_com += tree_son(com['children'])
return zi_com
@register.filter(is_safe=True)
def build_coment_tree(ret):
comment = ''
for comment_dicts in ret:
pk = comment_dicts['pk']
username = comment_dicts['username']
add_time = comment_dicts['add_time']
content = comment_dicts['content']
comment += f"""
<li class="list-group-item comment-{pk} mt-3 px-2 pt-3 pb-2 depth-0" comment_id={pk}>
<div class="clearfix" id="div-comment-{pk}">
<div class="media">
<img src="/static/picture/g-sdk_cFeAJq3pic4ekYTaQMJSx4Q_10.jpg"
class="mr-3 rounded-circle" width="50" height="50"
οnerrοr="javascript:this.src='/static/image/unknow.png';">
<div class="media-body">
<div class="comment-info">
<cite class="c3">
{username}
</cite>
</div>
<div class="comment-meta"><span
class="font-weight-light text-muted">{add_time}</span>
</div>
</div>
</div>
<p class="text-break mt-2">{content}</p>
<a class="btn btn-sm btn-secondary float-right"
οnclick="reply('div-comment-{pk}','{pk}')">回复</a>
</div> </li>
"""
if comment_dicts['children'] != []:
comment += tree_son(comment_dicts['children'])
return comment
over