diff --git a/app/Blog/Providers/ThemeServiceProvider.php b/app/Blog/Providers/ThemeServiceProvider.php index 3233728..bb1cdc9 100644 --- a/app/Blog/Providers/ThemeServiceProvider.php +++ b/app/Blog/Providers/ThemeServiceProvider.php @@ -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(); } } diff --git a/app/Blog/Support/ThemeInjector.php b/app/Blog/Support/ThemeInjector.php new file mode 100644 index 0000000..5d72139 --- /dev/null +++ b/app/Blog/Support/ThemeInjector.php @@ -0,0 +1,78 @@ + '页头( 前)', + 'header' => '页头下方', + 'sidebar_top' => '侧边栏顶部', + 'sidebar_bottom' => '侧边栏底部', + 'content_top' => '内容区顶部', + 'content_bottom' => '内容区底部', + 'post_content_top' => '文章正文前', + 'post_content_bottom' => '文章正文后', + 'footer' => '页脚前', + 'body_end' => '页面末尾( 前)', + ]; + + /** + * 读取某一注入点的所有注入块(多条,已排序、已过滤停用), + * 并把插件追加的 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); + } + } + }); + } +} diff --git a/app/Filament/Pages/CacheManager.php b/app/Filament/Pages/CacheManager.php deleted file mode 100644 index d12d2b7..0000000 --- a/app/Filament/Pages/CacheManager.php +++ /dev/null @@ -1,64 +0,0 @@ -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(); - } -} diff --git a/app/Filament/Pages/ThemeInject.php b/app/Filament/Pages/ThemeInject.php new file mode 100644 index 0000000..3e59aaf --- /dev/null +++ b/app/Filament/Pages/ThemeInject.php @@ -0,0 +1,142 @@ +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; + } +} diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index d9946e9..a2ae23a 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -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('管理'), diff --git a/docs/themes.md b/docs/themes.md index 40eb082..f6fbc2c 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -100,6 +100,30 @@ themes/{name}/ - 开发期:`GET /themes/{name}/assets/{path}` 流式返回(带缓存头) - 生产:`php artisan theme:publish` 复制到 `public/themes/`,由 Web 服务器托管 +## 内容注入点(广告 / 统计 / 临时链接) + +主题视图内置 10 个注入点(Blade `@stack`),后台「主题注入」页管理: + +| 注入点 | 位置 | 典型用途 | +|--------|------|----------| +| `theme:head` | `` 前 | 统计代码、广告、验证 | +| `theme:header` | 页头下方 | 顶部横幅/公告 | +| `theme:sidebar_top` | 侧边栏顶部 | 首屏广告 | +| `theme:sidebar_bottom` | 侧边栏底部 | 广告/临时链接 | +| `theme:content_top` | 内容区顶部 | 列表/文章上方横幅 | +| `theme:content_bottom` | 内容区底部 | 分页后广告 | +| `theme:post_content_top` | 文章正文前 | 正文顶部广告 | +| `theme:post_content_bottom` | 文章正文后 | 正文底部广告 | +| `theme:footer` | 页脚前 | 版权上方声明 | +| `theme:body_end` | `` 前 | 统计代码 | + +特性: +- **同一位置可添加多条**注入块(名称 + HTML + 排序 + 启停),按排序渲染 +- 插件可通过 filter `theme.inject.{point}` 追加 +- 主题变量(stylevar 复活):后台「主题注入」→「主题变量」Tab,视图用 `{{ theme('var', 'key') }}` 引用任意 HTML + +新主题须在对应 partial/视图放置 `@stack('theme:xxx')` 才能接收注入。 + ## 预览图 `theme.json` 的 `screenshot` 指向 `assets/` 下的预览图(如 `screenshot.png`),后台主题管理以卡片形式展示。可用无头浏览器截图生成:`node` + playwright 打开站点首页保存为 `themes/{name}/assets/screenshot.png`。 diff --git a/resources/views/archives.blade.php b/resources/views/archives.blade.php index 725c9b6..ce0e0b6 100644 --- a/resources/views/archives.blade.php +++ b/resources/views/archives.blade.php @@ -28,5 +28,6 @@ @include('partials.sidebar') @include('partials.footer') +@stack('theme:body_end') diff --git a/resources/views/comments.blade.php b/resources/views/comments.blade.php index 85a8e2a..ee2751a 100644 --- a/resources/views/comments.blade.php +++ b/resources/views/comments.blade.php @@ -24,5 +24,6 @@ @include('partials.sidebar') @include('partials.footer') +@stack('theme:body_end') diff --git a/resources/views/filament/pages/cache-manager.blade.php b/resources/views/filament/pages/cache-manager.blade.php deleted file mode 100644 index dc36706..0000000 --- a/resources/views/filament/pages/cache-manager.blade.php +++ /dev/null @@ -1,37 +0,0 @@ - -
- -

- 匿名访客的首页/列表/文章/归档等页面按 URL 缓存(TTL {{ config('blog.page_cache_ttl') }} 秒), - 文章浏览量由独立端点统计,不受缓存影响。发布/编辑文章后建议清空本缓存。 -

-
- 清空页面缓存 -
-
- - -

- 侧边栏(分类/最新文章/评论/标签/友链/统计)与站点设置、附件映射的缓存,最长 1 小时。 -

-
- 清空侧边栏 - 清空设置 -
-
- - -

清空应用全部缓存(含上述所有 + 其他临时缓存)。

-
- 清空全部缓存 -
-
- - -

清除并重建 Blade 视图 / 路由 / 配置编译缓存(发布或更新代码后使用)。

-
- 重建编译缓存 -
-
-
-
diff --git a/resources/views/filament/pages/theme-inject.blade.php b/resources/views/filament/pages/theme-inject.blade.php new file mode 100644 index 0000000..fa235e3 --- /dev/null +++ b/resources/views/filament/pages/theme-inject.blade.php @@ -0,0 +1,10 @@ + + +
+ {{ $this->form }} +
+ 保存 +
+
+
+
diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php index 5262de5..2981305 100644 --- a/resources/views/index.blade.php +++ b/resources/views/index.blade.php @@ -21,5 +21,6 @@ @include('partials.sidebar') @include('partials.footer') +@stack('theme:body_end') diff --git a/resources/views/links.blade.php b/resources/views/links.blade.php index 97be616..cb80ab6 100644 --- a/resources/views/links.blade.php +++ b/resources/views/links.blade.php @@ -19,5 +19,6 @@ @include('partials.sidebar') @include('partials.footer') +@stack('theme:body_end') diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php index 33d50df..32feac2 100644 --- a/resources/views/list.blade.php +++ b/resources/views/list.blade.php @@ -19,5 +19,6 @@ @include('partials.sidebar') @include('partials.footer') +@stack('theme:body_end') diff --git a/resources/views/login.blade.php b/resources/views/login.blade.php index b0e0605..d2de3a1 100644 --- a/resources/views/login.blade.php +++ b/resources/views/login.blade.php @@ -25,5 +25,6 @@ @include('partials.footer') +@stack('theme:body_end') diff --git a/resources/views/partials/footer.blade.php b/resources/views/partials/footer.blade.php index 8c2ee05..b4e19b4 100644 --- a/resources/views/partials/footer.blade.php +++ b/resources/views/partials/footer.blade.php @@ -1,3 +1,4 @@ +@stack('theme:footer')