使用 Filament Panels 快速建立獸醫診所患者管理系統

Marden發表於2023-11-20

概述

皮膚是 Filament 中的頂級容器,它允許你構建功能豐富的管理皮膚,其中包括頁面、資源、表單、表格、通知、Action、資訊列表和 Widget。所有皮膚都有一個預設的儀表盤,其中包含統計、圖表、表格等多種 Widget。

前置要求

使用 Filament 之前,你需要先熟悉 Laravel。Filament 是基於許多 Laravel 核心概念構建的,特別是資料庫遷移Eloquent ORM。如果你此前未曾用過 Laravel,或者需要複習,建議你跟著 Laravel Bootcamp 去建立一個小應用。那個教程涵蓋了建立 Laravel 應用的基礎知識。

Demo 專案

本教程介紹使用 Filament 為獸醫診所建立一個簡單的患者管理系統。它將支援新增新患者(貓、狗或兔子),將他們分配給主人(owners),並記錄他們接受的治療(treatments)。該系統將有一個儀錶板,上面有患者型別的統計資料,還有一張顯示過去一年治療次數的圖表。

設定資料庫和模型

這個專案中需要 3 個模型 - OwnerPatientTreatment。使用以下 artisan 命令建立:

php artisan make:model Owner -m
php artisan make:model Patient -m
php artisan make:model Treatment -m

定義遷移

資料庫遷移請使用如下 schema:


// create_owners_table
Schema::create('owners', function (Blueprint $table) {
    $table->id();
    $table->string('email');
    $table->string('name');
    $table->string('phone');
    $table->timestamps();
});

// create_patients_table
Schema::create('patients', function (Blueprint $table) {
    $table->id();
    $table->date('date_of_birth');
    $table->string('name');
    $table->foreignId('owner_id')->constrained('owners')->cascadeOnDelete();
    $table->string('type');
    $table->timestamps();
});

// create_treatments_table
Schema::create('treatments', function (Blueprint $table) {
    $table->id();
    $table->string('description');
    $table->text('notes')->nullable();
    $table->foreignId('patient_id')->constrained('patients')->cascadeOnDelete();
    $table->unsignedInteger('price')->nullable();
    $table->timestamps();
});

使用 php artisan migrate 執行遷移檔案。

解除所有模型的防護

為了本教程的簡潔,我們將禁用 Laravel 的批次賦值保護。Filament 透過只將有效資料存入到模型中,因此模型可以安全地解除防護。要一次性解除所有模型的防護,只需在 app/Providers/AppServiceProvider.phpboot() 方法中新增 Model::unguard()

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::unguard();
}

設定模型之間的關聯

讓我們來設定模型之間的關聯。在此係統中,寵物的主人可以擁有多個寵物(患者),而患者可能有多個治療:

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Owner extends Model
{
    public function patients(): HasMany
    {
        return $this->hasMany(Patient::class);
    }
}

class Patient extends Model
{
    public function owner(): BelongsTo
    {
        return $this->belongsTo(Owner::class);
    }

    public function treatments(): HasMany
    {
        return $this->hasMany(Treatment::class);
    }
}

class Treatment extends Model
{
    public function patient(): BelongsTo
    {
        return $this->belongsTo(Patient::class);
    }
}

介紹資源

在 Filament 中,資源是用於為你的 Eloquent 模型建立 CRUD 介面的靜態類。他們描述了後臺管理員應該如何在皮膚中和資料互動 - 透過表格和表單。

因為患者(寵物)是這個系統的核心實體。我們就從建立患者資源開始,該資源將幫助我們構建新建、檢視、更新及刪除患者的頁面

使用如下 artisan 命令為 Patient 模型建立一個新的 Filament 資源:

php artisan make:filament-resource Patient

這將在 app/Filament/Resources 目錄下建立多個檔案:

.
+-- PatientResource.php
+-- PatientResource
|   +-- Pages
|   |   +-- CreatePatient.php
|   |   +-- EditPatient.php
|   |   +-- ListPatients.php

在瀏覽器中訪問 /admin/patients,可以發現一個新的連結(Patients)已經被新增到側邊欄了。點選此連結將會展示一個空表格。讓我們來新增表單以建立新患者吧。

設定資源表單

如果你開啟 PatientResource.php 檔案,你會看到一個 form() 方法,其中有一個空的 schema([...]) 陣列。新增表單欄位到該 schema,構建一個用於新建及編輯患者的表單。

