iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Modern Web

Laravel Filament 從入門到實戰系列 第 7

Day 06:表格欄位怎麼呈現,格式化與縮圖顯示

  • 分享至 

  • xImage
  •  

Day 05:表單版面配置,用 Schema 排出一份不擁擠的表單 結尾留下一句話,Schema 這套表單與表格共用的定義方式,今天要延伸到表格這一側,商品列表頁目前顯示的表格欄位還是 Day 03:建立第一個 Resource,五分鐘做出一套 CRUD 介面 生成時最粗略的預設樣式,名稱怎麼顯示、價格要不要格式化成貨幣樣式,這些都還沒處理。今天要處理的正是這件事。

列表頁還停在最粗略的樣子

打開商品列表頁,畫面上其實已經能看到七個欄位的資料,但仔細看每一格顯示的內容,會發現落差不小。價格欄位顯示成一串沒有千分位也沒有幣別符號的數字,199 跟 19900 擺在一起,使用者得自己在心裡默默補上逗號才能分辨金額大小。上架狀態顯示成 0 或 1,這種原始值除了寫程式的人以外,沒有人看得出來 1 代表上架還是下架。進貨日期則直接印出資料庫存的原始時間格式,一長串帶著時區與秒數的字串,遠遠超過使用者真正需要知道的資訊量。

這些落差是必然的結果。

Day 03:建立第一個 Resource,五分鐘做出一套 CRUD 介面 用一道指令生成骨架時,Filament 對表格欄位做的事情就是把資料庫欄位值原封不動印出來,沒有多做任何轉換。這跟 Day 04:表單欄位全解析,從文字輸入到日期選擇器 一開始描述的情況幾乎是同一個故事的另一個版本。

能動不代表好用,今天面對的是這個故事在表格這一側的重演。

今天的任務範圍很明確,延續 Day 05 定案的 Schema,把這套定義方式套用到表格欄位上,讓商品列表頁用最適合閱讀與掃視的方式呈現同一批資料。今天不會處理篩選、排序或搜尋,那些機制留給明天,也不會碰任何操作按鈕,表格列上能不能編輯、能不能刪除,是後面才會處理的內容。專心處理一件事,讓表格裡的每一格資料,一眼就能被看懂。

表格欄位跟表單欄位,在意的是不同的事

在動手改表格欄位之前,有一個判斷角度需要先切換過來。表單欄位跟表格欄位雖然都在處理同一批商品資料,但兩者面對的任務完全不同。填一張表單的時候,使用者一次只處理一筆資料,操作節奏是慢下來、逐格確認、把每個欄位填對,Day 04:表單欄位全解析,從文字輸入到日期選擇器 討論的 TextInputSelectDatePicker,全部服務於降低填寫時的認知負擔。瀏覽一份列表的時候,使用者面對的是幾十筆資料同時攤在畫面上,操作節奏是快速掃過去,靠視覺特徵抓出自己要找的那一筆,這時候在意的是掃視效率,不是輸入體驗。

換個方式理解這個差異,同一份商品資料,填寫時像在填一張單據,一格一格對照著填;瀏覽時像在看一份目錄,眼睛快速滑過去,靠排版跟視覺線索抓重點。單據跟目錄服務的是完全不同的閱讀節奏,這正是表單欄位跟表格欄位分屬不同任務的原因。

但這兩種任務背後共用同一層定義方式,這正是 Day 05:表單版面配置,用 Schema 排出一份不擁擠的表單 定案 Schema 這個詞真正的價值所在。學會表單那一側怎麼用元件描述一個欄位的行為,表格這一側的欄位型別命名與使用方式高度相似,不需要重新學一套語法。舉例來說,表單裡處理短文字用 TextInput,表格裡對應的是 TextColumn;表單裡處理布林值用 Toggle,表格裡雖然沒有一對一對應的元件,但同樣可以用格式化方法把布林值轉換成有意義的文字;表單裡今天還沒處理過圖片,但表格這一側今天會新增一個對應的欄位型別。命名邏輯一路呼應下來,讀者不是在學一套全新的心智模型,而是把 Day 4 到 Day 5 已經建立的手感,套用到一個新的呈現場景上。

