|
|
@@ -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>
|