فهرست منبع

选择练习知识点

吴朋磊 2 روز پیش
والد
کامیت
56e9fdfdad

+ 4 - 0
src/http/api/target.js

@@ -49,6 +49,10 @@ const targetImprove = {
   knowledgePartition(data) {
     return post( base.prefix+ `/api/v1/studentTarge/knowledge_partition`, data)
   },
+  // 加入练习
+  addExercise(data) {
+    return post( base.prefix+ `/api/v1/studentTarge/add_exercise`, data)
+  },
 }
 
 export default targetImprove

+ 195 - 13
src/views/targetImprove/practice/knowledgeGraph.vue

@@ -27,15 +27,20 @@
       <circular-diffusion :subjectName="actualSubjectName" :subjectScoreRate="subjectScoreRate"
         :tableData="displayTableData"
         :classLevel="classLevel" :classGroupName="classGroupName"
-        :currentKnowledgeId="currentKnowledgeId || defaultKnowledgeId"
+        :currentKnowledgeId="currentLinkedKnowledgeId || currentKnowledgeId || defaultKnowledgeId"
+        :addedKnowledgeIds="addedKnowledgeIds"
         @legend-change="handleLegendChange" @activeTabChange="handleActiveTabChange"
         @add-practice="handleAddPractice"
         @knowledge-item-click="handleKnowledgeItemClick" />
-    </div>
-
 
-    <!-- 区域展示组件 -->
-    <region :allKnowledgeList="displayAllKnowledgeList" @add-practice="handleAddPractice" />
+      <!-- 区域展示组件 -->
+      <region
+        :partitionData="partitionData"
+        :addedKnowledgeIds="addedKnowledgeIds"
+        :selectedKnowledgeId="currentLinkedKnowledgeId || currentKnowledgeId || defaultKnowledgeId"
+        @add-practice="handleAddPractice"
+        @item-click="handleRegionItemClick" />
+    </div>
   </div>
 </template>
 
@@ -110,10 +115,21 @@ export default {
       knowList: [],
       // knowList 扁平化后的一维数组(用于统计、列表展示)
       flatKnowList: [],
+      // 知识点分区:由 knowledge_partition 接口返回
+      // conquerZone: 薄弱补强区 / improveZone: 能力提升区 / consolidateZone: 扎实巩固区
+      partitionData: {
+        conquerZone: [],
+        improveZone: [],
+        consolidateZone: []
+      },
+      // 当前会话中"已加入练习"的知识点 ID 容器(响应式:父级 $set 后自动驱动子组件按钮置灰)
+      // 形如 { '1001': true }
+      addedKnowledgeIds: {},
+      // 环形扩散图 ↔ 区域卡片 的联动选中知识点 ID
+      // - 点环形扩散图圆点 → 写这里 → region 读 prop :selectedKnowledgeId 高亮并滚动居中
+      // - 点 region 某行条目 → 写这里 → circularDiffusion 读 prop :currentKnowledgeId 放大圆点 + 更新右侧折线表格
+      currentLinkedKnowledgeId: '',
       // 假数据:表格树形结构数据(用于无接口数据时兜底)
-      // knowledgeType: 1=薄弱(红色), 2=良好(黄色), 3=优秀(绿色)
-      // gradeScoreRate: 年级得分率,用于判断节点是否有数据
-      // examNum: 考试次数,questionNum: 题目数量
       mockTableData: []
     };
   },
@@ -192,6 +208,7 @@ export default {
     subjectCode(newVal, oldVal) {
       if (newVal && newVal !== oldVal) {
         this.fetchKnowledgeGraph();
+        this.fetchKnowledgePartition();
       }
     }
   },
@@ -202,9 +219,9 @@ export default {
     this.calculateContainerHeight();
     // 监听窗口大小变化
     window.addEventListener('resize', this.calculateContainerHeight);
-    // 首次加载知识图谱数据
-
+    // 首次加载知识图谱数据 + 分区数据
     this.fetchKnowledgeGraph();
+    this.fetchKnowledgePartition();
   },
   beforeDestroy() {
     // 移除窗口大小变化监听
@@ -287,6 +304,79 @@ export default {
       });
     },
 
