当前位置:   article > 正文

Flask项目实战——11—(帖子详情展示、后台帖子加精管理、前台评论功能实现、Flask分页功能)_flask admin 详情页分段美化

flask admin 详情页分段美化

1、帖子详情展示

前台蓝图文件:apps/front/views.py,创建帖子详情页的路由

# -*- encoding: utf-8 -*-
"""
@File    : views.py
@Time    : 2020/5/11 9:59
@Author  : chen
前台蓝图文件:apps/front/views.py
"""
# 前台的蓝图文件  类视图函数写在这里
from flask import (
    Blueprint,
    render_template,
    views,
    make_response,                  # make_response生成response对象,用于返回前端模板
    request,
    session,
    g,
)

# 导入图像验证码生成文件
from utils.captcha import Captcha

# 图形验证码image是二进制数据,需要转换成字节流才能使用
from io import BytesIO

# 将图形验证码保存到Redis         restful输出信息弹窗
from utils import redis_captcha, restful

# 验证码表单信息验证
from .forms import (
    SignupForm,               # 注册的Form表单信息收集
    SigninForm,               # 登录的Form表单信息收集
    AddPostForm,              # 帖子提交表单信息
)

# 导入前台用户模型
from .models import (
    Front_User,
    PostModel,
)

# 导入数据库连接 db
from exts import db

# 确保URL安全的文件:utils/safe_url.py
from utils import safe_url

from apps.cms.models import (
    BannerModel,                      # 导入后台轮播图模型BannerModel
    BoardModel,                       # 导入后台板块管理模型
)
# 导入前台界面权限验证装饰器
from .decorators import login_required

front_bp = Blueprint("front", __name__)          # 前端不用前缀,直接在首页显示,front是蓝图,在front_signup.html调用生成图形验证码时候需要用

# 权限验证  需要在front_bp产生后,再导入
from .hooks import before_request


# BBS的首页界面路由
@front_bp.route("/")
def index():
    banners = BannerModel.query.order_by(BannerModel.priority.desc()).limit(4)   # 通过权重查询,每页显示4条
    boards = BoardModel.query.all()                                              # 查询板块中的所有
    board_id = request.args.get('board_id', type=int, default=None)              # get方法需要使用args,注意这里的数据类型需要改成int
    posts = PostModel.query.all()                                                # 帖子信息传输
    context = {                                                                  # 多种数据传输到前台界面
        "banners": banners,
        "boards": boards,
        "current_board_id": board_id,
        "posts": posts,
    }
    
    return render_template("front/front_index.html", **context)            # 渲染到首页界面,查询数据传输到前台界面


# 图形验证码路由
@front_bp.route("/captcha/")
def graph_captcha():
    try:                                                 # 异常处理
        # 图像验证码生成文件中返回两个参数   text, image
        text, image = Captcha.gene_graph_captcha()      # 生成图形验证码,image是二进制数据,需要转换成字节流才能使用
        print("发送的图形验证码是:{}".format(text))
        
        # 将图形验证码保存到Redis数据库中
        redis_captcha.redis_set(text.lower(), text.lower())  # redis_set中需要传参key和value,text没有唯一对应的key,只能都传参text
        
        # BytesIO是生成的字节流
        out = BytesIO()
        image.save(out, 'png')                          # 把图片image保存在字节流中,并指定为png格式
        # 文件流指针
        out.seek(0)                                     # 从字节流最初开始读取
        # 生成response对象,用于返回前端模板中
        resp = make_response(out.read())
        resp.content_type = 'image/png'                 # 指定数据类型
    except:
        return graph_captcha()                          # 没有生成验证码就再调用一次
        
    return resp                                         # 返回对象


# 测试referrer的跳转
@front_bp.route("/test/")
def test():
    return render_template("front/front_test.html")


# 用户注册类视图
class SingupView(views.MethodView):
    def get(self):
        # 图像验证码生成文件中返回两个参数   text, image
        # text, image = Captcha.gene_graph_captcha()
        # print(text)                      # 验证码
        # print(image)                     # 图形文件,图形类<PIL.Image.Image image mode=RGBA size=100x30 at 0x1EFC9000C88>

        # 从当前页面跳转过来就是None   从其他页面跳转过来输出就是上一个页面信息     referrer是页面的跳转
        # print(request.referrer)                           # http://127.0.0.1:9999/test/
        
        return_to = request.referrer
        # 确保URL安全的文件:utils/safe_url.py
        print(safe_url.is_safe_url(return_to))              # 判断return_to是否来自站内,是否是安全url,防爬虫
        
        if return_to and return_to != request.url and safe_url.is_safe_url(return_to):       # 跳转的url不能是当前页面,request.url是当前的url地址
            return render_template("front/front_signup.html", return_to=return_to)           # return_to渲染到前端界面
        else:
            return render_template("front/front_signup.html")                                # 如果没获取url,直接渲染注册界面
        
    # 验证码的form表单信息提交验证
    def post(self):
        form = SignupForm(request.form)                       # 收集表单信息
        
        # 表单验证通过
        if form.validate():
            # 保存到数据库
            telephone = form.telephone.data
            username = form.username.data
            password = form.password1.data                    # forms表单信息
            
            # 前台用户模型数据添加到数据库
            user = Front_User(telephone=telephone, username=username, password=password)
            db.session.add(user)
            db.session.commit()                                                   # 提交到数据库
            
            # 表单验证通过,提交到数据库成功
            return restful.success()
        else:
            return restful.params_error(message=form.get_error())                  # 表单信息验证出错


# 用户登录的类视图
class SinginView(views.MethodView):
    def get(self):
        return_to = request.referrer                                                    # referrer是上一个url
    
        if return_to and return_to != request.url and safe_url.is_safe_url(return_to):  # 跳转的url不能是当前页面,判断url是否安全
            return render_template("front/front_signin.html", return_to=return_to)      # return_to渲染到前端界面
        else:
            return render_template("front/front_signin.html")                           # 如果没获取url,直接渲染注册界面
    
    def post(self):
        form = SigninForm(request.form)                                            # 登录界面的Form表单信息
        
        if form.validate():                                                        # 表单信息存在
            # 收集form表单信息
            telephone = form.telephone.data
            password = form.password.data
            remember = form.remember.data
            
            user = Front_User.query.filter_by(telephone=telephone).first()         # 通过手机号验证该用户是否存在数据库
            if user and user.check_password(password):                             # 判断密码和用户是否正确
                # 'front_user_id'命名防止与后台验证session相同,会产生覆盖情况bug
                session['front_user_id'] = user.id                                 # 用户的id存储到session中,用于登录验证
                if remember:                                                       # 如果remember状态是1
                    # session持久化
                    session.permanent = True
                return restful.success()                                           # 成功
            else:
                return restful.params_error(message="手机号或者密码错误")           # 密码是、用户不正确
        else:
            return restful.params_error(message=form.get_error())                  # 表单信息不存在,输出异常信息
        

#  帖子编辑提交  的类视图     富文本编辑
class PostView(views.MethodView):
    # 登录验证,实现帖子编辑前进行权限验证
    decorators = [login_required]
    # 表单信息收集,传输
    def get(self):
        # 查询boards数据进行传输
        boards = BoardModel.query.all()                                           # boards是list类型
        return render_template("front/front_apost.html", boards=boards)           # boards数据传输到前端front_apost.html页面
    
    # 帖子的Form表单信息收集查询
    def post(self):
        form = AddPostForm(request.form)                     # 查询帖子提交的Form表单信息
        if form.validate():
            title = form.title.data
            board_id = form.board_id.data                    # 收集表单中提交的信息
            content = form.content.data
            
            # 查询用户信息是否在数据库中存在
            board = BoardModel.query.get(board_id)
            if not board:
                return restful.params_error(message="没有这个版块名称")           # 数据库中不存在,返回异常信息
            
            # 数据库中board信息存在,传输数据到数据库表中,并修改名称
            post = PostModel(title=title, board_id=board_id, content=content)
            post.board = board                                                   # 外键中的信息修改赋值
            post.author = g.front_user                                           # g对象

            db.session.add(post)
            db.session.commit()
            return restful.success()                                             # 提交成功,为json数据
        else:
            return restful.params_error(message=form.get_error())
            

# 前台 帖子详情 路由
@front_bp.route("/p/<post_id>")                  # 蹄子详情路由需要传参帖子id:post_id
def post_detail(post_id):
    post = PostModel.query.get(post_id)          # 通过post_id查找数据库中的帖子信息
    if not post:
        return restful.params_error(message="帖子不存在!")
    
    return render_template("front/front_detail.html", post=post)             # 查找到帖子信息,传输数据到帖子详情页渲染
    

# 绑定类视图的路由
front_bp.add_url_rule("/signup/", view_func=SingupView.as_view("signup"))          # "signup"视图中不需要反斜线,决定了url_for的路由地址
front_bp.add_url_rule("/signin/", view_func=SinginView.as_view("signin"))          # "signin"视图中不需要反斜线
front_bp.add_url_rule("/apost/", view_func=PostView.as_view("apost"))              # 绑定帖子编辑提交路由

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232

创建 帖子详情页面文件:templates/front/front_detail.html

<!-- 帖子详情页面文件:templates/front/front_detail.html  -->
{% extends 'front/front_base.html' %}

{% block title %}帖子详情{% endblock %}

{% block head %}
<!-- 百度的富文本编辑器加载 -->
<script src="{{ url_for('static', filename='ueditor/ueditor.config.js') }}"></script>
<script src="{{ url_for('static', filename='ueditor/ueditor.all.min.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='front/css/front_pdetail.css') }}">
<script src="{{ url_for('static', filename='front/js/front_pdetail.js') }}"></script>
{% endblock %}

