(十七)Django 模板、控件与页面组合
1. 沿已有模板知识读新写法
Django 模板也用 {{ }} 输出、{% %} 控制,但不是 Jinja 的同一套语法。模板不直接写 Python 函数调用,{% url %} 生成地址,{% static %} 生成静态路径,{% csrf_token %} 生成表单令牌。
<!-- 项目文件:reading_project/src/library/templates/base.html -->
{% load static %}
<!doctype html><html lang="zh-CN">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}阅读记录站{% endblock %} · Django</title>
<link rel="stylesheet" href="{% static 'library/style.css' %}"></head>
<body><main>
<header class="page-header"><div><p class="eyebrow">PYTHON 3.12 / DJANGO</p>
<a class="brand" href="{% url 'library:index' %}">阅读记录站</a><p class="muted">书架、阅读备注与管理后台。</p></div>
<nav aria-label="账号导航">
{% if user.is_authenticated %}
<span>你好,{{ user.username }}</span><a href="{% url 'library:index' %}">我的书架</a>
{% if user.is_staff %}<a href="{% url 'admin:index' %}">管理后台</a>{% endif %}
<form method="post" action="{% url 'logout' %}">{% csrf_token %}<button class="secondary">退出登录</button></form>
{% else %}<a href="{% url 'login' %}">登录</a><a href="{% url 'signup' %}">注册</a>{% endif %}
</nav>
</header>
{% for message in messages %}<p class="notice" role="status">{{ message }}</p>{% endfor %}
{% block content %}{% endblock %}
<footer>Django 页面 · 用户与权限 · SQLite 持久化</footer>
</main></body></html>user、perms 与 messages 来自上下文处理器。登录导航用 user.is_authenticated 判断,后台入口看 user.is_staff;这些显示判断帮助组织页面,实际接口仍执行权限检查。
2. BoundField 生成控件
<!-- 项目文件:reading_project/src/library/templates/_fields.html -->
<!-- BoundField 输出带转义和校验属性的控件;错误紧邻对应字段。 -->
{{ form.non_field_errors }}
{% for field in form %}
<div class="form-field">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{{ field.errors }}
{% if field.help_text %}<div class="muted help">{{ field.help_text }}</div>{% endif %}
</div>
{% endfor %}迭代表单得到 BoundField,它关联字段规则、本次值、控件和错误。输出 {{ field }} 生成 <input> 或 <textarea>,用户值在控件生成时转义。id_for_label 连接标签和输入,field.errors 放在对应控件旁,整体错误单独显示。
<!-- 项目文件:reading_project/src/library/templates/library/book_form.html -->
{% extends 'base.html' %}
{% block title %}保存阅读记录{% endblock %}
{% block content %}<section class="panel form-panel"><a href="{% url 'library:index' %}">返回书架</a>
<h1>{% if book %}修改书籍{% else %}添加书籍{% endif %}</h1>
<form method="post">{% csrf_token %}{% include '_fields.html' %}<button type="submit">保存书籍</button></form>
</section>{% endblock %}绑定失败的表单保留这次尝试值;未绑定编辑表单取实例初值。数据库记录与输入框值可以暂时不同,直到事务提交才合并成成功结果。
3. 详情显示标签、备注和历史
<!-- 项目文件:reading_project/src/library/templates/library/detail.html -->
{% extends 'base.html' %}
{% block title %}{{ book.title }}{% endblock %}
{% block content %}<section class="panel detail-panel">
<a href="{% url 'library:index' %}">返回书架</a><h1>{{ book.title }}</h1>
<p class="status">{{ book.finished|yesno:'已读,未读' }}</p><p class="muted">所属用户:{{ book.owner.username }}</p>
<p>标签:{% for tag in book.tags.all %}{{ tag.name }} {% empty %}未设置{% endfor %}</p>
<div class="notes">{{ book.notes|linebreaksbr }}</div>
<p><a class="button" href="{% url 'library:edit' book.pk %}">修改书籍</a></p>
<details class="delete-area"><summary>删除这本书</summary><p>确认后将删除这本书及其修改记录。</p>
<form method="post" action="{% url 'library:delete' book.pk %}">{% csrf_token %}<button class="danger">确认删除</button></form></details>
<h2 class="history-title">修改记录</h2><ul>{% for change in book.changes.all %}
<li>{{ change.get_action_display }} · {{ change.created_at|date:'Y-m-d H:i' }}</li>{% empty %}<li>还没有修改记录。</li>{% endfor %}</ul>
</section>{% endblock %}book.tags.all 在模板中调用无参数方法,使用视图已经预取的关联结果。yesno 把布尔值转换为指定文字,date 按格式显示时间;linebreaksbr 在转义备注文本后把换行组织成可显示的换行标记。普通书名和备注不加 safe。