Selaa lähdekoodia

feat: 文件上传类型校验

chenhao 2 vuotta sitten
vanhempi
commit
77730e98f0

+ 1 - 0
components.d.ts

@@ -25,6 +25,7 @@ declare module '@vue/runtime-core' {
     ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
     ATable: typeof import('ant-design-vue/es')['Table']
     ATextarea: typeof import('ant-design-vue/es')['Textarea']
+    ATooltip: typeof import('ant-design-vue/es')['Tooltip']
     AUpload: typeof import('ant-design-vue/es')['Upload']
     Block: typeof import('./src/components/block/index.vue')['default']
     Button: typeof import('./src/components/button/index.vue')['default']

+ 5 - 5
postcss.config.cjs

@@ -5,11 +5,11 @@ module.exports = {
     "postcss-pxtorem": {
       rootValue: 12.8,
       unitPrecision: 5,
-      propList: ['*'],
-      selectorBlackList: ['ignore-rem'],
+      propList: ["*"],
+      selectorBlackList: [],
       replace: true,
       mediaQuery: false,
-      minPixelValue: 0
-    }
+      minPixelValue: 0,
+    },
   },
-}
+};

+ 1 - 2
src/assets/less/antd.table.less

@@ -3,7 +3,7 @@
   .ant-table-cell {
     box-sizing: border-box;
     height: 34px;
-    padding: 0;
+    padding: 4px 8px;
   }
   .ant-table-container {
     .ant-table-thead {
@@ -28,7 +28,6 @@
           border: none;
           font-weight: bold;
           color: @font-color;
-          padding: 6px 15px;
           &:first-child {
             border-radius: @border-radius-base 0 0 @border-radius-base;
           }

+ 12 - 12
src/pages/school-manage/index.vue

@@ -94,7 +94,7 @@
     >
       <a-form :labelCol="{ span: 6 }">
         <a-form-item label="学校编码" v-bind="validateInfos.code">
-          <a-input v-model:value="schoolInfo.code"></a-input>
+          <a-input :disabled="schoolInfo.id" v-model:value="schoolInfo.code"></a-input>
         </a-form-item>
         <a-form-item label="学校名称" v-bind="validateInfos.name">
           <a-input v-model:value="schoolInfo.name"></a-input>
@@ -154,10 +154,10 @@ const schoolInfo = reactive<BaseSchoolInfo>({
 const schoolRules = {
   code: [{ required: true, message: "请填写学校编码" }],
   name: [{ required: true, message: "请填写学校名称" }],
-  contacts: [{ required: true, message: "请填写负责人" }],
-  region: [{ required: true, message: "请填写学校地区" }],
+  // contacts: [{ required: true, message: "请填写负责人" }],
+  // region: [{ required: true, message: "请填写学校地区" }],
   telephone: [
-    { required: true, message: "请填写联系方式" },
+    // { required: true, message: "请填写联系方式" },
     { pattern: /\d{11}/, message: "请填写正确联系方式" },
   ],
 };
@@ -176,15 +176,15 @@ const query = reactive<FetchSchoolListQuery>({
 
 /** table配置 */
 const columns: TableColumnType[] = [
-  { title: "序号", dataIndex: "index", align: "center" },
-  { title: "学校ID", dataIndex: "id" },
+  { title: "序号", dataIndex: "index", align: "center", width: 60 },
+  { title: "学校ID", dataIndex: "id", width: 80, ellipsis: true },
   { title: "学校名称", dataIndex: "name", ellipsis: true },
-  { title: "地区", dataIndex: "region",maxWidth: 300 },
-  { title: "状态", dataIndex: "enable", align: "center" },
-  { title: "负责人", dataIndex: "contacts", ellipsis: true },
-  { title: "联系方式", dataIndex: "telephone" },
-  { title: "更新时间", dataIndex: "updateTime", ellipsis: true },
-  { title: "操作", dataIndex: "operation" },
+  { title: "地区", dataIndex: "region", ellipsis: true },
+  { title: "状态", dataIndex: "enable", align: "center", width: 60 },
+  { title: "负责人", dataIndex: "contacts", width: 120, ellipsis: true },
+  { title: "联系方式", dataIndex: "telephone", width: 140, ellipsis: true },
+  { title: "更新时间", dataIndex: "updateTime", width: 200, ellipsis: true },
+  { title: "操作", dataIndex: "operation", width: 220 },
 ];
 
 /** 学校列表信息 */

+ 21 - 11
src/pages/subjects-manage/index.vue

@@ -106,11 +106,16 @@
           </template>
           <template v-else-if="column.dataIndex === 'courseName'">
             {{ text }}
-            <img
-              v-if="!record.groupFinish"
-              class="star-icon"
-              src="@imgs/common/star-icon.png"
-            />
+            <a-tooltip placement="right">
+              <template #title>
+                <span>主管分未完成分组</span>
+              </template>
+              <img
+                v-if="!record.groupFinish"
+                class="star-icon"
+                src="@imgs/common/star-icon.png"
+              />
+            </a-tooltip>
           </template>
           <template v-else-if="column.dataIndex === 'enable'">
             <template v-if="record.enable">
@@ -202,7 +207,7 @@ import {
 } from "@ant-design/icons-vue";
 
 import Block from "@/components/block/index.vue";
-import type { TableColumnType, UploadProps } from "ant-design-vue";
+import { message, TableColumnType, UploadProps } from "ant-design-vue";
 import { Form } from "ant-design-vue";
 import {
   getSubjectsListHttp,
@@ -216,6 +221,7 @@ import { getSchoolListHttp } from "@/apis/school";
 import { getExamListHttp } from "@/apis/exam";
 import { useMainStore } from "@/store/main";
 import { throttle } from "lodash-es";
+import { fileTypeCheck } from "@/utils/file-type";
 
 type ImportType = "subject" | "struct";
 
@@ -245,7 +251,7 @@ const ImportDownloadApi: Record<
 /** 导入参数 */
 const uploadQuery = reactive<{
   type: ImportType;
-  schoolId?: string| number;
+  schoolId?: string | number;
   examId?: string;
   fileList: UploadProps["fileList"];
   schoolTableData: MultiplePageData<SchoolListInfo>;
@@ -430,7 +436,11 @@ const handleRemove: UploadProps["onRemove"] = (file) => {
   uploadQuery.fileList = newFileList;
 };
 
-const beforeUpload: UploadProps["beforeUpload"] = (file) => {
+const beforeUpload: UploadProps["beforeUpload"] = async (file) => {
+  await fileTypeCheck(file, ["xls", "xlsx"]).catch((error) => {
+    message.error("文件类型错误, 请使用导入模板编辑");
+    return Promise.reject(error);
+  });
   uploadQuery.fileList = [file];
   return false;
 };
@@ -447,8 +457,8 @@ const downloadTemplate = async () => {
 /** 显示导入弹窗 */
 const showImportModalType = async (type: ImportType) => {
   uploadQuery.type = type;
-  uploadQuery.schoolId = query.schoolId || mainStore.systemUserInfo?.schoolId
-  showImportModal.value = true
+  uploadQuery.schoolId = query.schoolId || mainStore.systemUserInfo?.schoolId;
+  showImportModal.value = true;
   querySchoolList("", "form");
 };
 
@@ -458,7 +468,7 @@ const onImport = async () => {
     const valid = await validate();
     if (valid) {
       const formData = new FormData();
-      formData.append("examId", uploadQuery.examId || '');
+      formData.append("examId", uploadQuery.examId || "");
       uploadQuery.fileList?.forEach((file: any) => {
         formData.append("file", file);
       });

+ 7 - 2
src/pages/user-manage/index.vue

@@ -21,7 +21,7 @@
         <a-form-item label="登录名">
           <a-input
             v-model:value="query.loginName"
-            placeholder="登录手机号"
+            placeholder="登录"
           ></a-input>
         </a-form-item>
         <a-form-item label="角色">
@@ -277,6 +277,7 @@ import type { UploadProps, TableColumnType } from "ant-design-vue";
 import { useMainStore } from "@/store/main";
 import { throttle } from "lodash-es";
 import { ROLE } from "@/constants/dicts";
+import { fileTypeCheck } from "@/utils/file-type";
 
 const mainStore = useMainStore();
 
@@ -537,7 +538,11 @@ const handleRemove: UploadProps["onRemove"] = (file) => {
   importUserForm.fileList = newFileList;
 };
 
-const beforeUpload: UploadProps["beforeUpload"] = (file) => {
+const beforeUpload: UploadProps["beforeUpload"] = async (file) => {
+  await fileTypeCheck(file,['xls','xlsx']).catch((error)=>{
+    message.error('文件类型错误, 请使用导入模板编辑')
+    return Promise.reject(error)
+  })
   importUserForm.fileList = [file];
   return false;
 };

+ 9 - 7
src/plugins/request.ts

@@ -63,6 +63,15 @@ request.interceptors.response.use(
   },
   async (error: AxiosError<any>) => {
     if (error.isAxiosError && !error.config.noToast) {
+      if (
+        error.response &&
+        ([401, 403].includes(error.response.status) ||
+          error.response.data?.code?.toString()?.startsWith("401"))
+      ) {
+        router.push({ name: "login" });
+        message.error("登录状态已过期");
+        return Promise.reject(error);
+      }
       let msg = "";
       if (error.config?.responseType === "blob") {
         try {
@@ -83,13 +92,6 @@ request.interceptors.response.use(
     } else if (!error.isAxiosError) {
       error.message && message.error(error.message);
     }
-    if (
-      error.response &&
-      ([401, 403].includes(error.response.status) ||
-        error.response.data?.code?.toString()?.startsWith("401"))
-    ) {
-      router.push({ name: "login" });
-    }
     return Promise.reject(error);
   }
 );

+ 39 - 0
src/utils/file-type.ts

@@ -0,0 +1,39 @@
+const mimeTypeMap: Record<string, string[]> = {
+  "504B0304": ["xls", "xlsx"],
+};
+
+export function fileTypeCheck(file: File, validFileTypes?: string[]) {
+  if (!file?.type || !validFileTypes?.slice) {
+    return Promise.reject();
+  }
+
+  const suffix = file.name.split(".").pop() || "";
+
+  if (!validFileTypes?.includes(suffix)) {
+    return Promise.reject();
+  }
+
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onloadend = (evt) => {
+      if (evt.target?.readyState === FileReader.DONE) {
+        const uint = new Uint8Array(evt.target.result as ArrayBuffer);
+        const bytes: string[] = [];
+        uint.forEach((byte) => bytes.push(byte.toString(16).padStart(2, "0")));
+        const hex = bytes.join("").toUpperCase();
+        
+        const mimeTypes = mimeTypeMap[hex];
+
+        const find = validFileTypes.find((t) => mimeTypes.includes(t));
+
+        if (find) {
+          return resolve(find);
+        }
+
+        return reject(mimeTypes)
+      }
+    };
+    const blob = file.slice(0, 4);
+    reader.readAsArrayBuffer(blob);
+  });
+}