Просмотр исходного кода

Merge branch 'xbnl_20260904(应知应会区分学生/教职工)' into SCHOOL-XiBeiNongLin

dedsudiyu 1 день назад
Родитель
Сommit
d03fb9f714

+ 9 - 0
src/api/basicsModules/index.js

@@ -222,3 +222,12 @@ export function iotCameraGetPreviewURLs(query) {
     params: query
   })
 }
+
+//查询用户身份-校级/院级/管理员/负责人
+export function systemUserGetPost(data) {
+  return request({
+    url: '/system/user/getPost',
+    method: 'post',
+    data: data
+  })
+}

+ 29 - 5
src/api/safetyEducationExaminationNew/index.js

@@ -835,7 +835,7 @@ export function examElStatisticsGetAcademicYear(data) {
     data: data
   })
 }
-//考试统计列表
+//考试统计列表-常规考试
 export function examElStatisticsExamList(data) {
   return request({
     url: '/exam/elStatistics/exam/list',
@@ -843,7 +843,7 @@ export function examElStatisticsExamList(data) {
     data: data
   })
 }
-//考试统计图表
+//考试统计图表-常规考试
 export function examElStatisticsExamChart(data) {
   return request({
     url: '/exam/elStatistics/exam/chart',
@@ -851,12 +851,36 @@ export function examElStatisticsExamChart(data) {
     data: data
   })
 }
+//考试统计列表-分类考试
+export function examElStatisticsExamGroupedList(data) {
+  return request({
+    url: '/exam/elStatistics/exam/grouped/list',
+    method: 'post',
+    data: data
+  })
+}
+//考试统计图表-分类考试
+export function examElStatisticsExamGroupedChart(data) {
+  return request({
+    url: '/exam/elStatistics/exam/grouped/chart',
+    method: 'post',
+    data: data
+  })
+}
+//考试标题列表
+export function examElStatisticsExamTitleList(data) {
+  return request({
+    url: '/exam/elStatistics/exam/titleList',
+    method: 'post',
+    data: data
+  })
+}
 //实验室与人员匹配统计图表数据
-export function examElStatisticsLabChart(query) {
+export function examElStatisticsLabChart(data) {
   return request({
     url: '/exam/elStatistics/lab/chart',
-    method: 'get',
-    params: query
+    method: 'post',
+    data: data
   })
 }
 //实验室与人员匹配统计列表

+ 218 - 37
src/views/safetyEducationExaminationNew/dataStatistics/LabPersonMatch.vue

@@ -4,6 +4,22 @@
     <div class="card-header">
       <span class="card-title">实验室与人员匹配</span>
       <div class="header-right">
+        <el-select v-model="valueYear" @change="topChange()" size="small" style="width:220px">
+          <el-option
+            v-for="dict in optionList"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+        <!-- 分组按钮 -->
+        <div class="table-top-button-box">
+          <div>
+            <p @click="checkButton(2)" :class="checkIndex == 2?'check-table-button':''">研究生</p>
+            <p @click="checkButton(3)" :class="checkIndex == 3?'check-table-button':''">本科生</p>
+            <!--<p @click="checkButton(1)" :class="checkIndex == 1?'check-table-button':''">教职工</p>-->
+          </div>
+        </div>
         <p class="export-btn export-btn--blue" @click="exportButton1()">
           <i class="el-icon-download"></i> 导出各学院数据
         </p>
@@ -21,39 +37,94 @@
 
     <!-- 筛选 -->
     <div class="filter-row">
-      <el-cascader
-        style="width: 180px"
-        clearable
-        v-model="queryParams.deptId"
-        :options="optionListA"
-        :props="{
-                    emitPath:false,
-                    checkStrictly: true,
-                    value: 'deptId',
-                    label: 'deptName',
-                    children: 'child',
-                  }"
-        :show-all-levels="false"
-        filterable
-        placeholder="筛选学院"
-        @change="examElStatisticsLabList"
-      ></el-cascader>
-      <el-select v-model="queryParams.levelId" clearable @change="examElStatisticsLabList" placeholder="实验室等级" style="width:200px;">
-        <el-option v-for="item in optionListB" :key="item.levelId" :label="item.titleName"
-                   :value="item.levelId"/>
+      <!--<el-cascader-->
+        <!--v-if="adminType == 1"-->
+        <!--style="width: 180px"-->
+        <!--clearable-->
+        <!--v-model="queryParams.deptId"-->
+        <!--:options="optionListA"-->
+        <!--:props="{-->
+                    <!--emitPath:false,-->
+                    <!--checkStrictly: true,-->
+                    <!--value: 'deptId',-->
+                    <!--label: 'deptName',-->
+                    <!--children: 'child',-->
+                  <!--}"-->
+        <!--:show-all-levels="false"-->
+        <!--filterable-->
+        <!--placeholder="筛选学院"-->
+        <!--@change="examElStatisticsLabList"-->
+      <!--&gt;</el-cascader>-->
+      <el-select v-if="adminType == 1" v-model="queryParams.deptId" clearable @change="checkButtonFunction" placeholder="学院" style="width: 180px">
+        <el-option
+          v-for="dict in optionListA"
+          :key="dict.deptId"
+          :label="dict.deptName"
+          :value="dict.deptId"
+        />
       </el-select>
+      <!--<el-select v-model="queryParams.levelId" clearable @change="checkButtonFunction" placeholder="实验室等级" style="width:200px;">-->
+        <!--<el-option v-for="item in optionListB" :key="item.levelId" :label="item.titleName"-->
+                   <!--:value="item.levelId"/>-->
+      <!--</el-select>-->
     </div>
 
     <!-- 表格 -->
     <div class="page-content-box">
       <el-table class="table-box" :data="tableData" border size="small">
-        <el-table-column prop="deptName" label="学院" min-width="140" />
-        <el-table-column prop="level1Count" label="Ⅰ级人数" width="200" align="center" />
-        <el-table-column prop="level2Count" label="Ⅱ级人数" width="200" align="center" />
-        <el-table-column prop="level3Count" label="Ⅲ级人数" width="200" align="center" />
-        <el-table-column prop="level4Count" label="Ⅳ级人数" width="200" align="center" />
-        <el-table-column prop="totalCount" label="总人数" width="200" align="center"/>
+        <el-table-column prop="deptName" label="学院" >>
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.deptName}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="level1Count" label="Ⅰ级人数" width="120" align="center" >>
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.level1Count}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="level2Count" label="Ⅱ级人数" width="120" align="center" >>
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.level2Count}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="level3Count" label="Ⅲ级人数" width="120" align="center" >>
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.level3Count}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="level4Count" label="Ⅳ级人数" width="120" align="center" >>
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.level4Count}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="noLabNeedCount" v-if="checkIndex == 2" label="无需进入实验室人数" width="200" align="center">>
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.noLabNeedCount}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="unboundCount" v-if="checkIndex == 2" label="未操作人数" width="200" align="center">>
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.unboundCount}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="unboundCount" v-if="checkIndex == 3" label="未绑定人数" width="200" align="center">>
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.unboundCount}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="totalCount" label="总人数" width="200" align="center">>
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.totalCount}}</span>
+          </template>
+        </el-table-column>
       </el-table>
+      <pagination :page-sizes="[20, 30, 40, 50]"
+                  v-show="total>0"
+                  :total="total"
+                  :page.sync="queryParams.page"
+                  :limit.sync="queryParams.pageSize"
+                  @pagination="examElStatisticsLabList"
+      />
     </div>
   </div>
 </template>
@@ -61,7 +132,10 @@
 <script>
 import echarts from 'echarts'
 import {
-  examElStatisticsLabChart,examElStatisticsLabList,
+  systemUserGetPost,
+} from "@/api/basicsModules/index";
+import {
+  examElStatisticsLabChart,examElStatisticsLabList,examElStatisticsGetAcademicYear,
   laboratoryLevelConfigGetLevelTitleList,systemDeptCurrentDept,
 } from "@/api/safetyEducationExaminationNew/index";
 
@@ -69,26 +143,66 @@ export default {
   name: 'LabPersonMatch',
   data() {
     return {
+      adminType:0,
+      optionList:[],
       optionListA:[],
       optionListB:[],
+      valueYear:'',
+      checkIndex:2,
       queryParams:{
+        page:1,
+        pageSize:20,
         deptId:'',
         levelId:'',
       },
       chartData: [],
       tableData: [],
+      total:0
     }
   },
   mounted() {
-    this.laboratoryLevelConfigGetLevelTitleList();
-    this.systemDeptCurrentDept();
-    this.examElStatisticsLabChart();
-    this.examElStatisticsLabList();
+    this.systemUserGetPost();
   },
   beforeDestroy() {
     if (this._barChart) this._barChart.dispose()
   },
   methods: {
+    //查询当前登录用户身份-校级/院级
+    systemUserGetPost(){
+      systemUserGetPost({}).then(response => {
+        for(let i=0;i<response.data.length;i++){
+          if(response.data[i].postCode == 'school_level_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','1');
+            }
+          }else if(response.data[i].postCode == 'college_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','2');
+            }
+          }
+        }
+        this.examElStatisticsGetAcademicYear();
+        this.laboratoryLevelConfigGetLevelTitleList();
+        this.systemDeptCurrentDept();
+      });
+    },
+    //顶部学年切换
+    topChange(){
+      this.examElStatisticsLabChart();
+      this.examElStatisticsLabList();
+    },
+    //身份切换
+    checkButton(val){
+      if(this.checkIndex!=val){
+        this.$set(this,'checkIndex',val);
+        this.examElStatisticsLabChart();
+        this.examElStatisticsLabList();
+      }
+    },
+    checkButtonFunction(){
+      this.examElStatisticsLabChart();
+      this.examElStatisticsLabList();
+    },
     //导出
     exportButton1(){
       let self = this;
@@ -100,9 +214,13 @@ export default {
         let obj = {
           academicYear:this.valueYear,
           personType:this.checkIndex,
-          deptId:this.queryParams.deptId,
           examTypeId:this.queryParams.examTypeId,
         };
+        if(this.adminType == 1){
+          obj.deptId = this.queryParams.deptId;
+        }else{
+          obj.deptId = localStorage.getItem('deptId');
+        }
         self.download('/exam/elStatistics/lab/export', {...obj}, '实验室与人员匹配学院数据.xlsx')
       }).catch(() => {})
     },
@@ -116,9 +234,13 @@ export default {
         let obj = {
           academicYear:this.valueYear,
           personType:this.checkIndex,
-          deptId:this.queryParams.deptId,
           examTypeId:this.queryParams.examTypeId,
         };
+        if(this.adminType == 1){
+          obj.deptId = this.queryParams.deptId;
+        }else{
+          obj.deptId = localStorage.getItem('deptId');
+        }
         self.download('/exam/elStatistics/lab/detail-export', {...obj}, '实验室与人员匹配实验室人员明细.xlsx')
       }).catch(() => {})
     },
@@ -177,7 +299,16 @@ export default {
     },
     //图表
     examElStatisticsLabChart() {
-      examElStatisticsLabChart({}).then(response => {
+      let obj = {
+        academicYear:this.valueYear,
+        personType:this.checkIndex,
+      };
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else{
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsLabChart(obj).then(response => {
         this.$set(this, 'chartData', response.data)
         if(response.data[0]){
           this.$nextTick(() => {
@@ -188,8 +319,19 @@ export default {
     },
     //列表
     examElStatisticsLabList() {
-      examElStatisticsLabList(this.queryParams).then(response => {
-        this.$set(this, 'tableData', response.data)
+      let obj = JSON.parse(JSON.stringify(this.queryParams))
+      obj.academicYear = this.valueYear;
+      obj.personType = this.checkIndex;
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else{
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsLabList(obj).then(response => {
+        response.data.schoolSummary.type = 2
+        let list = response.data.records.concat([response.data.schoolSummary])
+        this.$set(this, 'tableData', list)
+        this.$set(this,'total',response.data.total);
       })
     },
     //查询风险等级
@@ -203,7 +345,20 @@ export default {
       systemDeptCurrentDept({}).then(response => {
         const list = response.data || [];
         this.formatTreeData(list);
-        this.$set(this,'optionListA',list);
+        if(list[0]){
+          this.$set(this,'optionListA',list[0].child);
+        }else{
+          this.$set(this,'optionListA',[]);
+        }
+      });
+    },
+    //学年列表
+    examElStatisticsGetAcademicYear(){
+      examElStatisticsGetAcademicYear({}).then(response => {
+        this.$set(this,'optionList',response.data);
+        this.$set(this,'valueYear',response.data[0].value);
+        this.examElStatisticsLabChart();
+        this.examElStatisticsLabList();
       });
     },
   }
@@ -231,6 +386,32 @@ export default {
     .header-right { display: flex; gap: 20px; }
   }
 
+  /* 分组筛选按钮 */
+  .table-top-button-box {
+    display: inline-block;
+    vertical-align: top;
+    div {
+      display: flex;
+      p {
+        border: 1px solid #DCDFE6;
+        width: 80px;
+        text-align: center;
+        height: 40px;
+        line-height: 40px;
+        cursor: pointer;
+        font-size: 14px;
+        &:nth-child(1) { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
+        /*&:nth-child(2) { border-left: none; border-right: none; }*/
+        &:nth-child(2) { border-top-right-radius: 4px; border-bottom-right-radius: 4px; }
+        &:nth-child(3) { border-top-right-radius: 4px; border-bottom-right-radius: 4px; }
+      }
+      .check-table-button{
+        border: 1px solid #0183fa!important;
+        background-color: #0183fa!important;;
+        color:#fff!important;;
+      }
+    }
+  }
   .export-btn {
     display: inline-block;
     padding: 0 16px;

+ 704 - 0
src/views/safetyEducationExaminationNew/dataStatistics/components/dataReport/ExamSituation.vue

@@ -0,0 +1,704 @@
+<!-- 考试情况统计 -->
+<template>
+  <div class="exam-situation-card">
+    <div class="card-header">
+      <span class="card-title">考试情况统计</span>
+      <div class="header-right">
+        <el-select v-model="valueYear" @change="topChange()" size="small" style="width:220px">
+          <el-option
+            v-for="dict in optionList"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+        <!-- 分组按钮 -->
+        <div class="table-top-button-box">
+          <div>
+            <p @click="checkButton(2)" :class="checkIndex == 2?'check-table-button':''">研究生</p>
+            <p @click="checkButton(3)" :class="checkIndex == 3?'check-table-button':''">本科生</p>
+            <p @click="checkButton(1)" :class="checkIndex == 1?'check-table-button':''">教职工</p>
+          </div>
+        </div>
+        <!-- 导出按钮 -->
+        <p class="export-btn" @click="exportButton()">
+          <i class="el-icon-download"></i> 导出数据
+        </p>
+      </div>
+    </div>
+
+    <!-- 图表区 -->
+    <div class="chart-area">
+      <!-- 堆叠柱状图 -->
+      <p class="bar-chart-wrap" v-if="!chartData.deptStats[0]" style="color:#999;font-size:14px;line-height:260px;text-align: center;">暂无数据</p>
+      <div class="bar-chart-wrap" v-if="chartData.deptStats[0]" id="bar-chart-wrap" ref="barChart"></div>
+      <!-- 环形图 -->
+      <p class="pie-chart-wrap" v-if="!chartData.deptStats[0]" style="color:#999;font-size:14px;line-height:260px;text-align: center;">暂无数据</p>
+      <div class="pie-chart-wrap" v-if="chartData.deptStats[0]" id="pie-chart-wrap" ref="pieChart"></div>
+    </div>
+
+    <!-- 筛选 -->
+    <div class="filter-row">
+      <!--<el-cascader-->
+        <!--v-if="adminType == 1"-->
+        <!--style="width: 180px"-->
+        <!--clearable-->
+        <!--v-model="queryParams.deptId"-->
+        <!--:options="optionListA"-->
+        <!--:props="{-->
+                    <!--emitPath:false,-->
+                    <!--checkStrictly: true,-->
+                    <!--value: 'deptId',-->
+                    <!--label: 'deptName',-->
+                    <!--children: 'child',-->
+                  <!--}"-->
+        <!--:show-all-levels="false"-->
+        <!--filterable-->
+        <!--placeholder="筛选学院"-->
+        <!--@change="queryParamsChange"-->
+      <!--&gt;</el-cascader>-->
+      <el-select v-if="adminType == 1"
+                 @change="queryParamsChange"
+                 clearable v-model="queryParams.deptId" placeholder="学院" style="width: 180px">
+        <el-option
+          v-for="dict in optionListA"
+          :key="dict.deptId"
+          :label="dict.deptName"
+          :value="dict.deptId"
+        />
+      </el-select>
+      <el-select
+        @change="queryParamsChange"
+        v-model="queryParams.examTypeId" clearable placeholder="考试类型" style="width: 160px">
+        <el-option
+          v-for="dict in optionListB"
+          :key="dict.id"
+          :label="dict.examTypeName"
+          :value="dict.id"
+        />
+      </el-select>
+    </div>
+
+    <!-- 统计表格 -->
+
+    <div class="page-content-box">
+      <el-table class="table-box" :data="dataList" border :span-method="objectSpanMethod">
+        <el-table-column prop="deptName" label="学院">
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 3?'color:#184c93;font-weight:700;':''">{{scope.row.deptName}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="examTypeName" label="考试类型" width="200">
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.examTypeName}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="expectedCount" label="应考人数" width="160" align="center">
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.expectedCount}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="examCount" label="实考人数" width="160" align="center">
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.examCount}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="passedCount" label="通过人数" width="160" align="center">
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.passedCount}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="notPassedCount" label="未通过人数" width="160" align="center">
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.notPassedCount}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="absentCount" label="未考人数" width="160" align="center">
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.absentCount}}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="passRate" label="通过率" width="160" align="center">
+          <template slot-scope="scope">
+            <span :style="scope.row.type == 2?'color:#184c93;font-weight:700;':''">{{scope.row.passRate}}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+      <pagination :page-sizes="[20, 30, 40, 50]"
+                  v-show="total>0"
+                  :total="total"
+                  :page.sync="queryParams.page"
+                  :limit.sync="queryParams.pageSize"
+                  @pagination="examElStatisticsExamGroupedList"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import echarts from 'echarts'
+import {
+  systemUserGetPost,
+} from "@/api/basicsModules/index";
+import {
+  examElStatisticsGetAcademicYear,examElStatisticsExamGroupedList,examElStatisticsExamGroupedChart,
+  examElExamTypeList,systemDeptCurrentDept,
+} from "@/api/safetyEducationExaminationNew/index";
+export default {
+  name: 'ExamSituation',
+  data() {
+    return {
+      adminType:0,
+      chartData: {
+        deptStats:[],
+        barGroups: []
+      },
+      chartList:[],
+      spanMap: {},
+      optionList:[],
+      optionListA:[],
+      optionListB:[],
+      valueYear:'',
+      checkIndex:2,
+      queryParams:{
+        page:1,
+        pageSize:20,
+        deptId:'',
+        examTypeId:'',
+      },
+      dataList:[],
+      total:0,
+    }
+  },
+  created() {
+
+  },
+  mounted() {
+    this.systemUserGetPost();
+  },
+  beforeDestroy() {
+    if (this._barChart) this._barChart.dispose()
+    if (this._pieChart) this._pieChart.dispose()
+  },
+  methods: {
+    //查询当前登录用户身份-校级/院级
+    systemUserGetPost(){
+      systemUserGetPost({}).then(response => {
+        for(let i=0;i<response.data.length;i++){
+          if(response.data[i].postCode == 'school_level_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','1');
+            }
+          }else if(response.data[i].postCode == 'college_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','2');
+            }
+          }
+        }
+        this.examElExamTypeList();
+        this.systemDeptCurrentDept();
+        this.examElStatisticsGetAcademicYear();
+      });
+    },
+    //导出
+    exportButton(){
+      let self = this;
+      self.$confirm(`确认导出数据?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(async () => {
+        let obj = {
+          academicYear:this.valueYear,
+          personType:this.checkIndex,
+          examTypeId:this.queryParams.examTypeId,
+        };
+        if(this.adminType == 1){
+          obj.deptId = this.queryParams.deptId;
+        }else{
+          obj.deptId = localStorage.getItem('deptId');
+        }
+        self.download('/exam/elStatistics/exam/export', {...obj}, '考试情况统计.xlsx')
+      }).catch(() => {})
+    },
+    //图表
+    initBarChart() {
+      const groups = this.chartData.deptStats
+      const chart = echarts.init(this.$refs.barChart)
+      this._barChart = chart
+
+      const PAGE_SIZE = 6 // 每屏最多 6 个学院
+
+      // ① 展平数据,并在每个学院的分组之间插入一个空槽位(形成组间距)
+      const categories = []
+      groups.forEach((g, gi) => {
+        (g.examTypeGroup || []).forEach(t => {
+          categories.push({
+            deptName: g.deptName,
+            examTypeName: t.examTypeName,
+            passedCount: t.passedCount,
+            notPassedCount: t.notPassedCount,
+            absentCount: t.absentCount,
+            isGap: false,
+          })
+        })
+        // 最后一个学院后面不再插空槽
+        if (gi < groups.length - 1) {
+          categories.push({ isGap: true })
+        }
+      })
+
+      // 每个学院下的考试类型数量(不写死 3)
+      const examTypeCount = groups.length
+        ? Math.max(...groups.map(g => (g.examTypeGroup || []).length))
+        : 1
+
+      // ② 每屏窗口:6 个学院 × (考试类型数 + 1 个空槽) - 尾部空槽
+      const pageSpan = Math.min(PAGE_SIZE * (examTypeCount + 1) - 2, categories.length - 1)
+
+      // ③ 第二根轴:学院名放在每组中间位置
+      const middle = Math.floor(examTypeCount / 2)
+      const groupAxisData = categories.map((c, i) => {
+        if (c.isGap) return ''
+        const posInGroup = i % (examTypeCount + 1)
+        return posInGroup === middle ? c.deptName : ''
+      })
+
+      chart.setOption({
+        legend: {
+          data: ['通过', '未通过', '未考'],
+          top: 10,
+          itemWidth: 14,
+          itemHeight: 14,
+          textStyle: { fontSize: 12, color: '#555' },
+        },
+        grid: { top: 60, bottom: 90, left: 40, right: 10, containLabel: false },
+        xAxis: [
+          {
+            // 柱子定位用,不显示标签
+            type: 'category',
+            data: categories.map(c => c.examTypeName || ''),
+            axisLine: { show: false },
+            axisLabel: { show: false },
+            axisTick: { show: false },
+          },
+          {
+            // 只显示学院名
+            type: 'category',
+            data: groupAxisData,
+            position: 'bottom',
+            offset: 8,
+            axisLine: { lineStyle: { color: '#ddd' } },
+            axisTick: { show: false },
+            axisLabel: {
+              fontSize: 11,
+              color: '#666',
+              interval: 0,
+              rotate: 25,
+              fontWeight: 600,
+            },
+          },
+        ],
+        yAxis: {
+          type: 'value',
+          splitLine: { lineStyle: { color: '#f0f0f0' } },
+          axisLabel: { fontSize: 11, color: '#aaa' },
+          axisLine: { show: false },
+          axisTick: { show: false },
+        },
+        dataZoom: [
+          {
+            type: 'slider',
+            xAxisIndex: [0, 1],
+            startValue: 0,
+            endValue: pageSpan,
+            height: 18,
+            bottom: 8,
+            borderColor: '#e0e0e0',
+            fillerColor: 'rgba(79, 190, 126, 0.15)',
+            handleStyle: { color: '#4fbe7e' },
+            moveHandleStyle: { color: '#4fbe7e' },
+            textStyle: { color: '#aaa', fontSize: 10 },
+            brushSelect: false,
+          },
+          {
+            type: 'inside',
+            xAxisIndex: [0, 1],
+            startValue: 0,
+            endValue: pageSpan,
+            zoomLock: true,
+          },
+        ],
+        series: [
+          {
+            name: '通过',
+            type: 'bar',
+            stack: 'total',
+            xAxisIndex: 0,
+            barMaxWidth: 40,
+            data: categories.map(c => (c.isGap ? null : c.passedCount)),
+            itemStyle: { color: '#4fbe7e' },
+            label: { show: true, position: 'inside', fontSize: 11, color: '#fff',
+              formatter: (p) => p.value > 0 ? p.value : '' },
+          },
+          {
+            name: '未通过',
+            type: 'bar',
+            stack: 'total',
+            xAxisIndex: 0,
+            barMaxWidth: 40,
+            data: categories.map(c => (c.isGap ? null : c.notPassedCount)),
+            itemStyle: { color: '#f56c6c' },
+            label: { show: true, position: 'inside', fontSize: 10, color: '#fff',
+              formatter: (p) => p.value > 0 ? p.value : '' },
+          },
+          {
+            name: '未考',
+            type: 'bar',
+            stack: 'total',
+            xAxisIndex: 0,
+            barMaxWidth: 40,
+            data: categories.map(c => (c.isGap ? null : c.absentCount)),
+            itemStyle: { color: '#c0c4cc' },
+            label: { show: true, position: 'inside', fontSize: 10, color: '#fff',
+              formatter: (p) => p.value > 0 ? p.value : '' },
+          },
+        ],
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' },
+          formatter: (params) => {
+            if (!params || !params.length) return ''
+            const c = categories[params[0].dataIndex]
+            if (!c || c.isGap) return '' // 空槽位不弹提示
+            // let html = `<div style="font-weight:600;margin-bottom:4px">${c.deptName} · ${c.examTypeName}</div>`
+            let html = `<div style="font-weight:600;margin-bottom:4px">${c.examTypeName}</div>`
+            params.forEach(p => {
+              html += `<div>${p.marker}${p.seriesName}:<b>${p.value}</b></div>`
+            })
+            return html
+          },
+        },
+      })
+    },
+    //图表
+    initPieChart() {
+      const pass    = this.chartData.totalPassed
+      const fail    = this.chartData.totalNotPassed
+      const notExam = this.chartData.totalAbsent
+      const chart = echarts.init(this.$refs.pieChart)
+      this._pieChart = chart
+      chart.setOption({
+        legend: {
+          orient: 'horizontal',
+          bottom: 0,
+          data: ['通过', '未通过', '未考'],
+          itemWidth: 12,
+          itemHeight: 12,
+          textStyle: { fontSize: 12, color: '#555' },
+        },
+        series: [
+          {
+            type: 'pie',
+            radius: ['45%', '68%'],
+            center: ['50%', '45%'],
+            label: {
+              formatter: '{b}',
+              fontSize: 12,
+              color: '#555',
+            },
+            labelLine: { length: 12, length2: 8 },
+            data: [
+              { value: pass,    name: '通过',  itemStyle: { color: '#4fbe7e' } },
+              { value: fail,    name: '未通过', itemStyle: { color: '#f56c6c' } },
+              { value: notExam, name: '未考',   itemStyle: { color: '#c0c4cc' } },
+            ],
+            emphasis: {
+              itemStyle: { shadowBlur: 6, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.2)' }
+            },
+          },
+        ],
+        tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
+      })
+    },
+    //顶部学年切换
+    topChange(){
+      this.$set(this.queryParams,'page',1);
+      this.examElStatisticsExamGroupedChart();
+      this.examElStatisticsExamGroupedList();
+    },
+    //身份切换
+    checkButton(val){
+      if(this.checkIndex!=val){
+        this.$set(this,'checkIndex',val);
+        this.$set(this.queryParams,'page',1);
+        this.examElStatisticsExamGroupedChart();
+        this.examElStatisticsExamGroupedList();
+      }
+    },
+    //条件变更
+    queryParamsChange(){
+      this.$set(this.queryParams,'page',1);
+      this.examElStatisticsExamGroupedChart();
+      this.examElStatisticsExamGroupedList();
+    },
+    //数据合并
+    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
+      // 只对第一列(学院列,columnIndex === 0)进行合并
+      if (columnIndex === 0) {
+        const list = this.dataList;
+
+        // 如果是第一行,或者当前行的学院与上一行的学院不同,则开始合并计算
+        if (rowIndex === 0 || row.deptName !== list[rowIndex - 1].deptName) {
+          let rowspan = 1;
+          // 向下遍历,统计有多少行是相同的学院
+          for (let i = rowIndex + 1; i < list.length; i++) {
+            if (list[i].deptName === row.deptName) {
+              rowspan++;
+            } else {
+              break; // 遇到不同的学院,停止计算
+            }
+          }
+          return { rowspan, colspan: 1 };
+        } else {
+          // 如果当前行的学院与上一行相同,则隐藏当前单元格(被合并了)
+          return { rowspan: 0, colspan: 0 };
+        }
+      }
+      // 其他列正常显示
+      return { rowspan: 1, colspan: 1 };
+    },
+    //考试统计图表
+    examElStatisticsExamGroupedChart(){
+      let obj = {
+        academicYear:this.valueYear,
+        personType:this.checkIndex,
+      }
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else if(this.adminType == 2){
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsExamGroupedChart(obj).then(response => {
+        this.$set(this,'chartData',response.data);
+        this.$nextTick(() => {
+          if(response.data.deptStats[0]){
+            this.$nextTick(() => {
+              this.initBarChart()
+              this.initPieChart()
+            })
+          }
+        })
+      });
+    },
+    //考试统计列表
+    examElStatisticsExamGroupedList(){
+      let obj = {
+        page:this.queryParams.page,
+        pageSize:this.queryParams.pageSize,
+        academicYear:this.valueYear,
+        personType:this.checkIndex,
+        examTypeId:this.queryParams.examTypeId,
+      }
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else{
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsExamGroupedList(obj).then(response => {
+        // this.$set(this,'dataList',response.data.records);
+        let list = [];
+        for(let i=0;i<response.data.deptStats.length;i++){
+          for(let o=0;o<response.data.deptStats[i].examTypeGroup.length;o++){
+            list.push({
+              deptName:response.data.deptStats[i].deptName,
+              examTypeName:response.data.deptStats[i].examTypeGroup[o].examTypeName,
+              expectedCount:response.data.deptStats[i].examTypeGroup[o].expectedCount,
+              examCount:response.data.deptStats[i].examTypeGroup[o].examCount,
+              passedCount:response.data.deptStats[i].examTypeGroup[o].passedCount,
+              notPassedCount:response.data.deptStats[i].examTypeGroup[o].notPassedCount,
+              absentCount:response.data.deptStats[i].examTypeGroup[o].absentCount,
+              passRate:response.data.deptStats[i].examTypeGroup[o].passRate,
+              type:1,
+            })
+          }
+          list.push({
+            deptName:response.data.deptStats[i].deptName,
+            examTypeName:'汇总',
+            expectedCount:response.data.deptStats[i].summary.expectedCount,
+            examCount:response.data.deptStats[i].summary.examCount,
+            passedCount:response.data.deptStats[i].summary.passedCount,
+            notPassedCount:response.data.deptStats[i].summary.notPassedCount,
+            absentCount:response.data.deptStats[i].summary.absentCount,
+            passRate:response.data.deptStats[i].summary.passRate,
+            type:2,
+          })
+        }
+
+        for(let o=0;o<response.data.schoolStats.examTypeGroup.length;o++){
+          list.push({
+            deptName:response.data.schoolStats.deptName,
+            examTypeName:response.data.schoolStats.examTypeGroup[o].examTypeName,
+            expectedCount:response.data.schoolStats.examTypeGroup[o].expectedCount,
+            examCount:response.data.schoolStats.examTypeGroup[o].examCount,
+            passedCount:response.data.schoolStats.examTypeGroup[o].passedCount,
+            notPassedCount:response.data.schoolStats.examTypeGroup[o].notPassedCount,
+            absentCount:response.data.schoolStats.examTypeGroup[o].absentCount,
+            passRate:response.data.schoolStats.examTypeGroup[o].passRate,
+            type:3,
+          })
+        }
+        list.push({
+          deptName:response.data.schoolStats.deptName,
+          examTypeName:'汇总',
+          expectedCount:response.data.schoolStats.summary.expectedCount,
+          examCount:response.data.schoolStats.summary.examCount,
+          passedCount:response.data.schoolStats.summary.passedCount,
+          notPassedCount:response.data.schoolStats.summary.notPassedCount,
+          absentCount:response.data.schoolStats.summary.absentCount,
+          passRate:response.data.schoolStats.summary.passRate,
+          type:2,
+        })
+        this.$set(this,'dataList',list);
+        this.$set(this,'total',response.data.total);
+      });
+    },
+    //查询全部考试类型
+    examElExamTypeList(){
+      examElExamTypeList({}).then(response => {
+        let list = [];
+        for(let i=0;i<response.data.length;i++){
+          if(response.data[i].examTypeCode == 'CLASSIFIED' || response.data[i].examTypeCode == 'yzyh' || response.data[i].examTypeCode == 'STUDENT'){
+            list.push(response.data[i]);
+          }
+        }
+        this.$set(this,'optionListB',list);
+      });
+    },
+    //学院列表
+    systemDeptCurrentDept(){
+      systemDeptCurrentDept({}).then(response => {
+        const list = response.data || [];
+        this.formatTreeData(list);
+        if(list[0]){
+          this.$set(this,'optionListA',list[0].child);
+        }else{
+          this.$set(this,'optionListA',[]);
+        }
+      });
+    },
+    //学年列表
+    examElStatisticsGetAcademicYear(){
+      examElStatisticsGetAcademicYear({}).then(response => {
+        this.$set(this,'optionList',response.data);
+        this.$set(this,'valueYear',response.data[0].value);
+        this.examElStatisticsExamGroupedList();
+        this.examElStatisticsExamGroupedChart();
+      });
+    },
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.exam-situation-card {
+  background: #fff;
+  /*border-radius: 6px;*/
+  padding: 20px;
+  margin-bottom: 12px;
+  /*box-shadow: 0 1px 4px rgba(0,0,0,0.06);*/
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid #e8edf5;
+  border-radius: 16px;
+  box-shadow: 0 4px 20px rgba(37, 50, 74, 0.06);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    .card-title { font-size: 16px; font-weight: 700; color: #222; }
+    .header-right { width:610px;display: flex; align-items: center; gap: 20px; }
+  }
+
+  /* 分组筛选按钮 */
+  .table-top-button-box {
+    display: inline-block;
+    vertical-align: top;
+    div {
+      display: flex;
+      p {
+        border: 1px solid #DCDFE6;
+        width: 80px;
+        text-align: center;
+        height: 40px;
+        line-height: 40px;
+        cursor: pointer;
+        font-size: 14px;
+        &:nth-child(1) { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
+        &:nth-child(2) { border-left: none; border-right: none; }
+        &:nth-child(3) { border-top-right-radius: 4px; border-bottom-right-radius: 4px; }
+      }
+      .check-table-button{
+        border: 1px solid #0183fa!important;
+        background-color: #0183fa!important;;
+        color:#fff!important;;
+      }
+    }
+  }
+
+  /* 导出按钮 */
+  .export-btn {
+    display: inline-block;
+    padding: 0 16px;
+    height: 40px;
+    line-height: 40px;
+    border-radius: 4px;
+    font-size: 14px;
+    cursor: pointer;
+    background-color: #0183fa;
+    color: #fff;
+    border: 1px solid #0183fa;
+  }
+
+  .chart-area {
+    display: flex;
+    gap: 0;
+    margin-bottom: 16px;
+    border: 1px solid #eee;
+    border-radius: 6px;
+    overflow: hidden;
+
+    .bar-chart-wrap {
+      flex: 1;
+      height: 280px;
+      border-right: 1px solid #eee;
+    }
+
+    .pie-chart-wrap {
+      width: 320px;
+      height: 280px;
+    }
+  }
+
+  .filter-row {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 12px;
+  }
+  .page-content-box{
+    height:500px;
+    padding:0;
+    ::v-deep .el-table--medium td:nth-child(1){
+      border-left:1px solid #dfe6ec!important;
+    }
+    ::v-deep .el-table__row:nth-child(4n){
+      /*border-left:none!important;*/
+      /*color:#184c93;*/
+      /*background-color: #edf5ff;*/
+    }
+  }
+
+  .link-blue { color: #409eff; }
+}
+</style>

+ 388 - 0
src/views/safetyEducationExaminationNew/dataStatistics/components/dataReport/StudyHourStats.vue

@@ -0,0 +1,388 @@
+<!-- 学时统计 -->
+<template>
+  <div class="study-hour-card">
+    <div class="card-header">
+      <span class="card-title">学时统计</span>
+      <p class="export-btn" @click="exportButton()">
+        <i class="el-icon-download"></i> 导出数据
+      </p>
+    </div>
+
+    <!-- 分组柱状图 -->
+    <p class="chart-wrap" v-if="!chartData[0]" style="color:#999;font-size:14px;line-height:260px;text-align: center;">暂无数据</p>
+    <div class="chart-wrap" v-if="chartData[0]" id="chart-wrap-bar-chart" ref="barChart"></div>
+
+    <!-- 筛选 -->
+    <div class="filter-row">
+      <el-select v-model="queryParams.personType" placeholder="用户类型" style="width:220px">
+        <el-option label="研究生" value="2" />
+        <el-option label="本科生" value="3" />
+        <el-option label="教职工" value="1" />
+      </el-select>
+      <!--<el-cascader-->
+        <!--v-if="adminType == 1"-->
+        <!--style="width: 180px"-->
+        <!--v-model="queryParams.deptId"-->
+        <!--:options="optionListA"-->
+        <!--:props="{-->
+                  <!--emitPath:false,-->
+                  <!--checkStrictly: true,-->
+                  <!--value: 'deptId',-->
+                  <!--label: 'deptName',-->
+                  <!--children: 'child',-->
+                <!--}"-->
+        <!--:show-all-levels="false"-->
+        <!--filterable-->
+        <!--placeholder="学院"-->
+      <!--&gt;</el-cascader>-->
+      <el-select v-if="adminType == 1" v-model="queryParams.deptId" placeholder="学院" style="width: 180px">
+        <el-option
+          v-for="dict in optionListA"
+          :key="dict.deptId"
+          :label="dict.deptName"
+          :value="dict.deptId"
+        />
+      </el-select>
+      <el-select v-model="queryParams.gradeNumber" placeholder="学年" style="width: 140px">
+        <el-option
+          v-for="dict in optionListB"
+          :key="dict.gradeNumber"
+          :label="dict.gradeNumber"
+          :value="dict.gradeNumber"
+        />
+      </el-select>
+      <el-input v-model="queryParams.searchValue" placeholder="学号/姓名" size="small" prefix-icon="el-icon-search" style="width:210px" clearable />
+      <p class="page-inquire-common-style-button" @click="handleQuery">查询</p>
+      <p class="page-reset-common-style-button" @click="resetQuery">重置</p>
+    </div>
+
+    <!-- 表格 -->
+    <div class="page-content-box">
+      <el-table class="table-box" :data="tableData" border>
+        <el-table-column prop="userName" label="姓名" width="200" />
+        <el-table-column prop="account" label="学工号" width="140" />
+        <el-table-column prop="year" label="学年" width="140" />
+        <el-table-column prop="deptName" label="所属学院" show-overflow-tooltip />
+        <el-table-column prop="year1CreditHours" label="第一年" width="140" align="center" />
+        <el-table-column prop="year2CreditHours" label="第二年" width="140" align="center" />
+        <el-table-column prop="year3CreditHours" label="第三年" width="140" align="center" />
+        <el-table-column prop="year4CreditHours" label="第四年" width="140" align="center" />
+        <el-table-column prop="totalCreditHours" label="总时长" width="140" align="center" />
+      </el-table>
+      <pagination :page-sizes="[20, 30, 40, 50]"
+                  v-show="total>0"
+                  :total="total"
+                  :page.sync="queryParams.page"
+                  :limit.sync="queryParams.pageSize"
+                  @pagination="examElStatisticsCreditHourList"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import echarts from 'echarts'
+import {
+  systemUserGetPost,
+} from "@/api/basicsModules/index";
+import {
+  examElStatisticsCreditHourChart,examElStatisticsCreditHourList,
+  systemUserGetAcademicYear,systemDeptCurrentDept,
+} from "@/api/safetyEducationExaminationNew/index";
+export default {
+  name: 'StudyHourStats',
+  data() {
+    return {
+      adminType:0,
+      filterData: {
+        personType: '',
+        college: '',
+        grade: '',
+        keyword: '',
+        page: 1,
+        colleges: ['化学与化工学院','生命科学学院','物理学院','电气工程学院','材料科学学院','机械工程学院','自动化学院','资源环境学院'],
+        grades: ['2017级','2018级','2019级','2020级','2021级','2022级'],
+      },
+      chartData: [
+        // { label: '化学与化工', graduate: 320, undergrad: 180, staff: 95 },
+        // { label: '生命科学',   graduate: 280, undergrad: 160, staff: 72 },
+        // { label: '物理',       graduate: 240, undergrad: 145, staff: 68 },
+        // { label: '电气工程',   graduate: 300, undergrad: 195, staff: 85 },
+        // { label: '材料科学',   graduate: 220, undergrad: 135, staff: 60 },
+        // { label: '机械工程',   graduate: 195, undergrad: 170, staff: 55 },
+        // { label: '自动化',     graduate: 180, undergrad: 120, staff: 48 },
+        // { label: '资源环境',   graduate: 165, undergrad: 110, staff: 52 },
+      ],
+      tableData: [],
+      optionListA:[],
+      optionListB:[],
+      queryParams:{
+        page:1,
+        pageSize:20,
+        personType:'',
+        deptId:'',
+        gradeNumber:'',
+        searchValue:'',
+      },
+      total:0,
+    }
+  },
+  mounted() {
+    this.systemUserGetPost();
+  },
+  beforeDestroy() {
+    if (this._barChart) this._barChart.dispose()
+  },
+  methods: {
+    //查询当前登录用户身份-校级/院级
+    systemUserGetPost(){
+      systemUserGetPost({}).then(response => {
+        for(let i=0;i<response.data.length;i++){
+          if(response.data[i].postCode == 'school_level_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','1');
+            }
+          }else if(response.data[i].postCode == 'college_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','2');
+            }
+          }
+        }
+        this.systemUserGetAcademicYear();
+        this.systemDeptCurrentDept();
+        this.examElStatisticsCreditHourChart();
+        this.examElStatisticsCreditHourList();
+      });
+    },
+    //导出
+    exportButton(){
+      let self = this;
+      self.$confirm(`确认导出数据?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(async () => {
+        let obj = {
+          academicYear:this.valueYear,
+          gradeNumber:this.queryParams.gradeNumber,
+          searchValue:this.queryParams.searchValue,
+        };
+        if(this.adminType == 1){
+          obj.deptId = this.queryParams.deptId;
+        }else{
+          obj.deptId = localStorage.getItem('deptId');
+        }
+        self.download('/exam/elStatistics/creditHour/export', {...obj}, '学时统计.xlsx')
+      }).catch(() => {})
+    },
+    initBarChart() {
+      const chart = echarts.init(this.$refs.barChart)
+      this._barChart = chart
+      const data = this.chartData
+      chart.setOption({
+        legend: {
+          data: ['研究生', '本科生', '教职工'],
+          top: 10,
+          right: 'center',
+          itemWidth: 14,
+          itemHeight: 14,
+          textStyle: { fontSize: 12, color: '#555' },
+        },
+        grid: { top: 50, bottom: 56, left: 56, right: 20, containLabel: false },
+        xAxis: {
+          type: 'category',
+          data: data.map(d => d.deptName),
+          axisLine: { lineStyle: { color: '#ddd' } },
+          axisTick: { show: false },
+          axisLabel: { fontSize: 12, color: '#666', rotate: 30 },
+        },
+        yAxis: {
+          type: 'value',
+          name: '学时',
+          nameTextStyle: { fontSize: 11, color: '#aaa', padding: [0, 30, 0, 0] },
+          max: 350,
+          interval: 50,
+          splitLine: { lineStyle: { color: '#f0f0f0' } },
+          axisLabel: { fontSize: 11, color: '#aaa' },
+          axisLine: { show: false },
+          axisTick: { show: false },
+        },
+        series: [
+          {
+            name: '研究生',
+            type: 'bar',
+            barGap: '10%',
+            barCategoryGap: '40%',
+            data: data.map(d => d.graduateCreditHours),
+            itemStyle: { color: '#5b9cf6' },
+            label: {
+              show: true,
+              position: 'top',
+              fontSize: 11,
+              color: '#555',
+              formatter: '{c}',
+            },
+          },
+          {
+            name: '本科生',
+            type: 'bar',
+            data: data.map(d => d.undergraduateCreditHours),
+            itemStyle: { color: '#4fbe7e' },
+            label: {
+              show: true,
+              position: 'top',
+              fontSize: 11,
+              color: '#555',
+              formatter: '{c}',
+            },
+          },
+          {
+            name: '教职工',
+            type: 'bar',
+            data: data.map(d => d.staffCreditHours),
+            itemStyle: { color: '#f59e42' },
+            label: {
+              show: true,
+              position: 'top',
+              fontSize: 11,
+              color: '#555',
+              formatter: '{c}',
+            },
+          },
+        ],
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' },
+        },
+      })
+    },
+    //学时统计图表
+    examElStatisticsCreditHourChart(){
+      let obj = {};
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else{
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsCreditHourChart(obj).then(response => {
+        this.$set(this,'chartData',response.data.deptStats);
+        if(response.data.deptStats[0]){
+          this.$nextTick(() => {
+            this.initBarChart()
+          })
+        }
+      });
+    },
+    //学时统计列表
+    examElStatisticsCreditHourList(){
+      let obj = JSON.parse(JSON.stringify(this.queryParams))
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else{
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsCreditHourList(obj).then(response => {
+        this.$set(this,'tableData',response.data.records);
+        this.$set(this,'total',response.data.total);
+      });
+    },
+    //学年下拉列表
+    systemUserGetAcademicYear(){
+      systemUserGetAcademicYear({}).then(response => {
+        this.$set(this,'optionListB',response.data);
+      });
+    },
+    //学院列表
+    systemDeptCurrentDept(){
+      systemDeptCurrentDept({}).then(response => {
+        const list = response.data || [];
+        this.formatTreeData(list);
+        if(list[0]){
+          this.$set(this,'optionListA',list[0].child);
+        }else{
+          this.$set(this,'optionListA',[]);
+        }
+      });
+    },
+    //查询按钮
+    handleQuery(){
+      this.$set(this.queryParams,'page',1);
+      this.examElStatisticsCreditHourList();
+    },
+    //重置按钮
+    resetQuery(){
+      this.$set(this,'dateRange',[])
+      this.$set(this,'queryParams',{
+        page:1,
+        pageSize:20,
+        personType:'',
+        deptId:'',
+        gradeNumber:'',
+        searchValue:'',
+      });
+      this.examElStatisticsCreditHourList();
+    },
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.study-hour-card {
+  background: #fff;
+  /*border-radius: 6px;*/
+  padding: 16px 20px;
+  margin-bottom: 12px;
+  /*box-shadow: 0 1px 4px rgba(0,0,0,0.06);*/
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid #e8edf5;
+  border-radius: 16px;
+  box-shadow: 0 4px 20px rgba(37, 50, 74, 0.06);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    .card-title { font-size: 16px; font-weight: 700; color: #222; }
+  }
+
+  .export-btn {
+    display: inline-block;
+    padding: 0 16px;
+    height: 40px;
+    line-height: 40px;
+    border-radius: 4px;
+    font-size: 14px;
+    cursor: pointer;
+    background-color: #0183fa;
+    color: #fff;
+    border: 1px solid #0183fa;
+  }
+
+  .chart-wrap {
+    border: 1px solid #eee;
+    border-radius: 6px;
+    margin-bottom: 16px;
+    height: 300px;
+  }
+
+  .filter-row {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 12px;
+    flex-wrap: wrap;
+  }
+  .page-content-box{
+    height:500px;
+    padding:0;
+  }
+
+  .link-blue { color: #409eff; }
+
+  .pagination-wrap {
+    display: flex;
+    justify-content: center;
+    margin-top: 14px;
+  }
+}
+</style>

+ 131 - 0
src/views/safetyEducationExaminationNew/dataStatistics/components/examStudyComponent.vue

@@ -0,0 +1,131 @@
+<template>
+  <div class="exam-study-component">
+    <div class="top-table-max-box">
+      <div class="top-table-box"
+           v-hasPermiRouter="['exam:dataStatistics:dataReport']"
+           :class="checkIndex == 1?'check-div':''" @click="checkButton(1)">
+        <p>数据报表</p>
+        <p></p>
+      </div>
+      <div class="top-table-box"
+           v-hasPermiRouter="['exam:dataStatistics:regularExam']"
+           :class="checkIndex == 2?'check-div':''" @click="checkButton(2)">
+        <p>常规考试</p>
+        <p></p>
+      </div>
+      <div class="top-table-box-flex" v-if="checkIndex!=0">
+        <p></p>
+        <p></p>
+      </div>
+    </div>
+    <dataReportExamSituation v-if="checkIndex == 1"></dataReportExamSituation>
+    <dataReportStudyHourStats v-if="checkIndex == 1"></dataReportStudyHourStats>
+    <regularExamExamSituation v-if="checkIndex == 2"></regularExamExamSituation>
+    <regularExamStudyHourStats v-if="checkIndex == 2"></regularExamStudyHourStats>
+  </div>
+</template>
+
+<script>
+  import dataReportExamSituation from './dataReport/ExamSituation.vue'
+  import dataReportStudyHourStats from './dataReport/StudyHourStats.vue'
+  import regularExamExamSituation from './regularExam/ExamSituation.vue'
+  import regularExamStudyHourStats from './regularExam/StudyHourStats.vue'
+  export default {
+    name: 'examStudyComponent',
+    components: {
+      dataReportExamSituation,
+      dataReportStudyHourStats,
+      regularExamExamSituation,
+      regularExamStudyHourStats,
+    },
+    data() {
+      return {
+        checkIndex:0,
+      }
+    },
+    created() {
+
+    },
+    mounted() {
+      this.buttonPermission();
+    },
+    methods: {
+      //权限字段检查并修改初始选中数据
+      buttonPermission(){
+        let dataReportButton = this.hasPermiDom(['exam:dataStatistics:dataReport']);
+        let regularExamButton = this.hasPermiDom(['exam:dataStatistics:regularExam']);
+        if(dataReportButton && regularExamButton){
+          this.$set(this,'checkIndex',1);
+        }else if(dataReportButton && !regularExamButton){
+          this.$set(this,'checkIndex',1);
+        }else if(!dataReportButton && regularExamButton){
+          this.$set(this,'checkIndex',2);
+        }else if(!dataReportButton && !regularExamButton){
+          this.$set(this,'checkIndex',0);
+        }
+      },
+      //选项卡切换
+      checkButton(type){
+        if(this.checkIndex != type){
+          this.$set(this,'checkIndex',type);
+        }
+      },
+    },
+  }
+</script>
+
+<style scoped lang="scss">
+  .exam-study-component{
+    /*background: #fff;*/
+    /*padding: 20px;*/
+    /*margin-bottom: 12px;*/
+    /*background: rgba(255, 255, 255, 0.95);*/
+    /*border: 1px solid #e8edf5;*/
+    /*border-radius: 16px;*/
+    /*box-shadow: 0 4px 20px rgba(37, 50, 74, 0.06);*/
+    .top-table-max-box{
+      display: flex;
+      margin-bottom:20px;
+      .top-table-box{
+        cursor: pointer;
+        width:120px;
+        text-align: center;
+        p:nth-child(1){
+          height:20px;
+          line-height:20px;
+          font-size:16px;
+          color:#78869a;
+          font-weight:600;
+        }
+        p:nth-child(2){
+          margin-top:11px;
+          height:1px;
+          background-color: #dedede;
+        }
+      }
+      .check-div{
+        p:nth-child(1){
+          color:#0045AF;
+        }
+        p:nth-child(2){
+          margin-top:10px;
+          height:2px;
+          background-color: #0045AF;
+        }
+      }
+      .top-table-box-flex{
+        flex:1;
+        p:nth-child(1){
+          height:20px;
+          line-height:20px;
+          font-size:16px;
+        }
+        p:nth-child(2){
+          margin-top:11px;
+          height:1px;
+          background-color: #dedede;
+        }
+      }
+    }
+  }
+</style>

+ 566 - 0
src/views/safetyEducationExaminationNew/dataStatistics/components/regularExam/ExamSituation.vue

@@ -0,0 +1,566 @@
+<!-- 考试情况统计 -->
+<template>
+  <div class="exam-situation-card">
+    <div class="card-header">
+      <span class="card-title">考试情况统计</span>
+      <div class="header-right">
+        <!--<el-select v-model="valueYear" @change="topChange()" size="small" style="width:220px">-->
+          <!--<el-option-->
+            <!--v-for="dict in optionList"-->
+            <!--:key="dict.value"-->
+            <!--:label="dict.label"-->
+            <!--:value="dict.value"-->
+          <!--/>-->
+        <!--</el-select>-->
+        <!-- 分组按钮 -->
+        <div class="table-top-button-box">
+          <div>
+            <p @click="checkButton(2)" :class="checkIndex == 2?'check-table-button':''">研究生</p>
+            <p @click="checkButton(3)" :class="checkIndex == 3?'check-table-button':''">本科生</p>
+            <p @click="checkButton(1)" :class="checkIndex == 1?'check-table-button':''">教职工</p>
+          </div>
+        </div>
+        <!-- 导出按钮 -->
+        <p class="export-btn" @click="exportButton()">
+          <i class="el-icon-download"></i> 导出数据
+        </p>
+      </div>
+    </div>
+
+    <!-- 图表区 -->
+    <div class="chart-area">
+      <!-- 堆叠柱状图 -->
+      <p class="bar-chart-wrap" v-if="!chartData.deptStats[0]" style="color:#999;font-size:14px;line-height:260px;text-align: center;">暂无数据</p>
+      <div class="bar-chart-wrap" v-if="chartData.deptStats[0]" id="bar-chart-wrap" ref="barChart"></div>
+      <!-- 环形图 -->
+      <p class="pie-chart-wrap" v-if="!chartData.deptStats[0]" style="color:#999;font-size:14px;line-height:260px;text-align: center;">暂无数据</p>
+      <div class="pie-chart-wrap" v-if="chartData.deptStats[0]" id="pie-chart-wrap" ref="pieChart"></div>
+    </div>
+
+    <!-- 筛选 -->
+    <div class="filter-row">
+      <el-select v-model="valueYear" @change="topChange()" size="small" style="width:220px">
+        <el-option
+          v-for="dict in optionList"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
+      <el-select filterable
+        @change="queryParamsChange"
+        v-model="queryParams.examId" clearable placeholder="请输入或选择考试名称" style="width: 360px">
+        <el-option
+          v-for="dict in optionListB"
+          :key="dict.examId"
+          :label="dict.examName"
+          :value="dict.examId"
+        />
+      </el-select>
+      <!--<el-cascader-->
+        <!--v-if="adminType == 1"-->
+        <!--style="width: 180px"-->
+        <!--clearable-->
+        <!--v-model="queryParams.deptId"-->
+        <!--:options="optionListA"-->
+        <!--:props="{-->
+                    <!--emitPath:false,-->
+                    <!--checkStrictly: true,-->
+                    <!--value: 'deptId',-->
+                    <!--label: 'deptName',-->
+                    <!--children: 'child',-->
+                  <!--}"-->
+        <!--:show-all-levels="false"-->
+        <!--filterable-->
+        <!--placeholder="筛选学院"-->
+        <!--@change="queryParamsChange"-->
+      <!--&gt;</el-cascader>-->
+      <el-select v-if="adminType == 1"
+                 @change="queryParamsChange"
+                 clearable
+                 v-model="queryParams.deptId" placeholder="学院" style="width: 180px">
+        <el-option
+          v-for="dict in optionListA"
+          :key="dict.deptId"
+          :label="dict.deptName"
+          :value="dict.deptId"
+        />
+      </el-select>
+      <p style="line-height:40px;font-size:14px;color:#1768d5;" v-if="!queryParams.deptId">共组织常规考试 <span style="font-weight:700;">{{optionListB.length}}</span> 次</p>
+    </div>
+
+    <!-- 统计表格 -->
+
+    <div class="page-content-box">
+      <el-table class="table-box" :data="dataList" border :span-method="objectSpanMethod">
+        <el-table-column prop="deptName" label="学院" />
+        <!--<el-table-column prop="examTypeName" label="考试类型" width="200" />-->
+        <el-table-column prop="expectedCount" label="应考人数" width="160" align="center" />
+        <el-table-column prop="examCount" label="实考人数" width="160" align="center" />
+        <el-table-column prop="passedCount" label="通过人数" width="160" align="center" />
+        <el-table-column prop="notPassedCount" label="未通过人数" width="160" align="center"/>
+        <el-table-column prop="absentCount" label="未考人数" width="160" align="center" />
+        <el-table-column prop="passRate" label="通过率" width="160" align="center" />
+      </el-table>
+      <pagination :page-sizes="[20, 30, 40, 50]"
+                  v-show="total>0"
+                  :total="total"
+                  :page.sync="queryParams.page"
+                  :limit.sync="queryParams.pageSize"
+                  @pagination="examElStatisticsExamList"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import echarts from 'echarts'
+import {
+  systemUserGetPost,
+} from "@/api/basicsModules/index";
+import {
+  examElStatisticsGetAcademicYear,examElStatisticsExamList,examElStatisticsExamChart,
+  examElStatisticsExamTitleList,systemDeptCurrentDept,
+} from "@/api/safetyEducationExaminationNew/index";
+export default {
+  name: 'ExamSituation',
+  data() {
+    return {
+      adminType:0,
+      chartData: {
+        deptStats:[],
+        barGroups: []
+      },
+      chartList:[],
+      spanMap: {},
+      optionList:[],
+      optionListA:[],
+      optionListB:[],
+      valueYear:'',
+      checkIndex:2,
+      queryParams:{
+        page:1,
+        pageSize:20,
+        deptId:'',
+        examId:'',
+      },
+      dataList:[],
+      total:0,
+    }
+  },
+  created() {
+
+  },
+  mounted() {
+    this.systemUserGetPost();
+  },
+  beforeDestroy() {
+    if (this._barChart) this._barChart.dispose()
+    if (this._pieChart) this._pieChart.dispose()
+  },
+  methods: {
+    //查询当前登录用户身份-校级/院级
+    systemUserGetPost(){
+      systemUserGetPost({}).then(response => {
+        for(let i=0;i<response.data.length;i++){
+          if(response.data[i].postCode == 'school_level_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','1');
+            }
+          }else if(response.data[i].postCode == 'college_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','2');
+            }
+          }
+        }
+        this.systemDeptCurrentDept();
+        this.examElStatisticsGetAcademicYear();
+      });
+    },
+    //导出
+    exportButton(){
+      let self = this;
+      self.$confirm(`确认导出数据?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(async () => {
+        let obj = {
+          academicYear:this.valueYear,
+          personType:this.checkIndex,
+          examId:this.queryParams.examId,
+        };
+        if(this.adminType == 1){
+          obj.deptId = this.queryParams.deptId;
+        }else{
+          obj.deptId = localStorage.getItem('deptId');
+        }
+        self.download('/exam/elStatistics/exam/export', {...obj}, '考试情况统计.xlsx')
+      }).catch(() => {})
+    },
+    //图表
+    initBarChart() {
+      const groups = this.chartData.deptStats
+      const chart = echarts.init(this.$refs.barChart)
+      this._barChart = chart
+
+      const PAGE_SIZE = 10 // 单页最多显示的柱体数(堆叠组数)
+
+      chart.setOption({
+        legend: {
+          data: ['通过', '未通过', '未考'],
+          top: 10,
+          itemWidth: 14,
+          itemHeight: 14,
+          textStyle: { fontSize: 12, color: '#555' },
+        },
+        grid: { top: 60, bottom: 70, left: 40, right: 10, containLabel: false },
+        xAxis: {
+          type: 'category',
+          data: groups.map(g => g.deptName),
+          axisLine: { lineStyle: { color: '#ddd' } },
+          axisLabel: {
+            fontSize: 11,
+            color: '#666',
+            interval: 0, // 滚动过程中每个类目都显示标签
+            rotate: 25,
+          },
+          axisTick: { show: false },
+        },
+        yAxis: {
+          type: 'value',
+          splitLine: { lineStyle: { color: '#f0f0f0' } },
+          axisLabel: { fontSize: 11, color: '#aaa' },
+          axisLine: { show: false },
+          axisTick: { show: false },
+        },
+        // 新增:横向滚动
+        dataZoom: [
+          {
+            type: 'slider', // 底部滑块,用户可拖动/点击滚动
+            xAxisIndex: 0,
+            startValue: 0,
+            endValue: Math.min(PAGE_SIZE - 1, groups.length - 1), // 初始窗口:6 个柱体
+            minValueSpan: PAGE_SIZE - 1, // 可选:限制最小窗口也是 6 个
+            height: 18,
+            bottom: 10,
+            borderColor: '#e0e0e0',
+            fillerColor: 'rgba(79, 190, 126, 0.15)',
+            handleStyle: { color: '#4fbe7e' },
+            moveHandleStyle: { color: '#4fbe7e' },
+            textStyle: { color: '#aaa', fontSize: 10 },
+            brushSelect: false,
+          },
+          {
+            type: 'inside', // 鼠标滚轮/触摸拖动也可滚动
+            xAxisIndex: 0,
+            startValue: 0,
+            endValue: Math.min(PAGE_SIZE - 1, groups.length - 1),
+            zoomLock: true, // 锁定缩放,只允许平移滚动
+          },
+        ],
+        series: [
+          {
+            name: '通过',
+            type: 'bar',
+            stack: 'total',
+            barMaxWidth: 60, // 新增:限制最大宽度,防止数据少时柱子过粗
+            data: groups.map(g => g.passedCount),
+            itemStyle: { color: '#4fbe7e' },
+            label: { show: true, position: 'inside', fontSize: 11, color: '#fff',
+              formatter: (p) => p.value > 0 ? p.value : '' },
+          },
+          {
+            name: '未通过',
+            type: 'bar',
+            stack: 'total',
+            barMaxWidth: 60,
+            data: groups.map(g => g.notPassedCount),
+            itemStyle: { color: '#f56c6c' },
+            label: { show: true, position: 'inside', fontSize: 10, color: '#fff',
+              formatter: (p) => p.value > 0 ? p.value : '' },
+          },
+          {
+            name: '未考',
+            type: 'bar',
+            stack: 'total',
+            barMaxWidth: 60,
+            data: groups.map(g => g.absentCount),
+            itemStyle: { color: '#c0c4cc' },
+            label: { show: true, position: 'inside', fontSize: 10, color: '#fff',
+              formatter: (p) => p.value > 0 ? p.value : '' },
+          },
+        ],
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' },
+        },
+      })
+    },
+    //图表
+    initPieChart() {
+      const pass    = this.chartData.totalPassed
+      const fail    = this.chartData.totalNotPassed
+      const notExam = this.chartData.totalAbsent
+      const chart = echarts.init(this.$refs.pieChart)
+      this._pieChart = chart
+      chart.setOption({
+        legend: {
+          orient: 'horizontal',
+          bottom: 0,
+          data: ['通过', '未通过', '未考'],
+          itemWidth: 12,
+          itemHeight: 12,
+          textStyle: { fontSize: 12, color: '#555' },
+        },
+        series: [
+          {
+            type: 'pie',
+            radius: ['45%', '68%'],
+            center: ['50%', '45%'],
+            label: {
+              formatter: '{b}',
+              fontSize: 12,
+              color: '#555',
+            },
+            labelLine: { length: 12, length2: 8 },
+            data: [
+              { value: pass,    name: '通过',  itemStyle: { color: '#4fbe7e' } },
+              { value: fail,    name: '未通过', itemStyle: { color: '#f56c6c' } },
+              { value: notExam, name: '未考',   itemStyle: { color: '#c0c4cc' } },
+            ],
+            emphasis: {
+              itemStyle: { shadowBlur: 6, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.2)' }
+            },
+          },
+        ],
+        tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
+      })
+    },
+    //顶部学年切换
+    topChange(){
+      this.examElStatisticsExamTitleList();
+      this.$set(this.queryParams,'examId','');
+      this.$set(this.queryParams,'page',1);
+      this.examElStatisticsExamChart();
+      this.examElStatisticsExamList();
+    },
+    //身份切换
+    checkButton(val){
+      if(this.checkIndex!=val){
+        this.$set(this,'checkIndex',val);
+        this.$set(this.queryParams,'page',1);
+        this.examElStatisticsExamChart();
+        this.examElStatisticsExamList();
+      }
+    },
+    //条件变更
+    queryParamsChange(){
+      this.$set(this.queryParams,'page',1);
+      this.examElStatisticsExamChart();
+      this.examElStatisticsExamList();
+    },
+    //数据合并
+    objectSpanMethod({ row, column, rowIndex, columnIndex }) {
+      // 只对第一列(学院列,columnIndex === 0)进行合并
+      if (columnIndex === 0) {
+        const list = this.dataList;
+
+        // 如果是第一行,或者当前行的学院与上一行的学院不同,则开始合并计算
+        if (rowIndex === 0 || row.deptName !== list[rowIndex - 1].deptName) {
+          let rowspan = 1;
+          // 向下遍历,统计有多少行是相同的学院
+          for (let i = rowIndex + 1; i < list.length; i++) {
+            if (list[i].deptName === row.deptName) {
+              rowspan++;
+            } else {
+              break; // 遇到不同的学院,停止计算
+            }
+          }
+          return { rowspan, colspan: 1 };
+        } else {
+          // 如果当前行的学院与上一行相同,则隐藏当前单元格(被合并了)
+          return { rowspan: 0, colspan: 0 };
+        }
+      }
+      // 其他列正常显示
+      return { rowspan: 1, colspan: 1 };
+    },
+    //考试统计图表
+    examElStatisticsExamChart(){
+      let obj = {
+        // academicYear:this.valueYear,
+        personType:this.checkIndex,
+        examId:this.queryParams.examId,
+      }
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else if(this.adminType == 2){
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsExamChart(obj).then(response => {
+        this.$set(this,'chartData',response.data);
+        this.$nextTick(() => {
+          if(response.data.deptStats[0]){
+            this.$nextTick(() => {
+              this.initBarChart()
+              this.initPieChart()
+            })
+          }
+        })
+      });
+    },
+    //考试统计列表
+    examElStatisticsExamList(){
+      let obj = {
+        page:this.queryParams.page,
+        pageSize:this.queryParams.pageSize,
+        academicYear:this.valueYear,
+        personType:this.checkIndex,
+        examId:this.queryParams.examId,
+      }
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else{
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsExamList(obj).then(response => {
+        this.$set(this,'dataList',response.data.records);
+        this.$set(this,'total',response.data.total);
+      });
+    },
+    //查询全部考试类型
+    examElStatisticsExamTitleList(){
+      let obj = {
+        year:this.valueYear
+      }
+      if(this.adminType == 2){
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsExamTitleList(obj).then(response => {
+        this.$set(this,'optionListB',response.data);
+      });
+    },
+    //学院列表
+    systemDeptCurrentDept(){
+      systemDeptCurrentDept({}).then(response => {
+        const list = response.data || [];
+        this.formatTreeData(list);
+        if(list[0]){
+          this.$set(this,'optionListA',list[0].child);
+        }else{
+          this.$set(this,'optionListA',[]);
+        }
+      });
+    },
+    //学年列表
+    examElStatisticsGetAcademicYear(){
+      examElStatisticsGetAcademicYear({}).then(response => {
+        this.$set(this,'optionList',response.data);
+        this.$set(this,'valueYear',response.data[0].value);
+        this.examElStatisticsExamTitleList();
+        this.examElStatisticsExamList();
+        this.examElStatisticsExamChart();
+      });
+    },
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.exam-situation-card {
+  background: #fff;
+  /*border-radius: 6px;*/
+  padding: 20px;
+  margin-bottom: 12px;
+  /*box-shadow: 0 1px 4px rgba(0,0,0,0.06);*/
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid #e8edf5;
+  border-radius: 16px;
+  box-shadow: 0 4px 20px rgba(37, 50, 74, 0.06);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    .card-title { font-size: 16px; font-weight: 700; color: #222; }
+    .header-right { width:370px;display: flex; align-items: center; gap: 20px; }
+  }
+
+  /* 分组筛选按钮 */
+  .table-top-button-box {
+    display: inline-block;
+    vertical-align: top;
+    div {
+      display: flex;
+      p {
+        border: 1px solid #DCDFE6;
+        width: 80px;
+        text-align: center;
+        height: 40px;
+        line-height: 40px;
+        cursor: pointer;
+        font-size: 14px;
+        &:nth-child(1) { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
+        &:nth-child(2) { border-left: none; border-right: none; }
+        &:nth-child(3) { border-top-right-radius: 4px; border-bottom-right-radius: 4px; }
+      }
+      .check-table-button{
+        border: 1px solid #0183fa!important;
+        background-color: #0183fa!important;;
+        color:#fff!important;;
+      }
+    }
+  }
+
+  /* 导出按钮 */
+  .export-btn {
+    display: inline-block;
+    padding: 0 16px;
+    height: 40px;
+    line-height: 40px;
+    border-radius: 4px;
+    font-size: 14px;
+    cursor: pointer;
+    background-color: #0183fa;
+    color: #fff;
+    border: 1px solid #0183fa;
+  }
+
+  .chart-area {
+    display: flex;
+    gap: 0;
+    margin-bottom: 16px;
+    border: 1px solid #eee;
+    border-radius: 6px;
+    overflow: hidden;
+
+    .bar-chart-wrap {
+      flex: 1;
+      height: 280px;
+      border-right: 1px solid #eee;
+    }
+
+    .pie-chart-wrap {
+      width: 320px;
+      height: 280px;
+    }
+  }
+
+  .filter-row {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 12px;
+  }
+  .page-content-box{
+    height:500px;
+    padding:0;
+    ::v-deep .el-table--medium td:nth-child(1){
+      border-left:1px solid #dfe6ec!important;
+    }
+  }
+
+  .link-blue { color: #409eff; }
+}
+</style>

+ 388 - 0
src/views/safetyEducationExaminationNew/dataStatistics/components/regularExam/StudyHourStats.vue

@@ -0,0 +1,388 @@
+<!-- 学时统计 -->
+<template>
+  <div class="study-hour-card">
+    <div class="card-header">
+      <span class="card-title">学时统计</span>
+      <p class="export-btn" @click="exportButton()">
+        <i class="el-icon-download"></i> 导出数据
+      </p>
+    </div>
+
+    <!-- 分组柱状图 -->
+    <p class="chart-wrap" v-if="!chartData[0]" style="color:#999;font-size:14px;line-height:260px;text-align: center;">暂无数据</p>
+    <div class="chart-wrap" v-if="chartData[0]" id="chart-wrap-bar-chart" ref="barChart"></div>
+
+    <!-- 筛选 -->
+    <div class="filter-row">
+      <el-select v-model="queryParams.personType" placeholder="用户类型" style="width:220px">
+        <el-option label="研究生" value="2" />
+        <el-option label="本科生" value="3" />
+        <el-option label="教职工" value="1" />
+      </el-select>
+      <!--<el-cascader-->
+        <!--v-if="adminType == 1"-->
+        <!--style="width: 180px"-->
+        <!--v-model="queryParams.deptId"-->
+        <!--:options="optionListA"-->
+        <!--:props="{-->
+                  <!--emitPath:false,-->
+                  <!--checkStrictly: true,-->
+                  <!--value: 'deptId',-->
+                  <!--label: 'deptName',-->
+                  <!--children: 'child',-->
+                <!--}"-->
+        <!--:show-all-levels="false"-->
+        <!--filterable-->
+        <!--placeholder="学院"-->
+      <!--&gt;</el-cascader>-->
+      <el-select v-if="adminType == 1" v-model="queryParams.deptId" placeholder="学院" style="width: 180px">
+        <el-option
+          v-for="dict in optionListA"
+          :key="dict.deptId"
+          :label="dict.deptName"
+          :value="dict.deptId"
+        />
+      </el-select>
+      <el-select v-model="queryParams.gradeNumber" placeholder="学年" style="width: 140px">
+        <el-option
+          v-for="dict in optionListB"
+          :key="dict.gradeNumber"
+          :label="dict.gradeNumber"
+          :value="dict.gradeNumber"
+        />
+      </el-select>
+      <el-input v-model="queryParams.searchValue" placeholder="学号/姓名" size="small" prefix-icon="el-icon-search" style="width:210px" clearable />
+      <p class="page-inquire-common-style-button" @click="handleQuery">查询</p>
+      <p class="page-reset-common-style-button" @click="resetQuery">重置</p>
+    </div>
+
+    <!-- 表格 -->
+    <div class="page-content-box">
+      <el-table class="table-box" :data="tableData" border>
+        <el-table-column prop="userName" label="姓名" width="200" />
+        <el-table-column prop="account" label="学工号" width="140" />
+        <el-table-column prop="year" label="学年" width="140" />
+        <el-table-column prop="deptName" label="所属学院" show-overflow-tooltip />
+        <el-table-column prop="year1CreditHours" label="第一年" width="140" align="center" />
+        <el-table-column prop="year2CreditHours" label="第二年" width="140" align="center" />
+        <el-table-column prop="year3CreditHours" label="第三年" width="140" align="center" />
+        <el-table-column prop="year4CreditHours" label="第四年" width="140" align="center" />
+        <el-table-column prop="totalCreditHours" label="总时长" width="140" align="center" />
+      </el-table>
+      <pagination :page-sizes="[20, 30, 40, 50]"
+                  v-show="total>0"
+                  :total="total"
+                  :page.sync="queryParams.page"
+                  :limit.sync="queryParams.pageSize"
+                  @pagination="examElStatisticsCreditHourList"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import echarts from 'echarts'
+import {
+  systemUserGetPost,
+} from "@/api/basicsModules/index";
+import {
+  examElStatisticsCreditHourChart,examElStatisticsCreditHourList,
+  systemUserGetAcademicYear,systemDeptCurrentDept,
+} from "@/api/safetyEducationExaminationNew/index";
+export default {
+  name: 'StudyHourStats',
+  data() {
+    return {
+      adminType:0,
+      filterData: {
+        personType: '',
+        college: '',
+        grade: '',
+        keyword: '',
+        page: 1,
+        colleges: ['化学与化工学院','生命科学学院','物理学院','电气工程学院','材料科学学院','机械工程学院','自动化学院','资源环境学院'],
+        grades: ['2017级','2018级','2019级','2020级','2021级','2022级'],
+      },
+      chartData: [
+        // { label: '化学与化工', graduate: 320, undergrad: 180, staff: 95 },
+        // { label: '生命科学',   graduate: 280, undergrad: 160, staff: 72 },
+        // { label: '物理',       graduate: 240, undergrad: 145, staff: 68 },
+        // { label: '电气工程',   graduate: 300, undergrad: 195, staff: 85 },
+        // { label: '材料科学',   graduate: 220, undergrad: 135, staff: 60 },
+        // { label: '机械工程',   graduate: 195, undergrad: 170, staff: 55 },
+        // { label: '自动化',     graduate: 180, undergrad: 120, staff: 48 },
+        // { label: '资源环境',   graduate: 165, undergrad: 110, staff: 52 },
+      ],
+      tableData: [],
+      optionListA:[],
+      optionListB:[],
+      queryParams:{
+        page:1,
+        pageSize:20,
+        personType:'',
+        deptId:'',
+        gradeNumber:'',
+        searchValue:'',
+      },
+      total:0,
+    }
+  },
+  mounted() {
+    this.systemUserGetPost();
+  },
+  beforeDestroy() {
+    if (this._barChart) this._barChart.dispose()
+  },
+  methods: {
+    //查询当前登录用户身份-校级/院级
+    systemUserGetPost(){
+      systemUserGetPost({}).then(response => {
+        for(let i=0;i<response.data.length;i++){
+          if(response.data[i].postCode == 'school_level_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','1');
+            }
+          }else if(response.data[i].postCode == 'college_admin'){
+            if(response.data[i].isPost){
+              this.$set(this,'adminType','2');
+            }
+          }
+        }
+        this.systemUserGetAcademicYear();
+        this.systemDeptCurrentDept();
+        this.examElStatisticsCreditHourChart();
+        this.examElStatisticsCreditHourList();
+      });
+    },
+    //导出
+    exportButton(){
+      let self = this;
+      self.$confirm(`确认导出数据?`, "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(async () => {
+        let obj = {
+          academicYear:this.valueYear,
+          gradeNumber:this.queryParams.gradeNumber,
+          searchValue:this.queryParams.searchValue,
+        };
+        if(this.adminType == 1){
+          obj.deptId = this.queryParams.deptId;
+        }else{
+          obj.deptId = localStorage.getItem('deptId');
+        }
+        self.download('/exam/elStatistics/creditHour/export', {...obj}, '学时统计.xlsx')
+      }).catch(() => {})
+    },
+    initBarChart() {
+      const chart = echarts.init(this.$refs.barChart)
+      this._barChart = chart
+      const data = this.chartData
+      chart.setOption({
+        legend: {
+          data: ['研究生', '本科生', '教职工'],
+          top: 10,
+          right: 'center',
+          itemWidth: 14,
+          itemHeight: 14,
+          textStyle: { fontSize: 12, color: '#555' },
+        },
+        grid: { top: 50, bottom: 56, left: 56, right: 20, containLabel: false },
+        xAxis: {
+          type: 'category',
+          data: data.map(d => d.deptName),
+          axisLine: { lineStyle: { color: '#ddd' } },
+          axisTick: { show: false },
+          axisLabel: { fontSize: 12, color: '#666', rotate: 30 },
+        },
+        yAxis: {
+          type: 'value',
+          name: '学时',
+          nameTextStyle: { fontSize: 11, color: '#aaa', padding: [0, 30, 0, 0] },
+          max: 350,
+          interval: 50,
+          splitLine: { lineStyle: { color: '#f0f0f0' } },
+          axisLabel: { fontSize: 11, color: '#aaa' },
+          axisLine: { show: false },
+          axisTick: { show: false },
+        },
+        series: [
+          {
+            name: '研究生',
+            type: 'bar',
+            barGap: '10%',
+            barCategoryGap: '40%',
+            data: data.map(d => d.graduateCreditHours),
+            itemStyle: { color: '#5b9cf6' },
+            label: {
+              show: true,
+              position: 'top',
+              fontSize: 11,
+              color: '#555',
+              formatter: '{c}',
+            },
+          },
+          {
+            name: '本科生',
+            type: 'bar',
+            data: data.map(d => d.undergraduateCreditHours),
+            itemStyle: { color: '#4fbe7e' },
+            label: {
+              show: true,
+              position: 'top',
+              fontSize: 11,
+              color: '#555',
+              formatter: '{c}',
+            },
+          },
+          {
+            name: '教职工',
+            type: 'bar',
+            data: data.map(d => d.staffCreditHours),
+            itemStyle: { color: '#f59e42' },
+            label: {
+              show: true,
+              position: 'top',
+              fontSize: 11,
+              color: '#555',
+              formatter: '{c}',
+            },
+          },
+        ],
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' },
+        },
+      })
+    },
+    //学时统计图表
+    examElStatisticsCreditHourChart(){
+      let obj = {};
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else{
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsCreditHourChart(obj).then(response => {
+        this.$set(this,'chartData',response.data.deptStats);
+        if(response.data.deptStats[0]){
+          this.$nextTick(() => {
+            this.initBarChart()
+          })
+        }
+      });
+    },
+    //学时统计列表
+    examElStatisticsCreditHourList(){
+      let obj = JSON.parse(JSON.stringify(this.queryParams))
+      if(this.adminType == 1){
+        obj.deptId = this.queryParams.deptId;
+      }else{
+        obj.deptId = localStorage.getItem('deptId');
+      }
+      examElStatisticsCreditHourList(obj).then(response => {
+        this.$set(this,'tableData',response.data.records);
+        this.$set(this,'total',response.data.total);
+      });
+    },
+    //学年下拉列表
+    systemUserGetAcademicYear(){
+      systemUserGetAcademicYear({}).then(response => {
+        this.$set(this,'optionListB',response.data);
+      });
+    },
+    //学院列表
+    systemDeptCurrentDept(){
+      systemDeptCurrentDept({}).then(response => {
+        const list = response.data || [];
+        this.formatTreeData(list);
+        if(list[0]){
+          this.$set(this,'optionListA',list[0].child);
+        }else{
+          this.$set(this,'optionListA',[]);
+        }
+      });
+    },
+    //查询按钮
+    handleQuery(){
+      this.$set(this.queryParams,'page',1);
+      this.examElStatisticsCreditHourList();
+    },
+    //重置按钮
+    resetQuery(){
+      this.$set(this,'dateRange',[])
+      this.$set(this,'queryParams',{
+        page:1,
+        pageSize:20,
+        personType:'',
+        deptId:'',
+        gradeNumber:'',
+        searchValue:'',
+      });
+      this.examElStatisticsCreditHourList();
+    },
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.study-hour-card {
+  background: #fff;
+  /*border-radius: 6px;*/
+  padding: 16px 20px;
+  margin-bottom: 12px;
+  /*box-shadow: 0 1px 4px rgba(0,0,0,0.06);*/
+  background: rgba(255, 255, 255, 0.95);
+  border: 1px solid #e8edf5;
+  border-radius: 16px;
+  box-shadow: 0 4px 20px rgba(37, 50, 74, 0.06);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    .card-title { font-size: 16px; font-weight: 700; color: #222; }
+  }
+
+  .export-btn {
+    display: inline-block;
+    padding: 0 16px;
+    height: 40px;
+    line-height: 40px;
+    border-radius: 4px;
+    font-size: 14px;
+    cursor: pointer;
+    background-color: #0183fa;
+    color: #fff;
+    border: 1px solid #0183fa;
+  }
+
+  .chart-wrap {
+    border: 1px solid #eee;
+    border-radius: 6px;
+    margin-bottom: 16px;
+    height: 300px;
+  }
+
+  .filter-row {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 12px;
+    flex-wrap: wrap;
+  }
+  .page-content-box{
+    height:500px;
+    padding:0;
+  }
+
+  .link-blue { color: #409eff; }
+
+  .pagination-wrap {
+    display: flex;
+    justify-content: center;
+    margin-top: 14px;
+  }
+}
+</style>

+ 7 - 3
src/views/safetyEducationExaminationNew/dataStatistics/index.vue

@@ -2,22 +2,26 @@
 <template>
   <div class="app-container dataStatistics">
     <div class="page-container dataStatisticsPage scrollbar-box">
+      <!--数据报表/常规考试-->
+      <examStudyComponent/>
       <!-- 考试情况统计 -->
-      <exam-situation/>
+      <!--<exam-situation/>-->
       <!-- 实验室与人员匹配 -->
-      <lab-person-match/>
+      <lab-person-match v-hasPermiRouter="['exam:dataStatistics:personBind']"/>
       <!-- 学时统计 -->
-      <study-hour-stats/>
+      <!--<study-hour-stats/>-->
     </div>
   </div>
 </template>
 <script>
+  import examStudyComponent from './components/examStudyComponent.vue'
   import ExamSituation from './ExamSituation.vue'
   import LabPersonMatch from './LabPersonMatch.vue'
   import StudyHourStats from './StudyHourStats.vue'
   export default {
     name: 'index',
     components: {
+      examStudyComponent,
       ExamSituation,
       LabPersonMatch,
       StudyHourStats,