+    /**
+     * 参数和 knowledgeGraph 相同:levelCode / gradeCodes / subjectCode
+     * 返回:conquerZone(薄弱) / improveZone(提升) / consolidateZone(巩固)
+     */
+    fetchKnowledgePartition() {
+      const levelCode = localStorage.getItem('student_level_code') || '';
+      let gradeCodes = this.currentGradeList && this.currentGradeList.length
+        ? this.currentGradeList
+        : (this.gradeList || []).map(g => g.value);
+      const subjectCode = this.subjectCode || '';
+      const params = { levelCode, gradeCodes, subjectCode };
+
+      this.$api.target.knowledgePartition(params).then(res => {
+        if (res.code == 200 && res.data) {
+          const d = res.data || {};
+          this.partitionData = {
+            conquerZone:    this.mapPartitionItem(Array.isArray(d.conquerZone)    ? d.conquerZone    : []),
+            improveZone:    this.mapPartitionItem(Array.isArray(d.improveZone)    ? d.improveZone    : []),
+            consolidateZone:this.mapPartitionItem(Array.isArray(d.consolidateZone)? d.consolidateZone : [])
+          };
+        } else {
+          this.resetPartitionEmpty();
+        }
+      }).catch(err => {
+        console.error('[KnowledgeGraph] knowledgePartition 接口异常:', err);
+        this.resetPartitionEmpty();
+      });
+    },
+
+    /**
+     * 分区接口返回的每个子数组字段规范化
+     *  - knowledgeId / knowledgeName / personalScoreRate / examNum / knowledgeType
+     * 顺便兜底转换成 Number,避免字符串导致渲染异常
+     */
+    mapPartitionItem(list) {
+      if (!Array.isArray(list)) return [];
+      return list.map(item => {
+        const node = { ...(item || {}) };
+        node.knowledgeId    = node.knowledgeId ?? node.id;
+        node.knowledgeName  = node.knowledgeName ?? node.name ?? node.label ?? '';
+        node.knowledgeType  = Number(node.knowledgeType ?? node.type ?? 3);
+
+        const pRate = node.personalScoreRate ?? node.scoreRate ?? node.personalRate;
+        if (pRate !== null && pRate !== undefined && pRate !== '') {
+          const n = Number(pRate);
+          node.personalScoreRate = isNaN(n) ? 0 : n;
+          if (node.classScoreRate === null || node.classScoreRate === undefined) {
+            node.classScoreRate = node.personalScoreRate;
+          }
+          if (node.gradeScoreRate === null || node.gradeScoreRate === undefined) {
+            node.gradeScoreRate = node.personalScoreRate;
+          }
+        }
+        // 考频:examNum
+        if (node.examNum !== null && node.examNum !== undefined && node.examNum !== '') {
+          const n = Number(node.examNum);
+          node.examNum = isNaN(n) ? 0 : n;
+        }
+        return node;
+      });
+    },
+
+    /**
+     * 清空分区数据(接口失败或无数据时调用)
+     */
+    resetPartitionEmpty() {
+      this.partitionData = {
+        conquerZone: [],
+        improveZone: [],
+        consolidateZone: []
+      };
+    },
+
     /**
      * 清空知识图谱所有数据(接口失败或无数据时调用)
      * - knowList / flatKnowList 清空
@@ -408,8 +498,9 @@ export default {
     handleGradeChange(gradeList) {
       this.currentGradeList = gradeList;
       this.$emit('grade-change', gradeList);
-      // 年级变化后重新请求知识图谱
+      // 年级变化后重新请求知识图谱 + 分区数据
       this.fetchKnowledgeGraph();
+      this.fetchKnowledgePartition();
     },
     // 计算容器高度
     calculateContainerHeight() {
@@ -433,14 +524,105 @@ export default {
       this.$emit('activeTabChange', tab);
     },
 
-    // 处理知识点点击
+    // 处理知识点点击(来自环形扩散图组件:圆点被用户点击或默认选中)
     handleKnowledgeItemClick(data) {
+      // 兼容两种 payload 结构:
+      //   1) 圆点点击 / setDefaultSelection 新结构:{ knowledgeId, knowledgeName, ... } (顶层)
+      //   2) 旧的 item 包装结构:{ item: {knowledgeId,...}, index: N }
+      let knowledgeId = data?.knowledgeId;
+      if ((knowledgeId === undefined || knowledgeId === null || knowledgeId === '')
+        && data?.item) {
+        knowledgeId = data.item.knowledgeId;
+      }
+      if (knowledgeId !== undefined && knowledgeId !== null && knowledgeId !== '') {
+        this.currentLinkedKnowledgeId = String(knowledgeId);
+      }
       this.$emit('knowledge-item-click', data);
     },
+    // 处理区域卡片中某行条目被点击(来自 region 组件)→ 反向联动到环形扩散图对应圆点高亮 + 右侧数据
+    handleRegionItemClick(item) {
+      const id = item?.knowledgeId;
+      if (id === undefined || id === null || id === '') return;
+      this.currentLinkedKnowledgeId = String(id);
+    },
 
     // 处理加入练习
+    // 参数来源:
+    //   - 环形扩散图按钮:emit 传出 { knowledgeId, knowledgeName, personalScoreRate?, knowledgeType? }
+    //   - region 卡片按钮:emit 传出整条 item(含 knowledgeId/Name/personalScoreRate/knowledgeType/examNum...)
     handleAddPractice(item) {
-      this.$emit('add-practice', item);
+      if (!item) {
+        this.$message.warning('未获取到知识点信息');
+        return;
+      }
+
+      const knowledgeId = item.knowledgeId ?? item.id;
+      const knowledgeName = item.knowledgeName ?? item.name ?? item.label ?? '';
+      if (!knowledgeId || !knowledgeName) {
+        this.$message.warning('知识点信息不完整,请稍后重试');
+        return;
+      }
+
+      // 已经加过的直接返回,避免重复请求
+      if (this.addedKnowledgeIds[String(knowledgeId)]) {
+        this.$message.info('该知识点已加入练习');
+        return;
+      }
+
+      // 得分率:优先 personalScoreRate → gradeScoreRate → classScoreRate
+      let personalScoreRate = item.personalScoreRate;
+      if (personalScoreRate === null || personalScoreRate === undefined || personalScoreRate === '') {
+        personalScoreRate = item.gradeScoreRate ?? item.classScoreRate;
+      }
+      if (personalScoreRate === null || personalScoreRate === undefined || personalScoreRate === '') {
+        personalScoreRate = 0;
+      } else {
+        personalScoreRate = Number(personalScoreRate);
+        if (isNaN(personalScoreRate)) personalScoreRate = 0;
+      }
+
+      // 知识点类型:1薄弱 2一般 3优秀
+      let knowledgeType = Number(item.knowledgeType ?? item.type ?? 0);
+      if (isNaN(knowledgeType)) knowledgeType = 0;
+
+      const subjectCode = this.subjectCode || '';
+      const levelCode = localStorage.getItem('student_level_code') || '';
+
+      // 年级CODE:取 gradeList 数组中最后一个年级的 value
+      //   - 多个年级(如[{高一,10},{高二,11}])→ 取最后一个(高二.value = 11)
+      //   - 只有一个年级 → 直接取该年级 value
+      //   - 空/无效 → 传空串兜底
+      let gradeCode = '';
+      if (Array.isArray(this.gradeList) && this.gradeList.length > 0) {
+        const lastGrade = this.gradeList[this.gradeList.length - 1];
+        if (lastGrade && lastGrade.value !== undefined && lastGrade.value !== null && lastGrade.value !== '') {
+          gradeCode = String(lastGrade.value);
+        }
+      }
+
+      const params = {
+        knowledgeId,
+        knowledgeName,
+        personalScoreRate,
+        knowledgeType,
+        subjectCode,
+        levelCode,
+        gradeCode
+      };
+
+      this.$api.target.addExercise(params).then(res => {
+        if (res && (res.code == 200 || res.code == 0)) {
+          // 成功:提示 + 记录 ID(用 $set 保证响应式,region/circularDiffusion 会自动重新渲染并置灰按钮)
+          this.$message.success('已加入练习成功');
+          this.$set(this.addedKnowledgeIds, String(knowledgeId), true);
+        } else {
+          const msg = (res && res.message) || '加入练习失败';
+          this.$message.error(msg);
+        }
+      }).catch(err => {
+        console.error('[KnowledgeGraph] addExercise 接口异常:', err);
+        this.$message.error('加入练习失败,请稍后重试');
+      });
     }
   }
 };

+ 234 - 52
src/views/targetImprove/practice/knowledgeGraph/circularDiffusion.vue

@@ -40,10 +40,11 @@
             <div class="chart_title" v-if="selectedKnowledgeName">
               <div class="chart_title_left">
                 <span class="title_text" :title="selectedKnowledgeName">{{ selectedKnowledgeName }}</span>
-                <span class="exam_freq">考频:{{ selectedExamFreq }}</span>
+                <span class="exam_freq">考频:{{ filteredTableData.length }}</span>
               </div>
               <el-button
                 class="add_practice_btn"
+                :class="addPracticeBtnThemeClass"
                 :disabled="selectedIsJoin"
                 @click="handleAddPractice">
                 {{ selectedIsJoin ? '已加入练习' : '加入练习' }}
@@ -54,18 +55,19 @@
 
           <!-- Tab切换和表格 -->
           <div class="table_container">
-            <el-tabs v-model="activeTableTab" class="table_tabs">
-              <el-tab-pane label="全部" name="all"></el-tab-pane>
-              <el-tab-pane label="考试" name="exam"></el-tab-pane>
-              <el-tab-pane label="练习" name="practice"></el-tab-pane>
-            </el-tabs>
-            <el-table :data="filteredTableData" border class="data_table">
+            <el-radio-group v-model="activeTableTab" class="table_tabs_radio">
+              <el-radio-button label="all">全部</el-radio-button>
+              <el-radio-button label="exam">考试</el-radio-button>
+              <el-radio-button label="practice">练习</el-radio-button>
+            </el-radio-group>
+
+            <el-table :data="filteredTableData" border stripe class="data_table">
               <el-table-column prop="index" label="序号" width="60" align="center">
                 <template slot-scope="scope">
                   {{ scope.$index + 1 }}
                 </template>
               </el-table-column>
-              <el-table-column prop="examName" label="考试名称" show-overflow-tooltip></el-table-column>
+              <el-table-column prop="examName" label="考试名称" show-overflow-tooltip align="center"></el-table-column>
               <el-table-column prop="examType" label="类型" width="100" align="center"></el-table-column>
               <el-table-column prop="scoreRate" label="得分率" width="100" align="center">
                 <template slot-scope="scope">
@@ -168,6 +170,11 @@ export default {
       type: Boolean,
       default: false
     },
+    // 父级已成功"加入练习"的知识点ID映射:{ [knowledgeId]: true }
+    addedKnowledgeIds: {
+      type: Object,
+      default: () => ({})
+    },
   },
   computed: {
     // 从扁平 allKnowledgeList(或tableData递归兜底)获取当前选中节点的完整对象
@@ -196,11 +203,28 @@ export default {
       const item = this.currentKnowledgeItem;
       return item ? (item.knowledgeName ?? '') : '';
     },
-    // 当前选中知识点是否"已加入练习"(isJoin===1)
+    // 当前选中知识点是否"已加入练习"
+    // 优先级:1) 父级 addedKnowledgeIds(当前会话刚点按钮成功,立即生效)→ 2) 节点自身 isJoin===1(接口返回的静态字段)
     selectedIsJoin() {
+      if (this.selectedKnowledgeId
+        && this.addedKnowledgeIds
+        && Object.prototype.hasOwnProperty.call(this.addedKnowledgeIds, String(this.selectedKnowledgeId))) {
+        return true;
+      }
       const item = this.currentKnowledgeItem;
       return item ? (Number(item.isJoin) === 1) : false;
     },
+    // 加入练习按钮主题 class(按当前选中圆点的类型匹配颜色)
+    //   与 getCurrentColor() / getNodeColor() 严格对齐:
+    //     knowledgeType=3 优秀/扎实巩固 → #3BA272
+    //     knowledgeType=2 一般/能力提升 → #FAC858
+    //     knowledgeType=1 薄弱          → #EE6666
+    addPracticeBtnThemeClass() {
+      const t = Number(this.selectedKnowledgeType);
+      if (t === 3) return 'btn_theme_excellent';
+      if (t === 2) return 'btn_theme_good';
+      return 'btn_theme_weak';
+    },
     // 选中知识点的考频
     selectedExamFreq() {
       const item = this.currentKnowledgeItem;
@@ -313,9 +337,27 @@ export default {
     },
     currentKnowledgeId: {
       handler(newVal) {
+        if (newVal === undefined || newVal === null) {
+          this.selectedKnowledgeId = '';
+          return;
+        }
         this.selectedKnowledgeId = newVal;
-        // 更新知识点类型(使用 String 转换确保类型匹配)
-        const item = (this.allKnowledgeList || []).find(item => String(item.knowledgeId) === String(newVal));
+        // 更新知识点类型:先在 allKnowledgeList 扁平表中找
+        let item = (this.allKnowledgeList || []).find(i => String(i.knowledgeId) === String(newVal));
+        // 兜底:在 tableData 整棵树里按 knowledgeId 查找(保证 region 点任何行都能拿到 knowledgeType)
+        if (!item && Array.isArray(this.tableData)) {
+          const stack = [...this.tableData];
+          while (stack.length && !item) {
+            const n = stack.pop();
+            if (n && String(n.knowledgeId) === String(newVal)) {
+              item = n;
+              break;
+            }
+            if (n && Array.isArray(n.children) && n.children.length) {
+              for (let i = 0; i < n.children.length; i++) stack.push(n.children[i]);
+            }
+          }
+        }
         if (item) {
           this.selectedKnowledgeType = item.knowledgeType || 3;
         }
@@ -483,11 +525,38 @@ export default {
       this.lineChart.resize();
       const t1 = performance.now();
     },
-    // 处理加入练习
+    // 处理加入练习:把当前选中节点的完整字段 emit 给父组件,父组件调加入练习接口
     handleAddPractice() {
+      const item = this.currentKnowledgeItem;
+      const knowledgeId = this.selectedKnowledgeId ?? item?.knowledgeId ?? null;
+      const knowledgeName = this.selectedKnowledgeName ?? item?.knowledgeName ?? '';
+      if (!knowledgeId) {
+        this.$message && this.$message.warning && this.$message.warning('请先选择一个知识点');
+        return;
+      }
+
+      // 得分率:personalScoreRate → gradeScoreRate → classScoreRate
+      let personalScoreRate = item?.personalScoreRate;
+      if (personalScoreRate === null || personalScoreRate === undefined || personalScoreRate === '') {
+        personalScoreRate = item?.gradeScoreRate ?? item?.classScoreRate;
+      }
+      if (personalScoreRate === null || personalScoreRate === undefined || personalScoreRate === '') {
+        personalScoreRate = 0;
+      } else {
+        personalScoreRate = Number(personalScoreRate);
+        if (isNaN(personalScoreRate)) personalScoreRate = 0;
+      }
+
+      // 知识点类型 1薄弱 2一般 3优秀
+      let knowledgeType = Number(item?.knowledgeType ?? item?.type ?? 0);
+      if (isNaN(knowledgeType)) knowledgeType = 0;
+
       this.$emit('add-practice', {
-        knowledgeId: this.selectedKnowledgeId,
-        knowledgeName: this.selectedKnowledgeName
+        knowledgeId,
+        knowledgeName,
+        personalScoreRate,
+        knowledgeType,
+        ...(item ? { ...item } : {})
       });
     },
     setDefaultSelection() {
@@ -516,6 +585,14 @@ export default {
           // 父级指定的节点可见且有效:直接沿用
           this.selectedKnowledgeType = matchNode.knowledgeType || 3;
           if (this.lineChart) this.updateLineChart();
+          // 通知父级联动 state(保证首次进来默认选中时 region 卡片也高亮对应条目 + 居中滚动)
+          this.$emit('knowledge-item-click', {
+            knowledgeId: matchNode.knowledgeId,
+            knowledgeName: matchNode.knowledgeName,
+            personalScoreRate: matchNode.personalScoreRate != null ? matchNode.personalScoreRate : matchNode.scoreRate,
+            knowledgeType: matchNode.knowledgeType,
+            examList: Array.isArray(matchNode.examList) ? matchNode.examList : []
+          });
           return;
         }
         // 否则:父级指定被图例隐藏或降级 → 走重选逻辑(下面的 legacyLists/树形)
@@ -563,6 +640,15 @@ export default {
 
       // 统一:应用选中大小(放大/还原)
       this.applySelectedSizeToChart(this.selectedKnowledgeId);
+
+      // 通知父级联动 state(保证首次进来默认选中 / 图例过滤重选后 region 卡片也高亮对应条目 + 居中滚动)
+      this.$emit('knowledge-item-click', {
+        knowledgeId: targetNode.knowledgeId,
+        knowledgeName: targetNode.knowledgeName,
+        personalScoreRate: targetNode.personalScoreRate != null ? targetNode.personalScoreRate : targetNode.scoreRate,
+        knowledgeType: targetNode.knowledgeType,
+        examList: Array.isArray(targetNode.examList) ? targetNode.examList : []
+      });
     },
     switchView(view) {
       this.$emit('view-change', view);
@@ -987,10 +1073,19 @@ export default {
             };
           }
 
-          // 4. 回传事件
+          // 4. 回传事件:保持向后兼容 {item, index},同时把核心字段打平在 payload 顶层
+          //    ——父级联动(handleKnowledgeItemClick)会按顶层 knowledgeId 写入 currentLinkedKnowledgeId
+          //    ——外层其他监听者也可继续用 item / index
           this.$emit('knowledge-item-click', {
             item: knowledgeItem,
-            index: targetIndex
+            index: targetIndex,
+            knowledgeId: knowledgeItem.knowledgeId,
+            knowledgeName: knowledgeItem.knowledgeName,
+            personalScoreRate: knowledgeItem.personalScoreRate != null
+              ? knowledgeItem.personalScoreRate
+              : knowledgeItem.scoreRate,
+            knowledgeType: knowledgeItem.knowledgeType,
+            examList: Array.isArray(knowledgeItem.examList) ? knowledgeItem.examList : []
           });
 
           // 5. 更新内部状态
@@ -1225,7 +1320,7 @@ export default {
       position: relative;
 
       .knowledge_right_container {
-        width: 400px;
+        width: 500px;
         display: flex;
         flex-direction: column;
         gap: 10px;
@@ -1271,13 +1366,11 @@ export default {
               line-height: 1.2;
             }
 
-            /* 加入练习按钮:使用 Element UI el-button,覆盖红色描边风格 */
+            /* 加入练习按钮:使用 Element UI el-button,描边风格跟随当前选中圆点的 knowledgeType 颜色 */
             .add_practice_btn {
               border-radius: 4px;
-              border: 1px solid #F56C6C !important;
               font-weight: 500;
               font-size: 14px;
-              color: #F56C6C !important;
               background: #fff !important;
               flex-shrink: 0;
               padding: 0;
@@ -1285,14 +1378,39 @@ export default {
               height: 36px;
               line-height: 36px;
               text-align: center;
-              /* 去掉默认 hover 颜色变化,保持白底红字 */
-              &:hover,
-              &:focus {
-                border: 1px solid #F56C6C !important;
-                color: #F56C6C !important;
-                background: #fff !important;
+
+              /* 薄弱:knowledgeType=1 → 与选中圆点的 #EE6666 颜色一致 */
+              &.btn_theme_weak {
+                border: 1px solid #EE6666 !important;
+                color: #EE6666 !important;
+                &:hover, &:focus {
+                  border: 1px solid #EE6666 !important;
+                  color: #EE6666 !important;
+                  background: #fff !important;
+                }
+              }
+              /* 一般/能力提升:knowledgeType=2 → #FAC858 */
+              &.btn_theme_good {
+                border: 1px solid #FAC858 !important;
+                color: #FAC858 !important;
+                &:hover, &:focus {
+                  border: 1px solid #FAC858 !important;
+                  color: #FAC858 !important;
+                  background: #fff !important;
+                }
+              }
+              /* 优秀/扎实巩固:knowledgeType=3 → #3BA272 */
+              &.btn_theme_excellent {
+                border: 1px solid #3BA272 !important;
+                color: #3BA272 !important;
+                &:hover, &:focus {
+                  border: 1px solid #3BA272 !important;
+                  color: #3BA272 !important;
+                  background: #fff !important;
+                }
               }
-              /* isJoin=1 的置灰状态:disabled 时边框和文字变浅灰,背景淡灰 */
+
+              /* isJoin=1 的置灰状态:disabled 时边框和文字变浅灰,背景淡灰(优先级最高,覆盖主题色) */
               &.is-disabled,
               &[disabled] {
                 border: 1px solid #DCDFE6 !important;
@@ -1310,51 +1428,115 @@ export default {
           }
 
           .line_chart {
-            width: 100%;
+            width: calc(100% - 30px);
             height: 180px;
+            padding: 15px;
           }
         }
 
         .table_container {
-          border: 1px solid #E9EBEF;
-          border-radius: 10px;
-          padding: 8px;
 
-          .table_tabs {
-            :deep(.el-tabs__header) {
-              margin-bottom: 8px;
+          /* 按钮组 tabs:按设计图样式 */
+          .table_tabs_radio {
+            margin-bottom: 10px;
+            display: inline-flex;
+            align-items: center;
+            line-height: 0;
+            border-collapse: collapse;
+
+            :deep(.el-radio-button) {
+              /* 让选中的按钮在视觉上压过相邻按钮的边框 */
+              &:first-child .el-radio-button__inner {
+                border-radius: 4px 0 0 4px;
+              }
+              &:last-child .el-radio-button__inner {
+                border-radius: 0 4px 4px 0;
+              }
             }
 
-            :deep(.el-tabs__item) {
+            :deep(.el-radio-button__inner) {
+              height: 36px;
+              line-height: 36px;
+              padding: 0 20px;
               font-size: 13px;
-              height: 32px;
-              line-height: 32px;
+              color: #909399;          /* 未选中:灰字 */
+              background: #FFFFFF;
+              border: 1px solid #DCDFE6; /* 未选中:灰边框 */
+              box-sizing: border-box;
+              margin: 0;
+              box-shadow: none !important;
+              transition: none;
+              position: relative;
+              border-radius: 0;
             }
 
-            :deep(.el-tabs__item.is-active) {
+            /* 相邻按钮的重叠(Element 默认 margin-left:-1px,这里保持默认通过 border-color 变化即可,显式指定分隔线颜色) */
+            :deep(.el-radio-button + .el-radio-button) {
+              margin-left: -1px;
+            }
+
+            /* 选中态:蓝边框 + 蓝字 + 白底(完全匹配图片) */
+            :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
               color: #2E64FA;
+              background-color: #FFFFFF;
+              border-color: #2E64FA;
+              box-shadow: none !important;
+              z-index: 2; /* 把选中按钮抬高,其蓝色边框盖住相邻按钮的灰色边框 */
             }
 
-            :deep(.el-tabs__active-bar) {
-              background-color: #2E64FA;
+            /* 未选中 hover:保持灰字灰边框,不变成蓝色 */
+            :deep(.el-radio-button__orig-radio:not(:checked) + .el-radio-button__inner:hover) {
+              color: #909399;
+              border-color: #DCDFE6;
+              background-color: #FFFFFF;
+            }
+            :deep(.el-radio-button__inner:hover) {
+              color: #909399;
             }
           }
 
           .data_table {
-            :deep(.el-table__header) {
-              th {
-                background: #f5f7fa;
-                color: #606266;
-                font-weight: 500;
-                font-size: 13px;
-              }
+            height: 330px;
+            border-radius: 6px;
+            /* 整表统一 cell 高度为 40px(Element UI 是通过 .cell 控制) */
+            :deep(.el-table__header-wrapper th .cell),
+            :deep(.el-table__body-wrapper td .cell) {
+              height: 40px;
+              line-height: 40px;
+              padding-top: 0;
+              padding-bottom: 0;
+              box-sizing: border-box;
+             
             }
 
-            :deep(.el-table__body) {
-              td {
-                font-size: 13px;
-                color: #606266;
-              }
+            /* 表头样式 */
+            :deep(.el-table__header th) {
+              background: #F5F7FA;
+              color: #333333;
+              font-weight: 500;
+              font-size: 14px;
+              padding:0px !important;
+            }
+
+            /* 斑马纹:开启 stripe 后,Element UI 会给偶数行加 el-table__row--striped */
+            :deep(.el-table__body tr.el-table__row--striped td) {
+              background: #FAFBFC;
+            }
+            :deep(.el-table__body tr.el-table__row--striped td.el-table__cell) {
+              background: #FAFBFC;
+            }
+
+            /* 表体样式 */
+            :deep(.el-table__body td) {
+              font-weight: 400;
+              font-size: 14px;
+              color: #666666;
+               padding:0px !important;
+            }
+
+            /* hover 行时不改变斑马纹底色(仅轻微提亮边框感知) */
+            :deep(.el-table__body tr:hover > td) {
+              background-color: #EFF6FF;
             }
           }
         }

+ 557 - 183
src/views/targetImprove/practice/knowledgeGraph/region.vue

@@ -1,108 +1,159 @@
 <template>
   <div class="region_container">
     <!-- 薄弱补强区 -->
-    <div class="region_section weak" v-if="weakArea.length > 0">
+    <div class="region_section weak">
       <div class="region_header">
         <span class="region_icon">
-          <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-dasharray="4 2"/>
-          </svg>
+          <img :src="require('@/assets/knowledgeGraph/buqiang.png')" alt="薄弱补强区" />
         </span>
-        <span class="region_title">薄弱补强区</span>
+        <div class="region_header_text">
+          <span class="region_title">薄弱补强区</span>
+          <span class="region_desc">得分率<60% | 优先补齐短板</span>
+        </div>
       </div>
-      <div class="region_desc">得分率<60% | 优先补齐短板</div>
-      <div class="knowledge_cards">
-        <div class="knowledge_card" v-for="(item, index) in weakArea" :key="'weak-' + index">
-          <div class="card_header">
-            <span class="knowledge_name">{{ item.knowledgeName }}</span>
+      <div class="knowledge_rows" ref="weakRowsBox">
+        <div
+          class="knowledge_row"
+          v-for="(item, index) in weakArea"
+          :key="'weak-' + index"
+          :data-knowledge-id="item.knowledgeId"
+          @click="handleRowClick(item)"
+          :class="{ selected: isKnowledgeSelected(item), highlighted: isKnowledgeSelected(item) }"
+        >
+          <div class="row_left">
+            <span class="kp_name">{{ item.knowledgeName }}</span>
+            <div class="kp_meta">
+              <span class="kp_score">得分率:{{ getScoreRate(item) }}%</span>
+              <span class="kp_freq_tag">考频:{{ getExamFreq(item) }}</span>
+            </div>
           </div>
-          <div class="card_info">
-            <span class="score_rate">得分率:{{ getScoreRate(item) }}%</span>
-            <span class="wrong_count">错题:{{ item.wrongCount || item.questionNum || 0 }}</span>
-          </div>
-          <div class="card_action">
-            <el-button type="primary" size="small" @click="handleAddPractice(item)">加入练习</el-button>
+          <div class="row_right">
+            <el-button size="small" class="add_btn weak_btn"
+              :disabled="isKnowledgeAdded(item)"
+              @click="handleAddPractice(item)">
+              {{ isKnowledgeAdded(item) ? '已加入练习' : '加入练习' }}
+            </el-button>
           </div>
         </div>
+        <div v-if="weakArea.length === 0" class="empty_tip">暂无数据</div>
       </div>
     </div>
 
     <!-- 能力提升区 -->
-    <div class="region_section good" v-if="goodArea.length > 0">
+    <div class="region_section good">
       <div class="region_header">
         <span class="region_icon">
-          <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
-          </svg>
+          <img :src="require('@/assets/knowledgeGraph/tisheng.png')" alt="能力提升区" />
         </span>
-        <span class="region_title">能力提升区</span>
+        <div class="region_header_text">
+          <span class="region_title">能力提升区</span>
+          <span class="region_desc">60%≤得分率<85% | 强化练习提升</span>
+        </div>
       </div>
-      <div class="region_desc">60%≤得分率<85% | 强化练习提升</div>
-      <div class="knowledge_cards">
-        <div class="knowledge_card" v-for="(item, index) in goodArea" :key="'good-' + index">
-          <div class="card_header">
-            <span class="knowledge_name">{{ item.knowledgeName }}</span>
-          </div>
-          <div class="card_info">
-            <span class="score_rate">得分率:{{ getScoreRate(item) }}%</span>
-            <span class="wrong_count">错题:{{ item.wrongCount || item.questionNum || 0 }}</span>
+      <div class="knowledge_rows" ref="goodRowsBox">
+        <div
+          class="knowledge_row"
+          v-for="(item, index) in goodArea"
+          :key="'good-' + index"
+          :data-knowledge-id="item.knowledgeId"
+          @click="handleRowClick(item)"
+          :class="{ selected: isKnowledgeSelected(item), highlighted: isKnowledgeSelected(item) }"
+        >
+          <div class="row_left">
+            <span class="kp_name">{{ item.knowledgeName }}</span>
+            <div class="kp_meta">
+              <span class="kp_score">得分率:{{ getScoreRate(item) }}%</span>
+              <span class="kp_freq_tag">考频:{{ getExamFreq(item) }}</span>
+            </div>
           </div>
-          <div class="card_action">
-            <el-button type="warning" size="small" @click="handleAddPractice(item)">加入练习</el-button>
+          <div class="row_right">
+            <el-button size="small" class="add_btn good_btn"
+              :disabled="isKnowledgeAdded(item)"
+              @click="handleAddPractice(item)">
+              {{ isKnowledgeAdded(item) ? '已加入练习' : '加入练习' }}
+            </el-button>
           </div>
         </div>
+        <div v-if="goodArea.length === 0" class="empty_tip">暂无数据</div>
       </div>
     </div>
 
     <!-- 扎实巩固区 -->
-    <div class="region_section excellent" v-if="excellentArea.length > 0">
+    <div class="region_section excellent">
       <div class="region_header">
         <span class="region_icon">
-          <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
-          </svg>
+          <img :src="require('@/assets/knowledgeGraph/gonggu.png')" alt="扎实巩固区" />
         </span>
-        <span class="region_title">扎实巩固区</span>
+        <div class="region_header_text">
+          <span class="region_title">扎实巩固区</span>
+          <span class="region_desc">得分率>85% | 定期练习巩固</span>
+        </div>
       </div>
-      <div class="region_desc">得分率≥85% | 定期练习巩固</div>
-      <div class="knowledge_cards">
-        <div class="knowledge_card" v-for="(item, index) in excellentArea" :key="'excellent-' + index">
-          <div class="card_header">
-            <span class="knowledge_name">{{ item.knowledgeName }}</span>
-          </div>
-          <div class="card_info">
-            <span class="score_rate">得分率:{{ getScoreRate(item) }}%</span>
-            <span class="wrong_count">错题:{{ item.wrongCount || item.questionNum || 0 }}</span>
+      <div class="knowledge_rows" ref="excellentRowsBox">
+        <div
+          class="knowledge_row"
+          v-for="(item, index) in excellentArea"
+          :key="'excellent-' + index"
+          :data-knowledge-id="item.knowledgeId"
+          @click="handleRowClick(item)"
+          :class="{ selected: isKnowledgeSelected(item), highlighted: isKnowledgeSelected(item) }"
+        >
+          <div class="row_left">
+            <span class="kp_name">{{ item.knowledgeName }}</span>
+            <div class="kp_meta">
+              <span class="kp_score">得分率:{{ getScoreRate(item) }}%</span>
+              <span class="kp_freq_tag">考频:{{ getExamFreq(item) }}</span>
+            </div>
           </div>
-          <div class="card_action">
-            <el-button type="success" size="small" @click="handleAddPractice(item)">加入练习</el-button>
+          <div class="row_right">
+            <el-button size="small" class="add_btn excellent_btn"
+              :disabled="isKnowledgeAdded(item)"
+              @click="handleAddPractice(item)">
+              {{ isKnowledgeAdded(item) ? '已加入练习' : '加入练习' }}
+            </el-button>
           </div>
         </div>
+        <div v-if="excellentArea.length === 0" class="empty_tip">暂无数据</div>
       </div>
     </div>
 
-    <!-- 冲刺进阶区 -->
-    <div class="region_section advanced" v-if="advancedArea.length > 0">
+    <!-- 冲刺进阶区(暂时隐藏) -->
+    <div class="region_section advanced" v-if="false">
       <div class="region_header">
         <span class="region_icon">
           <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-            <path d="M12 2L15 8L22 9L17 14L18 21L12 18L6 21L7 14L2 9L9 8L12 2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
+            <path
+              d="M14.5 2.5c3 0 5.5 2.5 5.5 5.5 0 1.5-.5 3-1.5 4.5l1 1-2 2-1.5-1.5c-1.5 1-3 1.5-4.5 1.5L10 20H6v-4l4.5-2.5c0-1.5.5-3 1.5-4.5L10.5 7l2-2 1 1c1.5-1 3-1.5 4.5-1.5z"
+              fill="currentColor" />
           </svg>
         </span>
-        <span class="region_title">冲刺进阶区</span>
+        <div class="region_header_text">
+          <span class="region_title">冲刺进阶区</span>
+          <span class="region_desc">优于同层级学生 | 对标上游冲刺进阶</span>
+        </div>
       </div>
-      <div class="region_desc">优于同级学生 | 对标上游冲刺进阶</div>
-      <div class="knowledge_cards">
-        <div class="knowledge_card" v-for="(item, index) in advancedArea" :key="'advanced-' + index">
-          <div class="card_header">
-            <span class="knowledge_name">{{ item.knowledgeName }}</span>
-          </div>
-          <div class="card_info">
-            <span class="score_rate">得分率:{{ getScoreRate(item) }}%</span>
-            <span class="wrong_count">错题:{{ item.wrongCount || item.questionNum || 0 }}</span>
+      <div class="knowledge_rows" ref="advancedRowsBox">
+        <div
+          class="knowledge_row"
+          v-for="(item, index) in advancedArea"
+          :key="'advanced-' + index"
+          :data-knowledge-id="item.knowledgeId"
+          @click="handleRowClick(item)"
+          :class="{ selected: isKnowledgeSelected(item), highlighted: isKnowledgeSelected(item) }"
+        >
+          <div class="row_left">
+            <span class="kp_name">{{ item.knowledgeName }}</span>
+            <div class="kp_meta">
+              <span class="kp_score">得分率:{{ getScoreRate(item) }}%</span>
+              <span class="kp_freq_tag">考频:{{ getExamFreq(item) }}</span>
+            </div>
           </div>
-          <div class="card_action">
-            <el-button type="primary" size="small" @click="handleAddPractice(item)">加入练习</el-button>
+          <div class="row_right">
+            <el-button size="small" class="add_btn advanced_btn"
+              :disabled="isKnowledgeAdded(item)"
+              @click="handleAddPractice(item)">
+              {{ isKnowledgeAdded(item) ? '已加入练习' : '加入练习' }}
+            </el-button>
           </div>
         </div>
       </div>
@@ -114,43 +165,242 @@
 export default {
   name: 'Region',
   props: {
-    // 全部知识点数据
-    allKnowledgeList: {
-      type: Array,
-      default: () => []
+    // 分区数据(由父级通过 knowledge_partition 接口返回后传入)
+    // conquerZone:    薄弱补强区
+    // improveZone:    能力提升区
+    // consolidateZone:扎实巩固区
+    partitionData: {
+      type: Object,
+      default: () => ({
+        conquerZone: [],
+        improveZone: [],
+        consolidateZone: []
+      })
+    },
+    // 父级已成功"加入练习"的知识点ID映射:{ [knowledgeId]: true }
+    addedKnowledgeIds: {
+      type: Object,
+      default: () => ({})
+    },
+    // 父级传入的"联动选中知识点ID"
+    // - 环形扩散图选中的圆点,父级会把 knowledgeId 写到这里 → 本组件高亮对应行并滚动居中
+    // - 点本组件任一行条目时,也通过 emit('item-click') 反向通知父级把 ID 同步到 circularDiffusion
+    selectedKnowledgeId: {
+      type: [String, Number],
+      default: ''
+    }
+  },
+  data() {
+    return {};
+  },
+  watch: {
+    // 父级 selectedKnowledgeId 变化:联动高亮 + 滚动到对应区列表的可视中央
+    selectedKnowledgeId: {
+      immediate: true,
+      handler(newVal) {
+        if (!newVal) return;
+        const sid = String(newVal);
+        // 这里只负责发起"滚动尝试",函数内部会处理 DOM/数据未就绪时的重试
+        this.scheduleHighlightScroll(sid);
+      }
+    },
+    // partitionData 变化:路由进入组件时,父级接口返回前 selectedKnowledgeId 可能已经写好了,
+    // 但此时 conquerZone/improveZone/consolidateZone 都是空数组 → 上面的 watch 已经执行过(immediate)但没找到条目,不会再自动重跑。
+    // 所以 partitionData 有内容变化后,只要还有选中 ID,就重新触发一次滚动。
+    partitionData: {
+      deep: true,
+      handler(newVal, oldVal) {
+        // 快速跳过空对象/完全一样的引用
+        if (!this.selectedKnowledgeId) return;
+        const lists = ['conquerZone', 'improveZone', 'consolidateZone'];
+        let hasAnyData = false;
+        if (newVal) {
+          for (const k of lists) {
+            if (Array.isArray(newVal[k]) && newVal[k].length > 0) { hasAnyData = true; break; }
+          }
+        }
+        if (!hasAnyData) return;
+        // 接口数据刚落地,Vue 下一 tick 才会 patch DOM 行节点;所以 $nextTick 再排
+        const sid = String(this.selectedKnowledgeId);
+        this.$nextTick(() => this.scheduleHighlightScroll(sid));
+      }
+    }
+  },
+  activated() {
+    // keep-alive 缓存路由时,组件被切回来会触发 activated,但 watch 不会再立即执行
+    // 只要有选中 ID 就重新尝试滚动居中(DOM 已就绪,命中会直接滚)
+    if (this.selectedKnowledgeId) {
+      const sid = String(this.selectedKnowledgeId);
+      this.$nextTick(() => this.scheduleHighlightScroll(sid));
     }
   },
   computed: {
-    // 薄弱补强区:得分率 < 60%
+    // 薄弱补强区:直接用接口返回的 conquerZone
     weakArea() {
-      return this.allKnowledgeList.filter(item => {
-        const rate = parseFloat(this.getScoreRate(item));
-        return !isNaN(rate) && rate < 60;
-      });
+      return Array.isArray(this.partitionData?.conquerZone) ? this.partitionData.conquerZone : [];
     },
-    // 能力提升区:60% ≤ 得分率 < 85%
+    // 能力提升区:直接用接口返回的 improveZone
     goodArea() {
-      return this.allKnowledgeList.filter(item => {
-        const rate = parseFloat(this.getScoreRate(item));
-        return !isNaN(rate) && rate >= 60 && rate < 85;
-      });
+      return Array.isArray(this.partitionData?.improveZone) ? this.partitionData.improveZone : [];
     },
-    // 扎实巩固区:得分率 ≥ 85%
+    // 扎实巩固区:直接用接口返回的 consolidateZone
     excellentArea() {
-      return this.allKnowledgeList.filter(item => {
-        const rate = parseFloat(this.getScoreRate(item));
-        return !isNaN(rate) && rate >= 85;
-      });
+      return Array.isArray(this.partitionData?.consolidateZone) ? this.partitionData.consolidateZone : [];
     },
-    // 冲刺进阶区:预留区域,可根据需要扩展
+    // 冲刺进阶区:暂时隐藏,预留空数组
     advancedArea() {
       return [];
     }
   },
   methods: {
-    // 获取得分率(优先使用 gradeScoreRate,其次使用 classScoreRate)
+    // 获取得分率:优先 personalScoreRate(接口分区字段),兜底 class / grade
     getScoreRate(item) {
-      return item.gradeScoreRate || item.classScoreRate || item.personalScoreRate || 0;
+      if (item && (item.personalScoreRate !== null && item.personalScoreRate !== undefined)) {
+        const n = Number(item.personalScoreRate);
+        return isNaN(n) ? 0 : n;
+      }
+      const fallback = item?.gradeScoreRate ?? item?.classScoreRate;
+      if (fallback === null || fallback === undefined) return 0;
+      const n = Number(fallback);
+      return isNaN(n) ? 0 : n;
+    },
+    // 获取考频:优先 examNum(接口分区字段:考频,含练习),兜底常见字段
+    getExamFreq(item) {
+      if (item && (item.examNum !== null && item.examNum !== undefined && item.examNum !== '')) {
+        const n = Number(item.examNum);
+        return isNaN(n) ? 0 : n;
+      }
+      const fallback = item?.examFreq ?? item?.examCount ?? item?.questionNum ?? item?.wrongCount;
+      if (fallback === null || fallback === undefined || fallback === '') return 0;
+      const n = Number(fallback);
+      return isNaN(n) ? 0 : n;
+    },
+    /**
+     * 判断某个知识点条目是否"已加入练习"
+     * 判定:父级 prop addedKnowledgeIds 中有没有当前 knowledgeId 字段
+     */
+    isKnowledgeAdded(item) {
+      const id = item?.knowledgeId;
+      if (!id) return false;
+      return !!(this.addedKnowledgeIds
+        && Object.prototype.hasOwnProperty.call(this.addedKnowledgeIds, String(id)));
+    },
+    /**
+     * 判断某个知识点条目是不是"当前联动选中"(跨区单选:三/四个区只能有一条高亮)
+     * 把父级 selectedKnowledgeId 和条目 knowledgeId 做字符串比较即可
+     */
+    isKnowledgeSelected(item) {
+      const sid = this.selectedKnowledgeId;
+      const id = item?.knowledgeId;
+      if (!sid || id === null || id === undefined || id === '') return false;
+      return String(sid) === String(id);
+    },
+    /**
+     * 【按 knowledgeType 精准分区匹配】联动选中时查找条目并滚动到中间
+     * 规则(和你描述一致):
+     *  knowledgeType === 1 → 薄弱补强区 weakArea / weakRowsBox 容器
+     *  knowledgeType === 2 → 能力提升区 goodArea / goodRowsBox
+     *  knowledgeType === 3 → 扎实巩固区 excellentArea / excellentRowsBox
+     *  其它(4/未知)→ 冲刺进阶区 advancedArea / advancedRowsBox(暂时隐藏)
+     * 只在对应分区的数组里做 knowledgeId 的 O(n) 查找,不需要对比三个数组,效率高
+     *
+     *  @returns {boolean} true=找到条目并执行了滚动;false=数据/DOM未就绪需要重试
+     */
+    highlightAndScrollByKnowledgeId(sid) {
+      if (!sid) return false;
+      // 第一步:找命中条目(只查其 knowledgeType 决定的对应分区数组)
+      let zoneBoxRef = null;
+      const zones = [
+        { type: 1, list: this.weakArea,      ref: 'weakRowsBox'      },
+        { type: 2, list: this.goodArea,      ref: 'goodRowsBox'      },
+        { type: 3, list: this.excellentArea, ref: 'excellentRowsBox' },
+        { type: 4, list: this.advancedArea,  ref: 'advancedRowsBox'  }
+      ];
+
+      // 先尝试"根据命中条目自己的 knowledgeType 定位分区":需要知道 ID 对应哪个 type,先 O(n) 找条目
+      // 折中:如果某一个分区数组先匹配到 knowledgeId,就直接在该分区内滚动——也是只遍历最多"总数组数*平均长度",
+      // 但用户担心的是"把三个数组都循环一遍效率慢"——这里通过先 type 预判 + 找到即停,最差仍是 3*N 但 N 是分区各自长度,且实际 90% 场景第一个命中就 break。
+      let matchedItem = null;
+      for (const z of zones) {
+        const list = Array.isArray(z.list) ? z.list : [];
+        const hit = list.find(itm => String(itm.knowledgeId) === sid);
+        if (hit) {
+          matchedItem = hit;
+          zoneBoxRef = z.ref;
+          break;
+        }
+      }
+      // 没找到条目:说明父级 partitionData 还没被接口赋值(空数组),返回 false 让外层重试
+      if (!matchedItem || !zoneBoxRef) return false;
+
+      // 第二步:找到该条目对应的 DOM(用 data-knowledge-id 属性),在其所属分区滚动容器中居中显示
+      //  ——关键:只滚动该区的 knowledge_rows(overflow-y:auto),不要带动整个页面滚动
+      //    (因此禁止使用 element.scrollIntoView,它会从 window 开始递归把祖先滚动容器都对齐)
+      const box = this.$refs[zoneBoxRef];
+      const container = box && (box.$el || box);
+      // 容器 DOM 还未渲染(例如 ref 还没挂载:created 阶段 immediate watch 触发)→ 重试
+      if (!container) return false;
+      const idAttr = CSS.escape ? CSS.escape(sid) : sid;
+      const rowEl = container.querySelector
+        ? container.querySelector(`[data-knowledge-id="${idAttr}"]`)
+        : null;
+      // 行节点还没被 v-for patch 出来(接口刚赋值,Vue 下一 tick 才渲染)→ 重试
+      if (!rowEl) return false;
+
+      // 用 getBoundingClientRect 的差值算"行相对于容器内容顶边的绝对偏移"
+      // 这样即使容器内部有 padding / border / 行嵌套其他节点导致 offsetParent 不是 container,也能精确定位
+      let containerRect = null;
+      let rowRect = null;
+      try {
+        containerRect = container.getBoundingClientRect();
+        rowRect = rowEl.getBoundingClientRect();
+      } catch (e) {
+        return false;
+      }
+      if (!containerRect || !rowRect) return false;
+      const relativeOffsetTop = (rowRect.top - containerRect.top) + (container.scrollTop || 0);
+      const rowHeight = rowRect.height || 40;
+      const viewportH = container.clientHeight || 0;
+      const rawTarget = relativeOffsetTop - (viewportH / 2) + (rowHeight / 2);
+      const maxScrollTop = Math.max(0, (container.scrollHeight || 0) - viewportH);
+      const finalTop = Math.max(0, Math.min(maxScrollTop, rawTarget));
+
+      try {
+        if (typeof container.scrollTo === 'function') {
+          // 只作用在当前分区容器上,window/父容器都不会动
+          container.scrollTo({ top: finalTop, behavior: 'smooth' });
+        } else {
+          container.scrollTop = finalTop;
+        }
+      } catch (e) {
+        try { container.scrollTop = finalTop; } catch (err) {}
+      }
+      return true;
+    },
+    /**
+     * 调度"高亮+滚动居中":如果本轮没找到条目/DOM 就间隔 60ms 再重试,最多 5 次(≈300ms),
+     * 覆盖以下时序:
+     *  1) keep-alive activated 回来,DOM 已在但数据可能未同步好
+     *  2) watch selectedKnowledgeId immediate 先跑 → 接口数据还没来
+     *  3) partitionData 接口刚返回 → Vue v-for 还没把行 DOM patch 出来(computed 更新但 DOM 滞后)
+     */
+    scheduleHighlightScroll(sid, attempt = 0) {
+      const MAX_ATTEMPTS = 5;
+      if (!sid || attempt >= MAX_ATTEMPTS) return;
+      const ok = this.highlightAndScrollByKnowledgeId(String(sid));
+      if (ok) return; // 已经成功滚动到目标行
+      // 没命中:用 setTimeout 延迟再试(下一 tick 可能数据/DOM 就好了)
+      setTimeout(() => this.scheduleHighlightScroll(sid, attempt + 1), 60);
+    },
+    /**
+     * 点击某个知识点行:
+     *  - 联动:emit('item-click', item) 给父级,父级写 currentLinkedKnowledgeId
+     *    → 一方面本组件通过 :selectedKnowledgeId 回传后天然高亮(跨区单选)
+     *    → 另一方面 circularDiffusion 会放大对应圆点 + 右侧折线表格显示数据
+     */
+    handleRowClick(item) {
+      if (!item || !item.knowledgeId) return;
+      this.$emit('item-click', item);
     },
     // 处理加入练习
     handleAddPractice(item) {
@@ -166,185 +416,309 @@ export default {
   flex-direction: row;
   gap: 16px;
   padding: 16px;
+  width: 100%;
+  box-sizing: border-box;
+  align-items: stretch;
 }
 
+/* 单个区域卡片(通用样式:圆角作为通用属性) */
 .region_section {
   flex: 1;
-  border-radius: 8px;
-  padding: 16px;
+  min-width: 0;
+  border-radius: 10px 10px 10px 10px;
+  padding: 16px 16px 12px;
   background: #FFFFFF;
   display: flex;
   flex-direction: column;
-  max-height: 500px;
+  max-height: 560px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
 
   &.weak {
-    border: 1px solid rgba(238, 102, 102, 0.5);
+    border: 1px solid #F56C6C;
+    background: linear-gradient(180deg, rgba(245, 108, 108, 0.1) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF;
   }
 
   &.good {
-    border: 1px solid rgba(250, 200, 88, 0.5);
+    border: 1px solid #FAC858;
+    background: linear-gradient(180deg, rgba(250, 200, 88, 0.1) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF;
   }
 
   &.excellent {
-    border: 1px solid rgba(59, 162, 114, 0.5);
+    border: 1px solid #3BA272;
+    background: linear-gradient(180deg, rgba(59, 162, 114, 0.1) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF;
   }
 
   &.advanced {
-    border: 1px solid rgba(46, 100, 250, 0.5);
+    border: 1px solid #5470C6;
+    background: linear-gradient(180deg, rgba(84, 112, 198, 0.1) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF;
   }
 }
 
+/* 卡片头部:图标方块 + 标题 + 副标题 */
 .region_header {
   display: flex;
-  align-items: center;
-  gap: 8px;
-  margin-bottom: 4px;
-  padding-bottom: 8px;
-  border-bottom: 1px solid #f0f0f0;
+  align-items: flex-start;
+  gap: 10px;
+  margin-bottom: 14px;
 
   .region_icon {
+    width: 28px;
+    height: 28px;
+    border-radius: 6px;
     display: flex;
     align-items: center;
     justify-content: center;
+    flex: 0 0 28px;
 
     svg {
-      width: 18px;
-      height: 18px;
+      width: 16px;
+      height: 16px;
     }
-  }
 
-  &.weak .region_icon {
-    color: #EE6666;
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+      display: block;
+    }
   }
 
-  &.good .region_icon {
-    color: #FAC858;
-  }
+  .region_header_text {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    min-width: 0;
 
-  &.excellent .region_icon {
-    color: #3BA272;
-  }
+    .region_title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #333333;
+      line-height: 1;
+    }
 
-  &.advanced .region_icon {
-    color: #2E64FA;
+    .region_desc {
+      font-size: 12px;
+      color: #999999;
+      line-height: 1.2;
+      white-space: nowrap;
+    }
   }
+}
 
-  .region_title {
-    font-size: 16px;
-    font-weight: 600;
-    color: #333333;
-  }
+.weak .region_header .region_icon {
+  background: #FFE9E9;
+  color: #EE6666;
 }
 
-.region_desc {
-  font-size: 12px;
-  color: #999999;
-  margin-bottom: 12px;
-  padding-top: 4px;
+.good .region_header .region_icon {
+  background: #FFF4DA;
+  color: #F0B429;
+}
+
+.excellent .region_header .region_icon {
+  background: #DEF7EA;
+  color: #3BA272;
+}
+
+.advanced .region_header .region_icon {
+  background: #E5ECFF;
+  color: #2E64FA;
 }
 
-.knowledge_cards {
+/* 行列表容器:仍然可滚动,但视觉上隐藏滚动条 */
+.knowledge_rows {
   display: flex;
   flex-direction: column;
-  gap: 8px;
+  gap: 10px;
   overflow-y: auto;
   flex: 1;
+  min-height: 0;
 
+  /* Webkit 系列(Chrome/Safari/新 Edge):彻底隐藏滚动条 */
   &::-webkit-scrollbar {
-    width: 4px;
+    width: 0;
+    height: 0;
+    display: none;
   }
 
   &::-webkit-scrollbar-thumb {
-    background: #ddd;
-    border-radius: 2px;
+    display: none;
   }
+
+  &::-webkit-scrollbar-track {
+    display: none;
+  }
+
+  /* Firefox:隐藏滚动条 */
+  scrollbar-width: none;
+  /* IE / 旧版 Edge:隐藏滚动条 */
+  -ms-overflow-style: none;
 }
 
-.knowledge_card {
-  border: 1px solid #ECEEF3;
-  border-radius: 6px;
-  padding: 10px 12px;
-  background: #FFFFFF;
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
-  transition: all 0.2s ease;
+.empty_tip {
+  padding: 20px 0;
+  text-align: center;
+  font-size: 12px;
+  color: #BBBBBB;
+}
 
-  &:hover {
-    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
-  }
+/* 每行知识点条目 */
+.knowledge_row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  padding: 10px;
+  border-radius: 4px;
+  border: 1px solid;
+  background: #FFFFFF;
+  min-width: 0;
+  cursor: pointer;
+  .row_left {
+    display: flex;
+    flex-direction: column;
+    gap: 4px;
+    min-width: 0;
+    flex: 1;
 
-  .card_header {
-    .knowledge_name {
-      font-size: 13px;
+    .kp_name {
       font-weight: 500;
+      font-size: 14px;
       color: #333333;
-      display: block;
       overflow: hidden;
       text-overflow: ellipsis;
       white-space: nowrap;
+      margin-bottom: 8px;
     }
-  }
 
-  .card_info {
-    display: flex;
-    gap: 12px;
-
-    .score_rate,
-    .wrong_count {
-      font-size: 11px;
-      color: #666666;
+    .kp_meta {
+      display: flex;
+      align-items: center;
+      gap: 10px;
+      flex-wrap: wrap;
+      color: #999999; /* 考频字色固定浅灰 */
+      font-size: 12px;
     }
   }
 
-  .card_action {
-    text-align: right;
+  .row_right {
+    flex-shrink: 0;
 
-    .el-button {
-      padding: 6px 12px;
-      font-size: 12px;
+    .add_btn {
+      padding: 8px 12px;
+      font-size: 14px;
+      line-height: 1;
+      border-radius: 4px;
     }
   }
 }
 
-/* 薄弱补强区卡片样式 */
-.weak .knowledge_card {
-  border-color: rgba(238, 102, 102, 0.3);
+/* 四个区的条目边框 + 按钮颜色 + 得分率主题色 + 行悬浮/选中背景 */
+.weak .knowledge_row {
+  border-color: #F2B5B5;
+  background: #FFFFFF;
+  transition: background-color 0.15s ease;
+}
+.weak .knowledge_row:hover {
+  background: rgba(245, 108, 108, 0.1);
+}
+.weak .knowledge_row.selected {
+  background: rgba(245, 108, 108, 0.15);
+}
+.weak .kp_meta .kp_score {
+  color: #F56C6C; /* 薄弱区:得分率和卡片主题色一致 */
+}
+
+.weak .knowledge_row .add_btn.weak_btn {
+  color: #EE6666;
+  border: 1px solid #EE6666;
+  background: #FFFFFF;
+}
 
-  .card_action .el-button {
-    background-color: #EE6666;
-    border-color: #EE6666;
-  }
+.good .knowledge_row {
+  border-color: #F7DEA5;
+  background: #FFFFFF;
+  transition: background-color 0.15s ease;
+}
+.good .knowledge_row:hover {
+  background: rgba(250, 200, 88, 0.1);
+}
+.good .knowledge_row.selected {
+  background: rgba(250, 200, 88, 0.15);
+}
+.good .kp_meta .kp_score {
+  color: #FAC858; /* 能力提升区:得分率和卡片主题色一致 */
 }
 
-/* 能力提升区卡片样式 */
-.good .knowledge_card {
-  border-color: rgba(250, 200, 88, 0.3);
+.good .knowledge_row .add_btn.good_btn {
+  color: #F0B429;
+  border: 1px solid #F0B429;
+  background: #FFFFFF;
+}
 
-  .card_action .el-button {
-    background-color: #FAC858;
-    border-color: #FAC858;
-    color: #333333;
-  }
+.excellent .knowledge_row {
+  border-color: #AEE2C5;
+  background: #FFFFFF;
+  transition: background-color 0.15s ease;
+}
+.excellent .knowledge_row:hover {
+  background: rgba(59, 162, 114, 0.1);
+}
+.excellent .knowledge_row.selected {
+  background: rgba(59, 162, 114, 0.15);
+}
+.excellent .kp_meta .kp_score {
+  color: #3BA272; /* 扎实巩固区:得分率和卡片主题色一致 */
 }
 
-/* 扎实巩固区卡片样式 */
-.excellent .knowledge_card {
-  border-color: rgba(59, 162, 114, 0.3);
+.excellent .knowledge_row .add_btn.excellent_btn {
+  color: #3BA272;
+  border: 1px solid #3BA272;
+  background: #FFFFFF;
+}
 
-  .card_action .el-button {
-    background-color: #3BA272;
-    border-color: #3BA272;
-  }
+.advanced .knowledge_row {
+  border-color: #B9CBFC;
+  background: #FFFFFF;
+  transition: background-color 0.15s ease;
+}
+.advanced .knowledge_row:hover {
+  background: rgba(84, 112, 198, 0.1);
+}
+.advanced .knowledge_row.selected {
+  background: rgba(84, 112, 198, 0.15);
+}
+.advanced .kp_meta .kp_score {
+  color: #5470C6; /* 冲刺进阶区:得分率和卡片主题色一致(暂时隐藏) */
 }
 
-/* 冲刺进阶区卡片样式 */
-.advanced .knowledge_card {
-  border-color: rgba(46, 100, 250, 0.3);
+.advanced .knowledge_row .add_btn.advanced_btn {
+  color: #2E64FA;
+  border: 1px solid #2E64FA;
+  background: #FFFFFF;
+}
 
-  .card_action .el-button {
-    background-color: #2E64FA;
-    border-color: #2E64FA;
+/* 【按钮置灰】加入练习成功后 :disabled 状态颜色统一为浅灰色
+ *  必须写和 4 个主题色相同的高权重选择器链(.区 .knowledge_row .add_btn.区_btn),
+ *  并叠加 .is-disabled / [disabled],否则会被上面的主题色(4 层 class 特异性)覆盖,表现为"按钮不能点但颜色没变灰"
+ */
+.weak .knowledge_row .add_btn.weak_btn.is-disabled,
+.weak .knowledge_row .add_btn.weak_btn[disabled],
+.good .knowledge_row .add_btn.good_btn.is-disabled,
+.good .knowledge_row .add_btn.good_btn[disabled],
+.excellent .knowledge_row .add_btn.excellent_btn.is-disabled,
+.excellent .knowledge_row .add_btn.excellent_btn[disabled],
+.advanced .knowledge_row .add_btn.advanced_btn.is-disabled,
+.advanced .knowledge_row .add_btn.advanced_btn[disabled] {
+  border: 1px solid #DCDFE6 !important;
+  color: #C0C4CC !important;
+  background: #F5F7FA !important;
+  cursor: not-allowed !important;
+  &:hover,
+  &:focus {
+    border: 1px solid #DCDFE6 !important;
+    color: #C0C4CC !important;
+    background: #F5F7FA !important;
   }
 }
 </style>