feat: topbar legacy-sync button, card view mode with cover fallback, year/month/day archives, richer page cache admin, stylevar repeater editor
- 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
This commit is contained in:
@@ -14,8 +14,14 @@ class ArchiveController extends Controller
|
|||||||
{
|
{
|
||||||
$posts = Post::published()->orderByDesc('published_at')->get(['id', 'title', 'slug', 'published_at']);
|
$posts = Post::published()->orderByDesc('published_at')->get(['id', 'title', 'slug', 'published_at']);
|
||||||
|
|
||||||
$archives = $posts->groupBy(fn (Post $post) => $post->published_at->format('Y'))
|
// 年 → 月(1-12) → 日 → 文章,用于按年/月/日索引
|
||||||
->map(fn ($yearPosts) => $yearPosts->groupBy(fn (Post $post) => $post->published_at->format('m')));
|
$archives = [];
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
$year = $post->published_at->format('Y');
|
||||||
|
$month = (int) $post->published_at->format('m');
|
||||||
|
$day = (int) $post->published_at->format('d');
|
||||||
|
$archives[$year][$month][$day][] = $post;
|
||||||
|
}
|
||||||
|
|
||||||
return theme_view('archives', compact('archives'));
|
return theme_view('archives', compact('archives'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class HomeViewMode
|
|||||||
|
|
||||||
public const FULL = 'full';
|
public const FULL = 'full';
|
||||||
|
|
||||||
|
public const CARD = 'card';
|
||||||
|
|
||||||
public const COOKIE = 'll_home_view';
|
public const COOKIE = 'll_home_view';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,12 +27,13 @@ class HomeViewMode
|
|||||||
return [
|
return [
|
||||||
self::LIST => '列表',
|
self::LIST => '列表',
|
||||||
self::FULL => '标准',
|
self::FULL => '标准',
|
||||||
|
self::CARD => '图文',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function isValid(mixed $value): bool
|
public static function isValid(mixed $value): bool
|
||||||
{
|
{
|
||||||
return is_string($value) && in_array($value, [self::LIST, self::FULL], true);
|
return is_string($value) && in_array($value, [self::LIST, self::FULL, self::CARD], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function resolve(?Request $request = null): string
|
public static function resolve(?Request $request = null): string
|
||||||
|
|||||||
@@ -41,11 +41,12 @@ class PageCacheMiddleware
|
|||||||
$response = $next($request);
|
$response = $next($request);
|
||||||
|
|
||||||
if ($response->getStatusCode() === 200) {
|
if ($response->getStatusCode() === 200) {
|
||||||
|
$body = $response->getContent();
|
||||||
Cache::put($key, [
|
Cache::put($key, [
|
||||||
'body' => $response->getContent(),
|
'body' => $body,
|
||||||
'type' => $response->headers->get('Content-Type') ?: 'text/html; charset=utf-8',
|
'type' => $response->headers->get('Content-Type') ?: 'text/html; charset=utf-8',
|
||||||
], $ttl);
|
], $ttl);
|
||||||
self::rememberKey($key, $request->fullUrl());
|
self::rememberKey($key, $request->fullUrl(), strlen($body), $ttl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
@@ -89,7 +90,7 @@ class PageCacheMiddleware
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<array{key: string, url: string, cached_at: string}>
|
* @return list<array{key: string, url: string, cached_at: string, size: int, expires_at: string}>
|
||||||
*/
|
*/
|
||||||
public static function entries(): array
|
public static function entries(): array
|
||||||
{
|
{
|
||||||
@@ -100,6 +101,8 @@ class PageCacheMiddleware
|
|||||||
'key' => (string) $key,
|
'key' => (string) $key,
|
||||||
'url' => is_array($meta) ? (string) ($meta['url'] ?? $key) : (string) $key,
|
'url' => is_array($meta) ? (string) ($meta['url'] ?? $key) : (string) $key,
|
||||||
'cached_at' => is_array($meta) ? (string) ($meta['cached_at'] ?? '') : '',
|
'cached_at' => is_array($meta) ? (string) ($meta['cached_at'] ?? '') : '',
|
||||||
|
'size' => is_array($meta) ? (int) ($meta['size'] ?? 0) : 0,
|
||||||
|
'expires_at' => is_array($meta) ? (string) ($meta['expires_at'] ?? '') : '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +125,7 @@ class PageCacheMiddleware
|
|||||||
Cache::put($key, (int) Cache::get($key, 0) + 1, now()->addDay());
|
Cache::put($key, (int) Cache::get($key, 0) + 1, now()->addDay());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function rememberKey(string $key, ?string $url = null): void
|
private static function rememberKey(string $key, ?string $url = null, int $size = 0, int $ttl = 0): void
|
||||||
{
|
{
|
||||||
$keys = (array) Cache::get(self::TAG.'_keys', []);
|
$keys = (array) Cache::get(self::TAG.'_keys', []);
|
||||||
$previous = $keys[$key] ?? [];
|
$previous = $keys[$key] ?? [];
|
||||||
@@ -130,6 +133,8 @@ class PageCacheMiddleware
|
|||||||
$keys[$key] = [
|
$keys[$key] = [
|
||||||
'url' => $url ?: (is_array($previous) ? (string) ($previous['url'] ?? '') : ''),
|
'url' => $url ?: (is_array($previous) ? (string) ($previous['url'] ?? '') : ''),
|
||||||
'cached_at' => now()->toDateTimeString(),
|
'cached_at' => now()->toDateTimeString(),
|
||||||
|
'size' => $size,
|
||||||
|
'expires_at' => $ttl > 0 ? now()->addSeconds($ttl)->toDateTimeString() : '',
|
||||||
];
|
];
|
||||||
|
|
||||||
Cache::put(self::TAG.'_keys', $keys, now()->addDay());
|
Cache::put(self::TAG.'_keys', $keys, now()->addDay());
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ class SablogImport extends Command
|
|||||||
{
|
{
|
||||||
$count = 0;
|
$count = 0;
|
||||||
foreach ($this->rows('categories') as $row) {
|
foreach ($this->rows('categories') as $row) {
|
||||||
|
$row['name'] = html_entity_decode((string) $row['name'], ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
$slug = SlugGenerator::make($row['name'], 'categories', 'slug', ignoreId: (int) $row['cid'], fallback: 'category-'.$row['cid']);
|
$slug = SlugGenerator::make($row['name'], 'categories', 'slug', ignoreId: (int) $row['cid'], fallback: 'category-'.$row['cid']);
|
||||||
DB::table('categories')->updateOrInsert(['id' => (int) $row['cid']], [
|
DB::table('categories')->updateOrInsert(['id' => (int) $row['cid']], [
|
||||||
'name' => $row['name'],
|
'name' => $row['name'],
|
||||||
@@ -291,7 +292,7 @@ class SablogImport extends Command
|
|||||||
DB::table('posts')->updateOrInsert(['id' => (int) $row['articleid']], [
|
DB::table('posts')->updateOrInsert(['id' => (int) $row['articleid']], [
|
||||||
'category_id' => $categoryId,
|
'category_id' => $categoryId,
|
||||||
'user_id' => (int) $row['uid'] ?: null,
|
'user_id' => (int) $row['uid'] ?: null,
|
||||||
'title' => $row['title'],
|
'title' => html_entity_decode((string) $row['title'], ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||||
'slug' => $slug,
|
'slug' => $slug,
|
||||||
'excerpt' => $row['description'] ?: null,
|
'excerpt' => $row['description'] ?: null,
|
||||||
'content' => $row['content'],
|
'content' => $row['content'],
|
||||||
@@ -323,6 +324,7 @@ class SablogImport extends Command
|
|||||||
// 而不是 sablog_tags.aids(那个映射是错的/过时的)
|
// 而不是 sablog_tags.aids(那个映射是错的/过时的)
|
||||||
foreach ($this->rows('articles') as $row) {
|
foreach ($this->rows('articles') as $row) {
|
||||||
$keywords = trim((string) ($row['keywords'] ?? ''));
|
$keywords = trim((string) ($row['keywords'] ?? ''));
|
||||||
|
$keywords = html_entity_decode($keywords, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
if ($keywords === '') {
|
if ($keywords === '') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -371,7 +373,7 @@ class SablogImport extends Command
|
|||||||
foreach ($this->rows('comments') as $row) {
|
foreach ($this->rows('comments') as $row) {
|
||||||
DB::table('comments')->updateOrInsert(['id' => (int) $row['commentid']], [
|
DB::table('comments')->updateOrInsert(['id' => (int) $row['commentid']], [
|
||||||
'post_id' => (int) $row['articleid'],
|
'post_id' => (int) $row['articleid'],
|
||||||
'author_name' => $row['author'] ?: '匿名',
|
'author_name' => html_entity_decode((string) ($row['author'] ?: '匿名'), ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||||
'author_url' => $row['url'] ?: null,
|
'author_url' => $row['url'] ?: null,
|
||||||
'content' => $row['content'],
|
'content' => $row['content'],
|
||||||
'ip' => $row['ipaddress'] ?: null,
|
'ip' => $row['ipaddress'] ?: null,
|
||||||
@@ -389,7 +391,7 @@ class SablogImport extends Command
|
|||||||
$count = 0;
|
$count = 0;
|
||||||
foreach ($this->rows('links') as $row) {
|
foreach ($this->rows('links') as $row) {
|
||||||
DB::table('links')->updateOrInsert(['id' => (int) $row['linkid']], [
|
DB::table('links')->updateOrInsert(['id' => (int) $row['linkid']], [
|
||||||
'name' => $row['name'],
|
'name' => html_entity_decode((string) $row['name'], ENT_QUOTES | ENT_HTML5, 'UTF-8'),
|
||||||
'url' => $row['url'],
|
'url' => $row['url'],
|
||||||
'note' => $row['note'] ?: null,
|
'note' => $row['note'] ?: null,
|
||||||
'display_order' => (int) $row['displayorder'],
|
'display_order' => (int) $row['displayorder'],
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use App\Blog\Support\ThemeInjector;
|
|||||||
use App\Blog\Support\ThemeManager;
|
use App\Blog\Support\ThemeManager;
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\KeyValue;
|
|
||||||
use Filament\Forms\Components\Repeater;
|
use Filament\Forms\Components\Repeater;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
@@ -38,9 +37,15 @@ class ThemeInject extends Page
|
|||||||
|
|
||||||
public function mount(): void
|
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([
|
$this->form->fill([
|
||||||
'inject' => $this->normalizeBlocks(Setting::get('inject.blocks', [])),
|
'inject' => $this->normalizeBlocks(Setting::get('inject.blocks', [])),
|
||||||
'theme_vars' => app(ThemeManager::class)->variables(),
|
'theme_vars' => $varRows,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +85,23 @@ class ThemeInject extends Page
|
|||||||
|
|
||||||
$tabs[] = Tab::make('主题变量(stylevar)')
|
$tabs[] = Tab::make('主题变量(stylevar)')
|
||||||
->schema([
|
->schema([
|
||||||
KeyValue::make('theme_vars')
|
Repeater::make('theme_vars')
|
||||||
->label('当前主题变量:视图用 {{ theme(\'var\', \'key\') }} 引用')
|
->label('当前主题变量:视图用 {{ theme(\'var\', \'key\') }} 引用')
|
||||||
->keyLabel('变量名')
|
->schema([
|
||||||
->valueLabel('内容(HTML)')
|
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()
|
->reorderable()
|
||||||
|
->reorderableWithButtons()
|
||||||
|
->addActionLabel('添加变量')
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -118,7 +135,16 @@ class ThemeInject extends Page
|
|||||||
}
|
}
|
||||||
|
|
||||||
Setting::set('inject.blocks', $blocks);
|
Setting::set('inject.blocks', $blocks);
|
||||||
Setting::set('theme_vars_'.app(ThemeManager::class)->active(), $data['theme_vars'] ?? []);
|
|
||||||
|
$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();
|
\App\Blog\Support\PageCacheMiddleware::flush();
|
||||||
|
|
||||||
Notification::make()->title('注入内容已保存')->success()->send();
|
Notification::make()->title('注入内容已保存')->success()->send();
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Blog\Support\PageCacheMiddleware;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶栏「同步旧站」按钮:点击后对 sablog 老库做增量导入(幂等,不产生重复)。
|
||||||
|
*/
|
||||||
|
class LegacySyncButton extends Component
|
||||||
|
{
|
||||||
|
public bool $running = false;
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->running = true;
|
||||||
|
|
||||||
|
$src = config('blog.sablog_source');
|
||||||
|
|
||||||
|
try {
|
||||||
|
Artisan::call('sablog:import', [
|
||||||
|
'--host' => $src['host'],
|
||||||
|
'--port' => $src['port'],
|
||||||
|
'--database' => $src['database'],
|
||||||
|
'--username' => $src['username'],
|
||||||
|
'--password' => $src['password'],
|
||||||
|
'--prefix' => $src['prefix'],
|
||||||
|
]);
|
||||||
|
$output = trim(Artisan::output());
|
||||||
|
|
||||||
|
PageCacheMiddleware::flush();
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('旧站数据同步完成')
|
||||||
|
->body(Str::afterLast($output, '报告:') ?: $output)
|
||||||
|
->success()
|
||||||
|
->persistent()
|
||||||
|
->send();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Notification::make()
|
||||||
|
->title('同步失败')
|
||||||
|
->body($e->getMessage())
|
||||||
|
->danger()
|
||||||
|
->persistent()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): \Illuminate\Contracts\View\View
|
||||||
|
{
|
||||||
|
return view('livewire.legacy-sync-button');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -133,6 +133,35 @@ class Post extends Model implements HasMedia
|
|||||||
return $cover->getUrl($conversion) ?: $cover->getUrl();
|
return $cover->getUrl($conversion) ?: $cover->getUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 封面回退链:显式封面 → 附件/正文里的第一张图。
|
||||||
|
* 都没有时返回 null,由调用方决定兜底(占位图或不显示)。
|
||||||
|
*/
|
||||||
|
public function coverOrFirstImageUrl(): ?string
|
||||||
|
{
|
||||||
|
if ($url = $this->coverImageUrl()) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 附件里的第一张图片
|
||||||
|
$attachment = $this->getMedia('attachments')
|
||||||
|
->first(fn ($m) => str_starts_with((string) $m->mime_type, 'image/'));
|
||||||
|
if ($attachment) {
|
||||||
|
return $attachment->getUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正文 HTML/Markdown 里的第一张图片地址
|
||||||
|
$content = (string) $this->content;
|
||||||
|
if (preg_match('/<img[^>]+src=["\']([^"\']+)["\']/i', $content, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
if (preg_match('/!\[[^\]]*\]\((https?:\/\/[^)\s]+)[^)]*\)/i', $content, $m)) {
|
||||||
|
return $m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public function comments(): HasMany
|
public function comments(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Comment::class);
|
return $this->hasMany(Comment::class);
|
||||||
|
|||||||
@@ -48,41 +48,6 @@ class AdminPanelProvider extends PanelProvider
|
|||||||
->sidebarWidth('13rem')
|
->sidebarWidth('13rem')
|
||||||
->maxContentWidth(\Filament\Support\Enums\Width::Full)
|
->maxContentWidth(\Filament\Support\Enums\Width::Full)
|
||||||
->userMenuItems([
|
->userMenuItems([
|
||||||
// 工具:同步旧站数据(sablog 增量导入,约 1 分钟)
|
|
||||||
\Filament\Actions\Action::make('sablog-import')
|
|
||||||
->label('同步旧站数据')
|
|
||||||
->icon('heroicon-o-arrow-path')
|
|
||||||
->color('info')
|
|
||||||
->requiresConfirmation()
|
|
||||||
->modalDescription('从旧 sablog 库增量导入文章/评论/标签等(幂等,不会产生重复),约需 1 分钟,期间请不要重复点击。')
|
|
||||||
->action(function (): void {
|
|
||||||
$src = config('blog.sablog_source');
|
|
||||||
try {
|
|
||||||
\Illuminate\Support\Facades\Artisan::call('sablog:import', [
|
|
||||||
'--host' => $src['host'],
|
|
||||||
'--port' => $src['port'],
|
|
||||||
'--database' => $src['database'],
|
|
||||||
'--username' => $src['username'],
|
|
||||||
'--password' => $src['password'],
|
|
||||||
'--prefix' => $src['prefix'],
|
|
||||||
]);
|
|
||||||
$output = trim(\Illuminate\Support\Facades\Artisan::output());
|
|
||||||
\App\Blog\Support\PageCacheMiddleware::flush();
|
|
||||||
\Filament\Notifications\Notification::make()
|
|
||||||
->title('旧站数据同步完成')
|
|
||||||
->body(\Illuminate\Support\Str::afterLast($output, '报告:') ?: $output)
|
|
||||||
->success()
|
|
||||||
->persistent()
|
|
||||||
->send();
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
\Filament\Notifications\Notification::make()
|
|
||||||
->title('同步失败')
|
|
||||||
->body($e->getMessage())
|
|
||||||
->danger()
|
|
||||||
->persistent()
|
|
||||||
->send();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// 工具:清空缓存(Livewire action,即时通知)
|
// 工具:清空缓存(Livewire action,即时通知)
|
||||||
\Filament\Actions\Action::make('flush-page-cache')
|
\Filament\Actions\Action::make('flush-page-cache')
|
||||||
->label('清空前台缓存')
|
->label('清空前台缓存')
|
||||||
|
|||||||
@@ -102,6 +102,90 @@ a:hover {
|
|||||||
padding: 18px 20px;
|
padding: 18px 20px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
/* 列表模式:纯标题列表,紧凑 */
|
||||||
|
.post-list-list .post-card {
|
||||||
|
padding: 8px 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.post-list-list .post-card:last-child {
|
||||||
|
border-bottom: 1px solid #d8dfe6;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.post-list-list .post-card .post-title {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.post-list-list .post-card .post-meta {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
/* 图文模式:卡片网格 */
|
||||||
|
.post-list-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.post-list-card .post-card {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.post-list-card .post-cover {
|
||||||
|
display: block;
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
background: #eef2f5;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.post-list-card .post-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.post-cover-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
color: #7a8a99;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #eef2f5 0%, #dde7ee 100%);
|
||||||
|
}
|
||||||
|
.post-list-card .post-card-body {
|
||||||
|
padding: 12px 14px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.post-list-card .post-title {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.post-list-card .post-excerpt {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.post-list-card {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.post-list-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-card.sticky {
|
.post-card.sticky {
|
||||||
border-left: 4px solid #f60;
|
border-left: 4px solid #f60;
|
||||||
}
|
}
|
||||||
@@ -347,7 +431,7 @@ a:hover {
|
|||||||
|
|
||||||
/* 兜底页面(归档/标签云/友链/评论/登录等使用默认布局) */
|
/* 兜底页面(归档/标签云/友链/评论/登录等使用默认布局) */
|
||||||
.container.main-layout {
|
.container.main-layout {
|
||||||
max-width: 960px;
|
max-width: 1380px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
.main-layout {
|
.main-layout {
|
||||||
@@ -452,6 +536,63 @@ a:hover {
|
|||||||
margin: 4px 0 10px 20px;
|
margin: 4px 0 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 年/月/日索引归档 */
|
||||||
|
.archive-months {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.archive-month {
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.archive-month.is-empty {
|
||||||
|
opacity: .45;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.archive-month-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.archive-month.has-posts .archive-month-title:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.archive-month-title .count {
|
||||||
|
color: #999;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.archive-days {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.archive-day {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 22px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #e8f0f8;
|
||||||
|
color: #1c62b0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.archive-day:hover {
|
||||||
|
background: #1c62b0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.archive-months {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.links-list {
|
.links-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,32 @@
|
|||||||
@include('partials.theme-inject', ['point' => 'content_top'])
|
@include('partials.theme-inject', ['point' => 'content_top'])
|
||||||
|
|
||||||
@forelse($archives as $year => $months)
|
@forelse($archives as $year => $months)
|
||||||
<h2 class="archive-year">{{ $year }} 年</h2>
|
<section class="archive-year-block">
|
||||||
<ul class="archive-list">
|
<h2 class="archive-year">{{ $year }} 年</h2>
|
||||||
@foreach($months as $month => $monthPosts)
|
<div class="archive-months">
|
||||||
<li>
|
@foreach(range(1, 12) as $month)
|
||||||
<a wire:navigate href="{{ route('archives.month', $year.$month) }}">{{ (int) $month }} 月</a>
|
@php $days = $months[$month] ?? []; @endphp
|
||||||
<span class="count">({{ $monthPosts->count() }} 篇)</span>
|
<div class="archive-month {{ count($days) ? 'has-posts' : 'is-empty' }}">
|
||||||
<ul class="archive-posts">
|
@if(count($days))
|
||||||
@foreach($monthPosts as $post)
|
<a wire:navigate class="archive-month-title" href="{{ route('archives.month', $year.str_pad((string) $month, 2, '0', STR_PAD_LEFT)) }}">
|
||||||
<li><a wire:navigate href="{{ route('posts.show', $post->id) }}">{{ $post->title }}</a> <time>{{ $post->published_at->format(blog_setting('listtime', 'm-d')) }}</time></li>
|
{{ $month }} 月 <span class="count">({{ collect($days)->flatten(1)->count() }})</span>
|
||||||
@endforeach
|
</a>
|
||||||
</ul>
|
<div class="archive-days">
|
||||||
</li>
|
@foreach($days as $day => $dayPosts)
|
||||||
@endforeach
|
@if(count($dayPosts) === 1)
|
||||||
</ul>
|
<a wire:navigate href="{{ route('posts.show', $dayPosts[0]->id) }}" class="archive-day" title="{{ $dayPosts[0]->title }}">{{ $day }}</a>
|
||||||
|
@else
|
||||||
|
<a wire:navigate href="{{ route('archives.month', $year.str_pad((string) $month, 2, '0', STR_PAD_LEFT)) }}" class="archive-day" title="{{ implode('、', array_map(fn ($p) => $p->title, $dayPosts)) }}">{{ $day }}</a>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<span class="archive-month-title">{{ $month }} 月</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@empty
|
@empty
|
||||||
<div class="empty">暂无归档</div>
|
<div class="empty">暂无归档</div>
|
||||||
@endforelse
|
@endforelse
|
||||||
|
|||||||
@@ -31,4 +31,6 @@
|
|||||||
>
|
>
|
||||||
写文章
|
写文章
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
|
|
||||||
|
<livewire:legacy-sync-button />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,7 +31,9 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-gray-200 text-left dark:border-white/10">
|
<tr class="border-b border-gray-200 text-left dark:border-white/10">
|
||||||
<th class="py-2 pr-4 font-medium">地址</th>
|
<th class="py-2 pr-4 font-medium">地址</th>
|
||||||
|
<th class="py-2 pr-4 font-medium">大小</th>
|
||||||
<th class="py-2 pr-4 font-medium">缓存时间</th>
|
<th class="py-2 pr-4 font-medium">缓存时间</th>
|
||||||
|
<th class="py-2 pr-4 font-medium">过期时间</th>
|
||||||
<th class="py-2 font-medium">操作</th>
|
<th class="py-2 font-medium">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -43,7 +45,9 @@
|
|||||||
{{ $entry['url'] }}
|
{{ $entry['url'] }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="py-2 pr-4 whitespace-nowrap">{{ $entry['size'] ? number_format($entry['size'] / 1024, 1).' KB' : '—' }}</td>
|
||||||
<td class="py-2 pr-4 whitespace-nowrap">{{ $entry['cached_at'] ?: '—' }}</td>
|
<td class="py-2 pr-4 whitespace-nowrap">{{ $entry['cached_at'] ?: '—' }}</td>
|
||||||
|
<td class="py-2 pr-4 whitespace-nowrap">{{ $entry['expires_at'] ?: '—' }}</td>
|
||||||
<td class="py-2">
|
<td class="py-2">
|
||||||
<x-filament::button
|
<x-filament::button
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -58,6 +62,10 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
共 {{ count($this->entries) }} 条,合计 {{ number_format(collect($this->entries)->sum('size') / 1024 / 1024, 2) }} MB。
|
||||||
|
缓存说明:只缓存未登录访客 GET 的 200 页面;登录用户、评论提交等动态请求不缓存;浏览量统计不受影响。
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|||||||
@@ -14,11 +14,13 @@
|
|||||||
@include('partials.view-mode-switcher')
|
@include('partials.view-mode-switcher')
|
||||||
@include('partials.theme-inject', ['point' => 'content_top'])
|
@include('partials.theme-inject', ['point' => 'content_top'])
|
||||||
|
|
||||||
@forelse($posts as $post)
|
<div class="post-list post-list-{{ $homeViewMode ?? 'list' }}">
|
||||||
@include('partials.post-card', ['post' => $post])
|
@forelse($posts as $post)
|
||||||
@empty
|
@include('partials.post-card', ['post' => $post])
|
||||||
<div class="empty">暂无文章</div>
|
@empty
|
||||||
@endforelse
|
<div class="empty">暂无文章</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
@include('partials.pagination', ['paginator' => $posts])
|
@include('partials.pagination', ['paginator' => $posts])
|
||||||
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
||||||
|
|||||||
@@ -27,11 +27,13 @@
|
|||||||
@include('partials.view-mode-switcher')
|
@include('partials.view-mode-switcher')
|
||||||
@include('partials.theme-inject', ['point' => 'content_top'])
|
@include('partials.theme-inject', ['point' => 'content_top'])
|
||||||
|
|
||||||
@forelse($posts as $post)
|
<div class="post-list post-list-{{ $homeViewMode ?? 'list' }}">
|
||||||
@include('partials.post-card', ['post' => $post])
|
@forelse($posts as $post)
|
||||||
@empty
|
@include('partials.post-card', ['post' => $post])
|
||||||
<div class="empty">该分类暂无文章</div>
|
@empty
|
||||||
@endforelse
|
<div class="empty">该分类暂无文章</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
@include('partials.pagination', ['paginator' => $posts])
|
@include('partials.pagination', ['paginator' => $posts])
|
||||||
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<div>
|
||||||
|
<x-filament::button
|
||||||
|
size="sm"
|
||||||
|
color="info"
|
||||||
|
icon="heroicon-o-arrow-path"
|
||||||
|
wire:click="run"
|
||||||
|
wire:loading.attr="disabled"
|
||||||
|
wire:confirm="将从旧 sablog 库增量导入文章/评论/标签等(幂等,不会产生重复),约需 1 分钟。确定开始?"
|
||||||
|
>
|
||||||
|
<span wire:loading.remove wire:target="run">同步旧站</span>
|
||||||
|
<span wire:loading wire:target="run">同步中…</span>
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
@@ -4,38 +4,78 @@
|
|||||||
&& blank($post->read_password)
|
&& blank($post->read_password)
|
||||||
&& empty(($post->meta ?? [])['members_only']);
|
&& empty(($post->meta ?? [])['members_only']);
|
||||||
@endphp
|
@endphp
|
||||||
<article class="post-card {{ $post->isPinned() ? 'sticky' : '' }} post-card-{{ $viewMode }}">
|
@if($viewMode === \App\Blog\Support\HomeViewMode::CARD)
|
||||||
@if($url = $post->coverImageUrl())
|
<article class="post-card post-card-card {{ $post->isPinned() ? 'sticky' : '' }}">
|
||||||
<a wire:navigate href="{{ route('posts.show', $post->id) }}" class="post-cover">
|
<a wire:navigate href="{{ route('posts.show', $post->id) }}" class="post-cover">
|
||||||
<img src="{{ $url }}" alt="{{ $post->title }}" loading="lazy">
|
@if($url = $post->coverOrFirstImageUrl())
|
||||||
|
<img src="{{ $url }}" alt="{{ $post->title }}" loading="lazy">
|
||||||
|
@else
|
||||||
|
<span class="post-cover-placeholder">{{ \Illuminate\Support\Str::limit($post->title, 24, '') }}</span>
|
||||||
|
@endif
|
||||||
</a>
|
</a>
|
||||||
@endif
|
<div class="post-card-body">
|
||||||
<h2 class="post-title">
|
<h2 class="post-title">
|
||||||
@if($label = $post->stickyLabel())<span class="badge">{{ $label }}</span>@endif
|
@if($label = $post->stickyLabel())<span class="badge">{{ $label }}</span>@endif
|
||||||
<a wire:navigate href="{{ route('posts.show', $post->id) }}">{{ $post->title }}</a>
|
<a wire:navigate href="{{ route('posts.show', $post->id) }}">{{ $post->title }}</a>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="post-meta">
|
<div class="post-excerpt">{{ \Illuminate\Support\Str::limit($post->excerpt_or_fallback, 80) }}</div>
|
||||||
<time datetime="{{ $post->published_at->toIso8601String() }}">{{ $post->published_at->format(blog_setting('normaltime', 'Y-m-d H:i')) }}</time>
|
<div class="post-meta">
|
||||||
@if($post->category)
|
<time datetime="{{ $post->published_at->toIso8601String() }}">{{ $post->published_at->format('Y-m-d') }}</time>
|
||||||
<span>·</span>
|
@if($post->category)
|
||||||
<a wire:navigate href="{{ route('category.show', $post->category->id) }}">{{ $post->category->name }}</a>
|
<span>·</span>
|
||||||
|
<a wire:navigate href="{{ route('category.show', $post->category->id) }}">{{ $post->category->name }}</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@elseif($viewMode === \App\Blog\Support\HomeViewMode::LIST)
|
||||||
|
<article class="post-card post-card-list {{ $post->isPinned() ? 'sticky' : '' }}">
|
||||||
|
<h2 class="post-title">
|
||||||
|
@if($label = $post->stickyLabel())<span class="badge">{{ $label }}</span>@endif
|
||||||
|
<a wire:navigate href="{{ route('posts.show', $post->id) }}">{{ $post->title }}</a>
|
||||||
|
</h2>
|
||||||
|
<div class="post-meta">
|
||||||
|
<time datetime="{{ $post->published_at->toIso8601String() }}">{{ $post->published_at->format(blog_setting('listtime', 'Y-m-d')) }}</time>
|
||||||
|
@if($post->category)
|
||||||
|
<span>·</span>
|
||||||
|
<a wire:navigate href="{{ route('category.show', $post->category->id) }}">{{ $post->category->name }}</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@else
|
||||||
|
<article class="post-card {{ $post->isPinned() ? 'sticky' : '' }} post-card-{{ $viewMode }}">
|
||||||
|
@if($url = $post->coverImageUrl())
|
||||||
|
<a wire:navigate href="{{ route('posts.show', $post->id) }}" class="post-cover">
|
||||||
|
<img src="{{ $url }}" alt="{{ $post->title }}" loading="lazy">
|
||||||
|
</a>
|
||||||
@endif
|
@endif
|
||||||
<span>· 阅读 {{ $post->views }}</span>
|
<h2 class="post-title">
|
||||||
<span>· 评论 {{ $post->comment_count }}</span>
|
@if($label = $post->stickyLabel())<span class="badge">{{ $label }}</span>@endif
|
||||||
</div>
|
<a wire:navigate href="{{ route('posts.show', $post->id) }}">{{ $post->title }}</a>
|
||||||
@if($showFull)
|
</h2>
|
||||||
<div class="post-content">
|
<div class="post-meta">
|
||||||
{!! app(\App\Blog\Services\PostContentRenderer::class)->render($post) !!}
|
<time datetime="{{ $post->published_at->toIso8601String() }}">{{ $post->published_at->format(blog_setting('normaltime', 'Y-m-d H:i')) }}</time>
|
||||||
|
@if($post->category)
|
||||||
|
<span>·</span>
|
||||||
|
<a wire:navigate href="{{ route('category.show', $post->category->id) }}">{{ $post->category->name }}</a>
|
||||||
|
@endif
|
||||||
|
<span>· 阅读 {{ $post->views }}</span>
|
||||||
|
<span>· 评论 {{ $post->comment_count }}</span>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@if($showFull)
|
||||||
<div class="post-excerpt">
|
<div class="post-content">
|
||||||
{{ \Illuminate\Support\Str::limit($post->excerpt_or_fallback, 200) }}
|
{!! app(\App\Blog\Services\PostContentRenderer::class)->render($post) !!}
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="post-excerpt">
|
||||||
|
{{ \Illuminate\Support\Str::limit($post->excerpt_or_fallback, 200) }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="post-footer">
|
||||||
|
<a wire:navigate href="{{ route('posts.show', $post->id) }}" class="read-more">阅读全文 »</a>
|
||||||
|
@foreach($post->tags as $tag)
|
||||||
|
<a wire:navigate href="{{ route('tags.show', $tag->name) }}" class="tag-link">#{{ $tag->name }}</a>
|
||||||
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@endif
|
</article>
|
||||||
<div class="post-footer">
|
@endif
|
||||||
<a wire:navigate href="{{ route('posts.show', $post->id) }}" class="read-more">阅读全文 »</a>
|
|
||||||
@foreach($post->tags as $tag)
|
|
||||||
<a wire:navigate href="{{ route('tags.show', $tag->name) }}" class="tag-link">#{{ $tag->name }}</a>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|||||||
@@ -6,4 +6,6 @@
|
|||||||
<a href="{{ \App\Blog\Support\HomeViewMode::url(\App\Blog\Support\HomeViewMode::FULL) }}" class="{{ $viewMode === \App\Blog\Support\HomeViewMode::FULL ? 'active' : '' }}">标准</a>
|
<a href="{{ \App\Blog\Support\HomeViewMode::url(\App\Blog\Support\HomeViewMode::FULL) }}" class="{{ $viewMode === \App\Blog\Support\HomeViewMode::FULL ? 'active' : '' }}">标准</a>
|
||||||
<span>|</span>
|
<span>|</span>
|
||||||
<a href="{{ \App\Blog\Support\HomeViewMode::url(\App\Blog\Support\HomeViewMode::LIST) }}" class="{{ $viewMode === \App\Blog\Support\HomeViewMode::LIST ? 'active' : '' }}">列表</a>
|
<a href="{{ \App\Blog\Support\HomeViewMode::url(\App\Blog\Support\HomeViewMode::LIST) }}" class="{{ $viewMode === \App\Blog\Support\HomeViewMode::LIST ? 'active' : '' }}">列表</a>
|
||||||
|
<span>|</span>
|
||||||
|
<a href="{{ \App\Blog\Support\HomeViewMode::url(\App\Blog\Support\HomeViewMode::CARD) }}" class="{{ $viewMode === \App\Blog\Support\HomeViewMode::CARD ? 'active' : '' }}">图文</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,11 +13,13 @@
|
|||||||
@include('partials.view-mode-switcher')
|
@include('partials.view-mode-switcher')
|
||||||
@include('partials.theme-inject', ['point' => 'content_top'])
|
@include('partials.theme-inject', ['point' => 'content_top'])
|
||||||
|
|
||||||
@forelse($posts as $post)
|
<div class="post-list post-list-{{ $homeViewMode ?? 'list' }}">
|
||||||
@include('partials.post-card', ['post' => $post])
|
@forelse($posts as $post)
|
||||||
@empty
|
@include('partials.post-card', ['post' => $post])
|
||||||
<div class="empty">没有找到相关文章</div>
|
@empty
|
||||||
@endforelse
|
<div class="empty">没有找到相关文章</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
@include('partials.pagination', ['paginator' => $posts])
|
@include('partials.pagination', ['paginator' => $posts])
|
||||||
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
@include('partials.view-mode-switcher')
|
@include('partials.view-mode-switcher')
|
||||||
@include('partials.theme-inject', ['point' => 'content_top'])
|
@include('partials.theme-inject', ['point' => 'content_top'])
|
||||||
|
|
||||||
@forelse($posts as $post)
|
<div class="post-list post-list-{{ $homeViewMode ?? 'list' }}">
|
||||||
@include('partials.post-card', ['post' => $post])
|
@forelse($posts as $post)
|
||||||
@empty
|
@include('partials.post-card', ['post' => $post])
|
||||||
<div class="empty">该标签暂无文章</div>
|
@empty
|
||||||
@endforelse
|
<div class="empty">该标签暂无文章</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
@include('partials.pagination', ['paginator' => $posts])
|
@include('partials.pagination', ['paginator' => $posts])
|
||||||
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
||||||
|
|||||||
@@ -102,6 +102,90 @@ a:hover {
|
|||||||
padding: 18px 20px;
|
padding: 18px 20px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
/* 列表模式:纯标题列表,紧凑 */
|
||||||
|
.post-list-list .post-card {
|
||||||
|
padding: 8px 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.post-list-list .post-card:last-child {
|
||||||
|
border-bottom: 1px solid #d8dfe6;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.post-list-list .post-card .post-title {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.post-list-list .post-card .post-meta {
|
||||||
|
display: inline;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
/* 图文模式:卡片网格 */
|
||||||
|
.post-list-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.post-list-card .post-card {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.post-list-card .post-cover {
|
||||||
|
display: block;
|
||||||
|
aspect-ratio: 16 / 10;
|
||||||
|
background: #eef2f5;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.post-list-card .post-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.post-cover-placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
color: #7a8a99;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #eef2f5 0%, #dde7ee 100%);
|
||||||
|
}
|
||||||
|
.post-list-card .post-card-body {
|
||||||
|
padding: 12px 14px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.post-list-card .post-title {
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.post-list-card .post-excerpt {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.post-list-card {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.post-list-card {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.post-card.sticky {
|
.post-card.sticky {
|
||||||
border-left: 4px solid #f60;
|
border-left: 4px solid #f60;
|
||||||
}
|
}
|
||||||
@@ -452,6 +536,63 @@ a:hover {
|
|||||||
margin: 4px 0 10px 20px;
|
margin: 4px 0 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 年/月/日索引归档 */
|
||||||
|
.archive-months {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.archive-month {
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.archive-month.is-empty {
|
||||||
|
opacity: .45;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.archive-month-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.archive-month.has-posts .archive-month-title:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.archive-month-title .count {
|
||||||
|
color: #999;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.archive-days {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.archive-day {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 22px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #e8f0f8;
|
||||||
|
color: #1c62b0;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.archive-day:hover {
|
||||||
|
background: #1c62b0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.archive-months {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.links-list {
|
.links-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,13 @@
|
|||||||
@include('partials.view-mode-switcher')
|
@include('partials.view-mode-switcher')
|
||||||
@include('partials.theme-inject', ['point' => 'content_top'])
|
@include('partials.theme-inject', ['point' => 'content_top'])
|
||||||
|
|
||||||
@forelse($posts as $post)
|
<div class="post-list post-list-{{ $homeViewMode ?? 'list' }}">
|
||||||
@include('partials.post-card', ['post' => $post])
|
@forelse($posts as $post)
|
||||||
@empty
|
@include('partials.post-card', ['post' => $post])
|
||||||
<div class="empty">暂无文章</div>
|
@empty
|
||||||
@endforelse
|
<div class="empty">暂无文章</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
@include('partials.pagination', ['paginator' => $posts])
|
@include('partials.pagination', ['paginator' => $posts])
|
||||||
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
||||||
|
|||||||
@@ -27,11 +27,13 @@
|
|||||||
@include('partials.view-mode-switcher')
|
@include('partials.view-mode-switcher')
|
||||||
@include('partials.theme-inject', ['point' => 'content_top'])
|
@include('partials.theme-inject', ['point' => 'content_top'])
|
||||||
|
|
||||||
@forelse($posts as $post)
|
<div class="post-list post-list-{{ $homeViewMode ?? 'list' }}">
|
||||||
@include('partials.post-card', ['post' => $post])
|
@forelse($posts as $post)
|
||||||
@empty
|
@include('partials.post-card', ['post' => $post])
|
||||||
<div class="empty">该分类暂无文章</div>
|
@empty
|
||||||
@endforelse
|
<div class="empty">该分类暂无文章</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
@include('partials.pagination', ['paginator' => $posts])
|
@include('partials.pagination', ['paginator' => $posts])
|
||||||
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
||||||
|
|||||||
@@ -13,11 +13,13 @@
|
|||||||
@include('partials.view-mode-switcher')
|
@include('partials.view-mode-switcher')
|
||||||
@include('partials.theme-inject', ['point' => 'content_top'])
|
@include('partials.theme-inject', ['point' => 'content_top'])
|
||||||
|
|
||||||
@forelse($posts as $post)
|
<div class="post-list post-list-{{ $homeViewMode ?? 'list' }}">
|
||||||
@include('partials.post-card', ['post' => $post])
|
@forelse($posts as $post)
|
||||||
@empty
|
@include('partials.post-card', ['post' => $post])
|
||||||
<div class="empty">没有找到相关文章</div>
|
@empty
|
||||||
@endforelse
|
<div class="empty">没有找到相关文章</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
@include('partials.pagination', ['paginator' => $posts])
|
@include('partials.pagination', ['paginator' => $posts])
|
||||||
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
@include('partials.view-mode-switcher')
|
@include('partials.view-mode-switcher')
|
||||||
@include('partials.theme-inject', ['point' => 'content_top'])
|
@include('partials.theme-inject', ['point' => 'content_top'])
|
||||||
|
|
||||||
@forelse($posts as $post)
|
<div class="post-list post-list-{{ $homeViewMode ?? 'list' }}">
|
||||||
@include('partials.post-card', ['post' => $post])
|
@forelse($posts as $post)
|
||||||
@empty
|
@include('partials.post-card', ['post' => $post])
|
||||||
<div class="empty">该标签暂无文章</div>
|
@empty
|
||||||
@endforelse
|
<div class="empty">该标签暂无文章</div>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
|
||||||
@include('partials.pagination', ['paginator' => $posts])
|
@include('partials.pagination', ['paginator' => $posts])
|
||||||
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
@include('partials.theme-inject', ['point' => 'content_bottom'])
|
||||||
|
|||||||
Reference in New Issue
Block a user