dedsudiyu 2 days ago
parent
commit
3e08a6b20f

+ 432 - 0
src/views/safetyEducationExaminationNew/workbench/ExamSituation.vue

@@ -0,0 +1,432 @@
+<!-- 考试情况统计 -->
+<template>
+  <div class="exam-situation-card">
+    <div class="card-header">
+      <span class="card-title">考试情况统计</span>
+      <div class="header-right">
+        <el-select v-model="filterData.year" size="small" style="width:140px">
+          <el-option label="2025-2026 学年" value="2025-2026" />
+          <el-option label="2024-2025 学年" value="2024-2025" />
+        </el-select>
+        <!-- 分组按钮 -->
+        <div class="table-top-button-box">
+          <div>
+            <p
+              v-for="(btn, i) in filterData.typeBtns"
+              :key="btn.value"
+              :class="filterData.activeType === btn.value ? 'check-table-button' : ''"
+              @click="filterData.activeType = btn.value"
+            >{{ btn.label }}</p>
+          </div>
+        </div>
+        <!-- 导出按钮 -->
+        <p class="export-btn" @click="() => {}">
+          <i class="el-icon-download"></i> 导出数据
+        </p>
+      </div>
+    </div>
+
+    <!-- 图表区 -->
+    <div class="chart-area">
+      <!-- 堆叠柱状图 -->
+      <div class="bar-chart-wrap">
+        <div class="chart-legend">
+          <span class="leg-item"><i class="dot green"></i>通过</span>
+          <span class="leg-item"><i class="dot red"></i>未通过</span>
+          <span class="leg-item"><i class="dot gray"></i>未考</span>
+        </div>
+        <div class="bar-chart">
+          <div class="y-axis">
+            <span v-for="y in yLabels" :key="y">{{ y }}</span>
+          </div>
+          <div class="bars-container">
+            <div v-for="(g, i) in chartData.barGroups" :key="i" class="bar-group">
+              <div class="bar-stack">
+                <div class="seg gray"  :style="{height: scaleH(g.notExam) + 'px'}">
+                  <span v-if="g.notExam > 0" class="seg-label">{{ g.notExam }}</span>
+                </div>
+                <div class="seg red"   :style="{height: scaleH(g.fail) + 'px'}">
+                  <span v-if="g.fail > 0" class="seg-label">{{ g.fail }}</span>
+                </div>
+                <div class="seg green" :style="{height: scaleH(g.pass) + 'px'}">
+                  <span v-if="g.pass > 0" class="seg-label">{{ g.pass }}</span>
+                </div>
+              </div>
+              <div class="bar-label">{{ g.label }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 环形图 -->
+      <div class="pie-chart-wrap">
+        <div class="donut-wrap">
+          <svg viewBox="0 0 120 120" width="160" height="160">
+            <!-- 通过 大部分 ~85% -->
+            <circle cx="60" cy="60" r="45" fill="none" stroke="#67c23a"
+              stroke-width="22"
+              stroke-dasharray="238 282"
+              stroke-dashoffset="0"
+              transform="rotate(-90 60 60)" />
+            <!-- 未通过 ~7% -->
+            <circle cx="60" cy="60" r="45" fill="none" stroke="#f56c6c"
+              stroke-width="22"
+              stroke-dasharray="20 282"
+              stroke-dashoffset="-238"
+              transform="rotate(-90 60 60)" />
+            <!-- 未考 ~8% -->
+            <circle cx="60" cy="60" r="45" fill="none" stroke="#c0c4cc"
+              stroke-width="22"
+              stroke-dasharray="24 282"
+              stroke-dashoffset="-258"
+              transform="rotate(-90 60 60)" />
+            <circle cx="60" cy="60" r="28" fill="#fff" />
+          </svg>
+          <!-- 标注线 -->
+          <div class="pie-annotation pie-ann-top">未考</div>
+          <div class="pie-annotation pie-ann-right">未通过</div>
+          <div class="pie-annotation pie-ann-bottom">通过</div>
+        </div>
+        <div class="pie-legend">
+          <div class="p-item"><i class="dot green"></i><span>通过</span></div>
+          <div class="p-item"><i class="dot red"></i><span>未通过</span></div>
+          <div class="p-item"><i class="dot gray"></i><span>未考</span></div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 筛选 -->
+    <div class="filter-row">
+      <el-select v-model="filterData.college" placeholder="筛选学院" size="small" clearable style="width:130px">
+        <el-option v-for="c in filterData.colleges" :key="c" :label="c" :value="c" />
+      </el-select>
+      <el-select v-model="filterData.examType" placeholder="考试类型" size="small" clearable style="width:120px">
+        <el-option label="新生分级考试" value="新生分级考试" />
+        <el-option label="应知应会考试" value="应知应会考试" />
+      </el-select>
+    </div>
+
+    <!-- 统计表格 -->
+    <el-table :data="tableData" border size="small" :span-method="spanMethod">
+      <el-table-column prop="college" label="学院" min-width="130" />
+      <el-table-column prop="examType" label="考试类型" min-width="120" />
+      <el-table-column prop="total" label="应考人数" width="90" align="center" />
+      <el-table-column prop="pass" label="通过人数" width="90" align="center" />
+      <el-table-column prop="fail" label="未通过人数" width="100" align="center">
+        <template slot-scope="scope">
+          <span :class="scope.row.examType !== '汇总' ? 'link-blue' : ''">{{ scope.row.fail }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="notExam" label="未考人数" width="90" align="center" />
+      <el-table-column prop="rate" label="通过率" width="80" align="center" />
+    </el-table>
+  </div>
+</template>
+
+<script>
+const colleges = [
+  { name: '化学与化工学院', d1: [45, 38, 4, 3, '84%'], d2: [75, 68, 4, 3, '91%'], sum: [120, 106, 8, 6, '88%'] },
+  { name: '生命科学学院',   d1: [35, 30, 3, 2, '86%'], d2: [60, 54, 4, 2, '90%'], sum: [95,  84,  7, 4, '88%'] },
+  { name: '物理学院',       d1: [28, 23, 3, 2, '82%'], d2: [50, 44, 3, 3, '88%'], sum: [78,  67,  6, 5, '86%'] },
+  { name: '电气工程学院',   d1: [40, 35, 3, 2, '88%'], d2: [70, 63, 4, 3, '90%'], sum: [110, 98,  7, 5, '89%'] },
+  { name: '材料科学学院',   d1: [32, 28, 2, 2, '88%'], d2: [56, 50, 3, 3, '89%'], sum: [88,  78,  5, 5, '89%'] },
+  { name: '机械工程学院',   d1: [25, 21, 2, 2, '84%'], d2: [47, 42, 3, 2, '89%'], sum: [72,  63,  5, 4, '88%'] },
+  { name: '自动化学院',     d1: [22, 20, 1, 1, '91%'], d2: [43, 39, 2, 2, '91%'], sum: [65,  59,  3, 3, '91%'] },
+  { name: '资源环境学院',   d1: [20, 17, 2, 1, '85%'], d2: [36, 32, 2, 2, '89%'], sum: [56,  49,  4, 3, '88%'] },
+]
+
+function buildTableData() {
+  const rows = []
+  colleges.forEach(c => {
+    rows.push({ college: c.name, examType: '新生分级考试', total: c.d1[0], pass: c.d1[1], fail: c.d1[2], notExam: c.d1[3], rate: c.d1[4] })
+    rows.push({ college: c.name, examType: '应知应会考试', total: c.d2[0], pass: c.d2[1], fail: c.d2[2], notExam: c.d2[3], rate: c.d2[4] })
+    rows.push({ college: c.name, examType: '汇总',        total: c.sum[0], pass: c.sum[1], fail: c.sum[2], notExam: c.sum[3], rate: c.sum[4] })
+  })
+  return rows
+}
+
+export default {
+  name: 'ExamSituation',
+  data() {
+    return {
+      filterData: {
+        year: '2025-2026',
+        activeType: 'graduate',
+        typeBtns: [
+          { label: '研究生', value: 'graduate' },
+          { label: '本科生', value: 'undergrad' },
+          { label: '教职工', value: 'staff' },
+        ],
+        college: '',
+        examType: '',
+        colleges: colleges.map(c => c.name),
+      },
+      yLabels: [120, 100, 80, 60, 40, 20, 0],
+      chartData: {
+        barGroups: [
+          { label: '化学与化工', pass: 106, fail: 8, notExam: 6 },
+          { label: '生命科学',   pass: 84,  fail: 7, notExam: 4 },
+          { label: '物理',       pass: 67,  fail: 6, notExam: 5 },
+          { label: '电气工程',   pass: 98,  fail: 7, notExam: 5 },
+          { label: '材料科学',   pass: 78,  fail: 5, notExam: 5 },
+          { label: '机械工程',   pass: 63,  fail: 5, notExam: 4 },
+          { label: '自动化',     pass: 59,  fail: 3, notExam: 3 },
+          { label: '资源环境',   pass: 49,  fail: 4, notExam: 3 },
+        ]
+      },
+      tableData: buildTableData(),
+      spanMap: {}
+    }
+  },
+  created() {
+    this.buildSpanMap()
+  },
+  methods: {
+    scaleH(val) {
+      return Math.round(val / 120 * 180)
+    },
+    buildSpanMap() {
+      const map = {}
+      this.tableData.forEach((row, i) => {
+        if (!map[row.college]) {
+          // find count
+          let count = 0
+          for (let j = i; j < this.tableData.length; j++) {
+            if (this.tableData[j].college === row.college) count++
+            else break
+          }
+          map[i] = count
+        } else {
+          map[i] = 0
+        }
+      })
+      this.spanMap = map
+    },
+    spanMethod({ row, column, rowIndex, columnIndex }) {
+      if (columnIndex === 0) {
+        const span = this.spanMap[rowIndex]
+        if (span !== undefined) return { rowspan: span || 0, colspan: span ? 1 : 0 }
+      }
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.exam-situation-card {
+  background: #fff;
+  border-radius: 6px;
+  padding: 20px;
+  margin-bottom: 12px;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    .card-title { font-size: 16px; font-weight: 700; color: #222; }
+    .header-right { display: flex; align-items: center; gap: 20px; }
+  }
+
+  /* 分组筛选按钮 */
+  .table-top-button-box {
+    display: inline-block;
+    vertical-align: top;
+    div {
+      display: flex;
+      p {
+        border: 1px solid #DCDFE6;
+        width: 80px;
+        text-align: center;
+        height: 40px;
+        line-height: 40px;
+        cursor: pointer;
+        font-size: 14px;
+        &:nth-child(1) { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
+        &:nth-child(2) { border-left: none; border-right: none; }
+        &:nth-child(3) { border-top-right-radius: 4px; border-bottom-right-radius: 4px; }
+        &.check-table-button {
+          border: 1px solid #0183fa;
+          background-color: #0183fa;
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  /* 导出按钮 */
+  .export-btn {
+    display: inline-block;
+    padding: 0 16px;
+    height: 40px;
+    line-height: 40px;
+    border-radius: 4px;
+    font-size: 14px;
+    cursor: pointer;
+    background-color: #0183fa;
+    color: #fff;
+    border: 1px solid #0183fa;
+  }
+
+  .chart-area {
+    display: flex;
+    gap: 0;
+    margin-bottom: 16px;
+    border: 1px solid #eee;
+    border-radius: 6px;
+    overflow: hidden;
+
+    .bar-chart-wrap {
+      flex: 1;
+      padding: 16px;
+      border-right: 1px solid #eee;
+
+      .chart-legend {
+        display: flex;
+        gap: 20px;
+        margin-bottom: 10px;
+        font-size: 12px;
+        color: #555;
+        justify-content: center;
+        .leg-item { display: flex; align-items: center; gap: 4px; }
+      }
+
+      .bar-chart {
+        display: flex;
+        gap: 4px;
+        align-items: flex-end;
+        height: 220px;
+
+        .y-axis {
+          display: flex;
+          flex-direction: column;
+          justify-content: space-between;
+          font-size: 11px;
+          color: #aaa;
+          height: 200px;
+          min-width: 28px;
+          text-align: right;
+        }
+
+        .bars-container {
+          flex: 1;
+          display: flex;
+          align-items: flex-end;
+          justify-content: space-around;
+          height: 200px;
+          border-left: 1px solid #e8e8e8;
+          border-bottom: 1px solid #e8e8e8;
+          padding: 0 8px 20px;
+          position: relative;
+
+          // 横向网格线
+          &::before {
+            content: '';
+            position: absolute;
+            left: 0; right: 0; top: 0; bottom: 20px;
+            background: repeating-linear-gradient(to bottom, #f5f5f5 0, #f5f5f5 1px, transparent 1px, transparent calc(100% / 6));
+            pointer-events: none;
+          }
+
+          .bar-group {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            position: relative;
+            z-index: 1;
+
+            .bar-stack {
+              display: flex;
+              flex-direction: column;
+              width: 28px;
+              gap: 0;
+              position: relative;
+
+              .seg {
+                width: 100%;
+                position: relative;
+                &.green { background: #4fbe7e; }
+                &.red   { background: #f56c6c; }
+                &.gray  { background: #c0c4cc; }
+
+                .seg-label {
+                  position: absolute;
+                  top: -16px;
+                  left: 50%;
+                  transform: translateX(-50%);
+                  font-size: 10px;
+                  color: #333;
+                  white-space: nowrap;
+                }
+              }
+            }
+
+            .bar-label {
+              position: absolute;
+              bottom: -18px;
+              left: 50%;
+              transform: translateX(-50%);
+              font-size: 10px;
+              color: #555;
+              white-space: nowrap;
+            }
+          }
+        }
+      }
+    }
+
+    .pie-chart-wrap {
+      width: 300px;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      padding: 20px;
+      gap: 16px;
+
+      .donut-wrap {
+        position: relative;
+
+        .pie-annotation {
+          position: absolute;
+          font-size: 11px;
+          color: #555;
+          white-space: nowrap;
+
+          &.pie-ann-top    { top: 10px; left: 50%; transform: translateX(-50%); }
+          &.pie-ann-right  { top: 50%; right: -8px; transform: translateY(-50%); }
+          &.pie-ann-bottom { bottom: 10px; left: 50%; transform: translateX(-50%); color: #4fbe7e; }
+        }
+      }
+
+      .pie-legend {
+        display: flex;
+        gap: 16px;
+        font-size: 12px;
+        color: #555;
+        .p-item { display: flex; align-items: center; gap: 5px; }
+      }
+    }
+  }
+
+  .filter-row {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 12px;
+  }
+
+  .link-blue { color: #409eff; }
+
+  .dot {
+    display: inline-block;
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+    flex-shrink: 0;
+    &.green { background: #4fbe7e; }
+    &.red   { background: #f56c6c; }
+    &.gray  { background: #c0c4cc; }
+  }
+}
+</style>

+ 0 - 343
src/views/safetyEducationExaminationNew/workbench/ExamStatsCard.vue

@@ -1,343 +0,0 @@
-<!-- 安监室/实验室安全分布表 + 考试成绩统计 -->
-<template>
-  <div class="exam-stats-wrap">
-
-    <!-- 安监室安全分布表 -->
-    <div class="section-card">
-      <div class="section-header">
-        <span class="section-title">安监室安全分布表</span>
-        <div class="header-right">
-          <el-radio-group v-model="tableFilter.safetyPeriod" size="mini">
-            <el-radio-button label="年">年</el-radio-button>
-            <el-radio-button label="季度">季度</el-radio-button>
-            <el-radio-button label="月">月</el-radio-button>
-          </el-radio-group>
-          <el-button size="mini" type="primary" icon="el-icon-download">下载</el-button>
-        </div>
-      </div>
-      <el-table :data="tableData.safetyTable" border size="small" stripe>
-        <el-table-column prop="name" label="姓名" width="70" />
-        <el-table-column prop="dept" label="部门单位" min-width="100" show-overflow-tooltip />
-        <el-table-column prop="userId" label="用户编号" width="100" />
-        <el-table-column prop="eduTitle" label="安全教育名称" min-width="160" show-overflow-tooltip />
-        <el-table-column prop="subject" label="科目" width="60" />
-        <el-table-column prop="status" label="状态" width="80">
-          <template slot-scope="scope">
-            <el-tag :type="scope.row.status === '完成' ? 'success' : 'warning'" size="mini" effect="plain">{{ scope.row.status }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="person" label="负责人" width="70" />
-        <el-table-column prop="startDate" label="开始时间" width="140" />
-        <el-table-column prop="endDate" label="截止时间" width="140" />
-        <el-table-column prop="score" label="成绩" width="60" align="center" />
-        <el-table-column label="操作" width="170" fixed="right">
-          <template slot-scope="scope">
-            <el-button size="mini" type="primary" plain>查看</el-button>
-            <el-button size="mini" :type="scope.row.status === '完成' ? 'success' : 'warning'" plain>
-              {{ scope.row.status === '完成' ? '重考' : '考试' }}
-            </el-button>
-            <el-button size="mini" type="danger" plain>提醒</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-    </div>
-
-    <!-- 实验室安全分布表 -->
-    <div class="section-card">
-      <div class="section-header">
-        <span class="section-title">实验室安全分布表</span>
-        <div class="header-right">
-          <el-radio-group v-model="tableFilter.labPeriod" size="mini">
-            <el-radio-button label="年">年</el-radio-button>
-            <el-radio-button label="季度">季度</el-radio-button>
-            <el-radio-button label="月">月</el-radio-button>
-          </el-radio-group>
-          <el-button size="mini" type="primary" icon="el-icon-download">下载</el-button>
-        </div>
-      </div>
-      <el-table :data="tableData.labTable" border size="small" stripe>
-        <el-table-column prop="name" label="姓名" width="70" />
-        <el-table-column prop="dept" label="部门单位" min-width="100" show-overflow-tooltip />
-        <el-table-column prop="userId" label="用户编号" width="100" />
-        <el-table-column prop="eduTitle" label="安全教育名称" min-width="160" show-overflow-tooltip />
-        <el-table-column prop="subject" label="科目" width="60" />
-        <el-table-column prop="status" label="状态" width="80">
-          <template slot-scope="scope">
-            <el-tag :type="scope.row.status === '完成' ? 'success' : 'warning'" size="mini" effect="plain">{{ scope.row.status }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="person" label="负责人" width="70" />
-        <el-table-column prop="startDate" label="开始时间" width="140" />
-        <el-table-column prop="endDate" label="截止时间" width="140" />
-        <el-table-column prop="score" label="成绩" width="60" align="center" />
-        <el-table-column label="操作" width="170" fixed="right">
-          <template slot-scope="scope">
-            <el-button size="mini" type="primary" plain>查看</el-button>
-            <el-button size="mini" :type="scope.row.status === '完成' ? 'success' : 'warning'" plain>
-              {{ scope.row.status === '完成' ? '重考' : '考试' }}
-            </el-button>
-            <el-button size="mini" type="danger" plain>提醒</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-    </div>
-
-    <!-- 考试成绩统计 -->
-    <div class="section-card">
-      <div class="section-header">
-        <span class="section-title">考试成绩统计</span>
-        <div class="header-right">
-          <el-select v-model="tableFilter.statsYear" size="mini" style="width:150px">
-            <el-option label="2025-2026 学年" value="2025-2026" />
-            <el-option label="2024-2025 学年" value="2024-2025" />
-          </el-select>
-          <el-radio-group v-model="tableFilter.statsPeriod" size="mini">
-            <el-radio-button label="季度">季度</el-radio-button>
-            <el-radio-button label="月">月</el-radio-button>
-          </el-radio-group>
-          <el-button size="mini" type="primary" icon="el-icon-download">下载</el-button>
-        </div>
-      </div>
-
-      <div class="chart-row">
-        <!-- 柱状图 -->
-        <div class="bar-chart-area">
-          <div class="chart-legend">
-            <span class="legend-item"><i class="dot green"></i>优秀 ≥90</span>
-            <span class="legend-item"><i class="dot blue"></i>良好 60-89</span>
-            <span class="legend-item"><i class="dot red"></i>不及格 &lt;60</span>
-            <span class="legend-item"><i class="dot gray"></i>未考</span>
-          </div>
-          <div class="bar-chart">
-            <div class="y-labels">
-              <span v-for="y in [100, 80, 60, 40, 20, 0]" :key="y">{{ y }}</span>
-            </div>
-            <div class="bars-container">
-              <div v-for="(g, i) in chartData.barGroups" :key="i" class="bar-group">
-                <div class="bar-stack">
-                  <div class="seg green" :style="{height: g.excellent + 'px'}"></div>
-                  <div class="seg blue" :style="{height: g.good + 'px'}"></div>
-                  <div class="seg red" :style="{height: g.fail + 'px'}"></div>
-                </div>
-                <div class="bar-label">{{ g.label }}</div>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 饼图 -->
-        <div class="pie-chart-area">
-          <div class="pie-wrap">
-            <svg viewBox="0 0 100 100" width="130" height="130">
-              <circle cx="50" cy="50" r="35" fill="none" stroke="#67c23a" stroke-width="30" stroke-dasharray="132 220" stroke-dashoffset="0" transform="rotate(-90 50 50)" />
-              <circle cx="50" cy="50" r="35" fill="none" stroke="#409eff" stroke-width="30" stroke-dasharray="66 220" stroke-dashoffset="-132" transform="rotate(-90 50 50)" />
-              <circle cx="50" cy="50" r="35" fill="none" stroke="#f56c6c" stroke-width="30" stroke-dasharray="22 220" stroke-dashoffset="-198" transform="rotate(-90 50 50)" />
-              <circle cx="50" cy="50" r="20" fill="#fff" />
-            </svg>
-          </div>
-          <div class="pie-legend">
-            <div class="p-item"><i class="dot green"></i><span>优秀 ≥90</span></div>
-            <div class="p-item"><i class="dot blue"></i><span>良好 60-89</span></div>
-            <div class="p-item"><i class="dot red"></i><span>不及格 &lt;60</span></div>
-          </div>
-        </div>
-      </div>
-
-      <!-- 统计表格 -->
-      <el-table :data="chartData.statsTable" border size="small" stripe>
-        <el-table-column prop="category" label="分类" min-width="120" />
-        <el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
-        <el-table-column prop="total" label="参加人数" width="90" align="center" />
-        <el-table-column prop="excellent" label="优秀" width="70" align="center" />
-        <el-table-column prop="good" label="良好" width="70" align="center" />
-        <el-table-column prop="fail" label="不及格" width="80" align="center" />
-        <el-table-column prop="rate" label="通过率" width="80" align="center" />
-      </el-table>
-    </div>
-
-  </div>
-</template>
-
-<script>
-const mockTableRow = (n) => ({
-  name: '姓名',
-  dept: `部门单位名称${n}`,
-  userId: `2025000${String(n).padStart(2, '0')}`,
-  eduTitle: '高校宝安全教育培训与考核内容',
-  subject: '无',
-  status: n % 3 !== 0 ? '完成' : '待完成',
-  person: '负责人',
-  startDate: '2026-04-28 16:30',
-  endDate: '2026-05-28 16:30',
-  score: n % 3 !== 0 ? String(70 + n * 3) : '--'
-})
-
-export default {
-  name: 'ExamStatsCard',
-  data() {
-    return {
-      tableFilter: {
-        safetyPeriod: '年',
-        labPeriod: '年',
-        statsYear: '2025-2026',
-        statsPeriod: '季度',
-      },
-      tableData: {
-        safetyTable: Array.from({ length: 6 }, (_, i) => mockTableRow(i + 1)),
-        labTable: Array.from({ length: 6 }, (_, i) => mockTableRow(i + 1)),
-      },
-      chartData: {
-        barGroups: [
-          { label: '生物类', excellent: 60, good: 40, fail: 15 },
-          { label: '辐射类', excellent: 80, good: 30, fail: 10 },
-          { label: '化学', excellent: 50, good: 50, fail: 20 },
-          { label: '总览', excellent: 90, good: 35, fail: 8 },
-          { label: '科目', excellent: 40, good: 60, fail: 25 },
-          { label: '生物类', excellent: 70, good: 45, fail: 12 },
-          { label: '辐射类', excellent: 55, good: 55, fail: 18 },
-          { label: '化学', excellent: 65, good: 38, fail: 14 },
-        ],
-        statsTable: [
-          { category: '安全教育', name: '安全教育名称一', total: 25, excellent: 10, good: 12, fail: 3, rate: '88%' },
-          { category: '安全教育', name: '安全教育名称二', total: 30, excellent: 15, good: 12, fail: 3, rate: '90%' },
-          { category: '实验室安全', name: '实验室安全名称', total: 20, excellent: 8, good: 9, fail: 3, rate: '85%' },
-          { category: '化学实验', name: '化学实验名称', total: 18, excellent: 7, good: 8, fail: 3, rate: '83%' },
-          { category: '辐射安全', name: '辐射安全名称', total: 22, excellent: 9, good: 10, fail: 3, rate: '86%' },
-        ]
-      }
-    }
-  }
-}
-</script>
-
-<style scoped lang="scss">
-.exam-stats-wrap {
-  .section-card {
-    background: #fff;
-    border-radius: 6px;
-    padding: 16px 20px;
-    margin-bottom: 12px;
-    box-shadow: 0 1px 4px rgba(0,0,0,0.06);
-
-    .section-header {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      margin-bottom: 14px;
-
-      .section-title { font-size: 14px; font-weight: 600; color: #333; }
-      .header-right { display: flex; align-items: center; gap: 10px; }
-    }
-
-    .chart-row {
-      display: flex;
-      gap: 24px;
-      margin-bottom: 16px;
-      align-items: flex-start;
-
-      .bar-chart-area {
-        flex: 1;
-
-        .chart-legend {
-          display: flex;
-          gap: 16px;
-          margin-bottom: 10px;
-          font-size: 12px;
-          color: #555;
-          .legend-item { display: flex; align-items: center; gap: 4px; }
-        }
-
-        .bar-chart {
-          display: flex;
-          gap: 8px;
-          align-items: flex-end;
-          height: 160px;
-
-          .y-labels {
-            display: flex;
-            flex-direction: column;
-            justify-content: space-between;
-            font-size: 11px;
-            color: #aaa;
-            height: 100%;
-            padding-bottom: 20px;
-            text-align: right;
-            min-width: 28px;
-          }
-
-          .bars-container {
-            flex: 1;
-            display: flex;
-            align-items: flex-end;
-            justify-content: space-around;
-            height: 100%;
-            border-left: 1px solid #eee;
-            border-bottom: 1px solid #eee;
-            padding: 0 8px 20px;
-
-            .bar-group {
-              display: flex;
-              flex-direction: column;
-              align-items: center;
-              position: relative;
-
-              .bar-stack {
-                display: flex;
-                flex-direction: column-reverse;
-                width: 20px;
-                gap: 1px;
-
-                .seg {
-                  width: 100%;
-                  border-radius: 1px;
-                  &.green { background: #67c23a; }
-                  &.blue { background: #409eff; }
-                  &.red { background: #f56c6c; }
-                }
-              }
-
-              .bar-label {
-                font-size: 10px;
-                color: #666;
-                margin-top: 4px;
-                white-space: nowrap;
-              }
-            }
-          }
-        }
-      }
-
-      .pie-chart-area {
-        width: 220px;
-        display: flex;
-        align-items: center;
-        gap: 16px;
-        padding-top: 28px;
-
-        .pie-legend {
-          .p-item {
-            display: flex;
-            align-items: center;
-            gap: 6px;
-            font-size: 12px;
-            color: #555;
-            margin-bottom: 8px;
-          }
-        }
-      }
-    }
-  }
-
-  .dot {
-    display: inline-block;
-    width: 10px;
-    height: 10px;
-    border-radius: 50%;
-    flex-shrink: 0;
-    &.green { background: #67c23a; }
-    &.blue { background: #409eff; }
-    &.red { background: #f56c6c; }
-    &.gray { background: #c0c4cc; }
-  }
-}
-</style>

+ 222 - 0
src/views/safetyEducationExaminationNew/workbench/LabBindingApply.vue

@@ -0,0 +1,222 @@
+<!-- 实验室绑定申请 -->
+<template>
+  <div class="lab-binding-card">
+    <div class="card-header">
+      <span class="card-title">实验室绑定申请</span>
+      <div class="header-right">
+        <!-- 分组筛选按钮 -->
+        <div class="table-top-button-box">
+          <div>
+            <p
+              v-for="(btn, i) in filterData.statusBtns" :key="btn.value"
+              :class="filterData.activeStatus === btn.value ? 'check-table-button' : ''"
+              @click="filterData.activeStatus = btn.value"
+            >{{ btn.label }}</p>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <el-table :data="filteredData" border size="small" :row-class-name="rowClass">
+      <el-table-column prop="applicant" label="申请人" width="80" />
+      <el-table-column prop="studentId" label="学号" width="110" />
+      <el-table-column prop="userType" label="用户类型" width="100" />
+      <el-table-column prop="college" label="所属学院" min-width="120" show-overflow-tooltip />
+      <el-table-column prop="lab" label="申请实验室" min-width="160" show-overflow-tooltip />
+      <el-table-column prop="level" label="分级" width="70" align="center">
+        <template slot-scope="scope">
+          <span class="level-tag" :style="{background: getLevelBg(scope.row.level), color: getLevelColor(scope.row.level)}">
+            {{ scope.row.level }}
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="category" label="分类" width="80" />
+      <el-table-column prop="reason" label="进入原因" min-width="120" show-overflow-tooltip />
+      <el-table-column prop="submitTime" label="提交时间" width="140" />
+      <el-table-column prop="passTime" label="通过时间" width="140" />
+      <el-table-column prop="status" label="状态" width="80" align="center">
+        <template slot-scope="scope">
+          <span :class="'status-txt status-' + scope.row.statusCode">{{ scope.row.status }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" min-width="180" align="left">
+        <template slot-scope="scope">
+          <template v-if="scope.row.statusCode === 'reject'">
+            <span class="reject-msg">{{ scope.row.rejectMsg }}</span>
+          </template>
+          <template v-else-if="scope.row.statusCode === 'pass'">
+          </template>
+          <template v-else>
+            <p class="action-btn btn-pass" @click="() => {}">通过</p>
+            <p class="action-btn btn-reject" @click="() => {}">驳回</p>
+          </template>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'LabBindingApply',
+  data() {
+    return {
+      filterData: {
+        activeStatus: 'all',
+        statusBtns: [
+          { label: '全部',   value: 'all' },
+          { label: '待审批', value: 'pending' },
+          { label: '已通过', value: 'pass' },
+          { label: '已驳回', value: 'reject' },
+        ]
+      },
+      tableData: [
+        { applicant: '张明远', studentId: '2025100001', userType: '硕士研究生', college: '化学与化工学院', lab: '有机化学实验室(A301)', level: 'Ⅱ', category: '化学类', reason: '', submitTime: '2025-09-02 14:30', passTime: '2025-09-09 14:30', status: '待审批', statusCode: 'pending', rejectMsg: '' },
+        { applicant: '赵雨晴', studentId: '2025100023', userType: '硕士研究生', college: '电气工程学院', lab: '电气工程实验室(C102)', level: 'Ⅲ', category: '机电类', reason: '', submitTime: '2025-09-03 09:15', passTime: '2025-09-10 09:15', status: '待审批', statusCode: 'pending', rejectMsg: '' },
+        { applicant: '刘芳华', studentId: '2025100078', userType: '硕士研究生', college: '化学与化工学院', lab: '高分子化学实验室(A402)', level: 'Ⅱ', category: '化学类', reason: '', submitTime: '2025-09-01 16:45', passTime: '2025-09-08 16:45', status: '已通过', statusCode: 'pass', rejectMsg: '' },
+        { applicant: '王浩然', studentId: '2024100088', userType: '硕士研究生', college: '物理学院', lab: '辐射防护实验室(D401)', level: 'Ⅰ', category: '辐射类', reason: '', submitTime: '2025-08-30 10:20', passTime: '2025-09-06 10:20', status: '已通过', statusCode: 'pass', rejectMsg: '' },
+        { applicant: '陈志强', studentId: '2024100056', userType: '硕士研究生', college: '材料科学学院', lab: '材料力学实验室(E203)', level: 'Ⅳ', category: '机电类', reason: '', submitTime: '2025-09-04 11:00', passTime: '2025-09-11 11:00', status: '已驳回', statusCode: 'reject', rejectMsg: '该实验室已满员,请联系系统管理员更换实验室' },
+        { applicant: '李佳欣', studentId: '2025200312', userType: '本科生', college: '生命科学学院', lab: '分子生物学实验室(B205)', level: 'Ⅰ', category: '生物类', reason: '毕业论文需要进入该实验...', submitTime: '2025-09-05 08:30', passTime: '2025-09-12 08:30', status: '待审批', statusCode: 'pending', rejectMsg: '' },
+        { applicant: '周文博', studentId: '2025100034', userType: '硕士研究生', college: '自动化学院', lab: '智能控制实验室(C305)', level: 'Ⅲ', category: '机电类', reason: '', submitTime: '2025-09-05 15:10', passTime: '2025-09-12 15:10', status: '待审批', statusCode: 'pending', rejectMsg: '' },
+        { applicant: '孙晓明', studentId: '2023100091', userType: '硕士研究生', college: '生命科学学院', lab: '微生物学实验室(B308)', level: 'Ⅱ', category: '生物类', reason: '', submitTime: '2025-08-28 13:40', passTime: '2025-09-04 13:40', status: '已通过', statusCode: 'pass', rejectMsg: '' },
+      ]
+    }
+  },
+  computed: {
+    filteredData() {
+      if (this.filterData.activeStatus === 'all') return this.tableData
+      return this.tableData.filter(r => r.statusCode === this.filterData.activeStatus)
+    }
+  },
+  methods: {
+    getLevelBg(level) {
+      return { 'Ⅰ': '#fff0f0', 'Ⅱ': '#fff7e6', 'Ⅲ': '#e8f4ff', 'Ⅳ': '#eef0ff' }[level] || '#f5f5f5'
+    },
+    getLevelColor(level) {
+      return { 'Ⅰ': '#E86452', 'Ⅱ': '#F59A48', 'Ⅲ': '#0183fa', 'Ⅳ': '#6ea8f7' }[level] || '#666'
+    },
+    rowClass({ row }) {
+      return row.statusCode === 'reject' ? 'row-reject' : ''
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.lab-binding-card {
+  background: #fff;
+  border-radius: 6px;
+  padding: 20px;
+  margin-bottom: 12px;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+
+    .card-title {
+      font-size: 16px;
+      font-weight: 700;
+      color: #222;
+    }
+
+    .header-right {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+    }
+  }
+
+  /* 分组筛选按钮 — 与其他页面保持一致 */
+  .table-top-button-box {
+    display: inline-block;
+    vertical-align: top;
+
+    div {
+      display: flex;
+
+      p {
+        border: 1px solid #DCDFE6;
+        width: 80px;
+        text-align: center;
+        height: 40px;
+        line-height: 40px;
+        cursor: pointer;
+        font-size: 14px;
+
+        &:nth-child(1) {
+          border-top-left-radius: 4px;
+          border-bottom-left-radius: 4px;
+        }
+        &:nth-child(2) { border-left: none; }
+        &:nth-child(3) { border-left: none; border-right: none; }
+        &:nth-child(4) {
+          border-top-right-radius: 4px;
+          border-bottom-right-radius: 4px;
+        }
+
+        &.check-table-button {
+          border: 1px solid #0183fa;
+          background-color: #0183fa;
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  /* 表格内操作按钮 */
+  .action-btn {
+    display: inline-block;
+    padding: 0 14px;
+    height: 28px;
+    line-height: 28px;
+    border-radius: 4px;
+    font-size: 13px;
+    cursor: pointer;
+    margin-right: 6px;
+
+    &.btn-pass {
+      border: 1px solid #00B176;
+      color: #fff;
+      background-color: #00B176;
+    }
+
+    &.btn-reject {
+      border: 1px solid #E86452;
+      color: #fff;
+      background-color: #E86452;
+    }
+  }
+
+  .level-tag {
+    display: inline-block;
+    padding: 0 8px;
+    border-radius: 4px;
+    font-size: 13px;
+    line-height: 24px;
+  }
+
+  .status-txt {
+    font-size: 13px;
+    &.status-pending { color: #F59A48; }
+    &.status-pass    { color: #00B176; }
+    &.status-reject  { color: #E86452; }
+  }
+
+  .reject-msg {
+    font-size: 12px;
+    color: #E86452;
+    line-height: 1.5;
+  }
+
+  ::v-deep .row-reject td {
+    background: #fffafa !important;
+  }
+
+  ::v-deep .el-table td {
+    padding: 10px 0;
+  }
+}
+</style>

+ 211 - 0
src/views/safetyEducationExaminationNew/workbench/LabPersonMatch.vue

@@ -0,0 +1,211 @@
+<!-- 实验室与人员匹配 -->
+<template>
+  <div class="lab-person-card">
+    <div class="card-header">
+      <span class="card-title">实验室与人员匹配</span>
+      <div class="header-right">
+        <p class="export-btn export-btn--blue" @click="() => {}">
+          <i class="el-icon-download"></i> 导出各学院数据
+        </p>
+        <p class="export-btn export-btn--green" @click="() => {}">
+          <i class="el-icon-download"></i> 导出实验室人员明细
+        </p>
+      </div>
+    </div>
+
+    <!-- 柱状图 -->
+    <div class="chart-wrap">
+      <div class="bar-chart">
+        <div class="y-axis">
+          <span v-for="y in yLabels" :key="y">{{ y }}</span>
+        </div>
+        <div class="bars-container">
+          <div v-for="(g, i) in chartData" :key="i" class="bar-group">
+            <div class="bar-value">{{ g.value }}</div>
+            <div class="bar-body" :style="{height: scaleH(g.value) + 'px', background: g.color}"></div>
+            <div class="bar-label">{{ g.label }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 筛选 -->
+    <div class="filter-row">
+      <el-select v-model="filterData.college" placeholder="筛选学院" size="small" clearable style="width:130px">
+        <el-option v-for="c in filterData.colleges" :key="c" :label="c" :value="c" />
+      </el-select>
+      <el-select v-model="filterData.level" placeholder="实验室等级" size="small" clearable style="width:120px">
+        <el-option label="Ⅰ级" value="1" />
+        <el-option label="Ⅱ级" value="2" />
+        <el-option label="Ⅲ级" value="3" />
+        <el-option label="Ⅳ级" value="4" />
+      </el-select>
+    </div>
+
+    <!-- 表格 -->
+    <el-table :data="tableData" border size="small">
+      <el-table-column prop="college" label="学院" min-width="140" />
+      <el-table-column prop="l1" label="Ⅰ级人数" width="100" align="center" />
+      <el-table-column prop="l2" label="Ⅱ级人数" width="100" align="center" />
+      <el-table-column prop="l3" label="Ⅲ级人数" width="100" align="center" />
+      <el-table-column prop="l4" label="Ⅳ级人数" width="100" align="center" />
+      <el-table-column prop="total" label="总人数" width="100" align="center">
+        <template slot-scope="scope">
+          <span class="total-link">{{ scope.row.total }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'LabPersonMatch',
+  data() {
+    return {
+      filterData: {
+        college: '',
+        level: '',
+        colleges: ['化学与化工学院','生命科学学院','物理学院','电气工程学院','材料科学学院','机械工程学院','自动化学院','资源环境学院'],
+      },
+      yLabels: [600, 500, 400, 300, 200, 100, 0],
+      chartData: [
+        { label: 'Ⅰ级(重大风险)', value: 156, color: '#f56c6c' },
+        { label: 'Ⅱ级(高风险)',   value: 342, color: '#f59e42' },
+        { label: 'Ⅲ级(中风险)',   value: 528, color: '#f59e42' },
+        { label: 'Ⅳ级(低风险)',   value: 215, color: '#6ea8f7' },
+      ],
+      tableData: [
+        { college: '化学与化工学院', l1: 18, l2: 45, l3: 62, l4: 25, total: 150 },
+        { college: '生命科学学院',   l1: 32, l2: 38, l3: 48, l4: 20, total: 138 },
+        { college: '物理学院',       l1: 25, l2: 30, l3: 42, l4: 18, total: 115 },
+        { college: '电气工程学院',   l1: 15, l2: 42, l3: 58, l4: 30, total: 145 },
+        { college: '材料科学学院',   l1: 20, l2: 35, l3: 50, l4: 22, total: 127 },
+        { college: '机械工程学院',   l1: 12, l2: 38, l3: 55, l4: 28, total: 133 },
+        { college: '自动化学院',     l1: 18, l2: 32, l3: 45, l4: 20, total: 115 },
+        { college: '资源环境学院',   l1: 16, l2: 28, l3: 40, l4: 18, total: 102 },
+      ]
+    }
+  },
+  methods: {
+    scaleH(val) {
+      return Math.round(val / 600 * 240)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.lab-person-card {
+  background: #fff;
+  border-radius: 6px;
+  padding: 20px;
+  margin-bottom: 12px;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    .card-title { font-size: 16px; font-weight: 700; color: #222; }
+    .header-right { display: flex; gap: 20px; }
+  }
+
+  .export-btn {
+    display: inline-block;
+    padding: 0 16px;
+    height: 40px;
+    line-height: 40px;
+    border-radius: 4px;
+    font-size: 14px;
+    cursor: pointer;
+    color: #fff;
+
+    &--blue  { background-color: #0183fa; border: 1px solid #0183fa; }
+    &--green { background-color: #00B176; border: 1px solid #00B176; }
+  }
+
+  .chart-wrap {
+    border: 1px solid #eee;
+    border-radius: 6px;
+    padding: 20px 20px 30px;
+    margin-bottom: 16px;
+
+    .bar-chart {
+      display: flex;
+      gap: 8px;
+      align-items: flex-end;
+      height: 280px;
+
+      .y-axis {
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        font-size: 11px;
+        color: #aaa;
+        height: 260px;
+        min-width: 32px;
+        text-align: right;
+      }
+
+      .bars-container {
+        flex: 1;
+        display: flex;
+        align-items: flex-end;
+        justify-content: space-around;
+        height: 260px;
+        border-bottom: 1px solid #e8e8e8;
+        padding: 0 40px 24px;
+        position: relative;
+
+        &::before {
+          content: '';
+          position: absolute;
+          left: 0; right: 0; top: 0; bottom: 24px;
+          background: repeating-linear-gradient(to bottom, #f5f5f5 0, #f5f5f5 1px, transparent 1px, transparent calc(100% / 6));
+          pointer-events: none;
+        }
+
+        .bar-group {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          position: relative;
+          z-index: 1;
+
+          .bar-value {
+            font-size: 12px;
+            color: #333;
+            font-weight: 600;
+            margin-bottom: 4px;
+          }
+
+          .bar-body {
+            width: 60px;
+            border-radius: 2px 2px 0 0;
+          }
+
+          .bar-label {
+            position: absolute;
+            bottom: -20px;
+            left: 50%;
+            transform: translateX(-50%);
+            font-size: 11px;
+            color: #555;
+            white-space: nowrap;
+          }
+        }
+      }
+    }
+  }
+
+  .filter-row {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 12px;
+  }
+
+  .total-link { color: #409eff; }
+}
+</style>

+ 0 - 182
src/views/safetyEducationExaminationNew/workbench/SafetyPersonChart.vue

@@ -1,182 +0,0 @@
-<!-- 实验室管与人员图 -->
-<template>
-  <div class="safety-person-card">
-    <div class="section-header">
-      <span class="section-title">实验室管与人员图</span>
-      <div class="header-right">
-        <el-button size="mini" type="primary">实验室管理下载</el-button>
-        <el-button size="mini" type="success">实验室人员下载</el-button>
-      </div>
-    </div>
-
-    <!-- 柱状图 -->
-    <div class="chart-area">
-      <div class="chart-legend">
-        <span class="legend-item"><i class="dot orange"></i>实验室管理</span>
-        <span class="legend-item"><i class="dot blue"></i>实验室人员</span>
-      </div>
-      <div class="bar-chart">
-        <div class="y-axis">
-          <span v-for="y in [500, 400, 300, 200, 100, 0]" :key="y">{{ y }}</span>
-        </div>
-        <div class="bars-wrap">
-          <div v-for="(g, i) in chartData.groups" :key="i" class="bar-group">
-            <div class="bars-pair">
-              <div class="bar orange" :style="{height: (g.v1 / 500 * 160) + 'px'}" :title="g.v1"></div>
-              <div class="bar blue" :style="{height: (g.v2 / 500 * 160) + 'px'}" :title="g.v2"></div>
-            </div>
-            <div class="group-label">{{ g.label }}</div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 汇总表格 -->
-    <el-table :data="chartData.tableData" border size="small" stripe>
-      <el-table-column prop="category" label="分类" min-width="140" show-overflow-tooltip />
-      <el-table-column prop="y1" label="第1学年" width="80" align="center" />
-      <el-table-column prop="y2" label="第2学年" width="80" align="center" />
-      <el-table-column prop="y3" label="第3学年" width="80" align="center" />
-      <el-table-column prop="y4" label="第4学年" width="80" align="center" />
-      <el-table-column prop="y5" label="第5学年" width="80" align="center" />
-      <el-table-column prop="y6" label="第6学年" width="80" align="center" />
-      <el-table-column prop="sum" label="合计" width="80" align="center">
-        <template slot-scope="scope">
-          <b>{{ scope.row.sum }}</b>
-        </template>
-      </el-table-column>
-    </el-table>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'SafetyPersonChart',
-  data() {
-    return {
-      chartData: {
-        groups: [
-          { label: '博(一年级)', v1: 60, v2: 100 },
-          { label: '博(二年级)', v1: 80, v2: 120 },
-          { label: '硕(一年级)', v1: 250, v2: 320 },
-          { label: '硕(二年级)', v1: 200, v2: 280 },
-          { label: '硕(三年级)', v1: 40, v2: 60 },
-        ],
-        tableData: [
-          { category: '化学与化工学院', y1: 10, y2: 20, y3: 30, y4: 0, y5: 0, y6: 0, sum: 60 },
-          { category: '生命科学与技术学院', y1: 15, y2: 25, y3: 20, y4: 10, y5: 0, y6: 0, sum: 70 },
-          { category: '环境科学与工程学院', y1: 8, y2: 12, y3: 15, y4: 5, y5: 0, y6: 0, sum: 40 },
-          { category: '电气/工程学院', y1: 20, y2: 30, y3: 25, y4: 15, y5: 5, y6: 0, sum: 95 },
-          { category: '物理学院', y1: 12, y2: 18, y3: 20, y4: 8, y5: 2, y6: 0, sum: 60 },
-          { category: '材料科学与工程学院', y1: 10, y2: 15, y3: 18, y4: 7, y5: 0, y6: 0, sum: 50 },
-        ]
-      }
-    }
-  }
-}
-</script>
-
-<style scoped lang="scss">
-.safety-person-card {
-  background: #fff;
-  border-radius: 6px;
-  padding: 16px 20px;
-  margin-bottom: 12px;
-  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
-
-  .section-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 14px;
-    .section-title { font-size: 14px; font-weight: 600; color: #333; }
-    .header-right { display: flex; gap: 10px; }
-  }
-
-  .chart-area {
-    background: #fafafa;
-    border: 1px solid #f0f0f0;
-    border-radius: 4px;
-    padding: 16px;
-    margin-bottom: 16px;
-
-    .chart-legend {
-      display: flex;
-      gap: 20px;
-      margin-bottom: 12px;
-      font-size: 12px;
-      color: #555;
-      .legend-item { display: flex; align-items: center; gap: 5px; }
-    }
-
-    .bar-chart {
-      display: flex;
-      gap: 8px;
-      align-items: flex-end;
-
-      .y-axis {
-        display: flex;
-        flex-direction: column;
-        justify-content: space-between;
-        font-size: 11px;
-        color: #aaa;
-        height: 180px;
-        min-width: 32px;
-        text-align: right;
-        padding-bottom: 22px;
-      }
-
-      .bars-wrap {
-        flex: 1;
-        display: flex;
-        align-items: flex-end;
-        justify-content: space-around;
-        height: 180px;
-        border-left: 1px solid #e8e8e8;
-        border-bottom: 1px solid #e8e8e8;
-        padding: 0 16px 22px;
-
-        .bar-group {
-          display: flex;
-          flex-direction: column;
-          align-items: center;
-          position: relative;
-
-          .bars-pair {
-            display: flex;
-            gap: 3px;
-            align-items: flex-end;
-
-            .bar {
-              width: 16px;
-              border-radius: 2px 2px 0 0;
-              &.orange { background: #f59e42; }
-              &.blue { background: #4e9cf6; }
-            }
-          }
-
-          .group-label {
-            position: absolute;
-            bottom: -18px;
-            left: 50%;
-            transform: translateX(-50%);
-            font-size: 10px;
-            color: #666;
-            white-space: nowrap;
-          }
-        }
-      }
-    }
-  }
-
-  .dot {
-    display: inline-block;
-    width: 10px;
-    height: 10px;
-    border-radius: 2px;
-    flex-shrink: 0;
-    &.orange { background: #f59e42; }
-    &.blue { background: #4e9cf6; }
-  }
-}
-</style>

+ 0 - 216
src/views/safetyEducationExaminationNew/workbench/StudentStats.vue

@@ -1,216 +0,0 @@
-<!-- 学期统计 -->
-<template>
-  <div class="student-stats-card">
-    <div class="section-header">
-      <span class="section-title">学期统计</span>
-      <el-button size="mini" type="primary" icon="el-icon-download">下载统计</el-button>
-    </div>
-
-    <!-- 柱状图 -->
-    <div class="chart-area">
-      <div class="chart-legend">
-        <span class="legend-item"><i class="dot green"></i>完成</span>
-        <span class="legend-item"><i class="dot blue"></i>进行中</span>
-        <span class="legend-item"><i class="dot yellow"></i>未开始</span>
-        <span class="legend-item"><i class="dot gray"></i>其他</span>
-      </div>
-      <div class="bar-chart">
-        <div class="y-axis">
-          <span v-for="y in [100, 80, 60, 40, 20, 0]" :key="y">{{ y }}</span>
-        </div>
-        <div class="bars-container">
-          <div v-for="(col, i) in statsData.chartCols" :key="i" class="chart-col">
-            <div class="stacked-bar">
-              <div class="seg gray"   :style="{height: col.other * 1.2 + 'px'}"></div>
-              <div class="seg yellow" :style="{height: col.notStart * 1.2 + 'px'}"></div>
-              <div class="seg blue"   :style="{height: col.pending * 1.2 + 'px'}"></div>
-              <div class="seg green"  :style="{height: col.done * 1.2 + 'px'}"></div>
-            </div>
-            <div class="col-label">{{ col.label }}</div>
-          </div>
-        </div>
-      </div>
-    </div>
-
-    <!-- 统计表格 -->
-    <el-table :data="statsData.tableData" border size="small" stripe>
-      <el-table-column prop="name" label="姓名" width="70" />
-      <el-table-column prop="userId" label="用户编号" width="100" />
-      <el-table-column prop="dept" label="部门" min-width="100" show-overflow-tooltip />
-      <el-table-column prop="eduName" label="安全教育名称" min-width="150" show-overflow-tooltip />
-      <el-table-column prop="term1" label="第1学期" width="80" align="center" />
-      <el-table-column prop="term2" label="第2学期" width="80" align="center" />
-      <el-table-column prop="term3" label="第3学期" width="80" align="center" />
-      <el-table-column prop="term4" label="第4学期" width="80" align="center" />
-      <el-table-column prop="total" label="总计" width="80" align="center">
-        <template slot-scope="scope">
-          <b>{{ scope.row.total }}</b>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <!-- 分页 -->
-    <div class="pagination-wrap">
-      <el-pagination
-        :current-page.sync="statsData.page"
-        :page-size="10"
-        :total="statsData.total"
-        layout="prev, pager, next"
-        background
-        small
-      />
-    </div>
-  </div>
-</template>
-
-<script>
-const mockRow = (n) => ({
-  name: '姓名',
-  userId: `2025000${String(n).padStart(2, '0')}`,
-  dept: '部门单位',
-  eduName: '安全教育名称',
-  term1: Math.floor(Math.random() * 15) + 5,
-  term2: Math.floor(Math.random() * 15) + 5,
-  term3: Math.floor(Math.random() * 10),
-  term4: Math.floor(Math.random() * 10),
-  get total() { return this.term1 + this.term2 + this.term3 + this.term4 }
-})
-
-export default {
-  name: 'StudentStats',
-  data() {
-    return {
-      statsData: {
-        page: 1,
-        total: 50,
-        chartCols: [
-          { label: '总览', done: 60, pending: 20, notStart: 10, other: 5 },
-          { label: '生物类', done: 50, pending: 30, notStart: 12, other: 3 },
-          { label: '辐射类', done: 70, pending: 15, notStart: 8, other: 2 },
-          { label: '化学', done: 40, pending: 35, notStart: 15, other: 5 },
-          { label: '科目类', done: 55, pending: 25, notStart: 10, other: 4 },
-          { label: '电气类', done: 65, pending: 20, notStart: 8, other: 3 },
-          { label: '物理类', done: 45, pending: 30, notStart: 14, other: 6 },
-          { label: '材料类', done: 75, pending: 12, notStart: 6, other: 2 },
-        ],
-        tableData: Array.from({ length: 8 }, (_, i) => mockRow(i + 1))
-      }
-    }
-  }
-}
-</script>
-
-<style scoped lang="scss">
-.student-stats-card {
-  background: #fff;
-  border-radius: 6px;
-  padding: 16px 20px;
-  margin-bottom: 12px;
-  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
-
-  .section-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 14px;
-    .section-title { font-size: 14px; font-weight: 600; color: #333; }
-  }
-
-  .chart-area {
-    background: #fafafa;
-    border: 1px solid #f0f0f0;
-    border-radius: 4px;
-    padding: 16px;
-    margin-bottom: 16px;
-
-    .chart-legend {
-      display: flex;
-      gap: 20px;
-      margin-bottom: 10px;
-      font-size: 12px;
-      color: #555;
-      .legend-item { display: flex; align-items: center; gap: 4px; }
-    }
-
-    .bar-chart {
-      display: flex;
-      gap: 8px;
-      align-items: flex-end;
-
-      .y-axis {
-        display: flex;
-        flex-direction: column;
-        justify-content: space-between;
-        font-size: 11px;
-        color: #aaa;
-        height: 160px;
-        min-width: 28px;
-        text-align: right;
-        padding-bottom: 22px;
-      }
-
-      .bars-container {
-        flex: 1;
-        display: flex;
-        align-items: flex-end;
-        justify-content: space-around;
-        height: 160px;
-        border-left: 1px solid #e8e8e8;
-        border-bottom: 1px solid #e8e8e8;
-        padding: 0 8px 22px;
-
-        .chart-col {
-          display: flex;
-          flex-direction: column;
-          align-items: center;
-          position: relative;
-
-          .stacked-bar {
-            display: flex;
-            flex-direction: column;
-            width: 22px;
-            gap: 1px;
-
-            .seg {
-              width: 100%;
-              border-radius: 1px;
-              &.green  { background: #67c23a; }
-              &.blue   { background: #409eff; }
-              &.yellow { background: #e6a23c; }
-              &.gray   { background: #c0c4cc; }
-            }
-          }
-
-          .col-label {
-            position: absolute;
-            bottom: -18px;
-            left: 50%;
-            transform: translateX(-50%);
-            font-size: 10px;
-            color: #666;
-            white-space: nowrap;
-          }
-        }
-      }
-    }
-  }
-
-  .pagination-wrap {
-    display: flex;
-    justify-content: center;
-    margin-top: 14px;
-  }
-
-  .dot {
-    display: inline-block;
-    width: 10px;
-    height: 10px;
-    border-radius: 2px;
-    flex-shrink: 0;
-    &.green  { background: #67c23a; }
-    &.blue   { background: #409eff; }
-    &.yellow { background: #e6a23c; }
-    &.gray   { background: #c0c4cc; }
-  }
-}
-</style>

+ 308 - 0
src/views/safetyEducationExaminationNew/workbench/StudyHourStats.vue

@@ -0,0 +1,308 @@
+<!-- 学时统计 -->
+<template>
+  <div class="study-hour-card">
+    <div class="card-header">
+      <span class="card-title">学时统计</span>
+      <p class="export-btn" @click="() => {}">
+        <i class="el-icon-download"></i> 导出数据
+      </p>
+    </div>
+
+    <!-- 分组柱状图 -->
+    <div class="chart-wrap">
+      <div class="chart-legend">
+        <span class="leg-item"><i class="dot blue"></i>研究生</span>
+        <span class="leg-item"><i class="dot green"></i>本科生</span>
+        <span class="leg-item"><i class="dot orange"></i>教职工</span>
+      </div>
+      <div class="bar-chart">
+        <div class="y-axis-label">学时</div>
+        <div class="y-axis">
+          <span v-for="y in yLabels" :key="y">{{ y }}</span>
+        </div>
+        <div class="bars-container">
+          <div v-for="(g, i) in chartData" :key="i" class="bar-group">
+            <div class="bars-triple">
+              <div class="single-bar">
+                <div class="bar-val">{{ g.graduate }}</div>
+                <div class="bar blue" :style="{height: scaleH(g.graduate) + 'px'}"></div>
+              </div>
+              <div class="single-bar">
+                <div class="bar-val">{{ g.undergrad }}</div>
+                <div class="bar green" :style="{height: scaleH(g.undergrad) + 'px'}"></div>
+              </div>
+              <div class="single-bar">
+                <div class="bar-val">{{ g.staff }}</div>
+                <div class="bar orange" :style="{height: scaleH(g.staff) + 'px'}"></div>
+              </div>
+            </div>
+            <div class="group-label">{{ g.label }}</div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 筛选 -->
+    <div class="filter-row">
+      <el-select v-model="filterData.userType" placeholder="用户类型" size="small" clearable style="width:110px">
+        <el-option label="研究生" value="graduate" />
+        <el-option label="本科生" value="undergrad" />
+        <el-option label="教职工" value="staff" />
+      </el-select>
+      <el-select v-model="filterData.college" placeholder="学院" size="small" clearable style="width:130px">
+        <el-option v-for="c in filterData.colleges" :key="c" :label="c" :value="c" />
+      </el-select>
+      <el-select v-model="filterData.grade" placeholder="年级" size="small" clearable style="width:100px">
+        <el-option v-for="g in filterData.grades" :key="g" :label="g" :value="g" />
+      </el-select>
+      <el-input v-model="filterData.keyword" placeholder="学号/姓名" size="small" prefix-icon="el-icon-search" style="width:130px" clearable />
+    </div>
+
+    <!-- 表格 -->
+    <el-table :data="tableData" border size="small">
+      <el-table-column prop="name" label="姓名" width="80" />
+      <el-table-column prop="studentId" label="学号" width="120" />
+      <el-table-column prop="grade" label="年级" width="80" />
+      <el-table-column prop="college" label="所属学院" min-width="140" show-overflow-tooltip />
+      <el-table-column prop="y1" label="第一年" width="80" align="center" />
+      <el-table-column prop="y2" label="第二年" width="80" align="center">
+        <template slot-scope="scope">
+          <span :class="scope.row.y2IsBlue ? 'link-blue' : ''">{{ scope.row.y2 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="y3" label="第三年" width="80" align="center">
+        <template slot-scope="scope">
+          <span :class="scope.row.y3IsBlue ? 'link-blue' : ''">{{ scope.row.y3 }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="y4" label="第四年" width="80" align="center" />
+      <el-table-column prop="total" label="总时长" width="80" align="center" />
+    </el-table>
+
+    <!-- 分页 -->
+    <div class="pagination-wrap">
+      <el-pagination
+        :current-page.sync="filterData.page"
+        :page-size="10"
+        :total="50"
+        layout="prev, pager, next"
+        background
+        small
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'StudyHourStats',
+  data() {
+    return {
+      filterData: {
+        userType: '',
+        college: '',
+        grade: '',
+        keyword: '',
+        page: 1,
+        colleges: ['化学与化工学院','生命科学学院','物理学院','电气工程学院','材料科学学院','机械工程学院','自动化学院','资源环境学院'],
+        grades: ['2017级','2018级','2019级','2020级','2021级','2022级'],
+      },
+      yLabels: [350, 300, 250, 200, 150, 100, 50, 0],
+      chartData: [
+        { label: '化学与化工', graduate: 320, undergrad: 180, staff: 95 },
+        { label: '生命科学',   graduate: 280, undergrad: 160, staff: 72 },
+        { label: '物理',       graduate: 240, undergrad: 145, staff: 68 },
+        { label: '电气工程',   graduate: 300, undergrad: 195, staff: 85 },
+        { label: '材料科学',   graduate: 220, undergrad: 135, staff: 60 },
+        { label: '机械工程',   graduate: 195, undergrad: 170, staff: 55 },
+        { label: '自动化',     graduate: 180, undergrad: 120, staff: 48 },
+        { label: '资源环境',   graduate: 165, undergrad: 110, staff: 52 },
+      ],
+      tableData: [
+        { name: '赵丽华', studentId: 'T2018056',   grade: '2018级', college: '化学与化工学院', y1: 4,   y2: 2.5, y2IsBlue: false, y3: 1.8, y3IsBlue: true,  y4: 0,   total: 8.3 },
+        { name: '刘佳琪', studentId: '2022200089',  grade: '2022级', college: '材料科学学院',   y1: 4,   y2: 1.2, y2IsBlue: false, y3: 0.8, y3IsBlue: false, y4: 0,   total: 6 },
+        { name: '王建军', studentId: 'T2020012',   grade: '2020级', college: '物理学院',       y1: 4,   y2: 1.5, y2IsBlue: false, y3: 0,   y3IsBlue: false, y4: 0,   total: 5.5 },
+        { name: '陈小红', studentId: '2021200134',  grade: '2021级', college: '生命科学学院',   y1: 4,   y2: 1.8, y2IsBlue: false, y3: 1.5, y3IsBlue: true,  y4: 0.8, total: 8.1 },
+        { name: '张伟',   studentId: 'T2019033',   grade: '2019级', college: '电气工程学院',   y1: 4,   y2: 2,   y2IsBlue: false, y3: 1.2, y3IsBlue: true,  y4: 0,   total: 7.2 },
+        { name: '李明',   studentId: '2022200201',  grade: '2022级', college: '机械工程学院',   y1: 4,   y2: 0.8, y2IsBlue: false, y3: 0,   y3IsBlue: false, y4: 0,   total: 4.8 },
+        { name: '周芳',   studentId: 'T2017045',   grade: '2017级', college: '化学与化工学院', y1: 4,   y2: 2.2, y2IsBlue: false, y3: 1.8, y3IsBlue: false, y4: 1.5, total: 9.5 },
+        { name: '吴强',   studentId: '2021200089',  grade: '2021级', college: '自动化学院',     y1: 4,   y2: 1.5, y2IsBlue: false, y3: 1.2, y3IsBlue: true,  y4: 0.5, total: 7.2 },
+      ]
+    }
+  },
+  methods: {
+    scaleH(val) {
+      return Math.round(val / 350 * 200)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.study-hour-card {
+  background: #fff;
+  border-radius: 6px;
+  padding: 16px 20px;
+  margin-bottom: 12px;
+  box-shadow: 0 1px 4px rgba(0,0,0,0.06);
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    .card-title { font-size: 16px; font-weight: 700; color: #222; }
+  }
+
+  .export-btn {
+    display: inline-block;
+    padding: 0 16px;
+    height: 40px;
+    line-height: 40px;
+    border-radius: 4px;
+    font-size: 14px;
+    cursor: pointer;
+    background-color: #0183fa;
+    color: #fff;
+    border: 1px solid #0183fa;
+  }
+
+  .chart-wrap {
+    border: 1px solid #eee;
+    border-radius: 6px;
+    padding: 16px 20px 30px;
+    margin-bottom: 16px;
+
+    .chart-legend {
+      display: flex;
+      gap: 20px;
+      margin-bottom: 8px;
+      font-size: 12px;
+      color: #555;
+      justify-content: center;
+      .leg-item { display: flex; align-items: center; gap: 4px; }
+    }
+
+    .bar-chart {
+      display: flex;
+      gap: 4px;
+      align-items: flex-end;
+      position: relative;
+
+      .y-axis-label {
+        font-size: 11px;
+        color: #aaa;
+        writing-mode: vertical-rl;
+        transform: rotate(180deg);
+        position: absolute;
+        left: -14px;
+        top: 0;
+        height: 220px;
+        display: flex;
+        align-items: center;
+      }
+
+      .y-axis {
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        font-size: 11px;
+        color: #aaa;
+        height: 220px;
+        min-width: 28px;
+        text-align: right;
+      }
+
+      .bars-container {
+        flex: 1;
+        display: flex;
+        align-items: flex-end;
+        justify-content: space-around;
+        height: 220px;
+        border-bottom: 1px solid #e8e8e8;
+        padding: 0 8px 24px;
+        position: relative;
+
+        &::before {
+          content: '';
+          position: absolute;
+          left: 0; right: 0; top: 0; bottom: 24px;
+          background: repeating-linear-gradient(to bottom, #f5f5f5 0, #f5f5f5 1px, transparent 1px, transparent calc(100% / 7));
+          pointer-events: none;
+        }
+
+        .bar-group {
+          display: flex;
+          flex-direction: column;
+          align-items: center;
+          position: relative;
+          z-index: 1;
+
+          .bars-triple {
+            display: flex;
+            gap: 2px;
+            align-items: flex-end;
+
+            .single-bar {
+              display: flex;
+              flex-direction: column;
+              align-items: center;
+              gap: 2px;
+
+              .bar-val {
+                font-size: 9px;
+                color: #333;
+                font-weight: 600;
+              }
+
+              .bar {
+                width: 14px;
+                border-radius: 2px 2px 0 0;
+                &.blue   { background: #5b9cf6; }
+                &.green  { background: #4fbe7e; }
+                &.orange { background: #f59e42; }
+              }
+            }
+          }
+
+          .group-label {
+            position: absolute;
+            bottom: -18px;
+            left: 50%;
+            transform: translateX(-50%);
+            font-size: 10px;
+            color: #555;
+            white-space: nowrap;
+          }
+        }
+      }
+    }
+  }
+
+  .filter-row {
+    display: flex;
+    gap: 20px;
+    margin-bottom: 12px;
+    flex-wrap: wrap;
+  }
+
+  .link-blue { color: #409eff; }
+
+  .pagination-wrap {
+    display: flex;
+    justify-content: center;
+    margin-top: 14px;
+  }
+
+  .dot {
+    display: inline-block;
+    width: 10px;
+    height: 10px;
+    border-radius: 2px;
+    flex-shrink: 0;
+    &.blue   { background: #5b9cf6; }
+    &.green  { background: #4fbe7e; }
+    &.orange { background: #f59e42; }
+  }
+}
+</style>

+ 16 - 17
src/views/safetyEducationExaminationNew/workbench/index.vue

@@ -4,21 +4,18 @@
     <div class="page-container workbenchPage scrollbar-box" v-if="pageType === 1">
       <!-- 用户信息卡片 -->
       <user-info-card />
-
       <!-- 工作台任务卡片 -->
       <task-card />
-
       <!-- 实验室安全素养资源卡 -->
       <resource-card />
-
-      <!-- 安监室/实验室安全分布表 + 考试成绩统计 -->
-      <exam-stats-card />
-
-      <!-- 实验室管与人员图 -->
-      <safety-person-chart />
-
-      <!-- 学期统计 -->
-      <student-stats />
+      <!-- 实验室绑定申请 -->
+      <lab-binding-apply />
+      <!-- 考试情况统计 -->
+      <exam-situation />
+      <!-- 实验室与人员匹配 -->
+      <lab-person-match />
+      <!-- 学时统计 -->
+      <study-hour-stats />
     </div>
   </div>
 </template>
@@ -27,9 +24,10 @@
   import UserInfoCard from './UserInfoCard.vue'
   import TaskCard from './TaskCard.vue'
   import ResourceCard from './ResourceCard.vue'
-  import ExamStatsCard from './ExamStatsCard.vue'
-  import SafetyPersonChart from './SafetyPersonChart.vue'
-  import StudentStats from './StudentStats.vue'
+  import LabBindingApply from './LabBindingApply.vue'
+  import ExamSituation from './ExamSituation.vue'
+  import LabPersonMatch from './LabPersonMatch.vue'
+  import StudyHourStats from './StudyHourStats.vue'
 
   export default {
     name: 'index',
@@ -37,9 +35,10 @@
       UserInfoCard,
       TaskCard,
       ResourceCard,
-      ExamStatsCard,
-      SafetyPersonChart,
-      StudentStats,
+      LabBindingApply,
+      ExamSituation,
+      LabPersonMatch,
+      StudyHourStats,
     },
     data() {
       return {