CheckAction.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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: '76px' } }">
  11. <a-form-item label="科目">
  12. <select-course
  13. v-model:value="searchModel.subjectCode"
  14. :exam-id="userStore.curExam.id"
  15. style="width: 150px"
  16. allow-clear
  17. ></select-course>
  18. </a-form-item>
  19. <a-form-item label="查找条件">
  20. <a-select
  21. v-model:value="searchModel.examStatus"
  22. placeholder="请选择"
  23. :options="absentCheckOptions"
  24. allow-clear
  25. ></a-select>
  26. </a-form-item>
  27. <a-form-item label="考号">
  28. <div class="exam-number">
  29. <a-textarea
  30. v-model:value="searchModel.examNumber"
  31. placeholder="请输入"
  32. :auto-size="{ minRows: 1, maxRows: 1 }"
  33. ></a-textarea>
  34. <div class="number-suffix">{{ examNumberCountCont }}</div>
  35. </div>
  36. <a-button
  37. class="ant-simple m-l-8px"
  38. type="link"
  39. @click="onEditExamNumber"
  40. >查看全部</a-button
  41. >
  42. </a-form-item>
  43. <a-row>
  44. <a-col :span="15">
  45. <a-form-item label="姓名">
  46. <a-input
  47. v-model:value="searchModel.name"
  48. placeholder="请输入"
  49. style="width: 140px"
  50. ></a-input>
  51. </a-form-item>
  52. </a-col>
  53. <a-col :span="9">
  54. <a-form-item style="text-align: right">
  55. <a-button class="m-r-8px" type="primary" @click="onSearch"
  56. >查询</a-button
  57. >
  58. <a-button @click="onExport">导出</a-button>
  59. </a-form-item>
  60. </a-col>
  61. </a-row>
  62. </a-form>
  63. </a-collapse-panel>
  64. <a-collapse-panel key="2">
  65. <template #header><PictureFilled />图片类别 </template>
  66. <a-radio-group v-model:value="imageType" @change="onImageTypeChange">
  67. <a-radio
  68. v-for="(item, index) in imageTypeOptions"
  69. :key="index"
  70. :value="item.value"
  71. >{{ item.label }}</a-radio
  72. >
  73. </a-radio-group>
  74. </a-collapse-panel>
  75. <a-collapse-panel key="3">
  76. <template #header><IdcardFilled />题卡信息 </template>
  77. <QuestionPanel
  78. v-if="dataCheckStore.curStudent && dataCheckStore.curPage"
  79. v-model:questions="questions"
  80. :info="questionInfo"
  81. :simple="isSliceImage"
  82. @change="onQuestionsChange"
  83. @exam-status-change="onExamStatusChange"
  84. />
  85. </a-collapse-panel>
  86. <a-collapse-panel key="4">
  87. <template #header><SettingFilled />缺考数据设置 </template>
  88. <a-button
  89. class="m-r-10px"
  90. type="primary"
  91. :rotate="90"
  92. @click="onImport"
  93. >
  94. <template #icon><SelectOutlined /></template>导入缺考名单
  95. </a-button>
  96. <a-button @click="onReset">
  97. <template #icon><RedoOutlined :rotate="270" /></template
  98. >重新生成缺考数据
  99. </a-button>
  100. </a-collapse-panel>
  101. </a-collapse>
  102. </div>
  103. <!-- ExportTypeDialog -->
  104. <ExportTypeDialog ref="exportTypeDialogRef" @confirm="onExportConfirm" />
  105. <!-- ResetDialog -->
  106. <ResetDialog ref="resetDialogRef" />
  107. <!-- ImportTypeDialog -->
  108. <ImportTypeDialog ref="importTypeDialogRef" />
  109. <!-- MoreExamNumberDialog -->
  110. <MoreExamNumberDialog
  111. ref="moreExamNumberDialogRef"
  112. :data="searchModel.examNumber"
  113. @confirm="onExamNumberEdited"
  114. />
  115. </template>
  116. <script setup lang="ts">
  117. import { computed, reactive, ref, watch } from "vue";
  118. import {
  119. FilterFilled,
  120. PictureFilled,
  121. IdcardFilled,
  122. SettingFilled,
  123. SelectOutlined,
  124. RedoOutlined,
  125. } from "@ant-design/icons-vue";
  126. import { message } from "ant-design-vue";
  127. import type { DefaultOptionType } from "ant-design-vue/es/vc-select/Select";
  128. import { useUserStore, useDataCheckStore } from "@/store";
  129. import { AbsentCheckListFilter, ImportType } from "@/ap/types/absentCheck";
  130. import { ExamStatus } from "@/ap/types/dataCheck";
  131. import { ReviewExportType } from "@/ap/types/review";
  132. import {
  133. absentCheckStudentExport,
  134. absentCheckRoomExport,
  135. examStatusSave,
  136. } from "@/ap/absentCheck";
  137. import { examNumberFillCount } from "@/ap/base";
  138. import useDictOption from "@/hooks/dictOption";
  139. import useLoading from "@/hooks/useLoading";
  140. import ExportTypeDialog from "../Review/ExportTypeDialog.vue";
  141. import ResetDialog from "./ResetDialog.vue";
  142. import ImportTypeDialog from "./ImportTypeDialog.vue";
  143. import QuestionPanel from "../DataCheck/QuestionPanel.vue";
  144. import MoreExamNumberDialog from "../DataCheck/MoreExamNumberDialog.vue";
  145. import { QuestionInfo } from "../DataCheck/types";
  146. import { getTextAreaRows } from "@/utils";
  147. defineOptions({
  148. name: "CheckAction",
  149. });
  150. const emit = defineEmits(["search"]);
  151. const userStore = useUserStore();
  152. const dataCheckStore = useDataCheckStore();
  153. const { optionList: imageTypeOptions } = useDictOption("IMAGE_TYPE");
  154. const panelKey = ref(["1", "2", "3", "4"]);
  155. const absentCheckOptions = ref([] as DefaultOptionType[]);
  156. async function getAbsentCheckOptions() {
  157. const res = await examNumberFillCount(userStore.curExam.id);
  158. absentCheckOptions.value = [
  159. {
  160. label: "缺考",
  161. value: "ABSENT",
  162. },
  163. {
  164. label: "正常",
  165. value: "OK",
  166. },
  167. {
  168. label: "客观题未作答+主观题有作答(待确认)",
  169. value: "UNCHECK1",
  170. },
  171. {
  172. label: `客观题未作答+主观题未作答+填涂≥${res}+有卷型(待确认)`,
  173. value: "UNCHECK2",
  174. },
  175. {
  176. label: `交叉对比缺考不一致(待确认)`,
  177. value: "UNCHECK3",
  178. },
  179. ];
  180. }
  181. getAbsentCheckOptions();
  182. // search
  183. const initSearchModel = {
  184. examId: userStore.curExam.id,
  185. examNumber: "",
  186. studentCode: "",
  187. name: "",
  188. subjectCode: "",
  189. examStatus: "",
  190. status: "SCANNED",
  191. };
  192. const searchModel = reactive<AbsentCheckListFilter>({ ...initSearchModel });
  193. const imageType = ref(dataCheckStore.imageType);
  194. // imageType
  195. const isSliceImage = computed(() => {
  196. return dataCheckStore.imageType === "SLICE";
  197. });
  198. const examNumberCountCont = computed(() => {
  199. const examNumbers = (searchModel.examNumber || "")
  200. .split("\n")
  201. .filter((item) => item);
  202. return `${examNumbers.length}/100`;
  203. });
  204. const moreExamNumberDialogRef = ref();
  205. function onEditExamNumber() {
  206. moreExamNumberDialogRef.value?.open();
  207. }
  208. function onExamNumberEdited(val: string) {
  209. searchModel.examNumber = val;
  210. }
  211. function onSearch() {
  212. emit("search", {
  213. ...searchModel,
  214. examNumber: (searchModel.examNumber || "")
  215. .split("\n")
  216. .filter((item) => item)
  217. .join(","),
  218. });
  219. }
  220. function onImageTypeChange() {
  221. dataCheckStore.setInfo({
  222. imageType: imageType.value,
  223. });
  224. }
  225. // question panel
  226. const questionInfo = computed(() => {
  227. if (!dataCheckStore.curStudent) return {} as QuestionInfo;
  228. return {
  229. examNumber: dataCheckStore.curStudent.examNumber,
  230. name: dataCheckStore.curStudent.name,
  231. examSite: dataCheckStore.curStudent.examSite,
  232. seatNumber: dataCheckStore.curStudent.seatNumber,
  233. paperType: dataCheckStore.curStudent.paperType,
  234. examStatus: dataCheckStore.curStudent.examStatus as string,
  235. packageCode: dataCheckStore.curStudent.packageCode as string,
  236. };
  237. });
  238. const questions = ref([] as string[]);
  239. watch(
  240. () => dataCheckStore.curPage?.question?.result,
  241. (val) => {
  242. if (!val) {
  243. questions.value = [];
  244. return;
  245. }
  246. questions.value = [...val];
  247. },
  248. {
  249. deep: true,
  250. immediate: true,
  251. }
  252. );
  253. async function onQuestionsChange() {
  254. if (!dataCheckStore.curPage) return;
  255. dataCheckStore.curPage.question.result = [...questions.value];
  256. await dataCheckStore
  257. .updateField({
  258. field: "QUESTION",
  259. value: JSON.stringify(dataCheckStore.curPage.question),
  260. })
  261. .catch(() => {});
  262. }
  263. async function onExamStatusChange(val: ExamStatus) {
  264. if (!dataCheckStore.curStudent) return;
  265. await examStatusSave({ id: dataCheckStore.curStudent.id, examStatus: val });
  266. dataCheckStore.curStudent.examStatus = val;
  267. }
  268. // 导出
  269. const { loading: downloading, setLoading } = useLoading();
  270. const exportTypeDialogRef = ref();
  271. function onExport() {
  272. if (downloading.value) return;
  273. if (!searchModel.subjectCode) {
  274. message.error("请选择科目");
  275. return;
  276. }
  277. if (!searchModel.examStatus || searchModel.examStatus === "OK") {
  278. message.error("请选择异常条件");
  279. return;
  280. }
  281. exportTypeDialogRef.value?.open();
  282. }
  283. async function onExportConfirm(type: ReviewExportType) {
  284. if (downloading.value) return;
  285. setLoading(true);
  286. const func =
  287. type === "STUDENT_CODE" ? absentCheckStudentExport : absentCheckRoomExport;
  288. const res = await func(searchModel).catch((e: Error) => {
  289. message.error(e.message || "下载失败,请重新尝试!");
  290. });
  291. setLoading(false);
  292. if (!res) return;
  293. message.success("导出成功!");
  294. }
  295. // 导入
  296. const importTypeDialogRef = ref();
  297. function onImport() {
  298. importTypeDialogRef.value?.open();
  299. }
  300. // 重置
  301. const resetDialogRef = ref();
  302. function onReset() {
  303. resetDialogRef.value?.open();
  304. }
  305. watch(
  306. () => searchModel.examNumber,
  307. (val, oldVal) => {
  308. if (getTextAreaRows(val) > 100) {
  309. window.$message.error("最多只能输入100行");
  310. searchModel.examNumber = oldVal;
  311. }
  312. if (val.includes(",") || val.includes(",")) {
  313. let arr = val.split(/[,,]/).filter(Boolean);
  314. if (arr.length > 100) {
  315. window.$message.error("输入的考号超过了100个,已自动截取前100条");
  316. arr = arr.slice(0, 100);
  317. }
  318. searchModel.examNumber = arr.join("\n");
  319. }
  320. }
  321. );
  322. </script>