Pārlūkot izejas kodu

feat: api-考生管理

zhangjie 3 dienas atpakaļ
vecāks
revīzija
b13a5bbbb1

+ 2 - 0
components.d.ts

@@ -60,6 +60,8 @@ declare module '@vue/runtime-core' {
     PageBreadcrumb: typeof import('./src/components/page-breadcrumb/index.vue')['default'];
     RouterLink: typeof import('vue-router')['RouterLink'];
     RouterView: typeof import('vue-router')['RouterView'];
+    SelectClass: typeof import('./src/components/select-class/index.vue')['default'];
+    SelectCollege: typeof import('./src/components/select-college/index.vue')['default'];
     SelectCourse: typeof import('./src/components/select-course/index.vue')['default'];
     SelectExam: typeof import('./src/components/select-exam/index.vue')['default'];
     SelectImgArea: typeof import('./src/components/select-img-area/index.vue')['default'];

+ 17 - 9
src/api/base.ts

@@ -1,17 +1,25 @@
 import axios from 'axios';
-import type { ExamItem, SubjectItem } from './types/base';
-// import type { ExamQueryItem } from './types/exam';
+import type { SubjectItem } from './types/base';
+import type { ExamQueryItem } from './types/exam';
 
 // 通用查询
 // 通用查询-考试
-export function examQuery(): Promise<ExamItem[]> {
-  return axios.post('/api/admin/apply/exam/list', {});
+export function examQuery(): Promise<ExamQueryItem[]> {
+  return axios.post('/api/admin/exam/list', {});
 }
 // 通用查询-科目
 export function subjectQuery(examId: number): Promise<SubjectItem[]> {
-  return axios.post(
-    '/api/admin/apply/subject/list',
-    {},
-    { params: { examId } }
-  );
+  return axios.post('/api/admin/subject/query', {}, { params: { examId } });
+}
+// 通用查询-学院
+export function collegeQuery(): Promise<string[]> {
+  return axios.post('/api/admin/student/college/list', {});
+}
+// 通用查询-班级
+export function classQuery(): Promise<string[]> {
+  return axios.post('/api/admin/student/className/list', {});
+}
+// 通用查询-考点
+export function examSiteQuery(): Promise<string[]> {
+  return axios.post('/api/admin/student/examSite/list', {});
 }

+ 82 - 2
src/api/student.ts

@@ -11,9 +11,89 @@ import {
 export function getStudentList(
   params: StudentListPageParam
 ): Promise<StudentListPageRes> {
-  return axios.post('/api/student/list', {}, { params });
+  return axios.post('/api/admin/student/list', {}, { params });
 }
 // 新增或编辑考生
 export function updateStudent(data: StudentUpdateParam): Promise<StudentItem> {
-  return axios.post('/api/student/update', data);
+  if (data.id) {
+    return axios.post('/api/admin/student/update', data);
+  }
+  return axios.post('/api/admin/student/add', data);
+}
+
+// 删除考生
+export function deleteStudent(id: number): Promise<StudentItem> {
+  return axios.post('/api/admin/student/delete', {}, { params: { id } });
+}
+
+// 重置违纪
+export function resetBreach(id: number): Promise<StudentItem> {
+  return axios.post('/api/admin/student/updateBreach', {}, { params: { id } });
+}
+
+// 导出考生
+export function exportStudent(
+  params: Record<string, any>
+): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/student/export',
+    {},
+    {
+      responseType: 'blob',
+      params,
+    }
+  );
+}
+
+// 下载考生导入模板
+export function studentTemplate(): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/student/template',
+    {},
+    {
+      responseType: 'blob',
+    }
+  );
+}
+// 下载缺考考生导入模板
+export function absentStudentTemplate(): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/student/absentTemplate',
+    {},
+    {
+      responseType: 'blob',
+    }
+  );
+}
+// 下载违纪考生导入模板
+export function breachStudentTemplate(): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/student/breachTemplate',
+    {},
+    {
+      responseType: 'blob',
+    }
+  );
+}
+
+// 下载整理异常导入模板
+export function exceptionStudentTemplate(): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/student/exceptionTemplate',
+    {},
+    {
+      responseType: 'blob',
+    }
+  );
+}
+
+// 下载多媒体考生上传模板
+export function mediaStudentTemplate(): Promise<AxiosResponse<Blob>> {
+  return axios.post(
+    '/api/admin/student/mediaTemplate',
+    {},
+    {
+      responseType: 'blob',
+    }
+  );
 }

+ 3 - 1
src/api/types/base.ts

@@ -97,8 +97,10 @@ export interface RoomUpdateParams {
 
 export interface ExamItem {
   id: number;
+  // 考试名称
   name: string;
-  code: string;
+  // 类型
+  type: ExamType;
 }
 export interface SubjectItem {
   name: string;

+ 1 - 0
src/api/types/exam.ts

@@ -80,6 +80,7 @@ export type ExamQueryItem = Pick<
   | 'startTime'
   | 'endTime'
   | 'status'
+  | 'markMode'
   | 'forceSpecialTag'
 >;
 

+ 51 - 34
src/api/types/student.ts

@@ -2,70 +2,87 @@ import { PageResult, PageParams } from './common';
 
 export interface StudentListFilter {
   // 姓名
-  name: string;
+  name?: string;
   // 准考证号
-  examNo: string;
+  examNumber?: string;
   // 密号
-  secretNo: string;
+  secretNumber?: string;
   // 学号
-  studentNo: string;
+  studentCode?: string;
   // 科目
-  subject: number | null;
+  subjectCode?: string;
   // 层次
-  level: string;
+  subjectLevel?: string;
   // 专业类型
-  majorType: string;
-  // 状态
-  status: string;
+  subjectCategory?: string;
+  // 状态相关 -- >
+  // 上传
+  upload?: boolean;
+  // 缺考
+  absent?: boolean;
+  // 人工指定缺考
+  manualAbsent?: boolean;
+  // 违纪
+  breach?: boolean;
+  // 状态相关 -- />
   // 批次编号
-  batchNo: string;
+  batchCode?: string;
   // 签到表编号
-  signBookNo: string;
+  packageCode?: string;
   // 学院
-  college: string;
+  college?: string;
   // 班级
-  className: string;
+  className?: string;
   // 任课老师
-  teacher: string;
+  teacher?: string;
   // 考点
-  examSite: string;
+  examSite?: string;
   // 考场
-  examRoom: string;
+  examRoom?: string;
   // 扫描张数
-  scanPages?: number;
+  sheetCount?: number;
 }
 export type StudentListPageParam = PageParams<StudentListFilter>;
 
 export interface StudentItem {
   id: number;
   // 准考证号
-  examNo: string;
+  examNumber: string;
   // 密号
-  secretNo: string;
+  secretNumber: string;
   // 姓名
   name: string;
   // 学号
-  studentNo: string;
+  studentCode: string;
   // 科目
-  subject: string;
+  subjectName: string;
+  subjectCode: string;
   // 试卷类型
-  examType: string;
+  paperType: string;
   // 扫描图
   sheetUrls: string[];
   // 层次
-  level: string;
+  subjectLevel: string;
   // 专业类型
-  majorType: string;
-  // 扫描识别
-  scanRecognition: boolean;
+  subjectCategory: string;
+  // 扫描识别相关-- >
+  // 上传
+  upload: boolean;
+  // 缺考
+  absent: boolean;
+  // 扫描识别相关-- />
+  // 人工指定相关-- >
+  // 人工指定缺考
+  manualAbsent: boolean;
+  // 违纪
+  breach: boolean;
+  // 人工指定相关-- />
   // 扫描张数
-  scanPages: number;
-  // 人工指定
-  manualAssign: boolean;
+  sheetCount: number;
   // 批次编号
-  batchNo: string;
+  batchCode: string;
   // 签到表编号
-  signBookNo: string;
+  packageCode: string;
   // 学院
   college: string;
   // 班级
@@ -85,9 +102,9 @@ export interface StudentUpdateParam {
   // 姓名
   name: string;
   // 学号
-  studentNo: string;
+  studentCode: string;
   // 准考证号
-  examno: string;
+  examNumber: string;
   // 科目
   subject: string;
   // 学院
@@ -97,5 +114,5 @@ export interface StudentUpdateParam {
   // 任课老师
   teacher: string;
   // 签到表编号
-  signBookNo: string;
+  packageCode: string;
 }

+ 4 - 0
src/components/index.ts

@@ -9,6 +9,8 @@ import SelectRangeTime from './select-range-time/index.vue';
 import UploadButton from './upload-button/index.vue';
 import SelectExam from './select-exam/index.vue';
 import SelectSubject from './select-subject/index.vue';
+import SelectCollege from './select-college/index.vue';
+import SelectClass from './select-class/index.vue';
 import ImportDialog from './import-dialog/index.vue';
 import TableField from './table-field/index.vue';
 import PageBreadcrumb from './page-breadcrumb/index.vue';
@@ -22,6 +24,8 @@ export default {
     Vue.component('SelectRangeTime', SelectRangeTime);
     Vue.component('UploadButton', UploadButton);
     Vue.component('SelectExam', SelectExam);
+    Vue.component('SelectCollege', SelectCollege);
+    Vue.component('SelectClass', SelectClass);
     Vue.component('SelectSubject', SelectSubject);
     Vue.component('ImportDialog', ImportDialog);
     Vue.component('TableField', TableField);

+ 84 - 0
src/components/select-class/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <el-select
+    v-model="selected"
+    :placeholder="props.placeholder"
+    :clearable="props.clearable"
+    :disabled="props.disabled"
+    filterable
+    default-first-option
+    v-bind="attrs"
+    @change="onChange"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.value"
+      :label="item.label"
+      :value="item.value"
+    />
+  </el-select>
+</template>
+
+<script setup lang="ts">
+  import { ref, useAttrs, watch } from 'vue';
+  import { classQuery } from '@/api/base';
+
+  defineOptions({
+    name: 'SelectClass',
+  });
+
+  type ValueType = string | Array<string> | null;
+
+  const props = withDefaults(
+    defineProps<{
+      modelValue: ValueType;
+      clearable?: boolean;
+      disabled?: boolean;
+      placeholder?: string;
+      multiple?: boolean;
+    }>(),
+    {
+      clearable: true,
+      disabled: false,
+      placeholder: '请选择',
+      multiple: false,
+    }
+  );
+  const emit = defineEmits(['update:modelValue', 'change']);
+  const attrs = useAttrs();
+
+  interface OptionListItem {
+    value: string;
+    label: string;
+  }
+
+  const selected = ref<ValueType>();
+  const optionList = ref<OptionListItem[]>([]);
+  const search = async () => {
+    const res = await classQuery();
+    optionList.value = res.map((item) => {
+      return { value: item, label: item };
+    });
+  };
+  search(); // Initial load
+
+  const onChange = () => {
+    const selectedData = props.multiple
+      ? optionList.value.filter(
+          (item) =>
+            selected.value && (selected.value as string[]).includes(item.value)
+        )
+      : optionList.value.filter((item) => selected.value === item.value);
+    emit('update:modelValue', selected.value || null);
+    emit('change', props.multiple ? selectedData : selectedData[0]);
+  };
+
+  watch(
+    () => props.modelValue,
+    (val) => {
+      selected.value = val || undefined;
+    },
+    {
+      immediate: true,
+    }
+  );
+</script>

+ 84 - 0
src/components/select-college/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <el-select
+    v-model="selected"
+    :placeholder="props.placeholder"
+    :clearable="props.clearable"
+    :disabled="props.disabled"
+    filterable
+    default-first-option
+    v-bind="attrs"
+    @change="onChange"
+  >
+    <el-option
+      v-for="item in optionList"
+      :key="item.value"
+      :label="item.label"
+      :value="item.value"
+    />
+  </el-select>
+</template>
+
+<script setup lang="ts">
+  import { ref, useAttrs, watch } from 'vue';
+  import { collegeQuery } from '@/api/base';
+
+  defineOptions({
+    name: 'SelectCollege',
+  });
+
+  type ValueType = string | Array<string> | null;
+
+  const props = withDefaults(
+    defineProps<{
+      modelValue: ValueType;
+      clearable?: boolean;
+      disabled?: boolean;
+      placeholder?: string;
+      multiple?: boolean;
+    }>(),
+    {
+      clearable: true,
+      disabled: false,
+      placeholder: '请选择',
+      multiple: false,
+    }
+  );
+  const emit = defineEmits(['update:modelValue', 'change']);
+  const attrs = useAttrs();
+
+  interface OptionListItem {
+    value: string;
+    label: string;
+  }
+
+  const selected = ref<ValueType>();
+  const optionList = ref<OptionListItem[]>([]);
+  const search = async () => {
+    const res = await collegeQuery();
+    optionList.value = res.map((item) => {
+      return { value: item, label: item };
+    });
+  };
+  search(); // Initial load
+
+  const onChange = () => {
+    const selectedData = props.multiple
+      ? optionList.value.filter(
+          (item) =>
+            selected.value && (selected.value as string[]).includes(item.value)
+        )
+      : optionList.value.filter((item) => selected.value === item.value);
+    emit('update:modelValue', selected.value || null);
+    emit('change', props.multiple ? selectedData : selectedData[0]);
+  };
+
+  watch(
+    () => props.modelValue,
+    (val) => {
+      selected.value = val || undefined;
+    },
+    {
+      immediate: true,
+    }
+  );
+</script>

+ 0 - 89
src/components/select-teaching/index.vue

@@ -1,89 +0,0 @@
-<template>
-  <el-select
-    v-model="selected"
-    :placeholder="placeholder"
-    :clearable="clearable"
-    :disabled="disabled"
-    filterable
-    default-first-option
-    v-bind="attrs"
-    @change="onChange"
-  >
-    <template v-if="prefix || prefixStr" #prefix>
-      <span style="padding-left: 12px"> {{ prefixStr || '教学点' }}</span>
-    </template>
-  </el-select>
-</template>
-
-<script setup lang="ts">
-  import { ref, useAttrs, watch } from 'vue';
-  // import { ElSelect, ElOption } from 'element-plus';
-
-  defineOptions({
-    name: 'SelectTeaching',
-  });
-
-  type ValueType = number | Array<number> | null;
-
-  const props = defineProps<{
-    modelValue: ValueType;
-    clearable?: boolean;
-    disabled?: boolean;
-    placeholder?: string;
-    multiple?: boolean;
-    prefix?: boolean;
-    prefixStr?: string;
-    flag?: boolean;
-    taskId?: any;
-  }>();
-  const emit = defineEmits(['update:modelValue', 'change', 'getOptions']);
-  const attrs = useAttrs();
-
-  interface OptionListItem {
-    value: number;
-    label: string;
-  }
-
-  const selected = ref<number | Array<number> | undefined>();
-  const optionList = ref<OptionListItem[]>([]);
-  const search = () => {
-    // TODO: get task list
-    // optionList.value = [].map((item) => {
-    //   return { ...item, value: item.id, label: item.name };
-    // });
-  };
-  search(); // Initial load
-
-  const onChange = () => {
-    const selectedData = props.multiple
-      ? optionList.value.filter(
-          (item) =>
-            selected.value && (selected.value as number[]).includes(item.value)
-        )
-      : optionList.value.filter((item) => selected.value === item.value);
-    emit('update:modelValue', selected.value || null);
-    emit('change', props.multiple ? selectedData : selectedData[0]);
-  };
-
-  watch(
-    () => props.modelValue,
-    (val) => {
-      selected.value = val || undefined;
-    },
-    {
-      immediate: true,
-    }
-  );
-  watch(
-    () => props.taskId,
-    (val) => {
-      if (!val) {
-        optionList.value = [];
-        selected.value = undefined;
-        emit('update:modelValue', selected.value || null);
-      } else {
-        search();
-      }
-    }
-  );
-</script>

+ 2 - 2
src/store/modules/app/types.ts

@@ -1,4 +1,4 @@
-import { ExamItem } from '@/api/types/base';
+import { ExamQueryItem } from '@/api/types/exam';
 
 export interface UserMenuItem {
   id: number;
@@ -29,5 +29,5 @@ export interface AppState {
   validRoutes: string[];
   breadcrumbs: string[];
   device: string;
-  curExam: ExamItem;
+  curExam: ExamQueryItem;
 }

+ 22 - 0
src/utils/download-export.ts

@@ -7,6 +7,14 @@ import {
   bindStudentTemplate,
   headerInspectorTemplate,
 } from '@/api/user';
+import {
+  studentTemplate,
+  absentStudentTemplate,
+  breachStudentTemplate,
+  exceptionStudentTemplate,
+  mediaStudentTemplate,
+  exportStudent,
+} from '@/api/student';
 import useLoading from '@/hooks/loading';
 
 import { downloadByApi } from './download';
@@ -14,6 +22,7 @@ import { downloadByApi } from './download';
 const { loading, setLoading } = useLoading();
 
 const downloadConfig = {
+  // user
   // 评卷员班级导入模板下载
   markerClassTemplate,
   // 按考试导出用户
@@ -26,6 +35,19 @@ const downloadConfig = {
   bindStudentTemplate,
   // 科组长-复核员导入模板下载
   headerInspectorTemplate,
+  // student
+  // 下载考生导入模板
+  studentTemplate,
+  // 下载缺考考生导入模板
+  absentStudentTemplate,
+  // 下载违纪考生导入模板
+  breachStudentTemplate,
+  // 下载整理异常导入模板
+  exceptionStudentTemplate,
+  // 下载多媒体考生上传模板
+  mediaStudentTemplate,
+  // 导出考生
+  exportStudent,
 };
 
 type DownloadType = keyof typeof downloadConfig;

+ 13 - 11
src/views/student/ModifyStudent.vue

@@ -22,12 +22,12 @@
       <el-form-item label="姓名" prop="name">
         <el-input v-model="formModel.name" placeholder="请输入姓名" />
       </el-form-item>
-      <el-form-item label="学号" prop="studentNo">
-        <el-input v-model="formModel.studentNo" placeholder="请输入学号" />
+      <el-form-item label="学号" prop="studentCode">
+        <el-input v-model="formModel.studentCode" placeholder="请输入学号" />
       </el-form-item>
-      <el-form-item label="准考证号" prop="examno">
+      <el-form-item label="准考证号" prop="examNumber">
         <el-input
-          v-model="formModel.examno"
+          v-model="formModel.examNumber"
           placeholder="请输入准考证号"
           :disabled="isEdit"
         />
@@ -43,7 +43,7 @@
       </el-form-item>
       <el-form-item label="签到表编号">
         <el-input
-          v-model="formModel.signBookNo"
+          v-model="formModel.packageCode"
           placeholder="请输入签到表编号"
         />
       </el-form-item>
@@ -92,12 +92,12 @@
   const formRef = ref<FormInstance>();
   const initialFormState: Partial<StudentUpdateParam> = {
     name: '',
-    studentNo: '',
-    examno: '',
+    studentCode: '',
+    examNumber: '',
     college: '',
     className: '',
     teacher: '',
-    signBookNo: '',
+    packageCode: '',
     subject: '',
   };
 
@@ -106,12 +106,14 @@
   const rules: FormRules<keyof StudentUpdateParam> = {
     subject: [{ required: true, message: '请选择科目', trigger: 'change' }],
     name: [{ required: true, message: '请输入姓名', trigger: 'change' }],
-    studentNo: [{ required: true, message: '请输入学号', trigger: 'change' }],
-    examno: [{ required: true, message: '请输入准考证号', trigger: 'change' }],
+    studentCode: [{ required: true, message: '请输入学号', trigger: 'change' }],
+    examNumber: [
+      { required: true, message: '请输入准考证号', trigger: 'change' },
+    ],
     college: [{ required: true, message: '请输入学院', trigger: 'change' }],
     className: [{ required: true, message: '请输入班级', trigger: 'change' }],
     teacher: [{ required: true, message: '请输入任课老师', trigger: 'change' }],
-    signBookNo: [
+    packageCode: [
       { required: true, message: '请输入签到表编号', trigger: 'change' },
     ],
   };

+ 173 - 121
src/views/student/StudentManage.vue

@@ -6,54 +6,49 @@
           v-model.trim="searchModel.name"
           placeholder="请选择"
           clearable
-          style="width: 120px"
         >
         </el-input>
       </el-form-item>
       <el-form-item label="准考证号">
         <el-input
-          v-model.trim="searchModel.examNo"
+          v-model.trim="searchModel.examNumber"
           placeholder="请选择"
           clearable
-          style="width: 120px"
         >
         </el-input>
       </el-form-item>
       <el-form-item label="密号">
         <el-input
-          v-model.trim="searchModel.secretNo"
+          v-model.trim="searchModel.secretNumber"
           placeholder="请选择"
           clearable
-          style="width: 120px"
         >
         </el-input>
       </el-form-item>
       <el-form-item label="学号">
         <el-input
-          v-model.trim="searchModel.studentNo"
+          v-model.trim="searchModel.studentCode"
           placeholder="请选择"
           clearable
-          style="width: 120px"
         >
         </el-input>
       </el-form-item>
       <el-form-item label="科目">
-        <select-subject v-model="searchModel.subject"></select-subject>
+        <select-subject v-model="searchModel.subjectCode"></select-subject>
       </el-form-item>
       <el-form-item label="层次">
         <el-select
-          v-model="searchModel.level"
-          placeholder="请选择"
+          v-model="searchModel.subjectLevel"
+          placeholder="不限"
           clearable
           style="width: 120px"
         >
           <el-option label="请选择" value="" />
-          <el-option label="不限" value="不限" />
         </el-select>
       </el-form-item>
       <el-form-item label="专业类型">
         <el-select
-          v-model="searchModel.majorType"
+          v-model="searchModel.subjectCategory"
           placeholder="请选择"
           clearable
           style="width: 120px"
@@ -64,68 +59,70 @@
       <el-form-item label="状态">
         <el-space>
           <el-select
-            v-model="searchModel.status"
-            placeholder="请选择"
+            v-model="searchModel.upload"
+            placeholder="不限"
             clearable
             style="width: 120px"
           >
-            <el-option label="不限" value="" />
+            <el-option label="已上传" :value="true" />
+            <el-option label="未上传" :value="false" />
           </el-select>
-          <el-select placeholder="不限" clearable style="width: 120px">
-            <el-option label="不限" value="" />
+          <el-select
+            v-model="searchModel.absent"
+            placeholder="不限"
+            clearable
+            style="width: 120px"
+          >
+            <el-option label="缺考" :value="true" />
+            <el-option label="正常" :value="false" />
           </el-select>
-          <el-select placeholder="不限" clearable style="width: 120px">
-            <el-option label="不限" value="" />
+          <el-select
+            v-model="searchModel.breach"
+            placeholder="不限"
+            clearable
+            style="width: 120px"
+          >
+            <el-option label="违纪" :value="true" />
+            <el-option label="正常" :value="false" />
           </el-select>
-          <el-select placeholder="不限" clearable style="width: 120px">
-            <el-option label="不限" value="" />
+          <el-select
+            v-model="searchModel.manualAbsent"
+            placeholder="不限"
+            clearable
+            style="width: 140px"
+          >
+            <el-option label="人工指定缺考" :value="true" />
+            <el-option label="正常" :value="false" />
           </el-select>
         </el-space>
       </el-form-item>
       <el-form-item label="签到表编号">
         <el-input
-          v-model.trim="searchModel.signBookNo"
+          v-model.trim="searchModel.packageCode"
           placeholder="请选择"
           clearable
-          style="width: 120px"
         >
         </el-input>
       </el-form-item>
       <el-form-item label="批次编号">
         <el-input
-          v-model.trim="searchModel.batchNo"
+          v-model.trim="searchModel.batchCode"
           placeholder="请选择"
           clearable
-          style="width: 120px"
         >
         </el-input>
       </el-form-item>
       <el-form-item label="学院">
-        <el-select
-          v-model="searchModel.college"
-          placeholder="请选择"
-          clearable
-          style="width: 120px"
-        >
-          <el-option label="请选择" value="" />
-        </el-select>
+        <select-college v-model="searchModel.college"> </select-college>
       </el-form-item>
       <el-form-item label="班级">
-        <el-select
-          v-model="searchModel.className"
-          placeholder="请选择"
-          clearable
-          style="width: 120px"
-        >
-          <el-option label="请选择" value="" />
-        </el-select>
+        <select-class v-model="searchModel.className"> </select-class>
       </el-form-item>
       <el-form-item label="任课老师">
         <el-input
           v-model.trim="searchModel.teacher"
           placeholder="请选择"
           clearable
-          style="width: 120px"
         >
         </el-input>
       </el-form-item>
@@ -134,7 +131,6 @@
           v-model.trim="searchModel.examSite"
           placeholder="请选择"
           clearable
-          style="width: 120px"
         >
         </el-input>
       </el-form-item>
@@ -143,15 +139,14 @@
           v-model.trim="searchModel.examRoom"
           placeholder="请选择"
           clearable
-          style="width: 120px"
         >
         </el-input>
       </el-form-item>
       <el-form-item label="扫描张数">
         <el-input-number
-          v-model.number="searchModel.scanPages"
-          :min="1"
-          :max="20"
+          v-model.number="searchModel.sheetCount"
+          :min="0"
+          :max="30"
           :step="1"
           :precision="0"
           :controls="false"
@@ -169,17 +164,17 @@
         <el-button type="success" @click="onImport">导入</el-button>
         <el-dropdown @command="onImportCommand">
           <el-button type="primary">
-            导入
+            导入名单
             <el-icon class="el-icon--right"><ArrowDown /> </el-icon>
           </el-button>
           <template #dropdown>
             <el-dropdown-menu>
-              <el-dropdown-item command="breaking">违纪名单</el-dropdown-item>
-              <el-dropdown-item command="miss">缺考名单</el-dropdown-item>
+              <el-dropdown-item command="breach">违纪名单</el-dropdown-item>
+              <el-dropdown-item command="absent">缺考名单</el-dropdown-item>
             </el-dropdown-menu>
           </template>
         </el-dropdown>
-        <el-button @click="exportData">导入名单</el-button>
+        <el-button @click="exportData">导</el-button>
       </el-space>
 
       <table-field v-model="headFields" :fields="tableFields"></table-field>
@@ -195,14 +190,14 @@
     >
       <el-table-column type="index" label="序号" width="60" />
       <el-table-column
-        v-if="checkFieldVisible('examNo')"
-        prop="examNo"
+        v-if="checkFieldVisible('examNumber')"
+        prop="examNumber"
         label="准考证号"
         width="120"
       />
       <el-table-column
-        v-if="checkFieldVisible('secretNo')"
-        prop="secretNo"
+        v-if="checkFieldVisible('secretNumber')"
+        prop="secretNumber"
         label="密号"
         min-width="100"
       />
@@ -213,46 +208,48 @@
         min-width="100"
       />
       <el-table-column
-        v-if="checkFieldVisible('studentNo')"
-        prop="studentNo"
+        v-if="checkFieldVisible('studentCode')"
+        prop="studentCode"
         label="学号"
         width="120"
       />
       <el-table-column
-        v-if="checkFieldVisible('subject')"
-        prop="subject"
+        v-if="checkFieldVisible('subjectName')"
+        prop="subjectName"
         label="科目"
         min-width="100"
       />
       <el-table-column
-        v-if="checkFieldVisible('examType')"
-        prop="examType"
+        v-if="checkFieldVisible('paperType')"
+        prop="paperType"
         label="试卷类型"
         width="100"
       />
       <el-table-column
-        v-if="checkFieldVisible('level')"
-        prop="level"
+        v-if="checkFieldVisible('subjectLevel')"
+        prop="subjectLevel"
         label="层次"
         width="80"
       />
       <el-table-column
-        v-if="checkFieldVisible('majorType')"
-        prop="majorType"
+        v-if="checkFieldVisible('subjectCategory')"
+        prop="subjectCategory"
         label="专业类型"
         width="100"
       />
       <el-table-column
-        v-if="checkFieldVisible('scanRecognition')"
+        v-if="checkFieldVisible('upload')"
         label="扫描识别"
         width="100"
       >
         <template #default="scope">
-          <el-tag :type="scope.row.scanRecognition ? 'success' : 'danger'">
-            {{ scope.row.scanRecognition ? '是' : '否' }}
+          <el-tag :type="scope.row.upload ? 'success' : 'danger'">
+            {{ scope.row.upload ? '是' : '否' }}
           </el-tag>
           <el-button
             v-if="scope.row.sheetUrls.length > 0"
+            size="small"
+            type="primary"
             link
             style="margin-left: 5px"
             @click="onPreviewImg(scope.row)"
@@ -262,31 +259,31 @@
         </template>
       </el-table-column>
       <el-table-column
-        v-if="checkFieldVisible('scanPages')"
-        prop="scanPages"
+        v-if="checkFieldVisible('sheetCount')"
+        prop="sheetCount"
         label="扫描张数"
         width="100"
       />
       <el-table-column
-        v-if="checkFieldVisible('manualAssign')"
+        v-if="checkFieldVisible('manualAbsent')"
         label="人工指定"
         width="100"
       >
         <template #default="scope">
-          <el-tag :type="scope.row.manualAssign ? 'success' : 'danger'">
-            {{ scope.row.manualAssign ? '是' : '否' }}
+          <el-tag :type="scope.row.manualAbsent ? 'success' : 'danger'">
+            {{ scope.row.manualAbsent ? '是' : '否' }}
           </el-tag>
         </template>
       </el-table-column>
       <el-table-column
-        v-if="checkFieldVisible('batchNo')"
-        prop="batchNo"
+        v-if="checkFieldVisible('batchCode')"
+        prop="batchCode"
         label="批次编号"
         width="100"
       />
       <el-table-column
-        v-if="checkFieldVisible('signBookNo')"
-        prop="signBookNo"
+        v-if="checkFieldVisible('packageCode')"
+        prop="packageCode"
         label="签到表编号"
         width="120"
       />
@@ -320,11 +317,19 @@
         label="考场"
         width="80"
       />
-      <el-table-column label="操作" width="100" fixed="right">
+      <el-table-column label="操作" width="120" fixed="right">
         <template #default="scope">
-          <el-button size="small" link @click="onEdit(scope.row)">
+          <el-button
+            size="small"
+            type="primary"
+            link
+            @click="onEdit(scope.row)"
+          >
             编辑
           </el-button>
+          <el-button size="small" type="danger" link @click="onDelete(row)"
+            >删除</el-button
+          >
         </template>
       </el-table-column>
     </el-table>
@@ -347,17 +352,17 @@
 
   <!-- 导入学生 -->
   <ImportDialog
-    ref="importDialogRef"
+    ref="importStudentDialogRef"
     title="导入学生"
-    upload-url="/api/admin/site/import"
+    :upload-url="importStudentUrl"
     :format="['xls', 'xlsx']"
-    :download-handle="downloadTemplate"
+    :download-handle="downloadStudentTemplate"
     download-filename="学生导入模板.xlsx"
   />
 
   <!-- 导入名单 -->
   <ImportDialog
-    ref="importUserDialogRef"
+    ref="importAbDialogRef"
     :title="importConfig?.title"
     :upload-url="importConfig?.url"
     :format="importConfig?.format"
@@ -377,9 +382,14 @@
 <script setup lang="ts">
   import { reactive, ref, computed } from 'vue';
   import { ArrowDown } from '@element-plus/icons-vue';
-  import { getStudentList } from '@/api/student';
+  import { ElMessage } from 'element-plus';
+  import { getStudentList, deleteStudent } from '@/api/student';
   import { StudentItem, StudentListFilter } from '@/api/types/student';
   import useTable from '@/hooks/table';
+  import useLoading from '@/hooks/loading';
+  import { modalConfirm } from '@/utils/ui';
+  import { downloadExport } from '@/utils/download-export';
+  import { useAppStore } from '@/store';
 
   import ModifyStudent from './ModifyStudent.vue';
 
@@ -387,23 +397,28 @@
     name: 'StudentManage',
   });
 
+  const appStore = useAppStore();
+
   const searchModel = reactive<StudentListFilter>({
     name: '',
-    examNo: '',
-    secretNo: '',
-    studentNo: '',
-    subject: null,
-    level: '',
-    majorType: '',
-    status: '',
-    batchNo: '',
-    signBookNo: '',
+    examNumber: '',
+    secretNumber: '',
+    studentCode: '',
+    subjectCode: '',
+    subjectLevel: '',
+    subjectCategory: '',
+    upload: undefined,
+    absent: undefined,
+    manualAbsent: undefined,
+    breach: undefined,
+    batchCode: '',
+    packageCode: '',
     college: '',
     className: '',
     teacher: '',
     examSite: '',
     examRoom: '',
-    scanPages: undefined,
+    sheetCount: undefined,
   });
 
   const { dataList, pagination, loading, getList, toPage, pageSizeChange } =
@@ -411,19 +426,19 @@
 
   // 表头配置
   const tableFields = [
-    { name: '准考证号', field: 'examNo' },
-    { name: '密号', field: 'secretNo' },
+    { name: '准考证号', field: 'examNumber' },
+    { name: '密号', field: 'secretNumber' },
     { name: '姓名', field: 'name' },
-    { name: '学号', field: 'studentNo' },
-    { name: '科目', field: 'subject' },
-    { name: '试卷类型', field: 'examType' },
-    { name: '层次', field: 'level' },
-    { name: '专业类型', field: 'majorType' },
-    { name: '扫描识别', field: 'scanRecognition' },
-    { name: '扫描张数', field: 'scanPages' },
-    { name: '人工指定', field: 'manualAssign' },
-    { name: '批次编号', field: 'batchNo' },
-    { name: '签到表编号', field: 'signBookNo' },
+    { name: '学号', field: 'studentCode' },
+    { name: '科目', field: 'subjectName' },
+    { name: '试卷类型', field: 'paperType' },
+    { name: '层次', field: 'subjectLevel' },
+    { name: '专业类型', field: 'subjectCategory' },
+    { name: '扫描识别', field: 'upload' },
+    { name: '扫描张数', field: 'sheetCount' },
+    { name: '人工指定', field: 'manualAbsent' },
+    { name: '批次编号', field: 'batchCode' },
+    { name: '签到表编号', field: 'packageCode' },
     { name: '学院', field: 'college' },
     { name: '班级', field: 'className' },
     { name: '任课老师', field: 'teacher' },
@@ -449,6 +464,28 @@
     modifyStudentRef.value?.open();
   }
 
+  // 删除
+  const { loading: deleteLoading, setLoading: setDeleteLoading } = useLoading();
+  async function onDelete(row: StudentItem) {
+    if (deleteLoading.value) return;
+    const confirm = await modalConfirm(
+      `确定要删除分类"${row.name}"吗?`,
+      '删除确认'
+    ).catch(() => false);
+    if (!confirm) return;
+
+    try {
+      setDeleteLoading(true);
+      await deleteStudent(row.id);
+      ElMessage.success('删除成功');
+      getList();
+    } catch (error) {
+      console.log('删除失败:', error);
+    } finally {
+      setDeleteLoading(false);
+    }
+  }
+
   // 查看图片
   const showPreview = ref(false);
   const curStudentSheetUrls = ref<string[]>([]);
@@ -458,48 +495,63 @@
   }
 
   // 导入学生
-  const importDialogRef = ref();
-  function downloadTemplate() {
-    // TODO: 实现下载模板功能
+  const importStudentDialogRef = ref();
+  const importStudentUrl = computed(() => {
+    return appStore.curExam?.type === 'SCAN_IMAGE'
+      ? '/api/admin/student/import'
+      : '/api/admin/student/uploadImport';
+  });
+  function downloadStudentTemplate() {
+    downloadExport(
+      appStore.curExam?.type === 'SCAN_IMAGE'
+        ? 'studentTemplate'
+        : 'mediaStudentTemplate'
+    );
   }
   function onImport() {
-    importDialogRef.value?.open();
+    importStudentDialogRef.value?.open();
   }
 
   // 导入名单
   const impoartConfigList = ref([
     {
-      code: 'breaking',
-      title: '缺考名单',
-      url: '/api/admin/subject/import/kgt',
+      code: 'absent',
+      title: '导入缺考名单',
+      url: '/api/admin/student/absentImport',
       format: ['xls', 'xlsx'],
       donloadFilename: '缺考名单导入模板.xlsx',
     },
     {
-      code: 'miss',
-      title: '违纪名单',
-      url: '/api/admin/subject/import/kgt',
+      code: 'breach',
+      title: '导入违纪名单',
+      url: '/api/admin/student/breachImport',
       format: ['xls', 'xlsx'],
       donloadFilename: '违纪名单导入模板.xlsx',
     },
   ]);
-  const importUserDialogRef = ref();
+  const importAbDialogRef = ref();
   const importData = reactive({
-    importType: 'breaking',
+    importType: 'absent',
   });
   const importConfig = computed(() => {
     return impoartConfigList.value.find(
       (item) => item.code === importData.importType
     );
   });
+  function downloadTemplate() {
+    downloadExport(
+      importData.importType === 'absent'
+        ? 'absentStudentTemplate'
+        : 'breachStudentTemplate'
+    );
+  }
   const onImportCommand = (command: string) => {
-    console.log('导入命令:', command);
     importData.importType = command;
-    importUserDialogRef.value?.open();
+    importAbDialogRef.value?.open();
   };
 
   // 导出
   function exportData() {
-    // TODO: 实现导出功能
+    downloadExport('exportStudent', searchModel);
   }
 </script>

+ 0 - 1
src/views/user/UserManage.vue

@@ -35,7 +35,6 @@
           v-model="searchModel.role"
           placeholder="请选择角色"
           clearable
-          style="width: 200px"
         >
           <el-option
             v-for="(val, key) in ROLE_TYPE"