ExamSituation.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  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-select v-model="valueYear" @change="topChange()" size="small" style="width:220px">
  41. <el-option
  42. v-for="dict in optionList"
  43. :key="dict.value"
  44. :label="dict.label"
  45. :value="dict.value"
  46. />
  47. </el-select>
  48. <el-select filterable
  49. @change="queryParamsChange"
  50. v-model="queryParams.examId" clearable placeholder="请输入或选择考试名称" style="width: 360px">
  51. <el-option
  52. v-for="dict in optionListB"
  53. :key="dict.examId"
  54. :label="dict.examName"
  55. :value="dict.examId"
  56. />
  57. </el-select>
  58. <!--<el-cascader-->
  59. <!--v-if="adminType == 1"-->
  60. <!--style="width: 180px"-->
  61. <!--clearable-->
  62. <!--v-model="queryParams.deptId"-->
  63. <!--:options="optionListA"-->
  64. <!--:props="{-->
  65. <!--emitPath:false,-->
  66. <!--checkStrictly: true,-->
  67. <!--value: 'deptId',-->
  68. <!--label: 'deptName',-->
  69. <!--children: 'child',-->
  70. <!--}"-->
  71. <!--:show-all-levels="false"-->
  72. <!--filterable-->
  73. <!--placeholder="筛选学院"-->
  74. <!--@change="queryParamsChange"-->
  75. <!--&gt;</el-cascader>-->
  76. <el-select v-if="adminType == 1"
  77. @change="queryParamsChange"
  78. clearable
  79. v-model="queryParams.deptId" placeholder="学院" style="width: 180px">
  80. <el-option
  81. v-for="dict in optionListA"
  82. :key="dict.deptId"
  83. :label="dict.deptName"
  84. :value="dict.deptId"
  85. />
  86. </el-select>
  87. <p style="line-height:40px;font-size:14px;color:#1768d5;" v-if="!queryParams.deptId">共组织常规考试 <span style="font-weight:700;">{{optionListB.length}}</span> 次</p>
  88. </div>
  89. <!-- 统计表格 -->
  90. <div class="page-content-box">
  91. <el-table class="table-box" :data="dataList" border :span-method="objectSpanMethod">
  92. <el-table-column prop="deptName" label="学院" />
  93. <!--<el-table-column prop="examTypeName" label="考试类型" width="200" />-->
  94. <el-table-column prop="expectedCount" label="应考人数" width="160" align="center" />
  95. <el-table-column prop="examCount" label="实考人数" width="160" align="center" />
  96. <el-table-column prop="passedCount" label="通过人数" width="160" align="center" />
  97. <el-table-column prop="notPassedCount" label="未通过人数" width="160" align="center"/>
  98. <el-table-column prop="absentCount" label="未考人数" width="160" align="center" />
  99. <el-table-column prop="passRate" label="通过率" width="160" align="center" />
  100. <el-table-column label="操作" width="200" show-overflow-tooltip>
  101. <template slot-scope="scope">
  102. <div class="table-button-box">
  103. <p class="table-button-null"></p>
  104. <p class="table-button-p"
  105. v-if="scope.row.type != 2"
  106. @click="tableButton(1,scope.row)"
  107. >人员明细</p>
  108. <p class="table-button-null"></p>
  109. </div>
  110. </template>
  111. </el-table-column>
  112. </el-table>
  113. <pagination :page-sizes="[20, 30, 40, 50]"
  114. v-show="total>0"
  115. :total="total"
  116. :page.sync="queryParams.page"
  117. :limit.sync="queryParams.pageSize"
  118. @pagination="examElStatisticsExamList"
  119. />
  120. </div>
  121. <el-drawer
  122. size="100%"
  123. custom-class="regularExam-index-drawer"
  124. :append-to-body="true"
  125. :title="drawerTitle"
  126. ref="drawer"
  127. :visible.sync="drawer"
  128. @close="drawerReturnButton"
  129. direction="rtl">
  130. <userListPage :drawerPropsData="drawerPropsData" v-if="drawerType == 1"></userListPage>
  131. </el-drawer>
  132. </div>
  133. </template>
  134. <script>
  135. import echarts from 'echarts'
  136. import {
  137. systemUserGetPost,
  138. } from "@/api/basicsModules/index";
  139. import {
  140. examElStatisticsGetAcademicYear,examElStatisticsExamList,examElStatisticsExamChart,
  141. examElStatisticsExamTitleList,systemDeptCurrentDept,
  142. } from "@/api/safetyEducationExaminationNew/index";
  143. import userListPage from "./userListPage.vue";
  144. export default {
  145. name: 'ExamSituation',
  146. components: {
  147. userListPage,
  148. },
  149. data() {
  150. return {
  151. adminType:0,
  152. chartData: {
  153. deptStats:[],
  154. barGroups: []
  155. },
  156. chartList:[],
  157. spanMap: {},
  158. optionList:[],
  159. optionListA:[],
  160. optionListB:[],
  161. valueYear:'',
  162. checkIndex:2,
  163. queryParams:{
  164. page:1,
  165. pageSize:20,
  166. deptId:'',
  167. examId:'',
  168. },
  169. dataList:[],
  170. total:0,
  171. //弹窗相关
  172. drawerTitle: '',
  173. drawerType: '',
  174. drawer: false,
  175. drawerPropsData:{},
  176. }
  177. },
  178. created() {
  179. },
  180. mounted() {
  181. this.systemUserGetPost();
  182. },
  183. beforeDestroy() {
  184. if (this._barChart) this._barChart.dispose()
  185. if (this._pieChart) this._pieChart.dispose()
  186. },
  187. methods: {
  188. //操作按钮
  189. tableButton(type,row){
  190. if(type == 1){
  191. this.$set(this, 'drawerTitle', '常规考试明细')
  192. let obj = {
  193. adminType:this.adminType,
  194. valueYear:this.valueYear,
  195. examId:this.queryParams.examId,
  196. personType:this.checkIndex,
  197. deptId:row.deptId,
  198. }
  199. this.$set(this, 'drawerPropsData', obj)
  200. this.$set(this, 'drawerType', 1)
  201. this.$set(this, 'drawer', true)
  202. }
  203. },
  204. //弹窗返回
  205. drawerReturnButton(){
  206. this.$set(this, 'drawerType', 0)
  207. this.$refs.drawer.closeDrawer();
  208. },
  209. //查询当前登录用户身份-校级/院级
  210. systemUserGetPost(){
  211. systemUserGetPost({}).then(response => {
  212. for(let i=0;i<response.data.length;i++){
  213. if(response.data[i].postCode == 'school_level_admin'){
  214. if(response.data[i].isPost){
  215. this.$set(this,'adminType','1');
  216. }
  217. }else if(response.data[i].postCode == 'college_admin'){
  218. if(response.data[i].isPost){
  219. this.$set(this,'adminType','2');
  220. }
  221. }
  222. }
  223. this.systemDeptCurrentDept();
  224. this.examElStatisticsGetAcademicYear();
  225. });
  226. },
  227. //导出
  228. exportButton(){
  229. let self = this;
  230. self.$confirm(`确认导出数据?`, "提示", {
  231. confirmButtonText: "确定",
  232. cancelButtonText: "取消",
  233. type: "warning"
  234. }).then(async () => {
  235. let obj = {
  236. academicYear:this.valueYear,
  237. personType:this.checkIndex,
  238. examId:this.queryParams.examId,
  239. };
  240. if(this.adminType == 1){
  241. obj.deptId = this.queryParams.deptId;
  242. }else{
  243. obj.deptId = localStorage.getItem('deptId');
  244. }
  245. self.download('/exam/elStatistics/exam/export', {...obj}, '考试情况统计.xlsx')
  246. }).catch(() => {})
  247. },
  248. //图表
  249. initBarChart() {
  250. const groups = this.chartData.deptStats
  251. const chart = echarts.init(this.$refs.barChart)
  252. this._barChart = chart
  253. const PAGE_SIZE = 10 // 单页最多显示的柱体数(堆叠组数)
  254. chart.setOption({
  255. legend: {
  256. data: ['通过', '未通过', '未考'],
  257. top: 10,
  258. itemWidth: 14,
  259. itemHeight: 14,
  260. textStyle: { fontSize: 12, color: '#555' },
  261. },
  262. grid: { top: 60, bottom: 70, left: 40, right: 10, containLabel: false },
  263. xAxis: {
  264. type: 'category',
  265. data: groups.map(g => g.deptName),
  266. axisLine: { lineStyle: { color: '#ddd' } },
  267. axisLabel: {
  268. fontSize: 11,
  269. color: '#666',
  270. interval: 0, // 滚动过程中每个类目都显示标签
  271. rotate: 25,
  272. },
  273. axisTick: { show: false },
  274. },
  275. yAxis: {
  276. type: 'value',
  277. splitLine: { lineStyle: { color: '#f0f0f0' } },
  278. axisLabel: { fontSize: 11, color: '#aaa' },
  279. axisLine: { show: false },
  280. axisTick: { show: false },
  281. },
  282. // 新增:横向滚动
  283. dataZoom: [
  284. {
  285. type: 'slider', // 底部滑块,用户可拖动/点击滚动
  286. xAxisIndex: 0,
  287. startValue: 0,
  288. endValue: Math.min(PAGE_SIZE - 1, groups.length - 1), // 初始窗口:6 个柱体
  289. minValueSpan: PAGE_SIZE - 1, // 可选:限制最小窗口也是 6 个
  290. height: 18,
  291. bottom: 10,
  292. borderColor: '#e0e0e0',
  293. fillerColor: 'rgba(79, 190, 126, 0.15)',
  294. handleStyle: { color: '#4fbe7e' },
  295. moveHandleStyle: { color: '#4fbe7e' },
  296. textStyle: { color: '#aaa', fontSize: 10 },
  297. brushSelect: false,
  298. },
  299. {
  300. type: 'inside', // 鼠标滚轮/触摸拖动也可滚动
  301. xAxisIndex: 0,
  302. startValue: 0,
  303. endValue: Math.min(PAGE_SIZE - 1, groups.length - 1),
  304. zoomLock: true, // 锁定缩放,只允许平移滚动
  305. },
  306. ],
  307. series: [
  308. {
  309. name: '通过',
  310. type: 'bar',
  311. stack: 'total',
  312. barMaxWidth: 60, // 新增:限制最大宽度,防止数据少时柱子过粗
  313. data: groups.map(g => g.passedCount),
  314. itemStyle: { color: '#4fbe7e' },
  315. label: { show: true, position: 'inside', fontSize: 12, color: '#fff',
  316. formatter: (p) => p.value > 0 ? p.value : '' },
  317. },
  318. {
  319. name: '未通过',
  320. type: 'bar',
  321. stack: 'total',
  322. barMaxWidth: 60,
  323. data: groups.map(g => g.notPassedCount),
  324. itemStyle: { color: '#f56c6c' },
  325. label: { show: true, position: 'inside', fontSize: 12, color: '#fff',
  326. formatter: (p) => p.value > 0 ? p.value : '' },
  327. },
  328. {
  329. name: '未考',
  330. type: 'bar',
  331. stack: 'total',
  332. barMaxWidth: 60,
  333. data: groups.map(g => g.absentCount),
  334. itemStyle: { color: '#c0c4cc' },
  335. label: { show: true, position: 'inside', fontSize: 12, color: '#fff',
  336. formatter: (p) => p.value > 0 ? p.value : '' },
  337. },
  338. ],
  339. tooltip: {
  340. trigger: 'axis',
  341. axisPointer: { type: 'shadow' },
  342. },
  343. })
  344. },
  345. //图表
  346. initPieChart() {
  347. const pass = this.chartData.totalPassed
  348. const fail = this.chartData.totalNotPassed
  349. const notExam = this.chartData.totalAbsent
  350. const chart = echarts.init(this.$refs.pieChart)
  351. this._pieChart = chart
  352. chart.setOption({
  353. legend: {
  354. orient: 'horizontal',
  355. bottom: 0,
  356. data: ['通过', '未通过', '未考'],
  357. itemWidth: 12,
  358. itemHeight: 12,
  359. textStyle: { fontSize: 12, color: '#555' },
  360. },
  361. series: [
  362. {
  363. type: 'pie',
  364. radius: ['45%', '68%'],
  365. center: ['50%', '45%'],
  366. label: {
  367. formatter: '{b}',
  368. fontSize: 12,
  369. color: '#555',
  370. },
  371. labelLine: { length: 12, length2: 8 },
  372. data: [
  373. { value: pass, name: '通过', itemStyle: { color: '#4fbe7e' } },
  374. { value: fail, name: '未通过', itemStyle: { color: '#f56c6c' } },
  375. { value: notExam, name: '未考', itemStyle: { color: '#c0c4cc' } },
  376. ],
  377. emphasis: {
  378. itemStyle: { shadowBlur: 6, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.2)' }
  379. },
  380. },
  381. ],
  382. tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
  383. })
  384. },
  385. //顶部学年切换
  386. topChange(){
  387. this.examElStatisticsExamTitleList();
  388. this.$set(this.queryParams,'examId','');
  389. this.$set(this.queryParams,'page',1);
  390. this.examElStatisticsExamChart();
  391. this.examElStatisticsExamList();
  392. },
  393. //身份切换
  394. checkButton(val){
  395. if(this.checkIndex!=val){
  396. this.$set(this,'checkIndex',val);
  397. this.$set(this.queryParams,'page',1);
  398. this.examElStatisticsExamChart();
  399. this.examElStatisticsExamList();
  400. }
  401. },
  402. //条件变更
  403. queryParamsChange(){
  404. this.$set(this.queryParams,'page',1);
  405. this.examElStatisticsExamChart();
  406. this.examElStatisticsExamList();
  407. },
  408. //数据合并
  409. objectSpanMethod({ row, column, rowIndex, columnIndex }) {
  410. // 只对第一列(学院列,columnIndex === 0)进行合并
  411. if (columnIndex === 0) {
  412. const list = this.dataList;
  413. // 如果是第一行,或者当前行的学院与上一行的学院不同,则开始合并计算
  414. if (rowIndex === 0 || row.deptName !== list[rowIndex - 1].deptName) {
  415. let rowspan = 1;
  416. // 向下遍历,统计有多少行是相同的学院
  417. for (let i = rowIndex + 1; i < list.length; i++) {
  418. if (list[i].deptName === row.deptName) {
  419. rowspan++;
  420. } else {
  421. break; // 遇到不同的学院,停止计算
  422. }
  423. }
  424. return { rowspan, colspan: 1 };
  425. } else {
  426. // 如果当前行的学院与上一行相同,则隐藏当前单元格(被合并了)
  427. return { rowspan: 0, colspan: 0 };
  428. }
  429. }
  430. // 其他列正常显示
  431. return { rowspan: 1, colspan: 1 };
  432. },
  433. //考试统计图表
  434. examElStatisticsExamChart(){
  435. let obj = {
  436. // academicYear:this.valueYear,
  437. personType:this.checkIndex,
  438. examId:this.queryParams.examId,
  439. }
  440. if(this.adminType == 1){
  441. obj.deptId = this.queryParams.deptId;
  442. }else if(this.adminType == 2){
  443. obj.deptId = localStorage.getItem('deptId');
  444. }
  445. examElStatisticsExamChart(obj).then(response => {
  446. this.$set(this,'chartData',response.data);
  447. this.$nextTick(() => {
  448. if(response.data.deptStats[0]){
  449. this.$nextTick(() => {
  450. this.initBarChart()
  451. this.initPieChart()
  452. })
  453. }
  454. })
  455. });
  456. },
  457. //考试统计列表
  458. examElStatisticsExamList(){
  459. let obj = {
  460. page:this.queryParams.page,
  461. pageSize:this.queryParams.pageSize,
  462. academicYear:this.valueYear,
  463. personType:this.checkIndex,
  464. examId:this.queryParams.examId,
  465. }
  466. if(this.adminType == 1){
  467. obj.deptId = this.queryParams.deptId;
  468. }else{
  469. obj.deptId = localStorage.getItem('deptId');
  470. }
  471. examElStatisticsExamList(obj).then(response => {
  472. this.$set(this,'dataList',response.data.records);
  473. this.$set(this,'total',response.data.total);
  474. });
  475. },
  476. //查询全部考试类型
  477. examElStatisticsExamTitleList(){
  478. let obj = {
  479. year:this.valueYear
  480. }
  481. if(this.adminType == 2){
  482. obj.deptId = localStorage.getItem('deptId');
  483. }
  484. examElStatisticsExamTitleList(obj).then(response => {
  485. this.$set(this,'optionListB',response.data);
  486. });
  487. },
  488. //学院列表
  489. systemDeptCurrentDept(){
  490. systemDeptCurrentDept({}).then(response => {
  491. const list = response.data || [];
  492. this.formatTreeData(list);
  493. if(list[0]){
  494. this.$set(this,'optionListA',list[0].child);
  495. }else{
  496. this.$set(this,'optionListA',[]);
  497. }
  498. });
  499. },
  500. //学年列表
  501. examElStatisticsGetAcademicYear(){
  502. examElStatisticsGetAcademicYear({}).then(response => {
  503. this.$set(this,'optionList',response.data);
  504. this.$set(this,'valueYear',response.data[0].value);
  505. this.examElStatisticsExamTitleList();
  506. this.examElStatisticsExamList();
  507. this.examElStatisticsExamChart();
  508. });
  509. },
  510. }
  511. }
  512. </script>
  513. <style scoped lang="scss">
  514. .exam-situation-card {
  515. background: #fff;
  516. /*border-radius: 6px;*/
  517. padding: 20px;
  518. margin-bottom: 12px;
  519. /*box-shadow: 0 1px 4px rgba(0,0,0,0.06);*/
  520. background: rgba(255, 255, 255, 0.95);
  521. border: 1px solid #e8edf5;
  522. border-radius: 16px;
  523. box-shadow: 0 4px 20px rgba(37, 50, 74, 0.06);
  524. .card-header {
  525. display: flex;
  526. justify-content: space-between;
  527. align-items: center;
  528. margin-bottom: 16px;
  529. .card-title { font-size: 16px; font-weight: 700; color: #222; }
  530. .header-right { width:370px;display: flex; align-items: center; gap: 20px; }
  531. }
  532. /* 分组筛选按钮 */
  533. .table-top-button-box {
  534. display: inline-block;
  535. vertical-align: top;
  536. div {
  537. display: flex;
  538. p {
  539. border: 1px solid #DCDFE6;
  540. width: 80px;
  541. text-align: center;
  542. height: 40px;
  543. line-height: 40px;
  544. cursor: pointer;
  545. font-size: 14px;
  546. &:nth-child(1) { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
  547. &:nth-child(2) { border-left: none; border-right: none; }
  548. &:nth-child(3) { border-top-right-radius: 4px; border-bottom-right-radius: 4px; }
  549. }
  550. .check-table-button{
  551. border: 1px solid #0183fa!important;
  552. background-color: #0183fa!important;;
  553. color:#fff!important;;
  554. }
  555. }
  556. }
  557. /* 导出按钮 */
  558. .export-btn {
  559. display: inline-block;
  560. padding: 0 16px;
  561. height: 40px;
  562. line-height: 40px;
  563. border-radius: 4px;
  564. font-size: 14px;
  565. cursor: pointer;
  566. background-color: #0183fa;
  567. color: #fff;
  568. border: 1px solid #0183fa;
  569. }
  570. .chart-area {
  571. display: flex;
  572. gap: 0;
  573. margin-bottom: 16px;
  574. border: 1px solid #eee;
  575. border-radius: 6px;
  576. overflow: hidden;
  577. .bar-chart-wrap {
  578. flex: 1;
  579. height: 280px;
  580. border-right: 1px solid #eee;
  581. }
  582. .pie-chart-wrap {
  583. width: 320px;
  584. height: 280px;
  585. }
  586. }
  587. .filter-row {
  588. display: flex;
  589. gap: 20px;
  590. margin-bottom: 12px;
  591. }
  592. .page-content-box{
  593. height:500px;
  594. padding:0;
  595. ::v-deep .el-table--medium td:nth-child(1){
  596. border-left:1px solid #dfe6ec!important;
  597. }
  598. }
  599. .link-blue { color: #409eff; }
  600. }
  601. </style>
  602. <style lang="scss">
  603. .regularExam-index-drawer{
  604. .el-drawer__body{
  605. padding:0;
  606. }
  607. .el-drawer__header{
  608. margin:0;
  609. }
  610. }
  611. </style>