PaperTemplateBuild.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  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
  33. type="primary"
  34. size="small"
  35. :loading="downloading"
  36. @click="toAction('downloadPdf')"
  37. >下载试卷</el-button
  38. >
  39. <el-button
  40. type="primary"
  41. size="small"
  42. :loading="downloading"
  43. @click="toAction('buildPackge')"
  44. >生成数据包</el-button
  45. >
  46. </div>
  47. <paper-build-config
  48. ref="PaperBuildConfig"
  49. :config-sources="configSources"
  50. :showConfirmBtn="false"
  51. ></paper-build-config>
  52. <paper-template-view
  53. ref="PaperTemplateView"
  54. id="paper-template-view"
  55. class="preview-body"
  56. :pages="pages"
  57. :page-config="paperTempJson.pageConfig"
  58. >
  59. <template #texts="{ texts }">
  60. <div
  61. v-for="groupItem in texts"
  62. :key="groupItem.id"
  63. :id="groupItem.id"
  64. :class="[
  65. 'group-item',
  66. { 'is-detail-title': groupItem.type === 'detail-title' },
  67. {
  68. 'is-detail-description':
  69. groupItem.type === 'detail-description',
  70. },
  71. ]"
  72. :style="groupItem.styles"
  73. >
  74. <table
  75. v-if="
  76. configModalForm.showDetailScoreTable &&
  77. groupItem.type === 'detail-title'
  78. "
  79. class="detail-score-table"
  80. >
  81. <tr>
  82. <th>得分</th>
  83. <th>评分人</th>
  84. </tr>
  85. <tr>
  86. <td></td>
  87. <td></td>
  88. </tr>
  89. </table>
  90. <!-- 内容 -->
  91. <template v-if="groupItem.elements && groupItem.elements.length">
  92. <elem-rich-text
  93. v-for="elem in groupItem.elements"
  94. :id="`rich-text-${elem.id}`"
  95. :key="elem.id"
  96. :data="elem"
  97. ></elem-rich-text>
  98. </template>
  99. </div>
  100. </template>
  101. </paper-template-view>
  102. <!-- process dom -->
  103. <div v-if="elementList.length" class="element-list">
  104. <elem-rich-text
  105. v-for="elem in elementList"
  106. :id="elem.id"
  107. :key="elem.id"
  108. :data="elem"
  109. ></elem-rich-text>
  110. </div>
  111. <!-- answer dom -->
  112. <answer-template-view
  113. ref="AnswerTemplateView"
  114. id="answer-template-view"
  115. class="preview-body"
  116. :answerData="paperJson"
  117. :pageCountMode="configModalForm.pageCountMode"
  118. ></answer-template-view>
  119. </div>
  120. </div>
  121. </template>
  122. <script>
  123. import ElemRichText from "../elements/rich-text/ElemRichText.vue";
  124. import PaperTemplateView from "../components/PaperTemplateView.vue";
  125. import PaperBuildConfig from "../components/PaperBuildConfig.vue";
  126. import AnswerTemplateView from "../components/AnswerTemplateView.vue";
  127. import { deepCopy } from "../../card/plugins/utils";
  128. import { paperDetailInfoApi } from "../../paper/api";
  129. import { paperTemplateListApi, paperAndAnswerPdfDownloadApi } from "../api";
  130. import paperTemplateBuildMixins from "./paperTemplateBuildMixins";
  131. import { buildPdf } from "@/plugins/htmlToPdf";
  132. export default {
  133. name: "PaperTemplateBuild",
  134. components: {
  135. PaperTemplateView,
  136. PaperBuildConfig,
  137. ElemRichText,
  138. AnswerTemplateView,
  139. },
  140. mixins: [paperTemplateBuildMixins],
  141. data() {
  142. return {
  143. paperId: this.$route.params.paperId,
  144. viewType: this.$route.params.viewType,
  145. seqMode: "MODE3",
  146. paperJson: {},
  147. paperTempJson: {
  148. pages: [],
  149. pageConfig: {},
  150. },
  151. maxColumnWidth: 200,
  152. maxColumnHeight: 200,
  153. paperTempList: [],
  154. curPaperTemp: {},
  155. downloading: false,
  156. fieldData: {},
  157. paperStructs: [],
  158. configModalForm: {
  159. showDetailNo: true,
  160. showDetailScoreInfo: true,
  161. showDetailScoreTable: false,
  162. pageCountMode: "SIMPLE",
  163. },
  164. configSources: [],
  165. prepareDownloadPdf: false,
  166. actionType: "",
  167. };
  168. },
  169. mounted() {
  170. if (this.viewType === "frame") {
  171. this.initFrame();
  172. return;
  173. }
  174. this.initData();
  175. },
  176. methods: {
  177. async initFrame() {
  178. try {
  179. const paperSet = window.parent.paperSet;
  180. if (!paperSet) {
  181. this.emitFrameResult(false, "数据缺失");
  182. return;
  183. }
  184. this.seqMode = paperSet.seqMode;
  185. this.curPaperTemp = paperSet.paperTemp;
  186. this.configModalForm = paperSet.configModalForm;
  187. await this.getPaperJson();
  188. let paperTempJson = this.curPaperTemp.content
  189. ? JSON.parse(this.curPaperTemp.content)
  190. : { pages: [], pageConfig: {} };
  191. this.paperTempJson = paperTempJson;
  192. console.log("paperTempJson", paperTempJson);
  193. this.pages = paperTempJson.pages;
  194. this.updaterFieldInfo();
  195. } catch (error) {
  196. this.emitFrameResult(false, "数据错误");
  197. }
  198. this.$nextTick(async () => {
  199. try {
  200. this.maxColumnWidth =
  201. document.getElementById("column-0-0").offsetWidth;
  202. this.maxColumnHeight =
  203. document.getElementById("column-0-0").offsetHeight - 10;
  204. this.buildElementsFromStruct();
  205. } catch (error) {
  206. this.emitFrameResult(false, "构建元素错误");
  207. }
  208. const loadRes = await this.waitAllImgLoaded().catch(() => {});
  209. if (!loadRes) {
  210. this.emitFrameResult(false, "数据缓存错误");
  211. return;
  212. }
  213. this.$nextTick(() => {
  214. try {
  215. this.updateElementWidthInfo();
  216. this.buildGroupsFromStruct();
  217. this.buildPrePages();
  218. } catch (error) {
  219. this.emitFrameResult(false, "预构建页面错误");
  220. }
  221. this.$nextTick(() => {
  222. try {
  223. this.updateGroupHeightInfo();
  224. this.buildPagesByAutoPage();
  225. } catch (error) {
  226. this.emitFrameResult(false, "构建pdf错误");
  227. }
  228. this.$nextTick(async () => {
  229. const paperBlob = await buildPdf(
  230. {
  231. element: document.getElementById("paper-template-view"),
  232. pageSize: this.paperTempJson.pageConfig.pageSize,
  233. },
  234. true
  235. ).catch((error) => {
  236. console.error(error);
  237. });
  238. if (!paperBlob) {
  239. this.emitFrameResult(false, "生成试卷pdf错误");
  240. return;
  241. }
  242. this.emitFrameResult(true, "", paperBlob);
  243. });
  244. });
  245. });
  246. });
  247. },
  248. emitFrameResult(success = true, errorMsg = "", blobCont = null) {
  249. window.parent &&
  250. window.parent.submitPaperTemp &&
  251. window.parent.submitPaperTemp({
  252. success,
  253. errorMsg,
  254. blobCont,
  255. contType: "paper",
  256. });
  257. },
  258. async initData() {
  259. await this.getPaperJson();
  260. await this.getPaperTempList();
  261. if (!this.paperTempList.length) {
  262. this.$message.error("导出模板缺失!");
  263. return;
  264. }
  265. this.paperTempChange(this.paperTempList[0]);
  266. },
  267. async getPaperJson() {
  268. const res = await paperDetailInfoApi({
  269. paperId: this.paperId,
  270. seqMode: this.seqMode,
  271. });
  272. this.paperJson = res.data;
  273. this.resetClozeSerialNo(this.paperJson);
  274. this.fieldData = {
  275. paperName: res.data.name,
  276. courseName: res.data.course.name,
  277. courseCode: res.data.course.code,
  278. totalScore: res.data.totalScore,
  279. rootOrgName: res.data.rootOrgName,
  280. };
  281. this.paperStructs = this.paperJson.paperDetails.map((detail) => {
  282. return {
  283. detailName: detail.name,
  284. questionCount: detail.unitCount,
  285. totalScore: detail.score,
  286. };
  287. });
  288. },
  289. resetClozeSerialNo(paperData) {
  290. const clozeQuestionTypes = ["CLOZE", "BANKED_CLOZE"];
  291. paperData.paperDetails.forEach((detail) => {
  292. detail.paperDetailUnits.forEach((question) => {
  293. if (!clozeQuestionTypes.includes(question.questionType)) return;
  294. question.question.quesBody.sections.forEach((section) => {
  295. section.blocks.forEach((block) => {
  296. if (block.type !== "cloze") return;
  297. block.value =
  298. question.question.subQuestions[block.value - 1].questionSeq;
  299. });
  300. });
  301. });
  302. });
  303. },
  304. async getPaperTempList() {
  305. const res = await paperTemplateListApi("PAPER_EXPORT");
  306. this.paperTempList = res.data;
  307. },
  308. getConfigSources() {
  309. const { pages } = this.paperTempJson;
  310. let sources = [],
  311. fieldAble = {};
  312. pages.forEach((page) => {
  313. page.columns.forEach((column) => {
  314. column.elements.forEach((element) => {
  315. if (element.type !== "PAPER_PROPS") return;
  316. if (!sources.length) {
  317. sources = deepCopy(element.props);
  318. }
  319. element.props.forEach((prop) => {
  320. fieldAble[prop.field] = fieldAble[prop.field] || prop.enable;
  321. });
  322. });
  323. });
  324. });
  325. sources.forEach((item) => {
  326. item.enable = fieldAble[item.field];
  327. });
  328. this.configSources = sources;
  329. },
  330. updaterFieldInfo() {
  331. const VALID_ELEMENTS_FOR_EXTERNAL = ["FIELD_TEXT"];
  332. this.paperTempJson.pages.forEach((page) => {
  333. if (page.pageNumberRule) {
  334. Object.keys(this.fieldData).forEach((field) => {
  335. page.pageNumberRule = page.pageNumberRule.replace(
  336. "${" + field + "}",
  337. this.fieldData[field]
  338. );
  339. });
  340. }
  341. page.columns.forEach((column) => {
  342. column.elements.forEach((elem) => {
  343. if (elem.type === "PAPER_STRUCT") {
  344. elem.structs = this.paperStructs;
  345. } else if (elem.type === "SCORE_TABLE") {
  346. elem.detailCount = this.paperStructs.length;
  347. } else if (elem.type === "PAPER_PROPS") {
  348. elem.props.forEach((prop) => {
  349. prop.value = this.configModalForm[prop.field];
  350. });
  351. }
  352. if (!elem.elements || !elem.elements.length) return;
  353. elem.elements.forEach((element) => {
  354. if (!VALID_ELEMENTS_FOR_EXTERNAL.includes(element.type)) return;
  355. if (element.type === "FIELD_TEXT" && element.field) {
  356. element.content = this.fieldData[element.field];
  357. }
  358. });
  359. });
  360. });
  361. });
  362. },
  363. async seqModeChange() {
  364. await this.getPaperJson();
  365. this.$nextTick(() => {
  366. this.buildData();
  367. });
  368. },
  369. buildConfigChange(val) {
  370. this.configModalForm = val;
  371. this.updaterFieldInfo();
  372. this.$nextTick(() => {
  373. this.buildData();
  374. });
  375. },
  376. paperTempChange(paperTemp) {
  377. // console.log(paperTemp);
  378. this.curPaperTemp = paperTemp;
  379. let paperTempJson = paperTemp.content
  380. ? JSON.parse(paperTemp.content)
  381. : { pages: [], pageConfig: {} };
  382. this.paperTempJson = paperTempJson;
  383. this.pages = paperTempJson.pages;
  384. this.getConfigSources();
  385. this.updaterFieldInfo();
  386. this.$nextTick(() => {
  387. this.buildData();
  388. });
  389. },
  390. async buildData() {
  391. this.maxColumnWidth = document.getElementById("column-0-0").offsetWidth;
  392. this.maxColumnHeight =
  393. document.getElementById("column-0-0").offsetHeight - 10;
  394. this.buildElementsFromStruct();
  395. const loadRes = await this.waitAllImgLoaded().catch(() => {});
  396. if (!loadRes) {
  397. this.$message.error("图片加载有误!");
  398. return;
  399. }
  400. this.$nextTick(() => {
  401. this.updateElementWidthInfo();
  402. this.buildGroupsFromStruct();
  403. this.buildPrePages();
  404. this.$nextTick(() => {
  405. this.updateGroupHeightInfo();
  406. this.buildPagesByAutoPage();
  407. if (this.prepareDownloadPdf) {
  408. this.$nextTick(async () => {
  409. await this.runAction().catch(() => {});
  410. this.prepareDownloadPdf = false;
  411. });
  412. }
  413. });
  414. });
  415. },
  416. // img ------ start >
  417. loadImg(url) {
  418. return new Promise((resolve, reject) => {
  419. const img = new Image();
  420. img.onload = function () {
  421. resolve(true);
  422. };
  423. img.onerror = function () {
  424. reject();
  425. };
  426. img.src = url;
  427. });
  428. },
  429. getRichJsonImgUrls(richJson) {
  430. let urls = [];
  431. if (!richJson) return urls;
  432. richJson.sections.forEach((section) => {
  433. section.blocks.forEach((elem) => {
  434. if (elem.type === "image" && elem.value.startsWith("http")) {
  435. urls.push(elem.value);
  436. }
  437. });
  438. });
  439. return urls;
  440. },
  441. async waitAllImgLoaded() {
  442. let imgUrls = [];
  443. this.elementList.forEach((item) => {
  444. imgUrls.push(...this.getRichJsonImgUrls(item.content));
  445. });
  446. // console.log(imgUrls);
  447. if (!imgUrls.length) return Promise.resolve(true);
  448. const imgLoads = imgUrls.map((item) => this.loadImg(item));
  449. const imgLoadResult = await Promise.all(imgLoads).catch(() => {});
  450. if (imgLoadResult && imgLoadResult.length) {
  451. return Promise.resolve(true);
  452. } else {
  453. return Promise.reject();
  454. }
  455. },
  456. // img ------ end >
  457. // download ------ start >
  458. async toAction(actionType) {
  459. const valid = await this.$refs.PaperBuildConfig.checkData().catch(
  460. () => {}
  461. );
  462. if (!valid) return;
  463. this.actionType = actionType;
  464. const configData = this.$refs.PaperBuildConfig.getData();
  465. if (JSON.stringify(configData) === JSON.stringify(this.configModalForm)) {
  466. this.runAction();
  467. } else {
  468. this.prepareDownloadPdf = true;
  469. this.buildConfigChange(configData);
  470. }
  471. },
  472. async runAction() {
  473. if (this.actionType === "downloadPdf") {
  474. await this.downloadPaperPdf();
  475. } else {
  476. await this.buildPackage();
  477. }
  478. },
  479. async downloadPaperPdf() {
  480. if (this.downloading) return;
  481. this.downloading = true;
  482. let result = true;
  483. await buildPdf({
  484. element: document.getElementById("paper-template-view"),
  485. filename: `${this.fieldData.courseName}(${this.fieldData.courseCode})_${this.fieldData.paperName}.pdf`,
  486. pageSize: this.paperTempJson.pageConfig.pageSize,
  487. }).catch((error) => {
  488. result = false;
  489. console.error(error);
  490. this.$message.error(error.message || "下载失败,请重新尝试!");
  491. });
  492. this.downloading = false;
  493. if (!result) return;
  494. this.$message.success("下载成功!");
  495. },
  496. async buildPackage() {
  497. if (this.downloading) return;
  498. this.downloading = true;
  499. // 试卷PDF
  500. const paperPdfName = `${this.fieldData.courseName}(${this.fieldData.courseCode})_${this.fieldData.paperName}.pdf`;
  501. const paperBlob = await buildPdf(
  502. {
  503. element: document.getElementById("paper-template-view"),
  504. pageSize: this.paperTempJson.pageConfig.pageSize,
  505. },
  506. true
  507. ).catch((error) => {
  508. this.downloading = false;
  509. console.error(error);
  510. this.$message.error("生成试卷pdf失败,请重新尝试!");
  511. });
  512. if (!paperBlob) return;
  513. // 答案pdf
  514. const answerPdfName = `${this.fieldData.courseName}(${this.fieldData.courseCode})_${this.fieldData.paperName}_答案.pdf`;
  515. const answerBlob = await buildPdf(
  516. {
  517. element: document.getElementById("answer-template-view"),
  518. pageSize: "A4",
  519. },
  520. true
  521. ).catch((error) => {
  522. this.downloading = false;
  523. console.error(error);
  524. this.$message.error("生成答案pdf失败,请重新尝试!");
  525. });
  526. if (!answerBlob) return;
  527. const data = new FormData();
  528. data.append(
  529. "paperPdf",
  530. new File([paperBlob], paperPdfName, { type: "application/pdf" })
  531. );
  532. data.append(
  533. "answerPdf",
  534. new File([answerBlob], answerPdfName, { type: "application/pdf" })
  535. );
  536. data.append("paperId", this.paperId);
  537. let result = true;
  538. await paperAndAnswerPdfDownloadApi(data).catch((error) => {
  539. result = false;
  540. console.error(error);
  541. });
  542. this.downloading = false;
  543. if (!result) return;
  544. this.$message.success("生成数据包成功!");
  545. },
  546. },
  547. };
  548. </script>
  549. <style>
  550. .paper-template-build {
  551. text-align: center;
  552. }
  553. .paper-template-build-body {
  554. display: inline-block;
  555. text-align: initial;
  556. }
  557. .paper-template-build .page-box {
  558. margin-top: 10px;
  559. margin-bottom: 10px;
  560. }
  561. .paper-template-build .paper-build-config {
  562. padding: 10px 15px 2px;
  563. margin-top: 5px;
  564. background: #fff;
  565. border-radius: 10px;
  566. }
  567. .paper-build-config .el-form-item {
  568. margin-bottom: 16px;
  569. margin-right: 30px;
  570. }
  571. .paper-template-build .element-list {
  572. visibility: hidden;
  573. position: absolute;
  574. width: 1200px;
  575. height: 600px;
  576. overflow: hidden;
  577. left: -9999px;
  578. top: 0;
  579. z-index: 1;
  580. }
  581. .paper-template-build .answer-template-view {
  582. position: absolute;
  583. left: -9999px;
  584. z-index: 999;
  585. visibility: hidden;
  586. }
  587. </style>