has('import_id') && !empty(request()->input('import_id')); // 2. 或者是管理员角色(直接检查 role 字段) $user = auth()->user(); $isAdmin = $user && in_array($user->role, ['super_admin', 'admin']); return $hasImportId || $isAdmin; } public static function canCreate(): bool { // 不允许手动创建候选题目,只能从 Markdown 解析生成 return false; } public static function canEdit(Model $record): bool { // 允许在“人工校对”阶段对候选题进行编辑(题干/选项/标记) return true; } public static function table(Tables\Table $table): Tables\Table { return $table ->columns([ TextColumn::make('sequence') ->label('序') ->sortable() ->width('60px'), TextColumn::make('index') ->label('题号') ->sortable() ->width('70px'), TextColumn::make('question_number') ->label('原题号') ->sortable() ->toggleable() ->width('80px'), TextColumn::make('part.title') ->label('区块') ->toggleable() ->limit(16), TextColumn::make('sourcePaper.title') ->label('卷子') ->toggleable() ->limit(16), TextColumn::make('raw_markdown') ->label('题目预览') ->html() ->formatStateUsing(function (?string $state, Model $record): string { $stem = $record->stem ? e(\Illuminate\Support\Str::limit($record->stem, 120)) : e(\Illuminate\Support\Str::limit((string) $state, 120)); $hasIssue = $record->is_valid_question === false || $record->status === 'pending'; $issueTag = $hasIssue ? '需修正' : ''; return <<
{$stem}
区块:{$record->part?->title} 卷子:{$record->sourcePaper?->title} {$issueTag}
HTML; }) ->wrap() ->toggleable(), Tables\Columns\ImageColumn::make('first_image') ->label('图片') ->height(60) ->width(60) ->circular(), TextColumn::make('ai_confidence') ->label('AI 置信度') ->badge() ->color(fn (Model $record): string => $record->confidence_badge) ->formatStateUsing(function (?float $state, Model $record): string { $val = $state ?? $record->confidence; return $val !== null ? number_format((float)$val * 100, 1) . '%' : 'N/A'; }), TextColumn::make('structured_json') ->label('结构化') ->getStateUsing(fn (?Model $record) => $record?->structured_json ? '已生成' : '未生成') ->badge() ->color(fn (?Model $record) => $record?->structured_json ? 'success' : 'gray') ->toggleable(isToggledHiddenByDefault: true), Tables\Columns\ToggleColumn::make('is_question_candidate') ->label('是题目'), TextColumn::make('status') ->label('状态') ->badge() ->color(fn (Model $record): string => $record->status_badge), ]) ->filters([ Tables\Filters\SelectFilter::make('import_id') ->label('导入记录') ->options(function () { return \App\Models\MarkdownImport::query() ->pluck('file_name', 'id') ->toArray(); }) ->query(function ($query, $data) { if (!empty($data['value'])) { $query->where('import_id', $data['value']); } }), TernaryFilter::make('is_question_candidate') ->label('是否为题目'), Tables\Filters\SelectFilter::make('status') ->label('审核状态') ->options([ 'ai_pending' => 'AI 解析中', 'pending' => '待审核', 'reviewed' => '已审核', 'accepted' => '已接受', 'rejected' => '已拒绝', 'superseded' => '已被新解析覆盖', ]), ], layout: FiltersLayout::AboveContentCollapsible) ->actions([ Action::make('review_edit') ->label('校对/编辑') ->icon('heroicon-o-pencil-square') ->color('primary') ->modalHeading(fn (Model $record): string => "校对候选题 #{$record->index}") ->form([ Section::make('审核标记') ->schema([ Toggle::make('is_question_candidate') ->label('是题目') ->default(fn (Model $record) => (bool) $record->is_question_candidate), TextInput::make('ai_confidence') ->label('AI 置信度') ->disabled(), ])->columns(2), Section::make('原始 Markdown(可编辑)') ->schema([ Textarea::make('raw_markdown') ->label('raw_markdown') ->rows(10) ->required(), ])->columnSpanFull(), Section::make('结构化字段(可编辑)') ->schema([ Textarea::make('stem') ->label('题干(stem)') ->rows(6), Textarea::make('options') ->label('选项(JSON)') ->rows(6) ->helperText('示例:{"A":"...","B":"..."};没有选项填空留空'), TagsInput::make('images') ->label('图片 URLs') ->placeholder('https://...'), Textarea::make('tables') ->label('表格(JSON 数组或 HTML)') ->rows(6) ->helperText('支持填写 JSON 数组(推荐)或直接粘贴 ...
'), ])->columns(2), ]) ->fillForm(function (Model $record): array { return [ 'is_question_candidate' => (bool) $record->is_question_candidate, 'ai_confidence' => $record->ai_confidence ? number_format($record->ai_confidence * 100, 1) : null, 'raw_markdown' => (string) $record->raw_markdown, 'stem' => $record->stem, 'options' => $record->options ? json_encode($record->options, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : null, 'images' => $record->images ?? [], 'tables' => $record->tables ? json_encode($record->tables, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : null, ]; }) ->action(function (array $data, Model $record): void { $options = null; if (!empty($data['options'])) { $decoded = json_decode((string) $data['options'], true); if (json_last_error() === JSON_ERROR_NONE) { $options = $decoded; } } $tables = []; if (!empty($data['tables'])) { $decoded = json_decode((string) $data['tables'], true); if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { $tables = $decoded; } else { $tables = [(string) $data['tables']]; } } $record->update([ 'raw_markdown' => (string) $data['raw_markdown'], 'stem' => $data['stem'] ?? null, 'options' => $options, 'images' => $data['images'] ?? [], 'tables' => $tables, 'is_question_candidate' => (bool) ($data['is_question_candidate'] ?? false), 'status' => 'reviewed', ]); }), ]) ->bulkActions([ BulkActionGroup::make([ \App\Filament\Resources\PreQuestionCandidateResource\Actions\MarkAsQuestionsBulkAction::make() ->label('标记为题目'), \App\Filament\Resources\PreQuestionCandidateResource\Actions\MarkAsNonQuestionsBulkAction::make() ->label('标记为非题目'), \App\Filament\Resources\PreQuestionCandidateResource\Actions\ConvertToPreQuestionsBulkAction::make() ->label('入库到筛选库'), ]), ]) ->recordClasses(fn (Model $record) => $record->is_valid_question === false ? 'bg-rose-50/60' : null) ->defaultSort('sequence', 'asc') ->paginated([20, 50, 100]); } public static function getPages(): array { return [ 'index' => Pages\ListPreQuestionCandidates::route('/'), ]; } }