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('管理'),
|
||||
|
||||
@@ -100,6 +100,30 @@ themes/{name}/
|
||||
- 开发期:`GET /themes/{name}/assets/{path}` 流式返回(带缓存头)
|
||||
- 生产:`php artisan theme:publish` 复制到 `public/themes/`,由 Web 服务器托管
|
||||
|
||||
## 内容注入点(广告 / 统计 / 临时链接)
|
||||
|
||||
主题视图内置 10 个注入点(Blade `@stack`),后台「主题注入」页管理:
|
||||
|
||||
| 注入点 | 位置 | 典型用途 |
|
||||
|--------|------|----------|
|
||||
| `theme:head` | `</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` | `</body>` 前 | 统计代码 |
|
||||
|
||||
特性:
|
||||
- **同一位置可添加多条**注入块(名称 + 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`。
|
||||
|
||||
@@ -28,5 +28,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<x-filament::section heading="前台页面缓存" :description="'当前缓存页面数:'.$this->page_cache_count">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
匿名访客的首页/列表/文章/归档等页面按 URL 缓存(TTL {{ config('blog.page_cache_ttl') }} 秒),
|
||||
文章浏览量由独立端点统计,不受缓存影响。发布/编辑文章后建议清空本缓存。
|
||||
</p>
|
||||
<div class="mt-4">
|
||||
<x-filament::button color="primary" wire:click="flushPages">清空页面缓存</x-filament::button>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="侧边栏与设置缓存">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
侧边栏(分类/最新文章/评论/标签/友链/统计)与站点设置、附件映射的缓存,最长 1 小时。
|
||||
</p>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<x-filament::button color="gray" wire:click="flushSidebar">清空侧边栏</x-filament::button>
|
||||
<x-filament::button color="gray" wire:click="flushSettings">清空设置</x-filament::button>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="全部缓存">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">清空应用全部缓存(含上述所有 + 其他临时缓存)。</p>
|
||||
<div class="mt-4">
|
||||
<x-filament::button color="danger" wire:click="flushAll" wire:confirm="确定清空全部缓存?">清空全部缓存</x-filament::button>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<x-filament::section heading="编译缓存">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">清除并重建 Blade 视图 / 路由 / 配置编译缓存(发布或更新代码后使用)。</p>
|
||||
<div class="mt-4">
|
||||
<x-filament::button color="gray" wire:click="recompile">重建编译缓存</x-filament::button>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
@@ -0,0 +1,10 @@
|
||||
<x-filament-panels::page>
|
||||
<x-filament::section :description="'同一位置可添加多条注入块(按排序渲染);插件也可通过 theme.inject.{point} 过滤器追加'">
|
||||
<form wire:submit="save">
|
||||
{{ $this->form }}
|
||||
<div class="mt-6">
|
||||
<x-filament::button type="submit" color="primary">保存</x-filament::button>
|
||||
</div>
|
||||
</form>
|
||||
</x-filament::section>
|
||||
</x-filament-panels::page>
|
||||
@@ -21,5 +21,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -25,5 +25,6 @@
|
||||
</main>
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@stack('theme:footer')
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p>
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ $siteName }}" href="{{ route('feed.rss') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/blog.css') }}">
|
||||
@stack('head')
|
||||
@stack('theme:head')
|
||||
</head>
|
||||
|
||||
@@ -28,3 +28,4 @@
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@stack('theme:header')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<aside class="sidebar">
|
||||
@stack('theme:sidebar_top')
|
||||
<section class="widget">
|
||||
<h3 class="widget-title">分类</h3>
|
||||
<ul class="widget-list">
|
||||
@@ -70,4 +71,5 @@
|
||||
<li>标签:{{ $blogStats['tags'] }} 个</li>
|
||||
</ul>
|
||||
</section>
|
||||
</aside>
|
||||
@stack('theme:sidebar_bottom')
|
||||
|
||||
|
||||
@@ -20,5 +20,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -36,5 +36,6 @@
|
||||
</main>
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<span>· 阅读 {{ $post->views }}</span>
|
||||
</div>
|
||||
|
||||
@stack('theme:post_content_top')
|
||||
<div class="post-content">
|
||||
{!! $contentHtml !!}
|
||||
</div>
|
||||
@@ -92,5 +93,6 @@
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
<script>fetch("{{ route('track-view', $post) }}")</script>
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -18,5 +18,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -273,5 +273,6 @@
|
||||
@if (Route::has('login'))
|
||||
<div class="h-14.5 hidden lg:block"></div>
|
||||
@endif
|
||||
</body>
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
+1
@@ -0,0 +1 @@
|
||||
fake-jpg
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Blog\Support\ThemeInjector;
|
||||
use App\Models\Category;
|
||||
use App\Models\Post;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ThemeInjectTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->seed();
|
||||
\App\Blog\Support\PageCacheMiddleware::flush();
|
||||
|
||||
$user = User::create(['name' => '作者', 'email' => 'i@t.com', 'password' => bcrypt('x')]);
|
||||
$cat = Category::create(['name' => '技术', 'slug' => 'tech']);
|
||||
Post::create([
|
||||
'user_id' => $user->id,
|
||||
'category_id' => $cat->id,
|
||||
'title' => '注入测试',
|
||||
'slug' => 'inject-test',
|
||||
'content' => '正文内容',
|
||||
'content_format' => 'markdown',
|
||||
'status' => 'published',
|
||||
'published_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_injection_blocks_render_in_head_and_body(): void
|
||||
{
|
||||
Setting::set('inject.blocks', [
|
||||
['point' => 'head', 'title' => '统计', 'html' => '<script>window.ADMIN_TEST_STAT=1;</script>', 'sort' => 1, 'enabled' => true],
|
||||
['point' => 'body_end', 'title' => '底部统计', 'html' => '<script>window.BOTTOM_TEST=1;</script>', 'sort' => 1, 'enabled' => true],
|
||||
['point' => 'sidebar_top', 'title' => '广告', 'html' => '<div class="ad-block">广告位A</div>', 'sort' => 1, 'enabled' => true],
|
||||
]);
|
||||
|
||||
$html = $this->get('/')->getContent();
|
||||
|
||||
$this->assertStringContainsString('ADMIN_TEST_STAT', $html); // head 注入
|
||||
$this->assertStringContainsString('BOTTOM_TEST', $html); // body_end 注入
|
||||
$this->assertStringContainsString('广告位A', $html); // sidebar_top 注入
|
||||
}
|
||||
|
||||
public function test_multiple_blocks_in_same_point_render_in_order(): void
|
||||
{
|
||||
Setting::set('inject.blocks', [
|
||||
['point' => 'footer', 'title' => '第一条', 'html' => '<div id="inject-1">first</div>', 'sort' => 1, 'enabled' => true],
|
||||
['point' => 'footer', 'title' => '第二条', 'html' => '<div id="inject-2">second</div>', 'sort' => 2, 'enabled' => true],
|
||||
]);
|
||||
|
||||
$html = $this->get('/posts/inject-test.shtml')->getContent();
|
||||
|
||||
$pos1 = strpos($html, 'id="inject-1"');
|
||||
$pos2 = strpos($html, 'id="inject-2"');
|
||||
$this->assertNotFalse($pos1);
|
||||
$this->assertNotFalse($pos2);
|
||||
$this->assertLessThan($pos2, $pos1); // 按 sort 顺序
|
||||
}
|
||||
|
||||
public function test_disabled_block_is_not_rendered(): void
|
||||
{
|
||||
Setting::set('inject.blocks', [
|
||||
['point' => 'head', 'title' => '停用', 'html' => '<div id="disabled-inject">x</div>', 'sort' => 1, 'enabled' => false],
|
||||
]);
|
||||
|
||||
$this->get('/')->assertDontSee('disabled-inject');
|
||||
}
|
||||
|
||||
public function test_post_content_injection_points(): void
|
||||
{
|
||||
Setting::set('inject.blocks', [
|
||||
['point' => 'post_content_top', 'title' => '正文前广告', 'html' => '<div id="post-top-ad">TOP</div>', 'sort' => 1, 'enabled' => true],
|
||||
['point' => 'post_content_bottom', 'title' => '正文后广告', 'html' => '<div id="post-bottom-ad">BOTTOM</div>', 'sort' => 1, 'enabled' => true],
|
||||
]);
|
||||
|
||||
$html = $this->get('/posts/inject-test.shtml')->getContent();
|
||||
|
||||
$this->assertStringContainsString('post-top-ad', $html);
|
||||
$this->assertStringContainsString('post-bottom-ad', $html);
|
||||
// 顶部注入应在正文容器前
|
||||
$this->assertLessThan(strpos($html, '<div class="post-content">'), strpos($html, 'post-top-ad'));
|
||||
}
|
||||
|
||||
public function test_theme_vars_are_usable(): void
|
||||
{
|
||||
Setting::set('theme_vars_sablog', ['ad_header' => '<div class="header-ad">头部广告</div>']);
|
||||
|
||||
// theme('var') 直接可用
|
||||
$this->assertSame('<div class="header-ad">头部广告</div>', theme('var', 'ad_header', ''));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
@stack('theme:footer')
|
||||
<footer class="m-footer">
|
||||
<div class="m-container">
|
||||
<p>© {{ date('Y') }} {{ $siteName }}
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ $siteName }}" href="{{ route('feed.rss') }}">
|
||||
<link rel="stylesheet" href="{{ theme('asset', 'style.css') }}">
|
||||
@stack('head')
|
||||
@stack('theme:head')
|
||||
</head>
|
||||
|
||||
@@ -22,3 +22,4 @@
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@stack('theme:header')
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>{{-- #page --}}
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>{{-- #page --}}
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@stack('theme:footer')
|
||||
<div id="footer">
|
||||
<p>© {{ date('Y') }} {{ $siteName }}
|
||||
@if($siteIcp)<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener">{{ $siteIcp }}</a>@endif
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ $siteName }}" href="{{ route('feed.rss') }}">
|
||||
<link rel="stylesheet" href="{{ theme('asset', 'style.css') }}">
|
||||
@stack('head')
|
||||
@stack('theme:head')
|
||||
</head>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<a href="{{ route('home') }}">{{ $siteName }}</a>
|
||||
<p class="description">{{ $siteDescription }}</p>
|
||||
</div>
|
||||
@stack('theme:header')
|
||||
<div id="nav">
|
||||
<ul>
|
||||
<li class="{{ request()->routeIs('home') ? 'current_page_item' : '' }}"><a href="{{ route('home') }}">首页</a></li>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div class="sidebar">
|
||||
@stack('theme:sidebar_top')
|
||||
<div class="searchbox">
|
||||
<form action="{{ route('search') }}" method="get">
|
||||
<input type="search" name="q" value="{{ request('q', request('keyword', '')) }}" placeholder="搜索文章">
|
||||
@@ -69,4 +70,5 @@
|
||||
<li>标签 {{ $blogStats['tags'] }} 个</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@stack('theme:sidebar_bottom')
|
||||
|
||||
@@ -21,5 +21,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>{{-- #page --}}
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -29,9 +29,11 @@
|
||||
<span>· 阅读 {{ $post->views }}</span>
|
||||
</div>
|
||||
|
||||
@stack('theme:post_content_top')
|
||||
<div class="post-content">
|
||||
{!! $contentHtml !!}
|
||||
</div>
|
||||
@stack('theme:post_content_bottom')
|
||||
|
||||
@if($post->tags->isNotEmpty())
|
||||
<div class="post-tags">
|
||||
@@ -86,5 +88,6 @@
|
||||
</div>{{-- #page --}}
|
||||
@include('partials.footer')
|
||||
<script>fetch("{{ route('track-view', $post) }}")</script>
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,5 +17,6 @@
|
||||
@include('partials.sidebar')
|
||||
</div>{{-- #page --}}
|
||||
@include('partials.footer')
|
||||
@stack('theme:body_end')
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user