使用 Filament Panels 快速创建兽医诊所患者管理系统
概述
面板是 Filament 中的顶级容器,它允许你构建功能丰富的管理面板,其中包括页面、资源、表单、表格、通知、Action、信息列表和 Widget。所有面板都有一个默认的仪表盘,其中包含统计、图表、表格等多种 Widget。
前置要求
使用 Filament 之前,你需要先熟悉 Laravel。Filament 是基于许多 Laravel 核心概念构建的,特别是数据库迁移和 Eloquent ORM。如果你此前未曾用过 Laravel,或者需要复习,建议你跟着 Laravel Bootcamp 去创建一个小应用。那个教程涵盖了创建 Laravel 应用的基础知识。
Demo 项目
本教程介绍使用 Filament 为兽医诊所建立一个简单的患者管理系统。它将支持添加新患者(猫、狗或兔子),将他们分配给主人(owners),并记录他们接受的治疗(treatments)。该系统将有一个仪表板,上面有患者类型的统计数据,还有一张显示过去一年治疗次数的图表。
设置数据库和模型
这个项目中需要 3 个模型 - Owner
、Patient
和 Treatment
。使用以下 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.php
的 boot()
方法中添加 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 address
,Phone
字段的标签设置为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
文件。你需要到 PatientResource
的 getRelations()
方法中注册新的关联管理器。
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 应用。以下是一些关于进阶学习的建议:
- 在面板中创建不属于资源的自定义页面。
- 学习如何将 Action 添加到页面和资源中,使用模态框去获取用户输入或确认。
- 了解可用字段以收集用户输入。
- 学习表单布局组件列表,用来搭建直观的表单结构。
- 了解如何在不触碰 CSS 的情况下构建复杂、响应迅速的表格布局。
- 为表格添加总结。
- 使用 helper 方法套件为你的面板编写自动测试。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: