miaofuhao 1 month ago
parent
commit
d367010baf

+ 4 - 0
apps/web-ele/src/locales/langs/zh-CN/page.json

@@ -10,5 +10,9 @@
10 10
     "title": "概览",
11 11
     "analytics": "分析页",
12 12
     "workspace": "工作台"
13
+  },
14
+  "examManage": {
15
+    "title": "考试管理",
16
+    "examStats": "考试统计"
13 17
   }
14 18
 }

+ 27 - 0
apps/web-ele/src/router/routes/modules/examManage.ts

@@ -0,0 +1,27 @@
1
+import type { RouteRecordRaw } from 'vue-router';
2
+import { $t } from '#/locales';
3
+
4
+const routes: RouteRecordRaw[] = [
5
+  {
6
+    meta: {
7
+      icon: 'lucide:book-open',
8
+      keepAlive: true,
9
+      title: $t('page.examManage.title'),
10
+    },
11
+    name: 'ExamManage',
12
+    path: '/examManage',
13
+    children: [
14
+      {
15
+        meta: {
16
+          title: $t('page.examManage.examStats'),
17
+        },
18
+        name: 'ExamStats',
19
+        path: '/examManage/examStats',
20
+        component: () => import('#/views/examManage/examStats/index.vue'),
21
+      },
22
+      // 可以添加更多考试管理相关的页面路由
23
+    ],
24
+  },
25
+];
26
+
27
+export default routes;

+ 770 - 5
apps/web-ele/src/views/examManage/examPaper/index.vue

@@ -1,20 +1,785 @@
1 1
 <script lang="ts" setup>
2
+// 导入依赖
3
+import type { VbenFormProps } from '@vben/common-ui';
4
+import type { EchartsUIType } from '@vben/plugins/echarts';
5
+
6
+import type { VxeGridProps } from '#/adapter/vxe-table';
7
+
8
+import { computed, onMounted, ref } from 'vue';
9
+
2 10
 import { Page } from '@vben/common-ui';
11
+import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
12
+
13
+import { ElButton } from 'element-plus';
14
+
15
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
16
+
17
+// ========================== 类型定义 ==========================
18
+// 考卷分类接口
19
+interface ExamCategory {
20
+  id: number;
21
+  name: string;
22
+  updateTime: string;
23
+  count: number;
24
+}
25
+
26
+// 题目统计接口
27
+interface QuestionStat {
28
+  id: number;
29
+  category: string;
30
+  name: string;
31
+  correctCount: number;
32
+  correctRate: string;
33
+}
34
+
35
+// 操作日志接口
36
+interface OperationLog {
37
+  id: number;
38
+  department: string;
39
+  operator: string;
40
+  content: string;
41
+  time: string;
42
+}
43
+
44
+// 人员得分接口
45
+interface ScoreRank {
46
+  id: number;
47
+  station: string;
48
+  name: string;
49
+  score: number;
50
+}
51
+
52
+// ========================== 数据管理 ==========================
53
+// 考卷分类数据
54
+const categories = ref<ExamCategory[]>([
55
+  { id: 1, name: '新员工入职考试', updateTime: '2025-11-11', count: 10 },
56
+  { id: 2, name: '安全知识考核', updateTime: '2025-11-10', count: 15 },
57
+  { id: 3, name: '设备操作考试', updateTime: '2025-11-09', count: 8 },
58
+  { id: 4, name: '质量管理考试', updateTime: '2025-11-08', count: 12 },
59
+  { id: 5, name: '日常操作考核', updateTime: '2025-11-07', count: 20 },
60
+]);
61
+
62
+// 搜索关键词
63
+const searchKeyword = ref('');
64
+
65
+// 筛选后的分类列表
66
+const filteredCategories = computed(() => {
67
+  if (!searchKeyword.value) {
68
+    return categories.value;
69
+  }
70
+  return categories.value.filter((category) =>
71
+    category.name.toLowerCase().includes(searchKeyword.value.toLowerCase()),
72
+  );
73
+});
74
+
75
+// 选中的考卷分类
76
+const selectedCategory = ref(1);
77
+
78
+// ========================== 页面状态管理 ==========================
79
+// 右侧内容标签页切换
80
+const activeTab = ref('dashboard');
81
+
82
+// ========================== 事件处理 ==========================
83
+// 选择考卷分类
84
+const selectCategory = (id: number) => {
85
+  selectedCategory.value = id;
86
+};
87
+
88
+// 切换右侧标签页
89
+const switchTab = (tab: string) => {
90
+  activeTab.value = tab;
91
+};
92
+
93
+// ========================== 题目统计表格 ==========================
94
+// 列配置
95
+const questionStatsColumns: VxeGridProps['columns'] = [
96
+  { title: '序号', width: 80, field: 'id', type: 'seq' },
97
+  { title: '题目类别', width: 120, field: 'category' },
98
+  { title: '题目名称', width: 400, field: 'name' },
99
+  { title: '正确数', width: 100, field: 'correctCount' },
100
+  { title: '正确率', width: 100, field: 'correctRate' },
101
+];
102
+
103
+// 表格配置
104
+const questionStatsGridOptions: VxeGridProps = {
105
+  columns: questionStatsColumns,
106
+  size: 'medium',
107
+  height: '300px',
108
+  useSearchForm: false,
109
+  proxyConfig: {
110
+    enabled: true,
111
+    autoLoad: true,
112
+    ajax: {
113
+      query: async () => {
114
+        const mockData: QuestionStat[] = [
115
+          {
116
+            id: 1,
117
+            category: '单选题',
118
+            name: '题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称',
119
+            correctCount: 89,
120
+            correctRate: '98.02%',
121
+          },
122
+          {
123
+            id: 2,
124
+            category: '单选题',
125
+            name: '题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称',
126
+            correctCount: 88,
127
+            correctRate: '98.82%',
128
+          },
129
+          {
130
+            id: 3,
131
+            category: '多选题',
132
+            name: '题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称',
133
+            correctCount: 90,
134
+            correctRate: '100%',
135
+          },
136
+          {
137
+            id: 4,
138
+            category: '多选题',
139
+            name: '题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称',
140
+            correctCount: 90,
141
+            correctRate: '100%',
142
+          },
143
+          {
144
+            id: 5,
145
+            category: '简答题',
146
+            name: '题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称',
147
+            correctCount: 86,
148
+            correctRate: '95.26%',
149
+          },
150
+          {
151
+            id: 6,
152
+            category: '简答题',
153
+            name: '题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称',
154
+            correctCount: 85,
155
+            correctRate: '94.87%',
156
+          },
157
+          {
158
+            id: 7,
159
+            category: '简答题',
160
+            name: '题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称题目名称',
161
+            correctCount: 85,
162
+            correctRate: '94.87%',
163
+          },
164
+        ];
165
+        return { items: mockData, total: mockData.length };
166
+      },
167
+    },
168
+  },
169
+  rowConfig: { keyField: 'id' },
170
+  toolbarConfig: {},
171
+  id: 'exam-paper-question-stats',
172
+};
173
+
174
+// 表单配置(无搜索表单)
175
+const questionStatsFormOptions: VbenFormProps = {
176
+  commonConfig: {
177
+    labelWidth: 80,
178
+    componentProps: { allowClear: true },
179
+  },
180
+  schema: [],
181
+};
182
+
183
+// 创建表格组件
184
+const [QuestionStatsTable] = useVbenVxeGrid({
185
+  formOptions: questionStatsFormOptions,
186
+  gridOptions: questionStatsGridOptions,
187
+});
188
+
189
+// ========================== 操作日志表格 ==========================
190
+// 列配置
191
+const operationLogsColumns: VxeGridProps['columns'] = [
192
+  { title: '序号', width: 80, field: 'id', type: 'seq' },
193
+  { title: '操作部门', width: 150, field: 'department' },
194
+  { title: '操作人', width: 120, field: 'operator' },
195
+  { title: '操作内容', width: 300, field: 'content' },
196
+  { title: '操作时间', width: 180, field: 'time' },
197
+];
198
+
199
+// 表格配置
200
+const operationLogsGridOptions: VxeGridProps = {
201
+  columns: operationLogsColumns,
202
+  size: 'medium',
203
+  height: '300px',
204
+  useSearchForm: false,
205
+  proxyConfig: {
206
+    enabled: true,
207
+    autoLoad: true,
208
+    ajax: {
209
+      query: async () => {
210
+        const mockData: OperationLog[] = [
211
+          {
212
+            id: 1,
213
+            department: '运营部',
214
+            operator: '张三',
215
+            content: '创建考试',
216
+            time: '2025-11-11 10:00:00',
217
+          },
218
+          {
219
+            id: 2,
220
+            department: '运营部',
221
+            operator: '李四',
222
+            content: '更新考试内容',
223
+            time: '2025-11-11 10:30:00',
224
+          },
225
+          {
226
+            id: 3,
227
+            department: '人事部',
228
+            operator: '王五',
229
+            content: '发送考试通知',
230
+            time: '2025-11-12 09:00:00',
231
+          },
232
+          {
233
+            id: 4,
234
+            department: '财务部',
235
+            operator: '赵六',
236
+            content: '查看考试结果',
237
+            time: '2025-11-13 14:00:00',
238
+          },
239
+          {
240
+            id: 5,
241
+            department: '运营部',
242
+            operator: '张三',
243
+            content: '导出考试数据',
244
+            time: '2025-11-13 15:30:00',
245
+          },
246
+        ];
247
+        return { items: mockData, total: mockData.length };
248
+      },
249
+    },
250
+  },
251
+  rowConfig: { keyField: 'id' },
252
+  toolbarConfig: {},
253
+  id: 'exam-paper-operation-logs',
254
+};
255
+
256
+// 表单配置(无搜索表单)
257
+const operationLogsFormOptions: VbenFormProps = {
258
+  commonConfig: {
259
+    labelWidth: 80,
260
+    componentProps: { allowClear: true },
261
+  },
262
+  schema: [],
263
+};
3 264
 
