BBS项目-文章评论功能
1 根评论
1.1 评论框样式
评论框在文章详情页下方,首先需要登录后才能显示。
登录后,参考博客园网站,评论框用的富文本编辑器,我们这里简化,就用大段文本输入框替代。评论上方有评论列表(评论楼)。
article_detail.html
{# 文章评论样式开始 #}
<div>
<p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
<div>
<textarea name="comment" id="id_comment" cols="60" rows="10"></textarea>
</div>
<button class="btn btn-primary" id="id_submit">提交评论</button>
</div>
{# 文章评论样式结束 #}
页面效果如下
评论框和点赞点踩图标齐平了,因为点赞点踩的div标签塌陷了,给点赞点踩div标签加一个clearfix的类属性。
article_detail.html 评论框样式需要判断登录用户才能显示,否则提醒登录。“登录”和“逛逛”用a标签,点击可以跳转到相应页面。
{# 文章评论样式开始 #}
{% if request.user.is_authenticated %}
<div>
<p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
<div>
<textarea name="comment" id="id_comment" cols="60" rows="10"></textarea>
</div>
<button class="btn btn-primary" id="id_submit">提交评论</button>
</div>
{% else %}
<p>
<span class="glyphicon glyphicon-comment"></span>
登录后才能查看或发表评论,立即<a href="{% url 'login' %}"> 登录</a> 或者<a href="{% url 'home' %}"> 逛逛</a> BBS首页
</p>
{% endif %}
{# 文章评论样式结束 #}
1.2 ajax请求
当用户登录后,在评论框输入内容,点击“提交评论”,朝后端接口发送post请求。评论功能单独开一个url和视图函数,用于跟前端进行ajax交互。
article_detail.html
$('#id_submit').click(function () {
let $conTent = $('#id_comment').val()
$.ajax({
url: '/comment/',
type: 'post',
data: {
'article_id': '{{ article_obj.pk }}',
'content': $conTent,
},
success: function (args) {
alert(args)
}
})
})
views.py 评论接口逻辑
def comment(request):
if request.is_ajax():
back_dic = {'code': 0, 'msg': ''}
# 校验用户是否登录
if request.user.is_authenticated():
article_id = request.POST.get('article_id')
content = request.POST.get('content')
"""
只要是登录用户都可以评论,自己可以评论自己文章,评论过的文章还可以评论,因此没有其他限制逻辑
直接操作数据库存储,注意文章表的comment_num字段要更新;django中开启事务,两个数据同时更新
"""
with transaction.atomic():
models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') + 1)
models.Comment.objects.create(user=request.user, article_id=article_id, content=content)
back_dic['msg'] = '感谢你的回复:)'
else:
back_dic['code'] = 1
back_dic['msg'] = '请先登录'
return JsonResponse(back_dic)
1.3 评论列表
上述已经完成了评论的前后端交互,但是评论后还需要展示出来,因此要搭建评论列表页面,此页面在文章详情页下方。
在文章详情页视图函数中, 获取当前文章所有评论内容并返回前端展示。
前端评论列表渲染,参考博客园,article_detail.html
{# 评论楼渲染开始 #}
{% if request.user.is_authenticated %}
<div>
<p style="font-size: 18px; font-weight: bolder">评论列表</p>
<ul class="list-group">
{% for comment in comment_list %}
<li class="list-group-item">
<span><a href="#{{ comment.pk }}">#{{ forloop.counter }}楼</a></span> <!--利用a标签锚点 指向评论内容-->
<span>{{ comment.comment_time|date:'Y-m-d H:i' }}</span>
<span><a href="/{{ comment.user.username }}/">{{ comment.user.username }}</a></span> <!--点击跳转到该用户个人站点-->
<span class="pull-right"><a href="#">回复</a></span>
<div><a href="" id="{{ comment.pk }}"></a>{{ comment.content }}</div>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{# 评论楼渲染结束 #}
在评论楼渲染下方,加一个div标签
{# 刷新页面 返回顶部开始 #}
<div class="pull-right" style="margin-right: 20px">
<a href="javascript:location.reload();" style="font-size: 16px">刷新页面</a>
<a href="#id_title" style="font-size: 16px">返回顶部</a> <!--锚点是文章标题的id-->
</div>
{# 刷新页面 返回首页结束 #}
拷贝 bootstrap 列表组样式,ul 套 li 标签。页面效果如下:
参考博客园评论功能,点击提交评论后,会临时渲染出一个评论内容框,页面刷新后评论才更新到评论列表中。
根评论的渲染分两步:1、DOM临时渲染 2、页面刷新render渲染
接下来,完成两件事情:1、点击提交按钮需要将评论框里面的内容清空 2、根评论的两步渲染
$('#id_submit').click(function () {
let $conTent = $('#id_comment').val()
$.ajax({
url: '/comment/',
type: 'post',
data: {
'article_id': '{{ article_obj.pk }}',
'content': $conTent,
},
success: function (args) {
if (args.code === 0 ){
$('#id_msg').text(args.msg) // 评论成功,展示提示信息
$conTent.val('') // 评论提交后,清空评论框内容
// 评论成功,DOM临时渲染评论楼
let $userName = '{{ request.user.username }}' // 需要当前评论用户的用户名
// 需要渲染一个格式,但是不会写,利用js模板字符串,Esc下面的` 拷贝评论楼li标签样式放在这里
let temp=`
<li class="list-group-item">
<span class="glyphicon glyphicon-comment"> ${$userName}:</span>
<div>${$conTent}</div>
</li>
`
$('.list-group').append(temp); // 把渲染好的temp标签添加到ul标签内
} else {
$('#id_msg').text(args.msg) // 评论失败,展示错误信息
}
}
})
})
2 子评论
子评论是对根评论(包括子评论)的回复,因此需要跟回复按钮绑定一个点击事件。当点击回复按钮时,发生了几件事,这里参考博客园。
1、评论框自动聚焦
2、将回复按钮所在的那一行评论人的姓名@username,显示在评论框
3、评论框内部自动换行,光标换行到@username下一行
因此,我们需要进行如下几步:
1、给回复标签绑定点击事件,由于它在for循环中,不能用id查找,我们先给它自定义一个class类属性。
2、我们需要先获取到回复的这条评论的评论人名字以及这条评论的主键值。给回复标签自定义用户名和主键值两个属性,通过属性获取到对应的值。
3、发表根评论和子评论都是朝同一个后端接口提交请求,他们的区别在parent_id,并且表模型允许该字段为null
点击回复按钮,它获取到根评论的主键值,就会修改这个全局的parentID, 传到后端存储,就表示是子评论
4、后端接口,只用多接收一个字段存入数据库即可,传过来的是根评论就是null,传过来的是子评论就会有评论主键值
5、拼接@username放到评论框,并换行,然后聚焦评论框。
$('.reply').click(function () {
//获取想回复的这条评论的用户名
let $commentUserName = $(this).attr('username')
//获取到根评论主键值,直接修改全局的parentID变量
$parentID = $(this).attr('comment_id')
// 拼接@username放到评论框,并换行,然后聚焦评论框
$('#id_comment').val('@' + $commentUserName + '\n').focus()
})
6、在向后端提交数据前,判断是否是子评论,因为我们拼接了@username放到评论框,它会被当做评论内容录入数据库,因此要先截取掉。
7、如果提交的是子评论,需要把全局的parentID清除
8、如果是子评论,前端要渲染出根评论人的姓名
在评论楼渲染中,要做判断,判断是否有parent_id
<div>
<a href="" id="{{ comment.pk }}"></a>
<!--判断当前评论是否是子评论 如果是 需要渲染 @对应的评论人的名字-->
{% if comment.parent_id %}
<p>@{{ comment.parent.user.username }}</p>
{% endif %}
{{ comment.content }}
</div>
3 修改评论与删除评论
1、评论楼渲染,判断评论人是否是当前登录用户,如果是,前端渲染出修改/删除按钮;如果不是,渲染出回复按钮
{% if comment.user == request.user %}
<span class="pull-right">
<!--给修改标签自定义content属性,通过属性即可以获取到评论内容的值-->
<a class="edit" content="{{ comment.content }}" comment_id="{{ comment.pk }}">修改</a>
<a class="delete" comment_id="{{ comment.pk }}">删除</a>
</span>
{% else %}
<!--回复按钮不要跳转,把a标签的href去掉,给它添加一个类用来绑定点击事件-->
<!--给回复标签自定义用户名和主键值两个属性,通过属性即可以获取想要的这两个值-->
<span><a class="pull-right reply" username="{{ comment.user }}" parent_id="{{ comment.pk }}">回复</a></span>
{% endif %}
2、点击修改按钮
第一:把当前评论内容展示到评论框内
第二:评论框下方的button按钮,文本从"提交评论" 变为"修改"
第三:button按钮"修改"旁边有一个a标签“不改了”,点击弹框提示,是否取消修改
$('.edit').click(function () {
//获取到根评论主键值
$commentID = $(this).attr('comment_id')
//获取想要修改的评论内容,放到评论框内
let $commentContent = $(this).attr('content')
$('#id_comment').val($commentContent).focus()
//替换button标签文本
$('#id_submit').text('修改')
//添加a标签文本
$('.no_edit').text('不改了')
})
a标签开始没有文本,是隐藏的,点击"修改"按钮才会出现
<button class="btn btn-primary" id="id_submit">提交评论</button>
<a class="no_edit"></a>
3、点击button按钮“修改”,跟“提交评论”共用一个按钮,需要做判断,判断的依据是是否有评论主键值(选择修改时,会先获取评论主键值)
直接评论是没有主键值的
// 全局变量,一个用来判断是否是子评论, 一个用来判断是否是修改评论
let $parentID = null;
let $commentID = null;
$('#id_submit').click(function () {
let $conTent = $('#id_comment').val();
//判断是否是修改评论
if ($commentID) {
$.ajax({
url: '/edit_comment/',
type: 'post',
data: {
'content': $conTent,
'comment_id': $commentID,
'csrfmiddlewaretoken': '{{ csrf_token }}',
},
success: function (args) {
if (args.code === 0) {
window.location.reload();
$commentID = null;
} else {
$('#id_msg').text(args.msg)
}
}
})
} else {
// 判断当前评论是否是子评论 如果是 需要将我们之前手动渲染的@username去除,这不是评论内容,不要提交到后端
if ($parentID) {
// 找到\n对应的索引利用切片,切掉\n以及前面的@username,切片顾头不顾尾巴,因此索引要+1
let indexNum = $conTent.indexOf('\n') + 1;
// 将indexNum前所有的字符剔除出去 只保留后面的部分,即真正的评论内容
$conTent = $conTent.slice(indexNum)
}
$.ajax({
url: '/comment/',
type: 'post',
data: {
'article_id': '{{ article_obj.pk }}',
'content': $conTent,
'parent_id': $parentID,
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function (args) {
if (args.code === 0) {
// 评论成功,展示提示信息
$('#id_msg').text(args.msg)
// 评论提交后,清空评论框内容
$('#id_comment').val('')
// 评论成功,DOM临时渲染评论楼
// 需要当前评论用户用户名,需要渲染一个格式,但是不会写,利用js模板字符串Esc下面的` 拷贝评论楼样式放在这里
let $userName = '{{ request.user.username }}'
let temp = `
<li class="list-group-item">
<span class="glyphicon glyphicon-comment"> ${$userName}:</span>
<div>${$conTent}</div>
</li>
`
// 把渲染好的temp标签添加到ul标签内
$('.list-group').append(temp);
// 提交一次子评论后,如果没刷新,全局的parentID会一直存在
// 接着再提交根评论,都会沿用parentID,都会是子评论,因此提交完子评论后要清空全局的parentID
$parentID = null;
} else {
$('#id_msg').text(args.msg)
}
}
})
}
})
4、点击不改了
$('.no_edit').click(function () {
let $res = confirm('确认取消修改吗?')
if ($res === true) {
window.location.reload()
}
})
5、点击“删除按钮”
$('.delete').click(function () {
let $res = confirm('确认删除评论吗?')
if ($res === true) {
$commentID = $(this).attr('comment_id')
$.ajax({
url: '/delete_comment/',
type: 'post',
data: {
'article_id': {{ article_obj.pk }},
'comment_id': $commentID,
'csrfmiddlewaretoken': '{{ csrf_token }}',
},
success: function (args) {
if (args.code === 0) {
window.location.reload();
$commentID = null;
} else {
$('#id_msg').text(args.msg)
}
}
})
}
})
6、修改评论后端view
def edit_comment(request):
if request.is_ajax():
back_dic = {'code': 0, 'msg': ''}
if request.user.is_authenticated:
content = request.POST.get('content')
comment_id = request.POST.get('comment_id')
models.Comment.objects.filter(pk=comment_id).update(content=content)
back_dic['msg'] = '修改成功'
else:
back_dic['code'] = 1
back_dic['msg'] = '请先登录'
return JsonResponse(back_dic)
7、删除评论后端view
def delete_comment(request):
if request.is_ajax():
back_dic = {'code': 0, 'msg': ''}
if request.user.is_authenticated:
comment_id = request.POST.get('comment_id')
article_id = request.POST.get('article_id')
with transaction.atomic():
models.Article.objects.filter(pk=article_id).update(comment_num=F('comment_num') - 1)
models.Comment.objects.filter(pk=comment_id).delete()
back_dic['msg'] = '删除成功'
else:
back_dic['code'] = 1
back_dic['msg'] = '请先登录'
return JsonResponse(back_dic)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人