You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Filament仪表板Stats Overview Widget布局样式问题求助

Filament Stats Overview Widget 布局异常问题解决

问题详情

在Filament仪表板中添加Stats Overview Widget时遇到布局样式异常:

  • 自定义仪表板页面配置了响应式列网格:
public function getColumns(): int | string | array
{
    return [
        'sm' => 1,
        'md' => 2,
        'xl' => 3,
    ];
}
  • 每个Stats Overview Widget设置了protected int | string | array $columnSpan = '1';
  • 浏览器检查器显示Widget占据了正确的网格位置,但内部内容布局错乱;临时手动设置grid-column: span 4;后显示正常,刷新后恢复异常
  • 将columnSpan设置为整行时布局正常,但需求是每行放置3个Widget;尝试自定义Blade文件但无法适配Widget的筛选器功能,希望找到无需重构的方案

相关代码

OrderDeliveredWidget 代码

<?php

namespace App\Filament\Widgets;

use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use App\Models\Order;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use Carbon\Carbon;
use Filament\Widgets\StatsOverviewWidget\Card;

class OrderDeliveredWidget extends BaseWidget
{
    use InteractsWithPageFilters;

    protected int | string | array $columnSpan = '1';

    protected function getStats(): array
    {
        $query = Order::query();
        
        if ($this->filters['Date']) {
            $query->where('created_at', '>=', $this->getFilterDate($this->filters['Date']));
        }

        $currentCount = $query->where('status', 'delivered')->count();
        
        // Get the count for the previous period
        $previousPeriodStart = $this->getPreviousPeriodStart($this->filters['Date']);
        $previousCount = Order::where('status', 'delivered')
            ->where('created_at', '>=', $previousPeriodStart)
            ->where('created_at', '<', $this->getFilterDate($this->filters['Date']))
            ->count();

        $percentageChange = $this->calculatePercentageChange($previousCount, $currentCount);
        $description = $this->getDescription($percentageChange);
        $icon = $this->getDescriptionIcon($percentageChange);
        $color = $this->getColor($percentageChange);

        return [
            Stat::make('Delivered Orders', $currentCount)
                ->description($description)
                ->descriptionIcon($icon)
                ->color($color),
            // Add more Stat::make() calls for other metrics
        ];
    }

    private function getFilterDate(?string $filter): Carbon
    {
        return match($filter) {
            'today' => now()->startOfDay(),
            'week' => now()->startOfWeek(),
            'month' => now()->startOfMonth(),
            'year' => now()->startOfYear(),
            default => Carbon::createFromTimestamp(0),
        };
    }

    protected function getFilters(): ?array
    {
        return [
            'today' => 'Today',
            'week' => 'This week',
            'month' => 'This month',
            'year' => 'This year',
        ];
    }

    private function calculatePercentageChange($oldValue, $newValue): float
    {
        if ($oldValue == 0) {
            return $newValue > 0 ? 100 : 0;
        }
        return (($newValue - $oldValue) / $oldValue) * 100;
    }

    private function getDescription(float $percentageChange): string
    {
        $formattedChange = number_format(abs($percentageChange), 1);
        $direction = $percentageChange >= 0 ? 'increase' : 'decrease';
        return "{$formattedChange}% {$direction}";
    }

    private function getDescriptionIcon(float $percentageChange): string
    {
        return $percentageChange >= 0 
            ? 'heroicon-m-arrow-trending-up'
            : 'heroicon-m-arrow-trending-down';
    }

    private function getColor(float $percentageChange): string
    {
        if ($percentageChange > 10) {
            return 'success';
        } elseif ($percentageChange < -10) {
            return 'danger';
        } else {
            return 'warning';
        }
    }

    private function getPreviousPeriodStart(?string $filter): Carbon
    {
        return match($filter) {
            'today' => now()->subDay()->startOfDay(),
            'week' => now()->subWeek()->startOfWeek(),
            'month' => now()->subMonth()->startOfMonth(),
            'year' => now()->subYear()->startOfYear(),
            default => Carbon::createFromTimestamp(0),
        };
    }
}

仪表板筛选器代码

public function filtersForm(Form $form): Form
{
    return $form
        ->schema([
            Section::make()
                ->schema([
                    Select::make('Date')
                        ->options([
                            "all" => "All Time",
                            'today' => "Today",
                            "week" => "Week",
                            "month" => "Month",
                            "year" => "Year"
                        ])
                        ->default('all'),
                ])
                ->columns(1),
        ]);
}

解决方案

方案1:自定义Widget视图修复内部网格

Stats Overview Widget默认内部使用grid-cols-4布局,当Widget仅占页面1列时,内部网格会因空间不足导致布局错乱。通过自定义视图修改内部布局,同时保留筛选器交互:

  1. 在Widget类中添加视图声明:
protected static string $view = 'filament.widgets.order-delivered-widget';
  1. 创建视图文件resources/views/filament/widgets/order-delivered-widget.blade.php,复制Filament默认的Stats Overview Widget视图代码,修改内部网格为单列:
<div class="grid gap-6">
    @foreach($stats as $stat)
        <x-filament-widgets::stats-overview-widget.card
            :color="$stat->getColor()"
            :description="$stat->getDescription()"
            :description-icon="$stat->getDescriptionIcon()"
            :icon="$stat->getIcon()"
            :icon-color="$stat->getIconColor()"
            :label="$stat->getLabel()"
            :value="$stat->getValue()"
            :extra-attributes="$stat->getExtraAttributes()"
        />
    @endforeach
</div>

这里移除了默认的grid-cols-4,改为自适应单列布局,每个Stat卡片会占满Widget宽度,解决挤压问题。

方案2:自定义CSS覆盖内部样式

如果不想修改视图,可通过自定义CSS强制修改Widget内部网格:

.filament-widgets-stats-overview-widget .grid {
    grid-template-columns: 1fr !important;
}

将这段CSS添加到Filament自定义CSS文件(如resources/css/filament.css),并在Filament配置中引入该文件。

方案3:排查筛选器参数冲突

检查页面全局筛选器与Widget筛选器的参数是否冲突:

  • 页面筛选器的Date选项包含all,但Widget的getFilterDate方法未处理all值,会默认返回时间戳0,可能导致数据异常间接影响布局
  • 修改Widget的getFilterDate方法,添加all分支:
private function getFilterDate(?string $filter): Carbon
{
    return match($filter) {
        'today' => now()->startOfDay(),
        'week' => now()->startOfWeek(),
        'month' => now()->startOfMonth(),
        'year' => now()->startOfYear(),
        'all' => Carbon::createFromTimestamp(0), // 新增处理all选项
        default => Carbon::createFromTimestamp(0),
    };
}

同时在getStats方法中调整判断逻辑,避免all时添加无效条件:

if ($this->filters['Date'] && $this->filters['Date'] !== 'all') {
    $query->where('created_at', '>=', $this->getFilterDate($this->filters['Date']));
}

内容的提问来源于stack exchange,提问作者Pierre Sameh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 08:33:09