Browse Source

feat: 考生管理

zhangjie 1 week ago
parent
commit
e1f86fd2f6

+ 1 - 1
auto-imports.d.ts

@@ -6,5 +6,5 @@
 // biome-ignore lint: disable
 export {}
 declare global {
-
+  const ElMessage: typeof import('element-plus/es')['ElMessage'];
 }

+ 19 - 0
src/api/student.ts

@@ -0,0 +1,19 @@
+import axios from 'axios';
+import {
+  StudentItem,
+  StudentListPageParam,
+  StudentListPageRes,
+  StudentUpdateParam,
+} from './types/student';
+
+// 考生管理
+// 获取考生列表 (分页)
+export function getStudentList(
+  params: StudentListPageParam
+): Promise<StudentListPageRes> {
+  return axios.post('/api/student/list', { params });
+}
+// 新增或编辑考生
+export function updateStudent(data: StudentUpdateParam): Promise<StudentItem> {
+  return axios.post('/api/student/update', data);
+}

+ 99 - 0
src/api/types/student.ts

@@ -0,0 +1,99 @@
+import { PageResult, PageParams } from './common';
+
+export interface StudentListFilter {
+  // 姓名
+  name: string;
+  // 准考证号
+  examNo: string;
+  // 密号
+  secretNo: string;
+  // 学号
+  studentNo: string;
+  // 科目
+  subject: string;
+  // 层次
+  level: string;
+  // 专业类型
+  majorType: string;
+  // 状态
+  status: string;
+  // 批次编号
+  batchNo: string;
+  // 签到表编号
+  signBookNo: string;
+  // 学院
+  college: string;
+  // 班级
+  className: string;
+  // 任课老师
+  teacher: string;
+  // 考点
+  examSite: string;
+  // 考场
+  examRoom: string;
+  // 扫描张数
+  scanPages?: number;
+}
+export type StudentListPageParam = PageParams<StudentListFilter>;
+
+export interface StudentItem {
+  id: number;
+  // 准考证号
+  examNo: string;
+  // 密号
+  secretNo: string;
+  // 姓名
+  name: string;
+  // 学号
+  studentNo: string;
+  // 科目
+  subject: string;
+  // 试卷类型
+  examType: string;
+  // 层次
+  level: string;
+  // 专业类型
+  majorType: string;
+  // 扫描识别
+  scanRecognition: boolean;
+  // 扫描张数
+  scanPages: number;
+  // 人工指定
+  manualAssign: boolean;
+  // 批次编号
+  batchNo: string;
+  // 签到表编号
+  signBookNo: string;
+  // 学院
+  college: string;
+  // 班级
+  className: string;
+  // 任课老师
+  teacher: string;
+  // 考点
+  examSite: string;
+  // 考场
+  examRoom: string;
+}
+
+export type StudentListPageRes = PageResult<StudentItem>;
+
+export interface StudentUpdateParam {
+  id?: number;
+  // 姓名
+  name: string;
+  // 学号
+  studentNo: string;
+  // 准考证号
+  examno: string;
+  // 科目
+  subject: string;
+  // 学院
+  college: string;
+  // 班级
+  className: string;
+  // 任课老师
+  teacher: string;
+  // 签到表编号
+  signBookNo: string;
+}

+ 2 - 0
src/api/types/user.ts

@@ -14,6 +14,7 @@ export interface UserItem {
   wechatLinked?: boolean; // 关联微信 (可选, 可能为显示字段)
   // 根据实际需要添加更多字段
 }
+export type UserListPageRes = PageResult<UserItem>;
 
 export interface UserListFilter {
   name?: string; // 按名称搜索
@@ -22,6 +23,7 @@ export interface UserListFilter {
   role?: string; // 按角色筛选
   // 根据实际需要添加更多筛选字段
 }
+export type UserListPageParam = PageParams<UserListFilter>;
 
 export interface UserUpdateParam {
   id?: number;

+ 4 - 5
src/api/user.ts

@@ -3,14 +3,13 @@ import { UserState } from '@/store/modules/user/types';
 import type {
   LoginData,
   UpdatePwdData,
-  UserListFilter,
-  UserItem,
+  UserListPageParam,
+  UserListPageRes,
   UserUpdateParam,
   ResetPasswordParam,
   EnableUserParam,
   BatchCreateUserParam,
 } from './types/user';
-import type { ListResult } from './types/base';
 
 // 登录
 export function login(data: LoginData): Promise<UserState> {
@@ -28,8 +27,8 @@ export function userLogout() {
 // 用户管理
 // 获取用户列表 (分页)
 export function userListPage(
-  params: UserListFilter & { pageNumber: number; pageSize: number }
-): Promise<ListResult<UserItem>> {
+  params: UserListPageParam
+): Promise<UserListPageRes> {
   return axios.get('/api/user/list', { params });
 }
 // 新增或编辑用户

+ 9 - 0
src/router/routes/modules/base.ts

@@ -18,6 +18,15 @@ const BASE: AppRouteRecordRaw = {
         requiresAuth: true,
       },
     },
+    {
+      path: '/student-manage',
+      name: 'StudentManage',
+      component: () => import('@/views/student/StudentManage.vue'),
+      meta: {
+        title: '学生管理',
+        requiresAuth: true,
+      },
+    },
   ],
 };
 

+ 162 - 0
src/views/student/ModifyStudent.vue

@@ -0,0 +1,162 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    width="500px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    top="10vh"
+    append-to-body
+    @close="handleClose"
+    @open="modalBeforeOpen"
+  >
+    <el-form
+      ref="formRef"
+      :model="formModel"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-form-item label="科目" prop="subject">
+        <el-select
+          v-model="formModel.subject"
+          placeholder="请选择科目"
+          style="width: 100%"
+        >
+          <el-option label="201904516-工程图学2" value="201904516-工程图学2" />
+          <el-option label="201904517-高等数学" value="201904517-高等数学" />
+          <el-option label="201904518-大学英语" value="201904518-大学英语" />
+        </el-select>
+      </el-form-item>
+      <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>
+      <el-form-item label="准考证号" prop="examno">
+        <el-input
+          v-model="formModel.examno"
+          placeholder="请输入准考证号"
+          :disabled="isEdit"
+        />
+      </el-form-item>
+      <el-form-item label="学院" prop="college">
+        <el-input v-model="formModel.college" placeholder="请输入学院" />
+      </el-form-item>
+      <el-form-item label="班级" prop="className">
+        <el-input v-model="formModel.className" placeholder="请输入班级" />
+      </el-form-item>
+      <el-form-item label="任课老师" prop="teacher">
+        <el-input v-model="formModel.teacher" placeholder="请输入任课老师" />
+      </el-form-item>
+      <el-form-item label="签到表编号">
+        <el-input
+          v-model="formModel.signBookNo"
+          placeholder="请输入签到表编号"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="close">返回</el-button>
+        <el-button type="primary" :loading="loading" @click="confirm"
+          >保存</el-button
+        >
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, reactive, computed } from 'vue';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import { ElMessage } from 'element-plus';
+  import type { StudentItem, StudentUpdateParam } from '@/api/types/student';
+  import useModal from '@/hooks/modal';
+  import useLoading from '@/hooks/loading';
+  import { objAssign, objModifyAssign } from '@/utils/utils';
+  import { updateStudent } from '@/api/student';
+
+  defineOptions({
+    name: 'ModifyStudent',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  interface Props {
+    rowData: StudentItem;
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    rowData: {} as StudentItem,
+  });
+  const emit = defineEmits(['modified']);
+
+  const isEdit = computed(() => !!props.rowData?.id);
+  const title = computed(() => `${isEdit.value ? '编辑' : '新增'}学生`);
+
+  const formRef = ref<FormInstance>();
+  const initialFormState: Partial<StudentUpdateParam> = {
+    name: '',
+    studentNo: '',
+    examno: '',
+    college: '',
+    className: '',
+    teacher: '',
+    signBookNo: '',
+    subject: '',
+  };
+
+  const formModel = reactive({ ...initialFormState });
+
+  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' }],
+    college: [{ required: true, message: '请输入学院', trigger: 'change' }],
+    className: [{ required: true, message: '请输入班级', trigger: 'change' }],
+    teacher: [{ required: true, message: '请输入任课老师', trigger: 'change' }],
+    signBookNo: [
+      { required: true, message: '请输入签到表编号', trigger: 'change' },
+    ],
+  };
+
+  const handleClose = () => {
+    formRef.value?.resetFields();
+  };
+
+  /* confirm */
+  const { loading, setLoading } = useLoading();
+  async function confirm() {
+    const valid = await formRef.value?.validate().catch(() => false);
+    if (!valid) return;
+
+    setLoading(true);
+    const datas = objAssign(formModel, {});
+    if (isEdit.value) {
+      datas.id = props.rowData.id;
+    }
+    let res = true;
+    await updateStudent(datas).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+    ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功!`);
+    emit('modified', datas);
+    close();
+  }
+
+  /* init modal */
+  function modalBeforeOpen() {
+    if (props.rowData.id) {
+      // 编辑模式,映射字段
+      objModifyAssign(formModel, props.rowData);
+    } else {
+      objModifyAssign(formModel, initialFormState);
+    }
+  }
+</script>

+ 306 - 0
src/views/student/StudentManage.vue

@@ -0,0 +1,306 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="姓名">
+        <el-input
+          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"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="密号">
+        <el-input
+          v-model.trim="searchModel.secretNo"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="学号">
+        <el-input
+          v-model.trim="searchModel.studentNo"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="科目">
+        <el-select
+          v-model="searchModel.subject"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="请选择" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="层次">
+        <el-select
+          v-model="searchModel.level"
+          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"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="请选择" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态">
+        <el-space>
+          <el-select
+            v-model="searchModel.status"
+            placeholder="请选择"
+            clearable
+            style="width: 120px"
+          >
+            <el-option label="不限" value="" />
+          </el-select>
+          <el-select placeholder="不限" clearable style="width: 120px">
+            <el-option label="不限" value="" />
+          </el-select>
+          <el-select placeholder="不限" clearable style="width: 120px">
+            <el-option label="不限" value="" />
+          </el-select>
+          <el-select placeholder="不限" clearable style="width: 120px">
+            <el-option label="不限" value="" />
+          </el-select>
+        </el-space>
+      </el-form-item>
+      <el-form-item label="签到表编号">
+        <el-input
+          v-model.trim="searchModel.signBookNo"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="批次编号">
+        <el-input
+          v-model.trim="searchModel.batchNo"
+          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>
+      </el-form-item>
+      <el-form-item label="班级">
+        <el-select
+          v-model="searchModel.className"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+          <el-option label="请选择" value="" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="任课老师">
+        <el-input
+          v-model.trim="searchModel.teacher"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="考点">
+        <el-input
+          v-model.trim="searchModel.examSite"
+          placeholder="请选择"
+          clearable
+          style="width: 120px"
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item label="考场">
+        <el-input
+          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"
+          :step="1"
+          :precision="0"
+          :controls="false"
+          step-strictly
+          style="width: 100px"
+        >
+        </el-input-number>
+      </el-form-item>
+    </el-form>
+    <el-space wrap>
+      <el-button type="primary" @click="toPage(1)">查询</el-button>
+      <el-button @click="onAdd">添加</el-button>
+      <el-button type="success" @click="onImport">导入</el-button>
+      <el-button @click="exportData">导出</el-button>
+    </el-space>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList" :loading="loading">
+      <el-table-column type="index" label="序号" width="60" />
+      <el-table-column property="examNo" label="准考证号" width="120" />
+      <el-table-column property="name" label="姓名" min-width="100" />
+      <el-table-column property="studentNo" label="学号" width="120" />
+      <el-table-column property="subject" label="科目" min-width="100" />
+      <el-table-column property="examType" label="试卷类型" width="100" />
+      <el-table-column property="level" label="层次" width="80" />
+      <el-table-column property="majorType" label="专业类型" width="100" />
+      <el-table-column property="college" label="学院" width="120" />
+      <el-table-column property="className" label="班级" width="100" />
+      <el-table-column property="teacher" label="任课老师" width="100" />
+      <el-table-column property="examSite" label="考点" width="100" />
+      <el-table-column property="examRoom" label="考场" width="80" />
+      <el-table-column property="signBookNo" label="签到表编号" width="120" />
+      <el-table-column property="batchNo" label="批次编号" width="100" />
+      <el-table-column label="扫描识别" width="80">
+        <template #default="scope">
+          <el-tag :type="scope.row.scanRecognition ? 'success' : 'danger'">
+            {{ scope.row.scanRecognition ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column property="scanPages" label="扫描张数" width="80" />
+      <el-table-column label="人工指定" width="80">
+        <template #default="scope">
+          <el-tag :type="scope.row.manualAssign ? 'success' : 'danger'">
+            {{ scope.row.manualAssign ? '是' : '否' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" fixed="right">
+        <template #default="scope">
+          <el-button size="small" link @click="onEdit(scope.row)">
+            编辑
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      v-model:current-page="pagination.pageNumber"
+      v-model:page-size="pagination.pageSize"
+      :layout="pagination.layout"
+      :total="pagination.total"
+      @size-change="pageSizeChange"
+      @current-change="toPage"
+    />
+  </div>
+
+  <!-- 新增/编辑学生弹窗 -->
+  <ModifyStudent
+    ref="modifyStudentRef"
+    :row-data="curRow"
+    @modified="getList"
+  />
+
+  <!-- 导入学生 -->
+  <ImportDialog
+    ref="importDialogRef"
+    title="导入学生"
+    upload-url="/api/admin/site/import"
+    :format="['xls', 'xlsx']"
+    :download-handle="downloadTemplate"
+    download-filename="学生导入模板.xlsx"
+  />
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref } from 'vue';
+  import { getStudentList } from '@/api/student';
+  import { StudentItem, StudentListFilter } from '@/api/types/student';
+  import useTable from '@/hooks/table';
+
+  import ModifyStudent from './ModifyStudent.vue';
+
+  defineOptions({
+    name: 'StudentManage',
+  });
+
+  const searchModel = reactive<StudentListFilter>({
+    name: '',
+    examNo: '',
+    secretNo: '',
+    studentNo: '',
+    subject: '',
+    level: '',
+    majorType: '',
+    status: '',
+    batchNo: '',
+    signBookNo: '',
+    college: '',
+    className: '',
+    teacher: '',
+    examSite: '',
+    examRoom: '',
+    scanPages: undefined,
+  });
+
+  const { dataList, pagination, loading, getList, toPage, pageSizeChange } =
+    useTable<StudentItem>(getStudentList, searchModel, false);
+
+  // table action
+  const curRow = ref({} as StudentItem);
+  const modifyStudentRef = ref();
+
+  function onEdit(row: StudentItem) {
+    curRow.value = row;
+    modifyStudentRef.value?.open();
+  }
+
+  function onAdd() {
+    curRow.value = {} as StudentItem;
+    modifyStudentRef.value?.open();
+  }
+
+  // 导入学生
+  const importDialogRef = ref();
+  function downloadTemplate() {
+    // TODO: 实现下载模板功能
+  }
+  function onImport() {
+    importDialogRef.value?.open();
+  }
+
+  function exportData() {
+    // TODO: 实现导出功能
+  }
+</script>