Explorar o código

修改练习按钮和图谱暂无数据

吴朋磊 hai 1 mes
pai
achega
9b2d31dd6a

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

@@ -69,15 +69,14 @@ const targetImprove = {
   knowledgeGraph(data) {
     return post( base.prefix+ `/api/v1/studentTarge/knowledge_graph`, data)
   },
-  
-  // 知识图谱 - 知识点分区
-  knowledgePartition(data) {
-    return post( base.prefix+ `/api/v1/studentTarge/knowledge_partition`, data)
-  },
   // 加入练习
   addExercise(data) {
     return post( base.prefix+ `/api/v1/studentTarge/add_exercise`, data)
   },
+  // 知识点练习作答-练习数量
+  waitExerciseNum(data) {
+    return get( base.prefix+ `/api/v1/studentTarge/wait_exercise_num`, data)
+  },
 }
 
 export default targetImprove

+ 77 - 6
src/views/targetImprove/practice/index.vue

@@ -7,8 +7,15 @@
       </div>
       <div class="step_line">
       </div>
-      <div class="step_item" :class="stepIndex==2?'step_item_cur':''" @click="ChangeStep(2)">
-        <div class="span_number">2</div>知识点练习作答 
+      <div
+        class="step_item js-practice-answer-step"
+        :class="stepIndex==2?'step_item_cur':''"
+        @click="ChangeStep(2)">
+        <div class="span_number">2</div>
+        <div class="step_item_label">
+          <span class="step_text">知识点练习作答</span>
+          <span class="step_count_badge" v-if="waitExerciseCount > 0">{{ waitExerciseCount }}</span>
+        </div>
       </div>
       <div class="step_line">
       </div>
@@ -21,7 +28,7 @@
 
     </div>
     <!-- 知识点图谱 -->
-    <knowledgeGraph v-if="stepIndex==1"></knowledgeGraph>
+    <knowledgeGraph v-if="stepIndex==1" @add-practice-success="FetchWaitExerciseNum"></knowledgeGraph>
     <PracticeAnswer v-if="stepIndex==2"></PracticeAnswer>
     <PracticeResults v-if="stepIndex==3"></PracticeResults>
   </div>
@@ -44,6 +51,7 @@ export default {
       userInfo: {},
 
       stepIndex:1,//步骤
+      waitExerciseCount: 0,//知识点练习作答待练习数量
     }
   },
   computed:{
@@ -56,14 +64,15 @@ export default {
       console.log('科目编码改变了', newVal, oldVal)
       // ========= 业务逻辑写这里 =========
       // 例如:切换科目,重置步骤、重新请求知识点图谱数据
-
-      
+      if (newVal) {
+        this.FetchWaitExerciseNum();
+      }
     }
   },
 
   mounted() 
   {
-
+    this.FetchWaitExerciseNum();
   },
   methods: { 
 
@@ -80,10 +89,72 @@ export default {
     ChangeStep(index)
     {
       this.stepIndex = index;
+      if (index === 1 || index === 2) {
+        this.FetchWaitExerciseNum();
+      }
+    },
+
+    // 获取知识点练习作答待练习数量
+    FetchWaitExerciseNum()
+    {
+      const levelCode = localStorage.getItem('student_level_code') || '';
+      const subjectCode = this.subjectCode || '';
+      this.$api.target.waitExerciseNum({
+        levelCode,
+        subjectCode
+      }).then(res => {
+        if (res && (res.code == 200 || res.code == 0)) {
+          const data = res.data;
+          const num = Number(
+            (data && typeof data === 'object')
+              ? (data.num ?? data.count ?? data.waitExerciseNum ?? data.waitNum ?? 0)
+              : data
+          );
+          this.waitExerciseCount = isNaN(num) ? 0 : num;
+        } else {
+          this.waitExerciseCount = 0;
+        }
+      }).catch(() => {
+        this.waitExerciseCount = 0;
+      });
     },
   }
 };
 </script>
 <style lang="scss" scoped>
