pdf-report-v3.blade.php 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072
  1. @php
  2. $v3 = $v3 ?? [];
  3. $summary = $v3['summary'] ?? [];
  4. $radar = $v3['radar'] ?? [];
  5. $modules = $v3['modules'] ?? [];
  6. $paths = $v3['paths'] ?? ['keep' => [], 'boost' => [], 'key' => []];
  7. $rawPaperId = $paper['id'] ?? $paper['paper_id'] ?? 'unknown';
  8. preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
  9. $reportCode = $matches[1] ?? preg_replace('/[^0-9]/', '', (string) $rawPaperId);
  10. $generateDateTime = now()->format('Y年m月d日 H:i:s');
  11. $scoreObtained = $summary['score_obtained'] ?? null;
  12. $scoreTotal = $summary['score_total'] ?? null;
  13. $scoreRate = $summary['score_rate'] ?? null;
  14. $averageMastery = $summary['average_mastery'] ?? null;
  15. $examHitKpSet = array_fill_keys(array_map('strval', $exam_hit_kp_codes ?? []), true);
  16. $difficultySummary = $summary['difficulty'] ?? [];
  17. $comparisonSummary = $summary['comparison'] ?? [];
  18. $overallLabelDetail = $summary['overall_label_detail'] ?? [];
  19. $historySummary = $comparisonSummary['history'] ?? [];
  20. $peerSummary = $comparisonSummary['peers'] ?? [];
  21. $overallScore = isset($overallLabelDetail['composite_score']) ? (float) $overallLabelDetail['composite_score'] : null;
  22. $overallGrade = (string) ($overallLabelDetail['grade'] ?? 'D');
  23. $currentPart = (float) ($overallLabelDetail['current_score'] ?? 0);
  24. $historyPart = (float) ($overallLabelDetail['history_score'] ?? 0);
  25. $peerPart = (float) ($overallLabelDetail['peer_score'] ?? 0);
  26. $adjustPart = (float) ($overallLabelDetail['difficulty_adjust'] ?? 0);
  27. $compositeFormulaResult = (0.50 * $currentPart) + (0.25 * $historyPart) + (0.25 * $peerPart) + $adjustPart;
  28. $overallBadge = function (string $grade): array {
  29. return match ($grade) {
  30. 'S' => ['bg' => '#f5f3ff', 'border' => '#6d28d9', 'text' => '#6d28d9', 'class' => 'badge-s'],
  31. 'A' => ['bg' => '#ecfdf3', 'border' => '#22c55e', 'text' => '#166534', 'class' => 'badge-excellent'],
  32. 'B' => ['bg' => '#eff6ff', 'border' => '#3b82f6', 'text' => '#1d4ed8', 'class' => 'badge-good'],
  33. 'C' => ['bg' => '#fff7ed', 'border' => '#f59e0b', 'text' => '#b45309', 'class' => 'badge-average'],
  34. default => ['bg' => '#fef2f2', 'border' => '#ef4444', 'text' => '#b91c1c', 'class' => 'badge-weak'],
  35. };
  36. };
  37. $overallVisual = $overallBadge((string) $overallGrade);
  38. $trendVisual = function (string $trend): array {
  39. return match ($trend) {
  40. '显著提升' => ['icon' => '▲', 'color' => '#16a34a'],
  41. '小幅提升' => ['icon' => '↗', 'color' => '#0ea5e9'],
  42. '基本持平' => ['icon' => '•', 'color' => '#64748b'],
  43. '小幅回落' => ['icon' => '↘', 'color' => '#f59e0b'],
  44. '明显回落' => ['icon' => '▼', 'color' => '#ef4444'],
  45. default => ['icon' => '•', 'color' => '#64748b'],
  46. };
  47. };
  48. $statusColor = function (string $status): string {
  49. return match ($status) {
  50. '已掌握' => '#16a34a',
  51. '薄弱' => '#f59e0b',
  52. '未入门' => '#ef4444',
  53. default => '#64748b',
  54. };
  55. };
  56. $analysisWrongMap = [];
  57. foreach (($analysis_data['question_analysis'] ?? []) as $qa) {
  58. $qid = $qa['question_bank_id'] ?? $qa['question_id'] ?? null;
  59. if ($qid === null || $qid === '') {
  60. continue;
  61. }
  62. $rawCorrect = $qa['is_correct'] ?? null;
  63. $isWrongFromAnalysis = false;
  64. if (is_array($rawCorrect)) {
  65. $isWrongFromAnalysis = in_array(0, $rawCorrect, true);
  66. } elseif ($rawCorrect !== null) {
  67. $isWrongFromAnalysis = !boolval($rawCorrect);
  68. }
  69. if ($isWrongFromAnalysis) {
  70. $analysisWrongMap[(string) $qid] = true;
  71. }
  72. }
  73. $wrongQuestions = [];
  74. foreach (($questions ?? []) as $qItem) {
  75. $isCorrectProbe = $qItem['is_correct'] ?? null;
  76. $studentAnswerProbe = $qItem['student_answer'] ?? null;
  77. $correctAnswerProbe = $qItem['answer'] ?? ($qItem['correct_answer'] ?? null);
  78. if ($isCorrectProbe === null && !empty($studentAnswerProbe) && !empty($correctAnswerProbe)) {
  79. $isCorrectProbe = (trim((string) $studentAnswerProbe) === trim((string) $correctAnswerProbe)) ? 1 : 0;
  80. }
  81. $normalizedCorrect = $isCorrectProbe;
  82. if ($isCorrectProbe !== null) {
  83. $normalizedCorrect = is_bool($isCorrectProbe) ? ($isCorrectProbe ? 1 : 0) : intval($isCorrectProbe);
  84. }
  85. $qidProbe = (string) ($qItem['question_bank_id'] ?? $qItem['question_id'] ?? '');
  86. $isWrongByAnalysis = ($qidProbe !== '' && isset($analysisWrongMap[$qidProbe]));
  87. if ($normalizedCorrect === 0 || $isWrongByAnalysis) {
  88. $wrongQuestions[] = $qItem;
  89. }
  90. }
  91. $kpStats = [];
  92. foreach (($questions ?? []) as $qItem) {
  93. $kpName = trim((string) ($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
  94. $kpName = $kpName === '' ? '未标注知识点' : $kpName;
  95. if (!isset($kpStats[$kpName])) {
  96. $kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
  97. }
  98. $kpStats[$kpName]['total']++;
  99. }
  100. foreach ($wrongQuestions as $qItem) {
  101. $kpName = trim((string) ($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
  102. $kpName = $kpName === '' ? '未标注知识点' : $kpName;
  103. if (!isset($kpStats[$kpName])) {
  104. $kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
  105. }
  106. $kpStats[$kpName]['wrong']++;
  107. }
  108. $kpWrongStats = [];
  109. foreach ($kpStats as $kpName => $stat) {
  110. if (($stat['wrong'] ?? 0) <= 0) {
  111. continue;
  112. }
  113. $total = max(1, intval($stat['total'] ?? 0));
  114. $wrong = intval($stat['wrong'] ?? 0);
  115. $kpWrongStats[] = [
  116. 'kp_name' => $kpName,
  117. 'wrong' => $wrong,
  118. 'total' => $total,
  119. 'rate' => $wrong / $total,
  120. ];
  121. }
  122. usort($kpWrongStats, function ($a, $b) {
  123. if ($a['rate'] === $b['rate']) {
  124. return $b['wrong'] <=> $a['wrong'];
  125. }
  126. return $b['rate'] <=> $a['rate'];
  127. });
  128. $childMasteryStatus = function ($mastery): string {
  129. if ($mastery === null) {
  130. return '未学习';
  131. }
  132. $m = (float) $mastery * 100; // 与 math.client-pc 统一:0-100 阈值(85/60)
  133. if ($m >= 85) {
  134. return '已掌握';
  135. }
  136. if ($m >= 60) {
  137. return '薄弱';
  138. }
  139. return '未入门';
  140. };
  141. $childStatusColor = function ($status): string {
  142. return match ($status) {
  143. '已掌握' => '#52c41a',
  144. '薄弱' => '#faad14',
  145. '未入门' => '#f5222d',
  146. default => '#d9d9d9',
  147. };
  148. };
  149. $calcStats = function (array $points): array {
  150. $total = count($points);
  151. $learned = 0;
  152. $mastered = 0;
  153. $weak = 0;
  154. $beginner = 0;
  155. $unlearned = 0;
  156. foreach ($points as $p) {
  157. if (($p['mastery_level'] ?? null) !== null) {
  158. $learned++;
  159. }
  160. $status = (string) ($p['status'] ?? '未学习');
  161. if ($status === '已掌握') {
  162. $mastered++;
  163. } elseif ($status === '薄弱') {
  164. $weak++;
  165. } elseif ($status === '未入门') {
  166. $beginner++;
  167. } else {
  168. $unlearned++;
  169. }
  170. }
  171. return [
  172. 'total' => $total,
  173. 'learned' => $learned,
  174. 'mastered' => $mastered,
  175. 'weak' => $weak,
  176. 'beginner' => $beginner,
  177. 'unlearned' => $unlearned,
  178. ];
  179. };
  180. $clusterCards = [];
  181. $allStagePoints = [];
  182. foreach ($radar as $moduleItem) {
  183. $children = is_array($moduleItem['children'] ?? null) ? $moduleItem['children'] : [];
  184. $greatMap = [];
  185. foreach ($children as $child) {
  186. $greatKey = trim((string) ($child['great_grand_parent_name'] ?? ''));
  187. $greatKey = $greatKey !== '' ? $greatKey : '未分组';
  188. $grandKey = trim((string) ($child['grand_parent_name'] ?? ''));
  189. $grandKey = $grandKey !== '' ? $grandKey : '未分组';
  190. $parentName = trim((string) ($child['parent_name'] ?? ''));
  191. if ($parentName === '') {
  192. $parentCode = trim((string) ($child['parent_code'] ?? ''));
  193. $parentName = $parentCode !== '' ? $parentCode : '未分组';
  194. }
  195. $childMasteryLevel = isset($child['mastery_level']) ? (float) $child['mastery_level'] : null;
  196. $status = $childMasteryStatus($childMasteryLevel);
  197. if (!isset($greatMap[$greatKey])) {
  198. $greatMap[$greatKey] = [];
  199. }
  200. if (!isset($greatMap[$greatKey][$grandKey])) {
  201. $greatMap[$greatKey][$grandKey] = [];
  202. }
  203. if (!isset($greatMap[$greatKey][$grandKey][$parentName])) {
  204. $greatMap[$greatKey][$grandKey][$parentName] = [];
  205. }
  206. $greatMap[$greatKey][$grandKey][$parentName][] = [
  207. 'code' => (string) ($child['code'] ?? ''),
  208. 'name' => (string) ($child['name'] ?? '未命名知识点'),
  209. 'path' => (string) ($child['path'] ?? ''),
  210. 'mastery_level' => $childMasteryLevel,
  211. 'change' => isset($child['change']) ? (float) $child['change'] : null,
  212. 'status' => $status,
  213. 'color' => $childStatusColor($status),
  214. 'is_hit' => !empty($child['is_hit']),
  215. ];
  216. $allStagePoints[] = [
  217. 'code' => (string) ($child['code'] ?? ''),
  218. 'name' => (string) ($child['name'] ?? '未命名知识点'),
  219. 'mastery_level' => $childMasteryLevel,
  220. 'status' => $status,
  221. 'change' => isset($child['change']) ? (float) $child['change'] : null,
  222. 'is_hit' => !empty($child['is_hit']),
  223. ];
  224. }
  225. $greatGroups = [];
  226. foreach ($greatMap as $greatName => $grandMap) {
  227. $grandGroups = [];
  228. foreach ($grandMap as $grandName => $parentMap) {
  229. $parentGroups = [];
  230. foreach ($parentMap as $parentName => $points) {
  231. usort($points, function ($a, $b) {
  232. $am = $a['mastery_level'] ?? -1;
  233. $bm = $b['mastery_level'] ?? -1;
  234. if ($am === $bm) {
  235. return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
  236. }
  237. return $bm <=> $am;
  238. });
  239. $parentGroups[] = [
  240. 'parent_name' => $parentName,
  241. 'points' => $points,
  242. 'stats' => $calcStats($points),
  243. ];
  244. }
  245. // 子模块级过滤:整行没有任何掌握度数字则不显示
  246. $parentGroups = array_values(array_filter($parentGroups, function ($pg) {
  247. return (($pg['stats']['learned'] ?? 0) > 0);
  248. }));
  249. if (empty($parentGroups)) {
  250. continue;
  251. }
  252. usort($parentGroups, function ($a, $b) {
  253. $sa = $a['stats'];
  254. $sb = $b['stats'];
  255. return ($sb['learned'] <=> $sa['learned']) ?: ($sb['total'] <=> $sa['total']);
  256. });
  257. $allGrandPoints = [];
  258. foreach ($parentGroups as $pg) {
  259. $allGrandPoints = array_merge($allGrandPoints, $pg['points']);
  260. }
  261. $grandGroups[] = [
  262. 'grand_name' => $grandName,
  263. 'parent_groups' => $parentGroups,
  264. 'stats' => $calcStats($allGrandPoints),
  265. ];
  266. }
  267. // 大块级过滤:整块没有任何掌握度数字则不显示
  268. $grandGroups = array_values(array_filter($grandGroups, function ($gg) {
  269. return (($gg['stats']['learned'] ?? 0) > 0);
  270. }));
  271. if (empty($grandGroups)) {
  272. continue;
  273. }
  274. usort($grandGroups, function ($a, $b) {
  275. $sa = $a['stats'];
  276. $sb = $b['stats'];
  277. return ($sb['learned'] <=> $sa['learned']) ?: ($sb['total'] <=> $sa['total']);
  278. });
  279. $allGreatPoints = [];
  280. foreach ($grandGroups as $gg) {
  281. foreach ($gg['parent_groups'] as $pg) {
  282. $allGreatPoints = array_merge($allGreatPoints, $pg['points']);
  283. }
  284. }
  285. $greatGroups[] = [
  286. 'great_name' => $greatName,
  287. 'grand_groups' => $grandGroups,
  288. 'stats' => $calcStats($allGreatPoints),
  289. ];
  290. }
  291. usort($greatGroups, function ($a, $b) {
  292. $sa = $a['stats'];
  293. $sb = $b['stats'];
  294. return ($sb['learned'] <=> $sa['learned']) ?: ($sb['total'] <=> $sa['total']);
  295. });
  296. // 严格参考 math.client-pc:扁平化为“grand 层卡片”(展示大块)
  297. foreach ($greatGroups as $great) {
  298. foreach (($great['grand_groups'] ?? []) as $grand) {
  299. $gStats = $grand['stats'] ?? ['learned' => 0, 'total' => 0];
  300. $clusterCards[] = [
  301. 'module_name' => (string) ($moduleItem['name'] ?? '未分组'),
  302. 'great_name' => $great['great_name'] ?? '未分组',
  303. 'grand_name' => $grand['grand_name'] ?? '未分组',
  304. 'parent_groups' => $grand['parent_groups'] ?? [],
  305. 'stats' => $gStats,
  306. ];
  307. }
  308. }
  309. }
  310. usort($clusterCards, function ($a, $b) {
  311. $sa = $a['stats'] ?? ['learned' => 0, 'total' => 0];
  312. $sb = $b['stats'] ?? ['learned' => 0, 'total' => 0];
  313. return (($sb['learned'] ?? 0) <=> ($sa['learned'] ?? 0))
  314. ?: (($sb['total'] ?? 0) <=> ($sa['total'] ?? 0));
  315. });
  316. $kpStatsTotal = [
  317. 'total' => count($allStagePoints),
  318. 'mastered' => 0,
  319. 'weak' => 0,
  320. 'beginner' => 0,
  321. 'unlearned' => 0,
  322. ];
  323. foreach ($allStagePoints as $p) {
  324. $st = (string) ($p['status'] ?? '未学习');
  325. if ($st === '已掌握') {
  326. $kpStatsTotal['mastered']++;
  327. } elseif ($st === '薄弱') {
  328. $kpStatsTotal['weak']++;
  329. } elseif ($st === '未入门') {
  330. $kpStatsTotal['beginner']++;
  331. } else {
  332. $kpStatsTotal['unlearned']++;
  333. }
  334. }
  335. $moduleRowsWithStatus = array_values(array_filter($modules, function ($m) {
  336. $status = trim((string) ($m['status'] ?? ''));
  337. $masteryLevel = $m['mastery_level'] ?? null;
  338. $questionCount = (int) ($m['question_count'] ?? 0);
  339. if ($masteryLevel !== null) {
  340. return true;
  341. }
  342. return $questionCount > 0 && $status !== '' && ! in_array($status, ['暂无', '-', '未涉及', '未学习'], true);
  343. }));
  344. $pathTagByModuleName = [];
  345. foreach (['keep' => '保分不错', 'boost' => '需要加强', 'key' => '优先加强'] as $bucket => $tagName) {
  346. foreach (($paths[$bucket] ?? []) as $item) {
  347. $n = trim((string) ($item['name'] ?? ''));
  348. if ($n === '') {
  349. continue;
  350. }
  351. $pathTagByModuleName[$n] = $tagName;
  352. }
  353. }
  354. $impactedModules = array_values(array_filter($moduleRowsWithStatus, function ($m) {
  355. return ((int) ($m['question_count'] ?? 0)) > 0;
  356. }));
  357. $radarModuleMap = [];
  358. foreach ($radar as $moduleItem) {
  359. $code = (string) ($moduleItem['code'] ?? '');
  360. if ($code !== '') {
  361. $radarModuleMap[$code] = $moduleItem;
  362. }
  363. }
  364. $moduleImpactChangeMap = [];
  365. foreach ($radarModuleMap as $moduleCode => $moduleItem) {
  366. $hitChanges = [];
  367. foreach (($moduleItem['children'] ?? []) as $child) {
  368. if (empty($child['is_hit'])) {
  369. continue;
  370. }
  371. $change = $child['change'] ?? null;
  372. if ($change === null || ! is_numeric($change)) {
  373. continue;
  374. }
  375. $hitChanges[] = (float) $change;
  376. }
  377. if (empty($hitChanges)) {
  378. continue;
  379. }
  380. $moduleImpactChangeMap[$moduleCode] = array_sum($hitChanges) / count($hitChanges);
  381. }
  382. $questionTypeLabelMap = [
  383. 'choice' => '选择题',
  384. 'multiple_choice' => '选择题',
  385. 'single_choice' => '选择题',
  386. 'select' => '选择题',
  387. 'fill' => '填空题',
  388. 'blank' => '填空题',
  389. 'answer' => '解答题',
  390. 'solution' => '解答题',
  391. ];
  392. $kpQuestionTypeMap = [];
  393. foreach (($questions ?? []) as $qItem) {
  394. $kpCode = trim((string) ($qItem['knowledge_point'] ?? ''));
  395. if ($kpCode === '') {
  396. continue;
  397. }
  398. $rawType = strtolower(trim((string) ($qItem['question_type'] ?? '')));
  399. $typeLabel = $questionTypeLabelMap[$rawType] ?? ((string) ($qItem['question_type'] ?? '未知题型'));
  400. if ($typeLabel === '') {
  401. $typeLabel = '未知题型';
  402. }
  403. if (! isset($kpQuestionTypeMap[$kpCode])) {
  404. $kpQuestionTypeMap[$kpCode] = [];
  405. }
  406. $kpQuestionTypeMap[$kpCode][$typeLabel] = true;
  407. }
  408. $moduleKpSuggestions = [];
  409. foreach ($moduleRowsWithStatus as $m) {
  410. $moduleCode = (string) ($m['module_code'] ?? '');
  411. $moduleName = (string) ($m['module_name'] ?? '-');
  412. $moduleChildren = $radarModuleMap[$moduleCode]['children'] ?? [];
  413. if (! is_array($moduleChildren) || empty($moduleChildren)) {
  414. continue;
  415. }
  416. $started = array_values(array_filter($moduleChildren, function ($c) {
  417. return isset($c['mastery_level']) && $c['mastery_level'] !== null;
  418. }));
  419. usort($started, function ($a, $b) {
  420. $am = (float) ($a['mastery_level'] ?? 0);
  421. $bm = (float) ($b['mastery_level'] ?? 0);
  422. if ($am === $bm) {
  423. return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
  424. }
  425. return $am <=> $bm;
  426. });
  427. $weakest = null;
  428. if (! empty($started)) {
  429. $lowestStarted = $started[0];
  430. $lowestStartedLevel = isset($lowestStarted['mastery_level']) ? (float) $lowestStarted['mastery_level'] : null;
  431. if ($lowestStartedLevel !== null && $lowestStartedLevel < 0.85) {
  432. // 规则1:已开始学习中掌握度最低
  433. $weakest = $lowestStarted;
  434. } else {
  435. // 规则2:若已开始学习均达标(>=85%),取“最近的未学习”
  436. $unlearned = array_values(array_filter($moduleChildren, function ($c) {
  437. return !isset($c['mastery_level']) || $c['mastery_level'] === null;
  438. }));
  439. if (! empty($unlearned)) {
  440. $anchorParent = (string) ($lowestStarted['parent_name'] ?? '');
  441. $anchorGrand = (string) ($lowestStarted['grand_parent_name'] ?? '');
  442. usort($unlearned, function ($a, $b) use ($anchorParent, $anchorGrand) {
  443. $score = function ($node) use ($anchorParent, $anchorGrand) {
  444. $parent = (string) ($node['parent_name'] ?? '');
  445. $grand = (string) ($node['grand_parent_name'] ?? '');
  446. if ($anchorParent !== '' && $parent === $anchorParent) {
  447. return 0;
  448. }
  449. if ($anchorGrand !== '' && $grand === $anchorGrand) {
  450. return 1;
  451. }
  452. return 2;
  453. };
  454. $sa = $score($a);
  455. $sb = $score($b);
  456. if ($sa === $sb) {
  457. return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
  458. }
  459. return $sa <=> $sb;
  460. });
  461. $weakest = $unlearned[0];
  462. }
  463. }
  464. } else {
  465. // 没有已开始学习数据时,回退到模块内任一未学习点
  466. $unlearned = array_values(array_filter($moduleChildren, function ($c) {
  467. return !isset($c['mastery_level']) || $c['mastery_level'] === null;
  468. }));
  469. if (! empty($unlearned)) {
  470. usort($unlearned, fn ($a, $b) => strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? '')));
  471. $weakest = $unlearned[0];
  472. }
  473. }
  474. if (! is_array($weakest)) {
  475. $moduleKpSuggestions[] = [
  476. 'module_name' => $moduleName,
  477. 'path_tag' => $pathTagByModuleName[$moduleName] ?? '待观察',
  478. 'kp_name' => '',
  479. 'kp_code' => '',
  480. 'mastery_level' => null,
  481. 'status' => '当前模块暂无需额外关注知识点',
  482. 'question_types' => [],
  483. 'is_empty' => true,
  484. ];
  485. continue;
  486. }
  487. $kpName = (string) ($weakest['name'] ?? '');
  488. if ($kpName === '') {
  489. continue;
  490. }
  491. $kpCode = (string) ($weakest['code'] ?? '');
  492. $types = array_keys($kpQuestionTypeMap[$kpCode] ?? []);
  493. $moduleKpSuggestions[] = [
  494. 'module_name' => $moduleName,
  495. 'path_tag' => $pathTagByModuleName[$moduleName] ?? '待观察',
  496. 'kp_name' => $kpName,
  497. 'kp_code' => $kpCode,
  498. 'mastery_level' => $weakest['mastery_level'] ?? null,
  499. 'status' => $childMasteryStatus($weakest['mastery_level'] ?? null),
  500. 'question_types' => $types,
  501. 'is_empty' => false,
  502. ];
  503. }
  504. $moduleSuggestionByName = [];
  505. foreach ($moduleKpSuggestions as $sug) {
  506. $name = trim((string) ($sug['module_name'] ?? ''));
  507. if ($name !== '') {
  508. $moduleSuggestionByName[$name] = $sug;
  509. }
  510. }
  511. $kpChangeItems = [];
  512. foreach (($mastery['items'] ?? []) as $item) {
  513. $code = trim((string) ($item['kp_code'] ?? $item['code'] ?? ''));
  514. if ($code !== '' && ! empty($examHitKpSet) && ! isset($examHitKpSet[$code])) {
  515. continue;
  516. }
  517. $level = $item['mastery_level'] ?? null;
  518. if ($level === null || ! is_numeric($level)) {
  519. continue;
  520. }
  521. $change = $item['mastery_change'] ?? $item['change'] ?? 0.0;
  522. $kpChangeItems[] = [
  523. 'code' => $code,
  524. 'name' => (string) ($item['kp_name'] ?? $item['name'] ?? ($code !== '' ? $code : '-')),
  525. 'mastery_level' => (float) $level,
  526. 'change' => is_numeric($change) ? (float) $change : 0.0,
  527. 'status' => $childMasteryStatus((float) $level),
  528. 'is_hit' => true,
  529. ];
  530. }
  531. usort($kpChangeItems, function ($a, $b) {
  532. return abs((float) ($b['change'] ?? 0)) <=> abs((float) ($a['change'] ?? 0));
  533. });
  534. $kpPct = function (int $count, int $total): string {
  535. if ($total <= 0) {
  536. return '0.0%';
  537. }
  538. return number_format(($count * 100.0) / $total, 1) . '%';
  539. };
  540. @endphp
  541. <!DOCTYPE html>
  542. <html lang="zh-CN">
  543. <head>
  544. <meta charset="UTF-8">
  545. <title>学情分析报告</title>
  546. <link rel="stylesheet" href="/css/katex/katex.min.css">
  547. <style>
  548. @page {
  549. size: A4;
  550. margin: 2.2cm 2cm 2.3cm 2cm;
  551. @top-left { content: "知了数学·{{ $generateDateTime }}"; font-size: 13px; color: #666; }
  552. @top-center { content: "{{ $student['name'] ?? '-' }}"; font-size: 13px; color: #666; }
  553. @top-right {
  554. content: "{{ $reportCode }}";
  555. font-size: 19px;
  556. font-weight: 600;
  557. font-family: "Noto Sans", "Liberation Sans", "Nimbus Sans", sans-serif;
  558. color: #222;
  559. }
  560. @bottom-left { content: "{{ $reportCode }}"; font-size: 11px; color: #666; }
  561. @bottom-right { content: counter(page) "/" counter(pages); font-size: 13px; color: #666; }
  562. }
  563. * { box-sizing: border-box; }
  564. body { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; margin: 0; color: #0f172a; font-size: 13px; line-height: 1.65; }
  565. .page { page-break-after: auto; }
  566. .header { text-align: left; margin-bottom: 16px; }
  567. .paper-title { font-size: 30px; font-weight: 700; margin-bottom: 8px; color: #0b3a75; letter-spacing: 1px; }
  568. .section { margin-bottom: 14px; page-break-inside: auto; break-inside: auto; }
  569. .section-title { font-size: 20px; margin-bottom: 10px; font-weight: 700; color: #0b3a75; border-left: 5px solid #3b82f6; padding-left: 10px; line-height: 1.3; }
  570. .card { border: 1px solid #dbeafe; border-radius: 12px; padding: 14px; background: #f8fbff; position: relative; }
  571. .summary-list { margin: 0; padding-left: 18px; }
  572. .summary-list li { margin: 6px 0; font-size: 13px; }
  573. .overall-badge {
  574. position: absolute;
  575. right: 14px;
  576. top: 12px;
  577. border-radius: 12px;
  578. border: 0;
  579. padding: 9px 16px;
  580. min-width: 0;
  581. width: auto;
  582. text-align: center;
  583. position: absolute;
  584. overflow: hidden;
  585. display: inline-block;
  586. white-space: nowrap;
  587. background: transparent !important;
  588. }
  589. .overall-badge .level { font-size: 28px; font-weight: 800; line-height: 1.05; letter-spacing: 1px; }
  590. .overall-badge .score { font-size: 13px; margin-top: 3px; }
  591. .overall-badge.badge-s {
  592. border: 5px solid #6d28d9;
  593. border-radius: 14px;
  594. box-shadow: none;
  595. transform: rotate(-7deg);
  596. }
  597. .overall-badge.badge-s::before {
  598. content: "";
  599. position: absolute;
  600. inset: 4px;
  601. border: 2px dashed rgba(109, 40, 217, 0.65);
  602. border-radius: 10px;
  603. pointer-events: none;
  604. }
  605. .overall-badge.badge-s .level {
  606. letter-spacing: 2px;
  607. text-shadow: 0 1px 0 rgba(109, 40, 217, 0.24);
  608. }
  609. .overall-badge.badge-excellent {
  610. border: 3px double #16a34a;
  611. border-radius: 999px;
  612. box-shadow: none;
  613. }
  614. .overall-badge.badge-good {
  615. border: 2px solid #2563eb;
  616. border-radius: 10px;
  617. clip-path: polygon(6% 0, 94% 0, 100% 50%, 94% 100%, 6% 100%, 0 50%);
  618. box-shadow: none;
  619. }
  620. .overall-badge.badge-average {
  621. border: 2px dashed #d97706;
  622. border-radius: 14px;
  623. box-shadow: none;
  624. }
  625. .overall-badge.badge-weak {
  626. border-left: 3px solid #ef4444;
  627. border-right: 0;
  628. border-top: 0;
  629. border-bottom: 2px solid #ef4444;
  630. border-radius: 0 10px 10px 0;
  631. box-shadow: none;
  632. }
  633. .overall-meta { margin-top: 8px; font-size: 9px; color: #64748b; line-height: 1.6; white-space: nowrap; }
  634. .dot { display: inline-block; width: 10px; height: 10px; border-radius: 999px; margin-right: 4px; vertical-align: middle; }
  635. .cluster-toolbar {
  636. margin-bottom: 8px;
  637. font-size: 12px;
  638. color: #475569;
  639. }
  640. .cluster-legend { display: inline-block; margin-right: 12px; }
  641. .cluster-grid {
  642. display: grid;
  643. grid-template-columns: 1fr 1fr;
  644. gap: 10px;
  645. }
  646. .cluster-card {
  647. border: 1px solid #e2e8f0;
  648. border-radius: 10px;
  649. padding: 10px;
  650. background: #fff;
  651. }
  652. .cluster-card-title {
  653. font-size: 14px;
  654. font-weight: 700;
  655. color: #0f172a;
  656. margin-bottom: 8px;
  657. }
  658. .cluster-subgroup {
  659. border-left: 2px solid #e5e7eb;
  660. padding-left: 8px;
  661. margin-bottom: 8px;
  662. }
  663. .cluster-subgroup:last-child { margin-bottom: 0; }
  664. .cluster-subgroup-title {
  665. font-size: 12px;
  666. font-weight: 600;
  667. color: #334155;
  668. margin-bottom: 4px;
  669. }
  670. .cluster-points {
  671. display: flex;
  672. flex-wrap: wrap;
  673. gap: 4px;
  674. }
  675. .cluster-point {
  676. width: 10px;
  677. height: 10px;
  678. border-radius: 2px;
  679. display: inline-block;
  680. border: 1px solid rgba(148, 163, 184, 0.35);
  681. }
  682. .cluster-empty {
  683. font-size: 12px;
  684. color: #64748b;
  685. background: #f8fafc;
  686. border: 1px dashed #cbd5e1;
  687. border-radius: 8px;
  688. padding: 10px;
  689. }
  690. .kp-stats-grid {
  691. display: grid;
  692. grid-template-columns: repeat(5, 1fr);
  693. border: 1px solid #e5e7eb;
  694. border-radius: 10px;
  695. overflow: hidden;
  696. margin-bottom: 10px;
  697. }
  698. .kp-stat-item {
  699. padding: 8px 10px;
  700. border-right: 1px solid #e5e7eb;
  701. background: #fff;
  702. }
  703. .kp-stat-item:last-child { border-right: none; }
  704. .kp-stat-label { font-size: 11px; color: #64748b; }
  705. .kp-stat-value { font-size: 18px; font-weight: 700; color: #111827; line-height: 1.2; margin-top: 2px; }
  706. .kp-stat-rate { font-size: 11px; margin-left: 4px; font-weight: 600; }
  707. .kp-change-box { margin-bottom: 10px; border: 1px solid #e5e7eb; border-radius: 10px; background: #f8fafc; padding: 10px 12px; }
  708. .kp-change-list { margin: 4px 0 0 16px; padding: 0; }
  709. .kp-change-list li { margin: 2px 0; color: #334155; }
  710. .kp-burst-card { margin-top: 10px; border: 1px solid #dbeafe; border-radius: 12px; padding: 10px; background: #fff; }
  711. .kp-burst-title { font-size: 13px; font-weight: 700; margin-bottom: 6px; color: #0b3a75; }
  712. .kp-burst-meta { font-size: 12px; color: #334155; margin-top: 6px; line-height: 1.6; }
  713. .kp-burst-list { margin-top: 6px; font-size: 11px; color: #334155; line-height: 1.5; }
  714. .kp-burst-list span { display: inline-block; margin-right: 10px; margin-bottom: 3px; }
  715. table { width: 100%; border-collapse: collapse; font-size: 12px; background: #fff; }
  716. th, td { border: 1px solid #d0d7e2; padding: 8px 10px; text-align: left; vertical-align: top; }
  717. th { background: #f1f5f9; color: #1e293b; font-weight: 700; }
  718. .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; color: #fff; font-size: 11px; font-weight: 600; }
  719. .module-table th { background: #edf2ff; color: #0f172a; }
  720. .module-table th { text-align: center; }
  721. .module-table td { line-height: 1.45; }
  722. .module-table th,
  723. .module-table td { vertical-align: middle; }
  724. .module-table th:nth-child(8),
  725. .module-table td:nth-child(8) { vertical-align: top; }
  726. .module-table th:nth-child(1),
  727. .module-table td:nth-child(1) { text-align: center; }
  728. .module-table td:nth-child(2),
  729. .module-table td:nth-child(3),
  730. .module-table td:nth-child(5),
  731. .module-table td:nth-child(6),
  732. .module-table td:nth-child(7) { text-align: center; white-space: nowrap; }
  733. .module-table td:nth-child(8) { font-size: 11px; color: #334155; }
  734. .module-table tbody tr:nth-child(even) td { background: #fcfdff; }
  735. .module-name { font-weight: 600; color: #0f172a; }
  736. .impact-yes { color:#2563eb; font-weight:600; }
  737. .tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; color: #334155; background: #e5e7eb; }
  738. .error-kp-tag { display: inline-block; margin: 0 6px 6px 0; padding: 1px 7px; border-radius: 999px; font-size: 10px; color: #334155; background: #f8fafc; border: 1px solid #d1d5db; }
  739. .error-kp-tag.high-risk { color: #b91c1c; border-color: #fca5a5; background: #fff; font-weight: 600; }
  740. .muted { color: #6b7280; font-size: 12px; }
  741. </style>
  742. </head>
  743. <body>
  744. <div class="page">
  745. <div class="header">
  746. <h1 class="paper-title">学情分析报告</h1>
  747. </div>
  748. <div class="section">
  749. <div class="section-title">一、总体评估</div>
  750. <div class="card">
  751. <div class="overall-badge {{ $overallVisual['class'] ?? '' }}"
  752. style="border-color:{{ $overallVisual['border'] }}; color:{{ $overallVisual['text'] }};">
  753. <div class="level">{{ $overallGrade }}</div>
  754. </div>
  755. <ul class="summary-list">
  756. <li>本次诊断得分:
  757. @if($scoreObtained !== null && $scoreTotal !== null && $scoreTotal > 0)
  758. {{ rtrim(rtrim(number_format((float) $scoreObtained, 1), '0'), '.') }}/{{ rtrim(rtrim(number_format((float) $scoreTotal, 1), '0'), '.') }}
  759. @else
  760. 暂无得分数据
  761. @endif
  762. </li>
  763. <li>平均掌握度:{{ $averageMastery !== null ? number_format((float) $averageMastery * 100, 1) . '%' : '暂无掌握度' }}</li>
  764. <li>
  765. 难度匹配:
  766. @if(!empty($difficultySummary['target_label']) && isset($difficultySummary['actual_average_difficulty']))
  767. 目标 {{ $difficultySummary['target_label'] }}
  768. @if(!empty($difficultySummary['target_range']))
  769. ({{ number_format((float)($difficultySummary['target_range']['min'] ?? 0), 2) }}~{{ number_format((float)($difficultySummary['target_range']['max'] ?? 0), 2) }})
  770. @endif
  771. ,实际 {{ number_format((float)($difficultySummary['actual_average_difficulty'] ?? 0), 3) }}
  772. ({{ $difficultySummary['status'] ?? '暂无' }})
  773. @else
  774. 暂无难度匹配数据
  775. @endif
  776. </li>
  777. @if(!empty($difficultySummary['explain']))
  778. <li>难度说明:{{ $difficultySummary['explain'] }}</li>
  779. @endif
  780. <li>
  781. 与历史自己对比:
  782. @if(!empty($historySummary['is_first_exam']))
  783. {{ $historySummary['message'] ?? '这是你的第一次分析报告,先积累样本再看趋势。' }}
  784. @elseif(!empty($historySummary['low_baseline_guard']))
  785. {{ $historySummary['message'] ?? '历史基线偏低,建议看连续趋势。' }}
  786. @elseif(!empty($historySummary['has_data']))
  787. @php
  788. $trendText = (string)($historySummary['trend'] ?? '—');
  789. $tVisual = $trendVisual($trendText);
  790. @endphp
  791. 近几次均值对比:
  792. {{ number_format((float)($historySummary['baseline_score_rate'] ?? 0) * 100, 1) }}%,
  793. 本次{{ ($historySummary['delta_score_rate'] ?? 0) >= 0 ? '提升' : '回落' }}
  794. {{ number_format(abs((float)($historySummary['delta_score_rate'] ?? 0)) * 100, 1) }}%
  795. (<span style="color:{{ $tVisual['color'] ?? '#64748b' }}; font-weight:600;">{{ $tVisual['icon'] ?? '•' }} {{ $trendText }}</span>)
  796. @else
  797. {{ $historySummary['message'] ?? '历史样本不足' }}
  798. @endif
  799. </li>
  800. @if(!empty($peerSummary['show_line']))
  801. <li>
  802. 与同群体对比:
  803. {{ $peerSummary['message'] ?? '' }}
  804. (<span style="color:{{ $peerSummary['band_color'] ?? '#64748b' }}; font-weight:600;">{{ $peerSummary['band_icon'] ?? '•' }} {{ $peerSummary['band'] ?? '—' }}</span>)
  805. </li>
  806. @endif
  807. <li>
  808. 整体水平:
  809. @if($overallScore !== null)
  810. {{ number_format($overallScore, 1) }} 分({{ $overallGrade }})
  811. @else
  812. 待计算
  813. @endif
  814. </li>
  815. </ul>
  816. <div class="overall-meta">
  817. 规则:综合分 = 当前50% + 历史25% + 同群体25% + 难度校正,即:(({{ number_format($scoreRate !== null ? (float)$scoreRate * 100 : 0, 1) }}×70% + {{ number_format($averageMastery !== null ? (float)$averageMastery * 100 : 0, 1) }}×30%)×50%) + {{ number_format($historyPart, 1) }}×25% + {{ number_format($peerPart, 1) }}×25% + {{ number_format($adjustPart, 1) }} = {{ number_format($overallScore ?? $compositeFormulaResult, 1) }}
  818. </div>
  819. </div>
  820. </div>
  821. <div class="section">
  822. <div class="section-title">二、知识点掌握聚类视图</div>
  823. <div class="cluster-toolbar">
  824. <span class="cluster-legend"><i class="dot" style="background:#52c41a"></i>已掌握</span>
  825. <span class="cluster-legend"><i class="dot" style="background:#faad14"></i>薄弱</span>
  826. <span class="cluster-legend"><i class="dot" style="background:#f5222d"></i>未入门</span>
  827. <span class="cluster-legend"><i class="dot" style="background:#d9d9d9"></i>未学习</span>
  828. <span>按“模块 → 子模块 → 知识点”聚类展示</span>
  829. </div>
  830. <div class="cluster-grid">
  831. @foreach($clusterCards as $cluster)
  832. <div class="cluster-card">
  833. <div class="cluster-card-title">
  834. {{ $cluster['module_name'] }} / {{ $cluster['grand_name'] }}
  835. </div>
  836. @if(!empty($cluster['parent_groups']))
  837. @foreach($cluster['parent_groups'] as $parent)
  838. <div class="cluster-subgroup">
  839. <div class="cluster-subgroup-title">{{ $parent['parent_name'] }}</div>
  840. <div class="cluster-points">
  841. @foreach($parent['points'] as $point)
  842. <span class="cluster-point"
  843. style="background:{{ $point['color'] }}"
  844. title="{{ $point['name'] }} · {{ $point['status'] }}{{ $point['mastery_level'] !== null ? '(' . number_format((float)$point['mastery_level'] * 100, 1) . '%)' : '' }}{{ $point['path'] !== '' ? ' · ' . $point['path'] : '' }}"></span>
  845. @endforeach
  846. </div>
  847. </div>
  848. @endforeach
  849. @else
  850. <div class="cluster-empty">当前模块暂无可展示的子知识点。</div>
  851. @endif
  852. </div>
  853. @endforeach
  854. </div>
  855. <div style="margin-top:10px;">
  856. <div class="kp-stats-grid">
  857. <div class="kp-stat-item">
  858. <div class="kp-stat-label">总知识点数</div>
  859. <div class="kp-stat-value">{{ $kpStatsTotal['total'] }}</div>
  860. </div>
  861. <div class="kp-stat-item">
  862. <div class="kp-stat-label">已掌握</div>
  863. <div class="kp-stat-value" style="color:#52c41a;">
  864. {{ $kpStatsTotal['mastered'] }}<span class="kp-stat-rate" style="color:#52c41a;">({{ $kpPct($kpStatsTotal['mastered'], $kpStatsTotal['total']) }})</span>
  865. </div>
  866. </div>
  867. <div class="kp-stat-item">
  868. <div class="kp-stat-label">薄弱</div>
  869. <div class="kp-stat-value" style="color:#faad14;">
  870. {{ $kpStatsTotal['weak'] }}<span class="kp-stat-rate" style="color:#faad14;">({{ $kpPct($kpStatsTotal['weak'], $kpStatsTotal['total']) }})</span>
  871. </div>
  872. </div>
  873. <div class="kp-stat-item">
  874. <div class="kp-stat-label">未入门</div>
  875. <div class="kp-stat-value" style="color:#f5222d;">
  876. {{ $kpStatsTotal['beginner'] }}<span class="kp-stat-rate" style="color:#f5222d;">({{ $kpPct($kpStatsTotal['beginner'], $kpStatsTotal['total']) }})</span>
  877. </div>
  878. </div>
  879. <div class="kp-stat-item">
  880. <div class="kp-stat-label">未学习</div>
  881. <div class="kp-stat-value" style="color:#9ca3af;">
  882. {{ $kpStatsTotal['unlearned'] }}<span class="kp-stat-rate" style="color:#9ca3af;">({{ $kpPct($kpStatsTotal['unlearned'], $kpStatsTotal['total']) }})</span>
  883. </div>
  884. </div>
  885. </div>
  886. </div>
  887. </div>
  888. <div class="section">
  889. <div class="section-title">三、模块现状与提分路径(全局+本学案影响)</div>
  890. <div class="kp-change-box">
  891. <div style="font-size:12px;font-weight:700;color:#0f172a;">本学案知识点变化情况</div>
  892. @if(!empty($kpChangeItems))
  893. <ul class="kp-change-list">
  894. @foreach($kpChangeItems as $item)
  895. @php
  896. $delta = (float) ($item['change'] ?? 0);
  897. $deltaArrow = $delta > 0.0005 ? '↑' : ($delta < -0.0005 ? '↓' : '→');
  898. $deltaText = $deltaArrow === '→'
  899. ? '→'
  900. : ($deltaArrow . number_format(abs($delta) * 100, 1) . '%');
  901. $deltaColor = $delta > 0 ? '#16a34a' : ($delta < 0 ? '#dc2626' : '#64748b');
  902. $masteryText = isset($item['mastery_level']) && $item['mastery_level'] !== null
  903. ? number_format((float) $item['mastery_level'] * 100, 1) . '%'
  904. : '--';
  905. @endphp
  906. <li>
  907. {{ $item['name'] ?? '-' }}:
  908. <span style="color:{{ $deltaColor }};font-weight:600;">{{ $deltaText }}</span>
  909. (掌握度{{ $masteryText }},{{ $item['status'] ?? '未学习' }})
  910. </li>
  911. @endforeach
  912. </ul>
  913. @else
  914. <div class="muted" style="margin-top:4px;">
  915. @if(!empty($kpWrongStats))
  916. 暂无本学案命中知识点的掌握度数据,以下方知识点错误率作为本学案影响依据。
  917. @else
  918. 暂无可用的知识点变化数据
  919. @endif
  920. </div>
  921. @endif
  922. </div>
  923. @if(!empty($kpWrongStats))
  924. <div style="margin-bottom:8px; padding:8px 10px; border:1px solid #e5e7eb; border-radius:8px; background:#fff7ed;">
  925. <div style="font-size:12px; font-weight:700; color:#9a3412; margin-bottom:6px;">知识点错误率</div>
  926. <div style="font-size:12px; color:#475569; line-height:1.7;">
  927. @foreach($kpWrongStats as $item)
  928. <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>
  929. @endforeach
  930. </div>
  931. </div>
  932. @endif
  933. <div style="margin-bottom:8px; padding:8px 10px; border:1px solid #e5e7eb; border-radius:8px; background:#fff;">
  934. <div style="font-size:12px;color:#334155;">
  935. 本次学案影响模块:
  936. @if(!empty($impactedModules))
  937. @foreach($impactedModules as $idx => $im)
  938. @php $mName = $im['module_name'] ?? '-'; @endphp
  939. <span style="display:inline-block;padding:2px 8px;border-radius:999px;background:#eef2ff;color:#3730a3;margin-right:4px;">
  940. {{ $mName }}({{ $im['question_count'] ?? 0 }}题)
  941. </span>
  942. @endforeach
  943. @else
  944. <span class="muted">暂无命中模块</span>
  945. @endif
  946. </div>
  947. </div>
  948. <table class="module-table">
  949. <thead>
  950. <tr>
  951. <th style="width: 14%;">模块</th>
  952. <th style="width: 10%; white-space: nowrap;">本次影响</th>
  953. <th style="width: 18%;">掌握度<br>(学案影响)</th>
  954. <th style="width: 12%;">掌握状态</th>
  955. <th style="width: 9%;">题目数</th>
  956. <th style="width: 11%;">得分</th>
  957. <th style="width: 11%;">路径建议</th>
  958. <th style="width: 25%;">关注知识点</th>
  959. </tr>
  960. </thead>
  961. <tbody>
  962. @forelse($moduleRowsWithStatus as $m)
  963. @php
  964. $status = (string) ($m['status'] ?? '暂无');
  965. $color = $statusColor($status);
  966. $rate = $m['exam_score_rate'] ?? null;
  967. $qCount = (int) ($m['question_count'] ?? 0);
  968. $isImpacted = $qCount > 0;
  969. $pathTag = $pathTagByModuleName[(string) ($m['module_name'] ?? '')] ?? '待观察';
  970. $pathColor = match ($pathTag) {
  971. '优先加强' => '#ef4444',
  972. '需要加强' => '#f59e0b',
  973. '保分不错' => '#16a34a',
  974. default => '#64748b',
  975. };
  976. $moduleCode = (string) ($m['module_code'] ?? '');
  977. $impactDelta = $moduleImpactChangeMap[$moduleCode] ?? null;
  978. $impactArrow = $impactDelta === null
  979. ? ''
  980. : ($impactDelta > 0.0005 ? '↑' : ($impactDelta < -0.0005 ? '↓' : '→'));
  981. $impactColor = $impactDelta === null
  982. ? '#64748b'
  983. : ($impactDelta > 0.0005 ? '#16a34a' : ($impactDelta < -0.0005 ? '#dc2626' : '#64748b'));
  984. $impactText = $impactDelta === null ? '' : number_format(abs($impactDelta) * 100, 1) . '%';
  985. $impactSuffix = $impactArrow === '→' ? '' : $impactText;
  986. $moduleName = (string) ($m['module_name'] ?? '');
  987. $focus = $moduleSuggestionByName[$moduleName] ?? null;
  988. $focusText = '-';
  989. if (is_array($focus)) {
  990. if (!empty($focus['is_empty'])) {
  991. $focusText = (string) ($focus['status'] ?? '当前模块暂无需额外关注知识点');
  992. } else {
  993. $focusName = (string) ($focus['kp_name'] ?? '');
  994. $focusTypes = !empty($focus['question_types']) ? implode('、', $focus['question_types']) : '';
  995. $focusMastery = isset($focus['mastery_level']) && $focus['mastery_level'] !== null
  996. ? number_format((float) $focus['mastery_level'] * 100, 1) . '%'
  997. : '--';
  998. $focusSuffix = $focusTypes !== '' ? (',' . $focusTypes) : '';
  999. $focusText = $focusName !== ''
  1000. ? ($focusName . '(' . $focusMastery . $focusSuffix . ')')
  1001. : '当前模块暂无需额外关注知识点';
  1002. }
  1003. }
  1004. @endphp
  1005. <tr>
  1006. <td><span class="module-name">{{ $m['module_name'] ?? '-' }}</span></td>
  1007. <td>
  1008. @if($isImpacted)
  1009. <span class="impact-yes">是</span>
  1010. @else
  1011. <span class="muted">否</span>
  1012. @endif
  1013. </td>
  1014. <td>
  1015. {{ isset($m['mastery_level']) && $m['mastery_level'] !== null ? number_format((float) $m['mastery_level'] * 100, 1) . '%' : '-' }}
  1016. @if($impactArrow !== '')
  1017. <span style="margin-left:4px;color:{{ $impactColor }};font-weight:700;">
  1018. {{ $impactArrow }}{{ $impactSuffix }}
  1019. </span>
  1020. @endif
  1021. </td>
  1022. <td><span class="badge" style="background:{{ $color }}">{{ $status }}</span></td>
  1023. <td>{{ $m['question_count'] ?? 0 }}</td>
  1024. <td>{{ $rate !== null ? number_format((float) $rate * 100, 1) . '%' : '-' }}</td>
  1025. <td><span style="color:{{ $pathColor }}; font-weight:700;">{{ $pathTag }}</span></td>
  1026. <td>{{ $focusText }}</td>
  1027. </tr>
  1028. @empty
  1029. <tr>
  1030. <td colspan="8" class="muted">暂无掌握状态数据</td>
  1031. </tr>
  1032. @endforelse
  1033. </tbody>
  1034. </table>
  1035. </div>
  1036. </div>
  1037. <script src="/js/katex.min.js"></script>
  1038. <script src="/js/auto-render.min.js"></script>
  1039. <script>
  1040. document.addEventListener('DOMContentLoaded', function() {
  1041. try {
  1042. renderMathInElement(document.body, {
  1043. delimiters: [
  1044. {left: "$$", right: "$$", display: true},
  1045. {left: "$", right: "$", display: false},
  1046. {left: "\\(", right: "\\)", display: false},
  1047. {left: "\\[", right: "\\]", display: true}
  1048. ],
  1049. throwOnError: false,
  1050. strict: false,
  1051. trust: true
  1052. });
  1053. } catch (e) {}
  1054. });
  1055. </script>
  1056. </body>
  1057. </html>