Kaynağa Gözat

refine v3 cluster focus layout and print-safe states

Improve focus connector placement and label spacing in the v3 PDF cluster view, and standardize point styles for clearer grayscale printing while preserving status readability.

Made-with: Cursor
yemeishu 3 hafta önce
ebeveyn
işleme
3c28222963

+ 72 - 10
app/Services/ExamPdfExportService.php

@@ -613,6 +613,12 @@ class ExamPdfExportService
                 $childrenByParent[$p][] = (string) $code;
             }
         }
+        $expandedExamHitSet = $examHitSet;
+        foreach (array_keys($examHitSet) as $hitCode) {
+            foreach ($this->collectLeafDescendants($hitCode, $childrenByParent) as $leafCode) {
+                $expandedExamHitSet[$leafCode] = true;
+            }
+        }
         $resolveHierarchyForCluster = function (string $kpId) use ($kpMeta, $rootCode): array {
             $rootName = trim((string) ($kpMeta[$rootCode]['name'] ?? $rootCode));
             $lineage = [];
@@ -667,7 +673,7 @@ class ExamPdfExportService
             $changeValue = array_key_exists($kpId, $kpChangeMap)
                 ? (float) $kpChangeMap[$kpId]
                 : null;
-            $isHit = isset($examHitSet[$kpId]);
+            $isHit = isset($expandedExamHitSet[$kpId]);
             $radarChildrenByModule[$moduleCode][] = [
                 'code' => $kpId,
                 'name' => $kpMeta[$kpId]['name'] ?? $kpId,
@@ -765,32 +771,46 @@ class ExamPdfExportService
         $lowToHigh = $items;
         $highToLow = array_reverse($items);
 
-        $keep = array_values(array_filter($highToLow, fn ($i) => $i['mastery_level'] >= 0.85));
-        $boost = array_values(array_filter($lowToHigh, fn ($i) => $i['mastery_level'] >= 0.6 && $i['mastery_level'] < 0.85));
-        $key = array_values(array_filter($lowToHigh, fn ($i) => $i['mastery_level'] < 0.6));
+        $keep = array_values(array_filter($highToLow, fn ($i) => ($this->toPcMasteryPercent($i['mastery_level']) ?? -1) >= 85));
+        $boost = array_values(array_filter($lowToHigh, function ($i) {
+            $percent = $this->toPcMasteryPercent($i['mastery_level']) ?? -1;
+            return $percent >= 60 && $percent < 85;
+        }));
+        $key = array_values(array_filter($lowToHigh, fn ($i) => ($this->toPcMasteryPercent($i['mastery_level']) ?? 101) < 60));
 
         return [
-            'keep' => array_slice($keep, 0, 2),
-            'boost' => array_slice($boost, 0, 2),
-            'key' => array_slice($key, 0, 2),
+            'keep' => $keep,
+            'boost' => $boost,
+            'key' => $key,
         ];
     }
 
     private function resolveMasteryStatus(?float $mastery): string
     {
-        if ($mastery === null) {
+        $percent = $this->toPcMasteryPercent($mastery);
+        if ($percent === null) {
             return '未学习';
         }
-        if ($mastery >= 0.85) {
+        if ($percent >= 85) {
             return '已掌握';
         }
-        if ($mastery >= 0.6) {
+        if ($percent >= 60) {
             return '薄弱';
         }
 
         return '未入门';
     }
 
+    private function toPcMasteryPercent(?float $mastery): ?int
+    {
+        if ($mastery === null) {
+            return null;
+        }
+
+        // PC API returns mastery rounded to 2 decimals, then the frontend Math.rounds it to 0-100.
+        return (int) round(round($mastery, 2) * 100);
+    }
+
     private function resolveOverallPerformanceLabel(?float $avgMastery): string
     {
         if ($avgMastery === null) {
@@ -1432,6 +1452,48 @@ class ExamPdfExportService
         }
     }
 
+    /**
+     * Expand a hit knowledge point to leaf descendants so PDF cluster callouts match higher-level paper targets.
+     *
+     * @param array<string, array<int, string>> $childrenByParent
+     * @return array<int, string>
+     */
+    private function collectLeafDescendants(string $rootCode, array $childrenByParent): array
+    {
+        $rootCode = trim($rootCode);
+        if ($rootCode === '') {
+            return [];
+        }
+
+        $leaves = [];
+        $queue = [$rootCode];
+        $visited = [];
+        while (! empty($queue)) {
+            $code = array_shift($queue);
+            if (isset($visited[$code])) {
+                continue;
+            }
+            $visited[$code] = true;
+
+            $children = $childrenByParent[$code] ?? [];
+            if (empty($children)) {
+                if ($code !== $rootCode) {
+                    $leaves[] = $code;
+                }
+                continue;
+            }
+
+            foreach ($children as $childCode) {
+                $childCode = trim((string) $childCode);
+                if ($childCode !== '') {
+                    $queue[] = $childCode;
+                }
+            }
+        }
+
+        return array_values(array_unique($leaves));
+    }
+
     private function resolveRadarStage(string $grade, array $fullParentLevels = []): string
     {
         $g = trim($grade);

+ 283 - 0
public/mockups/cluster-focus-demo.html

@@ -0,0 +1,283 @@
+<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>知识点聚类连线 - PDF场景复刻</title>
+  <style>
+    :root {
+      --bg: #f5f7fb;
+      --card: #ffffff;
+      --line: #0f172a;         /* 连线颜色(独立于标签文字) */
+      --line-dense: #111827;   /* 密集场景更深线色 */
+      --text: #0f172a;
+      --muted: #64748b;
+      --tag-text: #92400e;     /* 标签文字色 */
+      --dot-mastered: #22c55e;
+      --dot-weak: #f59e0b;
+      --dot-beginner: #ef4444;
+      --dot-unlearned: #d1d5db;
+    }
+    * { box-sizing: border-box; }
+    body {
+      margin: 0;
+      font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
+      color: var(--text);
+      background: var(--bg);
+      padding: 24px;
+    }
+    .wrap { max-width: 1420px; margin: 0 auto; }
+    h1 { margin: 0 0 8px; font-size: 34px; }
+    .legend { margin-bottom: 18px; color: var(--muted); font-size: 18px; }
+    .legend .dot {
+      width: 13px; height: 13px; border-radius: 999px; display: inline-block; margin: 0 6px 0 16px;
+      vertical-align: middle;
+    }
+    .grid {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 16px;
+    }
+    .card {
+      background: var(--card);
+      border: 1px solid #dfe4ea;
+      border-radius: 16px;
+      padding: 14px 16px 16px;
+      min-height: 250px;
+    }
+    .card h2 { margin: 0 0 10px; font-size: 38px; }
+    .group {
+      border-left: 3px solid #e5e7eb;
+      margin-bottom: 10px;
+      padding-left: 10px;
+      position: relative;
+      padding-right: 230px; /* 右侧留给标签区 */
+    }
+    .group-title { font-size: 34px; font-weight: 700; margin-bottom: 6px; line-height: 1.25; }
+    .points {
+      display: flex;
+      gap: 7px;
+      align-items: center;
+      flex-wrap: wrap;
+      min-height: 20px;
+      position: relative;
+    }
+    .point {
+      width: 16px;
+      height: 16px;
+      border-radius: 4px;
+      border: 1px solid #bfc7d1;
+      position: relative;
+      flex: 0 0 auto;
+    }
+    .mastered { background: var(--dot-mastered); }
+    .weak { background: var(--dot-weak); }
+    .beginner { background: var(--dot-beginner); }
+    .unlearned { background: var(--dot-unlearned); }
+
+    .point.focus {
+      box-shadow: 0 0 0 3px rgba(253, 224, 71, .45);
+    }
+    .focus-tag {
+      position: absolute;
+      left: 138px; /* 与最右侧方块拉开安全间距 */
+      top: 50%;
+      transform: translateY(-50%);
+      border: 2px solid var(--line);
+      border-radius: 999px;
+      padding: 2px 10px;
+      font-size: 27px;
+      line-height: 1.3;
+      color: var(--tag-text);
+      background: #fff7ed;
+      white-space: nowrap;
+      max-width: none;
+      overflow: visible;
+      text-overflow: clip;
+    }
+    .focus-tag.dense { left: 178px; top: 50%; }
+    .focus-tag.bottom { left: 148px; top: 44%; }
+
+    .focus-svg {
+      position: absolute;
+      left: 0;
+      top: -36px;
+      width: 162px;
+      height: 64px;
+      overflow: visible;
+      pointer-events: none;
+    }
+    .focus-svg path {
+      fill: none;
+      stroke: var(--line);
+      stroke-width: 2.2;
+      stroke-linecap: round;
+    }
+    .focus-svg.dense {
+      width: 170px;
+      height: 58px;
+      top: -30px;
+    }
+    .focus-svg.dense path { stroke: var(--line-dense); }
+    .note {
+      margin-top: 16px;
+      padding: 10px 14px;
+      border: 1px dashed #cbd5e1;
+      border-radius: 10px;
+      color: #334155;
+      background: #fff;
+      font-size: 16px;
+    }
+    .span-2 { grid-column: 1 / -1; min-height: 170px; }
+    .mini { min-height: 190px; }
+  </style>
+</head>
+<body>
+  <div class="wrap">
+    <h1>二、知识点掌握聚类视图(PDF场景复刻)</h1>
+    <div class="legend">
+      <span class="dot" style="background:var(--dot-mastered)"></span>已掌握
+      <span class="dot" style="background:var(--dot-weak)"></span>薄弱
+      <span class="dot" style="background:var(--dot-beginner)"></span>未入门
+      <span class="dot" style="background:var(--dot-unlearned)"></span>未学习
+      按“模块 -> 子模块 -> 知识点”聚类展示
+    </div>
+
+    <div class="grid">
+      <section class="card">
+        <h2>函数</h2>
+        <div class="group">
+          <div class="group-title">二次函数</div>
+          <div class="points">
+            <span class="point weak"></span><span class="point weak"></span><span class="point beginner"></span><span class="point beginner"></span><span class="point beginner"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+        <div class="group">
+          <div class="group-title">锐角三角函数</div>
+          <div class="points">
+            <span class="point beginner"></span>
+            <span class="point beginner focus">
+              <svg class="focus-svg" viewBox="0 0 150 64" aria-hidden="true">
+                <!-- 普通场景:起点锚定点中心(8,40),终点进入标签内侧(136,40) -->
+                <path d="M8,40 C16,40 22,26 40,20 C84,16 114,34 130,39 L136,40" />
+              </svg>
+              <span class="focus-tag">三角函数的应用</span>
+            </span>
+            <span class="point unlearned"></span>
+          </div>
+        </div>
+        <div class="group">
+          <div class="group-title">反比例函数</div>
+          <div class="points">
+            <span class="point mastered"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+      </section>
+
+      <section class="card">
+        <h2>相似与勾股</h2>
+        <div class="group">
+          <div class="group-title">相似三角形判定</div>
+          <div class="points">
+            <span class="point mastered"></span><span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+        <div class="group">
+          <div class="group-title">相似三角形性质</div>
+          <div class="points">
+            <span class="point weak"></span><span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+        <div class="group">
+          <div class="group-title">勾股定理与直角三角形</div>
+          <div class="points">
+            <span class="point beginner focus">
+              <svg class="focus-svg" viewBox="0 0 150 64" aria-hidden="true">
+                <path d="M8,40 C16,40 22,26 38,20 C80,16 110,34 130,39 L136,40" />
+              </svg>
+              <span class="focus-tag">直角三角形性质</span>
+            </span>
+            <span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+        <div class="group">
+          <div class="group-title">相似与勾股在压轴题中的整合</div>
+          <div class="points">
+            <span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+      </section>
+
+      <section class="card span-2 mini">
+        <h2>图形变化与度量 / 图形度量</h2>
+        <div class="group">
+          <div class="group-title">立体几何度量(表面积与体积)</div>
+          <div class="points">
+            <span class="point beginner focus">
+              <svg class="focus-svg bottom" viewBox="0 0 150 64" aria-hidden="true">
+                <!-- 跨列底部场景:弧线更高,避免贴近点阵 -->
+                <path d="M8,40 C16,40 22,24 40,18 C82,15 112,33 130,39 L136,40" />
+              </svg>
+              <span class="focus-tag bottom">立体几何展开图</span>
+            </span>
+            <span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+      </section>
+
+      <section class="card">
+        <h2>方程与不等式</h2>
+        <div class="group">
+          <div class="group-title">一元二次方程</div>
+          <div class="points">
+            <span class="point beginner"></span><span class="point beginner"></span><span class="point beginner"></span>
+            <span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+        <div class="group">
+          <div class="group-title">分式方程</div>
+          <div class="points">
+            <span class="point beginner focus">
+              <svg class="focus-svg" viewBox="0 0 150 64" aria-hidden="true">
+                <path d="M8,40 C16,40 22,26 38,20 C80,16 110,34 130,39 L136,40" />
+              </svg>
+              <span class="focus-tag">分式方程的应用</span>
+            </span>
+            <span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+      </section>
+
+      <section class="card">
+        <h2>数与代数</h2>
+        <div class="group">
+          <div class="group-title">数的认识与运算</div>
+          <div class="points">
+            <span class="point beginner"></span><span class="point beginner"></span><span class="point weak"></span>
+            <span class="point beginner focus">
+              <svg class="focus-svg dense" viewBox="0 0 150 64" aria-hidden="true">
+                <!-- 密集场景:前段快速抬升 + 终点内插,避免擦到后续灰块 -->
+                <path d="M8,40 C14,40 20,26 34,22 C78,18 124,26 154,34 L162,36" />
+              </svg>
+              <span class="focus-tag dense">幂与指数</span>
+            </span>
+            <span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+            <span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+        <div class="group">
+          <div class="group-title">代数式与整式运算</div>
+          <div class="points">
+            <span class="point beginner"></span><span class="point beginner"></span><span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span><span class="point unlearned"></span>
+          </div>
+        </div>
+      </section>
+    </div>
+
+    <div class="note">
+      这版覆盖了 PDF 中常见情况:长标题换行、密集点阵、上下卡片、多个关注点位置、右侧标签区。你确认这页视觉后,我再同步回正式模板。
+    </div>
+  </div>
+</body>
+</html>
+

+ 364 - 151
resources/views/exam-analysis/pdf-report-v3.blade.php

@@ -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>