Преглед на файлове

Merge branch 'master' of http://192.168.1.222:3000/hnsh-smart-steward/smart-steward-admin

miaofuhao преди 1 седмица
родител
ревизия
61be526a1f
променени са 1 файла, в които са добавени 537 реда и са изтрити 10 реда
  1. 537 10
      apps/web-ele/src/views/examManage/examAnalysis/cpn/examReport/index.vue

+ 537 - 10
apps/web-ele/src/views/examManage/examAnalysis/cpn/examReport/index.vue

@@ -1,17 +1,544 @@
1 1
 <script lang="ts" setup>
2
+// 导入部分
3
+import type { EchartsUIType } from '@vben/plugins/echarts';
4
+
5
+import type { VxeGridProps } from '#/adapter/vxe-table';
6
+
7
+import { onMounted, ref, watch } from 'vue';
8
+
2 9
 import { Page } from '@vben/common-ui';
10
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
11
+
12
+import { ElTabPane, ElTabs } from 'element-plus';
13
+
14
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
15
+
16
+// 类型定义
17
+interface ExamQuestion {
18
+  id: number;
19
+  questionType: string;
20
+  questionName: string;
21
+  correctCount: number;
22
+  correctRate: string;
23
+}
24
+
25
+interface OperationLog {
26
+  id: number;
27
+  operationTime: string;
28
+  operator: string;
29
+  operationType: string;
30
+  operationContent: string;
31
+}
32
+
33
+interface UserRanking {
34
+  id: number;
35
+  rank: number;
36
+  station: string;
37
+  userName: string;
38
+  score: number;
39
+}
40
+
41
+// 统一的图表颜色配置
42
+const CHART_COLORS = {
43
+  PARTICIPATION: '#5ab1ef',
44
+  PASS: '#52c41a',
45
+  BACKGROUND: '#f0f2f5',
46
+};
47
+
48
+// 响应式数据
49
+// 考试基本信息
50
+const examInfo = ref({
51
+  examName: '考试1',
52
+  createTime: '2025-11-11',
53
+  endTime: '2025-12-11',
54
+  participantCount: 68,
55
+  issuedCount: 100,
56
+  correctedCount: 60,
57
+  passCount: 50, // 及格人数
58
+});
59
+
60
+// 标签页切换
61
+const activeDataTab = ref('dataBoard');
62
+const activeStatusTab = ref('participation');
63
+
64
+// 图表相关数据
65
+const chartRef = ref<EchartsUIType>();
66
+const { renderEcharts: renderChart } = useEcharts(chartRef);
67
+
68
+// 统一的图表更新函数
69
+const updateChart = () => {
70
+  const isParticipationTab = activeStatusTab.value === 'participation';
71
+
72
+  const chartData = isParticipationTab
73
+    ? [
74
+        {
75
+          value: examInfo.value.participantCount,
76
+          name: '已参与',
77
+          itemStyle: { color: CHART_COLORS.PARTICIPATION },
78
+        },
79
+        {
80
+          value: examInfo.value.issuedCount - examInfo.value.participantCount,
81
+          name: '未参与',
82
+          itemStyle: { color: CHART_COLORS.BACKGROUND },
83
+        },
84
+      ]
85
+    : [
86
+        {
87
+          value: examInfo.value.passCount,
88
+          name: '及格',
89
+          itemStyle: { color: CHART_COLORS.PASS },
90
+        },
91
+        {
92
+          value: examInfo.value.participantCount - examInfo.value.passCount,
93
+          name: '不及格',
94
+          itemStyle: { color: CHART_COLORS.BACKGROUND },
95
+        },
96
+      ];
97
+
98
+  renderChart({
99
+    tooltip: {
100
+      trigger: 'item',
101
+      formatter: '{a} <br/>{b}: {c} ({d}%)',
102
+    },
103
+    series: [
104
+      {
105
+        name: isParticipationTab ? '参与情况' : '及格情况',
106
+        type: 'pie',
107
+        radius: ['70%', '85%'],
108
+        avoidLabelOverlap: false,
109
+        itemStyle: { borderRadius: 0, borderWidth: 0 },
110
+        label: {
111
+          show: true,
112
+          position: 'center',
113
+          formatter: `{b}\n{d}%`,
114
+          fontSize: 20,
115
+          fontWeight: 'bold',
116
+        },
117
+        emphasis: {
118
+          label: { show: true, fontSize: 24, fontWeight: 'bold' },
119
+        },
120
+        labelLine: { show: false },
121
+        data: chartData,
122
+      },
123
+    ],
124
+  });
125
+};
126
+
127
+// Mock数据配置
128
+const MOCK_EXAM_QUESTIONS: ExamQuestion[] = Array.from({ length: 6 })
129
+  .fill(0)
130
+  .map((_, i) => ({
131
+    id: i + 1,
132
+    questionType: '单选题',
133
+    questionName: '单选题',
134
+    correctCount: 90,
135
+    correctRate: '99.99%',
136
+  }));
137
+
138
+const MOCK_OPERATION_LOGS: OperationLog[] = [
139
+  {
140
+    id: 1,
141
+    operationTime: '2025-11-15 10:30',
142
+    operator: '管理员',
143
+    operationType: '创建考试',
144
+    operationContent: '创建了考试1',
145
+  },
146
+  {
147
+    id: 2,
148
+    operationTime: '2025-11-16 14:20',
149
+    operator: '管理员',
150
+    operationType: '发布考试',
151
+    operationContent: '发布了考试1',
152
+  },
153
+  {
154
+    id: 3,
155
+    operationTime: '2025-11-17 09:15',
156
+    operator: '用户A',
157
+    operationType: '参加考试',
158
+    operationContent: '用户A参加了考试1',
159
+  },
160
+  {
161
+    id: 4,
162
+    operationTime: '2025-11-17 10:05',
163
+    operator: '用户B',
164
+    operationType: '参加考试',
165
+    operationContent: '用户B参加了考试1',
166
+  },
167
+  {
168
+    id: 5,
169
+    operationTime: '2025-11-18 16:45',
170
+    operator: '管理员',
171
+    operationType: '批改考试',
172
+    operationContent: '批改了考试1的试卷',
173
+  },
174
+];
175
+
176
+const MOCK_USER_RANKINGS: UserRanking[] = Array.from({ length: 6 })
177
+  .fill(0)
178
+  .map((_, i) => ({
179
+    id: i + 1,
180
+    rank: i + 1,
181
+    station: '油站1',
182
+    userName: '张三',
183
+    score: 90,
184
+  }));
185
+
186
+// 表格配置
187
+const questionColumns: VxeGridProps['columns'] = [
188
+  { title: '序号', minWidth: 80, field: 'id' },
189
+  { title: '题目类别', minWidth: 120, field: 'questionType' },
190
+  { title: '题目名称', minWidth: 200, field: 'questionName' },
191
+  { title: '正确数', minWidth: 100, field: 'correctCount' },
192
+  { title: '正确率', minWidth: 100, field: 'correctRate' },
193
+];
194
+
195
+const questionGridOptions: VxeGridProps = {
196
+  columns: questionColumns,
197
+  size: 'medium',
198
+  useSearchForm: false,
199
+  proxyConfig: {
200
+    enabled: true,
201
+    autoLoad: true,
202
+    ajax: {
203
+      query: async () => ({
204
+        items: MOCK_EXAM_QUESTIONS,
205
+        total: MOCK_EXAM_QUESTIONS.length,
206
+      }),
207
+    },
208
+  },
209
+  rowConfig: { keyField: 'id' },
210
+  toolbarConfig: {},
211
+  id: 'exam-question-table',
212
+};
213
+
214
+const logColumns: VxeGridProps['columns'] = [
215
+  { title: '序号', minWidth: 80, field: 'id' },
216
+  { title: '操作时间', minWidth: 150, field: 'operationTime' },
217
+  { title: '操作人', minWidth: 100, field: 'operator' },
218
+  { title: '操作类型', minWidth: 120, field: 'operationType' },
219
+  { title: '操作内容', minWidth: 300, field: 'operationContent' },
220
+];
221
+
222
+const logGridOptions: VxeGridProps = {
223
+  columns: logColumns,
224
+  size: 'medium',
225
+  useSearchForm: false,
226
+  proxyConfig: {
227
+    enabled: true,
228
+    autoLoad: true,
229
+    ajax: {
230
+      query: async () => ({
231
+        items: MOCK_OPERATION_LOGS,
232
+        total: MOCK_OPERATION_LOGS.length,
233
+      }),
234
+    },
235
+  },
236
+  rowConfig: { keyField: 'id' },
237
+  toolbarConfig: {},
238
+  id: 'operation-log-table',
239
+};
240
+
241
+const rankingColumns: VxeGridProps['columns'] = [
242
+  { title: '名次', minWidth: 80, field: 'rank' },
243
+  { title: '所属油站', minWidth: 120, field: 'station' },
244
+  { title: '人员姓名', minWidth: 120, field: 'userName' },
245
+  { title: '得分', minWidth: 100, field: 'score' },
246
+  {
247
+    title: '操作',
248
+    minWidth: 100,
249
+    field: 'action',
250
+    slots: { default: 'action' },
251
+  },
252
+];
253
+
254
+const rankingGridOptions: VxeGridProps = {
255
+  columns: rankingColumns,
256
+  size: 'medium',
257
+  useSearchForm: false,
258
+  proxyConfig: {
259
+    enabled: true,
260
+    autoLoad: true,
261
+    ajax: {
262
+      query: async () => ({
263
+        items: MOCK_USER_RANKINGS,
264
+        total: MOCK_USER_RANKINGS.length,
265
+      }),
266
+    },
267
+  },
268
+  rowConfig: { keyField: 'id' },
269
+  toolbarConfig: {},
270
+  id: 'user-ranking-table',
271
+};
272
+
273
+// 创建表格实例
274
+const [QuestionTable] = useVbenVxeGrid({ gridOptions: questionGridOptions });
275
+const [LogTable] = useVbenVxeGrid({ gridOptions: logGridOptions });
276
+const [RankingTable] = useVbenVxeGrid({ gridOptions: rankingGridOptions });
277
+
278
+// 生命周期钩子和监听
279
+onMounted(() => {
280
+  updateChart();
281
+});
282
+
283
+// 监听标签页切换,重新渲染图表
284
+watch(
285
+  () => activeStatusTab.value,
286
+  () => {
287
+    updateChart();
288
+  },
289
+);
290
+
291
+// 添加操作函数
292
+const handleViewDetail = (row: UserRanking) => {
293
+  console.log('查看详情:', row);
294
+  // 这里可以添加查看详情的逻辑
295
+};
3 296
 </script>
