diff --git a/app/Blog/Providers/ThemeServiceProvider.php b/app/Blog/Providers/ThemeServiceProvider.php index 4a2d223..fdff64b 100644 --- a/app/Blog/Providers/ThemeServiceProvider.php +++ b/app/Blog/Providers/ThemeServiceProvider.php @@ -30,7 +30,13 @@ class ThemeServiceProvider extends ServiceProvider private function prependActiveThemeViews(): void { $manager = app(ThemeManager::class); - $views = $manager->path($manager->active()).'/views'; + + // 迁移尚未执行(如 migrate:fresh 首轮)时跳过 DB 查询,使用默认主题 + $active = \Illuminate\Support\Facades\Schema::hasTable('settings') + ? $manager->active() + : config('themes.default'); + + $views = $manager->path($active).'/views'; if (! is_dir($views)) { return; diff --git a/app/Filament/Pages/BlogSettings.php b/app/Filament/Pages/BlogSettings.php new file mode 100644 index 0000000..ccca61c --- /dev/null +++ b/app/Filament/Pages/BlogSettings.php @@ -0,0 +1,112 @@ +form->fill([ + 'site_name' => Setting::get('site_name', config('blog.name')), + 'site_description' => Setting::get('site_description', config('blog.description')), + 'site_icp' => Setting::get('site_icp', config('blog.icp')), + 'active_theme' => Setting::get('active_theme', config('themes.default')), + 'per_page' => Setting::get('per_page', config('blog.per_page')), + 'comment_audit' => Setting::get('comment_audit', '0'), + 'comment_min_len' => Setting::get('comment_min_len', config('blog.comment_min_len')), + 'comment_max_len' => Setting::get('comment_max_len', config('blog.comment_max_len')), + 'comment_post_space' => Setting::get('comment_post_space', config('blog.comment_post_space')), + 'rss_num' => Setting::get('rss_num', config('blog.rss_num')), + 'seo_default_keywords' => Setting::get('seo_default_keywords', ''), + 'seo_default_description' => Setting::get('seo_default_description', ''), + 'site_closed' => Setting::get('site_closed', '0'), + 'site_closed_note' => Setting::get('site_closed_note', ''), + ]); + } + + public function form(Schema $schema): Schema + { + return $schema + ->components([ + Section::make('站点信息') + ->schema([ + TextInput::make('site_name')->label('站点名称')->required(), + TextInput::make('site_description')->label('站点描述')->columnSpanFull(), + TextInput::make('site_icp')->label('ICP 备案号'), + ]), + Section::make('主题') + ->schema([ + Select::make('active_theme') + ->label('激活主题') + ->options(collect(app(\App\Blog\Support\ThemeManager::class)->all())->pluck('title', 'name')), + ]), + Section::make('列表与订阅') + ->schema([ + TextInput::make('per_page')->label('每页文章数')->numeric(), + TextInput::make('rss_num')->label('RSS 文章数')->numeric(), + ]), + Section::make('评论') + ->columns(2) + ->schema([ + Toggle::make('comment_audit')->label('新评论需审核')->default(false), + TextInput::make('comment_min_len')->label('评论最短字数')->numeric(), + TextInput::make('comment_max_len')->label('评论最长字数')->numeric(), + TextInput::make('comment_post_space')->label('评论间隔(秒)')->numeric(), + ]), + Section::make('SEO 默认值') + ->schema([ + TextInput::make('seo_default_keywords')->label('默认关键词'), + TextInput::make('seo_default_description')->label('默认描述')->columnSpanFull(), + ]), + Section::make('维护') + ->schema([ + Toggle::make('site_closed')->label('关闭站点')->default(false), + TextInput::make('site_closed_note')->label('关闭说明'), + ]), + ]) + ->statePath('data'); + } + + public function save(): void + { + $data = $this->form->getState(); + + foreach ($data as $key => $value) { + Setting::set($key, is_bool($value) ? (string) (int) $value : (string) $value); + } + + Notification::make()->title('设置已保存')->success()->send(); + } + + protected function getFormActions(): array + { + return [ + Action::make('save') + ->label('保存设置') + ->submit('save'), + ]; + } +} diff --git a/app/Filament/Resources/Categories/CategoryResource.php b/app/Filament/Resources/Categories/CategoryResource.php new file mode 100644 index 0000000..e4440a4 --- /dev/null +++ b/app/Filament/Resources/Categories/CategoryResource.php @@ -0,0 +1,50 @@ + ListCategories::route('/'), + 'create' => CreateCategory::route('/create'), + 'edit' => EditCategory::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Categories/Pages/CreateCategory.php b/app/Filament/Resources/Categories/Pages/CreateCategory.php new file mode 100644 index 0000000..a842af9 --- /dev/null +++ b/app/Filament/Resources/Categories/Pages/CreateCategory.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('name') + ->required(), + TextInput::make('slug'), + TextInput::make('display_order') + ->required() + ->numeric() + ->default(0), + TextInput::make('post_count') + ->required() + ->numeric() + ->default(0), + ]); + } +} diff --git a/app/Filament/Resources/Categories/Tables/CategoriesTable.php b/app/Filament/Resources/Categories/Tables/CategoriesTable.php new file mode 100644 index 0000000..cdebc8b --- /dev/null +++ b/app/Filament/Resources/Categories/Tables/CategoriesTable.php @@ -0,0 +1,48 @@ +columns([ + TextColumn::make('name') + ->searchable(), + TextColumn::make('slug') + ->searchable(), + TextColumn::make('display_order') + ->numeric() + ->sortable(), + TextColumn::make('post_count') + ->numeric() + ->sortable(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Comments/CommentResource.php b/app/Filament/Resources/Comments/CommentResource.php new file mode 100644 index 0000000..b52185c --- /dev/null +++ b/app/Filament/Resources/Comments/CommentResource.php @@ -0,0 +1,50 @@ + ListComments::route('/'), + 'create' => CreateComment::route('/create'), + 'edit' => EditComment::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Comments/Pages/CreateComment.php b/app/Filament/Resources/Comments/Pages/CreateComment.php new file mode 100644 index 0000000..5f48b2c --- /dev/null +++ b/app/Filament/Resources/Comments/Pages/CreateComment.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('author_name') + ->label('作者') + ->required(), + TextInput::make('author_email') + ->label('邮箱'), + TextInput::make('author_url') + ->label('网站') + ->url(), + Textarea::make('content') + ->label('内容') + ->required() + ->rows(5), + Select::make('status') + ->label('状态') + ->options([ + 'published' => '已发布', + 'pending' => '待审核', + 'spam' => '垃圾', + 'rejected' => '已拒绝', + ]) + ->required(), + ]); + } +} diff --git a/app/Filament/Resources/Comments/Tables/CommentsTable.php b/app/Filament/Resources/Comments/Tables/CommentsTable.php new file mode 100644 index 0000000..32021a3 --- /dev/null +++ b/app/Filament/Resources/Comments/Tables/CommentsTable.php @@ -0,0 +1,91 @@ +columns([ + TextColumn::make('author_name') + ->label('作者') + ->searchable() + ->weight('bold'), + TextColumn::make('content') + ->label('内容') + ->limit(60) + ->searchable(), + TextColumn::make('post.title') + ->label('文章') + ->limit(30) + ->url(fn ($record) => $record->post ? route('posts.show', $record->post->slug ?: $record->post->id) : null) + ->openUrlInNewTab(), + 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', + default => 'gray', + }), + TextColumn::make('ip') + ->label('IP'), + TextColumn::make('created_at') + ->label('时间') + ->dateTime('Y-m-d H:i') + ->sortable(), + ]) + ->filters([ + SelectFilter::make('status') + ->label('状态') + ->options([ + 'published' => '已发布', + 'pending' => '待审核', + 'spam' => '垃圾', + 'rejected' => '已拒绝', + ]), + ]) + ->recordActions([ + Action::make('approve') + ->label('通过') + ->icon('heroicon-o-check') + ->color('success') + ->visible(fn (Comment $record) => $record->status !== 'published') + ->action(function (Comment $record): void { + $record->update(['status' => 'published']); + $record->post?->increment('comment_count'); + }), + Action::make('spam') + ->label('垃圾') + ->icon('heroicon-o-x-mark') + ->color('danger') + ->visible(fn (Comment $record) => $record->status !== 'spam') + ->action(fn (Comment $record) => $record->update(['status' => 'spam'])), + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]) + ->defaultSort('created_at', 'desc'); + } +} diff --git a/app/Filament/Resources/Links/LinkResource.php b/app/Filament/Resources/Links/LinkResource.php new file mode 100644 index 0000000..7830f25 --- /dev/null +++ b/app/Filament/Resources/Links/LinkResource.php @@ -0,0 +1,50 @@ + ListLinks::route('/'), + 'create' => CreateLink::route('/create'), + 'edit' => EditLink::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Links/Pages/CreateLink.php b/app/Filament/Resources/Links/Pages/CreateLink.php new file mode 100644 index 0000000..5a94abe --- /dev/null +++ b/app/Filament/Resources/Links/Pages/CreateLink.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('name') + ->required(), + TextInput::make('url') + ->url() + ->required(), + TextInput::make('note'), + TextInput::make('display_order') + ->required() + ->numeric() + ->default(0), + Toggle::make('visible') + ->required(), + ]); + } +} diff --git a/app/Filament/Resources/Links/Tables/LinksTable.php b/app/Filament/Resources/Links/Tables/LinksTable.php new file mode 100644 index 0000000..d643c81 --- /dev/null +++ b/app/Filament/Resources/Links/Tables/LinksTable.php @@ -0,0 +1,50 @@ +columns([ + TextColumn::make('name') + ->searchable(), + TextColumn::make('url') + ->searchable(), + TextColumn::make('note') + ->searchable(), + TextColumn::make('display_order') + ->numeric() + ->sortable(), + IconColumn::make('visible') + ->boolean(), + TextColumn::make('created_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('updated_at') + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + // + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Media/MediaResource.php b/app/Filament/Resources/Media/MediaResource.php new file mode 100644 index 0000000..4d6b817 --- /dev/null +++ b/app/Filament/Resources/Media/MediaResource.php @@ -0,0 +1,50 @@ + ListMedia::route('/'), + 'create' => CreateMedia::route('/create'), + 'edit' => EditMedia::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Media/Pages/CreateMedia.php b/app/Filament/Resources/Media/Pages/CreateMedia.php new file mode 100644 index 0000000..c6d3698 --- /dev/null +++ b/app/Filament/Resources/Media/Pages/CreateMedia.php @@ -0,0 +1,11 @@ +components([ + // + ]); + } +} diff --git a/app/Filament/Resources/Media/Tables/MediaTable.php b/app/Filament/Resources/Media/Tables/MediaTable.php new file mode 100644 index 0000000..0215eee --- /dev/null +++ b/app/Filament/Resources/Media/Tables/MediaTable.php @@ -0,0 +1,67 @@ +columns([ + ImageColumn::make('preview') + ->label('预览') + ->state(fn ($record) => $record->getCustomProperty('isimage') ? $record->getUrl() : null) + ->circular() + ->defaultImageUrl('/images/file-icon.png'), + TextColumn::make('file_name') + ->label('文件名') + ->searchable() + ->url(fn ($record) => $record->getUrl()) + ->openUrlInNewTab(), + TextColumn::make('collection_name') + ->label('用途') + ->badge() + ->formatStateUsing(fn ($state) => match ($state) { + 'cover' => '封面', + 'attachments' => '附件', + default => $state, + }), + TextColumn::make('model_type') + ->label('关联') + ->formatStateUsing(fn ($state) => class_basename((string) $state)), + TextColumn::make('model_id') + ->label('ID'), + TextColumn::make('size') + ->label('大小') + ->formatStateUsing(fn ($state) => number_format((float) $state / 1024, 1).' KB'), + TextColumn::make('custom_properties.downloads') + ->label('下载'), + TextColumn::make('created_at') + ->label('上传时间') + ->dateTime('Y-m-d H:i'), + ]) + ->filters([ + \Filament\Tables\Filters\SelectFilter::make('collection_name') + ->label('用途') + ->options([ + 'attachments' => '附件', + 'cover' => '封面', + ]), + ]) + ->recordActions([ + \Filament\Actions\EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]) + ->defaultSort('created_at', 'desc'); + } +} diff --git a/app/Filament/Resources/Posts/Pages/CreatePost.php b/app/Filament/Resources/Posts/Pages/CreatePost.php new file mode 100644 index 0000000..47a1e04 --- /dev/null +++ b/app/Filament/Resources/Posts/Pages/CreatePost.php @@ -0,0 +1,11 @@ + ListPosts::route('/'), + 'create' => CreatePost::route('/create'), + 'edit' => EditPost::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Posts/Schemas/PostForm.php b/app/Filament/Resources/Posts/Schemas/PostForm.php new file mode 100644 index 0000000..b3300fa --- /dev/null +++ b/app/Filament/Resources/Posts/Schemas/PostForm.php @@ -0,0 +1,92 @@ +components([ + \Filament\Schemas\Components\Section::make('内容') + ->columns(2) + ->schema([ + TextInput::make('title') + ->label('标题') + ->required() + ->columnSpanFull(), + TextInput::make('slug') + ->label('Slug(留空自动生成)'), + Select::make('category_id') + ->label('分类') + ->relationship('category', 'name'), + Select::make('content_format') + ->label('内容格式') + ->options([ + 'markdown' => 'Markdown', + 'html' => 'HTML', + ]) + ->default('markdown'), + Select::make('status') + ->label('状态') + ->options([ + 'published' => '已发布', + 'draft' => '草稿', + 'private' => '私密', + ]) + ->default('published'), + MarkdownEditor::make('content') + ->label('正文') + ->required() + ->columnSpanFull(), + ]), + \Filament\Schemas\Components\Section::make('封面与摘要') + ->columns(2) + ->schema([ + SpatieMediaLibraryFileUpload::make('cover') + ->label('封面图') + ->collection('cover') + ->image() + ->imageEditor() + ->disk(MediaDisk::name()) + ->columnSpanFull(), + Textarea::make('excerpt') + ->label('摘要(留空自动截取)') + ->rows(3), + TextInput::make('keywords') + ->label('关键词(逗号分隔)'), + ]), + \Filament\Schemas\Components\Section::make('发布设置') + ->columns(3) + ->schema([ + DateTimePicker::make('published_at') + ->label('发布时间') + ->default(now()), + Toggle::make('is_sticky') + ->label('置顶'), + Toggle::make('close_comment') + ->label('关闭评论'), + TextInput::make('read_password') + ->label('阅读密码(留空公开)'), + TextInput::make('views') + ->label('浏览量') + ->numeric() + ->disabled(), + TextInput::make('comment_count') + ->label('评论数') + ->numeric() + ->disabled(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Posts/Tables/PostsTable.php b/app/Filament/Resources/Posts/Tables/PostsTable.php new file mode 100644 index 0000000..c1f1911 --- /dev/null +++ b/app/Filament/Resources/Posts/Tables/PostsTable.php @@ -0,0 +1,90 @@ +columns([ + TextColumn::make('title') + ->label('标题') + ->searchable() + ->limit(40) + ->description(fn ($record) => $record->category?->name), + TextColumn::make('content_format') + ->label('格式') + ->badge() + ->formatStateUsing(fn ($state) => $state === 'markdown' ? 'MD' : 'HTML') + ->color(fn ($state) => $state === 'markdown' ? 'success' : 'gray'), + TextColumn::make('status') + ->label('状态') + ->badge() + ->formatStateUsing(fn ($state) => match ($state) { + 'published' => '已发布', + 'draft' => '草稿', + 'private' => '私密', + default => $state, + }) + ->color(fn ($state) => match ($state) { + 'published' => 'success', + 'draft' => 'warning', + default => 'gray', + }), + IconColumn::make('is_sticky') + ->label('置顶') + ->boolean(), + TextColumn::make('views') + ->label('阅读') + ->numeric() + ->sortable(), + TextColumn::make('comment_count') + ->label('评论') + ->numeric() + ->sortable(), + TextColumn::make('published_at') + ->label('发布时间') + ->dateTime('Y-m-d H:i') + ->sortable(), + ]) + ->filters([ + SelectFilter::make('status') + ->label('状态') + ->options([ + 'published' => '已发布', + 'draft' => '草稿', + 'private' => '私密', + ]), + SelectFilter::make('content_format') + ->label('格式') + ->options([ + 'markdown' => 'Markdown', + 'html' => 'HTML', + ]), + TernaryFilter::make('is_sticky') + ->label('置顶'), + ]) + ->recordActions([ + ViewAction::make(), + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]) + ->defaultSort('published_at', 'desc'); + } +} diff --git a/app/Filament/Resources/Users/Pages/CreateUser.php b/app/Filament/Resources/Users/Pages/CreateUser.php new file mode 100644 index 0000000..125b3ff --- /dev/null +++ b/app/Filament/Resources/Users/Pages/CreateUser.php @@ -0,0 +1,11 @@ +components([ + TextInput::make('name') + ->label('昵称') + ->required(), + TextInput::make('email') + ->label('邮箱') + ->email() + ->required(), + TextInput::make('password') + ->label('密码(留空不修改)') + ->password() + ->dehydrated(false), + DateTimePicker::make('email_verified_at') + ->label('邮箱验证时间'), + TextInput::make('url') + ->label('主页') + ->url(), + TextInput::make('loginip') + ->label('最近登录 IP') + ->disabled(), + TextInput::make('regip') + ->label('注册 IP') + ->disabled(), + DateTimePicker::make('logintime') + ->label('最近登录时间') + ->disabled(), + TextInput::make('logincount') + ->label('登录次数') + ->numeric() + ->disabled(), + ]); + } +} diff --git a/app/Filament/Resources/Users/Tables/UsersTable.php b/app/Filament/Resources/Users/Tables/UsersTable.php new file mode 100644 index 0000000..7d482bb --- /dev/null +++ b/app/Filament/Resources/Users/Tables/UsersTable.php @@ -0,0 +1,62 @@ +columns([ + TextColumn::make('name') + ->label('昵称') + ->searchable() + ->weight('bold'), + TextColumn::make('email') + ->label('邮箱') + ->searchable(), + TextColumn::make('roles.name') + ->label('角色') + ->badge() + ->formatStateUsing(fn ($state) => match ($state) { + 'admin' => '管理组', + 'editor' => '撰写组', + 'member' => '会员', + default => $state, + }), + TextColumn::make('posts_count') + ->label('文章') + ->counts('posts') + ->sortable(), + TextColumn::make('logincount') + ->label('登录次数') + ->sortable(), + TextColumn::make('loginip') + ->label('最近 IP'), + TextColumn::make('created_at') + ->label('注册时间') + ->dateTime('Y-m-d') + ->sortable(), + ]) + ->filters([ + \Filament\Tables\Filters\SelectFilter::make('roles') + ->label('角色') + ->relationship('roles', 'name'), + ]) + ->recordActions([ + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]) + ->defaultSort('created_at', 'desc'); + } +} diff --git a/app/Filament/Resources/Users/UserResource.php b/app/Filament/Resources/Users/UserResource.php new file mode 100644 index 0000000..7f3f2c4 --- /dev/null +++ b/app/Filament/Resources/Users/UserResource.php @@ -0,0 +1,50 @@ + ListUsers::route('/'), + 'create' => CreateUser::route('/create'), + 'edit' => EditUser::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Widgets/BlogStats.php b/app/Filament/Widgets/BlogStats.php new file mode 100644 index 0000000..66bb0be --- /dev/null +++ b/app/Filament/Widgets/BlogStats.php @@ -0,0 +1,28 @@ +count()) + ->description('总浏览量 '.number_format(Post::sum('views'))) + ->icon('heroicon-o-document-text'), + Stat::make('评论', Comment::query()->where('status', 'published')->count()) + ->description('待审核 '.Comment::query()->where('status', 'pending')->count()) + ->icon('heroicon-o-chat-bubble-left-right'), + Stat::make('用户', User::count()) + ->icon('heroicon-o-users'), + ]; + } +} diff --git a/app/Filament/Widgets/RecentComments.php b/app/Filament/Widgets/RecentComments.php new file mode 100644 index 0000000..f28be4f --- /dev/null +++ b/app/Filament/Widgets/RecentComments.php @@ -0,0 +1,37 @@ +query(Comment::query()->latest('created_at')->limit(10)) + ->columns([ + TextColumn::make('author_name')->label('作者'), + TextColumn::make('content')->label('内容')->limit(50), + TextColumn::make('post.title')->label('文章')->limit(30), + TextColumn::make('status') + ->label('状态') + ->badge() + ->formatStateUsing(fn ($state) => match ($state) { + 'published' => '已发布', + 'pending' => '待审核', + 'spam' => '垃圾', + default => $state, + }), + TextColumn::make('created_at')->label('时间')->dateTime('Y-m-d H:i'), + ]) + ->paginated(false); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 963972e..31bc51c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,6 +3,8 @@ namespace App\Models; use Database\Factories\UserFactory; +use Filament\Models\Contracts\FilamentUser; +use Filament\Panel; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -10,11 +12,16 @@ use Illuminate\Notifications\Notifiable; use Illuminate\Support\Facades\Hash; use Spatie\Permission\Traits\HasRoles; -class User extends Authenticatable +class User extends Authenticatable implements FilamentUser { /** @use HasFactory */ use HasFactory, Notifiable, HasRoles; + public function canAccessPanel(Panel $panel): bool + { + return $this->hasRole(['admin', 'editor']); + } + /** * The attributes that are mass assignable. * diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 8ce4eb0..978c825 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -2,16 +2,23 @@ namespace App\Providers\Filament; +use App\Filament\Pages\BlogSettings; +use App\Filament\Resources\Categories\CategoryResource; +use App\Filament\Resources\Comments\CommentResource; +use App\Filament\Resources\Links\LinkResource; +use App\Filament\Resources\Media\MediaResource; +use App\Filament\Resources\Posts\PostResource; +use App\Filament\Resources\Users\UserResource; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; +use Filament\Navigation\NavigationGroup; use Filament\Pages\Dashboard; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; use Filament\Widgets\AccountWidget; -use Filament\Widgets\FilamentInfoWidget; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; @@ -29,17 +36,29 @@ class AdminPanelProvider extends PanelProvider ->path('admin') ->login() ->colors([ - 'primary' => Color::Amber, + 'primary' => Color::Sky, + ]) + ->brandName(config('blog.name')) + ->navigationGroups([ + NavigationGroup::make('内容'), + NavigationGroup::make('管理'), ]) ->discoverResources(in: app_path('Filament/Resources'), for: 'App\Filament\Resources') ->discoverPages(in: app_path('Filament/Pages'), for: 'App\Filament\Pages') ->pages([ Dashboard::class, ]) + ->resources([ + PostResource::class, + CategoryResource::class, + CommentResource::class, + LinkResource::class, + MediaResource::class, + UserResource::class, + ]) ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets') ->widgets([ AccountWidget::class, - FilamentInfoWidget::class, ]) ->middleware([ EncryptCookies::class, diff --git a/composer.json b/composer.json index 5cc9a76..465c374 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "require": { "php": "^8.2", "filament/filament": "^5.7", + "filament/spatie-laravel-media-library-plugin": "^5.7", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1", "league/commonmark": "^2.9", diff --git a/composer.lock b/composer.lock index 8b96709..0415452 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c0357e1996000075eb3879929f19e248", + "content-hash": "b3635d8ae6b629801f16a9df99622e56", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -1406,6 +1406,43 @@ }, "time": "2026-08-05T20:51:01+00:00" }, + { + "name": "filament/spatie-laravel-media-library-plugin", + "version": "v5.7.6", + "source": { + "type": "git", + "url": "https://github.com/filamentphp/spatie-laravel-media-library-plugin.git", + "reference": "16d3c2b0a4a47fd03dfe5f448f6b3bb5a53b0587" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filamentphp/spatie-laravel-media-library-plugin/zipball/16d3c2b0a4a47fd03dfe5f448f6b3bb5a53b0587", + "reference": "16d3c2b0a4a47fd03dfe5f448f6b3bb5a53b0587", + "shasum": "" + }, + "require": { + "filament/support": "self.version", + "php": "^8.2", + "spatie/laravel-medialibrary": "^11.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Filament\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Filament support for `spatie/laravel-medialibrary`.", + "homepage": "https://github.com/filamentphp/filament", + "support": { + "issues": "https://github.com/filamentphp/filament/issues", + "source": "https://github.com/filamentphp/filament" + }, + "time": "2026-07-17T11:47:27+00:00" + }, { "name": "filament/support", "version": "v5.7.6", diff --git a/resources/views/filament/pages/blog-settings.blade.php b/resources/views/filament/pages/blog-settings.blade.php new file mode 100644 index 0000000..501aaaf --- /dev/null +++ b/resources/views/filament/pages/blog-settings.blade.php @@ -0,0 +1,11 @@ + +
+ {{ $this->form }} + +
+ + {{ __('保存设置') }} + +
+
+
diff --git a/tests/Feature/AdminPagesTest.php b/tests/Feature/AdminPagesTest.php new file mode 100644 index 0000000..3e7d6e5 --- /dev/null +++ b/tests/Feature/AdminPagesTest.php @@ -0,0 +1,57 @@ +seed(); + + $this->admin = User::query()->firstOrCreate( + ['email' => 'admin@laralog.test'], + ['name' => '管理员', 'password' => bcrypt('password')] + ); + } + + public function test_dashboard_loads(): void + { + $this->actingAs($this->admin)->get('/admin')->assertOk(); + } + + public function test_post_resource_loads(): void + { + $this->actingAs($this->admin)->get('/admin/posts')->assertOk(); + $this->actingAs($this->admin)->get('/admin/posts/create')->assertOk(); + } + + public function test_settings_page_loads(): void + { + $this->actingAs($this->admin)->get('/admin/blog-settings')->assertOk(); + } + + public function test_comment_resource_loads(): void + { + $this->actingAs($this->admin)->get('/admin/comments')->assertOk(); + } + + public function test_media_resource_loads(): void + { + $this->actingAs($this->admin)->get('/admin/media')->assertOk(); + } + + public function test_guest_is_redirected_to_login(): void + { + $this->get('/admin')->assertRedirect('/admin/login'); + } +}