{% block main_content %}
<div class="main-container">
    <div class="lg-container">
        <div class="post-container">
            <!--      帖子标题,前台蓝图文件:apps/front/views.py中的路由定义中传输过来      -->
            <h2>{{ post.title }}</h2>
            <p class="post-info-group">
                <span>发表时间:{{ post.create_time }}</span>
                <!--  author和board这两个字段是PostModel的外键,关联了Front_User和BoardModel模型中的username、name字段       -->
                <span>作者:{{ post.author.username }}</span>
                <span>所属板块:{{ post.board.name }}</span>
                <span>阅读数:0</span>
                <span>评论数:0</span>
            </p>
            <article class="post-content" id="post-content" data-id="{{ post.id }}">
                <!--   safe用于转义成安全字符串,content_html才能在页面渲染出标签的效果,content中包含有标签内容         -->
                {{ post.content_html|safe }}
            </article>
        </div>
        <div class="comment-group">
            <h3>评论列表</h3>
            <ul class="comment-list-group">

                {% for comment in post.comments %}

                <li>
                    <div class="avatar-group">
                        <img src="{{ url_for('static', filename='common/images/logo.png') }}"
                             alt="">
                    </div>
                    <div class="comment-content">
                        <p class="author-info">
                            <span>{{ comment.author.username }}</span>
                            <span>{{ comment.create_time }}</span>
                        </p>
                        <p class="comment-txt">
                            {{ comment.content|safe }}
                        </p>
                    </div>
                </li>
                {% endfor %}

            </ul>
        </div>
        <div class="add-comment-group">
            <h3>发表评论</h3>
            <!--   这是绑定front_pdetail.js中的百度文本编辑器的id="editor",这里的标签是 script       -->
            <script id="editor" type="text/plain" style="height:100px;"></script>

            <div class="comment-btn-group">
                <button class="btn btn-primary" id="comment-btn">发表评论</button>
            </div>
        </div>
    </div>

    <div class="sm-container"></div>
</div>
{% endblock %}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

创建 前台帖子详情页面样式js文件:static/front/js/front_pdetail.js

/**
 * // 前台帖子详情页面样式js文件:static/front/js/front_pdetail.js
 */
var lgajax = {
    'get':function(args) {
        args['method'] = 'get';
        this.ajax(args);
    },
    'post':function(args) {
        args['method'] = 'post';
        this.ajax(args);
    },
    'ajax':function(args) {
        // 设置csrftoken
        this._ajaxSetup();
        $.ajax(args);
    },
    '_ajaxSetup': function() {
        $.ajaxSetup({
            'beforeSend':function(xhr,settings) {
                if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
                    var csrftoken = $('meta[name=csrf-token]').attr('content');
                    xhr.setRequestHeader("X-CSRFToken", csrftoken)
                }
            }
        });
    }
};

// 初始化 百度文本编辑器
$(function(){
    var ue = UE.getEditor("editor", {
        "serverUrl": "/ueditor/upload",               // 图片上传路径
       toolbars: [                                    // 复制http://fex.baidu.com/ueditor/#start-toolbar中的多行列表代码
        ['fullscreen', 'source', 'undo', 'redo'],
        ['bold', 'italic', 'underline', 'fontborder', 'strikethrough', 'superscript', 'subscript', 'removeformat',
         'formatmatch', 'autotypeset', 'blockquote', 'pasteplain', '|', 'forecolor', 'backcolor', 'insertorderedlist',
         'insertunorderedlist', 'selectall', 'cleardoc'],
]
    });
    window.ue = ue;
})

$(function () {
    $("#comment-btn").click(function (event) {
        event.preventDefault();

//        var content = $("#comment").val();
        var content = window.ue.getContent();
        var post_id = $("#post-content").attr("data-id");
        lgajax.post({
            'url': '/acomment/',
            'data':{
                'content': content,
                'post_id': post_id
            },
            'success': function (data) {
                if(data['code'] == 200){
                    window.location.reload();
                }else{
                    lgalert.alertInfo(data['message']);
                }
            }
        });
//        }
    });
});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

创建 前台帖子详情页面样式文件:static/front/css/front_pdetail.css

/**
 * // 前台帖子详情页面样式文件:static/front/css/front_pdetail.css
 */
.post-container{
    border: 1px solid #e6e6e6;
    padding: 10px;
}

.post-info-group{
    font-size: 12px;
    color: #8c8c8c;
    border-bottom: 1px solid #e6e6e6;
    margin-top: 20px;
    padding-bottom: 10px;
}

.post-info-group span{
    margin-right: 20px;
}

.post-content{
    margin-top: 20px;
}

.post-content img{
    max-width: 100%;
}

.comment-group{
    margin-top: 20px;
    border: 1px solid #e8e8e8;
    padding: 10px;
}

.add-comment-group{
    margin-top: 20px;
    padding: 10px;
    border: 1px solid #e8e8e8;
}

.add-comment-group h3{
    margin-bottom: 10px;
}

.comment-btn-group{
    margin-top: 10px;
    text-align:right;
}

.comment-list-group li{
    overflow: hidden;
    padding: 10px 0;
    border-bottom: 1px solid #e8e8e8;
}

.avatar-group{
    float: left;
}

.avatar-group img{
    width: 50px;
    height: 50px;
    border-radius: 50%;
}

.comment-content{
    float: left;
    margin-left:10px;
}

.comment-content .author-info{
    font-size: 12px;
    color: #8c8c8c;
}

.author-info span{
    margin-right: 10px;
}

.comment-content .comment-txt{
    margin-top: 10px;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

前台首页页面文件:templates/front/front_index.html,关联帖子详情页的路由,而且需要注意的是:url_for反转需要写的是路由的函数名,传参的name和value需要和js文件绑定。

<!-- 前台首页页面文件:templates/front/front_index.html  -->

{% extends 'front/front_base.html' %}

{% block title %}
首页
{% endblock %}

<!-- 模板继承 -->
{% block main_content %}
<!--   居中样式  -->
<div class="main-container">
    <div class="lg-container">
        <!-- bootstrop中复制来的轮播图  -->
        <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
            <!-- 指令 -->
            <ol class="carousel-indicators">
                <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
                <li data-target="#carousel-example-generic" data-slide-to="1"></li>
                <li data-target="#carousel-example-generic" data-slide-to="2"></li>
            </ol>

            <!-- 轮播图 -->
            <div class="carousel-inner" role="listbox">
                <!--    循环apps/front/views.py文件传输的banners数据      -->
                {% for banner in banners %}
                <!--    判断是否第一次循环      -->
                {% if loop.first %}
                <div class="item active">
                    {% else %}
                    <div class="item">
                        {% endif %}
                        <!--    轮播图路径,style="width: 300px;height: 300px"轮播图大小 -->
                        <img src="{{ banner.image_url }}" alt="..." style="width: 300px;height: 300px">
                        <div class="carousel-caption">
                        </div>
                    </div>
                    {% endfor %}
  
                </div>

                <!-- 轮播图左右切换按钮 -->
                <a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
                    <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
                    <span class="sr-only">Previous</span>
                </a>
                <a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
                    <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
                    <span class="sr-only">Next</span>
                </a>
            </div>
            <!-- bootstrop中复制来的轮播图 代码结束   -->

            <!--   帖子排序方式     -->
            <div class="post-group">
                <ul class="post-group-head">
                    <li class=""><a href="#">最新</a></li>
                    <li class=""><a href="#">精华帖子</a></li>
                    <li class=""><a href="#">点赞最多</a></li>
                    <li class=""><a href="#">评论最多</a></li>
                </ul>
                <ul class="post-list-group">
                    <!--         循环帖子信息,首页渲染           -->
                    {% for post in posts %}
                        <li>
                        <div class="author-avatar-group">
                            <img src="#" alt="">
                        </div>
                        <div class="post-info-group">
                            <p class="post-title">
                                <!--  front.post_detail反转需要写的是路由的函数名,post_id=post.id传输帖子id,post_id是名字,post.id是value      -->
                                <a href="{{ url_for('front.post_detail', post_id=post.id) }}">{{ post.title }}</a>
                                <span class="label label-danger">精华帖</span>
                            </p>
                            <p class="post-info">
                                <!-- post模型中的author外键调用Front_User中的username信息  -->
                                <span>作者:{{ post.author.username }}</span>
                                <span>发表时间:{{ post.create_time }}</span>
                                <span>评论:0</span>
                                <span>阅读:0</span>
                            </p>
                        </div>
                    </li>
                    {% endfor %}

                </ul>
                <div style="text-align:center;">
                </div>
            </div>
        </div>

        <!--      帖子标签内容      -->
        <div class="sm-container">
            <div style="padding-bottom:10px;">
                <!--       重定向到/apost/路由,文本编辑界面        -->
                <a href="{{ url_for('front.apost') }}" class="btn btn-warning btn-block">发布帖子</a>
            </div>
            <div class="list-group">
                <a href="/" class="list-group-item active">所有板块</a>
                <!--     循环显示前台蓝图文件:apps/front/views.py中传输的数据**context           -->
                {% for board in boards %}
                    <!--         注意这里的current_board_id数据类型是int,才能与board.id相比较           -->
                    {% if current_board_id == board.id %}
                        <!--    url_for('front.index', board_id=board.id)每次点击跳转到front_index.html页面,即当前界面,且传输给一个board_id的参数值,由board.id赋值            -->
                        <a href="{{ url_for('front.index', board_id=board.id ) }}" class="list-group-item active">{{ board.name }}</a>
                    {% else %}
                        <!--  没被选中,即没有被传输相同的board.id,图标样式是class="list-group-item">    -->
                        <a href="{{ url_for('front.index', board_id=board.id ) }}" class="list-group-item">{{ board.name }}</a>
                    {% endif %}
                {% endfor %}
            </div>
        </div>
    </div>
    <!--  居中样式  -->
{% endblock %}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115

2、后台帖子加精管理

后台模型文件:apps/cms/models.py, 创建精华帖子模型

# -*- encoding: utf-8 -*-
"""
@File    : models.py
@Time    : 2020/5/11 10:00
@Author  : chen
后台模型文件:apps/cms/models.py
"""
# 定义后端用户模型
from exts import db                                                               # 数据库
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash         # 导入密码加密,解密方法的库


# 权限定义,不是模型,没有继承db.Model
class CMSPersmission(object):
    # 255 二进制表示所有的权限
    ALL_PERMISSION = 0b11111111          # 每一位数代表一个权限,共7个权限,8位1个字节
    
    # 访问权限
    VISITOR        = 0b00000001
    
    # 管理帖子
    POSTER         = 0b00000010
    
    # 管理评论
    COMMENTER      = 0b00000100
    
    # 管理板块
    BOARDER        = 0b00001000
    
    # 管理后台用户
    CMSUSER        = 0b00010000
    # 管理前台用户
    FRONTUSER      = 0b00100000
    # 管理管理员用户
    ADMINER        = 0b01000000


# 权限与角色是多对多的关系,创建他们的中间表
cms_role_user = db.Table(
    "cms_role_user",
    db.Column("cms_role_id", db.Integer, db.ForeignKey('cms_role.id'), primary_key=True),
    db.Column("cms_user_id", db.Integer, db.ForeignKey('cms_user.id'), primary_key=True),
)


# 角色模型定义   继承了db.Model
class CMSRole(db.Model):
    __tablename__ = 'cms_role'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)      # 主键  自增
    name = db.Column(db.String(50), nullable=False)                       # 非空
    desc = db.Column(db.String(250), nullable=False)                      # 非空
    creat_time = db.Column(db.DateTime, default=datetime.now)
    permission = db.Column(db.Integer, default=CMSPersmission.VISITOR)    # 默认先给游客权限

    # 反向查询属性,关联中间表secondary=cms_role_user,对应了CMS_User模型,建立模型联系,不映射到数据库中
    users = db.relationship('CMS_User', secondary=cms_role_user, backref="roles")    # roles是CMS_User的外键
    
    
