Browse Source

Merge branch 'iteration-20241126' into dev

lex-xin 4 months ago
parent
commit
aaa760f3b6
46 changed files with 4253 additions and 218 deletions
  1. 21 2
      src/business-components/calendar/index.tsx
  2. 25 7
      src/business-components/user-detail/index.tsx
  3. 16 1
      src/helpers/hooks.ts
  4. 3 3
      src/helpers/utils.ts
  5. 24 0
      src/router/routes-teacher.ts
  6. 7 0
      src/student/group-class/group-detail.tsx
  7. 7 5
      src/student/teacher-dependent/components/practice.tsx
  8. 10 0
      src/styles/README.md
  9. 54 0
      src/styles/component-ui.less
  10. 129 0
      src/styles/global.less
  11. 1 0
      src/styles/index.less
  12. 2 2
      src/teacher/live-class/create-components/detail.tsx
  13. 96 0
      src/teacher/statistics/home-statistics-detail/buy-item/index.module.less
  14. 58 0
      src/teacher/statistics/home-statistics-detail/buy-item/index.tsx
  15. 243 0
      src/teacher/statistics/home-statistics-detail/echats/index.module.less
  16. 467 0
      src/teacher/statistics/home-statistics-detail/echats/index.tsx
  17. 38 0
      src/teacher/statistics/home-statistics-detail/index.module.less
  18. 40 0
      src/teacher/statistics/home-statistics-detail/index.tsx
  19. 77 0
      src/teacher/statistics/home-statistics-detail/list/index.module.less
  20. 224 0
      src/teacher/statistics/home-statistics-detail/list/index.tsx
  21. 90 0
      src/teacher/statistics/home-statistics-detail/teacher-item/index.module.less
  22. 59 0
      src/teacher/statistics/home-statistics-detail/teacher-item/index.tsx
  23. 155 0
      src/teacher/statistics/home-statistics/index.module.less
  24. 409 0
      src/teacher/statistics/home-statistics/index.tsx
  25. BIN
      src/teacher/statistics/images/filter-bg.png
  26. BIN
      src/teacher/statistics/images/icon-1.png
  27. BIN
      src/teacher/statistics/images/icon-2.png
  28. BIN
      src/teacher/statistics/images/icon-arrow.png
  29. BIN
      src/teacher/statistics/images/icon-arrow1-1.png
  30. BIN
      src/teacher/statistics/images/icon-arrow1.png
  31. BIN
      src/teacher/statistics/images/icon-download.png
  32. BIN
      src/teacher/statistics/images/icon-money.png
  33. BIN
      src/teacher/statistics/images/icon1.png
  34. 50 0
      src/teacher/statistics/practice-statistics-detail/echats/index.module.less
  35. 280 0
      src/teacher/statistics/practice-statistics-detail/echats/index.tsx
  36. 389 0
      src/teacher/statistics/practice-statistics-detail/index.module.less
  37. 568 0
      src/teacher/statistics/practice-statistics-detail/index.tsx
  38. 10 10
      src/tenant/music/coursewarePlay/index.tsx
  39. 77 4
      src/views/music/album-detail/index.module.less
  40. 165 76
      src/views/music/album-detail/index.tsx
  41. 125 0
      src/views/music/album-detail/search-group/index.module.less
  42. 109 0
      src/views/music/album-detail/search-group/index.tsx
  43. 69 5
      src/views/music/list/index.module.less
  44. 154 102
      src/views/music/list/index.tsx
  45. 1 0
      src/views/music/personal/collection.tsx
  46. 1 1
      vite.config.ts

+ 21 - 2
src/business-components/calendar/index.tsx

