index.vue 19 KB

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