|
|
@@ -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>
|