闪电 vor 1 Monat
Ursprung
Commit
9ef30d9483
22 geänderte Dateien mit 2191 neuen und 2165 gelöschten Zeilen
  1. 1 1
      apps/web-ele/.env.development
  2. 54 0
      apps/web-ele/src/api/exam/exam/category.ts
  3. 63 0
      apps/web-ele/src/api/exam/exam/exam.ts
  4. 6 0
      apps/web-ele/src/api/system/user/user.ts
  5. 1 0
      apps/web-ele/src/components/selectUsers/index.ts
  6. 1030 0
      apps/web-ele/src/components/selectUsers/src/index.vue
  7. 8 0
      apps/web-ele/src/utils/dicts.ts
  8. 1 3
      apps/web-ele/src/utils/render.tsx
  9. 56 0
      apps/web-ele/src/views/examManage/exam/config-data.tsx
  10. 54 0
      apps/web-ele/src/views/examManage/exam/cpn/chapter/config-data.tsx
  11. 92 0
      apps/web-ele/src/views/examManage/exam/cpn/chapter/drawer.vue
  12. 560 0
      apps/web-ele/src/views/examManage/exam/cpn/edit/drawer.vue
  13. 0 0
      apps/web-ele/src/views/examManage/exam/cpns/examPreview.vue
  14. 196 331
      apps/web-ele/src/views/examManage/examPaper/index.vue
  15. 0 948
      apps/web-ele/src/views/examManage/examPaper/cpn/examEdit.vue
  16. 0 212
      apps/web-ele/src/views/examManage/examPaper/cpn/examPreview.vue
  17. 0 526
      apps/web-ele/src/views/examManage/paperManage/cpn/paper-drawer.vue
  18. 10 1
      apps/web-ele/src/views/examManage/questionBank/cpn/chapter/config-data.tsx
  19. 6 2
      apps/web-ele/src/views/examManage/questionBank/cpn/chapter/drawer.vue
  20. 1 1
      apps/web-ele/src/views/examManage/questionBank/cpn/questionEdit/index.vue
  21. 0 118
      apps/web-ele/src/views/examManage/questionBank/cpn/questionPreview/index.vue
  22. 52 22
      apps/web-ele/src/views/examManage/questionBank/index.vue

+ 1 - 1
apps/web-ele/.env.development

@@ -5,7 +5,7 @@ VITE_BASE=/
5 5
 
6 6
 # 接口地址
7 7
 
8
-VITE_GLOB_API_URL=http://39.164.159.226:8088
8
+VITE_GLOB_API_URL=http://localhost:8080
9 9
 # VITE_GLOB_API_URL=http://192.168.1.7:8080
10 10
 # VITE_GLOB_API_URL=http://192.168.1.170:8080
11 11
 # VITE_GLOB_FLOW_URL=http://192.168.31.160:8082

+ 54 - 0
apps/web-ele/src/api/exam/exam/category.ts

@@ -0,0 +1,54 @@
1
+import { requestClient } from '#/api/request';
2
+
3
+enum Api {
4
+  base = '/examCategory/category',
5
+  tree = '/examCategory/category/list',
6
+}
7
+
8
+export interface ExamCategory {
9
+  children: ExamCategory[];
10
+  id: number;
11
+  name: string;
12
+  parentId: number;
13
+  icon: string;
14
+  description: string;
15
+  sortOrder: number;
16
+  delFlag: string;
17
+}
18
+
19
+/**
20
+ * 获取试题分类树形图
21
+ */
22
+async function examCategoryTree() {
23
+  return requestClient.get<ExamCategory[]>(Api.tree);
24
+}
25
+
26
+async function examCategoryOne(id: number) {
27
+  return requestClient.get<ExamCategory>(`${Api.base}/${id}`);
28
+}
29
+// 新增试题分类
30
+function addExamCategory(category: ExamCategory) {
31
+  return requestClient.post(Api.base, category, {
32
+    successMessageMode: 'message',
33
+  });
34
+}
35
+// 修改试题分类
36
+function updateExamCategory(category: ExamCategory) {
37
+  return requestClient.put(Api.base, category, {
38
+    successMessageMode: 'message',
39
+  });
40
+}
41
+// 删除试题分类
42
+function deleteExamCategory(categoryId: number) {
43
+  return requestClient.delete(`${Api.base}/${categoryId}`, {
44
+    successMessageMode: 'message',
45
+  });
46
+}
47
+
48
+export {
49
+  addExamCategory,
50
+  deleteExamCategory,
51
+  examCategoryOne,
52
+  examCategoryTree,
53
+  updateExamCategory,
54
+};

+ 63 - 0
apps/web-ele/src/api/exam/exam/exam.ts

@@ -0,0 +1,63 @@
1
+import { requestClient } from '#/api/request';
2
+
3
+enum Api {
4
+  base = '/exam/exam',
5
+  list = '/exam/exam/list',
6
+}
7
+
8
+export interface Exam {
9
+  id: number;
10
+  categoryId: number;
11
+  categoryName: string;
12
+  examName: string;
13
+  paperId: number;
14
+  examStartTime: string;
15
+  examEndTime: string;
16
+  examDuration: number;
17
+  passScore: number;
18
+  isLimit: number;
19
+  isSubjective: number;
20
+  examStatus: string;
21
+  createTime: string;
22
+  updateTime: string;
23
+  delFlag: string;
24
+}
25
+
26
+/**
27
+ * 获取试卷列表
28
+ */
29
+async function examList(params?: any) {
30
+  return requestClient.get<Exam>(Api.list, { params });
31
+}
32
+
33
+/**
34
+ * 获取试卷详情
35
+ */
36
+async function examDetail(id: number) {
37
+  return requestClient.get<Exam>(`${Api.base}/${id}`);
38
+}
39
+
40
+/**
41
+ * 新增试卷
42
+ */
43
+function addExam(params: any) {
44
+  return requestClient.post(Api.base, params, {
45
+    successMessageMode: 'message',
46
+  });
47
+}
48
+/**
49
+ * 修改试卷
50
+ */
51
+function updateExam(params: any) {
52
+  return requestClient.put(Api.base, params, {
53
+    successMessageMode: 'message',
54
+  });
55
+}
56
+// 删除试卷分类
57
+function deleteExam(ids: number[]) {
58
+  return requestClient.delete(`${Api.base}/${ids.join(',')}`, {
59
+    successMessageMode: 'message',
60
+  });
61
+}
62
+
63
+export { addExam, deleteExam, examDetail, examList, updateExam };

+ 6 - 0
apps/web-ele/src/api/system/user/user.ts

@@ -23,6 +23,7 @@ enum Api {
23 23
   exportUser = '/system/user/export',
24 24
   menuList = '/system/user/list',
25 25
   modifyPwd = '/system/user/resetPwd',
26
+  selectUsersApi = '/system/user/list',
26 27
   updateAvatar = '/system/user/profile/avatar',
27 28
   updateProfilePwd = '/system/user/profile/updatePwd',
28 29
   updateUser = '/system/user/changeStatus',
@@ -87,6 +88,11 @@ export function addUser(user: User) {
87 88
     successMessageMode: 'message',
88 89
   });
89 90
 }
91
+
92
+export function selectUserList(params?: any) {
93
+  return requestClient.post<BaseResult<User[]>>(Api.selectUsersApi, { params });
94
+}
95
+
90 96
 // 批量删除用户
