intelligent-exam-generation-simple.blade.php 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. <x-filament-panels::page>
  2. @push('styles')
  3. <style>
  4. .exam-card {
  5. transition: all 0.3s ease;
  6. border: 1px solid rgba(0, 0, 0, 0.05);
  7. }
  8. .exam-card:hover {
  9. transform: translateY(-2px);
  10. box-shadow: 0 10px 25px rgba(0,0,0,0.1);
  11. border-color: rgba(59, 130, 246, 0.2);
  12. }
  13. .generate-button {
  14. transition: all 0.3s ease;
  15. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  16. box-shadow: 0 4px 15px 0 rgba(102, 126, 234, 0.4);
  17. font-weight: 600;
  18. font-size: 16px;
  19. padding: 14px 28px;
  20. }
  21. .generate-button:hover:not(:disabled) {
  22. transform: translateY(-2px);
  23. box-shadow: 0 6px 20px 0 rgba(102, 126, 234, 0.6);
  24. background: linear-gradient(135deg, #5568d3 0%, #653a8b 100%);
  25. }
  26. .generate-button:active:not(:disabled) {
  27. transform: translateY(0);
  28. }
  29. .generate-button:disabled {
  30. background: #cbd5e1;
  31. box-shadow: none;
  32. cursor: not-allowed;
  33. }
  34. .step-indicator {
  35. position: relative;
  36. padding-left: 30px;
  37. }
  38. .step-indicator::before {
  39. content: '';
  40. position: absolute;
  41. left: 10px;
  42. top: 0;
  43. bottom: 0;
  44. width: 2px;
  45. background: #e5e7eb;
  46. }
  47. .step-item {
  48. position: relative;
  49. margin-bottom: 20px;
  50. }
  51. .step-item::before {
  52. content: attr(data-step);
  53. position: absolute;
  54. left: -30px;
  55. top: 0;
  56. width: 20px;
  57. height: 20px;
  58. background: #fff;
  59. border: 2px solid #d1d5db;
  60. border-radius: 50%;
  61. display: flex;
  62. align-items: center;
  63. justify-content: center;
  64. font-size: 11px;
  65. font-weight: 600;
  66. color: #9ca3af;
  67. }
  68. .step-item.completed::before {
  69. background: #10b981;
  70. border-color: #10b981;
  71. color: #fff;
  72. }
  73. .step-item.active::before {
  74. background: #3b82f6;
  75. border-color: #3b82f6;
  76. color: #fff;
  77. }
  78. .selection-card {
  79. transition: all 0.2s ease;
  80. }
  81. .selection-card:hover {
  82. background-color: #f9fafb;
  83. border-color: #3b82f6;
  84. }
  85. .selection-card.selected {
  86. background-color: #eff6ff;
  87. border-color: #3b82f6;
  88. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  89. }
  90. .form-select, .form-input {
  91. background-color: white;
  92. border: 2px solid #e5e7eb;
  93. transition: all 0.2s ease;
  94. }
  95. .form-select:focus, .form-input:focus {
  96. border-color: #3b82f6;
  97. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  98. outline: none;
  99. }
  100. </style>
  101. @endpush
  102. @push('scripts')
  103. <script>
  104. // 监听window事件,确保组件间通信正常
  105. document.addEventListener('DOMContentLoaded', function() {
  106. // 监听学生选择变化
  107. Livewire.on('window-student-changed', (data) => {
  108. console.log('Window学生变更事件:', data);
  109. });
  110. // 监听教师选择变化
  111. Livewire.on('window-teacher-changed', (data) => {
  112. console.log('Window教师变更事件:', data);
  113. });
  114. });
  115. window.printBoth = function() {
  116. const examFrame = document.getElementById('pdfFrame');
  117. const gradingFrame = document.getElementById('gradingFrame');
  118. if (examFrame?.contentWindow) {
  119. examFrame.contentWindow.focus();
  120. examFrame.contentWindow.print();
  121. }
  122. setTimeout(() => {
  123. if (gradingFrame?.contentWindow) {
  124. gradingFrame.contentWindow.focus();
  125. gradingFrame.contentWindow.print();
  126. }
  127. }, 600);
  128. };
  129. </script>
  130. @endpush
  131. <div class="space-y-6">
  132. <!-- 页面标题 -->
  133. <div class="flex justify-between items-center">
  134. <div>
  135. <h2 class="text-2xl font-bold text-gray-900">智能出卷系统</h2>
  136. <p class="mt-1 text-sm text-gray-500">
  137. 基于知识点掌握度,智能生成个性化试卷
  138. </p>
  139. </div>
  140. <div class="flex gap-3">
  141. <button
  142. wire:click="resetForm"
  143. type="button"
  144. class="filament-button filament-button-size-sm filament-button-color-gray filament-button-icon-start inline-flex items-center justify-center px-4 py-2 text-sm font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
  145. >
  146. 重置
  147. </button>
  148. </div>
  149. </div>
  150. <!-- 基本信息卡片 -->
  151. <div class="bg-white p-8 rounded-xl border shadow-sm exam-card">
  152. <div class="flex items-center gap-3 mb-6">
  153. <div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
  154. <svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  155. <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" />
  156. </svg>
  157. </div>
  158. <div>
  159. <h3 class="text-xl font-semibold text-gray-900">步骤 1:基本信息设置</h3>
  160. <p class="text-sm text-gray-500">设置试卷的基本参数</p>
  161. </div>
  162. </div>
  163. <div class="space-y-4">
  164. <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
  165. <div class="selection-card border rounded-lg p-4">
  166. <label class="block text-sm font-medium text-gray-700 mb-2">年级</label>
  167. <select wire:model="selectedGrade" class="form-select w-full px-3 py-2 rounded-lg text-sm">
  168. <option value="初中">初中整体</option>
  169. <option value="七年级">七年级</option>
  170. <option value="八年级">八年级</option>
  171. <option value="九年级">九年级</option>
  172. </select>
  173. </div>
  174. <div class="selection-card border rounded-lg p-4">
  175. <label class="block text-sm font-medium text-gray-700 mb-2">难度分类</label>
  176. <select wire:model="difficultyCategory" class="form-select w-full px-3 py-2 rounded-lg text-sm">
  177. <option value="基础">基础</option>
  178. <option value="进阶">进阶</option>
  179. <option value="竞赛">竞赛</option>
  180. </select>
  181. </div>
  182. <div class="selection-card border rounded-lg p-4">
  183. <label class="block text-sm font-medium text-gray-700 mb-2">题目数量 <span class="text-red-500">*</span></label>
  184. <input
  185. type="number"
  186. wire:model="totalQuestions"
  187. class="form-input w-full px-3 py-2 rounded-lg text-sm"
  188. min="6"
  189. max="100"
  190. required
  191. />
  192. </div>
  193. <div class="selection-card border rounded-lg p-4">
  194. <label class="block text-sm font-medium text-gray-700 mb-2">总分</label>
  195. <input
  196. type="number"
  197. wire:model="totalScore"
  198. class="form-input w-full px-3 py-2 rounded-lg text-sm"
  199. min="0"
  200. max="200"
  201. />
  202. </div>
  203. </div>
  204. <div class="selection-card border rounded-lg p-4">
  205. <label class="block text-sm font-medium text-gray-700 mb-2">试卷名称 <span class="text-gray-400 font-normal">(选填,未填则自动生成)</span></label>
  206. <input
  207. type="text"
  208. wire:model="paperName"
  209. class="form-input w-full px-3 py-2 rounded-lg text-sm"
  210. placeholder="例如:因式分解专项练习(基础版)"
  211. />
  212. </div>
  213. </div>
  214. </div>
  215. <!-- 教师和学生选择 -->
  216. <div class="bg-white p-8 rounded-xl border shadow-sm exam-card">
  217. <div class="flex items-center gap-3 mb-6">
  218. <div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
  219. <svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  220. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
  221. </svg>
  222. </div>
  223. <div>
  224. <h3 class="text-xl font-semibold text-gray-900">步骤 2:选择教师与学生</h3>
  225. <p class="text-sm text-gray-500">启用个性化出卷功能</p>
  226. </div>
  227. </div>
  228. <div class="space-y-6">
  229. <!-- 直接在父组件中显示教师和学生选择,避免组件间通信问题 -->
  230. @if(!$this->isTeacher)
  231. <div class="grid grid-cols-2 gap-4">
  232. <div class="selection-card border rounded-lg p-4">
  233. <label class="block text-sm font-medium text-gray-700 mb-2">选择教师</label>
  234. <select
  235. wire:model.live="selectedTeacherId"
  236. class="form-select w-full px-3 py-2 rounded-lg text-sm"
  237. >
  238. <option value="">-- 请选择教师 --</option>
  239. @foreach($this->teachers as $teacher)
  240. <option value="{{ $teacher->teacher_id }}">
  241. {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
  242. </option>
  243. @endforeach
  244. </select>
  245. </div>
  246. <div class="selection-card border rounded-lg p-4">
  247. <label class="block text-sm font-medium text-gray-700 mb-2">选择学生</label>
  248. <select
  249. wire:model.live="selectedStudentId"
  250. class="form-select w-full px-3 py-2 rounded-lg text-sm"
  251. @if(empty($selectedTeacherId)) disabled @endif
  252. >
  253. <option value="">
  254. @if(empty($selectedTeacherId))
  255. 请先选择教师
  256. @else
  257. -- 请选择学生 --
  258. @endif
  259. </option>
  260. @foreach($this->students as $student)
  261. <option value="{{ $student->student_id }}" @if(request('student_id') == $student->student_id) selected @endif>
  262. {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
  263. </option>
  264. @endforeach
  265. </select>
  266. </div>
  267. </div>
  268. @else
  269. <div class="grid grid-cols-1 gap-4">
  270. <div class="selection-card border rounded-lg p-4">
  271. <label class="block text-sm font-medium text-gray-700 mb-2">选择学生</label>
  272. <select
  273. wire:model.live="selectedStudentId"
  274. class="form-select w-full px-3 py-2 rounded-lg text-sm"
  275. >
  276. <option value="">-- 请选择学生 --</option>
  277. @foreach($this->students as $student)
  278. <option value="{{ $student->student_id }}" @if(request('student_id') == $student->student_id) selected @endif>
  279. {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
  280. </option>
  281. @endforeach
  282. </select>
  283. </div>
  284. </div>
  285. @endif
  286. <!-- 显示当前选择状态 -->
  287. <div class="mt-4 p-4 bg-gray-50 rounded-lg">
  288. <div class="grid grid-cols-2 gap-4">
  289. <div>
  290. <div class="text-xs font-medium text-gray-500 mb-1">当前选择的教师</div>
  291. <div class="text-sm bg-white px-3 py-2 rounded border">
  292. {{ $this->getSelectedTeacherName() }}
  293. </div>
  294. </div>
  295. <div>
  296. <div class="text-xs font-medium text-gray-500 mb-1">当前选择的学生</div>
  297. <div class="text-sm bg-white px-3 py-2 rounded border">
  298. {{ $this->getSelectedStudentName() }}
  299. </div>
  300. </div>
  301. </div>
  302. </div>
  303. @if($selectedTeacherId && $selectedStudentId)
  304. <div class="p-4 bg-blue-50 rounded-lg">
  305. <div class="flex items-start gap-3">
  306. <svg class="w-5 h-5 text-blue-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  307. <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>
  308. </svg>
  309. <div>
  310. <div class="font-medium text-blue-900">针对性出卷已启用</div>
  311. @if($this->hasStudentWeaknesses)
  312. <div class="text-sm text-blue-700 mt-1">
  313. 将根据所选学生的薄弱知识点进行智能推荐,建议自动勾选相关知识点
  314. </div>
  315. <label class="flex items-center gap-2 mt-3 cursor-pointer">
  316. <input
  317. type="checkbox"
  318. wire:model.live="filterByStudentWeakness"
  319. class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
  320. />
  321. <span class="text-sm text-blue-700">根据学生薄弱点自动选择知识点</span>
  322. </label>
  323. @else
  324. <div class="text-sm text-gray-500 mt-1">
  325. 该学生暂无薄弱知识点数据,请在下方手动选择知识点
  326. </div>
  327. <label class="flex items-center gap-2 mt-3 opacity-50 cursor-not-allowed">
  328. <input
  329. type="checkbox"
  330. disabled
  331. class="rounded border-gray-300 text-gray-400"
  332. />
  333. <span class="text-sm text-gray-400">根据学生薄弱点自动选择知识点(暂无数据)</span>
  334. </label>
  335. @endif
  336. </div>
  337. </div>
  338. </div>
  339. @endif
  340. </div>
  341. </div>
  342. <!-- 学生薄弱知识点展示区域 -->
  343. @if($selectedStudentId && $filterByStudentWeakness && count($this->studentWeaknesses) > 0)
  344. <div class="bg-white p-8 rounded-xl border shadow-sm exam-card">
  345. <div class="flex items-center gap-3 mb-6">
  346. <div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center">
  347. <svg class="w-6 h-6 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  348. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
  349. </svg>
  350. </div>
  351. <div>
  352. <h3 class="text-xl font-semibold text-gray-900">学生薄弱知识点分析</h3>
  353. <p class="text-sm text-gray-500">基于答题数据的智能分析(共{{ count($this->studentWeaknesses) }}个)</p>
  354. </div>
  355. </div>
  356. <div class="bg-gradient-to-r from-orange-50 to-amber-50 border border-orange-200 rounded-xl p-5 mb-6">
  357. <div class="flex items-start gap-3">
  358. <svg class="h-6 w-6 text-orange-500 flex-shrink-0 mt-0.5" viewBox="0 0 20 20" fill="currentColor">
  359. <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
  360. </svg>
  361. <div>
  362. <p class="text-sm text-orange-800 font-medium mb-1">
  363. 智能分析结果(可选)
  364. </p>
  365. <p class="text-sm text-orange-700">
  366. 以下是根据该学生的答题数据自动分析出的薄弱知识点(共{{ count($this->studentWeaknesses) }}个)。
  367. <strong>您可以选择这些薄弱点</strong>,或者在下方<strong>步骤3</strong>中手动选择任何知识点。
  368. 两个区域的知识点会合并生效。
  369. </p>
  370. </div>
  371. </div>
  372. </div>
  373. <div class="grid grid-cols-1 gap-3">
  374. @foreach($this->studentWeaknesses as $weakness)
  375. @php
  376. $isSelected = in_array($weakness['kp_code'], $selectedKpCodes);
  377. $masteryPercent = round(($weakness['mastery'] ?? 0) * 100, 1);
  378. $weaknessLevel = $weakness['weakness_level'] ?? (1 - ($weakness['mastery'] ?? 0));
  379. $priority = $weakness['priority'] ?? '中';
  380. $priorityColor = $priority === '高' ? 'bg-red-100 text-red-800 border-red-200' : ($priority === '中' ? 'bg-yellow-100 text-yellow-800 border-yellow-200' : 'bg-green-100 text-green-800 border-green-200');
  381. @endphp
  382. <div class="selection-card border rounded-xl p-5 {{ $isSelected ? 'selected' : '' }}">
  383. <div class="flex items-start justify-between">
  384. <div class="flex items-start gap-4 flex-1">
  385. <div class="mt-1">
  386. @if($isSelected)
  387. <svg class="w-6 h-6 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
  388. <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
  389. </svg>
  390. @else
  391. <input
  392. type="checkbox"
  393. wire:model.live="selectedKpCodes"
  394. value="{{ $weakness['kp_code'] }}"
  395. class="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
  396. wire:key="weakness-{{ $weakness['kp_code'] }}"
  397. />
  398. @endif
  399. </div>
  400. <div class="flex-1">
  401. <div class="font-semibold text-gray-900 text-base">
  402. {{ $weakness['kp_name'] ?? $weakness['kp_code'] }}
  403. <span class="ml-2 text-sm text-gray-500">({{ $weakness['kp_code'] }})</span>
  404. </div>
  405. <div class="mt-3 flex items-center gap-6 text-sm">
  406. <div class="flex items-center gap-2">
  407. <span class="text-gray-600">掌握度</span>
  408. <span class="font-bold text-lg {{ $masteryPercent < 50 ? 'text-red-600' : ($masteryPercent < 70 ? 'text-yellow-600' : 'text-green-600') }}">
  409. {{ $masteryPercent }}%
  410. </span>
  411. </div>
  412. <div class="flex items-center gap-2">
  413. <span class="text-gray-600">练习次数</span>
  414. <span class="font-semibold text-gray-900">{{ $weakness['practice_count'] ?? 0 }}</span>
  415. </div>
  416. <div class="flex items-center gap-2">
  417. <span class="text-gray-600">成功率</span>
  418. <span class="font-semibold text-gray-900">{{ round(($weakness['success_rate'] ?? 0) * 100, 1) }}%</span>
  419. </div>
  420. </div>
  421. </div>
  422. </div>
  423. <span class="px-3 py-1.5 text-sm font-semibold rounded-full {{ $priorityColor }}">
  424. {{ $priority }}优先级
  425. </span>
  426. </div>
  427. </div>
  428. @endforeach
  429. </div>
  430. <div class="mt-6 flex items-center justify-between bg-gray-50 rounded-xl p-4">
  431. <div class="flex items-center gap-3">
  432. <button
  433. wire:click="selectAllWeaknesses"
  434. type="button"
  435. class="px-5 py-2.5 bg-gradient-to-r from-orange-500 to-amber-500 text-white text-sm font-semibold rounded-lg hover:from-orange-600 hover:to-amber-600 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 transition-all shadow-md"
  436. >
  437. 全选薄弱知识点 ({{ count($this->studentWeaknesses) }})
  438. </button>
  439. <button
  440. wire:click="clearSelection"
  441. type="button"
  442. class="px-5 py-2.5 bg-white border-2 border-gray-300 text-gray-700 text-sm font-semibold rounded-lg hover:bg-gray-50 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 transition-all"
  443. >
  444. 清空选择
  445. </button>
  446. </div>
  447. <div class="flex items-center gap-2 text-sm text-gray-600" x-data x-effect="$el.querySelector('.count').textContent = $wire.selectedKpCodes.length">
  448. <span>已选择</span>
  449. <span class="font-bold text-lg text-blue-600 count">{{ count($selectedKpCodes) }}</span>
  450. <span>个知识点</span>
  451. </div>
  452. </div>
  453. </div>
  454. @endif
  455. <!-- 知识点选择 -->
  456. <div class="bg-white p-8 rounded-xl border shadow-sm exam-card">
  457. <div class="flex items-center gap-3 mb-6">
  458. <div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center">
  459. <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  460. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
  461. </svg>
  462. </div>
  463. <div>
  464. <h3 class="text-xl font-semibold text-gray-900">步骤 3:选择知识点</h3>
  465. <p class="text-sm text-gray-500" x-data x-effect="$el.textContent = `勾选要考查的知识点(已选择: ${$wire.selectedKpCodes.length} 个)`">
  466. 勾选要考查的知识点(已选择: {{ count($selectedKpCodes) }} 个)
  467. </p>
  468. @if($selectedStudentId && $filterByStudentWeakness && count($this->studentWeaknesses) > 0)
  469. <p class="text-xs text-blue-600 mt-1">
  470. 💡 提示:您也可以在上方选择学生的薄弱知识点,两个区域的知识点会合并生效
  471. </p>
  472. @endif
  473. </div>
  474. </div>
  475. <div class="grid grid-cols-1 md:grid-cols-3 gap-3 max-h-72 overflow-y-auto pr-1 scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-gray-100">
  476. @foreach($this->knowledgePoints as $kp)
  477. @php
  478. $isSelected = in_array($kp['kp_code'], $selectedKpCodes);
  479. @endphp
  480. <label class="selection-card flex items-start gap-3 p-3 border rounded-lg cursor-pointer hover:bg-blue-50 hover:border-blue-400 transition-all shadow-sm hover:shadow-md {{ $isSelected ? 'border-blue-500 bg-blue-50' : '' }}">
  481. <input
  482. type="checkbox"
  483. wire:model.live="selectedKpCodes"
  484. value="{{ $kp['kp_code'] }}"
  485. class="mt-0.5 w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
  486. wire:key="kp-{{ $kp['kp_code'] }}"
  487. />
  488. <div class="flex-1 min-w-0">
  489. <div class="font-semibold text-gray-900 text-sm leading-tight">{{ Str::limit($kp['cn_name'] ?? $kp['kp_code'], 16) }}</div>
  490. <div class="flex items-center gap-1 mt-1.5">
  491. <span class="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded-full font-medium">
  492. {{ $kp['kp_code'] }}
  493. </span>
  494. </div>
  495. @if(!empty($kp['description']))
  496. <div class="text-xs text-gray-500 mt-1 line-clamp-2">{{ Str::limit($kp['description'], 30) }}</div>
  497. @endif
  498. @if($isSelected)
  499. <div class="text-xs text-blue-600 mt-1 font-medium">✓ 已选择</div>
  500. @endif
  501. </div>
  502. </label>
  503. @endforeach
  504. </div>
  505. </div>
  506. <!-- 生成按钮 -->
  507. @php
  508. $hasTeacherStudent = !empty($selectedTeacherId) && !empty($selectedStudentId);
  509. $hasKnowledgePoints = count($selectedKpCodes) > 0;
  510. $questionCountValid = $totalQuestions >= 6;
  511. $readyToGenerate = $this->canGenerate();
  512. $missingSteps = [];
  513. if (empty($selectedTeacherId)) { $missingSteps[] = '选择教师'; }
  514. if (empty($selectedStudentId)) { $missingSteps[] = '选择学生'; }
  515. if (!$hasKnowledgePoints) { $missingSteps[] = '勾选至少 1 个知识点'; }
  516. if (!$questionCountValid) { $missingSteps[] = '题目数量需 ≥ 6 题'; }
  517. // 强制访问 selectedKpCodes 来触发 Livewire 刷新
  518. $selectedKpCodesForDebug = $selectedKpCodes;
  519. // 调试信息
  520. $debugInfo = "教师: " . ($selectedTeacherId ? "✓" : "✗") .
  521. " | 学生: " . ($selectedStudentId ? "✓" : "✗") .
  522. " | 知识点: " . ($hasKnowledgePoints ? "✓ (" . count($selectedKpCodesForDebug) . "个)" : "✗ (实际:" . count($selectedKpCodesForDebug) . "个)") .
  523. " | 题目数: " . ($questionCountValid ? "✓ ({$totalQuestions})" : "✗ ({$totalQuestions})");
  524. // 显示实际的知识点代码
  525. $kpCodesList = implode(', ', array_slice($selectedKpCodesForDebug, 0, 10));
  526. if (count($selectedKpCodesForDebug) > 10) {
  527. $kpCodesList .= '...';
  528. }
  529. @endphp
  530. <div class="bg-white p-8 rounded-xl border shadow-sm exam-card space-y-6">
  531. <div class="flex items-center justify-between">
  532. <div class="flex items-center gap-3">
  533. <div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center">
  534. <svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  535. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
  536. </svg>
  537. </div>
  538. <div>
  539. <h3 class="text-xl font-semibold text-gray-900">步骤 4:出卷前检查与生成</h3>
  540. <p class="text-sm text-gray-500">验证配置并生成试卷</p>
  541. </div>
  542. </div>
  543. <span class="inline-flex items-center gap-2 px-4 py-2 text-sm font-semibold rounded-full {{ $readyToGenerate ? 'bg-green-100 text-green-800 border-2 border-green-300' : 'bg-amber-100 text-amber-800 border-2 border-amber-300' }}">
  544. <span class="h-2.5 w-2.5 rounded-full {{ $readyToGenerate ? 'bg-green-500 animate-pulse' : 'bg-amber-500' }}"></span>
  545. {{ $readyToGenerate ? '✓ 可以生成试卷' : '⚠ 待完善配置' }}
  546. </span>
  547. </div>
  548. <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
  549. <div class="selection-card border-2 rounded-xl p-5 {{ $hasTeacherStudent ? 'border-green-400 bg-gradient-to-br from-green-50 to-emerald-50' : 'border-amber-300 bg-gradient-to-br from-amber-50 to-orange-50' }}">
  550. <div class="flex items-center gap-3 mb-2">
  551. <div class="w-10 h-10 rounded-lg {{ $hasTeacherStudent ? 'bg-green-100' : 'bg-amber-100' }} flex items-center justify-center">
  552. @if($hasTeacherStudent)
  553. <svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  554. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
  555. </svg>
  556. @else
  557. <svg class="w-5 h-5 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  558. <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" />
  559. </svg>
  560. @endif
  561. </div>
  562. <span class="text-sm font-bold {{ $hasTeacherStudent ? 'text-green-800' : 'text-amber-800' }}">教师 / 学生</span>
  563. </div>
  564. <p class="text-sm {{ $hasTeacherStudent ? 'text-green-700' : 'text-amber-700' }}">
  565. {{ $hasTeacherStudent ? '✓ ' . $this->getSelectedTeacherName() . ' / ' . $this->getSelectedStudentName() : '请选择教师和学生' }}
  566. </p>
  567. </div>
  568. <div class="selection-card border-2 rounded-xl p-5 {{ $hasKnowledgePoints ? 'border-green-400 bg-gradient-to-br from-green-50 to-emerald-50' : 'border-amber-300 bg-gradient-to-br from-amber-50 to-orange-50' }}">
  569. <div class="flex items-center gap-3 mb-2">
  570. <div class="w-10 h-10 rounded-lg {{ $hasKnowledgePoints ? 'bg-green-100' : 'bg-amber-100' }} flex items-center justify-center">
  571. @if($hasKnowledgePoints)
  572. <svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  573. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
  574. </svg>
  575. @else
  576. <svg class="w-5 h-5 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  577. <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" />
  578. </svg>
  579. @endif
  580. </div>
  581. <span class="text-sm font-bold {{ $hasKnowledgePoints ? 'text-green-800' : 'text-amber-800' }}">知识点选择</span>
  582. </div>
  583. <p class="text-sm {{ $hasKnowledgePoints ? 'text-green-700' : 'text-amber-700' }}" x-data x-effect="$el.textContent = $wire.selectedKpCodes.length > 0 ? `✓ 已选 ${$wire.selectedKpCodes.length} 个知识点` : '请选择至少1个知识点'">
  584. {{ $hasKnowledgePoints ? '✓ 已选 ' . count($selectedKpCodes) . ' 个知识点' : '请选择至少1个知识点' }}
  585. </p>
  586. </div>
  587. <div class="selection-card border-2 rounded-xl p-5 {{ $questionCountValid ? 'border-green-400 bg-gradient-to-br from-green-50 to-emerald-50' : 'border-amber-300 bg-gradient-to-br from-amber-50 to-orange-50' }}">
  588. <div class="flex items-center gap-3 mb-2">
  589. <div class="w-10 h-10 rounded-lg {{ $questionCountValid ? 'bg-green-100' : 'bg-amber-100' }} flex items-center justify-center">
  590. @if($questionCountValid)
  591. <svg class="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  592. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
  593. </svg>
  594. @else
  595. <svg class="w-5 h-5 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  596. <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" />
  597. </svg>
  598. @endif
  599. </div>
  600. <span class="text-sm font-bold {{ $questionCountValid ? 'text-green-800' : 'text-amber-800' }}">题目数量</span>
  601. </div>
  602. <p class="text-sm {{ $questionCountValid ? 'text-green-700' : 'text-amber-700' }}" x-data x-effect="$el.textContent = $wire.totalQuestions >= 6 ? `✓ 将生成 ${$wire.totalQuestions} 题` : '至少需要6题'">
  603. {{ $questionCountValid ? '✓ 将生成 ' . $totalQuestions . ' 题' : '至少需要6题' }}
  604. </p>
  605. </div>
  606. </div>
  607. @unless($readyToGenerate)
  608. <div class="rounded-xl border-2 border-amber-300 bg-gradient-to-r from-amber-50 to-orange-50 px-6 py-4">
  609. <div class="flex items-start gap-3">
  610. <svg class="w-6 h-6 text-amber-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  611. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
  612. </svg>
  613. <div>
  614. <p class="text-sm font-bold text-amber-800 mb-1">请完成以下必填项</p>
  615. <p class="text-sm text-amber-700">{{ implode(' / ', $missingSteps) }}</p>
  616. <p class="text-xs text-amber-600 mt-2">
  617. <strong>实时状态:</strong> {{ $debugInfo }}
  618. </p>
  619. <p class="text-xs text-amber-600 mt-1">
  620. <strong>实际代码:</strong> {{ $kpCodesList ?: '(无)' }}
  621. </p>
  622. </div>
  623. </div>
  624. </div>
  625. @endunless
  626. <div class="relative">
  627. <button
  628. wire:click="generateExam"
  629. type="button"
  630. class="generate-button w-full text-white rounded-xl py-4 px-8 text-lg font-bold flex items-center justify-center gap-3 {{ !$readyToGenerate ? 'opacity-50 cursor-not-allowed' : 'hover:shadow-2xl' }}"
  631. wire:loading.attr="disabled"
  632. @if(!$readyToGenerate) disabled @endif
  633. aria-disabled="{{ $readyToGenerate ? 'false' : 'true' }}"
  634. title="{{ $readyToGenerate ? '根据当前选择生成试卷' : '请先完成必选项再生成' }}"
  635. >
  636. @if($isGenerating)
  637. <svg class="animate-spin h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
  638. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
  639. <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
  640. </svg>
  641. <span class="text-lg">正在生成试卷,请稍候...</span>
  642. @else
  643. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  644. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M13 10V3L4 14h7v7l9-11h-7z" />
  645. </svg>
  646. <span class="text-lg">🚀 智能生成试卷</span>
  647. @endif
  648. </button>
  649. @if($readyToGenerate && !$isGenerating)
  650. <div class="absolute -top-3 -right-3 w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center animate-bounce">
  651. <svg class="w-5 h-5 text-yellow-800" fill="currentColor" viewBox="0 0 20 20">
  652. <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
  653. </svg>
  654. </div>
  655. @endif
  656. </div>
  657. @if($generatedPaperId)
  658. <div class="mt-6 p-6 bg-gradient-to-br from-green-50 to-emerald-50 border-2 border-green-300 rounded-xl">
  659. <div class="flex items-start gap-4">
  660. <div class="w-12 h-12 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
  661. <svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  662. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"/>
  663. </svg>
  664. </div>
  665. <div class="flex-1">
  666. <h4 class="text-xl font-bold text-green-800 mb-2">🎉 试卷生成成功!</h4>
  667. <div class="space-y-2 text-sm">
  668. <div class="flex items-center gap-2 text-green-700">
  669. <span class="font-semibold">试卷ID:</span>
  670. <span class="font-mono bg-white px-3 py-1 rounded border border-green-300">{{ $generatedPaperId }}</span>
  671. </div>
  672. <div class="text-green-700">
  673. 试卷已准备就绪,您可以查看预览或导出PDF
  674. </div>
  675. </div>
  676. <div class="mt-5 flex gap-3">
  677. <button
  678. onclick="document.getElementById('pdfPreview').scrollIntoView({behavior: 'smooth'})"
  679. type="button"
  680. class="px-5 py-2.5 bg-white border-2 border-green-500 text-green-700 font-semibold rounded-lg hover:bg-green-50 transition-all shadow-sm"
  681. >
  682. 查看预览
  683. </button>
  684. <button
  685. wire:click="exportToPdf"
  686. type="button"
  687. class="px-5 py-2.5 bg-gradient-to-r from-green-500 to-emerald-600 text-white font-semibold rounded-lg hover:from-green-600 hover:to-emerald-700 transition-all shadow-md"
  688. >
  689. 导出PDF
  690. </button>
  691. </div>
  692. </div>
  693. </div>
  694. </div>
  695. <!-- PDF 预览区域 -->
  696. <div id="pdfPreview" class="mt-8 bg-white rounded-xl border-2 border-gray-200 shadow-lg overflow-hidden">
  697. <div class="p-5 border-b bg-gradient-to-r from-gray-50 to-gray-100 flex justify-between items-center">
  698. <div class="flex items-center gap-3">
  699. <div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
  700. <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  701. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
  702. <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" />
  703. </svg>
  704. </div>
  705. <h3 class="text-lg font-bold text-gray-900">试卷预览</h3>
  706. </div>
  707. <button
  708. onclick="document.getElementById('pdfFrame').contentWindow.print()"
  709. class="px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-all shadow-md flex items-center gap-2"
  710. >
  711. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  712. <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" />
  713. </svg>
  714. 打印试卷
  715. </button>
  716. </div>
  717. <div class="p-6 bg-gray-100">
  718. <iframe
  719. id="pdfFrame"
  720. src="{{ route('filament.admin.auth.intelligent-exam.pdf', ['paper_id' => $generatedPaperId, 'answer' => 'false']) }}"
  721. class="w-full border-0 rounded-lg shadow-lg"
  722. style="height: 1200px; background: white;"
  723. title="试卷预览">
  724. </iframe>
  725. </div>
  726. </div>
  727. <!-- 判卷预览区域 -->
  728. <div id="gradingPreview" class="mt-8 bg-white rounded-xl border-2 border-gray-200 shadow-lg overflow-hidden">
  729. <div class="p-5 border-b bg-gradient-to-r from-amber-50 to-amber-100 flex justify-between items-center">
  730. <div class="flex items-center gap-3">
  731. <div class="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center">
  732. <svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  733. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
  734. </svg>
  735. </div>
  736. <h3 class="text-lg font-bold text-gray-900">判卷预览</h3>
  737. </div>
  738. <div class="flex gap-3">
  739. <button
  740. onclick="document.getElementById('gradingFrame').contentWindow.print()"
  741. class="px-4 py-2 bg-amber-600 text-white font-semibold rounded-lg hover:bg-amber-700 transition-all shadow-md flex items-center gap-2"
  742. >
  743. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  744. <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" />
  745. </svg>
  746. 打印判卷
  747. </button>
  748. <button
  749. onclick="printBoth()"
  750. class="px-4 py-2 border-2 border-amber-600 text-amber-700 font-semibold rounded-lg hover:bg-amber-50 transition-all shadow-sm flex items-center gap-2"
  751. >
  752. <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  753. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 17l4 4 4-4m0-10l-4-4-4 4" />
  754. </svg>
  755. 连续打印试卷+判卷
  756. </button>
  757. </div>
  758. </div>
  759. <div class="p-6 bg-gray-100">
  760. <iframe
  761. id="gradingFrame"
  762. src="{{ route('filament.admin.auth.intelligent-exam.grading', ['paper_id' => $generatedPaperId]) }}"
  763. class="w-full border-0 rounded-lg shadow-lg"
  764. style="height: 1200px; background: white;"
  765. title="判卷预览">
  766. </iframe>
  767. </div>
  768. </div>
  769. @endif
  770. </div>
  771. </div>
  772. </x-filament-panels::page>