index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. <template>
  2. <PageWrapper dense contentFullHeight fixedHeight contentClass="flex">
  3. <div class="h-full bg-white mr-2" style="width: 350px">
  4. <div class="h-full m-2">
  5. <BasicForm @register="formStudent">
  6. <template #formFooter>
  7. <div style="display: flex; justify-content: flex-end">
  8. <a-button type="primary" class="w-20" @click="handelStudenSearch">搜索</a-button>
  9. </div>
  10. </template>
  11. </BasicForm>
  12. <div class="h-full">
  13. <div style="height: calc(100% - 130px)">
  14. <ScrollContainer>
  15. <div class="student-list" v-for="(item, index) in studentGroup" :key="index">
  16. <div class="student-list-title">{{ item.name }}</div>
  17. <div class="student-list-title">已缴费</div>
  18. <div class="student-list-wrap grid gap-2 grid-cols-3">
  19. <Badge
  20. :count="
  21. getStudentSelectedIndex(it.userId) > -1
  22. ? getStudentSelectedIndex(it.userId) + 1
  23. : 0
  24. "
  25. v-for="(it, ix) in item.studentList.filter((item) => item.payStatus === 1)"
  26. :key="ix"
  27. :number-style="{ backgroundColor: '#13c2c2' }"
  28. >
  29. <div
  30. class="student-list-item"
  31. :class="{
  32. 'student-list-item-active': getStudentSelectedIndex(it.userId) > -1,
  33. 'student-list-item-ext': it.buildName,
  34. }"
  35. @click="handelStudentClick(it, item.teacherId)"
  36. >
  37. <div class="student-list-item-text">
  38. <div class="student-list-item-text-title">{{ it.studentName }}</div>
  39. <div class="student-list-item-text-sex">{{ it.genderCn }}</div>
  40. </div>
  41. <div class="student-list-item-note">
  42. <div v-if="it.buildName">
  43. <div>{{ it.buildName }}</div>
  44. <div>{{ it.roomName }} - {{ it.bedNumber }}</div>
  45. </div>
  46. <div v-else>未分配</div>
  47. </div>
  48. </div>
  49. </Badge>
  50. </div>
  51. <div class="student-list-title">未缴费</div>
  52. <div class="student-list-wrap grid gap-2 grid-cols-3">
  53. <Badge
  54. :count="
  55. getStudentSelectedIndex(it.userId) > -1
  56. ? getStudentSelectedIndex(it.userId) + 1
  57. : 0
  58. "
  59. v-for="(it, ix) in item.studentList.filter((item) => item.payStatus === 0)"
  60. :key="ix"
  61. :number-style="{ backgroundColor: '#13c2c2' }"
  62. >
  63. <div
  64. class="student-list-item"
  65. :class="{
  66. 'student-list-item-active': getStudentSelectedIndex(it.userId) > -1,
  67. 'student-list-item-ext': it.buildName,
  68. }"
  69. @click="handelStudentClick(it, item.teacherId)"
  70. >
  71. <div class="student-list-item-text">
  72. <div class="student-list-item-text-title">{{ it.studentName }}</div>
  73. <div class="student-list-item-text-sex">{{ it.genderCn }}</div>
  74. </div>
  75. <div class="student-list-item-note">
  76. <div v-if="it.buildName">
  77. <div>{{ it.buildName }}</div>
  78. <div>{{ it.roomName }} - {{ it.bedNumber }}</div>
  79. </div>
  80. <div v-else>未分配</div>
  81. </div>
  82. </div>
  83. </Badge>
  84. </div>
  85. </div>
  86. </ScrollContainer>
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. <div class="bg-white mr-2 h-full" style="flex: 1">
  92. <div class="m-2 h-full">
  93. <BasicForm @register="formRoom">
  94. <template #formFooter>
  95. <div style="display: flex; justify-content: flex-end">
  96. <a-button type="primary" class="w-20 mr-2" @click="handelRoomSearch">搜索</a-button>
  97. <a-button class="w-20" @click="handleClear">重置</a-button>
  98. </div>
  99. </template>
  100. </BasicForm>
  101. <div class="h-full">
  102. <div style="height: calc(100% - 230px)">
  103. <ScrollContainer>
  104. <div class="room-group grid gap-4 grid-cols-3">
  105. <div class="room-group-wrap" v-for="(item, index) in roomGroup" :key="index">
  106. <div class="room-group-item">
  107. <div class="room-group-title">
  108. <div>{{ item[0].buildName }}-{{ item[0].roomName }}</div>
  109. <div style="color: #ff0000">
  110. {{ item[0].isMax === 1 ? '[混合寝室]' : '' }}
  111. {{ `[${item[0].genderCn}寝]` }}
  112. </div>
  113. </div>
  114. <div class="room-list grid gap-2 grid-cols-4">
  115. <div class="room-list-item" v-for="(it, ix) in item" :key="ix">
  116. <div class="room-list-item-wrap" @click="handelRoomEdit(it)">
  117. <div class="room-list-item-text"> {{ it.className }}</div>
  118. <div class="room-list-item-text"> {{ it.studentName }}</div>
  119. <div class="room-list-item-text room-list-item-number">
  120. {{ it.bedNumber }}号床位
  121. </div>
  122. </div>
  123. <div
  124. class="room-list-item-delete"
  125. @click="handelRoomDelete(it)"
  126. v-if="it.studentUserId"
  127. >
  128. <Icon icon="lets-icons:dell-fill" :size="16" />
  129. </div>
  130. </div>
  131. </div>
  132. <div class="room-group-tool">
  133. <div class="room-group-tool-item" @click="handelRoomDeleteBath(item)">
  134. 批量移出
  135. </div>
  136. </div>
  137. <div
  138. class="room-group-class flex"
  139. style="flex-wrap: wrap"
  140. v-if="item[0].isMax === 1"
  141. >
  142. <div
  143. class="room-group-class-item"
  144. style="margin: 4px"
  145. v-for="(ir, id) in getGroupClass(item)"
  146. :key="id"
  147. >
  148. {{ ir.className }}【{{ ir.total }}人】
  149. </div>
  150. </div>
  151. </div>
  152. </div>
  153. </div>
  154. </ScrollContainer>
  155. </div>
  156. <div style="height: 100px; width: 100%; display: block">
  157. <div class="flex items-center">
  158. <div class="student-del-list flex-1">
  159. <Badge
  160. style="margin-right: 8px"
  161. :count="
  162. getStudentSelectedIndex(it.userId) > -1
  163. ? getStudentSelectedIndex(it.userId) + 1
  164. : 0
  165. "
  166. v-for="(it, ix) in studentDeleted"
  167. :key="ix"
  168. :number-style="{ backgroundColor: '#13c2c2' }"
  169. >
  170. <div
  171. class="student-del-list-item"
  172. :class="{
  173. 'student-del-list-item-active': getStudentSelectedIndex(it.userId) > -1,
  174. }"
  175. @click="handelStudentClick(it, it.teacherId, true)"
  176. >
  177. {{ it.userName }}
  178. </div>
  179. </Badge>
  180. </div>
  181. <div style="width: 60px">
  182. <a-button type="primary" @click="handelConfirm" :loading="saveLoading">
  183. 确定
  184. </a-button>
  185. </div>
  186. </div>
  187. </div>
  188. </div>
  189. </div>
  190. </div>
  191. </PageWrapper>
  192. </template>
  193. <script setup lang="ts">
  194. import { ref, onMounted } from 'vue';
  195. import { Badge } from 'ant-design-vue';
  196. import { PageWrapper } from '/@/components/Page';
  197. import { BasicForm, useForm } from '/@/components/Form';
  198. import { formStudentSchema, formRoomSchema } from './data.config';
  199. import {
  200. getRoomBedAdjustClassStudent,
  201. getRoomBedAdjustBedStudent,
  202. putRoomBedAdjustAdjustBedBatch,
  203. getRoomBedAdjustIsClassTeacher,
  204. } from '/@/services/apis/RoomBedAdjustController';
  205. import { ScrollContainer } from '/@/components/Container/index';
  206. import { Icon } from '/@/components/Icon';
  207. import { groupBy } from 'lodash-es';
  208. import { useMessage } from '/@/hooks/web/useMessage';
  209. import { useUserStore } from '/@/store/modules/user';
  210. const { createMessage } = useMessage();
  211. const studentGroup = ref<any>([]);
  212. const roomGroup = ref<any>([]);
  213. const userStore = useUserStore();
  214. const studentSelected = ref<string[]>([]);
  215. const studentSelectedList = ref<Recordable[]>([]);
  216. const studentDeleted = ref<any[]>([]);
  217. const studentUpdateList = ref<Recordable[]>([]);
  218. const classTeacher = ref<boolean>(false);
  219. const saveLoading = ref<boolean>(false);
  220. const getStudentSelectedIndex = (id) => {
  221. return studentSelected.value.findIndex((row) => row === id);
  222. };
  223. const handleClear = () => {
  224. resetFields();
  225. };
  226. const [formStudent, { validate: validateStudent }] = useForm({
  227. labelWidth: 60,
  228. schemas: formStudentSchema,
  229. compact: true,
  230. showActionButtonGroup: false,
  231. actionColOptions: { span: 24 },
  232. });
  233. const [formRoom, { validate: validateRoom, resetFields }] = useForm({
  234. labelWidth: 80,
  235. schemas: formRoomSchema,
  236. compact: true,
  237. showActionButtonGroup: false,
  238. actionColOptions: { span: 24 },
  239. });
  240. const handelStudenSearch = async (cleara: boolean) => {
  241. try {
  242. const values = await validateStudent();
  243. studentGroup.value = await getRoomBedAdjustClassStudent(values);
  244. if (cleara) {
  245. studentSelected.value = [];
  246. studentSelectedList.value = [];
  247. studentDeleted.value = [];
  248. studentUpdateList.value = [];
  249. }
  250. } catch {}
  251. };
  252. const handelRoomSearch = async () => {
  253. try {
  254. const values = await validateRoom();
  255. const data = await getRoomBedAdjustBedStudent(values);
  256. roomGroup.value = groupBy(data, 'roomId');
  257. } catch {}
  258. };
  259. const handelStudentClick = (item, teacherId, del = false) => {
  260. if (teacherId && teacherId !== userStore.getUserInfo.id && classTeacher.value === true) {
  261. createMessage.error(`只能调整本班学生`);
  262. return;
  263. }
  264. if (studentSelected.value.includes(item.userId)) {
  265. studentSelected.value.splice(
  266. studentSelected.value.findIndex((row) => row === item.userId),
  267. 1,
  268. );
  269. studentSelectedList.value.splice(
  270. studentSelectedList.value.findIndex((row) => row.userId === item.userId),
  271. 1,
  272. );
  273. } else {
  274. if (!studentUpdateList.value.find((row) => row.userId === item.userId) || del === true) {
  275. studentSelected.value.push(item.userId);
  276. studentSelectedList.value.push({
  277. userId: item.userId,
  278. genderCn: item.genderCn,
  279. // roomId: item.roomId,
  280. bedNumber: item.bedNumber,
  281. className: item.className,
  282. studentName: item.userName || item.studentName,
  283. teacherId: teacherId,
  284. });
  285. }
  286. }
  287. };
  288. const handelRoomDelete = async (item) => {
  289. if (item.teacherId !== userStore.getUserInfo.id && classTeacher.value === true) {
  290. createMessage.error(`只能调整本班学生`);
  291. return;
  292. }
  293. try {
  294. const findIndex = studentUpdateList.value.findIndex(
  295. (row) => row.userId === item.studentUserId,
  296. );
  297. let isAddDel = true;
  298. if (findIndex > -1) {
  299. if (studentUpdateList.value[findIndex].bedNumber) {
  300. studentUpdateList.value[findIndex].bedId = 0;
  301. } else {
  302. studentUpdateList.value.splice(findIndex, 1);
  303. isAddDel = false;
  304. }
  305. } else {
  306. studentUpdateList.value.push({
  307. userId: item.studentUserId,
  308. bedId: 0,
  309. bedNumber: item.bedNumber,
  310. });
  311. }
  312. if (isAddDel) {
  313. if (!studentDeleted.value.find((row) => row.userId === item.studentUserId)) {
  314. studentDeleted.value.push({
  315. userId: item.studentUserId,
  316. userName: item.studentName,
  317. genderCn: item.genderCn,
  318. bedNumber: item.bedNumber,
  319. className: item.className,
  320. teacherId: item.teacherId,
  321. });
  322. }
  323. }
  324. item.className = null;
  325. item.studentName = '';
  326. item.studentUserId = '';
  327. } catch {}
  328. };
  329. const handelRoomEdit = async (item) => {
  330. if (!item.studentUserId) {
  331. try {
  332. if (studentSelected.value.length > 0) {
  333. const userInfo = studentSelectedList.value[0];
  334. if (item.genderCn !== userInfo.genderCn) {
  335. createMessage.error(`${item.genderCn}生寝室不能调入${userInfo.genderCn}生`);
  336. return;
  337. }
  338. // item.bedNumber = userInfo.bedNumber;
  339. item.className = userInfo.className;
  340. item.studentName = userInfo.studentName;
  341. item.studentUserId = userInfo.userId;
  342. item.teacherId = userInfo.teacherId;
  343. const findIndex = studentUpdateList.value.findIndex(
  344. (row) => row.userId === userInfo.userId,
  345. );
  346. if (findIndex > -1) {
  347. studentUpdateList.value[findIndex].bedId = item.id;
  348. } else {
  349. studentUpdateList.value.push({
  350. userId: userInfo.userId,
  351. bedId: item.id,
  352. bedNumber: userInfo.bedNumber,
  353. });
  354. }
  355. // studentUpdateList.value.push({
  356. // userId: userInfo.userId,
  357. // bedId: item.id,
  358. // bedNumber: userInfo.bedNumber,
  359. // });
  360. studentSelected.value.splice(0, 1);
  361. studentSelectedList.value.splice(0, 1);
  362. if (studentDeleted.value.find((row) => row.userId === userInfo.userId)) {
  363. studentDeleted.value.splice(
  364. studentDeleted.value.findIndex((row) => row.userId === userInfo.userId),
  365. 1,
  366. );
  367. }
  368. // await handelStudenSearch(false);
  369. // await handelRoomSearch();
  370. }
  371. } catch {}
  372. }
  373. };
  374. const handelRoomDeleteBath = async (list) => {
  375. list.forEach((item) => {
  376. if (classTeacher.value === true) {
  377. if (item.studentUserId && item.teacherId === userStore.getUserInfo.id) {
  378. handelRoomDelete(item);
  379. }
  380. } else {
  381. if (item.studentUserId) {
  382. handelRoomDelete(item);
  383. }
  384. }
  385. });
  386. };
  387. const getGroupClass = (data) => {
  388. const groupClass = groupBy(data, 'className');
  389. const resData: any[] = [];
  390. for (let item in groupClass) {
  391. if (item !== 'null') {
  392. resData.push({ className: item, total: groupClass[item].length });
  393. }
  394. }
  395. return resData;
  396. };
  397. const handelConfirm = async () => {
  398. if (studentDeleted.value.length > 0) {
  399. createMessage.info(`还有未分配完的学生!`);
  400. return;
  401. }
  402. if (studentUpdateList.value.length === 0) {
  403. createMessage.info(`没有调整的学生`);
  404. return;
  405. }
  406. saveLoading.value = true;
  407. try {
  408. const postData = studentUpdateList.value.map((item) => {
  409. return { studentUserId: item.userId, bedId: item.bedId };
  410. });
  411. await putRoomBedAdjustAdjustBedBatch(postData);
  412. await handelStudenSearch(true);
  413. await handelRoomSearch();
  414. createMessage.success(`调整成功`);
  415. } finally {
  416. // createMessage.error(`调整失败`);
  417. saveLoading.value = false;
  418. }
  419. };
  420. const getIsTeacher = async () => {
  421. const data = await getRoomBedAdjustIsClassTeacher({});
  422. classTeacher.value = data === 1;
  423. if (data === 1) {
  424. await handelStudenSearch(false);
  425. }
  426. };
  427. onMounted(async () => {
  428. await getIsTeacher();
  429. });
  430. </script>
  431. <style scoped lang="less">
  432. .student-list {
  433. margin-top: 8px;
  434. padding: 8px;
  435. background-color: #f7f7f7;
  436. border-radius: 8px;
  437. &:last-child {
  438. margin-bottom: 16px;
  439. }
  440. &-title {
  441. margin: 8px 0;
  442. }
  443. &-item {
  444. border: 2px dashed rgba(255, 182, 0, 0.4);
  445. background-color: rgba(255, 182, 0, 0.4);
  446. padding: 4px;
  447. cursor: pointer;
  448. &-ext {
  449. background-color: rgba(rgba(0, 255, 25, 0.4));
  450. border: 2px dashed rgba(rgba(0, 255, 25, 0.4));
  451. }
  452. &-active {
  453. border: 2px dashed rgba(rgba(14, 4, 150, 0.4));
  454. }
  455. &-text {
  456. display: flex;
  457. justify-content: space-between;
  458. }
  459. &-note {
  460. margin-top: 4px;
  461. text-align: center;
  462. word-break: keep-all;
  463. }
  464. }
  465. }
  466. .room {
  467. &-group {
  468. &-wrap {
  469. margin-top: 8px;
  470. padding: 8px;
  471. background-color: #f7f7f7;
  472. border-radius: 8px;
  473. }
  474. &-item {
  475. position: relative;
  476. }
  477. &-title {
  478. display: flex;
  479. justify-content: space-between;
  480. margin: 8px 0;
  481. }
  482. &-tool {
  483. display: flex;
  484. justify-content: flex-end;
  485. margin-top: 8px;
  486. &-item {
  487. padding: 4px 16px;
  488. background-color: rgb(48, 111, 253);
  489. font-size: 12px;
  490. color: #fff;
  491. cursor: pointer;
  492. }
  493. }
  494. }
  495. &-list {
  496. position: relative;
  497. &-item {
  498. position: relative;
  499. border: 1px solid rgb(121, 121, 121);
  500. background-color: #fff;
  501. text-align: center;
  502. cursor: pointer;
  503. &-text {
  504. height: 25px;
  505. display: block;
  506. overflow: hidden;
  507. text-overflow: ellipsis;
  508. white-space: nowrap;
  509. }
  510. &-number {
  511. margin-top: 8px;
  512. }
  513. &-delete {
  514. position: absolute;
  515. z-index: 1000;
  516. right: -8px;
  517. top: -8px;
  518. }
  519. }
  520. }
  521. }
  522. .student-del-list {
  523. margin: 8px;
  524. padding: 8px;
  525. background-color: #f7f7f7;
  526. display: flex;
  527. flex-wrap: wrap;
  528. &-item {
  529. padding: 8px 16px;
  530. cursor: pointer;
  531. &-active {
  532. background-color: rgba(rgba(0, 255, 25, 0.4));
  533. }
  534. }
  535. }
  536. </style>