Flask-BBS论坛项目实战8-轮播图和七牛云

首页轮播图实现

轮播图的实现非常简单,基于bootstrap的轮播图更简单,这里就不多说了,我们自己上代码。

因为以后所有的内容都是在main-container里面,所以这里我们修改front_base.html,把放到里面去

1
2
3
<div class="main-container">
{% block body %}{% endblock %}
</div>

创建static/front/css/front_base.css, 并且在front_base.html中引入它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.main-container{
width: 990px;
margin: 0 auto;
overflow: hidden;
}

.lg-container{
width: 730px;
float: left;
}

.sm-container{
width: 250px;
float: right;
}

编辑front_index.html

1
2
3
4
{% block body %}
<div class="lg-container"></div>
<div class="sm-container"></div>
{% endblock %}

轮播图的代码在bootstrap v3中文文档中可以拷贝当到 lg-container中,然后进行修改如下

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
<div class="lg-container">
<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>
<li data-target="#carousel-example-generic" data-slide-to="3"></li>
</ol>

<!-- 轮播图 -->
<div class="carousel-inner" role="listbox">
<div class="item active">
<a href="#">
<img src="https://i1.mifile.cn/a4/xmad_15338857534213_DaEjU.jpg" alt="...">
</a>
</div>
<div class="item">
<a href="#">
<img src="https://i1.mifile.cn/a4/xmad_1535103285321_fPlOK.jpg" alt="...">
</a>
</div>
<div class="item">
<a href="#">
<img src="https://i1.mifile.cn/a4/xmad_15349329950127_PVrya.jpg" alt="...">
</a>
</div>
<div class="item">
<a href="#">
<img src="https://i1.mifile.cn/a4/xmad_15338982677936_eQTJk.jpg" alt="...">
</a>
</div>
</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>
</div>

我们来写个static/front/css/front_index.css来设置轮播图的边角位圆角和它的高度

1
2
3
4
5
6
7
8
9
10
.index-banner{
border-radius: 10px;
overflow: hidden;
max-width: 800px;
}

/*需要把图片的高度和轮播的一致*/
.index-banner img{
width: 100%;
}

在轮播图的 div加上类index-banner

1
<div id="carousel-example-generic" class="carousel slide index-banner" data-ride="carousel">

并引入static/front/css/front_index.css

1
2
3
{% block head %}
<link href="{{ url_for('static', filename='front/css/front_index.css') }}" rel="stylesheet">
{% endblock %}

cms轮播图管理页面布局

新建cms_banners.html继承cms_base.html
1
2
3
4
5
6
7
8
9
10
11
12
13
{% extends 'cms/cms_base.html' %}

{% block title %}
轮播图管理-CMS管理系统
{% endblock %}

{% block page_title %}
轮播图管理
{% endblock %}

{% block main_content %}
这是轮播图管理页面
{% endblock %}

编辑cms.views,配置视图

1
2
3
4
@bp.route('/banners/')
@login_required
def banners():
return render_template('cms/cms_banners.html')

编辑cms_base.html添加一个“轮播图管理”菜单

1
<li class="nav-group banner-manage"><a href="{{ url_for('cms.banners') }}">轮播图管理</a></li>

编辑cms_base.js,在末尾加上轮播图管理菜单被选中时的样式

1
2
3
4
else if(url.indexOf('banners') >= 0) {
var bannersManageLi = $('.banner-manage');
bannersManageLi.addClass('unfold').siblings().removeClass('unfold');
}

cms添加轮播图的模态对话框制作

