| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- <?php
- namespace App\Filament\Resources;
- use App\Filament\Resources\PreQuestionCandidateResource\Pages;
- use App\Models\PreQuestionCandidate;
- use BackedEnum;
- use Filament\Actions\Action;
- use Filament\Actions\BulkActionGroup;
- use Filament\Forms\Components\TagsInput;
- use Filament\Forms\Components\Textarea;
- use Filament\Forms\Components\TextInput;
- use Filament\Forms\Components\Toggle;
- use Filament\Resources\Resource;
- use Filament\Schemas\Components\Section;
- use Filament\Tables;
- use Filament\Tables\Filters\TernaryFilter;
- use Illuminate\Database\Eloquent\Model;
- use UnitEnum;
- class PreQuestionCandidateResource extends Resource
- {
- protected static ?string $model = PreQuestionCandidate::class;
- protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-check-circle';
- protected static ?string $navigationLabel = '题目校对';
- protected static ?string $modelLabel = '题目候选';
- protected static ?string $pluralModelLabel = '题目候选';
- protected static UnitEnum|string|null $navigationGroup = '题库管理';
- protected static ?int $navigationSort = 2;
- protected static ?string $title = '候选题目校对';
- protected static ?string $description = '从 Markdown 导入解析出的候选题目,支持 AI 辅助校对和批量操作';
- public static function canViewAny(): bool
- {
- // 允许访问的条件:
- // 1. URL 中有 import_id 参数(从 MarkdownImport 跳转进入)
- $hasImportId = request()->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([
- Tables\Columns\TextColumn::make('sequence')
- ->label('序')
- ->sortable()
- ->width('60px'),
- Tables\Columns\TextColumn::make('index')
- ->label('题号')
- ->sortable()
- ->width('70px'),
- Tables\Columns\ViewColumn::make('raw_markdown')
- ->label('题目预览')
- ->view('filament.tables.columns.markdown-preview')
- ->columnSpanFull(),
- Tables\Columns\ImageColumn::make('first_image')
- ->label('图片')
- ->height(60)
- ->width(60)
- ->circular(),
- Tables\Columns\TextColumn::make('ai_confidence')
- ->label('AI 置信度')
- ->badge()
- ->color(fn (Model $record): string => $record->confidence_badge)
- ->formatStateUsing(fn (?float $state): string => $state ? number_format($state * 100, 1) . '%' : 'N/A'),
- Tables\Columns\ToggleColumn::make('is_question_candidate')
- ->label('是题目'),
- Tables\Columns\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' => '已被新解析覆盖',
- ]),
- ])
- ->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 数组(推荐)或直接粘贴 <table>...</table>'),
- ])->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('入库到筛选库'),
- ]),
- ])
- ->defaultSort('sequence', 'asc')
- ->paginated([20, 50, 100])
- ->poll('10s');
- }
- public static function getPages(): array
- {
- return [
- 'index' => Pages\ListPreQuestionCandidates::route('/'),
- ];
- }
- }
|