浏览代码

优化分片上传组件

ZaiZai 2 年之前
父节点
当前提交
2cad3bbd77
共有 27 个文件被更改,包括 631 次插入620 次删除
  1. 0 2
      src/App.vue
  2. 0 56
      src/components/plugins/uploadFile/common/md5_bak.js
  3. 0 27
      src/components/plugins/uploadFile/common/utils.js
  4. 二进制
      src/components/plugins/uploadFile/images/audio-icon.png
  5. 二进制
      src/components/plugins/uploadFile/images/image-icon.png
  6. 二进制
      src/components/plugins/uploadFile/images/text-icon.png
  7. 二进制
      src/components/plugins/uploadFile/images/video-icon.png
  8. 二进制
      src/components/plugins/uploadFile/images/zip.png
  9. 0 8
      src/components/plugins/uploadFile/index.js
  10. 0 339
      src/components/plugins/uploadFile/index.vue
  11. 0 0
      src/global/components/hc-upload-file/common/file-events.js
  12. 0 0
      src/global/components/hc-upload-file/common/md5.js
  13. 24 0
      src/global/components/hc-upload-file/common/utils.js
  14. 0 0
      src/global/components/hc-upload-file/components/btn.vue
  15. 0 0
      src/global/components/hc-upload-file/components/drop.vue
  16. 14 108
      src/global/components/hc-upload-file/components/file.vue
  17. 0 0
      src/global/components/hc-upload-file/components/files.vue
  18. 0 0
      src/global/components/hc-upload-file/components/list.vue
  19. 0 0
      src/global/components/hc-upload-file/components/unsupport.vue
  20. 0 0
      src/global/components/hc-upload-file/components/uploader.vue
  21. 348 0
      src/global/components/hc-upload-file/file.vue
  22. 133 0
      src/global/components/hc-upload-file/index.vue
  23. 39 33
      src/global/components/hc-upload-file/style/index.scss
  24. 2 0
      src/global/components/index.js
  25. 0 2
      src/plugins/bus.js
  26. 26 23
      src/views/file/collection.vue
  27. 45 22
      src/views/file/records.vue

+ 0 - 2
src/App.vue

@@ -1,7 +1,6 @@
 <template>
     <AppConfig>
         <router-view/>
-        <GlobalUploadFile />
     </AppConfig>
 </template>
 
@@ -9,7 +8,6 @@
 import {nextTick, ref, watch} from "vue";
 import {useAppStore} from "~src/store";
 import AppConfig from "~com/AppConfig/index.vue";
-import GlobalUploadFile from "~com/plugins/uploadFile/index.vue";
 import {setElementMainColor, ulog, getObjValue} from "js-fast-way"
 import {useOsTheme} from '~src/plugins/useOsTheme';
 import config from '~src/config/index';

+ 0 - 56
src/components/plugins/uploadFile/common/md5_bak.js

@@ -1,56 +0,0 @@
-import SparkMD5 from 'spark-md5'
-
-/**
- * 分段计算MD5
- * @param file {File}
- * @param options {Object} - onProgress | onSuccess | onError
- */
-export function generateMD5(file, options = {}) {
-  const fileReader = new FileReader()
-  const time = new Date().getTime()
-  const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
-  const chunkSize = 10 * 1024 * 1000
-  const chunks = Math.ceil(file.size / chunkSize)
-  let currentChunk = 0
-  const spark = new SparkMD5.ArrayBuffer()
-  const loadNext = () => {
-    let start = currentChunk * chunkSize
-    let end = start + chunkSize >= file.size ? file.size : start + chunkSize
-
-    fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
-  }
-
-  loadNext()
-
-  fileReader.onload = (e) => {
-    spark.append(e.target.result)
-
-    if (currentChunk < chunks) {
-      currentChunk++
-      loadNext()
-      if (options.onProgress && typeof options.onProgress == 'function') {
-        options.onProgress(currentChunk, chunks)
-      }
-    } else {
-      let md5 = spark.end()
-
-      // md5计算完毕
-      if (options.onSuccess && typeof options.onSuccess == 'function') {
-        options.onSuccess(md5)
-      }
-
-      console.log(
-        `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${
-          new Date().getTime() - time
-        } ms`
-      )
-    }
-  }
-
-  fileReader.onerror = function () {
-    console.log('MD5计算失败')
-    if (options.onError && typeof options.onError == 'function') {
-      options.onError()
-    }
-  }
-}

+ 0 - 27
src/components/plugins/uploadFile/common/utils.js

@@ -1,27 +0,0 @@
-export function secondsToStr (temp) {
-  const years = Math.floor(temp / 31536000)
-  if (years) {
-    return years + ' year' + numberEnding(years)
-  }
-  const days = Math.floor((temp %= 31536000) / 86400)
-  if (days) {
-    return days + ' day' + numberEnding(days)
-  }
-  const hours = Math.floor((temp %= 86400) / 3600)
-  if (hours) {
-    return hours + ' hour' + numberEnding(hours)
-  }
-  const minutes = Math.floor((temp %= 3600) / 60)
-  if (minutes) {
-    return minutes + ' minute' + numberEnding(minutes)
-  }
-  const seconds = temp % 60
-  return seconds + ' second' + numberEnding(seconds)
-  function numberEnding (number) {
-    return (number > 1) ? 's' : ''
-  }
-}
-
-export function kebabCase (s) {
-  return s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
-}

二进制
src/components/plugins/uploadFile/images/audio-icon.png


二进制
src/components/plugins/uploadFile/images/image-icon.png


二进制
src/components/plugins/uploadFile/images/text-icon.png


二进制
src/components/plugins/uploadFile/images/video-icon.png


二进制
src/components/plugins/uploadFile/images/zip.png


+ 0 - 8
src/components/plugins/uploadFile/index.js

@@ -1,8 +0,0 @@
-/*import Uploader from './components/uploader.vue'
-import UploaderBtn from './components/btn.vue'
-import UploaderDrop from './components/drop.vue'
-import UploaderUnsupport from './components/unsupport.vue'
-import UploaderList from './components/list.vue'
-import UploaderFiles from './components/files.vue'
-import UploaderFile from './components/file.vue'
-*/

+ 0 - 339
src/components/plugins/uploadFile/index.vue