# 后台用户模型定义
class CMS_User(db.Model):
    __tablename__ = 'cms_user'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)           # 主键  自增
    username = db.Column(db.String(150), nullable=False)                       # 非空
    # password = db.Column(db.String(150), nullable=False)
    _password = db.Column(db.String(150), nullable=False)                      # 密码加密操作修改字段
    email = db.Column(db.String(50), nullable=False, unique=True)              # 非空、唯一
    join_time = db.Column(db.DateTime, default=datetime.now)                   # 默认当前时间
    
    # 修改密码加密操作中的字段,在manage.py映射数据库时候,使用字段还是保持相同
    def __init__(self, username, password, email):
        self.username = username
        self.password = password         # 调用该方法 返回下面的self._password数值,
        self.email = email
    
    # 密码加密操作
    @property
    def password(self):                   # 密码取值
        return self._password

    @password.setter                      # 密码加密
    def password(self, raw_password):
        self._password = generate_password_hash(raw_password)

    # 用于验证后台登录密码是否和数据库一致,raw_password是后台登录输入的密码
    def check_password(self, raw_password):
        result = check_password_hash(self.password, raw_password)   # 相当于用相同的hash加密算法加密raw_password,检测与数据库中是否一致
        return result
    
    # 封装用户的权限
    @property
    def permission(self):
        if not self.roles:           # 反向查询属性,backref="roles",
            return 0                 # 没有任何权限
        
        # 所有权限
        all_permissions = 0
        
        for role in self.roles:                    # 循环调用所有角色
            permissions = role.permission         # 将这个角色的权限都取出来  role.permission代表CMSRole中的属性
            all_permissions |= permissions         # 当前这个角色的权限都在all_permissions
            
        return all_permissions
    
    # 判断用户所具有的权限
    def has_permissions(self, permission):
        all_permissions = self.permission                 # 调用permission(self)方法
        #  若所有权限0b11111111 & 用户权限     等于 本身,则代表具有该权限
        result = all_permissions & permission == permission
        # print(result)
        return result
        
    # 判断是否是开发人员
    @property
    def is_developer(self):
         return self.has_permissions(CMSPersmission.ALL_PERMISSION)       # 调用has_permissions方法并传入所有权限
         
 
# 轮播图的模型创建
class BannerModel(db.Model):
    __tablename__ = 'banner'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键  自增
    name = db.Column(db.String(250), nullable=False)                  # 非空
    # 图片链接
    image_url = db.Column(db.String(250), nullable=False)             # 轮播图的链接资源
    # 跳转链接
    link_url = db.Column(db.String(50), nullable=False)
    priority = db.Column(db.Integer, default=0)                       # 权重选项
    create_time = db.Column(db.DateTime, default=datetime.now)        # 创建时间
    
    # 删除标志字段    0代表删除  1代表未删除
    is_delete = db.Column(db.Integer, default=1)
    
    
# 板块管理模型创建
class BoardModel(db.Model):
    __tablename__ = 'cms_board'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键  自增
    name = db.Column(db.String(250), nullable=False)                  # 非空
    create_time = db.Column(db.DateTime, default=datetime.now)        # 创建时间
    

# 精华帖子模型 创建,在这个表中的帖子post_id都是精华帖子
class HighlightPostModel(db.Model):
    __tablename__ = 'highlight_post'
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)  # 主键  自增
    post_id = db.Column(db.Integer, db.ForeignKey("post.id"))         # 外键
    create_time = db.Column(db.DateTime, default=datetime.now)          # 创建时间
    # 反转属性
    post = db.relationship("PostModel", backref='highlight')


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157

映射模型到数据库中文件: manage.py,导入后台帖子加精模型

"""
映射模型到数据库中文件: manage.py
"""
# 导入后台模型 才能映射到数据库 ,导入轮播图和文章的管理模块
from apps.cms.models import (
    BannerModel,
    BoardModel,
    HighlightPostModel,               # 后台帖子加精模型
    )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

加精帖子模型映射到数据库:
在这里插入图片描述
视图文件:apps/cms/views.py文件,创建帖子加精和取消加精的路由地址、方法,发送posts的数据信息参数,用于渲染到后端html页面。

注意:视图函数必须要有返回值,否则报错:ValueError: View function did not return a response;和 TypeError: The view function did not return a valid response. The function either returned…

# -*- encoding: utf-8 -*-
"""
@File    : views.py
@Time    : 2020/5/11 9:59
@Author  : chen
视图文件:apps/cms/views.py文件
"""
# 蓝图文件:实现模块化应用,应用可以分解成一系列的蓝图   后端的类视图函数写在这个文件
from flask import (
    request, redirect, url_for,                      # 页面跳转redirect   request请求收集
    Blueprint, render_template, views, session,      # 定义类视图,显示模板文件
    jsonify, g                                       # jsonify强制转换成json数据
)
from exts import db, mail                            # 数据库中更新密码、邮箱等使用

from apps.cms.forms import (
    LoginForm, ResetPwdForm,                        # ResetPwdForm修改密码的form信息
    ResetEmailForm,                                 # 导入forms.py文件中的邮箱验证的表单信息类
    AddBannerForm,                                  # 导入 添加轮播图 的表单信息
    UpdateBannerForm,                               # 导入 更新轮播图 的表单信息
    AddBoardsForm,                                  # 导入 增加板块管理 的表单信息
    UpdateBoardsForm,                               # 导入 编辑板块管理 的表单信息
)

from apps.cms.models import (
    CMS_User,                                       # 后台用户模型
    CMSPersmission,                                 # CMSPersmission验证用户不同模块权限
    CMSRole,                                        # 用户角色模型
    BannerModel,                                    # 导入 轮播图模型BannerModel
    BoardModel,                                     # 导入 板块管理模型
    HighlightPostModel,                             # 帖子加精模型
)
# 导入 帖子 模型文件
from apps.front.models import PostModel

from .decorators import permission_required            # 传参装饰器验证用户不同模块权限

# 导入装饰器:判断当前界面是否是登录界面,不是就将url重定向到登录界面,一般不用,使用的主要是钩子函数
from .decorators import login_required

# 导入restful.py中的访问网页状态码的函数          redis_captcha:redis存储、提取、删除验证码功能
from utils import restful, random_captcha, redis_captcha           # 随机生成验证码函数random_captcha()

# 导入flask-mail中的Message
from flask_mail import Message

cms_bp = Blueprint("cms", __name__, url_prefix='/cms/')     # URL前缀url_prefix

# 钩子函数是在cms_bp创建之后才创建的,顺序在cms_bp创建之后
from .hooks import before_request


@cms_bp.route("/")                                          # 后台界面
# @login_required             # 装饰器判定当前界面是否是登录界面,但是需要每个路由函数都要加该装饰器,比较麻烦,推荐使用钩子函数
def index():
    # return "cms index:后端类视图文件"
    return render_template('cms/cms_index.html')  # 登陆之后进入CMS后台管理界面


# 用户注销登录
@cms_bp.route("/logout/")                              # 需要关联到cms/cms_index.html中的注销属性
def logout():
    # session清除user_id
    del session['user_id']
    # 重定向到登录界面
    return redirect(url_for('cms.login'))             # 重定向(redirec)为把url变为重定向的url


# 定义个人中心的路由
@cms_bp.route("/profile/")
def profile():
    return render_template("cms/cms_profile.html")   # 模板渲染(render_template)则不会改变url,模板渲染是用模板来渲染请求的url


# 定义类视图,显示模板文件   用户登录功能实现
class LoginView(views.MethodView):
    def get(self, message=None):                                         # message=None时候不传输信息到cms_login.html页面
        return render_template("cms/cms_login.html", message=message)    # 针对post方法中同样要返回到cms_login.html页面进行代码简化
    
    # 用户登录操作验证
    def post(self):
        # 收集表单信息
        login_form = LoginForm(request.form)
        if login_form.validate():
            # 数据库验证
            email = login_form.email.data
            password = login_form.password.data
            remember = login_form.remember.data
            
            # 查询数据库中的用户信息
            user = CMS_User.query.filter_by(email=email).first()    # 邮箱唯一,用于查询验证用户
            if user and user.check_password(password):              # 验证用户和密码是否都正确
                session['user_id'] = user.id                        # 查询到用户数据时,保存session的id到浏览器
                # session['user_name'] = user.username                # 将数据库中的user.username保存到session中,在hooks.py中判断
                # session['user_email'] = user.email                  # 将数据库中的email保存到session中,方便html调用信息
                # session['user_join_time'] = user.join_time          # 将数据库中的join_time保存到session中,方便html调用信息
                
                if remember:                                        # 如果用户点击了remember选择,在浏览器中进行数据持久化
                    session.permanent = True                        # 数据持久化,默认31天,需要设置session_key在config.py中
            
                # 登录成功,跳转到后台首页
                return redirect(url_for('cms.index'))               # 在蓝图中必须加cms   跳转到index方法
            else:
                # return "邮箱或密码错误"                              # 登录出错,返回结果
                # return render_template("cms/cms_login.html", message="邮箱或密码错误")  # 登录出错,返回结果渲染到cms_login.html页面
                return self.get(message="邮箱或密码错误")             # 传参到get方法中,多加一个传输错误信息的参数到方法中
        else:
            # print(login_form.errors)                                 # forms.py中的错误信息  字典类型数据
            # print(login_form.errors.popitem())                       # forms.py中的错误信息  元祖类型数据
            # return "表单验证错误"                                     # 错误信息需要渲染到cms_login.html页面
            # return self.get(message=login_form.errors.popitem()[1][0])  # 字典类型数据信息提取
            return self.get(message=login_form.get_error())            # login_form是收集到的表单信息,信息提取放置到forms.py的父类中实现
    
    
# 修改密码的类视图验证
class ResetPwd(views.MethodView):
    def get(self):
        return render_template('cms/cms_resetpwd.html')         # 模板渲染到cms_resetpwd.html
    
    # post提交密码修改
    def post(self):
        # 先审查旧密码是否与数据库中的信息相同
        form = ResetPwdForm(request.form)
        if form.validate():
            oldpwd = form.oldpwd.data
            newpwd = form.newpwd.data
            # 对象
            user = g.cms_user
            # 将用户输入的密码进行加密检测是否与数据库中的相同
            if user.check_password(oldpwd):
                # 更新我的密码  将新密码赋值,此时的新密码已经经过验证二次密码是否一致
                user.password = newpwd         # user.password已经调用了models.py中的 @property装饰器进行密码加密
                # 数据库更新
                db.session.commit()
                # return jsonify({"code": 400, "message": "密码修改成功"})        # 代码改写为下面
                return restful.success("密码修改成功")             # 调用restful.py中定义的访问网页成功的函数
            else:
                # 当前用户输入的旧密码与数据库中的不符
                # return jsonify({"code": 400, "message": "旧密码输入错误"})
                return restful.params_error(message="旧密码输入错误")      # 参数错误
        else:
            # ajax 需要返回一个json类型的数据
            # message = form.errors.popitem()[1][0]                     # 收集错误信息
            # return jsonify({"code": 400, "message": message})         # 将数据转换成json类型
            return restful.params_error(message=form.get_error())       # 参数错误,信息的收集在forms.py的父类函数中实现  form是收集到的信息
        

# 定义修改邮箱的类视图 验证
class ResetEmail(views.MethodView):
    def get(self):
        return render_template("cms/cms_resetemail.html")      # 返回到修改邮箱页面url
    
    def post(self):
        form = ResetEmailForm(request.form)                    # 接收邮箱验证的form表单信息
        if form.validate():                                    # 验证表单信息是否通过
            email = form.email.data                            # 获取form表单中填写的邮箱地址
            
            # 查询数据库
            # CMS_User.query.filter_by(email=email).first()
            # CMS_User.query.filter(CMS_User.email == email).first()
            g.cms_user.email = email                           # 数据库中的查询在apps/cms/hooks.py文件中确定了该用户的数据库信息,用全局对象g.cms_user修改邮箱
            db.session.commit()
            return restful.success()                           # 邮箱修改成功
        else:
            return restful.params_error(form.get_error())      # form是这个类中的所有表单信息
        
        
# 发送测试邮件进行验证
@cms_bp.route("/send_email/")
def send_mail():
    message = Message('邮件发送', recipients=['727506892@qq.com'], body='测试邮件发送')   # 主题:邮件发送;收件人:recipients;邮件内容:测试邮件发送
    mail.send(message)                   # 发送邮件
    return "邮件已发送"


# 邮件发送
class EmailCaptcha(views.MethodView):
    def get(self):                                  # 根据resetemail.js中的ajax方法来写函数,不需要post请求
        email = request.args.get('email')           # 查询email参数是否存在
        if not email:
            return restful.params_error('请传递邮箱参数')
        
        # 发送邮件,内容为一个验证码:4、6位数字英文组合
        captcha = random_captcha.get_random_captcha(4)            # 生成4位验证码
        message = Message('BBS论坛邮箱验证码', recipients=[email], body='您的验证码是:%s' % captcha)
        
        # 异常处理
        try:
            mail.send(message)
        except:
            return restful.server_error(message="服务器错误,邮件验证码未发送!")   # 发送异常,服务器错误
        
        # 验证码保存,一般有时效性,且频繁请求变化,所以保存在Redis中
        redis_captcha.redis_set(key=email, value=captcha)        # redis中都是键值对类型,存储验证码
        return restful.success("邮件验证码发送成功!")
    

# 轮播图管理路由
@cms_bp.route("/banners/")
def banners():
    # 通过模型中定义的权重priority的倒叙来排序
    banners = BannerModel.query.order_by(BannerModel.priority.desc()).all()
    return render_template("cms/cms_banners.html", banners=banners)           # 传输banners数据到cms_banners.html界面渲染


# 添加轮播图功能路由,且方法需要与static/cms/js/banners.js中绑定的方法POST相同
@cms_bp.route("/abanner/", methods=['POST'])
def abanner():
    form = AddBannerForm(request.form)                  # 接收添加轮播图的form表单信息
    if form.validate():
        name = form.name.data
        image_url = form.image_url.data
        link_url = form.link_url.data
        priority = form.priority.data
        
        banner = BannerModel(name=name, image_url=image_url, link_url=link_url, priority=priority)     # 轮播图模型
        db.session.add(banner)                                                                         # 提交数据库
        db.session.commit()
        return restful.success()                                                                       # 轮播图信息提交成功
    else:
        return restful.params_error(message=form.get_error())                                          # 表单信息错误


# 修改 轮播图 路由,方法与static/cms/js/banners.js中绑定的方法POST相同
@cms_bp.route("/ubanner/", methods=['POST'])
def ubanner():
    # 修改根据banner_id查询再修改
    form = UpdateBannerForm(request.form)             # 表单信息UpdateBannerForm中的request
    if form.validate():                                # 先查询页面表单信息是否存在
        banner_id = form.banner_id.data               # 收集用户输入的表单信息
        name = form.name.data
        image_url = form.image_url.data
        link_url = form.link_url.data
        priority = form.priority.data
        
        banner = BannerModel.query.get(banner_id)     # 通过轮播图的模型BannerModel的banner_id查询数据库中轮播图对象
        if banner:                                     # 再查询数据库对象数据是否存在
            banner.name = name                        # 将UpdateBannerForm中收集到的form信息命名给数据库中的banner对象
            banner.image_url = image_url
            banner.link_url = link_url
            banner.priority = priority
            
            db.session.commit()                       # 数据库信息直接提交修改即可,不用添加新的对象
            return restful.success()
        else:
            return restful.params_error(message=form.get_error())    # 表单信息错误
    

# 删除  轮播图路由,路由命名与banners.js绑定
@cms_bp.route("/dbanner/", methods=['POST'])
def dbanner():
    '''
    request.form.get("key", type=str, default=None)      获取表单数据
    request.args.get("key")                              获取get请求参数
    request.values.get("key")                            获取所有参数
    '''
    # 修改根据banner_id查询再修改,获取post请求参数         get请求方式使用request.args.get()
    banner_id = request.form.get('banner_id')            # 获取表单数据,这里没有单独创建删除的Form表单,使用之前创建的
    if not banner_id:
        return restful.params_error(message="轮播图不存在")
    
    banner = BannerModel.query.get(banner_id)           # 根据banner_id查询数据库
    if banner:
        db.session.delete(banner)                       # 删除该banner
        db.session.commit()
        return restful.success()                        # 返回成功
    else:
        return restful.params_error("轮播图不存在")      # 根据banner_id查询数据库信息不存在
    

# 帖子管理路由 ,需要和cms_base.js中命名的相同才可以
@cms_bp.route("/posts/")
@permission_required(CMSPersmission.POSTER)                # 传参装饰器验证不同用户不同模块权限
def posts():
    posts = PostModel.query.all()                          # 数据库查询帖子信息,进行传输到后端页面cms_posts.html渲染
    return render_template("cms/cms_posts.html", posts=posts)
    

# 帖子 加精的 后台管理,路由名称在static/cms/js/posts.js文件定义好了
@cms_bp.route("/hpost/", methods=["POST"])                # 方法确定为post方式,默认支持的是get方法
@permission_required(CMSPersmission.POSTER)                # 传参装饰器验证不同用户不同模块权限
def hposts():
    # 接收外键,post接收方式使用form
    post_id = request.form.get("post_id")                  # 接收post_id进行查询
    if not post_id:
        return restful.params_error(message="请输入帖子ID")
    
    post = PostModel.query.get(post_id)                    # 从帖子的数据表中查找该帖子对象
    if not post:
        return restful.params_error(message="没有这篇帖子")
    
    highlight = HighlightPostModel()                      # 创建模型
    highlight.post = post                                 # 外键关联,加精帖子补充到新的表中
    db.session.add(highlight)
    db.session.commit()                                   # 提交
 	return restful.success()                              # 加精成功,视图函数必须有返回值

# 帖子 取消加精的 后台管理,路由名称在static/cms/js/posts.js文件定义好了
@cms_bp.route("/uhpost/", methods=["POST"])               # 方法确定为post方式,默认支持的是get方法
@permission_required(CMSPersmission.POSTER)                # 传参装饰器验证不同用户不同模块权限
def uhposts():
    # 接收外键,post接收方式使用form
    post_id = request.form.get("post_id")                  # 接收post_id进行查询
    if not post_id:
        return restful.params_error(message="请输入帖子ID")
    
    post = PostModel.query.get(post_id)                     # 从帖子的数据表中查找该帖子对象
    if not post:
        return restful.params_error(message="没有这篇帖子")
    
    highlight = HighlightPostModel.query.filter_by(post_id=post_id).first()
    db.session.delete(highlight)
    db.session.commit()                                     # 提交
    return restful.success()                                # 视图函数必须有返回值


# 评论管理路由
@cms_bp.route("/comments/")
@permission_required(CMSPersmission.COMMENTER)             # 传参装饰器验证不同用户不同模块权限
def comments():
    return render_template("cms/cms_comments.html")


# 板块管理路由
@cms_bp.route("/boards/")
@permission_required(CMSPersmission.BOARDER)               # 传参装饰器验证不同用户不同模块权限
def boards():
    boards = BoardModel.query.all()                        # 数据库查询所有板块名称
    return render_template("cms/cms_boards.html", boards=boards)        # 数据渲染到cms_boards.html


# 增加 板块管理名称 路由,与static/cms/js/banners.js中绑定的方法、路由要相同
@cms_bp.route("/aboard/", methods=['POST'])
@permission_required(CMSPersmission.BOARDER)               # 传参装饰器验证不同用户不同模块权限
def aboards():
    form = AddBoardsForm(request.form)                     # 表单信息传输过来,方便修改调用
    if form.validate():
        name = form.name.data                              # 表单信息收集
        
        board = BoardModel(name=name)                      # 添加信息到板块模型中
        db.session.add(board)
        db.session.commit()                                # 提交数据库
        return restful.success()                           # 数据库添加成功
    else:
        return restful.params_error(message=form.get_error())    # 表单信息错误


# 编辑 板块管理名称 路由,与static/cms/js/banners.js中绑定的方法、路由要相同
@cms_bp.route("/uboard/", methods=['POST'])
@permission_required(CMSPersmission.BOARDER)             # 传参装饰器验证不同用户不同模块权限
def uboards():
    form = UpdateBoardsForm(request.form)                # 表单信息传输过来,方便修改调用
    if form.validate():
        board_id = form.board_id.data                    # 表单信息收集
        name = form.name.data
        
        board = BoardModel.query.get(board_id)           # 根据表单中提交的board_id查询数据库中对象信息
        if board:
            board.name = name                            # 表单中提交的name命名给数据库中对象的名字
            db.session.commit()                          # 修改数据后提交数据库
            return restful.success()                     # 数据库修改成功
        else:
            return restful.params_error(message="没有这个分类板块")  # 数据库中对象信息不存在
    else:
        return restful.params_error(message=form.get_error())  # 表单信息错误


# 删除 板块管理名称 路由,与static/cms/js/banners.js中绑定的方法、路由要相同
@cms_bp.route("/dboard/", methods=['POST'])
@permission_required(CMSPersmission.BOARDER)             # 传参装饰器验证不同用户不同模块权限
def dboards():
    board_id = request.form.get('board_id')                  # 查询表单信息中的board_id,这里没有单独创建删除的Form表单,使用之前创建的
    if not board_id:
        return restful.params_error(message="分类板块不存在")  # 表单信息不存在

    board = BoardModel.query.get(board_id)               # 根据表单中提交的board_id查询数据库中对象信息,注意.get
    if not board:
        return restful.params_error(message="分类板块不存在")  # 数据库中对象信息不存在
    
    db.session.delete(board)                             # 删除数据库中的信息
    db.session.commit()                                  # 提交数据库修改
    return restful.success()                             # 删除成功


# 前台用户管理路由
@cms_bp.route("/fusers/")
@permission_required(CMSPersmission.FRONTUSER)             # 传参装饰器验证不同用户不同模块权限
def fuser():
    return render_template("cms/cms_fuser.html")


# 后用户管理路由
@cms_bp.route("/cusers/")
@permission_required(CMSPersmission.CMSUSER)               # 传参装饰器验证不同用户不同模块权限
def cuser():
    return render_template("cms/cms_cuser.html")


# 添加登录路由
cms_bp.add_url_rule("/login/", view_func=LoginView.as_view('login'))    # view_func 命名操作名字,"/login/"路由地址

# 类视图函数添加绑定路由  注意类视图需要修改ResetPwd.as_view('resetpwd')
cms_bp.add_url_rule("/resetpwd/", view_func=ResetPwd.as_view('resetpwd'))  # view_func 命名操作名字,/resetpwd/路由地址

# 添加修改邮箱的类视图路由绑定,路由的命名和cms_base.js中的命名要相同,否则不关联,url=/resetemail/必须要和resetemail.js中的ajax绑定的路由相同
cms_bp.add_url_rule("/resetemail/", view_func=ResetEmail.as_view('resetemail'))

# 绑定路由,路由的命名和cms_base.js中的命名要相同,必须要和resetemail.js中的ajax绑定的路由相同
cms_bp.add_url_rule("/email_captcha/", view_func=EmailCaptcha.as_view('email_captcha'))

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410

后台管理帖子页面:templates/cms/cms_posts.html,接收视图文件:apps/cms/views.py文件传输过来的posts的数据信息,循环渲染到该html页面。

<!--  后台管理帖子页面:templates/cms/cms_posts.html  -->

<!--  继承模板文件cms/cms_base.html  简化代码 -->
{% extends 'cms/cms_base.html' %}

<!-- 页面标题 -->
{% block title %}
    帖子管理
{% endblock %}

{% block head %}
    <script src="{{ url_for('static', filename='cms/js/posts.js') }}"></script>
{% endblock %}

<!--  标题  -->
{% block page_title %}
    {{self.title()}}
{% endblock %}


{% block content %}
    <table class="table table-bordered">
        <thead>
            <tr>
                <th>标题</th>
                <th>发布时间</th>
                <th>板块</th>
                <th>作者</th>
                <th>操作</th>
            </tr>
        </thead>
        <tbody>
        <!--   循环渲染帖子信息,posts数据由views.py文件传输过来     -->
        {% for post in posts  %}
            <!--data-id="{{ post.id }}和data-highlight属性传输给posts.js文件进行判断,{{ 1 if post.highlight else 0 }}三元运算符:如果post.highlight存在显示为1    -->
            <tr data-id="{{ post.id }}" data-highlight="{{ 1 if post.highlight else 0 }}">
                <!--  href链接跳转到前台的帖子路由,调用该路由方法进行反转,并传输名称为post_id,value值为post.id的参数         -->
                <td><a target="_blank" href="{{ url_for('front.post_detail',post_id=post.id) }}">{{ post.title }}</a></td>
                <td>{{ post.create_time }}</td>
                <!--    post.board是PostModel中的board反向查询属性,name是BoardModel中的name字段    -->
                <td>{{ post.board.name }}</td>
                <!--    post.author是PostModel中的author反向查询属性,username是Front_User中的username字段            -->
                <td>{{ post.author.username }}</td>

                <td>
                    <!--   post数据由views.py文件传输过来,highlight是模型中的post的外键        -->
                    {% if post.highlight %}
                        <button class="btn btn-default btn-xs highlight-btn">取消加精</button>
                    {% else %}
                        <button class="btn btn-default btn-xs highlight-btn">加精</button>
                    {% endif %}
                    <button class="btn btn-danger btn-xs">移除</button>
                </td>
            </tr>
        {% endfor %}

        </tbody>
    </table>
{% endblock %}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

3、前台评论功能实现

前台模型文件 apps/front/models.py,创建CommentModel评论模型。

# -*- encoding: utf-8 -*-
"""
@File    : models.py
@Time    : 2020/5/11 10:00
@Author  : chen
前台模型文件 apps/front/models.py
"""
# 前台管理的模型
from exts import db                   # 数据库连接
import shortuuid                      # 前台用户id加密
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash         # 导入密码加密,解密方法的库
import enum                           # 导入枚举
from markdown import markdown         # 导入帖子编辑的markdown显示功能库
import bleach        # 导入帖子编辑的markdown显示功能库


