千哥读书笔记:《Flask2+Vue.js实战派》第8章源代码深入解析

实战派 ESP32-S3,双模无线开发板

ESP32-S3 原生支持 ESP-IDF,WiFi + 蓝牙一次搞定

杨永刚老师所著的《Flask2+Vue.js实战派》结构清晰、内容由浅入深,是市面上为数不多的、全面介绍Flask框架的好教程。不过,由于纸版书籍篇幅的限制,加之每一个学习者的知识储备不同,这本书对一些技术细节的介绍不可能做到全全俱到。
为此,千哥将陆续将本书的一些学习心得通过笔记的形式呈现出来,既为开发Flask程序深入掌握这些知识,也为共同学习的读者留下更多的参考资料。
本书的源代码下载地址:https://pan.baidu.com/s/1WSvlR4qT0fcxVzksto6mtg
提取码: aabb
本书实例的开发环境为:Windows10 / Linux 、Python 3.9.13、Flask2.32。在安装完Python之后,可以通过书中的requirements.txt文件,一次性用pip install -r requirements.txt命令,将相关模块安装完。
《Flask2+Vue.js实战派》第8章是全书中重要的、承上启下的章节,通过“使用Flask+Bootstrap框架开发图书管理系统后台”,将前面7章的知识进行融汇贯通,并为后续的学习打好坚实的基础。第8章程序运行后的界面如下:

本章所涉及的知识内容如下图所示:

既然是读书笔记,在这里我只记录我认为重要的,或者书中没有的内容,而且内容结构不一定按照《Flask2+Vue.js实战派》的编排方式,感兴趣的朋友也可以根据自己的需要对本笔记进行收藏和补充。
一、代码组织管理
本章的“图书管理系统后台”,不再采用简单的“app.py(文件) + templates(目录)”形式,即:由app.py文件编写配置信息、路由和视图、各类模型、模板配置等,templates目录存放模板文件、静态文件等。这种代码组织的方式在第8章以前普遍采用,本地调试的时候可以用两种方式来执行程序:
python app.y
或者:
flask run
对于第二种方式,fask会自动寻找名为app.py的文件并且运行,还可以加入--debug选项,开启debug模式:
flask run --debug
如下图:

还可以用flask routes 命令来查看视图:

而第8章则采取了包的方式来组织管理代码,将配置信息、路由和视图、各类模型放在不同的python文件中,再分别导入和使用。
在mybook文件夹下,有三个重要的文件和文件夹:config.py、manage.py和apps:

其中config.py是基本的Flask配置文件,内容大致如下,部分代码省略:

import os

base_dir = os.path.abspath(os.path.dirname(__file__))

SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(base_dir, 'data1.sqlite')
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_COMMIT_TEARDOWN = True
#app.secret_key="1432rsaasfdasf"
SECRET_KEY = "12345678!@#$%^&*"

class BaseConfig(object):
    PORT = 5000
    APP_NAME = "flask-app"
    JSON_AS_ASCII = False  # 禁止中文转义
    # 因为flask的session是通过加密之后放到了cookie中。所以有加密就有密钥用于解密,所以,只要用到了flask的session模块就一定要配置“SECRET_KEY”这个全局宏。一般设置为24位的字符
    SECRET_KEY = os.urandom(24)
    # 权限白名单列表
    WHITE_LIST = ["/sys/user/login", "/callback/gitee"]
    # 环境变量中获取DB_NAME
    DB_NAME = os.getenv("DB_NAME")
    if DB_NAME is None:
        DB_NAME = "flaskrbac"
    # 环境变量中获取数据库DB_HOST
    DB_HOST = os.getenv("DB_HOST", "172.16.31.160")
    # 环境变量中获取数据库DB_PORT
    DB_PORT = os.getenv("DB_PORT", "3306")
    # 环境变量中获取数据库DB_USER
    DB_USER = os.getenv("DB_USER", "root")
    # 环境变量中获取数据库DB_PASSWORD
    DB_PASSWORD = os.getenv("DB_PASSWORD", "8Eli#gr#AUk")
    # 环境变量中获取数据库REDIS_HOST
    REDIS_HOST = os.getenv("REDIS_HOST", "172.16.31.160")
    # 环境变量中获取数据库REDIS_PORT
    REDIS_PORT = os.getenv("REDIS_PORT", "6379")
    # 环境变量中获取数据库REDIS_PASSWORD
    REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "gEwjEHLM")
    # mysql+pymysql://root:xxxxx@172.16.31.160/flaskrbac?charset=utf8mb4
    SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{DB_USER}:{parse.quote_plus(DB_PASSWORD)}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset=utf8mb4'
    PERMANENT_SESSION_LIFETIME = datetime.timedelta(days=90)
    SQLALCHEMY_TRACK_MODIFICATIONS = True
    SQLALCHEMY_RECORD_QUERIES = True
    SQLALCHEMY_ENGINE_OPTIONS = {
        'pool_size': 5,
        'pool_timeout': 90,
        'pool_recycle': 7200,
        'max_overflow': 1024
    }
    # REDIS_URL = "redis://:password@localhost:6379/0"
    REDIS_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/0'
    # 回调配置
    # 回调密码
    GITEE_CALLBACK_PASSWORD = os.getenv("GITEE_CALLBACK_PASSWORD", "666")
    # 回调执行的脚本
    GITEE_CALLBACK_SHELL = os.getenv("GITEE_CALLBACK_SHELL",
                                     "/home/admin/app/restart.sh")
#以下代码省略
#………
config = {
    "dev": Development,
    "development": Development,
    "test": Test,
    "prod": Production,
    "production": Production
}


代码中,包括数据库配置方式,用于加密会话数据、保护 CSRF 令牌的SECRET_KEY的设置等。
manage.py是启动文件,采取:python manage.py 运行程序。当然,也可以按照下面的方法,采取:flask run 运行:

export FLASK_APP=manage.py   # Linux/macOS
set FLASK_APP=manage.py      # Windows
flask run


manage.py里的代码很简单,如下:

from apps import app
#db.drop_all()
#db.create_all()

if __name__ == "__main__":
    app.run(debug=True, port=9000)

这里需要注意的是:

from apps import app


这条语句是从apps这个目录中导入一个名为app的Flask类的对象实例,由这个实例的app.run方法运行程序。
在apps目录下,有一个__init__.py文件,__init__.py中的代码如下:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os
import config
base_dir=os.path.abspath(os.path.dirname(__file__))
print(base_dir)

app=Flask(__name__)
app.config.from_object(config)
db=SQLAlchemy(app)


from apps import views


在manage.py中,名为app的Flask类的对象实例,由__init__.py中的语句:app=Flask(__name__) 生成。再由__init__.py中的语句:app.config.from_object(config)导入config.py文件的配置信息。
这里需注意的是:


1、app.config.from_object(config)这条语句:from_object(config)中的config指的是config.py模块,加载模块级变量,因此config.py中的BaseConfig类和其他类,以及包含有"dev"、 "development"、"test"、"prod"、"production"四种模式的字典变量config都没有使用。也就是config.py文件中,有效代码实际上只有这部分:

import os

base_dir = os.path.abspath(os.path.dirname(__file__))

SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(base_dir, 'data1.sqlite')
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_COMMIT_TEARDOWN = True
#app.secret_key="1432rsaasfdasf"
SECRET_KEY = "12345678!@#$%^&*"


而如果要使用config.py中的BaseConfig类和其他类,以及字典变量config,需在__init__.py中使用如下语句:

# 根据环境变量选择配置类,例如 FLASK_ENV=dev
env = os.environ.get("FLASK_ENV", "dev")
app.config.from_object(config[env])  # 加载 Development 类
或者:
# 加载 Development 类
app.config.from_object(config["dev"])


并且还要防止模块级变量覆盖BaseConfig类和其他类的同名变量。例如config.py文件中的模块级变量SQLALCHEMY_DATABASE_URI就和BaseConfig类中的变量SQLALCHEMY_DATABASE_URI同名。
因此,在app.config.from_object这条语句的时候,必须选择其中的一种模式:app.config.from_object(config)或app.config.from_object(config["dev"])。前者是使用所有的模块级变量,后者是使用config字典中的"dev"模式(也可以选择其他模式)。


2、用于加密会话数据、保护 CSRF 令牌的SECRET_KEY,可以在执行app.config.from_object语后,用print在后台打印出来检查是否有效:

app.config.from_object(config)
print("SECRET_KEY:", app.config["SECRET_KEY"])


3、__init__.py文件的作用,主要便于用包的方式来组织管理代码。如果一个目录包含__init__.py,python会将这个目录视为一个包,允许通过包的方式导入其内部的模块、类、函数或者对象。如果没有__init__.py这个文件,这个目录下的文件会当成普通文件,python无法直接导入相关的模块。
在这个例子中,正因为apps目录下,包含了__init__.py文件,所以python就能导入apps目录下的相关模块。apps的目录结构如下:
.
└── apps
    ├──  __init__.py
    ├── forms.py
    ├── models.py
    └── views.py
    └── static
    └── templates
其中 views.py编写的是路由和视图函数,models.py编写的是数据模型,forms.py编写的是Flask的表单类,templates和static分别是模板文件夹和静态文件夹。
__init__.py文件的最后一条语句:

from apps import views


就导入了views模块,从而可以使用 views.py中的路由和视图函数。views.py的部分代码如下:

from apps import app, db
from flask import request, render_template, redirect, url_for, flash, jsonify
from sqlalchemy.exc import SQLAlchemyError
from apps.models import Publish, Author, Book
from apps.forms import PublishForm, AuthorForm, BookForm
from werkzeug.datastructures import CombinedMultiDict
import os

base_dir = os.path.abspath(os.path.dirname(__file__))


@app.route("/pub_list", methods=["GET", "POST"])
def pub_list():
    if request.method == "GET":
        pubs = Publish.query.all()
        return render_template("publish/index.html", pubs=pubs)
    if request.method == "POST":
        name = request.form.get("name")
        if name:
            pubs = Publish.query.filter(Publish.name.ilike(f'%{name}%'))
        else:
            pubs = Publish.query.all()
        return render_template("publish/index.html", pubs=pubs)
        
#以下代码省略
#………


需注意的是这两行代码:

from apps.models import Publish, Author, Book
from apps.forms import PublishForm, AuthorForm, BookForm


通过这两行代码,就导入了models.py中的Flask的数据库模型类,forms.py中的Flask的表单类。当然,也可以用点号.进行相对导入:

from .models import Publish, Author, Book
from .forms import PublishForm, AuthorForm, BookForm


对于上述代码组织关系,如下图所示:

至于其他的模块的组织和导入方式,基本都是常规的方式,不再赘述。


二、后端需重点关注的代码
对于后端的Flask代码,需重点关注views.py文件中的author_list()、book_add()、book_edit() 、index() 四个视图函数。下面,结合models.py文件(数据模型类文件)、froms.py(表单类文件)进行分析。
(一)视图函数author_list()
代码如下:

# 作者相关
@app.route("/author_list", methods=["GET", "POST"])
def author_list():
    if request.method == "GET":
        name = request.args.get("name", "")
        mobile = request.args.get("mobile", "")
        search = dict()
        if name:
            search["name"] = name
        if mobile:
            search["mobile"] = mobile
        page = request.args.get("page", 1, type=int)
        pagination = Author.query.filter_by(**search).paginate(page,
                                                               per_page=2,
                                                               error_out=False)
        authors = pagination.items
        return render_template("author/index.html",
                               authors=authors,
                               pagination=pagination,
                               name=name,
                               mobile=mobile)


author_list() 的作用:
1、如果前端没有通过表单(name和mobile)提交查询数据,那么就通过pagination = Author.query.filter_by语句返回所有数据并进行分页。
2、如果有查询数据,那么就通过pagination = Author.query.filter_by语句,调用search字典返回查询结果并进行分页。


这里需要注意的是:


1、request.args.get()

name = request.args.get("name", "")
mobile = request.args.get("mobile", "")


- 作用:从 URL 的查询字符串中获取名为 name 和mobile的参数值。
例如:访问
/author_list?name=张三&mobile=13999888888 时:
- request.args.get("name", ""),会返回 "张三"
- request.args.get("mobile", ""),会返回 "13999888888"
- 参数:
- 第一个参数:要获取的键名(name)。
- 第二个参数:默认值(如果未找到name 参数,则返回空字符串 "")。
- 返回值:字符串类型(即使参数不存在或未传值,也能避免 KeyError)
- request 对象的使用场景:
① GET 请求参数(URL 参数)
- 通过request.args 字典获取 URL 中的查询参数:
page = request.args.get("page", 1, type=int)  # 获取 page 参数,默认值 1,类型转为 int
② POST 请求的表单数据
- 通过request.form 字典获取表单提交的数据:
username = request.form.get("username")
password = request.form.get("password")
③ JSON 数据
- 通过request.json 直接获取请求体中的 JSON 数据:
data = request.json
④ 上传文件
- 通过 request.files 字典获取上传的文件:
file = request.files.get("file")
file.save("path/to/save")
⑤ 请求头信息
- 通过request.headers 获取请求头:
user_agent = request.headers.get("User-Agent")
本例中,name和mobile在通过request.args.get获取时,需在前端的表单中,将form的 method设置为get:

<form id="search_form" method="get">


2、search = dict()
search变量是一个字典变量,用于接收name和mobile参数的数据:

if name:
    search["name"] = name
if mobile:
    search["mobile"] = mobile


如果name和mobile参数没有数据,search则是一个空字典。


3、pagination = Author.query.filter_by(**search).paginate(page, per_page=2,error_out=False)


其中,**search将字典数据传递到Author.query.filter_by中,如果字典为空,filter_by(**search)等价于无过滤条件Author.query.all(),最终查询的是Author表中的全部数据。
这里还涉及到Flask分页的知识,pagination对象分页后的结果包含以下关键属性:
- items: 当前页的数据列表(如 [Author1, Author2])。
- page: 当前页码。
- per_page: 每页显示的条目数。
- total: 总条目数。
- pages: 总页数。
- has_prev: 是否有上一页。
- has_next: 是否有下一页。
- prev_num: 上一页的页码。
- next_num: 下一页的页码。


对于接下来的代码来说:

authors = pagination.items
return render_template("author/index.html",
                               authors=authors,
                               pagination=pagination,
                               name=name,
                               mobile=mobile)


pagination.items 是分页查询的核心,直接提供当前页的数据列表。pagination.items 是一个包含当前页数据的 Python 列表,列表中的每个元素对应数据库查询结果的一行记录(例如 Author 模型对象)。
例如:
- 如果每页显示 2 条数据(per_page=2),当用户访问第 1 页时,items 会返回前 2 条数据。
- 如果用户访问第 3 页,items 会返回第 5~6 条数据(假设总共有 6 条数据)
至此,视图函数author_list()分析完毕。


(二)视图函数book_add()
代码如下:
 

@app.route("/book_add", methods=["GET", "POST"])
def book_add():
    if request.method == "GET":
        f = BookForm()
        return render_template("book/add.html", form=f)
    elif request.method == "POST":
        f = BookForm(CombinedMultiDict([request.form, request.files]))
        if f.validate_on_submit():
            filepath = os.path.abspath(os.path.dirname(__file__))
            book = Book()
            book.name = f.name.data
            book.isbn = f.isbn.data
            file = f.photo.data
            if file:
                filename = os.path.join(filepath, 'static/media/',
                                        file.filename)
                file.save(filename)
                book.photo = file.filename
            book.price = f.price.data
            book.pub_id = f.publish.data
            db.session.add(book)
            db.session.flush()  #保证可以返回b_id
            #返回自增id
            b_id = book.b_id
            book_obj=Book.query.filter(b_id==b_id).first()
            #获取前台多选框内的作者信息
            select_authors = request.form.getlist("author")
            for i in select_authors:
                author = Author.query.get(i)
                book_obj.back_author.append(author)
            db.session.commit()
            return redirect(url_for("book_list"))
        else:
            return render_template("book/add.html", form=f)

book_add() 的作用:
1、通过BookForm()类获取POST数据,将相关字段的数据保存到数据库中
2、通过request.files,将上传的图片数据保存到/mybook/apps/static/media中。


这里需要注意的是:


1、表单数据的获取

f = BookForm(CombinedMultiDict([request.form, request.files]))


在这条语句中,CombinedMultiDict 是 Werkzeug(Flask 的底层库)提供的一个工具类,用于合并多个 MultiDict 对象,常用于同时处理表单数据和文件上传的场景。
在 HTTP 请求中:
- 普通表单数据(如文本、数字)存储在 request.form(MultiDict 类型)。
- 文件上传数据存储在 request.files(同样是 MultiDict 类型)。
当表单同时包含普通字段和文件字段(例如 enctype="multipart/form-data"),Flask 会分别将数据解析到 request.form 和 request.files 中。若要将这些数据传递给表单验证工具(如 WTForms),需要将它们合并为一个对象,否则表单可能无法正确读取所有字段。
语法:
from werkzeug.datastructures import CombinedMultiDict
combined = CombinedMultiDict([dict1, dict2, ...])
- 参数:接受一个包含多个 MultiDict 的列表(如 [request.form, request.files])。
- 行为:按顺序从传入的 MultiDict 中查找键,返回第一个匹配的值。
示例代码:

from flask import request
from werkzeug.datastructures import CombinedMultiDict

# 合并表单数据和文件数据

combined_data = CombinedMultiDict([request.form, request.files])

# 将合并后的数据传递给表单

form = BookForm(combined_data)


 在本例中,CombinedMultiDict的作用:

f = BookForm(CombinedMultiDict([request.form, request.files]))


- 目的:将 request.form(普通字段)和 request.files(文件字段)合并,确保表单 BookForm 能同时验证普通字段(如 name、isbn)和文件字段(如 photo)。
- 必要性:如果不合并,表单可能无法获取文件字段的数据,导致验证失败。
注意事项:
- 键冲突:如果多个 MultiDict 中存在同名键,CombinedMultiDict 会返回第一个匹配的值。例如,若 request.form 和 request.files 都有键 photo,将优先返回 request.form 中的值(但实际场景中二者通常不会冲突)。
- 文件字段处理:确保表单类(如 BookForm)中正确声明了文件字段(例如使用 FileField)。


替代方案:
如果使用 Flask-WTF 扩展,可以直接通过 form = BookForm() 并单独处理文件字段,但合并方式更简洁且符合 Flask 的常见实践。
在一般情况下,如果仅有表单普通字段(如 name、isbn),没有文件字段(如 photo),可以使用 form = BookForm(request.form)。

但如果有文件字段(如 photo),必须用  form = BookForm() 或者 form = BookForm(CombinedMultiDict([request.form, request.files]))。当然,form = BookForm()会更简洁。

对比本例中,f = BookForm(request.form)和f = BookForm() 两种方式的差异:

行为

f = BookForm(request.form)

f = BookForm()

(自动合并)

数据来源

仅request.form

request.form+request.files

文件字段值

form.photo.data为None

form.photo.data包含文件对象

代码简洁性

如果要获得文件对象,需要手动处理文件字段,比如使用f = BookForm(CombinedMultiDict([request.form, request.files]))

自动处理所有字段

适用场景

特殊需求(如仅处理部分字段)

标准表单处理(推荐)


2、文件上传的安全性
在本例的源代码中,book_add() 还存在一定的安全隐患,体现在下面的语句中:

file = f.photo.data
if file:
    filename = os.path.join(filepath, 'static/media/',file.filename)
    file.save(filename)
    book.photo = file.filename


在这里,通过f.photo.data获得文件数据之后,没有通过werkzeug.utils的secure_filename函数对文件据进行安全处理,容易造成以下漏洞:
(1) 路径穿越攻击(Directory Traversal)
- 恶意文件名示例:../../../etc/passwd
- 后果:攻击者可能覆盖或读取服务器敏感文件。
- 原理:直接保存文件时,路径中的 ../ 会跳转到上级目录。
(2) 文件名冲突或覆盖
- 示例:两个用户上传同名文件 image.jpg,后上传的会覆盖前者。
- 后果:数据丢失或服务异常。
(3) 特殊字符导致异常
- 示例:文件名包含空格(my file.txt)、中文(我的文件.jpg)、#、$ 等。
- 后果:某些操作系统或代码逻辑无法处理这些字符,导致文件保存失败或路径解析错误。
(4) 修复方案
- 在views.py中导入secure_filename函数:

from werkzeug.utils import secure_filename  # 新增导入


- 将文件上传语句改为:

file = f.photo.data
if file:
# 新增文件名安全处理
    safe_filename = secure_filename(file.filename)  # 关键修改
    filename = os.path.join(filepath, 'static/media/',safe_filename)  # 使用安全文件名
    file.save(filename)
    book.photo = safe_filename  # 存储清理后的文件名到数据库


3、获取前台多选框内的作者信息

select_authors = request.form.getlist("author") 


这条语句是为了获取前台多选框内的作者信息,可以用下面的语句代替:

select_authors = f.author.data


在这里 request.form.getlist("author") 和f.author.data在这里是等效的,区别在于:
- request.form.getlist直接操作原始请求数据
- f.author.data通过表单对象提供了类型转换、验证集成等额外功能。使用 f.author.data 能确保数据经过表单验证(例如是否在允许的选项范围内)。直接操作 request.form.getlist会绕过表单验证逻辑,可能导致非法数据进入业务逻辑。此外,这种方式代码更简洁,避免手动操作原始请求数据,代码更符合 Flask-WTF 的设计哲学。


4、多选框内的作者信息入库

book_obj=Book.query.filter(b_id==b_id).first()
#获取前台多选框内的作者信息
select_authors = request.form.getlist("author")
for i in select_authors:
    author = Author.query.get(i)
    book_obj.back_author.append(author)
    db.session.commit()


 在这段代码中,存在一个语法错误,即这条语句:

book_obj=Book.query.filter(b_id==b_id).first()


- 当使用query.filter时,其语法应为:query.filter(模型名.字段名 == 值)
- 而使用query.filter_by,方式是query.filter_by(字段名=值)
- 注意query.filter采用的是两个等号(==),query.filter_by采用的是一个等号(=)。
因此,book_obj=Book.query.filter(b_id==b_id).first()应更改为:

book_obj = Book.query.filter(Book.b_id == b_id).first()


在 SQLAlchemy 中,filter 和 filter_by 是两种常用的查询过滤方法,但它们在语法、灵活性和使用场景上有显著区别。以下是两者的详细对比:
语法差异:
①filter
- 语法:query.filter(模型字段表达式)。
- 字段引用:必须明确指定模型类字段(如 Model.column)。
- 操作符:使用 Python 的比较操作符(==、!=、> 等)或 SQLAlchemy 的查询构造器(如 and_、or_)。
- 示例:

# 查询用户名为 "Alice" 的用户
User.query.filter(User.name == "Alice").first()

# 多条件查询
from sqlalchemy import and_
User.query.filter(and_(User.age > 18, User.country == "USA")).all()


② filter_by
- 语法:query.filter_by(字段名=值)。
- 字段引用:直接使用字段名(无需模型类前缀)。
- 操作符:仅支持等值比较(=),隐式使用 ==。
- 示例:

# 查询用户名为 "Alice" 的用户
User.query.filter_by(name="Alice").first()

# 多条件查询(隐式 AND)
User.query.filter_by(age=25, country="USA").all()

参数类型对比:

特性

filter

filter_by

参数类型

SQL 表达式(如User.name == "Alice")

关键字参数(如name="Alice")

多表支持

✅ 支持跨表关联查询(需显式join)

❌ 仅限当前模型的字段

复杂条件

✅ 支持and_、or_、in_等操作符

❌ 仅支持等值比较

灵活性对比:
①filter 的灵活性
- 多表关联查询:

# 查询作者为 "J.K. Rowling" 的书籍
Book.query.join(Author).filter(Author.name == "J.K. Rowling").all()


- 复杂条件组合:

from sqlalchemy import or_
User.query.filter(or_(User.age < 18, User.age > 65)).all()


- 非等值操作:

Product.query.filter(Product.price > 100).all()


②filter_by 的局限性
- 仅限单表等值查询:
# 无法直接处理多表关联或非等值条件

User.query.filter_by(age=25, country="USA").all()


 链式调用的区别:
① filter 的链式调用
- 可以自由混合不同模型的字段(需关联表):

session.query(User, Order)
        .join(Order)
        .filter(User.id == 1)
        .filter(Order.total > 100)
        .all()


②filter_by 的链式调用
- 仅限当前模型的字段:

User.query.filter_by(country="USA").filter_by(age=25).all()
# 等价于
User.query.filter_by(country="USA", age=25).all()

适用场景:

场景

推荐方法

理由

单表简单等值查询

filter_by

语法简洁,无需重复写模型类名。

多条件、非等值或跨表复杂查询

filter

支持复杂操作符和跨表关联。

动态构建查询条件

filter

可以通过条件表达式灵活拼接。

快速原型开发或简单 CRUD

filter_by

减少代码量,提高可读性。

示例对比:
①查询用户名为 "Alice" 且年龄为 25
- filter_by:

User.query.filter_by(name="Alice", age=25).first()


- filter:

User.query.filter(User.name == "Alice", User.age == 25).first()


② 查询价格大于 100 且库存小于 10 的商品
- 仅能用filter:

Product.query.filter(Product.price > 100, Product.stock < 10).all()


总结:
- filter_by:适用于单表简单等值查询,语法简洁。
- filter:适用于复杂条件、多表关联或非等值查询,灵活性更高。
- 选择原则:
- 如果查询条件简单且均为等值比较,优先使用filter_by。
- 需要复杂逻辑或跨表查询时,必须使用filter。


最后,数据提交这条语句:

db.session.commit()


在实际的程序运行中,为什么db.session.commit()前面,不加入db.session.add(book_obj)这条句语,也可以提交成功?
而在《Flask2+Vue.js实战派》第88页,多对多关系,增加数据的时候,是有db.session.add这一条句的。
在查阅了相关资料得知:在 SQLAlchemy 中,book_obj 对象已经处于会话的跟踪状态(即 "persistent" 状态),因此对其关联关系(如 back_author)的修改会自动被会话记录,无需显式调用 db.session.add(book_obj)。以下是详细解释:
对象状态分析:
①book_obj 的来源

book_obj = Book.query.filter(Book.b_id == b_id).first()


- 通过Book.query 查询得到的 book_obj 已自动被 SQLAlchemy 会话(Session)跟踪。
- 此时book_obj 处于 persistent(持久化) 状态,会话会监控其所有属性变化。
②关联关系修改

book_obj.back_author.append(author)

当向book_obj.back_author(多对多关系)添加 author 对象时,SQLAlchemy 会自动记录这一变更,无需手动操作中间表或调用 add()。
SQLAlchemy 的自动跟踪机制:
① Persistent 状态的对象
- 通过查询(如query.get()、query.first())获取的对象默认被会话跟踪。
- 对这些对象的属性修改或关联关系变更会被会话自动捕获,并在 commit() 时生成对应的 SQL(如插入中间表记录)。
②关联关系与中间表
- 如果back_author 是使用 relationship 定义的多对多关系,例如在下面的代码中:

class Book(Base):
    __tablename__ = "books"
    b_id = Column(Integer, primary_key=True)
    authors = relationship("Author", secondary="book_author", backref="books")

class Author(Base):
    __tablename__ = "authors"
    a_id = Column(Integer, primary_key=True)

# 中间表
book_author = Table(
    "book_author",
    Base.metadata,
    Column("book_id", Integer, ForeignKey("books.b_id")),
    Column("author_id", Integer, ForeignKey("authors.a_id")),
)


向 book_obj.authors 添加 author 对象时,SQLAlchemy 会自动向中间表 book_author 插入记录。
这就解释了为什么不需要 db.session.add(book_obj):


①对象已在会话中
- book_obj是查询得到的,已存在于会话的 Identity Map 中。
- 会话会跟踪其所有变更(包括关联关系)。
②关联对象的处理
- author对象通过 Author.query.get(i) 获取,同样处于会话跟踪状态。
- 向book_obj.back_author 添加 author 时,SQLAlchemy 会:检测到关联关系的变更,并自动生成对中间表的INSERT 语句。
③提交时的操作
- db.session.commit()
 会将会话中所有变更(包括关联关系的更新)一次性提交到数据库。
对比显式 add() 的场景:
①需要显式 add() 的情况
- 新增对象:例如创建一个新 Book 对象并保存:

new_book = Book(name="New Book")
db.session.add(new_book)  # 必须显式添加
db.session.commit()


- 游离(Detached)对象:如果对象未与会话关联(如从其他会话加载后未合并),需重新添加。
②不需要显式 add() 的情况
- 查询得到的对象:已自动被会话跟踪。
- 通过关联关系操作中间表:SQLAlchemy 自动处理。

结合前面的知识点,最终修改后的book_add()视图函数的代码:

@app.route("/book_add", methods=["GET", "POST"])
def book_add():
    if request.method == "GET":
        f = BookForm()
        return render_template("book/add.html", form=f)
    elif request.method == "POST":
        #f = BookForm(CombinedMultiDict([request.form, request.files]))
        f = BookForm(
        )  #使用 Flask-WTF 扩展,可以直接通过 form = BookForm() 并单独处理文件字段,但合并方式更简洁
        if f.validate_on_submit():
            filepath = os.path.abspath(os.path.dirname(__file__))
            book = Book()
            book.name = f.name.data
            book.isbn = f.isbn.data
            file = f.photo.data
            """存在安全隐患
            if file:
                filename = os.path.join(filepath, 'static/media/',
                                        file.filename)
                file.save(filename)
                book.photo = file.filename
            """

            if file:
                # 新增文件名安全处理
                safe_filename = secure_filename(file.filename)  # 关键修改
                filename = os.path.join(filepath, 'static/media/',
                                        safe_filename)  # 使用安全文件名
                file.save(filename)
                book.photo = safe_filename  # 存储清理后的文件名到数据库

            book.price = f.price.data
            book.pub_id = f.publish.data
            db.session.add(book)
            db.session.flush()  #保证可以返回b_id
            #返回自增id
            b_id = book.b_id
            book_obj = Book.query.filter(Book.b_id == b_id).first()
            #获取前台多选框内的作者信息
            #select_authors = request.form.getlist("author") #采用原生的request.form.getlist也可以
            select_authors = f.author.data  # 采用form对象的data属性也可以
            for i in select_authors:
                author = Author.query.get(i)
                book_obj.back_author.append(author)
            db.session.add(book_obj)
            db.session.commit()
            return redirect(url_for("book_list"))
        else:
            return render_template("book/add.html", form=f)

至此,视图函数book_add()分析完毕。


(三)视图函数book_edit()
代码如下:

app.route("/book_edit/<int:bid>", methods=["GET", "POST"])
def book_edit(bid):
    if request.method == "GET":
        # 根据bid找到具体的图书
        book_obj = Book.query.get(bid)
        #反向查询找到作者
        authors=book_obj.back_author
        #构造列表
        author_ids=[i.a_id for i in authors]
        f=BookForm(request.form)
        f.name.data=book_obj.name
        f.isbn.data=book_obj.isbn
        f.photo.data=book_obj.photo
        # 反向查询找到出版社
        publish_obj = book_obj.back_publish
        f.publish.data=publish_obj.p_id
        f.author.data=author_ids
        return render_template("book/edit.html", form= f, book=book_obj)
    elif request.method == "POST":
        f = BookForm(CombinedMultiDict([request.form, request.files]))
        book_obj = Book.query.get(bid)
        filepath=os.path.abspath(os.path.dirname(__file__))
        if f.validate_on_submit():
            book_obj.name = f.name.data
            book_obj.isbn = f.isbn.data
            file = f.photo.data  # 原文件名
            if file:
                filename=os.path.join(filepath,'static/media/',file.filename)
                file.save(filename)
                book_obj.photo = file.filename
            book_obj.price = f.price.data
            book_obj.pub_id = f.publish.data
            # 获取前台多选框内的作者信息
            select_authors = request.form.getlist("author")
            #将已经存在的关系删除
            for obj in book_obj.back_author:
                author = Author.query.get(obj.a_id)
                book_obj.back_author.remove(author)
            #再插入
            for i in select_authors:
                author=Author.query.get(i)
                book_obj.back_author.append(author)
            db.session.commit()
            return redirect(url_for("book_list"))
        else:
            return render_template("book/edit.html", form= f, book= book_obj)


book_edit() 的作用:
1、如果以GET方式访问,通过获得的bid进行查询,将查询到的相关数据传递到模板edit.html中,便于在前台进行数据的呈现。
2、如果以POST方式访问,就将前台提交的修改信息入库。


这里需要注意的是:


1、根据bid找到具体的图书

book_obj = Book.query.get(bid)


query.get 是 SQLAlchemy 中用于根据主键值查找数据库记录的方法。它接受一个主键值作为参数,在 Book 表中查找具有该主键值的记录,并返回对应的模型对象。如果找不到对应的记录,它将返回 None。
此外,可以用 Book.query.filter_by 代替 Book.query.get 来实现相同的功能 (这里数据库的book的ID字段为b_id):

# 原代码
book_obj = Book.query.get(bid) 

# 替代代码
book_obj = Book.query.filter_by(b_id=bid).first() 


query.get 和query.filter_by的区别在于:


参数类型
- query.get :只能接受主键值作为参数。它会直接根据主键去数据库中查找对应的记录,性能相对较高,因为主键通常有索引优化。
- query.filter_by :可以接受任意字段和对应的值作为参数,用于指定过滤条件。它可以根据多个字段进行组合查询,灵活性更高。例如:
Python复制# 根据 name 和 isbn 进行过滤查询
books = Book.query.filter_by(name='Python  编程', isbn='1234567890').all()
查询逻辑
- query.get :优先从会话的缓存中查找记录,如果缓存中不存在,再去数据库中查找。这种机制可以避免不必要的数据库查询,提高性能。
- query.filter_by :总是会发起数据库查询,不会从会话缓存中查找。因此,在某些情况下,使用 Book.query.filter_by 可能会比 Book.query.get 稍微慢一些。
返回结果
- Book.query.get :如果找不到对应的记录,会直接返回 None。
- Book.query.filter_by :如果没有找到符合条件的记录,first() 方法会返回 None,而 all() 方法会返回一个空列表。
综上所述,当你只想根据主键值查找记录时,使用 Book.query.get 更为简洁高效;当需要根据多个字段进行组合查询时,应该使用 Book.query.filter_by 。


2、反向查询找到作者和出版社信息
对于前面的Book这个模型来说,其代码在models.py文件中:

from apps import db

class Publish(db.Model):
    __tablename__ = "tb_publish"
    p_id = db.Column(db.Integer,comment='ID', primary_key=True,autoincrement=True)  # 主键
    name = db.Column(db.String(32),comment='出版社名称')
    address = db.Column(db.String(64),comment='出版社地址')
    intro = db.Column(db.String(500),comment='出版社简介', nullable=True)
    books = db.relationship("Book", backref="back_publish", lazy=True, passive_deletes=True)

#中间表
a_b=db.Table(
    "author_book",
    db.Column("a_id",db.Integer,db.ForeignKey("tb_author.a_id"),primary_key=True),
    db.Column("b_id", db.Integer, db.ForeignKey("tb_book.b_id"), primary_key=True),
)

class Author(db.Model):
    __tablename__="tb_author"
    a_id = db.Column(db.Integer,comment='ID', primary_key=True,autoincrement=True)
    name = db.Column(db.String(20),comment='作者姓名')
    age = db.Column(db.Integer,comment='作者年龄', default=1)
    mobile = db.Column(db.String(11),comment='电话')
    address = db.Column(db.String(20),comment='地址')
    intro = db.Column(db.Text,comment='简介', nullable=True)
    books=db.relationship("Book",secondary="author_book", backref="back_author")
    def __repr__(self):
        return f"{self.a_id}"

class Book(db.Model):
    __tablename__="tb_book"
    b_id = db.Column(db.Integer,comment='ID', primary_key=True,autoincrement=True)
    name = db.Column(db.String(50),comment='书籍名称')
    isbn = db.Column(db.String(50),comment='ISBN')
    photo = db.Column(db.String(50),comment='书籍封面')
    price = db.Column(db.Numeric(5,2),comment='价格')
    pub_id=db.Column(db.Integer,db.ForeignKey("tb_publish.p_id"))
models.py定义了三个数据模型:Publish、Author、Book,以及一个中间表a_b=db.Table。三个模型的关联关系如下表:

    主模型

    关联模型

    关系类型

    正向属性

    反向属性

    中间表(如有)

    Publish

    Book

    一对多

    books

    back_publish

    Author

    Book

    多对多

    books

    back_author

    a_b(author_book)

    - Publish和Book是一对多关系,Publish处于“一方”,Book处于“多方”。 Book的pub_id字段设置了外键db.ForeignKey("tb_publish.p_id"),关联到Publish的tb_publish表的p_id字段;Publish的books字段通过db.relationship的backref="back_publish"与Book建立了反向查询。


    - Book和Author是多对多关系,二者都是“多方”,也就是说,一本书可以对应多个作者,一个作者也可以对应多本书。二者通过中间表a_b=db.Table建立了联系,并为双方建立了外键db.ForeignKey("tb_author.a_id")和db.ForeignKey("tb_book.b_id")。在Author中,books字段通过db.relationship的backref="back_author"与Book建立了反向查询。而db.relationship的secondary="author_book",author_book是一个中间表,用来连接作者(Author)和书籍(Book)之间的多对多关系。


    -  SQLAlchemy 中,多对多关系只需在一侧模型(通常是主导关系的模型)中定义 secondary 参数,另一侧通过反向引用访问。例如,若 Author 是主导模型(如“作者可以写多本书”),则 Author 模型中定义 secondary,Book 模型通过 back_author 反向查询关联的作者


    - 多对多关系建立起来之后,Author 和 Book 可以互相查询。
    从 Author 查询关联的 Book:

    author = Author.query.get(1) 
    books = author.books   # 返回所有关联的 Book 对象 


    从 Book 查询关联的 Author:
     

    book = Book.query.get(1) 
    authors = book.back_author   #反向查询,返回所有关联的 Author 对象 

    -  一对一、多对多的区别:

    关系类型

    实现方式

    查询方向

    适用场景

    一对多

    一方定义relationship

    单向或双向

    如出版社与图书

    多对多

    需中间表 + 双方relationship

    双向互查

    如作者与图书

     关于例子中的数据库模型中的关系与反向查询,可以用以下思维导图表示:

    数据库模型中的关系与反向查询
    ├── One-to-Many (一对多)
    │   └── Example: Publisher to Book
    │       ├── 一个Publisher对应多本Books
    │       ├── 关系属性:
    │       │   - Publisher.books(关联到Book)
    │       │   - Book.back_publish(反向查询到Publisher)
    │       └── 实现方式:通过外键pub_id在Book表中引用Publisher的p_id
    ├── Many-to-Many (多对多)
    │   └── Example: Author to Book
    │       ├── 一个Author对应多个Books,一本书由多个Authors编写
    │       ├── 关系属性:
    │       │   - Author.books(关联到Book)
    │       │   - Book.back_author(反向查询到Author)
    │       └── 实现方式:使用中间表author_book存储关系
    └── Reverse Queries (反向查询)
    └── 示例:
    ├── 书籍如何访问其出版社和作者?
    │   - Book对象通过back_publish访问Publisher
    │   - Book对象通过back_author访问Author列表
    └── 实现方式:通过ORM框架(如SQLAlchemy)自动管理关系属性


    基于于上述模型的关联关系,继续分析这段代码:

    # 根据bid找到具体的图书
            book_obj = Book.query.get(bid)
    #反向查询找到作者
            authors=book_obj.back_author
            #构造列表
            author_ids=[i.a_id for i in authors]
            f=BookForm(request.form)
            f.name.data=book_obj.name
            f.isbn.data=book_obj.isbn
            f.photo.data=book_obj.photo
            # 反向查询找到出版社
            publish_obj = book_obj.back_publish
            f.publish.data=publish_obj.p_id
            f.author.data=author_ids
            return render_template("book/edit.html", form=f, book=book_obj)


     当执行book_obj = Book.query.get(bid)语句,获得图书信息的对象book_obj之后,能够通过authors=book_obj.back_author查询到一本书的多个作者信息。对于这条语句来说:
    authors=book_obj.back_author
    查询到的是多个作者的信息,这些信息的字段,就位于Author模型之中,包括a_id、name、age、mobile、address、intro等字段。
    这里需注意的是,在Author模型有这样一条语句:
     

     def __repr__(self):
            return f"{self.a_id}"


    在 Python 中,__repr__ () 是一个特殊方法(Magic Method),用于定义对象的无歧义字符串表示形式。其主要目的是为开发者提供明确的调试信息或日志记录内容。
    而return f"{self.a_id}",会自动返回a_id这个字段的值。如果没有 __repr__这个方法,在authors=book_obj.back_author后面,加入一条语句print(authors):

    authors=book_obj.back_author
    print(authors)


    调试信息打印出的是Author模型的数据集列表,比如一本书本的作者的a_id分别是:3、4、1、2,对应的Author模型的数据集列表就是:

    [<Author 3>, <Author 4>, <Author 1>, <Author 2>]


    而有这个__repr__这个方法,在authors=book_obj.back_author后面,加入一条语句print(authors):

    authors=book_obj.back_author
    print(authors)


    调试信息打印出的是Author模型的a_id列表,比如一本书本的作者的a_id分别是:3、4、1、2,对应的Author模型的a_id列表就是:

    [3, 4, 1, 2]


    这个细微的差别,就解释了为什么在本书第四章第65页,为什么要使用__repr__ ()方法。也就是通过__repr__ (),可以让模型返回一些有用的信息。
    接下来,book_edit()的这条语句:

    author_ids=[i.a_id for i in authors]


    当查询到多个作者的信息之后,通过列表推导式[i.a_id for i in authors]返回多个作者的a_id列表。而在Author模型中,使不使用__repr__ ()方法,其结果都是一样的。
    接下来,通过

    publish_obj = book_obj.back_publish


    这条语句,反向查询到Publish的数据。最后,将相关表单字段进行赋值:
     

     f=BookForm(request.form)
            f.name.data=book_obj.name
            f.isbn.data=book_obj.isbn
            f.photo.data=book_obj.photo
            # 反向查询找到出版社
            publish_obj = book_obj.back_publish
            f.publish.data=publish_obj.p_id
            f.author.data=author_ids
            return render_template("book/edit.html", form=f, book=book_obj)


    从而在进入编辑界面之后呈现原先已保存的数据:

    这里需注意的是,在对相关表单字段进行赋值的时候,这两条语句:

    f.publish.data=publish_obj.p_id
    f.author.data=author_ids


    出版社的信息f.publish.data(单一值)和作者的信息f.author.data(多个值)都是相关的ID值。例如,“电子工作出版社”对应值是1,作者“张三、李四、王五、王玲”对应的列表值是[3, 4, 1, 2]。


    那么,在进入编辑状态(比如: /book_edit/1)时,Flask是如何实现自动实现相关信息的选中状态的呢?

    我们先来看看form.py中,对BookForm这个类对相关字段的定义:

    class BookForm(FlaskForm):
        name = StringField(label="图书名称",
                           validators=[
                               DataRequired(message="图书名称不能为空"),
                               Length(4, 32, message="长度最少4位")
                           ],
                           render_kw={
                               'class': 'form-control',
                               'placeholder': "请输入图书名称"
                           })
    
        isbn = StringField(label="ISBN",
                           validators=[
                               DataRequired(message="ISBN不能为空"),
                               Length(1, 20, message="ISBN在1-20位之间")
                           ],
                           render_kw={
                               'class': 'form-control',
                               'placeholder': "请输入ISBN"
                           })
        
        photo = StringField(label="图书封面",
                            widget=widgets.FileInput(),
                            render_kw={'class': 'custom-file-input'})
    
        price = DecimalField(label="价格",
                             places=2,
                             validators=[DataRequired(message="价格不能为空")],
                             render_kw={
                                 'class': 'form-control',
                                 'placeholder': "请输入价格"
                             })
    
        publish = SelectField(label="出版社",
                              coerce=int,
                              validators=[DataRequired(message="出版社不能为空")],
                              choices=[('0', '请选择出版社...')] +
                              [(v.p_id, v.name) for v in Publish.query.all()],
                              render_kw={
                                  'class': 'form-control  custom-select',
                                  'placeholder': "请选择出版社"
                              })
    
        author = SelectMultipleField(label="作者",
                                     coerce=int,
                                     validators=[DataRequired(message="作者不能为空")],
                                     choices=[(v.a_id, v.name)
                                              for v in Author.query.all()],
                                     render_kw={
                                         'class': 'form-control  custom-select',
                                         'placeholder': "请选择作者"
                                     })


    以publish这个字段为例,其中有这样一条语句:
     

    choices=[('0', '请选择出版社...')] +[(v.p_id, v.name) for v in Publish.query.all()],


    这行代码会生成类似以下结构的选项列表:

    [('0', '请选择出版社...'),    (1, '电子工业出版社'),    (2, '人民邮电出版社'),    # 其他出版社...]


    每个选项由元组(值(p_id), 显示文本(name))构成。
    在book_edit()视图函数中,通过f.publish.data  = publish_obj.p_id将当前图书的出版社ID赋值给表单字段。例如,假设当前图书的出版社p_id是1,则f.publish.data的值为1。


    - 在模板渲染时,WTForms会根据data属性的值(即p_id)自动匹配choices中的选项:
    - 遍历choices列表:表单内部会遍历所有选项,检查每个选项的值(p_id)是否等于的data值。
    -  标记匹配项为选中状态:当找到匹配项(如(1, '电子工业出版社')),WTForms会在生成的HTML中为该选项添加selected属性,例如:

    <option value="1" selected>电子工业出版社</option>

    最终渲染的下拉菜单会显示所有出版社名称,但只有与p_id匹配的选项会被默认选中。例如:

    <select class="form-control" id="publish" name="publish">
        <option value="0">请选择出版社...</option>
        <option value="1" selected>电子工业出版社</option>
        <option value="2">人民邮电出版社</option>
        <!-- 其他选项 -->
    </select>


    浏览器会根据selected属性自动高亮显示匹配的选项。类似的,作者信息也通过这种方式实现了html渲染字段的关联。
    换句话说,只要执行了:

    f.publish.data=publish_obj.p_id
    f.author.data=author_ids


    Flask会自动将出版社的信息和作者的信息实现自动填充和选中。


    3、图片上传的安全性问题
    在BookForm这个类中,有一段关于photo字段的定义:

    photo = StringField(label="图书封面",widget=widgets.FileInput(),render_kw={'class': 'custom-file-input'})


    这里比较奇怪的是:photo这个用于文件上传的字段,用的是StringField。而在Flask-WTF中,FileField 才是专门用于处理文件上传的字段类型。当然,这里用的是widget=widgets.FileInput()这个属性,也可以同样达到FileField的效果。
    - Widget 可以扩展表单的功能。例如:
    - FileInput:用于文件上传。
    - CheckboxInput:用于复选框。
    - Select:用于下拉选择框。
    - 开发者还可以创建自定义 widget,以实现更复杂的表单元素(如富文本编辑器、日期选择器等)。
    但是,这段代码由于没有对图片格式进行限制,很容易被上传其他文件,可以作如下修改:

    from flask_wtf.file import FileField, FileAllowed, FileRequired
    
    photo = FileField(
            label="图书封面",
            validators=[
                FileRequired(message="请选择封面文件"),  # 确保文件已上传
                FileAllowed(['jpg', 'png','gif'], message="仅支持 JPG/PNG/gif 格式")  # 限制文件类型
            ],
            render_kw={'class': 'custom-file-input'})


    至此,视图函数book_edit()分析完毕。


    (四)视图函数index()
    代码如下:

    @app.route("/index", methods=["GET"])
    def index():
        # 获取统计信息
        pub_count = db.session.query(func.count(Publish.p_id)).scalar()
        author_count = db.session.query(func.count(Author.a_id)).scalar()
        book_count = db.session.query(func.count(Book.b_id)).scalar()
    
        # 图表的label和data组装
        #select count(b.name) from book a,publish b where a.pub_id=b.p_id group by b.name
        infos = Book.query.join(
            Publish, Book.pub_id == Publish.p_id).with_entities(
                Publish.name,
                func.Count(Publish.name)).group_by(Publish.name).all()
        #print(
        #    Book.query.join(Publish, Book.pub_id == Publish.p_id).with_entities(
        #        Publish.name, func.Count(Publish.name)).group_by(Publish.name))
    
        labels = [info[0] for info in infos]
        datas = [info[1] for info in infos]
        return render_template('index.html',
                               pub_count=pub_count,
                               author_count=author_count,
                               book_count=book_count,
                               labels=labels,
                               datas=datas)


    index()的作用是:
    1、通过聚合查询,获得相关统计信息
    2、将相关统计信息传入index.html模板,实现下面的效果:

    这里,重点讲解一下聚合查询。对于下面的语句:

    pub_count = db.session.query(func.count(Publish.p_id)).scalar()


    在 SQLAlchemy 中,scalar() 是一个查询方法,用于执行查询并返回结果的第一行第一列的值(即标量值)。它特别适用于聚合查询(如 COUNT(), SUM(), MAX() 等),这些查询通常只返回单个值。
    db.session.query(func.count(Publish.p_id))会生成一个 SQL 查询,类似sql语句:

    SELECT COUNT(publish.p_id) AS count_1 FROM publish


    scalar()的作用是:
    - 执行这个查询。
    - 获取结果的第一行。
    - 提取该行的第一列的值(即COUNT() 的结果)。
    - 最终返回一个单一的整数(例如42),而不是返回一个包含元组的列表(如 [(42,)])。
    对比其他方法:
    - .first():返回结果的第一行(一个元组),例如 (42,)。
    - .one():严格确保结果只有一行,否则抛出异常。
    - .scalar():直接提取第一行第一列的值,适合获取单个标量结果。
    如果没有使用 scalar():

    result = db.session.query(func.count(Publish.p_id)).first()
    # result 是一个元组,如 (42,)
    pub_count = result[0]  # 需要手动提取第一个元素


    总之,使用 scalar() 可以简化代码,直接得到标量值。
    而对于这条语句:

    infos = Book.query.join(
            Publish, Book.pub_id == Publish.p_id).with_entities(
                Publish.name,
                func.Count(Publish.name)).group_by(Publish.name).all()


    代码生成的 SQL 等价于:

    SELECT publish.name, COUNT(publish.name) FROM book JOIN publish ON book.pub_id = publish.p_id GROUP BY publish.name;


    目的是查询 每个出版社的名字及其出版的书籍数量。
    ①Book.query.join(Publish, Book.pub_id == Publish.p_id)
    - 作用:将 Book 表和 Publish 表通过外键关联(Book.pub_id = Publish.p_id)进行连接。
    - 等价 SQL:

    FROM book JOIN publish ON book.pub_id = publish.p_id


    ② .with_entities(Publish.name, func.Count(Publish.name))
    - 作用:指定查询返回的字段:
    - Publish.name:出版社名称。
    - func.Count(Publish.name):统计每个出版社的书籍数量。
    - 等价 SQL:

    SELECT publish.name, COUNT(publish.name)


    ③ .group_by(Publish.name)
    - 作用:按出版社名称分组,确保 COUNT 统计的是每个出版社的书籍数量。
    - 等价 SQL:

    GROUP BY publish.name


    ④ .all()
    - 作用:执行查询并返回所有结果,结果是一个列表,每个元素是一个元组,格式为 (出版社名称, 书籍数量),例如:

    [('电子工业出版社', 5), ('人民邮电出版社', 3), ('机械工业出版社', 8)]


    返回结果的用途
    - labels = [info["name"] for info in infos]:提取所有出版社名称(用于图表横坐标)。
    - datas = [info[1] for info in infos]:提取每个出版社的书籍数量(用于图表纵坐标)。
    最终返回的数据可能是这样的:

    labels = ['电子工业出版社', '人民邮电出版社', '机械工业出版社']
    datas = [5, 3, 8]


    至此,视图函数index()分析完毕。


    三、前端需重点学习的代码
    (一)base.html模板文件
    这个模板文件,位于mybook\templates\  下,是所有模板文件的公共模板文件。需要重点关注的代码有:


    1、HTML5的<aside>元素
    对于base.html中下面的代码:

    <aside class="main-sidebar sidebar-dark-primary elevation-4">
            <a href="{{ url_for("index")}}" class="brand-link">
                <img src="{{ url_for('static',filename='img/AdminLTELogo.png' ) }}" alt="AdminLTE Logo"
                     class="brand-image img-circle elevation-3" style="opacity: .8">
                <span class="brand-text font-weight-light">图书管理系统</span>
            </a>
            <div class="sidebar">
                <div class="user-panel mt-3 pb-3 mb-3 d-flex">
                    <div class="image">
                        <img src="{{ url_for('static',filename='img/user2-160x160.jpg' )}}" class="img-circle elevation-2" alt="User Image">
                    </div>
                    <div class="info"><p>
                        <a href="{{ url_for('logout')}}">退出</a></p>
                    </div>
                </div>
                <nav class="mt-2">
                    <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu"
                        data-accordion="false">
                        <li class="nav-header">书籍管理</li>
                            <li class="nav-item">
                                <a href="{{ url_for('pub_list') }}" class="nav-link">
                                    <i class="nav-icon far fa-image"></i>
                                    <p>
                                        出版社管理
                                    </p>
                                </a>
                            </li>
                            <li class="nav-item">
                                <a href="{{ url_for('author_list')}}" class="nav-link">
                                    <i class="nav-icon far fa-image"></i>
                                    <p>
                                        作者管理
                                    </p>
                                </a>
                            </li>
                            <li class="nav-item">
                                <a href="{{ url_for('book_list') }}" class="nav-link">
                                    <i class="nav-icon far fa-image"></i>
                                    <p>
                                        图书管理
                                    </p>
                                </a>
                            </li>
                        <li class="nav-header">系统管理</li>
                            <li class="nav-item">
                                <a href="{{ url_for('user_list')}}" class="nav-link">
                                    <i class="nav-icon far fa-circle text-danger"></i>
                                    <p class="text">用户管理</p>
                                </a>
                            </li>
                    </ul>
                </nav>
            </div>
        </aside>


    对应的界面是管理界面左边的侧边栏(打红框部分):

    在早期的HTML语法中,很多时候,是用frameset、frame、iframe和noframes等传统框架元素来实现侧边栏以及其他框架效果。

    而从HTML5标准开始,就明确移除了frameset、frame和noframes元素。只是iframe仍适用于部分场景,例如嵌入第三方内容(地图、广告)、实现沙箱隔离(如代码编辑器预览)、或需完全独立上下文的子页面(如支付网关)。


    HTML5引入了<aside>元素 ,其作用是:
    - 全局侧边栏
    - 用途:作为整个页面的侧边栏,包含与主内容松散相关的信息,如导航菜单、友情链接、广告等
    - 示例:

    <body>  
      <header>网站标题</header>  
      <main>主内容区</main>  
      <aside>  
        <h3>相关链接</h3>  
        <nav>  
          <ul>  
            <li><a href="#">友情链接1</a></li>  
            <li><a href="#">友情链接2</a></li>  
          </ul>  
        </nav>  
        <div class="ad">广告位</div>  
      </aside>  
    </body>  


    - 文章内附属信息
    - 用途:嵌入在<article>内部,展示与当前文章相关的补充内容,如名词解释、参考资料等
    - 示例:

    <article>  
      <h2>F#编程入门</h2>  
      <p>Lambda表达式可以创建词法闭包...</p>  
      <aside>  
        <h4>名词解释</h4>  
        <p>词法闭包:指将创建Lambda表达式时的环境保存起来的机制。</p>  
      </aside>  
    </article>  


    在本书的源代码中,由于使用了Bootstrap前端框架的分支框架AdminLTE,故可以通过特定的CSS来进行布局。
    而在其他模板文件中,例如mybook\templates\index.html ,通过模板标签{% block content %}和{% endblock %}中的内容,替换base.html中的{% block content %}{% endblock %},实现整体内容的展示。
    只要点击由<aside>元素包裹的左侧侧边栏的链节,就可以在右侧显示相应的内容:

    换句话说,通过Bootstrap的CSS,就可以实现过去包括frame等传统框架元素的布局 ,从而避免过去使用传统框架元素导致的页面结构复杂化、难以维护,以及在多设备适配时存在兼容性问题,甚至可能导致的安全风险,例如跨站脚本攻击(XSS)。


    2、Bootstrap的data-widget="fullscreen"和data-widget="control-sidebar"
    base.html中有这样一段代码:

    <ul class="navbar-nav ml-auto">
                <li class="nav-item">
                    <a class="nav-link" data-widget="fullscreen" href="#" role="button">
                        <i class="fas fa-expand-arrows-alt"></i>
                    </a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" data-widget="control-sidebar" data-slide="true" href="#" role="button">
                        <i class="fas fa-th-large"></i>
                    </a>
                </li>
    </ul>


    含义及作用
    ① data-widget="fullscreen"
    - 含义:data-widget 是一个自定义的 HTML5 数据属性,data-widget="fullscreen" 用于标识这个元素与全屏功能相关。在 Bootstrap 中,它本身并不是标准的 Bootstrap 属性,而是开发者自定义用来触发全屏功能的标识。
    - 作用:当用户点击带有这个属性的元素时,通常会触发页面进入全屏模式。这种方式可以将页面内容以全屏的形式展示,提供更沉浸式的体验。
    - 使用场景:适用于需要用户专注查看页面内容的场景,比如展示图片、视频、文档等。
    例如,在界面上点击右侧顶部的全屏按钮,就进入全屏模式。若要退出全展模式,需按Esc键:


     

    ② data-widget="control-sidebar"
    - 含义:同样,data-widget="control-sidebar" 也是自定义的 HTML5 数据属性,用于标识这个元素与侧边栏控制功能相关。它表明该元素可以控制侧边栏的显示与隐藏。
    - 作用:点击带有这个属性的元素时,会触发侧边栏的显示或隐藏操作。侧边栏通常用于放置一些额外的控制选项、导航菜单等,以节省页面的主要空间。
    - 使用场景:常见于管理后台、仪表盘等页面,用于收纳一些不常用但又需要随时访问的功能。
    例如,在界面上点击右侧顶部的侧边栏按钮,会出现一个黑色的区域,这是右侧的侧边栏:

    如何使用:


    ①实现全屏功能
    要实现 data-widget="fullscreen" 的全屏功能,需要结合 JavaScript 代码来监听点击事件,并触发全屏 API。以下是一个示例:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"> 
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
        
        <title>Fullscreen Example</title>
    </head>
    
    <body>
        <ul class="navbar-nav ml-auto">
            <li class="nav-item">
                <a class="nav-link" data-widget="fullscreen" href="#" role="button">
                    <i class="fas fa-expand-arrows-alt"></i>
                </a>
            </li>
        </ul>
    
        <script>
            document.addEventListener('click',  function (event) {
                if (event.target.getAttribute('data-widget')  === 'fullscreen') {
                    const doc = document.documentElement; 
                    if (doc.requestFullscreen)  {
                        doc.requestFullscreen(); 
                    } else if (doc.webkitRequestFullscreen)  {
                        doc.webkitRequestFullscreen(); 
                    } else if (doc.msRequestFullscreen)  {
                        doc.msRequestFullscreen(); 
                    }
                }
            });
        </script>
    </body>
    
    </html>

    在这个示例中,我们通过监听点击事件,当点击带有 data-widget="fullscreen" 的元素时,调用浏览器的全屏 API 实现全屏功能。


    ② 实现侧边栏控制功能

    要实现 data-widget="control-sidebar" 的侧边栏控制功能,同样需要结合 JavaScript 代码。以下是一个简单的示例:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css"> 
        <style>
           .control-sidebar {
                position: fixed;
                top: 0;
                right: -200px;
                width: 200px;
                height: 100%;
                background-color: #0e2e4f;
                transition: right 0.3s ease;
            }
    
           .control-sidebar.show  {
                right: 0;
            }
        </style>
        <title>Control Sidebar Example</title>
    </head>
    
    <body>
        <ul class="navbar-nav ml-auto">
            <li class="nav-item">
                <a class="nav-link" data-widget="control-sidebar" data-slide="true" href="#" role="button">
                    <i class="fas fa-th-large"></i>
                </a>
            </li>
        </ul>
        <div class="control-sidebar"></div>
    
        <script>
            document.addEventListener('click',  function (event) {
                if (event.target.getAttribute('data-widget')  === 'control-sidebar') {
                    const sidebar = document.querySelector('.control-sidebar'); 
                    sidebar.classList.toggle('show'); 
                }
            });
        </script>
    </body>
    
    </html>

    在这个示例中,我们定义了一个侧边栏元素,并通过 CSS 设置其初始位置为隐藏状态。当点击带有 data-widget="control-sidebar" 的元素时,通过 JavaScript 切换侧边栏的 show 类,从而实现侧边栏的显示与隐藏。
    当然,在不关心JavaScript的实现细节的情况下,我们只需要像basse.html那样,在模板文件中,导入下面的代码:

    <script src="{{ url_for('static',filename='plugins/jquery/jquery.min.js') }}"></script>
    <script src="{{ url_for('static',filename='plugins/bootstrap/js/bootstrap.js') }}"></script>

    (二)publish目录下的index.html模板文件


    这个模板文件,位于mybook\templates\publish\  下,是publish模块的首页展示文件。需要重点关注的代码有:


    1、HTML5的<section>元素


    index.html文件中,相应的代码如下:

    <section class="content">
                <div class="container-fluid">
                    <div class="row">
                        <div class="col-12 search-collapse">
                            <form id="search_form" method="get">
    {#                            {% form.csrf_token %}#}
                                <div class="form-group row">
                                    <label for="inputtext" class="col-form-label">出版社名称:</label>
                                    <div class="col-md-4">
                                        <input type="text" id="search_name" name="name" class="form-control"/>
                                    </div>
                                    <div class="col-md-4">
                                        <button type="submit" class="btn btn-info"><i class="fa fa-plus"></i>查询</button>
                                        <a class="btn btn-primary single" href="{{ url_for('pub_add') }}">
                                            <i class="fa fa-plus"></i> 新增
                                        </a>
                                    </div>
                                </div>
                            </form>
                        </div>
                    </div>
                </div>
            </section>


    这段代码,对应的是下面的界面打红框的部分:

    HTML5的<section>元素是用于结构化文档内容的语义化标签,主要作用是为页面内容提供逻辑分块。以下是其核心作用和正确使用方法:
    核心作用:
    ①语义化分块
    <section>表示文档中具有独立主题的内容区块(如章节、标签页等),需包含标题(<h1>-<h6>),帮助浏览器、屏幕阅读器和搜索引擎理解内容结构
    ②组织关联内容
    适合将相关联的内容分组(例如同一主题下的多个段落),强调内容的逻辑关联性而非独立性。
    ③优化可访问性
    屏幕阅读器可通过<section>识别内容结构,提升视障用户的访问体验。
    基本语法:

    <section>
      <h2>章节标题</h2>
      <p>相关段落或其他内容...</p>
    </section>


    典型场景:
    - 文章章节:划分长文的不同部分(如“引言”“方法”“结论”)。
    - 标签页内容:每个标签页的内容区块。
    - 专题内容组:如产品介绍页的“功能”“参数”“评价”模块。
    嵌套规则:
    允许嵌套使用以创建层级结构:

    <section>
      <h2>主章节</h2>
      <section>
        <h3>子章节</h3>
        <p>子内容...</p>
      </section>
    </section>


    与其他元素对比:
    - 与<div>的区别<div>是无语义的通用容器,仅用于布局或样式;<section>强调内容的逻辑分组。
    - 与<article>的区别<article>表示独立完整的内容(如一篇博客),而<section>侧重关联性内容的分组。注意事项:
    ①避免滥用
    - 不要仅为了样式需求使用<section>,应优先选择<div>。
    - 若无明确标题,应使用<div>替代。
    ②替代方案优先级
    若内容更适合用<article>(独立内容)、<aside>(侧边栏)或<nav>(导航),则无需使用<section>。
    错误用法示例:

    <!-- 错误:缺少标题 -->
    <section>
      <p>无标题的段落...</p>
    </section>
    
    <!-- 错误:仅用于样式容器 -->
    <section class="red-box">
      <p>仅为了红色边框而使用section</p>
    </section>


    通过合理使用<section>,可提升网页的语义化程度和可维护性。


    2、信息的删除与确认
    在每一条信息的展示上,有一个删除按钮:


    当点击相应的删除按钮,将弹出一个确认框:

    这个功能,对应了两段代码。这两段代码是Ajax异步请求数据的关键。
    代码1:

    <td width="20%">
        <a class="btn btn-primary single" href="{{ url_for('pub_edit',p_id=per.p_id) }}">
           <i class="fa fa-edit"></i> 修改
        </a>
        <a class="btn btn-danger" href="javascript:void(0)" onclick="showDeleteModal(this)">删除</a>
        <input type="hidden" id="id_hidden" value={{ per.p_id }}>
     </td>


    代码2:

    <!-- 信息删除确认 -->
        <div class="modal fade" id="delModal" tabindex="-1" aria-hidden="true">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title" style="float:left">提示信息</h4>
                        <button type="button" class="close" data-dismiss="modal"
                                aria-label="Close">
                            <span aria-hidden="true">×</span>
                        </button>
    
                    </div>
                    <div class="modal-body">
                        <p id="info">您确认要删除当前数据吗?</p>
                        <input type="hidden" id="del_id">
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
                        <a id="delButton" class="btn btn-success" data-dismiss="modal">确定</a>
                    </div>
                </div>
            </div>
        </div>
        <script>
            // 打开模态框并设置需要删除的ID
            function showDeleteModal(obj) {
                var $tds = $(obj).parent().children();// 获取到删除元素的所在列
                var delete_id = $($tds[2]).val();// 获取隐藏控件的ID
                console.log(delete_id)
                $("#del_id").val(delete_id);// 给模态框中需要删除的ID赋值
                $("#delModal").modal({
                    backdrop: 'static',
                    keyboard: false
                });
            };
    
            $(function () {
                // 模态框的确定按钮的点击事件
                $("#delButton").click(function () {
                    var id = $("#del_id").val();
                    console.log("del" + id)
                    // ajax异步删除
                    $.ajax({
                        url: "/pub_del",
                        type: "POST",
                        data:{p_id:id},
                        success: function (result) {
                            if (result.code == "200") {
                                $("#delModal").modal("hide");
                                alert(result.message);
                                window.location.href = "{{ url_for('pub_list') }}";
                            }
                            else{
                                alert(result.message);
                            }
                        }
                    })
                });
            });
        </script>


    在代码1中,语句
     

    <a class="btn btn-danger" href="javascript:void(0)" onclick="showDeleteModal(this)">删除</a>


    通过点击“删除”链节,触发了showDeleteModa(this)  函数,导致Bootstrap的模态框被打开,下面的JavScript代码:

    function showDeleteModal(obj) {
                var $tds = $(obj).parent().children();// 获取到删除元素的所在列
                var delete_id = $($tds[2]).val();// 获取隐藏控件的ID
                console.log(delete_id)
                $("#del_id").val(delete_id);// 给模态框中需要删除的ID赋值
                $("#delModal").modal({
                    backdrop: 'static',
                    keyboard: false
                });
            };


    语句:

    var $tds = $(obj).parent().children();// 获取到删除元素的所在列


    - obj是用户点击的删除按钮元素(即<a class="btn btn-danger">标签的DOM对象)。当用户点击删除按钮时,οnclick="showDeleteModal(this)"中的this指向被点击的按钮本身,作为参数传递给函数。
    - $(obj):将obj(删除按钮)转换为jQuery对象。
    - .parent():获取删除按钮的父元素,即所在的操作列<td>。
    - .children():获取该<td>下的所有直接子元素(包括修改按钮、删除按钮、隐藏的<input>)。

    最终结果:$tds是一个包含三个元素的jQuery集合,按顺序为:

    第一个子元素:修改按钮的<a>标签,

    第二个子元素:删除按钮的<a>标签(即obj自身),

    第三个子元素:隐藏的<input>(存储角色ID)。
    后续通过$($tds[2]).val()获取隐藏输入框的值(即角色ID)。
    语句:

    var delete_id = $($tds[2]).val();// 获取隐藏控件的ID


    - $("#del_id"):通过 jQuery 的 ID 选择器找到 HTML 中 id="del_id" 的元素。
    - .val(delete_id):将变量 delete_id 的值(即要删除的角色 ID)设置到该元素中(等价于修改其 value 属性)
    语句:

    $("#delModal").modal({
                    backdrop: 'static',
                    keyboard: false
                });


    代码作用:
    这段代码通过 jQuery 调用 Bootstrap 的模态框(Modal)组件,并配置了两个关键参数:
    - backdrop: 'static'
    - keyboard: false
    它的目的是以 强制交互模式 弹出一个模态框,用户必须明确点击模态框内的按钮(如“确认”或“取消”)才能关闭模态框,无法通过点击背景或按键盘 ESC 键关闭。
    参数详解:
    ① backdrop: 'static'
    - 默认行为:当 backdrop: true(默认值)时,模态框会显示一个半透明背景层,且点击背景层会自动关闭模态框。
    - static的行为:
    - 背景层依然显示,但点击背景层不会关闭模态框。
    - 用户只能通过模态框内的按钮操作关闭模态框。
    - 适用场景:需要强制用户明确操作(如确认删除)时使用。
    ②keyboard: false
    - 默认行为:当 keyboard: true(默认值)时,按下键盘的 ESC 键 会关闭模态框。
    - false的行为:
    - 禁用 ESC 键关闭模态框的功能。
    - 适用场景:防止用户误触 ESC 键导致意外关闭关键操作(如删除确认)。
    对应的 HTML 元素:
    在 HTML 中,需存在一个 id 为 delModal 的模态框结构,例如:

    <!-- 删除确认模态框 -->
    <div class="modal fade" id="delModal" tabindex="-1" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <!-- 模态框内容(标题、消息、按钮等) -->
                <div class="modal-header">
                    <h5 class="modal-title">确认删除</h5>
                </div>
                <div class="modal-body">
                    <p>确定要删除此数据吗?</p>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
                    <button type="button" class="btn btn-danger" id="confirmDelete">确认删除</button>
                </div>
            </div>
        </div>
    </div>


    实际效果:
    - 用户点击删除按钮 → 弹出 delModal 模态框。
    - 用户无法通过以下方式关闭模态框:
    - 点击模态框外的背景区域(backdrop: 'static')。
    - 按下键盘 ESC 键(keyboard: false)。
    - 用户必须明确选择:
    - 点击“取消” 按钮(关闭模态框)。
    - 点击“确认删除” 按钮(执行删除操作)。

    在信息删除确认的模态框中,有这样的代码:

    <div class="modal-footer">
         <button type="button" class="btn btn-default" data-dismiss="modal">取消</button>
         <a id="delButton" class="btn btn-success" data-dismiss="modal">确定</a>
    </div>


    其中的data-dismiss="modal" 是 Bootstrap 框架中用于关闭模态框(Modal)的一个关键属性。
    作用与用途:
    - 功能:当用户点击带有 data-dismiss="modal" 属性的元素(如按钮或链接)时,Bootstrap 会自动关闭当前显示的模态框。
    - 目的:无需编写额外 JavaScript 代码即可实现模态框的关闭逻辑,简化开发流程。
    在 Bootstrap 模态框中的应用:
    假设有以下模态框结构(简化示例):

    <!-- 模态框容器 -->
    <div class="modal fade" id="exampleModal" tabindex="-1">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">标题</h5>
            <!-- 关闭按钮(右上角的 ×) -->
            <button type="button" class="close" data-dismiss="modal">
              <span>&times;</span>
            </button>
          </div>
          <div class="modal-body">
            <p>模态框内容...</p>
          </div>
          <div class="modal-footer">
            <!-- 关闭按钮(底部) -->
            <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
            <button type="button" class="btn btn-primary">保存</button>
          </div>
        </div>
      </div>
    </div>


    - 关键元素:
    - 右上角的 “×” 按钮和底部的 “关闭” 按钮均添加了data-dismiss="modal"。
    - 用户点击这两个按钮时,Bootstrap 会直接关闭模态框。
    注意事项
    - 依赖关系:
    - 必须正确引入Bootstrap 的 JavaScript 文件,否则 data-dismiss 无法生效。
    - 模态框的 HTML 结构需符合 Bootstrap 规范(如包含.modal 类)。
    - Bootstrap 版本兼容性:
    - Bootstrap 4 和 5:均支持 data-dismiss="modal"。
    - Bootstrap 5 的变动:在 Bootstrap 5 中,某些 JavaScript 方法和属性可能有调整,但data-dismiss 的行为保持不变。
    - 自定义关闭逻辑:
    - 如果需要执行额外操作(如关闭前验证数据),可以结合 JavaScript 事件监听:

    $('#exampleModal').on('hide.bs.modal', function (e) {
      if (!validateForm()) {
        e.preventDefault(); // 阻止关闭
      }
    });

    示例场景:
    - 场景:用户点击 “关闭” 按钮后,模态框消失。
    - 代码实现:直接在按钮上添加 data-dismiss="modal":

    <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>


    在模态框的确定按钮的点击事件的JavaScript代码中:

    $(function () {
                // 模态框的确定按钮的点击事件
                $("#delButton").click(function () {
                    var id = $("#del_id").val();
                    console.log("del" + id)
                    // ajax异步删除
                    $.ajax({
                        url: "/pub_del",
                        type: "POST",
                        data:{p_id:id},
                        success: function (result) {
                            if (result.code == "200") {
                                $("#delModal").modal("hide");
                                alert(result.message);
                                window.location.href = "{{ url_for('pub_list') }}";
                            }
                            else{
                                alert(result.message);
                            }
                        }
                    })
                });
            });


    语句:

    $("#delModal").modal("hide");


    - $("#delModal"):通过 jQuery 选择器选中 HTML 中 id="delModal" 的模态框容器元素。
    - .modal("hide"):调用 Bootstrap 的 modal 方法,并传递参数 "hide",表示关闭(隐藏)该模态框。


    与其他关闭方式的区别:

    关闭方式

    描述

    是否需要编码

    $("#delModal").modal("hide")

    通过 JavaScript 主动触发关闭逻辑,需手动调用。

    是(需编写代码)

    data-dismiss="modal"

    点击按钮或链接时自动关闭模态框(依赖 HTML 属性)。

    否(无需编码)

    点击模态框外部(背景)

    默认行为下,点击模态框外的背景区域会关闭模态框(需设置backdrop: true)。

    否(依赖配置)

    关于《Flask2+Vue.js实战派》第8章的笔记暂时就写到这里,欢迎各位同学补充。

    实战派 ESP32-S3,双模无线开发板

    ESP32-S3 原生支持 ESP-IDF,WiFi + 蓝牙一次搞定

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值