Pārlūkot izejas kodu

feat: 试卷pdf方案改造

zhangjie 7 mēneši atpakaļ
vecāks
revīzija
5a1d719141

+ 2 - 1
package.json

@@ -22,6 +22,7 @@
     "docx-preview": "^0.3.2",
     "element-ui": "2.15.6",
     "html2canvas": "^1.4.1",
+    "html2pdf.js": "^0.10.2",
     "js-md5": "^0.7.3",
     "lodash": "^4.17.15",
     "moment": "^2.29.1",
@@ -66,4 +67,4 @@
       "git add"
     ]
   }
-}
+}

+ 1 - 1
src/directives/domObserver.js

@@ -23,7 +23,7 @@ Vue.directive("domObserver", {
     el.__vueSetInterval__ = setInterval(isResize, 300);
   },
   unbind(el) {
-    console.log(el, "解绑");
+    // console.log(el, "解绑");
     clearInterval(el.__vueSetInterval__);
   },
 });

+ 21 - 126
src/modules/paper-export/views/AnswerTemplateBuild.vue

@@ -1,24 +1,9 @@
 <template>
   <div class="paper-template-build">
     <div class="paper-template-build-body">
-      <!-- <paper-template-view
-        ref="PaperTemplateView"
-        class="preview-body"
-        :pages="pages"
-        :page-config="paperTempJson.pageConfig"
-      >
-      </paper-template-view> -->
-      <!-- process dom -->
-      <!-- <div v-if="elementList.length" class="element-list">
-        <elem-rich-text
-          v-for="elem in elementList"
-          :id="elem.id"
-          :key="elem.id"
-          :data="elem"
-        ></elem-rich-text>
-      </div> -->
       <answer-template-view
         ref="AnswerTemplateView"
+        id="answer-template-view"
         class="preview-body"
         :answerData="paperJson"
         :pageCountMode="pageCountMode"
@@ -28,45 +13,25 @@
 </template>
 
 <script>
-// import ElemRichText from "../elements/rich-text/ElemRichText.vue";
-// import PaperTemplateView from "../components/PaperTemplateView.vue";
 import AnswerTemplateView from "../components/AnswerTemplateView.vue";
-// import { deepCopy } from "../../card/plugins/utils";
-// import previewTemp from "../previewTemp";
-import previewTemp from "../previewAnswer";
 import { paperDetailInfoApi } from "../../paper/api";