编辑cms_banners.html

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
{% block main_content %}
<div class="top-box">
<button class="btn btn-warning" data-toggle="modal" data-target="#banner-dialog">添加轮播图</button>
</div>
<table class="table table-bordered">
<thead>
<tr>
<th>名称</th>
<th>图片链接</th>
<th>跳转链接</th>
<th>优先级</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>11</td>
<td><a href="#" target="_blank">测试数据</a></td>
<td><a href="#" target="_blank">测试数据</a></td>
<td>测试数据</td>
<td>测试数据</td>
<td>
<button class="btn btn-default btn-xs edit-banner-btn">编辑</button>
<button class="btn btn-danger btn-xs delete-banner-btn">删除</button>
</td>
</tr>
</tbody>
</table>
<div class="modal fade" id="banner-dialog" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title">添加轮播图</h4>
</div>
<div class="modal-body">
<form action="" class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label">名称:</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="name" placeholder="轮播图名称">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">图片:</label>
<div class="col-sm-7">
<input type="text" class="form-control" name="image_url" placeholder="轮播图图片">
</div>
<button class="btn btn-info col-sm-2" id="upload-btn">添加图片</button>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">跳转:</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="link_url" placeholder="跳转链接">
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label">权重:</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="priority" placeholder="优先级">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary">保存</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
{% endblock %}

头部添加样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
{% block head %}
<style>
.top-box button{
float: right;
}

.top-box{
overflow: hidden;
background: #ecedf0;
padding: 10px;
}
</style>
{% endblock %}

cms添加轮播图后端代码逻辑完成

首先,我们需要给轮播图设计一张表,因为轮播图前端要展示,CMS要管理,所以我们在apps下新建个models.py

编辑apps.models.py
1
2
3
4
5
6
7
8
9
10
11
12
from exts import db
from datetime import datetime


class BannerModel(db.Model):
__tablename__ = 'banner'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(255), nullable=False)
image_url = db.Column(db.String(255), nullable=False)
link_url = db.Column(db.String(255), nullable=False)
priority = db.Column(db.Integer, default=0)
create_time = db.Column(db.DateTime, default=datetime.now)

同步表到数据库

同步表前需要在manage.py中导入BannerModel,否则不会进行同步

1
2
python manage.py db migrate
python manage.py db upgrade

表单认证,编辑cms.forms.py

1
2
3
4
5
class AddBannerForm(BaseForm):
name = StringField(validators=[InputRequired(message='请输入轮播图名称!')])
image_url = StringField(validators=[InputRequired(message='请输入轮播图图片链接!')])
link_url = StringField(validators=[InputRequired(message='请输入轮播图跳转链接!')])
priority = IntegerField(validators=[InputRequired(message='请输入轮播图优先级!')])

视图,编辑cms.views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
from .forms import AddBannerForm
from apps.models import BannerModel

@bp.route('/abanner/',methods=['POST'])
@login_required
def abanner():
form = AddBannerForm(request.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 xjson.json_success()
else:
return xjson.json_param_error(message=form.get_error())

cms添加轮播图前端代码逻辑完成

首页我们在模态框中的保存按钮加一个id,这样方便我们选取这个按钮

1
<button type="button" class="btn btn-primary" id="save-banner-btn">保存</button>

在static/cms/js/下新建banners.js

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
$(function () {
$("#save-banner-btn").click(function (event) {
event.preventDefault();
var dialog = $("#banner-dialog");
var nameInput = dialog.find("input[name='name']");
var imageInput = dialog.find("input[name='image_url']");
var linkInput = dialog.find("input[name='link_url']");
var priorityInput = dialog.find("input[name='priority']");

var name = nameInput.val();
var image_url = imageInput.val();
var link_url = linkInput.val();
var priority = priorityInput.val();

if(!name || !image_url || !link_url || !priority){
zlalert.alertInfoToast('请输入完整的轮播图数据!');
return;
}

bbsajax.post({
"url": '/cms/abanner/',
"data": {
'name':name,
'image_url': image_url,
'link_url': link_url,
'priority':priority,
},
'success': function (data) {
dialog.modal("hide");
if(data['code'] == 200){
// 重新加载这个页面
window.location.reload();
}else{
zlalert.alertInfo(data['message']);
}
},
'fail': function () {
zlalert.alertNetworkError();
}

});
});
});

编辑cms.view.py中的banners视图, 获取数据库数据

1
2
3
4
5
@bp.route('/banners/')
@login_required
def banners():
banners = BannerModel.query.order_by(BannerModel.priority.desc()).all()
return render_template('cms/cms_banners.html', banners=banners)

编辑cms_banners.html,

中的内容动态展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<tbody>
{% for banner in banners %}
<tr>
<td>{{ banner.name }}</td>
<td><a href="{{ banner.image_url }}" target="_blank">{{ banner.image_url }}</a></td>
<td><a href="{{ banner.link_url }}" target="_blank">{{ banner.link_url }}</a></td>
<td>{{ banner.priority }}</td>
<td>{{ banner.create_time }}</td>
<td>
<button class="btn btn-default btn-xs edit-banner-btn">编辑</button>
<button class="btn btn-danger btn-xs delete-banner-btn">删除</button>
</td>
</tr>
{% endfor %}
</tbody>

然后在cms_banners.html中引入banners.js

1
<script src="{{ url_for('static', filename='cms/js/banners.js')}}"></script>

cms编辑轮播图功能完成

后端逻辑

表单验证, 这里编辑就是和添加的内容一样,所以可以直接继承添加轮播图的表单验证,然后多加一个轮播图的id即可

编辑cmd.forms.py

1
2
class UpdateBannerForm(AddBannerForm):
banner_id = IntegerField(validators=[InputRequired(message='请输入轮播图的id!')])

视图,编辑cms.views.py

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
...
from .forms import UpdateBannerForm


@bp.route('/ubanner/',methods=['POST'])
@login_required
def ubanner():
form = UpdateBannerForm(request.form)
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)
if banner:
banner.name = name
banner.image_url = image_url
banner.link_url = link_url
banner.priority = priority
db.session.commit()
return xjson.json_success()
else:
return xjson.json_param_error(message='没有这个轮播图!')
else:
return xjson.json_param_error(message=form.get_error())

