Переглянути джерело

feat: 添加文件上传示例

xiaoxian521 1 рік тому
батько
коміт
0887dd46d5

+ 1 - 0
locales/en.yaml

@@ -64,6 +64,7 @@ menus:
   hsStatistic: Statistic
   hsCollapse: Collapse
   hsProgress: Progress
+  hsUpload: File Upload
   hsmenus: MultiLevel Menu
   hsmenu1: Menu1
   hsmenu1-1: Menu1-1

+ 1 - 0
locales/zh-CN.yaml

@@ -64,6 +64,7 @@ menus:
   hsStatistic: 统计组件
   hsCollapse: 折叠面板
   hsProgress: 进度条
+  hsUpload: 文件上传
   hsmenus: 多级菜单
   hsmenu1: 菜单1
   hsmenu1-1: 菜单1-1

+ 8 - 8
package.json

@@ -54,8 +54,8 @@
     "@logicflow/extension": "^1.2.19",
     "@pureadmin/descriptions": "^1.2.0",
     "@pureadmin/table": "^3.0.1",
-    "@pureadmin/utils": "^2.4.0",
-    "@vueuse/core": "^10.7.1",
+    "@pureadmin/utils": "^2.4.3",
+    "@vueuse/core": "^10.7.2",
     "@vueuse/motion": "^2.0.0",
     "@wangeditor/editor": "^5.1.23",
     "@wangeditor/editor-for-vue": "^5.1.12",
@@ -77,20 +77,20 @@
     "nprogress": "^0.2.0",
     "path": "^0.12.7",
     "pinia": "^2.1.7",
-    "pinyin-pro": "^3.19.0",
+    "pinyin-pro": "^3.19.3",
     "qrcode": "^1.5.3",
     "qs": "^6.11.2",
     "responsive-storage": "^2.2.0",
-    "sortablejs": "^1.15.1",
+    "sortablejs": "^1.15.2",
     "swiper": "^11.0.5",
     "typeit": "8.7.1",
     "v-contextmenu": "3.0.0",
     "v3-infinite-loading": "^1.3.1",
     "version-rocket": "^1.7.1",
-    "vue": "^3.4.10",
+    "vue": "3.4.14",
     "vue-i18n": "^9.9.0",
     "vue-json-pretty": "^2.3.0",
-    "vue-pdf-embed": "^1.2.1",
+    "vue-pdf-embed": "1.2.1",
     "vue-router": "^4.2.5",
     "vue-tippy": "^6.4.1",
     "vue-types": "^5.1.1",
@@ -153,10 +153,10 @@
     "svgo": "^3.2.0",
     "tailwindcss": "^3.4.1",
     "typescript": "^5.3.3",
-    "vite": "^5.0.11",
+    "vite": "^5.0.12",
     "vite-plugin-cdn-import": "^0.3.5",
     "vite-plugin-compression": "^0.5.1",
-    "vite-plugin-fake-server": "2.0.0",
+    "vite-plugin-fake-server": "^2.1.1",
     "vite-plugin-remove-console": "^2.2.0",
     "vite-plugin-router-warn": "^1.0.0",
     "vite-svg-loader": "^5.1.0",

Різницю між файлами не показано, бо вона завелика
+ 237 - 192
pnpm-lock.yaml


+ 14 - 0
src/api/mock.ts

@@ -9,3 +9,17 @@ type Result = {
 export const mapJson = (params?: object) => {
   return http.request<Result>("get", "/get-map-info", { params });
 };
+
+/** 文件上传 */
+export const formUpload = data => {
+  return http.request<Result>(
+    "post",
+    "https://run.mocky.io/v3/3aa761d7-b0b3-4a03-96b3-6168d4f7467b",
+    { data },
+    {
+      headers: {
+        "Content-Type": "multipart/form-data"
+      }
+    }
+  );
+};

+ 9 - 0
src/router/modules/components.ts

@@ -26,6 +26,15 @@ export default {
         title: $t("menus.hsmessage")
       }
     },
