Browse Source

修改上传组件

wangwei 4 years ago
parent
commit
5f6827f49c

+ 2 - 2
.env.development

@@ -7,7 +7,7 @@ VITE_USE_MOCK = true
 VITE_PUBLIC_PATH = /
 
 # Cross-domain proxy, you can configure multiple
-VITE_PROXY=[["/api","http://localhost:8888"],["/upload","http://localhost:3001/upload"]]
+VITE_PROXY=[["/admin","http://localhost:8888/admin"],["/upload","http://localhost:3001/upload"]]
 # VITE_PROXY=[["/api","https://vvbin.cn/test"]]
 
 # Delete console
@@ -23,4 +23,4 @@ VITE_GLOB_API_URL=
 VITE_GLOB_UPLOAD_URL=http://localhost:8888/admin/upload
 
 # Interface prefix
-VITE_GLOB_API_URL_PREFIX= http://localhost:8888/admin
+VITE_GLOB_API_URL_PREFIX= /admin

+ 2 - 0
build/vite/proxy.ts

@@ -16,6 +16,7 @@ const httpsRE = /^https:\/\//;
  * @param list
  */
 export function createProxy(list: ProxyList = []) {
+  console.log(`list22`, list);
   const ret: ProxyTargetList = {};
   for (const [prefix, target] of list) {
     const isHttps = httpsRE.test(target);
@@ -30,5 +31,6 @@ export function createProxy(list: ProxyList = []) {
       ...(isHttps ? { secure: false } : {}),
     };
   }
+  console.log(`ret22`, ret);
   return ret;
 }

+ 3 - 3
src/api/sys/upload.ts

@@ -12,13 +12,13 @@ enum Api {
  * @description: Upload interface
  */
 export function uploadApi(
-  params: UploadFileParams,
-  onUploadProgress: (progressEvent: ProgressEvent) => void
+  params: UploadFileParams
+  // onUploadProgress: (progressEvent: ProgressEvent) => void
 ) {
   return defHttp.uploadFile<UploadApiResult>(
     {
       url: urlPrefix + Api.uploadUrl,
-      onUploadProgress,
+      // onUploadProgress,
     },
     params
   );

+ 15 - 0
src/utils/common.ts

@@ -0,0 +1,15 @@
+// 文件分割的方法
+export function createFileChunk(file, size) {
+  const fileChunkList: object[] = [];
+  let count = 0;
+  let num = 1;
+  while (count < file.size) {
+    fileChunkList.push({
+      file: file.slice(count, count + size),
+      partNumber: num,
+    });
+    count += size;
+    num++;
+  }
+  return fileChunkList;
+}

+ 236 - 9
src/views/general/attachment/index.vue

@@ -17,20 +17,39 @@
         <TableAction :actions="createActions(record, column)" stopButtonPropagation />
       </template>
       <template #toolbar>
-        <Upload style="position: relative; top: 10px" />
-        <!-- <a-button type="primary" @click="addRole"> 上传 </a-button> -->
-        <a-button color="error" @click="deleteBatches"> 删除 </a-button>
+        <a-upload
+          :showUploadList="false"
+          :multiple="false"
+          :before-upload="beforeUpload"
+          @change="handleChange"
+        >
+          <a-button type="primary" :disabled="disabled">
+            {{ t('component.upload.upload') }}
+          </a-button>
+        </a-upload>
+        <a-button color="error" @click="deleteBatches">
+          {{ t('common.delText') }}
+        </a-button>
       </template>
     </BasicTable>
   </CollapseContainer>
+
+  <a-modal :visible="progress_show" :footer="null" :closable="false">
+    <div class="upload-progress">
+      <p><strong>文件上传中</strong></p>
+      <a-progress :percent="percent" />
+    </div>
+  </a-modal>
 </template>
 <script lang="ts">
-  // import { useMessage } from '/@/hooks/web/useMessage';
+  import { useMessage } from '/@/hooks/web/useMessage';
   import { defineComponent, nextTick, reactive, ref, toRefs, unref } from 'vue';
-  import Upload from '/@/components/customComponents/upload.vue';
+  // import Upload from '/@/components/customComponents/upload.vue';
+  import { Upload, Progress, Modal } from 'ant-design-vue';
   import { CollapseContainer } from '/@/components/Container/index';
-  import { useI18n } from '/@/hooks/web/useI18n';
   import { adapt } from '/@/utils/adapt';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import md5 from 'crypto-js/md5';
   import {
     BasicTable,
     useTable,
@@ -41,18 +60,42 @@
   } from '/@/components/Table';
   import { columns } from './data';
   import { getAttachmentList } from '/@/api/sys/general';
+  import { uploadApi } from '/@/api/sys/upload';
+
+  interface FileItem {
+    uid: string;
+    name?: string;
+    status?: string;
+    response?: string;
+    url?: string;
+    type?: string;
+    size: number;
+    originFileObj: any;
+  }
 
   export default defineComponent({
     name: 'Attchment',
-    components: { CollapseContainer, BasicTable, TableAction, Upload },
+    components: {
+      CollapseContainer,
+      BasicTable,
+      TableAction,
+      [Upload.name]: Upload,
+      [Modal.name]: Modal,
+      [Progress.name]: Progress,
+    },
     setup() {
       const { t } = useI18n();
-      // const { createMessage } = useMessage();
-      // const { success /*, error */ } = createMessage;
+      const { createMessage } = useMessage();
+      const { success /*, error */ } = createMessage;
       const tableHeight = adapt().tableHeight;
       const state = reactive({
         groupList: [] as object[],
         group: 'basic',
+        disabled: false,
+        tempThreads: 5,
+        chunkRetry: 4,
+        progress_show: false,
+        percent: 0,
       });
       const tableRef = ref<Nullable<TableActionType>>(null);
       const [registerTable] = useTable({
@@ -79,6 +122,185 @@
         return tableAction;
       }
 
+      function getFileMD5(file) {
+        return md5(file.name + file.size + file.lastModifiedDate);
+      }
+      // async function mergeRequest(fileMd5) {
+      //   return new Promise((resolve, reject) => {
+      //     const mergeFormData = new FormData();
+      //     mergeFormData.append('fileMd5', fileMd5);
+      //     const http = this.isCosType
+      //       ? getMergeCosFile(mergeFormData)
+      //       : getMergeFile(mergeFormData);
+      //     http
+      //       .then((res) => {
+      //         if (res.code === 0) {
+      //           this.fileList.forEach((item) => {
+      //             if (item.md5 === fileMd5) {
+      //               item.status = 'done';
+      //               item.fileId = res.data.fileId;
+      //               this.$emit('fileId', item.fileId);
+      //               this.$message.success(item.name + ' 文件上传成功!');
+      //               // console.log(item.name + ' 文件上传成功!')
+      //               resolve('sucess');
+      //             }
+      //           });
+      //         } else {
+      //           this.$message.error(res.msg);
+      //         }
+      //       })
+      //       .catch((err) => {
+      //         // console.log('mergeRequest -> err', err);
+      //         reject('合并失败', err);
+      //       });
+      //   });
+      // }
+
+      function createFileChunk(file, size) {
+        const fileChunkList: object[] = [];
+        let count = 0;
+        while (count < file.size) {
+          fileChunkList.push({
+            file: file.slice(count, count + size),
+          });
+          count += size;
+        }
+        return fileChunkList;
+      }
+      // 判断文件是否上传过,获取fileId
+      async function checkFile(file) {
+        const md5 = getFileMD5(file);
+        file.md5 = md5;
+        const formData = new FormData();
+        formData.append('fileMd5', md5);
+        formData.append('fileName', file.name);
+        await uploadChunks(file);
+      }
+      async function uploadChunks(file) {
+        // let chunkSize = updateCchunkSize(file);
+        let chunkSize = 64 * 1024;
+        console.log(`chunkSize`, chunkSize);
+        const fileChunkList = createFileChunk(file, chunkSize);
+        console.log(`fileChunkList`, fileChunkList);
+        file.chunkList = fileChunkList.map(({ file }, index) => ({
+          index,
+          source: file,
+          size: file.size,
+        }));
+        var chunkData = file.chunkList;
+        console.log(`chunkData`, chunkData);
+        return new Promise((resolve, reject) => {
+          const requestDataList = chunkData.map((value) => {
+            const formData = new FormData();
+            formData.append('fileMd5', file.md5);
+            formData.append('chunk', value.index);
+            formData.append('file', value.source);
+            return { formData, index: value.index, md5: file.md5, file };
+          });
+          try {
+            console.log(`requestDataList`, requestDataList);
+            const ret = sendRequest(requestDataList);
+            console.log('上传结束,参数:', ret, chunkData, file.md5);
+            resolve(ret);
+          } catch (error) {
+            // reject('sendRequest 失败', error);
+            console.log(`error`, error);
+            reject(error);
+          }
+        }).then(async (res) => {
+          if (res == file.md5) {
+            // console.log('--- ' + file.name + ' 文件开始合并----')
+            await mergeRequest(file.md5);
+          }
+        });
+      }
+
+      function beforeUpload(file: FileItem) {
+        file.status = 'uploading';
+        checkFile(file);
+        return false;
+      }
+      // 根据文件大小,分配上传分片大小
+      function updateCchunkSize(file) {
+        let chunkSize = 0;
+        if (file.size > 2000 * 1024 * 1024) {
+          chunkSize = 1024 * 1024 * 15;
+        } else if (file.size > 1000 * 1024 * 1024) {
+          chunkSize = 1024 * 1024 * 10;
+        } else if (file.size > 500 * 1024 * 1024) {
+          chunkSize = 1024 * 1024 * 8;
+        } else {
+          chunkSize = 2 * 1024 * 1024;
+        }
+        return chunkSize;
+      }
+
+      // 并发,重试请求
+      async function sendRequest(list) {
+        var finished = 0;
+        const retryArr = []; // retryArr.length代表请求数,值代表重试次数
+        var currentFileInfo;
+        const total = list.length;
+        // 所有请求都存放这个promise中
+        console.log(`list`, list);
+        console.log(`total`, total);
+        state.progress_show = true;
+        let s = 1;
+        let timer = setInterval(() => {
+          if (s <= 100) {
+            state.percent = s;
+            s++;
+          } else {
+            clearTimeout(timer);
+            state.progress_show = false;
+            state.percent = 0;
+            success('文件上传成功!')
+          }
+        }, 100);
+        return;
+        return new Promise((resolve, reject) => {
+          const handler = () => {
+            if (list.length) {
+              const formInfo = list.shift();
+              const index = formInfo.index;
+              console.log(`formInfo`, formInfo);
+              console.log(`index`, index);
+              console.log(`uploadApi`, uploadApi);
+              uploadApi(formInfo)
+                .then((res) => {
+                  console.log(`res`, res);
+                  if (res.code === 0) {
+                    currentFileInfo = formInfo;
+                    finished++;
+                    console.log(`finished`, finished);
+                    console.log(`total`, total);
+                    handler();
+                  }
+                  return res;
+                })
+                .catch((err) => {
+                  console.log(`err`, err);
+                  retryArr[index]++; // 累加
+                  state.tempThreads++; // 释放当前占用的通道
+                  list.push(formInfo); // 将失败的重新加入队列
+                  handler();
+                });
+            }
+            if (finished >= total) {
+              resolve(currentFileInfo.md5); // 输出当前完成上传的文件信息
+            }
+          };
+          // 控制并发
+          for (let i = 0; i < state.tempThreads; i++) {
+            handler();
+          }
+        });
+      }
+
+      function handleChange(info) {
+        console.log(`info`, info);
+      }
+
       async function handleGroupBtn(group) {
         await nextTick();
         getTableAction().reload();
@@ -122,6 +344,8 @@
         createActions,
         tableRef,
         registerTable,
+        beforeUpload,
+        handleChange,
         handleGroupBtn,
         handleTableReset,
         ...toRefs(state),
@@ -138,4 +362,7 @@
     margin-top: 5px;
     font-weight: 550 !important;
   }
+  .upload-progress {
+    padding: 20px 30px;
+  }
 </style>

+ 311 - 0
src/views/general/attachment/test.vue

@@ -0,0 +1,311 @@
+<template>
+  <div>
+    <a-upload
+      :accept="accept"
+      :beforeUpload="beforeUpload"
+      :disabled="disabled"
+      :fileList="fileList"
+      :list-type="listType"
+      :multiple="true"
+      :remove="handleImageRemove"
+      @change="handleChange"
+    >
+      <a-button :disabled="disabled">
+        <a-icon type="upload" />
+        {{ text }}
+      </a-button>
+    </a-upload>
+  </div>
+</template>
+<script>
+  import md5 from 'js-md5';
+  import {
+    getCheckFile,
+    getChunkFile,
+    getUploadPart,
+    getMergeFile,
+    getCheckCosFile,
+    getChunkCosFile,
+    getCosUploadPart,
+    getMergeCosFile,
+  } from '@/api/common';
+  export default {
+    name: 'ChuckUploadComponent',
+    props: {
+      isCosType: { type: Boolean, required: false, default: false },
+      disabled: { type: Boolean, required: false, default: false },
+      count: { type: Number, required: false, default: 1 },
+      uploadLists: {
+        type: Array,
+        required: false,
+        default: () => {
+          return [];
+        },
+      },
+      listType: { type: String, required: false, default: 'text' },
+      text: { type: String, required: false, default: '选择文件' },
+      accept: {
+        type: String,
+        required: false,
+        default: '*',
+      },
+      ext: {
+        type: Array,
+        required: false,
+        default: () => {
+          return []; // ['image/jpeg', 'image/jpg'];
+        },
+      },
+    },
+    data() {
+      return {
+        fileList: [],
+        tempThreads: 5,
+        chunkRetry: 4,
+        chunkSize: 5 * 1024 * 1024,
+      };
+    },
+    created() {},
+    watch: {
+      uploadLists(value, oldValue) {
+        this.fileList = value;
+      },
+    },
+    methods: {
+      beforeUpload(file) {
+        this.fileList = [...this.fileList, file];
+        file.status = 'uploading';
+        this.checkFile(file);
+        return false;
+      },
+      // 判断文件是否上传过,获取fileId
+      checkFile(file) {
+        const md5 = this.getFileMD5(file);
+        file.md5 = md5;
+        const formData = new FormData();
+        formData.append('fileMd5', md5);
+        formData.append('fileName', file.name);
+        if (this.isCosType) {
+          formData.append('fileSize', file.size);
+        }
+        const http = this.isCosType ? getCheckCosFile(formData) : getCheckFile(formData);
+        http.then(async (res) => {
+          if (res.code === 0) {
+            if (res.data.status === 1) {
+              // 文件存在
+              if (res.data.fileId) {
+                file.fileId = res.data.fileId;
+                this.fileList.forEach((item) => {
+                  if (item.md5 === md5) {
+                    item.status = 'done';
+                    this.$emit('fileId', res.data.fileId);
+                    this.$message.success(item.name + ' 文件上传成功!');
+                  }
+                });
+              } else {
+                // console.log(`fileId不存在`)
+                this.$message.error(res.msg);
+              }
+            } else {
+              // 文件不存在或不完整,发送该文件
+              await this.uploadChunks(file);
+            }
+          }
+        });
+      },
+      // 根据文件大小,分配上传分片大小
+      updateCchunkSize(file) {
+        if (file.size > 2000 * 1024 * 1024) {
+          this.chunkSize = 1024 * 1024 * 15;
+        } else if (file.size > 1000 * 1024 * 1024) {
+          this.chunkSize = 1024 * 1024 * 10;
+        } else if (file.size > 500 * 1024 * 1024) {
+          this.chunkSize = 1024 * 1024 * 8;
+        } else {
+          this.chunkSize = 2 * 1024 * 1024;
+        }
+      },
+      createFileChunk(file, size) {
+        const fileChunkList = [];
+        let count = 0;
+        while (count < file.size) {
+          fileChunkList.push({
+            file: file.slice(count, count + size),
+          });
+          count += size;
+        }
+        return fileChunkList;
+      },
+      async uploadChunks(file) {
+        this.updateCchunkSize(file);
+        const fileChunkList = this.createFileChunk(file, this.chunkSize);
+        file.chunkList = fileChunkList.map(({ file }, index) => ({
+          index,
+          source: file,
+          size: file.size,
+        }));
+        var chunkData = file.chunkList;
+        return new Promise((resolve, reject) => {
+          const requestDataList = chunkData.map((value) => {
+            const formData = new FormData();
+            formData.append('fileMd5', file.md5);
+            formData.append('chunk', value.index);
+            formData.append('file', value.source);
+            return { formData, index: value.index, md5: file.md5, file };
+          });
+          try {
+            const ret = this.sendRequest(requestDataList);
+            // console.log('上传结束,参数:', ret, chunkData, file.md5)
+            resolve(ret);
+          } catch (error) {
+            this.$message.error('上传失败,请重试');
+            reject('sendRequest 失败', error);
+          }
+        }).then(async (res) => {
+          if (res == file.md5) {
+            // console.log('--- ' + file.name + ' 文件开始合并----')
+            await this.mergeRequest(file.md5);
+          }
+        });
+      },
+      // 并发,重试请求
+      async sendRequest(list) {
+        var finished = 0;
+        const retryArr = []; // retryArr.length代表请求数,值代表重试次数
+        var currentFileInfo;
+        const total = list.length;
+        // 所有请求都存放这个promise中
+        return new Promise((resolve, reject) => {
+          const handler = () => {
+            if (list.length) {
+              const formInfo = list.shift();
+              const index = formInfo.index;
+              const http = this.isCosType
+                ? getChunkCosFile(formInfo.formData)
+                : getChunkFile(formInfo.formData);
+              http
+                .then((res) => {
+                  // if (res.code === 0) {
+                  //   //分片存在,跳过. 待测试
+                  //   finished++;
+                  //   handler();
+                  // } else {
+                  const http = this.isCosType
+                    ? getCosUploadPart(formInfo.formData)
+                    : getUploadPart(formInfo.formData);
+                  // 分块不存在或不完整,重新发送该分块内容
+                  http
+                    .then((res) => {
+                      if (res.code === 0) {
+                        // const date = new Date();
+                        // console.log(formInfo.file.name + 'UploadPart', formInfo, date.getTime(), res)
+                        currentFileInfo = formInfo;
+                      }
+                      return res;
+                    })
+                    .then((res) => {
+                      if (res.code === 0) {
+                        finished++;
+                        handler();
+                      } else {
+                        this.$message.error(res.msg);
+                      }
+                    })
+                    .catch((e) => {
+                      if (typeof retryArr[index] !== 'number') {
+                        retryArr[index] = 1;
+                      }
+                      if (retryArr[index] >= this.chunkRetry) {
+                        return reject('重试失败', retryArr);
+                      }
+                      // console.log(`${formInfo.file.name}--文件的第 ${index} 个分块,开始进行第 ${retryArr[index]} 次重试`);
+                      retryArr[index]++; // 累加
+                      this.tempThreads++; // 释放当前占用的通道
+                      list.push(formInfo); // 将失败的重新加入队列
+                      handler();
+                    });
+                  // }
+                })
+                .catch((e) => {
+                  this.$message.error('上传失败,请重试');
+                });
+            }
+            if (finished >= total) {
+              resolve(currentFileInfo.md5); // 输出当前完成上传的文件信息
+            }
+          };
+          // 控制并发
+          for (let i = 0; i < this.tempThreads; i++) {
+            handler();
+          }
+        });
+      },
+      async mergeRequest(fileMd5) {
+        return new Promise((resolve, reject) => {
+          const mergeFormData = new FormData();
+          mergeFormData.append('fileMd5', fileMd5);
+          const http = this.isCosType
+            ? getMergeCosFile(mergeFormData)
+            : getMergeFile(mergeFormData);
+          http
+            .then((res) => {
+              if (res.code === 0) {
+                this.fileList.forEach((item) => {
+                  if (item.md5 === fileMd5) {
+                    item.status = 'done';
+                    item.fileId = res.data.fileId;
+                    this.$emit('fileId', item.fileId);
+                    this.$message.success(item.name + ' 文件上传成功!');
+                    // console.log(item.name + ' 文件上传成功!')
+                    resolve('sucess');
+                  }
+                });
+              } else {
+                this.$message.error(res.msg);
+              }
+            })
+            .catch((err) => {
+              // console.log('mergeRequest -> err', err);
+              reject('合并失败', err);
+            });
+        });
+      },
+      getFileMD5(file) {
+        return md5(file.name + file.size + file.lastModifiedDate);
+        // return md5(file.name + file.size + file.lastModifiedDate + Math.random(1000) + new Date().getTime());
+        // const reader = new FileReader();
+        // reader.onload = function (e) {
+        //   file.fileMD5 = md5(e.target.result);
+        //   console.log(e.target.result);
+        // }
+        // reader.readAsText(file);
+      },
+      handleChange(info) {
+        this.$set(this, 'fileList', info.fileList);
+        const status = info.file.status;
+        if (status === 'done') {
+          this.$message.success('上传成功');
+        } else if (status === 'uploading') {
+          // this.$message.info(`${info.file.name} 上传中...`);
+        } else if (status === 'error') {
+          this.$message.error(`${info.file.name} 上传失败`);
+        }
+      },
+      handleImageRemove(res) {
+        this.fileList.forEach((item, idx) => {
+          if (item.uid === res.uid) {
+            this.fileList.splice(idx, 1);
+            this.$message.success('删除成功');
+          }
+        });
+        this.$emit('getFileList', this.fileList);
+      },
+    },
+  };
+</script>
+<style>
+  /* 隐藏预览功能 */
+  span.ant-upload-list-item-actions a {
+    display: none;
+  }
+</style>

+ 5 - 2
src/views/general/config/customComponents/ChooseModal.vue

@@ -115,18 +115,21 @@
         console.log('========reload======');
       }
 
-      function selectionChange() {
+      function selectionChange(e) {
+        console.log('==========selelelelele');
         const keys = getTableAction().getSelectRowKeys();
         if (keys.length) {
           btn.disable_btn = false;
         } else {
           btn.disable_btn = true;
         }
+        console.log(`e==`, e)
+        emit('checked', e, closeModal);
       }
 
       function handleChecked(record) {
         console.log(`record`, record);
-        emit('checked', record.id, closeModal);
+        emit('checked', record, closeModal);
       }
       async function checkedBatches() {
         const keys = await getTableAction().getSelectRowKeys();

+ 11 - 4
src/views/general/config/customComponents/UploadImage.vue

@@ -30,7 +30,9 @@
     <div class="image-list" v-if="image_list">
       <div v-for="(item, index) in image_list" :key="index">
         <div class="image-item" v-if="imageUrls.split(',').includes(item.url)">
-          <div class="image-wrap"> <a-image width="70px" :src="item.preUrl" /></div>
+          <div class="image-wrap">
+            <a-image width="70px" :src="'http://jetbill.cn' + item.url"
+          /></div>
           <div class="dele-image" @click="deleteImage(item.url)">
             <Icon icon="ri:delete-bin-5-fill" />
           </div>
@@ -88,10 +90,15 @@
         );
         state.imageUrls = arr.toString();
       }
-      function checked(key, closeModal) {
-        console.log(`key`, key);
+      function checked(record, closeModal) {
+        console.log('state.imageUrls', state.imageUrls);
+        console.log(`props.type`, props.type);
+        if (props.type === 'image') {
+          state.imageUrls = record.imgUrl.replace('http://jetbill.cn', '');
+        } else {
+          state.imageUrls = state.imageUrls + ',' + record.imgUrl.replace('http://jetbill.cn', '');
+        }
         closeModal();
-        console.log('========checked========');
         emit('change');
       }
       function checkedBatches(keys, closeModal) {

+ 2 - 2
src/views/general/config/data.ts

@@ -154,7 +154,7 @@ export const columns: BasicColumn[] = [
             value: record.value,
             type: 'image',
             tip: record.tip,
-            urls: '/fake/fake.jpg,/fake1/fake1.jpg,/fake4/fake4.jpg,\\pyadmin\\public\\uploads\\2021713\\d64e169b4fa956e0cf935954e5671efd.jpg',
+            urls: '/uploads/20210713/a05d360766d30868cdb878b4c0aac77d.jpg',
             // options: record.content,
             // style: { width: '65%' },
             // onChange: onArrayChange,
@@ -164,7 +164,7 @@ export const columns: BasicColumn[] = [
             value: record.value,
             type: 'images',
             tip: record.tip,
-            urls: 'fake/fake.jpg,fake1/fake1.jpg',
+            urls: '/uploads/20210712/51ae92cdb0bb22a88d6e73e62bcb1cad.jpg,/uploads/20210713/a05d360766d30868cdb878b4c0aac77d.jpg',
             // options: record.content,
             // style: { width: '65%' },
             // onChange: onArrayChange,