(十七)列表筛选、详情与只读 JSON
1. GET 表单把筛选写进地址
列表中的筛选表单使用 method="get",因此关键词和状态进入查询串,刷新或复制地址都可以重复同一次查询。它不会改变书籍,不需要作为写入表单校验。
<!-- 项目文件:shelf_project/src/shelfweb/templates/books/index.html -->
{% extends 'base.html' %}
{% block title %}我的书架{% endblock %}
{% block content %}
<div class="section-heading"><div><p class="eyebrow">MY BOOKS</p><h1>我的书架</h1></div>
<a class="button" href="{{ url_for('shelf.create') }}">添加书籍</a></div>
<div class="metrics"><span>当前结果 <strong>{{ books|length }}</strong></span>
<span>其中已读 <strong>{{ books|selectattr('finished')|list|length }}</strong></span><span>存储 <strong>SQLite</strong></span></div>
<form class="filters panel" method="get" action="{{ url_for('shelf.index') }}">
<div><label for="q">按书名查找</label><input id="q" name="q" value="{{ q }}" maxlength="100" placeholder="输入书名关键词"></div>
<div><label for="filter-status">阅读状态</label><select id="filter-status" name="finished">
<option value="all" {% if finished == 'all' %}selected{% endif %}>全部</option>
<option value="0" {% if finished == '0' %}selected{% endif %}>未读</option>
<option value="1" {% if finished == '1' %}selected{% endif %}>已读</option>
</select></div><button type="submit">筛选</button><a href="{{ url_for('shelf.index') }}">重置</a>
</form>
<section class="book-grid" aria-label="书籍列表">
{% for book in books %}
<article class="panel book" data-book-id="{{ book.id }}">
<p class="status {{ 'done' if book.finished else 'unread' }}">{{ '已读' if book.finished else '未读' }}</p>
<h2><a href="{{ url_for('shelf.detail', book_id=book.id) }}">{{ book.title }}</a></h2>
<p class="muted">{{ book.tags|join(' · ') or '未设置标签' }}</p>
<a href="{{ url_for('shelf.edit', book_id=book.id) }}">修改阅读记录</a>
</article>
{% else %}
<div class="panel empty"><h2>这里还没有书</h2><p>可以调整筛选,或添加一本书。</p></div>
{% endfor %}
</section>
<p class="muted api-link">同一份记录也可查看 <a href="{{ url_for('api.books') }}">JSON 接口</a>。</p>
{% endblock %}| 把值交给模板过滤器。books|length 统计当前结果;selectattr('finished') 留下已读项,|list|length 把它们转成列表再计数。tags|join(' · ') 生成标签文字。它们处理的是已经查询完的数据,不在模板中悄悄执行数据库查询。
alice 的六本采集书目中,编号 2、5 已读,1、3、4、6 未读。finished=0 后页面只显示四本,指标也表示“当前结果”与“其中已读”。重置链接重新请求没有筛选参数的首页。
2. 详情页连接修改和删除
<!-- 项目文件:shelf_project/src/shelfweb/templates/books/detail.html -->
{% extends 'base.html' %}
{% from '_macros.html' import csrf_field %}
{% block title %}{{ book.title }}{% endblock %}
{% block content %}
<section class="panel detail-panel" data-book-id="{{ book.id }}">
<a href="{{ url_for('shelf.index') }}">返回书架</a>
<h1>{{ book.title }}</h1><p class="status">{{ '已读' if book.finished else '未读' }}</p>
<p class="muted">标签:{{ book.tags|join(' · ') or '未设置标签' }}</p>
<a class="button" href="{{ url_for('shelf.edit', book_id=book.id) }}">修改书籍</a>
<details class="delete-area"><summary>删除这本书</summary>
<p>确认后,这本书会从你的书架中删除。</p>
<form method="post" action="{{ url_for('shelf.delete', book_id=book.id) }}">
{{ csrf_field() }}<button class="danger" type="submit">确认删除</button>
</form>
</details>
</section>
{% endblock %}details 与 summary 是浏览器自带的展开控件。用户先展开删除区域,再点击确认按钮,表单向专用的 POST 地址发送请求;普通链接仍用于读取页面。
3. 同一批记录交回 JSON
# 项目文件:shelf_project/src/shelfweb/api.py
from flask import Blueprint, g, jsonify
from .auth import login_required
from .service import list_books
bp = Blueprint("api", __name__, url_prefix="/api")
@bp.get("/books")
@login_required
def books():
items = list_books(g.user["id"])
return jsonify(count=len(items), books=items) # 与页面读取同一批持久化记录。jsonify 编码相同的公开字典,数据库与归属规则没有重新实现。这个只读入口继续使用浏览器登录 Cookie;匿名调用得到 JSON 401。与前一章的 Bearer 请求相比,调用者如何携带身份发生了变化,用户归属判断仍然保留。