分享

Django blog项目《十一》:文章模块2 《文章列表、详情、评论、回复》

 看见就非常 2020-04-29

文章显示列表这里采用动态加载的方法,文章回复采用二级评论的方法来实现。

一、文章列表功能

1. urls.py配置

news/urls.py

from django.urls import path
from news import views


app_name = "news"

urlpatterns = [
    path("article_list/", views.ArticleView.as_view(), name="article_list"),
]

2. 后端view逻辑处理

news/views.py

import logging

from django.views import View
from django.shortcuts import render
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

from news import contains  # 上一章已有
from news import models as _models
from utils.res_code.json_function import to_json_data

logger = logging.getLogger("django")


class ArticleView(View):
    def get(self, request):
        # 1. 获取前端传来的数据文章类型id
        try:
            tag_id = int(request.GET.get("tag_id", 0))
        except Exception as e:
            logger.error("文章标签id参数错误:{}".format(e))
            tag_id = 0

        # 2. 获取前端传来的数据页码编号 page_num
        try:
            page_num = int(request.GET.get("page", 1))
        except Exception as e:
            logger.error("文章页码参数错误:{}".format(e))
            page_num = 1

        # 3. 从数据库中获取文章表签id为tag_id的数据
        article_queryset_total = _models.Articles.objects.select_related("author", "tag").only("id", "title", "digest",
                                                                                    "update_time",
                                                                                    "clicks","image_url",
                                                                                    "author__username",
                                                                                    "tag__name").filter(is_delete=False)

        article_tag_queryset = article_queryset_total.filter(is_delete=False, tag_id=tag_id) or                           article_queryset_total.filter(is_delete=False)

        # 4. 对数据尽心分页
        pagen = Paginator(article_tag_queryset,contains.PER_PAGE_DATA_NUMBER)  # 传递待分页对象 、每页多少个

        # 5. 返回当页数据
        try:
            article_list = pagen.page(page_num)
        except EmptyPage:
            logger.error("访问页数超过总页数")
            article_list = pagen.page(pagen.num_pages)

        # 6. 数据序列化
        article_per_page_list = []
        for article in article_list:
            article_per_page_list.append({
                "id": article.id,
                "title": article.title,
                "digest": article.digest,
                "update_time": article.update_time.strftime("%Y年%m月%d日 %H:%M"),
                "clicks": article.clicks,
                "image_url": article.image_url,
                "author": article.author.username,
                "tag_name": article.tag.name,
            })

        data = {
            "total_page": pagen.num_pages,
            "article": article_per_page_list
        }
        return to_json_data(data=data)


3. 前端js实现

js/news/index.js

$(function () {
    let iPage = 1;  // 设定默认的page页码为1
    let sCurrentTagId = 0; // 设定默认的tag_id为0
    let $newsList = $(".news-nav ul li"); //获取到标签栏
    let iTotalPage = 1;  //设定默认的总页数为1
    let bIsLoadData = true;   //是否正在向后端传递参数

    fn_load_content();   //调用向后端请求数据函数

    $newsList.click(function () {
        //点击分类标签,则为点击的标签加上一个class属性为active
        //冰衣橱其他兄弟元素的上的值为active的class属性
        $(this).addClass("active").siblings("li").attr("active");
        //获取绑定在当前选中分类上的data-id属性值
        let sClickTagId = $(this).children("a").attr("data-id");

        if (sClickTagId !== sCurrentTagId) {
            sCurrentTagId = sClickTagId; //记录当前的分类id
            iPage = 1;
            iTotalPage = 1;
            fn_load_content()
        }

    });

    // 滚动鼠标动态加载数据
    $(window).scroll(function () {
        // 浏览器窗口高度
        let showHeight = $(window).height();

        //整个网页的高度
        let pageHeight = $(document).height();

        //页面可以滚动的距离
        let canScrollHeight = pageHeight - showHeight;

        // 页面滚动了多少,整个是睡着页面滚动实时变化的
        let nowScroll = $(document).scrollTop();

        if ((canScrollHeight - nowScroll) < 100) {
            //判断页数,去更新新闻数据
            if (!bIsLoadData) {
                bIsLoadData = true;
                //如果当前页面数据如果小于总页数,那么才去加载数据
                if (iPage < iTotalPage) {
                    iPage += 1;
                    $(".btn-more").remove(); //删除标签
                    //去加载数据
                    fn_load_content()
                } else {
                    alert("已经全部加载,没有更多内容了!");
                    $(".btn-more").remove(); //删除标签
                    $(".news-list").append($('<a href="javascript:void(0)" class="btn-more">已全部加载 </a>'))
                }
            }

        }
    });

    // 向前端发送请求获取数据
    function fn_load_content() {
        //构建参数
        let sDataParams = {
            "page": iPage,
            "tag_id": sCurrentTagId,
        };

        //发送ajax请求获取数据
        $.ajax({
            url: "/news/article_list",
            type: "GET",
            data: sDataParams,
            dataType: "json",
            success: function (res) {
                if (res.errno === "200") {
                    iTotalPage = res.data.total_page;  //获取到总页数
                    if (iPage === 1) {
                        $(".news-list").html("");
                    }
                    res.data.article.forEach(function (one_article) {
                        let content = `
                            <li class="news-item">
                                <a href="/news/${ one_article.id }/" class="news-thumbnail" 
                                target="_blank">
                                    <img src="${ one_article.image_url }" alt="${ one_article.title }" title="${ one_article.title }">
                                </a>
                                <div class="news-content">
                                    <h4 class="news-title">
                                        <a href="/news/${ one_article.id }/">${ one_article.title }</a>
                                    </h4>
                                    <p class="news-details">${ one_article.digest }</p>
                                    <div class="news-other">
                                        <span class="news-type">${ one_article.tag_name }</span>
                                        <span class="news-clicks">${ one_article.clicks }</span>
                                        <span class="news-time">${ one_article.update_time}</span>
                                        <span class="news-author">${ one_article.author}</span>
                                    </div>
                                </div>
                            </li>
                        `;
                        $(".news-list").append(content)
                    });
                    $(".news-list").append($('<a href="javascript:void(0);" class="btn-more">点击加载更多</a>'));
                    bIsLoadData = false
                }
                else {
                    alert(res.errmsg)
                }
            },
            error: function () {
                alert("服务器请求超时,请重试")
            }


        })

    }

});

二、文章详情

1. urls.py配置

news/urls.py

from django.urls import path
from news import views


app_name = "news"

urlpatterns = [
    path("<int:article_id>/", views.ArticleDetailView.as_view(), name="article_detail"),
]

2. 后端view逻辑处理

news/views.py

from django.views import View
from django.shortcuts import render
from django.http import HttpResponseNotFound

from news import models as _models


class ArticleDetailView(View):
    """
    news detail
    """
    def get(self, request, article_id):
        # 1. 从数据库Articles中获取id=article_id的数据:title、update_time、content、tag_name、author,
        article = _models.Articles.objects.select_related("author", "tag").only("id", "title", "update_time", "content",
                                                                                "tag__name",
                                                                                "author__username").filter(is_delete=False, id=article_id).first()

        # 2. 获取文章评论数据
        comment_queryset_list = _models.Comments.objects.select_related("author", "parent").only("content",
                                                                                                 "update_time",
                                                                                                 "author__username",
                                                                                                 "parent__author__username",
                                                                                                 "parent__content",
                                                                                                 "parent__update_time").filter(
            is_delete=False, article_id=article_id)

        comment_list = []
        for comment in comment_queryset_list:
            comment_list.append(comment.to_dict_data())  # 引用Comments中自定义的字典转换

        le = len(comment_list)  # 文章评论数

        # 3. 判断是否取到文章数据
        if article:
            return render(request, "news/news_detail.html", locals())
        else:
            return HttpResponseNotFound("<h1>Page Not Found<h1>")

3. 前端html填充

news/article_detail.html

<div class="news-info">
    <div class="news-info-left">
        <span class="news-author">{{ article.author.username }}</span>
        <span class="news-pub-time">{{ article.update_time }}</span>
        <span class="news-type">{{ article.tag.name }}</span>
    </div>
</div>
<br/>
<br/>
<article class="news-content">
    {{ article.content | safe }}
</article>

三、文章评论和回复

1. urls.py配置

news/urls.py

from django.urls import path
from news import views


app_name = "news"

urlpatterns = [
    path("<int:article_id>/comments/", views.ArticleCommentView.as_view(), name="comments"),
]

2. 后端view逻辑处理

评论和回复用的是同一套处理逻辑

news/views.py

import json

from django.views import View

from news import models as _models
from utils.res_code.res_code import Code, error_map
from utils.res_code.json_function import to_json_data


class ArticleCommentView(View):
    """
    news comment replay view
    route: /news/<int:article_id>/comments
    """
    # 1. 创建1个post,url带有article_id参数
    def post(self, request, article_id):

        # 要先判断是否用户已经登录(必须登录后才能进行评论)
        if not request.user.is_authenticated:
            return to_json_data(errno=Code.SESSIONERR, errmsg=error_map[Code.SESSIONERR])

        # 2. 获取前端传来的参数
        try:
            json_data = request.body
            if not json_data:
                return to_json_data(errno=Code.PARAMERR, errmsg="参数错误,请重新输入")
            dict_data = json.loads(json_data)
        except Exception as e:
            return to_json_data(errno=Code.UNKOWNERR, errmsg=error_map[Code.UNKOWNERR])

        # 3. 获取前端传来的回复内容
        content = dict_data.get("content")

        # 4. 判断content是否为空
        if not content:
            return to_json_data(errno=Code.PARAMERR, errmsg="评论内容为空,请输入!")

        # 5. 获取父评论id
        parent_id = dict_data.get("parent_id")

        try:
            if parent_id:
                parent_id = int(parent_id)
                # 判断文章id和父评论id是否和传来过同时满足
                if not _models.Comments.objects.only("id").filter(is_delete=False, id=parent_id,
                                                              article_id=article_id).exists():
                    return to_json_data(errno=Code.PARAMERR, errmsg=error_map[Code.UNKOWNERR])
        except Exception as e:
            logger.error("父评论id异常:{}".format(e))
            return to_json_data(errno=Code.PARAMERR, errmsg="父评论ID参数异常")

        # 6. 保存到数据库
        article_comment = _models.Comments()  # 获取评论实例
        article_comment.content = content  # 保存评论内容
        article_comment.article_id = article_id  # 保存评论文章id
        article_comment.author = request.user
        article_comment.parent_id = parent_id if parent_id else None  # 判断是否为空
        article_comment.save()  # 保存实例

        count = _models.Comments.objects.only("id").filter(is_delete=False, article_id=article_id).count()  # 评论条数实时加载

        return to_json_data(data={
            "data": article_comment.to_dict_data(),
            "count": count,
        }
        )

3. 前端js实现

评论和回复的js实现基本一致

js/news/index.js

