Explorar el Código

添加组件树和知识点图谱

吴朋磊 hace 3 días
padre
commit
3f99dd4c4a

BIN
src/assets/knowledgeGraph/aiIcon.png


BIN
src/assets/knowledgeGraph/buqiang.png


BIN
src/assets/knowledgeGraph/gonggu.png


BIN
src/assets/knowledgeGraph/jinjie.png


BIN
src/assets/knowledgeGraph/tisheng.png


+ 566 - 0
src/components/CommonTree.vue

@@ -0,0 +1,566 @@
+<template>
+  <!-- 公共树形组件:基于 Element UI el-tree 封装 -->
+  <!-- common_tree_wrapper:用作非 scoped 样式的全局命名空间,防止样式泄漏到其他组件 -->
+  <div class="common_tree_wrapper common_tree">
+    <!--
+      el-tree 核心配置说明:
+      - ref: 用于父组件通过 this.$refs 调用 el-tree 原生方法(如 setCheckedKeys、getCheckedNodes 等)
+      - data: 树形数据源(由父组件通过 props 传入)
+      - props: 配置 el-tree 如何读取数据结构中的字段(label/children)
+      - node-key: 每个树节点的唯一标识字段,用于选中/高亮定位
+      - show-checkbox: 开启多选复选框
+      - highlight-current: 高亮当前点击选中的节点
+      - default-expand-all: 默认展开所有节点
+    -->
+    <el-tree ref="commonTree" class="tree_container" :data="treeData" :props="treeProps" :node-key="nodeKey"
+      :show-checkbox="showCheckbox" :highlight-current="highlightCurrent" :default-expand-all="defaultExpandAll"
+      :default-expanded-keys="defaultExpandedKeys" :current-node-key="currentNodeKey"
+      :expand-on-click-node="expandOnClickNode" @node-click="handleNodeClick" @check="handleNodeCheck">
+      <!--
+        自定义树节点内容插槽(slot-scope)
+        - node: 当前节点的完整数据对象(label、children、data 等)
+        - data: 当前节点绑定的用户数据(即 treeData 中的每一项)
+      -->
+      <span class="custom_tree_node" slot-scope="{ node, data }">
+        <!-- 节点名称区域 -->
+        <span class="node_label" :title="data.label">
+          {{ data.label }}
+          <!-- 右侧计数(可选,数据存在 count 字段时显示) -->
+          <span class="node_count" v-if="data.count !== undefined && data.count !== null">
+            ({{ data.count }})
+          </span>
+        </span>
+
+        <!--
+          AI 讲解按钮(Element UI 文字按钮 type="text")
+          - 默认隐藏(opacity: 0)
+          - 通过父级 hover 显示
+          - @click.stop 阻止冒泡,避免触发树节点点击
+        -->
+        <el-button class="ai_explain_btn" type="text" size="mini" icon="" @click.stop="handleAiExplain(node, data)">
+          <img src="@/assets/knowledgeGraph/aiIcon.png" alt="AI讲解" style="width: 16px; height: 16px;">
+          <span>AI讲解</span>
+        </el-button>
+      </span>
+    </el-tree>
+  </div>
+</template>
+
+<script>
+/**
+ * CommonTree 公共树形组件
+ *
+ * 功能说明:
+ * 1. 基于 Element UI el-tree 二次封装,支持数据传入渲染
+ * 2. 支持多选复选框
+ * 3. 节点点击回传、节点勾选回传
+ * 4. 鼠标悬停节点行显示 "AI讲解" 按钮,点击打印并回传当前节点
+ *
+ * 父组件使用示例:
+ * <CommonTree
+ *   :tree-data="knowledgeTreeData"
+ *   :tree-props="{ label: 'knowledgeName', children: 'children' }"
+ *   node-key="knowledgeId"
+ *   show-checkbox
+ *   @node-click="handleTreeClick"
+ *   @node-check="handleTreeCheck"
+ *   @ai-explain="handleAiExplain"
+ * />
+ */
+export default {
+  name: 'CommonTree',
+
+  // ==================== Props 配置(由父组件传入) ====================
+  props: {
+    /**
+     * 树形数据源
+     * 数据结构示例:
+     * [
+     *   { id: 1, label: '节点1', count: 10, children: [
+     *     { id: 11, label: '子节点1', count: 5 }
+     *   ]}
+     * ]
+     */
+    treeData: {
+      type: Array,
+      default: () => []
+    },
+
+    /**
+     * el-tree 的 props 配置,用于映射数据结构的字段
+     * 默认:{ label: 'label', children: 'children' }
+     * 如使用其他字段名,可传:{ label: 'knowledgeName', children: 'childList' }
+     */
+    treeProps: {
+      type: Object,
+      default: () => ({
+        label: 'label',
+        children: 'children'
+      })
+    },
+
+    /**
+     * 节点唯一标识字段名
+     * 用于选中、高亮、展开等操作的定位
+     */
+    nodeKey: {
+      type: String,
+      default: 'id'
+    },
+
+    /** 是否显示多选复选框 */
+    showCheckbox: {
+      type: Boolean,
+      default: false
+    },
+
+    /** 是否高亮当前点击的节点 */
+    highlightCurrent: {
+      type: Boolean,
+      default: true
+    },
+
+    /** 是否默认展开所有节点 */
+    defaultExpandAll: {
+      type: Boolean,
+      default: true
+    },
+
+    /**
+     * 默认展开的节点 key 数组
+     * 与 defaultExpandAll 互斥使用
+     */
+    defaultExpandedKeys: {
+      type: Array,
+      default: () => []
+    },
+
+    /** 当前高亮选中的节点 key */
+    currentNodeKey: {
+      type: [String, Number],
+      default: ''
+    },
+
+    /**
+     * 点击节点时是否展开/收起
+     * false:只有点击小箭头才展开;点击节点文字只触发选中
+     * true:点击节点文字也会触发展开/收起
+     */
+    expandOnClickNode: {
+      type: Boolean,
+      default: false
+    }
+  },
+
+  // ==================== 数据状态 ====================
+  data() {
+    return {
+      // 当前选中的节点数据(单选时使用)
+      currentSelectedNode: null
+    };
+  },
+
+  // ==================== 方法 ====================
+  methods: {
+    /**
+     * 树节点点击事件(单选)
+     * @param {Object} data  - 当前节点绑定的用户数据
+     * @param {Object} node  - el-tree 节点对象(包含 level、parent 等信息)
+     * 触发事件:this.$emit('node-click', data, node)
+     */
+    handleNodeClick(data, node) {
+      this.currentSelectedNode = data;
+      this.$emit('node-click', data, node);
+    },
+
+    /**
+     * 树节点勾选事件(多选,showCheckbox=true 时生效)
+     * @param {Object} data             - 当前勾选/取消的节点数据
+     * @param {Object} checkedInfo      - 勾选状态详情
+     * @param {Array}  checkedInfo.checkedNodes       - 所有被勾选的节点对象数组
+     * @param {Array}  checkedInfo.checkedKeys        - 所有被勾选的节点 key 数组(含半选)
+     * @param {Array}  checkedInfo.halfCheckedKeys    - 半选状态的节点 key 数组
+     */
+    handleNodeCheck(data, checkedInfo) {
+      this.$emit('node-check', data, checkedInfo);
+    },
+
+    /**
+     * AI 讲解按钮点击事件
+     * 1. 打印当前节点数据(方便调试)
+     * 2. 通过 $emit 回传给父组件处理
+     *
+     * @param {Object} node  - el-tree 节点对象
+     * @param {Object} data  - 当前节点绑定的用户数据
+     */
+    handleAiExplain(node, data) {
+      // ===== 打印调试信息 =====
+      console.log('========== [CommonTree] AI讲解点击 ==========');
+      console.log('当前节点数据 data:', data);
+      console.log('当前节点对象 node:', node);
+      console.log('节点标签 label:', data.label || (data[this.treeProps.label]));
+      console.log('节点唯一键 key:', data[this.nodeKey]);
+      console.log('节点层级 level:', node.level);
+      console.log('是否叶子节点 isLeaf:', node.isLeaf);
+      console.log('===============================================');
+
+      // ===== 回传给父组件 =====
+      this.$emit('ai-explain', {
+        nodeData: data,           // 当前节点的用户数据
+        nodeInstance: node,       // el-tree 节点实例
+        nodeKey: data[this.nodeKey], // 当前节点 key
+        nodeLabel: data.label || (data[this.treeProps.label]) // 当前节点显示名称
+      });
+    },
+
+    /**
+     * 对外暴露方法:获取所有勾选的节点(仅多选模式 showCheckbox=true 有效)
+     * @param {Boolean} leafOnly - 是否只返回叶子节点
+     * @returns {Array} 选中的节点对象数组
+     */
+    getCheckedNodes(leafOnly = false) {
+      return this.$refs.commonTree.getCheckedNodes(leafOnly);
+    },
+
+    /**
+     * 对外暴露方法:获取所有勾选节点的 key 数组
+     * @param {Boolean} leafOnly
+     * @returns {Array} 选中的节点 key 数组
+     */
+    getCheckedKeys(leafOnly = false) {
+      return this.$refs.commonTree.getCheckedKeys(leafOnly);
+    },
+
+    /**
+     * 对外暴露方法:通过 keys 设置哪些节点被选中
+     * @param {Array} keys       - 要选中的节点 key 数组
+     * @param {Boolean} leafOnly - 是否只设置叶子节点
+     */
+    setCheckedKeys(keys, leafOnly = false) {
+      this.$refs.commonTree.setCheckedKeys(keys, leafOnly);
+    },
+
+    /**
+     * 对外暴露方法:清空所有选中节点
+     */
+    clearChecked() {
+      this.$refs.commonTree.setCheckedKeys([]);
+    },
+
+    /**
+     * 对外暴露方法:设置当前高亮的节点
+     * @param {String|Number} key - 节点 key
+     */
+    setCurrentKey(key) {
+      this.$refs.commonTree.setCurrentKey(key);
+    }
+  }
+};
+</script>
+
+<!--
+  ==========================================================================
+  样式分两块:
+  1. 下面这个不带 scoped 的 style 块:专门覆盖 Element UI 原生类名(连接线、padding、间距、选中态)
+     - scoped + :deep 在 el-tree 深层级嵌套下经常不生效,改用非 scoped 搭配组件唯一根类
+       .common_tree_wrapper 做命名空间隔离,不会污染全局其他 el-tree
+  2. 后面带 scoped 的 style 块:放自定义节点内容(custom_tree_node / node_label / ai_explain_btn 等)
+  ==========================================================================
+-->
+<style lang="scss">
+/* ============== 可配置常量 ============== */
+$line_color: #DCDFE6; // 连接线颜色(虚线)
+$line_style: dashed; // 连接线样式
+$line_width: 1px; // 连接线宽度
+$indent_size: 35px; // 每层缩进
+$node_height: 36px; // 单行节点高度
+$node_padding_l: 15px; // 节点左侧 padding (用户要求:15px)
+$node_gap: 10px; // 节点上下间距   (用户要求:10px)
+$label_color: #333333; // 节点文字       (用户要求)
+$label_active: #2E64FA; // 选中节点文字    (用户要求)
+$label_fs: 14px; // 文字字号       (用户要求)
+$label_fw: 400; // 文字字重       (用户要求)
+$btn_color: #4F3EFF; // AI讲解按钮颜色  (用户要求)
+
+/* =========================================================
+   命名空间:.common_tree_wrapper(只影响本组件)
+   ========================================================= */
+.common_tree_wrapper {
+  width: 100%;
+  height: 100%;
+  box-sizing: border-box;
+  padding: 8px 0;
+
+  /* ===================== 基础 ===================== */
+  .tree_container,
+  .el-tree {
+    width: 100%;
+    background: transparent;
+  }
+
+  /* ===================== 节点行样式 =====================
+     padding-left: 15px,上下间距 10px,字体 400/14/#333
+  */
+  .el-tree-node__content {
+    padding-left: 0 !important;
+    padding-right: 12px;
+    height: $node_height;
+    line-height: $node_height;
+    background: transparent;
+    border-radius: 4px;
+    margin-top: $node_gap;
+    margin-bottom: $node_gap;
+    font-weight: $label_fw;
+    font-size: $label_fs;
+    color: $label_color;
+
+    &:hover {
+      background-color: #f0f7ff;
+    }
+  }
+
+  /* 第一级也统一 15px */
+  .el-tree>.el-tree-node>.el-tree-node__content {
+    padding-left: $node_padding_l !important;
+  }
+
+  /* ===================== 自定义展开收起图标样式 ===================== */
+  .el-tree-node__expand-icon {
+    width: 16px;
+    height: 16px;
+    display: flex !important;
+    align-items: center !important;
+    justify-content: center !important;
+    cursor: pointer;
+    margin-right: 8px;
+    padding: 0 !important;
+    border: none !important;
+    /* 去掉Element UI默认的transform旋转过渡动画,直接切换图标 */
+    transform: none !important;
+    transition: none !important;
+  }
+  /* 去掉展开状态的transform旋转 */
+  .el-tree-node__expand-icon.expanded {
+    transform: none !important;
+    -webkit-transform: none !important;
+  }
+
+  /* 隐藏Element UI默认的箭头伪元素 */
+  .el-tree-node__expand-icon::before {
+    display: none !important;
+  }
+
+  /* 收起状态:显示加号 icon_jia */
+  .el-tree-node__expand-icon:not(.expanded):not(.is-leaf)::after {
+    content: '\e64e';
+    font-family: 'iconfont' !important;
+    font-size: 16px;
+    color: #E0E3E9;
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    line-height: 16px;
+    text-align: center;
+  }
+
+  /* 展开状态:显示减号 icon_jian */
+  .el-tree-node__expand-icon.expanded:not(.is-leaf)::after {
+    content: '\e64f';
+    font-family: 'iconfont' !important;
+    font-size: 16px;
+    color: #2E64FA;
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    line-height: 16px;
+    text-align: center;
+
+  }
+
+  /* 叶子节点的占位符,保持对齐一致 */
+  .el-tree-node__expand-icon.is-leaf {
+    width: 16px !important;
+    height: 16px !important;
+    display: inline-block !important;
+    cursor: default;
+    visibility: hidden;
+    margin-right: 8px;
+    padding: 0 !important;
+  }
+
+  /* ===================== 虚线连接线(核心) ===================== */
+
+  /* 1. 竖虚线:画在 children 容器的左侧,贯穿所有子节点 */
+  .el-tree-node .el-tree-node__children {
+    overflow: visible;
+    position: relative;
+    margin-left: 0;
+    padding-left: $indent_size; // 层级缩进
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      bottom: $node_gap + $node_height / 2; // 末端停在最后一个节点"半高"处,和横线接上
+      left: 18px; // 竖线 x 坐标
+      width: $line_width;
+      border-left: $line_width $line_style $line_color;
+    }
+  }
+
+  /* 2. 横虚线:画在每个节点行的左侧,从竖线方向延伸到文字左边 */
+  .el-tree-node {
+    position: relative;
+
+    >.el-tree-node__content::before {
+      content: '';
+      position: absolute;
+      top: 50%;
+      left: -($indent_size - 18px); // 起点对齐竖线
+      width: $indent_size - 20px; // 横线长度
+      height: 0;
+      border-top: $line_width $line_style $line_color;
+      pointer-events: none;
+    }
+  }
+
+  /* 3. 第一级节点不画横线 */
+  .el-tree>.el-tree-node>.el-tree-node__content::before {
+    display: none;
+  }
+
+  /* 4. 最后一个子节点:竖线在横线之下的部分用白色盖掉,形成"L"拐角 */
+  .el-tree-node>.el-tree-node__children>.el-tree-node:last-child {
+    >.el-tree-node__content::after {
+      content: '';
+      position: absolute;
+      top: 50%;
+      bottom: -$node_gap;
+      left: 18px - $indent_size; // 与竖线同 x
+      width: $line_width + 2px;
+      background: #fff; // 白色遮挡(面板必须是白色背景)
+      z-index: 1;
+    }
+  }
+
+  /* ===================== 选中 & 复选框 ===================== */
+  .el-tree-node.is-current>.el-tree-node__content {
+    background-color: #e6f0ff;
+    color: $label_active; // 让文字内容(含原生 el-tree label 模式)也变色
+
+    /* 额外命中自定义 slot 中的 .node_label,防止 scoped 里写的 #333 覆盖掉选中色 */
+    .node_label {
+      color: $label_active !important;
+      font-weight: $label_fw;
+    }
+  }
+
+  .el-checkbox {
+    margin-right: 6px;
+  }
+
+  /* ===================== AI 讲解按钮(Element UI 文字按钮) =====================
+     字体 400 / 14 / #4F3EFF;opacity 0 默认隐藏,hover 节点行显示
+  */
+
+  .ai_explain_btn.el-button--text {
+    display: inline-flex !important;
+    align-items: center;
+    justify-content: center;
+    gap: 4px;
+    padding: 4px 6px !important;
+    height: auto !important;
+    min-height: 24px;
+    margin-left: 8px;
+    opacity: 0;
+    transition: opacity 0.2s ease;
+    white-space: nowrap;
+    flex-shrink: 0;
+    position: relative;
+    z-index: 3;
+    font-weight: 400; // 用户要求
+    font-size: 14px; // 用户要求
+    color: $btn_color !important; // 用户要求:#4F3EFF
+
+    // 确保内部img和span都居中对齐
+    img {
+      display: inline-block;
+      vertical-align: middle;
+      flex-shrink: 0;
+      margin-right: 4px;
+    }
+    span {
+      display: inline-block;
+      vertical-align: middle;
+      line-height: 1;
+    }
+  }
+
+  .ai_explain_btn.el-button--text:hover {
+    color: darken($btn_color, 10%) !important;
+  }
+
+  .ai_explain_btn .ai_icon {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    line-height: 16px;
+    text-align: center;
+    font-size: 10px;
+    font-weight: 600;
+    color: #fff;
+    background: $btn_color;
+    border-radius: 3px;
+    font-style: normal;
+    flex-shrink: 0;
+  }
+
+  .ai_explain_btn.el-button--text:hover .ai_icon {
+    background: darken($btn_color, 10%);
+  }
+
+  /* hover 节点行时,让 AI 讲解按钮淡入 */
+  .el-tree-node__content:hover .ai_explain_btn.el-button--text {
+    opacity: 1 !important;
+  }
+}
+</style>
+
+<!-- ===============================================================
+     以下 scoped 样式块:只处理"自定义节点内容"(我们自己模板里写的)
+     类名是自定义的,能被 scoped 正常命中,无需 :deep
+   =============================================================== -->
+<style lang="scss" scoped>
+.custom_tree_node {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex: 1;
+  padding-right: 8px;
+  height: 36px;
+  line-height: 36px;
+  overflow: hidden;
+  position: relative;
+  z-index: 2;
+
+  /* ===== 节点文字(用户要求:400 / 14px / #333333) ===== */
+  .node_label {
+    flex: 1;
+    display: inline-flex;
+    align-items: center;
+    font-weight: 400;
+    font-size: 14px;
+    color: #333333;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+
+    /* 计数(60):灰色小字 */
+    .node_count {
+      color: #909399;
+      margin-left: 4px;
+      font-size: 13px;
+    }
+  }
+}
+</style>

