feat: 文章/分类/评论关联细节完善

- 文章列表:会员插件注入「付费」徽章列(会员/付费¥xx,停插件即消失)
- 分类:真实文章数(withCount)、名称点击跳文章列表并按分类筛选(ListPosts booted 预置筛选)
- 分类:新增封面(medialibrary)+ 介绍(description 字段/表单);前台分类页顶部展示介绍区;SEO description + BreadcrumbList 结构化数据(GEO)
- 文章编辑:MarkdownEditor 内容区加大(26rem);文章编辑页新增「评论」关系管理页(可审/拒)
- 评论列表:文章列带摘要
- 三套主题补 .category-intro 样式
- 测试更新(付费徽章列)
This commit is contained in:
ak
2026-08-12 22:44:16 +08:00
parent a22175d5b7
commit 5f74dd76e4
18 changed files with 276 additions and 34 deletions
@@ -4,7 +4,11 @@ declare(strict_types=1);
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\Schemas\Components\Section;
use Filament\Schemas\Schema;
class CategoryForm
@@ -13,23 +17,39 @@ class CategoryForm
{
return $schema
->components([
TextInput::make('name')
->label(__('category.name'))
->required(),
TextInput::make('slug')
->label(__('category.slug'))
->helperText('留空自动生成'),
TextInput::make('display_order')
->label(__('category.display_order'))
->required()
->numeric()
->default(0),
TextInput::make('post_count')
->label(__('category.post_count'))
->required()
->numeric()
->default(0)
->disabled(),
Section::make('基本信息')
->columns(2)
->schema([
TextInput::make('name')
->label(__('category.name'))
->required(),
TextInput::make('slug')
->label(__('category.slug'))
->helperText('留空自动生成'),
TextInput::make('display_order')
->label(__('category.display_order'))
->required()
->numeric()
->default(0),
TextInput::make('post_count')
->label('文章数(自动统计)')
->numeric()
->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\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Support\Str;
class CategoriesTable
{
public static function configure(Table $table): Table
{
return $table
->modifyQueryUsing(fn ($query) => $query->withCount('posts'))
->columns([
TextColumn::make('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')
->label(__('category.slug'))
->searchable(),
@@ -26,8 +30,8 @@ class CategoriesTable
->label(__('category.display_order'))
->numeric()
->sortable(),
TextColumn::make('post_count')
->label(__('category.post_count'))
TextColumn::make('posts_count')
->label('文章数')
->numeric()
->sortable(),
TextColumn::make('created_at')
@@ -31,6 +31,7 @@ class CommentsTable
TextColumn::make('post.title')
->label('文章')
->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)
->openUrlInNewTab(),
TextColumn::make('status')
@@ -2,7 +2,6 @@
declare(strict_types=1);
namespace App\Filament\Resources\Posts\Pages;
use App\Filament\Resources\Posts\PostResource;
@@ -13,6 +12,18 @@ class ListPosts extends ListRecords
{
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
{
return [
@@ -54,7 +54,7 @@ class PostResource extends Resource
public static function getRelations(): array
{
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(),
])
->filters([
SelectFilter::make('category_id')
->label('分类')
->relationship('category', 'name'),
SelectFilter::make('status')
->label('状态')
->options([
+12 -2
View File
@@ -2,22 +2,25 @@
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
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 InteractsWithMedia;
protected $fillable = [
'name',
'slug',
'display_order',
'post_count',
'description',
];
public function posts(): HasMany
@@ -29,4 +32,11 @@ class Category extends Model
{
return route('category.show', $this->slug ?? $this->id);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('cover')
->useDisk(\App\Support\MediaDisk::name())
->singleFile();
}
}
+3
View File
@@ -81,6 +81,9 @@ class AppServiceProvider extends ServiceProvider
/* 操作列固定宽度(钉住右侧需 table sticky,见备注) */
.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-heading { display: none !important; }