“Name” 文字輸入框

Filament 捆綁了大量表單欄位。讓我們從最簡單的文字輸入框入手:

use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('name'),
        ]);
}

訪問 /admin/patients/create (或者點選 “New patient” 按鈕),可以看到一個患者名稱的表單欄位已經被新增進來了。

由於該欄位是資料庫中必須的,且有 255 字元的最大長度限制。讓我們新增兩個驗證規則到該欄位:

use Filament\Forms;

Forms\Components\TextInput::make('name')
    ->required()
    ->maxLength(255)

嘗試不輸入名字提交表單,就會發現出現一條資訊提示,告訴你 name 欄位是必須的。

“Type” 下拉選單

讓我們來新增第二個欄位,患者的型別 - 可以是貓、狗或者兔子。因為它是一組固定的選項供選擇,使用 Select 欄位是不錯的選擇:

use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('name')
                ->required()
                ->maxLength(255),
            Forms\Components\Select::make('type')
                ->options([
                    'cat' => 'Cat',
                    'dog' => 'Dog',
                    'rabbit' => 'Rabbit',
                ]),
        ]);
}

Select 欄位的 options() 方法接收了一個選項陣列,使使用者可以從中選擇。陣列的鍵應該匹配資料庫,陣列值將用作表單標籤。你可以按照你的需求新增儘可能多的寵物型別到該陣列中。

由於該欄位也是資料庫必須的欄位,因此我們需要加上 required() 驗證規則方法:

use Filament\Forms;

Forms\Components\Select::make('type')
    ->options([
        'cat' => 'Cat',
        'dog' => 'Dog',
        'rabbit' => 'Rabbit',
    ])
    ->required()

“生日” 選擇器

讓我們新增一個日期選擇器欄位用於 date_of_birth 列,同時新增驗證規則(生日為必須的,日期不應該遲於今天)。

use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('name')
                ->required()
                ->maxLength(255),
            Forms\Components\Select::make('type')
                ->options([
                    'cat' => 'Cat',
                    'dog' => 'Dog',
                    'rabbit' => 'Rabbit',
                ])
                ->required(),
            Forms\Components\DatePicker::make('date_of_birth')
                ->required()
                ->maxDate(now()),
        ]);
}

“Owner” 下拉選單

我們應該在建立新患者(Patient)的同時,新增其主人(owner)。因為我們在患者模型中新增了一個 BelongsTo 關聯(關聯到相應的 Owner 模型),我們可以在 Select 欄位中使用 relationship() 來載入主人列表,以供選擇:

use Filament\Forms;
use Filament\Forms\Form;

public static function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('name')
                ->required()
                ->maxLength(255),
            Forms\Components\Select::make('type')
                ->options([
                    'cat' => 'Cat',
                    'dog' => 'Dog',
                    'rabbit' => 'Rabbit',
                ])
                ->required(),
            Forms\Components\DatePicker::make('date_of_birth')
                ->required()
                ->maxDate(now()),
            Forms\Components\Select::make('owner_id')
                ->relationship('owner', 'name')
                ->required(),
        ]);
}

relationship() 方法的首個引數是要模型中定義關聯的函式名(用於載入 Select 選項) —— 即本例的 owner。而第二個引數是關聯表的欄位名,如本例中的 name

我同時讓主人是可搜尋的(searchable()),且預載入(preload())前 50 個主人到這個可搜尋的列表中(以防列表太長):

use Filament\Forms;

Forms\Components\Select::make('owner_id')
    ->relationship('owner', 'name')
    ->searchable()
    ->preload()
    ->required()
不離開頁面新建 Owner

此刻,資料庫中沒有 Owner。不去建立單獨的 Filament 主人資源,讓我們提供給使用者一個便捷的方式:透過模型表單新增主人(可透過 Select 欄位旁邊的 + 按鈕訪問)。使用 createOptionForm() 方法 來嵌入一個模態框,模態框中新增用於主人名稱、郵箱及電話的 TextInput 欄位

use Filament\Forms;

Forms\Components\Select::make('owner_id')
    ->relationship('owner', 'name')
    ->searchable()
    ->preload()
    ->createOptionForm([
        Forms\Components\TextInput::make('name')
            ->required()
            ->maxLength(255),
        Forms\Components\TextInput::make('email')
            ->label('Email address')
            ->email()
            ->required()
            ->maxLength(255),
        Forms\Components\TextInput::make('phone')
            ->label('Phone number')
            ->tel()
            ->required(),
    ])
    ->required()