前端逻辑

当我们点击编辑,也是弹出一个模态框,而且这个模态框其实是和添加轮播图的模态框是一样的。不同的地方就是编辑的模态框有内容。
因此我们可以复用之前的模态框,并把内用填充进表单即可。
现在关键是如何获取内容?
这里有个方法就是把需要的内容存到元素的属性中,如下,我们存入到tr标签的属性中
我们有存入:

  • data-name=”“
  • data-image=”“
  • data-link=”“
  • data-priority=”“
  • data-id=”“
1
2
3
4
5
6
7
8
9
10
11
12
13
{% for banner in banners %}
<tr data-name="{{ banner.name }}" data-image="{{ banner.image_url }}" data-link="{{ banner.link_url }}" data-priority="{{ banner.priority }}" data-id="{{ banner.id }}">
<td>{{ banner.name }}</td>
<td><a href="{{ banner.image_url }}" target="_blank">{{ banner.image_url }}</a></td>
<td><a href="{{ banner.link_url }}" target="_blank">{{ banner.link_url }}</a></td>
<td>{{ banner.priority }}</td>
<td>{{ banner.create_time }}</td>
<td>
<button class="btn btn-default btn-xs edit-banner-btn">编辑</button>
<button class="btn btn-danger btn-xs delete-banner-btn">删除</button>
</td>
</tr>
{% endfor %}

这样我们通过js获取定位到“编辑”按钮,获取这个 按钮的父元素(td)的父元素(tr)的属性值,然后把需要的值填入到表单就可以了。
编辑banners.js

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
$(function () {
$("#save-banner-btn").click(function (event) {
event.preventDefault();
self = $(this);
var dialog = $("#banner-dialog");
var nameInput = dialog.find("input[name='name']");
var imageInput = dialog.find("input[name='image_url']");
var linkInput = dialog.find("input[name='link_url']");
var priorityInput = dialog.find("input[name='priority']");

var name = nameInput.val();
var image_url = imageInput.val();
var link_url = linkInput.val();
var priority = priorityInput.val();
//通过保存按钮上的属性data-type,获取数据类型,如果它的值是update,就是编辑操作了,否则就是添加操作
var submitType = self.attr('data-type');
//这里通过保存按钮上的属性data-id获取到轮播图的id, 如果是添加轮播图这就是个空值,不用管它
var bannerId = self.attr("data-id");

if(!name || !image_url || !link_url || !priority){
zlalert.alertInfoToast('请输入完整的轮播图数据!');
return;
}

//根据submitType的值来判断url应该是添加还是编辑
var url = '';
if(submitType == 'update'){
url = '/cms/ubanner/';
}else{
url = '/cms/abanner/';
}

bbsajax.post({
"url": url, //这里就要改成动态获取上面的url了
"data": {
'name':name,
'image_url': image_url,
'link_url': link_url,
'priority':priority,
'banner_id': bannerId //这里需要多传入一个轮播图id,就是是添加操作也无所谓,后端也没接收
},
'success': function (data) {
dialog.modal("hide");
if(data['code'] == 200){
// 重新加载这个页面
window.location.reload();
}else{
zlalert.alertInfo(data['message']);
}
},
'fail': function () {
zlalert.alertNetworkError();
}

});
});
});

