MarkParamStructure.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. <template>
  2. <div class="mark-param-structure">
  3. <div class="part-box part-box-pad">
  4. <p class="tips-info">
  5. 1.请确认展示的试卷结构与提交的试卷、答题卡是否一致?
  6. </p>
  7. <p class="tips-info">2.请补充所有题目的小题分值,并确认试卷总分!</p>
  8. <p class="tips-info tips-error">
  9. 3.开始阅卷后不允许修改试卷结构,请确认清楚后再提交!
  10. </p>
  11. </div>
  12. <div class="part-box part-box-pad mb-0">
  13. <el-table
  14. ref="TableList"
  15. :data="tableData"
  16. border
  17. :row-class-name="getRowClassName"
  18. >
  19. <el-table-column width="50" align="center">
  20. <template slot-scope="scope" v-if="scope.row.mainFirstSub">
  21. <div
  22. :class="[
  23. 'expand-btn',
  24. { 'expand-btn-unexpand': !scope.row.expandSub },
  25. ]"
  26. @click="switchExpandSub(scope.row)"
  27. >
  28. <i
  29. :class="scope.row.expandSub ? 'el-icon-minus' : 'el-icon-plus'"
  30. ></i>
  31. </div>
  32. </template>
  33. </el-table-column>
  34. <template v-if="structureCanEdit">
  35. <el-table-column prop="mainTitle" label="大题名称">
  36. <span slot-scope="scope" v-if="scope.row.mainFirstSub">
  37. <el-input
  38. v-model.trim="scope.row.mainTitle"
  39. size="small"
  40. :maxlength="32"
  41. clearable
  42. @change="mainTitleChange(scope.row)"
  43. ></el-input>
  44. </span>
  45. </el-table-column>
  46. <el-table-column prop="questionType" label="题型" width="120">
  47. <template slot-scope="scope" v-if="scope.row.mainFirstSub">
  48. <el-select
  49. v-model="scope.row.questionType"
  50. placeholder="请选择"
  51. class="width-full"
  52. @change="qTypeChange(scope.row)"
  53. >
  54. <el-option
  55. v-for="item in QUESTION_TYPE_LIST"
  56. :key="item.code"
  57. :value="item.code"
  58. :label="item.name"
  59. >
  60. </el-option>
  61. </el-select>
  62. </template>
  63. </el-table-column>
  64. <el-table-column label="选项个数" width="100">
  65. <template
  66. slot-scope="scope"
  67. v-if="scope.row.questionType <= 2 && scope.row.mainFirstSub"
  68. >
  69. <el-input-number
  70. v-model="scope.row.optionCount"
  71. class="width-full"
  72. size="small"
  73. :min="2"
  74. :max="26"
  75. :step="1"
  76. step-strictly
  77. :controls="false"
  78. @change="optionCountChange(scope.row)"
  79. ></el-input-number>
  80. </template>
  81. </el-table-column>
  82. </template>
  83. <template v-else>
  84. <el-table-column prop="mainTitle" label="大题名称">
  85. <span slot-scope="scope" v-if="scope.row.mainFirstSub">
  86. {{ scope.row.mainTitle }}
  87. </span>
  88. </el-table-column>
  89. <el-table-column label="题型" width="120">
  90. <template slot-scope="scope" v-if="scope.row.mainFirstSub">
  91. {{ questionTypeDict[scope.row.questionType] }}
  92. </template>
  93. </el-table-column>
  94. </template>
  95. <el-table-column prop="mainNumber" label="大题号" width="80">
  96. <template slot-scope="scope" v-if="scope.row.mainFirstSub">
  97. <span>{{ scope.row.mainNumber }}</span>
  98. </template>
  99. </el-table-column>
  100. <el-table-column
  101. prop="subNumber"
  102. label="小题号"
  103. width="80"
  104. ></el-table-column>
  105. <el-table-column label="每题分值" width="120">
  106. <template slot-scope="scope" v-if="scope.row.mainFirstSub">
  107. <el-input-number
  108. v-model="scoresPerTopic[scope.row.mainId]"
  109. class="width-full"
  110. size="small"
  111. :min="0.5"
  112. :max="500"
  113. :step="0.5"
  114. step-strictly
  115. :controls="false"
  116. placeholder="每题分值"
  117. @change="(val) => scorePerTopicChange(val, scope.row)"
  118. ></el-input-number>
  119. </template>
  120. </el-table-column>
  121. <el-table-column prop="totalScore" label="小题满分" width="120">
  122. <template slot-scope="scope">
  123. <el-input-number
  124. v-model="scope.row.totalScore"
  125. class="width-full"
  126. size="small"
  127. :min="0.5"
  128. :max="500"
  129. :step="0.5"
  130. step-strictly
  131. :controls="false"
  132. placeholder="小题分值"
  133. @change="totalScoreChange(scope.row)"
  134. ></el-input-number>
  135. </template>
  136. </el-table-column>
  137. <el-table-column label="每题间隔分" width="120">
  138. <template slot-scope="scope" v-if="scope.row.mainFirstSub">
  139. <el-input-number
  140. v-model="intervalScorePerTopic[scope.row.mainId]"
  141. class="width-full"
  142. size="small"
  143. :min="0.5"
  144. :max="500"
  145. :step="0.5"
  146. step-strictly
  147. :controls="false"
  148. placeholder="每题间隔分"
  149. @change="(val) => intervalScorePerTopicChange(val, scope.row)"
  150. ></el-input-number>
  151. </template>
  152. </el-table-column>
  153. <el-table-column prop="intervalScore" label="间隔分" width="120">
  154. <template slot-scope="scope">
  155. <el-input-number
  156. v-model="scope.row.intervalScore"
  157. class="width-full"
  158. size="small"
  159. :min="0.5"
  160. :max="scope.row.totalScore"
  161. :step="0.5"
  162. step-strictly
  163. :controls="false"
  164. placeholder="小题间隔分"
  165. ></el-input-number>
  166. </template>
  167. </el-table-column>
  168. <el-table-column
  169. v-if="structureCanEdit"
  170. class-name="action-column"
  171. label="操作"
  172. width="200px"
  173. >
  174. <template slot-scope="scope">
  175. <el-button
  176. class="btn-primary"
  177. type="text"
  178. @click="toAddMain(scope.row)"
  179. >新增大题</el-button
  180. >
  181. <el-button
  182. class="btn-primary"
  183. type="text"
  184. @click="toAddSub(scope.row)"
  185. >新增小题</el-button
  186. >
  187. <el-button
  188. :disabled="tableData.length <= 1"
  189. class="btn-danger"
  190. type="text"
  191. @click="toDeleteSub(scope.row)"
  192. >删除</el-button
  193. >
  194. </template>
  195. </el-table-column>
  196. </el-table>
  197. </div>
  198. <div class="total-info">
  199. 试卷总分:<span>{{ paperTotalScore }}</span
  200. >分
  201. </div>
  202. <div class="mark-footer">
  203. <el-button type="primary" :disabled="loading" @click="submit"
  204. >提交</el-button
  205. >
  206. <el-button @click="cancel">取消</el-button>
  207. </div>
  208. </div>
  209. </template>
  210. <script>
  211. import { calcSum } from "@/plugins/utils";
  212. import { QUESTION_TYPE_LIST } from "@/constants/enumerate";
  213. import { mapState, mapMutations } from "vuex";
  214. import { markStructureSave } from "../../api";
  215. import { omit } from "lodash";
  216. export default {
  217. name: "mark-paper-structure",
  218. data() {
  219. return {
  220. tableData: [],
  221. QUESTION_TYPE_LIST,
  222. questionTypeDict: {},
  223. scoresPerTopic: {},
  224. intervalScorePerTopic: {},
  225. loading: false,
  226. };
  227. },
  228. computed: {
  229. ...mapState("markParam", [
  230. "basicInfo",
  231. "structureCanEdit",
  232. "paperStructureInfo",
  233. ]),
  234. paperTotalScore() {
  235. return calcSum(this.tableData.map((item) => item.totalScore || 0));
  236. },
  237. },
  238. mounted() {
  239. this.initData();
  240. },
  241. methods: {
  242. ...mapMutations("markParam", ["setPaperStructureInfo"]),
  243. initData() {
  244. let questionTypeDict = {};
  245. QUESTION_TYPE_LIST.forEach((item) => {
  246. questionTypeDict[item.code] = item.name;
  247. });
  248. this.questionTypeDict = questionTypeDict;
  249. let curMainNumber = null;
  250. let curMainId = null;
  251. let scoresPerTopic = {},
  252. intervalScorePerTopic = {};
  253. this.tableData = this.paperStructureInfo.map((item) => {
  254. let nitem = {
  255. ...item,
  256. key: this.$randomCode(),
  257. mainFirstSub: false,
  258. expandSub: true,
  259. };
  260. if (curMainNumber !== item.mainNumber) {
  261. curMainNumber = item.mainNumber;
  262. curMainId = this.$randomCode();
  263. scoresPerTopic[curMainNumber] = undefined;
  264. intervalScorePerTopic[curMainNumber] = undefined;
  265. nitem.mainFirstSub = true;
  266. }
  267. nitem.totalScore = nitem.totalScore || undefined;
  268. nitem.intervalScore = nitem.intervalScore || undefined;
  269. nitem.mainId = curMainId;
  270. return nitem;
  271. });
  272. this.scoresPerTopic = scoresPerTopic;
  273. this.intervalScorePerTopic = intervalScorePerTopic;
  274. if (!this.tableData.length && this.structureCanEdit) {
  275. this.createMain();
  276. }
  277. },
  278. getNewRow(val) {
  279. return this.$objAssign(
  280. {
  281. id: null,
  282. key: this.$randomCode(),
  283. mainId: null,
  284. objective: true,
  285. mainNumber: 1,
  286. subNumber: 1,
  287. mainTitle: "",
  288. answer: "",
  289. totalScore: undefined,
  290. intervalScore: undefined,
  291. objectivePolicy: null,
  292. questionType: null,
  293. optionCount: undefined,
  294. mainFirstSub: true,
  295. expandSub: true,
  296. },
  297. val
  298. );
  299. },
  300. createMain() {
  301. this.tableData.push(this.getNewRow({}));
  302. },
  303. getRowClassName({ row }) {
  304. let classNames = [];
  305. if (row.mainFirstSub) {
  306. classNames.push("row-main-first-sub");
  307. }
  308. if (!row.mainFirstSub && !row.expandSub) {
  309. classNames.push("row-unexpand-sub");
  310. }
  311. return classNames.join(" ");
  312. },
  313. getNextMainStartPos(startPos, curMainId) {
  314. let nextMainStartPos = null;
  315. for (let i = startPos, len = this.tableData.length; i < len; i++) {
  316. const element = this.tableData[i];
  317. if (element.mainId !== curMainId) {
  318. nextMainStartPos = i;
  319. return nextMainStartPos;
  320. }
  321. }
  322. if (nextMainStartPos === null) return this.tableData.length;
  323. },
  324. toAddMain(row) {
  325. const startPos = this.tableData.findIndex((item) => item.id === row.id);
  326. let nextMainStartPos = this.getNextMainStartPos(startPos, row.mainId);
  327. this.tableData.splice(
  328. nextMainStartPos,
  329. 0,
  330. this.getNewRow({
  331. mainId: this.$randomCode(),
  332. mainNumber: row.mainNumber + 1,
  333. totalScore: row.totalScore,
  334. questionType: row.questionType,
  335. })
  336. );
  337. this.updateMainData();
  338. },
  339. updateMainData() {
  340. let curMainNumber = 0,
  341. curMainId = null;
  342. this.tableData.forEach((item) => {
  343. if (item.mainId !== curMainId) {
  344. curMainId = item.mainId;
  345. curMainNumber++;
  346. }
  347. item.mainNumber = curMainNumber;
  348. });
  349. },
  350. toAddSub(row) {
  351. const subPos = this.tableData.findIndex((item) => item.id === row.id);
  352. this.tableData.splice(
  353. subPos + 1,
  354. 0,
  355. this.getNewRow({
  356. ...row,
  357. mainFirstSub: false,
  358. answer: "",
  359. key: this.$randomCode(),
  360. })
  361. );
  362. this.updateSubData(row.mainId);
  363. },
  364. updateSubData(mainId) {
  365. this.tableData
  366. .filter((item) => item.mainId === mainId)
  367. .forEach((item, index) => {
  368. item.subNumber = index + 1;
  369. });
  370. },
  371. toDeleteSub(row) {
  372. const subPos = this.tableData.findIndex((item) => item.id === row.id);
  373. this.tableData.splice(subPos, 1);
  374. this.tableData
  375. .filter((item) => item.mainId === row.mainId)
  376. .forEach((item, index) => {
  377. item.mainFirstSub = !index;
  378. });
  379. this.updateSubData(row.mainId);
  380. this.updateMainData();
  381. },
  382. switchExpandSub(row) {
  383. row.expandSub = !row.expandSub;
  384. this.tableData
  385. .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
  386. .forEach((item) => (item.expandSub = row.expandSub));
  387. },
  388. mainTitleChange(row) {
  389. this.tableData
  390. .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
  391. .forEach((item) => (item.mainTitle = row.mainTitle));
  392. },
  393. qTypeChange(row) {
  394. const curQt = this.QUESTION_TYPE_LIST.find(
  395. (item) => item.code === row.type
  396. );
  397. if (!curQt) return;
  398. this.tableData
  399. .filter((item) => item.mainId === row.mainId)
  400. .forEach((item) => {
  401. item.questionType = curQt.code;
  402. item.objective = curQt.qType === "objective";
  403. item.optionCount = curQt.optionCount;
  404. });
  405. },
  406. optionCountChange(row) {
  407. if (!row.optionCount) return;
  408. this.tableData
  409. .filter((item) => item.mainId === row.mainId && !item.mainFirstSub)
  410. .forEach((item) => {
  411. item.optionCount = row.optionCount;
  412. });
  413. },
  414. scorePerTopicChange(val, row) {
  415. if (!val) return;
  416. this.tableData
  417. .filter((item) => item.mainId === row.mainId)
  418. .forEach((item) => {
  419. item.totalScore = val;
  420. item.intervalScore = Math.min(item.totalScore, item.intervalScore);
  421. });
  422. },
  423. intervalScorePerTopicChange(val, row) {
  424. if (!val) return;
  425. this.tableData
  426. .filter((item) => item.mainId === row.mainId)
  427. .forEach((item) => {
  428. item.intervalScore = Math.min(item.totalScore, val);
  429. });
  430. },
  431. totalScoreChange(row) {
  432. row.intervalScore = Math.min(row.totalScore, row.intervalScore);
  433. },
  434. checkData() {
  435. let errorMessages = [];
  436. this.tableData.forEach((item) => {
  437. let errorMsg = ``;
  438. if (item.mainFirstSub) {
  439. let errorFields = [];
  440. if (!item.mainTitle) {
  441. errorFields.push("大题名称");
  442. }
  443. if (!item.mainNumber) {
  444. errorFields.push("大题号");
  445. }
  446. if (!item.optionCount && item.questionType <= 2) {
  447. errorFields.push("选项个数");
  448. }
  449. if (errorFields.length) {
  450. errorMsg += `${errorFields.join("、")}不能为空,`;
  451. }
  452. }
  453. let errorFields = [];
  454. if (!item.subNumber) {
  455. errorFields.push("小题号");
  456. }
  457. if (!item.totalScore) {
  458. errorFields.push("小题满分");
  459. }
  460. if (!item.intervalScore) {
  461. errorFields.push("评卷间隔分");
  462. }
  463. if (errorFields.length) {
  464. errorMsg += `第${item.subNumber}小题,${errorFields.join(
  465. "、"
  466. )}不能为空,`;
  467. }
  468. if (errorMsg) {
  469. errorMsg = `第${item.mainNumber}大题,${errorMsg}`;
  470. errorMessages.push(errorMsg);
  471. }
  472. });
  473. if (errorMessages.length) {
  474. this.$message.error(errorMessages.join("。"));
  475. return;
  476. }
  477. return true;
  478. },
  479. getData() {
  480. return this.tableData.map((item) => {
  481. return omit(item, ["key", "mainId", "expandSub"]);
  482. });
  483. },
  484. statPaperStructure() {
  485. const questionCount = this.tableData.length;
  486. const subjectiveQuestionCount = this.tableData.filter(
  487. (item) => !item.objective
  488. ).length;
  489. return {
  490. questionCount,
  491. paperTotalScore: calcSum(
  492. this.tableData.map((item) => item.totalScore || 0)
  493. ),
  494. subjectiveQuestionCount,
  495. objectiveQuestionCount: questionCount - subjectiveQuestionCount,
  496. };
  497. },
  498. async submit() {
  499. if (this.loading) return;
  500. if (!this.checkData()) return;
  501. const questions = this.getData();
  502. const paperStat = this.statPaperStructure();
  503. let tipsContent = `本试卷共${paperStat.questionCount}道小题,客观题${paperStat.objectiveQuestionCount}道,主观题${paperStat.subjectiveQuestionCount}道,总分为${paperStat.paperTotalScore}分。`;
  504. const confirm = await this.$confirm(
  505. `${tipsContent}确定要提交吗?`,
  506. "提示",
  507. {
  508. type: "warning",
  509. }
  510. ).catch(() => {});
  511. if (confirm !== "confirm") return;
  512. this.loading = true;
  513. const res = await markStructureSave({
  514. examId: this.basicInfo.examId,
  515. paperNumber: this.basicInfo.paperNumber,
  516. questions,
  517. }).catch(() => {});
  518. this.loading = false;
  519. if (!res) return;
  520. this.$message.success("保存成功!");
  521. this.setPaperStructureInfo(questions);
  522. this.$emit("confirm");
  523. },
  524. cancel() {
  525. this.$emit("cancel");
  526. },
  527. },
  528. };
  529. </script>