|
@@ -35,7 +35,6 @@ use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
|
|
use App\Support\TextEncoding;
|
|
use App\Support\TextEncoding;
|
|
|
use App\Rules\MarkdownFileExtension;
|
|
use App\Rules\MarkdownFileExtension;
|
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
|
-use Filament\Tables\Columns\ProgressColumn;
|
|
|
|
|
use Filament\Tables\Enums\FiltersLayout;
|
|
use Filament\Tables\Enums\FiltersLayout;
|
|
|
|
|
|
|
|
class MarkdownImportResource extends Resource
|
|
class MarkdownImportResource extends Resource
|
|
@@ -215,149 +214,106 @@ class MarkdownImportResource extends Resource
|
|
|
{
|
|
{
|
|
|
return $table
|
|
return $table
|
|
|
->columns([
|
|
->columns([
|
|
|
|
|
+ // 文件名列 - 固定宽度,可折行显示
|
|
|
TextColumn::make('file_name')
|
|
TextColumn::make('file_name')
|
|
|
- ->label('文件名')
|
|
|
|
|
|
|
+ ->label('文件')
|
|
|
->searchable()
|
|
->searchable()
|
|
|
- ->sortable(),
|
|
|
|
|
-
|
|
|
|
|
- TextColumn::make('remote_url')
|
|
|
|
|
- ->label('源文件')
|
|
|
|
|
- ->getStateUsing(fn (?Model $record) => $record?->remote_url ? '查看' : '—')
|
|
|
|
|
- ->icon('heroicon-o-document-arrow-down')
|
|
|
|
|
- ->color('primary')
|
|
|
|
|
- ->url(fn (?Model $record) => $record?->remote_url)
|
|
|
|
|
- ->openUrlInNewTab()
|
|
|
|
|
- ->toggleable(),
|
|
|
|
|
|
|
+ ->sortable()
|
|
|
|
|
+ ->weight('bold')
|
|
|
|
|
+ ->color('gray-900')
|
|
|
|
|
+ ->wrap()
|
|
|
|
|
+ ->width('200px'),
|
|
|
|
|
|
|
|
- TextColumn::make('filename_parse_status')
|
|
|
|
|
- ->label('命名解析')
|
|
|
|
|
- ->badge()
|
|
|
|
|
|
|
+ // 状态列 - 固定宽度
|
|
|
|
|
+ TextColumn::make('current_status')
|
|
|
|
|
+ ->label('状态')
|
|
|
->getStateUsing(function (?Model $record): string {
|
|
->getStateUsing(function (?Model $record): string {
|
|
|
- if (!$record) {
|
|
|
|
|
- return '未知';
|
|
|
|
|
- }
|
|
|
|
|
- $parsed = $record->parseFilename();
|
|
|
|
|
- return empty($parsed) ? '不规范' : '正常';
|
|
|
|
|
- })
|
|
|
|
|
- ->color(function (?Model $record): string {
|
|
|
|
|
- if (!$record) {
|
|
|
|
|
- return 'gray';
|
|
|
|
|
- }
|
|
|
|
|
- return empty($record->parseFilename()) ? 'warning' : 'success';
|
|
|
|
|
- })
|
|
|
|
|
- ->tooltip(function (?Model $record): ?string {
|
|
|
|
|
- if (!$record) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- $parsed = $record->parseFilename();
|
|
|
|
|
- if (empty($parsed)) {
|
|
|
|
|
- return '系列_年级_学期_学科_名称';
|
|
|
|
|
- }
|
|
|
|
|
- return sprintf(
|
|
|
|
|
- '系列:%s 年级:%s 学期:%s 学科:%s 名称:%s',
|
|
|
|
|
- $parsed['series'] ?? '-',
|
|
|
|
|
- $parsed['grade'] ?? '-',
|
|
|
|
|
- $parsed['term'] ?? '-',
|
|
|
|
|
- $parsed['subject'] ?? '-',
|
|
|
|
|
- $parsed['name'] ?? '-'
|
|
|
|
|
- );
|
|
|
|
|
- })
|
|
|
|
|
- ->url(fn (?Model $record): ?string => $record ? route('filament.admin.pages.markdown-import-workbench', [
|
|
|
|
|
- 'import_id' => $record->id,
|
|
|
|
|
- ]) : null)
|
|
|
|
|
- ->openUrlInNewTab(),
|
|
|
|
|
-
|
|
|
|
|
- TextColumn::make('source_name')
|
|
|
|
|
- ->label('来源')
|
|
|
|
|
- ->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
|
|
|
+ if (!$record) return '—';
|
|
|
|
|
|
|
|
- TextColumn::make('status')
|
|
|
|
|
- ->label('状态')
|
|
|
|
|
|
|
+ return match ($record->status) {
|
|
|
|
|
+ 'pending' => '⏳ 待处理',
|
|
|
|
|
+ 'processing' => '🟡 处理中',
|
|
|
|
|
+ 'parsed' => '✅ 已解析',
|
|
|
|
|
+ 'reviewed' => '📝 已校对',
|
|
|
|
|
+ 'completed' => '🎉 已完成',
|
|
|
|
|
+ 'failed' => '❌ 处理失败',
|
|
|
|
|
+ default => '—',
|
|
|
|
|
+ };
|
|
|
|
|
+ })
|
|
|
->badge()
|
|
->badge()
|
|
|
- ->color(fn (string $state): string => match ($state) {
|
|
|
|
|
|
|
+ ->color(fn (?Model $record): string => match ($record?->status ?? '') {
|
|
|
'pending' => 'gray',
|
|
'pending' => 'gray',
|
|
|
'processing' => 'warning',
|
|
'processing' => 'warning',
|
|
|
- 'parsed' => 'info',
|
|
|
|
|
|
|
+ 'parsed' => 'success',
|
|
|
'reviewed' => 'primary',
|
|
'reviewed' => 'primary',
|
|
|
'completed' => 'success',
|
|
'completed' => 'success',
|
|
|
'failed' => 'danger',
|
|
'failed' => 'danger',
|
|
|
default => 'gray',
|
|
default => 'gray',
|
|
|
})
|
|
})
|
|
|
|
|
+ ->width('120px'),
|
|
|
|
|
+
|
|
|
|
|
+ // 详细信息列 - 自适应剩余空间,完整显示所有内容
|
|
|
|
|
+ TextColumn::make('detailed_progress')
|
|
|
|
|
+ ->label('详情')
|
|
|
->getStateUsing(function (?Model $record): string {
|
|
->getStateUsing(function (?Model $record): string {
|
|
|
- if (!$record) {
|
|
|
|
|
- return '—';
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!$record) return '—';
|
|
|
|
|
|
|
|
return match ($record->status) {
|
|
return match ($record->status) {
|
|
|
- 'pending' => '待处理',
|
|
|
|
|
- 'processing' => $record->progress_label ?: '处理中',
|
|
|
|
|
- 'parsed' => '已解析(待校对)',
|
|
|
|
|
- 'reviewed' => '已校对(待入库)',
|
|
|
|
|
- 'completed' => '已完成(已入库)',
|
|
|
|
|
- 'failed' => '失败' . ($record->progress_message ? "({$record->progress_message})" : ''),
|
|
|
|
|
- default => (string) $record->status,
|
|
|
|
|
|
|
+ 'processing' => $record->progress_message ?: 'AI 正在解析题目...',
|
|
|
|
|
+ 'parsed' => sprintf(
|
|
|
|
|
+ '已解析 %d 个候选题,请进入校对环节',
|
|
|
|
|
+ $record->parsed_count ?? 0
|
|
|
|
|
+ ),
|
|
|
|
|
+ 'reviewed' => sprintf(
|
|
|
|
|
+ '已校对 %d 个候选题,请确认入库',
|
|
|
|
|
+ $record->accepted_count ?? 0
|
|
|
|
|
+ ),
|
|
|
|
|
+ 'completed' => sprintf(
|
|
|
|
|
+ '成功入库 %d 个题目',
|
|
|
|
|
+ $record->accepted_count ?? 0
|
|
|
|
|
+ ),
|
|
|
|
|
+ 'failed' => $record->error_message ?: '未知错误',
|
|
|
|
|
+ 'pending' => '准备就绪,等待开始解析',
|
|
|
|
|
+ default => '—',
|
|
|
};
|
|
};
|
|
|
- }),
|
|
|
|
|
-
|
|
|
|
|
- TextColumn::make('progress_message')
|
|
|
|
|
- ->label('当前步骤')
|
|
|
|
|
- ->getStateUsing(fn (?Model $record) => $record?->progress_message ?: '—')
|
|
|
|
|
|
|
+ })
|
|
|
->wrap()
|
|
->wrap()
|
|
|
- ->limit(60),
|
|
|
|
|
|
|
+ ->color('gray-600')
|
|
|
|
|
+ ->width('1fr'), // 占据剩余所有空间
|
|
|
|
|
|
|
|
- TextColumn::make('progress_label')
|
|
|
|
|
- ->label('进度')
|
|
|
|
|
- ->getStateUsing(fn (?Model $record) => $record?->progress_label ?: '—')
|
|
|
|
|
- ->color('gray')
|
|
|
|
|
- ->toggleable(),
|
|
|
|
|
|
|
+ // 快速操作列 - 固定宽度
|
|
|
|
|
+ TextColumn::make('quick_actions')
|
|
|
|
|
+ ->label('操作')
|
|
|
|
|
+ ->getStateUsing(function (?Model $record): string {
|
|
|
|
|
+ if (!$record) return '—';
|
|
|
|
|
|
|
|
- ProgressColumn::make('progress_percent')
|
|
|
|
|
- ->label('进度条')
|
|
|
|
|
- ->getStateUsing(function (?Model $record): ?int {
|
|
|
|
|
- if (!$record) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- return $record->progress_percent;
|
|
|
|
|
|
|
+ return match ($record->status) {
|
|
|
|
|
+ 'processing' => '🔄 处理中',
|
|
|
|
|
+ 'parsed', 'reviewed' => '👁️ 查看校对',
|
|
|
|
|
+ 'completed' => '📊 查看结果',
|
|
|
|
|
+ 'failed' => '🔁 重试',
|
|
|
|
|
+ 'pending' => '▶️ 开始',
|
|
|
|
|
+ default => '—',
|
|
|
|
|
+ };
|
|
|
})
|
|
})
|
|
|
- ->visible(fn (?Model $record): bool => $record?->status === 'processing')
|
|
|
|
|
- ->color(fn (?int $progress): string => match (true) {
|
|
|
|
|
- $progress === null => 'gray',
|
|
|
|
|
- $progress < 30 => 'danger',
|
|
|
|
|
- $progress < 70 => 'warning',
|
|
|
|
|
- default => 'success',
|
|
|
|
|
|
|
+ ->url(function (?Model $record): ?string {
|
|
|
|
|
+ if (!$record) return null;
|
|
|
|
|
+
|
|
|
|
|
+ return match ($record->status) {
|
|
|
|
|
+ 'parsed', 'reviewed' => route('filament.admin.resources.pre-question-candidates.index', [
|
|
|
|
|
+ 'import_id' => $record->id,
|
|
|
|
|
+ 'tab' => $record->status === 'reviewed' ? 'reviewed' : null
|
|
|
|
|
+ ]),
|
|
|
|
|
+ 'completed' => route('filament.admin.pages.markdown-import-workbench', [
|
|
|
|
|
+ 'import_id' => $record->id,
|
|
|
|
|
+ ]),
|
|
|
|
|
+ default => null,
|
|
|
|
|
+ };
|
|
|
})
|
|
})
|
|
|
- ->formatStateUsing(fn (?int $state): string => $state !== null ? "{$state}%" : '—'),
|
|
|
|
|
-
|
|
|
|
|
- TextColumn::make('parsed_count')
|
|
|
|
|
- ->label('候选题数')
|
|
|
|
|
- ->getStateUsing(fn (?Model $record) => $record?->parsed_count ?? 0)
|
|
|
|
|
- ->sortable(),
|
|
|
|
|
-
|
|
|
|
|
- TextColumn::make('accepted_count')
|
|
|
|
|
- ->label('已接受')
|
|
|
|
|
- ->getStateUsing(fn (?Model $record) => $record?->accepted_count ?? 0)
|
|
|
|
|
- ->sortable(),
|
|
|
|
|
-
|
|
|
|
|
- TextColumn::make('created_at')
|
|
|
|
|
- ->label('导入时间')
|
|
|
|
|
- ->dateTime()
|
|
|
|
|
- ->sortable(),
|
|
|
|
|
-
|
|
|
|
|
- TextColumn::make('processing_started_at')
|
|
|
|
|
- ->label('开始')
|
|
|
|
|
- ->dateTime('m-d H:i')
|
|
|
|
|
- ->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
|
-
|
|
|
|
|
- TextColumn::make('processing_finished_at')
|
|
|
|
|
- ->label('结束')
|
|
|
|
|
- ->dateTime('m-d H:i')
|
|
|
|
|
- ->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
|
-
|
|
|
|
|
- TextColumn::make('error_message')
|
|
|
|
|
- ->label('错误')
|
|
|
|
|
- ->visible(fn (?Model $record): bool => $record?->status === 'failed')
|
|
|
|
|
|
|
+ ->color('primary')
|
|
|
|
|
+ ->weight('medium')
|
|
|
->wrap()
|
|
->wrap()
|
|
|
- ->limit(80),
|
|
|
|
|
|
|
+ ->width('100px'),
|
|
|
])
|
|
])
|
|
|
->filters([
|
|
->filters([
|
|
|
Tables\Filters\SelectFilter::make('status')
|
|
Tables\Filters\SelectFilter::make('status')
|
|
@@ -410,22 +366,71 @@ class MarkdownImportResource extends Resource
|
|
|
}),
|
|
}),
|
|
|
], layout: FiltersLayout::AboveContentCollapsible)
|
|
], layout: FiltersLayout::AboveContentCollapsible)
|
|
|
->actions([
|
|
->actions([
|
|
|
|
|
+ // 第一行按钮 - 主要操作
|
|
|
EditAction::make()
|
|
EditAction::make()
|
|
|
- ->label('编辑'),
|
|
|
|
|
|
|
+ ->label('编辑')
|
|
|
|
|
+ ->size('sm'),
|
|
|
|
|
|
|
|
Action::make('workbench')
|
|
Action::make('workbench')
|
|
|
- ->label('导入工作台')
|
|
|
|
|
|
|
+ ->label('工作台')
|
|
|
->icon('heroicon-o-rectangle-stack')
|
|
->icon('heroicon-o-rectangle-stack')
|
|
|
->color('primary')
|
|
->color('primary')
|
|
|
|
|
+ ->size('sm')
|
|
|
->visible(fn (?Model $record): bool => !empty($record?->parseFilename()))
|
|
->visible(fn (?Model $record): bool => !empty($record?->parseFilename()))
|
|
|
->url(fn (?Model $record): string => route('filament.admin.pages.markdown-import-workbench', [
|
|
->url(fn (?Model $record): string => route('filament.admin.pages.markdown-import-workbench', [
|
|
|
'import_id' => $record?->id,
|
|
'import_id' => $record?->id,
|
|
|
])),
|
|
])),
|
|
|
|
|
|
|
|
|
|
+ Action::make('review')
|
|
|
|
|
+ ->label('校对')
|
|
|
|
|
+ ->icon('heroicon-o-clipboard-document-list')
|
|
|
|
|
+ ->color('success')
|
|
|
|
|
+ ->size('sm')
|
|
|
|
|
+ ->visible(fn (?Model $record): bool => in_array($record?->status, ['parsed', 'reviewed', 'completed']) && !empty($record?->parseFilename()))
|
|
|
|
|
+ ->url(function (?Model $record): string {
|
|
|
|
|
+ $importId = $record?->id;
|
|
|
|
|
+ $status = $record?->status;
|
|
|
|
|
+
|
|
|
|
|
+ if ($status === 'parsed') {
|
|
|
|
|
+ return route('filament.admin.resources.pre-question-candidates.index', [
|
|
|
|
|
+ 'import_id' => $importId
|
|
|
|
|
+ ]);
|
|
|
|
|
+ } elseif (in_array($status, ['reviewed', 'completed'])) {
|
|
|
|
|
+ return route('filament.admin.resources.pre-question-candidates.index', [
|
|
|
|
|
+ 'import_id' => $importId,
|
|
|
|
|
+ 'tab' => 'reviewed'
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return route('filament.admin.resources.pre-question-candidates.index', [
|
|
|
|
|
+ 'import_id' => $importId
|
|
|
|
|
+ ]);
|
|
|
|
|
+ }),
|
|
|
|
|
+
|
|
|
|
|
+ Action::make('delete')
|
|
|
|
|
+ ->label('删除')
|
|
|
|
|
+ ->icon('heroicon-o-trash')
|
|
|
|
|
+ ->color('danger')
|
|
|
|
|
+ ->size('sm')
|
|
|
|
|
+ ->requiresConfirmation()
|
|
|
|
|
+ ->modalHeading('删除导入记录')
|
|
|
|
|
+ ->modalDescription('确定要删除这条导入记录吗?此操作不可撤销。')
|
|
|
|
|
+ ->action(function (?Model $record) {
|
|
|
|
|
+ if ($record) {
|
|
|
|
|
+ $record->delete();
|
|
|
|
|
+ Notification::make()
|
|
|
|
|
+ ->title('删除成功')
|
|
|
|
|
+ ->success()
|
|
|
|
|
+ ->send();
|
|
|
|
|
+ }
|
|
|
|
|
+ }),
|
|
|
|
|
+
|
|
|
|
|
+ // 第二行按钮 - 处理操作
|
|
|
Action::make('run_pipeline')
|
|
Action::make('run_pipeline')
|
|
|
- ->label('触发全流程')
|
|
|
|
|
|
|
+ ->label('全流程')
|
|
|
->icon('heroicon-o-play-circle')
|
|
->icon('heroicon-o-play-circle')
|
|
|
->color('success')
|
|
->color('success')
|
|
|
|
|
+ ->size('sm')
|
|
|
->requiresConfirmation()
|
|
->requiresConfirmation()
|
|
|
->modalHeading('触发 Markdown 拆分 + AI 结构化')
|
|
->modalHeading('触发 Markdown 拆分 + AI 结构化')
|
|
|
->modalDescription('立即提交队列,按 source_file → source_paper → paper_part → candidate → AI 结构化 执行。')
|
|
->modalDescription('立即提交队列,按 source_file → source_paper → paper_part → candidate → AI 结构化 执行。')
|
|
@@ -450,9 +455,10 @@ class MarkdownImportResource extends Resource
|
|
|
}),
|
|
}),
|
|
|
|
|
|
|
|
Action::make('parse')
|
|
Action::make('parse')
|
|
|
- ->label('解析 Markdown')
|
|
|
|
|
|
|
+ ->label('解析')
|
|
|
->icon('heroicon-o-cog-6-tooth')
|
|
->icon('heroicon-o-cog-6-tooth')
|
|
|
->color('info')
|
|
->color('info')
|
|
|
|
|
+ ->size('sm')
|
|
|
->visible(fn (?Model $record): bool => in_array($record?->status, ['pending', 'failed']))
|
|
->visible(fn (?Model $record): bool => in_array($record?->status, ['pending', 'failed']))
|
|
|
->requiresConfirmation()
|
|
->requiresConfirmation()
|
|
|
->modalHeading('解析 Markdown')
|
|
->modalHeading('解析 Markdown')
|
|
@@ -464,9 +470,10 @@ class MarkdownImportResource extends Resource
|
|
|
}),
|
|
}),
|
|
|
|
|
|
|
|
Action::make('ai_parse')
|
|
Action::make('ai_parse')
|
|
|
- ->label('AI 解析')
|
|
|
|
|
|
|
+ ->label('AI解析')
|
|
|
->icon('heroicon-o-sparkles')
|
|
->icon('heroicon-o-sparkles')
|
|
|
->color('warning')
|
|
->color('warning')
|
|
|
|
|
+ ->size('sm')
|
|
|
->visible(fn (?Model $record): bool => in_array($record?->status, ['pending', 'processing', 'parsed', 'failed']))
|
|
->visible(fn (?Model $record): bool => in_array($record?->status, ['pending', 'processing', 'parsed', 'failed']))
|
|
|
->requiresConfirmation()
|
|
->requiresConfirmation()
|
|
|
->modalHeading('重新执行 AI 解析')
|
|
->modalHeading('重新执行 AI 解析')
|
|
@@ -478,50 +485,6 @@ class MarkdownImportResource extends Resource
|
|
|
|
|
|
|
|
static::triggerAiParsing($record);
|
|
static::triggerAiParsing($record);
|
|
|
}),
|
|
}),
|
|
|
-
|
|
|
|
|
- Action::make('review')
|
|
|
|
|
- ->label('进入校对')
|
|
|
|
|
- ->icon('heroicon-o-clipboard-document-list')
|
|
|
|
|
- ->color('success')
|
|
|
|
|
- ->visible(fn (?Model $record): bool => in_array($record?->status, ['parsed', 'reviewed', 'completed']) && !empty($record?->parseFilename()))
|
|
|
|
|
- ->url(function (?Model $record): string {
|
|
|
|
|
- // 根据状态跳转到不同页面
|
|
|
|
|
- $importId = $record?->id;
|
|
|
|
|
- $status = $record?->status;
|
|
|
|
|
-
|
|
|
|
|
- // 兼容 PHP 7.4 的写法
|
|
|
|
|
- if ($status === 'parsed') {
|
|
|
|
|
- return route('filament.admin.resources.pre-question-candidates.index', [
|
|
|
|
|
- 'import_id' => $importId
|
|
|
|
|
- ]);
|
|
|
|
|
- } elseif (in_array($status, ['reviewed', 'completed'])) {
|
|
|
|
|
- return route('filament.admin.resources.pre-question-candidates.index', [
|
|
|
|
|
- 'import_id' => $importId,
|
|
|
|
|
- 'tab' => 'reviewed' // 显示已校对标签页
|
|
|
|
|
- ]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return route('filament.admin.resources.pre-question-candidates.index', [
|
|
|
|
|
- 'import_id' => $importId
|
|
|
|
|
- ]);
|
|
|
|
|
- }),
|
|
|
|
|
-
|
|
|
|
|
- Action::make('delete')
|
|
|
|
|
- ->label('删除')
|
|
|
|
|
- ->icon('heroicon-o-trash')
|
|
|
|
|
- ->color('danger')
|
|
|
|
|
- ->requiresConfirmation()
|
|
|
|
|
- ->modalHeading('删除导入记录')
|
|
|
|
|
- ->modalDescription('确定要删除这条导入记录吗?此操作不可撤销。')
|
|
|
|
|
- ->action(function (?Model $record) {
|
|
|
|
|
- if ($record) {
|
|
|
|
|
- $record->delete();
|
|
|
|
|
- Notification::make()
|
|
|
|
|
- ->title('删除成功')
|
|
|
|
|
- ->success()
|
|
|
|
|
- ->send();
|
|
|
|
|
- }
|
|
|
|
|
- }),
|
|
|
|
|
])
|
|
])
|
|
|
->bulkActions([
|
|
->bulkActions([
|
|
|
BulkActionGroup::make([
|
|
BulkActionGroup::make([
|
|
@@ -567,13 +530,6 @@ class MarkdownImportResource extends Resource
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 启用自动刷新,每5秒更新一次进度
|
|
|
|
|
- */
|
|
|
|
|
- public static function getPollingInterval(): ?string
|
|
|
|
|
- {
|
|
|
|
|
- return '5s';
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 解析 Markdown
|
|
* 解析 Markdown
|