liushengqiang 2 лет назад
Родитель
Сommit
84f6ec2f66

BIN
src/common/images/icon_pen.png


+ 22 - 0
src/styles/index.less

@@ -133,4 +133,26 @@ body {
   padding: 2px 4px;
   border-radius: 4px;
 
+}
+
+// 自定义动画基类
+.popup-custom{
+  transition: all 0.25s;
+  background: transparent;
+  overflow: initial;
+}
+.popup-custom.van-scale{
+  transform-origin: center -25%;
+}
+
+/* 缩放动画 */
+.van-scale-enter-from,
+.van-scale-leave-to {
+  opacity  : 0;
+  transform: scale(0.3);
+}
+
+.van-scale-enter-active,
+.van-scale-leave-active {
+  transition: all 0.25s;
 }

+ 47 - 0
src/views/student-manage/api.ts

@@ -40,3 +40,50 @@ export const api_studentManageCoopSubjectList = (musicGroupId?: string) => {
 export const api_cooperationOrganMusicGroupPage = () => {
   return request.get('/api-web/cooperationOrgan/musicGroupPage');
 };
+
+/** 学校端-学员详情 */
+export const api_studentManageUserDetail = (data: any) => {
+  return request.post('/api-web/studentManage/userDetail', { data });
+};
+/**
+ * 学校端-学生所在乐团
+ * @param studentId 学生ID
+ * @returns
+ */
+export const api_studentManageUserMusicGroup = (studentId: string) => {
+  return request.post('/api-web/studentManage/userMusicGroup', {
+    params: { studentId }
+  });
+};
+
+/**
+ * 获取用户所在分部的年级列表
+ * @param organId 机构ID
+ * @returns
+ */
+export const api_organizationGetGradeList = (
+  organId: string | number,
+  gradeType: string
+) => {
+  return request.get('/api-web/organization/getGradeList', {
+    params: { organId, gradeType }
+  });
+};
+
+/**
+ * 学校端-修改学员班级信息
+ * @param data 
+ * @returns 
+ */
+export const api_studentManageUpdateGrade = (data: any) => {
+  return request.post('/api-web/studentManage/updateGrade', { data });
+};
+
+/**
+ * 申请退团
+ * @param data 
+ * @returns 
+ */
+export const api_studentManageQuitMusicGroup = (data: any) => {
+  return request.post('/api-web/studentManage/quitMusicGroup', { data });
+};

+ 0 - 68
src/views/student-manage/component/drop-down-modal.tsx

@@ -1,68 +0,0 @@
-import { Button, Picker, PickerColumn } from 'vant';
-import { PropType, defineComponent, onMounted, reactive, watch } from 'vue';
-
-export default defineComponent({
-  name: 'drop-down-modal',
-  props: {
-    selectValues: {
-      type: [String, Number],
-      default: null
-    },
-    columns: {
-      type: Array as PropType<PickerColumn>,
-      default: () => []
-    },
-    open: {
-      type: Boolean,
-      default: false
-    }
-  },
-  emits: ['dropDownClose', 'dropDownConfirm'],
-  setup(props, { emit }) {
-    const forms = reactive({
-      values: [] as any
-    });
-
-    onMounted(() => {
-      forms.values = [props.selectValues];
-    });
-
-    watch(
-      () => props.selectValues,
-      () => {
-        forms.values = [props.selectValues];
-      }
-    );
-    watch(
-      () => props.open,
-      () => {
-        setTimeout(() => {
-          forms.values = [props.selectValues];
-        }, 100);
-      }
-    );
-    return () => (
-      <>
-        <Picker
-          v-model={forms.values}
-          showToolbar={false}
-          visibleOptionNum={5}
-          columns={props.columns}
-        />
-        <div class={['btnGroupPopup', 'van-hairline--top']}>
-          <Button round onClick={() => emit('dropDownClose')}>
-            取消
-          </Button>
-          <Button
-            type="primary"
-            round
-            onClick={() => {
-              emit('dropDownConfirm', forms.values);
-            }}>
-            确定
-          </Button>
-        </div>
-      </>
-    );
-  }
-});

+ 4 - 0
src/views/student-manage/component/m-student/icons.json

@@ -0,0 +1,4 @@
+{
+  "icon_im": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAAXNSR0IArs4c6QAABRJJREFUWEftmF1sU2Ucxp/nbYfL0IDxY1FRTEQu1PgRl6CDteuGjPY40BhHply4aMQLLiDGxJAogwu5MFEuNHEkfsSAEiHRZWu7IdC1hflFNBr0AjGRSMYkESSwgevp+zenOz1rS9lae0qC4b3qOef/Pu/vffp/P4krrPAK48VV4Gr/Y9M7vLe3HrNqOinSDmAhwHpAatyFYgqQPwEcEbIPE6lPsXSl9Vy0FAeOxbzg2AYSrwC81l3AmdTknAjehMx+A4GAWRh9MXDs87n0zNoFwdKZpKv6ndgr6YmnEXjy79x28oFjMS/V2ADA1pygE6JUD9Lcjxo9ggtyUa8rAq+lFyl1KzzSQq3XALjF0ctA1wVznc4HTkRep8imbAUKPtL/eNeirW2sIqhSKw8OzlbXmO8I8Vy2ipAb4Qttdpgcrb299azxHM3mbAa22egqtS0349RQ+MMpaDknqfSC7ECccjgRWUeRt+2GT8gF792XzdnC3g4Ozmat+Ws2PYRcD19oqxXmADMe3gegxXopSnWjKeikhpvulayVjG6k1t12/H7xG5lxlQv8B4B5GWDx+NC8PFmyeDUChwaayHTClj4ufuP2fOBEZAIyuSiIyQVoDf1WDY6SNQ9G7qIpRzPxZEp8oVmFDoszMk3ciVbjWMni1QjcF55PL353mPxGJhtyU+IqcEXGu+pwPPoqmV5NUd/omnNr0dhx3oE7EG6g5laIUCS9Ds0rvnO+xWK1Sp1/V4hFAtkBn7Hlkp1yDTgZXkSNr53cUngZTcZb2WcO9R8Gea/9/Iv4jexvIBFdT9FOrHj4KJaEHK08eNeAh8KPkdjjAAOb4Dey8yWYCB+D4I7C6ch6Von+zSJ8zanrYQhLQtGiLrsG3N2tVMui7SLpVQC/F6/XwOK2k06jifBTFL6fmR41XkAgtHsqJfpupPJEAGkg1XY9erYLHR3p6gJn1Q8dqkFDQ6poYyLKnj910e+xWC0CgQvTDkrXHK5o6JdR+f8IbO3s52Ryj+oB+II/leGH+6GJ6P0U/aMtfEb8xtzCle5nAPfYwJ3wBXe6T1GG4lC4k8QnhdOjszSrZPg90bCOKNaC3Ss+44ky5F0PZTzSC8iKDI5Cj24yXspzGPnzq4hWLQgEh1wnKUUwGW2m1vuzex0RLEOz8WU+sPU1Hj4IoNHWPClaWhF4/HApbbgWE+u/j4rWYeJm+98eFp+xOKuffwiNRx4i5QAEdXbAWaHaiDNmD9rbx12DKibU11eHOd41FG2ddK6zYcdFuAT+0A/Fga238XAHgR0AvDm64yC/Ijhibe8z70UmNLwfw982PG1HEhFrIC9TwHxA5uTsaO1q9Iro2wA8AjhGWd9MAZ6F3/gsV7/4zU9yYCm1uRPgDdPCEKdldOymosvrwT0P0kxZG51A+f8M/xKRzmzezgxsRST7r1dabRCgC5BLgHNEfMF5IJ3Nf0Y8Hn6GgLWfqC0T9hTJD7RXbUHj8lPF6s583Wrds6nxRhALIaxX9mWgFk7AI7vRZBzJE45Hnyf1Nggm9xOT5VtChgGeKYTQgAliFIIj0HXDxe7TSnO4TGsmnb0IdlSoVsMXtEa9K2Vmh0ttxoKF3mZte+0qx0V7Aggsnzz5ulTcAb5MsFafKwe+jLCVAycj7dTyRX4amAEEVrqaBq4NOhWP9AjkxamcrS6sCw4PPkxt7gLktOj0qmo6m3W58hx2afSXKnMVuFSn/mvcv42YPUsbaK71AAAAAElFTkSuQmCC",
+  "icon_im_dis": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAAAXNSR0IArs4c6QAABQVJREFUWEftmF1oHUUUx8+Zm5uUapIaP4rfAWsfqon3zmyI1g/SDwUVRRCVaB8URX3oQ4sIUtC0PtgHQfugYAS1SKti+2ARVKwNwY/EmD2zNxfTh1ghYkm1oDWJxpDbzJG57F42N7fJtnc3EOk87eye+c9vzp4zXwjLrOAy44XzwEn/sQU93N7evrpQKHQi4n0AsBYAVgNAOmaoAgD8DgAjzPxpOp3+cGBgwNYrlorAHR0dNZOTkzuY+XlEvDBmwAXlmPlvRHy1vr7+ld7e3tPlxvOAM5nMKiHEAUTcvJSg5X0x81fGmIdyudxf4W9zgK1nJyYmvkDETSGjE8zcDQA9iDhmjJk36moGJoSoYeYrAGAjIj4DAJcHeha6oaHh7rCn5wArpV4CgF0hgL2FQmFrPp//pxqoqG1bW1svSKfTbwDA46E2XUT0clAvAfsJdiwUs3uJ6ImoncVpp5R6L4C2MZ1Op9cEiVgCllJuQ8TX/Y5PFAqF65fKs+WD9T39UxAezLxda73H2pWAlVJHbBzZl8y8U2sdDo04HRhJS0rZhYg7feMeIirmVRj4VwC4yge+Q2v9TSTlhIyklLcj4te+/HEiuroceCa0KKwhop8TYokkq5S6DgCO+cYFIqotB+ZAyRjT7HneL5GUEzLKZrPXCiFGA3kiKkZDOCTOA1fj/Fg9rJR6AQC2MPNAXV3d1v7+/n8DuGw26wgh9jAzGmO25XK5weBbc3PziqampjcRsR0A9hPR7jMNKjZgpZTt7PtQR88R0WtBXSn1IwDc4M82R7XWxWdbHMfZzswlW2a+RWsd1irJxgbsOM6dzPxloMzMu7TWwXwJSimbrNeUT0e2rpSyS+yLoeS+x/O8zyt5OTZgABBKqX3M/IgQQs/MzNybz+dPhrz4IDO/U8xoxKdc1z0Y8v4lzPwZIjqIuM91Xbv0zyYNXNRXSqWJyG6+KxXhvzSVPtpYHh0dnV4oKeP0cDXJH7nt/xLY7uwb/ey+SWudj+yOBAyllK2IOORLjxPRqmJehBJiGADW2boxptPzvI8S4Igs6ThOJzN/4Dc4SkTF6TG8H37LP6LY94eI6IHI6gkYSikPIeL9/h/v1lo/Owe4bH5lRNzoum5vAiyLSjqO08HMPYFDEfEu13UPzwG2FSnld4i43h/VSSHEJtd17cq1ZMVxnBuNMUcQ8TKfo09rfWsAMOcQ6jhOlpm/BYCVvsEkInbZUzMRTSVJrZRaaUPSrpwAUO/3NYWIt7mu61UE9r38MCLuB4CaEOAUM/cDwJgQonjMN8bMpFKp9wcHB/sWGkg2m11nfykA2P1tcRYKF2NMDSJeCQA3hxxlTU4z82Na64/D9hVvftra2jYbY+wscfEiXj1FRJdWWl4zmUwmlUrZjc6Gc/gzfyBiZxC3iwJbg5aWlotqa2t3AIBd788EPkZE9hxY2vzbtkqpRwHA7idWnCXsnwDw7vT09O7h4WH7PK8set1qb4PGx8fXCyHWImLpMtCGxOzs7MGhoaGRsKrjOE8aY95GxGA/YT//wMx9QojxcgJmtiH2mzFmpLGxsa/SfVokD5+lZ4rmFWAtyBbP8+wVQixlUQ9H7aUC7HFm3qC1Dk6+UaUWtIsFeKlg7UiqBl5K2KqBpZT2Zv6TUIIlEgaxJZ2UshsRn/YFE4eNw8MKEQ8AwCl7xos7wc5pHo4ltWMUqTrpYmSJJLXsgP8DfnSRSweZ4IsAAAAASUVORK5CYII="
+}

+ 27 - 6
src/views/student-manage/component/m-student/index.module.less

@@ -19,7 +19,10 @@
             border: none ;
         }
     }
