index.vue 15 KB

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