+.js-practice-answer-step {
+  width: auto;
+  min-width: 220px;
+  padding: 0 16px;
+  box-sizing: border-box;
 
+  .step_item_label {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    line-height: 1;
+  }
+
+  .step_text {
+    font-weight: 550;
+    font-size: 16px;
+    white-space: nowrap;
+  }
+
+  .step_count_badge {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    padding: 4px 6px;
+    background: #F56C6C;
+    color: #fff;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 500;
+    line-height: 1;
+    text-align: center;
+    box-sizing: border-box;
+    flex-shrink: 0;
+  }
+}
 </style>

+ 107 - 45
src/views/targetImprove/practice/knowledgeGraph.vue

@@ -22,7 +22,9 @@
       </div>
 
       <!-- 环形扩散图组件 -->
-      <circular-diffusion :subjectName="actualSubjectName" :subjectScoreRate="subjectScoreRate"
+      <circular-diffusion
+        ref="circularDiffusion"
+        :subjectName="actualSubjectName" :subjectScoreRate="subjectScoreRate"
         :tableData="displayTableData"
         :classLevel="classLevel" :classGroupName="classGroupName"
         :currentKnowledgeId="currentLinkedKnowledgeId || currentKnowledgeId || defaultKnowledgeId"
@@ -115,7 +117,7 @@ export default {
       knowList: [],
       // knowList 扁平化后的一维数组(用于统计、列表展示)
       flatKnowList: [],
-      // 知识点分区:由 knowledge_partition 接口返回
+      // 知识点分区:由 knowledgeGraph 接口返回
       // conquerZone: 薄弱补强区 / improveZone: 能力提升区 / consolidateZone: 扎实巩固区 / sprintZone: 冲刺进阶区
       partitionData: {
         conquerZone: [],
@@ -209,7 +211,6 @@ export default {
     subjectCode(newVal, oldVal) {
       if (newVal && newVal !== oldVal) {
         this.fetchKnowledgeGraph();
-        this.fetchKnowledgePartition();
       }
     },
     // 监听 vuex store 中 gradesList 变化(TargetSubject 异步接口返回后会更新 store)
@@ -226,9 +227,8 @@ export default {
     this.calculateContainerHeight();
     // 监听窗口大小变化
     window.addEventListener('resize', this.calculateContainerHeight);
-    // 首次加载知识图谱数据 + 分区数据
+    // 首次加载知识图谱数据(含分区四区)
     this.fetchKnowledgeGraph();
-    this.fetchKnowledgePartition();
   },
   // 当从其他路由切回来时(keep-alive),重新加载年级列表
   activated() {
@@ -320,48 +320,22 @@ export default {
           // 4. 默认选中:第一棵节点树的最后一个子节点(叶子节点)
           const defaultNode = this.findFirstTreeLastLeafNode(this.knowList);
           this.defaultKnowledgeId = defaultNode ? (defaultNode.knowledgeId ?? '') : '';
-        } else {
-          // 接口失败 / code !== 200 / 无data:清空数据,扩散图和统计数字归零
-          this.resetKnowledgeGraphEmpty();
-        }
-      }).catch(err => {
-        console.error('[KnowledgeGraph] knowledgeGraph 接口异常:', err);
-        this.resetKnowledgeGraphEmpty();
-      });
-    },
-
-    /**
-     * 参数和 knowledgeGraph 相同:levelCode / gradeCodes / subjectCode
-     * 返回:conquerZone(薄弱) / improveZone(提升) / consolidateZone(巩固)
-     */
-    fetchKnowledgePartition() {
-      const levelCode = localStorage.getItem('student_level_code') || '';
-      let gradeCodes = this.currentGradeList && this.currentGradeList.length
-        ? this.currentGradeList
-        : (this.gradeList && this.gradeList.length > 0
-            ? [this.gradeList[this.gradeList.length - 1].value]
-            : []);
-      const subjectCode = this.subjectCode || '';
-      const classType = 1;
-      const params = { levelCode, gradeCodes, subjectCode, classType };
 
-      this.$api.target.knowledgePartition(params).then(res => {
-        if (res.code == 200 && res.data) {
-          const d = res.data || {};
+          // 5. 四区数据(结构与原 knowledgePartition 一致)
           this.partitionData = {
-            conquerZone:    this.mapPartitionItem(Array.isArray(d.conquerZone)    ? d.conquerZone    : []),
-            improveZone:    this.mapPartitionItem(Array.isArray(d.improveZone)    ? d.improveZone    : []),
-            consolidateZone:this.mapPartitionItem(Array.isArray(d.consolidateZone)? d.consolidateZone : []),
-            sprintZone:    this.mapPartitionItem(Array.isArray(d.sprint)         ? d.sprint          : [])
+            conquerZone:     this.mapPartitionItem(Array.isArray(d.conquerZone)     ? d.conquerZone     : []),
+            improveZone:     this.mapPartitionItem(Array.isArray(d.improveZone)     ? d.improveZone     : []),
+            consolidateZone: this.mapPartitionItem(Array.isArray(d.consolidateZone) ? d.consolidateZone : []),
+            sprintZone:      this.mapPartitionItem(Array.isArray(d.sprintZone)      ? d.sprintZone      : (Array.isArray(d.sprint) ? d.sprint : []))
           };
-          // 默认选中:薄弱补强区第一条 → 能力提升区第一条 → 扎实巩固区第一条
           this.setDefaultSelectionFromPartition();
         } else {
-          this.resetPartitionEmpty();
+          // 接口失败 / code !== 200 / 无data:清空数据,扩散图和统计数字归零
+          this.resetKnowledgeGraphEmpty();
         }
       }).catch(err => {
-        console.error('[KnowledgeGraph] knowledgePartition 接口异常:', err);
-        this.resetPartitionEmpty();
+        console.error('[KnowledgeGraph] knowledgeGraph 接口异常:', err);
+        this.resetKnowledgeGraphEmpty();
       });
     },
 
@@ -426,6 +400,7 @@ export default {
         goodKnowNum: 0
       };
       this.defaultKnowledgeId = '';
+      this.resetPartitionEmpty();
     },
 
     /**
@@ -531,9 +506,8 @@ export default {
     handleGradeChange(gradeList) {
       this.currentGradeList = gradeList;
       this.$emit('grade-change', gradeList);
-      // 年级变化后重新请求知识图谱 + 分区数据
+      // 年级变化后重新请求知识图谱(含分区四区)
       this.fetchKnowledgeGraph();
-      this.fetchKnowledgePartition();
     },
 
     /**
@@ -604,7 +578,7 @@ export default {
     // 参数来源:
     //   - 环形扩散图按钮:emit 传出 { knowledgeId, knowledgeName, personalScoreRate?, knowledgeType? }
     //   - region 卡片按钮:emit 传出整条 item(含 knowledgeId/Name/personalScoreRate/knowledgeType/examNum...)
-    handleAddPractice(item) {
+    handleAddPractice(item, sourceEl) {
       if (!item) {
         this.$message.warning('未获取到知识点信息');
         return;
@@ -675,13 +649,20 @@ export default {
         schoolYearId
       };
 
-      this.$api.target.addExercise(params).then(res => {
+      this.$api.target.addExercise(params).then(async (res) => {
         if (res && (res.code == 200 || res.code == 0)) {
           this.$message.success('已加入练习成功');
           // 1. 记录已加入练习 ID → circularDiffusion 的加入练习按钮置灰
           this.$set(this.addedKnowledgeIds, String(knowledgeId), true);
-          // 2. 从 region 分区列表中移除该条数据
+          // 2. 先抓取当前按钮位置并开启动画(分区列表移除后 DOM 会消失)
+          const startEl = (sourceEl && sourceEl.nodeType === 1)
+            ? sourceEl
+            : this.getGrabSourceEl();
+          const animPromise = this.createGrabAnimation(startEl);
+          // 3. 从 region 分区列表中移除该条数据
           this.removeKnowledgeFromPartition(knowledgeId);
+          await animPromise;
+          this.$emit('add-practice-success');
         } else {
           this.$message.error(res.msg);
         }
@@ -691,6 +672,87 @@ export default {
       });
     },
 
+    /**
+     * 获取环形图右侧「加入练习」模块 DOM,作为抓取动画起点
+     */
+    getGrabSourceEl() {
+      const chartRef = this.$refs.circularDiffusion;
+      return (chartRef && chartRef.$refs && chartRef.$refs.grabSourceEl) || null;
+    },
+    /**
+     * 抓取飞入动画:从加入练习模块飞向顶部「知识点练习作答」
+     * 起点/终点均用 getBoundingClientRect(相对视口),自动适配页面滚动
+     */
+    createGrabAnimation(startEl) {
+      try {
+        const endEl = document.querySelector('.js-practice-answer-step');
+        if (!startEl || !endEl) return Promise.resolve();
+
+        const startRect = startEl.getBoundingClientRect();
+        const endRect = endEl.getBoundingClientRect();
+        if (!startRect.width || !startRect.height) return Promise.resolve();
+
+        const HOLD_MS = 60;
+        const FLY_MS = 600;
+        const FADE_MS = 100;
+
+        const clone = startEl.cloneNode(true);
+        clone.style.position = 'fixed';
+        clone.style.left = `${startRect.left}px`;
+        clone.style.top = `${startRect.top}px`;
+        clone.style.width = `${startRect.width}px`;
+        clone.style.height = `${startRect.height}px`;
+        clone.style.margin = '0';
+        clone.style.zIndex = '2000';
+        clone.style.transition = `transform ${FLY_MS}ms cubic-bezier(.22,.61,.36,1)`;
+        clone.style.boxSizing = 'border-box';
+        clone.style.backgroundColor = '#ffffff';
+        clone.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
+        clone.style.overflow = 'hidden';
+        clone.style.pointerEvents = 'none';
+        clone.style.transformOrigin = 'center center';
+        clone.style.opacity = '1';
+        clone.removeAttribute('id');
+        document.body.appendChild(clone);
+
+        const dx = endRect.left + endRect.width / 2 - (startRect.left + startRect.width / 2);
+        const dy = endRect.top + endRect.height / 2 - (startRect.top + startRect.height / 2);
+
+        clone.getBoundingClientRect();
+
+        return new Promise(resolve => {
+          let done = false;
+          const finish = () => {
+            if (done) return;
+            done = true;
+            clone.removeEventListener('transitionend', onEnd);
+            if (document.body.contains(clone)) {
+              try { document.body.removeChild(clone); } catch (e) {}
+            }
+            resolve();
+          };
+          const onEnd = (e) => {
+            if (e && e.target !== clone) return;
+            if (e && e.propertyName && e.propertyName !== 'transform') return;
+            // 飞到目标后再淡出移除,避免中途消失
+            clone.style.transition = `opacity ${FADE_MS}ms ease`;
+            clone.style.opacity = '0';
+            setTimeout(finish, FADE_MS);
+          };
+          clone.addEventListener('transitionend', onEnd);
+
+          setTimeout(() => {
+            clone.style.transform = `translate(${dx}px, ${dy}px)`;
+          }, HOLD_MS);
+
+          setTimeout(finish, HOLD_MS + FLY_MS + FADE_MS + 300);
+        });
+      } catch (e) {
+        console.error('创建抓取动画失败:', e);
+        return Promise.resolve();
+      }
+    },
+
     /**
      * 从分区列表中移除已加入练习的知识点
      * 遍历四个分区数组,按 knowledgeId 删除匹配项

+ 70 - 18
src/views/targetImprove/practice/knowledgeGraph/circularDiffusion.vue

@@ -40,7 +40,8 @@
               </el-button>
             </div>
           </div>
-          <div ref="chart" class="chart" style="width: 100%;"></div>
+          <div v-if="hasGraphData" ref="chart" class="chart" style="width: 100%;"></div>
+          <div v-else class="empty_tip no_content_data"><span>暂无数据</span></div>
         </div>
 
         <!-- 右侧容器 -->
@@ -53,13 +54,15 @@
                 <span class="title_text" :title="selectedKnowledgeName">{{ selectedKnowledgeName }}</span>
                 <span class="exam_freq">考频:{{ filteredTableData.length }}</span>
               </div>
-              <el-button
-                class="add_practice_btn"
-                :class="addPracticeBtnThemeClass"
-                :disabled="selectedIsJoin"
-                @click="handleAddPractice">
-                {{ selectedIsJoin ? '已加入练习' : '加入练习' }}
-              </el-button>
+              <div ref="grabSourceEl" class="add_practice_grab_source">
+                <el-button
+                  class="add_practice_btn"
+                  :class="addPracticeBtnThemeClass"
+                  :disabled="isAddPracticeBtnDisabled()"
+                  @click="handleAddPractice">
+                  {{ selectedIsJoin ? '已加入练习' : '加入练习' }}
+                </el-button>
+              </div>
             </div>
             <div ref="lineChart" class="line_chart"></div>
           </div>
@@ -127,7 +130,7 @@
 
       <!-- 列表查看容器 -->
       <div v-if="activeView === 'list'" class="list_content">
-        <vxe-table ref="treeTable" :data="filteredTableDataForList" style="width: 100%;" maxHeight="480px" border show-header
+        <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">
@@ -161,6 +164,7 @@
             </template>
           </vxe-table-column>
         </vxe-table>
+        <div v-else class="empty_tip no_content_data"><span>暂无数据</span></div>
       </div>
 
     </div>
@@ -288,6 +292,10 @@ export default {
       const item = this.currentKnowledgeItem;
       return item ? (item.knowledgeName ?? '') : '';
     },
+    // 图谱接口是否有树形数据
+    hasGraphData() {
+      return Array.isArray(this.tableData) && this.tableData.length > 0;
+    },
     // 当前选中知识点是否"已加入练习"
     // 优先级:1) 父级 addedKnowledgeIds(当前会话刚点按钮成功,立即生效)→ 2) 节点自身 isJoin===1(接口返回的静态字段)
     selectedIsJoin() {
@@ -476,16 +484,27 @@ export default {
     tableData: {
       deep: true,
       handler(newVal) {
-        if (this.chart) {
-          this.$nextTick(() => {
-            this.updateChart();
-            // 数据刷新后,如果有选中项则重新应用放大效果
-            if (this.selectedKnowledgeId) {
-              this.applySelectedSizeToChart(this.selectedKnowledgeId);
-            }
-            if (this.lineChart) this.updateLineChart();
-          });
+        const hasData = Array.isArray(newVal) && newVal.length > 0;
+        if (!hasData) {
+          if (this.chart) {
+            this.chart.dispose();
+            this.chart = null;
+          }
+          return;
         }
+        this.$nextTick(() => {
+          if (this.activeView === 'graph') {
+            if (this.chart) {
+              this.updateChart();
+              if (this.selectedKnowledgeId) {
+                this.applySelectedSizeToChart(this.selectedKnowledgeId);
+              }
+            } else {
+              this.initChart();
+            }
+          }
+          if (this.lineChart) this.updateLineChart();
+        });
       }
     },
     currentKnowledgeId: {
@@ -542,6 +561,15 @@ export default {
     },
   },
   methods: {
+    /**
+     * 加入练习按钮是否置灰:已加入练习,或 questionNum 为 0/null
+     */
+    isAddPracticeBtnDisabled() {
+      if (this.selectedIsJoin) return true;
+      const item = this.currentKnowledgeItem;
+      const num = item && item.questionNum;
+      return num === null || num === undefined || num === '' || Number(num) === 0;
+    },
     // 获取当前颜色(根据选中知识点类型)
     getCurrentColor() {
       const item = this.currentKnowledgeItem;
@@ -690,6 +718,7 @@ export default {
         this.$message && this.$message.warning && this.$message.warning('请先选择一个知识点');
         return;
       }
+      if (this.isAddPracticeBtnDisabled()) return;
 
       // 得分率:personalScoreRate → gradeScoreRate → classScoreRate
       let personalScoreRate = item?.personalScoreRate;
@@ -1545,6 +1574,10 @@ export default {
               line-height: 1.2;
             }
 
+            .add_practice_grab_source {
+              flex-shrink: 0;
+            }
+
             /* 加入练习按钮:使用 Element UI el-button,描边风格跟随当前选中圆点的 knowledgeType 颜色 */
             .add_practice_btn {
               border-radius: 4px;
@@ -1819,6 +1852,15 @@ export default {
           min-height: 590px;
         }
 
+        .empty_tip {
+          min-height: 590px;
+          width: 100%;
+          border-radius: 8px;
+          span {
+            margin-top: 23%;
+          }
+        }
+
       }
 
       .knowledge_list {
@@ -1900,6 +1942,16 @@ export default {
     // vxe表格样式
     .list_content {
       border-radius: 10px 10px 10px 10px;
+      position: relative;
+
+      .empty_tip {
+        min-height: 480px;
+        width: 100%;
+        border-radius: 8px;
+        span {
+          margin-top: 23%;
+        }
+      }
 
       .vxe-table {
         // 展开收起的样式

+ 35 - 7
src/views/targetImprove/practice/knowledgeGraph/region.vue

@@ -30,7 +30,8 @@
           </div>
           <div class="row_right">
             <el-button size="small" class="add_btn weak_btn"
-              @click="handleAddPractice(item)">
+              :disabled="isAddPracticeDisabled(item)"
+              @click.stop="handleAddPractice(item, $event)">
               加入练习
             </el-button>
           </div>
@@ -68,7 +69,8 @@
           </div>
           <div class="row_right">
             <el-button size="small" class="add_btn good_btn"
-              @click="handleAddPractice(item)">
+              :disabled="isAddPracticeDisabled(item)"
+              @click.stop="handleAddPractice(item, $event)">
               加入练习
             </el-button>
           </div>
@@ -106,7 +108,8 @@
           </div>
           <div class="row_right">
             <el-button size="small" class="add_btn excellent_btn"
-              @click="handleAddPractice(item)">
+              :disabled="isAddPracticeDisabled(item)"
+              @click.stop="handleAddPractice(item, $event)">
               加入练习
             </el-button>
           </div>
@@ -144,7 +147,8 @@
           </div>
           <div class="row_right">
             <el-button size="small" class="add_btn advanced_btn"
-              @click="handleAddPractice(item)">
+              :disabled="isAddPracticeDisabled(item)"
+              @click.stop="handleAddPractice(item, $event)">
               加入练习
             </el-button>
           </div>
@@ -383,9 +387,18 @@ export default {
       if (!item || !item.knowledgeId) return;
       this.$emit('item-click', item);
     },
-    // 处理加入练习
-    handleAddPractice(item) {
-      this.$emit('add-practice', item);
+    /**
+     * questionNum 为 0 或 null 时,加入练习按钮置灰不可点
+     */
+    isAddPracticeDisabled(item) {
+      const num = item && item.questionNum;
+      return num === null || num === undefined || num === '' || Number(num) === 0;
+    },
+    // 处理加入练习(第二个参数为点击源 DOM,供飞入动画使用)
+    handleAddPractice(item, e) {
+      if (this.isAddPracticeDisabled(item)) return;
+      const sourceEl = (e && e.currentTarget) || null;
+      this.$emit('add-practice', item, sourceEl);
     }
   }
 };
@@ -677,6 +690,21 @@ export default {
   background: #FFFFFF;
 }
 
+/* 无题目(questionNum 为 0/null)置灰:与环形图「已加入练习」disabled 样式一致 */
+.knowledge_row .add_btn.is-disabled,
+.knowledge_row .add_btn[disabled] {
+  border: 1px solid #DCDFE6 !important;
+  color: #C0C4CC !important;
+  background: #F5F7FA !important;
+  cursor: not-allowed !important;
+  &:hover,
+  &:focus {
+    border: 1px solid #DCDFE6 !important;
+    color: #C0C4CC !important;
+    background: #F5F7FA !important;
+  }
+}
+
 /* 响应式:小屏适配 */
 @media (max-width: 1366px) {
   .region_container {