• BBS+Blog项目代码


    项目目录结构:

     1 cnblog/
     2 |-- blog/(APP)
     3     |-- migrations(其中文件略)
     4     |-- templatetags/
     5         |-- my_tags.py
     6     |-- utils/
     7         |-- myForms.py
     8         |-- validCode.py
     9     |-- admin.py
    10     |-- apps.py
    11     |-- models.py
    12     |-- test.py
    13     |-- views.py
    14 
    15 |-- cnblog/
    16     |-- settings.py
    17     |-- urls.py
    18     |-- wsgi.py
    19 
    20 |-- media/
    21     |-- add_article_img/
    22         |-- (由用户上传)
    23     |-- avatars/
    24         |-- default.jpg
    25         |-- (由用户上传)
    26 
    27 |-- static/
    28     |-- blog/
    29         |-- bs/(引入的bootstrap文件夹)
    30         |-- css/
    31             |-- article_detail.css
    32             |-- backend.css
    33             |-- home_site.css
    34         |-- img/
    35             |-- default.jpg
    36             |-- downdown.gif
    37             |-- icon_form.gif
    38             |-- upup.gif
    39         |-- kindeditor/(引入的KindEditor编辑器)
    40         |-- font/(引入的字体)
    41             |-- KumoFont.ttf
    42         |-- js/ (引入的JQuery)
    43 
    44 |-- templates/
    45     |-- backend/
    46         |-- add_article.html
    47         |-- backend.html
    48         |-- base.html
    49         |-- edit_article.html
    50     |-- article_detail.html
    51     |-- base.html
    52     |-- classification.html
    53     |-- home_site.html
    54     |-- index.html
    55     |-- login.html
    56     |-- not_found.html
    57     |-- register.html
    58 
    59 |-- manage.py

    注: __init__.py 已省略

    项目代码:

    cnblog/settings.py

    """
    Django settings for cnblog project.
    
    Generated by 'django-admin startproject' using Django 2.0.1.
    
    For more information on this file, see
    https://docs.djangoproject.com/en/2.0/topics/settings/
    
    For the full list of settings and their values, see
    https://docs.djangoproject.com/en/2.0/ref/settings/
    """
    
    import os
    
    # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    
    
    # Quick-start development settings - unsuitable for production
    # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
    
    # SECURITY WARNING: keep the secret key used in production secret!
    SECRET_KEY = '2l2d&f8rq#&b!9=t^d=n$d*udx^iv9yd6u$f1$i&iz#@9$u4kf'
    
    # SECURITY WARNING: don't run with debug turned on in production!
    DEBUG = True
    
    ALLOWED_HOSTS = []
    
    
    # Application definition
    
    INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'blog.apps.BlogConfig',
    ]
    
    MIDDLEWARE = [
        'django.middleware.security.SecurityMiddleware',
        'django.contrib.sessions.middleware.SessionMiddleware',
        'django.middleware.common.CommonMiddleware',
        'django.middleware.csrf.CsrfViewMiddleware',
        'django.contrib.auth.middleware.AuthenticationMiddleware',
        'django.contrib.messages.middleware.MessageMiddleware',
        'django.middleware.clickjacking.XFrameOptionsMiddleware',
    ]
    
    ROOT_URLCONF = 'cnblog.urls'
    
    TEMPLATES = [
        {
            'BACKEND': 'django.template.backends.django.DjangoTemplates',
            'DIRS': [os.path.join(BASE_DIR, 'templates')]
            ,
            'APP_DIRS': True,
            'OPTIONS': {
                'context_processors': [
                    'django.template.context_processors.debug',
                    'django.template.context_processors.request',
                    'django.contrib.auth.context_processors.auth',
                    'django.contrib.messages.context_processors.messages',
                ],
            },
        },
    ]
    
    WSGI_APPLICATION = 'cnblog.wsgi.application'
    
    
    # Database
    # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
    
    # DATABASES = {
    #     'default': {
    #         'ENGINE': 'django.db.backends.sqlite3',
    #         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    #     }
    # }
    DATABASES = {
        'default':{
            'ENGINE':'django.db.backends.mysql',
            'NAME':'cnblog',
            'USER':'root',
            'PASSWORD':'tj037778',
            'HOST':'127.0.0.1',
            'PORT':3306
        }
    }
    
    
    # Password validation
    # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
    
    AUTH_PASSWORD_VALIDATORS = [
        {
            'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
        },
        {
            'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        },
        {
            'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
        },
        {
            'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
        },
    ]
    
    
    # Internationalization
    # https://docs.djangoproject.com/en/2.0/topics/i18n/
    
    LANGUAGE_CODE = 'en-us'
    
    # TIME_ZONE = 'UTC'
    TIME_ZONE = 'Asia/Shanghai'
    
    USE_I18N = True
    
    USE_L10N = True
    
    # USE_TZ = True
    USE_TZ = False
    
    
    # Static files (CSS, JavaScript, Images)
    # https://docs.djangoproject.com/en/2.0/howto/static-files/
    
    STATIC_URL = '/static/'
    STATICFILES_DIRS = [
        os.path.join(BASE_DIR,"static"),
    ]
    
    # 当自己的表继承了 AbstractUser 时,需要加上下面的这行代码
    AUTH_USER_MODEL = "blog.UserInfo"  # 格式:app_label.ModelName
    
    
    # 与用户上传相关的配置
    # 配置 media
    MEDIA_ROOT = os.path.join(BASE_DIR,"media")  # "media"文件夹也可以放到某个文件夹下面,此时也需要根据 media所在的文件夹 拼接出 media 的绝对路径
    MEDIA_URL = "/media/"   # MEDIA_URL = "/media/" 也是为上面的 MEDIA_ROOT 那个绝对路径起了一个别名;效果和 STATIC_URL = '/static/' 一样
    
    # 发送邮件配置参数
    EMAIL_HOST = "smtp.qq.com"
    EMAIL_PORT = 465
    EMAIL_HOST_USER = "380544011@qq.com"
    EMAIL_HOST_PASSWORD = "bzdorsaylqknbifi"
    # DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
    EMAIL_USE_SSL = True
    
    
    LOGIN_URL = "/login/"

    cnblog/urls.py

    """cnblog URL Configuration
    
    The `urlpatterns` list routes URLs to views. For more information please see:
        https://docs.djangoproject.com/en/2.0/topics/http/urls/
    Examples:
    Function views
        1. Add an import:  from my_app import views
        2. Add a URL to urlpatterns:  path('', views.home, name='home')
    Class-based views
        1. Add an import:  from other_app.views import Home
        2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
    Including another URLconf
        1. Import the include() function: from django.urls import include, path
        2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
    """
    from django.contrib import admin
    from django.urls import path,re_path
    
    from blog import views
    
    # 配置MEDIA_URL
    from django.views.static import serve
    from cnblog import settings
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path(r"login/",views.login),
        path(r"get_validCode_img/",views.get_validCode_img),
        path(r"index/",views.index),
        # IP+端口的根目录也是 index
        re_path(r"^$",views.index),
        path(r"register/",views.register),
        path(r"logout/",views.logout),
        # 输入框文件上传路径
        path(r"upload/",views.upload),
        # 点赞
        path(r"digg/",views.digg),
        # 评论
        path(r"comment/",views.comment),
        path(r"get_comment_tree/",views.get_comment_tree),
    
        # 后台管理url
        re_path("cn_backend/$",views.cn_backend),
        re_path("cn_backend/add_article/$",views.add_article),
        re_path(r"article/(d+)/edit/$",views.edit_article),
        re_path("article/(d+)/delete/$",views.delete_article),
    
        # 配置MEDIA_URL
        re_path(r"media/(?P<path>.*)/$",serve,{"document_root":settings.MEDIA_ROOT}),
    
        # 个人站点的url
        re_path(r"^(?P<username>w+)/$",views.home_site),
        # 个人站点页面的跳转
        re_path(r"^(?P<username>w+)/(?P<condition>category|tag|archive)/(?P<param>.*)/$",views.home_site),
    
        # 文章详情页
        re_path(r"(?P<username>w+)/articles/(?P<article_id>d+)/$",views.article_detail)
    ]

    blog/models.py

    from django.db import models
    
    # Create your models here.
    from django.contrib.auth.models import AbstractUser
    
    
    class UserInfo(AbstractUser):  # 通过继承AbstractUser,就能实现往 django 自带的 auth_user表中添加字段的功能;因为User类也是继承的AbstractUser
        "用户信息"
        nid = models.AutoField(primary_key=True)
        telephone = models.CharField(max_length=11, null=True, unique=True)
        # 下面的 default 的路径有疑问 ???
        avatar = models.FileField(upload_to="avatars/", default="/avatars/default.png")  # avatar 这个字段不传的时候(avatar字段为空时,也是上传了 avatar字段),才会使用 default 的默认值
        create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)  # auto_now_add=True表示默认取当前时间
    
        blog = models.OneToOneField(to="Blog", to_field="nid", null=True,on_delete=models.CASCADE)
    
        def __str__(self):
            return self.username
    
    
    class Blog(models.Model):
        "博客信息表(站点表)"
        nid = models.AutoField(primary_key=True)
        title = models.CharField(verbose_name="个人博客标题", max_length=64)
        site_name = models.CharField(verbose_name="站点名称", max_length=64)
        theme = models.CharField(verbose_name="博客主题", max_length=32)
    
        def __str__(self):
            return self.title
    
    
    class Category(models.Model):
        "博主个人文章分类表(和Blog表是一对多关系,所以和UserInfo表也是一对多关系)"
        nid = models.AutoField(primary_key=True)
        title = models.CharField(verbose_name="分类标题", max_length=32)
        blog = models.ForeignKey(verbose_name="所属博客", to="Blog", to_field="nid",on_delete=models.CASCADE)
    
        def __str__(self):
            return self.title
    
    
    class Tag(models.Model):
        "博客标签"
        nid = models.AutoField(primary_key=True)
        title = models.CharField(verbose_name="标签名称", max_length=32)
        blog = models.ForeignKey(verbose_name="所属博客", to="Blog", to_field="nid",on_delete=models.CASCADE)
    
        def __str__(self):
            return self.title
    
    
    class Article(models.Model):
        "文章表"
        nid = models.AutoField(primary_key=True)
        title = models.CharField(max_length=50, verbose_name="文章标题")
        desc = models.CharField(max_length=255, verbose_name="文章描述")
        create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
        content = models.TextField()
    
        comment_count = models.IntegerField(default=0)
        up_count = models.IntegerField(default=0)
        down_count = models.IntegerField(default=0)
        # 上面三个 count 是为了减少统计评论数等时的跨表查询(添加评论等时即让对应count自加1)
    
        user = models.ForeignKey(verbose_name="作者", to="UserInfo", to_field="nid",on_delete=models.CASCADE)
        category = models.ForeignKey(to="Category", to_field="nid", null=True,on_delete=models.CASCADE)
        tags = models.ManyToManyField(
            to="Tag",
            through="Article2Tag",
            through_fields=("article", "tag")
        )
    
        def __str__(self):
            return self.title
    
    
    class Article2Tag(models.Model):
        nid = models.AutoField(primary_key=True)
        article = models.ForeignKey(verbose_name="文章", to="Article", to_field="nid",on_delete=models.CASCADE)
        tag = models.ForeignKey(verbose_name="标签", to="Tag", to_field="nid",on_delete=models.CASCADE)
    
        class Meta(object):  # 表示联合唯一
            unique_together = [
                ("article", "tag"),
            ]
    
        def __str__(self):
            v = self.article.title + "---" + self.tag.title
            return v
    
    
    class ArticleUpDown(models.Model):
        "点赞表"
        nid = models.AutoField(primary_key=True)
        user = models.ForeignKey("UserInfo", null=True,on_delete=models.CASCADE)
        article = models.ForeignKey("Article", null=True,on_delete=models.CASCADE)
        is_up = models.BooleanField(default=True)
    
        class Meta:
            unique_together = [
                ("article", "user"),
            ]
    
    
    class Comment(models.Model):
        "评论表(哪个用户在哪个时间对哪篇文章做了什么评论)"
    
        nid = models.AutoField(primary_key=True)
        user = models.ForeignKey(verbose_name="评论者", to="UserInfo", to_field="nid",on_delete=models.CASCADE)
        article = models.ForeignKey(verbose_name="评论文章", to="Article", to_field="nid",on_delete=models.CASCADE)
        create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
        content = models.CharField(verbose_name="评论内容", max_length=255)
    
        parent_comment = models.ForeignKey("self",null=True,on_delete=models.CASCADE)  # 用这行代码去构建评论树; # ForeignKey("self")表示自关联(自己关联自己;等同于:ForeignKey("Comment"))
    
        def __str__(self):
            return self.content
    
        # 根评论:对文章的评论
        # 子评论:对评论的评论

    blog/views.py

    from django.shortcuts import render,HttpResponse,redirect
    from django.db.models import Count
    from bs4 import BeautifulSoup
    import os
    from django.http import JsonResponse
    import json
    from django.db.models import F
    from django.db import transaction
    # JsonResponse 能直接返回Json格式的字符串
    from django.contrib import auth
    from django.contrib.auth.decorators import login_required
    from blog.models import *
    from cnblog import settings
    
    # Create your views here.
    # 基于用户认证组件和Ajax实现登陆验证(图片验证码)
    def login(request):
    
        if request.method == "POST":
    
            response = {"user":None,"msg":None}
    
            user = request.POST.get("user")
            psw = request.POST.get("psw")
            valid_code = request.POST.get("valid_code")
    
            valid_code_str = request.session.get("valid_code_str")  # 获取储存到 session 中的验证码
    
            if valid_code.upper() == valid_code_str.upper():
                user_boj = auth.authenticate(username=user,password=psw)
                if user_boj:
                    # 此处是Ajax的POST请求;Ajax的POST时返回的不能是 render 或者 redirect(返回这两个没有意义),通常返回的是一个字典(如:HttpResponse);# Ajax POST请求时,只需要返回给前端一个结果让前端知道后端发生了什么就行
                    auth.login(request,user_boj)  # 注册session
                    response["user"] = user_boj.username
                else:
                    response["msg"] = "用户名密码错误!"
            else:
                response["msg"] = "验证码错误!"
    
            # 利用JsonResponse返回数据时,前端的Jquery会自动把该数据解析成相应的数据类型
            return JsonResponse(response)
    
        return render(request,"login.html")
    
    def get_validCode_img(request):
    
        from blog.utils.validCode import get_valid_code_img
        data = get_valid_code_img(request)
    
        return HttpResponse(data)
    
    """
    登陆验证功能总结:
    1. 一次请求会伴随着多次请求(加载静态文件)
    2. PIL模块
    3. session的存储(验证码要存到session中)
    4. 验证码刷新( .src+="?" )
    """
    
    
    def index(request):
    
        article_list = Article.objects.all() # 获取所有的文章对象列表
    
        return render(request,"index.html",{"article_list":article_list})
    
    
    from blog.utils.myForms import UserForm  # 实际项目中,所有导入的文件要统一一起放到最上面
    # 基于form组件和Ajax实现注册功能
    def register(request):
        # 判断请求是否为Ajax请求
        if request.is_ajax():
            print(request.POST)
            print(request.FILES)
    
            form = UserForm(request.POST)
    
            # Ajax的请求通常需要返回一个字典,用于告诉前端发生了什么事
            response = {"user":None,"msg":None}
    
            if form.is_valid():
                response["user"] = form.cleaned_data.get("user")
    
                # 生成一条用户记录
                # 不要用 create() 创建创建记录,因为这个create 出来的记录是明文的 密码;应该用 create_user() 或者 create_superuser(); 这两种方法能将 password 变成 密文;返回值是插入的这条记录对象
                # 在 form.cleaned_data 里面获取 user,psw,email; request.FILES中获取文件
                user = form.cleaned_data.get("user")
                print("user",user)
                psw = form.cleaned_data.get("psw")
                email = form.cleaned_data.get("email")
                avatar_obj = request.FILES.get("avatar")
    
                # /static/ 中放的是服务器自己的文件;/media/放的是用户上传的文件
                """
                if avatar_obj:  # avatar_obj 不为空,说明用户上传了头像;此时需要把 avatar_obj传给 avatar 字段
                    user_obj = UserInfo.objects.create_user(username=user,password=psw,email=email,avatar=avatar_obj)
                else: # avatar_obj 为空,说明用户没上传头像;此时就不要 写 avatar 字段,这样创建记录时 avatar 字段才会使用 default;注意:avatar字段 上传为空 和 不上传 是不一样的,只有不上传(创建记录时不给 avatar 字段传值)
                    user_obj = UserInfo.objects.create_user(username=user, password=psw, email=email)
                """
                # 上面的两行代码可做如下优化:
                extra = {}
                if avatar_obj:
                    extra["avatar"] = avatar_obj
                UserInfo.objects.create_user(username=user,password=psw,email=email,**extra)
                # 先定义一个空字典,如果 avatar_obj 有值,就把 avatar_obj 传到这个字典中,再把这个字典放入 create_user()中;由于 extra是一个字典,所以需要加上 **
    
                """
                一旦在settings.py中配置了 media 文件("media"是project中的一个文件夹名,可放在project下,也可放在 app 下):
                    MEDIA_ROOT = os.path.join(BASE_DIR,"media")
                Django会自动实现如下功能:
                    FileField字段(如UserInfo表中)和ImageField字段,Django会将所有文件对象下载到MEDIA_ROOT中的 avatars(因为UserInfo表中是:upload_to="avatars/")文件夹中(如果MEDIA_ROOT中没有 avatars 这个文件夹,Django会自动创建 );user_obj的avatar存的是文件的相对路径
                """
    
            else:
                print(form.cleaned_data)
                print(form.errors)
    
                response["msg"] = form.errors
            return JsonResponse(response)
    
        form = UserForm()
        return render(request,"register.html",{"form":form})
    
    def logout(request):
        auth.logout(request)  # 作用等同于: request.session.flush();# 会把 django_session 表中 相应的记录删除
    
        return redirect("/login/")
    
    from django.db.models.functions import TruncMonth
    # 个人站点视图函数
    def home_site(request,username,**kwargs):
        print("username",username)
        # UserInfo.objects.filter(username=username).exists():也可以用 .exists() 判断是否存在
        user = UserInfo.objects.filter(username=username).first()
        if not user:
            return render(request,"not_found.html")
    
    
        #  查询当前站点对象
        # blog = user.blog
    
        # 当前用户或者当前站点对应的所有文章全部读取出来
        # 方式一:基于对象查询
        # article_list = user.article_set.all()
        # 方式二:基于双下划线
        article_list = Article.objects.filter(user=user)
    
    
        # kwargs 用于区分访问的是站点页面还是站点下的跳转页面
        if kwargs: # 跳转页面
            condition = kwargs.get("condition")
            param = kwargs.get("param")
            # 跳转页面时,只有 article_list 发生了变化,其它的参数没有变
            if condition == "archive": # 如: param = "2018-05"
                year,month = param.split("-")
                article_list = article_list.filter(create_time__year=year,create_time__month=month)
            elif condition == "category":
                article_list = article_list.filter(category__title=param)
            else:
                article_list = article_list.filter(tags__title=param)
    
    
        # 利用 inclusion_tag 时,数据无需在此处读取
        """
        # 语法:“每一个”后的表模型.objects.values("pk").annotate(聚合函数(关联表__统计字段)).values("表模型的所有字段")
        # 查询每一个分类名称以及对应的文章数
        ret = Category.objects.values("pk").annotate(c=Count("article__title")).values("title","c")
        print(ret)
        
        # 查询当前站点的每一个分类名称以及对应的文章数
        cate_list = Category.objects.filter(blog=blog).values("pk").annotate(c=Count("article__title")).values("title","c")
        print(cate_list)
        
        # 查询当前站点的每一个标签名称以及对应的文章数
        tag_list = Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article__title")).values_list("title","c")
        print(tag_list)
        
        # 查询当前站点每一个年月的名称以及对应的文章数
        # 方式一:通过 extra() 函数 和 date_format 构建出了 "年-月"字段
        date_list = Article.objects.filter(user=user).extra(select={"y_m_date":"date_format(create_time,'%%Y-%%m')"}).values("y_m_date").annotate(c=Count("nid")).values("y_m_date","c")  # annotate()之前要先 .values()
        print(date_list)
        
        # 方式二:利用 TruncMonth
        # ret = Article.objects.filter(user=user).annotate(month=TruncMonth('create_time')).values("month").annotate(c=Count("nid")).values("month","c")
        # print(ret)
        
        return render(request,"home_site.html",locals())
        """
    
        return render(request, "home_site.html", {"username": username, "article_list": article_list})
    
    """
    # 由于 article_detail() 和 home_site() 函数都需要读取 标签、分类和日期归档的数据,我们把其定义为一个函数
    def get_classification_data(username):
    
        user = UserInfo.objects.filter(username=username).first()
    
        #  查询当前站点对象
        blog = user.blog
    
        # ret = Category.objects.values("pk").annotate(c=Count("article__title")).values("title", "c")
    
        cate_list = Category.objects.filter(blog=blog).values("pk").annotate(c=Count("article__title")).values("title", "c")
    
        tag_list = Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article__title")).values_list("title", "c")
    
        date_list = Article.objects.filter(user=user).extra(
            select={"y_m_date": "date_format(create_time,'%%Y-%%m')"}).values("y_m_date").annotate(c=Count("nid")).values(
            "y_m_date", "c")  # annotate()之前要先 .values()
        return {"blog":blog,"cate_list":cate_list,"tag_list":tag_list,"date_list":date_list}
    """
    
    def article_detail(request,username,article_id):
    
        # 也需要先把 标签、分类、日期归档的数据读取出来
        # 方式一:
        # content = get_classification_data(username)
    
        # 方式二:利用 inclusion_tag :inclusion_tag能把数据和样式结合成一个标签函数,通过 inclusion_tag 能够获取到一整套的 html 数据(数据和样式结合成一个整体)
    
        # 渲染文章详情
        # 获取文章数据
        article_obj = Article.objects.filter(pk=article_id).first()
        print(article_obj)
    
        # render渲染根评论
        comment_list = Comment.objects.filter(article_id=article_id).all()  # 这篇文章的所有评论
    
        return render(request,"article_detail.html",{"username":username,"article_obj":article_obj,"comment_list":comment_list})
    
    
    def digg(request):
    
        # urlencoded 编码格式传到后端的数据类型是 字符串 格式化的
        is_up = json.loads(request.POST.get("is_up"))  # 所以 request.POST.get("is_up") 是字符串格式(值为字符串"true" 或 字符串"false");所以需要用 json.loads() 反序列化
        article_id = request.POST.get("article_id")
        user_id = request.user.pk   # 点赞人即当前登陆人;从session中获取
    
        # 先判断该用户是否已经对这篇文章做过点赞或者反对的操作
        is_handled = ArticleUpDown.objects.filter(user_id=user_id,article_id=article_id).first()
        response = {"state":True}  # Ajax通常返回字典
        if not is_handled:
            # 在点赞表中生成记录
            ArticleUpDown.objects.create(user_id=user_id,article_id=article_id,is_up=is_up)
    
            # 在ArticleUpDown表生成一条赞(踩)记录,就应该对应把 Article表中的 up_count(down_count)字段加1
            queryset = Article.objects.filter(pk=article_id)
            if is_up:
                queryset.update(up_count=F("up_count")+1)  # QuerySet 调用 update()
            else:
                queryset.update(down_count=F("down_count") + 1)
        else:
            response["state"] = False
            response["handled"] = is_handled.is_up  # 告诉前端 该用户已经对这篇文章做过什么操作(“点赞”、“反对”)
    
        return JsonResponse(response)
    
    def comment(request):
        print(request.POST)
    
    
        article_id = request.POST.get("article_id")
        pid = request.POST.get("pid")
        content = request.POST.get("content")
        user_id = request.user.pk
    
        with transaction.atomic():  # with transaction.atomic() 里面的内容就会变成同一个事务:同成功或同失败
            # 生成一条评论记录(ajax)
            comment_obj = Comment.objects.create(user_id=user_id,article_id=article_id,content=content,parent_comment_id=pid)
            # 数据同步: Article表中的 comment_count 字段要加1
            Article.objects.filter(pk=article_id).update(comment_count=F("comment_count")+1)
    
        # 利用ajax渲染刚刚提交的那条根评论
        response = {}
        response["create_time"] = comment_obj.create_time.strftime("%Y-%m-%d %X")    # create_time是datetime.datetime 类型的对象,对象不能序列化,所以先要将其利用 strftime 转化为 字符串;%X表示 时间(time)格式
        response["username"] = request.user.username
        response["content"] = content
    
        # 如果该评论为子评论,需要把父评论的信息也返回给客户端
        if pid:
            parent_obj = Comment.objects.filter(pk=pid).first()  # 过滤出主键值等于 pid 的Comment对象即为其父评论
            response["parent_username"] = parent_obj.user.username
            response["parent_content"] = parent_obj.content
            print("parent_obj",parent_obj)
            print("response",response)
    
        # 发送邮件
        from django.core.mail import send_mail
        from cnblog import settings
    
        article_obj = Article.objects.filter(pk=article_id).first()
    
        """
        #语法: send_mail(subject, message, from_email, recipient_list)
        
        send_mail(
            "您的文章《%s》新增了一条评论内容"%article_obj.title,
            content,
            settings.EMAIL_HOST_USER,
            ["380544011@qq.com"]  # 此处的邮箱应该为用户的邮箱
        )  
        # 这样方式的缺点:运行速度太慢;应该用多线程
        """
        from threading import Thread
        t = Thread(target=send_mail,args=("您的文章《%s》新增了一条评论内容"%article_obj.title,content,settings.EMAIL_HOST_USER,["380544011@qq.com"]))
        t.start()
    
    
        return JsonResponse(response)
    
    def get_comment_tree(request):
        article_id = request.GET.get("article_id")
        ret = list(Comment.objects.filter(article_id=article_id).order_by("pk").values("pk","content","parent_comment_id"))  # Comment.objects.filter(article_id=article_id).values("pk","content","parent_comment_id") 是一个QuerySet(类似于[{},{},{}..] ),但其毕竟不是列表,所以需要用 list()方法将其强转为一个列表
        # 子评论一定在与其关联的父评论之后
    
        # 当用JsonResponse() 返回一个 非字典 的数据类型时, 要将 safe=False,要不然会报错
        return JsonResponse(ret,safe=False)
    
    
    # 后台管理
    @login_required
    def cn_backend(request):
        article_list = Article.objects.filter(user=request.user)
    
        return render(request,"backend/backend.html", locals())
    
    @login_required
    def add_article(request):
    
        if request.method == "POST":
            title = request.POST.get("article_title")
            content = request.POST.get("article_content")
            soup = BeautifulSoup(content, "html.parser")
            # 过滤出去 <script> 标签
            for tag in soup.find_all():
                if tag.name == "script":  # tag.name 表示 标签名(字符串格式)
                    tag.decompose()  # 从 soup 中把 该标签 删除
            # 截取文章摘要
            desc = "%s..." % soup.text[0:100]
    
            Article.objects.create(title=title,desc=desc,content=str(soup),user=request.user)  # str(soup) :过滤出<script>标签后的标签字符串
    
            return redirect("/cn_backend/")
    
        return render(request,"backend/add_article.html",locals())
    
    # 后台编辑文章
    
    def edit_article(request,edit_article_id):
        edit_article_obj = Article.objects.filter(pk=edit_article_id).first()
        if request.method == "POST":
            title = request.POST.get("article_title")
            content = request.POST.get("article_content")
            soup = BeautifulSoup(content, "html.parser")
            # 过滤出去 <script> 标签
            for tag in soup.find_all():
                if tag.name == "script":  # tag.name 表示 标签名(字符串格式)
                    tag.decompose()  # 从 soup 中把 该标签 删除
            # 截取文章摘要
            desc = "%s..." % soup.text[0:100]
    
            Article.objects.filter(pk=edit_article_id).update(title=title,desc=desc,content=str(soup))
    
            return redirect("/cn_backend/")
    
        return render(request,"backend/edit_article.html",{"edit_article_obj":edit_article_obj})
    
    # 后台删除文章
    def delete_article(request,delete_article_id):
        Article.objects.filter(pk=delete_article_id).delete()
        return redirect("/cn_backend/")
    
    # 输入框文件上传路径
    def upload(request):
        print(request.FILES)
    
        # 下载文件
        img = request.FILES.get("upload_img")
        print(img.name)  # 文件对象都有一个 name 属性, img.name 表示 文件对象的文件名
        path = os.path.join(settings.MEDIA_ROOT,"add_article_img",img.name)  # 文件的绝对路径
    
        with open(path,"wb") as f:
            for line in img:
                f.write(line)
    
        # 在前端的输入框中显示上传的文件
        # 下载完成后需要把文件对应的路径(得是json格式的数据)交给前端的文本编辑器
        response = {
            "error":0,
            "url": "/media/add_article_img/%s/"%img.name      # "url"表示前端能够用来预览该文件(如图片)的路径;不是上面的 path 路径,path路径是文件存储的绝对路径,这个路径发给前端没有任何意义
        }
    
        # import json
        # return HttpResponse(json.dumps(response))
        return JsonResponse(response)

    blog/admin.py

    from django.contrib import admin
    
    # Register your models here.
    from blog import models
    
    admin.site.register(models.UserInfo)
    admin.site.register(models.Blog)
    admin.site.register(models.Category)
    admin.site.register(models.Tag)
    admin.site.register(models.Article)
    admin.site.register(models.Article2Tag)
    admin.site.register(models.ArticleUpDown)
    admin.site.register(models.Comment)

    blog/templatetags/my_tags.py

    from django import template
    from django.db.models import Count
    from blog.models import *
    
    register = template.Library()
    
    @register.inclusion_tag("classification.html")  # inclusion_tag()中的参数表示所要引入的一套模板文件
    def get_classification_data(username):  # get_classification_data() 这个方法一旦被调用,它会先执行下面的数据查询,查询完之后会把下面的字典返回 "classification.html" 这个模板文件(没有返回给调用者),因为 "classification.html" 文件会需要下面的这几个变量;下面的变量传给 "classification.html"之后会进行 render 渲染,渲染成一堆完整的 html标签
        user = UserInfo.objects.filter(username=username).first()
    
        #  查询当前站点对象
        blog = user.blog
    
        cate_list = Category.objects.filter(blog=blog).values("pk").annotate(c=Count("article__title")).values("title", "c")
    
        tag_list = Tag.objects.filter(blog=blog).values("pk").annotate(c=Count("article__title")).values_list("title", "c")
    
        date_list = Article.objects.filter(user=user).extra(
            select={"y_m_date": "date_format(create_time,'%%Y-%%m')"}).values("y_m_date").annotate(c=Count("nid")).values(
            "y_m_date", "c")  # annotate()之前要先 .values()
        return {"username":username,"blog": blog, "cate_list": cate_list, "tag_list": tag_list, "date_list": date_list}

    blog/utils/myForms.py

    from blog.models import *
    # forms 组件
    from django import forms
    from django.forms import widgets
    
    from django.core.exceptions import ValidationError
    class UserForm(forms.Form):
        user = forms.CharField(max_length=32,
                               widget=widgets.TextInput(attrs={"class":"form-control"}),
                               label="用户名",
                               error_messages={"required":"该字段不能为空"})
        psw = forms.CharField(max_length=32,
                              widget=widgets.PasswordInput(attrs={"class":"form-control"}),
                              label="密码",
                              error_messages={"required": "该字段不能为空"})
        re_psw = forms.CharField(max_length=32,
                                 widget=widgets.PasswordInput(attrs={"class":"form-control"}),
                                 label="确认密码",
                                 error_messages={"required": "该字段不能为空"})
        email = forms.EmailField(max_length=32,
                                 widget=widgets.EmailInput(attrs={"class":"form-control"}),
                                 label="邮箱",
                                 error_messages={"required": "该字段不能为空"})
        # 头像字段不是必须的,用户可以传也可以不传;所以没必要校验
    
        # 局部钩子校验新注册的用户名是否已经存在
        def clean_user(self):
            user = self.cleaned_data.get("user")
            user_obj = UserInfo.objects.filter(username=user).first()
    
            if not user_obj:
                return user
            else:
                raise ValidationError("该用户名已被注册")
    
        # 全局钩子校验两次密码是否一致
        def clean(self):
            psw = self.cleaned_data.get("psw")
            re_psw = self.cleaned_data.get("re_psw")
    
            if psw and re_psw:
                if psw == re_psw:
                    return self.cleaned_data
                else:
                    raise ValidationError("两次密码不一致!")
            else:
                return self.cleaned_data

    blog/utils/validCode.py

    from random import randint
    import random
    def get_random_color():  # 用于随机生成颜色
        return (randint(0, 255), randint(0, 255), randint(0, 255))
    
    def get_valid_code_img(request):
    
        """
        # 方式二:通过磁盘(open就是操作磁盘)
        # 动态生成一张图片(pip install pillow:先下载安装pillow模块)
    
        from PIL import Image
        img = Image.new("RGB",(260,33),color=get_random_color())  # new()里面有三个参数:第一个表示模式(RGB表示彩色),第二个表示图片宽高(需要和css中设置的宽高一致),第三个表示背景颜色 # 得到一个Image对象img
        # 把随机生成的 img 对象存到一个文件中
        with open("validCode.png","wb") as f:
            img.save(f,"png")  # 把 img 以 png的格式存到 f 文件中
    
        # 把动态生成的图片发送给客户端
        with open("validCode.png","rb") as f:
            data = f.read()
        """
        """
        # 方式三:通过内存
        from io import BytesIO
        # BytesIO是内存管理工具
        from PIL import Image
        img = Image.new("RGB", (260, 33), color=get_random_color())
        # 内存处理
        f = BytesIO()  # f就是一个内存句柄
        img.save(f,"png")  # 把img保存到内存句柄中;# save()之后就能把img保存到内存中
        data = f.getvalue()  # 把保存到内存中的数据读取出来
        # BytesIO会有一个自动清除内存的操作
        """
        # 方式四:给生成的图片(画板)中添加文字
        from io import BytesIO
        # BytesIO是内存管理工具
        from PIL import Image, ImageDraw, ImageFont
        img = Image.new("RGB", (260, 33), color=get_random_color())  # new()里面有三个参数:第一个表示模式(RGB表示彩色),第二个表示图片宽高(需要和css中设置的宽高一致),第三个表示背景颜色 # 得到一个Image对象img
        # 往画板中添加文字
        draw = ImageDraw.Draw(img)  # 得到一个draw对象 # 可这么理解:用ImageDraw这个画笔往 img 画板上书写
        kumo_font = ImageFont.truetype("static/font/KumoFont.ttf",
                                       size=20)  # 定义字体;第一个参数是字体样式的路径,第二个是字体大小 # 路径中 static 前不能加 /
    
        valid_code_str = ""  # 用于保存验证码
        for i in range(4):
            random_num = str(randint(0, 9))  # 数字
            random_lower = chr(randint(97, 122))  # 小写
            random_upper = chr(randint(65, 90))  # 大写
            random_char = random.choice([random_num, random_lower, random_upper])
            draw.text((i * 60 + 30, 5), random_char, get_random_color(),
                      font=kumo_font)  # draw.text():利用draw对象往画板里面书写文字;第一个参数是一个元组(x,y),表示横坐标、纵坐标的距离;第二个参数表示文字内容;第三个参数表示字体颜色;第四个表示字体样式
            valid_code_str += random_char
    
        # 验证图片的噪点、噪线
        width = 260
        height = 33  # width 和 height要和前端验证图片的宽高一致
        # 噪线
        for i in range(5):
            x1 = randint(0, width)
            y1 = randint(0, height)  # (x1,y1)是线的起点
            x2 = randint(0, width)
            y2 = randint(0, height)  # (x2,y2)是线的终点
            draw.line((x1, y1, x2, y2), fill=get_random_color())
        # 噪点
        for i in range(100):
            draw.point([randint(0, width), randint(0, height)], fill=get_random_color())  # 在给定的坐标点上画一些点。
            x = randint(0, width)
            y = randint(0, height)
            draw.arc((x, y, x + 4, y + 4), 0, 90, fill=get_random_color())  # 在给定的区域内,在开始和结束角度之间绘制一条弧(圆的一部分)
        # 参考链接: https://blog.csdn.net/icamera0/article/details/50747084
    
        # 重点:储存随机生成的验证码(不能用 global 的方式去处理验证码 valid_code_str,因为此时当有其他用户登陆时验证码会被别人刷新掉;正确的方式是把该验证码存到 session 中 )
        request.session["valid_code_str"] = valid_code_str  # 注意:这句代码执行了三个操作过程
    
        # 内存处理
        f = BytesIO()  # f就是一个内存句柄
        img.save(f, "png")  # 把img保存到内存句柄中;# save()之后就能把img保存到内存中
        data = f.getvalue()  # 把保存到内存中的数据读取出来
        # BytesIO会有一个自动清除内存的操作
    
        return data

    templates/classification.html

    <div class="header">
        <div class="content">
            <p class="title">
    {#            <span>{{ blog.title }}</span>#}
                <a class="home_site" href="/{{ username }}/">{{ blog.title }}</a>
                <a href="/cn_backend/" class="backend">后台管理</a>
            </p>
        </div>
    </div>
    
    <div class="container">
        <div class="row">
    
            <div class="col-md-3">
                <div class="panel panel-danger">
                    <div class="panel-heading">我的标签</div>
                    <div class="panel-body">
                        {% for tag in tag_list %}
                            {# /{{ username }}/表示个人站点,/{{ tag.0 }}/表示标签名,views.py中的home_site()会根据该标签名过滤文章 #}
                            <p><a href="/{{ username }}/tag/{{ tag.0 }}/">{{ tag.0 }} ({{ tag.1 }})</a></p>
                        {% endfor %}
    
                    </div>
                </div>
                <div class="panel panel-warning">
                    <div class="panel-heading">随笔分类</div>
                    <div class="panel-body">
                        {% for cate in cate_list %}
                            <p><a href="/{{ username }}/category/{{ cate.title }}/">{{ cate.title }} ({{ cate.c }})</a></p>
                        {% endfor %}
    
                    </div>
                </div>
                <div class="panel panel-info">
                    <div class="panel-heading">文章归档</div>
                    <div class="panel-body">
    
                        {% for date in date_list %}
                            <p><a href="/{{ username }}/archive/{{ date.y_m_date }}/">{{ date.y_m_date }} ({{ date.c }})</a>
                            </p>
                        {% endfor %}
    
                    </div>
                </div>
            </div>

    templates/base.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    
        <link rel="stylesheet" href="/static/blog/css/home_site.css">
        <link rel="stylesheet" href="/static/blog/css/article_detail.css">
        <link rel="stylesheet" href="/static/blog/bs/css/bootstrap.min.css">
        <script src="/static/js/jquery-3.3.1.js"></script>
    </head>
    <body>
    
    
    {# 引入自定义的 my_tag #}
    {% load my_tags %}
    {# 调用 get_classification_data,需要传入 username 这个参数 #}
    {% get_classification_data username %}
    {# 调用完 get_classification_data username 就能把其对应的那一套 html 代码返回到此处 #}
    {# 以article_detail()为例,调用 get_classification_data username 这个 inclusion_tag的流程:进入 urls 控制器 ---> 进入article_detail() 函数 ---> 进入"article_detail.html",返回 "article_detail.html"之前先渲染这个 html文件 ---> 渲染"article_detail"之前先继承 "base.html" ---> 继承"base.html"时,遇到 "get_classification_data"这个 inclusion_tag ---> 进入 my_tags.py 中的 get_classification_data这个 inclusion_tag,这个inclusion_tag先获取相应的 变量的值,然后把这些变量的值返回给 "classification.html"去渲染,得到一整套的 html标签 ---> 得到的这一整套标签放到调用 这个 inclusion_tag 的地方(此处是"base.html"),得到完整的html标签 ---> 该完整的html标签继承给 "article_detail.html" ---> 把全部渲染好的html标签返回给客户端 #}
    
    <div class="col-md-9">
        {% block content %}
    
        {% endblock %}
    
    </div>
    </div>
    </div>
    
    </body>
    
    </html>

    templates/index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <link rel="stylesheet" href="/static/blog/bs/css/bootstrap.min.css">
        {# 先引入 jquery 再引入 bootstrap的js;Bootstrap的js是基于 jquery #}
        <script src="/static/js/jquery-3.3.1.js"></script>
        <script src="/static/blog/bs/js/bootstrap.js"></script>
    
        <style>
            #user_icon {
                font-size: 18px;
                margin-right: 10px;
                vertical-align: -3px;
            }
            .pub_info{
                margin-top: 10px;
            }
            .pub_info
        </style>
    
    </head>
    <body>
    
    <nav class="navbar navbar-default">
        <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="#">博客园</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><a href="#">博文</a></li>
    
                </ul>
    
                <ul class="nav navbar-nav navbar-right">
                    {# 根据用户是否登陆,显示相应的内容 #}
                    {% if request.user.is_authenticated %}
                        <li><a href="/{{ request.user.username }}/"><span id="user_icon" class="glyphicon glyphicon-user"></span>{{ 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="#">修改密码</a></li>
                                <li><a href="#">修改头像</a></li>
                                <li><a href="/logout/">注销</a></li>
                                <li role="separator" class="divider"></li>
                                <li><a href="#">Separated link</a></li>
                            </ul>
                        </li>
                    {% else %}
                        <li><a href="/login/">登陆</a></li>
                        <li><a href="/register">注册</a></li>
    
                    {% endif %}
    
                </ul>
            </div>
        </div>
    </nav>
    
    <div class="container-fluid">
        <div class="row">
            <div class="col-md-3">
                <div class="panel panel-warning">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
                <div class="panel papel-info">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
                <div class="panel panel-danger">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
            </div>
            <div class="col-md-6">
                <div class="article_list">
                    {# 循环读取 article_list 中的文章对象,并将其渲染到浏览器上 #}
                    {% for article in article_list %}
                        <div class="article-item">
                            {# 文章标题 #}
                            <h5><a href="/{{ article.user.username }}/articles/{{ article.pk }}/">{{ article.title }}</a></h5>
                            <div class="article-desc">
                                {# class="media-left" 和 class="media-right":使内容左、右浮 #}
                                <span class="media-left">
                                    {# 头像图片的获取:Article表中有一个外键字段 user,通过 Article.user.avatar 可获取该用户头像在 "avatars"文件夹下的相对路径,如:avatars/头像.jpg ;所以还需要在这个相对路径前加上 "media/" 这个别名 #}
                                    <a href="/{{ article.user.username }}/"><img width="56" height="56" src="media/{{ article.user.avatar }}" alt=""></a>
                                </span>
                                <span class="media-right">
                                    {{ article.desc|safe }}
                                </span>
                            </div>
    
                            {# class="small" 表示小字体 #}
                            <div class="small pub_info">
                                <span><a href="/{{ article.user.username }}/">{{ article.user.username }}</a></span> &nbsp;&nbsp;&nbsp;
                                {# date过滤器:分钟用 i 表示 #}
                                <span>发布于&nbsp;&nbsp;{{ article.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;&nbsp;
                                <span class="glyphicon glyphicon-comment" style="margin-right: 3px"></span>评论({{ article.comment_count }})&nbsp;&nbsp;
                                <span class="glyphicon glyphicon-thumbs-up" style="margin-right: 3px"></span>点赞({{ article.up_count }})
                            </div>
                        </div>
                        <hr>
                    {% endfor %}
    
                </div>
            </div>
            <div class="col-md-3">
                <div class="panel panel-primary">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
                <div class="panel panel-default">
                    <div class="panel-heading">Panel heading without title</div>
                    <div class="panel-body">
                        Panel content
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    </body>
    </html>

    templates/register.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Register</title>
        <link rel="stylesheet" href="/static/blog/bs/css/bootstrap.min.css">
        {# 当用户访问 /login/ 的GET请求时,浏览器会向服务器发送再次请求:第一次是获取到了 login.html 这个登陆页面;第二次是当浏览器加载 login.html 里面的字符串时,遇到了上面的 link bootstrap,此时浏览器会再次向服务器发送请求获取 bootstrap中相应的文件(页面中有静态文件时都会再次发送请求获取静态文件) #}
        <style>
            .reg_btn{
                margin-top: 10px;
            }
            .error{
                color: red;
            }
        </style>
    </head>
    <body>
    <h3>注册页面</h3>
    <div class="container">
    
        <div class="row">
            <div class="col-md-6 col-lg-offset-3">
                {# 利用Ajax传递数据 #}
                <form id="form">
                    {% csrf_token %}
                    {% for field in form %}
                        <div class="form-group">
                            {# field.auto_id 表示{{ field }}渲染出来的那个input标签的id值 #}
                            <label for="{{ field.auto_id }}">{{ field.label }}</label>
                            {# 通过 {{ field }}渲染出来的input标签的id是有规律的:id_字段(id_field) #}
                            {{ field }}
                            <span class="error pull-right"></span>
                        </div>
                    {% endfor %}
                    {# 因为forms组件中没有 头像 字段,所以需要单独处理 头像 上传 #}
                    <div class="form-group">
                            {# label标签有一个特点:如果 label的for属性值和input的id相等,那么在前端点击这个label标签就相当于点击了这个input标签 #}
                            <label for="avatar">
                                头像
                                {# 具体项目中不要像这样把css写到代码中 #}
                                <img id="avatar_img" style="margin-left: 20px; 60px;height: 60px" src="/static/blog/img/default.jpg" alt="">
                            </label>
                        {# 点击label中的img 就相当于点击下面的 input 标签 #}
                        <input type="file" id="avatar" style="display: none">
                        {# 点击lable就相当于点击 input 标签,所以可以把上面的 input 标签隐藏掉(display:none) #}
    
                        {#  ---头像预览的逻辑:#}
                        {#  1. 获取用户选中的文件对象#}
                        {#  2. 获取文件对象的路径#}
                        {#  3. 修改img的src 属性,使src = 文件对象的路径#}
                        </div>
    
                    <input type="button" class="btn btn-default reg_btn" value="注册">
    
                </form>
            </div>
        </div>
    </div>
    <script src="/static/js/jquery-3.3.1.js"></script>
    <script>
        {# 头像预览功能 #}
        {# 给上传头像的 input标签 邦定 change()事件(监听输入框中内容的变化) #}
        $("#avatar").change(function () {
            {#alert(123)#}
    
            {#  1. 获取用户选中的文件对象#}
            {# 通过 .files 的方法可以获取 type="file"的input 标签中的文件对象 #}
            var avatar_obj = $(this)[0].files[0];
    
            {#  2. 获取文件对象的路径 #}
            {# 获取文件路径时需要 利用一个 文件阅读器 FileReader() #}
            var reader = new FileReader();  // reader是一个实例对象
    
            reader.readAsDataURL(avatar_obj);  // reader.readAsDataURL(文件对象) 表示获取该文件的路径;此函数没有返回值,它会把 read 出来的结果放到 reader 这个对象内部;读取的结果可以通过 reader.result() 的方式获取到
            // readAsDataURL() 方法是新开的一个 异步线程
    
            {#  3. 修改img的src 属性,使src = 文件对象的路径#}
            {# 在把第二步获取到的路径赋值给 img的src之前,需要先确保 reader.readAsDataURL(avatar_obj) 这个异步线程已经执行完毕;此时就需要用到 onload 方法 #}
            {# 给reader对象添加一个 onload 事件:等 reader 加载完之后再执行后面的操作 #}
            reader.onload = function () {
                {# reader 不执行完,就不会执行下面的操作 #}
                {# 让 label 标签中的 img 显示 预览的图片 #}
                $("#avatar_img").attr("src",reader.result);  // 通过 reader.result获取到文件对象(avatar_obj)的路径
            }
        });
    
        {# 基于Ajax提交数据 #}
        {#  涉及到文件上传,一定要用 FormData 创建一个新的对象(formdata编码);固定格式  #}
        $(".reg_btn").click(function () {
            var formdata = new FormData();
    
            {# formdata.append("user",$("#id_user").val()); #}
            {# formdata.append("pwd",$("#id_psw").val()); #}
            {# formdata.append("re_psw",$("#id_re_psw").val()); #}
            {# formdata.append("email",$("#id_email").val()); #}
            {# 利用Ajax往同一个URL上发送post请求时,需要自己组装 csrfmiddlewaretoken 的键值 #}
            {#formdata.append("csrfmiddlewaretoken",$("[name='csrfmiddlewaretoken']").val());#}
            {# 利用 dom对象.files[0] 的方式获取 input 标签中的文件对象  #}
            formdata.append("avatar",$("#avatar")[0].files[0]);  // 这个需要自己手动写
    
            {# 上面的代码可简写为如下: #}
            {# console.log($("#form").serializeArray()); // $("#form").serializeArray() 是一个数组的形式,数组里面是 object(对象)#}
            {# 循环 $("#form").serializeArray() 这个数组 #}
            var request_data = $("#form").serializeArray();
            $.each(request_data,function (index,element) {  // $.each()也是遍历; index表示索引,element表示 数组中的对象
                formdata.append(element.name,element.value);  //  element.name 表示 表单控件的 name属性值,element.value 表示 表单控件的 value属性值
            });
    
            $.ajax({
                url:"",
                type:"post",
                contentType:false,
                processData:false,
                data:formdata,
                success:function (data) {
                    if (data.user){
                        // 注册成功,则进入登陆页面
                        location.href = "/login/"
                    }
                    {# 为input 标签添加错误信息 #}
                    else { // 注册失败
                        {# 展示错误信息前先把原先的错误信息清空,并将其父级元素中的 class="has-error"移除 #}
                        $("span.error").html("");
                        $(".form-group").removeClass("has-error");
    
                        $.each(data.msg,function (field,error_list) {  // 此时的 data.msg 是服务器发送过来的 form.errors;当两次密码一致时,form.errors 里面也会有 全局钩子 "__all__"
                            console.log(field,error_list);
    
                            {# 存放全局钩子错误信息 #}
                            if (field === "__all__" ){
                                {# 把“两次密码不一致”的错误信息放到 "确认密码"后面,并给其父级元素添加 class="has-error" #}
                                $("#id_re_psw").next().html(error_list[0]).parent().addClass("has-error")
                            }
    
                            {# 循环 data.msg(form.errors)这个对象(字典);field是错误信息对应的字段,error_list是错误信息列表 #}
                            {# input标签的id是有规律的:id_field,错误信息对应的字段是field;可通过这种规律把错误信息添加到对应的 span 标签中 #}
                            $("#id_"+field).next().html(error_list[0]);
                        {# next(): 获得匹配元素集合中每个元素紧邻的同胞元素。如果提供选择器,则取回下一个同胞元素中的、匹配该选择器的元素。 #}
    
                            {# 给表单控件的父级元素添加 "has-error" 的类,能让表单控件边框显示红色 #}
                            $("#id_"+field).parent().addClass("has-error");
                        })
                    }
                }
            })
        })
    
    </script>
    </body>
    </html>

    templates/login.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Login</title>
        <link rel="stylesheet" href="/static/blog/bs/css/bootstrap.min.css">
        {# 当用户访问 /login/ 的GET请求时,浏览器会向服务器发送再次请求:第一次是获取到了 login.html 这个登陆页面;第二次是当浏览器加载 login.html 里面的字符串时,遇到了上面的 link bootstrap,此时浏览器会再次向服务器发送请求获取 bootstrap中相应的文件(页面中有静态文件时都会再次发送请求获取静态文件) #}
        <style>
            .login_btn,.reg_btn{
                margin-top: 10px;
            }
        </style>
    </head>
    <body>
    <h3>登陆页面</h3>
    <div class="container">
    
        <div class="row">
            <div class="col-md-6 col-lg-offset-3">
                {# 利用Ajax传递数据 #}
                <form>
                    {% csrf_token %}
                    {# form-group能让表单控件之间有间距 #}
                    <div class="form-group">
                        <label for="user">用户名</label>
                        {# input标签无需再加 name属性;name属性是用于 type="submit"的input标签提交数据时组装数据,然后服务端利用name属性获取数据;现在我需要利用Ajax自己组装数据 #}
                        <input type="text" id="user" class="form-control">
                    </div>
                    <div class="form-group">
                        <label for="psw">密码</label>
                        <input type="password" id="psw" class="form-control">
                    </div>
    
                    <div class="form-group">
                        <label for="">验证码</label>
                        {# class="row"表示独占一行 #}
                        <div class="row">
                            <div class="col-md-6">
                                <input type="text" class="form-control valid_code">
                            </div>
                            <div class="col-md-6">
                                {# img的src可以写一个路径 #}
                                <img style=" 260px;height: 33px" id="valid-code" src="/get_validCode_img/" alt="">
                            </div>
                        </div>
                    </div>
                    <input type="button" class="btn btn-default login_btn" value="登陆"><span class="error"></span>
                    <a href="/register/" class="btn btn-success pull-right reg_btn">注册</a>
                </form>
            </div>
        </div>
    </div>
    <script src="/static/js/jquery-3.3.1.js"></script>
    <script>
    {#点击验证码 刷新验证码图片#}
        $("#valid-code").click(function () {
            $(this)[0].src += "?"
        })
        {# 此处不需要用Ajax;通过 .src+="?" 的方式就能局部刷新验证码图片  #}
    
    {# 给登陆按钮添加Ajax事件:登陆验证 #}
        $(".login_btn").click(function () {
            $.ajax({
                url:"",
                type:"post",
                data:{
                    "user":$("#user").val(),
                    "psw":$("#psw").val(),
                    "valid_code":$(".valid_code").val(),
                    {# 由于是用Ajax提交数据,所以需要加上下面这句代码;其作用:用于POST请求的csrf验证 #}
                    {# $("[name='csrfmiddlewaretoken']") :属性选择器#}
                    csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val()
                },
                success:function (data) {
                    console.log(data)
                    {# data直接就是 object 类型 #}
                    console.log(typeof data)
    
                    {#data = JSON.parse(data)#}
                    {#查看返回结果:如果通过验证,则跳转;否则把错误信息添加到 class="error"的span标签中#}
                    {# data.user:点语法 #}
                    if (data.user){
                        {# 前端跳转的方法:location.href="" #}
                        location.href = "/index/"
                    }
                    else {
                        $(".error").html(data.msg).css({"margin-left": "10px", color: "red"})
    
                        {#setTimeout(function () {$(".error").html("")},1000)#}
    
                    }
                }
            })
        })
    </script>
    </body>
    </html>

    templates/home_site.html

    {% extends "base.html" %}
    
    {% block content %}
        <div class="col-md-9">
            <p><a href="/" class="btn btn-success pull-right return_to_index">博客园</a></p>
            <div class="article_list">
                {# 循环读取 article_list 中的文章对象,并将其渲染到浏览器上 #}
                {% for article in article_list %}
                    <div class="article-item clearfix">
                        {# 文章标题 #}
                        <h5><a href="/{{ username }}/articles/{{ article.pk }}/">{{ article.title }}</a></h5>
                        <div class="article-desc">
                            {{ article.desc }}
                        </div>
    
                        {# class="small" 表示小字体 #}
                        <div class="small pub_info pull-right">
                            {# date过滤器:分钟用 i 表示 #}
                            <span>发布于&nbsp;&nbsp;{{ article.create_time|date:"Y-m-d H:i" }}</span>&nbsp;&nbsp;&nbsp;
                            <span class="glyphicon glyphicon-comment"
                                  style="margin-right: 3px"></span>评论({{ article.comment_count }})&nbsp;&nbsp;
                            <span class="glyphicon glyphicon-thumbs-up"
                                  style="margin-right: 3px"></span>点赞({{ article.up_count }})
                        </div>
                    </div>
                    <hr>
                {% endfor %}
    
            </div>
    
        </div>
    {% endblock %}

    templates/article_detail.html

    {% extends "base.html" %}
    {% block content %}
        {# 向当前页面提交post请求需要加 {% csrf_token %}   #}
        {% csrf_token %}
        <h3 class="text-center">{{ article_obj.title }}</h3>
        <div>{{ article_obj.content|safe }}</div>
        {# |safe过滤器 表示 渲染页面时不进行标签字符串转义  #}
        {# 父级 class = "clearfix" 表示 清除浮动 #}
        <div class="clearfix">
            <div id="div_digg">
                <div class="diggit action">
                    {# 从 Article表中读取 up_count 点赞数 #}
                    <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" style="color: red;"></div>
            </div>
        </div>
    
    
        <div class="comments list-group">
    
            {# 评论树 思路:先放根评论,再根据子评论的 parent_comment_id 和 父评论的 主键值 ,把子评论添加到根评论的下面 #}
            <p class="tree_btn">评论树</p>
    
            <ul class="comment_tree comment_list">
                {# 里面不要写任何数据,让其动态的去添加 #}
            </ul>
    
            <script>
                {# .one() 表示 一次性事件 #}
                $(".tree_btn").one("click",function () {
                    $.ajax({
                        url:"/get_comment_tree/",
                        type:"get",
                        data:{
                            "article_id":"{{ article_obj.pk }}"
                        },
                        success:function (comment_list) {
                            console.log(comment_list);
    
                            $.each(comment_list,function (index,comment_obj) {
                                var pk = comment_obj.pk;
                                var content = comment_obj.content;
                                var parent_comment_id = comment_obj.parent_comment_id;
    
                                {# 给 div 添加一个自定义的 comment_id 属性,属性值为它在Comment表中的主键值 #}
                                var str = `
                                    <li class="list-group-item comment_item" comment_id="${pk}">
                                        <span>${content}</span>
                                    </li>
                                `;
    
                                if (!parent_comment_id){
                                    {#!parent_comment_id 表示 parent_comment_id 为空,即没有父评论 #}
    
                                    {# 添加根评论 #}
                                    {# 把上面的 str  放到 "comment_tree"里面 #}
                                    $(".comment_tree").append(str);
                                }else {
                                    {# 添加子评论 #}
                                    {# 找到 主键值pk 等于 该评论 parent_comment_id 的评论,然后将这条子评论插到该父评论下面 #}
                                    $("[comment_id="+parent_comment_id+"]").append(str);
                                    {# "[comment_id="+parent_comment_id+"]":利用字符串拼接出 属性选择器  #}
    
                                }
    
                            })
    
                        }
                    })
                })
            </script>
    
    
            <p>评论列表</p>
            {# render渲染根评论 #}
            {# ul class="list-group" 表示列表组   #}
            <ul class="list-group comment_list">
                {% for comment in comment_list %}
                    <li class="list-group-item">
                        <div>
                            {# 楼层 #}
                            <a href=""># {{ forloop.counter }}楼</a> &nbsp;&nbsp;
                            <span>{{ comment.create_time|date:"Y-m-d H:i" }}</span> &nbsp;&nbsp;
                            <a href="/{{ comment.user.username }}/">{{ comment.user.username }}</a>
                            {# 下面a 标签中的 username 和 comment_pk 属性是自定义属性,用于储存 当前评论的用户名 和 当前评论在Comment表中的主键值 #}
                            <a class="pull-right reply_btn" username="{{ comment.user.username }}"
                               comment_pk="{{ comment.pk }}">回复</a>
                        </div>
    
                        {# 判断这条评论是否为子评论;如果是子评论就显示出它的父评论相关信息 #}
                        {% if comment.parent_comment_id %}
                            <div class="pid_info well">
                                <p>
                                    {{ comment.parent_comment.user.username }}:{{ comment.parent_comment.content }}
                                </p>
                            </div>
                        {% endif %}
    
                        <div class="comment_con">
                            <p>{{ comment.content }}</p>
                        </div>
                    </li>
                {% endfor %}
    
            </ul>
            <p>发表评论</p>
            <p>
                昵称:<input type="text" id="tbCommentAuthor" class="author" disabled="disabled" size="50"
                          value="{{ request.user.username }}">
            </p>
            <p>评论内容:</p>
            <textarea name="" id="comment_content" cols="60" rows="10"></textarea>
            <p>
                <button class="btn btn-default comment_btn">提交评论</button>
            </p>
        </div>
    
        <script>
            // 点赞请求
            $("#div_digg .action").click(function () {
                {# 通过 .hasClass("diggit") 的方式区分点击的是“支持”(class="diggit")还是“反对”(class="buryit");“支持”为true,“反对”为false #}
                var is_up = $(this).hasClass("diggit");
                console.log(is_up);
                $.ajax({
                    url: "/digg/",
                    type: "post",
                    {# data:哪个用户对哪篇文章做了支持还是反对;点赞人即为当前登陆人,所以点赞人不用传给后台(直接从session中取)#}
                    data: {
                        "is_up": is_up,
                        "article_id": "{{ article_obj.pk }}",
                        "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val()
                    },
                    success: function (data) {
                        if (data.state) {  // 该用户没有对这篇文章做过操作
                            if (is_up) { // 应该用 is_up 来判断是 点赞 还是反对
                                var val = parseInt($("#digg_count").text());  // 原先的点赞数; $("#digg_count").text()是 字符串 格式,加1之前需要先转化为 数字 类型,parseInt()
                                $("#digg_count").text(val + 1);  // js 弱类型语言
                            } else {
                                var val = parseInt($("#bury_count").text());
                                $("#bury_count").text(val + 1);
                            }
    
                        } else { // 该用户已经对这篇文章做过操作
                            if (data.handled) {  // 已经点过赞
                                $("#digg_tips").html("您已经推荐过!")
                            } else {
                                $("#digg_tips").html("您已经反对过!")
                            }
                        }
                    }
                })
            });
            {# 不管是点赞还是评论,当前点赞(评论)人都为当前登陆人 #}
            {# urlencoded 的请求头(编码) 发送过去的数据是 字符串 格式 #}
    
            {# 评论请求 #}
            var pid = "";  // 父评论id默认为空(全局变量)
            $(".comment_btn").click(function () {
    
                var content = $("#comment_content").val();
    
                {#判断pid是否为空;如果不为空说明是子评论,评论内容需要从第一个换行符处截取#}
                if (pid) {
                    var index = content.indexOf("
    ");
                    content = content.slice(index + 1);
                    // slice() 方法可从已有的数组中返回选定的元素。
                    // slice()方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。
                    // 语法: arrayObject.slice(start,end)
                }
    
                $.ajax({
                    url: "/comment/",
                    type: "post",
                    data: {
                        "csrfmiddlewaretoken": $("[name='csrfmiddlewaretoken']").val(),
                        "article_id": "{{ article_obj.pk }}",
                        "content": content,
                        "pid": pid
                    },
                    success: function (data) {
                        {#利用ajax渲染页面#}
                        var create_time = data.create_time;
                        var username = data.username;
                        var content = data.content;
    
                        {# 反引号 ``是ES6的语法,ES6能利用 ${变量名} 的方法把变量嵌入到 字符串中;如果利用 js 就只能利用 + 拼接 #}
                        {# 将Ajax返回回来的数据 插入到下面的标签中 #}
                        var str = `
                    <li class="list-group-item">
                        <div>
                            <span>${create_time}</span>  &nbsp;&nbsp;
                            <a href="/{username}/">${username}</a>
                        </div>
                        <div class="comment_con">
                           <p>${content}</p>
                        </div>
                    </li>
                    `;
    
                        {#判断该评论是否为子评论#}
                        if (pid) {
                            var parent_username = data.parent_username;
                            var parent_content = data.parent_content;
    
                            var str = `
                    <li class="list-group-item">
                        <div>
                            <span>${create_time}</span>  &nbsp;&nbsp;
                            <a href="/{username}/">${username}</a>
                        </div>
                        <div class="pid_info well">
                                <p>
                                    ${parent_username}:${parent_content}
                                </p>
                            </div>
                        <div class="comment_con">
                           <p>${content}</p>
                        </div>
                    </li>
                    `;
    
                        }
    
                        {# 将上面的 li 标签插入到评论评论列表的 ul 中 #}
                        $("ul.comment_list").append(str);
    
                        {#提交完评论后需要把pid改成默认的空值#}
                        pid = "";
                        {#清空评论框#}
                        $("#comment_content").val("")
                    }
                })
            })
    
            {# 回复按钮事件(子评论) #}
            $(".reply_btn").click(function () {
                $("#comment_content").focus(); // .focus() 表示获取焦点
                var val = "@" + $(this).attr("username") + "
    ";  // $(this).attr("username") 表示 获取该DOM元素的 username这个自定义属性和属性值
                $("#comment_content").val(val);
    
                {#    点击“回复”按钮,pid要变成 父评论的主键值 #}
                pid = $(this).attr("comment_pk");
            })
    
        </script>
    {% endblock %}

    templates/backend/base.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>博客后台管理</title>
        <link rel="stylesheet" href="/static/blog/bs/css/bootstrap.min.css">
        <script src="/static/js/jquery-3.3.1.js"></script>
        <script src="/static/blog/bs/js/bootstrap.min.js"></script>
        <link rel="stylesheet" href="/static/blog/css/backend.css">
    </head>
    <body>
    
    <div class="header">
        <div class="content">
            <p class="title">
                <a class="backend" href="/cn_backend/">后台管理</a>
                <a href="/{{ request.user.username }}/" class="home_site">个人首页</a>
            </p>
        </div>
    </div>
    
    <div class="container">
        <div class="row">
            <div class="col-md-3">
                <div class="panel panel-default">
                    <div class="panel-heading">操作</div>
                    <div class="panel-body">
                            <p><a href="/cn_backend/add_article/">添加文章</a></p>
                    </div>
            </div>
    
        </div>
    
            <div class="col-md-9">
                {% block content %}
    
                {% endblock %}
            </div>
    </div>
    
    </body>
    </html>

    templates/backend/backend.html

    {% extends "backend/base.html" %}
    
    {% block content %}
        <div class="article_list small">
            <table class="table table-hover table-striped">
                <thead>
                    <th>标题</th>
                    <th>评论数</th>
                    <th>点赞数</th>
                    <th>操作</th>
                    <th>操作</th>
                </thead>
                <tbody>
    
                    {% for article in article_list %}
                        <tr>
                            <td>{{ article.title }}</td>
                            <td>{{ article.comment_count }}</td>
                            <td>{{ article.up_count }}</td>
                            <td><a href="/article/{{ article.pk }}/edit/">编辑</a></td>
                            <td><a href="/article/{{ article.pk }}/delete/">删除</a></td>
                        </tr>
                    {% endfor %}
    
                </tbody>
            </table>
        </div>
    {% endblock %}

    templates/backend/add_article.html

    {% extends "backend/base.html" %}
    
    {% block content %}
        <form action="" method="post">
            {# cdrftoken 要写在 form 表单里面 #}
            {% csrf_token %}
            <div class="form-group">
                <label for="title">标题</label>
                <input type="text" name="article_title" class="form-control" id="title" placeholder="标题">
            </div>
            <div class="form-group">
                <label for="content">内容</label>
                <textarea class="form-control" name="article_content" id="content" cols="30" rows="10"></textarea>
            </div>
            <button type="submit" class="btn btn-default pull-right">提交</button>
        </form>
        <script src="/static/js/jquery-3.3.1.js"></script>
        <script charset="utf-8" src="/static/blog/kindeditor/kindeditor-all.js"></script>
    
        <script>
            KindEditor.ready(function (K) {
                window.editor = K.create('#content', {
                     "100%",
                    height: "200px",
                    items: [
                        'source', '|', 'undo', 'redo', '|', 'preview', 'print', 'template', 'code', 'cut', 'copy', 'paste',
                        'plainpaste', 'wordpaste', '|', 'justifyleft', 'justifycenter', 'justifyright',
                        'justifyfull', 'insertorderedlist', 'insertunorderedlist', 'indent', 'outdent', 'subscript',
                        'superscript', 'clearhtml', 'quickformat', 'selectall', '|', 'fullscreen', '/',
                        'formatblock', 'fontname', 'fontsize', '|', 'forecolor', 'hilitecolor', 'bold',
                        'italic', 'underline', 'strikethrough', 'lineheight', 'removeformat', '|', 'image', 'multiimage',
                        'flash', 'media', 'insertfile', 'table', 'hr', 'emoticons', 'baidumap', 'pagebreak',
                        'anchor', 'link', 'unlink', '|', 'about'
                    ],
                    uploadJson:"/upload/", // uploadJson对应的是一个路径
                    extraFileUploadParams:{
                        csrfmiddlewaretoken:$("[name=csrfmiddlewaretoken]").val()
                    },  // post请求,需要自己组装数据,所以要加上这个 key-value
                    filePostName:"upload_img"  // 所上传文件对应的 key
                });
            });
        </script>
    {% endblock %}

    templates/backend/edit_article.html

    {% extends "backend/base.html" %}
    
    {% block content %}
        <form action="" method="post">
            {# cdrftoken 要写在 form 表单里面 #}
            {% csrf_token %}
            <div class="form-group">
                <label for="title">标题</label>
                <input type="text" name="article_title" class="form-control" id="title" placeholder="标题" value="{{ edit_article_obj.title }}">
            </div>
            <div class="form-group">
                <label for="content">内容</label>
                <textarea class="form-control" name="article_content" id="content" cols="30" rows="10">{{ edit_article_obj.content }}</textarea>
            </div>
            <button type="submit" class="btn btn-default pull-right">提交</button>
        </form>
        <script src="/static/js/jquery-3.3.1.js"></script>
        <script charset="utf-8" src="/static/blog/kindeditor/kindeditor-all.js"></script>
        <script>
            KindEditor.ready(function (K) {
                window.editor = K.create('#content',{
                    uploadJson:"/upload/", // uploadJson对应的是一个路径
                    extraFileUploadParams:{
                        csrfmiddlewaretoken:$("[name='csrfmiddlewaretoken']").val()  // post请求,需要自己组装数据,所以要加上这个 key-value
                    },
                    filePostName:"upload_img"  // 所上传文件对应的 key
    
                });
            });
        </script>
    {% endblock %}

    static/css/home_site.css

    * {
        margin: 0;
        padding: 0;
    }
    
    .header {
        width: 100%;
        height: 60px;
        background-color: #369;
    }
    
    .header .title {
        font-size: 18px;
        font-weight: 100;
        line-height: 60px;
        color: white;
        margin-left: 15px;
        margin-top: -10px;
    }
    
    .backend {
        float: right;
        color: white;
        font-size: 14px;
        text-decoration: none;
        margin-right: 10px;
        margin-top: 10px;
    }
    
    .home_site{
        color: white;
        margin-top: 10px;
        text-decoration: none;
    }
    
    .pub_info {
        margin-top: 10px;
        color: grey;
    }
    
    .return_to_index{
        margin-top: 10px;
    }
    
    .article_list{
        margin-top: 50px;
    }

    static/css/article_detail.css

    #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/blog/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/blog/img/downdown.gif") no-repeat;
        text-align: center;
        cursor: pointer;
        margin-top: 2px;
        padding-top: 5px;
    }
    .clear {
        clear: both;
    }
    .diggword {
        margin-top: 5px;
        margin-left: 0;
        font-size: 12px;
        color: gray;
    }
    
    input.author {
        background-image: url("/static/blog/img/icon_form.gif");
        background-repeat: no-repeat;
        border: 1px solid #ccc;
        padding: 4px 4px 4px 30px;
        width: 300px;
        font-size: 13px;
        background-position: 3px -3px;
    }
    
    .comment_con{
        margin-top: 10px;
    }

    static/css/backend.css

    * {
        margin: 0;
        padding: 0;
    }
    
    .header {
        width: 100%;
        height: 60px;
        background-color: black;
        margin-bottom: 20px;
    }
    
    .header .title {
        font-size: 18px;
        font-weight: 100;
        line-height: 60px;
        color: white;
        margin-left: 15px;
        margin-top: -10px;
    }
    
    .backend {
        color: white;
        font-size: 20px;
        margin-top: 10px;
        text-decoration: none;
    }
    
    .home_site{
        float: right;
        color: white;
        font-size: 20px;
        line-height: 60px;
        text-decoration: none;
        margin-right: 10px;
        margin-top: 10px;
    }
  • 相关阅读:
    【PL/SQL练习】函数
    【PL/SQL练习】命名块: 存储过程、函数、触发器、包
    【PL/SQL练习】自定义异常
    织梦首页/列表页文章按权重排序
    织梦dede:channel取子栏目时重复显示同级栏目的解决方法
    织梦最新版后台一键更新网站、更新文档HTML卡死的解决方法
    虚拟主机二级域名(m.dedehtml.com)绑定到织梦子目录(m)做手机站
    织梦自定义表单数据校验不对的解决方法
    织梦自定义表单用js代替联动地区解决联动地区选择
    织梦自定义图片字段调用图片地址
  • 原文地址:https://www.cnblogs.com/neozheng/p/9280146.html
Copyright © 2020-2023  润新知