zeroScoreKnowledge.vue 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906
  1. <template>
  2. <div class="knowledge_graph">
  3. <div class="graph_header" v-if="activeView === 'list'">
  4. <!-- 自定义图例 -->
  5. <div class="legend">
  6. <div class="legend_item" :class="{ 'selected': selectedLegend.weak }" @click="toggleLegend('weak')">
  7. <span class="legend_dot weak" :style="selectedLegend.weak ? {} : { backgroundColor: '#ccc' }"></span>
  8. <span class="legend_text" :style="selectedLegend.weak ? {} : { color: '#999999' }">{{ '薄弱(0%≤得分率<60%)'
  9. }}</span>
  10. </div>
  11. <div class="legend_item" :class="{ 'selected': selectedLegend.good }" @click="toggleLegend('good')">
  12. <span class="legend_dot good" :style="selectedLegend.good ? {} : { backgroundColor: '#ccc' }"></span>
  13. <span class="legend_text" :style="selectedLegend.good ? {} : { color: '#999999' }">{{ '良好(60%≤得分率<85%)'
  14. }}</span>
  15. </div>
  16. <div class="legend_item" :class="{ 'selected': selectedLegend.excellent }" @click="toggleLegend('excellent')">
  17. <span class="legend_dot excellent"
  18. :style="selectedLegend.excellent ? {} : { backgroundColor: '#ccc' }"></span>
  19. <span class="legend_text" :style="selectedLegend.excellent ? {} : { color: '#999999' }">{{ '优秀(85%≤得分率)'
  20. }}</span>
  21. </div>
  22. </div>
  23. <!-- 视图切换按钮 - 移到knowledge_graph容器下 -->
  24. <div class="view_switcher_container">
  25. <el-button type="text" @click="switchView('graph')" class="switch_btn list_btn">
  26. <i class="icon_switch_graph">
  27. <img src="../../../assets/studentAnalysis/circleGrey.svg" alt="按图形查看">
  28. </i>
  29. 按图形查看
  30. </el-button>
  31. <el-button type="graph" class="switch_btn graph_btn" @click="switchView('list')">
  32. <i class="icon_switch_list">
  33. <img src="../../../assets/studentAnalysis/iconBlue.svg" alt="按列表查看">
  34. </i>
  35. 按列表查看
  36. </el-button>
  37. </div>
  38. </div>
  39. <!-- 图形查看容器 -->
  40. <div v-if="activeView === 'graph'" class="graph_content">
  41. <!-- 左侧图表 -->
  42. <div class="chart_container">
  43. <!-- 顶部容器:图例和视图切换按钮 -->
  44. <div class="top_container">
  45. <!-- 自定义图例 -->
  46. <div class="legend">
  47. <div class="legend_item" :class="{ 'selected': selectedLegend.weak }" @click="toggleLegend('weak')">
  48. <span class="legend_dot weak" :style="selectedLegend.weak ? {} : { backgroundColor: '#ccc' }"></span>
  49. <span class="legend_text" :style="selectedLegend.weak ? {} : { color: '#999999' }">{{ '薄弱(0%≤得分率<60%)'
  50. }}</span>
  51. </div>
  52. <div class="legend_item" :class="{ 'selected': selectedLegend.good }" @click="toggleLegend('good')">
  53. <span class="legend_dot good" :style="selectedLegend.good ? {} : { backgroundColor: '#ccc' }"></span>
  54. <span class="legend_text" :style="selectedLegend.good ? {} : { color: '#999999' }">{{ '良好(60%≤得分率<85%)'
  55. }}</span>
  56. </div>
  57. <div class="legend_item" :class="{ 'selected': selectedLegend.excellent }"
  58. @click="toggleLegend('excellent')">
  59. <span class="legend_dot excellent"
  60. :style="selectedLegend.excellent ? {} : { backgroundColor: '#ccc' }"></span>
  61. <span class="legend_text" :style="selectedLegend.excellent ? {} : { color: '#999999' }">{{ '优秀(85%≤得分率)'
  62. }}</span>
  63. </div>
  64. </div>
  65. <!-- 视图切换按钮 -->
  66. <div class="view_switcher_container" v-if="activeView === 'graph'" :class="activeView">
  67. <el-button type="graph" @click="switchView('graph')" class="switch_btn graph_btn">
  68. <i class="icon_switch_graph">
  69. <img src="../../../assets/studentAnalysis/circleBlue.svg" alt="按图形查看">
  70. </i>
  71. 按图形查看
  72. </el-button>
  73. <el-button type="text" class="switch_btn list_btn" @click="switchView('list')">
  74. <i class="icon_switch_list"><img src="../../../assets/studentAnalysis/iconGrey.svg" alt="按列表查看"></i>
  75. 按列表查看
  76. </el-button>
  77. </div>
  78. </div>
  79. <div ref="chart" class="chart" style="width: 100%;"></div>
  80. </div>
  81. <!-- 右侧容器,包含tab和知识点列表 -->
  82. <div class="knowledge_right_container">
  83. <!-- Tab切换 -->
  84. <div class="knowledge_tab">
  85. <div class="tab_item" :class="{ active: activeTab === 'all' }" @click="activeTab = 'all'">
  86. 全部知识点
  87. </div>
  88. <div class="tab_item" :class="{ active: activeTab === 'highFreq' }" @click="activeTab = 'highFreq'">
  89. 高频错题知识点
  90. </div>
  91. <div class="tab_item" :class="{ active: activeTab === 'zero' }" @click="activeTab = 'zero'">
  92. 零分知识点
  93. </div>
  94. </div>
  95. <!-- 右侧知识点列表 -->
  96. <div class="knowledge_list">
  97. <!-- 暂无数据提示 -->
  98. <div class="module_chart no_content_data no_data" style="height: 240px; " element-loading-background="#ffffff"
  99. v-if="!knowledgeItems || knowledgeItems.length === 0">
  100. <span>暂无数据</span>
  101. </div>
  102. <!-- 正常列表数据 -->
  103. <div class="list_item" v-for="(item, index) in knowledgeItems" :key="index"
  104. @click="handleItemClick(item, index)" :class="{ active: selectedIndex === index }" v-else>
  105. <div class="item_header">
  106. <span class="item_dot" :style="{ backgroundColor: getDotColor(item, 'personalScoreRate') }"></span>
  107. <el-tooltip :content="item.knowledgeName" placement="top" effect="light"
  108. :disabled="!item.knowledgeName || item.knowledgeName.length < 15">
  109. <span class="item_title">{{ item.knowledgeName }}</span>
  110. </el-tooltip>
  111. <!-- <span class="item_tag" v-if="item.scoreRateDiff"
  112. :style="{ backgroundColor: parseFloat(item.scoreRateDiff) > 0 ? '#3BA272' : '#F56C6C' }">
  113. <i>{{ item.scoreRateDiff }}% </i>
  114. <i v-if="parseFloat(item.scoreRateDiff) !== 0" style="margin-top: 0px">{{ parseFloat(item.scoreRateDiff) > 0 ? '↑' : '↓' }}</i>
  115. </span> -->
  116. <span class="item_tag" v-if="item.isPush == 1" style="background-color: #2E64FA;">
  117. 有推题
  118. </span>
  119. </div>
  120. <div class="item_info">
  121. <!-- 班级/学生得分率 -->
  122. <span class="item_score">
  123. <span class="score_label" v-if="item.personalScoreRate">个人得分率:</span>
  124. <span :style="{ color: getDotColor(item, 'personalScoreRate') }" v-if="item.personalScoreRate">{{
  125. item.personalScoreRate }}%</span>
  126. <!-- <span class="score_separator" v-if="item.classScoreRate">|</span>
  127. <span class="score_label">班级得分率:</span>
  128. <span v-if="item.classScoreRate" class="score_second">{{ item.classScoreRate }}%</span>
  129. <span v-if="!item.classScoreRate" :style="{ color: getDotColor(item, 'classScoreRate') }">{{
  130. item.classScoreRate }}%</span> -->
  131. </span>
  132. <!-- 班级/学生得分率 -->
  133. <span class="item_exam_count">考试数: <span class="exam_count">{{ item.examNum || 0 }}</span></span>
  134. </div>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. <!-- 列表查看容器 -->
  140. <div v-if="activeView === 'list'" class="list_content">
  141. <vxe-table ref="treeTable" :data="tableData" style="width: 100%;" maxHeight="480px" border show-header
  142. :tree-config="{ childrenField: 'children', hasChildField: 'hasChildren', showIcon: true, iconOpen: 'el-icon-remove', iconClose: 'el-icon-circle-plus' }"
  143. :scroll-y="{ enabled: true, gt: 480 }" fixed-header :header-cell-style="{ color: '#333' }"
  144. @row-click="handleRowClick" @cell-click="handleCellClick" :row-class-name="rowClassName">
  145. <vxe-table-column type="tree" tree-node prop="title" :title="subjectName">
  146. <template #default="{ row }">
  147. <div class="knowledge_item">
  148. <span class="knowledge_title">{{ row.knowledgeName }}</span>
  149. </div>
  150. </template>
  151. </vxe-table-column>
  152. <!-- 个人得分率 -->
  153. <vxe-table-column prop="personalScoreRate" title="个人得分率" width="180" align="center">
  154. <template #default="{ row }">
  155. <div class="rate_info" v-if="row.personalScoreRate !== null">
  156. <span class="rate_dot" :class="getRateClass(row.personalScoreRate)"></span>
  157. <span class="rate_value">{{ row.personalScoreRate }}%</span>
  158. </div>
  159. </template>
  160. </vxe-table-column>
  161. <!-- 个人知识点状态 -->
  162. <vxe-table-column prop="personalScoreRate" title="个人知识点状态" width="120" align="center">
  163. <template #default="{ row }">
  164. <div class="status_info" v-if="row.personalScoreRate !== null">
  165. <span class="status_value"
  166. :style="{ color: getRateName(row.personalScoreRate) === '优秀' ? '#3BA272' : getRateName(row.personalScoreRate) === '良好' ? '#FAC858' : '#EE6666' }">
  167. {{ getRateName(row.personalScoreRate) }}
  168. </span>
  169. </div>
  170. </template>
  171. </vxe-table-column>
  172. <!-- 班级得分率 -->
  173. <!-- <vxe-table-column prop="classScoreRate" title="班级得分率" width="180" align="center">
  174. <template #default="{ row }">
  175. <div class="rate_info" v-if="row.classScoreRate !== null">
  176. <span class="rate_dot" :class="getRateClass(row.classScoreRate)"></span>
  177. <span class="rate_value">{{ row.classScoreRate }}%</span>
  178. </div>
  179. </template>
  180. </vxe-table-column> -->
  181. <!-- 班级知识点状态 -->
  182. <!-- <vxe-table-column prop="classScoreRate" title="班级知识点状态" width="120" align="center">
  183. <template #default="{ row }">
  184. <div class="status_info" v-if="row.classScoreRate !== null">
  185. <span class="status_value"
  186. :style="{ color: getRateName(row.classScoreRate) === '优秀' ? '#3BA272' : getRateName(row.classScoreRate) === '良好' ? '#FAC858' : '#EE6666' }">
  187. {{ getRateName(row.classScoreRate) }}
  188. </span>
  189. </div>
  190. </template>
  191. </vxe-table-column> -->
  192. <!-- 只有当班级名称不是年级且不为空时,才显示得分率差列 -->
  193. <!-- <vxe-table-column prop="diff" title="得分率差" width="150" align="center">
  194. <template #default="{ row }">
  195. <div class="rate_info" v-if="row.scoreRateDiff !== null">
  196. <span class="rate_dot" :class="getRateClass(row.scoreRateDiff)"></span>
  197. <span class="diff_value"
  198. :class="{ 'diff_negative': row.scoreRateDiff !== null && row.scoreRateDiff < 0 }">
  199. {{ row.scoreRateDiff !== null && row.scoreRateDiff > 0 ? '+' : '' }}{{ row.scoreRateDiff }}%
  200. </span>
  201. </div>
  202. </template>
  203. </vxe-table-column> -->
  204. </vxe-table>
  205. </div>
  206. </div>
  207. </template>
  208. <script>
  209. // 引入echarts
  210. import * as echarts from 'echarts';
  211. export default {
  212. name: 'KnowledgeGraph',
  213. props: {
  214. activeView: { // 当前视图,graph或list 年级画像还是学生画像
  215. type: String,
  216. default: 'graph',
  217. validator: (value) => {
  218. return ['graph', 'list'].includes(value);
  219. }
  220. },
  221. subjectName: { // 科目名称
  222. type: String,
  223. default: '知识点'
  224. },
  225. subjectScoreRate: { // 科目得分率
  226. type: Number,
  227. default: 0
  228. },
  229. tableData: { // 知识点表格数据
  230. type: Array,
  231. default: () => []
  232. },
  233. fatalVulnerability: { // 零分知识点数据
  234. type: Array,
  235. default: () => []
  236. },
  237. highVulnerability: { // 高频错题知识点数据
  238. type: Array,
  239. default: () => []
  240. },
  241. allKnowledgeList: { // 全部知识点数据
  242. type: Array,
  243. default: () => []
  244. },
  245. classLevel: { // 判断是否是组合还是班级,还是年级
  246. type: [String, Number],
  247. default: ''
  248. },
  249. classGroupName: { // 班级组合名称
  250. type: String,
  251. default: ''
  252. },
  253. currentKnowledgeId: { // 当前选中的知识点ID
  254. type: [String, Number],
  255. default: ''
  256. },
  257. },
  258. computed: {
  259. // 根据当前tab返回对应的数据
  260. knowledgeItems() {
  261. if (this.activeTab === 'zero') {
  262. return this.fatalVulnerability || [];
  263. } else if (this.activeTab === 'highFreq') {
  264. return this.highVulnerability || [];
  265. } else if (this.activeTab === 'all') {
  266. return this.allKnowledgeList || [];
  267. }
  268. return [];
  269. }
  270. },
  271. data() {
  272. return {
  273. chart: null, // echarts实例
  274. // 组件挂载状态标记
  275. _isMounted: false,
  276. // 首次渲染标志,用于控制初始化时的透明度逻辑
  277. _isFirstRender: true,
  278. // Tab切换状态
  279. activeTab: 'all',
  280. // 图例选中状态
  281. selectedLegend: {
  282. weak: true,
  283. good: true,
  284. excellent: true
  285. },
  286. // 选中的知识点索引
  287. selectedIndex: 0,
  288. // 当前选中的行数据
  289. selectedRow: null,
  290. // 当前选中的知识点ID,用于控制节点透明度
  291. selectedKnowledgeId: '',
  292. // 树形表格展开行keys
  293. expandRowKeys: []
  294. };
  295. },
  296. mounted() {
  297. // 设置组件挂载状态
  298. this._isMounted = true;
  299. // 无论在什么模式下,只要默认视图是图形视图就初始化echarts图表
  300. if (this.activeView === 'graph') {
  301. this.$nextTick(() => {
  302. this.initChart();
  303. });
  304. }
  305. },
  306. beforeDestroy() {
  307. // 设置组件未挂载状态
  308. this._isMounted = false;
  309. // 销毁echarts实例
  310. if (this.chart) {
  311. this.chart.dispose();
  312. }
  313. },
  314. watch: {
  315. // 监听classLevel变化,当对比选择器数据变化时更新对比选项
  316. classLevel: {
  317. handler(newVal, oldVal) {
  318. },
  319. immediate: true
  320. },
  321. // 监听classGroupName变化,当班级组合名称变化时更新对比选项
  322. classGroupName: {
  323. handler(newVal, oldVal) {
  324. if (newVal) {
  325. }
  326. },
  327. immediate: true
  328. },
  329. // 监听activeView变化,当切换到图形视图时重新初始化图表,切换到列表视图时销毁图表
  330. activeView(newView, oldView) {
  331. if (newView === 'graph') {
  332. this.$nextTick(() => {
  333. this.initChart();
  334. });
  335. } else if (newView === 'list') {
  336. // 切换到列表视图时销毁图表实例
  337. if (this.chart) {
  338. this.chart.dispose();
  339. this.chart = null;
  340. }
  341. // 切换到列表视图时,展开所有树形节点
  342. this.$nextTick(() => {
  343. this.expandAllNodes();
  344. });
  345. }
  346. },
  347. // 监听tab切换,更新选中索引并通知父组件
  348. activeTab(newTab, oldTab) {
  349. this.selectedIndex = 0;
  350. // 根据当前tab获取对应的数据列表
  351. const currentList = this.knowledgeItems;
  352. // 如果有数据,更新selectedKnowledgeId为第一条数据的ID
  353. if (currentList && currentList.length > 0) {
  354. this.selectedKnowledgeId = currentList[0].knowledgeId;
  355. // 如果当前是图形视图且图表已初始化,更新图表选中状态
  356. if (this.activeView === 'graph' && this.chart) {
  357. this.updateChart();
  358. }
  359. } else {
  360. // 如果没有数据,清空selectedKnowledgeId
  361. this.selectedKnowledgeId = '';
  362. }
  363. // 只有当组件已挂载且tab值实际发生变化,并且不是初始化时,才向父组件发送事件
  364. // 这是为了避免在视图切换时重复调用接口
  365. if (this._isMounted && newTab !== oldTab) {
  366. // 向父组件发送tab切换事件
  367. this.$emit('activeTabChange', newTab);
  368. }
  369. },
  370. // 监听数据变化,重新设置默认选中项
  371. fatalVulnerability: {
  372. deep: true,
  373. handler() {
  374. // 仅当当前视图是图形视图且数据发生变化时才更新默认选中
  375. if (this.activeView === 'graph') {
  376. this.setDefaultSelection();
  377. }
  378. }
  379. },
  380. highVulnerability: {
  381. deep: true,
  382. handler() {
  383. // 仅当当前视图是图形视图且数据发生变化时才更新默认选中
  384. if (this.activeView === 'graph') {
  385. this.setDefaultSelection();
  386. }
  387. }
  388. },
  389. allKnowledgeList: {
  390. deep: true,
  391. handler() {
  392. // 仅当当前视图是图形视图且数据发生变化时才更新默认选中
  393. if (this.activeView === 'graph') {
  394. this.setDefaultSelection();
  395. }
  396. }
  397. },
  398. // 监听tableData变化,更新展开行
  399. tableData: {
  400. deep: true,
  401. handler() {
  402. // 当tableData变化时,重新展开所有节点
  403. if (this.activeView === 'list') {
  404. this.$nextTick(() => {
  405. this.expandAllNodes();
  406. });
  407. }
  408. if (this.chart) {
  409. this.$nextTick(() => {
  410. this.updateChart();
  411. });
  412. }
  413. }
  414. },
  415. // 监听当前选中的知识点ID变化,更新图表高亮状态
  416. currentKnowledgeId: {
  417. handler(newVal) {
  418. // 更新selectedKnowledgeId,确保图表使用正确的高亮状态
  419. this.selectedKnowledgeId = newVal;
  420. // 仅当当前视图是图形视图且图表已初始化时才更新
  421. if (this.activeView === 'graph' && this.chart) {
  422. this.updateChart();
  423. }
  424. }
  425. }
  426. },
  427. methods: {
  428. // 设置默认选中项
  429. setDefaultSelection() {
  430. // 保存当前tab值,用于比较是否需要修改
  431. const oldActiveTab = this.activeTab;
  432. let selectedItem = null;
  433. // 优先级:全部知识点> 高频错题知识点 > 零分知识点
  434. if (this.allKnowledgeList && this.allKnowledgeList.length > 0) {
  435. // 如果前两者都没有数据但全部知识点有数据,切换到全部知识点并选中第一条
  436. if (this.activeTab !== 'all') {
  437. this.activeTab = 'all';
  438. }
  439. this.selectedIndex = 0;
  440. selectedItem = this.allKnowledgeList[0];
  441. } else if (this.highVulnerability && this.highVulnerability.length > 0) {
  442. // 如果全部知识点没有数据但高频错题知识点有数据,切换到高频错题知识点并选中第一条
  443. if (this.activeTab !== 'highFreq') {
  444. this.activeTab = 'highFreq';
  445. }
  446. this.selectedIndex = 0;
  447. selectedItem = this.highVulnerability[0];
  448. } else if (this.fatalVulnerability && this.fatalVulnerability.length > 0) {
  449. // 如果零分知识点有数据,切换到零分知识点并选中第一条
  450. if (this.activeTab !== 'zero') {
  451. this.activeTab = 'zero';
  452. }
  453. this.selectedIndex = 0;
  454. selectedItem = this.fatalVulnerability[0];
  455. }
  456. // 如果有选中项,更新selectedKnowledgeId,实现初始化默认选择
  457. if (selectedItem) {
  458. this.selectedKnowledgeId = selectedItem.knowledgeId;
  459. // 如果当前是图形视图且图表已初始化,更新图表选中状态
  460. if (this.activeView === 'graph' && this.chart) {
  461. this.updateChart();
  462. }
  463. }
  464. // 只有当tab值实际发生变化时,才触发选中事件
  465. // 初始化时不触发,避免重复调用接口
  466. if (this._isMounted && oldActiveTab !== this.activeTab) {
  467. // 组件已挂载且tab值发生变化时才发送事件
  468. }
  469. },
  470. // 递归收集所有节点的key,用于展开所有树形节点
  471. collectAllRowKeys() {
  472. const keys = [];
  473. // 递归函数,收集所有有children的节点key
  474. const collectKeys = (data) => {
  475. if (!data || !Array.isArray(data)) return;
  476. data.forEach(item => {
  477. // 如果节点有children,将其key添加到keys数组中
  478. if (item.children && item.children.length > 0) {
  479. // 使用knowledgeId作为key
  480. keys.push(item.knowledgeId);
  481. // 递归处理children
  482. collectKeys(item.children);
  483. }
  484. });
  485. };
  486. // 调用递归函数
  487. collectKeys(this.tableData);
  488. // 更新expandRowKeys数组
  489. this.expandRowKeys = keys;
  490. },
  491. // 使用vxe-table API展开所有树形节点
  492. expandAllNodes() {
  493. // 获取vxe-table实例
  494. const treeTable = this.$refs.treeTable;
  495. if (treeTable) {
  496. // 递归收集所有需要展开的节点
  497. const expandNodes = [];
  498. const collectExpandNodes = (data) => {
  499. if (!data || !Array.isArray(data)) return;
  500. data.forEach(item => {
  501. if (item.children && item.children.length > 0) {
  502. expandNodes.push(item);
  503. collectExpandNodes(item.children);
  504. }
  505. });
  506. };
  507. collectExpandNodes(this.tableData);
  508. // 使用vxe-table 3.7.5兼容的API展开所有节点
  509. if (treeTable.setExpandRows) {
  510. // 如果支持setExpandRows方法
  511. treeTable.setExpandRows(expandNodes);
  512. } else if (treeTable.setTreeExpand) {
  513. // 如果支持setTreeExpand方法
  514. expandNodes.forEach(node => {
  515. treeTable.setTreeExpand(node, true);
  516. });
  517. }
  518. }
  519. },
  520. // 切换查看方式
  521. switchView(view) {
  522. // 通过emit事件通知父组件更新activeView
  523. this.$emit('view-change', view);
  524. },
  525. // 年级、班级组合判断
  526. classIsGroup() {
  527. if (this.classLevel == 1) {
  528. return true;
  529. } else
  530. return false;
  531. },
  532. // 切换图例选中状态
  533. toggleLegend(type) {
  534. this.selectedLegend[type] = !this.selectedLegend[type];
  535. // 生成scoreRateTypes数组
  536. let scoreRateTypes = [];
  537. if (this.selectedLegend.weak) scoreRateTypes.push(1);
  538. if (this.selectedLegend.good) scoreRateTypes.push(2);
  539. if (this.selectedLegend.excellent) scoreRateTypes.push(3);
  540. // 如果包含所有值,则传null,否则传数组
  541. if (scoreRateTypes.length === 3) {
  542. scoreRateTypes = null;
  543. }
  544. // 向父组件发送图例变化事件,包含scoreRateTypes和当前activeTab对应的knowledgeType
  545. let knowledgeType = 0;
  546. if (this.activeTab === 'highFreq') {
  547. knowledgeType = 1;
  548. } else if (this.activeTab === 'zero') {
  549. knowledgeType = 2;
  550. } else {
  551. knowledgeType = 0;
  552. }
  553. this.$emit('legend-change', { scoreRateTypes, knowledgeType });
  554. },
  555. // 根据得分率获取对应的颜色
  556. getNodeColor(scoreRate) {
  557. const rate = parseFloat(scoreRate);
  558. if (rate >= 85) {
  559. return '#3BA272'; // 优秀 - 绿色
  560. } else if (rate >= 60) {
  561. return '#FAC858'; // 良好 - 黄色
  562. } else {
  563. return '#EE6666'; // 薄弱 - 红色
  564. }
  565. },
  566. // 更新图表缩放和布局参数,防止窗口变化时图表变形
  567. updateChartLayout() {
  568. if (this.chart) {
  569. // 重新计算最外层知识点数量
  570. const countOutermostNodes = (data) => {
  571. let count = 0;
  572. const traverse = (nodes) => {
  573. nodes.forEach(node => {
  574. if (node.children && node.children.length > 0) {
  575. traverse(node.children);
  576. } else {
  577. count++;
  578. }
  579. });
  580. };
  581. traverse(data);
  582. return count;
  583. };
  584. const outermostCount = countOutermostNodes(this.tableData);
  585. // 重新计算合适的缩放比例
  586. const getOptimalZoom = () => {
  587. const screenWidth = window.innerWidth;
  588. let zoom = 1.05;
  589. if (screenWidth < 1100) {
  590. zoom = 1.05;
  591. } else if (screenWidth >= 1100 && screenWidth <= 1200) {
  592. zoom = 1.05;
  593. } else {
  594. zoom = 1.05;
  595. }
  596. if (outermostCount <= 5) {
  597. zoom *= 1.0;
  598. }
  599. return zoom;
  600. };
  601. // 重新计算内外半径
  602. const getOptimalRadius = () => {
  603. if (outermostCount <= 5) {
  604. return ['20%', '65%'];
  605. }
  606. return ['12%', '75%'];
  607. };
  608. // 更新图表的所有动态参数
  609. this.chart.setOption({
  610. series: [{
  611. // 更新缩放比例
  612. zoom: getOptimalZoom(),
  613. // 更新内外半径
  614. radius: getOptimalRadius(),
  615. // 更新层级间距
  616. layerPadding: outermostCount <= 5 ? [20, 10] : [10, 5]
  617. }]
  618. });
  619. }
  620. },
  621. // 初始化echarts图表
  622. initChart() {
  623. try {
  624. // 检查chart容器是否存在
  625. if (!this.$refs.chart) {
  626. console.error('图表容器不存在');
  627. return;
  628. }
  629. this.createChart();
  630. }
  631. catch (error) {
  632. console.error('图表初始化失败:', error);
  633. }
  634. },
  635. // 创建图表
  636. createChart() {
  637. try {
  638. // 检查chart容器是否存在
  639. if (!this.$refs.chart) {
  640. console.error('图表容器不存在');
  641. return;
  642. }
  643. // 销毁现有的图表实例
  644. if (this.chart) {
  645. this.chart.dispose();
  646. }
  647. // 创建echarts实例
  648. this.chart = echarts.init(this.$refs.chart);
  649. // 保存当前组件实例的引用
  650. const vm = this;
  651. // 根据得分率获取对应的级别
  652. const getNodeLevel = (scoreRate) => {
  653. const rate = parseFloat(scoreRate);
  654. // 处理scoreRate为null、undefined或非数字的情况
  655. if (isNaN(rate)) {
  656. return 'weak'; // 默认为薄弱
  657. }
  658. if (rate >= 85) {
  659. return 'excellent'; // 优秀
  660. } else if (rate >= 60) {
  661. return 'good'; // 良好
  662. } else {
  663. return 'weak'; // 薄弱
  664. }
  665. };
  666. // 基于表格数据生成树形图数据
  667. // 添加一个可选参数overrideKnowledgeId,用于临时覆盖当前的currentKnowledgeId
  668. const generateTreeData = (data, level = 0, overrideKnowledgeId = null) => {
  669. // 使用传入的overrideKnowledgeId或默认使用vm.selectedKnowledgeId
  670. const currentKnowledgeId = overrideKnowledgeId !== null ? overrideKnowledgeId : vm.selectedKnowledgeId;
  671. return data.map(item => {
  672. // 检查当前节点是否匹配选中的知识点ID
  673. const isMatched = String(item.knowledgeId) === String(currentKnowledgeId);
  674. // 获取节点级别,优先使用对应级别的得分率,若缺失则使用另一级别作为备选
  675. const scoreRate = item.personalScoreRate;
  676. const hasData = scoreRate !== undefined && scoreRate !== null && scoreRate !== '';
  677. // 设置节点大小:有数据的默认10px,选中后15px;无数据的5px
  678. let symbolSize = 5;
  679. if (hasData) {
  680. symbolSize = isMatched ? 15 : 10;
  681. }
  682. // 计算节点样式
  683. const itemColor = hasData ? vm.getNodeColor(scoreRate) : '#BFC1C7';
  684. // 获取节点级别用于图例过滤
  685. const nodeLevel = getNodeLevel(scoreRate);
  686. // 应用图例过滤
  687. const shouldShow = vm.selectedLegend[nodeLevel];
  688. // 设置节点透明度:显示为1,隐藏为0
  689. const opacity = shouldShow ? 1 : 0;
  690. const node = {
  691. name: item.knowledgeName,
  692. symbolSize: symbolSize,
  693. // 使用已计算的scoreRate作为value,确保数据一致性
  694. value: scoreRate,
  695. itemStyle: {
  696. color: itemColor,
  697. opacity: opacity
  698. },
  699. // 当节点被隐藏时,隐藏连接到该节点的线
  700. lineStyle: {
  701. opacity: shouldShow ? 1 : 0
  702. },
  703. // 保存知识ID用于匹配
  704. knowledgeId: item.knowledgeId
  705. };
  706. if (item.children && item.children.length > 0) {
  707. node.children = generateTreeData(item.children, level + 1, overrideKnowledgeId);
  708. }
  709. return node;
  710. });
  711. };
  712. // 计算最外层知识点数量(叶子节点数量),用于动态调整布局
  713. const countOutermostNodes = (data) => {
  714. let count = 0;
  715. const traverse = (nodes) => {
  716. nodes.forEach(node => {
  717. if (node.children && node.children.length > 0) {
  718. traverse(node.children);
  719. } else {
  720. count++;
  721. }
  722. });
  723. };
  724. traverse(data);
  725. return count;
  726. };
  727. // 获取最外层知识点数量
  728. const outermostCount = countOutermostNodes(this.tableData);
  729. // 计算合适的缩放比例,根据屏幕宽度和节点数量动态调整
  730. const getOptimalZoom = () => {
  731. const screenWidth = window.innerWidth;
  732. let zoom = 1.05;
  733. // 根据屏幕宽度调整缩放
  734. if (screenWidth < 1100) {
  735. zoom = 1.05;
  736. } else if (screenWidth >= 1100 && screenWidth <= 1200) {
  737. zoom = 1.05;
  738. } else {
  739. zoom = 1.05;
  740. }
  741. // 根据节点数量调整缩放,节点少的时候适当放大
  742. if (outermostCount <= 5) {
  743. zoom *= 1.0;
  744. }
  745. return zoom;
  746. };
  747. // 根据节点数量动态调整内外半径,防止节点遮盖
  748. const getOptimalRadius = () => {
  749. // 节点数量少的时候,增大内半径,缩小外半径,使节点分布更合理
  750. if (outermostCount <= 5) {
  751. return ['20%', '65%'];
  752. }
  753. // 节点数量多的时候,使用默认半径
  754. return ['12%', '75%'];
  755. };
  756. // 配置项
  757. const option = {
  758. tooltip: {
  759. formatter: (params) => {
  760. // 检查是否为根节点
  761. if (params.treePathInfo && params.treePathInfo.length === 1) {
  762. // 根节点也属于非最外层节点,只显示名称
  763. return params.name;
  764. }
  765. // 检查节点是否为最外层节点(叶子节点)
  766. // 使用isOutermost属性或检查data.isOutermost
  767. const isOutermost = params.data && params.data.isOutermost;
  768. if (isOutermost) {
  769. // 最外层节点显示完整信息(名称+得分率)
  770. return `${params.name}<br/>得分率: ${params.value !== null && params.value !== undefined ? params.value : '0'}%`;
  771. } else {
  772. // 非最外层节点只显示名称
  773. return params.name;
  774. }
  775. }
  776. },
  777. animationDurationUpdate: 1500,
  778. animationEasingUpdate: 'quinticInOut',
  779. series: [
  780. {
  781. type: 'tree',
  782. layout: 'radial',
  783. symbol: 'circle',
  784. initialTreeDepth: 999, // 设置一个足够大的值,确保所有节点都展开
  785. expandAndCollapse: false,
  786. // 调整树状图布局参数,确保节点均匀分布
  787. orient: 'radial',
  788. roam: false, // 禁用缩放和平移
  789. // 使用边距控制图表位置,实现水平垂直居中
  790. left: '8%', // 左边距(百分比/像素)
  791. right: '8%', // 右边距
  792. top: '8%', // 上边距
  793. bottom: '8%', // 下边距
  794. // 根据节点数量动态调整内外半径,防止节点遮盖
  795. radius: getOptimalRadius(),
  796. // 调整节点大小比例和间距,确保节点不遮盖
  797. nodeScaleRatio: 1,
  798. // 根据节点数量调整层级间距,节点少的时候增大间距
  799. layerPadding: outermostCount <= 5 ? [20, 10] : [10, 5],
  800. // 使用表格数据生成树形结构
  801. data: [
  802. {
  803. name: this.subjectName,
  804. symbolSize: 5, // 根节点大小
  805. value: this.subjectScoreRate, // 添加value属性,用于显示得分率
  806. itemStyle: {
  807. // 根节点为非最外层节点,使用#EBEEF5颜色
  808. color: '#BFC1C7'
  809. },
  810. // 标记为非最外层节点
  811. isOutermost: false,
  812. children: generateTreeData(this.tableData)
  813. }
  814. ],
  815. label: {
  816. show: false
  817. },
  818. lineStyle: {
  819. color: '#ECEEF3',
  820. width: 1,
  821. type: 'solid'
  822. },
  823. emphasis: {
  824. focus: 'adjacency',
  825. lineStyle: {
  826. width: 1
  827. }
  828. }
  829. }
  830. ]
  831. };
  832. // 设置配置项
  833. this.chart.setOption(option);
  834. // 初始化后调用resize确保图表正确显示
  835. this.chart.resize();
  836. // 添加图表点击事件监听
  837. this.chart.on('click', (params) => {
  838. // 明确识别点击类型
  839. const isOuterNodeClick = params.data && params.data.isOutermost;
  840. const isEmptyAreaClick = !params.data || !params.componentType || params.componentType === '';
  841. const isNonOuterNodeClick = params.data && !params.data.isOutermost;
  842. if (isOuterNodeClick) {
  843. // 处理最外层节点点击
  844. // 确定当前显示的列表数据
  845. let targetList = [];
  846. if (this.activeTab === 'zero') {
  847. targetList = this.fatalVulnerability;
  848. } else if (this.activeTab === 'highFreq') {
  849. targetList = this.highVulnerability;
  850. } else {
  851. targetList = this.allKnowledgeList;
  852. }
  853. // 1. 首先尝试直接从当前列表中根据名称查找
  854. let knowledgeItem = targetList.find(item => item.knowledgeName === params.name);
  855. let targetIndex = -1;
  856. if (knowledgeItem) {
  857. targetIndex = targetList.findIndex(item => item.knowledgeId === knowledgeItem.knowledgeId);
  858. }
  859. // 2. 如果在当前列表中找不到,尝试从其他列表中查找
  860. if (!knowledgeItem) {
  861. const allLists = [this.allKnowledgeList, this.highVulnerability, this.fatalVulnerability];
  862. for (const list of allLists) {
  863. knowledgeItem = list.find(item => item.knowledgeName === params.name);
  864. if (knowledgeItem) {
  865. // 找到后切换到对应tab
  866. if (list === this.highVulnerability) {
  867. this.activeTab = 'highFreq';
  868. targetList = this.highVulnerability;
  869. } else if (list === this.fatalVulnerability) {
  870. this.activeTab = 'zero';
  871. targetList = this.fatalVulnerability;
  872. } else {
  873. this.activeTab = 'all';
  874. targetList = this.allKnowledgeList;
  875. }
  876. targetIndex = targetList.findIndex(item => item.knowledgeId === knowledgeItem.knowledgeId);
  877. break;
  878. }
  879. }
  880. }
  881. // 3. 如果还是找不到,尝试从tableData中查找
  882. if (!knowledgeItem) {
  883. const findKnowledgeItem = (data) => {
  884. for (let item of data) {
  885. if (item.knowledgeName === params.name) {
  886. return item;
  887. }
  888. if (item.children && item.children.length > 0) {
  889. const found = findKnowledgeItem(item.children);
  890. if (found) return found;
  891. }
  892. }
  893. return null;
  894. };
  895. knowledgeItem = findKnowledgeItem(this.tableData);
  896. if (knowledgeItem) {
  897. // 在所有列表中查找匹配的knowledgeId
  898. const allLists = [this.allKnowledgeList, this.highVulnerability, this.fatalVulnerability];
  899. for (const list of allLists) {
  900. targetIndex = list.findIndex(item => {
  901. // 考虑类型转换,确保比较准确
  902. return String(item.knowledgeId) === String(knowledgeItem.knowledgeId);
  903. });
  904. if (targetIndex !== -1) {
  905. // 切换到对应tab
  906. if (list === this.highVulnerability) {
  907. this.activeTab = 'highFreq';
  908. targetList = this.highVulnerability;
  909. } else if (list === this.fatalVulnerability) {
  910. this.activeTab = 'zero';
  911. targetList = this.fatalVulnerability;
  912. } else {
  913. this.activeTab = 'all';
  914. targetList = this.allKnowledgeList;
  915. }
  916. break;
  917. }
  918. }
  919. }
  920. }
  921. if (knowledgeItem && targetIndex !== -1) {
  922. // 向父组件发送事件
  923. this.$emit('knowledge-item-click', {
  924. item: knowledgeItem,
  925. index: targetIndex
  926. });
  927. // 更新选中索引和选中知识点ID
  928. this.selectedIndex = targetIndex;
  929. this.selectedKnowledgeId = knowledgeItem.knowledgeId;
  930. // 将首次渲染标志设为false,后续点击操作将应用透明度降低逻辑
  931. // this._isFirstRender = false;
  932. // 重新生成图表数据,确保只有当前选中节点高亮
  933. this.chart.setOption({
  934. series: [{
  935. data: [
  936. {
  937. name: this.subjectName,
  938. symbolSize: 5,
  939. value: this.subjectScoreRate,
  940. itemStyle: {
  941. color: '#BFC1C7',
  942. opacity: 1
  943. },
  944. isOutermost: false,
  945. children: generateTreeData(this.tableData, 0, this.selectedKnowledgeId)
  946. }
  947. ]
  948. }]
  949. }, {
  950. animation: true,
  951. animationDuration: 300
  952. });
  953. // 滚动到选中项
  954. this.$nextTick(() => {
  955. const listContainer = this.$el.querySelector('.knowledge_list');
  956. const listItems = listContainer.querySelectorAll('.list_item');
  957. if (listItems[targetIndex]) {
  958. listContainer.scrollTop = listItems[targetIndex].offsetTop - 100;
  959. }
  960. });
  961. } else {
  962. // 打印详细调试信息
  963. console.log('点击处理调试信息:', {
  964. paramsName: params.name,
  965. params: params,
  966. knowledgeItem: knowledgeItem,
  967. targetIndex: targetIndex,
  968. activeTab: this.activeTab,
  969. targetListLength: targetList.length,
  970. targetListSample: targetList.slice(0, 3),
  971. tableDataSample: this.tableData.slice(0, 1)
  972. });
  973. }
  974. }
  975. else if (isEmptyAreaClick || isNonOuterNodeClick) {
  976. // 点击空白区域或非最外层节点,恢复所有节点透明度
  977. // 清空selectedKnowledgeId,确保所有节点都高亮
  978. this.selectedKnowledgeId = '';
  979. // 调用generateTreeData时传入overrideKnowledgeId为空字符串,确保所有节点都不透明
  980. const updatedOption = {
  981. series: [{
  982. data: [
  983. {
  984. name: this.subjectName,
  985. symbolSize: 5,
  986. value: this.subjectScoreRate,
  987. itemStyle: {
  988. color: '#BFC1C7',
  989. opacity: 1
  990. },
  991. isOutermost: false,
  992. // 传入overrideKnowledgeId为空字符串,确保生成的所有节点都不透明
  993. children: generateTreeData(this.tableData, 0, '')
  994. }
  995. ]
  996. }]
  997. };
  998. // 更新图表,使用动画实现平滑过渡
  999. this.chart.setOption(updatedOption, {
  1000. animation: true,
  1001. animationDuration: 300
  1002. });
  1003. // 向父组件发送事件,清除选中的知识点ID
  1004. this.$emit('knowledge-item-click', {
  1005. item: null,
  1006. index: -1
  1007. });
  1008. }
  1009. });
  1010. // 监听窗口大小变化
  1011. window.addEventListener('resize', () => {
  1012. if (this.chart) {
  1013. this.chart.resize();
  1014. this.updateChartLayout();
  1015. }
  1016. });
  1017. }
  1018. catch (error) {
  1019. console.error('图表创建失败:', error);
  1020. }
  1021. },
  1022. // 更新图表数据
  1023. updateChart() {
  1024. try {
  1025. if (this.activeView === 'graph' && this.$refs.chart) {
  1026. // 重新创建图表,实现刷新效果
  1027. this.createChart();
  1028. }
  1029. }
  1030. catch (error) {
  1031. console.error('图表更新失败:', error);
  1032. }
  1033. },
  1034. // 根据得分率获取对应的样式类
  1035. getRateClass(rate) {
  1036. const rateValue = parseFloat(rate);
  1037. if (rateValue >= 85) {
  1038. return 'rate_excellent';
  1039. } else if (rateValue >= 60) {
  1040. return 'rate_good';
  1041. } else {
  1042. return 'rate_weak';
  1043. }
  1044. },
  1045. // 根据得分率获取对应的状态名称
  1046. getRateName(rate) {
  1047. const rateValue = parseFloat(rate);
  1048. if (rateValue >= 85) {
  1049. return '优秀';
  1050. } else if (rateValue >= 60) {
  1051. return '良好';
  1052. } else {
  1053. return '薄弱';
  1054. }
  1055. },
  1056. // 行样式类名方法 - vxe-table 3.7.5版本兼容
  1057. rowClassName({ row }) {
  1058. // 使用selectedRow引用比较,避免修改数据
  1059. if (this.selectedRow && this.selectedRow.knowledgeId === row.knowledgeId) {
  1060. return 'selected-row';
  1061. }
  1062. return row.highlight ? 'row_highlight' : '';
  1063. },
  1064. // 处理知识点列表项点击事件
  1065. handleItemClick(item, index) {
  1066. // 更新选中索引
  1067. this.selectedIndex = index;
  1068. // 更新选中知识点ID,实现反向选择功能
  1069. this.selectedKnowledgeId = item.knowledgeId;
  1070. // 向父组件发送事件,传递点击的知识点数据
  1071. this.$emit('knowledge-item-click', { item, index });
  1072. // 如果当前是图形视图且图表已初始化,更新图表选中状态
  1073. if (this.activeView === 'graph' && this.chart) {
  1074. this.updateChart();
  1075. }
  1076. },
  1077. // 处理树形表格行点击事件
  1078. handleRowClick(row) {
  1079. // 检查是否为最后一级节点(没有children或children为空)
  1080. const isLastLevel = !row.children || row.children.length === 0;
  1081. if (isLastLevel) {
  1082. // 设置当前行为选中行,不修改数据,只更新引用
  1083. this.selectedRow = row;
  1084. // 调用handleItemClick方法处理点击事件
  1085. this.handleItemClick(row, -1);
  1086. }
  1087. },
  1088. // 处理单元格点击事件
  1089. handleCellClick({ row }) {
  1090. // 调用行点击事件处理逻辑
  1091. this.handleRowClick(row);
  1092. },
  1093. // 根据知识点得分率获取对应的点颜色
  1094. getDotColor(item, scoreRateType) {
  1095. // 个人得分率personalScoreRate
  1096. const scoreRate = scoreRateType === 'personalScoreRate' ? item.personalScoreRate : item.classScoreRate;
  1097. const rate = parseFloat(scoreRate);
  1098. if (rate >= 85) {
  1099. return '#3BA272'; // 优秀 - 绿色
  1100. } else if (rate >= 60 && rate < 84) {
  1101. return '#FAC858'; // 良好 - 黄色
  1102. } else if (rate < 59) {
  1103. return '#EE6666'; // 薄弱 - 红色
  1104. }
  1105. }
  1106. }
  1107. };
  1108. </script>
  1109. <style lang="scss" scoped>
  1110. .knowledge_graph {
  1111. background: #FFFFFF;
  1112. border-radius: 10px;
  1113. padding: 20px;
  1114. margin-bottom: 10px;
  1115. position: relative;
  1116. /* 选中行样式 - 确保所有情况下都能正确应用 */
  1117. :deep(.vxe-body--row.selected-row),
  1118. :deep(.selected-row) {
  1119. background-color: rgba(46, 100, 250, 0.1) !important;
  1120. color: #2E64FA !important;
  1121. /* 确保所有子元素也继承文字颜色 */
  1122. * {
  1123. color: #2E64FA !important;
  1124. }
  1125. /* 特殊处理评分点样式 */
  1126. .rate_dot {
  1127. border-color: rgba(46, 100, 250, 0.1) !important;
  1128. }
  1129. }
  1130. .graph_header {
  1131. margin-bottom: 20px;
  1132. position: relative;
  1133. display: flex;
  1134. justify-content: space-between;
  1135. align-items: center;
  1136. .legend {
  1137. display: flex;
  1138. gap: 20px;
  1139. .legend_item {
  1140. display: flex;
  1141. align-items: center;
  1142. gap: 5px;
  1143. cursor: pointer;
  1144. opacity: 0.7;
  1145. transition: all 0.3s ease;
  1146. font-weight: 500;
  1147. &.selected {
  1148. opacity: 1;
  1149. }
  1150. &:hover {
  1151. opacity: 1;
  1152. }
  1153. .legend_dot {
  1154. width: 20px;
  1155. height: 10px;
  1156. border-radius: 2px;
  1157. transition: all 0.3s ease;
  1158. background: #999999; // 默认灰色
  1159. &.weak {
  1160. background: #EE6666;
  1161. }
  1162. &.good {
  1163. background: #FAC858;
  1164. }
  1165. &.excellent {
  1166. background: #3BA272;
  1167. }
  1168. }
  1169. .legend_text {
  1170. font-size: 12px;
  1171. color: #999999; // 默认灰色文字
  1172. transition: all 0.3s ease;
  1173. }
  1174. // 选中状态样式
  1175. &.selected {
  1176. .legend_dot.weak {
  1177. background: #EE6666;
  1178. }
  1179. .legend_dot.good {
  1180. background: #FAC858;
  1181. }
  1182. .legend_dot.excellent {
  1183. background: #3BA272;
  1184. }
  1185. .legend_text {
  1186. color: #333;
  1187. }
  1188. }
  1189. }
  1190. }
  1191. }
  1192. // 视图切换按钮样式
  1193. .view_switcher_container {
  1194. display: flex;
  1195. gap: 20px;
  1196. z-index: 10;
  1197. justify-content: flex-end;
  1198. font-size: 14px;
  1199. .switch_btn {
  1200. display: flex;
  1201. align-items: center;
  1202. padding: 0;
  1203. background-color: transparent;
  1204. border: none;
  1205. border-radius: 0;
  1206. font-size: 14px;
  1207. transition: color 0.3s ease;
  1208. cursor: pointer;
  1209. font-weight: 400;
  1210. color: #999999;
  1211. &:hover {
  1212. color: #2E64FA;
  1213. }
  1214. }
  1215. .graph_btn {
  1216. color: #2E64FA;
  1217. }
  1218. // 图标样式,确保与文字对齐
  1219. .icon_switch_graph,
  1220. .icon_switch_list {
  1221. img {
  1222. display: inline-block;
  1223. vertical-align: middle;
  1224. margin: 0;
  1225. padding: 0;
  1226. margin-top: -3px;
  1227. }
  1228. }
  1229. // 鼠标悬停时图片换色效果
  1230. .switch_btn:hover .icon_switch_graph img,
  1231. .switch_btn:hover .icon_switch_list img {
  1232. filter: brightness(0) saturate(100%) invert(34%) sepia(100%) saturate(5000%) hue-rotate(210deg) brightness(95%) contrast(100%);
  1233. }
  1234. }
  1235. // 对比选择器样式
  1236. .comparison_selector {
  1237. display: flex;
  1238. align-items: center;
  1239. gap: 10px;
  1240. .student_position {
  1241. font-size: 14px;
  1242. color: #999999;
  1243. }
  1244. // 对比选择器按钮样式
  1245. :deep(.el-button-group) {
  1246. .el-button {
  1247. padding: 7px 10px;
  1248. // border-radius: 4px;
  1249. &:not(.el-button--primary) {
  1250. color: #999999;
  1251. background-color: #FFFFFF;
  1252. border-color: #DCDFE6;
  1253. &:hover {
  1254. color: #2E64FA;
  1255. border-color: #C6E2FF;
  1256. }
  1257. }
  1258. &.el-button--primary {
  1259. background-color: #2E64FA;
  1260. color: #FFFFFF;
  1261. border-color: #2E64FA;
  1262. &:hover {
  1263. background-color: #409EFF;
  1264. border-color: #409EFF;
  1265. }
  1266. }
  1267. }
  1268. }
  1269. }
  1270. .graph_content {
  1271. display: flex;
  1272. gap: 20px;
  1273. position: relative;
  1274. .knowledge_tab {
  1275. display: flex;
  1276. border-bottom: 1px solid #ECEEF3;
  1277. margin-bottom: 15px;
  1278. .tab_item {
  1279. height: 40px;
  1280. line-height: 40px;
  1281. cursor: pointer;
  1282. font-size: 14px;
  1283. color: #666666;
  1284. position: relative;
  1285. transition: all 0.3s ease;
  1286. margin-right: 20px;
  1287. &.active {
  1288. color: #2E64FA;
  1289. font-weight: 500;
  1290. &::after {
  1291. content: '';
  1292. position: absolute;
  1293. bottom: -1px;
  1294. left: 0;
  1295. width: 100%;
  1296. height: 2px;
  1297. background-color: #2E64FA;
  1298. }
  1299. }
  1300. &:hover {
  1301. color: #2E64FA;
  1302. }
  1303. // 零分知识点:根据文字宽度自适应,不需要内间距
  1304. // &:first-child {
  1305. // padding: 0;
  1306. // white-space: nowrap;
  1307. // }
  1308. // // 高频错题知识点:占满剩余宽度
  1309. // &:last-child {
  1310. // padding: 0;
  1311. // white-space: nowrap;
  1312. // }
  1313. }
  1314. }
  1315. .knowledge_right_container {
  1316. width: 350px;
  1317. display: flex;
  1318. flex-direction: column;
  1319. }
  1320. .chart_container {
  1321. flex: 1;
  1322. min-width: 0;
  1323. border-radius: 10px 10px 10px 10px;
  1324. border: 1px solid #E9EBEF;
  1325. position: relative;
  1326. padding: 20px;
  1327. overflow: hidden;
  1328. .top_container {
  1329. display: flex;
  1330. justify-content: space-between;
  1331. align-items: flex-start;
  1332. flex-wrap: wrap;
  1333. gap: 10px;
  1334. margin-bottom: 10px;
  1335. }
  1336. .legend {
  1337. display: flex;
  1338. gap: 20px;
  1339. flex-wrap: wrap; // 小屏幕下自动换行
  1340. flex: 1;
  1341. min-width: 0;
  1342. .legend_item {
  1343. display: flex;
  1344. align-items: center;
  1345. gap: 5px;
  1346. cursor: pointer;
  1347. opacity: 0.7;
  1348. transition: all 0.3s ease;
  1349. font-weight: 500;
  1350. &.selected {
  1351. opacity: 1;
  1352. }
  1353. &:hover {
  1354. opacity: 1;
  1355. }
  1356. .legend_dot {
  1357. width: 20px;
  1358. height: 10px;
  1359. border-radius: 2px;
  1360. transition: all 0.3s ease;
  1361. background: #999999; // 默认灰色
  1362. &.weak {
  1363. background: #EE6666;
  1364. }
  1365. &.good {
  1366. background: #FAC858;
  1367. }
  1368. &.excellent {
  1369. background: #3BA272;
  1370. }
  1371. }
  1372. .legend_text {
  1373. font-size: 12px;
  1374. color: #999999; // 默认灰色文字
  1375. transition: all 0.3s ease;
  1376. }
  1377. // 选中状态样式
  1378. &.selected {
  1379. .legend_dot.weak {
  1380. background: #EE6666;
  1381. }
  1382. .legend_dot.good {
  1383. background: #FAC858;
  1384. }
  1385. .legend_dot.excellent {
  1386. background: #3BA272;
  1387. }
  1388. .legend_text {
  1389. color: #333;
  1390. }
  1391. }
  1392. }
  1393. }
  1394. .chart {
  1395. display: flex;
  1396. justify-content: center;
  1397. align-items: center;
  1398. min-height: 590px;
  1399. }
  1400. .view_switcher_container {
  1401. display: flex;
  1402. gap: 20px;
  1403. z-index: 10;
  1404. justify-content: flex-end;
  1405. font-size: 14px;
  1406. white-space: nowrap; // 防止按钮换行
  1407. }
  1408. }
  1409. .knowledge_list {
  1410. width: 100%;
  1411. height: 630px;
  1412. overflow-y: auto;
  1413. padding-right: 10px;
  1414. position: relative;
  1415. /* 滚动条样式 */
  1416. &::-webkit-scrollbar {
  1417. width: 6px;
  1418. }
  1419. &::-webkit-scrollbar-track {
  1420. background: #f1f1f1;
  1421. border-radius: 3px;
  1422. }
  1423. &::-webkit-scrollbar-thumb {
  1424. background: #c1c1c1;
  1425. border-radius: 3px;
  1426. }
  1427. &::-webkit-scrollbar-thumb:hover {
  1428. background: #a8a8a8;
  1429. }
  1430. /* 暂无数据样式 */
  1431. .no_data {
  1432. position: absolute;
  1433. top: 50%;
  1434. left: 50%;
  1435. transform: translate(-50%, -50%);
  1436. font-size: 14px;
  1437. color: #909399;
  1438. text-align: center;
  1439. width: 100%;
  1440. height: 100%;
  1441. display: flex;
  1442. align-items: center;
  1443. justify-content: center;
  1444. border-radius: 8px;
  1445. span {
  1446. margin-top: 23%;
  1447. }
  1448. }
  1449. .list_item {
  1450. background: rgba(255, 255, 255, 0.1);
  1451. border-radius: 6px;
  1452. padding: 10px;
  1453. margin-bottom: 10px;
  1454. cursor: pointer;
  1455. &:last-child {
  1456. margin-bottom: 0;
  1457. }
  1458. &:hover,
  1459. &.active {
  1460. background: rgba(46, 100, 250, 0.1);
  1461. border-radius: 6px;
  1462. }
  1463. .item_header {
  1464. display: flex;
  1465. align-items: center;
  1466. margin-bottom: 8px;
  1467. .item_dot {
  1468. display: inline-block;
  1469. width: 8px;
  1470. height: 8px;
  1471. background: #EE6666;
  1472. border-radius: 50%;
  1473. margin-right: 8px;
  1474. }
  1475. .item_title {
  1476. font-size: 14px;
  1477. font-weight: 500;
  1478. color: #333;
  1479. flex: 1;
  1480. white-space: nowrap;
  1481. overflow: hidden;
  1482. text-overflow: ellipsis;
  1483. min-width: 0;
  1484. }
  1485. .item_tag {
  1486. color: #FFFFFF;
  1487. font-size: 12px;
  1488. padding: 4px 6px;
  1489. margin-left: 5px;
  1490. text-align: center;
  1491. border-radius: 4px;
  1492. font-weight: 400;
  1493. display: flex;
  1494. align-items: center;
  1495. justify-content: center;
  1496. gap: 4px;
  1497. i:last-child {
  1498. display: block;
  1499. }
  1500. }
  1501. }
  1502. .item_info {
  1503. font-size: 12px;
  1504. color: #999;
  1505. margin-top: 8px;
  1506. display: flex;
  1507. justify-content: space-between;
  1508. align-items: center;
  1509. .item_score {
  1510. text-align: left;
  1511. .score_label {
  1512. color: #999999;
  1513. }
  1514. .score_first {
  1515. color: #F56C6C;
  1516. }
  1517. .score_separator {
  1518. color: #999999;
  1519. margin: 0 4px;
  1520. }
  1521. .score_second {
  1522. color: #666666;
  1523. }
  1524. }
  1525. .item_exam_count {
  1526. text-align: right;
  1527. color: #999999;
  1528. .exam_count {
  1529. color: #2E64FA;
  1530. }
  1531. }
  1532. }
  1533. }
  1534. }
  1535. }
  1536. // vxe表格样式
  1537. .list_content {
  1538. border-radius: 10px 10px 10px 10px;
  1539. .vxe-table {
  1540. // 展开收起的样式
  1541. :deep(.el-icon-circle-plus) {
  1542. color: #DCDFE6 !important;
  1543. font-size: 16px !important;
  1544. }
  1545. :deep(.el-icon-remove) {
  1546. color: #2E64FA !important;
  1547. font-size: 16px !important;
  1548. }
  1549. }
  1550. /* 设置表格行高度和光标样式 */
  1551. :deep {
  1552. .vxe-table {
  1553. border-radius: 8px;
  1554. overflow-y: auto;
  1555. overflow-x: hidden;
  1556. // 使用CSS变量设置全局行高
  1557. --vxe-ui-table-row-height-default: 20px !important;
  1558. // 表头样式
  1559. .vxe-table--header {
  1560. display: table-header-group !important;
  1561. visibility: visible !important;
  1562. opacity: 1 !important;
  1563. height: 52px !important;
  1564. }
  1565. /* 设置表头高度 */
  1566. .vxe-table--header {
  1567. .vxe-header--row {
  1568. height: 52px !important;
  1569. }
  1570. }
  1571. // 行样式 - 使用更具体的选择器
  1572. .vxe-table--body {
  1573. // 表格行样式
  1574. tbody {
  1575. tr {
  1576. border-bottom: 1px solid #F0F2F5;
  1577. cursor: pointer !important;
  1578. height: 20px !important;
  1579. line-height: 20px !important;
  1580. min-height: 20px !important;
  1581. max-height: 20px !important;
  1582. &:hover {
  1583. background-color: #f5f7fa !important;
  1584. }
  1585. &.row_highlight {
  1586. background-color: rgba(195, 219, 255, 0.2);
  1587. cursor: pointer !important;
  1588. }
  1589. &.selected-row {
  1590. background-color: rgba(195, 219, 255, 0.2) !important;
  1591. color: #2E64FA !important;
  1592. cursor: pointer !important;
  1593. }
  1594. }
  1595. }
  1596. }
  1597. // 单元格样式,设置内边距为0,确保行高由行高属性控制
  1598. .vxe-body--column,
  1599. .vxe-header--column {
  1600. padding: 0 !important;
  1601. height: 20px !important;
  1602. line-height: 20px !important;
  1603. min-height: 20px !important;
  1604. max-height: 20px !important;
  1605. }
  1606. // 表格主体最小高度设置
  1607. .vxe-table--body {
  1608. min-height: 0 !important;
  1609. }
  1610. /* 自定义树形图标样式 */
  1611. .vxe-tree-icon {
  1612. display: inline-flex;
  1613. align-items: center;
  1614. justify-content: center;
  1615. width: 14px;
  1616. height: 14px;
  1617. border-radius: 50%;
  1618. margin-right: 6px;
  1619. font-size: 12px;
  1620. font-weight: bold;
  1621. line-height: 1;
  1622. }
  1623. /* 展开状态的节点 */
  1624. .vxe-tree-icon--minus {
  1625. background-color: #409EFF;
  1626. color: white;
  1627. }
  1628. /* 可展开但未展开的节点 */
  1629. .vxe-tree-icon--plus {
  1630. background-color: #C0C4CC;
  1631. color: white;
  1632. }
  1633. // 斑马纹
  1634. .vxe-table--body {
  1635. tr:nth-child(even) {
  1636. background-color: #F5F7FA;
  1637. }
  1638. }
  1639. }
  1640. }
  1641. .knowledge_item {
  1642. display: flex;
  1643. align-items: center;
  1644. .knowledge_title {
  1645. font-size: 14px;
  1646. color: #606266;
  1647. }
  1648. }
  1649. .rate_info {
  1650. // display: flex;
  1651. align-items: center;
  1652. gap: 8px;
  1653. justify-content: center;
  1654. width: 80px;
  1655. margin: 0 auto;
  1656. position: relative;
  1657. }
  1658. .rate_dot {
  1659. display: inline-block;
  1660. width: 6px;
  1661. height: 6px;
  1662. border-radius: 50%;
  1663. position: absolute;
  1664. top: 8px;
  1665. left: 2px;
  1666. &.rate_excellent {
  1667. background-color: #3BA272;
  1668. }
  1669. &.rate_good {
  1670. background-color: #FAC858;
  1671. }
  1672. &.rate_weak {
  1673. background-color: #EE6666;
  1674. }
  1675. }
  1676. .rate_value {
  1677. font-size: 14px;
  1678. color: #606266;
  1679. }
  1680. .diff_value {
  1681. font-size: 14px;
  1682. color: #606266;
  1683. &.diff_negative {
  1684. color: #F56C6C;
  1685. }
  1686. }
  1687. }
  1688. }
  1689. </style>