index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. import { computed, defineComponent, onMounted, reactive, ref } from 'vue';
  2. import styles from './index.module.less';
  3. import MSticky from '@/components/m-sticky';
  4. import MHeader from '@/components/m-header';
  5. import icon_detail_bg from '../images/icon_detail_bg.png';
  6. import icon_music from '@common/images/icon-music.png';
  7. import {
  8. Button,
  9. Cell,
  10. CellGroup,
  11. Checkbox,
  12. CheckboxGroup,
  13. Field,
  14. Grid,
  15. GridItem,
  16. Image,
  17. Picker,
  18. Popup,
  19. showToast
  20. } from 'vant';
  21. import MStudent from '../component/m-student';
  22. import { IMusicGroup, IStudentDetail } from '../type';
  23. import Assignment from '../component/Assignment';
  24. import Attendance from '../component/Attendance';
  25. import icon_pen from '@common/images/icon_pen.png';
  26. import icon_phone from '../images/icon-phone.png';
  27. import icon_message from '../images/icon-message.png';
  28. import SkeletionDetail from './skeletion-detail';
  29. import { useRoute } from 'vue-router';
  30. import {
  31. api_organizationGetGradeList,
  32. api_studentManageQuitMusicGroup,
  33. api_studentManageUpdateGrade,
  34. api_studentManageUserDetail,
  35. api_studentManageUserMusicGroup
  36. } from '../api';
  37. export default defineComponent({
  38. name: 'student-manage-detail',
  39. setup() {
  40. const route = useRoute();
  41. const studentId: string | undefined =
  42. route.query?.studentId?.toString() || '';
  43. const musicGroupIds: string[] =
  44. route.query.musicGroupIds?.toString()?.split(',') || [];
  45. const detailData = reactive({
  46. skelet: true,
  47. /** 加载 */
  48. loading: false,
  49. /** 乐团 */
  50. groupShow: false,
  51. /** 退团 */
  52. quitShow: false,
  53. /** 确定退团 */
  54. quitConfirmShow: false,
  55. /** 联系方式 */
  56. cancelShow: false,
  57. /** 年级 */
  58. gradeShow: false,
  59. gradeOptions: [[], []] as any,
  60. musicGroup: [] as IMusicGroup[],
  61. musicGroupTitle: '全部乐团',
  62. musicGroupId: musicGroupIds[0] || '',
  63. student: {} as IStudentDetail,
  64. /** 年级列表 */
  65. gradeList: null as any,
  66. /** 退团列表 */
  67. quitList: [] as any[],
  68. /** 退团原因 */
  69. reason: '',
  70. quitLoading: false
  71. });
  72. const checkboxRefs = ref<any[]>([]);
  73. /** 获取学生乐团 */
  74. const getMusicGroup = () => {
  75. api_studentManageUserMusicGroup(studentId).then(res => {
  76. if (Array.isArray(res.data)) {
  77. detailData.musicGroup = res.data.map((item: any) => {
  78. return {
  79. text: item.name,
  80. value: item.id,
  81. gradeType: item.gradeType
  82. };
  83. });
  84. if (detailData.musicGroup.length === 1) {
  85. detailData.musicGroupTitle = detailData.musicGroup[0].text;
  86. }
  87. }
  88. });
  89. };
  90. /** 获取年级分布 */
  91. const getGradeList = () => {
  92. if (detailData.student.organId && detailData.musicGroup.length) {
  93. if (detailData.gradeList) return;
  94. console.log(detailData.musicGroup);
  95. const gradeType = Array.from(
  96. new Set(detailData.musicGroup.map(group => group.gradeType))
  97. ).join(',');
  98. console.log('🚀 ~ gradeType:', gradeType);
  99. api_organizationGetGradeList(
  100. detailData.student.organId,
  101. gradeType
  102. ).then(res => {
  103. detailData.gradeList = res.data;
  104. detailData.gradeOptions[0] = Object.entries(res.data).map(
  105. (value: any) => {
  106. return {
  107. text: value[1],
  108. value: value[0]
  109. };
  110. }
  111. );
  112. detailData.gradeOptions[1] = new Array(30)
  113. .fill(1)
  114. .map((_, index: number) => ({
  115. text: `${index + 1}班`,
  116. value: `${index + 1}班`
  117. }));
  118. });
  119. return;
  120. }
  121. setTimeout(() => {
  122. getGradeList();
  123. }, 30);
  124. };
  125. const getDatail = () => {
  126. detailData.loading = true;
  127. api_studentManageUserDetail({
  128. studentId: studentId,
  129. musicGroupId: detailData.musicGroupId || ''
  130. })
  131. .then(res => {
  132. if (res.data) {
  133. if (res.data.phone) {
  134. res.data.phone =
  135. res.data.phone.slice(0, 3) + '****' + res.data.phone.slice(-4);
  136. }
  137. detailData.student = res.data;
  138. getGradeList();
  139. }
  140. })
  141. .finally(() => {
  142. setTimeout(() => {
  143. detailData.loading = false;
  144. detailData.skelet = false;
  145. }, 500);
  146. });
  147. };
  148. onMounted(() => {
  149. getMusicGroup();
  150. getDatail();
  151. });
  152. const quitName = computed(() => {
  153. const text = detailData.musicGroup
  154. .filter(group => detailData.quitList.includes(group.value))
  155. .map(group => {
  156. return '“' + group.text + '”';
  157. })
  158. .join('、');
  159. return `${detailData.student.studentName}从${text}`;
  160. });
  161. /** 设置学生班级 */
  162. const handleSetGrade = async (selectedOptions: any[]) => {
  163. const res = await api_studentManageUpdateGrade({
  164. currentClass: selectedOptions[1].value, // 班级
  165. currentGrade: selectedOptions[0].text, // 年级
  166. currentGradeNum: selectedOptions[0].value, // 年级(数字表示)
  167. musicGroupId: detailData.musicGroupId, // 乐团ID
  168. studentId: detailData.student.studentId // 学生ID
  169. });
  170. console.log(res);
  171. if (res.code === 200) {
  172. showToast('修改成功');
  173. }
  174. getDatail();
  175. };
  176. /** 退团 */
  177. const handleQuite = async () => {
  178. if (!detailData.reason) {
  179. showToast('请填写退团原因');
  180. return;
  181. }
  182. detailData.quitLoading = true;
  183. try {
  184. const res = await api_studentManageQuitMusicGroup({
  185. musicGroupId: detailData.quitList.join(','),
  186. reason: detailData.reason,
  187. reasonEnum: 'OTHER',
  188. userId: detailData.student.studentId
  189. });
  190. detailData.quitConfirmShow = false;
  191. if (res.code === 200) {
  192. detailData.quitList = [];
  193. getDatail();
  194. }
  195. } catch (error) {}
  196. detailData.quitLoading = false;
  197. };
  198. /** 去聊天 */
  199. const openIm = () => {
  200. postMessage({
  201. api: 'joinChatGroup',
  202. content: {
  203. type: 'single', // single 单人 multi 多人
  204. id: detailData.student.studentId
  205. }
  206. });
  207. };
  208. /** 打电话 */
  209. const hanldeCallPhone = () => {
  210. postMessage({
  211. api: 'callPhone',
  212. content: {
  213. phone: detailData.student.phone
  214. }
  215. });
  216. };
  217. return () => (
  218. <div class={styles.studentDetail}>
  219. <Image class={styles.bg} src={icon_detail_bg} />
  220. <MSticky position="top">
  221. <MHeader background="transparent" />
  222. </MSticky>
  223. <SkeletionDetail loading={detailData.skelet}>
  224. <Cell
  225. class={styles.musicGroup}
  226. title={detailData.musicGroupTitle}
  227. isLink={detailData.musicGroup.length > 1 ? true : false}
  228. clickable={detailData.musicGroup.length > 1 ? true : false}
  229. center
  230. border={false}
  231. onClick={() => {
  232. if (detailData.musicGroup.length < 2) return;
  233. detailData.groupShow = true;
  234. }}>
  235. {{
  236. icon: () => <Image class={styles.iconMusic} src={icon_music} />
  237. }}
  238. </Cell>
  239. <div class={styles.box}>
  240. <MStudent
  241. item={detailData.student}
  242. valueType={
  243. detailData.student.inGroupStatus === 'OUT'
  244. ? 'statued'
  245. : detailData.student.inGroupStatus === 'APPLY_OUT'
  246. ? 'statuing'
  247. : 'status'
  248. }
  249. isLink={false}
  250. onQuit={() => (detailData.quitShow = true)}
  251. onContact={() => (detailData.cancelShow = true)}
  252. />
  253. </div>
  254. <div class={styles.infobox}>
  255. <div class={styles.attendanceTitle}>
  256. <span>基本信息</span>
  257. </div>
  258. <div class={styles.infoItem}>
  259. <div>性别</div>
  260. <div>{detailData.student.gender ? '男' : '女'}</div>
  261. </div>
  262. <div class={styles.infoItem}>
  263. <div>联系电话</div>
  264. <div>{detailData.student.phone}</div>
  265. </div>
  266. <div class={styles.infoItem}>
  267. <div>年级</div>
  268. <div
  269. class={styles.edit}
  270. onClick={() => {
  271. if (detailData.student.inGroupStatus === 'OUT') return;
  272. detailData.gradeShow = true;
  273. }}>
  274. {detailData.student.currentGrade}年级
  275. {detailData.student.currentClass}
  276. {detailData.student.inGroupStatus !== 'OUT' && (
  277. <Image class={styles.iconPen} src={icon_pen} />
  278. )}
  279. </div>
  280. </div>
  281. <div class={styles.infoItem}>
  282. <div>艺术实践</div>
  283. <div>{detailData.student.artPracticeCount}次</div>
  284. </div>
  285. {detailData.student.quitTime && (
  286. <div class={styles.infoItem}>
  287. <div>退团时间</div>
  288. <div style={{ color: '#FF5A56' }}>
  289. {detailData.student.quitTime}
  290. </div>
  291. </div>
  292. )}
  293. </div>
  294. <div class={styles.box}>
  295. <Assignment item={detailData.student} />
  296. </div>
  297. <div class={styles.box}>
  298. <Attendance item={detailData.student} />
  299. </div>
  300. </SkeletionDetail>
  301. {/* 切换乐团 */}
  302. <Popup v-model:show={detailData.groupShow} position="bottom" round>
  303. <Picker
  304. visibleOptionNum={5}
  305. columns={detailData.musicGroup}
  306. onCancel={() => (detailData.groupShow = false)}
  307. onConfirm={value => {
  308. const option = value.selectedOptions[0];
  309. const oldGroupId = detailData.musicGroupId;
  310. detailData.musicGroupId = option.value;
  311. detailData.musicGroupTitle = option.text;
  312. detailData.groupShow = false;
  313. if (oldGroupId != option.value) {
  314. getDatail();
  315. }
  316. }}
  317. />
  318. </Popup>
  319. {/* 联系方式 */}
  320. <Popup
  321. v-model:show={detailData.cancelShow}
  322. position="bottom"
  323. round
  324. closeable>
  325. <div class={styles.concatBox}>
  326. <div class={styles.concatTitle}>联系方式</div>
  327. <div class={styles.concatContent}>
  328. <Grid columnNum={2} border={false} center>
  329. <GridItem text="发送消息" onClick={openIm}>
  330. {{
  331. icon: () => (
  332. <Image class={styles.concatIcon} src={icon_message} />
  333. )
  334. }}
  335. </GridItem>
  336. <GridItem text="拨打电话" onClick={hanldeCallPhone}>
  337. {{
  338. icon: () => (
  339. <Image class={styles.concatIcon} src={icon_phone} />
  340. )
  341. }}
  342. </GridItem>
  343. </Grid>
  344. </div>
  345. </div>
  346. </Popup>
  347. {/* 设置班级 */}
  348. <Popup v-model:show={detailData.gradeShow} position="bottom" round>
  349. <Picker
  350. visibleOptionNum={5}
  351. columns={detailData.gradeOptions}
  352. onCancel={() => (detailData.gradeShow = false)}
  353. onConfirm={value => {
  354. detailData.gradeShow = false;
  355. handleSetGrade(value.selectedOptions);
  356. }}
  357. />
  358. </Popup>
  359. {/* 选择退团列表 */}
  360. <Popup
  361. v-model:show={detailData.quitShow}
  362. class={['popup-custom', 'van-scale']}
  363. transition="van-scale">
  364. <div class={styles.quitBox}>
  365. <div class={styles.quitTitle}>选择乐团</div>
  366. <div class={styles.quitDes}>请选择要退出的乐团:</div>
  367. <CheckboxGroup
  368. v-model:modelValue={detailData.quitList}
  369. class={styles.optionBox}>
  370. <CellGroup border={false}>
  371. {detailData.musicGroup.map(
  372. (group: IMusicGroup, index: number) => {
  373. return (
  374. <Cell
  375. class={[
  376. detailData.quitList.includes(group.value) &&
  377. styles.cellActive
  378. ]}
  379. title={group.text}
  380. center
  381. border={false}
  382. onClick={() => {
  383. checkboxRefs.value[index]?.toggle();
  384. }}>
  385. {{
  386. value: () => (
  387. <Checkbox
  388. ref={el => (checkboxRefs.value[index] = el)}
  389. shape="square"
  390. name={group.value}
  391. onClick={(e: Event) =>
  392. e.stopPropagation()
  393. }></Checkbox>
  394. )
  395. }}
  396. </Cell>
  397. );
  398. }
  399. )}
  400. </CellGroup>
  401. </CheckboxGroup>
  402. <div class={['btnGroupPopup']}>
  403. <Button round onClick={() => (detailData.quitShow = false)}>
  404. 取消
  405. </Button>
  406. <Button
  407. type="primary"
  408. round
  409. disabled={!detailData.quitList.length}
  410. onClick={() => {
  411. // detailData.quitShow = false
  412. detailData.quitConfirmShow = true;
  413. }}>
  414. 下一步
  415. </Button>
  416. </div>
  417. </div>
  418. </Popup>
  419. {/* 确定退团 */}
  420. <Popup
  421. v-model:show={detailData.quitConfirmShow}
  422. class={['popup-custom', 'van-scale']}
  423. transition="van-scale">
  424. <div class={styles.quitBox}>
  425. <div class={styles.quitTitle}>学员退团</div>
  426. <div class={styles.quitDes}>
  427. 确认要将学员
  428. <span style={{ color: '#FF5A56' }}>{quitName.value}</span>
  429. 中退团吗?
  430. </div>
  431. <div style={{ color: '#333' }} class={styles.quitLabel}>
  432. <span style={{ color: '#FF5A56' }}>*</span>退团原因:
  433. </div>
  434. <div class={styles.quitLabel}>
  435. <Field
  436. style={{ padding: 0 }}
  437. v-model={detailData.reason}
  438. type="textarea"
  439. rows={3}
  440. required
  441. placeholder="请填写退团原因"></Field>
  442. </div>
  443. <div class={styles.quitLabel}>
  444. 确认后,我们将在7个工作日内与学生联系退费事宜
  445. </div>
  446. <div class={['btnGroupPopup']}>
  447. <Button
  448. round
  449. onClick={() => (detailData.quitConfirmShow = false)}>
  450. 取消
  451. </Button>
  452. <Button
  453. loading={detailData.quitLoading}
  454. type="primary"
  455. round
  456. onClick={() => handleQuite()}>
  457. 确定
  458. </Button>
  459. </div>
  460. </div>
  461. </Popup>
  462. </div>
  463. );
  464. }
  465. });