Bläddra i källkod

Merge branch 'iteration_0307' into jenkins

lex 2 år sedan
förälder
incheckning
9c0d12d261
41 ändrade filer med 2283 tillägg och 15 borttagningar
  1. 24 1
      src/router/routes-school.ts
  2. 256 0
      src/school/train-report/components/list.tsx
  3. BIN
      src/school/train-report/images/icon-arrow-point.png
  4. BIN
      src/school/train-report/images/icon-class.png
  5. BIN
      src/school/train-report/images/icon-photo.png
  6. BIN
      src/school/train-report/images/icon-share.png
  7. BIN
      src/school/train-report/images/month/banner-month.png
  8. BIN
      src/school/train-report/images/month/icon-train-month.png
  9. BIN
      src/school/train-report/images/month/popup-week-banner.png
  10. BIN
      src/school/train-report/images/month/share-month-banner.png
  11. BIN
      src/school/train-report/images/month/teacher-icon-arrow-line.png
  12. BIN
      src/school/train-report/images/month/teacher-icon-point.png
  13. BIN
      src/school/train-report/images/month/teacher-section-title-bg.png
  14. BIN
      src/school/train-report/images/month/teacher-section-title-bg1.png
  15. BIN
      src/school/train-report/images/month/teacher-section-title-bg2.png
  16. BIN
      src/school/train-report/images/month/teacher-train-bg.png
  17. BIN
      src/school/train-report/images/month/teacher-train-bg1.png
  18. BIN
      src/school/train-report/images/popup-qrcode-bg.png
  19. BIN
      src/school/train-report/images/small-icon.png
  20. BIN
      src/school/train-report/images/week/banner-week.png
  21. BIN
      src/school/train-report/images/week/icon-arrow-line.png
  22. BIN
      src/school/train-report/images/week/icon-point.png
  23. BIN
      src/school/train-report/images/week/icon-train-week.png
  24. BIN
      src/school/train-report/images/week/popup-week-banner.png
  25. BIN
      src/school/train-report/images/week/section-title-bg.png
  26. BIN
      src/school/train-report/images/week/section-title-bg1.png
  27. BIN
      src/school/train-report/images/week/section-title-bg2.png
  28. BIN
      src/school/train-report/images/week/share-week-banner.png
  29. BIN
      src/school/train-report/images/week/train-bg.png
  30. 64 0
      src/school/train-report/index.module.less
  31. 62 0
      src/school/train-report/index.tsx
  32. 194 0
      src/school/train-report/modal/index.module.less
  33. 80 0
      src/school/train-report/modal/orchestra-num.tsx
  34. 179 0
      src/school/train-report/modal/student-attendance.tsx
  35. 178 0
      src/school/train-report/modal/teacher-attendance.tsx
  36. 175 0
      src/school/train-report/modal/train-class.tsx
  37. 94 0
      src/school/train-report/modal/train-progress.tsx
  38. 333 0
      src/school/train-report/month-report.tsx
  39. 313 0
      src/school/train-report/report.module.less
  40. 319 0
      src/school/train-report/week-report.tsx
  41. 12 14
      src/views/subject-echarts/index.tsx

+ 24 - 1
src/router/routes-school.ts

@@ -23,6 +23,22 @@ const noLoginRouter = [
     meta: {
       title: '乐团管理老师注册'
     }
+  },
+  {
+    path: '/train-week-report',
+    name: 'train-week-report',
+    component: () => import('@/school/train-report/week-report'),
+    meta: {
+      title: '训练周报'
+    }
+  },
+  {
+    path: '/train-month-report',
+    name: 'train-month-report',
+    component: () => import('@/school/train-report/month-report'),
+    meta: {
+      title: '训练月报'
+    }
   }
 ]
 
@@ -406,7 +422,14 @@ export default [
           title: '考勤规则设置'
         }
       },
-
+      {
+        path: '/train-report',
+        name: 'train-report',
+        component: () => import('@/school/train-report'),
+        meta: {
+          title: '训练报告'
+        }
+      }
       //
     ]
   },

+ 256 - 0
src/school/train-report/components/list.tsx

@@ -0,0 +1,256 @@
+import request from '@/helpers/request'
+import {
+  Button,
+  Cell,
+  CellGroup,
+  DatePicker,
+  DatePickerColumnType,
+  List,
+  Picker,
+  Popup
+} from 'vant'
+import { defineComponent, onMounted, reactive, ref } from 'vue'
+import styles from '../index.module.less'
+import { state as baseState } from '@/state'
+import iconOrchestra from '@/views/mine-orchestra/images/icon-or.png'
+import OEmpty from '@/components/o-empty'
+import { useRouter } from 'vue-router'
+import dayjs from 'dayjs'
+import { formatterDatePicker } from '@/helpers/utils'
+import OFullRefresh from '@/components/o-full-refresh'
+
+export default defineComponent({
+  name: 'train-list',
+  props: {
+    type: {
+      type: String,
+      default: 'WEEKLY'
+    },
+    orchestraList: {
+      type: Array,
+      default: () => []
+    }
+  },
+  setup(props) {
+    const columnsType = ref<DatePickerColumnType[]>(['year', 'month'])
+    const router = useRouter()
+    const state = reactive({
+      showPopoverTime: false,
+      timeName: dayjs().format('YYYY') + '年' + dayjs().format('MM') + '月',
+      currentDate: [dayjs().format('YYYY'), dayjs().format('MM')],
+      orchestraStatus: false,
+      selectOrchestra: {} as any,
+      isClick: false,
+      list: [] as any,
+      listState: {
+        dataShow: true, // 判断是否有数据
+        loading: false,
+        finished: false,
+        refreshing: false,
+        height: 0 // 页面头部高度,为了处理下拉刷新用的
+      },
+      params: {
+        page: 1,
+        rows: 20
+      }
+    })
+
+    const getList = async () => {
+      try {
+        if (state.isClick) return
+        state.isClick = true
+
+        const dateRange = dayjs(state.currentDate[0] + '-' + state.currentDate[1] + '01')
+        const startTime = dateRange.startOf('month').format('YYYY-MM-DD')
+        const endTime = dateRange.endOf('month').format('YYYY-MM-DD')
+        const { data } = await request.post('/api-school/orchestraReport/page', {
+          data: {
+            reportType: props.type,
+            page: state.params.page,
+            rows: state.params.rows,
+            orchestraId: state.selectOrchestra.value,
+            startTime,
+            endTime
+          }
+        })
+
+        state.listState.loading = false
+        state.listState.refreshing = false
+        // 处理重复请求数据
+        if (state.list.length > 0 && data.current === 1) {
+          return
+        }
+        state.list = state.list.concat(data.rows || [])
+        state.listState.finished = data.current >= data.pages
+        state.params.page = data.current + 1
+        state.listState.dataShow = state.list.length > 0
+        state.isClick = false
+      } catch {
+        state.listState.dataShow = false
+        state.listState.finished = true
+        state.listState.refreshing = false
+        state.isClick = false
+      }
+    }
+
+    const onRefresh = () => {
+      state.params.page = 1
+      state.list = []
+      state.listState.dataShow = true // 判断是否有数据
+      state.listState.loading = false
+      state.listState.finished = false
+      getList()
+    }
+
+    const onDetail = (item: any) => {
+      const path = props.type === 'WEEKLY' ? '/train-week-report' : '/train-month-report'
+      router.push({
+        path,
+        query: {
+          id: item.id
+        }
+      })
+    }
+
+    const checkTimer = (val: any) => {
+      state.timeName = val.selectedValues[0] + '年' + val.selectedValues[1] + '月'
+      state.showPopoverTime = false
+      onRefresh()
+    }
+
+    onMounted(async () => {
+      try {
+        // 判断是否有乐团
+        if (props.orchestraList.length > 0) {
+          // const orchestraId = sessionStorage.getItem('orchestraStoryId')
+          // if (orchestraId) {
+          //   const item = props.orchestraList.find((child: any) => child.value === orchestraId)
+          //   state.selectOrchestra = item || props.orchestraList[0]
+          // } else {
+          state.selectOrchestra = props.orchestraList[0]
+          // }
+        }
+
+        await getList()
+      } catch {
+        //
+      }
+    })
+
+    return () => (
+      <div
+        style={{
+          minHeight: 'calc(100vh  - var(--van-tabs-line-height))',
+          overflow: 'hidden'
+        }}
+      >
+        <div class={'searchGroup'}>
+          <div
+            class={['searchItem', state.showPopoverTime && 'searchItem-active']}
+            onClick={() => {
+              state.showPopoverTime = true
+            }}
+          >
+            {state.timeName}
+            <i class={'arrow'}></i>
+          </div>
+          <div
+            class={['searchItem', state.orchestraStatus && 'searchItem-active']}
+            onClick={() => {
+              state.orchestraStatus = true
+            }}
+          >
+            <span>{state.selectOrchestra.text || ' '}</span>
+            <i class={'arrow'}></i>
+          </div>
+        </div>
+        <div
+          style={{
+            overflowY: 'auto',
+            height: 'calc(100vh - var(--van-tabs-line-height)  - 1.17333rem)'
+          }}
+        >
+          {state.listState.dataShow ? (
+            <OFullRefresh
+              v-model:modelValue={state.listState.refreshing}
+              onRefresh={onRefresh}
+              style={{
+                minHeight: 'calc(100vh - var(--van-tabs-line-height)  - 1.17333rem)'
+              }}
+            >
+              <List
+                // v-model:loading={state.listState.loading}
+                finished={state.listState.finished}
+                finishedText=" "
+                style={{
+                  overflow: 'hidden'
+                }}
+                onLoad={getList}
+                immediateCheck={false}
+              >
+                {state.list.map((item: any) => (
+                  <Cell center class={styles.reportList}>
+                    {{
+                      title: () => <div>{item.orchestraName}</div>,
+                      value: () => (
+                        <Button type="primary" round size="small" onClick={() => onDetail(item)}>
+                          查看报告
+                        </Button>
+                      ),
+                      label: () =>
+                        props.type === 'WEEKLY' ? (
+                          <div class={styles.time}>
+                            {item.startTime}~{item.endTime}
+                          </div>
+                        ) : (
+                          <div class={styles.time}>{item.monthlyTime}</div>
+                        )
+                    }}
+                  </Cell>
+                ))}
+              </List>
+            </OFullRefresh>
+          ) : (
+            <OEmpty btnStatus={false} tips="暂无训练报告" />
+          )}
+        </div>
+
+        <Popup
+          v-model:show={state.showPopoverTime}
+          position="bottom"
+          round
+          teleport={'body'}
+          class={'popupBottomSearch'}
+        >
+          <DatePicker
+            onCancel={() => {
+              state.showPopoverTime = false
+            }}
+            onConfirm={checkTimer}
+            v-model={state.currentDate}
+            formatter={formatterDatePicker}
+            columnsType={columnsType.value}
+          />
+        </Popup>
+
+        <Popup
+          v-model:show={state.orchestraStatus}
+          position="bottom"
+          round
+          class={'popupBottomSearch'}
+          teleport="body"
+        >
+          <Picker
+            columns={props.orchestraList as any}
+            onCancel={() => (state.orchestraStatus = false)}
+            onConfirm={(val: any) => {
+              state.selectOrchestra = val.selectedOptions[0]
+              state.orchestraStatus = false
+              onRefresh()
+            }}
+          />
+        </Popup>
+      </div>
+    )
+  }
+})

