Day 06:表格欄位怎麼呈現,格式化與縮圖顯示 結尾留下一句話,今天這批改善只用商品模組現有的少量測試資料驗證過,資料筆數不多的時候,就算每一欄都排得再好看,使用者也不會真正遇到在一長串列表裡找不到想要那一筆的困擾。一旦商品資料真的累積到幾十筆、上百筆,光靠欄位排得好看是不夠的。今天要處理的正是這件事。
先把 Day 6 做完的商品列表頁在腦中重新播放一次。價格有千分位跟幣別符號,庫存數量偏低時換上警示色,上架狀態變成清楚的徽章,進貨日期統一成一致的年月日格式,分類有各自的顏色分群,商品縮圖也已經到位。每一欄單獨看都很清楚,這點無庸置疑。
但這批改善從頭到尾只在小貓小狗幾筆測試資料上驗證過。真正把寵物用品批發商這個示範情境的後台想像成正式營運起來的樣子,商品筆數很快會累積到幾十筆、上百筆,橫跨寵物飼料、寵物玩具、清潔用品三種分類,價格從幾十元的零食到幾千元的貓籠都有,庫存數量高高低低,進貨日期分散在過去幾個月裡。這時候使用者想找某個價格區間的商品,或是只想看寵物玩具這個分類底下還有哪些庫存偏低的品項,能做的事情只有一路往下捲動整份列表,用眼睛一筆一筆核對。
資料筆數個位數的時候,這樣做不痛不癢,捲兩下就到底了。
要是資料筆數到達幾千個的時候,這件事會變成日常操作裡最耗時的瑣事,比填一張表單、看一次縮圖都花更多時間。
這也是這個系列第一次認真面對資料量本身這個維度。前面幾天處理的都是單筆資料該怎麼填、怎麼顯示,今天要處理的是一整份列表變大之後,介面撐不撐得住。今天的任務範圍很明確,不調整任何一個表格欄位的呈現方式,名稱依然是 TextColumn,圖片依然是 ImageColumn,這些在 Day 6 已經定案,不會重新討論。要做的是在既有的表格上疊加排序、篩選、搜尋三種機制,讓使用者不需要用眼睛核對就能找到目標資料。這三種機制其實不是新的查詢知識,Laravel 的查詢建構器裡熟悉的 orderBy、where、like,讀者應該都不陌生,今天要學的只是怎麼把這些已知的查詢邏輯,包裝成使用者能在表格介面上直接點選、輸入的操作元件。
三項機制裡,排序是最容易上手的起點,因為它解決的問題性質最單純,決定一整份列表的呈現順序,而不是從裡面挑出特定幾筆。價格、庫存數量、進貨日期這幾個欄位都帶有大小或先後的順序意義,適合開啟排序:
use Filament\Tables\Columns\TextColumn;
TextColumn::make('price')
->label('價格')
->money('TWD')
->sortable(),
TextColumn::make('stock')
->label('庫存數量')
->color(fn (int $state): string => $state < 10 ? 'danger' : 'gray')
->alignEnd()
->sortable(),
TextColumn::make('restocked_at')
->label('進貨日期')
->date('Y/m/d')
->sortable(),
每個欄位只需要加上 sortable(),Day 6 已經定案的格式化方法,money()、color()、date() 全部維持不變,排序是疊加上去的新能力,不是取代原有的呈現邏輯。掛上這個方法之後,商品列表頁的欄位標題會變成可以點擊的按鈕,點一下依這個欄位由小到大排列,再點一下切換成由大到小,畫面右上角會多出一個小箭頭圖示,提示使用者目前的排列方向。
使用者想看目前價格最高的商品,或是想確認最近進貨的批次是哪幾筆,答案都不是找出特定一筆資料,而是決定整份列表該用什麼順序攤開。這跟接下來要談的篩選、搜尋在問題性質上不一樣,篩選跟搜尋處理的是把不相關的資料排除掉,排序處理的是留下來的這些資料該怎麼排。
分類跟上架狀態這兩個欄位則不建議開啟排序。分類是寵物飼料、寵物玩具、清潔用品三個固定選項,彼此之間沒有誰大誰小的天然順序,上架狀態同樣只有上架或下架兩種狀態,排序在這裡沒有實際意義,硬是加上 sortable() 只會讓使用者點了按鈕卻看不出排列邏輯改變在哪裡。判斷一個欄位要不要開放排序,先問這個欄位的值之間有沒有大小或先後關係,有才值得開,沒有就維持原樣,不需要每個欄位無差別開啟這個功能。
排序解決的是順序問題,篩選解決的是另一種更常見的需求,使用者已經明確知道自己想看的是哪個範圍,只是不想在一長串列表裡自己動手挑出來。這種心態最貼近的比喻,有點像去書店找一本書,先靠分類縮小到某個書架,而不是把整間書店的書一本一本翻過去。
商品分類是最直接的篩選對象。Day 04:表單欄位全解析,從文字輸入到日期選擇器 已經用 Select 把分類限制在三個固定選項,篩選這一側可以直接對應這幾個既有的分類值,不需要額外設計新的選項清單:
use Filament\Tables\Filters\SelectFilter;
SelectFilter::make('category')
->label('分類')
->options([
'food' => '寵物飼料',
'toy' => '寵物玩具',
'cleaning' => '清潔用品',
]),
SelectFilter 會在表格上方生成一個下拉選單,使用者選擇寵物玩具,列表立刻只留下這個分類底下的商品,其他分類的資料整批被排除在畫面之外。這個篩選元件的選項定義方式,跟表單裡的 Select 幾乎一模一樣,同一套分類定義,只是換到不同的使用場景。
上架狀態是布林值欄位最直覺的篩選情境,呼應 Day 4 用 Toggle 處理這個欄位的判斷,篩選這一側用 TernaryFilter 對應:
use Filament\Tables\Filters\TernaryFilter;
TernaryFilter::make('is_active')
->label('上架狀態')
->trueLabel('只看已上架')
->falseLabel('只看已下架'),
TernaryFilter 顧名思義提供三種狀態,只看已上架、只看已下架,或是不篩選、兩種都看。使用者想找出所有已下架的商品準備清點,或是只想看目前還在賣的品項,這個篩選器直接對應這兩種常見的查看需求。
價格則是另一種篩選形態,區間篩選。使用者想看的往往不是單一個價格,而是一個範圍內的商品,這時候需要兩個輸入端點,而不是一份選項列表:
use Filament\Tables\Filters\Filter;
use Filament\Forms\Components\TextInput;
use Illuminate\Database\Eloquent\Builder;
Filter::make('price_range')
->label('價格區間')
->schema([
TextInput::make('price_from')
->label('最低價格')
->numeric(),
TextInput::make('price_to')
->label('最高價格')
->numeric(),
])
->query(function (Builder $query, array $data): Builder {
return $query
->when(
$data['price_from'],
fn (Builder $query, $price): Builder => $query->where('price', '>=', $price),
)
->when(
$data['price_to'],
fn (Builder $query, $price): Builder => $query->where('price', '<=', $price),
);
}),
這裡用的是 Filter 而不是前面兩個現成的篩選類型,因為區間篩選需要自己定義畫面上要出現哪些輸入欄位,schema 裡放的正是 Day 5 已經熟悉的表單欄位定義方式,這裡沿用 TextInput 讓使用者輸入區間的上下限。query() 方法接收使用者實際輸入的資料,回傳一個加上條件的查詢建構器,when() 的用法跟一般 Eloquent 查詢裡的寫法完全一樣,讀者不需要學一套新的查詢語法,只是把原本可能寫在 Controller 裡的查詢條件,搬到這個篩選元件的定義裡。
這三個篩選示範完,值得停下來點出篩選跟排序的差異。篩選是先把不相關的資料整批排除,只留下符合條件的子集合,排序才是決定這個子集合裡的呈現順序。兩者可以疊加使用,先篩選出寵物玩具分類,再依價格排序,使用者一次操作就能看到這個分類裡由便宜到貴排列的商品清單,這兩項機制不是互相取代,而是互相接續。
篩選建立在使用者已經知道明確條件之上,知道要看哪個分類、哪個上架狀態、哪個價格區間。但有些時候使用者手上只有一個模糊的線索,只記得商品名稱裡有某兩個字,不確定屬於哪個分類,也不確定價格範圍。延續前面書店的比喻,篩選是先靠分類縮小到某個書架,搜尋則更接近直接靠書名關鍵字精準定位,不需要先猜這本書會被放在哪個書架上。
商品名稱是最適合開放搜尋的欄位,Day 6 其實已經先掛上這個方法,只是當時沒有展開說明實際運作方式:
TextColumn::make('name')
->label('商品名稱')
->searchable(),
掛上 searchable() 之後,表格上方會出現一個搜尋輸入框,使用者輸入商品名稱的片段文字,例如只打「潔牙」兩個字,列表即時篩選出名稱包含這段文字的商品,「寵物潔牙骨 S 號」跟「寵物潔牙骨 M 號」會同時被找出來,不需要使用者記得完整的商品全名,也不需要先判斷這兩筆資料屬於哪個分類。
搜尋跟篩選的差異在使用情境上很明顯。篩選適合使用者已經掌握明確條件的時候,搜尋則適合使用者只掌握片段資訊、無法用固定選項或區間表達的時候。這兩者加上前面的排序,三項機制各自解決不同性質的查找需求,不是三選一的替代關係,而是可以同時疊加使用的組合,使用者可以先用搜尋框打出商品名稱的片段,再從剩下的結果裡依價格排序,一次操作串接兩種查找路徑。

今天做完這一輪調整,商品列表頁已經不只是排得好看,而是真正扛得住資料量變大的考驗。使用者可以依分類、上架狀態、價格區間篩選,可以依價格、庫存數量、進貨日期排序,也可以直接用商品名稱的關鍵字片段搜尋。這三項機制彼此獨立又能疊加使用,資料筆數不管累積到幾十筆還是上百筆,使用者都能在幾次操作內縮小到自己想要的那一批。
但這裡要點出一個新的落差:就算篩選出一群目標商品,例如所有已下架的清潔用品,表格上目前沒有任何一顆按鈕可以讓使用者對這群資料做什麼。想把這群商品一次重新上架,或是想針對單筆商品做編輯以外的操作,都還無處著落。商品表格現在排得好看,也找得到想要的資料了,但找到之後能不能動手,是完全另一個問題。
表格列上要有什麼操作、頁首要有什麼操作、表單裡又能掛載什麼操作,這幾種操作掛載的位置與方式,是明天要處理的內容。