@@ -104,6 +104,18 @@ export default defineComponent({
     this.maxDate = dayjs().add(1, 'day').endOf('month').toDate()
     this.minDate = dayjs().add(1, 'day').toDate()
 
+    // console.log(this.list, "this.list")
+    // for(const key in this.list) {
+    //   console.log(key, this.list[key])
+    //   let dataList = [] as any
+    //   if (this.list[key] && Array.isArray(this.list[key].courseTime)) {
+    //     dataList = [...this.list[key].courseTime].filter(n =>
+    //       dayjs().isBefore(dayjs(n.startTime))
+    //     )
+    //   }
+    //   this.list[key].courseTime = dataList
+    // }
+
     // 初始化日历
     // console.log(this.list, 323, this.maxDays)
   },
@@ -111,14 +123,21 @@ export default defineComponent({
     formatter(date: any) {
       const dateStr = dayjs(date.date).format('YYYY-MM-DD')
       const dateObj = this.list[dateStr]
+      // 格式化选择的时间
+      let courseTime = [] as any
+      if (dateObj && Array.isArray(dateObj.courseTime)) {
+        courseTime = [...dateObj.courseTime].filter(n =>
+          dayjs().isBefore(dayjs(n.startTime))
+        )
+      }
       date.type = ''
       // 判断是否有课程 并且 时间在当前时间之后
       if (dateObj && dayjs().subtract(1, 'day').isBefore(dayjs(date.date))) {
         // fullCourse当天是否排满 0: 未,1:满 , courseTime 当天没有课程
         if (
           dateObj.fullCourse ||
-          !dateObj?.courseTime ||
-          dateObj?.courseTime?.length <= 0
+          !courseTime ||
+          courseTime?.length <= 0
         ) {
           date.bottomInfo = '满'
           date.className = 'full'

+ 25 - 7
src/business-components/user-detail/index.tsx

@@ -95,13 +95,31 @@ export default defineComponent({
                   <span
                     class={styles.timers}
                   >
-                    <Icon
-                      name={iconTimer}
-                      size="16"
-                      style={{ marginRight: '5px' }}
-                    />
-                    开课时间:
-                    {this.userInfo.courseStartTime}
+                    <div class={styles.timerSpan}>
+                      <Icon
+                        name={iconTimer}
+                        size="16"
+                        style={{ marginRight: '5px' }}
+                      />
+                      开课时间:
+                      {this.userInfo.courseStartTime}
+                    </div>
+
+                    {this.showBuy && ['live', 'group'].includes(this.userInfo.type || '') && (
+                      <div class={[styles.buyNum, styles.buyNumOther]}>
+                        {this.userInfo.type === 'live' ? (
+                          <span>
+                            {this.userInfo.buyNum}人已
+                            {this.userInfo.lessonPrice <= 0 &&
+                            this.userInfo.auditVersion === 0
+                              ? '领取'
+                              : '购买'}
+                          </span>
+                        ) : this.userInfo.type === "group" ?
+                          ((this.userInfo.mixStudentNum || 0 > 0) ? <>剩余{this.userInfo.mixStudentNum}个名额</> : '')
+                        : ''}
+                      </div>
+                    )}
                   </span>
                 )
             }}

+ 16 - 1
src/helpers/hooks.ts

@@ -1,6 +1,7 @@
 import { reactive } from 'vue'
 import { postMessage } from './native-message'
 import request from './request'
+import { browser } from './utils'
 
 // 搜索关键字
 export const SubjectEnum = {
@@ -27,7 +28,7 @@ export const useSubjectId = (
     let subject = localStorage.getItem(key)
     subject = subject ? JSON.parse(subject) : { name: '', id: '' }
     return subject
-  } else if(type === "remove") {
+  } else if (type === 'remove') {
     localStorage.removeItem(key)
   } else {
     localStorage.setItem(key, value)
@@ -135,3 +136,17 @@ export const useEventTracking = (name: eventName) => {
     }
   })
 }
+
+/**
+ * 学生端 - 埋点统计
+ * @param params { objectType: string, objectId: number}
+ */
+export const useStatisticTracking = (params: {
+  objectType: 'VIP_COURSE' | 'PRACTICE' | 'GROUP' | 'LIVE' | 'VIDEO' | 'MUSIC'
+  objectId: number | string
+}) => {
+  request.post('/api-student/exposure/record', {
+    hideLoading: false,
+    data: [{ ...params, platform: browser().ios ? 'ios' : 'android' }]
+  })
+}

+ 3 - 3
src/helpers/utils.ts

@@ -94,13 +94,13 @@ export const getHttpOrigin = () => {
  */
 export const formatterDatePicker = (type: any, option: any) => {
   if (type === 'year') {
-    option.text += '年'
+    option += '年'
   }
   if (type === 'month') {
-    option.text += '月'
+    option += '月'
   }
   if (type === 'day') {
-    option.text += '日'
+    option += '日'
   }
   return option
 }

+ 24 - 0
src/router/routes-teacher.ts

@@ -395,6 +395,30 @@ export default [
         meta: {
           title: '协议'
         }
+      },
+      {
+        path: '/home-statistics',
+        name: 'home-statistics',
+        component: () => import('@/teacher/statistics/home-statistics/index'),
+        meta: {
+          title: '浏览/购买'
+        }
+      },
+      {
+        path: '/home-statistics-detail',
+        name: 'home-statistics-detail',
+        component: () => import('@/teacher/statistics/home-statistics-detail/index'),
+        meta: {
+          title: '数据详情'
+        }
+      },
+      {
+        path: '/practice-statistics-detail',
+        name: 'practice-statistics-detail',
+        component: () => import('@/teacher/statistics/practice-statistics-detail/index'),
+        meta: {
+          title: '练习统计'
+        }
       }
     ]
   },

+ 7 - 0
src/student/group-class/group-detail.tsx

@@ -20,6 +20,7 @@ import { tradeOrder } from '../trade/tradeOrder'
 import { courseType } from '@/constant'
 import GroupPlanStep from '@/business-components/group-plan-step'
 import TheSticky from '@/components/the-sticky'
+import { useStatisticTracking } from '@/helpers/hooks'
 interface IProps {
   courseTime: string
   coursePlan: string
@@ -131,6 +132,12 @@ export default defineComponent({
     } else {
       this.shareUrl = `${location.origin}/teacher/#/shareGroup?recomUserId=${state.user.data?.userId}&groupId=${this.groupId}&userType=${state.platformType}&p=tenant`
     }
+
+    /** 埋点 */
+    useStatisticTracking({
+      objectType: "GROUP",
+      objectId: this.groupId as any
+    })
   },
   methods: {
     async _init() {

+ 7 - 5
src/student/teacher-dependent/components/practice.tsx

@@ -159,12 +159,12 @@ export default defineComponent({
     async getList(date?: Date) {
       try {
         const tempDate = date || dayjs().add(1, 'day').toDate()
-        let params = {
+        const params = {
           day: dayjs(tempDate).format('DD'),
           month: dayjs(tempDate).format('MM'),
           year: dayjs(tempDate).format('YYYY')
         }
-        let res = await request.post(
+        const res = await request.post(
           '/api-student/courseSchedule/createPracticeCourseCalendar',
           {
             data: {
@@ -176,17 +176,19 @@ export default defineComponent({
           }
         )
         const result = res.data || []
-        let tempObj = {}
+        const tempObj = {}
         result.forEach((item: any) => {
           tempObj[item.date] = item
         })
         this.calendarList = tempObj
         this.calendarStatus = result.length > 0
-      } catch {}
+      } catch {
+        // 
+      }
     },
     onSelectDay(obj: any) {
       const result = obj || []
-      let list = [...this.selectCourseList] as any
+      const list = [...this.selectCourseList] as any
 
       result.forEach((item: any) => {
         const isExist = list.some(

+ 10 - 0
src/styles/README.md

@@ -0,0 +1,10 @@
+### components-ui
+
+1、在 Vant3.x 基础上封装一套独立样式,因环境差异较大数据应统一输入尽量不要请求接口;
+2、注意 Vant 库等必要依赖库的版本差异;
+3、组件采用 less 的方式编写;
+
+### 使用
+
+1、把项目中 index.less 在项目的根目录中使用;
+2、一些基础组件,只会在原生有 UI 组件上变更样式;

+ 54 - 0
src/styles/component-ui.less

@@ -0,0 +1,54 @@
+// 公用变量
+@import './global.less';
+
+// 选择框
+// 上拉选择 - ✅
+// 选择器 - ✅
+.van-picker {
+  --van-picker-toolbar-height: 44px !important;
+  .van-picker__toolbar {
+    position: relative;
+    &::after {
+      position: absolute;
+      box-sizing: border-box;
+      content: ' ';
+      pointer-events: none;
+      right: var(--van-padding-md);
+      bottom: 0;
+      left: var(--van-padding-md);
+      border-bottom: 1px solid var(--van-cell-border-color);
+      transform: scaleY(0.5);
+    }
+  }
+  .van-picker__columns {
+    padding: 0 24px;
+  }
+  .van-picker-column {
+    position: relative;
+    z-index: 1;
+  }
+  .van-picker__frame {
+    z-index: 0;
+    &::after {
+      background: var(--k-bg-4);
+      border-radius: 8px;
+    }
+  }
+  .van-picker__cancel,
+  .van-picker__confirm {
+    font-size: 15px;
+  }
+  .van-picker__cancel {
+    color: var(--k-gray-3);
+  }
+  .van-picker__confirm {
+    color: var(--k-font-primary);
+  }
+  .van-picker-column__item {
+    color: var(--k-gray-1);
+    font-size: 16px;
+  }
+  .van-picker-column__item--selected {
+    font-weight: 600;
+  }
+}

+ 129 - 0
src/styles/global.less

@@ -0,0 +1,129 @@
+// 注意:为什么要写两个重复的 :root?
+// 由于 vant 中的主题变量也是在 :root 下声明的,所以在有些情况下会由于优先级的问题无法成功覆盖。通过
+// :root:root 可以显式地让你所写内容的优先级更高一些,从而确保主题变量的成功覆盖。
+:root:root {
+  // 01 品牌色
+  --k-primary: #2dc7aa;
+
+  // 02 背景色
+  --k-bg-1: #fff;
+  --k-bg-2: #f8f8f8;
+  --k-bg-3: #f6f6f6;
+  --k-bg-4: #f2f2f2;
+
+  // 03 辅助色
+  --k-orange: #ffebdd;
+  --k-red: #f44541;
+  --k-blue: #64a9ff;
+  --k-purple: #8f80ff;
+
+  // 04 渐变色
+  --k-gradient-1: linear-gradient(90deg, #ff9c63 0%, #ff7144 100%);
+  --k-gradient-2: linear-gradient(270deg, #ff4f44 0%, #ffafab 100%);
+  --k-gradient-3: linear-gradient(90deg, #8cccff 0%, #459aff 100%);
+  --k-gradient-4: linear-gradient(90deg, #d4a9ff 0%, #8f80ff 100%);
+  --k-gradient-5: linear-gradient(90deg, #a9f0b4 0%, #09c58c 100%);
+
+  // 05 字体颜色
+  --k-font-primary: #2dc7aa;
+  --k-font-danger: #f44541;
+  --k-gray-1: #333333;
+  --k-gray-2: #666666;
+  --k-gray-3: #777777;
+  --k-gray-4: #aaaaaa;
+  --k-gray-5: #cccccc;
+
+  // 06 分割线
+  --k-hairline-dark: #eeeeee;
+  --k-hairline-shallow: #f2f2f2;
+
+  // 07 蒙层
+  --k-overlay-background-dark: rgba(0, 0, 0, 0.7);
+  --k-overlay-background-shallow: rgba(0, 0, 0, 0.5);
+
+  // 圆角
+  --k-radius-sm: 2px;
+  --k-radius-md: 4px;
+  --k-radius-lg: 8px;
+  --k-radius-xl: 10px;
+  --k-radius-max: 999px;
+
+  // 间距
+  --k-padding-base: 4px;
+  --k-padding-xs: 6px;
+  --k-padding-sm: 8px;
+  --k-padding-md: 12px;
+  --k-padding-lg: 16px;
+  --k-padding-xl: 20px;
+  --k-padding-page: 13px; // 页面是基础边距
+  --k-padding-card: 9px; // 卡片的基础边距
+
+  // 描边 投影
+  --k-border-color: var(--k-primary);
+  --k-border-width: 1px;
+  --k-shadow: 0px 2px 12px 0px rgba(100, 101, 102, 0.12);
+
+  // 设置Vant UI组件库中的默认样式;
+  --van-primary: var(--k-primary);
+  --van-primary-color: var(--van-primary);
+  --van-primary-text: var(--k-font-primary);
+  --van-text-color: var(--k-gray-1);
+
+  // 多选框
+  --van-checkbox-border-color: #dcdcdc;
+  --van-checkbox-label-color: var(--k-gray-1);
+  --van-checkbox-disabled-icon-color: #dcdcdc;
+  --van-checkbox-disabled-label-color: var(--k-gray-5);
+  --van-checkbox-disabled-background: #f7f8fa;
+
+  // 单选框
+  --van-radio-border-color: #dcdcdc;
+  --van-radio-disabled-icon-color: #dcdcdc;
+  --van-radio-disabled-background: #f7f8fa;
+
+  // 导航
+  --van-nav-bar-arrow-size: 20px;
+  --van-nav-bar-title-font-size: 18px;
+  --van-nav-bar-title-text-color: var(--k-gray-1);
+  --van-nav-bar-icon-color: var(--k-gray-1);
+
+  // tab 选择卡
+  --van-tab-text-color: var(--k-gray-3);
+  --van-tabs-bottom-bar-width: 40px;
+  --van-tab-active-text-color: var(--k-gray-1);
+
+  // 侧边导航栏(分类选择)
+  --van-sidebar-selected-border-width: 2px;
+  --van-sidebar-selected-border-height: 18px;
+  --van-sidebar-text-color: var(--k-gray-1);
+  --van-sidebar-selected-text-color: var(--k-primary);
+
+  // 宫格
+  --van-grid-item-text-color: var(--k-gray-1);
+  --van-grid-item-text-font-size: 14px;
+
+  // 步骤条
+  --van-step-horizontal-title-font-size: 14px;
+  --van-step-finish-text-color: var(--k-gray-1);
+  --van-step-text-color: #999;
+
+  // 按钮
+  --van-button-normal-font-size: 18px;
+
+  // 通知栏
+  --van-notice-bar-background: #ffe3d2;
+  --van-notice-bar-text-color: var(--k-font-primary);
+
+  // 开关
+  --van-switch-size: 22px;
+  --van-switch-width: calc(2em + 4px);
+  --van-switch-height: calc(1em + 4px);
+  // --van-switch-background: #fff;
+
+  // 折叠面板
+  --van-collapse-item-content-text-color: #999;
+
+  // 头部高度
+  --van-nav-bar-height: 44px;
+  --van-nav-bar-arrow-size: 22px;
+}

+ 1 - 0
src/styles/index.less

@@ -1,5 +1,6 @@
 @import url('./iconfont/iconfont.css');
 @import url('./font/index.less');
+@import url('./component-ui.less');
 
 :root {
   // Color Palette

+ 2 - 2
src/teacher/live-class/create-components/detail.tsx

@@ -23,13 +23,13 @@ export default defineComponent({
   computed: {
     userInfo() {
       const startTime = createState.live.coursePlanList[0].startTime
-      // const endTime = createState.live.coursePlanList[0].endTime
+      const endTime = createState.live.coursePlanList[0].endTime
       return {
         headUrl: state.user.data?.heardUrl,
         username:
           state.user.data?.username || `游客${state.user.data?.userId || ''}`,
         startTime:
-          `${dayjs(startTime).format('YYYY-MM-DD')} ${dayjs(startTime).format(
+          `${dayjs(startTime).format('YYYY-MM-DD')} ${dayjs(endTime).format(
             'HH:mm'
           )}` || '',
         courseStartTime: dayjs(startTime).format('YYYY-MM-DD HH:mm'),

+ 96 - 0
src/teacher/statistics/home-statistics-detail/buy-item/index.module.less

@@ -0,0 +1,96 @@
+.cell {
+  padding: 13px 12px;
+
+  .top {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .timer {
+    display: flex;
+    align-items: center;
+    font-size: 13px;
+    color: #666666;
+    line-height: 18px;
+    img {
+      width: 16px;
+      height: 16px;
+      margin-right: 6px;
+    }
+  }
+
+  .userInfo {
+    display: flex;
+    align-items: center;
+    img {
+      width: 20px;
+      height: 20px;
+      border-radius: 20px;
+      margin-left: 4px;
+    }
+    .name {
+      font-size: 13px;
+      color: #333333;
+      line-height: 17px;
+      max-width: 70px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+  }
+
+  .content {
+    display: flex;
+    // align-items: center;
+    padding-top: 12px;
+  }
+  .cover {
+    width: 90px;
+    height: 51px;
+    border-radius: 4px;
+    overflow: hidden;
+    flex-shrink: 0;
+    margin-right: 10px;
+  }
+  .cover1 {
+    width: 51px;
+  }
+
+  .info {
+    // flex: 1;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    padding: 3px 0;
+    .iTitle {
+      font-weight: 600;
+      font-size: 14px;
+      color: #333333;
+      line-height: 17px;
+      // width: 100%;
+      max-width: 220px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    .iPrice {
+      font-size: 13px;
+      color: #777777;
+      line-height: 17px;
+      display: flex;
+      align-items: center;
+
+      .price {
+        font-family: DINAlternate, DINAlternate;
+        font-weight: bold;
+        font-size: 18px;
+        color: #ff5a56;
+        line-height: 16px;
+        i {
+          font-style: normal;
+          font-size: 14px;
+        }
+      }
+    }
+  }
+}

+ 58 - 0
src/teacher/statistics/home-statistics-detail/buy-item/index.tsx

@@ -0,0 +1,58 @@
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+import { Cell, CellGroup } from 'vant'
+import iconTimer from '@/common/images/icon_timer2.png'
+import iconMoney from '../../images/icon-money.png'
+
+export default defineComponent({
+  name: 'teacher-item',
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    },
+    isSquare: {
+      type: Boolean,
+      default: false
+    }
+  },
+  setup(props) {
+    return () => (
+      <CellGroup border={false}>
+        {props.list.map((item, index) => (
+          <Cell class={styles.cell} center>
+            {{
+              title: () => (
+                <div class={styles.top}>
+                  <div class={styles.timer}>
+                    <img src={iconTimer} />
+                    <span>购买时间:2024-10-30 15:23</span>
+                  </div>
+                  <div class={styles.userInfo}>
+                    <span class={styles.name}>张涵宇张涵宇张涵宇</span>
+                    <img src={iconMoney} />
+                  </div>
+                </div>
+              ),
+              label: () => (
+                <div class={styles.content}>
+                  <img class={[styles.cover, props.isSquare && styles.cover1]} />
+                  <div class={styles.info}>
+                    <div class={styles.iTitle}>著名大号大师严琦带你去走近带你去走近带你去走近音</div>
+                    <div class={styles.iPrice}>
+                      <span>预计收入</span>
+                      <span class={styles.price}>
+                        <i>¥</i>
+                        560.00
+                      </span>
+                    </div>
+                  </div>
+                </div>
+              )
+            }}
+          </Cell>
+        ))}
+      </CellGroup>
+    )
+  }
+})

+ 243 - 0
src/teacher/statistics/home-statistics-detail/echats/index.module.less

@@ -0,0 +1,243 @@
+.homeHead {
+  display: flex;
+  justify-content: space-between;
+  padding-bottom: 12px;
+
+  .title {
+    display: flex;
+    align-items: center;
+    font-weight: 600;
+    font-size: 15px;
+    color: #333333;
+    line-height: 20px;
+
+    img {
+      width: 18px;
+      height: 18px;
+      margin-right: 6px;
+    }
+  }
+
+  .right {
+    display: flex;
+    align-items: center;
+  }
+
+  .showItem {
+    display: flex;
+    align-items: center;
+    font-size: 13px;
+    color: #131415;
+    line-height: 18px;
+
+    img {
+      margin-left: 4px;
+      width: 9px;
+      height: 5px;
+    }
+
+    &.showItemActive {
+      color: #2dc7aa;
+    }
+  }
+}
+
+.eChartSection {
+  background-color: #fff;
+  box-shadow: 0px 2px 10px 0px rgba(229, 229, 229, 0.1);
+  border-radius: 10px;
+  padding: 12px;
+  margin: 0 14px;
+  .eChartTitle {
+    display: flex;
+    justify-content: space-between;
+    background: #f8f8f8;
+    border-radius: 4px;
+    padding: 6px 12px;
+
+    .left {
+      display: flex;
+      align-items: center;
+    }
+
+    .item {
+      display: flex;
+      align-items: center;
+      margin-right: 12px;
+      --color: #2dc7aa;
+
+      &:last-child {
+        margin-right: 0;
+      }
+
+      .line {
+        display: inline-block;
+        width: 10px;
+        height: 3px;
+        background: var(--color);
+        border-radius: 3px;
+      }
+
+      .text {
+        font-size: 12px;
+        color: #333333;
+        line-height: 16px;
+        padding: 0 4px 0 6px;
+      }
+
+      .num {
+        font-weight: 600;
+        font-size: 12px;
+        color: var(--color);
+        line-height: 16px;
+      }
+    }
+  }
+
+  .eChart {
+    height: 240px;
+    padding: 0;
+  }
+}
+
+.popupContainer {
+  // max-height: 504px;
+  // overflow-x: hidden;
+  // overflow-y: auto;
+  .popupTitle {
+    position: sticky;
+    z-index: 1;
+    top: 0;
+    text-align: center;
+    font-weight: 600;
+    font-size: 18px;
+    color: #333333;
+    line-height: 24px;
+    padding: 18px 0 12px;
+  }
+
+  // .popupSearchList {
+  // min-height: 30vh;
+  // max-height: 50vh;
+  // overflow: hidden auto;
+  // }
+
+  .popupSection {
+    padding: 0 16px 18px;
+    .title {
+      display: flex;
+      justify-content: space-between;
+      padding-bottom: 10px;
+      span {
+        display: flex;
+        align-items: center;
+        font-weight: 600;
+        font-size: 15px;
+        color: #333333;
+        line-height: 18px;
+        &::before {
+          content: '';
+          display: inline-block;
+          width: 3px;
+          height: 12px;
+          background: linear-gradient(180deg, #59e5d4 0%, #2dc7aa 100%);
+          border-radius: 2px;
+          margin-right: 4px;
+        }
+      }
+    }
+
+    .timeCount {
+      display: flex;
+      align-items: center;
+
+      p {
+        margin-left: 10px;
+        flex: 1;
+        background: #f8f8f8;
+        border: 1px solid #f8f8f8;
+        border-radius: 4px;
+        font-size: 13px;
+        color: #999999;
+        line-height: 18px;
+        text-align: center;
+        padding: 6px 0;
+        &:first-child {
+          margin-left: 0;
+        }
+
+        &.active {
+          background: #e9fff8;
+          border-radius: 4px;
+          border: 1px solid #2dc7aa;
+          color: #2dc7aa;
+        }
+      }
+    }
+
+    .timeSubject {
+      flex-wrap: wrap;
+      margin-left: -5px;
+      margin-right: -5px;
+      p {
+        width: calc(33.333% - 10px);
+        padding: 6px 3px;
+        margin: 0 5px;
+        flex: none;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        margin-bottom: 9px;
+        box-sizing: border-box;
+        &:first-child {
+          margin-left: 5px;
+        }
+      }
+    }
+
+    .timeRang {
+      margin-top: 10px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .timeInput {
+        width: 159px;
+        line-height: 32px;
+        text-align: center;
+        background: #f8f8f8;
+        border-radius: 4px;
+        font-size: 13px;
+        color: #999999;
+        cursor: pointer;
+      }
+      .hasValue {
+        color: #333;
+      }
+
+      .timeUnit {
+        width: 12px;
+        height: 1px;
+        background: #d0d0d0;
+      }
+    }
+  }
+
+  .popupBottom {
+    position: sticky;
+    z-index: 1;
+    bottom: 0;
+    border-top: 1px solid #f2f2f2;
+    padding: 20px 13px 30px;
+    display: flex;
+    align-items: center;
+    :global {
+      .van-button {
+        font-size: 16px;
+      }
+      .van-button + .van-button {
+        margin-left: 15px;
+      }
+    }
+  }
+}

+ 467 - 0
src/teacher/statistics/home-statistics-detail/echats/index.tsx

@@ -0,0 +1,467 @@
+import {
+  defineComponent,
+  nextTick,
+  onMounted,
+  PropType,
+  reactive,
+  ref,
+  watch
+} from 'vue'
+import styles from './index.module.less'
+import icon1 from '../../images/icon1.png'
+import iconArrow from '../../images/icon-arrow.png'
+import iconArrow1 from '../../images/icon-arrow1.png'
+import iconArrow11 from '../../images/icon-arrow1-1.png'
+import * as echarts from 'echarts/core'
+import {
+  LineChart
+  // LineSeriesOption
+} from 'echarts/charts'
+// import { PieChart } from 'echarts/charts'
+import {
+  TitleComponent,
+  // 组件类型的定义后缀都为 ComponentOption
+  // TitleComponentOption,
+  TooltipComponent,
+  // TooltipComponentOption,
+  GridComponent,
+  // 数据集组件
+  DatasetComponent,
+  // DatasetComponentOption,
+  // 内置数据转换器组件 (filter, sort)
+  // TransformComponent,
+  LegendComponent,
+  ToolboxComponent,
+  DataZoomComponent
+} from 'echarts/components'
+import { LabelLayout } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import { Button, DatetimePicker, Popup } from 'vant'
+import { formatterDatePicker } from '@/helpers/utils'
+import dayjs from 'dayjs'
+import { getTimeRange, TIME_TYPE } from '../../home-statistics'
+
+// 注册必须的组件
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  DatasetComponent,
+  // TransformComponent,
+  LabelLayout,
+  // UniversalTransition,
+  CanvasRenderer,
+  // PieChart,
+  ToolboxComponent,
+  LegendComponent,
+  DataZoomComponent,
+  LineChart
+])
+
+const lineChartOption = (xAxisData: any, seriesData: any) => {
+  return {
+    title: {
+      text: '单位:次',
+      textStyle: {
+        color: '#777777',
+        fontSize: 13,
+        fontWeight: 400
+      }
+    },
+    legend: { show: false },
+    emphasis: { lineStyle: { width: 2 } },
+    xAxis: {
+      boundaryGap: false,
+      data: xAxisData,
+      type: 'category',
+      axisLine: { lineStyle: { color: '#8C8C8C' } },
+      lineStyle: { color: '#F2F2F2' }
+    },
+    color: [
+      '#2DC7AA',
+      '#FF6079'
+      // '#2DC7AA',
+      // '#FF602C',
+      // '#91DD1C',
+      // '#FFA92C',
+      // '#BE7E2E',
+      // '#1C96DD',
+      // '#D22CFF',
+      // '#FF3C3C',
+      // '#1AEE3E',
+      // '#00c9ff'
+    ],
+    series: [
+      {
+        lineStyle: { width: 1 },
+        data: seriesData[0],
+        symbol: 'circle',
+        name: '浏览次数',
+        type: 'line',
+        emphasis: { lineStyle: { width: 1 } }
+      },
+      {
+        lineStyle: { width: 1 },
+        data: seriesData[1],
+        symbol: 'circle',
+        name: '购买次数',
+        type: 'line',
+        areaStyle: {
+          color: {
+            type: 'linear',
+            x: 0,
+            y: 0,
+            x2: 0,
+            y2: 1,
+            colorStops: [
+              {
+                offset: 0,
+                color: 'rgba(255, 96, 121, 0.23)'
+                // 0% 处的颜色
+              },
+              {
+                offset: 1,
+                // 100% 处的颜色
+                color: 'rgba(255, 96, 121, 0)'
+              }
+            ]
+          }
+        },
+        emphasis: { lineStyle: { width: 1 } }
+      }
+    ],
+    grid: {
+      bottom: '3%',
+      containLabel: true,
+      left: '3%',
+      right: '5%',
+      top: '40'
+    },
+    tooltip: {
+      trigger: 'axis',
+      confine: true,
+      formatter: function (params: any) {
+        return params[0].name
+      },
+      backgroundColor: '#FF6079',
+      borderWidth: 0,
+      borderRadius: 24,
+      padding: [1, 4],
+      textStyle: {
+        color: '#FFFFFF',
+        fontSize: 12
+      }
+    },
+    yAxis: {
+      type: 'value',
+      splitLine: {
+        axisLine: { lineStyle: { color: '#8C8C8C' } },
+        lineStyle: { color: ['#f2f2f2'], type: 'dashed' }
+      }
+    },
+    dataZoom: [{ type: 'inside', throttle: 100 }],
+    toolbox: { feature: { saveAsImage: { show: false } } }
+  }
+}
+export default defineComponent({
+  name: 'eChats-model',
+  props: {
+    obj: {
+      type: Object,
+      default: () => ({})
+    },
+    currentType: {
+      type: String as PropType<TIME_TYPE>,
+      default: 'MONTH'
+    }
+  },
+  emits: ['confirm'],
+  setup(props, { emit }) {
+    const chartId = 'eChart' + Date.now()
+    const statisticCounts = ref({
+      browseCount: 0,
+      buyCount: 0
+    })
+    const currentType = ref<TIME_TYPE>(props.currentType)
+    const timeRange = getTimeRange(currentType.value)
+    const searchStatus = ref(false)
+    const forms = reactive({
+      loading: false,
+      dataShow: true,
+      subjectId: '' as any, // 选择的声部
+      subjectList: [] as any,
+      startTimeStatus: false,
+      endTimeMinDate: new Date(),
+      endTimeMaxDate: dayjs(new Date()).add(1, 'year').toDate(),
+      endTimeStatus: false,
+      startTime: new Date(timeRange?.startTime || ''),
+      startTimeStr: timeRange?.startTime || '',
+      endTime: new Date(timeRange?.endTime || ''),
+      endTimeStr: timeRange?.endTime || ''
+    })
+    let myChart: echarts.ECharts
+
+    const _initData = () => {
+      nextTick(() => {
+        statisticCounts.value.browseCount = props.obj.browseCount || 0
+        statisticCounts.value.buyCount = props.obj.buyCount || 0
+        myChart.clear()
+        lineChartOption &&
+          myChart.setOption(
+            lineChartOption(props.obj.xAxisData, props.obj.yAxisData)
+          )
+        myChart.on('highlight', function (params: any) {
+          const batch = params.batch || []
+          const options: any = myChart.getOption()
+          batch.forEach((item: any) => {
+            const batchIndex = item.dataIndex
+
+            const browseCount = options.series[0].data[batchIndex]
+            const buyCount = options.series[1].data[batchIndex]
+            statisticCounts.value = {
+              browseCount,
+              buyCount
+            }
+          })
+        })
+      })
+    }
+
+    nextTick(() => {
+      myChart = echarts.init(document.getElementById(chartId) as HTMLDivElement)
+      _initData()
+    })
+
+    watch(
+      () => props.obj,
+      () => {
+        _initData()
+      },
+      {
+        deep: true
+      }
+    )
+
+    watch(
+      () => props.currentType,
+      () => {
+        currentType.value = props.currentType
+      }
+    )
+
+    const onChangeTime = (type: TIME_TYPE) => {
+      if (currentType.value === type) return
+      currentType.value = type
+      resetTime(type)
+      // emit('confirm', currentType.value)
+    }
+
+    // 格式化
+    const resetTime = (type: TIME_TYPE) => {
+      const timeRang = getTimeRange(type)
+
+      forms.startTime = new Date(timeRang?.startTime || '')
+      forms.startTimeStr = timeRang?.startTime || ''
+      forms.endTimeMinDate = dayjs(timeRang?.startTime || '').toDate()
+      forms.endTimeMaxDate = dayjs(timeRang?.startTime || '')
+        .add(1, 'year')
+        .toDate()
+      forms.endTime = new Date(timeRang?.endTime || '')
+      forms.endTimeStr = timeRang?.endTime || ''
+    }
+
+    return () => (
+      <div class={styles.eChartSection}>
+        <div class={styles.homeHead}>
+          <div class={styles.title}>
+            <img src={icon1} />
+            <span>浏览/购买</span>
+          </div>
+
+          <div class={styles.right}>
+            <div
+              class={[
+                styles.showItem,
+                searchStatus.value && styles.showItemActive
+              ]}
+              onClick={() => (searchStatus.value = true)}
+            >
+              <span>本月</span>
+              <img src={searchStatus.value ? iconArrow11 : iconArrow1} />
+            </div>
+          </div>
+        </div>
+        <div class={styles.eChartTitle}>
+          <div class={styles.left}>
+            <div class={styles.item} style="--color: #2DC7AA">
+              <span class={styles.line}></span>
+              <span class={styles.text}>浏览次数</span>
+              <span class={styles.num}>
+                {statisticCounts.value.browseCount}次
+              </span>
+            </div>
+            <div class={styles.item} style="--color: #FF6079">
+              <span class={styles.line}></span>
+              <span class={styles.text}>购买次数</span>
+              <span class={styles.num}>{statisticCounts.value.buyCount}次</span>
+            </div>
+          </div>
+        </div>
+
+        <div class={styles.eChart}>
+          <div id={chartId} style="width: 100%; height: 100%;"></div>
+        </div>
+
+        <Popup
+          v-model:show={searchStatus.value}
+          closeable
+          round
+          position="bottom"
+          teleport="body"
+        >
+          <div class={styles.popupContainer}>
+            <div class={styles.popupTitle}>筛选</div>
+
+            <div class={styles.popupSearchList}>
+              <div class={styles.popupSection}>
+                <div class={styles.title}>
+                  <span>时间</span>
+                </div>
+
+                <div class={styles.timeCount}>
+                  <p
+                    onClick={() => onChangeTime('MONTH')}
+                    class={currentType.value === 'MONTH' ? styles.active : ''}
+                  >
+                    本月
+                  </p>
+                  <p
+                    onClick={() => onChangeTime('THREE_MONTH')}
+                    class={
+                      currentType.value === 'THREE_MONTH' ? styles.active : ''
+                    }
+                  >
+                    近三个月
+                  </p>
+                  <p
+                    onClick={() => onChangeTime('HALF_YEAR')}
+                    class={
+                      currentType.value === 'HALF_YEAR' ? styles.active : ''
+                    }
+                  >
+                    近半年
+                  </p>
+                  <p
+                    onClick={() => onChangeTime('YEAR')}
+                    class={currentType.value === 'YEAR' ? styles.active : ''}
+                  >
+                    近一年
+                  </p>
+                </div>
+
+                <div class={styles.timeRang}>
+                  <p
+                    class={[
+                      styles.timeInput,
+                      forms.startTimeStr && styles.hasValue
+                    ]}
+                    onClick={() => (forms.startTimeStatus = true)}
+                  >
+                    {forms.startTimeStr || '起始时间'}
+                  </p>
+                  <p class={styles.timeUnit}></p>
+                  <p
+                    class={[
+                      styles.timeInput,
+                      forms.endTimeStr && styles.hasValue
+                    ]}
+                    onClick={() => (forms.endTimeStatus = true)}
+                  >
+                    {forms.endTimeStr || '终止时间'}
+                  </p>
+                </div>
+              </div>
+            </div>
+
+            <div class={styles.popupBottom}>
+              <Button
+                round
+                block
+                type="default"
+                onClick={() => {
+                  currentType.value = props.currentType
+                  resetTime(props.currentType)
+                }}
+              >
+                重置
+              </Button>
+              <Button
+                round
+                block
+                type="primary"
+                onClick={() => {
+                  emit('confirm', {
+                    startTime: forms.startTimeStr,
+                    endTime: forms.endTimeStr
+                  })
+                  searchStatus.value = false
+                }}
+              >
+                确定
+              </Button>
+            </div>
+          </div>
+        </Popup>
+
+        {/* 开始日期 */}
+        <Popup
+          v-model:show={forms.startTimeStatus}
+          position="bottom"
+          round
+          class={'popupBottomSearch'}
+          teleport={'body'}
+        >
+          <DatetimePicker
+            v-model={forms.startTime}
+            type="date"
+            formatter={formatterDatePicker}
+            onCancel={() => (forms.startTimeStatus = false)}
+            onConfirm={(val: any) => {
+              forms.startTime = val
+              forms.startTimeStr = dayjs(val).format('YYYY-MM-DD')
+              forms.startTimeStatus = false
+              forms.endTime = null as any
+              forms.endTimeStr = ''
+              forms.endTimeMinDate = dayjs(val || new Date()).toDate()
+              forms.endTimeMaxDate = dayjs(val || new Date())
+                .add(1, 'year')
+                .toDate()
+            }}
+          />
+        </Popup>
+        {/* 结束日期 */}
+        <Popup
+          v-model:show={forms.endTimeStatus}
+          position="bottom"
+          round
+          class={'popupBottomSearch'}
+          teleport={'body'}
+        >
+          <DatetimePicker
+            v-model={forms.endTime}
+            type="date"
+            minDate={forms.endTimeMinDate}
+            maxDate={forms.endTimeMaxDate}
+            formatter={formatterDatePicker}
+            onCancel={() => (forms.endTimeStatus = false)}
+            onConfirm={(val: any) => {
+              forms.endTime = val
+              forms.endTimeStatus = false
+              forms.endTimeStr = dayjs(val).format('YYYY-MM-DD')
+            }}
+          />
+        </Popup>
+      </div>
+    )
+  }
+})

+ 38 - 0
src/teacher/statistics/home-statistics-detail/index.module.less

@@ -0,0 +1,38 @@
+.homeStatistics {
+  min-height: 100vh;
+  // background: #F6F7F8;
+  background: linear-gradient(to bottom, #beffe6 0px, #f6f7f8 595px);
+  // background: linear-gradient(90deg, #FE4083 0%, #FEC3D4 100%);
+}
+
+.tabs {
+  :global {
+    .van-tabs__nav {
+      background-color: transparent;
+      padding-bottom: 12px;
+    }
+    .van-tab {
+      font-size: 16px;
+      color: #666;
+    }
+    .van-tab--active {
+      font-weight: 600;
+      color: #333333;
+    }
+    .van-tabs__line {
+      width: 16px;
+      height: 4px;
+      background: #2dc7aa;
+      border-radius: 2px;
+    }
+    .van-tabs__content {
+      height: calc(100vh - var(--van-tabs-line-height) - var(--header-height, 0));
+      overflow-x: hidden;
+      overflow-y: auto;
+    }
+    .van-tab__panel {
+      height: 100%;
+    }
+  }
+}
+

+ 40 - 0
src/teacher/statistics/home-statistics-detail/index.tsx

@@ -0,0 +1,40 @@
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+import { Tab, Tabs } from 'vant'
+import ColHeader from '@/components/col-header'
+import TheSticky from '@/components/the-sticky'
+import List from './list'
+
+export default defineComponent({
+  name: 'HomeStatistics',
+  setup() {
+    return () => (
+      <div class={styles.homeStatistics}>
+        <TheSticky position="top">
+          <ColHeader border={false} background="transparent" />
+        </TheSticky>
+
+        <Tabs class={styles.tabs} swipeable>
+          <Tab title="VIP定制课" name="VIP_COURSE">
+            <List type="VIP_COURSE" />
+          </Tab>
+          <Tab title="趣纠课" name="PRACTICE">
+            <List type="PRACTICE" />
+          </Tab>
+          <Tab title="小组课" name="GROUP">
+            <List type="GROUP" />
+          </Tab>
+          <Tab title="直播课" name="LIVE">
+            <List type="LIVE" />
+          </Tab>
+          <Tab title="视频课" name="VIDEO">
+            <List type="VIDEO" />
+          </Tab>
+          <Tab title="乐谱" name="MUSIC">
+            <List type="MUSIC" />
+          </Tab>
+        </Tabs>
+      </div>
+    )
+  }
+})

+ 77 - 0
src/teacher/statistics/home-statistics-detail/list/index.module.less

@@ -0,0 +1,77 @@
+.list {
+  padding: 12px 0 30px;
+}
+
+.expectedIncome {
+  background: #ffffff;
+  border-radius: 10px;
+  margin: 12px 14px 0;
+  overflow: hidden;
+  .incomeTitle {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 15px 12px 12px;
+    .title {
+      display: flex;
+      align-items: center;
+      font-weight: 600;
+      font-size: 15px;
+      color: #333333;
+      line-height: 20px;
+    }
+    img {
+      width: 18px;
+      height: 18px;
+      margin-right: 6px;
+    }
+    .price {
+      font-weight: bold;
+      font-size: 16px;
+      color: #333333;
+      line-height: 16px;
+      font-family: DINAlternate, DINAlternate;
+      span {
+        font-size: 14px;
+      }
+    }
+  }
+  .incomeTip {
+    font-size: 13px;
+    color: #ef8548;
+    line-height: 18px;
+    background: #fff9f0;
+    box-shadow: 0px 2px 10px 0px rgba(229, 229, 229, 0.1);
+    border-radius: 6px;
+    border: 1px solid #ffefdf;
+    padding: 5px 10px;
+    margin: 0 12px 0;
+  }
+
+  .element {
+    position: relative;
+    height: 0;
+    border-top: 1px dashed #dedede;
+    margin: 16px 19px 0;
+
+    &::before,
+    &::after {
+      content: '';
+      position: absolute;
+      z-index: 1;
+      bottom: -6.5px;
+      display: inline-block;
+      width: 14px;
+      height: 14px;
+      background: #f6f7f8;
+      border-radius: 50%;
+    }
+
+    &::before {
+      left: -26px;
+    }
+    &::after {
+      right: -26px;
+    }
+  }
+}

+ 224 - 0
src/teacher/statistics/home-statistics-detail/list/index.tsx

@@ -0,0 +1,224 @@
+import { defineComponent, PropType, ref } from 'vue'
+import styles from './index.module.less'
+import { CellGroup, List, Sticky } from 'vant'
+import iconMoney from '../../images/icon-money.png'
+import ColResult from '@/components/col-result'
+import Echats from '../echats'
+import TeacherItem from '../teacher-item'
+import BuyItem from '../buy-item'
+import request from '@/helpers/request'
+import { getTimeRange, TIME_TYPE } from '../../home-statistics'
+import { moneyFormat } from '@/helpers/utils'
+
+export default defineComponent({
+  name: 'list',
+  props: {
+    type: {
+      type: String as PropType<
+        'VIP_COURSE' | 'PRACTICE' | 'GROUP' | 'LIVE' | 'VIDEO' | 'MUSIC'
+      >,
+      default: 'VIP_COURSE'
+    }
+  },
+  setup(props) {
+    const timeRange = ref(getTimeRange('MONTH'))
+
+    const obj = ref({
+      xAxisData: [] as any,
+      yAxisData: [] as any,
+      browseCount: 0,
+      buyCount: 0
+    })
+
+    const dataShow = ref(true) // 判断是否有数据
+    const state = {
+      statInfo: 0, // 预计课程总收入
+      accountPeriod: 0, // 几天后
+      loading: false,
+      finished: false,
+      params: {
+        page: 1,
+        rows: 20
+      }
+    }
+
+    const tableList = ref<any[]>([])
+
+    const getSysConfig = async () => {
+      try {
+        const { data } = await request.get('/api-teacher/sysConfig/list', {
+          params: {
+            group: 'ACCOUNT_PERIOD'
+          }
+        })
+        const result = data || []
+        result.forEach((item: any) => {
+          if (
+            props.type === 'VIP_COURSE' &&
+            item.paramName === 'vip_course_account_period'
+          ) {
+            state.accountPeriod = item.paramValue
+          } else if (
+            props.type === 'PRACTICE' &&
+            item.paramName === 'practice_account_period'
+          ) {
+            state.accountPeriod = item.paramValue
+          } else if (
+            props.type === 'GROUP' &&
+            item.paramName === 'group_course_account_period'
+          ) {
+            state.accountPeriod = item.paramValue
+          } else if (
+            props.type === 'LIVE' &&
+            item.paramName === 'live_account_period'
+          ) {
+            state.accountPeriod = item.paramValue
+          } else if (
+            props.type === 'VIDEO' &&
+            item.paramName === 'video_account_period'
+          ) {
+            state.accountPeriod = item.paramValue
+          } else if (
+            props.type === 'MUSIC' &&
+            item.paramName === 'music_account_period'
+          ) {
+            state.accountPeriod = item.paramValue
+          }
+        })
+      } catch {
+        //
+      }
+    }
+
+    const getDetail = async () => {
+      try {
+        const { data } = await request.post(
+          '/api-teacher/home/courseExposure',
+          {
+            data: { ...timeRange.value, type: props.type }
+          }
+        )
+
+        const buy = data.buy || []
+        const exposure = data.exposure || []
+        const xAxisData: string[] = []
+        const exposureList: number[] = []
+        exposure.forEach((item: any, index: number) => {
+          xAxisData.push(item.date)
+          exposureList.push(item.exposureNum)
+
+          if (exposure.length - 1 === index) {
+            obj.value.browseCount = item.exposureNum
+          }
+        })
+        const buyList: number[] = []
+        buy.forEach((item: any, index: number) => {
+          buyList.push(item.exposureNum)
+
+          if (buy.length - 1 === index) {
+            obj.value.buyCount = item.exposureNum
+          }
+        })
+        const yAxisData = [exposureList, buyList]
+
+        obj.value.xAxisData = xAxisData
+        obj.value.yAxisData = yAxisData
+      } catch {
+        //
+      }
+    }
+
+    const getList = async () => {
+      try {
+        const { data } = await request.post(
+          '/api-teacher/home/teacherIncomeList',
+          {
+            data: {
+              ...timeRange.value,
+              type: props.type,
+              ...state.params
+            }
+          }
+        )
+
+        state.loading = false
+        state.statInfo = data.statInfo || 0
+        tableList.value = tableList.value.concat(data.rows || [])
+
+        state.finished = data.pageNo >= data.totalPage
+        state.params.page = data.pageNo + 1
+        dataShow.value = tableList.value.length > 0
+      } catch {
+        dataShow.value = false
+        state.finished = true
+      }
+    }
+
+    getSysConfig()
+    getDetail()
+    getList()
+
+    return () => (
+      <div class={styles.list}>
+        <Echats
+          obj={obj.value}
+          currentType={'MONTH'}
+          onConfirm={(val: any) => {
+            // currentType.value = val
+            timeRange.value = val
+            getDetail()
+            getList()
+          }}
+        />
+
+        <div class={styles.expectedIncome}>
+          {/* <Sticky> */}
+          <div class={styles.incomeTitle}>
+            <div class={styles.title}>
+              <img src={iconMoney} />
+              <span>预计总收入</span>
+            </div>
+
+            <div class={styles.price}>
+              <span>¥ </span>
+              {moneyFormat(state.statInfo || 0)}
+            </div>
+          </div>
+          {/* </Sticky> */}
+          <div class={styles.incomeTip}>
+            实际收入将在课程结束{state.accountPeriod || 0}天后结算
+          </div>
+
+          <div class={styles.element}></div>
+          {dataShow.value ? (
+            <List
+              v-model:loading={state.loading}
+              finished={state.finished}
+              finishedText=" "
+              onLoad={getList}
+            >
+              <CellGroup border={false}>
+                {['VIP_COURSE', 'PRACTICE'].includes(props.type) && (
+                  <TeacherItem list={tableList.value} />
+                )}
+                {['GROUP', 'LIVE', 'VIDEO', 'MUSIC'].includes(props.type) && (
+                  <BuyItem
+                    list={tableList.value}
+                    isSquare={props.type === 'MUSIC'}
+                  />
+                )}
+              </CellGroup>
+            </List>
+          ) : (
+            <ColResult
+              type="empty"
+              btnStatus={false}
+              classImgSize="SMALL"
+              tips="暂无数据~"
+            />
+          )}
+        </div>
+      </div>
+    )
+  }
+})

+ 90 - 0
src/teacher/statistics/home-statistics-detail/teacher-item/index.module.less

@@ -0,0 +1,90 @@
+.cell {
+  padding: 13px 12px;
+
+  .timer {
+    display: flex;
+    align-items: center;
+    font-size: 13px;
+    color: #666666;
+    line-height: 18px;
+    img {
+      width: 16px;
+      height: 16px;
+      margin-right: 6px;
+    }
+  }
+
+  .content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding-top: 12px;
+    flex: 1;
+  }
+
+  .userInfo {
+    display: flex;
+    align-items: center;
+    // flex: 1;
+    flex-basis: 40%;
+    img {
+      width: 40px;
+      height: 40px;
+      border-radius: 20px;
+      margin-right: 9px;
+    }
+    .item {
+      flex-basis: auto;
+    }
+  }
+  .item {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    // flex: 1;
+    flex-basis: 30%;
+    .name {
+      font-size: 13px;
+      color: #333333;
+      line-height: 17px;
+      padding-bottom: 4px;
+      max-width: 70px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    .subjects {
+      font-size: 10px;
+      color: #ff8c00;
+      line-height: 14px;
+      background: #fff1de;
+      border-radius: 4px;
+      padding: 2px 4px;
+    }
+    .classNum {
+      font-family: DIN;
+      font-weight: bold;
+      font-size: 18px;
+      color: #333333;
+      display: flex;
+      align-items: center;
+      i {
+        font-style: normal;
+        font-size: 12px;
+        color: #777777;
+        padding-left: 1px;
+      }
+    }
+    .classPrice {
+      font-family: DIN;
+      font-weight: bold;
+      font-size: 16px;
+      color: #ff5a56;
+      line-height: 16px;
+      i {
+        font-size: 14px;
+        font-style: normal;
+      }
+    }
+  }
+}

+ 59 - 0
src/teacher/statistics/home-statistics-detail/teacher-item/index.tsx

@@ -0,0 +1,59 @@
+import { defineComponent } from 'vue'
+import styles from './index.module.less'
+import { Cell, CellGroup } from 'vant'
+import iconTimer from '@/common/images/icon_timer2.png'
+import iconMoney from '../../images/icon-money.png'
+import ColResult from '@/components/col-result'
+
+export default defineComponent({
+  name: 'teacher-item',
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    }
+  },
+  setup(props) {
+    return () => (
+      <CellGroup border={false}>
+        {props.list.map((item, index) => (
+          <Cell class={styles.cell} center>
+            {{
+              title: () => (
+                <div class={styles.timer}>
+                  <img src={iconTimer} />
+                  <span>购买时间:2024-10-30 15:23</span>
+                </div>
+              ),
+              label: () => (
+                <div class={styles.content}>
+                  <div class={styles.userInfo}>
+                    <img src={iconMoney} />
+                    <div class={styles.item}>
+                      <span class={styles.name}>
+                        {index === 0 ? '张涵宇张涵宇张涵宇' : '张涵宇'}
+                      </span>
+                      <span class={styles.subjects}>长笛</span>
+                    </div>
+                  </div>
+                  <div class={styles.item} style={{ alignItems: 'center' }}>
+                    <span class={styles.name}>课时数</span>
+                    <span class={styles.classNum}>
+                      12 <i>节</i>
+                    </span>
+                  </div>
+                  <div class={styles.item} style={{ alignItems: 'flex-end' }}>
+                    <span class={styles.name}>预计收入</span>
+                    <span class={styles.classPrice}>
+                      <i>¥</i> 560.00
+                    </span>
+                  </div>
+                </div>
+              )
+            }}
+          </Cell>
+        ))}
+      </CellGroup>
+    )
+  }
+})

+ 155 - 0
src/teacher/statistics/home-statistics/index.module.less

@@ -0,0 +1,155 @@
+.homeStatistics {
+  background: #FFFFFF;
+  box-shadow: 0px 2px 10px 0px rgba(229, 229, 229, 0.1);
+  border-radius: 10px;
+  padding: 12px;
+  margin: 0 14px;
+}
+
+.homeHead {
+  display: flex;
+  justify-content: space-between;
+  padding-bottom: 12px;
+
+  .title {
+    display: flex;
+    align-items: center;
+    font-weight: 600;
+    font-size: 15px;
+    color: #333333;
+    line-height: 20px;
+
+    img {
+      width: 18px;
+      height: 18px;
+      margin-right: 6px;
+    }
+  }
+
+  .more {
+    display: flex;
+    align-items: center;
+    font-weight: 400;
+    font-size: 12px;
+    color: rgba(45, 199, 170, 1);
+    line-height: 17px;
+    cursor: pointer;
+    background: rgba(45, 199, 170, 0.1);
+    border-radius: 9px;
+    padding: 0 6px;
+
+    img {
+      width: 5px;
+      height: 8px;
+      margin-left: 5px;
+    }
+  }
+}
+
+.eChartSection {
+  .eChartTitle {
+    display: flex;
+    justify-content: space-between;
+    background: #F8F8F8;
+    border-radius: 4px;
+    padding: 6px 12px;
+
+    .left {
+      display: flex;
+      align-items: center;
+    }
+
+    .item {
+      display: flex;
+      align-items: center;
+      margin-right: 12px;
+      --color: #2DC7AA;
+
+      &:last-child {
+        margin-right: 0;
+      }
+
+      .line {
+        display: inline-block;
+        width: 10px;
+        height: 3px;
+        background: var(--color);
+        border-radius: 3px;
+
+      }
+
+      .text {
+        font-size: 12px;
+        color: #333333;
+        line-height: 16px;
+        padding: 0 4px 0 6px;
+      }
+
+      .num {
+        font-weight: 600;
+        font-size: 12px;
+        color: var(--color);
+        line-height: 16px;
+      }
+    }
+
+    .right {
+      display: flex;
+      align-items: center;
+    }
+
+    .showItem {
+      display: flex;
+      align-items: center;
+      font-size: 13px;
+      color: #131415;
+      line-height: 18px;
+
+      img {
+        margin-left: 4px;
+        width: 9px;
+        height: 5px;
+      }
+
+      &.showItemActive {
+        color: #2DC7AA;
+      }
+    }
+  }
+
+  .eChart {
+    height: 240px;
+    padding: 0;
+  }
+}
+
+:global {
+  .select-time {
+    width: 81px;
+    background: #FFFFFF;
+    box-shadow: 0px 0px 18px 0px rgba(0, 0, 0, 0.15);
+    border-radius: 8px;
+    padding: 6px;
+
+    span {
+      display: block;
+      font-size: 13px;
+      color: #323233;
+      line-height: 20px;
+      text-align: center;
+      line-height: 28px;
+      margin-bottom: 6px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+
+    .active {
+      background: rgba(45, 199, 170, 0.1);
+      border-radius: 4px;
+      color: #2DC7AA;
+      font-weight: 500;
+    }
+  }
+}

+ 409 - 0
src/teacher/statistics/home-statistics/index.tsx

@@ -0,0 +1,409 @@
+import {
+  computed,
+  defineComponent,
+  nextTick,
+  onMounted,
+  reactive,
+  ref
+} from 'vue'
+import styles from './index.module.less'
+import icon1 from '../images/icon1.png'
+import iconArrow from '../images/icon-arrow.png'
+import iconArrow1 from '../images/icon-arrow1.png'
+import iconArrow11 from '../images/icon-arrow1-1.png'
+import { Popover } from 'vant'
+
+import * as echarts from 'echarts/core'
+import {
+  LineChart
+  // LineSeriesOption
+} from 'echarts/charts'
+// import { PieChart } from 'echarts/charts'
+import {
+  TitleComponent,
+  // 组件类型的定义后缀都为 ComponentOption
+  // TitleComponentOption,
+  TooltipComponent,
+  // TooltipComponentOption,
+  GridComponent,
+  // 数据集组件
+  DatasetComponent,
+  // DatasetComponentOption,
+  // 内置数据转换器组件 (filter, sort)
+  // TransformComponent,
+  LegendComponent,
+  ToolboxComponent,
+  DataZoomComponent
+} from 'echarts/components'
+import { LabelLayout } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import { format } from 'path'
+import request from '@/helpers/request'
+import dayjs from 'dayjs'
+import { postMessage } from '@/helpers/native-message'
+import { browser } from '@/helpers/utils'
+import { useRouter } from 'vue-router'
+
+// 注册必须的组件
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  DatasetComponent,
+  // TransformComponent,
+  LabelLayout,
+  // UniversalTransition,
+  CanvasRenderer,
+  // PieChart,
+  ToolboxComponent,
+  LegendComponent,
+  DataZoomComponent,
+  LineChart
+])
+
+const lineChartOption = (xAxisData: any, seriesData: any) => {
+  return {
+    title: {
+      text: '单位:次',
+      textStyle: {
+        color: '#777777',
+        fontSize: 13,
+        fontWeight: 400
+      }
+    },
+    legend: { show: false },
+    emphasis: { lineStyle: { width: 2 } },
+    xAxis: {
+      boundaryGap: false,
+      data: xAxisData,
+      type: 'category',
+      axisLine: { lineStyle: { color: '#8C8C8C' } },
+      lineStyle: { color: '#F2F2F2' }
+    },
+    color: [
+      '#2DC7AA',
+      '#FF6079'
+      // '#2DC7AA',
+      // '#FF602C',
+      // '#91DD1C',
+      // '#FFA92C',
+      // '#BE7E2E',
+      // '#1C96DD',
+      // '#D22CFF',
+      // '#FF3C3C',
+      // '#1AEE3E',
+      // '#00c9ff'
+    ],
+    series: [
+      {
+        lineStyle: { width: 1 },
+        data: seriesData[0],
+        symbol: 'circle',
+        name: '浏览次数',
+        type: 'line',
+        emphasis: { lineStyle: { width: 1 } }
+      },
+      {
+        lineStyle: { width: 1 },
+        data: seriesData[1],
+        symbol: 'circle',
+        name: '购买次数',
+        type: 'line',
+        areaStyle: {
+          color: {
+            type: 'linear',
+            x: 0,
+            y: 0,
+            x2: 0,
+            y2: 1,
+            colorStops: [
+              {
+                offset: 0,
+                color: 'rgba(255, 96, 121, 0.23)'
+                // 0% 处的颜色
+              },
+              {
+                offset: 1,
+                // 100% 处的颜色
+                color: 'rgba(255, 96, 121, 0)'
+              }
+            ]
+          }
+        },
+        emphasis: { lineStyle: { width: 1 } }
+      }
+    ],
+    grid: {
+      bottom: '3%',
+      containLabel: true,
+      left: '3%',
+      right: '5%',
+      top: '40'
+    },
+    tooltip: {
+      trigger: 'axis',
+      confine: true,
+      formatter: function (params: any) {
+        return params[0].name
+      },
+      backgroundColor: '#FF6079',
+      borderWidth: 0,
+      borderRadius: 24,
+      padding: [1, 4],
+      textStyle: {
+        color: '#FFFFFF',
+        fontSize: 12
+      }
+    },
+    yAxis: {
+      type: 'value',
+      splitLine: {
+        axisLine: { lineStyle: { color: '#8C8C8C' } },
+        lineStyle: { color: ['#f2f2f2'], type: 'dashed' }
+      }
+    },
+    dataZoom: [{ type: 'inside', throttle: 100 }],
+    toolbox: { feature: { saveAsImage: { show: false } } }
+  }
+}
+
+export type TIME_TYPE = 'MONTH' | 'THREE_MONTH' | 'HALF_YEAR' | 'YEAR'
+
+/** 获取时间范围 */
+export const getTimeRange = (
+  type: TIME_TYPE
+) => {
+  if (type === 'MONTH') {
+    return {
+      startTime: dayjs().format('YYYY-MM') + '-01',
+      endTime: dayjs().format('YYYY-MM-DD')
+    }
+  } else if (type === 'THREE_MONTH') {
+    return {
+      startTime: dayjs().subtract(3, 'month').format('YYYY-MM-DD'),
+      endTime: dayjs().format('YYYY-MM-DD')
+    }
+  } else if (type === 'HALF_YEAR') {
+    return {
+      startTime: dayjs().subtract(6, 'month').format('YYYY-MM-DD'),
+      endTime: dayjs().format('YYYY-MM-DD')
+    }
+  } else if (type === 'YEAR') {
+    return {
+      startTime: dayjs().subtract(1, 'year').format('YYYY-MM-DD'),
+      endTime: dayjs().format('YYYY-MM-DD')
+    }
+  }
+}
+
+export default defineComponent({
+  name: 'HomeStatistics',
+  setup() {
+    const homeStatisticsRef = ref()
+    const router = useRouter()
+    const popoverStatus = ref(false)
+    const currentType = ref<TIME_TYPE>(
+      'MONTH'
+    )
+    const timeRange = ref(getTimeRange(currentType.value))
+    const statisticCounts = ref({
+      browseCount: 0,
+      buyCount: 0
+    })
+    let myChart: echarts.ECharts
+    nextTick(() => {
+      myChart = echarts.init(
+        document.getElementById('eChart') as HTMLDivElement
+      )
+      getDetail()
+
+      const round = homeStatisticsRef.value?.getBoundingClientRect()
+      postMessage({
+        api: 'homeStatisticsHeight',
+        content: {
+          height: round.height || 300
+        }
+      })
+    })
+
+    const searchText = computed(() => {
+      const template = {
+        MONTH: '本月',
+        THREE_MONTH: '近三个月',
+        HALF_YEAR: '近年半',
+        YEAR: '近一年'
+      }
+      return template[currentType.value]
+    })
+
+    const getDetail = async () => {
+      try {
+        const { data } = await request.post(
+          '/api-teacher/home/courseExposure',
+          {
+            data: timeRange.value
+          }
+        )
+
+        const buy = data.buy || []
+        const exposure = data.exposure || []
+        const xAxisData: string[] = []
+        const exposureList: number[] = []
+        exposure.forEach((item: any, index: number) => {
+          xAxisData.push(item.date)
+          exposureList.push(item.exposureNum)
+
+          if(exposure.length - 1 === index) {
+            statisticCounts.value.browseCount = item.exposureNum
+          }
+        })
+        const buyList: number[] = []
+        buy.forEach((item: any, index: number) => {
+          buyList.push(item.exposureNum)
+
+          if(buy.length - 1 === index) {
+            statisticCounts.value.buyCount = item.exposureNum
+          }
+        })
+        const yAxisData = [exposureList, buyList]
+
+        myChart.clear()
+        lineChartOption &&
+          myChart.setOption(lineChartOption(xAxisData, yAxisData))
+        myChart.on('highlight', function (params: any) {
+          const batch = params.batch || []
+          const options: any = myChart.getOption()
+          batch.forEach((item: any) => {
+            const batchIndex = item.dataIndex
+
+            const browseCount = options.series[0].data[batchIndex]
+            const buyCount = options.series[1].data[batchIndex]
+            statisticCounts.value = {
+              browseCount,
+              buyCount
+            }
+          })
+        })
+      } catch {
+        //
+      }
+    }
+
+    const onChangeTime = (
+      type: TIME_TYPE
+    ) => {
+      popoverStatus.value = false
+      if (currentType.value === type) return
+      currentType.value = type
+      timeRange.value = getTimeRange(currentType.value)
+      getDetail()
+    }
+
+    /** 跳转详情 */
+    const goDetail = () => {
+      if(browser().isApp) {
+        postMessage({
+          api: 'openWebView',
+          content: {
+            url: `${location.origin}/teacher/#/home-statistics-detail`,
+            orientation: 1,
+            isHideTitle: false
+          }
+        })
+      } else {
+        router.push({ path: '/home-statistics-detail' })
+      }
+    }
+    return () => (
+      <div class={styles.homeStatistics} ref={homeStatisticsRef}>
+        <div class={styles.homeHead}>
+          <div class={styles.title}>
+            <img src={icon1} />
+            <span>浏览/购买</span>
+          </div>
+
+          <div class={styles.more} onClick={goDetail}>
+            <span>详情</span>
+            <img src={iconArrow} />
+          </div>
+        </div>
+
+        <div class={styles.eChartSection}>
+          <div class={styles.eChartTitle}>
+            <div class={styles.left}>
+              <div class={styles.item} style="--color: #2DC7AA">
+                <span class={styles.line}></span>
+                <span class={styles.text}>浏览次数</span>
+                <span class={styles.num}>
+                  {statisticCounts.value.browseCount}次
+                </span>
+              </div>
+              <div class={styles.item} style="--color: #FF6079">
+                <span class={styles.line}></span>
+                <span class={styles.text}>购买次数</span>
+                <span class={styles.num}>
+                  {statisticCounts.value.buyCount}次
+                </span>
+              </div>
+            </div>
+            <div class={styles.right}>
+              <Popover v-model:show={popoverStatus.value} showArrow={false}>
+                {{
+                  default: () => (
+                    <div class={'select-time'}>
+                      <span
+                        onClick={() => onChangeTime('MONTH')}
+                        class={currentType.value === 'MONTH' ? 'active' : ''}
+                      >
+                        本月
+                      </span>
+                      <span
+                        onClick={() => onChangeTime('THREE_MONTH')}
+                        class={
+                          currentType.value === 'THREE_MONTH' ? 'active' : ''
+                        }
+                      >
+                        近三个月
+                      </span>
+                      <span
+                        onClick={() => onChangeTime('HALF_YEAR')}
+                        class={
+                          currentType.value === 'HALF_YEAR' ? 'active' : ''
+                        }
+                      >
+                        近半年
+                      </span>
+                      <span
+                        onClick={() => onChangeTime('YEAR')}
+                        class={currentType.value === 'YEAR' ? 'active' : ''}
+                      >
+                        近一年
+                      </span>
+                    </div>
+                  ),
+                  reference: () => (
+                    <div
+                      class={[
+                        styles.showItem,
+                        popoverStatus.value && styles.showItemActive
+                      ]}
+                    >
+                      <span>{searchText.value}</span>
+                      <img
+                        src={popoverStatus.value ? iconArrow11 : iconArrow1}
+                      />
+                    </div>
+                  )
+                }}
+              </Popover>
+            </div>
+          </div>
+
+          <div class={styles.eChart}>
+            <div id="eChart" style="width: 100%; height: 100%;"></div>
+          </div>
+        </div>
+      </div>
+    )
+  }
+})

BIN
src/teacher/statistics/images/filter-bg.png


BIN
src/teacher/statistics/images/icon-1.png


BIN
src/teacher/statistics/images/icon-2.png


BIN
src/teacher/statistics/images/icon-arrow.png


BIN
src/teacher/statistics/images/icon-arrow1-1.png


BIN
src/teacher/statistics/images/icon-arrow1.png


BIN
src/teacher/statistics/images/icon-download.png


BIN
src/teacher/statistics/images/icon-money.png


BIN
src/teacher/statistics/images/icon1.png


+ 50 - 0
src/teacher/statistics/practice-statistics-detail/echats/index.module.less

@@ -0,0 +1,50 @@
+.eChartSection {
+  background-color: #fff;
+  box-shadow: 0px 2px 10px 0px rgba(229, 229, 229, 0.1);
+  border-radius: 10px;
+  padding: 12px 0 0;
+  margin: 0;
+  .eChartTitle {
+    display: flex;
+    justify-content: space-between;
+    background: #F8F8F8;
+    border-radius: 4px;
+    padding: 6px 12px;
+
+    .left {
+      display: flex;
+      align-items: center;
+    }
+
+    .item {
+      display: flex;
+      align-items: center;
+      margin-right: 12px;
+      --color: #2DC7AA;
+
+      &:last-child {
+        margin-right: 0;
+      }
+
+
+      .text {
+        font-size: 12px;
+        color: #333333;
+        line-height: 16px;
+        padding: 0 4px 0 0;
+      }
+
+      .num {
+        font-weight: 600;
+        font-size: 12px;
+        color: var(--color);
+        line-height: 16px;
+      }
+    }
+  }
+
+  .eChart {
+    height: 240px;
+    padding: 0;
+  }
+}

+ 280 - 0
src/teacher/statistics/practice-statistics-detail/echats/index.tsx

@@ -0,0 +1,280 @@
+import {
+  defineComponent,
+  nextTick,
+  onMounted,
+  ref,
+  shallowRef,
+  watch
+} from 'vue'
+import styles from './index.module.less'
+import * as echarts from 'echarts/core'
+import {
+  LineChart
+  // LineSeriesOption
+} from 'echarts/charts'
+// import { PieChart } from 'echarts/charts'
+import {
+  TitleComponent,
+  // 组件类型的定义后缀都为 ComponentOption
+  // TitleComponentOption,
+  TooltipComponent,
+  // TooltipComponentOption,
+  GridComponent,
+  // 数据集组件
+  DatasetComponent,
+  // DatasetComponentOption,
+  // 内置数据转换器组件 (filter, sort)
+  // TransformComponent,
+  LegendComponent,
+  ToolboxComponent,
+  DataZoomComponent
+} from 'echarts/components'
+import { LabelLayout } from 'echarts/features'
+import { CanvasRenderer } from 'echarts/renderers'
+import { formatSecToHMS } from '..'
+
+// 注册必须的组件
+echarts.use([
+  TitleComponent,
+  TooltipComponent,
+  GridComponent,
+  DatasetComponent,
+  // TransformComponent,
+  LabelLayout,
+  // UniversalTransition,
+  CanvasRenderer,
+  // PieChart,
+  ToolboxComponent,
+  LegendComponent,
+  DataZoomComponent,
+  LineChart
+])
+
+const lineChartOption = (params: {
+  xAxisData: any
+  seriesData: any
+  colors: {
+    lineColor?: string
+    startColor?: string
+    endColor?: string
+    unit?: string
+  }
+}) => {
+  return {
+    title: {
+      text: '单位:' + (params.colors.unit || '人'),
+      textStyle: {
+        color: '#777777',
+        fontSize: 13,
+        fontWeight: 400
+      }
+    },
+    legend: { show: false },
+    emphasis: { lineStyle: { width: 2 } },
+    xAxis: {
+      boundaryGap: false,
+      data: params.xAxisData,
+      type: 'category',
+      axisLine: { lineStyle: { color: '#8C8C8C' } },
+      lineStyle: { color: '#F2F2F2' }
+    },
+    color: [
+      params.colors.lineColor || '#2DC7AA'
+      // '#FF6079'
+      // '#2DC7AA',
+      // '#FF602C',
+      // '#91DD1C',
+      // '#FFA92C',
+      // '#BE7E2E',
+      // '#1C96DD',
+      // '#D22CFF',
+      // '#FF3C3C',
+      // '#1AEE3E',
+      // '#00c9ff'
+    ],
+    series: [
+      {
+        lineStyle: { width: 1 },
+        data: params.seriesData,
+        symbol: 'circle',
+        name: '购买次数',
+        type: 'line',
+        areaStyle: {
+          color: {
+            type: 'linear',
+            x: 0,
+            y: 0,
+            x2: 0,
+            y2: 1,
+            colorStops: [
+              {
+                offset: 0,
+                color: params.colors.startColor || 'rgba(45, 199, 170, 0.23)'
+                // 0% 处的颜色
+              },
+              {
+                offset: 1,
+                // 100% 处的颜色
+                color: params.colors.endColor || 'rgba(45, 199, 170, 0)'
+              }
+            ]
+          }
+        },
+        emphasis: { lineStyle: { width: 1 } }
+      }
+    ],
+    grid: {
+      bottom: '3%',
+      containLabel: true,
+      left: '3%',
+      right: '5%',
+      top: '40'
+    },
+    tooltip: {
+      trigger: 'axis',
+      confine: true,
+      formatter: function (params: any) {
+        return params[0].name
+      },
+      backgroundColor: '#FF6079',
+      borderWidth: 0,
+      borderRadius: 24,
+      padding: [1, 4],
+      textStyle: {
+        color: '#FFFFFF',
+        fontSize: 12
+      }
+    },
+    yAxis: {
+      type: 'value',
+      splitLine: {
+        axisLine: { lineStyle: { color: '#8C8C8C' } },
+        lineStyle: { color: ['#f2f2f2'], type: 'dashed' }
+      }
+    },
+    dataZoom: [{ type: 'inside', throttle: 100 }],
+    toolbox: { feature: { saveAsImage: { show: false } } }
+  }
+}
+export default defineComponent({
+  name: 'eChats-model',
+  props: {
+    obj: {
+      type: Object,
+      default: () => ({})
+    },
+    type: {
+      type: String as PropType<'TIME' | 'NUM'>,
+      default: 'TIME'
+    }
+  },
+  setup(props) {
+    const chartId = 'eChart_' + Date.now() + props.type
+    const color = props.type === 'NUM' ? '#FF955D' : '#2DC7AA'
+    const statisticCounts = ref({
+      time: '' as any,
+      count: 0 as any
+    })
+    let myChart: echarts.ECharts
+
+    nextTick(() => {
+      myChart = echarts.init(document.getElementById(chartId) as HTMLDivElement)
+    })
+
+    const _initData = () => {
+      nextTick(() => {
+        statisticCounts.value.time = props.obj.time || ''
+        statisticCounts.value.count =
+          props.type === 'NUM'
+            ? props.obj.count
+            : formatSecToHMS(props.obj.count).all
+
+        myChart.clear()
+        lineChartOption &&
+          myChart.setOption(
+            lineChartOption({
+              xAxisData: props.obj.xAxisData,
+              seriesData: props.obj.yAxisData,
+              colors: {
+                lineColor: color,
+                startColor:
+                  props.type === 'NUM'
+                    ? 'rgba(255, 149, 93, 0.23)'
+                    : 'rgba(45, 199, 170, 0.23)',
+                endColor:
+                  props.type === 'NUM'
+                    ? 'rgba(255, 149, 93, 0)'
+                    : 'rgba(45, 199, 170, 0)',
+                unit: props.type === 'NUM' ? '人' : '分钟'
+              }
+            })
+          )
+        myChart.on('highlight', function (params: any) {
+          const batch = params.batch || []
+          // const options: any = myChart.getOption()
+
+          batch.forEach((item: any) => {
+            const batchIndex = item.dataIndex
+
+            const count =
+              props.type === 'NUM'
+                ? props.obj.yAxisData[batchIndex]
+                : formatSecToHMS(props.obj.yAxisData[batchIndex] || 0).all
+            const time = props.obj.xAxisData[batchIndex]
+            statisticCounts.value = {
+              count,
+              time
+            }
+          })
+        })
+      })
+    }
+
+    nextTick(() => {
+      myChart = echarts.init(document.getElementById(chartId) as HTMLDivElement)
+      _initData()
+    })
+
+    watch(
+      () => props.obj,
+      () => {
+        _initData()
+      },
+      {
+        deep: true
+      }
+    )
+    return () => (
+      <div class={styles.eChartSection}>
+        <div class={styles.eChartTitle}>
+          <div class={styles.left}>
+            <div class={styles.item} style={{ '--color': color } as any}>
+              {/* <span class={styles.line}></span> */}
+              {props.type === 'NUM' ? (
+                <>
+                  <span class={styles.text}>
+                    {statisticCounts.value.time} 练习人数
+                  </span>
+                  <span class={styles.num}>
+                    {statisticCounts.value.count || 0}人
+                  </span>
+                </>
+              ) : (
+                <>
+                  <span class={styles.text}>
+                    {statisticCounts.value.time} 练习时长
+                  </span>
+                  <span class={styles.num}>{statisticCounts.value.count}</span>
+                </>
+              )}
+            </div>
+          </div>
+        </div>
+
+        <div class={styles.eChart}>
+          <div id={chartId} style="width: 100%; height: 100%;"></div>
+        </div>
+      </div>
+    )
+  }
+})

+ 389 - 0
src/teacher/statistics/practice-statistics-detail/index.module.less

@@ -0,0 +1,389 @@
+.practiceDetail {
+  min-height: 100vh;
+  background-color: #f6f7f8;
+  overflow: hidden;
+  background: linear-gradient(to bottom, #beffe6 0px, #f6f7f8 392px);
+}
+
+.groupContainer {
+  height: calc(100vh - var(--header-height, 0));
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.section {
+  background: #ffffff;
+  box-shadow: 0px 2px 10px 0px rgba(229, 229, 229, 0.1);
+  border-radius: 10px;
+  overflow: hidden;
+  padding: 12px;
+  margin: 12px 14px 0;
+  position: relative;
+  &:last-child {
+    margin-bottom: 30px;
+  }
+
+  .filter {
+    position: absolute;
+    top: 0;
+    right: 0;
+    background: url('../images/filter-bg.png') no-repeat center;
+    background-size: contain;
+    width: 114.5px;
+    height: 37px;
+    display: flex;
+    align-items: center;
+    font-size: 14px;
+    color: #131415;
+    line-height: 20px;
+    &.active {
+      color: #2dc7aa;
+    }
+    span {
+      padding-left: 49px;
+    }
+    img {
+      width: 9px;
+      height: 5px;
+      margin-left: 4px;
+    }
+  }
+
+  .title {
+    display: flex;
+    justify-content: space-between;
+    span {
+      display: flex;
+      align-items: center;
+      font-weight: 600;
+      font-size: 15px;
+      color: #333333;
+      line-height: 18px;
+      &::before {
+        content: '';
+        display: inline-block;
+        width: 3px;
+        height: 12px;
+        background: linear-gradient(180deg, #59e5d4 0%, #2dc7aa 100%);
+        border-radius: 2px;
+        margin-right: 4px;
+      }
+    }
+
+    .download {
+      display: flex;
+      align-items: center;
+      background: rgba(45, 199, 170, 0.1);
+      border-radius: 9px;
+      padding: 0 6px;
+      font-size: 12px;
+      color: #2dc7aa;
+      line-height: 17px;
+      img {
+        width: 10px;
+        height: 10px;
+        margin-left: 4px;
+      }
+    }
+  }
+  .leaveTime {
+    padding-top: 20px;
+    padding-bottom: 20px;
+    .num {
+      font-family: DIN;
+      font-weight: bold;
+      font-size: 30px;
+      color: #333333;
+      line-height: 35px;
+    }
+    .text {
+      font-weight: 400;
+      font-size: 14px;
+      color: #777777;
+      line-height: 26px;
+      padding: 0 2px;
+    }
+  }
+
+  .sList {
+    display: flex;
+  }
+  .sItem {
+    background: #f8f8f8;
+    box-shadow: 0px 2px 10px 0px rgba(229, 229, 229, 0.1);
+    border-radius: 4px;
+    padding: 10px;
+    flex: 1;
+    &:last-child {
+      margin-left: 10px;
+    }
+    .sTop {
+      display: flex;
+      align-items: center;
+      font-weight: 600;
+      font-size: 14px;
+      color: #2dc7aa;
+      line-height: 18px;
+      padding-bottom: 5px;
+      img {
+        margin-right: 6px;
+        width: 16px;
+        height: 16px;
+      }
+    }
+    .sBottom {
+      .num {
+        font-family: DIN;
+        font-weight: bold;
+        font-size: 22px;
+        color: #333333;
+        line-height: 26px;
+      }
+      .text {
+        font-size: 12px;
+        color: #777777;
+        line-height: 26px;
+      }
+    }
+  }
+}
+
+.scroll {
+  overflow-y: hidden;
+  overflow-x: auto;
+  position: relative;
+  z-index: auto;
+  height: 100%;
+  width: 100%;
+  padding-top: 12px;
+  &::-webkit-scrollbar {
+    display: none;
+  }
+}
+
+.dataTable {
+  word-break: break-word;
+  // transition: background-color .3s var(--n-bezier);
+  border-collapse: separate;
+  border-spacing: 0;
+
+  .tdFixedLeft {
+    position: sticky;
+    z-index: 1;
+    left: 0;
+  }
+
+  th {
+    font-weight: 600;
+    line-height: 26px;
+    margin-right: 2px;
+    font-weight: 600;
+    font-size: 12px;
+    &:last-child {
+      margin-right: 0;
+    }
+    &:first-child {
+      padding-left: 6px;
+      padding-right: 6px;
+      text-align: left;
+      background: #f8f8f8;
+    }
+    &:nth-child(5n),
+    &:nth-child(3n) {
+      background: #f8f1e9;
+      color: #df8010;
+    }
+    &:nth-child(2n),
+    &:nth-child(4n) {
+      background: #e8f4f4;
+      color: #17bda6;
+    }
+  }
+
+  td {
+    line-height: 34px;
+    height: 34px;
+    text-align: center;
+    background: #fff;
+    margin-right: 2px;
+    font-weight: bold;
+    font-family: DIN;
+    &:last-child {
+      margin-right: 0;
+    }
+    &:first-child {
+      padding-left: 6px;
+      padding-right: 6px;
+      text-align: left;
+      display: flex;
+      align-items: center;
+      font-weight: 400;
+      span {
+        font-size: 13px;
+        color: #333333;
+        line-height: 20px;
+        max-width: 60px;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
+      }
+    }
+    &:nth-child(4n) {
+      color: #17bda6;
+    }
+    &:nth-child(3n),
+    &:nth-child(5n) {
+      color: #df8010;
+    }
+    .userImg {
+      width: 16px;
+      height: 16px;
+      border-radius: 50%;
+      margin-right: 4px;
+      flex-shrink: 0;
+    }
+  }
+}
+
+.popupContainer {
+  // max-height: 504px;
+  // overflow-x: hidden;
+  // overflow-y: auto;
+  .popupTitle {
+    position: sticky;
+    z-index: 1;
+    top: 0;
+    text-align: center;
+    font-weight: 600;
+    font-size: 18px;
+    color: #333333;
+    line-height: 24px;
+    padding: 18px 0 12px;
+  }
+
+  .popupSearchList {
+    min-height: 30vh;
+    max-height: 50vh;
+    overflow: hidden auto;
+  }
+
+  .popupSection {
+    padding: 0 16px 18px;
+    .title {
+      display: flex;
+      justify-content: space-between;
+      padding-bottom: 10px;
+      span {
+        display: flex;
+        align-items: center;
+        font-weight: 600;
+        font-size: 15px;
+        color: #333333;
+        line-height: 18px;
+        &::before {
+          content: '';
+          display: inline-block;
+          width: 3px;
+          height: 12px;
+          background: linear-gradient(180deg, #59e5d4 0%, #2dc7aa 100%);
+          border-radius: 2px;
+          margin-right: 4px;
+        }
+      }
+    }
+
+    .timeCount {
+      display: flex;
+      align-items: center;
+
+      p {
+        margin-left: 10px;
+        flex: 1;
+        background: #f8f8f8;
+        border: 1px solid #f8f8f8;
+        border-radius: 4px;
+        font-size: 13px;
+        color: #999999;
+        line-height: 18px;
+        text-align: center;
+        padding: 6px 0;
+        &:first-child {
+          margin-left: 0;
+        }
+
+        &.active {
+          background: #e9fff8;
+          border-radius: 4px;
+          border: 1px solid #2dc7aa;
+          color: #2dc7aa;
+        }
+      }
+    }
+
+    .timeSubject {
+      flex-wrap: wrap;
+      margin-left: -5px;
+      margin-right: -5px;
+      p {
+        width: calc(33.333% - 10px);
+        padding: 6px 3px;
+        margin: 0 5px;
+        flex: none;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        margin-bottom: 9px;
+        box-sizing: border-box;
+        &:first-child {
+          margin-left: 5px;
+        }
+
+      }
+    }
+
+    .timeRang {
+      margin-top: 10px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .timeInput {
+        width: 159px;
+        line-height: 32px;
+        text-align: center;
+        background: #f8f8f8;
+        border-radius: 4px;
+        font-size: 13px;
+        color: #999999;
+        cursor: pointer;
+      }
+      .hasValue {
+        color: #333;
+      }
+
+      .timeUnit {
+        width: 12px;
+        height: 1px;
+        background: #d0d0d0;
+      }
+    }
+  }
+
+  .popupBottom {
+    position: sticky;
+    z-index: 1;
+    bottom: 0;
+    border-top: 1px solid #f2f2f2;
+    padding: 20px 13px 30px;
+    display: flex;
+    align-items: center;
+    :global {
+      .van-button {
+        font-size: 16px;
+      }
+      .van-button + .van-button {
+        margin-left: 15px;
+      }
+    }
+  }
+}

+ 568 - 0
src/teacher/statistics/practice-statistics-detail/index.tsx

@@ -0,0 +1,568 @@
+import { defineComponent, reactive, ref, shallowReactive } from 'vue'
+import styles from './index.module.less'
+import iconArrow1 from '../images/icon-arrow1.png'
+import iconArrow11 from '../images/icon-arrow1-1.png'
+import icon1 from '../images/icon-1.png'
+import icon2 from '../images/icon-2.png'
+import iconDownload from '../images/icon-download.png'
+import { Button, DatetimePicker, Popup } from 'vant'
+import Echats from './echats'
+import ColHeader from '@/components/col-header'
+import TheSticky from '@/components/the-sticky'
+import { formatterDatePicker } from '@/helpers/utils'
+import dayjs from 'dayjs'
+import request from '@/helpers/request'
+import { getTimeRange, TIME_TYPE } from '../home-statistics'
+import ColResult from '@/components/col-result'
+import { promisefiyPostMessage } from '@/helpers/native-message'
+
+/** 秒转分 */
+export const formatSecToMin = (second: number) => {
+  if (isNaN(second)) {
+    return '0'
+  }
+  const mm = (Math.floor(second / 60) + Math.floor(second % 60) / 60).toFixed(2)
+  return mm
+}
+
+/** 秒转时分秒 */
+export const formatSecToHMS = second => {
+  const hours = Math.floor(second / 3600)
+    .toString()
+    .padStart(2, '0')
+  const minutes = Math.floor((second % 3600) / 60)
+    .toString()
+    .padStart(2, '0')
+  const seconds = Math.round(second % 60)
+    .toString()
+    .padStart(2, '0')
+  return {
+    all: hours + '时' + minutes + '分' + seconds + '秒',
+    hours,
+    minutes,
+    seconds
+  }
+}
+
+export default defineComponent({
+  name: 'PracticeDetail',
+  setup() {
+    const searchStatus = ref(false)
+    const currentType = ref<TIME_TYPE>('MONTH')
+
+    const searchObj = reactive({
+      tempSubjectId: '' as any,
+      type: 'MONTH' as TIME_TYPE
+    })
+    const timeRange = getTimeRange(currentType.value)
+    const forms = reactive({
+      loading: false,
+      dataShow: true,
+      subjectId: '' as any, // 选择的声部
+      subjectList: [] as any,
+      startTimeStatus: false,
+      endTimeMinDate: new Date(),
+      endTimeMaxDate: dayjs(new Date()).add(1, 'year').toDate(),
+      endTimeStatus: false,
+      startTime: new Date(timeRange?.startTime || ''),
+      startTimeStr: timeRange?.startTime || '',
+      endTime: new Date(timeRange?.endTime || ''),
+      endTimeStr: timeRange?.endTime || ''
+    })
+
+    // 练习统计
+    const practiceSummary = shallowReactive({
+      averagePracticeTime: '0',
+      practiceCount: '0',
+      totalPracticeTime: '0',
+      totalTimes: {
+        hours: '00',
+        minutes: '00',
+        seconds: '00'
+      }
+    })
+    const obj = ref({
+      students: [] as any,
+      xAxisDataTime: [] as any,
+      yAxisDataTime: [] as any,
+      timeCount: 0,
+      timeStr: '',
+      xAxisDataCount: [] as any,
+      yAxisDataCount: [] as any,
+      countCount: 0,
+      countStr: ''
+    })
+
+    // const searchText = computed(() => {
+    //       const template = {
+    //         MONTH: '本月',
+    //         THREE_MONTH: '近三个月',
+    //         HALF_YEAR: '近年半',
+    //         YEAR: '近一年'
+    //       }
+    //       return template[currentType.value]
+    //     })
+
+    // 导出学生练习时长数据
+    const onExport = async () => {
+      try {
+        const { data } = await request.post(
+          '/api-teacher/home/exportStudentPractice'
+        )
+        console.log(data, 'data')
+
+        // promisefiyPostMessage({
+        //   api: 'downloadFile',
+        //   content: {
+        //     downloadUrl: staffData.musicPdfUrl,
+        //     fileName: songName
+        //   }
+        // })
+      } catch {
+        //
+      }
+    }
+
+    const getDetail = async () => {
+      forms.loading = true
+      try {
+        const { data } = await request.post('/api-teacher/home/practice', {
+          data: {
+            startTime: forms.startTimeStr,
+            endTime: forms.endTimeStr,
+            subjectId: forms.subjectId
+          }
+        })
+        const summary = data.practiceSummary || {}
+        practiceSummary.averagePracticeTime = formatSecToMin(
+          summary.averagePracticeTime || 0
+        )
+        practiceSummary.practiceCount = summary.practiceCount || 0
+        practiceSummary.totalPracticeTime = summary.totalPracticeTime || 0
+        practiceSummary.totalTimes = formatSecToHMS(
+          summary.totalPracticeTime || 0
+        )
+
+        // 练习时长
+        const practiceTimes = data.practiceTimes || []
+        const xAxisDataTimes: string[] = []
+        const practiceTimeList: number[] = []
+        practiceTimes.forEach((item: any, index: number) => {
+          xAxisDataTimes.push(item.date)
+          practiceTimeList.push(item.practiceTime)
+
+          if (practiceTimes.length - 1 === index) {
+            obj.value.timeCount = item.practiceTime
+            obj.value.timeStr = item.date
+          }
+        })
+
+        // 练习人数
+        const practiceCounts = data.practiceCounts || []
+        const xAxisDataCounts: string[] = []
+        const countList: number[] = []
+        practiceCounts.forEach((item: any, index: number) => {
+          xAxisDataCounts.push(item.date)
+          countList.push(item.practiceTime)
+
+          if (practiceCounts.length - 1 === index) {
+            obj.value.countCount = item.practiceTime
+            obj.value.countStr = item.date
+          }
+        })
+
+        obj.value.xAxisDataTime = xAxisDataTimes
+        obj.value.yAxisDataTime = practiceTimeList
+        obj.value.xAxisDataCount = xAxisDataCounts
+        obj.value.yAxisDataCount = countList
+
+        // 学员练习时长
+        const studentPracticeSummary = data.studentPracticeSummary || []
+        let tempStudents: any = []
+        studentPracticeSummary.forEach((item: any) => {
+          const student = {
+            avatar: item.avatar,
+            averagePracticeTime: formatSecToHMS(item.averagePracticeTime || 0),
+            practiceDays: item.practiceDays || 0,
+            studentName: item.studentName,
+            subjectName: item.subjectName,
+            totalPracticeTime: formatSecToHMS(item.totalPracticeTime || 0)
+          }
+          tempStudents = student
+        })
+        obj.value.students = tempStudents
+
+        forms.dataShow = tempStudents.length > 0 ? true : false
+      } catch {
+        //
+      }
+      forms.loading = false
+    }
+
+    const getSubjectList = async () => {
+      const { data } = await request.get(
+        `api-teacher/subject/subjectSelect?type=MUSIC&clientId=TEACHER`
+      )
+      if (Array.isArray(data)) {
+        // 初始化乐器编号
+        data.forEach((item: any) => {
+          if (Array.isArray(item.subjects)) {
+            item.subjects.forEach((child: any) => {
+              forms.subjectList.push(child)
+            })
+          }
+        })
+      }
+    }
+
+    getSubjectList()
+    getDetail()
+
+    const onChangeTime = (type: TIME_TYPE) => {
+      if (searchObj.type === type) return
+      searchObj.type = type
+
+      resetTime(type)
+    }
+    // 格式化
+    const resetTime = (type: TIME_TYPE) => {
+      const timeRang = getTimeRange(type)
+
+      forms.startTime = new Date(timeRang?.startTime || '')
+      forms.startTimeStr = timeRang?.startTime || ''
+      forms.endTimeMinDate = dayjs(timeRang?.startTime || '').toDate()
+      forms.endTimeMaxDate = dayjs(timeRang?.startTime || '')
+        .add(1, 'year')
+        .toDate()
+      forms.endTime = new Date(timeRang?.endTime || '')
+      forms.endTimeStr = timeRang?.endTime || ''
+    }
+
+    // 重置
+    const onConfirm = () => {
+      // timeRange.value = getTimeRange(currentType.value)
+      searchStatus.value = false
+      forms.subjectId = searchObj.tempSubjectId
+      getDetail()
+    }
+    return () => (
+      <div class={styles.practiceDetail}>
+        <TheSticky position="top">
+          <ColHeader background="transparent" border={false} />
+        </TheSticky>
+        <div class={styles.groupContainer}>
+          <div class={styles.section}>
+            <div
+              class={[styles.filter, searchStatus.value && styles.active]}
+              onClick={() => (searchStatus.value = true)}
+            >
+              <span>筛选</span>
+              <img src={searchStatus.value ? iconArrow11 : iconArrow1} />
+            </div>
+            <div class={styles.title}>
+              <span>练习详情</span>
+            </div>
+
+            <div class={styles.leaveTime}>
+              <span class={styles.num}>{practiceSummary.totalTimes.hours}</span>
+              <span class={styles.text}>时</span>
+              <span class={styles.num}>
+                {practiceSummary.totalTimes.minutes}
+              </span>
+              <span class={styles.text}>分</span>
+              <span class={styles.num}>
+                {practiceSummary.totalTimes.seconds}
+              </span>
+              <span class={styles.text}>秒</span>
+            </div>
+
+            <div class={styles.sList}>
+              <div class={styles.sItem}>
+                <div class={styles.sTop}>
+                  <img src={icon2} />
+                  <span>练习人数</span>
+                </div>
+                <div class={styles.sBottom}>
+                  <span class={styles.num}>
+                    {practiceSummary.practiceCount}
+                  </span>
+                  <span class={styles.text}>人</span>
+                </div>
+              </div>
+              <div class={styles.sItem}>
+                <div class={styles.sTop}>
+                  <img src={icon1} />
+                  <span>平均练习时长</span>
+                </div>
+                <div class={styles.sBottom}>
+                  <span class={styles.num}>
+                    {practiceSummary.averagePracticeTime}
+                  </span>
+                  <span class={styles.text}>分钟</span>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div class={styles.section}>
+            <div class={styles.title}>
+              <span>练习时长</span>
+            </div>
+
+            <Echats
+              obj={{
+                xAxisData: obj.value.xAxisDataTime,
+                yAxisData: obj.value.yAxisDataTime,
+                count: obj.value.timeCount,
+                time: obj.value.timeStr
+              }}
+            />
+          </div>
+
+          <div class={styles.section}>
+            <div class={styles.title}>
+              <span>练习人数</span>
+            </div>
+
+            <Echats
+              type="NUM"
+              obj={{
+                xAxisData: obj.value.xAxisDataCount,
+                yAxisData: obj.value.yAxisDataCount,
+                count: obj.value.countCount,
+                time: obj.value.countStr
+              }}
+            />
+          </div>
+
+          <div class={styles.section}>
+            <div class={styles.title}>
+              <span>学员练习时长</span>
+              <div class={styles.download} onClick={onExport}>
+                <div>导出</div>
+                <img src={iconDownload} />
+              </div>
+            </div>
+
+            <div class={styles.scroll}>
+              {forms.dataShow ? (
+                <table class={styles.dataTable} style={{ width: '486px' }}>
+                  <colgroup>
+                    <col style="width: 88px;" />
+                    <col style="width: 105px;" />
+                    <col style="width: 106px;" />
+                    <col style="width: 72px;" />
+                    <col style="width: 106px;" />
+                  </colgroup>
+                  <thead>
+                    <tr>
+                      <th class={styles.tdFixedLeft}>学员</th>
+                      <th>乐器</th>
+                      <th>
+                        <div>练习时长</div>
+                        {/* <div class={styles.filters}>
+                    </div> */}
+                      </th>
+                      <th>练习天数</th>
+                      <th>平均练习时长</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    {obj.value.students.map((item: any) => (
+                      <tr>
+                        <td class={styles.tdFixedLeft}>
+                          <img class={styles.userImg} src={item.avatar} />
+                          <span>{item.studentName}</span>
+                        </td>
+                        <td>{item.subjectName}</td>
+                        <td>
+                          {item.totalPracticeTime.hours}小时
+                          {item.totalPracticeTime.minutes}分
+                          {item.totalPracticeTime.seconds}秒
+                        </td>
+                        <td>{item.practiceDays}</td>
+                        <td>
+                          {item.averagePracticeTime.hours}小时
+                          {item.averagePracticeTime.minutes}分
+                          {item.averagePracticeTime.seconds}秒
+                        </td>
+                      </tr>
+                    ))}
+                  </tbody>
+                </table>
+              ) : (
+                <ColResult
+                  classImgSize="SMALL"
+                  btnStatus={false}
+                  tips="暂无数据~"
+                />
+              )}
+            </div>
+          </div>
+        </div>
+
+        <Popup
+          v-model:show={searchStatus.value}
+          closeable
+          round
+          position="bottom"
+        >
+          <div class={styles.popupContainer}>
+            <div class={styles.popupTitle}>筛选</div>
+
+            <div class={styles.popupSearchList}>
+              <div class={styles.popupSection}>
+                <div class={styles.title}>
+                  <span>学员练习时长</span>
+                </div>
+
+                <div class={styles.timeCount}>
+                  <p
+                    onClick={() => onChangeTime('MONTH')}
+                    class={searchObj.type === 'MONTH' ? styles.active : ''}
+                  >
+                    本月
+                  </p>
+                  <p
+                    onClick={() => onChangeTime('THREE_MONTH')}
+                    class={
+                      searchObj.type === 'THREE_MONTH' ? styles.active : ''
+                    }
+                  >
+                    近三个月
+                  </p>
+                  <p
+                    onClick={() => onChangeTime('HALF_YEAR')}
+                    class={searchObj.type === 'HALF_YEAR' ? styles.active : ''}
+                  >
+                    近半年
+                  </p>
+                  <p
+                    onClick={() => onChangeTime('YEAR')}
+                    class={searchObj.type === 'YEAR' ? styles.active : ''}
+                  >
+                    近一年
+                  </p>
+                </div>
+
+                <div class={styles.timeRang}>
+                  <p
+                    class={[
+                      styles.timeInput,
+                      forms.startTimeStr && styles.hasValue
+                    ]}
+                    onClick={() => (forms.startTimeStatus = true)}
+                  >
+                    {forms.startTimeStr || '起始时间'}
+                  </p>
+                  <p class={styles.timeUnit}></p>
+                  <p
+                    class={[
+                      styles.timeInput,
+                      forms.endTimeStr && styles.hasValue
+                    ]}
+                    onClick={() => (forms.endTimeStatus = true)}
+                  >
+                    {forms.endTimeStr || '终止时间'}
+                  </p>
+                </div>
+              </div>
+
+              <div class={styles.popupSection}>
+                <div class={styles.title}>
+                  <span>声部</span>
+                </div>
+
+                <div class={[styles.timeCount, styles.timeSubject]}>
+                  <p
+                    class={searchObj.tempSubjectId === '' ? styles.active : ''}
+                    onClick={() => (searchObj.tempSubjectId = '')}
+                  >
+                    全部
+                  </p>
+                  {forms.subjectList.map((item: any) => (
+                    <p
+                      class={
+                        searchObj.tempSubjectId === item.id ? styles.active : ''
+                      }
+                      onClick={() => {
+                        searchObj.tempSubjectId = item.id
+                      }}
+                    >
+                      {item.name}
+                    </p>
+                  ))}
+                </div>
+              </div>
+            </div>
+
+            <div class={styles.popupBottom}>
+              <Button
+                round
+                block
+                type="default"
+                onClick={() => {
+                  searchObj.tempSubjectId = ''
+                  searchObj.type = 'MONTH'
+                  resetTime('MONTH')
+                }}
+              >
+                重置
+              </Button>
+              <Button round block type="primary" onClick={onConfirm}>
+                确定
+              </Button>
+            </div>
+          </div>
+        </Popup>
+
+        {/* 开始日期 */}
+        <Popup
+          v-model:show={forms.startTimeStatus}
+          position="bottom"
+          round
+          class={'popupBottomSearch'}
+        >
+          <DatetimePicker
+            v-model={forms.startTime}
+            type="date"
+            formatter={formatterDatePicker}
+            onCancel={() => (forms.startTimeStatus = false)}
+            onConfirm={(val: any) => {
+              forms.startTime = val
+              forms.startTimeStr = dayjs(val).format('YYYY-MM-DD')
+              forms.startTimeStatus = false
+              forms.endTime = null as any
+              forms.endTimeStr = ''
+              forms.endTimeMinDate = dayjs(val || new Date()).toDate()
+              forms.endTimeMaxDate = dayjs(val || new Date())
+                .add(1, 'year')
+                .toDate()
+            }}
+          />
+        </Popup>
+        {/* 结束日期 */}
+        <Popup
+          v-model:show={forms.endTimeStatus}
+          position="bottom"
+          round
+          class={'popupBottomSearch'}
+        >
+          <DatetimePicker
+            v-model={forms.endTime}
+            type="date"
+            minDate={forms.endTimeMinDate}
+            maxDate={forms.endTimeMaxDate}
+            formatter={formatterDatePicker}
+            onCancel={() => (forms.endTimeStatus = false)}
+            onConfirm={(val: any) => {
+              forms.endTime = val
+              forms.endTimeStatus = false
+              forms.endTimeStr = dayjs(val).format('YYYY-MM-DD')
+            }}
+          />
+        </Popup>
+      </div>
+    )
+  }
+})

+ 10 - 10
src/tenant/music/coursewarePlay/index.tsx

@@ -207,17 +207,17 @@ export default defineComponent({
       for (let j = 0; j < materialList.length; j++) {
         const material = materialList[j]
         //请求本地缓存
-        // if (browserInfo.isApp && ['VIDEO', 'IMG'].includes(material.typeCode)) {
-        //   const localData: any = await getCacheFilePath(material)
+        if (browserInfo.isApp && ['VIDEO', 'IMG'].includes(material.typeCode)) {
+          const localData: any = await getCacheFilePath(material)
 
-        //   if (localData?.content?.localPath) {
-        //     material.url = material.content
-        //     material.content = localData.content.localPath
-        //   } else {
-        //     material.url = material.content + '?t=' + +new Date()
-        //     material.content = material.content + '?t=' + +new Date()
-        //   }
-        // }
+          if (localData?.content?.localPath) {
+            material.url = material.content
+            material.content = localData.content.localPath
+          } else {
+            material.url = material.content + '?t=' + +new Date()
+            material.content = material.content + '?t=' + +new Date()
+          }
+        }
 
         material.iframeRef = null
         material.videoEle = null

+ 77 - 4
src/views/music/album-detail/index.module.less

@@ -5,11 +5,18 @@
 
 .detail {
   overflow: hidden;
-
+  min-height: 100vh;
+  background-color: #fff;
   --van-nav-bar-background-color: transparent;
   --van-nav-bar-icon-color: #fff;
   --van-nav-bar-text-color: #fff;
   --van-nav-bar-title-text-color: #fff;
+
+  :global {
+    .btnMore {
+      justify-content: space-between;
+    }
+  }
 }
 
 .base {
@@ -193,7 +200,7 @@
     font-size: 18px;
     font-weight: 500;
     color: #fff;
-    padding-bottom: 8px;
+    padding-bottom: 6px;
   }
 
   .alumDes {
@@ -206,7 +213,7 @@
 }
 
 .tags {
-  margin: 6px -2px 22px -2px;
+  margin: 0 0 12px 0;
 
   .tag {
     margin: 0 2px;
@@ -214,6 +221,18 @@
     color: #000;
     background-color: rgba(113, 138, 147, 1);
     border-radius: 20px;
+    display: inline-block;
+  }
+}
+
+.btnGroup {
+  padding: 10px 28px 30px;
+  background-color: #fff;
+  box-shadow: 0px 0px 2px 0px rgba(216,216,216,0.5);
+  :global {
+    .van-button {
+      font-size: 16px;
+    }
   }
 }
 
@@ -263,7 +282,7 @@
 
 .alumnContainer {
   position: relative;
-  padding: 0 16px;
+  padding: 0;
   // margin-top: -16px;
   z-index: 12;
 
@@ -272,6 +291,60 @@
     border-radius: 18px;
     background-color: #fff;
     margin-bottom: 16px;
+    min-height: 300px;
+  }
+
+  .searchSection {
+    :global {
+      .van-search {
+        padding-left: 0;
+        padding-right: 0;
+      }
+
+      .van-dropdown-menu__bar {
+        box-shadow: none;
+        padding-right: 12px;
+      }
+      .van-dropdown-menu__title {
+        padding-left: 0;
+      }
+      .van-dropdown-item {
+        --van-dropdown-item-z-index: 100;
+      }
+
+      .van-dropdown-item__content {
+        --van-dropdown-menu-content-max-height: 322px;
+        border-bottom-left-radius: 20px;
+        border-bottom-right-radius: 20px;
+        margin-top: -1px;
+      }
+      .van-overlay {
+        top: 1px;
+      }
+    }
+
+    // .label {
+    //   margin-right: 8px;
+    //   font-size: 14px;
+    //   color: #131415;
+
+    //   &.labelActive {
+    //     color: #2DC7AA;
+    //     :global {
+    //       .iconfont-down {
+    //         transform: rotate(180deg);
+    //         color: #2DC7AA;
+    //       }
+    //     }
+    //   }
+    //   :global {
+    //     .iconfont-down {
+    //       font-size: 12px;
+    //       margin-left: 4px;
+    //       color: #999;
+    //     }
+    //   }
+    // }
   }
 
   .subjectSearch {

+ 165 - 76
src/views/music/album-detail/index.tsx

@@ -10,7 +10,18 @@ import { useRoute, useRouter } from 'vue-router'
 import request from '@/helpers/request'
 import ColHeader from '@/components/col-header'
 import { postMessage } from '@/helpers/native-message'
-import { Button, Dialog, Icon, Image, List, NavBar, Popup, Sticky } from 'vant'
+import {
+  Button,
+  Dialog,
+  DropdownItem,
+  DropdownMenu,
+  Icon,
+  Image,
+  List,
+  NavBar,
+  Popup,
+  Sticky
+} from 'vant'
 // import classNames from 'classnames'
 // import Footer from '../album/footer'
 // import FavoriteIcon from '../album/favorite.svg'
@@ -24,21 +35,24 @@ import { openDefaultWebView, state } from '@/state'
 import IconPan from './pan.png'
 import oStart from './oStart.png'
 import iStart from './iStart.png'
-import Title from '../component/title'
+// import Title from '../component/title'
 import Song from '../component/song'
 import ColResult from '@/components/col-result'
-import MusicGrid from '../component/music-grid'
+// import MusicGrid from '../component/music-grid'
 import { useEventTracking } from '@/helpers/hooks'
-import ColSticky from '@/components/col-sticky'
+// import ColSticky from '@/components/col-sticky'
 import { moneyFormat } from '@/helpers/utils'
 import { orderStatus } from '@/views/order-detail/orderStatus'
 import iconShare from '../album/icon_share.svg'
 import iconShare2 from '../album/icon_share2.svg'
 import ColShare from '@/components/col-share'
-import iconShareMusic from '/src/views/music/component/images/icon_album_active.png'
+// import iconShareMusic from '/src/views/music/component/images/icon_album_active.png'
 import SongShare from '../component/song-share'
 import icon_music_list from './icon_music_list.png'
 import SelectSubject from '../search/select-subject'
+import TheSticky from '@/components/the-sticky'
+import ColSearch from '@/components/col-search'
+import SearchGroup from './search-group'
 
 const noop = () => {}
 
@@ -56,17 +70,20 @@ export default defineComponent({
     const route = useRoute()
     const params = reactive({
       search: '',
-      relatedNum: 6, //相关专辑数
+      // relatedNum: 6, //相关专辑数
+      musicSheetName: '',
+      albumCategoryLevelId: null,
+      albumCategoryTypeId: null,
       page: 1,
-      rows: 200
+      rows: 20
     })
+    const title = ref(' ')
     const albumDetail = ref<any>(null)
-    // const data = ref<any>(null)
     const rows = ref<any[]>([])
     const loading = ref(false)
     const aId = Number(route.query.activityId) || 0
     const studentActivityId = ref(aId)
-    // const finished = ref(false)
+    const finished = ref(false)
     const isError = ref(false)
     const favorited = ref(0)
     const albumFavoriteCount = ref(0)
@@ -74,18 +91,19 @@ export default defineComponent({
     const background = ref<string>('rgba(55, 205, 177, 0)')
     const color = ref<string>('#fff')
     const heightInfo = ref<any>('auto')
+
+    const dropdownItemRef = ref()
+
     const subjects = reactive({
       show: false,
       name: route.query.subjectName || '全部声部',
       id: route.query.subjectId || null
     })
 
-    const FetchList = async (id?: any) => {
-      if (loading.value) {
-        return
-      }
-      loading.value = true
-      isError.value = false
+    const albumLevel = ref([])
+    const albumType = ref([])
+
+    const FetchDetail = async (id?: any) => {
       try {
         const res = await request.post('/music/album/detail', {
           prefix:
@@ -93,11 +111,13 @@ export default defineComponent({
           data: {
             id: id || route.params.id,
             ...params,
+            queryMusicSheet: false,
+            queryCategory: true,
+            queryRelatedAlbum: false,
             subjectIds: subjects.id
           }
         })
-        const { musicSheetList, ...rest } = res.data
-        rows.value = [...musicSheetList.rows]
+        const { albumLevelList, albumTypeList, ...rest } = res.data
         const musicTagNames = rest?.musicTagNames
           ? rest?.musicTagNames?.split(',')
           : []
@@ -105,8 +125,36 @@ export default defineComponent({
           ...rest,
           musicTagNames
         }
+        albumLevel.value = albumLevelList || []
+        albumType.value = albumTypeList || []
+
         favorited.value = rest.favorite
         albumFavoriteCount.value = rest.albumFavoriteCount
+      } catch {
+        //
+      }
+    }
+
+    const FetchList = async (id?: any) => {
+      if (loading.value) {
+        return
+      }
+      loading.value = true
+      isError.value = false
+      try {
+        const { data } = await request.post('/music/album/musicPage', {
+          prefix:
+            state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student',
+          data: {
+            id: id || route.params.id,
+            ...params,
+            subjectIds: subjects.id
+          }
+        })
+        const result = (rows.value || []).concat(data.rows || [])
+        rows.value = result
+        params.page = data.pageNo + 1
+        finished.value = data.pageNo >= data.totalPage
       } catch (error) {
         isError.value = true
       }
@@ -115,30 +163,23 @@ export default defineComponent({
 
     const favoriteLoading = ref(false)
 
-    onMounted(() => {
-      FetchList()
-      useEventListener(document, 'scroll', evt => {
-        const { y } = useWindowScroll()
-        if (y.value > 20) {
-          background.value = `rgba(255, 255, 255)`
-          color.value = 'black'
-          // postMessage({
-          //   api: 'backIconChange',
-          //   content: { iconStyle: 'black' }
-          // })
-        } else {
-          background.value = 'transparent'
-          color.value = '#fff'
-          // postMessage({
-          //   api: 'backIconChange',
-          //   content: { iconStyle: 'white' }
-          // })
-        }
-      })
-
-      useEventTracking('专辑')
+    FetchDetail()
+    FetchList()
+    useEventListener(document, 'scroll', () => {
+      const { y } = useWindowScroll()
+      if (y.value > 20) {
+        background.value = `rgba(255, 255, 255)`
+        color.value = 'black'
+        title.value = albumDetail.value?.albumName || ' '
+      } else {
+        background.value = 'transparent'
+        color.value = '#fff'
+        title.value = ' '
+      }
     })
 
+    useEventTracking('专辑')
+
     const toggleFavorite = async (id: number) => {
       favoriteLoading.value = true
       try {
@@ -173,7 +214,7 @@ export default defineComponent({
 
       const res = await request.post('/userOrder/getPendingOrder', {
         prefix:
-            state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student',
+          state.platformType === 'TEACHER' ? '/api-teacher' : '/api-student',
         data: {
           goodType: 'ALBUM',
           bizId: album.id
@@ -251,7 +292,6 @@ export default defineComponent({
       const userId = state.user.data.userId
       const id = route.params.id
       let activityId = 0
-      console.log(state.user, userId)
       if (state.platformType === 'TEACHER') {
         const res = await request.post('/api-teacher/open/vipProfit', {
           data: {
@@ -286,6 +326,7 @@ export default defineComponent({
             <ColHeader
               background={background.value}
               border={false}
+              title={title.value}
               color={color.value}
               backIconColor="white"
               onHeaderBack={() => {
@@ -372,7 +413,7 @@ export default defineComponent({
           </div>
           <div class={styles.alumnContainer}>
             <div class={styles.alumnList}>
-              <Title title="曲目列表" isMore={false}>
+              {/* <Title title="曲目列表" isMore={false}>
                 {{
                   right: () =>
                     albumDetail.value?.albumType === 'CONCERT' && (
@@ -387,39 +428,88 @@ export default defineComponent({
                       </div>
                     )
                 }}
-              </Title>
-              <Song
-                list={rows.value}
-                onDetail={(item: any) => {
-                  if (onItemClick === noop || !onItemClick) {
-                    const url =
-                      location.origin +
-                      location.pathname +
-                      '#/music-detail?id=' +
-                      item.id +
-                      '&albumId=' +
-                      route.params.id
-                    openDefaultWebView(url, () => {
-                      router.push({
-                        path: '/music-detail',
-                        query: {
-                          id: item.id,
-                          albumId: route.params.id
-                        }
-                      })
-                    })
-                  } else {
-                    onItemClick(item)
-                  }
+              </Title> */}
+              <ColSearch
+                class={styles.searchSection}
+                background="transparent"
+                placeholder="请输入曲目关键词"
+                onSearch={(val: string) => {
+                  params.musicSheetName = val
+                  rows.value = []
+                  params.page = 1
+                  FetchList()
+                }}
+                v-slots={{
+                  left: () =>
+                    albumLevel.value.length > 0 ||
+                    albumType.value.length > 0 ? (
+                      <DropdownMenu activeColor="#2DC7AA">
+                        <DropdownItem title="筛选" ref={dropdownItemRef}>
+                          <SearchGroup
+                            searchList={{
+                              albumLevelList: albumLevel.value,
+                              albumTypeList: albumType.value
+                            }}
+                            searchObj={{
+                              albumCategoryLevelId: params.albumCategoryLevelId,
+                              albumCategoryTypeId: params.albumCategoryTypeId
+                            }}
+                            onConfirm={(val: any) => {
+                              params.albumCategoryLevelId =
+                                val.albumCategoryLevelId
+                              params.albumCategoryTypeId =
+                                val.albumCategoryTypeId
+                              dropdownItemRef.value?.toggle()
+                              rows.value = []
+                              params.page = 1
+                              FetchList()
+                            }}
+                          />
+                        </DropdownItem>
+                      </DropdownMenu>
+                    ) : (
+                      ''
+                    )
                 }}
               />
-
-              {rows.value && rows.value.length <= 0 && (
-                <ColResult btnStatus={false} tips="暂无曲目" />
-              )}
+              <List
+                loading={loading.value}
+                finished={finished.value}
+                onLoad={FetchList}
+                error={isError.value}
+              >
+                <Song
+                  list={rows.value}
+                  onDetail={(item: any) => {
+                    if (onItemClick === noop || !onItemClick) {
+                      const url =
+                        location.origin +
+                        location.pathname +
+                        '#/music-detail?id=' +
+                        item.id +
+                        '&albumId=' +
+                        route.params.id
+                      openDefaultWebView(url, () => {
+                        router.push({
+                          path: '/music-detail',
+                          query: {
+                            id: item.id,
+                            albumId: route.params.id
+                          }
+                        })
+                      })
+                    } else {
+                      onItemClick(item)
+                    }
+                  }}
+                />
+                {rows.value && !loading.value && rows.value.length <= 0 && (
+                  <ColResult btnStatus={false} tips="暂无曲目" />
+                )}
+              </List>
             </div>
 
-            {albumDetail.value?.relatedMusicAlbum &&
+            {/* {albumDetail.value?.relatedMusicAlbum &&
               albumDetail.value?.relatedMusicAlbum.length > 0 && (
                 <>
                   <Title
@@ -448,21 +538,20 @@ export default defineComponent({
                     }}
                   />
                 </>
-              )}
+              )} */}
           </div>
 
           {/* 判断是否是收费 是否是已经购买 */}
           {albumDetail.value?.paymentType === 'CHARGE' &&
             albumDetail.value?.orderStatus !== 'PAID' && (
-              <ColSticky position="bottom" background="white">
+              <TheSticky position="bottom">
                 <div
                   class={[
-                    'btnGroup',
+                    styles.btnGroup,
                     buyVip.value &&
                       !(state.user.data.userVip?.vipType !== 'NOT_VIP') &&
                       'btnMore'
                   ]}
-                  style={{ padding: '0' }}
                 >
                   <Button
                     block
@@ -496,7 +585,7 @@ export default defineComponent({
                       </Button>
                     )}
                 </div>
-              </ColSticky>
+              </TheSticky>
             )}
 
           <Popup

+ 125 - 0
src/views/music/album-detail/search-group/index.module.less

@@ -0,0 +1,125 @@
+
+.popupContainer {
+  .popupSection {
+    padding: 0 16px 18px;
+
+    &:first-child {
+      padding-top: 10px;
+    }
+    .title {
+      display: flex;
+      justify-content: space-between;
+      padding-bottom: 10px;
+      span {
+        display: flex;
+        align-items: center;
+        font-weight: 600;
+        font-size: 15px;
+        color: #333333;
+        line-height: 18px;
+        &::before {
+          content: '';
+          display: inline-block;
+          width: 3px;
+          height: 12px;
+          background: linear-gradient(180deg, #59e5d4 0%, #2dc7aa 100%);
+          border-radius: 2px;
+          margin-right: 4px;
+        }
+      }
+    }
+
+    .timeCount {
+      display: flex;
+      align-items: center;
+
+      p {
+        margin-left: 10px;
+        flex: 1;
+        background: #f8f8f8;
+        border: 1px solid #f8f8f8;
+        border-radius: 4px;
+        font-size: 13px;
+        color: #999999;
+        line-height: 18px;
+        text-align: center;
+        padding: 6px 0;
+        &:first-child {
+          margin-left: 0;
+        }
+
+        &.active {
+          background: #e9fff8;
+          border-radius: 4px;
+          border: 1px solid #2dc7aa;
+          color: #2dc7aa;
+        }
+      }
+    }
+
+    .timeSubject {
+      flex-wrap: wrap;
+      margin-left: -5px;
+      margin-right: -5px;
+      p {
+        width: calc(33.333% - 10px);
+        padding: 6px 3px;
+        margin: 0 5px;
+        flex: none;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        margin-bottom: 9px;
+        box-sizing: border-box;
+        &:first-child {
+          margin-left: 5px;
+        }
+
+      }
+    }
+
+    .timeRang {
+      margin-top: 10px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .timeInput {
+        width: 159px;
+        line-height: 32px;
+        text-align: center;
+        background: #f8f8f8;
+        border-radius: 4px;
+        font-size: 13px;
+        color: #999999;
+        cursor: pointer;
+      }
+
+      .timeUnit {
+        width: 12px;
+        height: 1px;
+        background: #d0d0d0;
+      }
+    }
+  }
+
+  .popupBottom {
+    position: sticky;
+    z-index: 1;
+    bottom: 0;
+    border-top: 1px solid #f2f2f2;
+    padding: 20px 13px 17px;
+    display: flex;
+    align-items: center;
+    background-color: #fff;
+    border-radius: 0px 0px 20px 20px;
+    :global {
+      .van-button {
+        font-size: 16px;
+      }
+      .van-button + .van-button {
+        margin-left: 15px;
+      }
+    }
+  }
+}

+ 109 - 0
src/views/music/album-detail/search-group/index.tsx

@@ -0,0 +1,109 @@
+import { defineComponent, reactive, watch } from 'vue'
+import styles from './index.module.less'
+import { Button } from 'vant'
+
+export default defineComponent({
+  name: 'searchGroup',
+  props: {
+    searchList: {
+      type: Object,
+      default: () => ({ albumLevelList: [], albumTypeList: [] })
+    },
+    searchObj: {
+      type: Object,
+      default: () => ({ albumCategoryLevelId: null, albumCategoryTypeId: null })
+    }
+  },
+  emits: ['confirm'],
+  setup(props, { emit }) {
+    const searchList = reactive({
+      albumCategoryLevelId: props.searchObj.albumCategoryLevelId,
+      albumCategoryTypeId: props.searchObj.albumCategoryTypeId
+    })
+
+    watch(
+      () => props.searchObj,
+      () => {
+        ;(searchList.albumCategoryLevelId =
+          props.searchObj.albumCategoryLevelId),
+          (searchList.albumCategoryTypeId = props.searchObj.albumCategoryTypeId)
+      }
+    )
+
+    /** 重置 */
+    const onReset = () => {
+      searchList.albumCategoryLevelId = null
+      searchList.albumCategoryTypeId = null
+    }
+    /** 确认 */
+    const onSubmit = () => {
+      emit('confirm', searchList)
+    }
+    return () => (
+      <div class={styles.popupContainer}>
+        <div class={styles.popupSearchList}>
+          {props.searchList.albumLevelList.length > 0 ? (
+            <div class={styles.popupSection}>
+              <div class={styles.title}>
+                <span>级别</span>
+              </div>
+
+              <div class={[styles.timeCount, styles.timeSubject]}>
+                {props.searchList.albumLevelList.map((item: any) => (
+                  <p
+                    class={
+                      searchList.albumCategoryLevelId === item.id &&
+                      styles.active
+                    }
+                    onClick={() => {
+                      searchList.albumCategoryLevelId = item.id
+                    }}
+                  >
+                    {item.name}
+                  </p>
+                ))}
+              </div>
+            </div>
+          ) : (
+            ''
+          )}
+
+          {props.searchList.albumTypeList.length > 0 ? (
+            <div class={styles.popupSection}>
+              <div class={styles.title}>
+                <span>类型</span>
+              </div>
+
+              <div class={[styles.timeCount, styles.timeSubject]}>
+                {props.searchList.albumTypeList.map((item: any) => (
+                  <p
+                    class={
+                      searchList.albumCategoryTypeId === item.id &&
+                      styles.active
+                    }
+                    onClick={() => {
+                      searchList.albumCategoryTypeId = item.id
+                    }}
+                  >
+                    {item.name}
+                  </p>
+                ))}
+              </div>
+            </div>
+          ) : (
+            ''
+          )}
+        </div>
+
+        <div class={styles.popupBottom}>
+          <Button round block type="default" onClick={onReset}>
+            重置
+          </Button>
+          <Button round block type="primary" onClick={onSubmit}>
+            确认
+          </Button>
+        </div>
+      </div>
+    )
+  }
+})

+ 69 - 5
src/views/music/list/index.module.less

@@ -36,7 +36,7 @@
 }
 
 .searchGroup {
-  background-color: #F8F9FC;
+  background-color: #f8f9fc;
   :global {
     .van-search {
       padding-top: 0;
@@ -56,7 +56,6 @@
   }
 
   :global {
-
     .van-list__loading,
     .van-list__finished-text,
     .van-list__error-text {
@@ -69,11 +68,76 @@
   }
 }
 
+.woringHeader {
+  display: flex;
+  align-items: center;
+  height: var(--van-nav-bar-height);
+
+  .leftArrow {
+    padding: 0 var(--k-padding-md);
+    margin-right: 0;
+    color: #fff;
+  }
+
+  .tabSection {
+    // padding: 0 32px;
+    flex: 1;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    --van-tab-font-size: 16px;
+    --van-tabs-line-height: 28px;
+    --van-tab-text-color: rgba(51, 51, 51, 0.5);
+    --van-tab-active-text-color: #333333;
+    --van-tabs-nav-background: transparent;
+    --van-padding-xs: 0;
+
+    .moreIcon {
+      display: flex;
+      align-items: center;
+      position: relative;
+      span {
+        // padding-right: 4px;
+      }
+    }
+
+    :global {
+      .van-tabs__nav {
+        background-color: transparent;
+      }
+      .van-tabs__line {
+        display: inline-block;
+        width: 30px;
+        height: 7px;
+        background: #2dc7aa;
+        border-radius: 4px;
+        bottom: 18px;
+      }
+      .van-tab {
+        font-size: 16px;
+        color: #ffffff;
+      }
+      .van-tab--active {
+        font-weight: bold;
+      }
+      .van-tab__text {
+        line-height: normal;
+      }
+
+      .van-tab--shrink {
+        padding: 0 22px;
+        z-index: 9;
+      }
+    }
+  }
+}
+
 .alumnList {
   padding: 0 12px;
-  border-radius: 18px;
+  border-radius: 10px 10px 8px 8px;
   background-color: #fff;
-  margin: 14px;
+  margin: 0 0 14px;
+  min-height: 200px;
 
   &.alumnListOnly {
     margin-top: 0;
@@ -171,4 +235,4 @@
       }
     }
   }
-}
+}

+ 154 - 102
src/views/music/list/index.tsx

@@ -1,4 +1,12 @@
-import { computed, defineComponent,  nextTick, onMounted, reactive, ref, watch } from 'vue'
+import {
+  computed,
+  defineComponent,
+  nextTick,
+  onMounted,
+  reactive,
+  ref,
+  watch
+} from 'vue'
 import { Sticky, List, Popup, Icon, Switch, Tabs, Tab } from 'vant'
 import Search from '@/components/col-search'
 import request from '@/helpers/request'
@@ -57,7 +65,15 @@ export default defineComponent({
     }
   },
   setup(
-    { hideSearch, onlySearch, height, defauleParams, onItemClick, teacherId, myself },
+    {
+      hideSearch,
+      onlySearch,
+      height,
+      defauleParams,
+      onItemClick,
+      teacherId,
+      myself
+    },
     { expose }
   ) {
     const { isLoading, state } = useAsyncState(
@@ -75,7 +91,6 @@ export default defineComponent({
       stickyHeight.value = height
     }
 
-
     const teacherDetaultSubject = ref({
       id: '',
       name: ''
@@ -91,26 +106,26 @@ export default defineComponent({
       // const subjects: any = useSubjectId(SubjectEnum.SEARCH)
       // 判断是否已有数据
       // if (!subjects.id) {
-        const users = baseState.user.data
-        // const subjectId = users.subjectId
-        //   ? Number(users.subjectId.split(',')[0])
-        //   : ''
-        // const subjectName = users.subjectName
-        //   ? users.subjectName.split(',')[0]
-        //   : ''
-        // 存储instrumentId
-        const userInstrumentId = users.instrumentId
-        localStorage.setItem('userInstrumentId', userInstrumentId)
-        // if (subjectId) {
-        //   useSubjectId(
-        //     SubjectEnum.SEARCH,
-        //     JSON.stringify({
-        //       id: subjectId,
-        //       name: subjectName
-        //     }),
-        //     'set'
-        //   )
-        // }
+      const users = baseState.user.data
+      // const subjectId = users.subjectId
+      //   ? Number(users.subjectId.split(',')[0])
+      //   : ''
+      // const subjectName = users.subjectName
+      //   ? users.subjectName.split(',')[0]
+      //   : ''
+      // 存储instrumentId
+      const userInstrumentId = users.instrumentId
+      localStorage.setItem('userInstrumentId', userInstrumentId)
+      // if (subjectId) {
+      //   useSubjectId(
+      //     SubjectEnum.SEARCH,
+      //     JSON.stringify({
+      //       id: subjectId,
+      //       name: subjectName
+      //     }),
+      //     'set'
+      //   )
+      // }
       // }
     }
 
@@ -145,8 +160,10 @@ export default defineComponent({
     //   tempParams.subjectIds = getSubject.id
     // }
     //
+
     const params = reactive({
       search: (route.query.search as string) || '',
+      musicSortType: (route.query.type as string) || "",
       // exquisiteFlag: 1,
       musicTagIds: route.query.tagids || '',
       page: 1,
@@ -241,32 +258,31 @@ export default defineComponent({
       tagVisibility.value = false
     }
     const onComfirmSubject = item => {
-    //   params.page = 1
-    //   params.subjectIds = item.id
-    //   data.value = null
-    //   subject.instrumentId = item.instrumentId
-    //   localStorage.setItem('userInstrumentId', item.instrumentId || "")
-    //   if (baseState.platformType === 'TEACHER') {
-    //     teacherDetaultSubject.value = {
-    //       name: item.name,
-    //       id: item.id
-    //     }
-    //     setDefaultSubject(item.id)
-    //   } else {
-    //     subject.id = item.id
-    //     subject.name = item.name
-    //     useSubjectId(
-    //       SubjectEnum.SEARCH,
-    //       JSON.stringify({
-    //         id: item.id,
-    //         name: item.name
-    //       }),
-    //       'set'
-    //     )
-    //   }
-
-    //   FetchList()
-    //   subject.show = false
+      //   params.page = 1
+      //   params.subjectIds = item.id
+      //   data.value = null
+      //   subject.instrumentId = item.instrumentId
+      //   localStorage.setItem('userInstrumentId', item.instrumentId || "")
+      //   if (baseState.platformType === 'TEACHER') {
+      //     teacherDetaultSubject.value = {
+      //       name: item.name,
+      //       id: item.id
+      //     }
+      //     setDefaultSubject(item.id)
+      //   } else {
+      //     subject.id = item.id
+      //     subject.name = item.name
+      //     useSubjectId(
+      //       SubjectEnum.SEARCH,
+      //       JSON.stringify({
+      //         id: item.id,
+      //         name: item.name
+      //       }),
+      //       'set'
+      //     )
+      //   }
+      //   FetchList()
+      //   subject.show = false
     }
 
     // const getSubject: any = useSubjectId(SubjectEnum.SEARCH)
@@ -320,7 +336,7 @@ export default defineComponent({
     })
 
     return () => {
-      const tagList = ((state.value && state.value.data) as any) || []
+      // const tagList = ((state.value && state.value.data) as any) || []
       return (
         <>
           {!hideSearch && (
@@ -329,33 +345,63 @@ export default defineComponent({
                 <ColHeader
                   background="transparent"
                   isFixed={false}
-                  border={false}
-                  backIconColor="white"
-                  color="#fff"
                   v-slots={{
-                    right: () =>
-                      !isAudit.value && (
-                        <span
-                          class={styles.fleg}
+                    content: () => (
+                      <div class={styles.woringHeader}>
+                        <i
                           onClick={() => {
-                            // 不要看这个字段的意思
-                            exquisiteFlag.value != exquisiteFlag.value
-                            useSubjectId(
-                              SubjectEnum.MUSIC_FREE,
-                              JSON.stringify({
-                                chargeType: exquisiteFlag.value
-                              }),
-                              'set'
-                            )
-                            data.value = null
-                            params.page = 1
-                            FetchList()
+                            if (browser().isApp) {
+                              postMessage({
+                                api: 'back'
+                              })
+                            } else {
+                              router.back()
+                            }
+                          }}
+                          class={[
+                            'van-badge__wrapper van-icon van-icon-arrow-left van-nav-bar__arrow',
+                            styles.leftArrow
+                          ]}
+                        ></i>
+                        <Tabs
+                          class={styles.tabSection}
+                          v-model:active={params.musicSortType}
+                          shrink
+                          onClick-tab={(val) => {
+                            params.musicSortType = val.name
+                            onSearch(params.search)
                           }}
                         >
-                          <Switch v-model={exquisiteFlag.value} size="20px" />
-                          <span>免费</span>
-                        </span>
-                      )
+                          <Tab name="" title="全部"></Tab>
+                          <Tab name="TOP" title="推荐"></Tab>
+                          <Tab name="HOT" title="热门"></Tab>
+                          <Tab name="NEW" title="最新"></Tab>
+                        </Tabs>
+                      </div>
+                    )
+                    // right: () =>
+                    //   !isAudit.value && (
+                    //     <span
+                    //       class={styles.fleg}
+                    //       onClick={() => {
+                    //         // 不要看这个字段的意思
+                    //         exquisiteFlag.value != exquisiteFlag.value
+                    //         useSubjectId(
+                    //           SubjectEnum.MUSIC_FREE,
+                    //           JSON.stringify({
+                    //             chargeType: exquisiteFlag.value
+                    //           }),
+                    //           'set'
+                    //         )
+                    //         data.value = null
+                    //         params.page = 1
+                    //         FetchList()
+                    //       }}
+                    //     >
+                    //       <Switch v-model={exquisiteFlag.value} size="20px" />
+                    //       <span>免费</span>
+                    //     </span>
+                    //   )
                   }}
                 />
                 <Search
@@ -383,7 +429,7 @@ export default defineComponent({
                   //   )
                   // }}
                 />
-                <Tabs
+                {/* <Tabs
                   shrink
                   class={styles.tagTabs}
                   lineHeight={0}
@@ -398,37 +444,43 @@ export default defineComponent({
                   {tagList.map((tag: any) => (
                     <Tab title={tag.name} name={tag.id}></Tab>
                   ))}
-                </Tabs>
+                </Tabs> */}
               </TheSticky>
               <img class={styles.bgImg} src={bgImg} />
             </>
           )}
-          {onlySearch ? <Sticky position='top' offsetTop={stickyHeight.value as any}><Search
-                  onSearch={onSearch}
-                  background="transparent"
-                  inputBackground='white'
-                  // leftIcon={iconSearch}
-                  class={styles.searchGroup}
-                  // v-slots={{
-                  //   left: () => (
-                  //     <div
-                  //       class={[styles.label, styles.searchs]}
-                  //       onClick={() => (subject.show = true)}
-                  //     >
-                  //       {baseState.platformType === 'TEACHER'
-                  //         ? teacherDetaultSubject.value.name
-                  //         : subject.name}
+          {onlySearch ? (
+            <Sticky position="top" offsetTop={stickyHeight.value as any}>
+              <Search
+                onSearch={onSearch}
+                background="transparent"
+                inputBackground="white"
+                // leftIcon={iconSearch}
+                class={styles.searchGroup}
+                // v-slots={{
+                //   left: () => (
+                //     <div
+                //       class={[styles.label, styles.searchs]}
+                //       onClick={() => (subject.show = true)}
+                //     >
+                //       {baseState.platformType === 'TEACHER'
+                //         ? teacherDetaultSubject.value.name
+                //         : subject.name}
 
-                  //       <Icon
-                  //         classPrefix="iconfont"
-                  //         name="down"
-                  //         size={12}
-                  //         color="#949597"
-                  //       />
-                  //     </div>
-                  //   )
-                  // }}
-                /></Sticky> : ''}
+                //       <Icon
+                //         classPrefix="iconfont"
+                //         name="down"
+                //         size={12}
+                //         color="#949597"
+                //       />
+                //     </div>
+                //   )
+                // }}
+              />
+            </Sticky>
+          ) : (
+            ''
+          )}
 
           <div class={[styles.alumnList, onlySearch && styles.alumnListOnly]}>
             <List
@@ -448,12 +500,12 @@ export default defineComponent({
                         location.pathname +
                         '#/music-detail?id=' +
                         item.id
-                        //  + '&instrumentId=' + subject.instrumentId
+                      //  + '&instrumentId=' + subject.instrumentId
                       openDefaultWebView(url, () => {
                         router.push({
                           path: '/music-detail',
                           query: {
-                            id: item.id,
+                            id: item.id
                             // instrumentId: subject.instrumentId
                           }
                         })

+ 1 - 0
src/views/music/personal/collection.tsx

@@ -22,6 +22,7 @@ export default defineComponent({
     const params = reactive({
       search: (route.query.search as string) || '',
       musicTagIds: route.query.tagids || '',
+      providerType: props.type === "TENANT" ? 'TENANT' : 'PLATFORM',
       page: 1
     })
     const rows = ref<any[]>([])

+ 1 - 1
vite.config.ts

@@ -12,7 +12,7 @@ function resolve(dir: string) {
 // https://vitejs.dev/config/
 // https://github.com/vitejs/vite/issues/1930 .env
 // const proxyUrl = 'https://online.colexiu.com/'
-const proxyUrl = 'https://test.colexiu.com/'
+const proxyUrl = 'https://dev.colexiu.com/'
 // const proxyUrl = 'http://192.168.3.14:8000/'
 export default defineConfig({
   base: './',