PaperTemplateBuild.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  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. @change="seqModeChange"
  9. >
  10. <el-option value="MODE1" label="单题型连续"></el-option>
  11. <el-option value="MODE2" label="客观题整体连续"></el-option>
  12. <el-option value="MODE3" label="按大题独立"></el-option>
  13. <el-option value="MODE5" label="整卷连续"></el-option>
  14. </el-select>
  15. <el-select
  16. v-model="curPaperTemp"
  17. class="margin-right-10"
  18. placeholder="请选择"
  19. value-key="id"
  20. @change="paperTempChange"
  21. >
  22. <el-option
  23. v-for="item in paperTempList"
  24. :key="item.id"
  25. :label="item.name"
  26. :value="item"
  27. >
  28. </el-option>
  29. </el-select>
  30. <el-button type="primary" @click="toDownload">下载试卷</el-button>
  31. </div>
  32. <paper-template-view
  33. ref="PaperTemplateView"
  34. class="preview-body"
  35. :pages="pages"
  36. :page-config="paperTempJson.pageConfig"
  37. ></paper-template-view>
  38. </div>
  39. </div>
  40. </template>
  41. <script>
  42. import PaperTemplateView from "../components/PaperTemplateView.vue";
  43. import { getModel as getRichTextModel } from "../elements/rich-text/model";
  44. import { getModel as getPageModel } from "../elements/page/model";
  45. import { getElementId, randomCode, deepCopy } from "../../card/plugins/utils";
  46. import { calcSum, maxNum } from "@/plugins/utils";
  47. import previewTemp from "../previewTemp";
  48. import { paperDetailInfoApi } from "../../paper/api";
  49. import { paperTemplateListApi, paperPdfDownloadApi } from "../api";
  50. import { downloadByApi } from "@/plugins/download";
  51. // import paperJson from "./data/paper.json";
  52. // import paperTempJson from "./data/paper-temp.json";
  53. const numberToUpperCase = function (val) {
  54. if (val < 1 || val > 26) return;
  55. return String.fromCharCode(64 + val);
  56. };
  57. const checkRichTextHasCont = function (data) {
  58. if (!data) return false;
  59. if (!data.sections || !data.sections.length) return false;
  60. if (!data.sections[0].blocks || !data.sections[0].blocks.length) return false;
  61. return true;
  62. };
  63. export default {
  64. name: "PaperTemplateBuild",
  65. components: { PaperTemplateView },
  66. data() {
  67. return {
  68. paperId: this.$route.params.paperId,
  69. viewType: this.$route.params.viewType,
  70. seqMode: "MODE1",
  71. renderStructList: [],
  72. pages: [],
  73. paperJson: {},
  74. paperTempJson: {
  75. pages: [],
  76. pageConfig: {},
  77. },
  78. maxColumnWidth: 200,
  79. maxColumnHeight: 200,
  80. paperTempList: [],
  81. curPaperTemp: {},
  82. downloading: false,
  83. fieldData: {},
  84. paperStructs: [],
  85. };
  86. },
  87. mounted() {
  88. if (this.viewType === "frame") {
  89. this.initFrame();
  90. return;
  91. }
  92. this.initData();
  93. },
  94. methods: {
  95. async initFrame() {
  96. try {
  97. const paperSet = window.parent.paperSet;
  98. if (!paperSet) {
  99. this.emitFrameResult(false, "数据缺失");
  100. return;
  101. }
  102. this.seqMode = paperSet.seqMode;
  103. this.curPaperTemp = paperSet.paperTemp;
  104. await this.getPaperJson();
  105. let paperTempJson = this.curPaperTemp.content
  106. ? JSON.parse(this.curPaperTemp.content)
  107. : { pages: [], pageConfig: {} };
  108. this.paperTempJson = paperTempJson;
  109. this.pages = paperTempJson.pages;
  110. this.updaterFieldInfo();
  111. } catch (error) {
  112. this.emitFrameResult(false, "数据错误");
  113. }
  114. this.$nextTick(async () => {
  115. try {
  116. this.maxColumnWidth =
  117. document.getElementById("column-0-0").offsetWidth;
  118. this.maxColumnHeight =
  119. document.getElementById("column-0-0").offsetHeight;
  120. this.parseRenderStructList();
  121. this.buildPrePages();
  122. } catch (error) {
  123. this.emitFrameResult(false, "构建错误");
  124. }
  125. const loadRes = await this.waitAllImgLoaded().catch(() => {});
  126. if (!loadRes) {
  127. this.emitFrameResult(false, "数据缓存错误");
  128. return;
  129. }
  130. this.$nextTick(() => {
  131. try {
  132. this.buildReleasePages();
  133. } catch (error) {
  134. this.emitFrameResult(false, "构建pdf错误");
  135. }
  136. this.$nextTick(() => {
  137. this.emitFrameResult(true, "", this.getPreviewTemp());
  138. });
  139. });
  140. });
  141. },
  142. emitFrameResult(success = true, errorMsg = "", htmlCont = "") {
  143. window.parent &&
  144. window.parent.submitPaperTemp &&
  145. window.parent.submitPaperTemp({
  146. success,
  147. errorMsg,
  148. htmlCont,
  149. templateId: this.curPaperTemp.id,
  150. });
  151. },
  152. async initData() {
  153. await this.getPaperJson();
  154. await this.getPaperTempList();
  155. if (!this.paperTempList.length) {
  156. this.$message.error("导出模板缺失!");
  157. return;
  158. }
  159. this.paperTempChange(this.paperTempList[0]);
  160. // test--->
  161. // this.paperJson = paperJson;
  162. // this.paperTempJson = paperTempJson;
  163. // this.pages = paperTempJson.pages;
  164. // this.$nextTick(() => {
  165. // this.buildData();
  166. // });
  167. },
  168. async getPaperJson() {
  169. const res = await paperDetailInfoApi({
  170. paperId: this.paperId,
  171. seqMode: this.seqMode,
  172. });
  173. this.paperJson = res.data;
  174. this.fieldData = {
  175. paperName: res.data.name,
  176. courseName: `${res.data.course.name}(${res.data.course.code})`,
  177. totalScore: res.data.totalScore,
  178. rootOrgName: res.data.rootOrgName,
  179. };
  180. this.paperStructs = this.paperJson.paperDetails.map((detail) => {
  181. return {
  182. detailName: detail.name,
  183. questionCount: detail.unitCount,
  184. totalScore: detail.score,
  185. };
  186. });
  187. },
  188. async seqModeChange() {
  189. await this.getPaperJson();
  190. this.$nextTick(() => {
  191. this.buildData();
  192. });
  193. },
  194. async getPaperTempList() {
  195. const res = await paperTemplateListApi("PAPER_EXPORT");
  196. this.paperTempList = res.data;
  197. },
  198. paperTempChange(paperTemp) {
  199. // console.log(paperTemp);
  200. this.curPaperTemp = paperTemp;
  201. let paperTempJson = paperTemp.content
  202. ? JSON.parse(paperTemp.content)
  203. : { pages: [], pageConfig: {} };
  204. this.paperTempJson = paperTempJson;
  205. this.pages = paperTempJson.pages;
  206. this.updaterFieldInfo();
  207. this.$nextTick(() => {
  208. this.buildData();
  209. });
  210. },
  211. updaterFieldInfo() {
  212. const VALID_ELEMENTS_FOR_EXTERNAL = ["FIELD_TEXT"];
  213. this.paperTempJson.pages.forEach((page) => {
  214. page.columns.forEach((column) => {
  215. column.elements.forEach((elem) => {
  216. if (elem.type === "PAPER_STRUCT") {
  217. elem.structs = this.paperStructs;
  218. }
  219. if (!elem.elements || !elem.elements.length) return;
  220. elem.elements.forEach((element) => {
  221. if (!VALID_ELEMENTS_FOR_EXTERNAL.includes(element.type)) return;
  222. if (element.type === "FIELD_TEXT" && element.field) {
  223. element.content = this.fieldData[element.field];
  224. }
  225. });
  226. });
  227. });
  228. });
  229. },
  230. async buildData() {
  231. this.maxColumnWidth = document.getElementById("column-0-0").offsetWidth;
  232. this.maxColumnHeight = document.getElementById("column-0-0").offsetHeight;
  233. this.parseRenderStructList();
  234. this.buildPrePages();
  235. const loadRes = await this.waitAllImgLoaded().catch(() => {});
  236. if (!loadRes) {
  237. this.$message.error("图片加载有误!");
  238. return;
  239. }
  240. this.$nextTick(() => {
  241. this.buildReleasePages();
  242. });
  243. },
  244. parseRenderStructList() {
  245. let renderStructList = [];
  246. this.paperJson.paperDetails.forEach((detail) => {
  247. renderStructList.push(this.parseDetailTitle(detail));
  248. if (checkRichTextHasCont(detail.description)) {
  249. const descData = this.parseTitleOption(detail.description, "");
  250. renderStructList.push(...descData);
  251. }
  252. detail.paperDetailUnits.forEach((question) => {
  253. let questionInfo = question.question;
  254. if (questionInfo.subQuestions && questionInfo.subQuestions.length) {
  255. const bodys = this.parseTitleOption(questionInfo.quesBody, "");
  256. renderStructList.push(...bodys);
  257. const isMatches = this.checkIsMatches(questionInfo.questionType);
  258. if (
  259. isMatches &&
  260. questionInfo.quesOptions &&
  261. questionInfo.quesOptions.length
  262. ) {
  263. questionInfo.quesOptions.forEach((op) => {
  264. const obodys = this.parseTitleOption(
  265. op.optionBody,
  266. `${numberToUpperCase(op.number)}、`,
  267. "option"
  268. );
  269. renderStructList.push(...obodys);
  270. });
  271. }
  272. // 选词填空不展示小题
  273. if (questionInfo.questionType === "BANKED_CLOZE") {
  274. renderStructList.push(this.parseLineGap());
  275. return;
  276. }
  277. questionInfo.subQuestions.forEach((sq, sqindex) => {
  278. sq.subNumber = sqindex + 1;
  279. if (isMatches) sq.quesOptions = []; // 选词填空、段落匹配小题中不展示选项
  280. const contents = this.parseSimpleQuestion(sq, false);
  281. renderStructList.push(...contents);
  282. renderStructList.push(this.parseLineGap());
  283. });
  284. } else {
  285. questionInfo.number = question.number;
  286. const datas = this.parseSimpleQuestion(questionInfo, true);
  287. renderStructList.push(...datas);
  288. renderStructList.push(this.parseLineGap());
  289. }
  290. });
  291. });
  292. // 去掉最后一题的间隔行
  293. // console.log(renderStructList);
  294. this.renderStructList = renderStructList.slice(0, -1);
  295. },
  296. getRichStruct(blocks) {
  297. return {
  298. sections: [
  299. {
  300. blocks: [...blocks],
  301. },
  302. ],
  303. };
  304. },
  305. transformRichJson(richJson) {
  306. if (!richJson || !richJson.sections) return [];
  307. let contents = [];
  308. let curBlock = [];
  309. const checkNeedSplitSection = (block) => {
  310. if (block.type !== "image") return false;
  311. if (block.param) {
  312. if (block.param.width)
  313. return block.param.width > this.maxColumnWidth / 2;
  314. if (block.param.height) return block.param.height > 150;
  315. }
  316. return true;
  317. };
  318. richJson.sections.forEach((section) => {
  319. section.blocks.forEach((block) => {
  320. if (checkNeedSplitSection(block) && curBlock.length) {
  321. contents.push(this.getRichStruct(curBlock));
  322. curBlock = [];
  323. }
  324. curBlock.push(block);
  325. });
  326. if (curBlock.length) {
  327. contents.push(this.getRichStruct(curBlock));
  328. curBlock = [];
  329. }
  330. });
  331. return contents;
  332. },
  333. parseSimpleQuestion(question, isCommon) {
  334. let contents = [];
  335. const tbodys = this.parseTitleOption(
  336. question.quesBody,
  337. isCommon ? `${question.number}、` : `${question.subNumber}、`
  338. );
  339. contents.push(...tbodys);
  340. if (question.quesOptions && question.quesOptions.length) {
  341. question.quesOptions.forEach((op) => {
  342. const obodys = this.parseTitleOption(
  343. op.optionBody,
  344. `${numberToUpperCase(op.number)}、`,
  345. "option"
  346. );
  347. contents.push(...obodys);
  348. });
  349. }
  350. return contents;
  351. },
  352. parseDetailTitle(data) {
  353. let content = this.getRichStruct([
  354. {
  355. type: "text",
  356. value: `${data.cnNum}、${data.name}(共${data.unitCount}小题,满分${data.score}分)`,
  357. },
  358. ]);
  359. return getRichTextModel({
  360. styles: { width: "100%", fontWeight: 600 },
  361. content,
  362. });
  363. },
  364. parseTitleOption(richJson, noVal, contType = "content") {
  365. if (!richJson) return [];
  366. const bodys = this.transformRichJson(richJson);
  367. return bodys.map((body, index) => {
  368. if (index === 0 && noVal) {
  369. body.sections[0].blocks.unshift({
  370. type: "text",
  371. value: noVal,
  372. });
  373. }
  374. return getRichTextModel({
  375. contType,
  376. styles: { width: contType !== "option" ? "100%" : "auto" },
  377. content: body,
  378. });
  379. });
  380. },
  381. parseLineGap() {
  382. return getRichTextModel({
  383. contType: "gap",
  384. styles: { width: "100%", height: "20px" },
  385. content: this.getRichStruct([{ type: "text", value: "" }]),
  386. });
  387. },
  388. checkIsMatches(structType) {
  389. const matchesTypes = ["BANKED_CLOZE", "PARAGRAPH_MATCHING"];
  390. return matchesTypes.includes(structType);
  391. },
  392. buildPrePages() {
  393. let pages = deepCopy(this.paperTempJson.pages);
  394. const firstPageNo = pages.findIndex((p) => p.pageType !== "cover");
  395. pages[firstPageNo].columns[0].texts = [];
  396. pages[firstPageNo].columns[0].texts.push(...this.renderStructList);
  397. this.pages = pages;
  398. },
  399. buildReleasePages() {
  400. this.resetRenderStructSize();
  401. this.buildPageAutoPage();
  402. },
  403. resetRenderStructSize() {
  404. let curOptions = [];
  405. this.renderStructList.forEach((elem, eindex) => {
  406. const elemDom = document.getElementById(`rich-text-${elem.id}`);
  407. elem.w = elemDom.offsetWidth;
  408. elem.h = elemDom.offsetHeight;
  409. if (elem.contType !== "option") return;
  410. curOptions.push(elem);
  411. // 全选选项逻辑
  412. const nextElem = this.renderStructList[eindex + 1];
  413. if (nextElem && nextElem.contType === "option") return;
  414. curOptions.forEach((optionElem) => {
  415. optionElem._percent = this.getSizePercent(
  416. optionElem.w,
  417. this.maxColumnWidth
  418. );
  419. });
  420. const optionCount = curOptions.length;
  421. // 奇数选项,全部一行
  422. if (optionCount % 2 > 0) {
  423. curOptions.forEach((optionElem) => {
  424. optionElem._percent = 1;
  425. optionElem.styles.width = "100%";
  426. });
  427. curOptions = [];
  428. return;
  429. }
  430. const percents = curOptions.map((item) => item._percent);
  431. const maxPercent = maxNum(percents);
  432. let aveOptionPercent = 1;
  433. if (optionCount % 4 === 0) {
  434. aveOptionPercent = this.calcAveOptionPercent(maxPercent);
  435. } else {
  436. aveOptionPercent = maxPercent > 0.5 ? 1 : 0.5;
  437. }
  438. curOptions.forEach((optionElem) => {
  439. optionElem._percent = aveOptionPercent;
  440. optionElem.styles.width = aveOptionPercent * 100 + "%";
  441. });
  442. curOptions = [];
  443. });
  444. },
  445. buildPageAutoPage() {
  446. let pages = [];
  447. let curPage = null,
  448. curElem = null;
  449. let curColumn = null,
  450. curColumnNo = 0,
  451. curColumnHeight = 0;
  452. let curLinePercent = 0;
  453. const getNextElem = () => {
  454. return this.renderStructList.shift();
  455. };
  456. curElem = getNextElem();
  457. while (curElem) {
  458. if (!curPage) {
  459. curPage = this.getNewPageModel(pages.length);
  460. }
  461. if (!curColumn) {
  462. curColumn = curPage.columns[curColumnNo++];
  463. curColumnHeight = this.calcInitColumnHeight(curColumn);
  464. }
  465. if (
  466. curElem.contType !== "option" ||
  467. (curElem.contType === "option" && curElem._percent === 1)
  468. ) {
  469. // 非选项,单独占整行
  470. curLinePercent = 1;
  471. if (curElem.h + curColumnHeight > this.maxColumnHeight) {
  472. // 当前栏满了
  473. if (curColumnNo >= curPage.columnNumber) {
  474. // 当前页满了
  475. pages.push(curPage);
  476. curPage = null;
  477. curColumnNo = null;
  478. }
  479. curColumn = null;
  480. curColumnHeight = 0;
  481. } else {
  482. // 当前栏未满
  483. curColumnHeight += curElem.h;
  484. curColumn.texts.push(curElem);
  485. curElem = getNextElem();
  486. }
  487. } else {
  488. // 选项的处理
  489. if (curLinePercent + curElem._percent > 1) {
  490. // 行满了,放下一行
  491. if (curElem.h + curColumnHeight > this.maxColumnHeight) {
  492. curLinePercent = 1;
  493. // 当前栏满了
  494. if (curColumnNo >= curPage.columnNumber) {
  495. // 当前页满了
  496. pages.push(curPage);
  497. curPage = null;
  498. curColumnNo = null;
  499. }
  500. curColumn = null;
  501. curColumnHeight = 0;
  502. } else {
  503. // 当前栏未满,放下一行
  504. curLinePercent = curElem._percent;
  505. curColumnHeight += curElem.h;
  506. curColumn.texts.push(curElem);
  507. curElem = getNextElem();
  508. }
  509. } else {
  510. // 行未满,放当前行
  511. curLinePercent += curElem._percent;
  512. curColumn.texts.push(curElem);
  513. curElem = getNextElem();
  514. }
  515. }
  516. }
  517. if (curPage) {
  518. pages.push(curPage);
  519. curPage = null;
  520. }
  521. // 正文部分保证偶数页
  522. if (pages.length % 2) {
  523. pages.push(this.getNewPageModel(pages.length));
  524. }
  525. if (this.paperTempJson.pageConfig.showCover) {
  526. // 封面自动插入反面空白页
  527. let coverPages = deepCopy(
  528. this.paperTempJson.pages.filter((p) => p.pageType === "cover")
  529. );
  530. let coverBackPage = deepCopy(coverPages[0]);
  531. coverBackPage.columns.forEach((column) => {
  532. column.elements = [];
  533. });
  534. let nCoverPages = [];
  535. coverPages.forEach((cpage) => {
  536. nCoverPages.push(cpage);
  537. nCoverPages.push(coverBackPage);
  538. });
  539. pages = [...nCoverPages, ...pages];
  540. }
  541. this.pages = pages;
  542. },
  543. getNewPageModel(pageNo) {
  544. let contentPages = this.paperTempJson.pages.slice(-2);
  545. let pNo = pageNo % 2;
  546. const pageTemp = contentPages[pNo];
  547. let newPage = getPageModel({
  548. ...this.paperTempJson.pageConfig,
  549. pageType: pageTemp.pageType,
  550. });
  551. newPage.sides = pageTemp.sides.map((elem) => {
  552. let nelem = deepCopy(elem);
  553. nelem.id = getElementId();
  554. nelem.key = randomCode();
  555. if (pNo === 1 && nelem.type === "GUTTER") {
  556. nelem.direction = "right";
  557. }
  558. return nelem;
  559. });
  560. newPage.columns.forEach((column) => {
  561. column.texts = [];
  562. });
  563. if (pageNo > 1) return newPage;
  564. newPage.columns.forEach((column, cindex) => {
  565. column.elements = pageTemp.columns[cindex].elements.map((elem) => {
  566. let nelem = deepCopy(elem);
  567. nelem.id = getElementId();
  568. nelem.key = randomCode();
  569. if (nelem.elements && nelem.elements.length) {
  570. nelem.elements.forEach((celem) => {
  571. celem.id = getElementId();
  572. celem.key = randomCode();
  573. });
  574. }
  575. return nelem;
  576. });
  577. column.texts = [];
  578. });
  579. return newPage;
  580. },
  581. calcAveOptionPercent(maxPercent) {
  582. if (maxPercent > 0.5) return 1;
  583. if (maxPercent > 0.25) return 0.5;
  584. return 0.25;
  585. },
  586. calcInitColumnHeight(column) {
  587. return calcSum(column.elements.map((item) => item.h));
  588. },
  589. getSizePercent(size, fullSize) {
  590. const rate = size / fullSize;
  591. if (rate <= 0.25) return 0.25;
  592. if (rate <= 0.5) return 0.5;
  593. return 1;
  594. },
  595. // img
  596. loadImg(url) {
  597. return new Promise((resolve, reject) => {
  598. const img = new Image();
  599. img.onload = function () {
  600. resolve(true);
  601. };
  602. img.onerror = function () {
  603. reject();
  604. };
  605. img.src = url;
  606. });
  607. },
  608. getRichJsonImgUrls(richJson) {
  609. let urls = [];
  610. if (!richJson) return urls;
  611. richJson.sections.forEach((section) => {
  612. section.blocks.forEach((elem) => {
  613. if (elem.type === "image" && elem.value.startsWith("http")) {
  614. urls.push(elem.value);
  615. }
  616. });
  617. });
  618. return urls;
  619. },
  620. async waitAllImgLoaded() {
  621. let imgUrls = [];
  622. this.renderStructList.forEach((item) => {
  623. if (item.contType === "gap") return;
  624. imgUrls.push(...this.getRichJsonImgUrls(item.content));
  625. });
  626. // console.log(imgUrls);
  627. if (!imgUrls.length) return Promise.resolve(true);
  628. const imgLoads = imgUrls.map((item) => this.loadImg(item));
  629. const imgLoadResult = await Promise.all(imgLoads).catch(() => {});
  630. if (imgLoadResult && imgLoadResult.length) {
  631. return Promise.resolve(true);
  632. } else {
  633. return Promise.reject();
  634. }
  635. },
  636. getPreviewTemp() {
  637. return previewTemp(this.$refs.PaperTemplateView.$el.outerHTML);
  638. },
  639. async toDownload() {
  640. const htmlCont = this.getPreviewTemp();
  641. if (this.downloading) return;
  642. this.downloading = true;
  643. const res = await downloadByApi(() => {
  644. return paperPdfDownloadApi({
  645. content: htmlCont,
  646. templateId: this.curPaperTemp.id,
  647. paperId: this.paperId,
  648. });
  649. }).catch((e) => {
  650. this.$message.error(e || "下载失败,请重新尝试!");
  651. });
  652. this.downloading = false;
  653. if (!res) return;
  654. this.$message.success("下载成功!");
  655. },
  656. },
  657. };
  658. </script>
  659. <style>
  660. .paper-template-build {
  661. text-align: center;
  662. }
  663. .paper-template-build-body {
  664. display: inline-block;
  665. text-align: initial;
  666. }
  667. .paper-template-build .page-box {
  668. margin-top: 10px;
  669. margin-bottom: 10px;
  670. }
  671. </style>