# 性别选择的类
class GenderEnum(enum.Enum):
    MALE = 1
    FEMALE = 2
    SECRET = 3
    UNKNOW = 4


#   前台用户模型类
class Front_User(db.Model):
    __tablename__ = "front_user"
    # id 类型不用db.Integer类型,使用String是为了防止爆破,同时使用shortuuid进行加密
    id = db.Column(db.String(100), primary_key=True, default=shortuuid.uuid)
    telephone = db.Column(db.String(11), nullable=False, unique=True)             # 非空唯一
    username = db.Column(db.String(150), nullable=False)
    _password = db.Column(db.String(150), nullable=False)                         # 密码加密操作修改字段
    email = db.Column(db.String(50), unique=True)
    
    realname = db.Column(db.String(50))
    avatar = db.Column(db.String(150))                                            # 头像,二进制数据
    signatrue = db.Column(db.String(500))                                         # 签名
    gender = db.Column(db.Enum(GenderEnum), default=GenderEnum.UNKNOW)            # 性别枚举类,默认未知
    join_time = db.Column(db.DateTime, default=datetime.now)  # 默认当前时间
    
    # 修改密码加密操作,manage.py映射数据库时候,使用字段保持相同,由于字段太多,使用传参形式
    def __init__(self, *args, **kwargs):
        if 'password' in kwargs:                       # 如果传参中包含有password
            self.password = kwargs.get('password')     # 获取该参数值赋值给password
            kwargs.pop('password')                     # 模型参数中是_password,不是password,弹出
        
        # super(FrontUser, self).__init__(*args, **kwargs)   # python2的写法
        super().__init__(*args, **kwargs)

    # 密码加密操作
    @property
    def password(self):             # 密码取值
        return self._password

    @password.setter                # 密码加密
    def password(self, raw_password):
        self._password = generate_password_hash(raw_password)

    # 用于验证前台登录密码是否和数据库一致,raw_password是前台登录输入的密码
    def check_password(self, raw_password):
        result = check_password_hash(self.password, raw_password)  # 相当于用相同的hash加密算法加密raw_password,检测与数据库中是否一致
        return result
    
    
# 帖子编辑提交模型
class PostModel(db.Model):
    __tablename__ = "post"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(100), nullable=True)                                      # 帖子标题
    content = db.Column(db.Text, nullable=True)                                           # 帖子内容
    content_html = db.Column(db.Text)
    create_time = db.Column(db.DateTime, default=datetime.now)                            # 默认当前时间
    
    # 外键,用于查询排序
    board_id = db.Column(db.Integer, db.ForeignKey('cms_board.id'))      # 'cms_board.id'中cms_board是cms/models.py的表名
    author_id = db.Column(db.String(100), db.ForeignKey('front_user.id'))# 这里的id使用String是因为上面定义前台用户id时,使用的就是Str类型shortuuid

    # 反向查询属性,
    board = db.relationship("BoardModel", backref="posts")              # posts变成cms/models/BoardModel的属性
    author = db.relationship("Front_User", backref="posts")             # posts变成Front_User的属性

    # 实现将用户输入的content文件text类型转换成content_html的html文件,再进行存储
    @staticmethod
    def content_to_content_html(target, value, oldvalue, initiator):
        # content_html文件中允许使用的标签集合
        allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'ol', 'pre',
                        'strong', 'ul', 'h1', 'h2', 'h3', 'p', 'img', 'video', 'div', 'iframe',
                        'p', 'br', 'span', 'hr', 'src', 'class']
        # content_html文件中允许使用的属性
        allowed_attrs = {'*': ['class'],
                         'a': ['href', 'rel'],
                         'img': ['src', 'alt']}
        # 目标文件content_html,由bleach库进行转换         markdown将源文件显示成html文件
        target.content_html = bleach.linkify(bleach.clean(
            markdown(value, output_format='html'),                                # output_format='html'输出格式为html
            tags=allowed_tags, strip=True, attributes=allowed_attrs))             # strip=True去空格
        
        
# 监听PostModel.content文件如果调用了set方法,就调用content_to_content_html方法进行转换格式到html文件
db.event.listen(PostModel.content, 'set', PostModel.content_to_content_html)


# 添加评论  模型
class CommentModel(db.Model):
    __tablename__ = "comment"
    
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    content = db.Column(db.Text, nullable=True)                             # 帖子内容
    create_time = db.Column(db.DateTime, default=datetime.now)              # 默认当前时间

    # 添加评论的作者
    author_id = db.Column(db.String(100), db.ForeignKey("front_user.id"))       # 外键关联front_user表中的id字段
    # 帖子id   外键关联post表中的id字段
    post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
    # 反向属性backref命名任意,调用的时候需要一致
    post = db.relationship("PostModel", backref="comments")
    author = db.relationship("Front_User", backref="comments")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119

通过导入CommentModel 模型到manage.py中,再映射到数据库中:
在这里插入图片描述
前台表单信息:apps/front/forms.py,创建评论的Form表单验证。

# -*- encoding: utf-8 -*-
"""
@File    : forms.py
@Time    : 2020/5/11 10:00
@Author  : chen
前台表单信息:apps/front/forms.py
"""
# 前台form表单信息
from wtforms import Form, StringField, IntegerField, ValidationError
from wtforms.validators import EqualTo, Email, InputRequired, Length, Regexp
from utils import random_captcha                                               # 随机生成验证码
from utils import redis_captcha                                                # 保存验证码到redis数据库中


# 表单信息的父类文件
class BaseForm(Form):
    def get_error(self):
        message = self.errors.popitem()[1][0]          # 错误信息的收集,字典类型数据信息提取
        return message


# 注册界面的Form表单类
class SignupForm(BaseForm):
    telephone = StringField(validators=[Regexp(r'1[345789]\d{9}', message="请输入正确格式的手机号")])
    sms_captcha = StringField(validators=[Regexp(r'\w{4}', message="请输入正确格式的验证码")])              # \w包含字母
    username = StringField(validators=[Length(min=2, max=15, message="请输入正确长度的用户名")])
    password1 = StringField(validators=[Regexp(r'[0-9a-zA-Z_\.]{3,20}', message="请输入正确格式的密码")])
    password2 = StringField(validators=[EqualTo('password1', message="两次输入密码不一致")])
    graph_captcha = StringField(validators=[Regexp(r'\w{4}', message="请输入正确格式的验证码")])
    
    # 验证手机验证码字段
    def validate_sms_captcha(self, field):
        telephone = self.telephone.data
        sms_captcha = self.sms_captcha.data                            # 获得表单信息
        
        sms_captcha_redis = redis_captcha.redis_get(telephone)         # redis数据库中根据手机号调验证码,进行判定是否相同
        
        # 判断用户输入的验证码和redis中取出的验证码是否相同
        if not sms_captcha_redis or sms_captcha_redis.lower() != sms_captcha.lower():
            raise ValidationError(message="手机验证码输入错误")
        
        # if sms_captcha or sms_captcha.lower() == sms_captcha_redis.lower():
        #     pass
        # else:
        #     raise ValidationError("验证码输入错误")
      
    # 图形验证码字段验证
    def validate_graph_captcha(self, field):
        graph_captcha = self.graph_captcha.data                                 # 表单信息收集

        graph_captcha_redis = redis_captcha.redis_get(graph_captcha)            # redis中是将验证码的text当做key来保存的,调用也是一样

        # 判定图形验证码是否一致
        if not graph_captcha_redis or graph_captcha_redis.lower() != graph_captcha.lower():
            # print("ceshi")
            raise ValidationError(message="图形验证码输入错误")


# 登录界面的Form表单信息收集
class SigninForm(BaseForm):
    telephone = StringField(validators=[Regexp(r'1[345789]\d{9}', message="请输入正确格式的手机号")])
    password = StringField(validators=[Regexp(r'[0-9a-zA-Z_\.]{3,20}', message="请输入正确格式的密码")])
    remember = StringField(IntegerField())
    

# 帖子的Form表单信息收集
class AddPostForm(BaseForm):
    title = StringField(validators=[InputRequired(message="请输入标题")])
    content = StringField(validators=[InputRequired(message="请输入内容")])
    board_id = StringField(validators=[InputRequired(message="请输入板块名称")])
    

# 评论的Form表单验证
class AddCommentForm(BaseForm):
    # 表单信息的收集根据网页端发送的参数名称和类型
    content = StringField(validators=[InputRequired(message="请输入评论内容")])
    post_id = IntegerField(validators=[InputRequired(message="请选择一篇帖子进行评论")])

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78

帖子详情页面文件:templates/front/front_detail.html

<!-- 帖子详情页面文件:templates/front/front_detail.html  -->
{% extends 'front/front_base.html' %}

{% block title %}帖子详情{% endblock %}

{% block head %}
    <!-- 百度的富文本编辑器加载 -->
    <script src="{{ url_for('static', filename='ueditor/ueditor.config.js') }}"></script>
    <script src="{{ url_for('static', filename='ueditor/ueditor.all.min.js') }}"></script>
    <link rel="stylesheet" href="{{ url_for('static', filename='front/css/front_pdetail.css') }}">
    <script src="{{ url_for('static', filename='front/js/front_pdetail.js') }}"></script>
{% endblock %}

{% block main_content %}
<div class="main-container">
    <div class="lg-container">
        <div class="post-container">
            <!--      帖子标题,前台蓝图文件:apps/front/views.py中的路由定义中传输过来      -->
            <h2>{{ post.title }}</h2>
            <p class="post-info-group">
                <span>发表时间:{{ post.create_time }}</span>
                <!--  author和board这两个字段是PostModel的外键,关联了Front_User和BoardModel模型中的username、name字段       -->
                <span>作者:{{ post.author.username }}</span>
                <span>所属板块:{{ post.board.name }}</span>
                <span>阅读数:0</span>
                <span>评论数:0</span>
            </p>
            <!--     data-id="{{ post.id }} 传输帖子id到front_pdetail.js文件进行获取      -->
            <article class="post-content" id="post-content" data-id="{{ post.id }}">
                <!--   safe用于转义成安全字符串,content_html才能在页面渲染出标签的效果,content中包含有标签内容         -->
                {{ post.content_html|safe }}
            </article>
        </div>
        <div class="comment-group">
            <h3>评论列表</h3>
            <ul class="comment-list-group">
                <!--  comments是反向引用的属性  -->
                {% for comment in post.comments %}
                    <li>
                        <div class="avatar-group">
                            <img src="{{ url_for('static', filename='common/images/logo.png') }}"
                                 alt="">
                        </div>
                        <div class="comment-content">
                            <p class="author-info">
                                <!--   comment.author外键,从CommentModel中调用    -->
                                <span>{{ comment.author.username }}</span>
                                <span>{{ comment.create_time }}</span>
                            </p>
                            <p class="comment-txt">
                                {{ comment.content|safe }}
                            </p>
                        </div>
                    </li>
                {% endfor %}

            </ul>
        </div>
        <div class="add-comment-group">
            <h3>发表评论</h3>
            <!--   这是绑定front_pdetail.js中的百度文本编辑器的id="editor",这里的标签是 script       -->
            <script id="editor" type="text/plain" style="height:100px;"></script>

            <div class="comment-btn-group">
                <!--    绑定id="comment-btn"  -->
                <button class="btn btn-primary" id="comment-btn">发表评论</button>
            </div>
        </div>
    </div>

    <div class="sm-container"></div>
</div>
{% endblock %}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73

4、Flask分页功能

需要使用flask插件:flask paginate
在这里插入图片描述
映射模型到数据库中文件: manage.py,创建测试数据集200条:

# -*- encoding: utf-8 -*-
"""
@File    : manage.py
@Time    : 2020/5/10 17:36
@Author  : chen
映射模型到数据库中文件: manage.py
"""
# 数据库添加多条帖子信息,进行验证分页功能
@manage.command
def create_test_post():
    for i in range(1, 200):                                  # 循环产生200篇帖子信息
        title = "标题%s" % i
        content = "内容%s" % i
        author = Front_User.query.first()                    # 查询数据库中所有的用户信息
        
        post = PostModel(title=title, content=content)       # 循环的标题内容信息添加给PostModel
        post.author = author
        post.board_id = random.randint(2, 7)                # 随机选择cms_board表中的id的值为2-7
        db.session.add(post)
        db.session.commit()
    print("测试帖子添加成功!")       
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

命令行添加测试数据集200条。
在这里插入图片描述
在这里插入图片描述
前台蓝图文件:apps/front/views.py,将测试数据集传输到前端页面中front_index.html中

# -*- encoding: utf-8 -*-
"""
@File    : views.py
@Time    : 2020/5/11 9:59
@Author  : chen
前台蓝图文件:apps/front/views.py
"""
# 前台的蓝图文件  类视图函数写在这里
from flask import (
    Blueprint,
    render_template,
    views,
    make_response,                  # make_response生成response对象,用于返回前端模板
    request,
    session,
    g,
)

# 导入图像验证码生成文件
from utils.captcha import Captcha

# 图形验证码image是二进制数据,需要转换成字节流才能使用
from io import BytesIO

# 将图形验证码保存到Redis         restful输出信息弹窗
from utils import redis_captcha, restful

# 验证码表单信息验证
from .forms import (
    SignupForm,               # 注册的Form表单信息收集
    SigninForm,               # 登录的Form表单信息收集
    AddPostForm,              # 帖子提交表单信息
    AddCommentForm,           # 添加帖子评论
)

# 导入前台用户模型
from .models import (
    Front_User,
    PostModel,
    CommentModel,             # 评论模型
)

# 导入数据库连接 db
from exts import db

# 确保URL安全的文件:utils/safe_url.py
from utils import safe_url

from apps.cms.models import (
    BannerModel,                      # 导入后台轮播图模型BannerModel
    BoardModel,                       # 导入后台板块管理模型
)
# 导入分页功能库
from flask_paginate import Pagination, get_page_parameter
# 导入前台界面权限验证装饰器
from .decorators import login_required
# 导入配置文件
import config

front_bp = Blueprint("front", __name__)          # 前端不用前缀,直接在首页显示,front是蓝图,在front_signup.html调用生成图形验证码时候需要用

# 权限验证  需要在front_bp产生后,再导入
from .hooks import before_request


# BBS的首页界面路由
@front_bp.route("/")
def index():
    banners = BannerModel.query.order_by(BannerModel.priority.desc()).limit(4)   # 通过权重查询,每页显示4条
    boards = BoardModel.query.all()                                              # 查询板块中的所有
    board_id = request.args.get('board_id', type=int, default=None)              # get方法需要使用args,注意这里的数据类型需要改成int
    
    page = request.args.get(get_page_parameter(), type=int, default=1)           # 获取当前页码
    start = (page-1)*config.PER_PAGE                                             # 起始页码是(当前页码-1)*10
    end = start + config.PER_PAGE                                                # 每页都是起始页码+10
    
    # 实现根据不同board_id进行帖子分类显示,即用户选择不同板块,显示的帖子种类相对应
    if board_id:
        posts = PostModel.query.filter_by(board_id=board_id).slice(start, end) # 用户选择不同板块,查询相对应板块的数据,slice(start, end)分页
        total = PostModel.query.filter_by(board_id=board_id).count()           # 计算该板块的总数
    else:
        posts = PostModel.query.slice(start, end)                              # 帖子信息传输,如果用户不选择板块,查询所有
        total = PostModel.query.count()                                        # 计算帖子总数

    # pagination是一个对象,bs_version=3是bootstrap的版本为3,per_page参数添加,pagination.links正常显示所有
    pagination = Pagination(bs_version=3, page=page, total=total,
                            per_page=config.PER_PAGE,                          # config.py中的每页10条数据
                            inner_window=3, outer_window=1)                    # inner_window=3是内层显示页码的样式,默认为2,
    # print(pagination.links)                                                  # 当数据量小的时候,不显示,添加per_page参数就能解决
    
    context = {                                                                # 多种数据传输到前台界面
        "banners": banners,
        "boards": boards,
        "current_board_id": board_id,
        "posts": posts,
        "pagination": pagination,
    }
    
    return render_template("front/front_index.html", **context)            # 渲染到首页界面,查询数据传输到前台界面


# 图形验证码路由
@front_bp.route("/captcha/")
def graph_captcha():
    try:                                                 # 异常处理
        # 图像验证码生成文件中返回两个参数   text, image
        text, image = Captcha.gene_graph_captcha()      # 生成图形验证码,image是二进制数据,需要转换成字节流才能使用
        print("发送的图形验证码是:{}".format(text))
        
        # 将图形验证码保存到Redis数据库中
        redis_captcha.redis_set(text.lower(), text.lower())  # redis_set中需要传参key和value,text没有唯一对应的key,只能都传参text
        
        # BytesIO是生成的字节流
        out = BytesIO()
        image.save(out, 'png')                          # 把图片image保存在字节流中,并指定为png格式
        # 文件流指针
        out.seek(0)                                     # 从字节流最初开始读取
        # 生成response对象,用于返回前端模板中
        resp = make_response(out.read())
        resp.content_type = 'image/png'                 # 指定数据类型
    except:
        return graph_captcha()                          # 没有生成验证码就再调用一次
        
    return resp                                         # 返回对象


# 测试referrer的跳转
@front_bp.route("/test/")
def test():
    return render_template("front/front_test.html")


# 用户注册类视图
class SingupView(views.MethodView):
    def get(self):
        # 图像验证码生成文件中返回两个参数   text, image
        # text, image = Captcha.gene_graph_captcha()
        # print(text)                      # 验证码
        # print(image)                     # 图形文件,图形类<PIL.Image.Image image mode=RGBA size=100x30 at 0x1EFC9000C88>

        # 从当前页面跳转过来就是None   从其他页面跳转过来输出就是上一个页面信息     referrer是页面的跳转
        # print(request.referrer)                           # http://127.0.0.1:9999/test/
        
        return_to = request.referrer
        # 确保URL安全的文件:utils/safe_url.py
        print(safe_url.is_safe_url(return_to))              # 判断return_to是否来自站内,是否是安全url,防爬虫
        
        if return_to and return_to != request.url and safe_url.is_safe_url(return_to):       # 跳转的url不能是当前页面,request.url是当前的url地址
            return render_template("front/front_signup.html", return_to=return_to)           # return_to渲染到前端界面
        else:
            return render_template("front/front_signup.html")                                # 如果没获取url,直接渲染注册界面
        
    # 验证码的form表单信息提交验证
    def post(self):
        form = SignupForm(request.form)                       # 收集表单信息
        
        # 表单验证通过
        if form.validate():
            # 保存到数据库
            telephone = form.telephone.data
            username = form.username.data
            password = form.password1.data                    # forms表单信息
            
            # 前台用户模型数据添加到数据库
            user = Front_User(telephone=telephone, username=username, password=password)
            db.session.add(user)
            db.session.commit()                                                   # 提交到数据库
            
            # 表单验证通过,提交到数据库成功
            return restful.success()
        else:
            return restful.params_error(message=form.get_error())                  # 表单信息验证出错


