Selaa lähdekoodia

试卷导出pdf页面构建逻辑调整

zhangjie 1 vuosi sitten
vanhempi
commit
e795d71daa

+ 31 - 28
src/modules/paper-export/assets/styles/paper-temp-preview.scss

@@ -153,39 +153,11 @@
 }
 // elem-rich-text
 .elem-rich-text {
-  display: inline-block;
-  white-space: pre-wrap;
-
   img {
     max-width: 100%;
     max-height: 100%;
     height: auto !important;
   }
-
-  &.is-detail-title {
-    overflow: hidden;
-    padding-bottom: 5px;
-
-    .rich-text {
-      display: inline;
-    }
-  }
-
-  .detail-score-table {
-    float: left;
-    width: 120px;
-    border-spacing: 0;
-    border-collapse: collapse;
-    margin-right: 10px;
-
-    td,
-    th {
-      height: 30px;
-      line-height: 30px;
-      border: 1px solid #000;
-      font-size: 16px;
-    }
-  }
 }
 // elem-paper-struct
 .elem-paper-struct {
@@ -320,4 +292,35 @@
   .topic-element-preview {
     font-size: 14px;
   }
+  .group-item {
+    .elem-rich-text {
+      display: inline-block;
+      white-space: pre-wrap;
+      width: 100%;
+    }
+    &.is-detail-title {
+      overflow: hidden;
+      padding-bottom: 5px;
+
+      .rich-text {
+        display: inline;
+      }
+    }
+
+    .detail-score-table {
+      float: left;
+      width: 120px;
+      border-spacing: 0;
+      border-collapse: collapse;
+      margin-right: 10px;
+
+      td,
+      th {
+        height: 30px;
+        line-height: 30px;
+        border: 1px solid #000;
+        font-size: 16px;
+      }
+    }
+  }
 }

+ 27 - 32
src/modules/paper-export/assets/styles/temp.css

@@ -66,11 +66,11 @@
 .paper-page .page-column-element .element-item::before {
   border: none;
 }
-
 .elem-field-text .text-body {
   width: 100%;
   position: relative;
   min-height: 28px;
+  overflow: hidden;
 }
 .elem-field-text .text-body > span {
   display: inline-block;
@@ -99,7 +99,6 @@
   -webkit-transform-origin: 0 100%;
   transform-origin: 0 100%;
 }
-
 .elem-pane-box .elem-body {
   min-height: 60px;
   position: relative;
@@ -115,38 +114,11 @@
 .elem-pane-box .elem-pane-box-element .element-item {
   position: absolute;
 }
-
-.elem-rich-text {
-  display: inline-block;
-  white-space: pre-wrap;
-}
 .elem-rich-text img {
   max-width: 100%;
   max-height: 100%;
   height: auto !important;
 }
-.elem-rich-text.is-detail-title {
-  overflow: hidden;
-  padding-bottom: 5px;
-}
-.elem-rich-text.is-detail-title .rich-text {
-  display: inline;
-}
-.elem-rich-text .detail-score-table {
-  float: left;
-  width: 120px;
-  border-spacing: 0;
-  border-collapse: collapse;
-  margin-right: 10px;
-}
-.elem-rich-text .detail-score-table td,
-.elem-rich-text .detail-score-table th {
-  height: 30px;
-  line-height: 30px;
-  border: 1px solid #000;
-  font-size: 16px;
-}
-
 .elem-paper-struct li {
   padding-left: 30px;
   text-indent: -30px;
@@ -157,7 +129,6 @@
   width: 30px;
   text-align: right;
 }
-
 .elem-score-table {
   text-align: center;
 }
@@ -174,7 +145,6 @@
   height: 30px;
   line-height: 30px;
 }
-
 .elem-paper-props {
   display: table;
   width: 80%;
@@ -251,7 +221,6 @@
   border-bottom: 1px solid #000;
   text-align: center;
 }
-
 .paper-template-view .element-item-paper-struct {
   height: auto !important;
 }
@@ -264,3 +233,29 @@
 .paper-template-view .topic-element-preview {
   font-size: 14px;
 }
+.paper-template-view .group-item .elem-rich-text {
+  display: inline-block;
+  white-space: pre-wrap;
+  width: 100%;
+}
+.paper-template-view .group-item.is-detail-title {
+  overflow: hidden;
+  padding-bottom: 5px;
+}
+.paper-template-view .group-item.is-detail-title .rich-text {
+  display: inline;
+}
+.paper-template-view .group-item .detail-score-table {
+  float: left;
+  width: 120px;
+  border-spacing: 0;
+  border-collapse: collapse;
+  margin-right: 10px;
+}
+.paper-template-view .group-item .detail-score-table td,
+.paper-template-view .group-item .detail-score-table th {
+  height: 30px;
+  line-height: 30px;
+  border: 1px solid #000;
+  font-size: 16px;
+}

+ 4 - 4
src/modules/paper-export/components/PaperTemplateView.vue

@@ -40,14 +40,16 @@
                     :data="element"
                   ></topic-element-preview>
                   <!-- paper question info  -->
-                  <template v-if="column.texts && column.texts.length">
+                  <slot name="texts" v-bind="{ texts: column.texts }"> </slot>
+
+                  <!-- <template v-if="column.texts && column.texts.length">
                     <elem-rich-text
                       v-for="elem in column.texts"
                       :id="`rich-text-${elem.id}`"
                       :key="elem.id"
                       :data="elem"
                     ></elem-rich-text>
-                  </template>
+                  </template> -->
                 </div>
               </div>
               <page-number
@@ -82,7 +84,6 @@
 import TopicElementPreview from "./TopicElementPreview";
 import BoxElementPreview from "./BoxElementPreview.vue";
 import PageNumber from "./PageNumber";