本例中使用了一些 TextInput 新方法:

  • label() 覆蓋每個欄位自動生成的標籤。本例中我們將 Email 欄位的標籤設為 Email addressPhone 欄位的標籤設定為 Phone number
  • email() 確保只有有效的郵箱地址可以輸入到該欄位。同時它會改變移動端的鍵盤佈局。
  • tel() 確保只有有效的電話號碼可以輸入到該欄位中。同時會改變移動端的鍵盤佈局。

這個表單應該已經生效了!你可以嘗試建立新患者及它們的Owner。建立完成後,頁面會跳轉到編輯頁,你可以在此更新詳情。

設定患者表格

再次訪問 /admin/patients。如果你建立了一個患者,表格中理應有一行帶有編輯按鈕的空資料。讓我們新增一些欄位到表格中,這樣我們就能從中檢視實際患者資料。

開啟 PatientResource.php 檔案,你可以看到一個 table() 方法,它帶有一個空的 columns([...]) 陣列。你可以使用該陣列將欄位新增到 patients 表。

新增文字欄位

Filament 捆綁了一系列表格列欄位。讓我們使用最簡單的文字列,將其用在 patients 表格中的所有欄位:

use Filament\Tables;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('name'),
            Tables\Columns\TextColumn::make('type'),
            Tables\Columns\TextColumn::make('date_of_birth'),
            Tables\Columns\TextColumn::make('owner.name'),
        ]);
}

Filament 使用點語法來即時載入關聯資料。我們在表格中使用 owner.name 來展示 owner 的名稱,而非資訊較少的 ID 號。你同時應該為主人的郵箱和電話新增列。

使列可搜尋

隨著獸醫業務的增長,在表格中直接搜尋患者的能力很有用。你可以在列中鏈式呼叫 searchable() 方法使之可查詢。讓我們將患者名稱和主人名稱設成可搜尋。

use Filament\Tables;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('name')
                ->searchable(),
            Tables\Columns\TextColumn::make('type'),
            Tables\Columns\TextColumn::make('date_of_birth'),
            Tables\Columns\TextColumn::make('owner.name')
                ->searchable(),
        ]);
}

重新整理該頁面,你會發現表格中有一個新增的搜尋文字框欄位,使用搜尋條件過濾表格記錄。

使這些列可排序

要讓 patients 表格可以按照年齡排序,請將 sortable() 方法新增到 date_of_birth 表格列:

use Filament\Tables;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('name')
                ->searchable(),
            Tables\Columns\TextColumn::make('type'),
            Tables\Columns\TextColumn::make('date_of_birth')
                ->sortable(),
            Tables\Columns\TextColumn::make('owner.name')
                ->searchable(),
        ]);
}

這會在該列的表頭新增一個排序按鈕,點選該按鈕會讓表格按生日進行排序。

透過患者型別篩選

雖然你可以將 type 欄位設定成可搜尋的 searchable,不過使之可過濾會有更好的使用者體驗。

Filament 表格可以設定過濾器,過濾器是一個允許你限制 Eloquent 查詢範圍來減少表格記錄的元件。過濾器過濾器甚至可以包含自定義表單元件,這使之稱為構建介面的有力工具。

Filament 包含了一個預製的 SelectFilter,你可以將其新增到表格的 filters() 中:

use Filament\Tables;
use Filament\Tables\Table;

public static function table(Table $table): Table
{
    return $table
        ->columns([
            // ...
        ])
        ->filters([
            Tables\Filters\SelectFilter::make('type')
                ->options([
                    'cat' => 'Cat',
                    'dog' => 'Dog',
                    'rabbit' => 'Rabbit',
                ]),
        ]);
}

重新整理頁面,會發現表格的右上角有一個新的過濾器圖示(在搜尋表單旁邊)。這個過濾器開啟一個包含患者型別的選擇選單。現在,你可以嘗試透過患者型別進行資料過濾了。

介紹關聯管理器

此刻,系統中患者可以和它們的主人相關聯了。但是,如果我們想要第三個層級呢?患者來到診所尋求治療,系統應該可以記錄這些治療並與患者關聯。

其中一個方式是,新建一個 TreatmentResource,使用 Select 欄位讓治療和患者相關聯。但是,管理治療如果單獨放在患者資訊之外對使用者來說過於複雜。Filament 使用”管理管理器”來解決這一的問題。

關聯管理器是在父資源的編輯頁面中展示現有資源的關聯記錄的表格。比如,我們的專案中,你可以直接在編輯表單下直接檢視或者管理患者的治療。

你頁可以使用 Filament 的 Action 去開啟模態框表單,在患者表單中直接建立、編輯或刪除治療。

使用 make:filament-relation-manager artisan 命令,可以快速建立關聯,將患者資源和相關治療連線起來:

php artisan make:filament-relation-manager PatientResource treatments description
  • PatientResource 是 Patinent 模型的資源類名。由於治療(Treatment) 屬於患者(Patient),治療應該在患者編輯頁面中展示。
  • treatments 是此前我們在 Patient 模型中建立的關聯名。
  • description 是 treatments 表中要展示的列。

該命令將新建一個 PatientResource/RelationManagers/TreatmentsRelationManager 檔案。你需要到 PatientResourcegetRelations() 方法中註冊新的關聯管理器。

use App\Filament\PatientResource\RelationManagers;

public static function getRelations(): array
{
    return [
        RelationManagers\TreatmentsRelationManager::class,
    ];
}

TreatmentsRelationManager.php 檔案中包含一個使用 make:filament-relation-manager artisan 命令引數預先填充 form 和 table 類。你可以在關聯管理器中自定義表單欄位和表格列,類似於資源:

use Filament\Forms;
use Filament\Forms\Form;
use Filament\Tables;
use Filament\Tables\Table;

public function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('description')
                ->required()
                ->maxLength(255),
        ]);
}

public function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('description'),
        ]);
}

現在,訪問其中一位患者的編輯頁面。你應該已經可以建立、編輯、刪除並羅列該患者的治療。

設定治療表單

預設情況下,文字欄位只佔用表單寬度的一半。因為 description 欄位可能包含很多資訊,這裡我們使用 columnSpan('full') 方法讓該欄位佔用模特款表單的整個寬度:

use Filament\Forms;

Forms\Components\TextInput::make('description')
    ->required()
    ->maxLength(255)
    ->columnSpan('full')

讓我們新增 notes 欄位,用於新增更多治療的資訊。我們可以使用 Textarea 欄位,並使用 columnSpan('full')

use Filament\Forms;
use Filament\Forms\Form;

public function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('description')
                ->required()
                ->maxLength(255)
                ->columnSpan('full'),
            Forms\Components\Textarea::make('notes')
                ->maxLength(65535)
                ->columnSpan('full'),
        ]);
}

配置 price 欄位

