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()
->orderByDesc('id')
->get(['id', 'file_name'])
->mapWithKeys(function ($import) {
$label = $import->file_name ?: '未命名导入';
return [$import->id => $label];
})
->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('/'),
];
}
}