美文网首页
上传组件

上传组件

作者: 人猿Jim | 来源:发表于2021-02-22 18:31 被阅读0次

记录一个可拖拽,重传,删除的上传组件


封装自element

import-data.vue

<template>
  <div class="import-data">
    <!-- 拖拽/点击 上传 -->
    <div
      v-if="hasDrag"
      class="c-f-drag">
      <el-upload
        class="upload-demo"
        drag
        :accept="accept"
        action=""
        :multiple="multiple"
        :http-request="GetUploadFile"
        :on-progress = "onProgress"
        :on-success = "onSuccess"
        :on-change="handleChange"
        :file-list="filesList"
        :show-file-list="true"
        :auto-upload="true">
        <!-- <i class="el-icon-upload"></i> -->
        <div class="el-upload__text">
          <img :src="dragFileImg">
        </div>
        <div
          slot="tip"
          class="el-upload__tip">
          <div class="note">点击上传文书或将文件拖拽到这里上传</div>
          <div class="accept">支持扩展名: {{accept}}</div>
        </div>
        <!-- 文件上传进度展示 -->
        <div
          slot="file"
          slot-scope="{file}"
          class="upload-list-item">
          <upload-item
            :percentage="fixedProgress(file)"
            fileUrl=""
            :file="{name: file.name}"
            :status="file.status"
            @handleRefreshIconClick="handleRefreshIconClick(file)"
            @handleCloseIconClick="handleCloseIconClick(file)">
          </upload-item>
        </div>
      </el-upload>
    </div>
  </div>
</template>

<script>
/*
 *@description: 案例筛查
 *@dependencies:
 *@API:
 *@ 参数
 * accept - 上传文件格式
 * hasDrag - 是否展示拖拽
 *@ 事件
 * this.$emit('handleFilterClick', { filePath: this.filePath })
*/
import axios from 'axios'
import dragFileImg from '@/assets/import/dragFile.png'
import regular from '@/regular'
import UploadItem from '../upload-item/upload-item.vue'
import { startMock, getAuthToken } from '@/config'
export default {
  name: 'ImportData',
  components: {
    UploadItem
  },
  mixins: [],
  props: {
    accept: {
      type: String,
      // default: '.doc,.docx,.pdf,.jpg,.zip'
      default: '.zip'
    },
    multiple: {
      type: Boolean,
      default: true
    },
    hasDrag: {
      type: Boolean,
      default: true
    }
  },
  data: function() {
    return {
      isMock: startMock, // 是否是模拟上传
      dragFileImg,
      filesPaths: '', // 已上传的文件的拼接路径
      filesList: [], // 文件列表
      uploadingFiles: [], // 正在上传的文件
      uploadingFileParams: [], // 正在上传文件的params
      mockUplaodFileTimers: {} // 正在模拟上传文件的定时器

    }
  },
  computed: {
    acceptArr() {
      return this.accept.split(',') || []
    }
  },
  watch: {

  },
  created() {
    this.getFileList()
  },
  mounted() {

  },
  methods: {
    // 获取文件类型
    getFileType(name, type) {
      const fileType = name.split('.')[name.split('.').length - 1].toLowerCase()
      if (!type) {
        return fileType
      } else {
        return type.indexOf(fileType) > -1
      }
    },
    // 上传前验证
    handleBefore(file) {
      if (!file) {
        this.$message.warning('请选择文件')
        return false
      }
      const fileType = '.' + this.getFileType(file.name)
      let isJPG = false
      const isLt2M = file.size / 1024 / 1024 < 500
      this.acceptArr.forEach(element => {
        if (element === fileType) {
          isJPG = true
        }
      })
      if (!isJPG) {
        this.$message.error(`上传文件只能是(${this.acceptArr})格式!`)
      }
      if (!isLt2M) {
        this.$message.error('上传文件大小不能超过 500MB!')
      }
      return isJPG && isLt2M
    },
    handleChange(file) {
      console.log('handleChange', file)
      // 模拟上传
      if (this.isMock) {
        this.mockUploadFile(file)
      }
    },
    // 判断文件上传个数
    handleExceed(files, fileList) {
      this.$message.warning(`只能上传 ${this.limit} 个文件!`)
    },
    // 文件列表改变更新父组件文件路径字段
    getFilePath(url) {
      this.$emit('handleUploadClick', url)
    },
    onProgress(event, file, fileList) {
      // console.log(file)
      this.percent = Number(file.percentage.toFixed(0))
    },
    onSuccess(res, file, fileList) {
      console.log(res, file, fileList)
    },
    // 获取上传进度
    fixedProgress(file) {
      if (file.status === 'success') {
        return 100
      }
      return Number(file.percentage.toFixed(0))
    },
    // 文件上传
    GetUploadFile(params, isRefresh = false) {
      if (this.isMock) { return }
      console.log(params, 'params')
      params.file.percentage = 1
      let cancel = null
      const CancelToken = axios.CancelToken
      cancel = new CancelToken(function executor(c) {
        params.file.cancel = c
        // 这个参数 c 就是CancelToken构造函数里面自带的取消请求的函数,这里把该函数当参数用
      })
      if (!isRefresh) {
        params.file.raw = params.file
        this.filesList.push(params.file)
        this.uploadingFiles[params.file.uid] = params.file // 记录正在上传的文件
        this.uploadingFileParams[params.file.uid] = params // 记录正在上传的文件的params
      }
      // 上传
      const file = params.file
      file.percentage = 0
      const formData = new FormData()
      // 上传文件的名称
      formData.append('zipFile', file)
      const config = {
        noLoading: true,
        timeout: 10 * 60 * 1000, // 10分钟
        auth_token: getAuthToken(),
        headers: {
          'Content-Type': 'multipart/form-data;charset=utf-8'
        },
        notMessageErr: true,
        cancelToken: cancel,
        onUploadProgress: progressEvent => {
          const percent = progressEvent.loaded / progressEvent.total * 100 | 0
          params.onProgress({ percent: percent })
          this.$forceUpdate()
        }
      }
      this.$store.dispatch('main/upload', { formData, config }).then((res) => {
        if (res && res.code === 0) {
          const url = res.data
          delete this.uploadingFiles[file.uid] // 清除该文件在正在上传的列表记录
          delete this.uploadingFileParams[file.uid]
          if (this.isFiles) {
            this.getDisposePath(url, file.name, file.size, file.uid)
            this.getFileList()
          } else {
            this.filesPaths = url
          }
          if (this.filesPaths) {
            this.getFilePath(this.filesPaths)
          }
          // 隐藏进度
          // params.onProgress({ percent: 101 }) // 多文件
          // this.percent = 0 // 单文件
          file.status = 'success'
          this.$forceUpdate()
          this.$message({
            type: 'success',
            message: '上传成功'
          })
        }
      }).catch(err => {
        console.log(err)
        this.setStatusFailed(file)
      })
    },
    // 拼接文件url字符串集合
    getDisposePath(url, name, size, uid) {
      // 上传多个文件,文件url后面要加上name用来显示,uid用来进行删除
      if (!this.filesPaths) {
        this.filesPaths = url + '?fileName=' + name + '?size=' + size + '?uid=' + uid
      } else {
        this.filesPaths += ',' + url + '?fileName=' + name + '?size=' + size + '?uid=' + uid
      }
    },
    getFileList() {
      if (this.filesPaths) {
        // let [ ...oldFileList ] = this.fileList;
        const newFileList = []
        let fileList = []
        const hasUid = this.filesPaths.indexOf('uid') > -1
        if (hasUid) {
          fileList = this.filesPaths.match(regular.filePathReg[0])
        } else {
          fileList = this.filesPaths.split(',')
        }
        if (fileList && fileList.length > 0) {
          fileList.forEach((element, index) => {
            if (hasUid) {
              newFileList[index] = {
                url: element.split('?fileName=')[0],
                name: element.split('?fileName=')[1].split('?size=')[0],
                size: element.split('?fileName=')[1].split('?size=')[1].split('?uid=')[0],
                uid: element.split('?uid=')[1]
              }
            } else {
              newFileList[index] = {
                url: element,
                name: element
              }
            }
          })
          for (const uid in this.uploadingFiles) {
            newFileList.push(this.uploadingFiles[uid])
          }
          this.filesList = newFileList
        }
      }
    },
    // 删除文件
    handleRemove(file, fileList) {
      if (file.status === 'uploading') { // 删除下载文件时 触发
        file.cancel()
      }
      fileList.forEach((element, index) => {
        if (element.uid === file.uid) {
          fileList.splice(index, 1)
        }
      })
      delete this.uploadingFiles[file.uid]
      delete this.uploadingFileParams[file.uid]
      this.fileList = fileList
      this.filesPaths = ''
      if (this.fileList.length > 0) {
        // 删除操作之后重新拼接文件url集合
        this.fileList.forEach(element => {
          if (element.url) {
            this.getDisposePath(element.url, element.name, element.size, element.uid)
          }
        })
        this.getFilePath(this.filesPaths)
      } else {
        this.getFilePath(this.filesPaths)
      }
    },
    // 设置状态failed
    setStatusFailed(file) {
      file.status = 'failed'
      this.$forceUpdate()
      // if (this.filesList.length > 0) {
      //   this.filesList.forEach(t_file => {
      //     if (file.uid === t_file.uid) {
      //       t_file.status = 'failed'
      //     }
      //   })
      // }
    },
    // 刷新上传
    handleRefreshIconClick(file) {
      console.log(file)
      // alert(this.isMock)
      if (this.isMock) {
        this.mockUploadFile(file)
      } else {
        file.cancel()
        this.GetUploadFile(this.uploadingFileParams[file.uid], true)
      }
    },
    // 关闭上传
    handleCloseIconClick(file) {
      // console.log(file, this.filesList)
      if (this.isMock) {
        clearInterval(this.mockUplaodFileTimers[file.uid])
        this.mockUplaodFileTimers[file.uid] = null
        this.filesList = this.filesList.filter(file_item => file_item.uid !== file.uid)
      } else {
        this.handleRemove(file, this.filesList)
      }
    },
    // 模拟文件上传
    mockUploadFile(file) {
      // alert('mockUploadFile')
      if (this.mockUplaodFileTimers[file.uid]) {
        clearInterval(this.mockUplaodFileTimers[file.uid])
        this.mockUplaodFileTimers[file.uid] = null
      }
      file.percentage = 0
      file.status = 'uploading'
      this.mockUplaodFileTimers[file.uid] = setInterval(() => {
        file.percentage += 2
        if (file.percentage >= 100) {
          file.status = 'success'
          this.$message.success('上传成功')
          clearInterval(this.mockUplaodFileTimers[file.uid])
          this.mockUplaodFileTimers[file.uid] = null
          // 判断是否上传了两个文件
          if (Object.keys(this.mockUplaodFileTimers).length >= 2) {
            sessionStorage.setItem('isUpload', true)
          }
        }
        this.$forceUpdate()
      }, 100)
    }

  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.import-data {
    .c-f-drag {
        display: flex;
        justify-content: center;

        /* padding: 87px 0 115px 0; */
        padding: 0 0 115px 0;
        border: 1px dashed #bfc3cd;
        border-radius: 4px;

        /* margin-top: 40px; */
        background: #fff;

        .upload-demo {
            width: 100%;
            padding-top: 85px;

            /deep/ .el-upload {
                display: block;
                width: 343px;
                height: 243px;
                margin: 0 auto;
            }

            /deep/ .el-upload-dragger {
                width: 100%;
                height: 100%;
                border: none;

                .el-upload__text {
                    img {
                        width: 343px;
                        // margin-top: 50px;
                    }
                }
            }

            .el-upload__tip {
                margin-top: 14px;
                font-size: $fs16;
                text-align: center;

                .note {
                    color: $tColorContent;
                }

                .accept {
                    margin-top: 15px;
                    color: $tColorSecond;
                }
            }

            /deep/ .el-upload-list {
                display: flex;
                padding: 10px 40px;
                margin-top: 60px;

                .el-upload-list__item {
                    width: 195px;
                    border: none;
                    outline: none;
                    margin-top: 10px;
                    background: #fff;
                    line-height: 1.2;

                    .el-icon-close {
                        position: static;
                        display: inline-block;
                    }

                    +.el-upload-list__item {
                        margin-left: 25px;
                    }
                }
            }
        }
    }
}
</style>

upload-item.vue

<template>
  <div class="upload-item">
    <div>
      <slot></slot>
    </div>
    <!-- 图标 -->
    <div class="u-i-icon el-icon-document"></div>
    <!-- 文件信息 -->
    <div class="u-i-info">
      <div class="u-i-i-top">
        <!-- 文件名 -->
        <div class="u-i-name">
          <abbreviated-display
            ref="abbreviatedDisplay"
            :text="fileName"
            :lines="1">
          </abbreviated-display>
        </div>
        <!-- 操作 -->
        <div
          v-if="fileStatus!=='success'"
          class="u-i-handle">
          <span
            class="u-i-h-icon el-icon-refresh"
            @click.stop="handleRefreshIconClick">
          </span>
          <span
            class="u-i-h-icon el-icon-close"
            @click.stop="handleCloseIconClick">
          </span>
        </div>
      </div>
      <div class="u-i-i-bottom">
        <!-- 进度 -->
        <template v-if="fileStatus!=='success'">
          <el-progress
            v-if="showStatusByfile===''"
            :stroke-width="strokeWidth"
            :percentage="percentage"
            :show-text="false">
            {{percentage}}
          </el-progress>
          <el-progress
            v-else
            :stroke-width="strokeWidth"
            :percentage="percentage"
            :status="showStatusByfile"
            :show-text="false">
            {{percentage}}
          </el-progress>
        </template>
      </div>
    </div>
  </div>
</template>

<script>
/*
 *@description: 文件上传列表项展示组件
 *@dependencies: 说明引入依赖的组件
 *@API:
 *@ 参数
 * percentage
 * status
 * file
 * fileUrl
 *@ 事件
 * handleRefreshIconClick
 * handleCloseIconClick
*/
import AbbreviatedDisplay from '../abbreviated-display/abbreviated-display.vue'
export default {
  name: 'UploadItem',
  components: {
    AbbreviatedDisplay
  },
  mixins: [],
  props: {
    percentage: {
      type: Number,
      default: 0
    },
    status: {
      type: String,
      default: 'ready'
    },
    file: {
      type: Object,
      default: () => ({
      })
    },
    fileUrl: {
      type: String,
      default: ''
    },
    strokeWidth: {
      type: Number,
      default: 2
    }
  },
  data: function() {
    return {
      processStatus: ['ready', 'uploading'],
      fileStatus: this.status
    }
  },
  computed: {
    fileName() {
      return this.file.name || ''
    },
    showStatusByfile() {
      return this.processStatus.includes(this.fileStatus) ? '' : this.fileStatus === 'failed' ? 'exception' : 'success'
    }
  },
  watch: {
    status: {
      handler(val, old) {
        this.fileStatus = val
      },
      deep: true
    },
    fileStatus: {
      handler(val, old) {
        console.log(val, this.processStatus.includes(this.fileStatus), 11111111111)
        if (old === 'uploading' && val === 'success') {
          this.$nextTick(() => {
            this.$refs.abbreviatedDisplay.resetInit()
          })
        }
      },
      immediate: true,
      deep: true
    }
  },
  created() {
  },
  mounted() {
  },
  methods: {
    // 刷新点击
    handleRefreshIconClick() {
      this.$emit('handleRefreshIconClick')
    },
    // 关闭点击
    handleCloseIconClick() {
      this.$emit('handleCloseIconClick')
    }
  }

}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.upload-item {
    display: flex;
    width: 195px;
    color: #616366;
    font-size: $fs14;

    .u-i-icon {
        margin-top: 2px;
        margin-right: 6px;
    }

    .u-i-info {
        display: flex;
        flex: 1;
        flex-direction: column;
        justify-content: flex-start;
        // height: 25px;

        .u-i-i-top {
            position: relative;
            display: flex;
            justify-content: space-between;
            // padding: 3px 40px 3px 0;
            // padding-right: 50px;
            margin-bottom: 8px;

            .u-i-name {
                flex: 1;
            }

            .u-i-handle {
                // position: absolute;
                // top: 1px;
                // right: 0;
                width: 50px;
                text-align: right;

                .u-i-h-icon {
                    font-size: 18px;
                    cursor: pointer;

                    &:hover {
                        opacity: .8;
                    }

                    &:active {
                        opacity: 1;
                    }

                    +.u-i-h-icon {
                        margin-left: 6px;
                    }
                }
            }
        }

        .u-i-i-bottom {
            position: relative;

            /deep/ .el-progress {
                top: 0;
            }
        }
    }
}
</style>

abbreviated-display.vue

<template>
  <div
    ref="abbreviatedDisplay"
    v-initDom
    class="abbreviated-display">
    <div v-if="showTooltip">
      <el-tooltip
        placement="top-start"
        effect="light">
        <div
          slot="content"
          class="popper-content"
          :style="popperStyle">{{text}}</div>
        <span>{{abbreviatedText}}</span>
      </el-tooltip>
    </div>
    <span v-else>{{text}}</span>
  </div>
</template>

<script>
/*
 *@description: 缩略展示(文字超出)
 *@dependencies: 说明引入依赖的组件
 *@version V0.1.0
 *@API:
 *@ 参数
 * text
 * lines
 * popperStyle
 *@ 事件
 *
*/
import Regular from '@/regular/index.js'
import { getDomStyle, getTextWidth, debounce } from '@/utils/common.js'
export default {
  name: 'AbbreviatedDisplay',
  components: {
  },
  directives: {
    initDom: {
      inserted(el, binding, vnode) {
        // console.log(el, 'inserted')
        vnode.context.initContainer(el, binding)
      },
      update(el, binding, vnode) {
        // console.log(el, 'update')
        // vnode.context.initContainer(el, binding)
      },
      componentUpdated(el, binding, vnode) {
        // console.log(el, 'componentUpdated')
      },
      unbind(el, binding, vnode) {
        //
        window.removeEventListener('resize', vnode.context.resetInit)
      }
    }
  },
  mixins: [],
  props: {
    text: {
      type: String,
      default: ''
    },
    lines: {
      type: Number,
      default: 1
    },
    popperStyle: {
      type: Object,
      default: () => {
        return {
          maxWidth: '500px',
          fontSize: '14px',
          lineHeight: '23px'
        }
      }
    }
  },
  data: function() {
    return {
      showTooltip: false,
      textContainer: null, // 文字区域容器
      containerWidth: null, // 保存初始化时的容器宽度
      abbreviatedText: '' // 超出限制 处理
    }
  },
  computed: {
  },
  watch: {
    text: {
      handler(val, old) {
        this.textContainer && this.initContainer(this.textContainer)
      }
    },
    lines: {
      handler(val, old) {
        this.textContainer && this.initContainer(this.textContainer)
      }
    },
    popperStyle: {
      handler(val, old) {
        this.textContainer && this.initContainer(this.textContainer)
      },
      deep: true
    }
  },
  created() {
    this.reg = new RegExp(Regular.Punctuation[0])
  },
  mounted() {
  },
  methods: {
    // 初始化处理
    initContainer(el) {
      this.abbreviatedText = ''
      this.showTooltip = false
      if (!this.textContainer) {
        this.textContainer = el
      }
      if (this.text && this.text.length > 0) {
        const containerStyle = getDomStyle(el)
        const font = containerStyle.font
        const containerWidth = parseFloat(containerStyle.width)
        this.containerWidth = containerStyle.width
        const abbreviatedPLength = getTextWidth('...', font)
        // const maxLength = this.lines * containerWidth - abbreviatedPLength
        // console.log(maxLength, containerStyle.width)
        // console.log(getTextWidth(this.text, font))
        let initLine = 1
        let initLength = initLine === this.lines ? abbreviatedPLength : 0 // 初试计算宽度
        let curFont = '' // 当前循环位置
        let prevFont = '' // 循环前一个位置
        for (let i = 0; i < this.text.length; i++) {
          curFont = this.text[i]
          const fontWidth = getTextWidth(curFont, font)
          initLength += fontWidth
          if (initLength > containerWidth) {
            if (initLine === this.lines) {
              this.abbreviatedText = this.text.substr(0, i) + '...'
              this.showTooltip = true
              break
            } else {
              if (this.reg.test(curFont) || this.reg.test(prevFont)) {
                i -= 2
              } else {
                i--
              }
            }
            // console.log(curFont, '当前的字符', this.text)
            initLine += 1
            if (initLine === this.lines) {
              initLength = abbreviatedPLength
            } else {
              initLength = 0
            }
          }
          prevFont = curFont
        }
      }
      // 防抖提升性能
      window.addEventListener('resize', debounce(this.resetInit, 300))
      this.$once('hook:beforeDestory', () => {
        window.removeEventListener('resize', this.resetInit)
      })
    },
    // 重新初始化
    resetInit() {
      console.log('resetInit')
      const el = this.textContainer
      if (!el) {
        return
      }
      const containerStyle = getDomStyle(el)
      if (this.containerWidth === containerStyle.width) {
        return
      }
      this.initContainer(el)
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.abbreviated-display {
    width: 100%;
    white-space: normal;
    word-break: break-all;
}
</style>

相关文章

网友评论

      本文标题:上传组件

      本文链接:https://www.haomeiwen.com/subject/jhukfltx.html