index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. <template>
  2. <div class="subjects-manage">
  3. <Block class="header-block tw-flex tw-items-center">
  4. <a-form layout="inline" class="tw-flex-1">
  5. <a-form-item label="学校名称">
  6. <a-select
  7. v-model:value="query.schoolId"
  8. show-search
  9. :filterOption="false"
  10. @search="(name:string) => querySchoolList(name, 'list')"
  11. placeholder="学校名称"
  12. >
  13. <a-select-option
  14. v-for="school in schoolTableData.result"
  15. :key="school.id"
  16. :value="school.id"
  17. >{{ school.name }}</a-select-option
  18. >
  19. </a-select>
  20. </a-form-item>
  21. <a-form-item label="考试批次">
  22. <a-select
  23. v-model:value="query.examId"
  24. :filterOption="false"
  25. show-search
  26. @search="(name:string) => queryExamList(name, 'list')"
  27. placeholder="考试批次"
  28. >
  29. <a-select-option value="">全部</a-select-option>
  30. <a-select-option
  31. v-for="exam in examTableData.result"
  32. :key="exam.id"
  33. :value="`${exam.id}`"
  34. >{{ exam.name }}</a-select-option
  35. >
  36. </a-select>
  37. </a-form-item>
  38. <a-form-item label="分组状态">
  39. <a-select v-model:value="query.groupFinish" placeholder="考试批次">
  40. <a-select-option :value="0">全部</a-select-option>
  41. <a-select-option :value="1">已完成</a-select-option>
  42. <a-select-option :value="2">未完成</a-select-option>
  43. </a-select>
  44. </a-form-item>
  45. <a-form-item label="科目代码">
  46. <a-input
  47. v-model:value="query.courseCode"
  48. placeholder="科目代码"
  49. maxlength="50"
  50. ></a-input>
  51. </a-form-item>
  52. <a-form-item label="科目名称">
  53. <a-input
  54. v-model:value="query.courseName"
  55. placeholder="科目名称"
  56. maxlength="50"
  57. ></a-input>
  58. </a-form-item>
  59. <a-form-item label="试卷总分" class="range-item">
  60. <a-input v-model:value="query.totalScoreMin"></a-input>
  61. <a-input v-model:value="query.totalScoreMax"></a-input>
  62. </a-form-item>
  63. <a-form-item>
  64. <a-button
  65. class="search-button"
  66. type="primary"
  67. @click="querySubjectsList"
  68. >查询</a-button
  69. >
  70. </a-form-item>
  71. </a-form>
  72. <a-dropdown-button type="primary">
  73. <template #overlay>
  74. <a-menu>
  75. <a-menu-item key="1" @click="showImportModalType('subject')"
  76. >导入科目</a-menu-item
  77. >
  78. <a-menu-item key="2" @click="showImportModalType('struct')"
  79. >导入主观题</a-menu-item
  80. >
  81. </a-menu>
  82. </template>
  83. <template #icon><DownOutlined /></template>
  84. 导入
  85. </a-dropdown-button>
  86. <a-button
  87. type="primary"
  88. class="tw-flex tw-items-center operation-button"
  89. @click="downloadPaperStruct"
  90. >
  91. <template #icon>
  92. <UploadOutlined />
  93. </template>
  94. 导出
  95. </a-button>
  96. </Block>
  97. <Block class="subjects-table">
  98. <a-table
  99. :columns="columns"
  100. :data-source="subjectsTableData.result"
  101. :pagination="{
  102. total: subjectsTableData.totalCount,
  103. pageSize: query.pageSize,
  104. }"
  105. @change="currentPageChange"
  106. :row-class-name="
  107. (_:any, index:number) => (index % 2 === 1 ? 'table-striped' : null)
  108. "
  109. >
  110. <template #bodyCell="{ column, record, index, text }">
  111. <template v-if="column.dataIndex === 'index'">
  112. {{ index + 1 }}
  113. </template>
  114. <template v-else-if="column.dataIndex === 'courseName'">
  115. {{ text }}
  116. <a-tooltip placement="right">
  117. <template #title>
  118. <span>主观分未完成分组</span>
  119. </template>
  120. <img
  121. v-if="!record.groupFinish"
  122. class="star-icon"
  123. src="@imgs/common/star-icon.png"
  124. />
  125. </a-tooltip>
  126. </template>
  127. <template v-else-if="column.dataIndex === 'enable'">
  128. <template v-if="record.enable">
  129. <CheckCircleFilled style="color: #30bf78" />
  130. </template>
  131. <template v-else>
  132. <CloseCircleFilled style="color: #f4664a" />
  133. </template>
  134. </template>
  135. </template>
  136. </a-table>
  137. </Block>
  138. <a-modal
  139. v-model:visible="showImportModal"
  140. :maskClosable="false"
  141. :title="`导入${uploadQuery.type === 'subject' ? '科目' : '主观题'}`"
  142. okText="确认上传"
  143. cancelText="取消"
  144. @ok="onImport"
  145. :afterClose="resetFields"
  146. >
  147. <a-form :labelCol="{ span: 6 }">
  148. <a-form-item label="学校名称" v-bind="validateInfos.schoolId">
  149. <a-select
  150. v-model:value="uploadQuery.schoolId"
  151. show-search
  152. :filterOption="false"
  153. @search="(name: string) => querySchoolList(name,'form')"
  154. placeholder="学校名称"
  155. >
  156. <a-select-option
  157. v-for="school in uploadQuery.schoolTableData.result"
  158. :key="school.id"
  159. :value="school.id"
  160. >{{ school.name }}</a-select-option
  161. >
  162. </a-select>
  163. </a-form-item>
  164. <a-form-item label="考试批次" v-bind="validateInfos.examId">
  165. <a-select
  166. v-model:value="uploadQuery.examId"
  167. :filterOption="false"
  168. show-search
  169. @search="(name:string) => queryExamList(name, 'form')"
  170. placeholder="考试批次"
  171. >
  172. <a-select-option
  173. v-for="exam in uploadQuery.examTableData.result"
  174. :key="exam.id"
  175. :value="`${exam.id}`"
  176. >{{ exam.name }}</a-select-option
  177. >
  178. </a-select>
  179. </a-form-item>
  180. <a-form-item
  181. :label="`${
  182. uploadQuery.type === 'subject' ? '科目' : '主观题'
  183. }导入文件`"
  184. v-bind="validateInfos.fileList"
  185. >
  186. <a-upload
  187. :file-list="uploadQuery.fileList"
  188. :before-upload="beforeUpload"
  189. @remove="handleRemove"
  190. :max-count="1"
  191. type="primary"
  192. >
  193. <a-button>
  194. <upload-outlined></upload-outlined>
  195. 选择文件
  196. </a-button>
  197. <a class="tw-ml-4 tw-align-bottom" @click.stop="downloadTemplate">
  198. 下载导入模板
  199. </a>
  200. </a-upload>
  201. </a-form-item>
  202. </a-form>
  203. </a-modal>
  204. </div>
  205. </template>
  206. <script setup lang="ts" name="PageSubjects">
  207. import { onBeforeMount, reactive, ref, watch } from "vue";
  208. import {
  209. UploadOutlined,
  210. CheckCircleFilled,
  211. CloseCircleFilled,
  212. DownOutlined,
  213. } from "@ant-design/icons-vue";
  214. import Block from "@/components/block/index.vue";
  215. import { message, TableColumnType, UploadProps } from "ant-design-vue";
  216. import { Form } from "ant-design-vue";
  217. import {
  218. getSubjectsListHttp,
  219. importSubjectsHttp,
  220. importPaperStructHttp,
  221. downloadPaperStructHttp,
  222. downloadSubjectTemplateHttp,
  223. downloadPaperStructTemplateHttp,
  224. } from "@/apis/struct";
  225. import { getSchoolListHttp } from "@/apis/school";
  226. import { getExamListHttp } from "@/apis/exam";
  227. import { useMainStore } from "@/store/main";
  228. import { throttle } from "lodash-es";
  229. import { fileTypeCheck } from "@/utils/file-type";
  230. type ImportType = "subject" | "struct";
  231. const mainStore = useMainStore();
  232. const showImportModal = ref(false);
  233. const tableLoading = ref(false);
  234. const ImportDownloadApi: Record<
  235. ImportType,
  236. {
  237. upload: typeof importSubjectsHttp;
  238. download: typeof downloadSubjectTemplateHttp;
  239. }
  240. > = {
  241. subject: {
  242. upload: importSubjectsHttp,
  243. download: downloadSubjectTemplateHttp,
  244. },
  245. struct: {
  246. upload: importPaperStructHttp,
  247. download: downloadPaperStructTemplateHttp,
  248. },
  249. };
  250. /** 导入参数 */
  251. const uploadQuery = reactive<{
  252. type: ImportType;
  253. schoolId?: string | number;
  254. examId?: string;
  255. fileList: UploadProps["fileList"];
  256. schoolTableData: MultiplePageData<SchoolListInfo>;
  257. examTableData: MultiplePageData<ExamListInfo>;
  258. }>({
  259. type: "subject",
  260. schoolTableData: { totalCount: 0, result: [] },
  261. examTableData: { totalCount: 0, result: [] },
  262. schoolId: void 0,
  263. examId: void 0,
  264. fileList: [],
  265. });
  266. const uploadRules = {
  267. schoolId: [{ required: true, message: "请选择学校" }],
  268. examId: [{ required: true, message: "请选择考试批次" }],
  269. fileList: [
  270. { required: true, type: "array", len: 1, message: "请选择导入文件" },
  271. ],
  272. };
  273. const { validate, validateInfos, resetFields } = Form.useForm(
  274. uploadQuery,
  275. uploadRules
  276. );
  277. /** 查询参数 */
  278. const query = reactive<
  279. Omit<FetchSubjectsListQuery, "groupFinish"> & { groupFinish: number }
  280. >({
  281. /** 科目代码 */
  282. courseCode: "",
  283. courseName: "",
  284. /** 考试id */
  285. examId: "",
  286. /** 分组状态 */
  287. groupFinish: 0,
  288. /** 学校id */
  289. schoolId: mainStore.systemUserInfo?.schoolId || "",
  290. /** 总分截止值 */
  291. totalScoreMax: "",
  292. /** 总分起始值 */
  293. totalScoreMin: "",
  294. pageSize: 10,
  295. pageNumber: 1,
  296. });
  297. /** table配置 */
  298. const columns: TableColumnType[] = [
  299. { title: "序号", dataIndex: "index", align: "center", width: 60 },
  300. {
  301. title: "科目代码",
  302. dataIndex: "courseCode",
  303. align: "center",
  304. width: 120,
  305. ellipsis: true,
  306. },
  307. { title: "科目名称", dataIndex: "courseName", ellipsis: true },
  308. {
  309. title: "主观总分",
  310. dataIndex: "subjectiveScore",
  311. align: "center",
  312. width: 100,
  313. },
  314. { title: "试卷总分", dataIndex: "totalScore", align: "center", width: 100 },
  315. { title: "分组数", dataIndex: "groupCount", align: "center", width: 80 },
  316. ];
  317. /** 学校列表信息 */
  318. const schoolTableData = reactive<MultiplePageData<SchoolListInfo>>({
  319. totalCount: 0,
  320. result: [],
  321. });
  322. /** 考试列表信息 */
  323. const examTableData = reactive<MultiplePageData<ExamListInfo>>({
  324. totalCount: 0,
  325. result: [],
  326. });
  327. /** 科目列表信息 */
  328. const subjectsTableData = reactive<MultiplePageData<SubjectsListInfo>>({
  329. totalCount: 0,
  330. result: [],
  331. });
  332. /** 查询学校列表 */
  333. const querySchoolList = throttle(
  334. async (name: string = "", type: "list" | "form" = "list") => {
  335. const isList = type === "list";
  336. try {
  337. const { result = [], totalCount } = await getSchoolListHttp({
  338. name,
  339. pageNumber: 1,
  340. pageSize: 10,
  341. });
  342. Object.assign(isList ? schoolTableData : uploadQuery.schoolTableData, {
  343. result,
  344. totalCount,
  345. });
  346. } catch (error) {
  347. return Promise.reject(error);
  348. }
  349. },
  350. 100
  351. );
  352. /** 查询考试列表 */
  353. const queryExamList = throttle(
  354. async (name: string = "", type: "list" | "form" = "list") => {
  355. try {
  356. const isList = type === "list";
  357. const schoolId = isList ? query.schoolId : uploadQuery.schoolId;
  358. if (!schoolId) {
  359. return Promise.reject(`schoolId got : ${schoolId}`);
  360. }
  361. const { result = [], totalCount } = await getExamListHttp({
  362. pageNumber: 1,
  363. pageSize: 10,
  364. name,
  365. schoolId,
  366. });
  367. Object.assign(isList ? examTableData : uploadQuery.examTableData, {
  368. result,
  369. totalCount,
  370. });
  371. } catch (error) {
  372. return Promise.reject(error);
  373. }
  374. },
  375. 100
  376. );
  377. /** 查询科目列表 */
  378. const querySubjectsList = async () => {
  379. try {
  380. tableLoading.value = true;
  381. const { result = [], totalCount } = await getSubjectsListHttp({
  382. ...query,
  383. groupFinish: [void 0, true, false][query.groupFinish],
  384. });
  385. Object.assign(subjectsTableData, { result, totalCount });
  386. } catch (error) {
  387. return Promise.reject(error);
  388. }
  389. tableLoading.value = false;
  390. };
  391. watch(() => query.pageNumber, querySubjectsList);
  392. watch(
  393. () => query.schoolId,
  394. () => {
  395. query.examId = "";
  396. Object.assign(examTableData, { result: [], totalCount: 0 });
  397. if (query.schoolId) {
  398. queryExamList("", "list");
  399. }
  400. }
  401. );
  402. watch(
  403. () => uploadQuery.schoolId,
  404. () => {
  405. uploadQuery.examId = void 0;
  406. Object.assign(uploadQuery.examTableData, { result: [], totalCount: 0 });
  407. if (uploadQuery.schoolId) {
  408. queryExamList("", "form");
  409. }
  410. }
  411. );
  412. const currentPageChange = ({ current }: { current: number }) => {
  413. query.pageNumber = current;
  414. };
  415. /** 导出主观题 */
  416. const downloadPaperStruct = async () => {
  417. try {
  418. await downloadPaperStructHttp({
  419. ...query,
  420. groupFinish: [void 0, true, false][query.groupFinish],
  421. });
  422. } catch (error) {
  423. return Promise.reject(error);
  424. }
  425. };
  426. const handleRemove: UploadProps["onRemove"] = (file) => {
  427. const index = uploadQuery.fileList!.indexOf(file);
  428. const newFileList = uploadQuery.fileList!.slice();
  429. newFileList.splice(index, 1);
  430. uploadQuery.fileList = newFileList;
  431. };
  432. const beforeUpload: UploadProps["beforeUpload"] = async (file) => {
  433. await fileTypeCheck(file, ["xls", "xlsx"]).catch((error) => {
  434. message.error("文件类型错误, 请使用导入模板编辑");
  435. return Promise.reject(error);
  436. });
  437. uploadQuery.fileList = [file];
  438. return false;
  439. };
  440. /** 下载导入模板 */
  441. const downloadTemplate = async () => {
  442. try {
  443. await ImportDownloadApi[uploadQuery.type].download();
  444. } catch (error) {
  445. return Promise.reject(error);
  446. }
  447. };
  448. /** 显示导入弹窗 */
  449. const showImportModalType = async (type: ImportType) => {
  450. uploadQuery.type = type;
  451. uploadQuery.schoolId = query.schoolId || mainStore.systemUserInfo?.schoolId;
  452. showImportModal.value = true;
  453. querySchoolList("", "form");
  454. };
  455. /** 导入数据 */
  456. const onImport = async () => {
  457. try {
  458. const valid = await validate();
  459. if (valid) {
  460. const formData = new FormData();
  461. formData.append("examId", uploadQuery.examId || "");
  462. uploadQuery.fileList?.forEach((file: any) => {
  463. formData.append("file", file);
  464. });
  465. await ImportDownloadApi[uploadQuery.type].upload(formData);
  466. querySubjectsList();
  467. showImportModal.value = false;
  468. }
  469. } catch (error) {
  470. return Promise.reject(error);
  471. }
  472. };
  473. onBeforeMount(async () => {
  474. try {
  475. await querySchoolList();
  476. if (
  477. schoolTableData.result
  478. .map((school) => `${school.id}`)
  479. .some((id) => id === `${query.schoolId}`)
  480. ) {
  481. await queryExamList("", "list");
  482. await querySubjectsList();
  483. }
  484. } catch (error) {
  485. console.error(error);
  486. }
  487. });
  488. </script>
  489. <style scoped lang="less">
  490. .subjects-manage {
  491. .header-block {
  492. .ant-input {
  493. width: 160px;
  494. }
  495. :deep(.ant-select-selector) {
  496. width: 160px;
  497. }
  498. :deep(.ant-form-item) {
  499. margin-bottom: 8px;
  500. }
  501. .range-item {
  502. .ant-input {
  503. width: 69px;
  504. }
  505. }
  506. .search-button {
  507. background-color: @font-color;
  508. color: @white;
  509. border: none;
  510. width: 56px;
  511. padding: 0;
  512. &:after {
  513. display: none;
  514. opacity: 0;
  515. }
  516. }
  517. .operation-button {
  518. width: 72px;
  519. padding: 0;
  520. margin-left: 8px;
  521. }
  522. }
  523. .subjects-table {
  524. .star-icon {
  525. width: 14px;
  526. height: 14px;
  527. display: inline-block;
  528. vertical-align: middle;
  529. }
  530. }
  531. }
  532. </style>