瀏覽代碼

梳理西农教育考试需求,响应纪处工作台建设

1.新增工作台板块
2.新增数据统计报表菜单及功能
3.新增分级实验室学时配置
4.完善各角色系统中业务流程图
5.完善需求规格书
6.完善高保真可交互demo
stoney 1 周之前
父節點
當前提交
a4cb9108ee

二進制
assets/report_images/image1.png


二進制
assets/report_images/image2.png


二進制
assets/report_images/image3.png


二進制
assets/report_images/image4.png


二進制
assets/report_images/image5.png


二進制
assets/report_images/image6.png


二進制
assets/report_images/image7.png


二進制
assets/report_images/image8.png


二進制
assets/~$report.docx


+ 411 - 0
prd/需求规格说明书.md

@@ -0,0 +1,411 @@
+# 安全教育与考试系统 - 需求规格说明书
+
+## 文档信息
+
+| 项目 | 内容 |
+|------|------|
+| 项目名称 | 西农实验室安全智慧化管控系统 - 安全教育与考试系统 |
+| 文档版本 | V1.0 |
+| 编写日期 | 2025年5月 |
+| 文档状态 | 待评审 |
+
+---
+
+## 1 项目概述
+
+### 1.1 项目背景
+
+本系统为高等院校实验室安全智慧化管控系统的子系统之一。主系统包含安全教育与考试系统、安全准入系统、化学品智能管理系统、设备管理系统、安全检查系统、实验室综合管理系统、应急预警处置系统等。
+
+当前安全准入系统与安全教育考试系统未拉通,导致不同分级实验室的安全准入条件无法与学生考试通过状态建立关联关系。本次迭代核心目标是打通两个子系统的业务关联。
+
+### 1.2 系统定位
+
+安全教育与考试系统负责:
+- 安全知识学习资源管理与分发
+- 学时统计与管理
+- 在线考试(分级考试、应知应会考试、新生入学考试)
+- 准入证书生成与管理
+- 实验室绑定与审批流程
+
+### 1.3 用户角色
+
+| 角色 | 说明 |
+|------|------|
+| 硕士研究生(新入学) | 每年9月入学的研究生新生,需绑定实验室 |
+| 硕士研究生(在读) | 研二、研三、研四等非新生研究生 |
+| 本科生(新入学) | 每年9月入学的本科新生 |
+| 本科生(在读) | 大二、大三、大四等非新生本科生 |
+| 教职工(新入职) | 当年新入职的教职工 |
+| 教职工(存量) | 非当年入职的在职教职工 |
+| 实验室负责人 | 主系统内实验室的管理人员,拥有审批权限 |
+| 系统管理员 | 负责系统配置、考试管理、资源管理 |
+
+---
+
+## 2 名词定义
+
+| 名词 | 定义 |
+|------|------|
+| 实验室分级 | 依据危险源及存量分为Ⅰ、Ⅱ、Ⅲ、Ⅳ级(红、橙、黄、蓝),代表重大、高、中、低风险 |
+| 实验室分类 | 化学、生物、辐射、特种设备等类别 |
+| 学时 | 用户学习资料的时间计量,学习40分钟记做1学时 |
+| 资料学习时间 | 管理员为每个学习资料配置的学习时间,视频类未设置则取视频时长 |
+| 分级考试 | 针对研究生的考试类型,内容根据绑定实验室的分级和分类匹配 |
+| 应知应会考试 | 系统内置考试类型,面向教职工、非新生本科生、无需进入实验室的研究生 |
+| 新生入学考试 | 面向新入学本科生的考试类型 |
+| 准入证书 | 通过考试后系统自动生成的电子证书 |
+| 学习任务 | 一组资料聚合后生成的学习任务单元 |
+| 安全素养知识 | 系统中提供的资料分类名称 |
+
+---
+
+## 3 功能需求
+
+### 3.1 考试类型
+
+#### 3.1.1 新生入学考试
+
+**适用对象:** 新入学本科生
+
+**前置条件:** 完成4学时安全通识学习(学时数可配置)
+
+**业务流程:**
+1. 新本科生入学后进入系统
+2. 工作台显示所需学时(4学时)及新生入学考试
+3. 在系统中进行学习,累计达到4学时
+4. 达标后系统解锁新生入学考试
+5. 执行在线考试
+6. 通过考试,系统发放证书
+
+**后续操作:**
+- 通过考试后开放"绑定实验室"按钮
+- 绑定实验室弹窗复用研究生绑定弹窗,但去掉"无需进入实验室"选项
+- 新增必填字段"进入原因"
+- 底部提示:毕业论文或毕业设计,科创等相关需要在具体的实验室开展业务的情况可申请绑定实验室,绑定完毕后需完成对应的分级考试
+
+#### 3.1.2 分级考试
+
+**适用对象:** 绑定了实验室的研究生(新入学)、通过新生入学考试后绑定实验室的本科生
+
+**前置条件:** 完成对应分级学时要求
+
+**学时要求(准入/第一年):**
+
+| 实验室等级 | 准入学时 | 第二年起年度学时 |
+|-----------|---------|----------------|
+| Ⅰ级(重大风险) | 24学时 | 8学时 |
+| Ⅱ级(高风险) | 16学时 | 4学时 |
+| Ⅲ级(中风险) | 8学时 | 2学时 |
+| Ⅳ级(低风险) | 4学时 | 0学时 |
+
+**业务流程:**
+1. 研究生首次进入系统,弹出绑定实验室弹窗
+2. 搜索并选择实验室,提交绑定申请
+3. 实验室负责人审批(7天未处理自动通过,时间可配置)
+4. 审批通过后,系统根据实验室分级确定学时要求
+5. 完成对应学时学习
+6. 参加分级考试(内容根据实验室分级和分类匹配)
+7. 通过考试,系统发放准入证书
+
+#### 3.1.3 应知应会考试
+
+**适用对象:**
+- 选择"无需进入实验室"的研究生新生
+- 全部教职工
+- 非新生本科生(大二、大三、大四)
+- 研二、研三、研四等在读研究生
+
+**前置条件:**
+- 新入职教职工/无需进入实验室的研究生:完成4学时学习
+- 非新生本科生/存量教职工:无学时要求,可直接参加
+- 在读研究生:完成年度学时要求(按实验室分级取年度学时值)
+
+**业务流程:**
+1. 用户进入系统,工作台显示考试信息
+2. 如有学时要求,完成对应学时学习
+3. 学时达标后解锁考试(无学时要求则直接可考)
+4. 执行在线考试
+5. 通过考试,系统发放证书
+
+---
+
+### 3.2 实验室绑定
+
+#### 3.2.1 研究生绑定实验室弹窗
+
+**触发条件:** 新入学研究生首次进入系统自动弹出
+
+**弹窗内容:**
+- 可搜索下拉框:支持按实验室名称、房号、楼栋关键词模糊检索
+- 下拉选项格式:`学院-实验室名称(房号)-楼栋楼层`
+- 检索提示文字:此处请选择需要后续进入的科研实验室的名称,输入实验室名称、房号或楼栋关键词,再从检索结果中选择目标实验室
+- 选中后展示完整信息:负责人姓名(脱敏,如王*二)、分级、分类、类型
+- "无需进入实验室"选项:明确说明人文、体育等相关学院可选择此项
+- 须知复选框:《新生绑定实验室须知及责任说明》
+  - 内容1:保证进入的实验室为导师实际使用实验室或属于当前实验室所属课题组
+  - 内容2:研究生期间涉及到操作实验室内重要危险源,必须绑定实验室
+
+**提交规则:** 进入实验室和不进入实验室必须选一个
+
+#### 3.2.2 本科生绑定实验室弹窗
+
+**触发条件:** 通过新生入学考试后,手动点击"绑定实验室"按钮
+
+**与研究生弹窗差异:**
+- 去掉"无需进入实验室"选项
+- 新增必填字段"进入原因"
+- 底部提示文字:毕业论文或毕业设计,科创等相关需要在具体的实验室开展业务的情况可申请绑定实验室,绑定完毕后需完成对应的分级考试
+
+#### 3.2.3 审批流程
+
+- 提交后状态变为"审批中"
+- 实验室负责人在主系统进行审批(通过/驳回)
+- 自动审批机制:提交后7天(可配置)未处理则自动通过
+- 审批通过后系统确定学时要求并开放学习任务
+
+---
+
+### 3.3 用户工作台
+
+#### 3.3.1 页面布局
+
+页面自上而下:用户信息卡 → 工作台功能卡片区 → 实验室安全素养资源区 → 视频卡片资源区
+
+#### 3.3.2 用户信息卡
+
+全宽卡片,三栏布局:
+- **左栏 - 个人信息:** 用户姓名、学工号、用户类型、所属学院、实验室信息、负责人、绑定状态
+- **中栏 - 学时进度:** 学期标签、实验室分级标签、年度学时进度条、完成度KPI、目标考试
+- **右栏 - 任务状态:** 考试状态、证书状态、审批状态
+
+**特殊规则:**
+- 教职工角色隐藏实验室信息、负责人、绑定状态
+- 学时要求为0时隐藏年度学时进度和完成度
+
+#### 3.3.3 工作台功能卡片区
+
+五等分等宽卡片,高度统一:
+
+| 卡片 | 内容 |
+|------|------|
+| 学年数据统计 | 按学年倒序展示历年考试通过情况与成绩 |
+| 学习任务 | 学院下发的学习任务列表,最多显示4条,超出滚动 |
+| 资料类型 | 绑定实验室则显示对应分类资料,否则显示应知应会通识资料 |
+| 模拟练习 | 已练习题目数/题库总数、正确率 |
+| 考试信息 | 学时完成情况、考试状态、操作按钮 |
+
+#### 3.3.4 实验室安全素养资源区
+
+- 7个分类Tab:化学类、生物类、辐射类、机电类、基础类、安全操作及应急处置、科研伦理安全
+- 每个Tab内左右分栏:左侧为学习资料列表,右侧为题库分类卡片
+- 资料可点击进入详情页
+- 提供"查看更多"入口
+- 绑定实验室的用户自动定位到对应分类Tab
+
+#### 3.3.5 视频卡片资源区
+
+- 5个分类Tab:化学类、生物类、辐射类、机电类、其他类
+- 类流媒体平台展示:视频封面、标题、学习时长
+- 支持搜索和翻页
+
+---
+
+### 3.4 系统配置
+
+#### 3.4.1 分级学时配置
+
+管理员可配置各等级实验室的学时要求:
+
+| 配置项 | 说明 |
+|--------|------|
+| 准入学时(第一年) | 新绑定实验室用户首年需完成的学时数 |
+| 年度学时(第二年起) | 后续每年需完成的学时数 |
+
+配置修改后实时联动更新所有相关用户的学时要求。
+
+#### 3.4.2 其他可配置项
+
+- 新生入学考试学时要求(默认4学时)
+- 新入职教职工学时要求(默认4学时)
+- 自动审批超时时间(默认7天)
+- 证书是否自动生成(按考试配置)
+
+---
+
+### 3.5 数据统计
+
+侧边栏"数据统计"菜单进入。页面顶部为Tab栏,当前展示"报表"Tab。报表页面采用瀑布式平铺布局,自上而下依次展示三个统计板块。
+
+#### 3.5.1 考试情况统计
+
+**切换维度:** 研究生 / 本科生 Radio 按钮切换
+
+**图表区:**
+- 左侧:堆叠柱状图,X轴为各学院,Y轴为人数,分通过/未通过/未考三色堆叠,柱体顶部显示数值
+- 右侧:环形饼图,展示全校总体通过/未通过/未考占比
+
+**研究生报表:**
+
+每个学院占3行(学院列合并单元格),字段如下:
+
+| 学院 | 考试类型 | 应考人数 | 通过人数 | 未通过人数 | 未考人数 | 通过率 |
+|------|---------|---------|---------|-----------|---------|--------|
+| 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% |
+| XX学院 | 汇总 | N | N | N | N | N% |
+
+**通过率:** 以百分比数字展示,不使用进度条
+
+**导出:** 右上角"导出数据"按钮
+
+#### 3.5.2 实验室与人员匹配
+
+**图表:** 柱状图,X轴为Ⅰ-Ⅳ级实验室,Y轴为绑定总人数,柱体顶部显示数值
+
+**报表:** 按学院统计各级实验室人员数量
+
+| 学院 | Ⅰ级人数 | Ⅱ级人数 | Ⅲ级人数 | Ⅳ级人数 | 总人数 |
+|------|---------|---------|---------|---------|--------|
+| XX学院 | N | N | N | N | N |
+
+**导出:** 右上角两个按钮
+- 导出各学院数据
+- 导出实验室人员明细
+
+#### 3.5.3 学时统计
+
+**图表:** 分组柱状图,X轴为各学院,按研究生/本科生/教职工分组展示学习总时长(单位:学时),柱体顶部显示数值
+
+**报表:** 学生学时明细列表,支持翻页(每页8条)
+
+| 姓名 | 学号 | 年级 | 所属学院 | 第一年 | 第二年 | 第三年 | 第四年 | 总时长 |
+|------|------|------|---------|--------|--------|--------|--------|--------|
+| XX | XX | XX级 | XX学院 | N | N | N | N | N |
+
+**筛选项:**
+- 用户类型:研究生 / 本科生 / 教职工(下拉选择)
+- 学院单位:下拉选择
+- 年级:下拉选择
+- 学号/姓名:模糊检索输入框
+
+**导出:** 右上角"导出数据"按钮
+
+---
+
+## 4 业务流程
+
+### 4.1 新入学研究生(绑定实验室)
+
+```
+进入系统 → 弹出绑定弹窗 → 搜索选择实验室 → 勾选须知提交
+→ 负责人审批(7天自动通过) → 确定学时要求
+→ 完成准入学时 → 参加分级考试 → 通过获证
+```
+
+### 4.2 新入学研究生(无需进入实验室)
+
+```
+进入系统 → 弹出绑定弹窗 → 选择"无需进入实验室"
+→ 完成4学时学习 → 参加应知应会考试 → 通过获证
+```
+
+### 4.3 在读研究生(研二三)
+
+```
+新学年进入系统 → 查看年度学时要求(按分级)
+→ 完成年度学时 → 参加应知应会考试 → 通过获证
+```
+
+### 4.4 新入学本科生
+
+```
+进入系统 → 完成4学时学习 → 参加新生入学考试 → 通过获证
+→ (可选)绑定实验室 → 完成分级学时 → 参加分级考试 → 通过获证
+```
+
+### 4.5 在读本科生(二三四年级)
+
+```
+新学年进入系统 → 直接参加应知应会考试 → 通过获证
+```
+
+### 4.6 新入职教职工
+
+```
+进入系统 → 完成4学时学习 → 参加应知应会考试 → 通过获证
+```
+
+### 4.7 存量教职工
+
+```
+新学年进入系统 → 直接参加应知应会考试 → 通过获证
+```
+
+---
+
+## 5 非功能需求
+
+### 5.1 技术要求
+
+- 前端技术栈:Vue 2 + Element UI
+- 支持响应式布局(适配1920px、1440px、1100px、768px)
+- 图表库:ECharts
+- 与主系统通过统一身份认证对接
+
+### 5.2 安全要求
+
+- 实验室负责人姓名脱敏显示(隐藏中间字符)
+- 用户数据隔离,不同角色只能查看自身数据
+- 操作日志记录
+
+### 5.3 性能要求
+
+- 页面首屏加载时间 ≤ 3秒
+- 搜索响应时间 ≤ 500ms
+- 支持并发用户数 ≥ 5000
+
+---
+
+## 6 接口需求
+
+### 6.1 与主系统对接接口
+
+| 接口 | 说明 |
+|------|------|
+| 统一身份认证 | 用户登录鉴权 |
+| 实验室数据查询 | 获取实验室列表、详情、分级分类 |
+| 实验室负责人信息 | 获取负责人姓名(脱敏) |
+| 绑定审批流程 | 提交绑定申请、查询审批状态 |
+| 准入状态同步 | 考试通过后同步准入状态至准入系统 |
+
+### 6.2 系统内部接口
+
+| 接口 | 说明 |
+|------|------|
+| 学时统计 | 记录和查询用户学习时长 |
+| 考试管理 | 创建考试、提交答卷、查询成绩 |
+| 证书管理 | 生成证书、查询证书状态 |
+| 资源管理 | 资料CRUD、视频CRUD、题库管理 |
+| 配置管理 | 学时配置、审批超时配置 |
+
+---
+
+## 7 交付物清单
+
+| 序号 | 交付物 | 说明 |
+|------|--------|------|
+| 1 | 源代码 | 前端Vue2项目完整代码 |
+| 2 | 接口文档 | RESTful API设计文档 |
+| 3 | 数据库设计 | ER图及DDL脚本 |
+| 4 | 部署文档 | 部署步骤及环境要求 |
+| 5 | 测试报告 | 功能测试、性能测试报告 |
+| 6 | 用户手册 | 各角色操作指南 |