265
+// 创建表格组件
266
+const [OperationLogsTable] = useVbenVxeGrid({
267
+  formOptions: operationLogsFormOptions,
268
+  gridOptions: operationLogsGridOptions,
269
+});
270
+
271
+// ========================== 人员得分排名表格 ==========================
272
+// 列配置
273
+const scoreRanksColumns: VxeGridProps['columns'] = [
274
+  { title: '排名', width: 80, field: 'id', type: 'seq' },
275
+  { title: '所属油站', width: 150, field: 'station' },
276
+  { title: '人员姓名', width: 120, field: 'name' },
277
+  { title: '得分', width: 100, field: 'score' },
278
+  {
279
+    title: '操作',
280
+    width: 100,
281
+    field: 'action',
282
+    slots: { default: 'action' },
283
+  },
284
+];
285
+
286
+// 表格配置
287
+const scoreRanksGridOptions: VxeGridProps = {
288
+  columns: scoreRanksColumns,
289
+  size: 'medium',
290
+  height: '300px',
291
+  useSearchForm: false,
292
+  proxyConfig: {
293
+    enabled: true,
294
+    autoLoad: true,
295
+    ajax: {
296
+      query: async () => {
297
+        const mockData: ScoreRank[] = [
298
+          { id: 1, station: '所属油站', name: '张三', score: 100 },
299
+          { id: 2, station: '所属油站', name: '李四', score: 99 },
300
+          { id: 3, station: '所属油站', name: '王五', score: 98 },
301
+          { id: 4, station: '所属油站', name: '赵六', score: 97 },
302
+          { id: 5, station: '所属油站', name: '刘七', score: 96 },
303
+        ];
304
+        return { items: mockData, total: mockData.length };
305
+      },
306
+    },
307
+  },
308
+  rowConfig: { keyField: 'id' },
309
+  toolbarConfig: {},
310
+  id: 'exam-paper-score-ranks',
311
+};
312
+
313
+// 表单配置(无搜索表单)
314
+const scoreRanksFormOptions: VbenFormProps = {
315
+  commonConfig: {
316
+    labelWidth: 80,
317
+    componentProps: { allowClear: true },
318
+  },
319
+  schema: [],
320
+};
321
+
322
+// 创建表格组件
323
+const [ScoreRanksTable] = useVbenVxeGrid({
324
+  formOptions: scoreRanksFormOptions,
325
+  gridOptions: scoreRanksGridOptions,
326
+});
327
+
328
+// 查看详情操作
329
+const viewDetail = (id: number) => {
330
+  console.log('查看详情:', id);
331
+};
332
+
333
+// ========================== 图表配置 ==========================
334
+// 图表类型切换
335
+const chartType = ref('participation');
336
+
337
+// ECharts 引用
338
+const chartRef = ref<EchartsUIType>();
339
+const { renderEcharts } = useEcharts(chartRef);
340
+
341
+// 切换图表类型
342
+const switchChartType = (type: string) => {
343
+  chartType.value = type;
344
+  updateChart();
345
+};
346
+
347
+// 更新图表
348
+const updateChart = () => {
349
+  if (chartType.value === 'participation') {
350
+    // 参与情况图表
351
+    renderEcharts({
352
+      series: [
353
+        {
354
+          type: 'pie',
355
+          radius: ['60%', '80%'],
356
+          avoidLabelOverlap: false,
357
+          itemStyle: {
358
+            borderRadius: 0,
359
+            borderColor: '#fff',
360
+            borderWidth: 2,
361
+          },
362
+          label: {
363
+            show: true,
364
+            position: 'center',
365
+            formatter: '{b}\n{c}',
366
+            fontSize: 18,
367
+            fontWeight: 'bold',
368
+          },
369
+          emphasis: {
370
+            label: {
371
+              show: true,
372
+              fontSize: '20',
373
+              fontWeight: 'bold',
374
+            },
375
+          },
376
+          labelLine: { show: false },
377
+          data: [
378
+            { value: 60, name: '参与人数', itemStyle: { color: '#5ab1ef' } },
379
+            { value: 40, name: '未参与人数', itemStyle: { color: '#f5f7fa' } },
380
+          ],
381
+        },
382
+      ],
383
+      tooltip: {
384
+        formatter: '{b}: {c}<br/>参与率: 60.00%',
385
+      },
386
+    });
387
+  } else {
388
+    // 及格情况图表
389
+    renderEcharts({
390
+      series: [
391
+        {
392
+          type: 'pie',
393
+          radius: ['60%', '80%'],
394
+          avoidLabelOverlap: false,
395
+          itemStyle: {
396
+            borderRadius: 0,
397
+            borderColor: '#fff',
398
+            borderWidth: 2,
399
+          },
400
+          label: {
401
+            show: true,
402
+            position: 'center',
403
+            formatter: '{b}\n{c}',
404
+            fontSize: 18,
405
+            fontWeight: 'bold',
406
+          },
407
+          emphasis: {
408
+            label: {
409
+              show: true,
410
+              fontSize: '20',
411
+              fontWeight: 'bold',
412
+            },
413
+          },
414
+          labelLine: { show: false },
415
+          data: [
416
+            { value: 55, name: '及格人数', itemStyle: { color: '#52c41a' } },
417
+            { value: 5, name: '不及格人数', itemStyle: { color: '#f5f7fa' } },
418
+          ],
419
+        },
420
+      ],
421
+      tooltip: {
422
+        formatter: '{b}: {c}<br/>及格率: 91.67%',
423
+      },
424
+    });
425
+  }
426
+};
427
+
428
+// ========================== 生命周期钩子 ==========================
429
+onMounted(() => {
430
+  // 初始化图表
431
+  updateChart();
432
+});
4 433
 </script>
5 434
 
6 435
 <template>
7 436
   <Page>