文字與數字類欄位的格式化,讓數字說人話

先處理商品名稱這個最單純的欄位。表格裡對應的元件是 TextColumn,用法跟表單的 TextInput 一樣直覺:

use Filament\Tables\Columns\TextColumn;

TextColumn::make('name')
    ->label('商品名稱')
    ->searchable(),

這裡先掛上 searchable(),但只是先讓這個方法存在,實際的搜尋機制怎麼運作,留給明天展開,今天不深入討論。

價格欄位是今天第一個真正需要格式化的對象。開場點名的落差,一串沒有千分位也沒有幣別符號的數字,money() 這個方法直接對應解決:

TextColumn::make('price')
    ->label('價格')
    ->money('TWD'),

money('TWD') 讓價格欄位自動補上千分位逗號,並在數字前面加上對應的幣別符號,19900 這種原始數字會變成一眼就能辨識的金額格式。使用者掃視價格這一欄時,看到的不再是一串需要自己默默計算位數的裸數字,而是直接能判斷金額大小的呈現。

庫存數量的處理則稍微不同。庫存本身是單純的整數,不需要千分位或幣別符號,但庫存偏低這件事,值得在視覺上給使用者一點提示:

TextColumn::make('stock')
    ->label('庫存數量')
    ->color(fn (int $state): string => $state < 10 ? 'danger' : 'gray')
    ->alignEnd(),

color() 讓庫存數字在低於某個門檔時換成警示色,alignEnd() 則讓數字靠右對齊,一整欄的數字末位排在同一條線上,掃視起來更容易比較大小。這裡要特別說明一句,這個顏色提示只是呈現層面的視覺效果,判斷門檔寫在畫面呈現的程式碼裡,還不是一套真正的庫存警示機制,庫存低到什麼程度該通知誰、該不該觸發補貨流程,這些業務邏輯今天不展開,只是先讓使用者掃視列表時,多一層顏色線索。

進貨日期則是格式一致性的老問題再次出現。DatePicker 已經在 Day 04:表單欄位全解析,從文字輸入到日期選擇器 解決了輸入端的格式問題,今天要讓顯示端也保持同樣的一致性:

TextColumn::make('restocked_at')
    ->label('進貨日期')
    ->date('Y/m/d'),

date() 方法接受一組 PHP 日期格式標記,Y/m/d 讓每一筆進貨日期都用同一種年月日格式顯示,不會再看到資料庫原始時間格式裡那些使用者根本不需要的時區與秒數資訊。

上架狀態是這一段最後一個要處理的欄位,也是開場點名最直接的落差,0 或 1 這種原始值對使用者毫無意義。這裡用 formatStateUsing() 搭配 badge(),把布林值轉換成清楚的文字標示:

use Filament\Support\Enums\FontWeight;

TextColumn::make('is_active')
    ->label('上架狀態')
    ->badge()
    ->formatStateUsing(fn (bool $state): string => $state ? '已上架' : '已下架')
    ->color(fn (bool $state): string => $state ? 'success' : 'gray'),

formatStateUsing() 只改變畫面上顯示的文字,不動資料庫裡實際存的布林值,badge() 讓這段文字呈現成一顆帶底色的徽章樣式,再搭配 color() 依狀態切換顏色,已上架用綠色徽章,已下架用灰色徽章。使用者掃過這一欄,不需要理解 0 跟 1 背後代表什麼,徽章本身的顏色跟文字已經把答案直接說出來了。

分類與縮圖,讓表格多一層視覺辨識

分類欄位在表單那一側已經用 Select 限制成三種固定選項,寵物飼料、寵物玩具、清潔用品,表格這一側正好可以延續這個限定性,用徽章樣式呈現:

TextColumn::make('category')
    ->label('分類')
    ->badge()
    ->formatStateUsing(fn (string $state): string => match ($state) {
        'food' => '寵物飼料',
        'toy' => '寵物玩具',
        'cleaning' => '清潔用品',
    })
    ->color(fn (string $state): string => match ($state) {
        'food' => 'warning',
        'toy' => 'info',
        'cleaning' => 'success',
    }),

三種分類各自搭配固定的顏色,使用者掃視列表時,不需要逐字讀完分類名稱,光靠徽章顏色就能大致分群,這正是掃視效率這個任務在意的事,比起表單裡使用者一次只面對一個下拉選單,表格這裡使用者面對的是一整欄需要快速分辨的分類標示。

接下來是今天標題明確點名的重點,圖片縮圖。目前商品既有的七個欄位裡沒有圖片這一項,這裡要新增一個商品圖片欄位,這是本篇唯一新增的欄位,明確標註如下。

本篇新增欄位:image,用來存放商品縮圖的檔案路徑。這個新增只影響表格與表單的圖片呈現本身,不改動既有七個欄位的定義與判斷邏輯。Migration 端補上一個可為空的字串欄位:

Schema::table('products', function (Blueprint $table) {
    $table->string('image')->nullable();
});

表單端則需要對應加上一個上傳元件,讓使用者能實際選擇圖片檔案,這裡只做最基本的對應設定,完整的檔案儲存細節留給後續天數深入:

use Filament\Forms\Components\FileUpload;

FileUpload::make('image')
    ->label('商品圖片')
    ->image()
    ->directory('products'),

表格這一側才是今天真正的主戲,用 ImageColumn 顯示縮圖:

use Filament\Tables\Columns\ImageColumn;

ImageColumn::make('image')
    ->label('圖片')
    ->square()
    ->defaultImageUrl(url('/images/placeholder.png')),

square() 讓圖片以固定的正方形比例顯示,不會因為每張商品照片的原始尺寸不同而讓整欄高度參差不齊。defaultImageUrl() 則處理還沒上傳圖片的商品,顯示一張預設的佔位圖,而不是留一格空白讓使用者以為畫面壞了。

縮圖存在的價值,在寵物用品批發商這個示範情境裡特別明顯。想像列表裡同時有「寵物潔牙骨 S 號」跟「寵物潔牙骨 M 號」,兩個商品名稱高度相似,光靠文字要快速分辨哪一筆是哪一筆,得盯著名稱多看幾秒。一張小小的縮圖擺在名稱前面,使用者一眼掃過去,靠包裝顏色或形狀就能直接對上腦中要找的那一筆,不需要逐字比對名稱裡那個容易被忽略的 S 跟 M。

這正是掃視效率這個任務裡,文字本身經常做不到、圖像卻能立刻辦到的事。

商品列表頁格式化前後對照

表格讀起來清楚了,資料量還沒被真正考驗

今天做完這一輪調整,商品列表頁的樣貌已經跟開場描述的原始狀態完全不同。價格有千分位跟幣別符號,庫存數量在偏低時會換上警示色,上架狀態變成清楚的已上架、已下架徽章,進貨日期統一成一致的年月日格式,分類有各自的顏色分群,商品縮圖讓相似名稱的商品也能快速被分辨出來。這批改善直接呼應了 Day 5 結尾留下的伏筆,Schema 這套定義方式,確實不只服務表單,也同樣服務表格。

但這裡要老實補一句,目前這些改善只用商品模組現有的少量測試資料驗證過,列表裡可能只有小貓小狗幾筆商品。資料筆數不多的時候,就算每一欄都排得再好看,使用者也不會真正遇到「在一長串列表裡找不到想要的那一筆」的困擾,因為根本沒有一長串列表可以讓人迷失。

一旦商品資料真的累積到幾十筆、上百筆,使用者要在列表裡找到特定分類、特定價格區間的商品,光靠欄位排得好看是不夠的。篩選、排序與搜尋,是明天要處理的內容。


上一篇
Day 05:表單版面配置,用 Schema 排出一份不擁擠的表單
下一篇
Day 07:讓大量資料可用,篩選、排序與搜尋
系列文
Laravel Filament 從入門到實戰8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言