index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. <!--
  2. * @Author: zhangyu
  3. * @Date: 2019-11-28 10:05:28
  4. * @Info:
  5. * @LastEditTime: 2019-12-16 15:30:27
  6. -->
  7. <template>
  8. <div id="global-uploader">
  9. <!-- 上传 -->
  10. <uploader
  11. ref="uploader"
  12. :options="options"
  13. :autoStart="false"
  14. @file-added="onFileAdded"
  15. @file-success="onFileSuccess"
  16. @file-progress="onFileProgress"
  17. @file-error="onFileError"
  18. class="uploader-app">
  19. <uploader-unsupport></uploader-unsupport>
  20. <uploader-btn id="global-uploader-btn" :attrs="attrs" ref="uploadBtn">选择文件</uploader-btn>
  21. <uploader-list v-show="panelShow">
  22. <div class="file-panel" slot-scope="props" :class="{'collapse': collapse}">
  23. <div class="file-title">
  24. <h2>文件列表</h2>
  25. <div class="operate">
  26. <el-button @click="fileListShow" style="color:#999;" type="text" :title="collapse ? '折叠':'展开' ">
  27. <i class="iconfont" :class="collapse ? 'icon-shenglvehao': 'icon-shenglvehao'"></i>
  28. </el-button>
  29. <el-button @click="close" style="color:#999;" type="text" title="关闭">
  30. <i class="iconfont icon-guanbi"></i>
  31. </el-button>
  32. </div>
  33. </div>
  34. <ul class="file-list" v-show="collapse">
  35. <el-scrollbar style="height:100%;">
  36. <li v-for="file in props.fileList" :key="file.id">
  37. <uploader-file :class="'file_' + file.id" ref="files" :file="file" :list="true"></uploader-file>
  38. </li>
  39. <div class="no-file" v-if="!props.fileList.length"><i class="iconfont icon-wushuju"></i> 暂无待上传文件</div>
  40. </el-scrollbar>
  41. </ul>
  42. </div>
  43. </uploader-list>
  44. </uploader>
  45. </div>
  46. </template>
  47. <script>
  48. /**
  49. * 全局上传插件
  50. * 调用方法:Bus.$emit('openUploader', {}) 打开文件选择框,参数为需要传递的额外参数
  51. * 监听函数:Bus.$on('fileAdded', fn); 文件选择后的回调
  52. * Bus.$on('fileSuccess', fn); 文件上传成功的回调
  53. */
  54. import Bus from '@/utils/bus.js';
  55. import SparkMD5 from 'spark-md5';
  56. import { getUploadId, mergeMultipart } from '@/api/uploader';
  57. import request from "@/api/model/file.js";
  58. import { mapGetters, mapMutations } from 'vuex'
  59. export default {
  60. data() {
  61. return {
  62. options: {
  63. target: '/image-service/common/multipart_upload',
  64. chunkSize: 1*1024*1024,
  65. fileParameterName: 'file',
  66. maxChunkRetries: 5,
  67. testChunks: false, //是否开启服务器分片校验
  68. // 服务器分片校验函数,秒传及断点续传基础
  69. // checkChunkUploadedByResponse: function (chunk, message) {
  70. // let objMessage = JSON.parse(message);
  71. // if (objMessage.skipUpload) {
  72. // return true;
  73. // }
  74. // return (objMessage.uploaded || []).indexOf(chunk.offset + 1) >= 0
  75. // },
  76. // headers: {
  77. // Authorization: Ticket.get() && "Bearer " + Ticket.get().access_token
  78. // },
  79. query: this.getFileQuery
  80. },
  81. attrs: {
  82. // accept: ACCEPT_CONFIG.getAll()
  83. },
  84. panelShow: false, //选择文件后,展示上传panel
  85. collapse: false,
  86. }
  87. },
  88. mounted() {
  89. Bus.$on('openUploader', (query, file) => {
  90. if(!this.uploaderList.some(item => {return item.name === file.name})){
  91. if(query.uploadId){
  92. // this.params = query || {};
  93. file.uploadId = query.uploadId
  94. file.modelId = query.modelId? query.modelId: ''
  95. this.uploader.addFile(file)
  96. this.setUploaderList(this.uploader.files.map(item => {
  97. item.modelId = item.file.modelId?item.file.modelId:''
  98. return item
  99. }))
  100. } else {
  101. getUploadId({
  102. systemId: query.systemId?query.systemId:'revit',
  103. secret: query.secret?query.secret:'63afbef6906c342b',
  104. overwrite: query.overwrite? query.overwrite: false,
  105. key: file.name
  106. }, res => {
  107. if(res.UploadId){
  108. // this.params = Object.assign(query, { uploadId: res.UploadId }) || {};
  109. file = Object.assign(file, {
  110. ...query,
  111. uploadId: res.UploadId
  112. })
  113. this.uploader.addFile(file)
  114. this.setUploaderList(this.uploader.files.map(item => {
  115. item.modelId = item.file.modelId?item.file.modelId:''
  116. return item
  117. }))
  118. } else {
  119. this.$message.error(`请求分片上传接口失败!`);
  120. }
  121. })
  122. }
  123. }else {
  124. this.$message.error(`该文件在上传列表中已存在!`);
  125. }
  126. // if (this.$refs.uploadBtn) {
  127. // document.getElementById('global-uploader-btn').click();
  128. // // $('#global-uploader-btn').click();
  129. // }
  130. });
  131. },
  132. computed: {
  133. ...mapGetters('layout', ['uploaderList']),
  134. //Uploader实例
  135. uploader() {
  136. return this.$refs.uploader.uploader;
  137. }
  138. },
  139. methods: {
  140. ...mapMutations('layout', ['setUploaderList']),
  141. onFileAdded(file) {
  142. this.panelShow = true;
  143. this.collapse = true;
  144. this.computeMD5(file);
  145. Bus.$emit('fileAdded');
  146. },
  147. onFileProgress(rootFile, file, chunk) {
  148. console.log(`上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${chunk.endByte / 1024 / 1024}`)
  149. },
  150. onFileSuccess(rootFile, file, response, chunk) {
  151. let res = JSON.parse(response);
  152. // 服务器自定义的错误(即虽返回200,但是是错误的情况),这种错误是Uploader无法拦截的
  153. if (!res.Result || res.Result != 'success') {
  154. this.$message({ message: res.ResultMsg?res.ResultMsg:'上传失败!', type: 'error' });
  155. // 文件状态设为“失败”
  156. this.statusSet(file.id, 'failed');
  157. return
  158. }
  159. // 如果服务端返回需要合并
  160. console.log('获取分片数',file.chunks.length)
  161. if (parseInt(res.TotalCount) >= file.chunks.length) {
  162. // 文件状态设为“合并中”
  163. this.statusSet(file.id, 'merging');
  164. if(file.file.modelId){
  165. //模型文件合并专用接口
  166. request.mergeModelFile({ ModelId: file.file.modelId, UploadId: file.file.uploadId }, res => {
  167. Bus.$emit('modelStatusChange')
  168. // 文件合并成功
  169. Bus.$emit('fileSuccess');
  170. this.statusRemove(file.id);
  171. })
  172. } else {
  173. mergeMultipart({
  174. systemId: file.file.systemId?file.file.systemId:'revit',
  175. secret: file.file.secret?file.file.secret:'63afbef6906c342b',
  176. uploadId: file.file.uploadId
  177. }, res => {
  178. // 文件合并成功
  179. Bus.$emit('fileSuccess');
  180. this.statusRemove(file.id);
  181. })
  182. }
  183. // 不需要合并
  184. } else {
  185. Bus.$emit('fileSuccess');
  186. console.log('上传成功');
  187. }
  188. },
  189. onFileError(rootFile, file, response, chunk) {
  190. this.$message({
  191. message: response,
  192. type: 'error'
  193. })
  194. },
  195. /**
  196. * 计算md5,实现断点续传及秒传
  197. * @param file
  198. */
  199. computeMD5(file) {
  200. let fileReader = new FileReader();
  201. let time = new Date().getTime();
  202. let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
  203. let currentChunk = 0;
  204. const chunkSize = 1 * 1024 * 1024;
  205. let chunks = Math.floor(file.size / chunkSize);
  206. let spark = new SparkMD5.ArrayBuffer();
  207. // 文件状态设为"计算MD5"
  208. this.statusSet(file.id, 'md5');
  209. file.pause();
  210. loadNext();
  211. fileReader.onload = (e => {
  212. spark.append(e.target.result);
  213. if (currentChunk < chunks) {
  214. currentChunk++;
  215. loadNext();
  216. // 实时展示MD5的计算进度
  217. this.$nextTick(() => {
  218. document.getElementsByClassName(`myStatus_${file.id}`)[0].innerText = '校验MD5 '+ ((currentChunk/chunks)*100).toFixed(0)+'%'
  219. // $(`.myStatus_${file.id}`).text('校验MD5 '+ ((currentChunk/chunks)*100).toFixed(0)+'%')
  220. })
  221. } else {
  222. let md5 = spark.end();
  223. file.file.md5 = md5;
  224. this.computeMD5Success(md5, file);
  225. console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
  226. }
  227. });
  228. fileReader.onerror = function () {
  229. this.error(`文件${file.name}读取出错,请检查该文件`)
  230. file.cancel();
  231. };
  232. function loadNext() {
  233. let start = currentChunk * chunkSize;
  234. let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
  235. fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
  236. }
  237. },
  238. computeMD5Success(md5, file) {
  239. // 将自定义参数直接加载uploader实例的opts上
  240. console.log(this.uploader)
  241. // Object.assign(this.uploader.opts, {
  242. // query: {
  243. // uploadId: file.file.uploadId,
  244. // md5
  245. // }
  246. // })
  247. // file.uniqueIdentifier = this.params.uploadId;
  248. file.resume();
  249. this.statusRemove(file.id);
  250. },
  251. //获取文件参数
  252. getFileQuery(file, chunk) {
  253. return {
  254. uploadId: file.file.uploadId,
  255. md5: file.file.md5
  256. }
  257. },
  258. fileListShow() {
  259. this.collapse = this.collapse? false: true;
  260. // let $list = $('#global-uploader .file-list');
  261. // let $list = document.querySelector('#global-uploader .file-list')
  262. // if ($list.style.display == '' && $list.style.display == 'none') {
  263. // $list.style.display == 'block';//显示
  264. // this.collapse = false;
  265. // // $list.slideUp();//隐藏
  266. // // this.collapse = true;
  267. // } else {
  268. // $list.style.display == 'none';//隐藏
  269. // this.collapse = true;
  270. // // $list.slideDown();//显示
  271. // // this.collapse = false;
  272. // }
  273. },
  274. close() {
  275. if(this.uploader.isUploading()) {
  276. this.$alert(
  277. "您有文件正在上传,关闭会中断上传,是否继续?",
  278. "提示",
  279. {
  280. confirmButtonText: "确定",
  281. callback: action => {
  282. console.log(action);
  283. if (action == "confirm") {
  284. this.uploader.cancel();
  285. this.panelShow = false;
  286. this.uploader.files = [];
  287. this.setUploaderList([]);
  288. Bus.$emit('modelStatusChange')
  289. } else { }
  290. }
  291. }
  292. );
  293. } else {
  294. this.uploader.cancel();
  295. this.panelShow = false;
  296. }
  297. },
  298. /**
  299. * 新增的自定义的状态: 'md5'、'transcoding'、'failed'
  300. * @param id
  301. * @param status
  302. */
  303. statusSet(id, status) {
  304. let statusMap = {
  305. md5: {
  306. text: '校验MD5',
  307. bgc: '#fff'
  308. },
  309. merging: {
  310. text: '合并中',
  311. bgc: '#e2eeff'
  312. },
  313. transcoding: {
  314. text: '转码中',
  315. bgc: '#e2eeff'
  316. },
  317. failed: {
  318. text: '上传失败',
  319. bgc: '#e2eeff'
  320. }
  321. }
  322. this.$nextTick(() => {
  323. let node = document.createElement("P");
  324. let att = document.createAttribute("class");
  325. att.value = `status_style myStatus_${id}`;
  326. let text = document.createTextNode(`${statusMap[status].text}`);
  327. node.appendChild(text);
  328. node.setAttributeNode(att);
  329. node.style.backgroundColor = `${statusMap[status].bgc}`
  330. document.querySelector(`.file_${id} .uploader-file-status`).appendChild(node);
  331. // $(`<p class="myStatus_${id}"></p>`).appendTo(`.file_${id} .uploader-file-status`).css({
  332. // 'position': 'absolute',
  333. // 'top': '0',
  334. // 'left': '0',
  335. // 'right': '0',
  336. // 'bottom': '0',
  337. // 'zIndex': '1',
  338. // 'backgroundColor': statusMap[status].bgc
  339. // }).text(statusMap[status].text);
  340. })
  341. },
  342. statusRemove(id) {
  343. this.$nextTick(() => {
  344. let dom = document.getElementsByClassName(`myStatus_${id}`)[0];
  345. dom.parentNode.removeChild(dom);
  346. })
  347. },
  348. error(msg) {
  349. this.$notify({
  350. title: '错误',
  351. message: msg,
  352. type: 'error',
  353. duration: 2000
  354. })
  355. }
  356. },
  357. watch: {},
  358. destroyed() {
  359. Bus.$off('openUploader');
  360. },
  361. components: {}
  362. }
  363. </script>
  364. <style scoped lang="scss">
  365. #global-uploader {
  366. position: fixed;
  367. z-index: 20;
  368. top: auto;
  369. right: 15px;
  370. bottom: 0;
  371. .uploader-app {
  372. width: 633px;
  373. }
  374. .file-panel {
  375. background-color: #fff;
  376. border: 1px solid #e2e2e2;
  377. border-radius: 7px 7px 0 0;
  378. box-shadow: 0 0 10px rgba(0, 0, 0, .2);
  379. .file-title {
  380. display: flex;
  381. height: 40px;
  382. line-height: 40px;
  383. padding: 0 15px;
  384. border-bottom: 1px solid #ddd;
  385. h2{
  386. font-size: 14px;
  387. font-style: normal;
  388. cursor: text;
  389. font-weight: 200;
  390. color: #666;
  391. }
  392. .operate {
  393. flex: 1;
  394. text-align: right;
  395. }
  396. }
  397. .file-list {
  398. position: relative;
  399. height: 350px;
  400. overflow-x: hidden;
  401. overflow-y: auto;
  402. background-color: #fff;
  403. /deep/ .el-scrollbar__wrap{
  404. overflow-x: hidden;
  405. }
  406. > li {
  407. background-color: #fff;
  408. }
  409. /deep/ .status_style{
  410. position: absolute;
  411. top: 0;
  412. left: 0;
  413. right: 0;
  414. bottom: 0;
  415. z-index: 1;
  416. }
  417. }
  418. // &.collapse {
  419. // .file-title {
  420. // background-color: #E7ECF2;
  421. // }
  422. // }
  423. }
  424. .no-file {
  425. position: absolute;
  426. top: 50%;
  427. left: 50%;
  428. transform: translate(-50%, -50%);
  429. font-size: 16px;
  430. text-align: center;
  431. }
  432. /deep/.uploader-file-icon {
  433. &:before {
  434. content: '' !important;
  435. }
  436. // &[icon=image] {
  437. // background: url(./images/image-icon.png);
  438. // }
  439. // &[icon=video] {
  440. // background: url(./images/video-icon.png);
  441. // }
  442. // &[icon=document] {
  443. // background: url(./images/text-icon.png);
  444. // }
  445. }
  446. /deep/ .uploader-file-actions > span {
  447. margin-right: 6px;
  448. }
  449. }
  450. /* 隐藏上传按钮 */
  451. #global-uploader-btn {
  452. position: absolute;
  453. clip: rect(0, 0, 0, 0);
  454. }
  455. </style>