-// import { paperTemplateListApi, paperPdfDownloadApi } from "../api";
-import { paperPdfDownloadApi } from "../api";
-import { downloadByApi } from "@/plugins/download";
-import paperTemplateBuildMixins from "./paperTemplateBuildMixins";
+import { buildPdf } from "@/plugins/htmlToPdf";
 
 export default {
   name: "AnswerTemplateBuild",
   components: { AnswerTemplateView },
-  mixins: [paperTemplateBuildMixins],
   data() {
     return {
       paperId: this.$route.params.paperId,
       viewType: this.$route.params.viewType,
       seqMode: "MODE3",
       paperJson: {},
-      paperTempJson: {
-        pages: [],
-        pageConfig: {},
-      },
       maxColumnWidth: 200,
       maxColumnHeight: 200,
-      paperTempList: [],
       curPaperTemp: {},
       downloading: false,
       fieldData: {},
       paperStructs: [],
-      configModalForm: {
-        showDetailNo: true,
-        showDetailScoreInfo: true,
-        showDetailScoreTable: false,
-        pageCountMode: "SIMPLE",
-      },
       configSources: [],
       prepareDownloadPdf: false,
       pageCountMode: "SIMPLE",
@@ -89,28 +54,39 @@ export default {
 
         this.seqMode = answerSet.seqMode;
         this.pageCountMode = answerSet.pageCountMode;
-        // this.curPaperTemp = answerSet.paperTemp;
-        // this.configModalForm = answerSet.configModalForm;
 
         await this.getPaperJson();
       } catch (error) {
         this.emitFrameResult(false, "数据错误");
       }
 
-      this.$nextTick(() => {
-        this.emitFrameResult(true, "", this.getPreviewTemp());
+      this.$nextTick(async () => {
+        const answerBlob = await buildPdf(
+          {
+            element: document.getElementById("answer-template-view"),
+            pageSize: "A4",
+          },
+          true
+        ).catch((error) => {
+          console.error(error);
+        });
+        if (!answerBlob) {
+          this.emitFrameResult(false, "生成答案pdf错误");
+          return;
+        }
+
+        this.emitFrameResult(true, "", answerBlob);
       });
     },
-    emitFrameResult(success = true, errorMsg = "", htmlCont = "") {
+    emitFrameResult(success = true, errorMsg = "", blobCont = null) {
       // console.log("htmlC", htmlCont);
       window.parent &&
         window.parent.submitPaperTemp &&
         window.parent.submitPaperTemp({
           success,
           errorMsg,
-          htmlCont,
-          // templateId: this.curPaperTemp.id,
-          templateId: null,
+          blobCont,
+          contType: "answer",
         });
     },
     async getPaperJson() {
@@ -120,20 +96,6 @@ export default {
       });
       this.paperJson = res.data;
       this.resetClozeSerialNo(this.paperJson);
-      this.fieldData = {
-        paperName: res.data.name,
-        courseName: res.data.course.name,
-        courseCode: res.data.course.code,
-        totalScore: res.data.totalScore,
-        rootOrgName: res.data.rootOrgName,
-      };
-      this.paperStructs = this.paperJson.paperDetails.map((detail) => {
-        return {
-          detailName: detail.name,
-          questionCount: detail.unitCount,
-          totalScore: detail.score,
-        };
-      });
     },
     resetClozeSerialNo(paperData) {
       const clozeQuestionTypes = ["CLOZE", "BANKED_CLOZE"];
@@ -150,73 +112,6 @@ export default {
         });
       });
     },
-    // img ------ start >
-    loadImg(url) {
-      return new Promise((resolve, reject) => {
-        const img = new Image();
-        img.onload = function () {
-          resolve(true);
-        };
-        img.onerror = function () {
-          reject();
-        };
-        img.src = url;
-      });
-    },
-    getRichJsonImgUrls(richJson) {
-      let urls = [];
-      if (!richJson) return urls;
-      richJson.sections.forEach((section) => {
-        section.blocks.forEach((elem) => {
-          if (elem.type === "image" && elem.value.startsWith("http")) {
-            urls.push(elem.value);
-          }
-        });
-      });
-      return urls;
-    },
-    async waitAllImgLoaded() {
-      let imgUrls = [];
-      this.elementList.forEach((item) => {
-        imgUrls.push(...this.getRichJsonImgUrls(item.content));
-      });
-
-      // console.log(imgUrls);
-
-      if (!imgUrls.length) return Promise.resolve(true);
-      const imgLoads = imgUrls.map((item) => this.loadImg(item));
-      const imgLoadResult = await Promise.all(imgLoads).catch(() => {});
-      if (imgLoadResult && imgLoadResult.length) {
-        return Promise.resolve(true);
-      } else {
-        return Promise.reject();
-      }
-    },
-    // img ------ end >
-    // download ------ start >
-    getPreviewTemp() {
-      return previewTemp(this.$refs.AnswerTemplateView.$el.outerHTML);
-    },
-    async downloadPaperPdf() {
-      const htmlCont = this.getPreviewTemp();
-
-      if (this.downloading) return;
-      this.downloading = true;
-
-      const res = await downloadByApi(() => {
-        return paperPdfDownloadApi({
-          content: htmlCont,
-          templateId: this.curPaperTemp.id,
-          paperId: this.paperId,
-        });
-      }).catch((e) => {
-        this.$message.error(e || "下载失败,请重新尝试!");
-      });
-      this.downloading = false;
-
-      if (!res) return;
-      this.$message.success("下载成功!");
-    },
   },
 };
 </script>

+ 137 - 111
src/modules/paper-export/views/PaperTemplateBuild.vue

@@ -9,7 +9,7 @@
           @change="seqModeChange"
         >
           <!-- <el-option value="MODE1" label="单题型连续"></el-option>
-          <el-option value="MODE2" label="客观题整体连续"></el-option> -->
+            <el-option value="MODE2" label="客观题整体连续"></el-option> -->
           <el-option value="MODE3" label="按大题独立"></el-option>
           <el-option value="MODE5" label="整卷连续"></el-option>
         </el-select>
@@ -29,17 +29,29 @@
           >
           </el-option>
         </el-select>
