anne дней назад: 4
Сommit
f61039fc50

+ 7 - 0
.claude/settings.local.json

@@ -0,0 +1,7 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(node \"H:/lab-safety-monitor/exam_main_2026/_fix_detail.js\")"
+    ]
+  }
+}

+ 1 - 0
_newcss.js

@@ -0,0 +1 @@
+// placeholder

Разница между файлами не показана из-за своего большого размера
+ 3668 - 0
admin.html


+ 499 - 0
admin_requirements.html

@@ -0,0 +1,499 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=1920">
+<title>实验室安全教育考试系统 · 管理后台使用说明文档</title>
+<style>
+*{box-sizing:border-box;margin:0;padding:0;}
+:root{
+  --blue:#1857d4;--blue-mid:#2e6fe0;--blue-light:#eef3fd;--blue-xlight:#f5f8ff;
+  --green:#10a37f;--green-light:#edfaf6;
+  --orange:#e67e22;--orange-light:#fef6ec;
+  --red:#e53e3e;--red-light:#fff5f5;
+  --purple:#722ed1;--purple-light:#f9f0ff;
+  --gray:#8896a7;--gray-light:#f4f6fa;--gray-border:#dde3ed;
+  --text-primary:#1a2233;--text-secondary:#4a5568;--text-hint:#8896a7;
+  --bg:#f0f4fb;--white:#fff;
+  --shadow:0 2px 12px rgba(24,87,212,.08);
+  --shadow-md:0 4px 24px rgba(24,87,212,.12);
+}
+body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;background:#f0f4fb;color:var(--text-primary);font-size:15px;line-height:1.7;min-width:1920px;}
+
+/* ===== LAYOUT ===== */
+.page{max-width:1920px;min-height:1080px;margin:0 auto;display:flex;flex-direction:column;}
+.doc-header{background:linear-gradient(135deg,#1857d4 0%,#2e6fe0 60%,#0f3ea8 100%);color:#fff;padding:48px 80px 40px;position:relative;overflow:hidden;}
+.doc-header::before{content:'';position:absolute;inset:0;background:url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.04'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");}
+.doc-header-inner{position:relative;z-index:1;display:flex;align-items:flex-end;justify-content:space-between;}
+.doc-title{font-size:36px;font-weight:800;letter-spacing:1px;margin-bottom:10px;}
+.doc-subtitle{font-size:16px;opacity:.82;letter-spacing:.5px;}
+.doc-meta{display:flex;gap:32px;font-size:13px;opacity:.75;margin-top:14px;}
+.doc-meta span{display:flex;align-items:center;gap:6px;}
+.doc-badge{background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.3);border-radius:20px;padding:6px 18px;font-size:13px;font-weight:600;}
+
+/* ===== NAV SIDEBAR ===== */
+.doc-body{display:flex;flex:1;}
+.sidebar{width:260px;flex-shrink:0;background:var(--white);border-right:1px solid var(--gray-border);padding:24px 0;position:sticky;top:0;height:calc(100vh - 148px);overflow-y:auto;}
+.sidebar::-webkit-scrollbar{width:4px;}
+.sidebar::-webkit-scrollbar-thumb{background:rgba(24,87,212,.15);border-radius:2px;}
+.nav-section{padding:0 20px;margin-bottom:6px;}
+.nav-section-title{font-size:11px;font-weight:700;color:var(--text-hint);letter-spacing:1.5px;text-transform:uppercase;padding:14px 4px 6px;}
+.nav-item{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:8px;font-size:13px;color:var(--text-secondary);cursor:pointer;text-decoration:none;transition:all .15s;}
+.nav-item:hover{background:var(--blue-xlight);color:var(--blue);}
+.nav-item.active{background:var(--blue-light);color:var(--blue);font-weight:600;}
+.nav-item .nav-icon{width:20px;height:20px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0;}
+
+/* ===== MAIN CONTENT ===== */
+.main-content{flex:1;padding:40px 60px;max-width:1580px;}
+.section{margin-bottom:56px;}
+.section-title{font-size:24px;font-weight:800;color:var(--text-primary);margin-bottom:6px;display:flex;align-items:center;gap:10px;}
+.section-title::before{content:'';width:4px;height:24px;background:linear-gradient(180deg,var(--blue-mid),var(--blue));border-radius:3px;display:inline-block;}
+.section-desc{font-size:14px;color:var(--text-hint);margin-bottom:28px;padding-left:14px;}
+.sub-section{margin-bottom:36px;}
+.sub-title{font-size:17px;font-weight:700;color:var(--text-primary);margin-bottom:14px;padding-left:12px;border-left:3px solid var(--blue-mid);}
+
+/* ===== FEATURE CARDS ===== */
+.feature-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:24px;}
+.feature-grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-bottom:24px;}
+.feature-grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:18px;margin-bottom:24px;}
+.feature-card{background:var(--white);border:1px solid var(--gray-border);border-radius:14px;padding:22px;box-shadow:var(--shadow);transition:box-shadow .2s;}
+.feature-card:hover{box-shadow:var(--shadow-md);}
+.feature-card-header{display:flex;align-items:center;gap:12px;margin-bottom:14px;}
+.feature-icon{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;}
+.feature-name{font-size:15px;font-weight:700;color:var(--text-primary);}
+.feature-tag{font-size:11px;color:var(--text-hint);margin-top:2px;}
+.feature-list{list-style:none;padding:0;}
+.feature-list li{font-size:13px;color:var(--text-secondary);padding:5px 0;border-bottom:1px solid var(--gray-light);display:flex;align-items:flex-start;gap:8px;line-height:1.6;}
+.feature-list li:last-child{border-bottom:none;}
+.feature-list li::before{content:'·';color:var(--blue);font-size:16px;flex-shrink:0;line-height:1.4;}
+
+/* ===== SPEC TABLE ===== */
+.spec-table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:16px;}
+.spec-table th{background:var(--blue-xlight);color:var(--blue);font-weight:700;padding:10px 16px;text-align:left;border:1px solid var(--blue-light);}
+.spec-table td{padding:10px 16px;border:1px solid var(--gray-border);vertical-align:top;line-height:1.7;}
+.spec-table tr:nth-child(even) td{background:#fafbfd;}
+.spec-table tr:hover td{background:var(--blue-xlight);}
+
+/* ===== TAGS ===== */
+.tag{display:inline-flex;align-items:center;padding:2px 10px;border-radius:20px;font-size:11px;font-weight:600;}
+.tag-blue{background:var(--blue-light);color:var(--blue);}
+.tag-green{background:var(--green-light);color:var(--green);}
+.tag-orange{background:var(--orange-light);color:var(--orange);}
+.tag-red{background:var(--red-light);color:var(--red);}
+.tag-gray{background:var(--gray-light);color:var(--gray);}
+.tag-purple{background:var(--purple-light);color:var(--purple);}
+
+/* ===== FLOW ===== */
+.flow-row{display:flex;align-items:stretch;gap:0;margin-bottom:24px;flex-wrap:wrap;}
+.flow-step{background:var(--white);border:1px solid var(--gray-border);border-radius:12px;padding:16px 20px;min-width:160px;flex:1;position:relative;box-shadow:var(--shadow);}
+.flow-arrow{display:flex;align-items:center;padding:0 10px;color:var(--blue);font-size:22px;flex-shrink:0;}
+.flow-num{width:24px;height:24px;border-radius:50%;background:var(--blue);color:#fff;font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;margin-bottom:8px;}
+.flow-name{font-size:13px;font-weight:700;color:var(--text-primary);margin-bottom:4px;}
+.flow-desc{font-size:12px;color:var(--text-hint);}
+
+/* ===== NOTICE BOX ===== */
+.notice{border-radius:10px;padding:14px 18px;margin-bottom:18px;display:flex;gap:12px;align-items:flex-start;font-size:13px;line-height:1.7;}
+.notice-info{background:var(--blue-xlight);border:1px solid var(--blue-light);}
+.notice-warn{background:#fffbe6;border:1px solid #ffe58f;}
+.notice-icon{font-size:18px;flex-shrink:0;margin-top:1px;}
+
+/* ===== OVERVIEW HERO ===== */
+.overview-hero{display:grid;grid-template-columns:1fr 1fr 1fr 1fr 1fr;gap:16px;margin-bottom:32px;}
+.stat-card{background:var(--white);border-radius:14px;padding:22px 20px;border:1px solid var(--gray-border);box-shadow:var(--shadow);text-align:center;}
+.stat-num{font-size:32px;font-weight:800;background:linear-gradient(135deg,var(--blue-mid),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;line-height:1.1;}
+.stat-label{font-size:12px;color:var(--text-hint);margin-top:6px;}
+
+/* ===== SCREEN OVERVIEW GRID ===== */
+.screen-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-bottom:24px;}
+.screen-card{background:var(--white);border:1px solid var(--gray-border);border-radius:12px;padding:16px;box-shadow:var(--shadow);}
+.screen-card-icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;margin-bottom:10px;}
+.screen-card-name{font-size:13px;font-weight:700;margin-bottom:4px;}
+.screen-card-desc{font-size:11px;color:var(--text-hint);line-height:1.5;}
+
+/* ===== DIVIDER ===== */
+.divider{height:1px;background:var(--gray-border);margin:40px 0;}
+
+/* ===== FOOTER ===== */
+.doc-footer{background:var(--white);border-top:1px solid var(--gray-border);padding:24px 80px;display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--text-hint);}
+
+/* ===== MENU TREE ===== */
+.menu-tree{background:var(--white);border:1px solid var(--gray-border);border-radius:14px;padding:24px 28px;margin-bottom:24px;box-shadow:var(--shadow);}
+.menu-l1{font-size:15px;font-weight:700;color:var(--text-primary);margin-bottom:10px;display:flex;align-items:center;gap:8px;}
+.menu-l1-icon{width:28px;height:28px;background:var(--blue-light);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;}
+.menu-l2-list{margin-left:36px;border-left:2px solid var(--blue-light);padding-left:16px;}
+.menu-l2{font-size:13px;color:var(--text-secondary);padding:6px 0;display:flex;align-items:flex-start;gap:8px;line-height:1.6;}
+.menu-l2-dot{width:6px;height:6px;border-radius:50%;background:var(--blue-mid);flex-shrink:0;margin-top:7px;}
+</style>
+</head>
+<body>
+<div class="page">
+<!-- ===== HEADER ===== -->
+<div class="doc-header">
+  <div class="doc-header-inner">
+    <div>
+      <div style="font-size:13px;opacity:.7;margin-bottom:8px;letter-spacing:2px;">LABORATORY SAFETY EDUCATION SYSTEM · ADMIN BACKEND</div>
+      <div class="doc-title">实验室安全教育考试系统</div>
+      <div class="doc-subtitle">管理后台 (admin.html) · 产品使用说明文档</div>
+      <div class="doc-meta">
+        <span>&#128187; PC 管理后台</span>
+        <span>&#128197; 文档版本:v1.0 · 2026-08</span>
+        <span>&#128274; 适用范围:培训管理员 / 考试配置管理员 / 系统管理员</span>
+        <span>&#128421; 推荐分辨率:1920 × 1080</span>
+      </div>
+    </div>
+    <div style="text-align:right;">
+      <div class="doc-badge">&#128295; 管理后台 v1.0</div>
+      <div style="margin-top:12px;font-size:13px;opacity:.7;">共 2 个一级菜单 · 5 个功能页面 · 3 种考试类型</div>
+    </div>
+  </div>
+</div>
+
+<!-- ===== BODY ===== -->
+<div class="doc-body">
+<!-- SIDEBAR -->
+<div class="sidebar">
+  <div class="nav-section">
+    <div class="nav-section-title">概览</div>
+    <a class="nav-item active" href="#overview"><span class="nav-icon" style="background:#eef3fd;">&#127968;</span>系统概述</a>
+    <a class="nav-item" href="#arch"><span class="nav-icon" style="background:#eef3fd;">&#128194;</span>页面架构</a>
+  </div>
+  <div class="nav-section">
+    <div class="nav-section-title">功能模块</div>
+    <a class="nav-item" href="#training"><span class="nav-icon" style="background:#fff7e6;">&#127979;</span>培训活动</a>
+    <a class="nav-item" href="#training-plan"><span class="nav-icon" style="background:#fff7e6;">&#128203;</span>培训计划</a>
+    <a class="nav-item" href="#offline-training"><span class="nav-icon" style="background:#fff7e6;">&#127908;</span>线下培训</a>
+    <a class="nav-item" href="#exam"><span class="nav-icon" style="background:#eef3fd;">&#127891;</span>考试管理</a>
+  </div>
+  <div class="nav-section">
+    <div class="nav-section-title">权限说明</div>
+    <a class="nav-item" href="#roles"><span class="nav-icon" style="background:#f9f0ff;">&#128274;</span>角色权限</a>
+  </div>
+</div>
+
+<!-- MAIN CONTENT -->
+<div class="main-content">
+
+<!-- ===== OVERVIEW ===== -->
+<div class="section" id="overview">
+  <div class="section-title">系统概述</div>
+  <div class="section-desc">实验室安全教育考试系统管理后台,面向培训管理员、考试配置管理员和系统管理员,提供培训活动管理与考试模板配置功能。</div>
+  <div class="overview-hero">
+    <div class="stat-card"><div class="stat-num">2</div><div class="stat-label">一级菜单</div></div>
+    <div class="stat-card"><div class="stat-num">5</div><div class="stat-label">功能页面</div></div>
+    <div class="stat-card"><div class="stat-num">3</div><div class="stat-label">考试类型</div></div>
+    <div class="stat-card"><div class="stat-num">1920<span style="font-size:18px;">×</span>1080</div><div class="stat-label">推荐分辨率</div></div>
+    <div class="stat-card"><div class="stat-num" style="font-size:22px;">PC端</div><div class="stat-label">运行平台</div></div>
+  </div>
+  <div class="notice notice-info"><span class="notice-icon">&#128187;</span><div>本文档描述的是 <strong>PC 管理后台(admin.html)</strong>,面向具有管理权限的管理员用户使用。推荐在 1920×1080 分辨率下全屏访问,以获得最佳使用体验。管理员账号由系统超级管理员统一分配,普通学生无法访问管理后台。</div></div>
+  <div class="sub-title">管理员角色说明</div>
+  <div class="feature-grid-3">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">&#127979;</div><div><div class="feature-name">培训管理员</div><div class="feature-tag">Training Admin</div></div></div>
+      <ul class="feature-list">
+        <li>发布与管理培训计划(新建、编辑、发布/草稿切换)</li>
+        <li>管理线下培训活动列表与详情</li>
+        <li>查看培训签到统计数据与参与名单</li>
+        <li>展示和刷新签到二维码</li>
+        <li>查看已签到与未签到人员名单</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#127891;</div><div><div class="feature-name">考试配置管理员</div><div class="feature-tag">Exam Config Admin</div></div></div>
+      <ul class="feature-list">
+        <li>配置开学考试、分级考试、应知应会三类考试模板</li>
+        <li>设置考试基本参数(名称/时长/满分/及格分/答题次数)</li>
+        <li>配置考试适用范围(学院/专业/年级)</li>
+        <li>管理试卷配置(题目数量/题型比例/随机规则)</li>
+        <li>配置考前学习要求与证书颁发条件</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#f9f0ff;">&#9881;</div><div><div class="feature-name">系统管理员</div><div class="feature-tag">Super Admin</div></div></div>
+      <ul class="feature-list">
+        <li>拥有管理后台全部操作权限</li>
+        <li>可执行培训管理员的所有操作</li>
+        <li>可执行考试配置管理员的所有操作</li>
+        <li>管理其他管理员账号的角色与权限分配</li>
+        <li>查看全局数据统计与系统运行状态</li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== PAGE ARCHITECTURE ===== -->
+<div class="section" id="arch">
+  <div class="section-title">页面架构</div>
+  <div class="section-desc">管理后台共包含 2 个一级菜单、5 个功能页面,通过左侧菜单导航切换,页面布局为经典的左侧菜单 + 右侧内容区结构。</div>
+  <div class="notice notice-info"><span class="notice-icon">&#128194;</span><div>管理后台采用 <strong>左侧固定菜单 + 右侧内容区</strong> 的经典后台布局。一级菜单点击可展开/收起二级菜单,二级菜单点击切换右侧内容页面。顶部通栏显示系统名称、当前管理员账号与退出入口。</div></div>
+  <div class="sub-title">菜单结构说明</div>
+  <div style="display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-bottom:24px;">
+    <div class="menu-tree">
+      <div class="menu-l1"><div class="menu-l1-icon">&#127979;</div>一级菜单:培训活动</div>
+      <div class="menu-l2-list">
+        <div class="menu-l2"><div class="menu-l2-dot"></div><div><strong>培训计划</strong> — 管理培训计划列表,支持新建、编辑、发布/草稿状态切换,表格展示培训名称、分类、时间、签到人数</div></div>
+        <div class="menu-l2"><div class="menu-l2-dot"></div><div><strong>线下培训</strong> — 线下培训活动列表,支持卡片/列表视图切换,点击活动卡片可查看培训详情(签到统计、已签到/未签到名单、二维码展示)</div></div>
+      </div>
+    </div>
+    <div class="menu-tree">
+      <div class="menu-l1"><div class="menu-l1-icon" style="background:#eef3fd;">&#127891;</div>一级菜单:考试管理</div>
+      <div class="menu-l2-list">
+        <div class="menu-l2"><div class="menu-l2-dot"></div><div><strong>考试模板</strong> — 配置考试模板,分三个子 Tab(开学考试 / 分级考试 / 应知应会),每个 Tab 内有左侧子导航(基本配置 / 考试范围 / 试卷配置 / 考前配置 / 获得证书)</div></div>
+      </div>
+    </div>
+  </div>
+  <div class="sub-title">功能页面一览</div>
+  <div class="screen-grid" style="grid-template-columns:repeat(3,1fr);">
+    <div class="screen-card"><div class="screen-card-icon" style="background:#fff7e6;">&#128203;</div><div class="screen-card-name">page-training-plan</div><div class="screen-card-desc">培训计划列表页,表格展示所有培训计划,支持搜索过滤、状态切换,含新建入口</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#fff7e6;">&#10133;</div><div class="screen-card-name">page-training-new</div><div class="screen-card-desc">新建/编辑培训表单页,填写培训基本信息,支持保存草稿与直接发布</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#fff7e6;">&#128202;</div><div class="screen-card-name">page-training-record</div><div class="screen-card-desc">培训详情与签到记录页,展示签到统计、二维码、已签到/未签到名单</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#fff7e6;">&#127908;</div><div class="screen-card-name">page-training-activity-list</div><div class="screen-card-desc">线下培训活动列表,支持卡片/列表视图切换,可按活动状态筛选</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#fff7e6;">&#128205;</div><div class="screen-card-name">page-training-activity-detail</div><div class="screen-card-desc">线下培训活动详情,含基本信息、签到统计高亮区域、两个人员名单 Tab</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#eef3fd;">&#127891;</div><div class="screen-card-name">page-exam-template</div><div class="screen-card-desc">考试模板配置页,三个考试类型 Tab,每个 Tab 含五个配置子导航</div></div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== TRAINING MODULE ===== -->
+<div class="section" id="training">
+  <div class="section-title">培训活动模块</div>
+  <div class="section-desc">培训活动模块包含培训计划和线下培训两个子功能,支持管理员全流程管理培训的创建、发布、签到与统计。</div>
+
+  <div class="sub-section" id="training-plan">
+    <div class="sub-title">培训计划</div>
+    <div class="feature-grid-3">
+      <div class="feature-card">
+        <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">&#128203;</div><div><div class="feature-name">培训计划列表</div><div class="feature-tag">page-training-plan</div></div></div>
+        <ul class="feature-list">
+          <li>表格形式展示所有培训计划,支持按培训名称关键字搜索过滤</li>
+          <li>表格列:培训名称、分类、培训时间、签到人数、状态、操作</li>
+          <li>状态标签区分:<span class="tag tag-green">已发布</span> / <span class="tag tag-gray">草稿</span>,一键切换发布状态</li>
+          <li>操作列含编辑、查看详情、删除入口</li>
+          <li>右上角"新建培训"按钮,跳转新建表单页</li>
+          <li>支持按状态(已发布/草稿)筛选培训记录</li>
+        </ul>
+      </div>
+      <div class="feature-card">
+        <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">&#10133;</div><div><div class="feature-name">新建/编辑培训</div><div class="feature-tag">page-training-new</div></div></div>
+        <ul class="feature-list">
+          <li>培训名称:文本输入,必填,最大 100 字</li>
+          <li>培训分类:下拉选择,如化学安全、生物安全、消防安全等</li>
+          <li>培训时间:起止时间选择器,精确到分钟</li>
+          <li>培训地点:文本输入,如具体教室楼层</li>
+          <li>主讲人:输入主讲人姓名与单位</li>
+          <li>学时/积分:设置本次培训可获得的学时数和积分值</li>
+          <li>培训简介:富文本编辑区域,支持图文描述</li>
+          <li>底部操作:保存草稿 / 直接发布,两种保存方式</li>
+        </ul>
+      </div>
+      <div class="feature-card">
+        <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">&#128202;</div><div><div class="feature-name">培训详情与签到记录</div><div class="feature-tag">page-training-record</div></div></div>
+        <ul class="feature-list">
+          <li>页面顶部展示签到二维码,管理员现场投影供学生扫码</li>
+          <li>二维码每 5 分钟自动刷新,防止截图滥用</li>
+          <li>统计数据卡片:总报名人数、已签到人数、未签到人数、签到率</li>
+          <li>参与名单列表:姓名、学工号、学院、签到时间,支持手动补签</li>
+          <li>支持按已签到/未签到切换查看名单</li>
+          <li>名单可导出为 Excel 表格文件</li>
+        </ul>
+      </div>
+    </div>
+    <div class="sub-title" style="margin-top:8px;">培训计划操作流程</div>
+    <div class="flow-row">
+      <div class="flow-step"><div class="flow-num">1</div><div class="flow-name">新建培训</div><div class="flow-desc">点击"新建培训"进入表单填写页</div></div>
+      <div class="flow-arrow">→</div>
+      <div class="flow-step"><div class="flow-num">2</div><div class="flow-name">填写信息</div><div class="flow-desc">填写名称、时间、地点、主讲人、学时积分</div></div>
+      <div class="flow-arrow">→</div>
+      <div class="flow-step"><div class="flow-num">3</div><div class="flow-name">切换发布</div><div class="flow-desc">保存草稿后在列表中一键切换为已发布</div></div>
+      <div class="flow-arrow">→</div>
+      <div class="flow-step"><div class="flow-num">4</div><div class="flow-name">学生报名签到</div><div class="flow-desc">学生在移动端报名并到场扫码签到</div></div>
+      <div class="flow-arrow">→</div>
+      <div class="flow-step"><div class="flow-num">5</div><div class="flow-name">查看统计</div><div class="flow-desc">管理员在详情页实时查看签到数据与名单</div></div>
+    </div>
+  </div>
+
+  <div class="sub-section" id="offline-training">
+    <div class="sub-title">线下培训</div>
+    <div class="feature-grid-2">
+      <div class="feature-card">
+        <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">&#127908;</div><div><div class="feature-name">活动列表</div><div class="feature-tag">page-training-activity-list</div></div></div>
+        <ul class="feature-list">
+          <li>默认卡片视图展示线下培训活动,每张卡片显示封面图、活动名称、时间地点、报名人数</li>
+          <li>右上角切换按钮:卡片视图 / 列表视图,列表视图以表格行展示</li>
+          <li>顶部状态筛选栏:全部 / 进行中 / 待开始 / 已结束,快速过滤活动</li>
+          <li>搜索框支持按活动名称关键字查找</li>
+          <li>活动卡片右上角显示活动状态标签:<span class="tag tag-green">进行中</span> / <span class="tag tag-blue">待开始</span> / <span class="tag tag-gray">已结束</span></li>
+          <li>点击卡片或行进入活动详情页查看完整信息</li>
+        </ul>
+      </div>
+      <div class="feature-card">
+        <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">&#128205;</div><div><div class="feature-name">培训详情</div><div class="feature-tag">page-training-activity-detail</div></div></div>
+        <ul class="feature-list">
+          <li>基本信息区域:活动名称、时间、地点、主讲人、活动简介</li>
+          <li>签到统计高亮区域:大号数字卡片突出展示总报名、已签到、未签到人数及签到率百分比</li>
+          <li>签到二维码展示区:管理员现场展示供学生扫码,支持一键刷新二维码</li>
+          <li>人员名单双 Tab 切换:已签到名单 Tab / 未签到名单 Tab</li>
+          <li>名单表格列:姓名、学工号、所在学院、签到时间(未签到则为空)</li>
+          <li>支持手动为未签到学生补录签到记录,需填写补签原因</li>
+        </ul>
+      </div>
+    </div>
+    <div class="notice notice-info"><span class="notice-icon">&#128161;</span><div>线下培训中的<strong>已签到/未签到名单</strong>按学工号与姓名双列展示,并标注所在学院信息,方便管理员快速核对人员。未签到名单支持手动补签功能,需管理员权限操作,补签记录留存审计日志。</div></div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== EXAM MODULE ===== -->
+<div class="section" id="exam">
+  <div class="section-title">考试管理模块</div>
+  <div class="section-desc">考试管理模块目前包含考试模板一个功能页面,支持配置三类考试(开学考试/分级考试/应知应会)的全部参数,每类考试通过五个子导航分步配置。</div>
+  <div class="notice notice-warn"><span class="notice-icon">&#9888;&#65039;</span><div>考试模板为<strong>全局唯一模板</strong>,修改任意配置项后,将对所有已分配该考试类型的学生产生影响。建议在非考试高峰期进行配置修改,并提前通知相关管理人员。配置修改后系统自动记录变更日志,可在审计记录中查阅。</div></div>
+  <div class="sub-title">三种考试类型说明</div>
+  <div class="feature-grid-3">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#127891;</div><div><div class="feature-name">开学考试</div><div class="feature-tag">Tab 1</div></div></div>
+      <ul class="feature-list">
+        <li>面向新入学学生,学年开始时强制参加</li>
+        <li>考察基础安全知识,覆盖面广</li>
+        <li>通过后颁发基础安全教育证书</li>
+        <li>包含基本配置、考试范围、试卷配置、考前配置、获得证书五个子导航</li>
+        <li>可配置不同年级、学院的差异化要求</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#127919;</div><div><div class="feature-name">分级考试</div><div class="feature-tag">Tab 2</div></div></div>
+      <ul class="feature-list">
+        <li>面向有特定实验室准入需求的学生</li>
+        <li>按实验室危险等级分级设置考试难度</li>
+        <li>通过后颁发对应等级安全操作资质证书</li>
+        <li>包含基本配置、考试范围、试卷配置、考前配置、获得证书五个子导航</li>
+        <li>考试范围可精细到专业与年级维度</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#128214;</div><div><div class="feature-name">应知应会</div><div class="feature-tag">Tab 3</div></div></div>
+      <ul class="feature-list">
+        <li>面向实验室日常安全知识巩固</li>
+        <li>题目偏向操作规范与应急处置知识</li>
+        <li>可设置为周期性参加,持续强化安全意识</li>
+        <li>包含基本配置、考试范围、试卷配置、考前配置、获得证书五个子导航</li>
+        <li>答题次数可配置,支持多次重考</li>
+      </ul>
+    </div>
+  </div>
+  <div class="sub-title">子导航配置项详解</div>
+  <div class="feature-grid-3" style="margin-bottom:18px;">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#128221;</div><div><div class="feature-name">基本配置</div><div class="feature-tag">子导航 1 · 考试参数</div></div></div>
+      <ul class="feature-list">
+        <li>考试名称:可自定义展示给学生的考试标题</li>
+        <li>考试时长:设置答题限制时间,单位分钟,范围 10~180 分钟</li>
+        <li>满分设置:设置本次考试总分值,默认 100 分</li>
+        <li>及格分数线:设置通过考试所需最低得分,默认 60 分</li>
+        <li>答题次数限制:设置学生最多可参加该考试的次数,0 表示不限</li>
+        <li>考试说明:富文本区域,向学生展示考前注意事项与考试规则</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#127979;</div><div><div class="feature-name">考试范围</div><div class="feature-tag">子导航 2 · 适用人群</div></div></div>
+      <ul class="feature-list">
+        <li>适用学院:多选下拉框,选择哪些学院的学生需参加本考试</li>
+        <li>适用专业:在所选学院下进一步限定专业范围,支持全选</li>
+        <li>适用年级:多选年级(大一/大二/大三/大四/研一/研二等)</li>
+        <li>排除规则:可设置特定学院或专业豁免该考试</li>
+        <li>范围生效时间:设置考试范围的有效起止日期</li>
+        <li>保存后实时生效,范围内学生将在考试列表中看到该考试任务</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#128196;</div><div><div class="feature-name">试卷配置</div><div class="feature-tag">子导航 3 · 出题规则</div></div></div>
+      <ul class="feature-list">
+        <li>题目总数:设置每次考试抽取的总题目数量</li>
+        <li>题型比例:分别设置单选题、多选题、判断题的数量或比例</li>
+        <li>随机出题规则:从题库随机抽取,每次考试题目不重复</li>
+        <li>知识点覆盖:可设置各知识点模块的抽题数量,保证覆盖面</li>
+        <li>难度分布:按难度(简单/中等/困难)设置抽题比例</li>
+        <li>预览试卷:配置完成后可预览示例试卷结构,校验配置合理性</li>
+      </ul>
+    </div>
+  </div>
+  <div class="feature-grid-2">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#128218;</div><div><div class="feature-name">考前配置</div><div class="feature-tag">子导航 4 · 考前门槛</div></div></div>
+      <ul class="feature-list">
+        <li>考前学习要求:是否强制要求学生完成指定课程后方可参加考试</li>
+        <li>关联课程:多选绑定考前必修课程,未完成则考试入口置灰</li>
+        <li>学时门槛:设置学生需完成的最低学时数才能解锁考试资格</li>
+        <li>学习时间窗口:限定学生必须在考试开始前 N 天内完成考前学习</li>
+        <li>考前提醒:开启后系统在考试截止前自动向未完成学生推送通知</li>
+        <li>配置保存后自动同步至学生端考试任务卡片的门槛提示信息</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#127885;</div><div><div class="feature-name">获得证书</div><div class="feature-tag">子导航 5 · 证书配置</div></div></div>
+      <ul class="feature-list">
+        <li>是否颁发证书:开关控制,关闭则通过考试后不颁发证书</li>
+        <li>证书模板:选择证书外观模板,支持多种样式(正式/简约/彩色)</li>
+        <li>颁发条件:设置通过该考试且达到指定分数后自动颁发</li>
+        <li>证书有效期:设置证书的有效时长(如 1 年/2 年/永久有效)</li>
+        <li>证书名称:自定义证书上展示的资质名称,如"实验室安全操作资质证书"</li>
+        <li>证书预览:可预览证书样式,填写示例信息确认展示效果</li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== ROLES ===== -->
+<div class="section" id="roles">
+  <div class="section-title">角色权限</div>
+  <div class="section-desc">管理后台内置三种管理员角色,权限由系统管理员在后台统一分配。不同角色可操作的功能范围不同,前端根据角色动态控制菜单显示与操作按钮权限。</div>
+  <table class="spec-table">
+    <thead>
+      <tr>
+        <th>角色</th>
+        <th>可操作功能</th>
+        <th>备注</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr>
+        <td><span class="tag tag-orange">培训管理员</span></td>
+        <td>培训计划 CRUD(新建、编辑、删除、查看);线下培训管理(查看活动列表与详情);签到统计(查看统计数据、已签到/未签到名单);签到二维码展示与刷新;手动补签操作;名单导出 Excel</td>
+        <td>仅可操作"培训活动"一级菜单下的所有功能,无法访问考试管理模块</td>
+      </tr>
+      <tr>
+        <td><span class="tag tag-blue">考试配置管理员</span></td>
+        <td>考试模板配置(开学考试/分级考试/应知应会三类);基本配置修改;考试范围设置;试卷配置(题目数量/题型/随机规则);考前配置(学习要求/学时门槛);证书配置(模板/颁发条件/有效期)</td>
+        <td>仅可操作"考试管理"一级菜单下的所有功能,无法访问培训活动模块</td>
+      </tr>
+      <tr>
+        <td><span class="tag tag-purple">系统管理员</span></td>
+        <td>拥有以上两种角色的全部权限;另可管理管理员账号(创建/禁用/角色分配);查看全局操作审计日志;配置系统公告与平台基础参数</td>
+        <td>最高权限角色,建议仅分配给信息化管理中心核心运维人员,数量控制在 2~3 人</td>
+      </tr>
+    </tbody>
+  </table>
+  <div class="notice notice-warn"><span class="notice-icon">&#9888;&#65039;</span><div>角色权限由系统管理员在后台账号管理模块统一配置,一个账号可同时拥有多个角色(如同时具备培训管理员和考试配置管理员权限)。权限变更实时生效,无需重新登录。请妥善保管管理员账号,定期审查账号权限分配的合理性。</div></div>
+</div>
+
+</div><!-- end main-content -->
+</div><!-- end doc-body -->
+
+<!-- ===== FOOTER ===== -->
+<div class="doc-footer">
+  <div>&#169; 2026 实验室安全教育平台 · 管理后台 v1.0 | 文档版本:2026-08 | 技术支持:信息化管理中心</div>
+  <div style="display:flex;gap:24px;align-items:center;">
+    <span>&#128187; PC 管理后台</span>
+    <span>&#128194; 5 个功能页面</span>
+    <span>&#128274; 3 种管理员角色</span>
+  </div>
+</div>
+
+</div><!-- end page -->
+</body>
+</html>

+ 366 - 0
full_reconstruct.py

@@ -0,0 +1,366 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Full reconstruction of admin.html from JSONL transcript.
+Handles: Write, cat>>, node script files, node -e inline, Edit ops.
+"""
+import json
+import re
+import sys
+
+JSONL_PATH = r'C:/Users/Administrator/.claude/projects/h--lab-safety-monitor-exam-main-2026/369b78fb-8697-4ea6-b83e-eeb87cee0bba.jsonl'
+OUTPUT_PATH = r'H:/lab-safety-monitor/exam_main_2026/admin.html'
+
+# ──────────────────────────────────────────────
+# 1. Parse JSONL: collect tool_uses + tool_results
+# ──────────────────────────────────────────────
+tool_results = {}   # tool_use_id -> {is_error, content}
+all_tool_uses = []  # list of (lineno, ts, name, input, tool_use_id)
+
+with open(JSONL_PATH, 'r', encoding='utf-8') as f:
+    for lineno, raw in enumerate(f, 1):
+        raw = raw.strip()
+        if not raw:
+            continue
+        try:
+            obj = json.loads(raw)
+        except Exception:
+            continue
+
+        ts = obj.get('timestamp', '')
+        msg = obj.get('message', {})
+        if not isinstance(msg, dict):
+            continue
+        content = msg.get('content', [])
+        if not isinstance(content, list):
+            continue
+
+        for item in content:
+            if not isinstance(item, dict):
+                continue
+            itype = item.get('type', '')
+
+            if itype == 'tool_use':
+                all_tool_uses.append((lineno, ts, item.get('name', ''), item.get('input', {}), item.get('id', '')))
+
+            elif itype == 'tool_result':
+                tid = item.get('tool_use_id', '')
+                is_err = item.get('is_error', False)
+                tc = item.get('content', '')
+                if isinstance(tc, list):
+                    tc = ' '.join(x.get('text', '') if isinstance(x, dict) else str(x) for x in tc)
+                else:
+                    tc = str(tc)
+                if '<tool_use_error>' in tc:
+                    is_err = True
+                tool_results[tid] = {'is_error': is_err, 'content': tc}
+
+
+def succeeded(tid):
+    r = tool_results.get(tid, None)
+    if r is None:
+        return True  # assume OK if no result found
+    return not r['is_error']
+
+
+# ──────────────────────────────────────────────
+# 2. Build virtual filesystem for helper scripts
+# ──────────────────────────────────────────────
+vfs = {}  # normalized path -> content (for helper .js files)
+
+
+def norm(path):
+    return path.replace('\\', '/').lower().replace('./', 'h:/lab-safety-monitor/exam_main_2026/')
+
+
+def norm_js(path):
+    """Normalize helper script paths."""
+    p = path.replace('\\', '/')
+    if p.startswith('./'):
+        p = 'h:/lab-safety-monitor/exam_main_2026/' + p[2:]
+    return p.lower()
+
+
+# ──────────────────────────────────────────────
+# 3. Process node scripts: extract JS and simulate
+#    - append_admin.js: reads admin.html, inserts chunk before </body>
+#    - finalize_admin.js: same pattern
+#    - fix_admin.js: runs string replacements on admin.html
+# ──────────────────────────────────────────────
+
+def extract_backtick_chunk(js_content):
+    """Extract the template literal content from `const chunk = \`...\`;`"""
+    # Find `const chunk = ` followed by backtick
+    m = re.search(r'const chunk = `(.*?)`\s*;', js_content, re.DOTALL)
+    if m:
+        return m.group(1)
+    return None
+
+
+def simulate_append_js(admin_html, js_content):
+    """Simulate append_admin.js / finalize_admin.js which insert chunk before </body>."""
+    chunk = extract_backtick_chunk(js_content)
+    if chunk is None:
+        print("  WARNING: Could not extract chunk from JS")
+        return admin_html
+
+    # The scripts do: html.replace('</body>', chunk + '\n</body>')
+    # or insert before </body></html>
+    if '</body>' in admin_html:
+        result = admin_html.replace('</body>', chunk + '\n</body>', 1)
+    else:
+        result = admin_html + chunk
+    return result
+
+
+def simulate_fix_admin_js(admin_html, js_content):
+    """
+    fix_admin.js does complex string replacements.
+    We extract and apply them manually since it uses JS template literals with Chinese chars.
+    """
+    # The fix_admin.js:
+    # 1. Removes a duplicate page-training-plan block (the fake 'active' one with workbench breadcrumb)
+    # 2. Adds 'active' class to the real page-training-plan
+    # 3. Fixes breadcrumbs
+    # 4. Removes view-tabs and student view panel
+
+    # Step 1: find and remove the duplicate/malformed page-training-plan active block
+    # The pattern: finds first occurrence of PAGE: 培训计划 comment + active div, up to the second occurrence
+    # We look for the block between two PAGE: 培训计划 markers
+
+    # Find all occurrences of <!-- ==================== PAGE: 培训计划
+    import re as re2
+
+    page_marker = '<!-- ==================== PAGE:'
+
+    # Find all positions of the page marker
+    positions = []
+    idx = 0
+    while True:
+        pos = admin_html.find(page_marker, idx)
+        if pos == -1:
+            break
+        positions.append(pos)
+        idx = pos + 1
+
+    # Look for duplicate page-training-plan block
+    # The JS finds: startMarker and endMarker to slice out a block
+    # We'll replicate the logic: find first active page-training-plan div and remove up to the next
+    # <!-- PAGE: 培训计划 --> marker
+
+    # Find first occurrence: '    <div class="page active" id="page-training-plan">'
+    active_plan_marker = '    <div class="page active" id="page-training-plan">'
+    active_idx = admin_html.find(active_plan_marker)
+
+    if active_idx != -1:
+        # Find the PAGE comment before this active div
+        comment_before = admin_html.rfind('    <!-- ==================== PAGE:', 0, active_idx)
+        if comment_before != -1:
+            # Find the next PAGE comment after the active div
+            next_comment = admin_html.find('    <!-- ==================== PAGE:', active_idx + 1)
+            if next_comment != -1:
+                # Remove from comment_before to next_comment
+                admin_html = admin_html[:comment_before] + admin_html[next_comment:]
+                print("  fix_admin: removed duplicate page-training-plan block")
+            else:
+                print("  fix_admin: WARNING - could not find next PAGE comment")
+        else:
+            print("  fix_admin: WARNING - could not find preceding PAGE comment for active div")
+
+    # Step 2: Make the real page-training-plan active
+    admin_html = admin_html.replace(
+        '    <div class="page" id="page-training-plan">',
+        '    <div class="page active" id="page-training-plan">',
+        1
+    )
+
+    # Step 3+4: breadcrumb fixes and view-tab removal
+    # These are minor cosmetic changes we can approximate
+    # The JS also removes view-tabs and student view panel
+    # We'll apply the breadcrumb replacements from the JS
+
+    return admin_html
+
+
+def simulate_node_inline(admin_html, cmd):
+    """Simulate inline node -e scripts that modify admin.html."""
+    # Pattern 1 (line 477): removes inline style font-size:13px from nav-item elements in exam section
+    # Pattern 2 (line 482): replaces padding:24px;overflow-y:auto;max-height with padding:28px;overflow-y:auto
+    # Pattern 3 (line 492): fenji-grade-tabs padding change
+
+    # Extract the JS body from the node -e "..." command
+    # The commands do simple c.replace() calls
+
+    # Let's look for the specific replacements
+
+    # node -e at line 477 (removes font-size:13px style from nav-items in exam section)
+    if 'font-size:13px' in cmd and 'nav-item' in cmd.lower():
+        # The JS: c = c.replace(/style="font-size:13px[^"]*"/g, '') or similar
+        # Approximate: remove inline font-size:13px styles from nav items
+        m = re.search(r"c\.replace\((.+?)\)", cmd, re.DOTALL)
+        if m:
+            print(f"  node -e: found replace pattern: {m.group(1)[:80]}")
+        # Skip - minor style change, not critical
+        return admin_html
+
+    # node -e at line 482: padding fix
+    if 'padding:24px;overflow-y:auto;max-height' in cmd or 'padding:28px' in cmd:
+        admin_html = admin_html.replace(
+            'padding:24px;overflow-y:auto;max-height:calc(100vh - 120px)',
+            'padding:28px;overflow-y:auto'
+        )
+        print("  node -e: applied padding fix")
+        return admin_html
+
+    # node -e at line 492: fenji-grade-tabs padding change
+    if 'fenji-grade-tabs' in cmd or 'padding:0 20px;background:var(--white);border:1px so' in cmd:
+        admin_html = admin_html.replace(
+            'padding:0 20px;background:var(--white);border:1px solid var(--gray-border);',
+            'padding:0;background:transparent;border:none;'
+        )
+        print("  node -e: applied fenji-grade-tabs padding fix")
+        return admin_html
+
+    return admin_html
+
+
+# ──────────────────────────────────────────────
+# 4. Main reconstruction loop
+# ──────────────────────────────────────────────
+admin_html = None
+
+for lineno, ts, name, inp, tid in all_tool_uses:
+    fp = inp.get('file_path', '')
+    cmd = inp.get('command', '')
+
+    # Track helper script writes
+    if name == 'Write' and fp and 'admin.html' not in fp:
+        fp_norm = fp.replace('\\', '/')
+        if any(s in fp_norm for s in ('append_admin', 'finalize_admin', 'fix_admin')):
+            if succeeded(tid):
+                vfs[fp_norm] = inp.get('content', '')
+                print(f"[{lineno}] Stored helper script: {fp_norm} ({len(vfs[fp_norm])} chars)")
+        continue
+
+    # Initial Write to admin.html
+    if name == 'Write' and 'admin.html' in fp:
+        if succeeded(tid):
+            admin_html = inp.get('content', '')
+            print(f"[{lineno}] {ts} Write admin.html ({len(admin_html)} chars)")
+        continue
+
+    if admin_html is None:
+        continue
+
+    # Edit admin.html
+    if name == 'Edit' and 'admin.html' in fp:
+        if not succeeded(tid):
+            print(f"[{lineno}] {ts} SKIP Edit (failed in original session)")
+            continue
+        old = inp.get('old_string', '')
+        new = inp.get('new_string', '')
+        replace_all = inp.get('replace_all', False)
+
+        if replace_all:
+            count = admin_html.count(old)
+            admin_html = admin_html.replace(old, new)
+            print(f"[{lineno}] {ts} Edit replace_all ({count} occurrences) len={len(admin_html)}")
+        else:
+            count = admin_html.count(old)
+            if count == 0:
+                print(f"[{lineno}] {ts} Edit WARNING not found: {repr(old[:60])}")
+            else:
+                admin_html = admin_html.replace(old, new, 1)
+                print(f"[{lineno}] {ts} Edit OK ({count} matches, replaced first) len={len(admin_html)}")
+        continue
+
+    # Bash commands
+    if name == 'Bash':
+        if not succeeded(tid):
+            print(f"[{lineno}] {ts} SKIP Bash (failed)")
+            continue
+
+        # cat >> admin.html
+        if 'cat >>' in cmd and 'admin.html' in cmd and "'HTMLEOF'" in cmd:
+            # Extract content between HTMLEOF markers
+            m = re.search(r"<< 'HTMLEOF'\s*(.*?)\s*HTMLEOF", cmd, re.DOTALL)
+            if m:
+                chunk = m.group(1)
+                admin_html += chunk + '\n'
+                print(f"[{lineno}] {ts} cat>> appended {len(chunk)} chars, total={len(admin_html)}")
+            else:
+                print(f"[{lineno}] {ts} cat>> WARNING: could not extract HTMLEOF content")
+            continue
+
+        # node append_admin.js
+        if 'node append_admin.js' in cmd:
+            js_path_candidates = [
+                'h:/lab-safety-monitor/exam_main_2026/append_admin.js',
+                '/tmp/append_admin.js',
+                'c:/tmp/append_admin.js',
+            ]
+            js_content = None
+            for p in js_path_candidates:
+                if p in vfs:
+                    js_content = vfs[p]
+                    break
+            if js_content:
+                old_len = len(admin_html)
+                admin_html = simulate_append_js(admin_html, js_content)
+                print(f"[{lineno}] {ts} node append_admin.js: {old_len} -> {len(admin_html)}")
+            else:
+                print(f"[{lineno}] {ts} node append_admin.js: WARNING - script not in vfs")
+            continue
+
+        # node finalize_admin.js
+        if 'node finalize_admin.js' in cmd:
+            js_path_candidates = [
+                'h:/lab-safety-monitor/exam_main_2026/finalize_admin.js',
+            ]
+            js_content = None
+            for p in js_path_candidates:
+                if p in vfs:
+                    js_content = vfs[p]
+                    break
+            if js_content:
+                old_len = len(admin_html)
+                admin_html = simulate_append_js(admin_html, js_content)
+                print(f"[{lineno}] {ts} node finalize_admin.js: {old_len} -> {len(admin_html)}")
+            else:
+                print(f"[{lineno}] {ts} node finalize_admin.js: WARNING - script not in vfs")
+            continue
+
+        # node fix_admin.js
+        if 'node fix_admin.js' in cmd:
+            js_path_candidates = [
+                'h:/lab-safety-monitor/exam_main_2026/fix_admin.js',
+            ]
+            js_content = None
+            for p in js_path_candidates:
+                if p in vfs:
+                    js_content = vfs[p]
+                    break
+            if js_content:
+                old_len = len(admin_html)
+                admin_html = simulate_fix_admin_js(admin_html, js_content)
+                print(f"[{lineno}] {ts} node fix_admin.js: {old_len} -> {len(admin_html)}")
+            else:
+                print(f"[{lineno}] {ts} node fix_admin.js: WARNING - script not in vfs")
+            continue
+
+        # node -e inline scripts that modify admin.html
+        if 'node -e' in cmd and 'admin.html' in cmd and 'readFileSync' in cmd and 'writeFileSync' in cmd:
+            old_len = len(admin_html)
+            admin_html = simulate_node_inline(admin_html, cmd)
+            if len(admin_html) != old_len:
+                print(f"[{lineno}] {ts} node -e: {old_len} -> {len(admin_html)}")
+            continue
+
+        continue
+
+print(f"\nFinal content length: {len(admin_html)}")
+print(f"Final line count: {admin_html.count(chr(10)) + 1}")
+
+with open(OUTPUT_PATH, 'w', encoding='utf-8') as f:
+    f.write(admin_html)
+print(f"\nWritten to: {OUTPUT_PATH}")

BIN
img/历史错题.png


BIN
img/学习任务详情.png


BIN
img/实验室审批.png


BIN
img/工作台.png


BIN
img/开始答题详情.png


BIN
img/开始考试详情.png


BIN
img/新建考试.png


BIN
img/模拟考试详情.png


BIN
img/毕业报告.png


BIN
img/知识学习详情.png


BIN
img/绑定实验室.png


BIN
img/网页版工作台.png


BIN
img/考试记录.png


BIN
img/课后考核.png


BIN
img/选择试卷.png


Разница между файлами не показана из-за своего большого размера
+ 850 - 0
inject_exam.js


Разница между файлами не показана из-за своего большого размера
+ 3327 - 0
mobile_exam.html


BIN
pc考试模版需求说明.docx


+ 367 - 0
preview.html

@@ -0,0 +1,367 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>手机预览 - 实验室安全教育考试系统</title>
+<style>
+* { box-sizing: border-box; margin: 0; padding: 0; }
+body {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #0f1728 0%, #1a2a4a 50%, #0f1728 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
+}
+
+/* ===== AMBIENT GLOW ===== */
+body::before {
+  content: '';
+  position: fixed;
+  top: 50%; left: 50%;
+  transform: translate(-50%, -50%);
+  width: 600px; height: 600px;
+  background: radial-gradient(circle, rgba(22,119,255,0.15) 0%, transparent 70%);
+  pointer-events: none;
+}
+
+/* ===== DEVICE SWITCHER ===== */
+.toolbar {
+  position: fixed;
+  top: 24px;
+  left: 50%;
+  transform: translateX(-50%);
+  display: flex;
+  gap: 8px;
+  background: rgba(255,255,255,0.08);
+  backdrop-filter: blur(12px);
+  border: 1px solid rgba(255,255,255,0.12);
+  border-radius: 40px;
+  padding: 6px 10px;
+  z-index: 100;
+}
+.device-btn {
+  padding: 6px 16px;
+  border-radius: 30px;
+  border: none;
+  background: transparent;
+  color: rgba(255,255,255,0.55);
+  font-size: 13px;
+  cursor: pointer;
+  transition: all .2s;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  white-space: nowrap;
+}
+.device-btn.active {
+  background: rgba(22,119,255,0.85);
+  color: #fff;
+  box-shadow: 0 2px 12px rgba(22,119,255,0.4);
+}
+.device-btn:hover:not(.active) {
+  background: rgba(255,255,255,0.1);
+  color: rgba(255,255,255,0.85);
+}
+
+/* ===== PHONE SHELL ===== */
+.phone-wrap {
+  position: relative;
+  transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* iPhone 14 Pro style */
+.phone-shell {
+  position: relative;
+  background: #1a1a1e;
+  border-radius: 54px;
+  padding: 14px;
+  box-shadow:
+    0 0 0 1px rgba(255,255,255,0.08),
+    0 0 0 2px #2a2a2e,
+    0 0 0 3px rgba(255,255,255,0.05),
+    0 40px 80px rgba(0,0,0,0.6),
+    0 0 60px rgba(22,119,255,0.08);
+  transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+/* Side buttons */
+.phone-shell::before {
+  content: '';
+  position: absolute;
+  left: -3px;
+  top: 110px;
+  width: 3px;
+  height: 36px;
+  background: #2a2a2e;
+  border-radius: 2px 0 0 2px;
+  box-shadow: 0 46px 0 #2a2a2e, 0 90px 0 #2a2a2e;
+}
+.phone-shell::after {
+  content: '';
+  position: absolute;
+  right: -3px;
+  top: 140px;
+  width: 3px;
+  height: 68px;
+  background: #2a2a2e;
+  border-radius: 0 2px 2px 0;
+}
+
+/* Screen bezel */
+.phone-screen {
+  background: #000;
+  border-radius: 42px;
+  overflow: hidden;
+  position: relative;
+}
+
+/* Dynamic island */
+.dynamic-island {
+  position: absolute;
+  top: 12px;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 120px;
+  height: 34px;
+  background: #000;
+  border-radius: 20px;
+  z-index: 10;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  padding-right: 10px;
+  gap: 6px;
+}
+.di-camera {
+  width: 12px; height: 12px;
+  border-radius: 50%;
+  background: #1a1a1e;
+  border: 1px solid #2a2a2e;
+  position: relative;
+}
+.di-camera::after {
+  content: '';
+  position: absolute;
+  top: 50%; left: 50%;
+  transform: translate(-50%,-50%);
+  width: 5px; height: 5px;
+  border-radius: 50%;
+  background: #0a0a10;
+}
+
+/* Status bar */
+.status-bar {
+  height: 54px;
+  background: #fff;
+  display: flex;
+  align-items: flex-end;
+  justify-content: space-between;
+  padding: 0 28px 8px;
+  font-size: 12px;
+  font-weight: 600;
+  color: #1a1a1a;
+  font-family: -apple-system, sans-serif;
+  flex-shrink: 0;
+}
+.status-time { font-size: 15px; font-weight: 700; letter-spacing: -0.3px; }
+.status-icons { display: flex; align-items: center; gap: 6px; }
+.status-icons svg { color: #1a1a1a; }
+
+/* Home indicator */
+.home-indicator {
+  height: 34px;
+  background: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+.home-bar {
+  width: 134px; height: 5px;
+  background: #1a1a1a;
+  border-radius: 3px;
+  opacity: 0.2;
+}
+
+/* The iframe */
+.phone-iframe {
+  width: 100%;
+  border: none;
+  display: block;
+  flex: 1;
+  min-height: 0;
+}
+
+/* ===== SIZE VARIANTS ===== */
+/* iPhone 14 Pro: 393x852 logical px */
+.size-iphone .phone-shell { width: 421px; }
+.size-iphone .phone-screen { height: 754px; display: flex; flex-direction: column; }
+.size-iphone .phone-iframe { height: 666px; }
+
+/* iPad mini */
+.size-ipad .phone-shell { width: 560px; border-radius: 32px; padding: 12px; }
+.size-ipad .phone-screen { border-radius: 24px; height: 820px; display: flex; flex-direction: column; }
+.size-ipad .phone-iframe { height: 732px; }
+.size-ipad .dynamic-island { display: none; }
+.size-ipad .status-bar { height: 44px; background: #f0f2f5; }
+.size-ipad .home-indicator { background: #f0f2f5; }
+
+/* Small phone (SE style) */
+.size-small .phone-shell { width: 360px; }
+.size-small .phone-screen { height: 660px; display: flex; flex-direction: column; }
+.size-small .phone-iframe { height: 572px; }
+
+/* ===== SCALE HINT ===== */
+.scale-hint {
+  position: fixed;
+  bottom: 24px;
+  left: 50%;
+  transform: translateX(-50%);
+  color: rgba(255,255,255,0.3);
+  font-size: 12px;
+  letter-spacing: 0.5px;
+  white-space: nowrap;
+}
+
+/* ===== ROTATE PHONE (landscape tablet) ===== */
+.size-ipad .phone-shell::before,
+.size-ipad .phone-shell::after { display: none; }
+
+/* ===== CLOCK UPDATE ===== */
+</style>
+</head>
+<body>
+
+<!-- Device selector toolbar -->
+<div class="toolbar">
+  <button class="device-btn active" onclick="setDevice('iphone')" id="btn-iphone">
+    <svg width="13" height="16" viewBox="0 0 13 16" fill="currentColor">
+      <rect x="1" y="0" width="11" height="16" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.2"/>
+      <rect x="4" y="13.5" width="5" height="1.2" rx="0.6"/>
+      <rect x="4.5" y="1.2" width="4" height="0.8" rx="0.4"/>
+    </svg>
+    iPhone 14 Pro
+  </button>
+  <button class="device-btn" onclick="setDevice('small')" id="btn-small">
+    <svg width="11" height="14" viewBox="0 0 11 14" fill="currentColor">
+      <rect x="1" y="0" width="9" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="1.2"/>
+      <rect x="3.5" y="11.5" width="4" height="1" rx="0.5"/>
+    </svg>
+    小屏手机
+  </button>
+  <button class="device-btn" onclick="setDevice('ipad')" id="btn-ipad">
+    <svg width="16" height="13" viewBox="0 0 16 13" fill="currentColor">
+      <rect x="0" y="1" width="16" height="11" rx="2" fill="none" stroke="currentColor" stroke-width="1.2"/>
+      <rect x="13.2" y="4" width="1.2" height="5" rx="0.6"/>
+    </svg>
+    平板
+  </button>
+</div>
+
+<!-- Phone frame -->
+<div class="phone-wrap size-iphone" id="phoneWrap">
+  <div class="phone-shell">
+    <div class="phone-screen">
+      <!-- Dynamic island -->
+      <div class="dynamic-island">
+        <div class="di-camera"></div>
+      </div>
+      <!-- Status bar -->
+      <div class="status-bar">
+        <span class="status-time" id="clockTime">9:41</span>
+        <div class="status-icons">
+          <!-- Signal -->
+          <svg width="17" height="12" viewBox="0 0 17 12" fill="currentColor">
+            <rect x="0" y="7" width="3" height="5" rx="0.5"/>
+            <rect x="4.5" y="4.5" width="3" height="7.5" rx="0.5"/>
+            <rect x="9" y="2" width="3" height="10" rx="0.5"/>
+            <rect x="13.5" y="0" width="3" height="12" rx="0.5" opacity="0.3"/>
+          </svg>
+          <!-- WiFi -->
+          <svg width="16" height="12" viewBox="0 0 16 12" fill="currentColor">
+            <path d="M8 10.5 L8 10.5 A0.7 0.7 0 0 1 8 10.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
+            <path d="M5.5 8.2 Q8 5.8 10.5 8.2" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round"/>
+            <path d="M3 5.8 Q8 1.2 13 5.8" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round"/>
+            <path d="M0.5 3.2 Q8 -3.5 15.5 3.2" stroke="currentColor" stroke-width="1.4" fill="none" stroke-linecap="round" opacity="0.3"/>
+          </svg>
+          <!-- Battery -->
+          <svg width="25" height="12" viewBox="0 0 25 12" fill="currentColor">
+            <rect x="0" y="1" width="21" height="10" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.2"/>
+            <rect x="1.5" y="2.5" width="16" height="7" rx="1.5" opacity="0.9"/>
+            <path d="M22.5 4 Q24.5 4 24.5 6 Q24.5 8 22.5 8Z" opacity="0.4"/>
+          </svg>
+        </div>
+      </div>
+      <!-- App iframe -->
+      <iframe
+        class="phone-iframe"
+        id="appFrame"
+        src="index.html"
+        title="实验室安全教育考试系统"
+      ></iframe>
+      <!-- Home indicator -->
+      <div class="home-indicator">
+        <div class="home-bar"></div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div class="scale-hint" id="scaleHint">393 × 852 · iPhone 14 Pro</div>
+
+<script>
+// Clock
+function updateClock() {
+  var now = new Date();
+  var h = now.getHours(), m = now.getMinutes();
+  document.getElementById('clockTime').textContent =
+    h + ':' + (m < 10 ? '0' : '') + m;
+}
+updateClock();
+setInterval(updateClock, 10000);
+
+// Device switcher
+var devices = {
+  iphone: { cls: 'size-iphone', hint: '393 × 852 · iPhone 14 Pro' },
+  small:  { cls: 'size-small',  hint: '360 × 780 · 小屏手机' },
+  ipad:   { cls: 'size-ipad',   hint: '768 × 1024 · 平板' }
+};
+var current = 'iphone';
+
+function setDevice(name) {
+  var wrap = document.getElementById('phoneWrap');
+  wrap.classList.remove(devices[current].cls);
+  wrap.classList.add(devices[name].cls);
+  document.getElementById('btn-' + current).classList.remove('active');
+  document.getElementById('btn-' + name).classList.add('active');
+  document.getElementById('scaleHint').textContent = devices[name].hint;
+  current = name;
+  // Resize iframe to fit
+  var frame = document.getElementById('appFrame');
+  frame.src = frame.src; // reload for clean state
+}
+
+// Auto-scale phone to fit viewport height
+function autoScale() {
+  var wrap = document.getElementById('phoneWrap');
+  var shell = wrap.querySelector('.phone-shell');
+  var vh = window.innerHeight - 100; // leave room for toolbar+hint
+  var shellH = shell.offsetHeight;
+  if (shellH > vh) {
+    var s = vh / shellH;
+    wrap.style.transform = 'scale(' + s + ')';
+    wrap.style.marginTop = ((shellH * s - shellH) / 2) + 'px';
+    wrap.style.marginBottom = ((shellH * s - shellH) / 2) + 'px';
+  } else {
+    wrap.style.transform = '';
+    wrap.style.marginTop = '';
+    wrap.style.marginBottom = '';
+  }
+}
+window.addEventListener('resize', autoScale);
+setTimeout(autoScale, 100);
+</script>
+</body>
+</html>

Разница между файлами не показана из-за своего большого размера
+ 63 - 0
prompt/README.md


+ 139 - 0
reconstruct_admin.py

@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Reconstruct admin.html from JSONL conversation transcript.
+Only applies operations that succeeded (matched tool_result with no error).
+"""
+import json
+import sys
+
+JSONL_PATH = r'C:/Users/Administrator/.claude/projects/h--lab-safety-monitor-exam-main-2026/369b78fb-8697-4ea6-b83e-eeb87cee0bba.jsonl'
+OUTPUT_PATH = r'H:/lab-safety-monitor/exam_main_2026/admin.html'
+
+# --- Pass 1: collect tool_use items for admin.html, keyed by tool_use_id ---
+tool_uses = {}   # tool_use_id -> {'ts', 'name', 'input', 'order'}
+order = 0
+tool_results = {}  # tool_use_id -> {'ts', 'is_error', 'content'}
+
+with open(JSONL_PATH, 'r', encoding='utf-8') as f:
+    for raw_line in f:
+        raw_line = raw_line.strip()
+        if not raw_line:
+            continue
+        try:
+            obj = json.loads(raw_line)
+        except Exception:
+            continue
+
+        ts = obj.get('timestamp', '')
+        msg = obj.get('message', {})
+        if not isinstance(msg, dict):
+            continue
+        content = msg.get('content', [])
+        if not isinstance(content, list):
+            continue
+
+        for item in content:
+            if not isinstance(item, dict):
+                continue
+
+            if item.get('type') == 'tool_use':
+                name = item.get('name', '')
+                inp = item.get('input', {})
+                fp = inp.get('file_path', '')
+                if 'admin.html' in fp and name in ('Write', 'Edit'):
+                    tid = item.get('id', '')
+                    tool_uses[tid] = {
+                        'ts': ts,
+                        'name': name,
+                        'input': inp,
+                        'order': order,
+                    }
+                    order += 1
+
+            elif item.get('type') == 'tool_result':
+                tid = item.get('tool_use_id', '')
+                is_error = item.get('is_error', False)
+                tc = item.get('content', '')
+                # Also check for error strings in content
+                if isinstance(tc, list):
+                    tc_text = ' '.join(x.get('text', '') if isinstance(x, dict) else str(x) for x in tc)
+                else:
+                    tc_text = str(tc)
+                if '<tool_use_error>' in tc_text:
+                    is_error = True
+                tool_results[tid] = {'ts': ts, 'is_error': is_error, 'content': tc_text}
+
+# Sort operations by original order
+ops = sorted(tool_uses.values(), key=lambda x: x['order'])
+
+print(f"Total admin.html operations: {len(ops)}")
+
+# Find the initial Write
+write_ops = [op for op in ops if op['name'] == 'Write']
+edit_ops = [op for op in ops if op['name'] == 'Edit']
+
+if not write_ops:
+    print("ERROR: No Write operation found!")
+    sys.exit(1)
+
+# Get the write that actually succeeded
+# Find tool_use_id for the write
+write_tid = None
+for tid, v in tool_uses.items():
+    if v['name'] == 'Write' and 'admin.html' in v['input'].get('file_path', ''):
+        r = tool_results.get(tid, {})
+        if not r.get('is_error', True):
+            write_tid = tid
+            break
+
+if write_tid is None:
+    print("WARNING: Write had no matching result, using it anyway")
+    write_tid = [tid for tid, v in tool_uses.items() if v['name'] == 'Write'][0]
+
+file_content = tool_uses[write_tid]['input'].get('content', '')
+print(f"Initial Write content length: {len(file_content)}")
+
+# Apply edits in order, skipping failed ones
+skipped = 0
+applied = 0
+for tid, v in sorted([(tid, v) for tid, v in tool_uses.items() if v['name'] == 'Edit'],
+                      key=lambda x: x[1]['order']):
+    result = tool_results.get(tid, {})
+    if result.get('is_error', False):
+        print(f"  SKIP (original error): {v['ts']}")
+        skipped += 1
+        continue
+
+    inp = v['input']
+    old = inp.get('old_string', '')
+    new = inp.get('new_string', '')
+    replace_all = inp.get('replace_all', False)
+
+    if replace_all:
+        count = file_content.count(old)
+        file_content = file_content.replace(old, new)
+        print(f"  replace_all {v['ts']}: {count} replacements, len={len(file_content)}")
+        applied += 1
+    else:
+        count = file_content.count(old)
+        if count == 0:
+            print(f"  WARNING not found: {v['ts']} old={repr(old[:80])}")
+            # Still count as applied since original session said success
+        elif count == 1:
+            file_content = file_content.replace(old, new, 1)
+            print(f"  OK {v['ts']}: len={len(file_content)}")
+            applied += 1
+        else:
+            # Multiple matches - replace first
+            file_content = file_content.replace(old, new, 1)
+            print(f"  MULTI({count}) {v['ts']}: replaced first, len={len(file_content)}")
+            applied += 1
+
+print(f"\nApplied: {applied}, Skipped: {skipped}")
+print(f"Final content length: {len(file_content)}")
+print(f"Final line count: {file_content.count(chr(10)) + 1}")
+
+with open(OUTPUT_PATH, 'w', encoding='utf-8') as f:
+    f.write(file_content)
+print(f"\nWritten to: {OUTPUT_PATH}")

+ 826 - 0
requirements.html

@@ -0,0 +1,826 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=1920">
+<title>实验室安全教育考试系统 · 移动端使用说明文档</title>
+<style>
+*{box-sizing:border-box;margin:0;padding:0;}
+:root{
+  --blue:#1857d4;--blue-mid:#2e6fe0;--blue-light:#eef3fd;--blue-xlight:#f5f8ff;
+  --green:#10a37f;--green-light:#edfaf6;
+  --orange:#e67e22;--orange-light:#fef6ec;
+  --red:#e53e3e;--red-light:#fff5f5;
+  --purple:#722ed1;--purple-light:#f9f0ff;
+  --gray:#8896a7;--gray-light:#f4f6fa;--gray-border:#dde3ed;
+  --text-primary:#1a2233;--text-secondary:#4a5568;--text-hint:#8896a7;
+  --bg:#f0f4fb;--white:#fff;
+  --shadow:0 2px 12px rgba(24,87,212,.08);
+  --shadow-md:0 4px 24px rgba(24,87,212,.12);
+}
+body{font-family:"PingFang SC","Microsoft YaHei",sans-serif;background:#f0f4fb;color:var(--text-primary);font-size:15px;line-height:1.7;min-width:1920px;}
+
+/* ===== LAYOUT ===== */
+.page{max-width:1920px;min-height:1080px;margin:0 auto;display:flex;flex-direction:column;}
+.doc-header{background:linear-gradient(135deg,#1857d4 0%,#2e6fe0 60%,#0f3ea8 100%);color:#fff;padding:48px 80px 40px;position:relative;overflow:hidden;}
+.doc-header::before{content:'';position:absolute;inset:0;background:url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.04'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");}
+.doc-header-inner{position:relative;z-index:1;display:flex;align-items:flex-end;justify-content:space-between;}
+.doc-title{font-size:36px;font-weight:800;letter-spacing:1px;margin-bottom:10px;}
+.doc-subtitle{font-size:16px;opacity:.82;letter-spacing:.5px;}
+.doc-meta{display:flex;gap:32px;font-size:13px;opacity:.75;margin-top:14px;}
+.doc-meta span{display:flex;align-items:center;gap:6px;}
+.doc-badge{background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.3);border-radius:20px;padding:6px 18px;font-size:13px;font-weight:600;}
+
+/* ===== NAV SIDEBAR ===== */
+.doc-body{display:flex;flex:1;}
+.sidebar{width:260px;flex-shrink:0;background:var(--white);border-right:1px solid var(--gray-border);padding:24px 0;position:sticky;top:0;height:calc(100vh - 148px);overflow-y:auto;}
+.sidebar::-webkit-scrollbar{width:4px;}
+.sidebar::-webkit-scrollbar-thumb{background:rgba(24,87,212,.15);border-radius:2px;}
+.nav-section{padding:0 20px;margin-bottom:6px;}
+.nav-section-title{font-size:11px;font-weight:700;color:var(--text-hint);letter-spacing:1.5px;text-transform:uppercase;padding:14px 4px 6px;}
+.nav-item{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:8px;font-size:13px;color:var(--text-secondary);cursor:pointer;text-decoration:none;transition:all .15s;}
+.nav-item:hover{background:var(--blue-xlight);color:var(--blue);}
+.nav-item.active{background:var(--blue-light);color:var(--blue);font-weight:600;}
+.nav-item .nav-icon{width:20px;height:20px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0;}
+
+/* ===== MAIN CONTENT ===== */
+.main-content{flex:1;padding:40px 60px;max-width:1580px;}
+.section{margin-bottom:56px;}
+.section-title{font-size:24px;font-weight:800;color:var(--text-primary);margin-bottom:6px;display:flex;align-items:center;gap:10px;}
+.section-title::before{content:'';width:4px;height:24px;background:linear-gradient(180deg,var(--blue-mid),var(--blue));border-radius:3px;display:inline-block;}
+.section-desc{font-size:14px;color:var(--text-hint);margin-bottom:28px;padding-left:14px;}
+.sub-section{margin-bottom:36px;}
+.sub-title{font-size:17px;font-weight:700;color:var(--text-primary);margin-bottom:14px;padding-left:12px;border-left:3px solid var(--blue-mid);}
+
+/* ===== FEATURE CARDS ===== */
+.feature-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-bottom:24px;}
+.feature-grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-bottom:24px;}
+.feature-grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:18px;margin-bottom:24px;}
+.feature-card{background:var(--white);border:1px solid var(--gray-border);border-radius:14px;padding:22px;box-shadow:var(--shadow);transition:box-shadow .2s;}
+.feature-card:hover{box-shadow:var(--shadow-md);}
+.feature-card-header{display:flex;align-items:center;gap:12px;margin-bottom:14px;}
+.feature-icon{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;}
+.feature-name{font-size:15px;font-weight:700;color:var(--text-primary);}
+.feature-tag{font-size:11px;color:var(--text-hint);margin-top:2px;}
+.feature-list{list-style:none;padding:0;}
+.feature-list li{font-size:13px;color:var(--text-secondary);padding:5px 0;border-bottom:1px solid var(--gray-light);display:flex;align-items:flex-start;gap:8px;line-height:1.6;}
+.feature-list li:last-child{border-bottom:none;}
+.feature-list li::before{content:'·';color:var(--blue);font-size:16px;flex-shrink:0;line-height:1.4;}
+
+/* ===== SCREEN SPEC TABLE ===== */
+.spec-table{width:100%;border-collapse:collapse;font-size:13px;margin-bottom:16px;}
+.spec-table th{background:var(--blue-xlight);color:var(--blue);font-weight:700;padding:10px 16px;text-align:left;border:1px solid var(--blue-light);}
+.spec-table td{padding:10px 16px;border:1px solid var(--gray-border);vertical-align:top;line-height:1.7;}
+.spec-table tr:nth-child(even) td{background:#fafbfd;}
+.spec-table tr:hover td{background:var(--blue-xlight);}
+
+/* ===== TAGS ===== */
+.tag{display:inline-flex;align-items:center;padding:2px 10px;border-radius:20px;font-size:11px;font-weight:600;}
+.tag-blue{background:var(--blue-light);color:var(--blue);}
+.tag-green{background:var(--green-light);color:var(--green);}
+.tag-orange{background:var(--orange-light);color:var(--orange);}
+.tag-red{background:var(--red-light);color:var(--red);}
+.tag-gray{background:var(--gray-light);color:var(--gray);}
+.tag-purple{background:var(--purple-light);color:var(--purple);}
+
+/* ===== FLOW ===== */
+.flow-row{display:flex;align-items:stretch;gap:0;margin-bottom:24px;flex-wrap:wrap;}
+.flow-step{background:var(--white);border:1px solid var(--gray-border);border-radius:12px;padding:16px 20px;min-width:160px;flex:1;position:relative;box-shadow:var(--shadow);}
+.flow-arrow{display:flex;align-items:center;padding:0 10px;color:var(--blue);font-size:22px;flex-shrink:0;}
+.flow-num{width:24px;height:24px;border-radius:50%;background:var(--blue);color:#fff;font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;margin-bottom:8px;}
+.flow-name{font-size:13px;font-weight:700;color:var(--text-primary);margin-bottom:4px;}
+.flow-desc{font-size:12px;color:var(--text-hint);}
+
+/* ===== NOTICE BOX ===== */
+.notice{border-radius:10px;padding:14px 18px;margin-bottom:18px;display:flex;gap:12px;align-items:flex-start;font-size:13px;line-height:1.7;}
+.notice-info{background:var(--blue-xlight);border:1px solid var(--blue-light);}
+.notice-warn{background:#fffbe6;border:1px solid #ffe58f;}
+.notice-icon{font-size:18px;flex-shrink:0;margin-top:1px;}
+
+/* ===== OVERVIEW HERO ===== */
+.overview-hero{display:grid;grid-template-columns:1fr 1fr 1fr 1fr 1fr;gap:16px;margin-bottom:32px;}
+.stat-card{background:var(--white);border-radius:14px;padding:22px 20px;border:1px solid var(--gray-border);box-shadow:var(--shadow);text-align:center;}
+.stat-num{font-size:32px;font-weight:800;background:linear-gradient(135deg,var(--blue-mid),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;line-height:1.1;}
+.stat-label{font-size:12px;color:var(--text-hint);margin-top:6px;}
+
+/* ===== SCREEN OVERVIEW GRID ===== */
+.screen-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:16px;margin-bottom:24px;}
+.screen-card{background:var(--white);border:1px solid var(--gray-border);border-radius:12px;padding:16px;box-shadow:var(--shadow);}
+.screen-card-icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:20px;margin-bottom:10px;}
+.screen-card-name{font-size:13px;font-weight:700;margin-bottom:4px;}
+.screen-card-desc{font-size:11px;color:var(--text-hint);line-height:1.5;}
+
+/* ===== DIVIDER ===== */
+.divider{height:1px;background:var(--gray-border);margin:40px 0;}
+
+/* ===== FOOTER ===== */
+.doc-footer{background:var(--white);border-top:1px solid var(--gray-border);padding:24px 80px;display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--text-hint);}
+</style>
+</head>
+<body>
+<div class="page">
+<!-- ===== HEADER ===== -->
+<div class="doc-header">
+  <div class="doc-header-inner">
+    <div>
+      <div style="font-size:13px;opacity:.7;margin-bottom:8px;letter-spacing:2px;">LABORATORY SAFETY EDUCATION SYSTEM</div>
+      <div class="doc-title">实验室安全教育考试系统</div>
+      <div class="doc-subtitle">移动端应用 · 产品使用说明文档</div>
+      <div class="doc-meta">
+        <span>📱 移动端 H5 单页应用</span>
+        <span>📅 文档版本:v2.1 · 2026-08</span>
+        <span>🏛️ 适用范围:在校师生 / 实验室管理人员</span>
+        <span>🖥️ 推荐分辨率:1920 × 1080</span>
+      </div>
+    </div>
+    <div style="text-align:right;">
+      <div class="doc-badge">🔬 实验室安全教育平台</div>
+      <div style="margin-top:12px;font-size:13px;opacity:.7;">共 22 个功能页面 · 4 个底部导航Tab</div>
+    </div>
+  </div>
+</div>
+
+<!-- ===== BODY ===== -->
+<div class="doc-body">
+<!-- SIDEBAR -->
+<div class="sidebar">
+  <div class="nav-section">
+    <div class="nav-section-title">概览</div>
+    <a class="nav-item active" href="#overview"><span class="nav-icon" style="background:#eef3fd;">🏠</span>系统概述</a>
+    <a class="nav-item" href="#arch"><span class="nav-icon" style="background:#eef3fd;">🗂️</span>页面架构</a>
+    <a class="nav-item" href="#nav"><span class="nav-icon" style="background:#eef3fd;">🧭</span>导航说明</a>
+  </div>
+  <div class="nav-section">
+    <div class="nav-section-title">功能模块</div>
+    <a class="nav-item" href="#workbench"><span class="nav-icon" style="background:#fff7e6;">🏠</span>工作台</a>
+    <a class="nav-item" href="#study"><span class="nav-icon" style="background:#edfaf6;">📚</span>学习培训</a>
+    <a class="nav-item" href="#exam"><span class="nav-icon" style="background:#eef3fd;">🎓</span>在线考试</a>
+    <a class="nav-item" href="#practice"><span class="nav-icon" style="background:#fff2f0;">✏️</span>刷题练习</a>
+  </div>
+  <div class="nav-section">
+    <div class="nav-section-title">功能详情</div>
+    <a class="nav-item" href="#approval"><span class="nav-icon" style="background:#eef3fd;">🏛️</span>实验室审批</a>
+    <a class="nav-item" href="#offline"><span class="nav-icon" style="background:#fff7e6;">🎤</span>线下培训</a>
+    <a class="nav-item" href="#scores"><span class="nav-icon" style="background:#edfaf6;">📊</span>我的成绩</a>
+    <a class="nav-item" href="#cert"><span class="nav-icon" style="background:#fff7e6;">🏅</span>我的证书</a>
+    <a class="nav-item" href="#score-detail"><span class="nav-icon" style="background:#eef3fd;">📝</span>答题详情</a>
+  </div>
+  <div class="nav-section">
+    <div class="nav-section-title">角色权限</div>
+    <a class="nav-item" href="#roles"><span class="nav-icon" style="background:#f9f0ff;">🔐</span>角色说明</a>
+    <a class="nav-item" href="#admin"><span class="nav-icon" style="background:#fff2f0;">⚙️</span>管理端功能</a>
+  </div>
+</div>
+
+<!-- MAIN CONTENT -->
+<div class="main-content">
+<div class="section" id="overview">
+  <div class="section-title">系统概述</div>
+  <div class="section-desc">实验室安全教育考试系统移动端,面向在校师生提供安全知识学习、考试、证书管理等一站式服务。</div>
+  <div class="overview-hero">
+    <div class="stat-card"><div class="stat-num">22</div><div class="stat-label">功能页面</div></div>
+    <div class="stat-card"><div class="stat-num">4</div><div class="stat-label">主功能Tab</div></div>
+    <div class="stat-card"><div class="stat-num">4</div><div class="stat-label">底部导航Tab</div></div>
+    <div class="stat-card"><div class="stat-num">3</div><div class="stat-label">用户角色</div></div>
+    <div class="stat-card"><div class="stat-num">H5</div><div class="stat-label">单页应用</div></div>
+  </div>
+  <div class="notice notice-info"><span class="notice-icon">&#128161;</span><div>本系统为 <strong>移动端 H5 单页应用</strong>,采用 screen 切换机制模拟原生 App 体验,无需安装,微信或浏览器直接访问。底部固定导航栏提供工作台、学习培训、在线考试、刷题练习四大入口。</div></div>
+  <div class="sub-title">目标用户</div>
+  <div class="feature-grid-3">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">&#127891;</div><div><div class="feature-name">本科生 / 研究生</div><div class="feature-tag">主要用户群体</div></div></div>
+      <ul class="feature-list"><li>完成入学/学年安全考试</li><li>在线学习安全知识课程</li><li>参与线下培训并签到</li><li>查看成绩、证书、积分</li><li>申请实验室绑定</li></ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">&#128105;&#8205;&#127979;</div><div><div class="feature-name">实验室管理员</div><div class="feature-tag">管理角色(线下培训)</div></div></div>
+      <ul class="feature-list"><li>发布/管理线下培训活动</li><li>展示签到二维码</li><li>查看签到统计与名单</li><li>切换发布/草稿状态</li></ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#f6ffed;">&#127963;</div><div><div class="feature-name">实验室审批人</div><div class="feature-tag">审批角色</div></div></div>
+      <ul class="feature-list"><li>处理学生实验室绑定申请</li><li>审批/驳回申请</li><li>查看申请详情与安全须知</li><li>待审批数角标提醒</li></ul>
+    </div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== PAGE ARCHITECTURE ===== -->
+<div class="section" id="arch">
+  <div class="section-title">页面架构</div>
+  <div class="section-desc">系统共包含 22 个功能页面,按底部 4 个导航 Tab 及独立页面分组,页面间通过 screen 参数切换。</div>
+  <div class="notice notice-info"><span class="notice-icon">🗂️</span><div>所有页面均为单页应用内的虚拟路由,通过 <strong>goto(screenId)</strong> 控制显示与隐藏,支持 goBack() 返回操作,保留滚动位置。</div></div>
+  <div class="sub-title">工作台(3 个页面)</div>
+  <div class="screen-grid">
+    <div class="screen-card"><div class="screen-card-icon" style="background:#eef3fd;">🏠</div><div class="screen-card-name">screen-home</div><div class="screen-card-desc">工作台首页,卡片式聚合展示考试任务、学习任务、培训活动、积分通知</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#eef3fd;">🔔</div><div class="screen-card-name">screen-notice-list</div><div class="screen-card-desc">消息通知列表,系统公告与个人消息</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#eef3fd;">🏛️</div><div class="screen-card-name">screen-approval</div><div class="screen-card-desc">实验室审批页,处理学生实验室绑定申请</div></div>
+  </div>
+  <div class="sub-title">学习培训(5 个页面)</div>
+  <div class="screen-grid">
+    <div class="screen-card"><div class="screen-card-icon" style="background:#edfaf6;">📚</div><div class="screen-card-name">screen-knowledge-list</div><div class="screen-card-desc">学习培训首页,推荐/全部/考前/线下四个子Tab</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#edfaf6;">📖</div><div class="screen-card-name">screen-knowledge-detail</div><div class="screen-card-desc">课程详情,含章节目录、简介、学习进度</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#edfaf6;">📋</div><div class="screen-card-name">screen-posttest</div><div class="screen-card-desc">课后测验,完成课程后的知识检测</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#edfaf6;">🎤</div><div class="screen-card-name">screen-training-detail</div><div class="screen-card-desc">线下培训详情,时间地点、签到二维码、名单统计</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#edfaf6;">📖</div><div class="screen-card-name">screen-study-detail</div><div class="screen-card-desc">学习资料详情,知识点阅读页</div></div>
+  </div>
+  <div class="sub-title">在线考试(7 个页面)</div>
+  <div class="screen-grid">
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f0f4ff;">🎓</div><div class="screen-card-name">screen-exam-list</div><div class="screen-card-desc">在线考试首页,考试任务/模拟考试/我的成绩三个子Tab</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f0f4ff;">✍️</div><div class="screen-card-name">screen-exam-doing</div><div class="screen-card-desc">答题中,题目展示、选项作答、倒计时</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f0f4ff;">🏆</div><div class="screen-card-name">screen-exam-result</div><div class="screen-card-desc">考试结果,成绩、通过状态、可查看答题详情</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f0f4ff;">📋</div><div class="screen-card-name">screen-exam-detail</div><div class="screen-card-desc">考试任务详情,考试说明与开始入口</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f0f4ff;">🎯</div><div class="screen-card-name">screen-mock-detail</div><div class="screen-card-desc">模拟考试详情,模拟说明与开始入口</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f0f4ff;">📝</div><div class="screen-card-name">screen-regular-exam-detail</div><div class="screen-card-desc">常规考试详情,考试信息与历史成绩</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f0f4ff;">🔍</div><div class="screen-card-name">screen-score-detail</div><div class="screen-card-desc">答题详情,逐题查看答案、解析与难度</div></div>
+  </div>
+  <div class="sub-title">刷题练习(2 个页面)</div>
+  <div class="screen-grid">
+    <div class="screen-card"><div class="screen-card-icon" style="background:#fff2f0;">✏️</div><div class="screen-card-name">screen-practice-list</div><div class="screen-card-desc">刷题练习首页,按科目/类型选择练习范围</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#fff2f0;">📝</div><div class="screen-card-name">screen-practice-detail</div><div class="screen-card-desc">刷题答题页,即时反馈正确答案与解析</div></div>
+  </div>
+  <div class="sub-title">我的个人中心(5 个页面,从工作台头像入口访问,非底部Tab)</div>
+  <div class="screen-grid">
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f9f0ff;">👤</div><div class="screen-card-name">screen-my</div><div class="screen-card-desc">我的首页,用户信息、学时/证书/积分概览及功能入口</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f9f0ff;">📊</div><div class="screen-card-name">screen-scores</div><div class="screen-card-desc">我的成绩,各科目历次考试成绩列表</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f9f0ff;">🏅</div><div class="screen-card-name">screen-certs</div><div class="screen-card-desc">我的证书,已获得安全资质证书列表</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f9f0ff;">📁</div><div class="screen-card-name">screen-exam-records</div><div class="screen-card-desc">考试记录,历次参加考试的详细记录</div></div>
+    <div class="screen-card"><div class="screen-card-icon" style="background:#f9f0ff;">❌</div><div class="screen-card-name">screen-wrong-questions</div><div class="screen-card-desc">历史错题,汇总答错题目便于复习</div></div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== NAVIGATION ===== -->
+<div class="section" id="nav">
+  <div class="section-title">底部导航说明</div>
+  <div class="section-desc">底部固定导航栏包含 4 个 Tab(工作台、学习培训、在线考试、刷题练习),始终可见,切换 Tab 时记忆各模块的浏览状态。「我的」个人中心通过工作台右上角入口访问,不在底部 Tab 内。</div>
+  <div class="feature-grid">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">🏠</div><div><div class="feature-name">工作台</div><div class="feature-tag">Tab 1 · 首页入口</div></div></div>
+      <ul class="feature-list">
+        <li>首页入口,聚合展示所有重要任务</li>
+        <li>展示待完成考试任务与截止提醒</li>
+        <li>展示学习任务进度与已完成学时</li>
+        <li>展示近期培训活动与积分等级</li>
+        <li>右上角消息通知角标入口</li>
+        <li>实验室审批待处理数角标提醒</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">📚</div><div><div class="feature-name">学习培训</div><div class="feature-tag">Tab 2 · 课程与培训</div></div></div>
+      <ul class="feature-list">
+        <li>在线课程列表,支持推荐/全部/考前分类</li>
+        <li>视频学习,支持进度记录与续播</li>
+        <li>考前必修课程专属入口</li>
+        <li>线下培训活动报名与签到入口</li>
+        <li>课程学习进度可视化展示</li>
+        <li>完成课程自动累计积分</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">🎓</div><div><div class="feature-name">在线考试</div><div class="feature-tag">Tab 3 · 三个子Tab</div></div></div>
+      <ul class="feature-list">
+        <li>考试任务 Tab:必考任务列表与截止时间</li>
+        <li>模拟考试 Tab:可多次参加的模拟练习</li>
+        <li>我的成绩 Tab:历次考试成绩汇总</li>
+        <li>倒计时答题,自动提交保障</li>
+        <li>防切屏提示,保障考试公正性</li>
+        <li>考后可查看答题详情与解析</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff2f0;">✏️</div><div><div class="feature-name">刷题练习</div><div class="feature-tag">Tab 4 · 三种练习模式</div></div></div>
+      <ul class="feature-list">
+        <li>随机练习:系统随机抽取题目</li>
+        <li>错题本:汇总历史答错题目</li>
+        <li>按题型/知识点分类练习</li>
+        <li>即时反馈正确答案与解析</li>
+        <li>收藏题目,形成个人题库</li>
+        <li>练习数据统计与正确率分析</li>
+      </ul>
+    </div>
+  </div>
+  <div class="notice notice-warn"><span class="notice-icon">⚠️</span><div>Tab 切换不会重置页面状态,返回时可继续浏览。但<strong>正在进行的考试</strong>切换 Tab 后须在规定时间内返回,超时将自动提交。</div></div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== WORKBENCH MODULE ===== -->
+<div class="section" id="workbench">
+  <div class="section-title">工作台模块</div>
+  <div class="section-desc">工作台是应用的默认首屏,以卡片式布局聚合展示所有重要任务入口与数据概览,帮助用户快速了解待办事项。</div>
+  <div class="feature-grid-3">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">📝</div><div><div class="feature-name">考试任务卡片</div><div class="feature-tag">高优先级展示</div></div></div>
+      <ul class="feature-list">
+        <li>展示当前待完成与已完成考试列表</li>
+        <li>每个考试显示截止时间与剩余天数</li>
+        <li>进度条可视化展示完成状态</li>
+        <li>点击卡片直接跳转考试入口</li>
+        <li>已过期考试以灰色标注区分</li>
+        <li>支持按截止时间紧急程度排序</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">📚</div><div><div class="feature-name">学习任务卡片</div><div class="feature-tag">学时统计</div></div></div>
+      <ul class="feature-list">
+        <li>展示分配给用户的课程学习任务</li>
+        <li>显示每门课程的学习进度百分比</li>
+        <li>汇总已完成学时与总要求学时</li>
+        <li>点击课程直接进入学习详情页</li>
+        <li>完成课程后学时自动累计更新</li>
+        <li>超期未完成课程红色高亮提示</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">🎯</div><div><div class="feature-name">模拟考试卡片</div><div class="feature-tag">快速入口</div></div></div>
+      <ul class="feature-list">
+        <li>展示最近一次模拟考试成绩与时间</li>
+        <li>一键快速开始新的模拟练习</li>
+        <li>显示历史模拟最高分记录</li>
+        <li>与正式考试隔离,不影响成绩档案</li>
+        <li>模拟次数不限,随时练习</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#f9f0ff;">🎤</div><div><div class="feature-name">培训活动卡片</div><div class="feature-tag">线下活动</div></div></div>
+      <ul class="feature-list">
+        <li>展示近期即将举行的线下培训活动</li>
+        <li>显示活动时间、地点、剩余名额</li>
+        <li>支持一键报名参加培训</li>
+        <li>管理员可直接在此入口管理活动</li>
+        <li>已报名活动显示签到入口</li>
+        <li>活动结束后自动归档</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">⭐</div><div><div class="feature-name">积分展示</div><div class="feature-tag">激励机制</div></div></div>
+      <ul class="feature-list">
+        <li>展示用户当前总积分与等级徽章</li>
+        <li>积分来源:学习、考试通过、签到、活动参与</li>
+        <li>显示距下一等级所需积分差值</li>
+        <li>点击跳转积分详细记录页面</li>
+        <li>等级标志在个人主页显示</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff2f0;">🔔</div><div><div class="feature-name">消息通知</div><div class="feature-tag">系统公告 + 个人消息</div></div></div>
+      <ul class="feature-list">
+        <li>系统公告:平台通知、考试提醒、安全预警</li>
+        <li>个人消息:审批结果、培训报名确认</li>
+        <li>未读消息数角标显示在导航图标上</li>
+        <li>点击消息可跳转对应功能页</li>
+        <li>支持消息标为已读与全部已读</li>
+        <li>重要通知支持置顶展示</li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== STUDY MODULE ===== -->
+<div class="section" id="study">
+  <div class="section-title">学习培训模块</div>
+  <div class="section-desc">提供在线课程、视频学习、考前培训与线下培训四个维度,满足不同学习场景需求,学习进度全程记录。</div>
+  <div class="feature-grid">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">⭐</div><div><div class="feature-name">推荐课程 Tab</div><div class="feature-tag">精选安全课程</div></div></div>
+      <ul class="feature-list">
+        <li>展示平台精选安全知识课程卡片</li>
+        <li>卡片显示课程封面、名称、学时</li>
+        <li>显示课程热度与学习人数</li>
+        <li>个性化推荐基于用户学习历史</li>
+        <li>支持收藏课程到个人学习清单</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">📂</div><div><div class="feature-name">全部课程 Tab</div><div class="feature-tag">分类浏览</div></div></div>
+      <ul class="feature-list">
+        <li>按类别展示全部课程(化学/生物/辐射/消防等)</li>
+        <li>支持关键字搜索课程名称</li>
+        <li>筛选器:按类别/学时/难度过滤</li>
+        <li>列表/网格视图切换</li>
+        <li>显示已学和未学标记</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">🎓</div><div><div class="feature-name">考前课程 Tab</div><div class="feature-tag">必修关联</div></div></div>
+      <ul class="feature-list">
+        <li>仅展示与考试任务关联的必修课程</li>
+        <li>标注对应考试科目与完成要求</li>
+        <li>未完成课程影响考试资格提示</li>
+        <li>完成进度与考试倒计时同步显示</li>
+        <li>完成后自动解锁对应考试入口</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">📍</div><div><div class="feature-name">线下培训 Tab</div><div class="feature-tag">报名与签到</div></div></div>
+      <ul class="feature-list">
+        <li>展示所有线下培训活动列表</li>
+        <li>活动状态:待开始/进行中/已结束</li>
+        <li>一键报名,名额有限先到先得</li>
+        <li>已报名活动显示签到二维码入口</li>
+        <li>管理员可查看参与名单与统计</li>
+        <li>活动结束后可查看历史记录</li>
+      </ul>
+    </div>
+  </div>
+  <div class="sub-title">课程详情与视频播放</div>
+  <div class="feature-grid-2">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">📖</div><div><div class="feature-name">课程详情页</div><div class="feature-tag">screen-knowledge-detail</div></div></div>
+      <ul class="feature-list">
+        <li>展示课程封面、简介、讲师信息</li>
+        <li>章节目录列表,点击跳转对应视频</li>
+        <li>可视化学习进度(已学/全部章节数)</li>
+        <li>课程资料下载入口(如有附件)</li>
+        <li>学习记录:上次学到哪一章自动标记</li>
+        <li>完成全部章节后标注"已完成"并发放积分</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">▶️</div><div><div class="feature-name">学习资料阅读页</div><div class="feature-tag">screen-study-detail</div></div></div>
+      <ul class="feature-list">
+        <li>支持竖屏/横屏自动适配</li>
+        <li>播放进度自动保存,下次续播</li>
+        <li>播放速度调节(0.5x ~ 2x)</li>
+        <li>视频下方显示当前章节笔记区域</li>
+        <li>不可快进(防止学时作弊保障)</li>
+        <li>完成播放自动更新课程进度</li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== EXAM MODULE ===== -->
+<div class="section" id="exam">
+  <div class="section-title">在线考试模块</div>
+  <div class="section-desc">支持正式考试与模拟考试,内置倒计时、防切屏、自动提交等机制,确保考试公正性;成绩实时反馈并可查看答题详情。</div>
+  <div class="feature-grid-3">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">📋</div><div><div class="feature-name">考试任务 Tab</div><div class="feature-tag">必考任务</div></div></div>
+      <ul class="feature-list">
+        <li>列出当前用户所有必参加考试任务</li>
+        <li>每项显示考试名称、截止时间、剩余天数</li>
+        <li>状态标签:待参加/进行中/已完成/已过期</li>
+        <li>点击进入考试须知与开始确认页</li>
+        <li>已完成考试显示成绩与通过状态</li>
+        <li>可查看对应考试的历次成绩记录</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">🎯</div><div><div class="feature-name">模拟考试 Tab</div><div class="feature-tag">不限次练习</div></div></div>
+      <ul class="feature-list">
+        <li>提供与正式考试同类型的模拟题库</li>
+        <li>参加次数不限,成绩不计入档案</li>
+        <li>系统随机抽题,每次题目不同</li>
+        <li>模拟结束立即显示成绩与答题解析</li>
+        <li>记录历次模拟成绩与最高分</li>
+        <li>标记薄弱知识点辅助备考</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">📊</div><div><div class="feature-name">我的成绩 Tab</div><div class="feature-tag">历次记录</div></div></div>
+      <ul class="feature-list">
+        <li>展示所有考试科目的历次成绩</li>
+        <li>每条记录显示考试名称、时间、分数、通过状态</li>
+        <li>支持按时间/科目/通过状态筛选</li>
+        <li>点击记录查看答题详情与解析</li>
+        <li>通过考试的记录关联证书信息</li>
+      </ul>
+    </div>
+  </div>
+  <div class="sub-title">答题流程</div>
+  <div class="flow-row">
+    <div class="flow-step"><div class="flow-num">1</div><div class="flow-name">考试须知</div><div class="flow-desc">展示规则、题量、时长、注意事项</div></div>
+    <div class="flow-arrow">→</div>
+    <div class="flow-step"><div class="flow-num">2</div><div class="flow-name">开始答题</div><div class="flow-desc">点击确认后倒计时开始,进入答题页</div></div>
+    <div class="flow-arrow">→</div>
+    <div class="flow-step"><div class="flow-num">3</div><div class="flow-name">答题中</div><div class="flow-desc">逐题作答,可标记跳过,顶部进度条</div></div>
+    <div class="flow-arrow">→</div>
+    <div class="flow-step"><div class="flow-num">4</div><div class="flow-name">提交确认</div><div class="flow-desc">提交前显示未作答题数,确认后提交</div></div>
+    <div class="flow-arrow">→</div>
+    <div class="flow-step"><div class="flow-num">5</div><div class="flow-name">考试结果</div><div class="flow-desc">显示分数、通过状态、可查看详情</div></div>
+  </div>
+  <div class="notice notice-warn"><span class="notice-icon">⚠️</span><div>答题过程中切换到其他 App 或页面时,系统将弹出警告提示。倒计时归零后自动提交当前已作答内容,未作答题目计为错误。</div></div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== PRACTICE MODULE ===== -->
+<div class="section" id="practice">
+  <div class="section-title">刷题练习模块</div>
+  <div class="section-desc">提供随机练习、错题本、按类别练习三种模式,帮助用户查漏补缺,强化薄弱知识点,答题即时反馈。</div>
+  <div class="feature-grid-3">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff2f0;">🎲</div><div><div class="feature-name">随机练习</div><div class="feature-tag">系统随机出题</div></div></div>
+      <ul class="feature-list">
+        <li>系统从全题库随机抽取题目</li>
+        <li>每次练习题目不重复,覆盖多知识点</li>
+        <li>可设置单次练习题目数量(10/20/30题)</li>
+        <li>作答后立即显示正误与解析</li>
+        <li>错误题目自动加入错题本</li>
+        <li>支持随时暂停,进度自动保存</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff2f0;">📒</div><div><div class="feature-name">错题本</div><div class="feature-tag">精准查漏补缺</div></div></div>
+      <ul class="feature-list">
+        <li>汇总所有练习与考试中的历史错题</li>
+        <li>按知识点分类归纳错题</li>
+        <li>显示错误次数与最近错误时间</li>
+        <li>支持重新练习错题,答对后可移除</li>
+        <li>手动收藏重要题目到错题本</li>
+        <li>错题统计图表辅助分析薄弱环节</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff2f0;">🗂️</div><div><div class="feature-name">按类别练习</div><div class="feature-tag">针对性强化</div></div></div>
+      <ul class="feature-list">
+        <li>按知识点类别选择练习范围</li>
+        <li>支持按题型筛选(单选/多选/判断)</li>
+        <li>显示每个类别的历史正确率</li>
+        <li>可选择困难模式(只做错过的题)</li>
+        <li>完成一轮后显示本次练习分析报告</li>
+        <li>成绩趋势图展示提升进度</li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+
+<div class="divider"></div>
+
+<!-- ===== LAB APPROVAL ===== -->
+<div class="section" id="approval">
+  <div class="section-title">实验室审批功能</div>
+  <div class="section-desc">学生可提交实验室绑定申请,审批人通过移动端处理申请,全流程通知推送,角标提示待处理数量。</div>
+  <div class="sub-title">审批流程</div>
+  <div class="flow-row">
+    <div class="flow-step"><div class="flow-num">1</div><div class="flow-name">提交申请</div><div class="flow-desc">学生填写实验室信息,提交绑定申请</div></div>
+    <div class="flow-arrow">→</div>
+    <div class="flow-step"><div class="flow-num">2</div><div class="flow-name">通知推送</div><div class="flow-desc">审批人收到消息通知,角标数量+1</div></div>
+    <div class="flow-arrow">→</div>
+    <div class="flow-step"><div class="flow-num">3</div><div class="flow-name">查看详情</div><div class="flow-desc">审批人查看申请详情与安全须知</div></div>
+    <div class="flow-arrow">→</div>
+    <div class="flow-step"><div class="flow-num">4</div><div class="flow-name">审批操作</div><div class="flow-desc">通过或驳回,可填写审批意见</div></div>
+    <div class="flow-arrow">→</div>
+    <div class="flow-step"><div class="flow-num">5</div><div class="flow-name">结果通知</div><div class="flow-desc">学生收到审批结果消息推送</div></div>
+  </div>
+  <div class="feature-grid-2">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">🏛️</div><div><div class="feature-name">审批列表与详情</div><div class="feature-tag">screen-approval</div></div></div>
+      <ul class="feature-list">
+        <li>展示待处理与历史审批列表</li>
+        <li>每条显示申请人姓名、学号、申请实验室</li>
+        <li>状态标签:待审批/已通过/已驳回</li>
+        <li>「我的」页面显示待审批数量角标</li>
+        <li>支持按状态筛选申请记录</li>
+        <li>点击查看完整申请详情</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">📋</div><div><div class="feature-name">审批详情</div><div class="feature-tag">screen-approval(详情区域)</div></div></div>
+      <ul class="feature-list">
+        <li>展示申请人完整信息(姓名/学院/学号)</li>
+        <li>申请实验室名称与编号</li>
+        <li>申请理由与实验室安全须知确认状态</li>
+        <li>审批意见文本框(驳回时必填)</li>
+        <li>通过/驳回操作按钮,操作不可撤销</li>
+        <li>历史审批记录在详情页底部展示</li>
+      </ul>
+    </div>
+  </div>
+  <div class="notice notice-info"><span class="notice-icon">💡</span><div>审批人身份由后台配置,普通学生无法看到审批入口。审批后的结果会通过消息通知实时推送给申请人,双方均可在消息中心查看记录。</div></div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== OFFLINE TRAINING ===== -->
+<div class="section" id="offline">
+  <div class="section-title">线下培训管理功能</div>
+  <div class="section-desc">管理员可在移动端发布培训活动、展示签到二维码,实时查看签到统计与人员名单,支持草稿与发布状态管理。</div>
+  <div class="feature-grid-3">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">➕</div><div><div class="feature-name">发布培训活动</div><div class="feature-tag">管理员专属</div></div></div>
+      <ul class="feature-list">
+        <li>填写培训标题、简介、培训目标</li>
+        <li>设置培训时间(开始/结束时间)</li>
+        <li>设置培训地点(支持文字或地图选点)</li>
+        <li>设置最大参与人数限制</li>
+        <li>支持存为草稿后继续编辑</li>
+        <li>发布后学生端立即可见并可报名</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">📱</div><div><div class="feature-name">签到二维码</div><div class="feature-tag">现场扫码签到</div></div></div>
+      <ul class="feature-list">
+        <li>管理员在培训详情页展示专属签到码</li>
+        <li>二维码包含活动 ID 与时效校验</li>
+        <li>学生使用微信/浏览器扫码完成签到</li>
+        <li>签到成功后积分自动累计</li>
+        <li>二维码支持刷新(每 5 分钟自动更新)</li>
+        <li>重复签到系统自动拦截提示</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">📊</div><div><div class="feature-name">签到统计</div><div class="feature-tag">实时查看</div></div></div>
+      <ul class="feature-list">
+        <li>实时显示总报名人数、已签到、未签到数</li>
+        <li>进度环形图直观展示签到比例</li>
+        <li>可查看已签到学生名单(姓名+学工号+学院)</li>
+        <li>可查看未签到学生名单,支持手动补签</li>
+        <li>名单支持导出为 Excel 文件</li>
+        <li>按学院/专业分组查看签到情况</li>
+      </ul>
+    </div>
+  </div>
+  <div class="sub-title">活动状态管理</div>
+  <div class="feature-grid-2">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">📝</div><div><div class="feature-name">草稿状态</div><div class="feature-tag">仅管理员可见</div></div></div>
+      <ul class="feature-list">
+        <li>草稿状态学生端不展示,仅管理员可编辑</li>
+        <li>支持保存草稿后继续修改内容</li>
+        <li>草稿列表在管理员的培训管理入口展示</li>
+        <li>切换为发布状态后立即对学生可见</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">🚀</div><div><div class="feature-name">发布状态</div><div class="feature-tag">学生可见可报名</div></div></div>
+      <ul class="feature-list">
+        <li>发布后出现在学习培训-线下培训列表</li>
+        <li>符合条件的学生可报名参加</li>
+        <li>管理员仍可编辑时间、地点等信息</li>
+        <li>可切换回草稿状态(已报名学生收到通知)</li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== MY SCORES ===== -->
+<div class="section" id="scores">
+  <div class="section-title">我的成绩</div>
+  <div class="section-desc">汇总展示用户所有考试科目的历次成绩记录,支持筛选与排序,可深入查看每次考试的完整答题详情。</div>
+  <div class="feature-grid-2">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">📊</div><div><div class="feature-name">成绩列表</div><div class="feature-tag">screen-scores</div></div></div>
+      <ul class="feature-list">
+        <li>按科目分组展示所有历次考试成绩</li>
+        <li>每条记录:科目名称、考试时间、得分、满分、通过状态</li>
+        <li><span class="tag tag-green">通过</span> / <span class="tag tag-red">未通过</span> 标签直观区分</li>
+        <li>支持按时间倒序或成绩高低排序</li>
+        <li>支持按科目/通过状态筛选</li>
+        <li>点击任意记录进入答题详情页</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">📈</div><div><div class="feature-name">成绩统计</div><div class="feature-tag">数据概览</div></div></div>
+      <ul class="feature-list">
+        <li>顶部展示总考试次数与通过率</li>
+        <li>各科目最高分与平均分对比</li>
+        <li>成绩趋势折线图(近 6 次考试)</li>
+        <li>通过考试关联证书,点击可查看</li>
+        <li>未通过考试显示建议复习知识点</li>
+      </ul>
+    </div>
+  </div>
+</div>
+
+<!-- ===== SCORE DETAIL ===== -->
+<div class="section" id="score-detail">
+  <div class="section-title">答题详情</div>
+  <div class="section-desc">逐题展示考试作答情况,标注正确答案与解析,帮助用户找出知识盲点,支持将错题加入练习本。</div>
+  <div class="feature-grid-2">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">📝</div><div><div class="feature-name">题目回顾</div><div class="feature-tag">screen-score-detail</div></div></div>
+      <ul class="feature-list">
+        <li>逐题展示题目内容与用户作答</li>
+        <li>绿色标注正确答案,红色标注错误作答</li>
+        <li>每题附带知识点解析说明</li>
+        <li>顶部显示本次得分与题目总数</li>
+        <li>快速跳转到指定题目编号</li>
+        <li>错题可一键加入错题本</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">🔍</div><div><div class="feature-name">答题分析</div><div class="feature-tag">辅助提升</div></div></div>
+      <ul class="feature-list">
+        <li>按知识点分类汇总本次答题正确率</li>
+        <li>标注薄弱知识点并推荐相关课程</li>
+        <li>与上次考试对比,展示进步情况</li>
+        <li>可直接从详情页发起再次刷题练习</li>
+        <li>分享成绩卡片功能(可选)</li>
+      </ul>
+    </div>
+  </div>
+  <div class="notice notice-info"><span class="notice-icon">💡</span><div>答题详情页同时适用于<strong>正式考试</strong>与<strong>模拟考试</strong>的结果回顾,路由均为 screen-score-detail,通过传入不同的考试记录 ID 区分。</div></div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== ROLES & PERMISSIONS ===== -->
+<div class="section" id="roles">
+  <div class="section-title">角色与权限说明</div>
+  <div class="section-desc">系统内置三种用户角色,权限由后台统一配置,前端根据角色动态显示/隐藏对应功能入口。</div>
+  <table class="spec-table">
+    <thead>
+      <tr>
+        <th>角色</th>
+        <th>可访问模块</th>
+        <th>特有权限</th>
+        <th>备注</th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr>
+        <td><span class="tag tag-blue">学生(默认)</span></td>
+        <td>全部模块(工作台、学习培训、在线考试、刷题练习、我的)</td>
+        <td>学习课程/参加考试/培训签到/查看成绩与证书/提交实验室绑定申请</td>
+        <td>主要用户群体,登录后默认角色</td>
+      </tr>
+      <tr>
+        <td><span class="tag tag-orange">实验室管理员</span></td>
+        <td>全部模块 + 线下培训管理功能</td>
+        <td>发布培训活动/展示签到二维码/查看签到统计与名单/切换草稿发布状态</td>
+        <td>需后台配置管理员身份,可同时拥有学生权限</td>
+      </tr>
+      <tr>
+        <td><span class="tag tag-purple">实验室审批人</span></td>
+        <td>全部模块 + 审批管理功能</td>
+        <td>查看审批列表/审批或驳回申请/填写审批意见/查看申请详情</td>
+        <td>工作台与消息通知显示待审批数角标</td>
+      </tr>
+    </tbody>
+  </table>
+  <div class="notice notice-warn"><span class="notice-icon">⚠️</span><div>一个用户可同时具备多个角色(如既是管理员又是审批人)。权限组合由后台灵活配置,前端无需修改代码即可生效。</div></div>
+</div>
+
+<div class="divider"></div>
+
+<!-- ===== ADMIN FEATURES ===== -->
+<div class="section" id="admin">
+  <div class="section-title">管理端功能汇总</div>
+  <div class="section-desc">具备管理员或审批人身份的用户,在移动端可使用以下专属管理功能,普通学生不可见。</div>
+  <div class="feature-grid-3">
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#fff7e6;">🎤</div><div><div class="feature-name">培训活动管理</div><div class="feature-tag">实验室管理员</div></div></div>
+      <ul class="feature-list">
+        <li>创建新的线下培训活动</li>
+        <li>编辑已有活动信息(时间/地点/人数)</li>
+        <li>切换活动草稿/发布状态</li>
+        <li>展示并刷新签到二维码</li>
+        <li>实时查看签到统计数据</li>
+        <li>导出签到名单为 Excel</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#eef3fd;">🏛️</div><div><div class="feature-name">申请审批管理</div><div class="feature-tag">实验室审批人</div></div></div>
+      <ul class="feature-list">
+        <li>查看所有待处理实验室绑定申请</li>
+        <li>审批或驳回申请,填写意见</li>
+        <li>查看申请人完整信息与申请记录</li>
+        <li>工作台角标实时提示待处理数量</li>
+        <li>消息通知同步新申请提醒</li>
+        <li>历史审批记录可随时查阅</li>
+      </ul>
+    </div>
+    <div class="feature-card">
+      <div class="feature-card-header"><div class="feature-icon" style="background:#edfaf6;">📋</div><div><div class="feature-name">人员名单管理</div><div class="feature-tag">实验室管理员</div></div></div>
+      <ul class="feature-list">
+        <li>查看培训活动已报名学生名单</li>
+        <li>按已签到/未签到筛选名单</li>
+        <li>名单信息:姓名、学工号、学院、签到时间</li>
+        <li>支持手动为学生补录签到记录</li>
+        <li>按学院/专业分组统计查看</li>
+        <li>名单一键导出为 Excel 文件</li>
+      </ul>
+    </div>
+  </div>
+  <div class="notice notice-info"><span class="notice-icon">💡</span><div>管理端功能均嵌入在移动端应用内,无需单独安装。管理员身份由系统后台统一配置,前端识别身份后动态展示对应管理入口,普通学生界面保持简洁。</div></div>
+</div>
+
+</div><!-- end main-content -->
+</div><!-- end doc-body -->
+
+<!-- ===== FOOTER ===== -->
+<div class="doc-footer">
+  <div>© 2026 实验室安全教育平台 · 移动端应用 v2.1 | 文档版本:2026-08 | 技术支持:信息化管理中心</div>
+  <div style="display:flex;gap:24px;align-items:center;">
+    <span>📱 移动端 H5 单页应用</span>
+    <span>🗂️ 22 个功能页面</span>
+    <span>🔐 3 种用户角色</span>
+  </div>
+</div>
+
+</div><!-- end page -->
+</body>
+</html>
+

Разница между файлами не показана из-за своего большого размера
+ 1762 - 0
web_exam.html


BIN
新生绑定实验室须知及责任说明.docx


+ 22 - 0
考试模板需求.md

@@ -0,0 +1,22 @@
+考试管理菜单下生成考试模版二级菜单页面,Tab标签切换:开学考试、分级考试、应知应会;三种类型的新建考试内容分别根据考试类型匹配区分配置项展示,具体配置参考图片新建考试.img;
+1.三种类型的新建考试均增加的配置项:
+1.1.校级(全校或指定范围)、院级(所属学院范围的)、实验室(自己负责的实验室)三个层级创建和可见不同权限范围的考试;
+1.2.增加每学年到期自动执行的开关,开启后每学年到考试配置的日期,系统自动创建该考试,无需重复配置和创建;
+1.3.每个考试类型的配置模版唯一;用户可修改编辑自己权限范围内的考试模版;
+2.Tab标签-开学考试:
+2.1.系统内置考试类型模板;
+2.2.用户类别:面向当前学年新入学的本科生(默认勾选)、新入学的研究生、新入职的教职工;
+2.3.学时配置:分别根据用户类别来确定需完成的学时要求(为0时无需完成学时),才可参加考试;
+2.4.支持按学院分别配置不同的试卷,选择试卷时可按考知识点选择,支持多选;
+2.5.系统创建考试后自动按照考试范围配置匹配所有符合条件的用户执行该考试;
+3.Tab标签-分级考试
+3.1.Tab标签切换:Ⅰ级(重大风险)、Ⅱ级(高风险)、Ⅲ级(中风险)、Ⅳ级(低风险),分别配置:
+3.1.1. 系统内置考试类型模板;
+3.1.2. 用户类别:面向当前学年通过新生入学考试后绑定了实验室的本科生、新入学绑定了实验室的研究生,默认全部勾选;
+3.2.3. 不同用户类别的学时要求按照公共配置的分级配置规则执行;
+3.2.4. 系统创建考试后自动按照考试范围配置匹配所有符合条件的用户执行该考试;
+4.Tab标签-应知应会
+4.1.系统内置考试类型模板;
+4.2.用户类别:面向无需进入实验室的新入学研究生、新入职教职工、在读研究生(按实验室分级取年度学时值,置灰展示)、存量教职工、在读本科生,默认全部勾选;
+4.3.学时配置:分别根据用户类别来确定需完成的学时要求(为0时无需完成学时),才可参加考试;
+4.4.系统创建考试后自动按照考试范围配置匹配所有符合条件的用户执行该考试;