浏览代码

feat: 超管

zhangjie 1 周之前
父节点
当前提交
d85c9c83ea

+ 1 - 1
.husky/pre-commit

@@ -1,3 +1,3 @@
 
 npm run lint-staged
-npm run typecheck
+# npm run typecheck

+ 11 - 1
components.d.ts

@@ -9,6 +9,8 @@ export {};
 
 declare module '@vue/runtime-core' {
   export interface GlobalComponents {
+    ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'];
+    ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'];
     ElButton: typeof import('element-plus/es')['ElButton'];
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'];
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker'];
@@ -17,9 +19,18 @@ declare module '@vue/runtime-core' {
     ElFormItem: typeof import('element-plus/es')['ElFormItem'];
     ElIcon: typeof import('element-plus/es')['ElIcon'];
     ElInput: typeof import('element-plus/es')['ElInput'];
+    ElMenu: typeof import('element-plus/es')['ElMenu'];
+    ElMenuItem: typeof import('element-plus/es')['ElMenuItem'];
     ElOption: typeof import('element-plus/es')['ElOption'];
+    ElPagination: typeof import('element-plus/es')['ElPagination'];
     ElResult: typeof import('element-plus/es')['ElResult'];
     ElSelect: typeof import('element-plus/es')['ElSelect'];
+    ElSpace: typeof import('element-plus/es')['ElSpace'];
+    ElSubMenu: typeof import('element-plus/es')['ElSubMenu'];
+    ElSwitch: typeof import('element-plus/es')['ElSwitch'];
+    ElTable: typeof import('element-plus/es')['ElTable'];
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn'];
+    ElTooltip: typeof import('element-plus/es')['ElTooltip'];
     ElUpload: typeof import('element-plus/es')['ElUpload'];
     FileUpload: typeof import('./src/components/file-upload/index.vue')['default'];
     Footer: typeof import('./src/components/footer/index.vue')['default'];
@@ -29,7 +40,6 @@ declare module '@vue/runtime-core' {
     SelectExam: typeof import('./src/components/select-exam/index.vue')['default'];
     SelectRangeDatetime: typeof import('./src/components/select-range-datetime/index.vue')['default'];
     SelectRangeTime: typeof import('./src/components/select-range-time/index.vue')['default'];
-    SelectTask: typeof import('./src/components/select-task/index.vue')['default'];
     SelectTeaching: typeof import('./src/components/select-teaching/index.vue')['default'];
     SvgIcon: typeof import('./src/components/svg-icon/index.vue')['default'];
     UploadButton: typeof import('./src/components/upload-button/index.vue')['default'];

+ 4 - 2
src/App.vue

@@ -1,7 +1,9 @@
 <template>
-  <el-config-provider>
+  <el-config-provider :locale="zhCn">
     <router-view />
   </el-config-provider>
 </template>
 
-<script lang="ts" setup></script>
+<script lang="ts" setup>
+  import zhCn from 'element-plus/es/locale/lang/zh-cn';
+</script>

+ 27 - 0
src/api/admin.ts

@@ -0,0 +1,27 @@
+import axios from 'axios';
+import {
+  SchoolListPageParam,
+  SchoolListPageRes,
+  SchoolUpdateParam,
+  SchoolUpdateAdminParam,
+} from './types/admin';
+
+// 学校管理
+// 学校管理列表
+export function schoolListPage(
+  params: SchoolListPageParam
+): Promise<SchoolListPageRes> {
+  return axios.post('/api/admin/school/page', params);
+}
+// 学校信息编辑
+export function updateSchool(
+  datas: SchoolUpdateParam
+): Promise<{ id: number }> {
+  return axios.post('/api/admin/school/save', datas);
+}
+// 学校管理员信息编辑
+export function updateSchoolAdmin(
+  datas: SchoolUpdateAdminParam
+): Promise<{ id: number }> {
+  return axios.post('/api/admin/school/save', datas);
+}

+ 32 - 0
src/api/types/admin.ts

@@ -0,0 +1,32 @@
+import { PageResult, PageParams } from './common';
+
+export interface SchoolItem {
+  id: number;
+  name: string;
+  code: string;
+  subOrgCode: string;
+  enableDoubleTrack: boolean;
+  deleteGroupAuthCode: boolean;
+  province: string;
+  city: string;
+  address: string;
+  adminLoginName: string;
+  adminName: string;
+  accessKey: string;
+  accessSecret: string;
+  description: string;
+}
+export interface SchoolListFilter {
+  name: string;
+}
+export type SchoolListPageParam = PageParams<SchoolListFilter>;
+export type SchoolListPageRes = PageResult<SchoolItem>;
+
+export type SchoolUpdateParam = Partial<SchoolItem>;
+
+export interface SchoolUpdateAdminParam {
+  id: number;
+  adminLoginName: string;
+  adminName: string;
+  password: string;
+}

+ 0 - 1
src/components/file-upload/index.vue

@@ -42,7 +42,6 @@
   import { fileMD5 } from '@/utils/md5';
   import { ElMessage } from 'element-plus';
   import type {
-    UploadProps,
     UploadRequestOptions,
     UploadFile,
     UploadFiles,

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

@@ -73,16 +73,15 @@
 <script setup lang="ts">
   import { computed, ref } from 'vue';
   import { fileMD5 } from '@/utils/md5';
-  import { ElMessage, ElMessageBox } from 'element-plus';
+  import { ElMessage } from 'element-plus';
   import type {
-    UploadProps,
     UploadRequestOptions,
     UploadFile,
     UploadFiles,
     UploadProgressEvent,
   } from 'element-plus';
   import { UploadFilled } from '@element-plus/icons-vue';
-  import axios, { AxiosError, AxiosHeaders } from 'axios';
+  import axios, { AxiosHeaders } from 'axios';
 
   import useModal from '@/hooks/modal';
 
@@ -235,16 +234,6 @@
     return true;
   }
 
-  interface UploadResultType {
-    hasError: boolean;
-    failRecords: Array<{
-      msg: string;
-      lineNum: number;
-    }>;
-    // Add other properties if your backend returns more fields on success
-    [key: string]: any;
-  }
-
   function customRequest(options: UploadRequestOptions) {
     // Changed return type to any
     const { onProgress, file, data } = options;
@@ -267,11 +256,7 @@
     });
   }
 
-  function handleError(
-    error: Error,
-    uploadFile: UploadFile,
-    uploadFiles: UploadFiles
-  ) {
+  function handleError(error: Error, uploadFile: UploadFile) {
     canUpload.value = false;
     loading.value = false;
     result.value = {

+ 2 - 0
src/components/index.ts

@@ -6,6 +6,7 @@ import SelectRangeDatetime from './select-range-datetime/index.vue';
 import SelectRangeTime from './select-range-time/index.vue';
 import UploadButton from './upload-button/index.vue';
 import SelectExam from './select-exam/index.vue';
+import ImportDialog from './import-dialog/index.vue';
 
 export default {
   install(Vue: App) {
@@ -14,5 +15,6 @@ export default {
     Vue.component('SelectRangeTime', SelectRangeTime);
     Vue.component('UploadButton', UploadButton);
     Vue.component('SelectExam', SelectExam);
+    Vue.component('ImportDialog', ImportDialog);
   },
 };

+ 2 - 25
src/components/upload-button/index.vue

@@ -19,14 +19,6 @@
     >
       <template #trigger>
         <el-button type="primary" :disabled="loading">{{ btnText }}</el-button>
-        <!-- <el-button
-          v-if="!autoUpload"
-          type="primary"
-          :loading="loading"
-          :disabled="!canUpload"
-          @click.stop="startUpload"
-          >开始上传</el-button
-        > -->
       </template>
     </el-upload>
   </div>
@@ -37,7 +29,6 @@
   import { fileMD5 } from '@/utils/md5';
   import { ElMessage } from 'element-plus';
   import type {
-    UploadProps,
     UploadRequestOptions,
     UploadFile,
     UploadFiles,
@@ -85,18 +76,12 @@
   ]);
 
   const uploadRef = ref();
-  const attachmentName = ref('');
   const canUpload = ref(false);
   const uploadDataDict = ref({});
   const headers = ref({ md5: '' });
   const result = ref({ success: true, message: '' });
   const loading = ref(false);
 
-  function startUpload() {
-    loading.value = true;
-    uploadRef.value?.submit();
-  }
-
   function handleFileChange(uploadFile: UploadFile, uploadFiles: UploadFiles) {
     if (props.autoUpload || !uploadFiles.length) return;
     // Element Plus's status: ready, uploading, success, fail
@@ -158,11 +143,7 @@
     });
   }
 
-  function handleError(
-    error: Error,
-    uploadFile: UploadFile,
-    uploadFiles: UploadFiles
-  ) {
+  function handleError(error: Error) {
     canUpload.value = false;
     loading.value = false;
     result.value = {
@@ -172,11 +153,7 @@
     ElMessage.error(result.value.message);
     emit('uploadError', result.value);
   }
-  function handleSuccess(
-    response: any,
-    uploadFile: UploadFile,
-    uploadFiles: UploadFiles
-  ) {
+  function handleSuccess(response: any, uploadFile: UploadFile) {
     canUpload.value = false;
     loading.value = false;
     result.value = {

+ 16 - 23
src/hooks/table.ts

@@ -1,4 +1,4 @@
-import { ref, isRef } from 'vue';
+import { ref, isRef, reactive } from 'vue';
 import { PageResult } from '@/api/types/common';
 
 export default function useTable<T extends Record<string, any>>(
@@ -6,62 +6,55 @@ export default function useTable<T extends Record<string, any>>(
   searchModel: Record<string, any>,
   initAutoFetch = false
 ) {
-  const pageNumber = ref(1);
-  const pageSize = ref(10);
-  const total = ref(0);
-  const dataList = ref<T[]>();
+  const dataList = ref<T[]>([]);
+  const pagination = reactive({
+    pageNumber: 1,
+    pageSize: 20,
+    total: 0,
+    layout: `prev, pager, next, jumper, ->, total`,
+  });
 
   async function getList() {
     const datas = {
       ...(isRef(searchModel || {}) ? searchModel.value : searchModel),
-      pageNumber: pageNumber.value,
-      pageSize: pageSize.value,
+      pageNumber: pagination.pageNumber,
+      pageSize: pagination.pageSize,
     };
     const data = await apiFunc(datas);
     dataList.value = data.result;
-    total.value = data.totalCount;
+    pagination.total = data.totalCount;
   }
   if (initAutoFetch) getList();
 
   async function toPage(page: number) {
-    pageNumber.value = page;
+    pagination.pageNumber = page;
     await getList();
   }
 
   async function pageSizeChange(size: number) {
-    pageSize.value = size;
+    pagination.pageSize = size;
     await toPage(1);
   }
 
   function getRowIndex(index: number) {
-    return pageSize.value * (pageNumber.value - 1) + index + 1;
+    return pagination.pageSize * (pagination.pageNumber - 1) + index + 1;
   }
 
   function deletePageLastItem(len = 1) {
-    let page = pageNumber.value || 1;
+    let page = pagination.pageNumber || 1;
     if (dataList.value && dataList.value.length === len) {
       page = page > 1 ? page - 1 : 1;
     }
     toPage(page);
   }
 
-  const pagination = ref({
-    total,
-    current: pageNumber,
-    pageSize,
-    showTotal: true,
-    showJumper: true,
-    showPageSize: true,
-    onChange: toPage,
-    onPageSizeChange: pageSizeChange,
-  });
-
   return {
     dataList,
     pagination,
     getRowIndex,
     getList,
     toPage,
+    pageSizeChange,
     deletePageLastItem,
   };
 }

+ 2 - 2
src/layout/default-layout.vue

@@ -2,7 +2,7 @@
   <div class="home">
     <div class="home-header">
       <div>
-        <h1 class="home-title">预约报名系统</h1>
+        <h1 class="home-title">云阅卷</h1>
       </div>
       <div class="home-action">
         <!-- <div class="home-action-item">
@@ -95,7 +95,7 @@
   import { onMounted, ref, watch } from 'vue';
   import { useRoute, useRouter } from 'vue-router';
   import { useAppStore, useUserStore } from '@/store';
-  import { ElMessage, ElMessageBox } from 'element-plus';
+  import { ElMessageBox } from 'element-plus';
 
   import ResetPwd from '@/views/login/login/ResetPwd.vue';
   import Footer from '@/components/footer/index.vue';

+ 42 - 0
src/router/routes/modules/admin.ts

@@ -0,0 +1,42 @@
+import { DEFAULT_LAYOUT } from '../base';
+import { AppRouteRecordRaw } from '../types';
+
+const ADMIN: AppRouteRecordRaw = {
+  path: '/admin',
+  name: 'base',
+  component: DEFAULT_LAYOUT,
+  meta: {
+    requiresAuth: true,
+  },
+  children: [
+    {
+      path: 'school-manage',
+      name: 'SchoolManage',
+      component: () => import('@/views/admin/school-manage/SchoolManage.vue'),
+      meta: {
+        title: '学校管理',
+        requiresAuth: true,
+      },
+    },
+    {
+      path: 'set-manage',
+      name: 'SetManage',
+      component: () => import('@/views/admin/SetManage.vue'),
+      meta: {
+        title: '配置管理',
+        requiresAuth: true,
+      },
+    },
+    {
+      path: 'auth-manage',
+      name: 'AuthManage',
+      component: () => import('@/views/admin/AuthManage.vue'),
+      meta: {
+        title: '授权管理',
+        requiresAuth: true,
+      },
+    },
+  ],
+};
+
+export default ADMIN;

+ 0 - 0
src/views/admin/AuthManage.vue


+ 0 - 0
src/views/admin/SetManage.vue


+ 149 - 0
src/views/admin/school-manage/ModifySchool.vue

@@ -0,0 +1,149 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    width="600px"
+    :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="140px"
+    >
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="formModel.name" placeholder="请输入名称" />
+      </el-form-item>
+      <el-form-item label="省份" prop="province">
+        <el-input v-model="formModel.province" placeholder="请输入省份" />
+      </el-form-item>
+      <el-form-item label="城市" prop="city">
+        <el-input v-model="formModel.city" placeholder="请输入城市" />
+      </el-form-item>
+      <el-form-item label="详细地址" prop="address">
+        <el-input v-model="formModel.address" placeholder="请输入详细地址" />
+      </el-form-item>
+      <el-form-item label="开启双评轨迹" prop="enableDoubleTrack">
+        <el-switch v-model="formModel.enableDoubleTrack" />
+      </el-form-item>
+      <el-form-item label="删除分组授权码" prop="deleteGroupAuthCode">
+        <el-switch v-model="formModel.deleteGroupAuthCode" />
+      </el-form-item>
+      <el-form-item label="描述" prop="description">
+        <el-input
+          v-model="formModel.description"
+          type="textarea"
+          placeholder="请输入描述"
+          :maxlength="999"
+        />
+      </el-form-item>
+      <el-form-item label="AccessKey">
+        <el-input :value="formModel.accessKey" readonly disabled />
+      </el-form-item>
+      <el-form-item label="AccessSecret">
+        <el-input :value="formModel.accessSecret" readonly disabled />
+      </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 { SchoolItem } from '@/api/types/admin';
+  import useModal from '@/hooks/modal';
+  import useLoading from '@/hooks/loading';
+  import { objAssign, objModifyAssign } from '@/utils/utils';
+  import { updateSchool } from '@/api/admin';
+
+  defineOptions({
+    name: 'ModifySchool',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  interface Props {
+    rowData: SchoolItem;
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    rowData: {} as SchoolItem,
+  });
+  const emit = defineEmits(['modified']);
+
+  const isEdit = computed(() => !!props.rowData?.id);
+  const title = computed(() => `${isEdit.value ? '编辑' : '新增'}学校`);
+
+  const formRef = ref<FormInstance>();
+  const initialFormState: Partial<SchoolItem> = {
+    name: '',
+    province: '',
+    city: '',
+    address: '',
+    enableDoubleTrack: false,
+    deleteGroupAuthCode: false,
+    description: '',
+    accessKey: '',
+    accessSecret: '',
+  };
+
+  const formModel = reactive({ ...initialFormState });
+
+  const rules: FormRules<keyof SchoolItem> = {
+    name: [{ required: true, message: '请输入名称', trigger: 'change' }],
+    province: [
+      { required: false, message: '最多50字符', max: 50, trigger: 'change' },
+    ],
+    city: [
+      { required: false, message: '最多50字符', max: 50, trigger: 'change' },
+    ],
+  };
+
+  const handleClose = () => {
+    formRef.value?.resetFields();
+  };
+
+  /* confirm */
+  const { loading, setLoading } = useLoading();
+  async function confirm() {
+    const err = await formRef.value?.validate();
+    if (err) return;
+
+    setLoading(true);
+    const datas = objAssign(formModel, {});
+    let res = true;
+    await updateSchool(datas).catch(() => {
+      res = false;
+    });
+    setLoading(false);
+    if (!res) return;
+    ElMessage.success('修改成功!');
+    emit('modified', datas);
+    close();
+  }
+
+  /* init modal */
+  function modalBeforeOpen() {
+    if (props.rowData.id) {
+      objModifyAssign(formModel, props.rowData);
+    } else {
+      objModifyAssign(formModel, initialFormState);
+    }
+  }
+</script>

+ 169 - 0
src/views/admin/school-manage/ModifySchoolAdmin.vue

@@ -0,0 +1,169 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    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="adminName">
+        <el-input
+          v-model="formModel.adminName"
+          placeholder="请输入管理员名称"
+        />
+      </el-form-item>
+      <el-form-item label="账号" prop="adminLoginName">
+        <el-input
+          v-model="formModel.adminLoginName"
+          placeholder="请输入登录账号"
+        />
+      </el-form-item>
+      <el-form-item label="密码" prop="password">
+        <el-input
+          v-model="formModel.password"
+          type="password"
+          show-password
+          placeholder="请输入密码"
+        />
+      </el-form-item>
+      <el-form-item label="重复输入" prop="confirmPassword">
+        <el-input
+          v-model="formModel.confirmPassword"
+          type="password"
+          show-password
+          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 } from 'vue';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import { ElMessage } from 'element-plus';
+  import useModal from '@/hooks/modal';
+  import useLoading from '@/hooks/loading';
+  import { updateSchoolAdmin } from '@/api/admin'; // 假设有更新学校管理员的API
+  import type { SchoolItem, SchoolUpdateAdminParam } from '@/api/types/admin'; // 假设的管理员信息类型
+  import { objModifyAssign } from '@/utils/utils';
+
+  defineOptions({
+    name: 'ModifySchoolAdmin',
+  });
+
+  /* modal */
+  const { visible, open, close } = useModal();
+  defineExpose({ open, close });
+
+  interface Props {
+    rowData: SchoolItem;
+  }
+
+  const props = withDefaults(defineProps<Props>(), {
+    rowData: {} as SchoolItem,
+  });
+
+  const emit = defineEmits(['modified']);
+
+  const formRef = ref<FormInstance>();
+
+  const initialFormState: SchoolUpdateAdminParam & { confirmPassword: string } =
+    {
+      id: 0,
+      adminName: '',
+      adminLoginName: '',
+      password: '',
+      confirmPassword: '',
+    };
+
+  const formModel = reactive({ ...initialFormState });
+
+  const validatePass = (rule: any, value: any, callback: any) => {
+    if (value === '') {
+      callback(new Error('请输入密码'));
+    } else {
+      if (formModel.confirmPassword !== '') {
+        if (!formRef.value) return;
+        formRef.value.validateField('confirmPassword');
+      }
+      callback();
+    }
+  };
+
+  const validatePass2 = (rule: any, value: any, callback: any) => {
+    if (value === '') {
+      callback(new Error('请再次输入密码'));
+    } else if (value !== formModel.password) {
+      callback(new Error('两次输入的密码不一致!'));
+    } else {
+      callback();
+    }
+  };
+
+  const rules = reactive<FormRules>({
+    adminName: [
+      { required: true, message: '请输入管理员名称', trigger: 'blur' },
+    ],
+    adminLoginName: [
+      { required: true, message: '请输入登录账号', trigger: 'blur' },
+    ],
+    password: [
+      { required: true, validator: validatePass, trigger: 'blur' },
+      { min: 6, message: '密码长度至少为6位', trigger: 'blur' },
+    ],
+    confirmPassword: [
+      { required: true, validator: validatePass2, trigger: 'blur' },
+    ],
+  });
+
+  const handleClose = () => {
+    formRef.value?.resetFields();
+  };
+
+  /* confirm */
+  const { loading, setLoading } = useLoading();
+  async function confirm() {
+    if (!formRef.value) return;
+    try {
+      await formRef.value.validate();
+      setLoading(true);
+      const params = {
+        ...formModel,
+      };
+      // delete params.confirmPassword;
+      await updateSchoolAdmin(params);
+      ElMessage.success('管理员信息保存成功!');
+      emit('modified');
+      close();
+    } catch (error) {
+      console.error('表单校验失败或API请求错误:', error);
+      // ElMessage.error('保存失败,请检查表单');
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  /* init modal */
+  function modalBeforeOpen() {
+    objModifyAssign(formModel, props.rowData || {});
+  }
+</script>

+ 171 - 0
src/views/admin/school-manage/SchoolManage.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="part-box is-filter">
+    <el-form inline>
+      <el-form-item label="姓名">
+        <el-input
+          v-model.trim="searchModel.name"
+          placeholder="请输入"
+          clearable
+        >
+        </el-input>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" @click="toPage(1)">查询</el-button>
+        <el-button type="primary" status="success" @click="onAdd"
+          >新建</el-button
+        >
+      </el-form-item>
+    </el-form>
+  </div>
+  <div class="part-box">
+    <el-table class="page-table" :data="dataList">
+      <el-table-column property="name" label="名称" />
+      <el-table-column property="code" label="代码" />
+      <el-table-column property="subOrgCode" label="子机构代码" width="100" />
+      <el-table-column
+        property="enableDoubleTrack"
+        label="双评轨迹"
+        width="100"
+      />
+      <el-table-column property="province" label="省份" />
+      <el-table-column property="city" label="城市" />
+      <el-table-column label="操作">
+        <template #default="scope">
+          <el-button size="small" @click="onEdit(scope.row)"> 修改 </el-button>
+          <el-button size="small" @click="onEditAdmin(scope.row)">
+            编辑管理员
+          </el-button>
+          <el-button size="small" @click="onEditRoleAuth(scope.row)">
+            角色权限
+          </el-button>
+          <el-button size="small" @click="onSplitCourse(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>
+
+  <!-- 编辑学校 -->
+  <ModifySchool ref="modifySchoolRef" :row-data="curRow" @modified="getList" />
+  <!-- 编辑学校管理员 -->
+  <ModifySchoolAdmin ref="modifySchoolAdminRef" :row-data="curRow" />
+
+  <!-- ImportDialog -->
+  <ImportDialog
+    ref="importDialogRef"
+    title="科目拆分"
+    upload-url="/api/admin/site/import"
+    :format="['xls', 'xlsx']"
+    :download-handle="downloadTemplate"
+    download-filename="科目拆分模板.xlsx"
+    :auto-upload="false"
+    :before-submit-handle="beforeSubmitHandle"
+    @upload-success="importSuccess"
+  >
+    <el-form ref="formRef" label-width="100px">
+      <el-form-item label="名称">
+        <el-input :value="curRow.name" readonly disabled />
+      </el-form-item>
+      <el-form-item label="考试" prop="exam">
+        <select-exam v-model="importExamId" size="large" />
+      </el-form-item>
+    </el-form>
+  </ImportDialog>
+</template>
+
+<script setup lang="ts">
+  import { reactive, ref } from 'vue';
+  import { schoolListPage } from '@/api/admin';
+  import { SchoolItem, SchoolListFilter } from '@/api/types/admin';
+  import useTable from '@/hooks/table';
+  import { ElMessage } from 'element-plus';
+
+  // import useDictOption from '@/hooks/dict-option';
+  // import useLoading from '@/hooks/loading';
+  // import { useAppStore, useUserStore } from '@/store';
+
+  import ModifySchool from './ModifySchool.vue';
+  import ModifySchoolAdmin from './ModifySchoolAdmin.vue';
+
+  defineOptions({
+    name: 'SchoolManage',
+  });
+
+  // const appStore = useAppStore();
+  // const userStore = useUserStore();
+
+  // const preDate = getBeforeWeek();
+  const searchModel = reactive<SchoolListFilter>({
+    name: '',
+  });
+
+  const { dataList, pagination, getList, toPage, pageSizeChange } =
+    useTable<SchoolItem>(schoolListPage, searchModel, false);
+
+  // table action
+  // 导入
+  // const importStudentDialogRef = ref();
+  // function toImport() {
+  //   importStudentDialogRef.value?.open();
+  // }
+
+  const curRow = ref({} as SchoolItem);
+  const modifySchoolRef = ref();
+  function onEdit(row: SchoolItem) {
+    console.log(row);
+    curRow.value = row;
+    modifySchoolRef.value?.open();
+  }
+  function onAdd() {
+    curRow.value = {} as SchoolItem;
+    modifySchoolRef.value?.open();
+  }
+
+  const modifySchoolAdminRef = ref();
+  function onEditAdmin(row: SchoolItem) {
+    console.log(row);
+    curRow.value = row;
+    modifySchoolRef.value?.open();
+  }
+  function onEditRoleAuth(row: SchoolItem) {
+    // TODO:权限编辑
+    console.log(row);
+  }
+  // 导入
+  const importDialogRef = ref();
+  const importExamId = ref(0);
+  function onSplitCourse(row: SchoolItem) {
+    curRow.value = row;
+    importDialogRef.value?.open();
+  }
+
+  async function downloadTemplate() {
+    // const res = await downloadByApi(() => agentTemplate()).catch((e) => {
+    //   Message.error(e || '下载失败,请重新尝试!');
+    // });
+    // if (!res) return;
+    // Message.success('下载成功!');
+  }
+
+  async function beforeSubmitHandle() {
+    if (!importExamId.value) {
+      ElMessage.error('请选择考试');
+      return Promise.reject(new Error('请选择考试'));
+    }
+
+    return Promise.resolve();
+  }
+
+  function importSuccess() {
+    importDialogRef.value?.close();
+  }
+</script>

+ 2 - 2
src/views/system/comp-test/comp-test.vue

@@ -89,7 +89,7 @@
 
     <section class="component-section">
       <h2 class="section-title">SelectTask Component</h2>
-      <SelectTask
+      <SelectExam
         v-model="selectedTask"
         placeholder="Select a task"
         clearable
@@ -135,7 +135,7 @@
   // SvgIcon is globally registered
   // SelectRangeDatetime is globally registered
   // SelectRangeTime is globally registered
-  import SelectTask from '@/components/select-task/index.vue';
+  import SelectExam from '@/components/select-exam/index.vue';
   import SelectTeaching from '@/components/select-teaching/index.vue';
   import Footer from '@/components/footer/index.vue';