瀏覽代碼

Merge branch 'master' of http://git.dayaedu.com/lex/mschool

lex 2 年之前
父節點
當前提交
fba51b7f5a

二進制
src/common/images/icon-vip.png


+ 25 - 1
src/router/routes-common.ts

@@ -131,6 +131,30 @@ export default [
         }
       },
       {
+        path: '/student-manage',
+        name: 'student-manage',
+        component: () => import('@/views/student-manage'),
+        meta: {
+          title: '学员信息'
+        }
+      },
+      {
+        path: '/student-manage-withdraw',
+        name: 'student-manage-withdraw',
+        component: () => import('@/views/student-manage/withdraw'),
+        meta: {
+          title: '退团学员'
+        }
+      },
+      {
+        path: '/student-manage-detail',
+        name: 'student-manage-detail',
+        component: () => import('@/views/student-manage/detail'),
+        meta: {
+          title: '学员详情'
+        }
+      },
+      {
         path: '/lesson-list',
         name: 'lesson-list',
         component: () => import('@/views/lesson-list/index'),
@@ -145,7 +169,7 @@ export default [
         meta: {
           title: '课时详情'
         }
-      },
+      }
     ]
   },
   ...rootRouter

+ 3 - 0
src/views/schedule-manage/index.tsx

@@ -98,6 +98,9 @@ export default defineComponent({
         status: todoData.activeOption
       })
         .then(res => {
+          if (todoData.completeResh){
+            todoData.completeList = []
+          }
           const rows: MusicGroupQuitPageDto[] = Array.isArray(res?.data?.rows)
             ? res.data.rows
             : [];

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

@@ -0,0 +1,42 @@
+import request from '@/helpers/request';
+
+/**
+ * 学员分页
+ * @param data
+ * @returns
+ */
+export const api_studentManageUserPage = (data: any) => {
+  return request.post('/api-web/studentManage/userPage', {
+    data
+  });
+};
+
+/**
+ * 学校端-人数统计
+ * @param data
+ * @returns
+ */
+export const api_studentManageUserCount = (data: any) => {
+  return request.post('/api-web/studentManage/userCount', {
+    data
+  });
+};
+
+/**
+ * 获取声部列表
+ * @param data
+ * @returns
+ */
+export const api_studentManageCoopSubjectList = (musicGroupId?: string) => {
+  return request.post('/api-web/studentManage/coopSubjectList', {
+    params: { musicGroupId }
+  });
+};
+
+/**
+ * 合作单位的乐团
+ * @returns
+ */
+export const api_cooperationOrganMusicGroupPage = () => {
+  return request.get('/api-web/cooperationOrgan/musicGroupPage');
+};

+ 66 - 0
src/views/student-manage/component/Assignment.tsx

@@ -0,0 +1,66 @@
+import { PropType, defineComponent } from 'vue';
+import styles from '../index.module.less';
+import { IStudentManage } from '../type';
+
+export default defineComponent({
+  name: 'Attendance',
+  props: {
+    item: {
+      type: Object as PropType<IStudentManage>,
+      default: () => ({})
+    }
+  },
+  setup(props) {
+    const item = props.item;
+    return () => (
+      <div class={styles.attendance}>
+        <div class={[styles.attendanceTitle, styles.assignmentTitle]}>
+          <span>本学期出勤</span>
+        </div>
+        <div class={styles.items}>
+          <div class={styles.item}>
+            <div>
+              <span style={{ color: '#FC1A19' }}>
+                {item.actualAttendanceCount}
+              </span>
+              /{item.shouldAttendanceCount}
+            </div>
+            <div class={styles.label}>出勤情况</div>
+          </div>
+
+          <div class={styles.item}>
+            <div>
+              <span style={{ color: '#00B2A7' }}>{item.normalAttendanceCount}</span>
+              <span class={styles.ci}>次</span>
+            </div>
+            <div class={styles.label}>正常出勤</div>
+          </div>
+
+          <div class={styles.item}>
+            <div>
+              <span style={{ color: '#4498F5' }}>{item.lateCount}</span>
+              <span class={styles.ci}>次</span>
+            </div>
+            <div class={styles.label}>迟到</div>
+          </div>
+
+          <div class={styles.item}>
+            <div>
+              <span style={{ color: '#F08226' }}>{item.leaveCount}</span>
+              <span class={styles.ci}>次</span>
+            </div>
+            <div class={styles.label}>请假</div>
+          </div>
+
+          <div class={styles.item}>
+            <div>
+              <span style={{ color: '#FC1A19' }}>{item.truancyCount}</span>
+              <span class={styles.ci}>次</span>
+            </div>
+            <div class={styles.label}>旷课</div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

+ 56 - 0
src/views/student-manage/component/Attendance.tsx

@@ -0,0 +1,56 @@
+import { PropType, defineComponent } from 'vue';
+import styles from '../index.module.less';
+import { IStudentManage } from '../type';
+
+export default defineComponent({
+  name: 'Attendance',
+  props: {
+    item: {
+      type: Object as PropType<IStudentManage>,
+      default: () => ({})
+    }
+  },
+  setup(props) {
+    const item = props.item;
+    return () => (
+      <div class={styles.attendance}>
+        <div class={styles.attendanceTitle}>
+          <span>本学期作业</span>
+        </div>
+        <div class={styles.items}>
+          <div class={styles.item}>
+            <div>
+              <span style={{ color: '#FC1A19' }}>{item.actualSubmitCount}</span>
+              /{item.shouldSubmitCount}
+            </div>
+            <div class={styles.label}>作业情况</div>
+          </div>
+
+          <div class={styles.item}>
+            <div>
+              <span style={{ color: '#00B2A7' }}>{item.qualifiedCount}</span>
+              <span class={styles.ci}>次</span>
+            </div>
+            <div class={styles.label}>合格</div>
+          </div>
+
+          <div class={styles.item}>
+            <div>
+              <span style={{ color: '#4498F5' }}>{item.unqualifiedCount}</span>
+              <span class={styles.ci}>次</span>
+            </div>
+            <div class={styles.label}>不合格</div>
+          </div>
+
+          <div class={styles.item}>
+            <div>
+              <span style={{ color: '#F08226' }}>{item.unsubmitCount}</span>
+              <span class={styles.ci}>次</span>
+            </div>
+            <div class={styles.label}>未提交</div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

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

@@ -0,0 +1,68 @@
+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>
+      </>
+    );
+  }
+});

+ 51 - 0
src/views/student-manage/component/m-student/index.module.less

@@ -0,0 +1,51 @@
+.student {
+    background-color: transparent;
+    padding: 15px 12px;
+    border-radius: 10px;
+    :global {
+        .van-cell__title {
+            color: #333;
+            line-height: 22px;
+            font-size: 16px;
+        }
+
+        .van-cell__label {
+            font-size: 14px;
+            line-height: 20px;
+            color: #777;
+        }
+        .van-badge{
+            background-color: transparent !important;
+            border: none ;
+        }
+    }
+
+    .iconTeacher {
+        width: 48px;
+        height: 48px;
+        margin-right: 12px;
+    }
+    .dot{
+        width: 16px;
+        height: 16px;
+    }
+    .statusBox{
+        display: flex;
+        justify-content: flex-end;
+    }
+    .status{
+        font-size: 14px;
+        color: #fff;
+        background-color: #C1C1C1;
+        border-radius: 14px;
+        line-height: 20px;
+        padding: 3px 16px;
+    }
+}
+.studentInfo{
+    :global{
+        .van-cell__title {
+            flex: 3;
+        }
+    }
+}

+ 66 - 0
src/views/student-manage/component/m-student/index.tsx

@@ -0,0 +1,66 @@
+import { Badge, Cell, Image } from 'vant';
+import { PropType, defineComponent, toRef, toRefs } from 'vue';
+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';
+
+export default defineComponent({
+  name: 'm-student',
+  props: {
+    valueType: {
+      type: String as PropType<'status'>,
+      default: ''
+    },
+    item: {
+      type: Object as PropType<IStudentManage>,
+      default: () => ({})
+    }
+  },
+  setup(props) {
+    const {item} = toRefs(props);
+    const valueType = props.valueType;
+    return () => (
+      <Cell
+        class={[styles.student, valueType === 'status' ? '' : styles.studentInfo]}
+        center
+        border={false}
+        title={item.value.studentName}
+        label={'丁曼杰' + item.value.vipFlag}
+        isLink
+        onClick={() => {}}>
+        {{
+          icon: () => (
+            <Badge offset={[-14, 10]}>
+              {{
+                default: () => (
+                  <Image
+                    src={item.value.studentAvatar || icon_student_man}
+                    class={styles.iconTeacher}
+                    fit="contain"
+                  />
+                ),
+                content: () => (
+                  <Image
+                    style={{ display: item.value.vipFlag ? '' : 'none' }}
+                    class={styles.dot}
+                    src={icon_vip}
+                  />
+                )
+              }}
+            </Badge>
+          ),
+          value: () => (
+            <>
+              {valueType === 'status' && (
+                <div class={styles.statusBox}>
+                  <div class={styles.status}>退团中</div>
+                </div>
+              )}
+            </>
+          )
+        }}
+      </Cell>
+    );
+  }
+});

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


+ 8 - 0
src/views/student-manage/detail/index.tsx

@@ -0,0 +1,8 @@
+import { defineComponent } from "vue";
+
+export default defineComponent({
+    name: 'student-manage-detail',
+    setup(){
+        return () => <div></div>
+    }
+})

二進制
src/views/student-manage/images/icon-tuituan.png


二進制
src/views/student-manage/images/icon-zaidu.png


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

@@ -0,0 +1,114 @@
+.container {
+    padding: 12px 13px;
+    :global{
+        .van-dropdown-item__option--active .van-cell__title{
+            color: var(--van-primary-color);
+        }
+    }
+}
+
+.list{
+    min-height: 50vh;
+    :global{
+        .van-empty{
+            height: calc(80vh - var(--header-height));
+        }
+    }
+}
+
+.student {
+    background-color: #fff;
+    border-radius: 10px;
+    margin-bottom: 12px;
+}
+
+.statistics{
+    display: flex;
+    padding: 20px 30px;
+    background-color: #fff;
+    border-radius: 10px;
+    margin-bottom: 12px;
+    width: 100%;
+    .statisticsItem{
+        flex: 1;
+        display: flex;
+        align-items: center;
+        justify-content: space-evenly;
+        &:first-child{
+            border-right: 1px solid #F2F2F2;
+        }
+    }
+    .iconstatistics{
+        width: 40px;
+        height: 40px;
+    }
+    .statisticsDes{
+        text-align: center;
+        color: #777;
+        font-size: 13px;
+        line-height: 18px;
+    }
+    .statisticsNum{
+        font-size: 22px;
+        font-weight: bold;
+        font-family: DINAlternate-Bold, DINAlternate;
+    }
+}
+
+.attendance {
+    padding: 5px 12px 15px;
+    .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: #B4E8FF;
+        }
+    }
+    .assignmentTitle{
+        &::before {
+            background: #A1EDCB;
+        }
+    }
+
+    .items {
+        display: flex;
+        margin: 0 -11px;
+
+        .item {
+            width: calc(100% / 5);
+            text-align: center;
+            line-height: 24px;
+            font-size: 20px;
+            font-weight: bold;
+            font-family: DINAlternate-Bold, DINAlternate;
+
+            .label {
+                line-height: 16px;
+                font-size: 12px;
+                color: #777;
+                font-weight: 400;
+                margin-top: 5px;
+                white-space: nowrap;
+            }
+
+            .ci {
+                font-size: 12px;
+                margin-left: 2px;
+            }
+        }
+    }
+}

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

@@ -0,0 +1,351 @@
+import MHeader from '@/components/m-header';
+import MSearch from '@/components/m-search';
+import MSticky from '@/components/m-sticky';
+import {
+  Button,
+  DropdownItem,
+  DropdownItemOption,
+  DropdownMenu,
+  Image,
+  List,
+  Picker
+} from 'vant';
+import { defineComponent, onMounted, reactive, ref } from 'vue';
+import styles from './index.module.less';
+import MStudent from './component/m-student/index';
+import Attendance from './component/Attendance';
+import Assignment from './component/Assignment';
+import icon_tuituan from './images/icon-tuituan.png';
+import icon_zaidu from './images/icon-zaidu.png';
+import { useRouter } from 'vue-router';
+import SkeletionIndex from './skeletion-index';
+import {
+  api_cooperationOrganMusicGroupPage,
+  api_studentManageCoopSubjectList,
+  api_studentManageUserCount,
+  api_studentManageUserPage
+} 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({
+  name: 'student-manage',
+  setup() {
+    const router = useRouter();
+    const fromData = reactive({
+      page: 1,
+      rows: 20,
+      /** 关键词 */
+      keyword: '',
+      /** 乐团ID */
+      musicGroupId: '',
+      /** 学生乐团状态 */
+      statusList: '',
+      /** 声部ID */
+      subjectId: '',
+      /** 是否会员 */
+      vipFlag: ''
+    });
+    const studentMagege = reactive({
+      skelet: true,
+      refresh: false,
+      loading: false,
+      finshed: false,
+      list: [] as IStudentManage[],
+      studentCount: 0,
+      quitCount: 0,
+      musicGroups: [] as IMusicGroup[],
+      musicGroupId: '',
+      musicGroupName: '全部乐团',
+      subjects: [] as ISubject[],
+      subjectId: '',
+      subjectName: '全部声部',
+      studentTypes: [
+        { text: '全部学员', value: '' },
+        { text: '团练宝学员', value: '1' },
+        { text: '普通学员', value: '2' }
+      ] as DropdownItemOption[],
+      studentTypeName: '全部学员'
+    });
+    const musicGroupRef = ref();
+    const subjectRef = ref();
+
+    /** 获取乐团列表 */
+    const getGroups = () => {
+      api_cooperationOrganMusicGroupPage().then(res => {
+        let data = Array.isArray(res?.data) ? res.data : [];
+        if (data.length) {
+          data = [{ name: '全部乐团', id: '' }].concat(data);
+          studentMagege.musicGroups = data.map((item: any) => {
+            return {
+              text: item.name,
+              value: item.id
+            };
+          });
+        }
+      });
+    };
+
+    /** 获取声部列表 */
+    const getSubjects = () => {
+      api_studentManageCoopSubjectList().then(res => {
+        let data = Array.isArray(res?.data) ? res.data : [];
+        if (data.length) {
+          data = [{ name: '全部声部', id: '' }].concat(data);
+          studentMagege.subjects = data.map((item: any) => {
+            return {
+              text: item.name,
+              value: item.id
+            };
+          });
+        }
+      });
+    };
+
+    const getSchoolData = () => {
+      api_studentManageUserCount({
+        ...fromData,
+        vipFlag:
+          fromData.vipFlag === '1'
+            ? true
+            : fromData.vipFlag === '2'
+            ? false
+            : ''
+      }).then(res => {
+        const data = res?.data;
+        if (data) {
+          studentMagege.studentCount = data.studentCount;
+          studentMagege.quitCount = data.quitCount;
+        }
+      });
+    };
+
+    const getData = async () => {
+      studentMagege.loading = true;
+      api_studentManageUserPage({
+        ...fromData,
+        vipFlag:
+          fromData.vipFlag === '1'
+            ? true
+            : fromData.vipFlag === '2'
+            ? false
+            : ''
+      })
+        .then(res => {
+          if (studentMagege.refresh) {
+            studentMagege.list = [];
+          }
+          const rows: IStudentManage[] = Array.isArray(res?.data?.rows)
+            ? res.data.rows
+            : [];
+          studentMagege.list = studentMagege.list.concat(rows);
+          if (!rows.length || rows.length < fromData.rows) {
+            studentMagege.finshed = true;
+          }
+          fromData.page++;
+        })
+        .catch(() => {
+          studentMagege.finshed = true;
+        })
+        .finally(() => {
+          setTimeout(() => {
+            studentMagege.loading = false;
+            studentMagege.refresh = false;
+            studentMagege.skelet = false;
+          }, 500);
+        });
+    };
+
+    const handleSearch = () => {
+      fromData.page = 1;
+      studentMagege.refresh = true;
+      getSchoolData();
+      getData();
+    };
+
+    onMounted(() => {
+      getGroups();
+      getSubjects();
+      getSchoolData();
+    });
+
+    return () => (
+      <div class={styles.container}>
+        <MSticky position="top">
+          <MHeader />
+          <MSearch
+            onSearch={value => {
+              fromData.keyword = value;
+              handleSearch();
+            }}
+          />
+          <DropdownMenu>
+            <DropdownItem
+              ref={musicGroupRef}
+              title={studentMagege.musicGroupName}>
+              <Picker
+                showToolbar={false}
+                visibleOptionNum={5}
+                columns={studentMagege.musicGroups}
+                onChange={value => {
+                  studentMagege.musicGroupId = value.selectedValues[0];
+                }}
+              />
+              <div class={['btnGroupPopup', 'van-hairline--top']}>
+                <Button
+                  round
+                  onClick={() => {
+                    musicGroupRef.value?.toggle(false);
+                  }}>
+                  取消
+                </Button>
+                <Button
+                  disabled={!studentMagege.musicGroups.length}
+                  type="primary"
+                  round
+                  onClick={() => {
+                    musicGroupRef.value?.toggle(false);
+                    fromData.musicGroupId = studentMagege.musicGroupId;
+                    studentMagege.musicGroupName =
+                      studentMagege.musicGroups.find(
+                        _item => _item.value == studentMagege.musicGroupId
+                      )?.text || '全部乐团';
+                    handleSearch();
+                  }}>
+                  确定
+                </Button>
+              </div>
+            </DropdownItem>
+            <DropdownItem ref={subjectRef} title={studentMagege.subjectName}>
+              <Picker
+                showToolbar={false}
+                visibleOptionNum={5}
+                columns={studentMagege.subjects}
+                onChange={value => {
+                  const option = value.selectedOptions[0];
+                  studentMagege.subjectId = option.value;
+                }}
+              />
+              <div class={['btnGroupPopup', 'van-hairline--top']}>
+                <Button
+                  round
+                  onClick={() => {
+                    subjectRef.value?.toggle(false);
+                  }}>
+                  取消
+                </Button>
+                <Button
+                  disabled={!studentMagege.subjects.length}
+                  type="primary"
+                  round
+                  onClick={() => {
+                    subjectRef.value?.toggle(false);
+                    fromData.subjectId = studentMagege.subjectId;
+                    studentMagege.subjectName =
+                      studentMagege.subjects.find(
+                        _item => _item.value == studentMagege.subjectId
+                      )?.text || '全部声部';
+                    handleSearch();
+                  }}>
+                  确定
+                </Button>
+              </div>
+            </DropdownItem>
+            <DropdownItem
+              title={studentMagege.studentTypeName}
+              v-model={fromData.vipFlag}
+              options={studentMagege.studentTypes}
+              onChange={value => {
+                studentMagege.studentTypeName =
+                  studentMagege.studentTypes.find(_o => _o.value == value)
+                    ?.text || '';
+                handleSearch();
+              }}></DropdownItem>
+          </DropdownMenu>
+        </MSticky>
+
+        <MFullRefresh
+          v-model:modelValue={studentMagege.refresh}
+          onRefresh={() => {
+            fromData.page = 1;
+            studentMagege.finshed = false;
+            getData();
+          }}>
+          <List
+            class={styles.list}
+            loading={studentMagege.loading}
+            finished={studentMagege.finshed}
+            onLoad={() => {
+              studentMagege.loading = true;
+              console.log('触底了');
+              getData();
+            }}>
+            <SkeletionIndex loading={studentMagege.skelet}>
+              <>
+                <div class={styles.statistics}>
+                  <div class={styles.statisticsItem}>
+                    <Image class={styles.iconstatistics} src={icon_zaidu} />
+                    <div class={styles.statisticsDes}>
+                      <div style={{ color: '#333' }}>
+                        <span
+                          class={styles.statisticsNum}
+                          style={{ color: '#333' }}>
+                          {studentMagege.studentCount}
+                        </span>
+                        人
+                      </div>
+                      <div>在读学员</div>
+                    </div>
+                  </div>
+
+                  <div
+                    class={styles.statisticsItem}
+                    onClick={() => {
+                      router.push({
+                        path: '/student-manage-withdraw'
+                      });
+                    }}>
+                    <Image class={styles.iconstatistics} src={icon_tuituan} />
+                    <div class={styles.statisticsDes}>
+                      <div style={{ color: '#333' }}>
+                        <span
+                          class={styles.statisticsNum}
+                          style={{ color: '#FC1A19' }}>
+                          {studentMagege.quitCount}
+                        </span>
+                        人
+                      </div>
+                      <div>退团人数</div>
+                    </div>
+                  </div>
+                </div>
+                {studentMagege.list.map((item: IStudentManage) => (
+                  <div class={styles.student}>
+                    <MStudent item={item} />
+                    {!!item.shouldAttendanceCount && (
+                      <>
+                        <Assignment item={item} />
+                        <Attendance item={item} />
+                      </>
+                    )}
+                  </div>
+                ))}
+
+                {!studentMagege.loading && !studentMagege.list.length && (
+                  <MEmpty
+                    description="暂无数据"
+                    style={{
+                      minHeight: '100%'
+                    }}
+                  />
+                )}
+              </>
+            </SkeletionIndex>
+          </List>
+        </MFullRefresh>
+      </div>
+    );
+  }
+});

+ 77 - 0
src/views/student-manage/skeletion-index.tsx

@@ -0,0 +1,77 @@
+import { defineComponent } from 'vue';
+import styles from './index.module.less';
+import {
+  Cell,
+  Grid,
+  GridItem,
+  Skeleton,
+  SkeletonAvatar,
+  SkeletonParagraph
+} from 'vant';
+
+export default defineComponent({
+  name: 'student-manage-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'
+              }}>
+              <div class={styles.statistics}>
+                <div class={styles.statisticsItem}>
+                  <SkeletonAvatar avatarSize={'1rem'} />
+                  <div class={styles.statisticsDes} style={{ width: '50%' }}>
+                    <SkeletonParagraph rowWidth="80%" />
+                    <SkeletonParagraph rowWidth="90%" />
+                  </div>
+                </div>
+
+                <div class={styles.statisticsItem}>
+                  <SkeletonAvatar avatarSize={'1rem'} />
+                  <div class={styles.statisticsDes} style={{ width: '50%' }}>
+                    <SkeletonParagraph rowWidth="80%" />
+                    <SkeletonParagraph rowWidth="90%" />
+                  </div>
+                </div>
+              </div>
+
+              {new Array(2).fill(1).map(stu => (
+                <div style={{ overflow: 'hidden' }} class={styles.student}>
+                  <Cell border={false}>
+                    {{
+                      icon: () => <SkeletonAvatar avatarSize={'1.28rem'} />,
+                      title: () => <SkeletonParagraph rowWidth="50%" />,
+                      label: () => <SkeletonParagraph rowWidth="30%" />
+                    }}
+                  </Cell>
+                  {new Array(2).fill(1).map(n => (
+                    <Grid border={false}>
+                      {new Array(4).fill(1).map(n => (
+                        <GridItem>
+                          <SkeletonParagraph rowWidth="80%" />
+                          <SkeletonParagraph rowWidth="90%" />
+                        </GridItem>
+                      ))}
+                    </Grid>
+                  ))}
+                </div>
+              ))}
+            </div>
+          ),
+          default: () => slots.default?.()
+        }}
+      </Skeleton>
+    );
+  }
+});

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

