exam-detail.blade.php 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. <div>
  2. <div class="space-y-6">
  3. <!-- 页面顶部:返回按钮和标题 -->
  4. <div class="flex items-center justify-between">
  5. <div class="flex items-center gap-4">
  6. <a href="{{ url('/admin/exam-history') }}"
  7. class="btn btn-ghost btn-sm">
  8. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  9. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
  10. </svg>
  11. 返回列表
  12. </a>
  13. <div>
  14. <h2 class="text-2xl font-bold text-gray-900">试卷详情</h2>
  15. <p class="mt-1 text-sm text-gray-500">
  16. 查看和编辑试卷信息,管理试卷中的题目
  17. </p>
  18. </div>
  19. </div>
  20. </div>
  21. @if(empty($paperDetail))
  22. <div class="alert alert-error">
  23. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  24. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
  25. </svg>
  26. <span>试卷不存在或已被删除</span>
  27. </div>
  28. @else
  29. <!-- 试卷基本信息卡片 -->
  30. <div class="card bg-base-100 shadow-xl">
  31. <div class="card-body">
  32. <div class="flex items-start justify-between">
  33. <div class="flex-1">
  34. <div class="flex items-center gap-3 mb-4">
  35. <h3 class="card-title text-xl">{{ $paperDetail['paper_name'] ?? '未命名试卷' }}</h3>
  36. <span class="badge badge-{{ $this->getStatusColor($paperDetail['status']) }}">
  37. {{ $this->getStatusLabel($paperDetail['status']) }}
  38. </span>
  39. <span class="badge badge-{{ $this->getDifficultyColor($paperDetail['difficulty_category']) }}">
  40. {{ $paperDetail['difficulty_category'] }}
  41. </span>
  42. </div>
  43. <div class="stats stats-horizontal shadow bg-base-200">
  44. <div class="stat">
  45. <div class="stat-title">题目数量</div>
  46. <div class="stat-value text-primary">{{ $paperDetail['question_count'] }}</div>
  47. <div class="stat-desc">题</div>
  48. </div>
  49. <div class="stat">
  50. <div class="stat-title">总分</div>
  51. <div class="stat-value text-secondary">{{ $paperDetail['total_score'] }}</div>
  52. <div class="stat-desc">分</div>
  53. </div>
  54. <div class="stat">
  55. <div class="stat-title">创建时间</div>
  56. <div class="stat-value text-lg" style="font-size: 1rem;">
  57. {{ \Carbon\Carbon::parse($paperDetail['created_at'])->format('Y-m-d') }}
  58. </div>
  59. <div class="stat-desc">
  60. {{ \Carbon\Carbon::parse($paperDetail['created_at'])->format('H:i') }}
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. <div class="flex gap-2">
  66. <button
  67. wire:click="startEditExam"
  68. class="btn btn-outline btn-sm">
  69. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  70. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
  71. </svg>
  72. 编辑试卷
  73. </button>
  74. <button
  75. wire:click="previewPaper"
  76. class="btn btn-outline btn-sm"
  77. title="预览试卷卷子,可以直接打印">
  78. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  79. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
  80. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
  81. </svg>
  82. 预览卷子
  83. </button>
  84. <button
  85. wire:click="duplicateExam"
  86. class="btn btn-outline btn-sm">
  87. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  88. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
  89. </svg>
  90. 复制配置
  91. </button>
  92. <button
  93. wire:click="$toggle('showAddQuestionModal')"
  94. class="btn btn-primary btn-sm">
  95. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  96. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
  97. </svg>
  98. 添加题目
  99. </button>
  100. </div>
  101. </div>
  102. </div>
  103. </div>
  104. <!-- 题目列表 -->
  105. <div class="card bg-base-100 shadow-xl">
  106. <div class="card-body">
  107. <div class="flex items-center justify-between mb-4">
  108. <h3 class="card-title">试卷题目</h3>
  109. <span class="badge badge-lg">{{ count($paperDetail['questions']) }} 题</span>
  110. </div>
  111. @forelse($paperDetail['questions'] as $question)
  112. <div class="border rounded-lg p-4 mb-4 hover:bg-gray-50">
  113. <div class="flex items-start justify-between gap-4">
  114. <div class="flex-1">
  115. <!-- 题目头部信息 -->
  116. <div class="flex items-center gap-2 mb-3">
  117. <span class="badge badge-primary badge-lg">
  118. 第 {{ $question['question_number'] }} 题
  119. </span>
  120. <span class="badge badge-sm">
  121. {{ $question['question_type'] }}
  122. </span>
  123. <span class="badge badge-outline badge-sm">
  124. {{ $question['knowledge_point'] }}
  125. </span>
  126. <span class="badge badge-{{ $question['difficulty'] <= 0.4 ? 'success' : ($question['difficulty'] <= 0.7 ? 'warning' : 'error') }} badge-sm">
  127. {{ $question['difficulty_label'] }}
  128. </span>
  129. <span class="badge badge-secondary badge-sm">
  130. {{ $question['score'] }} 分
  131. </span>
  132. <span class="badge badge-ghost badge-sm">
  133. {{ $question['estimated_time'] }} 秒
  134. </span>
  135. </div>
  136. <!-- 题目题干 -->
  137. <div class="bg-base-200 p-4 rounded-lg mb-3">
  138. <div class="text-sm text-gray-500 mb-2">题干:</div>
  139. <div class="prose prose-sm max-w-none">
  140. @php
  141. $stemHtml = nl2br(\App\Services\MathFormulaProcessor::processFormulas($question['stem'] ?? ''));
  142. @endphp
  143. {!! $stemHtml !!}
  144. </div>
  145. </div>
  146. <!-- 答案和解析 -->
  147. <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
  148. @if($question['answer'])
  149. <div class="bg-success/10 p-3 rounded-lg">
  150. <div class="text-sm font-semibold text-success mb-1">答案:</div>
  151. <div class="text-sm">{!! nl2br(e($question['answer'])) !!}</div>
  152. </div>
  153. @endif
  154. @if($question['solution'])
  155. <div class="bg-info/10 p-3 rounded-lg">
  156. <div class="text-sm font-semibold text-info mb-1">解析:</div>
  157. <div class="text-sm">{!! nl2br(e($question['solution'])) !!}</div>
  158. </div>
  159. @endif
  160. </div>
  161. <!-- 题目代码 -->
  162. @if($question['question_code'])
  163. <div class="text-xs text-gray-400 mt-2">
  164. 题目编号:{{ $question['question_code'] }}
  165. </div>
  166. @endif
  167. </div>
  168. <!-- 操作按钮 -->
  169. <div class="flex flex-col gap-2">
  170. <button
  171. wire:click="deleteQuestion({{ $question['id'] }})"
  172. wire:confirm="确定要删除这道题目吗?"
  173. class="btn btn-error btn-outline btn-sm">
  174. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  175. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
  176. </svg>
  177. 删除
  178. </button>
  179. </div>
  180. </div>
  181. </div>
  182. @empty
  183. <div class="text-center py-12 text-gray-400">
  184. <svg class="w-16 h-16 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  185. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
  186. </svg>
  187. <p>试卷中暂无题目</p>
  188. <button
  189. wire:click="$toggle('showAddQuestionModal')"
  190. class="btn btn-primary btn-sm mt-4">
  191. 立即添加题目
  192. </button>
  193. </div>
  194. @endforelse
  195. </div>
  196. </div>
  197. <!-- 试卷预览区域 -->
  198. @if($showPreview && !empty($paperDetail))
  199. @php
  200. $paperPreviewUrl = route('filament.admin.auth.intelligent-exam.pdf', [
  201. 'paper_id' => $paperDetail['paper_id'] ?? ($paperId ?? ''),
  202. 'answer' => 'false',
  203. ]);
  204. $gradingPreviewUrl = route('filament.admin.auth.intelligent-exam.grading', [
  205. 'paper_id' => $paperDetail['paper_id'] ?? ($paperId ?? ''),
  206. ]);
  207. @endphp
  208. <div class="bg-white p-6 rounded-lg border shadow-sm">
  209. <div class="flex items-center justify-between mb-4">
  210. <div class="flex items-center gap-3">
  211. <div class="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
  212. <svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  213. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
  214. </svg>
  215. </div>
  216. <div>
  217. <h3 class="text-lg font-semibold text-gray-900">试卷预览 - {{ $paperDetail['paper_name'] }}</h3>
  218. <p class="text-sm text-gray-500">预览区默认展示试卷正文,底部自动附加判卷页,打印会连续输出两份</p>
  219. </div>
  220. </div>
  221. <div class="flex items-center gap-3">
  222. <a
  223. href="{{ $paperPreviewUrl }}"
  224. target="_blank"
  225. class="btn btn-ghost btn-sm"
  226. title="新窗口打开试卷预览">
  227. 试卷新窗口预览
  228. </a>
  229. <a
  230. href="{{ $gradingPreviewUrl }}"
  231. target="_blank"
  232. class="btn btn-ghost btn-sm"
  233. title="新窗口打开判卷预览">
  234. 判卷新窗口预览
  235. </a>
  236. <button
  237. wire:click="printPaper"
  238. class="btn btn-primary btn-sm">
  239. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  240. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z"></path>
  241. </svg>
  242. 打印试卷+判卷
  243. </button>
  244. <button
  245. wire:click="$set('showPreview', false)"
  246. class="btn btn-outline btn-sm">
  247. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  248. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
  249. </svg>
  250. 关闭预览
  251. </button>
  252. </div>
  253. </div>
  254. @if(!empty($paperPreviewUrl))
  255. <div class="bg-gray-100 border rounded-lg overflow-hidden mb-4">
  256. <iframe
  257. id="examPreviewFrame"
  258. src="{{ $paperPreviewUrl }}"
  259. class="w-full"
  260. style="min-height: 900px; background: white;"
  261. title="试卷预览(智能出卷)">
  262. </iframe>
  263. </div>
  264. <div class="bg-amber-50 border rounded-lg overflow-hidden">
  265. <div class="px-4 py-2 text-sm text-amber-700 bg-amber-100 border-b">
  266. 判卷页面(将随试卷一起打印,含答题方框与答案解析)
  267. </div>
  268. <iframe
  269. id="examGradingFrame"
  270. src="{{ $gradingPreviewUrl }}"
  271. class="w-full"
  272. style="min-height: 800px; background: white;"
  273. title="判卷预览">
  274. </iframe>
  275. </div>
  276. @else
  277. <div class="alert alert-error">
  278. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  279. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
  280. </svg>
  281. <span>未找到试卷 ID,无法加载智能出卷预览。</span>
  282. </div>
  283. @endif
  284. </div>
  285. @endif
  286. @endif
  287. </div>
  288. <!-- 编辑试卷模态框 -->
  289. @if($editingExamId)
  290. <div class="modal modal-open">
  291. <div class="modal-box">
  292. <h3 class="font-bold text-lg mb-4">编辑试卷</h3>
  293. <div class="space-y-4">
  294. <div class="form-control">
  295. <label class="label">
  296. <span class="label-text">试卷名称</span>
  297. </label>
  298. <input type="text" wire:model="editForm.paper_name"
  299. class="input input-bordered input-primary"
  300. placeholder="请输入试卷名称" />
  301. @error('editForm.paper_name')
  302. <label class="label">
  303. <span class="label-text-alt text-error">{{ $message }}</span>
  304. </label>
  305. @enderror
  306. </div>
  307. <div class="form-control">
  308. <label class="label">
  309. <span class="label-text">难度分类</span>
  310. </label>
  311. <select wire:model="editForm.difficulty_category"
  312. class="select select-bordered select-primary">
  313. <option value="">-- 请选择难度 --</option>
  314. <option value="基础">基础</option>
  315. <option value="进阶">进阶</option>
  316. <option value="竞赛">竞赛</option>
  317. </select>
  318. @error('editForm.difficulty_category')
  319. <label class="label">
  320. <span class="label-text-alt text-error">{{ $message }}</span>
  321. </label>
  322. @enderror
  323. </div>
  324. <div class="form-control">
  325. <label class="label">
  326. <span class="label-text">状态</span>
  327. </label>
  328. <select wire:model="editForm.status"
  329. class="select select-bordered select-primary">
  330. <option value="">-- 请选择状态 --</option>
  331. <option value="draft">草稿</option>
  332. <option value="completed">已完成</option>
  333. <option value="graded">已评分</option>
  334. </select>
  335. @error('editForm.status')
  336. <label class="label">
  337. <span class="label-text-alt text-error">{{ $message }}</span>
  338. </label>
  339. @enderror
  340. </div>
  341. </div>
  342. <div class="modal-action">
  343. <button wire:click="cancelEdit" class="btn btn-ghost">取消</button>
  344. <button wire:click="saveExamEdit" class="btn btn-primary">保存</button>
  345. </div>
  346. </div>
  347. </div>
  348. @endif
  349. <!-- 添加题目模态框 -->
  350. @if($showAddQuestionModal)
  351. <div class="modal modal-open">
  352. <div class="modal-box max-w-4xl">
  353. <h3 class="font-bold text-lg mb-4">添加题目</h3>
  354. <div class="space-y-4">
  355. <div class="alert alert-info">
  356. <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  357. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
  358. </svg>
  359. <span>
  360. <strong>提示:</strong>添加题目功能需要连接外部题库API,当前版本暂未开放此功能。
  361. 您可以删除现有题目,或通过其他方式管理试卷题目。
  362. </span>
  363. </div>
  364. </div>
  365. <div class="modal-action">
  366. <button wire:click="$toggle('showAddQuestionModal')" class="btn btn-ghost">关闭</button>
  367. </div>
  368. </div>
  369. </div>
  370. @endif
  371. <!-- 打印/预览功能脚本(统一使用智能出卷的 PDF 页面) -->
  372. <script>
  373. document.addEventListener('livewire:init', () => {
  374. Livewire.on('print-paper', (payload) => {
  375. const url = typeof payload === 'string' ? payload : payload?.url;
  376. if (!url) return;
  377. const printWindow = window.open(url, '_blank');
  378. if (!printWindow) return;
  379. const handleLoad = () => {
  380. try {
  381. printWindow.focus();
  382. printWindow.print();
  383. } catch (e) {
  384. console.warn('打印失败,请检查浏览器弹窗权限', e);
  385. }
  386. };
  387. if (printWindow.document?.readyState === 'complete') {
  388. handleLoad();
  389. } else {
  390. printWindow.addEventListener('load', handleLoad, { once: true });
  391. }
  392. });
  393. Livewire.on('refresh-preview', (payload) => {
  394. const previewUrl = typeof payload === 'string'
  395. ? payload
  396. : payload?.previewUrl || payload?.url;
  397. const gradingUrl = typeof payload === 'object' ? payload?.gradingUrl : null;
  398. const frame = document.getElementById('examPreviewFrame');
  399. if (frame && previewUrl) {
  400. frame.src = previewUrl;
  401. }
  402. const gradingFrame = document.getElementById('examGradingFrame');
  403. if (gradingFrame && gradingUrl) {
  404. gradingFrame.src = gradingUrl;
  405. }
  406. });
  407. });
  408. </script>
  409. </div>