OCRRecordResource.php 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. <?php
  2. namespace App\Filament\Resources;
  3. use BackedEnum;
  4. use App\Filament\Resources\OCRRecordResource\Pages;
  5. use App\Models\OCRRecord;
  6. use App\Models\Student;
  7. use Filament\Resources\Resource;
  8. use Filament\Schemas\Schema;
  9. use Filament\Forms\Components\Select;
  10. use Filament\Forms\Components\FileUpload;
  11. use Filament\Forms\Components\Hidden;
  12. use Filament\Tables;
  13. use Filament\Tables\Table;
  14. use Illuminate\Database\Eloquent\Builder;
  15. use Filament\Infolists\Infolist;
  16. use Filament\Infolists\Components\TextEntry;
  17. use Filament\Infolists\Components\ImageEntry;
  18. use Filament\Infolists\Components\Section;
  19. use Filament\Resources\Pages\Page;
  20. use Filament\Resources\Pages\CreateRecord;
  21. use Illuminate\Database\Eloquent\Model;
  22. use Illuminate\Database\Eloquent\Collection;
  23. use Illuminate\Support\Facades\DB;
  24. class OCRRecordResource extends Resource
  25. {
  26. protected static ?string $model = OCRRecord::class;
  27. protected static ?string $slug = 'ocr-records-legacy';
  28. protected static ?string $modelLabel = 'OCR记录';
  29. protected static ?string $pluralModelLabel = 'OCR记录';
  30. protected static BackedEnum | string | null $navigationIcon = 'heroicon-o-camera';
  31. protected static ?string $navigationLabel = 'OCR识别记录';
  32. protected static ?int $navigationSort = 3;
  33. // 隐藏此Resource的导航,使用自定义DaisyUI页面
  34. protected static bool $shouldRegisterNavigation = false;
  35. public static function canCreate(): bool
  36. {
  37. return true;
  38. }
  39. public static function form(Schema $schema): Schema
  40. {
  41. return $schema->components([
  42. Select::make('student_id')
  43. ->label('选择学生')
  44. ->options(function () {
  45. return Student::query()
  46. ->orderBy('name')
  47. ->get()
  48. ->mapWithKeys(fn ($student) => [
  49. $student->student_id => "{$student->name} ({$student->grade} - {$student->class_name})"
  50. ]);
  51. })
  52. ->searchable()
  53. ->required(),
  54. FileUpload::make('file_path')
  55. ->label('卷子图片')
  56. ->image()
  57. ->directory('ocr-uploads')
  58. ->required()
  59. ->maxSize(10240)
  60. ->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp'])
  61. ->helperText('支持 JPG、PNG、WebP 格式,最大 10MB'),
  62. Hidden::make('status')
  63. ->default('pending'),
  64. Hidden::make('total_questions')
  65. ->default(0),
  66. Hidden::make('processed_questions')
  67. ->default(0),
  68. ]);
  69. }
  70. public static function table(Table $table): Table
  71. {
  72. return $table
  73. ->columns([
  74. Tables\Columns\TextColumn::make('student.name')
  75. ->label('学生姓名')
  76. ->searchable()
  77. ->sortable(),
  78. Tables\Columns\TextColumn::make('student.grade')
  79. ->label('年级')
  80. ->sortable(),
  81. Tables\Columns\TextColumn::make('student.class_name')
  82. ->label('班级')
  83. ->sortable(),
  84. Tables\Columns\TextColumn::make('image_filename')
  85. ->label('图片名称')
  86. ->searchable()
  87. ->wrap(),
  88. Tables\Columns\TextColumn::make('total_questions')
  89. ->label('题目总数')
  90. ->sortable()
  91. ->alignCenter(),
  92. Tables\Columns\TextColumn::make('processed_questions')
  93. ->label('已处理')
  94. ->sortable()
  95. ->alignCenter(),
  96. Tables\Columns\TextColumn::make('progress')
  97. ->label('处理进度')
  98. ->formatStateUsing(function ($record) {
  99. $total = $record->total_questions ?? 0;
  100. $processed = $record->processed_questions ?? 0;
  101. if ($total > 0) {
  102. return round(($processed / $total) * 100, 1) . '%';
  103. }
  104. return '-';
  105. })
  106. ->alignCenter(),
  107. Tables\Columns\TextColumn::make('confidence_avg')
  108. ->label('平均置信度')
  109. ->formatStateUsing(fn ($state) => $state ? round($state * 100, 2) . '%' : '-')
  110. ->color(fn ($state) => $state && $state > 0.7 ? 'success' : ($state && $state > 0.5 ? 'warning' : 'danger'))
  111. ->alignCenter(),
  112. Tables\Columns\TextColumn::make('status')
  113. ->label('状态')
  114. ->badge()
  115. ->formatStateUsing(fn (string $state): string => match ($state) {
  116. 'pending' => '待处理',
  117. 'processing' => '处理中',
  118. 'completed' => '已完成',
  119. 'failed' => '失败',
  120. default => $state,
  121. })
  122. ->color(fn (string $state): string => match ($state) {
  123. 'pending' => 'gray',
  124. 'processing' => 'info',
  125. 'completed' => 'success',
  126. 'failed' => 'danger',
  127. default => 'gray',
  128. })
  129. ->alignCenter(),
  130. Tables\Columns\TextColumn::make('created_at')
  131. ->label('创建时间')
  132. ->dateTime('Y-m-d H:i:s')
  133. ->sortable()
  134. ->toggleable(isToggledHiddenByDefault: false),
  135. Tables\Columns\TextColumn::make('processed_at')
  136. ->label('处理完成时间')
  137. ->dateTime('Y-m-d H:i:s')
  138. ->sortable()
  139. ->toggleable(isToggledHiddenByDefault: true),
  140. ])
  141. ->filters([
  142. Tables\Filters\SelectFilter::make('status')
  143. ->label('处理状态')
  144. ->options([
  145. 'pending' => '待处理',
  146. 'processing' => '处理中',
  147. 'completed' => '已完成',
  148. 'failed' => '失败',
  149. ]),
  150. Tables\Filters\SelectFilter::make('student_grade')
  151. ->label('年级')
  152. ->options(fn () => self::gradeOptions())
  153. ->query(function (Builder $query, array $data): void {
  154. if (!empty($data['value'])) {
  155. $query->whereHas('student', fn ($q) => $q->where('grade', $data['value']));
  156. }
  157. }),
  158. Tables\Filters\SelectFilter::make('student_class')
  159. ->label('班级')
  160. ->options(fn () => self::classOptions())
  161. ->query(function (Builder $query, array $data): void {
  162. if (!empty($data['value'])) {
  163. $query->whereHas('student', fn ($q) => $q->where('class_name', $data['value']));
  164. }
  165. }),
  166. Tables\Filters\Filter::make('created_today')
  167. ->label('今日创建')
  168. ->query(fn (Builder $query): Builder => $query->whereDate('created_at', today()))
  169. ->toggle(),
  170. ])
  171. ->actions([])
  172. ->defaultSort('created_at', 'desc')
  173. ->paginated([10, 25, 50, 100])
  174. ->poll('10s');
  175. }
  176. public static function getRelations(): array
  177. {
  178. return [
  179. //
  180. ];
  181. }
  182. public static function getPages(): array
  183. {
  184. return [
  185. 'index' => Pages\ListOCRRecords::route('/'),
  186. 'create' => Pages\CreateOCRRecord::route('/create'),
  187. 'view' => Pages\ViewOCRRecord::route('/{record}'),
  188. ];
  189. }
  190. protected static function gradeOptions(): array
  191. {
  192. return DB::table('students')
  193. ->distinct()
  194. ->pluck('grade', 'grade')
  195. ->toArray();
  196. }
  197. protected static function classOptions(): array
  198. {
  199. return DB::table('students')
  200. ->distinct()
  201. ->pluck('class_name', 'class_name')
  202. ->toArray();
  203. }
  204. public static function getRecordTitle(?Model $record): string|null
  205. {
  206. if (!$record) {
  207. return null;
  208. }
  209. $studentName = optional($record->student)->name ?? '未知学生';
  210. return "{$studentName} - {$record->image_filename}";
  211. }
  212. }