@@ -0,0 +1,47 @@
+/** 学生信息管理 */
+export interface IStudentManage {
+  /** 实际出勤数*/
+  actualAttendanceCount: number;
+  /** 实际提交作业数*/
+  actualSubmitCount: number;
+  /** 迟到*/
+  lateCount: number;
+  /** 请假*/
+  leaveCount: number;
+  /** 正常出勤数*/
+  normalAttendanceCount: number;
+  /** 合格 */
+  qualifiedCount: number;
+  /** 应出勤数*/
+  shouldAttendanceCount: number;
+  /** 应提交作业数*/
+  shouldSubmitCount: number;
+  /** 学生头像 */
+  studentAvatar: string;
+  /** 学生ID */
+  studentId: number;
+  /** 学生姓名*/
+  studentName: string;
+  /** 旷课 */
+  truancyCount: number;
+  /** 不合格 */
+  unqualifiedCount: number;
+  /** 未提交 */
+  unsubmitCount: number;
+  /** 是否vip */
+  vipFlag: boolean;
+}
+
+/** 乐团 */
+export interface IMusicGroup {
+  /** 乐团ID */
+  text: string;
+  /** 乐团名称 */
+  value: string;
+}
+
+/** 声部 */
+export interface ISubject {
+  text: string;
+  value: string;
+}

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

