allClaAddStu.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. <template>
  2. <div class="dia_box" v-loading="boxLoading">
  3. <div class="dia_box_item">
  4. <div class="addcla_box_item">
  5. <span class="addcla_box_item_span">{{ lang.ssTargetClass }}</span>
  6. <el-cascader
  7. :options="cascaderData"
  8. v-model="cascaderDataPath"
  9. @change="handleChangecascader"
  10. popper-class="myCascader"
  11. :props="{
  12. label: 'name',
  13. value: 'id',
  14. children: 'children',
  15. }"
  16. >
  17. <template slot-scope="{ data, node }">
  18. <!-- 一级选项(年级) -->
  19. <span v-if="node.level === 1">{{ data.name }}</span>
  20. <!-- 二级选项(班级)- 可以自定义显示 -->
  21. <span v-else-if="node.level === 2" class="cascader-class-item">
  22. <span>{{ data.name }}</span>
  23. </span>
  24. <!-- 其他级别(兜底) -->
  25. <span v-else>{{ data.name }}</span>
  26. </template>
  27. </el-cascader>
  28. </div>
  29. <div class="addcla_box_item" style="position: relative">
  30. <span class="addcla_box_item_span">{{ lang.ssSearchStuLbl }}</span>
  31. <el-input
  32. v-model.trim="searchStuName"
  33. :placeholder="lang.ssEnterStuNameMul"
  34. slot="reference"
  35. @change="searchStu"
  36. @focus="popoverVisible = true"
  37. ></el-input>
  38. <div
  39. class="search_stu_box"
  40. v-if="popoverVisible"
  41. v-loading="editTabloading"
  42. >
  43. <div class="search_stu_box_content">
  44. <el-tag
  45. v-for="item in searchStuData"
  46. :key="item.userid"
  47. closable
  48. :type="item.acc"
  49. @close="handleClose(item)"
  50. >
  51. <el-tooltip
  52. class="item"
  53. effect="dark"
  54. :content="item.acc"
  55. placement="top-start"
  56. >
  57. <span>{{ item.username }}</span>
  58. </el-tooltip>
  59. </el-tag>
  60. </div>
  61. <div class="search_stu_box_footer">
  62. <div>{{ lang.ssInputStuCnt.replace(/\*/g, searchStuData.length) }}</div>
  63. <div class="dialog-footer">
  64. <!-- <el-button size="small" @click="selectNoneStu">取消</el-button> -->
  65. <div @click="selectNoneStu" class="kbtn">{{ lang.Cancel }}</div>
  66. <div @click="selectStu" class="kbtn" style="background: #3681FC; color: #E3EDFF;">{{ lang.Confirm }}</div>
  67. <!-- <el-button size="small" type="primary" @click="selectStu"
  68. >确认</el-button
  69. > -->
  70. </div>
  71. </div>
  72. </div>
  73. </div>
  74. <div class="addcla_box_item">
  75. <span class="addcla_box_item_span">{{ lang.Selected }}:</span>
  76. <div class="selected_stu_box">
  77. <el-tag
  78. v-for="(item, index) in selectedStuData"
  79. :key="index"
  80. closable
  81. :type="item.acc"
  82. @close="handleSelectedStuClose(item)"
  83. >
  84. <el-tooltip
  85. class="item"
  86. effect="dark"
  87. :content="item.acc"
  88. placement="top-start"
  89. >
  90. <span>{{ item.username }}</span>
  91. </el-tooltip>
  92. </el-tag>
  93. </div>
  94. </div>
  95. <div style="width: 100%">{{ lang.ssSelectStuTip }}</div>
  96. <el-table
  97. :data="allClaAllStuData"
  98. ref="tableData"
  99. style="width: 100%"
  100. :header-cell-style="{ background: '#E7EAF0' }"
  101. height="300px"
  102. :show-header="false"
  103. class="tableList"
  104. v-loading="tableLoading || confirmLoading"
  105. @selection-change="handleInterestClaSelectionChange"
  106. >
  107. <el-table-column
  108. type="selection"
  109. :selectable="checkSelectable"
  110. width="55px"
  111. >
  112. </el-table-column>
  113. <el-table-column
  114. v-for="(item, index) in allClaColumns"
  115. :key="index"
  116. :label="item.label"
  117. :min-width="item.width"
  118. >
  119. <template slot-scope="scope">
  120. <div v-if="item.label == lang.Account">
  121. <span>{{ scope.row.acc.split("@")[0] }}</span>
  122. </div>
  123. <div class="cla_box" v-else-if="item.label == lang.Class">
  124. <span
  125. class="cla_span"
  126. v-for="(cla, ind) in scope.row.classname.split('/')"
  127. :key="ind + 'cla'"
  128. >
  129. {{ cla }}
  130. </span>
  131. </div>
  132. <div v-else>
  133. <span>{{ scope.row[item.prop] }}</span>
  134. </div>
  135. </template>
  136. </el-table-column>
  137. </el-table>
  138. <div class="page_box">
  139. <div>
  140. {{ lang.ssPageInfoL
  141. .replace(/\*/g, (page - 1) * pageSize + 1)
  142. .replace(/\#/g, page * pageSize)
  143. .replace(/\$/g, total)
  144. }}
  145. </div>
  146. <el-pagination
  147. background
  148. @current-change="handlePageChange"
  149. :current-page.sync="page"
  150. :page-size="pageSize"
  151. layout="prev, pager, next"
  152. :total="total"
  153. >
  154. </el-pagination>
  155. </div>
  156. </div>
  157. <div class="dialog-footer">
  158. <div class="kbtn" @click="closeAllClaAddStu">{{ lang.Cancel }}</div>
  159. <div @click="confirmAllClaAddStu" class="kbtn" style="background: #3681FC; color: #E3EDFF;">{{ lang.Confirm }}</div>
  160. </div>
  161. <div class="pop-upBox">
  162. <el-dialog
  163. :visible.sync="popBoxdia"
  164. width="480px"
  165. append-to-body
  166. class="upBox"
  167. :show-close="false"
  168. >
  169. <div class="msgCon">
  170. <img style="width: 20px;" src="../../../../assets/stuImg/duil.svg" alt="">
  171. <div class="msgContxt" v-if="selectedStuData.length">
  172. {{ selectedStuData[0].username + lang.ssAddStuSucc.replace(/\*/g, selectedStuData.length) }}
  173. </div>
  174. </div>
  175. <span slot="footer" class="dialog-footer">
  176. <el-button type="primary"
  177. class="btnpop"
  178. @click="confirmDelStu"
  179. >{{ lang.Confirm }}</el-button
  180. >
  181. </span>
  182. </el-dialog>
  183. </div>
  184. </div>
  185. </template>
  186. <script>
  187. import { myMixin } from "@/mixins/mixin.js"
  188. export default {
  189. mixins: [ myMixin ],
  190. name: "allClaAddStu",
  191. props: {
  192. classInfo: {
  193. type: Object,
  194. default: () => {},
  195. },
  196. },
  197. data() {
  198. return {
  199. userid: this.$route.query.userid,
  200. org: this.$route.query.org,
  201. oid: this.$route.query.oid,
  202. role: this.$route.query.role,
  203. boxLoading: false,
  204. tableLoading: false,
  205. editTabloading: false,
  206. confirmLoading: false,
  207. cascaderDataPath: [],
  208. cascaderData: [],
  209. searchStuName: "",
  210. searchStuData: [],
  211. selectedStuData: [],
  212. popoverVisible: false,
  213. allClaAllStuData: [],
  214. page: 1,
  215. popBoxdia:false,
  216. total: 0,
  217. pageSize: 10,
  218. allClaColumns: [
  219. {
  220. label: this.lang.StudentName,
  221. prop: "username",
  222. width: "30%",
  223. },
  224. {
  225. label: this.lang.Account,
  226. prop: "acc",
  227. width: "30%",
  228. },
  229. {
  230. label: this.lang.Class,
  231. prop: "classname",
  232. width: "40%",
  233. },
  234. ],
  235. };
  236. },
  237. methods: {
  238. clearData(){
  239. this.cascaderDataPath = [];
  240. this.searchStuName="";
  241. this.searchStuData= [];
  242. this.selectedStuData= [];
  243. this.allClaAllStuData=[];
  244. this.page= 1;
  245. this.total=0;
  246. this.pageSize= 10;
  247. },
  248. confirmDelStu(){
  249. this.$refs.tableData.clearSelection();
  250. this.selectedStuData = [];
  251. this.popBoxdia = false;
  252. },
  253. // 删除已选择学生
  254. handleSelectedStuClose(item) {
  255. // 从已选择数据中移除
  256. this.selectedStuData = this.selectedStuData.filter(
  257. (e) => e.userid !== item.userid
  258. );
  259. // 如果该学生在当前页的表格中,取消其选中状态
  260. this.$nextTick(() => {
  261. if (this.$refs.tableData) {
  262. const row = this.allClaAllStuData.find(
  263. (row) => row.userid === item.userid
  264. );
  265. if (row) {
  266. this.$refs.tableData.toggleRowSelection(row, false);
  267. }
  268. }
  269. });
  270. },
  271. closeAllClaAddStu() {
  272. this.$emit("closeAllClaAddStu");
  273. },
  274. confirmAllClaAddStu() {
  275. if (this.confirmLoading) {
  276. this.$message.warning(this.lang.ssUploading);
  277. return;
  278. }
  279. if (this.selectedStuData.length === 0) {
  280. this.$message({
  281. message: this.lang.ssSelectStu,
  282. type: "error",
  283. });
  284. return;
  285. }
  286. console.log("上传学生数据");
  287. this.confirmLoading = true;
  288. const promises2 = this.selectedStuData.map((item) => {
  289. let box = [...item.classid.split(","), this.classInfo.id];
  290. box = box.filter((item) => item != "");
  291. return this.moveClassStudentPromise(item, box.join(","));
  292. });
  293. Promise.all(promises2)
  294. .then(() => {
  295. this.popBoxdia = true
  296. this.$emit("getStudent");
  297. if (this.cascaderDataPath.length > 0) {
  298. this.getdata(this.cascaderDataPath[1]);
  299. }
  300. this.addOp3('1', "", { type: "student_move" }, "success")
  301. })
  302. .catch((err) => {
  303. console.error(err);
  304. this.$message({
  305. message: this.lang.ssMoveStuFail,
  306. type: "error",
  307. });
  308. this.addOp3('1', "", { type: "student_move" }, err)
  309. })
  310. .finally(() => {
  311. this.confirmLoading = false;
  312. });
  313. },
  314. moveClassStudentPromise(item, cid) {
  315. return new Promise((resolve, reject) => {
  316. let params = [
  317. {
  318. userid: item.userid,
  319. username: item.acc,
  320. alias: item.username,
  321. ph: item.phonenumber,
  322. sid: item.studentid,
  323. cid: cid,
  324. },
  325. ];
  326. console.log(params);
  327. this.ajax
  328. .post(this.$store.state.api + "updateStudentInfo", params)
  329. .then((res) => {
  330. resolve(res);
  331. })
  332. .catch((err) => {
  333. console.error(err);
  334. reject(err);
  335. });
  336. });
  337. },
  338. handleChangecascader(val) {
  339. console.log(val);
  340. this.getdata(val[1]);
  341. },
  342. selectNoneStu() {
  343. this.popoverVisible = false;
  344. this.searchStuName = "";
  345. this.searchStuData = [];
  346. },
  347. searchStu() {
  348. if (!this.searchStuName) {
  349. this.$message({
  350. message: this.lang.enterstuname,
  351. type: "error",
  352. });
  353. return;
  354. }
  355. this.editTabloading = true;
  356. // 处理输入:去除多余空格,确保逗号分隔格式正确
  357. let inpValue = this.searchStuName
  358. .split(",")
  359. .map((name) => name.trim())
  360. .filter((name) => name)
  361. .join(",");
  362. console.log('inpValue',inpValue);
  363. let params = [{
  364. oid: this.oid,
  365. inp: inpValue,
  366. }];
  367. this.ajax
  368. .post(this.$store.state.api + "selectchStu", params)
  369. .then((res) => {
  370. console.log(res.data[0], "res");
  371. let box = res.data[0].filter(
  372. (item) => item.classid.indexOf(this.classInfo.id) == -1
  373. );
  374. this.searchStuData = box;
  375. this.editTabloading = false;
  376. })
  377. .catch((err) => {
  378. this.editTabloading = false;
  379. console.error(err);
  380. });
  381. },
  382. handleClose(item) {
  383. this.searchStuData = this.searchStuData.filter((e) => e !== item);
  384. },
  385. selectStu() {
  386. this.popoverVisible = false;
  387. const existingIds = new Set(
  388. this.selectedStuData.map((item) => item.userid)
  389. );
  390. // 过滤掉重复的,只添加新的学生
  391. const newStuData = this.searchStuData.filter(
  392. (item) => !existingIds.has(item.userid)
  393. );
  394. this.selectedStuData.push(...newStuData);
  395. this.handleSelectedStuData(this.selectedStuData);
  396. this.searchStuData = [];
  397. this.searchStuName = "";
  398. },
  399. handleInterestClaSelectionChange(val) {
  400. const currentPageIds = new Set(
  401. (this.allClaAllStuData || []).map((row) => row.userid)
  402. );
  403. const otherPageSelected = (this.selectedStuData || []).filter(
  404. (item) => !currentPageIds.has(item.userid)
  405. );
  406. this.selectedStuData = [...otherPageSelected, ...val];
  407. },
  408. checkSelectable(row) {
  409. return row.classid.indexOf(this.classInfo.id) == -1; // 状态为disabled的行禁用选择
  410. },
  411. handlePageChange(val) {
  412. this.page = val;
  413. this.getdata(this.cascaderDataPath[1]);
  414. },
  415. //获取年级列表
  416. getGrade() {
  417. this.boxLoading = true;
  418. let params = {
  419. oid: this.oid,
  420. };
  421. this.ajax
  422. .get(this.$store.state.api + "selectGradeNew", params)
  423. .then((res) => {
  424. this.boxLoading = false;
  425. let box3 = JSON.parse(JSON.stringify(res.data[0]));
  426. let box4 = JSON.parse(JSON.stringify(res.data[1]));
  427. box3.forEach((e) => {
  428. e.children = [];
  429. e.children = box4.filter((item) => item.pid == e.id);
  430. });
  431. this.cascaderData = box3;
  432. })
  433. .catch((err) => {
  434. this.boxLoading = false;
  435. console.error(err);
  436. });
  437. },
  438. getdata(cid) {
  439. this.tableLoading = true;
  440. let params = {
  441. oid: this.oid,
  442. cid: cid,
  443. inp: "",
  444. page: this.page,
  445. num: this.pageSize,
  446. };
  447. this.ajax
  448. .get(this.$store.state.api + "selectStudentManage", params)
  449. .then((res) => {
  450. this.total = res.data[0].length > 0 ? res.data[0][0].num : 0;
  451. this.allClaAllStuData = res.data[0];
  452. this.tableLoading = false;
  453. this.handleSelectedStuData(this.selectedStuData);
  454. })
  455. .catch((err) => {
  456. this.tableLoading = false;
  457. console.error(err);
  458. });
  459. },
  460. // 恢复表格选中状态
  461. handleSelectedStuData(val) {
  462. this.$nextTick(() => {
  463. if (this.$refs.tableData && val.length > 0) {
  464. const selectedIds = new Set(val.map((item) => item.userid));
  465. this.allClaAllStuData.forEach((row) => {
  466. if (selectedIds.has(row.userid)) {
  467. this.$refs.tableData.toggleRowSelection(row, true);
  468. }
  469. });
  470. }
  471. });
  472. },
  473. },
  474. mounted() {
  475. this.getGrade();
  476. },
  477. };
  478. </script>
  479. <style>
  480. .myCascader
  481. .el-cascader-panel
  482. .el-cascader-menu:first-child
  483. .el-scrollbar__wrap
  484. ul
  485. li
  486. label,
  487. .myCascader
  488. .el-cascader-panel
  489. .el-cascader-menu:first-child
  490. .el-scrollbar__wrap
  491. ul
  492. li
  493. .el-checkbox {
  494. display: none !important;
  495. }
  496. .myCascader .el-cascader-panel{
  497. width: 670px !important;
  498. }
  499. .myCascader .el-cascader-menu__list{
  500. width: 335px;
  501. }
  502. </style>
  503. <style scoped>
  504. .upBox >>> .el-dialog{
  505. border-radius: 8px !important;
  506. margin-top: 30vh !important;
  507. }
  508. .upBox >>> .el-dialog__body{
  509. padding: 10px 23px;
  510. }
  511. .dia_box_item {
  512. display: flex;
  513. align-items: center;
  514. gap: 10px;
  515. }
  516. .dia_box_item {
  517. display: flex;
  518. flex-direction: column;
  519. gap: 10px;
  520. }
  521. .addcla_box_item {
  522. min-height: 35px;
  523. /* line-height: 30px; */
  524. width: 100%;
  525. display: flex;
  526. align-items: center;
  527. gap: 10px;
  528. }
  529. .addcla_box_item >>> .el-cascader {
  530. width: 100%;
  531. }
  532. .addcla_box_item >>> .el-select {
  533. width: 100%;
  534. }
  535. .addcla_box_item >>> .el-input__inner {
  536. width: 100%;
  537. }
  538. .addcla_box_item >>> .el-input__icon {
  539. width: 100%;
  540. }
  541. .addcla_box_item > .addcla_box_item_span {
  542. width: 80px;
  543. flex-shrink: 0;
  544. text-align: right;
  545. }
  546. .tableList >>> .el-table__body-wrapper {
  547. height: 350px;
  548. }
  549. .cla_span {
  550. background: #e8f3ff;
  551. color: #0052d9;
  552. padding: 5px 10px;
  553. border-radius: 5px;
  554. box-sizing: border-box;
  555. }
  556. .search_stu_box {
  557. width: 670px;
  558. position: absolute;
  559. display: flex;
  560. flex-direction: column;
  561. top: 100%;
  562. left: 90px;
  563. z-index: 1000;
  564. min-height: 110px;
  565. background: #fff;
  566. border: 1px solid #e8f3ff;
  567. box-sizing: border-box;
  568. border-radius: 2px;
  569. box-shadow: 0px 5px 5px -3px rgba(0,0,0,0.1), 0px 8px 10px 1px rgba(0,0,0,0.06), 0px 3px 14px 2px rgba(0,0,0,0.05);
  570. }
  571. .search_stu_box_content {
  572. display: flex;
  573. gap: 10px;
  574. flex: 1;
  575. padding: 10px;
  576. flex-wrap: wrap;
  577. box-sizing: border-box;
  578. }
  579. .selected_stu_box_item {
  580. background: #e8f3ff;
  581. padding: 0px 10px;
  582. height: 30px;
  583. line-height: 30px;
  584. border-radius: 5px;
  585. box-sizing: border-box;
  586. }
  587. .search_stu_box_footer {
  588. display: flex;
  589. height: 40px;
  590. padding: 0 10px;
  591. box-sizing: border-box;
  592. justify-content: space-between;
  593. align-items: center;
  594. box-sizing: border-box;
  595. border-top: 1px #e7e7e7 solid;
  596. }
  597. .selected_stu_box {
  598. display: flex;
  599. flex-wrap: wrap;
  600. gap: 10px;
  601. }
  602. .page_box {
  603. display: flex;
  604. gap: 10px;
  605. width: 100%;
  606. align-items: center;
  607. }
  608. .cla_box {
  609. display: flex;
  610. flex-wrap: wrap;
  611. align-items: center;
  612. gap: 5px;
  613. }
  614. .dialog-footer {
  615. display: flex;
  616. justify-content: flex-end;
  617. gap: 10px;
  618. }
  619. .msgCon{
  620. display: flex;
  621. gap: 10px;
  622. align-items: flex-start;
  623. box-sizing: border-box;
  624. }
  625. .msgContxt{
  626. display: flex;
  627. flex-direction: column;
  628. gap: 10px;
  629. font-size: 16px;
  630. color: #000000;
  631. }
  632. .btnpop{
  633. width: 60px !important;
  634. height: 32px !important;
  635. display: flex;
  636. align-items: center;
  637. justify-content: center;
  638. background: #3681FC;
  639. }
  640. .kbtn{
  641. text-align: center;
  642. padding: 8px 14px;
  643. cursor: pointer;
  644. border-radius: 3px;
  645. box-sizing: border-box;
  646. font-size: 14px;
  647. }
  648. .dialog-footer {
  649. display: flex;
  650. justify-content: flex-end;
  651. gap: 10px;
  652. }
  653. </style>