|
|
@@ -132,11 +132,22 @@
|
|
|
return $b['rate'] <=> $a['rate'];
|
|
|
});
|
|
|
|
|
|
- $childMasteryStatus = function ($mastery): string {
|
|
|
+ $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 '未学习';
|
|
|
}
|
|
|
- $m = (float) $mastery * 100; // 与 math.client-pc 统一:0-100 阈值(85/60)
|
|
|
if ($m >= 85) {
|
|
|
return '已掌握';
|
|
|
}
|
|
|
@@ -160,10 +171,14 @@
|
|
|
$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++;
|
|
|
@@ -182,6 +197,7 @@
|
|
|
'weak' => $weak,
|
|
|
'beginner' => $beginner,
|
|
|
'unlearned' => $unlearned,
|
|
|
+ 'hit' => $hit,
|
|
|
];
|
|
|
};
|
|
|
|
|
|
@@ -211,23 +227,30 @@
|
|
|
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' => (string) ($child['code'] ?? ''),
|
|
|
+ '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' => !empty($child['is_hit']),
|
|
|
+ 'is_hit' => $isHit,
|
|
|
];
|
|
|
$allStagePoints[] = [
|
|
|
- 'code' => (string) ($child['code'] ?? ''),
|
|
|
+ 'code' => $childCode,
|
|
|
'name' => (string) ($child['name'] ?? '未命名知识点'),
|
|
|
+ 'parent_code' => $childParentCode,
|
|
|
'mastery_level' => $childMasteryLevel,
|
|
|
'status' => $status,
|
|
|
'change' => isset($child['change']) ? (float) $child['change'] : null,
|
|
|
- 'is_hit' => !empty($child['is_hit']),
|
|
|
+ 'is_hit' => $isHit,
|
|
|
];
|
|
|
}
|
|
|
|
|
|
@@ -253,7 +276,7 @@
|
|
|
}
|
|
|
// 子模块级过滤:整行没有任何掌握度数字则不显示
|
|
|
$parentGroups = array_values(array_filter($parentGroups, function ($pg) {
|
|
|
- return (($pg['stats']['learned'] ?? 0) > 0);
|
|
|
+ return (($pg['stats']['learned'] ?? 0) > 0) || (($pg['stats']['hit'] ?? 0) > 0);
|
|
|
}));
|
|
|
if (empty($parentGroups)) {
|
|
|
continue;
|
|
|
@@ -275,7 +298,7 @@
|
|
|
}
|
|
|
// 大块级过滤:整块没有任何掌握度数字则不显示
|
|
|
$grandGroups = array_values(array_filter($grandGroups, function ($gg) {
|
|
|
- return (($gg['stats']['learned'] ?? 0) > 0);
|
|
|
+ return (($gg['stats']['learned'] ?? 0) > 0) || (($gg['stats']['hit'] ?? 0) > 0);
|
|
|
}));
|
|
|
if (empty($grandGroups)) {
|
|
|
continue;
|
|
|
@@ -361,6 +384,27 @@
|
|
|
$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;
|
|
|
}));
|
|
|
@@ -371,50 +415,6 @@
|
|
|
$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) {
|
|
|
- $kpCode = trim((string) ($qItem['knowledge_point'] ?? ''));
|
|
|
- if ($kpCode === '') {
|
|
|
- continue;
|
|
|
- }
|
|
|
- $rawType = strtolower(trim((string) ($qItem['question_type'] ?? '')));
|
|
|
- $typeLabel = $questionTypeLabelMap[$rawType] ?? ((string) ($qItem['question_type'] ?? '未知题型'));
|
|
|
- if ($typeLabel === '') {
|
|
|
- $typeLabel = '未知题型';
|
|
|
- }
|
|
|
- if (! isset($kpQuestionTypeMap[$kpCode])) {
|
|
|
- $kpQuestionTypeMap[$kpCode] = [];
|
|
|
- }
|
|
|
- $kpQuestionTypeMap[$kpCode][$typeLabel] = true;
|
|
|
- }
|
|
|
$moduleKpSuggestions = [];
|
|
|
foreach ($moduleRowsWithStatus as $m) {
|
|
|
$moduleCode = (string) ($m['module_code'] ?? '');
|
|
|
@@ -423,13 +423,60 @@
|
|
|
if (! is_array($moduleChildren) || empty($moduleChildren)) {
|
|
|
continue;
|
|
|
}
|
|
|
- $started = array_values(array_filter($moduleChildren, function ($c) {
|
|
|
+ $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;
|
|
|
@@ -439,7 +486,7 @@
|
|
|
if (! empty($started)) {
|
|
|
$lowestStarted = $started[0];
|
|
|
$lowestStartedLevel = isset($lowestStarted['mastery_level']) ? (float) $lowestStarted['mastery_level'] : null;
|
|
|
- if ($lowestStartedLevel !== null && $lowestStartedLevel < 0.85) {
|
|
|
+ if ($lowestStartedLevel !== null && ($pcMasteryPercent($lowestStartedLevel) ?? 0) < 85) {
|
|
|
// 规则1:已开始学习中掌握度最低
|
|
|
$weakest = $lowestStarted;
|
|
|
} else {
|
|
|
@@ -490,7 +537,6 @@
|
|
|
'kp_code' => '',
|
|
|
'mastery_level' => null,
|
|
|
'status' => '当前模块暂无需额外关注知识点',
|
|
|
- 'question_types' => [],
|
|
|
'is_empty' => true,
|
|
|
];
|
|
|
continue;
|
|
|
@@ -500,7 +546,6 @@
|
|
|
continue;
|
|
|
}
|
|
|
$kpCode = (string) ($weakest['code'] ?? '');
|
|
|
- $types = array_keys($kpQuestionTypeMap[$kpCode] ?? []);
|
|
|
$moduleKpSuggestions[] = [
|
|
|
'module_name' => $moduleName,
|
|
|
'path_tag' => $pathTagByModuleName[$moduleName] ?? '待观察',
|
|
|
@@ -508,7 +553,6 @@
|
|
|
'kp_code' => $kpCode,
|
|
|
'mastery_level' => $weakest['mastery_level'] ?? null,
|
|
|
'status' => $childMasteryStatus($weakest['mastery_level'] ?? null),
|
|
|
- 'question_types' => $types,
|
|
|
'is_empty' => false,
|
|
|
];
|
|
|
}
|
|
|
@@ -519,6 +563,23 @@
|
|
|
$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'] ?? ''));
|
|
|
@@ -548,6 +609,20 @@
|
|
|
}
|
|
|
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>
|
|
|
@@ -643,11 +718,38 @@
|
|
|
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: 999px; margin-right: 4px; vertical-align: middle; }
|
|
|
+ .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: 12px;
|
|
|
+ font-size: 11px;
|
|
|
color: #475569;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
.cluster-legend { display: inline-block; margin-right: 12px; }
|
|
|
.cluster-grid {
|
|
|
@@ -660,6 +762,8 @@
|
|
|
border-radius: 10px;
|
|
|
padding: 10px;
|
|
|
background: #fff;
|
|
|
+ position: relative;
|
|
|
+ overflow: visible;
|
|
|
}
|
|
|
.cluster-card-title {
|
|
|
font-size: 14px;
|
|
|
@@ -670,7 +774,9 @@
|
|
|
.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 {
|
|
|
@@ -690,7 +796,80 @@
|
|
|
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;
|
|
|
@@ -733,16 +912,14 @@
|
|
|
.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(6) { vertical-align: middle; }
|
|
|
+ .module-table td:nth-child(6) { 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 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; }
|
|
|
@@ -835,28 +1012,83 @@
|
|
|
<div class="section">
|
|
|
<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 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">
|
|
|
- <div class="cluster-card-title">
|
|
|
- {{ $cluster['module_name'] }} / {{ $cluster['grand_name'] }}
|
|
|
- </div>
|
|
|
+ @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)
|
|
|
- <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
|
|
|
+ @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
|
|
|
@@ -906,23 +1138,22 @@
|
|
|
<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);
|
|
|
- $deltaArrow = $delta > 0.0005 ? '↑' : ($delta < -0.0005 ? '↓' : '→');
|
|
|
- $deltaText = $deltaArrow === '→'
|
|
|
- ? '→'
|
|
|
- : ($deltaArrow . number_format(abs($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>
|
|
|
+ @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
|
|
|
@@ -948,13 +1179,17 @@
|
|
|
<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
|
|
|
+ @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
|
|
|
@@ -963,42 +1198,31 @@
|
|
|
<table class="module-table">
|
|
|
<thead>
|
|
|
<tr>
|
|
|
- <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: 9%;">题目数</th>
|
|
|
- <th style="width: 11%;">得分</th>
|
|
|
- <th style="width: 11%;">路径建议</th>
|
|
|
- <th style="width: 25%;">关注知识点</th>
|
|
|
+ <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);
|
|
|
- $rate = $m['exam_score_rate'] ?? null;
|
|
|
- $qCount = (int) ($m['question_count'] ?? 0);
|
|
|
+ $status = (string) ($m['status'] ?? '暂无');
|
|
|
+ $color = $statusColor($status);
|
|
|
+ $qCount = (int) ($m['question_count'] ?? 0);
|
|
|
$isImpacted = $qCount > 0;
|
|
|
- $pathTag = $pathTagByModuleName[(string) ($m['module_name'] ?? '')] ?? '待观察';
|
|
|
+ $basePathTag = $pathTagByModuleName[(string) ($m['module_name'] ?? '')]
|
|
|
+ ?? $globalPathTagByMastery($m['mastery_level'] ?? null);
|
|
|
+ $pathTag = $isImpacted ? $basePathTag : $overallPathTag($basePathTag);
|
|
|
$pathColor = match ($pathTag) {
|
|
|
- '优先加强' => '#ef4444',
|
|
|
- '需要加强' => '#f59e0b',
|
|
|
- '保分不错' => '#16a34a',
|
|
|
+ '优先加强', '整体优先' => '#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'] ?? '');
|
|
|
+ $moduleName = (string) ($m['module_name'] ?? '');
|
|
|
$focus = $moduleSuggestionByName[$moduleName] ?? null;
|
|
|
$focusText = '-';
|
|
|
if (is_array($focus)) {
|
|
|
@@ -1006,13 +1230,11 @@
|
|
|
$focusText = (string) ($focus['status'] ?? '当前模块暂无需额外关注知识点');
|
|
|
} else {
|
|
|
$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) : '';
|
|
|
+ $focusMastery = isset($focus['mastery_level']) && $focus['mastery_level'] !== null
|
|
|
+ ? $formatMasteryPct($focus['mastery_level'])
|
|
|
+ : '--';
|
|
|
$focusText = $focusName !== ''
|
|
|
- ? ($focusName . '(' . $focusMastery . $focusSuffix . ')')
|
|
|
+ ? ($focusName . '(' . $focusMastery . ')')
|
|
|
: '当前模块暂无需额外关注知识点';
|
|
|
}
|
|
|
}
|
|
|
@@ -1026,23 +1248,14 @@
|
|
|
<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>{{ $m['question_count'] ?? 0 }}</td>
|
|
|
- <td>{{ $rate !== null ? number_format((float) $rate * 100, 1) . '%' : '-' }}</td>
|
|
|
- <td><span style="color:{{ $pathColor }}; font-weight:700;">{{ $pathTag }}</span></td>
|
|
|
- <td>{{ $focusText }}</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="8" class="muted">暂无掌握状态数据</td>
|
|
|
+ <td colspan="6" class="muted">暂无掌握状态数据</td>
|
|
|
</tr>
|
|
|
@endforelse
|
|
|
</tbody>
|