BIN
src/school/train-report/images/icon-arrow-point.png


BIN
src/school/train-report/images/icon-class.png


BIN
src/school/train-report/images/icon-photo.png


BIN
src/school/train-report/images/icon-share.png


BIN
src/school/train-report/images/month/banner-month.png


BIN
src/school/train-report/images/month/icon-train-month.png


BIN
src/school/train-report/images/month/popup-week-banner.png


BIN
src/school/train-report/images/month/share-month-banner.png


BIN
src/school/train-report/images/month/teacher-icon-arrow-line.png


BIN
src/school/train-report/images/month/teacher-icon-point.png


BIN
src/school/train-report/images/month/teacher-section-title-bg.png


BIN
src/school/train-report/images/month/teacher-section-title-bg1.png


BIN
src/school/train-report/images/month/teacher-section-title-bg2.png


BIN
src/school/train-report/images/month/teacher-train-bg.png


BIN
src/school/train-report/images/month/teacher-train-bg1.png


BIN
src/school/train-report/images/popup-qrcode-bg.png


BIN
src/school/train-report/images/small-icon.png


BIN
src/school/train-report/images/week/banner-week.png


BIN
src/school/train-report/images/week/icon-arrow-line.png


BIN
src/school/train-report/images/week/icon-point.png


BIN
src/school/train-report/images/week/icon-train-week.png


BIN
src/school/train-report/images/week/popup-week-banner.png


BIN
src/school/train-report/images/week/section-title-bg.png


BIN
src/school/train-report/images/week/section-title-bg1.png


BIN
src/school/train-report/images/week/section-title-bg2.png


BIN
src/school/train-report/images/week/share-week-banner.png


BIN
src/school/train-report/images/week/train-bg.png


+ 64 - 0
src/school/train-report/index.module.less

@@ -0,0 +1,64 @@
+.train {
+  --van-tab-active-text-color: var(--van-primary-color);
+  --van-tab-text-color: #333;
+  --van-tab-font-size: 16px;
+  :global {
+    .van-tab {
+      // font-weight: 400;
+    }
+    .van-tabs__wrap {
+      // padding-bottom: 3px;
+    }
+  }
+
+  .cellGroup {
+    margin: 12px 13px 0;
+    overflow: hidden;
+    border-radius: 10px;
+    .select {
+      height: 45px;
+
+      .icon {
+        width: 17px;
+        height: 17px;
+        margin-right: 4px;
+        flex-shrink: 0;
+      }
+
+      :global {
+        .van-cell__title {
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+
+        .van-cell__right-icon {
+          color: #333;
+          transform: rotate(90deg);
+          margin: 0 0 0 4px;
+        }
+      }
+    }
+  }
+}
+
+.reportList {
+  width: auto;
+  margin: 12px 13px 0;
+  padding: 15px 12px;
+  border-radius: 10px;
+
+  :global {
+    .van-cell__title {
+      font-size: 16px;
+      color: #333333;
+    }
+    .van-cell_label {
+      font-size: 14px;
+      color: #777;
+    }
+    .van-cell__value {
+      flex: 0 auto;
+    }
+  }
+}

+ 62 - 0
src/school/train-report/index.tsx

@@ -0,0 +1,62 @@
+import request from '@/helpers/request'
+import { Tab, Tabs } from 'vant'
+import { defineComponent, reactive } from 'vue'
+import List from './components/list'
+import styles from './index.module.less'
+
+export default defineComponent({
+  name: 'train-report',
+  setup() {
+    const state = reactive({
+      loading: true,
+      orchestraList: [] as any
+    })
+    // 获取乐团列表
+    const getOrchestras = async () => {
+      try {
+        state.loading = true
+        const { data } = await request.post('/api-school/orchestra/page', {
+          data: {
+            page: 1,
+            rows: 100,
+            status: 'DONE'
+          }
+        })
+        const temps = data.rows || []
+        const s = [] as any
+        temps.forEach((item: any) => {
+          s.push({
+            text: item.name,
+            value: item.id
+          })
+        })
+        s.unshift({
+          text: '全部乐团',
+          value: ''
+        })
+        state.orchestraList = [...s]
+
+        state.loading = false
+      } catch {
+        //
+        state.loading = false
+      }
+    }
+
+    getOrchestras()
+    return () => (
+      <div class={styles.train}>
+        {!state.loading && (
+          <Tabs sticky lineWidth={20} lineHeight={4} swipeable animated>
+            <Tab title="周报" name="WEEKLY">
+              <List type="WEEKLY" orchestraList={state.orchestraList} />
+            </Tab>
+            <Tab title="月报" name="MONTHLY">
+              <List type="MONTHLY" orchestraList={state.orchestraList} />
+            </Tab>
+          </Tabs>
+        )}
+      </div>
+    )
+  }
+})

+ 194 - 0
src/school/train-report/modal/index.module.less

@@ -0,0 +1,194 @@
+.trainSection {
+  background: url('../images/week/section-title-bg.png') no-repeat top center;
+  background-size: contain;
+  position: relative;
+  z-index: 1;
+  margin: 0 13px 10px;
+  &.studentSection {
+    background: url('../images/week/section-title-bg1.png') no-repeat top center;
+    background-size: contain;
+  }
+  &.teacherSection {
+    background: url('../images/week/section-title-bg2.png') no-repeat top center;
+    background-size: contain;
+  }
+
+  &.teacherTrainSection {
+    background: url('../images/month/teacher-section-title-bg.png') no-repeat top center;
+    background-size: contain;
+    &.studentSection {
+      background: url('../images/month/teacher-section-title-bg1.png') no-repeat top center;
+      background-size: contain;
+    }
+    &.teacherSection {
+      background: url('../images/month/teacher-section-title-bg2.png') no-repeat top center;
+      background-size: contain;
+    }
+  }
+
+  .trainTitle {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-top: 4px;
+    height: 36px;
+    .allowLine {
+      width: 13px;
+      height: 13px;
+      margin-right: 6px;
+    }
+    .arrowPoint {
+      height: 5px;
+      width: 22px;
+      margin-right: 15px;
+    }
+    .name {
+      display: flex;
+      align-items: center;
+      padding-left: 12px;
+      font-size: 16px;
+      font-weight: 600;
+      color: #000977;
+      line-height: 22px;
+    }
+  }
+  .countNums {
+    display: flex;
+    align-items: center;
+    margin-right: 20px;
+    font-size: 14px;
+    font-weight: 600;
+    color: #ffffff;
+    line-height: 20px;
+    span {
+      color: #42ffe2;
+      padding: 0 6px;
+    }
+  }
+
+  .trainSectionContent {
+    background: linear-gradient(180deg, #ffffff 0%, #ffffff 16%, rgba(255, 255, 255, 0.6) 100%);
+    box-shadow: 0px -1px 9px 0px rgba(129, 187, 193, 0.49);
+    // border: 1px solid;
+    // border-image: linear-gradient(156deg, rgba(255, 255, 255, 0.11), rgba(96, 155, 198, 0.98)) 1 1;
+    padding: 12px;
+    border-radius: 20px;
+  }
+  .tContent {
+    background: #ffffff;
+    box-shadow: 0px 2px 22px 0px rgba(83, 109, 233, 0.79);
+    border-radius: 10px;
+    padding: 10px 10px 0;
+    display: flex;
+    flex-wrap: wrap;
+
+    .tItem {
+      margin: 0 10px 10px 0;
+      width: 53px;
+      height: 55px;
+      background: linear-gradient(
+        132deg,
+        rgba(199, 239, 243, 0.39) 0%,
+        rgba(229, 206, 251, 0.39) 40%,
+        rgba(147, 194, 254, 0.39) 100%
+      );
+      box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.62);
+      border-radius: 4px;
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      justify-content: center;
+      &:nth-child(5n + 5) {
+        margin-right: 0;
+      }
+      .pople {
+        font-size: 20px;
+        font-family: DINA;
+        font-weight: bold;
+        color: #1c4aff;
+        line-height: 24px;
+        span {
+          font-size: 12px;
+          color: #777777;
+          line-height: 17px;
+        }
+      }
+      .subjectName {
+        font-size: 12px;
+        color: #333333;
+        line-height: 17px;
+      }
+    }
+  }
+
+  .calssEchartTitle {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    width: 100%;
+    padding-bottom: 10px;
+    .unit {
+      font-size: 12px;
+      color: #777777;
+    }
+    .unitType {
+      background: linear-gradient(
+        132deg,
+        rgba(199, 239, 243, 0.25) 0%,
+        rgba(223, 213, 250, 0.25) 32%,
+        rgba(147, 194, 254, 0.25) 100%
+      );
+      box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.62);
+      border-radius: 12px;
+      padding: 4px 8px;
+      font-size: 12px;
+      color: #000977;
+    }
+  }
+  .classEcharts {
+    height: 210px;
+    width: 100%;
+  }
+
+  .tProgress {
+    display: block;
+  }
+  .progressItem {
+    padding-bottom: 16px;
+    .className {
+      padding-bottom: 6px;
+      font-size: 14px;
+      font-weight: 600;
+      color: #333333;
+      line-height: 20px;
+      .line {
+        display: inline-block;
+        width: 4px;
+        height: 12px;
+        background: #d1d1d1;
+        border-radius: 3px;
+        margin-right: 6px;
+      }
+    }
+    .classNum {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      font-size: 12px;
+      padding-bottom: 6px;
+      color: #333333;
+      span {
+        font-size: 16px;
+        padding: 0 4px;
+        font-weight: bold;
+        font-family: DINA;
+      }
+      .konowCount {
+        color: #777;
+        span {
+          color: #f44541;
+        }
+      }
+    }
+  }
+}