我們來為治療新增一個 price 欄位。我們可以使用文字輸入框,並進行一些定製工作使之適於貨幣輸入。它是 numberic() 的,新增驗證的同時,改變移動裝置中鍵盤的佈局。同時使用 prefix() 方法新增貨幣字首,比如,prefix(‘€’)會在不影響儲存的輸出值的情況下,在文字輸入框前面新增€`:

use Filament\Forms;
use Filament\Forms\Form;

public function form(Form $form): Form
{
    return $form
        ->schema([
            Forms\Components\TextInput::make('description')
                ->required()
                ->maxLength(255)
                ->columnSpan('full'),
            Forms\Components\Textarea::make('notes')
                ->maxLength(65535)
                ->columnSpan('full'),
            Forms\Components\TextInput::make('price')
                ->numeric()
                ->prefix('€')
                ->maxValue(42949672.95),
        ]);
}
將價格轉成整型

Filament 將貨幣值儲存成整型(而非浮點型),以避免取整和精度問題 —— 這是一個被 Laravel 社群廣泛接受的方式。然而,這要求在 Laravel 中建立一個 Cast,使其在檢索時將整型轉換成浮點型,在存入資料庫時轉換回整型。使用如下命令建立 Cast:

php artisan make:cast MoneyCast

app/Casts/MoneyCast.php 檔案中,更新 get()set() 方法:

public function get($model, string $key, $value, array $attributes): float
{
    // Transform the integer stored in the database into a float.
    return round(floatval($value) / 100, precision: 2);
}

public function set($model, string $key, $value, array $attributes): float
{
    // Transform the float into an integer for storage.
    return round(floatval($value) * 100);
}

現在,在 Treatment 模型中將 MoneyCast 新增到 price 屬性:

use App\Casts\MoneyCast;
use Illuminate\Database\Eloquent\Model;

class Treatment extends Model
{
    protected $casts = [
        'price' => MoneyCast::class,
    ];

    // ...
}

設定治療表格

生成關聯管理器時,已經自動新增了 description 文字欄位。讓我們同時新增一個 sortable() 可排序的帶貨幣字首的 price 欄位。使用 Filament 的 money() 方法將 price 欄位格式化為金額 —— 比如此處的 EUR ():

use Filament\Tables;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('description'),
            Tables\Columns\TextColumn::make('price')
                ->money('EUR')
                ->sortable(),
        ]);
}

同時使用預設的 created_at 時間戳新增一個欄位說明治療時間。使用 dateTime() 方法使之展示成人類可讀的格式:

use Filament\Tables;
use Filament\Tables\Table;

public function table(Table $table): Table
{
    return $table
        ->columns([
            Tables\Columns\TextColumn::make('description'),
            Tables\Columns\TextColumn::make('price')
                ->money('usd')
                ->sortable(),
            Tables\Columns\TextColumn::make('created_at')
                ->dateTime(),
        ]);
}

你可以傳入任何有效的PHP 日期格式字串dateTime() 方法 (e.g. dateTime('m-d-Y h:i A'))。

介紹 Widget

Filament Widget 是一個在儀表盤上展示資訊的元件,特別是統計資訊。Wideget 通常被新增到皮膚中預設的儀表盤中,不過你也可以將其新增到任何頁面,包括資源頁。Filament 自帶了一些內建 Widget,比如統計 Widget用以在簡單的卡片中渲染重要的統計資訊;圖表 widget用來渲染直觀的圖表;表格 Widget 允許你內嵌表格構造器。

讓我們為預設的儀表盤頁新增一個統計 Widget,使之包含每個患者型別的統計資訊以及一個隨時間推進的視覺化治療圖表。

建立統計外掛

使用如下 artisan 命令建立 stats widget,用來渲染患者型別:

php artisan make:filament-widget PatientTypeOverview --stats-overview

出現提示時,不要指定資源,並選擇 “admin” 作為路徑。

這會生成 app/Filament/Widgets/PatientTypeOverview.php 檔案。開啟在 getCards() 方法中返回 Stat 例項:

<?php

namespace App\Filament\Widgets;

use App\Models\Patient;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;

class PatientTypeOverview extends BaseWidget
{
    protected function getCards(): array
    {
        return [
            Stat::make('Cats', Patient::query()->where('type', 'cat')->count()),
            Stat::make('Dogs', Patient::query()->where('type', 'dog')->count()),
            Stat::make('Rabbits', Patient::query()->where('type', 'rabbit')->count()),
        ];
    }
}

開啟儀表盤,你就應該能看到新的 Widget。每個統計顯示了特定型別的患者總數。

建立圖表 widget

讓我們在儀表盤中新增一個圖表,用來展示隨時間推進的治療。使用以下 artisan 命令建立一個新的圖表 Widget:

php artisan make:filament-widget TreatmentsChart --chart

當出現提示時,不要指定資源,選擇 “admin” 作為目標路徑,並選擇 “line chart” 作為型別。

開啟 app/Filament/Widgets/TreatmentsChart.php 並將圖表的標題 $heading 設定成 “Treatments”。

getData() 方法返回一個資料集和標籤組成的陣列。每個資料集都是要在圖表上繪製的標籤點陣列,每個標籤都是一個字串。此結構與 Chart.js 庫相同,Filament 用它來渲染圖表。

要從 Eloquent 模型中生成圖表資料,Filament 推薦你安裝 flowframe/laravel-trend 包,你可以檢視檔案。安裝該包:

composer require flowframe/laravel-trend

更新 getData(),展示過去一年中每個月的治療數量:

use App\Models\Treatment;
use Flowframe\Trend\Trend;
use Flowframe\Trend\TrendValue;

protected function getData(): array
{
    $data = Trend::model(Treatment::class)
        ->between(
            start: now()->subYear(),
            end: now(),
        )
        ->perMonth()
        ->count();

    return [
        'datasets' => [
            [
                'label' => 'Treatments',
                'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
            ],
        ],
        'labels' => $data->map(fn (TrendValue $value) => $value->date),
    ];
}

現在,請檢視儀表盤中新增的 Widget!

你可以自定義儀表盤頁,跳轉網格及展示的 Widget 數。

皮膚的進階內容

恭喜你,你已經學會瞭如何建立一個基礎的 Filament 應用。以下是一些關於進階學習的建議:

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Laravel Filament 中文站站長

相關文章