PaperTemplateBuild.vue 20 KB

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