TextbookTable.php 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <?php
  2. namespace App\Filament\Resources\TextbookResource\Tables;
  3. use App\Filament\Resources\TextbookResource;
  4. use Filament\Actions\EditAction;
  5. use Filament\Actions\Action;
  6. use Filament\Actions\BulkAction;
  7. use Filament\Tables;
  8. use Filament\Tables\Columns\TextColumn;
  9. use Filament\Tables\Filters\SelectFilter;
  10. use Filament\Tables\Table;
  11. use Illuminate\Database\Eloquent\Model;
  12. use Illuminate\Support\Facades\Storage;
  13. use Illuminate\Support\Str;
  14. use Filament\Tables\Enums\FiltersLayout;
  15. use Filament\Tables\Filters\Filter;
  16. use Filament\Forms\Components\TextInput;
  17. class TextbookTable
  18. {
  19. public static function make(Table $table): Table
  20. {
  21. // 直接返回配置好的表格,不使用query()
  22. return $table
  23. ->columns([
  24. TextColumn::make('official_title')
  25. ->label('教材信息')
  26. ->html()
  27. ->formatStateUsing(function ($state, Model $record): string {
  28. $cover = $record->cover_path ?? null;
  29. $coverUrl = null;
  30. if ($cover) {
  31. $coverUrl = Str::startsWith($cover, ['http://', 'https://', '/'])
  32. ? $cover
  33. : Storage::disk('public')->url($cover);
  34. }
  35. $seriesName = $record->series->name ?? '未归类系列';
  36. $stage = match ($record->stage) {
  37. 'primary' => '小学',
  38. 'junior' => '初中',
  39. 'senior' => '高中',
  40. default => $record->stage ?: '未标注',
  41. };
  42. $semester = match ($record->semester) {
  43. 1 => '上学期',
  44. 2 => '下学期',
  45. default => '未标注',
  46. };
  47. $naming = match ($record->naming_scheme) {
  48. 'new' => '新体系',
  49. 'old' => '旧体系',
  50. default => $record->naming_scheme ?: '未标注',
  51. };
  52. $status = match ($record->status) {
  53. 'draft' => '草稿',
  54. 'published' => '已发布',
  55. 'archived' => '已归档',
  56. default => $record->status ?: '未知',
  57. };
  58. $badgeTone = match ($record->status) {
  59. 'published' => 'text-emerald-600 bg-emerald-50 border-emerald-100',
  60. 'draft' => 'text-amber-600 bg-amber-50 border-amber-100',
  61. 'archived' => 'text-slate-500 bg-slate-100 border-slate-200',
  62. default => 'text-slate-500 bg-slate-100 border-slate-200',
  63. };
  64. $title = e($state ?: '未命名教材');
  65. $coverHtml = $coverUrl
  66. ? "<img src=\"{$coverUrl}\" alt=\"封面\" class=\"h-16 w-12 rounded-md border border-slate-200 object-cover\" />"
  67. : "<div class=\"flex h-16 w-12 items-center justify-center rounded-md border border-dashed border-slate-200 bg-slate-50 text-xs text-slate-400\">封面</div>";
  68. $gradeLabel = $record->grade ? "{$record->grade}年级" : '年级未标注';
  69. $isbnLabel = $record->isbn ?: '未填写';
  70. return <<<HTML
  71. <div class="flex items-start gap-4">
  72. {$coverHtml}
  73. <div class="min-w-0 flex-1">
  74. <div class="flex flex-wrap items-center gap-2">
  75. <div class="font-semibold text-slate-900">{$title}</div>
  76. <span class="inline-flex items-center rounded-full border px-2 py-0.5 text-xs {$badgeTone}">{$status}</span>
  77. </div>
  78. <div class="mt-1 text-xs text-slate-500">{$seriesName} · {$stage} · {$gradeLabel} · {$semester}</div>
  79. <div class="mt-2 flex flex-wrap gap-2 text-xs text-slate-500">
  80. <span class="ui-tag">体系:{$naming}</span>
  81. <span class="ui-tag">ISBN:{$isbnLabel}</span>
  82. <span class="ui-tag">ID:{$record->id}</span>
  83. </div>
  84. </div>
  85. </div>
  86. HTML;
  87. })
  88. ->wrap(),
  89. TextColumn::make('created_at')
  90. ->label('创建时间')
  91. ->dateTime()
  92. ->sortable()
  93. ->toggleable(isToggledHiddenByDefault: true),
  94. TextColumn::make('updated_at')
  95. ->label('更新时间')
  96. ->dateTime()
  97. ->sortable()
  98. ->toggleable(isToggledHiddenByDefault: true),
  99. ])
  100. ->filters([
  101. SelectFilter::make('stage')
  102. ->label('学段')
  103. ->options([
  104. 'primary' => '小学',
  105. 'junior' => '初中',
  106. 'senior' => '高中',
  107. ]),
  108. SelectFilter::make('grade')
  109. ->label('年级')
  110. ->options(collect(range(1, 12))->mapWithKeys(fn ($grade) => [$grade => "{$grade}年级"])->all()),
  111. SelectFilter::make('semester')
  112. ->label('学期')
  113. ->options([
  114. 1 => '上学期',
  115. 2 => '下学期',
  116. ]),
  117. SelectFilter::make('naming_scheme')
  118. ->label('教材体系')
  119. ->options([
  120. 'new' => '新体系',
  121. 'old' => '旧体系',
  122. ]),
  123. SelectFilter::make('status')
  124. ->label('发布状态')
  125. ->options([
  126. 'draft' => '草稿',
  127. 'published' => '已发布',
  128. 'archived' => '已归档',
  129. ]),
  130. Filter::make('keyword')
  131. ->label('关键词')
  132. ->form([
  133. TextInput::make('value')
  134. ->placeholder('教材名称 / ISBN / 系列'),
  135. ]),
  136. ], layout: FiltersLayout::AboveContentCollapsible)
  137. ->actions([
  138. EditAction::make()
  139. ->label('编辑')
  140. ->icon('heroicon-o-pencil-square')
  141. ->iconButton()
  142. ->tooltip('编辑'),
  143. Action::make('delete')
  144. ->label('删除')
  145. ->color('danger')
  146. ->icon('heroicon-o-trash')
  147. ->iconButton()
  148. ->tooltip('删除')
  149. ->requiresConfirmation()
  150. ->modalHeading('删除教材')
  151. ->modalDescription('确定要删除这个教材吗?此操作无法撤销。')
  152. ->action(function (Model $record) {
  153. // 添加调试日志
  154. \Log::info('Deleting textbook', ['id' => $record->id, 'record' => $record]);
  155. if (!$record || !$record->id) {
  156. \Filament\Notifications\Notification::make()
  157. ->title('错误')
  158. ->body('无效的教材记录。')
  159. ->danger()
  160. ->send();
  161. return;
  162. }
  163. $apiService = app(\App\Services\TextbookApiService::class);
  164. $deleted = $apiService->deleteTextbook($record->id);
  165. \Log::info('Delete result', ['deleted' => $deleted]);
  166. if ($deleted) {
  167. \Filament\Notifications\Notification::make()
  168. ->title('成功')
  169. ->body('教材删除成功。')
  170. ->success()
  171. ->send();
  172. } else {
  173. \Filament\Notifications\Notification::make()
  174. ->title('错误')
  175. ->body('删除失败,请重试。')
  176. ->danger()
  177. ->send();
  178. }
  179. }),
  180. Action::make('view_catalog')
  181. ->label('查看目录')
  182. ->icon('heroicon-o-list-bullet')
  183. ->iconButton()
  184. ->tooltip('查看目录')
  185. ->url(fn(Model $record): string =>
  186. route('filament.admin.resources.textbook-catalogs.index', ['tableFilters[textbook_id][value]' => $record->id])
  187. ),
  188. ])
  189. ->bulkActions([
  190. \Filament\Actions\BulkActionGroup::make([
  191. \Filament\Actions\DeleteBulkAction::make()
  192. ->label('批量删除'),
  193. BulkAction::make('archive')
  194. ->label('批量归档')
  195. ->color('warning')
  196. ->icon('heroicon-o-archive-box')
  197. ->requiresConfirmation()
  198. ->action(function ($records): void {
  199. $apiService = app(\App\Services\TextbookApiService::class);
  200. foreach ($records as $record) {
  201. $apiService->updateTextbook((int) $record->id, ['status' => 'archived']);
  202. }
  203. }),
  204. ]),
  205. ])
  206. ->recordUrl(fn (Model $record): string => route('filament.admin.resources.textbooks.view', $record))
  207. ->defaultSort('sort_order')
  208. ->paginated([10, 25, 50, 100]);
  209. }
  210. }