+ 388 - 0
prototype_opus/css/styles.css

@@ -0,0 +1,388 @@
+/**
+ * 安全教育考试系统 - 样式表
+ */
+
+/* Reset & Base */
+* { margin: 0; padding: 0; box-sizing: border-box; }
+html, body { height: 100%; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; font-size: 14px; color: #25324a; background: #f3f6fb; }
+
+/* Layout */
+.layout { display: flex; height: 100vh; overflow: hidden; }
+
+/* Sidebar */
+.sidebar {
+  width: 240px; min-width: 240px;
+  background: linear-gradient(180deg, #34445d 0%, #2e3b51 100%);
+  display: flex; flex-direction: column;
+  color: #fff; overflow-y: auto;
+}
+.sidebar-logo {
+  padding: 20px 16px; display: flex; align-items: center; gap: 10px;
+  border-bottom: 1px solid rgba(255,255,255,0.1);
+}
+.logo-icon { font-size: 28px; color: #5b95f7; }
+.logo-text { font-size: 15px; font-weight: 600; white-space: nowrap; }
+.nav-menu { list-style: none; padding: 12px 0; flex: 1; }
+.nav-item {
+  padding: 12px 20px; display: flex; align-items: center; gap: 10px;
+  cursor: pointer; transition: all 0.2s; color: rgba(255,255,255,0.7);
+  font-size: 14px; border-left: 3px solid transparent;
+}
+.nav-item:hover { background: rgba(255,255,255,0.08); color: #fff; }
+.nav-item.active {
+  background: rgba(91,149,247,0.15); color: #5b95f7;
+  border-left-color: #5b95f7; font-weight: 500;
+}
+.nav-item i { font-size: 18px; width: 20px; text-align: center; }
+
+/* Main Wrapper */
+.main-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
+
+/* Topbar */
+.topbar {
+  height: 56px; background: #fff; display: flex; align-items: center;
+  justify-content: space-between; padding: 0 24px;
+  border-bottom: 1px solid #e8edf3; flex-shrink: 0;
+}
+.breadcrumb { font-size: 13px; color: #60708a; }
+.breadcrumb .sep { margin: 0 6px; }
+.breadcrumb .current { color: #25324a; font-weight: 500; }
+.user-badge { display: flex; align-items: center; gap: 8px; }
+.user-name { font-size: 13px; color: #25324a; }
+
+/* Content */
+.content { flex: 1; overflow-y: auto; padding: 20px 24px; }
+
+/* Page Toolbar */
+.page-toolbar {
+  display: flex; align-items: center; justify-content: space-between;
+  margin-bottom: 20px; flex-wrap: wrap; gap: 12px;
+}
+.page-title { font-size: 20px; font-weight: 600; color: #25324a; }
+.role-switcher { display: flex; flex-wrap: wrap; gap: 8px; }
+
+/* Panel */
+.panel {
+  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);
+  overflow: hidden; margin-bottom: 20px;
+}
+.panel-head {
+  padding: 20px 24px 0; display: flex; justify-content: space-between;
+  align-items: center; flex-wrap: wrap; gap: 8px;
+}
+.panel-title { display: flex; align-items: center; gap: 8px; }
+.panel-title::before {
+  content: ''; width: 4px; height: 18px;
+  background: linear-gradient(180deg, #5b95f7, #8fc0ff); border-radius: 99px;
+}
+.panel-title h2 { margin: 0; font-size: 18px; line-height: 1.4; }
+.panel-desc { color: #60708a; font-size: 13px; }
+
+/* User Card Grid */
+.user-card-grid {
+  display: grid; grid-template-columns: 1.5fr 1fr 1fr;
+  gap: 16px; padding: 16px 24px 24px;
+}
+.card-box {
+  background: linear-gradient(180deg, #fff, #f7faff);
+  border: 1px solid #eaf0f8; border-radius: 14px; padding: 20px;
+}
+.meta-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 18px; }
+.meta-item span { display: block; font-size: 12px; color: #93a0b5; margin-bottom: 4px; }
+.meta-item strong { font-size: 14px; line-height: 1.5; color: #25324a; }
+.summary-tip { margin-top: 14px; font-size: 12px; color: #60708a; padding-top: 12px; border-top: 1px solid #eaf0f8; }
+
+/* Tags */
+.tag-row { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
+.mini-tag {
+  display: inline-flex; align-items: center; padding: 5px 10px;
+  border-radius: 99px; font-size: 11px; font-weight: 600;
+}
+.mini-tag.normal { color: #3978e6; background: #eef4ff; }
+.mini-tag.level-i { color: #c62828; background: #ffe2e5; }
+.mini-tag.level-ii { color: #b95e0f; background: #ffe7d5; }
+.mini-tag.level-iii { color: #947000; background: #fff6d8; }
+.mini-tag.level-iv { color: #1565c0; background: #e3f2fd; }
+
+/* Progress Bar */
+.hours-progress { margin-bottom: 14px; }
+.progress-head { display: flex; justify-content: space-between; font-size: 12px; color: #60708a; margin-bottom: 8px; }
+.progress-bar { width: 100%; height: 8px; background: #edf2fa; border-radius: 99px; overflow: hidden; }
+.progress-bar > span { display: block; height: 100%; background: linear-gradient(90deg, #5b95f7, #7eb4ff); border-radius: inherit; transition: width 0.3s; }
+
+/* KPI */
+.kpi-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+.kpi-box { background: #f7faff; border: 1px solid #e5edf8; border-radius: 12px; padding: 12px; }
+.kpi-box span { display: block; font-size: 11px; color: #93a0b5; margin-bottom: 4px; }
+.kpi-box strong { font-size: 22px; color: #25324a; }
+.kpi-box em { font-style: normal; font-size: 12px; color: #60708a; margin-left: 2px; }
+.kpi-exam { font-size: 15px !important; }
+
+/* Status List */
+.status-list { display: grid; gap: 8px; }
+.status-item {
+  display: flex; justify-content: space-between; align-items: center;
+  font-size: 13px; padding: 10px 12px; border-radius: 10px;
+  background: #f7f9fd; border: 1px solid #e8eef9;
+}
+.status-chip {
+  padding: 3px 10px; border-radius: 99px; font-weight: 600; font-size: 11px;
+}
+.status-chip.success { background: #e8f8f1; color: #18794e; }
+.status-chip.pending { background: #fff5dc; color: #9c6c00; }
+.status-chip.locked { background: #eef2f8; color: #637187; }
+.status-chip.danger { background: #ffe2e5; color: #c62828; }
+
+/* Feature Grid */
+.feature-grid {
+  display: grid; grid-template-columns: repeat(5, 1fr);
+  gap: 14px; padding: 16px 24px 24px;
+}
+.feature-card {
+  border: 1px solid #e8edf5; border-radius: 14px; padding: 18px;
+  background: linear-gradient(180deg, #fff, #f8fbff);
+  min-height: 220px; display: flex; flex-direction: column; gap: 10px;
+}
+.feature-card h3 { margin: 0; font-size: 15px; color: #25324a; }
+.feature-card > p { margin: 0; font-size: 12px; color: #60708a; line-height: 1.5; }
+
+/* Record/Task Items */
+.record-list { display: flex; flex-direction: column; gap: 8px; flex: 1; min-height: 0; }
+.record-list--fixed { max-height: 140px; overflow: hidden; flex: none; }
+.record-list--scroll { max-height: 168px; overflow-y: auto; flex: none; }
+.record-list--scroll::-webkit-scrollbar { width: 4px; }
+.record-list--scroll::-webkit-scrollbar-thumb { background: #d4dbe6; border-radius: 4px; }
+.record-item {
+  padding: 10px 12px; border-radius: 10px; background: #fff;
+  border: 1px solid #edf2f7; flex-shrink: 0;
+}
+.record-item strong { display: block; font-size: 13px; margin-bottom: 4px; color: #25324a; }
+.record-item span { color: #60708a; font-size: 11px; line-height: 1.5; }
+.task-dot {
+  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
+  margin-right: 4px; vertical-align: middle;
+}
+.task-dot.pending { background: #ffb020; }
+.task-dot.locked { background: #c0c8d4; }
+.task-dot.in-progress { background: #5b95f7; }
+
+/* Meter Box */
+.meter-box {
+  padding: 12px; border-radius: 12px; background: #fff; border: 1px solid #edf2f7;
+}
+.meter-box strong { display: block; font-size: 22px; color: #25324a; margin-bottom: 4px; }
+.meter-box span { font-size: 12px; color: #60708a; }
+
+/* Resource Section */
+.resource-section, .video-section {
+  background: #fff; border-radius: 12px; padding: 24px;
+  box-shadow: 0 2px 12px rgba(37,50,74,0.06); margin-bottom: 24px;
+}
+.section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
+.section-title { font-size: 16px; font-weight: 600; color: #25324a; margin: 0; }
+.section-hint { font-size: 12px; }
+.resource-content { padding: 8px 0; }
+.resource-split { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
+.resource-block { margin-bottom: 0; }
+.resource-block:last-child { margin-bottom: 0; }
+.block-header {
+  display: flex; align-items: center; justify-content: space-between;
+  margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid #f0f2f5;
+}
+.block-header h5 {
+  font-size: 14px; font-weight: 600; color: #25324a; margin: 0;
+  display: flex; align-items: center; gap: 6px;
+}
+.block-header h5 i { color: #5b95f7; font-size: 16px; }
+
+/* Doc Cards */
+.doc-list { display: flex; flex-direction: column; gap: 10px; }
+.doc-card {
+  border: 1px solid #e8edf3; border-radius: 10px; padding: 14px;
+  display: flex; align-items: flex-start; gap: 12px; cursor: pointer;
+  transition: all 0.2s; background: #fafbfd;
+}
+.doc-card:hover { border-color: #5b95f7; background: #f0f6ff; box-shadow: 0 2px 8px rgba(91,149,247,0.1); }
+.doc-icon {
+  width: 40px; height: 40px; border-radius: 8px; background: #eef3ff;
+  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
+}
+.doc-icon i { font-size: 20px; color: #5b95f7; }
+.doc-body { flex: 1; min-width: 0; }
+.doc-title { font-size: 13px; color: #25324a; font-weight: 500; display: block; margin-bottom: 6px; line-height: 1.4; }
+.doc-meta-row { display: flex; align-items: center; justify-content: space-between; }
+.doc-meta { font-size: 11px; color: #93a0b5; }
+.doc-duration { font-size: 11px; color: #60708a; display: flex; align-items: center; gap: 3px; }
+.doc-duration i { font-size: 12px; }
+
+/* Question Cards */
+.question-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
+.question-card {
+  border: 1px solid #e8edf3; border-radius: 10px; padding: 16px 14px;
+  display: flex; flex-direction: column; align-items: center; gap: 8px;
+  cursor: pointer; transition: all 0.2s; background: #fafbfd; text-align: center;
+}
+.question-card:hover { border-color: #5b95f7; background: #f0f6ff; }
+.question-card-icon {
+  width: 36px; height: 36px; border-radius: 50%; background: #eef3ff;
+  display: flex; align-items: center; justify-content: center;
+}
+.question-card-icon i { font-size: 18px; color: #5b95f7; }
+.question-cat-name { font-size: 13px; color: #25324a; font-weight: 500; }
+.question-cat-count { font-size: 12px; color: #93a0b5; }
+.total-card { background: #f0f6ff; border-color: #d4e4fd; }
+.total-card .question-card-icon { background: #5b95f7; }
+.total-card .question-card-icon i { color: #fff; }
+
+/* Video Section */
+.video-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-wrap: wrap; gap: 12px; }
+.video-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 16px; }
+.video-card {
+  border-radius: 10px; overflow: hidden; cursor: pointer;
+  box-shadow: 0 2px 8px rgba(37,50,74,0.08); transition: transform 0.2s;
+}
+.video-card:hover { transform: translateY(-2px); }
+.video-cover {
+  height: 120px; background: linear-gradient(135deg, #34445d, #5b95f7);
+  display: flex; align-items: center; justify-content: center; position: relative;
+}
+.video-cover i { font-size: 36px; color: rgba(255,255,255,0.8); }
+.video-duration {
+  position: absolute; bottom: 8px; right: 8px;
+  background: rgba(0,0,0,0.6); color: #fff; font-size: 11px;
+  padding: 2px 6px; border-radius: 4px;
+}
+.video-info { padding: 12px; }
+.video-title { font-size: 13px; color: #25324a; font-weight: 500; margin-bottom: 4px; line-height: 1.4; }
+.video-meta { font-size: 11px; color: #93a0b5; }
+.video-pagination { text-align: center; }
+
+/* Config Page */
+.config-card {
+  background: #fff; border-radius: 12px; padding: 24px;
+  box-shadow: 0 2px 12px rgba(37,50,74,0.06);
+}
+
+/* Statistics Page */
+.stats-section {
+  background: #fff; border-radius: 12px; border: 1px solid #e8edf5;
+  padding: 20px; margin-bottom: 20px;
+}
+.stats-section-header {
+  display: flex; align-items: center; justify-content: space-between;
+  margin-bottom: 16px; flex-wrap: wrap; gap: 10px;
+}
+.stats-section-header h3 { margin: 0; font-size: 16px; font-weight: 600; color: #25324a; }
+.stats-section-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
+.stats-chart-row {
+  display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;
+}
+.stats-chart-box {
+  background: #f7f9fd; border-radius: 12px; border: 1px solid #e8edf5;
+  padding: 16px; height: 280px;
+}
+.stats-chart-full { grid-column: 1 / -1; }
+.stats-filter-row {
+  display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap;
+}
+
+/* Flow Page */
+.flow-page {
+  display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px;
+}
+.flow-card {
+  background: #fff; border-radius: 14px; padding: 20px;
+  border: 1px solid #e8edf5; box-shadow: 0 2px 12px rgba(37,50,74,0.05);
+}
+.flow-title {
+  font-size: 15px; font-weight: 600; color: #25324a; margin: 0 0 16px;
+  padding-bottom: 12px; border-bottom: 1px solid #f0f2f5;
+}
+.flow-chart { display: flex; flex-direction: column; align-items: center; }
+.flow-step { display: flex; flex-direction: column; align-items: center; width: 100%; }
+.flow-node {
+  display: flex; align-items: center; gap: 10px; width: 100%;
+  padding: 10px 14px; border-radius: 10px; background: #f7f9fd;
+  border: 1px solid #e8eef9; transition: all 0.2s;
+}
+.flow-node:hover { border-color: #5b95f7; background: #f0f6ff; }
+.flow-node.start { background: #eef4ff; border-color: #c5d9f8; }
+.flow-node.end { background: #e8f8f1; border-color: #b8e6d0; }
+.flow-node.decision { background: #fff8e6; border-color: #f0dfa0; }
+.flow-num {
+  width: 22px; height: 22px; border-radius: 50%; background: #5b95f7;
+  color: #fff; font-size: 11px; font-weight: 600;
+  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
+}
+.flow-node.end .flow-num { background: #36b37e; }
+.flow-node.decision .flow-num { background: #ffb020; }
+.flow-text { font-size: 13px; color: #25324a; line-height: 1.4; }
+.flow-arrow {
+  display: flex; flex-direction: column; align-items: center;
+  padding: 4px 0; color: #c0c8d4;
+}
+.flow-arrow i { font-size: 14px; }
+.flow-condition {
+  font-size: 11px; color: #5b95f7; background: #f0f6ff;
+  padding: 2px 8px; border-radius: 4px; margin-top: 2px;
+}
+
+/* Placeholder Page */
+.placeholder-page {
+  display: flex; flex-direction: column; align-items: center;
+  justify-content: center; height: 400px; color: #93a0b5;
+}
+.placeholder-page i { font-size: 64px; margin-bottom: 16px; }
+.placeholder-page p { font-size: 16px; }
+
+/* PRD Page */
+.prd-page {
+  background: #fff; border-radius: 14px; padding: 32px 40px;
+  box-shadow: 0 2px 12px rgba(37,50,74,0.06);
+  line-height: 1.8; color: #25324a; font-size: 14px;
+}
+.prd-page h1 { font-size: 24px; margin: 0 0 8px; padding-bottom: 12px; border-bottom: 2px solid #5b95f7; }
+.prd-page h2 { font-size: 20px; margin: 32px 0 12px; padding-bottom: 8px; border-bottom: 1px solid #e8edf3; }
+.prd-page h3 { font-size: 16px; margin: 24px 0 10px; }
+.prd-page h4 { font-size: 14px; margin: 16px 0 8px; }
+.prd-page p { margin: 8px 0; }
+.prd-page table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
+.prd-page th, .prd-page td { border: 1px solid #e8edf3; padding: 8px 12px; text-align: left; }
+.prd-page th { background: #f7f9fd; font-weight: 600; }
+.prd-page code { background: #f3f6fb; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
+.prd-page pre { background: #f3f6fb; padding: 14px 18px; border-radius: 8px; overflow-x: auto; font-size: 13px; line-height: 1.6; }
+.prd-page pre code { background: none; padding: 0; }
+.prd-page blockquote { border-left: 3px solid #5b95f7; padding: 8px 16px; margin: 12px 0; background: #f7f9fd; color: #60708a; }
+.prd-page ul, .prd-page ol { padding-left: 20px; margin: 8px 0; }
+.prd-page li { margin: 4px 0; }
+.prd-page strong { color: #25324a; }
+.prd-page hr { border: none; border-top: 1px solid #e8edf3; margin: 24px 0; }
+
+/* Dialog */
+.bind-dialog-content .search-hint { font-size: 12px; color: #93a0b5; margin-top: 8px; line-height: 1.6; }
+.bind-dialog-content .selected-lab-info { margin-top: 16px; }
+.bind-dialog-content .no-lab-option { padding: 12px; background: #f8f9fc; border-radius: 8px; }
+.notice-content p { font-size: 14px; color: #25324a; line-height: 2.2; padding-left: 4px; }
+
+/* Responsive */
+@media (max-width: 1400px) {
+  .feature-grid { grid-template-columns: repeat(3, 1fr); }
+  .video-grid { grid-template-columns: repeat(3, 1fr); }
+  .user-card-grid { grid-template-columns: 1fr; }
+}
+@media (max-width: 1100px) {
+  .feature-grid { grid-template-columns: repeat(2, 1fr); }
+  .video-grid { grid-template-columns: repeat(2, 1fr); }
+  .resource-split { grid-template-columns: 1fr; }
+}
+@media (max-width: 768px) {
+  .sidebar { width: 60px; min-width: 60px; }
+  .logo-text, .nav-item span { display: none; }
+  .nav-item { justify-content: center; padding: 12px; }
+  .feature-grid { grid-template-columns: 1fr; }
+  .video-grid { grid-template-columns: 1fr; }
+  .role-switcher { flex-wrap: wrap; }
+  .meta-grid { grid-template-columns: 1fr; }
+  .kpi-row { grid-template-columns: 1fr; }
+}

+ 570 - 0
prototype_opus/index.html

@@ -0,0 +1,570 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>安全教育与考试 - 工作台</title>
+  <!-- Element UI CSS -->
+  <link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
+  <!-- Custom Styles -->
+  <link rel="stylesheet" href="./css/styles.css">
+</head>
+<body>
+  <div id="app">
+    <div class="layout">
+      <!-- 侧边栏 -->
+      <aside class="sidebar">
+        <div class="sidebar-logo">
+          <i class="el-icon-s-cooperation logo-icon"></i>
+          <span class="logo-text">安全教育与考试</span>
+        </div>
+        <ul class="nav-menu">
+          <li v-for="item in menuItems" :key="item.key"
+              :class="['nav-item', { active: activePage === item.key }]"
+              @click="activePage = item.key">
+            <i :class="item.icon"></i>
+            <span>{{ item.label }}</span>
+          </li>
+        </ul>
+      </aside>
+
+      <!-- 主内容区 -->
+      <div class="main-wrapper">
+        <!-- 顶部栏 -->
+        <header class="topbar">
+          <div class="breadcrumb">
+            <span>首页</span>
+            <span class="sep">/</span>
+            <span class="current">{{ currentPageLabel }}</span>
+          </div>
+          <div class="user-badge">
+            <el-avatar :size="32" icon="el-icon-user-solid"></el-avatar>
+            <span class="user-name">{{ currentRole.name }}</span>
+          </div>
+        </header>
+
+        <!-- 工作台页面 -->
+        <main class="content" v-if="activePage === 'workbench'">
+          <!-- 角色切换器 -->
+          <div class="page-toolbar">
+            <h2 class="page-title">工作台</h2>
+            <div class="role-switcher">
+              <el-button type="info" size="small" icon="el-icon-document" @click="activePage = 'prd'">需求说明</el-button>
+              <el-button type="success" size="small" icon="el-icon-s-operation" @click="activePage = 'flow'">业务流程说明</el-button>
+              <el-button v-for="btn in roleButtons" :key="btn.key"
+                :type="activeRole === btn.key ? 'primary' : 'default'"
+                size="small" @click="switchRole(btn.key)">
+                {{ btn.label }}
+              </el-button>
+            </div>
+          </div>
+
+          <!-- 用户信息卡 -->
+          <section class="panel">
+            <div class="panel-head">
+              <div class="panel-title"><h2>用户信息卡</h2></div>
+              <div class="panel-desc">当前推荐资料类型:{{ resolvedResourceTab }}</div>
+            </div>
+            <div class="user-card-grid">
+              <!-- 左:个人信息 -->
+              <div class="card-box meta-box">
+                <div class="meta-grid">
+                  <div class="meta-item"><span>用户姓名</span><strong>{{ currentRole.name }}</strong></div>
+                  <div class="meta-item"><span>学工号</span><strong>{{ currentRole.code }}</strong></div>
+                  <div class="meta-item"><span>用户类型</span><strong>{{ currentRole.type }}</strong></div>
+                  <div class="meta-item"><span>所属单位</span><strong>{{ currentRole.org }}</strong></div>
+                  <div class="meta-item" v-if="!isStaffRole"><span>实验室信息</span><strong>{{ currentRole.lab ? currentRole.lab.name + '(' + currentRole.lab.room + ')' : '暂无绑定实验室' }}</strong></div>
+                  <div class="meta-item" v-if="!isStaffRole"><span>负责人</span><strong>{{ currentRole.lab ? currentRole.lab.director : '不适用' }}</strong></div>
+                </div>
+                <div class="summary-tip" v-if="!isStaffRole">
+                  绑定状态:<strong>{{ bindingStatusText }}</strong>
+                  <el-button v-if="currentRole.bindingStatus === 'unbound'" type="text" size="mini" @click="openBindDialog('graduate')" style="margin-left:8px">去绑定</el-button>
+                </div>
+              </div>
+              <!-- 中:学时进度 -->
+              <div class="card-box status-box">
+                <div class="tag-row">
+                  <span class="mini-tag normal">{{ currentRole.term }}</span>
+                  <span v-if="currentRole.lab" :class="['mini-tag', levelClass(currentRole.lab.level)]">{{ currentRole.lab.level }}级实验室</span>
+                  <span v-if="currentRole.lab" class="mini-tag normal">{{ currentRole.lab.category }}</span>
+                </div>
+                <template v-if="currentRole.requiredHours > 0">
+                  <div class="hours-progress">
+                    <div class="progress-head">
+                      <span>年度学时进度</span>
+                      <strong>{{ currentRole.completedHours }} / {{ currentRole.requiredHours }} 学时</strong>
+                    </div>
+                    <div class="progress-bar"><span :style="{ width: hoursPercentage + '%' }"></span></div>
+                  </div>
+                  <div class="kpi-row">
+                    <div class="kpi-box">
+                      <span>完成度</span>
+                      <strong>{{ hoursPercentage }}<em>%</em></strong>
+                    </div>
+                    <div class="kpi-box">
+                      <span>目标考试</span>
+                      <strong class="kpi-exam">{{ currentRole.examType }}</strong>
+                    </div>
+                  </div>
+                </template>
+                <template v-else>
+                  <div class="kpi-row" style="margin-top:12px">
+                    <div class="kpi-box">
+                      <span>学时要求</span>
+                      <strong class="kpi-exam">无</strong>
+                    </div>
+                    <div class="kpi-box">
+                      <span>目标考试</span>
+                      <strong class="kpi-exam">{{ currentRole.examType }}</strong>
+                    </div>
+                  </div>
+                </template>
+              </div>
+              <!-- 右:任务状态 -->
+              <div class="card-box status-box">
+                <div class="panel-desc" style="margin-bottom:10px">当前学年任务状态</div>
+                <div class="status-list">
+                  <div class="status-item">
+                    <span>考试状态</span>
+                    <span :class="['status-chip', examChipClass]">{{ currentRole.examStatus }}</span>
+                  </div>
+                  <div class="status-item">
+                    <span>证书状态</span>
+                    <span :class="['status-chip', certChipClass]">{{ currentRole.certificateStatus }}</span>
+                  </div>
+                  <div class="status-item" v-if="currentRole.bindingStatus !== 'none'">
+                    <span>审批状态</span>
+                    <span :class="['status-chip', approvalChipClass]">{{ bindingStatusText }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </section>
+
+          <!-- 功能卡片区 -->
+          <section class="panel">
+            <div class="panel-head">
+              <div class="panel-title"><h2>工作台功能卡片区</h2></div>
+            </div>
+            <div class="feature-grid">
+              <!-- 学年数据统计 -->
+              <article class="feature-card">
+                <h3>学年数据统计</h3>
+                <p>按学年统计历年考试通过情况与成绩</p>
+                <div class="record-list record-list--fixed">
+                  <div class="record-item" v-for="(item, idx) in currentRole.yearlyRecords" :key="idx">
+                    <strong>{{ item.year }}</strong>
+                    <span>{{ item.status }} · {{ item.score }}</span>
+                  </div>
+                </div>
+              </article>
+              <!-- 学习任务 -->
+              <article class="feature-card">
+                <h3>学习任务</h3>
+                <p>学院下发的学习任务</p>
+                <div class="record-list record-list--scroll">
+                  <div class="record-item" v-for="(task, idx) in collegeTasks" :key="idx">
+                    <strong>{{ task.title }}</strong>
+                    <span><i :class="['task-dot', task.status]"></i> {{ task.note }}</span>
+                  </div>
+                </div>
+              </article>
+              <!-- 资料类型 -->
+              <article class="feature-card">
+                <h3>资料类型</h3>
+                <p>{{ currentRole.lab ? '根据实验室 ' + currentRole.lab.category + ' 分类推荐' : '默认推荐应知应会通识资源' }}</p>
+                <div class="record-list">
+                  <div class="record-item" v-for="(res, ri) in currentRole.resources" :key="ri">
+                    <strong>{{ res.title }}</strong>
+                    <span>{{ res.desc }}</span>
+                  </div>
+                </div>
+              </article>
+              <!-- 模拟练习 -->
+              <article class="feature-card">
+                <h3>模拟练习</h3>
+                <p>刷题进度和正确率</p>
+                <div class="meter-box">
+                  <strong>{{ currentRole.practice.done }} / {{ currentRole.practice.total }}</strong>
+                  <span>已完成题目数 / 题库总数</span>
+                </div>
+                <div class="meter-box">
+                  <strong>{{ currentRole.practice.correctRate }}</strong>
+                  <span>当前模拟练习正确率</span>
+                </div>
+              </article>
+              <!-- 考试信息 -->
+              <article class="feature-card">
+                <h3>{{ currentRole.examType }}</h3>
+                <p>{{ examCardDesc }}</p>
+                <div class="record-list record-list--fixed">
+                  <div class="record-item">
+                    <strong>学时完成</strong>
+                    <span>{{ currentRole.requiredHours > 0 ? currentRole.completedHours + ' / ' + currentRole.requiredHours + ' 学时' : '无学时要求' }}</span>
+                  </div>
+                  <div class="record-item">
+                    <strong>考试状态</strong>
+                    <span><i :class="['task-dot', currentRole.examStatus === '已通过' ? 'in-progress' : 'locked']"></i> {{ currentRole.examStatus }}</span>
+                  </div>
+                </div>
+                <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>
+              </article>
+            </div>
+          </section>
+
+          <!-- 安全素养资源区 -->
+          <div class="resource-section">
+            <div class="section-header">
+              <h3 class="section-title">实验室安全素养资源</h3>
+              <span class="section-hint" v-if="currentRole.lab">
+                <el-tag size="mini" type="warning">当前绑定:{{ currentRole.lab.category }}</el-tag>
+              </span>
+            </div>
+            <el-tabs v-model="activeResourceTab" type="card">
+              <el-tab-pane v-for="tab in resourceTabs" :key="tab" :label="tab" :name="tab">
+                <div class="resource-content resource-split">
+                  <!-- 左:资料区 -->
+                  <div class="resource-block resource-left">
+                    <div class="block-header">
+                      <h5><i class="el-icon-folder-opened"></i> 学习资料</h5>
+                      <el-button type="text" size="mini" icon="el-icon-more">查看更多</el-button>
+                    </div>
+                    <div class="doc-list">
+                      <div class="doc-card" v-for="(doc, i) in getResourceDocs(tab)" :key="i" @click="viewDocDetail(doc)">
+                        <div class="doc-icon">
+                          <i :class="doc.type === 'PDF' ? 'el-icon-document' : 'el-icon-data-board'"></i>
+                        </div>
+                        <div class="doc-body">
+                          <span class="doc-title">{{ doc.title }}</span>
+                          <div class="doc-meta-row">
+                            <span class="doc-meta">{{ doc.type }} · {{ doc.pages }}页</span>
+                            <span class="doc-duration"><i class="el-icon-time"></i> {{ doc.duration }}</span>
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  </div>
+                  <!-- 右:题库区 -->
+                  <div class="resource-block resource-right">
+                    <div class="block-header">
+                      <h5><i class="el-icon-edit-outline"></i> 题库</h5>
+                      <el-button type="text" size="mini" icon="el-icon-more">查看更多</el-button>
+                    </div>
+                    <div class="question-grid">
+                      <div class="question-card" v-for="(cat, ci) in getResourceQuestions(tab).categories" :key="ci">
+                        <div class="question-card-icon"><i class="el-icon-notebook-2"></i></div>
+                        <span class="question-cat-name">{{ cat.name }}</span>
+                        <span class="question-cat-count">{{ cat.count }} 题</span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </el-tab-pane>
+            </el-tabs>
+          </div>
+
+          <!-- 视频资源区 -->
+          <div class="video-section">
+            <h3 class="section-title" style="margin-bottom:16px">视频学习资源</h3>
+            <div class="video-toolbar">
+              <el-tabs v-model="activeVideoTab" @tab-click="videoPage = 1">
+                <el-tab-pane v-for="tab in videoTabs" :key="tab" :label="tab" :name="tab"></el-tab-pane>
+              </el-tabs>
+              <el-input v-model="videoSearch" placeholder="搜索视频" prefix-icon="el-icon-search" size="small" style="width:200px" clearable></el-input>
+            </div>
+            <div class="video-grid">
+              <div class="video-card" v-for="video in paginatedVideos" :key="video.id">
+                <div class="video-cover">
+                  <i class="el-icon-video-play"></i>
+                  <span class="video-duration">{{ video.duration }}</span>
+                </div>
+                <div class="video-info">
+                  <p class="video-title">{{ video.title }}</p>
+                  <p class="video-meta">{{ video.views }} 次观看</p>
+                </div>
+              </div>
+            </div>
+            <div class="video-pagination">
+              <el-pagination small layout="prev, pager, next" :total="filteredVideos.length" :page-size="4" :current-page.sync="videoPage"></el-pagination>
+            </div>
+          </div>
+        </main>
+
+        <!-- 数据统计页面 -->
+        <main class="content" v-if="activePage === 'statistics'">
+          <div class="page-toolbar">
+            <h2 class="page-title">数据统计</h2>
+          </div>
+          <el-tabs v-model="statsTopTab" type="border-card">
+            <el-tab-pane label="报表" name="current">
+              <!-- 考试情况统计 -->
+              <section class="stats-section">
+                <div class="stats-section-header">
+                  <h3>考试情况统计</h3>
+                  <div class="stats-section-actions">
+                    <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-group>
+                    <el-button size="mini" type="primary" icon="el-icon-download">导出数据</el-button>
+                  </div>
+                </div>
+                <div class="stats-chart-row">
+                  <div class="stats-chart-box" ref="examBarChart"></div>
+                  <div class="stats-chart-box" ref="examPieChart"></div>
+                </div>
+                <div class="stats-filter-row">
+                  <el-select v-model="examFilterCollege" placeholder="筛选学院" size="mini" clearable style="width:160px">
+                    <el-option v-for="c in statisticsData.colleges" :key="c" :label="c" :value="c"></el-option>
+                  </el-select>
+                  <el-select v-model="examFilterExamType" placeholder="考试类型" size="mini" clearable style="width:140px">
+                    <el-option v-if="statsExamType === 'graduate'" label="新生分级考试" value="新生分级考试"></el-option>
+                    <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 label="汇总" value="汇总"></el-option>
+                  </el-select>
+                </div>
+                <!-- 研究生:区分新生分级考试和老生应知应会 -->
+                <el-table v-if="statsExamType === 'graduate'" :data="filteredGraduateData" 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>
+                <!-- 本科生 -->
+                <el-table v-if="statsExamType === 'undergraduate'" :data="filteredUndergraduateData" 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>
+
+              <!-- 实验室与人员匹配 -->
+              <section class="stats-section">
+                <div class="stats-section-header">
+                  <h3>实验室与人员匹配</h3>
+                  <div class="stats-section-actions">
+                    <el-button size="mini" type="primary" icon="el-icon-download">导出各学院数据</el-button>
+                    <el-button size="mini" type="success" icon="el-icon-download">导出实验室人员明细</el-button>
+                  </div>
+                </div>
+                <div class="stats-chart-row">
+                  <div class="stats-chart-box stats-chart-full" ref="labLevelChart"></div>
+                </div>
+                <div class="stats-filter-row">
+                  <el-select v-model="labFilterCollege" placeholder="筛选学院" size="mini" clearable style="width:160px">
+                    <el-option v-for="c in statisticsData.colleges" :key="c" :label="c" :value="c"></el-option>
+                  </el-select>
+                  <el-select v-model="labFilterLevel" placeholder="实验室等级" size="mini" clearable style="width:140px">
+                    <el-option label="Ⅰ级" value="Ⅰ"></el-option>
+                    <el-option label="Ⅱ级" value="Ⅱ"></el-option>
+                    <el-option label="Ⅲ级" value="Ⅲ"></el-option>
+                    <el-option label="Ⅳ级" value="Ⅳ"></el-option>
+                  </el-select>
+                </div>
+                <el-table :data="filteredLabCollegeData" border size="small" style="width:100%;margin-top:8px">
+                  <el-table-column prop="college" label="学院" min-width="160"></el-table-column>
+                  <el-table-column prop="level1" label="Ⅰ级人数" min-width="100"></el-table-column>
+                  <el-table-column prop="level2" label="Ⅱ级人数" min-width="100"></el-table-column>
+                  <el-table-column prop="level3" label="Ⅲ级人数" min-width="100"></el-table-column>
+                  <el-table-column prop="level4" label="Ⅳ级人数" min-width="100"></el-table-column>
+                  <el-table-column prop="totalCount" label="总人数" min-width="90"></el-table-column>
+                </el-table>
+              </section>
+
+              <!-- 学时统计 -->
+              <section class="stats-section">
+                <div class="stats-section-header">
+                  <h3>学时统计</h3>
+                  <div class="stats-section-actions">
+                    <el-button size="mini" type="primary" icon="el-icon-download">导出数据</el-button>
+                  </div>
+                </div>
+                <div class="stats-chart-row">
+                  <div class="stats-chart-box stats-chart-full" ref="hoursChart"></div>
+                </div>
+                <div class="stats-filter-row">
+                  <el-select v-model="hoursFilterType" placeholder="用户类型" size="mini" clearable style="width:120px">
+                    <el-option label="研究生" value="研究生"></el-option>
+                    <el-option label="本科生" value="本科生"></el-option>
+                    <el-option label="教职工" value="教职工"></el-option>
+                  </el-select>
+                  <el-select v-model="hoursFilterCollege" placeholder="学院" size="mini" clearable style="width:160px">
+                    <el-option v-for="c in statisticsData.colleges" :key="c" :label="c" :value="c"></el-option>
+                  </el-select>
+                  <el-select v-model="hoursFilterGrade" placeholder="年级" size="mini" clearable style="width:120px">
+                    <el-option v-for="g in gradeOptions" :key="g" :label="g" :value="g"></el-option>
+                  </el-select>
+                  <el-input v-model="hoursFilterKey" placeholder="学号/姓名" size="mini" prefix-icon="el-icon-search" clearable style="width:160px"></el-input>
+                </div>
+                <el-table :data="filteredHoursDetail" border size="small" style="width:100%">
+                  <el-table-column prop="name" label="姓名" min-width="80"></el-table-column>
+                  <el-table-column prop="code" label="学号" min-width="120"></el-table-column>
+                  <el-table-column prop="grade" label="年级" min-width="80"></el-table-column>
+                  <el-table-column prop="college" label="所属学院" min-width="140"></el-table-column>
+                  <el-table-column prop="y1" label="第一年" min-width="70"></el-table-column>
+                  <el-table-column prop="y2" label="第二年" min-width="70"></el-table-column>
+                  <el-table-column prop="y3" label="第三年" min-width="70"></el-table-column>
+                  <el-table-column prop="y4" label="第四年" min-width="70"></el-table-column>
+                  <el-table-column prop="totalHours" label="总时长" min-width="80"></el-table-column>
+                </el-table>
+                <div style="text-align:center;margin-top:12px">
+                  <el-pagination small layout="prev, pager, next" :total="hoursDetailFiltered.length" :page-size="hoursPageSize" :current-page.sync="hoursPage"></el-pagination>
+                </div>
+              </section>
+            </el-tab-pane>
+          </el-tabs>
+        </main>
+
+        <!-- 系统配置页面 -->
+        <main class="content" v-if="activePage === 'config'">
+          <div class="page-toolbar">
+            <h2 class="page-title">分级学时配置</h2>
+          </div>
+          <div class="config-card">
+            <el-table :data="configTableData" border style="width: 100%">
+              <el-table-column prop="levelLabel" label="风险等级" width="200"></el-table-column>
+              <el-table-column prop="level" label="级别" width="100"></el-table-column>
+              <el-table-column label="准入学时要求(第一年)">
+                <template slot-scope="scope">
+                  <el-input-number v-model="scope.row.firstYear" :min="0" :max="100" size="small"></el-input-number>
+                </template>
+              </el-table-column>
+              <el-table-column label="年度学时要求(第二年起)">
+                <template slot-scope="scope">
+                  <el-input-number v-model="scope.row.annual" :min="0" :max="100" size="small"></el-input-number>
+                </template>
+              </el-table-column>
+            </el-table>
+            <div style="margin-top:16px;text-align:right">
+              <el-button type="primary" @click="saveConfig">保存配置</el-button>
+            </div>
+          </div>
+        </main>
+
+        <!-- 其他页面占位 -->
+        <main class="content" v-if="!['workbench','config','flow','prd','statistics'].includes(activePage)">
+          <div class="placeholder-page">
+            <i class="el-icon-s-opportunity"></i>
+            <p>{{ currentPageLabel }} - 功能开发中</p>
+          </div>
+        </main>
+
+        <!-- 需求说明页面 -->
+        <main class="content" v-if="activePage === 'prd'">
+          <div class="page-toolbar">
+            <h2 class="page-title">需求规格说明书</h2>
+            <el-button size="small" icon="el-icon-back" @click="activePage = 'workbench'">返回工作台</el-button>
+          </div>
+          <div class="prd-page" v-html="prdContent"></div>
+        </main>
+
+        <!-- 业务流程说明页面 -->
+        <main class="content" v-if="activePage === 'flow'">
+          <div class="page-toolbar">
+            <h2 class="page-title">业务流程说明</h2>
+            <el-button size="small" icon="el-icon-back" @click="activePage = 'workbench'">返回工作台</el-button>
+          </div>
+          <div class="flow-page">
+            <div class="flow-card" v-for="(flow, fi) in flowData" :key="fi">
+              <h3 class="flow-title">{{ flow.role }}</h3>
+              <div class="flow-chart">
+                <div class="flow-step" v-for="(step, si) in flow.steps" :key="si">
+                  <div class="flow-node" :class="step.type || ''">
+                    <span class="flow-num">{{ si + 1 }}</span>
+                    <span class="flow-text">{{ step.text }}</span>
+                  </div>
+                  <div class="flow-arrow" v-if="si < flow.steps.length - 1">
+                    <i class="el-icon-arrow-down"></i>
+                    <span class="flow-condition" v-if="step.condition">{{ step.condition }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </main>
+      </div>
+    </div>
+
+    <!-- 绑定实验室弹窗 -->
+    <el-dialog title="绑定实验室" :visible.sync="showBindDialog" width="620px" :close-on-click-modal="false">
+      <div class="bind-dialog-content">
+        <!-- 单一可搜索下拉框 -->
+        <el-select v-model="selectedLabId" placeholder="输入实验室名称、房号或楼栋关键词进行搜索" style="width:100%" filterable :filter-method="labFilterMethod" @change="onLabSelect">
+          <el-option v-for="lab in filteredLabs" :key="lab.id"
+            :label="lab.college + '-' + lab.name + '(' + lab.room + ')-' + lab.building"
+            :value="lab.id">
+          </el-option>
+        </el-select>
+        <p class="search-hint">此处请选择需要后续进入的科研实验室的名称,输入实验室名称、房号或楼栋关键词,再从检索结果中选择目标实验室。</p>
+
+        <!-- 选中后展示完整信息 -->
+        <div class="selected-lab-info" v-if="selectedLabDetail">
+          <el-descriptions :column="2" size="small" border>
+            <el-descriptions-item label="实验室名称">{{ selectedLabDetail.name }}({{ selectedLabDetail.room }})</el-descriptions-item>
+            <el-descriptions-item label="所在位置">{{ selectedLabDetail.building }}</el-descriptions-item>
+            <el-descriptions-item label="负责人">{{ selectedLabDetail.directorMasked }}</el-descriptions-item>
+            <el-descriptions-item label="所属学院">{{ selectedLabDetail.college }}</el-descriptions-item>
+            <el-descriptions-item label="实验室分级">
+              <el-tag :type="levelTagType(selectedLabDetail.level)" size="mini">{{ selectedLabDetail.level }}级</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="实验室分类">{{ selectedLabDetail.category }}</el-descriptions-item>
+            <el-descriptions-item label="实验室类型">{{ selectedLabDetail.type }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
+
+        <!-- 进入原因(本科生专属) -->
+        <div v-if="bindDialogMode === 'undergraduate'" style="margin-top:16px">
+          <el-input v-model="bindReason" type="textarea" placeholder="请填写进入实验室原因(必填)" :rows="2"></el-input>
+          <p class="search-hint" style="margin-top:6px">毕业论文或毕业设计,科创等相关需要在具体的实验室开展业务的情况可申请绑定实验室,绑定完毕后需完成对应的分级考试。</p>
+        </div>
+
+        <!-- 无需进入实验室(研究生专属) -->
+        <div v-if="bindDialogMode === 'graduate'" class="no-lab-option" style="margin-top:16px">
+          <el-checkbox v-model="noLabMode">无需进入实验室</el-checkbox>
+          <p class="search-hint" style="margin-top:4px">人文、体育等相关学院无需进入实验室的新研究生可选择此项,选择后学时要求为4小时,需通过应知应会考试。</p>
+        </div>
+
+        <!-- 须知复选框 -->
+        <div class="bind-notice" style="margin-top:16px">
+          <el-checkbox v-model="noticeChecked">我已阅读并同意<el-button type="text" @click="showNoticeDetail = true">《新生绑定实验室须知及责任说明》</el-button></el-checkbox>
+        </div>
+      </div>
+      <span slot="footer">
+        <el-button @click="showBindDialog = false">取消</el-button>
+        <el-button type="primary" @click="confirmBind" :disabled="!canBind">确认绑定</el-button>
+      </span>
+    </el-dialog>
+
+    <!-- 须知详情弹窗 -->
+    <el-dialog title="新生绑定实验室须知及责任说明" :visible.sync="showNoticeDetail" width="520px" append-to-body>
+      <div class="notice-content">
+        <p>1. 保证进入的实验室为导师实际使用实验室或属于当前实验室所属课题组。</p>
+        <p>2. 研究生期间涉及到操作实验室内重要危险源,必须绑定实验室。</p>
+      </div>
+      <span slot="footer">
+        <el-button type="primary" @click="showNoticeDetail = false">我已知晓</el-button>
+      </span>
+    </el-dialog>
+  </div>
+
+  <!-- Vendor Scripts (CDN - 生产环境应本地化) -->
+  <script src="https://unpkg.com/vue@2.7.16/dist/vue.min.js"></script>
+  <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>
+  <!-- App Scripts -->
+  <script src="./js/data.js"></script>
+  <script src="./js/app.js"></script>
+</body>
+</html>

+ 519 - 0
prototype_opus/js/app.js

@@ -0,0 +1,519 @@
+/**
+ * 安全教育考试系统 - Vue实例
+ */
+new Vue({
+  el: '#app',
+  data: function() {
+    return {
+      activePage: 'workbench',
+      activeRole: 'newGraduate',
+      activeResourceTab: '化学类',
+      activeVideoTab: '化学类',
+      videoSearch: '',
+      videoPage: 1,
+      // 弹窗
+      showBindDialog: false,
+      showNoticeDetail: false,
+      labSearchKey: '',
+      selectedLabId: '',
+      noLabMode: false,
+      noticeChecked: false,
+      bindReason: '',
+      bindDialogMode: 'graduate',
+      labFilteredList: [],
+      // 数据
+      menuItems: menuItems,
+      roleButtons: roleButtons,
+      resourceTabs: resourceTabs,
+      videoTabs: videoTabs,
+      rolesData: JSON.parse(JSON.stringify(roleProfiles)),
+      prdContent: '',
+      // 数据统计
+      statisticsData: statisticsData,
+      statsTopTab: 'current',
+      statsExamType: 'graduate',
+      examFilterCollege: '',
+      examFilterExamType: '',
+      labFilterCollege: '',
+      labFilterLevel: '',
+      hoursFilterType: '',
+      hoursFilterCollege: '',
+      hoursFilterGrade: '',
+      hoursFilterKey: '',
+      hoursPage: 1,
+      hoursPageSize: 8,
+      // 配置表
+      configTableData: [
+        { level: 'Ⅰ', levelLabel: 'Ⅰ级(重大风险)', firstYear: levelHoursConfig['Ⅰ'].firstYear, annual: levelHoursConfig['Ⅰ'].annual },
+        { level: 'Ⅱ', levelLabel: 'Ⅱ级(高风险)', firstYear: levelHoursConfig['Ⅱ'].firstYear, annual: levelHoursConfig['Ⅱ'].annual },
+        { level: 'Ⅲ', levelLabel: 'Ⅲ级(中风险)', firstYear: levelHoursConfig['Ⅲ'].firstYear, annual: levelHoursConfig['Ⅲ'].annual },
+        { level: 'Ⅳ', levelLabel: 'Ⅳ级(低风险)', firstYear: levelHoursConfig['Ⅳ'].firstYear, annual: levelHoursConfig['Ⅳ'].annual }
+      ],
+      // 业务流程数据
+      flowData: [
+        { role: '新入学研究生(绑定实验室)', steps: [
+          { text: '首次进入系统', type: 'start' },
+          { text: '弹出绑定实验室弹窗', condition: '搜索并选择实验室' },
+          { text: '勾选须知并提交绑定申请' },
+          { text: '实验室负责人审批', type: 'decision', condition: '7天未处理自动通过' },
+          { text: '审批通过,确定学时要求(按实验室分级)' },
+          { text: '完成准入学时学习(Ⅰ:24h / Ⅱ:16h / Ⅲ:8h / Ⅳ:4h)' },
+          { text: '参加分级考试(内容匹配实验室分级分类)' },
+          { text: '通过考试,获取准入证书', type: 'end' }
+        ]},
+        { role: '新入学研究生(无需进入实验室)', steps: [
+          { text: '首次进入系统', type: 'start' },
+          { text: '弹出绑定实验室弹窗' },
+          { text: '选择"无需进入实验室"', condition: '人文/体育等学院' },
+          { text: '完成4学时安全通识学习' },
+          { text: '参加应知应会考试' },
+          { text: '通过考试,获取证书', type: 'end' }
+        ]},
+        { role: '研二三(在读研究生)', steps: [
+          { text: '新学年开始,进入系统', type: 'start' },
+          { text: '查看年度学时要求(按实验室分级:Ⅰ:8h / Ⅱ:4h / Ⅲ:2h / Ⅳ:0h)' },
+          { text: '完成年度安全学时学习' },
+          { text: '参加应知应会考试', condition: '学时完成后解锁' },
+          { text: '通过考试,更新准入证书', type: 'end' }
+        ]},
+        { role: '新入学本科生', steps: [
+          { text: '首次进入系统', type: 'start' },
+          { text: '完成4学时安全通识学习' },
+          { text: '参加新生入学考试', condition: '学时完成后解锁' },
+          { text: '通过新生入学考试,获取证书', type: 'decision' },
+          { text: '(可选)点击绑定实验室', condition: '毕设/科创等需求' },
+          { text: '填写进入原因,提交绑定申请' },
+          { text: '审批通过,完成对应分级学时' },
+          { text: '参加分级考试并通过', type: 'end' }
+        ]},
+        { role: '本科二三四(在读本科生)', steps: [
+          { text: '新学年开始,进入系统', type: 'start' },
+          { text: '无学时要求,直接参加考试' },
+          { text: '参加应知应会考试' },
+          { text: '通过考试,获取年度证书', type: 'end' }
+        ]},
+        { role: '新入职教职工', steps: [
+          { text: '入职后首次进入系统', type: 'start' },
+          { text: '完成4学时安全通识学习' },
+          { text: '参加应知应会考试', condition: '学时完成后解锁' },
+          { text: '通过考试,获取证书', type: 'end' }
+        ]},
+        { role: '存量教职工(第二年起)', steps: [
+          { text: '新学年开始,进入系统', type: 'start' },
+          { text: '无学时要求,直接参加考试' },
+          { text: '参加应知应会考试' },
+          { text: '通过考试,获取年度证书', type: 'end' }
+        ]}
+      ]
+    };
+  },
+  computed: {
+    currentRole: function() { return this.rolesData[this.activeRole]; },
+    currentPageLabel: function() {
+      var p = this.activePage;
+      var item = this.menuItems.find(function(m) { return m.key === p; });
+      return item ? item.label : '';
+    },
+    hoursPercentage: function() {
+      var r = this.currentRole;
+      if (!r.requiredHours) return 0;
+      return Math.min(100, Math.round((r.completedHours / r.requiredHours) * 100));
+    },
+    isStaffRole: function() {
+      return this.activeRole === 'newStaff' || this.activeRole === 'existingStaff';
+    },
+    collegeTasks: function() {
+      // 只显示学院/人事处下发的学习任务
+      return (this.currentRole.tasks || []).filter(function(t) {
+        return t.note && (t.note.indexOf('下发') !== -1);
+      });
+    },
+    bindingStatusText: function() {
+      var s = this.currentRole.bindingStatus;
+      var map = { unbound: '待绑定', pending: '审批中', approved: '已通过', rejected: '已驳回', nolab: '无需进入实验室', none: '不适用' };
+      return map[s] || s;
+    },
+    resolvedResourceTab: function() {
+      var r = this.currentRole;
+      if (r.resourceType === 'lab-bindable' && r.lab) return r.lab.category;
+      return '应知应会通识';
+    },
+    examChipClass: function() {
+      var s = this.currentRole.examStatus;
+      if (s === '已通过') return 'success';
+      if (s === '未开始' || s === '待绑定后确认') return 'locked';
+      return 'pending';
+    },
+    certChipClass: function() {
+      var s = this.currentRole.certificateStatus;
+      if (s.indexOf('已获') !== -1) return 'success';
+      return 'locked';
+    },
+    approvalChipClass: function() {
+      var s = this.currentRole.bindingStatus;
+      if (s === 'approved') return 'success';
+      if (s === 'pending') return 'pending';
+      if (s === 'rejected') return 'danger';
+      return 'locked';
+    },
+    examCardDesc: function() {
+      var r = this.currentRole;
+      if (r.requiredHours > 0 && !r.examUnlocked) return '完成 ' + r.requiredHours + ' 学时后开放考试';
+      if (r.examStatus === '已通过') return '已通过,成绩:' + r.examScore + '分';
+      return '满足前置条件后可参加考试';
+    },
+    filteredLabs: function() { return this.labFilteredList; },
+    selectedLabDetail: function() {
+      if (!this.selectedLabId) return null;
+      var lab = labList.find(function(l) { return l.id === this.selectedLabId; }.bind(this));
+      if (!lab) return null;
+      var name = lab.director.replace(/\*+/g, '');
+      var masked = name.length === 2 ? name[0] + '*' : name.length >= 3 ? name[0] + '*' + name.slice(2) : name;
+      return Object.assign({}, lab, { directorMasked: masked });
+    },
+    canBind: function() {
+      if (!this.noticeChecked) return false;
+      if (this.bindDialogMode === 'graduate') { if (!this.noLabMode && !this.selectedLabId) return false; }
+      if (this.bindDialogMode === 'undergraduate') { if (!this.selectedLabId || !this.bindReason.trim()) return false; }
+      return true;
+    },
+    filteredVideos: function() {
+      var videos = videoData[this.activeVideoTab] || [];
+      var s = this.videoSearch.toLowerCase();
+      if (!s) return videos;
+      return videos.filter(function(v) { return v.title.toLowerCase().includes(s); });
+    },
+    paginatedVideos: function() {
+      var start = (this.videoPage - 1) * 4;
+      return this.filteredVideos.slice(start, start + 4);
+    },
+    currentExamData: function() {
+      return statisticsData.undergraduateExam.map(function(d) {
+        var total = d.freshTotal + d.seniorTotal;
+        var passed = d.freshPassed + d.seniorPassed;
+        return { college: d.college, total: total, passed: passed, failed: d.freshFailed + d.seniorFailed, absent: d.freshAbsent + d.seniorAbsent, rate: Math.round(passed / total * 100) + '%' };
+      });
+    },
+    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 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) + '%';
+        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 });
+      });
+      return rows;
+    },
+    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 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) + '%';
+        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;
+    },
+    labCollegeData: function() {
+      return statisticsData.labCollegeStats.map(function(d) {
+        return Object.assign({}, d, { totalCount: d.level1 + d.level2 + d.level3 + d.level4 });
+      });
+    },
+    filteredGraduateData: function() {
+      var data = this.graduateDetailData;
+      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;
+    },
+    filteredUndergraduateData: function() {
+      var data = this.undergraduateDetailData;
+      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;
+    },
+    filteredLabCollegeData: function() {
+      var data = this.labCollegeData;
+      var college = this.labFilterCollege;
+      var level = this.labFilterLevel;
+      if (college) data = data.filter(function(d) { return d.college === college; });
+      if (level) {
+        var key = { 'Ⅰ': 'level1', 'Ⅱ': 'level2', 'Ⅲ': 'level3', 'Ⅳ': 'level4' }[level];
+        if (key) data = data.filter(function(d) { return d[key] > 0; });
+      }
+      return data;
+    },
+    labLevelTableData: function() {
+      return statisticsData.labLevelStats;
+    },
+    hoursDetailFiltered: function() {
+      var data = statisticsData.studyDetail;
+      var type = this.hoursFilterType;
+      var college = this.hoursFilterCollege;
+      var grade = this.hoursFilterGrade;
+      var key = this.hoursFilterKey.toLowerCase();
+      if (type) data = data.filter(function(d) { return d.type === type; });
+      if (college) data = data.filter(function(d) { return d.college === college; });
+      if (grade) data = data.filter(function(d) { return d.grade === grade; });
+      if (key) data = data.filter(function(d) { return d.name.toLowerCase().includes(key) || d.code.toLowerCase().includes(key); });
+      return data;
+    },
+    gradeOptions: function() {
+      var grades = {};
+      statisticsData.studyDetail.forEach(function(d) { grades[d.grade] = true; });
+      return Object.keys(grades).sort();
+    },
+    filteredHoursDetail: function() {
+      var start = (this.hoursPage - 1) * this.hoursPageSize;
+      return this.hoursDetailFiltered.slice(start, start + this.hoursPageSize);
+    }
+  },
+  methods: {
+    switchRole: function(key) {
+      this.activeRole = key;
+      if (key === 'newGraduate' && this.rolesData.newGraduate.bindingStatus === 'unbound') {
+        this.openBindDialog('graduate');
+      }
+    },
+    openBindDialog: function(mode) {
+      this.bindDialogMode = mode || 'graduate';
+      this.resetBindForm();
+      this.showBindDialog = true;
+    },
+    labFilterMethod: function(query) {
+      if (!query) { this.labFilteredList = []; return; }
+      var key = query.toLowerCase();
+      this.labFilteredList = labList.filter(function(lab) {
+        return lab.name.toLowerCase().includes(key) || lab.room.toLowerCase().includes(key) ||
+               lab.building.toLowerCase().includes(key) || lab.college.toLowerCase().includes(key);
+      });
+    },
+    onLabSelect: function(labId) { if (labId) this.noLabMode = false; },
+    levelTagType: function(level) {
+      return { 'Ⅰ': 'danger', 'Ⅱ': 'warning', 'Ⅲ': '', 'Ⅳ': '' }[level] || '';
+    },
+    levelClass: function(level) {
+      return { 'Ⅰ': 'level-i', 'Ⅱ': 'level-ii', 'Ⅲ': 'level-iii', 'Ⅳ': 'level-iv' }[level] || '';
+    },
+    simulatePassExam: function() {
+      var role = this.rolesData[this.activeRole];
+      role.examStatus = '已通过';
+      role.examScore = Math.floor(Math.random() * 20) + 80;
+      role.certificateStatus = '已获得';
+      this.$message.success('考试通过!成绩:' + role.examScore + '分');
+      // 本科生通过新生入学考试后开放绑定实验室
+      if (this.activeRole === 'newUndergraduate') {
+        role.bindingStatus = 'unbound'; // 从none变为unbound,允许绑定
+        this.$notify({ title: '提示', message: '恭喜通过新生入学考试!如需进入实验室开展科研,请点击"绑定实验室"按钮。', type: 'success', duration: 5000 });
+      }
+    },
+    confirmBind: function() {
+      var role = this.rolesData[this.activeRole];
+      if (this.noLabMode && this.bindDialogMode === 'graduate') {
+        role.bindingStatus = 'nolab';
+        role.requiredHours = 4;
+        role.lab = null;
+        role.examType = '应知应会考试';
+        role.examStatus = '未开始';
+        role.tasks = [
+          { title: '完成安全通识学习', note: '0 / 4 学时', status: 'in-progress' },
+          { title: '通过应知应会考试', note: '完成学时后解锁', status: 'locked' }
+        ];
+      } else {
+        var lab = labList.find(function(l) { return l.id === this.selectedLabId; }.bind(this));
+        if (lab) {
+          role.bindingStatus = 'pending'; // 提交后进入审批中
+          role.lab = JSON.parse(JSON.stringify(lab));
+          var cfg = this.configTableData.find(function(c) { return c.level === lab.level; });
+          role.requiredHours = cfg ? (role.isFirstYear ? cfg.firstYear : cfg.annual) : 8;
+          role.examStatus = '未开始';
+          if (this.bindDialogMode === 'undergraduate') {
+            role.examType = '分级考试';
+          }
+          // 更新任务
+          var bindTask = role.tasks.find(function(t) { return t.title.indexOf('绑定') !== -1; });
+          if (bindTask) { bindTask.note = '已提交,审批中'; bindTask.status = 'pending'; }
+          var hoursTask = role.tasks.find(function(t) { return t.title.indexOf('学时') !== -1; });
+          if (hoursTask) { hoursTask.note = '0 / ' + role.requiredHours + ' 学时'; hoursTask.status = 'in-progress'; }
+        }
+      }
+      this.showBindDialog = false;
+      this.resetBindForm();
+      this.$message.success('已提交绑定申请!');
+      // 模拟3秒后审批通过
+      var self = this;
+      setTimeout(function() {
+        if (role.bindingStatus === 'pending') {
+          role.bindingStatus = 'approved';
+          var t = role.tasks.find(function(t) { return t.title.indexOf('绑定') !== -1; });
+          if (t) { t.note = '审批通过'; t.status = 'in-progress'; }
+          self.$notify({ title: '审批通知', message: '实验室绑定已通过审批!', type: 'success' });
+        }
+      }, 3000);
+    },
+    // 模拟审批操作
+    simulateApproval: function(status) {
+      var role = this.currentRole;
+      role.bindingStatus = status;
+      if (status === 'rejected') {
+        this.$message.warning('绑定申请已被驳回');
+      } else {
+        this.$message.success('绑定申请已通过');
+      }
+    },
+    resetBindForm: function() {
+      this.labSearchKey = ''; this.selectedLabId = ''; this.noLabMode = false;
+      this.noticeChecked = false; this.bindReason = ''; this.labFilteredList = [];
+    },
+    saveConfig: function() {
+      var self = this;
+      this.configTableData.forEach(function(item) {
+        levelHoursConfig[item.level] = { firstYear: item.firstYear, annual: item.annual };
+      });
+      // 联动更新已绑定实验室的角色学时
+      Object.keys(this.rolesData).forEach(function(key) {
+        var role = self.rolesData[key];
+        if (role.lab && role.lab.level) {
+          var cfg = levelHoursConfig[role.lab.level];
+          role.requiredHours = role.isFirstYear ? cfg.firstYear : cfg.annual;
+        }
+      });
+      this.$message.success('配置保存成功,已联动更新角色学时要求');
+    },
+    getResourceDocs: function(tab) { return (resourceData[tab] && resourceData[tab].docs) || []; },
+    getResourceQuestions: function(tab) { return (resourceData[tab] && resourceData[tab].questions) || { total: 0, categories: [] }; },
+    viewDocDetail: function(doc) { this.$message({ message: '正在打开:' + doc.title, type: 'info' }); },
+    renderCharts: function() {},  // Charts removed in favor of record-list display
+    renderStatsCharts: function() {
+      if (this.activePage !== 'statistics') return;
+      var self = this;
+      this.$nextTick(function() {
+        self.renderExamCharts();
+        self.renderLabChart();
+        self.renderHoursChart();
+      });
+    },
+    renderExamCharts: function() {
+      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 colleges = data.map(function(d) { return d.college.replace('学院',''); });
+      var barChart = echarts.init(barEl);
+      barChart.setOption({
+        tooltip: { trigger: 'axis' },
+        legend: { top: 0, textStyle: { fontSize: 11 } },
+        grid: { top: 36, bottom: 30, left: 50, right: 10 },
+        xAxis: { type: 'category', data: colleges, axisLabel: { fontSize: 10, rotate: 20 } },
+        yAxis: { type: 'value', axisLabel: { fontSize: 10 } },
+        series: [
+          { name: '通过', type: 'bar', stack: 'total', data: data.map(function(d){return d.passed}), itemStyle: { color: '#36b37e' }, label: { show: true, position: 'inside', fontSize: 10 } },
+          { name: '未通过', type: 'bar', stack: 'total', data: data.map(function(d){return d.failed}), itemStyle: { color: '#e84d5b' }, label: { show: true, position: 'inside', fontSize: 10 } },
+          { name: '未考', type: 'bar', stack: 'total', data: data.map(function(d){return d.absent}), itemStyle: { color: '#c0c8d4' }, label: { show: true, position: 'inside', fontSize: 10 } }
+        ]
+      });
+      var totalPassed = data.reduce(function(s,d){return s+d.passed},0);
+      var totalFailed = data.reduce(function(s,d){return s+d.failed},0);
+      var totalAbsent = data.reduce(function(s,d){return s+d.absent},0);
+      var pieChart = echarts.init(pieEl);
+      pieChart.setOption({
+        tooltip: { trigger: 'item' },
+        legend: { bottom: 0, textStyle: { fontSize: 11 } },
+        series: [{ type: 'pie', radius: ['40%','65%'], center: ['50%','45%'],
+          data: [
+            { value: totalPassed, name: '通过', itemStyle: { color: '#36b37e' } },
+            { value: totalFailed, name: '未通过', itemStyle: { color: '#e84d5b' } },
+            { value: totalAbsent, name: '未考', itemStyle: { color: '#c0c8d4' } }
+          ],
+          label: { fontSize: 11 }
+        }]
+      });
+      this._statsCharts = this._statsCharts || [];
+      this._statsCharts.push(barChart, pieChart);
+    },
+    renderLabChart: function() {
+      var el = this.$refs.labLevelChart;
+      if (!el) return;
+      var chart = echarts.init(el);
+      var data = statisticsData.labLevelStats;
+      chart.setOption({
+        tooltip: { trigger: 'axis' },
+        grid: { top: 20, bottom: 30, left: 60, right: 20 },
+        xAxis: { type: 'category', data: data.map(function(d){return d.level}), axisLabel: { fontSize: 11 } },
+        yAxis: { type: 'value', axisLabel: { fontSize: 11 } },
+        series: [{ type: 'bar', barWidth: 50, data: data.map(function(d){ return { value: d.count, itemStyle: { color: d.color } } }), label: { show: true, position: 'top', fontSize: 11 } }]
+      });
+      this._statsCharts = this._statsCharts || [];
+      this._statsCharts.push(chart);
+    },
+    renderHoursChart: function() {
+      var el = this.$refs.hoursChart;
+      if (!el) return;
+      var chart = echarts.init(el);
+      var data = statisticsData.studyHoursByType;
+      var colleges = data.map(function(d){return d.college.replace('学院','')});
+      chart.setOption({
+        tooltip: { trigger: 'axis' },
+        legend: { top: 0, textStyle: { fontSize: 11 } },
+        grid: { top: 36, bottom: 30, left: 50, right: 10 },
+        xAxis: { type: 'category', data: colleges, axisLabel: { fontSize: 10, rotate: 20 } },
+        yAxis: { type: 'value', name: '学时', axisLabel: { fontSize: 10 } },
+        series: [
+          { name: '研究生', type: 'bar', data: data.map(function(d){return d.graduate}), itemStyle: { color: '#5b95f7' }, label: { show: true, position: 'top', fontSize: 10 } },
+          { name: '本科生', type: 'bar', data: data.map(function(d){return d.undergraduate}), itemStyle: { color: '#36b37e' }, label: { show: true, position: 'top', fontSize: 10 } },
+          { name: '教职工', type: 'bar', data: data.map(function(d){return d.staff}), itemStyle: { color: '#ffb020' }, label: { show: true, position: 'top', fontSize: 10 } }
+        ]
+      });
+      this._statsCharts = this._statsCharts || [];
+      this._statsCharts.push(chart);
+    },
+    examSpanMethod: function(params) {
+      // Only merge college column when no examType filter is active (showing all 3 rows per college)
+      if (this.examFilterExamType) return;
+      var rowIndex = params.rowIndex;
+      var columnIndex = params.columnIndex;
+      if (columnIndex === 0) {
+        if (rowIndex % 3 === 0) return { rowspan: 3, colspan: 1 };
+        return { rowspan: 0, colspan: 0 };
+      }
+    }
+  },
+  watch: {
+    activePage: function() {
+      if (this.activePage === 'workbench') this.$nextTick(this.renderCharts);
+      if (this.activePage === 'statistics') this.renderStatsCharts();
+    },
+    activeRole: function() {
+      var role = this.currentRole;
+      if (role && role.lab && role.lab.category && this.resourceTabs.indexOf(role.lab.category) !== -1) {
+        this.activeResourceTab = role.lab.category;
+      }
+    },
+    statsExamType: function() { this.renderStatsCharts(); }
+  },
+  mounted: function() {
+    var self = this;
+    window.addEventListener('resize', function() { if (self._charts) self._charts.forEach(function(c) { c.resize(); }); });
+    // 加载PRD markdown
+    fetch('../prd/需求规格说明书.md').then(function(res) { return res.text(); }).then(function(md) {
+      self.prdContent = marked.parse(md);
+    }).catch(function() { self.prdContent = '<p>文档加载失败,请确保通过HTTP服务访问。</p>'; });
+  }
+});

+ 479 - 0
prototype_opus/js/data.js

@@ -0,0 +1,479 @@
+/**
+ * 安全教育考试系统 - 模拟数据
+ * 包含6个角色的完整数据定义
+ */
+
+// 分级学时配置(可由管理员修改)
+// firstYear: 准入学时要求(第一年), annual: 第二年往后每年学时要求
+var levelHoursConfig = {
+  'Ⅰ': { firstYear: 24, annual: 8 },
+  'Ⅱ': { firstYear: 16, annual: 4 },
+  'Ⅲ': { firstYear: 8, annual: 2 },
+  'Ⅳ': { firstYear: 4, annual: 0 }
+};
+
+// 实验室数据
+var labList = [
+  { id: 'lab001', name: '有机化学实验室', room: 'A301', building: '理学楼A栋3层', college: '化学与化工学院', director: '张**', level: 'Ⅱ', category: '化学类', type: '教学实验室' },
+  { id: 'lab002', name: '分子生物学实验室', room: 'B205', building: '生命科学楼B栋2层', college: '生命科学学院', director: '李**', level: 'Ⅰ', category: '生物类', type: '科研实验室' },
+  { id: 'lab003', name: '电气工程实验室', room: 'C102', building: '工学楼C栋1层', college: '电气工程学院', director: '王**', level: 'Ⅲ', category: '机电类', type: '教学实验室' },
+  { id: 'lab004', name: '辐射防护实验室', room: 'D401', building: '物理楼D栋4层', college: '物理学院', director: '赵**', level: 'Ⅰ', category: '辐射类', type: '科研实验室' },
+  { id: 'lab005', name: '材料力学实验室', room: 'E203', building: '材料楼E栋2层', college: '材料科学学院', director: '刘**', level: 'Ⅳ', category: '机电类', type: '教学实验室' },
+  { id: 'lab006', name: '无机化学实验室', room: 'A205', building: '理学楼A栋2层', college: '化学与化工学院', director: '陈**', level: 'Ⅲ', category: '化学类', type: '教学实验室' },
+  { id: 'lab007', name: '微生物学实验室', room: 'B308', building: '生命科学楼B栋3层', college: '生命科学学院', director: '孙**', level: 'Ⅱ', category: '生物类', type: '科研实验室' },
+  { id: 'lab008', name: '高分子化学实验室', room: 'A402', building: '理学楼A栋4层', college: '化学与化工学院', director: '周**', level: 'Ⅱ', category: '化学类', type: '科研实验室' },
+  { id: 'lab009', name: '基础物理实验室', room: 'D102', building: '物理楼D栋1层', college: '物理学院', director: '吴**', level: 'Ⅳ', category: '基础类', type: '教学实验室' },
+  { id: 'lab010', name: '智能控制实验室', room: 'C305', building: '工学楼C栋3层', college: '自动化学院', director: '郑**', level: 'Ⅲ', category: '机电类', type: '科研实验室' }
+];
+
+// 角色数据定义
+var roleProfiles = {
+  newGraduate: {
+    name: '刘子衡', code: '2025100001', type: '硕士研究生', org: '资源环境学院',
+    term: '2025-2026 学年', isFirstYear: true,
+    bindingStatus: 'unbound',
+    lab: null,
+    requiredHours: 0, completedHours: 0,
+    examType: '分级考试', examStatus: '待绑定后确认',
+    examUnlocked: false, examScore: null, certificateStatus: '未生成',
+    yearlyRecords: [
+      { year: '2025-2026', score: '—', status: '待启动' }
+    ],
+    tasks: [
+      { title: '绑定实验室', note: '当前未提交', status: 'pending' },
+      { title: '完成准入学时', note: '待绑定后确定学时要求', status: 'locked' },
+      { title: '通过分级考试', note: '完成学时后解锁', status: 'locked' },
+      { title: '获取准入证书', note: '通过考试后生成', status: 'locked' },
+      { title: '实验室安全准入培训', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '新生安全教育第一课', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '危化品识别与存储规范', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '实验室应急疏散演练', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '科研诚信与学术规范', note: '学院下发 · 待开始', status: 'pending' }
+    ],
+    resources: [
+      { title: '待绑定实验室后推荐分级资源', desc: '绑定实验室后将根据分类推荐对应学习资料' }
+    ],
+    resourceType: 'lab-bindable',
+    practice: { done: 46, total: 200, correctRate: '81%' },
+    showBindModal: true
+  },
+  graduateSenior: {
+    name: '李思涵', code: '2023100042', type: '硕士研究生', org: '生命科学学院',
+    term: '2025-2026 学年', isFirstYear: false,
+    bindingStatus: 'approved',
+    lab: { id: 'lab002', name: '分子生物学实验室', room: 'B205', building: '生命科学楼B栋2层', college: '生命科学学院', director: '李**', level: 'Ⅰ', category: '生物类', type: '科研实验室' },
+    requiredHours: 8, completedHours: 6,
+    examType: '应知应会考试', examStatus: '未开始',
+    examUnlocked: false, examScore: null, certificateStatus: '未获得',
+    yearlyRecords: [
+      { year: '2025-2026', score: '—', status: '进行中' },
+      { year: '2024-2025', score: '88', status: '通过' },
+      { year: '2023-2024', score: '92', status: '通过' }
+    ],
+    tasks: [
+      { title: '完成年度安全学时', note: '6 / 8 学时', status: 'in-progress' },
+      { title: '通过应知应会考试', note: '完成学时后解锁', status: 'locked' },
+      { title: '更新准入证书', note: '通过考试后更新', status: 'locked' },
+      { title: '生物安全年度复训', note: '学院下发 · 待完成', status: 'pending' },
+      { title: '实验室危废处理规范学习', note: '学院下发 · 已完成', status: 'in-progress' },
+      { title: '高致病性微生物操作培训', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '实验动物伦理规范学习', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '生物样本运输安全培训', note: '学院下发 · 待开始', status: 'pending' }
+    ],
+    resources: [
+      { title: '生物安全实验室管理规范', desc: '根据绑定实验室(生物类)推荐' },
+      { title: '微生物操作安全指南', desc: '生物类必修资料' }
+    ],
+    resourceType: 'lab-bindable',
+    practice: { done: 156, total: 200, correctRate: '89%' },
+    showBindModal: false
+  },
+  newUndergraduate: {
+    name: '王小雨', code: '2025200156', type: '本科生', org: '电气工程学院',
+    term: '2025-2026 学年', isFirstYear: true,
+    bindingStatus: 'none', lab: null,
+    requiredHours: 4, completedHours: 2,
+    examType: '新生入学考试', examStatus: '未开始',
+    examUnlocked: false, examScore: null, certificateStatus: '未获得',
+    yearlyRecords: [
+      { year: '2025-2026', score: '—', status: '待启动' }
+    ],
+    tasks: [
+      { title: '完成安全通识学习', note: '2 / 4 学时', status: 'in-progress' },
+      { title: '通过新生入学考试', note: '完成学时后解锁', status: 'locked' },
+      { title: '新生安全教育第一课', note: '学院下发 · 已完成', status: 'in-progress' },
+      { title: '消防安全知识学习', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '电气安全基础培训', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '实验室准入制度学习', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '个人防护装备使用培训', note: '学院下发 · 待开始', status: 'pending' }
+    ],
+    resources: [
+      { title: '实验室通用安全规范', desc: '应知应会通识资料' },
+      { title: '消防安全知识手册', desc: '基础安全必修' }
+    ],
+    resourceType: 'general',
+    practice: { done: 45, total: 100, correctRate: '76%' },
+    showBindModal: false
+  },
+  undergraduateSenior: {
+    name: '刘佳琪', code: '2022200089', type: '本科生', org: '材料科学学院',
+    term: '2025-2026 学年', isFirstYear: false,
+    bindingStatus: 'none', lab: null,
+    requiredHours: 0, completedHours: 0,
+    examType: '应知应会考试', examStatus: '未开始',
+    examUnlocked: true, examScore: null, certificateStatus: '未获得',
+    yearlyRecords: [
+      { year: '2025-2026', score: '—', status: '待启动' },
+      { year: '2024-2025', score: '92', status: '通过' },
+      { year: '2023-2024', score: '85', status: '通过' },
+      { year: '2022-2023', score: '78', status: '通过' }
+    ],
+    tasks: [
+      { title: '通过年度应知应会考试', note: '可直接参加', status: 'pending' },
+      { title: '实验室安全年度复训', note: '学院下发 · 待完成', status: 'pending' },
+      { title: '科研伦理知识学习', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '材料安全数据表(MSDS)学习', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '实验室废弃物分类培训', note: '学院下发 · 待开始', status: 'pending' }
+    ],
+    resources: [
+      { title: '实验室通用安全规范', desc: '应知应会通识资料' },
+      { title: '个人防护装备使用', desc: '基础安全资料' }
+    ],
+    resourceType: 'general',
+    practice: { done: 180, total: 200, correctRate: '93%' },
+    showBindModal: false
+  },
+  newStaff: {
+    name: '陈建国', code: 'T2025001', type: '教职工', org: '物理学院',
+    term: '2025-2026 学年', isFirstYear: true,
+    bindingStatus: 'none', lab: null,
+    requiredHours: 4, completedHours: 1,
+    examType: '应知应会考试', examStatus: '未开始',
+    examUnlocked: false, examScore: null, certificateStatus: '未获得',
+    yearlyRecords: [
+      { year: '2025-2026', score: '—', status: '待启动' }
+    ],
+    tasks: [
+      { title: '完成安全通识学习', note: '1 / 4 学时', status: 'in-progress' },
+      { title: '通过应知应会考试', note: '完成学时后解锁', status: 'locked' },
+      { title: '新入职安全培训', note: '人事处下发 · 待完成', status: 'pending' },
+      { title: '实验室管理制度学习', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '辐射安全基础知识', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '实验室安全责任制度', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '危险源辨识与管控', note: '人事处下发 · 待开始', status: 'pending' }
+    ],
+    resources: [
+      { title: '实验室通用安全规范', desc: '应知应会通识资料' },
+      { title: '消防安全知识手册', desc: '基础安全必修' }
+    ],
+    resourceType: 'general',
+    practice: { done: 20, total: 100, correctRate: '70%' },
+    showBindModal: false
+  },
+  existingStaff: {
+    name: '赵丽华', code: 'T2018056', type: '教职工', org: '化学与化工学院',
+    term: '2025-2026 学年', isFirstYear: false,
+    bindingStatus: 'none', lab: null,
+    requiredHours: 0, completedHours: 0,
+    examType: '应知应会考试', examStatus: '未开始',
+    examUnlocked: true, examScore: null, certificateStatus: '未获得',
+    yearlyRecords: [
+      { year: '2025-2026', score: '—', status: '待启动' },
+      { year: '2024-2025', score: '95', status: '通过' },
+      { year: '2023-2024', score: '93', status: '通过' },
+      { year: '2022-2023', score: '90', status: '通过' }
+    ],
+    tasks: [
+      { title: '通过年度应知应会考试', note: '可直接参加', status: 'pending' },
+      { title: '危化品管理年度复训', note: '学院下发 · 待完成', status: 'pending' },
+      { title: '实验室安全责任书签署', note: '学院下发 · 待签署', status: 'pending' },
+      { title: '化学废弃物处理规范复训', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '实验室安全检查制度学习', note: '学院下发 · 待开始', status: 'pending' },
+      { title: '应急预案年度更新培训', note: '学院下发 · 待开始', status: 'pending' }
+    ],
+    resources: [
+      { title: '危险化学品管理规范', desc: '根据所属学院推荐' },
+      { title: '化学废弃物处理指南', desc: '化学类相关资料' }
+    ],
+    resourceType: 'general',
+    practice: { done: 195, total: 200, correctRate: '96%' },
+    showBindModal: false
+  }
+};
+
+// 角色按钮配置
+var roleButtons = [
+  { key: 'newGraduate', label: '新入学研究生' },
+  { key: 'graduateSenior', label: '研二三' },
+  { key: 'newUndergraduate', label: '新入学本科生' },
+  { key: 'undergraduateSenior', label: '本科二三四' },
+  { key: 'newStaff', label: '新入职教职工' },
+  { key: 'existingStaff', label: '存量教职工' }
+];
+
+// 安全素养资源数据
+var resourceTabs = ['化学类', '生物类', '辐射类', '机电类', '基础类', '安全操作及应急处置', '科研伦理安全'];
+
+var resourceData = {
+  '化学类': {
+    docs: [
+      { title: '化学实验室安全手册', type: 'PDF', pages: 45, duration: '45分钟', status: 'new' },
+      { title: '危险化学品管理规范', type: 'PDF', pages: 32, duration: '30分钟', status: 'new' },
+      { title: '化学废弃物处理指南', type: 'PDF', pages: 28, duration: '25分钟', status: 'new' },
+      { title: '有机溶剂安全使用', type: 'PPT', pages: 60, duration: '50分钟', status: 'new' },
+      { title: '实验室通风系统操作', type: 'PDF', pages: 18, duration: '20分钟', status: 'new' },
+      { title: '强酸强碱安全防护', type: 'PDF', pages: 22, duration: '20分钟', status: 'new' }
+    ],
+    questions: {
+      total: 320,
+      categories: [
+        { name: '基础知识', count: 120 },
+        { name: '操作规范', count: 110 },
+        { name: '应急处理', count: 90 }
+      ]
+    }
+  },
+  '生物类': {
+    docs: [
+      { title: '生物安全实验室管理规范', type: 'PDF', pages: 38, duration: '35分钟', status: 'new' },
+      { title: '微生物操作安全指南', type: 'PDF', pages: 25, duration: '25分钟', status: 'new' },
+      { title: '生物样本储存规范', type: 'PPT', pages: 42, duration: '40分钟', status: 'new' },
+      { title: '高压灭菌器操作规程', type: 'PDF', pages: 15, duration: '15分钟', status: 'new' },
+      { title: '生物安全柜使用指南', type: 'PDF', pages: 20, duration: '20分钟', status: 'new' }
+    ],
+    questions: {
+      total: 280,
+      categories: [
+        { name: '基础知识', count: 100 },
+        { name: '操作规范', count: 100 },
+        { name: '应急处理', count: 80 }
+      ]
+    }
+  },
+  '辐射类': {
+    docs: [
+      { title: '辐射防护基础知识', type: 'PDF', pages: 50, duration: '50分钟', status: 'new' },
+      { title: '放射性同位素安全操作', type: 'PDF', pages: 35, duration: '35分钟', status: 'new' },
+      { title: '辐射监测与防护设备', type: 'PPT', pages: 48, duration: '45分钟', status: 'new' },
+      { title: '辐射事故应急处理', type: 'PDF', pages: 30, duration: '30分钟', status: 'new' }
+    ],
+    questions: {
+      total: 200,
+      categories: [
+        { name: '基础知识', count: 80 },
+        { name: '防护措施', count: 70 },
+        { name: '应急处理', count: 50 }
+      ]
+    }
+  },
+  '机电类': {
+    docs: [
+      { title: '机电设备安全操作规程', type: 'PDF', pages: 40, duration: '40分钟', status: 'new' },
+      { title: '电气安全基础知识', type: 'PDF', pages: 30, duration: '30分钟', status: 'new' },
+      { title: '机械加工安全防护', type: 'PPT', pages: 55, duration: '50分钟', status: 'new' },
+      { title: '特种设备安全管理', type: 'PDF', pages: 28, duration: '25分钟', status: 'new' }
+    ],
+    questions: {
+      total: 260,
+      categories: [
+        { name: '基础知识', count: 90 },
+        { name: '操作规范', count: 100 },
+        { name: '应急处理', count: 70 }
+      ]
+    }
+  },
+  '基础类': {
+    docs: [
+      { title: '实验室通用安全规范', type: 'PDF', pages: 35, duration: '35分钟', status: 'new' },
+      { title: '消防安全知识手册', type: 'PDF', pages: 28, duration: '25分钟', status: 'new' },
+      { title: '实验室准入制度说明', type: 'PPT', pages: 30, duration: '30分钟', status: 'new' },
+      { title: '个人防护装备使用', type: 'PDF', pages: 20, duration: '20分钟', status: 'new' },
+      { title: '实验室安全标识识别', type: 'PDF', pages: 16, duration: '15分钟', status: 'new' }
+    ],
+    questions: {
+      total: 350,
+      categories: [
+        { name: '通用安全', count: 130 },
+        { name: '消防知识', count: 120 },
+        { name: '准入制度', count: 100 }
+      ]
+    }
+  },
+  '安全操作及应急处置': {
+    docs: [
+      { title: '实验室应急预案汇编', type: 'PDF', pages: 60, duration: '55分钟', status: 'new' },
+      { title: '常见事故应急处理流程', type: 'PDF', pages: 42, duration: '40分钟', status: 'new' },
+      { title: '急救知识与技能', type: 'PPT', pages: 38, duration: '35分钟', status: 'new' },
+      { title: '化学品泄漏处置方案', type: 'PDF', pages: 25, duration: '25分钟', status: 'new' }
+    ],
+    questions: {
+      total: 180,
+      categories: [
+        { name: '应急预案', count: 60 },
+        { name: '急救技能', count: 65 },
+        { name: '事故处理', count: 55 }
+      ]
+    }
+  },
+  '科研伦理安全': {
+    docs: [
+      { title: '科研伦理基本准则', type: 'PDF', pages: 30, duration: '30分钟', status: 'new' },
+      { title: '实验动物伦理规范', type: 'PDF', pages: 25, duration: '25分钟', status: 'new' },
+      { title: '学术诚信与数据安全', type: 'PPT', pages: 35, duration: '35分钟', status: 'new' }
+    ],
+    questions: {
+      total: 150,
+      categories: [
+        { name: '伦理准则', count: 55 },
+        { name: '动物实验', count: 50 },
+        { name: '数据安全', count: 45 }
+      ]
+    }
+  }
+};
+
+// 视频资源数据
+var videoTabs = ['化学类', '生物类', '辐射类', '机电类', '其他类'];
+
+var videoData = {
+  '化学类': [
+    { id: 'v01', title: '化学实验室安全操作规范', duration: '15:30', cover: '', views: 1250 },
+    { id: 'v02', title: '危险化学品存储与管理', duration: '12:45', cover: '', views: 980 },
+    { id: 'v03', title: '化学品泄漏应急处理', duration: '08:20', cover: '', views: 1560 },
+    { id: 'v04', title: '通风橱正确使用方法', duration: '06:15', cover: '', views: 2100 },
+    { id: 'v05', title: '有机溶剂安全使用指南', duration: '10:40', cover: '', views: 870 },
+    { id: 'v06', title: '化学废液处理流程', duration: '09:55', cover: '', views: 1340 }
+  ],
+  '生物类': [
+    { id: 'v07', title: '生物安全柜操作规范', duration: '11:20', cover: '', views: 1100 },
+    { id: 'v08', title: '微生物实验安全防护', duration: '14:30', cover: '', views: 890 },
+    { id: 'v09', title: '生物样本运输规范', duration: '07:45', cover: '', views: 650 },
+    { id: 'v10', title: '高压灭菌器使用教程', duration: '09:10', cover: '', views: 1420 },
+    { id: 'v11', title: '生物废弃物处理', duration: '08:30', cover: '', views: 760 }
+  ],
+  '辐射类': [
+    { id: 'v12', title: '辐射防护基本原则', duration: '13:20', cover: '', views: 920 },
+    { id: 'v13', title: '个人剂量监测方法', duration: '10:15', cover: '', views: 680 },
+    { id: 'v14', title: '放射源安全管理', duration: '16:40', cover: '', views: 540 },
+    { id: 'v15', title: 'X射线设备安全操作', duration: '11:50', cover: '', views: 730 }
+  ],
+  '机电类': [
+    { id: 'v16', title: '机床安全操作规程', duration: '12:30', cover: '', views: 1050 },
+    { id: 'v17', title: '电气安全与触电急救', duration: '14:20', cover: '', views: 1680 },
+    { id: 'v18', title: '起重设备安全使用', duration: '09:45', cover: '', views: 580 },
+    { id: 'v19', title: '焊接作业安全防护', duration: '11:10', cover: '', views: 720 },
+    { id: 'v20', title: '压力容器安全知识', duration: '13:55', cover: '', views: 490 }
+  ],
+  '其他类': [
+    { id: 'v21', title: '实验室消防安全', duration: '10:30', cover: '', views: 2350 },
+    { id: 'v22', title: '实验室急救基础', duration: '15:20', cover: '', views: 1890 },
+    { id: 'v23', title: '个人防护装备使用', duration: '08:40', cover: '', views: 1560 },
+    { id: 'v24', title: '实验室废弃物分类', duration: '07:25', cover: '', views: 1120 }
+  ]
+};
+
+// 侧边栏菜单
+var menuItems = [
+  { key: 'workbench', icon: 'el-icon-s-platform', label: '工作台' },
+  { key: 'statistics', icon: 'el-icon-s-data', label: '数据统计' },
+  { key: 'config', icon: 'el-icon-setting', label: '系统配置' }
+];
+
+// 数据统计 - Mock数据
+var statisticsData = {
+  colleges: ['化学与化工学院', '生命科学学院', '物理学院', '电气工程学院', '材料科学学院', '机械工程学院', '自动化学院', '资源环境学院'],
+  // 研究生考试情况(区分新生分级考试 + 老生应知应会)
+  graduateExamDetail: [
+    { college: '化学与化工学院', newTotal: 45, newPassed: 38, newFailed: 4, newAbsent: 3, seniorTotal: 75, seniorPassed: 68, seniorFailed: 4, seniorAbsent: 3 },
+    { college: '生命科学学院', newTotal: 35, newPassed: 30, newFailed: 3, newAbsent: 2, seniorTotal: 60, seniorPassed: 54, seniorFailed: 4, seniorAbsent: 2 },
+    { college: '物理学院', newTotal: 28, newPassed: 23, newFailed: 3, newAbsent: 2, seniorTotal: 50, seniorPassed: 44, seniorFailed: 3, seniorAbsent: 3 },
+    { college: '电气工程学院', newTotal: 40, newPassed: 35, newFailed: 3, newAbsent: 2, seniorTotal: 70, seniorPassed: 63, seniorFailed: 4, seniorAbsent: 3 },
+    { college: '材料科学学院', newTotal: 32, newPassed: 28, newFailed: 2, newAbsent: 2, seniorTotal: 56, seniorPassed: 50, seniorFailed: 3, seniorAbsent: 3 },
+    { college: '机械工程学院', newTotal: 25, newPassed: 21, newFailed: 2, newAbsent: 2, seniorTotal: 47, seniorPassed: 42, seniorFailed: 3, seniorAbsent: 2 },
+    { college: '自动化学院', newTotal: 22, newPassed: 20, newFailed: 1, newAbsent: 1, seniorTotal: 43, seniorPassed: 39, seniorFailed: 2, seniorAbsent: 2 },
+    { college: '资源环境学院', newTotal: 20, newPassed: 17, newFailed: 2, newAbsent: 1, seniorTotal: 36, seniorPassed: 32, seniorFailed: 2, seniorAbsent: 2 }
+  ],
+  // 实验室各学院分级人数
+  labCollegeStats: [
+    { college: '化学与化工学院', level1: 18, level2: 45, level3: 62, level4: 25 },
+    { college: '生命科学学院', level1: 32, level2: 38, level3: 48, level4: 20 },
+    { college: '物理学院', level1: 25, level2: 30, level3: 42, level4: 18 },
+    { college: '电气工程学院', level1: 15, level2: 42, level3: 58, level4: 30 },
+    { college: '材料科学学院', level1: 20, level2: 35, level3: 50, level4: 22 },
+    { college: '机械工程学院', level1: 12, level2: 38, level3: 55, level4: 28 },
+    { college: '自动化学院', level1: 18, level2: 32, level3: 45, level4: 20 },
+    { college: '资源环境学院', level1: 16, level2: 28, level3: 40, level4: 18 }
+  ],
+  // 本科生考试情况(区分新生入学考试 + 应知应会考试)
+  undergraduateExam: [
+    { college: '化学与化工学院', freshTotal: 80, freshPassed: 72, freshFailed: 5, freshAbsent: 3, seniorTotal: 200, seniorPassed: 185, seniorFailed: 8, seniorAbsent: 7 },
+    { college: '生命科学学院', freshTotal: 65, freshPassed: 60, freshFailed: 3, freshAbsent: 2, seniorTotal: 155, seniorPassed: 142, seniorFailed: 7, seniorAbsent: 6 },
+    { college: '物理学院', freshTotal: 55, freshPassed: 48, freshFailed: 4, freshAbsent: 3, seniorTotal: 140, seniorPassed: 128, seniorFailed: 7, seniorAbsent: 5 },
+    { college: '电气工程学院', freshTotal: 90, freshPassed: 82, freshFailed: 5, freshAbsent: 3, seniorTotal: 220, seniorPassed: 200, seniorFailed: 12, seniorAbsent: 8 },
+    { college: '材料科学学院', freshTotal: 50, freshPassed: 45, freshFailed: 3, freshAbsent: 2, seniorTotal: 130, seniorPassed: 118, seniorFailed: 7, seniorAbsent: 5 },
+    { college: '机械工程学院', freshTotal: 70, freshPassed: 63, freshFailed: 4, freshAbsent: 3, seniorTotal: 170, seniorPassed: 152, seniorFailed: 10, seniorAbsent: 8 },
+    { 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 }
+  ],
+  // 实验室分级人数
+  labLevelStats: [
+    { level: 'Ⅰ级(重大风险)', count: 156, color: '#e84d5b' },
+    { level: 'Ⅱ级(高风险)', count: 342, color: '#ff8c00' },
+    { level: 'Ⅲ级(中风险)', count: 528, color: '#ffb020' },
+    { level: 'Ⅳ级(低风险)', count: 215, color: '#5b95f7' }
+  ],
+  // 实验室绑定学生名单(示例)
+  labStudents: [
+    { name: '张明远', code: '2025100001', college: '化学与化工学院', lab: '有机化学实验室(A301)', level: 'Ⅱ' },
+    { name: '李思涵', code: '2023100042', college: '生命科学学院', lab: '分子生物学实验室(B205)', level: 'Ⅰ' },
+    { name: '王浩然', code: '2024100088', college: '物理学院', lab: '辐射防护实验室(D401)', level: 'Ⅰ' },
+    { name: '赵雨晴', code: '2025100023', college: '电气工程学院', lab: '电气工程实验室(C102)', level: 'Ⅲ' },
+    { name: '陈志强', code: '2024100056', college: '材料科学学院', lab: '材料力学实验室(E203)', level: 'Ⅳ' },
+    { name: '刘芳华', code: '2025100078', college: '化学与化工学院', lab: '高分子化学实验室(A402)', level: 'Ⅱ' },
+    { name: '孙晓明', code: '2023100091', college: '生命科学学院', lab: '微生物学实验室(B308)', level: 'Ⅱ' },
+    { name: '周文博', code: '2025100034', college: '自动化学院', lab: '智能控制实验室(C305)', level: 'Ⅲ' },
+    { name: '吴佳琪', code: '2024100067', college: '化学与化工学院', lab: '无机化学实验室(A205)', level: 'Ⅲ' },
+    { name: '郑天宇', code: '2025100045', college: '物理学院', lab: '基础物理实验室(D102)', level: 'Ⅳ' }
+  ],
+  // 应知应会学习时长(各学院平均)
+  studyHours: [
+    { college: '化学与化工学院', avgHours: 6.8 },
+    { college: '生命科学学院', avgHours: 5.9 },
+    { college: '物理学院', avgHours: 5.2 },
+    { college: '电气工程学院', avgHours: 6.1 },
+    { college: '材料科学学院', avgHours: 5.5 },
+    { college: '机械工程学院', avgHours: 4.8 },
+    { college: '自动化学院', avgHours: 5.3 },
+    { college: '资源环境学院', avgHours: 6.4 }
+  ],
+  // 学院个人学习时长明细(含每年学时)
+  studyDetail: [
+    { name: '赵丽华', code: 'T2018056', college: '化学与化工学院', type: '教职工', grade: '2018级', y1: 4.0, y2: 2.5, y3: 1.8, y4: 0, totalHours: 8.3 },
+    { name: '刘佳琪', code: '2022200089', college: '材料科学学院', type: '本科生', grade: '2022级', y1: 4.0, y2: 1.2, y3: 0.8, y4: 0, totalHours: 6.0 },
+    { name: '王建军', code: 'T2020012', college: '物理学院', type: '教职工', grade: '2020级', y1: 4.0, y2: 1.5, y3: 0, y4: 0, totalHours: 5.5 },
+    { name: '陈小红', code: '2021200134', college: '生命科学学院', type: '本科生', grade: '2021级', y1: 4.0, y2: 1.8, y3: 1.5, y4: 0.8, totalHours: 8.1 },
+    { name: '张伟', code: 'T2019033', college: '电气工程学院', type: '教职工', grade: '2019级', y1: 4.0, y2: 2.0, y3: 1.2, y4: 0, totalHours: 7.2 },
+    { name: '李明', code: '2022200201', college: '机械工程学院', type: '本科生', grade: '2022级', y1: 4.0, y2: 0.8, y3: 0, y4: 0, totalHours: 4.8 },
+    { name: '周芳', code: 'T2017045', college: '化学与化工学院', type: '教职工', grade: '2017级', y1: 4.0, y2: 2.2, y3: 1.8, y4: 1.5, totalHours: 9.5 },
+    { name: '吴强', code: '2021200089', college: '自动化学院', type: '本科生', grade: '2021级', y1: 4.0, y2: 1.5, y3: 1.2, y4: 0.5, totalHours: 7.2 },
+    { name: '李思涵', code: '2023100042', college: '生命科学学院', type: '研究生', grade: '2023级', y1: 24.0, y2: 8.0, y3: 0, y4: 0, totalHours: 32.0 },
+    { name: '张明远', code: '2025100001', college: '化学与化工学院', type: '研究生', grade: '2025级', y1: 16.0, y2: 0, y3: 0, y4: 0, totalHours: 16.0 },
+    { name: '王浩然', code: '2024100088', college: '物理学院', type: '研究生', grade: '2024级', y1: 24.0, y2: 6.0, y3: 0, y4: 0, totalHours: 30.0 },
+    { name: '赵雨晴', code: '2025100023', college: '电气工程学院', type: '研究生', grade: '2025级', y1: 8.0, y2: 0, y3: 0, y4: 0, totalHours: 8.0 },
+    { name: '陈志强', code: '2024100056', college: '材料科学学院', type: '研究生', grade: '2024级', y1: 4.0, y2: 0, y3: 0, y4: 0, totalHours: 4.0 },
+    { name: '刘芳华', code: '2025100078', college: '化学与化工学院', type: '研究生', grade: '2025级', y1: 16.0, y2: 0, y3: 0, y4: 0, totalHours: 16.0 },
+    { name: '孙晓明', code: '2023100091', college: '生命科学学院', type: '研究生', grade: '2023级', y1: 16.0, y2: 4.0, y3: 0, y4: 0, totalHours: 20.0 },
+    { name: '周文博', code: '2025100034', college: '自动化学院', type: '研究生', grade: '2025级', y1: 8.0, y2: 0, y3: 0, y4: 0, totalHours: 8.0 }
+  ],
+  // 各学院分类型学习总时长(用于柱状图)
+  studyHoursByType: [
+    { college: '化学与化工学院', graduate: 320, undergraduate: 180, staff: 95 },
+    { college: '生命科学学院', graduate: 280, undergraduate: 160, staff: 72 },
+    { college: '物理学院', graduate: 240, undergraduate: 145, staff: 68 },
+    { college: '电气工程学院', graduate: 300, undergraduate: 195, staff: 85 },
+    { college: '材料科学学院', graduate: 220, undergraduate: 135, staff: 60 },
+    { college: '机械工程学院', graduate: 195, undergraduate: 170, staff: 55 },
+    { college: '自动化学院', graduate: 180, undergraduate: 120, staff: 48 },
+    { college: '资源环境学院', graduate: 165, undergraduate: 110, staff: 52 }
+  ]
+};

文件差異過大導致無法顯示
+ 45 - 0
prototype_opus/vendor/echarts.min.js


文件差異過大導致無法顯示
+ 47 - 3
userReq/prompt.md