| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- <?php
- namespace App\Filament\Resources\TextbookResource\Schemas;
- use App\Filament\Resources\TextbookResource;
- use App\Services\TextbookApiService;
- use App\Services\TextbookCoverStorageService;
- use Closure;
- use Filament\Forms\Components\BaseFileUpload;
- use Filament\Forms\Components\FileUpload;
- use Filament\Forms\Components\Select;
- use Filament\Schemas\Components\Section;
- use Filament\Forms\Components\TextInput;
- use Filament\Forms\Components\Textarea;
- use Filament\Schemas\Schema;
- use Illuminate\Filesystem\FilesystemAdapter;
- use League\Flysystem\UnableToCheckFileExistence;
- use Throwable;
- class TextbookFormSchema
- {
- /**
- * 封面多图上传。库存为春笋云等返回的绝对 URL 时,Filament 默认会对 disk 做 exists(),
- * 非磁盘路径会被过滤掉,编辑页无法预览;需关闭远程信息拉取并为绝对 URL 直接提供预览地址。
- */
- public static function coverPathFileUpload(Closure $saveUploadedFileUsing): FileUpload
- {
- return FileUpload::make('cover_path')
- ->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);
- }
- }
|