Explorar o código

5月9日客户评审需求添加,需求规格书完善及更新

1.数据统计->考试情况统计新增教职工分类统计,新增学年选择;
2.补充需求规格说明书关于数据导出部分的明确要求;
3.新增毕业生毕业报告模板;
4.补充通过入学考试的本科生绑定实验室交互;
5.新增模拟新入学本科生考试通过按钮;
6.新增需求页面截图脚本demo-2doc-pdf;
7.新增需求确认书说明文档nwafu-exam-202604.docx。
stoney hai 6 días
pai
achega
32dfd1ed5f

+ 5 - 2
.gitignore

@@ -1,4 +1,4 @@
-# ---> macOS
+# ---> macOS
 .DS_Store
 .AppleDouble
 .LSOverride
@@ -24,4 +24,7 @@ Icon
 Network Trash Folder
 Temporary Items
 .apdisk
-
+
+# ---> Node
+node_modules/
+

BIN=BIN
assets/images/logo.png


+ 38 - 0
assets/script2doc/demo2doc-Pdf.sh

@@ -0,0 +1,38 @@
+#!/bin/bash
+# ============================================================
+# demo2doc-Pdf.sh
+# 自动截图所有角色/页面并生成横版A4 PDF文档
+# 输出: prd/nwafu-exam-demoImages-202604.pdf
+# ============================================================
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
+
+echo "=== 安全教育考试系统 Demo 截图生成工具 ==="
+echo "项目根目录: $PROJECT_ROOT"
+echo ""
+
+# 检查 node
+if ! command -v node &> /dev/null; then
+  echo "错误: 未找到 node,请先安装 Node.js"
+  exit 1
+fi
+
+# 检查/安装 puppeteer-core
+if ! node -e "require('puppeteer-core')" 2>/dev/null; then
+  echo "正在安装 puppeteer-core..."
+  cd "$PROJECT_ROOT"
+  npm install puppeteer-core --save-dev --no-fund --no-audit
+  echo ""
+fi
+
+echo "开始截图..."
+echo ""
+
+node "$SCRIPT_DIR/screenshot.js"
+
+echo ""
+echo "=== 完成 ==="
+echo "PDF文件位置: $PROJECT_ROOT/prd/nwafu-exam-demoImages-202604.pdf"

+ 197 - 0
assets/script2doc/screenshot.js

