mistake-book.blade.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <div class="min-h-screen bg-[#f5f7fb] p-6">
  2. <div class="max-w-7xl mx-auto space-y-6">
  3. <div class="rounded-2xl bg-gradient-to-r from-sky-50 via-white to-indigo-50 border border-slate-100 shadow-sm p-6">
  4. <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
  5. <div class="lg:col-span-1">
  6. <p class="text-xs uppercase tracking-[0.2em] text-slate-400">MistakeBook</p>
  7. <h1 class="text-3xl font-bold text-slate-900 mt-1">错题本 · 诊断与重练</h1>
  8. <p class="text-sm text-slate-500 mt-1">完全复用上传卷子/智能出卷的师生联动:先选老师,再选学生,再刷新数据。</p>
  9. </div>
  10. <div class="lg:col-span-2">
  11. <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
  12. {{-- 选择老师(老师登录时隐藏) --}}
  13. @if(!$this->isTeacher)
  14. <div class="form-control w-full">
  15. <label class="label">
  16. <span class="label-text font-medium">选择老师 <span class="text-error">*</span></span>
  17. </label>
  18. <select
  19. wire:model.live="teacherId"
  20. class="select select-bordered w-full select-lg"
  21. >
  22. <option value="">请选择老师...</option>
  23. @foreach($this->teachers as $teacher)
  24. <option value="{{ $teacher->teacher_id }}">
  25. {{ trim($teacher->name ?? $teacher->teacher_id) . ($teacher->subject ? " ({$teacher->subject})" : '') }}
  26. </option>
  27. @endforeach
  28. </select>
  29. </div>
  30. @endif
  31. <div class="form-control w-full">
  32. <label class="label">
  33. <span class="label-text font-medium">选择学生 <span class="text-error">*</span></span>
  34. </label>
  35. <select
  36. wire:model.live="studentId"
  37. class="select select-bordered w-full select-lg"
  38. @if(empty($teacherId)) disabled @endif
  39. >
  40. <option value="">
  41. @if(empty($teacherId))
  42. 请先选择老师
  43. @else
  44. 请选择学生...
  45. @endif
  46. </option>
  47. @foreach($this->students as $student)
  48. <option value="{{ $student->student_id }}">
  49. {{ trim($student->name ?? $student->student_id) . " ({$student->grade} - {$student->class_name})" }}
  50. </option>
  51. @endforeach
  52. </select>
  53. </div>
  54. </div>
  55. <div class="mt-3 flex items-center justify-between">
  56. <div class="text-xs text-slate-500">按照上传卷子/智能出卷同款逻辑联动师生</div>
  57. <button
  58. wire:click="loadMistakeData"
  59. class="btn btn-primary btn-md"
  60. >
  61. <span wire:loading.remove>刷新</span>
  62. <span wire:loading class="loading loading-spinner loading-xs"></span>
  63. </button>
  64. </div>
  65. </div>
  66. </div>
  67. </div>
  68. @if ($actionMessage)
  69. <div class="alert {{ $actionMessageType === 'danger' ? 'alert-error' : ($actionMessageType === 'warning' ? 'alert-warning' : 'alert-success') }} shadow-sm">
  70. <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
  71. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
  72. </svg>
  73. <span>{{ $actionMessage }}</span>
  74. </div>
  75. @endif
  76. @if ($errorMessage)
  77. <div class="alert alert-error shadow-sm">
  78. <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
  79. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
  80. </svg>
  81. <div>
  82. <h3 class="font-bold">加载失败</h3>
  83. <div class="text-xs">{{ $errorMessage }}</div>
  84. </div>
  85. </div>
  86. @endif
  87. <div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
  88. <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 flex items-center gap-3">
  89. <div class="w-10 h-10 rounded-full bg-sky-100 text-sky-600 flex items-center justify-center font-semibold">总</div>
  90. <div>
  91. <p class="text-xs text-slate-500">总错题</p>
  92. <p class="text-3xl font-bold text-slate-900">{{ $summary['total'] ?? 0 }}</p>
  93. </div>
  94. </div>
  95. <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 flex items-center gap-3">
  96. <div class="w-10 h-10 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center font-semibold">7d</div>
  97. <div>
  98. <p class="text-xs text-slate-500">本周错题</p>
  99. <p class="text-3xl font-bold text-slate-900">{{ $summary['this_week'] ?? 0 }}</p>
  100. </div>
  101. </div>
  102. <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 flex items-center gap-3">
  103. <div class="w-10 h-10 rounded-full bg-amber-100 text-amber-600 flex items-center justify-center font-semibold">待</div>
  104. <div>
  105. <p class="text-xs text-slate-500">待复习</p>
  106. <p class="text-3xl font-bold text-amber-600">{{ $summary['pending_review'] ?? 0 }}</p>
  107. </div>
  108. </div>
  109. <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 flex items-center gap-3">
  110. <div class="w-10 h-10 rounded-full bg-emerald-100 text-emerald-600 flex items-center justify-center font-semibold">AI</div>
  111. <div class="flex-1">
  112. <p class="text-xs text-slate-500">掌握率</p>
  113. @php $masteryRate = $summary['mastery_rate'] ?? null; @endphp
  114. <p class="text-3xl font-bold text-emerald-700">
  115. {{ $masteryRate !== null ? number_format($masteryRate * 100, 1) . '%' : '--' }}
  116. </p>
  117. <div class="mt-1 h-2 bg-slate-200 rounded-full overflow-hidden">
  118. <div class="h-2 bg-emerald-500" style="width: {{ $masteryRate ? $masteryRate * 100 : 0 }}%"></div>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. <div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
  124. <div class="lg:col-span-4 space-y-4">
  125. <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 space-y-4">
  126. <div class="flex items-center justify-between">
  127. <h3 class="font-semibold text-slate-900">多维筛选</h3>
  128. <button class="btn btn-primary btn-sm" wire:click="applyFilters">应用</button>
  129. </div>
  130. <div class="space-y-3">
  131. <div class="form-control">
  132. <label class="label pb-1">
  133. <span class="label-text font-medium text-slate-800">知识点</span>
  134. <span class="text-xs text-slate-400">多选</span>
  135. </label>
  136. <select
  137. multiple
  138. size="6"
  139. wire:model="filters.kp_ids"
  140. class="select select-bordered w-full bg-white"
  141. >
  142. @foreach($filterOptions['knowledge_points'] as $kp)
  143. <option value="{{ $kp['code'] }}">{{ $kp['name'] }} ({{ $kp['code'] }})</option>
  144. @endforeach
  145. </select>
  146. </div>
  147. <div class="form-control">
  148. <label class="label pb-1">
  149. <span class="label-text font-medium text-slate-800">技能</span>
  150. <span class="text-xs text-slate-400">联动</span>
  151. </label>
  152. @php
  153. $selectedKps = $filters['kp_ids'] ?? [];
  154. $skillOptions = collect($filterOptions['skills'] ?? [])
  155. ->when(!empty($selectedKps), function($c) use ($selectedKps) {
  156. return $c->filter(fn($item) => empty($item['kp_code']) || in_array($item['kp_code'], $selectedKps));
  157. })
  158. ->values()
  159. ->all();
  160. @endphp
  161. <select
  162. multiple
  163. size="6"
  164. wire:model="filters.skill_ids"
  165. class="select select-bordered w-full bg-white"
  166. >
  167. @foreach($skillOptions as $skill)
  168. <option value="{{ $skill['id'] }}">
  169. {{ $skill['name'] ?? $skill['id'] }}
  170. @if(!empty($skill['kp_code']))
  171. · {{ $skill['kp_code'] }}
  172. @endif
  173. </option>
  174. @endforeach
  175. </select>
  176. </div>
  177. <div>
  178. <p class="label-text font-medium mb-2">错误类型</p>
  179. <div class="grid grid-cols-2 gap-2">
  180. @foreach(['计算错误', '概念错误', '方法错误', '审题错误', '表达错误'] as $type)
  181. <label class="flex items-center gap-2 rounded-lg px-2 py-1 bg-slate-50 border border-slate-200">
  182. <input
  183. type="checkbox"
  184. class="checkbox checkbox-sm checkbox-primary"
  185. value="{{ $type }}"
  186. wire:model="filters.error_types"
  187. >
  188. <span class="text-sm text-slate-700">{{ $type }}</span>
  189. </label>
  190. @endforeach
  191. </div>
  192. </div>
  193. <div>
  194. <p class="label-text font-medium mb-2">时间范围</p>
  195. <div class="grid grid-cols-3 gap-2">
  196. <button class="btn btn-sm {{ $filters['time_range'] === 'last_7' ? 'btn-primary' : 'btn-outline' }}" wire:click="$set('filters.time_range', 'last_7')">7天</button>
  197. <button class="btn btn-sm {{ $filters['time_range'] === 'last_30' ? 'btn-primary' : 'btn-outline' }}" wire:click="$set('filters.time_range', 'last_30')">30天</button>
  198. <button class="btn btn-sm {{ $filters['time_range'] === 'custom' ? 'btn-primary' : 'btn-outline' }}" wire:click="$set('filters.time_range', 'custom')">自定义</button>
  199. </div>
  200. @if($filters['time_range'] === 'custom')
  201. <div class="mt-3 space-y-2">
  202. <input type="date" class="input input-bordered w-full" wire:model="filters.start_date">
  203. <input type="date" class="input input-bordered w-full" wire:model="filters.end_date">
  204. <button class="btn btn-ghost btn-xs" wire:click="clearCustomRange">清空</button>
  205. </div>
  206. @endif
  207. </div>
  208. <button
  209. wire:click="applyFilters"
  210. class="btn btn-primary btn-md w-full"
  211. >
  212. <span wire:loading.remove>应用筛选</span>
  213. <span wire:loading class="loading loading-spinner"></span>
  214. </button>
  215. </div>
  216. </div>
  217. <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 space-y-3">
  218. <div class="flex items-center justify-between">
  219. <h3 class="font-semibold text-slate-900">推荐补救路径</h3>
  220. <button class="btn btn-ghost btn-sm text-indigo-600" wire:click="refreshPatterns">刷新</button>
  221. </div>
  222. @if(!empty($patterns['recommend_path']))
  223. <ul class="timeline timeline-vertical">
  224. @foreach($patterns['recommend_path'] as $step)
  225. <li>
  226. <div class="timeline-middle">
  227. <div class="w-2.5 h-2.5 rounded-full bg-indigo-500"></div>
  228. </div>
  229. <div class="timeline-end timeline-box bg-white border border-indigo-100 text-sm text-slate-700">
  230. {{ $step['title'] ?? ($step['kp'] ?? '学习步骤') }}
  231. @if(!empty($step['description']))
  232. <p class="text-xs text-slate-500 mt-1">{{ $step['description'] }}</p>
  233. @endif
  234. </div>
  235. <hr class="bg-indigo-200"/>
  236. </li>
  237. @endforeach
  238. </ul>
  239. @else
  240. <p class="text-sm text-slate-500">暂无推荐路径,稍后重试</p>
  241. @endif
  242. </div>
  243. </div>
  244. <div class="lg:col-span-8 space-y-5">
  245. <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5">
  246. <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
  247. <div>
  248. <h3 class="text-lg font-semibold text-slate-900">错题列表</h3>
  249. <p class="text-sm text-slate-500">题干、作答、AI 解析与操作</p>
  250. </div>
  251. <div class="flex flex-wrap gap-2 items-center">
  252. <button class="btn btn-md btn-secondary" wire:click="generatePracticeFromSelection">
  253. 📚 基于错题生成练习
  254. </button>
  255. <div class="badge badge-outline">
  256. 已选 {{ count($selectedMistakeIds) }} / {{ count($mistakes) }}
  257. </div>
  258. </div>
  259. </div>
  260. @if ($isLoading)
  261. <div class="flex items-center justify-center py-10 text-slate-500">
  262. <span class="loading loading-spinner loading-lg mr-3"></span>
  263. 正在加载错题...
  264. </div>
  265. @elseif(empty($mistakes))
  266. <div class="text-center py-12 text-slate-500">
  267. <svg class="mx-auto h-12 w-12 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
  268. <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6l4 2m6-2a9 9 0 11-18 0 9 9 0 0118 0z" />
  269. </svg>
  270. <p class="mt-3">暂无错题,先选择老师和学生</p>
  271. </div>
  272. @else
  273. <div class="space-y-4 mt-4">
  274. @foreach($mistakes as $mistake)
  275. <div class="rounded-xl border border-slate-200 bg-white shadow-sm p-4 space-y-4" wire:key="mistake-{{ $mistake['id'] ?? $loop->index }}">
  276. <div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
  277. <div class="flex items-center gap-3">
  278. <input
  279. type="checkbox"
  280. class="checkbox checkbox-primary"
  281. wire:click="toggleSelection('{{ $mistake['id'] ?? '' }}')"
  282. @checked(in_array($mistake['id'] ?? '', $selectedMistakeIds, true))
  283. >
  284. <div>
  285. <p class="text-xs text-slate-400">{{ $mistake['id'] ?? 'ID' }}</p>
  286. <p class="text-sm text-slate-500">
  287. {{ $mistake['created_at'] ?? '' }}
  288. </p>
  289. </div>
  290. </div>
  291. <div class="flex flex-wrap gap-2">
  292. @foreach(($mistake['kp_ids'] ?? []) as $kp)
  293. <span class="badge badge-ghost">KP {{ $kp }}</span>
  294. @endforeach
  295. @foreach(($mistake['skill_ids'] ?? []) as $skill)
  296. <span class="badge badge-outline badge-info">{{ $skill }}</span>
  297. @endforeach
  298. @if(!empty($mistake['error_type']))
  299. <span class="badge badge-warning badge-outline">{{ $mistake['error_type'] }}</span>
  300. @endif
  301. @if(isset($mistake['correct']))
  302. <span class="badge {{ $mistake['correct'] ? 'badge-success' : 'badge-error' }}">
  303. {{ $mistake['correct'] ? '已掌握' : '错误' }}
  304. </span>
  305. @endif
  306. @if(!empty($mistake['reviewed']))
  307. <span class="badge badge-success badge-outline">已复习</span>
  308. @endif
  309. @if(!empty($mistake['favorite']))
  310. <span class="badge badge-primary badge-outline">已收藏</span>
  311. @endif
  312. </div>
  313. </div>
  314. <div class="rounded-lg bg-slate-50 p-4 border border-slate-100">
  315. <p class="text-sm font-semibold text-slate-800 mb-2">题干</p>
  316. <div class="prose max-w-none question-content text-slate-800">
  317. <x-math-render :content="$mistake['question']['stem'] ?? ($mistake['question']['content'] ?? '暂无题干')" class="text-base" />
  318. </div>
  319. </div>
  320. <div class="grid grid-cols-1 md:grid-cols-3 gap-3">
  321. <div class="bg-white border border-slate-200 rounded-lg p-3">
  322. <p class="text-xs text-slate-500 mb-1">学生作答</p>
  323. <p class="text-sm text-slate-800 break-words">{{ $mistake['student_answer'] ?? '无' }}</p>
  324. </div>
  325. <div class="bg-white border border-slate-200 rounded-lg p-3">
  326. <p class="text-xs text-slate-500 mb-1">正确答案</p>
  327. <p class="text-sm text-emerald-700 break-words">
  328. {{ $mistake['question']['answer'] ?? ($mistake['correct_answer'] ?? '未知') }}
  329. </p>
  330. </div>
  331. <div class="bg-white border border-slate-200 rounded-lg p-3">
  332. <p class="text-xs text-slate-500 mb-1">错误类型</p>
  333. <p class="text-sm text-amber-700">
  334. {{ $mistake['error_type'] ?? '未分类' }}
  335. </p>
  336. </div>
  337. </div>
  338. <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
  339. <div class="bg-gradient-to-br from-amber-50 to-white border border-amber-100 rounded-lg p-4 space-y-2">
  340. <p class="text-sm font-semibold text-amber-800">错误原因分析</p>
  341. <p class="text-sm text-amber-700">{{ $mistake['ai_analysis']['reason'] ?? $mistake['ai_analysis'] ?? '暂无分析' }}</p>
  342. <p class="text-sm font-semibold text-amber-800">对应技能</p>
  343. <p class="text-sm text-amber-700">{{ $mistake['ai_analysis']['skill'] ?? ($mistake['skill_desc'] ?? '未识别') }}</p>
  344. </div>
  345. <div class="bg-gradient-to-br from-emerald-50 to-white border border-emerald-100 rounded-lg p-4 space-y-2">
  346. <p class="text-sm font-semibold text-emerald-800">正确解法</p>
  347. <p class="text-sm text-emerald-700">{{ $mistake['ai_analysis']['solution'] ?? '可向AI请求解析' }}</p>
  348. <p class="text-sm font-semibold text-emerald-800">避免类似错误</p>
  349. <p class="text-sm text-emerald-700">{{ $mistake['ai_analysis']['tip'] ?? ($mistake['ai_analysis']['suggestion'] ?? '加强审题与演算步骤复查') }}</p>
  350. </div>
  351. </div>
  352. <div class="divider my-2"></div>
  353. <div class="flex flex-wrap gap-2">
  354. <button class="btn btn-sm btn-ghost" wire:click="toggleFavorite('{{ $mistake['id'] ?? '' }}')">
  355. {{ !empty($mistake['favorite']) ? '取消收藏' : '收藏' }}
  356. </button>
  357. <button class="btn btn-sm btn-ghost" wire:click="markReviewed('{{ $mistake['id'] ?? '' }}')">
  358. 标记已复习
  359. </button>
  360. <button class="btn btn-sm btn-ghost" wire:click="addToRetryList('{{ $mistake['id'] ?? '' }}')">
  361. 加入重练清单
  362. </button>
  363. <button class="btn btn-sm btn-outline" wire:click="loadRelatedQuestions('{{ $mistake['id'] ?? '' }}')">
  364. 查看关联题
  365. </button>
  366. </div>
  367. @if(!empty($relatedQuestions[$mistake['id'] ?? ''] ?? []))
  368. <div class="bg-slate-50 border border-slate-200 rounded-lg p-3 space-y-2">
  369. <p class="text-sm font-semibold text-slate-800">关联题目</p>
  370. <div class="grid grid-cols-1 md:grid-cols-2 gap-2">
  371. @foreach($relatedQuestions[$mistake['id']] as $related)
  372. <div class="p-3 bg-white border border-slate-200 rounded-lg">
  373. <p class="text-xs text-slate-400 mb-1">ID: {{ $related['id'] ?? '' }}</p>
  374. <p class="text-sm text-slate-800 overflow-hidden max-h-14">{{ $related['stem'] ?? $related['content'] ?? '相关题目' }}</p>
  375. <p class="text-xs text-slate-500 mt-2">
  376. 难度: {{ $related['difficulty'] ?? '中等' }}
  377. @if(!empty($related['kp_codes']))
  378. · KP: {{ is_array($related['kp_codes']) ? implode(',', $related['kp_codes']) : $related['kp_codes'] }}
  379. @endif
  380. </p>
  381. </div>
  382. @endforeach
  383. </div>
  384. </div>
  385. @endif
  386. </div>
  387. @endforeach
  388. </div>
  389. @endif
  390. </div>
  391. @if(!empty($recommendations))
  392. <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 space-y-4">
  393. <div class="flex items-center justify-between">
  394. <div>
  395. <h3 class="text-lg font-semibold text-slate-900">重练题单</h3>
  396. <p class="text-sm text-slate-500">基于错题推荐的新题,支持导出</p>
  397. </div>
  398. <a href="{{ url('/admin/question-management') }}" class="btn btn-outline btn-sm" target="_blank">打开题库</a>
  399. </div>
  400. <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
  401. @foreach($recommendations as $rec)
  402. <div class="p-4 border border-slate-200 rounded-lg bg-slate-50">
  403. <p class="text-xs text-slate-400 mb-1">题目 ID: {{ $rec['id'] ?? '' }}</p>
  404. <p class="text-sm text-slate-800 overflow-hidden max-h-16">{{ $rec['stem'] ?? $rec['content'] ?? '推荐题目' }}</p>
  405. <div class="flex flex-wrap gap-2 mt-2">
  406. @if(!empty($rec['kp_codes']))
  407. <span class="badge badge-ghost">KP {{ is_array($rec['kp_codes']) ? implode(',', $rec['kp_codes']) : $rec['kp_codes'] }}</span>
  408. @endif
  409. @if(!empty($rec['skills']))
  410. <span class="badge badge-outline badge-info">{{ is_array($rec['skills']) ? implode(',', $rec['skills']) : $rec['skills'] }}</span>
  411. @endif
  412. </div>
  413. </div>
  414. @endforeach
  415. </div>
  416. </div>
  417. @endif
  418. <div class="rounded-2xl bg-white border border-slate-200 shadow-sm p-5 space-y-6">
  419. <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
  420. <div>
  421. <h3 class="text-lg font-semibold text-slate-900">智能分析</h3>
  422. <p class="text-sm text-slate-500">错误类型雷达图 · 弱点技能排名 · 薄弱知识点</p>
  423. </div>
  424. <button class="btn btn-ghost btn-sm" wire:click="refreshPatterns">
  425. 重新拉取
  426. </button>
  427. </div>
  428. <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
  429. @if(!empty($patterns['error_types']))
  430. <div class="bg-slate-50 rounded-xl border border-slate-200 p-4">
  431. <div class="flex items-center justify-between mb-3">
  432. <h4 class="font-semibold text-slate-800">错误类型雷达图</h4>
  433. <span class="badge badge-ghost">AI</span>
  434. </div>
  435. <canvas id="mistakeRadarChart" class="w-full h-64"></canvas>
  436. </div>
  437. @endif
  438. @if(!empty($patterns['top_skills']))
  439. <div class="bg-slate-50 rounded-xl border border-slate-200 p-4">
  440. <div class="flex items-center justify-between mb-3">
  441. <h4 class="font-semibold text-slate-800">弱点技能排名</h4>
  442. <span class="badge badge-warning badge-outline">Top</span>
  443. </div>
  444. <canvas id="skillBarChart" class="w-full h-64"></canvas>
  445. </div>
  446. @endif
  447. </div>
  448. @if(!empty($patterns['top_kps']))
  449. <div class="bg-slate-50 rounded-xl border border-slate-200 p-4">
  450. <h4 class="font-semibold text-slate-800 mb-3">薄弱知识点热力</h4>
  451. <div class="space-y-2">
  452. @foreach(($patterns['top_kps'] ?? []) as $kp)
  453. <div>
  454. <div class="flex items-center justify-between text-sm text-slate-700">
  455. <span>{{ $kp['name'] ?? ($kp['kp'] ?? $kp['kp_code'] ?? '知识点') }}</span>
  456. <span class="text-xs text-slate-500">错误 {{ $kp['count'] ?? $kp['mistake_count'] ?? 0 }}</span>
  457. </div>
  458. @php
  459. $score = ($kp['score'] ?? $kp['accuracy'] ?? 0);
  460. if ($score > 1) $score = $score / 100;
  461. $width = max(10, min(100, (1 - (float) $score) * 100));
  462. @endphp
  463. <progress class="progress progress-error w-full" value="{{ $width }}" max="100"></progress>
  464. </div>
  465. @endforeach
  466. </div>
  467. </div>
  468. @endif
  469. </div>
  470. </div>
  471. </div>
  472. </div>
  473. </div>
  474. @push('scripts')
  475. <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  476. <script>
  477. document.addEventListener('livewire:navigated', initMistakeCharts);
  478. document.addEventListener('livewire:load', initMistakeCharts);
  479. function initMistakeCharts() {
  480. const radarCanvas = document.getElementById('mistakeRadarChart');
  481. const barCanvas = document.getElementById('skillBarChart');
  482. if ((!radarCanvas && !barCanvas)) {
  483. return;
  484. }
  485. const errorTypes = @json($patterns['error_types'] ?? []);
  486. const skills = @json($patterns['top_skills'] ?? []);
  487. const radarLabels = errorTypes.map(e => e.name || e.type || '错误');
  488. const radarData = errorTypes.map(e => e.count || e.value || 0);
  489. const skillLabels = skills.map(s => s.name || s.skill || '技能');
  490. const skillData = skills.map(s => s.score || s.count || 0);
  491. if (window.mistakeRadarChart) {
  492. window.mistakeRadarChart.destroy();
  493. }
  494. if (window.skillBarChart) {
  495. window.skillBarChart.destroy();
  496. }
  497. if (radarCanvas && radarLabels.length) {
  498. window.mistakeRadarChart = new Chart(radarCanvas.getContext('2d'), {
  499. type: 'radar',
  500. data: {
  501. labels: radarLabels,
  502. datasets: [{
  503. label: '错误频次',
  504. data: radarData,
  505. backgroundColor: 'rgba(248, 180, 0, 0.2)',
  506. borderColor: '#f59e0b',
  507. borderWidth: 2,
  508. pointBackgroundColor: '#f97316'
  509. }]
  510. },
  511. options: {
  512. plugins: { legend: { display: false } },
  513. scales: {
  514. r: {
  515. beginAtZero: true,
  516. ticks: { stepSize: 1 }
  517. }
  518. }
  519. }
  520. });
  521. }
  522. if (barCanvas && skillLabels.length) {
  523. window.skillBarChart = new Chart(barCanvas.getContext('2d'), {
  524. type: 'bar',
  525. data: {
  526. labels: skillLabels,
  527. datasets: [{
  528. label: '弱点指数',
  529. data: skillData,
  530. backgroundColor: 'rgba(59, 130, 246, 0.2)',
  531. borderColor: '#3b82f6',
  532. borderWidth: 1,
  533. borderRadius: 6
  534. }]
  535. },
  536. options: {
  537. plugins: { legend: { display: false }, tooltip: { mode: 'index' } },
  538. scales: {
  539. x: { ticks: { color: '#475569' } },
  540. y: { beginAtZero: true }
  541. }
  542. }
  543. });
  544. }
  545. }
  546. </script>
  547. @endpush