一、功能需求分析

1.功能

  1. 新闻详情

  2. 加载评论功能

  3. 添加评论功能

二、新闻详情页

1.业务流程分析

业务流程:

  1. 判断前端传递新闻id是否为空,是否为整数,是否存在

2.接口设计

  1. 接口说明:

请求方法GET
url定义/news/<int:news_id>/
参数格式url路径参数
  1. 参数说明:

news_id整数新闻id
  1. 返回结果:

    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.业务流程分析

业务处理流程:

  1. 判断用户是否登录

  2. 判断前端传的新闻id是否为空,是否我整数,是否存在

  3. 判断评论内容是否为空

  4. 判断是否有父评论,父评论id是否与新闻id匹配

  5. 保存新闻评论

2.接口设计

  1. 接口说明:

请求方法POST
url定义/news/<int:news_id>/comment/
参数格式url路径参数,表单参数
  1. 参数说明:

news_id整数新闻id
content字符串新闻评论内容
parent_id整数父评论id

注意:post请求需要携带csrftoken

  1. 返回结果:

    {
        "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('服务器超时,请重试!');
            })

    })


});
02-11 01:56