+    {
+      path: "/components/upload",
+      name: "PureUpload",
+      component: () => import("@/views/components/upload/index.vue"),
+      meta: {
+        title: $t("menus.hsUpload"),
+        extraIcon: "IF-pure-iconfont-new svg"
+      }
+    },
     {
       path: "/components/date-picker",
       name: "DatePicker",

+ 0 - 25
src/style/element-plus.scss

@@ -7,35 +7,10 @@
   font-weight: 400 !important;
 }
 
-.el-upload {
-  input[type="file"] {
-    display: none !important;
-  }
-}
-
-.el-upload__input {
-  display: none;
-}
-
-.upload-container {
-  .el-upload {
-    width: 100%;
-
-    .el-upload-dragger {
-      width: 100%;
-      height: 200px;
-    }
-  }
-}
-
 .el-dropdown-menu {
   padding: 0 !important;
 }
 
-.el-range-separator {
-  box-sizing: content-box;
-}
-
 .is-dark {
   z-index: 9999 !important;
 }

+ 1 - 1
src/views/components/datetime-picker.vue

@@ -198,7 +198,7 @@ watch(size, val =>
       @change="value1 = ''"
     >
       <el-radio label="">Date</el-radio>
-      <el-radio label="YYYY-MM-DD h:m:s a">年月日 时分秒</el-radio>
+      <el-radio label="YYYY-MM-DD HH:mm:ss">年月日 时分秒</el-radio>
       <el-radio label="x">时间戳</el-radio>
     </el-radio-group>
     <br />

+ 94 - 0
src/views/components/upload/form.vue

@@ -0,0 +1,94 @@
+<script lang="ts" setup>
+import { reactive, ref } from "vue";
+import { formUpload } from "@/api/mock";
+import { message } from "@/utils/message";
+import { createFormData } from "@pureadmin/utils";
+
+import UploadIcon from "@iconify-icons/ri/upload-2-line";
+
+const formRef = ref();
+const uploadRef = ref();
+const validateForm = reactive({
+  fileList: [],
+  date: ""
+});
+
+const submitForm = formEl => {
+  if (!formEl) return;
+  formEl.validate(valid => {
+    if (valid) {
+      // 多个 file 在一个接口同时上传
+      const formData = createFormData({
+        files: validateForm.fileList.map(file => ({ raw: file.raw })), // file 文件
+        date: validateForm.date // 别的字段
+      });
+      formUpload(formData)
+        .then(({ success }) => {
+          if (success) {
+            message("提交成功", { type: "success" });
+          } else {
+            message("提交失败");
+          }
+        })
+        .catch(error => {
+          message(`提交异常 ${error}`, { type: "error" });
+        });
+    } else {
+      return false;
+    }
+  });
+};
+
+const resetForm = formEl => {
+  if (!formEl) return;
+  formEl.resetFields();
+};
+</script>
+
+<template>
+  <el-form ref="formRef" :model="validateForm" label-width="82px">
+    <el-form-item
+      label="附件"
+      prop="fileList"
+      :rules="[{ required: true, message: '附件不能为空' }]"
+    >
+      <el-upload
+        ref="uploadRef"
+        v-model:file-list="validateForm.fileList"
+        drag
+        multiple
+        action="#"
+        class="!w-[200px]"
+        :auto-upload="false"
+      >
+        <div class="el-upload__text">
+          <IconifyIconOffline
+            :icon="UploadIcon"
+            width="26"
+            class="m-auto mb-2"
+          />
+          可点击或拖拽上传
+        </div>
+      </el-upload>
+    </el-form-item>
+    <el-form-item
+      label="日期"
+      prop="date"
+      :rules="[{ required: true, message: '日期不能为空' }]"
+    >
+      <el-date-picker
+        v-model="validateForm.date"
+        type="datetime"
+        class="!w-[200px]"
+        placeholder="请选择日期时间"
+        value-format="YYYY-MM-DD HH:mm:ss"
+      />
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" text bg @click="submitForm(formRef)">
+        提交
+      </el-button>
+      <el-button text bg @click="resetForm(formRef)">重置</el-button>
+    </el-form-item>
+  </el-form>
+</template>

BIN
src/views/components/upload/imgs/1.jpg


BIN
src/views/components/upload/imgs/2.jpg


BIN
src/views/components/upload/imgs/3.jpg


+ 313 - 0
src/views/components/upload/index.vue

@@ -0,0 +1,313 @@
+<script setup lang="ts">
+import axios from "axios";
+import Sortable from "sortablejs";
+import UploadForm from "./form.vue";
+import { ref, computed } from "vue";
+import { useRouter } from "vue-router";
+import { message } from "@/utils/message";
+import type { UploadFile } from "element-plus";
+import { getKeyList, extractFields, downloadByData } from "@pureadmin/utils";
+
+import Add from "@iconify-icons/ep/plus";
+import Eye from "@iconify-icons/ri/eye-line";
+import Delete from "@iconify-icons/ri/delete-bin-7-line";
+
+defineOptions({
+  name: "PureUpload"
+});
+
+const fileList = ref([]);
+const router = useRouter();
+const curOpenImgIndex = ref(0);
+const dialogVisible = ref(false);
+
+const urlList = computed(() => getKeyList(fileList.value, "url"));
+const imgInfos = computed(() => extractFields(fileList.value, "name", "size"));
+
+const getImgUrl = name => new URL(`./imgs/${name}.jpg`, import.meta.url).href;
+const srcList = Array.from({ length: 3 }).map((_, index) => {
+  return getImgUrl(index + 1);
+});
+
+/** 上传文件前校验 */
+const onBefore = file => {
+  if (!["image/jpeg", "image/png", "image/gif"].includes(file.type)) {
+    message("只能上传图片");
+    return false;
+  }
+  const isExceed = file.size / 1024 / 1024 > 2;
+  if (isExceed) {
+    message(`单个图片大小不能超过2MB`);
+    return false;
+  }
+};
+
+/** 超出最大上传数时触发 */
+const onExceed = () => {
+  message("最多上传3张图片,请先删除在上传");
+};
+
+/** 移除上传的文件 */
+const handleRemove = (file: UploadFile) => {
+  fileList.value.splice(fileList.value.indexOf(file), 1);
+};
+
+/** 大图预览 */
+const handlePictureCardPreview = (file: UploadFile) => {
+  curOpenImgIndex.value = fileList.value.findIndex(img => img.uid === file.uid);
+  dialogVisible.value = true;
+};
+
+const getUploadItem = () => document.querySelectorAll("#pure-upload-item");
+
+/** 缩略图拖拽排序 */
+const imgDrop = uid => {
+  const CLASSNAME = "el-upload-list";
+  const _curIndex = fileList.value.findIndex(img => img.uid === uid);
+  getUploadItem()?.[_curIndex]?.classList?.add(`${CLASSNAME}__item-actions`);
+  const wrapper: HTMLElement = document.querySelector(`.${CLASSNAME}`);
+  Sortable.create(wrapper, {
+    handle: `.${CLASSNAME}__item`,
+    onEnd: ({ newIndex, oldIndex }) => {
+      const oldFile = fileList.value[oldIndex];
+      fileList.value.splice(oldIndex, 1);
+      fileList.value.splice(newIndex, 0, oldFile);
+      // fix: https://github.com/SortableJS/Sortable/issues/232 (firefox is ok, but chromium is bad. see https://bugs.chromium.org/p/chromium/issues/detail?id=410328)
+      getUploadItem().forEach(ele => {
+        ele.classList.remove(`${CLASSNAME}__item-actions`);
+      });
+    }
+  });
+};
+
+/** 下载图片 */
+const onDownload = () => {
+  [
+    { name: "巴旦木.jpeg", type: "img" },
+    { name: "恭喜发财.png", type: "img" },
+    { name: "可爱动物.gif", type: "gif" },
+    { name: "pure-upload.csv", type: "other" },
+    { name: "pure-upload.txt", type: "other" }
+  ].forEach(img => {
+    axios
+      .get(`https://xiaoxian521.github.io/hyperlink/${img.type}/${img.name}`, {
+        responseType: "blob"
+      })
+      .then(({ data }) => {
+        downloadByData(data, img.name);
+      });
+  });
+};
+</script>
+
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <div class="card-header">
+        <el-link
+          v-tippy="{
+            content: '点击查看详细文档'
+          }"
+          href="https://element-plus.org/zh-CN/component/upload.html"
+          target="_blank"
+          style="font-size: 16px; font-weight: 800"
+        >
+          文件上传
+        </el-link>
+        <span class="header-right">
+          <el-button class="ml-1" text bg @click="onDownload">
+            点击下载安全文件进行上传测试
+          </el-button>
+        </span>
+      </div>
+    </template>
+
+    <p class="mb-4">
+      综合示例<span class="text-[14px]">
+        ( <span class="text-[red]">自动上传</span>
+        、拖拽上传、拖拽排序、设置请求头、上传进度、大图预览、多选文件、最大文件数量、文件类型限制、文件大小限制、删除文件)
+      </span>
+    </p>
+    <p v-show="fileList.length > 0" class="mb-4">
+      {{ imgInfos }}
+    </p>
+    <el-upload
+      v-model:file-list="fileList"
+      drag
+      multiple
+      class="pure-upload"
+      list-type="picture-card"
+      accept="image/jpeg,image/png,image/gif"
+      action="https://run.mocky.io/v3/3aa761d7-b0b3-4a03-96b3-6168d4f7467b"
+      :limit="3"
+      :headers="{ Authorization: 'eyJhbGciOiJIUzUxMiJ9.admin' }"
+      :on-exceed="onExceed"
+      :before-upload="onBefore"
+    >
+      <IconifyIconOffline :icon="Add" class="m-auto mt-4" width="30" />
+      <template #file="{ file }">
+        <div
+          v-if="file.status == 'ready' || file.status == 'uploading'"
+          class="mt-[35%] m-auto"
+        >
+          <p class="font-medium">文件上传中</p>
+          <el-progress
+            class="mt-2"
+            :stroke-width="2"
+            :text-inside="true"
+            :show-text="false"
+            :percentage="file.percentage"
+          />
+        </div>
+        <div v-else @mouseenter.stop="imgDrop(file.uid)">
+          <img
+            class="el-upload-list__item-thumbnail select-none"
+            :src="file.url"
+          />
+          <span
+            id="pure-upload-item"
+            :class="[
+              'el-upload-list__item-actions',
+              fileList.length > 1 && '!cursor-move'
+            ]"
+          >
+            <span
+              title="查看"
+              class="hover:text-primary"
+              @click="handlePictureCardPreview(file)"
+            >
+              <IconifyIconOffline
+                :icon="Eye"
+                class="hover:scale-125 duration-100"
+              />
+            </span>
+            <span
+              class="el-upload-list__item-delete"
+              @click="handleRemove(file)"
+            >
+              <span title="移除" class="hover:text-[var(--el-color-danger)]">
+                <IconifyIconOffline
+                  :icon="Delete"
+                  class="hover:scale-125 duration-100"
+                />
+              </span>
+            </span>
+          </span>
+        </div>
+      </template>
+    </el-upload>
+    <!-- 有时文档没写并不代表没有,多看源码好处多多😝 https://github.com/element-plus/element-plus/tree/dev/packages/components/image-viewer/src (emm...这让我想起刚开始写这个项目时,很多东西只有英文或者没有文档,需要看源码时,想笑🥹。那些美好时光都给这些坑了,giao) -->
+    <el-image-viewer
+      v-if="dialogVisible"
+      :initialIndex="curOpenImgIndex"
+      :url-list="urlList"
+      :zoom-rate="1.2"
+      :max-scale="7"
+      :min-scale="0.2"
+      @close="dialogVisible = false"
+      @switch="index => (curOpenImgIndex = index)"
+    />
+    <!-- 将自定义内容插入到body里,有了它在图片预览的时候,想插入个分页器或者别的东东在预览区某个位置就很方便咯(用户需求可以很灵活,开源组件库几乎不可能尽善尽美,很多时候寻找别的解决途径或许更好) -->
+    <teleport to="body">
+      <div
+        v-if="fileList[curOpenImgIndex] && dialogVisible"
+        effect="dark"
+        round
+        size="large"
+        type="info"
+        class="img-name"
+      >
+        <p class="text-[#fff] dark:text-black">
+          {{ fileList[curOpenImgIndex].name }}
+        </p>
+      </div>
+    </teleport>
+    <p class="el-upload__tip">
+      可拖拽上传最多3张单个不超过2MB且格式为jpeg/png/gif的图片
+    </p>
+    <el-divider />
+
+    <p class="mb-4 mt-4">
+      结合表单校验进行<span class="text-[red]">手动上传</span>
+      <span class="text-[14px]">
+        (可先打开浏览器控制台找到Network,然后填写表单内容后点击点提交观察请求变化)
+      </span>
+    </p>
+    <div class="flex justify-between">
+      <UploadForm />
+      <div>
+        <p class="text-center">上传接口相关截图</p>
+        <el-image
+          class="w-[200px] rounded-md"
+          :src="srcList[0]"
+          :preview-src-list="srcList"
+          fit="cover"
+        />
+      </div>
+    </div>
+    <el-divider />
+
+    <div class="flex flex-wrap">
+      <p>
+        裁剪、上传头像请参考
+        <span
+          class="font-bold text-[18x] cursor-pointer hover:text-[red]"
+          @click="router.push({ name: 'SystemUser' })"
+        >
+          系统管理-用户管理
+        </span>
+        表格操作栏中的上传头像功能
+      </p>
+      <p class="text-[red] text-[12px] flex flex-auto items-center justify-end">
+        免责声明:上传接口使用免费开源的
+        <el-link
+          href="https://designer.mocky.io/"
+          target="_blank"
+          style="font-size: 16px; font-weight: 800"
+        >
+          &nbsp;Mocky&nbsp;
+        </el-link>
+        <span class="font-bold text-[18x]"> 请不要上传重要信息 </span
+        >,如果造成任何损失,我们概不负责
+      </p>
+    </div>
+  </el-card>
+</template>
+
+<style lang="scss" scoped>
+:deep(.card-header) {
+  display: flex;
+
+  .header-right {
+    display: flex;
+    flex: auto;
+    align-items: center;
+    justify-content: flex-end;
+    font-size: 14px;
+  }
+}
+
+:deep(.pure-upload) {
+  .el-upload-dragger {
+    background-color: transparent;
+    border: none;
+  }
+}
+
+.img-name {
+  position: absolute;
+  bottom: 80px;
+  left: 50%;
+  z-index: 4000;
+  padding: 5px 23px;
+  background-color: var(--el-text-color-regular);
+  border-radius: 22px;
+  transform: translateX(-50%);
+
+  /** 将下面的 left: 50%; bottom: 80px; transform: translateX(-50%); 注释掉
+   *  解开下面 left: 40px; top: 40px; 注释,体验不一样的感觉。啊?还是差强人意,自己调整位置吧🥹
+   */
+  // left: 40px;
+  // top: 40px;
+}
+</style>

Деякі файли не було показано, через те що забагато файлів було змінено