+ 468 - 40
src/views/targetImprove/practice/knowledgeGraph.vue

@@ -1,40 +1,97 @@
 <template>
   <div class="knowledge_graph" ref="knowledgeGraph" :style="{ maxHeight: containerHeight + 'px' }">
-    <!-- 环形扩散图组件 -->
-    <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"
-      @knowledge-item-click="handleKnowledgeItemClick"
-    />
+
+    <div class="graph_top">
+      <!-- 顶部信息模块 -->
+      <div class="knowledge_header">
+        <div class="header_top">
+          <span class="header_title">我的知识图谱</span>
+          <!-- 年级选择标签,只有一个年级时不显示 -->
+          <el-checkbox-group v-if="gradeList && gradeList.length > 1" v-model="currentGradeList" @change="handleGradeChange">
+            <el-checkbox v-for="grade in gradeList" :key="grade.value" :label="grade.value">
+              {{ grade.label }}
+            </el-checkbox>
+          </el-checkbox-group>
+        </div>
+        <div class="header_stats">
+          <span class="stats_text">
+            你的知识图谱共包含 <span class="stats_num">{{ totalCount }}</span> 个知识点。当前
+            <span class="stats_weak">红色薄弱知识点 {{ weakCount }}</span> 个,
+            <span class="stats_good">黄色待提升知识点 {{ goodCount }}</span> 个,
+            <span class="stats_excellent">坚持练习,把全部知识点都进阶为绿色已掌握状态吧!</span>
+          </span>
+        </div>
+      </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"
+        @knowledge-item-click="handleKnowledgeItemClick" />
+    </div>
+
 
     <!-- 区域展示组件 -->