91 97
 export function deleteUsers(ids: number[]) {
92 98
   return requestClient.delete(`${Api.baseUser}/${ids}`, {

+ 1 - 0
apps/web-ele/src/components/selectUsers/index.ts

@@ -0,0 +1 @@
1
+export { default, default as SelectUsers } from './src/index.vue';

Datei-Diff unterdrückt, da er zu groß ist
+ 1030 - 0
apps/web-ele/src/components/selectUsers/src/index.vue


+ 8 - 0
apps/web-ele/src/utils/dicts.ts

@@ -16,6 +16,14 @@ export const difficultyOptions = [
16 16
   { label: '困难', value: 'hard', color: 'red' },
17 17
 ];
18 18
 
19
+// p=pending-待发布、ongoing-已发布、finished-已结束、cancelled-已关闭
20
+export const examStatusOptions = [
21
+  { label: '已发布', value: 'ongoing', color: 'success' },
22
+  { label: '待发布', value: 'pending', color: 'primary' },
23
+  { label: '已关闭', value: 'cancelled', color: 'danger' },
24
+  { label: '已结束', value: 'finished', color: 'info' },
25
+];
26
+
19 27
 export const getLabel = (dict: any[], value: string) => {
20 28
   const item = dict.find((item) => item.value === value);
21 29
   return item?.label || value;

+ 1 - 3
apps/web-ele/src/utils/render.tsx

@@ -39,7 +39,7 @@ import { getDictOptions } from './dict';
39 39
  * @param color 颜色
40 40
  * @returns render
41 41
  */
42
-function renderTag(text: string, color?: string) {
42
+export function renderTag(text: string, color?: string) {
43 43
   return <ElTag color={color}>{text}</ElTag>;
44 44
 }
45 45
 
@@ -234,5 +234,3 @@ export function renderBrowserIcon(browser: string, center = false) {
234 234
   const icon = current ? current.icon : DefaultBrowserIcon;
235 235
   return renderIconSpan(icon, browser, center, '5px');
236 236
 }
237
-
238
-

+ 56 - 0
apps/web-ele/src/views/examManage/exam/config-data.tsx

@@ -0,0 +1,56 @@
1
+import type { FormSchemaGetter } from '#/adapter/form';
2
+import type { VxeGridProps } from '#/adapter/vxe-table';
3
+
4
+import { examStatusOptions } from '#/utils/dicts';
5
+import { renderTag } from '#/utils/render';
6
+
7
+/**
8
+ * 查询表单schema
9
+ */
10
+export const queryFormSchema: FormSchemaGetter = () => [
11
+  {
12
+    component: 'Input',
13
+    fieldName: 'examName',
14
+    label: '考试名称',
15
+    componentProps: {
16
+      placeholder: '请输入考试名称',
17
+    },
18
+  },
19
+];
20
+
21
+/**
22
+ * 表格列配置
23
+ */
24
+export const tableColumns: VxeGridProps['columns'] = [
25
+  { type: 'checkbox', width: 80 },
26
+  { field: 'examName', title: '考试名称', minWidth: 200 },
27
+  { field: 'categoryName', title: '类别', minWidth: 120 },
28
+  { field: 'examStartTime', title: '开始时间', minWidth: 120 },
29
+  { field: 'examEndTime', title: '结束时间', minWidth: 120 },
30
+  { field: 'examDuration', title: '考试时长', minWidth: 120 },
31
+  { field: 'passScore', title: '及格分数线', minWidth: 120 },
32
+  { field: 'createBy', title: '创建人', minWidth: 120 },
33
+  { field: 'totalScore', title: '考试总分', minWidth: 100 },
34
+  { field: 'questionCount', title: '试题总数', minWidth: 100 },
35
+  { field: 'updateTime', title: '更新日期', minWidth: 120 },
36
+  {
37
+    field: 'examStatus',
38
+    title: '考试状态',
39
+    minWidth: 120,
40
+    slots: {
41
+      default: ({ row }) => {
42
+        const status = examStatusOptions.find(
43
+          (item) => item.value === row.examStatus,
44
+        );
45
+        return renderTag(status?.label || row.examStatus, status?.color);
46
+      },
47
+    },
48
+  },
49
+  {
50
+    field: 'action',
51
+    fixed: 'right',
52
+    slots: { default: 'action' },
53
+    title: '操作',
54
+    width: 240,
55
+  },
56
+];

+ 54 - 0
apps/web-ele/src/views/examManage/exam/cpn/chapter/config-data.tsx

@@ -0,0 +1,54 @@
1
+import type { FormSchemaGetter } from '#/adapter/form';
2
+
3
+import { examCategoryTree } from '#/api/exam/exam/category';
4
+
5
+export const drawerFormSchema: FormSchemaGetter = () => [
6
+  {
7
+    component: 'Input',
8
+    dependencies: {
9
+      show: () => false,
10
+      triggerFields: [''],
11
+    },
12
+    fieldName: 'id',
13
+  },
14
+  {
15
+    component: 'ApiTreeSelect',
16
+    componentProps: {
17
+      api: async () => {
18
+        const data: any = await examCategoryTree();
19
+        // 增加一个根目录
20
+        data.rows = [
21
+          {
22
+            id: 0,
23
+            name: '根目录',
24
+            children: [],
25
+          },
26
+          ...data.rows,
27
+        ];
28
+        return data.rows;
29
+      },
30
+      labelField: 'name',
31
+      valueField: 'id',
32
+      childrenField: 'children',
33
+      checkStrictly: true,
34
+    },
35
+    dependencies: {
36
+      if(values: any) {
37
+        return values.parentId !== 0 || !values.isUpdate;
38
+      },
39
+      triggerFields: ['parentId'],
40
+    },
41
+    fieldName: 'parentId',
42
+    label: '上级分类',
43
+    rules: 'selectRequired',
44
+  },
45
+  {
46
+    component: 'Input',
47
+    fieldName: 'name',
48
+    componentProps: {
49
+      maxlength: 30,
50
+    },
51
+    label: '分类名称',
52
+    rules: 'required',
53
+  },
54
+];

+ 92 - 0
apps/web-ele/src/views/examManage/exam/cpn/chapter/drawer.vue

@@ -0,0 +1,92 @@
1
+<script setup lang="ts">
2
+// @ts-ignore
3
+import type { Department } from '#/api/system/dept/model';
4
+
5
+import { ref } from 'vue';
6
+
7
+// @ts-ignore
8
+import { useVbenDrawer } from '@vben/common-ui';
9
+
10
+// @ts-ignore
11
+import { useVbenForm } from '#/adapter/form';
12
+import {
13
+  addExamCategory,
14
+  examCategoryOne,
15
+  updateExamCategory,
16
+  // @ts-ignore
17
+} from '#/api/exam/exam/category';
18
+
19
+import { drawerFormSchema } from './config-data';
20
+
21
+const emit = defineEmits<{ reload: [] }>();
22
+
23
+const [Form, formApi] = useVbenForm({
24
+  // 不显示提交和重置按钮
25
+  showDefaultActions: false,
26
+  // 垂直布局,label和input在不同行,值为vertical
27
+  // 水平布局,label和input在同一行
28
+  layout: 'horizontal',
29
+  schema: drawerFormSchema(),
30
+});
31
+const isUpdateRef = ref<boolean>(false);
32
+const [Drawer, drawerApi] = useVbenDrawer({
33
+  async onOpenChange(isOpen: any) {
34
+    if (!isOpen) {
35
+      return;
36
+    }
37
+    try {
38
+      drawerApi.drawerLoading(true);
39
+      let { id, isUpdate, parentId } = drawerApi.getData();
40
+      isUpdateRef.value = isUpdate;
41
+      if (isUpdate) {
42
+        const deptData = await examCategoryOne(id);
43
+        await formApi.setValues({ ...deptData, isUpdate });
44
+      } else {
45
+        // 如果父节点是0的话说明是根节点,将本节点id复制给根节点用于侧拉显示根节点。
46
+        if (parentId === 0) {
47
+          parentId = id;
48
+        }
49
+        await formApi.setValues({ parentId, isUpdate });
50
+      }
51
+    } catch (error) {
52
+      console.error(error);
53
+    } finally {
54
+      drawerApi.drawerLoading(false);
55
+    }
56
+  },
57
+  onClosed() {
58
+    formApi.resetForm();
59
+  },
60
+  async onConfirm() {
61
+    const { valid } = await formApi.validate();
62
+    if (!valid) {
63
+      return;
64
+    }
65
+    try {
66
+      this.confirmLoading = true;
67
+      const data = await formApi.getValues<Department>();
68
+      // 动态判断调用新增还是修改
69
+      isUpdateRef.value
70
+        ? await updateExamCategory(data)
71
+        : await addExamCategory(data);
72
+      emit('reload');
73
+      drawerApi.close();
74
+    } catch (error) {
75
+      console.error(error);
76
+    } finally {
77
+      this.confirmLoading = false;
78
+    }
79
+  },
80
+});
81
+</script>
82
+
83
+<template>
84
+  <Drawer :title="isUpdateRef ? '编辑' : '新增'">
85
+    <Form />
86
+  </Drawer>
87
+</template>
88
+<style scoped lang="scss">
89
+:deep(.el-input-number) {
90
+  width: 100%;
91
+}
92
+</style>

+ 560 - 0
apps/web-ele/src/views/examManage/exam/cpn/edit/drawer.vue

@@ -0,0 +1,560 @@
1
+<script lang="ts" setup>
2
+import { computed, onMounted, reactive, ref } from 'vue';
3
+
4
+// @ts-ignore
5
+import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
6
+
7
+import dayjs from 'dayjs';
8
+import { ElMessage } from 'element-plus';
9
+
10
+import {
11
+  examCategoryTree,
12
+  // @ts-ignore
13
+} from '#/api/exam/exam/category';
14
+// @ts-ignore
15
+import { addExam, examDetail, updateExam } from '#/api/exam/exam/exam';
16
+import {
17
+  PaperCategoryTree,
18
+  // @ts-ignore
19
+} from '#/api/exam/paper/category';
20
+// @ts-ignore
21
+import { paperList } from '#/api/exam/paper/paper';
22
+// @ts-ignore
23
+import { SelectUsers } from '#/components/selectUsers';
24
+
25
+const emit = defineEmits<{ reload: [] }>();
26
+
27
+// 抽屉可见性
28
+const drawerVisible = ref(false);
29
+
30
+const isUpdateRef = ref<boolean>(false);
31
+const [Drawer, drawerApi] = useVbenDrawer({
32
+  async onOpenChange(isOpen: any) {
33
+    if (!isOpen) {
34
+      return;
35
+    }
36
+    try {
37
+      drawerApi.drawerLoading(true);
38
+      let { id, isUpdate, parentId } = drawerApi.getData();
39
+      isUpdateRef.value = isUpdate;
40
+      if (isUpdate) {
41
+        const examInfo: any = await examDetail(id);
42
+        if (examInfo) {
43
+          examForm = {
44
+            ...examInfo,
45
+            selectExecutors: examInfo.users || [],
46
+            executorCount: examInfo.users?.length || 0,
47
+          };
48
+
49
+          // 试卷列表
50
+          const paperListRes: any = await paperList({
51
+            id: examInfo.paperId,
52
+          });
53
+          if (paperListRes?.rows?.length) {
54
+            selectedPapers.value = paperListRes.rows || [];
55
+          }
56
+        }
57
+      } else {
58
+        // 如果父节点是0的话说明是根节点,将本节点id复制给根节点用于侧拉显示根节点。
59
+        if (parentId === 0) {
60
+          parentId = id;
61
+        }
62
+      }
63
+    } catch (error) {
64
+      console.error(error);
65
+    } finally {
66
+      drawerApi.drawerLoading(false);
67
+    }
68
+  },
69
+  onClosed() {
70
+    // formApi.resetForm();
71
+  },
72
+  async onConfirm() {
73
+    try {
74
+      this.confirmLoading = true;
75
+
76
+      // 表单验证
77
+      if (!examForm.examName) {
78
+        ElMessage.error('请输入考试名称');
79
+        return;
80
+      }
81
+      if (!examForm.categoryId) {
82
+        ElMessage.error('请选择考试分类');
83
+        return;
84
+      }
85
+      if (!examForm.passScore) {
86
+        ElMessage.error('请输入及格分');
87
+        return;
88
+      }
89
+      if (examForm.paperId === '') {
90
+        ElMessage.error('请选择试卷');
91
+        return;
92
+      }
93
+
94
+      const params: any = {
95
+        categoryId: examForm.categoryId,
96
+        passScore: examForm.passScore,
97
+        paperId: examForm.paperId,
98
+        examName: examForm.examName,
99
+        remark: examForm.remark,
100
+        examStartTime: examForm.examStartTime
101
+          ? dayjs(examForm.examStartTime).format('YYYY-MM-DD HH:mm:ss')
102
+          : '',
103
+        examEndTime: examForm.examEndTime
104
+          ? dayjs(examForm.examEndTime).format('YYYY-MM-DD HH:mm:ss')
105
+          : '',
106
+        examDuration: examForm.examDuration,
107
+        examStatus: 'ongoing',
108
+        isLimit: examForm?.executorUsers?.length > 0 ? 1 : 0,
109
+        users: null,
110
+      };
111
+
112
+      if (params.isLimit && examForm.executorUsers.length > 0) {
113
+        params.users = JSON.stringify(
114
+          examForm.executorUsers.map((user: any) => {
115
+            return {
116
+              id: user.id,
117
+              name: user.name,
118
+            };
119
+          }),
120
+        );
121
+      }
122
+
123
+      if (isUpdateRef.value && !drawerApi.getData().isCopy) {
124
+        params.id = drawerApi.getData().id;
125
+      }
126
+
127
+      await (isUpdateRef.value ? updateExam(params) : addExam(params));
128
+
129
+      emit('reload');
130
+      drawerApi.close();
131
+    } catch (error) {
132
+      console.error(error);
133
+    } finally {
134
+      this.confirmLoading = false;
135
+    }
136
+  },
137
+});
138
+
139
+// 考试设置表单数据
140
+let examForm = reactive({
141
+  examName: '',
142
+  categoryId: '',
143
+  remark: '',
144
+  paperId: '',
145
+  passScore: '',
146
+  examStartTime: '',
147
+  examEndTime: '',
148
+  examDuration: '',
149
+  executorCount: 0, // 模拟已选择23人
150
+  executorUsers: [],
151
+  paperCategoryId: '',
152
+});
153
+
154
+// 已选择的试卷
155
+let selectedPapers: any = ref([]);
156
+
157
+// 打开抽屉
158
+const openDrawer = () => {
159
+  drawerVisible.value = true;
160
+};
161
+
162
+// 选择考试执行人
163
+const selectExecutors = () => {
164
+  executorModalApi.open();
165
+};
166
+
167
+// 删除已选择的试卷
168
+const removePaper = () => {
169
+  selectedPapers.value = [];
170
+  examForm.paperId = '';
171
+};
172
+
173
+const executorUserIds = computed(() => {
174
+  return examForm.executorUsers.map((o: any) => o.id);
175
+});
176
+
177
+const papers: any = ref([]);
178
+
179
+const getPapers = async () => {
180
+  const params = {
181
+    categoryId: examForm.paperCategoryId,
182
+  };
183
+
184
+  const res: any = await paperList(params);
185
+  if (res?.rows?.length > 0) {
186
+    papers.value = res.rows;
187
+  }
188
+};
189
+
190
+const handlePaperCategoryChange = () => {
191
+  getPapers().then();
192
+};
193
+
194
+// 监听试卷名称选择变化
195
+const handlePaperNameChange = (values: any) => {
196
+  const paperInfo = papers.value.find((paper: any) => paper.id === values);
197
+  if (paperInfo) {
198
+    examForm.paperId = paperInfo.id;
199
+
200
+    selectedPapers.value.push(paperInfo);
201
+  } else {
202
+    selectedPapers.value = [];
203
+  }
204
+};
205
+
206
+// 暴露方法给父组件
207
+defineExpose({
208
+  openDrawer,
209
+});
210
+
211
+// 获取试卷分类树
212
+const categoryTree = ref([]);
213
+const getCategoryOptions = async () => {
214
+  const res: any = await examCategoryTree();
215
+  categoryTree.value = res.rows || [];
216
+};
217
+
218
+const executorRef: any = ref(null);
219
+
220
+const [ExecutorModal, executorModalApi] = useVbenModal({
221
+  title: '选择考试执行人',
222
+  width: 1000,
223
+  height: 800,
224
+  showCancelBtn: false,
225
+  onConfirm: async () => {
226
+    const users = await executorRef.value.confirmSelection();
227
+    if (users?.length) {
228
+      examForm.executorCount = users.length;
229
+      examForm.executorUsers = users;
230
+      executorModalApi.close();
231
+    }
232
+  },
233
+});
234
+
235
+const paperCategoryTree = ref([]);
236
+
237
+const getPaperCategoryOptions = async () => {
238
+  const res: any = await PaperCategoryTree();
239
+  paperCategoryTree.value = res.rows || [];
240
+};
241
+
242
+onMounted(async () => {
243
+  getCategoryOptions().then();
244
+  getPaperCategoryOptions().then();
245
+});
246
+</script>
247
+
248
+<template>
249
+  <div>
250
+    <!-- 发布考试抽屉 -->
251
+    <Drawer class="w-[800px]">
252
+      <div class="exam-publish-drawer">
253
+        <!-- 第一部分:考试设置 -->
254
+        <div class="exam-setting-section">
255
+          <h3 class="section-title">考试设置</h3>
256
+
257
+          <ElForm
258
+            :model="examForm"
259
+            label-position="top"
260
+            class="grid-cols-1 gap-4 md:grid-cols-2"
261
+          >
262
+            <!-- 考试名称 -->
263
+            <ElFormItem label="考试名称" class="form-item-with-border" required>
264
+              <ElInput
265
+                v-model="examForm.examName"
266
+                placeholder="请输入考试名称"
267
+                maxlength="30"
268
+                show-word-limit
269
+              />
270
+            </ElFormItem>
271
+
272
+            <!-- 考试分类 -->
273
+            <ElFormItem label="考试分类" class="form-item-with-border" required>
274
+              <!-- <ElSelect
275
+                v-model="examForm.categoryId"
276
+                placeholder="请选择考试分类"
277
+                style="width: 100%"
278
+              >
279
+                <ElOption
280
+                  v-for="option in categoryOptions"
281
+                  :key="option.value"
282
+                  :label="option.label"
283
+                  :value="option.value"
284
+                />
285
+              </ElSelect> -->
286
+              <ElTreeSelect
287
+                v-model="examForm.categoryId"
288
+                :data="categoryTree"
289
+                :props="{
290
+                  label: 'name',
291
+                  value: 'id',
292
+                  children: 'children',
293
+                }"
294
+                placeholder="请选择分类"
295
+                class="select"
296
+              />
297
+            </ElFormItem>
298
+
299
+            <!-- 考试说明 -->
300
+            <ElFormItem
301
+              label="考试说明"
302
+              class="form-item-with-border md:col-span-2"
303
+            >
304
+              <ElInput
305
+                v-model="examForm.remark"
306
+                type="textarea"
307
+                placeholder="请输入考试说明"
308
+                rows="3"
309
+              />
310
+            </ElFormItem>
311
+
312
+            <!-- 考试总分 -->
313
+            <!-- <ElFormItem label="考试总分" class="form-item-with-border">
314
+              <ElInput
315
+                v-model="examForm.totalScore"
316
+                placeholder="请输入考试总分"
317
+                type="number"
318
+                min="1"
319
+              >
320
+                <template #append>分</template>
321
+              </ElInput>
322
+            </ElFormItem> -->
323
+
324
+            <!-- 及格分 -->
325
+            <ElFormItem label="及格分" class="form-item-with-border" required>
326
+              <ElInput
327
+                v-model="examForm.passScore"
328
+                placeholder="请输入及格分"
329
+                type="number"
330
+                min="1"
331
+              >
332
+                <template #append>分</template>
333
+              </ElInput>
334
+            </ElFormItem>
335
+
336
+            <!-- 答卷时长 -->
337
+            <ElFormItem label="答卷时长" class="form-item-with-border">
338
+              <ElInput
339
+                v-model="examForm.examDuration"
340
+                placeholder="请输入答卷时长"
341
+                type="number"
342
+                min="1"
343
+              >
344
+                <template #append>分钟</template>
345
+              </ElInput>
346
+            </ElFormItem>
347
+
348
+            <!-- 考试时间 -->
349
+            <ElFormItem
350
+              label="考试时间"
351
+              class="form-item-with-border md:col-span-2"
352
+            >
353
+              <ElDatePicker
354
+                v-model="examForm.examStartTime"
355
+                type="datetime"
356
+                placeholder="开始时间"
357
+                style="width: calc(50% - 15px)"
358
+              />
359
+              <span style="margin: 0 6px">至</span>
360
+              <ElDatePicker
361
+                v-model="examForm.examEndTime"
362
+                type="datetime"
363
+                placeholder="结束时间"
364
+                style="width: calc(50% - 15px)"
365
+              />
366
+            </ElFormItem>
367
+
368
+            <!-- 考试执行人 -->
369
+            <ElFormItem
370
+              label="考试执行人"
371
+              class="form-item-with-border md:col-span-2"
372
+            >
373
+              <ElButton @click="selectExecutors"> 选择考试执行人 </ElButton>
374
+              <span
375
+                style="margin-left: 10px; color: #86909c"
376
+                v-if="examForm.executorCount > 0"
377
+              >
378
+                已选择{{ examForm.executorCount }}人
379
+              </span>
380
+              <span style="margin-left: 10px; color: #86909c" v-else>
381
+                未选择执行人,将不限制考试人员
382
+              </span>
383
+            </ElFormItem>
384
+          </ElForm>
385
+        </div>
386
+
387
+        <!-- 第二部分:选择试卷 -->
388
+        <div class="select-paper-section">
389
+          <h3 class="section-title">选择试卷</h3>
390
+
391
+          <div class="mb-4 grid-cols-1 gap-4 md:grid-cols-2">
392
+            <!-- 选择试卷分类 -->
393
+            <ElFormItem label="选择试卷分类" class="form-item-with-border">
394
+              <ElTreeSelect
395
+                v-model="examForm.paperCategoryId"
396
+                :data="paperCategoryTree"
397
+                @change="handlePaperCategoryChange"
398
+                :props="{
399
+                  label: 'name',
400
+                  value: 'id',
401
+                  children: 'children',
402
+                }"
403
+              />
404
+            </ElFormItem>
405
+
406
+            <!-- 选择试卷名称 -->
407
+            <ElFormItem label="选择试卷名称" class="form-item-with-border">
408
+              <ElSelect
409
+                v-model="examForm.paperId"
410
+                filterable
411
+                allow-create
412
+                default-first-option
413
+                placeholder="请选择试卷名称"
414
+                style="width: 100%"
415
+                @change="handlePaperNameChange"
416
+              >
417
+                <ElOption
418
+                  v-for="option in papers"
419
+                  :key="option.id"
420
+                  :label="option.paperName"
421
+                  :value="option.id"
422
+                />
423
+              </ElSelect>
424
+            </ElFormItem>
425
+          </div>
426
+
427
+          <!-- 选择的试卷列表 -->
428
+          <div class="selected-papers-list" v-if="selectedPapers.length > 0">
429
+            <ElTable
430
+              :data="selectedPapers"
431
+              style="width: 100%"
432
+              :header-cell-style="{ backgroundColor: '#F7F9FA', color: '#000' }"
433
+            >
434
+              <ElTableColumn
435
+                prop="paperName"
436
+                label="试卷名称"
437
+                min-width="200"
438
+              />
439
+              <ElTableColumn prop="categoryName" label="类别" min-width="120" />
440
+              <ElTableColumn prop="createBy" label="创建人" min-width="120" />
441
+              <ElTableColumn
442
+                prop="totalScore"
443
+                label="试卷总分"
444
+                min-width="100"
445
+              />
446
+              <ElTableColumn
447
+                prop="questionCount"
448
+                label="试题总数"
449
+                min-width="100"
450
+              />
451
+              <ElTableColumn
452
+                prop="updateTime"
453
+                label="更新时间"
454
+                min-width="120"
455
+              />
456
+              <ElTableColumn label="操作" width="80" fixed="right">
457
+                <template #default>
458
+                  <ElButton
459
+                    link
460
+                    type="warning"
461
+                    size="small"
462
+                    @click="removePaper"
463
+                    class="custom-delete-btn"
464
+                  >
465
+                    删除
466
+                  </ElButton>
467
+                </template>
468
+              </ElTableColumn>
469
+            </ElTable>
470
+          </div>
471
+        </div>
472
+      </div>
473
+    </Drawer>
474
+
475
+    <ExecutorModal class="w-[700px]">
476
+      <SelectUsers ref="executorRef" :model-value="executorUserIds" />
477
+    </ExecutorModal>
478
+  </div>
479
+</template>
480
+
481
+<style lang="scss" scoped>
482
+.exam-publish-drawer {
483
+  padding: 20px;
484
+}
485
+
486
+:deep(.el-drawer__header) {
487
+  .el-drawer__title {
488
+    color: #000;
489
+  }
490
+}
491
+
492
+.section-title {
493
+  position: relative;
494
+  padding-left: 12px;
495
+  margin-bottom: 20px;
496
+  font-size: 16px;
497
+  font-weight: 500;
498
+  color: #303133;
499
+
500
+  &::before {
501
+    position: absolute;
502
+    top: 0;
503
+    bottom: 0;
504
+    left: 0;
505
+    width: 3px;
506
+    content: '';
507
+    background-color: #409eff;
508
+    border-radius: 2px;
509
+  }
510
+}
511
+
512
+:deep(.form-item-with-border) {
513
+  .el-form-item__label {
514
+    position: relative;
515
+    padding-left: 12px;
516
+    font-weight: 500;
517
+    color: #000;
518
+  }
519
+}
520
+
521
+.grid-cols-1 {
522
+  display: grid;
523
+  grid-template-columns: 1fr;
524
+}
525
+
526
+.md\:grid-cols-2 {
527
+  @media (min-width: 768px) {
528
+    grid-template-columns: repeat(2, 1fr);
529
+  }
530
+}
531
+
532
+.md\:col-span-2 {
533
+  @media (min-width: 768px) {
534
+    grid-column: span 2;
535
+  }
536
+}
537
+
538
+.gap-4 {
539
+  gap: 1rem;
540
+}
541
+
542
+.mb-4 {
543
+  margin-bottom: 1rem;
544
+}
545
+
546
+.action-buttons {
547
+  display: flex;
548
+  gap: 12px;
549
+  justify-content: flex-start;
550
+  margin-top: 30px;
551
+}
552
+
553
+.selected-papers-list {
554
+  margin-top: 20px;
555
+}
556
+
557
+.custom-delete-btn {
558
+  color: #f0864d;
559
+}
560
+</style>

apps/web-ele/src/views/examManage/examPaper/cpns/examPreview.vue → apps/web-ele/src/views/examManage/exam/cpns/examPreview.vue


+ 196 - 331
apps/web-ele/src/views/examManage/examPaper/index.vue

@@ -1,111 +1,61 @@
1 1
 <script lang="ts" setup>
2
+// @ts-ignore
2 3
 import type { VbenFormProps } from '@vben/common-ui';
3 4
 
5
+// @ts-ignore
4 6
 import type { VxeGridProps } from '#/adapter/vxe-table';
5 7
 
6 8
 import { onMounted, ref } from 'vue';
7
-import { useRoute, useRouter } from 'vue-router';
9
+import { useRouter } from 'vue-router';
8 10
 
9
-import { Page } from '@vben/common-ui';
11
+// @ts-ignore
12
+import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
10 13
 
11 14
 import {
12 15
   CopyDocument,
13 16
   DArrowLeft,
14 17
   Delete,
15
-  Download,
16 18
   Edit,
17 19
   Expand,
18 20
   Plus,
19 21
 } from '@element-plus/icons-vue';
20
-import {
21
-  ElButton,
22
-  ElDialog,
23
-  ElIcon,
24
-  ElMessage,
25
-  ElMessageBox,
26
-  ElSpace,
27
-  ElTree,
28
-} from 'element-plus';
22
+import { ElMessage, ElMessageBox } from 'element-plus';
29 23
 
24
+// @ts-ignore
30 25
 import { useVbenVxeGrid } from '#/adapter/vxe-table';
31
-
32
-// 导入预览组件
33
-import ExamPreview from './cpn/examPreview.vue';
26
+import {
27
+  deleteExamCategory,
28
+  examCategoryTree,
29
+  // @ts-ignore
30
+} from '#/api/exam/exam/category';
31
+// @ts-ignore
32
+// @ts-ignore
33
+import { deleteExam, examList } from '#/api/exam/exam/exam';
34
+// @ts-ignore
35
+import PaperPreview from '#/components/paper';
36
+// @ts-ignore
37
+
38
+import { queryFormSchema, tableColumns } from './config-data';
39
+// @ts-ignore
40
+import chapterDrawer from './cpn/chapter/drawer.vue';
41
+// @ts-ignore
42
+import examDrawer from './cpn/edit/drawer.vue';
34 43
 
35 44
 // 路由实例
36 45
 const router = useRouter();
37
-const route = useRoute();
38
-const classId = route.params.classId;
39
-
46
+// const route = useRoute();
40 47
 // 菜单折叠状态
41 48
 const isCollapse = ref(false);
42 49
 
43 50
 // 章节数据
44
-const chapterData = ref([
45
-  {
46
-    id: 1,
47
-    label: '第一章 基础知识',
48
-    children: [
49
-      { id: 2, label: '1.1 概述', saved: true },
50
-      { id: 3, label: '1.2 基本概念', saved: false },
51
-      { id: 4, label: '1.3 核心原理', saved: true },
52
-    ],
53
-  },
54
-  {
55
-    id: 5,
56
-    label: '第二章 进阶知识',
57
-    children: [
58
-      { id: 6, label: '2.1 高级特性', saved: true },
59
-      { id: 7, label: '2.2 最佳实践', saved: true },
60
-    ],
61
-  },
62
-  {
63
-    id: 8,
64
-    label: '第三章 案例分析',
65
-    children: [
66
-      { id: 9, label: '3.1 案例一', saved: false },
67
-      { id: 10, label: '3.2 案例二', saved: true },
68
-    ],
69
-  },
70
-]);
51
+const chapterData = ref([]);
71 52
 
72 53
 // 选中的章节
73 54
 const selectedChapter = ref({
74
-  id: '1',
75
-  label: '第一章 基础知识',
55
+  id: '',
56
+  label: '',
76 57
 });
77 58
 
78
-// 模拟试卷数据
79
-const mockExamData = ref([
80
-  {
81
-    id: 1,
82
-    name: '基础知识测试卷',
83
-    category: '基础知识',
84
-    creator: '管理员',
85
-    totalScore: 100,
86
-    questionCount: 20,
87
-    updateDate: '2024-01-15',
88
-  },
89
-  {
90
-    id: 2,
91
-    name: 'Vue框架测试卷',
92
-    category: '框架知识',
93
-    creator: '教师1',
94
-    totalScore: 120,
95
-    questionCount: 25,
96
-    updateDate: '2024-01-16',
97
-  },
98
-  {
99
-    id: 3,
100
-    name: '前端综合测试卷',
101
-    category: '前端基础',
102
-    creator: '教师2',
103
-    totalScore: 150,
104
-    questionCount: 30,
105
-    updateDate: '2024-01-17',
106
-  },
107
-]);
108
-
109 59
 // 表单配置
110 60
 const formOptions: VbenFormProps = {
111 61
   commonConfig: {
@@ -114,16 +64,7 @@ const formOptions: VbenFormProps = {
114 64
       allowClear: true,
115 65
     },
116 66
   },
117
-  schema: [
118
-    {
119
-      component: 'Input',
120
-      fieldName: 'name',
121
-      label: '试卷名称',
122
-      componentProps: {
123
-        placeholder: '请输入试卷名称',
124
-      },
125
-    },
126
-  ],
67
+  schema: queryFormSchema(),
127 68
   wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
128 69
 };
129 70
 
@@ -134,49 +75,7 @@ const gridOptions: VxeGridProps = {
134 75
     reserve: true,
135 76
     trigger: 'default',
136 77
   },
137
-  columns: [
138
-    {
139
-      type: 'checkbox',
140
-      width: 80,
141
-    },
142
-    {
143
-      field: 'name',
144
-      title: '试卷名称',
145
-      minWidth: 200,
146
-    },
147
-    {
148
-      field: 'category',
149
-      title: '类别',
150
-      minWidth: 120,
151
-    },
152
-    {
153
-      field: 'creator',
154
-      title: '创建人',
155
-      minWidth: 120,
156
-    },
157
-    {
158
-      field: 'totalScore',
159
-      title: '试卷总分',
160
-      minWidth: 100,
161
-    },
162
-    {
163
-      field: 'questionCount',
164
-      title: '试题总数',
165
-      minWidth: 100,
166
-    },
167
-    {
168
-      field: 'updateDate',
169
-      title: '更新日期',
170
-      minWidth: 120,
171
-    },
172
-    {
173
-      field: 'action',
174
-      fixed: 'right',
175
-      slots: { default: 'action' },
176
-      title: '操作',
177
-      width: 300,
178
-    },
179
-  ],
78
+  columns: tableColumns,
180 79
   size: 'medium',
181 80
   // 固定表格高度,避免高度不断增加
182 81
   height: 800,
@@ -197,28 +96,18 @@ const gridOptions: VxeGridProps = {
197 96
     },
198 97
     ajax: {
199 98
       query: async ({ page }, formValues = {}) => {
200
-        // 模拟API请求
201
-        console.log('查询参数:', formValues, page);
202
-
203
-        // 模拟搜索过滤
204
-        let filteredData = [...mockExamData.value];
205
-
206
-        if (formValues.name) {
207
-          filteredData = filteredData.filter((item) =>
208
-            item.name.includes(formValues.name),
209
-          );
210
-        }
211
-
212
-        // 模拟分页
213
-        const start = (page.currentPage - 1) * page.pageSize;
214
-        const end = start + page.pageSize;
215
-        const paginatedData = filteredData.slice(start, end);
216
-        console.log('分页数据:', paginatedData);
217
-
99
+        // 当前选择的章节ID
100
+        const categoryId = selectedChapter.value.id || 0;
101
+        const data: any = await examList({
102
+          categoryId,
103
+          ...formValues,
104
+          pageNum: page.currentPage,
105
+          pageSize: page.pageSize,
106
+        });
218 107
         // 确保返回格式正确,使用 items 作为数据键
219 108
         return {
220
-          items: paginatedData,
221
-          total: filteredData.length,
109
+          items: data.rows,
110
+          total: data.total,
222 111
         };
223 112
       },
224 113
     },
@@ -239,8 +128,19 @@ const [BasicTable, basicTableApi] = useVbenVxeGrid({
239 128
   gridOptions,
240 129
 });
241 130
 
131
+// 章节抽屉组件
132
+const [ChapterVbenDrawer, chapterVbenDrawerApi] = useVbenDrawer({
133
+  connectedComponent: chapterDrawer,
134
+});
135
+
136
+// 添加组件
137
+const [ExamDrawer, drawerApi] = useVbenDrawer({
138
+  connectedComponent: examDrawer,
139
+  title: '发布考试',
140
+});
141
+
242 142
 // 点击章节
243
-const handleChapterClick = (node) => {
143
+const handleChapterClick = (node: any) => {
244 144
   selectedChapter.value = {
245 145
     id: node.id,
246 146
     label: node.label,
@@ -250,34 +150,56 @@ const handleChapterClick = (node) => {
250 150
 };
251 151
 
252 152
 // 添加章节
253
-const addChapter = (parentId = null) => {
254
-  ElMessage.info('添加章节功能待实现');
153
+const addChapter = (chapter: any = {}) => {
154
+  chapterVbenDrawerApi.setData({ ...chapter, isUpdate: false }).open();
255 155
 };
256 156
 
257 157
 // 编辑章节
258
-const editChapter = (node) => {
259
-  ElMessage.info('编辑章节功能待实现');
158
+const editChapter = (chapter: any) => {
159
+  chapterVbenDrawerApi.setData({ ...chapter, isUpdate: true }).open();
260 160
 };
261 161
 
262 162
 // 删除章节
263
-const deleteChapter = (node) => {
264
-  ElMessage.info('删除章节功能待实现');
163
+const deleteChapter = (chapter: any) => {
164
+  ElMessageBox.confirm('确认删除该章节吗?', '提示', {
165
+    confirmButtonText: '确定',
166
+    cancelButtonText: '取消',
167
+    type: 'warning',
168
+  }).then(async () => {
169
+    await deleteExamCategory(chapter.id);
170
+    await getChapterTree();
171
+  });
265 172
 };
266 173
 
267
-// 新增试卷
268
-const addExam = () => {
269
-  // 跳转到新页面
270
-  router.push('/examManage/examEdit/0');
174
+// 获取章节树
175
+const getChapterTree = async () => {
176
+  const data: any = await examCategoryTree();
177
+  chapterData.value = data.rows || [];
271 178
 };
272 179
 
273
-// 添加时间
274
-const addTime = () => {
275
-  ElMessage.info('添加时间功能待实现');
276
-};
180
+// // 新增试卷
181
+// const addExam = () => {
182
+//   // 打开抽屉
183
+//   paperDrawerRef.value.openDrawer();
184
+// };
185
+
186
+// // 添加时间
187
+// const addTime = () => {
188
+//   ElMessage.info('添加时间功能待实现');
189
+// };
277 190
 
278 191
 // 删除试卷
279
-const deleteExam = (row) => {
280
-  ElMessage.info('删除试卷功能待实现');
192
+const deleteExams = (row: any) => {
193
+  // 进行二次确认
194
+  ElMessageBox.confirm('确认删除该试卷吗?', '提示', {
195
+    confirmButtonText: '确定',
196
+    cancelButtonText: '取消',
197
+    type: 'warning',
198
+  }).then(async () => {
199
+    await deleteExam([row.id]);
200
+    // 刷新表格数据
201
+    await basicTableApi.reload();
202
+  });
281 203
 };
282 204
 
283 205
 // 批量删除试卷
@@ -285,6 +207,8 @@ const deleteHandle = () => {
285 207
   const checkRecords = basicTableApi.grid.getCheckboxRecords();
286 208
   const ids = checkRecords.map((item: any) => item.id);
287 209
   if (ids.length <= 0) {
210
+    // 提示用户选择要删除的试卷
211
+    ElMessage.warning('请选择要删除的试卷');
288 212
     return;
289 213
   }
290 214
 
@@ -294,164 +218,57 @@ const deleteHandle = () => {
294 218
     type: 'warning',
295 219
   }).then(async () => {
296 220
     // 模拟批量删除操作
297
-    console.log('批量删除试卷:', ids);
221
+    await deleteExam(ids);
298 222
     await basicTableApi.reload();
299 223
   });
300 224
 };
301 225
 
302 226
 // 编辑试卷
303
-const editExam = (row) => {
227
+const editExam = (row: any) => {
304 228
   // 跳转到编辑页面,并传递试卷ID
305
-  router.push(`/examManage/examEdit/${row.id}`);
229
+  router.push({ name: 'PaperEdit', params: { id: row.id } });
306 230
 };
307 231
 
308 232
 // 预览弹框状态
309
-const previewDialogVisible = ref(false);
310
-const previewExamData = ref(null);
311
-
312
-// 预览试卷
313
-const previewExam = (row) => {
314
-  // 模拟试卷数据,实际项目中应该通过API获取
315
-  previewExamData.value = {
316
-    id: row.id,
317
-    name: row.name,
318
-    questions: [
319
-      {
320
-        id: 1,
321
-        type: 1,
322
-        title: '以下哪个是Vue的核心特性?',
323
-        score: 5,
324
-        options: [
325
-          { id: 1, text: '响应式数据绑定', isCorrect: true },
326
-          { id: 2, text: '命令式编程', isCorrect: false },
327
-          { id: 3, text: '双向数据流', isCorrect: false },
328
-          { id: 4, text: '静态模板', isCorrect: false },
329
-        ],
330
-        correctAnswer: 'A',
331
-      },
332
-      {
333
-        id: 2,
334
-        type: 2,
335
-        title: 'Vue组件的生命周期钩子中,哪个在组件挂载后调用?',
336
-        score: 5,
337
-        options: [
338
-          { id: 1, text: 'beforeCreate', isCorrect: false },
339
-          { id: 2, text: 'created', isCorrect: false },
340
-          { id: 3, text: 'mounted', isCorrect: true },
341
-          { id: 4, text: 'beforeMount', isCorrect: false },
342
-        ],
343
-        correctAnswer: 'C',
344
-      },
345
-      {
346
-        id: 3,
347
-        type: 3,
348
-        title: 'Vuex是Vue的状态管理库。',
349
-        score: 2,
350
-        options: [
351
-          { id: 1, text: '正确', isCorrect: true },
352
-          { id: 2, text: '错误', isCorrect: false },
353
-        ],
354
-        correctAnswer: '正确',
355
-      },
356
-      {
357
-        id: 4,
358
-        type: 1,
359
-        title: 'Vue中,哪个指令用于条件渲染?',
360
-        score: 5,
361
-        options: [
362
-          { id: 1, text: 'v-for', isCorrect: false },
363
-          { id: 2, text: 'v-if', isCorrect: true },
364
-          { id: 3, text: 'v-bind', isCorrect: false },
365
-          { id: 4, text: 'v-on', isCorrect: false },
366
-        ],
367
-        correctAnswer: 'B',
368
-      },
369
-      {
370
-        id: 5,
371
-        type: 2,
372
-        title: '以下哪些是Vue的内置指令?(多选)',
373
-        score: 8,
374
-        options: [
375
-          { id: 1, text: 'v-model', isCorrect: true },
376
-          { id: 2, text: 'v-show', isCorrect: true },
377
-          { id: 3, text: 'v-class', isCorrect: false },
378
-          { id: 4, text: 'v-style', isCorrect: false },
379
-        ],
380
-        correctAnswer: 'A,B',
381
-      },
382
-      {
383
-        id: 6,
384
-        type: 3,
385
-        title: 'Vue 3兼容Vue 2的所有语法。',
386
-        score: 2,
387
-        options: [
388
-          { id: 1, text: '正确', isCorrect: false },
389
-          { id: 2, text: '错误', isCorrect: true },
390
-        ],
391
-        correctAnswer: '错误',
392
-      },
393
-      {
394
-        id: 7,
395
-        type: 4,
396
-        title: 'Vue中,______是用于在组件之间传递数据的属性。',
397
-        score: 3,
398
-        correctAnswer: 'props',
399
-      },
400
-      {
401
-        id: 8,
402
-        type: 5,
403
-        title: '请简述Vue的响应式原理。',
404
-        score: 10,
405
-        correctAnswer:
406
-          'Vue的响应式原理基于Object.defineProperty()方法,通过对数据对象的属性进行getter和setter的劫持,实现数据变化时自动更新视图。当数据被访问时触发getter,当数据被修改时触发setter,setter会通知依赖收集器更新相关的视图。',
407
-      },
408
-      {
409
-        id: 9,
410
-        type: 1,
411
-        title: 'Vue Router的模式中,哪种需要后端配置支持?',
412
-        score: 5,
413
-        options: [
414
-          { id: 1, text: 'hash', isCorrect: false },
415
-          { id: 2, text: 'history', isCorrect: true },
416
-          { id: 3, text: 'abstract', isCorrect: false },
417
-          { id: 4, text: 'memory', isCorrect: false },
418
-        ],
419
-        correctAnswer: 'B',
420
-      },
421
-      {
422
-        id: 10,
423
-        type: 2,
424
-        title: 'Vue 3中新增的Composition API包括哪些?(多选)',
425
-        score: 8,
426
-        options: [
427
-          { id: 1, text: 'setup()', isCorrect: true },
428
-          { id: 2, text: 'ref()', isCorrect: true },
429
-          { id: 3, text: 'reactive()', isCorrect: true },
430
-          { id: 4, text: 'watchEffect()', isCorrect: true },
431
-        ],
432
-        correctAnswer: 'A,B,C,D',
433
-      },
434
-    ],
435
-  };
436
-  previewDialogVisible.value = true;
233
+const previewPaperId = ref('');
234
+// 预览试题模态框
235
+const [PreviewModal, previewModalApi] = useVbenModal({
236
+  // connectedComponent: QuestionPreview,
237
+  title: '预览',
238
+  width: 800,
239
+  height: 600,
240
+  showCancelBtn: false,
241
+});
242
+
243
+const previewExam = (row: any) => {
244
+  previewPaperId.value = row.paperId || '';
245
+  previewModalApi.open();
437 246
 };
438 247
 
439 248
 // 复制试卷
440
-const copyExam = (row) => {
441
-  ElMessage.info('复制试卷功能待实现');
249
+const copyExam = (row: any) => {
250
+  // 跳转到添加试卷页面
251
+  drawerApi.setData({ ...row, isUpdate: true, isCopy: true }).open();
252
+};
253
+
254
+const addExam = () => {
255
+  // 跳转到添加试卷页面
256
+  drawerApi.setData({}).open();
442 257
 };
443 258
 
444
-// 下载试卷
445
-const downloadExam = (row) => {
446
-  ElMessage.info('下载试卷功能待实现');
259
+const updateExam = (row: any) => {
260
+  drawerApi.setData({ ...row, isUpdate: true }).open();
447 261
 };
448 262
 
449 263
 // 页面加载时获取数据
450 264
 onMounted(() => {
451
-  // 默认选中第一个章节
452
-  if (chapterData.value.length > 0) {
453
-    handleChapterClick(chapterData.value[0]);
454
-  }
265
+  // 获取试卷分类树形图
266
+  getChapterTree().then(() => {
267
+    // 默认选中第一个章节
268
+    if (chapterData.value.length > 0) {
269
+      handleChapterClick(chapterData.value[0]);
270
+    }
271
+  });
455 272
 });
456 273
 </script>
457 274
 
@@ -473,6 +290,7 @@ onMounted(() => {
473 290
               <ElTree
474 291
                 :data="chapterData"
475 292
                 node-key="id"
293
+                :props="{ label: 'name' }"
476 294
                 default-expand-all
477 295
                 :expand-on-click-node="false"
478 296
                 @node-click="handleChapterClick"
@@ -485,8 +303,8 @@ onMounted(() => {
485 303
                         type="text"
486 304
                         size="small"
487 305
                         circle
488
-                        @click.stop="addExam"
489
-                        title="添加试卷"
306
+                        @click.stop="addChapter(data)"
307
+                        title="添加章节"
490 308
                       >
491 309
                         <ElIcon><Plus /></ElIcon>
492 310
                       </ElButton>
@@ -511,6 +329,18 @@ onMounted(() => {
511 329
                     </div>
512 330
                   </div>
513 331
                 </template>
332
+                <template #empty>
333
+                  <div class="tree-empty">
334
+                    <span class="el-tree__empty-text">暂无分类</span>
335
+                    <ElButton
336
+                      type="primary"
337
+                      class="mt-10 w-full"
338
+                      @click="addChapter()"
339
+                    >
340
+                      <ElIcon><Plus /></ElIcon> 添加分类
341
+                    </ElButton>
342
+                  </div>
343
+                </template>
514 344
               </ElTree>
515 345
             </div>
516 346
           </div>
@@ -533,6 +363,9 @@ onMounted(() => {
533 363
             <BasicTable table-title="试卷管理列表">
534 364
               <template #toolbar-tools>
535 365
                 <ElSpace>
366
+                  <ElButton type="primary" @click="addExam">
367
+                    发布考试
368
+                  </ElButton>
536 369
                   <ElButton
537 370
                     type="danger"
538 371
                     :disabled="
@@ -542,9 +375,9 @@ onMounted(() => {
542 375
                   >
543 376
                     批量删除
544 377
                   </ElButton>
545
-                  <ElButton type="primary" @click="addExam">
546
-                    新增试卷
547
-                  </ElButton>
378
+                  <!-- <ElButton type="primary" @click="addExam">
379
+                    发布考试
380
+                  </ElButton> -->
548 381
                 </ElSpace>
549 382
               </template>
550 383
               <template #action="{ row }">
@@ -566,7 +399,17 @@ onMounted(() => {
566 399
                     <ElIcon><CopyDocument /></ElIcon>
567 400
                     复制
568 401
                   </ElButton>
569
-                  <ElButton
402
+                  <!-- <ElButton
403
+                    type="text"
404
+                    size="small"
405
+                    @click="generateLink(row)"
406
+                    style="margin-left: 0px"
407
+                  >
408
+                    <ElIcon><Link /></ElIcon>
409
+                    生成链接
410
+                  </ElButton> -->
411
+
412
+                  <!-- <ElButton
570 413
                     type="text"
571 414
                     size="small"
572 415
                     @click="downloadExam(row)"
@@ -574,22 +417,23 @@ onMounted(() => {
574 417
                   >
575 418
                     <ElIcon><Download /></ElIcon>
576 419
                     下载
577
-                  </ElButton>
420
+                  </ElButton> -->
578 421
                   <ElButton
579 422
                     type="text"
580 423
                     size="small"
581
-                    @click="editExam(row)"
424
+                    @click="updateExam(row)"
582 425
                     style="margin-left: 0px"
583 426
                   >
584 427
                     <ElIcon><Edit /></ElIcon>
585 428
                     编辑
586 429
                   </ElButton>
587 430
                   <ElButton
588
-                    type="text"
431
+                    type="danger"
589 432
                     size="small"
590
-                    @click="deleteExam(row)"
433
+                    @click="deleteExams(row)"
591 434
                     class="text-red-500"
592 435
                     style="margin-left: 0px"
436
+                    link
593 437
                   >
594 438
                     <ElIcon><Delete /></ElIcon>
595 439
                     删除
@@ -602,19 +446,18 @@ onMounted(() => {
602 446
       </div>
603 447
     </div>
604 448
 
605
-    <!-- 预览弹框 -->
606
-    <ElDialog
607
-      v-model="previewDialogVisible"
608
-      :title="`试卷预览 - ${previewExamData?.name || ''}`"
609
-      width="800px"
610
-      top="5vh"
611
-      :close-on-click-modal="false"
612
-    >
613
-      <ExamPreview
614
-        v-if="previewExamData"
615
-        :questions="previewExamData.questions"
616
-      />
617
-    </ElDialog>
449
+    <!-- 试卷抽屉组件 -->
450
+    <!-- <PaperDrawer ref="paperDrawerRef" /> -->
451
+    <!-- 章节抽屉组件 -->
452
+    <ChapterVbenDrawer @reload="getChapterTree" />
453
+
454
+    <!-- 添加考试抽屉组件 -->
455
+    <ExamDrawer @reload="basicTableApi.reload()" />
456
+
457
+    <!-- 预览试题模态框 -->
458
+    <PreviewModal>
459
+      <PaperPreview :paper-id="previewPaperId" />
460
+    </PreviewModal>
618 461
   </Page>
619 462
 </template>
620 463
 
@@ -689,6 +532,28 @@ onMounted(() => {
689 532
   overflow-y: auto;
690 533
 }
691 534
 
535
+.tree-empty {
536
+  display: contents;
537
+  // flex-direction: column;
538
+  align-items: center;
539
+  // justify-content: center;
540
+  padding: 20px 20px;
541
+  text-align: center;
542
+  min-height: 300px;
543
+}
544
+
545
+.tree-empty .el-tree__empty-text {
546
+  margin-top: 0px;
547
+  font-size: 16px;
548
+  color: #909399;
549
+}
550
+
551
+.tree-empty .el-button {
552
+  margin-top: 100px;
553
+  width: 200px;
554
+  margin-bottom: 0px;
555
+}
556
+
692 557
 .tree-node-content {
693 558
   display: flex;
694 559
   align-items: center;

+ 0 - 948
apps/web-ele/src/views/examManage/examPaper/cpn/examEdit.vue

@@ -1,948 +0,0 @@
1
-<script setup lang="ts">
2
-import { ref, computed } from 'vue';
3
-import { ElAnchor, ElAnchorLink, ElCollapse, ElCollapseItem, ElTable } from 'element-plus';
4
-import ExamPreview from './examPreview.vue';
5
-
6
-// 试卷表单数据
7
-const examForm = ref({
8
-  examName: '',
9
-  examCategory: '',
10
-});
11
-
12
-// 试卷题目数据
13
-const examQuestions = ref([
14
-  {
15
-    title: '第一题 选择题',
16
-    fixedQuestions: [
17
-      { type: '填空题', count: 120, score: 0 },
18
-      { type: '简答题', count: 120, score: 0 },
19
-      { type: '单选题', count: 120, score: 0 },
20
-      { type: '多选题', count: 120, score: 0 },
21
-    ],
22
-    randomQuestions: [
23
-      { type: '填空题', count: 120, score: 0 },
24
-      { type: '简答题', count: 120, score: 0 },
25
-      { type: '单选题', count: 120, score: 0 },
26
-      { type: '多选题', count: 120, score: 0 },
27
-    ],
28
-  },
29
-  {
30
-    title: '第二题 填空题',
31
-    fixedQuestions: [],
32
-    randomQuestions: [],
33
-  },
34
-  {
35
-    title: '第三题 判断题',
36
-    fixedQuestions: [],
37
-    randomQuestions: [],
38
-  },
39
-]);
40
-
41
-// 折叠面板状态,默认展开所有面板
42
-const activeCollapse = ref<(string | number | boolean)[]>(examQuestions.value.map((_, index) => index));
43
-
44
-// 计算总题数
45
-const totalQuestions = computed(() => examQuestions.value.length);
46
-
47
-// 计算小题总数
48
-const totalSubQuestions = computed(() => {
49
-  return examQuestions.value.reduce((total, question) => {
50
-    return (
51
-      total + question.fixedQuestions.length + question.randomQuestions.length
52
-    );
53
-  }, 0);
54
-});
55
-
56
-// 计算总分
57
-const totalScore = computed(() => {
58
-  let score = 0;
59
-  examQuestions.value.forEach((question) => {
60
-    question.fixedQuestions.forEach((item) => {
61
-      score += item.score;
62
-    });
63
-    question.randomQuestions.forEach((item) => {
64
-      score += item.score;
65
-    });
66
-  });
67
-  return score;
68
-});
69
-
70
-// 获取单个题目总分
71
-const getQuestionTotalScore = (index: number) => {
72
-  const question = examQuestions.value[index];
73
-  let score = 0;
74
-  question.fixedQuestions.forEach((item) => {
75
-    score += item.score;
76
-  });
77
-  question.randomQuestions.forEach((item) => {
78
-    score += item.score;
79
-  });
80
-  return score;
81
-};
82
-
83
-// 添加大题
84
-const addBigQuestion = () => {
85
-  examQuestions.value.push({
86
-    title: `第${examQuestions.value.length + 1}题 新题目`,
87
-    fixedQuestions: [],
88
-    randomQuestions: [],
89
-  });
90
-};
91
-// 容器引用
92
-const containerRef = ref<HTMLElement | null>(null);
93
-// 试题表格引用
94
-const questionTable = ref<InstanceType<typeof ElTable> | null>(null);
95
-
96
-const handleClick = (e: MouseEvent) => {
97
-  e.preventDefault();
98
-};
99
-
100
-// 弹框相关
101
-const dialogVisible = ref(false);
102
-const currentQuestionIndex = ref(-1);
103
-const currentQuestionType = ref(''); // 'fixed' or 'random'
104
-
105
-// 搜索表单
106
-const searchForm = ref({
107
-  questionName: '',
108
-  questionType: '',
109
-  questionCategory: '',
110
-  questionDifficulty: '',
111
-});
112
-
113
-// 模拟试题数据
114
-const questionList = ref([
115
-  { id: 1, name: '题目1', type: '单选题', category: '分类1', difficulty: '简单' },
116
-  { id: 2, name: '题目2', type: '多选题', category: '分类2', difficulty: '中等' },
117
-  { id: 3, name: '题目3', type: '填空题', category: '分类1', difficulty: '困难' },
118
-  { id: 4, name: '题目4', type: '简答题', category: '分类3', difficulty: '简单' },
119
-  { id: 5, name: '题目5', type: '判断题', category: '分类2', difficulty: '中等' },
120
-]);
121
-
122
-// 分页数据
123
-const currentPage = ref(1);
124
-const pageSize = ref(20);
125
-const total = ref(37);
126
-
127
-// 存储选中的试题
128
-const selectedQuestions = ref<any[]>([]);
129
-
130
-// 处理选择变化
131
-const handleSelectionChange = (val: any[]) => {
132
-  selectedQuestions.value = val;
133
-};
134
-
135
-// 打开弹框
136
-const openDialog = (index: number, type: string) => {
137
-  currentQuestionIndex.value = index;
138
-  currentQuestionType.value = type;
139
-  // 清空之前的选择
140
-  selectedQuestions.value = [];
141
-  dialogVisible.value = true;
142
-};
143
-
144
-// 关闭弹框
145
-const closeDialog = () => {
146
-  dialogVisible.value = false;
147
-};
148
-
149
-// 查询
150
-const handleSearch = () => {
151
-  // 实际项目中这里会调用API获取数据
152
-  console.log('搜索参数:', searchForm.value);
153
-};
154
-
155
-// 重置
156
-const handleReset = () => {
157
-  searchForm.value = {
158
-    questionName: '',
159
-    questionType: '',
160
-    questionCategory: '',
161
-    questionDifficulty: '',
162
-  };
163
-};
164
-
165
-// 预览
166
-const previewQuestion = (id: number) => {
167
-  console.log('预览题目:', id);
168
-};
169
-
170
-// 分页方法
171
-const handleSizeChange = (size: number) => {
172
-  pageSize.value = size;
173
-  // 实际项目中这里会重新调用API获取数据
174
-  console.log('每页条数:', size);
175
-};
176
-
177
-const handleCurrentChange = (current: number) => {
178
-  currentPage.value = current;
179
-  // 实际项目中这里会重新调用API获取数据
180
-  console.log('当前页码:', current);
181
-};
182
-
183
-// 保存选中的试题
184
-const saveQuestions = () => {
185
-  if (currentQuestionIndex.value === -1 || !currentQuestionType.value) {
186
-    return;
187
-  }
188
-
189
-  // 使用通过handleSelectionChange方法存储的选中数据
190
-  const selectedRows = selectedQuestions.value;
191
-  
192
-  if (selectedRows.length === 0) {
193
-    return;
194
-  }
195
-
196
-  // 根据类型添加到对应的试题列表
197
-  const question = examQuestions.value[currentQuestionIndex.value];
198
-  if (currentQuestionType.value === 'fixed') {
199
-    // 为每个选中的试题创建一个新的对象,包含必要的字段
200
-    selectedRows.forEach((row: any) => {
201
-      question.fixedQuestions.push({
202
-        type: row.type,
203
-        count: 1, // 每个试题算1个
204
-        score: 0 // 默认分数为0
205
-      });
206
-    });
207
-  } else if (currentQuestionType.value === 'random') {
208
-    // 为每个选中的试题创建一个新的对象,包含必要的字段
209
-    selectedRows.forEach((row: any) => {
210
-      question.randomQuestions.push({
211
-        type: row.type,
212
-        count: 1, // 每个试题算1个
213
-        score: 0 // 默认分数为0
214
-      });
215
-    });
216
-  }
217
-
218
-  // 关闭弹框
219
-  closeDialog();
220
-};
221
-
222
-// 随机试题设置表单
223
-const randomQuestionForm = ref({
224
-  questionTypes: [] as string[],
225
-  questionCategories: [] as string[],
226
-  questionDifficulty: '',
227
-  questionCount: 1,
228
-});
229
-
230
-// 题型选项
231
-const questionTypeOptions = [
232
-  { label: '单选题', value: '单选题' },
233
-  { label: '多选题', value: '多选题' },
234
-  { label: '填空题', value: '填空题' },
235
-  { label: '简答题', value: '简答题' },
236
-  { label: '判断题', value: '判断题' },
237
-];
238
-
239
-// 分类选项
240
-const questionCategoryOptions = [
241
-  { label: '分类1', value: '分类1' },
242
-  { label: '分类2', value: '分类2' },
243
-  { label: '分类3', value: '分类3' },
244
-];
245
-
246
-// 难度选项
247
-const questionDifficultyOptions = [
248
-  { label: '简单', value: '简单' },
249
-  { label: '中等', value: '中等' },
250
-  { label: '困难', value: '困难' },
251
-];
252
-
253
-// 预览相关
254
-const previewDialogVisible = ref(false);
255
-const previewQuestions = ref<any[]>([]);
256
-
257
-// 打开预览弹框
258
-const openPreviewDialog = (index: number, questionType: string, questionIndex: number) => {
259
-  const question = examQuestions.value[index];
260
-  const targetQuestions = questionType === 'fixed' ? question.fixedQuestions : question.randomQuestions;
261
-  const selectedQuestion = targetQuestions[questionIndex];
262
-  
263
-  // 模拟生成预览数据
264
-  generatePreviewData(selectedQuestion.type, selectedQuestion.count);
265
-  
266
-  previewDialogVisible.value = true;
267
-};
268
-
269
-// 关闭预览弹框
270
-const closePreviewDialog = () => {
271
-  previewDialogVisible.value = false;
272
-};
273
-
274
-// 生成预览数据
275
-const generatePreviewData = (type: string, count: number) => {
276
-  previewQuestions.value = [];
277
-  
278
-  for (let i = 0; i < count; i++) {
279
-    switch (type) {
280
-      case '单选题':
281
-        previewQuestions.value.push({
282
-          id: `single-${i}`,
283
-          questionType: '单选题',
284
-          questionTitle: '这是本题的题目,请选择',
285
-          options: [
286
-            { content: '答案1', isCorrect: true },
287
-            { content: '答案2', isCorrect: false },
288
-            { content: '答案3', isCorrect: false },
289
-            { content: '答案4', isCorrect: false }
290
-          ],
291
-          score: 2
292
-        });
293
-        break;
294
-      case '多选题':
295
-        previewQuestions.value.push({
296
-          id: `multiple-${i}`,
297
-          questionType: '多选题',
298
-          questionTitle: '这是本题的题目,请选择',
299
-          options: [
300
-            { content: '答案1', isCorrect: true },
301
-            { content: '答案2', isCorrect: true },
302
-            { content: '答案3', isCorrect: false },
303
-            { content: '答案4', isCorrect: false }
304
-          ],
305
-          score: 4
306
-        });
307
-        break;
308
-      case '判断题':
309
-        previewQuestions.value.push({
310
-          id: `judge-${i}`,
311
-          questionType: '判断题',
312
-          questionTitle: '这是本题的题目,请判断',
313
-          options: [
314
-            { content: '正确', isCorrect: true },
315
-            { content: '错误', isCorrect: false }
316
-          ],
317
-          score: 1
318
-        });
319
-        break;
320
-      case '填空题':
321
-        previewQuestions.value.push({
322
-          id: `fill-${i}`,
323
-          questionType: '填空题',
324
-          questionTitle: '这是本题的题目,请作答',
325
-          correctAnswer: '这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案。这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案',
326
-          score: 3
327
-        });
328
-        break;
329
-      case '简答题':
330
-        previewQuestions.value.push({
331
-          id: `essay-${i}`,
332
-          questionType: '简答题',
333
-          questionTitle: '这是本题的题目,请作答',
334
-          correctAnswer: '这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案。这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案这是正确答案',
335
-          score: 6
336
-        });
337
-        break;
338
-    }
339
-  }
340
-};
341
-
342
-// 保存随机试题设置
343
-const saveRandomQuestions = () => {
344
-  if (currentQuestionIndex.value === -1) {
345
-    return;
346
-  }
347
-
348
-  // 获取当前设置的随机试题参数
349
-  const { questionTypes, questionCategories, questionDifficulty, questionCount } = randomQuestionForm.value;
350
-
351
-  // 确保至少选择了一种题型
352
-  if (!questionTypes || questionTypes.length === 0) {
353
-    return;
354
-  }
355
-
356
-  // 根据类型添加到对应的试题列表
357
-  const question = examQuestions.value[currentQuestionIndex.value];
358
-  
359
-  // 为每种选中的题型创建一个随机试题设置
360
-  questionTypes.forEach((type: string) => {
361
-    question.randomQuestions.push({
362
-      type: type,
363
-      count: questionCount,
364
-      score: 0 // 默认分数为0
365
-    });
366
-  });
367
-
368
-  // 关闭弹框
369
-  closeDialog();
370
-};
371
-</script>
372
-
373
-<template>
374
-  <div class="exam-edit-container">
375
-    <!-- 顶部区域 -->
376
-    <div class="top-section">
377
-      <div class="left-section">
378
-        <div class="form-item">
379
-          <label class="label">试卷名称:</label>
380
-          <el-input
381
-            v-model="examForm.examName"
382
-            placeholder="请输入试卷名称"
383
-            :maxlength="30"
384
-            show-word-limit
385
-            class="input"
386
-          />
387
-        </div>
388
-        <div class="form-item">
389
-          <label class="label">试卷分类:</label>
390
-          <el-select
391
-            v-model="examForm.examCategory"
392
-            placeholder="请选择"
393
-            class="select"
394
-          >
395
-            <el-option label="选择题" value="1" />
396
-            <el-option label="填空题" value="2" />
397
-            <el-option label="简答题" value="3" />
398
-            <el-option label="判断题" value="4" />
399
-          </el-select>
400
-        </div>
401
-      </div>
402
-      <div class="right-section">
403
-        <div class="stats">
404
-          <span>{{ totalQuestions }}大题</span>
405
-          <span class="separator">|</span>
406
-          <span>{{ totalSubQuestions }}小题</span>
407
-          <span class="separator">|</span>
408
-          <span>总分:{{ totalScore }}</span>
409
-        </div>
410
-        <el-button type="primary" size="default" class="save-btn">
411
-          保存
412
-        </el-button>
413
-      </div>
414
-    </div>
415
-
416
-    <!-- 主体区域 -->
417
-    <div class="main-section">
418
-      <!-- 左侧导航 -->
419
-      <div class="left-nav">
420
-        <div class="nav-title">大题导航</div>
421
-        <ElAnchor  
422
-            :container="containerRef"
423
-            direction="vertical"
424
-            default-active="question-0"
425
-            type="default"
426
-            :offset="30"
427
-            @click="handleClick">
428
-            <ElAnchorLink
429
-              v-for="(question, index) in examQuestions"
430
-              :key="index"
431
-              :href="`#question-${index}`"
432
-              :title="question.title"
433
-            />
434
-        </ElAnchor>
435
-      </div>
436
-      <!-- 右侧内容 -->
437
-      <div class="right-content" ref="containerRef">
438
-        <div
439
-          v-for="(question, index) in examQuestions"
440
-          :key="index"
441
-          :id="`question-${index}`"
442
-          class="question-block"
443
-        >
444
-          <!-- 移除原来的question-header -->
445
-          <el-collapse v-model="activeCollapse">
446
-            <el-collapse-item :name="index">
447
-              <template #title>
448
-                <div style="display: flex; justify-content: space-between; align-items: center; font-size: 18px; width: 100%;">
449
-                  <span>{{question.title}}</span>
450
-                  <span>合计:{{getQuestionTotalScore(index)}}分</span>
451
-                </div>
452
-              </template>
453
-              <!-- 固定试题区块 -->
454
-              <div class="question-type-section">
455
-                <div class="section-title">固定试题</div>
456
-                <el-table :data="question.fixedQuestions" style="width: 100%" :header-cell-style="{ backgroundColor: '#F7F9FA', color: '#31373D' }">
457
-                  <el-table-column prop="type" label="试题类型" />
458
-                  <el-table-column prop="count" label="试题数" />
459
-                  <el-table-column prop="score" label="每道题分数" >
460
-                    <template #default="scope">
461
-                      <el-input-number
462
-                        v-model="scope.row.score"
463
-                        :min="0"
464
-                        :max="100"
465
-                        size="small"
466
-                      />
467
-                    </template>
468
-                  </el-table-column>
469
-                  <el-table-column label="操作" >
470
-                    <template #default="scope">
471
-                      <el-button type="text" size="small" class="preview-btn" @click="openPreviewDialog(index, 'fixed', scope.$index)">
472
-                        预览
473
-                      </el-button>
474
-                      <el-button type="text" size="small" class="delete-btn">
475
-                        删除
476
-                      </el-button>
477
-                    </template>
478
-                  </el-table-column>
479
-                </el-table>
480
-                <el-button type="text" icon="Plus" class="add-question-btn" @click="openDialog(index, 'fixed')">
481
-                  + 试题库中选题
482
-                </el-button>
483
-              </div>
484
-              
485
-              <!-- 随机试题区块 -->
486
-              <div class="question-type-section">
487
-                <div class="section-title">随机试题</div>
488
-                <el-table :data="question.randomQuestions" style="width: 100%" :header-cell-style="{ backgroundColor: '#F7F9FA', color: '#31373D' }">
489
-                  <el-table-column prop="type" label="试题类型" />
490
-                  <el-table-column prop="count" label="试题数" />
491
-                  <el-table-column prop="score" label="每道题分数" >
492
-                    <template #default="scope">
493
-                      <el-input-number
494
-                        v-model="scope.row.score"
495
-                        :min="0"
496
-                        :max="100"
497
-                        size="small"
498
-                      />
499
-                    </template>
500
-                  </el-table-column>
501
-                  <el-table-column label="操作" >
502
-                    <template #default="scope">
503
-                      <el-button type="text" size="small" class="preview-btn" @click="openPreviewDialog(index, 'random', scope.$index)">
504
-                        预览
505
-                      </el-button>
506
-                      <el-button type="text" size="small" class="delete-btn">
507
-                        删除
508
-                      </el-button>
509
-                    </template>
510
-                  </el-table-column>
511
-                </el-table>
512
-                <el-button type="text" icon="Plus" class="add-question-btn" @click="openDialog(index, 'random')">
513
-                  + 试题库中选题
514
-                </el-button>
515
-              </div>
516
-            </el-collapse-item>
517
-          </el-collapse>
518
-        </div>
519
-
520
-        <el-button
521
-          type="dashed"
522
-          icon="Plus"
523
-          class="add-big-question-btn"
524
-          @click="addBigQuestion"
525
-        >
526
-          + 添加大题
527
-        </el-button>
528
-      </div>
529
-    </div>
530
-  </div>
531
-
532
-  <!-- 添加固定试题弹框 -->
533
-  <el-dialog
534
-    v-if="currentQuestionType === 'fixed'"
535
-    title="添加固定试题"
536
-    v-model="dialogVisible"
537
-    width="1000px"
538
-  >
539
-    <!-- 搜索表单 -->
540
-    <div class="search-form" style="margin-bottom: 20px;">
541
-      <el-form :model="searchForm" label-width="80px">
542
-        <el-row :gutter="20">
543
-          <el-col :span="6">
544
-            <el-form-item label="题目名称">
545
-              <el-input v-model="searchForm.questionName" placeholder="请输入" />
546
-            </el-form-item>
547
-          </el-col>
548
-          <el-col :span="6">
549
-            <el-form-item label="试题题型">
550
-              <el-select v-model="searchForm.questionType" placeholder="请选择">
551
-                <el-option label="单选题" value="单选题" />
552
-                <el-option label="多选题" value="多选题" />
553
-                <el-option label="填空题" value="填空题" />
554
-                <el-option label="简答题" value="简答题" />
555
-                <el-option label="判断题" value="判断题" />
556
-              </el-select>
557
-            </el-form-item>
558
-          </el-col>
559
-          <el-col :span="6">
560
-            <el-form-item label="试题分类">
561
-              <el-select v-model="searchForm.questionCategory" placeholder="请选择">
562
-                <el-option label="分类1" value="分类1" />
563
-                <el-option label="分类2" value="分类2" />
564
-                <el-option label="分类3" value="分类3" />
565
-              </el-select>
566
-            </el-form-item>
567
-          </el-col>
568
-          <el-col :span="6">
569
-            <el-form-item label="试题难度">
570
-              <el-select v-model="searchForm.questionDifficulty" placeholder="请选择">
571
-                <el-option label="简单" value="简单" />
572
-                <el-option label="中等" value="中等" />
573
-                <el-option label="困难" value="困难" />
574
-              </el-select>
575
-            </el-form-item>
576
-          </el-col>
577
-        </el-row>
578
-        <el-row>
579
-          <el-col :span="24" style="text-align: right;">
580
-            <el-button type="primary" @click="handleSearch">查询</el-button>
581
-            <el-button @click="handleReset">重置</el-button>
582
-          </el-col>
583
-        </el-row>
584
-      </el-form>
585
-    </div>
586
-
587
-    <!-- 试题列表 -->
588
-    <el-table ref="questionTable" :data="questionList" style="width: 100%" @selection-change="handleSelectionChange">
589
-      <el-table-column type="selection" width="55" />
590
-      <el-table-column prop="name" label="题目名称" />
591
-      <el-table-column prop="type" label="试题题型" />
592
-      <el-table-column prop="category" label="试题分类" />
593
-      <el-table-column prop="difficulty" label="试题难度" />
594
-      <el-table-column label="操作" width="100">
595
-        <template #default="scope">
596
-          <el-button type="text" size="small" @click="previewQuestion(scope.row.id)">预览</el-button>
597
-        </template>
598
-      </el-table-column>
599
-    </el-table>
600
-
601
-    <!-- 分页 -->
602
-    <div class="pagination" style="margin-top: 20px; text-align: right;">
603
-      <el-pagination
604
-        v-model:current-page="currentPage"
605
-        v-model:page-size="pageSize"
606
-        :page-sizes="[20, 50, 100]"
607
-        layout="total, sizes, prev, pager, next, jumper"
608
-        :total="total"
609
-        @size-change="handleSizeChange"
610
-        @current-change="handleCurrentChange"
611
-      />
612
-    </div>
613
-
614
-    <!-- 底部按钮 -->
615
-    <template #footer>
616
-      <span class="dialog-footer">
617
-        <el-button @click="closeDialog">取消</el-button>
618
-        <el-button type="primary" @click="saveQuestions">保存</el-button>
619
-      </span>
620
-    </template>
621
-  </el-dialog>
622
-
623
-  <!-- 添加随机试题弹框 -->
624
-  <el-dialog
625
-    v-if="currentQuestionType === 'random'"
626
-    title="添加随机试题"
627
-    v-model="dialogVisible"
628
-    width="500px"
629
-  >
630
-    <el-form :model="randomQuestionForm" label-width="100px">
631
-      <el-form-item label="试题类型">
632
-        <el-select
633
-          v-model="randomQuestionForm.questionTypes"
634
-          multiple
635
-          placeholder="请选择试题类型"
636
-          style="width: 100%"
637
-        >
638
-          <el-option
639
-            v-for="option in questionTypeOptions"
640
-            :key="option.value"
641
-            :label="option.label"
642
-            :value="option.value"
643
-          />
644
-        </el-select>
645
-      </el-form-item>
646
-      
647
-      <el-form-item label="试题分类">
648
-        <el-select
649
-          v-model="randomQuestionForm.questionCategories"
650
-          multiple
651
-          placeholder="请选择试题分类"
652
-          style="width: 100%"
653
-        >
654
-          <el-option
655
-            v-for="option in questionCategoryOptions"
656
-            :key="option.value"
657
-            :label="option.label"
658
-            :value="option.value"
659
-          />
660
-        </el-select>
661
-      </el-form-item>
662
-      
663
-      <el-form-item label="试题难度">
664
-        <el-select
665
-          v-model="randomQuestionForm.questionDifficulty"
666
-          placeholder="请选择试题难度"
667
-          style="width: 100%"
668
-        >
669
-          <el-option
670
-            v-for="option in questionDifficultyOptions"
671
-            :key="option.value"
672
-            :label="option.label"
673
-            :value="option.value"
674
-          />
675
-        </el-select>
676
-      </el-form-item>
677
-      
678
-      <el-form-item label="试题数量">
679
-        <el-input-number
680
-          v-model="randomQuestionForm.questionCount"
681
-          :min="1"
682
-          :max="100"
683
-          placeholder="请输入试题数量"
684
-          style="width: 100%"
685
-        />
686
-      </el-form-item>
687
-    </el-form>
688
-
689
-    <!-- 底部按钮 -->
690
-    <template #footer>
691
-      <span class="dialog-footer">
692
-        <el-button @click="closeDialog">取消</el-button>
693
-        <el-button type="primary" @click="saveRandomQuestions">保存</el-button>
694
-      </span>
695
-    </template>
696
-  </el-dialog>
697
-
698
-<!-- 预览弹框 -->
699
-<el-dialog
700
-  title="试题预览"
701
-  v-model="previewDialogVisible"
702
-  width="800px"
703
->
704
-  <ExamPreview :questions="previewQuestions" />
705
-</el-dialog>
706
-</template>
707
-
708
-<style scoped>
709
-  
710
-  .exam-edit-container {
711
-    padding: 1rem;
712
-    background-color: #f5f7fa;
713
-    height: calc(100vh - 90px);
714
-  }
715
-
716
-  /* 顶部区域 */
717
-  .top-section {
718
-    display: flex;
719
-    justify-content: space-between;
720
-    align-items: flex-start;
721
-    margin-bottom: 10px;
722
-    background-color: white;
723
-    padding: 20px;
724
-    border-radius: 8px;
725
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
726
-  }
727
-
728
-  .left-section {
729
-    display: flex;
730
-    flex-direction: column;
731
-    gap: 20px;
732
-    flex: 1;
733
-  }
734
-
735
-  .form-item {
736
-    display: flex;
737
-    align-items: center;
738
-    gap: 10px;
739
-  }
740
-
741
-  .label {
742
-    font-size: 14px;
743
-    color: #000;
744
-    min-width: 80px;
745
-    font-weight: 400;
746
-    text-align: right;
747
-  }
748
-
749
-  .input {
750
-    width: 400px;
751
-  }
752
-
753
-  .select {
754
-    width: 230px;
755
-  }
756
-
757
-  .right-section {
758
-    display: flex;
759
-    flex-direction: column;
760
-    align-items: flex-end;
761
-    gap: 15px;
762
-  }
763
-
764
-  .stats {
765
-    display: flex;
766
-    align-items: center;
767
-    gap: 15px;
768
-    font-size: 14px;
769
-    color: #303133;
770
-  }
771
-
772
-  .separator {
773
-    color: #dcdfe6;
774
-    font-size: 16px;
775
-  }
776
-
777
-  .save-btn {
778
-    width: 100px;
779
-    margin-top: 18px;
780
-  }
781
-
782
-  /* 主体区域 */
783
-  .main-section {
784
-    display: flex;
785
-    border-radius: 8px;
786
-    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
787
-  }
788
-
789
-  /* 左侧导航 */
790
-  .left-nav {
791
-    width: 350px;
792
-    padding: 20px;
793
-    margin-right: 10px;
794
-    background-color: white;
795
-    height: calc(100vh - 245px);
796
-  }
797
-
798
-  .nav-title {
799
-    font-size: 16px;
800
-    font-weight: bold;
801
-    margin-bottom: 15px;
802
-    color: #303133;
803
-  }
804
-
805
-  /* ElAnchor and ElAnchorLink styles */
806
-  :deep(.el-anchor) {
807
-    padding-left: 0;
808
-  }
809
-  :deep(.el-anchor__list) {
810
-    padding-left: 0 !important;
811
-  }
812
-
813
-  :deep(.el-anchor__link) { 
814
-    padding-left: 15px;
815
-    margin-bottom: 10px;
816
-    border-radius: 4px;
817
-    cursor: pointer;
818
-    transition: all 0.3s;
819
-    background-color: #ffffff;
820
-    border: 1px solid #e6e8eb;
821
-    position: relative;
822
-    width: 315px;
823
-    height: 34px;
824
-    color: #303133;
825
-    box-sizing: border-box;
826
-  }
827
-
828
-  :deep(.el-anchor__link:hover) {
829
-    background-color: #ecf5ff;
830
-    color: #409eff;
831
-  }
832
-
833
-  :deep(.el-anchor__link.is-active) {
834
-    background-color: #ecf5ff;
835
-    color: #409eff;
836
-    border-left: 3px solid #409eff;
837
-  }
838
-  :deep(.el-anchor__link-title) {
839
-    font-size: 14px;
840
-    font-weight: 400;
841
-    color: #303133;
842
-    padding: 0;
843
-    white-space: normal;
844
-    line-height: 1.4;
845
-  }
846
-
847
-  :deep(.el-anchor__link.active .el-anchor__link-title) { 
848
-    color: #409eff;
849
-  }
850
-
851
-  :deep(.el-anchor-fix) {
852
-    display: none;
853
-  }
854
-
855
-  /* 右侧内容 */
856
-  .right-content {
857
-    flex: 1;
858
-    background-color: white;
859
-    border-radius: 8px;
860
-    height: calc(100vh - 245px);
861
-    overflow-y: auto;
862
-    /* 隐藏滚动条 */
863
-    -ms-overflow-style: none;  /* Internet Explorer 10+ */
864
-    scrollbar-width: none;  /* Firefox */
865
-    ::-webkit-scrollbar {
866
-      display: none;
867
-    }
868
-  }
869
-
870
-  /* 题目块 */
871
-  .question-block {
872
-    margin-bottom: 20px;
873
-    padding: 0px 20px;
874
-    background-color: #ffffff;
875
-    border-radius: 8px;
876
-  }
877
-
878
-  /* 题目类型区块 */
879
-  .question-type-section {
880
-    margin-bottom: 20px;
881
-    padding: 15px;
882
-    background-color: white;
883
-    border-radius: 6px;
884
-  }
885
-
886
-  .section-title {
887
-    font-size: 16px;
888
-    font-weight: bold;
889
-    margin-bottom: 15px;
890
-    color: #303133;
891
-  }
892
-
893
-  .add-question-btn {
894
-    color: #409eff;
895
-    margin-top: 10px;
896
-  }
897
-
898
-  /* 添加大题按钮 */
899
-  .add-big-question-btn {
900
-    width: 100%;
901
-    margin-top: 20px;
902
-  }
903
-
904
-  /* 操作按钮样式 */
905
-  .preview-btn {
906
-    color: #409eff;
907
-    margin-right: 10px;
908
-  }
909
-
910
-  .delete-btn {
911
-    color: #f56c6c;
912
-  }
913
-
914
-  /* 响应式设计 */
915
-  @media (max-width: 1200px) {
916
-    .input,
917
-    .select {
918
-      width: 250px;
919
-    }
920
-  }
921
-
922
-  @media (max-width: 768px) {
923
-    .top-section {
924
-      flex-direction: column;
925
-    }
926
-
927
-    .right-section {
928
-      align-items: flex-start;
929
-    }
930
-
931
-    .main-section {
932
-      flex-direction: column;
933
-    }
934
-
935
-    .left-nav {
936
-      width: 100%;
937
-      border-right: none;
938
-      border-bottom: 1px solid #e6e8eb;
939
-      padding-right: 0;
940
-      padding-bottom: 20px;
941
-      margin-bottom: 20px;
942
-    }
943
-
944
-    .right-content {
945
-      padding-left: 0;
946
-    }
947
-  }
948
-</style>

+ 0 - 212
apps/web-ele/src/views/examManage/examPaper/cpn/examPreview.vue

@@ -1,212 +0,0 @@
1
-<template>
2
-  <div v-if="displayQuestions.length > 0" class="questions-container">
3
-    <div v-for="(question, index) in displayQuestions" :key="question.id" class="preview-question-item">
4
-      
5
-      <!-- 单选题、多选题、判断题 -->
6
-      <div v-if="question.options" class="question-content">
7
-        <p class="question-title">{{ index + 1 }}. {{ question.title }}</p>
8
-        <div class="options-list">
9
-          <div 
10
-            v-for="(option, optIndex) in question.options" 
11
-            :key="optIndex" 
12
-            class="option-item"
13
-            :class="{ 'correct-option': option.isCorrect }"
14
-          >
15
-            <input 
16
-              :type="['单选题', '判断题'].includes(getQuestionType(question.type)) ? 'radio' : 'checkbox'" 
17
-              :name="['单选题', '判断题'].includes(getQuestionType(question.type)) ? question.id : `${question.id}_${optIndex}`" 
18
-              :disabled="true"
19
-              :checked="option.isCorrect"
20
-            >
21
-            <span class="option-content">{{ String.fromCharCode(65 + optIndex) }}. {{ option.text }}</span>
22
-          </div>
23
-        </div>
24
-      </div>
25
-      
26
-      <!-- 填空题、简答题 -->
27
-      <div v-else class="question-content">
28
-        <p class="question-title">{{ index + 1 }}. {{ question.title }}</p>
29
-        <div class="answer-section">
30
-          <p class="answer-label">正确答案:</p>
31
-          <p class="answer-content">{{ question.correctAnswer }}</p>
32
-        </div>
33
-      </div>
34
-    </div>
35
-    <!-- 如果有更多题目,显示提示 -->
36
-    <div v-if="questions.length > maxQuestions" class="more-questions-hint">
37
-      注:仅显示前 {{ maxQuestions }} 题,共 {{ questions.length }} 题
38
-    </div>
39
-  </div>
40
-  <div v-else class="no-questions">
41
-    暂无预览数据
42
-  </div>
43
-</template>
44
-
45
-<script setup lang="ts">
46
-import { computed } from 'vue';
47
-
48
-// 定义组件属性
49
-interface Option {
50
-  id: number;
51
-  text: string;
52
-  isCorrect: boolean;
53
-}
54
-
55
-interface Question {
56
-  id: number;
57
-  type: number;
58
-  title: string;
59
-  options?: Option[];
60
-  correctAnswer?: string;
61
-  score: number;
62
-}
63
-
64
-const props = defineProps<{
65
-  questions: Question[];
66
-  maxQuestions?: number;
67
-}>();
68
-
69
-// 设置默认显示题数为5
70
-const maxQuestions = props.maxQuestions || 30;
71
-
72
-// 计算要显示的题目
73
-const displayQuestions = computed(() => {
74
-  return props.questions.slice(0, maxQuestions);
75
-});
76
-
77
-// 根据类型ID获取题目类型名称
78
-const getQuestionType = (type: number): string => {
79
-  switch(type) {
80
-    case 1: return '单选题';
81
-    case 2: return '多选题';
82
-    case 3: return '判断题';
83
-    case 4: return '填空题';
84
-    case 5: return '简答题';
85
-    default: return '其他题型';
86
-  }
87
-};
88
-</script>
89
-
90
-<style scoped>
91
-  .questions-container {
92
-    max-height:600px;
93
-    overflow-y: auto;
94
-    padding-right: 10px;
95
-  }
96
-
97
-  .preview-question-item {
98
-    margin-bottom: 20px;
99
-    padding-bottom: 20px;
100
-    border-bottom: 1px solid #e6e8eb;
101
-  }
102
-
103
-  .preview-question-item:last-child {
104
-    border-bottom: none;
105
-    margin-bottom: 0;
106
-    padding-bottom: 0;
107
-  }
108
-
109
-  .question-header {
110
-    display: flex;
111
-    align-items: center;
112
-    margin-bottom: 10px;
113
-  }
114
-
115
-  .question-type {
116
-    font-weight: bold;
117
-    color: #303133;
118
-    margin-right: 10px;
119
-  }
120
-
121
-  .question-score {
122
-    color: #909399;
123
-    font-size: 14px;
124
-  }
125
-
126
-  .question-content {
127
-    margin-left: 20px;
128
-  }
129
-
130
-  .question-title {
131
-    font-size: 16px;
132
-    color: #303133;
133
-    margin-bottom: 15px;
134
-    line-height: 1.5;
135
-  }
136
-
137
-  .options-list {
138
-    margin-left: 20px;
139
-  }
140
-
141
-  .option-item {
142
-    display: flex;
143
-    align-items: center;
144
-    margin-bottom: 10px;
145
-    padding: 8px 12px;
146
-    border-radius: 4px;
147
-    transition: all 0.3s;
148
-  }
149
-
150
-  .option-item:hover {
151
-    background-color: #f5f7fa;
152
-  }
153
-
154
-  .option-item input[type="radio"],
155
-  .option-item input[type="checkbox"] {
156
-    margin-right: 10px;
157
-  }
158
-
159
-  .option-content {
160
-    font-size: 14px;
161
-    color: #606266;
162
-    line-height: 1.5;
163
-  }
164
-
165
-  .correct-option {
166
-    background-color: #f0f9eb;
167
-    border-left: 3px solid #67c23a;
168
-  }
169
-
170
-  .correct-option .option-content {
171
-    color: #303133;
172
-    font-weight: 500;
173
-  }
174
-
175
-  .answer-section {
176
-    margin-top: 10px;
177
-    padding: 12px;
178
-    background-color: #f5f7fa;
179
-    border-radius: 4px;
180
-  }
181
-
182
-  .answer-label {
183
-    font-weight: bold;
184
-    color: #303133;
185
-    margin-bottom: 8px;
186
-  }
187
-
188
-  .answer-content {
189
-    font-size: 14px;
190
-    color: #606266;
191
-    line-height: 1.5;
192
-    white-space: pre-wrap;
193
-    word-break: break-all;
194
-  }
195
-
196
-  .no-questions {
197
-    text-align: center;
198
-    color: #909399;
199
-    padding: 40px 0;
200
-  }
201
-
202
-  .more-questions-hint {
203
-    text-align: center;
204
-    color: #909399;
205
-    font-size: 14px;
206
-    margin-top: 20px;
207
-    padding: 10px;
208
-    background-color: #f5f7fa;
209
-    border-radius: 4px;
210
-  }
211
-</style>
212
-

+ 0 - 526
apps/web-ele/src/views/examManage/paperManage/cpn/paper-drawer.vue

@@ -1,526 +0,0 @@
1
-<script lang="ts" setup>
2
-import { ref, reactive } from 'vue';
3
-import { ElMessage } from 'element-plus';
4
-
5
-// 抽屉可见性
6
-const drawerVisible = ref(false);
7
-
8
-// 考试设置表单数据
9
-const examForm = reactive({
10
-  name: '',
11
-  category: '',
12
-  description: '',
13
-  totalScore: '',
14
-  passingScore: '',
15
-  startTime: '',
16
-  endTime: '',
17
-  duration: '',
18
-  executorCount: 23, // 模拟已选择23人
19
-});
20
-
21
-// 考试分类选项
22
-const categoryOptions = [
23
-  { label: '基础知识', value: 'basic' },
24
-  { label: '框架知识', value: 'framework' },
25
-  { label: '前端基础', value: 'frontend' },
26
-  { label: '后端基础', value: 'backend' },
27
-  { label: '综合测试', value: 'comprehensive' },
28
-];
29
-
30
-// 试卷分类选项
31
-const paperCategoryOptions = [
32
-  { label: '基础知识', value: 'basic' },
33
-  { label: '框架知识', value: 'framework' },
34
-  { label: '前端基础', value: 'frontend' },
35
-  { label: '后端基础', value: 'backend' },
36
-  { label: '综合测试', value: 'comprehensive' },
37
-];
38
-
39
-// 试卷名称选项
40
-const paperNameOptions = [
41
-  { label: '基础知识测试卷', value: '1', category: '基础知识', creator: '管理员', totalScore: 100, questionCount: 20, updateDate: '2024-01-15' },
42
-  { label: 'Vue框架测试卷', value: '2', category: '框架知识', creator: '教师1', totalScore: 120, questionCount: 25, updateDate: '2024-01-16' },
43
-  { label: '前端综合测试卷', value: '3', category: '前端基础', creator: '教师2', totalScore: 150, questionCount: 30, updateDate: '2024-01-17' },
44
-  { label: '后端综合测试卷', value: '4', category: '后端基础', creator: '教师3', totalScore: 120, questionCount: 25, updateDate: '2024-01-18' },
45
-  { label: '全栈综合测试卷', value: '5', category: '综合测试', creator: '管理员', totalScore: 200, questionCount: 40, updateDate: '2024-01-19' },
46
-];
47
-
48
-// 已选择的试卷
49
-const selectedPapers = ref([
50
-  { id: '1', name: '基础知识测试卷', category: '基础知识', creator: '管理员', totalScore: 100, questionCount: 20, updateDate: '2024-01-15' },
51
-  { id: '2', name: 'Vue框架测试卷', category: '框架知识', creator: '教师1', totalScore: 120, questionCount: 25, updateDate: '2024-01-16' },
52
-]);
53
-
54
-// 选中的试卷分类
55
-const selectedPaperCategory = ref('');
56
-
57
-// 选中的试卷名称
58
-const selectedPaperNames = ref([]);
59
-
60
-// 打开抽屉
61
-const openDrawer = () => {
62
-  drawerVisible.value = true;
63
-};
64
-
65
-// 关闭抽屉
66
-const closeDrawer = () => {
67
-  drawerVisible.value = false;
68
-};
69
-
70
-// 执行人选择弹框可见性
71
-const executorDialogVisible = ref(false);
72
-
73
-// 执行人树数据
74
-const executorTreeData = ref([
75
-  {
76
-    label: '部门一',
77
-    value: 'dept1',
78
-    children: [
79
-      {
80
-        label: '组1',
81
-        value: 'group1',
82
-        children: [
83
-          { label: '张三', value: 'user1' },
84
-          { label: '张三', value: 'user2' },
85
-          { label: '张三', value: 'user3' },
86
-          { label: '张三', value: 'user4' },
87
-          { label: '张三', value: 'user5' },
88
-          { label: '张三', value: 'user6' },
89
-          { label: '张三', value: 'user7' },
90
-          { label: '张三', value: 'user8' },
91
-          { label: '张三', value: 'user9' },
92
-          { label: '张三', value: 'user10' },
93
-          { label: '张三', value: 'user11' },
94
-          { label: '张三', value: 'user12' },
95
-          { label: '张三', value: 'user13' },
96
-          { label: '张三', value: 'user14' },
97
-          { label: '张三', value: 'user15' },
98
-        ],
99
-      },
100
-      {
101
-        label: '组2',
102
-        value: 'group2',
103
-        children: [],
104
-      },
105
-    ],
106
-  },
107
-  {
108
-    label: '部门二',
109
-    value: 'dept2',
110
-    children: [
111
-      {
112
-        label: '组1',
113
-        value: 'group3',
114
-        children: [
115
-          { label: '1-1-1-1-1', value: 'user16' },
116
-        ],
117
-      },
118
-    ],
119
-  },
120
-]);
121
-
122
-// 已选择的执行人
123
-const selectedExecutors = ref(['dept1', 'group1', 'user1', 'user2', 'user3', 'user4', 'user5', 'user6', 'user7', 'user8', 'user9', 'user10', 'user11', 'user12', 'user13', 'user14', 'user15']);
124
-
125
-// 选择考试执行人
126
-const selectExecutors = () => {
127
-  executorDialogVisible.value = true;
128
-};
129
-
130
-// 提交执行人选择
131
-const submitExecutorSelection = () => {
132
-  // 统计已选择的用户数量
133
-  const getUserCount = (nodes, selectedKeys) => {
134
-    let count = 0;
135
-    nodes.forEach(node => {
136
-      if (node.children && node.children.length > 0) {
137
-        count += getUserCount(node.children, selectedKeys);
138
-      } else if (selectedKeys.includes(node.value)) {
139
-        count++;
140
-      }
141
-    });
142
-    return count;
143
-  };
144
-  
145
-  const userCount = getUserCount(executorTreeData.value, selectedExecutors.value);
146
-  examForm.executorCount = userCount;
147
-  executorDialogVisible.value = false;
148
-  ElMessage.success('已选择' + userCount + '人');
149
-};
150
-
151
-// 取消执行人选择
152
-const cancelExecutorSelection = () => {
153
-  executorDialogVisible.value = false;
154
-};
155
-
156
-// 删除已选择的试卷
157
-const removePaper = (id) => {
158
-  selectedPapers.value = selectedPapers.value.filter(paper => paper.id !== id);
159
-};
160
-
161
-// 发布考试
162
-const publishExam = () => {
163
-  // 表单验证
164
-  if (!examForm.name) {
165
-    ElMessage.error('请输入考试名称');
166
-    return;
167
-  }
168
-  if (!examForm.category) {
169
-    ElMessage.error('请选择考试分类');
170
-    return;
171
-  }
172
-  if (!examForm.totalScore) {
173
-    ElMessage.error('请输入考试总分');
174
-    return;
175
-  }
176
-  if (!examForm.passingScore) {
177
-    ElMessage.error('请输入及格分');
178
-    return;
179
-  }
180
-  if (!examForm.startTime || !examForm.endTime) {
181
-    ElMessage.error('请选择考试时间');
182
-    return;
183
-  }
184
-  if (!examForm.duration) {
185
-    ElMessage.error('请输入答卷时长');
186
-    return;
187
-  }
188
-  if (selectedPapers.value.length === 0) {
189
-    ElMessage.error('请选择至少一张试卷');
190
-    return;
191
-  }
192
-  
193
-  // 模拟发布考试
194
-  console.log('发布考试:', examForm);
195
-  console.log('选择的试卷:', selectedPapers.value);
196
-  ElMessage.success('考试发布成功');
197
-  closeDrawer();
198
-};
199
-
200
-// 监听试卷名称选择变化
201
-const handlePaperNameChange = (values) => {
202
-  // 模拟根据选择的试卷名称获取试卷信息
203
-  selectedPapers.value = paperNameOptions
204
-    .filter(paper => values.includes(paper.value))
205
-    .map(paper => ({
206
-      id: paper.value,
207
-      name: paper.label,
208
-      category: paper.category,
209
-      creator: paper.creator,
210
-      totalScore: paper.totalScore,
211
-      questionCount: paper.questionCount,
212
-      updateDate: paper.updateDate,
213
-    }));
214
-};
215
-
216
-// 暴露方法给父组件
217
-defineExpose({
218
-  openDrawer,
219
-});
220
-</script>
221
-
222
-<template>
223
-  <div>
224
-    <!-- 发布考试抽屉 -->
225
-    <el-drawer
226
-      v-model="drawerVisible"
227
-      title="发布考试"
228
-      size="1200px"
229
-      destroy-on-close
230
-    >
231
-      <div class="exam-publish-drawer">
232
-        <!-- 第一部分:考试设置 -->
233
-        <div class="exam-setting-section">
234
-          <h3 class="section-title">考试设置</h3>
235
-          
236
-          <el-form
237
-            :model="examForm"
238
-            label-position="top"
239
-            class="grid-cols-1 md:grid-cols-2 gap-4">
240
-            <!-- 考试名称 -->
241
-            <el-form-item label="考试名称" class="form-item-with-border">
242
-              <el-input
243
-                v-model="examForm.name"
244
-                placeholder="请输入考试名称"
245
-                maxlength="30"
246
-                show-word-limit
247
-              />
248
-            </el-form-item>
249
-            
250
-            <!-- 考试分类 -->
251
-            <el-form-item label="考试分类" class="form-item-with-border">
252
-              <el-select
253
-                v-model="examForm.category"
254
-                placeholder="请选择考试分类"
255
-                style="width: 100%"
256
-              >
257
-                <el-option
258
-                  v-for="option in categoryOptions"
259
-                  :key="option.value"
260
-                  :label="option.label"
261
-                  :value="option.value"
262
-                />
263
-              </el-select>
264
-            </el-form-item>
265
-            
266
-            <!-- 考试说明 -->
267
-            <el-form-item label="考试说明" class="form-item-with-border md:col-span-2">
268
-              <el-input
269
-                v-model="examForm.description"
270
-                type="textarea"
271
-                placeholder="请输入考试说明"
272
-                rows="3"
273
-              />
274
-            </el-form-item>
275
-            
276
-            <!-- 考试总分 -->
277
-            <el-form-item label="考试总分" class="form-item-with-border">
278
-              <el-input
279
-                v-model="examForm.totalScore"
280
-                placeholder="请输入考试总分"
281
-                type="number"
282
-                min="1"
283
-              >
284
-                <template #append>分</template>
285
-              </el-input>
286
-            </el-form-item>
287
-            
288
-            <!-- 及格分 -->
289
-            <el-form-item label="及格分" class="form-item-with-border">
290
-              <el-input
291
-                v-model="examForm.passingScore"
292
-                placeholder="请输入及格分"
293
-                type="number"
294
-                min="1"
295
-              >
296
-                <template #append>分</template>
297
-              </el-input>
298
-            </el-form-item>
299
-            
300
-            <!-- 考试时间 -->
301
-            <el-form-item label="考试时间" class="form-item-with-border">
302
-              <el-date-picker
303
-                v-model="examForm.startTime"
304
-                type="datetime"
305
-                placeholder="开始时间"
306
-                style="width: calc(50% - 15px)"
307
-              />
308
-              <span style="margin: 0 6px;">至</span>
309
-              <el-date-picker
310
-                v-model="examForm.endTime"
311
-                type="datetime"
312
-                placeholder="结束时间"
313
-                style="width: calc(50% - 15px)"
314
-              />
315
-            </el-form-item>
316
-            
317
-            <!-- 答卷时长 -->
318
-            <el-form-item label="答卷时长" class="form-item-with-border">
319
-              <el-input
320
-                v-model="examForm.duration"
321
-                placeholder="请输入答卷时长"
322
-                type="number"
323
-                min="1"
324
-              >
325
-                <template #append>分钟</template>
326
-              </el-input>
327
-            </el-form-item>
328
-            
329
-            <!-- 考试执行人 -->
330
-            <el-form-item label="考试执行人" class="form-item-with-border md:col-span-2">
331
-              <el-button @click="selectExecutors">
332
-                选择考试执行人
333
-              </el-button>
334
-              <span style="margin-left: 10px; color: #86909c;">已选择{{ examForm.executorCount }}人</span> 
335
-            </el-form-item>
336
-          </el-form>
337
-        </div>
338
-        
339
-        <!-- 第二部分:选择试卷 -->
340
-        <div class="select-paper-section">
341
-          <h3 class="section-title">选择试卷</h3>
342
-          
343
-          <div class="grid-cols-1 md:grid-cols-2 gap-4 mb-4">
344
-            <!-- 选择试卷分类 -->
345
-            <el-form-item label="选择试卷分类" class="form-item-with-border">
346
-              <el-select
347
-                v-model="selectedPaperCategory"
348
-                placeholder="请选择试卷分类"
349
-                filterable
350
-                allow-create
351
-                default-first-option
352
-                style="width: 100%"
353
-              >
354
-                <el-option
355
-                  v-for="option in paperCategoryOptions"
356
-                  :key="option.value"
357
-                  :label="option.label"
358
-                  :value="option.value"
359
-                />
360
-              </el-select>
361
-            </el-form-item>
362
-            
363
-            <!-- 选择试卷名称 -->
364
-            <el-form-item label="选择试卷名称" class="form-item-with-border">
365
-              <el-select
366
-                v-model="selectedPaperNames"
367
-                multiple
368
-                filterable
369
-                allow-create
370
-                default-first-option
371
-                placeholder="请选择试卷名称"
372
-                style="width: 100%"
373
-                @change="handlePaperNameChange"
374
-              >
375
-                <el-option
376
-                  v-for="option in paperNameOptions"
377
-                  :key="option.value"
378
-                  :label="option.label"
379
-                  :value="option.value"
380
-                />
381
-              </el-select>
382
-            </el-form-item>
383
-          </div>
384
-          
385
-          <!-- 选择的试卷列表 -->
386
-          <div class="selected-papers-list" v-if="selectedPapers.length > 0">
387
-            <el-table :data="selectedPapers" style="width: 100%" :header-cell-style="{ backgroundColor: '#F7F9FA', color: '#000' }">
388
-              <el-table-column prop="name" label="试卷名称" min-width="200" />
389
-              <el-table-column prop="category" label="类别" min-width="120" />
390
-              <el-table-column prop="creator" label="创建人" min-width="120" />
391
-              <el-table-column prop="totalScore" label="试卷总分" min-width="100" />
392
-              <el-table-column prop="questionCount" label="试题总数" min-width="100" />
393
-              <el-table-column prop="updateDate" label="更新日期" min-width="120" />
394
-              <el-table-column label="操作" width="80" fixed="right">
395
-                <template #default="{ row }">
396
-                  <el-button
397
-                    link
398
-                    type="warning"
399
-                    size="small"
400
-                    @click="removePaper(row.id)"
401
-                    class="custom-delete-btn"
402
-                  >
403
-                    删除
404
-                  </el-button>
405
-                </template>
406
-              </el-table-column>
407
-            </el-table>
408
-          </div>
409
-        </div>
410
-        
411
-        <!-- 操作按钮 -->
412
-        <div class="action-buttons">
413
-          <el-button @click="closeDrawer">取消</el-button>
414
-          <el-button type="primary" @click="publishExam">发布</el-button>
415
-        </div>
416
-      </div>
417
-    </el-drawer>
418
-
419
-    <!-- 选择考试执行人弹框 -->
420
-    <el-dialog
421
-      v-model="executorDialogVisible"
422
-      :title="`考试执行人 已选择 ${examForm.executorCount} 人`"
423
-      width="400px"
424
-      top="200px"
425
-      append-to-body
426
-    >
427
-      <el-tree
428
-        v-model="selectedExecutors"
429
-        :data="executorTreeData"
430
-        show-checkbox
431
-        node-key="value"
432
-        default-expand-all
433
-        style="max-height: 500px; overflow-y: auto;"
434
-      />
435
-      <template #footer>
436
-        <div style="display: flex; gap: 12px; justify-content: flex-end;">
437
-          <el-button @click="cancelExecutorSelection">取消</el-button>
438
-          <el-button type="primary" @click="submitExecutorSelection">提交</el-button>
439
-        </div>
440
-      </template>
441
-    </el-dialog>
442
-  </div>
443
-</template>
444
-
445
-<style lang="scss" scoped>
446
-.exam-publish-drawer {
447
-  padding: 20px;
448
-}
449
-
450
-:deep(.el-drawer__header) {
451
-
452
-  .el-drawer__title {
453
-    color: #000;
454
-  }
455
-}
456
-
457
-.section-title {
458
-  position: relative;
459
-  padding-left: 12px;
460
-  margin-bottom: 20px;
461
-  font-size: 16px;
462
-  font-weight: 500;
463
-  color: #303133;
464
-  
465
-  &::before {
466
-    position: absolute;
467
-    top: 0;
468
-    bottom: 0;
469
-    left: 0;
470
-    width: 3px;
471
-    content: '';
472
-    background-color: #409eff;
473
-    border-radius: 2px;
474
-  }
475
-}
476
-
477
-:deep(.form-item-with-border) {
478
-  .el-form-item__label {
479
-    position: relative;
480
-    padding-left: 12px;
481
-    font-weight: 500;
482
-    color: #000;
483
-    
484
-  }
485
-}
486
-
487
-.grid-cols-1 {
488
-  display: grid;
489
-  grid-template-columns: 1fr;
490
-}
491
-
492
-.md\:grid-cols-2 {
493
-  @media (min-width: 768px) {
494
-    grid-template-columns: repeat(2, 1fr);
495
-  }
496
-}
497
-
498
-.md\:col-span-2 {
499
-  @media (min-width: 768px) {
500
-    grid-column: span 2;
501
-  }
502
-}
503
-
504
-.gap-4 {
505
-  gap: 1rem;
506
-}
507
-
508
-.mb-4 {
509
-  margin-bottom: 1rem;
510
-}
511
-
512
-.action-buttons {
513
-  display: flex;
514
-  gap: 12px;
515
-  justify-content: flex-start;
516
-  margin-top: 30px;
517
-}
518
-
519
-.selected-papers-list {
520
-  margin-top: 20px;
521
-}
522
-
523
-.custom-delete-btn {
524
-  color: #F0864D;
525
-}
526
-</style>

+ 10 - 1
apps/web-ele/src/views/examManage/questionBank/cpn/chapter/config-data.tsx

@@ -16,7 +16,16 @@ export const drawerFormSchema: FormSchemaGetter = () => [
16 16
     componentProps: {
17 17
       api: async () => {
18 18
         const data: any = await questionCategoryTree();
19
-        return data?.rows;
19
+        // 增加一个根目录
20
+        data.rows = [
21
+          {
22
+            id: 0,
23
+            name: '根目录',
24
+            children: [],
25
+          },
26
+          ...data.rows,
27
+        ];
28
+        return data.rows;
20 29
       },
21 30
       labelField: 'name',
22 31
       valueField: 'id',

+ 6 - 2
apps/web-ele/src/views/examManage/questionBank/cpn/chapter/drawer.vue

@@ -1,15 +1,19 @@
1 1
 <script setup lang="ts">
2
+// @ts-ignore
2 3
 import type { Department } from '#/api/system/dept/model';
3 4
 
4
-import { defineEmits, ref } from 'vue';
5
+import { ref } from 'vue';
5 6
 
7
+// @ts-ignore
6 8
 import { useVbenDrawer } from '@vben/common-ui';
7 9
 
10
+// @ts-ignore
8 11
 import { useVbenForm } from '#/adapter/form';
9 12
 import {
10 13
   addQuestionCategory,
11 14
   questionCategoryOne,
12 15
   updateQuestionCategory,
16
+  // @ts-ignore
13 17
 } from '#/api/exam/question/category';
14 18
 
15 19
 import { drawerFormSchema } from './config-data';
@@ -26,7 +30,7 @@ const [Form, formApi] = useVbenForm({
26 30
 });
27 31
 const isUpdateRef = ref<boolean>(false);
28 32
 const [Drawer, drawerApi] = useVbenDrawer({
29
-  async onOpenChange(isOpen) {
33
+  async onOpenChange(isOpen: any) {
30 34
     if (!isOpen) {
31 35
       return;
32 36
     }

+ 1 - 1
apps/web-ele/src/views/examManage/questionBank/cpn/questionEdit/index.vue

@@ -67,7 +67,7 @@ const removeOption = (index: number) => {
67 67
 
68 68
     // 如果删除后没有正确答案,默认选择第一个选项
69 69
     if (formData.value.correctAnswer.length === 0) {
70
-      formData.value.correctAnswer = [0];
70
+      formData.value.correctAnswer = ['0'];
71 71
     }
72 72
   }
73 73
 };

+ 0 - 118
apps/web-ele/src/views/examManage/questionBank/cpn/questionPreview/index.vue

@@ -1,118 +0,0 @@
1
-<template>
2
-  <div class="question-preview">
3
-    <div class="p-4 bg-white rounded-lg shadow-sm">
4
-      <div class="flex items-center justify-between mb-4">
5
-        <h2 class="text-lg font-semibold text-gray-800">
6
-          试题预览
7
-        </h2>
8
-        <el-button @click="handleBack">返回列表</el-button>
9
-      </div>
10
-      
11
-      <!-- 试题预览内容 -->
12
-      <div v-if="questionData" class="question-content">
13
-        <div class="mb-4">
14
-          <h3 class="text-lg font-medium text-gray-900 mb-2">
15
-            试题内容
16
-          </h3>
17
-          <p class="text-gray-700">{{ questionData.content }}</p>
18
-        </div>
19
-        
20
-        <div class="mb-4">
21
-          <h3 class="text-lg font-medium text-gray-900 mb-2">
22
-            试题信息
23
-          </h3>
24
-          <el-descriptions :column="2" border>
25
-            <el-descriptions-item label="试题类型">
26
-              {{ getQuestionTypeText(questionData.questionType) }}
27
-            </el-descriptions-item>
28
-            <el-descriptions-item label="难度等级">
29
-              {{ getDifficultyText(questionData.difficulty) }}
30
-            </el-descriptions-item>
31
-            <el-descriptions-item label="所属章节">
32
-              {{ questionData.chapterName }}
33
-            </el-descriptions-item>
34
-            <el-descriptions-item label="分值">
35
-              {{ questionData.score }}分
36
-            </el-descriptions-item>
37
-          </el-descriptions>
38
-        </div>
39
-      </div>
40
-      
41
-      <div v-else class="text-center py-10 text-gray-500">
42
-        加载中...
43
-      </div>
44
-    </div>
45
-  </div>
46
-</template>
47
-
48
-<script setup lang="ts">
49
-import { ref, onMounted } from 'vue';
50
-import { useRoute, useRouter } from 'vue-router';
51
-
52
-const route = useRoute();
53
-const router = useRouter();
54
-const id = route.params.id as string;
55
-
56
-// 试题数据
57
-const questionData = ref(null);
58
-
59
-// 加载试题数据
60
-const loadQuestionData = async () => {
61
-  if (id) {
62
-    // 这里应该调用API获取试题详情
63
-    // 暂时使用模拟数据
64
-    questionData.value = {
65
-      id: id,
66
-      content: '这是一道测试题,用于预览功能展示。',
67
-      questionType: '1',
68
-      difficulty: '2',
69
-      chapterId: '1',
70
-      chapterName: '第一章 基础知识',
71
-      score: 10,
72
-    };
73
-  }
74
-};
75
-
76
-// 获取试题类型文本
77
-const getQuestionTypeText = (type) => {
78
-  const typeMap = {
79
-    '1': '单选题',
80
-    '2': '多选题',
81
-    '3': '判断题',
82
-    '4': '填空题',
83
-    '5': '简答题',
84
-  };
85
-  return typeMap[type] || '未知类型';
86
-};
87
-
88
-// 获取难度等级文本
89
-const getDifficultyText = (difficulty) => {
90
-  const difficultyMap = {
91
-    '1': '简单',
92
-    '2': '中等',
93
-    '3': '困难',
94
-  };
95
-  return difficultyMap[difficulty] || '未知难度';
96
-};
97
-
98
-// 返回列表
99
-const handleBack = () => {
100
-  router.push('/examManage/questionBank');
101
-};
102
-
103
-onMounted(() => {
104
-  loadQuestionData();
105
-});
106
-</script>
107
-
108
-<style scoped>
109
-.question-preview {
110
-  min-height: 100vh;
111
-  background-color: #f5f7fa;
112
-  padding: 20px;
113
-}
114
-
115
-.question-content {
116
-  line-height: 1.6;
117
-}
118
-</style>

+ 52 - 22
apps/web-ele/src/views/examManage/questionBank/index.vue

@@ -1,11 +1,14 @@
1 1
 <script lang="ts" setup>
2
+// @ts-ignore
2 3
 import type { VbenFormProps } from '@vben/common-ui';
3 4
 
5
+// @ts-ignore
4 6
 import type { VxeGridProps } from '#/adapter/vxe-table';
5 7
 
6 8
 import { onMounted, ref } from 'vue';
7
-import { useRoute, useRouter } from 'vue-router';
9
+import { useRouter } from 'vue-router';
8 10
 
11
+// @ts-ignore
9 12
 import { Page, useVbenDrawer, useVbenModal } from '@vben/common-ui';
10 13
 
11 14
 import {
@@ -15,31 +18,29 @@ import {
15 18
   Expand,
16 19
   Plus,
17 20
 } from '@element-plus/icons-vue';
18
-import {
19
-  ElButton,
20
-  ElIcon,
21
-  ElMessage,
22
-  ElMessageBox,
23
-  ElSpace,
24
-  ElTree,
25
-} from 'element-plus';
21
+import { ElButton, ElIcon, ElMessageBox, ElSpace, ElTree } from 'element-plus';
26 22
 
23
+// @ts-ignore
27 24
 import { useVbenVxeGrid } from '#/adapter/vxe-table';
28 25
 import {
29 26
   deleteQuestionCategory,
30 27
   questionCategoryTree,
28
+  // @ts-ignore
31 29
 } from '#/api/exam/question/category';
30
+// @ts-ignore
32 31
 import { deleteQuestion, questionList } from '#/api/exam/question/question';
33 32
 // 导入预览组件
33
+// @ts-ignore
34 34
 import QuestionPreview from '#/components/question';
35 35
 
36 36
 import { queryFormSchema, tableColumns } from './config-data';
37
+// @ts-ignore
37 38
 import ChapterDrawer from './cpn/chapter/drawer.vue';
38 39
 
39 40
 // 路由实例
40 41
 const router = useRouter();
41
-const route = useRoute();
42
-const classId = route.params.classId;
42
+// const route = useRoute();
43
+// const classId = route.params.classId;
43 44
 
44 45
 // 菜单折叠状态
45 46
 const isCollapse = ref(false);
@@ -125,7 +126,7 @@ const [BasicTable, basicTableApi] = useVbenVxeGrid({
125 126
 });
126 127
 
127 128
 // 点击章节
128
-const handleChapterClick = (node) => {
129
+const handleChapterClick = (node: any) => {
129 130
   selectedChapter.value = {
130 131
     id: node.id,
131 132
     label: node.label,
@@ -165,14 +166,9 @@ const addQuestion = () => {
165 166
 };
166 167
 
167 168
 // 导入试题
168
-const importQuestions = () => {
169
-  ElMessage.info('导入试题功能待实现');
170
-};
171
-
172
-// 导出试题
173
-const exportQuestions = () => {
174
-  ElMessage.info('导出试题功能待实现');
175
-};
169
+// const importQuestions = () => {
170
+//   ElMessage.info('导入试题功能待实现');
171
+// };
176 172
 
177 173
 // 编辑试题
178 174
 const editQuestion = (row: any) => {
@@ -306,6 +302,18 @@ onMounted(() => {
306 302
                     </div>
307 303
                   </div>
308 304
                 </template>
305
+                <template #empty>
306
+                  <div class="tree-empty">
307
+                    <span class="el-tree__empty-text">暂无分类</span>
308
+                    <ElButton
309
+                      type="primary"
310
+                      class="mt-10 w-full"
311
+                      @click="addChapter"
312
+                    >
313
+                      <ElIcon><Plus /></ElIcon> 添加分类
314
+                    </ElButton>
315
+                  </div>
316
+                </template>
309 317
               </ElTree>
310 318
             </div>
311 319
           </div>
@@ -340,9 +348,9 @@ onMounted(() => {
340 348
                   <ElButton type="primary" @click="addQuestion">
341 349
                     新增试题
342 350
                   </ElButton>
343
-                  <ElButton type="primary" @click="importQuestions">
351
+                  <!-- <ElButton type="primary" @click="importQuestions">
344 352
                     导入
345
-                  </ElButton>
353
+                  </ElButton> -->
346 354
                   <!-- <ElButton type="primary" @click="exportQuestions">
347 355
                     导出
348 356
                   </ElButton> -->
@@ -515,4 +523,26 @@ onMounted(() => {
515 523
   display: flex;
516 524
   //gap: 8px;
517 525
 }
526
+
527
+.tree-empty {
528
+  display: contents;
529
+  // flex-direction: column;
530
+  align-items: center;
531
+  // justify-content: center;
532
+  padding: 20px 20px;
533
+  text-align: center;
534
+  min-height: 300px;
535
+}
536
+
537
+.tree-empty .el-tree__empty-text {
538
+  margin-top: 0px;
539
+  font-size: 16px;
540
+  color: #909399;
541
+}
542
+
543
+.tree-empty .el-button {
544
+  margin-top: 100px;
545
+  width: 200px;
546
+  margin-bottom: 0px;
547
+}
518 548
 </style>