| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285 |
- @php
- $v3 = $v3 ?? [];
- $summary = $v3['summary'] ?? [];
- $radar = $v3['radar'] ?? [];
- $modules = $v3['modules'] ?? [];
- $paths = $v3['paths'] ?? ['keep' => [], 'boost' => [], 'key' => []];
- $rawPaperId = $paper['id'] ?? $paper['paper_id'] ?? 'unknown';
- preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
- $reportCode = $matches[1] ?? preg_replace('/[^0-9]/', '', (string) $rawPaperId);
- $generateDateTime = now()->format('Y年m月d日 H:i:s');
- $scoreObtained = $summary['score_obtained'] ?? null;
- $scoreTotal = $summary['score_total'] ?? null;
- $scoreRate = $summary['score_rate'] ?? null;
- $averageMastery = $summary['average_mastery'] ?? null;
- $examHitKpSet = array_fill_keys(array_map('strval', $exam_hit_kp_codes ?? []), true);
- $difficultySummary = $summary['difficulty'] ?? [];
- $comparisonSummary = $summary['comparison'] ?? [];
- $overallLabelDetail = $summary['overall_label_detail'] ?? [];
- $historySummary = $comparisonSummary['history'] ?? [];
- $peerSummary = $comparisonSummary['peers'] ?? [];
- $overallScore = isset($overallLabelDetail['composite_score']) ? (float) $overallLabelDetail['composite_score'] : null;
- $overallGrade = (string) ($overallLabelDetail['grade'] ?? 'D');
- $currentPart = (float) ($overallLabelDetail['current_score'] ?? 0);
- $historyPart = (float) ($overallLabelDetail['history_score'] ?? 0);
- $peerPart = (float) ($overallLabelDetail['peer_score'] ?? 0);
- $adjustPart = (float) ($overallLabelDetail['difficulty_adjust'] ?? 0);
- $compositeFormulaResult = (0.50 * $currentPart) + (0.25 * $historyPart) + (0.25 * $peerPart) + $adjustPart;
- $overallBadge = function (string $grade): array {
- return match ($grade) {
- 'S' => ['bg' => '#f5f3ff', 'border' => '#6d28d9', 'text' => '#6d28d9', 'class' => 'badge-s'],
- 'A' => ['bg' => '#ecfdf3', 'border' => '#22c55e', 'text' => '#166534', 'class' => 'badge-excellent'],
- 'B' => ['bg' => '#eff6ff', 'border' => '#3b82f6', 'text' => '#1d4ed8', 'class' => 'badge-good'],
- 'C' => ['bg' => '#fff7ed', 'border' => '#f59e0b', 'text' => '#b45309', 'class' => 'badge-average'],
- default => ['bg' => '#fef2f2', 'border' => '#ef4444', 'text' => '#b91c1c', 'class' => 'badge-weak'],
- };
- };
- $overallVisual = $overallBadge((string) $overallGrade);
- $trendVisual = function (string $trend): array {
- return match ($trend) {
- '显著提升' => ['icon' => '▲', 'color' => '#16a34a'],
- '小幅提升' => ['icon' => '↗', 'color' => '#0ea5e9'],
- '基本持平' => ['icon' => '•', 'color' => '#64748b'],
- '小幅回落' => ['icon' => '↘', 'color' => '#f59e0b'],
- '明显回落' => ['icon' => '▼', 'color' => '#ef4444'],
- default => ['icon' => '•', 'color' => '#64748b'],
- };
- };
- $statusColor = function (string $status): string {
- return match ($status) {
- '已掌握' => '#16a34a',
- '薄弱' => '#f59e0b',
- '未入门' => '#ef4444',
- default => '#64748b',
- };
- };
- $analysisWrongMap = [];
- foreach (($analysis_data['question_analysis'] ?? []) as $qa) {
- $qid = $qa['question_bank_id'] ?? $qa['question_id'] ?? null;
- if ($qid === null || $qid === '') {
- continue;
- }
- $rawCorrect = $qa['is_correct'] ?? null;
- $isWrongFromAnalysis = false;
- if (is_array($rawCorrect)) {
- $isWrongFromAnalysis = in_array(0, $rawCorrect, true);
- } elseif ($rawCorrect !== null) {
- $isWrongFromAnalysis = !boolval($rawCorrect);
- }
- if ($isWrongFromAnalysis) {
- $analysisWrongMap[(string) $qid] = true;
- }
- }
- $wrongQuestions = [];
- foreach (($questions ?? []) as $qItem) {
- $isCorrectProbe = $qItem['is_correct'] ?? null;
- $studentAnswerProbe = $qItem['student_answer'] ?? null;
- $correctAnswerProbe = $qItem['answer'] ?? ($qItem['correct_answer'] ?? null);
- if ($isCorrectProbe === null && !empty($studentAnswerProbe) && !empty($correctAnswerProbe)) {
- $isCorrectProbe = (trim((string) $studentAnswerProbe) === trim((string) $correctAnswerProbe)) ? 1 : 0;
- }
- $normalizedCorrect = $isCorrectProbe;
- if ($isCorrectProbe !== null) {
- $normalizedCorrect = is_bool($isCorrectProbe) ? ($isCorrectProbe ? 1 : 0) : intval($isCorrectProbe);
- }
- $qidProbe = (string) ($qItem['question_bank_id'] ?? $qItem['question_id'] ?? '');
- $isWrongByAnalysis = ($qidProbe !== '' && isset($analysisWrongMap[$qidProbe]));
- if ($normalizedCorrect === 0 || $isWrongByAnalysis) {
- $wrongQuestions[] = $qItem;
- }
- }
- $kpStats = [];
- foreach (($questions ?? []) as $qItem) {
- $kpName = trim((string) ($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
- $kpName = $kpName === '' ? '未标注知识点' : $kpName;
- if (!isset($kpStats[$kpName])) {
- $kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
- }
- $kpStats[$kpName]['total']++;
- }
- foreach ($wrongQuestions as $qItem) {
- $kpName = trim((string) ($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? '未标注知识点'));
- $kpName = $kpName === '' ? '未标注知识点' : $kpName;
- if (!isset($kpStats[$kpName])) {
- $kpStats[$kpName] = ['total' => 0, 'wrong' => 0];
- }
- $kpStats[$kpName]['wrong']++;
- }
- $kpWrongStats = [];
- foreach ($kpStats as $kpName => $stat) {
- if (($stat['wrong'] ?? 0) <= 0) {
- continue;
- }
- $total = max(1, intval($stat['total'] ?? 0));
- $wrong = intval($stat['wrong'] ?? 0);
- $kpWrongStats[] = [
- 'kp_name' => $kpName,
- 'wrong' => $wrong,
- 'total' => $total,
- 'rate' => $wrong / $total,
- ];
- }
- usort($kpWrongStats, function ($a, $b) {
- if ($a['rate'] === $b['rate']) {
- return $b['wrong'] <=> $a['wrong'];
- }
- return $b['rate'] <=> $a['rate'];
- });
- $pcMasteryPercent = function ($mastery): ?int {
- if ($mastery === null) {
- return null;
- }
- // 与 PC 保持一致:API 先保留 2 位小数,前端再 Math.round 到 0-100。
- return (int) round(round((float) $mastery, 2) * 100);
- };
- $formatMasteryPct = function ($mastery) use ($pcMasteryPercent): string {
- $percent = $pcMasteryPercent($mastery);
- return $percent === null ? '-' : ($percent . '%');
- };
- $childMasteryStatus = function ($mastery) use ($pcMasteryPercent): string {
- $m = $pcMasteryPercent($mastery);
- if ($m === null) {
- return '未学习';
- }
- if ($m >= 85) {
- return '已掌握';
- }
- if ($m >= 60) {
- return '薄弱';
- }
- return '未入门';
- };
- $childStatusColor = function ($status): string {
- return match ($status) {
- '已掌握' => '#52c41a',
- '薄弱' => '#faad14',
- '未入门' => '#f5222d',
- default => '#d9d9d9',
- };
- };
- $calcStats = function (array $points): array {
- $total = count($points);
- $learned = 0;
- $mastered = 0;
- $weak = 0;
- $beginner = 0;
- $unlearned = 0;
- $hit = 0;
- foreach ($points as $p) {
- if (($p['mastery_level'] ?? null) !== null) {
- $learned++;
- }
- if (! empty($p['is_hit'])) {
- $hit++;
- }
- $status = (string) ($p['status'] ?? '未学习');
- if ($status === '已掌握') {
- $mastered++;
- } elseif ($status === '薄弱') {
- $weak++;
- } elseif ($status === '未入门') {
- $beginner++;
- } else {
- $unlearned++;
- }
- }
- return [
- 'total' => $total,
- 'learned' => $learned,
- 'mastered' => $mastered,
- 'weak' => $weak,
- 'beginner' => $beginner,
- 'unlearned' => $unlearned,
- 'hit' => $hit,
- ];
- };
- $clusterCards = [];
- $allStagePoints = [];
- foreach ($radar as $moduleItem) {
- $children = is_array($moduleItem['children'] ?? null) ? $moduleItem['children'] : [];
- $greatMap = [];
- foreach ($children as $child) {
- $greatKey = trim((string) ($child['great_grand_parent_name'] ?? ''));
- $greatKey = $greatKey !== '' ? $greatKey : '未分组';
- $grandKey = trim((string) ($child['grand_parent_name'] ?? ''));
- $grandKey = $grandKey !== '' ? $grandKey : '未分组';
- $parentName = trim((string) ($child['parent_name'] ?? ''));
- if ($parentName === '') {
- $parentCode = trim((string) ($child['parent_code'] ?? ''));
- $parentName = $parentCode !== '' ? $parentCode : '未分组';
- }
- $childMasteryLevel = isset($child['mastery_level']) ? (float) $child['mastery_level'] : null;
- $status = $childMasteryStatus($childMasteryLevel);
- if (!isset($greatMap[$greatKey])) {
- $greatMap[$greatKey] = [];
- }
- if (!isset($greatMap[$greatKey][$grandKey])) {
- $greatMap[$greatKey][$grandKey] = [];
- }
- if (!isset($greatMap[$greatKey][$grandKey][$parentName])) {
- $greatMap[$greatKey][$grandKey][$parentName] = [];
- }
- $childCode = (string) ($child['code'] ?? '');
- $childParentCode = (string) ($child['parent_code'] ?? '');
- $isHit = !empty($child['is_hit'])
- || ($childCode !== '' && isset($examHitKpSet[$childCode]))
- || ($childParentCode !== '' && isset($examHitKpSet[$childParentCode]));
- $greatMap[$greatKey][$grandKey][$parentName][] = [
- 'code' => $childCode,
- 'name' => (string) ($child['name'] ?? '未命名知识点'),
- 'parent_code' => $childParentCode,
- 'path' => (string) ($child['path'] ?? ''),
- 'mastery_level' => $childMasteryLevel,
- 'change' => isset($child['change']) ? (float) $child['change'] : null,
- 'status' => $status,
- 'color' => $childStatusColor($status),
- 'is_hit' => $isHit,
- ];
- $allStagePoints[] = [
- 'code' => $childCode,
- 'name' => (string) ($child['name'] ?? '未命名知识点'),
- 'parent_code' => $childParentCode,
- 'mastery_level' => $childMasteryLevel,
- 'status' => $status,
- 'change' => isset($child['change']) ? (float) $child['change'] : null,
- 'is_hit' => $isHit,
- ];
- }
- $greatGroups = [];
- foreach ($greatMap as $greatName => $grandMap) {
- $grandGroups = [];
- foreach ($grandMap as $grandName => $parentMap) {
- $parentGroups = [];
- foreach ($parentMap as $parentName => $points) {
- usort($points, function ($a, $b) {
- $am = $a['mastery_level'] ?? -1;
- $bm = $b['mastery_level'] ?? -1;
- if ($am === $bm) {
- return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
- }
- return $bm <=> $am;
- });
- $parentGroups[] = [
- 'parent_name' => $parentName,
- 'points' => $points,
- 'stats' => $calcStats($points),
- ];
- }
- // 子模块级过滤:整行没有任何掌握度数字则不显示
- $parentGroups = array_values(array_filter($parentGroups, function ($pg) {
- return (($pg['stats']['learned'] ?? 0) > 0) || (($pg['stats']['hit'] ?? 0) > 0);
- }));
- if (empty($parentGroups)) {
- continue;
- }
- usort($parentGroups, function ($a, $b) {
- $sa = $a['stats'];
- $sb = $b['stats'];
- return ($sb['learned'] <=> $sa['learned']) ?: ($sb['total'] <=> $sa['total']);
- });
- $allGrandPoints = [];
- foreach ($parentGroups as $pg) {
- $allGrandPoints = array_merge($allGrandPoints, $pg['points']);
- }
- $grandGroups[] = [
- 'grand_name' => $grandName,
- 'parent_groups' => $parentGroups,
- 'stats' => $calcStats($allGrandPoints),
- ];
- }
- // 大块级过滤:整块没有任何掌握度数字则不显示
- $grandGroups = array_values(array_filter($grandGroups, function ($gg) {
- return (($gg['stats']['learned'] ?? 0) > 0) || (($gg['stats']['hit'] ?? 0) > 0);
- }));
- if (empty($grandGroups)) {
- continue;
- }
- usort($grandGroups, function ($a, $b) {
- $sa = $a['stats'];
- $sb = $b['stats'];
- return ($sb['learned'] <=> $sa['learned']) ?: ($sb['total'] <=> $sa['total']);
- });
- $allGreatPoints = [];
- foreach ($grandGroups as $gg) {
- foreach ($gg['parent_groups'] as $pg) {
- $allGreatPoints = array_merge($allGreatPoints, $pg['points']);
- }
- }
- $greatGroups[] = [
- 'great_name' => $greatName,
- 'grand_groups' => $grandGroups,
- 'stats' => $calcStats($allGreatPoints),
- ];
- }
- usort($greatGroups, function ($a, $b) {
- $sa = $a['stats'];
- $sb = $b['stats'];
- return ($sb['learned'] <=> $sa['learned']) ?: ($sb['total'] <=> $sa['total']);
- });
- // 严格参考 math.client-pc:扁平化为“grand 层卡片”(展示大块)
- foreach ($greatGroups as $great) {
- foreach (($great['grand_groups'] ?? []) as $grand) {
- $gStats = $grand['stats'] ?? ['learned' => 0, 'total' => 0];
- $clusterCards[] = [
- 'module_name' => (string) ($moduleItem['name'] ?? '未分组'),
- 'great_name' => $great['great_name'] ?? '未分组',
- 'grand_name' => $grand['grand_name'] ?? '未分组',
- 'parent_groups' => $grand['parent_groups'] ?? [],
- 'stats' => $gStats,
- ];
- }
- }
- }
- usort($clusterCards, function ($a, $b) {
- $sa = $a['stats'] ?? ['learned' => 0, 'total' => 0];
- $sb = $b['stats'] ?? ['learned' => 0, 'total' => 0];
- return (($sb['learned'] ?? 0) <=> ($sa['learned'] ?? 0))
- ?: (($sb['total'] ?? 0) <=> ($sa['total'] ?? 0));
- });
- $kpStatsTotal = [
- 'total' => count($allStagePoints),
- 'mastered' => 0,
- 'weak' => 0,
- 'beginner' => 0,
- 'unlearned' => 0,
- ];
- foreach ($allStagePoints as $p) {
- $st = (string) ($p['status'] ?? '未学习');
- if ($st === '已掌握') {
- $kpStatsTotal['mastered']++;
- } elseif ($st === '薄弱') {
- $kpStatsTotal['weak']++;
- } elseif ($st === '未入门') {
- $kpStatsTotal['beginner']++;
- } else {
- $kpStatsTotal['unlearned']++;
- }
- }
- $moduleRowsWithStatus = array_values(array_filter($modules, function ($m) {
- $status = trim((string) ($m['status'] ?? ''));
- $masteryLevel = $m['mastery_level'] ?? null;
- $questionCount = (int) ($m['question_count'] ?? 0);
- if ($masteryLevel !== null) {
- return true;
- }
- return $questionCount > 0 && $status !== '' && ! in_array($status, ['暂无', '-', '未涉及', '未学习'], true);
- }));
- $pathTagByModuleName = [];
- foreach (['keep' => '保分不错', 'boost' => '需要加强', 'key' => '优先加强'] as $bucket => $tagName) {
- foreach (($paths[$bucket] ?? []) as $item) {
- $n = trim((string) ($item['name'] ?? ''));
- if ($n === '') {
- continue;
- }
- $pathTagByModuleName[$n] = $tagName;
- }
- }
- $globalPathTagByMastery = function ($mastery) use ($pcMasteryPercent): string {
- if ($mastery === null || ! is_numeric($mastery)) {
- return '待观察';
- }
- $m = $pcMasteryPercent($mastery);
- if ($m >= 85) {
- return '保分不错';
- }
- if ($m >= 60) {
- return '需要加强';
- }
- return '优先加强';
- };
- $overallPathTag = function (string $tag): string {
- return match ($tag) {
- '优先加强' => '整体优先',
- '需要加强' => '整体加强',
- '保分不错' => '整体巩固',
- default => $tag,
- };
- };
- $impactedModules = array_values(array_filter($moduleRowsWithStatus, function ($m) {
- return ((int) ($m['question_count'] ?? 0)) > 0;
- }));
- $radarModuleMap = [];
- foreach ($radar as $moduleItem) {
- $code = (string) ($moduleItem['code'] ?? '');
- if ($code !== '') {
- $radarModuleMap[$code] = $moduleItem;
- }
- }
- $moduleKpSuggestions = [];
- foreach ($moduleRowsWithStatus as $m) {
- $moduleCode = (string) ($m['module_code'] ?? '');
- $moduleName = (string) ($m['module_name'] ?? '-');
- $moduleChildren = $radarModuleMap[$moduleCode]['children'] ?? [];
- if (! is_array($moduleChildren) || empty($moduleChildren)) {
- continue;
- }
- $moduleHitCandidates = [];
- foreach (($mastery['items'] ?? []) as $item) {
- $hitCode = trim((string) ($item['kp_code'] ?? $item['code'] ?? ''));
- if ($hitCode === '') {
- continue;
- }
- if (! empty($examHitKpSet) && ! isset($examHitKpSet[$hitCode])) {
- continue;
- }
- $hitLevel = $item['mastery_level'] ?? null;
- if ($hitLevel === null || ! is_numeric($hitLevel)) {
- continue;
- }
- $matchedChild = null;
- foreach ($moduleChildren as $child) {
- $childCode = trim((string) ($child['code'] ?? ''));
- $parentCode = trim((string) ($child['parent_code'] ?? ''));
- if ($childCode === $hitCode || $parentCode === $hitCode) {
- $matchedChild = $child;
- break;
- }
- }
- if (! is_array($matchedChild)) {
- continue;
- }
- $moduleHitCandidates[$hitCode] = [
- 'code' => $hitCode,
- 'name' => (string) ($item['kp_name'] ?? $item['name'] ?? ($matchedChild['parent_name'] ?? $matchedChild['name'] ?? $hitCode)),
- 'parent_code' => (string) ($matchedChild['parent_code'] ?? ''),
- 'parent_name' => (string) ($matchedChild['parent_name'] ?? ''),
- 'grand_parent_name' => (string) ($matchedChild['grand_parent_name'] ?? ''),
- 'mastery_level' => (float) $hitLevel,
- 'is_hit' => true,
- ];
- }
- $startedByCode = $moduleHitCandidates;
- foreach (array_values(array_filter($moduleChildren, function ($c) {
- return isset($c['mastery_level']) && $c['mastery_level'] !== null;
- })) as $child) {
- $childCode = trim((string) ($child['code'] ?? ''));
- if ($childCode !== '' && ! isset($startedByCode[$childCode])) {
- $startedByCode[$childCode] = $child;
- }
- }
- $started = array_values($startedByCode);
- usort($started, function ($a, $b) {
- $am = (float) ($a['mastery_level'] ?? 0);
- $bm = (float) ($b['mastery_level'] ?? 0);
- if ($am === $bm) {
- $ah = !empty($a['is_hit']) ? 0 : 1;
- $bh = !empty($b['is_hit']) ? 0 : 1;
- if ($ah !== $bh) {
- return $ah <=> $bh;
- }
- return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
- }
- return $am <=> $bm;
- });
- $weakest = null;
- if (! empty($started)) {
- $lowestStarted = $started[0];
- $lowestStartedLevel = isset($lowestStarted['mastery_level']) ? (float) $lowestStarted['mastery_level'] : null;
- if ($lowestStartedLevel !== null && ($pcMasteryPercent($lowestStartedLevel) ?? 0) < 85) {
- // 规则1:已开始学习中掌握度最低
- $weakest = $lowestStarted;
- } else {
- // 规则2:若已开始学习均达标(>=85%),取“最近的未学习”
- $unlearned = array_values(array_filter($moduleChildren, function ($c) {
- return !isset($c['mastery_level']) || $c['mastery_level'] === null;
- }));
- if (! empty($unlearned)) {
- $anchorParent = (string) ($lowestStarted['parent_name'] ?? '');
- $anchorGrand = (string) ($lowestStarted['grand_parent_name'] ?? '');
- usort($unlearned, function ($a, $b) use ($anchorParent, $anchorGrand) {
- $score = function ($node) use ($anchorParent, $anchorGrand) {
- $parent = (string) ($node['parent_name'] ?? '');
- $grand = (string) ($node['grand_parent_name'] ?? '');
- if ($anchorParent !== '' && $parent === $anchorParent) {
- return 0;
- }
- if ($anchorGrand !== '' && $grand === $anchorGrand) {
- return 1;
- }
- return 2;
- };
- $sa = $score($a);
- $sb = $score($b);
- if ($sa === $sb) {
- return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
- }
- return $sa <=> $sb;
- });
- $weakest = $unlearned[0];
- }
- }
- } else {
- // 没有已开始学习数据时,回退到模块内任一未学习点
- $unlearned = array_values(array_filter($moduleChildren, function ($c) {
- return !isset($c['mastery_level']) || $c['mastery_level'] === null;
- }));
- if (! empty($unlearned)) {
- usort($unlearned, fn ($a, $b) => strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? '')));
- $weakest = $unlearned[0];
- }
- }
- if (! is_array($weakest)) {
- $moduleKpSuggestions[] = [
- 'module_name' => $moduleName,
- 'path_tag' => $pathTagByModuleName[$moduleName] ?? '待观察',
- 'kp_name' => '',
- 'kp_code' => '',
- 'mastery_level' => null,
- 'status' => '当前模块暂无需额外关注知识点',
- 'is_empty' => true,
- ];
- continue;
- }
- $kpName = (string) ($weakest['name'] ?? '');
- if ($kpName === '') {
- continue;
- }
- $kpCode = (string) ($weakest['code'] ?? '');
- $moduleKpSuggestions[] = [
- 'module_name' => $moduleName,
- 'path_tag' => $pathTagByModuleName[$moduleName] ?? '待观察',
- 'kp_name' => $kpName,
- 'kp_code' => $kpCode,
- 'mastery_level' => $weakest['mastery_level'] ?? null,
- 'status' => $childMasteryStatus($weakest['mastery_level'] ?? null),
- 'is_empty' => false,
- ];
- }
- $moduleSuggestionByName = [];
- foreach ($moduleKpSuggestions as $sug) {
- $name = trim((string) ($sug['module_name'] ?? ''));
- if ($name !== '') {
- $moduleSuggestionByName[$name] = $sug;
- }
- }
- $focusMarkerByCode = [];
- foreach ($moduleKpSuggestions as $sug) {
- if (! empty($sug['is_empty'])) {
- continue;
- }
- $code = trim((string) ($sug['kp_code'] ?? ''));
- $name = trim((string) ($sug['kp_name'] ?? ''));
- if ($code === '' || $name === '') {
- continue;
- }
- $focusMarkerByCode[$code] = [
- 'name' => $name,
- 'module_name' => (string) ($sug['module_name'] ?? ''),
- 'mastery_level' => $sug['mastery_level'] ?? null,
- ];
- }
- $renderedFocusMarkerCodes = [];
- $kpChangeItems = [];
- foreach (($mastery['items'] ?? []) as $item) {
- $code = trim((string) ($item['kp_code'] ?? $item['code'] ?? ''));
- if ($code !== '' && ! empty($examHitKpSet) && ! isset($examHitKpSet[$code])) {
- continue;
- }
- $level = $item['mastery_level'] ?? null;
- if ($level === null || ! is_numeric($level)) {
- continue;
- }
- $change = $item['mastery_change'] ?? $item['change'] ?? 0.0;
- $kpChangeItems[] = [
- 'code' => $code,
- 'name' => (string) ($item['kp_name'] ?? $item['name'] ?? ($code !== '' ? $code : '-')),
- 'mastery_level' => (float) $level,
- 'change' => is_numeric($change) ? (float) $change : 0.0,
- 'status' => $childMasteryStatus((float) $level),
- 'is_hit' => true,
- ];
- }
- usort($kpChangeItems, function ($a, $b) {
- return abs((float) ($b['change'] ?? 0)) <=> abs((float) ($a['change'] ?? 0));
- });
- $kpPct = function (int $count, int $total): string {
- if ($total <= 0) {
- return '0.0%';
- }
- return number_format(($count * 100.0) / $total, 1) . '%';
- };
- $changeText = function ($change): string {
- if ($change === null || ! is_numeric($change)) {
- return '';
- }
- $delta = (float) $change;
- $points = number_format(abs($delta) * 100, 1);
- if ($delta > 0.0005) {
- return '较上次提升' . $points . '个百分点';
- }
- if ($delta < -0.0005) {
- return '较上次下降' . $points . '个百分点';
- }
- return '较上次基本持平';
- };
- @endphp
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <title>学情分析报告</title>
- <link rel="stylesheet" href="/css/katex/katex.min.css">
- <style>
- @page {
- size: A4;
- margin: 2.2cm 2cm 2.3cm 2cm;
- @top-left { content: "知了数学·{{ $generateDateTime }}"; font-size: 13px; color: #666; }
- @top-center { content: "{{ $student['name'] ?? '-' }}"; font-size: 13px; color: #666; }
- @top-right {
- content: "{{ $reportCode }}";
- font-size: 19px;
- font-weight: 600;
- font-family: "Noto Sans", "Liberation Sans", "Nimbus Sans", sans-serif;
- color: #222;
- }
- @bottom-left { content: "{{ $reportCode }}"; font-size: 11px; color: #666; }
- @bottom-right { content: counter(page) "/" counter(pages); font-size: 13px; color: #666; }
- }
- * { box-sizing: border-box; }
- body { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; margin: 0; color: #0f172a; font-size: 13px; line-height: 1.65; }
- .page { page-break-after: auto; }
- .header { text-align: left; margin-bottom: 16px; }
- .paper-title { font-size: 30px; font-weight: 700; margin-bottom: 8px; color: #0b3a75; letter-spacing: 1px; }
- .section { margin-bottom: 14px; page-break-inside: auto; break-inside: auto; }
- .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; }
- .card { border: 1px solid #dbeafe; border-radius: 12px; padding: 14px; background: #f8fbff; position: relative; }
- .summary-list { margin: 0; padding-left: 18px; }
- .summary-list li { margin: 6px 0; font-size: 13px; }
- .overall-badge {
- position: absolute;
- right: 14px;
- top: 12px;
- border-radius: 12px;
- border: 0;
- padding: 9px 16px;
- min-width: 0;
- width: auto;
- text-align: center;
- position: absolute;
- overflow: hidden;
- display: inline-block;
- white-space: nowrap;
- background: transparent !important;
- }
- .overall-badge .level { font-size: 28px; font-weight: 800; line-height: 1.05; letter-spacing: 1px; }
- .overall-badge .score { font-size: 13px; margin-top: 3px; }
- .overall-badge.badge-s {
- border: 5px solid #6d28d9;
- border-radius: 14px;
- box-shadow: none;
- transform: rotate(-7deg);
- }
- .overall-badge.badge-s::before {
- content: "";
- position: absolute;
- inset: 4px;
- border: 2px dashed rgba(109, 40, 217, 0.65);
- border-radius: 10px;
- pointer-events: none;
- }
- .overall-badge.badge-s .level {
- letter-spacing: 2px;
- text-shadow: 0 1px 0 rgba(109, 40, 217, 0.24);
- }
- .overall-badge.badge-excellent {
- border: 3px double #16a34a;
- border-radius: 999px;
- box-shadow: none;
- }
- .overall-badge.badge-good {
- border: 2px solid #2563eb;
- border-radius: 10px;
- clip-path: polygon(6% 0, 94% 0, 100% 50%, 94% 100%, 6% 100%, 0 50%);
- box-shadow: none;
- }
- .overall-badge.badge-average {
- border: 2px dashed #d97706;
- border-radius: 14px;
- box-shadow: none;
- }
- .overall-badge.badge-weak {
- border-left: 3px solid #ef4444;
- border-right: 0;
- border-top: 0;
- border-bottom: 2px solid #ef4444;
- border-radius: 0 10px 10px 0;
- box-shadow: none;
- }
- .overall-meta { margin-top: 8px; font-size: 9px; color: #64748b; line-height: 1.6; white-space: nowrap; }
- .dot {
- display: inline-block;
- width: 10px;
- height: 10px;
- border-radius: 2px;
- margin-right: 4px;
- vertical-align: middle;
- border: 1px solid #374151;
- background: #fff;
- }
- .dot-mastered {
- background: #111827;
- border-style: solid;
- }
- .dot-weak {
- background: #9ca3af;
- border-style: solid;
- }
- .dot-beginner {
- background: #e5e7eb;
- border: 1px dashed #6b7280;
- }
- .dot-unlearned {
- background: #ffffff;
- border-style: solid;
- border-color: #9ca3af;
- }
- .cluster-toolbar {
- margin-bottom: 8px;
- font-size: 11px;
- color: #475569;
- white-space: nowrap;
- }
- .cluster-legend { display: inline-block; margin-right: 12px; }
- .cluster-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 10px;
- }
- .cluster-card {
- border: 1px solid #e2e8f0;
- border-radius: 10px;
- padding: 10px;
- background: #fff;
- position: relative;
- overflow: visible;
- }
- .cluster-card-title {
- font-size: 14px;
- font-weight: 700;
- color: #0f172a;
- margin-bottom: 8px;
- }
- .cluster-subgroup {
- border-left: 2px solid #e5e7eb;
- padding-left: 8px;
- padding-right: 128px; /* 右侧空白区域再缩小 */
- margin-bottom: 8px;
- position: relative;
- }
- .cluster-subgroup:last-child { margin-bottom: 0; }
- .cluster-subgroup-title {
- font-size: 12px;
- font-weight: 600;
- color: #334155;
- margin-bottom: 4px;
- }
- .cluster-points {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- }
- .cluster-point {
- width: 10px;
- height: 10px;
- border-radius: 2px;
- display: inline-block;
- border: 1px solid rgba(148, 163, 184, 0.35);
- position: relative;
- }
- .cluster-point.status-mastered {
- background: #111827;
- border: 1px solid #1f2937;
- }
- .cluster-point.status-weak {
- background: #9ca3af;
- border: 1px solid #1f2937;
- }
- .cluster-point.status-beginner {
- background: #e5e7eb !important;
- border: 1px dashed #6b7280;
- }
- .cluster-point.status-unlearned {
- background: #ffffff !important;
- border: 1px solid #9ca3af;
- }
- .cluster-point.focus-source {
- border-color: rgba(148, 163, 184, 0.35);
- box-shadow: 0 0 0 2px #fde68a, 0 0 0 4px rgba(251, 191, 36, 0.18);
- margin-right: 4px;
- margin-bottom: 4px;
- z-index: 2;
- overflow: visible;
- }
- .cluster-focus-connector {
- position: absolute;
- left: 0;
- top: -12px;
- width: 112px;
- height: 46px;
- overflow: visible;
- pointer-events: none;
- z-index: 2;
- }
- .cluster-focus-connector path {
- fill: none;
- stroke: #0f172a;
- stroke-width: 1;
- stroke-linecap: round;
- }
- .cluster-focus-connector.dense {
- width: 128px;
- height: 46px;
- }
- .cluster-focus-connector.bottom {
- width: 118px;
- height: 46px;
- }
- .cluster-point-focus-label {
- position: absolute;
- left: 102px; /* 放到右侧空白区,并远离最右侧方块 */
- top: 50%; /* 与点位在同一水平带,避免压住文字 */
- transform: translateY(-50%);
- display: inline-block;
- max-width: none;
- border: 1px solid #0f172a;
- border-radius: 6px;
- background: #fffbeb;
- color: #92400e;
- font-size: 9px;
- font-weight: 700;
- padding: 1px 6px;
- line-height: 1.25;
- white-space: nowrap;
- z-index: 3;
- overflow: visible;
- }
- .cluster-point-focus-label.focus-offset-a { top: 50%; left: 102px; transform: translateY(-50%); }
- .cluster-point-focus-label.focus-offset-b { top: 42%; left: 102px; transform: translateY(-50%); }
- .cluster-point-focus-label.focus-offset-c { top: 58%; left: 102px; transform: translateY(-50%); }
- .cluster-point-focus-label.dense { left: 116px; top: 50%; transform: translateY(-50%); }
- .cluster-point-focus-label.bottom { left: 108px; top: 44%; transform: translateY(-50%); }
- .cluster-empty {
- font-size: 12px;
- color: #64748b;
- background: #f8fafc;
- border: 1px dashed #cbd5e1;
- border-radius: 8px;
- padding: 10px;
- }
- .kp-stats-grid {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- border: 1px solid #e5e7eb;
- border-radius: 10px;
- overflow: hidden;
- margin-bottom: 10px;
- }
- .kp-stat-item {
- padding: 8px 10px;
- border-right: 1px solid #e5e7eb;
- background: #fff;
- }
- .kp-stat-item:last-child { border-right: none; }
- .kp-stat-label { font-size: 11px; color: #64748b; }
- .kp-stat-value { font-size: 18px; font-weight: 700; color: #111827; line-height: 1.2; margin-top: 2px; }
- .kp-stat-rate { font-size: 11px; margin-left: 4px; font-weight: 600; }
- .kp-change-box { margin-bottom: 10px; border: 1px solid #e5e7eb; border-radius: 10px; background: #f8fafc; padding: 10px 12px; }
- .kp-change-list { margin: 4px 0 0 16px; padding: 0; }
- .kp-change-list li { margin: 2px 0; color: #334155; }
- .kp-burst-card { margin-top: 10px; border: 1px solid #dbeafe; border-radius: 12px; padding: 10px; background: #fff; }
- .kp-burst-title { font-size: 13px; font-weight: 700; margin-bottom: 6px; color: #0b3a75; }
- .kp-burst-meta { font-size: 12px; color: #334155; margin-top: 6px; line-height: 1.6; }
- .kp-burst-list { margin-top: 6px; font-size: 11px; color: #334155; line-height: 1.5; }
- .kp-burst-list span { display: inline-block; margin-right: 10px; margin-bottom: 3px; }
- table { width: 100%; border-collapse: collapse; font-size: 12px; background: #fff; }
- th, td { border: 1px solid #d0d7e2; padding: 8px 10px; text-align: left; vertical-align: top; }
- th { background: #f1f5f9; color: #1e293b; font-weight: 700; }
- .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; color: #fff; font-size: 11px; font-weight: 600; }
- .module-table th { background: #edf2ff; color: #0f172a; }
- .module-table th { text-align: center; }
- .module-table td { line-height: 1.45; }
- .module-table th,
- .module-table td { vertical-align: middle; }
- .module-table th:nth-child(6) { vertical-align: middle; }
- .module-table td:nth-child(6) { vertical-align: middle; text-align: center; }
- .module-table th:nth-child(1),
- .module-table td:nth-child(1) { text-align: center; }
- .module-table td:nth-child(2),
- .module-table td:nth-child(3),
- .module-table td:nth-child(5) { text-align: center; white-space: nowrap; }
- .module-table td:nth-child(6) { font-size: 11px; color: #334155; }
- .module-table tbody tr:nth-child(even) td { background: #fcfdff; }
- .module-name { font-weight: 600; color: #0f172a; }
- .impact-yes { color:#2563eb; font-weight:600; }
- .tag { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; color: #334155; background: #e5e7eb; }
- .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; }
- .error-kp-tag.high-risk { color: #b91c1c; border-color: #fca5a5; background: #fff; font-weight: 600; }
- .muted { color: #6b7280; font-size: 12px; }
- </style>
- </head>
- <body>
- <div class="page">
- <div class="header">
- <h1 class="paper-title">学情分析报告</h1>
- </div>
- <div class="section">
- <div class="section-title">一、总体评估</div>
- <div class="card">
- <div class="overall-badge {{ $overallVisual['class'] ?? '' }}"
- style="border-color:{{ $overallVisual['border'] }}; color:{{ $overallVisual['text'] }};">
- <div class="level">{{ $overallGrade }}</div>
- </div>
- <ul class="summary-list">
- <li>本次诊断得分:
- @if($scoreObtained !== null && $scoreTotal !== null && $scoreTotal > 0)
- {{ rtrim(rtrim(number_format((float) $scoreObtained, 1), '0'), '.') }}/{{ rtrim(rtrim(number_format((float) $scoreTotal, 1), '0'), '.') }}
- @else
- 暂无得分数据
- @endif
- </li>
- <li>平均掌握度:{{ $averageMastery !== null ? number_format((float) $averageMastery * 100, 1) . '%' : '暂无掌握度' }}</li>
- <li>
- 难度匹配:
- @if(!empty($difficultySummary['target_label']) && isset($difficultySummary['actual_average_difficulty']))
- 目标 {{ $difficultySummary['target_label'] }}
- @if(!empty($difficultySummary['target_range']))
- ({{ number_format((float)($difficultySummary['target_range']['min'] ?? 0), 2) }}~{{ number_format((float)($difficultySummary['target_range']['max'] ?? 0), 2) }})
- @endif
- ,实际 {{ number_format((float)($difficultySummary['actual_average_difficulty'] ?? 0), 3) }}
- ({{ $difficultySummary['status'] ?? '暂无' }})
- @else
- 暂无难度匹配数据
- @endif
- </li>
- @if(!empty($difficultySummary['explain']))
- <li>难度说明:{{ $difficultySummary['explain'] }}</li>
- @endif
- <li>
- 与历史自己对比:
- @if(!empty($historySummary['is_first_exam']))
- {{ $historySummary['message'] ?? '这是你的第一次分析报告,先积累样本再看趋势。' }}
- @elseif(!empty($historySummary['low_baseline_guard']))
- {{ $historySummary['message'] ?? '历史基线偏低,建议看连续趋势。' }}
- @elseif(!empty($historySummary['has_data']))
- @php
- $trendText = (string)($historySummary['trend'] ?? '—');
- $tVisual = $trendVisual($trendText);
- @endphp
- 近几次均值对比:
- {{ number_format((float)($historySummary['baseline_score_rate'] ?? 0) * 100, 1) }}%,
- 本次{{ ($historySummary['delta_score_rate'] ?? 0) >= 0 ? '提升' : '回落' }}
- {{ number_format(abs((float)($historySummary['delta_score_rate'] ?? 0)) * 100, 1) }}%
- (<span style="color:{{ $tVisual['color'] ?? '#64748b' }}; font-weight:600;">{{ $tVisual['icon'] ?? '•' }} {{ $trendText }}</span>)
- @else
- {{ $historySummary['message'] ?? '历史样本不足' }}
- @endif
- </li>
- @if(!empty($peerSummary['show_line']))
- <li>
- 与同群体对比:
- {{ $peerSummary['message'] ?? '' }}
- (<span style="color:{{ $peerSummary['band_color'] ?? '#64748b' }}; font-weight:600;">{{ $peerSummary['band_icon'] ?? '•' }} {{ $peerSummary['band'] ?? '—' }}</span>)
- </li>
- @endif
- <li>
- 整体水平:
- @if($overallScore !== null)
- {{ number_format($overallScore, 1) }} 分({{ $overallGrade }})
- @else
- 待计算
- @endif
- </li>
- </ul>
- <div class="overall-meta">
- 规则:综合分 = 当前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) }}
- </div>
- </div>
- </div>
- <div class="section">
- <div class="section-title">二、知识点掌握聚类视图</div>
- <div class="cluster-toolbar">
- <span class="cluster-legend"><i class="dot dot-mastered"></i>已掌握(深色实心)</span>
- <span class="cluster-legend"><i class="dot dot-weak"></i>薄弱(浅灰实心)</span>
- <span class="cluster-legend"><i class="dot dot-beginner"></i>未入门(浅灰虚线框)</span>
- <span class="cluster-legend"><i class="dot dot-unlearned"></i>未学习(白色)</span>
- <span>按“模块 → 子模块 → 知识点”聚类展示</span>
- </div>
- <div class="cluster-grid">
- @foreach($clusterCards as $cluster)
- <div class="cluster-card">
- @php
- $clusterModuleName = trim((string) ($cluster['module_name'] ?? '未分组'));
- $clusterGrandName = trim((string) ($cluster['grand_name'] ?? ''));
- $clusterTitle = ($clusterGrandName !== '' && $clusterGrandName !== $clusterModuleName)
- ? ($clusterModuleName . ' / ' . $clusterGrandName)
- : $clusterModuleName;
- @endphp
- <div class="cluster-card-title">
- {{ $clusterTitle }}
- </div>
- @if(!empty($cluster['parent_groups']))
- @foreach($cluster['parent_groups'] as $parent)
- <div class="cluster-subgroup">
- <div class="cluster-subgroup-title">{{ $parent['parent_name'] }}</div>
- <div class="cluster-points">
- @foreach($parent['points'] as $point)
- @php
- $pointCode = trim((string) ($point['code'] ?? ''));
- $pointParentCode = trim((string) ($point['parent_code'] ?? ''));
- $focusMarker = null;
- $focusMarkerCode = '';
- if ($pointCode !== '' && isset($focusMarkerByCode[$pointCode]) && empty($renderedFocusMarkerCodes[$pointCode])) {
- $focusMarker = $focusMarkerByCode[$pointCode];
- $focusMarkerCode = $pointCode;
- } elseif ($pointParentCode !== '' && isset($focusMarkerByCode[$pointParentCode]) && empty($renderedFocusMarkerCodes[$pointParentCode])) {
- $focusMarker = $focusMarkerByCode[$pointParentCode];
- $focusMarkerCode = $pointParentCode;
- }
- if ($focusMarkerCode !== '') {
- $renderedFocusMarkerCodes[$focusMarkerCode] = true;
- }
- $focusName = is_array($focusMarker) ? (string) ($focusMarker['name'] ?? '') : '';
- $pointStatusClass = match ((string) ($point['status'] ?? '')) {
- '已掌握' => 'status-mastered',
- '薄弱' => 'status-weak',
- '未入门' => 'status-beginner',
- default => 'status-unlearned',
- };
- $focusLayoutClass = '';
- if ($focusName === '幂与指数') {
- $focusLayoutClass = 'dense';
- } elseif (str_contains($clusterModuleName, '图形变化') || str_contains($clusterModuleName, '图形度量')) {
- $focusLayoutClass = 'bottom';
- }
- @endphp
- <span class="cluster-point {{ $pointStatusClass }}{{ $focusName !== '' ? ' focus-source' : '' }}"
- title="{{ $point['name'] }} · {{ $point['status'] }}{{ $point['mastery_level'] !== null ? '(' . $formatMasteryPct($point['mastery_level']) . ')' : '' }}{{ $point['path'] !== '' ? ' · ' . $point['path'] : '' }}">
- @if($focusName !== '')
- @php
- $focusOffsetClass = match ($loop->index % 3) {
- 1 => 'focus-offset-b',
- 2 => 'focus-offset-c',
- default => 'focus-offset-a',
- };
- @endphp
- <svg class="cluster-focus-connector {{ $focusLayoutClass }}" viewBox="0 0 150 64" preserveAspectRatio="none" aria-hidden="true">
- @if($focusLayoutClass === 'dense')
- <path d="M8,24 C18,24 24,18 38,16 C84,14 124,20 138,23 L146,24" />
- @elseif($focusLayoutClass === 'bottom')
- <path d="M8,23 C18,23 24,16 42,13 C86,11 114,20 132,23 L140,23" />
- @else
- <path d="M8,24 C18,24 24,18 42,15 C86,13 114,21 132,24 L142,24" />
- @endif
- </svg>
- <span class="cluster-point-focus-label {{ $focusOffsetClass }} {{ $focusLayoutClass }}">{{ $focusName }}</span>
- @endif
- </span>
- @endforeach
- </div>
- </div>
- @endforeach
- @else
- <div class="cluster-empty">当前模块暂无可展示的子知识点。</div>
- @endif
- </div>
- @endforeach
- </div>
- <div style="margin-top:10px;">
- <div class="kp-stats-grid">
- <div class="kp-stat-item">
- <div class="kp-stat-label">总知识点数</div>
- <div class="kp-stat-value">{{ $kpStatsTotal['total'] }}</div>
- </div>
- <div class="kp-stat-item">
- <div class="kp-stat-label">已掌握</div>
- <div class="kp-stat-value" style="color:#52c41a;">
- {{ $kpStatsTotal['mastered'] }}<span class="kp-stat-rate" style="color:#52c41a;">({{ $kpPct($kpStatsTotal['mastered'], $kpStatsTotal['total']) }})</span>
- </div>
- </div>
- <div class="kp-stat-item">
- <div class="kp-stat-label">薄弱</div>
- <div class="kp-stat-value" style="color:#faad14;">
- {{ $kpStatsTotal['weak'] }}<span class="kp-stat-rate" style="color:#faad14;">({{ $kpPct($kpStatsTotal['weak'], $kpStatsTotal['total']) }})</span>
- </div>
- </div>
- <div class="kp-stat-item">
- <div class="kp-stat-label">未入门</div>
- <div class="kp-stat-value" style="color:#f5222d;">
- {{ $kpStatsTotal['beginner'] }}<span class="kp-stat-rate" style="color:#f5222d;">({{ $kpPct($kpStatsTotal['beginner'], $kpStatsTotal['total']) }})</span>
- </div>
- </div>
- <div class="kp-stat-item">
- <div class="kp-stat-label">未学习</div>
- <div class="kp-stat-value" style="color:#9ca3af;">
- {{ $kpStatsTotal['unlearned'] }}<span class="kp-stat-rate" style="color:#9ca3af;">({{ $kpPct($kpStatsTotal['unlearned'], $kpStatsTotal['total']) }})</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="section">
- <div class="section-title">三、模块现状与提分路径(全局+本学案影响)</div>
- <div class="kp-change-box">
- <div style="font-size:12px;font-weight:700;color:#0f172a;">本学案知识点变化情况</div>
- @if(!empty($kpChangeItems))
- <ul class="kp-change-list">
- @foreach($kpChangeItems as $item)
- @php
- $delta = (float) ($item['change'] ?? 0);
- $deltaColor = $delta > 0 ? '#16a34a' : ($delta < 0 ? '#dc2626' : '#64748b');
- $deltaText = $changeText($delta);
- $masteryText = isset($item['mastery_level']) && $item['mastery_level'] !== null
- ? $formatMasteryPct($item['mastery_level'])
- : '--';
- @endphp
- <li>
- {{ $item['name'] ?? '-' }}:
- 当前掌握度{{ $masteryText }}({{ $item['status'] ?? '未学习' }})
- @if($deltaText !== '')
- ,<span style="color:{{ $deltaColor }};font-weight:600;">{{ $deltaText }}</span>
- @endif
- </li>
- @endforeach
- </ul>
- @else
- <div class="muted" style="margin-top:4px;">
- @if(!empty($kpWrongStats))
- 暂无本学案命中知识点的掌握度数据,以下方知识点错误率作为本学案影响依据。
- @else
- 暂无可用的知识点变化数据
- @endif
- </div>
- @endif
- </div>
- @if(!empty($kpWrongStats))
- <div style="margin-bottom:8px; padding:8px 10px; border:1px solid #e5e7eb; border-radius:8px; background:#fff7ed;">
- <div style="font-size:12px; font-weight:700; color:#9a3412; margin-bottom:6px;">知识点错误率</div>
- <div style="font-size:12px; color:#475569; line-height:1.7;">
- @foreach($kpWrongStats as $item)
- <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>
- @endforeach
- </div>
- </div>
- @endif
- <div style="margin-bottom:8px; padding:8px 10px; border:1px solid #e5e7eb; border-radius:8px; background:#fff;">
- <div style="font-size:12px;color:#334155;">
- 本次学案影响模块:
- @if(!empty($impactedModules))
- @foreach($impactedModules as $idx => $im)
- @php
- $mName = $im['module_name'] ?? '-';
- $mQuestionCount = (int) ($im['question_count'] ?? 0);
- $mScore = $im['exam_obtained_score'] ?? null;
- @endphp
- <span style="display:inline-block;padding:2px 8px;border-radius:999px;background:#eef2ff;color:#3730a3;margin-right:4px;">
- {{ $mName }}({{ $mQuestionCount }}题,得分{{ $mScore !== null ? number_format((float) $mScore, 1) : '-' }})
- </span>
- @endforeach
- @else
- <span class="muted">暂无命中模块</span>
- @endif
- </div>
- </div>
- <table class="module-table">
- <thead>
- <tr>
- <th style="width: 18%;">模块</th>
- <th style="width: 12%; white-space: nowrap;">本次影响</th>
- <th style="width: 18%;">当前掌握度</th>
- <th style="width: 14%;">掌握状态</th>
- <th style="width: 14%;">路径建议</th>
- <th style="width: 24%;">关注知识点</th>
- </tr>
- </thead>
- <tbody>
- @forelse($moduleRowsWithStatus as $m)
- @php
- $status = (string) ($m['status'] ?? '暂无');
- $color = $statusColor($status);
- $qCount = (int) ($m['question_count'] ?? 0);
- $isImpacted = $qCount > 0;
- $basePathTag = $pathTagByModuleName[(string) ($m['module_name'] ?? '')]
- ?? $globalPathTagByMastery($m['mastery_level'] ?? null);
- $pathTag = $isImpacted ? $basePathTag : $overallPathTag($basePathTag);
- $pathColor = match ($pathTag) {
- '优先加强', '整体优先' => '#ef4444',
- '需要加强', '整体加强' => '#f59e0b',
- '保分不错', '整体巩固' => '#16a34a',
- default => '#64748b',
- };
- $moduleName = (string) ($m['module_name'] ?? '');
- $focus = $moduleSuggestionByName[$moduleName] ?? null;
- $focusText = '-';
- if (is_array($focus)) {
- if (!empty($focus['is_empty'])) {
- $focusText = (string) ($focus['status'] ?? '当前模块暂无需额外关注知识点');
- } else {
- $focusName = (string) ($focus['kp_name'] ?? '');
- $focusMastery = isset($focus['mastery_level']) && $focus['mastery_level'] !== null
- ? $formatMasteryPct($focus['mastery_level'])
- : '--';
- $focusText = $focusName !== ''
- ? ($focusName . '(' . $focusMastery . ')')
- : '当前模块暂无需额外关注知识点';
- }
- }
- @endphp
- <tr>
- <td><span class="module-name">{{ $m['module_name'] ?? '-' }}</span></td>
- <td>
- @if($isImpacted)
- <span class="impact-yes">是</span>
- @else
- <span class="muted">否</span>
- @endif
- </td>
- <td>{{ isset($m['mastery_level']) && $m['mastery_level'] !== null ? $formatMasteryPct($m['mastery_level']) : '-' }}</td>
- <td><span class="badge" style="background:{{ $color }}">{{ $status }}</span></td>
- <td><span style="color:{{ $pathColor }}; font-weight:700;">{{ $pathTag }}</span></td>
- <td>{{ $focusText }}</td>
- </tr>
- @empty
- <tr>
- <td colspan="6" class="muted">暂无掌握状态数据</td>
- </tr>
- @endforelse
- </tbody>
- </table>
- </div>
- </div>
- <script src="/js/katex.min.js"></script>
- <script src="/js/auto-render.min.js"></script>
- <script>
- document.addEventListener('DOMContentLoaded', function() {
- try {
- renderMathInElement(document.body, {
- delimiters: [
- {left: "$$", right: "$$", display: true},
- {left: "$", right: "$", display: false},
- {left: "\\(", right: "\\)", display: false},
- {left: "\\[", right: "\\]", display: true}
- ],
- throwOnError: false,
- strict: false,
- trust: true
- });
- } catch (e) {}
- });
- </script>
- </body>
- </html>
|