+ 80 - 0
src/school/train-report/modal/orchestra-num.tsx

@@ -0,0 +1,80 @@
+import { defineComponent, reactive, watch } from 'vue'
+import { Image } from 'vant'
+import styles from './index.module.less'
+import arrowLine from '../images/week/icon-arrow-line.png'
+import arrowPoint from '../images/icon-arrow-point.png'
+import teacherArrowLine from '../images/month/teacher-icon-arrow-line.png'
+import { reportCourseType } from '../week-report'
+
+export default defineComponent({
+  name: 'orchestra-num',
+  props: {
+    type: {
+      type: String,
+      default: 'week'
+    },
+    reportData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  setup(props) {
+    const forms = reactive({
+      total: 0,
+      list: [] as any
+    })
+    const formatCourse = () => {
+      const data = props.reportData || {}
+
+      for (const i in data) {
+        if (i === 'TOTAL') {
+          forms.total = data[i]
+        } else {
+          forms.list.push({
+            text: reportCourseType[i],
+            value: data[i]
+          })
+        }
+      }
+    }
+    formatCourse()
+
+    watch(
+      () => props.reportData,
+      () => {
+        formatCourse()
+      }
+    )
+    return () => (
+      <div class={[styles.trainSection, props.type === 'month' ? styles.teacherTrainSection : '']}>
+        <div class={styles.trainTitle}>
+          <div class={styles.name}>
+            <Image
+              src={props.type === 'month' ? teacherArrowLine : arrowLine}
+              class={styles.allowLine}
+            />
+            乐团人数
+          </div>
+          <div class={styles.countNums}>
+            <Image src={arrowPoint} class={styles.arrowPoint} />
+            乐团总人数<span>{forms.total}</span>人
+          </div>
+        </div>
+
+        <div class={styles.trainSectionContent}>
+          <div class={styles.tContent}>
+            {forms.list.map((item: any) => (
+              <div class={styles.tItem}>
+                <p class={styles.pople}>
+                  {item.value}
+                  <span>人</span>
+                </p>
+                <p class={styles.subjectName}>{item.text}</p>
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+    )
+  }
+})

+ 179 - 0
src/school/train-report/modal/student-attendance.tsx

@@ -0,0 +1,179 @@
+import { defineComponent, onMounted, reactive, watch } from 'vue'
+import { Image } from 'vant'
+import styles from './index.module.less'
+import arrowLine from '../images/week/icon-arrow-line.png'
+import arrowPoint from '../images/icon-arrow-point.png'
+import teacherArrowLine from '../images/month/teacher-icon-arrow-line.png'
+import * as echarts from 'echarts/core'
+import {
+  TitleComponent,
+  // 组件类型的定义后缀都为 ComponentOption
+  TooltipComponent,
+  GridComponent,
+  // 数据集组件
+  DatasetComponent,
+  // 内置数据转换器组件 (filter, sort)
+  TransformComponent,
+  LegendComponent,
+  ToolboxComponent,
+  DataZoomComponent
+} from 'echarts/components'
+import { LineChart } from 'echarts/charts'
+import { LabelLayout, UniversalTransition } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import { reportCourseType } from '../week-report'
+// type EChartsOption = echarts.EChartsOption
+
+// 注册必须的组件
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  DatasetComponent,
+  TransformComponent,
+  LabelLayout,
+  UniversalTransition,
+  CanvasRenderer,
+  ToolboxComponent,
+  LegendComponent,
+  DataZoomComponent,
+  LineChart
+])
+
+export default defineComponent({
+  name: 'orchestra-num',
+  props: {
+    type: {
+      type: String,
+      default: 'week'
+    },
+    reportData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  setup(props) {
+    const forms = reactive({
+      total: 0
+    })
+
+    watch(
+      () => props.reportData,
+      () => {
+        handleInit()
+      }
+    )
+
+    let myChart: any
+    const handleInit = () => {
+      if (myChart) {
+        myChart.dispose()
+      }
+
+      const data = props.reportData || {}
+      const titleList: any = []
+      const valueList: any = []
+      let lineColor = '#356BF7'
+      let textColor = '#1B4FD7'
+      for (const i in data) {
+        if (i === 'TOTAL') {
+          forms.total = data[i] * 100
+          if (forms.total <= 59) {
+            lineColor = '#FF8057'
+            textColor = '#F67146'
+          }
+        } else {
+          titleList.push(reportCourseType[i])
+          valueList.push(data[i] * 100)
+        }
+      }
+
+      const chartDom = document.getElementById('studentEcharts')
+      myChart = echarts.init(chartDom as HTMLDivElement)
+      const option = {
+        xAxis: {
+          type: 'category',
+          data: titleList,
+          axisLabel: {
+            rotate: 45,
+            fontSize: 10,
+            color: '#333'
+          }
+        },
+        yAxis: {
+          type: 'value'
+        },
+        grid: {
+          left: 0,
+          top: 22,
+          right: 0,
+          bottom: 12,
+          containLabel: true
+        },
+        series: [
+          {
+            type: 'line',
+            showBackground: false,
+            itemStyle: {
+              color: lineColor
+            },
+            label: {
+              show: true,
+              position: 'top',
+              fontSize: 10,
+              color: textColor
+            },
+            data: valueList
+          }
+        ]
+      }
+
+      option && myChart.setOption(option)
+    }
+
+    onMounted(() => {
+      handleInit()
+    })
+
+    return () => (
+      <div
+        class={[
+          styles.trainSection,
+          props.type === 'month' ? styles.teacherTrainSection : '',
+          styles.studentSection
+        ]}
+      >
+        <div class={styles.trainTitle}>
+          <div class={styles.name}>
+            <Image
+              src={props.type === 'month' ? teacherArrowLine : arrowLine}
+              class={styles.allowLine}
+            />
+            本{props.type === 'month' ? '月' : '周'}学生出勤
+          </div>
+          <div class={styles.countNums}>
+            <Image src={arrowPoint} class={styles.arrowPoint} />
+            总出勤率
+            <span
+              style={{
+                color: forms.total > 59 ? '#42FFE2' : '#FF7C88'
+              }}
+            >
+              {forms.total}
+            </span>
+            %
+          </div>
+        </div>
+
+        <div class={styles.trainSectionContent}>
+          <div class={styles.tContent}>
+            <div class={styles.calssEchartTitle}>
+              <span class={styles.unit}>单位:%</span>
+            </div>
+            <div id="studentEcharts" class={styles.classEcharts}></div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+})

+ 178 - 0
src/school/train-report/modal/teacher-attendance.tsx

@@ -0,0 +1,178 @@
+import { defineComponent, onMounted, reactive, watch } from 'vue'
+import { Image } from 'vant'
+import styles from './index.module.less'
+import arrowLine from '../images/week/icon-arrow-line.png'
+import arrowPoint from '../images/icon-arrow-point.png'
+import teacherArrowLine from '../images/month/teacher-icon-arrow-line.png'
+import * as echarts from 'echarts/core'
+import {
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  // 数据集组件
+  DatasetComponent,
+  // 内置数据转换器组件 (filter, sort)
+  TransformComponent,
+  LegendComponent,
+  ToolboxComponent,
+  DataZoomComponent
+} from 'echarts/components'
+import { LineChart } from 'echarts/charts'
+import { LabelLayout, UniversalTransition } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import { reportCourseType } from '../week-report'
+// type EChartsOption = echarts.EChartsOption
+
+// 注册必须的组件
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  DatasetComponent,
+  TransformComponent,
+  LabelLayout,
+  UniversalTransition,
+  CanvasRenderer,
+  ToolboxComponent,
+  LegendComponent,
+  DataZoomComponent,
+  LineChart
+])
+
+export default defineComponent({
+  name: 'orchestra-num',
+  props: {
+    type: {
+      type: String,
+      default: 'week'
+    },
+    reportData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  setup(props) {
+    const forms = reactive({
+      total: 0
+    })
+
+    watch(
+      () => props.reportData,
+      () => {
+        handleInit()
+      }
+    )
+
+    let myChart: any
+    const handleInit = () => {
+      if (myChart) {
+        myChart.dispose()
+      }
+
+      const data = props.reportData || {}
+      const titleList: any = []
+      const valueList: any = []
+      let lineColor = '#356BF7'
+      let textColor = '#1B4FD7'
+      for (const i in data) {
+        if (i === 'TOTAL') {
+          forms.total = data[i] * 100
+          if (forms.total <= 59) {
+            lineColor = '#FF8057'
+            textColor = '#F67146'
+          }
+        } else {
+          titleList.push(reportCourseType[i])
+          valueList.push(data[i] * 100)
+        }
+      }
+
+      const chartDom = document.getElementById('teacherEcharts')
+      myChart = echarts.init(chartDom as HTMLDivElement)
+      const option = {
+        xAxis: {
+          type: 'category',
+          data: titleList,
+          axisLabel: {
+            rotate: 45,
+            fontSize: 10,
+            color: '#333'
+          }
+        },
+        yAxis: {
+          type: 'value'
+        },
+        grid: {
+          left: 0,
+          top: 22,
+          right: 0,
+          bottom: 12,
+          containLabel: true
+        },
+        series: [
+          {
+            type: 'line',
+            showBackground: false,
+            itemStyle: {
+              color: lineColor
+            },
+            label: {
+              show: true,
+              position: 'top',
+              fontSize: 10,
+              color: textColor
+            },
+            data: valueList
+          }
+        ]
+      }
+
+      option && myChart.setOption(option)
+    }
+
+    onMounted(() => {
+      handleInit()
+    })
+
+    return () => (
+      <div
+        class={[
+          styles.trainSection,
+          props.type === 'month' ? styles.teacherTrainSection : '',
+          styles.teacherSection
+        ]}
+      >
+        <div class={styles.trainTitle}>
+          <div class={styles.name}>
+            <Image
+              src={props.type === 'month' ? teacherArrowLine : arrowLine}
+              class={styles.allowLine}
+            />
+            本{props.type === 'month' ? '月' : '周'}伴学指导出勤
+          </div>
+          <div class={styles.countNums}>
+            <Image src={arrowPoint} class={styles.arrowPoint} />
+            总出勤率
+            <span
+              style={{
+                color: forms.total > 59 ? '#42FFE2' : '#FF7C88'
+              }}
+            >
+              {forms.total}
+            </span>
+            %
+          </div>
+        </div>
+
+        <div class={styles.trainSectionContent}>
+          <div class={styles.tContent}>
+            <div class={styles.calssEchartTitle}>
+              <span class={styles.unit}>单位:%</span>
+            </div>
+            <div id="teacherEcharts" class={styles.classEcharts}></div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+})

+ 175 - 0
src/school/train-report/modal/train-class.tsx

@@ -0,0 +1,175 @@
+import { defineComponent, onMounted, reactive, watch } from 'vue'
+import { Image } from 'vant'
+import styles from './index.module.less'
+import arrowLine from '../images/week/icon-arrow-line.png'
+import arrowPoint from '../images/icon-arrow-point.png'
+import teacherArrowLine from '../images/month/teacher-icon-arrow-line.png'
+import * as echarts from 'echarts/core'
+import {
+  TitleComponent,
+  // 组件类型的定义后缀都为 ComponentOption
+  TooltipComponent,
+  GridComponent,
+  // 数据集组件
+  DatasetComponent,
+  // 内置数据转换器组件 (filter, sort)
+  TransformComponent,
+  LegendComponent,
+  ToolboxComponent,
+  DataZoomComponent
+} from 'echarts/components'
+import { BarChart } from 'echarts/charts'
+import { PieChart } from 'echarts/charts'
+import { LabelLayout, UniversalTransition } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import { reportCourseType } from '../week-report'
+// type EChartsOption = echarts.EChartsOption
+
+// 注册必须的组件
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  DatasetComponent,
+  TransformComponent,
+  BarChart,
+  LabelLayout,
+  UniversalTransition,
+  CanvasRenderer,
+  PieChart,
+  ToolboxComponent,
+  LegendComponent,
+  DataZoomComponent
+])
+
+export default defineComponent({
+  name: 'orchestra-num',
+  props: {
+    type: {
+      type: String,
+      default: 'week'
+    },
+    reportData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  setup(props) {
+    const forms = reactive({
+      total: 0
+    })
+
+    watch(
+      () => props.reportData,
+      () => {
+        handleInit()
+      }
+    )
+
+    let myChart: any
+    const handleInit = () => {
+      if (myChart) {
+        myChart.dispose()
+      }
+
+      const data = props.reportData || {}
+      const titleList: any = []
+      const valueList: any = []
+      for (const i in data) {
+        if (i === 'TOTAL') {
+          forms.total = data[i]
+        } else {
+          titleList.push(reportCourseType[i])
+          valueList.push(data[i])
+        }
+      }
+
+      const chartDom = document.getElementById('classEcharts')
+      myChart = echarts.init(chartDom as HTMLDivElement)
+      const option = {
+        xAxis: {
+          type: 'category',
+          data: titleList,
+          axisLabel: {
+            rotate: 45,
+            fontSize: 10,
+            color: '#333'
+          }
+        },
+        yAxis: {
+          type: 'value'
+        },
+        grid: {
+          left: 0,
+          top: 22,
+          right: 0,
+          bottom: 12,
+          containLabel: true
+        },
+        series: [
+          {
+            type: 'bar',
+            showBackground: false,
+            barWidth: 15,
+            itemStyle: {
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                { offset: 0, color: '#69DCE8' },
+                { offset: 0.5, color: '#E5CEFB' },
+                { offset: 1, color: '#58A2FF' }
+              ])
+            },
+            emphasis: {
+              itemStyle: {
+                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                  { offset: 0, color: '#69DCE8' },
+                  { offset: 0.5, color: '#E5CEFB' },
+                  { offset: 1, color: '#58A2FF' }
+                ])
+              }
+            },
+            label: {
+              show: true,
+              position: 'top',
+              fontSize: 10,
+              color: '#777'
+            },
+            data: valueList
+          }
+        ]
+      }
+      option && myChart.setOption(option)
+    }
+
+    onMounted(() => {
+      handleInit()
+    })
+
+    return () => (
+      <div class={[styles.trainSection, props.type === 'month' ? styles.teacherTrainSection : '']}>
+        <div class={styles.trainTitle}>
+          <div class={styles.name}>
+            <Image
+              src={props.type === 'month' ? teacherArrowLine : arrowLine}
+              class={styles.allowLine}
+            />
+            训练课程
+          </div>
+          <div class={styles.countNums}>
+            <Image src={arrowPoint} class={styles.arrowPoint} />
+            乐团累计总课时<span>272</span>课时
+          </div>
+        </div>
+
+        <div class={styles.trainSectionContent}>
+          <div class={styles.tContent}>
+            <div class={styles.calssEchartTitle}>
+              <span class={styles.unit}>单位:课时</span>
+              <span class={styles.unitType}>本{props.type === 'month' ? '月' : '周'}训练</span>
+            </div>
+            <div id="classEcharts" class={styles.classEcharts}></div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+})

+ 94 - 0
src/school/train-report/modal/train-progress.tsx

@@ -0,0 +1,94 @@
+import { defineComponent, reactive, watch } from 'vue'
+import { Image, Progress } from 'vant'
+import styles from './index.module.less'
+import arrowLine from '../images/week/icon-arrow-line.png'
+import arrowPoint from '../images/icon-arrow-point.png'
+import teacherArrowLine from '../images/month/teacher-icon-arrow-line.png'
+import { reportCourseType } from '../week-report'
+export default defineComponent({
+  name: 'orchestra-num',
+  props: {
+    type: {
+      type: String,
+      default: 'week'
+    },
+    reportData: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  setup(props) {
+    const forms = reactive({
+      total: 0,
+      knowledgeList: [] as any
+    })
+
+    watch(
+      () => props.reportData,
+      () => {
+        formatCourse()
+      }
+    )
+
+    const formatCourse = () => {
+      const knowledge = props.reportData
+      for (const i in knowledge) {
+        i != 'TOTAL' &&
+          forms.knowledgeList.push({
+            courseName: reportCourseType[i],
+            ...knowledge[i]
+          })
+      }
+    }
+
+    formatCourse()
+    return () => (
+      <div class={[styles.trainSection, props.type === 'month' ? styles.teacherTrainSection : '']}>
+        <div class={styles.trainTitle}>
+          <div class={styles.name}>
+            <Image
+              src={props.type === 'month' ? teacherArrowLine : arrowLine}
+              class={styles.allowLine}
+            />
+            训练进度
+          </div>
+          <div class={styles.countNums}>
+            <Image src={arrowPoint} class={styles.arrowPoint} />
+          </div>
+        </div>
+
+        <div class={styles.trainSectionContent}>
+          <div class={[styles.tContent, styles.tProgress]}>
+            {forms.knowledgeList.map((item: any) => (
+              <div class={styles.progressItem}>
+                <div class={styles.className}>
+                  <i class={styles.line}></i>
+                  {item.courseName}
+                </div>
+                <div class={styles.classNum}>
+                  <div class={styles.konowCount}>
+                    已学习知识点<span>{item.learned || 0}</span>个
+                  </div>
+                  <div class={styles.allCount}>
+                    共<span>{item.total || 0}</span>个
+                  </div>
+                </div>
+
+                <Progress
+                  color={'linear-gradient(90deg, #FFC3A1 0%, #FF9895 100%);'}
+                  trackColor="#ECECEC"
+                  showPivot={false}
+                  style={{
+                    borderRadius: '10px'
+                  }}
+                  percentage={10}
+                  strokeWidth={8}
+                />
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+    )
+  }
+})

+ 333 - 0
src/school/train-report/month-report.tsx

@@ -0,0 +1,333 @@
+import OHeader from '@/components/o-header'
+import { defineComponent, onMounted, reactive } from 'vue'
+import styles from './report.module.less'
+import iconOrchestra from '@/views/mine-orchestra/images/icon-or.png'
+import {
+  closeToast,
+  Grid,
+  GridItem,
+  Icon,
+  Image,
+  Popup,
+  showFailToast,
+  showLoadingToast,
+  showSuccessToast,
+  showToast
+} from 'vant'
+import trainWeek from './images/month/icon-train-month.png'
+import OrchestraNum from './modal/orchestra-num'
+import TrainClass from './modal/train-class'
+import StudentAttendance from './modal/student-attendance'
+import TeacherAttendance from './modal/teacher-attendance'
+import TrainProgress from './modal/train-progress'
+import iconPhoto from './images/icon-photo.png'
+import iconClass from './images/icon-class.png'
+import iconSaveImage from '@/school/orchestra/images/icon-save-image.png'
+import iconWechat from '@/school/orchestra/images/icon-wechat.png'
+import OQrcode from '@/components/o-qrcode'
+import request from '@/helpers/request'
+import { useRoute, useRouter } from 'vue-router'
+import { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
+import html2canvas from 'html2canvas'
+
+export const reportCourseType = {
+  PERCUSSION: '打击乐',
+  FLUTE: '长笛',
+  SAX: '萨克斯',
+  CLARINET: '单簧管',
+  TRUMPET: '小号',
+  TROMBONE: '长号',
+  HORN: '圆号',
+  BARITONE_TUBA: '上低音号-大号',
+  EUPHONIUM: '上低音号',
+  TUBA: '大号',
+  MUSIC_THEORY: '乐理',
+  INSTRUMENTAL_ENSEMBLE: '合奏'
+}
+
+export default defineComponent({
+  name: 'train-report',
+  setup() {
+    const router = useRouter()
+    const route = useRoute()
+    const forms = reactive({
+      id: route.query.id,
+      share: route.query.share as any,
+      showQrcode: false,
+      url: window.location.href + '&share=1'
+    })
+    const reportData = reactive({
+      orchestraName: null,
+      monthlyTime: null,
+      startTime: null,
+      endTime: null,
+      COURSEWARE: {},
+      coursewareList: [] as any,
+      COURSE_SCHEDULE: {},
+      KNOWLEDGE: {},
+      ORCHESTRA: {},
+      PHOTO: {} as any,
+      STUDENT_ATTENDANCE: {},
+      TEACHER_ATTENDANCE: {}
+    })
+
+    const getDetail = async () => {
+      try {
+        const { data } = await request.get('/api-school/open/orchestraReport/detail/' + forms.id)
+        reportData.COURSEWARE = data.reportItem.COURSEWARE || {}
+        reportData.COURSE_SCHEDULE = data.reportItem.COURSE_SCHEDULE || {}
+        reportData.KNOWLEDGE = data.reportItem.KNOWLEDGE || {}
+        reportData.ORCHESTRA = data.reportItem.ORCHESTRA || {}
+        reportData.PHOTO = data.reportItem.PHOTO || {}
+        reportData.STUDENT_ATTENDANCE = data.reportItem.STUDENT_ATTENDANCE || {}
+        reportData.TEACHER_ATTENDANCE = data.reportItem.TEACHER_ATTENDANCE || {}
+        reportData.orchestraName = data.orchestraName || ''
+        reportData.monthlyTime = data.monthlyTime || ''
+        reportData.startTime = data.startTime || ''
+        reportData.endTime = data.endTime || ''
+
+        const courseware = reportData.COURSEWARE
+        for (const i in courseware) {
+          i != 'TOTAL' && reportData.coursewareList.push(reportCourseType[i])
+        }
+      } catch {
+        //
+      }
+    }
+
+    const imgs = reactive({
+      saveLoading: false,
+      image: null as any,
+      shareLoading: false
+    })
+    const onSaveImg = async () => {
+      // 判断是否在保存中...
+      if (imgs.saveLoading) {
+        return
+      }
+      imgs.saveLoading = true
+      // 判断是否已经生成图片
+      if (imgs.image) {
+        saveImg()
+      } else {
+        const container: any = document.getElementById(`preview-container`)
+        html2canvas(container, {
+          allowTaint: true,
+          useCORS: true,
+          backgroundColor: null
+        })
+          .then(async (canvas) => {
+            const url = canvas.toDataURL('image/png')
+            imgs.image = url
+            saveImg()
+          })
+          .catch(() => {
+            closeToast()
+            imgs.saveLoading = false
+          })
+      }
+    }
+    const onShare = () => {
+      if (imgs.shareLoading) {
+        return
+      }
+      imgs.shareLoading = true
+      if (imgs.image) {
+        openShare()
+      } else {
+        const container: any = document.getElementById(`preview-container`)
+        html2canvas(container, {
+          allowTaint: true,
+          useCORS: true,
+          backgroundColor: null
+        })
+          .then(async (canvas) => {
+            const url = canvas.toDataURL('image/png')
+            imgs.image = url
+            openShare()
+          })
+          .catch(() => {
+            closeToast()
+            imgs.shareLoading = false
+          })
+      }
+    }
+    const openShare = () => {
+      const image = imgs.image
+      setTimeout(() => {
+        imgs.shareLoading = false
+      }, 100)
+      if (image) {
+        postMessage(
+          {
+            api: 'shareTripartite',
+            content: {
+              title: '',
+              desc: '',
+              image,
+              video: '',
+              type: 'image',
+              // button: ['copy']
+              shareType: 'wechat'
+            }
+          },
+          (res: any) => {
+            if (res && res.content) {
+              showToast(res.content.message || (res.content.status ? '分享成功' : '分享失败'))
+            }
+          }
+        )
+      }
+    }
+    const saveImg = async () => {
+      showLoadingToast({ message: '图片生成中...', forbidClick: true })
+      setTimeout(() => {
+        imgs.saveLoading = false
+      }, 100)
+      const res = await promisefiyPostMessage({
+        api: 'savePicture',
+        content: {
+          base64: imgs.image
+        }
+      })
+      if (res?.content?.status === 'success') {
+        showSuccessToast('保存成功')
+      } else {
+        showFailToast('保存失败')
+      }
+    }
+
+    onMounted(() => {
+      getDetail()
+    })
+    return () => (
+      <div
+        class={[
+          styles.trainWeek,
+          styles.trainMonth,
+          forms.share == 1 ? styles.trasinMonthShare : ''
+        ]}
+      >
+        <div class={styles.trainContainer}></div>
+        <OHeader background="transparent" border={false} title=" " backIconColor="white">
+          {{
+            right: () =>
+              forms.share != 1 && (
+                <i class={styles.iconShare} onClick={() => (forms.showQrcode = true)}></i>
+              )
+          }}
+        </OHeader>
+        <div class={[styles.headerContant, styles.teacherHeaderContant]}>
+          <div class={styles.orchestra}>
+            <Image src={iconOrchestra} class={styles.iconOrchestra} />
+            <span>{reportData.orchestraName}</span>
+          </div>
+          <div>
+            <Image src={trainWeek} class={styles.iconTrainWeek} />
+          </div>
+          <div class={styles.trainTimer}>{reportData.monthlyTime}</div>
+        </div>
+
+        <OrchestraNum type="month" reportData={reportData.ORCHESTRA} />
+        <TrainClass type="month" reportData={reportData.COURSE_SCHEDULE} />
+
+        <div class={[styles.trainPhoto, styles.teacherTrainPhoto]}>
+          <Image src={iconPhoto} class={styles.iconPhoto} />
+          <p>
+            本月上传<span>{reportData.PHOTO.TOTAL || 0}</span>张训练照片
+          </p>
+          <p
+            onClick={() => {
+              if (forms.share == 1) return
+              router.push({
+                path: '/school-photo'
+              })
+            }}
+          >
+            本月上传<span>{reportData.PHOTO.TOTAL || 0}</span>张训练照片
+            {forms.share != 1 && <Icon name="arrow" />}
+          </p>
+        </div>
+
+        <StudentAttendance type="month" reportData={reportData.STUDENT_ATTENDANCE} />
+        <TeacherAttendance type="month" reportData={reportData.TEACHER_ATTENDANCE} />
+
+        <div class={[styles.trainClass, styles.teacherTrainClass]}>
+          <Image src={iconClass} class={styles.iconPhoto} />
+          <div>
+            <p class={styles.subjectTips}>课件使用未达标班级</p>
+            <p class={styles.subjectNames}>
+              {reportData.coursewareList.map((item: string) => item + ' ')}
+            </p>
+          </div>
+        </div>
+        <TrainProgress type="month" reportData={reportData.KNOWLEDGE} />
+
+        <Popup
+          v-model:show={forms.showQrcode}
+          position="bottom"
+          style={{ background: 'transparent' }}
+        >
+          <div class={styles.codeContainer}>
+            <div class={[styles.codeImg, styles.teacherCodeImg]} id="preview-container">
+              <div class={styles.codeContent}>
+                <div
+                  class={[
+                    styles.headerContant,
+                    styles.teacherHeaderContant,
+                    styles.headerContantPopup
+                  ]}
+                >
+                  <div class={styles.orchestra}>
+                    <Image src={iconOrchestra} class={styles.iconOrchestra} />
+                    <span>{reportData.orchestraName}</span>
+                  </div>
+                  <div>
+                    <Image src={trainWeek} class={styles.iconTrainWeek} />
+                  </div>
+                  <div class={styles.trainTimer}>{reportData.monthlyTime}</div>
+                </div>
+
+                <div class={styles.codeQr}>
+                  <OQrcode text={forms.url} size={'100%'} logoSize="small" />
+                </div>
+                <div style={{ textAlign: 'center' }}>
+                  <span class={styles.codeBtnText}>
+                    扫描上方二维码<span>查看训练月报</span>
+                  </span>
+                </div>
+              </div>
+            </div>
+            <div class={styles.codeBottom}>
+              <Icon
+                name="cross"
+                size={22}
+                class={styles.close}
+                color="#666"
+                onClick={() => (forms.showQrcode = false)}
+              />
+
+              <h3 class={styles.title}>
+                <i></i>分享方式
+              </h3>
+              <Grid columnNum={2} border={false}>
+                <GridItem onClick={onSaveImg}>
+                  {{
+                    icon: () => <Image class={styles.shareImg} src={iconSaveImage} />,
+                    text: () => <div class={styles.shareText}>保存图片</div>
+                  }}
+                </GridItem>
+                <GridItem onClick={onShare}>
+                  {{
+                    icon: () => <Image class={styles.shareImg} src={iconWechat} />,
+                    text: () => <div class={styles.shareText}>微信</div>
+                  }}
+                </GridItem>
+              </Grid>
+            </div>
+          </div>
+        </Popup>
+      </div>
+    )
+  }
+})

+ 313 - 0
src/school/train-report/report.module.less

@@ -0,0 +1,313 @@
+.trainWeek {
+  position: relative;
+  min-height: 100vh;
+  background: linear-gradient(180deg, #225fff 0%, #0e2cdf 94%, #0d29dc 100%);
+  overflow: hidden;
+
+  .trainContainer {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    background: url('./images/week/banner-week.png') no-repeat top center;
+    background-size: contain;
+    height: 342px;
+  }
+
+  .iconShare {
+    width: 20px;
+    height: 20px;
+    display: inline-block;
+    background: url('./images/icon-share.png') no-repeat center;
+    background-size: contain;
+  }
+
+  &.trainMonth {
+    background: linear-gradient(180deg, #c7c7ff 0%, #9c9bff 100%);
+
+    .trainContainer {
+      background: url('./images/month/banner-month.png') no-repeat top center;
+      background-size: contain;
+      height: 342px;
+    }
+  }
+
+  &.trasinMonthShare {
+    .trainContainer {
+      background: url('./images/month/share-month-banner.png') no-repeat top center;
+      background-size: contain;
+      height: 342px;
+    }
+  }
+
+  &.trasinWeekShare {
+    .trainContainer {
+      background: url('./images/week/share-week-banner.png') no-repeat top center;
+      background-size: contain;
+      height: 342px;
+    }
+  }
+}
+
+.headerContant {
+  padding-top: 16px;
+  padding-left: 17px;
+  margin-bottom: 50px;
+  &.teacherHeaderContant {
+    .orchestra {
+      color: #3e0ed7;
+    }
+    .trainTimer {
+      color: #3a17fd;
+      &::before,
+      &::after {
+        background: url('./images/month/teacher-icon-point.png') no-repeat center;
+        background-size: contain;
+      }
+    }
+  }
+  .orchestra {
+    padding: 3px 7px;
+    display: inline-flex;
+    background: rgba(255, 255, 255, 0.32);
+    border-radius: 12px;
+    font-size: 12px;
+    color: #ffffff;
+    span {
+      max-width: 200px;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+  }
+  .iconOrchestra {
+    width: 14px;
+    height: 14px;
+    margin-right: 4px;
+  }
+  .iconTrainWeek {
+    height: 38px;
+    width: 156px;
+    padding: 18px 0 8px;
+  }
+  .trainTimer {
+    display: flex;
+    align-items: center;
+    font-size: 13px;
+    color: #ffffff;
+    line-height: 18px;
+
+    &::before,
+    &::after {
+      display: inline-block;
+      margin-left: 4px;
+      margin-right: 4px;
+      content: ' ';
+      width: 6px;
+      height: 6px;
+      background: url('./images/week/icon-point.png') no-repeat center;
+      background-size: contain;
+    }
+  }
+}
+
+.trainPhoto {
+  height: 58px;
+  font-size: 14px;
+  font-weight: 600;
+  color: #ffffff;
+  background: url('./images/week/train-bg.png') no-repeat center;
+  background-size: contain;
+  padding: 0 15px;
+  margin: 0 13px 10px;
+  display: flex;
+  align-items: center;
+
+  &.teacherTrainPhoto {
+    background: url('./images/month/teacher-train-bg.png') no-repeat center;
+    background-size: contain;
+    height: 60px;
+  }
+
+  .iconPhoto {
+    width: 38px;
+    height: 42px;
+    margin-right: 5px;
+  }
+  span {
+    padding: 0 4px;
+    color: #41ffe2;
+    font-family: DINA;
+  }
+}
+
+.trainClass {
+  position: relative;
+  font-size: 14px;
+  font-weight: 600;
+  color: #ffffff;
+  padding: 0 15px;
+  margin: 0 13px 10px;
+  display: flex;
+  align-items: center;
+  min-height: 76px;
+  background: rgba(66, 109, 255, 0.97) linear-gradient(276deg, #0a53d4 0%, #1133c0 100%);
+  box-shadow: inset 6px 6px 0px 0px rgba(15, 57, 204, 0.41);
+  border-radius: 16px 100px 100px 18px;
+  border: 3px solid #99ffdd;
+  overflow: hidden;
+  &::after {
+    position: absolute;
+    right: -40px;
+    top: -30px;
+    content: ' ';
+    background: url('./images/small-icon.png') no-repeat center;
+    background-size: cover;
+    height: 100%;
+    width: 63px;
+  }
+
+  .iconPhoto {
+    width: 38px;
+    height: 42px;
+    margin-right: 5px;
+  }
+  span {
+    padding: 0 4px;
+    color: #41ffe2;
+    font-family: DINA;
+  }
+  .subjectTips {
+    line-height: 20px;
+  }
+  .subjectNames {
+    padding-top: 2px;
+    font-weight: 600;
+    color: #ff7c88;
+    line-height: 20px;
+    font-size: 14px;
+  }
+}
+.teacherTrainClass {
+  background: rgba(92, 72, 239, 0.71);
+  box-shadow: inset 4px 4px 0px 0px #7765ef;
+  border: 3px solid #99ffdd;
+}
+
+.codeContainer {
+  .codeImg {
+    width: 315px;
+    height: 440px;
+    background: url('./images/week/popup-week-banner.png') no-repeat top center;
+    background-size: contain;
+    margin: 0 auto;
+    background-color: #ffffff;
+    border-radius: 8px;
+
+    &.teacherCodeImg {
+      background: url('./images/month/popup-week-banner.png') no-repeat top center;
+      background-size: contain;
+      background-color: #ffffff;
+    }
+
+    .headerContantPopup {
+      padding-top: 23px;
+      padding-left: 18px;
+    }
+
+    .codeTitle {
+      text-align: center;
+      padding-top: 13px;
+      font-size: 24px;
+      font-weight: bold;
+      color: #ffffff;
+      text-shadow: 1px 1px 7px #f4672a;
+      max-width: 90%;
+      padding-left: 5%;
+    }
+
+    .codeName {
+      padding: 36px 8px 0;
+      font-size: 18px;
+      font-weight: 600;
+      color: #ffffff;
+    }
+    .codeQr {
+      margin: 108px auto 0;
+      width: 126px;
+      height: 126px;
+      padding: 12px;
+      background: url('./images/popup-qrcode-bg.png') no-repeat center;
+      background-size: contain;
+      border-radius: 11px;
+      overflow: hidden;
+      box-sizing: border-box;
+      img {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+    .codeBtnText {
+      margin: 20px auto 0;
+      display: inline-block;
+      background: linear-gradient(
+        132deg,
+        rgba(199, 239, 243, 0.25) 0%,
+        rgba(229, 206, 251, 0.25) 40%,
+        rgba(147, 194, 254, 0.25) 100%
+      );
+      box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.62);
+      border-radius: 18px;
+      padding: 6px 16px;
+      font-size: 14px;
+      color: #000977;
+    }
+
+    .codeTips {
+      padding-top: 10px;
+      font-size: 13px;
+      font-weight: 600;
+      color: #f16437;
+      line-height: 18px;
+      text-align: center;
+    }
+  }
+  .close {
+    position: absolute;
+    top: 12px;
+    right: 15px;
+  }
+  .codeBottom {
+    position: relative;
+    margin-top: 32px;
+    background: #ffffff;
+    border-radius: 20px 20px 0px 0px;
+    padding-bottom: 10px;
+  }
+  .title {
+    font-size: 16px;
+    font-weight: 600;
+    color: #333333;
+    line-height: 22px;
+    padding: 15px 15px 0;
+    i {
+      display: inline-block;
+      margin-right: 6px;
+      width: 4px;
+      height: 12px;
+      background: #ff8057;
+      border-radius: 2px;
+    }
+  }
+  .shareImg {
+    width: 47px;
+    height: 47px;
+  }
+  .shareText {
+    padding-top: 6px;
+    font-size: 14px;
+    color: #333333;
+    line-height: 20px;
+  }
+}

+ 319 - 0
src/school/train-report/week-report.tsx

@@ -0,0 +1,319 @@
+import OHeader from '@/components/o-header'
+import { defineComponent, onMounted, reactive } from 'vue'
+import styles from './report.module.less'
+import iconOrchestra from '@/views/mine-orchestra/images/icon-or.png'
+import {
+  closeToast,
+  Grid,
+  GridItem,
+  Icon,
+  Image,
+  Popup,
+  showFailToast,
+  showLoadingToast,
+  showSuccessToast,
+  showToast
+} from 'vant'
+import trainWeek from './images/week/icon-train-week.png'
+import OrchestraNum from './modal/orchestra-num'
+import TrainClass from './modal/train-class'
+import StudentAttendance from './modal/student-attendance'
+import TeacherAttendance from './modal/teacher-attendance'
+import TrainProgress from './modal/train-progress'
+import iconPhoto from './images/icon-photo.png'
+import iconClass from './images/icon-class.png'
+import iconSaveImage from '@/school/orchestra/images/icon-save-image.png'
+import iconWechat from '@/school/orchestra/images/icon-wechat.png'
+import OQrcode from '@/components/o-qrcode'
+import { useRoute, useRouter } from 'vue-router'
+import request from '@/helpers/request'
+import { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
+import html2canvas from 'html2canvas'
+
+export const reportCourseType = {
+  PERCUSSION: '打击乐',
+  FLUTE: '长笛',
+  SAX: '萨克斯',
+  CLARINET: '单簧管',
+  TRUMPET: '小号',
+  TROMBONE: '长号',
+  HORN: '圆号',
+  BARITONE_TUBA: '上低音号-大号',
+  EUPHONIUM: '上低音号',
+  TUBA: '大号',
+  MUSIC_THEORY: '乐理',
+  INSTRUMENTAL_ENSEMBLE: '合奏'
+}
+
+export default defineComponent({
+  name: 'train-report',
+  setup() {
+    const router = useRouter()
+    const route = useRoute()
+    const forms = reactive({
+      id: route.query.id,
+      showQrcode: false,
+      share: route.query.share as any,
+      url: window.location.href + '&share=1'
+    })
+    const reportData = reactive({
+      orchestraName: null,
+      startTime: null,
+      endTime: null,
+      COURSEWARE: {},
+      coursewareList: [] as any,
+      COURSE_SCHEDULE: {},
+      KNOWLEDGE: {},
+      ORCHESTRA: {},
+      PHOTO: {} as any,
+      STUDENT_ATTENDANCE: {},
+      TEACHER_ATTENDANCE: {}
+    })
+
+    const getDetail = async () => {
+      try {
+        const { data } = await request.get('/api-school/open/orchestraReport/detail/' + forms.id)
+        reportData.COURSEWARE = data.reportItem.COURSEWARE || {}
+        reportData.COURSE_SCHEDULE = data.reportItem.COURSE_SCHEDULE || {}
+        reportData.KNOWLEDGE = data.reportItem.KNOWLEDGE || {}
+        reportData.ORCHESTRA = data.reportItem.ORCHESTRA || {}
+        reportData.PHOTO = data.reportItem.PHOTO || {}
+        reportData.STUDENT_ATTENDANCE = data.reportItem.STUDENT_ATTENDANCE || {}
+        reportData.TEACHER_ATTENDANCE = data.reportItem.TEACHER_ATTENDANCE || {}
+        reportData.orchestraName = data.orchestraName || ''
+        reportData.startTime = data.startTime || ''
+        reportData.endTime = data.endTime || ''
+
+        const courseware = reportData.COURSEWARE
+        for (const i in courseware) {
+          i != 'TOTAL' && reportData.coursewareList.push(reportCourseType[i])
+        }
+      } catch {
+        //
+      }
+    }
+
+    const imgs = reactive({
+      saveLoading: false,
+      image: null as any,
+      shareLoading: false
+    })
+    const onSaveImg = async () => {
+      // 判断是否在保存中...
+      if (imgs.saveLoading) {
+        return
+      }
+      imgs.saveLoading = true
+      // 判断是否已经生成图片
+      if (imgs.image) {
+        saveImg()
+      } else {
+        const container: any = document.getElementById(`preview-container`)
+        html2canvas(container, {
+          allowTaint: true,
+          useCORS: true,
+          backgroundColor: null
+        })
+          .then(async (canvas) => {
+            const url = canvas.toDataURL('image/png')
+            imgs.image = url
+            saveImg()
+          })
+          .catch(() => {
+            closeToast()
+            imgs.saveLoading = false
+          })
+      }
+    }
+    const onShare = () => {
+      if (imgs.shareLoading) {
+        return
+      }
+      imgs.shareLoading = true
+      if (imgs.image) {
+        openShare()
+      } else {
+        const container: any = document.getElementById(`preview-container`)
+        html2canvas(container, {
+          allowTaint: true,
+          useCORS: true,
+          backgroundColor: null
+        })
+          .then(async (canvas) => {
+            const url = canvas.toDataURL('image/png')
+            imgs.image = url
+            openShare()
+          })
+          .catch(() => {
+            closeToast()
+            imgs.shareLoading = false
+          })
+      }
+    }
+    const openShare = () => {
+      const image = imgs.image
+      setTimeout(() => {
+        imgs.shareLoading = false
+      }, 100)
+      if (image) {
+        postMessage(
+          {
+            api: 'shareTripartite',
+            content: {
+              title: '',
+              desc: '',
+              image,
+              video: '',
+              type: 'image',
+              // button: ['copy']
+              shareType: 'wechat'
+            }
+          },
+          (res: any) => {
+            if (res && res.content) {
+              showToast(res.content.message || (res.content.status ? '分享成功' : '分享失败'))
+            }
+          }
+        )
+      }
+    }
+    const saveImg = async () => {
+      showLoadingToast({ message: '图片生成中...', forbidClick: true })
+      setTimeout(() => {
+        imgs.saveLoading = false
+      }, 100)
+      const res = await promisefiyPostMessage({
+        api: 'savePicture',
+        content: {
+          base64: imgs.image
+        }
+      })
+      if (res?.content?.status === 'success') {
+        showSuccessToast('保存成功')
+      } else {
+        showFailToast('保存失败')
+      }
+    }
+
+    onMounted(() => {
+      getDetail()
+    })
+
+    return () => (
+      <div class={[styles.trainWeek, forms.share == 1 ? styles.trasinWeekShare : '']}>
+        <div class={styles.trainContainer}></div>
+        <OHeader background="transparent" border={false} title=" " backIconColor="white">
+          {{
+            right: () =>
+              forms.share != 1 && (
+                <i class={styles.iconShare} onClick={() => (forms.showQrcode = true)}></i>
+              )
+          }}
+        </OHeader>
+        <div class={styles.headerContant}>
+          <div class={styles.orchestra}>
+            <Image src={iconOrchestra} class={styles.iconOrchestra} />
+            <span>{reportData.orchestraName}</span>
+          </div>
+          <div>
+            <Image src={trainWeek} class={styles.iconTrainWeek} />
+          </div>
+          <div class={styles.trainTimer}>
+            {reportData.startTime}-{reportData.endTime}
+          </div>
+        </div>
+
+        <OrchestraNum reportData={reportData.ORCHESTRA} />
+        <TrainClass reportData={reportData.COURSE_SCHEDULE} />
+
+        <div class={styles.trainPhoto}>
+          <Image src={iconPhoto} class={styles.iconPhoto} />
+          <p
+            onClick={() => {
+              if (forms.share == 1) return
+              router.push({
+                path: '/school-photo'
+              })
+            }}
+          >
+            本周上传<span>{reportData.PHOTO.TOTAL || 0}</span>张训练照片
+            {forms.share != 1 && <Icon name="arrow" />}
+          </p>
+        </div>
+
+        <StudentAttendance reportData={reportData.STUDENT_ATTENDANCE} />
+        <TeacherAttendance reportData={reportData.TEACHER_ATTENDANCE} />
+
+        <div class={[styles.trainClass]}>
+          <Image src={iconClass} class={styles.iconPhoto} />
+          <div>
+            <p class={styles.subjectTips}>课件使用未达标班级</p>
+            <p class={styles.subjectNames}>
+              {reportData.coursewareList.map((item: string) => item + ' ')}
+            </p>
+          </div>
+        </div>
+        <TrainProgress reportData={reportData.KNOWLEDGE} />
+
+        <Popup
+          v-model:show={forms.showQrcode}
+          position="bottom"
+          style={{ background: 'transparent' }}
+        >
+          <div class={styles.codeContainer}>
+            <div class={styles.codeImg} id="preview-container">
+              <div class={styles.codeContent}>
+                <div class={[styles.headerContant, styles.headerContantPopup]}>
+                  <div class={styles.orchestra}>
+                    <Image src={iconOrchestra} class={styles.iconOrchestra} />
+                    <span>{reportData.orchestraName}</span>
+                  </div>
+                  <Image src={trainWeek} class={styles.iconTrainWeek} />
+                  <div class={styles.trainTimer}>
+                    {reportData.startTime}-{reportData.endTime}
+                  </div>
+                </div>
+
+                <div class={styles.codeQr}>
+                  <OQrcode text={forms.url} size={'100%'} logoSize="small" />
+                </div>
+                <div style={{ textAlign: 'center' }}>
+                  <span class={styles.codeBtnText}>
+                    扫描上方二维码<span>查看训练周报</span>
+                  </span>
+                </div>
+              </div>
+            </div>
+            <div class={styles.codeBottom}>
+              <Icon
+                name="cross"
+                size={22}
+                class={styles.close}
+                color="#666"
+                onClick={() => (forms.showQrcode = false)}
+              />
+
+              <h3 class={styles.title}>
+                <i></i>分享方式
+              </h3>
+              <Grid columnNum={2} border={false}>
+                <GridItem onClick={onSaveImg}>
+                  {{
+                    icon: () => <Image class={styles.shareImg} src={iconSaveImage} />,
+                    text: () => <div class={styles.shareText}>保存图片</div>
+                  }}
+                </GridItem>
+                <GridItem onClick={onShare}>
+                  {{
+                    icon: () => <Image class={styles.shareImg} src={iconWechat} />,
+                    text: () => <div class={styles.shareText}>微信</div>
+                  }}
+                </GridItem>
+              </Grid>
+            </div>
+          </div>
+        </Popup>
+      </div>
+    )
+  }
+})

+ 12 - 14
src/views/subject-echarts/index.tsx

@@ -96,8 +96,8 @@ export default defineComponent({
             lineStyle: {
               width: 30,
               type: 'solid',
-              opacity: 0.2,
-            },
+              opacity: 0.2
+            }
           }
         },
         grid: {
@@ -236,15 +236,15 @@ export default defineComponent({
         practiceThisWeeks: new Array(Math.ceil(Math.random() * 9)).fill(1).map((n, i) => {
           return {
             /** 声部名称 */
-          subjectName: '声部' + (i + 1),
-          /**达标率 */
-          practiceRate: Math.ceil(Math.random() * 100),
-          /** 达标人数 */
-          passNum: Math.floor(Math.random() * 1000),
-          /** 未达标人数 */
-          noPassNum: Math.floor(Math.random() * 1000),
-          /** 非会员 */
-          noMemberNum: Math.floor(Math.random() * 1000)
+            subjectName: '声部' + (i + 1),
+            /**达标率 */
+            practiceRate: Math.ceil(Math.random() * 100),
+            /** 达标人数 */
+            passNum: Math.floor(Math.random() * 1000),
+            /** 未达标人数 */
+            noPassNum: Math.floor(Math.random() * 1000),
+            /** 非会员 */
+            noMemberNum: Math.floor(Math.random() * 1000)
           }
         })
       }
@@ -276,9 +276,7 @@ export default defineComponent({
       <div class={styles.subjectEcharts}>
         <div class={[styles.container, styles.ensemble]}>
           <div class={styles.head}>
-            <div
-              class={styles.headLeft}
-            >
+            <div class={styles.headLeft}>
               <img class={styles.icon} src={iconEnsemble} />
               <div>总体情况</div>
             </div>