*/ public array $coverUrls = []; /** 本次待追加的本地文件(可多次选择、多次点「追加到列表」) */ public array $photos = []; public ?string $saveFeedback = null; public string $saveFeedbackType = 'info'; public bool $hasUnsavedChanges = false; /** * 必须与路由参数名一致({record}),Livewire 全页组件会把 URL 段注入 mount,勿仅用 Request::route(部分环境下为空)。 */ public function mount(int|string $record): void { $this->recordId = (int) $record; if ($this->recordId <= 0) { abort(404); } $data = app(TextbookApiService::class)->getTextbook($this->recordId); if (! $data) { abort(404); } $this->officialTitle = (string) ($data['official_title'] ?? ''); $this->seriesName = (string) ( $data['series_name'] ?? data_get($data, 'series.name') ?? TextbookSeries::query()->whereKey($data['series_id'] ?? null)->value('name') ?? '' ); $this->coverUrls = $this->splitCoverPath($data['cover_path'] ?? null); $this->hydrateSiblingTextbooks($data); $this->hasUnsavedChanges = false; } protected function hydrateSiblingTextbooks(array $data): void { $seriesId = isset($data['series_id']) ? (int) $data['series_id'] : 0; if ($seriesId <= 0 || $this->recordId === null) { $this->previousTextbook = null; $this->nextTextbook = null; return; } $records = Textbook::query() ->where('series_id', $seriesId) ->orderBy('grade') ->orderBy('semester') ->orderBy('id') ->get(['id', 'official_title', 'grade', 'semester']); $currentIndex = $records->search(fn (Textbook $textbook): bool => (int) $textbook->getKey() === $this->recordId); if (! is_int($currentIndex)) { $this->previousTextbook = null; $this->nextTextbook = null; return; } $this->previousTextbook = $this->makeSiblingPayload($records->get($currentIndex - 1)); $this->nextTextbook = $this->makeSiblingPayload($records->get($currentIndex + 1)); } protected function makeSiblingPayload(?Textbook $textbook): ?array { if (! $textbook) { return null; } return [ 'id' => (int) $textbook->getKey(), 'title' => (string) ($textbook->official_title ?? ''), 'meta' => $this->formatTextbookOrderLabel( isset($textbook->grade) ? (int) $textbook->grade : null, isset($textbook->semester) ? (int) $textbook->semester : null, ), 'url' => TextbookResource::getUrl('covers', ['record' => $textbook->getKey()]), ]; } protected function formatTextbookOrderLabel(?int $grade, ?int $semester): string { $gradeLabel = match ($grade) { 1 => '一年级', 2 => '二年级', 3 => '三年级', 4 => '四年级', 5 => '五年级', 6 => '六年级', 7 => '七年级', 8 => '八年级', 9 => '九年级', 10 => '高一', 11 => '高二', 12 => '高三', default => $grade ? "{$grade}年级" : '未分年级', }; $semesterLabel = match ($semester) { 1 => '上册', 2 => '下册', default => '未分学期', }; return "{$gradeLabel} · {$semesterLabel}"; } /** * @return list */ protected function splitCoverPath(?string $path): array { if ($path === null || $path === '') { return []; } return array_values(array_unique(array_filter(array_map('trim', explode(',', $path))))); } public function appendPhotos(): void { if ($this->photos === []) { \Filament\Notifications\Notification::make() ->title('请先选择图片') ->warning() ->send(); return; } $this->validate( [ 'photos' => 'array', 'photos.*' => 'image|max:10240', ], [ 'photos.*.uploaded' => '图片上传失败(可能文件过大或上传中断),请重试。', 'photos.*.image' => '仅支持 JPG / PNG / WEBP 图片。', 'photos.*.max' => '单张图片不能超过 10MB。', ] ); $storage = app(TextbookCoverStorageService::class); $added = 0; foreach ($this->photos as $file) { if (! $file instanceof TemporaryUploadedFile) { continue; } $url = $storage->uploadCover($file, (string) $this->recordId); if ($url !== null && $url !== '') { $this->coverUrls[] = $url; $added++; } } $this->photos = []; $n = \Filament\Notifications\Notification::make() ->title($added > 0 ? "已追加 {$added} 张" : '未能上传') ->body($added > 0 ? '确认顺序后,点击页面底部「保存到服务器」写入教材。' : '请检查图片格式或存储配置。'); if ($added > 0) { $this->hasUnsavedChanges = true; $n->success(); $this->dispatch('covers-appended', count: $added); } else { $n->warning(); } $n->send(); } public function removeAt(int $index): void { if (! isset($this->coverUrls[$index])) { return; } unset($this->coverUrls[$index]); $this->coverUrls = array_values($this->coverUrls); $this->hasUnsavedChanges = true; } public function moveUp(int $index): void { if ($index < 1 || ! isset($this->coverUrls[$index])) { return; } $tmp = $this->coverUrls[$index - 1]; $this->coverUrls[$index - 1] = $this->coverUrls[$index]; $this->coverUrls[$index] = $tmp; $this->hasUnsavedChanges = true; } public function moveDown(int $index): void { $n = count($this->coverUrls); if ($index >= $n - 1 || ! isset($this->coverUrls[$index])) { return; } $tmp = $this->coverUrls[$index + 1]; $this->coverUrls[$index + 1] = $this->coverUrls[$index]; $this->coverUrls[$index] = $tmp; $this->hasUnsavedChanges = true; } public function saveCovers(): void { \Log::info('ManageTextbookCovers::saveCovers start', [ 'record_id' => $this->recordId, 'count' => count($this->coverUrls), ]); $csv = $this->coverUrls === [] ? null : implode(',', $this->coverUrls); try { $response = app(TextbookApiService::class)->updateTextbook($this->recordId, [ 'cover_path' => $csv, ]); } catch (Throwable $e) { \Log::error('ManageTextbookCovers::saveCovers failed', [ 'record_id' => $this->recordId, 'error' => $e->getMessage(), ]); $this->saveFeedback = '保存失败:' . $e->getMessage(); $this->saveFeedbackType = 'error'; \Filament\Notifications\Notification::make() ->title('保存失败') ->body($e->getMessage()) ->danger() ->send(); return; } $row = $response['data'] ?? $response; if (is_array($row) && isset($row['cover_path']) && is_string($row['cover_path'])) { $this->coverUrls = $this->splitCoverPath($row['cover_path']); } \Log::info('ManageTextbookCovers::saveCovers success', [ 'record_id' => $this->recordId, 'saved_count' => count($this->coverUrls), ]); $isCleared = ($this->coverUrls === []); $this->saveFeedback = ($isCleared ? '已清空并保存' : '保存成功') . '(' . now()->format('H:i:s') . ')'; $this->saveFeedbackType = 'success'; $this->hasUnsavedChanges = false; \Filament\Notifications\Notification::make() ->title($isCleared ? '配图已清空' : '配图已保存') ->success() ->send(); } public function getTitle(): string { return '教材配图'; } public function getView(): string { return 'filament.resources.textbook-resource.manage-covers'; } }