- topbar: Livewire LegacySyncButton next to 写文章 (moved from user menu) - view modes: add 图文(card) grid with cover fallback chain (cover → attachment → first content image → placeholder); list mode now title-only compact - archives: year/month/day index grid - page cache admin: size + expiry columns, total size, cache policy note - theme inject: stylevars switch from KeyValue to Repeater with multi-line HTML editing - import: html_entity_decode for category/post-title/link/keywords/comment author
183 lines
6.0 KiB
PHP
183 lines
6.0 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Filament\Pages;
|
||
|
||
use App\Blog\Support\ThemeInjector;
|
||
use App\Blog\Support\ThemeManager;
|
||
use App\Models\Setting;
|
||
use Filament\Actions\Action;
|
||
use Filament\Forms\Components\Repeater;
|
||
use Filament\Forms\Components\Textarea;
|
||
use Filament\Forms\Components\TextInput;
|
||
use Filament\Forms\Components\Toggle;
|
||
use Filament\Forms\Concerns\InteractsWithForms;
|
||
use Filament\Notifications\Notification;
|
||
use Filament\Pages\Page;
|
||
use Filament\Schemas\Components\Tabs;
|
||
use Filament\Schemas\Components\Tabs\Tab;
|
||
use Filament\Schemas\Schema;
|
||
|
||
class ThemeInject extends Page
|
||
{
|
||
use InteractsWithForms;
|
||
|
||
protected static \UnitEnum|string|null $navigationGroup = '管理';
|
||
|
||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-squares-plus';
|
||
|
||
protected static ?string $navigationLabel = '主题注入';
|
||
|
||
protected static ?string $title = '主题注入';
|
||
|
||
protected string $view = 'filament.pages.theme-inject';
|
||
|
||
public array $data = [];
|
||
|
||
public function mount(): void
|
||
{
|
||
$vars = app(ThemeManager::class)->variables();
|
||
$varRows = [];
|
||
foreach ($vars as $key => $value) {
|
||
$varRows[] = ['key' => (string) $key, 'value' => (string) $value];
|
||
}
|
||
|
||
$this->form->fill([
|
||
'inject' => $this->normalizeBlocks(Setting::get('inject.blocks', [])),
|
||
'theme_vars' => $varRows,
|
||
]);
|
||
}
|
||
|
||
public function form(Schema $schema): Schema
|
||
{
|
||
$groups = [
|
||
'页面级' => ['head', 'header', 'content_top', 'content_bottom', 'footer', 'body_end'],
|
||
'侧边栏级' => ['sidebar_top', 'sidebar_bottom'],
|
||
'文章级' => ['post_content_top', 'post_content_bottom'],
|
||
];
|
||
|
||
$tabs = [];
|
||
|
||
foreach ($groups as $groupLabel => $points) {
|
||
$sections = [];
|
||
foreach ($points as $point) {
|
||
$sections[] = \Filament\Schemas\Components\Section::make(ThemeInjector::POINTS[$point])
|
||
->description($point)
|
||
->schema([
|
||
Repeater::make("inject.{$point}")
|
||
->label('注入块(多条按拖拽顺序渲染)')
|
||
->schema([
|
||
TextInput::make('title')->label('名称')->placeholder('如:百度统计')->columnSpan(2),
|
||
Textarea::make('html')->label('内容(HTML)')->rows(3)->columnSpanFull(),
|
||
Toggle::make('enabled')->label('启用')->default(true),
|
||
])
|
||
->columns(3)
|
||
->defaultItems(0)
|
||
->collapsible()
|
||
->reorderable()
|
||
->reorderableWithButtons()
|
||
->addActionLabel('添加一条'),
|
||
]);
|
||
}
|
||
$tabs[] = Tab::make($groupLabel)->schema($sections);
|
||
}
|
||
|
||
$tabs[] = Tab::make('主题变量(stylevar)')
|
||
->schema([
|
||
Repeater::make('theme_vars')
|
||
->label('当前主题变量:视图用 {{ theme(\'var\', \'key\') }} 引用')
|
||
->schema([
|
||
TextInput::make('key')
|
||
->label('变量名')
|
||
->required(),
|
||
Textarea::make('value')
|
||
->label('内容(HTML)')
|
||
->rows(4),
|
||
])
|
||
->columns(1)
|
||
->defaultItems(0)
|
||
->collapsible()
|
||
->itemLabel(fn (array $state): ?string => $state['key'] ?? null)
|
||
->reorderable()
|
||
->reorderableWithButtons()
|
||
->addActionLabel('添加变量')
|
||
->columnSpanFull(),
|
||
]);
|
||
|
||
return $schema
|
||
->components([
|
||
Tabs::make('inject-tabs')
|
||
->tabs($tabs),
|
||
])
|
||
->statePath('data');
|
||
}
|
||
|
||
public function save(): void
|
||
{
|
||
$data = $this->form->getState();
|
||
|
||
$blocks = [];
|
||
foreach ($data['inject'] ?? [] as $point => $items) {
|
||
$order = 0;
|
||
foreach ($items ?? [] as $item) {
|
||
if (trim((string) ($item['html'] ?? '')) === '') {
|
||
continue;
|
||
}
|
||
$blocks[] = [
|
||
'point' => $point,
|
||
'title' => $item['title'] ?? '',
|
||
'html' => $item['html'],
|
||
'sort' => $order++,
|
||
'enabled' => (bool) ($item['enabled'] ?? true),
|
||
];
|
||
}
|
||
}
|
||
|
||
Setting::set('inject.blocks', $blocks);
|
||
|
||
$vars = [];
|
||
foreach ($data['theme_vars'] ?? [] as $row) {
|
||
$key = trim((string) ($row['key'] ?? ''));
|
||
if ($key === '') {
|
||
continue;
|
||
}
|
||
$vars[$key] = (string) ($row['value'] ?? '');
|
||
}
|
||
Setting::set('theme_vars_'.app(ThemeManager::class)->active(), $vars);
|
||
\App\Blog\Support\PageCacheMiddleware::flush();
|
||
|
||
Notification::make()->title('注入内容已保存')->success()->send();
|
||
}
|
||
|
||
protected function getFormActions(): array
|
||
{
|
||
return [
|
||
Action::make('save')->label('保存')->submit('save'),
|
||
];
|
||
}
|
||
|
||
private function normalizeBlocks(mixed $blocks): array
|
||
{
|
||
if (! is_array($blocks)) {
|
||
$blocks = json_decode((string) $blocks, true) ?: [];
|
||
}
|
||
|
||
$normalized = [];
|
||
foreach ($blocks as $b) {
|
||
$point = $b['point'] ?? null;
|
||
if (! $point) {
|
||
continue;
|
||
}
|
||
$normalized[$point][] = [
|
||
'title' => $b['title'] ?? '',
|
||
'html' => $b['html'] ?? '',
|
||
'sort' => (int) ($b['sort'] ?? 0),
|
||
'enabled' => (bool) ($b['enabled'] ?? true),
|
||
];
|
||
}
|
||
|
||
return $normalized;
|
||
}
|
||
}
|