pdf-report.blade.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. @php
  2. // 【参考试卷和判卷格式】学情报告以3开头 + 12位paper_id数字部分
  3. $rawPaperId = $paper['id'] ?? $paper['paper_id'] ?? 'unknown';
  4. // 从 paper_id 提取12位数字部分(格式: paper_xxxxxxxxxxxx)
  5. if (preg_match('/paper_(\d{12})/', $rawPaperId, $matches)) {
  6. $paperIdNum = $matches[1];
  7. } else {
  8. // 兼容旧格式,取数字部分或生成哈希
  9. $paperIdNum = preg_replace('/[^0-9]/', '', $rawPaperId);
  10. $paperIdNum = str_pad(substr($paperIdNum, 0, 12), 12, '0', STR_PAD_LEFT);
  11. }
  12. $reportCode = '3' . $paperIdNum; // 学情报告识别码:3 + 12位数字
  13. $averageMastery = isset($mastery['average']) ? number_format($mastery['average'] * 100, 1) . '%' : '无数据';
  14. // 【修复】从insights中获取AI分析结果(而不是从analysis_data)
  15. $questionAnalysis = $insights ?? [];
  16. @endphp
  17. <!DOCTYPE html>
  18. <html lang="zh-CN">
  19. <head>
  20. <meta charset="UTF-8">
  21. <title>学情报告 - {{ $paper['name'] ?? '试卷' }}</title>
  22. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
  23. <style>
  24. @page {
  25. size: A4;
  26. margin: 1.5cm 1.5cm 2cm 1.5cm;
  27. @top-left {
  28. content: "知了数学";
  29. font-size: 10px;
  30. color: #666;
  31. }
  32. @top-right {
  33. content: "{{ $reportCode }}";
  34. font-size: 10px;
  35. color: #666;
  36. }
  37. @bottom-left {
  38. content: "{{ $reportCode }}";
  39. font-size: 10px;
  40. color: #666;
  41. }
  42. @bottom-right {
  43. content: counter(page) "/" counter(pages);
  44. font-size: 10px;
  45. color: #666;
  46. }
  47. }
  48. * { box-sizing: border-box; }
  49. body {
  50. font-family: "SimSun", "Songti SC", serif;
  51. margin: 0;
  52. padding: 0;
  53. color: #000;
  54. background: #fff;
  55. font-size: 12px;
  56. line-height: 1.6;
  57. }
  58. h1, h2, h3 { margin: 0; color: #000; }
  59. .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); }
  60. .header { text-align: center; margin-bottom: 1rem; border-bottom: 2px solid #000; padding-bottom: 0.5rem; }
  61. .school-name { font-size: 24px; font-weight: bold; margin-bottom: 10px; }
  62. .paper-title { font-size: 20px; font-weight: bold; margin-bottom: 15px; }
  63. .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 12px; }
  64. .tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; color: #374151; background: #e5e7eb; }
  65. .section-title { font-size: 16px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
  66. .pill { padding: 4px 10px; border-radius: 999px; font-size: 12px; }
  67. .pill.green { background: #ecfdf3; color: #15803d; }
  68. .pill.amber { background: #fef3c7; color: #b45309; }
  69. .pill.red { background: #fef2f2; color: #b91c1c; }
  70. table { width: 100%; border-collapse: collapse; font-size: 13px; }
  71. th, td { padding: 8px 10px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
  72. th { background: #f3f4f6; color: #111827; }
  73. .muted { color: #6b7280; font-size: 12px; }
  74. .progress-wrap { background: #f3f4f6; border-radius: 999px; overflow: hidden; height: 10px; }
  75. .progress-bar { height: 100%; background: linear-gradient(90deg, #4f46e5, #10b981); }
  76. .recommend-card { border: 1px dashed #cbd5e1; border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; background: #f8fafc; }
  77. </style>
  78. </head>
  79. <body>
  80. <div class="page">
  81. <div class="header">
  82. <h1 class="paper-title">学情分析报告</h1>
  83. <div style="margin-top: 10px; font-size: 14px;">
  84. 试卷:{{ $paper['name'] ?? '-' }} | 学生:{{ $student['name'] ?? '-' }} | 年级:{{ $student['grade'] ?? '-' }}
  85. </div>
  86. <div style="margin-top: 6px; font-size: 14px;">
  87. 题目数:{{ is_array($questions ?? null) ? count($questions) : ($paper['total_questions'] ?? '-') }}
  88. </div>
  89. </div>
  90. <div class="card">
  91. <div class="section-title">知识点掌握度</div>
  92. @php
  93. // 【修复】过滤掉K-GENERAL等通用知识点,只显示有值的知识点
  94. $filteredMasteryItems = [];
  95. if (!empty($mastery['items'])) {
  96. foreach ($mastery['items'] as $item) {
  97. $kpCode = $item['kp_code'] ?? '';
  98. $masteryLevel = $item['mastery_level'] ?? 0;
  99. // 过滤条件:不是通用知识点,且掌握度>0
  100. if (!in_array($kpCode, ['K-GENERAL', 'GENERAL', 'DEFAULT']) && $masteryLevel > 0) {
  101. $filteredMasteryItems[] = $item;
  102. }
  103. }
  104. }
  105. @endphp
  106. @if(!empty($filteredMasteryItems))
  107. @foreach($filteredMasteryItems as $item)
  108. @php
  109. $pct = min(100, max(0, ($item['mastery_level'] ?? 0) * 100));
  110. $barColor = $pct >= 80 ? '#10b981' : ($pct >= 60 ? '#f59e0b' : '#ef4444');
  111. $delta = $item['mastery_change'] ?? null;
  112. // 只有当有变化值时才显示变化信息
  113. $changeText = '';
  114. if ($delta !== null && $delta !== '' && abs($delta) > 0.001) {
  115. $changeText = ($delta > 0 ? '↑ ' : '↓ ') . number_format(abs($delta) * 100, 1) . '%';
  116. }
  117. @endphp
  118. <div style="margin-bottom:12px; padding: 8px; border: 1px solid #e5e7eb; border-radius: 6px;">
  119. <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 6px;">
  120. <div style="font-weight: 600; font-size: 14px;">{{ $item['kp_name'] ?? $item['kp_code'] ?? '未知知识点' }}</div>
  121. <div style="font-weight: 600; color: {{ $barColor }}; font-size: 14px;">
  122. {{ number_format($pct, 1) }}%
  123. <span style="margin-left: 8px; color: #666; font-size: 12px;">{{ $changeText }}</span>
  124. </div>
  125. </div>
  126. <div class="progress-wrap" style="height: 12px;">
  127. <div class="progress-bar" style="width: {{ $pct }}%; background: {{ $barColor }};"></div>
  128. </div>
  129. </div>
  130. @endforeach
  131. @else
  132. <div class="muted">暂无有效掌握度数据(已过滤通用知识点和零值)</div>
  133. @endif
  134. </div>
  135. <div class="card">
  136. <div class="section-title">📊 知识点层级掌握度分析</div>
  137. @php
  138. $parentMasteryLevels = $parent_mastery_levels ?? [];
  139. $hasParentMastery = !empty($parentMasteryLevels);
  140. @endphp
  141. @if($hasParentMastery)
  142. @foreach($parentMasteryLevels as $parentData)
  143. @php
  144. $pct = $parentData['mastery_percentage'] ?? 0;
  145. $barColor = $pct >= 80 ? '#10b981' : ($pct >= 60 ? '#f59e0b' : '#ef4444');
  146. $parentName = $parentData['kp_name'] ?? $parentData['kp_code'];
  147. $children = $parentData['children'] ?? [];
  148. $level = $parentData['level'] ?? 1;
  149. $delta = $parentData['mastery_change'] ?? null;
  150. // 只有当有变化值时才显示变化信息
  151. $changeText = '';
  152. if ($delta !== null && $delta !== '' && abs($delta) > 0.001) {
  153. $changeText = ($delta > 0 ? '↑ ' : '↓ ') . number_format(abs($delta) * 100, 1) . '%';
  154. }
  155. @endphp
  156. <div style="margin-bottom: 14px; padding: 10px; border: 1px solid #e0f2fe; border-radius: 6px; background: #f8fafc;">
  157. <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 8px;">
  158. <div style="font-weight: 700; font-size: 15px; color: #0f172a;">
  159. 【第{{ $level }}级】{{ $parentName }}
  160. <span style="margin-left: 8px; font-size: 11px; color: #64748b; font-weight: 400;">
  161. ({{ $parentData['kp_code'] }})
  162. </span>
  163. </div>
  164. <div style="font-weight: 700; color: {{ $barColor }}; font-size: 15px;">
  165. {{ number_format($pct, 1) }}%
  166. @if(!empty($changeText))
  167. <span style="margin-left: 8px; color: #666; font-size: 12px;">{{ $changeText }}</span>
  168. @endif
  169. </div>
  170. </div>
  171. <div class="progress-wrap" style="height: 12px; margin-bottom: 8px;">
  172. <div class="progress-bar" style="width: {{ $pct }}%; background: {{ $barColor }};"></div>
  173. </div>
  174. @if(!empty($children))
  175. <div style="font-size: 12px; color: #475569;">
  176. <strong>包含子知识点({{ count($children) }}个):</strong>
  177. @foreach($children as $index => $child)
  178. @if($index < 8)
  179. <span style="display:inline-block; margin: 2px 4px; padding: 2px 6px; background: #e0f2fe; border-radius: 3px; font-size: 11px;">
  180. {{ $child['kp_name'] }}
  181. </span>
  182. @endif
  183. @endforeach
  184. @if(count($children) > 8)
  185. <span style="color: #64748b; font-style: italic;">...等{{ count($children) }}个知识点</span>
  186. @endif
  187. </div>
  188. @else
  189. <div style="font-size: 12px; color: #64748b;">
  190. <em>无直接子知识点或子知识点掌握度为0</em>
  191. </div>
  192. @endif
  193. </div>
  194. @endforeach
  195. <div style="margin-top: 12px; padding: 8px; background: #fefce8; border-left: 4px solid #eab308; border-radius: 4px;">
  196. <div style="font-size: 12px; color: #854d0e; line-height: 1.6;">
  197. <strong>学习建议:</strong>
  198. 建议重点关注掌握度较低的知识点,通过专项练习提升整体学习水平。优先练习掌握度低于60%的知识点。
  199. </div>
  200. </div>
  201. @else
  202. <div class="muted">暂无父节点掌握度数据</div>
  203. <div style="margin-top: 8px; font-size: 12px; color: #64748b;">
  204. 当前分析主要基于具体知识点掌握度。完整的层级掌握度分析需要在系统中建立完整的知识点层级关系。
  205. </div>
  206. @endif
  207. </div>
  208. <div class="card">
  209. <div class="section-title">题目列表</div>
  210. @php
  211. $insightMap = [];
  212. foreach (($question_insights ?? []) as $insight) {
  213. $no = $insight['question_number'] ?? $insight['question_id'] ?? null;
  214. if ($no !== null) {
  215. $insightMap[$no] = $insight;
  216. }
  217. }
  218. @endphp
  219. @foreach($questions as $q)
  220. @php
  221. // 【修复】从题目数据中获取学生答案、正确答案和判分结果
  222. $studentAnswer = $q['student_answer'] ?? $q['student_answer'] ?? null;
  223. $correctAnswer = $q['answer'] ?? $q['correct_answer'] ?? null;
  224. $isCorrect = $q['is_correct'] ?? null; // 1=正确,0=错误,null=未判
  225. // 如果未判分但有学生答案和正确答案,自动比较判断
  226. if ($isCorrect === null && !empty($studentAnswer) && !empty($correctAnswer)) {
  227. $isCorrect = (trim($studentAnswer) === trim($correctAnswer)) ? 1 : 0;
  228. }
  229. // 判分状态显示逻辑
  230. $showStatus = false;
  231. $statusText = '';
  232. $statusColor = '';
  233. if (!empty($studentAnswer)) {
  234. // 学生有答题,显示判分结果
  235. $showStatus = true;
  236. if ($isCorrect === 1) {
  237. $statusText = '正确';
  238. $statusColor = '#10b981';
  239. } elseif ($isCorrect === 0) {
  240. $statusText = '错误';
  241. $statusColor = '#ef4444';
  242. }
  243. }
  244. // 没答题的不显示状态
  245. $insight = $insightMap[$q['question_number']] ?? ($insightMap[$q['display_number'] ?? null] ?? []);
  246. // 【修复】得分显示:答错显示实际得分,答对显示满分
  247. $fullScore = $insight['full_score'] ?? ($q['score'] ?? null);
  248. if ($isCorrect === 1) {
  249. // 答对了,显示满分
  250. $score = $fullScore;
  251. } elseif ($isCorrect === 0) {
  252. // 答错了,显示实际得分(可能为0分或其他分数)
  253. $score = $q['score_obtained'] ?? 0;
  254. } else {
  255. // 未判分或未答题,不显示得分
  256. $score = null;
  257. }
  258. $analysisRaw = $insight['analysis']
  259. ?? $insight['thinking_process']
  260. ?? $insight['feedback']
  261. ?? $insight['suggestions']
  262. ?? $insight['reason']
  263. ?? ($insight['correct_solution'] ?? null);
  264. // 若有下一步建议,追加
  265. if (empty($analysisRaw) && !empty($insight['next_steps'])) {
  266. $analysisRaw = '后续建议:' . (is_array($insight['next_steps']) ? implode(';', $insight['next_steps']) : $insight['next_steps']);
  267. }
  268. $analysis = is_array($analysisRaw) ? json_encode($analysisRaw, JSON_UNESCAPED_UNICODE) : $analysisRaw;
  269. if ($analysis === null || $analysis === '') {
  270. $analysis = '暂无解题思路,待补充';
  271. }
  272. $stepsRaw = $insight['steps'] ?? $insight['solution_steps'] ?? $insight['analysis_steps'] ?? null;
  273. $steps = [];
  274. if (is_array($stepsRaw)) {
  275. $steps = $stepsRaw;
  276. } elseif (is_string($stepsRaw) && trim($stepsRaw) !== '') {
  277. $steps = preg_split('/[\r\n]+/', trim($stepsRaw));
  278. }
  279. $typeMap = ['choice' => '选择题', 'fill' => '填空题', 'answer' => '解答题'];
  280. $typeLabel = $typeMap[$q['question_type'] ?? ''] ?? ($q['question_type'] ?? '题型未标注');
  281. $questionText = is_string($q['question_text']) ? $q['question_text'] : json_encode($q['question_text'], JSON_UNESCAPED_UNICODE);
  282. $solution = $q['solution'] ?? null;
  283. @endphp
  284. <div style="border:1px solid #e5e7eb; border-radius:8px; padding:8px 12px; margin-bottom:8px; background:#fff; page-break-inside: avoid;">
  285. <div style="display:flex; justify-content:space-between; align-items:center; gap:8px; margin-bottom:4px;">
  286. <div style="display:flex; align-items:center; gap:8px; font-weight:600;">
  287. <span class="tag">题号 {{ $q['display_number'] ?? $q['question_number'] }}</span>
  288. @php
  289. $kpName = $q['knowledge_point_name'] ?? $q['knowledge_point'] ?? null;
  290. if (!empty($kpName) && $kpName !== '-' && $kpName !== '未标注') {
  291. echo '<span class="tag" style="background: #eef2ff; color:#4338ca;">' . e($kpName) . '</span>';
  292. }
  293. @endphp
  294. @if($showStatus)
  295. <span class="tag" style="background: {{ $statusColor }}; color:#fff;">{{ $statusText }}</span>
  296. @endif
  297. </div>
  298. @if($score !== null && $fullScore !== null)
  299. <div class="muted">得分 {{ $score }} / {{ $fullScore }}</div>
  300. @endif
  301. </div>
  302. {{-- 【新增】学生答案显示(如果有) --}}
  303. @if(!empty($studentAnswer))
  304. <div style="margin-bottom:6px; padding:6px; background:#fef2f2; border-left:3px solid #ef4444; border-radius:4px;">
  305. <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:3px;">学生答案</div>
  306. <div class="math-content" style="font-size:12px; line-height:1.5; color:#374151;">
  307. {!! nl2br(e($studentAnswer)) !!}
  308. </div>
  309. </div>
  310. @endif
  311. <div class="math-content" style="margin-bottom:6px; font-size:12px;">{!! $questionText !!}</div>
  312. <div class="muted" style="margin-bottom:6px; font-size:12px;">题型:{{ $typeLabel }}</div>
  313. {{-- 【修复】正确答案显示 --}}
  314. @if(!empty($correctAnswer))
  315. <div style="margin-bottom:6px; padding:6px; background:#f0fdf4; border-left:3px solid #10b981; border-radius:4px;">
  316. <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:3px;">正确答案</div>
  317. <div class="math-content" style="font-size:12px; line-height:1.5; color:#374151;">
  318. {!! nl2br(e($correctAnswer)) !!}
  319. </div>
  320. </div>
  321. @endif
  322. {{-- 【修改】解题思路显示(优先显示solution,其次显示analysis) --}}
  323. @if(!empty($solution))
  324. <div style="margin-top:6px; padding:6px; background:#eff6ff; border-left:3px solid #3b82f6; border-radius:4px;">
  325. <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:4px;">解题思路</div>
  326. <div class="math-content" style="font-size:12px; line-height:1.5; color:#374151;">
  327. {!! is_array($solution) ? json_encode($solution, JSON_UNESCAPED_UNICODE) : nl2br(e($solution)) !!}
  328. </div>
  329. </div>
  330. @elseif(!empty($analysis) && $analysis !== '暂无解题思路记录')
  331. <div style="margin-top:6px; padding:6px; background:#eff6ff; border-left:3px solid #3b82f6; border-radius:4px;">
  332. <div style="font-weight:600; font-size:12px; color:#111827; margin-bottom:4px;">解题思路</div>
  333. <div style="font-size:12px; line-height:1.5; color:#374151;">{!! nl2br(e($analysis)) !!}</div>
  334. </div>
  335. @endif
  336. {{-- 解题步骤(如果有) --}}
  337. @if(!empty($steps))
  338. <div style="margin-top:6px; font-size:12px;">
  339. <div style="font-weight:600; margin-bottom:3px;">解题步骤</div>
  340. <ol style="margin:0; padding-left:18px;">
  341. @foreach($steps as $s)
  342. <li style="margin-bottom:2px;">{!! nl2br(e(is_array($s) ? json_encode($s, JSON_UNESCAPED_UNICODE) : $s)) !!}</li>
  343. @endforeach
  344. </ol>
  345. </div>
  346. @endif
  347. </div>
  348. @endforeach
  349. </div>
  350. </div> {{-- 闭合page div --}}
  351. <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
  352. <script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
  353. <script>
  354. document.addEventListener('DOMContentLoaded', function() {
  355. try {
  356. renderMathInElement(document.body, {
  357. delimiters: [
  358. {left: "$$", right: "$$", display: true},
  359. {left: "$", right: "$", display: false},
  360. {left: "\\(", right: "\\)", display: false},
  361. {left: "\\[", right: "\\]", display: true}
  362. ],
  363. throwOnError: false,
  364. strict: false,
  365. trust: true
  366. });
  367. } catch (e) {}
  368. });
  369. </script>
  370. </body>
  371. </html>