MarkParamGroup.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. <template>
  2. <div class="mark-param-group">
  3. <div class="box-justify part-box part-box-pad">
  4. <div>
  5. <p v-if="unsetQuestionNos" class="tips-info tips-error">
  6. 试题{{
  7. unsetQuestionNos
  8. }}未设置评卷员,请点击列表里设置评卷员选择评卷员
  9. </p>
  10. </div>
  11. <div>
  12. <el-button type="primary" @click="toPrev(1)">上一步</el-button>
  13. <el-button type="primary" @click="toNext(1)">下一步</el-button>
  14. </div>
  15. </div>
  16. <div class="part-box part-box-pad">
  17. <el-table
  18. :data="subjectiveTaskList"
  19. border
  20. :span-method="openMergeMarker ? objectSpanMethod : undefined"
  21. >
  22. <el-table-column label="大题名称" prop="mainTitle"> </el-table-column>
  23. <el-table-column label="大题号" prop="mainNumber" width="80">
  24. </el-table-column>
  25. <el-table-column label="小题号" prop="subNumber" width="80">
  26. </el-table-column>
  27. <el-table-column label="评卷员" min-width="200">
  28. <template slot-scope="scope">
  29. <el-tag
  30. v-for="user in scope.row.markers"
  31. :key="user.markUserQuestionId"
  32. class="tag-spin tag-wrap"
  33. size="medium"
  34. effect="dark"
  35. closable
  36. @close="toDeleteMarker(scope.row, user)"
  37. >
  38. {{ user.name }}({{ user.loginName }})
  39. </el-tag>
  40. <el-button
  41. type="text"
  42. class="btn-primary"
  43. @click="toSetMarker(scope.row)"
  44. >
  45. 设置评卷员
  46. </el-button>
  47. </template>
  48. </el-table-column>
  49. <el-table-column label="评卷方式" width="100">
  50. <template slot-scope="scope">
  51. {{ scope.row.doubleRate > 0 ? "双评" : "单评" }}
  52. </template>
  53. </el-table-column>
  54. <el-table-column label="仲裁阈值" width="100">
  55. <template v-if="scope.row.doubleEnable" slot-scope="scope">
  56. {{ scope.row.arbitrateThreshold }}
  57. </template>
  58. </el-table-column>
  59. <el-table-column label="合分策略" width="100">
  60. <template v-if="scope.row.doubleEnable" slot-scope="scope">
  61. {{ SCORE_POLICY_TYPE[scope.row.scorePolicy] }}
  62. </template>
  63. </el-table-column>
  64. <el-table-column label="双评比例" width="100">
  65. <template v-if="scope.row.doubleEnable" slot-scope="scope">
  66. {{ scope.row.doubleRate }}%
  67. </template>
  68. </el-table-column>
  69. <el-table-column label="评卷区" width="80" align="center">
  70. <template slot-scope="scope">
  71. <i
  72. v-if="scope.row.pictureConfigs?.length"
  73. class="el-icon-success color-success"
  74. ></i>
  75. </template>
  76. </el-table-column>
  77. <el-table-column class-name="action-column" label="操作" width="160px">
  78. <template slot-scope="scope">
  79. <el-button
  80. class="btn-primary"
  81. type="text"
  82. @click="toSetMarkType(scope.row)"
  83. >评卷方式设置</el-button
  84. >
  85. <el-button
  86. class="btn-primary"
  87. type="text"
  88. :disabled="!paperList.length"
  89. @click="toSetArea(scope.row)"
  90. >评卷区</el-button
  91. >
  92. </template>
  93. </el-table-column>
  94. </el-table>
  95. <!-- subjective answer -->
  96. <el-form class="mt-2" label-width="150px">
  97. <el-form-item label="请上传标答PDF文档:">
  98. <mark-param-subjective-answer></mark-param-subjective-answer>
  99. </el-form-item>
  100. </el-form>
  101. </div>
  102. <!-- ModifyMarkType -->
  103. <modify-mark-type
  104. ref="ModifyMarkType"
  105. :instance="curRow"
  106. @modified="markTypeModified"
  107. ></modify-mark-type>
  108. <!-- ModifyMarkMarker -->
  109. <modify-mark-marker
  110. ref="ModifyMarkMarker"
  111. :instance="curRow"
  112. :course-id="basicInfo.courseId"
  113. :question-list="curRowQuestions"
  114. @modified="markMarkerModified"
  115. ></modify-mark-marker>
  116. <!-- ModifyMarkArea -->
  117. <modify-mark-area
  118. ref="ModifyMarkArea"
  119. :base-info="basicInfo"
  120. :group="curRow"
  121. :paper-list="paperList"
  122. @modified="areaModified"
  123. ></modify-mark-area>
  124. </div>
  125. </template>
  126. <script>
  127. import { mapState, mapMutations } from "vuex";
  128. import ModifyMarkType from "./ModifyMarkType.vue";
  129. import ModifyMarkArea from "./ModifyMarkArea.vue";
  130. import ModifyMarkMarker from "./ModifyMarkMarker.vue";
  131. import MarkParamSubjectiveAnswer from "./MarkParamSubjectiveAnswer.vue";
  132. import {
  133. examStructureFindJpg,
  134. markSubjectiveBindMarker,
  135. markSubjectiveUpdateMarkType,
  136. markSubjectiveUpdateMarkArea,
  137. markSubjectiveUnbindMarker,
  138. } from "../../api";
  139. import { cardDetail } from "../../../card/api";
  140. import { SCORE_POLICY_TYPE } from "@/constants/enumerate";
  141. export default {
  142. name: "mark-param-group",
  143. components: {
  144. ModifyMarkType,
  145. ModifyMarkArea,
  146. ModifyMarkMarker,
  147. MarkParamSubjectiveAnswer,
  148. },
  149. data() {
  150. return {
  151. SCORE_POLICY_TYPE,
  152. questionCount: 0,
  153. curRow: {},
  154. curRowQuestions: [],
  155. MARK_TYPE: {
  156. 0: "单评",
  157. 1: "双评",
  158. },
  159. paperList: [],
  160. cardPages: [],
  161. fillQuestionRanges: [],
  162. };
  163. },
  164. computed: {
  165. ...mapState("markParam", [
  166. "basicInfo",
  167. "paperStructureInfo",
  168. "openMergeMarker",
  169. "subjectiveTaskList",
  170. ]),
  171. unsetQuestionNos() {
  172. return this.subjectiveTaskList
  173. .filter((item) => !item.markers.length)
  174. .map((item) => `${item.mainNumber}-${item.subNumber}`)
  175. .join(",");
  176. },
  177. },
  178. mounted() {
  179. this.initFillQuestionRanges();
  180. this.getPaperList();
  181. this.getCardPages();
  182. },
  183. methods: {
  184. ...mapMutations("markParam", [
  185. "setSubjectiveTaskList",
  186. "updateSubjectiveTaskItem",
  187. ]),
  188. async getPaperList() {
  189. this.paperList = [];
  190. const data = await examStructureFindJpg({
  191. examId: this.basicInfo.examId,
  192. courseId: this.basicInfo.courseId,
  193. paperNumber: this.basicInfo.paperNumber,
  194. serialNumber: this.basicInfo.serialNumber,
  195. });
  196. const papers = data || [];
  197. papers.sort((a, b) => a.index - b.index);
  198. this.paperList = papers.map((paper) => {
  199. return {
  200. imgUrl: paper.path,
  201. areas: [],
  202. };
  203. });
  204. },
  205. async getCardPages() {
  206. const detData = await cardDetail(this.basicInfo.cardId);
  207. const cardContent = JSON.parse(detData.content);
  208. this.cardPages = cardContent.pages;
  209. },
  210. initFillQuestionRanges() {
  211. if (!this.openMergeMarker) return;
  212. const qRangeDict = {};
  213. this.fillQuestionRanges = this.subjectiveTaskList.map((item, index) => {
  214. if (item.questionType !== 4) return;
  215. if (qRangeDict[item.mainNumber]) {
  216. qRangeDict[item.mainNumber].push(index);
  217. } else {
  218. qRangeDict[item.mainNumber] = [index];
  219. }
  220. });
  221. this.fillQuestionRanges = Object.values(qRangeDict);
  222. },
  223. objectSpanMethod({ rowIndex, columnIndex }) {
  224. // 第四列为评卷员
  225. if (columnIndex === 3 || columnIndex === 8) {
  226. const pos = this.fillQuestionRanges.findIndex((item) =>
  227. item.includes(rowIndex)
  228. );
  229. if (pos === -1) return;
  230. const range = this.fillQuestionRanges[pos];
  231. if (range[0] === rowIndex) {
  232. return {
  233. rowspan: range.length,
  234. colspan: 1,
  235. };
  236. } else {
  237. return {
  238. rowspan: 0,
  239. colspan: 0,
  240. };
  241. }
  242. }
  243. },
  244. getCurrentQuestions(row) {
  245. let curRowQuestions = [];
  246. if (this.openMergeMarker && row.questionType === 4) {
  247. // 填空题按大题批量处理
  248. const pos = this.subjectiveTaskList.findIndex(
  249. (item) => item.id === row.id
  250. );
  251. if (pos === -1) return;
  252. const range = this.fillQuestionRanges.find((item) =>
  253. item.includes(pos)
  254. );
  255. if (!range) return;
  256. curRowQuestions = range.map((index) => {
  257. const item = this.subjectiveTaskList[index];
  258. return {
  259. id: item.id,
  260. mainNumber: item.mainNumber,
  261. subNumber: item.subNumber,
  262. };
  263. });
  264. } else {
  265. // 非填空题按单题处理
  266. curRowQuestions = [
  267. {
  268. id: row.id,
  269. mainNumber: row.mainNumber,
  270. subNumber: row.subNumber,
  271. },
  272. ];
  273. }
  274. return curRowQuestions;
  275. },
  276. toSetMarker(row) {
  277. this.curRow = row;
  278. this.curRowQuestions = this.getCurrentQuestions(row);
  279. this.$refs.ModifyMarkMarker.open();
  280. },
  281. async markMarkerModified(row) {
  282. await markSubjectiveBindMarker({
  283. examId: this.basicInfo.examId,
  284. paperNumber: this.basicInfo.paperNumber,
  285. questionIds: this.curRowQuestions.map((item) => item.id),
  286. markers: row.markers,
  287. });
  288. this.curRowQuestions.forEach((item) => {
  289. this.updateSubjectiveTaskItem({
  290. id: item.id,
  291. markers: row.markers,
  292. });
  293. });
  294. },
  295. toSetArea(row) {
  296. this.curRow = row;
  297. this.curRowQuestions = this.getCurrentQuestions(row);
  298. this.$refs.ModifyMarkArea.open();
  299. },
  300. async areaModified(row) {
  301. await markSubjectiveUpdateMarkArea({
  302. examId: this.basicInfo.examId,
  303. paperNumber: this.basicInfo.paperNumber,
  304. questionIds: this.curRowQuestions.map((item) => item.id),
  305. pictureConfigs: row.pictureConfigs,
  306. });
  307. this.curRowQuestions.forEach((item) => {
  308. this.updateSubjectiveTaskItem({
  309. id: item.id,
  310. pictureConfigs: row.pictureConfigs,
  311. });
  312. });
  313. },
  314. async toDeleteMarker(row, marker) {
  315. const confirm = await this.$confirm(`确定要删除当前评卷员吗?`, "提示", {
  316. type: "warning",
  317. }).catch(() => {});
  318. if (confirm !== "confirm") return;
  319. const markers = row.markers.filter(
  320. (item) => item.markUserQuestionId !== marker.markUserQuestionId
  321. );
  322. await markSubjectiveUnbindMarker(marker.markUserQuestionId);
  323. this.updateSubjectiveTaskItem({
  324. id: row.id,
  325. markers,
  326. });
  327. },
  328. toSetMarkType(row) {
  329. this.curRow = row;
  330. this.$refs.ModifyMarkType.open();
  331. },
  332. async markTypeModified(row) {
  333. const pos = this.subjectiveTaskList.findIndex(
  334. (item) => item.id === row.id
  335. );
  336. if (pos === -1) return;
  337. const datas = {
  338. questionId: row.id,
  339. doubleEnable: row.doubleEnable,
  340. doubleRate: row.doubleRate,
  341. arbitrateThreshold: row.arbitrateThreshold,
  342. scorePolicy: row.scorePolicy,
  343. };
  344. await markSubjectiveUpdateMarkType(datas);
  345. this.updateSubjectiveTaskItem({
  346. ...datas,
  347. id: row.id,
  348. });
  349. },
  350. autoParsePictureConfigList(questions) {
  351. if (!questions.length) return [];
  352. let pictureConfigs = [];
  353. const structs = questions.map(
  354. (item) => `${item.mainNumber}_${item.subNumber}`
  355. );
  356. this.cardPages.forEach((page, pindex) => {
  357. page.exchange.answer_area.forEach((area) => {
  358. const [x, y, w, h] = area.area;
  359. const qStruct = `${area.main_number}_${area.sub_number}`;
  360. const pConfig = {
  361. i: pindex + 1,
  362. x,
  363. y,
  364. w,
  365. h,
  366. qStruct,
  367. };
  368. if (typeof area.sub_number === "number") {
  369. if (!structs.includes(qStruct)) return;
  370. pictureConfigs.push(pConfig);
  371. return;
  372. }
  373. // 复合区域处理,比如填空题,多个小题合并为一个区域
  374. if (typeof area.sub_number === "string") {
  375. const areaStructs = area.sub_number
  376. .split(",")
  377. .map((subNumber) => `${area.main_number}_${subNumber}`);
  378. if (
  379. structs.some((struct) => areaStructs.includes(struct)) &&
  380. !pictureConfigs.find((item) => item.qStruct === qStruct)
  381. ) {
  382. pictureConfigs.push(pConfig);
  383. }
  384. }
  385. });
  386. });
  387. pictureConfigs.forEach((item) => {
  388. delete item.qStruct;
  389. });
  390. // console.log(pictureConfigs);
  391. // 合并相邻区域
  392. pictureConfigs.sort((a, b) => {
  393. return a.i - b.i || a.x - b.x || a.y - b.y;
  394. });
  395. let combinePictureConfigList = [];
  396. let prevConfig = null;
  397. pictureConfigs.forEach((item, index) => {
  398. if (!index) {
  399. prevConfig = { ...item };
  400. combinePictureConfigList.push(prevConfig);
  401. return;
  402. }
  403. const elasticRate = 0.01;
  404. if (
  405. prevConfig.i === item.i &&
  406. prevConfig.y + prevConfig.h + elasticRate >= item.y &&
  407. prevConfig.w === item.w &&
  408. prevConfig.x === item.x
  409. ) {
  410. prevConfig.h = item.y + item.h - prevConfig.y;
  411. } else {
  412. prevConfig = { ...item };
  413. combinePictureConfigList.push(prevConfig);
  414. }
  415. });
  416. // console.log(combinePictureConfigList);
  417. return combinePictureConfigList;
  418. },
  419. checkData() {
  420. const errorList = [];
  421. this.subjectiveTaskList.forEach((item) => {
  422. const errors = [];
  423. if (!item.markers?.length) {
  424. errors.push("评卷员未设置");
  425. }
  426. if (!item.pictureConfigs?.length) {
  427. errors.push("评卷区未设置");
  428. }
  429. if (errors.length) {
  430. errorList.push(
  431. `${item.mainNumber}-${item.subNumber}:${errors.join("、")}。`
  432. );
  433. }
  434. });
  435. if (errorList.length) {
  436. this.$message.error(errorList.join("\n"));
  437. return false;
  438. }
  439. return true;
  440. },
  441. toPrev(step = 1) {
  442. this.$emit("prev", step);
  443. },
  444. toNext(step = 1) {
  445. if (!this.checkData()) return;
  446. this.$emit("next", step);
  447. },
  448. },
  449. };
  450. </script>