CheckAction.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  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. };
  191. const searchModel = reactive<AbsentCheckListFilter>({ ...initSearchModel });
  192. const imageType = ref(dataCheckStore.imageType);
  193. // imageType
  194. const isSliceImage = computed(() => {
  195. return dataCheckStore.imageType === "SLICE";
  196. });
  197. const examNumberCountCont = computed(() => {
  198. const examNumbers = (searchModel.examNumber || "")
  199. .split("\n")
  200. .filter((item) => item);
  201. return `${examNumbers.length}/100`;
  202. });
  203. const moreExamNumberDialogRef = ref();
  204. function onEditExamNumber() {
  205. moreExamNumberDialogRef.value?.open();
  206. }
  207. function onExamNumberEdited(val: string) {
  208. searchModel.examNumber = val;
  209. }
  210. function onSearch() {
  211. emit("search", {
  212. ...searchModel,
  213. examNumber: (searchModel.examNumber || "")
  214. .split("\n")
  215. .filter((item) => item)
  216. .join(","),
  217. });
  218. }
  219. function onImageTypeChange() {
  220. dataCheckStore.setInfo({
  221. imageType: imageType.value,
  222. });
  223. }
  224. // question panel
  225. const questionInfo = computed(() => {
  226. if (!dataCheckStore.curStudent) return {} as QuestionInfo;
  227. return {
  228. examNumber: dataCheckStore.curStudent.examNumber,
  229. name: dataCheckStore.curStudent.name,
  230. examSite: dataCheckStore.curStudent.examSite,
  231. seatNumber: dataCheckStore.curStudent.seatNumber,
  232. paperType: dataCheckStore.curStudent.paperType,
  233. examStatus: dataCheckStore.curStudent.examStatus as string,
  234. packageCode: dataCheckStore.curStudent.packageCode as string,
  235. };
  236. });
  237. const questions = ref([] as string[]);
  238. watch(
  239. () => dataCheckStore.curPage?.question?.result,
  240. (val) => {
  241. if (!val) {
  242. questions.value = [];
  243. return;
  244. }
  245. questions.value = [...val];
  246. },
  247. {
  248. deep: true,
  249. immediate: true,
  250. }
  251. );
  252. async function onQuestionsChange() {
  253. if (!dataCheckStore.curPage) return;
  254. dataCheckStore.curPage.question.result = [...questions.value];
  255. await dataCheckStore
  256. .updateField({
  257. field: "QUESTION",
  258. value: JSON.stringify(dataCheckStore.curPage.question),
  259. })
  260. .catch(() => {});
  261. }
  262. async function onExamStatusChange(val: ExamStatus) {
  263. if (!dataCheckStore.curStudent) return;
  264. await examStatusSave({ id: dataCheckStore.curStudent.id, examStatus: val });
  265. dataCheckStore.curStudent.examStatus = val;
  266. }
  267. // 导出
  268. const { loading: downloading, setLoading } = useLoading();
  269. const exportTypeDialogRef = ref();
  270. function onExport() {
  271. if (downloading.value) return;
  272. if (!searchModel.subjectCode) {
  273. message.error("请选择科目");
  274. return;
  275. }
  276. if (!searchModel.examStatus || searchModel.examStatus === "OK") {
  277. message.error("请选择异常条件");
  278. return;
  279. }
  280. exportTypeDialogRef.value?.open();
  281. }
  282. async function onExportConfirm(type: ReviewExportType) {
  283. if (downloading.value) return;
  284. setLoading(true);
  285. const func =
  286. type === "STUDENT_CODE" ? absentCheckStudentExport : absentCheckRoomExport;
  287. const res = await func(searchModel).catch((e: Error) => {
  288. message.error(e.message || "下载失败,请重新尝试!");
  289. });
  290. setLoading(false);
  291. if (!res) return;
  292. message.success("导出成功!");
  293. }
  294. // 导入
  295. const importTypeDialogRef = ref();
  296. function onImport() {
  297. importTypeDialogRef.value?.open();
  298. }
  299. // 重置
  300. const resetDialogRef = ref();
  301. function onReset() {
  302. resetDialogRef.value?.open();
  303. }
  304. watch(
  305. () => searchModel.examNumber,
  306. (val, oldVal) => {
  307. if (getTextAreaRows(val) > 100) {
  308. window.$message.error("最多只能输入100行");
  309. searchModel.examNumber = oldVal;
  310. }
  311. if (val.includes(",") || val.includes(",")) {
  312. let arr = val.split(/[,,]/).filter(Boolean);
  313. if (arr.length > 100) {
  314. window.$message.error("输入的考号超过了100个,已自动截取前100条");
  315. arr = arr.slice(0, 100);
  316. }
  317. searchModel.examNumber = arr.join("\n");
  318. }
  319. }
  320. );
  321. </script>