@@ -0,0 +1,276 @@
+import MHeader from '@/components/m-header';
+import MSearch from '@/components/m-search';
+import MSticky from '@/components/m-sticky';
+import {
+  Button,
+  DropdownItem,
+  DropdownItemOption,
+  DropdownMenu,
+  Image,
+  List,
+  Picker
+} from 'vant';
+import { defineComponent, onMounted, reactive, ref } from 'vue';
+import styles from '../index.module.less';
+import MStudent from '../component/m-student/index';
+import Attendance from '../component/Attendance';
+import Assignment from '../component/Assignment';
+import icon_tuituan from '../images/icon-tuituan.png';
+import icon_zaidu from '../images/icon-zaidu.png';
+import { useRouter } from 'vue-router';
+import SkeletionIndex from '../skeletion-index';
+import {
+  api_cooperationOrganMusicGroupPage,
+  api_studentManageCoopSubjectList,
+  api_studentManageUserCount,
+  api_studentManageUserPage
+} 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({
+  name: 'student-manage',
+  setup() {
+    const router = useRouter();
+    const fromData = reactive({
+      page: 1,
+      rows: 20,
+      /** 关键词 */
+      keyword: '',
+      /** 乐团ID */
+      musicGroupId: '',
+      /** 学生乐团状态 */
+      statusList: ['QUIT'],
+      /** 声部ID */
+      subjectId: '',
+      /** 是否会员 */
+      vipFlag: ''
+    });
+    const studentMagege = reactive({
+      skelet: true,
+      refresh: false,
+      loading: false,
+      finshed: false,
+      list: [] as IStudentManage[],
+      musicGroups: [] as IMusicGroup[],
+      musicGroupId: '',
+      musicGroupName: '全部乐团',
+      subjects: [] as ISubject[],
+      subjectId: '',
+      subjectName: '全部声部'
+    });
+    const musicGroupRef = ref();
+    const subjectRef = ref();
+
+    /** 获取乐团列表 */
+    const getGroups = () => {
+      api_cooperationOrganMusicGroupPage().then(res => {
+        let data = Array.isArray(res?.data) ? res.data : [];
+        if (data.length) {
+          data = [{ name: '全部乐团', id: '' }].concat(data);
+          studentMagege.musicGroups = data.map((item: any) => {
+            return {
+              text: item.name,
+              value: item.id
+            };
+          });
+        }
+      });
+    };
+
+    /** 获取声部列表 */
+    const getSubjects = () => {
+      api_studentManageCoopSubjectList().then(res => {
+        let data = Array.isArray(res?.data) ? res.data : [];
+        if (data.length) {
+          data = [{ name: '全部声部', id: '' }].concat(data);
+          studentMagege.subjects = data.map((item: any) => {
+            return {
+              text: item.name,
+              value: item.id
+            };
+          });
+        }
+      });
+    };
+
+    const getData = async () => {
+      studentMagege.loading = true;
+      api_studentManageUserPage({
+        ...fromData,
+        vipFlag:
+          fromData.vipFlag === '1'
+            ? true
+            : fromData.vipFlag === '2'
+            ? false
+            : ''
+      })
+        .then(res => {
+          if (studentMagege.refresh) {
+            studentMagege.list = [];
+          }
+          const rows: IStudentManage[] = Array.isArray(res?.data?.rows)
+            ? res.data.rows
+            : [];
+          studentMagege.list = studentMagege.list.concat(rows);
+          if (!rows.length || rows.length < fromData.rows) {
+            studentMagege.finshed = true;
+          }
+          fromData.page++;
+        })
+        .catch(() => {
+          studentMagege.finshed = true;
+        })
+        .finally(() => {
+          setTimeout(() => {
+            studentMagege.loading = false;
+            studentMagege.refresh = false;
+            studentMagege.skelet = false;
+          }, 500);
+        });
+    };
+
+    const handleSearch = () => {
+      studentMagege.skelet = true;
+      fromData.page = 1;
+      studentMagege.refresh = true;
+      getData();
+    };
+
+    onMounted(() => {
+      getGroups();
+      getSubjects();
+    });
+
+    return () => (
+      <div class={styles.container}>
+        <MSticky position="top">
+          <MHeader />
+          <MSearch
+            onSearch={value => {
+              fromData.keyword = value;
+              handleSearch();
+            }}
+          />
+          <DropdownMenu>
+            <DropdownItem
+              ref={musicGroupRef}
+              title={studentMagege.musicGroupName}>
+              <Picker
+                showToolbar={false}
+                visibleOptionNum={5}
+                columns={studentMagege.musicGroups}
+                onChange={value => {
+                  studentMagege.musicGroupId = value.selectedValues[0];
+                }}
+              />
+              <div class={['btnGroupPopup', 'van-hairline--top']}>
+                <Button
+                  round
+                  onClick={() => {
+                    musicGroupRef.value?.toggle(false);
+                  }}>
+                  取消
+                </Button>
+                <Button
+                  disabled={!studentMagege.musicGroups.length}
+                  type="primary"
+                  round
+                  onClick={() => {
+                    musicGroupRef.value?.toggle(false);
+                    fromData.musicGroupId = studentMagege.musicGroupId;
+                    studentMagege.musicGroupName =
+                      studentMagege.musicGroups.find(
+                        _item => _item.value == studentMagege.musicGroupId
+                      )?.text || '全部乐团';
+                    handleSearch();
+                  }}>
+                  确定
+                </Button>
+              </div>
+            </DropdownItem>
+            <DropdownItem ref={subjectRef} title={studentMagege.subjectName}>
+              <Picker
+                showToolbar={false}
+                visibleOptionNum={5}
+                columns={studentMagege.subjects}
+                onChange={value => {
+                  studentMagege.subjectId = value.selectedValues[0];
+                }}
+              />
+              <div class={['btnGroupPopup', 'van-hairline--top']}>
+                <Button
+                  round
+                  onClick={() => {
+                    subjectRef.value?.toggle(false);
+                  }}>
+                  取消
+                </Button>
+                <Button
+                  disabled={!studentMagege.subjects.length}
+                  type="primary"
+                  round
+                  onClick={() => {
+                    subjectRef.value?.toggle(false);
+                    fromData.subjectId = studentMagege.subjectId;
+                    studentMagege.subjectName =
+                      studentMagege.subjects.find(
+                        _item => _item.value == studentMagege.subjectId
+                      )?.text || '全部声部';
+                    handleSearch();
+                  }}>
+                  确定
+                </Button>
+              </div>
+            </DropdownItem>
+          </DropdownMenu>
+        </MSticky>
+
+        <MFullRefresh
+          v-model:modelValue={studentMagege.refresh}
+          onRefresh={() => {
+            fromData.page = 1;
+            studentMagege.finshed = false;
+            getData();
+          }}>
+          <List
+            class={styles.list}
+            loading={studentMagege.loading}
+            finished={studentMagege.finshed}
+            onLoad={() => {
+              studentMagege.loading = true;
+              console.log('触底了');
+              getData();
+            }}>
+            <SkeletionIndex loading={studentMagege.skelet}>
+              <>
+                {studentMagege.list.map((item: IStudentManage) => (
+                  <div class={styles.student}>
+                    <MStudent item={item} />
+                    {!!item.shouldAttendanceCount && (
+                      <>
+                        <Assignment item={item} />
+                        <Attendance item={item} />
+                      </>
+                    )}
+                  </div>
+                ))}
+
+                {!studentMagege.loading && !studentMagege.list.length && (
+                  <MEmpty
+                    description="暂无数据"
+                    style={{
+                      minHeight: '100%'
+                    }}
+                  />
+                )}
+              </>
+            </SkeletionIndex>
+          </List>
+        </MFullRefresh>
+      </div>
+    );
+  }
+});