一、功能需求分析
1.功能
新闻详情
加载评论功能
添加评论功能
二、新闻详情页
1.业务流程分析
业务流程:
判断前端传递新闻id是否为空,是否为整数,是否存在
2.接口设计
接口说明:
类目 | 说明 |
---|---|
请求方法 | GET |
url定义 | /news/<int:news_id>/ |
参数格式 | url路径参数 |
参数说明:
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
news_id | 整数 | 是 | 新闻id |
返回结果:
html页面,直接通过模板渲染的方式实现
3.后端代码
视图
# 在news/views.py中定义如下视图 class NewDetailView(View): def get(self, request, news_id): news = News.objects.select_related('tag', 'author').only( 'title', 'content', 'update_time', 'tag__name', 'author__username').filter( is_delete=False, id=news_id).first() if news: return render(request, 'news/news_detail.html', context={ 'news': news, }) else: return HttpResponseNotFound('<h1>Page not found</h1>') # 快捷方式 # 1. 去数据库获取新闻数据 # news_queryset = News.objects.select_related('tag', 'author').only('title', 'content', 'update_time', 'tag__name', 'author__username') # news = get_object_or_404(news_queryset, is_delete=False, id=news_id) # 2. 返回渲染页面 # return render(request, 'news/news_detail.html', context={'news': news})
路由
# 在news/urls.py中定义如下路由 urlpatterns = [ #.... path('news/<int:news_id>/', views.NewDetailView.as_view(), name='news_detail') ]
4.前端代码
html
<!-- templates/news/news_detail.html --> {% extends 'base/base.html' %} {% load static %} {% block title %}文章详情{% endblock %} {% block link %} <link rel="stylesheet" href="{% static 'css/news/news-detail.css' %}"> {% endblock %} {% block main_contain %} <!-- news-contain start --> <div class="news-contain"> <h1 class="news-title">{{ news.title }}</h1> <div class="news-info"> <div class="news-info-left"> <span class="news-author">{{ news.author.username }}</span> <span class="news-pub-time">{{ news.update_time }}</span> <span class="news-type">{{ news.tag.name }}</span> </div> </div> <article class="news-content"> {{ news.content|safe }} </article> <div class="comment-contain"> <div class="comment-pub clearfix"> <div class="new-comment"> 文章评论(<span class="comment-count">0</span>) </div> <div class="comment-control please-login-comment" style="display:none;"> <input type="text" placeholder="请登录后参加评论"> </div> <div class="comment-control logged-comment"> <input type="text" placeholder="请填写评论"> </div> <button class="comment-btn">发表评论</button> </div> <ul class="comment-list"> <li class="comment-item"> <div class="comment-info clearfix"> <img src="../images/avatar.jpeg" alt="avatar" class="comment-avatar"> <span class="comment-user">评论人</span> <span class="comment-pub-time">1小时前</span> </div> <div class="comment-content">这是一条评论</div> </li> <li class="comment-item"> <div class="comment-info clearfix"> <img src="../images/avatar.jpeg" alt="avatar" class="comment-avatar"> <span class="comment-user">评论人</span> <span class="comment-pub-time">1小时前</span> </div> <div class="comment-content">这是一条评论</div> </li> </ul> </div> </div> <!-- news-contain end --> {% endblock %} {% block script %} {% endblock %}
css
/* 为文章内容添加样式 */ /* 在static/css/news/news-detail.css文件中需要添加如下内容:*/ .news-content p { font-size: 16px; line-height: 26px; text-align: justify; word-wrap: break-word; padding: 3px 0 }
三、加载新闻评论
1.接口设计
新闻详情页,直接渲染新闻评论
2.后端代码
模型代码
# 本项目设计二级评论,修改Comments模型,添加一个parent字段 parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)
修改模型后一定要及时迁移
导入数据,文件见我的文件中的sql数据包.rar
# 导入测试数据tb_comments_20181222.sql # 一定要保证tb_users中有id为1,2,3的三个用户,不然导入测试数据会报错 mysql -u用户名 -p -D 数据库名< tb_comments_20181222.sql
注意: 如果你想删除有外键关联的表是不能在navicat中用查询-新建查询的命令truncate table_name 来删除表中所有数据的,只能重新弄过
视图代码
# 修改news/views.py中的NewsDetailView视图 class NewDetailView(View): def get(self, request, news_id): news = News.objects.select_related('tag', 'author').only( 'title', 'content', 'update_time', 'tag__name', 'author__username').filter( is_delete=False, id=news_id).first() if news: comments = Comments.objects.select_related('author', 'parent').only( 'content', 'author__username', 'update_time', 'parent__author__username', 'parent__content', 'parent__update_time').filter(is_delete=False, news_id=news_id) return render(request, 'news/news_detail.html', context={ 'news': news, 'comments': comments }) else: return HttpResponseNotFound('<h1>Page not found</h1>')
3.前端代码
css
/* 在static/css/news/news-detail.css中添加如下代码: */ .comment-list .comment-item { /*把这条样式注释掉*/ /*border-bottom: 1px solid #ddd;*/ margin-bottom: 30px; } /* ========= 为父评论添加样式 start============ */ .left_float{ float:left; } .right_float{ float:right; } .parent_comment_text{ width:698px; padding:8px; background: #f4facf; margin:10px 0 0 60px; } .comment_time{ font-size:12px; color:#999; margin:10px 0 0 60px; } .parent_comment_text .parent_username{ font-size:12px; color:#000; display:inline-block; } .parent_comment_text .comment_time{ display: inline-block; float:right; } .parent_comment_text .parent_content_text{ color:#666; font-size:14px; margin-top: 20px; } .reply_a_tag{ font-size:12px; color:#999; text-indent:20px; margin:10px 0 0 20px; background:url('/static/images/content_icon.png') left center no-repeat; } .reply_form{ width:718px; overflow:hidden; margin:10px 0 0 60px; display:none; } .reply_input{ float:left; width:692px; height:30px; border-radius:4px; padding:10px; outline:none; border:1px solid #2185ed; } .reply_btn,.reply_cancel{ width:40px; height:23px; background:#76b6f4; border:0px; border-radius:2px; color:#fff; margin:10px 5px 0 10px; cursor:pointer; } .reply_cancel{ background:#fff; color: #909090; } /* ========= 为父评论添加样式 end============ */
将content_icon.png图片放到static/images/中
html
<!-- 在templates/news/news_detail.html文件中class="comment-contain"里面加入如下代码: --> <div class="comment-contain"> <div class="comment-pub clearfix"> <div class="new-comment"> 文章评论(<span class="comment-count">0</span>) </div> <div class="comment-control please-login-comment" style="display:none;"> <input type="text" placeholder="请登录后参加评论"> </div> <div class="comment-control logged-comment"> <input type="text" placeholder="请填写评论"> </div> <button class="comment-btn">发表评论</button> </div> <ul class="comment-list"> {% for comment in comments %} <li class="comment-item"> <div class="comment-info clearfix"> <img src="/static/images/avatar.jpeg" alt="avatar" class="comment-avatar"> <span class="comment-user">{{ comment.author.username }}</span> <span class="comment-pub-time">{{ comment.update_time }}</span> </div> <div class="comment-content">{{ comment.content }}</div> {% if comment.parent %} <div class="parent_comment_text"> <div class="parent_username">{{ comment.parent.author }}</div> <div class="comment_time">{{ comment.parent.update_time }}</div> <div class="parent_content_text"> {{ comment.parent.content }} </div> </div> {% endif %} <a href="javascript:void(0);" class="reply_a_tag right_float">回复</a> <form class="reply_form left_float" comment-id="{{ comment.id }}" news-id="{{ comment.news_id }}"> <textarea class="reply_input"></textarea> <input type="button" value="回复" class="reply_btn right_float"> <input type="reset" name="" value="取消" class="reply_cancel right_float"> </form> </li> {% endfor comments %} </ul> </div>
js代码
// 在static/js/news/news_detail.js中加入如下代码: $(function () { $('.comment-list').delegate('a,input', 'click', function () { //获取回复按钮的class属性 let sClassValue = $(this).prop('class'); // 如果点击的是回复按钮,就显示输入框 if (sClassValue.indexOf('reply_a_tag') >= 0) { $(this).next().toggle(); } // 如果点击的是取消按钮,就隐藏输入框 if (sClassValue.indexOf('reply_cancel') >= 0) { $(this).parent().toggle(); } if (sClassValue.indexOf('reply_btn') >= 0) { // 评论 } }); });
四、添加新闻评论功能
1.业务流程分析
业务处理流程:
判断用户是否登录
判断前端传的新闻id是否为空,是否我整数,是否存在
判断评论内容是否为空
判断是否有父评论,父评论id是否与新闻id匹配
保存新闻评论
2.接口设计
接口说明:
类目 | 说明 |
---|---|
请求方法 | POST |
url定义 | /news/<int:news_id>/comment/ |
参数格式 | url路径参数,表单参数 |
参数说明:
参数名 | 类型 | 是否必须 | 描述 |
---|---|---|---|
news_id | 整数 | 是 | 新闻id |
content | 字符串 | 是 | 新闻评论内容 |
parent_id | 整数 | 否 | 父评论id |
注意:post请求需要携带csrftoken
返回结果:
{ "errno": "0", "errmsg": "", "data": { "news_id": 1170, "content_id": 3569, "content": "评论比较中肯。", "author": "admin", "update_time": "2019年08月19日 16:00", "parent": { "news_id": 1170, "content_id": 893, "content": "行文思路简单肤浅,文章结构平面呆板。", "author": "xinlan", "update_time": "2018年12月21日 11:17", "parent": null } } }
3.后端代码
视图代码
# 在news/views.py中编写如下视图 class NewsCommentView(View): """ 添加评论视图 url: /news/<int:news_id>/comment/ """ def post(self, request, news_id): # 是否登录 if not request.user.is_authenticated: return json_response(errno=Code.SESSIONERR, errmsg=error_map[Code.SESSIONERR]) # 新闻是否存在 if not News.objects.only('id').filter(is_delete=False, id=news_id).exists(): return json_response(errno=Code.PARAMERR, errmsg='新闻不存在!') content = request.POST.get('content') # 内容是否为空 if not content: return json_response(errno=Code.PARAMERR, errmsg='评论内容不能为空!') # 父id是否正常 parent_id = request.POST.get('parent_id') if parent_id: try: parent_id = int(parent_id) if not Comments.objects.only('id').filter(is_delete=False, id=parent_id, news_id=news_id).exists(): return json_response(errno=Code.PARAMERR, errmsg=error_map[Code.PARAMERR]) except Exception as e: logger.info('前端传递过来的parent_id异常\n{}'.format(e)) return json_response(errno=Code.PARAMERR, errmsg='未知异常') # 保存到数据库 new_comment = Comments() new_comment.content = content new_comment.news_id = news_id new_comment.author = request.user new_comment.parent_id = parent_id if parent_id else None new_comment.save() return json_response(data=new_comment.to_dict_data())
序列化comment对象(也就是在上面代码的结尾处用到的方法to_dict_data的书写), 这里我们只实现了2层评论关系,无法多个嵌套
# 在news/models.py的Comment模型中添加如下方法,用来序列化 def to_dict_data(self): comment_dict = { 'news_id': self.news_id, 'content_id': self.id, 'content': self.content, 'author': self.author.username, 'update_time': self.update_time.astimezone().strftime('%Y年%m月%d日 %H:%M'), 'parent': self.parent.to_dict_data() if self.parent else None } return comment_dict
路由
# 在news/urls.py中添加如下路由 path('news/<int:news_id>/comment/', views.NewsCommentView.as_view(), name='news_comment'),
4.前端代码
html
<!-- 修改templates/news/news_detail.html中评论部分代码如下 --> <!-- news comment start --> <div class="comment-contain"> <div class="comment-pub clearfix"> <div class="new-comment"> 文章评论(<span class="comment-count">0</span>) </div> {% if user.is_authenticated %} <div class="comment-control logged-comment" news-id="{{ news.id }}"> <input type="text" placeholder="请填写评论"> </div> {% else %} <div class="comment-control please-login-comment"> <input type="text" placeholder="请登录后参加评论" readonly> </div> {% endif %} <button class="comment-btn">发表评论</button> {% csrf_token %} </div> <!-- news comment end -->
js代码
// 修改static/js/news/news_detail.js中的代码如下 // 修改static/js/news/news_detail.js中的代码如下 $(function () { // 对评论进行评论 $('.comment-list').delegate('a,input', 'click', function () { //获取回复按钮的class属性 let sClassValue = $(this).prop('class'); // 如果点击的是回复按钮,就显示输入框 if (sClassValue.indexOf('reply_a_tag') >= 0) { $(this).next().toggle(); } // 如果点击的是取消按钮,就隐藏输入框 if (sClassValue.indexOf('reply_cancel') >= 0) { $(this).parent().toggle(); } if (sClassValue.indexOf('reply_btn') >= 0) { // 评论 let $this = $(this); let news_id = $this.parent().attr('news-id'); let parent_id = $this.parent().attr('comment-id'); let content = $this.prev().val(); if (!content) { message.showError('请输入评论内容!'); return } $ .ajax({ url: '/news/' + news_id + '/comment/', type: 'POST', data: { content: content, parent_id: parent_id }, dataType: "json" }) .done((res) => { if (res.errno === '0') { let comment = res.data; let html_comment = `<li class="comment-item"> <div class="comment-info clearfix"> <img src="/static/images/avatar.jpeg" alt="avatar" class="comment-avatar"> <span class="comment-user">${comment.author}</span> </div> <div class="comment-content">${comment.content}</div> <div class="parent_comment_text"> <div class="parent_username">${comment.parent.author}</div> <div class="comment_time">${comment.parent.update_time}</div> <div class="parent_content_text"> ${comment.parent.content} </div> </div> <div class="comment_time left_float">${comment.update_time}</div> <a href="javascript:;" class="reply_a_tag right_float">回复</a> <form class="reply_form left_float" comment-id="${comment.content_id}" news-id="${comment.news_id}"> <textarea class="reply_input"></textarea> <input type="button" value="回复" class="reply_btn right_float"> <input type="reset" name="" value="取消" class="reply_cancel right_float"> </form> </li>`; message.showSuccess('评论成功!'); setTimeout(() => { $('.comment-list').prepend(html_comment); }, 800); $this.prev().val(''); // 清空输入框 $this.parent().hide(); // 关闭评论框 } else if (res.errno === '4101') { // 用户未登录 message.showError(res.errmsg); setTimeout(() => { window.location.href = '/user/login/' }, 800) } else { // 失败 message.showError(res.errmsg) } }) .fail(() => { message.showError('服务器超时,请重试') }) } }); // 对新闻评论 let $newsComment = $('.logged-comment input'); // 新闻评论框 let $sendComment = $('.comment-pub .comment-btn'); // 新闻评论按钮 $sendComment.click(function () { let $this = $(this); if ($this.prev().hasClass('please-login-comment')) { message.showError('未登录,请登录后再评论!'); setTimeout(() => { window.location.href = '/user/login/' }, 800); return } let news_id = $this.prev().attr('news-id'); let content = $newsComment.val(); if (!content) { message.showError('请输入评论内容!'); return } $ .ajax({ url: '/news/' + news_id + '/comment/', type: 'POST', data: { content: content }, dataType: 'json' }) .done((res) => { if (res.errno === '0') { let comment = res.data; let html_comment = `<li class="comment-item"> <div class="comment-info clearfix"> <img src="/static/images/avatar.jpeg" alt="avatar" class="comment-avatar"> <span class="comment-user">${comment.author}</span> <span class="comment-pub-time">${ comment.update_time }</span> </div> <div class="comment-content">${comment.content}</div> <a href="javascript:;" class="reply_a_tag right_float">回复</a> <form class="reply_form left_float" comment-id="${comment.content_id}" news-id="${comment.news_id}"> <textarea class="reply_input"></textarea> <input type="button" value="回复" class="reply_btn right_float"> <input type="reset" name="" value="取消" class="reply_cancel right_float"> </form> </li>`; message.showSuccess('评论成功!'); setTimeout(() => { $(".comment-list").prepend(html_comment); }, 800); // 清空 $newsComment.val(''); } else if (res.errno === '4101') { // 用户未登录 message.showError(res.errmsg); setTimeout(() => { window.location.href = '/user/login/' }, 800) } else { message.showError(res.errmsg); } }) .fail(() => { message.showError('服务器超时,请重试!'); }) }) });