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>
💡 小技巧: 分页链接里一定要带上 statuspriority 参数。不然你筛选后点下一页,筛选条件就丢了。我刚开始做的时候忘了这个,被测试妹子追着改 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()

下一章,我会带你做工单的详情页面和状态流转。到时候你会看到,怎么把工单从「待处理」变成「处理中」,再变成「已关闭」。嗯,那个环节更有意思。