Ver Fonte

首页图标

liushengqiang há 2 anos atrás
pai
commit
69f0807621

+ 69 - 5
package-lock.json

@@ -13,6 +13,7 @@
         "@vant/use": "^1.5.1",
         "clean-deep": "^3.4.0",
         "dayjs": "^1.11.7",
+        "echarts": "^5.4.2",
         "numeral": "^2.0.6",
         "plyr": "^3.7.8",
         "query-string": "^8.1.0",
@@ -3633,6 +3634,20 @@
       "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
       "dev": true
     },
+    "node_modules/echarts": {
+      "version": "5.4.2",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz",
+      "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==",
+      "dependencies": {
+        "tslib": "2.3.0",
+        "zrender": "5.4.3"
+      }
+    },
+    "node_modules/echarts/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.4.371",
       "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.371.tgz",
@@ -8052,6 +8067,19 @@
       "resolved": "https://registry.npmmirror.com/yallist/-/yallist-2.1.2.tgz",
       "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
       "dev": true
+    },
+    "node_modules/zrender": {
+      "version": "5.4.3",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz",
+      "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==",
+      "dependencies": {
+        "tslib": "2.3.0"
+      }
+    },
+    "node_modules/zrender/node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
     }
   },
   "dependencies": {
@@ -9771,13 +9799,15 @@
     "@vant/use": {
       "version": "1.5.1",
       "resolved": "https://registry.npmmirror.com/@vant/use/-/use-1.5.1.tgz",
-      "integrity": "sha512-Zxd7lDz/LliVYEQi3PR9a8CQa/kGCVzF0u9hqDMaTlgXlbG0wHMFPllrcG0ThR6bfs8xrYVuSFM9pJn6HSoUGQ=="
+      "integrity": "sha512-Zxd7lDz/LliVYEQi3PR9a8CQa/kGCVzF0u9hqDMaTlgXlbG0wHMFPllrcG0ThR6bfs8xrYVuSFM9pJn6HSoUGQ==",
+      "requires": {}
     },
     "@vitejs/plugin-vue": {
       "version": "4.1.0",
       "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz",
       "integrity": "sha512-++9JOAFdcXI3lyer9UKUV4rfoQ3T1RN8yDqoCLar86s0xQct5yblxAE+yWgRnU5/0FOlVCpTZpYSBV/bGWrSrQ==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "@vitejs/plugin-vue-jsx": {
       "version": "3.0.1",
@@ -10022,7 +10052,8 @@
       "version": "5.3.2",
       "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
       "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "aggregate-error": {
       "version": "3.1.0",
@@ -10616,6 +10647,22 @@
       "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
       "dev": true
     },
+    "echarts": {
+      "version": "5.4.2",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.2.tgz",
+      "integrity": "sha512-2W3vw3oI2tWJdyAz+b8DuWS0nfXtSDqlDmqgin/lfzbkB01cuMEN66KWBlmur3YMp5nEDEEt5s23pllnAzB4EA==",
+      "requires": {
+        "tslib": "2.3.0",
+        "zrender": "5.4.3"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+          "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+        }
+      }
+    },
     "electron-to-chromium": {
       "version": "1.4.371",
       "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.371.tgz",
@@ -10838,7 +10885,8 @@
       "version": "8.8.0",
       "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
       "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "eslint-plugin-prettier": {
       "version": "4.2.1",
@@ -12859,7 +12907,8 @@
       "version": "6.0.0",
       "resolved": "https://registry.npmmirror.com/postcss-pxtorem/-/postcss-pxtorem-6.0.0.tgz",
       "integrity": "sha512-ZRXrD7MLLjLk2RNGV6UA4f5Y7gy+a/j1EqjAfp9NdcNYVjUMvg5HTYduTjSkKBkRkfqbg/iKrjMO70V4g1LZeg==",
-      "dev": true
+      "dev": true,
+      "requires": {}
     },
     "postcss-selector-parser": {
       "version": "6.0.11",
@@ -14057,6 +14106,21 @@
           "dev": true
         }
       }
+    },
+    "zrender": {
+      "version": "5.4.3",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.3.tgz",
+      "integrity": "sha512-DRUM4ZLnoaT0PBVvGBDO9oWIDBKFdAVieNWxWwK0niYzJCMwGchRk21/hsE+RKkIveH3XHCyvXcJDkgLVvfizQ==",
+      "requires": {
+        "tslib": "2.3.0"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.3.0",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+          "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+        }
+      }
     }
   }
 }

+ 1 - 0
package.json

@@ -26,6 +26,7 @@
     "@vant/use": "^1.5.1",
     "clean-deep": "^3.4.0",
     "dayjs": "^1.11.7",
+    "echarts": "^5.4.2",
     "numeral": "^2.0.6",
     "plyr": "^3.7.8",
     "query-string": "^8.1.0",

+ 43 - 0
src/views/home/api.ts

@@ -0,0 +1,43 @@
+import request from '@/helpers/request';
+
+/**
+ * 根据合作单位获取所有乐团列表
+ * @param cooperationId 合作单位
+ * @returns 
+ */
+export const api_musicGroupFindByCooperationId = (cooperationId: string) => {
+  return request.get('/api-web/musicGroup/findByCooperationId', {
+    params: { cooperationId }
+  });
+};
+/**
+ * 统计
+ * @returns 
+ */
+export const api_schoolIndexStat = (data: any) => {
+  return request.post('/api-web/schoolIndex/stat', {
+    data
+  });
+};
+
+/**
+ * 学员出勤统计
+ * @param data 
+ * @returns 
+ */
+export const api_schoolIndexAttendanceStat = (data: any) => {
+  return request.post('/api-web/schoolIndex/attendanceStat', {
+    data
+  });
+};
+
+/**
+ * 学员练习统计
+ * @param data 
+ * @returns 
+ */
+export const api_schoolIndexLessonStat = (data: any) => {
+  return request.post('/api-web/schoolIndex/lessonStat', {
+    data
+  });
+};

+ 199 - 0
src/views/home/component/Attendance.tsx

