Ver código fonte

知识点图谱开发

吴朋磊 2 dias atrás
pai
commit
b14a802761

+ 0 - 15
src/http/api/konwledgGraph.js

@@ -1,15 +0,0 @@
-import { get, post,downLoadByBlob} from "../common/http";
-import base from "../common/base";
-
-const report = {
-    // 知识点分区
-    knowledgePartition(data){
-        return post(`${base.prefix}/api/v1/studentTarge/knowledge_partition`,data);
-    },
-    // 知识图谱
-    knowledgeGraph(data){
-        return post(`${base.prefix}/api/v1/studentTarge/knowledge_graph`,data);
-    },
-}
-
-export default report;

+ 1 - 1
src/styles/common.scss

@@ -96,7 +96,7 @@ li {
       .user_main_right {
         width: calc(100% - 160px);
         height: 100%;
-        background-color: #fff;
+        // background-color: #fff;
         padding: 10px;
         box-sizing: border-box;
         border-radius: 10px;

+ 232 - 207
src/views/targetImprove/practice/knowledgeGraph.vue

@@ -24,11 +24,12 @@
       </div>
 
       <!-- 环形扩散图组件 -->
-      <circular-diffusion :activeView="activeView" :subjectName="subjectName" :subjectScoreRate="subjectScoreRate"
-        :tableData="displayTableData" :fatalVulnerability="displayFatalVulnerability"
-        :highVulnerability="displayHighVulnerability" :allKnowledgeList="displayAllKnowledgeList"
-        :classLevel="classLevel" :classGroupName="classGroupName" :currentKnowledgeId="currentKnowledgeId"
-        @view-change="handleViewChange" @legend-change="handleLegendChange" @activeTabChange="handleActiveTabChange"
+      <circular-diffusion :subjectName="actualSubjectName" :subjectScoreRate="subjectScoreRate"
+        :tableData="displayTableData"
+        :classLevel="classLevel" :classGroupName="classGroupName"
+        :currentKnowledgeId="currentKnowledgeId || defaultKnowledgeId"
+        @legend-change="handleLegendChange" @activeTabChange="handleActiveTabChange"
+        @add-practice="handleAddPractice"
         @knowledge-item-click="handleKnowledgeItemClick" />
     </div>
 
@@ -41,6 +42,7 @@
 <script>
 import CircularDiffusion from './knowledgeGraph/circularDiffusion.vue';
 import Region from './knowledgeGraph/region.vue';
+import { mapState } from 'vuex';
 
 export default {
   name: 'KnowledgeGraph',
@@ -49,14 +51,6 @@ export default {
     Region
   },
   props: {
-    activeView: {
-      type: String,
-      default: 'graph'
-    },
-    subjectName: {
-      type: String,
-      default: '数学知识点'
-    },
     subjectScoreRate: {
       type: Number,
       default: 85
@@ -102,222 +96,75 @@ export default {
       currentGradeList: [],
       // 年级列表(从localStorage的student_gradesList读取)
       gradeList: [],
-      // 假数据:表格树形结构数据(用于图表渲染)
+      // 中心圆点学科名称(通过 subjectCode 从 student_subjectList 匹配)
+      subjectNameState: '数学知识点',
+      // 默认选中知识点ID:接口返回的 knowList 中"第一棵节点树的最后一个子节点(叶子)"
+      defaultKnowledgeId: '',
+      // 接口返回的真实统计数据
+      graphStats: {
+        allKnowNum: 0,   // 知识点总数
+        weakKnowNum: 0,  // 薄弱数量
+        goodKnowNum: 0   // 待提升数量
+      },
+      // 接口返回的树形知识数据(第一层直接传给环形扩散图)
+      knowList: [],
+      // knowList 扁平化后的一维数组(用于统计、列表展示)
+      flatKnowList: [],
+      // 假数据:表格树形结构数据(用于无接口数据时兜底)
       // knowledgeType: 1=薄弱(红色), 2=良好(黄色), 3=优秀(绿色)
       // gradeScoreRate: 年级得分率,用于判断节点是否有数据
       // examNum: 考试次数,questionNum: 题目数量
-      mockTableData: [
-        {
-          knowledgeId: '1',
-          knowledgeName: '函数与方程',
-          knowledgeType: 1,
-          gradeScoreRate: 45,
-          classScoreRate: 45,
-          examNum: 8,
-          questionNum: 120,
-          scoreRateDiff: -5,
-          children: [
-            {
-              knowledgeId: '1-1',
-              knowledgeName: '一元二次方程',
-              knowledgeType: 1,
-              gradeScoreRate: 35,
-              classScoreRate: 35,
-              examNum: 5,
-              questionNum: 60,
-              scoreRateDiff: -10,
-              children: []
-            },
-            {
-              knowledgeId: '1-2',
-              knowledgeName: '函数图像',
-              knowledgeType: 1,
-              gradeScoreRate: 55,
-              classScoreRate: 55,
-              examNum: 3,
-              questionNum: 40,
-              scoreRateDiff: -2,
-              children: []
-            }
-          ]
-        },
-        {
-          knowledgeId: '2',
-          knowledgeName: '几何图形',
-          knowledgeType: 2,
-          gradeScoreRate: 72,
-          classScoreRate: 72,
-          examNum: 4,
-          questionNum: 80,
-          scoreRateDiff: 3,
-          children: [
-            {
-              knowledgeId: '2-1',
-              knowledgeName: '三角形',
-              knowledgeType: 2,
-              gradeScoreRate: 68,
-              classScoreRate: 68,
-              examNum: 2,
-              questionNum: 30,
-              scoreRateDiff: 2,
-              children: []
-            },
-            {
-              knowledgeId: '2-2',
-              knowledgeName: '圆的性质',
-              knowledgeType: 2,
-              gradeScoreRate: 78,
-              classScoreRate: 78,
-              examNum: 2,
-              questionNum: 50,
-              scoreRateDiff: 5,
-              children: []
-            }
-          ]
-        },
-        {
-          knowledgeId: '3',
-          knowledgeName: '概率统计',
-          knowledgeType: 3,
-          gradeScoreRate: 90,
-          classScoreRate: 90,
-          examNum: 1,
-          questionNum: 25,
-          scoreRateDiff: 8,
-          children: [
-            {
-              knowledgeId: '3-1',
-              knowledgeName: '数据分析',
-              knowledgeType: 3,
-              gradeScoreRate: 92,
-              classScoreRate: 92,
-              examNum: 1,
-              questionNum: 15,
-              scoreRateDiff: 10,
-              children: []
-            },
-            {
-              knowledgeId: '3-2',
-              knowledgeName: '概率计算',
-              knowledgeType: 3,
-              gradeScoreRate: 88,
-              classScoreRate: 88,
-              examNum: 1,
-              questionNum: 10,
-              scoreRateDiff: 6,
-              children: []
-            }
-          ]
-        },
-        {
-          knowledgeId: '4',
-          knowledgeName: '数列',
-          knowledgeType: 2,
-          gradeScoreRate: 65,
-          classScoreRate: 65,
-          examNum: 6,
-          questionNum: 90,
-          scoreRateDiff: 1,
-          children: []
-        },
-        {
-          knowledgeId: '5',
-          knowledgeName: '立体几何',
-          knowledgeType: 1,
-          gradeScoreRate: 50,
-          classScoreRate: 50,
-          examNum: 7,
-          questionNum: 100,
-          scoreRateDiff: -3,
-          children: []
-        },
-        {
-          knowledgeId: '6',
-          knowledgeName: '向量',
-          knowledgeType: 2,
-          gradeScoreRate: 82,
-          classScoreRate: 82,
-          examNum: 3,
-          questionNum: 45,
-          scoreRateDiff: 4,
-          children: []
-        },
-        {
-          knowledgeId: '7',
-          knowledgeName: '导数应用',
-          knowledgeType: 3,
-          gradeScoreRate: 95,
-          classScoreRate: 95,
-          examNum: 2,
-          questionNum: 20,
-          scoreRateDiff: 12,
-          children: []
-        },
-        {
-          knowledgeId: '8',
-          knowledgeName: '不等式',
-          knowledgeType: 1,
-          gradeScoreRate: 58,
-          classScoreRate: 58,
-          examNum: 5,
-          questionNum: 75,
-          scoreRateDiff: -1,
-          children: []
-        }
-      ]
+      mockTableData: []
     };
   },
   computed: {
-    // 统计知识点总数
+    ...mapState('target', ['subjectCode']),
+    // 中心圆点学科名称:优先从 localStorage 的 student_subjectList 匹配 subjectCode
+    actualSubjectName() {
+      return this.subjectNameState || '知识点';
+    },
+    // 统计知识点总数:优先使用接口返回的 allKnowNum
     totalCount() {
+      if (this.graphStats.allKnowNum !== null && this.graphStats.allKnowNum !== undefined) {
+        return this.graphStats.allKnowNum;
+      }
       return this.displayAllKnowledgeList.length;
     },
-    // 薄弱知识点数量(knowledgeType === 1)
+    // 薄弱知识点数量(knowledgeType === 1):优先使用接口返回的 weakKnowNum
     weakCount() {
+      if (this.graphStats.weakKnowNum !== null && this.graphStats.weakKnowNum !== undefined) {
+        return this.graphStats.weakKnowNum;
+      }
       return this.displayAllKnowledgeList.filter(item => item.knowledgeType === 1).length;
     },
-    // 良好/待提升知识点数量(knowledgeType === 2)
+    // 良好/待提升知识点数量(knowledgeType === 2):优先使用接口返回的 goodKnowNum
     goodCount() {
+      if (this.graphStats.goodKnowNum !== null && this.graphStats.goodKnowNum !== undefined) {
+        return this.graphStats.goodKnowNum;
+      }
       return this.displayAllKnowledgeList.filter(item => item.knowledgeType === 2).length;
     },
     // 优秀知识点数量(knowledgeType === 3)
     excellentCount() {
       return this.displayAllKnowledgeList.filter(item => item.knowledgeType === 3).length;
     },
-    // 实际显示的表格数据:props为空时使用假数据
+    // 实际显示的表格数据:接口knowList > props.tableData > 假数据
     displayTableData() {
+      if (this.knowList && this.knowList.length > 0) {
+        return this.knowList;
+      }
       if (this.tableData && this.tableData.length > 0) {
         return this.tableData;
       }
       return this.mockTableData;
     },
-    // 实际显示的零分知识点数据
-    displayFatalVulnerability() {
-      if (this.fatalVulnerability && this.fatalVulnerability.length > 0) {
-        return this.fatalVulnerability;
-      }
-      // 假数据:零分知识点
-      return [
-        { knowledgeId: '1-1', knowledgeName: '一元二次方程', knowledgeType: 1, gradeScoreRate: 35, classScoreRate: 35, examNum: 5, questionNum: 60, scoreRateDiff: -10 },
-        { knowledgeId: '5', knowledgeName: '立体几何', knowledgeType: 1, gradeScoreRate: 50, classScoreRate: 50, examNum: 7, questionNum: 100, scoreRateDiff: -3 },
-        { knowledgeId: '8', knowledgeName: '不等式', knowledgeType: 1, gradeScoreRate: 58, classScoreRate: 58, examNum: 5, questionNum: 75, scoreRateDiff: -1 }
-      ];
-    },
-    // 实际显示的高频错题知识点数据
-    displayHighVulnerability() {
-      if (this.highVulnerability && this.highVulnerability.length > 0) {
-        return this.highVulnerability;
-      }
-      // 假数据:高频错题知识点
-      return [
-        { knowledgeId: '1', knowledgeName: '函数与方程', knowledgeType: 1, gradeScoreRate: 45, classScoreRate: 45, examNum: 8, questionNum: 120, scoreRateDiff: -5 },
-        { knowledgeId: '4', knowledgeName: '数列', knowledgeType: 2, gradeScoreRate: 65, classScoreRate: 65, examNum: 6, questionNum: 90, scoreRateDiff: 1 },
-        { knowledgeId: '2-1', knowledgeName: '三角形', knowledgeType: 2, gradeScoreRate: 68, classScoreRate: 68, examNum: 2, questionNum: 30, scoreRateDiff: 2 },
-        { knowledgeId: '2-2', knowledgeName: '圆的性质', knowledgeType: 2, gradeScoreRate: 78, classScoreRate: 78, examNum: 2, questionNum: 50, scoreRateDiff: 5 }
-      ];
-    },
-    // 实际显示的全部知识点数据
+
+
+    // 实际显示的全部知识点数据:flatKnowList > props.allKnowledgeList > 假数据
     displayAllKnowledgeList() {
+      if (this.flatKnowList && this.flatKnowList.length > 0) {
+        return this.flatKnowList;
+      }
       if (this.allKnowledgeList && this.allKnowledgeList.length > 0) {
         return this.allKnowledgeList;
       }
@@ -340,6 +187,14 @@ export default {
       ];
     }
   },
+  watch: {
+    // 监听学科变化,重新调用知识图谱接口
+    subjectCode(newVal, oldVal) {
+      if (newVal && newVal !== oldVal) {
+        this.fetchKnowledgeGraph();
+      }
+    }
+  },
   mounted() {
     // 从localStorage读取年级列表
     this.loadGradeListFromStorage();
@@ -347,6 +202,9 @@ export default {
     this.calculateContainerHeight();
     // 监听窗口大小变化
     window.addEventListener('resize', this.calculateContainerHeight);
+    // 首次加载知识图谱数据
+
+    this.fetchKnowledgeGraph();
   },
   beforeDestroy() {
     // 移除窗口大小变化监听
@@ -377,10 +235,181 @@ export default {
       }
     },
 
+    /**
+     * 【接口调用】获取知识图谱数据
+     * 参数:
+     *  - levelCode:    学段CODE(localStorage.student_level_code)
+     *  - gradeCodes:   年级codes数组(checkbox选中的value,即使只有1个也要传)
+     *  - subjectCode:  学科code(vuex -> target.subjectCode)
+     * 调用成功后打印返回数据
+     */
+    fetchKnowledgeGraph() {
+      // 读取学段CODE
+      const levelCode = localStorage.getItem('student_level_code') || '';
+      // 年级codes:当前选中的value数组;如未初始化则取gradeList全部value
+      let gradeCodes = this.currentGradeList && this.currentGradeList.length
+        ? this.currentGradeList
+        : (this.gradeList || []).map(g => g.value);
+      // 学科code
+      const subjectCode = this.subjectCode || '';
+
+      const params = {
+        levelCode,
+        gradeCodes,
+        subjectCode
+      };
+      this.$api.target.knowledgeGraph(params).then(res => {
+        if (res.code == 200 && res.data) {
+          const d = res.data;
+          // 1. 统计数据
+          this.graphStats = {
+            allKnowNum:  d.allKnowNum  !== undefined ? Number(d.allKnowNum)  : 0,
+            weakKnowNum: d.weakKnowNum !== undefined ? Number(d.weakKnowNum) : 0,
+            goodKnowNum: d.goodKnowNum !== undefined ? Number(d.goodKnowNum) : 0
+          };
+          // 2. 中心圆点学科名称:通过 subjectCode 从 localStorage.student_subjectList 匹配
+          this.resolveSubjectNameByCode();
+          // 3. knowList 树形数据 + 拍平(并把 personalScoreRate 映射为 classScoreRate / gradeScoreRate 以便图表组件识别)
+          const list = Array.isArray(d.knowList) ? d.knowList : [];
+          this.knowList = this.mapKnowListItem(list);
+          this.flatKnowList = this.flattenKnowList(this.knowList);
+
+          // 4. 默认选中:第一棵节点树的最后一个子节点(叶子节点)
+          const defaultNode = this.findFirstTreeLastLeafNode(this.knowList);
+          this.defaultKnowledgeId = defaultNode ? (defaultNode.knowledgeId ?? '') : '';
+        } else {
+          // 接口失败 / code !== 200 / 无data:清空数据,扩散图和统计数字归零
+          this.resetKnowledgeGraphEmpty();
+        }
+      }).catch(err => {
+        console.error('[KnowledgeGraph] knowledgeGraph 接口异常:', err);
+        this.resetKnowledgeGraphEmpty();
+      });
+    },
+
+    /**
+     * 清空知识图谱所有数据(接口失败或无数据时调用)
+     * - knowList / flatKnowList 清空
+     * - graphStats 三项归零
+     * - defaultKnowledgeId 清空
+     * - subjectNameState 不重置(保持本地匹配兜底名,避免名称闪烁)
+     */
+    resetKnowledgeGraphEmpty() {
+      this.knowList = [];
+      this.flatKnowList = [];
+      this.graphStats = {
+        allKnowNum: 0,
+        weakKnowNum: 0,
+        goodKnowNum: 0
+      };
+      this.defaultKnowledgeId = '';
+    },
+
+    /**
+     * 从树形 knowList 中取"第一棵节点树的最后一个子节点(叶子)"
+     * 规则:取 list[0] 作为根节点,然后一路沿 children[children.length-1] 下钻
+     *       直到某节点没有 children 或 children 为空,返回该节点;
+     *       list 为空返回 null
+     */
+    findFirstTreeLastLeafNode(list) {
+      if (!Array.isArray(list) || list.length === 0) return null;
+      let node = list[0];
+      while (node && Array.isArray(node.children) && node.children.length > 0) {
+        node = node.children[node.children.length - 1];
+      }
+      return node || null;
+    },
+
+    /**
+     * 通过 subjectCode 从 localStorage.student_subjectList 匹配学科名称
+     * 并写入 this.subjectNameState
+     */
+    resolveSubjectNameByCode() {
+      try {
+        const subjectCode = String(this.subjectCode ?? '');
+        if (!subjectCode) return;
+        const raw = localStorage.getItem('student_subjectList');
+        if (!raw) return;
+        const list = JSON.parse(raw);
+        if (!Array.isArray(list) || list.length === 0) return;
+
+        const match = list.find(item => {
+          const val = String(item.value ?? item.code ?? item.subjectCode ?? item.id ?? '');
+          return val === subjectCode;
+        });
+        if (match) {
+          const name = match.label ?? match.name ?? match.subjectName ?? String(match);
+          if (name) {
+            this.subjectNameState = name;
+            return;
+          }
+        }
+        // 兜底:取第一个
+        const first = list[0];
+        if (first) {
+          const fallbackName = first.label ?? first.name ?? first.subjectName ?? String(first);
+          if (fallbackName) this.subjectNameState = fallbackName;
+        }
+      } catch (e) {
+        console.warn('[KnowledgeGraph] 解析 localStorage.student_subjectList 失败:', e);
+      }
+    },
+
+    /**
+     * 递归遍历 knowList,确保每个节点具备环形图需要的字段:
+     * - knowledgeId / knowledgeName / knowledgeType
+     * - personalScoreRate → 同时映射为 classScoreRate、gradeScoreRate(组件读取的是这两个)
+     * - isJoin 原样保留(子组件需要据此显示"已加入练习")
+     * - children 递归处理
+     */
+    mapKnowListItem(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) {
+          // 环形扩散图 generateTreeData 中会按 classLevel / mode 读取 classScoreRate 或 gradeScoreRate
+          // 为了兼容两种路径,把 personalScoreRate 同时复制到两者
+          if (node.classScoreRate === null || node.classScoreRate === undefined) {
+            node.classScoreRate = Number(pRate);
+          }
+          if (node.gradeScoreRate === null || node.gradeScoreRate === undefined) {
+            node.gradeScoreRate = Number(pRate);
+          }
+          node.personalScoreRate = Number(pRate);
+        }
+        if (node.children && node.children.length > 0) {
+          node.children = this.mapKnowListItem(node.children);
+        }
+        return node;
+      });
+    },
+
+    /**
+     * 将 knowList(树形)拍平为一维数组(保留 isJoin 等全部字段)
+     */
+    flattenKnowList(list, out = []) {
+      if (!Array.isArray(list)) return out;
+      list.forEach(item => {
+        out.push(item);
+        if (item.children && item.children.length > 0) {
+          this.flattenKnowList(item.children, out);
+        }
+      });
+      return out;
+    },
+
     // 年级切换(多选)
     handleGradeChange(gradeList) {
       this.currentGradeList = gradeList;
       this.$emit('grade-change', gradeList);
+      // 年级变化后重新请求知识图谱
+      this.fetchKnowledgeGraph();
     },
     // 计算容器高度
     calculateContainerHeight() {
@@ -393,10 +422,6 @@ export default {
         this.containerHeight = Math.max(maxHeight, 400);
       }
     },
-    // 处理视图切换
-    handleViewChange(view) {
-      this.$emit('view-change', view);
-    },
 
     // 处理图例变化
     handleLegendChange(data) {

Diferenças do arquivo suprimidas por serem muito extensas
+ 513 - 310
src/views/targetImprove/practice/knowledgeGraph/circularDiffusion.vue


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff