Explorar el Código

优化图谱树形节点

吴朋磊 hace 1 mes
padre
commit
cce9916c30

+ 17 - 6
src/views/targetImprove/practice/knowledgeGraph.vue

@@ -32,17 +32,18 @@
         @legend-change="handleLegendChange"
         @activeTabChange="handleActiveTabChange"
         @add-practice="handleAddPractice"
-        @knowledge-item-click="handleKnowledgeItemClick" />
+        @knowledge-item-click="handleKnowledgeItemClick"
+        @view-change="handleViewChange" />
+    </div>
     </div>
-      
 
-      <!-- 区域展示组件 -->
+      <!-- 区域展示组件:仅图形查看时显示 -->
       <region
+        v-if="showRegion && activeView === 'graph'"
         :partitionData="partitionData"
         :selectedKnowledgeId="currentLinkedKnowledgeId || currentKnowledgeId || defaultKnowledgeId"
         @add-practice="handleAddPractice"
         @item-click="handleRegionItemClick" />
-    </div>
   </div>
 </template>
 
@@ -133,7 +134,9 @@ export default {
       // - 点 region 某行条目 → 写这里 → circularDiffusion 读 prop :currentKnowledgeId 放大圆点 + 更新右侧折线表格
       currentLinkedKnowledgeId: '',
       // 假数据:表格树形结构数据(用于无接口数据时兜底)
-      mockTableData: []
+      mockTableData: [],
+      // 图形 / 列表视图(列表查看时隐藏下方分区卡片)
+      activeView: 'graph'
     };
   },
   computed: {
@@ -584,6 +587,11 @@ export default {
       this.$emit('activeTabChange', tab);
     },
 
+    // 图形 / 列表切换:列表查看时隐藏区域展示
+    handleViewChange(view) {
+      this.activeView = view === 'list' ? 'list' : 'graph';
+    },
+
     // 处理知识点点击(来自环形扩散图组件:圆点被用户点击或默认选中)
     handleKnowledgeItemClick(data) {
       // 兼容两种 payload 结构:
@@ -854,12 +862,15 @@ export default {
   .graph_top {
     width: 100%;
     box-sizing: border-box;
-    overflow: hidden;
+    overflow: visible;
     .knowledge_info{
       border-radius: 10px;
       padding: 20px;
       background: #FFFFFF;
       margin-bottom: 10px;
+      overflow: hidden;
+      position: relative;
+      z-index: 0;
     }
   }
 

+ 326 - 163
src/views/targetImprove/practice/knowledgeGraph/circularDiffusion.vue

@@ -135,41 +135,69 @@
 
       <!-- 列表查看容器 -->
       <div v-if="activeView === 'list'" class="list_content">
-        <vxe-table v-if="hasGraphData" ref="treeTable" :data="filteredTableDataForList" style="width: 100%;" maxHeight="480px" border show-header
-          :tree-config="{ childrenField: 'children', hasChildField: 'hasChildren', showIcon: true, iconOpen: 'el-icon-remove', iconClose: 'el-icon-circle-plus' }"
-          :scroll-y="{ enabled: true, gt: 480 }" fixed-header :header-cell-style="{ color: '#333' }"
-          @row-click="handleRowClick" @cell-click="handleCellClick" :row-class-name="rowClassName">
-
-          <vxe-table-column type="tree" tree-node prop="title" :title="subjectName">
-            <template #default="{ row }">
-              <div class="knowledge_item">
-                <span class="knowledge_title">{{ row.knowledgeName }}</span>
-              </div>
-            </template>
-          </vxe-table-column>
-
-          <!-- 个人得分率 -->
-          <vxe-table-column prop="personalScoreRate" title="个人得分率" width="180" align="center">
-            <template #default="{ row }">
-              <div class="rate_info" v-if="row.personalScoreRate !== null">
-                <span class="rate_dot" :class="getRateClass(row.personalScoreRate)"></span>
-                <span class="rate_value">{{ row.personalScoreRate }}%</span>
-              </div>
-            </template>
-          </vxe-table-column>
-          <!-- 个人知识点状态 -->
-          <vxe-table-column prop="personalScoreRate" title="个人知识点状态" width="120" align="center">
-            <template #default="{ row }">
-              <div class="status_info" v-if="row.personalScoreRate !== null">
-                <span class="status_value"
-                  :style="{ color: getRateName(row.personalScoreRate) === '优秀' ? '#3BA272' : getRateName(row.personalScoreRate) === '良好' ? '#FAC858' : '#EE6666' }">
-                  {{ getRateName(row.personalScoreRate) }}
-                </span>
+        <div v-if="!treeTableData.length" class="empty_tip no_content_data"><span>暂无数据</span></div>
+        <div v-else class="tree_table page_table">
+          <div class="tree_table_header">
+            <div class="tree_table_header_inner">
+              <div class="col col_name">{{ subjectName }}</div>
+              <div class="col col_personal_rate">个人得分率</div>
+              <div class="col col_personal_status">个人知识点状态</div>
+            </div>
+            <div class="tree_table_gutter"></div>
+          </div>
+          <div class="tree_table_body">
+            <el-tree
+              :key="treeTableKey"
+              ref="knowledgeTree"
+              class="knowledge_tree"
+              :data="treeTableData"
+              node-key="_rowKey"
+              :props="treeProps"
+              :indent="0"
+              default-expand-all
+              :expand-on-click-node="true"
+              :render-after-expand="false"
+              @node-click="handleNodeClick"
+              @node-expand="syncTreeStripe"
+              @node-collapse="syncTreeStripe">
+              <div
+                slot-scope="{ node, data }"
+                class="tree_row"
+                :class="{
+                  'selected-row': isSelectedRow(data),
+                  row_highlight: data.highlight
+                }">
+                <div class="col col_name">
+                  <div class="knowledge_item" :style="{ paddingLeft: ((node.level - 1) * 20) + 'px' }">
+                    <i
+                      v-if="!node.isLeaf"
+                      class="tree_toggle_icon"
+                      :class="node.expanded ? 'el-icon-remove' : 'el-icon-circle-plus'"
+                      @click.stop="toggleTreeNode(node)"
+                    ></i>
+                    <span v-else class="tree_toggle_placeholder"></span>
+                    <span class="knowledge_title" :title="data.knowledgeName">{{ data.knowledgeName }}</span>
+                  </div>
+                </div>
+                <div class="col col_personal_rate">
+                  <div class="rate_info" v-if="hasPersonalRate(data)">
+                    <span class="rate_dot" :class="getRateClass(data.personalScoreRate)"></span>
+                    <span class="rate_value">{{ data.personalScoreRate }}%</span>
+                  </div>
+                </div>
+                <div class="col col_personal_status">
+                  <div class="status_info" v-if="hasPersonalRate(data)">
+                    <span
+                      class="status_value"
+                      :style="{ color: getRateName(data.personalScoreRate) === '优秀' ? '#3BA272' : getRateName(data.personalScoreRate) === '良好' ? '#FAC858' : '#EE6666' }">
+                      {{ getRateName(data.personalScoreRate) }}
+                    </span>
+                  </div>
+                </div>
               </div>
-            </template>
-          </vxe-table-column>
-        </vxe-table>
-        <div v-else class="empty_tip no_content_data"><span>暂无数据</span></div>
+            </el-tree>
+          </div>
+        </div>
       </div>
 
     </div>
@@ -423,6 +451,9 @@ export default {
         return result;
       };
       return removeHidden(filtered);
+    },
+    treeTableData() {
+      return this.normalizeTreeData(this.filteredTableDataForList);
     }
   },
   data() {
@@ -446,11 +477,14 @@ export default {
       showKnowledgePrompt: this.isRecordKnowledge,
       activeTableTab: 'all',
       selectedRow: null,
-      expandRowKeys: []
+      treeTableKey: 0,
+      treeProps: { children: 'children', label: 'knowledgeName' },
+      _stripeRaf: null
     };
   },
   mounted() {
     this._isMounted = true;
+    this.$emit('view-change', this.activeView);
     this.$nextTick(() => {
       this.initChart();
       this.initLineChart();
@@ -464,11 +498,23 @@ export default {
     if (this.lineChart) {
       this.lineChart.dispose();
     }
+    if (this._stripeRaf) {
+      cancelAnimationFrame(this._stripeRaf);
+      this._stripeRaf = null;
+    }
   },
   watch: {
     // 监听activeView变化,切换到列表视图时展开所有树形节点,切换到图形视图时初始化图表
-    activeView(newView, oldView) {
+    activeView(newView) {
       if (newView === 'list') {
+        if (this.chart) {
+          this.chart.dispose();
+          this.chart = null;
+        }
+        if (this.lineChart) {
+          this.lineChart.dispose();
+          this.lineChart = null;
+        }
         this.$nextTick(() => {
           this.expandAllNodes();
         });
@@ -902,40 +948,81 @@ export default {
       }
       return row.highlight ? 'row_highlight' : '';
     },
+    isSelectedRow(row) {
+      return !!(this.selectedRow && row && String(this.selectedRow.knowledgeId) === String(row.knowledgeId));
+    },
+    hasPersonalRate(row) {
+      return row && row.personalScoreRate !== null && row.personalScoreRate !== undefined && row.personalScoreRate !== '';
+    },
+    normalizeTreeData(list, prefix = '') {
+      if (!Array.isArray(list)) return [];
+      return list.map((item, index) => {
+        const id = item.knowledgeId != null && item.knowledgeId !== '' ? item.knowledgeId : (item.id != null ? item.id : index);
+        const key = prefix === '' ? String(id) : `${prefix}-${id}`;
+        const node = { ...item, _rowKey: key };
+        if (item.children && item.children.length > 0) {
+          node.children = this.normalizeTreeData(item.children, key);
+        } else {
+          node.children = undefined;
+        }
+        return node;
+      });
+    },
+    handleNodeClick(data, node) {
+      if (node && node.isLeaf) {
+        this.handleRowClick(data);
+      }
+    },
+    toggleTreeNode(node) {
+      if (!node || node.isLeaf) return;
+      if (node.expanded) {
+        node.collapse();
+      } else {
+        node.expand();
+      }
+      this.syncTreeStripe();
+    },
+    syncTreeStripe() {
+      if (this._stripeRaf) cancelAnimationFrame(this._stripeRaf);
+      this._stripeRaf = requestAnimationFrame(() => {
+        this._stripeRaf = null;
+        const root = this.$refs.knowledgeTree && this.$refs.knowledgeTree.$el;
+        if (!root) return;
+        const rows = root.querySelectorAll('.el-tree-node__content');
+        let visibleIndex = 0;
+        rows.forEach(el => {
+          if (el.offsetHeight === 0) return;
+          const row = el.querySelector('.tree_row');
+          if (!row) return;
+          row.classList.toggle('is_stripe', visibleIndex % 2 === 1);
+          visibleIndex += 1;
+        });
+      });
+    },
     // 处理树形表格行点击事件
     handleRowClick(row) {
       const isLastLevel = !row.children || row.children.length === 0;
       if (isLastLevel) {
         this.selectedRow = row;
+        this.selectedKnowledgeId = row.knowledgeId;
+        this.selectedKnowledgeType = row.knowledgeType || 3;
+        this.$emit('knowledge-item-click', {
+          item: row,
+          index: -1,
+          knowledgeId: row.knowledgeId,
+          knowledgeName: row.knowledgeName,
+          personalScoreRate: row.personalScoreRate != null ? row.personalScoreRate : row.scoreRate,
+          knowledgeType: row.knowledgeType,
+          examList: Array.isArray(row.examList) ? row.examList : []
+        });
       }
     },
-    // 处理单元格点击事件
     handleCellClick({ row }) {
       this.handleRowClick(row);
     },
-    // 展开所有树形节点
     expandAllNodes() {
-      const treeTable = this.$refs.treeTable;
-      if (treeTable) {
-        const expandNodes = [];
-        const collectExpandNodes = (data) => {
-          if (!data || !Array.isArray(data)) return;
-          data.forEach(item => {
-            if (item.children && item.children.length > 0) {
-              expandNodes.push(item);
-              collectExpandNodes(item.children);
-            }
-          });
-        };
-        collectExpandNodes(this.filteredTableDataForList);
-        if (treeTable.setExpandRows) {
-          treeTable.setExpandRows(expandNodes);
-        } else if (treeTable.setTreeExpand) {
-          expandNodes.forEach(node => {
-            treeTable.setTreeExpand(node, true);
-          });
-        }
-      }
+      this.treeTableKey += 1;
+      this.$nextTick(() => this.syncTreeStripe());
     },
     classIsGroup() {
       if (this.classLevel == 1) {
@@ -1467,9 +1554,10 @@ export default {
 .knowledge_graph_student {
   .knowledge_class {
 
+    :deep(.tree_row.selected-row),
     :deep(.vxe-body--row.selected-row),
     :deep(.selected-row) {
-      background-color: rgba(46, 100, 250, 0.1) !important;
+      background-color: #EAF0FF !important;
       color: #2E64FA !important;
 
       * {
@@ -1999,149 +2087,223 @@ export default {
       }
     }
 
-    // vxe表格样式
+    // 列表树形表格(列表模式下下方分区已隐藏,用满图谱可视高度)
     .list_content {
-      border-radius: 10px 10px 10px 10px;
+      height: 630px;
+      max-height: 630px;
+      border-radius: 10px;
+      display: flex;
+      flex-direction: column;
+      min-height: 0;
       position: relative;
+      z-index: 0;
+      overflow: hidden;
+      background: #fff;
 
       .empty_tip {
-        min-height: 480px;
+        min-height: 400px;
         width: 100%;
         border-radius: 8px;
         span {
-          margin-top: -1%;
+          margin-top: 23%;
         }
       }
 
-      .vxe-table {
-        // 展开收起的样式
-        :deep(.el-icon-circle-plus) {
-          color: #DCDFE6 !important;
-          font-size: 16px !important;
-        }
+      .tree_table {
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        border: 1px solid #e9e9e9;
+        border-radius: 8px;
+        overflow: hidden;
+        box-sizing: border-box;
+      }
 
-        :deep(.el-icon-remove) {
-          color: #2E64FA !important;
-          font-size: 16px !important;
-        }
+      .tree_table_header {
+        flex-shrink: 0;
+        display: flex;
+        align-items: stretch;
+        height: 40px;
+        background-color: #f4f6f8;
+        color: #333;
+        font-size: 16px;
+        font-weight: 600;
+        border-bottom: 1px solid #e9e9e9;
+        box-sizing: border-box;
       }
 
-      /* 设置表格行高度和光标样式 */
-      :deep {
-        .vxe-table {
-          border-radius: 8px;
-          overflow-y: auto;
-          overflow-x: hidden;
+      .tree_table_header_inner {
+        flex: 1;
+        min-width: 0;
+        display: flex;
+        align-items: center;
+        overflow: hidden;
+      }
 
-          --vxe-ui-table-row-height-default: 20px !important;
+      .tree_table_gutter {
+        flex-shrink: 0;
+        width: 8px;
+        background-color: #f4f6f8;
+      }
 
-          // 表头样式
-          .vxe-table--header {
-            display: table-header-group !important;
-            visibility: visible !important;
-            opacity: 1 !important;
-            height: 52px !important;
-          }
+      .tree_table_body {
+        flex: 1;
+        min-height: 0;
+        overflow-y: scroll;
+        overflow-x: hidden;
+        scrollbar-gutter: stable;
+        background: #fff;
+      }
 
-          .vxe-table--header {
-            .vxe-header--row {
-              height: 52px !important;
-            }
-          }
+      .col {
+        box-sizing: border-box;
+        height: 100%;
+        padding: 0 5px;
+        overflow: hidden;
+        border-right: 1px solid #e9e9e9;
 
-          // 行样式
-          .vxe-table--body {
-            tbody {
-              tr {
-                border-bottom: 1px solid #F0F2F5;
-                cursor: pointer !important;
-                height: 20px !important;
-                line-height: 20px !important;
-                min-height: 20px !important;
-                max-height: 20px !important;
-
-                &:hover {
-                  background-color: #f5f7fa !important;
-                }
+        &:last-child {
+          border-right: none;
+        }
+      }
 
-                &.row_highlight {
-                  background-color: rgba(195, 219, 255, 0.2);
-                  cursor: pointer !important;
-                }
+      .col_name {
+        flex: 1;
+        min-width: 0;
+        display: flex;
+        align-items: center;
+        justify-content: flex-start;
+        padding-left: 10px;
+        text-align: left;
+      }
 
-                &.selected-row {
-                  background-color: rgba(195, 219, 255, 0.2) !important;
-                  color: #2E64FA !important;
-                  cursor: pointer !important;
-                }
-              }
-            }
-          }
+      .col_personal_rate,
+      .col_personal_status {
+        width: 180px;
+        flex-shrink: 0;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        text-align: center;
+      }
 
-          // 单元格样式
-          .vxe-body--column,
-          .vxe-header--column {
-            padding: 0 !important;
-            height: 20px !important;
-            line-height: 20px !important;
-            min-height: 20px !important;
-            max-height: 20px !important;
-          }
+      .tree_row {
+        display: flex;
+        align-items: center;
+        flex: 1;
+        min-width: 0;
+        width: 0;
+        height: 40px;
+        color: #666;
+        font-size: 14px;
+        font-weight: 400;
 
-          .vxe-table--body {
-            min-height: 0 !important;
-          }
+        &.is_stripe {
+          background-color: #F5F7FA;
+        }
 
-          /* 自定义树形图标样式 */
-          .vxe-tree-icon {
-            display: inline-flex;
-            align-items: center;
-            justify-content: center;
-            width: 14px;
-            height: 14px;
-            border-radius: 50%;
-            margin-right: 6px;
-            font-size: 12px;
-            font-weight: bold;
-            line-height: 1;
-          }
+        &:hover,
+        &.row_highlight,
+        &.selected-row {
+          background-color: #EAF0FF;
+        }
+      }
 
-          .vxe-tree-icon--minus {
-            background-color: #409EFF;
-            color: white;
-          }
+      :deep(.knowledge_tree) {
+        width: 100%;
+        background: transparent;
 
-          .vxe-tree-icon--plus {
-            background-color: #C0C4CC;
-            color: white;
-          }
+        .el-tree-node {
+          width: 100%;
+        }
 
-          // 斑马纹
-          .vxe-table--body {
-            tr:nth-child(even) {
-              background-color: #F5F7FA;
-            }
+        .el-tree-node__content {
+          display: flex;
+          width: 100%;
+          height: 40px;
+          padding: 0 !important;
+          box-sizing: border-box;
+          border-bottom: 1px solid #e9e9e9;
+          background: transparent !important;
+          cursor: pointer;
+
+          &:hover {
+            background: transparent !important;
           }
         }
+
+        .el-tree-node:focus > .el-tree-node__content {
+          background: transparent !important;
+        }
+
+        .el-tree-node__expand-icon {
+          display: none !important;
+          width: 0 !important;
+          height: 0 !important;
+          padding: 0 !important;
+          margin: 0 !important;
+        }
       }
 
       .knowledge_item {
         display: flex;
         align-items: center;
+        min-width: 0;
+        width: 100%;
+
+        .tree_toggle_icon {
+          flex-shrink: 0;
+          width: 16px;
+          height: 16px;
+          margin-right: 6px;
+          font-size: 16px;
+          line-height: 16px;
+          cursor: pointer;
+
+          &.el-icon-circle-plus {
+            color: #DCDFE6;
+          }
+
+          &.el-icon-remove {
+            color: #2E64FA;
+          }
+        }
+
+        .tree_toggle_placeholder {
+          flex-shrink: 0;
+          width: 16px;
+          height: 16px;
+          margin-right: 6px;
+        }
 
         .knowledge_title {
+          min-width: 0;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
           font-size: 14px;
-          color: #606266;
+          color: #666;
+          cursor: pointer;
         }
       }
 
       .rate_info {
+        position: relative;
+        display: flex;
         align-items: center;
-        gap: 8px;
         justify-content: center;
         width: 80px;
         margin: 0 auto;
-        position: relative;
+      }
+
+      .status_info {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+      }
+
+      .status_value {
+        font-size: 14px;
       }
 
       .rate_dot {
@@ -2150,8 +2312,9 @@ export default {
         height: 6px;
         border-radius: 50%;
         position: absolute;
-        top: 8px;
-        left: 2px;
+        top: 50%;
+        left: 8px;
+        transform: translateY(-50%);
 
         &.rate_excellent {
           background-color: #3BA272;
@@ -2168,7 +2331,7 @@ export default {
 
       .rate_value {
         font-size: 14px;
-        color: #606266;
+        color: #666;
       }
     }
   }

+ 2 - 0
src/views/targetImprove/practice/knowledgeGraph/region.vue

@@ -424,6 +424,8 @@ export default {
   box-sizing: border-box;
   align-items: stretch;
   flex-wrap: wrap;
+  position: relative;
+  z-index: 1;
 }
 
 /* 单个区域卡片(通用样式:圆角作为通用属性) */