[django项目] 在新闻详情页发表评论

本文详细介绍了一个新闻详情页面及评论系统的实现过程,包括前后端交互、评论加载、二级评论支持、用户评论添加等功能,涵盖了从接口设计到前端展示的全过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

新闻详情页

I. 功能需求分析

1>功能

  1. 新闻详情
  2. 加载评论功能
  3. 添加评论功能

II. 新闻详情页

1>业务流程分析

业务流程:

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

2>接口设计

  1. 接口说明:
类目说明
请求方法GET
url定义/news/<int:news_id>/
参数格式url路径参数
  1. 参数说明:
参数名类型是否必须描述
news_id整数新闻id
  1. 返回结果:

    html页面,直接通过模板渲染的方式实现

3>后端代码

3.1>后端视图

第一种写法

# 在news/views.py中定义如下视图
class NewsDetailView(View):
    """
    新闻详情视图
    url: '/news/<int:news_id>/
    """
    # 传递news_id的目的就是为了获取新闻详情页
    def get(self, request, news_id):
        # 1. 校验是否存在
        # 2. 获取数据, 通过select_related获取外键字段
        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:
            # 如果没有对应的news id 就返回Page Not Found
            return HttpResponseNotFound('<h1>Page Not Found</h1>')

第二种写法, 等价于上面的方法, 代码量更少且返回的是404页面

news_queryset = News.objects.select_related('tag', 'author').only(
            'title', 'content', 'update_time', 'tag__name',
            'author__username')
        # 若news的id 不匹配着返回404页面
        news = get_object_or_404(news_queryset, is_delete=False, id=news_id)
        return render(request, 'news/news_detail.html', context={'news': news})
3.2>配置路由
# 在news/urls.py中定义如下路由

urlpatterns = [
	...
    path('news/<int:news_id>/', views.NewDetailView.as_view(), name='news_detail')
]

到这里可以去页面上试一下, 访问一个不存在的news_id, 看是不是会显示Page Not Found

4>前端代码

4.1>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>

    </div>
    <!-- news-contain end  -->
{% endblock %}

{{ news.content|safe }}中的safe一定要加, 不然浏览器会认为不安全的内容, 加载就会出错

4.2>css
/* 为文章内容添加样式 */
/* 在static/css/news/news-detail.css文件中需要添加如下内容:*/

.content p {
	font-size: 16px;
	line-height: 26px;
	text-align: justify;
	word-wrap: break-word;
	padding: 3px 0
}

III. 加载新闻评论

1>接口设计

新闻详情页,直接渲染新闻评论

2>后端代码

2.1>模型代码

评论虽然可以直接跟随新闻详情页一起加载, 但并不能支持二级评论

我们可以让Comments外键关联它自己, 这种方法也是在评论种用的最多的

# 本项目涉及二级评论,修改Comments模型,添加一个parent字段
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True)

修改模型字段后一定要及时迁移

2.2>导入数据
# 导入测试数据tb_comments_20181222.sql
# 一定要保证tb_users中有id为1,2,3的三个用户,不然导入测试数据会报错
mysql -u用户名 -p -D 数据库名< tb_comments_20181222.sql
2.3>视图代码
# 修改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>前端代码

3.1>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/中
3.2>html
<!-- 在templates/news/news_detail.html文件中class="comment-contain"里面加入如下代码: -->
<div class="comment-contain">
            <div class="comment-pub clearfix">...</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>
3.3>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) {
      // 评论
    }
  });
 
});

要记得在news_detail.html中引入

{% block script %}
    <script src="{% static 'js/news/news_detail.js' %}"></script>
{% endblock %}

IIII. 添加新闻评论功能

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>后端代码

3.1>视图代码
# 在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())
3.2>序列化comment对象
# 在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
3.4>路由
# 在news/urls.py中添加如下路由
path('news/<int:news_id>/comment/', views.NewsCommentView.as_view(), name='news_comment'),

4>前端代码

4.1>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 -->
4.2>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('服务器超时,请重试!');
            })
    })
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值