-    <region
-      :allKnowledgeList="displayAllKnowledgeList"
-      @add-practice="handleAddPractice"
-    />
+    <region :allKnowledgeList="displayAllKnowledgeList" @add-practice="handleAddPractice" />
+
+    <!-- ==================== 测试:CommonTree 公共树形组件 ==================== -->
+    <div class="tree_test_section">
+      <div class="tree_test_title">
+        <span>📋 公共树形组件(CommonTree)测试区域</span>
+        <el-button-group>
+          <el-button size="mini" type="primary" @click="testGetChecked">获取选中节点</el-button>
+          <el-button size="mini" @click="testClearChecked">清空选中</el-button>
+        </el-button-group>
+      </div>
+
+      <!-- 左侧树形组件 -->
+      <div class="tree_test_body">
+        <div class="tree_panel">
+          <CommonTree
+            ref="treeTestRef"
+            :tree-data="mockTreeData"
+            :tree-props="{ label: 'label', children: 'children' }"
+            node-key="id"
+            show-checkbox
+            :default-expand-all="false"
+            @node-click="handleTestNodeClick"
+            @node-check="handleTestNodeCheck"
+            @ai-explain="handleTestAiExplain"
+          />
+        </div>
+
+        <!-- 右侧回传日志面板 -->
+        <div class="tree_log_panel">
+          <div class="log_title">回传事件日志:</div>
+          <div class="log_list">
+            <div v-for="(log, idx) in eventLogs" :key="idx" class="log_item" :class="'log-' + log.type">
+              <div class="log_tag">[{{ log.tag }}]</div>
+              <div class="log_time">{{ log.time }}</div>
+              <pre class="log_content">{{ log.msg }}</pre>
+            </div>
+            <div v-if="eventLogs.length === 0" class="log_empty">
+              请点击左侧树节点 / 勾选复选框 / 悬停后点"AI讲解"按钮 ↖
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 
 <script>
 import CircularDiffusion from './knowledgeGraph/circularDiffusion.vue';
 import Region from './knowledgeGraph/region.vue';
