浏览代码

feat: 细节&交互优化

chenhao 2 年之前
父节点
当前提交
5af37a3a58

+ 1 - 0
components.d.ts

@@ -14,6 +14,7 @@ declare module '@vue/runtime-core' {
     AFormItem: typeof import('ant-design-vue/es')['FormItem']
     AInput: typeof import('ant-design-vue/es')['Input']
     AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
+    AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
     AMenu: typeof import('ant-design-vue/es')['Menu']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
     AModal: typeof import('ant-design-vue/es')['Modal']

+ 0 - 6
src/assets/less/antd.modal.less

@@ -15,10 +15,4 @@
       height: 36px;
     }
   }
-  .operation-group {
-    padding: 24px 0;
-    .ant-btn ~ .ant-btn {
-      margin-left: 12px;
-    }
-  }
 }

+ 6 - 0
src/assets/less/antd.table.less

@@ -1,4 +1,10 @@
 .ant-table {
+  line-height: 1;
+  .ant-table-cell {
+    box-sizing: border-box;
+    height: 34px;
+    padding: 0;
+  }
   .ant-table-container {
     .ant-table-thead {
       tr:first-child {

+ 2 - 0
src/assets/less/global.less

@@ -10,6 +10,8 @@ body {
 }
 
 #app {
+  width: 100%;
+  overflow-x: hidden;
   min-height: 100%;
 }
 

+ 3 - 3
src/layout/index.vue

@@ -16,16 +16,16 @@ import LeftMenu from "@/layout/left-menu.vue";
 <style scoped lang="less">
 .layout {
   width: 100vw;
-  min-height: 100vh;
+  height: 100vh;
   display: flex;
   .layout-left {
     width: 200px;
-    min-height: 100%;
+    height: 100%;
     background: @white;
   }
   .layout-content {
     flex: 1 1 0;
-    min-height: 100%;
+    height: 100%;
     overflow: auto;
     padding: 24px;
   }

+ 2 - 1
src/layout/left-menu.vue

@@ -49,9 +49,10 @@ const handleExitLogin = () => {
 <style scoped lang="less">
 .menu {
   width: 100%;
-  min-height: 100%;
+  height: 100vh;
   display: flex;
   flex-direction: column;
+  position: relative;
   .menu-header {
     padding: 0 16px;
     height: 86px;

+ 60 - 41
src/pages/exam-manage/index.vue

@@ -7,7 +7,7 @@
             v-model:value="query.schoolId"
             show-search
             :filterOption="false"
-            @search="querySchoolList"
+            @search="(name:string) => querySchoolList(name, 'list')"
             placeholder="学校名称"
           >
             <a-select-option
@@ -84,7 +84,7 @@
     </Block>
     <a-modal
       v-model:visible="showModal"
-      :title="`${examInfo.id ? '编辑' :'新增'}考试`"
+      :title="`${examInfo.id ? '编辑' : '新增'}考试`"
       okText="确定"
       cancelText="取消"
       :maskClosable="false"
@@ -97,12 +97,12 @@
             v-model:value="examInfo.schoolId"
             show-search
             :disabled="!!examInfo.id"
-            @search="querySchoolList"
+            @search="(name: string) => querySchoolList(name,'form')"
             :filterOption="false"
             placeholder="学校名称"
           >
             <a-select-option
-              v-for="school in schoolTableData.result"
+              v-for="school in examInfo.schoolTableData.result"
               :key="school.id"
               :value="school.id"
               >{{ school.name }}</a-select-option
@@ -131,7 +131,7 @@
 </template>
 
 <script setup lang="ts" name="PageExam">
-import { reactive, ref, watch } from "vue";
+import { reactive, ref, watch, unref, markRaw, toRaw } from "vue";
 import { PlusCircleOutlined } from "@ant-design/icons-vue";
 import { getSchoolListHttp } from "@/apis/school";
 import {
@@ -151,30 +151,12 @@ const mainStore = useMainStore();
 const showModal = ref(false);
 const tableLoading = ref(false);
 
-const examInfo = ref<BaseExamInfo>({
-  examStatus: "",
-  name: "",
-  schoolId: "",
-  id: void 0,
-});
-
-const examRules = {
-  name: [{ required: true, message: "请填写考试批次" }],
-  examStatus: [{ required: true, message: "请选择考试状态" }],
-  schoolId: [{ required: true, message: "请选择学校" }],
-};
-
-const { validate, validateInfos, resetFields } = Form.useForm(
-  examInfo.value,
-  examRules
-);
-
 /** 请求参数 */
 const query = reactive<FetchExamListQuery>({
   name: "",
   schoolId: mainStore.systemUserInfo?.schoolId || "",
   pageNumber: 1,
-  pageSize: 8,
+  pageSize: 10,
 });
 
 /** table配置 */
@@ -199,42 +181,78 @@ const examTableData = reactive<MultiplePageData<ExamListInfo>>({
   result: [],
 });
 
+const examInfo = reactive<
+  BaseExamInfo & { schoolTableData: MultiplePageData<SchoolListInfo> }
+>({
+  schoolTableData: { totalCount: 0, result: [] },
+  examStatus: "",
+  name: "",
+  schoolId: "",
+  id: void 0,
+});
+
+const examRules = {
+  name: [{ required: true, message: "请填写考试批次" }],
+  examStatus: [{ required: true, message: "请选择考试状态" }],
+  schoolId: [{ required: true, message: "请选择学校" }],
+};
+
+const { validate, validateInfos, resetFields } = Form.useForm(
+  examInfo,
+  examRules
+);
+
 /** 查询学校列表 */
-const querySchoolList = throttle(async (name: string = "") => {
-  try {
-    tableLoading.value = true;
-    const { result = [], totalCount } = await getSchoolListHttp({
-      pageNumber: 1,
-      pageSize: 10,
-      name,
-    });
-    tableLoading.value = false;
-    Object.assign(schoolTableData, { result, totalCount });
-  } catch (error) {
-    console.error(error);
-  }
-}, 100);
+const querySchoolList = throttle(
+  async (name: string = "", type: "list" | "form" = "list") => {
+    const isList = type === "list";
+    try {
+      const { result = [], totalCount } = await getSchoolListHttp({
+        pageNumber: 1,
+        pageSize: 10,
+        name,
+      });
+      Object.assign(isList ? schoolTableData : examInfo.schoolTableData, {
+        result,
+        totalCount,
+      });
+    } catch (error) {
+      console.error(error);
+    }
+  },
+  100
+);
 
 /** 显示新增考试弹窗 */
 const toggleAddExamModal = (show: boolean = true) => {
   showModal.value = show;
+  if (show) {
+    querySchoolList("", "form");
+  }
 };
 
 /** 查询考试列表 */
 const queryExamList = async () => {
   try {
+    tableLoading.value = true;
     const { result = [], totalCount } = await getExamListHttp(query);
     Object.assign(examTableData, { result, totalCount });
   } catch (error) {
     console.error(error);
   }
+  tableLoading.value = false;
 };
 
 watch(() => query.pageNumber, queryExamList);
 
 /** 编辑考试 */
 const onEdit = (record: ExamListInfo) => {
-  Object.assign(examInfo.value, { ...record });
+  Object.assign(examInfo, {
+    id: record.id,
+    examStatus: record.examStatus,
+    name: record.name,
+    schoolId: record.schoolId,
+  });
   toggleAddExamModal(true);
 };
 
@@ -242,8 +260,9 @@ const onEdit = (record: ExamListInfo) => {
 const onAddNewExam = () => {
   validate().then((valid) => {
     if (valid) {
-      editExamInfoHttp(examInfo.value).then(() => {
-        message.success(`${examInfo.value.id ? "修改" : "添加"}成功`);
+      const { schoolTableData, ...info } = examInfo;
+      editExamInfoHttp(info).then(() => {
+        message.success(`${info.id ? "修改" : "添加"}成功`);
         toggleAddExamModal(false);
         query.schoolId && queryExamList();
       });

+ 2 - 3
src/pages/login/index.vue

@@ -21,16 +21,15 @@
           </a-input>
         </a-form-item>
         <a-form-item v-bind="validateInfos.password">
-          <a-input
+          <a-input-password
             class="login-input login-input-pwd"
-            type="password"
             v-model:value="loginModel.password"
             placeholder="请输入密码"
           >
             <template #prefix>
               <SvgIcon class="input-icon" name="pwd-icon" />
             </template>
-          </a-input>
+          </a-input-password>
         </a-form-item>
         <a-button
           class="login-button"

+ 19 - 20
src/pages/school-manage/index.vue

@@ -39,8 +39,7 @@
           (_:any, index:number) => (index % 2 === 1 ? 'table-striped' : null)
         "
       >
-        <template #bodyCell="{ column, record, index,text }">
-
+        <template #bodyCell="{ column, record, index, text }">
           <template v-if="column.dataIndex === 'index'">
             {{ index + 1 }}
           </template>
@@ -53,7 +52,7 @@
             </template>
           </template>
           <template v-else-if="column.dataIndex === 'updateTime'">
-           {{text || record['createTime']}}
+            {{ text || record["createTime"] }}
           </template>
           <template v-else-if="column.dataIndex === 'operation'">
             <div class="tw-flex tw-items-center">
@@ -91,7 +90,7 @@
       cancelText="取消"
       :maskClosable="false"
       @ok="onAddNewSchool"
-      :after-close="resetSchoolInfo"
+      :afterClose="resetFields"
     >
       <a-form :labelCol="{ span: 6 }">
         <a-form-item label="学校编码" v-bind="validateInfos.code">
@@ -142,7 +141,7 @@ import VueQrCode from "vue-qrcode";
 
 const showModal = ref(false);
 
-const schoolInfo = ref<BaseSchoolInfo>({
+const schoolInfo = reactive<BaseSchoolInfo>({
   code: "",
   contacts: "",
   name: "",
@@ -163,7 +162,10 @@ const schoolRules = {
   ],
 };
 
-const { validate, validateInfos } = Form.useForm(schoolInfo.value, schoolRules);
+const { validate, validateInfos, resetFields } = Form.useForm(
+  schoolInfo,
+  schoolRules
+);
 
 /** 请求参数 */
 const query = reactive<FetchSchoolListQuery>({
@@ -221,7 +223,15 @@ const updateSchoolStatus = (record: SchoolListInfo) => {
 
 /** 编辑学校 */
 const onEdit = (record: SchoolListInfo) => {
-  Object.assign(schoolInfo.value, { ...record });
+  Object.assign(schoolInfo, {
+    code: record.code,
+    contacts: record.contacts,
+    name: record.name,
+    region: record.region,
+    telephone: record.telephone,
+    enable: !!record.enable,
+    id: record.id
+  });
   toggleAddSchoolModal(true);
 };
 
@@ -229,8 +239,8 @@ const onEdit = (record: SchoolListInfo) => {
 const onAddNewSchool = () => {
   validate().then((valid) => {
     if (valid) {
-      editSchoolInfoHttp(schoolInfo.value).then(() => {
-        message.success(`${schoolInfo.value.code ? "修改" : "添加"}成功`);
+      editSchoolInfoHttp(schoolInfo).then(() => {
+        message.success(`${schoolInfo.code ? "修改" : "添加"}成功`);
         toggleAddSchoolModal(false);
         querySchoolList();
       });
@@ -238,17 +248,6 @@ const onAddNewSchool = () => {
   });
 };
 
-/** 初始化schoolInfo */
-const resetSchoolInfo = () => {
-  schoolInfo.value = {
-    contacts: "",
-    name: "",
-    region: "",
-    telephone: "",
-    enable: true,
-  };
-};
-
 /** effect */
 querySchoolList();
 </script>

+ 227 - 121
src/pages/subjects-manage/index.vue

@@ -7,7 +7,7 @@
             v-model:value="query.schoolId"
             show-search
             :filterOption="false"
-            @search="querySchoolList"
+            @search="(name:string) => querySchoolList(name, 'list')"
             placeholder="学校名称"
           >
             <a-select-option
@@ -23,7 +23,7 @@
             v-model:value="query.examId"
             :filterOption="false"
             show-search
-            @search="queryExamList"
+            @search="(name:string) => queryExamList(name, 'list')"
             placeholder="考试批次"
           >
             <a-select-option
@@ -104,8 +104,12 @@
             {{ index + 1 }}
           </template>
           <template v-else-if="column.dataIndex === 'courseName'">
-            {{text}}
-            <img v-if="!record.groupFinish" class="star-icon" src="@imgs/common/star-icon.png" />
+            {{ text }}
+            <img
+              v-if="!record.groupFinish"
+              class="star-icon"
+              src="@imgs/common/star-icon.png"
+            />
           </template>
           <template v-else-if="column.dataIndex === 'enable'">
             <template v-if="record.enable">
@@ -120,34 +124,75 @@
     </Block>
     <a-modal
       v-model:visible="showImportModal"
-      :title="`导入${importType === 'subject' ? '科目' : '试卷结构'}`"
-      :footer="false"
       :maskClosable="false"
-      :after-close="clearFileList"
+      :title="`导入${uploadQuery.type === 'subject' ? '科目' : '主观题'}`"
+      okText="确认上传"
+      cancelText="取消"
+      @ok="onImport"
+      :afterClose="resetFields"
     >
-      <a-upload
-        :file-list="fileList"
-        :before-upload="beforeUpload"
-        @remove="handleRemove"
-        :max-count="1"
-        type="primary"
-      >
-        <a-button>
-          <upload-outlined></upload-outlined>
-          选择文件
-        </a-button>
-      </a-upload>
-      <div class="operation-group">
-        <a-button type="primary" @click="downloadTemplate">下载模板</a-button>
-        <a-button type="primary" @click="clearFileList">清空上传文件</a-button>
-        <a-button type="primary" @click="onImport">确认上传</a-button>
-      </div>
+      <a-form :labelCol="{ span: 6 }">
+        <a-form-item label="学校名称" v-bind="validateInfos.schoolId">
+          <a-select
+            v-model:value="uploadQuery.schoolId"
+            show-search
+            :filterOption="false"
+            @search="(name: string) => querySchoolList(name,'form')"
+            placeholder="学校名称"
+          >
+            <a-select-option
+              v-for="school in uploadQuery.schoolTableData.result"
+              :key="school.id"
+              :value="`${school.id}`"
+              >{{ school.name }}</a-select-option
+            >
+          </a-select>
+        </a-form-item>
+        <a-form-item label="考试批次" v-bind="validateInfos.examId">
+          <a-select
+            v-model:value="uploadQuery.examId"
+            :filterOption="false"
+            show-search
+            @search="(name:string) => queryExamList(name, 'form')"
+            placeholder="考试批次"
+          >
+            <a-select-option
+              v-for="exam in uploadQuery.examTableData.result"
+              :key="exam.id"
+              :value="`${exam.id}`"
+              >{{ exam.name }}</a-select-option
+            >
+          </a-select>
+        </a-form-item>
+        <a-form-item
+          :label="`${
+            uploadQuery.type === 'subject' ? '科目' : '主观题'
+          }导入文件`"
+          v-bind="validateInfos.fileList"
+        >
+          <a-upload
+            :file-list="uploadQuery.fileList"
+            :before-upload="beforeUpload"
+            @remove="handleRemove"
+            :max-count="1"
+            type="primary"
+          >
+            <a-button>
+              <upload-outlined></upload-outlined>
+              选择文件
+            </a-button>
+            <a class="tw-ml-4 tw-align-bottom" @click.stop="downloadTemplate">
+              下载导入模板
+            </a>
+          </a-upload>
+        </a-form-item>
+      </a-form>
     </a-modal>
   </div>
 </template>
 
 <script setup lang="ts" name="PageSubjects">
-import { reactive, ref, watch } from "vue";
+import { onBeforeMount, reactive, ref, watch } from "vue";
 import {
   UploadOutlined,
   CheckCircleFilled,
@@ -157,6 +202,7 @@ import {
 
 import Block from "@/components/block/index.vue";
 import type { TableColumnType, UploadProps } from "ant-design-vue";
+import { Form } from "ant-design-vue";
 import {
   getSubjectsListHttp,
   importSubjectsHttp,
@@ -168,15 +214,64 @@ import {
 import { getSchoolListHttp } from "@/apis/school";
 import { getExamListHttp } from "@/apis/exam";
 import { useMainStore } from "@/store/main";
+import { throttle } from "lodash-es";
+
+type ImportType = "subject" | "struct";
 
 const mainStore = useMainStore();
 
 const showImportModal = ref(false);
-const importType = ref("subject");
 
-const fileList = ref<UploadProps["fileList"]>([]);
+const tableLoading = ref(false);
+
+const ImportDownloadApi: Record<
+  ImportType,
+  {
+    upload: typeof importSubjectsHttp;
+    download: typeof downloadSubjectTemplateHttp;
+  }
+> = {
+  subject: {
+    upload: importSubjectsHttp,
+    download: downloadSubjectTemplateHttp,
+  },
+  struct: {
+    upload: importPaperStructHttp,
+    download: downloadPaperStructTemplateHttp,
+  },
+};
+
+/** 导入参数 */
+const uploadQuery = reactive<{
+  type: ImportType;
+  schoolId: string;
+  examId: string;
+  fileList: UploadProps["fileList"];
+  schoolTableData: MultiplePageData<SchoolListInfo>;
+  examTableData: MultiplePageData<ExamListInfo>;
+}>({
+  type: "subject",
+  schoolTableData: { totalCount: 0, result: [] },
+  examTableData: { totalCount: 0, result: [] },
+  schoolId: "",
+  examId: "",
+  fileList: [],
+});
+
+const uploadRules = {
+  schoolId: [{ required: true, message: "请选择学校" }],
+  examId: [{ required: true, message: "请选择考试批次" }],
+  fileList: [
+    { required: true, type: "array", len: 1, message: "请选择导入文件" },
+  ],
+};
+
+const { validate, validateInfos, resetFields } = Form.useForm(
+  uploadQuery,
+  uploadRules
+);
 
-/** 请求参数 */
+/** 查询参数 */
 const query = reactive<
   Omit<FetchSubjectsListQuery, "groupFinish"> & { groupFinish: number }
 >({
@@ -188,7 +283,7 @@ const query = reactive<
   /** 	分组状态 */
   groupFinish: 0,
   /** 学校id */
-  schoolId: mainStore.systemUserInfo?.schoolId || '',
+  schoolId: mainStore.systemUserInfo?.schoolId || "",
   /** 总分截止值 */
   totalScoreMax: "",
   /** 总分起始值 */
@@ -226,101 +321,96 @@ const subjectsTableData = reactive<MultiplePageData<SubjectsListInfo>>({
 });
 
 /** 查询学校列表 */
-const querySchoolList = async (name?: string) => {
-  try {
-    const { result = [], totalCount } = await getSchoolListHttp({
-      name,
-      pageNumber: 1,
-      pageSize: 10,
-    });
-    Object.assign(schoolTableData, { result, totalCount });
-  } catch (error) {
-    console.error(error);
-  }
-};
+const querySchoolList = throttle(
+  async (name: string = "", type: "list" | "form" = "list") => {
+    const isList = type === "list";
+    try {
+      const { result = [], totalCount } = await getSchoolListHttp({
+        name,
+        pageNumber: 1,
+        pageSize: 10,
+      });
+      Object.assign(isList ? schoolTableData : uploadQuery.schoolTableData, {
+        result,
+        totalCount,
+      });
+    } catch (error) {
+      return Promise.reject(error);
+    }
+  },
+  100
+);
 
 /** 查询考试列表 */
-const queryExamList = async (name?: string) => {
-  try {
-    if(!query.schoolId){
-      return 
+const queryExamList = throttle(
+  async (name: string = "", type: "list" | "form" = "list") => {
+    try {
+      const isList = type === "list";
+      const schoolId = isList ? query.schoolId : uploadQuery.schoolId;
+      if (!schoolId) {
+        return Promise.reject(`schoolId got : ${schoolId}`);
+      }
+      const { result = [], totalCount } = await getExamListHttp({
+        pageNumber: 1,
+        pageSize: 10,
+        name,
+        schoolId,
+      });
+      Object.assign(isList ? examTableData : uploadQuery.examTableData, {
+        result,
+        totalCount,
+      });
+    } catch (error) {
+      return Promise.reject(error);
     }
-    const { result = [], totalCount } = await getExamListHttp({
-      pageNumber: 1,
-      pageSize: 10,
-      name,
-      schoolId: query.schoolId,
-    });
-    query.examId = `${result?.[0]?.id || ''}`
-    Object.assign(examTableData, { result, totalCount });
-  } catch (error) {
-    console.error(error);
-  }
-};
+  },
+  100
+);
 
 /** 查询科目列表 */
 const querySubjectsList = async () => {
   try {
+    tableLoading.value = true;
     const { result = [], totalCount } = await getSubjectsListHttp({
       ...query,
       groupFinish: [void 0, true, false][query.groupFinish],
     });
     Object.assign(subjectsTableData, { result, totalCount });
   } catch (error) {
-    console.error(error);
+    return Promise.reject(error);
   }
+  tableLoading.value = false;
 };
 
 watch(() => query.pageNumber, querySubjectsList);
+
 watch(
   () => query.schoolId,
   () => {
     query.examId = "";
     Object.assign(examTableData, { result: [], totalCount: 0 });
-    queryExamList().then(()=>{
-      query.pageNumber = 1
-      querySubjectsList()
-    });
-  },
-  {
-    immediate: true
+    if (query.schoolId) {
+      queryExamList("", "list");
+    }
   }
 );
 
-const currentPageChange = ({ current }: { current: number }) => {
-  query.pageNumber = current;
-};
-
-/** 导入科目 */
-const importSubject = async () => {
-  try {
-    const formData = new FormData();
-    formData.append("examId", query.examId);
-    fileList.value?.forEach((file: any) => {
-      formData.append("file", file);
-    });
-    await importSubjectsHttp(formData).then(querySubjectsList);
-  } catch (error) {
-    console.error(error);
+watch(
+  () => uploadQuery.schoolId,
+  () => {
+    uploadQuery.examId = "";
+    Object.assign(uploadQuery.examTableData, { result: [], totalCount: 0 });
+    if (uploadQuery.schoolId) {
+      queryExamList("", "form");
+    }
   }
-};
+);
 
-/** 导入试卷结构 */
-const importPaperStruct = async () => {
-  try {
-    const formData = new FormData();
-    formData.append("examId", query.examId);
-    fileList.value?.forEach((file: any) => {
-      formData.append("file", file);
-    });
-    await importPaperStructHttp(formData).then(querySubjectsList);
-  } catch (error) {
-    console.error(error);
-  }
+const currentPageChange = ({ current }: { current: number }) => {
+  query.pageNumber = current;
 };
 
-/** 导出试卷结构 */
-
+/** 导出主观题 */
 const downloadPaperStruct = async () => {
   try {
     await downloadPaperStructHttp({
@@ -328,56 +418,72 @@ const downloadPaperStruct = async () => {
       groupFinish: [void 0, true, false][query.groupFinish],
     });
   } catch (error) {
-    console.error(error);
+    return Promise.reject(error);
   }
 };
 
 const handleRemove: UploadProps["onRemove"] = (file) => {
-  const index = fileList.value!.indexOf(file);
-  const newFileList = fileList.value!.slice();
+  const index = uploadQuery.fileList!.indexOf(file);
+  const newFileList = uploadQuery.fileList!.slice();
   newFileList.splice(index, 1);
-  fileList.value = newFileList;
+  uploadQuery.fileList = newFileList;
 };
 
 const beforeUpload: UploadProps["beforeUpload"] = (file) => {
-  fileList.value = [file];
+  uploadQuery.fileList = [file];
   return false;
 };
 
-const clearFileList = () => {
-  fileList.value = [];
-};
-
+/** 下载导入模板 */
 const downloadTemplate = async () => {
   try {
-    if (importType.value === "subject") {
-      await downloadSubjectTemplateHttp();
-    } else {
-      await downloadPaperStructTemplateHttp();
-    }
+    await ImportDownloadApi[uploadQuery.type].download();
   } catch (error) {
-    console.error(error);
+    return Promise.reject(error);
   }
 };
 
-const showImportModalType = (type: "subject" | "struct") => {
-  importType.value = type;
+/** 显示导入弹窗 */
+const showImportModalType = async (type: ImportType) => {
+  uploadQuery.type = type;
   showImportModal.value = true;
+  querySchoolList("", "form");
 };
 
-const onImport = () => {
-  if (importType.value === "subject") {
-    importSubject().then(() => {
-      showImportModal.value = false;
-    });
-  } else {
-    importPaperStruct().then(() => {
+/** 导入数据 */
+const onImport = async () => {
+  try {
+    const valid = await validate();
+    if (valid) {
+      const formData = new FormData();
+      formData.append("examId", uploadQuery.examId);
+      uploadQuery.fileList?.forEach((file: any) => {
+        formData.append("file", file);
+      });
+      await ImportDownloadApi[uploadQuery.type].upload(formData);
+      querySubjectsList();
       showImportModal.value = false;
-    });
+    }
+  } catch (error) {
+    return Promise.reject(error);
   }
 };
 
-querySchoolList();
+onBeforeMount(async () => {
+  try {
+    await querySchoolList();
+    if (
+      schoolTableData.result
+        .map((school) => `${school.id}`)
+        .some((id) => id === `${query.schoolId}`)
+    ) {
+      await queryExamList("", "list");
+      await querySubjectsList();
+    }
+  } catch (error) {
+    console.error(error);
+  }
+});
 </script>
 
 <style scoped lang="less">

+ 197 - 86
src/pages/user-manage/index.vue

@@ -7,7 +7,7 @@
             v-model:value="query.schoolId"
             show-search
             :filterOption="false"
-            @search="querySchoolList"
+            @search="(name:string) => querySchoolList(name,'list')"
             placeholder="学校名称"
           >
             <a-select-option
@@ -19,12 +19,19 @@
           </a-select>
         </a-form-item>
         <a-form-item label="登录名">
-          <a-input v-model:value="query.loginName"></a-input>
+          <a-input
+            v-model:value="query.loginName"
+            placeholder="登录手机号"
+          ></a-input>
         </a-form-item>
         <a-form-item label="角色">
-          <a-select v-model:value="query.role">
-            <a-select-option value="SCHOOL_ADMIN">学校管理员</a-select-option>
-            <a-select-option value="SECTION_LEADER">科组长</a-select-option>
+          <a-select v-model:value="query.role" placeholder="用户角色">
+            <a-select-option value="SCHOOL_ADMIN">{{
+              ROLE.SCHOOL_ADMIN
+            }}</a-select-option>
+            <a-select-option value="SECTION_LEADER">{{
+              ROLE.SECTION_LEADER
+            }}</a-select-option>
           </a-select>
         </a-form-item>
         <a-form-item>
@@ -80,7 +87,10 @@
             </template>
           </template>
           <template v-else-if="column.dataIndex === 'operation'">
-            <div class="tw-flex tw-items-center">
+            <div
+              class="tw-flex tw-items-center"
+              v-if="record.role !== 'SUPER_ADMIN'"
+            >
               <span
                 class="tw-cursor-pointer tw-p-2"
                 @click="updateUserStatus(record)"
@@ -108,7 +118,7 @@
       cancelText="取消"
       :maskClosable="false"
       @ok="onPutUser"
-      :after-close="resetUserFields"
+      :afterClose="resetUserFields"
     >
       <a-form :labelCol="{ span: 6 }">
         <a-form-item label="学校" v-bind="validateInfos.schoolId">
@@ -117,11 +127,11 @@
             show-search
             :disabled="!!userInfo.id"
             :filterOption="false"
-            @search="querySchoolList"
+            @search="(name:string) => querySchoolList(name,'form')"
             placeholder="学校名称"
           >
             <a-select-option
-              v-for="school in schoolTableData.result"
+              v-for="school in userInfo.schoolTableData.result"
               :key="school.id"
               :value="school.id"
               >{{ school.name }}</a-select-option
@@ -129,22 +139,37 @@
           </a-select>
         </a-form-item>
         <a-form-item label="姓名" v-bind="validateInfos.name">
-          <a-input v-model:value="userInfo.name"></a-input>
+          <a-input
+            v-model:value="userInfo.name"
+            placeholder="请输入姓名"
+          ></a-input>
         </a-form-item>
         <a-form-item label="登录名" v-bind="validateInfos.loginName">
           <a-input
             :disabled="!!userInfo.id"
             v-model:value="userInfo.loginName"
+            maxlength="11"
             placeholder="请输入登录手机号"
           ></a-input>
         </a-form-item>
-        <a-form-item label="密码" v-bind="validateInfos.passwd">
-          <a-input v-model:value="userInfo.passwd"></a-input>
+        <a-form-item
+          v-if="!userInfo.id"
+          label="密码"
+          v-bind="validateInfos.passwd"
+        >
+          <a-input-password
+            v-model:value="userInfo.passwd"
+            placeholder="请输入密码"
+          ></a-input-password>
         </a-form-item>
         <a-form-item label="角色" v-bind="validateInfos.role">
-          <a-select v-model:value="userInfo.role">
-            <a-select-option value="SCHOOL_ADMIN">学校管理员</a-select-option>
-            <a-select-option value="SECTION_LEADER">科组长</a-select-option>
+          <a-select v-model:value="userInfo.role" placeholder="请选择角色">
+            <a-select-option value="SCHOOL_ADMIN">{{
+              ROLE.SCHOOL_ADMIN
+            }}</a-select-option>
+            <a-select-option value="SECTION_LEADER">{{
+              ROLE.SECTION_LEADER
+            }}</a-select-option>
           </a-select>
         </a-form-item>
         <a-form-item
@@ -152,7 +177,10 @@
           label="所属科目"
           v-bind="validateInfos.course"
         >
-          <a-textarea v-model:value="userInfo.course"></a-textarea>
+          <a-textarea
+            v-model:value="userInfo.course"
+            placeholder="请填写所属科目代码, 以','分隔;"
+          ></a-textarea>
         </a-form-item>
       </a-form>
     </a-modal>
@@ -164,39 +192,59 @@
       cancelText="取消"
       :maskClosable="false"
       @ok="onUpdateUserPwd"
-      :after-close="clearFileList"
+      :afterClose="resetPwdFields"
     >
       <a-form :labelCol="{ span: 3 }">
         <a-form-item label="密码" v-bind="validatePwdInfos.passwd">
-          <a-input v-model:value="resetPwd.passwd"></a-input>
+          <a-input-password v-model:value="resetPwd.passwd"></a-input-password>
         </a-form-item>
       </a-form>
     </a-modal>
 
     <a-modal
       v-model:visible="showImportModal"
-      title="导入用户"
-      :footer="false"
       :maskClosable="false"
-      :after-close="resetPwdFields"
+      title="导入用户"
+      okText="确认上传"
+      cancelText="取消"
+      @ok="onImportUserList"
+      :afterClose="resetImportFields"
     >
-      <a-upload
-        :file-list="fileList"
-        :before-upload="beforeUpload"
-        @remove="handleRemove"
-        :max-count="1"
-        type="primary"
-      >
-        <a-button>
-          <upload-outlined></upload-outlined>
-          选择文件
-        </a-button>
-      </a-upload>
-      <div class="operation-group">
-        <a-button type="primary" @click="downloadTemplate">下载模板</a-button>
-        <a-button type="primary" @click="clearFileList">清空上传文件</a-button>
-        <a-button type="primary" @click="onImportUserList">确认上传</a-button>
-      </div>
+      <a-form :labelCol="{ span: 6 }">
+        <a-form-item label="学校名称" v-bind="validateImportInfos.schoolId">
+          <a-select
+            v-model:value="importUserForm.schoolId"
+            show-search
+            :filterOption="false"
+            @search="(name: string) => querySchoolList(name,'import')"
+            placeholder="学校名称"
+          >
+            <a-select-option
+              v-for="school in importUserForm.schoolTableData.result"
+              :key="school.id"
+              :value="school.id"
+              >{{ school.name }}</a-select-option
+            >
+          </a-select>
+        </a-form-item>
+        <a-form-item label="用户导入文件" v-bind="validateImportInfos.fileList">
+          <a-upload
+            :file-list="importUserForm.fileList"
+            :before-upload="beforeUpload"
+            @remove="handleRemove"
+            :max-count="1"
+            type="primary"
+          >
+            <a-button>
+              <upload-outlined></upload-outlined>
+              选择文件
+            </a-button>
+            <a class="tw-ml-4 tw-align-bottom" @click.stop="downloadTemplate">
+              下载导入模板
+            </a>
+          </a-upload>
+        </a-form-item>
+      </a-form>
     </a-modal>
   </div>
 </template>
@@ -223,6 +271,8 @@ import { Form } from "ant-design-vue";
 import { getSchoolListHttp } from "@/apis/school";
 import type { UploadProps, TableColumnType } from "ant-design-vue";
 import { useMainStore } from "@/store/main";
+import { throttle } from "lodash-es";
+import { ROLE } from "@/constants/dicts";
 
 const mainStore = useMainStore();
 
@@ -230,17 +280,36 @@ const showModal = ref(false);
 const showResetPwdModal = ref(false);
 const showImportModal = ref(false);
 
-const fileList = ref<UploadProps["fileList"]>([]);
+const importUserForm = reactive<{
+  schoolId: string;
+  fileList: UploadProps["fileList"];
+  schoolTableData: MultiplePageData<SchoolListInfo>;
+}>({
+  schoolId: "",
+  fileList: [],
+  schoolTableData: { totalCount: 0, result: [] },
+});
+
+const importRules = {
+  schoolId: [{ required: true, message: "请选择学校" }],
+  fileList: [
+    { required: true, type: "array", len: 1, message: "请选择导入文件" },
+  ],
+};
 
-const userInfo = ref<EditUserInfo>({
+const userInfo = reactive<
+  EditUserInfo & { schoolTableData: MultiplePageData<SchoolListInfo> }
+>({
   schoolId: "",
   name: "",
   loginName: "",
-  role: void 0,
   course: "",
+  role: void 0,
+  id: void 0,
+  schoolTableData: { totalCount: 0, result: [] },
 });
 
-const resetPwd = ref({
+const resetPwd = reactive({
   passwd: "",
   userId: "",
 });
@@ -276,12 +345,19 @@ const {
   validate,
   validateInfos,
   resetFields: resetUserFields,
-} = Form.useForm(userInfo.value, userRules);
+} = Form.useForm(userInfo, userRules);
+
 const {
   validate: validatePwd,
   validateInfos: validatePwdInfos,
   resetFields: resetPwdFields,
-} = Form.useForm(resetPwd.value, pwdRules);
+} = Form.useForm(resetPwd, pwdRules);
+
+const {
+  validate: validateImport,
+  validateInfos: validateImportInfos,
+  resetFields: resetImportFields,
+} = Form.useForm(importUserForm, importRules);
 
 /** 请求参数 */
 const query = reactive<FetchUserListQuery>({
@@ -318,22 +394,41 @@ const schoolTableData = reactive<MultiplePageData<SchoolListInfo>>({
 });
 
 /** 查询学校列表 */
-const querySchoolList = async (name?: string) => {
-  try {
-    const { result = [], totalCount } = await getSchoolListHttp({
-      name,
-      pageNumber: 1,
-      pageSize: 10,
-    });
-    Object.assign(schoolTableData, { result, totalCount });
-  } catch (error) {
-    console.error(error);
-  }
-};
+const querySchoolList = throttle(
+  async (name: string = "", type: "list" | "form" | "import" = "list") => {
+    const isList = type === "list";
+    const isForm = type === "form";
+    try {
+      const { result = [], totalCount } = await getSchoolListHttp({
+        name,
+        pageNumber: 1,
+        pageSize: 10,
+      });
+      Object.assign(
+        isList
+          ? schoolTableData
+          : isForm
+          ? userInfo.schoolTableData
+          : importUserForm.schoolTableData,
+        {
+          result,
+          totalCount,
+        }
+      );
+    } catch (error) {
+      return Promise.reject(error);
+    }
+  },
+  100
+);
 
 /** 显示新增用户弹窗 */
 const toggleAddUserModal = (show: boolean = true) => {
   showModal.value = show;
+  if (show) {
+    Object.assign(userInfo, { schoolId: mainStore.systemUserInfo?.schoolId });
+    querySchoolList("", "form");
+  }
 };
 
 /** 查询用户列表 */
@@ -357,17 +452,20 @@ const updateUserStatus = (record: UserInfo) => {
 
 /** 编辑用户 */
 const onEdit = (record: UserInfo) => {
-  Object.assign(userInfo.value, {
-    ...record,
+  Object.assign(userInfo, {
     course: record.courseCodes?.join(","),
-    role: `${record.roleId}` === "2" ? "SCHOOL_ADMIN" : "SECTION_LEADER",
+    id: record.id,
+    schoolId: record.schoolId,
+    name: record.name,
+    loginName: record.loginName,
+    role: record.role,
   });
   toggleAddUserModal(true);
 };
 
 /** 重置密码 */
 const onResetPwd = (record: UserInfo) => {
-  Object.assign(resetPwd.value, { passwd: "", userId: `${record.id}` });
+  Object.assign(resetPwd, { passwd: "", userId: `${record.id}` });
   showResetPwdModal.value = true;
 };
 
@@ -375,10 +473,15 @@ const onResetPwd = (record: UserInfo) => {
 const onPutUser = () => {
   validate().then((valid) => {
     if (valid) {
-      editUserInfoHttp(userInfo.value).then(() => {
-        message.success(`${userInfo.value.id ? "修改" : "添加"}成功`);
-        toggleAddUserModal(false);
+      const { role, course, schoolTableData, ...info } = userInfo;
+      editUserInfoHttp({
+        ...info,
+        role,
+        course: role === "SECTION_LEADER" ? course : "",
+      }).then(() => {
+        message.success(`${userInfo.id ? "修改" : "添加"}成功`);
         queryUserList();
+        toggleAddUserModal(false);
       });
     }
   });
@@ -386,49 +489,57 @@ const onPutUser = () => {
 
 /** 导入用户 */
 const importUserList = () => {
-  if (!query.schoolId) {
-    return message.error("请选择学校");
-  }
   showImportModal.value = true;
+  Object.assign(importUserForm, {
+    schoolId: mainStore.systemUserInfo?.schoolId,
+  });
+  querySchoolList("", "import");
 };
 
 const handleRemove: UploadProps["onRemove"] = (file) => {
-  const index = fileList.value!.indexOf(file);
-  const newFileList = fileList.value!.slice();
+  const index = importUserForm.fileList!.indexOf(file);
+  const newFileList = importUserForm.fileList!.slice();
   newFileList.splice(index, 1);
-  fileList.value = newFileList;
+  importUserForm.fileList = newFileList;
 };
 
 const beforeUpload: UploadProps["beforeUpload"] = (file) => {
-  fileList.value = [file];
+  importUserForm.fileList = [file];
   return false;
 };
 
-const clearFileList = () => {
-  fileList.value = [];
-};
-
-const downloadTemplate = () => {
-  downloadImportUserHttp();
+/** 下载导入模板 */
+const downloadTemplate = async () => {
+  try {
+    await downloadImportUserHttp();
+  } catch (error) {
+    return Promise.reject(error);
+  }
 };
 
-const onImportUserList = () => {
-  if (!query.schoolId) {
-    return;
+const onImportUserList = async () => {
+  try {
+    const valid = await validateImport();
+    if (valid) {
+      const formData = new FormData();
+      formData.append("schoolId", `${importUserForm.schoolId}`);
+      importUserForm.fileList?.forEach((file: any) => {
+        formData.append("file", file);
+      });
+      await importUserHttp(formData);
+      queryUserList();
+      showImportModal.value = false;
+    }
+  } catch (error) {
+    return Promise.reject(error);
   }
-  const formData = new FormData();
-  formData.append("schoolId", `${query.schoolId}`);
-  fileList.value?.forEach((file: any) => {
-    formData.append("file", file);
-  });
-  importUserHttp(formData);
 };
 
 /** 修改密码 */
 const onUpdateUserPwd = () => {
   validatePwd().then((valid) => {
     if (valid) {
-      resetUserPwdHttp(resetPwd.value).then(() => {
+      resetUserPwdHttp(resetPwd).then(() => {
         message.success(`重置成功`);
         showResetPwdModal.value = false;
       });

+ 4 - 0
types/project.d.ts

@@ -127,6 +127,10 @@ interface UserInfo extends Required<BaseUserInfo> {
   /** 学校名称 */
   schoolName: string;
   updateTime: string;
+  /** 角色CODE */
+  role: string,
+  /** 角色名称 */
+  roleName: string
 }
 
 type EditUserInfo = Omit<BaseUserInfo, "roleId"> & {