OfflineExamUploadCug.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. <template>
  2. <div>
  3. <Upload
  4. ref="uploadComp"
  5. :headers="headers"
  6. :data="{ fileType: fileType }"
  7. :before-upload="handleBeforeUpload"
  8. :action="
  9. '/api/branch_ecs_oe_admin/offlineExam/submitPaper?examRecordDataId=' +
  10. course.examRecordDataId
  11. "
  12. :max-size="1024 * 30"
  13. :format="uploadFileFormat"
  14. :accept="uploadFileAccept"
  15. :on-format-error="handleFormatError"
  16. :on-exceeded-size="handleMaxSize"
  17. :on-success="handleSuccess"
  18. :on-error="handleError"
  19. :show-upload-list="false"
  20. >
  21. <i-button
  22. icon="ios-cloud-upload-outline"
  23. class="qm-primary-button"
  24. style="width: 100%;"
  25. >
  26. 上传作答
  27. </i-button>
  28. </Upload>
  29. <div v-if="file !== null && loadingStatus">
  30. 待上传文件: {{ file.name }}
  31. <i-button :loading="loadingStatus">
  32. {{ loadingStatus ? "上传中..." : "上传" }}
  33. </i-button>
  34. </div>
  35. <Modal v-model="showPreview" fullscreen footer-hide :closable="false">
  36. <!-- <div slot="header"></div> -->
  37. <img :id="previewId" class="preview-image" />
  38. </Modal>
  39. </div>
  40. </template>
  41. <script>
  42. import { printCurrentPage } from "./imageToPdf";
  43. export default {
  44. name: "EcsOfflineUploadCug",
  45. props: {
  46. course: {
  47. type: Object,
  48. default() {
  49. return {};
  50. },
  51. },
  52. },
  53. data() {
  54. return {
  55. headers: {
  56. token: window.sessionStorage.getItem("token"),
  57. key: window.localStorage.getItem("key"),
  58. },
  59. file: null,
  60. fileType: null,
  61. loadingStatus: false,
  62. uploadFileFormat: [],
  63. uploadFileAccept: "",
  64. // imageSrc
  65. previewImage: null,
  66. showPreview: false,
  67. previewId: "previewId" + Date.now(),
  68. };
  69. },
  70. async created() {
  71. const res = await this.$http.get(
  72. "/api/ecs_exam_work/exam/getExamPropertyFromCacheByStudentSession/" +
  73. this.course.examId +
  74. `/OFFLINE_UPLOAD_FILE_TYPE`
  75. );
  76. this.uploadFileFormat =
  77. (res.data.OFFLINE_UPLOAD_FILE_TYPE &&
  78. JSON.parse(res.data.OFFLINE_UPLOAD_FILE_TYPE)) ||
  79. [];
  80. this.uploadFileAccept =
  81. "image/jpeg," +
  82. this.uploadFileFormat.map((v) => "application/" + v.toLowerCase()).join();
  83. if (this.uploadFileAccept.includes("zip")) {
  84. this.uploadFileAccept = ".zip," + this.uploadFileAccept;
  85. }
  86. this.uploadFileFormat.push(...["jpeg", "jpg"]);
  87. this.uploadFileFormat = this.uploadFileFormat.map((v) => v.toLowerCase());
  88. },
  89. methods: {
  90. fileFormatCheck(file, resolve, reject) {
  91. function getMimetype(signature) {
  92. switch (signature) {
  93. case "89504E47":
  94. return "image/png";
  95. case "47494638":
  96. return "image/gif";
  97. case "25504446":
  98. return "application/pdf";
  99. case "FFD8FFDB":
  100. case "FFD8FFE0":
  101. case "FFD8FFE1":
  102. return "image/jpeg";
  103. case "504B0304":
  104. return "application/zip";
  105. case "504B34":
  106. return "application/zip";
  107. default:
  108. return "Unknown filetype";
  109. }
  110. }
  111. console.log(file);
  112. const filereader = new FileReader();
  113. let uploads = [];
  114. filereader.onloadend = (evt) => {
  115. if (evt.target.readyState === FileReader.DONE) {
  116. const uint = new Uint8Array(evt.target.result);
  117. let bytes = [];
  118. uint.forEach((byte) => {
  119. bytes.push(byte.toString(16));
  120. });
  121. const hex = bytes.join("").toUpperCase();
  122. uploads.push({
  123. filename: file.name,
  124. filetype: file.type ? file.type : "Unknown/Extension missing",
  125. binaryFileType: getMimetype(hex),
  126. hex: hex,
  127. });
  128. if (["application/pdf"].includes(getMimetype(hex))) {
  129. if (!file.name.endsWith(".pdf")) {
  130. this.loadingStatus = false;
  131. this.$Notice.warning({
  132. title: "文件内容与文件的后缀不一致",
  133. // desc: file.name + " 文件是pdf文档,但文件名后缀不是.pdf!"
  134. });
  135. this.file = null;
  136. reject("文件内容与文件的后缀不一致,请确认文件是pdf文档!");
  137. } else {
  138. resolve();
  139. }
  140. } else if (["application/zip"].includes(getMimetype(hex))) {
  141. if (!file.name.endsWith(".zip")) {
  142. this.loadingStatus = false;
  143. // this.$refs.uploadComp.fileList.splice(0);
  144. // this.$refs.uploadComp.fileList = [];
  145. this.$Notice.warning({
  146. title: "文件内容与文件的后缀不一致",
  147. // desc: file.name + " 文件是zip压缩包,但文件名后缀不是.zip!"
  148. });
  149. this.file = null;
  150. reject("文件内容与文件的后缀不一致,请确认文件是zip压缩包!");
  151. } else {
  152. resolve();
  153. }
  154. } else if (["image/jpeg"].includes(getMimetype(hex))) {
  155. if (file.name.endsWith(".jpeg") || file.name.endsWith(".jpg")) {
  156. printCurrentPage()
  157. .then((filename) => {
  158. // const fs = nodeRequire("fs");
  159. const path = window.nodeRequire("path");
  160. const fileNew = {
  161. name: path.basename(filename),
  162. path: filename,
  163. type: "application/pdf",
  164. // lastModified
  165. // lastModifiedDate
  166. // size
  167. };
  168. this.file = fileNew;
  169. file = fileNew;
  170. // console.log(this.$refs.uploadComp.fileList);
  171. // this.$refs.uploadComp.fileList = [file];
  172. // console.log(this.$refs.uploadComp.fileList);
  173. resolve();
  174. })
  175. .catch(() => {
  176. reject();
  177. });
  178. } else {
  179. this.loadingStatus = false;
  180. // this.$refs.uploadComp.fileList.splice(0);
  181. // this.$refs.uploadComp.fileList = [];
  182. this.$Notice.warning({
  183. title: "文件内容与文件的后缀不一致",
  184. });
  185. this.file = null;
  186. reject("文件内容与文件的后缀不一致,请确认文件是jpg文件!");
  187. }
  188. } else {
  189. console.log("binary file type check: not zip or pdf");
  190. window._hmt.push([
  191. "_trackEvent",
  192. "离线考试页面",
  193. "上传作答",
  194. "文件格式非zip或pdf",
  195. ]);
  196. this.$Notice.warning({
  197. title: "作答文件损坏",
  198. desc:
  199. file.name +
  200. " 文件无法以 " +
  201. this.uploadFileFormat.join(" 或 ") +
  202. " 格式读取。",
  203. });
  204. this.file = null;
  205. this.loadingStatus = false;
  206. reject("作答文件损坏");
  207. }
  208. }
  209. };
  210. const blob = file.slice(0, 4);
  211. filereader.readAsArrayBuffer(blob);
  212. },
  213. handleSuccess() {
  214. window._hmt.push(["_trackEvent", "离线考试页面", "上传作答", "上传成功"]);
  215. this.file = null;
  216. this.loadingStatus = false;
  217. this.$Message.success({
  218. content: "上传成功",
  219. duration: 5,
  220. closable: true,
  221. });
  222. this.$emit("reloadList");
  223. this.showPreview = false;
  224. },
  225. handleError(error, file) {
  226. window._hmt.push(["_trackEvent", "离线考试页面", "上传作答", "上传失败"]);
  227. this.file = null;
  228. this.loadingStatus = false;
  229. console.log(error);
  230. this.$Message.error({
  231. content: (file && file.desc) || "上传失败",
  232. duration: 15,
  233. closable: true,
  234. });
  235. this.showPreview = false;
  236. },
  237. handleFormatError(file) {
  238. this.file = null;
  239. this.loadingStatus = false;
  240. this.$Notice.warning({
  241. title: "作答文件格式不对",
  242. desc:
  243. file.name +
  244. " 文件格式不对,请选择 " +
  245. this.uploadFileFormat.join(" 或 ") +
  246. " 文件。",
  247. });
  248. this.showPreview = false;
  249. },
  250. handleMaxSize(file) {
  251. this.file = null;
  252. this.loadingStatus = false;
  253. this.$Notice.warning({
  254. title: "超出文件大小限制",
  255. desc: file.name + " 太大,作答文件不能超过30M.",
  256. });
  257. this.showPreview = false;
  258. },
  259. async handleBeforeUpload(file) {
  260. const suffix = file.name.split(".").pop();
  261. if (suffix.toLowerCase() !== suffix) {
  262. this.$Notice.error({
  263. title: "文件名后缀必须是小写",
  264. desc: file.name + " 文件名后缀必须是小写。",
  265. });
  266. return Promise.reject("file suffix should be lower case");
  267. }
  268. if (file.name.endsWith(".jpeg") || file.name.endsWith(".jpg")) {
  269. new Promise((resolve, reject) => {
  270. if (this.course.offlineFileUrl) {
  271. this.$Modal.confirm({
  272. title: "已有作答附件,是否覆盖?",
  273. onCancel: () => reject("离线考试,取消覆盖"),
  274. onOk: () => resolve(),
  275. });
  276. } else {
  277. resolve();
  278. }
  279. })
  280. .then(() => {
  281. this.showPreview = true;
  282. this.$Message.info({
  283. content: "正在准备将图片转为PDF...",
  284. duration: 1.5,
  285. closable: true,
  286. });
  287. return new Promise((resolve) => {
  288. var reader = new FileReader();
  289. reader.onload = (e) => {
  290. document
  291. .getElementById(this.previewId)
  292. .setAttribute("src", e.target.result);
  293. setTimeout(resolve, 2500);
  294. // resolve();
  295. };
  296. //Imagepath.files[0] is blob type
  297. reader.readAsDataURL(file);
  298. });
  299. })
  300. .then(() => {
  301. // return;
  302. return printCurrentPage()
  303. .then((filename) => {
  304. const fs = window.nodeRequire("fs");
  305. const path = window.nodeRequire("path");
  306. // const fileNew = {
  307. // name: path.basename(filename),
  308. // // filename: path.basename(filename),
  309. // uri: "file://" + filename,
  310. // // path: filename,
  311. // type: "application/pdf",
  312. // };
  313. const fileNew = new File(
  314. [fs.readFileSync(filename)],
  315. path.basename(filename),
  316. { type: "application/pdf" } // what I upload is image.
  317. );
  318. this.file = fileNew;
  319. this.$Message.info({
  320. content: "图片转换成功,正在上传...",
  321. duration: 2,
  322. closable: true,
  323. });
  324. var stats = fs.statSync(filename);
  325. var fileSizeInBytes = stats["size"];
  326. if (fileSizeInBytes > 1024 * 30 * 1024) {
  327. this.handleMaxSize();
  328. throw new Error("附件超过最大限制");
  329. }
  330. const formData = new FormData();
  331. formData.append("fileType", "pdf");
  332. formData.append("file", fileNew);
  333. return this.$http
  334. .post(
  335. "/api/branch_ecs_oe_admin/offlineExam/submitPaper?examRecordDataId=" +
  336. this.course.examRecordDataId,
  337. formData
  338. )
  339. .then(() => {
  340. this.handleSuccess();
  341. })
  342. .catch(() => {
  343. this.handleError();
  344. });
  345. })
  346. .catch((error) => {
  347. console.log(error);
  348. this.handleError();
  349. });
  350. });
  351. return Promise.reject("图片另外路径上传");
  352. }
  353. return new Promise((resolve, reject) => {
  354. if (this.course.offlineFileUrl) {
  355. this.$Modal.confirm({
  356. title: "已有作答附件,是否覆盖?",
  357. onCancel: () => reject("离线考试,取消覆盖"),
  358. onOk: () => resolve(),
  359. });
  360. } else {
  361. resolve();
  362. }
  363. }).then(() => {
  364. if (
  365. file.type.includes("/pdf")
  366. // ||
  367. // file.type.includes("/jpeg") ||
  368. // file.type.includes("/jpg")
  369. ) {
  370. this.fileType = "pdf";
  371. } else if (file.type.includes("/zip")) {
  372. this.fileType = "zip";
  373. }
  374. this.file = file;
  375. this.loadingStatus = true;
  376. return new Promise((resolve, reject) =>
  377. this.fileFormatCheck(file, resolve, reject)
  378. );
  379. });
  380. },
  381. },
  382. };
  383. </script>
  384. <style scoped>
  385. .list .ivu-upload-select {
  386. width: 100%;
  387. }
  388. .preview-image {
  389. max-width: 100vw;
  390. max-height: 100vh;
  391. margin: -16px;
  392. object-fit: contain;
  393. }
  394. </style>