$(function () {
$(".edit-banner-btn").click(function (event) {
var self = $(this);
var dialog = $("#banner-dialog");
dialog.modal("show");

var tr = self.parent().parent();
var name = tr.attr("data-name");
var image_url = tr.attr("data-image");
var link_url = tr.attr("data-link");
var priority = tr.attr("data-priority");

var nameInput = dialog.find("input[name='name']");
var imageInput = dialog.find("input[name='image_url']");
var linkInput = dialog.find("input[name='link_url']");
var priorityInput = dialog.find("input[name='priority']");
var saveBtn = dialog.find("#save-banner-btn");

nameInput.val(name);
imageInput.val(image_url);
linkInput.val(link_url);
priorityInput.val(priority);
saveBtn.attr("data-type",'update');
saveBtn.attr('data-id',tr.attr('data-id'));
});
});

到此,我们轮播图的编辑功能就完成了!

cms删除轮播图功能完成

后台逻辑

编辑cms.views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@bp.route('/dbanner/',methods=['POST'])
@login_required
def dbanner():
banner_id = request.form.get('banner_id')
if not banner_id:
return xjson.json_param_error(message='请传入轮播图id!')

banner = BannerModel.query.get(banner_id)
if not banner:
return xjson.json_param_error(message='没有这个轮播图!')

db.session.delete(banner)
db.session.commit()
return xjson.json_success()

前台逻辑

编辑banner.js

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
$(function () {
$(".delete-banner-btn").click(function (event) {
var self = $(this);
var tr = self.parent().parent();
var banner_id = tr.attr('data-id');
xtalert.alertConfirm({
"msg":"您确定要删除这个轮播图吗?",
'confirmCallback': function () {
bbsajax.post({
'url': '/cms/dbanner/',
'data':{
'banner_id': banner_id
},
'success': function (data) {
if(data['code'] == 200){
window.location.reload();
}else{
xtalert.alertInfo(data['message']);
}
}
})
}
});
});
});

cms轮播图上传到七牛功能完成

  • 登录七牛云,进入“对象存储”, 新建存储空间(Bucket),
  • 我创建的空间命名为flask-bbs
  • 创建完Bucket,七牛会给我们提供一个测试域名,生产环境中,我们需要绑定自己的域名
  • 在个人面板中进去密钥管理,获取AccessKey/SecretKey
  • 安装七牛python SKD
1
pip install qiniu

后端需要根据认证信息传递token给前端,编辑common.view.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import  jsonify
import qiniu
...

@bp.route('/uptoken/')
def uptoken():
access_key = '8A9ledAc13rRahJb74VuMUTCMvEIORp36XP5-_Zr'
secret_key = 'bDYTou2LULvSbj8lb-ouUvYXrHOSndKMa2oPmhJE'
q = qiniu.Auth(access_key, secret_key)

bucket = 'flask-bbs'
token = q.upload_token(bucket)
return jsonify({'uptoken': token})

前端需要用到js版的jdk,编辑cms_banners.html,引入以下3个js

1
2
3
<script src="https://cdn.staticfile.org/Plupload/2.1.1/moxie.js"></script>
<script src="https://cdn.staticfile.org/Plupload/2.1.1/plupload.dev.js"></script>
<script src="https://cdn.staticfile.org/qiniu-js-sdk/1.0.14-beta/qiniu.js"></script>

在static/common/js下新建bbsqiniu.js,对jdk 进行一层封装,使用起来更方便

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
'use strict';

