4. 工单列表与分页展示:查询所有工单、按状态/优先级筛选、分页逻辑实现
好,咱们接着往下走。工单系统最核心的页面是哪个?我个人觉得,就是工单列表页。你想想看,客服每天打开系统,第一眼看到的就是这个列表。它要清晰、要快、还要能筛选。今天我就带你一步步把这个功能做扎实。
4.1 查询所有工单——基础列表
先来个最简单的:把数据库里所有工单都查出来。用 Flask-SQLAlchemy 做这个,其实就一行代码的事。
# 在 routes.py 里
from flask import render_template
from models import Ticket
@app.route('/tickets')
def ticket_list():
tickets = Ticket.query.all()
return render_template('tickets.html', tickets=tickets)
嗯,这里要注意。如果工单数量很少,比如几十条,这么干没问题。但一旦上了几百上千条,页面加载就会变慢。我在项目里遇到过,客户反馈说「点一下列表要等两秒」,其实就是因为没做分页。
.all() 查全部数据。除非你确定数据量永远不超过 100 条。
4.2 按状态/优先级筛选——加个条件查询
光有全部列表还不够。客服人员经常需要只看「待处理」的工单,或者只看「紧急」的工单。这时候就要加筛选条件了。
我习惯用 request.args 来获取 URL 参数,然后动态拼查询条件。说白了,就是根据用户点的是哪个筛选按钮,来决定 SQL 的 where 子句。
from flask import request
@app.route('/tickets')
def ticket_list():
status = request.args.get('status', '')
priority = request.args.get('priority', '')
query = Ticket.query
if status:
query = query.filter(Ticket.status == status)
if priority:
query = query.filter(Ticket.priority == priority)
tickets = query.all()
return render_template('tickets.html', tickets=tickets)
你看,这样 URL 就变成了 /tickets?status=open&priority=high。前端点一下筛选按钮,后端就自动过滤了。我曾经踩过一个坑:忘了处理空字符串的情况。如果 status 参数传了个空字符串,filter 会匹配不到任何数据。所以一定要加 if status 判断。
4.3 分页逻辑实现——Flask-SQLAlchemy paginate
好了,重点来了。分页怎么做?Flask-SQLAlchemy 自带了 paginate 方法,用起来非常顺手。
# 分页参数
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
# 使用 paginate 代替 all()
pagination = Ticket.query.paginate(
page=page,
per_page=per_page,
error_out=False
)
# pagination 对象包含以下常用属性:
# .items —— 当前页的数据列表
# .total —— 总记录数
# .pages —— 总页数
# .page —— 当前页码
# .has_prev —— 是否有上一页
# .has_next —— 是否有下一页
# .prev_num —— 上一页页码
# .next_num —— 下一页页码
这里有个小细节:error_out=False 是什么意思?默认情况下,如果你访问的页码不存在(比如总共 5 页,你访问第 6 页),它会报 404 错误。加上 error_out=False 后,它会返回空列表,而不是报错。我个人建议一直加上这个参数,用户体验更好。
把筛选和分页结合起来,就是完整的列表查询了:
@app.route('/tickets')
def ticket_list():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
status = request.args.get('status', '')
priority = request.args.get('priority', '')
query = Ticket.query
if status:
query = query.filter(Ticket.status == status)
if priority:
query = query.filter(Ticket.priority == priority)
# 按创建时间倒序排列,最新的工单在最前面
query = query.order_by(Ticket.created_at.desc())
pagination = query.paginate(
page=page,
per_page=per_page,
error_out=False
)
return render_template(
'tickets.html',
pagination=pagination,
status=status,
priority=priority
)
4.4 前端模板——怎么展示分页
后端写好了,前端也得跟上。在 Jinja2 模板里,我们可以这样渲染分页导航:
<!-- 工单列表 -->
<table>
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>状态</th>
<th>优先级</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
{% for ticket in pagination.items %}
<tr>
<td>{{ ticket.id }}</td>
<td>{{ ticket.title }}</td>
<td>{{ ticket.status }}</td>
<td>{{ ticket.priority }}</td>
<td>{{ ticket.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- 分页导航 -->
<div class="pagination">
{% if pagination.has_prev %}
<a href="{{ url_for('ticket_list', page=pagination.prev_num, status=status, priority=priority) }}">上一页</a>
{% endif %}
<span>第 {{ pagination.page }} / {{ pagination.pages }} 页</span>
{% if pagination.has_next %}
<a href="{{ url_for('ticket_list', page=pagination.next_num, status=status, priority=priority) }}">下一页</a>
{% endif %}
</div>
status 和 priority 参数。不然你筛选后点下一页,筛选条件就丢了。我刚开始做的时候忘了这个,被测试妹子追着改 bug……
4.5 完整代码示例——一步到位
最后,我把上面所有内容整合成一个完整的视图函数。你直接复制就能用:
@app.route('/tickets')
def ticket_list():
# 获取分页和筛选参数
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
status = request.args.get('status', '')
priority = request.args.get('priority', '')
# 构建查询
query = Ticket.query
if status:
query = query.filter(Ticket.status == status)
if priority:
query = query.filter(Ticket.priority == priority)
# 排序:最新的工单排前面
query = query.order_by(Ticket.created_at.desc())
# 分页
pagination = query.paginate(
page=page,
per_page=per_page,
error_out=False
)
return render_template(
'tickets.html',
pagination=pagination,
status=status,
priority=priority
)
你看,整个逻辑其实就三步:拿参数 → 拼查询 → 分页返回。是不是挺清晰的?
- 用
request.args.get()获取筛选参数 - 用
paginate()代替all()实现分页 - 分页链接要保留筛选参数
- 排序用
order_by(),记得加.desc()或.asc()
下一章,我会带你做工单的详情页面和状态流转。到时候你会看到,怎么把工单从「待处理」变成「处理中」,再变成「已关闭」。嗯,那个环节更有意思。