Quellcode durchsuchen

知识点图谱

吴朋磊 vor 4 Tagen
Ursprung
Commit
40bf36cc82

+ 1 - 1
src/views/targetImprove/components/SideBar.vue

@@ -48,7 +48,7 @@ export default {
 
 
 
-    this.activeMenu();
+    // this.activeMenu();
   },
   methods: {
     // activeMenu() {

+ 119 - 0
src/views/targetImprove/practice/knowledgeGraph.vue

@@ -0,0 +1,119 @@
+<template>
+  <div class="knowledge_graph">
+    <!-- 环形扩散图组件 -->
+    <circular-diffusion
+      :activeView="activeView"
+      :subjectName="subjectName"
+      :subjectScoreRate="subjectScoreRate"
+      :tableData="tableData"
+      :fatalVulnerability="fatalVulnerability"
+      :highVulnerability="highVulnerability"
+      :allKnowledgeList="allKnowledgeList"
+      :classLevel="classLevel"
+      :classGroupName="classGroupName"
+      :currentKnowledgeId="currentKnowledgeId"
+      @view-change="handleViewChange"
+      @legend-change="handleLegendChange"
+      @activeTabChange="handleActiveTabChange"
+      @knowledge-item-click="handleKnowledgeItemClick"
+    />
+
+    <!-- 区域展示组件 -->
+    <region
+      v-if="showRegion"
+      :allKnowledgeList="allKnowledgeList"
+      @add-practice="handleAddPractice"
+    />
+  </div>
+</template>
+
+<script>
+import CircularDiffusion from './knowledgeGraph/circularDiffusion.vue';
+import Region from './knowledgeGraph/region.vue';
+
+export default {
+  name: 'KnowledgeGraph',
+  components: {
+    CircularDiffusion,
+    Region
+  },
+  props: {
+    activeView: {
+      type: String,
+      default: 'graph'
+    },
+    subjectName: {
+      type: String,
+      default: '知识点'
+    },
+    subjectScoreRate: {
+      type: Number,
+      default: 0
+    },
+    tableData: {
+      type: Array,
+      default: () => []
+    },
+    fatalVulnerability: {
+      type: Array,
+      default: () => []
+    },
+    highVulnerability: {
+      type: Array,
+      default: () => []
+    },
+    allKnowledgeList: {
+      type: Array,
+      default: () => []
+    },
+    classLevel: {
+      type: [String, Number],
+      default: ''
+    },
+    classGroupName: {
+      type: String,
+      default: ''
+    },
+    currentKnowledgeId: {
+      type: [String, Number],
+      default: ''
+    },
+    showRegion: {
+      type: Boolean,
+      default: false
+    }
+  },
+  methods: {
+    // 处理视图切换
+    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);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.knowledge_graph {
+  width: 100%;
+}
+</style>

+ 1557 - 0
src/views/targetImprove/practice/knowledgeGraph/circularDiffusion.vue

@@ -0,0 +1,1557 @@
+<template>
+  <div class="knowledge_graph">
+
+
+    <div class="graph_header" v-if="activeView === 'list'">
+      <!-- 自定义图例 -->
+      <div class="legend">
+        <div class="legend_item" :class="{ 'selected': selectedLegend.weak }" @click="toggleLegend('weak')">
+          <span class="legend_dot weak" :style="selectedLegend.weak ? {} : { backgroundColor: '#ccc' }"></span>
+          <span class="legend_text" :style="selectedLegend.weak ? {} : { color: '#999999' }">{{ '薄弱(0%≤得分率<60%)'
+              }}</span>
+        </div>
+        <div class="legend_item" :class="{ 'selected': selectedLegend.good }" @click="toggleLegend('good')">
+          <span class="legend_dot good" :style="selectedLegend.good ? {} : { backgroundColor: '#ccc' }"></span>
+          <span class="legend_text" :style="selectedLegend.good ? {} : { color: '#999999' }">{{ '良好(60%≤得分率<85%)'
+              }}</span>
+        </div>
+        <div class="legend_item" :class="{ 'selected': selectedLegend.excellent }" @click="toggleLegend('excellent')">
+          <span class="legend_dot excellent"
+            :style="selectedLegend.excellent ? {} : { backgroundColor: '#ccc' }"></span>
+          <span class="legend_text" :style="selectedLegend.excellent ? {} : { color: '#999999' }">{{ '优秀(85%≤得分率)'
+            }}</span>
+        </div>
+      </div>
+      <!-- 视图切换按钮 - 移到knowledge_graph容器下 -->
+      <div class="view_switcher_container">
+        <el-button type="text" @click="switchView('graph')" class="switch_btn list_btn">
+          <i class="icon_switch_graph">
+            <img src="../../../../assets/studentAnalysis/circleGrey.svg" alt="按图形查看">
+          </i>
+          按图形查看
+        </el-button>
+        <el-button type="graph" class="switch_btn graph_btn" @click="switchView('list')">
+          <i class="icon_switch_list">
+            <img src="../../../../assets/studentAnalysis/iconBlue.svg" alt="按列表查看">
+          </i>
+          按列表查看
+        </el-button>
+      </div>
+    </div>
+
+
+    <!-- 图形查看容器 -->
+    <div v-if="activeView === 'graph'" class="graph_content">
+      <!-- 左侧图表 -->
+      <div class="chart_container">
+        <!-- 顶部容器:图例和视图切换按钮 -->
+        <div class="top_container">
+          <!-- 自定义图例 -->
+          <div class="legend">
+            <div class="legend_item" :class="{ 'selected': selectedLegend.weak }" @click="toggleLegend('weak')">
+              <span class="legend_dot weak" :style="selectedLegend.weak ? {} : { backgroundColor: '#ccc' }"></span>
+              <span class="legend_text" :style="selectedLegend.weak ? {} : { color: '#999999' }">{{ '薄弱(0%≤得分率<60%)'
+                  }}</span>
+            </div>
+            <div class="legend_item" :class="{ 'selected': selectedLegend.good }" @click="toggleLegend('good')">
+              <span class="legend_dot good" :style="selectedLegend.good ? {} : { backgroundColor: '#ccc' }"></span>
+              <span class="legend_text" :style="selectedLegend.good ? {} : { color: '#999999' }">{{ '良好(60%≤得分率<85%)'
+                  }}</span>
+            </div>
+            <div class="legend_item" :class="{ 'selected': selectedLegend.excellent }"
+              @click="toggleLegend('excellent')">
+              <span class="legend_dot excellent"
+                :style="selectedLegend.excellent ? {} : { backgroundColor: '#ccc' }"></span>
+              <span class="legend_text" :style="selectedLegend.excellent ? {} : { color: '#999999' }">{{ '优秀(85%≤得分率)'
+                }}</span>
+            </div>
+          </div>
+          <!-- 视图切换按钮  -->
+          <div class="view_switcher_container" v-if="activeView === 'graph'" :class="activeView">
+            <el-button type="graph" @click="switchView('graph')" class="switch_btn graph_btn">
+              <i class="icon_switch_graph">
+                <img src="../../../../assets/studentAnalysis/circleBlue.svg" alt="按图形查看">
+              </i>
+              按图形查看
+            </el-button>
+            <el-button type="text" class="switch_btn list_btn" @click="switchView('list')">
+              <i class="icon_switch_list"><img src="../../../../assets/studentAnalysis/iconGrey.svg" alt="按列表查看"></i>
+              按列表查看
+            </el-button>
+          </div>
+        </div>
+        <div ref="chart" class="chart" style="width: 100%;"></div>
+      </div>
+
+      <!-- 右侧容器,包含tab和知识点列表 -->
+      <div class="knowledge_right_container">
+        <!-- Tab切换 -->
+        <div class="knowledge_tab">
+          <div class="tab_item" :class="{ active: activeTab === 'all' }" @click="activeTab = 'all'">
+            全部知识点
+          </div>
+          <div class="tab_item" :class="{ active: activeTab === 'highFreq' }" @click="activeTab = 'highFreq'">
+            高频错题知识点
+          </div>
+          <div class="tab_item" :class="{ active: activeTab === 'zero' }" @click="activeTab = 'zero'">
+            零分知识点
+          </div>
+        </div>
+
+        <!-- 右侧知识点列表 -->
+        <div class="knowledge_list">
+          <!-- 暂无数据提示 -->
+          <div class="module_chart no_content_data no_data" style="height: 240px; " element-loading-background="#ffffff"
+            v-if="!knowledgeItems || knowledgeItems.length === 0">
+            <span>暂无数据</span>
+          </div>
+          <!-- 正常列表数据 -->
+
+          <div class="list_item" v-for="(item, index) in knowledgeItems" :key="index"
+            @click="handleItemClick(item, index)" :class="{ active: selectedIndex === index }" v-else>
+            <div class="item_header">
+              <span class="item_dot" :style="{ backgroundColor: getDotColor(item, 'personalScoreRate') }"></span>
+              <el-tooltip :content="item.knowledgeName" placement="top" effect="light"
+                :disabled="!item.knowledgeName || item.knowledgeName.length < 15">
+                <span class="item_title">{{ item.knowledgeName }}</span>
+              </el-tooltip>
+              <span class="item_tag" v-if="item.isPush == 1" style="background-color: #2E64FA;">
+                有推题
+              </span>
+            </div>
+
+            <div class="item_info">
+              <!-- 班级/学生得分率 -->
+              <span class="item_score">
+                <span class="score_label" v-if="item.personalScoreRate">个人得分率:</span>
+                <span :style="{ color: getDotColor(item, 'personalScoreRate') }" v-if="item.personalScoreRate">{{
+                  item.personalScoreRate }}%</span>
+              </span>
+
+              <!-- 班级/学生得分率 -->
+              <span class="item_exam_count">考试数: <span class="exam_count">{{ item.examNum || 0 }}</span></span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 列表查看容器 -->
+    <div v-if="activeView === 'list'" class="list_content">
+
+      <vxe-table ref="treeTable" :data="tableData" 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>
+          </template>
+        </vxe-table-column>
+      </vxe-table>
+    </div>
+  </div>
+</template>
+
+<script>
+// 引入echarts
+import * as echarts from 'echarts';
+
+export default {
+  name: 'CircularDiffusion',
+  props: {
+    activeView: { // 当前视图,graph或list 年级画像还是学生画像
+      type: String,
+      default: 'graph',
+      validator: (value) => {
+        return ['graph', 'list'].includes(value);
+      }
+    },
+    subjectName: {  // 科目名称
+      type: String,
+      default: '知识点'
+    },
+    subjectScoreRate: { // 科目得分率
+      type: Number,
+      default: 0
+    },
+    tableData: { // 知识点表格数据
+      type: Array,
+      default: () => []
+    },
+    fatalVulnerability: { // 零分知识点数据
+      type: Array,
+      default: () => []
+    },
+    highVulnerability: { // 高频错题知识点数据
+      type: Array,
+      default: () => []
+    },
+    allKnowledgeList: { // 全部知识点数据
+      type: Array,
+      default: () => []
+    },
+    classLevel: { // 判断是否是组合还是班级,还是年级
+      type: [String, Number],
+      default: ''
+    },
+    classGroupName: { // 班级组合名称
+      type: String,
+      default: ''
+    },
+    currentKnowledgeId: { // 当前选中的知识点ID
+      type: [String, Number],
+      default: ''
+    },
+  },
+  computed: {
+    // 根据当前tab返回对应的数据
+    knowledgeItems() {
+      if (this.activeTab === 'zero') {
+        return this.fatalVulnerability || [];
+      } else if (this.activeTab === 'highFreq') {
+        return this.highVulnerability || [];
+      } else if (this.activeTab === 'all') {
+        return this.allKnowledgeList || [];
+      }
+      return [];
+    }
+  },
+  data() {
+    return {
+      chart: null, // echarts实例
+      // 组件挂载状态标记
+      _isMounted: false,
+      // 首次渲染标志,用于控制初始化时的透明度逻辑
+      _isFirstRender: true,
+      // Tab切换状态
+      activeTab: 'all',
+      // 图例选中状态
+      selectedLegend: {
+        weak: true,
+        good: true,
+        excellent: true
+      },
+      // 选中的知识点索引
+      selectedIndex: 0,
+      // 当前选中的行数据
+      selectedRow: null,
+      // 当前选中的知识点ID,用于控制节点透明度
+      selectedKnowledgeId: '',
+      // 树形表格展开行keys
+      expandRowKeys: []
+    };
+  },
+  mounted() {
+    // 设置组件挂载状态
+    this._isMounted = true;
+
+    // 无论在什么模式下,只要默认视图是图形视图就初始化echarts图表
+    if (this.activeView === 'graph') {
+      this.$nextTick(() => {
+        this.initChart();
+      });
+    }
+  },
+  beforeDestroy() {
+    // 设置组件未挂载状态
+    this._isMounted = false;
+    // 销毁echarts实例
+    if (this.chart) {
+      this.chart.dispose();
+    }
+  },
+  watch: {
+    // 监听classLevel变化,当对比选择器数据变化时更新对比选项
+    classLevel: {
+      handler(newVal, oldVal) {
+
+      },
+      immediate: true
+    },
+    // 监听classGroupName变化,当班级组合名称变化时更新对比选项
+    classGroupName: {
+      handler(newVal, oldVal) {
+        if (newVal) {
+        }
+      },
+      immediate: true
+    },
+    // 监听activeView变化,当切换到图形视图时重新初始化图表,切换到列表视图时销毁图表
+    activeView(newView, oldView) {
+      if (newView === 'graph') {
+        this.$nextTick(() => {
+          this.initChart();
+        });
+      } else if (newView === 'list') {
+        // 切换到列表视图时销毁图表实例
+        if (this.chart) {
+          this.chart.dispose();
+          this.chart = null;
+        }
+        // 切换到列表视图时,展开所有树形节点
+        this.$nextTick(() => {
+          this.expandAllNodes();
+        });
+      }
+    },
+    // 监听tab切换,更新选中索引并通知父组件
+    activeTab(newTab, oldTab) {
+      this.selectedIndex = 0;
+
+      // 根据当前tab获取对应的数据列表
+      const currentList = this.knowledgeItems;
+      // 如果有数据,更新selectedKnowledgeId为第一条数据的ID
+      if (currentList && currentList.length > 0) {
+        this.selectedKnowledgeId = currentList[0].knowledgeId;
+        // 如果当前是图形视图且图表已初始化,更新图表选中状态
+        if (this.activeView === 'graph' && this.chart) {
+          this.updateChart();
+        }
+      } else {
+        // 如果没有数据,清空selectedKnowledgeId
+        this.selectedKnowledgeId = '';
+      }
+
+      // 只有当组件已挂载且tab值实际发生变化,并且不是初始化时,才向父组件发送事件
+      // 这是为了避免在视图切换时重复调用接口
+      if (this._isMounted && newTab !== oldTab) {
+        // 向父组件发送tab切换事件
+        this.$emit('activeTabChange', newTab);
+      }
+    },
+    // 监听数据变化,重新设置默认选中项
+    fatalVulnerability: {
+      deep: true,
+      handler() {
+        // 仅当当前视图是图形视图且数据发生变化时才更新默认选中
+        if (this.activeView === 'graph') {
+          this.setDefaultSelection();
+        }
+      }
+    },
+    highVulnerability: {
+      deep: true,
+      handler() {
+        // 仅当当前视图是图形视图且数据发生变化时才更新默认选中
+        if (this.activeView === 'graph') {
+          this.setDefaultSelection();
+        }
+      }
+    },
+    allKnowledgeList: {
+      deep: true,
+      handler() {
+        // 仅当当前视图是图形视图且数据发生变化时才更新默认选中
+        if (this.activeView === 'graph') {
+          this.setDefaultSelection();
+        }
+      }
+    },
+    // 监听tableData变化,更新展开行
+    tableData: {
+      deep: true,
+      handler() {
+        // 当tableData变化时,重新展开所有节点
+        if (this.activeView === 'list') {
+          this.$nextTick(() => {
+            this.expandAllNodes();
+          });
+        }
+        if (this.chart) {
+          this.$nextTick(() => {
+            this.updateChart();
+          });
+        }
+      }
+    },
+    // 监听当前选中的知识点ID变化,更新图表高亮状态
+    currentKnowledgeId: {
+      handler(newVal) {
+        // 更新selectedKnowledgeId,确保图表使用正确的高亮状态
+        this.selectedKnowledgeId = newVal;
+        // 仅当当前视图是图形视图且图表已初始化时才更新
+        if (this.activeView === 'graph' && this.chart) {
+          this.updateChart();
+        }
+      }
+    }
+  },
+  methods: {
+    // 设置默认选中项
+    setDefaultSelection() {
+      // 保存当前tab值,用于比较是否需要修改
+      const oldActiveTab = this.activeTab;
+      let selectedItem = null;
+
+      // 优先级:全部知识点> 高频错题知识点 > 零分知识点 
+      if (this.allKnowledgeList && this.allKnowledgeList.length > 0) {
+        // 如果前两者都没有数据但全部知识点有数据,切换到全部知识点并选中第一条
+        if (this.activeTab !== 'all') {
+          this.activeTab = 'all';
+        }
+        this.selectedIndex = 0;
+        selectedItem = this.allKnowledgeList[0];
+      } else if (this.highVulnerability && this.highVulnerability.length > 0) {
+        // 如果全部知识点没有数据但高频错题知识点有数据,切换到高频错题知识点并选中第一条
+        if (this.activeTab !== 'highFreq') {
+          this.activeTab = 'highFreq';
+        }
+        this.selectedIndex = 0;
+        selectedItem = this.highVulnerability[0];
+      } else if (this.fatalVulnerability && this.fatalVulnerability.length > 0) {
+        // 如果零分知识点有数据,切换到零分知识点并选中第一条
+        if (this.activeTab !== 'zero') {
+          this.activeTab = 'zero';
+        }
+        this.selectedIndex = 0;
+        selectedItem = this.fatalVulnerability[0];
+      }
+
+      // 如果有选中项,更新selectedKnowledgeId,实现初始化默认选择
+      if (selectedItem) {
+        this.selectedKnowledgeId = selectedItem.knowledgeId;
+        // 如果当前是图形视图且图表已初始化,更新图表选中状态
+        if (this.activeView === 'graph' && this.chart) {
+          this.updateChart();
+        }
+      }
+    },
+
+    // 切换查看方式
+    switchView(view) {
+      // 通过emit事件通知父组件更新activeView
+      this.$emit('view-change', view);
+    },
+
+    // 切换图例选中状态
+    toggleLegend(type) {
+      this.selectedLegend[type] = !this.selectedLegend[type];
+
+      // 生成scoreRateTypes数组
+      let scoreRateTypes = [];
+      if (this.selectedLegend.weak) scoreRateTypes.push(1);
+      if (this.selectedLegend.good) scoreRateTypes.push(2);
+      if (this.selectedLegend.excellent) scoreRateTypes.push(3);
+
+      // 如果包含所有值,则传null,否则传数组
+      if (scoreRateTypes.length === 3) {
+        scoreRateTypes = null;
+      }
+
+      // 向父组件发送图例变化事件,包含scoreRateTypes和当前activeTab对应的knowledgeType
+      let knowledgeType = 0;
+      if (this.activeTab === 'highFreq') {
+        knowledgeType = 1;
+      } else if (this.activeTab === 'zero') {
+        knowledgeType = 2;
+      } else {
+        knowledgeType = 0;
+      }
+
+      this.$emit('legend-change', { scoreRateTypes, knowledgeType });
+    },
+
+    // 根据得分率获取对应的颜色
+    getNodeColor(scoreRate) {
+      const rate = parseFloat(scoreRate);
+      if (rate >= 85) {
+        return '#3BA272'; // 优秀 - 绿色
+      } else if (rate >= 60) {
+        return '#FAC858'; // 良好 - 黄色
+      } else {
+        return '#EE6666'; // 薄弱 - 红色
+      }
+    },
+
+    // 初始化echarts图表
+    initChart() {
+      try {
+        // 检查chart容器是否存在
+        if (!this.$refs.chart) {
+          console.error('图表容器不存在');
+          return;
+        }
+        this.createChart();
+      }
+      catch (error) {
+        console.error('图表初始化失败:', error);
+      }
+    },
+
+    // 创建图表
+    createChart() {
+      try {
+        // 检查chart容器是否存在
+        if (!this.$refs.chart) {
+          console.error('图表容器不存在');
+          return;
+        }
+        // 销毁现有的图表实例
+        if (this.chart) {
+          this.chart.dispose();
+        }
+        // 创建echarts实例
+        this.chart = echarts.init(this.$refs.chart);
+
+        // 保存当前组件实例的引用
+        const vm = this;
+
+        // 根据得分率获取对应的级别
+        const getNodeLevel = (scoreRate) => {
+          const rate = parseFloat(scoreRate);
+          // 处理scoreRate为null、undefined或非数字的情况
+          if (isNaN(rate)) {
+            return 'weak'; // 默认为薄弱
+          }
+          if (rate >= 85) {
+            return 'excellent'; // 优秀
+          } else if (rate >= 60) {
+            return 'good'; // 良好
+          } else {
+            return 'weak'; // 薄弱
+          }
+        };
+
+        // 基于表格数据生成树形图数据
+        // 添加一个可选参数overrideKnowledgeId,用于临时覆盖当前的currentKnowledgeId
+        const generateTreeData = (data, level = 0, overrideKnowledgeId = null) => {
+          // 使用传入的overrideKnowledgeId或默认使用vm.selectedKnowledgeId
+          const currentKnowledgeId = overrideKnowledgeId !== null ? overrideKnowledgeId : vm.selectedKnowledgeId;
+
+          return data.map(item => {
+            // 检查当前节点是否匹配选中的知识点ID
+            const isMatched = String(item.knowledgeId) === String(currentKnowledgeId);
+
+            // 获取节点级别,优先使用对应级别的得分率,若缺失则使用另一级别作为备选
+            const scoreRate = item.personalScoreRate;
+            const hasData = scoreRate !== undefined && scoreRate !== null && scoreRate !== '';
+            
+            // 设置节点大小:有数据的默认10px,选中后15px;无数据的5px
+            let symbolSize = 5;
+            if (hasData) {
+              symbolSize = isMatched ? 15 : 10;
+            }
+
+            // 计算节点样式
+            const itemColor = hasData ? vm.getNodeColor(scoreRate) : '#BFC1C7';
+            
+            // 获取节点级别用于图例过滤
+            const nodeLevel = getNodeLevel(scoreRate);
+            // 应用图例过滤
+            const shouldShow = vm.selectedLegend[nodeLevel];
+            
+            // 设置节点透明度:显示为1,隐藏为0
+            const opacity = shouldShow ? 1 : 0;
+            
+            const node = {
+              name: item.knowledgeName,
+              symbolSize: symbolSize,
+              // 使用已计算的scoreRate作为value,确保数据一致性
+              value: scoreRate,
+              itemStyle: {
+                color: itemColor,
+                opacity: opacity
+              },
+              // 当节点被隐藏时,隐藏连接到该节点的线
+              lineStyle: {
+                opacity: shouldShow ? 1 : 0
+              },
+              // 保存知识ID用于匹配
+              knowledgeId: item.knowledgeId
+            };
+
+            if (item.children && item.children.length > 0) {
+              node.children = generateTreeData(item.children, level + 1, overrideKnowledgeId);
+            }
+
+            return node;
+          });
+        };
+
+        // 计算最外层知识点数量(叶子节点数量),用于动态调整布局
+        const countOutermostNodes = (data) => {
+          let count = 0;
+          const traverse = (nodes) => {
+            nodes.forEach(node => {
+              if (node.children && node.children.length > 0) {
+                traverse(node.children);
+              } else {
+                count++;
+              }
+            });
+          };
+          traverse(data);
+          return count;
+        };
+        
+        // 获取最外层知识点数量
+        const outermostCount = countOutermostNodes(this.tableData);
+        
+        // 计算合适的缩放比例,根据屏幕宽度和节点数量动态调整
+        const getOptimalZoom = () => {
+          const screenWidth = window.innerWidth;
+          let zoom = 1.05;
+          
+          // 根据节点数量调整缩放,节点少的时候适当放大
+          if (outermostCount <= 5) {
+            zoom *= 1.0;
+          }
+          
+          return zoom;
+        };
+        
+        // 根据节点数量动态调整内外半径,防止节点遮盖
+        const getOptimalRadius = () => {
+          // 节点数量少的时候,增大内半径,缩小外半径,使节点分布更合理
+          if (outermostCount <= 5) {
+            return ['20%', '65%'];
+          }
+          // 节点数量多的时候,使用默认半径
+          return ['12%', '75%'];
+        };
+        
+        // 配置项
+        const option = {
+          tooltip: {
+            formatter: (params) => {
+              // 检查是否为根节点
+              if (params.treePathInfo && params.treePathInfo.length === 1) {
+                // 根节点也属于非最外层节点,只显示名称
+                return params.name;
+              }
+
+              // 检查节点是否为最外层节点(叶子节点)
+              // 使用isOutermost属性或检查data.isOutermost
+              const isOutermost = params.data && params.data.isOutermost;
+
+              if (isOutermost) {
+                // 最外层节点显示完整信息(名称+得分率)
+                return `${params.name}<br/>得分率: ${params.value !== null && params.value !== undefined ? params.value : '0'}%`;
+              } else {
+                // 非最外层节点只显示名称
+                return params.name;
+              }
+            }
+          },
+          animationDurationUpdate: 1500,
+          animationEasingUpdate: 'quinticInOut',
+          series: [
+            {
+              type: 'tree',
+              layout: 'radial',
+              symbol: 'circle',
+              initialTreeDepth: 999, // 设置一个足够大的值,确保所有节点都展开
+              expandAndCollapse: false,
+              // 调整树状图布局参数,确保节点均匀分布
+              orient: 'radial',
+              roam: false, // 禁用缩放和平移
+              
+              // 使用边距控制图表位置,实现水平垂直居中
+              left: '8%',   // 左边距(百分比/像素)
+              right: '8%',  // 右边距
+              top: '8%',    // 上边距
+              bottom: '8%', // 下边距
+
+              // 根据节点数量动态调整内外半径,防止节点遮盖
+              radius: getOptimalRadius(),
+
+              // 调整节点大小比例和间距,确保节点不遮盖
+              nodeScaleRatio: 1,
+              // 根据节点数量调整层级间距,节点少的时候增大间距
+              layerPadding: outermostCount <= 5 ? [20, 10] : [10, 5],
+
+              // 使用表格数据生成树形结构
+              data: [
+                {
+                  name: this.subjectName,
+                  symbolSize: 5, // 根节点大小
+                  value: this.subjectScoreRate, // 添加value属性,用于显示得分率
+                  itemStyle: {
+                    // 根节点为非最外层节点,使用#EBEEF5颜色
+                    color: '#BFC1C7'
+                  },
+                  // 标记为非最外层节点
+                  isOutermost: false,
+                  children: generateTreeData(this.tableData)
+                }
+              ],
+              label: {
+                show: false
+              },
+              lineStyle: {
+                color: '#ECEEF3',
+                width: 1,
+                type: 'solid'
+              },
+              emphasis: {
+                focus: 'adjacency',
+                lineStyle: {
+                  width: 1
+                }
+              }
+            }
+          ]
+        };
+
+        // 设置配置项
+        this.chart.setOption(option);
+
+        // 初始化后调用resize确保图表正确显示
+        this.chart.resize();
+
+        // 添加图表点击事件监听
+        this.chart.on('click', (params) => {
+          // 明确识别点击类型
+          const isOuterNodeClick = params.data && params.data.isOutermost;
+          const isEmptyAreaClick = !params.data || !params.componentType || params.componentType === '';
+          const isNonOuterNodeClick = params.data && !params.data.isOutermost;
+
+          if (isOuterNodeClick) {
+            // 处理最外层节点点击
+            // 确定当前显示的列表数据
+            let targetList = [];
+            if (this.activeTab === 'zero') {
+              targetList = this.fatalVulnerability;
+            } else if (this.activeTab === 'highFreq') {
+              targetList = this.highVulnerability;
+            } else {
+              targetList = this.allKnowledgeList;
+            }
+
+            // 1. 首先尝试直接从当前列表中根据名称查找
+            let knowledgeItem = targetList.find(item => item.knowledgeName === params.name);
+            let targetIndex = -1;
+
+            if (knowledgeItem) {
+              targetIndex = targetList.findIndex(item => item.knowledgeId === knowledgeItem.knowledgeId);
+            }
+
+            // 2. 如果在当前列表中找不到,尝试从其他列表中查找
+            if (!knowledgeItem) {
+              const allLists = [this.allKnowledgeList, this.highVulnerability, this.fatalVulnerability];
+              for (const list of allLists) {
+                knowledgeItem = list.find(item => item.knowledgeName === params.name);
+                if (knowledgeItem) {
+                  // 找到后切换到对应tab
+                  if (list === this.highVulnerability) {
+                    this.activeTab = 'highFreq';
+                    targetList = this.highVulnerability;
+                  } else if (list === this.fatalVulnerability) {
+                    this.activeTab = 'zero';
+                    targetList = this.fatalVulnerability;
+                  } else {
+                    this.activeTab = 'all';
+                    targetList = this.allKnowledgeList;
+                  }
+                  targetIndex = targetList.findIndex(item => item.knowledgeId === knowledgeItem.knowledgeId);
+                  break;
+                }
+              }
+            }
+
+            if (knowledgeItem && targetIndex !== -1) {
+              // 向父组件发送事件
+              this.$emit('knowledge-item-click', {
+                item: knowledgeItem,
+                index: targetIndex
+              });
+
+              // 更新选中索引和选中知识点ID
+              this.selectedIndex = targetIndex;
+              this.selectedKnowledgeId = knowledgeItem.knowledgeId;
+
+              // 重新生成图表数据,确保只有当前选中节点高亮
+              this.chart.setOption({
+                series: [{
+                  data: [
+                    {
+                      name: this.subjectName,
+                      symbolSize: 5,
+                      value: this.subjectScoreRate,
+                      itemStyle: {
+                        color: '#BFC1C7',
+                        opacity: 1
+                      },
+                      isOutermost: false,
+                      children: generateTreeData(this.tableData, 0, this.selectedKnowledgeId)
+                    }
+                  ]
+                }]
+              }, {
+                animation: true,
+                animationDuration: 300
+              });
+
+              // 滚动到选中项
+              this.$nextTick(() => {
+                const listContainer = this.$el.querySelector('.knowledge_list');
+                const listItems = listContainer.querySelectorAll('.list_item');
+                if (listItems[targetIndex]) {
+                  listContainer.scrollTop = listItems[targetIndex].offsetTop - 100;
+                }
+              });
+            }
+          }
+          else if (isEmptyAreaClick || isNonOuterNodeClick) {
+            // 点击空白区域或非最外层节点,恢复所有节点透明度
+            this.selectedKnowledgeId = '';
+
+            // 调用generateTreeData时传入overrideKnowledgeId为空字符串,确保所有节点都不透明
+            const updatedOption = {
+              series: [{
+                data: [
+                  {
+                    name: this.subjectName,
+                    symbolSize: 5,
+                    value: this.subjectScoreRate,
+                    itemStyle: {
+                      color: '#BFC1C7',
+                      opacity: 1
+                    },
+                    isOutermost: false,
+                    children: generateTreeData(this.tableData, 0, '')
+                  }
+                ]
+              }]
+            };
+            // 更新图表,使用动画实现平滑过渡
+            this.chart.setOption(updatedOption, {
+              animation: true,
+              animationDuration: 300
+            });
+
+            // 向父组件发送事件,清除选中的知识点ID
+            this.$emit('knowledge-item-click', {
+              item: null,
+              index: -1
+            });
+          }
+        });
+
+        // 监听窗口大小变化
+        window.addEventListener('resize', () => {
+          if (this.chart) {
+            this.chart.resize();
+          }
+        });
+      }
+      catch (error) {
+        console.error('图表创建失败:', error);
+      }
+    },
+
+    // 更新图表数据
+    updateChart() {
+      try {
+        if (this.activeView === 'graph' && this.$refs.chart) {
+          // 重新创建图表,实现刷新效果
+          this.createChart();
+        }
+      }
+      catch (error) {
+        console.error('图表更新失败:', error);
+      }
+    },
+
+    // 根据得分率获取对应的样式类
+    getRateClass(rate) {
+      const rateValue = parseFloat(rate);
+      if (rateValue >= 85) {
+        return 'rate_excellent';
+      } else if (rateValue >= 60) {
+        return 'rate_good';
+      } else {
+        return 'rate_weak';
+      }
+    },
+
+    // 根据得分率获取对应的状态名称
+    getRateName(rate) {
+      const rateValue = parseFloat(rate);
+      if (rateValue >= 85) {
+        return '优秀';
+      } else if (rateValue >= 60) {
+        return '良好';
+      } else {
+        return '薄弱';
+      }
+    },
+
+    // 处理知识点列表项点击事件
+    handleItemClick(item, index) {
+      // 更新选中索引
+      this.selectedIndex = index;
+      // 更新选中知识点ID,实现反向选择功能
+      this.selectedKnowledgeId = item.knowledgeId;
+      // 向父组件发送事件,传递点击的知识点数据
+      this.$emit('knowledge-item-click', { item, index });
+
+      // 如果当前是图形视图且图表已初始化,更新图表选中状态
+      if (this.activeView === 'graph' && this.chart) {
+        this.updateChart();
+      }
+    },
+
+    // 处理树形表格行点击事件
+    handleRowClick(row) {
+      // 检查是否为最后一级节点(没有children或children为空)
+      const isLastLevel = !row.children || row.children.length === 0;
+
+      if (isLastLevel) {
+        // 调用handleItemClick方法处理点击事件
+        this.handleItemClick(row, -1);
+      }
+    },
+
+    // 处理单元格点击事件
+    handleCellClick({ row }) {
+      // 调用行点击事件处理逻辑
+      this.handleRowClick(row);
+    },
+
+    // 根据知识点得分率获取对应的点颜色
+    getDotColor(item, scoreRateType) {
+      const scoreRate = scoreRateType === 'personalScoreRate' ? item.personalScoreRate : item.classScoreRate;
+      const rate = parseFloat(scoreRate);
+      if (rate >= 85) {
+        return '#3BA272'; // 优秀 - 绿色
+      } else if (rate >= 60 && rate < 84) {
+        return '#FAC858'; // 良好 - 黄色
+      } else if (rate < 59) {
+        return '#EE6666'; // 薄弱 - 红色
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.knowledge_graph {
+  background: #FFFFFF;
+  border-radius: 10px;
+  padding: 20px;
+  margin-bottom: 10px;
+  position: relative;
+
+  /* 选中行样式 - 确保所有情况下都能正确应用 */
+  :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;
+    }
+  }
+
+  .graph_header {
+    margin-bottom: 20px;
+    position: relative;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .legend {
+      display: flex;
+      gap: 20px;
+
+      .legend_item {
+        display: flex;
+        align-items: center;
+        gap: 5px;
+        cursor: pointer;
+        opacity: 0.7;
+        transition: all 0.3s ease;
+        font-weight: 500;
+
+        &.selected {
+          opacity: 1;
+        }
+
+        &:hover {
+          opacity: 1;
+        }
+
+        .legend_dot {
+          width: 20px;
+          height: 10px;
+          border-radius: 2px;
+          transition: all 0.3s ease;
+          background: #999999; // 默认灰色
+
+          &.weak {
+            background: #EE6666;
+          }
+
+          &.good {
+            background: #FAC858;
+          }
+
+          &.excellent {
+            background: #3BA272;
+          }
+        }
+
+        .legend_text {
+          font-size: 12px;
+          color: #999999; // 默认灰色文字
+          transition: all 0.3s ease;
+        }
+
+        // 选中状态样式
+        &.selected {
+          .legend_dot.weak {
+            background: #EE6666;
+          }
+
+          .legend_dot.good {
+            background: #FAC858;
+          }
+
+          .legend_dot.excellent {
+            background: #3BA272;
+          }
+
+          .legend_text {
+            color: #333;
+          }
+        }
+      }
+
+    }
+  }
+
+  // 视图切换按钮样式
+  .view_switcher_container {
+    display: flex;
+    gap: 20px;
+    z-index: 10;
+    justify-content: flex-end;
+    font-size: 14px;
+
+    .switch_btn {
+      display: flex;
+      align-items: center;
+      padding: 0;
+      background-color: transparent;
+      border: none;
+      border-radius: 0;
+      font-size: 14px;
+      transition: color 0.3s ease;
+      cursor: pointer;
+      font-weight: 400;
+      color: #999999;
+
+      &:hover {
+        color: #2E64FA;
+      }
+    }
+
+    .graph_btn {
+      color: #2E64FA;
+    }
+
+    // 图标样式,确保与文字对齐
+    .icon_switch_graph,
+    .icon_switch_list {
+      img {
+        display: inline-block;
+        vertical-align: middle;
+        margin: 0;
+        padding: 0;
+        margin-top: -3px;
+      }
+    }
+  }
+
+  .graph_content {
+    display: flex;
+    gap: 20px;
+    position: relative;
+
+    .knowledge_tab {
+      display: flex;
+      border-bottom: 1px solid #ECEEF3;
+      margin-bottom: 15px;
+
+      .tab_item {
+        height: 40px;
+        line-height: 40px;
+        cursor: pointer;
+        font-size: 14px;
+        color: #666666;
+        position: relative;
+        transition: all 0.3s ease;
+        margin-right: 20px;
+
+        &.active {
+          color: #2E64FA;
+          font-weight: 500;
+
+          &::after {
+            content: '';
+            position: absolute;
+            bottom: -1px;
+            left: 0;
+            width: 100%;
+            height: 2px;
+            background-color: #2E64FA;
+          }
+        }
+
+        &:hover {
+          color: #2E64FA;
+        }
+      }
+    }
+
+    .knowledge_right_container {
+      width: 350px;
+      display: flex;
+      flex-direction: column;
+    }
+
+    .chart_container {
+      flex: 1;
+      min-width: 0;
+      border-radius: 10px 10px 10px 10px;
+      border: 1px solid #E9EBEF;
+      position: relative;
+      padding: 20px;
+      overflow: hidden;
+
+      .top_container {
+        display: flex;
+        justify-content: space-between;
+        align-items: flex-start;
+        flex-wrap: wrap;
+        gap: 10px;
+        margin-bottom: 10px;
+      }
+
+      .legend {
+        display: flex;
+        gap: 20px;
+        flex-wrap: wrap; // 小屏幕下自动换行
+        flex: 1;
+        min-width: 0;
+
+        .legend_item {
+          display: flex;
+          align-items: center;
+          gap: 5px;
+          cursor: pointer;
+          opacity: 0.7;
+          transition: all 0.3s ease;
+          font-weight: 500;
+
+          &.selected {
+            opacity: 1;
+          }
+
+          &:hover {
+            opacity: 1;
+          }
+
+          .legend_dot {
+            width: 20px;
+            height: 10px;
+            border-radius: 2px;
+            transition: all 0.3s ease;
+            background: #999999; // 默认灰色
+
+            &.weak {
+              background: #EE6666;
+            }
+
+            &.good {
+              background: #FAC858;
+            }
+
+            &.excellent {
+              background: #3BA272;
+            }
+          }
+
+          .legend_text {
+            font-size: 12px;
+            color: #999999; // 默认灰色文字
+            transition: all 0.3s ease;
+          }
+
+          // 选中状态样式
+          &.selected {
+            .legend_dot.weak {
+              background: #EE6666;
+            }
+
+            .legend_dot.good {
+              background: #FAC858;
+            }
+
+            .legend_dot.excellent {
+              background: #3BA272;
+            }
+
+            .legend_text {
+              color: #333;
+            }
+          }
+        }
+
+      }
+
+      .chart {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        min-height: 590px;
+      }
+
+      .view_switcher_container {
+        display: flex;
+        gap: 20px;
+        z-index: 10;
+        justify-content: flex-end;
+        font-size: 14px;
+        white-space: nowrap; // 防止按钮换行
+      }
+    }
+
+    .knowledge_list {
+      width: 100%;
+      height: 630px;
+      overflow-y: auto;
+      padding-right: 10px;
+      position: relative;
+
+      /* 滚动条样式 */
+      &::-webkit-scrollbar {
+        width: 6px;
+      }
+
+      &::-webkit-scrollbar-track {
+        background: #f1f1f1;
+        border-radius: 3px;
+      }
+
+      &::-webkit-scrollbar-thumb {
+        background: #c1c1c1;
+        border-radius: 3px;
+      }
+
+      &::-webkit-scrollbar-thumb:hover {
+        background: #a8a8a8;
+      }
+
+      /* 暂无数据样式 */
+      .no_data {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        font-size: 14px;
+        color: #909399;
+        text-align: center;
+        width: 100%;
+        height: 100%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        border-radius: 8px;
+
+        span {
+          margin-top: 23%;
+        }
+      }
+
+      .list_item {
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 6px;
+        padding: 10px;
+        margin-bottom: 10px;
+        cursor: pointer;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+
+        &:hover,
+        &.active {
+          background: rgba(46, 100, 250, 0.1);
+          border-radius: 6px;
+        }
+
+        .item_header {
+          display: flex;
+          align-items: center;
+          margin-bottom: 8px;
+
+          .item_dot {
+            display: inline-block;
+            width: 8px;
+            height: 8px;
+            background: #EE6666;
+            border-radius: 50%;
+            margin-right: 8px;
+          }
+
+          .item_title {
+            font-size: 14px;
+            font-weight: 500;
+            color: #333;
+            flex: 1;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            min-width: 0;
+          }
+
+          .item_tag {
+            color: #FFFFFF;
+            font-size: 12px;
+            padding: 4px 6px;
+            margin-left: 5px;
+            text-align: center;
+            border-radius: 4px;
+            font-weight: 400;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 4px;
+          }
+        }
+
+        .item_info {
+          font-size: 12px;
+          color: #999;
+          margin-top: 8px;
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+
+          .item_score {
+            text-align: left;
+
+            .score_label {
+              color: #999999;
+            }
+
+            .score_second {
+              color: #666666;
+            }
+          }
+
+          .item_exam_count {
+            text-align: right;
+            color: #999999;
+
+            .exam_count {
+              color: #2E64FA;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // vxe表格样式
+  .list_content {
+    border-radius: 10px 10px 10px 10px;
+
+    .vxe-table {
+      // 展开收起的样式
+      :deep(.el-icon-circle-plus) {
+        color: #DCDFE6 !important;
+        font-size: 16px !important;
+      }
+
+      :deep(.el-icon-remove) {
+        color: #2E64FA !important;
+        font-size: 16px !important;
+      }
+    }
+
+    /* 设置表格行高度和光标样式 */
+    :deep {
+      .vxe-table {
+        border-radius: 8px;
+        overflow-y: auto;
+        overflow-x: hidden;
+
+        // 使用CSS变量设置全局行高
+        --vxe-ui-table-row-height-default: 20px !important;
+
+        // 表头样式
+        .vxe-table--header {
+          display: table-header-group !important;
+          visibility: visible !important;
+          opacity: 1 !important;
+          height: 52px !important;
+        }
+
+        /* 设置表头高度 */
+        .vxe-table--header {
+          .vxe-header--row {
+            height: 52px !important;
+          }
+        }
+
+        // 行样式
+        .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;
+              }
+
+              &.row_highlight {
+                background-color: rgba(195, 219, 255, 0.2);
+                cursor: pointer !important;
+              }
+
+              &.selected-row {
+                background-color: rgba(195, 219, 255, 0.2) !important;
+                color: #2E64FA !important;
+                cursor: pointer !important;
+              }
+            }
+          }
+        }
+
+        // 单元格样式
+        .vxe-body--column,
+        .vxe-header--column {
+          padding: 0 !important;
+          height: 20px !important;
+          line-height: 20px !important;
+          min-height: 20px !important;
+          max-height: 20px !important;
+        }
+
+        // 表格主体最小高度设置
+        .vxe-table--body {
+          min-height: 0 !important;
+        }
+
+        /* 自定义树形图标样式 */
+        .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;
+        }
+
+        /* 展开状态的节点 */
+        .vxe-tree-icon--minus {
+          background-color: #409EFF;
+          color: white;
+        }
+
+        /* 可展开但未展开的节点 */
+        .vxe-tree-icon--plus {
+          background-color: #C0C4CC;
+          color: white;
+        }
+
+        // 斑马纹
+        .vxe-table--body {
+          tr:nth-child(even) {
+            background-color: #F5F7FA;
+          }
+        }
+      }
+    }
+
+    .knowledge_item {
+      display: flex;
+      align-items: center;
+
+      .knowledge_title {
+        font-size: 14px;
+        color: #606266;
+      }
+    }
+
+    .rate_info {
+      align-items: center;
+      gap: 8px;
+      justify-content: center;
+      width: 80px;
+      margin: 0 auto;
+      position: relative;
+    }
+
+    .rate_dot {
+      display: inline-block;
+      width: 6px;
+      height: 6px;
+      border-radius: 50%;
+      position: absolute;
+      top: 8px;
+      left: 2px;
+
+      &.rate_excellent {
+        background-color: #3BA272;
+      }
+
+      &.rate_good {
+        background-color: #FAC858;
+      }
+
+      &.rate_weak {
+        background-color: #EE6666;
+      }
+    }
+
+    .rate_value {
+      font-size: 14px;
+      color: #606266;
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,331 @@
+<template>
+  <div class="region-container">
+    <!-- 薄弱补强区 -->
+    <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>
+      </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>
+          <div class="card-info">
+            <span class="score-rate">得分率:{{ item.personalScoreRate || 0 }}%</span>
+            <span class="wrong-count">错题:{{ item.wrongCount || 0 }}</span>
+          </div>
+          <div class="card-action">
+            <el-button type="primary" size="small" @click="handleAddPractice(item)">加入练习</el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 能力提升区 -->
+    <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>
+      </div>
+      <div class="region-desc">60%≤得分率<85% | 强化练习提升</div>
+      <div class="knowledge-cards">
+        <div class="knowledge-card" v-for="(item, index) in goodArea" :key="'good-' + index">
+          <div class="card-header">
+            <span class="knowledge-name">{{ item.knowledgeName }}</span>
+          </div>
+          <div class="card-info">
+            <span class="score-rate">得分率:{{ item.personalScoreRate || 0 }}%</span>
+            <span class="wrong-count">错题:{{ item.wrongCount || 0 }}</span>
+          </div>
+          <div class="card-action">
+            <el-button type="warning" size="small" @click="handleAddPractice(item)">加入练习</el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 扎实巩固区 -->
+    <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>
+      </div>
+      <div class="region-desc">得分率≥85% | 定期练习巩固</div>
+      <div class="knowledge-cards">
+        <div class="knowledge-card" v-for="(item, index) in excellentArea" :key="'excellent-' + index">
+          <div class="card-header">
+            <span class="knowledge-name">{{ item.knowledgeName }}</span>
+          </div>
+          <div class="card-info">
+            <span class="score-rate">得分率:{{ item.personalScoreRate || 0 }}%</span>
+            <span class="wrong-count">错题:{{ item.wrongCount || 0 }}</span>
+          </div>
+          <div class="card-action">
+            <el-button type="success" size="small" @click="handleAddPractice(item)">加入练习</el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 冲刺进阶区 -->
+    <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>
+      </div>
+      <div class="region-desc">优于同级学生 | 对标上游冲刺进阶</div>
+      <div class="knowledge-cards">
+        <div class="knowledge-card" v-for="(item, index) in advancedArea" :key="'advanced-' + index">
+          <div class="card-header">
+            <span class="knowledge-name">{{ item.knowledgeName }}</span>
+          </div>
+          <div class="card-info">
+            <span class="score-rate">得分率:{{ item.personalScoreRate || 0 }}%</span>
+            <span class="wrong-count">错题:{{ item.wrongCount || 0 }}</span>
+          </div>
+          <div class="card-action">
+            <el-button type="primary" size="small" @click="handleAddPractice(item)">加入练习</el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'Region',
+  props: {
+    // 全部知识点数据
+    allKnowledgeList: {
+      type: Array,
+      default: () => []
+    }
+  },
+  computed: {
+    // 薄弱补强区:得分率 < 60%
+    weakArea() {
+      return this.allKnowledgeList.filter(item => {
+        const rate = parseFloat(item.personalScoreRate);
+        return !isNaN(rate) && rate < 60;
+      });
+    },
+    // 能力提升区:60% ≤ 得分率 < 85%
+    goodArea() {
+      return this.allKnowledgeList.filter(item => {
+        const rate = parseFloat(item.personalScoreRate);
+        return !isNaN(rate) && rate >= 60 && rate < 85;
+      });
+    },
+    // 扎实巩固区:得分率 ≥ 85%
+    excellentArea() {
+      return this.allKnowledgeList.filter(item => {
+        const rate = parseFloat(item.personalScoreRate);
+        return !isNaN(rate) && rate >= 85;
+      });
+    },
+    // 冲刺进阶区:预留区域,可根据需要扩展
+    advancedArea() {
+      return [];
+    }
+  },
+  methods: {
+    // 处理加入练习
+    handleAddPractice(item) {
+      this.$emit('add-practice', item);
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.region-container {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  padding: 20px;
+}
+
+.region-section {
+  border-radius: 12px;
+  padding: 20px;
+  background: #FFFFFF;
+  position: relative;
+
+  &.weak {
+    border: 2px solid #EE6666;
+  }
+
+  &.good {
+    border: 2px solid #FAC858;
+  }
+
+  &.excellent {
+    border: 2px solid #3BA272;
+  }
+
+  &.advanced {
+    border: 2px solid #2E64FA;
+  }
+}
+
+.region-header {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 4px;
+
+  .region-icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+
+    svg {
+      width: 20px;
+      height: 20px;
+    }
+  }
+
+  &.weak .region-icon {
+    color: #EE6666;
+  }
+
+  &.good .region-icon {
+    color: #FAC858;
+  }
+
+  &.excellent .region-icon {
+    color: #3BA272;
+  }
+
+  &.advanced .region-icon {
+    color: #2E64FA;
+  }
+
+  .region-title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #333333;
+  }
+}
+
+.region-desc {
+  font-size: 14px;
+  color: #999999;
+  margin-bottom: 16px;
+  padding-left: 28px;
+}
+
+.knowledge-cards {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+}
+
+.knowledge-card {
+  border: 1px solid #ECEEF3;
+  border-radius: 8px;
+  padding: 12px 16px;
+  background: #FFFFFF;
+  min-width: 200px;
+  flex: 1;
+  max-width: calc(25% - 12px);
+  transition: all 0.3s ease;
+
+  &:hover {
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+    transform: translateY(-2px);
+  }
+
+  .card-header {
+    margin-bottom: 8px;
+
+    .knowledge-name {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333333;
+      display: block;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
+  .card-info {
+    display: flex;
+    gap: 16px;
+    margin-bottom: 10px;
+
+    .score-rate,
+    .wrong-count {
+      font-size: 12px;
+      color: #666666;
+    }
+  }
+
+  .card-action {
+    .el-button {
+      width: 100%;
+    }
+  }
+}
+
+/* 薄弱补强区卡片样式 */
+.weak .knowledge-card {
+  border-color: rgba(238, 102, 102, 0.3);
+
+  .card-action .el-button {
+    background-color: #EE6666;
+    border-color: #EE6666;
+  }
+}
+
+/* 能力提升区卡片样式 */
+.good .knowledge-card {
+  border-color: rgba(250, 200, 88, 0.3);
+
+  .card-action .el-button {
+    background-color: #FAC858;
+    border-color: #FAC858;
+    color: #333333;
+  }
+}
+
+/* 扎实巩固区卡片样式 */
+.excellent .knowledge-card {
+  border-color: rgba(59, 162, 114, 0.3);
+
+  .card-action .el-button {
+    background-color: #3BA272;
+    border-color: #3BA272;
+  }
+}
+
+/* 冲刺进阶区卡片样式 */
+.advanced .knowledge-card {
+  border-color: rgba(46, 100, 250, 0.3);
+
+  .card-action .el-button {
+    background-color: #2E64FA;
+    border-color: #2E64FA;
+  }
+}
+</style>

+ 13 - 0
src/views/targetImprove/practice/practiceAnswer.vue

@@ -0,0 +1,13 @@
+<template>
+    <div class="practiceAnswer">
+        <div class="title">
+            <span>练习答案</span>
+        </div>
+    </div>
+</template>
+
+<style scoped lang="scss">
+.practiceAnswer {
+  background-color: #ffffff;
+}
+</style>

+ 13 - 0
src/views/targetImprove/practice/practiceResults.vue

@@ -0,0 +1,13 @@
+<template>
+    <div class="practiceAnswer">
+        <div class="title">
+            <span>练习结果</span>
+        </div>
+    </div>
+</template>
+
+<style scoped lang="scss">
+.practiceAnswer {
+  background-color: #ffffff;
+}
+</style>