iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
Claude AI

從零開始 Claude Code:30 天打造 AI 任務管理 Web App系列 第 17 篇

[Day 17] 任務開始變多:加入分類與篩選

  • 分享至 

  • xImage
  •  

Day 16 已經可以直接在首頁修改任務狀態。
現在首頁上的每一筆任務都有:

狀態
標題
分類
截止日期
編輯
刪除
快速更新狀態

但任務一多,全部混在一起看就會開始不好找。
而且首頁上方原本的:

全部
待處理
進行中
已完成

之前都只是畫面,按了不會真的篩選。
所以 Day 17 的目標是:
讓首頁可以依照狀態和分類篩選任務。


今天的範圍

今天要做兩種篩選:

狀態篩選
   +
分類篩選

例如:

狀態:待處理
分類:生活

首頁就只會顯示同時符合:

status = todo
category = 生活

的任務。
今天先不做:

分類 CRUD
搜尋
分頁
排序切換
services/
routes/
Claude API

先讓 Claude Code 規劃

我先把目前狀態和 Day 17 的範圍交給 Claude Code:

目前狀態:

 - Task CRUD 已完成
 - 首頁已經從 MySQL 顯示真正的 tasks
 - 任務可以在首頁直接修改 status
 - Task 有 todo / in_progress / done 三種狀態
 - Task 透過 category_id 關聯 Category
 - categories 目前已有「學習 / 專案 / 生活」
 - 首頁上方已有「全部 / 待處理 / 進行中 / 已完成」按鈕,但目前沒有功能

今天只做:
「任務狀態篩選 + 分類篩選」

需求:

1. 首頁的狀態按鈕要真的可以篩選:
   - 全部
   - 待處理 todo
   - 進行中 in_progress
   - 已完成 done

2. 加入分類篩選:
   - 全部分類
   - 其他分類從 categories 資料表產生

3. 使用 GET query string,不用 POST
4. 例如:
   /?status=todo
   /?category=專案
   /?status=todo&category=專案

5. 狀態和分類可以同時篩選
6. 沒有帶篩選條件時顯示全部任務
7. status 只允許 todo / in_progress / done
8. 分類名稱不要再手動寫死
9. 排序維持 created_at desc、id desc
10. 目前選中的狀態/分類要顯示 active
11. 篩選後沒有資料時顯示空狀態

今天不要:
 - 分類 CRUD
 - 搜尋
 - 分頁
 - services/
 - routes/
 - Claude API

請先規劃會修改哪些檔案,
不要直接修改程式。

最後 Day 17 主要修改:

app.py
templates/index.html

原本的篩選按鈕樣式可以繼續沿用。


為什麼篩選用 GET?

前幾天新增、修改、刪除資料時,使用的是:

POST

因為那些操作會改變資料庫。
但 Day 17 的篩選只是:

決定要查看哪些資料。

不會修改 Task。
所以適合使用:

GET

例如:

/

代表全部任務。

/?status=todo

代表只看待處理。

/?category=生活

代表只看生活分類。
兩個也可以一起用:

/?status=todo&category=生活

Query String 是什麼?

以:

/?status=todo&category=生活

為例。
? 後面的:

status=todo
category=生活

就是 Query String。
不同參數之間使用:

&

分隔。
Flask 可以透過:

request.args

讀取。
例如:

status_filter = request.args.get("status")
category_filter = request.args.get("category")

app.py:先取得篩選條件

Day 17 的 index() 改成:

@app.route("/")
def index():
    status_filter = request.args.get("status")
    if status_filter not in Task.STATUS_LABELS:
        status_filter = None

    category_filter = request.args.get("category")

這裡先處理狀態。
目前合法的 status 只有:

todo
in_progress
done

而 Task Model 已經有:

STATUS_LABELS = {
    "todo": "待處理",
    "in_progress": "進行中",
    "done": "已完成",
}

所以可以直接拿它來驗證。


不合法的 status 怎麼處理?

例如正常網址:

/?status=todo

會保留:

status_filter = "todo"