var bbsqiniu = {
'setUp': function(args) {
var domain = args['domain'];
var params = {
browse_button:args['browse_btn'],
runtimes: 'html5,flash,html4', //上传模式,依次退化
max_file_size: '500mb', //文件最大允许的尺寸
dragdrop: false, //是否开启拖拽上传
chunk_size: '4mb', //分块上传时,每片的大小
uptoken_url: args['uptoken_url'], //ajax请求token的url
domain: domain, //图片下载时候的域名
get_new_uptoken: false, //是否每次上传文件都要从业务服务器获取token
auto_start: true, //如果设置了true,只要选择了图片,就会自动上传
unique_names: true,
multi_selection: false,
filters: {
mime_types :[
{title:'Image files',extensions: 'jpg,gif,png'},
{title:'Video files',extensions: 'flv,mpg,mpeg,avi,wmv,mov,asf,rm,rmvb,mkv,m4v,mp4'}
]
},
log_level: 5, //log级别
init: {
'FileUploaded': function(up,file,info) {
if(args['success']){
var success = args['success'];
file.name = domain + file.target_name;
success(up,file,info);
}
},
'Error': function(up,err,errTip) {
if(args['error']){
var error = args['error'];
error(up,err,errTip);
}
},
'UploadProgress': function (up,file) {
if(args['progress']){
args['progress'](up,file);
}
},
'FilesAdded': function (up,files) {
if(args['fileadded']){
args['fileadded'](up,files);
}
},
'UploadComplete': function () {
if(args['complete']){
args['complete']();
}
}
}
};

// 把args中的参数放到params中去
for(var key in args){
params[key] = args[key];
}
var uploader = Qiniu.uploader(params);
return uploader;
}
};

cms_banners.html中引用此js

1
<script src="{{ url_for('static', filename='common/js/bbsqiniu.js') }}"></script>

然后编辑banners.js, 对”添加图片” 这个按钮进行操作

1
2
3
4
5
6
7
8
9
10
11
$(function () {
bbsqiniu.setUp({
'domain': 'http://sei5g2ne0.bkt.clouddn.com/', //七牛的域名
'browse_btn': 'upload-btn', //按钮的id
'uptoken_url': '/c/uptoken/', //后端的url获取token
'success': function (up,file,info) {
var imageInput = $("input[name='image_url']");
imageInput.val(file.name); //把图片的完整地址填入到表单中
}
});
});


现在我们多添加几张轮播图

这样利用七牛存储图片就完成了
编辑front.views.py

1
2
3
4
5
6
7
8
9
10
from apps.models import BannerModel
...

@bp.route('/')
def index():
banners = BannerModel.query.order_by(BannerModel.priority.desc()).all()
context = {
'banners': banners
}
return render_template('front/front_index.html', **context)

编辑front_index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

<!-- 轮播图 -->
<div class="carousel-inner" role="listbox">

{% for banner in banners %}
{% if loop.first %}
<div class="item active">
{% else %}
<div class="item">
{% endif %}
<a href="{{ banner.link_url }}">
<img src="{{ banner.image_url }}" alt="...">
</a>
</div>
{% endfor %}
</div>

大功告成!轮播图有点丑!

板块管理

cms布局

编辑 cms_boards.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{% block main_content %}
<div class="top-box">
<button class="btn btn-warning" data-toggle="modal" data-target="#banner-dialog">添加新板块</button>
</div>

<table class="table table-bordered">
<thead>
<tr>
<th>板块名称</th>
<th>帖子数量</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}

给 “添加新版块“加上样式”

1
2
3
4
5
6
7
8
9
10
11
12
13
{% block head %}
<style>
.top-box button{
float: right;
}

.top-box{
overflow: hidden;
background: #ecedf0;
padding: 10px;
}
</style>
{% endblock %}

添加新板块后端

首先添加个表, 因为板块前后端都要用到,编辑apps.models.py

1
2
3
4
5
class BoardModel(db.Model):
__tablename__ = 'board'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(20), nullable=False)
create_time = db.Column(db.DateTime, default=datetime.now)

然后别忘了迁移数据

添加个表单验证,编辑cms.forms.py

1
2
class AddBoardForm(BaseForm):
name = StringField(validators=[InputRequired(message='请输入板块名称')])

编辑cms.views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

...
from apps.models import BoardModel
from .forms import AddBoardForm

@bp.route('/aboard/', methods=['POST'])
@login_required
@permission_required(CMSPersmission.BOARDER)
def aboard():
add_form_board = AddBoardForm(request.form)
if add_form_board.validate():
name = add_form_board.name.data
board = BoardModel(name=name)
db.session.add(board)
db.session.commit()
return xjson.json_success(message='添加板块成功')
else:
return xjson.json_param_error(message=add_form_board.get_error())

添加板块的逻辑写好了,我们顺便把更新板块,删除板块一起写了
编辑cms.forms.py

1
2
class UpdateBoardForm(AddBoardForm):
board_id = IntegerField(validators=[InputRequired(message='请输入板块id')])

编辑cms.views.py

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
from .forms import UpdateBoardForm

@bp.route('/uboard/', methods=['POST'])
@login_required
@permission_required(CMSPersmission.BOARDER)
def uboard():
update_board_form = UpdateBoardForm(request.form)
if update_board_form.validate():
board_id = update_board_form.board_id.data
name = update_board_form.name.data
if board_id:
board = BoardModel.query.get(board_id)
board.name = name
db.session.commit()
return xjson.json_success(message='更新成功')
else:
return xjson.json_param_error(message='板块不存在')
else:
return xjson.json_param_error(message=update_board_form.get_error())


@bp.route('/dboard/', methods=['POST'])
@login_required
@permission_required(CMSPersmission.BOARDER)
def dboard():
board_id = request.form.get('board_id')
if not board_id:
return xjson.json_param_error(message='请传入板块id')
board = BoardModel.query.get(board_id)
if not board:
return xjson.json_param_error(message='没有这个板块')
db.session.delete(board)
db.session.commit()
return xjson.json_success(message='删除板块成功')

传递板块数据到前端页面

1
2
3
4
5
6
7
8
9
@bp.route('/boards/')
@login_required
@permission_required(CMSPersmission.BOARDER)
def boards():
all_boards = BoardModel.query.all()
context = {
'boards': all_boards
}
return render_template('cms/cms_boards.html', **context)

cms 前端js

给“添加新板块”按钮加上 id

1
<button class="btn btn-warning" data-toggle="modal" data-target="#banner-dialog" id="add-board-btn">添加新板块</button>

创建static/cms/js/boards.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$(function () {
$('#add-board-btn').click(function (event) {
event.preventDefault();
xtalert.alertOneInput({
'text': '请输入板块名称',
'placeholder': '板块名称',
'confirmCallback': function (inputValue) {
bbsajax.post({
'url': '/cms/aboard/',
'data': {
'name':inputValue
},
'success': function (data) {
if(data['code'] ==200){
window.location.reload();
}else{
xtalert.alertInfo(data['message'])
}
}
});
}
});
});
});

编辑cms_boards.html
引入boards.js

1
2
3
4
{% block head %}
<script src="{{ url_for('static', filename='cms/js/boards.js')}}"></script>
...
{% endblock %}

动态展示数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<table class="table table-bordered">
<thead>
<tr>
<th>板块名称</th>
<th>帖子数量</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for board in boards %}
<tr>
<td>{{ board.name }}</td>
<td>暂未实现</td>
<td>{{ board.create_time }}</td>
<td>
<button class="btn btn-default btn-xs edit-board-btn">编辑</button>
<button class="btn btn-danger btn-xs delete-board-btn">删除</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>


编辑和删除板块

我们先把 板块名称和板块id 绑定到 tr上面,以方便js 获取

1
2
3
4
5
6
7
8
9
10
11
{% for board in boards %}
<tr data-id="{{ board.id }}" data-name="{{ board.name }}">
<td>{{ board.name }}</td>
<td>暂未实现</td>
<td>{{ board.create_time }}</td>
<td>
<button class="btn btn-default btn-xs edit-board-btn">编辑</button>
<button class="btn btn-danger btn-xs delete-board-btn">删除</button>
</td>
</tr>
{% endfor %}

编辑banners.js

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
$(function () {
$('.edit-board-btn').click(function () {
var self = $(this);
var tr = self.parent().parent();
var name = tr.attr('data-name');
var board_id = tr.attr('data-id');

xtalert.alertOneInput({
'text': '请输入新板块名称',
'placeholder': name,
'confirmCallback': function (inputValue) {
bbsajax.post({
'url': '/cms/uboard/',
'data': {
'board_id': board_id,
'name': inputValue
},
'success': function (data) {
if (data['code'] == 200) {
window.location.reload();
} else {
xtalert.alertInfo(data['message'])
}
}
});
}
});
})
});

$(function () {
$('.delete-board-btn').click(function () {
var self = $(this);
var tr = self.parent().parent();
var board_id = tr.attr('data-id');

bbsajax.post({
'url': '/cms/dboard/',
'data': {
'board_id': board_id
},
'success': function (data) {
if (data['code'] == 200) {
window.location.reload();
} else {
xtalert.alertInfo(data['message'])
}
}
});
});
});

前台页面完成

视图函数传递板块信息,编辑front_views.py

1
2
3
4
5
6
7
8
9
@bp.route('/')
def index():
banners = BannerModel.query.order_by(BannerModel.priority.desc()).all()
boards = BoardModel.query.all()
context = {
'banners': banners,
'boards': boards
}
return render_template('front/front_index.html', **context)

编辑front_index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="lg-container">
<div class="sm-container">
<div style="padding-bottom: 10px">
<button class="btn btn-warning btn-block">发布帖子</button>
</div>

<div class="list-group">
<a href="#" class="list-group-item active">所有板块</a>
{% for board in boards %}
<a href="#" class="list-group-item ">{{ board.name }}</a>
{% endfor %}
</div>
</div>
</div>
</div>

UEditor编辑器集成以及配置上传文件到七牛

相关链接
flask配置

编辑配置文件config.py,添加如下配置

1
2
3
4
5
6
7
8
9
#上传到本地
UEDITOR_UPLOAD_PATH = os.path.join(os.path.dirname(__file__),'images')

#上传到七牛
UEDITOR_UPLOAD_TO_QINIU = True #如果上传到七牛这里设置为True,上传到本地则为False
UEDITOR_QINIU_ACCESS_KEY = "xxxxxxx"
UEDITOR_QINIU_SECRET_KEY = "xxxxxxx"
UEDITOR_QINIU_BUCKET_NAME = "xxxxxx"
UEDITOR_QINIU_DOMAIN = "xxxxxxx"

flask需要有一个视图路由来处理,这里我们来配置一个蓝图,创建ueditor.py

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
#encoding: utf-8

from flask import (
Blueprint,
request,
jsonify,
url_for,
send_from_directory,
current_app as app
)
import json
import re
import string
import time
import hashlib
import random
import base64
import sys
import os
from urllib import parse
# 更改工作目录。这么做的目的是七牛qiniu的sdk
# 在设置缓存路径的时候默认会设置到C:/Windows/System32下面
# 会造成没有权限创建。
os.chdir(os.path.dirname(__file__))
try:
import qiniu
except:
pass
from io import BytesIO

bp = Blueprint('ueditor',__name__,url_prefix='/ueditor')

UEDITOR_UPLOAD_PATH = ""
UEDITOR_UPLOAD_TO_QINIU = False
UEDITOR_QINIU_ACCESS_KEY = ""
UEDITOR_QINIU_SECRET_KEY = ""
UEDITOR_QINIU_BUCKET_NAME = ""
UEDITOR_QINIU_DOMAIN = ""

@bp.before_app_first_request
def before_first_request():
global UEDITOR_UPLOAD_PATH
global UEDITOR_UPLOAD_TO_QINIU
global UEDITOR_QINIU_ACCESS_KEY
global UEDITOR_QINIU_SECRET_KEY
global UEDITOR_QINIU_BUCKET_NAME
global UEDITOR_QINIU_DOMAIN
UEDITOR_UPLOAD_PATH = app.config.get('UEDITOR_UPLOAD_PATH')
if UEDITOR_UPLOAD_PATH and not os.path.exists(UEDITOR_UPLOAD_PATH):
os.mkdir(UEDITOR_UPLOAD_PATH)

UEDITOR_UPLOAD_TO_QINIU = app.config.get("UEDITOR_UPLOAD_TO_QINIU")
if UEDITOR_UPLOAD_TO_QINIU:
try:
UEDITOR_QINIU_ACCESS_KEY = app.config["UEDITOR_QINIU_ACCESS_KEY"]
UEDITOR_QINIU_SECRET_KEY = app.config["UEDITOR_QINIU_SECRET_KEY"]
UEDITOR_QINIU_BUCKET_NAME = app.config["UEDITOR_QINIU_BUCKET_NAME"]
UEDITOR_QINIU_DOMAIN = app.config["UEDITOR_QINIU_DOMAIN"]
except Exception as e:
option = e.args[0]
raise RuntimeError('请在app.config中配置%s!'%option)

csrf = app.extensions.get('csrf')
if csrf:
csrf.exempt(upload)


def _random_filename(rawfilename):
letters = string.ascii_letters
random_filename = str(time.time()) + "".join(random.sample(letters,5))
filename = hashlib.md5(random_filename.encode('utf-8')).hexdigest()
subffix = os.path.splitext(rawfilename)[-1]
return filename + subffix


@bp.route('/upload/',methods=['GET','POST'])
def upload():
action = request.args.get('action')
result = {}
if action == 'config':
config_path = os.path.join(bp.static_folder or app.static_folder,'ueditor','config.json')
with open(config_path,'r',encoding='utf-8') as fp:
result = json.loads(re.sub(r'\/\*.*\*\/','',fp.read()))

elif action in ['uploadimage','uploadvideo','uploadfile']:
image = request.files.get("upfile")
filename = image.filename
save_filename = _random_filename(filename)
result = {
'state': '',
'url': '',
'title': '',
'original': ''
}
if UEDITOR_UPLOAD_TO_QINIU:
if not sys.modules.get('qiniu'):
raise RuntimeError('没有导入qiniu模块!')
q = qiniu.Auth(UEDITOR_QINIU_ACCESS_KEY,UEDITOR_QINIU_SECRET_KEY)
token = q.upload_token(UEDITOR_QINIU_BUCKET_NAME)
buffer = BytesIO()
image.save(buffer)
buffer.seek(0)
ret,info = qiniu.put_data(token,save_filename,buffer.read())
if info.ok:
result['state'] = "SUCCESS"
result['url'] = parse.urljoin(UEDITOR_QINIU_DOMAIN,ret['key'])
result['title'] = ret['key']
result['original'] = ret['key']
else:
image.save(os.path.join(UEDITOR_UPLOAD_PATH, save_filename))
result['state'] = "SUCCESS"
result['url'] = url_for('ueditor.files',filename=save_filename)
result['title'] = save_filename,
result['original'] = image.filename

elif action == 'uploadscrawl':
base64data = request.form.get("upfile")
img = base64.b64decode(base64data)
filename = _random_filename('xx.png')
filepath = os.path.join(UEDITOR_UPLOAD_PATH,filename)
with open(filepath,'wb') as fp:
fp.write(img)
result = {
"state": "SUCCESS",
"url": url_for('files',filename=filename),
"title": filename,
"original": filename
}
return jsonify(result)


@bp.route('/files/<filename>/')
def files(filename):
return send_from_directory(UEDITOR_UPLOAD_PATH,filename)

在主程序中注册蓝图

1
from ueditor import bp

编辑index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="{{ url_for('static',filename='ueditor/ueditor.config.js') }}"></script>
<script src="{{ url_for('static',filename='ueditor/ueditor.all.min.js') }}"></script>
<title>Title</title>
</head>
<body>
<script id="editor" type="text/plain">
</script>
<script>
var ue = UE.getEditor("editor",{
'serverUrl': '/ueditor/upload/'
});
</script>
</body>
</html>

-------------本文结束感谢您的阅读-------------