ExamSituation.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. <!-- 考试情况统计 -->
  2. <template>
  3. <div class="exam-situation-card">
  4. <div class="card-header">
  5. <span class="card-title">考试情况统计</span>
  6. <div class="header-right">
  7. <el-select v-model="valueYear" @change="topChange()" size="small" style="width:220px">
  8. <el-option
  9. v-for="dict in optionList"
  10. :key="dict.value"
  11. :label="dict.label"
  12. :value="dict.value"
  13. />
  14. </el-select>
  15. <!-- 分组按钮 -->
  16. <div class="table-top-button-box">
  17. <div>
  18. <p @click="checkButton(2)" :class="checkIndex == 2?'check-table-button':''">研究生</p>
  19. <p @click="checkButton(3)" :class="checkIndex == 3?'check-table-button':''">本科生</p>
  20. <p @click="checkButton(1)" :class="checkIndex == 1?'check-table-button':''">教职工</p>
  21. </div>
  22. </div>
  23. <!-- 导出按钮 -->
  24. <p class="export-btn" @click="exportButton()">
  25. <i class="el-icon-download"></i> 导出数据
  26. </p>
  27. </div>
  28. </div>
  29. <!-- 图表区 -->
  30. <div class="chart-area">
  31. <!-- 堆叠柱状图 -->
  32. <p class="bar-chart-wrap" v-if="!chartData.deptStats[0]" style="color:#999;font-size:14px;line-height:260px;text-align: center;">暂无数据</p>
  33. <div class="bar-chart-wrap" v-if="chartData.deptStats[0]" id="bar-chart-wrap" ref="barChart"></div>
  34. <!-- 环形图 -->
  35. <p class="pie-chart-wrap" v-if="!chartData.deptStats[0]" style="color:#999;font-size:14px;line-height:260px;text-align: center;">暂无数据</p>
  36. <div class="pie-chart-wrap" v-if="chartData.deptStats[0]" id="pie-chart-wrap" ref="pieChart"></div>
  37. </div>
  38. <!-- 筛选 -->
  39. <div class="filter-row">
  40. <el-cascader
  41. style="width: 180px"
  42. clearable
  43. v-model="queryParams.deptId"
  44. :options="optionListA"
  45. :props="{
  46. emitPath:false,
  47. checkStrictly: true,
  48. value: 'deptId',
  49. label: 'deptName',
  50. children: 'child',
  51. }"
  52. :show-all-levels="false"
  53. filterable
  54. placeholder="筛选学院"
  55. @change="queryParamsChange"
  56. ></el-cascader>
  57. <el-select
  58. @change="queryParamsChange"
  59. v-model="queryParams.examTypeId" clearable placeholder="考试类型" style="width: 160px">
  60. <el-option
  61. v-for="dict in optionListB"
  62. :key="dict.id"
  63. :label="dict.examTypeName"
  64. :value="dict.id"
  65. />
  66. </el-select>
  67. </div>
  68. <!-- 统计表格 -->
  69. <div class="page-content-box">
  70. <el-table class="table-box" :data="dataList" border :span-method="objectSpanMethod">
  71. <el-table-column prop="deptName" label="学院" />
  72. <el-table-column prop="examTypeName" label="考试类型" width="200" />
  73. <el-table-column prop="examCount" label="应考人数" width="160" align="center" />
  74. <el-table-column prop="passedCount" label="通过人数" width="160" align="center" />
  75. <el-table-column prop="notPassedCount" label="未通过人数" width="160" align="center"/>
  76. <el-table-column prop="absentCount" label="未考人数" width="160" align="center" />
  77. <el-table-column prop="passRate" label="通过率" width="160" align="center" />
  78. </el-table>
  79. </div>
  80. </div>
  81. </template>
  82. <script>
  83. import echarts from 'echarts'
  84. import {
  85. examElStatisticsGetAcademicYear,examElStatisticsExamList,examElStatisticsExamChart,
  86. examElExamTypeList,systemDeptCurrentDept,
  87. } from "@/api/safetyEducationExaminationNew/index";
  88. export default {
  89. name: 'ExamSituation',
  90. data() {
  91. return {
  92. chartData: {
  93. barGroups: [
  94. { label: '化学与化工', pass: 106, fail: 8, notExam: 6 },
  95. { label: '生命科学', pass: 84, fail: 7, notExam: 4 },
  96. { label: '物理', pass: 67, fail: 6, notExam: 5 },
  97. { label: '电气工程', pass: 98, fail: 7, notExam: 5 },
  98. { label: '材料科学', pass: 78, fail: 5, notExam: 5 },
  99. { label: '机械工程', pass: 63, fail: 5, notExam: 4 },
  100. { label: '自动化', pass: 59, fail: 3, notExam: 3 },
  101. { label: '资源环境', pass: 49, fail: 4, notExam: 3 },
  102. ]
  103. },
  104. chartList:[],
  105. spanMap: {},
  106. optionList:[],
  107. optionListA:[],
  108. optionListB:[],
  109. valueYear:'',
  110. checkIndex:2,
  111. queryParams:{
  112. deptId:'',
  113. examTypeId:'',
  114. },
  115. dataList:[
  116. {
  117. deptName:'学院1',
  118. examType:'类型1',
  119. total:'1',
  120. pass:'2',
  121. fail:'3',
  122. notExam:'4',
  123. rate:'5',
  124. },
  125. {
  126. deptName:'学院12',
  127. examType:'类型1',
  128. total:'1',
  129. pass:'2',
  130. fail:'3',
  131. notExam:'4',
  132. rate:'5',
  133. },
  134. {
  135. deptName:'学院1',
  136. examType:'类型2',
  137. total:'1',
  138. pass:'2',
  139. fail:'3',
  140. notExam:'4',
  141. rate:'5',
  142. },
  143. ],
  144. }
  145. },
  146. created() {
  147. },
  148. mounted() {
  149. this.examElExamTypeList();
  150. this.systemDeptCurrentDept();
  151. this.examElStatisticsGetAcademicYear();
  152. },
  153. beforeDestroy() {
  154. if (this._barChart) this._barChart.dispose()
  155. if (this._pieChart) this._pieChart.dispose()
  156. },
  157. methods: {
  158. //导出
  159. exportButton(){
  160. let self = this;
  161. self.$confirm(`确认导出数据?`, "提示", {
  162. confirmButtonText: "确定",
  163. cancelButtonText: "取消",
  164. type: "warning"
  165. }).then(async () => {
  166. let obj = {
  167. academicYear:this.valueYear,
  168. personType:this.checkIndex,
  169. deptId:this.queryParams.deptId,
  170. examTypeId:this.queryParams.examTypeId,
  171. };
  172. self.download('/exam/elStatistics/exam/export', {...obj}, '考试情况统计.xlsx')
  173. }).catch(() => {})
  174. },
  175. //图表
  176. initBarChart() {
  177. const groups = this.chartData.deptStats
  178. const chart = echarts.init(this.$refs.barChart)
  179. this._barChart = chart
  180. chart.setOption({
  181. legend: {
  182. data: ['通过', '未通过', '未考'],
  183. top: 10,
  184. itemWidth: 14,
  185. itemHeight: 14,
  186. textStyle: { fontSize: 12, color: '#555' },
  187. },
  188. grid: { top: 60, bottom: 60, left: 40, right: 10, containLabel: false },
  189. xAxis: {
  190. type: 'category',
  191. data: groups.map(g => g.deptName),
  192. axisLine: { lineStyle: { color: '#ddd' } },
  193. axisLabel: { fontSize: 11, color: '#666', rotate: 25 },
  194. axisTick: { show: false },
  195. },
  196. yAxis: {
  197. type: 'value',
  198. splitLine: { lineStyle: { color: '#f0f0f0' } },
  199. axisLabel: { fontSize: 11, color: '#aaa' },
  200. axisLine: { show: false },
  201. axisTick: { show: false },
  202. },
  203. series: [
  204. {
  205. name: '通过',
  206. type: 'bar',
  207. stack: 'total',
  208. data: groups.map(g => g.passedCount),
  209. itemStyle: { color: '#4fbe7e' },
  210. label: { show: true, position: 'inside', fontSize: 11, color: '#fff',
  211. formatter: (p) => p.value > 0 ? p.value : '' },
  212. },
  213. {
  214. name: '未通过',
  215. type: 'bar',
  216. stack: 'total',
  217. data: groups.map(g => g.notPassedCount),
  218. itemStyle: { color: '#f56c6c' },
  219. label: { show: true, position: 'inside', fontSize: 10, color: '#fff',
  220. formatter: (p) => p.value > 0 ? p.value : '' },
  221. },
  222. {
  223. name: '未考',
  224. type: 'bar',
  225. stack: 'total',
  226. data: groups.map(g => g.absentCount),
  227. itemStyle: { color: '#c0c4cc' },
  228. label: { show: true, position: 'inside', fontSize: 10, color: '#fff',
  229. formatter: (p) => p.value > 0 ? p.value : '' },
  230. },
  231. ],
  232. tooltip: {
  233. trigger: 'axis',
  234. axisPointer: { type: 'shadow' },
  235. },
  236. })
  237. },
  238. //图表
  239. initPieChart() {
  240. const pass = this.chartData.totalPassed
  241. const fail = this.chartData.totalNotPassed
  242. const notExam = this.chartData.totalAbsent
  243. const chart = echarts.init(this.$refs.pieChart)
  244. this._pieChart = chart
  245. chart.setOption({
  246. legend: {
  247. orient: 'horizontal',
  248. bottom: 0,
  249. data: ['通过', '未通过', '未考'],
  250. itemWidth: 12,
  251. itemHeight: 12,
  252. textStyle: { fontSize: 12, color: '#555' },
  253. },
  254. series: [
  255. {
  256. type: 'pie',
  257. radius: ['45%', '68%'],
  258. center: ['50%', '45%'],
  259. label: {
  260. formatter: '{b}',
  261. fontSize: 12,
  262. color: '#555',
  263. },
  264. labelLine: { length: 12, length2: 8 },
  265. data: [
  266. { value: pass, name: '通过', itemStyle: { color: '#4fbe7e' } },
  267. { value: fail, name: '未通过', itemStyle: { color: '#f56c6c' } },
  268. { value: notExam, name: '未考', itemStyle: { color: '#c0c4cc' } },
  269. ],
  270. emphasis: {
  271. itemStyle: { shadowBlur: 6, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.2)' }
  272. },
  273. },
  274. ],
  275. tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
  276. })
  277. },
  278. //顶部学年切换
  279. topChange(){
  280. this.examElStatisticsExamChart();
  281. this.examElStatisticsExamList();
  282. },
  283. //身份切换
  284. checkButton(val){
  285. if(this.checkIndex!=val){
  286. this.$set(this,'checkIndex',val);
  287. this.examElStatisticsExamChart();
  288. this.examElStatisticsExamList();
  289. }
  290. },
  291. //条件变更
  292. queryParamsChange(){
  293. this.examElStatisticsExamList();
  294. },
  295. //数据合并
  296. objectSpanMethod({ row, column, rowIndex, columnIndex }) {
  297. // 只对第一列(学院列,columnIndex === 0)进行合并
  298. if (columnIndex === 0) {
  299. const list = this.dataList;
  300. // 如果是第一行,或者当前行的学院与上一行的学院不同,则开始合并计算
  301. if (rowIndex === 0 || row.deptName !== list[rowIndex - 1].deptName) {
  302. let rowspan = 1;
  303. // 向下遍历,统计有多少行是相同的学院
  304. for (let i = rowIndex + 1; i < list.length; i++) {
  305. if (list[i].deptName === row.deptName) {
  306. rowspan++;
  307. } else {
  308. break; // 遇到不同的学院,停止计算
  309. }
  310. }
  311. return { rowspan, colspan: 1 };
  312. } else {
  313. // 如果当前行的学院与上一行相同,则隐藏当前单元格(被合并了)
  314. return { rowspan: 0, colspan: 0 };
  315. }
  316. }
  317. // 其他列正常显示
  318. return { rowspan: 1, colspan: 1 };
  319. },
  320. //考试统计图表
  321. examElStatisticsExamChart(){
  322. let obj = {
  323. academicYear:this.valueYear,
  324. personType:this.checkIndex,
  325. }
  326. examElStatisticsExamChart(obj).then(response => {
  327. this.$set(this,'chartData',response.data);
  328. this.$nextTick(() => {
  329. if(response.data.deptStats[0]){
  330. this.$nextTick(() => {
  331. this.initBarChart()
  332. this.initPieChart()
  333. })
  334. }
  335. })
  336. });
  337. },
  338. //考试统计列表
  339. examElStatisticsExamList(){
  340. let obj = {
  341. academicYear:this.valueYear,
  342. personType:this.checkIndex,
  343. deptId:this.queryParams.deptId,
  344. examTypeId:this.queryParams.examTypeId,
  345. }
  346. examElStatisticsExamList(obj).then(response => {
  347. this.$set(this,'dataList',response.data);
  348. });
  349. },
  350. //查询全部考试类型
  351. examElExamTypeList(){
  352. examElExamTypeList({}).then(response => {
  353. this.$set(this,'optionListB',response.data);
  354. });
  355. },
  356. //学院列表
  357. systemDeptCurrentDept(){
  358. systemDeptCurrentDept({}).then(response => {
  359. const list = response.data || [];
  360. this.formatTreeData(list);
  361. this.$set(this,'optionListA',list);
  362. });
  363. },
  364. //学年列表
  365. examElStatisticsGetAcademicYear(){
  366. examElStatisticsGetAcademicYear({}).then(response => {
  367. this.$set(this,'optionList',response.data);
  368. this.$set(this,'valueYear',response.data[0].gradeNumber);
  369. this.examElStatisticsExamList();
  370. this.examElStatisticsExamChart();
  371. });
  372. },
  373. }
  374. }
  375. </script>
  376. <style scoped lang="scss">
  377. .exam-situation-card {
  378. background: #fff;
  379. /*border-radius: 6px;*/
  380. padding: 20px;
  381. margin-bottom: 12px;
  382. /*box-shadow: 0 1px 4px rgba(0,0,0,0.06);*/
  383. background: rgba(255, 255, 255, 0.95);
  384. border: 1px solid #e8edf5;
  385. border-radius: 16px;
  386. box-shadow: 0 4px 20px rgba(37, 50, 74, 0.06);
  387. .card-header {
  388. display: flex;
  389. justify-content: space-between;
  390. align-items: center;
  391. margin-bottom: 16px;
  392. .card-title { font-size: 16px; font-weight: 700; color: #222; }
  393. .header-right { width:610px;display: flex; align-items: center; gap: 20px; }
  394. }
  395. /* 分组筛选按钮 */
  396. .table-top-button-box {
  397. display: inline-block;
  398. vertical-align: top;
  399. div {
  400. display: flex;
  401. p {
  402. border: 1px solid #DCDFE6;
  403. width: 80px;
  404. text-align: center;
  405. height: 40px;
  406. line-height: 40px;
  407. cursor: pointer;
  408. font-size: 14px;
  409. &:nth-child(1) { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
  410. &:nth-child(2) { border-left: none; border-right: none; }
  411. &:nth-child(3) { border-top-right-radius: 4px; border-bottom-right-radius: 4px; }
  412. }
  413. .check-table-button{
  414. border: 1px solid #0183fa!important;
  415. background-color: #0183fa!important;;
  416. color:#fff!important;;
  417. }
  418. }
  419. }
  420. /* 导出按钮 */
  421. .export-btn {
  422. display: inline-block;
  423. padding: 0 16px;
  424. height: 40px;
  425. line-height: 40px;
  426. border-radius: 4px;
  427. font-size: 14px;
  428. cursor: pointer;
  429. background-color: #0183fa;
  430. color: #fff;
  431. border: 1px solid #0183fa;
  432. }
  433. .chart-area {
  434. display: flex;
  435. gap: 0;
  436. margin-bottom: 16px;
  437. border: 1px solid #eee;
  438. border-radius: 6px;
  439. overflow: hidden;
  440. .bar-chart-wrap {
  441. flex: 1;
  442. height: 280px;
  443. border-right: 1px solid #eee;
  444. }
  445. .pie-chart-wrap {
  446. width: 320px;
  447. height: 280px;
  448. }
  449. }
  450. .filter-row {
  451. display: flex;
  452. gap: 20px;
  453. margin-bottom: 12px;
  454. }
  455. .page-content-box{
  456. height:500px;
  457. padding:0;
  458. ::v-deep .el-table--medium td:nth-child(1){
  459. border-left:1px solid #dfe6ec!important;
  460. }
  461. }
  462. .link-blue { color: #409eff; }
  463. }
  464. </style>