+import CommonTree from '@/components/CommonTree.vue';
 
 export default {
   name: 'KnowledgeGraph',
   components: {
     CircularDiffusion,
-    Region
+    Region,
+    CommonTree
   },
   props: {
     activeView: {
@@ -86,6 +143,92 @@ export default {
     return {
       // 容器最大高度
       containerHeight: 600,
+      // 当前选中的年级列表(多选)
+      currentGradeList: ['grade3'],
+      // 年级列表
+      gradeList: [
+        { label: '初一', value: 'grade1' },
+        { label: '初二', value: 'grade2' },
+        { label: '初三', value: 'grade3' }
+      ],
+      // CommonTree 事件日志列表(用于右侧回传展示)
+      eventLogs: [],
+      // ========== 测试 CommonTree 组件的树形假数据 ==========
+      // 结构:id / label / count(可选)/ children(可选)
+      mockTreeData: [
+        {
+          id: 'chinese',
+          label: '语言文学应用',
+          count: 60,
+          children: [
+            {
+              id: 'modern-reading',
+              label: '现代文阅读',
+              count: 60,
+              children: [
+                { id: 'discourse', label: '论述类文本阅读', count: 60 },
+                { id: 'practical', label: '实用类文本阅读', count: 60 },
+                { id: 'literary', label: '文学类文本阅读', count: 60 }
+              ]
+            },
+            {
+              id: 'ancient-reading',
+              label: '古代诗文阅读',
+              count: 60,
+              children: [
+                { id: 'classical-chinese', label: '文言文阅读', count: 60 },
+                { id: 'ancient-poetry', label: '古代诗歌阅读', count: 60 },
+                { id: 'water-resource', label: '水资源跨区域调配对区域发展的影响', count: 60 }
+              ]
+            },
+            {
+              id: 'whole-book',
+              label: '整本书阅读',
+              count: 60,
+              children: [
+                {
+                  id: 'literary-classics',
+                  label: '文学名著',
+                  count: 60,
+                  children: [
+                    { id: 'luoshen', label: '《洛神赋》', count: 60 },
+                    { id: 'tengwang', label: '《滕王阁序》', count: 60 }
+                  ]
+                },
+                { id: 'culture-classics', label: '文化经典', count: 60 },
+                { id: 'academic-works', label: '学术著作', count: 60 }
+              ]
+            }
+          ]
+        },
+        {
+          id: 'math',
+          label: '数学知识体系',
+          count: 140,
+          children: [
+            {
+              id: 'math-function',
+              label: '函数与方程',
+              count: 30,
+              children: [
+                { id: 'quadratic', label: '一元二次方程', count: 20 },
+                { id: 'function-graph', label: '函数图像', count: 10 }
+              ]
+            },
+            {
+              id: 'math-geometry',
+              label: '几何图形',
+              count: 50,
+              children: [
+                { id: 'triangle', label: '三角形', count: 20 },
+                { id: 'circle-prop', label: '圆的性质', count: 30 }
+              ]
+            },
+            { id: 'math-probability', label: '概率统计', count: 30 },
+            { id: 'math-solid', label: '立体几何', count: 30 }
+          ]
+        }
+      ],
       // 假数据:表格树形结构数据(用于图表渲染)
       // knowledgeType: 1=薄弱(红色), 2=良好(黄色), 3=优秀(绿色)
       // gradeScoreRate: 年级得分率,用于判断节点是否有数据
@@ -252,6 +395,22 @@ export default {
     };
   },
   computed: {
+    // 统计知识点总数
+    totalCount() {
+      return this.displayAllKnowledgeList.length;
+    },
+    // 薄弱知识点数量(knowledgeType === 1)
+    weakCount() {
+      return this.displayAllKnowledgeList.filter(item => item.knowledgeType === 1).length;
+    },
+    // 良好/待提升知识点数量(knowledgeType === 2)
+    goodCount() {
+      return this.displayAllKnowledgeList.filter(item => item.knowledgeType === 2).length;
+    },
+    // 优秀知识点数量(knowledgeType === 3)
+    excellentCount() {
+      return this.displayAllKnowledgeList.filter(item => item.knowledgeType === 3).length;
+    },
     // 实际显示的表格数据:props为空时使用假数据
     displayTableData() {
       if (this.tableData && this.tableData.length > 0) {
@@ -319,6 +478,11 @@ export default {
     window.removeEventListener('resize', this.calculateContainerHeight);
   },
   methods: {
+    // 年级切换(多选)
+    handleGradeChange(gradeList) {
+      this.currentGradeList = gradeList;
+      this.$emit('grade-change', gradeList);
+    },
     // 计算容器高度
     calculateContainerHeight() {
       if (this.$refs.knowledgeGraph) {
@@ -334,25 +498,114 @@ export default {
     handleViewChange(view) {
       this.$emit('view-change', view);
     },
-    
+
     // 处理图例变化
     handleLegendChange(data) {
       this.$emit('legend-change', data);
     },
-    
+
     // 处理tab切换
     handleActiveTabChange(tab) {
       this.$emit('activeTabChange', tab);
     },
-    
+
     // 处理知识点点击
     handleKnowledgeItemClick(data) {
       this.$emit('knowledge-item-click', data);
     },
-    
+
     // 处理加入练习
     handleAddPractice(item) {
       this.$emit('add-practice', item);
+    },
+
+    // ==================== CommonTree 组件测试方法 ====================
+
+    /**
+     * 追加日志到右侧面板
+     * @param {String} type - 日志类型(info/warn/success/error)
+     * @param {String} tag  - 显示标签
+     * @param {String} msg  - 日志内容
+     */
+    appendLog(type, tag, msg) {
+      const now = new Date();
+      const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
+      this.eventLogs.unshift({ type, tag, time, msg });
+      // 只保留最近10条日志
+      if (this.eventLogs.length > 10) this.eventLogs.pop();
+    },
+
+    /**
+     * 【事件回传1】树节点点击(单选)
+     * @param {Object} data - 当前节点用户数据
+     * @param {Object} node - el-tree 节点实例
+     */
+    handleTestNodeClick(data, node) {
+      const logMsg =
+        `节点ID: ${data.id}\n` +
+        `节点名称: ${data.label}\n` +
+        `节点层级: ${node.level}\n` +
+        `是否叶子: ${node.isLeaf ? '是' : '否'}\n` +
+        `计数count: ${data.count ?? '无'}\n` +
+        `\n完整data: ${JSON.stringify(data, null, 2)}`;
+      console.log('%c[KnowledgeGraph] node-click 回传数据: ', 'background:#2E64FA;color:#fff;padding:4px 8px;border-radius:4px;', { data, node });
+      this.appendLog('info', '节点点击', logMsg);
+    },
+
+    /**
+     * 【事件回传2】复选框勾选变化(多选)
+     * @param {Object} data        - 当前操作的节点数据
+     * @param {Object} checkedInfo - 勾选状态详情 { checkedNodes, checkedKeys, halfCheckedKeys }
+     */
+    handleTestNodeCheck(data, checkedInfo) {
+      const selectedLabels = checkedInfo.checkedNodes.map(n => n.label).join('、') || '(无)';
+      const logMsg =
+        `当前操作节点: ${data.label}\n` +
+        `已勾选节点数: ${checkedInfo.checkedNodes.length}\n` +
+        `已勾选key: [${checkedInfo.checkedKeys.join(', ')}]\n` +
+        `半选key: [${(checkedInfo.halfCheckedKeys || []).join(', ') || '无'}]\n` +
+        `\n已勾选节点名称: ${selectedLabels}`;
+      console.log('%c[KnowledgeGraph] node-check 回传数据: ', 'background:#3BA272;color:#fff;padding:4px 8px;border-radius:4px;', { data, checkedInfo });
+      this.appendLog('success', '复选框勾选', logMsg);
+    },
+
+    /**
+     * 【事件回传3】AI讲解按钮点击
+     * @param {Object} payload { nodeData, nodeInstance, nodeKey, nodeLabel }
+     */
+    handleTestAiExplain(payload) {
+      const { nodeData, nodeKey, nodeLabel } = payload;
+      const logMsg =
+        `节点Key: ${nodeKey}\n` +
+        `节点名称: ${nodeLabel}\n` +
+        `节点层级: ${payload.nodeInstance.level}\n` +
+        `计数count: ${nodeData.count ?? '无'}\n` +
+        `\n完整节点数据: ${JSON.stringify(nodeData, null, 2)}`;
+      console.log('%c[KnowledgeGraph] ai-explain 回传数据: ', 'background:#FAC858;color:#333;padding:4px 8px;border-radius:4px;', payload);
+      this.appendLog('warn', 'AI讲解', logMsg);
+    },
+
+    /**
+     * 按钮测试:获取所有勾选的节点(调用组件暴露方法)
+     */
+    testGetChecked() {
+      const nodes = this.$refs.treeTestRef.getCheckedNodes(false);
+      const keys = this.$refs.treeTestRef.getCheckedKeys(false);
+      const logMsg =
+        `勾选节点对象数: ${nodes.length}\n` +
+        `勾选节点key数组: ${JSON.stringify(keys)}\n` +
+        `勾选节点名称: ${nodes.map(n => n.label).join('、') || '(无)'}`;
+      console.log('%c[KnowledgeGraph] getCheckedNodes 结果: ', 'background:#67C23A;color:#fff;padding:4px 8px;border-radius:4px;', nodes, keys);
+      this.appendLog('success', '获取选中节点', logMsg);
+    },
+
+    /**
+     * 按钮测试:清空所有勾选节点
+     */
+    testClearChecked() {
+      this.$refs.treeTestRef.clearChecked();
+      console.log('%c[KnowledgeGraph] clearChecked 已清空', 'background:#E6A23C;color:#fff;padding:4px 8px;border-radius:4px;');
+      this.appendLog('info', '清空选中', '已通过 clearChecked() 方法清空所有复选框勾选状态');
     }
   }
 };
@@ -362,24 +615,199 @@ export default {
 .knowledge_graph {
   overflow-y: auto;
   overflow-x: hidden;
-}
 
-/* 自定义滚动条样式 */
-.knowledge_graph::-webkit-scrollbar {
-  width: 6px;
-}
+  .graph_top {
+    background: #FFFFFF;
+    border-radius: 10px;
+    padding: 20px;
+  }
 
-.knowledge_graph::-webkit-scrollbar-track {
-  background: #f1f1f1;
-  border-radius: 3px;
-}
+  /* 顶部信息模块 */
+  .knowledge_header {
+    margin-bottom: 10px;
+
+    .header_top {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+      margin-bottom: 10px;
+
+      .header_title {
+        font-size: 16px;
+        font-weight: 500;
+        color: #333;
+      }
+      .el-checkbox-group{
+        .el-checkbox{
+          margin-right:20px;
+        }
+      }
+    }
+
+    .header_stats {
+       font-size: 14px;
+      .stats_text {
+        color: #999999;
+      }
+
+      .stats_num {
+        font-weight: 600;
+        color: #2E64FA;
+        margin: 0 4px;
+      }
+
+      .stats_weak {
+        color: #F56C6C;
+      }
+
+      .stats_good {
+        color: #FAC858;
+      }
+    }
+  }
+
+  /* ==================== CommonTree 测试区域样式 ==================== */
+  .tree_test_section {
+    margin-top: 15px;
+    background: #fff;
+    border-radius: 10px;
+    padding: 15px 20px 20px;
+
+    /* 测试区标题栏 */
+    .tree_test_title {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding-bottom: 12px;
+      margin-bottom: 12px;
+      border-bottom: 1px solid #eceef3;
+      font-size: 15px;
+      font-weight: 600;
+      color: #333;
+    }
 
-.knowledge_graph::-webkit-scrollbar-thumb {
-  background: #c1c1c1;
-  border-radius: 3px;
+    /* 测试区主体:左树右日志 */
+    .tree_test_body {
+      display: flex;
+      gap: 16px;
+    }
 
-  &:hover {
-    background: #a8a8a8;
+    /* 左侧树形面板(必须白色,因为树形末尾竖线用白色块截断) */
+    .tree_panel {
+      flex: 1;
+      height: 420px;
+      border: 1px solid #eceef3;
+      border-radius: 8px;
+      padding: 8px 0 8px 4px;
+      overflow-y: auto;
+      background: #fff;
+    }
+
+    /* 右侧事件日志面板 */
+    .tree_log_panel {
+      width: 420px;
+      height: 420px;
+      border: 1px solid #eceef3;
+      border-radius: 8px;
+      display: flex;
+      flex-direction: column;
+      overflow: hidden;
+
+      .log_title {
+        padding: 10px 14px;
+        font-size: 13px;
+        font-weight: 600;
+        color: #666;
+        background: #f5f7fa;
+        border-bottom: 1px solid #eceef3;
+      }
+
+      .log_list {
+        flex: 1;
+        overflow-y: auto;
+        padding: 10px 12px;
+
+        /* 单条日志 */
+        .log_item {
+          border: 1px solid #eef0f3;
+          border-radius: 6px;
+          margin-bottom: 8px;
+          overflow: hidden;
+
+          .log_tag, .log_time {
+            display: inline-block;
+            padding: 2px 8px;
+            font-size: 12px;
+          }
+
+          .log_tag {
+            font-weight: 600;
+            color: #fff;
+          }
+
+          .log_time {
+            color: #909399;
+            background: #f5f7fa;
+            border-bottom: 1px solid #eef0f3;
+          }
+
+          .log_content {
+            margin: 0;
+            padding: 8px 10px;
+            font-size: 12px;
+            color: #444;
+            line-height: 1.6;
+            white-space: pre-wrap;
+            word-break: break-all;
+            font-family: Consolas, Monaco, monospace;
+          }
+
+          /* 按事件类型设置标签颜色 */
+          &.log-info {
+            .log_tag { background: #2E64FA; }
+            border-left: 3px solid #2E64FA;
+          }
+          &.log-success {
+            .log_tag { background: #3BA272; }
+            border-left: 3px solid #3BA272;
+          }
+          &.log-warn {
+            .log_tag { background: #E6A23C; }
+            border-left: 3px solid #E6A23C;
+          }
+          &.log-error {
+            .log_tag { background: #F56C6C; }
+            border-left: 3px solid #F56C6C;
+          }
+        }
+
+        .log_empty {
+          padding: 30px 12px;
+          text-align: center;
+          color: #909399;
+          font-size: 13px;
+        }
+      }
+    }
+  }
+
+  /* 自定义滚动条样式 */
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 3px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 3px;
+
+    &:hover {
+      background: #a8a8a8;
+    }
   }
 }
 </style>

+ 320 - 23
src/views/targetImprove/practice/knowledgeGraph/circularDiffusion.vue

@@ -1,9 +1,9 @@
 <template>
-  <div class="knowledge_graph">
+  <div class="circularDiffusion">
     <!-- 教师端样式 -->
     <div class="knowledge_class">
       <!-- 图形查看容器 -->
-      <div v-if="activeView === 'graph'" class="graph_content">
+      <div  class="graph_content">
         <!-- 左侧图表 -->
         <div class="chart_container">
           <!-- 顶部容器:图例和视图切换按钮 -->
@@ -20,11 +20,12 @@
                 <span class="legend_text">
                   {{ mode === 'student' && activeComparison ? '持平(-10%≤得分率差值≤10%)' : '良好(60%≤得分率< 85 %) ' }}</span>
               </div>
-              <div class="legend_item" :class="{ 'selected': selectedLegend.excellent }"
-                @click="toggleLegend('excellent')">
-                <span class="legend_dot excellent"></span>
-                <span class="legend_text">{{ mode === 'student' && activeComparison ? '优势(得分率差值>10%)' : '优秀(85%≤得分率)'
-                }}</span>
+              <div class="legend_item" :class="{ ' selected': selectedLegend.excellent }"
+                    @click="toggleLegend('excellent')">
+                    <span class="legend_dot excellent"></span>
+                    <span class="legend_text">{{ mode === 'student' && activeComparison ? '优势(得分率差值>10%)' :
+                      '优秀(85%≤得分率)'
+                      }}</span>
               </div>
             </div>
           </div>
@@ -33,8 +34,36 @@
 
         <!-- 右侧容器 -->
         <div class="knowledge_right_container">
-          <!-- 右侧容器 -->
-          <div class="knowledge_list">
+          <!-- 折线图 -->
+          <div class="line_chart_container">
+            <div ref="lineChart" class="line_chart"></div>
+            <div class="chart_title" v-if="selectedKnowledgeName">
+              <span class="title_text">{{ selectedKnowledgeName }}</span>
+              <span class="add_practice_btn" @click="handleAddPractice">加入练习</span>
+            </div>
+          </div>
+
+          <!-- 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-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="examType" label="类型" width="100" align="center"></el-table-column>
+              <el-table-column prop="scoreRate" label="得分率" width="100" align="center">
+                <template slot-scope="scope">
+                  <span :style="{ color: getCurrentColor() }">{{ scope.row.scoreRate }}%</span>
+                </template>
+              </el-table-column>
+            </el-table>
           </div>
         </div>
       </div>
@@ -139,10 +168,29 @@ export default {
     },
   },
   computed: {
+    // 选中的知识点名称
+    selectedKnowledgeName() {
+      if (this.selectedKnowledgeId) {
+        const item = this.allKnowledgeList.find(item => item.knowledgeId === this.selectedKnowledgeId);
+        return item ? item.knowledgeName : '';
+      }
+      return '';
+    },
+    // 筛选表格数据
+    filteredTableData() {
+      if (this.activeTableTab === 'all') {
+        return this.mockTableData;
+      } else if (this.activeTableTab === 'exam') {
+        return this.mockTableData.filter(item => item.type === 'exam');
+      } else {
+        return this.mockTableData.filter(item => item.type === 'practice');
+      }
+    }
   },
   data() {
     return {
       chart: null,
+      lineChart: null,
       _isMounted: false,
       _isFirstRender: true,
       activeComparison: null,
@@ -154,8 +202,24 @@ export default {
       legendNumbers: [1, 2, 3],
       selectedIndex: 0,
       selectedKnowledgeId: '',
+      selectedKnowledgeType: 3,
       chartRefreshKey: 0,
       showKnowledgePrompt: this.isRecordKnowledge,
+      activeTableTab: 'all',
+      mockTableData: [
+        { examName: '高二四月测验', examType: '期中', scoreRate: 82.92, type: 'exam' },
+        { examName: '高二综合性练习', examType: '期中', scoreRate: 83.29, type: 'exam' },
+        { examName: '高二零模考试', examType: '期中', scoreRate: 93.47, type: 'exam' },
+        { examName: '高二期中检测考试', examType: '期中', scoreRate: 89.36, type: 'exam' },
+        { examName: '高二3月月考', examType: '期中', scoreRate: 69.36, type: 'exam' },
+        { examName: '高二阶段性练习', examType: '期中', scoreRate: 54.38, type: 'exam' },
+        { examName: '2023-08-09 16:00:00', examType: '自主练习', scoreRate: 91.29, type: 'practice' },
+        { examName: '2023-08-09 16:00:00', examType: '指定练习', scoreRate: 85.39, type: 'practice' }
+      ],
+      mockLineData: {
+        xAxis: ['高二四月', '高二综合', '高二零模', '高二期中', '高二3月', '高二阶段', '自主练习', '指定练习'],
+        scores: [82.92, 83.29, 93.47, 89.36, 69.36, 54.38, 91.29, 85.39]
+      }
     };
   },
   mounted() {
@@ -163,6 +227,7 @@ export default {
     if (this.activeView === 'graph') {
       this.$nextTick(() => {
         this.initChart();
+        this.initLineChart();
       });
     }
   },
@@ -171,15 +236,34 @@ export default {
     if (this.chart) {
       this.chart.dispose();
     }
+    if (this.lineChart) {
+      this.lineChart.dispose();
+    }
   },
   watch: {
     activeView(newView) {
       if (newView === 'graph') {
         this.$nextTick(() => {
           this.initChart();
+          this.initLineChart();
         });
       }
     },
+    // 当知识点列表数据变化时,更新选中项的类型
+    allKnowledgeList: {
+      handler(newVal) {
+        if (newVal && newVal.length > 0 && this.selectedKnowledgeId) {
+          const item = newVal.find(item => String(item.knowledgeId) === String(this.selectedKnowledgeId));
+          if (item) {
+            this.selectedKnowledgeType = item.knowledgeType || 3;
+            if (this.lineChart) {
+              this.updateLineChart();
+            }
+          }
+        }
+      },
+      deep: true
+    },
     tableData: {
       deep: true,
       handler() {
@@ -193,10 +277,20 @@ export default {
     currentKnowledgeId: {
       handler(newVal) {
         this.selectedKnowledgeId = newVal;
+        // 更新知识点类型(使用 String 转换确保类型匹配)
+        const item = this.allKnowledgeList.find(item => String(item.knowledgeId) === String(newVal));
+        if (item) {
+          this.selectedKnowledgeType = item.knowledgeType || 3;
+        }
         if (this.activeView === 'graph' && this.chart) {
           this.updateChart();
         }
-      }
+        // 更新折线图颜色
+        if (this.lineChart) {
+          this.updateLineChart();
+        }
+      },
+      immediate: true
     },
     isRecordKnowledge: {
       handler(newVal) {
@@ -218,6 +312,108 @@ export default {
     },
   },
   methods: {
+    // 获取当前颜色(根据选中知识点类型)
+    getCurrentColor() {
+      if (this.selectedKnowledgeType === 3) {
+        return '#3BA272';
+      } else if (this.selectedKnowledgeType === 2) {
+        return '#FAC858';
+      } else {
+        return '#EE6666';
+      }
+    },
+    // 初始化折线图
+    initLineChart() {
+      if (!this.$refs.lineChart) return;
+      if (this.lineChart) {
+        this.lineChart.dispose();
+      }
+      this.lineChart = echarts.init(this.$refs.lineChart);
+      this.updateLineChart();
+    },
+    // 更新折线图
+    updateLineChart() {
+      if (!this.lineChart) return;
+      const color = this.getCurrentColor();
+      const option = {
+        tooltip: {
+          trigger: 'axis',
+          formatter: '{b}: {c}%'
+        },
+        grid: {
+          left: '10%',
+          right: '5%',
+          top: '20%',
+          bottom: '20%'
+        },
+        xAxis: {
+          type: 'category',
+          data: this.mockLineData.xAxis,
+          axisLabel: {
+            fontSize: 10,
+            color: '#999',
+            rotate: 30
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#ECEEF3'
+            }
+          },
+          axisTick: {
+            show: false
+          }
+        },
+        yAxis: {
+          type: 'value',
+          min: 0,
+          max: 100,
+          axisLabel: {
+            fontSize: 10,
+            color: '#999',
+            formatter: '{value}%'
+          },
+          splitLine: {
+            lineStyle: {
+              color: '#f5f5f5',
+              type: 'dashed'
+            }
+          }
+        },
+        series: [{
+          data: this.mockLineData.scores,
+          type: 'line',
+          smooth: true,
+          symbol: 'circle',
+          symbolSize: 8,
+          itemStyle: {
+            color: '#fff',
+            borderColor: color,
+            borderWidth: 2
+          },
+          lineStyle: {
+            color: color,
+            width: 2
+          },
+          areaStyle: {
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+              { offset: 0, color: color + '40' },
+              { offset: 1, color: color + '05' }
+            ])
+          },
+          // 确保圆点显示在折线上方,遮挡线条
+          z: 10
+        }]
+      };
+      this.lineChart.setOption(option);
+      this.lineChart.resize();
+    },
+    // 处理加入练习
+    handleAddPractice() {
+      this.$emit('add-practice', {
+        knowledgeId: this.selectedKnowledgeId,
+        knowledgeName: this.selectedKnowledgeName
+      });
+    },
     setDefaultSelection() {
       let targetList = [];
       if (this.allKnowledgeList && this.allKnowledgeList.length > 0) {
@@ -227,11 +423,17 @@ export default {
       } else if (this.fatalVulnerability && this.fatalVulnerability.length > 0) {
         targetList = this.fatalVulnerability;
       }
-      
+
       if (targetList.length > 0) {
         this.selectedIndex = 0;
         this.selectedKnowledgeId = targetList[0].knowledgeId;
-        
+        this.selectedKnowledgeType = targetList[0].knowledgeType || 3;
+
+        // 初始化折线图(确保颜色正确)
+        if (this.lineChart) {
+          this.updateLineChart();
+        }
+
         // 更新图表高亮状态
         if (this.chart) {
           const vm = this;
@@ -279,7 +481,7 @@ export default {
               return node;
             });
           };
-          
+
           this.chart.setOption({
             series: [{
               data: [
@@ -477,12 +679,12 @@ export default {
         };
         this.chart.setOption(option);
         this.chart.resize();
-        
+
         // 设置默认选中项
         this.$nextTick(() => {
           this.setDefaultSelection();
         });
-        
+
         this.chart.on('click', (params) => {
           const hasDataNodeClick = params.data && params.data.hasData;
           if (hasDataNodeClick) {
@@ -490,7 +692,7 @@ export default {
             let knowledgeItem = null;
             let targetIndex = -1;
             let targetList = this.allKnowledgeList;
-            
+
             // 直接在所有列表中查找
             for (const list of allLists) {
               knowledgeItem = list.find(item => item.knowledgeName === params.name);
@@ -500,7 +702,7 @@ export default {
                 break;
               }
             }
-            
+
             // 如果没找到,从表格数据中递归查找
             if (!knowledgeItem) {
               const findKnowledgeItem = (data) => {
@@ -535,6 +737,11 @@ export default {
               });
               this.selectedIndex = targetIndex;
               this.selectedKnowledgeId = knowledgeItem.knowledgeId;
+              this.selectedKnowledgeType = knowledgeItem.knowledgeType || 3;
+              // 立即更新折线图颜色
+              if (this.lineChart) {
+                this.updateLineChart();
+              }
               this.chart.setOption({
                 series: [{
                   data: [
@@ -648,24 +855,24 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.knowledge_graph {
-  background: #FFFFFF;
-  border-radius: 10px;
-  padding: 20px;
-  margin-bottom: 10px;
+.circularDiffusion {
   position: relative;
+
 }
 
-.knowledge_graph,
+.circularDiffusion,
 .knowledge_graph_student {
   .knowledge_class {
+
     :deep(.vxe-body--row.selected-row),
     :deep(.selected-row) {
       background-color: rgba(46, 100, 250, 0.1) !important;
       color: #2E64FA !important;
+
       * {
         color: #2E64FA !important;
       }
+
       .rate_dot {
         border-color: rgba(46, 100, 250, 0.1) !important;
       }
@@ -752,6 +959,96 @@ export default {
         width: 400px;
         display: flex;
         flex-direction: column;
+        gap: 12px;
+
+        .line_chart_container {
+          border: 1px solid #E9EBEF;
+          border-radius: 10px;
+          padding: 12px;
+          position: relative;
+
+          .line_chart {
+            width: 100%;
+            height: 180px;
+          }
+
+          .chart_title {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-top: 8px;
+            padding-top: 8px;
+            border-top: 1px solid #f0f0f0;
+
+            .title_text {
+              font-size: 14px;
+              font-weight: 500;
+              color: #333;
+              overflow: hidden;
+              text-overflow: ellipsis;
+              white-space: nowrap;
+              max-width: 250px;
+            }
+
+            .add_practice_btn {
+              padding: 4px 12px;
+              font-size: 12px;
+              color: #fff;
+              background: #2E64FA;
+              border-radius: 4px;
+              cursor: pointer;
+              white-space: nowrap;
+
+              &:hover {
+                background: #1a4fd6;
+              }
+            }
+          }
+        }
+
+        .table_container {
+          border: 1px solid #E9EBEF;
+          border-radius: 10px;
+          padding: 8px;
+
+          .table_tabs {
+            :deep(.el-tabs__header) {
+              margin-bottom: 8px;
+            }
+
+            :deep(.el-tabs__item) {
+              font-size: 13px;
+              height: 32px;
+              line-height: 32px;
+            }
+
+            :deep(.el-tabs__item.is-active) {
+              color: #2E64FA;
+            }
+
+            :deep(.el-tabs__active-bar) {
+              background-color: #2E64FA;
+            }
+          }
+
+          .data_table {
+            :deep(.el-table__header) {
+              th {
+                background: #f5f7fa;
+                color: #606266;
+                font-weight: 500;
+                font-size: 13px;
+              }
+            }
+
+            :deep(.el-table__body) {
+              td {
+                font-size: 13px;
+                color: #606266;
+              }
+            }
+          }
+        }
       }
 
       .item_chart_container {

+ 79 - 79
src/views/targetImprove/practice/knowledgeGraph/region.vue

@@ -1,26 +1,26 @@
 <template>
-  <div class="region-container">
+  <div class="region_container">
     <!-- 薄弱补强区 -->
-    <div class="region-section weak" v-if="weakArea.length > 0">
-      <div class="region-header">
-        <span class="region-icon">
+    <div class="region_section weak" v-if="weakArea.length > 0">
+      <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>
         </span>
-        <span class="region-title">薄弱补强区</span>
+        <span class="region_title">薄弱补强区</span>
       </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="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>
-          <div class="card-info">
-            <span class="score-rate">得分率:{{ getScoreRate(item) }}%</span>
-            <span class="wrong-count">错题:{{ item.wrongCount || item.questionNum || 0 }}</span>
+          <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">
+          <div class="card_action">
             <el-button type="primary" size="small" @click="handleAddPractice(item)">加入练习</el-button>
           </div>
         </div>
@@ -28,26 +28,26 @@
     </div>
 
     <!-- 能力提升区 -->
-    <div class="region-section good" v-if="goodArea.length > 0">
-      <div class="region-header">
-        <span class="region-icon">
+    <div class="region_section good" v-if="goodArea.length > 0">
+      <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>
         </span>
-        <span class="region-title">能力提升区</span>
+        <span class="region_title">能力提升区</span>
       </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 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="card_info">
+            <span class="score_rate">得分率:{{ getScoreRate(item) }}%</span>
+            <span class="wrong_count">错题:{{ item.wrongCount || item.questionNum || 0 }}</span>
           </div>
-          <div class="card-action">
+          <div class="card_action">
             <el-button type="warning" size="small" @click="handleAddPractice(item)">加入练习</el-button>
           </div>
         </div>
@@ -55,26 +55,26 @@
     </div>
 
     <!-- 扎实巩固区 -->
-    <div class="region-section excellent" v-if="excellentArea.length > 0">
-      <div class="region-header">
-        <span class="region-icon">
+    <div class="region_section excellent" v-if="excellentArea.length > 0">
+      <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>
         </span>
-        <span class="region-title">扎实巩固区</span>
+        <span class="region_title">扎实巩固区</span>
       </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 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="card_info">
+            <span class="score_rate">得分率:{{ getScoreRate(item) }}%</span>
+            <span class="wrong_count">错题:{{ item.wrongCount || item.questionNum || 0 }}</span>
           </div>
-          <div class="card-action">
+          <div class="card_action">
             <el-button type="success" size="small" @click="handleAddPractice(item)">加入练习</el-button>
           </div>
         </div>
@@ -82,26 +82,26 @@
     </div>
 
     <!-- 冲刺进阶区 -->
-    <div class="region-section advanced" v-if="advancedArea.length > 0">
-      <div class="region-header">
-        <span class="region-icon">
+    <div class="region_section advanced" v-if="advancedArea.length > 0">
+      <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"/>
           </svg>
         </span>
-        <span class="region-title">冲刺进阶区</span>
+        <span class="region_title">冲刺进阶区</span>
       </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 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="card_info">
+            <span class="score_rate">得分率:{{ getScoreRate(item) }}%</span>
+            <span class="wrong_count">错题:{{ item.wrongCount || item.questionNum || 0 }}</span>
           </div>
-          <div class="card-action">
+          <div class="card_action">
             <el-button type="primary" size="small" @click="handleAddPractice(item)">加入练习</el-button>
           </div>
         </div>
@@ -161,14 +161,14 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-.region-container {
+.region_container {
   display: flex;
   flex-direction: row;
   gap: 16px;
   padding: 16px;
 }
 
-.region-section {
+.region_section {
   flex: 1;
   border-radius: 8px;
   padding: 16px;
@@ -194,7 +194,7 @@ export default {
   }
 }
 
-.region-header {
+.region_header {
   display: flex;
   align-items: center;
   gap: 8px;
@@ -202,7 +202,7 @@ export default {
   padding-bottom: 8px;
   border-bottom: 1px solid #f0f0f0;
 
-  .region-icon {
+  .region_icon {
     display: flex;
     align-items: center;
     justify-content: center;
@@ -213,37 +213,37 @@ export default {
     }
   }
 
-  &.weak .region-icon {
+  &.weak .region_icon {
     color: #EE6666;
   }
 
-  &.good .region-icon {
+  &.good .region_icon {
     color: #FAC858;
   }
 
-  &.excellent .region-icon {
+  &.excellent .region_icon {
     color: #3BA272;
   }
 
-  &.advanced .region-icon {
+  &.advanced .region_icon {
     color: #2E64FA;
   }
 
-  .region-title {
+  .region_title {
     font-size: 16px;
     font-weight: 600;
     color: #333333;
   }
 }
 
-.region-desc {
+.region_desc {
   font-size: 12px;
   color: #999999;
   margin-bottom: 12px;
   padding-top: 4px;
 }
 
-.knowledge-cards {
+.knowledge_cards {
   display: flex;
   flex-direction: column;
   gap: 8px;
@@ -260,7 +260,7 @@ export default {
   }
 }
 
-.knowledge-card {
+.knowledge_card {
   border: 1px solid #ECEEF3;
   border-radius: 6px;
   padding: 10px 12px;
@@ -274,8 +274,8 @@ export default {
     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
   }
 
-  .card-header {
-    .knowledge-name {
+  .card_header {
+    .knowledge_name {
       font-size: 13px;
       font-weight: 500;
       color: #333333;
@@ -286,18 +286,18 @@ export default {
     }
   }
 
-  .card-info {
+  .card_info {
     display: flex;
     gap: 12px;
 
-    .score-rate,
-    .wrong-count {
+    .score_rate,
+    .wrong_count {
       font-size: 11px;
       color: #666666;
     }
   }
 
-  .card-action {
+  .card_action {
     text-align: right;
 
     .el-button {
@@ -308,20 +308,20 @@ export default {
 }
 
 /* 薄弱补强区卡片样式 */
-.weak .knowledge-card {
+.weak .knowledge_card {
   border-color: rgba(238, 102, 102, 0.3);
 
-  .card-action .el-button {
+  .card_action .el-button {
     background-color: #EE6666;
     border-color: #EE6666;
   }
 }
 
 /* 能力提升区卡片样式 */
-.good .knowledge-card {
+.good .knowledge_card {
   border-color: rgba(250, 200, 88, 0.3);
 
-  .card-action .el-button {
+  .card_action .el-button {
     background-color: #FAC858;
     border-color: #FAC858;
     color: #333333;
@@ -329,20 +329,20 @@ export default {
 }
 
 /* 扎实巩固区卡片样式 */
-.excellent .knowledge-card {
+.excellent .knowledge_card {
   border-color: rgba(59, 162, 114, 0.3);
 
-  .card-action .el-button {
+  .card_action .el-button {
     background-color: #3BA272;
     border-color: #3BA272;
   }
 }
 
 /* 冲刺进阶区卡片样式 */
-.advanced .knowledge-card {
+.advanced .knowledge_card {
   border-color: rgba(46, 100, 250, 0.3);
 
-  .card-action .el-button {
+  .card_action .el-button {
     background-color: #2E64FA;
     border-color: #2E64FA;
   }