book = config('question_bank.default_book', ''); $this->page = 1; // 预填可选书名(扫描 /data/mineru_raw) $root = base_path('../data/mineru_raw'); $dirs = is_dir($root) ? scandir($root) : []; $opts = []; foreach ($dirs as $d) { if ($d === '.' || $d === '..') { continue; } if (is_dir($root . '/' . $d)) { $opts[] = $d; } } $this->bookOptions = $opts; if ($this->book) { $this->refreshPageOptions($this->book); } } public function form(Schema $form): Schema { return $form ->components([ Forms\Components\Select::make('book') ->label('书名/目录名') ->options(array_combine($this->bookOptions, $this->bookOptions)) ->native(false) ->searchable() ->placeholder('选择书名目录') ->reactive() ->afterStateUpdated(fn($state) => $this->onBookChange($state)) ->required(), Forms\Components\Select::make('page') ->label('页码') ->options(fn() => array_combine( array_map('strval', $this->pageOptions), array_map(fn($p) => "第{$p}页", $this->pageOptions) )) ->native(false) ->searchable() ->placeholder('选择页码') ->reactive() ->afterStateUpdated(fn($state) => $this->onPageChange($state)) ->required(), ]); } protected function getHeaderActions(): array { return [ Action::make('load') ->label('加载') ->color('primary') ->action('loadPage'), Action::make('save') ->label('保存到草稿') ->color('success') ->disabled(fn() => empty($this->builder)) ->action('saveDraft'), Action::make('toggleOverlay') ->label(fn() => $this->showOverlay ? '隐藏标框' : '显示标框') ->color('gray') ->action('toggleOverlay'), ]; } protected function refreshPageOptions(string $book): void { $dir = base_path("../data/mineru_raw/{$book}/pages"); $pages = []; if (is_dir($dir)) { foreach (glob($dir . '/page_*.json') as $file) { if (preg_match('/page_(\\d+)\\.json$/', $file, $m)) { $pages[] = (int)$m[1]; } } } sort($pages); $this->pageOptions = $pages; if (!empty($pages)) { $this->page = $pages[0]; } } public function onBookChange($book): void { $this->book = (string)$book; $this->refreshPageOptions($this->book); if ($this->book && $this->page) { $this->loadPage(); } } public function onPageChange($page): void { if ($page === null || $page === '') { return; } $this->page = (int)$page; if ($this->book) { $this->loadPage(); } } public function toggleOverlay(): void { $this->showOverlay = !$this->showOverlay; } public function loadPage(): void { $apiBase = rtrim(config('services.question_bank.base_url'), '/'); $url = $apiBase . '/review/' . $this->book . '/' . $this->page; try { // 重置,避免读取失败时残留旧数据 $this->mineru = []; $this->builder = []; $this->builderJson = ''; $this->pagePngBase64 = null; $this->paths = []; $resp = Http::timeout(10)->get($url); if (!$resp->ok()) { $this->message = "加载失败: {$resp->status()} {$resp->body()} (检查 QuestionBankService /review/{book}/{page} 是否可用,或该页是否已生成)"; return; } $data = $resp->json(); $this->mineru = $data['mineru'] ?? []; $this->builder = $data['builder'] ?? []; $this->builderJson = json_encode($this->builder, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); $this->pagePngBase64 = $data['page_png_base64'] ?? null; $this->paths = $data['paths'] ?? []; $this->message = '加载成功'; } catch (\Throwable $e) { Log::warning('[QuestionReview] load failed: ' . $e->getMessage()); $this->message = '加载异常: ' . $e->getMessage(); } } public function saveDraft(): void { $payloadBuilder = $this->builder; if ($this->builderJson) { try { $decoded = json_decode($this->builderJson, true, 512, JSON_THROW_ON_ERROR); if (is_array($decoded)) { $payloadBuilder = $decoded; } } catch (\Throwable $e) { $this->message = '保存异常: 题目 JSON 解析失败 - ' . $e->getMessage(); Notification::make()->title('保存异常')->body($this->message)->danger()->send(); return; } } if (empty($payloadBuilder)) { $this->message = '无可保存的数据,请先加载'; return; } $apiBase = rtrim(config('services.question_bank.base_url'), '/'); $url = $apiBase . '/review/' . $this->book . '/' . $this->page; $payload = [ 'status' => 'reviewed', 'questions' => $payloadBuilder['questions'] ?? $payloadBuilder, 'source_type' => 'workbook', ]; try { $this->saving = true; $resp = Http::timeout(10)->post($url, $payload); $this->saving = false; if (!$resp->ok()) { $this->message = "保存失败: {$resp->status()} {$resp->body()}"; Notification::make()->title('保存失败')->body($this->message)->danger()->send(); return; } $this->message = '保存成功'; Notification::make()->title('保存成功')->success()->send(); } catch (\Throwable $e) { $this->saving = false; Log::warning('[QuestionReview] save failed: ' . $e->getMessage()); $this->message = '保存异常: ' . $e->getMessage(); Notification::make()->title('保存异常')->body($this->message)->danger()->send(); } } public function saveQuestion(int $index): void { $list = $this->builder['questions'] ?? []; if (!isset($list[$index])) { $this->message = '未找到该题目'; Notification::make()->title('保存失败')->body($this->message)->danger()->send(); return; } $apiBase = rtrim(config('services.question_bank.base_url'), '/'); $url = $apiBase . '/review/' . $this->book . '/' . $this->page; $payload = [ 'status' => 'reviewed', 'questions' => [$list[$index]], 'source_type' => 'workbook', ]; try { $resp = Http::timeout(10)->post($url, $payload); if (!$resp->ok()) { $this->message = "保存失败: {$resp->status()} {$resp->body()}"; Notification::make()->title('保存失败')->body($this->message)->danger()->send(); return; } $this->message = '单题保存成功'; Notification::make()->title('保存成功')->success()->send(); } catch (\Throwable $e) { $this->message = '保存异常: ' . $e->getMessage(); Notification::make()->title('保存异常')->body($this->message)->danger()->send(); } } }