day08 文章详情页设计
文章详情页
# url设计
/username/article/1
# 先验证url是否会被其他url顶替
# 文章详情页和个人站点基本一致 所以用模版继承
# 侧边栏的渲染需要传输数据才能渲染 并且该侧边栏在很多页面都需要使用
1.哪个地方用就拷贝需要的代码(不推荐 有点繁琐)
2.将侧边栏制作成inclusion_tag
"""
步骤
1.在应用下创建一个名字必须叫templatetags文件夹
2.在该文件夹内创建一个任意名称的py文件
3.在该py文件内先固定写两行代码
from django import template
register = template.Library()
# 自定义过滤器
# 自定义标签
# 自定义inclusion_tag
"""
# 自定义inclusion_tag
@register.inclusion_tag('left_menu.html')
def left_menu(username):
# 构造侧边栏需要的数据
user_obj = models.UserInfo.objects.filter(username=username).first()
blog = user_obj.blog
# 1 查询当前用户所有的分类及分类下的文章数
category_list = models.Category.objects.filter(blog__userinfo__username=username).annotate(count_num=Count('article__pk')).values_list('title','count_num','pk')
# print(category_list) # <QuerySet [('jason的分类一', 2), ('jason的分类二', 1), ('jason的分类三', 1)]>
# 2 查询当前用户所有的标签及标签下的文章数
tag_list = models.Tag.objects.filter(blog__userinfo__username=username).annotate(count_num=Count('article__pk')).values_list('title','count_num','pk')
# print(tag_list) # <QuerySet [('tank的标签一', 1), ('tank的标签二', 1), ('tank的标签三', 2)]>
# 3 按照年月统计所有的文章
date_list = models.Article.objects.filter(user__username=username).annotate(month=TruncMonth('create_time')).values('month').annotate(count_num=Count('pk')).values_list('month','count_num')
# print(date_list)
return locals()
侧边栏页面渲染left_menu.html
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">文章分类</h3>
</div>
<div class="panel-body">
{% for category in category_list %}
<p><a href="/{{ username }}/category/{{ category.2 }}">{{ category.0 }}({{ category.1 }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">文章标签</h3>
</div>
<div class="panel-body">
{% for tag in tag_list %}
<p><a href="/{{ username }}/tag/{{ tag.2 }}">{{ tag.0 }}({{ tag.1 }})</a></p>
{% endfor %}
</div>
</div>
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">日期归档</h3>
</div>
<div class="panel-body">
{% for date in date_list %}
<p><a href="/{{ username }}/archive/{{ date.0|date:'Y-m' }}">{{ date.0|date:'Y年m月' }}({{ date.1 }})</a></p>
{% endfor %}
</div>
</div>
将首页框架放入base.html文件中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="/media/css/{{ blog.site_theme }}/">
{% block css %}
{% endblock %}
</head>
<body>
<nav class="navbar navbar-inverse">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">{{ blog.site_title }}</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">博客 <span class="sr-only">(current)</span></a></li>
<li><a href="#">文章</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">更多 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">One more separated link</a></li>
</ul>
</li>
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<ul class="nav navbar-nav navbar-right">
{% if request.user.is_authenticated %}
<li><a href="#">{{ request.user.username }}</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">更多操作 <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#" data-toggle="modal" data-target=".bs-example-modal-lg">修改密码</a></li>
<li><a href="#">修改头像</a></li>
<li><a href="#">后台管理</a></li>
<li role="separator" class="divider"></li>
<li><a href="{% url 'logout' %}">退出登陆</a></li>
</ul>
<div class="modal fade bs-example-modal-lg" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<h1 class="text-center">修改密码</h1>
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="form-group">
<label for="">用户名</label>
<input type="text" disabled value="{{ request.user.username }}" class="form-control">
</div>
<div class="form-group">
<label for="">原密码</label>
<input type="password" id="id_old_password" class="form-control">
</div>
<div class="form-group">
<label for="">新密码</label>
<input type="password" id="id_new_password" class="form-control">
</div>
<div class="form-group">
<label for="">确认密码</label>
<input type="password" id="id_confirm_password" class="form-control">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
<button class="btn btn-primary" id="id_edit">修改</button>
<span style="color: red" id="password_error"></span>
</div>
<br>
<br>
</div>
</div>
</div>
</div>
</div>
</li>
{% else %}
<li><a href="{% url 'reg' %}">注册</a></li>
<li><a href="{% url 'login' %}">登陆</a></li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div class="container-fluid">
<div class="row">
<div class="col-md-3">
{% load mytag %}
{% left_menu username %}
</div>
<div class="col-md-9">
{% block content %}
{% endblock %}
</div>
</div>
</div>
{% block js %}
{% endblock %}
</body>
</html>
视图view.py
def article_detail(request,username,article_id):
"""
应该需要校验username和article_id是否存在,但是我们这里先只完成正确的情况
默认不会瞎搞
:param request:
:param username:
:param article_id:
:return:
"""
user_obj = models.UserInfo.objects.filter(username=username).first()
blog = user_obj.blog
# 先获取文章对象
article_obj = models.Article.objects.filter(pk=article_id,user__username=username).first()
if not article_obj:
return render(request,'errors.html')
return render(request,'article_detail.html',locals())
文章详情页面渲染 article_detail.html
{% extends 'base.html' %}
{% block content %}
<h1>{{ article_obj.title }}</h1>
<div class="article_content">
{{ article_obj.content|safe }}
</div>
{% endblock %}
个人站点页面渲染优化
将视图侧边栏逻辑注释
site.html
{% extends 'base.html' %}
{% block content %}
<ul class="media-list">
{% for article_obj in article_list %}
<li class="media">
<h4 class="media-heading"><a href="/{{ username }}/article/{{ article_obj.pk }}/">{{ article_obj.title }}</a></h4>
<div class="media-left">
<a href="#">
<img class="media-object" src="/media/{{ article_obj.user.avatar }}" alt="..." width="80">
</a>
</div>
<div class="media-body">
{{ article_obj.desc }}
</div>
{# posted @ 2015-10-20 01:02 wahaha 阅读(68527) 评论(24) 推荐(58) 编辑)#}
<div class="pull-right">
<span>posted </span>
<span>@ </span>
<span>{{ article_obj.create_time|date:'Y-m-d' }} </span>
<span>{{ article_obj.user.username }} </span>
<span><span class="glyphicon glyphicon-comment"></span>评论({{ article_obj.comment_count }}) </span>
<span><span class="glyphicon glyphicon-thumbs-up"></span>点赞({{ article_obj.up_count }})</span>
<span><a href="#">编辑</a></span>
</div>
</li>
<hr>
{% endfor %}
</ul>
{% endblock %}
路由设置
# 文章详情页
url(r'^(?P<username>\w+)/article/(?P<article_id>\d+)/',views.article_detail)
文章点赞点踩
"""
浏览器上你看到的花里胡哨的页面,内部都是HTML(前端)代码
那现在我们的文章内容应该写什么??? >>> html代码
如何拷贝文章
copy outerhtml
1.拷贝文章
2.拷贝点赞点踩
1.拷贝前端点赞点踩图标 只拷了html
2.css也要拷贝
由于有图片防盗链的问题 所以将图片直接下载到本地
课下思考:
前端如何区分用户是点了赞还是点了踩
1.给标签各自绑定一个事件
两个标签对应的代码其实基本一样,仅仅是是否点赞点踩这一个参数不一样而已
2.二合一
给两个标签绑定一个事件
// 给所有的action类绑定事件
$('.action').click(function () {
alert($(this).hasClass('diggit'))
})
由于点赞点踩内部有一定的业务逻辑,所以后端单独开设视图函数处理
"""
# 个人建议:写代码先把所有正确的逻辑写完再去考虑错误的逻辑 不要试图两者兼得
import json
from django.db.models import F
def up_or_down(request):
"""
1.校验用户是否登陆
2.判断当前文章是否是当前用户自己写的(自己不能点自己的文章)
3.当前用户是否已经给当前文章点过了
4.操作数据库了
:param request:
:return:
"""
if request.is_ajax():
back_dic = {'code': 1000, 'msg': ''}
# 1 先判断当前用户是否登陆
if request.user.is_authenticated:
article_id = request.POST.get('article_id')
is_up = request.POST.get('is_up')
# print(is_up,type(is_up)) # true <class 'str'>
is_up = json.loads(is_up) # 记得转换
# print(is_up, type(is_up)) # True <class 'bool'>
# 2 判断当前文章是否是当前用户自己写的 根据文章id查询文章对象 根据文章对象查作者 根request.user比对
article_obj = models.Article.objects.filter(pk=article_id).first()
if not article_obj.user == request.user:
# 3 校验当前用户是否已经点了 哪个地方记录了用户到底点没点
is_click = models.ArticleUpDown.objects.filter(user=request.user, article=article_obj)
if not is_click:
# 4 操作数据库 记录数据 要同步操作普通字段
# 判断当前用户点了赞还是踩 从而决定给哪个字段加一
if is_up:
# 给点赞数加一
models.Article.objects.filter(pk=article_id).update(up_count=F('up_count') + 1)
back_dic['msg'] = '点赞成功'
else:
# 给点踩数加一
models.Article.objects.filter(pk=article_id).update(down_count=F('down_count') + 1)
back_dic['msg'] = '点踩成功'
# 操作点赞点踩表
models.ArticleUpDown.objects.create(user=request.user, article=article_obj, is_up=is_up)
else:
back_dic['code'] = 1001
back_dic['msg'] = '你已经点过了,不能再点了' # 这里你可以做的更加的详细 提示用户到底点了赞还是点了踩
else:
back_dic['code'] = 1002
back_dic['msg'] = '你个臭不要脸的!'
else:
back_dic['code'] = 1003
back_dic['msg'] = '请先<a href="/login/">登陆</a>'
return JsonResponse(back_dic)
<script>
// 给所有的action类绑定事件
$('.action').click(function () {
{#alert($(this).hasClass('diggit'))#}
let isUp = $(this).hasClass('diggit');
let $div = $(this);
// 朝后端发送ajax请求
$.ajax({
url:'/up_or_down/',
type:'post',
data:{
'article_id':'{{ article_obj.pk }}',
'is_up':isUp,
'csrfmiddlewaretoken':'{{ csrf_token }}'
},
success:function (args) {
if(args.code == 1000){
$('#digg_tips').text(args.msg)
// 将前端的数字加一
// 先获取到之前的数字
let oldNum = $div.children().text(); // 文本 是字符类型
// 易错点
$div.children().text(Number(oldNum) + 1) // 字符串拼接了 1+1 = 11 11 + 1 = 111
}else{
$('#digg_tips').html(args.msg)
}
}
})
})
</script>
url.py
#点赞点踩
url(r'up_or_down',views.up_or_down),
包含点赞点踩页面渲染article_detail.html
{% extends 'base.html' %}
{% block css %}
<style>
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
width: 125px;
text-align: center;
margin-top: 10px;
}
.diggit {
float: left;
width: 46px;
height: 52px;
background: url('/static/img/upup.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.buryit {
float: right;
margin-left: 20px;
width: 46px;
height: 52px;
background: url('/static/img/downdown.gif') no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.clear {
clear: both;
}
</style>
{% endblock %}
{% block content %}
<h1>{{ article_obj.title }}</h1>
<div class="article_content">
{{ article_obj.content|safe }}
</div>
{# 点赞点踩样式开始#}
<div>
<div id="div_digg">
<div class="diggit action" >
<span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
</div>
<div class="buryit action" >
<span class="burynum" id="bury_count">{{ article_obj.down_count }}</span>
</div>
<div class="clear"></div>
<div class="diggword" id="digg_tips">
</div>
</div>
</div>
{# 点赞点踩样式结束#}
{% endblock %}
{% block js %}
<script>
// 给所有的action类绑定事件
$('.action').click(function () {
{#alert($(this).hasClass('diggit'))#}
let isUp = $(this).hasClass('diggit');
let $div = $(this);
// 朝后端发送ajax请求
$.ajax({
url: '/up_or_down/',
type: 'post',
data: {
'article_id': '{{ article_obj.pk }}',
'is_up': isUp,
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function (args) {
if (args.code == 1000) {
$('#digg_tips').text(args.msg)
// 将前端的数字加一
// 先获取到之前的数字
let oldNum = $div.children().text(); // 文本 是字符类型
// 易错点
$div.children().text(Number(oldNum) + 1) // 字符串拼接了 1+1 = 11 11 + 1 = 111
} else {
$('#digg_tips').html(args.msg)
}
}
})
})
</script>
{% endblock %}
文章评论
"""
我们先写根评论
再写子评论
点击评论按钮需要将评论框里面的内容清空
根评论有两步渲染方式
1.DOM临时渲染
2.页面刷新render渲染
子评论
点击回复按钮发生了几件事
1.评论框自动聚焦
2.将回复按钮所在的那一行评论人的姓名
@username
3.评论框内部自动换行
根评论子评论都是点击一个按钮朝后端提交数据的
parent_id
根评论子评论区别在哪?
parent_id
"""
文章详情视图增加查询评论列表sql
# 获取当前 文章所有的评论内容
comment_list = models.Comment.objects.filter(article=article_obj)
view.py相关视图参考
def article_detail(request, username, article_id):
"""
应该需要校验username和article_id是否存在,但是我们这里先只完成正确的情况
默认不会瞎搞
:param request:
:param username:
:param article_id:
:return:
"""
user_obj = models.UserInfo.objects.filter(username=username).first()
blog = user_obj.blog
# 先获取文章对象
article_obj = models.Article.objects.filter(pk=article_id, user__username=username).first()
if not article_obj:
return render(request, 'errors.html')
# 获取当前 文章所有的评论内容
comment_list = models.Comment.objects.filter(article=article_obj)
return render(request, 'article_detail.html', locals())
from django.db.models import F
from django.db import transaction
def comment(request):
# 自己也可以给自己的文章评论内容
if request.is_ajax():
back_dic = {'code': 1000, 'msg': ""}
if request.method == 'POST':
if request.user.is_authenticated:
article_id = request.POST.get('article_id')
content = request.POST.get("content")
parent_id = request.POST.get('parent_id')
# 直接操作评论表 存储数据 两张表
with transaction.atomic():
models.Article.objects.filter(pk=article_id).update(comment_count=F('comment_count') + 1)
models.Comment.objects.create(user=request.user, article_id=article_id, content=content,
parent_comment_id=parent_id)
back_dic['msg'] = '评论成功'
else:
back_dic['code'] = 1001
back_dic['msg'] = '用户未登陆'
return JsonResponse(back_dic)
url.py
#评论
url(r'comment',views.comment),
article_detail.html文章详情
{% extends 'base.html' %}
{% block css %}
<style>
#div_digg {
float: right;
margin-bottom: 10px;
margin-right: 30px;
font-size: 12px;
width: 125px;
text-align: center;
margin-top: 10px;
}
.diggit {
float: left;
width: 46px;
height: 52px;
background: url(/static/img/upup.gif) no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.buryit {
float: right;
margin-left: 20px;
width: 46px;
height: 52px;
background: url(/static/img/downdown.gif) no-repeat;
text-align: center;
cursor: pointer;
margin-top: 2px;
padding-top: 5px;
}
.clear {
clear: both;
}
</style>
{% endblock %}
{% block content %}
<h1>{{ article_obj.title }}</h1>
<div class="article_content">
{{ article_obj.content|safe }}
</div>
{# 点赞点踩样式开始#}
<div>
<div id="div_digg">
<div class="diggit action" >
<span class="diggnum" id="digg_count">{{ article_obj.up_count }}</span>
</div>
<div class="buryit action" >
<span class="burynum" id="bury_count">{{ article_obj.down_count }}</span>
</div>
<div class="clear"></div>
<div class="diggword" id="digg_tips">
</div>
</div>
</div>
{# 点赞点踩样式结束#}
{# 评论楼渲染开始#}
{# #3楼 2020-05-14 14:11 代码一字狂#}
<div>
<ul class="list-group">
{% for comment in comment_list %}
<li class="list-group-item">
<span>#{{ forloop.counter }}楼</span>
<span>{{ comment.create_time|date:'Y-m-d h:i:s' }}</span>
<span>{{ comment.user.username }}</span>
<span><a class="pull-right reply" username="{{ comment.user.username }}" nid="{{ comment.pk }}">回复</a></span>
<div>
{# 判断当前评论是否是子评论 如果是需要渲染对应的评论人名#}
{% if comment.parent_comment_id %}
<p>@{{ comment.parent_comment.user.username }}</p>
{% endif %}
{{ comment.content }}
</div>
</li>
{% endfor %}
</ul>
</div>
{# 评论楼渲染结束#}
{# 文章评论样式开始 #}
{% if request.user.is_authenticated %}
<div>
<p><span class="glyphicon glyphicon-comment"></span>发表评论</p>
<div>
<textarea name="comment" id="id_comment" cols="60" rows="10" ></textarea>
</div>
<button class="btn btn-primary" id="id_submit">提交评论</button>
<span style="color: red" id="errors"></span>
</div>
{% else %}
<li><a href="{% url 'reg' %}">注册</a></li>
<li><a href="{% url 'login' %}">登陆</a></li>
{% endif %}
{# 文章评论样式结束 #}
{% endblock %}
{% block js %}
<script>
// 给所有的action类绑定事件
$('.action').click(function () {
{#alert($(this).hasClass('diggit'))#}
let isUp = $(this).hasClass('diggit');
let $div = $(this);
// 朝后端发送ajax请求
$.ajax({
url: '/up_or_down/',
type: 'post',
data: {
'article_id': '{{ article_obj.pk }}',
'is_up': isUp,
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function (args) {
if (args.code == 1000) {
$('#digg_tips').text(args.msg)
// 将前端的数字加一
// 先获取到之前的数字
let oldNum = $div.children().text(); // 文本 是字符类型
// 易错点
$div.children().text(Number(oldNum) + 1) // 字符串拼接了 1+1 = 11 11 + 1 = 111
} else {
$('#digg_tips').html(args.msg)
}
}
})
})
// 设置一个全局的parentID字段
let parentId = null;
// 用户点击评论按钮朝后端发送ajax请求
$('#id_submit').click(function () {
// 获取用户评论的内容
let conTent = $('#id_comment').val();
// 判断当前评论是否是子评论 如果是 需要将我们之前手动渲染的@username去除
if(parentId){
// 找到\n对应的索引 然后利用切片 但是前片顾头不顾尾 所以索引+1
let indexNum = conTent.indexOf('\n') + 1;
conTent = conTent.slice(indexNum) // 将indexNum之前的所有数据切除 只保留后面的部分
}
$.ajax({
url:'/comment/',
type:'post',
data:{
'article_id':'{{ article_obj.pk }}',
'content':conTent,
// 如果parantId没有值 那么就是null 后端存储null没有任何关系
'parent_id':parentId,
'csrfmiddlewaretoken':'{{ csrf_token }}'
},
success:function (args) {
if(args.code ==1000){
$('#error').text(args.msg)
// 将评论框里面的内容清空
$('#id_comment').val('');
// 临时渲染评论楼
let userName = '{{ request.user.username }}';
let temp = `
<li class="list-group-item">
<span>${userName}</span>
<span><a href="#" class="pull-right">回复</a></span>
<div>
${conTent}
</div>
</li>
`
// 将生成好的标签添加到ul标签内
$('.list-group').append(temp);
// 清空全局的parentId
parentId = null;
}
}
})
})
// 给回复按钮绑定点击事件
$('.reply').click(function () {
// 需要评论对应的评论人姓名 还需要评论的主键值
// 获取用户名
let commentUserName = $(this).attr('username');
// 获取主键值 直接修改全局
parentId = $(this).attr('nid');
// 拼接信息塞给评论框
$('#id_comment').val('@' + commentUserName + '\n').focus()
})
</script>
{% endblock %}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具