Админ-панель 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 | Требуемые разрешения |
$js | array | JavaScript файлы для загрузки |
$css | array | CSS файлы для загрузки |
Методы жизненного цикла
| Метод | Описание |
|---|---|
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), // Отключить анимацию
];
}Modal — Модальное окно
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_INFODropDown — Выпадающее меню
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');
}
}