-
+    .title{
+        display: flex;
+        align-items: center;
+    }
     .iconTeacher {
         width: 48px;
         height: 48px;
@@ -32,14 +35,27 @@
     .statusBox{
         display: flex;
         justify-content: flex-end;
+        & > div{
+            font-size: 14px;
+            color: #fff;
+            border-radius: 14px;
+            line-height: 20px;
+            padding: 3px 16px;
+            &:active{
+                opacity: .8;
+            }
+        }
     }
     .status{
-        font-size: 14px;
-        color: #fff;
+        background: var(--van-primary-color);
+    }
+    .statuing{
         background-color: #C1C1C1;
-        border-radius: 14px;
-        line-height: 20px;
-        padding: 3px 16px;
+        pointer-events: none;
+    }
+    .statued{
+        color: #FF5A56;
+        pointer-events: none;
     }
 }
 .studentInfo{
@@ -48,4 +64,9 @@
             flex: 3;
         }
     }
+}
+.iconIm{
+    width: 22px;
+    height: 22px;
+    margin-left: 6px;
 }

+ 54 - 10
src/views/student-manage/component/m-student/index.tsx

@@ -4,31 +4,49 @@ import icon_student_man from '@/common/images/icon-student-default.png';
 import icon_vip from '@/common/images/icon-vip.png';
 import styles from './index.module.less';
 import { IStudentManage } from '../../type';
