CheckAction.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. <template>
  2. <div class="check-action">
  3. <a-collapse
  4. v-model:activeKey="panelKey"
  5. :bordered="false"
  6. expandIconPosition="end"
  7. >
  8. <a-collapse-panel key="1">
  9. <template #header><FilterFilled />搜索条件 </template>
  10. <a-form :label-col="{ style: { width: '83px' } }">
  11. <a-form-item label="科目">
  12. <a-select
  13. v-model:value="searchModel.subjectCode"
  14. placeholder="请选择"
  15. :options="courses"
  16. :field-names="fieldNames"
  17. filter-option
  18. allow-clear
  19. style="width: 150px"
  20. ></a-select>
  21. </a-form-item>
  22. <a-form-item label="查找条件">
  23. <a-select
  24. v-model:value="searchDataCheckType"
  25. placeholder="请选择"
  26. :options="dataCheckOptions"
  27. allow-clear
  28. @change="dataCheckTypeChange"
  29. ></a-select>
  30. </a-form-item>
  31. </a-form>
  32. <div class="box-justify">
  33. <div></div>
  34. <div>
  35. <a-button class="m-r-8px" type="primary" @click="onSearch"
  36. >查询</a-button
  37. >
  38. <a-button @click="onExport('common')">导出</a-button>
  39. </div>
  40. </div>
  41. </a-collapse-panel>
  42. <a-collapse-panel key="2">
  43. <template #header><FilterFilled />搜索条件 (自定义)</template>
  44. <a-form :label-col="{ style: { width: '83px' } }">
  45. <a-form-item label="考号">
  46. <div class="exam-number">
  47. <a-textarea
  48. v-model:value="customSearchModel.examNumber"
  49. placeholder="请输入"
  50. :auto-size="{ minRows: 1, maxRows: 1 }"
  51. ></a-textarea>
  52. <div class="number-suffix">{{ examNumberCountCont }}</div>
  53. </div>
  54. <a-button
  55. class="ant-simple m-l-8px"
  56. type="link"
  57. @click="onEditExamNumber"
  58. >查看全部</a-button
  59. >
  60. </a-form-item>
  61. <a-form-item label="姓名">
  62. <a-input
  63. v-model:value="customSearchModel.name"
  64. placeholder="请输入"
  65. style="width: 150px"
  66. ></a-input>
  67. </a-form-item>
  68. <a-form-item label="科目">
  69. <a-select
  70. v-model:value="customSearchModel.subjectCode"
  71. placeholder="请选择"
  72. :options="courses"
  73. :field-names="fieldNames"
  74. filter-option
  75. allow-clear
  76. style="width: 150px"
  77. ></a-select>
  78. </a-form-item>
  79. <a-row>
  80. <a-col :span="12">
  81. <a-form-item label="客观题作答">
  82. <SelectBoolean
  83. v-model:value="customSearchModel.questionFilled"
  84. style="width: 85px"
  85. />
  86. </a-form-item>
  87. </a-col>
  88. <a-col :span="12">
  89. <a-form-item label="主观题作答">
  90. <SelectBoolean
  91. v-model:value="customSearchModel.subjectiveFilled"
  92. style="width: 100%"
  93. />
  94. </a-form-item>
  95. </a-col>
  96. <a-col :span="12">
  97. <a-form-item label="有作答">
  98. <SelectBoolean
  99. v-model:value="customSearchModel.hasFilled"
  100. style="width: 85px"
  101. />
  102. </a-form-item>
  103. </a-col>
  104. <a-col :span="12">
  105. <a-form-item label="试卷类型">
  106. <a-select
  107. v-model:value="customSearchModel.paperTypeStatus"
  108. placeholder="请选择"
  109. :options="paperTypeOptions"
  110. style="width: 100%"
  111. allow-clear
  112. ></a-select>
  113. </a-form-item>
  114. </a-col>
  115. </a-row>
  116. <a-row>
  117. <a-col :span="14">
  118. <a-form-item label="缺考">
  119. <a-select
  120. v-model:value="customSearchModel.examStatus"
  121. placeholder="请选择"
  122. :options="examStatusOptions"
  123. style="width: 85px"
  124. allow-clear
  125. ></a-select>
  126. </a-form-item>
  127. </a-col>
  128. <a-col :span="10">
  129. <a-form-item style="text-align: right">
  130. <a-button class="m-r-8px" type="primary" @click="onCustomSearch"
  131. >查询</a-button
  132. >
  133. <a-button @click="onExport('custom')">导出</a-button>
  134. </a-form-item>
  135. </a-col>
  136. </a-row>
  137. </a-form>
  138. </a-collapse-panel>
  139. <a-collapse-panel key="3">
  140. <template #header><PictureFilled />图片类别 </template>
  141. <a-radio-group v-model:value="imageType" @change="onImageTypeChange">
  142. <a-radio
  143. v-for="(item, index) in imageTypeOptions"
  144. :key="index"
  145. :value="item.value"
  146. >{{ item.label }}</a-radio
  147. >
  148. </a-radio-group>
  149. </a-collapse-panel>
  150. <a-collapse-panel key="4">
  151. <template #header><IdcardFilled />题卡信息 </template>
  152. <QuestionPanel
  153. v-if="dataCheckStore.curStudent && dataCheckStore.curPage"
  154. v-model:questions="questions"
  155. :info="questionInfo"
  156. :simple="isSliceImage"
  157. @change="onQuestionsChange"
  158. @exam-status-change="onExamStatusChange"
  159. />
  160. </a-collapse-panel>
  161. </a-collapse>
  162. </div>
  163. <!-- ExportTypeDialog -->
  164. <ExportTypeDialog ref="exportTypeDialogRef" @confirm="onExportConfirm" />
  165. <!-- MoreExamNumberDialog -->
  166. <MoreExamNumberDialog
  167. ref="moreExamNumberDialogRef"
  168. :data="customSearchModel.examNumber"
  169. @confirm="onExamNumberEdited"
  170. />
  171. </template>
  172. <script setup lang="ts">
  173. import { computed, reactive, ref, watch } from "vue";
  174. import { getSubjectList } from "@/ap/base";
  175. import {
  176. FilterFilled,
  177. PictureFilled,
  178. IdcardFilled,
  179. } from "@ant-design/icons-vue";
  180. import { message } from "ant-design-vue";
  181. import { useUserStore, useDataCheckStore } from "@/store";
  182. import {
  183. DataCheckListFilter,
  184. DataStatus,
  185. ExamStatus,
  186. } from "@/ap/types/dataCheck";
  187. import { SubjectItem } from "@/ap/types/base";
  188. import { ReviewExportType } from "@/ap/types/review";
  189. import { QuestionInfo } from "./types";
  190. import { dataCheckStudentExport, dataCheckRoomExport } from "@/ap/dataCheck";
  191. import { examStatusSave } from "@/ap/absentCheck";
  192. import useDictOption from "@/hooks/dictOption";
  193. import useLoading from "@/hooks/useLoading";
  194. import { ImageType } from "@/constants/enumerate";
  195. import { objModifyAssign } from "@/utils/tool";
  196. import ExportTypeDialog from "../Review/ExportTypeDialog.vue";
  197. import QuestionPanel from "./QuestionPanel.vue";
  198. import MoreExamNumberDialog from "./MoreExamNumberDialog.vue";
  199. import { getTextAreaRows } from "@/utils";
  200. defineOptions({
  201. name: "CheckAction",
  202. });
  203. const emit = defineEmits(["search"]);
  204. const userStore = useUserStore();
  205. const dataCheckStore = useDataCheckStore();
  206. const { optionList: dataCheckOptions } = useDictOption("DATA_CHECK_TYPE");
  207. const { optionList: paperTypeOptions } = useDictOption("PAPER_TYPE_STATUS");
  208. const { optionList: imageTypeOptions } = useDictOption("IMAGE_TYPE");
  209. const { optionList: examStatusOptions } = useDictOption("EXAM_STATUS");
  210. const panelKey = ref(["1", "2", "3", "4"]);
  211. // course data
  212. const courses = ref<SubjectItem[]>([]);
  213. async function getCourses() {
  214. const res = await getSubjectList({ examId: userStore.curExam.id });
  215. courses.value = (res || []).map((item: any) => {
  216. item.name = item.code + "-" + item.name;
  217. return item;
  218. });
  219. }
  220. getCourses();
  221. const fieldNames = { label: "name", value: "code" };
  222. // search
  223. const initSearchModel = {
  224. examId: userStore.curExam.id,
  225. status: "",
  226. examStatus: "",
  227. examNumber: "",
  228. studentCode: "",
  229. name: "",
  230. packageCode: "",
  231. campusCode: "",
  232. subjectCode: "",
  233. examSite: "",
  234. examRoom: "",
  235. province: "",
  236. paperTypeStatus: "",
  237. device: "",
  238. absentSuspect: null,
  239. omrAbsent: null,
  240. assigned: null,
  241. incomplete: null,
  242. questionFilled: null,
  243. subjectiveFilled: null,
  244. hasFilled: null,
  245. withOmrDetail: null,
  246. };
  247. const searchModel = reactive<DataCheckListFilter>({ ...initSearchModel });
  248. const customSearchModel = reactive<DataCheckListFilter>({ ...initSearchModel });
  249. const searchDataCheckType = ref();
  250. const imageType = ref(dataCheckStore.imageType);
  251. const actionType = ref("common");
  252. // imageType
  253. const isSliceImage = computed(() => {
  254. return dataCheckStore.imageType === "SLICE";
  255. });
  256. const examNumberCountCont = computed(() => {
  257. const examNumbers = (customSearchModel.examNumber || "")
  258. .split("\n")
  259. .filter((item) => item);
  260. return `${examNumbers.length}/100`;
  261. });
  262. const moreExamNumberDialogRef = ref();
  263. function onEditExamNumber() {
  264. moreExamNumberDialogRef.value?.open();
  265. }
  266. function onExamNumberEdited(val: string) {
  267. customSearchModel.examNumber = val;
  268. }
  269. const clearFieldsWithSelect = () => {
  270. searchModel.examStatus = "";
  271. searchModel.hasFilled = null;
  272. searchModel.questionFilled = null;
  273. searchModel.subjectiveFilled = null;
  274. searchModel.paperTypeStatus = "";
  275. };
  276. function dataCheckTypeChange() {
  277. //清空部分字段先
  278. clearFieldsWithSelect();
  279. switch (searchDataCheckType.value) {
  280. // 缺考有作答
  281. case "1":
  282. searchModel.examStatus = ["ABSENT"].join();
  283. searchModel.hasFilled = true;
  284. break;
  285. // 客观题无作答,主观题有作答;
  286. case "2":
  287. searchModel.questionFilled = false;
  288. searchModel.subjectiveFilled = true;
  289. break;
  290. // 不缺考,无条码,有作答;(正常或待审核考生,条码为空,有作答)
  291. case "3":
  292. searchModel.examStatus = [
  293. "OK",
  294. "UNCHECK1",
  295. "UNCHECK2",
  296. "UNCHECK3",
  297. ].join();
  298. searchModel.paperTypeStatus = "BLANK";
  299. searchModel.hasFilled = true;
  300. break;
  301. // 不缺考,无条码,无作答;(正常或待审核考生,条码为空,没有作答)
  302. case "4":
  303. searchModel.examStatus = [
  304. "OK",
  305. "UNCHECK1",
  306. "UNCHECK2",
  307. "UNCHECK3",
  308. ].join();
  309. searchModel.paperTypeStatus = "BLANK";
  310. searchModel.hasFilled = false;
  311. break;
  312. // 条码异常(识别的条码没与库中匹配上)
  313. case "5":
  314. searchModel.paperTypeStatus = "ERROR";
  315. break;
  316. // 缺考有条码
  317. case "6":
  318. searchModel.paperTypeStatus = "OK";
  319. searchModel.examStatus = ["ABSENT"].join();
  320. break;
  321. default:
  322. objModifyAssign(searchModel, {
  323. ...initSearchModel,
  324. subjectCode: searchModel.subjectCode,
  325. });
  326. break;
  327. }
  328. }
  329. function onSearch() {
  330. emit("search", searchModel);
  331. }
  332. function onCustomSearch() {
  333. // emit("search", customSearchModel);
  334. emit("search", {
  335. ...customSearchModel,
  336. examNumber: (customSearchModel.examNumber || "")
  337. .split("\n")
  338. .filter((item) => item)
  339. .join(","),
  340. });
  341. }
  342. function onImageTypeChange() {
  343. dataCheckStore.setInfo({
  344. imageType: imageType.value,
  345. });
  346. }
  347. // question panel
  348. const questionInfo = computed(() => {
  349. if (!dataCheckStore.curStudent) return {} as QuestionInfo;
  350. return {
  351. examNumber: dataCheckStore.curStudent.examNumber,
  352. name: dataCheckStore.curStudent.name,
  353. examSite: dataCheckStore.curStudent.examSite,
  354. seatNumber: dataCheckStore.curStudent.seatNumber,
  355. paperType: dataCheckStore.curStudent.paperType,
  356. examStatus: dataCheckStore.curStudent.examStatus as string,
  357. packageCode: dataCheckStore.curStudent.packageCode as string,
  358. };
  359. });
  360. const questions = ref([] as string[]);
  361. watch(
  362. () => dataCheckStore.curPage?.question?.result,
  363. (val) => {
  364. if (!val) {
  365. questions.value = [];
  366. return;
  367. }
  368. questions.value = [...val];
  369. },
  370. {
  371. deep: true,
  372. immediate: true,
  373. }
  374. );
  375. watch(
  376. () => dataCheckStore.imageType,
  377. (val) => {
  378. imageType.value = val;
  379. }
  380. );
  381. async function onQuestionsChange() {
  382. if (!dataCheckStore.curPage) return;
  383. dataCheckStore.curPage.question.result = [...questions.value];
  384. await dataCheckStore
  385. .updateField({
  386. field: "QUESTION",
  387. value: JSON.stringify(dataCheckStore.curPage.question),
  388. })
  389. .catch(() => {});
  390. }
  391. async function onExamStatusChange(val: ExamStatus) {
  392. if (!dataCheckStore.curStudent) return;
  393. await examStatusSave({ id: dataCheckStore.curStudent.id, examStatus: val });
  394. dataCheckStore.curStudent.examStatus = val;
  395. }
  396. // 导出
  397. const { loading: downloading, setLoading } = useLoading();
  398. const exportTypeDialogRef = ref();
  399. function onExport(type: string) {
  400. if (downloading.value) return;
  401. actionType.value = type;
  402. exportTypeDialogRef.value?.open();
  403. }
  404. async function onExportConfirm(type: ReviewExportType) {
  405. if (downloading.value) return;
  406. setLoading(true);
  407. const func =
  408. type === "STUDENT_CODE" ? dataCheckStudentExport : dataCheckRoomExport;
  409. const params =
  410. actionType.value === "common" ? searchModel : customSearchModel;
  411. const res = await func(params).catch((e: Error) => {
  412. message.error(e.message || "下载失败,请重新尝试!");
  413. });
  414. setLoading(false);
  415. if (!res) return;
  416. message.success("导出成功!");
  417. }
  418. watch(
  419. () => customSearchModel.examNumber,
  420. (val, oldVal) => {
  421. if (getTextAreaRows(val) > 100) {
  422. window.$message.error("最多只能输入100行");
  423. customSearchModel.examNumber = oldVal;
  424. }
  425. }
  426. );
  427. </script>