label('教材配图') ->image() ->multiple() ->reorderable() ->directory('textbook-covers') ->fetchFileInformation(false) ->formatStateUsing(function ($state) { // FileUploadStateCast::get() 会把 DB 里「逗号拼接的一整串」变成单元素数组 // ['https://a.jpg,https://b.jpg'],必须逐项再按逗号拆开,否则会当成一个路径无法预览。 if (is_string($state)) { return array_values(array_filter(array_map('trim', explode(',', $state)))); } if (! is_array($state)) { return []; } $urls = []; foreach ($state as $part) { if (! is_string($part) || $part === '') { continue; } foreach (array_map('trim', explode(',', $part)) as $piece) { if ($piece !== '') { $urls[] = $piece; } } } return array_values(array_unique($urls)); }) ->dehydrateStateUsing(function ($state) { if (is_string($state)) { return trim($state); } if (! is_array($state)) { return null; } $urls = array_values(array_filter(array_map(static fn ($item) => trim((string) $item), $state))); return empty($urls) ? null : implode(',', $urls); }) ->getUploadedFileUsing(static function (BaseFileUpload $component, string $file, string|array|null $storedFileNames): ?array { $trimmed = trim($file); if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) { $path = parse_url($trimmed, PHP_URL_PATH) ?: ''; $name = basename((string) $path) ?: 'image'; return [ 'name' => $name, 'size' => 0, 'type' => null, 'url' => $trimmed, ]; } /** @var FilesystemAdapter $storage */ $storage = $component->getDisk(); $shouldFetchFileInformation = $component->shouldFetchFileInformation(); if ($shouldFetchFileInformation) { try { if (! $storage->exists($file)) { return null; } } catch (UnableToCheckFileExistence $exception) { return null; } } $url = null; if ($component->getVisibility() === 'private') { try { $url = $storage->temporaryUrl( $file, now()->addMinutes(30)->endOfHour(), ); } catch (Throwable $exception) { // Driver may not support temporary URLs. } } $url ??= $storage->url($file); return [ 'name' => ($component->isMultiple() ? ($storedFileNames[$file] ?? null) : $storedFileNames) ?? basename($file), 'size' => $shouldFetchFileInformation ? $storage->size($file) : 0, 'type' => $shouldFetchFileInformation ? $storage->mimeType($file) : null, 'url' => $url, ]; }) ->saveUploadedFileUsing($saveUploadedFileUsing) ->helperText('支持多张图片上传(如封面、目录页),保存时按上传/排序顺序以英文逗号拼接。'); } public static function make(Schema $schema): Schema { return $schema ->schema([ Section::make('基本信息') ->schema([ Select::make('series_id') ->label('教材系列') ->options(function () { $series = app(TextbookApiService::class)->getTextbookSeries(); $options = []; foreach ($series['data'] as $s) { $displayName = $s['name'] ?? '未命名系列'; if (!empty($s['publisher'])) { $displayName .= ' (' . $s['publisher'] . ')'; } $options[$s['id']] = $displayName; } return $options; }) ->required() ->searchable() ->preload(), TextInput::make('official_title') ->label('教材名称') ->maxLength(512) ->helperText('教材名称(用户输入)'), TextInput::make('isbn') ->label('ISBN') ->maxLength(20), Textarea::make('aliases') ->label('别名') ->helperText('JSON 格式,如:["别名1", "别名2"]') ->formatStateUsing(fn ($state) => is_array($state) ? json_encode($state, JSON_UNESCAPED_UNICODE) : $state) ->dehydrateStateUsing(fn ($state) => is_string($state) ? json_decode($state, true) : $state) ->columnSpanFull(), ]) ->columns(2), Section::make('学段/年级/学期') ->schema([ Select::make('stage') ->label('学段') ->options([ 'primary' => '小学', 'junior' => '初中', 'senior' => '高中', ]) ->default('junior') ->required() ->reactive(), Select::make('schooling_system') ->label('学制') ->options([ '63' => '六三学制', '54' => '五四学制', ]) ->default('63') ->visible(fn ($get): bool => in_array($get('stage'), ['primary', 'junior'])), TextInput::make('grade') ->label('年级') ->numeric() ->minValue(1) ->maxValue(12) ->helperText('数字1-12,例:1年级填1'), Select::make('semester') ->label('学期') ->options([ 1 => '上学期', 2 => '下学期', ]) ->required(), ]) ->columns(2), Section::make('封面上传') ->schema([ static::coverPathFileUpload(function ($component, $file) { $uploader = app(TextbookCoverStorageService::class); $url = $uploader->uploadCover($file, null); if ($url) { return $url; } return $file->storePubliclyAs( $component->getDirectory(), $component->getUploadedFileNameForStorage($file), $component->getDiskName(), ); }), ]), Section::make('发布与排序') ->schema([ Select::make('status') ->label('状态') ->options([ 'draft' => '草稿', 'published' => '已发布', 'archived' => '已归档', ]) ->default('draft') ->required(), TextInput::make('sort_order') ->label('排序') ->numeric() ->default(0) ->helperText('数字越小排序越靠前'), Textarea::make('meta') ->label('扩展信息') ->helperText('JSON 格式') ->formatStateUsing(fn ($state) => is_array($state) ? json_encode($state, JSON_UNESCAPED_UNICODE) : $state) ->dehydrateStateUsing(fn ($state) => is_string($state) ? json_decode($state, true) : $state) ->columnSpanFull(), ]) ->columns(2), ]) ->columns(2); } }