OfflineExamUpload.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. <template>
  2. <div style="width: 350px">
  3. <div v-if="selectedFileType === 'IMAGE'" class="total-images">
  4. <div
  5. v-for="(item, index) in uploadFileList"
  6. :key="index"
  7. class="demo-upload-list"
  8. >
  9. <img
  10. :ref="'image' + index"
  11. class="image-small"
  12. :data-src="blobToSrc(item, index)"
  13. />
  14. <div class="demo-upload-list-cover">
  15. <Icon
  16. type="ios-eye-outline"
  17. size="30"
  18. @click.native="handleView('.total-images', index)"
  19. ></Icon>
  20. <Icon
  21. type="ios-trash-outline"
  22. size="30"
  23. style="position: absolute; top: 0px"
  24. @click.native="handleRemoveTotal(index)"
  25. ></Icon>
  26. </div>
  27. </div>
  28. </div>
  29. <Upload
  30. ref="uploadComp"
  31. action
  32. :headers="headers"
  33. :data="{ fileType: fileType }"
  34. :before-upload="handleBeforeUpload"
  35. :format="uploadFileFormat"
  36. :accept="uploadFileAccept"
  37. :on-format-error="handleFormatError"
  38. :on-success="handleSuccess"
  39. :on-error="handleError"
  40. >
  41. <div v-if="selectedFileType !== 'IMAGE'">
  42. <i-button
  43. icon="ios-cloud-upload-outline"
  44. class="qm-primary-button"
  45. style=""
  46. @click="clickUpload"
  47. >
  48. 选择文件
  49. </i-button>
  50. <span v-if="uploadFileList.length > 0">{{
  51. uploadFileList[0].name
  52. }}</span>
  53. </div>
  54. <div v-else>
  55. <div
  56. v-if="uploadFileList.length < 6"
  57. class="demo-upload-list plus"
  58. @click="clickUpload"
  59. >
  60. +
  61. </div>
  62. </div>
  63. </Upload>
  64. <div>
  65. <i-button
  66. icon="ios-cloud-upload-outline"
  67. class="qm-primary-button"
  68. style="width: 40%; margin-right: 20px"
  69. :disabled="uploadFileList.length === 0"
  70. :loading="uploading"
  71. @click="uploadFiles"
  72. >
  73. 确认上传
  74. </i-button>
  75. <i-button
  76. class="qm-primary-button"
  77. style="width: 40%"
  78. @click="$emit('close-modal')"
  79. >
  80. 取消上传
  81. </i-button>
  82. </div>
  83. </div>
  84. </template>
  85. <script>
  86. import MD5 from "js-md5";
  87. import "viewerjs/dist/viewer.css";
  88. import Viewer from "viewerjs";
  89. export default {
  90. name: "EcsOfflineUpload",
  91. props: {
  92. course: {
  93. type: Object,
  94. default() {
  95. return {};
  96. },
  97. },
  98. selectedFileType: {
  99. type: String,
  100. default: "",
  101. },
  102. uploadFileFormat: {
  103. type: Array,
  104. default: () => [],
  105. },
  106. uploadFileAccept: {
  107. type: String,
  108. default: "",
  109. },
  110. },
  111. data() {
  112. return {
  113. headers: {
  114. token: window.sessionStorage.getItem("token"),
  115. key: window.localStorage.getItem("key"),
  116. },
  117. file: null,
  118. fileType: null,
  119. loadingStatus: false,
  120. uploadFileList: [],
  121. uploading: false,
  122. // uploadFileFormat: [],
  123. // uploadFileAccept: "",
  124. };
  125. },
  126. watch: {
  127. selectedFileType() {
  128. this.uploadFileList = [];
  129. },
  130. },
  131. async created() {
  132. // const res = await this.$http.get(
  133. // "/api/ecs_exam_work/exam/getExamPropertyFromCacheByStudentSession/" +
  134. // this.course.examId +
  135. // `/OFFLINE_UPLOAD_FILE_TYPE`
  136. // );
  137. // this.uploadFileFormat =
  138. // (res.data.OFFLINE_UPLOAD_FILE_TYPE &&
  139. // JSON.parse(res.data.OFFLINE_UPLOAD_FILE_TYPE)) ||
  140. // [];
  141. // this.uploadFileFormat = this.uploadFileFormat.map((v) => v.toLowerCase());
  142. // this.uploadFileAccept = this.uploadFileFormat
  143. // .map((v) => "application/" + v)
  144. // .join();
  145. // if (this.uploadFileAccept.includes("zip")) {
  146. // this.uploadFileAccept = ".zip," + this.uploadFileAccept;
  147. // }
  148. },
  149. methods: {
  150. clickUpload(e) {
  151. // console.log(e);
  152. e.preventDefault();
  153. if (!this.selectedFileType) {
  154. this.$Notice.warning({
  155. title: "请先选择上传文件类型",
  156. // desc: file.name + " 文件是pdf文档,但文件名后缀不是.pdf!"
  157. });
  158. e.stopPropagation();
  159. return false;
  160. }
  161. if (this.selectedFileType !== "IMAGE") this.uploadFileList = [];
  162. },
  163. fileFormatCheck(file, resolve, reject) {
  164. function getMimetype(signature) {
  165. switch (signature) {
  166. case "89504E47":
  167. return "image/png";
  168. case "47494638":
  169. return "image/gif";
  170. case "25504446":
  171. return "application/pdf";
  172. case "FFD8FFE0":
  173. case "FFD8FFE1":
  174. case "FFD8FFE2":
  175. case "FFD8FFE3":
  176. return "image/jpeg";
  177. case "504B0304":
  178. return "application/zip";
  179. case "504B34":
  180. return "application/zip";
  181. default:
  182. return "Unknown filetype";
  183. }
  184. }
  185. const filereader = new FileReader();
  186. let uploads = [];
  187. filereader.onloadend = (evt) => {
  188. if (evt.target.readyState === FileReader.DONE) {
  189. const uint = new Uint8Array(evt.target.result);
  190. let bytes = [];
  191. uint.forEach((byte) => {
  192. bytes.push(byte.toString(16));
  193. });
  194. const hex = bytes.join("").toUpperCase();
  195. uploads.push({
  196. filename: file.name,
  197. filetype: file.type ? file.type : "Unknown/Extension missing",
  198. binaryFileType: getMimetype(hex),
  199. hex: hex,
  200. });
  201. if (!getMimetype(hex).toUpperCase().includes(this.selectedFileType)) {
  202. this.$Notice.warning({
  203. title: "文件内容与所选文件类型不一致",
  204. });
  205. resolve("和所选文件类型不一致");
  206. }
  207. if (["application/pdf"].includes(getMimetype(hex))) {
  208. if (!file.name.endsWith(".pdf")) {
  209. this.loadingStatus = false;
  210. this.$Notice.warning({
  211. title: "文件内容与文件的后缀不一致",
  212. // desc: file.name + " 文件是pdf文档,但文件名后缀不是.pdf!"
  213. });
  214. this.file = null;
  215. reject("文件内容与文件的后缀不一致,请确认文件是pdf文档!");
  216. } else {
  217. resolve(true);
  218. }
  219. } else if (["application/zip"].includes(getMimetype(hex))) {
  220. if (!file.name.endsWith(".zip")) {
  221. this.loadingStatus = false;
  222. // this.$refs.uploadComp.fileList.splice(0);
  223. // this.$refs.uploadComp.fileList = [];
  224. this.$Notice.warning({
  225. title: "文件内容与文件的后缀不一致",
  226. // desc: file.name + " 文件是zip压缩包,但文件名后缀不是.zip!"
  227. });
  228. this.file = null;
  229. reject("文件内容与文件的后缀不一致,请确认文件是zip压缩包!");
  230. } else {
  231. resolve(true);
  232. }
  233. } else if (["image/jpeg"].includes(getMimetype(hex))) {
  234. if (file.name.endsWith(".jpeg") || file.name.endsWith(".jpg")) {
  235. resolve(true);
  236. } else {
  237. this.loadingStatus = false;
  238. // this.$refs.uploadComp.fileList.splice(0);
  239. // this.$refs.uploadComp.fileList = [];
  240. this.$Notice.warning({
  241. title: "文件内容与文件的后缀不一致",
  242. // desc: file.name + " 文件是zip压缩包,但文件名后缀不是.zip!"
  243. });
  244. this.file = null;
  245. reject("文件内容与文件的后缀不一致,请确认文件是jpeg文件!");
  246. }
  247. } else if (["image/png"].includes(getMimetype(hex))) {
  248. if (!file.name.endsWith(".png")) {
  249. this.loadingStatus = false;
  250. // this.$refs.uploadComp.fileList.splice(0);
  251. // this.$refs.uploadComp.fileList = [];
  252. this.$Notice.warning({
  253. title: "文件内容与文件的后缀不一致",
  254. // desc: file.name + " 文件是zip压缩包,但文件名后缀不是.zip!"
  255. });
  256. this.file = null;
  257. reject("文件内容与文件的后缀不一致,请确认文件是png文件!");
  258. } else {
  259. resolve(true);
  260. }
  261. } else {
  262. console.log("binary file type check: not zip or pdf");
  263. window._hmt.push([
  264. "_trackEvent",
  265. "离线考试页面",
  266. "上传作答",
  267. "文件格式非zip或pdf",
  268. ]);
  269. this.$Notice.warning({
  270. title: "作答文件损坏",
  271. desc:
  272. file.name +
  273. " 文件无法以 " +
  274. this.uploadFileFormat.join(" 或 ") +
  275. " 格式读取。",
  276. });
  277. this.file = null;
  278. this.loadingStatus = false;
  279. reject("作答文件损坏");
  280. }
  281. }
  282. };
  283. const blob = file.slice(0, 4);
  284. filereader.readAsArrayBuffer(blob);
  285. },
  286. fileSizeCheck(file, resolve, reject) {
  287. const maxSize = this.selectedFileType === "IMAGE" ? 5 : 30;
  288. if (file.size > maxSize * 1024 * 1024) {
  289. this.$Notice.warning({
  290. title: "超出文件大小限制",
  291. desc: file.name + ` 太大,作答文件不能超过${maxSize}MB.`,
  292. });
  293. reject("附件大小超出限制");
  294. } else {
  295. resolve(true);
  296. }
  297. },
  298. handleSuccess() {
  299. // window._hmt.push(["_trackEvent", "离线考试页面", "上传作答", "上传成功"]);
  300. // this.file = null;
  301. // this.loadingStatus = false;
  302. // this.$Message.success({
  303. // content: "上传成功",
  304. // duration: 5,
  305. // closable: true,
  306. // });
  307. // this.uploadFileList = [];
  308. // this.$emit("close-modal");
  309. // this.$emit("reload-list");
  310. },
  311. handleError(error, file) {
  312. window._hmt.push(["_trackEvent", "离线考试页面", "上传作答", "上传失败"]);
  313. this.file = null;
  314. this.loadingStatus = false;
  315. console.log(error);
  316. this.$Message.error({
  317. content: (file && file.desc) || "上传失败",
  318. duration: 15,
  319. closable: true,
  320. });
  321. },
  322. handleFormatError(file) {
  323. this.file = null;
  324. this.loadingStatus = false;
  325. this.$Notice.warning({
  326. title: "作答文件格式不对",
  327. desc:
  328. file.name +
  329. " 文件格式不对,请选择 " +
  330. this.uploadFileFormat.join(" 或 ") +
  331. " 文件。",
  332. });
  333. },
  334. handleMaxSize(file) {
  335. this.file = null;
  336. this.loadingStatus = false;
  337. this.$Notice.warning({
  338. title: "超出文件大小限制",
  339. desc: file.name + " 太大,作答文件不能超过30M.",
  340. });
  341. },
  342. async handleBeforeUpload(file) {
  343. const suffix = file.name.split(".").pop();
  344. if (suffix.toLowerCase() !== suffix) {
  345. this.$Notice.error({
  346. title: "文件名后缀必须是小写",
  347. desc: file.name + " 文件名后缀必须是小写。",
  348. });
  349. return Promise.reject("file suffix should be lower case");
  350. }
  351. // this.uploadFileList.push(file);
  352. // const oldLength = this.uploadFileList.length;
  353. // await new Promise((resolve) => setTimeout(() => resolve(), 1000));
  354. // const newLength = this.uploadFileList.length;
  355. // if (oldLength !== newLength) {
  356. // console.log("还有新的文件要上传,取消本次上传");
  357. // throw "取消";
  358. // }
  359. // console.log(this.uploadFileList);
  360. if (this.uploadFileList.length > 1) {
  361. if (
  362. this.uploadFileList.map((v) => v.type).includes("application/pdf")
  363. ) {
  364. console.log("PDF文件只允许单独上传");
  365. throw "取消";
  366. }
  367. if (
  368. this.uploadFileList.map((v) => v.type).includes("application/zip")
  369. ) {
  370. console.log("ZIP文件只允许单独上传");
  371. throw "取消";
  372. }
  373. }
  374. console.log("上传");
  375. // return;
  376. // if (file.type.includes("/pdf")) {
  377. // this.fileType = "pdf";
  378. // } else if (file.type.includes("/zip")) {
  379. // this.fileType = "zip";
  380. // }
  381. this.file = file;
  382. this.loadingStatus = true;
  383. const format = await new Promise((resolve, reject) =>
  384. this.fileFormatCheck(file, resolve, reject)
  385. );
  386. if (format !== true) {
  387. console.log({ format });
  388. throw "文件类型检测不通过";
  389. }
  390. const fileSizeCheckResult = await new Promise((resolve, reject) =>
  391. this.fileSizeCheck(file, resolve, reject)
  392. );
  393. if (fileSizeCheckResult !== true) {
  394. console.log({ fileSizeCheckResult });
  395. throw "文件大小检测不通过";
  396. }
  397. this.uploadFileList.push(file);
  398. throw "stop uploading by <Upload>";
  399. },
  400. async uploadFiles() {
  401. if (this.uploadFileList.length === 0) {
  402. return;
  403. }
  404. if (this.course.offlineFiles) {
  405. const res = await new Promise((resolve, reject) => {
  406. this.$Modal.confirm({
  407. title: "已有作答附件,是否覆盖?",
  408. onCancel: () => reject(-1),
  409. onOk: () => resolve(),
  410. });
  411. });
  412. if (res === -1) {
  413. return false;
  414. }
  415. }
  416. this.uploading = true;
  417. let params = new FormData();
  418. params.append("examRecordDataId", this.course.examRecordDataId);
  419. params.append("fileType", this.selectedFileType);
  420. async function blobToArray(blob) {
  421. return new Promise((resolve) => {
  422. var reader = new FileReader();
  423. reader.addEventListener("loadend", function () {
  424. // reader.result contains the contents of blob as a typed array
  425. resolve(reader.result);
  426. });
  427. reader.readAsArrayBuffer(blob);
  428. });
  429. }
  430. for (const file of this.uploadFileList) {
  431. const ab = await blobToArray(file);
  432. params.append("fileArray", file);
  433. params.append("fileMd5Array", MD5(ab));
  434. }
  435. // for (let f of this.fileList) {
  436. // params.append("fileArray", f.raw);
  437. // }
  438. // //先对文件md5进行排序(按索引正序排列)
  439. // this.summaryList.sort((a, b) => a.index - b.index);
  440. // let summaries = [];
  441. // for (let s of this.summaryList) {
  442. // summaries.push(s.summary);
  443. // }
  444. // params.append("fileMd5Array", summaries);
  445. const upMsg = this.$Message.info({
  446. content: "上传中...",
  447. duration: 30,
  448. closable: true,
  449. });
  450. try {
  451. await this.$http.post(
  452. "/api/branch_ecs_oe_admin/offlineExam/batchSubmitPaper",
  453. params,
  454. { headers: { "Content-Type": "multipart/form-data" } }
  455. );
  456. } catch (error) {
  457. this.logger({
  458. action: "离线考试附件没有正常上传",
  459. detail: error,
  460. errorJSON: JSON.stringify(error, (key, value) =>
  461. key === "token" ? "" : value
  462. ),
  463. errorName: error.name,
  464. errorMessage: error.message,
  465. errorStack: error.stack,
  466. });
  467. throw error;
  468. } finally {
  469. this.uploading = false;
  470. // this.$Message.destroy();
  471. upMsg();
  472. }
  473. this.$Message.success({
  474. content: "上传成功",
  475. duration: 5,
  476. closable: true,
  477. });
  478. this.uploadFileList = [];
  479. this.$emit("close-modal");
  480. this.$emit("reload-list");
  481. },
  482. async blobToSrc(item, index) {
  483. var fr = new FileReader();
  484. fr.onload = (eve) => {
  485. const e = this.$refs["image" + index];
  486. e[0].src = eve.target.result;
  487. };
  488. fr.readAsDataURL(item);
  489. },
  490. handleView(imagesClass, index) {
  491. const viewer = new Viewer(document.querySelector(imagesClass), {
  492. container: "#app",
  493. zIndex: 99999,
  494. title: false,
  495. toolbar: {
  496. zoomIn: 1,
  497. zoomOut: 1,
  498. oneToOne: 1,
  499. reset: 1,
  500. prev: 1,
  501. play: {
  502. show: 0,
  503. size: "large",
  504. },
  505. next: 1,
  506. rotateLeft: 1,
  507. rotateRight: 1,
  508. flipHorizontal: 1,
  509. flipVertical: 1,
  510. },
  511. ready() {
  512. // viewer.zoomTo(1);
  513. viewer.view(index);
  514. // console.log("once");
  515. },
  516. hidden() {
  517. viewer.destroy();
  518. },
  519. });
  520. viewer.show();
  521. },
  522. handleRemoveTotal(index) {
  523. this.uploadFileList.splice(index, 1);
  524. },
  525. },
  526. };
  527. </script>
  528. <style lang="postcss">
  529. .list .ivu-upload-select {
  530. width: 100%;
  531. }
  532. /* .image-small {
  533. width: 100px;
  534. height: 100px;
  535. } */
  536. .demo-upload-list {
  537. display: inline-block;
  538. width: 100px;
  539. height: 100px;
  540. text-align: center;
  541. line-height: 100px;
  542. border: 1px solid transparent;
  543. border-radius: 4px;
  544. overflow: hidden;
  545. background: #fff;
  546. position: relative;
  547. box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
  548. margin-right: 4px;
  549. /* cursor: move; */
  550. }
  551. .demo-upload-list img {
  552. width: 100%;
  553. height: 100%;
  554. }
  555. .plus {
  556. font-size: 48px;
  557. }
  558. .plus:hover {
  559. cursor: pointer;
  560. color: blueviolet;
  561. }
  562. .demo-upload-list-cover {
  563. display: none;
  564. position: absolute;
  565. top: 0;
  566. bottom: 0;
  567. left: 0;
  568. right: 0;
  569. background: rgba(0, 0, 0, 0.6);
  570. }
  571. .demo-upload-list:hover .demo-upload-list-cover {
  572. display: block;
  573. }
  574. .demo-upload-list-cover i {
  575. color: #fff;
  576. font-size: 20px;
  577. cursor: pointer;
  578. margin: 0 2px;
  579. }
  580. </style>