index.html 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>安全教育与考试 - 工作台</title>
  7. <!-- Element UI CSS -->
  8. <link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
  9. <!-- Custom Styles -->
  10. <link rel="stylesheet" href="./css/styles.css">
  11. </head>
  12. <body>
  13. <div id="app">
  14. <div class="layout">
  15. <!-- 侧边栏 -->
  16. <aside class="sidebar">
  17. <div class="sidebar-logo">
  18. <i class="el-icon-s-cooperation logo-icon"></i>
  19. <span class="logo-text">安全教育与考试</span>
  20. </div>
  21. <ul class="nav-menu">
  22. <li v-for="item in visibleMenuItems" :key="item.key"
  23. :class="['nav-item', { active: activePage === item.key }]"
  24. @click="activePage = item.key">
  25. <i :class="item.icon"></i>
  26. <span>{{ item.label }}</span>
  27. </li>
  28. </ul>
  29. </aside>
  30. <!-- 主内容区 -->
  31. <div class="main-wrapper">
  32. <!-- 顶部栏 -->
  33. <header class="topbar">
  34. <div class="breadcrumb">
  35. <span>首页</span>
  36. <span class="sep">/</span>
  37. <span class="current">{{ currentPageLabel }}</span>
  38. </div>
  39. <div class="user-badge">
  40. <el-avatar :size="32" icon="el-icon-user-solid"></el-avatar>
  41. <span class="user-name">{{ currentRole.name }}</span>
  42. </div>
  43. </header>
  44. <!-- 工作台页面 -->
  45. <main class="content" v-if="activePage === 'workbench'">
  46. <!-- 角色切换器 -->
  47. <div class="page-toolbar">
  48. <h2 class="page-title">工作台</h2>
  49. <div class="role-switcher">
  50. <el-button type="info" size="small" icon="el-icon-document" @click="activePage = 'prd'">需求说明</el-button>
  51. <el-button type="success" size="small" icon="el-icon-s-operation" @click="activePage = 'flow'">业务流程说明</el-button>
  52. <el-button v-for="btn in roleButtons" :key="btn.key"
  53. :type="activeRole === btn.key ? 'primary' : 'default'"
  54. size="small" @click="switchRole(btn.key)">
  55. {{ btn.label }}
  56. </el-button>
  57. </div>
  58. </div>
  59. <!-- 用户信息卡 -->
  60. <section class="panel">
  61. <div class="panel-head">
  62. <div class="panel-title"><h2>用户信息卡</h2></div>
  63. <div class="panel-desc">当前推荐资料类型:{{ resolvedResourceTab }}</div>
  64. </div>
  65. <div class="user-card-grid">
  66. <!-- 左:个人信息 -->
  67. <div class="card-box meta-box">
  68. <div class="meta-grid">
  69. <div class="meta-item"><span>用户姓名</span><strong>{{ currentRole.name }}</strong></div>
  70. <div class="meta-item"><span>学工号</span><strong>{{ currentRole.code }}</strong></div>
  71. <div class="meta-item"><span>用户类型</span><strong>{{ currentRole.type }}</strong></div>
  72. <div class="meta-item"><span>所属单位</span><strong>{{ currentRole.org }}</strong></div>
  73. <div class="meta-item" v-if="!isStaffRole"><span>实验室信息</span><strong>{{ currentRole.lab ? currentRole.lab.name + '(' + currentRole.lab.room + ')' : '暂无绑定实验室' }}</strong></div>
  74. <div class="meta-item" v-if="!isStaffRole"><span>负责人</span><strong>{{ currentRole.lab ? currentRole.lab.director : '不适用' }}</strong></div>
  75. </div>
  76. <div class="summary-tip" v-if="!isStaffRole">
  77. 绑定状态:<strong>{{ bindingStatusText }}</strong>
  78. <el-button v-if="currentRole.bindingStatus === 'unbound'" type="text" size="mini" @click="openBindDialog('graduate')" style="margin-left:8px">去绑定</el-button>
  79. </div>
  80. </div>
  81. <!-- 中:学时进度 -->
  82. <div class="card-box status-box">
  83. <div class="tag-row">
  84. <span class="mini-tag normal">{{ currentRole.term }}</span>
  85. <span v-if="currentRole.lab" :class="['mini-tag', levelClass(currentRole.lab.level)]">{{ currentRole.lab.level }}级实验室</span>
  86. <span v-if="currentRole.lab" class="mini-tag normal">{{ currentRole.lab.category }}</span>
  87. </div>
  88. <template v-if="currentRole.requiredHours > 0">
  89. <div class="hours-progress">
  90. <div class="progress-head">
  91. <span>年度学时进度</span>
  92. <strong>{{ currentRole.completedHours }} / {{ currentRole.requiredHours }} 学时</strong>
  93. </div>
  94. <div class="progress-bar"><span :style="{ width: hoursPercentage + '%' }"></span></div>
  95. </div>
  96. <div class="kpi-row">
  97. <div class="kpi-box">
  98. <span>完成度</span>
  99. <strong>{{ hoursPercentage }}<em>%</em></strong>
  100. </div>
  101. <div class="kpi-box">
  102. <span>目标考试</span>
  103. <strong class="kpi-exam">{{ currentRole.examType }}</strong>
  104. </div>
  105. </div>
  106. </template>
  107. <template v-else>
  108. <div class="kpi-row" style="margin-top:12px">
  109. <div class="kpi-box">
  110. <span>学时要求</span>
  111. <strong class="kpi-exam">无</strong>
  112. </div>
  113. <div class="kpi-box">
  114. <span>目标考试</span>
  115. <strong class="kpi-exam">{{ currentRole.examType }}</strong>
  116. </div>
  117. </div>
  118. </template>
  119. </div>
  120. <!-- 右:任务状态 -->
  121. <div class="card-box status-box">
  122. <div class="panel-desc" style="margin-bottom:10px">当前学年任务状态</div>
  123. <div class="status-list">
  124. <div class="status-item">
  125. <span>考试状态</span>
  126. <span :class="['status-chip', examChipClass]">{{ currentRole.examStatus }}</span>
  127. </div>
  128. <div class="status-item">
  129. <span>证书状态</span>
  130. <span :class="['status-chip', certChipClass]">{{ currentRole.certificateStatus }}</span>
  131. </div>
  132. <div class="status-item" v-if="currentRole.bindingStatus !== 'none'">
  133. <span>审批状态</span>
  134. <span :class="['status-chip', approvalChipClass]">{{ bindingStatusText }}</span>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. </section>
  140. <!-- 功能卡片区 -->
  141. <section class="panel">
  142. <div class="panel-head">
  143. <div class="panel-title"><h2>工作台功能卡片区</h2></div>
  144. </div>
  145. <div class="feature-grid">
  146. <!-- 学年数据统计 -->
  147. <article class="feature-card">
  148. <div class="feature-card-header">
  149. <h3>学年数据统计</h3>
  150. <el-button v-if="canShowGraduateReport" type="text" size="mini" class="report-btn" @click="showGraduateReport = true"><i class="el-icon-trophy"></i> 毕业报告</el-button>
  151. </div>
  152. <p>按学年统计历年考试通过情况与成绩</p>
  153. <div class="record-list record-list--fixed">
  154. <div class="record-item" v-for="(item, idx) in currentRole.yearlyRecords" :key="idx">
  155. <strong>{{ item.year }}</strong>
  156. <span>{{ item.status }} · {{ item.score }}</span>
  157. </div>
  158. </div>
  159. </article>
  160. <!-- 学习任务 -->
  161. <article class="feature-card">
  162. <h3>学习任务</h3>
  163. <p>学院下发的学习任务</p>
  164. <div class="record-list record-list--scroll">
  165. <div class="record-item" v-for="(task, idx) in collegeTasks" :key="idx">
  166. <strong>{{ task.title }}</strong>
  167. <span><i :class="['task-dot', task.status]"></i> {{ task.note }}</span>
  168. </div>
  169. </div>
  170. </article>
  171. <!-- 资料类型 -->
  172. <article class="feature-card">
  173. <h3>资料类型</h3>
  174. <p>{{ currentRole.lab ? '根据实验室 ' + currentRole.lab.category + ' 分类推荐' : '默认推荐应知应会通识资源' }}</p>
  175. <div class="record-list">
  176. <div class="record-item" v-for="(res, ri) in currentRole.resources" :key="ri">
  177. <strong>{{ res.title }}</strong>
  178. <span>{{ res.desc }}</span>
  179. </div>
  180. </div>
  181. </article>
  182. <!-- 模拟练习 -->
  183. <article class="feature-card">
  184. <h3>模拟练习</h3>
  185. <p>刷题进度和正确率</p>
  186. <div class="meter-box">
  187. <strong>{{ currentRole.practice.done }} / {{ currentRole.practice.total }}</strong>
  188. <span>已完成题目数 / 题库总数</span>
  189. </div>
  190. <div class="meter-box">
  191. <strong>{{ currentRole.practice.correctRate }}</strong>
  192. <span>当前模拟练习正确率</span>
  193. </div>
  194. </article>
  195. <!-- 考试信息 -->
  196. <article class="feature-card">
  197. <h3>{{ currentRole.examType }}</h3>
  198. <p>{{ examCardDesc }}</p>
  199. <div class="record-list record-list--fixed">
  200. <div class="record-item">
  201. <strong>学时完成</strong>
  202. <span>{{ currentRole.requiredHours > 0 ? currentRole.completedHours + ' / ' + currentRole.requiredHours + ' 学时' : '无学时要求' }}</span>
  203. </div>
  204. <div class="record-item">
  205. <strong>考试状态</strong>
  206. <span><i :class="['task-dot', currentRole.examStatus === '已通过' ? 'in-progress' : 'locked']"></i> {{ currentRole.examStatus }}</span>
  207. </div>
  208. </div>
  209. <el-button v-if="currentRole.examUnlocked && currentRole.examStatus !== '已通过'" type="primary" size="small" style="margin-top:auto" @click="simulatePassExam">模拟通过考试</el-button>
  210. <el-button v-if="!currentRole.examUnlocked && currentRole.requiredHours > 0" type="info" size="small" disabled style="margin-top:auto">完成学时后解锁</el-button>
  211. <el-button v-if="activeRole === 'newUndergraduate' && currentRole.examStatus === '已通过'" type="warning" size="small" style="margin-top:8px" @click="openBindDialog('undergraduate')">绑定实验室</el-button>
  212. <el-button v-if="!currentRole.examUnlocked && currentRole.requiredHours > 0" type="text" size="mini" style="margin-top:4px;color:#909399" @click="simulateCompleteAndPass">[ 模拟完成学时并通过考试 ]</el-button>
  213. </article>
  214. </div>
  215. </section>
  216. <!-- 安全素养资源区 -->
  217. <div class="resource-section">
  218. <div class="section-header">
  219. <h3 class="section-title">实验室安全素养资源</h3>
  220. <span class="section-hint" v-if="currentRole.lab">
  221. <el-tag size="mini" type="warning">当前绑定:{{ currentRole.lab.category }}</el-tag>
  222. </span>
  223. </div>
  224. <el-tabs v-model="activeResourceTab" type="card">
  225. <el-tab-pane v-for="tab in resourceTabs" :key="tab" :label="tab" :name="tab">
  226. <div class="resource-content resource-split">
  227. <!-- 左:资料区 -->
  228. <div class="resource-block resource-left">
  229. <div class="block-header">
  230. <h5><i class="el-icon-folder-opened"></i> 学习资料</h5>
  231. <el-button type="text" size="mini" icon="el-icon-more">查看更多</el-button>
  232. </div>
  233. <div class="doc-list">
  234. <div class="doc-card" v-for="(doc, i) in getResourceDocs(tab)" :key="i" @click="viewDocDetail(doc)">
  235. <div class="doc-icon">
  236. <i :class="doc.type === 'PDF' ? 'el-icon-document' : 'el-icon-data-board'"></i>
  237. </div>
  238. <div class="doc-body">
  239. <span class="doc-title">{{ doc.title }}</span>
  240. <div class="doc-meta-row">
  241. <span class="doc-meta">{{ doc.type }} · {{ doc.pages }}页</span>
  242. <span class="doc-duration"><i class="el-icon-time"></i> {{ doc.duration }}</span>
  243. </div>
  244. </div>
  245. </div>
  246. </div>
  247. </div>
  248. <!-- 右:题库区 -->
  249. <div class="resource-block resource-right">
  250. <div class="block-header">
  251. <h5><i class="el-icon-edit-outline"></i> 题库</h5>
  252. <el-button type="text" size="mini" icon="el-icon-more">查看更多</el-button>
  253. </div>
  254. <div class="question-grid">
  255. <div class="question-card" v-for="(cat, ci) in getResourceQuestions(tab).categories" :key="ci">
  256. <div class="question-card-icon"><i class="el-icon-notebook-2"></i></div>
  257. <span class="question-cat-name">{{ cat.name }}</span>
  258. <span class="question-cat-count">{{ cat.count }} 题</span>
  259. </div>
  260. </div>
  261. </div>
  262. </div>
  263. </el-tab-pane>
  264. </el-tabs>
  265. </div>
  266. <!-- 视频资源区 -->
  267. <div class="video-section">
  268. <h3 class="section-title" style="margin-bottom:16px">视频学习资源</h3>
  269. <div class="video-toolbar">
  270. <el-tabs v-model="activeVideoTab" @tab-click="videoPage = 1">
  271. <el-tab-pane v-for="tab in videoTabs" :key="tab" :label="tab" :name="tab"></el-tab-pane>
  272. </el-tabs>
  273. <el-input v-model="videoSearch" placeholder="搜索视频" prefix-icon="el-icon-search" size="small" style="width:200px" clearable></el-input>
  274. </div>
  275. <div class="video-grid">
  276. <div class="video-card" v-for="video in paginatedVideos" :key="video.id">
  277. <div class="video-cover">
  278. <i class="el-icon-video-play"></i>
  279. <span class="video-duration">{{ video.duration }}</span>
  280. </div>
  281. <div class="video-info">
  282. <p class="video-title">{{ video.title }}</p>
  283. <p class="video-meta">{{ video.views }} 次观看</p>
  284. </div>
  285. </div>
  286. </div>
  287. <div class="video-pagination">
  288. <el-pagination small layout="prev, pager, next" :total="filteredVideos.length" :page-size="4" :current-page.sync="videoPage"></el-pagination>
  289. </div>
  290. </div>
  291. </main>
  292. <!-- 数据统计页面 -->
  293. <main class="content" v-if="activePage === 'statistics'">
  294. <div class="page-toolbar">
  295. <h2 class="page-title">数据统计</h2>
  296. </div>
  297. <el-tabs v-model="statsTopTab" type="border-card">
  298. <el-tab-pane label="报表" name="current">
  299. <!-- 考试情况统计 -->
  300. <section class="stats-section">
  301. <div class="stats-section-header">
  302. <h3>考试情况统计</h3>
  303. <div class="stats-section-actions">
  304. <el-select v-model="examYear" size="mini" style="width:130px;margin-right:8px">
  305. <el-option v-for="y in statisticsData.examYears" :key="y" :label="y + ' 学年'" :value="y"></el-option>
  306. </el-select>
  307. <el-radio-group v-model="statsExamType" size="mini" @change="renderStatsCharts">
  308. <el-radio-button label="graduate">研究生</el-radio-button>
  309. <el-radio-button label="undergraduate">本科生</el-radio-button>
  310. <el-radio-button label="staff">教职工</el-radio-button>
  311. </el-radio-group>
  312. <el-button size="mini" type="primary" icon="el-icon-download">导出数据</el-button>
  313. </div>
  314. </div>
  315. <div class="stats-chart-row">
  316. <div class="stats-chart-box" ref="examBarChart"></div>
  317. <div class="stats-chart-box" ref="examPieChart"></div>
  318. </div>
  319. <div class="stats-filter-row">
  320. <el-select v-model="examFilterCollege" placeholder="筛选学院" size="mini" clearable style="width:160px">
  321. <el-option v-for="c in statisticsData.colleges" :key="c" :label="c" :value="c"></el-option>
  322. </el-select>
  323. <el-select v-model="examFilterExamType" placeholder="考试类型" size="mini" clearable style="width:140px">
  324. <el-option v-if="statsExamType === 'graduate'" label="新生分级考试" value="新生分级考试"></el-option>
  325. <el-option v-if="statsExamType === 'graduate'" label="应知应会考试" value="应知应会考试"></el-option>
  326. <el-option v-if="statsExamType === 'undergraduate'" label="新生入学考试" value="新生入学考试"></el-option>
  327. <el-option v-if="statsExamType === 'undergraduate'" label="应知应会考试" value="应知应会考试"></el-option>
  328. <el-option v-if="statsExamType === 'staff'" label="新入职教职工" value="新入职教职工"></el-option>
  329. <el-option v-if="statsExamType === 'staff'" label="存量教职工" value="存量教职工"></el-option>
  330. <el-option label="汇总" value="汇总"></el-option>
  331. </el-select>
  332. </div>
  333. <!-- 研究生:区分新生分级考试和老生应知应会 -->
  334. <el-table v-if="statsExamType === 'graduate'" :data="filteredGraduateData" border size="small" style="width:100%;margin-top:8px" :span-method="examSpanMethod">
  335. <el-table-column prop="college" label="学院" min-width="150"></el-table-column>
  336. <el-table-column prop="examType" label="考试类型" min-width="120"></el-table-column>
  337. <el-table-column prop="total" label="应考人数" min-width="90"></el-table-column>
  338. <el-table-column prop="passed" label="通过人数" min-width="90"></el-table-column>
  339. <el-table-column prop="failed" label="未通过人数" min-width="100"></el-table-column>
  340. <el-table-column prop="absent" label="未考人数" min-width="90"></el-table-column>
  341. <el-table-column prop="rate" label="通过率" min-width="80"></el-table-column>
  342. </el-table>
  343. <!-- 本科生 -->
  344. <el-table v-if="statsExamType === 'undergraduate'" :data="filteredUndergraduateData" border size="small" style="width:100%;margin-top:8px" :span-method="examSpanMethod">
  345. <el-table-column prop="college" label="学院" min-width="150"></el-table-column>
  346. <el-table-column prop="examType" label="考试类型" min-width="120"></el-table-column>
  347. <el-table-column prop="total" label="应考人数" min-width="90"></el-table-column>
  348. <el-table-column prop="passed" label="通过人数" min-width="90"></el-table-column>
  349. <el-table-column prop="failed" label="未通过人数" min-width="100"></el-table-column>
  350. <el-table-column prop="absent" label="未考人数" min-width="90"></el-table-column>
  351. <el-table-column prop="rate" label="通过率" min-width="80"></el-table-column>
  352. </el-table>
  353. <!-- 教职工 -->
  354. <el-table v-if="statsExamType === 'staff'" :data="filteredStaffData" border size="small" style="width:100%;margin-top:8px" :span-method="examSpanMethod">
  355. <el-table-column prop="college" label="学院" min-width="150"></el-table-column>
  356. <el-table-column prop="examType" label="考试类型" min-width="120"></el-table-column>
  357. <el-table-column prop="total" label="应考人数" min-width="90"></el-table-column>
  358. <el-table-column prop="passed" label="通过人数" min-width="90"></el-table-column>
  359. <el-table-column prop="failed" label="未通过人数" min-width="100"></el-table-column>
  360. <el-table-column prop="absent" label="未考人数" min-width="90"></el-table-column>
  361. <el-table-column prop="rate" label="通过率" min-width="80"></el-table-column>
  362. </el-table>
  363. </section>
  364. <!-- 实验室与人员匹配 -->
  365. <section class="stats-section">
  366. <div class="stats-section-header">
  367. <h3>实验室与人员匹配</h3>
  368. <div class="stats-section-actions">
  369. <el-button size="mini" type="primary" icon="el-icon-download">导出各学院数据</el-button>
  370. <el-button size="mini" type="success" icon="el-icon-download">导出实验室人员明细</el-button>
  371. </div>
  372. </div>
  373. <div class="stats-chart-row">
  374. <div class="stats-chart-box stats-chart-full" ref="labLevelChart"></div>
  375. </div>
  376. <div class="stats-filter-row">
  377. <el-select v-model="labFilterCollege" placeholder="筛选学院" size="mini" clearable style="width:160px">
  378. <el-option v-for="c in statisticsData.colleges" :key="c" :label="c" :value="c"></el-option>
  379. </el-select>
  380. <el-select v-model="labFilterLevel" placeholder="实验室等级" size="mini" clearable style="width:140px">
  381. <el-option label="Ⅰ级" value="Ⅰ"></el-option>
  382. <el-option label="Ⅱ级" value="Ⅱ"></el-option>
  383. <el-option label="Ⅲ级" value="Ⅲ"></el-option>
  384. <el-option label="Ⅳ级" value="Ⅳ"></el-option>
  385. </el-select>
  386. </div>
  387. <el-table :data="filteredLabCollegeData" border size="small" style="width:100%;margin-top:8px">
  388. <el-table-column prop="college" label="学院" min-width="160"></el-table-column>
  389. <el-table-column prop="level1" label="Ⅰ级人数" min-width="100"></el-table-column>
  390. <el-table-column prop="level2" label="Ⅱ级人数" min-width="100"></el-table-column>
  391. <el-table-column prop="level3" label="Ⅲ级人数" min-width="100"></el-table-column>
  392. <el-table-column prop="level4" label="Ⅳ级人数" min-width="100"></el-table-column>
  393. <el-table-column prop="totalCount" label="总人数" min-width="90"></el-table-column>
  394. </el-table>
  395. </section>
  396. <!-- 学时统计 -->
  397. <section class="stats-section">
  398. <div class="stats-section-header">
  399. <h3>学时统计</h3>
  400. <div class="stats-section-actions">
  401. <el-button size="mini" type="primary" icon="el-icon-download">导出数据</el-button>
  402. </div>
  403. </div>
  404. <div class="stats-chart-row">
  405. <div class="stats-chart-box stats-chart-full" ref="hoursChart"></div>
  406. </div>
  407. <div class="stats-filter-row">
  408. <el-select v-model="hoursFilterType" placeholder="用户类型" size="mini" clearable style="width:120px">
  409. <el-option label="研究生" value="研究生"></el-option>
  410. <el-option label="本科生" value="本科生"></el-option>
  411. <el-option label="教职工" value="教职工"></el-option>
  412. </el-select>
  413. <el-select v-model="hoursFilterCollege" placeholder="学院" size="mini" clearable style="width:160px">
  414. <el-option v-for="c in statisticsData.colleges" :key="c" :label="c" :value="c"></el-option>
  415. </el-select>
  416. <el-select v-model="hoursFilterGrade" placeholder="年级" size="mini" clearable style="width:120px">
  417. <el-option v-for="g in gradeOptions" :key="g" :label="g" :value="g"></el-option>
  418. </el-select>
  419. <el-input v-model="hoursFilterKey" placeholder="学号/姓名" size="mini" prefix-icon="el-icon-search" clearable style="width:160px"></el-input>
  420. </div>
  421. <el-table :data="filteredHoursDetail" border size="small" style="width:100%">
  422. <el-table-column prop="name" label="姓名" min-width="80"></el-table-column>
  423. <el-table-column prop="code" label="学号" min-width="120"></el-table-column>
  424. <el-table-column prop="grade" label="年级" min-width="80"></el-table-column>
  425. <el-table-column prop="college" label="所属学院" min-width="140"></el-table-column>
  426. <el-table-column prop="y1" label="第一年" min-width="70"></el-table-column>
  427. <el-table-column prop="y2" label="第二年" min-width="70"></el-table-column>
  428. <el-table-column prop="y3" label="第三年" min-width="70"></el-table-column>
  429. <el-table-column prop="y4" label="第四年" min-width="70"></el-table-column>
  430. <el-table-column prop="totalHours" label="总时长" min-width="80"></el-table-column>
  431. </el-table>
  432. <div style="text-align:center;margin-top:12px">
  433. <el-pagination small layout="prev, pager, next" :total="hoursDetailFiltered.length" :page-size="hoursPageSize" :current-page.sync="hoursPage"></el-pagination>
  434. </div>
  435. </section>
  436. </el-tab-pane>
  437. </el-tabs>
  438. </main>
  439. <!-- 人员审批页面 -->
  440. <main class="content" v-if="activePage === 'approval'">
  441. <div class="page-toolbar">
  442. <h2 class="page-title">人员审批</h2>
  443. </div>
  444. <section class="stats-section">
  445. <div class="stats-section-header">
  446. <h3>实验室绑定申请</h3>
  447. <div class="stats-section-actions">
  448. <el-radio-group v-model="approvalFilter" size="mini">
  449. <el-radio-button label="all">全部</el-radio-button>
  450. <el-radio-button label="pending">待审批</el-radio-button>
  451. <el-radio-button label="approved">已通过</el-radio-button>
  452. <el-radio-button label="rejected">已驳回</el-radio-button>
  453. </el-radio-group>
  454. </div>
  455. </div>
  456. <el-table :data="filteredApprovalData" border size="small" style="width:100%">
  457. <el-table-column prop="studentName" label="申请人" min-width="80"></el-table-column>
  458. <el-table-column prop="studentCode" label="学号" min-width="110"></el-table-column>
  459. <el-table-column prop="studentType" label="用户类型" min-width="100"></el-table-column>
  460. <el-table-column prop="college" label="所属学院" min-width="130"></el-table-column>
  461. <el-table-column label="申请实验室" min-width="160">
  462. <template slot-scope="scope">{{ scope.row.labName }}({{ scope.row.labRoom }})</template>
  463. </el-table-column>
  464. <el-table-column label="分级" min-width="60">
  465. <template slot-scope="scope">
  466. <el-tag :type="levelTagType(scope.row.labLevel)" size="mini">{{ scope.row.labLevel }}</el-tag>
  467. </template>
  468. </el-table-column>
  469. <el-table-column prop="labCategory" label="分类" min-width="70"></el-table-column>
  470. <el-table-column prop="reason" label="进入原因" min-width="140" show-overflow-tooltip></el-table-column>
  471. <el-table-column prop="submitTime" label="提交时间" min-width="130"></el-table-column>
  472. <el-table-column prop="autoApproveDate" label="通过时间" min-width="130"></el-table-column>
  473. <el-table-column label="状态" min-width="80">
  474. <template slot-scope="scope">
  475. <el-tag v-if="scope.row.status === 'pending'" type="warning" size="mini">待审批</el-tag>
  476. <el-tag v-if="scope.row.status === 'approved'" type="success" size="mini">已通过</el-tag>
  477. <el-tag v-if="scope.row.status === 'rejected'" type="danger" size="mini">已驳回</el-tag>
  478. </template>
  479. </el-table-column>
  480. <el-table-column label="操作" min-width="140" fixed="right">
  481. <template slot-scope="scope">
  482. <template v-if="scope.row.status === 'pending'">
  483. <el-button type="success" size="mini" @click="approveItem(scope.row)">通过</el-button>
  484. <el-button type="danger" size="mini" @click="openRejectDialog(scope.row)">驳回</el-button>
  485. </template>
  486. <span v-if="scope.row.status === 'rejected'" class="reject-reason-text">{{ scope.row.rejectReason }}</span>
  487. </template>
  488. </el-table-column>
  489. </el-table>
  490. </section>
  491. </main>
  492. <!-- 系统配置页面 -->
  493. <main class="content" v-if="activePage === 'config'">
  494. <div class="page-toolbar">
  495. <h2 class="page-title">分级学时配置</h2>
  496. </div>
  497. <div class="config-card">
  498. <el-table :data="configTableData" border style="width: 100%">
  499. <el-table-column prop="levelLabel" label="风险等级" width="200"></el-table-column>
  500. <el-table-column prop="level" label="级别" width="100"></el-table-column>
  501. <el-table-column label="准入学时要求(第一年)">
  502. <template slot-scope="scope">
  503. <el-input-number v-model="scope.row.firstYear" :min="0" :max="100" size="small"></el-input-number>
  504. </template>
  505. </el-table-column>
  506. <el-table-column label="年度学时要求(第二年起)">
  507. <template slot-scope="scope">
  508. <el-input-number v-model="scope.row.annual" :min="0" :max="100" size="small"></el-input-number>
  509. </template>
  510. </el-table-column>
  511. </el-table>
  512. <div style="margin-top:16px;text-align:right">
  513. <el-button type="primary" @click="saveConfig">保存配置</el-button>
  514. </div>
  515. </div>
  516. </main>
  517. <!-- 其他页面占位 -->
  518. <main class="content" v-if="!['workbench','config','flow','prd','statistics','approval'].includes(activePage)">
  519. <div class="placeholder-page">
  520. <i class="el-icon-s-opportunity"></i>
  521. <p>{{ currentPageLabel }} - 功能开发中</p>
  522. </div>
  523. </main>
  524. <!-- 需求说明页面 -->
  525. <main class="content" v-if="activePage === 'prd'">
  526. <div class="page-toolbar">
  527. <h2 class="page-title">需求规格说明书</h2>
  528. <el-button size="small" icon="el-icon-back" @click="activePage = 'workbench'">返回工作台</el-button>
  529. </div>
  530. <div class="prd-page" v-html="prdContent"></div>
  531. </main>
  532. <!-- 业务流程说明页面 -->
  533. <main class="content" v-if="activePage === 'flow'">
  534. <div class="page-toolbar">
  535. <h2 class="page-title">业务流程说明</h2>
  536. <el-button size="small" icon="el-icon-back" @click="activePage = 'workbench'">返回工作台</el-button>
  537. </div>
  538. <div class="flow-page">
  539. <div class="flow-card" v-for="(flow, fi) in flowData" :key="fi">
  540. <h3 class="flow-title">{{ flow.role }}</h3>
  541. <div class="flow-chart">
  542. <div class="flow-step" v-for="(step, si) in flow.steps" :key="si">
  543. <div class="flow-node" :class="step.type || ''">
  544. <span class="flow-num">{{ si + 1 }}</span>
  545. <span class="flow-text">{{ step.text }}</span>
  546. </div>
  547. <div class="flow-arrow" v-if="si < flow.steps.length - 1">
  548. <i class="el-icon-arrow-down"></i>
  549. <span class="flow-condition" v-if="step.condition">{{ step.condition }}</span>
  550. </div>
  551. </div>
  552. </div>
  553. </div>
  554. </div>
  555. </main>
  556. </div>
  557. </div>
  558. <!-- 绑定实验室弹窗 -->
  559. <el-dialog title="绑定实验室" :visible.sync="showBindDialog" width="620px" :close-on-click-modal="false">
  560. <div class="bind-dialog-content">
  561. <!-- 单一可搜索下拉框 -->
  562. <el-select v-model="selectedLabId" placeholder="输入实验室名称、房号或楼栋关键词进行搜索" style="width:100%" filterable :filter-method="labFilterMethod" @change="onLabSelect">
  563. <el-option v-for="lab in filteredLabs" :key="lab.id"
  564. :label="lab.college + '-' + lab.name + '(' + lab.room + ')-' + lab.building"
  565. :value="lab.id">
  566. </el-option>
  567. </el-select>
  568. <p class="search-hint">此处请选择需要后续进入的科研实验室的名称,输入实验室名称、房号或楼栋关键词,再从检索结果中选择目标实验室。</p>
  569. <!-- 选中后展示完整信息 -->
  570. <div class="selected-lab-info" v-if="selectedLabDetail">
  571. <el-descriptions :column="2" size="small" border>
  572. <el-descriptions-item label="实验室名称">{{ selectedLabDetail.name }}({{ selectedLabDetail.room }})</el-descriptions-item>
  573. <el-descriptions-item label="所在位置">{{ selectedLabDetail.building }}</el-descriptions-item>
  574. <el-descriptions-item label="负责人">{{ selectedLabDetail.directorMasked }}</el-descriptions-item>
  575. <el-descriptions-item label="所属学院">{{ selectedLabDetail.college }}</el-descriptions-item>
  576. <el-descriptions-item label="实验室分级">
  577. <el-tag :type="levelTagType(selectedLabDetail.level)" size="mini">{{ selectedLabDetail.level }}级</el-tag>
  578. </el-descriptions-item>
  579. <el-descriptions-item label="实验室分类">{{ selectedLabDetail.category }}</el-descriptions-item>
  580. <el-descriptions-item label="实验室类型">{{ selectedLabDetail.type }}</el-descriptions-item>
  581. </el-descriptions>
  582. </div>
  583. <!-- 进入原因(本科生专属) -->
  584. <div v-if="bindDialogMode === 'undergraduate'" style="margin-top:16px">
  585. <el-input v-model="bindReason" type="textarea" placeholder="请填写进入实验室原因(必填)" :rows="2"></el-input>
  586. <p class="search-hint" style="margin-top:6px">毕业论文或毕业设计,科创等相关需要在具体的实验室开展业务的情况可申请绑定实验室,绑定完毕后需完成对应的分级考试。</p>
  587. </div>
  588. <!-- 无需进入实验室(研究生专属) -->
  589. <div v-if="bindDialogMode === 'graduate'" class="no-lab-option" style="margin-top:16px">
  590. <el-checkbox v-model="noLabMode">无需进入实验室</el-checkbox>
  591. <p class="search-hint" style="margin-top:4px">人文、体育等相关学院无需进入实验室的新研究生可选择此项,选择后学时要求为4小时,需通过应知应会考试。</p>
  592. </div>
  593. <!-- 须知复选框 -->
  594. <div class="bind-notice" style="margin-top:16px">
  595. <el-checkbox v-model="noticeChecked">我已阅读并同意<el-button type="text" @click="showNoticeDetail = true">《新生绑定实验室须知及责任说明》</el-button></el-checkbox>
  596. </div>
  597. </div>
  598. <span slot="footer">
  599. <el-button @click="showBindDialog = false">取消</el-button>
  600. <el-button type="primary" @click="confirmBind" :disabled="!canBind">确认绑定</el-button>
  601. </span>
  602. </el-dialog>
  603. <!-- 须知详情弹窗 -->
  604. <el-dialog title="新生绑定实验室须知及责任说明" :visible.sync="showNoticeDetail" width="520px" append-to-body>
  605. <div class="notice-content">
  606. <p>1. 保证进入的实验室为导师实际使用实验室或属于当前实验室所属课题组。</p>
  607. <p>2. 研究生期间涉及到操作实验室内重要危险源,必须绑定实验室。</p>
  608. </div>
  609. <span slot="footer">
  610. <el-button type="primary" @click="showNoticeDetail = false">我已知晓</el-button>
  611. </span>
  612. </el-dialog>
  613. <!-- 驳回原因弹窗 -->
  614. <el-dialog title="驳回申请" :visible.sync="showRejectDialog" width="450px">
  615. <p style="margin-bottom:12px;color:#60708a;font-size:13px">请填写驳回原因,将通知申请人:</p>
  616. <el-input v-model="rejectReasonInput" type="textarea" :rows="3" placeholder="请输入驳回原因(必填)"></el-input>
  617. <span slot="footer">
  618. <el-button @click="showRejectDialog = false">取消</el-button>
  619. <el-button type="danger" @click="confirmReject" :disabled="!rejectReasonInput.trim()">确认驳回</el-button>
  620. </span>
  621. </el-dialog>
  622. <!-- 毕业报告弹窗 -->
  623. <el-dialog title="毕业教育考试数据统计报告" :visible.sync="showGraduateReport" width="740px" top="3vh">
  624. <div class="report-wrapper" ref="reportContent" v-if="graduateReportData">
  625. <div class="report-certificate">
  626. <div class="report-logo">
  627. <img src="../assets/images/logo.png" alt="西北农林科技大学">
  628. </div>
  629. <h1 class="report-title">实验室安全教育毕业报告</h1>
  630. <div class="report-info-card">
  631. <div class="report-info-row">
  632. <span><b>姓名:</b>{{ graduateReportData.name }}</span>
  633. <span><b>学号:</b>{{ graduateReportData.code }}</span>
  634. </div>
  635. <div class="report-info-row">
  636. <span><b>学院:</b>{{ graduateReportData.org }}</span>
  637. <span><b>年级:</b>{{ graduateReportData.grade }}</span>
  638. </div>
  639. <div class="report-info-row">
  640. <span><b>用户类型:</b>{{ graduateReportData.type }}</span>
  641. </div>
  642. </div>
  643. <h3 class="report-section-title">学年学时完成情况</h3>
  644. <table class="report-table">
  645. <thead><tr><th>学年</th><th>完成学时</th><th>要求学时</th></tr></thead>
  646. <tbody>
  647. <tr v-for="h in graduateReportData.yearlyHours" :key="h.year">
  648. <td>{{ h.year }}</td><td>{{ h.completed }}</td><td>{{ h.required }}</td>
  649. </tr>
  650. </tbody>
  651. <tfoot><tr><td><b>累计</b></td><td colspan="2"><b>{{ graduateReportData.totalHours }} 学时</b></td></tr></tfoot>
  652. </table>
  653. <h3 class="report-section-title">考试通过记录</h3>
  654. <table class="report-table">
  655. <thead><tr><th>学年</th><th>考试类型</th><th>成绩</th></tr></thead>
  656. <tbody>
  657. <tr v-for="r in graduateReportData.examRecords" :key="r.year">
  658. <td>{{ r.year }}</td><td>{{ graduateReportData.examType }}</td><td>{{ r.score }}</td>
  659. </tr>
  660. </tbody>
  661. </table>
  662. <p class="report-comment">该同学在校期间认真完成实验室安全教育各项学习任务,累计完成 <b>{{ graduateReportData.totalHours }}</b> 学时安全培训,通过全部年度安全考核,具备良好的实验室安全意识和规范操作能力,特此证明。</p>
  663. <div class="report-footer">
  664. <div class="report-sign">
  665. <p>实验室安全与条件保障处</p>
  666. <p>2026年7月</p>
  667. </div>
  668. <div class="report-stamp">
  669. <span>实验室安全与<br>条件保障处</span>
  670. </div>
  671. </div>
  672. </div>
  673. </div>
  674. <span slot="footer">
  675. <el-button @click="showGraduateReport = false">关闭</el-button>
  676. <el-button type="primary" icon="el-icon-download" @click="downloadReport">下载PDF</el-button>
  677. </span>
  678. </el-dialog>
  679. </div>
  680. <!-- Vendor Scripts (CDN - 生产环境应本地化) -->
  681. <script src="https://unpkg.com/vue@2.7.16/dist/vue.min.js"></script>
  682. <script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
  683. <script src="./vendor/echarts.min.js"></script>
  684. <script src="https://unpkg.com/marked@9.1.6/marked.min.js"></script>
  685. <script src="https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
  686. <script src="https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
  687. <!-- App Scripts -->
  688. <script src="./js/data.js"></script>
  689. <script src="./js/app.js"></script>
  690. </body>
  691. </html>