# 用户登录的类视图
class SinginView(views.MethodView):
    def get(self):
        return_to = request.referrer                                                    # referrer是上一个url
    
        if return_to and return_to != request.url and safe_url.is_safe_url(return_to):  # 跳转的url不能是当前页面,判断url是否安全
            return render_template("front/front_signin.html", return_to=return_to)      # return_to渲染到前端界面
        else:
            return render_template("front/front_signin.html")                           # 如果没获取url,直接渲染注册界面
    
    def post(self):
        form = SigninForm(request.form)                                            # 登录界面的Form表单信息
        
        if form.validate():                                                        # 表单信息存在
            # 收集form表单信息
            telephone = form.telephone.data
            password = form.password.data
            remember = form.remember.data
            
            user = Front_User.query.filter_by(telephone=telephone).first()         # 通过手机号验证该用户是否存在数据库
            if user and user.check_password(password):                             # 判断密码和用户是否正确
                # 'front_user_id'命名防止与后台验证session相同,会产生覆盖情况bug
                session['front_user_id'] = user.id                                 # 用户的id存储到session中,用于登录验证
                if remember:                                                       # 如果remember状态是1
                    # session持久化
                    session.permanent = True
                return restful.success()                                           # 成功
            else:
                return restful.params_error(message="手机号或者密码错误")           # 密码是、用户不正确
        else:
            return restful.params_error(message=form.get_error())                  # 表单信息不存在,输出异常信息
        

#  帖子编辑提交  的类视图     富文本编辑
class PostView(views.MethodView):
    # 登录验证,实现帖子编辑前进行权限验证
    decorators = [login_required]
    # 表单信息收集,传输
    def get(self):
        # 查询boards数据进行传输
        boards = BoardModel.query.all()                                           # boards是list类型
        return render_template("front/front_apost.html", boards=boards)           # boards数据传输到前端front_apost.html页面
    
    # 帖子的Form表单信息收集查询
    def post(self):
        form = AddPostForm(request.form)                     # 查询帖子提交的Form表单信息
        if form.validate():
            title = form.title.data
            board_id = form.board_id.data                    # 收集表单中提交的信息
            content = form.content.data
            
            # 查询用户信息是否在数据库中存在
            board = BoardModel.query.get(board_id)
            if not board:
                return restful.params_error(message="没有这个版块名称")           # 数据库中不存在,返回异常信息
            
            # 数据库中board信息存在,传输数据到数据库表中,并修改名称
            post = PostModel(title=title, board_id=board_id, content=content)
            post.board = board                                                   # 外键中的信息修改赋值
            post.author = g.front_user                                           # g对象

            db.session.add(post)
            db.session.commit()
            return restful.success()                                             # 提交成功,为json数据
        else:
            return restful.params_error(message=form.get_error())
            

# 前台 帖子详情 路由
@front_bp.route("/p/<post_id>")                  # 蹄子详情路由需要传参帖子id:post_id
def post_detail(post_id):
    post = PostModel.query.get(post_id)          # 通过post_id查找数据库中的帖子信息
    if not post:
        return restful.params_error(message="帖子不存在!")
    
    return render_template("front/front_detail.html", post=post)             # 查找到帖子信息,传输数据到帖子详情页渲染
 

# 添加评论  的路由
@front_bp.route("/acomment/", methods=['POST'])
@login_required                                      # 登录验证
def add_comment():
    form = AddCommentForm(request.form)              # 网页发送的request.form表单信息放入AddCommentForm进行验证
    if form.validate():
        content = form.content.data                  # form表单信息
        post_id = form.post_id.data
        
        post = PostModel.query.get(post_id)          # 通过post_id查询帖子信息
        if post:
            comment = CommentModel(content=content)  # 将AddCommentForm中验证后的content信息传给CommentModel模型
            # 外键关联,反向属性backref,从CommentModel中调用
            comment.post = post                      # 外键关联的是post表中的id字段
            comment.author = g.front_user            # 将apps/front/hooks.py中的g对象赋值外键的作者的id
            db.session.add(comment)                  # 添加对象信息到数据库
            db.session.commit()
            return restful.success()                 # 提交成功
        else:
            return restful.params_error(message="没有这篇帖子")  # 数据库中查询不到信息
    else:
        return restful.params_error(message=form.get_error())   # 表单验证失败
            
        
# 绑定类视图的路由
front_bp.add_url_rule("/signup/", view_func=SingupView.as_view("signup"))          # "signup"视图中不需要反斜线,决定了url_for的路由地址
front_bp.add_url_rule("/signin/", view_func=SinginView.as_view("signin"))          # "signin"视图中不需要反斜线
front_bp.add_url_rule("/apost/", view_func=PostView.as_view("apost"))              # 绑定帖子编辑提交路由

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281

项目配置文件:config.py

'''
项目配置文件:config.py
'''
# 每页显示数据数目
PER_PAGE = 10
  • 1
  • 2
  • 3
  • 4
  • 5

前台首页页面文件:templates/front/front_index.html,接收pagination参数渲染到页面。

<!-- 前台首页页面文件:templates/front/front_index.html  -->

{% extends 'front/front_base.html' %}

{% block title %}
首页
{% endblock %}

<!-- 模板继承 -->
{% block main_content %}
<!--   居中样式  -->
<div class="main-container">
    <div class="lg-container">
        <!-- bootstrop中复制来的轮播图  -->
        <div id="carousel-example-generic" class="carousel slide" data-ride="carousel">
            <!-- 指令 -->
            <ol class="carousel-indicators">
                <li data-target="#carousel-example-generic" data-slide-to="0" class="active"></li>
                <li data-target="#carousel-example-generic" data-slide-to="1"></li>
                <li data-target="#carousel-example-generic" data-slide-to="2"></li>
            </ol>

            <!-- 轮播图 -->
            <div class="carousel-inner" role="listbox">
                <!--    循环apps/front/views.py文件传输的banners数据      -->
                {% for banner in banners %}
                <!--    判断是否第一次循环      -->
                {% if loop.first %}
                <div class="item active">
                    {% else %}
                    <div class="item">
                        {% endif %}
                        <!--    轮播图路径,style="width: 300px;height: 300px"轮播图大小 -->
                        <img src="{{ banner.image_url }}" alt="..." style="width: 300px;height: 300px">
                        <div class="carousel-caption">
                        </div>
                    </div>
                    {% endfor %}
                </div>

                <!-- 轮播图左右切换按钮 -->
                <a class="left carousel-control" href="#carousel-example-generic" role="button" data-slide="prev">
                    <span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>
                    <span class="sr-only">Previous</span>
                </a>
                <a class="right carousel-control" href="#carousel-example-generic" role="button" data-slide="next">
                    <span class="glyphicon glyphicon-chevron-right" aria-hidden="true"></span>
                    <span class="sr-only">Next</span>
                </a>
            </div>
            <!-- bootstrop中复制来的轮播图 代码结束   -->

            <!--   帖子排序方式     -->
            <div class="post-group">
                <ul class="post-group-head">
                    <li class=""><a href="#">最新</a></li>
                    <li class=""><a href="#">精华帖子</a></li>
                    <li class=""><a href="#">点赞最多</a></li>
                    <li class=""><a href="#">评论最多</a></li>
                </ul>
                <ul class="post-list-group">
                    <!--         循环帖子信息,首页渲染           -->
                    {% for post in posts %}
                        <li>
                        <div class="author-avatar-group">
                            <img src="#" alt="">
                        </div>
                        <div class="post-info-group">
                            <p class="post-title">
                                <!--  front.post_detail反转需要写的是路由的函数名,post_id=post.id传输帖子id,post_id是名字,post.id是value      -->
                                <a href="{{ url_for('front.post_detail', post_id=post.id) }}">{{ post.title }}</a>
                                <span class="label label-danger">精华帖</span>
                            </p>
                            <p class="post-info">
                                <!-- post模型中的author外键调用Front_User中的username信息  -->
                                <span>作者:{{ post.author.username }}</span>
                                <span>发表时间:{{ post.create_time }}</span>
                                <span>评论:0</span>
                                <span>阅读:0</span>
                            </p>
                        </div>
                    </li>
                    {% endfor %}

                </ul>
                <div style="text-align:center;">
                <!--       页码分页展示, pagination.links数据由 apps/front/views.py传输过来           -->
                    {{ pagination.links }}
                </div>
            </div>
        </div>

        <!--      帖子标签内容      -->
        <div class="sm-container">
            <div style="padding-bottom:10px;">
                <!--       重定向到/apost/路由,文本编辑界面        -->
                <a href="{{ url_for('front.apost') }}" class="btn btn-warning btn-block">发布帖子</a>
            </div>
            <div class="list-group">
                <a href="/" class="list-group-item active">所有板块</a>
                <!--     循环显示前台蓝图文件:apps/front/views.py中传输的数据**context           -->
                {% for board in boards %}
                    <!--         注意这里的current_board_id数据类型是int,才能与board.id相比较           -->
                    {% if current_board_id == board.id %}
                        <!--    url_for('front.index', board_id=board.id)每次点击跳转到front_index.html页面,即当前界面,且传输给一个board_id的参数值,由board.id赋值            -->
                        <a href="{{ url_for('front.index', board_id=board.id ) }}" class="list-group-item active">{{ board.name }}</a>
                    {% else %}
                        <!--  没被选中,即没有被传输相同的board.id,图标样式是class="list-group-item">    -->
                        <a href="{{ url_for('front.index', board_id=board.id ) }}" class="list-group-item">{{ board.name }}</a>
                    {% endif %}
                {% endfor %}
            </div>
        </div>
    </div>
    <!--  居中样式  -->
{% endblock %}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

分页功能实现效果如下:
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/572962
推荐阅读
相关标签
  

闽ICP备14008679号