ManageTextbookCovers.php 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. <?php
  2. namespace App\Filament\Resources\TextbookResource\Pages;
  3. use App\Filament\Resources\TextbookResource;
  4. use App\Services\TextbookApiService;
  5. use App\Services\TextbookCoverStorageService;
  6. use Filament\Resources\Pages\Page;
  7. use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
  8. use Livewire\WithFileUploads;
  9. use Throwable;
  10. /**
  11. * 教材配图独立页:与「编辑教材」分离,支持多次选择多图追加、排序、删除后统一保存。
  12. */
  13. class ManageTextbookCovers extends Page
  14. {
  15. use WithFileUploads;
  16. protected static string $resource = TextbookResource::class;
  17. public ?int $recordId = null;
  18. public string $officialTitle = '';
  19. /** @var list<string> */
  20. public array $coverUrls = [];
  21. /** 本次待追加的本地文件(可多次选择、多次点「追加到列表」) */
  22. public array $photos = [];
  23. public ?string $saveFeedback = null;
  24. public string $saveFeedbackType = 'info';
  25. /**
  26. * 必须与路由参数名一致({record}),Livewire 全页组件会把 URL 段注入 mount,勿仅用 Request::route(部分环境下为空)。
  27. */
  28. public function mount(int|string $record): void
  29. {
  30. $this->recordId = (int) $record;
  31. if ($this->recordId <= 0) {
  32. abort(404);
  33. }
  34. $data = app(TextbookApiService::class)->getTextbook($this->recordId);
  35. if (! $data) {
  36. abort(404);
  37. }
  38. $this->officialTitle = (string) ($data['official_title'] ?? '');
  39. $this->coverUrls = $this->splitCoverPath($data['cover_path'] ?? null);
  40. }
  41. /**
  42. * @return list<string>
  43. */
  44. protected function splitCoverPath(?string $path): array
  45. {
  46. if ($path === null || $path === '') {
  47. return [];
  48. }
  49. return array_values(array_unique(array_filter(array_map('trim', explode(',', $path)))));
  50. }
  51. public function appendPhotos(): void
  52. {
  53. if ($this->photos === []) {
  54. \Filament\Notifications\Notification::make()
  55. ->title('请先选择图片')
  56. ->warning()
  57. ->send();
  58. return;
  59. }
  60. $this->validate([
  61. 'photos' => 'array',
  62. 'photos.*' => 'image|max:5120',
  63. ]);
  64. $storage = app(TextbookCoverStorageService::class);
  65. $added = 0;
  66. foreach ($this->photos as $file) {
  67. if (! $file instanceof TemporaryUploadedFile) {
  68. continue;
  69. }
  70. $url = $storage->uploadCover($file, (string) $this->recordId);
  71. if ($url !== null && $url !== '') {
  72. $this->coverUrls[] = $url;
  73. $added++;
  74. }
  75. }
  76. $this->photos = [];
  77. $n = \Filament\Notifications\Notification::make()
  78. ->title($added > 0 ? "已追加 {$added} 张" : '未能上传')
  79. ->body($added > 0 ? '确认顺序后,点击页面底部「保存到服务器」写入教材。' : '请检查图片格式或存储配置。');
  80. if ($added > 0) {
  81. $n->success();
  82. } else {
  83. $n->warning();
  84. }
  85. $n->send();
  86. }
  87. public function removeAt(int $index): void
  88. {
  89. if (! isset($this->coverUrls[$index])) {
  90. return;
  91. }
  92. unset($this->coverUrls[$index]);
  93. $this->coverUrls = array_values($this->coverUrls);
  94. }
  95. public function moveUp(int $index): void
  96. {
  97. if ($index < 1 || ! isset($this->coverUrls[$index])) {
  98. return;
  99. }
  100. $tmp = $this->coverUrls[$index - 1];
  101. $this->coverUrls[$index - 1] = $this->coverUrls[$index];
  102. $this->coverUrls[$index] = $tmp;
  103. }
  104. public function moveDown(int $index): void
  105. {
  106. $n = count($this->coverUrls);
  107. if ($index >= $n - 1 || ! isset($this->coverUrls[$index])) {
  108. return;
  109. }
  110. $tmp = $this->coverUrls[$index + 1];
  111. $this->coverUrls[$index + 1] = $this->coverUrls[$index];
  112. $this->coverUrls[$index] = $tmp;
  113. }
  114. public function saveCovers(): void
  115. {
  116. if ($this->coverUrls === []) {
  117. $this->saveFeedback = '当前没有可保存的配图,请先上传并加入列表。';
  118. $this->saveFeedbackType = 'warning';
  119. \Filament\Notifications\Notification::make()
  120. ->title('暂无可保存图片')
  121. ->warning()
  122. ->send();
  123. return;
  124. }
  125. \Log::info('ManageTextbookCovers::saveCovers start', [
  126. 'record_id' => $this->recordId,
  127. 'count' => count($this->coverUrls),
  128. ]);
  129. $csv = $this->coverUrls === [] ? null : implode(',', $this->coverUrls);
  130. try {
  131. $response = app(TextbookApiService::class)->updateTextbook($this->recordId, [
  132. 'cover_path' => $csv,
  133. ]);
  134. } catch (Throwable $e) {
  135. \Log::error('ManageTextbookCovers::saveCovers failed', [
  136. 'record_id' => $this->recordId,
  137. 'error' => $e->getMessage(),
  138. ]);
  139. $this->saveFeedback = '保存失败:' . $e->getMessage();
  140. $this->saveFeedbackType = 'error';
  141. \Filament\Notifications\Notification::make()
  142. ->title('保存失败')
  143. ->body($e->getMessage())
  144. ->danger()
  145. ->send();
  146. return;
  147. }
  148. $row = $response['data'] ?? $response;
  149. if (is_array($row) && isset($row['cover_path']) && is_string($row['cover_path'])) {
  150. $this->coverUrls = $this->splitCoverPath($row['cover_path']);
  151. }
  152. \Log::info('ManageTextbookCovers::saveCovers success', [
  153. 'record_id' => $this->recordId,
  154. 'saved_count' => count($this->coverUrls),
  155. ]);
  156. $this->saveFeedback = '保存成功(' . now()->format('H:i:s') . ')';
  157. $this->saveFeedbackType = 'success';
  158. \Filament\Notifications\Notification::make()
  159. ->title('配图已保存')
  160. ->success()
  161. ->send();
  162. }
  163. public function getTitle(): string
  164. {
  165. return '教材配图';
  166. }
  167. public function getView(): string
  168. {
  169. return 'filament.resources.textbook-resource.manage-covers';
  170. }
  171. }