|
@@ -22,7 +22,9 @@
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- 环形扩散图组件 -->
|
|
<!-- 环形扩散图组件 -->
|
|
|
- <circular-diffusion :subjectName="actualSubjectName" :subjectScoreRate="subjectScoreRate"
|
|
|
|
|
|
|
+ <circular-diffusion
|
|
|
|
|
+ ref="circularDiffusion"
|
|
|
|
|
+ :subjectName="actualSubjectName" :subjectScoreRate="subjectScoreRate"
|
|
|
:tableData="displayTableData"
|
|
:tableData="displayTableData"
|
|
|
:classLevel="classLevel" :classGroupName="classGroupName"
|
|
:classLevel="classLevel" :classGroupName="classGroupName"
|
|
|
:currentKnowledgeId="currentLinkedKnowledgeId || currentKnowledgeId || defaultKnowledgeId"
|
|
:currentKnowledgeId="currentLinkedKnowledgeId || currentKnowledgeId || defaultKnowledgeId"
|
|
@@ -115,7 +117,7 @@ export default {
|
|
|
knowList: [],
|
|
knowList: [],
|
|
|
// knowList 扁平化后的一维数组(用于统计、列表展示)
|
|
// knowList 扁平化后的一维数组(用于统计、列表展示)
|
|
|
flatKnowList: [],
|
|
flatKnowList: [],
|
|
|
- // 知识点分区:由 knowledge_partition 接口返回
|
|
|
|
|
|
|
+ // 知识点分区:由 knowledgeGraph 接口返回
|
|
|
// conquerZone: 薄弱补强区 / improveZone: 能力提升区 / consolidateZone: 扎实巩固区 / sprintZone: 冲刺进阶区
|
|
// conquerZone: 薄弱补强区 / improveZone: 能力提升区 / consolidateZone: 扎实巩固区 / sprintZone: 冲刺进阶区
|
|
|
partitionData: {
|
|
partitionData: {
|
|
|
conquerZone: [],
|
|
conquerZone: [],
|
|
@@ -209,7 +211,6 @@ export default {
|
|
|
subjectCode(newVal, oldVal) {
|
|
subjectCode(newVal, oldVal) {
|
|
|
if (newVal && newVal !== oldVal) {
|
|
if (newVal && newVal !== oldVal) {
|
|
|
this.fetchKnowledgeGraph();
|
|
this.fetchKnowledgeGraph();
|
|
|
- this.fetchKnowledgePartition();
|
|
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
// 监听 vuex store 中 gradesList 变化(TargetSubject 异步接口返回后会更新 store)
|
|
// 监听 vuex store 中 gradesList 变化(TargetSubject 异步接口返回后会更新 store)
|
|
@@ -226,9 +227,8 @@ export default {
|
|
|
this.calculateContainerHeight();
|
|
this.calculateContainerHeight();
|
|
|
// 监听窗口大小变化
|
|
// 监听窗口大小变化
|
|
|
window.addEventListener('resize', this.calculateContainerHeight);
|
|
window.addEventListener('resize', this.calculateContainerHeight);
|
|
|
- // 首次加载知识图谱数据 + 分区数据
|
|
|
|
|
|
|
+ // 首次加载知识图谱数据(含分区四区)
|
|
|
this.fetchKnowledgeGraph();
|
|
this.fetchKnowledgeGraph();
|
|
|
- this.fetchKnowledgePartition();
|
|
|
|
|
},
|
|
},
|
|
|
// 当从其他路由切回来时(keep-alive),重新加载年级列表
|
|
// 当从其他路由切回来时(keep-alive),重新加载年级列表
|
|
|
activated() {
|
|
activated() {
|
|
@@ -320,48 +320,22 @@ export default {
|
|
|
// 4. 默认选中:第一棵节点树的最后一个子节点(叶子节点)
|
|
// 4. 默认选中:第一棵节点树的最后一个子节点(叶子节点)
|
|
|
const defaultNode = this.findFirstTreeLastLeafNode(this.knowList);
|
|
const defaultNode = this.findFirstTreeLastLeafNode(this.knowList);
|
|
|
this.defaultKnowledgeId = defaultNode ? (defaultNode.knowledgeId ?? '') : '';
|
|
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 = {
|
|
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();
|
|
this.setDefaultSelectionFromPartition();
|
|
|
} else {
|
|
} else {
|
|
|
- this.resetPartitionEmpty();
|
|
|
|
|
|
|
+ // 接口失败 / code !== 200 / 无data:清空数据,扩散图和统计数字归零
|
|
|
|
|
+ this.resetKnowledgeGraphEmpty();
|
|
|
}
|
|
}
|
|
|
}).catch(err => {
|
|
}).catch(err => {
|
|
|
- console.error('[KnowledgeGraph] knowledgePartition 接口异常:', err);
|
|
|
|
|
- this.resetPartitionEmpty();
|
|
|
|
|
|
|
+ console.error('[KnowledgeGraph] knowledgeGraph 接口异常:', err);
|
|
|
|
|
+ this.resetKnowledgeGraphEmpty();
|
|
|
});
|
|
});
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -426,6 +400,7 @@ export default {
|
|
|
goodKnowNum: 0
|
|
goodKnowNum: 0
|
|
|
};
|
|
};
|
|
|
this.defaultKnowledgeId = '';
|
|
this.defaultKnowledgeId = '';
|
|
|
|
|
+ this.resetPartitionEmpty();
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -531,9 +506,8 @@ export default {
|
|
|
handleGradeChange(gradeList) {
|
|
handleGradeChange(gradeList) {
|
|
|
this.currentGradeList = gradeList;
|
|
this.currentGradeList = gradeList;
|
|
|
this.$emit('grade-change', gradeList);
|
|
this.$emit('grade-change', gradeList);
|
|
|
- // 年级变化后重新请求知识图谱 + 分区数据
|
|
|
|
|
|
|
+ // 年级变化后重新请求知识图谱(含分区四区)
|
|
|
this.fetchKnowledgeGraph();
|
|
this.fetchKnowledgeGraph();
|
|
|
- this.fetchKnowledgePartition();
|
|
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -604,7 +578,7 @@ export default {
|
|
|
// 参数来源:
|
|
// 参数来源:
|
|
|
// - 环形扩散图按钮:emit 传出 { knowledgeId, knowledgeName, personalScoreRate?, knowledgeType? }
|
|
// - 环形扩散图按钮:emit 传出 { knowledgeId, knowledgeName, personalScoreRate?, knowledgeType? }
|
|
|
// - region 卡片按钮:emit 传出整条 item(含 knowledgeId/Name/personalScoreRate/knowledgeType/examNum...)
|
|
// - region 卡片按钮:emit 传出整条 item(含 knowledgeId/Name/personalScoreRate/knowledgeType/examNum...)
|
|
|
- handleAddPractice(item) {
|
|
|
|
|
|
|
+ handleAddPractice(item, sourceEl) {
|
|
|
if (!item) {
|
|
if (!item) {
|
|
|
this.$message.warning('未获取到知识点信息');
|
|
this.$message.warning('未获取到知识点信息');
|
|
|
return;
|
|
return;
|
|
@@ -675,13 +649,20 @@ export default {
|
|
|
schoolYearId
|
|
schoolYearId
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- this.$api.target.addExercise(params).then(res => {
|
|
|
|
|
|
|
+ this.$api.target.addExercise(params).then(async (res) => {
|
|
|
if (res && (res.code == 200 || res.code == 0)) {
|
|
if (res && (res.code == 200 || res.code == 0)) {
|
|
|
this.$message.success('已加入练习成功');
|
|
this.$message.success('已加入练习成功');
|
|
|
// 1. 记录已加入练习 ID → circularDiffusion 的加入练习按钮置灰
|
|
// 1. 记录已加入练习 ID → circularDiffusion 的加入练习按钮置灰
|
|
|
this.$set(this.addedKnowledgeIds, String(knowledgeId), true);
|
|
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);
|
|
this.removeKnowledgeFromPartition(knowledgeId);
|
|
|
|
|
+ await animPromise;
|
|
|
|
|
+ this.$emit('add-practice-success');
|
|
|
} else {
|
|
} else {
|
|
|
this.$message.error(res.msg);
|
|
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 删除匹配项
|
|
* 遍历四个分区数组,按 knowledgeId 删除匹配项
|