123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- <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>
|