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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. <x-filament-panels::page>
  2. @push('styles')
  3. <style>
  4. .exam-card {
  5. transition: all 0.3s ease;
  6. }
  7. .exam-card:hover {
  8. transform: translateY(-2px);
  9. box-shadow: 0 10px 25px rgba(0,0,0,0.1);
  10. }
  11. </style>
  12. @endpush
  13. @push('scripts')
  14. <script>
  15. // 监听window事件,确保组件间通信正常
  16. document.addEventListener('DOMContentLoaded', function() {
  17. // 监听学生选择变化
  18. Livewire.on('window-student-changed', (data) => {
  19. console.log('Window学生变更事件:', data);
  20. });
  21. // 监听教师选择变化
  22. Livewire.on('window-teacher-changed', (data) => {
  23. console.log('Window教师变更事件:', data);
  24. });
  25. });
  26. </script>
  27. @endpush
  28. <div class="space-y-6">
  29. <!-- 页面标题 -->
  30. <div class="flex justify-between items-center">
  31. <div>
  32. <h2 class="text-2xl font-bold text-gray-900">智能出卷系统</h2>
  33. <p class="mt-1 text-sm text-gray-500">
  34. 基于知识点掌握度,智能生成个性化试卷
  35. </p>
  36. </div>
  37. <div class="flex gap-3">
  38. <button
  39. wire:click="resetForm"
  40. type="button"
  41. 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"
  42. >
  43. 重置
  44. </button>
  45. </div>
  46. </div>
  47. <!-- 基本信息卡片 -->
  48. <div class="bg-white p-6 rounded-lg border shadow-sm">
  49. <h3 class="text-lg font-semibold text-gray-900 mb-4">基本信息</h3>
  50. <div class="space-y-4">
  51. <div class="grid grid-cols-3 gap-4">
  52. <div>
  53. <label class="block text-sm font-medium text-gray-700 mb-2">难度分类</label>
  54. <select wire:model="difficultyCategory" class="form-select w-full px-3 py-2 border rounded-lg">
  55. <option value="基础">基础</option>
  56. <option value="进阶">进阶</option>
  57. <option value="竞赛">竞赛</option>
  58. </select>
  59. </div>
  60. <div>
  61. <label class="block text-sm font-medium text-gray-700 mb-2">题目数量 <span class="text-red-500">*</span></label>
  62. <input
  63. type="number"
  64. wire:model="totalQuestions"
  65. class="form-input w-full px-3 py-2 border rounded-lg"
  66. min="6"
  67. max="100"
  68. required
  69. />
  70. </div>
  71. <div>
  72. <label class="block text-sm font-medium text-gray-700 mb-2">总分</label>
  73. <input
  74. type="number"
  75. wire:model="totalScore"
  76. class="form-input w-full px-3 py-2 border rounded-lg"
  77. min="0"
  78. max="200"
  79. />
  80. </div>
  81. </div>
  82. <div>
  83. <label class="block text-sm font-medium text-gray-700 mb-2">试卷名称 <span class="text-gray-400 font-normal">(选填,未填则自动生成)</span></label>
  84. <input
  85. type="text"
  86. wire:model="paperName"
  87. class="form-input w-full px-3 py-2 border rounded-lg"
  88. placeholder="例如:因式分解专项练习(基础版)"
  89. />
  90. </div>
  91. </div>
  92. </div>
  93. <!-- 教师和学生选择 -->
  94. <div class="bg-white p-6 rounded-lg border shadow-sm">
  95. <h3 class="text-lg font-semibold text-gray-900 mb-4">针对性出卷</h3>
  96. <div class="space-y-6">
  97. <!-- 直接在父组件中显示教师和学生选择,避免组件间通信问题 -->
  98. <div class="grid grid-cols-2 gap-4">
  99. <div>
  100. <label class="block text-sm font-medium text-gray-700 mb-2">选择教师</label>
  101. <select
  102. wire:model.live="selectedTeacherId"
  103. class="w-full px-3 py-2 border rounded-lg"
  104. >
  105. <option value="">-- 请选择教师 --</option>
  106. @foreach($this->teachers as $teacher)
  107. <option value="{{ $teacher->teacher_id }}">
  108. {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
  109. </option>
  110. @endforeach
  111. </select>
  112. </div>
  113. <div>
  114. <label class="block text-sm font-medium text-gray-700 mb-2">选择学生</label>
  115. <select
  116. wire:model.live="selectedStudentId"
  117. class="w-full px-3 py-2 border rounded-lg"
  118. @if(empty($selectedTeacherId)) disabled @endif
  119. >
  120. <option value="">
  121. @if(empty($selectedTeacherId))
  122. 请先选择教师
  123. @else
  124. -- 请选择学生 --
  125. @endif
  126. </option>
  127. @foreach($this->students as $student)
  128. <option value="{{ $student->student_id }}">
  129. {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
  130. </option>
  131. @endforeach
  132. </select>
  133. </div>
  134. </div>
  135. <!-- 原有的组件暂时保留但不显示,用于调试 -->
  136. <div style="display:none;">
  137. <livewire:teacher-student-selector
  138. :initial-teacher-id="$selectedTeacherId"
  139. :initial-student-id="$selectedStudentId"
  140. />
  141. </div>
  142. <!-- 显示当前选择状态 -->
  143. <div class="mt-4 p-4 bg-gray-50 rounded-lg">
  144. <div class="grid grid-cols-2 gap-4">
  145. <div>
  146. <div class="text-xs font-medium text-gray-500 mb-1">当前选择的教师</div>
  147. <div class="text-sm bg-white px-3 py-2 rounded border">
  148. {{ $this->getSelectedTeacherName() }}
  149. </div>
  150. </div>
  151. <div>
  152. <div class="text-xs font-medium text-gray-500 mb-1">当前选择的学生</div>
  153. <div class="text-sm bg-white px-3 py-2 rounded border">
  154. {{ $this->getSelectedStudentName() }}
  155. </div>
  156. </div>
  157. </div>
  158. </div>
  159. @if($selectedTeacherId && $selectedStudentId)
  160. <div class="p-4 bg-blue-50 rounded-lg">
  161. <div class="flex items-start gap-3">
  162. <svg class="w-5 h-5 text-blue-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  163. <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>
  164. </svg>
  165. <div>
  166. <div class="font-medium text-blue-900">针对性出卷已启用</div>
  167. <div class="text-sm text-blue-700 mt-1">
  168. 将根据所选学生的薄弱知识点进行智能推荐,建议自动勾选相关知识点
  169. </div>
  170. <label class="flex items-center gap-2 mt-3">
  171. <input
  172. type="checkbox"
  173. wire:model.live="filterByStudentWeakness"
  174. class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
  175. />
  176. <span class="text-sm text-blue-700">根据学生薄弱点自动选择知识点</span>
  177. </label>
  178. </div>
  179. </div>
  180. </div>
  181. @endif
  182. </div>
  183. </div>
  184. <!-- 学生薄弱知识点展示区域 -->
  185. @if($selectedStudentId && $filterByStudentWeakness && count($this->studentWeaknesses) > 0)
  186. <div class="bg-white p-6 rounded-lg border shadow-sm">
  187. <h3 class="text-lg font-semibold text-gray-900 mb-4 flex items-center gap-2">
  188. <svg class="w-5 h-5 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  189. <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"></path>
  190. </svg>
  191. 学生薄弱知识点
  192. <span class="text-sm font-normal text-gray-500">(共{{ count($this->studentWeaknesses) }}个)</span>
  193. </h3>
  194. <div class="bg-orange-50 border-l-4 border-orange-400 p-4 mb-4">
  195. <div class="flex">
  196. <div class="flex-shrink-0">
  197. <svg class="h-5 w-5 text-orange-400" viewBox="0 0 20 20" fill="currentColor">
  198. <path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
  199. </svg>
  200. </div>
  201. <div class="ml-3">
  202. <p class="text-sm text-orange-700">
  203. 以下是根据该学生的答题数据自动分析出的薄弱知识点(共{{ count($this->studentWeaknesses) }}个)。
  204. <strong>请手动勾选</strong>您希望该学生练习的知识点,或点击下方按钮进行批量操作。
  205. </p>
  206. </div>
  207. </div>
  208. </div>
  209. <div class="grid grid-cols-1 gap-3">
  210. @foreach($this->studentWeaknesses as $weakness)
  211. @php
  212. $isSelected = in_array($weakness['kp_code'], $selectedKpCodes);
  213. $masteryPercent = round(($weakness['mastery'] ?? 0) * 100, 1);
  214. $weaknessLevel = $weakness['weakness_level'] ?? (1 - ($weakness['mastery'] ?? 0));
  215. $priority = $weakness['priority'] ?? '中';
  216. $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');
  217. @endphp
  218. <div class="border rounded-lg p-4 {{ $isSelected ? 'bg-blue-50 border-blue-300' : 'bg-white' }}">
  219. <div class="flex items-start justify-between">
  220. <div class="flex items-start gap-3 flex-1">
  221. <div class="mt-1">
  222. @if($isSelected)
  223. <svg class="w-5 h-5 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
  224. <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>
  225. </svg>
  226. @else
  227. <input
  228. type="checkbox"
  229. wire:model="selectedKpCodes"
  230. value="{{ $weakness['kp_code'] }}"
  231. class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
  232. />
  233. @endif
  234. </div>
  235. <div class="flex-1">
  236. <div class="font-medium text-gray-900">
  237. {{ $weakness['kp_name'] ?? $weakness['kp_code'] }}
  238. <span class="ml-2 text-xs text-gray-500">({{ $weakness['kp_code'] }})</span>
  239. </div>
  240. <div class="mt-2 flex items-center gap-4 text-sm">
  241. <div class="flex items-center gap-1">
  242. <span class="text-gray-600">掌握度:</span>
  243. <span class="font-semibold {{ $masteryPercent < 50 ? 'text-red-600' : ($masteryPercent < 70 ? 'text-yellow-600' : 'text-green-600') }}">
  244. {{ $masteryPercent }}%
  245. </span>
  246. </div>
  247. <div class="flex items-center gap-1">
  248. <span class="text-gray-600">练习次数:</span>
  249. <span class="text-gray-900">{{ $weakness['practice_count'] ?? 0 }}</span>
  250. </div>
  251. <div class="flex items-center gap-1">
  252. <span class="text-gray-600">成功率:</span>
  253. <span class="text-gray-900">{{ round(($weakness['success_rate'] ?? 0) * 100, 1) }}%</span>
  254. </div>
  255. </div>
  256. </div>
  257. </div>
  258. <span class="px-2 py-1 text-xs font-medium rounded border {{ $priorityColor }}">
  259. 优先级: {{ $priority }}
  260. </span>
  261. </div>
  262. </div>
  263. @endforeach
  264. </div>
  265. <div class="mt-4 flex items-center justify-between">
  266. <div class="flex items-center gap-3">
  267. <button
  268. wire:click="selectAllWeaknesses"
  269. type="button"
  270. class="px-4 py-2 bg-orange-600 text-white text-sm font-medium rounded hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2"
  271. >
  272. 全选薄弱知识点 ({{ count($this->studentWeaknesses) }})
  273. </button>
  274. <button
  275. wire:click="clearSelection"
  276. type="button"
  277. class="px-4 py-2 bg-gray-500 text-white text-sm font-medium rounded hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
  278. >
  279. 清空选择
  280. </button>
  281. </div>
  282. <div class="text-sm text-gray-600">
  283. 已选择 <span class="font-semibold">{{ count($selectedKpCodes) }}</span> 个知识点
  284. </div>
  285. </div>
  286. </div>
  287. @endif
  288. <!-- 知识点选择 -->
  289. <div class="bg-white p-6 rounded-lg border shadow-sm">
  290. <div class="flex items-center justify-between mb-4">
  291. <h3 class="text-lg font-semibold text-gray-900">知识点选择</h3>
  292. <div class="text-sm text-gray-500">
  293. 已选择: {{ count($selectedKpCodes) }} 个
  294. </div>
  295. </div>
  296. <div class="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-64 overflow-y-auto">
  297. @foreach($this->knowledgePoints as $kp)
  298. <label class="flex items-start gap-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer">
  299. <input
  300. type="checkbox"
  301. wire:model="selectedKpCodes"
  302. value="{{ $kp['kp_code'] }}"
  303. class="mt-1"
  304. />
  305. <div class="flex-1">
  306. <div class="font-medium text-gray-900">{{ $kp['cn_name'] ?? $kp['kp_code'] }}</div>
  307. @if(!empty($kp['description']))
  308. <div class="text-sm text-gray-500 mt-1">{{ Str::limit($kp['description'], 80) }}</div>
  309. @endif
  310. <div class="flex items-center gap-2 mt-2">
  311. <span class="text-xs px-2 py-0.5 bg-blue-100 text-blue-700 rounded">
  312. {{ $kp['kp_code'] }}
  313. </span>
  314. </div>
  315. </div>
  316. </label>
  317. @endforeach
  318. </div>
  319. </div>
  320. <!-- 生成按钮 -->
  321. <div class="bg-white p-6 rounded-lg border shadow-sm">
  322. <button
  323. wire:click="generateExam"
  324. type="button"
  325. class="filament-button filament-button-size-lg filament-button-color-primary filament-button-icon-start inline-flex items-center justify-center w-full px-6 py-3 text-base font-medium transition-colors border border-transparent rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
  326. wire:loading.attr="disabled"
  327. >
  328. @if($isGenerating)
  329. <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
  330. <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
  331. <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>
  332. </svg>
  333. 生成中...
  334. @else
  335. <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  336. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
  337. </svg>
  338. 智能生成试卷
  339. @endif
  340. </button>
  341. @if($generatedPaperId)
  342. <div class="mt-4 p-4 bg-green-50 rounded-lg">
  343. <div class="flex items-center gap-2 text-green-800">
  344. <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  345. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
  346. </svg>
  347. <div class="font-medium">生成成功</div>
  348. </div>
  349. <div class="mt-2 text-sm text-green-700">
  350. 已生成试卷ID: <span class="font-mono">{{ $generatedPaperId }}</span>
  351. </div>
  352. <div class="mt-4 flex gap-2">
  353. <button
  354. onclick="document.getElementById('pdfPreview').scrollIntoView({behavior: 'smooth'})"
  355. type="button"
  356. class="filament-button filament-button-size-md filament-button-color-secondary">
  357. 查看预览
  358. </button>
  359. <button wire:click="exportToPdf" type="button" class="filament-button filament-button-size-md filament-button-color-primary">
  360. 新窗口打开
  361. </button>
  362. </div>
  363. </div>
  364. <!-- PDF 预览区域 -->
  365. <div id="pdfPreview" class="mt-6 bg-white rounded-lg border shadow-sm">
  366. <div class="p-4 border-b bg-gray-50 flex justify-between items-center">
  367. <h3 class="text-lg font-semibold">试卷预览</h3>
  368. <button
  369. onclick="document.getElementById('pdfFrame').contentWindow.print()"
  370. class="filament-button filament-button-size-sm filament-button-color-primary">
  371. 打印试卷
  372. </button>
  373. </div>
  374. <div class="p-4">
  375. <iframe
  376. id="pdfFrame"
  377. src="{{ route('filament.admin.auth.intelligent-exam.pdf', ['paper_id' => $generatedPaperId]) }}"
  378. class="w-full border-0"
  379. style="height: 1200px;"
  380. title="试卷预览">
  381. </iframe>
  382. </div>
  383. </div>
  384. @endif
  385. </div>
  386. </div>
  387. </x-filament-panels::page>