ExamDetail.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Services\QuestionBankService;
  4. use BackedEnum;
  5. use Filament\Notifications\Notification;
  6. use Filament\Pages\Page;
  7. use UnitEnum;
  8. use Livewire\Attributes\Computed;
  9. class ExamDetail extends Page
  10. {
  11. protected static ?string $title = '试卷详情';
  12. protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
  13. protected static ?string $navigationLabel = '试卷详情';
  14. protected static string|UnitEnum|null $navigationGroup = '管理';
  15. protected static ?int $navigationSort = 15;
  16. protected string $view = 'filament.pages.exam-detail';
  17. // 试卷ID(从URL参数获取)
  18. public ?string $paperId = null;
  19. // 试卷详情
  20. public array $paperDetail = [];
  21. // 编辑功能
  22. public ?string $editingExamId = null;
  23. public array $editForm = [];
  24. // 题目编辑功能
  25. public array $availableQuestions = [];
  26. public ?string $selectedKnowledgePoint = null;
  27. public ?string $selectedQuestionType = null;
  28. public bool $showAddQuestionModal = false;
  29. // 是否正在加载
  30. public bool $isLoading = false;
  31. // 是否显示预览
  32. public bool $showPreview = false;
  33. public function mount()
  34. {
  35. // 从URL查询参数获取paperId
  36. $this->paperId = request()->query('paperId');
  37. if ($this->paperId) {
  38. $this->loadPaperDetail();
  39. }
  40. }
  41. protected function loadPaperDetail()
  42. {
  43. if (!$this->paperId) {
  44. $this->paperDetail = [];
  45. return;
  46. }
  47. $paper = \App\Models\Paper::with(['questions' => function($query) {
  48. $query->orderBy('question_number');
  49. }])->find($this->paperId);
  50. if ($paper) {
  51. $this->paperDetail = [
  52. 'paper_id' => $paper->paper_id,
  53. 'paper_name' => $paper->paper_name,
  54. 'question_count' => $paper->questions->count(),
  55. 'total_score' => $paper->total_score,
  56. 'difficulty_category' => $paper->difficulty_category,
  57. 'status' => $paper->status,
  58. 'created_at' => $paper->created_at,
  59. 'updated_at' => $paper->updated_at,
  60. 'questions' => $paper->questions->map(function($question) {
  61. // 直接使用paper_questions表中的数据,不依赖外部Question模型
  62. return [
  63. 'id' => $question->id,
  64. 'question_id' => $question->question_id,
  65. 'question_number' => $question->question_number,
  66. 'question_bank_id' => $question->question_bank_id,
  67. 'question_type' => $question->question_type,
  68. 'score' => $question->score,
  69. 'knowledge_point' => $question->knowledge_point,
  70. 'difficulty' => $question->difficulty,
  71. 'estimated_time' => $question->estimated_time,
  72. // 使用question_text字段(如果有的话)
  73. 'stem' => $question->question_text ?: '题目详情请查看题库系统',
  74. 'answer' => '',
  75. 'solution' => '',
  76. 'question_code' => 'QB_' . $question->question_bank_id,
  77. 'difficulty_label' => $this->getDifficultyLabel($question->difficulty),
  78. ];
  79. })->toArray(),
  80. ];
  81. } else {
  82. $this->paperDetail = [];
  83. }
  84. // 切换试卷时关闭预览,避免展示旧数据
  85. $this->showPreview = false;
  86. }
  87. /**
  88. * 根据难度值获取难度标签
  89. */
  90. private function getDifficultyLabel(?float $difficulty): string
  91. {
  92. if ($difficulty === null) {
  93. return '未知';
  94. }
  95. return match (true) {
  96. $difficulty <= 0.4 => '基础',
  97. $difficulty <= 0.7 => '中等',
  98. default => '拔高',
  99. };
  100. }
  101. public function startEditExam()
  102. {
  103. $paper = \App\Models\Paper::find($this->paperId);
  104. if ($paper) {
  105. $this->editingExamId = $this->paperId;
  106. $this->editForm = [
  107. 'paper_name' => $paper->paper_name,
  108. 'difficulty_category' => $paper->difficulty_category,
  109. 'status' => $paper->status,
  110. ];
  111. }
  112. }
  113. public function saveExamEdit()
  114. {
  115. try {
  116. $paper = \App\Models\Paper::find($this->paperId);
  117. if (!$paper) {
  118. throw new \Exception('试卷不存在');
  119. }
  120. // 验证表单数据
  121. $validated = \Illuminate\Support\Facades\Validator::make($this->editForm, [
  122. 'paper_name' => 'required|string|max:255',
  123. 'difficulty_category' => 'required|in:基础,进阶,竞赛',
  124. 'status' => 'required|in:draft,completed,graded',
  125. ])->validate();
  126. $paper->update($validated);
  127. Notification::make()
  128. ->title('修改成功')
  129. ->body('试卷信息已更新')
  130. ->success()
  131. ->send();
  132. $this->reset('editingExamId', 'editForm');
  133. $this->loadPaperDetail();
  134. } catch (\Illuminate\Validation\ValidationException $e) {
  135. Notification::make()
  136. ->title('验证失败')
  137. ->body('请检查输入的数据是否正确')
  138. ->danger()
  139. ->send();
  140. } catch (\Exception $e) {
  141. \Illuminate\Support\Facades\Log::error('修改试卷失败', [
  142. 'paper_id' => $this->paperId,
  143. 'error' => $e->getMessage()
  144. ]);
  145. Notification::make()
  146. ->title('修改失败')
  147. ->body($e->getMessage())
  148. ->danger()
  149. ->send();
  150. }
  151. }
  152. public function cancelEdit()
  153. {
  154. $this->reset('editingExamId', 'editForm');
  155. }
  156. /**
  157. * 删除试卷中的题目
  158. */
  159. public function deleteQuestion(int $questionId)
  160. {
  161. try {
  162. \Illuminate\Support\Facades\DB::beginTransaction();
  163. $paperQuestion = \App\Models\PaperQuestion::where('paper_id', $this->paperId)
  164. ->where('id', $questionId)
  165. ->first();
  166. if (!$paperQuestion) {
  167. throw new \Exception('题目不存在');
  168. }
  169. $paperQuestion->delete();
  170. // 更新试卷的题目数量
  171. $paper = \App\Models\Paper::find($this->paperId);
  172. if ($paper) {
  173. $paper->question_count = $paper->questions()->count();
  174. $paper->total_score = $paper->questions()->sum('score');
  175. $paper->save();
  176. }
  177. \Illuminate\Support\Facades\DB::commit();
  178. Notification::make()
  179. ->title('删除成功')
  180. ->body('题目已从试卷中删除')
  181. ->success()
  182. ->send();
  183. $this->loadPaperDetail();
  184. } catch (\Exception $e) {
  185. \Illuminate\Support\Facades\DB::rollBack();
  186. \Illuminate\Support\Facades\Log::error('删除题目失败', [
  187. 'paper_id' => $this->paperId,
  188. 'question_id' => $questionId,
  189. 'error' => $e->getMessage()
  190. ]);
  191. Notification::make()
  192. ->title('删除失败')
  193. ->body($e->getMessage())
  194. ->danger()
  195. ->send();
  196. }
  197. }
  198. /**
  199. * 搜索可选题目
  200. * 注意:由于没有questions表,这里返回空数组
  201. * 实际使用时需要连接外部题库API
  202. */
  203. public function searchQuestions()
  204. {
  205. if (!$this->selectedKnowledgePoint && !$this->selectedQuestionType) {
  206. $this->availableQuestions = [];
  207. return;
  208. }
  209. try {
  210. // TODO: 连接外部题库API获取题目
  211. // 这里暂时返回空数组,实际项目中需要调用题库服务
  212. $this->availableQuestions = [];
  213. // 如果需要,可以显示提示信息
  214. Notification::make()
  215. ->title('提示')
  216. ->body('题库功能需要连接外部API,当前显示模拟数据')
  217. ->info()
  218. ->send();
  219. } catch (\Exception $e) {
  220. \Illuminate\Support\Facades\Log::error('搜索题目失败', ['error' => $e->getMessage()]);
  221. $this->availableQuestions = [];
  222. }
  223. }
  224. /**
  225. * 添加题目到试卷
  226. * 注意:由于没有questions表,这里暂时禁用了添加功能
  227. */
  228. public function addQuestion(int $questionBankId)
  229. {
  230. try {
  231. // TODO: 连接外部题库API获取题目详情
  232. // 这里暂时返回错误提示
  233. Notification::make()
  234. ->title('功能暂未开放')
  235. ->body('添加题目功能需要连接外部题库API,当前暂未开放')
  236. ->warning()
  237. ->send();
  238. } catch (\Exception $e) {
  239. \Illuminate\Support\Facades\Log::error('添加题目失败', [
  240. 'paper_id' => $this->paperId,
  241. 'question_bank_id' => $questionBankId,
  242. 'error' => $e->getMessage()
  243. ]);
  244. Notification::make()
  245. ->title('添加失败')
  246. ->body($e->getMessage())
  247. ->danger()
  248. ->send();
  249. }
  250. }
  251. /**
  252. * 根据题目确定题型
  253. * 已移除对Question模型的依赖
  254. */
  255. /**
  256. * 根据难度计算分数
  257. */
  258. private function calculateScore(float $difficulty): float
  259. {
  260. return match (true) {
  261. $difficulty <= 0.4 => 5.0,
  262. $difficulty <= 0.7 => 10.0,
  263. default => 15.0,
  264. };
  265. }
  266. /**
  267. * 根据难度计算预计用时(秒)
  268. */
  269. private function calculateEstimatedTime(float $difficulty): int
  270. {
  271. return match (true) {
  272. $difficulty <= 0.4 => 120,
  273. $difficulty <= 0.7 => 180,
  274. default => 300,
  275. };
  276. }
  277. /**
  278. * 预览卷子
  279. * 显示试卷预览,可以直接打印
  280. */
  281. public function previewPaper()
  282. {
  283. $previewUrl = $this->getPreviewUrl();
  284. $gradingUrl = $this->getGradingUrl();
  285. if (!$previewUrl || !$gradingUrl) {
  286. Notification::make()
  287. ->title('无法预览试卷')
  288. ->body('当前试卷 ID 不存在或尚未加载,无法展示试卷或判卷')
  289. ->danger()
  290. ->send();
  291. return;
  292. }
  293. $this->showPreview = true;
  294. // 通知前端刷新 iframe,保持与智能出卷页面一致
  295. $this->dispatch('refresh-preview', previewUrl: $previewUrl, gradingUrl: $gradingUrl);
  296. Notification::make()
  297. ->title('已打开试卷与判卷预览')
  298. ->body('预览区域会依次展示试卷正文和判卷页,便于连贯查看')
  299. ->success()
  300. ->send();
  301. }
  302. /**
  303. * 打印试卷
  304. */
  305. public function printPaper()
  306. {
  307. $previewUrl = $this->getPreviewUrl();
  308. $gradingUrl = $this->getGradingUrl();
  309. if (!$previewUrl || !$gradingUrl) {
  310. Notification::make()
  311. ->title('无法打印试卷或判卷')
  312. ->body('当前试卷 ID 不存在或尚未加载,无法启动打印')
  313. ->danger()
  314. ->send();
  315. return;
  316. }
  317. $this->showPreview = true;
  318. // 使用智能出卷 PDF 预览进行打印,保证样式一致
  319. $this->dispatch('print-paper', url: $previewUrl);
  320. $this->dispatch('print-paper', url: $gradingUrl);
  321. Notification::make()
  322. ->title('打印功能已启动')
  323. ->body('已同时打开试卷与判卷的打印窗口,如未弹出请检查浏览器弹窗设置')
  324. ->info()
  325. ->send();
  326. }
  327. private function getPreviewUrl(): ?string
  328. {
  329. $paperId = $this->paperDetail['paper_id'] ?? $this->paperId;
  330. if (empty($paperId)) {
  331. return null;
  332. }
  333. return route('filament.admin.auth.intelligent-exam.pdf', [
  334. 'paper_id' => $paperId,
  335. 'answer' => 'false',
  336. ]);
  337. }
  338. private function getGradingUrl(): ?string
  339. {
  340. $paperId = $this->paperDetail['paper_id'] ?? $this->paperId;
  341. if (empty($paperId)) {
  342. return null;
  343. }
  344. return route('filament.admin.auth.intelligent-exam.grading', [
  345. 'paper_id' => $paperId,
  346. ]);
  347. }
  348. /**
  349. * 复制试卷配置
  350. */
  351. public function duplicateExam()
  352. {
  353. $learningService = app(\App\Services\LearningAnalyticsService::class);
  354. // 提取试卷配置
  355. $examConfig = [
  356. 'paper_name' => $this->paperDetail['paper_name'] . ' (副本)',
  357. 'total_questions' => $this->paperDetail['question_count'],
  358. 'difficulty_category' => $this->paperDetail['difficulty_category'] ?? '基础',
  359. 'question_type_ratio' => [
  360. '选择题' => 40,
  361. '填空题' => 30,
  362. '解答题' => 30,
  363. ],
  364. 'difficulty_ratio' => [
  365. '基础' => 50,
  366. '中等' => 35,
  367. '拔高' => 15,
  368. ],
  369. ];
  370. Notification::make()
  371. ->title('试卷配置已复制')
  372. ->body('请前往智能出卷页面查看并使用该配置')
  373. ->success()
  374. ->send();
  375. }
  376. public function getStatusColor(string $status): string
  377. {
  378. return match($status) {
  379. 'draft' => 'ghost',
  380. 'completed' => 'success',
  381. 'graded' => 'primary',
  382. default => 'ghost',
  383. };
  384. }
  385. public function getStatusLabel(string $status): string
  386. {
  387. return match($status) {
  388. 'draft' => '草稿',
  389. 'completed' => '已完成',
  390. 'graded' => '已评分',
  391. default => '未知',
  392. };
  393. }
  394. public function getDifficultyColor(string $difficulty): string
  395. {
  396. return match($difficulty) {
  397. '基础' => 'success',
  398. '进阶' => 'warning',
  399. '竞赛' => 'error',
  400. default => 'ghost',
  401. };
  402. }
  403. }