zeroScoreKnowledge.vue 58 KB

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