Browse Source

feat: 用户创建以及tag调整

zhangjie 1 tuần trước cách đây
mục cha
commit
2bada799aa

+ 1 - 0
components.d.ts

@@ -68,6 +68,7 @@ declare module '@vue/runtime-core' {
     SelectRangeDatetime: typeof import('./src/components/select-range-datetime/index.vue')['default']
     SelectRangeTime: typeof import('./src/components/select-range-time/index.vue')['default']
     SelectSubject: typeof import('./src/components/select-subject/index.vue')['default']
+    StatusTag: typeof import('./src/components/status-tag/index.vue')['default']
     SvgIcon: typeof import('./src/components/svg-icon/index.vue')['default']
     TableField: typeof import('./src/components/table-field/index.vue')['default']
     UploadButton: typeof import('./src/components/upload-button/index.vue')['default']

+ 17 - 5
src/api/check.ts

@@ -1,4 +1,4 @@
-import axios from 'axios';
+import axios, { AxiosResponse } from 'axios';
 import {
   ImageCheckDataListFilter,
   ImageCheckPageParams,
@@ -6,31 +6,43 @@ import {
   ManualConfirmDataListFilter,
   ManualConfirmPageRes,
   ResultCheckDataListFilter,
+  CheckRes,
 } from './types/check';
 
 // 人工确认查询
 export function getManualConfirmDataList(
   params: ManualConfirmDataListFilter
 ): Promise<ManualConfirmPageRes> {
-  return axios.post('/api/manualConfirmDataList', {}, { params });
+  return axios.post('/api/admin/exam/check/student/list', {}, { params });
 }
 
 // 图片检查查询
 export function getImageCheckDataList(
   params: ImageCheckPageParams
 ): Promise<ImageCheckPageRes> {
-  return axios.post('/api/imageCheckDataList', {}, { params });
+  return axios.post('/api/admin/exam/check/image/list', {}, { params });
 }
 
 // 导出图片检查数据
-export function exportImageCheckData(params: ImageCheckDataListFilter) {
+export function exportImageCheckData(
+  params: ImageCheckDataListFilter
+): Promise<AxiosResponse<Blob>> {
   return axios.post(
-    '/api/admin/exam/check/export',
+    '/api/admin/exam/check/image/export',
     {},
     { responseType: 'blob', params }
   );
 }
 
+// 开始图片检查
+export function startImageCheck(): Promise<CheckRes> {
+  return axios.post('/api/admin/exam/check/image/run', {});
+}
+// 图片检查状态
+export function getImageCheckStatus(): Promise<boolean> {
+  return axios.post('/api/admin/exam/check/image/status', {});
+}
+
 // 识别结果查询
 export function getResultCheckDataList(
   params: ResultCheckDataListFilter

+ 7 - 2
src/api/types/check.ts

@@ -64,9 +64,9 @@ export type ImageCheckPageRes = PageResult<ImageCheckDataListItem>;
 // 人工确认 数据列表筛选条件
 export interface ManualConfirmDataListFilter {
   // 确认类型
-  type: number | null;
+  type: string;
   // 科目
-  subjectCode: string | null;
+  subjectCode: string;
   // 考点
   examSite: string;
 }
@@ -93,3 +93,8 @@ export interface ManualConfirmDataListItem {
   uploadTime: string;
 }
 export type ManualConfirmPageRes = PageResult<ManualConfirmDataListItem>;
+
+export interface CheckRes {
+  message: string;
+  success: boolean;
+}

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

@@ -71,6 +71,14 @@ export interface UserUpdateParam {
   role: RoleType;
   // 密码
   password: string;
+  // 绑定科目代码: 科组长/复核员/学校查询员
+  subjectCodeString?: string;
+  // 绑定考试ID: 学校查询员
+  examIdString?: string;
+  // 绑定考生文件: 学校查询员
+  studentFile?: File;
+  // 绑定学院: 学院管理员
+  colleges?: string;
 }
 
 export interface ResetPasswordParam {

+ 3 - 2
src/components/file-upload/index.vue

@@ -132,9 +132,10 @@
     const md5 = await fileMD5(file);
     headers.value.md5 = md5;
 
-    if (props.autoUpload) loading.value = true;
+    if (!props.autoUpload) return Promise.reject();
 
-    return true;
+    loading.value = true;
+    return Promise.resolve();
   }
 
   function customRequest(options: UploadRequestOptions) {

+ 3 - 2
src/components/import-dialog/index.vue

@@ -229,9 +229,10 @@
     const md5 = await fileMD5(file);
     headers.value.md5 = md5;
 
-    if (props.autoUpload) loading.value = true;
+    if (!props.autoUpload) return Promise.reject();
 
-    return true;
+    loading.value = true;
+    return Promise.resolve();
   }
 
   function customRequest(options: UploadRequestOptions) {

+ 2 - 0
src/components/index.ts

@@ -14,6 +14,7 @@ import TableField from './table-field/index.vue';
 import PageBreadcrumb from './page-breadcrumb/index.vue';
 import SelectOption from './select-option/index.vue';
 import SelectData from './select-data/index.vue';
+import StatusTag from './status-tag/index.vue';
 
 export default {
   install(Vue: App) {
@@ -24,6 +25,7 @@ export default {
     Vue.component('SelectRangeTime', SelectRangeTime);
     Vue.component('UploadButton', UploadButton);
     Vue.component('SelectExam', SelectExam);
+    Vue.component('StatusTag', StatusTag);
     Vue.component('SelectData', SelectData);
     Vue.component('SelectOption', SelectOption);
     Vue.component('SelectSubject', SelectSubject);

+ 25 - 24
src/components/select-data/search.ts

@@ -7,26 +7,30 @@ export interface OptionListItem {
   value: number;
   label: string;
 }
-type TransformType = (item: Record<string, any>) => OptionListItem;
 
-type ConfigItem = string | { url: string; transform?: TransformType };
+interface SelectConfigItem {
+  url: string;
+  transform?: (data: Array<Record<string, any>>) => OptionListItem[];
+}
 
-const defaultTransform = (item: {
-  id: number;
-  name: string;
-}): OptionListItem => ({
-  value: item.id,
-  label: item.name,
-});
+type SelectConfigValue = string | SelectConfigItem;
 
-export type SelectType = 'problemType' | 'rejectType';
+const defaultTransform = (
+  data: Array<{ id: number; name: string }>
+): OptionListItem[] => {
+  return data.map((item) => ({
+    value: item.id,
+    label: item.name,
+  }));
+};
 
-const selectConfig: Record<SelectType, ConfigItem> = {
+const selectConfig: Record<string, SelectConfigValue> = {
   // 问题卷分类
   problemType: '/api/admin/exam/problem/type/list',
   // 打回卷分类
   rejectType: '/api/admin/exam/reject/type/find',
 };
+export type SelectType = keyof typeof selectConfig;
 
 export default function useSearch(type: SelectType) {
   const { loading, setLoading } = useLoading();
@@ -34,27 +38,24 @@ export default function useSearch(type: SelectType) {
 
   async function search(params?: Record<string, any>) {
     if (loading.value) return;
-    const config = selectConfig[type];
-    if (!config) {
+    const configValue = selectConfig[type];
+    if (!configValue) {
       console.error('下拉列表类型错误!');
       return;
     }
 
+    // 统一处理配置格式
+    const config: SelectConfigItem =
+      typeof configValue === 'string' ? { url: configValue } : configValue;
+
     try {
       setLoading(true);
-      let url = '';
-      let transform = defaultTransform;
-      if (typeof config === 'string') {
-        url = config;
+      const res = await axios.post(config.url, {}, { params });
+      if (config.transform) {
+        optionList.value = config.transform(res as Array<Record<string, any>>);
       } else {
-        url = config.url;
-        transform = config.transform || defaultTransform;
+        optionList.value = defaultTransform(res as Array<Record<string, any>>);
       }
-      const res = (await axios.post(url, {}, { params })) as Array<
-        Record<string, any>
-      >;
-
-      optionList.value = res.map(transform);
     } catch (error) {
       console.error(error);
     } finally {

+ 43 - 15
src/components/select-option/search.ts

@@ -3,7 +3,19 @@ import { ref } from 'vue';
 
 import useLoading from '@/hooks/loading';
 
-const selectConfig = {
+export interface OptionListItem {
+  value: string;
+  label: string;
+}
+
+interface SelectConfigItem {
+  url: string;
+  transform?: (data: Array<Record<string, string>>) => OptionListItem[];
+}
+
+type SelectConfigValue = string | SelectConfigItem;
+
+const selectConfig: Record<string, SelectConfigValue> = {
   // 卷型: subjectCode
   subjectPaperType: '/api/admin/subject/getPaperType',
   // 科目层次
@@ -18,14 +30,26 @@ const selectConfig = {
   subjectCategory: '/api/admin/student/category/list',
   // 仲裁类型
   arbitrationType: '/api/admin/exam/arbitrate/status',
+  // 人工确认类型
+  manualConfirmType: {
+    url: '/api/admin/exam/check/student/types',
+    transform: (data: { value: string; name: string }[]) =>
+      data.map((item) => ({
+        value: item.value,
+        label: item.name,
+      })),
+  },
 };
 
 export type SelectType = keyof typeof selectConfig;
 
-export interface OptionListItem {
-  value: string;
-  label: string;
-}
+// 默认的转换函数,用于处理 string[] 格式的数据
+const defaultTransform = (data: string[]): OptionListItem[] => {
+  return data.map((item) => ({
+    value: item,
+    label: item,
+  }));
+};
 
 export default function useSearch(type: SelectType) {
   const { loading, setLoading } = useLoading();
@@ -33,23 +57,27 @@ export default function useSearch(type: SelectType) {
 
   async function search(params?: Record<string, any>) {
     if (loading.value) return;
-    if (!selectConfig[type]) {
+    const configValue = selectConfig[type];
+    if (!configValue) {
       console.error('下拉列表类型错误!');
       return;
     }
 
+    // 统一处理配置格式
+    const config: SelectConfigItem =
+      typeof configValue === 'string' ? { url: configValue } : configValue;
+
     try {
       setLoading(true);
-      const res = (await axios.post(
-        selectConfig[type],
-        {},
-        { params }
-      )) as string[];
+      const res = await axios.post(config.url, {}, { params });
 
-      optionList.value = res.map((item) => ({
-        value: item,
-        label: item,
-      }));
+      if (config.transform) {
+        optionList.value = config.transform(
+          res as Array<Record<string, string>>
+        );
+      } else {
+        optionList.value = defaultTransform(res as string[]);
+      }
     } catch (error) {
       console.error(error);
     } finally {

+ 35 - 0
src/components/status-tag/index.vue

@@ -0,0 +1,35 @@
+<template>
+  <el-tag :color="theme">{{ label || '-' }}</el-tag>
+</template>
+
+<script setup lang="ts">
+  import { computed } from 'vue';
+
+  defineOptions({
+    name: 'StatusTag',
+  });
+
+  const configs = {
+    enable: {
+      true: 'primary',
+      false: 'danger',
+    },
+    upload: {
+      true: 'primary',
+      false: 'danger',
+    },
+  };
+  type ConfigKeyType = keyof typeof configs;
+
+  const props = defineProps<{
+    value: string | number | boolean;
+    label: string;
+    type: ConfigKeyType;
+  }>();
+
+  const theme = computed(() => {
+    const config = configs[props.type];
+    if (!config) return 'primary';
+    return config[String(props.value)] || 'primary';
+  });
+</script>

+ 70 - 73
src/components/upload-button/index.vue

@@ -1,31 +1,28 @@
 <template>
-  <div class="file-upload">
-    <el-upload
-      ref="uploadRef"
-      :action="uploadUrl"
-      :headers="headers"
-      :data="uploadDataDict"
-      :show-file-list="false"
-      :auto-upload="autoUpload"
-      :http-request="customRequest"
-      :disabled="disabled"
-      :before-upload="handleBeforeUpload"
-      :accept="accept"
-      :multiple="multiple"
-      :on-exceed="handleExceededSize"
-      @change="handleFileChange"
-      @error="handleError"
-      @success="handleSuccess"
-    >
-      <template #trigger>
-        <el-button type="primary" :disabled="loading">{{ btnText }}</el-button>
-      </template>
-    </el-upload>
-  </div>
+  <el-upload
+    ref="uploadRef"
+    :action="uploadUrl"
+    :headers="headers"
+    :data="uploadDataDict"
+    :show-file-list="false"
+    :http-request="customRequest"
+    :disabled="disabled"
+    :before-upload="handleBeforeUpload"
+    :accept="accept"
+    :multiple="multiple"
+    :on-exceed="handleExceededSize"
+    @change="handleFileChange"
+    @error="handleError"
+    @success="handleSuccess"
+  >
+    <template #trigger>
+      <el-button type="primary" :disabled="loading">{{ btnText }}</el-button>
+    </template>
+  </el-upload>
 </template>
 
 <script setup lang="ts">
-  import { ref } from 'vue';
+  import { ref, computed } from 'vue';
   import { fileMD5 } from '@/utils/md5';
   import { ElMessage } from 'element-plus';
   import type {
@@ -50,7 +47,7 @@
       autoUpload?: boolean;
       disabled?: boolean;
       btnText?: string;
-      accept?: string;
+      format?: string[];
       multiple?: boolean;
     }>(),
     {
@@ -58,12 +55,12 @@
       uploadData: () => {
         return {};
       },
+      format: () => ['xls', 'xlsx'],
       maxSize: 20 * 1024 * 1024,
       uploadFileAlias: 'file',
       autoUpload: true,
       disabled: false,
       btnText: '选择',
-      accept: '.xls,.xlsx',
       multiple: false,
     }
   );
@@ -73,6 +70,7 @@
     'uploadError',
     'uploadSuccess',
     'validError',
+    'fileReady',
   ]);
 
   const uploadRef = ref();
@@ -82,6 +80,15 @@
   const result = ref({ success: true, message: '' });
   const loading = ref(false);
 
+  const accept = computed(() => {
+    return props.format.map((el) => `.${el}`).join();
+  });
+
+  function checkFileFormat(fileType: string) {
+    const fileFormat = fileType.split('.').pop()?.toLocaleLowerCase();
+    return props.format.some((item) => item.toLocaleLowerCase() === fileFormat);
+  }
+
   function handleFileChange(uploadFile: UploadFile, uploadFiles: UploadFiles) {
     if (props.autoUpload || !uploadFiles.length) return;
     // Element Plus's status: ready, uploading, success, fail
@@ -90,35 +97,38 @@
     canUpload.value = uploadFiles[0]?.status === 'ready';
   }
 
-  async function handleBeforeUpload(rawFile: UploadRawFile) {
+  async function handleBeforeUpload(file: UploadRawFile) {
     uploadDataDict.value = {
       ...props.uploadData,
-      filename: rawFile.name,
+      filename: file.name,
     };
 
-    if (rawFile.size > props.maxSize) {
-      // Element Plus uses on-exceed for this, but we can also check here
-      // For consistency with original logic, we call handleExceededSize and reject
-      // However, on-exceed will also be triggered if :limit is set for file count, not size directly.
-      // Here, we manually trigger the message and prevent upload.
-      const content = `文件大小不能超过${Math.floor(
-        props.maxSize / 1024 / 1024
-      )}MB`;
-      ElMessage.error(content);
-      result.value = {
-        success: false,
-        message: content,
-      };
-      emit('validError', result.value);
-      return Promise.reject(new Error(content));
+    if (file.size > props.maxSize) {
+      handleExceededSize();
+      return Promise.reject(result.value);
+    }
+
+    if (!checkFileFormat(file.name)) {
+      handleFormatError();
+      return Promise.reject(result.value);
     }
 
-    const md5 = await fileMD5(rawFile);
+    const md5 = await fileMD5(file);
     headers.value.md5 = md5;
 
-    if (props.autoUpload) loading.value = true;
+    if (!props.autoUpload) {
+      console.log('111');
 
-    return true;
+      emit('fileReady', {
+        md5,
+        filename: file.name,
+        file,
+      });
+      return Promise.reject();
+    }
+
+    loading.value = true;
+    return Promise.resolve();
   }
 
   function customRequest(options: UploadRequestOptions) {
@@ -168,13 +178,8 @@
     });
   }
 
-  function handleExceededSize(files: File[]) {
-    // This function is now primarily called by el-upload's on-exceed prop
-    // The before-upload also has a check, this is a fallback or for multiple files if `limit` (count) is exceeded.
-    const file = files[0]; // Assuming single file upload context for this message
-    const content = `文件 ${file.name} 大小超过限制 (${Math.floor(
-      props.maxSize / 1024 / 1024
-    )}MB)`;
+  function handleFormatError() {
+    const content = `只支持文件格式为${props.format.join('/')}`;
     result.value = {
       success: false,
       message: content,
@@ -183,24 +188,16 @@
     ElMessage.error(content);
     emit('validError', result.value);
   }
-</script>
-
-<style lang="scss">
-  .file-upload {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    width: 100%;
-    .arco-upload-hide {
-      display: inline-block;
-    }
-    .el-input__wrapper {
-      flex-grow: 2;
-      margin-right: 10px;
-    }
-    .arco-upload {
-      flex-grow: 0;
-      flex-shrink: 0;
-    }
+  function handleExceededSize() {
+    const content = `文件大小不能超过${Math.floor(
+      props.maxSize / (1024 * 1024)
+    )}M`;
+    result.value = {
+      success: false,
+      message: content,
+    };
+    loading.value = false;
+    ElMessage.error(content);
+    emit('validError', result.value);
   }
-</style>
+</script>

+ 18 - 5
src/views/check/ImageCheck.vue

@@ -67,14 +67,18 @@
 
 <script setup lang="ts">
   import { reactive } from 'vue';
-  import { getImageCheckDataList } from '@/api/check';
+  import {
+    getImageCheckDataList,
+    startImageCheck,
+    getImageCheckStatus,
+  } from '@/api/check';
+  import { ElMessage } from 'element-plus';
   import type {
     ImageCheckDataListItem,
     ImageCheckDataListFilter,
   } from '@/api/types/check';
   import useTable from '@/hooks/table';
   import { downloadExport } from '@/utils/download-export';
-  import ls from '@/utils/storage';
 
   defineOptions({
     name: 'ImageCheck',
@@ -94,8 +98,17 @@
     await downloadExport('exportImageCheckData', searchModel);
   }
 
-  function startCheck() {
-    ls.set('image-check', searchModel);
-    // TODO:开始处理
+  async function startCheck() {
+    const checkStatus = await getImageCheckStatus();
+    if (checkStatus) {
+      ElMessage.error('检查已启动');
+      return;
+    }
+    const res = await startImageCheck();
+    if (res.success) {
+      ElMessage.success('检查已启动');
+    } else {
+      ElMessage.error(res.message);
+    }
   }
 </script>

+ 4 - 10
src/views/check/ManualConfirm.vue

@@ -2,14 +2,8 @@
   <div class="part-box is-filter">
     <el-form inline>
       <el-form-item label="确认类型">
-        <el-select
-          v-model="searchModel.type"
-          placeholder="手动更新"
-          clearable
-          style="width: 120px"
-        >
-          <el-option label="手动更新" :value="1" />
-        </el-select>
+        <select-option v-model="searchModel.type" type="manualConfirmType">
+        </select-option>
       </el-form-item>
       <el-form-item label="科目">
         <select-subject v-model="searchModel.subjectCode"></select-subject>
@@ -74,8 +68,8 @@
   });
 
   const searchModel = reactive<ManualConfirmDataListFilter>({
-    type: null,
-    subjectCode: null,
+    type: '',
+    subjectCode: '',
     examSite: '',
   });
 

+ 94 - 13
src/views/user/ModifyUser.vue

@@ -2,7 +2,7 @@
   <el-dialog
     v-model="visible"
     :title="title"
-    width="500px"
+    width="600px"
     :close-on-click-modal="false"
     :close-on-press-escape="false"
     top="10vh"
@@ -53,6 +53,53 @@
           />
         </el-select>
       </el-form-item>
+      <el-form-item v-if="showSubjectCode" label="绑定科目代码">
+        <el-input
+          v-model="formModel.subjectCodeString"
+          placeholder="请输入"
+          type="textarea"
+          :rows="4"
+        />
+      </el-form-item>
+      <el-form-item v-if="showExamId" label="绑定考试ID">
+        <el-input
+          v-model="formModel.examIdString"
+          placeholder="请输入"
+          type="textarea"
+          :rows="4"
+        />
+      </el-form-item>
+      <el-form-item v-if="showColleges" label="绑定学院">
+        <el-input
+          v-model="formModel.colleges"
+          placeholder="请输入"
+          type="textarea"
+          :rows="4"
+        />
+      </el-form-item>
+      <el-form-item v-if="showStudentFile" label="绑定考生">
+        <el-space>
+          <upload-button
+            ref="uploadRef"
+            upload-url="/api/file/upload"
+            btn-text="选择文件"
+            :auto-upload="false"
+            @file-ready="handleFileReady"
+            @valid-error="onClearFile"
+          >
+          </upload-button>
+          <el-button @click="onClearFile">清除</el-button>
+          <el-button
+            link
+            type="primary"
+            @click="() => downloadExport('bindStudentTemplate')"
+            >下载模板</el-button
+          >
+        </el-space>
+        <p v-if="formModel.studentFile" style="width: 100%"
+          >已选文件:{{ formModel.studentFile.name }}</p
+        >
+      </el-form-item>
     </el-form>
     <template #footer>
       <span class="dialog-footer">
@@ -76,6 +123,7 @@
   import { ROLE_TYPE } from '@/constants/enumerate';
 
   import { objAssign, objModifyAssign } from '@/utils/utils';
+  import { downloadExport } from '@/utils/download-export';
 
   defineOptions({
     name: 'ModifyUser',
@@ -97,17 +145,38 @@
 
   const formRef = ref<FormInstance>();
 
-  const initialFormState: UserUpdateParam = {
-    id: 0,
-    loginName: '',
-    name: '',
-    employeeId: '',
-    password: '',
-    enable: true,
-    role: 'EVALUATOR', // 默认角色
-  };
+  function getInitialFormState(): UserUpdateParam {
+    return {
+      id: 0,
+      loginName: '',
+      name: '',
+      empno: '',
+      password: '',
+      enable: true,
+      role: 'MARKER', // 默认角色
+      subjectCodeString: '',
+      examIdString: '',
+      colleges: '',
+      studentFile: undefined,
+    };
+  }
 
-  const formModel = reactive<UserUpdateParam>({ ...initialFormState });
+  const formModel = reactive<UserUpdateParam>(getInitialFormState());
+
+  const showSubjectCode = computed(() => {
+    return ['SUBJECT_HEADER', 'INSPECTOR', 'SCHOOL_VIEWER'].includes(
+      formModel.role
+    );
+  });
+  const showExamId = computed(() => {
+    return ['SCHOOL_VIEWER'].includes(formModel.role);
+  });
+  const showStudentFile = computed(() => {
+    return ['SCHOOL_VIEWER'].includes(formModel.role);
+  });
+  const showColleges = computed(() => {
+    return ['COLLEGE_ADMIN'].includes(formModel.role);
+  });
 
   const validatePass = (rule: any, value: any, callback: any) => {
     if (!isEdit.value && value === '') {
@@ -136,9 +205,21 @@
     role: [{ required: true, message: '请选择角色', trigger: 'change' }],
   };
 
+  function handleFileReady(result: {
+    file: File;
+    md5: string;
+    filename: string;
+  }) {
+    formModel.studentFile = result.file;
+  }
+
+  function onClearFile() {
+    formModel.studentFile = undefined;
+  }
+
   const handleClose = () => {
     formRef.value?.resetFields();
-    Object.assign(formModel, initialFormState);
+    Object.assign(formModel, getInitialFormState());
   };
 
   /* confirm */
@@ -173,7 +254,7 @@
       objModifyAssign(formModel, props.rowData);
     } else {
       // 新增时,重置为初始状态(确保密码字段为空)
-      objModifyAssign(formModel, initialFormState);
+      objModifyAssign(formModel, getInitialFormState());
     }
   }
 </script>

+ 2 - 3
src/views/user/components/BatchCreateUserDialog.vue

@@ -92,10 +92,9 @@
   import useLoading from '@/hooks/loading';
   import useModal from '@/hooks/modal';
   import { BATCH_ADD_ROLE } from '@/constants/enumerate';
-
-  import type { SubjectItem } from '@/api/types/base';
-  import type { BatchCreateUserParam } from '@/api/types/user';
   import { batchAddUser } from '@/api/user';
+  import type { BatchCreateUserParam } from '@/api/types/user';
+  import type { SubjectItem } from '@/api/types/base';
 
   import SelectSubjectDialog from './SelectSubjectDialog.vue';
 

+ 2 - 1
src/views/user/components/SelectSubjectDialog.vue

@@ -74,10 +74,11 @@
   import type { TableInstance } from 'element-plus';
   import useLoading from '@/hooks/loading';
   import useModal from '@/hooks/modal';
-  import type { SubjectItem } from '@/api/types/base'; // Assuming SubjectItem type exists
   import { subjectQuery } from '@/api/base'; // Assuming API for subjects exists
   import { useAppStore } from '@/store';
 
+  import type { SubjectItem } from '@/api/types/base'; // Assuming SubjectItem type exists
+
   defineOptions({
     name: 'SelectSubjectDialog',
   });