-import ElemRichText from "../elements/rich-text/ElemRichText.vue";
 
 export default {
   name: "PaperTemplateView",
@@ -90,7 +91,6 @@ export default {
     TopicElementPreview,
     BoxElementPreview,
     PageNumber,
-    ElemRichText,
   },
   props: {
     pages: {

+ 7 - 7
src/modules/paper-export/elements/rich-text/ElemRichText.vue

@@ -1,11 +1,11 @@
 <template>
-  <div :class="['elem-rich-text', data.classNames]" :style="data.styles">
-    <rich-text
-      v-if="data.contType !== 'gap'"
-      :text-json="data.content"
-      :style="data.textStyles"
-    ></rich-text>
-  </div>
+  <rich-text
+    v-if="data.contType !== 'gap'"
+    class="elem-rich-text"
+    :text-json="data.content"
+    :style="data.textStyles"
+  ></rich-text>
+  <div v-else class="elem-rich-text"></div>
 </template>
 
 <script>

+ 0 - 2
src/modules/paper-export/elements/rich-text/model.js

@@ -12,9 +12,7 @@ const MODEL = {
   w: 200,
   h: 50,
   contType: "content",
-  styles: {},
   textStyles: {},
-  classNames: "",
   content: {
     sections: [{ blocks: [] }],
   },

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
src/modules/paper-export/previewTemp.js


+ 981 - 0
src/modules/paper-export/views/PaperTemplateBuild.old.vue

@@ -0,0 +1,981 @@
+<template>
+  <div class="paper-template-build">
+    <div class="paper-template-build-body">
+      <div class="margin_top_10">
+        <el-select
+          v-model="seqMode"
+          class="margin-right-10"
+          size="small"
+          @change="seqModeChange"
+        >
+          <el-option value="MODE1" 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>
+        <el-select
+          v-model="curPaperTemp"
+          class="margin-right-10"
+          placeholder="请选择"
+          value-key="id"
+          size="small"
+          @change="paperTempChange"
+        >
+          <el-option
+            v-for="item in paperTempList"
+            :key="item.id"
+            :label="item.name"
+            :value="item"
+          >
+          </el-option>
+        </el-select>
+        <el-button type="primary" size="small" @click="toDownload"
+          >下载试卷</el-button
+        >
+      </div>
+      <paper-build-config
+        ref="PaperBuildConfig"
+        :config-sources="configSources"
+        @confirm="buildConfigChange"
+      ></paper-build-config>
+      <paper-template-view
+        ref="PaperTemplateView"
+        class="preview-body"
+        :pages="pages"
+        :page-config="paperTempJson.pageConfig"
+      ></paper-template-view>
+    </div>
+  </div>
+</template>
+
+<script>
+import PaperTemplateView from "../components/PaperTemplateView.vue";
+import PaperBuildConfig from "../components/PaperBuildConfig.vue";
+import { getModel as getRichTextModel } from "../elements/rich-text/model";
+import { getModel as getPageModel } from "../elements/page/model";
+import { getElementId, randomCode, deepCopy } from "../../card/plugins/utils";
+import { calcSum, maxNum } from "@/plugins/utils";
+import previewTemp from "../previewTemp";
+import { paperDetailInfoApi } from "../../paper/api";
+import { paperTemplateListApi, paperPdfDownloadApi } from "../api";
+import { downloadByApi } from "@/plugins/download";
+// import paperJson from "./data/paper.json";
+// import paperTempJson from "./data/paper-temp.json";
+
+const numberToUpperCase = function (val) {
+  if (val < 1 || val > 26) return;
+
+  return String.fromCharCode(64 + val);
+};
+
+const checkRichTextHasCont = function (data) {
+  if (!data) return false;
+  if (!data.sections || !data.sections.length) return false;
+
+  if (!data.sections[0].blocks || !data.sections[0].blocks.length) return false;
+
+  return true;
+};
+
+export default {
+  name: "PaperTemplateBuild",
+  components: { PaperTemplateView, PaperBuildConfig },
+  data() {
+    return {
+      paperId: this.$route.params.paperId,
+      viewType: this.$route.params.viewType,
+      seqMode: "MODE1",
+      renderStructList: [],
+      pages: [],
+      paperJson: {},
+      paperTempJson: {
+        pages: [],
+        pageConfig: {},
+      },
+      maxColumnWidth: 200,
+      maxColumnHeight: 200,
+      paperTempList: [],
+      curPaperTemp: {},
+      downloading: false,
+      fieldData: {},
+      paperStructs: [],
+      TEXT_INDENT_SIZE: 28,
+      textIndent: 28,
+      configModalForm: {
+        showDetailNo: true,
+        showDetailScoreTable: false,
+      },
+      configSources: [],
+      prepareDownloadPdf: false,
+    };
+  },
+  mounted() {
+    if (this.viewType === "frame") {
+      this.initFrame();
+      return;
+    }
+    this.initData();
+  },
+  methods: {
+    getTextIndexStyle() {
+      return {
+        textIndent: `-${this.textIndent}px`,
+        paddingLeft: `${this.textIndent}px`,
+      };
+    },
+    async initFrame() {
+      try {
+        const paperSet = window.parent.paperSet;
+        if (!paperSet) {
+          this.emitFrameResult(false, "数据缺失");
+          return;
+        }
+
+        this.seqMode = paperSet.seqMode;
+        this.curPaperTemp = paperSet.paperTemp;
+        this.configModalForm = paperSet.configModalForm;
+
+        await this.getPaperJson();
+
+        let paperTempJson = this.curPaperTemp.content
+          ? JSON.parse(this.curPaperTemp.content)
+          : { pages: [], pageConfig: {} };
+        this.paperTempJson = paperTempJson;
+        this.pages = paperTempJson.pages;
+        this.updaterFieldInfo();
+      } catch (error) {
+        this.emitFrameResult(false, "数据错误");
+      }
+
+      this.$nextTick(async () => {
+        try {
+          this.maxColumnWidth =
+            document.getElementById("column-0-0").offsetWidth;
+          this.maxColumnHeight =
+            document.getElementById("column-0-0").offsetHeight - 10;
+          this.parseRenderStructList();
+          this.buildPrePages();
+        } catch (error) {
+          this.emitFrameResult(false, "构建错误");
+        }
+
+        const loadRes = await this.waitAllImgLoaded().catch(() => {});
+        if (!loadRes) {
+          this.emitFrameResult(false, "数据缓存错误");
+          return;
+        }
+        this.$nextTick(() => {
+          try {
+            this.addDetailScoreTable();
+            this.resetRenderStructSize();
+            this.buildPageAutoPage();
+          } catch (error) {
+            this.emitFrameResult(false, "构建pdf错误");
+          }
+
+          this.$nextTick(() => {
+            this.addDetailScoreTable();
+            this.emitFrameResult(true, "", this.getPreviewTemp());
+          });
+        });
+      });
+    },
+    emitFrameResult(success = true, errorMsg = "", htmlCont = "") {
+      window.parent &&
+        window.parent.submitPaperTemp &&
+        window.parent.submitPaperTemp({
+          success,
+          errorMsg,
+          htmlCont,
+          templateId: this.curPaperTemp.id,
+        });
+    },
+    async initData() {
+      await this.getPaperJson();
+      await this.getPaperTempList();
+
+      if (!this.paperTempList.length) {
+        this.$message.error("导出模板缺失!");
+        return;
+      }
+      this.paperTempChange(this.paperTempList[0]);
+
+      // test--->
+      // this.paperJson = paperJson;
+      // this.paperTempJson = paperTempJson;
+      // this.pages = paperTempJson.pages;
+      // this.$nextTick(() => {
+      //   this.buildData();
+      // });
+    },
+    async getPaperJson() {
+      const res = await paperDetailInfoApi({
+        paperId: this.paperId,
+        seqMode: this.seqMode,
+      });
+      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"];
+      paperData.paperDetails.forEach((detail) => {
+        detail.paperDetailUnits.forEach((question) => {
+          if (!clozeQuestionTypes.includes(question.questionType)) return;
+          question.question.quesBody.sections.forEach((section) => {
+            section.blocks.forEach((block) => {
+              if (block.type !== "cloze") return;
+              block.value =
+                question.question.subQuestions[block.value - 1].questionSeq;
+            });
+          });
+        });
+      });
+    },
+    async seqModeChange() {
+      await this.getPaperJson();
+      this.$nextTick(() => {
+        this.buildData();
+      });
+    },
+    async getPaperTempList() {
+      const res = await paperTemplateListApi("PAPER_EXPORT");
+      this.paperTempList = res.data;
+    },
+    getConfigSources() {
+      const { pages } = this.paperTempJson;
+      let sources = [],
+        fieldAble = {};
+      pages.forEach((page) => {
+        page.columns.forEach((column) => {
+          column.elements.forEach((element) => {
+            if (element.type !== "PAPER_PROPS") return;
+            if (!sources.length) {
+              sources = deepCopy(element.props);
+            }
+            element.props.forEach((prop) => {
+              fieldAble[prop.field] = fieldAble[prop.field] || prop.enable;
+            });
+          });
+        });
+      });
+
+      sources.forEach((item) => {
+        item.enable = fieldAble[item.field];
+      });
+      this.configSources = sources;
+    },
+    buildConfigChange(val) {
+      this.configModalForm = val;
+      this.updaterFieldInfo();
+
+      this.$nextTick(() => {
+        this.buildData();
+      });
+    },
+    paperTempChange(paperTemp) {
+      // console.log(paperTemp);
+      this.curPaperTemp = paperTemp;
+      let paperTempJson = paperTemp.content
+        ? JSON.parse(paperTemp.content)
+        : { pages: [], pageConfig: {} };
+      this.paperTempJson = paperTempJson;
+      this.pages = paperTempJson.pages;
+      this.getConfigSources();
+      this.updaterFieldInfo();
+
+      this.$nextTick(() => {
+        this.buildData();
+      });
+    },
+    updaterFieldInfo() {
+      const VALID_ELEMENTS_FOR_EXTERNAL = ["FIELD_TEXT"];
+      this.paperTempJson.pages.forEach((page) => {
+        page.columns.forEach((column) => {
+          column.elements.forEach((elem) => {
+            if (elem.type === "PAPER_STRUCT") {
+              elem.structs = this.paperStructs;
+            } else if (elem.type === "SCORE_TABLE") {
+              elem.detailCount = this.paperStructs.length;
+            } else if (elem.type === "PAPER_PROPS") {
+              elem.props.forEach((prop) => {
+                prop.value = this.configModalForm[prop.field];
+              });
+            }
+
+            if (!elem.elements || !elem.elements.length) return;
+
+            elem.elements.forEach((element) => {
+              if (!VALID_ELEMENTS_FOR_EXTERNAL.includes(element.type)) return;
+
+              if (element.type === "FIELD_TEXT" && element.field) {
+                element.content = this.fieldData[element.field];
+              }
+            });
+          });
+        });
+      });
+    },
+    async buildData() {
+      this.maxColumnWidth = document.getElementById("column-0-0").offsetWidth;
+      this.maxColumnHeight =
+        document.getElementById("column-0-0").offsetHeight - 10;
+      this.parseRenderStructList();
+      this.buildPrePages();
+
+      const loadRes = await this.waitAllImgLoaded().catch(() => {});
+      if (!loadRes) {
+        this.$message.error("图片加载有误!");
+        return;
+      }
+      this.$nextTick(() => {
+        this.buildReleasePages();
+      });
+    },
+    parseRenderStructList() {
+      let renderStructList = [this.parseLineGap(), this.parseLineGap()];
+
+      this.paperJson.paperDetails.forEach((detail) => {
+        renderStructList.push(this.parseDetailTitle(detail));
+        if (checkRichTextHasCont(detail.description)) {
+          const descData = this.parseTitleOption(detail.description, "");
+          renderStructList.push(...descData);
+        }
+        detail.paperDetailUnits.forEach((question) => {
+          let questionInfo = question.question;
+          if (questionInfo.subQuestions && questionInfo.subQuestions.length) {
+            const bodys = this.parseTitleOption(questionInfo.quesBody, "");
+            renderStructList.push(...bodys);
+
+            const isMatches = this.checkIsMatches(questionInfo.questionType);
+            if (
+              isMatches &&
+              questionInfo.quesOptions &&
+              questionInfo.quesOptions.length
+            ) {
+              questionInfo.quesOptions.forEach((op) => {
+                const obodys = this.parseTitleOption(
+                  op.optionBody,
+                  `${numberToUpperCase(op.number)}、`,
+                  true,
+                  {
+                    contType: "option",
+                    textStyles: { paddingLeft: `${this.textIndent}px` },
+                  }
+                );
+                renderStructList.push(...obodys);
+              });
+            }
+
+            // 选词填空不展示小题
+            if (questionInfo.questionType === "BANKED_CLOZE") {
+              renderStructList.push(this.parseLineGap());
+              return;
+            }
+
+            questionInfo.subQuestions.forEach((sq, sqindex) => {
+              sq.subNumber = sqindex + 1;
+              if (isMatches) sq.quesOptions = []; // 选词填空、段落匹配小题中不展示选项
+              const contents = this.parseSimpleQuestion(sq, false);
+              renderStructList.push(...contents);
+              renderStructList.push(this.parseLineGap());
+            });
+          } else {
+            questionInfo.number = question.number;
+            const datas = this.parseSimpleQuestion(questionInfo, true);
+            renderStructList.push(...datas);
+            renderStructList.push(this.parseLineGap());
+          }
+        });
+      });
+
+      // 去掉最后一题的间隔行
+      // console.log(renderStructList);
+      this.renderStructList = renderStructList.slice(0, -1);
+    },
+    getRichStruct(blocks) {
+      return {
+        sections: [
+          {
+            blocks: [...blocks],
+          },
+        ],
+      };
+    },
+    transformRichJson(richJson) {
+      if (!richJson || !richJson.sections) return [];
+      let contents = [];
+      let curBlock = [];
+      const checkNeedSplitSection = (block) => {
+        if (block.type !== "image") return false;
+
+        if (block.param) {
+          if (block.param.width)
+            return block.param.width > this.maxColumnWidth / 2;
+          if (block.param.height) return block.param.height > 150;
+        }
+        return true;
+      };
+
+      richJson.sections.forEach((section) => {
+        section.blocks.forEach((block) => {
+          if (checkNeedSplitSection(block) && curBlock.length) {
+            contents.push(this.getRichStruct(curBlock));
+            curBlock = [];
+          }
+          curBlock.push(block);
+        });
+
+        if (curBlock.length) {
+          contents.push(this.getRichStruct(curBlock));
+          curBlock = [];
+        }
+      });
+      return contents;
+    },
+    changeRichTextCloze(richText) {
+      return {
+        sections: richText.sections.map((section) => {
+          return {
+            blocks: section.blocks.map((item) => {
+              if (item.type === "cloze") {
+                return {
+                  type: "text",
+                  value: "_____",
+                };
+              } else {
+                return { ...item };
+              }
+            }),
+          };
+        }),
+      };
+    },
+    parseSimpleQuestion(question) {
+      let contents = [];
+      let quesBody = question.quesBody;
+      if (question.questionType === "FILL_BLANK_QUESTION") {
+        quesBody = this.changeRichTextCloze(quesBody);
+      }
+
+      const tbodys = this.parseTitleOption(
+        quesBody,
+        `${question.questionSeq}、`,
+        true,
+        {
+          textStyles: { paddingLeft: `${this.textIndent}px` },
+        }
+      );
+      contents.push(...tbodys);
+      const hasNobody = !tbodys.length;
+
+      if (question.quesOptions && question.quesOptions.length) {
+        question.quesOptions.forEach((op, oIndex) => {
+          let noVal = `${numberToUpperCase(op.number)}、`;
+          if (!hasNobody) {
+            const obodys = this.parseTitleOption(op.optionBody, noVal, false, {
+              contType: "option",
+              textStyles: { paddingLeft: `${this.textIndent}px` },
+            });
+            contents.push(...obodys);
+            return;
+          }
+
+          if (!oIndex) {
+            // 针对如完形填空的小题做的特殊处理
+            noVal = `${question.questionSeq}、${noVal}`;
+          }
+          this.textIndent = 2 * this.TEXT_INDENT_SIZE;
+          const obodys = this.parseTitleOption(op.optionBody, noVal, true, {
+            contType: "option",
+            textStyles: { paddingLeft: `${this.textIndent}px` },
+          });
+          contents.push(...obodys);
+          this.textIndent = this.TEXT_INDENT_SIZE;
+        });
+      }
+      return contents;
+    },
+    parseDetailTitle(data) {
+      let blocks = [
+        {
+          type: "text",
+          value: `${data.name}(共${data.unitCount}小题,满分${data.score}分)`,
+        },
+      ];
+      if (this.configModalForm.showDetailNo) {
+        blocks.unshift({
+          type: "text",
+          value: `${data.cnNum}、`,
+        });
+      }
+      let content = this.getRichStruct(blocks);
+      return getRichTextModel({
+        styles: { width: "100%", fontWeight: 900 },
+        content,
+        classNames: this.configModalForm.showDetailScoreTable
+          ? "is-detail-title"
+          : "",
+      });
+    },
+    parseTitleOption(
+      richJson,
+      noVal,
+      needIndent = false,
+      modelData = { textStyles: null, styles: null, contType: "" }
+    ) {
+      if (!richJson) return [];
+      const { textStyles, styles, contType } = modelData;
+      const bodys = this.transformRichJson(richJson);
+
+      return bodys.map((body, index) => {
+        let presetData = {
+          content: body,
+        };
+        if (contType) presetData.contType = contType;
+        if (textStyles) presetData.textStyles = textStyles;
+        if (styles) {
+          presetData.styles = styles;
+        } else {
+          presetData.styles = {
+            width: contType === "option" ? "auto" : "100%",
+          };
+        }
+
+        if (index === 0 && noVal) {
+          let cont = {
+            type: "text",
+            value: noVal,
+          };
+          if (needIndent) {
+            cont.param = {
+              width: this.textIndent,
+            };
+            presetData.textStyles = {
+              ...(presetData.textStyles || {}),
+              ...this.getTextIndexStyle(),
+            };
+          }
+          body.sections[0].blocks.unshift(cont);
+        }
+
+        return getRichTextModel(presetData);
+      });
+    },
+    parseLineGap() {
+      return getRichTextModel({
+        contType: "gap",
+        styles: { width: "100%", height: "10px" },
+        content: this.getRichStruct([{ type: "text", value: "" }]),
+      });
+    },
+    checkIsMatches(structType) {
+      const matchesTypes = ["BANKED_CLOZE", "PARAGRAPH_MATCHING"];
+      return matchesTypes.includes(structType);
+    },
+    buildPrePages() {
+      let pages = deepCopy(this.paperTempJson.pages);
+      const firstPageNo = pages.findIndex((p) => p.pageType !== "cover");
+      pages[firstPageNo].columns[0].texts = [];
+      pages[firstPageNo].columns[0].texts.push(...this.renderStructList);
+      this.pages = pages;
+    },
+    buildReleasePages() {
+      this.addDetailScoreTable();
+      this.resetRenderStructSize();
+      // console.log(this.renderStructList);
+      this.buildPageAutoPage();
+
+      this.$nextTick(async () => {
+        this.addDetailScoreTable();
+
+        if (this.prepareDownloadPdf) {
+          await this.downloadPaperPdf().catch(() => {});
+          this.prepareDownloadPdf = false;
+        }
+      });
+    },
+    addDetailScoreTable() {
+      if (!this.configModalForm.showDetailScoreTable) return;
+
+      const scoreTableHtml = `<table class="detail-score-table"><tr><th>得分</th><th>评分人</th></tr><tr><td></td><td></td></tr></table>`;
+      const dom = document.createElement("div");
+      dom.innerHTML = scoreTableHtml;
+      document.querySelectorAll(".is-detail-title").forEach((node) => {
+        const hasScoreTable =
+          node.firstChild.className.includes("detail-score-table");
+        if (hasScoreTable) return;
+        node.insertBefore(dom.firstChild.cloneNode(true), node.firstChild);
+      });
+    },
+    resetRenderStructSize() {
+      let curOptions = [];
+      this.renderStructList.forEach((elem, eindex) => {
+        const elemDom = document.getElementById(`rich-text-${elem.id}`);
+        elem.w = elemDom.offsetWidth;
+        elem.h = elemDom.offsetHeight;
+
+        if (elem.contType !== "option") return;
+
+        curOptions.push(elem);
+
+        // 全选选项逻辑
+        const nextElem = this.renderStructList[eindex + 1];
+        if (nextElem && nextElem.contType === "option") return;
+
+        curOptions.forEach((optionElem) => {
+          optionElem._percent = this.getSizePercent(
+            optionElem.w,
+            this.maxColumnWidth
+          );
+        });
+
+        const optionCount = curOptions.length;
+        // 奇数选项,全部一行
+        if (optionCount <= 7 && optionCount % 2 > 0) {
+          curOptions.forEach((optionElem) => {
+            optionElem._percent = 1;
+            optionElem.styles.width = "100%";
+          });
+          curOptions = [];
+          return;
+        }
+
+        const percents = curOptions.map((item) => item._percent);
+        const maxPercent = maxNum(percents);
+        // let aveOptionPercent = 1;
+        // if (optionCount % 4 === 0) {
+        //   aveOptionPercent = this.calcAveOptionPercent(maxPercent);
+        // } else {
+        //   aveOptionPercent = maxPercent > 0.5 ? 1 : 0.5;
+        // }
+        const aveOptionPercent = this.calcAveOptionPercent(maxPercent);
+
+        curOptions.forEach((optionElem) => {
+          optionElem._percent = aveOptionPercent;
+          optionElem.styles.width = aveOptionPercent * 100 + "%";
+        });
+
+        curOptions = [];
+      });
+      this.renderStructList.forEach((elem) => {
+        if (elem.styles.width === "100%") {
+          this.$set(elem.styles, "display", "block");
+        }
+      });
+    },
+    buildPageAutoPage() {
+      let pages = [];
+      let curPage = null,
+        curElem = null;
+      let curColumn = null,
+        curColumnNo = 0,
+        curColumnHeight = 0;
+      let curLinePercent = 0;
+      let groups = [],
+        curGroup = [];
+
+      // 分组自动分页 选项分组
+      const getNextElem = () => {
+        return this.renderStructList.shift();
+      };
+
+      curElem = getNextElem();
+      while (curElem) {
+        if (
+          curElem.contType !== "option" ||
+          (curElem.contType === "option" && curElem._percent === 1)
+        ) {
+          if (curGroup.length) {
+            groups.push(curGroup);
+            curGroup = [];
+            curLinePercent = 0;
+          }
+
+          groups.push([curElem]);
+          curElem = getNextElem();
+        } else {
+          if (curLinePercent + curElem._percent > 1) {
+            groups.push(curGroup);
+            curGroup = [];
+            curLinePercent = 0;
+          } else {
+            curGroup.push(curElem);
+            curLinePercent += curElem._percent;
+            curElem = getNextElem();
+          }
+        }
+      }
+      if (curGroup.length) {
+        groups.push(curGroup);
+        curGroup = [];
+      }
+      // console.log(groups);
+
+      const getNextGroup = () => {
+        return groups.shift();
+      };
+      curGroup = getNextGroup();
+      while (curGroup) {
+        if (!curPage) {
+          curPage = this.getNewPageModel(pages.length);
+        }
+
+        if (!curColumn) {
+          curColumn = curPage.columns[curColumnNo++];
+          curColumnHeight = this.calcInitColumnHeight(curColumn);
+        }
+
+        let curGroupHeigth =
+          curGroup.length === 1
+            ? curGroup[0].h
+            : Math.max.apply(
+                null,
+                curGroup.map((item) => item.h)
+              );
+
+        if (curGroupHeigth + curColumnHeight > this.maxColumnHeight) {
+          // 当前栏第一个元素就超过最大高度时,直接放当前栏
+          if (!curColumn.texts.length) {
+            curColumn.texts.push(...curGroup);
+            curGroup = getNextGroup();
+          }
+          // 当前栏满了
+          if (curColumnNo >= curPage.columnNumber) {
+            // 当前页满了
+            pages.push(curPage);
+            curPage = null;
+            curColumnNo = 0;
+          }
+          curColumn = null;
+          curColumnHeight = 0;
+        } else {
+          // 当前栏未满
+          curColumnHeight += curGroupHeigth;
+          curColumn.texts.push(...curGroup);
+          curGroup = getNextGroup();
+        }
+      }
+
+      if (curPage) {
+        pages.push(curPage);
+        curPage = null;
+      }
+      // 正文部分保证偶数页
+      if (pages.length % 2) {
+        pages.push(this.getNewPageModel(pages.length));
+      }
+
+      if (this.paperTempJson.pageConfig.showCover) {
+        // 封面自动插入反面空白页
+        let coverPages = deepCopy(
+          this.paperTempJson.pages.filter((p) => p.pageType === "cover")
+        );
+        let coverBackPage = deepCopy(coverPages[0]);
+        coverBackPage.columns.forEach((column) => {
+          column.elements = [];
+        });
+        let nCoverPages = [];
+        coverPages.forEach((cpage) => {
+          nCoverPages.push(cpage);
+          nCoverPages.push(coverBackPage);
+        });
+        pages = [...nCoverPages, ...pages];
+      }
+
+      this.pages = pages;
+    },
+    getNewPageModel(pageNo) {
+      let contentPages = this.paperTempJson.pages.slice(-2);
+      let pNo = pageNo % 2;
+      const pageTemp = contentPages[pNo];
+      let newPage = getPageModel({
+        ...this.paperTempJson.pageConfig,
+        pageType: pageTemp.pageType,
+      });
+      newPage.sides = pageTemp.sides.map((elem) => {
+        let nelem = deepCopy(elem);
+        nelem.id = getElementId();
+        nelem.key = randomCode();
+
+        // if (pNo === 1 && nelem.type === "GUTTER") {
+        //   nelem.direction = "right";
+        // }
+        return nelem;
+      });
+      newPage.columns.forEach((column) => {
+        column.texts = [];
+      });
+
+      if (pageNo > 1) return newPage;
+
+      newPage.columns.forEach((column, cindex) => {
+        column.elements = pageTemp.columns[cindex].elements.map((elem) => {
+          let nelem = deepCopy(elem);
+          nelem.id = getElementId();
+          nelem.key = randomCode();
+          nelem.h = this.getElementHeight(`preview-${elem.id}`);
+          if (nelem.elements && nelem.elements.length) {
+            nelem.elements.forEach((celem) => {
+              celem.id = getElementId();
+              celem.key = randomCode();
+            });
+          }
+          return nelem;
+        });
+        column.texts = [];
+      });
+      return newPage;
+    },
+    getElementHeight(elementId) {
+      const dom = document.getElementById(elementId);
+      return dom ? dom.offsetHeight : 0;
+    },
+    calcAveOptionPercent(maxPercent) {
+      if (maxPercent > 0.5) return 1;
+      if (maxPercent > 0.25) return 0.5;
+      return 0.25;
+    },
+    calcInitColumnHeight(column) {
+      return calcSum(column.elements.map((item) => item.h));
+    },
+    getSizePercent(size, fullSize) {
+      const rate = size / fullSize;
+      if (rate <= 0.25) return 0.25;
+      if (rate <= 0.5) return 0.5;
+      return 1;
+    },
+    // img
+    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.renderStructList.forEach((item) => {
+        if (item.contType === "gap") return;
+        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();
+      }
+    },
+    getPreviewTemp() {
+      const elementDoms =
+        this.$refs.PaperTemplateView.$el.querySelectorAll(".elem-rich-text");
+      elementDoms.forEach((eDom) => {
+        const width = eDom.offsetWidth;
+        if (eDom.firstChild && eDom.firstChild.nodeName === "DIV") {
+          eDom.firstChild.style.width = width + "px";
+          if (
+            eDom.firstChild.firstChild &&
+            eDom.firstChild.firstChild.nodeName === "DIV"
+          )
+            eDom.firstChild.firstChild.style.width = "100%";
+        }
+      });
+      return previewTemp(this.$refs.PaperTemplateView.$el.outerHTML);
+    },
+    async toDownload() {
+      const valid = await this.$refs.PaperBuildConfig.checkData().catch(
+        () => {}
+      );
+      if (!valid) return;
+
+      const configData = this.$refs.PaperBuildConfig.getData();
+      if (JSON.stringify(configData) === JSON.stringify(this.configModalForm)) {
+        this.downloadPaperPdf();
+      } else {
+        this.prepareDownloadPdf = true;
+        this.buildConfigChange(configData);
+      }
+    },
+    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>
+
+<style>
+.paper-template-build {
+  text-align: center;
+}
+.paper-template-build-body {
+  display: inline-block;
+  text-align: initial;
+}
+
+.paper-template-build .page-box {
+  margin-top: 10px;
+  margin-bottom: 10px;
+}
+.paper-template-build .paper-build-config {
+  padding: 10px 15px 2px;
+  margin-top: 5px;
+  background: #fff;
+  border-radius: 10px;
+}
+.paper-build-config .el-form-item {
+  margin-bottom: 16px;
+  margin-right: 30px;
+}
+</style>

+ 108 - 597
src/modules/paper-export/views/PaperTemplateBuild.vue

@@ -43,50 +43,79 @@
         class="preview-body"
         :pages="pages"
         :page-config="paperTempJson.pageConfig"
-      ></paper-template-view>
+      >
+        <template #texts="{ texts }">
+          <div
+            v-for="groupItem in texts"
+            :key="groupItem.id"
+            :id="groupItem.id"
+            :class="[
+              'group-item',
+              { 'is-detail-title': groupItem.type === 'detail-title' },
+            ]"
+            :style="groupItem.styles"
+          >
+            <table
+              v-if="
+                configModalForm.showDetailScoreTable &&
+                groupItem.type === 'detail-title'
+              "
+              class="detail-score-table"
+            >
+              <tr>
+                <th>得分</th>
+                <th>评分人</th>
+              </tr>
+              <tr>
+                <td></td>
+                <td></td>
+              </tr>
+            </table>
+            <!-- 内容 -->
+            <template v-if="groupItem.elements && groupItem.elements.length">
+              <elem-rich-text
+                v-for="elem in groupItem.elements"
+                :id="`rich-text-${elem.id}`"
+                :key="elem.id"
+                :data="elem"
+              ></elem-rich-text>
+            </template>
+          </div>
+        </template>
+      </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>
     </div>
   </div>
 </template>
 
 <script>
+import ElemRichText from "../elements/rich-text/ElemRichText.vue";
 import PaperTemplateView from "../components/PaperTemplateView.vue";
 import PaperBuildConfig from "../components/PaperBuildConfig.vue";
-import { getModel as getRichTextModel } from "../elements/rich-text/model";
-import { getModel as getPageModel } from "../elements/page/model";
-import { getElementId, randomCode, deepCopy } from "../../card/plugins/utils";
-import { calcSum, maxNum } from "@/plugins/utils";
+import { deepCopy } from "../../card/plugins/utils";
 import previewTemp from "../previewTemp";
 import { paperDetailInfoApi } from "../../paper/api";
 import { paperTemplateListApi, paperPdfDownloadApi } from "../api";
 import { downloadByApi } from "@/plugins/download";
-// import paperJson from "./data/paper.json";
-// import paperTempJson from "./data/paper-temp.json";
-
-const numberToUpperCase = function (val) {
-  if (val < 1 || val > 26) return;
-
-  return String.fromCharCode(64 + val);
-};
-
-const checkRichTextHasCont = function (data) {
-  if (!data) return false;
-  if (!data.sections || !data.sections.length) return false;
-
-  if (!data.sections[0].blocks || !data.sections[0].blocks.length) return false;
-
-  return true;
-};
+import paperTemplateBuildMixins from "./paperTemplateBuildMixins";
 
 export default {
   name: "PaperTemplateBuild",
-  components: { PaperTemplateView, PaperBuildConfig },
+  components: { PaperTemplateView, PaperBuildConfig, ElemRichText },
+  mixins: [paperTemplateBuildMixins],
   data() {
     return {
       paperId: this.$route.params.paperId,
       viewType: this.$route.params.viewType,
       seqMode: "MODE1",
-      renderStructList: [],
-      pages: [],
       paperJson: {},
       paperTempJson: {
         pages: [],
@@ -99,8 +128,6 @@ export default {
       downloading: false,
       fieldData: {},
       paperStructs: [],
-      TEXT_INDENT_SIZE: 28,
-      textIndent: 28,
       configModalForm: {
         showDetailNo: true,
         showDetailScoreTable: false,
@@ -117,12 +144,6 @@ export default {
     this.initData();
   },
   methods: {
-    getTextIndexStyle() {
-      return {
-        textIndent: `-${this.textIndent}px`,
-        paddingLeft: `${this.textIndent}px`,
-      };
-    },
     async initFrame() {
       try {
         const paperSet = window.parent.paperSet;
@@ -199,14 +220,6 @@ export default {
         return;
       }
       this.paperTempChange(this.paperTempList[0]);
-
-      // test--->
-      // this.paperJson = paperJson;
-      // this.paperTempJson = paperTempJson;
-      // this.pages = paperTempJson.pages;
-      // this.$nextTick(() => {
-      //   this.buildData();
-      // });
     },
     async getPaperJson() {
       const res = await paperDetailInfoApi({
@@ -245,12 +258,6 @@ export default {
         });
       });
     },
-    async seqModeChange() {
-      await this.getPaperJson();
-      this.$nextTick(() => {
-        this.buildData();
-      });
-    },
     async getPaperTempList() {
       const res = await paperTemplateListApi("PAPER_EXPORT");
       this.paperTempList = res.data;
@@ -278,29 +285,6 @@ export default {
       });
       this.configSources = sources;
     },
-    buildConfigChange(val) {
-      this.configModalForm = val;
-      this.updaterFieldInfo();
-
-      this.$nextTick(() => {
-        this.buildData();
-      });
-    },
-    paperTempChange(paperTemp) {
-      // console.log(paperTemp);
-      this.curPaperTemp = paperTemp;
-      let paperTempJson = paperTemp.content
-        ? JSON.parse(paperTemp.content)
-        : { pages: [], pageConfig: {} };
-      this.paperTempJson = paperTempJson;
-      this.pages = paperTempJson.pages;
-      this.getConfigSources();
-      this.updaterFieldInfo();
-
-      this.$nextTick(() => {
-        this.buildData();
-      });
-    },
     updaterFieldInfo() {
       const VALID_ELEMENTS_FOR_EXTERNAL = ["FIELD_TEXT"];
       this.paperTempJson.pages.forEach((page) => {
@@ -329,536 +313,65 @@ export default {
         });
       });
     },
-    async buildData() {
-      this.maxColumnWidth = document.getElementById("column-0-0").offsetWidth;
-      this.maxColumnHeight =
-        document.getElementById("column-0-0").offsetHeight - 10;
-      this.parseRenderStructList();
-      this.buildPrePages();
-
-      const loadRes = await this.waitAllImgLoaded().catch(() => {});
-      if (!loadRes) {
-        this.$message.error("图片加载有误!");
-        return;
-      }
+    async seqModeChange() {
+      await this.getPaperJson();
       this.$nextTick(() => {
-        this.buildReleasePages();
-      });
-    },
-    parseRenderStructList() {
-      let renderStructList = [this.parseLineGap(), this.parseLineGap()];
-
-      this.paperJson.paperDetails.forEach((detail) => {
-        renderStructList.push(this.parseDetailTitle(detail));
-        if (checkRichTextHasCont(detail.description)) {
-          const descData = this.parseTitleOption(detail.description, "");
-          renderStructList.push(...descData);
-        }
-        detail.paperDetailUnits.forEach((question) => {
-          let questionInfo = question.question;
-          if (questionInfo.subQuestions && questionInfo.subQuestions.length) {
-            const bodys = this.parseTitleOption(questionInfo.quesBody, "");
-            renderStructList.push(...bodys);
-
-            const isMatches = this.checkIsMatches(questionInfo.questionType);
-            if (
-              isMatches &&
-              questionInfo.quesOptions &&
-              questionInfo.quesOptions.length
-            ) {
-              questionInfo.quesOptions.forEach((op) => {
-                const obodys = this.parseTitleOption(
-                  op.optionBody,
-                  `${numberToUpperCase(op.number)}、`,
-                  true,
-                  {
-                    contType: "option",
-                    textStyles: { paddingLeft: `${this.textIndent}px` },
-                  }
-                );
-                renderStructList.push(...obodys);
-              });
-            }
-
-            // 选词填空不展示小题
-            if (questionInfo.questionType === "BANKED_CLOZE") {
-              renderStructList.push(this.parseLineGap());
-              return;
-            }
-
-            questionInfo.subQuestions.forEach((sq, sqindex) => {
-              sq.subNumber = sqindex + 1;
-              if (isMatches) sq.quesOptions = []; // 选词填空、段落匹配小题中不展示选项
-              const contents = this.parseSimpleQuestion(sq, false);
-              renderStructList.push(...contents);
-              renderStructList.push(this.parseLineGap());
-            });
-          } else {
-            questionInfo.number = question.number;
-            const datas = this.parseSimpleQuestion(questionInfo, true);
-            renderStructList.push(...datas);
-            renderStructList.push(this.parseLineGap());
-          }
-        });
-      });
-
-      // 去掉最后一题的间隔行
-      // console.log(renderStructList);
-      this.renderStructList = renderStructList.slice(0, -1);
-    },
-    getRichStruct(blocks) {
-      return {
-        sections: [
-          {
-            blocks: [...blocks],
-          },
-        ],
-      };
-    },
-    transformRichJson(richJson) {
-      if (!richJson || !richJson.sections) return [];
-      let contents = [];
-      let curBlock = [];
-      const checkNeedSplitSection = (block) => {
-        if (block.type !== "image") return false;
-
-        if (block.param) {
-          if (block.param.width)
-            return block.param.width > this.maxColumnWidth / 2;
-          if (block.param.height) return block.param.height > 150;
-        }
-        return true;
-      };
-
-      richJson.sections.forEach((section) => {
-        section.blocks.forEach((block) => {
-          if (checkNeedSplitSection(block) && curBlock.length) {
-            contents.push(this.getRichStruct(curBlock));
-            curBlock = [];
-          }
-          curBlock.push(block);
-        });
-
-        if (curBlock.length) {
-          contents.push(this.getRichStruct(curBlock));
-          curBlock = [];
-        }
-      });
-      return contents;
-    },
-    changeRichTextCloze(richText) {
-      return {
-        sections: richText.sections.map((section) => {
-          return {
-            blocks: section.blocks.map((item) => {
-              if (item.type === "cloze") {
-                return {
-                  type: "text",
-                  value: "_____",
-                };
-              } else {
-                return { ...item };
-              }
-            }),
-          };
-        }),
-      };
-    },
-    parseSimpleQuestion(question) {
-      let contents = [];
-      let quesBody = question.quesBody;
-      if (question.questionType === "FILL_BLANK_QUESTION") {
-        quesBody = this.changeRichTextCloze(quesBody);
-      }
-
-      const tbodys = this.parseTitleOption(
-        quesBody,
-        `${question.questionSeq}、`,
-        true,
-        {
-          textStyles: { paddingLeft: `${this.textIndent}px` },
-        }
-      );
-      contents.push(...tbodys);
-      const hasNobody = !tbodys.length;
-
-      if (question.quesOptions && question.quesOptions.length) {
-        question.quesOptions.forEach((op, oIndex) => {
-          let noVal = `${numberToUpperCase(op.number)}、`;
-          if (!hasNobody) {
-            const obodys = this.parseTitleOption(op.optionBody, noVal, false, {
-              contType: "option",
-              textStyles: { paddingLeft: `${this.textIndent}px` },
-            });
-            contents.push(...obodys);
-            return;
-          }
-
-          if (!oIndex) {
-            // 针对如完形填空的小题做的特殊处理
-            noVal = `${question.questionSeq}、${noVal}`;
-          }
-          this.textIndent = 2 * this.TEXT_INDENT_SIZE;
-          const obodys = this.parseTitleOption(op.optionBody, noVal, true, {
-            contType: "option",
-            textStyles: { paddingLeft: `${this.textIndent}px` },
-          });
-          contents.push(...obodys);
-          this.textIndent = this.TEXT_INDENT_SIZE;
-        });
-      }
-      return contents;
-    },
-    parseDetailTitle(data) {
-      let blocks = [
-        {
-          type: "text",
-          value: `${data.name}(共${data.unitCount}小题,满分${data.score}分)`,
-        },
-      ];
-      if (this.configModalForm.showDetailNo) {
-        blocks.unshift({
-          type: "text",
-          value: `${data.cnNum}、`,
-        });
-      }
-      let content = this.getRichStruct(blocks);
-      return getRichTextModel({
-        styles: { width: "100%", fontWeight: 900 },
-        content,
-        classNames: this.configModalForm.showDetailScoreTable
-          ? "is-detail-title"
-          : "",
-      });
-    },
-    parseTitleOption(
-      richJson,
-      noVal,
-      needIndent = false,
-      modelData = { textStyles: null, styles: null, contType: "" }
-    ) {
-      if (!richJson) return [];
-      const { textStyles, styles, contType } = modelData;
-      const bodys = this.transformRichJson(richJson);
-
-      return bodys.map((body, index) => {
-        let presetData = {
-          content: body,
-        };
-        if (contType) presetData.contType = contType;
-        if (textStyles) presetData.textStyles = textStyles;
-        if (styles) {
-          presetData.styles = styles;
-        } else {
-          presetData.styles = {
-            width: contType === "option" ? "auto" : "100%",
-          };
-        }
-
-        if (index === 0 && noVal) {
-          let cont = {
-            type: "text",
-            value: noVal,
-          };
-          if (needIndent) {
-            cont.param = {
-              width: this.textIndent,
-            };
-            presetData.textStyles = {
-              ...(presetData.textStyles || {}),
-              ...this.getTextIndexStyle(),
-            };
-          }
-          body.sections[0].blocks.unshift(cont);
-        }
-
-        return getRichTextModel(presetData);
-      });
-    },
-    parseLineGap() {
-      return getRichTextModel({
-        contType: "gap",
-        styles: { width: "100%", height: "10px" },
-        content: this.getRichStruct([{ type: "text", value: "" }]),
-      });
-    },
-    checkIsMatches(structType) {
-      const matchesTypes = ["BANKED_CLOZE", "PARAGRAPH_MATCHING"];
-      return matchesTypes.includes(structType);
-    },
-    buildPrePages() {
-      let pages = deepCopy(this.paperTempJson.pages);
-      const firstPageNo = pages.findIndex((p) => p.pageType !== "cover");
-      pages[firstPageNo].columns[0].texts = [];
-      pages[firstPageNo].columns[0].texts.push(...this.renderStructList);
-      this.pages = pages;
-    },
-    buildReleasePages() {
-      this.addDetailScoreTable();
-      this.resetRenderStructSize();
-      // console.log(this.renderStructList);
-      this.buildPageAutoPage();
-
-      this.$nextTick(async () => {
-        this.addDetailScoreTable();
-
-        if (this.prepareDownloadPdf) {
-          await this.downloadPaperPdf().catch(() => {});
-          this.prepareDownloadPdf = false;
-        }
+        this.buildData();
       });
     },
-    addDetailScoreTable() {
-      if (!this.configModalForm.showDetailScoreTable) return;
+    buildConfigChange(val) {
+      this.configModalForm = val;
+      this.updaterFieldInfo();
 
-      const scoreTableHtml = `<table class="detail-score-table"><tr><th>得分</th><th>评分人</th></tr><tr><td></td><td></td></tr></table>`;
-      const dom = document.createElement("div");
-      dom.innerHTML = scoreTableHtml;
-      document.querySelectorAll(".is-detail-title").forEach((node) => {
-        const hasScoreTable =
-          node.firstChild.className.includes("detail-score-table");
-        if (hasScoreTable) return;
-        node.insertBefore(dom.firstChild.cloneNode(true), node.firstChild);
+      this.$nextTick(() => {
+        this.buildData();
       });
     },
-    resetRenderStructSize() {
-      let curOptions = [];
-      this.renderStructList.forEach((elem, eindex) => {
-        const elemDom = document.getElementById(`rich-text-${elem.id}`);
-        elem.w = elemDom.offsetWidth;
-        elem.h = elemDom.offsetHeight;
-
-        if (elem.contType !== "option") return;
-
-        curOptions.push(elem);
-
-        // 全选选项逻辑
-        const nextElem = this.renderStructList[eindex + 1];
-        if (nextElem && nextElem.contType === "option") return;
-
-        curOptions.forEach((optionElem) => {
-          optionElem._percent = this.getSizePercent(
-            optionElem.w,
-            this.maxColumnWidth
-          );
-        });
-
-        const optionCount = curOptions.length;
-        // 奇数选项,全部一行
-        if (optionCount <= 7 && optionCount % 2 > 0) {
-          curOptions.forEach((optionElem) => {
-            optionElem._percent = 1;
-            optionElem.styles.width = "100%";
-          });
-          curOptions = [];
-          return;
-        }
-
-        const percents = curOptions.map((item) => item._percent);
-        const maxPercent = maxNum(percents);
-        // let aveOptionPercent = 1;
-        // if (optionCount % 4 === 0) {
-        //   aveOptionPercent = this.calcAveOptionPercent(maxPercent);
-        // } else {
-        //   aveOptionPercent = maxPercent > 0.5 ? 1 : 0.5;
-        // }
-        const aveOptionPercent = this.calcAveOptionPercent(maxPercent);
-
-        curOptions.forEach((optionElem) => {
-          optionElem._percent = aveOptionPercent;
-          optionElem.styles.width = aveOptionPercent * 100 + "%";
-        });
+    paperTempChange(paperTemp) {
+      // console.log(paperTemp);
+      this.curPaperTemp = paperTemp;
+      let paperTempJson = paperTemp.content
+        ? JSON.parse(paperTemp.content)
+        : { pages: [], pageConfig: {} };
+      this.paperTempJson = paperTempJson;
+      this.pages = paperTempJson.pages;
+      this.getConfigSources();
+      this.updaterFieldInfo();
 
-        curOptions = [];
-      });
-      this.renderStructList.forEach((elem) => {
-        if (elem.styles.width === "100%") {
-          this.$set(elem.styles, "display", "block");
-        }
+      this.$nextTick(() => {
+        this.buildData();
       });
     },
-    buildPageAutoPage() {
-      let pages = [];
-      let curPage = null,
-        curElem = null;
-      let curColumn = null,
-        curColumnNo = 0,
-        curColumnHeight = 0;
-      let curLinePercent = 0;
-      let groups = [],
-        curGroup = [];
-
-      // 分组自动分页 选项分组
-      const getNextElem = () => {
-        return this.renderStructList.shift();
-      };
-
-      curElem = getNextElem();
-      while (curElem) {
-        if (
-          curElem.contType !== "option" ||
-          (curElem.contType === "option" && curElem._percent === 1)
-        ) {
-          if (curGroup.length) {
-            groups.push(curGroup);
-            curGroup = [];
-            curLinePercent = 0;
-          }
-
-          groups.push([curElem]);
-          curElem = getNextElem();
-        } else {
-          if (curLinePercent + curElem._percent > 1) {
-            groups.push(curGroup);
-            curGroup = [];
-            curLinePercent = 0;
-          } else {
-            curGroup.push(curElem);
-            curLinePercent += curElem._percent;
-            curElem = getNextElem();
-          }
-        }
-      }
-      if (curGroup.length) {
-        groups.push(curGroup);
-        curGroup = [];
-      }
-      // console.log(groups);
-
-      const getNextGroup = () => {
-        return groups.shift();
-      };
-      curGroup = getNextGroup();
-      while (curGroup) {
-        if (!curPage) {
-          curPage = this.getNewPageModel(pages.length);
-        }
-
-        if (!curColumn) {
-          curColumn = curPage.columns[curColumnNo++];
-          curColumnHeight = this.calcInitColumnHeight(curColumn);
-        }
-
-        let curGroupHeigth =
-          curGroup.length === 1
-            ? curGroup[0].h
-            : Math.max.apply(
-                null,
-                curGroup.map((item) => item.h)
-              );
-
-        if (curGroupHeigth + curColumnHeight > this.maxColumnHeight) {
-          // 当前栏第一个元素就超过最大高度时,直接放当前栏
-          if (!curColumn.texts.length) {
-            curColumn.texts.push(...curGroup);
-            curGroup = getNextGroup();
-          }
-          // 当前栏满了
-          if (curColumnNo >= curPage.columnNumber) {
-            // 当前页满了
-            pages.push(curPage);
-            curPage = null;
-            curColumnNo = 0;
-          }
-          curColumn = null;
-          curColumnHeight = 0;
-        } else {
-          // 当前栏未满
-          curColumnHeight += curGroupHeigth;
-          curColumn.texts.push(...curGroup);
-          curGroup = getNextGroup();
-        }
-      }
-
-      if (curPage) {
-        pages.push(curPage);
-        curPage = null;
-      }
-      // 正文部分保证偶数页
-      if (pages.length % 2) {
-        pages.push(this.getNewPageModel(pages.length));
-      }
+    async buildData() {
+      this.maxColumnWidth = document.getElementById("column-0-0").offsetWidth;
+      this.maxColumnHeight =
+        document.getElementById("column-0-0").offsetHeight - 10;
 
-      if (this.paperTempJson.pageConfig.showCover) {
-        // 封面自动插入反面空白页
-        let coverPages = deepCopy(
-          this.paperTempJson.pages.filter((p) => p.pageType === "cover")
-        );
-        let coverBackPage = deepCopy(coverPages[0]);
-        coverBackPage.columns.forEach((column) => {
-          column.elements = [];
-        });
-        let nCoverPages = [];
-        coverPages.forEach((cpage) => {
-          nCoverPages.push(cpage);
-          nCoverPages.push(coverBackPage);
-        });
-        pages = [...nCoverPages, ...pages];
+      this.buildElementsFromStruct();
+      const loadRes = await this.waitAllImgLoaded().catch(() => {});
+      if (!loadRes) {
+        this.$message.error("图片加载有误!");
+        return;
       }
+      this.$nextTick(() => {
+        this.updateElementWidthInfo();
+        this.buildGroupsFromStruct();
+        this.buildPrePages();
 
-      this.pages = pages;
-    },
-    getNewPageModel(pageNo) {
-      let contentPages = this.paperTempJson.pages.slice(-2);
-      let pNo = pageNo % 2;
-      const pageTemp = contentPages[pNo];
-      let newPage = getPageModel({
-        ...this.paperTempJson.pageConfig,
-        pageType: pageTemp.pageType,
-      });
-      newPage.sides = pageTemp.sides.map((elem) => {
-        let nelem = deepCopy(elem);
-        nelem.id = getElementId();
-        nelem.key = randomCode();
-
-        // if (pNo === 1 && nelem.type === "GUTTER") {
-        //   nelem.direction = "right";
-        // }
-        return nelem;
-      });
-      newPage.columns.forEach((column) => {
-        column.texts = [];
-      });
-
-      if (pageNo > 1) return newPage;
+        this.$nextTick(() => {
+          this.updateGroupHeightInfo();
+          this.buildPagesByAutoPage();
 
-      newPage.columns.forEach((column, cindex) => {
-        column.elements = pageTemp.columns[cindex].elements.map((elem) => {
-          let nelem = deepCopy(elem);
-          nelem.id = getElementId();
-          nelem.key = randomCode();
-          nelem.h = this.getElementHeight(`preview-${elem.id}`);
-          if (nelem.elements && nelem.elements.length) {
-            nelem.elements.forEach((celem) => {
-              celem.id = getElementId();
-              celem.key = randomCode();
+          if (this.prepareDownloadPdf) {
+            this.$nextTick(async () => {
+              await this.downloadPaperPdf().catch(() => {});
+              this.prepareDownloadPdf = false;
             });
           }
-          return nelem;
         });
-        column.texts = [];
       });
-      return newPage;
-    },
-    getElementHeight(elementId) {
-      const dom = document.getElementById(elementId);
-      return dom ? dom.offsetHeight : 0;
     },
-    calcAveOptionPercent(maxPercent) {
-      if (maxPercent > 0.5) return 1;
-      if (maxPercent > 0.25) return 0.5;
-      return 0.25;
-    },
-    calcInitColumnHeight(column) {
-      return calcSum(column.elements.map((item) => item.h));
-    },
-    getSizePercent(size, fullSize) {
-      const rate = size / fullSize;
-      if (rate <= 0.25) return 0.25;
-      if (rate <= 0.5) return 0.5;
-      return 1;
-    },
-    // img
+    // img ------ start >
     loadImg(url) {
       return new Promise((resolve, reject) => {
         const img = new Image();
@@ -885,8 +398,7 @@ export default {
     },
     async waitAllImgLoaded() {
       let imgUrls = [];
-      this.renderStructList.forEach((item) => {
-        if (item.contType === "gap") return;
+      this.elementList.forEach((item) => {
         imgUrls.push(...this.getRichJsonImgUrls(item.content));
       });
 
@@ -901,20 +413,9 @@ export default {
         return Promise.reject();
       }
     },
+    // img ------ end >
+    // download ------ start >
     getPreviewTemp() {
-      const elementDoms =
-        this.$refs.PaperTemplateView.$el.querySelectorAll(".elem-rich-text");
-      elementDoms.forEach((eDom) => {
-        const width = eDom.offsetWidth;
-        if (eDom.firstChild && eDom.firstChild.nodeName === "DIV") {
-          eDom.firstChild.style.width = width + "px";
-          if (
-            eDom.firstChild.firstChild &&
-            eDom.firstChild.firstChild.nodeName === "DIV"
-          )
-            eDom.firstChild.firstChild.style.width = "100%";
-        }
-      });
       return previewTemp(this.$refs.PaperTemplateView.$el.outerHTML);
     },
     async toDownload() {
@@ -978,4 +479,14 @@ export default {
   margin-bottom: 16px;
   margin-right: 30px;
 }
+.paper-template-build .element-list {
+  visibility: hidden;
+  position: absolute;
+  width: 1200px;
+  height: 600px;
+  overflow: hidden;
+  left: -9999px;
+  top: 0;
+  z-index: 1;
+}
 </style>

+ 640 - 0
src/modules/paper-export/views/paperTemplateBuildMixins.js

@@ -0,0 +1,640 @@
+import { isAnEmptyRichText, numberToUpperCase } from "@/utils/utils";
+import { getModel as getRichTextModel } from "../elements/rich-text/model";
+import { maxNum, randomCode, deepCopy, calcSum } from "@/plugins/utils";
+import { getModel as getPageModel } from "../elements/page/model";
+import { getElementId } from "../../card/plugins/utils";
+
+/**
+- 根据试卷构建文本元素,序号和内容分开。同时确定分组信息。
+- 第一次渲染元素,确定宽度信息。
+- 根据分组信息,细化分组段落信息。
+- 第二次渲染段落,确定各段落高度信息。
+- 自动分页。
+- 第三次渲染。
+ */
+export default {
+  data() {
+    return {
+      elementList: [],
+      groups: [],
+      sections: [],
+      structRelateElement: [],
+      TEXT_INDENT_SIZE: 28,
+      pages: [],
+    };
+  },
+  methods: {
+    buildData() {
+      this.maxColumnWidth = document.getElementById("column-0-0").offsetWidth;
+      this.maxColumnHeight =
+        document.getElementById("column-0-0").offsetHeight - 10;
+    },
+    buildElementsFromStruct() {
+      let elementList = [];
+      let structRelateElement = [];
+
+      this.paperJson.paperDetails.forEach((detail) => {
+        let detailRelateElement = { title: [], description: [], questions: [] };
+        // 大题名称
+        const detailTitleElement = this.parseDetailTitle(detail);
+        elementList.push(detailTitleElement);
+        detailRelateElement.title = [detailTitleElement.id];
+        // 大题描述
+        if (isAnEmptyRichText(detail.description)) {
+          const { elements, elementIds } = this.parseTitleOption(
+            detail.description,
+            ""
+          );
+          elementList.push(...elements);
+          detailRelateElement.description = elementIds;
+        }
+        // 小题
+        detail.paperDetailUnits.forEach((question) => {
+          let questionInfo = question.question;
+
+          questionInfo.number = question.number;
+          const { contents, questionRelateElement } =
+            this.parseSimpleQuestion(questionInfo);
+          elementList.push(...contents);
+          detailRelateElement.questions.push(questionRelateElement);
+        });
+
+        structRelateElement.push(detailRelateElement);
+      });
+      // console.log(elements);
+      this.structRelateElement = structRelateElement;
+      this.elementList = elementList;
+    },
+    updateElementWidthInfo() {
+      this.elementList.forEach((element) => {
+        element.w = document.getElementById(element.id).offsetWidth;
+      });
+    },
+    buildGroupsFromStruct() {
+      let groups = [];
+      let elementMap = {};
+      this.elementList.forEach((item) => {
+        elementMap[item.id] = item;
+      });
+      // utils
+      const getElementsByIds = (ids) => {
+        return ids.map((id) => elementMap[id]);
+      };
+      const getGroupData = ({
+        type = "content",
+        sno = "",
+        elements = [],
+        styles = {},
+      }) => {
+        return {
+          id: `group-${randomCode()}`,
+          type,
+          sno,
+          elements,
+          styles,
+          h: null,
+        };
+      };
+      const getLineGap = () => {
+        return getGroupData({
+          type: "gap",
+          styles: { height: "10px" },
+        });
+      };
+      // 选项
+      const parseOptions = (options, questionType) => {
+        const optionCount = options.length;
+        const IS_PARAGRAPH_MATCHING = questionType === "PARAGRAPH_MATCHING";
+        const IS_BANKED_CLOZE = questionType === "BANKED_CLOZE";
+
+        // 构建选项元素信息
+        const opitonList = options.map((optionIds) => {
+          const elements = getElementsByIds(optionIds);
+          const width = maxNum(elements.map((elem) => elem.w));
+          const percent = this.getSizePercent(
+            width,
+            this.maxColumnWidth - this.TEXT_INDENT_SIZE
+          );
+          return {
+            percent,
+            resetPercent: null,
+            elements,
+          };
+        });
+
+        // 确定选项宽度比例
+        let resetPercent = 1;
+        // 奇数选项,段落匹配,全部一行
+        if (
+          (optionCount % 2 > 0 && !IS_BANKED_CLOZE) ||
+          IS_PARAGRAPH_MATCHING
+        ) {
+          resetPercent = 1;
+        } else {
+          const maxPercent = maxNum(opitonList.map((item) => item.percent));
+          resetPercent = this.getResetPercent(maxPercent);
+        }
+        opitonList.forEach((item) => {
+          item.resetPercent = resetPercent;
+          // 不占一行的选项合并结构
+          if (resetPercent < 1 && item.elements.length > 1) {
+            let element = getRichTextModel({
+              contType: "option",
+              content: this.mergeRichStruct(
+                item.elements.forEach((el) => el.content)
+              ),
+            });
+            item.elements = [element];
+          }
+          // 更新选项元素宽度信息
+          item.elements.forEach((elem) => {
+            elem.textStyles.width = resetPercent * 100 + "%";
+          });
+        });
+
+        // 选项分组
+        let curGroupElements = [],
+          optionGroups = [],
+          curGroupPercent = 0;
+        opitonList.forEach((option) => {
+          // 占一行的选项可能会有多个分组。
+          if (resetPercent === 1) {
+            const optionItemGroups = option.elements.map((item, ind) => {
+              // 第一个分组首行缩进,之后的首行不缩进
+              const textStyles = IS_PARAGRAPH_MATCHING
+                ? ind === 0
+                  ? this.getTextIndentStyle()
+                  : {}
+                : ind === 0
+                ? this.getFullTextIndentStyle()
+                : this.getIndentStyle();
+              item.textStyles = { ...item.textStyles, ...textStyles };
+              return getGroupData({
+                type: "option",
+                elements: [item],
+                styles: this.getIndentStyle(),
+              });
+            });
+            optionGroups.push(...optionItemGroups);
+            return;
+          }
+
+          // 不占一行的选项只创建一个分组
+          if (curGroupPercent + option.resetPercent > 1) {
+            if (curGroupElements.length) {
+              optionGroups.push(
+                getGroupData({
+                  type: "option",
+                  elements: curGroupElements,
+                  styles: this.getIndentStyle(),
+                })
+              );
+              curGroupElements = [];
+              curGroupPercent = 0;
+            }
+          }
+
+          curGroupElements.push(...option.elements);
+          curGroupPercent += option.resetPercent;
+        });
+
+        if (curGroupElements.length) {
+          optionGroups.push(
+            getGroupData({
+              type: "option",
+              elements: curGroupElements,
+              styles: this.getIndentStyle(),
+            })
+          );
+        }
+        return optionGroups;
+      };
+      // 试题
+      const parseQuestion = (question) => {
+        // 题干
+        // 没有题目序号时,不缩进
+        const hasQuestionSeq = !!question.questionSeq;
+        const bodyGroups = getElementsByIds(question.body).map(
+          (item, index) => {
+            if (!hasQuestionSeq) {
+              return getGroupData({
+                elements: [item],
+              });
+            }
+
+            if (!index) item.textStyles = this.getTextIndentStyle();
+            return getGroupData({
+              elements: [item],
+              styles: this.getIndentStyle(),
+            });
+          }
+        );
+        groups.push(...bodyGroups);
+        // 选项
+        if (question.options.length) {
+          let optionsGroups = parseOptions(
+            question.options,
+            question.questionType
+          );
+          // 无题干有选项时,题号放选项第一分组第一行
+          // 主要是针对完形填空的特殊处理
+          if (!bodyGroups.length) {
+            optionsGroups[0].elements[0].content.sections[0].blocks.unshift({
+              type: "text",
+              value: `${question.questionSeq}、`,
+              param: {
+                width: this.TEXT_INDENT_SIZE,
+              },
+            });
+            optionsGroups[0].styles = {};
+            if (optionsGroups[0].elements[0].textStyles.paddingLeft) {
+              optionsGroups[0].elements[0].textStyles = {
+                ...optionsGroups[0].elements[0].textStyles,
+                textIndent: `-${this.TEXT_INDENT_SIZE * 2}px`,
+                paddingLeft: `${this.TEXT_INDENT_SIZE * 2}px`,
+              };
+            } else {
+              optionsGroups[0].elements[0].textStyles = {
+                ...optionsGroups[0].elements[0].textStyles,
+                ...this.getFullTextIndentStyle(),
+              };
+            }
+          }
+          groups.push(...optionsGroups);
+        }
+        // 小题
+        if (question.subQuestions && question.subQuestions.length) {
+          question.subQuestions.forEach((subQuestion) => {
+            parseQuestion(subQuestion);
+          });
+        } else {
+          groups.push(getLineGap());
+        }
+      };
+
+      // body
+      groups = [getLineGap(), getLineGap()];
+      this.structRelateElement.forEach((detail) => {
+        // 大题名称
+        const detailTitleGroups = getElementsByIds(detail.title).map((item) =>
+          getGroupData({
+            elements: [item],
+            type: "detail-title",
+            styles: { fontWeight: 600 },
+          })
+        );
+        groups.push(...detailTitleGroups);
+        // 大题描述
+        const detailDescriptionGroups = getElementsByIds(
+          detail.description
+        ).map((item) => getGroupData({ elements: [item] }));
+        groups.push(...detailDescriptionGroups);
+        // 小题
+        detail.questions.forEach((question) => {
+          parseQuestion(question);
+        });
+      });
+
+      this.groups = groups;
+    },
+    buildPrePages() {
+      let pages = deepCopy(this.paperTempJson.pages);
+      const firstPageNo = pages.findIndex((p) => p.pageType !== "cover");
+      pages[firstPageNo].columns[0].texts = [];
+      pages[firstPageNo].columns[0].texts.push(...this.groups);
+      this.pages = pages;
+    },
+    updateGroupHeightInfo() {
+      this.groups.forEach((group) => {
+        group.h = document.getElementById(group.id).offsetHeight;
+      });
+    },
+    buildPagesByAutoPage() {
+      let pages = [];
+      let curPage = null;
+      let curColumn = null;
+      let curColumnNo = 0;
+      let curColumnHeight = 0;
+      let curGroup = [];
+
+      const getNextGroup = () => {
+        return this.groups.shift();
+      };
+      curGroup = getNextGroup();
+      while (curGroup) {
+        if (!curPage) {
+          curPage = this.getNewPageModel(pages.length);
+        }
+
+        if (!curColumn) {
+          curColumn = curPage.columns[curColumnNo++];
+          curColumnHeight = this.calcInitColumnHeight(curColumn);
+        }
+
+        if (curGroup.h + curColumnHeight > this.maxColumnHeight) {
+          // 当前栏第一个元素就超过最大高度时,直接放当前栏
+          if (!curColumn.texts.length) {
+            curColumn.texts.push(curGroup);
+            curGroup = getNextGroup();
+          }
+          // 当前栏满了
+          if (curColumnNo >= curPage.columnNumber) {
+            // 当前页满了
+            pages.push(curPage);
+            curPage = null;
+            curColumnNo = 0;
+          }
+          curColumn = null;
+          curColumnHeight = 0;
+        } else {
+          // 当前栏未满
+          curColumnHeight += curGroup.h;
+          curColumn.texts.push(curGroup);
+          curGroup = getNextGroup();
+        }
+      }
+
+      if (curPage) {
+        pages.push(curPage);
+        curPage = null;
+      }
+      // 正文部分保证偶数页
+      if (pages.length % 2) {
+        pages.push(this.getNewPageModel(pages.length));
+      }
+
+      if (this.paperTempJson.pageConfig.showCover) {
+        // 封面自动插入反面空白页
+        let coverPages = deepCopy(
+          this.paperTempJson.pages.filter((p) => p.pageType === "cover")
+        );
+        let coverBackPage = deepCopy(coverPages[0]);
+        coverBackPage.columns.forEach((column) => {
+          column.elements = [];
+        });
+        let nCoverPages = [];
+        coverPages.forEach((cpage) => {
+          nCoverPages.push(cpage);
+          nCoverPages.push(coverBackPage);
+        });
+        pages = [...nCoverPages, ...pages];
+      }
+
+      this.pages = pages;
+    },
+    // other
+    getFullTextIndentStyle() {
+      return {
+        textIndent: `-${this.TEXT_INDENT_SIZE}px`,
+        paddingLeft: `${this.TEXT_INDENT_SIZE}px`,
+      };
+    },
+    getIndentStyle() {
+      return {
+        paddingLeft: `${this.TEXT_INDENT_SIZE}px`,
+      };
+    },
+    getTextIndentStyle() {
+      return {
+        textIndent: `-${this.TEXT_INDENT_SIZE}px`,
+      };
+    },
+    getSizePercent(size, fullSize) {
+      const rate = size / fullSize;
+      if (rate <= 0.25) return 0.25;
+      if (rate <= 0.5) return 0.5;
+      return 1;
+    },
+    getResetPercent(maxPercent) {
+      if (maxPercent > 0.5) return 1;
+      if (maxPercent > 0.25) return 0.5;
+      return 0.25;
+    },
+    calcInitColumnHeight(column) {
+      return calcSum(column.elements.map((item) => item.h));
+    },
+    checkIsMatches(structType) {
+      const matchesTypes = ["BANKED_CLOZE", "PARAGRAPH_MATCHING"];
+      return matchesTypes.includes(structType);
+    },
+    changeRichTextCloze(richText) {
+      return {
+        sections: richText.sections.map((section) => {
+          return {
+            blocks: section.blocks.map((item) => {
+              if (item.type === "cloze") {
+                return {
+                  type: "text",
+                  value: "_____",
+                };
+              } else {
+                return { ...item };
+              }
+            }),
+          };
+        }),
+      };
+    },
+    getRichStruct(blocks) {
+      return {
+        sections: [
+          {
+            blocks: [...blocks],
+          },
+        ],
+      };
+    },
+    mergeRichStruct(structs) {
+      let sections = [];
+      structs.forEach((item) => {
+        sections.push(...item.sections);
+      });
+      return { sections };
+    },
+    transformRichJson(richJson) {
+      if (!richJson || !richJson.sections) return [];
+
+      let contents = [];
+      let curBlock = [];
+      const checkNeedSplitSection = (block) => {
+        if (block.type !== "image") return false;
+
+        if (block.param) {
+          if (block.param.width)
+            return block.param.width > this.maxColumnWidth / 2;
+          if (block.param.height) return block.param.height > 150;
+        }
+        return true;
+      };
+
+      richJson.sections.forEach((section) => {
+        section.blocks.forEach((block) => {
+          if (checkNeedSplitSection(block) && curBlock.length) {
+            contents.push(this.getRichStruct(curBlock));
+            curBlock = [];
+          }
+          curBlock.push(block);
+        });
+
+        if (curBlock.length) {
+          contents.push(this.getRichStruct(curBlock));
+          curBlock = [];
+        }
+      });
+      return contents;
+    },
+    parseLineGap() {
+      return getRichTextModel({
+        contType: "gap",
+        styles: { width: "100%", height: "10px" },
+        content: this.getRichStruct([{ type: "text", value: "" }]),
+      });
+    },
+    parseDetailTitle(data) {
+      let blocks = [
+        {
+          type: "text",
+          value: `${data.name}(共${data.unitCount}小题,满分${data.score}分)`,
+        },
+      ];
+      if (this.configModalForm.showDetailNo) {
+        blocks.unshift({
+          type: "text",
+          value: `${data.cnNum}、`,
+        });
+      }
+      let content = this.getRichStruct(blocks);
+      return getRichTextModel({
+        styles: { width: "100%", fontWeight: 900 },
+        content,
+        classNames: this.configModalForm.showDetailScoreTable
+          ? "is-detail-title"
+          : "",
+      });
+    },
+    parseTitleOption(richJson, noVal, contType = "content") {
+      if (isAnEmptyRichText(richJson)) return { elements: [], elementIds: [] };
+
+      const bodys = this.transformRichJson(richJson);
+      const elements = bodys.map((body, index) => {
+        let presetData = {
+          content: body,
+          contType,
+        };
+
+        if (index === 0 && noVal) {
+          let cont = {
+            type: "text",
+            value: noVal,
+            param: {
+              width: this.TEXT_INDENT_SIZE,
+            },
+          };
+          body.sections[0].blocks.unshift(cont);
+        }
+
+        return getRichTextModel(presetData);
+      });
+      return { elements, elementIds: elements.map((item) => item.id) };
+    },
+    parseSimpleQuestion(question) {
+      let questionRelateElement = {
+        questionType: question.questionType,
+        questionSeq: question.questionSeq,
+        body: [],
+        options: [],
+        subQuestions: [],
+      };
+      const isMatches = this.checkIsMatches(question.questionType);
+      let contents = [];
+      let quesBody = question.quesBody;
+      if (question.questionType === "FILL_BLANK_QUESTION") {
+        quesBody = this.changeRichTextCloze(quesBody);
+      }
+
+      const { elements: bodyElements, elementIds: bodyElementIds } =
+        this.parseTitleOption(
+          quesBody,
+          question.questionSeq ? `${question.questionSeq}、` : ""
+        );
+      contents.push(...bodyElements);
+      questionRelateElement.body = bodyElementIds;
+
+      if (question.quesOptions && question.quesOptions.length) {
+        question.quesOptions.forEach((op) => {
+          let noVal = `${numberToUpperCase(op.number)}、`;
+          const { elements, elementIds } = this.parseTitleOption(
+            op.optionBody,
+            noVal,
+            "option"
+          );
+          contents.push(...elements);
+          questionRelateElement.options.push(elementIds);
+        });
+      }
+
+      // 选词填空不展示小题
+      if (question.questionType === "BANKED_CLOZE") {
+        return { contents, questionRelateElement };
+      }
+
+      if (question.subQuestions && question.subQuestions.length) {
+        question.subQuestions.forEach((sq, sqindex) => {
+          sq.subNumber = sqindex + 1;
+          if (isMatches) sq.quesOptions = []; // 选词填空、段落匹配小题中不展示选项
+          const {
+            contents: subContents,
+            questionRelateElement: subQuestionRelateElement,
+          } = this.parseSimpleQuestion(sq);
+          contents.push(...subContents);
+          questionRelateElement.subQuestions.push(subQuestionRelateElement);
+        });
+      }
+
+      return { contents, questionRelateElement };
+    },
+    // page
+    getNewPageModel(pageNo) {
+      let contentPages = this.paperTempJson.pages.slice(-2);
+      let pNo = pageNo % 2;
+      const pageTemp = contentPages[pNo];
+      let newPage = getPageModel({
+        ...this.paperTempJson.pageConfig,
+        pageType: pageTemp.pageType,
+      });
+      newPage.sides = pageTemp.sides.map((elem) => {
+        let nelem = deepCopy(elem);
+        nelem.id = getElementId();
+        nelem.key = randomCode();
+
+        // if (pNo === 1 && nelem.type === "GUTTER") {
+        //   nelem.direction = "right";
+        // }
+        return nelem;
+      });
+      newPage.columns.forEach((column) => {
+        column.texts = [];
+      });
+
+      if (pageNo > 1) return newPage;
+
+      newPage.columns.forEach((column, cindex) => {
+        column.elements = pageTemp.columns[cindex].elements.map((elem) => {
+          let nelem = deepCopy(elem);
+          nelem.id = getElementId();
+          nelem.key = randomCode();
+          nelem.h = document.getElementById(`preview-${elem.id}`).offsetHeight;
+          if (nelem.elements && nelem.elements.length) {
+            nelem.elements.forEach((celem) => {
+              celem.id = getElementId();
+              celem.key = randomCode();
+            });
+          }
+          return nelem;
+        });
+        column.texts = [];
+      });
+      return newPage;
+    },
+  },
+};

+ 11 - 0
src/utils/utils.js

@@ -37,3 +37,14 @@ export function checkRichTextHasAudio(cont) {
     section.blocks.some((block) => block.type === "audio")
   );
 }
+
+/**
+ * 数字转大写字母
+ * @param {number} val 数字
+ * @returns 字母
+ */
+export function numberToUpperCase(val) {
+  if (val < 1 || val > 26) return;
+
+  return String.fromCharCode(64 + val);
+}

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä