0、前言:
- ★这部分内容是基于之前Flask学习内容的一个实战项目梳理内容,没有可以直接抄下来跑的代码,是学习了之前Flask基础知识之后,再来看这部分内容,就会对Flask项目开发流程有更清楚的认知,对一些开发细节可以进一步的学习。
- 项目功能,通过Flask制作个人博客。
- 项目架构:项目分为两部分,第一部分是展示给别人的前端页面,第二部分是展示给自己的后台数据管理页面。
- 该项目是前后端不分离项目
- 在搭建一个项目之前一定要先把数据库设计好,把数据库中的表设计好,表和表之间的关系弄清楚
1、项目设计流程:
- 数据库设计 > 架构设计 > 代码实现
2、架构设计:
- 1、由于项目中视图函数可能会有多个,因此最好是通过设计views的python包来管理视图函数,因此在views包中有两个视图函数文件一个是views管理展示给别人的视图函数,views_admin管理展示给自己的后台数据管理的视图函数。
- 2、同理由于该项目需要也设置了models的python包将数据分为前端展示数据(models)和后台数据(models_admin)两部分。做完这两部分修改后,注意在初始化中修改导包,修改app绑定的蓝图,有了两个视图函数文件,就会有两个蓝图,一个蓝图叫blog管理前端页面,一个蓝图叫admin管理博客后台管理系统。
3、数据库设计:
- 1、models中数据表设计:【分类表(1):文章表(N)、相册表】
- 2、models_admin中数据表设计:【用户信息表】
4、项目框架展示
- 1、注意项目中用到的图片有两种添加方式,图片在数据表中是通过url的方式存储的,添加图片第一种方式是数据服务器,第二种方式是使用静态文件,一般正规公司项目开发都是使用静态文件的方式,通过数据服务器存放图片url。
- 2、因为views和models都分为了两个包,分别存放展示页面与后台管理页面相关内容,所以这两部分的页面和这两部分的数据库是分开的,因此这两部分视图函数也是分开的,功能上互不干预。
5、项目主要文件代码展示
- 1、app
# Flask个人博客项目
from App import creat_app
app = creat_app()
if __name__ == '__main__':
app.run(debug=True)
- 2、models
# models.py : 模型,数据库
'''
模型 === 数据库
类 ——> 表结构
类属性 ——> 表字段
一个对象 ——> 表的一行数据
'''
from ..exts import db # 导入db对象就能通过python实现ORM技术,避免了写SQL语句。
# 分类数据表
class CategoryModel(db.Model):
# 表名
__tablename__ = 'tb_category' # 数据迁移就是让模型变成表,ORM就是让类变成模型
# 定义表字段
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(30), unique=True)
describe = db.Column(db.Text(), default='describe')
# 关联外键(可以在分类表中获取对应的所有文章)
articles = db.relationship('ArticleModel', backref='category', lazy='dynamic')
# 文章数据表
class ArticleModel(db.Model):
# 表名
__tablename__ = 'tb_article' # 数据迁移就是让模型变成表,ORM就是让类变成模型
# 定义表字段
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(30), unique=True)
keyword = db.Column(db.String(255), default='keyword')
content = db.Column(db.Text(), default='content')
img = db.Column(db.Text(), default='img')
# 外键
category_id = db.Column(db.Integer, db.ForeignKey(CategoryModel.id))
# 相册
class PhotoModel(db.Model):
# 表名
__tablename__ = 'tb_photo'
# 定义表字段
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
url = db.Column(db.Text())
name = db.Column(db.String(30), unique=True)
describe = db.Column(db.Text(), default='describe')
- 3、models_admin
from ..exts import db # 导入db对象就能通过python实现ORM技术,避免了写SQL语句。
# 模型Model:类
# 必须继承 db.Model User才能从普通的类变成模型
class AdminUserModel(db.Model):
# 表名
__tablename__ = 'tb_adminuser' # 数据迁移就是让模型变成表,ORM就是让类变成模型
# 定义表字段
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(30), unique=True)
passwd = db.Column(db.String(30))
# 通过orm技术得到的db,就是用于替代数据库,后面用到数据库相关操作,可以检索。
- 4、views
# 在views.py中放路由和视图函数
from flask import Blueprint, render_template, request
from ..models.models import * #后面是用views来控制数据库的,所以要在views中导入models文件
# 蓝图(蓝图可以有多个,方便对路径进行区分)
blog = Blueprint('blog', __name__)
# 博客首页
@blog.route('/')
@blog.route('/index/')
def index():
# 修改首页中“我的相册”当中的6张图,调用photos数据库中前6张图片
# 修改首页中“文章分类”
# 修改首页中“iKun推荐”
# 修改首页中右侧的文章
photos = PhotoModel.query.limit(6)
categories = CategoryModel.query.all()
articles = ArticleModel.query.all()
commend_arc = articles[:4]
return render_template('home/index.html',
photos=photos,
categories=categories,
articles=commend_arc,
arts = articles)
# 我的相册
@blog.route('/photos/')
def blog_photos():
photos = PhotoModel.query.all()
return render_template('home/photos.html', photos = photos)
# 我的日记
@blog.route('/article/')
def blog_aritcle():
articles = ArticleModel.query.all()
categories = CategoryModel.query.all()
return render_template('home/article.html', articles = articles, categories = categories)
# 关于我
@blog.route('/about/')
def blog_about():
photos1 = PhotoModel.query.all()
categories = CategoryModel.query.all()
photos = photos1[:6]
return render_template('home/about.html', photos = photos, categories = categories)
- 4、views_admin
# 在views.py中放路由和视图函数
from flask import Blueprint, render_template, request, redirect, jsonify
from ..models.models_admin import * #后面是用views来控制数据库的,所以要在views中导入models文件
from ..models.models import * # 首页面需要查询models当中的表
import time
# 蓝图(蓝图可以有多个,方便对路径进行区分)
admin = Blueprint('admin', __name__)
# ---------------------- 后台管理 ----------------------
# 装饰器:做登录验证,就不用每次都写一遍登陆验证了
from functools import wraps # 为了在多个函数使用装饰器时,能够让函数名正确传递
def login_required(fn):
@wraps(fn)
def inner(*args, **kwargs):
# 判断用户是否登录
# 获取cookie,得到登录的用户
user_id = request.cookies.get('user_id', None)
if user_id:
# 登陆过,进入后台管理系统
user = AdminUserModel.query.get(user_id)
request.user = user
return fn(*args, **kwargs) # 登陆过就去调用对应的视图函数
else:
# 没有登陆过,跳转到登录页面
return redirect('/admin/login/')
return inner
# 后台管理——首页面
@admin.route('/admin/')
@admin.route('/admin/index/')
@login_required
def index():
# user_id = request.cookies.get('user_id', None) # 默认值给空,如果没有的话,默认为空
# if user_id:
# # 登陆过或登录成功,有cookie的情况
# user = AdminUserModel.query.get(user_id)
# categorys = CategoryModel.query.filter()
# articles = ArticleModel.query.filter()
# photos = PhotoModel.query.filter()
# return render_template('admin/index.html',
# username = user.name,
# categorys=categorys,
# articles=articles,
# photos=photos)
# else:
# return redirect('/admin/login/')
user = request.user
categorys = CategoryModel.query.filter()
articles = ArticleModel.query.filter()
photos = PhotoModel.query.filter()
return render_template('admin/index.html',
username=user.name,
categorys=categorys,
articles=articles,
photos=photos)
# 后台管理——登录
@admin.route('/admin/login/',methods=['GET', 'POST'])
def admin_login():
if request.method == 'GET': # 网页向服务器请求数据
return render_template('admin/login.html')
elif request.method == 'POST': # 网页向服务器提交数据
username = request.form.get('username') # 用户名
userpwd = request.form.get('userpwd') # 密码
# 登陆验证
user = AdminUserModel.query.filter_by(name = username, passwd = userpwd).first()
if user:
# 登陆成功
response = redirect('/admin/index/')
'''
1、回顾下cookie的工作流程:前端发送请求-后端返回前端一个cookie-前端保存cookie-前端在cookie有效期
登录可以免密登录。
2、注意下面设置response的cookie中参数时,'user_id'是cookie的名称, str(user.id)是cookie的值,
cookie值要求必须是每个用户独一无二的。
'''
response.set_cookie('user_id', str(user.id), max_age=7*12*3600) # cookie设置7天有效
return response
else:
return 'login failed'
# 后台管理——退出登录
@admin.route('/admin/logout/')
def admin_logout():
response = redirect('/admin/login/')
response.delete_cookie('user_id')
return response
# ---------------------- 后台管理——分类管理 ----------------------
@admin.route('/admin/category/')
@login_required
def admin_category():
user = request.user
categories = CategoryModel.query.all()
return render_template('/admin/category.html',
username = user.name,
categories = categories)
# 分类页面添加分类功能实现
@admin.route('/admin/addcategory/', methods=['GET','POST'])
@login_required
def add_category():
if request.method == 'POST':
name = request.form.get('name')
describe = request.form.get('describe')
# 添加分类(在数据表中创建一条数据就是创建一个对象)
category = CategoryModel()
category.name = name
category.describe = describe
try:
db.session.add(category)
db.session.commit()
except Exception as e:
print('e',e)
db.session.rollback()
return redirect('/admin/category/') # 不论成功还是失败,重新跳转,相当于刷新了页面
return '请求方式错误'
# 分类页面删除分类功能实现
@admin.route('/admin/delcategory/', methods=['GET','POST'])
@login_required
def del_category():
if request.method == 'POST':
# 先查询
id = request.form.get('id')
# print(id)
category = CategoryModel.query.get(id)
# 再删除
try:
db.session.delete(category)
db.session.commit()
except Exception as e:
print('e:',e)
return jsonify({'code':200,'msg':'删除成功!'})
else:
return jsonify({'code':400,'msg':'请求方式错误!'})
# 分类页面修改分类功能实现
@admin.route('/admin/updatecategory/<id>/', methods=['GET','POST'])
@login_required
def update_category(id):
user = request.user
if request.method == 'GET':
category = CategoryModel.query.get(id)
return render_template('admin/category_update.html',
username = user.name,
category=category)
elif request.method == 'POST':
name = request.form.get('name')
describe = request.form.get('describe')
# 修改
category = CategoryModel.query.get(id)
category.name = name
category.describe = describe
# 提交
try:
db.session.commit()
except Exception as e:
print('e:',e)
db.session.rollback()
return redirect('/admin/category/')
else:
return "请求方式错误!"
# ---------------------- 后台管理——文章管理 ----------------------
# 文章管理
@admin.route('/admin/article/')
@login_required
def admin_article():
user = request.user
articles = ArticleModel.query.all()
return render_template('/admin/article.html',
username = user.name,
articles = articles)
# 删除文章
@admin.route('/admin/delarticle/', methods=['GET','POST'])
@login_required
def del_article():
if request.method == 'POST':
id = request.form.get('id')
article = ArticleModel.query.get(id)
try:
db.session.delete(article)
db.session.commit()
except Exception as e:
print('e:',e)
db.session.rollback()
return jsonify({'code': 500, 'msg': '删除失败'})
return jsonify({'code':200, 'msg':'删除文章成功' })
return jsonify({'code':400, 'msg':'请求方式错误!' })
# 添加文章
@admin.route('/admin/addarticle/', methods=['GET','POST'])
@login_required
def add_article():
if request.method == 'GET':
categories = CategoryModel.query.all()
articles = ArticleModel.query.all()
return render_template('/admin/article_add.html',
username = request.user.name,
articles = articles,
categories = categories)
elif request.method == 'POST':
# 添加文章
name = request.form.get('name')
keywords = request.form.get('keywords')
content = request.form.get('content')
category = request.form.get('category')
img = request.files.get('img')
# 图片存储路径
img_name = f'{time.time()}-{img.filename}'
img_url = f'/static/home/uploads/{img_name}'
# 添加文章
try:
article = ArticleModel()
article.name = name
article.keyword = keywords
article.content = content
article.img = img_url
article.category_id = category
db.session.add(article)
db.session.commit()
except Exception as e:
print('e:',e)
db.session.rollback()
db.session.flush()
else:
# 如果添加数据库成功,就要把图片存入本地
img_data = img.read()
with open(f'App/{img_url}','wb') as fp:
fp.write(img_data)
fp.flush()
return redirect('/admin/article')
# 修改文章
@admin.route('/admin/updatearticle/<id>/', methods=['GET','POST'])
@login_required
def upddate_article(id):
article = ArticleModel.query.get(id)
if request.method == 'GET':
categories = CategoryModel.query.all()
return render_template('/admin/article_update.html/',
username=request.user.name,
categories=categories,
article=article)
elif request.method == 'POST':
# 修改文章
name = request.form.get('name')
keywords = request.form.get('keywords')
content = request.form.get('content')
category = request.form.get('category')
img = request.files.get('img')
# 图片存储路径
img_name = f'{time.time()}-{img.filename}'
img_url = f'/static/home/uploads/{img_name}'
# 添加文章
try:
article.name = name
article.keyword = keywords
article.content = content
article.img = img_url
article.category_id = category
db.session.commit()
except Exception as e:
print('e:', e)
db.session.rollback()
db.session.flush()
else:
# 如果添加数据库成功,就要把图片存入本地
img_data = img.read()
with open(f'App/{img_url}', 'wb') as fp:
fp.write(img_data)
fp.flush()
return redirect('/admin/article')
- 5、exts
from flask_sqlalchemy import SQLAlchemy # orm技术
from flask_migrate import Migrate # 数据迁移技术
db = SQLAlchemy()
migrate = Migrate()
def init_exts(app):
db.init_app(app=app)
migrate.init_app(app=app, db=db)
- App的init
# __init__.py : 初始化文件,创建Flask应用
from flask import Flask
from .views.views import blog
from .views.views_admin import admin
from .exts import init_exts
def creat_app():
app = Flask(__name__)
# 注册蓝图
app.register_blueprint(blueprint=blog)
app.register_blueprint(blueprint=admin)
# 配置数据库(配置不同数据库软件,就要用不同配置,配置的目的,就是在用到数据库的时候让项目知道找什么数据库,去哪找数据库)
# db_uri = 'sqlite:///sqlite3.db'
db_uri = 'mysql+pymysql://root:123456@localhost:3306/blogdb' # mysql的配置
app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 禁止对象追踪修改(为了不浪费服务器资源进行的设置)
# 初始化插件
init_exts(app=app)
return app
6、用户管理页面与展示页面设计:
- 1、可以先把展示页面设计出来,放上写死的数据,然后通过项目的视图函数调用调试,让前端页面先跑通,然后再给后端添加测试数据,通过结合模板语言修改前端代码,实现后台数据与前端页面的交互展示。【注意:前后端交互本质,就是将后端数据库当中数据表的数据拿到后,准确给到前端对应位置即可,因为有时候传递数据较多,通过模板语言就能更有逻辑的显示后端传递过来的数据,放到前端对应位置。】
- 2、其中分为admin(用户管理页面)、home(展示页面),展示页面中index是首页,photos是相册页面,article是我的日记页面,about是关于我页面;用户管理页面中index是后台管理的首页面,login是后台管理的登录页面,category是分类管理页面,category_update是在category页面中点击修改分类后跳转的页面,article是文章,article_add是添加文章,article_update是修改文章数据表。
7、展示页面数据交互方式
- 1、展示页面数据交互流程图
- 2、前端页面的修改其实很简单,就是在views(视图函数)中通过数据表查询到需要传递的数据,然后在前端中通过“模板语言”把传递过来的数据渲染到前端页面即可,如下所示就是一个前端页面渲染的例子:
8、用户管理页面实现:增、删、改、查
8.1、关键问题概述
- 1、后端页面设计的登录页面中,关于cookie的设置和返回问题,可以参考“Flask会话技术”,设置了cookie,就可以保持登录一段时间,下次登录时间如果在保持登录时间内,就能用cookie登录进去,只有通过退出登录,才能删除cookie。
- 2、后端数据往前端传递的过程:先查询到相关数据,然后通过render_template通过逗号隔开挨个传递数据。
- 3、前端往后端传递数据的过程:先通过flask当中内置的request对象访问客户端发送给服务器的请求信息,然后将对应的信息通过flask当中的表操作,传递给数据表。
- 4、前端向后端传递数据的方式是:POST;后端向前端传递参数,前端获取数据的方式是:GET,写视图函数的时候,通过这两个参数区分。
- 5、图片通过网页上传到后端,后端可以给图片存储到项目路径下的文件夹当中。 但是要注意同一文件夹中不能有名字相同的图片,所以可以导入时间,将每个文件上传的时间戳加文件名,就不会有重名了。 一般是先将图片的url存放到数据表中,然后通过文件操作,手动将文件存放到对应url中。
8.2、关键问题详述
- 1、如何实现后台登录验证:
如下面的代码中,首先通过装饰器模板写好内部函数inner,在inner中进行判断,如果从cookies中得到了user_id,就证明了之前有登陆过,就可以获取user_id,否则就返回None给到user_id。如果有登录过,就从inner函数中调用外部函数fn,fn就是对应跳转页面的视图函数,就能够跳转到相应的页面中。下面的首页面加了登录验证的装饰器,就可以做登录判断了,当然也可以每个视图函数都写一次登录判断。
# 装饰器:做登录验证,就不用每次都写一遍登陆验证了
from functools import wraps # 为了在多个函数使用装饰器时,能够让函数名正确传递
def login_required(fn):
@wraps(fn)
def inner(*args, **kwargs):
# 判断用户是否登录
# 获取cookie,得到登录的用户
user_id = request.cookies.get('user_id', None)
if user_id:
# 登陆过,进入后台管理系统
user = AdminUserModel.query.get(user_id)
request.user = user
return fn(*args, **kwargs) # 登陆过就去调用对应的视图函数
else:
# 没有登陆过,跳转到登录页面
return redirect('/admin/login/')
return inner
# 登录首页的视图函数
@admin.route('/admin/')
@admin.route('/admin/index/')
@login_required
def index():
user = request.user
categorys = CategoryModel.query.filter()
articles = ArticleModel.query.filter()
photos = PhotoModel.query.filter()
return render_template('admin/index.html',
username=user.name,
categorys=categorys,
articles=articles,
photos=photos)
- 2、如何实现对数据表的添加操作:
以该项目中的 类型数据表 为例,如下图,可以看到该数据库中主要的字段有三个:id、name、describe,由于id是自增长的,所以只需要有name和describe就可以添加一条数据给到数据表。
因此,后台管理页面中,给 类型数据表 添加新类型,就需要设计一个新的页面来收集name和describe这两个字段,如下图所以就是给 类型数据表 添加新类型的页面。
当然,这个功能也一定是通过Flask当中的视图函数实现的。如下所示的视图函数add_category就实现了该功能,首先,添加路由装饰器和登陆验证装饰器,因为是添加数据,需要把页面表单中的数据提交到后台的视图函数中,所以视图函数首先会判断request的方式是不是POST,如果是,就说明表单正在往视图函数提交数据,这个时候,视图函数就可以通过request(全局对象,在flask处理请求时自动创建)获取到表单当中提交过来的数据,然后创建数据表对象,给字段赋值,然后尝试添加到数据表当中即可。添加完成后,重定向到分类页面,重定向就可以重新加载一次,就能够看到我们添加数据的操作是否成功。
# 以下代码写在views_admin.py当中
# 分类页面添加分类功能实现
@admin.route('/admin/addcategory/', methods=['GET','POST'])
@login_required
def add_category():
if request.method == 'POST':
name = request.form.get('name')
describe = request.form.get('describe')
# 添加分类(在数据表中创建一条数据就是创建一个对象)
category = CategoryModel()
category.name = name
category.describe = describe
try:
db.session.add(category)
db.session.commit()
except Exception as e:
print('e',e)
db.session.rollback()
return redirect('/admin/category/') # 不论成功还是失败,重新跳转,相当于刷新了页面
return '请求方式错误'
这个过程也是需要前端代码配合才行,如下就是前端表单的html代码,可以看到,表单中两处input都是通过name命名的,这样,就能在视图函数中通过request获取对应的表单内容。
<form action="/admin/addcategory/" method="post" autocomplete="off">
<div class="form-group">
<label for="category-name">分类名称</label>
<input type="text" id="category-name" name="name" class="form-control"
placeholder="在此处输入分类名称" required autocomplete="off">
</div>
<div class="form-group">
<label for="category-describe">分类描述</label>
<textarea class="form-control" id="category-describe" name="describe" rows="4"
autocomplete="off"></textarea>
</div>
<button class="btn btn-primary" type="submit" name="submit">添加新分类</button>
</form>
- 3、如何实现对数据表的删除操作:
同样以该项目中的 类型数据表 为例,如下图所示,就是分类管理中展示的所有 类型数据表 的值,通过html当中的a标签实现删除操作
首先通过html当中的代码,可以看出,删除操作相对复杂,需要通过js向视图函数提交要删除数据的id,然后再接收来自视图函数的消息,判断是否删除成功,如果删除成功,就在js当中刷新页面。
{% for category in categories %}
<tr>
<td>{{ category.id }}</td>
<td>{{ category.name }}</td>
<td>
<a href="/admin/updatecategory/{{ category.id }}/">修改</a>
<a href="#" cid="{{ category.id }}">删除</a>
</td>
</tr>
{% endfor %}
<script>
//是否确认删除
$(function () {
$("#main table tbody tr td a").click(function () {
var that = $(this);
var id = that.attr("cid"); //对应id
if (event.srcElement.outerText === "删除") {
if (window.confirm("此操作不可逆,是否确认?")) {
$.post('/admin/delcategory/', {'id': id}, function (data){
console.log(data.msg)
if (data.code == 200){
location.reload()
}
})
}
}
})
});
</script>
对于视图函数而言,删除操作就是只需要知道你想删除的数据在数据表中对应的id,然后通过表操作直接删除该数据即可。
# 以下代码写在views_admin.py当中
# 分类页面删除分类功能实现
@admin.route('/admin/delcategory/', methods=['GET','POST'])
@login_required
def del_category():
if request.method == 'POST':
# 先查询
id = request.form.get('id')
# print(id)
category = CategoryModel.query.get(id)
# 再删除
try:
db.session.delete(category)
db.session.commit()
except Exception as e:
print('e:',e)
return jsonify({'code':200,'msg':'删除成功!'})
else:
return jsonify({'code':400,'msg':'请求方式错误!'})
- 4、如何实现对数据表的修改操作:
同样以该项目中的 类型数据表 为例,参考 对数据表的删除操作 讲解中的前端页面截图和html代码即可,可以知道,通过模板语言加载过程中,就已经可以通过 category.id 拿到分类表中要修改的数据的id了,通过a连接跳转到路由 /admin/updatecategory/{{ category.id }}/ 对应的新页面,这个过程中带着参数即可,然后这个参数就会被传递到该路由对应的视图函数当中,视图函数如下,注意路由写的时候也要带着参数,由于a标签打开路由页面的请求是GET请求,所以在判断请求方式是GET时,就能获取到传递过来的参数id,然后通过id,查找到对应的数据表当中的数据对象。
# 以下代码写在views_admin.py当中
# 分类页面修改分类功能实现
@admin.route('/admin/updatecategory/<id>/', methods=['GET','POST'])
@login_required
def update_category(id):
user = request.user
if request.method == 'GET':
category = CategoryModel.query.get(id)
return render_template('admin/category_update.html',
username = user.name,
category=category)
elif request.method == 'POST':
name = request.form.get('name')
describe = request.form.get('describe')
# 修改
category = CategoryModel.query.get(id)
category.name = name
category.describe = describe
# 提交
try:
db.session.commit()
except Exception as e:
print('e:',e)
db.session.rollback()
return redirect('/admin/category/')
else:
return "请求方式错误!"
然后,将查到的数据同模板(admin/category_update.html)一同打开,如下图所示就是修改页面中,对应修改数据填充后的展示结果。
admin/category_update.html(修改页面)中html就会有一个表单,类似添加页面,在写好之后,点击更新分类,就会修改对应的数据表当中的数据,点击更新分类,前端页面就会提交表单到路由函数update_category中,此时请求方式就是POST请求,然后通过request获取对应的数据,再查询数据表得到对应的数据对象,替换其字段的值即可完成修改,修改完成后,重定向刷新页面。
总结:
- 1、项目框架确定后,搭建项目时,一定要注意修改模板,如果有多个视图函数,还得在init中创建对应数量的蓝图。
- 2、展示页面的前后端数据交互相对简单。
- 3、后台管理页面,实现增删改查有对应模板套用即可,单删除操作相对复杂。