columns(self::pcColumns(3)) ->components([ self::contentSection(), self::sidebarGroup(), ]); } /** * Append sections or fields into the right-hand sidebar (2:1 layout). * * @param array $components */ public static function appendToSidebar(Schema $schema, array $components): void { $sidebar = self::findKeyedComponent($schema->getComponents(), self::SIDEBAR_KEY); if (! $sidebar instanceof Component) { $schema->components([ ...$schema->getComponents(), ...$components, ]); return; } $existing = $sidebar->getDefaultChildComponents(); if ($existing instanceof Schema) { $existing->components([ ...$existing->getComponents(), ...$components, ]); return; } $sidebar->schema([ ...(is_array($existing) ? $existing : []), ...$components, ]); } /** * Append fields into the access group (password / members / price). * * @param array $fields */ public static function appendToAccessSection(Schema $schema, array $fields): void { $sidebar = self::findKeyedComponent($schema->getComponents(), self::SIDEBAR_KEY); $access = $sidebar instanceof Component ? self::findKeyedComponent(self::childList($sidebar), self::ACCESS_KEY) : null; if (! $access instanceof Component) { self::appendToSidebar($schema, [ Section::make('访问设置') ->key(self::ACCESS_KEY) ->schema($fields), ]); return; } $existing = $access->getDefaultChildComponents(); if ($existing instanceof Schema) { $existing->components([ ...$existing->getComponents(), ...$fields, ]); return; } $access->schema([ ...(is_array($existing) ? $existing : []), ...$fields, ]); } private static function contentSection(): Section { return Section::make('正文') ->columnSpan(self::pcColumns(2)) ->columns(self::pcColumns(3)) ->schema([ TextInput::make('title') ->label('标题') ->required() ->columnSpanFull(), TextInput::make('slug') ->label('Slug(留空自动生成)') ->columnSpanFull(), Select::make('category_id') ->label('分类') ->relationship('category', 'name'), ToggleButtons::make('content_format') ->label('内容格式') ->options([ 'markdown' => 'Markdown', 'html' => 'HTML', ]) ->default('markdown') ->inline() ->grouped() ->live(), ToggleButtons::make('status') ->label('状态') ->options([ 'published' => '已发布', 'draft' => '草稿', 'private' => '私密', ]) ->default('published') ->inline() ->grouped(), MediaPicker::make('cover_media_id') ->label('封面图') ->module('articles') ->mediaType('image') ->relationship('coverMedia') ->extraFieldWrapperAttributes(['class' => 'll-media-picker-preview']) ->columnSpanFull(), MarkdownEditor::make('content') ->label('正文') ->required() ->minHeight('32rem') ->helperText('Markdown 插入图片用 ![说明](media:ID),文件用 [文件名](media:ID)。不要把 CDN / 对象存储地址写进正文。') ->fileAttachments(false) ->columnSpanFull(), MediaPicker::make('insert_media_id') ->label('从媒体库插入正文') ->module('articles') ->mediaType(null) ->dehydrated(false) ->extraFieldWrapperAttributes(['class' => 'll-media-picker-preview']) ->helperText('选好媒体后点「插入到正文」。Markdown 写入 media:ID 引用,渲染时再换成真实地址。') ->columnSpanFull(), Actions::make([ Action::make('insertSelectedMedia') ->label('插入到正文') ->action(function (Get $get, Set $set): void { $mediaId = $get('insert_media_id'); $media = is_numeric($mediaId) ? MediaLibrary::query()->find((int) $mediaId) : null; if (! $media instanceof MediaLibrary) { Notification::make()->title('请先从媒体库选择文件')->warning()->send(); return; } $snippet = app(MarkdownMediaEmbed::class)->snippet( $media, (string) ($get('content_format') ?: 'markdown') ); $set('content', rtrim((string) $get('content'))."\n\n".$snippet."\n"); $set('insert_media_id', null); Notification::make()->title('已写入正文引用')->success()->send(); }), ])->columnSpanFull(), ]); } private static function sidebarGroup(): Group { return Group::make([ Section::make('发布设置') ->schema([ DateTimePicker::make('published_at') ->label('发布时间') ->default(now()), ToggleButtons::make('sticky_level') ->label('置顶等级') ->options(Post::stickyLevelOptions()) ->default(Post::STICKY_NONE) ->inline() ->grouped() ->helperText('轻置顶压过普通文章,强置顶压过轻置顶,置顶头条永远排在最上面。'), ToggleButtons::make('close_comment') ->label('评论状态') ->boolean('关闭评论', '开放评论') ->default(false) ->inline() ->grouped() ->dehydrateStateUsing(fn (mixed $state): bool => (bool) $state), Textarea::make('excerpt') ->label('摘要(留空自动截取)') ->rows(4), TextInput::make('keywords') ->label('关键词(逗号分隔)'), ]), Section::make('访问设置') ->key(self::ACCESS_KEY) ->schema([ TextInput::make('read_password') ->label('阅读密码(留空公开)'), ]), ]) ->key(self::SIDEBAR_KEY) ->columnSpan(self::pcColumns(1)); } /** * Admin writing is almost always on a PC. Filament's columns(3) only * applies from the lg breakpoint, so we pin every breakpoint. * * @return array */ private static function pcColumns(int $count): array { return [ 'default' => $count, 'sm' => $count, 'md' => $count, 'lg' => $count, 'xl' => $count, '2xl' => $count, ]; } /** * @param array $components */ private static function findKeyedComponent(array $components, string $key): ?Component { foreach ($components as $component) { if (! $component instanceof Component) { continue; } if (self::rawKey($component) === $key) { return $component; } } return null; } private static function rawKey(Component $component): ?string { $property = (new ReflectionClass($component))->getProperty('key'); $key = $property->getValue($component); return is_string($key) ? $key : null; } /** * @return array */ private static function childList(Component $component): array { $children = $component->getDefaultChildComponents(); if ($children instanceof Schema) { return $children->getComponents(); } return is_array($children) ? $children : []; } }