297
+
4 298
 <template>
5
-<Page>
6
-<div class="wrap">
7
-        考试统计功能开发中
8
-</div>
9
-</Page>
299
+  <Page :auto-content-height="true">
300
+    <div class="exam-report-container">
301
+      <!-- 顶部区域 -->
302
+      <div class="top-section">
303
+        <div class="exam-name">{{ examInfo.examName }}</div>
304
+        <div class="exam-stats">
305
+          <span>创建时间: {{ examInfo.createTime }}</span>
306
+          <span class="separator">|</span>
307
+          <span>截止时间: {{ examInfo.endTime }}</span>
308
+          <span class="separator">|</span>
309
+          <span>参与人数: {{ examInfo.participantCount }}</span>
310
+          <span class="separator">|</span>
311
+          <span>发放人数: {{ examInfo.issuedCount }}</span>
312
+          <span class="separator">|</span>
313
+          <span>批改人数: {{ examInfo.correctedCount }}</span>
314
+        </div>
315
+      </div>
316
+
317
+      <!-- 数据看板和操作日志切换 -->
318
+      <div class="tab-container">
319
+        <ElTabs v-model="activeDataTab">
320
+          <ElTabPane label="数据看板" name="dataBoard" />
321
+          <ElTabPane label="操作日志" name="operationLog" />
322
+        </ElTabs>
323
+      </div>
324
+
325
+      <!-- 数据内容区域 -->
326
+      <div class="data-content">
327
+        <!-- 数据看板 -->
328
+        <div v-if="activeDataTab === 'dataBoard'" class="data-board">
329
+          <QuestionTable />
330
+        </div>
331
+
332
+        <!-- 操作日志 -->
333
+        <div v-if="activeDataTab === 'operationLog'" class="operation-log">
334
+          <LogTable />
335
+        </div>
336
+      </div>
337
+
338
+      <!-- 参与情况和人员排名区域 -->
339
+      <div class="bottom-section">
340
+        <!-- 左侧:参与情况和及格情况 -->
341
+        <div class="left-section">
342
+          <div class="status-tab-container">
343
+            <ElTabs v-model="activeStatusTab">
344
+              <ElTabPane label="参与情况" name="participation" />
345
+              <ElTabPane label="及格情况" name="pass" />
346
+            </ElTabs>
347
+          </div>
348
+
349
+          <!-- 统一的图表容器 -->
350
+          <div class="status-content">
351
+            <div class="chart-container">
352
+              <EchartsUI ref="chartRef" style="width: 100%; height: 250px" />
353
+            </div>
354
+            <div class="stats-container">
355
+              <!-- 动态显示统计数据 -->
356
+              <div class="stat-item">
357
+                <div class="stat-label">
358
+                  {{
359
+                    activeStatusTab === 'participation'
360
+                      ? '发放人数'
361
+                      : '参与人数'
362
+                  }}
363
+                </div>
364
+                <div class="stat-value">
365
+                  {{
366
+                    activeStatusTab === 'participation'
367
+                      ? examInfo.issuedCount
368
+                      : examInfo.participantCount
369
+                  }}
370
+                </div>
371
+              </div>
372
+              <div class="stat-item">
373
+                <div class="stat-label">
374
+                  {{
375
+                    activeStatusTab === 'participation'
376
+                      ? '参与人数'
377
+                      : '及格人数'
378
+                  }}
379
+                </div>
380
+                <div class="stat-value">
381
+                  {{
382
+                    activeStatusTab === 'participation'
383
+                      ? examInfo.participantCount
384
+                      : examInfo.passCount
385
+                  }}
386
+                </div>
387
+              </div>
388
+            </div>
389
+          </div>
390
+        </div>
391
+
392
+        <!-- 右侧:人员得分排名 -->
393
+        <div class="right-section">
394
+          <div class="ranking-header">
395
+            <h3>人员得分排名</h3>
396
+          </div>
397
+          <div class="ranking-table">
398
+            <RankingTable>
399
+              <template #action="{ row }">
400
+                <el-button
401
+                  type="primary"
402
+                  link
403
+                  size="small"
404
+                  @click="handleViewDetail(row)"
405
+                >
406
+                  查看
407
+                </el-button>
408
+              </template>
409
+            </RankingTable>
410
+          </div>
411
+        </div>
412
+      </div>
413
+    </div>
414
+  </Page>
10 415
 </template>
11
-<style lang="scss" scoped>
12
-.wrap {
13
-padding: 16px;
14
-background-color: #fff;
15
-border-radius: 6px;
416
+
417
+<style scoped lang="scss">
418
+.exam-report-container {
419
+  min-height: 100vh;
420
+  padding: 16px;
421
+  background-color: #fff;
422
+
423
+  .top-section {
424
+    display: flex;
425
+    justify-content: space-between;
426
+    align-items: center;
427
+    margin-bottom: 16px;
428
+    padding-bottom: 16px;
429
+    border-bottom: 1px solid #e6e8eb;
430
+
431
+    .exam-name {
432
+      font-size: 20px;
433
+      font-weight: bold;
434
+      color: #303133;
435
+    }
436
+
437
+    .exam-stats {
438
+      display: flex;
439
+      align-items: center;
440
+      gap: 10px;
441
+      font-size: 14px;
442
+      color: #606266;
443
+
444
+      .separator {
445
+        color: #dcdfe6;
446
+      }
447
+    }
448
+  }
449
+
450
+  .data-content {
451
+    margin-bottom: 16px;
452
+    background-color: #fff;
453
+    border-radius: 6px;
454
+    padding: 16px;
455
+    padding-top: 0;
456
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
457
+  }
458
+
459
+  .bottom-section {
460
+    display: flex;
461
+    gap: 16px;
462
+
463
+    .left-section {
464
+      flex: 1;
465
+      background-color: #fff;
466
+      border-radius: 6px;
467
+      padding: 16px;
468
+      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
469
+
470
+      .status-tab-container {
471
+        margin-bottom: 16px;
472
+      }
473
+
474
+      .status-content {
475
+        display: flex;
476
+        gap: 16px;
477
+
478
+        .chart-container {
479
+          flex: 1;
480
+          display: flex;
481
+          justify-content: center;
482
+          align-items: center;
483
+        }
484
+
485
+        .stats-container {
486
+          flex: 1;
487
+          display: flex;
488
+          flex-direction: row;
489
+          justify-content: space-between;
490
+          gap: 16px;
491
+
492
+          .stat-item {
493
+            align-items: center;
494
+            border-radius: 6px;
495
+            height: 100px;
496
+            text-align: center;
497
+            margin-top: 80px;
498
+            .stat-label {
499
+              font-size: 14px;
500
+              color: #606266;
501
+              background-color: #f2f5ff;
502
+              padding: 4px 8px;
503
+              border-radius: 4px;
504
+              width: 150px;
505
+              height: 32px;
506
+              line-height: 32px;
507
+            }
508
+
509
+            .stat-value {
510
+              font-size: 24px;
511
+              font-weight: bold;
512
+              color: #303133;
513
+              background-color: #eaefff;
514
+              padding: 4px 12px;
515
+              border-radius: 4px;
516
+              width: 150px;
517
+              height: 60px;
518
+              line-height: 60px;
519
+            }
520
+          }
521
+        }
522
+      }
523
+    }
524
+
525
+    .right-section {
526
+      flex: 1;
527
+      background-color: #fff;
528
+      border-radius: 6px;
529
+      padding: 16px;
530
+      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
531
+
532
+      .ranking-header {
533
+        margin-bottom: 16px;
534
+
535
+        h3 {
536
+          font-size: 16px;
537
+          font-weight: 600;
538
+          color: #303133;
539
+        }
540
+      }
541
+    }
542
+  }
16 543
 }
17 544
 </style>