PaperTemplateBuild.vue 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981
  1. <template>
  2. <div class="paper-template-build">
  3. <div class="paper-template-build-body">
  4. <div class="margin_top_10">
  5. <el-select
  6. v-model="seqMode"
  7. class="margin-right-10"
  8. size="small"
  9. @change="seqModeChange"
  10. >
  11. <el-option value="MODE1" label="单题型连续"></el-option>
  12. <el-option value="MODE2" label="客观题整体连续"></el-option>
  13. <el-option value="MODE3" label="按大题独立"></el-option>
  14. <el-option value="MODE5" label="整卷连续"></el-option>
  15. </el-select>
  16. <el-select
  17. v-model="curPaperTemp"
  18. class="margin-right-10"
  19. placeholder="请选择"
  20. value-key="id"
  21. size="small"
  22. @change="paperTempChange"
  23. >
  24. <el-option
  25. v-for="item in paperTempList"
  26. :key="item.id"
  27. :label="item.name"
  28. :value="item"
  29. >
  30. </el-option>
  31. </el-select>
  32. <el-button type="primary" size="small" @click="toDownload"
  33. >下载试卷</el-button
  34. >
  35. </div>
  36. <paper-build-config
  37. ref="PaperBuildConfig"
  38. :config-sources="configSources"
  39. @confirm="buildConfigChange"
  40. ></paper-build-config>
  41. <paper-template-view
  42. ref="PaperTemplateView"
  43. class="preview-body"
  44. :pages="pages"
  45. :page-config="paperTempJson.pageConfig"
  46. ></paper-template-view>
  47. </div>
  48. </div>
  49. </template>
  50. <script>
  51. import PaperTemplateView from "../components/PaperTemplateView.vue";
  52. import PaperBuildConfig from "../components/PaperBuildConfig.vue";
  53. import { getModel as getRichTextModel } from "../elements/rich-text/model";
  54. import { getModel as getPageModel } from "../elements/page/model";
  55. import { getElementId, randomCode, deepCopy } from "../../card/plugins/utils";
  56. import { calcSum, maxNum } from "@/plugins/utils";
  57. import previewTemp from "../previewTemp";
  58. import { paperDetailInfoApi } from "../../paper/api";
  59. import { paperTemplateListApi, paperPdfDownloadApi } from "../api";
  60. import { downloadByApi } from "@/plugins/download";
  61. // import paperJson from "./data/paper.json";
  62. // import paperTempJson from "./data/paper-temp.json";
  63. const numberToUpperCase = function (val) {
  64. if (val < 1 || val > 26) return;
  65. return String.fromCharCode(64 + val);
  66. };
  67. const checkRichTextHasCont = function (data) {
  68. if (!data) return false;
  69. if (!data.sections || !data.sections.length) return false;
  70. if (!data.sections[0].blocks || !data.sections[0].blocks.length) return false;
  71. return true;
  72. };
  73. export default {
  74. name: "PaperTemplateBuild",
  75. components: { PaperTemplateView, PaperBuildConfig },
  76. data() {
  77. return {
  78. paperId: this.$route.params.paperId,
  79. viewType: this.$route.params.viewType,
  80. seqMode: "MODE1",
  81. renderStructList: [],
  82. pages: [],
  83. paperJson: {},
  84. paperTempJson: {
  85. pages: [],
  86. pageConfig: {},
  87. },
  88. maxColumnWidth: 200,
  89. maxColumnHeight: 200,
  90. paperTempList: [],
  91. curPaperTemp: {},
  92. downloading: false,
  93. fieldData: {},
  94. paperStructs: [],
  95. TEXT_INDENT_SIZE: 28,
  96. textIndent: 28,
  97. configModalForm: {
  98. showDetailNo: true,
  99. showDetailScoreTable: false,
  100. },
  101. configSources: [],
  102. prepareDownloadPdf: false,
  103. };
  104. },
  105. mounted() {
  106. if (this.viewType === "frame") {
  107. this.initFrame();
  108. return;
  109. }
  110. this.initData();
  111. },
  112. methods: {
  113. getTextIndexStyle() {
  114. return {
  115. textIndent: `-${this.textIndent}px`,
  116. paddingLeft: `${this.textIndent}px`,
  117. };
  118. },
  119. async initFrame() {
  120. try {
  121. const paperSet = window.parent.paperSet;
  122. if (!paperSet) {
  123. this.emitFrameResult(false, "数据缺失");
  124. return;
  125. }
  126. this.seqMode = paperSet.seqMode;
  127. this.curPaperTemp = paperSet.paperTemp;
  128. this.configModalForm = paperSet.configModalForm;
  129. await this.getPaperJson();
  130. let paperTempJson = this.curPaperTemp.content
  131. ? JSON.parse(this.curPaperTemp.content)
  132. : { pages: [], pageConfig: {} };
  133. this.paperTempJson = paperTempJson;
  134. this.pages = paperTempJson.pages;
  135. this.updaterFieldInfo();
  136. } catch (error) {
  137. this.emitFrameResult(false, "数据错误");
  138. }
  139. this.$nextTick(async () => {
  140. try {
  141. this.maxColumnWidth =
  142. document.getElementById("column-0-0").offsetWidth;
  143. this.maxColumnHeight =
  144. document.getElementById("column-0-0").offsetHeight - 10;
  145. this.parseRenderStructList();
  146. this.buildPrePages();
  147. } catch (error) {
  148. this.emitFrameResult(false, "构建错误");
  149. }
  150. const loadRes = await this.waitAllImgLoaded().catch(() => {});
  151. if (!loadRes) {
  152. this.emitFrameResult(false, "数据缓存错误");
  153. return;
  154. }
  155. this.$nextTick(() => {
  156. try {
  157. this.addDetailScoreTable();
  158. this.resetRenderStructSize();
  159. this.buildPageAutoPage();
  160. } catch (error) {
  161. this.emitFrameResult(false, "构建pdf错误");
  162. }
  163. this.$nextTick(() => {
  164. this.addDetailScoreTable();
  165. this.emitFrameResult(true, "", this.getPreviewTemp());
  166. });
  167. });
  168. });
  169. },
  170. emitFrameResult(success = true, errorMsg = "", htmlCont = "") {
  171. window.parent &&
  172. window.parent.submitPaperTemp &&
  173. window.parent.submitPaperTemp({
  174. success,
  175. errorMsg,
  176. htmlCont,
  177. templateId: this.curPaperTemp.id,
  178. });
  179. },
  180. async initData() {
  181. await this.getPaperJson();
  182. await this.getPaperTempList();
  183. if (!this.paperTempList.length) {
  184. this.$message.error("导出模板缺失!");
  185. return;
  186. }
  187. this.paperTempChange(this.paperTempList[0]);
  188. // test--->
  189. // this.paperJson = paperJson;
  190. // this.paperTempJson = paperTempJson;
  191. // this.pages = paperTempJson.pages;
  192. // this.$nextTick(() => {
  193. // this.buildData();
  194. // });
  195. },
  196. async getPaperJson() {
  197. const res = await paperDetailInfoApi({
  198. paperId: this.paperId,
  199. seqMode: this.seqMode,
  200. });
  201. this.paperJson = res.data;
  202. this.resetClozeSerialNo(this.paperJson);
  203. this.fieldData = {
  204. paperName: res.data.name,
  205. courseName: res.data.course.name,
  206. courseCode: res.data.course.code,
  207. totalScore: res.data.totalScore,
  208. rootOrgName: res.data.rootOrgName,
  209. };
  210. this.paperStructs = this.paperJson.paperDetails.map((detail) => {
  211. return {
  212. detailName: detail.name,
  213. questionCount: detail.unitCount,
  214. totalScore: detail.score,
  215. };
  216. });
  217. },
  218. resetClozeSerialNo(paperData) {
  219. const clozeQuestionTypes = ["CLOZE", "BANKED_CLOZE"];
  220. paperData.paperDetails.forEach((detail) => {
  221. detail.paperDetailUnits.forEach((question) => {
  222. if (!clozeQuestionTypes.includes(question.questionType)) return;
  223. question.question.quesBody.sections.forEach((section) => {
  224. section.blocks.forEach((block) => {
  225. if (block.type !== "cloze") return;
  226. block.value =
  227. question.question.subQuestions[block.value - 1].questionSeq;
  228. });
  229. });
  230. });
  231. });
  232. },
  233. async seqModeChange() {
  234. await this.getPaperJson();
  235. this.$nextTick(() => {
  236. this.buildData();
  237. });
  238. },
  239. async getPaperTempList() {
  240. const res = await paperTemplateListApi("PAPER_EXPORT");
  241. this.paperTempList = res.data;
  242. },
  243. getConfigSources() {
  244. const { pages } = this.paperTempJson;
  245. let sources = [],
  246. fieldAble = {};
  247. pages.forEach((page) => {
  248. page.columns.forEach((column) => {
  249. column.elements.forEach((element) => {
  250. if (element.type !== "PAPER_PROPS") return;
  251. if (!sources.length) {
  252. sources = deepCopy(element.props);
  253. }
  254. element.props.forEach((prop) => {
  255. fieldAble[prop.field] = fieldAble[prop.field] || prop.enable;
  256. });
  257. });
  258. });
  259. });
  260. sources.forEach((item) => {
  261. item.enable = fieldAble[item.field];
  262. });
  263. this.configSources = sources;
  264. },
  265. buildConfigChange(val) {
  266. this.configModalForm = val;
  267. this.updaterFieldInfo();
  268. this.$nextTick(() => {
  269. this.buildData();
  270. if (this.prepareDownloadPdf) {
  271. this.$nextTick(async () => {
  272. await this.downloadPaperPdf().catch(() => {});
  273. this.prepareDownloadPdf = false;
  274. });
  275. }
  276. });
  277. },
  278. paperTempChange(paperTemp) {
  279. // console.log(paperTemp);
  280. this.curPaperTemp = paperTemp;
  281. let paperTempJson = paperTemp.content
  282. ? JSON.parse(paperTemp.content)
  283. : { pages: [], pageConfig: {} };
  284. this.paperTempJson = paperTempJson;
  285. this.pages = paperTempJson.pages;
  286. this.getConfigSources();
  287. this.updaterFieldInfo();
  288. this.$nextTick(() => {
  289. this.buildData();
  290. });
  291. },
  292. updaterFieldInfo() {
  293. const VALID_ELEMENTS_FOR_EXTERNAL = ["FIELD_TEXT"];
  294. this.paperTempJson.pages.forEach((page) => {
  295. page.columns.forEach((column) => {
  296. column.elements.forEach((elem) => {
  297. if (elem.type === "PAPER_STRUCT") {
  298. elem.structs = this.paperStructs;
  299. } else if (elem.type === "SCORE_TABLE") {
  300. elem.detailCount = this.paperStructs.length;
  301. } else if (elem.type === "PAPER_PROPS") {
  302. elem.props.forEach((prop) => {
  303. prop.value = this.configModalForm[prop.field];
  304. });
  305. }
  306. if (!elem.elements || !elem.elements.length) return;
  307. elem.elements.forEach((element) => {
  308. if (!VALID_ELEMENTS_FOR_EXTERNAL.includes(element.type)) return;
  309. if (element.type === "FIELD_TEXT" && element.field) {
  310. element.content = this.fieldData[element.field];
  311. }
  312. });
  313. });
  314. });
  315. });
  316. },
  317. async buildData() {
  318. this.maxColumnWidth = document.getElementById("column-0-0").offsetWidth;
  319. this.maxColumnHeight =
  320. document.getElementById("column-0-0").offsetHeight - 10;
  321. this.parseRenderStructList();
  322. this.buildPrePages();
  323. const loadRes = await this.waitAllImgLoaded().catch(() => {});
  324. if (!loadRes) {
  325. this.$message.error("图片加载有误!");
  326. return;
  327. }
  328. this.$nextTick(() => {
  329. this.buildReleasePages();
  330. });
  331. },
  332. parseRenderStructList() {
  333. let renderStructList = [];
  334. this.paperJson.paperDetails.forEach((detail) => {
  335. renderStructList.push(this.parseDetailTitle(detail));
  336. if (checkRichTextHasCont(detail.description)) {
  337. const descData = this.parseTitleOption(detail.description, "");
  338. renderStructList.push(...descData);
  339. }
  340. detail.paperDetailUnits.forEach((question) => {
  341. let questionInfo = question.question;
  342. if (questionInfo.subQuestions && questionInfo.subQuestions.length) {
  343. const bodys = this.parseTitleOption(questionInfo.quesBody, "");
  344. renderStructList.push(...bodys);
  345. const isMatches = this.checkIsMatches(questionInfo.questionType);
  346. if (
  347. isMatches &&
  348. questionInfo.quesOptions &&
  349. questionInfo.quesOptions.length
  350. ) {
  351. questionInfo.quesOptions.forEach((op) => {
  352. const obodys = this.parseTitleOption(
  353. op.optionBody,
  354. `${numberToUpperCase(op.number)}、`,
  355. true,
  356. {
  357. contType: "option",
  358. textStyles: { paddingLeft: `${this.textIndent}px` },
  359. }
  360. );
  361. renderStructList.push(...obodys);
  362. });
  363. }
  364. // 选词填空不展示小题
  365. if (questionInfo.questionType === "BANKED_CLOZE") {
  366. renderStructList.push(this.parseLineGap());
  367. return;
  368. }
  369. questionInfo.subQuestions.forEach((sq, sqindex) => {
  370. sq.subNumber = sqindex + 1;
  371. if (isMatches) sq.quesOptions = []; // 选词填空、段落匹配小题中不展示选项
  372. const contents = this.parseSimpleQuestion(sq, false);
  373. renderStructList.push(...contents);
  374. renderStructList.push(this.parseLineGap());
  375. });
  376. } else {
  377. questionInfo.number = question.number;
  378. const datas = this.parseSimpleQuestion(questionInfo, true);
  379. renderStructList.push(...datas);
  380. renderStructList.push(this.parseLineGap());
  381. }
  382. });
  383. });
  384. // 去掉最后一题的间隔行
  385. // console.log(renderStructList);
  386. this.renderStructList = renderStructList.slice(0, -1);
  387. },
  388. getRichStruct(blocks) {
  389. return {
  390. sections: [
  391. {
  392. blocks: [...blocks],
  393. },
  394. ],
  395. };
  396. },
  397. transformRichJson(richJson) {
  398. if (!richJson || !richJson.sections) return [];
  399. let contents = [];
  400. let curBlock = [];
  401. const checkNeedSplitSection = (block) => {
  402. if (block.type !== "image") return false;
  403. if (block.param) {
  404. if (block.param.width)
  405. return block.param.width > this.maxColumnWidth / 2;
  406. if (block.param.height) return block.param.height > 150;
  407. }
  408. return true;
  409. };
  410. richJson.sections.forEach((section) => {
  411. section.blocks.forEach((block) => {
  412. if (checkNeedSplitSection(block) && curBlock.length) {
  413. contents.push(this.getRichStruct(curBlock));
  414. curBlock = [];
  415. }
  416. curBlock.push(block);
  417. });
  418. if (curBlock.length) {
  419. contents.push(this.getRichStruct(curBlock));
  420. curBlock = [];
  421. }
  422. });
  423. return contents;
  424. },
  425. changeRichTextCloze(richText) {
  426. return {
  427. sections: richText.sections.map((section) => {
  428. return {
  429. blocks: section.blocks.map((item) => {
  430. if (item.type === "cloze") {
  431. return {
  432. type: "text",
  433. value: "_____",
  434. };
  435. } else {
  436. return { ...item };
  437. }
  438. }),
  439. };
  440. }),
  441. };
  442. },
  443. parseSimpleQuestion(question) {
  444. let contents = [];
  445. let quesBody = question.quesBody;
  446. if (question.questionType === "FILL_BLANK_QUESTION") {
  447. quesBody = this.changeRichTextCloze(quesBody);
  448. }
  449. const tbodys = this.parseTitleOption(
  450. quesBody,
  451. `${question.questionSeq}、`,
  452. true,
  453. {
  454. textStyles: { paddingLeft: `${this.textIndent}px` },
  455. }
  456. );
  457. contents.push(...tbodys);
  458. const hasNobody = !tbodys.length;
  459. if (question.quesOptions && question.quesOptions.length) {
  460. question.quesOptions.forEach((op, oIndex) => {
  461. let noVal = `${numberToUpperCase(op.number)}、`;
  462. if (!hasNobody) {
  463. const obodys = this.parseTitleOption(op.optionBody, noVal, false, {
  464. contType: "option",
  465. textStyles: { paddingLeft: `${this.textIndent}px` },
  466. });
  467. contents.push(...obodys);
  468. return;
  469. }
  470. if (!oIndex) {
  471. // 针对如完形填空的小题做的特殊处理
  472. noVal = `${question.questionSeq}、${noVal}`;
  473. }
  474. this.textIndent = 2 * this.TEXT_INDENT_SIZE;
  475. const obodys = this.parseTitleOption(op.optionBody, noVal, true, {
  476. contType: "option",
  477. textStyles: { paddingLeft: `${this.textIndent}px` },
  478. });
  479. contents.push(...obodys);
  480. this.textIndent = this.TEXT_INDENT_SIZE;
  481. });
  482. }
  483. return contents;
  484. },
  485. parseDetailTitle(data) {
  486. let blocks = [
  487. {
  488. type: "text",
  489. value: `${data.name}(共${data.unitCount}小题,满分${data.score}分)`,
  490. },
  491. ];
  492. if (this.configModalForm.showDetailNo) {
  493. blocks.unshift({
  494. type: "text",
  495. value: `${data.cnNum}、`,
  496. });
  497. }
  498. let content = this.getRichStruct(blocks);
  499. return getRichTextModel({
  500. styles: { width: "100%", fontWeight: 900 },
  501. content,
  502. classNames: this.configModalForm.showDetailScoreTable
  503. ? "is-detail-title"
  504. : "",
  505. });
  506. },
  507. parseTitleOption(
  508. richJson,
  509. noVal,
  510. needIndent = false,
  511. modelData = { textStyles: null, styles: null, contType: "" }
  512. ) {
  513. if (!richJson) return [];
  514. const { textStyles, styles, contType } = modelData;
  515. const bodys = this.transformRichJson(richJson);
  516. return bodys.map((body, index) => {
  517. let presetData = {
  518. content: body,
  519. };
  520. if (contType) presetData.contType = contType;
  521. if (textStyles) presetData.textStyles = textStyles;
  522. if (styles) {
  523. presetData.styles = styles;
  524. } else {
  525. presetData.styles = {
  526. width: contType === "option" ? "auto" : "100%",
  527. };
  528. }
  529. if (index === 0 && noVal) {
  530. let cont = {
  531. type: "text",
  532. value: noVal,
  533. };
  534. if (needIndent) {
  535. cont.param = {
  536. width: this.textIndent,
  537. };
  538. presetData.textStyles = {
  539. ...(presetData.textStyles || {}),
  540. ...this.getTextIndexStyle(),
  541. };
  542. }
  543. body.sections[0].blocks.unshift(cont);
  544. }
  545. return getRichTextModel(presetData);
  546. });
  547. },
  548. parseLineGap() {
  549. return getRichTextModel({
  550. contType: "gap",
  551. styles: { width: "100%", height: "10px" },
  552. content: this.getRichStruct([{ type: "text", value: "" }]),
  553. });
  554. },
  555. checkIsMatches(structType) {
  556. const matchesTypes = ["BANKED_CLOZE", "PARAGRAPH_MATCHING"];
  557. return matchesTypes.includes(structType);
  558. },
  559. buildPrePages() {
  560. let pages = deepCopy(this.paperTempJson.pages);
  561. const firstPageNo = pages.findIndex((p) => p.pageType !== "cover");
  562. pages[firstPageNo].columns[0].texts = [];
  563. pages[firstPageNo].columns[0].texts.push(...this.renderStructList);
  564. this.pages = pages;
  565. },
  566. buildReleasePages() {
  567. this.addDetailScoreTable();
  568. this.resetRenderStructSize();
  569. // console.log(this.renderStructList);
  570. this.buildPageAutoPage();
  571. this.$nextTick(() => {
  572. this.addDetailScoreTable();
  573. });
  574. },
  575. addDetailScoreTable() {
  576. if (!this.configModalForm.showDetailScoreTable) return;
  577. const scoreTableHtml = `<table class="detail-score-table"><tr><th>得分</th><th>评分人</th></tr><tr><td></td><td></td></tr></table>`;
  578. const dom = document.createElement("div");
  579. dom.innerHTML = scoreTableHtml;
  580. document.querySelectorAll(".is-detail-title").forEach((node) => {
  581. const hasScoreTable =
  582. node.firstChild.className.includes("detail-score-table");
  583. if (hasScoreTable) return;
  584. node.insertBefore(dom.firstChild.cloneNode(true), node.firstChild);
  585. });
  586. },
  587. resetRenderStructSize() {
  588. let curOptions = [];
  589. this.renderStructList.forEach((elem, eindex) => {
  590. const elemDom = document.getElementById(`rich-text-${elem.id}`);
  591. elem.w = elemDom.offsetWidth;
  592. elem.h = elemDom.offsetHeight;
  593. if (elem.contType !== "option") return;
  594. curOptions.push(elem);
  595. // 全选选项逻辑
  596. const nextElem = this.renderStructList[eindex + 1];
  597. if (nextElem && nextElem.contType === "option") return;
  598. curOptions.forEach((optionElem) => {
  599. optionElem._percent = this.getSizePercent(
  600. optionElem.w,
  601. this.maxColumnWidth
  602. );
  603. });
  604. const optionCount = curOptions.length;
  605. // 奇数选项,全部一行
  606. if (optionCount % 2 > 0) {
  607. curOptions.forEach((optionElem) => {
  608. optionElem._percent = 1;
  609. optionElem.styles.width = "100%";
  610. });
  611. curOptions = [];
  612. return;
  613. }
  614. const percents = curOptions.map((item) => item._percent);
  615. const maxPercent = maxNum(percents);
  616. let aveOptionPercent = 1;
  617. if (optionCount % 4 === 0) {
  618. aveOptionPercent = this.calcAveOptionPercent(maxPercent);
  619. } else {
  620. aveOptionPercent = maxPercent > 0.5 ? 1 : 0.5;
  621. }
  622. curOptions.forEach((optionElem) => {
  623. optionElem._percent = aveOptionPercent;
  624. optionElem.styles.width = aveOptionPercent * 100 + "%";
  625. });
  626. curOptions = [];
  627. });
  628. this.renderStructList.forEach((elem) => {
  629. if (elem.styles.width === "100%") {
  630. this.$set(elem.styles, "display", "block");
  631. }
  632. });
  633. },
  634. buildPageAutoPage() {
  635. let pages = [];
  636. let curPage = null,
  637. curElem = null;
  638. let curColumn = null,
  639. curColumnNo = 0,
  640. curColumnHeight = 0;
  641. let curLinePercent = 0;
  642. let groups = [],
  643. curGroup = [];
  644. // 分组自动分页 选项分组
  645. const getNextElem = () => {
  646. return this.renderStructList.shift();
  647. };
  648. curElem = getNextElem();
  649. while (curElem) {
  650. if (
  651. curElem.contType !== "option" ||
  652. (curElem.contType === "option" && curElem._percent === 1)
  653. ) {
  654. if (curGroup.length) {
  655. groups.push(curGroup);
  656. curGroup = [];
  657. curLinePercent = 0;
  658. }
  659. groups.push([curElem]);
  660. curElem = getNextElem();
  661. } else {
  662. if (curLinePercent + curElem._percent > 1) {
  663. groups.push(curGroup);
  664. curGroup = [];
  665. curLinePercent = 0;
  666. } else {
  667. curGroup.push(curElem);
  668. curLinePercent += curElem._percent;
  669. curElem = getNextElem();
  670. }
  671. }
  672. }
  673. if (curGroup.length) {
  674. groups.push(curGroup);
  675. curGroup = [];
  676. }
  677. // console.log(groups);
  678. const getNextGroup = () => {
  679. return groups.shift();
  680. };
  681. curGroup = getNextGroup();
  682. while (curGroup) {
  683. if (!curPage) {
  684. curPage = this.getNewPageModel(pages.length);
  685. }
  686. if (!curColumn) {
  687. curColumn = curPage.columns[curColumnNo++];
  688. curColumnHeight = this.calcInitColumnHeight(curColumn);
  689. }
  690. let curGroupHeigth =
  691. curGroup.length === 1
  692. ? curGroup[0].h
  693. : Math.max.apply(
  694. null,
  695. curGroup.map((item) => item.h)
  696. );
  697. if (curGroupHeigth + curColumnHeight > this.maxColumnHeight) {
  698. // 当前栏第一个元素就超过最大高度时,直接放当前栏
  699. if (!curColumn.texts.length) {
  700. curColumn.texts.push(...curGroup);
  701. curGroup = getNextGroup();
  702. }
  703. // 当前栏满了
  704. if (curColumnNo >= curPage.columnNumber) {
  705. // 当前页满了
  706. pages.push(curPage);
  707. curPage = null;
  708. curColumnNo = 0;
  709. }
  710. curColumn = null;
  711. curColumnHeight = 0;
  712. } else {
  713. // 当前栏未满
  714. curColumnHeight += curGroupHeigth;
  715. curColumn.texts.push(...curGroup);
  716. curGroup = getNextGroup();
  717. }
  718. }
  719. if (curPage) {
  720. pages.push(curPage);
  721. curPage = null;
  722. }
  723. // 正文部分保证偶数页
  724. if (pages.length % 2) {
  725. pages.push(this.getNewPageModel(pages.length));
  726. }
  727. if (this.paperTempJson.pageConfig.showCover) {
  728. // 封面自动插入反面空白页
  729. let coverPages = deepCopy(
  730. this.paperTempJson.pages.filter((p) => p.pageType === "cover")
  731. );
  732. let coverBackPage = deepCopy(coverPages[0]);
  733. coverBackPage.columns.forEach((column) => {
  734. column.elements = [];
  735. });
  736. let nCoverPages = [];
  737. coverPages.forEach((cpage) => {
  738. nCoverPages.push(cpage);
  739. nCoverPages.push(coverBackPage);
  740. });
  741. pages = [...nCoverPages, ...pages];
  742. }
  743. this.pages = pages;
  744. },
  745. getNewPageModel(pageNo) {
  746. let contentPages = this.paperTempJson.pages.slice(-2);
  747. let pNo = pageNo % 2;
  748. const pageTemp = contentPages[pNo];
  749. let newPage = getPageModel({
  750. ...this.paperTempJson.pageConfig,
  751. pageType: pageTemp.pageType,
  752. });
  753. newPage.sides = pageTemp.sides.map((elem) => {
  754. let nelem = deepCopy(elem);
  755. nelem.id = getElementId();
  756. nelem.key = randomCode();
  757. // if (pNo === 1 && nelem.type === "GUTTER") {
  758. // nelem.direction = "right";
  759. // }
  760. return nelem;
  761. });
  762. newPage.columns.forEach((column) => {
  763. column.texts = [];
  764. });
  765. if (pageNo > 1) return newPage;
  766. newPage.columns.forEach((column, cindex) => {
  767. column.elements = pageTemp.columns[cindex].elements.map((elem) => {
  768. let nelem = deepCopy(elem);
  769. nelem.id = getElementId();
  770. nelem.key = randomCode();
  771. nelem.h = this.getElementHeight(`preview-${elem.id}`);
  772. if (nelem.elements && nelem.elements.length) {
  773. nelem.elements.forEach((celem) => {
  774. celem.id = getElementId();
  775. celem.key = randomCode();
  776. });
  777. }
  778. return nelem;
  779. });
  780. column.texts = [];
  781. });
  782. return newPage;
  783. },
  784. getElementHeight(elementId) {
  785. const dom = document.getElementById(elementId);
  786. return dom ? dom.offsetHeight : 0;
  787. },
  788. calcAveOptionPercent(maxPercent) {
  789. if (maxPercent > 0.5) return 1;
  790. if (maxPercent > 0.25) return 0.5;
  791. return 0.25;
  792. },
  793. calcInitColumnHeight(column) {
  794. return calcSum(column.elements.map((item) => item.h));
  795. },
  796. getSizePercent(size, fullSize) {
  797. const rate = size / fullSize;
  798. if (rate <= 0.25) return 0.25;
  799. if (rate <= 0.5) return 0.5;
  800. return 1;
  801. },
  802. // img
  803. loadImg(url) {
  804. return new Promise((resolve, reject) => {
  805. const img = new Image();
  806. img.onload = function () {
  807. resolve(true);
  808. };
  809. img.onerror = function () {
  810. reject();
  811. };
  812. img.src = url;
  813. });
  814. },
  815. getRichJsonImgUrls(richJson) {
  816. let urls = [];
  817. if (!richJson) return urls;
  818. richJson.sections.forEach((section) => {
  819. section.blocks.forEach((elem) => {
  820. if (elem.type === "image" && elem.value.startsWith("http")) {
  821. urls.push(elem.value);
  822. }
  823. });
  824. });
  825. return urls;
  826. },
  827. async waitAllImgLoaded() {
  828. let imgUrls = [];
  829. this.renderStructList.forEach((item) => {
  830. if (item.contType === "gap") return;
  831. imgUrls.push(...this.getRichJsonImgUrls(item.content));
  832. });
  833. // console.log(imgUrls);
  834. if (!imgUrls.length) return Promise.resolve(true);
  835. const imgLoads = imgUrls.map((item) => this.loadImg(item));
  836. const imgLoadResult = await Promise.all(imgLoads).catch(() => {});
  837. if (imgLoadResult && imgLoadResult.length) {
  838. return Promise.resolve(true);
  839. } else {
  840. return Promise.reject();
  841. }
  842. },
  843. getPreviewTemp() {
  844. const elementDoms =
  845. this.$refs.PaperTemplateView.$el.querySelectorAll(".elem-rich-text");
  846. elementDoms.forEach((eDom) => {
  847. const width = eDom.offsetWidth;
  848. if (eDom.firstChild && eDom.firstChild.nodeName === "DIV") {
  849. eDom.firstChild.style.width = width + "px";
  850. if (
  851. eDom.firstChild.firstChild &&
  852. eDom.firstChild.firstChild.nodeName === "DIV"
  853. )
  854. eDom.firstChild.firstChild.style.width = "100%";
  855. }
  856. });
  857. return previewTemp(this.$refs.PaperTemplateView.$el.outerHTML);
  858. },
  859. async toDownload() {
  860. const valid = await this.$refs.PaperBuildConfig.checkData().catch(
  861. () => {}
  862. );
  863. if (!valid) return;
  864. const configData = this.$refs.PaperBuildConfig.getData();
  865. if (JSON.stringify(configData) === JSON.stringify(this.configModalForm)) {
  866. this.downloadPaperPdf();
  867. } else {
  868. this.prepareDownloadPdf = true;
  869. this.buildConfigChange(configData);
  870. }
  871. },
  872. async downloadPaperPdf() {
  873. const htmlCont = this.getPreviewTemp();
  874. if (this.downloading) return;
  875. this.downloading = true;
  876. const res = await downloadByApi(() => {
  877. return paperPdfDownloadApi({
  878. content: htmlCont,
  879. templateId: this.curPaperTemp.id,
  880. paperId: this.paperId,
  881. });
  882. }).catch((e) => {
  883. this.$message.error(e || "下载失败,请重新尝试!");
  884. });
  885. this.downloading = false;
  886. if (!res) return;
  887. this.$message.success("下载成功!");
  888. },
  889. },
  890. };
  891. </script>
  892. <style>
  893. .paper-template-build {
  894. text-align: center;
  895. }
  896. .paper-template-build-body {
  897. display: inline-block;
  898. text-align: initial;
  899. }
  900. .paper-template-build .page-box {
  901. margin-top: 10px;
  902. margin-bottom: 10px;
  903. }
  904. .paper-template-build .paper-build-config {
  905. padding: 10px 15px 2px;
  906. margin-top: 5px;
  907. background: #fff;
  908. border-radius: 10px;
  909. }
  910. .paper-build-config .el-form-item {
  911. margin-bottom: 16px;
  912. margin-right: 30px;
  913. }
  914. </style>