QuestionPanel.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <template>
  2. <div class="question-panel">
  3. <a-descriptions v-if="info" class="panel-info" :column="10">
  4. <a-descriptions-item label="考号" :span="6">
  5. {{ info.examNumber }}
  6. </a-descriptions-item>
  7. <a-descriptions-item label="姓名" :span="4">
  8. {{ info.name }}
  9. </a-descriptions-item>
  10. <a-descriptions-item label="卷袋号" :span="6">
  11. {{ info.packageCode }}
  12. </a-descriptions-item>
  13. <a-descriptions-item label="考点" :span="4">
  14. {{ info.examSite }}
  15. </a-descriptions-item>
  16. <a-descriptions-item label="卷型号" :span="6">
  17. <template v-if="simple">
  18. {{ paperTypeDisplay }}
  19. </template>
  20. <template v-else>
  21. <a-button class="ant-gray m-r-4px">{{ paperTypeDisplay }}</a-button>
  22. <a-button v-if="paperTypeArea && editable" @click="onEditPaperType">
  23. <template #icon><SwapOutlined /></template>
  24. </a-button>
  25. </template>
  26. </a-descriptions-item>
  27. <a-descriptions-item v-if="!simple && editable" label="缺考" :span="4">
  28. <a-radio-group
  29. v-model:value="examStatus"
  30. name="examStatus"
  31. @change="onExamStatusChange"
  32. >
  33. <a-radio
  34. v-for="(item, index) in examStatusOptions"
  35. :key="index"
  36. :value="item.value"
  37. >{{ item.label }}</a-radio
  38. >
  39. </a-radio-group>
  40. </a-descriptions-item>
  41. </a-descriptions>
  42. <div
  43. v-if="!simple && dataCheckStore.curPage?.question"
  44. ref="panelBodyRef"
  45. class="panel-body"
  46. >
  47. <div class="panel-body-title">
  48. <h4>客观题</h4>
  49. <p>多于一个填涂显示>号,未填涂显示#号</p>
  50. </div>
  51. <div
  52. v-for="(item, index) in questionList"
  53. :key="index"
  54. :class="['question-item', `question-item-${index}`]"
  55. >
  56. <span>{{ getQuestionNo(index) }}:</span>
  57. <a-button
  58. v-if="editable || item.length <= 1"
  59. :class="['ant-gray', { 'is-active': curQuestionIndex === index }]"
  60. :disabled="!editable"
  61. @click="onEditQuestion(index)"
  62. >{{ getQuesionCont(item) }}</a-button
  63. >
  64. <a-tooltip v-else placement="top">
  65. <template #title>
  66. <span>{{ item.split("").join(",") }}</span>
  67. </template>
  68. <a-button disabled>{{ getQuesionCont(item) }}</a-button>
  69. </a-tooltip>
  70. </div>
  71. <div
  72. v-if="quesionEditShow && editable"
  73. class="queston-edit"
  74. :style="quesionEditStyle"
  75. v-ele-click-outside-directive="hideEditQuestion"
  76. @keydown.stop
  77. @keyup.enter="onSaveQuesion"
  78. >
  79. <a-input
  80. v-model:value="curQuestion"
  81. style="width: 64px"
  82. @change="toUpperCase"
  83. ></a-input>
  84. <a-button class="ant-simple m-l-8px" type="link" @click="onSaveQuesion"
  85. >保存(Enter)</a-button
  86. >
  87. </div>
  88. </div>
  89. </div>
  90. <ModifyPaperType
  91. ref="modifyPaperTypeRef"
  92. :area-img="paperTypeImg"
  93. :area-result="paperTypeResult"
  94. :ocr-result="paperTypeOrcResult"
  95. @confirm="paperTypeModified"
  96. />
  97. </template>
  98. <script setup lang="ts" name="QuestionPanel">
  99. import { computed, ref, watch, onMounted } from "vue";
  100. import { message } from "ant-design-vue";
  101. import { SwapOutlined } from "@ant-design/icons-vue";
  102. import { QuestionInfo } from "./types";
  103. import { parseRecogData } from "@/utils/recog/recog";
  104. import useDictOption from "@/hooks/dictOption";
  105. import ModifyPaperType from "./ModifyPaperType.vue";
  106. import { useDataCheckStore } from "@/store";
  107. import { vEleClickOutsideDirective } from "@/directives/eleClickOutside";
  108. import { getSliceFileUrl } from "@/utils/tool";
  109. // defineOptions({
  110. // name: "QuestionPanel",
  111. // });
  112. const props = withDefaults(
  113. defineProps<{
  114. questions: string[];
  115. info: QuestionInfo;
  116. simple: boolean;
  117. editable?: boolean;
  118. }>(),
  119. {
  120. questions: () => [],
  121. simple: false,
  122. editable: true,
  123. }
  124. );
  125. const emit = defineEmits(["update:questions", "change", "examStatusChange"]);
  126. const dataCheckStore = useDataCheckStore();
  127. const { optionList: examStatusOptions } = useDictOption("EXAM_SIMPLE_STATUS");
  128. const examStatus = ref("");
  129. const questionList = ref([] as string[]);
  130. const curQuestion = ref("");
  131. const curQuestionIndex = ref(-1);
  132. const toUpperCase = () => {
  133. if ((curQuestion.value || "").includes("#")) {
  134. if (curQuestion.value.length > 1) {
  135. curQuestion.value = "#";
  136. }
  137. } else {
  138. curQuestion.value = curQuestion.value.replace(/[^a-zA-Z]/g, "");
  139. }
  140. curQuestion.value = curQuestion.value.toUpperCase();
  141. };
  142. function onExamStatusChange() {
  143. emit("examStatusChange", examStatus.value);
  144. }
  145. function getQuestionNo(index: number) {
  146. const no = index + 1;
  147. return no < 10 ? `0${no}` : `${no}`;
  148. }
  149. function getQuesionCont(cont: string) {
  150. if (!cont) return "#";
  151. if (cont.length > 1) return ">";
  152. return cont;
  153. }
  154. // question edit
  155. const quesionEditShow = ref(false);
  156. const quesionEditPos = ref({
  157. left: 0,
  158. top: 0,
  159. });
  160. const quesionEditStyle = computed(() => {
  161. return {
  162. top: `${quesionEditPos.value.top}px`,
  163. left: `${quesionEditPos.value.left}px`,
  164. };
  165. });
  166. const panelBodyRef = ref();
  167. function onEditQuestion(index: number) {
  168. curQuestionIndex.value = index;
  169. const qcont = questionList.value[curQuestionIndex.value];
  170. curQuestion.value = qcont;
  171. quesionEditShow.value = true;
  172. updateQuestionEditPos(index);
  173. }
  174. function updateQuestionEditPos(index: number) {
  175. const panelBodyDom = panelBodyRef.value as HTMLDivElement;
  176. const itemDom = panelBodyDom.querySelector(
  177. `.question-item-${index}`
  178. ) as HTMLDivElement;
  179. let left = itemDom.offsetLeft + 30;
  180. left = Math.min(panelBodyDom.clientWidth - 165, left);
  181. quesionEditPos.value.left = left;
  182. quesionEditPos.value.top = itemDom.offsetTop - 54;
  183. }
  184. function hideEditQuestion() {
  185. quesionEditShow.value = false;
  186. curQuestionIndex.value = -1;
  187. }
  188. function onSaveQuesion() {
  189. if (!quesionEditShow.value) return;
  190. if (!curQuestion.value) {
  191. message.error("请输入答案!");
  192. return;
  193. }
  194. const questionCont = curQuestion.value;
  195. if (!questionCont) {
  196. message.error("请输入答案!");
  197. return;
  198. }
  199. questionList.value[curQuestionIndex.value] = questionCont;
  200. quesionEditShow.value = false;
  201. emit("update:questions", questionList.value);
  202. emit("change", questionList.value);
  203. }
  204. // edit paper
  205. const modifyPaperTypeRef = ref();
  206. const paperTypeArea = ref<AreaSize | null>(null);
  207. const paperTypeImg = ref("");
  208. const paperTypeResult = ref("");
  209. const paperTypeOrcResult = ref("");
  210. const paperTypeType = ref("");
  211. const paperTypeDisplay = computed(() => {
  212. if (paperTypeResult.value === "#") return "空";
  213. if (paperTypeResult.value === "?") return "异常";
  214. return paperTypeResult.value;
  215. });
  216. function onEditPaperType() {
  217. if (!dataCheckStore.curPage) return;
  218. modifyPaperTypeRef.value?.open();
  219. }
  220. const curPage = computed(() => dataCheckStore.curPage);
  221. async function paperTypeModified(paperType: string) {
  222. if (!dataCheckStore.curStudent) return;
  223. await dataCheckStore.updateField({
  224. field: "PAPER_TYPE",
  225. value: JSON.stringify({
  226. type: paperTypeType.value,
  227. result: paperType || "#",
  228. }),
  229. });
  230. dataCheckStore.modifyPaperType(paperType);
  231. paperTypeResult.value = paperType;
  232. }
  233. watch(
  234. () => dataCheckStore.curStudent,
  235. async (val) => {
  236. paperTypeArea.value = null;
  237. if (!val) return;
  238. // 只取第一张第一页的数据
  239. const curStudentFirstPage = val.papers[0]?.pages[0];
  240. if (!curStudentFirstPage) return;
  241. paperTypeResult.value = curStudentFirstPage.paperType.result;
  242. paperTypeType.value = curStudentFirstPage.paperType.type;
  243. const recogData = curStudentFirstPage.recogData;
  244. const regdata = parseRecogData(recogData);
  245. if (!regdata) return;
  246. // console.log(regdata);
  247. paperTypeOrcResult.value = regdata.paperType.content;
  248. if (!paperTypeOrcResult.value) paperTypeOrcResult.value = "空";
  249. const rect = regdata.paperType.rect || null;
  250. if (!rect) {
  251. paperTypeArea.value = null;
  252. paperTypeImg.value = "";
  253. return;
  254. }
  255. const hasArea = rect.some((item) => item);
  256. paperTypeArea.value = hasArea
  257. ? {
  258. x: rect[0],
  259. y: rect[1],
  260. w: rect[2],
  261. h: rect[3],
  262. }
  263. : null;
  264. if (paperTypeArea.value) {
  265. paperTypeImg.value = await getSliceFileUrl(
  266. curStudentFirstPage.sheetUri,
  267. paperTypeArea.value
  268. );
  269. } else {
  270. paperTypeImg.value = "";
  271. }
  272. },
  273. {
  274. immediate: true,
  275. }
  276. );
  277. watch(
  278. () => props.questions,
  279. (val) => {
  280. if (!val) return;
  281. questionList.value = [...val];
  282. },
  283. {
  284. immediate: true,
  285. }
  286. );
  287. watch(
  288. () => props.info,
  289. (val) => {
  290. examStatus.value = val.examStatus;
  291. }
  292. );
  293. onMounted(() => {
  294. if (props.info?.examStatus) {
  295. examStatus.value = props.info?.examStatus;
  296. }
  297. });
  298. </script>
  299. <style lang="less" scoped>
  300. .question-panel {
  301. .panel-body {
  302. margin: 15px -14px 0;
  303. padding: 0 14px;
  304. border-top: 1px solid @border-color1;
  305. position: relative;
  306. font-size: 0;
  307. &-title {
  308. padding: 12px 0 8px;
  309. display: flex;
  310. justify-content: space-between;
  311. align-items: center;
  312. font-size: 14px;
  313. > p {
  314. color: @text-color3;
  315. }
  316. }
  317. .question-item {
  318. display: inline-block;
  319. vertical-align: middle;
  320. width: 20%;
  321. font-size: 14px;
  322. margin-bottom: 12px;
  323. > span {
  324. display: inline-block;
  325. width: 30px;
  326. padding-right: 4px;
  327. text-align: right;
  328. font-size: 13px;
  329. }
  330. .ant-btn {
  331. padding-left: 10px;
  332. padding-right: 10px;
  333. &.is-active {
  334. border-color: @brand-color;
  335. }
  336. }
  337. }
  338. .queston-edit {
  339. position: absolute;
  340. width: 165px;
  341. background: #f2f3f5;
  342. box-shadow: 0px 4px 8px 0px rgba(54, 61, 89, 0.2);
  343. border-radius: 8px;
  344. padding: 8px;
  345. border: 1px solid @border-color1;
  346. z-index: 9;
  347. }
  348. }
  349. :deep(.ant-descriptions-row) {
  350. .ant-descriptions-item {
  351. padding-bottom: 8px;
  352. .ant-descriptions-item-label {
  353. display: block;
  354. color: @text-color2;
  355. }
  356. .ant-descriptions-item-label::after {
  357. margin-inline: 2px 4px;
  358. }
  359. &:first-child {
  360. .ant-descriptions-item-label {
  361. width: 66px;
  362. text-align: right;
  363. }
  364. }
  365. }
  366. .ant-descriptions-item-content {
  367. align-items: center;
  368. }
  369. .ant-radio-wrapper span.ant-radio + * {
  370. padding-inline-start: 4px;
  371. padding-inline-end: 4px;
  372. }
  373. }
  374. :deep(.ant-descriptions-row:nth-of-type(2)) {
  375. .ant-descriptions-item {
  376. padding-bottom: 4px;
  377. }
  378. }
  379. :deep(.ant-descriptions-row:last-child) {
  380. .ant-descriptions-item {
  381. padding-bottom: 0;
  382. .ant-descriptions-item-container {
  383. align-items: center;
  384. }
  385. .ant-descriptions-item-label {
  386. line-height: 28px;
  387. }
  388. .ant-btn {
  389. padding: 2px 6px;
  390. height: 28px;
  391. min-width: 28px;
  392. }
  393. }
  394. }
  395. }
  396. </style>