pdf-report.blade.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. @php
  2. // 提取15位paper_id数字部分作为学案编号
  3. $rawPaperId = $paper['id'] ?? $paper['paper_id'] ?? 'unknown';
  4. preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
  5. $reportCode = $matches[1] ?? preg_replace('/[^0-9]/', '', $rawPaperId);
  6. $averageMastery = isset($mastery['average']) ? number_format($mastery['average'] * 100, 1) . '%' : '无数据';
  7. // 【修复】从insights中获取AI分析结果(而不是从analysis_data)
  8. $questionAnalysis = $insights ?? [];
  9. // 生成时间(格式:2026年01月30日 15:04:05)
  10. $generateDateTime = now()->format('Y年m月d日 H:i:s');
  11. @endphp
  12. <!DOCTYPE html>
  13. <html lang="zh-CN">
  14. <head>
  15. <meta charset="UTF-8">
  16. <title>学情报告 - {{ $paper['name'] ?? '试卷' }}</title>
  17. <link rel="stylesheet" href="/css/katex/katex.min.css">
  18. <style>
  19. @page {
  20. size: A4;
  21. margin: 2.2cm 2cm 2.3cm 2cm;
  22. @top-left {
  23. content: "知了数学·{{ $generateDateTime }}";
  24. font-size: 13px;
  25. color: #666;
  26. }
  27. @top-center {
  28. content: "{{ $student['name'] ?? '-' }}";
  29. font-size: 13px;
  30. color: #666;
  31. }
  32. @top-right {
  33. content: "{{ $reportCode }}";
  34. font-size: 19px;
  35. font-weight: 600;
  36. font-family: "Noto Sans", "Liberation Sans", "Nimbus Sans", sans-serif;
  37. letter-spacing: 0;
  38. padding-right: 3mm;
  39. padding-top: 1.8mm;
  40. color: #222;
  41. }
  42. @bottom-left {
  43. content: "{{ $reportCode }}";
  44. font-size: 11px;
  45. color: #666;
  46. }
  47. @bottom-right {
  48. content: counter(page) "/" counter(pages);
  49. font-size: 13px;
  50. color: #666;
  51. }
  52. }
  53. * { box-sizing: border-box; }
  54. body {
  55. font-family: "SimSun", "Songti SC", serif;
  56. margin: 0;
  57. padding: 0;
  58. color: #000;
  59. background: #fff;
  60. font-size: 12px;
  61. line-height: 1.6;
  62. }
  63. h1, h2, h3 { margin: 0; color: #000; }
  64. .card { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 16px; margin-bottom: 12px; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); }
  65. .header { text-align: center; margin-bottom: 1.5rem; border-bottom: 2px solid #000; padding-bottom: 1rem; }
  66. .paper-title { font-size: 22px; font-weight: bold; margin-bottom: 14px; }
  67. .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
  68. .tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; color: #374151; background: #e5e7eb; }
  69. .section-title { font-size: 16px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
  70. .pill { padding: 4px 10px; border-radius: 999px; font-size: 12px; }
  71. .pill.green { background: #ecfdf3; color: #15803d; }
  72. .pill.amber { background: #fef3c7; color: #b45309; }
  73. .pill.red { background: #fef2f2; color: #b91c1c; }
  74. table { width: 100%; border-collapse: collapse; font-size: 13px; }
  75. th, td { padding: 8px 10px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
  76. th { background: #f3f4f6; color: #111827; }
  77. .muted { color: #6b7280; font-size: 12px; }
  78. .progress-wrap { background: #f3f4f6; border-radius: 999px; overflow: hidden; height: 10px; }
  79. .progress-bar { height: 100%; background: linear-gradient(90deg, #4f46e5, #10b981); }
  80. .recommend-card { border: 1px dashed #cbd5e1; border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; background: #f8fafc; }
  81. .relation-board { margin: 6px 0 10px; padding: 10px 12px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc; }
  82. .relation-block { margin-bottom: 10px; padding: 8px; border: 1px solid #dbeafe; border-radius: 8px; background: #fff; }
  83. .tree-cards { width: 100%; border-collapse: collapse; table-layout: fixed; }
  84. .tree-cards td { border: none; padding: 0; vertical-align: top; }
  85. .tree-left { width: 45%; padding-right: 10px; }
  86. .tree-right { width: 55%; padding-left: 10px; }
  87. .tree-parent {
  88. display: inline-block;
  89. margin-bottom: 6px;
  90. padding: 4px 8px;
  91. border-radius: 999px;
  92. border: 1px solid #93c5fd;
  93. background: #dbeafe;
  94. color: #1e3a8a;
  95. font-weight: 700;
  96. font-size: 12px;
  97. }
  98. .tree-lines { margin: 0; padding-left: 14px; border-left: 2px solid #cbd5e1; }
  99. .tree-line {
  100. font-size: 11px;
  101. color: #334155;
  102. margin-bottom: 3px;
  103. white-space: normal;
  104. overflow: visible;
  105. text-overflow: clip;
  106. word-break: break-all;
  107. line-height: 1.45;
  108. }
  109. .tree-line.hit { font-weight: 700; color: #1d4ed8; }
  110. .tree-line-badge { display: inline-block; margin-left: 6px; padding: 1px 5px; border-radius: 999px; font-size: 10px; border: 1px solid transparent; }
  111. .tree-line-badge.high { background: #ecfdf3; color: #15803d; border-color: #86efac; }
  112. .tree-line-badge.mid { background: #fffbeb; color: #b45309; border-color: #fcd34d; }
  113. .tree-line-badge.low { background: #fef2f2; color: #b91c1c; border-color: #fca5a5; }
  114. .tree-line-badge.miss { background: #f3f4f6; color: #6b7280; border-color: #d1d5db; }
  115. .detail-card {
  116. border: 1px solid #e2e8f0;
  117. border-radius: 6px;
  118. background: #f8fafc;
  119. padding: 6px 8px;
  120. margin-bottom: 5px;
  121. }
  122. .detail-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; }
  123. .detail-title { font-size: 11px; color: #0f172a; font-weight: 700; margin-bottom: 2px; }
  124. .detail-meta { font-size: 11px; color: #475569; margin-top: 3px; }
  125. .detail-mastery { font-size: 38px; color: #0f172a; font-weight: 700; line-height: 1; margin: 0; }
  126. .detail-mastery.high { color: #16a34a; }
  127. .detail-mastery.mid { color: #d97706; }
  128. .detail-mastery.low { color: #dc2626; }
  129. .node-tags { margin-top: 2px; }
  130. .node-tag {
  131. display: inline-block;
  132. margin: 0 6px 6px 0;
  133. padding: 1px 7px;
  134. border-radius: 999px;
  135. font-size: 10px;
  136. line-height: 1.4;
  137. color: #475569;
  138. background: #f1f5f9;
  139. border: 1px solid #cbd5e1;
  140. }
  141. .suggest-tags { display: inline; }
  142. .suggest-tag {
  143. display: inline-block;
  144. margin: 0 6px 6px 0;
  145. padding: 1px 7px;
  146. border-radius: 999px;
  147. font-size: 10px;
  148. line-height: 1.4;
  149. color: #334155;
  150. background: #f8fafc;
  151. border: 1px solid #d1d5db;
  152. }
  153. .weak-kp-tags { margin-top: 6px; }
  154. .weak-kp-tag {
  155. display: inline-block;
  156. margin: 0 6px 6px 0;
  157. padding: 1px 7px;
  158. border-radius: 999px;
  159. font-size: 10px;
  160. line-height: 1.45;
  161. color: #854d0e;
  162. background: #fef3c7;
  163. border: 1px solid #fcd34d;
  164. }
  165. .error-kp-tag {
  166. display: inline-block;
  167. margin: 0 6px 6px 0;
  168. padding: 1px 7px;
  169. border-radius: 999px;
  170. font-size: 10px;
  171. line-height: 1.45;
  172. color: #334155;
  173. background: #f8fafc;
  174. border: 1px solid #d1d5db;
  175. }
  176. .error-kp-tag.high-risk {
  177. color: #b91c1c;
  178. border-color: #fca5a5;
  179. background: #fff;
  180. font-weight: 600;
  181. }
  182. .detail-change-up { color: #16a34a; font-weight: 700; }
  183. .detail-change-down { color: #dc2626; font-weight: 700; }
  184. .aggregate-tip { font-size: 11px; color: #475569; margin-top: 6px; }
  185. .question-card { border:1px solid #e5e7eb; border-radius:8px; padding:6px 9px; margin-bottom:5px; background:#fff; page-break-inside: auto; break-inside: auto; }
  186. .question-block { margin-bottom: 5px; padding: 5px; border-radius: 4px; }
  187. .solution-content {
  188. display: inline-block;
  189. line-height: 1.75;
  190. white-space: normal;
  191. word-break: break-word;
  192. }
  193. </style>
  194. </head>
  195. <body>
  196. <div class="page">
  197. <div class="header">
  198. <h1 class="paper-title">学情分析报告</h1>
  199. @php
  200. $teacherName = trim((string) ($teacher['name'] ?? ''));
  201. $showTeacher = $teacherName !== '' && $teacherName !== '________' && $teacherName !== '未知老师';
  202. @endphp
  203. <div style="display:flex;justify-content:space-between;font-size:14px;margin-top:8px;">
  204. @if($showTeacher)
  205. <span>老师:{{ $teacherName }}</span>
  206. @endif
  207. <span>年级:@formatGrade($student['grade'] ?? '________')</span>
  208. @if(!empty($paper['assemble_type_label']) && $paper['assemble_type_label'] !== '未知类型')
  209. <span>类型:{{ $paper['assemble_type_label'] }}</span>
  210. @endif
  211. <span>姓名:{{ $student['name'] ?? '________' }}</span>
  212. <span>题目数:{{ $paper['total_questions'] ?? (is_array($questions ?? null) ? count($questions) : '-') }}</span>
  213. </div>
  214. </div>
  215. <div class="card">
  216. <div class="section-title">本次命中子知识点掌握度</div>
  217. @php
  218. // 【修复】过滤掉K-GENERAL等通用知识点,只显示有值的知识点
  219. $filteredMasteryItems = [];
  220. if (!empty($mastery['items'])) {
  221. foreach ($mastery['items'] as $item) {
  222. $kpCode = $item['kp_code'] ?? '';
  223. $masteryLevel = $item['mastery_level'] ?? 0;
  224. // 过滤条件:不是通用知识点,且掌握度>0
  225. if (!in_array($kpCode, ['K-GENERAL', 'GENERAL', 'DEFAULT']) && $masteryLevel > 0) {
  226. $filteredMasteryItems[] = $item;
  227. }
  228. }
  229. }
  230. @endphp
  231. @if(!empty($filteredMasteryItems))
  232. @foreach($filteredMasteryItems as $item)
  233. @php
  234. $pct = min(100, max(0, ($item['mastery_level'] ?? 0) * 100));
  235. $barColor = $pct >= 80 ? '#10b981' : ($pct >= 60 ? '#f59e0b' : '#ef4444');
  236. $delta = $item['mastery_change'] ?? null;
  237. // 只有当有变化值时才显示变化信息
  238. $changeText = '';
  239. if ($delta !== null && $delta !== '' && abs($delta) > 0.001) {
  240. $changeText = ($delta > 0 ? '↑ ' : '↓ ') . number_format(abs($delta) * 100, 1) . '%';
  241. }
  242. @endphp
  243. <div style="margin-bottom:12px; padding: 8px; border: 1px solid #e5e7eb; border-radius: 6px;">
  244. <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 6px;">
  245. <div style="font-weight: 600; font-size: 14px;">{{ $item['kp_name'] ?? $item['kp_code'] ?? '未知知识点' }}</div>
  246. <div style="font-weight: 600; color: {{ $barColor }}; font-size: 14px;">
  247. {{ number_format($pct, 1) }}%
  248. <span style="margin-left: 8px; color: #666; font-size: 12px;">{{ $changeText ? '子节点变化 ' . $changeText : '' }}</span>
  249. </div>
  250. </div>
  251. <div class="progress-wrap" style="height: 12px;">
  252. <div class="progress-bar" style="width: {{ $pct }}%; background: {{ $barColor }};"></div>
  253. </div>
  254. </div>
  255. @endforeach
  256. @else
  257. <div class="muted">暂无有效掌握度数据(已过滤通用知识点和零值)</div>
  258. @endif
  259. </div>
  260. <div class="card">
  261. <div class="section-title">📊 知识点层级掌握度分析</div>
  262. @php
  263. $parentMasteryLevels = $parent_mastery_levels ?? [];
  264. $hasParentMastery = !empty($parentMasteryLevels);
  265. $childMasteryMap = [];
  266. foreach (($filteredMasteryItems ?? []) as $it) {
  267. $childMasteryMap[$it['kp_code']] = [
  268. 'level' => floatval($it['mastery_level'] ?? 0),
  269. 'delta' => isset($it['mastery_change']) ? floatval($it['mastery_change']) : null,
  270. ];
  271. }
  272. @endphp
  273. @if($hasParentMastery)
  274. <div class="relation-board">
  275. <div style="font-size:12px; color:#334155; margin-bottom:6px; font-weight:600;">父子知识点关系</div>
  276. @foreach($parentMasteryLevels as $parentData)
  277. @php
  278. $childrenAll = $parentData['children_all'] ?? [];
  279. $children = $parentData['children'] ?? []; // 命中子节点
  280. $childCount = count($childrenAll);
  281. $hitCount = count($children);
  282. $hitAvg = $parentData['children_hit_avg_mastery'] ?? null;
  283. $parentPct = number_format(floatval($parentData['mastery_percentage'] ?? 0), 1);
  284. $parentLevel = floatval($parentData['mastery_level'] ?? 0);
  285. $parentClass = $parentLevel >= 0.8 ? 'high' : ($parentLevel >= 0.6 ? 'mid' : 'low');
  286. $delta = $parentData['mastery_change'] ?? null;
  287. $hitNames = array_values(array_map(fn($c) => $c['kp_name'] ?? ($c['kp_code'] ?? ''), $children));
  288. $allChildrenPerfect = !empty($childrenAll) && count(array_filter($childrenAll, function ($c) {
  289. return floatval($c['mastery_level'] ?? 0) >= 0.999;
  290. })) === count($childrenAll);
  291. @endphp
  292. <div class="relation-block">
  293. <table class="tree-cards">
  294. <tr>
  295. <td class="tree-left">
  296. <div class="tree-parent">{{ $parentData['kp_name'] ?? $parentData['kp_code'] }}</div>
  297. @if(!empty($childrenAll))
  298. <div class="tree-lines">
  299. @foreach($childrenAll as $child)
  300. @php
  301. $isHit = !empty($child['is_hit']);
  302. $m = floatval($child['mastery_level'] ?? 0);
  303. $badgeClass = $m >= 0.8 ? 'high' : ($m >= 0.6 ? 'mid' : ($m > 0 ? 'low' : 'miss'));
  304. $badgeText = number_format($m * 100, 1) . '%';
  305. @endphp
  306. <div class="tree-line {{ $isHit ? 'hit' : '' }}">
  307. └─ {{ $child['kp_name'] }}
  308. <span class="tree-line-badge {{ $badgeClass }}">{{ $badgeText }}</span>
  309. @if($isHit)
  310. <span class="tree-line-badge high">本次命中</span>
  311. @endif
  312. </div>
  313. @endforeach
  314. </div>
  315. @else
  316. <div class="muted">无命中子知识点</div>
  317. @endif
  318. </td>
  319. <td class="tree-right">
  320. <div class="detail-card">
  321. <div class="detail-head">
  322. <div class="detail-title">{{ $parentData['kp_name'] ?? $parentData['kp_code'] }}</div>
  323. <div class="detail-mastery {{ $parentClass }}">{{ $parentPct }}%</div>
  324. </div>
  325. <div class="detail-meta">
  326. 父节点变化:
  327. @if($delta !== null)
  328. <span class="{{ $delta >= 0 ? 'detail-change-up' : 'detail-change-down' }}">
  329. {{ $delta >= 0 ? '↑ ' : '↓ ' }}{{ number_format(abs($delta) * 100, 1) }}%
  330. </span>
  331. @else
  332. -
  333. @endif
  334. </div>
  335. <div class="detail-meta" style="margin-top: 3px;">
  336. 本次重点子节点:
  337. @if($hitCount > 0)
  338. <span class="node-tags">
  339. @foreach($hitNames as $hitName)
  340. @if(!empty(trim($hitName)))
  341. <span class="node-tag">{{ $hitName }}</span>
  342. @endif
  343. @endforeach
  344. </span>
  345. @else
  346. @endif
  347. </div>
  348. <div class="detail-meta">
  349. 子节点总数 {{ $childCount }} 个,本次命中 {{ $hitCount }} 个,命中均值 {{ $hitAvg !== null ? number_format(floatval($hitAvg) * 100, 1) . '%' : '-' }}
  350. </div>
  351. </div>
  352. </td>
  353. </tr>
  354. </table>
  355. </div>
  356. @endforeach
  357. </div>
  358. @php
  359. $allParentPerfect = !empty($parentMasteryLevels) && count(array_filter($parentMasteryLevels, function ($p) {
  360. return floatval($p['mastery_level'] ?? 0) >= 0.999;
  361. })) === count($parentMasteryLevels);
  362. $allHitChildrenPerfect = !empty($filteredMasteryItems) && count(array_filter($filteredMasteryItems, function ($it) {
  363. return floatval($it['mastery_level'] ?? 0) >= 0.999;
  364. })) === count($filteredMasteryItems);
  365. $isAllPerfect = $allParentPerfect && $allHitChildrenPerfect;
  366. // 1) 优先:本次命中子知识点中的低掌握度(<60%)
  367. $hitWeakChildren = [];
  368. $hitWeakKeys = [];
  369. foreach (($filteredMasteryItems ?? []) as $hitItem) {
  370. $level = floatval($hitItem['mastery_level'] ?? 0);
  371. $name = trim((string) ($hitItem['kp_name'] ?? $hitItem['kp_code'] ?? ''));
  372. if ($name === '' || $level >= 0.6) {
  373. continue;
  374. }
  375. $key = (string) ($hitItem['kp_code'] ?? $name);
  376. $hitWeakChildren[$key] = [
  377. 'name' => $name,
  378. 'level' => $level,
  379. ];
  380. $hitWeakKeys[$key] = true;
  381. }
  382. $hitWeakChildren = array_values($hitWeakChildren);
  383. usort($hitWeakChildren, function ($a, $b) {
  384. return $a['level'] <=> $b['level'];
  385. });
  386. // 2) 兜底:若命中子知识点都 >=60%,再从其他低掌握度子知识点补
  387. $otherWeakChildren = [];
  388. foreach (($parentMasteryLevels ?? []) as $pData) {
  389. foreach (($pData['children_all'] ?? []) as $child) {
  390. $level = floatval($child['mastery_level'] ?? 0);
  391. $name = trim((string) ($child['kp_name'] ?? $child['kp_code'] ?? ''));
  392. if ($name === '' || $level >= 0.6) {
  393. continue;
  394. }
  395. $key = ($child['kp_code'] ?? $name);
  396. if (isset($hitWeakKeys[$key])) {
  397. continue;
  398. }
  399. $otherWeakChildren[$key] = [
  400. 'name' => $name,
  401. 'level' => $level,
  402. ];
  403. }
  404. }
  405. $otherWeakChildren = array_values($otherWeakChildren);
  406. usort($otherWeakChildren, function ($a, $b) {
  407. return $a['level'] <=> $b['level'];
  408. });
  409. // 3) 最终展示:总数不超过5
  410. $globalWeakChildren = [];
  411. if (!empty($hitWeakChildren)) {
  412. $globalWeakChildren = array_slice($hitWeakChildren, 0, 5);
  413. } else {
  414. $globalWeakChildren = array_slice($otherWeakChildren, 0, 5);
  415. }
  416. @endphp
  417. <div style="margin-top: 12px; padding: 8px; background: #fefce8; border-left: 4px solid #eab308; border-radius: 4px;">
  418. <div style="font-size: 12px; color: #854d0e; line-height: 1.6;">
  419. <strong>学习建议:</strong>
  420. @if($isAllPerfect)
  421. 本次学案表现非常出色,相关知识点掌握稳定且完整。建议继续进入新的知识点专题学习,优先选择同层级未覆盖内容或更高难度综合题,保持进阶节奏。
  422. @else
  423. 建议重点关注掌握度较低的知识点,通过专项练习提升整体学习水平。优先练习掌握度低于60%的知识点。
  424. @endif
  425. @if(!empty($globalWeakChildren))
  426. <div class="weak-kp-tags">
  427. @foreach($globalWeakChildren as $weakChild)
  428. <span class="weak-kp-tag">{{ $weakChild['name'] }}({{ number_format($weakChild['level'] * 100, 1) }}%)</span>
  429. @endforeach
  430. </div>
  431. @endif
  432. </div>
  433. </div>
  434. @else
  435. <div class="muted">暂无父节点掌握度数据</div>
  436. <div style="margin-top: 8px; font-size: 12px; color: #64748b;">
  437. 当前分析主要基于具体知识点掌握度。完整的层级掌握度分析需要在系统中建立完整的知识点层级关系。
  438. </div>
  439. @endif
  440. </div>
  441. @php
  442. $insightMap = [];
  443. foreach (($question_insights ?? []) as $insight) {
  444. $no = $insight['question_number'] ?? $insight['question_id'] ?? null;
  445. if ($no !== null) {
  446. $insightMap[$no] = $insight;
  447. }
  448. }
  449. $analysisWrongMap = [];
  450. foreach (($analysis_data['question_analysis'] ?? []) as $qa) {
  451. $qid = $qa['question_bank_id'] ?? $qa['question_id'] ?? null;
  452. if ($qid === null || $qid === '') {
  453. continue;
  454. }
  455. $rawCorrect = $qa['is_correct'] ?? null;
  456. $isWrongFromAnalysis = false;
  457. if (is_array($rawCorrect)) {
  458. $isWrongFromAnalysis = in_array(0, $rawCorrect, true);
  459. } elseif ($rawCorrect !== null) {
  460. $isWrongFromAnalysis = !boolval($rawCorrect);
  461. }
  462. if ($isWrongFromAnalysis) {
  463. $analysisWrongMap[(string)$qid] = true;
  464. }
  465. }
  466. $wrongQuestions = [];
  467. foreach (($questions ?? []) as $qItem) {
  468. $studentAnswerProbe = $qItem['student_answer'] ?? null;
  469. $correctAnswerProbe = $qItem['answer'] ?? ($qItem['correct_answer'] ?? null);
  470. $isCorrectProbe = $qItem['is_correct'] ?? null;
  471. if ($isCorrectProbe === null && !empty($studentAnswerProbe) && !empty($correctAnswerProbe)) {
  472. $isCorrectProbe = (trim((string)$studentAnswerProbe) === trim((string)$correctAnswerProbe)) ? 1 : 0;
  473. }
  474. $normalizedCorrect = $isCorrectProbe;
  475. if ($isCorrectProbe !== null) {
  476. $normalizedCorrect = is_bool($isCorrectProbe) ? ($isCorrectProbe ? 1 : 0) : intval($isCorrectProbe);
  477. }
  478. $qidProbe = (string)($qItem['question_bank_id'] ?? $qItem['question_id'] ?? '');
  479. $isWrongByAnalysis = ($qidProbe !== '' && isset($analysisWrongMap[$qidProbe]));
  480. if ($normalizedCorrect === 0 || $isWrongByAnalysis) {
  481. $wrongQuestions[] = $qItem;
  482. }
  483. }
  484. // 错题知识点聚合统计(同知识点错几题/共几题)
  485. $kpStats = [];
  486. foreach (($questions ?? []) as $qItem) {
  487. $kpName = trim((string)($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
  488. if ($kpName === '') {
  489. $kpName = '未标注知识点';
  490. }
  491. if (!isset($kpStats[$kpName])) {
  492. $kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
  493. }
  494. $kpStats[$kpName]['total']++;
  495. }
  496. foreach ($wrongQuestions as $qItem) {
  497. $kpName = trim((string)($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
  498. if ($kpName === '') {
  499. $kpName = '未标注知识点';
  500. }
  501. if (!isset($kpStats[$kpName])) {
  502. $kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
  503. }
  504. $kpStats[$kpName]['wrong']++;
  505. }
  506. $kpWrongStats = [];
  507. foreach ($kpStats as $kpName => $stat) {
  508. if (($stat['wrong'] ?? 0) <= 0) {
  509. continue;
  510. }
  511. $total = max(1, intval($stat['total'] ?? 0));
  512. $wrong = intval($stat['wrong'] ?? 0);
  513. $kpWrongStats[] = [
  514. 'kp_name' => $kpName,
  515. 'wrong' => $wrong,
  516. 'total' => $total,
  517. 'rate' => $wrong / $total,
  518. ];
  519. }
  520. usort($kpWrongStats, function ($a, $b) {
  521. if ($a['rate'] === $b['rate']) {
  522. return $b['wrong'] <=> $a['wrong'];
  523. }
  524. return $b['rate'] <=> $a['rate'];
  525. });
  526. @endphp
  527. @if(!empty($wrongQuestions))
  528. <div class="card">
  529. <div class="section-title">错题列表</div>
  530. @if(!empty($kpWrongStats))
  531. <div style="margin-bottom:8px; padding:8px; border:1px solid #e5e7eb; border-radius:6px; background:#f8fafc;">
  532. <div style="font-size:12px; font-weight:600; color:#111827; margin-bottom:6px;">知识点错误率</div>
  533. <div style="font-size:11px; color:#475569; line-height:1.7;">
  534. @foreach($kpWrongStats as $item)
  535. <span class="error-kp-tag {{ $item['rate'] > 0.5 ? 'high-risk' : '' }}">{{ $item['kp_name'] }}:{{ $item['wrong'] }}/{{ $item['total'] }}({{ number_format($item['rate'] * 100, 1) }}%)</span>
  536. @endforeach
  537. </div>
  538. </div>
  539. @endif
  540. @foreach($wrongQuestions as $q)
  541. @php
  542. // 【修复】从题目数据中获取学生答案、正确答案和判分结果
  543. $studentAnswer = $q['student_answer'] ?? $q['student_answer'] ?? null;
  544. $correctAnswer = $q['answer'] ?? $q['correct_answer'] ?? null;
  545. $isCorrect = $q['is_correct'] ?? null; // 1=正确,0=错误,null=未判
  546. // 如果未判分但有学生答案和正确答案,自动比较判断
  547. if ($isCorrect === null && !empty($studentAnswer) && !empty($correctAnswer)) {
  548. $isCorrect = (trim($studentAnswer) === trim($correctAnswer)) ? 1 : 0;
  549. }
  550. // 判分状态显示逻辑
  551. $showStatus = false;
  552. $statusText = '';
  553. $statusColor = '';
  554. if (!empty($studentAnswer)) {
  555. // 学生有答题,显示判分结果
  556. $showStatus = true;
  557. if ($isCorrect === 1) {
  558. $statusText = '正确';
  559. $statusColor = '#10b981';
  560. } elseif ($isCorrect === 0) {
  561. $statusText = '错误';
  562. $statusColor = '#ef4444';
  563. }
  564. }
  565. // 没答题的不显示状态
  566. $insight = $insightMap[$q['question_number']] ?? ($insightMap[$q['display_number'] ?? null] ?? []);
  567. // 【修复】得分显示:答错显示实际得分,答对显示满分
  568. $fullScore = $insight['full_score'] ?? ($q['score'] ?? null);
  569. if ($isCorrect === 1) {
  570. // 答对了,显示满分
  571. $score = $fullScore;
  572. } elseif ($isCorrect === 0) {
  573. // 答错了,显示实际得分(可能为0分或其他分数)
  574. $score = $q['score_obtained'] ?? 0;
  575. } else {
  576. // 未判分或未答题,不显示得分
  577. $score = null;
  578. }
  579. $analysisRaw = $insight['analysis']
  580. ?? $insight['thinking_process']
  581. ?? $insight['feedback']
  582. ?? $insight['suggestions']
  583. ?? $insight['reason']
  584. ?? ($insight['correct_solution'] ?? null);
  585. // 若有下一步建议,追加
  586. if (empty($analysisRaw) && !empty($insight['next_steps'])) {
  587. $analysisRaw = '后续建议:' . (is_array($insight['next_steps']) ? implode(';', $insight['next_steps']) : $insight['next_steps']);
  588. }
  589. $analysis = is_array($analysisRaw) ? json_encode($analysisRaw, JSON_UNESCAPED_UNICODE) : $analysisRaw;
  590. if ($analysis === null || $analysis === '') {
  591. $analysis = '暂无解题思路,待补充';
  592. }
  593. $formatSolutionLikeGrading = function ($text) {
  594. if (!is_string($text) || trim($text) === '') {
  595. return $text;
  596. }
  597. $normalized = preg_replace('/\s*;\s*步骤\s*(\d+)/u', ";\n步骤$1", $text);
  598. $normalized = preg_replace('/\s*。\s*步骤\s*(\d+)/u', "。\n步骤$1", $normalized);
  599. $normalized = preg_replace('/(?<!^)(步骤\s*\d+\s*[::])/u', "\n$1", $normalized);
  600. $normalized = preg_replace('/(?<!^)(第\s*\d+\s*步\s*[::]?)/u', "\n$1", $normalized);
  601. $normalized = preg_replace('/\n{3,}/u', "\n\n", $normalized);
  602. // 去掉每行左侧缩进空白,避免出现“左边空好几个字符”
  603. $normalized = preg_replace('/^[\h\x{3000}]+/mu', '', $normalized);
  604. return trim($normalized);
  605. };
  606. $stepsRaw = $insight['steps'] ?? $insight['solution_steps'] ?? $insight['analysis_steps'] ?? null;
  607. $steps = [];
  608. if (is_array($stepsRaw)) {
  609. $steps = $stepsRaw;
  610. } elseif (is_string($stepsRaw) && trim($stepsRaw) !== '') {
  611. $steps = preg_split('/[\r\n]+/', trim($stepsRaw));
  612. }
  613. $typeMap = ['choice' => '选择题', 'fill' => '填空题', 'answer' => '解答题'];
  614. $typeLabel = $typeMap[$q['question_type'] ?? ''] ?? ($q['question_type'] ?? '题型未标注');
  615. $questionText = is_string($q['question_text']) ? $q['question_text'] : json_encode($q['question_text'], JSON_UNESCAPED_UNICODE);
  616. $solution = $q['solution'] ?? null;
  617. $solution = $formatSolutionLikeGrading($solution);
  618. $analysis = $formatSolutionLikeGrading($analysis);
  619. @endphp
  620. <div class="question-card">
  621. <div style="display:flex; justify-content:space-between; align-items:center; gap:8px; margin-bottom:4px;">
  622. <div style="display:flex; align-items:center; gap:8px; font-weight:600;">
  623. <span class="tag">题号 {{ $q['display_number'] ?? $q['question_number'] }} · {{ $typeLabel }}</span>
  624. @php
  625. $kpName = $q['knowledge_point_name'] ?? $q['knowledge_point'] ?? null;
  626. if (!empty($kpName) && $kpName !== '-' && $kpName !== '未标注') {
  627. echo '<span class="tag" style="background: #eef2ff; color:#4338ca;">' . e($kpName) . '</span>';
  628. }
  629. @endphp
  630. @if($showStatus)
  631. <span class="tag" style="background: {{ $statusColor }}; color:#fff;">{{ $statusText }}</span>
  632. @endif
  633. </div>
  634. @if($score !== null && $fullScore !== null)
  635. <div class="muted">得分 {{ $score }} / {{ $fullScore }}</div>
  636. @endif
  637. </div>
  638. {{-- 【新增】学生答案显示(如果有) --}}
  639. @if(!empty($studentAnswer))
  640. <div class="question-block" style="background:#fef2f2; border-left:3px solid #ef4444;">
  641. <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:3px;">学生答案</div>
  642. <div class="math-content" style="font-size:12px; line-height:1.5; color:#374151;">
  643. {!! nl2br(e($studentAnswer)) !!}
  644. </div>
  645. </div>
  646. @endif
  647. <div class="math-content" style="margin-bottom:6px; font-size:12px;">{!! $questionText !!}</div>
  648. {{-- 【修复】正确答案显示 --}}
  649. @if(!empty($correctAnswer))
  650. <div class="question-block" style="background:#f0fdf4; border-left:3px solid #10b981;">
  651. <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:3px;">正确答案</div>
  652. <div class="math-content" style="font-size:12px; line-height:1.5; color:#374151;">
  653. {!! is_string($correctAnswer) ? $correctAnswer : json_encode($correctAnswer, JSON_UNESCAPED_UNICODE) !!}
  654. </div>
  655. </div>
  656. @endif
  657. {{-- 【修改】解题思路显示(优先显示solution,其次显示analysis) --}}
  658. @if(!empty($solution))
  659. <div class="question-block" style="margin-top:6px; background:#eff6ff; border-left:3px solid #3b82f6;">
  660. <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:4px;">解题思路</div>
  661. <div class="math-content solution-content" style="font-size:12px; color:#374151;">
  662. {!! nl2br(e(is_array($solution) ? json_encode($solution, JSON_UNESCAPED_UNICODE) : (string) $solution)) !!}
  663. </div>
  664. </div>
  665. @elseif(!empty($analysis) && $analysis !== '暂无解题思路记录')
  666. <div class="question-block" style="margin-top:6px; background:#eff6ff; border-left:3px solid #3b82f6;">
  667. <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:4px;">解题思路</div>
  668. <div class="math-content solution-content" style="font-size:12px; color:#374151;">{!! nl2br(e((string) $analysis)) !!}</div>
  669. </div>
  670. @endif
  671. {{-- 解题步骤(如果有) --}}
  672. @if(!empty($steps))
  673. <div style="margin-top:6px; font-size:12px;">
  674. <div style="font-weight:600; margin-bottom:3px;">解题步骤</div>
  675. <ol style="margin:0; padding-left:18px;">
  676. @foreach($steps as $s)
  677. <li style="margin-bottom:2px;">{!! nl2br(e(is_array($s) ? json_encode($s, JSON_UNESCAPED_UNICODE) : $s)) !!}</li>
  678. @endforeach
  679. </ol>
  680. </div>
  681. @endif
  682. </div>
  683. @endforeach
  684. </div>
  685. @endif
  686. </div> {{-- 闭合page div --}}
  687. <script src="/js/katex.min.js"></script>
  688. <script src="/js/auto-render.min.js"></script>
  689. <script>
  690. document.addEventListener('DOMContentLoaded', function() {
  691. try {
  692. renderMathInElement(document.body, {
  693. delimiters: [
  694. {left: "$$", right: "$$", display: true},
  695. {left: "$", right: "$", display: false},
  696. {left: "\\(", right: "\\)", display: false},
  697. {left: "\\[", right: "\\]", display: true}
  698. ],
  699. throwOnError: false,
  700. strict: false,
  701. trust: true
  702. });
  703. } catch (e) {}
  704. });
  705. </script>
  706. </body>
  707. </html>