文章显示列表这里采用动态加载的方法,文章回复采用二级评论的方法来实现。
一、文章列表功能
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>
|