@@ -0,0 +1,199 @@
+import {
+  PropType,
+  defineComponent,
+  nextTick,
+  onMounted,
+  ref,
+  toRefs,
+  watch
+} from 'vue';
+import styles from '../index.module.less';
+import icons from '../icons.json';
+import { Badge, Icon, Image, Tab, Tabs } from 'vant';
+import * as echarts from 'echarts';
+import { IStudentAttendance } from '../type';
+import { useRouter } from 'vue-router';
+import icon_4 from '../image/icon_4.png';
+
+type EChartsOption = echarts.EChartsOption;
+const colors = [
+  {
+    color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+      { offset: 0, color: '#02E2DB' },
+      { offset: 1, color: '#01C1B5' }
+    ]),
+    name: '正常',
+    key: 'normalNum',
+    background: 'linear-gradient(180deg, #02E2DB 0%, #01C1B5 100%)'
+  },
+  {
+    color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+      { offset: 0, color: '#5B8FF9' },
+      { offset: 1, color: '#94C2FD' }
+    ]),
+    name: '迟到',
+    key: 'lateNum',
+    background:
+      'linear-gradient(180deg, rgba(148, 194, 253, 0.85) 0%, rgba(91, 143, 249, 0.85) 100%)'
+  },
+  {
+    color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+      { offset: 0, color: '#FBE031' },
+      { offset: 1, color: '#F6BD16' }
+    ]),
+    name: '请假',
+    key: 'leaveNum',
+    background:
+      'linear-gradient(180deg, rgba(251,224,49,0.85) 0%, rgba(246,189,22,0.85) 100%)'
+  },
+  {
+    color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+      { offset: 0, color: '#F5A181' },
+      { offset: 1, color: '#E8684A' }
+    ]),
+    name: '旷课',
+    key: 'truantNum',
+    background:
+      'linear-gradient(180deg, rgba(245,161,129,0.85) 0%, rgba(232,104,74,0.85) 100%)'
+  }
+];
+
+export default defineComponent({
+  name: 'Attendance',
+  props: {
+    data: {
+      type: Object as PropType<IStudentAttendance>,
+      default: () => ({})
+    }
+  },
+  emits: ['change'],
+  setup(props, { emit }) {
+    const firstInit = ref(false);
+    const router = useRouter();
+    const { data } = toRefs(props);
+    watch(
+      () => data.value,
+      () => {
+        firstInit.value = true;
+        nextTick(() => {
+          handleInit();
+        });
+      }
+    );
+    let myChart: echarts.ECharts;
+    const handleInit = () => {
+      if (!data.value.attendanceRate) return
+      if (myChart) {
+        myChart.dispose();
+      }
+      const chartDom = document.getElementById('attendanceEcharts')!;
+      myChart = echarts.init(chartDom, {}, { renderer: 'svg' });
+      const option: EChartsOption = {
+        title: {
+          text: `${data.value.attendanceRate}%`,
+          subtext: '出勤率',
+          textAlign: 'center',
+          left: '48%',
+          top: '38%',
+          textStyle: {
+            fontSize: '22px',
+            fontWeight: 'bold',
+            color: '#333',
+            fontFamily: 'DINAlternate-Bold, DINAlternate'
+          },
+          subtextStyle: {
+            fontSize: '12px',
+            color: '#777'
+          }
+        },
+        tooltip: {
+          trigger: 'item',
+          confine: true
+        },
+        series: [
+          {
+            type: 'pie',
+            radius: ['54%', '70%'],
+            itemStyle: {
+              borderRadius: 2,
+              borderColor: '#fff',
+              borderWidth: 1
+            },
+            label: {
+              show: false
+            },
+            labelLine: {
+              show: false
+            },
+            avoidLabelOverlap: false,
+            data: colors.map(item => {
+              return {
+                name: item.name,
+                value: data.value[item.key],
+                itemStyle: {
+                  color: item.color
+                }
+              };
+            })
+          }
+        ]
+      };
+
+      option && myChart.setOption(option);
+    };
+    return () => (
+      <div class={styles.item}>
+        <div class={styles.top}>
+          <Image class={styles.iconRight} src={icons.right} />
+          <span>学员出勤</span>
+          <Image class={styles.iconLeft} src={icons.left} />
+        </div>
+        <div class={styles.tabsContainer}>
+          <Tabs
+            shrink
+            active={"本周"}
+            onChange={value => {
+              console.log(value, 123);
+              emit('change', value);
+            }}>
+            <Tab name="本周" title="本周"></Tab>
+            <Tab name="本月" title="本月"></Tab>
+            <Tab name="本学期" title="本学期"></Tab>
+          </Tabs>
+          <div
+            class={styles.tagRight}
+            onClick={() => {
+              router.push({
+                path: '/student-manage'
+              });
+            }}>
+            学员信息 <Icon name="arrow" color="rgba(216,216,216,1)" />
+          </div>
+        </div>
+        <div
+          style={{ display: data.value.attendanceRate ? '' : 'none' }}
+          class={styles.attendanceContainer}>
+          <div class={styles.attendanceEcharts} id="attendanceEcharts"></div>
+          <div class={styles.tags}>
+            {colors.map((item, index) => (
+              <div class={styles.tag}>
+                <div
+                  class={styles.rect}
+                  style={{ background: item.background }}
+                />
+                <div class={styles.des}>{item.name}</div>
+                <span class={styles.tagNum}>{data.value[item.key] || 0}</span>
+              </div>
+            ))}
+          </div>
+        </div>
+
+        {!data.value.attendanceRate && (
+          <div class={[styles.gradeContainer, styles.itemEmtry]}>
+            <Image src={icon_4} />
+          </div>
+        )}
+      </div>
+    );
+  }
+});

+ 182 - 0
src/views/home/component/CurrentStudent.tsx

