index.vue 16 KB

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