8
-    <div class="wrap">
9
-         考卷管理
437
+    <div class="exam-paper-container">
438
+      <!-- 左侧考卷分类 -->
439
+      <div class="left-panel">
440
+        <div class="panel-header">
441
+          <h2>考卷分类</h2>
442
+        </div>
443
+        <div class="search-container">
444
+          <input
445
+            v-model="searchKeyword"
446
+            type="text"
447
+            class="search-input"
448
+            placeholder="搜索考卷分类"
449
+          />
450
+        </div>
451
+        <div class="category-list">
452
+          <div
453
+            v-for="category in filteredCategories"
454
+            :key="category.id"
455
+            class="category-card"
456
+            :class="{ active: selectedCategory === category.id }"
457
+            @click="selectCategory(category.id)"
458
+          >
459
+            <div class="category-name">{{ category.name }}</div>
460
+            <div class="category-info">
461
+              <span>更新时间: {{ category.updateTime }}</span>
462
+              <span>考卷数量: {{ category.count }}</span>
463
+            </div>
464
+          </div>
465
+        </div>
466
+      </div>
467
+
468
+      <!-- 右侧考试统计 -->
469
+      <div class="right-panel">
470
+        <div class="panel-header">
471
+          <h2>考试统计</h2>
472
+          <div class="exam-info">
473
+            <span>考试名称</span>
474
+            <span>创建时间: 2025-11-11</span>
475
+            <span>截止时间: 2025-11-12</span>
476
+            <span>发放人数: 100</span>
477
+            <span>参考人数: 68</span>
478
+            <span>提交人数: 60</span>
479
+          </div>
480
+        </div>
481
+
482
+        <!-- 数据看板和操作日志切换 -->
483
+        <div class="tab-container">
484
+          <div
485
+            class="tab-item"
486
+            :class="{ active: activeTab === 'dashboard' }"
487
+            @click="switchTab('dashboard')"
488
+          >
489
+            数据看板
490
+          </div>
491
+          <div
492
+            class="tab-item"
493
+            :class="{ active: activeTab === 'log' }"
494
+            @click="switchTab('log')"
495
+          >
496
+            操作日志
497
+          </div>
498
+        </div>
499
+
500
+        <!-- 数据看板内容 -->
501
+        <div v-if="activeTab === 'dashboard'" class="dashboard-content">
502
+          <QuestionStatsTable table-title="题目统计" />
503
+        </div>
504
+
505
+        <!-- 操作日志内容 -->
506
+        <div v-if="activeTab === 'log'" class="log-content">
507
+          <OperationLogsTable table-title="操作日志" />
508
+        </div>
509
+
510
+        <!-- 下方统计区域 -->
511
+        <div class="bottom-stats">
512
+          <!-- 数据占比圆环图 -->
513
+          <div class="chart-section">
514
+            <div class="section-header">
515
+              <div
516
+                class="tab-item"
517
+                :class="{ active: chartType === 'participation' }"
518
+                @click="switchChartType('participation')"
519
+              >
520
+                参与情况
521
+              </div>
522
+              <div
523
+                class="tab-item"
524
+                :class="{ active: chartType === 'pass' }"
525
+                @click="switchChartType('pass')"
526
+              >
527
+                及格情况
528
+              </div>
529
+            </div>
530
+            <div class="chart-container">
531
+              <EchartsUI ref="chartRef" />
532
+              <div class="total-count">
533
+                <div class="count-label">发放人数</div>
534
+                <div class="count-value">100</div>
535
+              </div>
536
+            </div>
537
+          </div>
538
+
539
+          <!-- 人员得分排名 -->
540
+          <div class="rank-section">
541
+            <div class="section-header">
542
+              <h3>人员得分排名</h3>
543
+            </div>
544
+            <ScoreRanksTable table-title="人员得分排名">
545
+              <template #action="{ row }">
546
+                <ElButton size="small" @click="viewDetail(row.id)">
547
+                  查看
548
+                </ElButton>
549
+              </template>
550
+            </ScoreRanksTable>
551
+          </div>
552
+        </div>
553
+      </div>
10 554
     </div>
11 555
   </Page>
12 556
 </template>
13 557
 
14 558
 <style lang="scss" scoped>
15
-.wrap {
559
+.exam-paper-container {
560
+  display: grid;
561
+  grid-template-columns: 300px 1fr;
562
+  gap: 20px;
563
+  height: calc(100vh - 120px);
564
+  padding: 0;
565
+  overflow: hidden;
566
+}
567
+
568
+.left-panel,
569
+.right-panel {
570
+  display: flex;
571
+  flex-direction: column;
572
+  background-color: #fff;
573
+  border-radius: 6px;
574
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
575
+}
576
+
577
+.panel-header {
578
+  display: flex;
579
+  gap: 16px;
580
+  justify-content: space-between;
581
+  align-items: center;
582
+  padding: 16px;
583
+  border-bottom: 1px solid #e8e8e8;
584
+}
585
+
586
+.search-container {
16 587
   padding: 16px;
588
+  border-bottom: 1px solid #e8e8e8;
589
+}
590
+
591
+.search-input {
592
+  width: 100%;
593
+  padding: 8px 12px;
594
+  font-size: 14px;
595
+  border: 1px solid #d9d9d9;
596
+  border-radius: 4px;
597
+  transition: all 0.3s;
598
+  box-sizing: border-box;
599
+
600
+  &:focus {
601
+    outline: none;
602
+    border-color: #1890ff;
603
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
604
+  }
605
+}
606
+
607
+.panel-header h2 {
608
+  margin: 0;
609
+  font-size: 16px;
610
+  font-weight: 600;
611
+  color: #333;
612
+}
613
+
614
+.exam-info {
615
+  display: flex;
616
+  flex-wrap: wrap;
617
+  gap: 16px;
618
+  font-size: 12px;
619
+  color: #666;
620
+}
621
+
622
+.category-list {
623
+  height: calc(100vh - 160px);
624
+  padding: 16px;
625
+  overflow-y: auto;
626
+}
627
+
628
+.right-panel {
629
+  display: flex;
630
+  flex-direction: column;
631
+  height: 100%;
632
+  overflow: hidden;
633
+}
634
+
635
+.category-card {
636
+  position: relative;
637
+  padding: 12px;
638
+  margin-bottom: 12px;
639
+  cursor: pointer;
640
+  background-color: #f5f5f5;
641
+  border: 2px solid transparent;
642
+  border-radius: 6px;
643
+  transition: all 0.3s;
644
+
645
+  &:hover {
646
+    background-color: #e6f7ff;
647
+    box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
648
+  }
649
+
650
+  &.active {
651
+    background-color: #e6f7ff;
652
+    border-color: #1890ff;
653
+    box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
654
+  }
655
+}
656
+
657
+.category-name {
658
+  margin-bottom: 8px;
659
+  font-size: 14px;
660
+  font-weight: 500;
661
+  color: #333;
662
+}
663
+
664
+.category-info {
665
+  font-size: 12px;
666
+  color: #666;
667
+}
668
+
669
+.category-info span {
670
+  display: block;
671
+  margin-bottom: 4px;
672
+}
673
+
674
+/* 右侧占位符样式 */
675
+.placeholder {
676
+  display: flex;
677
+  justify-content: center;
678
+  align-items: center;
679
+  height: 100%;
680
+  font-size: 16px;
681
+  color: #999;
682
+  background-color: #fafafa;
683
+  border-radius: 4px;
684
+}
685
+
686
+/* 标签页样式 */
687
+.tab-container {
688
+  display: flex;
689
+  padding: 0 16px;
690
+  border-bottom: 1px solid #e8e8e8;
691
+}
692
+
693
+.tab-item {
694
+  padding: 12px 20px;
695
+  font-size: 14px;
696
+  cursor: pointer;
697
+  color: #666;
698
+  border-bottom: 2px solid transparent;
699
+  transition: all 0.3s;
700
+
701
+  &:hover {
702
+    color: #1890ff;
703
+  }
704
+
705
+  &.active {
706
+    color: #1890ff;
707
+    border-bottom-color: #1890ff;
708
+    font-weight: 500;
709
+  }
710
+}
711
+
712
+/* 数据看板和操作日志内容 */
713
+.dashboard-content,
714
+.log-content {
715
+  flex: 1;
716
+  padding: 16px;
717
+  overflow-y: auto;
718
+  min-height: 300px;
719
+}
720
+
721
+/* 下方统计区域 */
722
+.bottom-stats {
723
+  display: grid;
724
+  grid-template-columns: 1fr 1fr;
725
+  gap: 20px;
726
+  padding: 16px;
727
+  border-top: 1px solid #e8e8e8;
728
+}
729
+
730
+/* 图表部分 */
731
+.chart-section,
732
+.rank-section {
733
+  display: flex;
734
+  flex-direction: column;
735
+  background-color: #fafafa;
17 736
   border-radius: 6px;
18
-  background-color: #ffffff;
737
+  padding: 16px;
738
+}
739
+
740
+.section-header {
741
+  display: flex;
742
+  justify-content: space-between;
743
+  align-items: center;
744
+  margin-bottom: 16px;
745
+}
746
+
747
+.section-header h3 {
748
+  margin: 0;
749
+  font-size: 14px;
750
+  font-weight: 600;
751
+  color: #333;
752
+}
753
+
754
+.chart-container {
755
+  position: relative;
756
+  height: 250px;
757
+}
758
+
759
+.total-count {
760
+  position: absolute;
761
+  top: 50%;
762
+  left: 50%;
763
+  transform: translate(-50%, -50%);
764
+  text-align: center;
765
+}
766
+
767
+.count-label {
768
+  font-size: 12px;
769
+  color: #666;
770
+}
771
+
772
+.count-value {
773
+  font-size: 24px;
774
+  font-weight: bold;
775
+  color: #333;
776
+}
777
+
778
+/* 表格样式 */
779
+:deep(.vben-basic-table) {
780
+  .vben-table-wrapper {
781
+    border: 1px solid #e8e8e8;
782
+    border-radius: 4px;
783
+  }
19 784
 }
20
-</style>
785
+</style>

+ 0 - 0
apps/web-ele/src/views/examManage/examStats/index.vue


+ 183 - 5
apps/web-ele/src/views/examManage/myExamPaper/index.vue

@@ -1,20 +1,198 @@
1 1
 <script lang="ts" setup>
2
+import { computed, ref } from 'vue';
3
+
2 4
 import { Page } from '@vben/common-ui';
3 5
 
6
+// 定义考卷分类接口
7
+interface ExamCategory {
8
+  id: number;
9
+  name: string;
10
+  updateTime: string;
11
+  count: number;
12
+}
13
+
14
+// 初始化考卷分类数据
15
+const categories = ref<ExamCategory[]>([
16
+  { id: 1, name: '新员工入职考试', updateTime: '2025-11-11', count: 10 },
17
+  { id: 2, name: '安全知识考核', updateTime: '2025-11-10', count: 15 },
18
+  { id: 3, name: '设备操作考试', updateTime: '2025-11-09', count: 8 },
19
+  { id: 4, name: '质量管理考试', updateTime: '2025-11-08', count: 12 },
20
+  { id: 5, name: '日常操作考核', updateTime: '2025-11-07', count: 20 },
21
+]);
22
+
23
+// 搜索功能
24
+const searchKeyword = ref('');
25
+
26
+// 筛选后的分类列表
27
+const filteredCategories = computed(() => {
28
+  if (!searchKeyword.value) {
29
+    return categories.value;
30
+  }
31
+  return categories.value.filter((category) =>
32
+    category.name.toLowerCase().includes(searchKeyword.value.toLowerCase()),
33
+  );
34
+});
35
+
36
+// 选中的考卷分类
37
+const selectedCategory = ref(1);
38
+
39
+// 选择考卷分类
40
+const selectCategory = (id: number) => {
41
+  selectedCategory.value = id;
42
+};
4 43
 </script>
5 44
 
6 45
 <template>
7 46
   <Page>
8
-    <div class="wrap">
9
-         我的考卷
47
+    <div class="exam-paper-container">
48
+      <!-- 左侧考卷分类 -->
49
+      <div class="left-panel">
50
+        <div class="panel-header">
51
+          <h2>考卷分类</h2>
52
+        </div>
53
+        <div class="category-list">
54
+          <div
55
+            v-for="category in filteredCategories"
56
+            :key="category.id"
57
+            class="category-card"
58
+            :class="{ active: selectedCategory === category.id }"
59
+            @click="selectCategory(category.id)"
60
+          >
61
+            <div class="category-name">{{ category.name }}</div>
62
+            <div class="category-info">
63
+              <span>更新时间: {{ category.updateTime }}</span>
64
+              <span>考卷数量: {{ category.count }}</span>
65
+            </div>
66
+          </div>
67
+        </div>
68
+      </div>
69
+
70
+      <!-- 右侧考卷列表(暂时为空) -->
71
+      <div class="right-panel">
72
+        <div class="panel-header">
73
+          <h2>考卷列表</h2>
74
+        </div>
75
+        <div class="exam-paper-list">
76
+          <!-- 右侧内容暂未实现 -->
77
+          <div class="placeholder">右侧我的问卷内容暂未实现</div>
78
+        </div>
79
+      </div>
10 80
     </div>
81
+
82
+    <!-- 抽屉组件 -->
11 83
   </Page>
12 84
 </template>
13 85
 
14 86
 <style lang="scss" scoped>
15
-.wrap {
87
+.exam-paper-container {
88
+  display: grid;
89
+  grid-template-columns: 300px 1fr;
90
+  gap: 20px;
91
+  height: calc(100vh - 120px);
92
+  padding: 0;
93
+  overflow: hidden;
94
+}
95
+
96
+.left-panel,
97
+.right-panel {
98
+  display: flex;
99
+  flex-direction: column;
100
+  background-color: #fff;
101
+  border-radius: 6px;
102
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
103
+}
104
+
105
+.panel-header {
106
+  display: flex;
107
+  gap: 16px;
108
+  justify-content: space-between;
16 109
   padding: 16px;
110
+  border-bottom: 1px solid #e8e8e8;
111
+}
112
+
113
+.search-container {
114
+  padding: 16px;
115
+  border-bottom: 1px solid #e8e8e8;
116
+}
117
+
118
+.search-input {
119
+  width: 100%;
120
+  padding: 8px 12px;
121
+  font-size: 14px;
122
+  border: 1px solid #d9d9d9;
123
+  border-radius: 4px;
124
+  transition: all 0.3s;
125
+  box-sizing: border-box;
126
+
127
+  &:focus {
128
+    outline: none;
129
+    border-color: #1890ff;
130
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
131
+  }
132
+}
133
+
134
+.panel-header h2 {
135
+  margin: 0;
136
+  font-size: 16px;
137
+  font-weight: 600;
138
+  color: #333;
139
+}
140
+
141
+.category-list,
142
+.exam-paper-list {
143
+  height: calc(100vh - 160px);
144
+  padding: 16px;
145
+  overflow-y: auto;
146
+}
147
+
148
+.category-card {
149
+  position: relative;
150
+  padding: 12px;
151
+  margin-bottom: 12px;
152
+  cursor: pointer;
153
+  background-color: #f5f5f5;
154
+  border: 2px solid transparent;
17 155
   border-radius: 6px;
18
-  background-color: #ffffff;
156
+  transition: all 0.3s;
157
+
158
+  &:hover {
159
+    background-color: #e6f7ff;
160
+    box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
161
+  }
162
+
163
+  &.active {
164
+    background-color: #e6f7ff;
165
+    border-color: #1890ff;
166
+    box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
167
+  }
168
+}
169
+
170
+.category-name {
171
+  margin-bottom: 8px;
172
+  font-size: 14px;
173
+  font-weight: 500;
174
+  color: #333;
175
+}
176
+
177
+.category-info {
178
+  font-size: 12px;
179
+  color: #666;
180
+}
181
+
182
+.category-info span {
183
+  display: block;
184
+  margin-bottom: 4px;
185
+}
186
+
187
+/* 右侧占位符样式 */
188
+.placeholder {
189
+  display: flex;
190
+  justify-content: center;
191
+  align-items: center;
192
+  height: 100%;
193
+  font-size: 16px;
194
+  color: #999;
195
+  background-color: #fafafa;
196
+  border-radius: 4px;
19 197
 }
20
-</style>
198
+</style>

+ 183 - 5
apps/web-ele/src/views/examManage/questScoring/index.vue

@@ -1,20 +1,198 @@
1 1
 <script lang="ts" setup>
2
+import { computed, ref } from 'vue';
3
+
2 4
 import { Page } from '@vben/common-ui';
3 5
 
6
+// 定义考卷分类接口
7
+interface ExamCategory {
8
+  id: number;
9
+  name: string;
10
+  updateTime: string;
11
+  count: number;
12
+}
13
+
14
+// 初始化考卷分类数据
15
+const categories = ref<ExamCategory[]>([
16
+  { id: 1, name: '新员工入职考试', updateTime: '2025-11-11', count: 10 },
17
+  { id: 2, name: '安全知识考核', updateTime: '2025-11-10', count: 15 },
18
+  { id: 3, name: '设备操作考试', updateTime: '2025-11-09', count: 8 },
19
+  { id: 4, name: '质量管理考试', updateTime: '2025-11-08', count: 12 },
20
+  { id: 5, name: '日常操作考核', updateTime: '2025-11-07', count: 20 },
21
+]);
22
+
23
+// 搜索功能
24
+const searchKeyword = ref('');
25
+
26
+// 筛选后的分类列表
27
+const filteredCategories = computed(() => {
28
+  if (!searchKeyword.value) {
29
+    return categories.value;
30
+  }
31
+  return categories.value.filter((category) =>
32
+    category.name.toLowerCase().includes(searchKeyword.value.toLowerCase()),
33
+  );
34
+});
35
+
36
+// 选中的考卷分类
37
+const selectedCategory = ref(1);
38
+
39
+// 选择考卷分类
40
+const selectCategory = (id: number) => {
41
+  selectedCategory.value = id;
42
+};
4 43
 </script>
5 44
 
6 45
 <template>
7 46
   <Page>
8
-    <div class="wrap">
9
-         问卷打分
47
+    <div class="exam-paper-container">
48
+      <!-- 左侧考卷分类 -->
49
+      <div class="left-panel">
50
+        <div class="panel-header">
51
+          <h2>考卷分类</h2>
52
+        </div>
53
+        <div class="category-list">
54
+          <div
55
+            v-for="category in filteredCategories"
56
+            :key="category.id"
57
+            class="category-card"
58
+            :class="{ active: selectedCategory === category.id }"
59
+            @click="selectCategory(category.id)"
60
+          >
61
+            <div class="category-name">{{ category.name }}</div>
62
+            <div class="category-info">
63
+              <span>更新时间: {{ category.updateTime }}</span>
64
+              <span>考卷数量: {{ category.count }}</span>
65
+            </div>
66
+          </div>
67
+        </div>
68
+      </div>
69
+
70
+      <!-- 右侧考卷列表(暂时为空) -->
71
+      <div class="right-panel">
72
+        <div class="panel-header">
73
+          <h2>考卷列表</h2>
74
+        </div>
75
+        <div class="exam-paper-list">
76
+          <!-- 右侧内容暂未实现 -->
77
+          <div class="placeholder">问卷得分内容暂未实现</div>
78
+        </div>
79
+      </div>
10 80
     </div>
81
+
82
+    <!-- 抽屉组件 -->
11 83
   </Page>
12 84
 </template>
13 85
 
14 86
 <style lang="scss" scoped>
15
-.wrap {
87
+.exam-paper-container {
88
+  display: grid;
89
+  grid-template-columns: 300px 1fr;
90
+  gap: 20px;
91
+  height: calc(100vh - 120px);
92
+  padding: 0;
93
+  overflow: hidden;
94
+}
95
+
96
+.left-panel,
97
+.right-panel {
98
+  display: flex;
99
+  flex-direction: column;
100
+  background-color: #fff;
101
+  border-radius: 6px;
102
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
103
+}
104
+
105
+.panel-header {
106
+  display: flex;
107
+  gap: 16px;
108
+  justify-content: space-between;
16 109
   padding: 16px;
110
+  border-bottom: 1px solid #e8e8e8;
111
+}
112
+
113
+.search-container {
114
+  padding: 16px;
115
+  border-bottom: 1px solid #e8e8e8;
116
+}
117
+
118
+.search-input {
119
+  width: 100%;
120
+  padding: 8px 12px;
121
+  font-size: 14px;
122
+  border: 1px solid #d9d9d9;
123
+  border-radius: 4px;
124
+  transition: all 0.3s;
125
+  box-sizing: border-box;
126
+
127
+  &:focus {
128
+    outline: none;
129
+    border-color: #1890ff;
130
+    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
131
+  }
132
+}
133
+
134
+.panel-header h2 {
135
+  margin: 0;
136
+  font-size: 16px;
137
+  font-weight: 600;
138
+  color: #333;
139
+}
140
+
141
+.category-list,
142
+.exam-paper-list {
143
+  height: calc(100vh - 160px);
144
+  padding: 16px;
145
+  overflow-y: auto;
146
+}
147
+
148
+.category-card {
149
+  position: relative;
150
+  padding: 12px;
151
+  margin-bottom: 12px;
152
+  cursor: pointer;
153
+  background-color: #f5f5f5;
154
+  border: 2px solid transparent;
17 155
   border-radius: 6px;
18
-  background-color: #ffffff;
156
+  transition: all 0.3s;
157
+
158
+  &:hover {
159
+    background-color: #e6f7ff;
160
+    box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
161
+  }
162
+
163
+  &.active {
164
+    background-color: #e6f7ff;
165
+    border-color: #1890ff;
166
+    box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
167
+  }
168
+}
169
+
170
+.category-name {
171
+  margin-bottom: 8px;
172
+  font-size: 14px;
173
+  font-weight: 500;
174
+  color: #333;
175
+}
176
+
177
+.category-info {
178
+  font-size: 12px;
179
+  color: #666;
180
+}
181
+
182
+.category-info span {
183
+  display: block;
184
+  margin-bottom: 4px;
185
+}
186
+
187
+/* 右侧占位符样式 */
188
+.placeholder {
189
+  display: flex;
190
+  justify-content: center;
191
+  align-items: center;
192
+  height: 100%;
193
+  font-size: 16px;
194
+  color: #999;
195
+  background-color: #fafafa;
196
+  border-radius: 4px;
19 197
 }
20
-</style>
198
+</style>

+ 249 - 219
apps/web-ele/src/views/examManage/questionBank/index.vue

@@ -96,8 +96,6 @@ const questions = ref<Question[]>([
96 96
   },
97 97
 ]);
98 98
 
99
-
100
-
101 99
 // 填空题替换方法
102 100
 const replaceBlanks = (content: string) => {
103 101
   return content.replaceAll(
@@ -106,13 +104,11 @@ const replaceBlanks = (content: string) => {
106 104
   );
107 105
 };
108 106
 
109
-
110
-
111 107
 // 弹窗相关
112 108
 const isModalVisible = ref(false);
113 109
 const modalTitle = ref('');
114 110
 const currentQuestion = ref<null | Question>(null);
115
-const currentCategory = ref<null | Category>(null);
111
+const currentCategory = ref<Category | null>(null);
116 112
 const newCategoryName = ref('');
117 113
 const inputError = ref('');
118 114
 
@@ -161,7 +157,6 @@ const selectCategory = (id: number) => {
161 157
   selectedCategory.value = id;
162 158
 };
163 159
 
164
-
165 160
 // 打开新增题目弹窗
166 161
 const openAddModal = () => {
167 162
   modalTitle.value = '添加题目';
@@ -177,8 +172,12 @@ const createNewQuestion = (type: QuestionType): Question => {
177 172
     id: 0, // 临时ID,保存时会生成
178 173
     type,
179 174
     content: '',
180
-    options: type === 'true_false' ? ['正确', '错误'] : 
181
-            type === 'single_choice' || type === 'multiple_choice' ? [''] : undefined,
175
+    options:
176
+      type === 'true_false'
177
+        ? ['正确', '错误']
178
+        : (type === 'single_choice' || type === 'multiple_choice'
179
+          ? ['']
180
+          : undefined),
182 181
     answer: '',
183 182
   };
184 183
 };
@@ -200,25 +199,29 @@ const validateQuestion = (question: Question): boolean => {
200 199
     alert('请输入题目内容');
201 200
     return false;
202 201
   }
203
-  
204
-  if (question.type === 'single_choice' || question.type === 'multiple_choice' || question.type === 'true_false') {
202
+
203
+  if (
204
+    question.type === 'single_choice' ||
205
+    question.type === 'multiple_choice' ||
206
+    question.type === 'true_false'
207
+  ) {
205 208
     if (!question.options || question.options.length < 2) {
206 209
       alert('请至少添加两个选项');
207 210
       return false;
208 211
     }
209
-    
210
-    const hasEmptyOption = question.options.some(option => !option.trim());
212
+
213
+    const hasEmptyOption = question.options.some((option) => !option.trim());
211 214
     if (hasEmptyOption) {
212 215
       alert('选项内容不能为空');
213 216
       return false;
214 217
     }
215 218
   }
216
-  
219
+
217 220
   if (!question.answer.trim()) {
218 221
     alert('请输入答案');
219 222
     return false;
220 223
   }
221
-  
224
+
222 225
   return true;
223 226
 };
224 227
 
@@ -230,9 +233,9 @@ const saveAllQuestions = () => {
230 233
       return;
231 234
     }
232 235
   }
233
-  
236
+
234 237
   // 为每个新题目生成唯一ID
235
-  const maxId = Math.max(...questions.value.map(q => q.id), 0);
238
+  const maxId = Math.max(...questions.value.map((q) => q.id), 0);
236 239
   newQuestions.value.forEach((q, index) => {
237 240
     q.id = maxId + index + 1;
238 241
     questions.value.push(q);
@@ -242,14 +245,14 @@ const saveAllQuestions = () => {
242 245
 
243 246
 // 保存单个编辑的题目
244 247
 const saveSingleQuestion = () => {
245
-  if (currentQuestion.value) {
246
-    if (validateQuestion(currentQuestion.value)) {
247
-      const index = questions.value.findIndex(q => q.id === currentQuestion.value!.id);
248
-      if (index !== -1) {
249
-        questions.value[index] = { ...currentQuestion.value };
250
-      }
251
-      closeModal();
248
+  if (currentQuestion.value && validateQuestion(currentQuestion.value)) {
249
+    const index = questions.value.findIndex(
250
+      (q) => q.id === currentQuestion.value!.id,
251
+    );
252
+    if (index !== -1) {
253
+      questions.value[index] = { ...currentQuestion.value };
252 254
     }
255
+    closeModal();
253 256
   }
254 257
 };
255 258
 
@@ -274,7 +277,9 @@ const openEditCategoryModal = (category: Category) => {
274 277
 // 删除题目类型
275 278
 const deleteCategory = (id: number) => {
276 279
   if (confirm('确定要删除这个题目类型吗?')) {
277
-    categories.value = categories.value.filter(category => category.id !== id);
280
+    categories.value = categories.value.filter(
281
+      (category) => category.id !== id,
282
+    );
278 283
     // 如果删除的是当前选中的分类,切换到第一个分类
279 284
     if (selectedCategory.value === id && categories.value.length > 0) {
280 285
       selectedCategory.value = categories.value[0].id;
@@ -287,28 +292,30 @@ const saveCategory = () => {
287 292
   if (!validateInput(newCategoryName.value)) {
288 293
     return;
289 294
   }
290
-  
295
+
291 296
   if (currentCategory.value) {
292 297
     // 修改现有分类
293
-    const index = categories.value.findIndex(cat => cat.id === currentCategory.value!.id);
298
+    const index = categories.value.findIndex(
299
+      (cat) => cat.id === currentCategory.value!.id,
300
+    );
294 301
     if (index !== -1) {
295 302
       categories.value[index] = {
296 303
         ...categories.value[index],
297 304
         name: newCategoryName.value,
298
-        updateTime: new Date().toISOString().split('T')[0]
305
+        updateTime: new Date().toISOString().split('T')[0],
299 306
       };
300 307
     }
301 308
   } else {
302 309
     // 添加新分类
303
-    const newId = Math.max(...categories.value.map(cat => cat.id), 0) + 1;
310
+    const newId = Math.max(...categories.value.map((cat) => cat.id), 0) + 1;
304 311
     categories.value.push({
305 312
       id: newId,
306 313
       name: newCategoryName.value,
307 314
       updateTime: new Date().toISOString().split('T')[0],
308
-      count: 0
315
+      count: 0,
309 316
     });
310 317
   }
311
-  
318
+
312 319
   closeModal();
313 320
 };
314 321
 
@@ -404,18 +411,19 @@ onUnmounted(() => {
404 411
   if (categoryListRef.value) {
405 412
     categoryListRef.value.removeEventListener('scroll', handleScroll);
406 413
   }
407
-}); 
414
+});
408 415
 </script>
409 416
 
410 417
 <template>
411 418
   <Page>
412
-  
413 419
     <div class="question-bank-container">
414 420
       <!-- 左侧题库分类 -->
415 421
       <div class="left-panel">
416 422
         <div class="panel-header">
417 423
           <h2>题库分类</h2>
418
-          <button class="add-btn" @click="openAddCategoryModal">添加题目类型</button>
424
+          <button class="add-btn" @click="openAddCategoryModal">
425
+            添加题目类型
426
+          </button>
419 427
         </div>
420 428
         <div class="category-list" ref="categoryListRef">
421 429
           <!-- @ts-ignore -->
@@ -432,8 +440,18 @@ onUnmounted(() => {
432 440
               <span>题库数量: {{ category.count }}</span>
433 441
             </div>
434 442
             <div class="category-actions">
435
-              <button class="edit-category-btn" @click.stop="openEditCategoryModal(category)">修改</button>
436
-              <button class="delete-category-btn" @click.stop="deleteCategory(category.id)">删除</button>
443
+              <button
444
+                class="edit-category-btn"
445
+                @click.stop="openEditCategoryModal(category)"
446
+              >
447
+                修改
448
+              </button>
449
+              <button
450
+                class="delete-category-btn"
451
+                @click.stop="deleteCategory(category.id)"
452
+              >
453
+                删除
454
+              </button>
437 455
             </div>
438 456
           </div>
439 457
 
@@ -539,8 +557,6 @@ onUnmounted(() => {
539 557
               </div>
540 558
             </div>
541 559
           </div>
542
-
543
-
544 560
         </div>
545 561
       </div>
546 562
     </div>
@@ -553,205 +569,220 @@ onUnmounted(() => {
553 569
         <button class="close-btn" @click="closeModal">&times;</button>
554 570
       </div>
555 571
       <div class="drawer-body">
556
-          <!-- 题目类型表单 -->
557
-          <div v-if="modalTitle.includes('题目类型')" class="category-form">
558
-            <div class="form-item">
559
-              <label class="form-label">题目类型名称</label>
560
-              <input 
561
-                type="text" 
562
-                class="form-input" 
563
-                v-model="newCategoryName" 
564
-                placeholder="请输入题目类型名称(最多50个字)"
565
-                maxlength="50"
566
-                @input="validateInput(newCategoryName)"
567
-              />
568
-              <div v-if="inputError" class="error-message">{{ inputError }}</div>
569
-              <div class="char-count">{{ newCategoryName.length }}/50</div>
570
-            </div>
572
+        <!-- 题目类型表单 -->
573
+        <div v-if="modalTitle.includes('题目类型')" class="category-form">
574
+          <div class="form-item">
575
+            <label class="form-label">题目类型名称</label>
576
+            <input
577
+              type="text"
578
+              class="form-input"
579
+              v-model="newCategoryName"
580
+              placeholder="请输入题目类型名称(最多50个字)"
581
+              maxlength="50"
582
+              @input="validateInput(newCategoryName)"
583
+            />
584
+            <div v-if="inputError" class="error-message">{{ inputError }}</div>
585
+            <div class="char-count">{{ newCategoryName.length }}/50</div>
571 586
           </div>
572
-          <!-- 题目编辑表单 -->
573
-          <div v-else-if="modalTitle === '编辑题目' && currentQuestion" class="question-form">
574
-            <!-- 题目类型 -->
575
-            <div class="form-item">
576
-              <label class="form-label">题目类型</label>
577
-              <div class="type-tabs">
578
-                <button 
579
-                  v-for="type in questionTypes" 
580
-                  :key="type.value"
581
-                  class="type-tab"
582
-                  :class="{ active: currentQuestion.type === type.value }"
583
-                  @click="currentQuestion.type = type.value as QuestionType; 
584
-                          if ((type.value === 'single_choice' || type.value === 'multiple_choice' || type.value === 'true_false') && !currentQuestion.options) { 
585
-                            currentQuestion.options = type.value === 'true_false' ? ['正确', '错误'] : ['']; 
586
-                          } else if (type.value !== 'single_choice' && type.value !== 'multiple_choice' && type.value !== 'true_false') { 
587
-                            currentQuestion.options = undefined; 
588
-                          }"
589
-                >
590
-                  {{ type.label }}
591
-                </button>
592
-              </div>
593
-            </div>
594
-            
595
-            <!-- 题目内容 -->
596
-            <div class="form-item">
597
-              <label class="form-label">题目内容</label>
598
-              <textarea 
599
-                class="form-textarea" 
600
-                v-model="currentQuestion.content" 
601
-                placeholder="请输入题目内容"
602
-              ></textarea>
587
+        </div>
588
+        <!-- 题目编辑表单 -->
589
+        <div
590
+          v-else-if="modalTitle === '编辑题目' && currentQuestion"
591
+          class="question-form"
592
+        >
593
+          <!-- 题目类型 -->
594
+          <div class="form-item">
595
+            <label class="form-label">题目类型</label>
596
+            <div class="type-tabs">
597
+              <button
598
+                v-for="type in questionTypes"
599
+                :key="type.value"
600
+                class="type-tab"
601
+                :class="{ active: currentQuestion.type === type.value }"
602
+                @click="
603
+                  currentQuestion.type = type.value as QuestionType;
604
+                  if (
605
+                    (type.value === 'single_choice' ||
606
+                      type.value === 'multiple_choice' ||
607
+                      type.value === 'true_false') &&
608
+                    !currentQuestion.options
609
+                  ) {
610
+                    currentQuestion.options =
611
+                      type.value === 'true_false' ? ['正确', '错误'] : [''];
612
+                  } else if (
613
+                    type.value !== 'single_choice' &&
614
+                    type.value !== 'multiple_choice' &&
615
+                    type.value !== 'true_false'
616
+                  ) {
617
+                    currentQuestion.options = undefined;
618
+                  }
619
+                "
620
+              >
621
+                {{ type.label }}
622
+              </button>
603 623
             </div>
604
-            
605
-            <!-- 选项 -->
606
-            <div v-if="currentQuestion.options" class="form-item">
607
-              <label class="form-label">选项</label>
608
-              <div class="options-list">
609
-                <div 
610
-                  v-for="(option, index) in currentQuestion.options" 
611
-                  :key="index"
612
-                  class="option-item"
613
-                >
614
-                  <input 
615
-                    type="text" 
616
-                    class="form-input"
617
-                    v-model="currentQuestion.options![index]"
618
-                    placeholder="请输入选项内容"
619
-                  />
620
-                  <button 
621
-                    class="remove-option-btn"
622
-                    @click="removeOption(currentQuestion, index)"
623
-                    :disabled="currentQuestion.options.length <= 1"
624
-                  >
625
-                    删除
626
-                  </button>
627
-                </div>
628
-                <button 
629
-                  class="add-option-btn"
630
-                  @click="addOption(currentQuestion)"
624
+          </div>
625
+
626
+          <!-- 题目内容 -->
627
+          <div class="form-item">
628
+            <label class="form-label">题目内容</label>
629
+            <textarea
630
+              class="form-textarea"
631
+              v-model="currentQuestion.content"
632
+              placeholder="请输入题目内容"
633
+            ></textarea>
634
+          </div>
635
+
636
+          <!-- 选项 -->
637
+          <div v-if="currentQuestion.options" class="form-item">
638
+            <label class="form-label">选项</label>
639
+            <div class="options-list">
640
+              <div
641
+                v-for="(option, index) in currentQuestion.options"
642
+                :key="index"
643
+                class="option-item"
644
+              >
645
+                <input
646
+                  type="text"
647
+                  class="form-input"
648
+                  v-model="currentQuestion.options![index]"
649
+                  placeholder="请输入选项内容"
650
+                />
651
+                <button
652
+                  class="remove-option-btn"
653
+                  @click="removeOption(currentQuestion, index)"
654
+                  :disabled="currentQuestion.options.length <= 1"
631 655
                 >
632
-                  + 添加选项
656
+                  删除
633 657
                 </button>
634 658
               </div>
659
+              <button
660
+                class="add-option-btn"
661
+                @click="addOption(currentQuestion)"
662
+              >
663
+                + 添加选项
664
+              </button>
635 665
             </div>
636
-            
637
-            <!-- 答案 -->
638
-            <div class="form-item">
639
-              <label class="form-label">答案</label>
640
-              <textarea 
641
-                class="form-textarea" 
642
-                v-model="currentQuestion.answer" 
643
-                placeholder="请输入答案"
644
-              ></textarea>
666
+          </div>
667
+
668
+          <!-- 答案 -->
669
+          <div class="form-item">
670
+            <label class="form-label">答案</label>
671
+            <textarea
672
+              class="form-textarea"
673
+              v-model="currentQuestion.answer"
674
+              placeholder="请输入答案"
675
+            ></textarea>
676
+          </div>
677
+        </div>
678
+
679
+        <!-- 添加题目表单 -->
680
+        <div v-else-if="modalTitle === '添加题目'" class="question-form">
681
+          <!-- 题目类型选择 -->
682
+          <div class="form-item">
683
+            <label class="form-label">选择题目类型</label>
684
+            <div class="type-tabs">
685
+              <button
686
+                v-for="type in questionTypes"
687
+                :key="type.value"
688
+                class="type-tab"
689
+                @click="addNewQuestion(type.value as QuestionType)"
690
+              >
691
+                {{ type.label }}
692
+              </button>
645 693
             </div>
646 694
           </div>
647
-          
648
-          <!-- 添加题目表单 -->
649
-          <div v-else-if="modalTitle === '添加题目'" class="question-form">
650
-            <!-- 题目类型选择 -->
651
-            <div class="form-item">
652
-              <label class="form-label">选择题目类型</label>
653
-              <div class="type-tabs">
654
-                <button 
655
-                  v-for="type in questionTypes" 
656
-                  :key="type.value"
657
-                  class="type-tab"
658
-                  @click="addNewQuestion(type.value as QuestionType)"
695
+
696
+          <!-- 所有题目编辑区域 -->
697
+          <div v-if="newQuestions.length > 0" class="all-questions">
698
+            <div
699
+              v-for="(question, qIndex) in newQuestions"
700
+              :key="qIndex"
701
+              class="question-item-container"
702
+            >
703
+              <div class="question-header">
704
+                <h4>
705
+                  题目{{ qIndex + 1 }} {{ getQuestionTypeLabel(question.type) }}
706
+                </h4>
707
+                <button
708
+                  class="delete-question-btn"
709
+                  @click="newQuestions.splice(qIndex, 1)"
659 710
                 >
660
-                  {{ type.label }}
711
+                  删除题目
661 712
                 </button>
662 713
               </div>
663
-            </div>
664
-            
665
-            <!-- 所有题目编辑区域 -->
666
-            <div v-if="newQuestions.length > 0" class="all-questions">
667
-              <div 
668
-                v-for="(question, qIndex) in newQuestions" 
669
-                :key="qIndex"
670
-                class="question-item-container"
671
-              >
672
-                <div class="question-header">
673
-                  <h4>题目{{ qIndex + 1 }} {{ getQuestionTypeLabel(question.type) }}</h4>
674
-                  <button 
675
-                    class="delete-question-btn"
676
-                    @click="newQuestions.splice(qIndex, 1)"
714
+
715
+              <div class="form-item">
716
+                <label class="form-label">题目内容</label>
717
+                <textarea
718
+                  class="form-textarea"
719
+                  v-model="question.content"
720
+                  placeholder="请输入题目内容"
721
+                ></textarea>
722
+              </div>
723
+
724
+              <!-- 选项 -->
725
+              <div v-if="question.options" class="form-item">
726
+                <label class="form-label">选项</label>
727
+                <div class="options-list">
728
+                  <div
729
+                    v-for="(option, optIndex) in question.options"
730
+                    :key="optIndex"
731
+                    class="option-item"
677 732
                   >
678
-                    删除题目
679
-                  </button>
680
-                </div>
681
-                
682
-                <div class="form-item">
683
-                  <label class="form-label">题目内容</label>
684
-                  <textarea 
685
-                    class="form-textarea" 
686
-                    v-model="question.content" 
687
-                    placeholder="请输入题目内容"
688
-                  ></textarea>
689
-                </div>
690
-                
691
-                <!-- 选项 -->
692
-                <div v-if="question.options" class="form-item">
693
-                  <label class="form-label">选项</label>
694
-                  <div class="options-list">
695
-                    <div 
696
-                      v-for="(option, optIndex) in question.options" 
697
-                      :key="optIndex"
698
-                      class="option-item"
699
-                    >
700
-                      <input 
701
-                        type="text" 
702
-                        class="form-input"
703
-                        v-model="question.options![optIndex]"
704
-                        placeholder="请输入选项内容"
705
-                      />
706
-                      <button 
707
-                        class="remove-option-btn"
708
-                        @click="removeOption(question, optIndex)"
709
-                        :disabled="question.options.length <= 1"
710
-                      >
711
-                        删除
712
-                      </button>
713
-                    </div>
714
-                    <button 
715
-                      class="add-option-btn"
716
-                      @click="addOption(question)"
733
+                    <input
734
+                      type="text"
735
+                      class="form-input"
736
+                      v-model="question.options![optIndex]"
737
+                      placeholder="请输入选项内容"
738
+                    />
739
+                    <button
740
+                      class="remove-option-btn"
741
+                      @click="removeOption(question, optIndex)"
742
+                      :disabled="question.options.length <= 1"
717 743
                     >
718
-                      + 添加选项
744
+                      删除
719 745
                     </button>
720 746
                   </div>
721
-                </div>
722
-                
723
-                <!-- 答案 -->
724
-                <div class="form-item">
725
-                  <label class="form-label">答案</label>
726
-                  <textarea 
727
-                    class="form-textarea" 
728
-                    v-model="question.answer" 
729
-                    placeholder="请输入答案"
730
-                  ></textarea>
747
+                  <button class="add-option-btn" @click="addOption(question)">
748
+                    + 添加选项
749
+                  </button>
731 750
                 </div>
732 751
               </div>
752
+
753
+              <!-- 答案 -->
754
+              <div class="form-item">
755
+                <label class="form-label">答案</label>
756
+                <textarea
757
+                  class="form-textarea"
758
+                  v-model="question.answer"
759
+                  placeholder="请输入答案"
760
+                ></textarea>
761
+              </div>
733 762
             </div>
734 763
           </div>
735
-        
736
-      </div>
737
-      <div class="drawer-footer">
764
+        </div>
765
+        <div class="drawer-footer">
738 766
           <button class="cancel-btn" @click="closeModal">取消</button>
739
-          <button 
740
-            class="confirm-btn" 
741
-            @click="modalTitle.includes('题目类型') ? saveCategory() : 
742
-                  modalTitle.includes('编辑') ? saveSingleQuestion() : saveAllQuestions()"
767
+          <button
768
+            class="confirm-btn"
769
+            @click="
770
+              modalTitle.includes('题目类型')
771
+                ? saveCategory()
772
+                : modalTitle.includes('编辑')
773
+                  ? saveSingleQuestion()
774
+                  : saveAllQuestions()
775
+            "
743 776
           >
744 777
             确定
745 778
           </button>
779
+        </div>
746 780
       </div>
747 781
     </div>
748
-    
749
-  
750 782
   </Page>
751 783
 </template>
752 784
 
753 785
 <style lang="scss" scoped>
754
-
755 786
 @keyframes fadeIn {
756 787
   from {
757 788
     opacity: 0;
@@ -876,7 +907,8 @@ onUnmounted(() => {
876 907
   opacity: 1;
877 908
 }
878 909
 
879
-.edit-category-btn, .delete-category-btn {
910
+.edit-category-btn,
911
+.delete-category-btn {
880 912
   padding: 4px 8px;
881 913
   font-size: 12px;
882 914
   cursor: pointer;
@@ -888,7 +920,7 @@ onUnmounted(() => {
888 920
 .edit-category-btn {
889 921
   color: #1890ff;
890 922
   background-color: rgb(24 144 255 / 10%);
891
-  
923
+
892 924
   &:hover {
893 925
     background-color: rgb(24 144 255 / 20%);
894 926
   }
@@ -897,7 +929,7 @@ onUnmounted(() => {
897 929
 .delete-category-btn {
898 930
   color: #ff4d4f;
899 931
   background-color: rgb(255 77 79 / 10%);
900
-  
932
+
901 933
   &:hover {
902 934
     background-color: rgb(255 77 79 / 20%);
903 935
   }
@@ -1072,8 +1104,6 @@ onUnmounted(() => {
1072 1104
   }
1073 1105
 }
1074 1106
 
1075
-
1076
-
1077 1107
 /* 滚动条样式优化 */
1078 1108
 .category-list::-webkit-scrollbar,
1079 1109
 .question-list::-webkit-scrollbar,
@@ -1198,7 +1228,7 @@ onUnmounted(() => {
1198 1228
   border: 1px solid #d9d9d9;
1199 1229
   border-radius: 4px;
1200 1230
   transition: all 0.3s;
1201
-  
1231
+
1202 1232
   &:focus {
1203 1233
     border-color: #1890ff;
1204 1234
     outline: none;
@@ -1215,7 +1245,7 @@ onUnmounted(() => {
1215 1245
   border: 1px solid #d9d9d9;
1216 1246
   border-radius: 4px;
1217 1247
   transition: all 0.3s;
1218
-  
1248
+
1219 1249
   &:focus {
1220 1250
     border-color: #1890ff;
1221 1251
     outline: none;
@@ -1253,12 +1283,12 @@ onUnmounted(() => {
1253 1283
   border: 1px solid #d9d9d9;
1254 1284
   border-radius: 4px;
1255 1285
   transition: all 0.3s;
1256
-  
1286
+
1257 1287
   &:hover {
1258 1288
     color: #1890ff;
1259 1289
     border-color: #1890ff;
1260 1290
   }
1261
-  
1291
+
1262 1292
   &.active {
1263 1293
     color: #1890ff;
1264 1294
     background-color: #e6f7ff;
@@ -1331,11 +1361,11 @@ onUnmounted(() => {
1331 1361
   border: 1px solid #ffccc7;
1332 1362
   border-radius: 4px;
1333 1363
   transition: all 0.3s;
1334
-  
1364
+
1335 1365
   &:hover:not(:disabled) {
1336 1366
     background-color: #ffccc7;
1337 1367
   }
1338
-  
1368
+
1339 1369
   &:disabled {
1340 1370
     cursor: not-allowed;
1341 1371
     opacity: 0.5;
@@ -1351,7 +1381,7 @@ onUnmounted(() => {
1351 1381
   border: none;
1352 1382
   border-radius: 4px;
1353 1383
   transition: all 0.3s;
1354
-  
1384
+
1355 1385
   &:hover {
1356 1386
     color: #40a9ff;
1357 1387
     text-decoration: underline;

+ 1 - 1
packages/effects/plugins/src/vxe-table/use-vxe-grid.vue

@@ -129,7 +129,7 @@ const toolbarOptions = computed(() => {
129 129
     tools: (gridOptions.value?.toolbarConfig?.tools ??
130 130
       []) as VxeToolbarPropTypes.ToolConfig[],
131 131
   };
132
-  if (gridOptions.value?.toolbarConfig?.search && !!formOptions.value) {
132
+  if (gridOptions.value?.toolbarConfig?.search !== false && !!formOptions.value) {
133 133
     toolbarConfig.tools = Array.isArray(toolbarConfig.tools)
134 134
       ? [...toolbarConfig.tools, searchBtn]
135 135
       : [searchBtn];