|
@@ -4,7 +4,6 @@
|
|
|
$radar = $v3['radar'] ?? [];
|
|
$radar = $v3['radar'] ?? [];
|
|
|
$modules = $v3['modules'] ?? [];
|
|
$modules = $v3['modules'] ?? [];
|
|
|
$paths = $v3['paths'] ?? ['keep' => [], 'boost' => [], 'key' => []];
|
|
$paths = $v3['paths'] ?? ['keep' => [], 'boost' => [], 'key' => []];
|
|
|
- $overallPlan = $v3['overall_plan'] ?? [];
|
|
|
|
|
|
|
|
|
|
$rawPaperId = $paper['id'] ?? $paper['paper_id'] ?? 'unknown';
|
|
$rawPaperId = $paper['id'] ?? $paper['paper_id'] ?? 'unknown';
|
|
|
preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
|
|
preg_match('/paper_(\d{15})/', $rawPaperId, $matches);
|
|
@@ -162,6 +161,399 @@
|
|
|
return $b['rate'] <=> $a['rate'];
|
|
return $b['rate'] <=> $a['rate'];
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ $childMasteryStatus = function ($mastery): string {
|
|
|
|
|
+ if ($mastery === null) {
|
|
|
|
|
+ return '未学习';
|
|
|
|
|
+ }
|
|
|
|
|
+ $m = (float) $mastery * 100; // 与 math.client-pc 统一:0-100 阈值(85/60)
|
|
|
|
|
+ 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;
|
|
|
|
|
+ foreach ($points as $p) {
|
|
|
|
|
+ if (($p['mastery_level'] ?? null) !== null) {
|
|
|
|
|
+ $learned++;
|
|
|
|
|
+ }
|
|
|
|
|
+ $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,
|
|
|
|
|
+ ];
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ $clusterCards = [];
|
|
|
|
|
+ $allClusterPoints = [];
|
|
|
|
|
+ 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 : '未分组';
|
|
|
|
|
+ }
|
|
|
|
|
+ $mastery = isset($child['mastery_level']) ? (float) $child['mastery_level'] : null;
|
|
|
|
|
+ $status = $childMasteryStatus($mastery);
|
|
|
|
|
+ if (!isset($greatMap[$greatKey])) {
|
|
|
|
|
+ $greatMap[$greatKey] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!isset($greatMap[$greatKey][$grandKey])) {
|
|
|
|
|
+ $greatMap[$greatKey][$grandKey] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!isset($greatMap[$greatKey][$grandKey][$parentName])) {
|
|
|
|
|
+ $greatMap[$greatKey][$grandKey][$parentName] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ $greatMap[$greatKey][$grandKey][$parentName][] = [
|
|
|
|
|
+ 'code' => (string) ($child['code'] ?? ''),
|
|
|
|
|
+ 'name' => (string) ($child['name'] ?? '未命名知识点'),
|
|
|
|
|
+ 'path' => (string) ($child['path'] ?? ''),
|
|
|
|
|
+ 'mastery_level' => $mastery,
|
|
|
|
|
+ 'change' => isset($child['change']) ? (float) $child['change'] : null,
|
|
|
|
|
+ 'status' => $status,
|
|
|
|
|
+ 'color' => $childStatusColor($status),
|
|
|
|
|
+ 'is_hit' => !empty($child['is_hit']),
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $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);
|
|
|
|
|
+ }));
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }));
|
|
|
|
|
+ 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[] = [
|
|
|
|
|
+ '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));
|
|
|
|
|
+ });
|
|
|
|
|
+ foreach ($clusterCards as $card) {
|
|
|
|
|
+ foreach (($card['parent_groups'] ?? []) as $pg) {
|
|
|
|
|
+ foreach (($pg['points'] ?? []) as $p) {
|
|
|
|
|
+ $allClusterPoints[] = $p;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $kpStatsTotal = [
|
|
|
|
|
+ 'total' => count($allClusterPoints),
|
|
|
|
|
+ 'mastered' => 0,
|
|
|
|
|
+ 'weak' => 0,
|
|
|
|
|
+ 'beginner' => 0,
|
|
|
|
|
+ 'unlearned' => 0,
|
|
|
|
|
+ ];
|
|
|
|
|
+ foreach ($allClusterPoints 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;
|
|
|
|
|
+ if ($masteryLevel !== null) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return $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;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $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;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $moduleImpactChangeMap = [];
|
|
|
|
|
+ foreach ($radarModuleMap as $moduleCode => $moduleItem) {
|
|
|
|
|
+ $hitChanges = [];
|
|
|
|
|
+ foreach (($moduleItem['children'] ?? []) as $child) {
|
|
|
|
|
+ if (empty($child['is_hit'])) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $change = $child['change'] ?? null;
|
|
|
|
|
+ if ($change === null || ! is_numeric($change)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $hitChanges[] = (float) $change;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (empty($hitChanges)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $moduleImpactChangeMap[$moduleCode] = array_sum($hitChanges) / count($hitChanges);
|
|
|
|
|
+ }
|
|
|
|
|
+ $questionTypeLabelMap = [
|
|
|
|
|
+ 'choice' => '选择题',
|
|
|
|
|
+ 'multiple_choice' => '选择题',
|
|
|
|
|
+ 'single_choice' => '选择题',
|
|
|
|
|
+ 'select' => '选择题',
|
|
|
|
|
+ 'fill' => '填空题',
|
|
|
|
|
+ 'blank' => '填空题',
|
|
|
|
|
+ 'answer' => '解答题',
|
|
|
|
|
+ 'solution' => '解答题',
|
|
|
|
|
+ ];
|
|
|
|
|
+ $kpQuestionTypeMap = [];
|
|
|
|
|
+ foreach (($questions ?? []) as $qItem) {
|
|
|
|
|
+ $kpName = trim((string) ($qItem['knowledge_point_name'] ?? $qItem['knowledge_point'] ?? ''));
|
|
|
|
|
+ if ($kpName === '') {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $rawType = strtolower(trim((string) ($qItem['question_type'] ?? '')));
|
|
|
|
|
+ $typeLabel = $questionTypeLabelMap[$rawType] ?? ((string) ($qItem['question_type'] ?? '未知题型'));
|
|
|
|
|
+ if ($typeLabel === '') {
|
|
|
|
|
+ $typeLabel = '未知题型';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (! isset($kpQuestionTypeMap[$kpName])) {
|
|
|
|
|
+ $kpQuestionTypeMap[$kpName] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ $kpQuestionTypeMap[$kpName][$typeLabel] = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ $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;
|
|
|
|
|
+ }
|
|
|
|
|
+ $started = array_values(array_filter($moduleChildren, function ($c) {
|
|
|
|
|
+ return isset($c['mastery_level']) && $c['mastery_level'] !== null;
|
|
|
|
|
+ }));
|
|
|
|
|
+ usort($started, function ($a, $b) {
|
|
|
|
|
+ $am = (float) ($a['mastery_level'] ?? 0);
|
|
|
|
|
+ $bm = (float) ($b['mastery_level'] ?? 0);
|
|
|
|
|
+ if ($am === $bm) {
|
|
|
|
|
+ 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 && $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 {
|
|
|
|
|
+ $weakest = $lowestStarted;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } 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)) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $kpName = (string) ($weakest['name'] ?? '');
|
|
|
|
|
+ if ($kpName === '') {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ $types = array_keys($kpQuestionTypeMap[$kpName] ?? []);
|
|
|
|
|
+ $moduleKpSuggestions[] = [
|
|
|
|
|
+ 'module_name' => $moduleName,
|
|
|
|
|
+ 'path_tag' => $pathTagByModuleName[$moduleName] ?? '待观察',
|
|
|
|
|
+ 'kp_name' => $kpName,
|
|
|
|
|
+ 'mastery_level' => $weakest['mastery_level'] ?? null,
|
|
|
|
|
+ 'status' => $childMasteryStatus($weakest['mastery_level'] ?? null),
|
|
|
|
|
+ 'question_types' => $types,
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+ $moduleSuggestionByName = [];
|
|
|
|
|
+ foreach ($moduleKpSuggestions as $sug) {
|
|
|
|
|
+ $name = trim((string) ($sug['module_name'] ?? ''));
|
|
|
|
|
+ if ($name !== '') {
|
|
|
|
|
+ $moduleSuggestionByName[$name] = $sug;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ $kpChangeItems = array_values(array_filter($allClusterPoints, function ($p) {
|
|
|
|
|
+ $change = $p['change'] ?? null;
|
|
|
|
|
+ return $change !== null && is_numeric($change) && !empty($p['is_hit']);
|
|
|
|
|
+ }));
|
|
|
|
|
+ if (empty($kpChangeItems)) {
|
|
|
|
|
+ $kpChangeItems = array_values(array_filter($allClusterPoints, function ($p) {
|
|
|
|
|
+ $change = $p['change'] ?? null;
|
|
|
|
|
+ return $change !== null && is_numeric($change);
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
|
|
+ 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) . '%';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
@endphp
|
|
@endphp
|
|
|
<!DOCTYPE html>
|
|
<!DOCTYPE html>
|
|
|
<html lang="zh-CN">
|
|
<html lang="zh-CN">
|
|
@@ -266,6 +658,81 @@
|
|
|
.radar-right { width: 100%; padding-left: 0; margin-top: 10px; }
|
|
.radar-right { width: 100%; padding-left: 0; margin-top: 10px; }
|
|
|
.radar-desc { border: 1px solid #dbeafe; background: #f8fbff; border-radius: 12px; padding: 12px; text-align: left; }
|
|
.radar-desc { border: 1px solid #dbeafe; background: #f8fbff; border-radius: 12px; padding: 12px; text-align: left; }
|
|
|
.radar-item { display: block; margin: 6px 0; font-size: 12px; }
|
|
.radar-item { display: block; margin: 6px 0; font-size: 12px; }
|
|
|
|
|
+ .cluster-toolbar {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #475569;
|
|
|
|
|
+ }
|
|
|
|
|
+ .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;
|
|
|
|
|
+ }
|
|
|
|
|
+ .cluster-card-title {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #0f172a;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .cluster-subgroup {
|
|
|
|
|
+ border-left: 2px solid #e5e7eb;
|
|
|
|
|
+ padding-left: 8px;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .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);
|
|
|
|
|
+ }
|
|
|
|
|
+ .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-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-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-meta { font-size: 12px; color: #334155; margin-top: 6px; line-height: 1.6; }
|
|
@@ -275,16 +742,24 @@
|
|
|
th, td { border: 1px solid #d0d7e2; padding: 8px 10px; text-align: left; vertical-align: top; }
|
|
th, td { border: 1px solid #d0d7e2; padding: 8px 10px; text-align: left; vertical-align: top; }
|
|
|
th { background: #f1f5f9; color: #1e293b; font-weight: 700; }
|
|
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; }
|
|
.badge { display: inline-block; padding: 2px 8px; border-radius: 999px; color: #fff; font-size: 11px; font-weight: 600; }
|
|
|
- .path-stack { display: block; }
|
|
|
|
|
- .path-box { border: 1px solid #e5e7eb; border-radius: 12px; padding: 12px 14px; margin-bottom: 10px; }
|
|
|
|
|
- .path-box.keep { background: #f0fdf4; border-color: #86efac; }
|
|
|
|
|
- .path-box.boost { background: #fff7ed; border-color: #fdba74; }
|
|
|
|
|
- .path-box.key { background: #fff1f2; border-color: #fda4af; }
|
|
|
|
|
- .path-title { font-size: 16px; font-weight: 700; margin-bottom: 6px; color: #111827; }
|
|
|
|
|
- .path-box ul { margin: 0; padding-left: 16px; }
|
|
|
|
|
- .path-box li { font-size: 13px; margin: 2px 0; }
|
|
|
|
|
- .plan { background: #eef4ff; border-left: 4px solid #3b82f6; border-radius: 12px; padding: 12px 14px; }
|
|
|
|
|
- .plan ol { margin: 0; padding-left: 18px; }
|
|
|
|
|
|
|
+ .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(8),
|
|
|
|
|
+ .module-table td:nth-child(8) { vertical-align: top; }
|
|
|
|
|
+ .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),
|
|
|
|
|
+ .module-table td:nth-child(6),
|
|
|
|
|
+ .module-table td:nth-child(7) { text-align: center; white-space: nowrap; }
|
|
|
|
|
+ .module-table td:nth-child(8) { 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; }
|
|
.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 { 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; }
|
|
.error-kp-tag.high-risk { color: #b91c1c; border-color: #fca5a5; background: #fff; font-weight: 600; }
|
|
@@ -441,175 +916,202 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="section">
|
|
<div class="section">
|
|
|
- <div class="section-title">二、知识点掌握雷达图</div>
|
|
|
|
|
- <div class="radar-split">
|
|
|
|
|
- <div class="radar-left">
|
|
|
|
|
- <svg width="430" height="320" viewBox="0 0 430 320">
|
|
|
|
|
- <polygon points="{{ $outerPoints }}" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1"/>
|
|
|
|
|
- <polygon points="{{ $innerPoints }}" fill="rgba(59,130,246,0.28)" stroke="#3b82f6" stroke-width="2"/>
|
|
|
|
|
- @foreach($outer as $i => $p)
|
|
|
|
|
- <line x1="{{ $cx }}" y1="{{ $cy }}" x2="{{ round($p[0],2) }}" y2="{{ round($p[1],2) }}" stroke="#e2e8f0" stroke-width="1"/>
|
|
|
|
|
- @endforeach
|
|
|
|
|
- @foreach($outer as $i => $p)
|
|
|
|
|
- @php
|
|
|
|
|
- $name = $radar[$i]['name'] ?? '';
|
|
|
|
|
- $children = $radar[$i]['children'] ?? [];
|
|
|
|
|
- $labelX = $p[0] + (($p[0] >= $cx) ? 9 : -9);
|
|
|
|
|
- $labelY = $p[1] + (($p[1] >= $cy) ? 12 : -8);
|
|
|
|
|
- $anchor = $p[0] >= $cx ? 'start' : 'end';
|
|
|
|
|
- $dotColor = $statusColor((string) ($radar[$i]['status'] ?? '暂无'));
|
|
|
|
|
- $value = number_format((float) ($radar[$i]['value'] ?? 0), 2);
|
|
|
|
|
- $axisAngle = atan2(($p[1] - $cy), ($p[0] - $cx));
|
|
|
|
|
- @endphp
|
|
|
|
|
- <text x="{{ round($labelX,2) }}" y="{{ round($labelY,2) }}" font-size="11" fill="#334155" text-anchor="{{ $anchor }}">{{ $name }} {{ $value }}</text>
|
|
|
|
|
- <circle cx="{{ round($inner[$i][0],2) }}" cy="{{ round($inner[$i][1],2) }}" r="4" fill="{{ $dotColor }}" />
|
|
|
|
|
- @if(!empty($children))
|
|
|
|
|
- @foreach($children as $cIdx => $child)
|
|
|
|
|
- @php
|
|
|
|
|
- $childN = max(1, count($children));
|
|
|
|
|
- $depth = max(1, intval($child['depth'] ?? 1));
|
|
|
|
|
- $offset = ($cIdx - (($childN - 1) / 2)) * 0.04;
|
|
|
|
|
- $childAngle = $axisAngle + $offset;
|
|
|
|
|
- // 子知识点必须从父轴外圈向外发散,避免父轴值低时挤在中心
|
|
|
|
|
- $axisOuterR = sqrt(pow(($outer[$i][0] - $cx), 2) + pow(($outer[$i][1] - $cy), 2));
|
|
|
|
|
- $startR = max($axisOuterR + 4, 112 + (($depth - 1) * 10));
|
|
|
|
|
- $endR = $startR + 12 + (($depth - 1) * 4);
|
|
|
|
|
- $sx = $cx + $startR * cos($childAngle);
|
|
|
|
|
- $sy = $cy + $startR * sin($childAngle);
|
|
|
|
|
- $ex = $cx + $endR * cos($childAngle);
|
|
|
|
|
- $ey = $cy + $endR * sin($childAngle);
|
|
|
|
|
- $changed = !empty($child['changed']);
|
|
|
|
|
- $cColor = $changed ? '#e11d48' : '#94a3b8';
|
|
|
|
|
- $cWidth = $changed ? 1.5 : 0.8;
|
|
|
|
|
- $masteryPct = isset($child['mastery_level']) ? max(0, min(100, (float) $child['mastery_level'] * 100)) : null;
|
|
|
|
|
- $label = $masteryPct !== null ? number_format($masteryPct, 1) . '%' : '—';
|
|
|
|
|
- $tx = $cx + ($endR + 5 + (($depth - 1) * 2)) * cos($childAngle);
|
|
|
|
|
- $ty = $cy + ($endR + 5 + (($depth - 1) * 2)) * sin($childAngle);
|
|
|
|
|
- $anchor = cos($childAngle) >= 0 ? 'start' : 'end';
|
|
|
|
|
- @endphp
|
|
|
|
|
- <line x1="{{ round($sx,2) }}" y1="{{ round($sy,2) }}" x2="{{ round($ex,2) }}" y2="{{ round($ey,2) }}"
|
|
|
|
|
- stroke="{{ $cColor }}" stroke-width="{{ $cWidth }}" opacity="{{ $changed ? 0.95 : 0.85 }}"/>
|
|
|
|
|
- <circle cx="{{ round($ex,2) }}" cy="{{ round($ey,2) }}" r="{{ $changed ? 1.6 : 1.0 }}" fill="{{ $cColor }}" />
|
|
|
|
|
- <text x="{{ round($tx,2) }}" y="{{ round($ty,2) }}" font-size="9" fill="{{ $cColor }}" text-anchor="{{ $anchor }}">{{ $label }}</text>
|
|
|
|
|
|
|
+ <div class="section-title">二、知识点掌握聚类视图</div>
|
|
|
|
|
+ <div class="cluster-toolbar">
|
|
|
|
|
+ <span class="cluster-legend"><i class="dot" style="background:#52c41a"></i>已掌握</span>
|
|
|
|
|
+ <span class="cluster-legend"><i class="dot" style="background:#faad14"></i>薄弱</span>
|
|
|
|
|
+ <span class="cluster-legend"><i class="dot" style="background:#f5222d"></i>未入门</span>
|
|
|
|
|
+ <span class="cluster-legend"><i class="dot" style="background:#d9d9d9"></i>未学习</span>
|
|
|
|
|
+ <span>按“模块 → 子模块 → 知识点”聚类展示</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="cluster-grid">
|
|
|
|
|
+ @foreach($clusterCards as $cluster)
|
|
|
|
|
+ <div class="cluster-card">
|
|
|
|
|
+ <div class="cluster-card-title">
|
|
|
|
|
+ {{ $cluster['grand_name'] }}
|
|
|
|
|
+ </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)
|
|
|
|
|
+ <span class="cluster-point"
|
|
|
|
|
+ style="background:{{ $point['color'] }}"
|
|
|
|
|
+ title="{{ $point['name'] }} · {{ $point['status'] }}{{ $point['mastery_level'] !== null ? '(' . number_format((float)$point['mastery_level'] * 100, 1) . '%)' : '' }}{{ $point['path'] !== '' ? ' · ' . $point['path'] : '' }}"></span>
|
|
|
|
|
+ @endforeach
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
@endforeach
|
|
@endforeach
|
|
|
|
|
+ @else
|
|
|
|
|
+ <div class="cluster-empty">当前模块暂无可展示的子知识点。</div>
|
|
|
@endif
|
|
@endif
|
|
|
- @endforeach
|
|
|
|
|
- </svg>
|
|
|
|
|
- <div class="legend">
|
|
|
|
|
- <span><i class="dot" style="background:#16a34a"></i>良好(4.0-5.0)</span>
|
|
|
|
|
- <span><i class="dot" style="background:#f97316"></i>一般(2.0-3.9)</span>
|
|
|
|
|
- <span><i class="dot" style="background:#e11d48"></i>薄弱(0-1.9)</span>
|
|
|
|
|
- <span><i class="dot" style="background:#64748b"></i>未涉及</span>
|
|
|
|
|
- <span><i class="dot" style="background:#94a3b8"></i>子知识点</span>
|
|
|
|
|
- <span><i class="dot" style="background:#e11d48"></i>子知识点变化</span>
|
|
|
|
|
- <span>外圈越远表示层级越深</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="radar-right">
|
|
|
|
|
- <div class="radar-desc">
|
|
|
|
|
- <strong>雷达图解读</strong>
|
|
|
|
|
- @foreach($radar as $item)
|
|
|
|
|
- @php $color = $statusColor((string) ($item['status'] ?? '暂无')); @endphp
|
|
|
|
|
- <span class="radar-item">
|
|
|
|
|
- <i class="dot" style="background:{{ $color }}"></i>
|
|
|
|
|
- {{ $item['name'] }}:{{ $item['status'] }}
|
|
|
|
|
- ({{ !empty($item['has_mastery']) ? number_format((float) ($item['value'] ?? 0), 2) . '/5' : '—' }})
|
|
|
|
|
- @if(!empty($item['children']))
|
|
|
|
|
- ,子知识点 {{ count($item['children']) }} 个
|
|
|
|
|
- @endif
|
|
|
|
|
- </span>
|
|
|
|
|
- @endforeach
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </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>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="section">
|
|
<div class="section">
|
|
|
- <div class="section-title">三、模块能力分析表</div>
|
|
|
|
|
- <table>
|
|
|
|
|
|
|
+ <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);
|
|
|
|
|
+ $deltaText = ($delta >= 0 ? '+' : '') . number_format($delta * 100, 1) . '%';
|
|
|
|
|
+ $deltaColor = $delta > 0 ? '#16a34a' : ($delta < 0 ? '#dc2626' : '#64748b');
|
|
|
|
|
+ $masteryText = isset($item['mastery_level']) && $item['mastery_level'] !== null
|
|
|
|
|
+ ? number_format((float) $item['mastery_level'] * 100, 1) . '%'
|
|
|
|
|
+ : '--';
|
|
|
|
|
+ @endphp
|
|
|
|
|
+ <li>
|
|
|
|
|
+ {{ $item['name'] ?? '-' }}:
|
|
|
|
|
+ <span style="color:{{ $deltaColor }};font-weight:600;">{{ $deltaText }}</span>
|
|
|
|
|
+ (掌握度{{ $masteryText }},{{ $item['status'] ?? '未学习' }})
|
|
|
|
|
+ </li>
|
|
|
|
|
+ @endforeach
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ @else
|
|
|
|
|
+ <div class="muted" style="margin-top:4px;">暂无可用的知识点变化数据</div>
|
|
|
|
|
+ @endif
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <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'] ?? '-'; @endphp
|
|
|
|
|
+ <span style="display:inline-block;padding:2px 8px;border-radius:999px;background:#eef2ff;color:#3730a3;margin-right:4px;">
|
|
|
|
|
+ {{ $mName }}({{ $im['question_count'] ?? 0 }}题)
|
|
|
|
|
+ </span>
|
|
|
|
|
+ @endforeach
|
|
|
|
|
+ @else
|
|
|
|
|
+ <span class="muted">暂无命中模块</span>
|
|
|
|
|
+ @endif
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <table class="module-table">
|
|
|
<thead>
|
|
<thead>
|
|
|
<tr>
|
|
<tr>
|
|
|
- <th style="width: 16%;">模块</th>
|
|
|
|
|
- <th style="width: 13%;">掌握分值</th>
|
|
|
|
|
|
|
+ <th style="width: 14%;">模块</th>
|
|
|
|
|
+ <th style="width: 10%; white-space: nowrap;">本次影响</th>
|
|
|
|
|
+ <th style="width: 18%;">掌握度<br>(学案影响)</th>
|
|
|
<th style="width: 12%;">掌握状态</th>
|
|
<th style="width: 12%;">掌握状态</th>
|
|
|
- <th style="width: 10%;">样本数</th>
|
|
|
|
|
- <th style="width: 12%;">得分率</th>
|
|
|
|
|
- <th>学生当前能力</th>
|
|
|
|
|
|
|
+ <th style="width: 9%;">题目数</th>
|
|
|
|
|
+ <th style="width: 11%;">得分率</th>
|
|
|
|
|
+ <th style="width: 11%;">路径建议</th>
|
|
|
|
|
+ <th style="width: 25%;">关注知识点</th>
|
|
|
</tr>
|
|
</tr>
|
|
|
</thead>
|
|
</thead>
|
|
|
<tbody>
|
|
<tbody>
|
|
|
- @foreach($modules as $m)
|
|
|
|
|
|
|
+ @forelse($moduleRowsWithStatus as $m)
|
|
|
@php
|
|
@php
|
|
|
$status = (string) ($m['status'] ?? '暂无');
|
|
$status = (string) ($m['status'] ?? '暂无');
|
|
|
$color = $statusColor($status);
|
|
$color = $statusColor($status);
|
|
|
$rate = $m['exam_score_rate'] ?? null;
|
|
$rate = $m['exam_score_rate'] ?? null;
|
|
|
|
|
+ $qCount = (int) ($m['question_count'] ?? 0);
|
|
|
|
|
+ $isImpacted = $qCount > 0;
|
|
|
|
|
+ $pathTag = $pathTagByModuleName[(string) ($m['module_name'] ?? '')] ?? '待观察';
|
|
|
|
|
+ $pathColor = match ($pathTag) {
|
|
|
|
|
+ '优先加强' => '#ef4444',
|
|
|
|
|
+ '需要加强' => '#f59e0b',
|
|
|
|
|
+ '保分不错' => '#16a34a',
|
|
|
|
|
+ default => '#64748b',
|
|
|
|
|
+ };
|
|
|
|
|
+ $moduleCode = (string) ($m['module_code'] ?? '');
|
|
|
|
|
+ $impactDelta = $moduleImpactChangeMap[$moduleCode] ?? null;
|
|
|
|
|
+ $impactArrow = $impactDelta === null
|
|
|
|
|
+ ? ''
|
|
|
|
|
+ : ($impactDelta > 0.0005 ? '↑' : ($impactDelta < -0.0005 ? '↓' : '→'));
|
|
|
|
|
+ $impactColor = $impactDelta === null
|
|
|
|
|
+ ? '#64748b'
|
|
|
|
|
+ : ($impactDelta > 0.0005 ? '#16a34a' : ($impactDelta < -0.0005 ? '#dc2626' : '#64748b'));
|
|
|
|
|
+ $impactText = $impactDelta === null ? '' : number_format(abs($impactDelta) * 100, 1) . '%';
|
|
|
|
|
+ $impactSuffix = $impactArrow === '→' ? '' : $impactText;
|
|
|
|
|
+ $moduleName = (string) ($m['module_name'] ?? '');
|
|
|
|
|
+ $focus = $moduleSuggestionByName[$moduleName] ?? null;
|
|
|
|
|
+ $focusText = '-';
|
|
|
|
|
+ if (is_array($focus)) {
|
|
|
|
|
+ $focusName = (string) ($focus['kp_name'] ?? '');
|
|
|
|
|
+ $focusTypes = !empty($focus['question_types']) ? implode('、', $focus['question_types']) : '';
|
|
|
|
|
+ $focusMastery = isset($focus['mastery_level']) && $focus['mastery_level'] !== null
|
|
|
|
|
+ ? number_format((float) $focus['mastery_level'] * 100, 1) . '%'
|
|
|
|
|
+ : '--';
|
|
|
|
|
+ $focusSuffix = $focusTypes !== '' ? (',' . $focusTypes) : '';
|
|
|
|
|
+ $focusText = $focusName !== ''
|
|
|
|
|
+ ? ($focusName . '(' . $focusMastery . $focusSuffix . ')')
|
|
|
|
|
+ : '-';
|
|
|
|
|
+ }
|
|
|
@endphp
|
|
@endphp
|
|
|
<tr>
|
|
<tr>
|
|
|
- <td>{{ $m['module_name'] ?? '-' }}</td>
|
|
|
|
|
- <td>{{ $m['mastery_score_5'] !== null ? number_format((float) $m['mastery_score_5'], 2) . '/5' : '-' }}</td>
|
|
|
|
|
|
|
+ <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 ? number_format((float) $m['mastery_level'] * 100, 1) . '%' : '-' }}
|
|
|
|
|
+ @if($impactArrow !== '')
|
|
|
|
|
+ <span style="margin-left:4px;color:{{ $impactColor }};font-weight:700;">
|
|
|
|
|
+ {{ $impactArrow }}{{ $impactSuffix }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ @endif
|
|
|
|
|
+ </td>
|
|
|
<td><span class="badge" style="background:{{ $color }}">{{ $status }}</span></td>
|
|
<td><span class="badge" style="background:{{ $color }}">{{ $status }}</span></td>
|
|
|
- <td>{{ $m['kp_count'] ?? 0 }}</td>
|
|
|
|
|
|
|
+ <td>{{ $m['question_count'] ?? 0 }}</td>
|
|
|
<td>{{ $rate !== null ? number_format((float) $rate * 100, 1) . '%' : '-' }}</td>
|
|
<td>{{ $rate !== null ? number_format((float) $rate * 100, 1) . '%' : '-' }}</td>
|
|
|
- <td>{{ $m['ability_text'] ?? '-' }}</td>
|
|
|
|
|
|
|
+ <td><span style="color:{{ $pathColor }}; font-weight:700;">{{ $pathTag }}</span></td>
|
|
|
|
|
+ <td>{{ $focusText }}</td>
|
|
|
</tr>
|
|
</tr>
|
|
|
- @endforeach
|
|
|
|
|
|
|
+ @empty
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <td colspan="8" class="muted">暂无掌握状态数据</td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ @endforelse
|
|
|
</tbody>
|
|
</tbody>
|
|
|
</table>
|
|
</table>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div class="section">
|
|
|
|
|
- <div class="section-title">四、分模块提分路径</div>
|
|
|
|
|
- <div class="path-stack">
|
|
|
|
|
- <div class="path-box keep">
|
|
|
|
|
- <div class="path-title">保分模块(保持优势)</div>
|
|
|
|
|
- <ul>
|
|
|
|
|
- @foreach(($paths['keep'] ?? []) as $item)
|
|
|
|
|
- <li>{{ $item['name'] }}:掌握度 {{ number_format((float) ($item['mastery_level'] ?? 0) * 100, 1) }}%</li>
|
|
|
|
|
- @endforeach
|
|
|
|
|
- </ul>
|
|
|
|
|
- @if(empty($paths['keep']))
|
|
|
|
|
- <div class="muted">暂无数据</div>
|
|
|
|
|
- @endif
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="path-box boost">
|
|
|
|
|
- <div class="path-title">涨分模块(重点突破)</div>
|
|
|
|
|
- <ul>
|
|
|
|
|
- @foreach(($paths['boost'] ?? []) as $item)
|
|
|
|
|
- <li>{{ $item['name'] }}:掌握度 {{ number_format((float) ($item['mastery_level'] ?? 0) * 100, 1) }}%</li>
|
|
|
|
|
- @endforeach
|
|
|
|
|
- </ul>
|
|
|
|
|
- @if(empty($paths['boost']))
|
|
|
|
|
- <div class="muted">暂无数据</div>
|
|
|
|
|
- @endif
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="path-box key">
|
|
|
|
|
- <div class="path-title">提分关键(优先补短)</div>
|
|
|
|
|
- <ul>
|
|
|
|
|
- @foreach(($paths['key'] ?? []) as $item)
|
|
|
|
|
- <li>{{ $item['name'] }}:掌握度 {{ number_format((float) ($item['mastery_level'] ?? 0) * 100, 1) }}%</li>
|
|
|
|
|
- @endforeach
|
|
|
|
|
- </ul>
|
|
|
|
|
- @if(empty($paths['key']))
|
|
|
|
|
- <div class="muted">暂无数据</div>
|
|
|
|
|
- @endif
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div class="section">
|
|
|
|
|
- <div class="section-title">五、整体提升方案</div>
|
|
|
|
|
- <div class="plan">
|
|
|
|
|
- <ol>
|
|
|
|
|
- @foreach($overallPlan as $line)
|
|
|
|
|
- <li>{{ $line }}</li>
|
|
|
|
|
- @endforeach
|
|
|
|
|
- </ol>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
@if(!empty($wrongQuestions))
|
|
@if(!empty($wrongQuestions))
|
|
|
<div class="section" style="page-break-inside:auto; break-inside:auto;">
|
|
<div class="section" style="page-break-inside:auto; break-inside:auto;">
|
|
|
- <div class="section-title">六、这次错题记录</div>
|
|
|
|
|
|
|
+ <div class="section-title">四、这次错题记录</div>
|
|
|
@if(!empty($kpWrongStats))
|
|
@if(!empty($kpWrongStats))
|
|
|
<div style="margin-bottom:8px; padding:8px; border:1px solid #e5e7eb; border-radius:6px; background:#f8fafc;">
|
|
<div style="margin-bottom:8px; padding:8px; border:1px solid #e5e7eb; border-radius:6px; background:#f8fafc;">
|
|
|
<div style="font-size:12px; font-weight:600; margin-bottom:6px;">知识点错误率</div>
|
|
<div style="font-size:12px; font-weight:600; margin-bottom:6px;">知识点错误率</div>
|