+import { useRouter } from 'vue-router';
+import icons from './icons.json';
 
 export default defineComponent({
   name: 'm-student',
   props: {
     valueType: {
-      type: String as PropType<'status'>,
+      type: String as PropType<'status' | 'statuing' | 'statued'>,
       default: ''
     },
+    isLink: {
+      type: Boolean,
+      default: true
+    },
     item: {
       type: Object as PropType<IStudentManage>,
       default: () => ({})
     }
   },
-  setup(props) {
-    const {item} = toRefs(props);
+  emits: ['quit', 'contact'],
+  setup(props, { emit }) {
+    const router = useRouter();
+    const { item, isLink } = toRefs(props);
     const valueType = props.valueType;
     return () => (
       <Cell
-        class={[styles.student, valueType === 'status' ? '' : styles.studentInfo]}
+        class={[
+          styles.student,
+          valueType === 'status' ? '' : styles.studentInfo
+        ]}
         center
         border={false}
-        title={item.value.studentName}
-        label={'丁曼杰' + item.value.vipFlag}
-        isLink
-        onClick={() => {}}>
+        label={item.value.subjectName}
+        isLink={isLink.value}
+        onClick={() => {
+          router.push({
+            path: '/student-manage-detail',
+            query: {
+              studentId: item.value.studentId,
+              musicGroupIds: item.value.musicGroupIds
+            }
+          });
+        }}>
         {{
           icon: () => (
             <Badge offset={[-14, 10]}>
@@ -50,11 +68,37 @@ export default defineComponent({
               }}
             </Badge>
           ),
+          title: () => (
+            <div
+              class={styles.title}
+              onClick={() => {
+                if (valueType === 'statued') return;
+                console.log('去聊天');
+                emit('contact')
+              }}>
+              {item.value.studentName}{' '}
+              {valueType === 'statued' ? (
+                <Image class={styles.iconIm} src={icons.icon_im_dis} />
+              ) : (
+                <Image class={styles.iconIm} src={icons.icon_im} />
+              )}
+            </div>
+          ),
           value: () => (
             <>
-              {valueType === 'status' && (
+              {valueType === 'status' ? (
+                <div class={styles.statusBox}>
+                  <div class={styles.status} onClick={() => emit('quit')}>
+                    退团
+                  </div>
+                </div>
+              ) : valueType === 'statuing' ? (
+                <div class={styles.statusBox}>
+                  <div class={styles.statuing}>退团中</div>
+                </div>
+              ) : (
                 <div class={styles.statusBox}>
-                  <div class={styles.status}>退团中</div>
+                  <div class={styles.statued}>已退团</div>
                 </div>
               )}
             </>

+ 196 - 0
src/views/student-manage/detail/index.module.less

@@ -0,0 +1,196 @@
+.studentDetail {
+    padding: 12px 13px;
+}
+
+.bg {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+}
+
+.musicGroup {
+    border-radius: 22px;
+    margin-bottom: 16px;
+
+    .iconMusic {
+        width: 18px;
+        height: 19px;
+        margin-right: 4px;
+    }
+
+    :global {
+        .van-cell__title {
+            font-weight: 500;
+            font-size: 15px;
+            color: #333;
+            line-height: 21px;
+        }
+
+        .van-cell__right-icon {
+            transform: rotate(90deg);
+            color: #333;
+            font-weight: bold;
+            font-size: 14px;
+        }
+    }
+}
+
+.box {
+    position: relative;
+    background: #fff;
+    border-radius: 10px;
+    z-index: 1;
+    margin-bottom: 12px;
+}
+
+.infobox {
+    padding: 16px 12px;
+    position: relative;
+    background: #fff;
+    border-radius: 10px;
+    z-index: 1;
+    margin-bottom: 12px;
+
+    .infoItem {
+        display: flex;
+        justify-content: space-between;
+        font-size: 15px;
+        line-height: 21px;
+        color: #777;
+        padding-bottom: 20px;
+
+        &>div:last-child {
+            color: #333;
+        }
+    }
+
+    .infoItem:last-child {
+        padding-bottom: 0;
+    }
+
+    .edit {
+        display: flex;
+        align-items: center;
+
+        .iconPen {
+            width: 14px;
+            height: 15px;
+            margin-left: 6px;
+        }
+    }
+}
+
+.attendanceTitle {
+    position: relative;
+    font-size: 14px;
+    line-height: 20px;
+    font-weight: bold;
+    color: #333;
+    margin-bottom: 12px;
+
+    &>span {
+        position: relative;
+        z-index: 1;
+    }
+
+    &::before {
+        content: '';
+        position: absolute;
+        left: 0;
+        bottom: 0;
+        width: 70px;
+        height: 8px;
+        background: #FFB4B4;
+    }
+}
+
+.quitBox {
+    width: calc(100vw - 60px);
+    border-radius: 12px;
+    background: #fff;
+
+}
+
+.quitTitle {
+    font-size: 18px;
+    font-weight: 600;
+    line-height: 25px;
+    text-align: center;
+    padding: 20px;
+}
+
+.quitDes {
+    font-size: 16px;
+    line-height: 26px;
+    padding: 10px 20px;
+}
+
+.quitLabel {
+    font-size: 14px;
+    color: #777777;
+    line-height: 26px;
+    padding: 10px 20px 0 20px;
+}
+
+.optionBox {
+    padding: 0 20px;
+
+    :global {
+        .van-cell {
+            margin: 5px 0;
+        }
+
+        .van-cell__value {
+            display: flex;
+            align-items: center;
+            justify-content: flex-end;
+
+            .van-checkbox {
+                .van-badge__wrapper {
+                    border-radius: 2px;
+                }
+            }
+        }
+    }
+}
+
+.cellActive {
+    border-radius: 10px;
+    background: #F6F6F6;
+}
+
+.concatBox {
+    .concatTitle {
+        position: relative;
+        font-size: 16px;
+        font-weight: 500;
+        color: #333333;
+        line-height: 22px;
+        padding: 15px 25px;
+
+        &::before {
+            content: '';
+            position: absolute;
+            left: 15px;
+            top: 50%;
+            transform: translateY(-50%);
+            width: 4px;
+            height: 12px;
+            background: #01C1B5;
+            border-radius: 2px;
+        }
+    }
+}
+:global{
+    .van-popup__close-icon{
+        color: #333;
+    }
+}
+.concatContent{
+    padding: 0 20px 50px 20px;
+    .concatIcon{
+        width: 47px;
+        height: 47px;
+    }
+}

+ 485 - 6
src/views/student-manage/detail/index.tsx

@@ -1,8 +1,487 @@
-import { defineComponent } from "vue";
+import { computed, defineComponent, onMounted, reactive, ref } from 'vue';
+import styles from './index.module.less';
+import MSticky from '@/components/m-sticky';
+import MHeader from '@/components/m-header';
+import icon_detail_bg from '../images/icon_detail_bg.png';
+import icon_music from '@common/images/icon-music.png';
+import {
+  Button,
+  Cell,
+  CellGroup,
+  Checkbox,
+  CheckboxGroup,
+  Field,
+  Grid,
+  GridItem,
+  Image,
+  Picker,
+  Popup,
+  showToast
+} from 'vant';
+import MStudent from '../component/m-student';
+import { IMusicGroup, IStudentDetail } from '../type';
+import Assignment from '../component/Assignment';
+import Attendance from '../component/Attendance';
+import icon_pen from '@common/images/icon_pen.png';
+import icon_phone from '../images/icon-phone.png';
+import icon_message from '../images/icon-message.png';
+import SkeletionDetail from './skeletion-detail';
+import { useRoute } from 'vue-router';
+import {
+  api_organizationGetGradeList,
+  api_studentManageQuitMusicGroup,
+  api_studentManageUpdateGrade,
+  api_studentManageUserDetail,
+  api_studentManageUserMusicGroup
+} from '../api';
 
 export default defineComponent({
-    name: 'student-manage-detail',
-    setup(){
-        return () => <div></div>
-    }
-})
+  name: 'student-manage-detail',
+  setup() {
+    const route = useRoute();
+    const studentId: string | undefined =
+      route.query?.studentId?.toString() || '';
+    const musicGroupIds: string[] =
+      route.query.musicGroupIds?.toString()?.split(',') || [];
+    const detailData = reactive({
+      skelet: true,
+      /** 加载 */
+      loading: false,
+      /** 乐团 */
+      groupShow: false,
+      /** 退团 */
+      quitShow: false,
+      /** 确定退团 */
+      quitConfirmShow: false,
+      /** 联系方式 */
+      cancelShow: false,
+      /** 年级 */
+      gradeShow: false,
+      gradeOptions: [[], []] as any,
+      musicGroup: [] as IMusicGroup[],
+      musicGroupTitle: '全部乐团',
+      musicGroupId: musicGroupIds[0] || '',
+      student: {} as IStudentDetail,
+      /** 年级列表 */
+      gradeList: null as any,
+      /** 退团列表 */
+      quitList: [] as any[],
+      /** 退团原因 */
+      reason: '',
+      quitLoading: false
+    });
+    const checkboxRefs = ref<any[]>([]);
+    /** 获取学生乐团 */
+    const getMusicGroup = () => {
+      api_studentManageUserMusicGroup(studentId).then(res => {
+        if (Array.isArray(res.data)) {
+          detailData.musicGroup = res.data.map((item: any) => {
+            return {
+              text: item.name,
+              value: item.id,
+              gradeType: item.gradeType
+            };
+          });
+          if (detailData.musicGroup.length === 1) {
+            detailData.musicGroupTitle = detailData.musicGroup[0].text;
+          }
+        }
+      });
+    };
+
+    /** 获取年级分布 */
+    const getGradeList = () => {
+      if (detailData.student.organId && detailData.musicGroup.length) {
+        if (detailData.gradeList) return;
+        console.log(detailData.musicGroup);
+        const gradeType = Array.from(
+          new Set(detailData.musicGroup.map(group => group.gradeType))
+        ).join(',');
+        console.log('🚀 ~ gradeType:', gradeType);
+        api_organizationGetGradeList(
+          detailData.student.organId,
+          gradeType
+        ).then(res => {
+          detailData.gradeList = res.data;
+          detailData.gradeOptions[0] = Object.entries(res.data).map(
+            (value: any) => {
+              return {
+                text: value[1],
+                value: value[0]
+              };
+            }
+          );
+          detailData.gradeOptions[1] = new Array(30)
+            .fill(1)
+            .map((_, index: number) => ({
+              text: `${index + 1}班`,
+              value: `${index + 1}班`
+            }));
+        });
+        return;
+      }
+
+      setTimeout(() => {
+        getGradeList();
+      }, 30);
+    };
+
+    const getDatail = () => {
+      detailData.loading = true;
+
+      api_studentManageUserDetail({
+        studentId: studentId,
+        musicGroupId: detailData.musicGroupId || ''
+      })
+        .then(res => {
+          if (res.data) {
+            if (res.data.phone) {
+              res.data.phone =
+                res.data.phone.slice(0, 3) + '****' + res.data.phone.slice(-4);
+            }
+            detailData.student = res.data;
+            getGradeList();
+          }
+        })
+        .finally(() => {
+          setTimeout(() => {
+            detailData.loading = false;
+            detailData.skelet = false;
+          }, 500);
+        });
+    };
+    onMounted(() => {
+      getMusicGroup();
+      getDatail();
+    });
+
+    const quitName = computed(() => {
+      const text = detailData.musicGroup
+        .filter(group => detailData.quitList.includes(group.value))
+        .map(group => {
+          return '“' + group.text + '”';
+        })
+        .join('、');
+      return `${detailData.student.studentName}从${text}`;
+    });
+
+    /** 设置学生班级 */
+    const handleSetGrade = async (selectedOptions: any[]) => {
+      const res = await api_studentManageUpdateGrade({
+        currentClass: selectedOptions[1].value, // 班级
+        currentGrade: selectedOptions[0].text, // 年级
+        currentGradeNum: selectedOptions[0].value, // 年级(数字表示)
+        musicGroupId: detailData.musicGroupId, // 乐团ID
+        studentId: detailData.student.studentId // 学生ID
+      });
+      console.log(res);
+      if (res.code === 200) {
+        showToast('修改成功');
+      }
+      getDatail();
+    };
+
+    /** 退团 */
+    const handleQuite = async () => {
+      if (!detailData.reason) {
+        showToast('请填写退团原因');
+        return;
+      }
+      detailData.quitLoading = true;
+      try {
+        const res = await api_studentManageQuitMusicGroup({
+          musicGroupId: detailData.quitList.join(','),
+          reason: detailData.reason,
+          reasonEnum: 'OTHER',
+          userId: detailData.student.studentId
+        });
+        detailData.quitConfirmShow = false;
+        if (res.code === 200) {
+          detailData.quitList = [];
+          getDatail();
+        }
+      } catch (error) {}
+      detailData.quitLoading = false;
+    };
+
+    /** 去聊天 */
+    const openIm = () => {
+      postMessage({
+        api: 'joinChatGroup',
+        content: {
+          type: 'single', // single 单人 multi 多人
+          id: detailData.student.studentId
+        }
+      });
+    };
+
+    /** 打电话 */
+    const hanldeCallPhone = () => {
+      postMessage({
+        api: 'callPhone',
+        content: {
+          phone: detailData.student.phone
+        }
+      });
+    };
+    return () => (
+      <div class={styles.studentDetail}>
+        <Image class={styles.bg} src={icon_detail_bg} />
+        <MSticky position="top">
+          <MHeader background="transparent" />
+        </MSticky>
+
+        <SkeletionDetail loading={detailData.skelet}>
+          <Cell
+            class={styles.musicGroup}
+            title={detailData.musicGroupTitle}
+            isLink={detailData.musicGroup.length > 1 ? true : false}
+            clickable={detailData.musicGroup.length > 1 ? true : false}
+            center
+            border={false}
+            onClick={() => {
+              if (detailData.musicGroup.length < 2) return;
+              detailData.groupShow = true;
+            }}>
+            {{
+              icon: () => <Image class={styles.iconMusic} src={icon_music} />
+            }}
+          </Cell>
+
+          <div class={styles.box}>
+            <MStudent
+              item={detailData.student}
+              valueType={
+                detailData.student.inGroupStatus === 'OUT'
+                  ? 'statued'
+                  : detailData.student.inGroupStatus === 'APPLY_OUT'
+                  ? 'statuing'
+                  : 'status'
+              }
+              isLink={false}
+              onQuit={() => (detailData.quitShow = true)}
+              onContact={() => (detailData.cancelShow = true)}
+            />
+          </div>
+
+          <div class={styles.infobox}>
+            <div class={styles.attendanceTitle}>
+              <span>基本信息</span>
+            </div>
+            <div class={styles.infoItem}>
+              <div>性别</div>
+              <div>{detailData.student.gender ? '男' : '女'}</div>
+            </div>
+            <div class={styles.infoItem}>
+              <div>联系电话</div>
+              <div>{detailData.student.phone}</div>
+            </div>
+            <div class={styles.infoItem}>
+              <div>年级</div>
+              <div
+                class={styles.edit}
+                onClick={() => {
+                  if (detailData.student.inGroupStatus === 'OUT') return;
+                  detailData.gradeShow = true;
+                }}>
+                {detailData.student.currentGrade}年级
+                {detailData.student.currentClass}
+                {detailData.student.inGroupStatus !== 'OUT' && (
+                  <Image class={styles.iconPen} src={icon_pen} />
+                )}
+              </div>
+            </div>
+            <div class={styles.infoItem}>
+              <div>艺术实践</div>
+              <div>{detailData.student.artPracticeCount}次</div>
+            </div>
+            {detailData.student.quitTime && (
+              <div class={styles.infoItem}>
+                <div>退团时间</div>
+                <div style={{ color: '#FF5A56' }}>
+                  {detailData.student.quitTime}
+                </div>
+              </div>
+            )}
+          </div>
+
+          <div class={styles.box}>
+            <Assignment item={detailData.student} />
+          </div>
+
+          <div class={styles.box}>
+            <Attendance item={detailData.student} />
+          </div>
+        </SkeletionDetail>
+
+        {/* 切换乐团 */}
+        <Popup v-model:show={detailData.groupShow} position="bottom" round>
+          <Picker
+            visibleOptionNum={5}
+            columns={detailData.musicGroup}
+            onCancel={() => (detailData.groupShow = false)}
+            onConfirm={value => {
+              const option = value.selectedOptions[0];
+              const oldGroupId = detailData.musicGroupId;
+              detailData.musicGroupId = option.value;
+              detailData.musicGroupTitle = option.text;
+              detailData.groupShow = false;
+              if (oldGroupId != option.value) {
+                getDatail();
+              }
+            }}
+          />
+        </Popup>
+
+        {/* 联系方式 */}
+        <Popup
+          v-model:show={detailData.cancelShow}
+          position="bottom"
+          round
+          closeable>
+          <div class={styles.concatBox}>
+            <div class={styles.concatTitle}>联系方式</div>
+            <div class={styles.concatContent}>
+              <Grid columnNum={2} border={false} center>
+                <GridItem text="发送消息" onClick={openIm}>
+                  {{
+                    icon: () => (
+                      <Image class={styles.concatIcon} src={icon_message} />
+                    )
+                  }}
+                </GridItem>
+                <GridItem text="拨打电话" onClick={hanldeCallPhone}>
+                  {{
+                    icon: () => (
+                      <Image class={styles.concatIcon} src={icon_phone} />
+                    )
+                  }}
+                </GridItem>
+              </Grid>
+            </div>
+          </div>
+        </Popup>
+
+        {/* 设置班级 */}
+        <Popup v-model:show={detailData.gradeShow} position="bottom" round>
+          <Picker
+            visibleOptionNum={5}
+            columns={detailData.gradeOptions}
+            onCancel={() => (detailData.gradeShow = false)}
+            onConfirm={value => {
+              detailData.gradeShow = false;
+              handleSetGrade(value.selectedOptions);
+            }}
+          />
+        </Popup>
+
+        {/* 选择退团列表 */}
+        <Popup
+          v-model:show={detailData.quitShow}
+          class={['popup-custom', 'van-scale']}
+          transition="van-scale">
+          <div class={styles.quitBox}>
+            <div class={styles.quitTitle}>选择乐团</div>
+            <div class={styles.quitDes}>请选择要退出的乐团:</div>
+            <CheckboxGroup
+              v-model:modelValue={detailData.quitList}
+              class={styles.optionBox}>
+              <CellGroup border={false}>
+                {detailData.musicGroup.map(
+                  (group: IMusicGroup, index: number) => {
+                    return (
+                      <Cell
+                        class={[
+                          detailData.quitList.includes(group.value) &&
+                            styles.cellActive
+                        ]}
+                        title={group.text}
+                        center
+                        border={false}
+                        onClick={() => {
+                          checkboxRefs.value[index]?.toggle();
+                        }}>
+                        {{
+                          value: () => (
+                            <Checkbox
+                              ref={el => (checkboxRefs.value[index] = el)}
+                              shape="square"
+                              name={group.value}
+                              onClick={(e: Event) =>
+                                e.stopPropagation()
+                              }></Checkbox>
+                          )
+                        }}
+                      </Cell>
+                    );
+                  }
+                )}
+              </CellGroup>
+            </CheckboxGroup>
+            <div class={['btnGroupPopup']}>
+              <Button round onClick={() => (detailData.quitShow = false)}>
+                取消
+              </Button>
+              <Button
+                type="primary"
+                round
+                disabled={!detailData.quitList.length}
+                onClick={() => {
+                  // detailData.quitShow = false
+                  detailData.quitConfirmShow = true;
+                }}>
+                下一步
+              </Button>
+            </div>
+          </div>
+        </Popup>
+
+        {/* 确定退团 */}
+        <Popup
+          v-model:show={detailData.quitConfirmShow}
+          class={['popup-custom', 'van-scale']}
+          transition="van-scale">
+          <div class={styles.quitBox}>
+            <div class={styles.quitTitle}>学员退团</div>
+            <div class={styles.quitDes}>
+              确认要将学员
+              <span style={{ color: '#FF5A56' }}>{quitName.value}</span>
+              中退团吗?
+            </div>
+            <div style={{ color: '#333' }} class={styles.quitLabel}>
+              <span style={{ color: '#FF5A56' }}>*</span>退团原因:
+            </div>
+            <div class={styles.quitLabel}>
+              <Field
+                style={{ padding: 0 }}
+                v-model={detailData.reason}
+                type="textarea"
+                rows={3}
+                required
+                placeholder="请填写退团原因"></Field>
+            </div>
+
+            <div class={styles.quitLabel}>
+              确认后,我们将在7个工作日内与学生联系退费事宜
+            </div>
+
+            <div class={['btnGroupPopup']}>
+              <Button
+                round
+                onClick={() => (detailData.quitConfirmShow = false)}>
+                取消
+              </Button>
+              <Button
+                loading={detailData.quitLoading}
+                type="primary"
+                round
+                onClick={() => handleQuite()}>
+                确定
+              </Button>
+            </div>
+          </div>
+        </Popup>
+      </div>
+    );
+  }
+});

+ 103 - 0
src/views/student-manage/detail/skeletion-detail.tsx

@@ -0,0 +1,103 @@
+import { defineComponent } from 'vue';
+import styles from './index.module.less';
+import {
+  Cell,
+  CellGroup,
+  Grid,
+  GridItem,
+  Skeleton,
+  SkeletonAvatar,
+  SkeletonParagraph
+} from 'vant';
+
+export default defineComponent({
+  name: 'student-manage-detail-skeletion',
+  props: {
+    loading: {
+      type: Boolean,
+      default: true
+    }
+  },
+  setup(props, { slots }) {
+    return () => (
+      <Skeleton loading={props.loading}>
+        {{
+          template: () => (
+            <div
+              style={{
+                width: '100%',
+                height: 'calc(100vh - 30px - var(--header-height))',
+                overflow: 'hidden'
+              }}>
+              <CellGroup class={styles.infobox} border={false}>
+                <Cell border={false} center style={{padding: 0}}>
+                  {{
+                    icon: () => <SkeletonAvatar avatarSize={'1rem'} />,
+                    title: () => <SkeletonParagraph rowWidth="80%" />,
+                  }}
+                </Cell>
+              </CellGroup>
+
+              <CellGroup class={styles.infobox} border={false}>
+                <Cell border={false} center style={{padding: 0}}>
+                  {{
+                    icon: () => <SkeletonAvatar avatarSize={'1rem'} />,
+                    title: () => <SkeletonParagraph rowWidth="80%" />,
+                    label: () => <SkeletonParagraph rowWidth="40%" />,
+                    value: () => (
+                      <SkeletonParagraph
+                        rowWidth="80%"
+                        style={{ marginLeft: 'auto' }}
+                      />
+                    )
+                  }}
+                </Cell>
+              </CellGroup>
+
+              <CellGroup class={styles.infobox}border={false}>
+                {new Array(4).fill(1).map(_item => (
+                  <Cell border={false}>
+                    {{
+                      title: () => <SkeletonParagraph rowWidth="80%" />,
+                      value: () => (
+                        <SkeletonParagraph
+                          rowWidth="80%"
+                          style={{ marginLeft: 'auto' }}
+                        />
+                      )
+                    }}
+                  </Cell>
+                ))}
+              </CellGroup>
+
+              <div style={{ overflow: 'hidden' }} class={styles.infobox}>
+                <SkeletonParagraph rowWidth="30%" />
+                <Grid border={false}>
+                  {new Array(4).fill(1).map(n => (
+                    <GridItem>
+                      <SkeletonParagraph rowWidth="90%" />
+                      <SkeletonParagraph rowWidth="90%" />
+                    </GridItem>
+                  ))}
+                </Grid>
+              </div>
+
+              <div style={{ overflow: 'hidden' }} class={styles.infobox}>
+                <SkeletonParagraph rowWidth="30%" />
+                <Grid border={false}>
+                  {new Array(4).fill(1).map(n => (
+                    <GridItem>
+                      <SkeletonParagraph rowWidth="90%" />
+                      <SkeletonParagraph rowWidth="90%" />
+                    </GridItem>
+                  ))}
+                </Grid>
+              </div>
+            </div>
+          ),
+          default: () => slots.default?.()
+        }}
+      </Skeleton>
+    );
+  }
+});

BIN
src/views/student-manage/images/icon-message.png


BIN
src/views/student-manage/images/icon-phone.png


BIN
src/views/student-manage/images/icon_detail_bg.png


+ 0 - 1
src/views/student-manage/index.tsx

@@ -27,7 +27,6 @@ import {
 } from './api';
 import MFullRefresh from '@/components/m-full-refresh';
 import { IMusicGroup, IStudentManage, ISubject } from './type';
-import DropDownModal from './component/drop-down-modal';
 import MEmpty from '@/components/m-empty';
 
 export default defineComponent({

+ 29 - 0
src/views/student-manage/type.ts

@@ -30,6 +30,10 @@ export interface IStudentManage {
   unsubmitCount: number;
   /** 是否vip */
   vipFlag: boolean;
+  /** 声部名称 */
+  subjectName: string;
+  /** 乐团ID */
+  musicGroupIds: string;
 }
 
 /** 乐团 */
@@ -38,6 +42,8 @@ export interface IMusicGroup {
   text: string;
   /** 乐团名称 */
   value: string;
+  /** 乐团类型 */
+  gradeType: string
 }
 
 /** 声部 */
@@ -45,3 +51,26 @@ export interface ISubject {
   text: string;
   value: string;
 }
+
+export interface IStudentDetail extends IStudentManage {
+  /** 艺术实践次数 */
+  artPracticeCount: number;
+  /** 班级 */
+  currentClass: string;
+  /** 年级 */
+  currentGrade: string;
+  /** 年级(数字表示) */
+  currentGradeNum: number;
+  /** 性别0女1男 */
+  gender: number;
+  /** 在团状态 在团 IN 退团 OUT 申请退团 APPLY_OUT 报名:APPLY 休学:QUIT_SCHOOL,可用值:IN,OUT,QUIT_SCHOOL,APPLY,APPLY_OUT */
+  inGroupStatus: 'IN' | 'OUT' | 'APPLY_OUT' | 'APPLY' | 'QUIT_SCHOOL';
+  /** 手机号 */
+  phone: string;
+  /** 退团时间 */
+  quitTime: string;
+  /** 声部ID */
+  subjectId: number;
+  /** 机构ID */
+  organId: number
+}

+ 0 - 1
src/views/student-manage/withdraw/index.tsx

@@ -27,7 +27,6 @@ import {
 } from '../api';
 import MFullRefresh from '@/components/m-full-refresh';
 import { IMusicGroup, IStudentManage, ISubject } from '../type';
-import DropDownModal from '../component/drop-down-modal';
 import MEmpty from '@/components/m-empty';
 
 export default defineComponent({