QuestionReview.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <?php
  2. namespace App\Filament\Pages;
  3. use Filament\Forms;
  4. use Filament\Pages\Page;
  5. use Filament\Actions\Action;
  6. use Filament\Schemas\Schema;
  7. use Illuminate\Support\Facades\Http;
  8. use Illuminate\Support\Facades\Log;
  9. use Filament\Notifications\Notification;
  10. class QuestionReview extends Page implements Forms\Contracts\HasForms
  11. {
  12. use Forms\Concerns\InteractsWithForms;
  13. protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-clipboard-document-check';
  14. protected static string|\UnitEnum|null $navigationGroup = '题库校验';
  15. protected static ?string $title = '题目校验';
  16. protected string $view = 'filament.pages.question-review';
  17. public string $book = '';
  18. public int $page = 1;
  19. public array $mineru = [];
  20. public array $builder = [];
  21. public string $message = '';
  22. public bool $saving = false;
  23. public string $builderJson = '';
  24. public array $bookOptions = [];
  25. public array $pageOptions = [];
  26. public ?string $pagePngBase64 = null;
  27. public bool $showOverlay = true;
  28. public array $paths = [];
  29. public function mount(): void
  30. {
  31. // 默认值可在 .env 中配置
  32. $this->book = config('question_bank.default_book', '');
  33. $this->page = 1;
  34. // 预填可选书名(扫描 /data/mineru_raw)
  35. $root = base_path('../data/mineru_raw');
  36. $dirs = is_dir($root) ? scandir($root) : [];
  37. $opts = [];
  38. foreach ($dirs as $d) {
  39. if ($d === '.' || $d === '..') {
  40. continue;
  41. }
  42. if (is_dir($root . '/' . $d)) {
  43. $opts[] = $d;
  44. }
  45. }
  46. $this->bookOptions = $opts;
  47. if ($this->book) {
  48. $this->refreshPageOptions($this->book);
  49. }
  50. }
  51. public function form(Schema $form): Schema
  52. {
  53. return $form
  54. ->components([
  55. Forms\Components\Select::make('book')
  56. ->label('书名/目录名')
  57. ->options(array_combine($this->bookOptions, $this->bookOptions))
  58. ->native(false)
  59. ->searchable()
  60. ->placeholder('选择书名目录')
  61. ->reactive()
  62. ->afterStateUpdated(fn($state) => $this->onBookChange($state))
  63. ->required(),
  64. Forms\Components\Select::make('page')
  65. ->label('页码')
  66. ->options(fn() => array_combine(
  67. array_map('strval', $this->pageOptions),
  68. array_map(fn($p) => "第{$p}页", $this->pageOptions)
  69. ))
  70. ->native(false)
  71. ->searchable()
  72. ->placeholder('选择页码')
  73. ->reactive()
  74. ->afterStateUpdated(fn($state) => $this->onPageChange($state))
  75. ->required(),
  76. ]);
  77. }
  78. protected function getHeaderActions(): array
  79. {
  80. return [
  81. Action::make('load')
  82. ->label('加载')
  83. ->color('primary')
  84. ->action('loadPage'),
  85. Action::make('save')
  86. ->label('保存到草稿')
  87. ->color('success')
  88. ->disabled(fn() => empty($this->builder))
  89. ->action('saveDraft'),
  90. Action::make('toggleOverlay')
  91. ->label(fn() => $this->showOverlay ? '隐藏标框' : '显示标框')
  92. ->color('gray')
  93. ->action('toggleOverlay'),
  94. ];
  95. }
  96. protected function refreshPageOptions(string $book): void
  97. {
  98. $dir = base_path("../data/mineru_raw/{$book}/pages");
  99. $pages = [];
  100. if (is_dir($dir)) {
  101. foreach (glob($dir . '/page_*.json') as $file) {
  102. if (preg_match('/page_(\\d+)\\.json$/', $file, $m)) {
  103. $pages[] = (int)$m[1];
  104. }
  105. }
  106. }
  107. sort($pages);
  108. $this->pageOptions = $pages;
  109. if (!empty($pages)) {
  110. $this->page = $pages[0];
  111. }
  112. }
  113. public function onBookChange($book): void
  114. {
  115. $this->book = (string)$book;
  116. $this->refreshPageOptions($this->book);
  117. if ($this->book && $this->page) {
  118. $this->loadPage();
  119. }
  120. }
  121. public function onPageChange($page): void
  122. {
  123. if ($page === null || $page === '') {
  124. return;
  125. }
  126. $this->page = (int)$page;
  127. if ($this->book) {
  128. $this->loadPage();
  129. }
  130. }
  131. public function toggleOverlay(): void
  132. {
  133. $this->showOverlay = !$this->showOverlay;
  134. }
  135. public function loadPage(): void
  136. {
  137. $apiBase = rtrim(config('services.question_bank.base_url'), '/');
  138. $url = $apiBase . '/review/' . $this->book . '/' . $this->page;
  139. try {
  140. // 重置,避免读取失败时残留旧数据
  141. $this->mineru = [];
  142. $this->builder = [];
  143. $this->builderJson = '';
  144. $this->pagePngBase64 = null;
  145. $this->paths = [];
  146. $resp = Http::timeout(10)->get($url);
  147. if (!$resp->ok()) {
  148. $this->message = "加载失败: {$resp->status()} {$resp->body()} (检查 QuestionBankService /review/{book}/{page} 是否可用,或该页是否已生成)";
  149. return;
  150. }
  151. $data = $resp->json();
  152. $this->mineru = $data['mineru'] ?? [];
  153. $this->builder = $data['builder'] ?? [];
  154. $this->builderJson = json_encode($this->builder, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
  155. $this->pagePngBase64 = $data['page_png_base64'] ?? null;
  156. $this->paths = $data['paths'] ?? [];
  157. $this->message = '加载成功';
  158. } catch (\Throwable $e) {
  159. Log::warning('[QuestionReview] load failed: ' . $e->getMessage());
  160. $this->message = '加载异常: ' . $e->getMessage();
  161. }
  162. }
  163. public function saveDraft(): void
  164. {
  165. $payloadBuilder = $this->builder;
  166. if ($this->builderJson) {
  167. try {
  168. $decoded = json_decode($this->builderJson, true, 512, JSON_THROW_ON_ERROR);
  169. if (is_array($decoded)) {
  170. $payloadBuilder = $decoded;
  171. }
  172. } catch (\Throwable $e) {
  173. $this->message = '保存异常: 题目 JSON 解析失败 - ' . $e->getMessage();
  174. Notification::make()->title('保存异常')->body($this->message)->danger()->send();
  175. return;
  176. }
  177. }
  178. if (empty($payloadBuilder)) {
  179. $this->message = '无可保存的数据,请先加载';
  180. return;
  181. }
  182. $apiBase = rtrim(config('services.question_bank.base_url'), '/');
  183. $url = $apiBase . '/review/' . $this->book . '/' . $this->page;
  184. $payload = [
  185. 'status' => 'reviewed',
  186. 'questions' => $payloadBuilder['questions'] ?? $payloadBuilder,
  187. 'source_type' => 'workbook',
  188. ];
  189. try {
  190. $this->saving = true;
  191. $resp = Http::timeout(10)->post($url, $payload);
  192. $this->saving = false;
  193. if (!$resp->ok()) {
  194. $this->message = "保存失败: {$resp->status()} {$resp->body()}";
  195. Notification::make()->title('保存失败')->body($this->message)->danger()->send();
  196. return;
  197. }
  198. $this->message = '保存成功';
  199. Notification::make()->title('保存成功')->success()->send();
  200. } catch (\Throwable $e) {
  201. $this->saving = false;
  202. Log::warning('[QuestionReview] save failed: ' . $e->getMessage());
  203. $this->message = '保存异常: ' . $e->getMessage();
  204. Notification::make()->title('保存异常')->body($this->message)->danger()->send();
  205. }
  206. }
  207. public function saveQuestion(int $index): void
  208. {
  209. $list = $this->builder['questions'] ?? [];
  210. if (!isset($list[$index])) {
  211. $this->message = '未找到该题目';
  212. Notification::make()->title('保存失败')->body($this->message)->danger()->send();
  213. return;
  214. }
  215. $apiBase = rtrim(config('services.question_bank.base_url'), '/');
  216. $url = $apiBase . '/review/' . $this->book . '/' . $this->page;
  217. $payload = [
  218. 'status' => 'reviewed',
  219. 'questions' => [$list[$index]],
  220. 'source_type' => 'workbook',
  221. ];
  222. try {
  223. $resp = Http::timeout(10)->post($url, $payload);
  224. if (!$resp->ok()) {
  225. $this->message = "保存失败: {$resp->status()} {$resp->body()}";
  226. Notification::make()->title('保存失败')->body($this->message)->danger()->send();
  227. return;
  228. }
  229. $this->message = '单题保存成功';
  230. Notification::make()->title('保存成功')->success()->send();
  231. } catch (\Throwable $e) {
  232. $this->message = '保存异常: ' . $e->getMessage();
  233. Notification::make()->title('保存异常')->body($this->message)->danger()->send();
  234. }
  235. }
  236. }