feat: 主题内容注入点系统(10 个标准位置,同位置多条注入块可排序/启停,stylevar 变量复活)+ 后台主题注入管理页(Tab 分区)+ 缓存/设置收进顶部用户下拉菜单(Livewire action 即时通知)
This commit is contained in:
@@ -28,5 +28,8 @@ class ThemeServiceProvider extends ServiceProvider
|
||||
['index', 'show', 'list', 'archives', 'tags', 'tag', 'search', 'links', 'comments', 'login', 'register', 'profile', 'membership.*', 'payments.*'],
|
||||
SidebarComposer::class
|
||||
);
|
||||
|
||||
// 主题注入点:广告/统计/临时链接(多条、可排序、可启停)
|
||||
\App\Blog\Support\ThemeInjector::registerComposer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Blog\Support;
|
||||
|
||||
use App\Models\Setting;
|
||||
|
||||
/**
|
||||
* 主题内容注入点:在标准位置注入广告/统计/临时链接等 HTML。
|
||||
*
|
||||
* 注入点(Blade @stack,主题视图内置):
|
||||
* head / header / sidebar_top / sidebar_bottom / content_top / content_bottom
|
||||
* post_content_top / post_content_bottom / footer / body_end
|
||||
*
|
||||
* 特性:
|
||||
* 1. 同一位置可配置「多条」注入块(后台主题注入页管理),按 sort 顺序渲染
|
||||
* 2. 每条可启停
|
||||
* 3. 插件可通过 filter `theme.inject.{point}` 追加
|
||||
* 4. 主题变量(stylevar):theme('var', 'key') 引用任意键值 HTML
|
||||
*/
|
||||
class ThemeInjector
|
||||
{
|
||||
public const POINTS = [
|
||||
'head' => '页头(</head> 前)',
|
||||
'header' => '页头下方',
|
||||
'sidebar_top' => '侧边栏顶部',
|
||||
'sidebar_bottom' => '侧边栏底部',
|
||||
'content_top' => '内容区顶部',
|
||||
'content_bottom' => '内容区底部',
|
||||
'post_content_top' => '文章正文前',
|
||||
'post_content_bottom' => '文章正文后',
|
||||
'footer' => '页脚前',
|
||||
'body_end' => '页面末尾(</body> 前)',
|
||||
];
|
||||
|
||||
/**
|
||||
* 读取某一注入点的所有注入块(多条,已排序、已过滤停用),
|
||||
* 并把插件追加的 HTML 合并进来。
|
||||
*/
|
||||
public static function blocksFor(string $point): array
|
||||
{
|
||||
$raw = Setting::get('inject.blocks', []);
|
||||
$raw = is_array($raw) ? $raw : (json_decode((string) $raw, true) ?: []);
|
||||
|
||||
$blocks = collect($raw)
|
||||
->filter(fn (array $b) => ($b['point'] ?? null) === $point && ($b['enabled'] ?? true))
|
||||
->sortBy(fn (array $b) => (int) ($b['sort'] ?? 0))
|
||||
->pluck('html')
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$extra = (string) app(PluginManager::class)->applyFilters("theme.inject.{$point}", '');
|
||||
|
||||
if ($extra !== '') {
|
||||
$blocks[] = $extra;
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 View Composer:渲染前台视图前把各注入点内容 push 到对应 @stack。
|
||||
*/
|
||||
public static function registerComposer(): void
|
||||
{
|
||||
$views = ['index', 'show', 'list', 'archives', 'tags', 'tag', 'search', 'links', 'comments', 'login', 'register', 'profile', 'membership.*', 'payments.*'];
|
||||
|
||||
\Illuminate\Support\Facades\View::composer($views, function (\Illuminate\View\View $view) {
|
||||
foreach (array_keys(self::POINTS) as $point) {
|
||||
foreach (self::blocksFor($point) as $html) {
|
||||
$view->getFactory()->startPush("theme:{$point}", $html);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Blog\Support\PageCacheMiddleware;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class CacheManager extends Page
|
||||
{
|
||||
protected static \UnitEnum|string|null $navigationGroup = '管理';
|
||||
|
||||
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-bolt';
|
||||
|
||||
protected static ?string $navigationLabel = '缓存管理';
|
||||
|
||||
protected string $view = 'filament.pages.cache-manager';
|
||||
|
||||
public function flushPages(): void
|
||||
{
|
||||
PageCacheMiddleware::flush();
|
||||
Notification::make()->title('前台页面缓存已清空')->success()->send();
|
||||
}
|
||||
|
||||
public function flushSidebar(): void
|
||||
{
|
||||
foreach (['blog.sidebar.categories', 'blog.sidebar.recent_posts', 'blog.sidebar.recent_comments', 'blog.sidebar.hot_tags', 'blog.sidebar.links', 'blog.sidebar.stats'] as $key) {
|
||||
Cache::forget($key);
|
||||
}
|
||||
Notification::make()->title('侧边栏缓存已清空')->success()->send();
|
||||
}
|
||||
|
||||
public function flushSettings(): void
|
||||
{
|
||||
\App\Models\Setting::flushCache();
|
||||
Cache::forget('attach.legacy_ids');
|
||||
Notification::make()->title('设置缓存已清空')->success()->send();
|
||||
}
|
||||
|
||||
public function flushAll(): void
|
||||
{
|
||||
PageCacheMiddleware::flush();
|
||||
\App\Models\Setting::flushCache();
|
||||
Cache::flush();
|
||||
Notification::make()->title('全部缓存已清空')->success()->send();
|
||||
}
|
||||
|
||||
public function recompile(): void
|
||||
{
|
||||
Artisan::call('view:clear');
|
||||
Artisan::call('route:clear');
|
||||
Artisan::call('config:clear');
|
||||
Notification::make()->title('视图/路由/配置缓存已重建')->success()->send();
|
||||
}
|
||||
|
||||
public function getPageCacheCountProperty(): int
|
||||
{
|
||||
return PageCacheMiddleware::count();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?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\KeyValue;
|
||||
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 string $view = 'filament.pages.theme-inject';
|
||||
|
||||
public array $data = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->form->fill([
|
||||
'inject' => $this->normalizeBlocks(Setting::get('inject.blocks', [])),
|
||||
'theme_vars' => app(ThemeManager::class)->variables(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
$tabs = [];
|
||||
$i = 0;
|
||||
|
||||
foreach (ThemeInjector::POINTS as $point => $label) {
|
||||
$tabs[] = Tab::make(($i + 1).'. '.$label)
|
||||
->schema([
|
||||
Repeater::make("inject.{$point}")
|
||||
->label('注入块(可多条,按排序渲染)')
|
||||
->schema([
|
||||
TextInput::make('title')->label('名称')->placeholder('如:百度统计')->columnSpan(2),
|
||||
Textarea::make('html')->label('内容(HTML)')->rows(3)->columnSpanFull(),
|
||||
TextInput::make('sort')->label('排序')->numeric()->default(0)->columnSpan(1),
|
||||
Toggle::make('enabled')->label('启用')->default(true)->columnSpan(1),
|
||||
])
|
||||
->columns(3)
|
||||
->defaultItems(0)
|
||||
->collapsible()
|
||||
->addActionLabel('添加一条'),
|
||||
]);
|
||||
$i++;
|
||||
}
|
||||
|
||||
$tabs[] = Tab::make('主题变量(stylevar)')
|
||||
->schema([
|
||||
KeyValue::make('theme_vars')
|
||||
->label('当前主题变量:视图用 {{ theme(\'var\', \'key\') }} 引用')
|
||||
->keyLabel('变量名')
|
||||
->valueLabel('内容(HTML)')
|
||||
->reorderable()
|
||||
->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) {
|
||||
foreach ($items ?? [] as $item) {
|
||||
if (trim((string) ($item['html'] ?? '')) === '') {
|
||||
continue;
|
||||
}
|
||||
$blocks[] = [
|
||||
'point' => $point,
|
||||
'title' => $item['title'] ?? '',
|
||||
'html' => $item['html'],
|
||||
'sort' => (int) ($item['sort'] ?? 0),
|
||||
'enabled' => (bool) ($item['enabled'] ?? true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Setting::set('inject.blocks', $blocks);
|
||||
Setting::set('theme_vars_'.app(ThemeManager::class)->active(), $data['theme_vars'] ?? []);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,36 @@ class AdminPanelProvider extends PanelProvider
|
||||
->brandName(config('blog.name'))
|
||||
->sidebarWidth('13rem')
|
||||
->maxContentWidth(\Filament\Support\Enums\Width::Full)
|
||||
->userMenuItems([
|
||||
// 工具:清空缓存(Livewire action,即时通知)
|
||||
\Filament\Actions\Action::make('flush-page-cache')
|
||||
->label('清空前台缓存')
|
||||
->icon('heroicon-o-bolt')
|
||||
->color('warning')
|
||||
->action(function (): void {
|
||||
\App\Blog\Support\PageCacheMiddleware::flush();
|
||||
\Filament\Notifications\Notification::make()->title('前台页面缓存已清空')->success()->send();
|
||||
}),
|
||||
\Filament\Actions\Action::make('flush-all-cache')
|
||||
->label('清空全部缓存')
|
||||
->icon('heroicon-o-trash')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(function (): void {
|
||||
\App\Blog\Support\PageCacheMiddleware::flush();
|
||||
\App\Models\Setting::flushCache();
|
||||
\Illuminate\Support\Facades\Cache::flush();
|
||||
\Filament\Notifications\Notification::make()->title('全部缓存已清空')->success()->send();
|
||||
}),
|
||||
\Filament\Actions\Action::make('blog-settings')
|
||||
->label('博客设置')
|
||||
->icon('heroicon-o-cog-6-tooth')
|
||||
->url(fn (): string => \App\Filament\Pages\BlogSettings::getUrl()),
|
||||
\Filament\Actions\Action::make('theme-inject')
|
||||
->label('主题注入')
|
||||
->icon('heroicon-o-squares-plus')
|
||||
->url(fn (): string => \App\Filament\Pages\ThemeInject::getUrl()),
|
||||
])
|
||||
->navigationGroups([
|
||||
NavigationGroup::make('内容'),
|
||||
NavigationGroup::make('管理'),
|
||||
|
||||
Reference in New Issue
Block a user