@@ -0,0 +1,182 @@
+import {
+  PropType,
+  defineComponent,
+  nextTick,
+  onMounted,
+  ref,
+  toRefs,
+  watch
+} from 'vue';
+import styles from '../index.module.less';
+import icons from '../icons.json';
+import { Badge, Image, Skeleton, SkeletonImage, SkeletonParagraph } from 'vant';
+import * as echarts from 'echarts';
+import { IGradeDistribution } from '../type';
+import icon_1 from '../image/icon_1.png';
+
+type EChartsOption = echarts.EChartsOption;
+
+const colors = [
+  '#5B8FF9',
+  '#F6BD16',
+  '#5AD8A6',
+  '#E8684A',
+  '#5D7092',
+  '#6DC8EC',
+  '#FF9530',
+  '#B87BDD',
+  '#92DE97',
+
+  '#5B8FF9',
+  '#F6BD16',
+  '#5AD8A6',
+  '#E8684A',
+  '#5D7092',
+  '#6DC8EC',
+  '#FF9530',
+  '#B87BDD',
+  '#92DE97'
+];
+
+export default defineComponent({
+  name: 'CurrentStudent',
+  props: {
+    list: {
+      type: Array as PropType<IGradeDistribution[]>,
+      default: () => []
+    }
+  },
+  setup(props) {
+    const firstInit = ref(false);
+    const { list } = toRefs(props);
+    watch(
+      () => list.value,
+      () => {
+        firstInit.value = true;
+        nextTick(() => {
+          handleInit();
+        });
+      }
+    );
+    let myChart: echarts.ECharts;
+    const handleInit = () => {
+      if (!list.value.length) return;
+      if (myChart) {
+        myChart.dispose();
+      }
+      const chartDom = document.getElementById('CurrentStudent')!;
+      myChart = echarts.init(chartDom, {}, { renderer: 'svg' });
+      const option: EChartsOption = {
+        title: {
+          text: list.value
+            .reduce((total, item: IGradeDistribution) => {
+              total += item.studentNum;
+              return total;
+            }, 0)
+            .toString(),
+          subtext: '在读人数',
+          textAlign: 'center',
+          left: '48%',
+          top: '38%',
+          textStyle: {
+            fontSize: '22px',
+            fontWeight: 'bold',
+            color: '#333',
+            fontFamily: 'DINAlternate-Bold, DINAlternate'
+          },
+          subtextStyle: {
+            fontSize: '12px',
+            color: '#777'
+          }
+        },
+        tooltip: {
+          trigger: 'item',
+          confine: true
+        },
+        series: [
+          {
+            type: 'pie',
+            radius: ['40%', '70%'],
+            itemStyle: {
+              borderRadius: 2,
+              borderColor: '#fff',
+              borderWidth: 1
+            },
+            label: {
+              show: false
+            },
+            labelLine: {
+              show: false
+            },
+            avoidLabelOverlap: false,
+            data: list.value.map((item: IGradeDistribution, index: number) => {
+              return {
+                value: item.studentNum,
+                name: item.grade,
+                itemStyle: {
+                  color: colors[index]
+                }
+              };
+            })
+          }
+        ]
+      };
+
+      option && myChart.setOption(option);
+    };
+
+    return () => (
+      <div class={styles.item}>
+        <div class={styles.top}>
+          <Image class={styles.iconRight} src={icons.right} />
+          <span>在读学员</span>
+          <Image class={styles.iconLeft} src={icons.left} />
+        </div>
+        <div class={styles.itemTop}>
+          <Image class={styles.icon} src={icons[1]} />
+          <div class={styles.title}>年级分布</div>
+          <div class={styles.des}>(单位:人)</div>
+        </div>
+
+        {!firstInit.value && (
+          <Skeleton class={styles.itemEmtry}>
+            {{
+              template: () => (
+                <div style={{ display: 'flex', width: '100%' }}>
+                  <SkeletonImage />
+                  <div style={{ flex: 1, marginLeft: '16px' }}>
+                    <SkeletonParagraph row-width="60%" />
+                    <SkeletonParagraph />
+                    <SkeletonParagraph />
+                    <SkeletonParagraph />
+                  </div>
+                </div>
+              )
+            }}
+          </Skeleton>
+        )}
+
+        <div
+          style={{ display: list.value.length ? '' : 'none' }}
+          class={styles.gradeContainer}>
+          <div class={styles.gradeEcharts} id="CurrentStudent"></div>
+          <div class={styles.tags}>
+            {list.value.map((item: IGradeDistribution, i: number) => (
+              <div class={styles.tag}>
+                <Badge dot color={colors[i]} />
+                <div>{item.grade}</div>
+                <span class={styles.tagNum}>{item.studentNum}</span>
+              </div>
+            ))}
+          </div>
+        </div>
+
+        {firstInit.value && !list.value.length && (
+          <div class={[styles.gradeContainer, styles.itemEmtry]}>
+            <Image src={icon_1} />
+          </div>
+        )}
+      </div>
+    );
+  }
+});

+ 137 - 0
src/views/home/component/DetailData.tsx

@@ -0,0 +1,137 @@
+import {
+  PropType,
+  computed,
+  defineComponent,
+  onMounted,
+  toRefs,
+  watch
+} from 'vue';
+import styles from '../index.module.less';
+import icons from '../icons.json';
+import { Badge, Image } from 'vant';
+import { ISubjectGradeDistribution } from '../type';
+
+const colors = [
+  '#5B8FF9',
+  '#F6BD16',
+  '#5AD8A6',
+  '#E8684A',
+  '#5D7092',
+  '#6DC8EC'
+];
+
+export default defineComponent({
+  name: 'DetailData',
+  props: {
+    data: {
+      type: Array as PropType<ISubjectGradeDistribution[]>,
+      default: () => []
+    }
+  },
+  setup(props) {
+    const { data } = toRefs(props);
+
+    /** 声部列表 */
+    const subjects = computed(() => {
+      let list = data.value.reduce((list: any[], item) => {
+        const _itemIndex = list.findIndex(
+          _item => _item.subjectName === item.subjectName
+        );
+        if (_itemIndex > -1) {
+          const listItem = list[_itemIndex];
+          listItem[item.grade] = item.studentNum;
+        } else {
+          list.push({
+            subjectName: item.subjectName,
+            [item.grade]: item.studentNum
+          });
+        }
+        return list;
+      }, []);
+      if (!list.length) {
+        list = ['长笛', '单簧管', '萨克斯', '小号', '圆号', '长号', '上低音号'].map((n, i) => {
+          return {
+            subjectName: n,
+            '总人数': 0,
+            '一年级': 0,
+            '二年级': 0,
+            '三年级': 0,
+            '四年级': 0,
+            '五年级': 0,
+            '六年级': 0,
+          };
+        })
+        
+      }
+      return list;
+    });
+
+    /** 年级列表 */
+    const gradeList = computed(() => {
+      let index = -1;
+      let _list = Array.from(new Set(data.value.map(item => item.grade)))
+        .filter(item => item !== '总人数')
+        .map(item => {
+          if (index >= colors.length - 1) index = 0;
+          else ++index;
+          return {
+            name: item,
+            color: colors[index]
+          };
+        });
+      if (!_list.length) {
+        _list = ['一', '二', '三', '四', '五', '六'].map((n, i) => {
+          if (index >= colors.length - 1) index = 0;
+          else ++index;
+          return {
+            name: n + '年级',
+            color: colors[index]
+          };
+        });
+      }
+      return _list;
+    });
+
+    return () => (
+      <div class={styles.item}>
+        <div class={styles.itemTop}>
+          <Image class={styles.icon} src={icons[3]} />
+          <div class={styles.title}>详细数据</div>
+          <div class={styles.des}>(单位:人)</div>
+        </div>
+        <div class={styles.detailDataContainer}>
+          <div class={styles.detailLeft}>
+            <div>
+              <div class={styles.tableTitle}>声部</div>
+              {subjects.value.map(item => (
+                <div class={styles.tableTr}>{item.subjectName}</div>
+              ))}
+            </div>
+            <div class={styles.center}>
+              <div class={styles.tableTitle}>总人数</div>
+              {subjects.value.map(item => (
+                <div class={styles.tableTr}>{item['总人数']}</div>
+              ))}
+            </div>
+          </div>
+          <div
+            class={[styles.detailRight, styles.center]}
+            onTouchmove={(e: Event) => e.stopPropagation()}
+            onMousemove={(e: Event) => e.stopPropagation()}>
+            {gradeList.value.map(item => (
+              <div style={{ color: item.color }}>
+                <div class={styles.tableTitle}>
+                  <div style={{ background: item.color }}></div>
+                  <div>{item.name}</div>
+                </div>
+                {subjects.value.map((_n, _index) => (
+                  <div class={styles.tableTr}>{_n[item.name] || 0}</div>
+                ))}
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

+ 249 - 0
src/views/home/component/MusicGroup.tsx

@@ -0,0 +1,249 @@
+import {
+  PropType,
+  computed,
+  defineComponent,
+  nextTick,
+  onMounted,
+  ref,
+  toRefs,
+  watch
+} from 'vue';
+import styles from '../index.module.less';
+import icons from '../icons.json';
+import { Badge, Icon, Image, Popover } from 'vant';
+import * as echarts from 'echarts';
+import { IMusicGradeDistribution } from '../type';
+import icon_3 from '../image/icon_3.png';
+
+type EChartsOption = echarts.EChartsOption;
+
+const colors = [
+  '91, 143, 249',
+  '90, 216, 166',
+  '232, 104, 74',
+  '93, 112, 146',
+  '109, 200, 236',
+  '255, 149, 48',
+  '184, 123, 221',
+  '146, 222, 151',
+
+  '91, 143, 249',
+  '90, 216, 166',
+  '232, 104, 74',
+  '93, 112, 146',
+  '109, 200, 236',
+  '255, 149, 48',
+  '184, 123, 221',
+  '146, 222, 151'
+];
+
+export default defineComponent({
+  name: 'MusicGroup',
+  props: {
+    data: {
+      type: Array as PropType<IMusicGradeDistribution[]>,
+      default: () => []
+    }
+  },
+  setup(props) {
+    const activeSubject = ref('');
+    const { data } = toRefs(props);
+
+    /** 根据声部 过滤数据源 */
+    const list = computed(() => {
+      // 初始化 声部
+      if (!activeSubject.value) {
+        activeSubject.value =
+          data.value.find(item => item.subjectName)?.subjectName || '';
+      }
+      return data.value.filter(
+        item => item.subjectName === activeSubject.value
+      );
+    });
+
+    watch(
+      () => list.value,
+      () => {
+        nextTick(() => {
+          handleInit();
+        })
+      }
+    );
+
+    /** 乐团列表 */
+    const musicGroups = computed(() => {
+      const _list = Array.from(
+        new Set(data.value.map(item => item.musicGroupName))
+      );
+      const obj: { [_key: string]: { text: string; color: string } } =
+        _list.reduce(
+          (
+            obj: { [_key: string]: { text: string; color: string } },
+            value: string,
+            index: number
+          ) => {
+            obj[value] = {
+              text: value,
+              color: colors[index]
+            };
+            return obj;
+          },
+          {}
+        );
+
+      return obj;
+    });
+
+    /** 声部列表 */
+    const subjects = computed(() => {
+      const _names = Array.from(
+        new Set(data.value.map(item => item.subjectName))
+      ).filter(Boolean);
+      return _names.map(n => {
+        return {
+          text: n
+        };
+      });
+    });
+
+    /** 年级列表 */
+    const gradeList = computed(() => {
+      return Array.from(new Set(data.value.map(item => item.grade)));
+    });
+
+    const filterData = (data: IMusicGradeDistribution[]) => {
+      const musicGrade: {
+        [_key: string]: { [_key: string]: IMusicGradeDistribution[] };
+      } = {};
+      for (let i = 0; i < data.length; i++) {
+        const item = data[i];
+        if (musicGrade[item.grade]) {
+          // 年级有乐团
+          if (musicGrade[item.grade][item.musicGroupName]) {
+            musicGrade[item.grade][item.musicGroupName].push(item);
+          } else {
+            musicGrade[item.grade][item.musicGroupName] = [item];
+          }
+        } else {
+          musicGrade[item.grade] = {};
+          musicGrade[item.grade][item.musicGroupName] = [item];
+        }
+      }
+      return musicGrade;
+    };
+    let myChart: echarts.ECharts;
+    const handleInit = () => {
+      if (!list.value.length) return;
+      const musicGrade = filterData(list.value);
+      console.log('🚀 ~ musicGrade:', musicGrade);
+      if (myChart) {
+        myChart.dispose();
+      }
+      const chartDom = document.getElementById('musicGroupEcharts')!;
+      myChart = echarts.init(chartDom, {}, { renderer: 'svg' });
+      const option: EChartsOption = {
+        grid: {
+          left: 8,
+          top: 5,
+          right: 10,
+          bottom: 5,
+          containLabel: true
+        },
+        xAxis: {
+          type: 'value',
+          boundaryGap: [0, 0.01]
+        },
+        yAxis: {
+          type: 'category',
+          axisTick: {
+            show: false
+          },
+          axisLabel: {
+            color: '#777',
+            fontSize: 10
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#F2F2F2'
+            }
+          },
+          data: gradeList.value
+        },
+        series: Object.values(musicGroups.value).map(group => {
+          const _data = gradeList.value.map(grade => {
+            const _list = musicGrade[grade]?.[group.text] || [];
+            const total = _list.reduce(
+              (_total, value) => (_total += value.studentNum),
+              0
+            );
+            return total;
+          });
+          return {
+            data: _data,
+            type: 'bar',
+            label: {
+              show: true,
+              position: 'right',
+              formatter: _item => {
+                return _item.value ? `${_item.value}` : '';
+              }
+            },
+            itemStyle: {
+              color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
+                { offset: 0, color: `rgba(${group.color}, .5)` },
+                { offset: 1, color: `rgba(${group.color}, 1)` }
+              ])
+            },
+            barMaxWidth: 20
+          };
+        })
+      };
+
+      option && myChart.setOption(option);
+    };
+
+    return () => (
+      <div class={styles.item}>
+        <div class={styles.itemTop}>
+          <Image class={styles.icon} src={icons[3]} />
+          <div class={styles.title}>乐团年级分布</div>
+          <div class={styles.des}>(单位:人)</div>
+          <Popover
+            actions={subjects.value}
+            placement="bottom-end"
+            onSelect={value => {
+              activeSubject.value = value.text;
+            }}>
+            {{
+              reference: () => (
+                <div>
+                  {activeSubject.value}{' '}
+                  <Icon name="arrow-down" color="rgba(216,216,216,1)" />
+                </div>
+              )
+            }}
+          </Popover>
+        </div>
+        <div
+          style={{ display: data.value.length ? '' : 'none' }}
+          class={styles.musicGroupContainer}>
+          <div class={styles.musicGroupEcharts} id="musicGroupEcharts"></div>
+          <div class={styles.tags}>
+            {Object.values(musicGroups.value).map(item => (
+              <div class={styles.tag}>
+                <Badge dot color={`rgb(${item.color})`} />
+                <div>{item.text}</div>
+              </div>
+            ))}
+          </div>
+        </div>
+
+        {!list.value.length && (
+          <div class={[styles.gradeContainer, styles.itemEmtry]}>
+            <Image src={icon_3} />
+          </div>
+        )}
+      </div>
+    );
+  }
+});

+ 131 - 0
src/views/home/component/Practice.tsx

@@ -0,0 +1,131 @@
+import { PropType, defineComponent, onMounted, toRefs, watch } from 'vue';
+import styles from '../index.module.less';
+import icons from '../icons.json';
+import { Badge, Grid, GridItem, Icon, Image, Progress, Tab, Tabs } from 'vant';
+import { IStudentLessons } from '../type';
+const colors = [
+  {
+    name: '正常',
+    background: 'linear-gradient(180deg, #02E2DB 0%, #01C1B5 100%)'
+  },
+  {
+    text: '迟到',
+    background:
+      'linear-gradient(180deg, rgba(148, 194, 253, 0.85) 0%, rgba(91, 143, 249, 0.85) 100%)'
+  },
+  {
+    text: '请假',
+    background:
+      'linear-gradient(180deg, rgba(251,224,49,0.85) 0%, rgba(246,189,22,0.85) 100%)'
+  },
+  {
+    text: '旷课',
+    background:
+      'linear-gradient(180deg, rgba(245,161,129,0.85) 0%, rgba(232,104,74,0.85) 100%)'
+  }
+];
+
+export default defineComponent({
+  name: 'Practice',
+  props: {
+    data: {
+      type: Object as PropType<IStudentLessons>,
+      default: () => ({})
+    }
+  },
+  emits: ['change'],
+  setup(props, { emit }) {
+    const { data } = toRefs(props);
+    return () => (
+      <div class={styles.item}>
+        <div class={styles.top}>
+          <Image class={styles.iconRight} src={icons.right} />
+          <span>学员练习</span>
+          <Image class={styles.iconLeft} src={icons.left} />
+        </div>
+        <div class={styles.tabsContainer}>
+          <Tabs
+            shrink
+            active={"本周"}
+            onChange={value => {
+              console.log(value);
+              emit('change', value);
+            }}>
+            <Tab name="本周" title="本周"></Tab>
+            <Tab name="本月" title="本月"></Tab>
+            <Tab name="本学期" title="本学期"></Tab>
+          </Tabs>
+        </div>
+        <div class={styles.practiceContainer}>
+          <Grid border={false}>
+            <GridItem>
+              {{
+                icon: () => (
+                  <div>
+                    <span class={styles.tagNum}>{data.value.commitRate || 0}</span>%
+                  </div>
+                ),
+                text: () => <div>提交率</div>
+              }}
+            </GridItem>
+            <GridItem>
+              {{
+                icon: () => (
+                  <div>
+                    <span class={styles.tagNum}>{data.value.passRate || 0}</span>%
+                  </div>
+                ),
+                text: () => <div>合格率</div>
+              }}
+            </GridItem>
+          </Grid>
+          <div class={styles.progressItem}>
+            <Progress
+              percentage={data.value.expectNum || 1}
+              strokeWidth={8}
+              trackColor="transparent"
+              showPivot={false}
+              color="linear-gradient(to right, #9AC6FF, #8DB2FF)"></Progress>
+            <div>
+              应提交{' '}
+              <span style={{ color: '#8DB2FF' }} class={styles.tagNum}>
+                {data.value.expectNum || 0}
+              </span>
+              人
+            </div>
+          </div>
+          <div class={styles.progressItem}>
+            <Progress
+              percentage={data.value.actualNum || 1}
+              strokeWidth={8}
+              trackColor="transparent"
+              showPivot={false}
+              color="linear-gradient(to right, #91F4DA, #85DFCF)"></Progress>
+            <div>
+              实际提交{' '}
+              <span style={{ color: '#85DFCF' }} class={styles.tagNum}>
+                {data.value.actualNum || 0}
+              </span>
+              人
+            </div>
+          </div>
+          <div class={styles.progressItem}>
+            <Progress
+              percentage={data.value.passNum || 1}
+              strokeWidth={8}
+              trackColor="transparent"
+              showPivot={false}
+              color="linear-gradient(to right, #FFDCAC, #FFD378)"></Progress>
+            <div>
+              合格提交{' '}
+              <span style={{ color: '#FFD378' }} class={styles.tagNum}>
+                {data.value.passNum || 0}
+              </span>
+              人
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

+ 147 - 0
src/views/home/component/Subjects.tsx

@@ -0,0 +1,147 @@
+import {
+  PropType,
+  defineComponent,
+  nextTick,
+  onMounted,
+  ref,
+  toRefs,
+  watch
+} from 'vue';
+import styles from '../index.module.less';
+import icons from '../icons.json';
+import { Badge, Image, Skeleton, SkeletonParagraph } from 'vant';
+import * as echarts from 'echarts';
+import { ISubjectDistribution } from '../type';
+import icon_2 from '../image/icon_2.png';
+
+type EChartsOption = echarts.EChartsOption;
+
+const colors = [
+  '#5B8FF9',
+  '#F6BD16',
+  '#5AD8A6',
+  '#E8684A',
+  '#5D7092',
+  '#6DC8EC'
+];
+
+export default defineComponent({
+  name: 'Subjects',
+  props: {
+    list: {
+      type: Array as PropType<ISubjectDistribution[]>,
+      default: () => []
+    }
+  },
+  setup(props) {
+    const firstInit = ref(false);
+    const { list } = toRefs(props);
+    watch(
+      () => list.value,
+      () => {
+        firstInit.value = true;
+        nextTick(() => {
+          handleInit();
+        });
+      }
+    );
+    let myChart: echarts.ECharts;
+    const handleInit = () => {
+      if (!list.value.length) return;
+      if (myChart) {
+        myChart.dispose();
+      }
+
+      const chartDom = document.getElementById('subjectEcharts')!;
+      myChart = echarts.init(chartDom, {}, { renderer: 'svg' });
+      const option: EChartsOption = {
+        grid: {
+          left: 8,
+          top: 16,
+          right: 5,
+          bottom: 5,
+          containLabel: true
+        },
+        xAxis: {
+          type: 'category',
+          axisTick: {
+            show: false
+          },
+          axisLabel: {
+            color: '#333',
+            fontSize: 10,
+            rotate: 30
+          },
+          axisLine: {
+            lineStyle: {
+              color: '#F2F2F2'
+            }
+          },
+          data: list.value.map(item => item.subjectName)
+        },
+        yAxis: {
+          type: 'value'
+        },
+        series: [
+          {
+            data: list.value.map(item => item.studentNum),
+            type: 'bar',
+            label: {
+              show: true,
+              position: 'top'
+            },
+            itemStyle: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                { offset: 0, color: '#FFD1A9' },
+                { offset: 1, color: '#FFCAD0' }
+              ])
+            },
+            barWidth: 20
+          }
+        ]
+      };
+
+      option && myChart.setOption(option);
+    };
+
+    return () => (
+      <div class={styles.item}>
+        <div class={styles.itemTop}>
+          <Image class={styles.icon} src={icons[2]} />
+          <div class={styles.title}>声部分布</div>
+          <div class={styles.des}>(单位:人)</div>
+        </div>
+
+        {!firstInit.value && (
+          <div class={[styles.subjectContainer, styles.itemEmtry]}>
+            <Skeleton loading={true}>
+              {{
+                template: () => (
+                  <div style={{ display: 'flex', width: '100%' }}>
+                    <div style={{ width: '100%' }}>
+                      <SkeletonParagraph />
+                      <SkeletonParagraph />
+                      <SkeletonParagraph />
+                      <SkeletonParagraph />
+                    </div>
+                  </div>
+                )
+              }}
+            </Skeleton>
+          </div>
+        )}
+        <div
+          style={{ display: list.value.length ? '' : 'none' }}
+          class={styles.subjectContainer}>
+          <div class={styles.subjectEcharts} id="subjectEcharts"></div>
+        </div>
+
+        {firstInit.value && !list.value.length && (
+          <div class={[styles.gradeContainer, styles.itemEmtry]}>
+            <Image src={icon_2} />
+          </div>
+        )}
+      </div>
+    );
+  }
+});

+ 114 - 0
src/views/home/contentItem.tsx

@@ -0,0 +1,114 @@
+import { defineComponent, onMounted, reactive, toRefs } from 'vue';
+import CurrentStudent from './component/CurrentStudent';
+import Subjects from './component/Subjects';
+import MusicGroup from './component/MusicGroup';
+import DetailData from './component/DetailData';
+import Attendance from './component/Attendance';
+import Practice from './component/Practice';
+import {
+  api_schoolIndexAttendanceStat,
+  api_schoolIndexLessonStat,
+  api_schoolIndexStat
+} from './api';
+import {
+  IGradeDistribution,
+  IMusicGradeDistribution,
+  IStudentAttendance,
+  IStudentLessons,
+  ISubjectDistribution,
+  ISubjectGradeDistribution
+} from './type';
+
+export default defineComponent({
+  name: 'content',
+  props: {
+    musicGroupId: {
+      type: String,
+      default: ''
+    }
+  },
+  setup(props) {
+    const data = reactive({
+      gradeDistributions: [] as IGradeDistribution[],
+      /** 声部分布 */
+      subjectDistributions: [] as ISubjectDistribution[],
+      /** 年级分布 */
+      musicGradeDistributions: [] as IMusicGradeDistribution[],
+      subjectGradeDistributions: [] as ISubjectGradeDistribution[],
+      /** 出勤 */
+      studentAttendance: {} as IStudentAttendance,
+      /** 练习 */
+      studentLessons: {} as IStudentLessons
+    });
+    const { musicGroupId } = toRefs(props);
+    /** 统计 */
+    const getSchool = async () => {
+      const res = await api_schoolIndexStat({});
+      if (res.data) {
+        const {
+          gradeDistributions,
+          subjectDistributions,
+          musicGradeDistributions,
+          subjectGradeDistributions
+        } = res.data;
+        if (gradeDistributions) {
+          data.gradeDistributions = gradeDistributions;
+        }
+        if (subjectDistributions) {
+          data.subjectDistributions = subjectDistributions;
+        }
+        if (musicGradeDistributions) {
+          data.musicGradeDistributions = musicGradeDistributions;
+        }
+        if (subjectGradeDistributions) {
+          data.subjectGradeDistributions = subjectGradeDistributions;
+        }
+      }
+    };
+
+    /** 出勤统计 */
+    const getAttendanceStat = async () => {
+      const res = await api_schoolIndexAttendanceStat({});
+      if (res.data) {
+        data.studentAttendance = res.data;
+      }
+    };
+
+    /** 练习统计 */
+    const getLessonStat = async () => {
+      const res = await api_schoolIndexLessonStat({});
+      if (res.data) {
+        data.studentLessons = res.data;
+      }
+    };
+
+    const init = () => {
+      getSchool();
+      getAttendanceStat();
+      getLessonStat();
+    };
+    onMounted(() => {
+      init();
+    });
+    return () => (
+      <div>
+        <CurrentStudent list={data.gradeDistributions} />
+        <Subjects list={data.subjectDistributions} />
+        <MusicGroup data={data.musicGradeDistributions} />
+        <DetailData data={data.subjectGradeDistributions} />
+        <Attendance
+          data={data.studentAttendance}
+          onChange={() => {
+            getAttendanceStat();
+          }}
+        />
+        <Practice
+          data={data.studentLessons}
+          onChange={() => {
+            getLessonStat();
+          }}
+        />
+      </div>
+    );
+  }
+});

Diff do ficheiro suprimidas por serem muito extensas
+ 1 - 0
src/views/home/icons.json


BIN
src/views/home/image/icon_1.png


BIN
src/views/home/image/icon_2.png


BIN
src/views/home/image/icon_3.png


BIN
src/views/home/image/icon_4.png


+ 337 - 0
src/views/home/index.module.less

@@ -0,0 +1,337 @@
+.homeTab {
+    & :global {
+        .van-tabs__nav:not(.van-tabs__nav--shrink) {
+            background: rgba(248, 249, 252, 1);
+
+            .van-tab {
+                z-index: 10;
+            }
+
+            .van-tabs__line {
+                bottom: 0.7rem;
+                width: 1.5rem;
+                height: 6px;
+                background: linear-gradient(250deg, rgba(45, 199, 170, 0.22) 0%, #2DC7AA 100%);
+            }
+        }
+
+
+    }
+}
+
+.item {
+    margin: 12px;
+    border-radius: 10px;
+    background: #FFFFFF;
+}
+.itemEmtry{
+    padding: 12px;
+}
+
+.top {
+    position: relative;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: calc(100% - 4px);
+    height: 35px;
+    background: linear-gradient(180deg, #CFF2EB 0%, rgba(255, 255, 255, 0) 100%);
+    border-radius: 10px;
+    box-shadow: -2px -2px 0 rgba(255, 255, 255, 1), 0 -2px 0 rgba(255, 255, 255, 1), 2px 0 0 rgba(255, 255, 255, 1);
+    margin: 0 auto;
+
+    .iconLeft,
+    .iconRight {
+        width: 15px;
+        height: 8px;
+    }
+
+    span {
+        line-height: 21px;
+        margin: 0 7px;
+        font-size: 14px;
+        color: #333;
+        font-weight: bold;
+    }
+}
+
+.itemTop {
+    position: relative;
+    z-index: 1;
+    display: flex;
+    align-items: center;
+    margin-top: -2px;
+    padding: 12px;
+    background-color: #fff;
+    border-radius: 10px 10px 0 0;
+
+    .icon {
+        width: 18px;
+        height: 18px;
+        margin-right: 8px;
+    }
+
+    .title {
+        color: #333;
+        font-size: 14px;
+        line-height: 20px;
+        margin-right: 4px;
+    }
+
+    .des {
+        color: #aaa;
+        font-size: 14px;
+        line-height: 20px;
+    }
+
+    :global {
+        .van-popover__wrapper {
+            margin-left: auto;
+        }
+    }
+}
+
+.tabsContainer {
+    display: flex;
+    align-items: center;
+    height: var(--van-tabs-line-height);
+
+    :global {
+        .van-tabs__line {
+            width: 14px;
+            height: 4px;
+            border-radius: 2px;
+        }
+    }
+
+    .tagRight {
+        display: flex;
+        align-items: center;
+        margin-left: auto;
+        margin-right: 14px;
+        color: #333;
+        height: 100%;
+    }
+}
+
+.gradeContainer {
+    display: flex;
+    align-items: center;
+
+    .gradeEcharts {
+        width: 180px;
+        height: 180px;
+    }
+
+    .tags {
+        flex: 1;
+        display: flex;
+        flex-wrap: wrap;
+        font-size: 12px;
+        color: #777;
+        line-height: 16px;
+
+        .tag {
+            width: 50%;
+            display: flex;
+            align-items: center;
+            justify-content: space-evenly;
+            padding: 6px 5px;
+
+            :global {
+                .van-badge--dot {
+                    transform: none;
+                    width: 5px;
+                    height: 5px;
+                }
+            }
+        }
+
+        .tagNum {
+            color: #333;
+            font-weight: bold;
+            font-family: DINAlternate-Bold, DINAlternate;
+        }
+    }
+}
+
+.subjectContainer {
+    .subjectEcharts {
+        width: 100%;
+        height: 200px;
+    }
+}
+
+.musicGroupContainer {
+    .musicGroupEcharts {
+        width: 100%;
+        min-height: 400px;
+    }
+
+    .tags {
+        flex: 1;
+        display: flex;
+        justify-content: space-evenly;
+        flex-wrap: wrap;
+        font-size: 12px;
+        color: #777;
+        line-height: 16px;
+        padding: 12px;
+
+        .tag {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            padding: 6px 5px;
+
+            :global {
+                .van-badge--dot {
+                    transform: none;
+                    width: 5px;
+                    height: 5px;
+                    margin-right: 4px;
+                }
+            }
+        }
+    }
+}
+
+.detailDataContainer {
+    display: flex;
+    font-family: DINAlternate-Bold, DINAlternate;
+    .detailLeft {
+        display: flex;
+        padding-left: 12px;
+        padding-bottom: 12px;
+    }
+
+    .detailRight {
+        display: flex;
+        flex: 1;
+        overflow-x: auto;
+        background: rgba(249, 249, 249, 1);
+        padding-bottom: 12px;
+        &::-webkit-scrollbar {
+            width: 0;
+            display: none;
+        }
+    }
+
+    .tableTitle {
+        position: relative;
+        padding: 6px;
+        font-size: 10px;
+        line-height: 14px;
+        white-space: nowrap;
+        & > div:first-child{
+            position: absolute;
+            left: 0;
+            top: 0;
+            width: 100%;
+            height: 100%;
+            opacity: .1;
+        }
+        & > div:last-child{
+            position: relative;
+            z-index: 2;
+        }
+    }
+
+    .center {
+        text-align: center;
+    }
+
+    .tableTr {
+        padding: 7px 6px;
+        font-size: 12px;
+        line-height: 17px;
+        white-space: nowrap;
+    }
+    .tableTr:not(:last-child){
+        border-bottom: 1Px solid #F2F2F2;
+    }
+}
+
+.attendanceContainer {
+    display: flex;
+    align-items: center;
+
+    .attendanceEcharts {
+        width: 170px;
+        height: 170px;
+    }
+
+    .tags {
+        flex: 1;
+        display: flex;
+        flex-wrap: wrap;
+        font-size: 12px;
+        color: #777;
+        line-height: 16px;
+
+        .tag {
+            width: 50%;
+            display: flex;
+            align-items: center;
+            padding: 6px 5px;
+
+            .des {
+                margin: 0 5px;
+            }
+
+            :global {
+                .van-badge--dot {
+                    transform: none;
+                    width: 5px;
+                    height: 5px;
+                }
+            }
+        }
+
+        .rect {
+            width: 8px;
+            height: 8px;
+            border-radius: 2px;
+
+
+        }
+
+        .tagNum {
+            color: #333;
+            font-weight: bold;
+            font-family: DINAlternate-Bold, DINAlternate;
+        }
+    }
+}
+
+.practiceContainer {
+    :global {
+        .van-grid-item {
+            margin: 10px 0;
+
+            .van-grid-item__content {
+                padding-top: 0;
+                padding-bottom: 0;
+                font-size: 12px;
+                color: #777;
+                line-height: 18px;
+            }
+        }
+
+        .van-grid-item:first-child {
+            border-right: 1px dashed #777;
+        }
+    }
+
+    .tagNum {
+        color: #333;
+        font-weight: bold;
+        font-family: DINAlternate-Bold, DINAlternate;
+        font-size: 22px;
+    }
+
+    .progressItem {
+        padding: 5px 15px;
+        line-height: 34px;
+    }
+}

+ 29 - 6
src/views/home/index.tsx

@@ -1,14 +1,37 @@
-import { Button } from 'vant';
-import { defineComponent } from 'vue';
+import { Button, Tab, Tabs } from 'vant';
+import { defineComponent, onMounted, reactive } from 'vue';
+import styles from './index.module.less'
+
+import { api_musicGroupFindByCooperationId, api_schoolIndexStat } from './api';
+import { state } from '@/state';
+import ContentItem from './contentItem';
+
 
 export default defineComponent({
   name: 'home-page',
   setup() {
+    const homeData = reactive({
+
+    })
+    /** 获取学校乐团列表 */
+    const getMusicGroup = async () => {
+      await api_musicGroupFindByCooperationId(state?.user?.data.schoolId)
+    }
+
+    
+    onMounted(() => {
+      getMusicGroup()
+    })
     return () => (
-      <div style={{ fontSize: '18px' }}>
-        <Button type="primary" block>
-          主要按钮
-        </Button>
+      <div class={styles.home}>
+        <Tabs class={styles.homeTab} animated swipeable lazyRender sticky>
+          <Tab title="数据汇总">
+            <ContentItem />
+          </Tab>
+          <Tab title="武汉小学乐团"></Tab>
+          <Tab title="武汉小学预备团"></Tab>
+          <Tab title="武汉小学标准团"></Tab>
+        </Tabs>
       </div>
     );
   }

+ 72 - 0
src/views/home/type.ts

@@ -0,0 +1,72 @@
+/** 年级分布 */
+export interface IGradeDistribution {
+  /** 年级 */
+  grade: string;
+  /** 在读人数 */
+  studentNum: number;
+}
+
+/** 乐团年级分布 */
+export interface IMusicGradeDistribution {
+  /** 年级 */
+  grade: string;
+  /** 乐团名称 */
+  musicGroupName: string;
+  /** 在读人数 */
+  studentNum: number;
+  /** 声部编号 */
+  subjectId: number;
+  /** 声部名称 */
+  subjectName: string;
+}
+
+/** 学员出勤 */
+export interface IStudentAttendance {
+  /** 出勤率 */
+  attendanceRate: number
+  /** 迟到人数 */
+  lateNum: number
+  /** 请假人数 */
+  leaveNum: number
+  /** 正常人数 */
+  normalNum: number
+  /** 总人数 */
+  totalNum: number
+  /** 旷课人数 */
+  truantNum: number
+  [_key: string]: any
+}
+
+/** 学员练习 */
+export interface IStudentLessons {
+  /** 实际提交人数 */
+  actualNum: number;
+  /** 提交率 */
+  commitRate: number;
+  /** 应交人数 */
+  expectNum: number;
+  /** 合格提交人数 */
+  passNum: number;
+  /** 合格率 */
+  passRate: number;
+}
+
+/** 声部分布 */
+export interface ISubjectDistribution {
+  /** 在读人数 */
+  studentNum: number;
+  /** 声部 */
+  subjectName: string;
+}
+
+export interface ISubjectGradeDistribution {
+  /** 年级 */
+  grade: string;
+  /** 在读人数 */
+  studentNum: number;
+  /** 声部编号 */
+  subjectId: number;
+  /** 声部名称 */
+  subjectName: string;
+}
+

+ 1 - 1
src/views/student-manage/api.ts

@@ -85,5 +85,5 @@ export const api_studentManageUpdateGrade = (data: any) => {
  * @returns 
  */
 export const api_studentManageQuitMusicGroup = (data: any) => {
-  return request.post('/api-web/studentManage/quitMusicGroup', { data });
+  return request.post('/api-web/studentManage/quitMusicGroup', { data, requestType: 'form' });
 };

+ 2 - 1
src/views/student-manage/component/m-student/index.module.less

@@ -35,6 +35,7 @@
     .statusBox{
         display: flex;
         justify-content: flex-end;
+        white-space: nowrap;
         & > div{
             font-size: 14px;
             color: #fff;
@@ -61,7 +62,7 @@
 .studentInfo{
     :global{
         .van-cell__title {
-            flex: 3;
+            flex: 2;
         }
     }
 }

+ 6 - 7
src/views/student-manage/component/m-student/index.tsx

@@ -26,13 +26,12 @@ export default defineComponent({
   emits: ['quit', 'contact'],
   setup(props, { emit }) {
     const router = useRouter();
-    const { item, isLink } = toRefs(props);
-    const valueType = props.valueType;
+    const { item, isLink, valueType } = toRefs(props);
     return () => (
       <Cell
         class={[
           styles.student,
-          valueType === 'status' ? '' : styles.studentInfo
+          valueType.value === 'status' ? '' : styles.studentInfo
         ]}
         center
         border={false}
@@ -72,12 +71,12 @@ export default defineComponent({
             <div
               class={styles.title}
               onClick={() => {
-                if (valueType === 'statued') return;
+                if (valueType.value === 'statued') return;
                 console.log('去聊天');
                 emit('contact')
               }}>
               {item.value.studentName}{' '}
-              {valueType === 'statued' ? (
+              {valueType.value === 'statued' ? (
                 <Image class={styles.iconIm} src={icons.icon_im_dis} />
               ) : (
                 <Image class={styles.iconIm} src={icons.icon_im} />
@@ -86,13 +85,13 @@ export default defineComponent({
           ),
           value: () => (
             <>
-              {valueType === 'status' ? (
+              {valueType.value === 'status' ? (
                 <div class={styles.statusBox}>
                   <div class={styles.status} onClick={() => emit('quit')}>
                     退团
                   </div>
                 </div>
-              ) : valueType === 'statuing' ? (
+              ) : valueType.value === 'statuing' ? (
                 <div class={styles.statusBox}>
                   <div class={styles.statuing}>退团中</div>
                 </div>

+ 2 - 1
src/views/student-manage/detail/index.tsx

@@ -195,8 +195,9 @@ export default defineComponent({
           reasonEnum: 'OTHER',
           userId: detailData.student.studentId
         });
-        detailData.quitConfirmShow = false;
         if (res.code === 200) {
+          detailData.quitConfirmShow = false;
+          detailData.quitShow = false;
           detailData.quitList = [];
           getDatail();
         }

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff