Flask入门学习教程,从入门到精通, Flask模板 — 完整知识点与案例代码 (2)

Flask模板 --- 完整知识点与案例代码


一、模板与模板引擎 Jinja2

1.1 什么是模板

模板是一个包含动态内容占位符 的文本文件(通常是 .html),Flask 在渲染时将变量替换为实际值,最终返回完整的 HTML 给浏览器。

1.2 Jinja2 简介

Jinja2 是 Flask 内置的模板引擎,支持:

  • 变量替换
  • 过滤器
  • 控制结构(if / for)
  • 宏(macro,类似函数)
  • 模板继承(block / extends)

1.3 渲染模板的基本方法

python 复制代码
# ========================
# app.py --- Flask 应用主文件
# ========================

# 从 flask 模块导入 Flask 类(应用核心)和 render_template 函数(渲染模板)
from flask import Flask, render_template

# 创建 Flask 应用实例,__name__ 帮助 Flask 确定资源路径
app = Flask(__name__)

# 定义路由:当用户访问根路径 "/" 时执行下方函数
@app.route('/')
def index():
    # render_template() 会在 templates/ 文件夹中查找 index.html
    # 并将该模板渲染为完整 HTML 字符串返回给浏览器
    return render_template('index.html')

# 启动开发服务器,debug=True 开启调试模式(代码修改后自动重启)
if __name__ == '__main__':
    app.run(debug=True)
复制代码
项目目录结构:
myproject/
├── app.py
├── templates/          ← Flask 默认在此目录中查找模板
│   └── index.html
└── static/             ← 静态文件目录(CSS/JS/图片)
html 复制代码
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <!-- 这是一个纯静态模板示例,暂无动态内容 -->
    <h1>欢迎来到 Flask 模板世界!</h1>
</body>
</html>

二、模板基础语法

Jinja2 模板中有三种特殊定界符(分隔符号),分别用于不同用途:

定界符 用途 示例
{``{ ... }} 输出变量/表达式的值 {``{ username }}
{% ... %} 执行语句(if/for/macro等) {% if user %}
{# ... #} 注释(不会出现在HTML中) {# 这是注释 #}
python 复制代码
# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    # 传递多个变量给模板
    return render_template(
        'base_syntax.html',
        title='基础语法演示',       # 字符串变量
        username='张三',            # 字符串变量
        age=25,                     # 整数变量
        is_login=True,              # 布尔变量
        items=['苹果', '香蕉', '橘子']  # 列表变量
    )

if __name__ == '__main__':
    app.run(debug=True)
html 复制代码
<!-- templates/base_syntax.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    {# 页面标题,使用 {{ }} 输出变量 #}
    <title>{{ title }}</title>
</head>
<body>
    {# ============================ #}
    {# 1. 变量输出 --- 使用 {{ }} #}
    {# ============================ #}
    <h1>{{ title }}</h1>
    <p>用户名:{{ username }}</p>
    <p>年龄:{{ age }}</p>

    {# ============================ #}
    {# 2. 表达式计算 --- {{ }} 内可以写表达式 #}
    {# ============================ #}
    <p>明年年龄:{{ age + 1 }}</p>
    <p>用户名大写:{{ username.upper() }}</p>
    <p>用户名长度:{{ username | length }}</p>

    {# ============================ #}
    {# 3. 控制语句 --- 使用 {% %} #}
    {# ============================ #}
    {% if is_login %}
        <p>欢迎回来,{{ username }}!</p>
    {% else %}
        <p>请先登录。</p>
    {% endif %}

    {# ============================ #}
    {# 4. 循环 --- 使用 {% for %} #}
    {# ============================ #}
    <ul>
    {% for item in items %}
        <li>{{ item }}</li>
    {% endfor %}
    </ul>

    {# ============================ #}
    {# 5. Jinja2 注释 --- 使用 {# #} #}
    {# 这行注释不会出现在最终的 HTML 源码中 #}
    {# 而 HTML 注释 <!-- --> 会出现在源码中 #}
    <!-- 这是HTML注释,会出现在源码中 -->
</body>
</html>

三、模板变量

3.1 传递变量的方式

python 复制代码
# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/variables')
def variables_demo():
    # ---------- 方式一:使用关键字参数传递 ----------
    # 直接把变量名作为参数传入
    name = '李四'
    score = 95.6
    hobbies = ['阅读', '游泳', '编程']
    address = {'city': '北京', 'street': '长安街'}

    return render_template(
        'variables.html',
        name=name,            # 传递字符串
        score=score,          # 传递浮点数
        hobbies=hobbies,      # 传递列表
        address=address,      # 传递字典
        students=[            # 传递复杂列表(字典列表)
            {'name': '王五', 'age': 20, 'grade': 'A'},
            {'name': '赵六', 'age': 22, 'grade': 'B'},
            {'name': '孙七', 'age': 21, 'grade': 'A'},
        ]
    )

@app.route('/variables2')
def variables_demo2():
    # ---------- 方式二:使用 **locals() 传递 ----------
    # locals() 返回当前函数中所有局部变量的字典
    # 用 ** 解包后作为关键字参数传入
    username = '王小明'
    user_age = 30
    user_email = 'wangxm@example.com'

    # **locals() 等价于 username=username, user_age=user_age, user_email=user_email
    # 注意:locals() 包含所有局部变量,包括不需要的,建议谨慎使用
    return render_template('variables2.html', **locals())

if __name__ == '__main__':
    app.run(debug=True)
html 复制代码
<!-- templates/variables.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>模板变量演示</title>
</head>
<body>
    {# ========== 基本变量输出 ========== #}
    <h2>1. 基本变量</h2>
    <p>姓名:{{ name }}</p>
    <p>分数:{{ score }}</p>

    {# ========== 列表变量 ========== #}
    <h2>2. 列表访问</h2>
    <p>所有爱好:{{ hobbies }}</p>
    {# 使用索引访问列表元素,Jinja2 中用 . 或 [] 均可 #}
    <p>第一个爱好:{{ hobbies[0] }} 或 {{ hobbies.0 }}</p>
    <p>最后一个爱好:{{ hobbies[-1] }}</p>

    {# ========== 字典变量 ========== #}
    <h2>3. 字典访问</h2>
    {# 字典可以用 . 或 [] 访问键值 #}
    <p>城市:{{ address.city }} 或 {{ address['city'] }}</p>
    <p>街道:{{ address.street }}</p>

    {# ========== 复杂数据结构 ========== #}
    <h2>4. 字典列表(学生信息表)</h2>
    <table border="1" cellpadding="8">
        <tr>
            <th>姓名</th>
            <th>年龄</th>
            <th>等级</th>
        </tr>
        {% for stu in students %}
        <tr>
            {# 访问字典的键 #}
            <td>{{ stu.name }}</td>
            <td>{{ stu.age }}</td>
            <td>{{ stu.grade }}</td>
        </tr>
        {% endfor %}
    </table>

    {# ========== Jinja2 全局变量 ========== #}
    <h2>5. Jinja2 内置全局变量</h2>
    {# config: Flask 应用的配置对象 #}
    <p>应用名称:{{ config.APP_NAME if config.APP_NAME is defined else '未设置' }}</p>
    {# request: 当前请求对象 #}
    <p>请求路径:{{ request.path }}</p>
    {# session: 会话对象(需配置 SECRET_KEY) #}
    <p>会话数据:{{ session }}</p>
    {# g: 全局临时对象 #}
    <p>g 对象:{{ g }}</p>
</body>
</html>
html 复制代码
<!-- templates/variables2.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>变量传递方式二</title>
</head>
<body>
    {# 这些变量通过 **locals() 自动传入 #}
    <h1>用户信息</h1>
    <p>用户名:{{ username }}</p>
    <p>年龄:{{ user_age }}</p>
    <p>邮箱:{{ user_email }}</p>
</body>
</html>

3.2 变量的安全转义(XSS 防护)

python 复制代码
@app.route('/escape')
def escape_demo():
    # 包含 HTML 标签的字符串
    html_content = '<script>alert("XSS攻击!")</script>'
    safe_html = '<strong>这是安全的加粗文本</strong>'

    return render_template(
        'escape.html',
        html_content=html_content,
        safe_html=safe_html
    )
html 复制代码
<!-- templates/escape.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>转义演示</title>
</head>
<body>
    {# =============================== #}
    {# Jinja2 默认自动转义 HTML 特殊字符 #}
    {# < 变成 &lt;  > 变成 &gt;        #}
    {# 这是为了防止 XSS 攻击             #}
    {# =============================== #}

    {# 自动转义:HTML 标签会被转义为纯文本显示 #}
    <p>转义后的内容:{{ html_content }}</p>
    {# 页面上显示: <script>alert("XSS攻击!")</script> #}
    {# 但不会执行 JavaScript,因为被转义了 #}

    {# =============================== #}
    {# 使用 |safe 过滤器关闭转义        #}
    {# ⚠️ 只对可信内容使用 safe!       #}
    {# =============================== #}
    <p>安全标记后的HTML:{{ safe_html | safe }}</p>
    {# 这里的 <strong> 标签会正常渲染为加粗 #}

    {# =============================== #}
    {# 使用 Markup 对象(在 Python 端标记安全) #}
    {# =============================== #}
</body>
</html>
python 复制代码
# 在 Python 端使用 Markup 标记安全内容
from flask import Flask, render_template
from markupsafe import Markup  # 导入 Markup 类

app = Flask(__name__)

@app.route('/escape2')
def escape_demo2():
    # 用 Markup() 包裹的内容在模板中不会被自动转义
    safe_content = Markup('<h2 style="color:green">安全的标题</h2>')

    return render_template('escape2.html', safe_content=safe_content)
html 复制代码
<!-- templates/escape2.html -->
<body>
    {# safe_content 在 Python 端已被 Markup 标记为安全,不会被转义 #}
    {{ safe_content }}
    {# 页面渲染为:<h2 style="color:green">安全的标题</h2> #}
</body>

四、过滤器

4.1 过滤器基本语法

过滤器使用管道符号 | 对变量进行处理,格式:{``{ 变量 | 过滤器名(参数) }}

python 复制代码
# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/filters')
def filters_demo():
    return render_template(
        'filters.html',
        name='   flask Jinja2 模板引擎   ',   # 带空格的字符串
        text='<p>这是HTML段落</p>',              # 含HTML的字符串
        num=3.14159265,                         # 浮点数
        price=12345.678,                        # 价格
        items=['Python', 'Flask', 'Jinja2'],    # 列表
        scores=[85, 92, 78, 95, 60],            # 数字列表
        my_dict={'name': '张三', 'age': 25},    # 字典
        my_html='<h1>标题</h1><p>段落</p>',       # HTML字符串
        long_text='这是一段很长很长很长很长很长很长很长很长的文本内容需要被截断显示',
        words='hello world flask jinja2',       # 空格分隔的字符串
        is_none=None,                           # None值
        empty_list=[],                          # 空列表
    )

if __name__ == '__main__':
    app.run(debug=True)
html 复制代码
<!-- templates/filters.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>过滤器大全</title>
    <style>
        body { font-family: sans-serif; padding: 20px; }
        table { border-collapse: collapse; width: 100%; margin: 10px 0; }
        th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
        th { background: #f0f0f0; }
        .section { margin: 30px 0; }
    </style>
</head>
<body>
    <h1>Jinja2 内置过滤器完整演示</h1>

    {# ======================================== #}
    {# 一、字符串相关过滤器                        #}
    {# ======================================== #}
    <div class="section">
    <h2>1. 字符串过滤器</h2>
    <table>
        <tr><th>过滤器</th><th>代码</th><th>结果</th></tr>

        {# safe: 标记字符串为安全,不进行HTML转义 #}
        <tr>
            <td>safe</td>
            <td>{{ '{{ text | safe }}' }}</td>
            <td>{{ text | safe }}</td>
        </tr>

        {# capitalize: 首字母大写,其余小写 #}
        <tr>
            <td>capitalize</td>
            <td>{{ '{{ name | capitalize }}' }}</td>
            <td>{{ name | capitalize }}</td>
        </tr>

        {# title: 每个单词首字母大写 #}
        <tr>
            <td>title</td>
            <td>{{ '{{ name | title }}' }}</td>
            <td>{{ name | title }}</td>
        </tr>

        {# upper: 全部转为大写 #}
        <tr>
            <td>upper</td>
            <td>{{ '{{ name | upper }}' }}</td>
            <td>{{ name | upper }}</td>
        </tr>

        {# lower: 全部转为小写 #}
        <tr>
            <td>lower</td>
            <td>{{ '{{ name | lower }}' }}</td>
            <td>{{ name | lower }}</td>
        </tr>

        {# trim: 去除两端空白字符 #}
        <tr>
            <td>trim</td>
            <td>{{ '{{ name | trim }}' }}</td>
            <td>{{ name | trim }}</td>
        </tr>

        {# truncate(length): 截断字符串到指定长度,默认255 #}
        {# 默认在最后一个空格处截断,并添加 "..." #}
        <tr>
            <td>truncate(20)</td>
            <td>{{ '{{ long_text | truncate(20) }}' }}</td>
            <td>{{ long_text | truncate(20) }}</td>
        </tr>

        {# truncate(length, killwords=True): 不保留完整单词 #}
        <tr>
            <td>truncate(20, killwords=True)</td>
            <td>{{ '{{ long_text | truncate(20, killwords=True) }}' }}</td>
            <td>{{ long_text | truncate(20, killwords=True) }}</td>
        </tr>

        {# truncate(length, end='...'): 自定义截断后缀 #}
        <tr>
            <td>truncate(15, end='~~')</td>
            <td>{{ '{{ long_text | truncate(15, end="~~") }}' }}</td>
            <td>{{ long_text | truncate(15, end='~~') }}</td>
        </tr>

        {# replace(old, new): 字符串替换 #}
        <tr>
            <td>replace</td>
            <td>{{ '{{ name | replace("Jinja2", "Django") }}' }}</td>
            <td>{{ name | replace('Jinja2', 'Django') }}</td>
        </tr>

        {# striptags: 去除所有HTML标签 #}
        <tr>
            <td>striptags</td>
            <td>{{ '{{ my_html | striptags }}' }}</td>
            <td>{{ my_html | striptags }}</td>
        </tr>

        {# join(sep): 将列表用指定分隔符连接为字符串 #}
        <tr>
            <td>join(' / ')</td>
            <td>{{ '{{ items | join(" / ") }}' }}</td>
            <td>{{ items | join(' / ') }}</td>
        </tr>

        {# center(width): 将字符串居中,两侧用空格填充到指定宽度 #}
        <tr>
            <td>center(30)</td>
            <td>【{{ 'hello' | center(30) }}】</td>
        </tr>

        {# wordwrap: 按指定宽度换行 #}
        <tr>
            <td>wordwrap</td>
            <td>{{ words | wordwrap(10) }}</td>
        </tr>
    </table>
    </div>

    {# ======================================== #}
    {# 二、数值相关过滤器                          #}
    {# ======================================== #}
    <div class="section">
    <h2>2. 数值过滤器</h2>
    <table>
        <tr><th>过滤器</th><th>代码</th><th>结果</th></tr>

        {# round(precision, method): 四舍五入 #}
        {# method: 'common'(默认), 'ceil'(向上), 'floor'(向下) #}
        <tr>
            <td>round(2)</td>
            <td>{{ '{{ num | round(2) }}' }}</td>
            <td>{{ num | round(2) }}</td>
        </tr>
        <tr>
            <td>round(1, 'ceil')</td>
            <td>{{ '{{ num | round(1, "ceil") }}' }}</td>
            <td>{{ num | round(1, 'ceil') }}</td>
        </tr>
        <tr>
            <td>round(1, 'floor')</td>
            <td>{{ '{{ num | round(1, "floor") }}' }}</td>
            <td>{{ num | round(1, 'floor') }}</td>
        </tr>

        {# int: 转为整数 #}
        <tr>
            <td>int</td>
            <td>{{ '{{ num | int }}' }}</td>
            <td>{{ num | int }}</td>
        </tr>

        {# float: 转为浮点数 #}
        <tr>
            <td>float</td>
            <td>{{ '{{ 5 | float }}' }}</td>
            <td>{{ 5 | float }}</td>
        </tr>

        {# abs: 取绝对值 #}
        <tr>
            <td>abs</td>
            <td>{{ '{{ -42 | abs }}' }}</td>
            <td>{{ -42 | abs }}</td>
        </tr>

        {# sum: 对列表求和 #}
        <tr>
            <td>sum</td>
            <td>{{ '{{ scores | sum }}' }}</td>
            <td>{{ scores | sum }}</td>
        </tr>

        {# min / max: 取最小值 / 最大值 #}
        <tr>
            <td>min / max</td>
            <td>{{ '{{ scores | min }}' }} / {{ '{{ scores | max }}' }}</td>
            <td>{{ scores | min }} / {{ scores | max }}</td>
        </tr>

        {# length: 获取长度(列表元素个数、字符串长度) #}
        <tr>
            <td>length</td>
            <td>{{ '{{ scores | length }}' }}</td>
            <td>{{ scores | length }}</td>
        </tr>

        {# filesizeformat: 将字节数格式化为人类可读的文件大小 #}
        <tr>
            <td>filesizeformat</td>
            <td>{{ '{{ 1024000 | filesizeformat }}' }}</td>
            <td>{{ 1024000 | filesizeformat }}</td>
        </tr>
    </table>
    </div>

    {# ======================================== #}
    {# 三、列表相关过滤器                          #}
    {# ======================================== #}
    <div class="section">
    <h2>3. 列表过滤器</h2>
    <table>
        <tr><th>过滤器</th><th>代码</th><th>结果</th></tr>

        {# first: 取列表第一个元素 #}
        <tr>
            <td>first</td>
            <td>{{ '{{ items | first }}' }}</td>
            <td>{{ items | first }}</td>
        </tr>

        {# last: 取列表最后一个元素 #}
        <tr>
            <td>last</td>
            <td>{{ '{{ items | last }}' }}</td>
            <td>{{ items | last }}</td>
        </tr>

        {# length: 列表长度 #}
        <tr>
            <td>length</td>
            <td>{{ '{{ items | length }}' }}</td>
            <td>{{ items | length }}</td>
        </tr>

        {# sort: 排序(不改变原列表) #}
        <tr>
            <td>sort</td>
            <td>{{ '{{ scores | sort }}' }}</td>
            <td>{{ scores | sort }}</td>
        </tr>

        {# reverse: 反转列表 #}
        <tr>
            <td>reverse</td>
            <td>{{ '{{ items | reverse | list }}' }}</td>
            <td>{{ items | reverse | list }}</td>
        </tr>

        {# unique: 去重 #}
        <tr>
            <td>unique</td>
            <td>{{ '{{ [1,2,2,3,3,3] | unique | list }}' }}</td>
            <td>{{ [1,2,2,3,3,3] | unique | list }}</td>
        </tr>

        {# random: 从列表中随机选择一个元素 #}
        <tr>
            <td>random</td>
            <td>{{ '{{ items | random }}' }}</td>
            <td>{{ items | random }}</td>
        </tr>

        {# batch(num): 将列表分成指定大小的子列表 #}
        <tr>
            <td>batch(2)</td>
            <td>{{ '{{ scores | batch(2) | list }}' }}</td>
            <td>{{ scores | batch(2) | list }}</td>
        </tr>

        {# slice(num, fill_with): 将列表等分成指定数量的子列表 #}
        <tr>
            <td>slice(3)</td>
            <td>{{ '{{ scores | slice(3) | list }}' }}</td>
            <td>{{ scores | slice(3) | list }}</td>
        </tr>

        {# map(attribute): 提取字典列表中某个属性 #}
        <tr>
            <td>map('upper')</td>
            <td>{{ '{{ items | map("upper") | list }}' }}</td>
            <td>{{ items | map('upper') | list }}</td>
        </tr>

        {# selectattr / rejectattr: 按属性过滤 #}
        <tr>
            <td>attr 过滤</td>
            <td colspan="2">见下方字典过滤器</td>
        </tr>
    </table>
    </div>

    {# ======================================== #}
    {# 四、字典相关过滤器                          #}
    {# ======================================== #}
    <div class="section">
    <h2>4. 字典过滤器</h2>
    <table>
        <tr><th>过滤器</th><th>代码</th><th>结果</th></tr>

        {# dictsort: 按键名排序字典 #}
        <tr>
            <td>dictsort</td>
            <td>{{ '{{ my_dict | dictsort }}' }}</td>
            <td>{{ my_dict | dictsort }}</td>
        </tr>

        {# tojson: 将数据转为JSON格式字符串 #}
        <tr>
            <td>tojson</td>
            <td>{{ '{{ my_dict | tojson }}' }}</td>
            <td>{{ my_dict | tojson }}</td>
        </tr>

        {# attr(obj, 'key'): 获取对象属性 #}
        <tr>
            <td>attr</td>
            <td>{{ '{{ my_dict | attr("name") }}' }}</td>
            <td>{{ my_dict | attr('name') }}</td>
        </tr>
    </table>
    </div>

    {# ======================================== #}
    {# 五、默认值过滤器                            #}
    {# ======================================== #}
    <div class="section">
    <h2>5. 默认值过滤器</h2>
    <table>
        <tr><th>过滤器</th><th>代码</th><th>结果</th></tr>

        {# default(value): 如果变量未定义或为None,使用默认值 #}
        {# 注意:空字符串""不会触发默认值 #}
        <tr>
            <td>default('未设置')</td>
            <td>{{ '{{ is_none | default("未设置") }}' }}</td>
            <td>{{ is_none | default('未设置') }}</td>
        </tr>

        {# default(value, boolean=True): 空字符串""、0、False也会触发默认值 #}
        <tr>
            <td>default('空', boolean=True)</td>
            <td>{{ '{{ empty_list | default("空", boolean=True) }}' }}</td>
            <td>{{ empty_list | default('空', boolean=True) }}</td>
        </tr>

        {# d 是 default 的简写 #}
        <tr>
            <td>d('默认')</td>
            <td>{{ '{{ is_none | d("默认") }}' }}</td>
            <td>{{ is_none | d('默认') }}</td>
        </tr>
    </table>
    </div>

    {# ======================================== #}
    {# 六、过滤器链(多个过滤器串联使用)            #}
    {# ======================================== #}
    <div class="section">
    <h2>6. 过滤器链</h2>
    {# 多个过滤器用 | 依次连接,前一个的输出作为后一个的输入 #}
    <p>原始文本:「{{ long_text }}」</p>
    <p>去空白 → 大写 → 截断:{{ long_text | trim | upper | truncate(25) }}</p>
    <p>列表 → 排序 → 取第一个:{{ scores | sort | first }}</p>
    <p>列表 → 去重 → 排序 → 连接:{{ [3,1,2,2,3] | unique | sort | join(', ') }}</p>
    </div>
</body>
</html>

4.2 自定义过滤器

python 复制代码
# app.py --- 自定义过滤器
from flask import Flask, render_template
from datetime import datetime

app = Flask(__name__)

# ============================================================
# 自定义过滤器注册方式一:使用 @app.template_filter() 装饰器
# ============================================================

# 装饰器参数 'datetime_fmt' 是过滤器名称
# 在模板中通过 {{ 变量 | datetime_fmt }} 使用
@app.template_filter('datetime_fmt')
def datetime_fmt_filter(value, fmt='%Y年%m月%d日 %H:%M'):
    """
    自定义日期时间格式化过滤器
    参数:
        value: datetime 对象
        fmt: 格式化字符串,默认为 '年月日 时:分'
    返回:
        格式化后的字符串
    """
    if value is None:
        return ''
    # 使用 strftime 将 datetime 对象格式化为指定格式的字符串
    return value.strftime(fmt)


# 自定义过滤器:将价格格式化为带人民币符号的字符串
@app.template_filter('cny')
def cny_filter(value):
    """
    将数字格式化为人民币格式
    例如: 1234.5 → ¥1,234.50
    """
    # :,.2f 表示千位分隔符 + 两位小数
    return f'¥{value:,.2f}'


# 自定义过滤器:计算列表中及格(>=60)的数量
@app.template_filter('count_pass')
def count_pass_filter(scores, passing=60):
    """
    统计及格人数
    参数:
        scores: 分数列表
        passing: 及格线,默认60
    """
    # sum() 配合生成器表达式,统计满足条件的数量
    return sum(1 for s in scores if s >= passing)


# ============================================================
# 自定义过滤器注册方式二:使用 app.add_template_filter()
# ============================================================
def reverse_string(s):
    """反转字符串的过滤器"""
    # 切片 [::-1] 反转字符串
    return s[::-1]

# 手动注册过滤器,第一个参数是函数,第二个参数是过滤器名称
app.add_template_filter(reverse_string, 'reverse_str')


# ============================================================
# 路由
# ============================================================
@app.route('/custom_filters')
def custom_filters_demo():
    return render_template(
        'custom_filters.html',
        now=datetime.now(),                          # 当前时间
        price=2999.5,                                # 价格
        scores=[85, 42, 97, 55, 60, 78, 33, 91],    # 分数列表
        greeting='Hello Flask Jinja2',               # 字符串
    )

if __name__ == '__main__':
    app.run(debug=True)
html 复制代码
<!-- templates/custom_filters.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>自定义过滤器</title>
</head>
<body>
    <h1>自定义过滤器演示</h1>

    {# 调用自定义的日期格式化过滤器 #}
    <p>当前时间(默认格式):{{ now | datetime_fmt }}</p>
    {# 传入自定义格式参数 #}
    <p>当前时间(自定义格式):{{ now | datetime_fmt('%Y-%m-%d %H:%M:%S') }}</p>

    {# 调用人民币格式化过滤器 #}
    <p>商品价格:{{ price | cny }}</p>

    {# 调用及格统计过滤器 #}
    <p>分数列表:{{ scores }}</p>
    <p>及格人数:{{ scores | count_pass }} / {{ scores | length }}</p>
    {# 传入自定义及格线 #}
    <p>50分以上人数:{{ scores | count_pass(50) }}</p>

    {# 调用字符串反转过滤器(通过 add_template_filter 注册的) #}
    <p>原始字符串:{{ greeting }}</p>
    <p>反转后:{{ greeting | reverse_str }}</p>

    {# 过滤器链:反转 → 大写 #}
    <p>反转+大写:{{ greeting | reverse_str | upper }}</p>
</body>
</html>

五、选择结构(条件判断)

5.1 if / elif / else

python 复制代码
# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/conditions')
def conditions_demo():
    return render_template(
        'conditions.html',
        user={
            'name': '张三',
            'age': 17,
            'role': 'vip',        # 角色: guest, user, vip, admin
            'is_active': True,    # 账号是否激活
            'score': 88,          # 积分
        },
        items=[],                 # 空列表
        title=None,               # None 值
    )

if __name__ == '__main__':
    app.run(debug=True)
html 复制代码
<!-- templates/conditions.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>选择结构</title>
    <style>
        .box { border: 1px solid #ccc; padding: 15px; margin: 10px 0; border-radius: 5px; }
        .warn { color: orange; }
        .error { color: red; }
        .success { color: green; }
        .info { color: blue; }
    </style>
</head>
<body>
    <h1>Jinja2 选择结构演示</h1>

    {# ================================ #}
    {# 1. 基本 if / else                #}
    {# ================================ #}
    <div class="box">
    <h2>1. 基本 if/else</h2>
    {# if 语句用 {% %} 包裹,必须有 endif 关闭标签 #}
    {% if user.is_active %}
        <p class="success">用户 {{ user.name }} 已激活。</p>
    {% else %}
        <p class="error">用户 {{ user.name }} 未激活,请检查邮箱。</p>
    {% endif %}
    </div>

    {# ================================ #}
    {# 2. if / elif / else 多分支       #}
    {# ================================ #}
    <div class="box">
    <h2>2. 多分支判断 --- 用户角色</h2>
    {% if user.role == 'admin' %}
        <p class="error">管理员面板 --- 拥有全部权限</p>
    {% elif user.role == 'vip' %}
        <p class="success">VIP 用户 --- 拥有高级功能</p>
    {% elif user.role == 'user' %}
        <p class="info">普通用户 --- 基础功能</p>
    {% else %}
        <p class="warn">访客 --- 请登录后使用</p>
    {% endif %}
    </div>

    {# ================================ #}
    {# 3. 比较运算符                      #}
    {# ================================ #}
    <div class="box">
    <h2>3. 比较运算符</h2>
    {# Jinja2 支持: ==, !=, >, <, >=, <= #}
    <p>年龄:{{ user.age }}</p>

    {% if user.age >= 18 %}
        <p class="success">已成年(age >= 18)</p>
    {% else %}
        <p class="warn">未成年(age < 18),今年 {{ user.age }} 岁,还差 {{ 18 - user.age }} 年成年</p>
    {% endif %}

    {% if user.score != 0 %}
        <p>积分不为零(score != 0):{{ user.score }} 分</p>
    {% endif %}
    </div>

    {# ================================ #}
    {# 4. 逻辑运算符:and / or / not      #}
    {# ================================ #}
    <div class="box">
    <h2>4. 逻辑运算符</h2>

    {# and: 两个条件同时满足 #}
    {% if user.is_active and user.role == 'vip' %}
        <p class="success">活跃的VIP用户,可享受专属折扣!</p>
    {% endif %}

    {# or: 满足其一即可 #}
    {% if user.role == 'admin' or user.role == 'vip' %}
        <p class="info">管理员或VIP用户,可以查看高级内容</p>
    {% endif %}

    {# not: 取反 #}
    {% if not user.is_active %}
        <p class="error">用户未激活!</p>
    {% else %}
        <p class="success">用户状态正常(not user.is_active 为 False)</p>
    {% endif %}

    {# 组合使用 #}
    {% if user.is_active and (user.role == 'admin' or user.role == 'vip') %}
        <p class="success">活跃的高级用户</p>
    {% endif %}
    </div>

    {# ================================ #}
    {# 5. Jinja2 内置测试函数 (is)        #}
    {# ================================ #}
    <div class="box">
    <h2>5. 内置测试函数(is 关键字)</h2>

    {# defined / undefined: 变量是否已定义 #}
    {% if title is defined %}
        <p>title 已定义</p>
    {% else %}
        <p>title 未定义或为 None</p>
    {% endif %}

    {# none: 是否为 None #}
    {% if title is none %}
        <p>title 是 None</p>
    {% endif %}

    {# empty: 列表/字符串是否为空 #}
    {% if items is empty %}
        <p>items 列表为空</p>
    {% endif %}

    {# string / number / sequence / mapping: 类型判断 #}
    {% if user.name is string %}
        <p>user.name 是字符串类型</p>
    {% endif %}

    {% if user.age is number %}
        <p>user.age 是数字类型</p>
    {% endif %}

    {% if items is sequence %}
        <p>items 是序列类型(列表/元组)</p>
    {% endif %}

    {% if user is mapping %}
        <p>user 是映射类型(字典)</p>
    {% endif %}

    {# even / odd: 奇偶判断 #}
    {% if user.age is even %}
        <p>年龄是偶数</p>
    {% elif user.age is odd %}
        <p>年龄是奇数</p>
    {% endif %}

    {# divisibleby(num): 是否能被整除 #}
    {% if user.age is divisibleby(5) %}
        <p>年龄能被5整除</p>
    {% endif %}

    {# lower / upper: 字符串是否全小写/全大写 #}
    {% if 'hello' is lower %}
        <p>'hello' 全部是小写</p>
    {% endif %}

    {# 匹配正则表达式 #}
    {% if user.name is match('^张') %}
        <p>用户名以"张"开头</p>
    {% endif %}

    {# in: 成员判断 #}
    {% if 'vip' in ['admin', 'vip', 'svip'] %}
        <p>user.role 在高级角色列表中</p>
    {% endif %}
    </div>

    {# ================================ #}
    {# 6. if 嵌套                        #}
    {# ================================ #}
    <div class="box">
    <h2>6. 嵌套 if</h2>
    {% if user.is_active %}
        {% if user.role == 'admin' %}
            <p class="error">欢迎管理员 {{ user.name }}!</p>
        {% elif user.role == 'vip' %}
            {% if user.score >= 80 %}
                <p class="success">金牌VIP {{ user.name }},当前积分 {{ user.score }}</p>
            {% else %}
                <p class="info">VIP {{ user.name }},继续加油!积分 {{ user.score }}</p>
            {% endif %}
        {% else %}
            <p>普通用户 {{ user.name }}</p>
        {% endif %}
    {% else %}
        <p class="error">账号已禁用</p>
    {% endif %}
    </div>

    {# ================================ #}
    {# 7. 行内 if 表达式(三元运算)       #}
    {# ================================ #}
    <div class="box">
    <h2>7. 行内 if 表达式</h2>
    {# 语法: {{ 值1 if 条件 else 值2 }} #}
    <p>用户状态:{{ '已激活' if user.is_active else '未激活' }}</p>
    <p>角色等级:{{ '高级' if user.role in ['admin', 'vip'] else '普通' }}</p>
    {# 也可以没有 else,此时条件不满足返回空字符串 #}
    <p>{{ '★VIP专属' if user.role == 'vip' }}</p>
    </div>
</body>
</html>

六、循环结构

6.1 for 循环基础

python 复制代码
# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/loops')
def loops_demo():
    return render_template(
        'loops.html',
        fruits=['苹果', '香蕉', '橘子', '葡萄', '西瓜'],  # 简单列表
        users=[                                          # 字典列表
            {'name': '张三', 'age': 25, 'city': '北京'},
            {'name': '李四', 'age': 30, 'city': '上海'},
            {'name': '王五', 'age': 22, 'city': '广州'},
            {'name': '赵六', 'age': 28, 'city': '深圳'},
        ],
        scores={'语文': 90, '数学': 85, '英语': 92, '物理': 78},  # 字典
        matrix=[                      # 嵌套列表(二维数组)
            [1, 2, 3],
            [4, 5, 6],
            [7, 8, 9],
        ],
        empty_list=[],                # 空列表,用于演示 else
    )

if __name__ == '__main__':
    app.run(debug=True)
html 复制代码
<!-- templates/loops.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>循环结构</title>
    <style>
        body { font-family: sans-serif; padding: 20px; }
        .box { border: 1px solid #ccc; padding: 15px; margin: 15px 0; border-radius: 5px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: center; }
        th { background: #f5f5f5; }
        .odd { background: #fff9e6; }        /* 奇数行背景色 */
        .even { background: #f0f8ff; }       /* 偶数行背景色 */
        .highlight { background: #ffe0e0; }  /* 高亮背景 */
    </style>
</head>
<body>
    <h1>Jinja2 循环结构演示</h1>

    {# ================================ #}
    {# 1. 基本 for 循环                  #}
    {# ================================ #}
    <div class="box">
    <h2>1. 遍历简单列表</h2>
    <ul>
    {# for 变量名 in 可迭代对象 #}
    {% for fruit in fruits %}
        <li>{{ fruit }}</li>
    {% endfor %}
    </ul>
    </div>

    {# ================================ #}
    {# 2. loop 变量(循环内置变量)        #}
    {# ================================ #}
    <div class="box">
    <h2>2. loop 变量详解</h2>
    {#
        Jinja2 在每个 for 循环内部提供一个特殊的 loop 对象:
        loop.index   --- 当前迭代次数(从1开始)
        loop.index0  --- 当前迭代次数(从0开始)
        loop.revindex --- 距离结束还有多少次(从1开始计数)
        loop.revindex0 --- 距离结束还有多少次(从0开始计数)
        loop.first   --- 是否是第一次迭代(布尔值)
        loop.last    --- 是否是最后一次迭代(布尔值)
        loop.length  --- 序列总长度
        loop.cycle   --- 循环取值函数
        loop.depth   --- 当前递归深度(嵌套循环时,从1开始)
        loop.depth0  --- 当前递归深度(从0开始)
        loop.previtem --- 上一个元素的值(第一个元素时为 undefined)
        loop.nextitem --- 下一个元素的值(最后一个元素时为 undefined)
    #}
    <table>
        <tr>
            <th>loop.index</th>
            <th>loop.index0</th>
            <th>loop.revindex</th>
            <th>loop.revindex0</th>
            <th>loop.first</th>
            <th>loop.last</th>
            <th>loop.length</th>
            <th>元素</th>
        </tr>
        {% for fruit in fruits %}
        <tr class="{{ 'highlight' if loop.first or loop.last else '' }}">
            <td>{{ loop.index }}</td>
            <td>{{ loop.index0 }}</td>
            <td>{{ loop.revindex }}</td>
            <td>{{ loop.revindex0 }}</td>
            <td>{{ loop.first }}</td>
            <td>{{ loop.last }}</td>
            <td>{{ loop.length }}</td>
            <td>{{ fruit }}</td>
        </tr>
        {% endfor %}
    </table>
    </div>

    {# ================================ #}
    {# 3. loop.cycle --- 交替样式           #}
    {# ================================ #}
    <div class="box">
    <h2>3. loop.cycle --- 交替应用样式</h2>
    {# loop.cycle('样式1', '样式2', ...) 在每次循环中交替返回参数值 #}
    {% for fruit in fruits %}
        {# 循环中交替使用 odd 和 even 两个 CSS 类名 #}
        <p class="{{ loop.cycle('odd', 'even') }}">
            第 {{ loop.index }} 个:{{ fruit }}
        </p>
    {% endfor %}
    </div>

    {# ================================ #}
    {# 4. 遍历字典列表(表格展示)          #}
    {# ================================ #}
    <div class="box">
    <h2>4. 遍历字典列表</h2>
    <table>
        <tr>
            <th>序号</th>
            <th>姓名</th>
            <th>年龄</th>
            <th>城市</th>
            <th>状态</th>
        </tr>
        {% for user in users %}
        {# 使用 loop.cycle 交替行背景色 #}
        <tr class="{{ loop.cycle('odd', 'even') }}">
            {# loop.index 从1开始计数 #}
            <td>{{ loop.index }}</td>
            {# 访问字典的键 #}
            <td>{{ user.name }}</td>
            <td>{{ user.age }}</td>
            <td>{{ user.city }}</td>
            {# 行内 if:判断是否是第一个或最后一个 #}
            <td>
                {% if loop.first %}[第一条]{% endif %}
                {% if loop.last %}[最后一条]{% endif %}
            </td>
        </tr>
        {% endfor %}
    </table>
    </div>

    {# ================================ #}
    {# 5. 遍历字典(键值对)               #}
    {# ================================ #}
    <div class="box">
    <h2>5. 遍历字典</h2>
    {# 遍历字典时,可以用 items() 获取键值对 #}
    <table>
        <tr><th>科目</th><th>成绩</th><th>等级</th></tr>
        {% for subject, score in scores.items() %}
        <tr>
            <td>{{ subject }}</td>
            <td>{{ score }}</td>
            {# 嵌套 if 判断等级 #}
            <td>
                {% if score >= 90 %}
                    A (优秀)
                {% elif score >= 80 %}
                    B (良好)
                {% elif score >= 60 %}
                    C (及格)
                {% else %}
                    D (不及格)
                {% endif %}
            </td>
        </tr>
        {% endfor %}
    </table>
    {# 遍历字典的键 #}
    <p>所有科目:{% for subject in scores %}{{ subject }}{% if not loop.last %}、{% endif %}{% endfor %}</p>
    </div>

    {# ================================ #}
    {# 6. 嵌套循环                        #}
    {# ================================ #}
    <div class="box">
    <h2>6. 嵌套循环(九九乘法表)</h2>
    <table>
        {% for row in range(1, 10) %}
        <tr>
            {% for col in range(1, row + 1) %}
            {# 内层循环:只打印到当前行号为止 #}
            <td style="font-size: 12px; padding: 4px;">
                {{ col }}×{{ row }}={{ row * col }}
            </td>
            {% endfor %}
        </tr>
        {% endfor %}
    </table>
    </div>

    {# ================================ #}
    {# 7. 嵌套循环中的 loop 变量           #}
    {# ================================ #}
    <div class="box">
    <h2>7. 二维列表遍历</h2>
    <table>
        {% for row in matrix %}
        <tr>
            {% for cell in row %}
            {# 内层的 loop 是内层循环的 loop 对象 #}
            <td>{{ cell }} ({{ loop.index }}, {{ loop.parent.index }})</td>
            {# loop.parent 可以访问外层循环的 loop 对象 #}
            {% endfor %}
        </tr>
        {% endfor %}
    </table>
    </div>

    {# ================================ #}
    {# 8. for ... else                   #}
    {# ================================ #}
    <div class="box">
    <h2>8. for...else 结构</h2>
    {#
        Jinja2 特有的语法:
        如果 for 循环的可迭代对象为空(没有执行任何循环体),
        则执行 else 块中的内容。
        ⚠️ 与 Python 的 for...else 不同!
        Python 中 else 在没有 break 时执行;
        Jinja2 中 else 在可迭代对象为空时执行。
    #}
    <h3>有数据的列表:</h3>
    {% for fruit in fruits %}
        <span>{{ fruit }}{% if not loop.last %}、{% endif %}</span>
    {% else %}
        {# fruits 不为空,所以不会执行这里的代码 #}
        <p>没有水果数据</p>
    {% endfor %}

    <h3>空列表:</h3>
    {% for item in empty_list %}
        <p>{{ item }}</p>
    {% else %}
        {# empty_list 为空,执行这里 #}
        <p style="color: red;">列表为空,暂无数据!</p>
    {% endfor %}
    </div>

    {# ================================ #}
    {# 9. range() 函数循环                #}
    {# ================================ #}
    <div class="box">
    <h2>9. range() 循环</h2>
    {# range(stop): 0 到 stop-1 #}
    <p>range(5): {% for i in range(5) %}{{ i }} {% endfor %}</p>

    {# range(start, stop): start 到 stop-1 #}
    <p>range(3, 8): {% for i in range(3, 8) %}{{ i }} {% endfor %}</p>

    {# range(start, stop, step): 带步长 #}
    <p>range(0, 10, 2): {% for i in range(0, 10, 2) %}{{ i }} {% endfor %}</p>

    {# 倒序:range(5, 0, -1) #}
    <p>倒计时: {% for i in range(5, 0, -1) %}{{ i }}... {% endfor %}发射!</p>
    </div>

    {# ================================ #}
    {# 10. recursive(递归遍历树结构)     #}
    {# ================================ #}
    {# 递归循环用于遍历树形结构 #}
    {# 先在模板中定义一个递归宏(后续宏章节详解) #}
    <div class="box">
    <h2>10. 递归遍历(使用 recursive 关键字)</h2>
    {#
        假设有一个树形结构的列表:
        site_map = [
            {'name': '首页', 'children': []},
            {'name': '产品', 'children': [
                {'name': '手机', 'children': [
                    {'name': '小米14', 'children': []},
                    {'name': '小米14 Pro', 'children': []},
                ]},
                {'name': '电脑', 'children': []},
            ]},
            {'name': '关于我们', 'children': []},
        ]
    #}
    {# 这里用 Python 端传入的变量来演示 #}
    </div>

    {# ================================ #}
    {# 11. 循环中的特殊技巧                 #}
    {# ================================ #}
    <div class="box">
    <h2>11. 循环技巧</h2>

    {# 分隔符技巧:用 if not loop.last 在元素间添加分隔符 #}
    <p>逗号分隔:{% for f in fruits %}{{ f }}{% if not loop.last %},{% endif %}{% endfor %}</p>

    {# 使用 join 过滤器更简洁 #}
    <p>join方式:{{ fruits | join('、') }}</p>

    {# 条件循环:只遍历满足条件的元素 #}
    <p>30岁以上的用户:
        {% for user in users if user.age >= 30 %}
            {{ user.name }}({{ user.age }}岁){% if not loop.last %}、{% endif %}
        {% endfor %}
    </p>

    {# 配合 slice 过滤器实现分列显示 #}
    <h3>分3列显示:</h3>
    <table>
        {% for row in fruits | batch(3, '-') %}
        <tr>
            {% for item in row %}
            <td>{{ item }}</td>
            {% endfor %}
        </tr>
        {% endfor %}
    </table>
    </div>
</body>
</html>

七、宏的定义与调用(Macro)

宏类似于 Python 中的函数,可以封装可复用的模板代码,接受参数,避免重复。

7.1 宏的定义

python 复制代码
# app.py
from flask import Flask, render_template

app = Flask(__name__)

@app.route('/macro')
def macro_demo():
    return render_template(
        'macro_demo.html',
        users=[
            {'name': '张三', 'age': 25, 'email': 'zhangsan@example.com'},
            {'name': '李四', 'age': 30, 'email': 'lisi@example.com'},
            {'name': '王五', 'age': 22, 'email': 'wangwu@example.com'},
        ],
        form_data={'username': '', 'password': '', 'email': 'test@example.com'},
    )

if __name__ == '__main__':
    app.run(debug=True)
html 复制代码
<!-- templates/macro_demo.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>宏演示</title>
    <style>
        body { font-family: sans-serif; padding: 20px; max-width: 900px; margin: auto; }
        .box { border: 1px solid #ddd; padding: 20px; margin: 20px 0; border-radius: 8px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ccc; padding: 10px; text-align: left; }
        th { background: #f0f0f0; }
        input { padding: 6px 10px; margin: 3px 0; border: 1px solid #ccc; border-radius: 4px; }
        label { display: inline-block; width: 100px; }
        .btn { background: #4CAF50; color: white; padding: 8px 20px; border: none; border-radius: 4px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>宏(Macro)完整演示</h1>

    {# ============================================================ #}
    {# 一、宏的定义                                                      #}
    {# 语法: {% macro 宏名(参数1, 参数2, ...) %} ... {% endmacro %} #}
    {# ============================================================ #}

    {# ---------- 宏1:生成用户信息卡片 ---------- #}
    {% macro user_card(name, age, email, role='普通用户') %}
    {#
        参数说明:
        name  --- 用户名(必填)
        age   --- 年龄(必填)
        email --- 邮箱(必填)
        role  --- 角色(可选,默认 '普通用户')
    #}
    <div style="border: 1px solid #2196F3; padding: 10px; margin: 5px 0; border-radius: 5px; background: #f0f8ff;">
        <strong>👤 {{ name }}</strong> |
        年龄:{{ age }} |
        邮箱:{{ email }} |
        角色:{{ role }}
    </div>
    {% endmacro %}

    {# ---------- 宏2:生成表单输入框 ---------- #}
    {% macro form_input(name, label_text, type='text', value='', placeholder='', required=false) %}
    {#
        参数说明:
        name        --- input 的 name 属性
        label_text  --- 标签文字
        type        --- 输入类型,默认 'text'(可选 password/email/number等)
        value       --- 默认值
        placeholder --- 占位文字
        required    --- 是否必填(布尔值)
    #}
    <div style="margin: 10px 0;">
        {# 使用 label 标签,for 属性关联 input 的 id #}
        <label for="{{ name }}">{{ label_text }}:</label>
        {# 输入框,使用 Jinja2 的行内 if 设置属性 #}
        <input
            type="{{ type }}"
            id="{{ name }}"
            name="{{ name }}"
            value="{{ value }}"
            placeholder="{{ placeholder }}"
            {{ 'required' if required else '' }}
        >
    </div>
    {% endmacro %}

    {# ---------- 宏3:生成表格 ---------- #}
    {% macro data_table(headers, rows) %}
    {#
        参数说明:
        headers --- 表头列表,如 ['姓名', '年龄', '邮箱']
        rows    --- 数据列表,如 [{'name':'张三', 'age':25, 'email':'...'}, ...]
        注意:headers 的顺序应与 rows 中字典键的顺序一致
    #}
    <table>
        <tr>
            {# 遍历表头列表,生成 <th> 标签 #}
            {% for header in headers %}
            <th>{{ header }}</th>
            {% endfor %}
        </tr>
        {# 遍历数据行 #}
        {% for row in rows %}
        <tr>
            {# 遍历字典的值,生成 <td> 标签 #}
            {% for value in row.values() %}
            <td>{{ value }}</td>
            {% endfor %}
        </tr>
        {% endfor %}
    </table>
    {% endmacro %}

    {# ---------- 宏4:生成按钮组 ---------- #}
    {% macro button_group(buttons) %}
    {#
        参数说明:
        buttons --- 字典列表,每项包含 text(按钮文字)和 type(按钮类型/样式类名)
    #}
    <div style="margin: 10px 0;">
        {% for btn in buttons %}
        <button class="btn" style="background: {{ btn.color | default('#4CAF50') }};">
            {{ btn.text }}
        </button>
        {% endfor %}
    </div>
    {% endmacro %}

    {# ---------- 宏5:带 caller 的宏(可调用块) ---------- #}
    {% macro alert_box(type='info', title='提示') %}
    {#
        caller 是 Jinja2 的高级特性:
        允许宏接受一段 HTML 块作为"内容"(类似 slot/插槽)
        定义了 caller 的宏,调用时需要用 {% call %} 语法
    #}
    <div style="border: 2px solid
        {% if type == 'success' %}#4CAF50
        {% elif type == 'error' %}#f44336
        {% elif type == 'warning' %}#ff9800
        {% else %}#2196F3{% endif %};
        padding: 15px; margin: 10px 0; border-radius: 5px;
        background: {% if type == 'success' %}#f0fff0
        {% elif type == 'error' %}#fff0f0
        {% elif type == 'warning' %}#fff8e1
        {% else %}#f0f8ff{% endif %};">
        <strong>{{ title }}</strong>
        <div style="margin-top: 8px;">
            {# caller() 渲染 {% call %} 块中传入的内容 #}
            {{ caller() }}
        </div>
    </div>
    {% endmacro %}

    {# ============================================================ #}
    {# 二、宏的调用                                                      #}
    {# ============================================================ #}

    {# ---------- 调用宏1:用户卡片 ---------- #}
    <div class="box">
    <h2>1. 调用 user_card 宏</h2>

    {# 方式一:在同一模板中直接调用(宏定义在同一个文件中) #}
    {{ user_card('张三', 25, 'zhangsan@example.com') }}
    {# 使用默认参数 role='普通用户' #}

    {# 传递所有参数(包括可选参数) #}
    {{ user_card('李四', 30, 'lisi@example.com', role='管理员') }}

    {# 使用关键字参数(顺序可以不一致) #}
    {{ user_card(role='VIP', name='王五', age=22, email='wangwu@example.com') }}

    {# 配合循环批量调用 #}
    <h3>循环调用宏:</h3>
    {% for user in users %}
        {{ user_card(user.name, user.age, user.email) }}
    {% endfor %}
    </div>

    {# ---------- 调用宏2:表单输入框 ---------- #}
    <div class="box">
    <h2>2. 调用 form_input 宏(登录表单)</h2>
    <form>
        {{ form_input('username', '用户名', placeholder='请输入用户名', required=true) }}
        {{ form_input('password', '密码', type='password', placeholder='请输入密码', required=true) }}
        {{ form_input('email', '邮箱', type='email', value=form_data.email, placeholder='请输入邮箱') }}
        {{ form_input('age', '年龄', type='number', placeholder='请输入年龄') }}
        <button class="btn" type="submit">提交</button>
    </form>
    </div>

    {# ---------- 调用宏3:数据表格 ---------- #}
    <div class="box">
    <h2>3. 调用 data_table 宏</h2>
    {{ data_table(
        headers=['姓名', '年龄', '邮箱'],
        rows=users
    ) }}
    </div>

    {# ---------- 调用宏4:按钮组 ---------- #}
    <div class="box">
    <h2>4. 调用 button_group 宏</h2>
    {{ button_group([
        {'text': '保存', 'color': '#4CAF50'},
        {'text': '编辑', 'color': '#2196F3'},
        {'text': '删除', 'color': '#f44336'},
        {'text': '返回', 'color': '#9e9e9e'},
    ]) }}
    </div>

    {# ---------- 调用带 caller 的宏 ---------- #}
    <div class="box">
    <h2>5. 调用带 caller 的 alert_box 宏</h2>

    {# 使用 {% call %} 语法调用,中间的内容会传给宏内的 caller() #}
    {% call alert_box(type='success', title='操作成功') %}
        <p>数据已成功保存到数据库!</p>
        <p>影响了 3 条记录。</p>
    {% endcall %}

    {% call alert_box(type='error', title='错误') %}
        <p>网络连接失败,请检查网络设置。</p>
    {% endcall %}

    {% call alert_box(type='warning', title='警告') %}
        <p>此操作不可恢复,请谨慎操作!</p>
    {% endcall %}

    {% call alert_box(type='info', title='提示') %}
        <p>系统将于今晚 22:00 进行维护。</p>
    {% endcall %}
    </div>

</body>
</html>

7.2 宏的导入(跨模板复用)

html 复制代码
<!-- templates/macros/forms.html -->
{# ============================================================ #}
{# 将宏定义在单独的文件中,方便在多个模板中复用                      #}
{# 这个文件专门存放表单相关的宏                                     #}
{# ============================================================ #}

{# 输入框宏 #}
{% macro input(name, label='', type='text', value='', placeholder='', required=false) %}
<div style="margin: 8px 0;">
    {% if label %}
    <label for="{{ name }}">{{ label }}</label>
    {% endif %}
    <input type="{{ type }}" id="{{ name }}" name="{{ name }}"
           value="{{ value }}" placeholder="{{ placeholder }}"
           {{ 'required' if required else '' }}
           style="padding: 6px; border: 1px solid #ccc; border-radius: 4px;">
</div>
{% endmacro %}

{# 文本域宏 #}
{% macro textarea(name, label='', rows=4, value='', placeholder='') %}
<div style="margin: 8px 0;">
    {% if label %}
    <label for="{{ name }}">{{ label }}</label><br>
    {% endif %}
    <textarea id="{{ name }}" name="{{ name }}" rows="{{ rows }}"
              placeholder="{{ placeholder }}"
              style="width: 100%; padding: 6px; border: 1px solid #ccc; border-radius: 4px;">{{ value }}</textarea>
</div>
{% endmacro %}

{# 下拉选择框宏 #}
{% macro select(name, label='', options=[], selected='') %}
{#
    参数说明:
    name     --- select 的 name 属性
    label    --- 标签文字
    options  --- 选项列表,每项是 {'value': '...', 'text': '...'} 或简单字符串
    selected --- 默认选中的值
#}
<div style="margin: 8px 0;">
    {% if label %}
    <label for="{{ name }}">{{ label }}</label>
    {% endif %}
    <select id="{{ name }}" name="{{ name }}"
            style="padding: 6px; border: 1px solid #ccc; border-radius: 4px;">
        {% for opt in options %}
        {# 兼容字典和简单字符串两种格式 #}
        {% if opt is mapping %}
            <option value="{{ opt.value }}" {{ 'selected' if opt.value == selected else '' }}>
                {{ opt.text }}
            </option>
        {% else %}
            <option value="{{ opt }}" {{ 'selected' if opt == selected else '' }}>
                {{ opt }}
            </option>
        {% endif %}
        {% endfor %}
    </select>
</div>
{% endmacro %}

{# 复选框宏 #}
{% macro checkbox(name, label, checked=false) %}
<div style="margin: 8px 0;">
    <input type="checkbox" id="{{ name }}" name="{{ name }}"
           {{ 'checked' if checked else '' }}>
    <label for="{{ name }}">{{ label }}</label>
</div>
{% endmacro %}

{# 提交按钮宏 #}
{% macro submit(text='提交', color='#4CAF50') %}
<button type="submit"
        style="background: {{ color }}; color: white; padding: 10px 24px;
               border: none; border-radius: 4px; cursor: pointer; font-size: 14px;">
    {{ text }}
</button>
{% endmacro %}
html 复制代码
<!-- templates/macro_import.html -->
{# ============================================================ #}
{# 三、使用 import / from...import 导入外部宏                       #}
{# ============================================================ #}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>宏的导入</title>
</head>
<body>
    <h1>宏的跨模板导入演示</h1>

    {# ------ 方式一:import 导入整个宏文件,用命名空间访问 ------ #}
    {% import 'macros/forms.html' as forms %}

    <div style="border: 1px solid #ccc; padding: 20px; margin: 20px 0;">
        <h2>方式一:{% raw %}{% import 'macros/forms.html' as forms %}{% endraw %}</h2>
        <form>
            {# 使用 命名空间.宏名() 的方式调用 #}
            {{ forms.input('username', '用户名', placeholder='请输入用户名', required=true) }}
            {{ forms.input('password', '密码', type='password', placeholder='请输入密码') }}
            {{ forms.select('city', '城市', options=[
                {'value': 'bj', 'text': '北京'},
                {'value': 'sh', 'text': '上海'},
                {'value': 'gz', 'text': '广州'},
            ], selected='sh') }}
            {{ forms.checkbox('remember', '记住我', checked=true) }}
            {{ forms.submit('登录') }}
        </form>
    </div>

    {# ------ 方式二:from...import 导入指定宏 ------ #}
    {% from 'macros/forms.html' import input, submit as btn %}

    <div style="border: 1px solid #ccc; padding: 20px; margin: 20px 0;">
        <h2>方式二:{% raw %}{% from 'macros/forms.html' import input, submit as btn %}{% endraw %}</h2>
        <form>
            {# 直接使用宏名调用(无需命名空间前缀) #}
            {{ input('email', '邮箱', type='email', placeholder='请输入邮箱') }}
            {# 使用 as 重命名:submit 导入为 btn #}
            {{ btn('注册', color='#2196F3') }}
        </form>
    </div>

    {# ------ 方式三:with context --- 导入宏时传递模板上下文变量 ------ #}
    {% from 'macros/forms.html' import input with context %}
    {#
        加了 with context 后,导入的宏可以访问当前模板的所有变量
        不加的话,宏无法访问当前模板的变量(只能使用宏的参数)
        注意:with context 可能影响性能,仅在需要时使用
    #}

</body>
</html>

7.3 宏的高级特性

html 复制代码
<!-- templates/macro_advanced.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>宏的高级特性</title>
</head>
<body>

    {# ========== 1. 宏内部的 caller 对象 ========== #}
    {# 定义一个布局宏,使用 caller() 插入外部内容 #}
    {% macro card(title, width='300px') %}
    <div style="border: 1px solid #ddd; width: {{ width }}; border-radius: 8px; overflow: hidden; margin: 10px;">
        {# 卡片头部 #}
        <div style="background: #f5f5f5; padding: 10px; font-weight: bold;">
            {{ title }}
        </div>
        {# 卡片正文:caller() 渲染调用者传入的内容块 #}
        <div style="padding: 15px;">
            {{ caller() }}
        </div>
    </div>
    {% endmacro %}

    {# 调用带 caller 的宏 #}
    {% call card('用户信息', width='400px') %}
        {# 这里的 HTML 内容会传给宏中的 caller() #}
        <p>姓名:张三</p>
        <p>年龄:25</p>
        <p>邮箱:zhangsan@example.com</p>
    {% endcall %}

    {# ========== 2. caller 中传递参数 ========== #}
    {% macro list_container(title) %}
    <h3>{{ title }}</h3>
    <ul>
        {# caller 可以接受参数,在 call 块中用 set 接收 #}
        {{ caller('列表项1') }}
        {{ caller('列表项2') }}
        {{ caller('列表项3') }}
    </ul>
    {% endmacro %}

    {# call 块中接收 caller 传来的参数 #}
    {% call(item) list_container('带参数的caller') %}
        <li>{{ item }} --- 自定义内容</li>
    {% endcall %}

    {# ========== 3. variadic 宏(可变参数) ========== #}
    {# Jinja2 不直接支持 *args/**kwargs,但可以通过 caller 或特殊变量实现 #}
    {# 使用 varargs 关键字访问多余的位置参数 #}
    {% macro debug_info() %}
    <pre>
    调用参数(varargs):{{ varargs }}
    调用参数数量:{{ varargs | length }}
    </pre>
    {% endmacro %}

    {# 调用时传入任意数量的参数 #}
    {{ debug_info('a', 'b', 'c', 'd') }}

    {# ========== 4. kwargs 获取关键字参数 ========== #}
    {% macro show_attrs() %}
    <ul>
        {# kwargs 是一个字典,包含所有未在宏参数列表中声明的关键字参数 #}
        {% for key, value in kwargs.items() %}
        <li>{{ key }}: {{ value }}</li>
        {% endfor %}
    </ul>
    {% endmacro %}

    {{ show_attrs(name='张三', age=25, city='北京', hobby='编程') }}

    {# ========== 5. 宏的内部变量 ========== #}
    {% macro inspect_macro(arg1, arg2='默认值') %}
    <pre>
    参数 arg1 = {{ arg1 }}
    参数 arg2 = {{ arg2 }}
    宏的名称: {{ caller | default('无caller') }}
    当前模板名: {{ _template }}
    </pre>
    {% endmacro %}

    {{ inspect_macro('测试') }}

</body>
</html>

八、消息闪现(Flash Messages)

消息闪现用于在一次请求中存储消息 ,在下一次请求中显示,然后自动清除。常用于表单提交后的提示、操作成功/失败的反馈等。

python 复制代码
# app.py --- 消息闪现完整示例
from flask import Flask, render_template, request, redirect, url_for, flash

app = Flask(__name__)

# flash() 必须设置 SECRET_KEY,用于对 session 中的消息进行加密签名
# SECRET_KEY 应该是一个复杂的随机字符串,生产环境中不要硬编码
app.secret_key = 'your-secret-key-here-change-in-production'

@app.route('/flash_demo', methods=['GET', 'POST'])
def flash_demo():
    if request.method == 'POST':
        # 获取表单数据
        username = request.form.get('username', '').strip()
        email = request.form.get('email', '').strip()

        # ---------- 表单验证 ----------
        if not username:
            # flash(message, category) --- 第二个参数是消息类别
            # 默认类别是 'message'
            # 常用类别:'success', 'info', 'warning', 'error'/'danger'
            flash('用户名不能为空!', 'error')
        elif len(username) < 2:
            flash('用户名至少需要2个字符!', 'warning')
        elif not email:
            flash('邮箱不能为空!', 'error')
        elif '@' not in email:
            flash('请输入有效的邮箱地址!', 'warning')
        else:
            # 验证通过,模拟注册成功
            flash(f'用户 {username} 注册成功!欢迎加入!', 'success')
            flash('验证邮件已发送至您的邮箱,请查收。', 'info')

            # 重定向到 GET 请求(避免刷新页面重复提交)
            return redirect(url_for('flash_demo'))

    # GET 请求:渲染模板
    return render_template('flash_demo.html')


@app.route('/flash_clear')
def flash_clear():
    """演示手动清除所有闪现消息"""
    flash('这是一条消息', 'info')
    flash('这是另一条消息', 'success')
    # 注意:消息会在下次 get_flashed_messages() 调用后自动清除
    return redirect(url_for('show_messages'))


@app.route('/show_messages')
def show_messages():
    return render_template('show_messages.html')


if __name__ == '__main__':
    app.run(debug=True)
html 复制代码
<!-- templates/flash_demo.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>消息闪现演示</title>
    <style>
        body { font-family: sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
        .flash-messages { margin: 20px 0; }
        .flash {
            padding: 12px 16px;
            margin: 8px 0;
            border-radius: 4px;
            border-left: 4px solid;
            animation: slideIn 0.3s ease;
        }
        /* 不同消息类别的样式 */
        .flash-success { background: #f0fff0; border-color: #4CAF50; color: #2e7d32; }
        .flash-error   { background: #fff0f0; border-color: #f44336; color: #c62828; }
        .flash-warning { background: #fff8e1; border-color: #ff9800; color: #e65100; }
        .flash-info    { background: #f0f8ff; border-color: #2196F3; color: #1565c0; }
        .flash-message { background: #f5f5f5; border-color: #9e9e9e; color: #424242; }
        @keyframes slideIn {
            from { opacity: 0; transform: translateX(-20px); }
            to   { opacity: 1; transform: translateX(0); }
        }
        input { padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 100%; margin: 5px 0; }
        label { font-weight: bold; margin-top: 10px; display: block; }
        button { background: #4CAF50; color: white; padding: 10px 24px; border: none; border-radius: 4px; cursor: pointer; margin-top: 15px; }
    </style>
</head>
<body>
    <h1>消息闪现(Flash Messages)</h1>

    {# ============================================================ #}
    {# 显示闪现消息                                                       #}
    {# get_flashed_messages(with_categories=true)                      #}
    {#   --- 获取所有闪现消息                                               #}
    {#   --- with_categories=true 返回 (category, message) 元组列表     #}
    {#   --- 调用后消息自动从 session 中清除(只显示一次)                    #}
    {#   --- category_filter 参数可以只获取指定类别的消息                     #}
    {# ============================================================ #}

    <div class="flash-messages">
    {# with_categories=true:同时获取消息类别和消息内容 #}
    {% with messages = get_flashed_messages(with_categories=true) %}
        {# messages 是一个列表,元素为 (类别, 消息内容) 的元组 #}
        {% if messages %}
            {% for category, message in messages %}
            {# 根据类别设置不同的 CSS 类 #}
            <div class="flash flash-{{ category }}">
                {# 类别图标标识 #}
                {% if category == 'success' %}[ 成功 ]
                {% elif category == 'error' %}[ 错误 ]
                {% elif category == 'warning' %}[ 警告 ]
                {% elif category == 'info' %}[ 提示 ]
                {% else %}[ 消息 ]
                {% endif %}
                {{ message }}
            </div>
            {% endfor %}
        {% endif %}
    {% endwith %}
    </div>

    {# ============================================================ #}
    {# 注册表单                                                          #}
    {# ============================================================ #}
    <form method="POST" action="{{ url_for('flash_demo') }}">
        <label for="username">用户名:</label>
        <input type="text" id="username" name="username" placeholder="请输入用户名">

        <label for="email">邮箱:</label>
        <input type="email" id="email" name="email" placeholder="请输入邮箱">

        <button type="submit">注册</button>
    </form>

    {# ============================================================ #}
    {# 只显示特定类别的消息                                                #}
    {# ============================================================ #}
    <h3>仅显示错误消息示例(单独获取):</h3>
    {# category_filter 参数:只获取 'error' 类别的消息 #}
    {% set error_messages = get_flashed_messages(category_filter=['error']) %}
    {% if error_messages %}
        {% for msg in error_messages %}
        <p style="color: red;">错误:{{ msg }}</p>
        {% endfor %}
    {% else %}
        <p style="color: green;">没有错误消息</p>
    {% endif %}
</body>
</html>
html 复制代码
<!-- templates/show_messages.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>查看消息</title>
</head>
<body>
    <h1>消息列表</h1>

    {# 不带类别,只获取消息内容 #}
    {% with messages = get_flashed_messages() %}
        {% if messages %}
            <ul>
            {% for message in messages %}
                <li>{{ message }}</li>
            {% endfor %}
            </ul>
        {% else %}
            <p>没有消息。</p>
        {% endif %}
    {% endwith %}

    <a href="{{ url_for('flash_demo') }}">返回注册页面</a>
</body>
</html>

九、静态文件的加载

Flask 默认在 static/ 目录中查找静态文件(CSS、JavaScript、图片、字体等)。

9.1 目录结构

复制代码
项目目录结构:
myproject/
├── app.py
├── templates/
│   └── static_demo.html
└── static/                  ← 静态文件根目录
    ├── css/
    │   └── style.css        ← 样式表
    ├── js/
    │   └── main.js          ← JavaScript
    ├── images/
    │   ├── logo.png         ← 图片
    │   └── bg.jpg           ← 背景图
    └── fonts/
        └── custom.woff2     ← 自定义字体

9.2 自定义静态文件目录

python 复制代码
# app.py
from flask import Flask, render_template

# 可以自定义静态文件目录路径和URL前缀
app = Flask(
    __name__,
    static_folder='static',       # 静态文件目录(相对于应用根目录),默认为 'static'
    static_url_path='/static'     # URL 前缀,默认为 '/static'
)

# 也可以在创建实例后修改:
# app.static_folder = 'my_static_files'
# app.static_url_path = '/assets'

@app.route('/static_demo')
def static_demo():
    return render_template('static_demo.html')

if __name__ == '__main__':
    app.run(debug=True)

9.3 在模板中加载静态文件

html 复制代码
<!-- templates/static_demo.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>静态文件加载演示</title>

    {# ============================================================ #}
    {# 一、加载 CSS 样式表                                               #}
    {# 使用 url_for('static', filename='路径') 生成静态文件URL         #}
    {# 生成的URL: /static/css/style.css                              #}
    {# ============================================================ #}
    <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">

    {# 加载外部字体 #}
    <link rel="stylesheet" href="{{ url_for('static', filename='fonts/custom.woff2') }}">

    {# 加载 favicon(网站图标) #}
    <link rel="icon" type="image/png" href="{{ url_for('static', filename='images/favicon.png') }}">

    {# 加载外部 CDN 资源(不是静态文件,直接写 URL) #}
    <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;700&display=swap" rel="stylesheet">
</head>
<body>
    <h1>静态文件加载完整演示</h1>

    {# ============================================================ #}
    {# 二、加载图片                                                      #}
    {# ============================================================ #}
    <div>
        <h2>1. 图片加载</h2>

        {# 基本图片加载 #}
        <img src="{{ url_for('static', filename='images/logo.png') }}"
             alt="Logo"
             width="200">

        {# 背景图片通过 CSS 加载(在 style.css 中) #}
        <div class="hero-banner" style="width:100%; height:200px; border:1px solid #ccc;">
            背景图片区域(通过CSS加载)
        </div>

        {# 直接在 style 属性中使用 url_for #}
        <div style="width: 300px; height: 150px;
                    background-image: url('{{ url_for('static', filename='images/bg.jpg') }}');
                    background-size: cover;
                    background-position: center;
                    border: 1px solid #ccc;">
        </div>
    </div>

    {# ============================================================ #}
    {# 三、加载 JavaScript                                               #}
    {# ============================================================ #}
    <div>
        <h2>2. JavaScript 加载</h2>
        <p id="demo">点击按钮查看效果</p>
        <button onclick="changeText()">点击我</button>
    </div>

    {# 加载 JS 文件(通常放在 body 末尾) #}
    {# 方式一:使用 url_for 加载本地 JS #}
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>

    {# 方式二:加载外部 CDN 的 JS 库 #}
    {# <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script> #}

    {# ============================================================ #}
    {# 四、内联方式使用 url_for 生成 URL                                 #}
    {# ============================================================ #}
    <div>
        <h2>3. 其他用法</h2>

        {# 链接到静态文件(如PDF下载) #}
        <a href="{{ url_for('static', filename='files/user_guide.pdf') }}">
            下载用户指南(PDF)
        </a>

        {# 传递给 JavaScript 使用 #}
        <script>
            // 将静态文件路径赋值给 JS 变量
            var staticBaseUrl = "{{ url_for('static', filename='') }}";
            console.log('静态文件基础路径:' + staticBaseUrl);

            function changeText() {
                document.getElementById('demo').innerText = 'JavaScript 加载成功!';
            }
        </script>
    </div>

    {# ============================================================ #}
    {# 五、在 CSS 中引用图片(相对路径)                                    #}
    {# ============================================================ #}
    {#
        在 CSS 文件(style.css)中引用图片时,路径相对于 CSS 文件的位置:
        .hero-banner {
            background-image: url('../images/bg.jpg');  ← 相对于 css/ 目录
        }
    #}

</body>
</html>
css 复制代码
/* static/css/style.css */

/* 全局样式 */
body {
    font-family: 'Noto Sans SC', sans-serif;
    max-width: 900px;
    margin: 0 auto;
    padding: 20px;
    background-color: #fafafa;
    color: #333;
}

h1 {
    color: #2c3e50;
    border-bottom: 3px solid #3498db;
    padding-bottom: 10px;
}

h2 {
    color: #34495e;
    margin-top: 30px;
}

/* 背景图片样式 */
/* CSS 中使用相对路径引用 static/images/ 中的图片 */
.hero-banner {
    background-image: url('../images/bg.jpg');
    background-size: cover;
    background-position: center;
    border-radius: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
    font-size: 20px;
    text-shadow: 1px 1px 3px rgba(0,0,0,0.5);
}

/* 按钮样式 */
button {
    background: #3498db;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

button:hover {
    background: #2980b9;
}

/* 图片样式 */
img {
    max-width: 100%;
    height: auto;
    border-radius: 4px;
}
javascript 复制代码
// static/js/main.js

// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', function() {
    console.log('main.js 加载完成!');

    // 获取静态文件路径(如果需要动态加载资源)
    // 可以通过 data 属性或全局变量传入
});

/**
 * 按钮点击事件处理函数
 * 改变演示区域的文本内容
 */
function changeText() {
    // 获取演示元素
    var demoElement = document.getElementById('demo');
    // 修改文本内容
    demoElement.innerText = 'JavaScript 加载成功!按钮点击有效!';
    // 添加样式
    demoElement.style.color = '#27ae60';
    demoElement.style.fontWeight = 'bold';
}

十、模板继承(Template Inheritance)

模板继承是 Jinja2 最强大的功能之一,允许定义一个基础模板(父模板) ,其他模板继承它并覆盖特定区块。

10.1 基础模板(父模板)

html 复制代码
<!-- templates/base.html -->
{# ============================================================ #}
{# 基础模板(父模板)                                                    #}
{# 定义网站的整体结构和可被子模板覆盖的区块                                  #}
{# ============================================================ #}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    {#
        block 标签定义了一个可被子模板覆盖的区块
        语法: {% block 区块名 %}默认内容{% endblock %}
        子模板通过 {% block 区块名 %}新内容{% endblock %} 来覆盖
    #}
    <title>{% block title %}默认网站标题{% endblock %}</title>

    {# 基础 CSS 样式 #}
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Noto Sans SC', sans-serif; background: #f5f5f5; color: #333; }

        /* 导航栏 */
        nav {
            background: linear-gradient(135deg, #1a1a2e, #16213e);
            padding: 15px 30px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        nav .logo { color: #e8c547; font-size: 24px; font-weight: bold; text-decoration: none; }
        nav ul { list-style: none; display: flex; gap: 20px; }
        nav ul li a { color: #eee; text-decoration: none; transition: color 0.2s; }
        nav ul li a:hover { color: #e8c547; }

        /* 主体内容 */
        .container { max-width: 1000px; margin: 20px auto; padding: 0 20px; }

        /* 侧边栏布局 */
        .layout { display: flex; gap: 20px; }
        .main-content { flex: 1; }
        .sidebar { width: 250px; }

        /* 页脚 */
        footer {
            background: #1a1a2e;
            color: #aaa;
            text-align: center;
            padding: 20px;
            margin-top: 40px;
        }

        /* 通用样式 */
        .card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
    </style>

    {# 预留区块:子模板可以添加额外的 CSS #}
    {% block extra_css %}{% endblock %}
</head>
<body>

    {# =================== 导航栏区块 =================== #}
    {# block 标签内可以有默认内容,子模板不覆盖时显示默认内容 #}
    {% block navbar %}
    <nav>
        <a class="logo" href="/">MyFlask</a>
        <ul>
            <li><a href="{{ url_for('index') }}">首页</a></li>
            <li><a href="{{ url_for('about') }}">关于</a></li>
            <li><a href="{{ url_for('contact') }}">联系我们</a></li>
        </ul>
    </nav>
    {% endblock %}

    {# =================== 主体内容区块 =================== #}
    <div class="container">
        {# 子模板通过覆盖 content 区块来提供页面内容 #}
        {% block content %}
        <p>默认内容 --- 子模板应覆盖此区块</p>
        {% endblock %}
    </div>

    {# =================== 侧边栏区块 =================== #}
    {# 定义一个可选的侧边栏区块,默认为空 #}
    {% block sidebar %}{% endblock %}

    {# =================== 页脚区块 =================== #}
    {% block footer %}
    <footer>
        <p>&copy; 2024 MyFlask. All rights reserved.</p>
        <p>Powered by Flask + Jinja2</p>
    </footer>
    {% endblock %}

    {# 预留区块:子模板可以添加额外的 JavaScript #}
    {% block extra_js %}{% endblock %}
</body>
</html>

10.2 子模板继承

python 复制代码
# app.py --- 模板继承的路由
from flask import Flask, render_template

app = Flask(__name__)
app.secret_key = 'my-secret-key'

@app.route('/')
def index():
    """首页"""
    return render_template(
        'index.html',
        title='Flask 模板继承示例',
        features=[
            {'name': '模板继承', 'desc': '减少重复代码,统一网站风格'},
            {'name': '区块覆盖', 'desc': '灵活定制每个页面的特定区域'},
            {'name': '宏的复用', 'desc': '封装可复用的模板组件'},
        ],
        stats={'users': 1280, 'posts': 5620, 'views': 89300}
    )

@app.route('/about')
def about():
    """关于页面"""
    return render_template('about.html', team_members=[
        {'name': '张三', 'role': '全栈开发', 'experience': '5年'},
        {'name': '李四', 'role': 'UI设计师', 'experience': '3年'},
        {'name': '王五', 'role': '产品经理', 'experience': '4年'},
    ])

@app.route('/contact')
def contact():
    """联系我们页面"""
    return render_template('contact.html')

if __name__ == '__main__':
    app.run(debug=True)
html 复制代码
<!-- templates/index.html -->
{# ============================================================ #}
{# 子模板:首页                                                       #}
{# 使用 {% extends %} 继承基础模板                                     #}
{# extends 必须是模板中的第一个标签(除了注释)                            #}
{# ============================================================ #}

{# 继承 base.html(路径相对于 templates/ 目录) #}
{% extends 'base.html' %}

{# ---------- 覆盖 title 区块 ---------- #}
{% block title %}{{ title }} --- 首页{% endblock %}

{# ---------- 覆盖 content 区块 ---------- #}
{% block content %}
<div class="card">
    <h1>{{ title }}</h1>
    <p>这个页面演示了 Jinja2 的模板继承功能。</p>
    <p>通过 <code>{% raw %}{% extends %}{% endraw %}</code> 和
       <code>{% raw %}{% block %}{% endraw %}</code> 实现代码复用。</p>
</div>

<div class="card">
    <h2>核心特性</h2>
    {# 使用 Jinja2 的 for 循环 #}
    {% for feature in features %}
    <div style="padding: 10px; margin: 5px 0; background: #f8f9fa; border-radius: 4px;">
        <strong>{{ loop.index }}. {{ feature.name }}</strong>
        <p style="color: #666; margin-top: 5px;">{{ feature.desc }}</p>
    </div>
    {% endfor %}
</div>

<div class="card">
    <h2>站点统计</h2>
    <div style="display: flex; gap: 20px;">
        <div style="text-align: center;">
            <div style="font-size: 28px; color: #3498db;">{{ stats.users }}</div>
            <div>注册用户</div>
        </div>
        <div style="text-align: center;">
            <div style="font-size: 28px; color: #2ecc71;">{{ stats.posts }}</div>
            <div>文章数量</div>
        </div>
        <div style="text-align: center;">
            <div style="font-size: 28px; color: #e74c3c;">{{ stats.views }}</div>
            <div>总访问量</div>
        </div>
    </div>
</div>
{% endblock %}

{# ---------- 覆盖 sidebar 区块 ---------- #}
{% block sidebar %}
<div class="card">
    <h3>最新公告</h3>
    <ul>
        <li>网站已上线</li>
        <li>新功能开发中</li>
        <li>欢迎反馈建议</li>
    </ul>
</div>
{% endblock %}

{# ---------- 覆盖 extra_css 区块(添加页面特有的CSS) ---------- #}
{% block extra_css %}
<style>
    code {
        background: #e8e8e8;
        padding: 2px 6px;
        border-radius: 3px;
        font-size: 13px;
    }
</style>
{% endblock %}
html 复制代码
<!-- templates/about.html -->
{# 子模板:关于页面 #}
{% extends 'base.html' %}

{# 覆盖标题 #}
{% block title %}关于我们{% endblock %}

{# 覆盖导航栏(完全替换默认导航栏) #}
{% block navbar %}
<nav>
    <a class="logo" href="/">MyFlask</a>
    <ul>
        {# 当前页面的链接高亮 #}
        <li><a href="{{ url_for('index') }}">首页</a></li>
        <li><a href="{{ url_for('about') }}" style="color: #e8c547;">关于我们</a></li>
        <li><a href="{{ url_for('contact') }}">联系我们</a></li>
    </ul>
</nav>
{% endblock %}

{# 覆盖内容 #}
{% block content %}
<div class="card">
    <h1>关于我们</h1>
    <p>我们是一个专注于 Flask Web 开发的技术团队。</p>
</div>

<div class="card">
    <h2>团队成员</h2>
    <table style="width: 100%; border-collapse: collapse;">
        <tr style="background: #f0f0f0;">
            <th style="padding: 10px; text-align: left;">姓名</th>
            <th style="padding: 10px; text-align: left;">角色</th>
            <th style="padding: 10px; text-align: left;">经验</th>
        </tr>
        {% for member in team_members %}
        {# 使用 loop.cycle 交替行背景色 #}
        <tr style="background: {{ '#f9f9f9' if loop.index is even else '#fff' }};">
            <td style="padding: 10px;">{{ member.name }}</td>
            <td style="padding: 10px;">{{ member.role }}</td>
            <td style="padding: 10px;">{{ member.experience }}</td>
        </tr>
        {% endfor %}
    </table>
</div>
{% endblock %}

{# 不覆盖 sidebar,about 页面就没有侧边栏(因为 base.html 中 sidebar 区块默认为空) #}
html 复制代码
<!-- templates/contact.html -->
{# 子模板:联系我们页面 #}
{% extends 'base.html' %}

{% block title %}联系我们{% endblock %}

{% block content %}
<div class="card">
    <h1>联系我们</h1>
    <p>有任何问题或建议,欢迎通过以下方式联系我们:</p>

    {# 导入并使用宏 #}
    {% from 'macros/forms.html' import input, textarea, submit %}

    <form method="POST" style="margin-top: 20px;">
        {{ input('name', '您的姓名', placeholder='请输入姓名', required=true) }}
        {{ input('email', '邮箱地址', type='email', placeholder='请输入邮箱', required=true) }}
        {{ textarea('message', '留言内容', rows=5, placeholder='请输入您想说的话...') }}
        {{ submit('发送消息', color='#3498db') }}
    </form>
</div>
{% endblock %}

{# 自定义页脚 #}
{% block footer %}
<footer>
    <p>联系我们:contact@myflask.com</p>
    <p>地址:北京市海淀区中关村大街1号</p>
    <p>&copy; 2024 MyFlask</p>
</footer>
{% endblock %}

10.3 super() --- 调用父区块的内容

html 复制代码
<!-- templates/extended_index.html -->
{# ============================================================ #}
{# super() 函数                                                    #}
{# 在子模板中调用父模板同名区块的内容(追加而不是替换)                    #}
{# ============================================================ #}
{% extends 'base.html' %}

{% block title %}首页 --- 自定义标题{% endblock %}

{% block extra_css %}
{# super() 会先渲染父模板中 extra_css 区块的内容(这里是空的) #}
{# 然后追加子模板中的内容 #}
{{ super() }}
<style>
    /* 子模板特有的额外样式 */
    .custom-style { color: red; }
</style>
{% endblock %}

{% block content %}
{# 如果需要保留父模板的 content 默认内容,使用 super() #}
{# {{ super() }} ← 这会先输出父模板 content 区块的内容 #}

{# 然后添加子模板的内容 #}
<div class="card">
    <h1>使用 super() 的示例</h1>
    <p>父模板的 content 默认内容被替换(因为没有调用 super())</p>
    <p>但 extra_css 区块使用了 super(),所以父模板的样式被保留</p>
</div>
{% endblock %}

{% block footer %}
{# 调用 super(),保留父模板页脚,并追加内容 #}
{{ super() }}
<div style="text-align: center; padding: 10px; background: #eee;">
    <p>这是子模板追加的页脚内容</p>
</div>
{% endblock %}

10.4 嵌套继承(多级继承)

html 复制代码
<!-- templates/base_admin.html -->
{# ============================================================ #}
{# 嵌套继承:管理后台专用基础模板                                       #}
{# base_admin.html 继承 base.html,然后 admin_*.html 继承 base_admin #}
{# 继承链:admin_*.html → base_admin.html → base.html              #}
{# ============================================================ #}
{% extends 'base.html' %}

{# 覆盖导航栏为管理后台导航 #}
{% block navbar %}
<nav>
    <a class="logo" href="/admin">管理后台</a>
    <ul>
        <li><a href="/admin/dashboard">仪表盘</a></li>
        <li><a href="/admin/users">用户管理</a></li>
        <li><a href="/admin/posts">文章管理</a></li>
        <li><a href="/">返回前台</a></li>
    </ul>
</nav>
{% endblock %}

{# 管理后台的布局:左侧菜单 + 右侧内容 #}
{% block content %}
<div style="display: flex; gap: 20px;">
    {# 左侧菜单 #}
    <div style="width: 200px;">
        <div class="card">
            <h3>管理菜单</h3>
            <ul style="list-style: none; padding: 0;">
                <li style="padding: 8px 0;"><a href="/admin/dashboard">仪表盘</a></li>
                <li style="padding: 8px 0;"><a href="/admin/users">用户管理</a></li>
                <li style="padding: 8px 0;"><a href="/admin/settings">系统设置</a></li>
            </ul>
        </div>
    </div>
    {# 右侧主内容区 --- 由更深层的子模板覆盖 #}
    <div style="flex: 1;">
        {% block admin_content %}
        <p>管理后台默认内容</p>
        {% endblock %}
    </div>
</div>
{% endblock %}
html 复制代码
<!-- templates/admin_dashboard.html -->
{# 继承管理后台基础模板 #}
{% extends 'base_admin.html' %}

{% block title %}管理后台 --- 仪表盘{% endblock %}

{# 覆盖 admin_content 区块(而不是 content) #}
{% block admin_content %}
<div class="card">
    <h2>仪表盘</h2>
    <p>欢迎回来,管理员!</p>
    <div style="display: flex; gap: 15px; margin-top: 15px;">
        <div style="background: #3498db; color: white; padding: 20px; border-radius: 8px; flex: 1; text-align: center;">
            <div style="font-size: 28px;">1,280</div>
            <div>用户总数</div>
        </div>
        <div style="background: #2ecc71; color: white; padding: 20px; border-radius: 8px; flex: 1; text-align: center;">
            <div style="font-size: 28px;">5,620</div>
            <div>文章总数</div>
        </div>
        <div style="background: #e74c3c; color: white; padding: 20px; border-radius: 8px; flex: 1; text-align: center;">
            <div style="font-size: 28px;">89,300</div>
            <div>访问量</div>
        </div>
    </div>
</div>
{% endblock %}

10.5 include --- 包含子模板

html 复制代码
<!-- templates/includes/header.html -->
{# ============================================================ #}
{# 独立的子模板片段(用于 include)                                     #}
{# 注意:这不是用来继承的,而是被其他模板包含的                            #}
{# ============================================================ #}
<header style="background: #2c3e50; color: white; padding: 20px; text-align: center;">
    <h1>{{ site_name | default('我的网站') }}</h1>
    <p>{{ site_desc | default('欢迎访问') }}</p>
</header>
html 复制代码
<!-- templates/includes/footer.html -->
<footer style="background: #34495e; color: #bbb; text-align: center; padding: 15px;">
    <p>&copy; 2024 {{ company | default('MyCompany') }}. All rights reserved.</p>
</footer>
html 复制代码
<!-- templates/includes/alert.html -->
{# 一个可复用的提示消息组件 #}
<div style="padding: 12px; margin: 10px 0; border-radius: 4px;
            background: {% if type == 'success' %}#d4edda
            {% elif type == 'error' %}#f8d7da
            {% elif type == 'warning' %}#fff3cd
            {% else %}#d1ecf1{% endif %}; color: {% if type == 'success' %}#155724
            {% elif type == 'error' %}#721c24
            {% elif type == 'warning' %}#856404
            {% else %}#0c5460{% endif %};">
    {{ message }}
</div>
html 复制代码
<!-- templates/include_demo.html -->
{# ============================================================ #}
{# include 用法演示                                                  #}
{# include 可以在任何位置引入另一个模板文件的内容                          #}
{# ============================================================ #}
{% extends 'base.html' %}

{% block title %}include 演示{% endblock %}

{% block content %}

{# include 引入子模板片段(自动传递当前模板的所有变量) #}
{% include 'includes/header.html' ignore missing %}

{# ignore missing: 如果文件不存在不会报错(可选) #}

<div class="card">
    <h2>include 的用法</h2>

    {# 引入提示消息组件,并传入变量 #}
    {% set type = 'success' %}
    {% set message = '操作成功!数据已保存。' %}
    {% include 'includes/alert.html' %}

    {% set type = 'warning' %}
    {% set message = '请注意:此操作不可撤销。' %}
    {% include 'includes/alert.html' %}

    {% set type = 'error' %}
    {% set message = '错误:连接数据库失败。' %}
    {% include 'includes/alert.html' %}

    {% set type = 'info' %}
    {% set message = '提示:系统将于今晚 22:00 维护。' %}
    {% include 'includes/alert.html' %}
</div>

{# 也可以在循环中使用 include #}
<div class="card">
    <h2>循环中使用 include</h2>
    {% for item in ['项目A', '项目B', '项目C'] %}
        {% set message = '处理中:' ~ item %}
        {% set type = 'info' %}
        {% include 'includes/alert.html' %}
    {% endfor %}
</div>

{% endblock %}

{# 引入页脚片段 #}
{% block footer %}
{% include 'includes/footer.html' %}
{% endblock %}

知识点总结表

知识点 语法 用途
变量输出 {``{ variable }} 输出变量值或表达式结果
控制语句 {% if %} / {% for %} 条件判断和循环
注释 {# 注释内容 #} 模板注释(不出现在HTML中)
过滤器 `{``{ var filter(args) }}`
自定义过滤器 @app.template_filter() 注册自定义过滤器
宏定义 {% macro name(args) %} 定义可复用的模板函数
宏调用 {``{ name(args) }} / {% call %} 调用宏
宏导入 {% from 'file' import name %} 跨模板导入宏
消息闪现 flash() / get_flashed_messages() 一次性提示消息
静态文件 {``{ url_for('static', filename='') }} 生成静态文件URL
模板继承 {% extends 'base.html' %} 继承父模板
区块覆盖 {% block name %}...{% endblock %} 定义/覆盖可替换区块
调用父区块 {``{ super() }} 追加(而非替换)父模板区块内容
包含子模板 {% include 'file.html' %} 引入其他模板片段
相关推荐
不懒不懒3 小时前
基于 Flask —— 异步任务处理接口服务
后端·python·flask
happybasic4 小时前
Python库升级标准流程~
linux·前端·python
彦为君4 小时前
JavaSE-11-BIO/NIO/AIO(多人聊天室)
java·开发语言·python·ai·nio
恣艺4 小时前
Python 实用工具与机器学习入门:Rich + Tqdm + Faker + Schedule + Scikit-learn
python·机器学习·scikit-learn
humors2214 小时前
突破学习瓶颈:十个需要克服的障碍
大数据·学习·程序人生
GEO从入门到精通4 小时前
在哪里能买到GEO学习工具或课程?
人工智能·学习
测试员周周4 小时前
【Appium 系列】第14节-断言与验证 — Validator 的设计
android·人工智能·python·功能测试·ios·单元测试·appium
心中有国也有家4 小时前
从零上手 CANN 学习中心:像逛技术便利店一样学昇腾
学习·算法·开源
Hanniel4 小时前
Python __slots__ 入门指南
开发语言·python·性能优化