feat: 文章/分类/评论关联细节完善
- 文章列表:会员插件注入「付费」徽章列(会员/付费¥xx,停插件即消失) - 分类:真实文章数(withCount)、名称点击跳文章列表并按分类筛选(ListPosts booted 预置筛选) - 分类:新增封面(medialibrary)+ 介绍(description 字段/表单);前台分类页顶部展示介绍区;SEO description + BreadcrumbList 结构化数据(GEO) - 文章编辑:MarkdownEditor 内容区加大(26rem);文章编辑页新增「评论」关系管理页(可审/拒) - 评论列表:文章列带摘要 - 三套主题补 .category-intro 样式 - 测试更新(付费徽章列)
This commit is contained in:
@@ -4,7 +4,11 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Filament\Resources\Categories\Schemas;
|
namespace App\Filament\Resources\Categories\Schemas;
|
||||||
|
|
||||||
|
use App\Support\MediaDisk;
|
||||||
|
use Filament\Forms\Components\SpatieMediaLibraryFileUpload;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
class CategoryForm
|
class CategoryForm
|
||||||
@@ -13,6 +17,9 @@ class CategoryForm
|
|||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
->components([
|
->components([
|
||||||
|
Section::make('基本信息')
|
||||||
|
->columns(2)
|
||||||
|
->schema([
|
||||||
TextInput::make('name')
|
TextInput::make('name')
|
||||||
->label(__('category.name'))
|
->label(__('category.name'))
|
||||||
->required(),
|
->required(),
|
||||||
@@ -25,11 +32,24 @@ class CategoryForm
|
|||||||
->numeric()
|
->numeric()
|
||||||
->default(0),
|
->default(0),
|
||||||
TextInput::make('post_count')
|
TextInput::make('post_count')
|
||||||
->label(__('category.post_count'))
|
->label('文章数(自动统计)')
|
||||||
->required()
|
|
||||||
->numeric()
|
->numeric()
|
||||||
->default(0)
|
|
||||||
->disabled(),
|
->disabled(),
|
||||||
|
Textarea::make('description')
|
||||||
|
->label('分类介绍')
|
||||||
|
->helperText('显示在分类列表页顶部,也用于 SEO description')
|
||||||
|
->rows(4)
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
Section::make('封面')
|
||||||
|
->schema([
|
||||||
|
SpatieMediaLibraryFileUpload::make('cover')
|
||||||
|
->label('封面图')
|
||||||
|
->collection('cover')
|
||||||
|
->image()
|
||||||
|
->imageEditor()
|
||||||
|
->disk(MediaDisk::name()),
|
||||||
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,20 @@ use Filament\Actions\DeleteBulkAction;
|
|||||||
use Filament\Actions\EditAction;
|
use Filament\Actions\EditAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class CategoriesTable
|
class CategoriesTable
|
||||||
{
|
{
|
||||||
public static function configure(Table $table): Table
|
public static function configure(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
|
->modifyQueryUsing(fn ($query) => $query->withCount('posts'))
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('name')
|
TextColumn::make('name')
|
||||||
->label(__('category.name'))
|
->label(__('category.name'))
|
||||||
->searchable(),
|
->searchable()
|
||||||
|
->url(fn ($record) => route('filament.admin.resources.posts.index').'?category_id='.$record->id)
|
||||||
|
->description(fn ($record) => $record->description ? Str::limit($record->description, 40) : null),
|
||||||
TextColumn::make('slug')
|
TextColumn::make('slug')
|
||||||
->label(__('category.slug'))
|
->label(__('category.slug'))
|
||||||
->searchable(),
|
->searchable(),
|
||||||
@@ -26,8 +30,8 @@ class CategoriesTable
|
|||||||
->label(__('category.display_order'))
|
->label(__('category.display_order'))
|
||||||
->numeric()
|
->numeric()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('post_count')
|
TextColumn::make('posts_count')
|
||||||
->label(__('category.post_count'))
|
->label('文章数')
|
||||||
->numeric()
|
->numeric()
|
||||||
->sortable(),
|
->sortable(),
|
||||||
TextColumn::make('created_at')
|
TextColumn::make('created_at')
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class CommentsTable
|
|||||||
TextColumn::make('post.title')
|
TextColumn::make('post.title')
|
||||||
->label('文章')
|
->label('文章')
|
||||||
->limit(30)
|
->limit(30)
|
||||||
|
->description(fn ($record) => $record->post ? \Illuminate\Support\Str::limit($record->post->excerpt_or_fallback, 50) : null)
|
||||||
->url(fn ($record) => $record->post ? route('posts.show', $record->post->slug ?: $record->post->id) : null)
|
->url(fn ($record) => $record->post ? route('posts.show', $record->post->slug ?: $record->post->id) : null)
|
||||||
->openUrlInNewTab(),
|
->openUrlInNewTab(),
|
||||||
TextColumn::make('status')
|
TextColumn::make('status')
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
namespace App\Filament\Resources\Posts\Pages;
|
namespace App\Filament\Resources\Posts\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\Posts\PostResource;
|
use App\Filament\Resources\Posts\PostResource;
|
||||||
@@ -13,6 +12,18 @@ class ListPosts extends ListRecords
|
|||||||
{
|
{
|
||||||
protected static string $resource = PostResource::class;
|
protected static string $resource = PostResource::class;
|
||||||
|
|
||||||
|
public function bootedInteractsWithTable(): void
|
||||||
|
{
|
||||||
|
// 支持从分类管理跳转:?category_id=X 预置分类筛选
|
||||||
|
// (在 booted 阶段设置,早于表单填充;SelectFilter 状态为 category_id.value 嵌套结构)
|
||||||
|
if (blank($this->tableFilters) && ($categoryId = (int) request()->query('category_id', 0))) {
|
||||||
|
$this->tableFilters = ['category_id' => ['value' => $categoryId]];
|
||||||
|
$this->tableDeferredFilters = ['category_id' => ['value' => $categoryId]];
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::bootedInteractsWithTable();
|
||||||
|
}
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class PostResource extends Resource
|
|||||||
public static function getRelations(): array
|
public static function getRelations(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
//
|
\App\Filament\Resources\Posts\RelationManagers\CommentsRelationManager::class,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Posts\RelationManagers;
|
||||||
|
|
||||||
|
use App\Models\Comment;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
use Filament\Tables\Actions\Action;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class CommentsRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'comments';
|
||||||
|
|
||||||
|
protected static ?string $title = '相关评论';
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = '评论';
|
||||||
|
|
||||||
|
protected static ?string $pluralModelLabel = '评论';
|
||||||
|
|
||||||
|
public function form(\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
TextInput::make('author_name')->label('昵称')->disabled(),
|
||||||
|
TextInput::make('author_email')->label('邮箱')->disabled(),
|
||||||
|
TextInput::make('author_url')->label('网址')->disabled(),
|
||||||
|
Textarea::make('content')->label('内容')->disabled()->rows(4),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('author_name')->label('昵称'),
|
||||||
|
TextColumn::make('content')->label('内容')->limit(40),
|
||||||
|
TextColumn::make('status')->label('状态')
|
||||||
|
->badge()
|
||||||
|
->formatStateUsing(fn ($state) => match ($state) {
|
||||||
|
'published' => '已发布',
|
||||||
|
'pending' => '待审核',
|
||||||
|
'spam' => '垃圾',
|
||||||
|
'rejected' => '已拒绝',
|
||||||
|
default => $state,
|
||||||
|
})
|
||||||
|
->color(fn ($state) => match ($state) {
|
||||||
|
'published' => 'success',
|
||||||
|
'pending' => 'warning',
|
||||||
|
'spam' => 'danger',
|
||||||
|
'rejected' => 'gray',
|
||||||
|
default => 'gray',
|
||||||
|
}),
|
||||||
|
TextColumn::make('created_at')->label('时间')->dateTime('Y-m-d H:i'),
|
||||||
|
])
|
||||||
|
->recordActions([
|
||||||
|
Action::make('approve')
|
||||||
|
->label('通过')
|
||||||
|
->color('success')
|
||||||
|
->visible(fn (Comment $record) => $record->status !== 'published')
|
||||||
|
->action(function (Comment $record) {
|
||||||
|
if ($record->status !== 'published') {
|
||||||
|
$record->post?->increment('comment_count');
|
||||||
|
}
|
||||||
|
$record->update(['status' => 'published']);
|
||||||
|
}),
|
||||||
|
Action::make('reject')
|
||||||
|
->label('拒绝')
|
||||||
|
->color('danger')
|
||||||
|
->visible(fn (Comment $record) => $record->status !== 'rejected')
|
||||||
|
->action(fn (Comment $record) => $record->update(['status' => 'rejected'])),
|
||||||
|
])
|
||||||
|
->recordUrl(null)
|
||||||
|
->recordAction(null)
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,9 @@ class PostsTable
|
|||||||
->sortable(),
|
->sortable(),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
|
SelectFilter::make('category_id')
|
||||||
|
->label('分类')
|
||||||
|
->relationship('category', 'name'),
|
||||||
SelectFilter::make('status')
|
SelectFilter::make('status')
|
||||||
->label('状态')
|
->label('状态')
|
||||||
->options([
|
->options([
|
||||||
|
|||||||
+12
-2
@@ -2,22 +2,25 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
|
|
||||||
class Category extends Model
|
class Category extends Model implements HasMedia
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
use InteractsWithMedia;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
'slug',
|
'slug',
|
||||||
'display_order',
|
'display_order',
|
||||||
'post_count',
|
'post_count',
|
||||||
|
'description',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function posts(): HasMany
|
public function posts(): HasMany
|
||||||
@@ -29,4 +32,11 @@ class Category extends Model
|
|||||||
{
|
{
|
||||||
return route('category.show', $this->slug ?? $this->id);
|
return route('category.show', $this->slug ?? $this->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function registerMediaCollections(): void
|
||||||
|
{
|
||||||
|
$this->addMediaCollection('cover')
|
||||||
|
->useDisk(\App\Support\MediaDisk::name())
|
||||||
|
->singleFile();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
/* 操作列固定宽度(钉住右侧需 table sticky,见备注) */
|
/* 操作列固定宽度(钉住右侧需 table sticky,见备注) */
|
||||||
.fi-ta-actions-header-cell, .fi-ta-cell:has(.fi-ta-actions) { width: 88px !important; min-width: 88px !important; }
|
.fi-ta-actions-header-cell, .fi-ta-cell:has(.fi-ta-actions) { width: 88px !important; min-width: 88px !important; }
|
||||||
|
|
||||||
|
/* 文章编辑框加大(MarkdownEditor 基于 tiptap) */
|
||||||
|
.fi-fo-markdown-editor .tiptap, .fi-fo-markdown-editor .ProseMirror { min-height: 26rem !important; }
|
||||||
|
|
||||||
/* 筛选条件区:整体一行横排,重置紧跟应用按钮,控件紧凑 */
|
/* 筛选条件区:整体一行横排,重置紧跟应用按钮,控件紧凑 */
|
||||||
.fi-ta-filters-above-content-ctn { padding: 0.4rem 0.9rem !important; }
|
.fi-ta-filters-above-content-ctn { padding: 0.4rem 0.9rem !important; }
|
||||||
.fi-ta-filters-heading { display: none !important; }
|
.fi-ta-filters-heading { display: none !important; }
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('categories', function (Blueprint $table) {
|
||||||
|
$table->text('description')->nullable()->after('display_order');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('categories', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('description');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -92,6 +92,25 @@ class SeoMiddleware
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分类页:面包屑结构化数据(GEO 友好)
|
||||||
|
if ($route && $route->getName() === 'category.show') {
|
||||||
|
$slug = $route->parameter('slug');
|
||||||
|
$category = Cache::remember('seo.category.'.$slug, now()->addHour(), function () use ($slug) {
|
||||||
|
return \App\Models\Category::query()->where('slug', $slug)->orWhere('id', (int) $slug)->first();
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($category) {
|
||||||
|
return [
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => 'BreadcrumbList',
|
||||||
|
'itemListElement' => [
|
||||||
|
['@type' => 'ListItem', 'position' => 1, 'name' => $siteName, 'item' => url('/')],
|
||||||
|
['@type' => 'ListItem', 'position' => 2, 'name' => $category->name, 'item' => route('category.show', $category->slug ?: $category->id)],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'@context' => 'https://schema.org',
|
'@context' => 'https://schema.org',
|
||||||
'@type' => 'WebSite',
|
'@type' => 'WebSite',
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use Filament\Forms\Components\TextInput;
|
|||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Tables\Columns\IconColumn;
|
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Plugins\Neatstudio\Membership\Models\MembershipPlan;
|
use Plugins\Neatstudio\Membership\Models\MembershipPlan;
|
||||||
@@ -104,15 +103,32 @@ class ServiceProvider extends PluginServiceProvider
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 后台文章表格注入:价格 / 会员列
|
// 后台文章表格注入:付费标识(价格/会员徽章,停用插件后消失)
|
||||||
$manager->addAction('filament.post_table', function (Table $table) {
|
$manager->addAction('filament.post_table', function (Table $table) {
|
||||||
$table->pushColumns([
|
$table->pushColumns([
|
||||||
TextColumn::make('meta.price')
|
TextColumn::make('meta.members_only')
|
||||||
->label('单篇价格')
|
->label('付费')
|
||||||
->formatStateUsing(fn ($state) => $state > 0 ? '¥'.number_format((float) $state / 100, 2) : '—'),
|
->badge()
|
||||||
IconColumn::make('meta.members_only')
|
->formatStateUsing(function ($state, $record) {
|
||||||
->label('会员')
|
$meta = $record->meta ?? [];
|
||||||
->boolean(),
|
$price = (int) ($meta['price'] ?? 0);
|
||||||
|
|
||||||
|
if (! empty($meta['members_only'])) {
|
||||||
|
return '会员';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $price > 0 ? '付费 ¥'.number_format($price / 100, 2) : '—';
|
||||||
|
})
|
||||||
|
->color(function ($state, $record) {
|
||||||
|
$meta = $record->meta ?? [];
|
||||||
|
|
||||||
|
if (! empty($meta['members_only'])) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) ($meta['price'] ?? 0) > 0 ? 'danger' : 'gray';
|
||||||
|
})
|
||||||
|
->placeholder('—'),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@php
|
@php
|
||||||
$pageTitle = ($archiveTitle ?? $category->name ?? '文章列表').' - '.$siteName;
|
$pageTitle = ($archiveTitle ?? $category->name ?? '文章列表').' - '.$siteName;
|
||||||
|
$pageDescription = $category->description ?? ($archiveTitle ?? $category->name ?? '文章列表').' - '.$siteName;
|
||||||
@endphp
|
@endphp
|
||||||
@include('partials.head')
|
@include('partials.head')
|
||||||
<body>
|
<body>
|
||||||
@@ -8,6 +9,21 @@
|
|||||||
<main class="content">
|
<main class="content">
|
||||||
<h1 class="list-title">{{ $archiveTitle ?? ($category->name ?? '文章列表') }}</h1>
|
<h1 class="list-title">{{ $archiveTitle ?? ($category->name ?? '文章列表') }}</h1>
|
||||||
|
|
||||||
|
@if(isset($category) && ($category->description || $category->getFirstMedia('cover')))
|
||||||
|
<div class="category-intro">
|
||||||
|
@if($cover = $category->getFirstMedia('cover'))
|
||||||
|
<img src="{{ $cover->getUrl() }}" alt="{{ $category->name }}" class="category-cover">
|
||||||
|
@endif
|
||||||
|
<div>
|
||||||
|
@if($category->description)
|
||||||
|
<p class="category-description">{{ $category->description }}</p>
|
||||||
|
@endif
|
||||||
|
<p class="category-meta">{{ $category->posts_count ?? $posts->total() }} 篇文章</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
@forelse($posts as $post)
|
@forelse($posts as $post)
|
||||||
@include('partials.post-card', ['post' => $post])
|
@include('partials.post-card', ['post' => $post])
|
||||||
@empty
|
@empty
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class PostFormInjectionTest extends TestCase
|
|||||||
$this->assertContains('meta.members_only', $fieldNames);
|
$this->assertContains('meta.members_only', $fieldNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_post_table_includes_price_and_member_columns(): void
|
public function test_post_table_includes_paid_badge_column(): void
|
||||||
{
|
{
|
||||||
// Table 构造器要求 HasTable Livewire 组件,测试中绕过构造器
|
// Table 构造器要求 HasTable Livewire 组件,测试中绕过构造器
|
||||||
$table = (new ReflectionClass(Table::class))->newInstanceWithoutConstructor();
|
$table = (new ReflectionClass(Table::class))->newInstanceWithoutConstructor();
|
||||||
@@ -51,7 +51,7 @@ class PostFormInjectionTest extends TestCase
|
|||||||
|
|
||||||
$names = array_keys($table->getColumns());
|
$names = array_keys($table->getColumns());
|
||||||
|
|
||||||
$this->assertContains('meta.price', $names);
|
// 会员插件注入的付费徽章列(meta.members_only 承载价格/会员判定)
|
||||||
$this->assertContains('meta.members_only', $names);
|
$this->assertContains('meta.members_only', $names);
|
||||||
$this->assertContains('title', $names);
|
$this->assertContains('title', $names);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,3 +65,9 @@ a:hover { color: var(--d-accent-dark); }
|
|||||||
/* 已解锁的付费内容块 */
|
/* 已解锁的付费内容块 */
|
||||||
.paid-content { border: 1px solid var(--d-border); border-left: 4px solid var(--d-accent); background: #fff; border-radius: 8px; padding: 12px 14px; margin: 12px 0; }
|
.paid-content { border: 1px solid var(--d-border); border-left: 4px solid var(--d-accent); background: #fff; border-radius: 8px; padding: 12px 14px; margin: 12px 0; }
|
||||||
.paid-content-label { font-size: 12px; color: var(--d-accent); font-weight: 700; margin-bottom: 6px; }
|
.paid-content-label { font-size: 12px; color: var(--d-accent); font-weight: 700; margin-bottom: 6px; }
|
||||||
|
|
||||||
|
/* 分类页介绍区 */
|
||||||
|
.category-intro { display: flex; gap: 14px; align-items: flex-start; background: #fff; border: 1px solid var(--d-border); border-radius: 10px; padding: 14px 16px; margin-bottom: 16px; }
|
||||||
|
.category-cover { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
|
||||||
|
.category-description { margin: 0 0 6px; font-size: 14px; color: #555; line-height: 1.7; }
|
||||||
|
.category-meta { margin: 0; font-size: 12px; color: #999; }
|
||||||
|
|||||||
@@ -190,3 +190,9 @@ a:hover { opacity: 0.8; }
|
|||||||
/* 已解锁的付费内容块 */
|
/* 已解锁的付费内容块 */
|
||||||
.paid-content { border: 1px solid var(--border); border-left: 4px solid var(--accent); background: var(--bg); border-radius: 10px; padding: 14px 16px; margin: 14px 0; }
|
.paid-content { border: 1px solid var(--border); border-left: 4px solid var(--accent); background: var(--bg); border-radius: 10px; padding: 14px 16px; margin: 14px 0; }
|
||||||
.paid-content-label { font-size: 12px; color: var(--accent); font-weight: 600; margin-bottom: 6px; }
|
.paid-content-label { font-size: 12px; color: var(--accent); font-weight: 600; margin-bottom: 6px; }
|
||||||
|
|
||||||
|
/* 分类页介绍区 */
|
||||||
|
.category-intro { display: flex; gap: 14px; align-items: flex-start; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; margin-bottom: 18px; }
|
||||||
|
.category-cover { width: 128px; height: 84px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
|
||||||
|
.category-description { margin: 0 0 6px; font-size: 14px; color: var(--muted); line-height: 1.7; }
|
||||||
|
.category-meta { margin: 0; font-size: 12px; color: var(--muted); }
|
||||||
|
|||||||
@@ -167,3 +167,9 @@ a:hover { color: #f60; text-decoration: underline; }
|
|||||||
/* 已解锁的付费内容块 */
|
/* 已解锁的付费内容块 */
|
||||||
.paid-content { border: 1px dashed #f5c6cb; border-left: 4px solid #f60; background: #fff8f0; border-radius: 6px; padding: 12px 14px; margin: 12px 0; }
|
.paid-content { border: 1px dashed #f5c6cb; border-left: 4px solid #f60; background: #fff8f0; border-radius: 6px; padding: 12px 14px; margin: 12px 0; }
|
||||||
.paid-content-label { font-size: 12px; color: #f60; font-weight: 700; margin-bottom: 6px; }
|
.paid-content-label { font-size: 12px; color: #f60; font-weight: 700; margin-bottom: 6px; }
|
||||||
|
|
||||||
|
/* 分类页介绍区 */
|
||||||
|
.category-intro { display: flex; gap: 14px; align-items: flex-start; background: #fff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 14px 16px; margin-bottom: 16px; }
|
||||||
|
.category-cover { width: 120px; height: 80px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
|
||||||
|
.category-description { margin: 0 0 6px; font-size: 14px; color: #555; line-height: 1.7; }
|
||||||
|
.category-meta { margin: 0; font-size: 12px; color: #999; }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@php
|
@php
|
||||||
$pageTitle = ($archiveTitle ?? $category->name ?? '文章列表').' - '.$siteName;
|
$pageTitle = ($archiveTitle ?? $category->name ?? '文章列表').' - '.$siteName;
|
||||||
|
$pageDescription = $category->description ?? ($archiveTitle ?? $category->name ?? '文章列表').' - '.$siteName;
|
||||||
@endphp
|
@endphp
|
||||||
@include('partials.head')
|
@include('partials.head')
|
||||||
<body>
|
<body>
|
||||||
@@ -8,6 +9,21 @@
|
|||||||
<main id="content">
|
<main id="content">
|
||||||
<h1 class="list-title">{{ $archiveTitle ?? ($category->name ?? '文章列表') }}</h1>
|
<h1 class="list-title">{{ $archiveTitle ?? ($category->name ?? '文章列表') }}</h1>
|
||||||
|
|
||||||
|
@if(isset($category) && ($category->description || $category->getFirstMedia('cover')))
|
||||||
|
<div class="category-intro">
|
||||||
|
@if($cover = $category->getFirstMedia('cover'))
|
||||||
|
<img src="{{ $cover->getUrl() }}" alt="{{ $category->name }}" class="category-cover">
|
||||||
|
@endif
|
||||||
|
<div>
|
||||||
|
@if($category->description)
|
||||||
|
<p class="category-description">{{ $category->description }}</p>
|
||||||
|
@endif
|
||||||
|
<p class="category-meta">{{ $category->posts_count ?? $posts->total() }} 篇文章</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
|
||||||
@forelse($posts as $post)
|
@forelse($posts as $post)
|
||||||
@include('partials.post-card', ['post' => $post])
|
@include('partials.post-card', ['post' => $post])
|
||||||
@empty
|
@empty
|
||||||
|
|||||||
Reference in New Issue
Block a user