Procházet zdrojové kódy

新增大文件分片上传 以及 断点续传,更新依赖

ZaiZai před 2 roky
rodič
revize
62b5357f67
27 změnil soubory, kde provedl 1597 přidání a 61 odebrání
  1. 3 0
      package.json
  2. 2 0
      src/App.vue
  3. 0 13
      src/api/modules/upload.js
  4. 18 0
      src/api/request/httpApi.js
  5. 3 0
      src/api/util/mergeSimpleUpload.js
  6. 1 0
      src/components/AppConfig/index.vue
  7. 2 0
      src/components/plugins/uploadFile/common/file-events.js
  8. 56 0
      src/components/plugins/uploadFile/common/md5.js
  9. 27 0
      src/components/plugins/uploadFile/common/utils.js
  10. 65 0
      src/components/plugins/uploadFile/components/btn.vue
  11. 67 0
      src/components/plugins/uploadFile/components/drop.vue
  12. 407 0
      src/components/plugins/uploadFile/components/file.vue
  13. 44 0
      src/components/plugins/uploadFile/components/files.vue
  14. 44 0
      src/components/plugins/uploadFile/components/list.vue
  15. 36 0
      src/components/plugins/uploadFile/components/unsupport.vue
  16. 193 0
      src/components/plugins/uploadFile/components/uploader.vue
  17. binární
      src/components/plugins/uploadFile/images/audio-icon.png
  18. binární
      src/components/plugins/uploadFile/images/image-icon.png
  19. binární
      src/components/plugins/uploadFile/images/text-icon.png
  20. binární
      src/components/plugins/uploadFile/images/video-icon.png
  21. binární
      src/components/plugins/uploadFile/images/zip.png
  22. 8 0
      src/components/plugins/uploadFile/index.js
  23. 338 0
      src/components/plugins/uploadFile/index.vue
  24. 227 0
      src/components/plugins/uploadFile/style/index.scss
  25. 2 0
      src/plugins/bus.js
  26. 39 48
      src/views/file/collection.vue
  27. 15 0
      yarn.lock

+ 3 - 0
package.json

@@ -32,8 +32,11 @@
         "autoprefixer": "^10.4.14",
         "codemirror": "^6.0.1",
         "js-fast-way": "^0.0.8",
+        "mitt": "^3.0.0",
         "postcss": "^8.4.21",
         "sass": "^1.60.0",
+        "simple-uploader.js": "^0.6.0",
+        "spark-md5": "^3.0.2",
         "tailwindcss": "3.1.8",
         "unplugin-auto-import": "^0.15.2",
         "unplugin-vue-components": "^0.24.1",

+ 2 - 0
src/App.vue

@@ -1,6 +1,7 @@
 <template>
     <AppConfig>
         <router-view/>
+        <GlobalUploadFile />
     </AppConfig>
 </template>
 
@@ -8,6 +9,7 @@
 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 - 13
src/api/modules/upload.js

@@ -1,13 +0,0 @@
-import request from "~src/api/request/index";
-
-//上传文件
-export const useUploadFile = ({file, onProgress}) => request({
-    url: '/api/blade-resource/oss/endpoint/upload-file',
-    method: 'post',
-    data: file,
-    onUploadProgress: (progressEvent) => { //原生获取上传进度的事件
-        if(progressEvent.lengthComputable){
-            onProgress(progressEvent);
-        }
-    },
-});

+ 18 - 0
src/api/request/httpApi.js

@@ -1,6 +1,24 @@
 import request from "./index";
 import { getObjValue } from "js-fast-way"
 
+export const HcApi = async (obj) => {
+    return new Promise( (resolve) => {
+        request(obj).then((res) => {
+            resolve({
+                error: false,
+                res: res,
+                data: res?.data,
+            });
+        }).catch((res) => {
+            resolve({
+                error: true,
+                res: res,
+                data: {},
+            });
+        })
+    });
+}
+
 //封装的请求
 export const httpApi = async (obj, message= true) => {
     return new Promise( (resolve) => {

+ 3 - 0
src/api/util/mergeSimpleUpload.js

@@ -0,0 +1,3 @@
+export function mergeSimpleUpload() {
+    return Promise.resolve()
+}

+ 1 - 0
src/components/AppConfig/index.vue

@@ -1,6 +1,7 @@
 <template>
     <el-config-provider :locale="zhCn">
         <slot></slot>
+
     </el-config-provider>
 </template>
 

+ 2 - 0
src/components/plugins/uploadFile/common/file-events.js

@@ -0,0 +1,2 @@
+const events = ['fileProgress', 'fileSuccess', 'fileComplete', 'fileError']
+export default events

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

@@ -0,0 +1,56 @@
+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()
+    }
+  }
+}

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

@@ -0,0 +1,27 @@
+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()}`)
+}

+ 65 - 0
src/components/plugins/uploadFile/components/btn.vue

@@ -0,0 +1,65 @@
+<template>
+    <label class="uploader-btn" ref="btn" v-show="support">
+        <slot></slot>
+    </label>
+</template>
+
+<script>
+import {inject, nextTick, ref, onMounted} from 'vue'
+
+const COMPONENT_NAME = 'uploader-btn'
+
+export default {
+    name: COMPONENT_NAME,
+    props: {
+        directory: {
+            type: Boolean,
+            default: false
+        },
+        single: {
+            type: Boolean,
+            default: false
+        },
+        attrs: {
+            type: Object,
+            default() {
+                return {}
+            }
+        }
+    },
+    setup(props) {
+        const btn = ref(null)
+        const uploader = inject('uploader').proxy.uploader
+        const support = uploader.support
+        onMounted(() => {
+            nextTick(() => {
+                uploader.assignBrowse(btn.value, props.directory, props.single, props.attrs)
+            })
+        })
+        return {
+            btn,
+            support
+        }
+    }
+}
+</script>
+
+<style>
+.uploader-btn {
+    display: inline-block;
+    position: relative;
+    padding: 4px 8px;
+    font-size: 100%;
+    line-height: 1.4;
+    color: #666;
+    border: 1px solid #666;
+    cursor: pointer;
+    border-radius: 2px;
+    background: none;
+    outline: none;
+}
+
+.uploader-btn:hover {
+    background-color: rgba(0, 0, 0, .08);
+}
+</style>

+ 67 - 0
src/components/plugins/uploadFile/components/drop.vue

@@ -0,0 +1,67 @@
+<template>
+    <div class="uploader-drop" :class="dropClass" ref="drop" v-show="support">
+        <slot></slot>
+    </div>
+</template>
+
+<script>
+import {inject, nextTick, ref, onBeforeUnmount} from 'vue'
+
+const COMPONENT_NAME = 'uploader-drop'
+
+export default {
+    name: COMPONENT_NAME,
+    setup() {
+        const uploader = inject('uploader').proxy.uploader
+        let drop = ref(null)
+        let dropClass = ref('')
+        const support = uploader.support
+        const onDragEnter = () => {
+            dropClass = 'uploader-dragover'
+        }
+        const onDragLeave = () => {
+            dropClass = ''
+        }
+        const onDrop = () => {
+            dropClass = 'uploader-droped'
+        }
+        nextTick(() => {
+            const dropEle = drop.value
+            uploader.assignDrop(dropEle)
+            uploader.on('dragenter', onDragEnter)
+            uploader.on('dragleave', onDragLeave)
+            uploader.on('drop', onDrop)
+        })
+        onBeforeUnmount(() => {
+            const dropEle = drop.value
+            uploader.off('dragenter', onDragEnter)
+            uploader.off('dragleave', onDragLeave)
+            uploader.off('drop', onDrop)
+            uploader.unAssignDrop(dropEle)
+        })
+        return {
+            drop,
+            dropClass,
+            support,
+            onDragEnter,
+            onDragLeave,
+            onDrop
+        }
+    }
+}
+</script>
+
+<style>
+.uploader-drop {
+    position: relative;
+    padding: 10px;
+    overflow: hidden;
+    border: 1px dashed #ccc;
+    background-color: #f5f5f5;
+}
+
+.uploader-dragover {
+    border-color: #999;
+    background-color: #f7f7f7;
+}
+</style>

+ 407 - 0
src/components/plugins/uploadFile/components/file.vue

@@ -0,0 +1,407 @@
+<template>
+    <div class="uploader-file" :status="status">
+        <slot
+            :file="file"
+            :list="list"
+            :status="status"
+            :paused="paused"
+            :error="error"
+            :response="response"
+            :average-speed="averageSpeed"
+            :formated-average-speed="formatedAverageSpeed"
+            :current-speed="currentSpeed"
+            :is-complete="isComplete"
+            :is-uploading="isUploading"
+            :size="size"
+            :formated-size="formatedSize"
+            :uploaded-size="uploadedSize"
+            :progress="progress"
+            :progress-style="progressStyle"
+            :progressing-class="progressingClass"
+            :time-remaining="timeRemaining"
+            :formated-time-remaining="formatedTimeRemaining"
+            :type="type"
+            :extension="extension"
+            :file-category="fileCategory"
+        >
+            <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>
+                <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>
+                <div class="uploader-file-actions">
+                    <span class="uploader-file-pause" @click="pause"></span>
+                    <span class="uploader-file-resume" @click="resume">️</span>
+                    <span class="uploader-file-retry" @click="retry"></span>
+                    <span class="uploader-file-remove" @click="remove"></span>
+                </div>
+            </div>
+        </slot>
+    </div>
+</template>
+
+<script>
+import {computed, ref, toRaw, watch, onMounted, onUnmounted, getCurrentInstance} from 'vue'
+import Uploader from 'simple-uploader.js'
+import {secondsToStr} from '../common/utils'
+import events from '../common/file-events'
+
+const COMPONENT_NAME = 'uploader-file'
+
+export default {
+    name: COMPONENT_NAME,
+    props: {
+        file: {
+            type: Object,
+            default() {
+                return {}
+            }
+        },
+        list: {
+            type: Boolean,
+            default: false
+        }
+    },
+    setup(props) {
+        const instance = getCurrentInstance()
+        let handlers = {}
+        let tid = 0
+        const response = ref(null)
+        const paused = ref(false)
+        const error = ref(false)
+        const averageSpeed = ref(0)
+        const currentSpeed = ref(0)
+        const isComplete = ref(false)
+        const isUploading = ref(false)
+        const size = ref(0)
+        const formatedSize = ref('')
+        const uploadedSize = ref(0)
+        const progress = ref(0)
+        const timeRemaining = ref(0)
+        const type = ref('')
+        const extension = ref('')
+        const progressingClass = ref('')
+        const fileCategory = computed(() => {
+            const isFolder = props.file.isFolder
+            let type = isFolder ? 'folder' : 'unknown'
+            const categoryMap = props.file.uploader.opts.categoryMap
+            const typeMap = categoryMap || {
+                image: ['gif', 'jpg', 'jpeg', 'png', 'bmp', 'webp'],
+                video: ['mp4', 'm3u8', 'rmvb', 'avi', 'swf', '3gp', 'mkv', 'flv'],
+                audio: ['mp3', 'wav', 'wma', 'ogg', 'aac', 'flac'],
+                document: ['doc', 'txt', 'docx', 'pages', 'epub', 'pdf', 'numbers', 'csv', 'xls', 'xlsx', 'keynote', 'ppt', 'pptx']
+            }
+            Object.keys(typeMap).forEach((_type) => {
+                const extensions = typeMap[_type]
+                if (extensions.indexOf(extension.value) > -1) {
+                    type = _type
+                }
+            })
+            return type
+        })
+        const progressStyle = computed(() => {
+            progress.value = Math.floor(progress.value * 100)
+            const style = `translateX(${Math.floor(progress.value - 100)}%)`
+            return {
+                progress: `${progress.value}%`,
+                webkitTransform: style,
+                mozTransform: style,
+                msTransform: style,
+                transform: style
+            }
+        })
+        const formatedAverageSpeed = computed(() => {
+            return `${Uploader.utils.formatSize(averageSpeed.value)} / s`
+        })
+        const status = computed(() => {
+            let isError = error
+            if (isComplete.value) {
+                return 'success'
+            } else if (isError.value) {
+                return 'error'
+            } else if (isUploading.value) {
+                return 'uploading'
+            } else if (paused.value) {
+                return 'paused'
+            } else {
+                return 'waiting'
+            }
+        })
+        const statusText = computed(() => {
+            const fileStatusText = props.file.uploader.fileStatusText
+            let txt = status.value
+            if (typeof fileStatusText === 'function') {
+                txt = fileStatusText(status.value, response.value)
+            } else {
+                txt = fileStatusText[status.value]
+            }
+            return txt || status
+        })
+        const formatedTimeRemaining = computed(() => {
+            const file = props.file
+            if (timeRemaining.value === Number.POSITIVE_INFINITY || timeRemaining.value === 0) {
+                return ''
+            }
+            let parsedTimeRemaining = secondsToStr(timeRemaining.value)
+            const parseTimeRemaining = file.uploader.opts.parseTimeRemaining
+            if (parseTimeRemaining) {
+                parsedTimeRemaining = parseTimeRemaining(timeRemaining.value, parsedTimeRemaining)
+            }
+            return parsedTimeRemaining
+        })
+        const actionCheck = () => {
+            paused.value = props.file.paused
+            error.value = props.file.error
+            isUploading.value = props.file.isUploading()
+        }
+        const pause = () => {
+            props.file.pause()
+            actionCheck()
+            fileProgress()
+        }
+        const resume = () => {
+            props.file.resume()
+            actionCheck()
+        }
+        const remove = () => {
+            props.file.cancel()
+        }
+        const retry = () => {
+            props.file.retry()
+            actionCheck()
+        }
+        const processResponse = (message) => {
+            let res = message
+            try {
+                res = JSON.parse(message)
+            } catch (e) {
+            }
+            response.value = res
+        }
+        const fileEventsHandler = (event, args) => {
+            const rootFile = args[0]
+            const file = args[1]
+            const target = props.list ? rootFile : file
+            if (toRaw(props.file) === toRaw(target)) {
+                if (props.list && event === 'fileSuccess') {
+                    processResponse(args[2])
+                    return
+                }
+                instance.setupState[event](...args)
+            }
+        }
+        const fileProgress = () => {
+            progress.value = props.file.progress()
+            averageSpeed.value = props.file.averageSpeed
+            currentSpeed.value = props.file.currentSpeed
+            timeRemaining.value = props.file.timeRemaining()
+            uploadedSize.value = props.file.sizeUploaded()
+            actionCheck()
+        }
+        const fileSuccess = (rootFile, file, message) => {
+            if (rootFile) {
+                processResponse(message)
+            }
+            fileProgress()
+            error.value = false
+            isComplete.value = true
+            isUploading.value = false
+        }
+        const fileComplete = () => {
+            fileSuccess()
+        }
+        const fileError = (rootFile, file, message) => {
+            fileProgress()
+            processResponse(message)
+            error.value = true
+            isComplete.value = false
+            isUploading.value = false
+        }
+        watch(status, (newStatus, oldStatus) => {
+            if (oldStatus && newStatus === 'uploading' && oldStatus !== 'uploading') {
+                tid = setTimeout(() => {
+                    progressingClass.value = 'uploader-file-progressing'
+                }, 200)
+            } else {
+                clearTimeout(tid)
+                progressingClass.value = ''
+            }
+        })
+        onMounted(() => {
+            paused.value = props.file['paused']
+            error.value = props.file['error']
+            averageSpeed.value = props.file['averageSpeed']
+            currentSpeed.value = props.file['currentSpeed']
+            isComplete.value = props.file.isComplete()
+            isUploading.value = props.file.isUploading()
+            size.value = props.file.getSize()
+            formatedSize.value = props.file.getFormatSize()
+            uploadedSize.value = props.file.sizeUploaded()
+            progress.value = props.file.progress()
+            timeRemaining.value = props.file.timeRemaining()
+            type.value = props.file.getType()
+            extension.value = props.file.getExtension()
+            const eventHandler = (event) => {
+                handlers[event] = (...args) => {
+                    fileEventsHandler(event, args)
+                }
+                return handlers[event]
+            }
+            events.forEach((event) => {
+                props.file.uploader.on(event, eventHandler(event))
+            })
+        })
+        onUnmounted(() => {
+            events.forEach((event) => {
+                props.file.uploader.off(event, handlers[event])
+            })
+            handlers = null
+        })
+        return {
+            response,
+            paused,
+            error,
+            averageSpeed,
+            currentSpeed,
+            isComplete,
+            isUploading,
+            size,
+            formatedSize,
+            uploadedSize,
+            progress,
+            timeRemaining,
+            type,
+            extension,
+            progressingClass,
+            fileCategory,
+            progressStyle,
+            formatedAverageSpeed,
+            status,
+            statusText,
+            formatedTimeRemaining,
+            actionCheck,
+            pause,
+            resume,
+            remove,
+            retry,
+            processResponse,
+            fileEventsHandler,
+            fileProgress,
+            fileSuccess,
+            fileComplete,
+            fileError
+        }
+    }
+}
+</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;
+}
+
+.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;
+}
+</style>

+ 44 - 0
src/components/plugins/uploadFile/components/files.vue

@@ -0,0 +1,44 @@
+<template>
+    <div class="uploader-files">
+        <slot :files="files">
+            <ul>
+                <li v-for="file in files" :key="file.id">
+                    <uploader-file :file="file"></uploader-file>
+                </li>
+            </ul>
+        </slot>
+    </div>
+</template>
+
+<script>
+import {inject, computed} from 'vue'
+import UploaderFile from './file.vue'
+
+const COMPONENT_NAME = 'uploader-files'
+
+export default {
+    name: COMPONENT_NAME,
+    components: {
+        UploaderFile
+    },
+    setup() {
+        const uploader = inject('uploader').proxy
+
+        return {
+            files: computed(() => uploader.files)
+        }
+    }
+}
+</script>
+
+<style>
+.uploader-files {
+    position: relative;
+}
+
+.uploader-files > ul {
+    list-style: none;
+    margin: 0;
+    padding: 0
+}
+</style>

+ 44 - 0
src/components/plugins/uploadFile/components/list.vue

@@ -0,0 +1,44 @@
+<template>
+    <div class="uploader-list">
+        <slot :file-list="fileList">
+            <ul>
+                <li v-for="file in fileList" :key="file.id">
+                    <uploader-file :file="file" :list="true"/>
+                </li>
+            </ul>
+        </slot>
+    </div>
+</template>
+
+<script>
+import {inject, computed} from 'vue'
+import UploaderFile from './file.vue'
+
+const COMPONENT_NAME = 'uploader-list'
+
+export default {
+    name: COMPONENT_NAME,
+    components: {
+        UploaderFile
+    },
+    setup() {
+        const uploader = inject('uploader').proxy
+
+        return {
+            fileList: computed(() => uploader.fileList)
+        }
+    }
+}
+</script>
+
+<style>
+.uploader-list {
+    position: relative;
+}
+
+.uploader-list > ul {
+    list-style: none;
+    margin: 0;
+    padding: 0
+}
+</style>

+ 36 - 0
src/components/plugins/uploadFile/components/unsupport.vue

@@ -0,0 +1,36 @@
+<template>
+    <div class="uploader-unsupport" v-show="!support">
+        <slot>
+            <p>
+                非常抱歉,您的浏览器不支持此功能,建议您下载最新的edge浏览器或者chrome浏览器。
+                <a href="http://www.w3.org/TR/FileAPI/">the HTML5 File API</a> along with <a
+                href="http://www.w3.org/TR/FileAPI/#normalization-of-params">file slicing</a>.
+            </p>
+        </slot>
+    </div>
+</template>
+
+<script>
+import {inject} from 'vue'
+
+const COMPONENT_NAME = 'uploader-unsupport'
+
+export default {
+    name: COMPONENT_NAME,
+    setup() {
+        const uploader = inject('uploader').proxy.uploader
+        const support = uploader.support
+        return {
+            support
+        }
+    }
+}
+</script>
+
+<style>
+.uploader-unsupport {
+    position: relative;
+    z-index: 10;
+    overflow: hidden;
+}
+</style>

+ 193 - 0
src/components/plugins/uploadFile/components/uploader.vue

@@ -0,0 +1,193 @@
+<template>
+    <div class="uploader">
+        <slot :files="files" :file-list="fileList" :started="started">
+            <uploader-unsupport/>
+            <uploader-drop>
+                <p>拖动文件到这里上传</p>
+                <uploader-btn>选择文件</uploader-btn>
+                <uploader-btn :directory="true">选择文件夹</uploader-btn>
+            </uploader-drop>
+            <uploader-list/>
+        </slot>
+    </div>
+</template>
+
+<script>
+import {provide, ref, onUnmounted, getCurrentInstance} from 'vue'
+import Uploader from 'simple-uploader.js'
+import {kebabCase} from '../common/utils'
+import UploaderBtn from './btn.vue'
+import UploaderDrop from './drop.vue'
+import UploaderUnsupport from './unsupport.vue'
+import UploaderList from './list.vue'
+import UploaderFiles from './files.vue'
+import UploaderFile from './file.vue'
+
+const COMPONENT_NAME = 'uploader'
+const FILE_ADDED_EVENT = 'fileAdded'
+const FILES_ADDED_EVENT = 'filesAdded'
+const UPLOAD_START_EVENT = 'uploadStart'
+
+const ALL_EVENTS = [
+    'change',
+    'dragover',
+    'dragenter',
+    'dragleave',
+    'file-success',
+    'file-complete',
+    'file-progress',
+    'file-added',
+    'files-added',
+    'files-submitted',
+    'file-removed',
+    'file-retry',
+    'file-error',
+    'upload-start',
+    'complete'
+]
+
+export default {
+    name: COMPONENT_NAME,
+    props: {
+        options: {
+            type: Object,
+            default() {
+                return {}
+            }
+        },
+        autoStart: {
+            type: Boolean,
+            default: true
+        },
+        fileStatusText: {
+            type: [Object, Function],
+            default() {
+                return {
+                    success: 'success',
+                    error: 'error',
+                    uploading: 'uploading',
+                    paused: 'paused',
+                    waiting: 'waiting'
+                }
+            }
+        },
+        onFileAdded: Function,
+        onFilesAdded: Function
+    },
+    emits: ALL_EVENTS,
+    setup(props, {emit}) {
+        const started = ref(false)
+        const files = ref([])
+        const fileList = ref([])
+        const instance = getCurrentInstance()
+        let uploader = new Uploader(props.options)
+        const uploadStart = () => {
+            started.value = true
+        }
+        const fileAdded = (file) => {
+            const _file = file
+            if (props.onFileAdded) {
+                const ignored = props.onFileAdded(_file)
+                if (ignored === false || _file.ignored) {
+                    return false
+                }
+            } else {
+                emit(kebabCase(FILE_ADDED_EVENT), _file)
+                if (_file.ignored) {
+                    // is ignored, filter it
+                    return false
+                }
+            }
+        }
+        const filesAdded = (files, fileList) => {
+            if (props.onFilesAdded) {
+                const ignored = props.onFilesAdded(files, fileList)
+                if (ignored === false || (files.ignored || fileList.ignored)) {
+                    return false
+                }
+            } else {
+                emit(kebabCase(FILES_ADDED_EVENT), files, fileList)
+                if (files.ignored || fileList.ignored) {
+                    // is ignored, filter it
+                    return false
+                }
+            }
+        }
+        const fileRemoved = () => {
+            files.value = [...uploader.files]
+            fileList.value = [...uploader.fileList]
+        }
+        const filesSubmitted = () => {
+            files.value = [...uploader.files]
+            fileList.value = [...uploader.fileList]
+            if (props.autoStart) {
+                uploader.upload()
+            }
+        }
+        const allEvent = (...args) => {
+            const name = args[0]
+            const EVENTSMAP = {
+                [FILE_ADDED_EVENT]: true,
+                [FILES_ADDED_EVENT]: true,
+                [UPLOAD_START_EVENT]: 'uploadStart'
+            }
+            const handler = EVENTSMAP[name]
+            if (handler) {
+                if (handler === true) {
+                    return
+                }
+                instance.setupState[handler](...args.slice(1))
+            }
+            args[0] = kebabCase(name)
+            emit(...args)
+        }
+
+        props.options.initialPaused = !props.autoStart
+        uploader.fileStatusText = props.fileStatusText
+        uploader.on('catchAll', allEvent)
+        uploader.on(FILE_ADDED_EVENT, fileAdded)
+        uploader.on(FILES_ADDED_EVENT, filesAdded)
+        uploader.on('fileRemoved', fileRemoved)
+        uploader.on('filesSubmitted', filesSubmitted)
+        // uploader[UPLOAD_START_EVENT] = uploadStart
+
+        onUnmounted(() => {
+            uploader.off('catchAll', allEvent)
+            uploader.off(FILE_ADDED_EVENT, fileAdded)
+            uploader.off(FILES_ADDED_EVENT, filesAdded)
+            uploader.off('fileRemoved', fileRemoved)
+            uploader.off('filesSubmitted', filesSubmitted)
+            uploader = null
+        })
+
+        provide('uploader', instance)
+
+        return {
+            uploader,
+            started,
+            files,
+            fileList,
+            uploadStart,
+            fileAdded,
+            filesAdded,
+            fileRemoved,
+            filesSubmitted,
+            allEvent
+        }
+    },
+    components: {
+        UploaderBtn,
+        UploaderDrop,
+        UploaderUnsupport,
+        UploaderList,
+        UploaderFiles,
+        UploaderFile
+    }
+}
+</script>
+
+<style>
+.uploader {
+    position: relative;
+}
+</style>

binární
src/components/plugins/uploadFile/images/audio-icon.png


binární
src/components/plugins/uploadFile/images/image-icon.png


binární
src/components/plugins/uploadFile/images/text-icon.png


binární
src/components/plugins/uploadFile/images/video-icon.png


binární
src/components/plugins/uploadFile/images/zip.png


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

@@ -0,0 +1,8 @@
+/*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'
+*/

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

@@ -0,0 +1,338 @@
+<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} 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.value
+            // 计算MD5
+            const md5 = await computeMD5(file)
+            startUpload(file, md5)
+        }
+
+        function computeMD5(file) {
+            // 文件状态设为"计算MD5"
+            statusSet(file.id, 'md5')
+            // 暂停文件
+            file.pause()
+            // 计算MD5时隐藏”开始“按钮
+            setResumeStyle(file.id, 'none')
+            // 开始计算MD5
+            return new Promise((resolve, reject) => {
+                generateMD5(file, {
+                    onProgress(currentChunk, chunks) {
+                        // 实时展示MD5的计算进度
+                        nextTick(() => {
+                            document.querySelector(`.custom-status-${file.id}`).innerText = '校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%'
+                        })
+                    },
+                    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>

+ 227 - 0
src/components/plugins/uploadFile/style/index.scss

@@ -0,0 +1,227 @@
+#global-uploader {
+    &:not(.global-uploader-single) {
+        position: fixed;
+        z-index: 2000;
+        right: 10px;
+        bottom: 0;
+        box-sizing: border-box;
+    }
+    /* 隐藏上传按钮 */
+    #global-uploader-btn {
+        position: absolute;
+        clip: rect(0, 0, 0, 0);
+    }
+    .uploader-app {
+        width: 520px;
+        .uploader-list {
+            position: relative;
+            .file-panel {
+                background-color: #fff;
+                border: 1px solid #e2e2e2;
+                border-radius: 7px 7px 0 0;
+                box-shadow: 0 0 10px #0003;
+                .file-title {
+                    display: flex;
+                    height: 40px;
+                    line-height: 40px;
+                    padding: 0 15px;
+                    border-bottom: 1px solid #ddd;
+                    .operate {
+                        flex: 1;
+                        text-align: right;
+                        .el-button {
+                            --el-button-hover-link-text-color: #1ECC95;
+                            i {
+                                font-size: 19px;
+                            }
+                            + .el-button {
+                                margin-left: 8px;
+                            }
+                        }
+                    }
+                }
+                .file-list {
+                    position: relative;
+                    height: 240px;
+                    overflow-x: hidden;
+                    overflow-y: auto;
+                    background-color: #fff;
+                    transition: all .3s;
+                    font-size: 14px;
+                    .file-item {
+                        background-color: #fff;
+                        .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: translate(-100%);
+                            }
+                            .uploader-file-info {
+                                position: relative;
+                                z-index: 1;
+                                height: 100%;
+                                overflow: hidden;
+                                em, i {
+                                    font-style: normal;
+                                }
+                                .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%;
+                                    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;
+                                        &:before {
+                                            font-family: remixicon !important;
+                                            font-style: normal;
+                                            -webkit-font-smoothing: antialiased;
+                                            content: '' !important;
+                                        }
+                                        &[icon='image']:before {
+                                            content: "\ee4a" !important;
+                                            color: #8044de;
+                                        }
+                                        &[icon='audio']:before {
+                                            content: "\ecf6" !important;
+                                            color: #1ECC95;
+                                        }
+                                        &[icon='video']:before {
+                                            content: "\ed20" !important;
+                                            color: #e54d42;
+                                        }
+                                        &[icon='document']:before {
+                                            content: "\ed0e" !important;
+                                            color: #0081ff;
+                                        }
+                                        &[icon='unknown']:before {
+                                            content: "\ed12" !important;
+                                            color: #a5673f;
+                                        }
+                                    }
+                                }
+                                .uploader-file-size {
+                                    width: 13%;
+                                    text-indent: 10px;
+                                }
+                                .uploader-file-meta {
+                                    width: 8%;
+                                }
+                                .uploader-file-status {
+                                    width: 22%;
+                                    text-indent: 20px;
+                                }
+                                .uploader-file-actions {
+                                    width: 12%;
+                                    span {
+                                        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;
+                                            font-style: normal;
+                                            -webkit-font-smoothing: antialiased;
+                                            content: "";
+                                            transition: color 0.3s;
+                                        }
+                                        &:hover {
+                                            color: #1ECC95;
+                                        }
+                                    }
+                                    .uploader-file-pause:before {
+                                        content: "\efd7";
+                                    }
+                                    .uploader-file-resume:before {
+                                        content: "\f00a";
+                                    }
+                                    .uploader-file-retry:before {
+                                        content: "\f064";
+                                    }
+                                    .uploader-file-remove {
+                                        display: block;
+                                        font-size: 22px;
+                                        &:before {
+                                            content: "\eb98";
+                                        }
+                                    }
+                                }
+                            }
+                            &.md5 .uploader-file-info .uploader-file-actions .uploader-file-resume {
+                                display: none;
+                            }
+                        }
+                        .uploader-file[status='uploading'] .uploader-file-info .uploader-file-actions .uploader-file-pause,
+                        .uploader-file[status='waiting'] .uploader-file-info .uploader-file-actions .uploader-file-pause,
+                        .uploader-file[status='paused'] .uploader-file-info .uploader-file-actions .uploader-file-resume,
+                        .uploader-file[status='error'] .uploader-file-info .uploader-file-actions .uploader-file-retry {
+                            display: block;
+                        }
+                        .uploader-file[status='error'] .uploader-file-progress {
+                            background: #ffe0e0;
+                        }
+                    }
+                }
+                &.collapse {
+                    .file-title {
+                        background-color: #e7ecf2;
+                    }
+                    .file-list {
+                        height: 0;
+                    }
+                }
+            }
+        }
+    }
+    .no-file {
+        position: absolute;
+        top: 45%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        color: #999;
+        svg {
+            vertical-align: text-bottom;
+        }
+    }
+    .custom-status {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        z-index: 1;
+    }
+    &.global-uploader-single {
+        #global-uploader-btn {
+            position: relative;
+        }
+    }
+}
+
+

+ 2 - 0
src/plugins/bus.js

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

+ 39 - 48
src/views/file/collection.vue

@@ -303,18 +303,13 @@
             <template #footer>
                 <div class="lr-dialog-footer">
                     <div class="left flex items-center">
-                        <!--input ref="uploadFileRef" type="file" multiple
-                               accept="image/png,image/jpg,image/jpeg,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,application/pdf,.doc,.docx,application/msword"
-                               @change="uploadFile" /-->
-                        <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>
                     </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>
@@ -364,15 +359,14 @@
 </template>
 
 <script setup>
-import {ref, watch, onMounted,nextTick } from "vue";
+import {ref, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
 import {useAppStore} from "~src/store";
 import HcTree from "~src/components/tree/hc-tree.vue"
-import HcFileUpload from "./components/HcFileUpload.vue"
 import notableform from '~src/assets/view/notableform.svg';
 import {delMessage, rowsToId, rowsToIdNumArr} from "~uti/tools";
 import archiveFileApi from "~api/archiveFile/archiveFile";
-import {useUploadFile} from "~api/upload";
-import {getArrValue, deepClone} from "js-fast-way"
+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";
 
@@ -383,6 +377,9 @@ const contractId = ref(useAppState.getContractId);
 const projectInfo = ref(useAppState.getProjectInfo);
 const isCollapse = ref(useAppState.getCollapse)
 
+//上传进度
+const uploadsLoading = ref(false)
+
 //监听
 watch(() => [
     useAppState.getCollapse
@@ -394,33 +391,32 @@ 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',{})
+    })
 })
 
-//
-const uploadFile = async (event) => {
-    const files = event.target.files;
-    if (files.length > 0) {
-        for (let i = 0; i < files.length; i++) {
-            await uploadFileApi(files[i])
-        }
-    }
-    console.log(files)
-}
+onBeforeUnmount(() => {
+    Bus.off('fileSuccess')
+    Bus.off('filesChange')
+    Bus.off('fileProgress')
+})
 
-const uploadFileApi = async (fileData) => {
-    const formData = new FormData();
-    formData.append('file', fileData);
-    useUploadFile({
-        file: formData,
-        onProgress: (e) => {
-            console.log(e)
-        },
-    }).then(res => {
-        console.log(res)
-    })
+const uploadFileClick = () => {
+    // 打开文件选择框
+    Bus.emit('openUploader',{})
 }
 
-
 //树加载
 const treeLoading = ref(false)
 const treeNodeLoading = () => {
@@ -1072,11 +1068,10 @@ const setTableUploadColumn = () => {
 const tableUploadData = ref([])
 
 //上传的文件结果
-const uploadsChange = ({fileList}) => {
-    let newArr = []
-    const sheet = sheetType.value, source = sheetSourceType.value
-    for (let i = 0; i < fileList.length; i++) {
-        const item = fileList[i]
+const uploadsChange = (item) => {
+    if (getObjVal(item)) {
+        let newArr = tableUploadData.value
+        const sheet = sheetType.value, source = sheetSourceType.value
         let name = item['originalName'] || ''
         let fileName = name.substring(0, name.lastIndexOf("."))
         newArr.push({
@@ -1098,14 +1093,10 @@ const uploadsChange = ({fileList}) => {
             pdfFileUrl: item?.pdfUrl || '',
             filePage: item?.page || '',
         })
+        tableUploadData.value = newArr
+    } else {
+        console.log(item)
     }
-    tableUploadData.value = newArr
-}
-
-//上传进度
-const uploadsLoading = ref(false)
-const uploadsProgress = (val) => {
-    uploadsLoading.value = val
 }
 
 //表单下拉数据

+ 15 - 0
yarn.lock

@@ -950,6 +950,11 @@ minimist@^1.2.6:
   resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz"
   integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
 
+mitt@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
+  integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
+
 mlly@^1.1.1, mlly@^1.2.0:
   version "1.2.0"
   resolved "https://registry.npmjs.org/mlly/-/mlly-1.2.0.tgz"
@@ -1171,6 +1176,11 @@ scule@^1.0.0:
   resolved "https://registry.npmjs.org/scule/-/scule-1.0.0.tgz"
   integrity sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==
 
+simple-uploader.js@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/simple-uploader.js/-/simple-uploader.js-0.6.0.tgz#55724264c89ff70df4a9855bcd28eb92f0b88ced"
+  integrity sha512-EXN+o+LD6PVnfzTq/usP8k8yYrI6wKrAx8e+fPcPLVzzttonkyn1KT+Ycx5JnPBSnp6lpiVhNG4JhDJucdPnhA==
+
 sortablejs@1.14.0:
   version "1.14.0"
   resolved "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz"
@@ -1196,6 +1206,11 @@ sourcemap-codec@^1.4.8:
   resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz"
   integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
 
+spark-md5@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.2.tgz#7952c4a30784347abcee73268e473b9c0167e3fc"
+  integrity sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==
+
 split.js@^1.6.5:
   version "1.6.5"
   resolved "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz"