Skip to Content

Админ-панель Flute CMS

Flute CMS предоставляет мощную и гибкую систему для создания административных интерфейсов. Система построена на основе пакетов (Packages), экранов (Screens) и лейаутов (Layouts), что позволяет быстро создавать сложные CRUD-интерфейсы с минимальным количеством кода.

Архитектура

Админ-панель состоит из нескольких ключевых компонентов:

  • AdminPanel — главный класс, управляющий инициализацией и регистрацией пакетов
  • AdminPackageFactory — фабрика для загрузки и инициализации пакетов
  • AbstractAdminPackage — базовый класс для создания пакетов
  • Screen — базовый класс для создания экранов
  • LayoutFactory — фабрика для создания лейаутов
  • Field — базовый класс для полей форм
┌─────────────────────────────────────────────────────────┐ │ AdminPanel │ │ ┌─────────────────────────────────────────────────┐ │ │ │ AdminPackageFactory │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ Package │ │ Package │ │ Package │ ... │ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ │ │ │ │ Screen │ │ Screen │ │ Screen │ ... │ │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ │ │ │ │ ┌────▼────────────▼──────────▼────┐ │ │ │ │ │ Layouts │ │ │ │ │ │ (Table, Rows, Tabs, Modal...) │ │ │ │ │ └─────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘

Создание админ-пакета

Структура пакета

    • MyModuleAdminPackage.php
      • ItemListScreen.php
      • ItemEditScreen.php
    • routes.php

Базовый пакет

Все админ-пакеты наследуются от AbstractAdminPackage:

<?php namespace Flute\Modules\MyModule\Admin; use Flute\Admin\Support\AbstractAdminPackage; class MyModuleAdminPackage extends AbstractAdminPackage { public function initialize(): void { parent::initialize(); // Загрузка маршрутов из файла $this->loadRoutesFromFile('routes.php'); // Регистрация представлений с namespace $this->loadViews('Resources/views', 'admin-mymodule'); // Загрузка переводов $this->loadTranslations('Resources/lang'); // Регистрация SCSS для админки $this->registerScss('Resources/assets/scss/admin.scss'); } public function getPermissions(): array { return ['admin', 'admin.mymodule']; } public function getMenuItems(): array { return [ [ 'type' => 'header', 'title' => __('admin-mymodule.menu.header'), ], [ 'title' => __('admin-mymodule.menu.items'), 'icon' => 'ph.bold.list-bold', 'url' => url('/admin/mymodule'), 'permission' => 'admin.mymodule', ], ]; } public function getPriority(): int { return 50; // Чем меньше, тем выше в списке } }

Методы AbstractAdminPackage

МетодОписание
initialize()Инициализация пакета: регистрация маршрутов, видов, переводов
boot()Дополнительная логика после инициализации всех пакетов
getPermissions()Массив разрешений для доступа к пакету
getMenuItems()Массив элементов меню
getPriority()Приоритет пакета (порядок загрузки и отображения в меню)
loadRoutesFromFile(string $file)Загрузка маршрутов из файла
loadRoutes(array $routes)Программная загрузка маршрутов
loadViews(string $path, string $namespace)Регистрация представлений
loadTranslations(string $langDir)Загрузка переводов
registerScss(string $path)Регистрация SCSS файла для админки
getBasePath()Получение базового пути пакета

Конфигурация меню

Элементы меню поддерживают следующие параметры:

public function getMenuItems(): array { return [ // Заголовок раздела [ 'type' => 'header', 'title' => 'Раздел меню', ], // Простой элемент [ 'title' => 'Пункт меню', 'icon' => 'ph.bold.house-bold', 'url' => url('/admin/path'), 'permission' => 'admin.permission', ], // Элемент с вложенными пунктами [ 'title' => 'Родительский пункт', 'icon' => 'ph.bold.folder-bold', 'permission' => 'admin.parent', 'children' => [ [ 'title' => 'Дочерний пункт 1', 'icon' => 'ph.regular.file', 'url' => url('/admin/parent/child1'), ], [ 'title' => 'Дочерний пункт 2', 'icon' => 'ph.regular.file', 'url' => url('/admin/parent/child2'), ], ], ], // Элемент с режимом проверки разрешений [ 'title' => 'Специальный пункт', 'icon' => 'ph.bold.shield-bold', 'url' => url('/admin/special'), 'permission' => ['admin.perm1', 'admin.perm2'], 'permission_mode' => 'any', // 'all' (по умолчанию) или 'any' ], ]; }

Иконки используют формат Phosphor Icons: ph.{weight}.{name}, где weight — regular, bold, fill, duotone, thin, light.

Маршрутизация

Файл routes.php

<?php use Flute\Core\Router\Router; use Flute\Modules\MyModule\Admin\Screens\ItemListScreen; use Flute\Modules\MyModule\Admin\Screens\ItemEditScreen; // Регистрация экранов Router::screen('/admin/mymodule', ItemListScreen::class); Router::screen('/admin/mymodule/{id}/edit', ItemEditScreen::class);

Макрос Router::screen() автоматически:

  • Добавляет middleware can:admin
  • Оборачивает компонент в layout admin::layouts.screen
  • Поддерживает HTMX/Yoyo запросы

Программная регистрация маршрутов

public function initialize(): void { // Прямая регистрация экрана router()->screen('/admin/mymodule', ItemListScreen::class); // Или через loadRoutes $this->loadRoutes([ [ 'method' => 'GET', 'uri' => '/admin/mymodule/api/items', 'action' => [ItemController::class, 'list'], ], ]); }

Экраны (Screens)

Экраны — основные строительные блоки интерфейса админ-панели. Каждый экран представляет собой Yoyo-компонент с поддержкой реактивности.

Базовый экран

<?php namespace Flute\Modules\MyModule\Admin\Screens; use Flute\Admin\Platform\Screen; use Flute\Admin\Platform\Layouts\LayoutFactory; class ItemListScreen extends Screen { public ?string $name = 'admin-mymodule.screen.list.title'; public ?string $description = 'admin-mymodule.screen.list.description'; public ?string $permission = 'admin.mymodule'; public $items; public function mount(): void { breadcrumb() ->add(__('def.admin_panel'), url('/admin')) ->add(__('admin-mymodule.breadcrumbs.items')); $this->items = rep(Item::class)->select()->orderBy('id', 'DESC'); } public function layout(): array { return [ // Лейауты экрана ]; } public function commandBar(): array { return [ // Кнопки в шапке экрана ]; } }

Свойства экрана

СвойствоТипОписание
$name?stringЗаголовок экрана (поддерживает ключи переводов)
$description?stringОписание экрана
$permission?string|arrayТребуемые разрешения
$jsarrayJavaScript файлы для загрузки
$cssarrayCSS файлы для загрузки

Методы жизненного цикла

МетодОписание
mount()Инициализация данных экрана (вызывается при первом рендере)
layout()Возвращает массив лейаутов для отображения
commandBar()Возвращает массив действий в шапке экрана
render()Рендеринг экрана (обычно не переопределяется)

Методы для работы с данными

class ItemEditScreen extends Screen { public $item; public function mount(): void { $id = request()->route('id'); $this->item = Item::findByPK($id); if (!$this->item) { $this->flashMessage(__('admin-mymodule.messages.not_found'), 'error'); $this->redirectTo('/admin/mymodule'); } } // Метод-обработчик действия public function save(): void { $data = request()->input(); $validation = $this->validate([ 'title' => ['required', 'string', 'max-str-len:255'], 'content' => ['required', 'string'], ], $data); if (!$validation) { return; // Ошибки валидации автоматически отображаются } $this->item->title = $data['title']; $this->item->content = $data['content']; $this->item->save(); $this->flashMessage(__('admin-mymodule.messages.saved'), 'success'); } // Метод удаления public function delete(): void { $id = request()->input('id'); $item = Item::findByPK($id); if ($item) { $item->delete(); $this->flashMessage(__('admin-mymodule.messages.deleted'), 'success'); } } // Массовое удаление public function bulkDelete(): void { $ids = request()->input('selected', []); foreach ($ids as $id) { $item = Item::findByPK((int) $id); if ($item) { $item->delete(); } } $this->flashMessage(__('admin-mymodule.messages.bulk_deleted'), 'success'); } }

Модальные окна

Экраны поддерживают модальные окна через специальные методы:

class ItemListScreen extends Screen { public function commandBar(): array { return [ Button::make(__('admin-mymodule.buttons.create')) ->icon('ph.bold.plus-bold') ->modal('createModal') ->type(Color::PRIMARY), ]; } // Метод модального окна public function createModal(Repository $parameters) { return LayoutFactory::modal($parameters, [ LayoutFactory::field( Input::make('title') ->type('text') ->placeholder(__('admin-mymodule.fields.title.placeholder')) ) ->label(__('admin-mymodule.fields.title.label')) ->required(), LayoutFactory::field( TextArea::make('content') ->rows(5) ) ->label(__('admin-mymodule.fields.content.label')), ]) ->title(__('admin-mymodule.modal.create.title')) ->applyButton(__('admin-mymodule.modal.create.submit')) ->method('saveItem'); } // Модальное окно с параметрами public function editModal(Repository $parameters) { $itemId = $parameters->get('id'); $item = Item::findByPK($itemId); if (!$item) { $this->flashMessage(__('admin-mymodule.messages.not_found'), 'error'); return; } return LayoutFactory::modal($parameters, [ LayoutFactory::field( Input::make('title') ->value($item->title) ) ->label(__('admin-mymodule.fields.title.label')) ->required(), ]) ->title(__('admin-mymodule.modal.edit.title')) ->applyButton(__('admin-mymodule.modal.edit.submit')) ->method('updateItem'); } public function saveItem(): void { // Логика сохранения $this->closeModal(); $this->flashMessage(__('admin-mymodule.messages.created'), 'success'); } public function updateItem(): void { $itemId = $this->modalParams->get('id'); // Логика обновления $this->closeModal(); $this->flashMessage(__('admin-mymodule.messages.updated'), 'success'); } }

Лейауты (Layouts)

Лейауты определяют структуру и отображение контента на экране. Все лейауты создаются через LayoutFactory.

Доступные лейауты

ЛейаутОписание
table()Таблица с данными, пагинацией, сортировкой и поиском
rows()Вертикальная группа полей формы
columns()Горизонтальное расположение элементов
split()Разделение на две колонки
tabs()Табы с вкладками
modal()Модальное окно
block()Блок-обертка
view()Произвольный Blade-шаблон
field()Одиночное поле с label
metrics()Блок метрик
chart()График
sortable()Сортируемый список
blank()Пустой контейнер
wrapper()Кастомная обертка

Table — Таблица

use Flute\Admin\Platform\Fields\TD; use Flute\Admin\Platform\Actions\Button; use Flute\Admin\Platform\Actions\DropDown; use Flute\Admin\Platform\Actions\DropDownItem; use Flute\Admin\Platform\Support\Color; public function layout(): array { return [ LayoutFactory::table('items', [ // Колонка выбора (чекбоксы) TD::selection('id'), // Обычная колонка TD::make('title', __('admin-mymodule.table.title')) ->sort() ->cantHide() ->minWidth('200px'), // Колонка с кастомным рендером TD::make('status', __('admin-mymodule.table.status')) ->render(fn (Item $item) => view('admin-mymodule::cells.status', compact('item'))) ->width('120px') ->alignCenter(), // Колонка с датой TD::make('createdAt', __('admin-mymodule.table.created_at')) ->asComponent(\Flute\Admin\Platform\Components\Cells\DateTime::class) ->sort() ->defaultSort(true, 'desc') ->width('150px'), // Колонка действий TD::make('actions', __('admin-mymodule.table.actions')) ->class('actions-col') ->alignCenter() ->cantHide() ->disableSearch() ->width('100px') ->render(fn (Item $item) => DropDown::make() ->icon('ph.regular.dots-three-outline-vertical') ->list([ DropDownItem::make(__('admin-mymodule.buttons.edit')) ->redirect(url('/admin/mymodule/' . $item->id . '/edit')) ->icon('ph.bold.pencil-bold') ->type(Color::OUTLINE_PRIMARY) ->size('small') ->fullWidth(), DropDownItem::make(__('admin-mymodule.buttons.delete')) ->confirm(__('admin-mymodule.confirms.delete')) ->method('delete', ['id' => $item->id]) ->icon('ph.bold.trash-bold') ->type(Color::OUTLINE_DANGER) ->size('small') ->fullWidth(), ])), ]) ->title(__('admin-mymodule.table.title')) ->description(__('admin-mymodule.table.description')) ->searchable(['title', 'description']) ->perPage(15) ->compact() ->commands([ Button::make(__('admin-mymodule.buttons.create')) ->icon('ph.bold.plus-bold') ->redirect(url('/admin/mymodule/create')), ]) ->bulkActions([ Button::make(__('admin.bulk.delete_selected')) ->icon('ph.bold.trash-bold') ->type(Color::OUTLINE_DANGER) ->confirm(__('admin.confirms.delete_selected')) ->method('bulkDelete'), ]), ]; }

Методы TD (Table Data)

МетодОписание
make(string $name, ?string $title)Создание колонки
selection(string $name)Колонка с чекбоксами
render(callable $callback)Кастомный рендер
asComponent(string $class)Рендер через компонент
sort(bool $enabled)Включить сортировку
defaultSort(bool $enabled, string $direction)Сортировка по умолчанию
searchable(bool $enabled)Включить поиск по колонке
disableSearch()Отключить поиск
width(string $width)Ширина колонки
minWidth(string $width)Минимальная ширина
align(string $align)Выравнивание: start, center, end
alignCenter() / alignRight()Быстрое выравнивание
cantHide()Запретить скрытие колонки
defaultHidden(bool $hidden)Скрыта по умолчанию
class(string $class)CSS класс
style(string $style)Inline стили

Методы Table

МетодОписание
searchable(?array $columns)Включить поиск
setSearchableColumns(array $columns)Установить колонки для поиска
perPage(int $count)Записей на страницу
title(?string $title)Заголовок таблицы
description(?string $description)Описание
compact(bool $compact)Компактный режим
commands(array $actions)Кнопки над таблицей
bulkActions(array $actions)Массовые действия
prepareContent(callable $callback)Обработка строк
dataCallback(callable $callback)Обработка всего датасета

Rows — Группа полей

public function layout(): array { return [ LayoutFactory::rows([ Input::make('title') ->type('text') ->placeholder(__('admin-mymodule.fields.title.placeholder')) ->required(), TextArea::make('description') ->rows(3), Select::make('category_id') ->options($this->categories) ->empty(__('admin-mymodule.fields.category.empty')), Toggle::make('is_active') ->label(__('admin-mymodule.fields.is_active.label')) ->checked($this->item?->is_active ?? true), ]) ->title(__('admin-mymodule.sections.main')) ->description(__('admin-mymodule.sections.main_description')), ]; }

Columns — Колонки

public function layout(): array { return [ LayoutFactory::columns([ LayoutFactory::rows([ Input::make('title')->type('text'), TextArea::make('content'), ])->title(__('admin-mymodule.sections.content')), LayoutFactory::rows([ Select::make('status')->options($this->statuses), Input::make('published_at')->type('datetime-local'), ])->title(__('admin-mymodule.sections.settings')), ]), ]; }

Tabs — Вкладки

use Flute\Admin\Platform\Fields\Tab; public function layout(): array { return [ LayoutFactory::tabs([ Tab::make(__('admin-mymodule.tabs.general')) ->badge($this->items->count()) ->icon('ph.regular.info') ->layouts([ LayoutFactory::rows([ Input::make('title'), TextArea::make('description'), ]), ]), Tab::make(__('admin-mymodule.tabs.seo')) ->icon('ph.regular.magnifying-glass') ->layouts([ LayoutFactory::rows([ Input::make('meta_title'), TextArea::make('meta_description'), ]), ]), Tab::make(__('admin-mymodule.tabs.advanced')) ->layouts([ LayoutFactory::view('admin-mymodule::partials.advanced', [ 'item' => $this->item, ]), ]), ]) ->slug('item_tabs') ->pills() // Стиль pills вместо tabs ->sticky() // Прилипающие табы ->lazyload() // Ленивая загрузка контента ->morph(false), // Отключить анимацию ]; }
public function editModal(Repository $parameters) { return LayoutFactory::modal($parameters, [ LayoutFactory::field( Input::make('title')->value($parameters->get('title')) )->label(__('admin-mymodule.fields.title.label'))->required(), LayoutFactory::field( Select::make('status') ->options(['draft' => 'Черновик', 'published' => 'Опубликовано']) ->value($parameters->get('status')) )->label(__('admin-mymodule.fields.status.label')), ]) ->title(__('admin-mymodule.modal.edit.title')) ->applyButton(__('def.save')) ->closeButton(__('def.cancel')) ->method('updateItem') ->size(Modal::SIZE_LG) // 'sm', 'lg', 'xl' ->right() // Открытие справа ->withoutApplyButton() // Без кнопки применения ->withoutCloseButton() // Без кнопки закрытия ->removeOnClose(); // Удалять DOM при закрытии }

Metrics — Метрики

public function layout(): array { return [ LayoutFactory::metrics([ __('admin-mymodule.metrics.total') => 'totalItems', __('admin-mymodule.metrics.active') => 'activeItems', __('admin-mymodule.metrics.views') => 'totalViews', ]) ->title(__('admin-mymodule.metrics.title')) ->setIcons([ __('admin-mymodule.metrics.total') => 'ph.regular.list', __('admin-mymodule.metrics.active') => 'ph.regular.check-circle', __('admin-mymodule.metrics.views') => 'ph.regular.eye', ]), ]; } public function mount(): void { $this->totalItems = Item::query()->count(); $this->activeItems = Item::query()->where('is_active', true)->count(); $this->totalViews = Item::query()->sum('views'); }

Chart — Графики

use Flute\Core\Charts\FluteChart; public function layout(): array { return [ LayoutFactory::chart('viewsChart', __('admin-mymodule.charts.views')) ->type('line') // 'line', 'bar', 'donut', 'area' ->height(300) ->colors(['#4F46E5', '#10B981']) ->labels($this->chartLabels) ->dataset($this->chartData), // Или из готового объекта FluteChart LayoutFactory::chart('salesChart') ->from($this->salesChart) ->title(__('admin-mymodule.charts.sales')) ->description(__('admin-mymodule.charts.sales_description')), ]; } public function mount(): void { $this->chartLabels = ['Янв', 'Фев', 'Мар', 'Апр', 'Май']; $this->chartData = [ ['name' => 'Просмотры', 'data' => [100, 150, 200, 180, 250]], ['name' => 'Уникальные', 'data' => [50, 80, 120, 100, 150]], ]; $this->salesChart = (new FluteChart()) ->setType('bar') ->setLabels(['Q1', 'Q2', 'Q3', 'Q4']) ->setDataset([1200, 1500, 1800, 2100]); }

Sortable — Сортируемый список

use Flute\Admin\Platform\Fields\Sight; public function layout(): array { return [ LayoutFactory::sortable('items', [ Sight::make()->render(fn (Item $item) => view('admin-mymodule::cells.item', compact('item'))), Sight::make('actions', __('admin-mymodule.table.actions')) ->render(fn (Item $item) => DropDown::make() ->icon('ph.regular.dots-three-outline-vertical') ->list([ DropDownItem::make(__('def.edit')) ->modal('editModal', ['id' => $item->id]), DropDownItem::make(__('def.delete')) ->confirm(__('admin.confirms.delete')) ->method('delete', ['id' => $item->id]), ])), ])->onSortEnd('saveSortOrder'), ]; } public function saveSortOrder(): void { $result = json_decode(request()->input('sortableResult', '[]'), true); foreach (array_reverse($result) as $index => $itemData) { $item = Item::findByPK((int) $itemData['id']); if ($item) { $item->position = $index; $item->save(); } } orm()->getHeap()->clean(); $this->loadItems(); }

View — Кастомный шаблон

public function layout(): array { return [ LayoutFactory::view('admin-mymodule::partials.custom-section', [ 'items' => $this->items, 'stats' => $this->stats, ]), ]; }

Поля форм (Fields)

Input — Текстовое поле

Input::make('title') ->type('text') // text, email, password, number, date, datetime-local, color, icon, file ->name('custom_name') ->value('Default value') ->placeholder('Введите название') ->required() ->disabled() ->readOnly() ->mask('99.99.9999') // Маска ввода ->prefix('$') // Префикс ->postPrefix('/month') // Постфикс ->size('medium') // small, medium, large ->tooltip('Подсказка') ->datalist(['Option 1', 'Option 2']) // Список подсказок ->popover('Дополнительная информация'); // Файловое поле с FilePond Input::make('image') ->type('file') ->filePond() ->filePondOptions([ 'acceptedFileTypes' => ['image/*'], 'maxFileSize' => '2MB', ]) ->defaultFile($this->item?->image) ->multiple(); // Поле выбора иконки Input::make('icon') ->type('icon') ->iconPacks(['phosphor', 'material']); // Поле выбора цвета Input::make('color') ->type('color') ->value('#4F46E5');

Select — Выпадающий список

// Простой select Select::make('category_id') ->options([ 1 => 'Категория 1', 2 => 'Категория 2', 3 => 'Категория 3', ]) ->value($this->item?->category_id) ->empty('Выберите категорию', '') ->required(); // Multiple select Select::make('tags') ->options($this->allTags) ->value($this->item?->tags?->pluck('id')->toArray()) ->multiple() ->maxItems(5) ->removeButton() ->clearButton(); // Select с поиском по базе данных Select::make('user_id') ->fromDatabase('users', 'name', 'id', ['name', 'email']) ->searchable(true, 2, 300) // minLength, delay ->preload() ->limit(20); // Select из Enum Select::make('status') ->fromEnum(ItemStatus::class, 'label') ->value($this->item?->status); // Select с возможностью добавления Select::make('tags') ->options($this->tags) ->multiple() ->allowAdd() ->taggable();

TextArea — Многострочное поле

TextArea::make('content') ->rows(5) ->placeholder('Введите текст...') ->required();

Toggle — Переключатель

Toggle::make('is_active') ->label('Активен') ->checked($this->item?->is_active ?? true) ->sendTrueOrFalse() // Отправлять true/false вместо 1/0 ->yesvalue('active') // Значение при включении ->novalue('inactive') // Значение при выключении ->disabled() ->yoyo(); // Реактивное обновление

CheckBox — Чекбокс

CheckBox::make('terms') ->label('Я согласен с условиями') ->checked(true) ->popover('Обязательное поле'); // Группа чекбоксов foreach ($permissions as $id => $name) { LayoutFactory::field( CheckBox::make("permissions.{$name}") ->label($name) ->checked($role->hasPermission($id)) ); }

Radio — Радиокнопки

Radio::make('type') ->options([ 'post' => 'Статья', 'page' => 'Страница', 'news' => 'Новость', ]) ->value($this->item?->type ?? 'post');

RichText — Редактор

RichText::make('content') ->value($this->item?->content) ->placeholder('Начните вводить текст...');

Group — Группа полей

Group::make([ Input::make('first_name')->placeholder('Имя'), Input::make('last_name')->placeholder('Фамилия'), ])->title('ФИО');

Label — Информационная метка

Label::make('info') ->title('Важная информация') ->value('Этот пункт будет удален через 24 часа');

ViewField — Кастомное поле

ViewField::make('custom') ->view('admin-mymodule::fields.custom-field') ->with(['data' => $this->customData]);

Обертка поля

LayoutFactory::field( Input::make('email')->type('email') ) ->label('Email адрес') ->required() ->small('Будет использоваться для уведомлений') ->popover('Email должен быть уникальным');

Действия (Actions)

Button — Кнопка

use Flute\Admin\Platform\Actions\Button; use Flute\Admin\Platform\Support\Color; // Простая кнопка с переходом Button::make(__('admin.buttons.create')) ->icon('ph.bold.plus-bold') ->redirect(url('/admin/items/create')) ->type(Color::PRIMARY); // Кнопка вызова метода Button::make(__('admin.buttons.save')) ->icon('ph.bold.floppy-disk-bold') ->method('save') ->type(Color::SUCCESS); // Кнопка с подтверждением Button::make(__('admin.buttons.delete')) ->icon('ph.bold.trash-bold') ->method('delete', ['id' => $item->id]) ->confirm(__('admin.confirms.delete'), 'error') ->type(Color::DANGER); // Кнопка открытия модального окна Button::make(__('admin.buttons.edit')) ->icon('ph.bold.pencil-bold') ->modal('editModal', ['id' => $item->id]) ->type(Color::OUTLINE_PRIMARY); // Кнопка-ссылка Button::make(__('admin.buttons.preview')) ->icon('ph.bold.eye-bold') ->href(url('/items/' . $item->slug)) ->type(Color::OUTLINE_SECONDARY); // Дополнительные настройки Button::make('Custom') ->size('small') // small, medium, large ->fullWidth() // На всю ширину ->disabled() // Отключена ->tooltip('Подсказка') ->withLoading(true) // Индикатор загрузки ->addClass('custom-class') ->novalidate(); // Без валидации формы

Типы кнопок (Color)

use Flute\Admin\Platform\Support\Color; Color::PRIMARY // Основная Color::SECONDARY // Вторичная Color::SUCCESS // Успех Color::DANGER // Опасность Color::WARNING // Предупреждение Color::INFO // Информация Color::LIGHT // Светлая Color::DARK // Темная Color::OUTLINE_PRIMARY // Контурная основная Color::OUTLINE_SECONDARY Color::OUTLINE_SUCCESS Color::OUTLINE_DANGER Color::OUTLINE_WARNING Color::OUTLINE_INFO
use Flute\Admin\Platform\Actions\DropDown; use Flute\Admin\Platform\Actions\DropDownItem; DropDown::make() ->icon('ph.regular.dots-three-outline-vertical') ->list([ DropDownItem::make(__('admin.buttons.view')) ->redirect(url('/items/' . $item->id)) ->icon('ph.regular.eye') ->type(Color::OUTLINE_PRIMARY) ->size('small') ->fullWidth(), DropDownItem::make(__('admin.buttons.edit')) ->modal('editModal', ['id' => $item->id]) ->icon('ph.regular.pencil') ->type(Color::OUTLINE_PRIMARY) ->size('small') ->fullWidth(), DropDownItem::make(__('admin.buttons.duplicate')) ->method('duplicate', ['id' => $item->id]) ->icon('ph.regular.copy') ->type(Color::OUTLINE_SECONDARY) ->size('small') ->fullWidth(), DropDownItem::make(__('admin.buttons.delete')) ->confirm(__('admin.confirms.delete')) ->method('delete', ['id' => $item->id]) ->icon('ph.regular.trash') ->type(Color::OUTLINE_DANGER) ->size('small') ->fullWidth(), ]);

Интеграция модулей

Регистрация в ModuleServiceProvider

<?php namespace Flute\Modules\MyModule\Providers; use Flute\Core\Support\ModuleServiceProvider; use Flute\Modules\MyModule\Admin\MyModuleAdminPackage; class MyModuleServiceProvider extends ModuleServiceProvider { public function boot(\DI\Container $container): void { $this->loadEntities(); $this->loadConfigs(); $this->loadTranslations(); $this->loadRouterAttributes(); $this->loadViews('Resources/views', 'mymodule'); // Регистрация админ-пакета только при доступе к админке if (is_admin_path() && user()->can('admin')) { $this->loadPackage(new MyModuleAdminPackage()); } } public function extensions(): array { return []; } }

Всегда проверяйте is_admin_path() и user()->can('admin') перед загрузкой админ-пакета для оптимизации производительности.

События

Админ-панель генерирует следующие события:

PackageRegisteredEvent

Срабатывает при регистрации пакета:

use Flute\Admin\Events\PackageRegisteredEvent; events()->addListener(PackageRegisteredEvent::NAME, function (PackageRegisteredEvent $event) { $package = $event->getPackage(); // Логика после регистрации пакета });

PackageInitializedEvent

Срабатывает после инициализации пакета:

use Flute\Admin\Events\PackageInitializedEvent; events()->addListener(PackageInitializedEvent::NAME, function (PackageInitializedEvent $event) { $package = $event->getPackage(); // Логика после инициализации });

Компоненты ячеек таблицы

Для форматирования данных в таблицах используются компоненты:

use Flute\Admin\Platform\Components\Cells\DateTime; use Flute\Admin\Platform\Components\Cells\DateTimeSplit; use Flute\Admin\Platform\Components\Cells\Boolean; use Flute\Admin\Platform\Components\Cells\Currency; use Flute\Admin\Platform\Components\Cells\Number; use Flute\Admin\Platform\Components\Cells\Percentage; use Flute\Admin\Platform\Components\Cells\BadgeLink; TD::make('createdAt')->asComponent(DateTime::class); TD::make('is_active')->asComponent(Boolean::class); TD::make('price')->asComponent(Currency::class); TD::make('views')->asComponent(Number::class); TD::make('progress')->asComponent(Percentage::class);

Утилиты экрана

Flash-сообщения

$this->flashMessage('Операция выполнена успешно', 'success'); $this->flashMessage('Произошла ошибка', 'error'); $this->flashMessage('Предупреждение', 'warning'); $this->flashMessage('Информация', 'info');

Редирект

$this->redirectTo('/admin/items'); $this->redirect('admin.items.list'); // По имени роута

Модальные окна

$this->openModal('editModal', ['id' => 123]); $this->closeModal();

Валидация

$validation = $this->validate([ 'title' => ['required', 'string', 'max-str-len:255'], 'email' => ['required', 'email', 'unique:users,email'], 'category_id' => ['required', 'integer', 'exists:categories,id'], ], request()->input()); if (!$validation) { return; // Ошибки отображаются автоматически }

Загрузка JS/CSS

public function mount(): void { $this->loadJS('app/Modules/MyModule/Resources/assets/js/admin.js'); $this->loadCSS('app/Modules/MyModule/Resources/assets/css/admin.css'); }

Хлебные крошки

public function mount(): void { breadcrumb() ->add(__('def.admin_panel'), url('/admin')) ->add(__('admin-mymodule.breadcrumbs.items'), url('/admin/mymodule')) ->add(__('admin-mymodule.breadcrumbs.edit')); }

Лучшие практики

Структура модуля

Создайте структуру папок

app/Modules/MyModule/ ├── Admin/ │ ├── MyModuleAdminPackage.php │ ├── Screens/ │ │ ├── ItemListScreen.php │ │ └── ItemEditScreen.php │ ├── routes.php │ └── Resources/ │ ├── views/ │ │ └── cells/ │ ├── assets/scss/ │ └── lang/

Создайте пакет с базовой конфигурацией

Создайте экраны для CRUD-операций

Зарегистрируйте пакет в ServiceProvider модуля

Производительность

  • Используйте ленивую загрузку (lazyload()) для табов
  • Кешируйте тяжелые запросы в mount()
  • Используйте пагинацию для больших списков
  • Загружайте только необходимые связи через load()

Безопасность

  • Всегда указывайте $permission в экранах
  • Проверяйте права в методах-обработчиках
  • Валидируйте все входящие данные
  • Используйте user()->can() для проверки действий над объектами

UX

  • Используйте понятные заголовки и описания
  • Добавляйте подтверждения для деструктивных действий
  • Показывайте flash-сообщения о результатах операций
  • Используйте хлебные крошки для навигации
// Пример проверки прав над объектом public function delete(): void { $item = Item::findByPK(request()->input('id')); if (!$item) { $this->flashMessage(__('admin.messages.not_found'), 'error'); return; } if (!user()->can($item)) { $this->flashMessage(__('admin.messages.access_denied'), 'error'); return; } $item->delete(); $this->flashMessage(__('admin.messages.deleted'), 'success'); }

Пример полного CRUD-экрана

<?php namespace Flute\Modules\Blog\Admin\Screens; use Flute\Admin\Platform\Actions\Button; use Flute\Admin\Platform\Actions\DropDown; use Flute\Admin\Platform\Actions\DropDownItem; use Flute\Admin\Platform\Fields\Input; use Flute\Admin\Platform\Fields\Select; use Flute\Admin\Platform\Fields\Tab; use Flute\Admin\Platform\Fields\TD; use Flute\Admin\Platform\Fields\TextArea; use Flute\Admin\Platform\Fields\Toggle; use Flute\Admin\Platform\Layouts\LayoutFactory; use Flute\Admin\Platform\Repository; use Flute\Admin\Platform\Screen; use Flute\Admin\Platform\Support\Color; use Flute\Modules\Blog\Database\Entities\Article; use Flute\Modules\Blog\Database\Entities\Category; class ArticleListScreen extends Screen { public ?string $name = 'blog.admin.articles.title'; public ?string $description = 'blog.admin.articles.description'; public ?string $permission = 'admin.blog'; public $articles; public $categories; public function mount(): void { breadcrumb() ->add(__('def.admin_panel'), url('/admin')) ->add(__('blog.admin.articles.title')); $this->articles = rep(Article::class) ->select() ->load('category') ->orderBy('createdAt', 'DESC'); $this->categories = collect(Category::findAll()) ->pluck('name', 'id') ->toArray(); } public function commandBar(): array { return [ Button::make(__('blog.admin.buttons.create')) ->icon('ph.bold.plus-bold') ->modal('createModal') ->type(Color::PRIMARY), ]; } public function layout(): array { return [ LayoutFactory::table('articles', [ TD::selection('id'), TD::make('title', __('blog.admin.table.title')) ->render(fn (Article $a) => view('admin-blog::cells.article', ['article' => $a])) ->sort() ->minWidth('250px'), TD::make('category.name', __('blog.admin.table.category')) ->width('150px'), TD::make('is_published', __('blog.admin.table.status')) ->render(fn (Article $a) => $a->is_published ? '<span class="badge bg-success">' . __('blog.admin.status.published') . '</span>' : '<span class="badge bg-secondary">' . __('blog.admin.status.draft') . '</span>') ->alignCenter() ->width('120px'), TD::make('views', __('blog.admin.table.views')) ->sort() ->alignCenter() ->width('100px'), TD::make('createdAt', __('blog.admin.table.created')) ->asComponent(\Flute\Admin\Platform\Components\Cells\DateTime::class) ->sort() ->defaultSort(true, 'desc') ->width('150px'), TD::make('actions') ->alignCenter() ->cantHide() ->disableSearch() ->width('80px') ->render(fn (Article $a) => DropDown::make() ->icon('ph.regular.dots-three-outline-vertical') ->list([ DropDownItem::make(__('def.edit')) ->modal('editModal', ['id' => $a->id]) ->icon('ph.regular.pencil') ->type(Color::OUTLINE_PRIMARY) ->size('small') ->fullWidth(), DropDownItem::make(__('def.delete')) ->confirm(__('blog.admin.confirms.delete')) ->method('delete', ['id' => $a->id]) ->icon('ph.regular.trash') ->type(Color::OUTLINE_DANGER) ->size('small') ->fullWidth(), ])), ]) ->searchable(['title', 'content']) ->perPage(15) ->bulkActions([ Button::make(__('admin.bulk.delete_selected')) ->icon('ph.bold.trash-bold') ->type(Color::OUTLINE_DANGER) ->confirm(__('admin.confirms.delete_selected')) ->method('bulkDelete'), ]), ]; } public function createModal(Repository $params) { return LayoutFactory::modal($params, [ LayoutFactory::tabs([ Tab::make(__('blog.admin.tabs.content'))->layouts([ LayoutFactory::field( Input::make('title')->placeholder(__('blog.admin.fields.title.placeholder')) )->label(__('blog.admin.fields.title.label'))->required(), LayoutFactory::field( TextArea::make('content')->rows(10) )->label(__('blog.admin.fields.content.label'))->required(), ]), Tab::make(__('blog.admin.tabs.settings'))->layouts([ LayoutFactory::field( Select::make('category_id') ->options($this->categories) ->empty(__('blog.admin.fields.category.empty')) )->label(__('blog.admin.fields.category.label')), LayoutFactory::field( Toggle::make('is_published')->label(__('blog.admin.fields.published.label')) ), ]), ])->slug('create_article_tabs'), ]) ->title(__('blog.admin.modal.create.title')) ->applyButton(__('def.create')) ->method('store') ->size('lg'); } public function editModal(Repository $params) { $article = Article::findByPK($params->get('id')); if (!$article) { $this->flashMessage(__('blog.admin.messages.not_found'), 'error'); return; } return LayoutFactory::modal($params, [ LayoutFactory::tabs([ Tab::make(__('blog.admin.tabs.content'))->layouts([ LayoutFactory::field( Input::make('title')->value($article->title) )->label(__('blog.admin.fields.title.label'))->required(), LayoutFactory::field( TextArea::make('content')->rows(10)->value($article->content) )->label(__('blog.admin.fields.content.label'))->required(), ]), Tab::make(__('blog.admin.tabs.settings'))->layouts([ LayoutFactory::field( Select::make('category_id') ->options($this->categories) ->value($article->category_id) ->empty(__('blog.admin.fields.category.empty')) )->label(__('blog.admin.fields.category.label')), LayoutFactory::field( Toggle::make('is_published') ->checked($article->is_published) ->label(__('blog.admin.fields.published.label')) ), ]), ])->slug('edit_article_tabs'), ]) ->title(__('blog.admin.modal.edit.title')) ->applyButton(__('def.save')) ->method('update') ->size('lg'); } public function store(): void { $data = request()->input(); $valid = $this->validate([ 'title' => ['required', 'string', 'max-str-len:255'], 'content' => ['required', 'string'], 'category_id' => ['nullable', 'integer', 'exists:blog_categories,id'], ], $data); if (!$valid) return; $article = new Article(); $article->title = $data['title']; $article->content = $data['content']; $article->category_id = $data['category_id'] ?: null; $article->is_published = isset($data['is_published']); $article->author_id = user()->id; $article->save(); $this->closeModal(); $this->flashMessage(__('blog.admin.messages.created'), 'success'); $this->articles = rep(Article::class) ->select() ->load('category') ->orderBy('createdAt', 'DESC'); } public function update(): void { $id = $this->modalParams->get('id'); $article = Article::findByPK($id); if (!$article) { $this->flashMessage(__('blog.admin.messages.not_found'), 'error'); return; } $data = request()->input(); $valid = $this->validate([ 'title' => ['required', 'string', 'max-str-len:255'], 'content' => ['required', 'string'], 'category_id' => ['nullable', 'integer', 'exists:blog_categories,id'], ], $data); if (!$valid) return; $article->title = $data['title']; $article->content = $data['content']; $article->category_id = $data['category_id'] ?: null; $article->is_published = isset($data['is_published']); $article->save(); $this->closeModal(); $this->flashMessage(__('blog.admin.messages.updated'), 'success'); } public function delete(): void { $article = Article::findByPK(request()->input('id')); if ($article) { $article->delete(); $this->flashMessage(__('blog.admin.messages.deleted'), 'success'); } } public function bulkDelete(): void { $ids = request()->input('selected', []); foreach ($ids as $id) { $article = Article::findByPK((int) $id); $article?->delete(); } $this->flashMessage(__('blog.admin.messages.bulk_deleted'), 'success'); } }