@@ -0,0 +1,197 @@
+/**
+ * 自动截图脚本 - 遍历所有角色和页面,生成截图并拼接为横版A4 PDF
+ * 依赖:puppeteer-core (使用本地 Chrome)
+ * 用法:node screenshot.js
+ */
+const puppeteer = require('puppeteer-core');
+const path = require('path');
+const fs = require('fs');
+
+const PROTO_PATH = path.resolve(__dirname, '../../prototype/index.html');
+const OUTPUT_DIR = path.resolve(__dirname, '../../prd');
+const OUTPUT_PDF = path.join(OUTPUT_DIR, 'nwafu-exam-demoImages-202604.pdf');
+const TEMP_DIR = path.join(__dirname, '_temp_screenshots');
+
+// 本地 Chrome 路径
+const CHROME_PATH = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
+
+// 角色列表
+const ROLES = [
+  { key: 'newGraduate', label: '研究生新入学' },
+  { key: 'graduateSenior', label: '研究生在读(研四)' },
+  { key: 'newUndergraduate', label: '本科生新入学' },
+  { key: 'undergraduateSenior', label: '本科生在读(大四)' },
+  { key: 'newStaff', label: '教职工新入职' },
+  { key: 'existingStaff', label: '教职工存量' }
+];
+
+// 页面列表
+const PAGES = [
+  { key: 'workbench', label: '工作台' },
+  { key: 'statistics', label: '数据统计' },
+  { key: 'approval', label: '人员审批' }
+];
+
+(async () => {
+  // 确保临时目录存在
+  if (!fs.existsSync(TEMP_DIR)) fs.mkdirSync(TEMP_DIR, { recursive: true });
+
+  const browser = await puppeteer.launch({
+    headless: 'new',
+    executablePath: CHROME_PATH,
+    args: ['--no-sandbox', '--disable-setuid-sandbox']
+  });
+
+  const page = await browser.newPage();
+  // 横版 1920x1080
+  await page.setViewport({ width: 1920, height: 1080 });
+
+  const fileUrl = 'file://' + PROTO_PATH;
+  await page.goto(fileUrl, { waitUntil: 'networkidle0', timeout: 30000 });
+
+  // 等待 Vue 渲染
+  await page.waitForSelector('#app .layout', { timeout: 10000 });
+
+  const screenshots = [];
+
+  for (const role of ROLES) {
+    // 切换角色
+    await page.evaluate((roleKey) => {
+      const app = document.querySelector('#app').__vue__;
+      app.switchRole(roleKey);
+    }, role.key);
+    await sleep(300);
+
+    // 关闭所有弹窗
+    await page.evaluate(() => {
+      const app = document.querySelector('#app').__vue__;
+      app.showBindDialog = false;
+      app.showGraduateReport = false;
+      app.showRejectDialog = false;
+    });
+    await sleep(300);
+
+    for (const pg of PAGES) {
+      // 切换页面
+      await page.evaluate((pageKey) => {
+        const app = document.querySelector('#app').__vue__;
+        app.activePage = pageKey;
+      }, pg.key);
+      await sleep(800);
+
+      // 如果是统计页面,等待图表渲染
+      if (pg.key === 'statistics') {
+        await page.evaluate(() => {
+          const app = document.querySelector('#app').__vue__;
+          if (app.renderStatsCharts) app.renderStatsCharts();
+        });
+        await sleep(1000);
+      }
+
+      const filename = `${role.key}_${pg.key}.png`;
+      const filepath = path.join(TEMP_DIR, filename);
+
+      // 隐藏左侧菜单栏,只截取主内容区
+      await page.evaluate(() => {
+        const sidebar = document.querySelector('.sidebar');
+        if (sidebar) sidebar.style.display = 'none';
+        const mainWrapper = document.querySelector('.main-wrapper');
+        if (mainWrapper) mainWrapper.style.marginLeft = '0';
+      });
+      await sleep(200);
+
+      await page.screenshot({ path: filepath, fullPage: false });
+
+      // 恢复侧边栏
+      await page.evaluate(() => {
+        const sidebar = document.querySelector('.sidebar');
+        if (sidebar) sidebar.style.display = '';
+        const mainWrapper = document.querySelector('.main-wrapper');
+        if (mainWrapper) mainWrapper.style.marginLeft = '';
+      });
+
+      screenshots.push({ filepath, title: `${role.label} - ${pg.label}` });
+      console.log(`  截图完成: ${role.label} - ${pg.label}`);
+    }
+  }
+
+  // 生成横版 A4 PDF (297mm x 210mm)
+  console.log('\n正在生成PDF...');
+
+  const pdfPage = await browser.newPage();
+  await pdfPage.setViewport({ width: 1920, height: 1080 });
+
+  // 构建 HTML 页面,每张截图占一页
+  const imagesHtml = screenshots.map(s => {
+    const imgData = fs.readFileSync(s.filepath);
+    const base64 = imgData.toString('base64');
+    return `
+      <div class="page">
+        <div class="page-title">${s.title}</div>
+        <img src="data:image/png;base64,${base64}" />
+      </div>
+    `;
+  }).join('');
+
+  const html = `<!DOCTYPE html>
+<html>
+<head>
+<style>
+  * { margin: 0; padding: 0; box-sizing: border-box; }
+  body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; }
+  .page {
+    width: 297mm;
+    height: 210mm;
+    padding: 8mm;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    page-break-after: always;
+    overflow: hidden;
+  }
+  .page:last-child { page-break-after: auto; }
+  .page-title {
+    font-size: 14px;
+    font-weight: 600;
+    color: #333;
+    margin-bottom: 6mm;
+    text-align: center;
+  }
+  .page img {
+    max-width: 100%;
+    max-height: 185mm;
+    object-fit: contain;
+    border: 1px solid #e0e0e0;
+    border-radius: 4px;
+  }
+</style>
+</head>
+<body>${imagesHtml}</body>
+</html>`;
+
+  await pdfPage.setContent(html, { waitUntil: 'networkidle0' });
+
+  await pdfPage.pdf({
+    path: OUTPUT_PDF,
+    landscape: true,
+    format: 'A4',
+    printBackground: true,
+    margin: { top: '0', bottom: '0', left: '0', right: '0' }
+  });
+
+  console.log(`PDF已生成: ${OUTPUT_PDF}`);
+
+  // 清理临时文件
+  for (const s of screenshots) {
+    fs.unlinkSync(s.filepath);
+  }
+  fs.rmdirSync(TEMP_DIR);
+  console.log('临时文件已清理');
+
+  await browser.close();
+})();
+
+function sleep(ms) {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 1014 - 0
package-lock.json


+ 5 - 0
package.json

@@ -0,0 +1,5 @@
+{
+  "devDependencies": {
+    "puppeteer-core": "^24.43.0"
+  }
+}

BIN=BIN
prd/nwafu-exam-demoImages-202604.pdf


BIN=BIN
prd/nwafu-exam-req-202604.docx


BIN=BIN
prd/nwafu-exam-req-202604.pdf


+ 85 - 13
prd/prd.md

@@ -7,7 +7,7 @@
 | 项目名称 | 西农实验室安全智慧化管控系统 - 安全教育与考试系统 |
 | 文档版本 | V1.0 |
 | 编写日期 | 2025年5月 |
-| 文档状态 | 评审 |
+| 文档状态 | 评审 |
 
 ---
 
@@ -239,7 +239,9 @@
 
 #### 3.5.1 考试情况统计
 
-**切换维度:** 研究生 / 本科生 Radio 按钮切换
+**切换维度:** 研究生 / 本科生 / 教职工 Radio 按钮切换
+
+**学年选择:** 下拉选择学年(如 2025-2026、2024-2025 等),切换后图表和报表数据联动更新
 
 **图表区:**
 - 左侧:堆叠柱状图,X轴为各学院,Y轴为人数,分通过/未通过/未考三色堆叠,柱体顶部显示数值
@@ -263,9 +265,25 @@
 | XX学院 | 应知应会考试 | N | N | N | N | N% |
 | XX学院 | 汇总 | N | N | N | N | N% |
 
+**教职工报表:** 排版与研究生一致
+
+| 学院 | 考试类型 | 应考人数 | 通过人数 | 未通过人数 | 未考人数 | 通过率 |
+|------|---------|---------|---------|-----------|---------|--------|
+| XX学院 | 新入职教职工 | N | N | N | N | N% |
+| XX学院 | 存量教职工 | N | N | N | N | N% |
+| XX学院 | 汇总 | N | N | N | N | N% |
+
 **通过率:** 以百分比数字展示,不使用进度条
 
-**导出:** 右上角"导出数据"按钮
+**导出:** 右上角"导出数据"按钮,导出当前选中维度(研究生/本科生/教职工)和学年的完整报表,格式为 Excel:
+
+| 学年 | 学院 | 考试类型 | 应考人数 | 通过人数 | 未通过人数 | 未考人数 | 通过率 |
+|------|------|---------|---------|---------|-----------|---------|--------|
+| 2025-2026 | XX学院 | 新生分级考试 | N | N | N | N | N% |
+| 2025-2026 | XX学院 | 应知应会考试 | N | N | N | N | N% |
+| 2025-2026 | XX学院 | 汇总 | N | N | N | N | N% |
+
+文件名格式:`考试情况统计_研究生_2025-2026学年.xlsx`
 
 #### 3.5.2 实验室与人员匹配
 
@@ -281,6 +299,22 @@
 - 导出各学院数据
 - 导出实验室人员明细
 
+**导出各学院数据格式:**
+
+| 学院 | Ⅰ级(重大风险)人数 | Ⅱ级(高风险)人数 | Ⅲ级(中风险)人数 | Ⅳ级(低风险)人数 | 总人数 |
+|------|-------------------|-------------------|-------------------|-------------------|--------|
+| XX学院 | N | N | N | N | N |
+
+文件名格式:`实验室人员匹配_各学院统计.xlsx`
+
+**导出实验室人员明细格式:**
+
+| 姓名 | 学号/工号 | 用户类型 | 所属学院 | 绑定实验室 | 实验室房间号 | 实验室等级 | 实验室分类 | 绑定状态 |
+|------|----------|---------|---------|-----------|------------|-----------|-----------|---------|
+| XX | XX | 研究生 | XX学院 | XX实验室 | A301 | Ⅱ级 | 化学类 | 已通过 |
+
+文件名格式:`实验室人员匹配_人员明细.xlsx`
+
 #### 3.5.3 学时统计
 
 **图表:** 分组柱状图,X轴为各学院,按研究生/本科生/教职工分组展示学习总时长(单位:学时),柱体顶部显示数值
@@ -297,7 +331,46 @@
 - 年级:下拉选择
 - 学号/姓名:模糊检索输入框
 
-**导出:** 右上角"导出数据"按钮
+**导出:** 右上角"导出数据"按钮,导出当前筛选条件下的全部学时明细(不受分页限制),格式为 Excel:
+
+| 姓名 | 学号/工号 | 用户类型 | 所属学院 | 年级 | 第一学年(学时) | 第二学年(学时) | 第三学年(学时) | 第四学年(学时) | 累计总学时 |
+|------|----------|---------|---------|------|--------------|--------------|--------------|--------------|-----------|
+| XX | XX | 研究生 | XX学院 | XX级 | N | N | N | N | N |
+
+文件名格式:`学时统计明细_全部.xlsx`(如有筛选则文件名带筛选条件,如 `学时统计明细_研究生_化学与化工学院.xlsx`)
+
+#### 3.5.4 毕业教育考试数据统计报告
+
+**适用角色:** 毕业年级学生(研四/本科大四)
+
+**入口位置:** 工作台"学年数据统计"卡片右上角,显示"毕业报告"按钮(奖杯图标),仅毕业年级角色可见
+
+**交互流程:**
+1. 用户点击"毕业报告"按钮
+2. 弹窗加载并展示证书风格的报告内容
+3. 用户可预览报告全貌
+4. 点击"下载PDF"按钮,前端生成PDF文件并下载
+
+**报告内容:**
+
+报告采用荣誉证书风格排版,包含以下信息:
+
+- **顶部:** 学校LOGO(西北农林科技大学)居中展示
+- **标题:** "实验室安全教育毕业报告"
+- **个人信息区:** 姓名、学号、学院、年级、用户类型
+- **学年学时完成情况表:** 按学年列出每年完成学时与要求学时,底部汇总累计学时
+- **考试通过记录表:** 按学年列出考试类型与成绩
+- **评语:** "该同学在校期间认真完成实验室安全教育各项学习任务,累计完成 N 学时安全培训,通过全部年度安全考核,具备良好的实验室安全意识和规范操作能力,特此证明。"
+- **署名:** 实验室安全与条件保障处
+- **时间:** 当前学年7月(如2026年7月)
+
+**视觉风格:**
+- 金色双线边框,米白色背景
+- 标题使用衬线字体,大号居中
+- 个人信息用浅色卡片包裹
+- 右下角红色圆形印章样式
+
+**PDF生成:** 使用 html2canvas + jsPDF 纯前端方案,将报告DOM截图转为A4尺寸PDF下载,文件名格式:`毕业报告_姓名_学号.pdf`
 
 ---
 
@@ -356,10 +429,10 @@
 
 ### 5.1 技术要求
 
-- 前端技术栈:Vue 2 + Element UI
+- 前端技术栈:Vue 2 + Element UI  //与当前考试系统采用相同技术栈即可
 - 支持响应式布局(适配1920px、1440px、1100px、768px)
-- 图表库:ECharts
-- 与主系统通过统一身份认证对接
+- 图表库:ECharts //同类charts即可
+- 与主系统通过统一身份认证对接。//这个目前已经实现
 
 ### 5.2 安全要求
 
@@ -403,9 +476,8 @@
 
 | 序号 | 交付物 | 说明 |
 |------|--------|------|
-| 1 | 源代码 | 前端Vue2项目完整代码 |
-| 2 | 接口文档 | RESTful API设计文档 |
-| 3 | 数据库设计 | ER图及DDL脚本 |
-| 4 | 部署文档 | 部署步骤及环境要求 |
-| 5 | 测试报告 | 功能测试、性能测试报告 |
-| 6 | 用户手册 | 各角色操作指南 |
+| 1 | 接口文档 | RESTful API设计文档 |
+| 2 | 数据库设计 | ER图及DDL脚本 |
+| 3 | 部署文档 | 部署步骤及环境要求 |
+| 4 | 测试报告 | 功能测试、性能测试报告 |
+| 5 | 用户手册 | 各角色操作指南 |

+ 105 - 0
prototype/css/styles.css

@@ -146,6 +146,11 @@ html, body { height: 100%; font-family: 'PingFang SC', 'Microsoft YaHei', sans-s
 }
 .feature-card h3 { margin: 0; font-size: 15px; color: #25324a; }
 .feature-card > p { margin: 0; font-size: 12px; color: #60708a; line-height: 1.5; }
+.feature-card-header { display: flex; align-items: center; justify-content: space-between; }
+.feature-card-header h3 { margin: 0; font-size: 15px; color: #25324a; }
+.report-btn { color: #e6a23c; font-weight: 600; font-size: 13px; padding: 0; }
+.report-btn:hover { color: #c78c1c; }
+.report-btn i { margin-right: 2px; }
 
 /* Record/Task Items */
 .record-list { display: flex; flex-direction: column; gap: 8px; flex: 1; min-height: 0; }
@@ -387,3 +392,103 @@ html, body { height: 100%; font-family: 'PingFang SC', 'Microsoft YaHei', sans-s
   .meta-grid { grid-template-columns: 1fr; }
   .kpi-row { grid-template-columns: 1fr; }
 }
+
+/* 毕业报告证书样式 */
+.report-wrapper { padding: 10px; }
+.report-certificate {
+  background: #fffdf5;
+  border: 3px double #c9a84c;
+  border-radius: 8px;
+  padding: 40px 48px;
+  position: relative;
+}
+.report-logo { text-align: center; margin-bottom: 16px; background: #1a6e3a; border-radius: 6px; padding: 12px 20px; }
+.report-logo img { height: 60px; object-fit: contain; }
+.report-title {
+  text-align: center;
+  font-size: 22px;
+  font-weight: 700;
+  color: #8b4513;
+  letter-spacing: 4px;
+  margin-bottom: 24px;
+  font-family: 'SimSun', 'STSong', serif;
+}
+.report-info-card {
+  background: #faf6ec;
+  border: 1px solid #e8dcc8;
+  border-radius: 6px;
+  padding: 14px 20px;
+  margin-bottom: 20px;
+}
+.report-info-row {
+  display: flex;
+  gap: 40px;
+  margin-bottom: 6px;
+  font-size: 14px;
+  color: #4a3c2a;
+}
+.report-info-row:last-child { margin-bottom: 0; }
+.report-section-title {
+  font-size: 14px;
+  color: #8b4513;
+  margin: 18px 0 10px;
+  padding-left: 8px;
+  border-left: 3px solid #c9a84c;
+}
+.report-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 13px;
+  margin-bottom: 8px;
+}
+.report-table th {
+  background: #f5edd8;
+  color: #5a4320;
+  font-weight: 600;
+  padding: 8px 12px;
+  border-bottom: 1px solid #d4c5a0;
+  text-align: center;
+}
+.report-table td {
+  padding: 7px 12px;
+  border-bottom: 1px solid #ebe3d0;
+  text-align: center;
+  color: #4a3c2a;
+}
+.report-table tfoot td {
+  font-weight: 600;
+  background: #faf6ec;
+  border-top: 1px solid #d4c5a0;
+}
+.report-comment {
+  margin: 24px 0;
+  font-size: 14px;
+  line-height: 1.8;
+  color: #4a3c2a;
+  text-indent: 2em;
+}
+.report-footer {
+  display: flex;
+  justify-content: flex-end;
+  align-items: flex-end;
+  gap: 30px;
+  margin-top: 30px;
+  padding-right: 20px;
+}
+.report-sign { text-align: right; font-size: 14px; color: #4a3c2a; }
+.report-sign p { margin-bottom: 4px; }
+.report-stamp {
+  width: 80px;
+  height: 80px;
+  border: 2px dashed #c0392b;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #c0392b;
+  font-size: 11px;
+  font-weight: 600;
+  text-align: center;
+  line-height: 1.3;
+  transform: rotate(-15deg);
+}

+ 81 - 1
prototype/index.html

@@ -149,7 +149,10 @@
             <div class="feature-grid">
               <!-- 学年数据统计 -->
               <article class="feature-card">
-                <h3>学年数据统计</h3>
+                <div class="feature-card-header">
+                  <h3>学年数据统计</h3>
+                  <el-button v-if="canShowGraduateReport" type="text" size="mini" class="report-btn" @click="showGraduateReport = true"><i class="el-icon-trophy"></i> 毕业报告</el-button>
+                </div>
                 <p>按学年统计历年考试通过情况与成绩</p>
                 <div class="record-list record-list--fixed">
                   <div class="record-item" v-for="(item, idx) in currentRole.yearlyRecords" :key="idx">
@@ -210,6 +213,7 @@
                 <el-button v-if="currentRole.examUnlocked && currentRole.examStatus !== '已通过'" type="primary" size="small" style="margin-top:auto" @click="simulatePassExam">模拟通过考试</el-button>
                 <el-button v-if="!currentRole.examUnlocked && currentRole.requiredHours > 0" type="info" size="small" disabled style="margin-top:auto">完成学时后解锁</el-button>
                 <el-button v-if="activeRole === 'newUndergraduate' && currentRole.examStatus === '已通过'" type="warning" size="small" style="margin-top:8px" @click="openBindDialog('undergraduate')">绑定实验室</el-button>
+                <el-button v-if="!currentRole.examUnlocked && currentRole.requiredHours > 0" type="text" size="mini" style="margin-top:4px;color:#909399" @click="simulateCompleteAndPass">[ 模拟完成学时并通过考试 ]</el-button>
               </article>
             </div>
           </section>
@@ -304,9 +308,13 @@
                 <div class="stats-section-header">
                   <h3>考试情况统计</h3>
                   <div class="stats-section-actions">
+                    <el-select v-model="examYear" size="mini" style="width:130px;margin-right:8px">
+                      <el-option v-for="y in statisticsData.examYears" :key="y" :label="y + ' 学年'" :value="y"></el-option>
+                    </el-select>
                     <el-radio-group v-model="statsExamType" size="mini" @change="renderStatsCharts">
                       <el-radio-button label="graduate">研究生</el-radio-button>
                       <el-radio-button label="undergraduate">本科生</el-radio-button>
+                      <el-radio-button label="staff">教职工</el-radio-button>
                     </el-radio-group>
                     <el-button size="mini" type="primary" icon="el-icon-download">导出数据</el-button>
                   </div>
@@ -324,6 +332,8 @@
                     <el-option v-if="statsExamType === 'graduate'" label="应知应会考试" value="应知应会考试"></el-option>
                     <el-option v-if="statsExamType === 'undergraduate'" label="新生入学考试" value="新生入学考试"></el-option>
                     <el-option v-if="statsExamType === 'undergraduate'" label="应知应会考试" value="应知应会考试"></el-option>
+                    <el-option v-if="statsExamType === 'staff'" label="新入职教职工" value="新入职教职工"></el-option>
+                    <el-option v-if="statsExamType === 'staff'" label="存量教职工" value="存量教职工"></el-option>
                     <el-option label="汇总" value="汇总"></el-option>
                   </el-select>
                 </div>
@@ -347,6 +357,16 @@
                   <el-table-column prop="absent" label="未考人数" min-width="90"></el-table-column>
                   <el-table-column prop="rate" label="通过率" min-width="80"></el-table-column>
                 </el-table>
+                <!-- 教职工 -->
+                <el-table v-if="statsExamType === 'staff'" :data="filteredStaffData" border size="small" style="width:100%;margin-top:8px" :span-method="examSpanMethod">
+                  <el-table-column prop="college" label="学院" min-width="150"></el-table-column>
+                  <el-table-column prop="examType" label="考试类型" min-width="120"></el-table-column>
+                  <el-table-column prop="total" label="应考人数" min-width="90"></el-table-column>
+                  <el-table-column prop="passed" label="通过人数" min-width="90"></el-table-column>
+                  <el-table-column prop="failed" label="未通过人数" min-width="100"></el-table-column>
+                  <el-table-column prop="absent" label="未考人数" min-width="90"></el-table-column>
+                  <el-table-column prop="rate" label="通过率" min-width="80"></el-table-column>
+                </el-table>
               </section>
 
               <!-- 实验室与人员匹配 -->
@@ -620,6 +640,64 @@
         <el-button type="danger" @click="confirmReject" :disabled="!rejectReasonInput.trim()">确认驳回</el-button>
       </span>
     </el-dialog>
+
+    <!-- 毕业报告弹窗 -->
+    <el-dialog title="毕业教育考试数据统计报告" :visible.sync="showGraduateReport" width="740px" top="3vh">
+      <div class="report-wrapper" ref="reportContent" v-if="graduateReportData">
+        <div class="report-certificate">
+          <div class="report-logo">
+            <img src="../assets/images/logo.png" alt="西北农林科技大学">
+          </div>
+          <h1 class="report-title">实验室安全教育毕业报告</h1>
+          <div class="report-info-card">
+            <div class="report-info-row">
+              <span><b>姓名:</b>{{ graduateReportData.name }}</span>
+              <span><b>学号:</b>{{ graduateReportData.code }}</span>
+            </div>
+            <div class="report-info-row">
+              <span><b>学院:</b>{{ graduateReportData.org }}</span>
+              <span><b>年级:</b>{{ graduateReportData.grade }}</span>
+            </div>
+            <div class="report-info-row">
+              <span><b>用户类型:</b>{{ graduateReportData.type }}</span>
+            </div>
+          </div>
+          <h3 class="report-section-title">学年学时完成情况</h3>
+          <table class="report-table">
+            <thead><tr><th>学年</th><th>完成学时</th><th>要求学时</th></tr></thead>
+            <tbody>
+              <tr v-for="h in graduateReportData.yearlyHours" :key="h.year">
+                <td>{{ h.year }}</td><td>{{ h.completed }}</td><td>{{ h.required }}</td>
+              </tr>
+            </tbody>
+            <tfoot><tr><td><b>累计</b></td><td colspan="2"><b>{{ graduateReportData.totalHours }} 学时</b></td></tr></tfoot>
+          </table>
+          <h3 class="report-section-title">考试通过记录</h3>
+          <table class="report-table">
+            <thead><tr><th>学年</th><th>考试类型</th><th>成绩</th></tr></thead>
+            <tbody>
+              <tr v-for="r in graduateReportData.examRecords" :key="r.year">
+                <td>{{ r.year }}</td><td>{{ graduateReportData.examType }}</td><td>{{ r.score }}</td>
+              </tr>
+            </tbody>
+          </table>
+          <p class="report-comment">该同学在校期间认真完成实验室安全教育各项学习任务,累计完成 <b>{{ graduateReportData.totalHours }}</b> 学时安全培训,通过全部年度安全考核,具备良好的实验室安全意识和规范操作能力,特此证明。</p>
+          <div class="report-footer">
+            <div class="report-sign">
+              <p>实验室安全与条件保障处</p>
+              <p>2026年7月</p>
+            </div>
+            <div class="report-stamp">
+              <span>实验室安全与<br>条件保障处</span>
+            </div>
+          </div>
+        </div>
+      </div>
+      <span slot="footer">
+        <el-button @click="showGraduateReport = false">关闭</el-button>
+        <el-button type="primary" icon="el-icon-download" @click="downloadReport">下载PDF</el-button>
+      </span>
+    </el-dialog>
   </div>
 
   <!-- Vendor Scripts (CDN - 生产环境应本地化) -->
@@ -627,6 +705,8 @@
   <script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
   <script src="./vendor/echarts.min.js"></script>
   <script src="https://unpkg.com/marked@9.1.6/marked.min.js"></script>
+  <script src="https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
+  <script src="https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
   <!-- App Scripts -->
   <script src="./js/data.js"></script>
   <script src="./js/app.js"></script>

+ 116 - 12
prototype/js/app.js

@@ -34,6 +34,7 @@ new Vue({
       statsExamType: 'graduate',
       examFilterCollege: '',
       examFilterExamType: '',
+      examYear: '2025-2026',
       labFilterCollege: '',
       labFilterLevel: '',
       hoursFilterType: '',
@@ -48,6 +49,8 @@ new Vue({
       showRejectDialog: false,
       rejectReasonInput: '',
       rejectingItem: null,
+      // 毕业报告
+      showGraduateReport: false,
       // 配置表
       configTableData: [
         { level: 'Ⅰ', levelLabel: 'Ⅰ级(重大风险)', firstYear: levelHoursConfig['Ⅰ'].firstYear, annual: levelHoursConfig['Ⅰ'].annual },
@@ -214,14 +217,16 @@ new Vue({
     },
     undergraduateDetailData: function() {
       var rows = [];
-      statisticsData.undergraduateExam.forEach(function(d) {
-        var freshRate = Math.round(d.freshPassed / d.freshTotal * 100) + '%';
-        var seniorRate = Math.round(d.seniorPassed / d.seniorTotal * 100) + '%';
+      var source = this.currentYearExamData.undergraduateExam;
+      if (!source) return rows;
+      source.forEach(function(d) {
+        var freshRate = d.freshTotal > 0 ? Math.round(d.freshPassed / d.freshTotal * 100) + '%' : '—';
+        var seniorRate = d.seniorTotal > 0 ? Math.round(d.seniorPassed / d.seniorTotal * 100) + '%' : '—';
         var allTotal = d.freshTotal + d.seniorTotal;
         var allPassed = d.freshPassed + d.seniorPassed;
         var allFailed = d.freshFailed + d.seniorFailed;
         var allAbsent = d.freshAbsent + d.seniorAbsent;
-        var allRate = Math.round(allPassed / allTotal * 100) + '%';
+        var allRate = allTotal > 0 ? Math.round(allPassed / allTotal * 100) + '%' : '—';
         rows.push({ college: d.college, examType: '新生入学考试', total: d.freshTotal, passed: d.freshPassed, failed: d.freshFailed, absent: d.freshAbsent, rate: freshRate });
         rows.push({ college: d.college, examType: '应知应会考试', total: d.seniorTotal, passed: d.seniorPassed, failed: d.seniorFailed, absent: d.seniorAbsent, rate: seniorRate });
         rows.push({ college: d.college, examType: '汇总', total: allTotal, passed: allPassed, failed: allFailed, absent: allAbsent, rate: allRate });
@@ -230,14 +235,16 @@ new Vue({
     },
     graduateDetailData: function() {
       var rows = [];
-      statisticsData.graduateExamDetail.forEach(function(d) {
-        var newRate = Math.round(d.newPassed / d.newTotal * 100) + '%';
-        var seniorRate = Math.round(d.seniorPassed / d.seniorTotal * 100) + '%';
+      var source = this.currentYearExamData.graduateExamDetail;
+      if (!source) return rows;
+      source.forEach(function(d) {
+        var newRate = d.newTotal > 0 ? Math.round(d.newPassed / d.newTotal * 100) + '%' : '—';
+        var seniorRate = d.seniorTotal > 0 ? Math.round(d.seniorPassed / d.seniorTotal * 100) + '%' : '—';
         var allTotal = d.newTotal + d.seniorTotal;
         var allPassed = d.newPassed + d.seniorPassed;
         var allFailed = d.newFailed + d.seniorFailed;
         var allAbsent = d.newAbsent + d.seniorAbsent;
-        var allRate = Math.round(allPassed / allTotal * 100) + '%';
+        var allRate = allTotal > 0 ? Math.round(allPassed / allTotal * 100) + '%' : '—';
         rows.push({ college: d.college, examType: '新生分级考试', total: d.newTotal, passed: d.newPassed, failed: d.newFailed, absent: d.newAbsent, rate: newRate });
         rows.push({ college: d.college, examType: '应知应会考试', total: d.seniorTotal, passed: d.seniorPassed, failed: d.seniorFailed, absent: d.seniorAbsent, rate: seniorRate });
         rows.push({ college: d.college, examType: '汇总', total: allTotal, passed: allPassed, failed: allFailed, absent: allAbsent, rate: allRate });
@@ -265,6 +272,42 @@ new Vue({
       if (examType) data = data.filter(function(d) { return d.examType === examType; });
       return data;
     },
+    staffDetailData: function() {
+      var rows = [];
+      var source = this.currentYearExamData.staffExam;
+      if (!source) return rows;
+      source.forEach(function(d) {
+        var newRate = d.newTotal > 0 ? Math.round(d.newPassed / d.newTotal * 100) + '%' : '—';
+        var seniorRate = d.seniorTotal > 0 ? Math.round(d.seniorPassed / d.seniorTotal * 100) + '%' : '—';
+        var allTotal = d.newTotal + d.seniorTotal;
+        var allPassed = d.newPassed + d.seniorPassed;
+        var allFailed = d.newFailed + d.seniorFailed;
+        var allAbsent = d.newAbsent + d.seniorAbsent;
+        var allRate = allTotal > 0 ? Math.round(allPassed / allTotal * 100) + '%' : '—';
+        rows.push({ college: d.college, examType: '新入职教职工', total: d.newTotal, passed: d.newPassed, failed: d.newFailed, absent: d.newAbsent, rate: newRate });
+        rows.push({ college: d.college, examType: '存量教职工', total: d.seniorTotal, passed: d.seniorPassed, failed: d.seniorFailed, absent: d.seniorAbsent, rate: seniorRate });
+        rows.push({ college: d.college, examType: '汇总', total: allTotal, passed: allPassed, failed: allFailed, absent: allAbsent, rate: allRate });
+      });
+      return rows;
+    },
+    filteredStaffData: function() {
+      var data = this.staffDetailData;
+      var college = this.examFilterCollege;
+      var examType = this.examFilterExamType;
+      if (college) data = data.filter(function(d) { return d.college === college; });
+      if (examType) data = data.filter(function(d) { return d.examType === examType; });
+      return data;
+    },
+    currentYearExamData: function() {
+      if (this.examYear === '2025-2026') {
+        return {
+          graduateExamDetail: statisticsData.graduateExamDetail,
+          undergraduateExam: statisticsData.undergraduateExam,
+          staffExam: statisticsData.staffExam
+        };
+      }
+      return statisticsData.examHistoryData[this.examYear] || { graduateExamDetail: [], undergraduateExam: [], staffExam: [] };
+    },
     filteredLabCollegeData: function() {
       var data = this.labCollegeData;
       var college = this.labFilterCollege;
@@ -299,6 +342,26 @@ new Vue({
     filteredHoursDetail: function() {
       var start = (this.hoursPage - 1) * this.hoursPageSize;
       return this.hoursDetailFiltered.slice(start, start + this.hoursPageSize);
+    },
+    graduateReportData: function() {
+      var role = this.currentRole;
+      if (!role || !role.yearlyHours) return null;
+      var totalHours = role.yearlyHours.reduce(function(s, h) { return s + h.completed; }, 0);
+      var examRecords = (role.yearlyRecords || []).slice().reverse();
+      return {
+        name: role.name,
+        code: role.code,
+        org: role.org,
+        type: role.type,
+        grade: role.grade || '',
+        yearlyHours: role.yearlyHours,
+        totalHours: totalHours,
+        examRecords: examRecords,
+        examType: role.examType
+      };
+    },
+    canShowGraduateReport: function() {
+      return this.activeRole === 'graduateSenior' || this.activeRole === 'undergraduateSenior';
     }
   },
   methods: {
@@ -340,6 +403,19 @@ new Vue({
         this.$notify({ title: '提示', message: '恭喜通过新生入学考试!如需进入实验室开展科研,请点击"绑定实验室"按钮。', type: 'success', duration: 5000 });
       }
     },
+    simulateCompleteAndPass: function() {
+      var role = this.rolesData[this.activeRole];
+      // 模拟学时完成
+      role.completedHours = role.requiredHours;
+      role.examUnlocked = true;
+      // 更新任务状态
+      var hoursTask = role.tasks.find(function(t) { return t.title.indexOf('学时') !== -1 || t.title.indexOf('通识') !== -1; });
+      if (hoursTask) { hoursTask.note = role.requiredHours + ' / ' + role.requiredHours + ' 学时(已完成)'; hoursTask.status = 'in-progress'; }
+      this.$message.success('学时已完成 ' + role.requiredHours + '/' + role.requiredHours);
+      // 模拟考试通过
+      var self = this;
+      setTimeout(function() { self.simulatePassExam(); }, 800);
+    },
     confirmBind: function() {
       var role = this.rolesData[this.activeRole];
       if (this.noLabMode && this.bindDialogMode === 'graduate') {
@@ -430,9 +506,15 @@ new Vue({
       var barEl = this.$refs.examBarChart;
       var pieEl = this.$refs.examPieChart;
       if (!barEl || !pieEl) return;
-      var data = this.statsExamType === 'graduate'
-        ? statisticsData.graduateExamDetail.map(function(d) { return { college: d.college, passed: d.newPassed + d.seniorPassed, failed: d.newFailed + d.seniorFailed, absent: d.newAbsent + d.seniorAbsent }; })
-        : statisticsData.undergraduateExam.map(function(d) { return { college: d.college, passed: d.freshPassed + d.seniorPassed, failed: d.freshFailed + d.seniorFailed, absent: d.freshAbsent + d.seniorAbsent }; });
+      var yearData = this.currentYearExamData;
+      var data;
+      if (this.statsExamType === 'graduate') {
+        data = (yearData.graduateExamDetail || []).map(function(d) { return { college: d.college, passed: d.newPassed + d.seniorPassed, failed: d.newFailed + d.seniorFailed, absent: d.newAbsent + d.seniorAbsent }; });
+      } else if (this.statsExamType === 'undergraduate') {
+        data = (yearData.undergraduateExam || []).map(function(d) { return { college: d.college, passed: d.freshPassed + d.seniorPassed, failed: d.freshFailed + d.seniorFailed, absent: d.freshAbsent + d.seniorAbsent }; });
+      } else {
+        data = (yearData.staffExam || []).map(function(d) { return { college: d.college, passed: d.newPassed + d.seniorPassed, failed: d.newFailed + d.seniorFailed, absent: d.newAbsent + d.seniorAbsent }; });
+      }
       var colleges = data.map(function(d) { return d.college.replace('学院',''); });
       var barChart = echarts.init(barEl);
       barChart.setOption({
@@ -530,6 +612,27 @@ new Vue({
       this.showRejectDialog = false;
       this.rejectingItem = null;
       this.rejectReasonInput = '';
+    },
+    downloadReport: function() {
+      var el = this.$refs.reportContent;
+      if (!el) return;
+      var role = this.currentRole;
+      var self = this;
+      this.$message({ message: '正在生成PDF,请稍候...', type: 'info' });
+      html2canvas(el, { scale: 2, useCORS: true, backgroundColor: '#fffdf5' }).then(function(canvas) {
+        var imgData = canvas.toDataURL('image/png');
+        var pdf = new jspdf.jsPDF('p', 'mm', 'a4');
+        var pdfWidth = pdf.internal.pageSize.getWidth();
+        var pdfHeight = pdf.internal.pageSize.getHeight();
+        var imgWidth = pdfWidth - 20;
+        var imgHeight = canvas.height * imgWidth / canvas.width;
+        if (imgHeight > pdfHeight - 20) imgHeight = pdfHeight - 20;
+        var x = (pdfWidth - imgWidth) / 2;
+        var y = (pdfHeight - imgHeight) / 2;
+        pdf.addImage(imgData, 'PNG', x, y, imgWidth, imgHeight);
+        pdf.save('毕业报告_' + role.name + '_' + role.code + '.pdf');
+        self.$message.success('PDF下载成功');
+      });
     }
   },
   watch: {
@@ -543,7 +646,8 @@ new Vue({
         this.activeResourceTab = role.lab.category;
       }
     },
-    statsExamType: function() { this.renderStatsCharts(); }
+    statsExamType: function() { this.examFilterExamType = ''; this.renderStatsCharts(); },
+    examYear: function() { this.examFilterCollege = ''; this.examFilterExamType = ''; this.renderStatsCharts(); }
   },
   mounted: function() {
     var self = this;

+ 95 - 3
prototype/js/data.js

@@ -59,7 +59,7 @@ var roleProfiles = {
   },
   graduateSenior: {
     name: '李思涵', code: '2023100042', type: '硕士研究生', org: '生命科学学院',
-    term: '2025-2026 学年', isFirstYear: false,
+    term: '2025-2026 学年', isFirstYear: false, grade: '2023级',
     bindingStatus: 'approved',
     lab: { id: 'lab002', name: '分子生物学实验室', room: 'B205', building: '生命科学楼B栋2层', college: '生命科学学院', director: '李**', level: 'Ⅰ', category: '生物类', type: '科研实验室' },
     requiredHours: 8, completedHours: 6,
@@ -70,6 +70,12 @@ var roleProfiles = {
       { year: '2024-2025', score: '88', status: '通过' },
       { year: '2023-2024', score: '92', status: '通过' }
     ],
+    yearlyHours: [
+      { year: '2022-2023', completed: 24, required: 24 },
+      { year: '2023-2024', completed: 8, required: 8 },
+      { year: '2024-2025', completed: 8, required: 8 },
+      { year: '2025-2026', completed: 6, required: 8 }
+    ],
     tasks: [
       { title: '完成年度安全学时', note: '6 / 8 学时', status: 'in-progress' },
       { title: '通过应知应会考试', note: '完成学时后解锁', status: 'locked' },
@@ -117,7 +123,7 @@ var roleProfiles = {
   },
   undergraduateSenior: {
     name: '刘佳琪', code: '2022200089', type: '本科生', org: '材料科学学院',
-    term: '2025-2026 学年', isFirstYear: false,
+    term: '2025-2026 学年', isFirstYear: false, grade: '2022级',
     bindingStatus: 'none', lab: null,
     requiredHours: 0, completedHours: 0,
     examType: '应知应会考试', examStatus: '未开始',
@@ -128,6 +134,12 @@ var roleProfiles = {
       { year: '2023-2024', score: '85', status: '通过' },
       { year: '2022-2023', score: '78', status: '通过' }
     ],
+    yearlyHours: [
+      { year: '2022-2023', completed: 4, required: 4 },
+      { year: '2023-2024', completed: 4, required: 4 },
+      { year: '2024-2025', completed: 4, required: 4 },
+      { year: '2025-2026', completed: 0, required: 0 }
+    ],
     tasks: [
       { title: '通过年度应知应会考试', note: '可直接参加', status: 'pending' },
       { title: '实验室安全年度复训', note: '学院下发 · 待完成', status: 'pending' },
@@ -375,7 +387,7 @@ var videoData = {
 // 侧边栏菜单
 var menuItems = [
   { key: 'workbench', icon: 'el-icon-s-platform', label: '工作台' },
-  { key: 'statistics', icon: 'el-icon-s-data', label: '数据统计' },
+  { key: 'statistics', icon: 'el-icon-s-data', label: '数据统计', staffOnly: true },
   { key: 'approval', icon: 'el-icon-s-check', label: '人员审批', staffOnly: true },
   { key: 'config', icon: 'el-icon-setting', label: '系统配置' }
 ];
@@ -428,6 +440,86 @@ var statisticsData = {
     { college: '自动化学院', freshTotal: 48, freshPassed: 44, freshFailed: 2, freshAbsent: 2, seniorTotal: 117, seniorPassed: 106, seniorFailed: 6, seniorAbsent: 5 },
     { college: '资源环境学院', freshTotal: 42, freshPassed: 38, freshFailed: 2, freshAbsent: 2, seniorTotal: 103, seniorPassed: 95, seniorFailed: 4, seniorAbsent: 4 }
   ],
+  // 教职工考试情况(区分新入职教职工 + 存量教职工)
+  staffExam: [
+    { college: '化学与化工学院', newTotal: 8, newPassed: 7, newFailed: 1, newAbsent: 0, seniorTotal: 35, seniorPassed: 33, seniorFailed: 1, seniorAbsent: 1 },
+    { college: '生命科学学院', newTotal: 6, newPassed: 5, newFailed: 1, newAbsent: 0, seniorTotal: 28, seniorPassed: 26, seniorFailed: 1, seniorAbsent: 1 },
+    { college: '物理学院', newTotal: 5, newPassed: 5, newFailed: 0, newAbsent: 0, seniorTotal: 22, seniorPassed: 20, seniorFailed: 1, seniorAbsent: 1 },
+    { college: '电气工程学院', newTotal: 7, newPassed: 6, newFailed: 1, newAbsent: 0, seniorTotal: 30, seniorPassed: 28, seniorFailed: 1, seniorAbsent: 1 },
+    { college: '材料科学学院', newTotal: 4, newPassed: 4, newFailed: 0, newAbsent: 0, seniorTotal: 25, seniorPassed: 23, seniorFailed: 1, seniorAbsent: 1 },
+    { college: '机械工程学院', newTotal: 5, newPassed: 4, newFailed: 1, newAbsent: 0, seniorTotal: 20, seniorPassed: 19, seniorFailed: 1, seniorAbsent: 0 },
+    { college: '自动化学院', newTotal: 3, newPassed: 3, newFailed: 0, newAbsent: 0, seniorTotal: 18, seniorPassed: 17, seniorFailed: 1, seniorAbsent: 0 },
+    { college: '资源环境学院', newTotal: 4, newPassed: 3, newFailed: 1, newAbsent: 0, seniorTotal: 15, seniorPassed: 14, seniorFailed: 1, seniorAbsent: 0 }
+  ],
+  // 学年列表(倒序)
+  examYears: ['2025-2026', '2024-2025', '2023-2024', '2022-2023'],
+  // 历史学年考试数据
+  examHistoryData: {
+    '2024-2025': {
+      graduateExamDetail: [
+        { college: '化学与化工学院', newTotal: 42, newPassed: 36, newFailed: 4, newAbsent: 2, seniorTotal: 68, seniorPassed: 62, seniorFailed: 3, seniorAbsent: 3 },
+        { college: '生命科学学院', newTotal: 33, newPassed: 28, newFailed: 3, newAbsent: 2, seniorTotal: 55, seniorPassed: 50, seniorFailed: 3, seniorAbsent: 2 },
+        { college: '物理学院', newTotal: 26, newPassed: 22, newFailed: 2, newAbsent: 2, seniorTotal: 45, seniorPassed: 40, seniorFailed: 3, seniorAbsent: 2 },
+        { college: '电气工程学院', newTotal: 38, newPassed: 33, newFailed: 3, newAbsent: 2, seniorTotal: 65, seniorPassed: 59, seniorFailed: 3, seniorAbsent: 3 },
+        { college: '材料科学学院', newTotal: 30, newPassed: 26, newFailed: 2, newAbsent: 2, seniorTotal: 50, seniorPassed: 45, seniorFailed: 3, seniorAbsent: 2 },
+        { college: '机械工程学院', newTotal: 23, newPassed: 20, newFailed: 2, newAbsent: 1, seniorTotal: 42, seniorPassed: 38, seniorFailed: 2, seniorAbsent: 2 },
+        { college: '自动化学院', newTotal: 20, newPassed: 18, newFailed: 1, newAbsent: 1, seniorTotal: 38, seniorPassed: 35, seniorFailed: 2, seniorAbsent: 1 },
+        { college: '资源环境学院', newTotal: 18, newPassed: 15, newFailed: 2, newAbsent: 1, seniorTotal: 32, seniorPassed: 29, seniorFailed: 2, seniorAbsent: 1 }
+      ],
+      undergraduateExam: [
+        { college: '化学与化工学院', freshTotal: 75, freshPassed: 68, freshFailed: 4, freshAbsent: 3, seniorTotal: 185, seniorPassed: 170, seniorFailed: 9, seniorAbsent: 6 },
+        { college: '生命科学学院', freshTotal: 60, freshPassed: 55, freshFailed: 3, freshAbsent: 2, seniorTotal: 145, seniorPassed: 132, seniorFailed: 8, seniorAbsent: 5 },
+        { college: '物理学院', freshTotal: 50, freshPassed: 44, freshFailed: 4, freshAbsent: 2, seniorTotal: 130, seniorPassed: 118, seniorFailed: 7, seniorAbsent: 5 },
+        { college: '电气工程学院', freshTotal: 85, freshPassed: 78, freshFailed: 4, freshAbsent: 3, seniorTotal: 205, seniorPassed: 188, seniorFailed: 10, seniorAbsent: 7 },
+        { college: '材料科学学院', freshTotal: 46, freshPassed: 42, freshFailed: 2, freshAbsent: 2, seniorTotal: 120, seniorPassed: 110, seniorFailed: 6, seniorAbsent: 4 },
+        { college: '机械工程学院', freshTotal: 65, freshPassed: 59, freshFailed: 3, freshAbsent: 3, seniorTotal: 158, seniorPassed: 142, seniorFailed: 9, seniorAbsent: 7 },
+        { college: '自动化学院', freshTotal: 45, freshPassed: 41, freshFailed: 2, freshAbsent: 2, seniorTotal: 108, seniorPassed: 98, seniorFailed: 5, seniorAbsent: 5 },
+        { college: '资源环境学院', freshTotal: 38, freshPassed: 35, freshFailed: 2, freshAbsent: 1, seniorTotal: 95, seniorPassed: 88, seniorFailed: 4, seniorAbsent: 3 }
+      ],
+      staffExam: [
+        { college: '化学与化工学院', newTotal: 6, newPassed: 5, newFailed: 1, newAbsent: 0, seniorTotal: 32, seniorPassed: 30, seniorFailed: 1, seniorAbsent: 1 },
+        { college: '生命科学学院', newTotal: 5, newPassed: 4, newFailed: 1, newAbsent: 0, seniorTotal: 26, seniorPassed: 24, seniorFailed: 1, seniorAbsent: 1 },
+        { college: '物理学院', newTotal: 4, newPassed: 4, newFailed: 0, newAbsent: 0, seniorTotal: 20, seniorPassed: 19, seniorFailed: 1, seniorAbsent: 0 },
+        { college: '电气工程学院', newTotal: 6, newPassed: 5, newFailed: 1, newAbsent: 0, seniorTotal: 28, seniorPassed: 26, seniorFailed: 1, seniorAbsent: 1 },
+        { college: '材料科学学院', newTotal: 3, newPassed: 3, newFailed: 0, newAbsent: 0, seniorTotal: 22, seniorPassed: 21, seniorFailed: 1, seniorAbsent: 0 },
+        { college: '机械工程学院', newTotal: 4, newPassed: 3, newFailed: 1, newAbsent: 0, seniorTotal: 18, seniorPassed: 17, seniorFailed: 1, seniorAbsent: 0 },
+        { college: '自动化学院', newTotal: 3, newPassed: 3, newFailed: 0, newAbsent: 0, seniorTotal: 16, seniorPassed: 15, seniorFailed: 1, seniorAbsent: 0 },
+        { college: '资源环境学院', newTotal: 3, newPassed: 2, newFailed: 1, newAbsent: 0, seniorTotal: 14, seniorPassed: 13, seniorFailed: 1, seniorAbsent: 0 }
+      ]
+    },
+    '2023-2024': {
+      graduateExamDetail: [
+        { college: '化学与化工学院', newTotal: 40, newPassed: 34, newFailed: 4, newAbsent: 2, seniorTotal: 62, seniorPassed: 56, seniorFailed: 3, seniorAbsent: 3 },
+        { college: '生命科学学院', newTotal: 30, newPassed: 26, newFailed: 2, newAbsent: 2, seniorTotal: 50, seniorPassed: 45, seniorFailed: 3, seniorAbsent: 2 },
+        { college: '物理学院', newTotal: 24, newPassed: 20, newFailed: 2, newAbsent: 2, seniorTotal: 40, seniorPassed: 36, seniorFailed: 2, seniorAbsent: 2 },
+        { college: '电气工程学院', newTotal: 35, newPassed: 30, newFailed: 3, newAbsent: 2, seniorTotal: 58, seniorPassed: 52, seniorFailed: 3, seniorAbsent: 3 },
+        { college: '材料科学学院', newTotal: 28, newPassed: 24, newFailed: 2, newAbsent: 2, seniorTotal: 45, seniorPassed: 40, seniorFailed: 3, seniorAbsent: 2 },
+        { college: '机械工程学院', newTotal: 20, newPassed: 17, newFailed: 2, newAbsent: 1, seniorTotal: 38, seniorPassed: 34, seniorFailed: 2, seniorAbsent: 2 },
+        { college: '自动化学院', newTotal: 18, newPassed: 16, newFailed: 1, newAbsent: 1, seniorTotal: 35, seniorPassed: 32, seniorFailed: 2, seniorAbsent: 1 },
+        { college: '资源环境学院', newTotal: 16, newPassed: 14, newFailed: 1, newAbsent: 1, seniorTotal: 28, seniorPassed: 25, seniorFailed: 2, seniorAbsent: 1 }
+      ],
+      undergraduateExam: [
+        { college: '化学与化工学院', freshTotal: 70, freshPassed: 63, freshFailed: 4, freshAbsent: 3, seniorTotal: 175, seniorPassed: 160, seniorFailed: 9, seniorAbsent: 6 },
+        { college: '生命科学学院', freshTotal: 55, freshPassed: 50, freshFailed: 3, freshAbsent: 2, seniorTotal: 135, seniorPassed: 122, seniorFailed: 7, seniorAbsent: 6 },
+        { college: '物理学院', freshTotal: 48, freshPassed: 42, freshFailed: 3, freshAbsent: 3, seniorTotal: 120, seniorPassed: 108, seniorFailed: 7, seniorAbsent: 5 },
+        { college: '电气工程学院', freshTotal: 80, freshPassed: 72, freshFailed: 5, freshAbsent: 3, seniorTotal: 190, seniorPassed: 174, seniorFailed: 9, seniorAbsent: 7 },
+        { college: '材料科学学院', freshTotal: 42, freshPassed: 38, freshFailed: 2, freshAbsent: 2, seniorTotal: 110, seniorPassed: 100, seniorFailed: 6, seniorAbsent: 4 },
+        { college: '机械工程学院', freshTotal: 60, freshPassed: 54, freshFailed: 3, freshAbsent: 3, seniorTotal: 145, seniorPassed: 130, seniorFailed: 8, seniorAbsent: 7 },
+        { college: '自动化学院', freshTotal: 40, freshPassed: 36, freshFailed: 2, freshAbsent: 2, seniorTotal: 100, seniorPassed: 90, seniorFailed: 5, seniorAbsent: 5 },
+        { college: '资源环境学院', freshTotal: 35, freshPassed: 32, freshFailed: 2, freshAbsent: 1, seniorTotal: 88, seniorPassed: 80, seniorFailed: 4, seniorAbsent: 4 }
+      ],
+      staffExam: [
+        { college: '化学与化工学院', newTotal: 5, newPassed: 4, newFailed: 1, newAbsent: 0, seniorTotal: 30, seniorPassed: 28, seniorFailed: 1, seniorAbsent: 1 },
+        { college: '生命科学学院', newTotal: 4, newPassed: 4, newFailed: 0, newAbsent: 0, seniorTotal: 24, seniorPassed: 22, seniorFailed: 1, seniorAbsent: 1 },
+        { college: '物理学院', newTotal: 3, newPassed: 3, newFailed: 0, newAbsent: 0, seniorTotal: 18, seniorPassed: 17, seniorFailed: 1, seniorAbsent: 0 },
+        { college: '电气工程学院', newTotal: 5, newPassed: 4, newFailed: 1, newAbsent: 0, seniorTotal: 25, seniorPassed: 23, seniorFailed: 1, seniorAbsent: 1 },
+        { college: '材料科学学院', newTotal: 3, newPassed: 3, newFailed: 0, newAbsent: 0, seniorTotal: 20, seniorPassed: 19, seniorFailed: 1, seniorAbsent: 0 },
+        { college: '机械工程学院', newTotal: 3, newPassed: 2, newFailed: 1, newAbsent: 0, seniorTotal: 16, seniorPassed: 15, seniorFailed: 1, seniorAbsent: 0 },
+        { college: '自动化学院', newTotal: 2, newPassed: 2, newFailed: 0, newAbsent: 0, seniorTotal: 14, seniorPassed: 13, seniorFailed: 1, seniorAbsent: 0 },
+        { college: '资源环境学院', newTotal: 2, newPassed: 2, newFailed: 0, newAbsent: 0, seniorTotal: 12, seniorPassed: 11, seniorFailed: 1, seniorAbsent: 0 }
+      ]
+    }
+  },
   // 实验室分级人数
   labLevelStats: [
     { level: 'Ⅰ级(重大风险)', count: 156, color: '#e84d5b' },