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")) # 綁定帖子編輯提交路由
創建 帖子詳情頁面文件: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 %}
創建 前臺帖子詳情頁面樣式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']);
}
}
});
// }
});
});
創建 前臺帖子詳情頁面樣式文件: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;
}
前臺首頁頁面文件: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 %}
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')
映射模型到數據庫中文件: manage.py,導入後臺帖子加精模型
"""
映射模型到數據庫中文件: manage.py
"""
# 導入後臺模型 才能映射到數據庫 ,導入輪播圖和文章的管理模塊
from apps.cms.models import (
BannerModel,
BoardModel,
HighlightPostModel, # 後臺帖子加精模型
)
加精帖子模型映射到數據庫:
視圖文件: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=['[email protected]'], 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'))
後臺管理帖子頁面: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 %}
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")
通過導入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="請選擇一篇帖子進行評論")])
帖子詳情頁面文件: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 %}
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("測試帖子添加成功!")
命令行添加測試數據集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")) # 綁定帖子編輯提交路由
項目配置文件:config.py
'''
項目配置文件:config.py
'''
# 每頁顯示數據數目
PER_PAGE = 10
前臺首頁頁面文件: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 %}
分頁功能實現效果如下: