index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. <template>
  2. <div id="global-uploader" :class="{ 'global-uploader-single': !global }">
  3. <!-- 上传 -->
  4. <HcUploader
  5. ref="uploaderRef"
  6. class="uploader-app"
  7. :options="optionsValue"
  8. :file-status-text="fileStatusText"
  9. :auto-start="false"
  10. @file-added="onFileAdded"
  11. @file-success="onFileSuccess"
  12. @file-progress="onFileProgress"
  13. @file-error="onFileError"
  14. >
  15. <HcUploaderUnsupport/>
  16. <HcUploaderBtn id="global-uploader-btn" ref="uploadBtnRef">选择文件</HcUploaderBtn>
  17. <HcUploaderList v-show="panelShow">
  18. <template #default="{ fileList }">
  19. <div class="file-panel" :class="{ collapse: collapse }">
  20. <div class="file-title">
  21. <div class="title">文件列表 {{fileList.length > 0 ? `(${fileList.length})` : ''}}</div>
  22. <div class="operate">
  23. <el-button :title="collapse ? '展开' : '折叠'" link @click="collapse = !collapse">
  24. <i :class="collapse ? 'ri-fullscreen-line' : 'ri-subtract-line'"/>
  25. </el-button>
  26. <el-button title="关闭" link @click="close">
  27. <i class="ri-close-line"/>
  28. </el-button>
  29. </div>
  30. </div>
  31. <ul class="file-list">
  32. <li v-for="file in fileList" :key="file.id" class="file-item">
  33. <HcUploaderFile ref="files" :class="['file_' + file.id, customStatus]" :file="file" :list="true"/>
  34. </li>
  35. <div v-if="!fileList.length" class="no-file">
  36. <i class="ri-file-text-line" style="font-size: 24px"/>
  37. 暂无待上传文件
  38. </div>
  39. </ul>
  40. </div>
  41. </template>
  42. </HcUploaderList>
  43. </HcUploader>
  44. </div>
  45. </template>
  46. <script>
  47. import {computed, nextTick, onMounted, ref} from 'vue'
  48. import {getTokenHeader} from '~src/api/request/header';
  49. import HcUploader from './components/uploader.vue'
  50. import HcUploaderBtn from './components/btn.vue'
  51. import HcUploaderUnsupport from './components/unsupport.vue'
  52. import HcUploaderList from './components/list.vue'
  53. import HcUploaderFile from './components/file.vue'
  54. import {getArrValue, getObjValue, getFileSuffix} from "js-fast-way";
  55. import {ElNotification} from "element-plus";
  56. import {generateMD5} from './common/md5'
  57. import Bus from '~src/plugins/bus.js'
  58. const acceptType = 'image/png,image/jpg,image/jpeg,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel,application/pdf,.doc,.docx,application/msword'
  59. export default {
  60. name: 'GlobalUploader',
  61. props: {
  62. global: {
  63. type: Boolean,
  64. default: true
  65. }
  66. },
  67. components: {
  68. HcUploader,
  69. HcUploaderBtn,
  70. HcUploaderUnsupport,
  71. HcUploaderList,
  72. HcUploaderFile
  73. },
  74. emits: ['fileAdded', 'fileSuccess', 'fileError', 'filesChange', 'fileProgress'],
  75. setup(props, {emit}) {
  76. const optionsValue = {
  77. target: '/api/blade-resource/largeFile/endpoint/upload-file',
  78. chunkSize: '2048000',
  79. fileParameterName: 'file',
  80. maxChunkRetries: 3,
  81. headers: getTokenHeader(),
  82. // 是否开启服务器分片校验
  83. testChunks: true,
  84. testMethod: 'POST',
  85. // 服务器分片校验函数,秒传及断点续传基础
  86. checkChunkUploadedByResponse: (chunk, message) => {
  87. let skip = false
  88. try {
  89. let objMessage = getObjValue(JSON.parse(message))
  90. if (objMessage['skipUpload']) {
  91. skip = true
  92. } else {
  93. skip = (getArrValue(objMessage['uploaded'])).indexOf(chunk.offset + 1) >= 0
  94. }
  95. } catch (e) {}
  96. return skip
  97. },
  98. query: (file, chunk) => {
  99. return {...file.params}
  100. }
  101. }
  102. const initOptions = ({target, fileName, maxChunk, accept}) => {
  103. // 自定义上传url
  104. if (target) {
  105. uploader.value.opts.target = target
  106. }
  107. // 自定义文件上传参数名
  108. if (fileName) {
  109. uploader.value.opts.fileParameterName = fileName
  110. }
  111. // 并发上传数量
  112. if (maxChunk) {
  113. uploader.value.opts.maxChunkRetries = maxChunk
  114. }
  115. // 自定义文件上传类型
  116. if (accept) {
  117. nextTick(() => {
  118. let input = document.querySelector('#global-uploader-btn input')
  119. input.setAttribute('accept', accept ? accept : acceptType)
  120. })
  121. }
  122. }
  123. const fileStatusText = {
  124. success: '上传成功',
  125. error: '上传失败',
  126. uploading: '上传中',
  127. paused: '已暂停',
  128. waiting: '等待上传'
  129. }
  130. const customStatus = ref('')
  131. const panelShow = ref(false)
  132. const collapse = ref(false)
  133. const uploaderRef = ref()
  134. const uploadBtnRef = ref()
  135. const uploader = computed(() => uploaderRef.value?.uploader)
  136. let customParams = {}
  137. async function onFileAdded(file) {
  138. panelShow.value = true
  139. trigger('fileAdded')
  140. // 将额外的参数赋值到每个文件上,以不同文件使用不同params的需求
  141. file.params = {
  142. ...customParams,
  143. objectType: getFileSuffix(file.name),
  144. fileType: file.fileType,
  145. }
  146. // 计算MD5
  147. const md5 = await computeMD5(file)
  148. startUpload(file, md5)
  149. }
  150. function computeMD5(file) {
  151. // 文件状态设为"计算MD5"
  152. statusSet(file.id, 'md5')
  153. // 暂停文件
  154. file.pause()
  155. // 计算MD5时隐藏”开始“按钮
  156. setResumeStyle(file.id, 'none')
  157. nextTick(() => {
  158. document.querySelector(`.custom-status-${file.id}`).innerText = '校验MD5中'
  159. })
  160. // 开始计算MD5
  161. return new Promise((resolve, reject) => {
  162. generateMD5(file, {
  163. onSuccess(md5) {
  164. statusRemove(file.id)
  165. resolve(md5)
  166. },
  167. onError() {
  168. error(`文件${file.name}读取出错,请检查该文件`)
  169. file.cancel()
  170. statusRemove(file.id)
  171. reject()
  172. }
  173. })
  174. })
  175. }
  176. const setResumeStyle = (id, val = 'none') => {
  177. nextTick(() => {
  178. try {
  179. document.querySelector(`.file_${id} .uploader-file-resume`).style.display = val
  180. } catch (e) {}
  181. })
  182. }
  183. // md5计算完毕,开始上传
  184. const beforeFileNum = ref(0)
  185. function startUpload(file, md5) {
  186. const fileList = uploader.value.fileList;
  187. //判断是否满足条件
  188. const result = fileList.every(({uniqueIdentifier}) => {
  189. return uniqueIdentifier !== md5
  190. })
  191. if (result) {
  192. file.uniqueIdentifier = md5
  193. setResumeStyle(file.id,'')
  194. beforeFileNum.value ++;
  195. file.resume()
  196. trigger('fileProgress', true)
  197. } else {
  198. file.cancel()
  199. error('请不要重复上传相同文件')
  200. }
  201. }
  202. //上传完成
  203. const finishFileNum = ref(0)
  204. function onFileSuccess(rootFile, file, response, chunk) {
  205. let res = JSON.parse(response)
  206. // 服务端自定义的错误(即http状态码为200,但是是错误的情况),这种错误是Uploader无法拦截的
  207. if (res.code !== 200) {
  208. errorFileNum.value ++;
  209. error(res.msg)
  210. // 文件状态设为“失败”
  211. statusSet(file.id, 'failed')
  212. } else {
  213. finishFileNum.value ++;
  214. trigger('fileSuccess', getObjValue(res.data))
  215. }
  216. if (beforeFileNum.value === (finishFileNum.value + errorFileNum.value)) {
  217. trigger('filesChange')
  218. trigger('fileProgress', false)
  219. }
  220. }
  221. function onFileProgress(rootFile, file, chunk) {
  222. console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
  223. }
  224. //上传失败
  225. const errorFileNum = ref(0)
  226. function onFileError(rootFile, file, response, chunk) {
  227. errorFileNum.value ++;
  228. error(response)
  229. trigger('fileError')
  230. }
  231. function close() {
  232. finishFileNum.value = 0
  233. beforeFileNum.value = 0
  234. errorFileNum.value = 0
  235. uploader.value.cancel()
  236. panelShow.value = false
  237. }
  238. //新增的自定义的状态: 'md5'、'merging'、'transcoding'、'failed'
  239. function statusSet(id, status) {
  240. const statusMap = {
  241. md5: {text: '校验MD5', bgc: '#fff'},
  242. merging: {text: '合并中', bgc: '#e2eeff'},
  243. transcoding: {text: '转码中', bgc: '#e2eeff'},
  244. failed: {text: '上传失败', bgc: '#e2eeff'}
  245. }
  246. customStatus.value = status
  247. nextTick(() => {
  248. const statusTag = document.createElement('p')
  249. statusTag.className = `custom-status-${id} custom-status`
  250. statusTag.innerText = statusMap[status].text
  251. statusTag.style.backgroundColor = statusMap[status].bgc
  252. const statusWrap = document.querySelector(`.file_${id} .uploader-file-status`)
  253. statusWrap.appendChild(statusTag)
  254. })
  255. }
  256. function statusRemove(id) {
  257. customStatus.value = ''
  258. nextTick(() => {
  259. const statusTag = document.querySelector(`.custom-status-${id}`)
  260. statusTag.remove()
  261. })
  262. }
  263. function trigger(key, data) {
  264. Bus.emit(key, data)
  265. emit(key, data)
  266. }
  267. function error(msg) {
  268. ElNotification({
  269. title: '错误',
  270. message: msg,
  271. type: 'error',
  272. duration: 2000
  273. })
  274. }
  275. onMounted(() => {
  276. finishFileNum.value = 0
  277. beforeFileNum.value = 0
  278. errorFileNum.value = 0
  279. nextTick(() => {
  280. let input = document.querySelector('#global-uploader-btn input')
  281. input.setAttribute('accept', acceptType)
  282. })
  283. //打开上传器
  284. Bus.on('openUploader', ({params = {}, options = {}}) => {
  285. customParams = params
  286. initOptions(options)
  287. if (uploadBtnRef.value) {
  288. uploadBtnRef.value.$el.click()
  289. }
  290. })
  291. //关闭上传器
  292. Bus.on('closeUploader', () => {
  293. close()
  294. })
  295. })
  296. return {
  297. optionsValue,
  298. initOptions,
  299. fileStatusText,
  300. customStatus,
  301. panelShow,
  302. collapse,
  303. uploaderRef,
  304. uploadBtnRef,
  305. onFileAdded,
  306. onFileSuccess,
  307. onFileProgress,
  308. onFileError,
  309. close
  310. }
  311. }
  312. }
  313. </script>
  314. <style lang="scss">
  315. @import "./style/index";
  316. </style>