-        <el-button type="primary" size="small" @click="toDownload"
+        <el-button
+          type="primary"
+          size="small"
+          :loading="downloading"
+          @click="toAction('downloadPdf')"
           >下载试卷</el-button
         >
+        <el-button
+          type="primary"
+          size="small"
+          :loading="downloading"
+          @click="toAction('buildPackge')"
+          >生成数据包</el-button
+        >
       </div>
       <paper-build-config
         ref="PaperBuildConfig"
         :config-sources="configSources"
-        @confirm="buildConfigChange"
+        :showConfirmBtn="false"
       ></paper-build-config>
       <paper-template-view
         ref="PaperTemplateView"
+        id="paper-template-view"
         class="preview-body"
         :pages="pages"
         :page-config="paperTempJson.pageConfig"
@@ -96,17 +108,15 @@
           :data="elem"
         ></elem-rich-text>
       </div>
-    </div>
-    <div
-      v-if="answerPreviewUrl && viewType !== 'iframe'"
-      class="design-preview-frame"
-    >
-      <iframe
-        :src="answerPreviewUrl"
-        frameborder="0"
-        width="1000"
-        height="800"
-      ></iframe>
+
+      <!-- answer dom -->
+      <answer-template-view
+        ref="AnswerTemplateView"
+        id="answer-template-view"
+        class="preview-body"
+        :answerData="paperJson"
+        :pageCountMode="configModalForm.pageCountMode"
+      ></answer-template-view>
     </div>
   </div>
 </template>
@@ -115,24 +125,25 @@
 import ElemRichText from "../elements/rich-text/ElemRichText.vue";
 import PaperTemplateView from "../components/PaperTemplateView.vue";
 import PaperBuildConfig from "../components/PaperBuildConfig.vue";
+import AnswerTemplateView from "../components/AnswerTemplateView.vue";
+
 import { deepCopy } from "../../card/plugins/utils";
-import previewTemp from "../previewTemp";
 import { paperDetailInfoApi } from "../../paper/api";
-import {
-  paperTemplateListApi,
-  paperPdfDownloadApi,
-  paperAndAnswerPdfDownloadApi,
-} from "../api";
-import { downloadByApi } from "@/plugins/download";
+import { paperTemplateListApi, paperAndAnswerPdfDownloadApi } from "../api";
 import paperTemplateBuildMixins from "./paperTemplateBuildMixins";
+import { buildPdf } from "@/plugins/htmlToPdf";
 
 export default {
   name: "PaperTemplateBuild",
-  components: { PaperTemplateView, PaperBuildConfig, ElemRichText },
+  components: {
+    PaperTemplateView,
+    PaperBuildConfig,
+    ElemRichText,
+    AnswerTemplateView,
+  },
   mixins: [paperTemplateBuildMixins],
   data() {
     return {
-      answerPreviewUrl: "",
       paperId: this.$route.params.paperId,
       viewType: this.$route.params.viewType,
       seqMode: "MODE3",
@@ -156,14 +167,9 @@ export default {
       },
       configSources: [],
       prepareDownloadPdf: false,
-      answerHtmlContent: "",
-      waitingAnswer: true,
-      creatingZip: false,
+      actionType: "",
     };
   },
-  created() {
-    this.createGetAnswerFunc();
-  },
   mounted() {
     if (this.viewType === "frame") {
       this.initFrame();
@@ -171,42 +177,7 @@ export default {
     }
     this.initData();
   },
-  beforeDestroy() {
-    delete window.submitPaperTemp;
-  },
-  watch: {
-    waitingAnswer(val) {
-      if (!val && this.creatingZip) {
-        this.createPaperAndAnswerZip();
-      }
-    },
-  },
   methods: {
-    createGetAnswerFunc() {
-      window.submitPaperTemp = async ({ success, errorMsg, htmlCont }) => {
-        if (!success) {
-          this.$message.error(errorMsg);
-          return;
-        }
-        this.answerHtmlContent = htmlCont;
-        this.waitingAnswer = false;
-      };
-      window.answerSet = {
-        pageCountMode: this.configModalForm.pageCountMode,
-        seqMode: this.seqMode,
-      };
-      const { href } = this.$router.resolve({
-        name: "AnswerTemplateBuild",
-        params: {
-          paperId: this.paperId,
-          viewType: "frame",
-        },
-        query: {
-          t: Date.now(),
-        },
-      });
-      this.answerPreviewUrl = href;
-    },
     async initFrame() {
       try {
         const paperSet = window.parent.paperSet;
@@ -265,21 +236,35 @@ export default {
               this.emitFrameResult(false, "构建pdf错误");
             }
 
-            this.$nextTick(() => {
-              this.emitFrameResult(true, "", this.getPreviewTemp());
+            this.$nextTick(async () => {
+              const paperBlob = await buildPdf(
+                {
+                  element: document.getElementById("paper-template-view"),
+                  pageSize: this.paperTempJson.pageConfig.pageSize,
+                },
+                true
+              ).catch((error) => {
+                console.error(error);
+              });
+              if (!paperBlob) {
+                this.emitFrameResult(false, "生成试卷pdf错误");
+                return;
+              }
+
+              this.emitFrameResult(true, "", paperBlob);
             });
           });
         });
       });
     },
-    emitFrameResult(success = true, errorMsg = "", htmlCont = "") {
+    emitFrameResult(success = true, errorMsg = "", blobCont = null) {
       window.parent &&
         window.parent.submitPaperTemp &&
         window.parent.submitPaperTemp({
           success,
           errorMsg,
-          htmlCont,
-          templateId: this.curPaperTemp.id,
+          blobCont,
+          contType: "paper",
         });
     },
     async initData() {
@@ -405,31 +390,6 @@ export default {
       this.$nextTick(() => {
         this.buildData();
       });
-      setTimeout(() => {
-        this.createPaperAndAnswerZip();
-      });
-    },
-    async createPaperAndAnswerZip() {
-      this.creatingZip = true;
-      if (this.waitingAnswer) {
-        return;
-      }
-      const paperHtmlCont = this.getPreviewTemp();
-      const answerHtmlCont = this.answerHtmlContent;
-      if (!answerHtmlCont) {
-        this.$message.error("试卷答案获取失败,请刷新页面重试!");
-        return;
-      }
-
-      paperAndAnswerPdfDownloadApi({
-        paperContent: paperHtmlCont,
-        templateId: this.curPaperTemp.id,
-        paperId: Number(this.paperId),
-        answerContent: answerHtmlCont,
-        answerSize: "A4",
-      }).finally(() => {
-        this.creatingZip = false;
-      });
     },
     paperTempChange(paperTemp) {
       // console.log(paperTemp);
@@ -467,7 +427,7 @@ export default {
           this.buildPagesByAutoPage();
           if (this.prepareDownloadPdf) {
             this.$nextTick(async () => {
-              await this.downloadPaperPdf().catch(() => {});
+              await this.runAction().catch(() => {});
               this.prepareDownloadPdf = false;
             });
           }
@@ -518,42 +478,101 @@ export default {
     },
     // img ------ end >
     // download ------ start >
-    getPreviewTemp() {
-      return previewTemp(this.$refs.PaperTemplateView.$el.outerHTML);
-    },
-    async toDownload() {
+    async toAction(actionType) {
       const valid = await this.$refs.PaperBuildConfig.checkData().catch(
         () => {}
       );
       if (!valid) return;
 
+      this.actionType = actionType;
       const configData = this.$refs.PaperBuildConfig.getData();
       if (JSON.stringify(configData) === JSON.stringify(this.configModalForm)) {
-        this.downloadPaperPdf();
+        this.runAction();
       } else {
         this.prepareDownloadPdf = true;
         this.buildConfigChange(configData);
       }
     },
+    async runAction() {
+      if (this.actionType === "downloadPdf") {
+        await this.downloadPaperPdf();
+      } else {
+        await this.buildPackage();
+      }
+    },
     async downloadPaperPdf() {
-      const htmlCont = this.getPreviewTemp();
+      if (this.downloading) return;
+      this.downloading = true;
+
+      let result = true;
+      await buildPdf({
+        element: document.getElementById("paper-template-view"),
+        filename: `${this.fieldData.courseName}(${this.fieldData.courseCode})_${this.fieldData.paperName}.pdf`,
+        pageSize: this.paperTempJson.pageConfig.pageSize,
+      }).catch((error) => {
+        result = false;
+        console.error(error);
+        this.$message.error(error.message || "下载失败,请重新尝试!");
+      });
 
+      this.downloading = false;
+      if (!result) return;
+      this.$message.success("下载成功!");
+    },
+    async buildPackage() {
       if (this.downloading) return;
       this.downloading = true;
 
-      const res = await downloadByApi(() => {
-        return paperPdfDownloadApi({
-          content: htmlCont,
-          templateId: this.curPaperTemp.id,
-          paperId: this.paperId,
-        });
-      }).catch((e) => {
-        this.$message.error(e || "下载失败,请重新尝试!");
+      // 试卷PDF
+      const paperPdfName = `${this.fieldData.courseName}(${this.fieldData.courseCode})_${this.fieldData.paperName}.pdf`;
+      const paperBlob = await buildPdf(
+        {
+          element: document.getElementById("paper-template-view"),
+          pageSize: this.paperTempJson.pageConfig.pageSize,
+        },
+        true
+      ).catch((error) => {
+        this.downloading = false;
+        console.error(error);
+        this.$message.error("生成试卷pdf失败,请重新尝试!");
+      });
+      if (!paperBlob) return;
+
+      // 答案pdf
+      const answerPdfName = `${this.fieldData.courseName}(${this.fieldData.courseCode})_${this.fieldData.paperName}_答案.pdf`;
+      const answerBlob = await buildPdf(
+        {
+          element: document.getElementById("answer-template-view"),
+          pageSize: "A4",
+        },
+        true
+      ).catch((error) => {
+        this.downloading = false;
+        console.error(error);
+        this.$message.error("生成答案pdf失败,请重新尝试!");
+      });
+      if (!answerBlob) return;
+
+      const data = new FormData();
+      data.append(
+        "paperPdf",
+        new File([paperBlob], paperPdfName, { type: "application/pdf" })
+      );
+      data.append(
+        "answerPdf",
+        new File([answerBlob], answerPdfName, { type: "application/pdf" })
+      );
+      data.append("paperId", this.paperId);
+
+      let result = true;
+      await paperAndAnswerPdfDownloadApi(data).catch((error) => {
+        result = false;
+        console.error(error);
       });
       this.downloading = false;
 
-      if (!res) return;
-      this.$message.success("下载成功!");
+      if (!result) return;
+      this.$message.success("生成数据包成功!");
     },
   },
 };
@@ -592,4 +611,11 @@ export default {
   top: 0;
   z-index: 1;
 }
+
+.paper-template-build .answer-template-view {
+  position: absolute;
+  left: -9999px;
+  z-index: 999;
+  visibility: hidden;
+}
 </style>

+ 8 - 8
src/modules/paper-export/views/paperTemplateBuildMixins.js

@@ -26,11 +26,11 @@ export default {
     };
   },
   methods: {
-    buildData() {
-      this.maxColumnWidth = document.getElementById("column-0-0").offsetWidth;
-      this.maxColumnHeight =
-        document.getElementById("column-0-0").offsetHeight - 10;
-    },
+    // buildData() {
+    //   this.maxColumnWidth = document.getElementById("column-0-0").offsetWidth;
+    //   this.maxColumnHeight =
+    //     document.getElementById("column-0-0").offsetHeight - 10;
+    // },
     checkIsNouns(detail) {
       return (
         detail.name.includes("名词解释") &&
@@ -141,17 +141,17 @@ export default {
         const IS_NOUNS = questionType === "NOUNS";
 
         // 构建选项元素信息
-        console.log("options.length", options.length);
+        // console.log("options.length", options.length);
         const opitonList = options.map((optionIds) => {
           const elements = getElementsByIds(optionIds);
           const width = maxNum(elements.map((elem) => elem.w));
-          console.log(width, this.maxColumnWidth, this.TEXT_INDENT_SIZE);
+          // console.log(width, this.maxColumnWidth, this.TEXT_INDENT_SIZE);
           const percent = this.getSizePercent(
             width,
             this.maxColumnWidth - this.TEXT_INDENT_SIZE,
             options.length
           );
-          console.log("percent", percent);
+          // console.log("percent", percent);
           return {
             percent,
             resetPercent: null,

+ 13 - 33
src/modules/questions/views/GenPaper.vue

@@ -472,12 +472,8 @@ import PaperBuildConfig from "../../paper-export/components/PaperBuildConfig.vue
 import { QUESTION_API } from "@/constants/constants";
 import { LEVEL_TYPE, PUBLICITY_LIST } from "../constants/constants";
 import { mapState } from "vuex";
-import { downloadByApi } from "@/plugins/download";
-import {
-  paperTemplateListApi,
-  paperPdfDownloadApi,
-  answerPdfDownloadApi,
-} from "../../paper-export/api";
+import { downloadByApi, downloadByBlob } from "@/plugins/download";
+import { paperTemplateListApi } from "../../paper-export/api";
 import { deepCopy } from "@/plugins/utils";
 
 export default {
@@ -529,6 +525,7 @@ export default {
       exportDialog: false,
       exportModel: {
         id: "",
+        paperName: "",
         courseCode: "",
         courseName: "",
         templateId: "",
@@ -931,6 +928,7 @@ export default {
       this.isShow = true;
       this.exportDialog = true;
       this.exportModel.id = row.id;
+      this.exportModel.paperName = row.name;
       this.exportModel.courseCode = row.course.code;
       this.exportModel.courseName = row.course.name;
       this.exportModel.exportContent = "";
@@ -1179,12 +1177,12 @@ export default {
       window.submitPaperTemp = async ({
         success,
         errorMsg,
-        htmlCont,
-        templateId,
+        blobCont,
+        contType,
       }) => {
         if (!success) {
           this.downloading = false;
-          if (templateId) {
+          if (contType === "paper") {
             delete window.paperSet;
             this.paperPreviewUrl = "";
           } else {
@@ -1196,37 +1194,19 @@ export default {
           this.exportDialog = false;
           return;
         }
-        // this.paperPreviewUrl = "";
-        if (templateId) {
-          const res = await downloadByApi(() => {
-            return paperPdfDownloadApi({
-              content: htmlCont,
-              templateId,
-              paperId: this.exportModel.id,
-            });
-          }).catch((e) => {
-            this.$message.error(e || "下载失败,请重新尝试!");
-          });
+
+        if (contType === "paper") {
+          const paperPdfName = `${this.exportModel.courseName}(${this.exportModel.courseCode})_${this.exportModel.paperName}.pdf`;
+          downloadByBlob(blobCont, paperPdfName);
           this.exportDialog = false;
           this.downloading = false;
-
-          if (!res) return;
           this.$message.success("下载成功!");
           delete window.paperSet;
         } else {
-          const res = await downloadByApi(() => {
-            return answerPdfDownloadApi({
-              content: htmlCont,
-              size: "A4",
-              paperId: this.exportModel.id,
-            });
-          }).catch((e) => {
-            this.$message.error(e || "下载失败,请重新尝试!");
-          });
+          const answerPdfName = `${this.exportModel.courseName}(${this.exportModel.courseCode})_${this.exportModel.paperName}_答案.pdf`;
+          downloadByBlob(blobCont, answerPdfName);
           this.exportDialog = false;
           this.downloading = false;
-
-          if (!res) return;
           this.$message.success("下载成功!");
           delete window.answerSet;
         }

+ 0 - 2
src/modules/questions/views/OrgProperty.vue

@@ -598,8 +598,6 @@ export default {
           GEN_PAPER_QUESTION_END,
         },
       };
-      // 调试
-      // params.properties.CHECK_LIMIT_CHAR_COUNT = undefined;
       let res = await this.$httpWithMsg.put(url, params).catch(() => {});
       if (!res) return;
       if (this.tab4Entered) {

+ 33 - 0
src/plugins/htmlToPdf.js

@@ -0,0 +1,33 @@
+import html2pdf from "html2pdf.js";
+
+// jsPDF config doc
+// https://rawgit.com/MrRio/jsPDF/master/docs/jsPDF.html
+export const jsPDFConfigs = {
+  A3: { unit: "mm", format: "a3", orientation: "landscape" },
+  A4: { unit: "mm", format: "a4", orientation: "portrait" },
+  "8K": {
+    unit: "mm",
+    format: [370, 255],
+    orientation: "landscape",
+  },
+};
+
+export async function buildPdf(
+  { element, pageSize, filename },
+  returnBlob = false
+) {
+  const opt = {
+    margin: 0,
+    html2canvas: { scale: 6 },
+    filename: filename || "",
+    jsPDF: {
+      ...jsPDFConfigs[pageSize],
+    },
+  };
+
+  if (returnBlob) {
+    return await html2pdf().set(opt).from(element).outputPdf("blob");
+  } else {
+    await html2pdf().set(opt).from(element).save();
+  }
+}

+ 108 - 1
yarn.lock

@@ -1005,6 +1005,13 @@
   dependencies:
     regenerator-runtime "^0.14.0"
 
+"@babel/runtime@^7.12.5", "@babel/runtime@^7.23.2":
+  version "7.26.0"
+  resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1"
+  integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==
+  dependencies:
+    regenerator-runtime "^0.14.0"
+
 "@babel/template@^7.22.15", "@babel/template@^7.23.9", "@babel/template@^7.24.0":
   version "7.24.0"
   resolved "https://registry.npmmirror.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50"
@@ -1344,6 +1351,11 @@
   resolved "https://registry.npmmirror.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b"
   integrity sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==
 
+"@types/raf@^3.4.0":
+  version "3.4.3"
+  resolved "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04"
+  integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==
+
 "@types/range-parser@*":
   version "1.2.7"
   resolved "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
@@ -2321,6 +2333,11 @@ browserslist@^4.0.0, browserslist@^4.16.3, browserslist@^4.21.10, browserslist@^
     node-releases "^2.0.14"
     update-browserslist-db "^1.0.13"
 
+btoa@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73"
+  integrity sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==
+
 buffer-from@^1.0.0:
   version "1.1.2"
   resolved "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -2408,6 +2425,20 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599:
   resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz#93a3ee17a35aa6a9f0c6ef1b2ab49507d1ab9079"
   integrity sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==
 
+canvg@^3.0.6:
+  version "3.0.10"
+  resolved "https://registry.npmmirror.com/canvg/-/canvg-3.0.10.tgz#8e52a2d088b6ffa23ac78970b2a9eebfae0ef4b3"
+  integrity sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==
+  dependencies:
+    "@babel/runtime" "^7.12.5"
+    "@types/raf" "^3.4.0"
+    core-js "^3.8.3"
+    raf "^3.4.1"
+    regenerator-runtime "^0.13.7"
+    rgbcolor "^1.0.1"
+    stackblur-canvas "^2.0.0"
+    svg-pathdata "^6.0.3"
+
 case-sensitive-paths-webpack-plugin@^2.3.0:
   version "2.4.0"
   resolved "https://registry.npmmirror.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4"
@@ -2737,6 +2768,11 @@ core-js@^2.4.0:
   resolved "https://registry.npmmirror.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
   integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
 
+core-js@^3.6.0:
+  version "3.39.0"
+  resolved "https://registry.npmmirror.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83"
+  integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==
+
 core-js@^3.8.3:
   version "3.36.1"
   resolved "https://registry.npmmirror.com/core-js/-/core-js-3.36.1.tgz#c97a7160ebd00b2de19e62f4bbd3406ab720e578"
@@ -3181,6 +3217,11 @@ domhandler@^5.0.2, domhandler@^5.0.3:
   dependencies:
     domelementtype "^2.3.0"
 
+dompurify@^2.5.4:
+  version "2.5.7"
+  resolved "https://registry.npmmirror.com/dompurify/-/dompurify-2.5.7.tgz#6e0d36b9177db5a99f18ade1f28579db5ab839d7"
+  integrity sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q==
+
 domready@1.0.8:
   version "1.0.8"
   resolved "https://registry.npmmirror.com/domready/-/domready-1.0.8.tgz#91f252e597b65af77e745ae24dd0185d5e26d58c"
@@ -3346,6 +3387,11 @@ es-module-lexer@^1.2.1:
   resolved "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.5.0.tgz#4878fee3789ad99e065f975fdd3c645529ff0236"
   integrity sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==
 
+es6-promise@^4.2.5:
+  version "4.2.8"
+  resolved "https://registry.npmmirror.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
+  integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
+
 escalade@^3.1.1:
   version "3.1.2"
   resolved "https://registry.npmmirror.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27"
@@ -3749,6 +3795,11 @@ faye-websocket@^0.11.3:
   dependencies:
     websocket-driver ">=0.5.1"
 
+fflate@^0.8.1:
+  version "0.8.2"
+  resolved "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
+  integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==
+
 figures@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmmirror.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@@ -4186,7 +4237,7 @@ html-webpack-plugin@^5.1.0:
     pretty-error "^4.0.0"
     tapable "^2.0.0"
 
-html2canvas@^1.4.1:
+html2canvas@^1.0.0, html2canvas@^1.0.0-rc.5, html2canvas@^1.4.1:
   version "1.4.1"
   resolved "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
   integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
@@ -4194,6 +4245,15 @@ html2canvas@^1.4.1:
     css-line-break "^2.1.0"
     text-segmentation "^1.0.3"
 
+html2pdf.js@^0.10.2:
+  version "0.10.2"
+  resolved "https://registry.npmmirror.com/html2pdf.js/-/html2pdf.js-0.10.2.tgz#29c0e4cebf2cbde4ed0c2e8abb98eecac22ff66f"
+  integrity sha512-WyHVeMb18Bp7vYTmBv1GVsThH//K7SRfHdSdhHPkl4JvyQarNQXnailkYn0QUbRRmnN5rdbbmSIGEsPZtzPy2Q==
+  dependencies:
+    es6-promise "^4.2.5"
+    html2canvas "^1.0.0"
+    jspdf "^2.3.1"
+
 htmlparser2@^3.8.3:
   version "3.10.1"
   resolved "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
@@ -4693,6 +4753,21 @@ jsonfile@^6.0.1:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
+jspdf@^2.3.1:
+  version "2.5.2"
+  resolved "https://registry.npmmirror.com/jspdf/-/jspdf-2.5.2.tgz#3c35bb1063ee3ad9428e6353852b0d685d1f923a"
+  integrity sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==
+  dependencies:
+    "@babel/runtime" "^7.23.2"
+    atob "^2.1.2"
+    btoa "^1.2.1"
+    fflate "^0.8.1"
+  optionalDependencies:
+    canvg "^3.0.6"
+    core-js "^3.6.0"
+    dompurify "^2.5.4"
+    html2canvas "^1.0.0-rc.5"
+
 jszip@>=3.0.0:
   version "3.10.1"
   resolved "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
@@ -5574,6 +5649,11 @@ path-type@^4.0.0:
   resolved "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
   integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
 
+performance-now@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+  integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
+
 picocolors@^0.2.1:
   version "0.2.1"
   resolved "https://registry.npmmirror.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
@@ -6024,6 +6104,13 @@ queue-microtask@^1.2.2:
   resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
   integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
 
+raf@^3.4.1:
+  version "3.4.1"
+  resolved "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
+  integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
+  dependencies:
+    performance-now "^2.1.0"
+
 randombytes@^2.1.0:
   version "2.1.0"
   resolved "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@@ -6111,6 +6198,11 @@ regenerator-runtime@^0.11.0:
   resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
   integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
 
+regenerator-runtime@^0.13.7:
+  version "0.13.11"
+  resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
 regenerator-runtime@^0.14.0:
   version "0.14.1"
   resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
@@ -6256,6 +6348,11 @@ rfdc@^1.3.0:
   resolved "https://registry.npmmirror.com/rfdc/-/rfdc-1.3.1.tgz#2b6d4df52dffe8bb346992a10ea9451f24373a8f"
   integrity sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==
 
+rgbcolor@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d"
+  integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==
+
 rimraf@^3.0.2:
   version "3.0.2"
   resolved "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -6704,6 +6801,11 @@ stable@^0.1.8:
   resolved "https://registry.npmmirror.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
   integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
 
+stackblur-canvas@^2.0.0:
+  version "2.7.0"
+  resolved "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz#af931277d0b5096df55e1f91c530043e066989b6"
+  integrity sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==
+
 stackframe@^1.3.4:
   version "1.3.4"
   resolved "https://registry.npmmirror.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310"
@@ -6892,6 +6994,11 @@ svg-baker@^1.5.0, svg-baker@^1.7.0:
     query-string "^4.3.2"
     traverse "^0.6.6"
 
+svg-pathdata@^6.0.3:
+  version "6.0.3"
+  resolved "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz#80b0e0283b652ccbafb69ad4f8f73e8d3fbf2cac"
+  integrity sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==
+
 svg-sprite-loader@^6.0.11:
   version "6.0.11"
   resolved "https://registry.npmmirror.com/svg-sprite-loader/-/svg-sprite-loader-6.0.11.tgz#a4d60cee3d74232a2c17d31c73a2008295f61220"