@@ -1,339 +0,0 @@
-<template>
-    <div id="global-uploader" :class="{ 'global-uploader-single': !global }">
-        <!-- 上传 -->
-        <HcUploader
-            ref="uploaderRef"
-            class="uploader-app"
-            :options="optionsValue"
-            :file-status-text="fileStatusText"
-            :auto-start="false"
-            @file-added="onFileAdded"
-            @file-success="onFileSuccess"
-            @file-progress="onFileProgress"
-            @file-error="onFileError"
-        >
-            <HcUploaderUnsupport/>
-            <HcUploaderBtn id="global-uploader-btn" ref="uploadBtnRef">选择文件</HcUploaderBtn>
-            <HcUploaderList v-show="panelShow">
-                <template #default="{ fileList }">
-                    <div class="file-panel" :class="{ collapse: collapse }">
-                        <div class="file-title">
-                            <div class="title">文件列表 {{fileList.length > 0 ? `(${fileList.length})` : ''}}</div>
-                            <div class="operate">
-                                <el-button :title="collapse ? '展开' : '折叠'" link @click="collapse = !collapse">
-                                    <i :class="collapse ? 'ri-fullscreen-line' : 'ri-subtract-line'"/>
-                                </el-button>
-                                <el-button title="关闭" link @click="close">
-                                    <i class="ri-close-line"/>
-                                </el-button>
-                            </div>
-                        </div>
-
-                        <ul class="file-list">
-                            <li v-for="file in fileList" :key="file.id" class="file-item">
-                                <HcUploaderFile ref="files" :class="['file_' + file.id, customStatus]" :file="file" :list="true"/>
-                            </li>
-                            <div v-if="!fileList.length" class="no-file">
-                                <i class="ri-file-text-line" style="font-size: 24px"/>
-                                暂无待上传文件
-                            </div>
-                        </ul>
-                    </div>
-                </template>
-            </HcUploaderList>
-        </HcUploader>
-    </div>
-</template>
-
-<script>
-import {computed, nextTick, onMounted, ref} from 'vue'
-import {getTokenHeader} from '~src/api/request/header';
-import HcUploader from './components/uploader.vue'
-import HcUploaderBtn from './components/btn.vue'
-import HcUploaderUnsupport from './components/unsupport.vue'
-import HcUploaderList from './components/list.vue'
-import HcUploaderFile from './components/file.vue'
-import {getArrValue, getObjValue, getFileSuffix} from "js-fast-way";
-import {ElNotification} from "element-plus";
-import {generateMD5} from './common/md5'
-import Bus from '~src/plugins/bus.js'
-
-const acceptType = 'image/png,image/jpg,image/jpeg,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,application/pdf,.doc,.docx,application/msword'
-
-export default {
-    name: 'GlobalUploader',
-    props: {
-        global: {
-            type: Boolean,
-            default: true
-        }
-    },
-    components: {
-        HcUploader,
-        HcUploaderBtn,
-        HcUploaderUnsupport,
-        HcUploaderList,
-        HcUploaderFile
-    },
-    emits: ['fileAdded', 'fileSuccess', 'fileError', 'filesChange', 'fileProgress'],
-    setup(props, {emit}) {
-        const optionsValue = {
-            target: '/api/blade-resource/largeFile/endpoint/upload-file',
-            chunkSize: '2048000',
-            fileParameterName: 'file',
-            maxChunkRetries: 3,
-            headers: getTokenHeader(),
-            // 是否开启服务器分片校验
-            testChunks: true,
-            testMethod: 'POST',
-            // 服务器分片校验函数,秒传及断点续传基础
-            checkChunkUploadedByResponse: (chunk, message) => {
-                let skip = false
-                try {
-                    let objMessage = getObjValue(JSON.parse(message))
-                    if (objMessage['skipUpload']) {
-                        skip = true
-                    } else {
-                        skip = (getArrValue(objMessage['uploaded'])).indexOf(chunk.offset + 1) >= 0
-                    }
-                } catch (e) {}
-                return skip
-            },
-            query: (file, chunk) => {
-                return {...file.params}
-            }
-        }
-
-        const initOptions = ({target, fileName, maxChunk, accept}) => {
-            // 自定义上传url
-            if (target) {
-                uploader.value.opts.target = target
-            }
-            // 自定义文件上传参数名
-            if (fileName) {
-                uploader.value.opts.fileParameterName = fileName
-            }
-            // 并发上传数量
-            if (maxChunk) {
-                uploader.value.opts.maxChunkRetries = maxChunk
-            }
-            // 自定义文件上传类型
-            if (accept) {
-                nextTick(() => {
-                    let input = document.querySelector('#global-uploader-btn input')
-                    input.setAttribute('accept', accept ? accept : acceptType)
-                })
-            }
-        }
-        const fileStatusText = {
-            success: '上传成功',
-            error: '上传失败',
-            uploading: '上传中',
-            paused: '已暂停',
-            waiting: '等待上传'
-        }
-        const customStatus = ref('')
-        const panelShow = ref(false)
-        const collapse = ref(false)
-        const uploaderRef = ref()
-        const uploadBtnRef = ref()
-
-        const uploader = computed(() => uploaderRef.value?.uploader)
-
-        let customParams = {}
-
-        async function onFileAdded(file) {
-            panelShow.value = true
-            trigger('fileAdded')
-            // 将额外的参数赋值到每个文件上,以不同文件使用不同params的需求
-            file.params = {
-                ...customParams,
-                objectType: getFileSuffix(file.name),
-                fileType: file.fileType,
-            }
-            // 计算MD5
-            const md5 = await computeMD5(file)
-            startUpload(file, md5)
-        }
-
-        function computeMD5(file) {
-            // 文件状态设为"计算MD5"
-            statusSet(file.id, 'md5')
-            // 暂停文件
-            file.pause()
-            // 计算MD5时隐藏”开始“按钮
-            setResumeStyle(file.id, 'none')
-            nextTick(() => {
-                document.querySelector(`.custom-status-${file.id}`).innerText = '校验MD5中'
-            })
-            // 开始计算MD5
-            return new Promise((resolve, reject) => {
-                generateMD5(file, {
-                    onSuccess(md5) {
-                        statusRemove(file.id)
-                        resolve(md5)
-                    },
-                    onError() {
-                        error(`文件${file.name}读取出错,请检查该文件`)
-                        file.cancel()
-                        statusRemove(file.id)
-                        reject()
-                    }
-                })
-            })
-        }
-
-        const setResumeStyle = (id, val = 'none') => {
-            nextTick(() => {
-                try {
-                    document.querySelector(`.file_${id} .uploader-file-resume`).style.display = val
-                } catch (e) {}
-            })
-        }
-
-        // md5计算完毕,开始上传
-        const beforeFileNum = ref(0)
-        function startUpload(file, md5) {
-            const fileList = uploader.value.fileList;
-            //判断是否满足条件
-            const result = fileList.every(({uniqueIdentifier}) => {
-                return uniqueIdentifier !== md5
-            })
-            if (result) {
-                file.uniqueIdentifier = md5
-                setResumeStyle(file.id,'')
-                beforeFileNum.value ++;
-                file.resume()
-                trigger('fileProgress', true)
-            } else {
-                file.cancel()
-                error('请不要重复上传相同文件')
-            }
-        }
-
-        //上传完成
-        const finishFileNum = ref(0)
-        function onFileSuccess(rootFile, file, response, chunk) {
-            let res = JSON.parse(response)
-            // 服务端自定义的错误(即http状态码为200,但是是错误的情况),这种错误是Uploader无法拦截的
-            if (res.code !== 200) {
-                errorFileNum.value ++;
-                error(res.msg)
-                // 文件状态设为“失败”
-                statusSet(file.id, 'failed')
-            } else {
-                finishFileNum.value ++;
-                trigger('fileSuccess', getObjValue(res.data))
-            }
-            if (beforeFileNum.value === (finishFileNum.value + errorFileNum.value)) {
-                trigger('filesChange')
-                trigger('fileProgress', false)
-            }
-        }
-
-        function onFileProgress(rootFile, file, chunk) {
-            console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
-        }
-
-        //上传失败
-        const errorFileNum = ref(0)
-        function onFileError(rootFile, file, response, chunk) {
-            errorFileNum.value ++;
-            error(response)
-            trigger('fileError')
-        }
-
-        function close() {
-            finishFileNum.value = 0
-            beforeFileNum.value = 0
-            errorFileNum.value = 0
-            uploader.value.cancel()
-            panelShow.value = false
-        }
-
-        //新增的自定义的状态: 'md5'、'merging'、'transcoding'、'failed'
-        function statusSet(id, status) {
-            const statusMap = {
-                md5: {text: '校验MD5', bgc: '#fff'},
-                merging: {text: '合并中', bgc: '#e2eeff'},
-                transcoding: {text: '转码中', bgc: '#e2eeff'},
-                failed: {text: '上传失败', bgc: '#e2eeff'}
-            }
-            customStatus.value = status
-            nextTick(() => {
-                const statusTag = document.createElement('p')
-                statusTag.className = `custom-status-${id} custom-status`
-                statusTag.innerText = statusMap[status].text
-                statusTag.style.backgroundColor = statusMap[status].bgc
-
-                const statusWrap = document.querySelector(`.file_${id} .uploader-file-status`)
-                statusWrap.appendChild(statusTag)
-            })
-        }
-
-        function statusRemove(id) {
-            customStatus.value = ''
-            nextTick(() => {
-                const statusTag = document.querySelector(`.custom-status-${id}`)
-                statusTag.remove()
-            })
-        }
-
-        function trigger(key, data) {
-            Bus.emit(key, data)
-            emit(key, data)
-        }
-
-        function error(msg) {
-            ElNotification({
-                title: '错误',
-                message: msg,
-                type: 'error',
-                duration: 2000
-            })
-        }
-
-        onMounted(() => {
-            finishFileNum.value = 0
-            beforeFileNum.value = 0
-            errorFileNum.value = 0
-            nextTick(() => {
-                let input = document.querySelector('#global-uploader-btn input')
-                input.setAttribute('accept', acceptType)
-            })
-            //打开上传器
-            Bus.on('openUploader', ({params = {}, options = {}}) => {
-                customParams = params
-                initOptions(options)
-                if (uploadBtnRef.value) {
-                    uploadBtnRef.value.$el.click()
-                }
-            })
-            //关闭上传器
-            Bus.on('closeUploader', () => {
-                close()
-            })
-        })
-
-        return {
-            optionsValue,
-            initOptions,
-            fileStatusText,
-            customStatus,
-            panelShow,
-            collapse,
-            uploaderRef,
-            uploadBtnRef,
-            onFileAdded,
-            onFileSuccess,
-            onFileProgress,
-            onFileError,
-            close
-        }
-    }
-}
-</script>
-
-<style lang="scss">
-@import "./style/index";
-</style>

+ 0 - 0
src/components/plugins/uploadFile/common/file-events.js → src/global/components/hc-upload-file/common/file-events.js


+ 0 - 0
src/components/plugins/uploadFile/common/md5.js → src/global/components/hc-upload-file/common/md5.js


+ 24 - 0
src/global/components/hc-upload-file/common/utils.js

@@ -0,0 +1,24 @@
+export function secondsToStr(temp) {
+    const years = Math.floor(temp / 31536000)
+    if (years) {
+        return years + ' 年'
+    }
+    const days = Math.floor((temp %= 31536000) / 86400)
+    if (days) {
+        return days + ' 天'
+    }
+    const hours = Math.floor((temp %= 86400) / 3600)
+    if (hours) {
+        return hours + ' 时'
+    }
+    const minutes = Math.floor((temp %= 3600) / 60)
+    if (minutes) {
+        return minutes + ' 分'
+    }
+    const seconds = temp % 60
+    return seconds + ' 秒'
+}
+
+export function kebabCase(s) {
+    return s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)
+}

+ 0 - 0
src/components/plugins/uploadFile/components/btn.vue → src/global/components/hc-upload-file/components/btn.vue


+ 0 - 0
src/components/plugins/uploadFile/components/drop.vue → src/global/components/hc-upload-file/components/drop.vue


+ 14 - 108
src/components/plugins/uploadFile/components/file.vue → src/global/components/hc-upload-file/components/file.vue

@@ -26,18 +26,15 @@
         >
             <div class="uploader-file-progress" :class="progressingClass" :style="progressStyle"></div>
             <div class="uploader-file-info">
-                <div class="uploader-file-name"><i class="uploader-file-icon" :icon="fileCategory"></i>{{ file.name }}
+                <div class="uploader-file-name">
+                    <i class="uploader-file-icon" :icon="fileCategory"/>
+                    <span>{{ file.name }}</span>
                 </div>
                 <div class="uploader-file-size">{{ formatedSize }}</div>
-                <div class="uploader-file-meta"></div>
-                <div class="uploader-file-status">
-                    <span v-show="status !== 'uploading'">{{ statusText }}</span>
-                    <span v-show="status === 'uploading'">
-            <span>{{ progressStyle.progress }}&nbsp;</span>
-            <em>{{ formatedAverageSpeed }}&nbsp;</em>
-            <i>{{ formatedTimeRemaining }}</i>
-          </span>
+                <div class="uploader-file-status" v-show="status === 'uploading'">
+                    {{ progressStyle.progress }} / {{ formatedAverageSpeed}} / {{ formatedTimeRemaining}}
                 </div>
+                <div class="uploader-file-status text" :class="'hc-custom-status-' + file.id" v-show="status !== 'uploading'">{{ statusText }}</div>
                 <div class="uploader-file-actions">
                     <span class="uploader-file-pause" @click="pause"></span>
                     <span class="uploader-file-resume" @click="resume">️</span>
@@ -120,7 +117,7 @@ export default {
             }
         })
         const formatedAverageSpeed = computed(() => {
-            return `${Uploader.utils.formatSize(averageSpeed.value)} / s`
+            return `${Uploader.utils.formatSize(averageSpeed.value)}/s`
         })
         const status = computed(() => {
             let isError = error
@@ -304,104 +301,13 @@ export default {
 }
 </script>
 
-<style>
-.uploader-file {
-    position: relative;
-    height: 49px;
-    line-height: 49px;
-    overflow: hidden;
-    border-bottom: 1px solid #cdcdcd;
-}
-
-.uploader-file-progress {
-    position: absolute;
-    width: 100%;
-    height: 100%;
-    background: #e2eeff;
-    transform: translateX(-100%);
-}
-
-.uploader-file-progressing {
-    transition: all .4s linear;
-}
-
+<style lang="scss" scoped>
 .uploader-file-info {
-    position: relative;
-    z-index: 1;
-    height: 100%;
-    overflow: hidden;
-}
-
-.uploader-file-info:hover {
-    background-color: rgba(240, 240, 240, 0.2);
-}
-
-.uploader-file-info i,
-.uploader-file-info em {
-    font-style: normal;
-}
-
-.uploader-file-name,
-.uploader-file-size,
-.uploader-file-meta,
-.uploader-file-status,
-.uploader-file-actions {
-    float: left;
-    position: relative;
-    height: 100%;
-}
-
-.uploader-file-name {
-    width: 45%;
-    overflow: hidden;
-    white-space: nowrap;
-    text-overflow: ellipsis;
-    text-indent: 14px;
-}
-
-.uploader-file-icon {
-    width: 24px;
-    height: 24px;
-    display: inline-block;
-    vertical-align: top;
-    margin-top: 13px;
-    margin-right: 8px;
-}
-
-.uploader-file-icon::before {
-    content: "";
-    display: block;
-    height: 100%;
-    font-size: 24px;
-    line-height: 1;
-    text-indent: 0;
-}
-
-.uploader-file-size {
-    width: 13%;
-    text-indent: 10px;
-}
-
-.uploader-file-meta {
-    width: 8%;
-}
-
-.uploader-file-status {
-    width: 24%;
-    text-indent: 20px;
-}
-
-.uploader-file-actions {
-    width: 10%;
-}
-
-.uploader-file-actions > span {
-    display: none;
-    float: left;
-    width: 16px;
-    height: 16px;
-    margin-top: 16px;
-    margin-right: 10px;
-    cursor: pointer;
+    i, em {
+        font-style: normal;
+    }
+    &:hover {
+        background-color: #e9faf4;
+    }
 }
 </style>

+ 0 - 0
src/components/plugins/uploadFile/components/files.vue → src/global/components/hc-upload-file/components/files.vue


+ 0 - 0
src/components/plugins/uploadFile/components/list.vue → src/global/components/hc-upload-file/components/list.vue


+ 0 - 0
src/components/plugins/uploadFile/components/unsupport.vue → src/global/components/hc-upload-file/components/unsupport.vue


+ 0 - 0
src/components/plugins/uploadFile/components/uploader.vue → src/global/components/hc-upload-file/components/uploader.vue


+ 348 - 0
src/global/components/hc-upload-file/file.vue

@@ -0,0 +1,348 @@
+<template>
+    <div id="hc-global-upload-file" class="hc-global-upload-file-box" :class="{ 'global-uploader-single': !global }">
+        <!-- 上传 -->
+        <HcUploader ref="uploaderRef" class="uploader-app"
+                    :options="optionsValue"
+                    :file-status-text="fileStatusText"
+                    :auto-start="false"
+                    @file-added="onFileAdded"
+                    @file-success="onFileSuccess"
+                    @file-progress="onFileProgress"
+                    @file-error="onFileError"
+        >
+            <HcUploaderUnsupport/>
+            <HcUploaderBtn class="hc-global-upload-btn" id="hc-global-upload-btn" ref="uploadBtnRef">选择文件</HcUploaderBtn>
+            <HcUploaderList v-show="panelShow">
+                <template #default="{ fileList }">
+                    <div class="file-panel" :class="{ collapse: collapse }">
+                        <div class="file-title">
+                            <div class="title">文件列表 {{fileList.length > 0 ? `(${fileList.length})` : ''}}</div>
+                            <div class="operate">
+                                <el-button :title="collapse ? '展开' : '折叠'" link @click="collapse = !collapse">
+                                    <i :class="collapse ? 'ri-fullscreen-line' : 'ri-subtract-line'"/>
+                                </el-button>
+                                <el-button title="关闭" link @click="close">
+                                    <i class="ri-close-line"/>
+                                </el-button>
+                            </div>
+                        </div>
+
+                        <ul class="file-list">
+                            <li v-for="file in fileList" :key="file.id" class="file-item">
+                                <HcUploaderFile ref="files" :class="['file_' + file.id, customStatus]" :file="file" :list="true"/>
+                            </li>
+                            <div v-if="!fileList.length" class="no-file">
+                                <i class="ri-file-text-line"/>
+                                <span>暂无待上传文件</span>
+                            </div>
+                        </ul>
+                    </div>
+                </template>
+            </HcUploaderList>
+        </HcUploader>
+    </div>
+</template>
+
+<script setup>
+import {computed, nextTick, onMounted, ref, watch} from 'vue'
+import {getTokenHeader} from '~src/api/request/header';
+import HcUploader from './components/uploader.vue'
+import HcUploaderBtn from './components/btn.vue'
+import HcUploaderUnsupport from './components/unsupport.vue'
+import HcUploaderList from './components/list.vue'
+import HcUploaderFile from './components/file.vue'
+import {getObjValue, getFileSuffix} from "js-fast-way";
+import {generateMD5} from './common/md5'
+
+const props = defineProps({
+    global: {
+        type: Boolean,
+        default: true
+    },
+    // 发送给服务器的额外参数
+    params: {
+        type: Object,
+        default: () => ({})
+    },
+    options: {
+        type: Object,
+        default: () => ({})
+    }
+})
+
+//初始变量
+const customStatus = ref('')
+const panelShow = ref(false)
+const collapse = ref(false)
+const uploaderRef = ref(null)
+const uploadBtnRef = ref(null)
+const customParams = ref({})
+const uploader = computed(() => uploaderRef.value?.uploader)
+const acceptType = 'image/png,image/jpg,image/jpeg,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,application/pdf,.doc,.docx,application/msword'
+
+const fileStatusText = {
+    success: '上传成功',
+    error: '上传失败',
+    uploading: '上传中',
+    paused: '已暂停',
+    waiting: '等待上传'
+}
+
+//事件
+const emit = defineEmits(['fileAdded', 'fileSuccess', 'fileError', 'filesChange', 'fileProgress'])
+
+//监听
+watch(() => [
+    props.params,
+    props.options
+], ([params, options]) => {
+    if (params) {
+        customParams.value = params
+    }
+    if (options) {
+        initOptions(options)
+    }
+})
+
+onMounted(() => {
+    remove()
+    nextTick(() => {
+        let input = document.querySelector('#hc-global-upload-btn input')
+        input.setAttribute('accept', acceptType)
+    })
+    customParams.value = props.params
+    initOptions(props.options)
+})
+
+//配置
+const optionsValue = ref({
+    target: '/api/blade-resource/largeFile/endpoint/upload-file',
+    chunkSize: '2048000',
+    fileParameterName: 'file',
+    maxChunkRetries: 3,
+    headers: getTokenHeader(),
+    // 是否开启服务器分片校验
+    testChunks: true,
+    testMethod: 'POST',
+    // 服务器分片校验函数,秒传及断点续传基础
+    checkChunkUploadedByResponse: () => {
+        return false
+    },
+    query: (file) => {
+        return {...file.params}
+    }
+})
+
+//设置配置
+const initOptions = ({target, fileName, maxChunk, accept}) => {
+    // 自定义上传url
+    if (target) {
+        uploader.value.opts.target = target
+    }
+    // 自定义文件上传参数名
+    if (fileName) {
+        uploader.value.opts.fileParameterName = fileName
+    }
+    // 并发上传数量
+    if (maxChunk) {
+        uploader.value.opts.maxChunkRetries = maxChunk
+    }
+    // 自定义文件上传类型
+    if (accept) {
+        nextTick(() => {
+            let input = document.querySelector('#hc-global-upload-btn input')
+            input.setAttribute('accept', accept ? accept : acceptType)
+        })
+    }
+}
+
+//选择了文件
+const onFileAdded = async (file) => {
+    panelShow.value = true
+    trigger('fileAdded')
+    // 将额外的参数赋值到每个文件上,以不同文件使用不同params的需求
+    file.params = {
+        ...customParams,
+        objectType: getFileSuffix(file.name),
+        fileType: file.fileType,
+    }
+    // 计算MD5
+    const md5 = await computeMD5(file)
+    startUpload(file, md5)
+}
+
+//计算文件的MD5
+const computeMD5 = (file) => {
+    // 文件状态设为"计算MD5"
+    statusSet(file.id, 'md5')
+    // 暂停文件
+    file.pause()
+    // 计算MD5时隐藏”开始“按钮
+    setResumeStyle(file.id, 'none')
+    nextTick(() => {
+        document.querySelector(`.hc-custom-status-${file.id}`).innerText = '校验MD5中'
+    })
+    // 开始计算MD5
+    return new Promise((resolve, reject) => {
+        generateMD5(file, {
+            onSuccess(md5) {
+                statusRemove(file.id)
+                resolve(md5)
+            },
+            onError() {
+                error(`文件${file.name}读取出错,请检查该文件`)
+                file.cancel()
+                statusRemove(file.id)
+                reject()
+            }
+        })
+    })
+}
+
+const setResumeStyle = (id, val = 'none') => {
+    nextTick(() => {
+        try {
+            document.querySelector(`.file_${id} .uploader-file-resume`).style.display = val
+        } catch (e) {}
+    })
+}
+
+// md5计算完毕,开始上传
+const beforeFileNum = ref(0)
+const startUpload = (file, md5) => {
+    const fileList = uploader.value.fileList;
+    //判断是否满足条件
+    const result = fileList.every(({uniqueIdentifier}) => {
+        return uniqueIdentifier !== md5
+    })
+    if (result) {
+        file.uniqueIdentifier = md5
+        setResumeStyle(file.id,'')
+        beforeFileNum.value ++;
+        file.resume()
+        trigger('fileProgress', true)
+    } else {
+        file.cancel()
+        error('请不要重复上传相同文件')
+    }
+}
+
+//上传中
+const onFileProgress = (rootFile, file, chunk) => {
+    console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
+}
+
+//上传完成
+const finishFileNum = ref(0)
+const onFileSuccess = (rootFile, file, response) => {
+    let res = JSON.parse(response)
+    // 服务端自定义的错误(即http状态码为200,但是是错误的情况),这种错误是Uploader无法拦截的
+    if (res.code !== 200) {
+        errorFileNum.value ++;
+        error(res.msg)
+        // 文件状态设为“失败”
+        statusSet(file.id, 'failed')
+    } else {
+        finishFileNum.value ++;
+        trigger('fileSuccess', getObjValue(res.data))
+    }
+    if (beforeFileNum.value === (finishFileNum.value + errorFileNum.value)) {
+        trigger('filesChange')
+        trigger('fileProgress', false)
+    }
+}
+
+//上传失败
+const errorFileNum = ref(0)
+const onFileError = (rootFile, file, response) => {
+    errorFileNum.value ++;
+    error(response)
+    trigger('fileError')
+}
+
+//新增的自定义的状态: 'md5'、'merging'、'transcoding'、'failed'
+const statusSet = (id, status) => {
+    const statusMap = {
+        md5: {text: '校验MD5', bgc: '#fff'},
+        merging: {text: '合并中', bgc: '#e2eeff'},
+        transcoding: {text: '转码中', bgc: '#e2eeff'},
+        failed: {text: '上传失败', bgc: '#e2eeff'}
+    }
+    customStatus.value = status
+    nextTick(() => {
+        const statusTag = document.createElement('p')
+        statusTag.className = `custom-status-${id} custom-status`
+        statusTag.innerText = statusMap[status].text
+        statusTag.style.backgroundColor = statusMap[status].bgc
+
+        const statusWrap = document.querySelector(`.file_${id} .uploader-file-status`)
+        statusWrap.appendChild(statusTag)
+    })
+}
+
+const statusRemove = (id) => {
+    customStatus.value = ''
+    nextTick(() => {
+        const statusTag = document.querySelector(`.custom-status-${id}`)
+        statusTag.remove()
+    })
+}
+
+//事件
+const trigger = (key, data = {}) => {
+    emit(key, data)
+}
+
+//错误
+const error = (msg) => {
+    window?.$notification({
+        title: '错误',
+        message: msg,
+        type: 'error',
+        duration: 2000
+    })
+}
+
+//关闭
+const close = () => {
+    remove()
+    isShow(false)
+}
+
+//清除
+const remove = () => {
+    finishFileNum.value = 0
+    beforeFileNum.value = 0
+    errorFileNum.value = 0
+    cancel()
+}
+
+const cancel = () => {
+    uploader?.value.cancel()
+}
+
+//是否显示
+const isShow = (res) => {
+    panelShow.value = res
+}
+
+//点击上传按钮
+const btnUpload = () => {
+    if (uploadBtnRef.value) {
+        uploadBtnRef?.value.$el.click()
+    }
+}
+
+// 暴露出去
+defineExpose({
+    close,
+    remove,
+    cancel,
+    isShow,
+    btnUpload
+})
+</script>
+
+<style lang="scss">
+@import "./style/index";
+</style>

+ 133 - 0
src/global/components/hc-upload-file/index.vue

@@ -0,0 +1,133 @@
+<template>
+    <div class="hc-global-upload-file-box-hide" v-if="isBody">
+        <Teleport to="#app">
+            <UploadFile ref="uploadFileRef"
+                        :global="customGlobal"
+                        :params="customParams"
+                        :options="customOptions"
+                        @fileAdded="uploadFileAdded"
+                        @fileProgress="uploadFileProgress"
+                        @fileSuccess="uploadFileSuccess"
+                        @filesChange="uploadFileChange"
+                        @fileError="uploadFileError"
+            />
+        </Teleport>
+    </div>
+</template>
+
+<script setup>
+import {nextTick, onMounted, ref, watch} from 'vue'
+import UploadFile from './file.vue'
+
+const props = defineProps({
+    global: {
+        type: Boolean,
+        default: true
+    },
+    // 发送给服务器的额外参数
+    params: {
+        type: Object,
+        default: () => ({})
+    },
+    options: {
+        type: Object,
+        default: () => ({})
+    }
+})
+
+//初始变量
+const isBody = ref(false)
+const uploadFileRef = ref(null)
+const customGlobal = ref(props.global)
+const customParams = ref(props.params)
+const customOptions = ref(props.options)
+
+//事件
+const emit = defineEmits(['fileAdded', 'fileSuccess', 'fileError', 'filesChange', 'fileProgress'])
+
+//监听
+watch(() => [
+    props.params,
+    props.options,
+    props.global
+], ([params, options, global]) => {
+    customGlobal.value = global
+    if (params) {
+        customParams.value = params
+    }
+    if (options) {
+        customOptions.value = options
+    }
+})
+
+onMounted(() => {
+    //页面渲染完成后,再让 vue3 的 Teleport,把弹出框挂载到外部节点上。
+    nextTick(() => {
+        isBody.value = true
+    })
+})
+
+//选择了文件
+const uploadFileAdded = () => {
+    emit('fileAdded')
+}
+
+// 文件上传进度
+const uploadFileProgress = (res) => {
+    emit('fileProgress', res)
+}
+// 文件上传成功的回调
+const uploadFileSuccess = (res) => {
+    emit('fileSuccess', res)
+}
+
+//文件上传失败
+const uploadFileError = () => {
+    emit('fileError')
+}
+
+// 文件全部上传完成
+const uploadFileChange = () => {
+    emit('filesChange')
+}
+
+//关闭
+const close = () => {
+    remove()
+    isShow(false)
+}
+
+//清除
+const remove = () => {
+    uploadFileRef.value?.remove()
+}
+
+const cancel = () => {
+    uploadFileRef.value?.cancel()
+}
+
+//是否显示
+const isShow = (res) => {
+    uploadFileRef.value?.isShow(res)
+}
+
+//点击上传按钮
+const btnUpload = () => {
+    uploadFileRef.value?.btnUpload()
+}
+
+// 暴露出去
+defineExpose({
+    close,
+    remove,
+    cancel,
+    isShow,
+    btnUpload
+})
+</script>
+
+<style lang="scss">
+.hc-global-upload-file-box-hide {
+    display: none;
+}
+</style>

+ 39 - 33
src/components/plugins/uploadFile/style/index.scss → src/global/components/hc-upload-file/style/index.scss

@@ -1,4 +1,4 @@
-#global-uploader {
+.hc-global-upload-file-box {
     &:not(.global-uploader-single) {
         position: fixed;
         z-index: 2000;
@@ -7,12 +7,17 @@
         box-sizing: border-box;
     }
     /* 隐藏上传按钮 */
-    #global-uploader-btn {
+    .hc-global-upload-btn {
         position: absolute;
         clip: rect(0, 0, 0, 0);
+        padding: 0;
+        font-size: initial;
+        line-height: initial;
+        border: initial;
+        border-radius: 0;
     }
     .uploader-app {
-        width: 520px;
+        width: 620px;
         .uploader-list {
             position: relative;
             .file-panel {
@@ -60,7 +65,7 @@
                                 position: absolute;
                                 width: 100%;
                                 height: 100%;
-                                background: #e2eeff;
+                                background: #d2f5ea;
                                 transform: translate(-100%);
                             }
                             .uploader-file-info {
@@ -68,31 +73,26 @@
                                 z-index: 1;
                                 height: 100%;
                                 overflow: hidden;
-                                em, i {
-                                    font-style: normal;
-                                }
+                                display: flex;
+                                align-items: center;
                                 .uploader-file-actions,
-                                .uploader-file-meta,
                                 .uploader-file-name,
                                 .uploader-file-size,
                                 .uploader-file-status {
-                                    float: left;
                                     position: relative;
                                     height: 100%;
                                 }
                                 .uploader-file-name {
-                                    width: 45%;
+                                    flex: 1;
                                     overflow: hidden;
                                     white-space: nowrap;
                                     text-overflow: ellipsis;
-                                    text-indent: 14px;
                                     .uploader-file-icon {
-                                        width: 24px;
-                                        height: 24px;
                                         display: inline-block;
                                         vertical-align: top;
-                                        margin-top: 13px;
                                         margin-right: 8px;
+                                        font-size: 22px;
+                                        margin-left: 8px;
                                         &:before {
                                             font-family: remixicon !important;
                                             font-style: normal;
@@ -122,27 +122,29 @@
                                     }
                                 }
                                 .uploader-file-size {
-                                    width: 13%;
-                                    text-indent: 10px;
-                                }
-                                .uploader-file-meta {
-                                    width: 8%;
+                                    width: auto;
+                                    text-align: center;
+                                    margin: 0 24px;
                                 }
                                 .uploader-file-status {
-                                    width: 22%;
-                                    text-indent: 20px;
+                                    width: auto;
+                                    margin: 0 24px;
+                                    text-align: center;
+                                    &.text {
+                                        width: 100px;
+                                    }
                                 }
                                 .uploader-file-actions {
-                                    width: 12%;
+                                    width: auto;
+                                    text-align: center;
+                                    display: flex;
+                                    align-items: center;
+                                    margin-right: 5px;
                                     span {
+                                        flex: 1;
                                         display: none;
-                                        float: left;
-                                        width: 16px;
-                                        height: 16px;
-                                        margin-top: 0;
                                         cursor: pointer;
                                         margin-right: 8px;
-                                        background: initial;
                                         font-size: 18px;
                                         &:before {
                                             font-family: remixicon !important;
@@ -184,7 +186,13 @@
                             display: block;
                         }
                         .uploader-file[status='error'] .uploader-file-progress {
-                            background: #ffe0e0;
+                            background: #f7cac6;
+                        }
+                        .uploader-file[status='success'] .uploader-file-info .uploader-file-status.text {
+                            color: #1bb886;
+                        }
+                        .uploader-file[status='error'] .uploader-file-info .uploader-file-status.text {
+                            color: #ce453b;
                         }
                     }
                 }
@@ -203,11 +211,9 @@
         position: absolute;
         top: 45%;
         left: 50%;
-        transform: translate(-50%, -50%);
         color: #999;
-        svg {
-            vertical-align: text-bottom;
-        }
+        font-size: 16px;
+        transform: translate(-50%, -50%);
     }
     .custom-status {
         position: absolute;
@@ -218,7 +224,7 @@
         z-index: 1;
     }
     &.global-uploader-single {
-        #global-uploader-btn {
+        .hc-global-upload-btn {
             position: relative;
         }
     }

+ 2 - 0
src/global/components/index.js

@@ -28,6 +28,7 @@ import HcReportExperts from './hc-report-experts/index.vue'
 import HcTasksUser from './hc-tasks-user/index.vue'
 import HcLoading from './hc-loading/index.vue'
 import HcOnlineOffice from './hc-online-office/index.vue'
+import HcUploadFile from './hc-upload-file/index.vue'
 
 //注册全局组件
 export const setupComponents = (App) => {
@@ -61,4 +62,5 @@ export const setupComponents = (App) => {
     App.component('HcTasksUser', HcTasksUser)
     App.component('HcLoading', HcLoading)
     App.component('HcOnlineOffice', HcOnlineOffice)
+    App.component('HcUploadFile', HcUploadFile)
 }

+ 0 - 2
src/plugins/bus.js

@@ -1,2 +0,0 @@
-import mitt from 'mitt'
-export default mitt()

+ 26 - 23
src/views/file/collection.vue

@@ -355,18 +355,22 @@
                 </div>
             </template>
         </el-dialog>
+
+        <HcUploadFile ref="HcUploadFileRef"
+                      @fileProgress="HcUploadFileProgress"
+                      @fileSuccess="HcUploadFileSuccess"
+                      @filesChange="HcUploadFileChange"/>
     </div>
 </template>
 
 <script setup>
-import {ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
+import {ref, watch, onMounted, nextTick } from "vue";
 import {useAppStore} from "~src/store";
 import HcTree from "~src/components/tree/hc-tree.vue"
 import notableform from '~src/assets/view/notableform.svg';
 import {delMessage, rowsToId, rowsToIdNumArr} from "~uti/tools";
 import archiveFileApi from "~api/archiveFile/archiveFile";
 import {getArrValue, deepClone, getObjVal} from "js-fast-way"
-import Bus from '~src/plugins/bus.js'
 import tasksApi from '~api/tasks/data';
 import ossApi from "~api/oss";
 
@@ -376,6 +380,7 @@ const projectId = ref(useAppState.getProjectId);
 const contractId = ref(useAppState.getContractId);
 const projectInfo = ref(useAppState.getProjectInfo);
 const isCollapse = ref(useAppState.getCollapse)
+const HcUploadFileRef = ref(null)
 
 //上传进度
 const uploadsLoading = ref(false)
@@ -391,32 +396,30 @@ watch(() => [
 onMounted(() => {
     treeLoading.value = true
     setTableColumns()
-    // 文件上传成功的回调
-    Bus.on('fileSuccess', (res) => {
-        console.log('文件上传成功', res)
-        uploadsChange(res)
-    })
-    // 文件上传进度
-    Bus.on('fileProgress', (res) => {
-        uploadsLoading.value = res
-    })
-    // 文件全部上传成功
-    Bus.on('filesChange', () => {
-        Bus.emit('closeUploader',{})
-    })
-})
-
-onBeforeUnmount(() => {
-    Bus.off('fileSuccess')
-    Bus.off('filesChange')
-    Bus.off('fileProgress')
 })
 
+//打开文件选择框
 const uploadFileClick = () => {
-    // 打开文件选择框
-    Bus.emit('openUploader',{})
+    HcUploadFileRef?.value.btnUpload()
 }
 
+// 文件上传进度
+const HcUploadFileProgress = (res) => {
+    uploadsLoading.value = res
+}
+// 文件上传成功的回调
+const HcUploadFileSuccess = (res) => {
+    console.log('文件上传成功', res)
+    uploadsChange(res)
+}
+
+// 文件全部上传成功
+const HcUploadFileChange = () => {
+    console.log('文件全部上传成功')
+    HcUploadFileRef?.value.close()
+}
+
+
 //树加载
 const treeLoading = ref(false)
 const treeNodeLoading = () => {

+ 45 - 22
src/views/file/records.vue

@@ -233,17 +233,14 @@
             <template #footer>
                 <div class="lr-dialog-footer">
                     <div class="left flex items-center">
-                        <HcFileUpload @change="uploadsChange" @progress="uploadsProgress">
-                            <el-button type="primary" hc-btn :loading="uploadsLoading" :disabled="uploadSaveLoading">
-                                <HcIcon name="add-circle"/>
-                                <span>新增上传</span>
-                            </el-button>
-                        </HcFileUpload>
-
+                        <el-button type="primary" hc-btn @click="uploadFileClick">
+                            <HcIcon name="add-circle"/>
+                            <span>新增上传</span>
+                        </el-button>
                         <el-alert title="提示:每次不超过10份,文件量越多,响应容易超时" type="error" :closable="false"/>
                     </div>
                     <div class="right">
-                        <el-button size="large" @click="batchUploadCancel">
+                        <el-button size="large" @click="batchUploadCancel" :disabled="uploadsLoading">
                             <HcIcon name="close"/>
                             <span>取消</span>
                         </el-button>
@@ -256,6 +253,10 @@
             </template>
         </el-dialog>
 
+        <HcUploadFile ref="HcUploadFileRef"
+                      @fileProgress="HcUploadFileProgress"
+                      @fileSuccess="HcUploadFileSuccess"
+                      @filesChange="HcUploadFileChange"/>
     </div>
 </template>
 
@@ -263,10 +264,9 @@
 import {ref, watch, onMounted,nextTick } from "vue";
 import {useAppStore} from "~src/store";
 import HcTree from "~src/components/tree/hc-tree.vue"
-import HcFileUpload from "./components/HcFileUploadLarge.vue"
 import notableform from '~src/assets/view/notableform.svg';
 import {delMessage, rowsToId, rowsToIdNumArr} from "~uti/tools";
-import {getArrValue, deepClone, downloadBlob} from "js-fast-way"
+import {getArrValue, deepClone, downloadBlob, getObjVal} from "js-fast-way"
 import tasksApi from '~api/tasks/data';
 import ossApi from "~api/oss";
 import archiveFileApi from "~api/archiveFile/archiveFileAuto.js";
@@ -279,6 +279,10 @@ const projectInfo = ref(useAppState.getProjectInfo);
 const isCollapse = ref(useAppState.getCollapse)
 const userInfo = ref(useAppState.getUserInfo)
 
+//上传变量
+const HcUploadFileRef = ref(null)
+const uploadsLoading = ref(false)
+
 //监听
 watch(() => [
     useAppState.getCollapse
@@ -297,6 +301,29 @@ onMounted(() => {
     getSecurityLevel()
 })
 
+
+//打开文件选择框
+const uploadFileClick = () => {
+    HcUploadFileRef?.value.btnUpload()
+}
+
+// 文件上传进度
+const HcUploadFileProgress = (res) => {
+    uploadsLoading.value = res
+}
+// 文件上传成功的回调
+const HcUploadFileSuccess = (res) => {
+    console.log('文件上传成功', res)
+    uploadsChange(res)
+}
+
+// 文件全部上传成功
+const HcUploadFileChange = () => {
+    console.log('文件全部上传成功')
+    HcUploadFileRef?.value.close()
+}
+
+
 //树加载
 const treeLoading = ref(false)
 const treeNodeLoading = () => {
@@ -957,14 +984,12 @@ const setTableUploadColumn = () => {
 const tableUploadData = ref([])
 
 //上传的文件结果
-const uploadsChange = ({fileList}) => {
-    let newArr = []
-    console.log(fileList)
-    for (let i = 0; i < fileList.length; i++) {
-        const item = fileList[i]
+const uploadsChange = (item) => {
+    if (getObjVal(item)) {
+        let newArr = tableUploadData.value
         let name = item['originalName'] || ''
         let fileName = name.substring(0, name.lastIndexOf("."))
-        tableUploadData.value.push({
+        newArr.push({
             projectId: projectId.value,
             contractId: contractId.value,
             nodeId: nodeIds.value,
@@ -981,17 +1006,15 @@ const uploadsChange = ({fileList}) => {
                 filePage: item?.page || '',
                 isApproval:0,
                 isNeedCertification:0,
-                dutyUser:userInfo.value.real_name
+                dutyUser: userInfo.value.real_name
             }]
         })
+        tableUploadData.value = newArr
+    } else {
+        console.log(item)
     }
 }
 
-//上传进度
-const uploadsLoading = ref(false)
-const uploadsProgress = (val) => {
-    uploadsLoading.value = val
-}
 
 //表单下拉数据
 const whetherData = ref([