$(function () {
    // 未登录提示框
    let $loginComment = $('.please-login-comment input');  //获取请登录框
    let $sendComment = $('.logged-comment .comment-btn');  //获取到评论按钮
    let $commentCount = $('.new-comment .comment-count');   //获取到评论条数


    $('.comment-list').delegate('a,input', 'click', function () {  //delegate委托事件
        let sClassValue = $(this).prop('class'); //取出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) {
            //进行发送ajax请求数据给后端
            let $this = $(this);  //获取当前点击回复
            let article_id = $this.parent().attr("article_id");  //上一个评论的的文章id
            let parent_id = $this.parent().attr("comment_id");   //上一个评论的id
            let content = $this.prev().val();//评论内容

            //判断评论内容是是否为空
            if (!content) {
                alert("评论内容为空,请重新输入");
            }

            //构造ajax请求参数
            let sDataParams = {
                "content": content,
                "parent_id": parent_id,
            };

            //发送ajax请求
            $.ajax({
                url: '/news/' + article_id + "/comments/",
                type: "POST",
                contentType: "application/json, charset=utf-8",
                data: JSON.stringify(sDataParams),
                dataType: 'json',
            })
                .done(function (res) {
                    if (res.errno === "200") {
                        let comment = res.data.data;
                        let html_content = ``;
                        $commentCount.empty();  //移除文本内容
                        $commentCount.text(res.data.count); //添加新的评论数

                        html_content += `
                        <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 }
                            </div>
                            <div class="comment-content">${ comment.content }</div>
                            <div class="parent_comment_text">
                                <div class="parent_username">${ comment.parent.author }</div>
                                <br/>
                                <div class="parent_content_text">${ comment.parent.content }</div>
                            </div>
                            
                        
                            <div class="comment_time left_float">${comment.update_time}</div>
                            <a href="javascript:void(0);" class="reply_a_tag right_float">回复</a>
                            <form class="reply_form left_float" comment_id="${comment.comment_id}" article_id="${comment.article_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>`;

                        $('.comment-list').prepend(html_content);
                        $this.prev().val("");   //清空输入框
                        $this.parent().hide();   //关闭评论框
                    } else if (res.errno = "4101") {
                        alert("请登录后再评论");
                        setTimeout(function () {
                            //重定向到登录界面
                            window.location.href = "/users/login"
                        }, 800)
                    } else {
                        alert(res.errmsg)
                    }
                })
                .fail(function () {
                    alert("服务器超时,请重试!")
                })
        }
    });

    //点击评论框,重定向到用户登录页面
    $loginComment.click(function () {
        $.ajax({
            url: "/news/" + $(".please-login-comment").attr("article_id") + "/comments/",
            type: "POST",
            contentType: "applications/json, charset=utf-8",
            dataType: "json",
        })
            .done(function (res) {
                if (res.errno === "4101") {
                    setTimeout(function () {
                        window.location.href = "/users/login";
                    }, 800)
                } else {
                    alert(res.message);
                }
            })
            .fail(function () {
                alert("服务器超时,请重试!");
            })
    });


    // 发表评论
    $sendComment.click(function () {
        //获取到文章的id,评论的id,评论内容
        let $this = $(this);
        let article_id = $this.parent().attr("article_id");
        let content = $this.prev().val();


        if (!content) {
            alert("评论内容为空,请重新输入!");
            return
        }
        let sDataParams = {
            "content": content,
        };

        $.ajax({
            url: '/news/' + article_id + "/comments/",
            type: "POST",
            contentType: "application/json, charset=utf-8",
            data: JSON.stringify(sDataParams),
            dataType: 'json',
        })
            .done(function (res) {
                if (res.errno === "200") {
                    let comment = res.data.data;
                    let html_content = ``;
                    $commentCount.empty();
                    $commentCount.text(res.data.count); //添加新的评论数

                    html_content += `
                        <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>                          

                            <div class="comment_time left_float">${ comment.update_time }</div>
                            <a href="javascript:void(0);" class="reply_a_tag right_float">回复</a>
                            <form class="reply_form left_float" comment_id="${comment.comment_id}" article_id="${comment.article_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>`;

                    $('.comment-list').prepend(html_content);
                    $this.prev().val("");   //清空输入框
                } else if (res.errno = "4101") {
                    alert("请登录后再评论");
                    setTimeout(function () {
                        //重定向到登录界面
                        window.location.href = "/users/login";
                    }, 800)
                } else {
                    alert(res.errmsg)
                }
            })
            .fail(function () {
                alert("服务器超时,请重试!")
            })
    });


    // get cookie using jQuery
    function getCookie(name) {
        let cookieValue = null;
        if (document.cookie && document.cookie !== '') {
            let cookies = document.cookie.split(';');
            for (let i = 0; i < cookies.length; i++) {
                let cookie = jQuery.trim(cookies[i]);
                // Does this cookie string begin with the name we want?
                if (cookie.substring(0, name.length + 1) === (name + '=')) {
                    cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                    break;
                }
            }
        }
        return cookieValue;
    }

    function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
    }

    // Setting the token on the AJAX request
    $.ajaxSetup({
        beforeSend: function (xhr, settings) {
            if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
                xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
            }
        }
    });

});

4. 前端html填充

news/index.html

<div class="comment-contain">
    <div class="comment-pub clearfix">
        <div class="new-comment">文章评论(<span class="comment-count">{{ le }}</span>)</div>

        {# 用户登录后才能评论 #}
        {% if user.is_authenticated %}
        <div class="comment-control logged-comment" article_id="{{ article.id }}">
            <input type="text" placeholder="请填写评论">
            <button class="comment-btn">发表评论</button>
        </div>

        {% else %}
        <div class="comment-control please-login-comment" article_id="{{ article.id }}"
             style="display: none">
            <input type="text" placeholder="请登录后参加评论" readonly>
            <button class="comment-btn">发表评论</button>
        </div>
        {% endif %}


    </div>

    <ul class="comment-list">
        {# 评论内容 #}
        {% for comment in comment_list %}
        <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>

            {# 子评论内容 #}
            {% if comment.parent %}
            <div class="parent_comment_text">
                <div class="parent_username">{{ comment.parent.author }}</div>
                <br/>
                <div class="parent_content_text">{{ comment.parent.content }}</div>
            </div>

            {% endif %}

            <div class="comment_time left_float">{{ comment.parent.update_time }}</div>
            <a href="javascript:void(0);" class="reply_a_tag right_float">回复</a>
            <form class="reply_form left_float" comment_id="{{ comment.comment_id }}" article_id="
                                                                                                  {{ comment.article_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 %}
    </ul>
</div>

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多