filament-widgets
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFilamentPHP Widgets Generation Skill
FilamentPHP小部件生成指南
Overview
概述
This skill generates FilamentPHP v4 dashboard widgets including stats overview widgets, chart widgets, table widgets, and custom widgets.
本指南介绍如何生成FilamentPHP v4仪表盘小部件,包括统计概览小部件、图表小部件、表格小部件和自定义小部件。
Documentation Reference
文档参考
CRITICAL: Before generating widgets, read:
/home/mwguerra/projects/mwguerra/claude-code-plugins/filament-specialist/skills/filament-docs/references/widgets/
**重要提示:**生成小部件前,请阅读:
/home/mwguerra/projects/mwguerra/claude-code-plugins/filament-specialist/skills/filament-docs/references/widgets/
Creating Widgets
创建小部件
Generate with Artisan
使用Artisan命令生成
bash
undefinedbash
undefinedBasic widget
Basic widget
php artisan make:filament-widget StatsOverview
php artisan make:filament-widget StatsOverview
Stats overview widget
Stats overview widget
php artisan make:filament-widget StatsOverview --stats-overview
php artisan make:filament-widget StatsOverview --stats-overview
Chart widget
Chart widget
php artisan make:filament-widget RevenueChart --chart
php artisan make:filament-widget RevenueChart --chart
Table widget
Table widget
php artisan make:filament-widget LatestOrders --table
php artisan make:filament-widget LatestOrders --table
Resource widget
Resource widget
php artisan make:filament-widget PostStats --resource=PostResource
undefinedphp artisan make:filament-widget PostStats --resource=PostResource
undefinedStats Overview Widget
统计概览小部件
php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use App\Models\User;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StatsOverview extends BaseWidget
{
protected static ?int $sort = 1;
protected function getStats(): array
{
return [
// Basic stat
Stat::make('Total Users', User::count())
->description('All registered users')
->descriptionIcon('heroicon-o-users')
->color('primary'),
// Stat with trend
Stat::make('New Users', User::whereMonth('created_at', now()->month)->count())
->description('32% increase')
->descriptionIcon('heroicon-m-arrow-trending-up')
->color('success')
->chart([7, 3, 4, 5, 6, 3, 5, 8]) // Sparkline data
->chartColor('success'),
// Stat with decrease trend
Stat::make('Bounce Rate', '21%')
->description('7% decrease')
->descriptionIcon('heroicon-m-arrow-trending-down')
->color('danger'),
// Revenue stat with formatting
Stat::make('Revenue', '$' . number_format(Order::sum('total'), 2))
->description('This month')
->descriptionIcon('heroicon-o-currency-dollar')
->color('success')
->chart([1200, 1400, 1100, 1800, 2200, 1900, 2400])
->chartColor('success'),
// Stat with extra info
Stat::make('Pending Orders', Order::where('status', 'pending')->count())
->description('Requires attention')
->descriptionIcon('heroicon-o-clock')
->color('warning')
->extraAttributes([
'class' => 'cursor-pointer',
'wire:click' => 'goToOrders',
]),
];
}
// Optional: Make stats live
protected static ?string $pollingInterval = '15s';
// Optional: Column span
protected int | string | array $columnSpan = 'full';
}php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use App\Models\User;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StatsOverview extends BaseWidget
{
protected static ?int $sort = 1;
protected function getStats(): array
{
return [
// Basic stat
Stat::make('Total Users', User::count())
->description('All registered users')
->descriptionIcon('heroicon-o-users')
->color('primary'),
// Stat with trend
Stat::make('New Users', User::whereMonth('created_at', now()->month)->count())
->description('32% increase')
->descriptionIcon('heroicon-m-arrow-trending-up')
->color('success')
->chart([7, 3, 4, 5, 6, 3, 5, 8]) // Sparkline data
->chartColor('success'),
// Stat with decrease trend
Stat::make('Bounce Rate', '21%')
->description('7% decrease')
->descriptionIcon('heroicon-m-arrow-trending-down')
->color('danger'),
// Revenue stat with formatting
Stat::make('Revenue', '$' . number_format(Order::sum('total'), 2))
->description('This month')
->descriptionIcon('heroicon-o-currency-dollar')
->color('success')
->chart([1200, 1400, 1100, 1800, 2200, 1900, 2400])
->chartColor('success'),
// Stat with extra info
Stat::make('Pending Orders', Order::where('status', 'pending')->count())
->description('Requires attention')
->descriptionIcon('heroicon-o-clock')
->color('warning')
->extraAttributes([
'class' => 'cursor-pointer',
'wire:click' => 'goToOrders',
]),
];
}
// Optional: Make stats live
protected static ?string $pollingInterval = '15s';
// Optional: Column span
protected int | string | array $columnSpan = 'full';
}Chart Widgets
图表小部件
Line Chart
折线图
php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Carbon;
class RevenueChart extends ChartWidget
{
protected static ?string $heading = 'Revenue';
protected static ?int $sort = 2;
protected int | string | array $columnSpan = 'full';
protected function getData(): array
{
$data = collect(range(1, 12))->map(function ($month) {
return Order::whereMonth('created_at', $month)
->whereYear('created_at', now()->year)
->sum('total');
});
return [
'datasets' => [
[
'label' => 'Revenue',
'data' => $data->values()->toArray(),
'borderColor' => '#10b981',
'backgroundColor' => 'rgba(16, 185, 129, 0.1)',
'fill' => true,
],
],
'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
];
}
protected function getType(): string
{
return 'line';
}
protected function getOptions(): array
{
return [
'plugins' => [
'legend' => [
'display' => false,
],
],
'scales' => [
'y' => [
'beginAtZero' => true,
'ticks' => [
'callback' => '(value) => "$" + value.toLocaleString()',
],
],
],
];
}
}php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Widgets\ChartWidget;
use Illuminate\Support\Carbon;
class RevenueChart extends ChartWidget
{
protected static ?string $heading = 'Revenue';
protected static ?int $sort = 2;
protected int | string | array $columnSpan = 'full';
protected function getData(): array
{
$data = collect(range(1, 12))->map(function ($month) {
return Order::whereMonth('created_at', $month)
->whereYear('created_at', now()->year)
->sum('total');
});
return [
'datasets' => [
[
'label' => 'Revenue',
'data' => $data->values()->toArray(),
'borderColor' => '#10b981',
'backgroundColor' => 'rgba(16, 185, 129, 0.1)',
'fill' => true,
],
],
'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
];
}
protected function getType(): string
{
return 'line';
}
protected function getOptions(): array
{
return [
'plugins' => [
'legend' => [
'display' => false,
],
],
'scales' => [
'y' => [
'beginAtZero' => true,
'ticks' => [
'callback' => '(value) => "$" + value.toLocaleString()',
],
],
],
];
}
}Bar Chart
柱状图
php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Widgets\ChartWidget;
class OrdersPerCategory extends ChartWidget
{
protected static ?string $heading = 'Orders by Category';
protected static ?int $sort = 3;
protected function getData(): array
{
$categories = \App\Models\Category::withCount('orders')->get();
return [
'datasets' => [
[
'label' => 'Orders',
'data' => $categories->pluck('orders_count')->toArray(),
'backgroundColor' => [
'#3b82f6',
'#10b981',
'#f59e0b',
'#ef4444',
'#8b5cf6',
],
],
],
'labels' => $categories->pluck('name')->toArray(),
];
}
protected function getType(): string
{
return 'bar';
}
}php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Widgets\ChartWidget;
class OrdersPerCategory extends ChartWidget
{
protected static ?string $heading = 'Orders by Category';
protected static ?int $sort = 3;
protected function getData(): array
{
$categories = \App\Models\Category::withCount('orders')->get();
return [
'datasets' => [
[
'label' => 'Orders',
'data' => $categories->pluck('orders_count')->toArray(),
'backgroundColor' => [
'#3b82f6',
'#10b981',
'#f59e0b',
'#ef4444',
'#8b5cf6',
],
],
],
'labels' => $categories->pluck('name')->toArray(),
];
}
protected function getType(): string
{
return 'bar';
}
}Doughnut/Pie Chart
环形图/饼图
php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Widgets\ChartWidget;
class OrderStatusChart extends ChartWidget
{
protected static ?string $heading = 'Order Status Distribution';
protected static ?int $sort = 4;
protected function getData(): array
{
$statuses = Order::selectRaw('status, COUNT(*) as count')
->groupBy('status')
->pluck('count', 'status');
return [
'datasets' => [
[
'data' => $statuses->values()->toArray(),
'backgroundColor' => [
'#f59e0b', // pending - warning
'#3b82f6', // processing - primary
'#10b981', // completed - success
'#ef4444', // cancelled - danger
],
],
],
'labels' => $statuses->keys()->map(fn ($s) => ucfirst($s))->toArray(),
];
}
protected function getType(): string
{
return 'doughnut'; // or 'pie'
}
protected function getOptions(): array
{
return [
'plugins' => [
'legend' => [
'position' => 'bottom',
],
],
];
}
}php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Widgets\ChartWidget;
class OrderStatusChart extends ChartWidget
{
protected static ?string $heading = 'Order Status Distribution';
protected static ?int $sort = 4;
protected function getData(): array
{
$statuses = Order::selectRaw('status, COUNT(*) as count')
->groupBy('status')
->pluck('count', 'status');
return [
'datasets' => [
[
'data' => $statuses->values()->toArray(),
'backgroundColor' => [
'#f59e0b', // pending - warning
'#3b82f6', // processing - primary
'#10b981', // completed - success
'#ef4444', // cancelled - danger
],
],
],
'labels' => $statuses->keys()->map(fn ($s) => ucfirst($s))->toArray(),
];
}
protected function getType(): string
{
return 'doughnut'; // or 'pie'
}
protected function getOptions(): array
{
return [
'plugins' => [
'legend' => [
'position' => 'bottom',
],
],
];
}
}Interactive Chart with Filters
带筛选器的交互式图表
php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Widgets\ChartWidget;
class FilterableRevenueChart extends ChartWidget
{
protected static ?string $heading = 'Revenue Over Time';
public ?string $filter = 'week';
protected function getFilters(): ?array
{
return [
'today' => 'Today',
'week' => 'Last 7 days',
'month' => 'This month',
'year' => 'This year',
];
}
protected function getData(): array
{
$data = match ($this->filter) {
'today' => $this->getTodayData(),
'week' => $this->getWeekData(),
'month' => $this->getMonthData(),
'year' => $this->getYearData(),
};
return [
'datasets' => [
[
'label' => 'Revenue',
'data' => $data['values'],
'borderColor' => '#3b82f6',
],
],
'labels' => $data['labels'],
];
}
protected function getType(): string
{
return 'line';
}
private function getTodayData(): array
{
// Implementation
}
private function getWeekData(): array
{
// Implementation
}
private function getMonthData(): array
{
// Implementation
}
private function getYearData(): array
{
// Implementation
}
}php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
use Filament\Widgets\ChartWidget;
class FilterableRevenueChart extends ChartWidget
{
protected static ?string $heading = 'Revenue Over Time';
public ?string $filter = 'week';
protected function getFilters(): ?array
{
return [
'today' => 'Today',
'week' => 'Last 7 days',
'month' => 'This month',
'year' => 'This year',
];
}
protected function getData(): array
{
$data = match ($this->filter) {
'today' => $this->getTodayData(),
'week' => $this->getWeekData(),
'month' => $this->getMonthData(),
'year' => $this->getYearData(),
};
return [
'datasets' => [
[
'label' => 'Revenue',
'data' => $data['values'],
'borderColor' => '#3b82f6',
],
],
'labels' => $data['labels'],
];
}
protected function getType(): string
{
return 'line';
}
private function getTodayData(): array
{
// Implementation
}
private function getWeekData(): array
{
// Implementation
}
private function getMonthData(): array
{
// Implementation
}
private function getYearData(): array
{
// Implementation
}
}Table Widget
表格小部件
php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class LatestOrders extends BaseWidget
{
protected static ?string $heading = 'Latest Orders';
protected static ?int $sort = 5;
protected int | string | array $columnSpan = 'full';
public function table(Table $table): Table
{
return $table
->query(
Order::query()
->latest()
->limit(10)
)
->columns([
Tables\Columns\TextColumn::make('number')
->searchable(),
Tables\Columns\TextColumn::make('customer.name')
->label('Customer')
->searchable(),
Tables\Columns\BadgeColumn::make('status')
->colors([
'warning' => 'pending',
'primary' => 'processing',
'success' => 'completed',
'danger' => 'cancelled',
]),
Tables\Columns\TextColumn::make('total')
->money('usd')
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable(),
])
->actions([
Tables\Actions\Action::make('view')
->url(fn (Order $record): string => route('filament.admin.resources.orders.view', $record))
->icon('heroicon-o-eye'),
])
->paginated(false);
}
}php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class LatestOrders extends BaseWidget
{
protected static ?string $heading = 'Latest Orders';
protected static ?int $sort = 5;
protected int | string | array $columnSpan = 'full';
public function table(Table $table): Table
{
return $table
->query(
Order::query()
->latest()
->limit(10)
)
->columns([
Tables\Columns\TextColumn::make('number')
->searchable(),
Tables\Columns\TextColumn::make('customer.name')
->label('Customer')
->searchable(),
Tables\Columns\BadgeColumn::make('status')
->colors([
'warning' => 'pending',
'primary' => 'processing',
'success' => 'completed',
'danger' => 'cancelled',
]),
Tables\Columns\TextColumn::make('total')
->money('usd')
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable(),
])
->actions([
Tables\Actions\Action::make('view')
->url(fn (Order $record): string => route('filament.admin.resources.orders.view', $record))
->icon('heroicon-o-eye'),
])
->paginated(false);
}
}Custom Widget
自定义小部件
php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Task;
use Filament\Widgets\Widget;
class TasksWidget extends Widget
{
protected static string $view = 'filament.widgets.tasks-widget';
protected static ?int $sort = 6;
protected int | string | array $columnSpan = 1;
public array $tasks = [];
public function mount(): void
{
$this->tasks = Task::where('user_id', auth()->id())
->whereNull('completed_at')
->orderBy('due_date')
->limit(5)
->get()
->toArray();
}
public function completeTask(int $taskId): void
{
Task::find($taskId)->update(['completed_at' => now()]);
$this->mount(); // Refresh tasks
}
}Blade view ():
resources/views/filament/widgets/tasks-widget.blade.phpblade
<x-filament-widgets::widget>
<x-filament::section>
<x-slot name="heading">
My Tasks
</x-slot>
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse ($tasks as $task)
<li class="py-3 flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">
{{ $task['title'] }}
</p>
<p class="text-xs text-gray-500">
Due: {{ \Carbon\Carbon::parse($task['due_date'])->format('M j, Y') }}
</p>
</div>
<x-filament::icon-button
icon="heroicon-o-check"
wire:click="completeTask({{ $task['id'] }})"
color="success"
/>
</li>
@empty
<li class="py-3 text-sm text-gray-500">
No pending tasks
</li>
@endforelse
</ul>
</x-filament::section>
</x-filament-widgets::widget>php
<?php
declare(strict_types=1);
namespace App\Filament\Widgets;
use App\Models\Task;
use Filament\Widgets\Widget;
class TasksWidget extends Widget
{
protected static string $view = 'filament.widgets.tasks-widget';
protected static ?int $sort = 6;
protected int | string | array $columnSpan = 1;
public array $tasks = [];
public function mount(): void
{
$this->tasks = Task::where('user_id', auth()->id())
->whereNull('completed_at')
->orderBy('due_date')
->limit(5)
->get()
->toArray();
}
public function completeTask(int $taskId): void
{
Task::find($taskId)->update(['completed_at' => now()]);
$this->mount(); // Refresh tasks
}
}Blade视图 ():
resources/views/filament/widgets/tasks-widget.blade.phpblade
<x-filament-widgets::widget>
<x-filament::section>
<x-slot name="heading">
My Tasks
</x-slot>
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
@forelse ($tasks as $task)
<li class="py-3 flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">
{{ $task['title'] }}
</p>
<p class="text-xs text-gray-500">
Due: {{ \Carbon\Carbon::parse($task['due_date'])->format('M j, Y') }}
</p>
</div>
<x-filament::icon-button
icon="heroicon-o-check"
wire:click="completeTask({{ $task['id'] }})"
color="success"
/>
</li>
@empty
<li class="py-3 text-sm text-gray-500">
No pending tasks
</li>
@endforelse
</ul>
</x-filament::section>
</x-filament-widgets::widget>Widget Registration
小部件注册
Dashboard Widgets
仪表盘小部件
php
// In AdminPanelProvider.php
->widgets([
Widgets\AccountWidget::class, // Default
Widgets\FilamentInfoWidget::class, // Default
\App\Filament\Widgets\StatsOverview::class,
\App\Filament\Widgets\RevenueChart::class,
\App\Filament\Widgets\LatestOrders::class,
])php
// In AdminPanelProvider.php
->widgets([
Widgets\AccountWidget::class, // Default
Widgets\FilamentInfoWidget::class, // Default
\App\Filament\Widgets\StatsOverview::class,
\App\Filament\Widgets\RevenueChart::class,
\App\Filament\Widgets\LatestOrders::class,
])Resource Page Widgets
资源页面小部件
php
// In resource class
public static function getWidgets(): array
{
return [
Widgets\PostStatsOverview::class,
];
}
// In ListRecords page
protected function getHeaderWidgets(): array
{
return [
Widgets\PostStatsOverview::class,
];
}
protected function getFooterWidgets(): array
{
return [
Widgets\RecentPosts::class,
];
}php
// In resource class
public static function getWidgets(): array
{
return [
Widgets\PostStatsOverview::class,
];
}
// In ListRecords page
protected function getHeaderWidgets(): array
{
return [
Widgets\PostStatsOverview::class,
];
}
protected function getFooterWidgets(): array
{
return [
Widgets\RecentPosts::class,
];
}Widget Configuration
小部件配置
php
class MyWidget extends Widget
{
// Sort order
protected static ?int $sort = 1;
// Column span (1, 2, 'full', or responsive array)
protected int | string | array $columnSpan = [
'md' => 2,
'xl' => 3,
];
// Polling interval
protected static ?string $pollingInterval = '10s';
// Visibility
public static function canView(): bool
{
return auth()->user()->isAdmin();
}
// Lazy loading
protected static bool $isLazy = true;
}php
class MyWidget extends Widget
{
// Sort order
protected static ?int $sort = 1;
// Column span (1, 2, 'full', or responsive array)
protected int | string | array $columnSpan = [
'md' => 2,
'xl' => 3,
];
// Polling interval
protected static ?string $pollingInterval = '10s';
// Visibility
public static function canView(): bool
{
return auth()->user()->isAdmin();
}
// Lazy loading
protected static bool $isLazy = true;
}Output
输出结果
Generated widgets include:
- Proper widget type selection
- Data fetching methods
- Chart configuration
- Styling and layout
- Interactivity (filters, actions)
- Performance optimizations
生成的小部件包含:
- 正确的小部件类型选择
- 数据获取方法
- 图表配置
- 样式与布局
- 交互功能(筛选器、操作)
- 性能优化