但如果自己輸入:

/?status=xxx

因為:

xxx

不在 Task.STATUS_LABELS 裡,所以:

status_filter = None

最後就會視為:

全部狀態

而不是讓程式出錯。


查詢改成先建立 query

Day 13 原本是直接:

tasks = Task.query.order_by(
    Task.created_at.desc(),
    Task.id.desc(),
).all()

Day 17 改成:

query = Task.query

再依照目前的條件慢慢加篩選。


狀態篩選

如果有合法的:

status_filter

就:

if status_filter:
    query = query.filter_by(status=status_filter)

例如:

status_filter = todo

概念上就是:

WHERE status = 'todo'

只留下待處理任務。


分類篩選

接著處理:

category_filter

實際程式是:

if category_filter:
    category = Category.query.filter_by(
        name=category_filter
    ).first()

    query = query.filter_by(
        category_id=category.id if category else -1
    )

例如:

category=生活

Flask 會先到 categories 找:

name = 生活

取得對應的 id,再用 category_id 篩選 Task。


如果網址輸入不存在的分類?

例如:

/?category=不存在

這時:

category = None

程式會改查:

category_id = -1

正常資料不會有:

id = -1

所以查詢結果自然是空的。
頁面不會出現 500。


狀態和分類可以一起篩選

Day 17 的查詢會依序疊加:

query = Task.query

if status_filter:
    query = query.filter_by(status=status_filter)

if category_filter:
    ...
    query = query.filter_by(category_id=...)

所以:

/?status=todo&category=生活

會變成:

status = todo
     AND
category = 生活

也就是兩個條件的交集。


排序維持原本規則

最後才執行:

tasks = query.order_by(
    Task.created_at.desc(),
    Task.id.desc(),
).all()

所以不管怎麼篩選,都還是:

最新建立的任務
	  ↓
較早建立的任務

分類按鈕不是寫死的

Day 17 另外查:

categories = Category.query.order_by(Category.id).all()

再傳給 index.html。
完整概念:

return render_template(
    "index.html",
    tasks=tasks,
    categories=categories,
    current_status=status_filter,
    current_category=category_filter,
)

所以模板現在會知道:

tasks
→ 要顯示哪些任務

categories
→ 資料庫目前有哪些分類

current_status
→ 現在選哪個狀態

current_category
→ 現在選哪個分類

狀態按鈕開始真的有作用

原本是:

<button>全部</button>
<button>待處理</button>
<button>進行中</button>
<button>已完成</button>

Day 17 改成 <a>:

<nav class="filter-tabs" aria-label="任務狀態篩選">
    <a
        class="filter-button{% if not current_status %} active{% endif %}"
        href="{{ url_for('index', category=current_category) }}">
        全部
    </a>

    <a
        class="filter-button{% if current_status == 'todo' %} active{% endif %}"
        href="{{ url_for('index', status='todo', category=current_category) }}">
        待處理
    </a>

    <a
        class="filter-button{% if current_status == 'in_progress' %} active{% endif %}"
        href="{{ url_for('index', status='in_progress', category=current_category) }}">
        進行中
    </a>

    <a
        class="filter-button{% if current_status == 'done' %} active{% endif %}"
        href="{{ url_for('index', status='done', category=current_category) }}">
        已完成
    </a>
</nav>

現在按:

待處理

網址就會變成:

/?status=todo

active 會跟著目前條件切換

例如:

/?status=todo

這段:

{% if current_status == 'todo' %} active{% endif %}

就會讓:

待處理

變成紫色 active 狀態。
不再永遠只有「全部」亮著。


分類也改成真正的篩選按鈕

Day 17 這次沒有做 <select>,而是直接沿用首頁原本的按鈕樣式,增加第二排:

<nav class="filter-tabs" aria-label="分類篩選">
    <a
        class="filter-button{% if not current_category %} active{% endif %}"
        href="{{ url_for('index', status=current_status) }}">
        全部分類
    </a>

    {% for category in categories %}
    <a
        class="filter-button{% if current_category == category.name %} active{% endif %}"
        href="{{ url_for(
            'index',
            status=current_status,
            category=category.name
        ) }}">
        {{ category.name }}
    </a>
    {% endfor %}
</nav>

這裡最重要的是:

{% for category in categories %}

分類名稱不是重新寫:

學習
專案
生活

而是直接從 MySQL 的 categories 資料表產生。


兩種篩選條件不會互相洗掉

這是 Day 17 很重要的一點。
例如先選:

生活

網址:

/?category=生活

再按:

待處理

因為狀態連結裡還有:

category=current_category

所以網址會變成:

/?status=todo&category=生活

分類不會消失。
反過來也一樣。
如果目前:

/?status=in_progress

再選:

生活

就會變成:

/?status=in_progress&category=生活

「全部」只清掉狀態

例如目前:

/?status=todo&category=生活

按:

全部

會使用:

href="{{ url_for('index', category=current_category) }}"

所以最後變成:

/?category=生活

意思是:

全部狀態
   +
生活分類

不會把分類一起清掉。


「全部分類」只清掉分類

同樣地:

href="{{ url_for('index', status=current_status) }}"

例如原本:

/?status=todo&category=生活

按:

全部分類

就會變成:

/?status=todo

只清除分類條件。


測試狀態篩選

按下:

待處理

網址變成:

/?status=todo

首頁只剩:

status = todo

的任務。
而「待處理」按鈕也會變成 active。
https://ithelp.ithome.com.tw/upload/images/20261001/20183857YthYNEwSmq.png


測試分類篩選

例如選:

生活

網址會帶:

/?category=生活

首頁就只會顯示生活分類的任務。
https://ithelp.ithome.com.tw/upload/images/20261001/20183857HVxwYrJHt7.png


測試兩個條件一起使用

我也實際測試:

待處理
  +
 生活

畫面上可以看到兩個按鈕同時 active。
網址會是:

/?status=todo&category=生活

如果目前沒有任何任務同時符合這兩個條件,畫面就顯示空狀態。
https://ithelp.ithome.com.tw/upload/images/20261001/20183857ZkSq4HjFgO.png
這也證明:

status
 AND
category

的組合篩選有正常運作。


Postman 也可以測 GET 篩選

除了瀏覽器,我也可以用 Postman 測。
Method:

GET

Body 不需要填。
例如測全部:

http://127.0.0.1:5000/

測待處理:

http://127.0.0.1:5000/?status=todo

測生活:

http://127.0.0.1:5000/?category=生活

測組合條件:

http://127.0.0.1:5000/?status=todo&category=生活

也可以在 Postman 的 Params 裡輸入:

KEY       VALUE
status    todo
category  生活

Postman 會自動組出 Query String。


測試不合法的 status

還可以自己輸入:

/?status=xxx

因為:

xxx

不是合法狀態,所以:

status_filter = None

最後會等同:

全部狀態

頁面不會出錯。


篩選不會修改 MySQL

Day 16 的狀態更新是:

POST
→ 修改 Task

Day 17 則只是:

GET
→ 查詢 Task

所以不管按:

全部
待處理
進行中
已完成
學習
專案
生活

MySQL 裡的資料內容都不會改變。
Day 17 只是決定:

哪些 Task 要顯示在首頁

Day 17 做完後

現在首頁已經可以:

查看全部任務
只看待處理
只看進行中
只看已完成
只看某個分類
狀態 + 分類一起篩選

而且篩選條件會直接保留在網址:

/?status=todo&category=生活

所以重新整理後,篩選條件仍然存在。
分類按鈕也已經是從:

categories

資料表動態產生,不需要在首頁再寫死分類名稱。

下篇不再增加新的篩選,而是開始處理程式發生錯誤時該怎麼看、怎麼找問題,以及怎麼讓 Claude Code 協助除錯。


上一篇
[Day 16] 任務不只新增刪除:開始管理狀態
系列文
從零開始 Claude Code:30 天打造 AI 任務管理 Web App 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言