Browse Source

添加配置

lex 8 months ago
parent
commit
da8c146875
45 changed files with 3923 additions and 8550 deletions
  1. 1 8516
      package-lock.json
  2. 4 0
      package.json
  3. 1 1
      src/components/o-sticky/index.tsx
  4. 7 7
      src/main.ts
  5. 38 0
      src/router/index.ts
  6. 4 1
      src/state.ts
  7. 72 0
      src/views/choise-homework/classify/index.module.less
  8. 78 0
      src/views/choise-homework/classify/index.tsx
  9. 208 0
      src/views/choise-homework/classroom-detail/child-node.tsx
  10. BIN
      src/views/choise-homework/classroom-detail/image/icon-arrow-down.png
  11. BIN
      src/views/choise-homework/classroom-detail/image/icon-arrow.png
  12. BIN
      src/views/choise-homework/classroom-detail/image/icon-cache-point.png
  13. BIN
      src/views/choise-homework/classroom-detail/image/icon-check-disabled.png
  14. BIN
      src/views/choise-homework/classroom-detail/image/icon-check.png
  15. BIN
      src/views/choise-homework/classroom-detail/image/icon-close.png
  16. BIN
      src/views/choise-homework/classroom-detail/image/icon-course-lock.png
  17. BIN
      src/views/choise-homework/classroom-detail/image/icon-course.png
  18. BIN
      src/views/choise-homework/classroom-detail/image/icon-imm.png
  19. BIN
      src/views/choise-homework/classroom-detail/image/icon-list.png
  20. BIN
      src/views/choise-homework/classroom-detail/image/icon-nocheck.png
  21. BIN
      src/views/choise-homework/classroom-detail/image/icon-question.png
  22. BIN
      src/views/choise-homework/classroom-detail/image/iconTip.png
  23. 13 0
      src/views/choise-homework/classroom-detail/image/look.svg
  24. 384 0
      src/views/choise-homework/classroom-detail/index.module.less
  25. 381 0
      src/views/choise-homework/classroom-detail/index.tsx
  26. BIN
      src/views/choise-homework/images/banner-bg.png
  27. 156 0
      src/views/choise-homework/index.module.less
  28. 309 0
      src/views/choise-homework/index.tsx
  29. BIN
      src/views/choise-homework/music-list/icons/icon_search.png
  30. BIN
      src/views/choise-homework/music-list/icons/music.png
  31. 22 0
      src/views/choise-homework/music-list/icons/recording.svg
  32. BIN
      src/views/choise-homework/music-list/icons/tips.png
  33. BIN
      src/views/choise-homework/music-list/icons/vip.png
  34. 296 0
      src/views/choise-homework/music-list/index.module.less
  35. 584 0
      src/views/choise-homework/music-list/index.tsx
  36. 31 0
      src/views/choise-homework/music-list/modals/choosePartName/index-rem.module.less
  37. 44 0
      src/views/choise-homework/music-list/modals/choosePartName/index.module.less
  38. 64 0
      src/views/choise-homework/music-list/modals/choosePartName/index.tsx
  39. 92 0
      src/views/choise-homework/pageState.tsx
  40. 173 24
      src/views/coursewarePlay/index.tsx
  41. 306 0
      src/views/exercise-after-class/index.module.less
  42. 413 0
      src/views/exercise-after-class/index.tsx
  43. 6 0
      src/views/exercise-after-class/types.ts
  44. 233 0
      src/views/exercise-after-class/video-class.tsx
  45. 3 1
      src/views/lessonCourseware/component/CourseItem/index.tsx

File diff suppressed because it is too large
+ 1 - 8516
package-lock.json


+ 4 - 0
package.json

@@ -27,11 +27,13 @@
     "cos-js-sdk-v5": "^1.4.21",
     "dayjs": "^1.11.7",
     "html2canvas": "^1.4.1",
+    "lodash": "^4.17.21",
     "numeral": "^2.0.6",
     "pinia": "^2.0.34",
     "plyr": "^3.7.8",
     "query-string": "^8.1.0",
     "rollup-plugin-terser": "^7.0.2",
+    "store": "^2.0.12",
     "swiper": "^11.0.3",
     "tcplayer.js": "^4.8.0",
     "umi-request": "^1.4.0",
@@ -43,7 +45,9 @@
   "devDependencies": {
     "@babel/core": "^7.21.4",
     "@babel/preset-env": "^7.21.4",
+    "@types/lodash": "^4.17.6",
     "@types/node": "^16.18.23",
+    "@types/store": "^2.0.5",
     "@typescript-eslint/eslint-plugin": "^5.57.1",
     "@typescript-eslint/parser": "^5.57.1",
     "@vitejs/plugin-legacy": "^4.1.1",

+ 1 - 1
src/components/o-sticky/index.tsx

@@ -62,7 +62,7 @@ export default defineComponent({
         forms.divStyle.bottom = props.offsetBottom || '0px';
       }
       try {
-        useResizeObserver(divRef.value, (entries: any) => {
+        useResizeObserver(div2Ref.value, (entries: any) => {
           const entry = entries[0];
           const { height } = entry.contentRect;
           if (Math.abs(height - forms.heightV) > 1) {

+ 7 - 7
src/main.ts

@@ -39,13 +39,13 @@ const urlIsTeacher =
     : false;
 // const paymentType = (window as any).paymentType; // 浏览器设置
 // 判断是哪个环境
-if (urlIsTeacher) {
-  state.platformType = 'TEACHER';
-  state.platformApi = '/api-teacher';
-} else {
-  state.platformType = 'STUDENT';
-  state.platformApi = '/api-student';
-}
+// if (urlIsTeacher) {
+state.platformType = 'TEACHER';
+state.platformApi = '/api-teacher';
+// } else {
+//   state.platformType = 'STUDENT';
+//   state.platformApi = '/api-student';
+// }
 
 const app = createApp(App);
 

+ 38 - 0
src/router/index.ts

@@ -32,6 +32,44 @@ const router: Router = createRouter({
           }
         },
         {
+          path: '/choise-homework',
+          name: 'choise-homework',
+          component: () => import('@/views/choise-homework'),
+          meta: {
+            title: '选择练习内容'
+          }
+        },
+        {
+          path: '/classify',
+          name: 'classify',
+          component: () => import('@/views/choise-homework/classify'),
+          meta: {
+            title: '声部云练'
+          }
+        },
+        {
+          path: '/music-list/:id',
+          component: () => import('@/views/choise-homework/music-list'),
+          meta: {
+            title: '声部云练'
+          }
+        },
+        {
+          path: '/classroom-detail',
+          component: () => import('@/views/choise-homework/classroom-detail'),
+          meta: {
+            title: '教材详情'
+          }
+        },
+        {
+          path: '/exerciseAfterClass',
+          name: 'exerciseAfterClass',
+          component: () => import('@/views/exercise-after-class/index'),
+          meta: {
+            title: '观看视频'
+          }
+        },
+        {
           path: '/login',
           name: 'login',
           component: () => import('@/views/layout/login'),

+ 4 - 1
src/state.ts

@@ -23,7 +23,10 @@ export const state = reactive({
   navBarHeight: 0, // 状态栏高度
   ossUploadUrl: 'https://ks3-cn-beijing.ksyuncs.com/',
   musicCertStatus: false as boolean, // 是否音乐认证
-  openLiveStatus: false as boolean // 是否开通直播
+  openLiveStatus: false as boolean, // 是否开通直播
+  max: -1, // 表示不限制
+  vIds: [], // 已经选择的视频
+  choiseType: '', // 需要选择资源的类型
 });
 
 // 预览上传到oss的地址

+ 72 - 0
src/views/choise-homework/classify/index.module.less

@@ -0,0 +1,72 @@
+.container{
+
+  :global{
+    .van-nav-bar .van-icon {
+      color:#333;
+    }
+    .van-nav-bar__text{
+      color:#01C1B5
+    }
+  }
+
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  .content{
+    flex: 1;
+    background-color: #F5F5F5;
+    padding: 12px;
+    .arrow {
+      font-size: 18px;
+      width: 20px;
+      height: 20px;
+    }
+    .title{
+      color: #333333;
+      font-size: 18px;
+      font-weight: 500;
+      display: flex;
+      align-items: center;
+      &::before{
+        content: '';
+        display: block;
+        width: 4px;
+        height: 18px;
+        border-radius: 2px;
+        background-color: #01C1B5;
+        margin-right: 6px;
+      }
+    }
+    .items{
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      >div{
+        margin-top: 20px;
+        width: 33.333%;
+        max-width: 180px;
+        text-align: center;
+        .inner {
+          width: 105px;
+          margin: 0 auto;
+        }
+        .img{
+          margin-top: 0;
+        }
+        :global {
+          .van-image, .van-image__error,.van-image__loading {
+            width: 105px;
+            min-height: 134px;
+          }
+        }
+        >div{
+          margin-top: 7px;
+          font-size: 14px;
+          font-weight: 500;
+          color: #1A1A1A;
+          line-height: 20px;
+        }
+      }
+    }
+  }
+}

+ 78 - 0
src/views/choise-homework/classify/index.tsx

@@ -0,0 +1,78 @@
+import { defineComponent } from 'vue';
+import { Image } from 'vant';
+import styles from './index.module.less';
+import OSticky from '@/components/o-sticky';
+import OHeader from '@/components/o-header';
+
+export default defineComponent({
+  name: 'classify-list',
+  data() {
+    return {
+      list: [] as any[],
+      liveConfig: false
+    };
+  },
+  mounted() {
+    // localStorage.setItem('behaviorId', getRandomKey())
+    // this.FetchList()
+    const musicScoreList = sessionStorage.getItem('musicScoreList');
+    if (musicScoreList) {
+      const tempMusicScoreList = JSON.parse(musicScoreList);
+
+      this.list = tempMusicScoreList; // 从上面页面获取分类信息
+    } else {
+      (this as any).$router.replace('/');
+    }
+
+    // appState.subjectOptions = [{value: 0, text: '全部声部'}]
+    // appState.subjectId = appState.origanSubjectId;
+
+    // const parseSearch: any = qs.parse(location.search);
+    // this.liveConfig = !!parseSearch.liveConfig;
+  },
+  methods: {
+    toDetail(item: any) {
+      (this as any).$router.push({
+        path: '/music-list/' + item.id,
+        query: {
+          ...this.$route.query
+        }
+      });
+    },
+    goBack() {
+      (this as any).$router.push({
+        path: '/',
+        query: {
+          ...this.$route.query
+        }
+      });
+    }
+  },
+  render() {
+    return (
+      <div class={styles.container}>
+        <OSticky position="top">
+          <OHeader
+            isBack={true}
+            border={false}
+            isFixed={false}
+            backIconColor="white"></OHeader>
+        </OSticky>
+        <div class={styles.content}>
+          <div class={styles.title}>教材</div>
+
+          <div class={styles.items}>
+            {this.list.map(item => (
+              <div key={item.id} onClick={() => this.toDetail(item)}>
+                <div class={styles.inner}>
+                  <Image src={item.coverImg} class={styles.img} />
+                  <div class="van-ellipsis">{item.name}</div>
+                </div>
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+    );
+  }
+});

+ 208 - 0
src/views/choise-homework/classroom-detail/child-node.tsx

@@ -0,0 +1,208 @@
+import { defineComponent } from 'vue';
+import styles from './index.module.less';
+import { Cell, Collapse, CollapseItem } from 'vant';
+import iconNoCheck from './image/icon-nocheck.png';
+import iconImm from './image/icon-imm.png';
+import iconCheck from './image/icon-check.png';
+import iconCheckDisabled from './image/icon-check-disabled.png';
+
+export const onMenuChange = (item: any, status?: string) => {
+  if (item.status === 'disabled') {
+    return;
+  }
+  // 判断父级元素
+  if (status) {
+    item.status = status;
+  } else {
+    if (item.status === 'disabled') {
+      return;
+    }
+    if (item.status === 'checked') {
+      item.status = 'nochecked';
+    } else {
+      item.status = 'checked';
+    }
+  }
+  if (Array.isArray(item.materialList)) {
+    // console.log(item.status, 'item.status', status);
+    item.materialList.forEach((material: any) => {
+      if (material.status === 'disabled') {
+        return;
+      }
+      if (item.status === 'nochecked') {
+        material.status = 'nochecked';
+      } else {
+        material.status = 'checked';
+      }
+    });
+  }
+  if (Array.isArray(item.children)) {
+    item.children.forEach((child: any) => {
+      onMenuChange(child, item.status);
+    });
+  }
+};
+
+export const onParentChangeStatus = (parent: any, childName = 'children') => {
+  let arrLength = 0;
+  let checkedLength = 0;
+  let indeterminateLength = 0;
+  if (parent.materialList && parent.materialList.length > 0) {
+    parent.materialList.forEach((item: any) => {
+      if (item.status !== 'disabled') {
+        arrLength += 1;
+      }
+
+      if (item.status === 'checked') {
+        checkedLength += 1;
+      }
+    });
+  }
+  if (Array.isArray(parent[childName])) {
+    parent[childName].forEach((item: any) => {
+      if (item.status !== 'disabled') {
+        arrLength += 1;
+      }
+
+      if (item.status === 'checked') {
+        checkedLength += 1;
+      }
+
+      if (item.status === 'indeterminate') {
+        indeterminateLength += 1;
+      }
+    });
+  }
+
+  if (checkedLength >= arrLength) {
+    parent.status = 'checked';
+  } else if (checkedLength > 0) {
+    parent.status = 'indeterminate';
+  } else {
+    parent.status = 'nochecked';
+  }
+  // 只能目录才会大于0,素材是不会有
+  if (indeterminateLength > 0) {
+    parent.status = 'indeterminate';
+  }
+};
+
+const ChildNode = defineComponent({
+  name: 'child-node',
+  props: {
+    list: {
+      type: Array,
+      default: () => []
+    },
+    collapse: {
+      type: String,
+      default: ''
+    }
+  },
+  emits: ['update:collapse', 'menuChange', 'materialChange'],
+  setup(props, { emit }) {
+    return () => (
+      <Collapse
+        modelValue={props.collapse}
+        onUpdate:modelValue={(val: any) => {
+          emit('update:collapse', val);
+        }}
+        border={false}
+        accordion>
+        {props.list?.map((point: any) => (
+          <CollapseItem
+            clickable={false}
+            center
+            class={styles.collapseChild}
+            name={point.id}>
+            {{
+              title: () => (
+                <div
+                  class={[
+                    styles.itemTitle,
+                    props.collapse === point.id ? styles.itemTitleActive : ''
+                  ]}>
+                  <i class={[styles.arrow]}></i>
+                  {point.name}
+                </div>
+              ),
+              default: () => (
+                <>
+                  {Array.isArray(point?.materialList) &&
+                    point.materialList.map((n: any) => (
+                      <Cell>
+                        {{
+                          title: () => n.name,
+                          value: () => (
+                            <img
+                              src={
+                                n.status === 'disabled'
+                                  ? iconCheckDisabled
+                                  : n.status === 'checked'
+                                  ? iconCheck
+                                  : iconNoCheck
+                              }
+                              class={[styles.radioBtn]}
+                              onClick={(e: any) => {
+                                e.stopPropagation();
+                                if (n.status === 'disabled') {
+                                  return;
+                                }
+                                if (n.status === 'checked') {
+                                  n.status = 'nochecked';
+                                } else {
+                                  n.status = 'checked';
+                                }
+
+                                onParentChangeStatus(point);
+                                emit('menuChange');
+                              }}
+                            />
+                          )
+                        }}
+                      </Cell>
+                    ))}
+                  {Array.isArray(point?.children) && (
+                    <ChildNode
+                      list={point.children}
+                      collapse={point.collapse}
+                      onUpdate:collapse={val => {
+                        point.collapse = val;
+                      }}
+                      onMenuChange={() => {
+                        onParentChangeStatus(point);
+                        emit('menuChange');
+                      }}
+                    />
+                  )}
+                </>
+              ),
+              'right-icon': () => (
+                <img
+                  src={
+                    point.status === 'indeterminate'
+                      ? iconImm
+                      : point.status === 'disabled'
+                      ? iconCheckDisabled
+                      : point.status === 'checked'
+                      ? iconCheck
+                      : iconNoCheck
+                  }
+                  class={[styles.radioBtn]}
+                  onClick={(e: any) => {
+                    e.stopPropagation();
+                    onMenuChange(point);
+
+                    emit('menuChange');
+                  }}
+                />
+              )
+            }}
+          </CollapseItem>
+        ))}
+      </Collapse>
+    );
+  }
+});
+
+export default ChildNode;

BIN
src/views/choise-homework/classroom-detail/image/icon-arrow-down.png


BIN
src/views/choise-homework/classroom-detail/image/icon-arrow.png


BIN
src/views/choise-homework/classroom-detail/image/icon-cache-point.png


BIN
src/views/choise-homework/classroom-detail/image/icon-check-disabled.png


BIN
src/views/choise-homework/classroom-detail/image/icon-check.png


BIN
src/views/choise-homework/classroom-detail/image/icon-close.png


BIN
src/views/choise-homework/classroom-detail/image/icon-course-lock.png


BIN
src/views/choise-homework/classroom-detail/image/icon-course.png


BIN
src/views/choise-homework/classroom-detail/image/icon-imm.png


BIN
src/views/choise-homework/classroom-detail/image/icon-list.png


BIN
src/views/choise-homework/classroom-detail/image/icon-nocheck.png


BIN
src/views/choise-homework/classroom-detail/image/icon-question.png


BIN
src/views/choise-homework/classroom-detail/image/iconTip.png


+ 13 - 0
src/views/choise-homework/classroom-detail/image/look.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="13px" height="13px" viewBox="0 0 13 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>切片</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="选择课件" transform="translate(-282.000000, -159.000000)">
+            <g id="锁备份" transform="translate(282.000000, 159.000000)">
+                <circle id="椭圆形" fill="#FFFFFF" cx="6.5" cy="8" r="1"></circle>
+                <rect id="矩形" stroke="#FFFFFF" stroke-width="1.2" x="1.6" y="4.1" width="9.8" height="7.8" rx="2"></rect>
+                <path d="M4.5,4 L4.5,3 C4.5,1.8954305 5.3954305,1 6.5,1 C7.6045695,1 8.5,1.8954305 8.5,3 L8.5,4 L8.5,4" id="路径" stroke="#FFFFFF" stroke-width="1.2"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 384 - 0
src/views/choise-homework/classroom-detail/index.module.less

@@ -0,0 +1,384 @@
+.courseList {
+  min-height: 100vh;
+  background-color: #fff;
+  background-image: linear-gradient(180deg, #7DEFE6 0%, rgba(251, 233, 213, 0) 198px);
+  padding: 10px 0;
+  box-sizing: border-box;
+}
+
+.periodContent {
+  display: flex;
+  padding: 20px;
+
+  .cover {
+    position: relative;
+    width: 107px;
+    min-height: 130px;
+    margin-right: 30px;
+    border-radius: 2px;
+    overflow: hidden;
+    box-shadow: 0px 2px 6px 0px rgba(221, 168, 133, 0.67);
+    overflow: hidden;
+    background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAABaCAMAAAAPdrEwAAABOFBMVEUAAAC0tLTT09PU1NTt7e3////T09P////T09P////T09P////X19f////U1NTU1NTT09PT09PU1NTU1NTV1dXT09PU1NTU1NTV1dXZ2dn+/v7R0dH////////KysrIyMjT09O0tLTU1NS0tLTU1NS0tLTT09PT09PT09PU1NTU1NTU1NTT09O2trbU1NTV1dXV1dXW1tbV1dXW1tbV1dXJycm4uLj///+0tLT///+0tLT////p6em0tLT4+Pj///+2trb///+1tbX///+0tLT////T09P5+fn+/v63t7e8vLz8/Pzl5eXW1tbBwcH39/fw8PDs7Ozb29v7+/vg4ODOzs65ubny8vLJycnp6enGxsbFxcW+vr62trb09PTY2NjLy8vCwsLn5+fi4uLS0tLu7u7Q0NDd3d02Mu/gAAAARHRSTlMAnPwvBfv07ODdw5AbGezmyrirl5B4aT0kDZyclTwsIPj48+Ta1tPS0LGhiIaEcGJgV05FNhMS7++8vKyslZRzc1ZWJRSpe/cAAALfSURBVFjD7djXcuIwGAVg1pTQAiT0Ekiy6b1v7whsA6b3EGAJu+//BstaiWNiCYGlXGTG55KBDzH6j2xjMmJET5ZdDivQFavDtTxLXlsCFFlam7HmiUxl49ftApRxYWkHLe3A0lZa2oqlAXUMGk/fHG9vpCnSk/pZNJ18m6aOWELRN1CmtbMI+jjNJH0Evc2GlhD0Bhu6h6DTjPIq6Pqonb8XXoCWmjKSHTCnpccp5ges6bHCZAW2dBc8JcOWLqjoPFu6oqLLr2bVQxX9h/GElBWmKDCmxSKAyXWYt1GE677rLtLGejsLcuOCQMI7lVFlsMjxJJUeW3bP+OSTskBJqcuSFppAFT4vsqMrYDrVAitazMHV8kBJs8WGzsPmNoRCFSj5fcuA7vJKvcQ8UJKrCNT0WH0mdEpASbFOSdfhyClvzagGsSzR0D14LHTSSho1/mkQaw39dB9u2vThCU8KQj1JtFiVt+z5ONSLmnoSaeszuia/+lfzAaGS09STQDum6SH/cOXX5rY9Xz0Rj6Sqy0YmjUzrbp56ah6kIT2AH8Pe4M5TT83jP6ThsnALmq+eiD8tlIt/G8POWU8FVNMNuXc8rnH4epJpOHg1jIivJ5mWcvLg4ZqMryeZhnOLm1hyPfF0C95R9PAcvp4jcSYNL7UDHEWuJ56GO46FyPXE0+/+/7Lhgs+ffU093yPoDwCAEcki1/Mjgv4BQFUkSeR6/jQh8gn0SQy5np9NqFi+CgSDWE9rwmJCJ/X9yxt9iSdisVji2y+TESNGXjiW6LU7FYkkzQ9JRiIp93XUoodyX12cBg/9qyvOHbvXxmUw4Wxe+45zZdV/GDy9uHJbZqPhk7jTjrTI4ezO+EkY8wUBW4Y6tgCSPvNxtDLnOzOhEw2HAvt7Hj2oZ28/EApHyRtpvjwPBY/8B5Ot9O1u2b2eTds6x8nr4tZtmx6vfWvXN9nCA/9RMHR+aZ5sohEji+YfnN0/51L6d+cAAAAASUVORK5CYII=');
+    background-repeat: no-repeat;
+    background-position: center;
+    background-size: 50%;
+    flex-shrink: 0;
+
+    &>img {
+      display: block;
+      width: 100%;
+      min-height: 140px;
+      opacity: 0;
+      transition: opacity .3s;
+      object-fit: cover;
+    }
+
+    :global {
+      .van-image__loading {
+        position: relative;
+        min-height: 130px;
+        animation: van-skeleton-blink var(--van-skeleton-duration) ease-in-out infinite;
+      }
+    }
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 5px;
+      width: 5px;
+      height: 100%;
+      background: linear-gradient(270deg, rgba(0, 0, 0, 0.25) 0%, rgba(0, 0, 0, 0.03) 100%);
+      box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.2);
+      z-index: 1;
+    }
+  }
+
+  .contentTitle {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333;
+    line-height: 22px;
+    padding-bottom: 8px;
+  }
+
+  .contentLabel {
+    font-size: 12px;
+    font-weight: 400;
+    color: rgb(96, 96, 96);
+    line-height: 20px;
+  }
+}
+
+
+.periodTitle {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 20px 20px 0;
+
+  .pIcon {
+    width: 20px;
+    height: 20px;
+    margin-right: 6px;
+  }
+
+  .pTitle {
+    font-size: 16px;
+    font-weight: 600;
+    color: #2D3131;
+    margin-right: 8px;
+  }
+
+  .pNum {
+    font-size: 12px;
+    font-weight: 400;
+    color: rgba(0, 0, 0, 0.5);
+  }
+
+  .left {
+    display: flex;
+    align-items: center;
+  }
+
+  .iconQuestion {
+    width: 18px;
+    height: 18px;
+    cursor: pointer;
+  }
+}
+
+// .periodList {
+//   :global {
+//     .van-cell-group--inset {
+//       margin: 0;
+//     }
+
+//     .van-cell-group,
+//     .van-cell {
+//       background: transparent;
+//     }
+
+//     .van-cell {
+//       padding: 18px 20px;
+
+//       &::after {
+//         left: 20px;
+//         right: 20px;
+//         border-color: rgba(242, 242, 242, 1);
+//         transform: none;
+//       }
+
+//       .van-cell__title {
+//         padding-right: 8px;
+
+//         span {
+//           font-size: 15px;
+//           font-weight: 600;
+//           color: #333333;
+//           line-height: 21px;
+//           word-break: break-all;
+//         }
+
+//         .van-cell__label {
+//           font-size: 12px;
+//           font-weight: 400;
+//           color: #AAAAAA;
+//           line-height: 17px;
+//           margin: 0;
+//         }
+//       }
+
+//       .van-cell__value {
+//         flex: inherit;
+//         flex-shrink: 0;
+//       }
+//     }
+//   }
+
+//   .baseBtn {
+//     width: 73px;
+//     height: 26px;
+//     line-height: 1;
+//     color: #fff;
+//     font-size: 13px;
+//     font-weight: 500;
+//     border: 0;
+//     border-radius: 13px;
+//     flex-shrink: 0;
+
+//     :global {
+//       .van-button__text {
+//         white-space: nowrap;
+//       }
+//     }
+
+//     &.disabled {
+//       opacity: 0.6 !important;
+//     }
+
+//     &.look {
+//       background: linear-gradient(270deg, #4BE0A1 0%, #01C1B5 100%);
+//     }
+
+//     &.down {
+//       background: linear-gradient(270deg, #4BE0A1 0%, #01C1B5 100%);
+//       ;
+//     }
+
+//     &.downing {
+//       background: linear-gradient(270deg, #47CFE5 0%, #2FB3FF 100%);
+//     }
+
+//     &.disable {
+//       opacity: 1;
+//       background: linear-gradient(180deg, #D3D3D3 0%, #8F8F8F 100%);
+//     }
+//   }
+// }
+
+.periodItem {
+  width: 36px;
+  height: 40px;
+  margin-right: 8px;
+  flex-shrink: 0;
+
+  img {
+    width: 100%;
+    height: 100%;
+    display: block;
+    object-fit: cover;
+  }
+}
+
+.courseDialog {
+  width: 315px;
+
+  :global {
+    .van-dialog__header {
+      // padding-top: 0;
+
+    }
+  }
+
+  .iconCross {
+    position: absolute;
+    top: 10px;
+    right: 10px;
+    font-size: 22px;
+    color: #ccc;
+    z-index: 99;
+  }
+}
+
+.periodItemModel {
+  position: relative;
+
+  .iconCachePoint {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    display: block;
+    object-fit: contain;
+    width: 13px;
+    height: 13px;
+  }
+
+  .periodTip {
+    position: absolute;
+    left: -7px;
+    top: 0;
+    max-height: 15px;
+    display: block;
+    object-fit: contain;
+  }
+
+  .downloading {
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    top: 0;
+    background: rgba(0, 0, 0, 0.4);
+    font-size: 11px;
+    color: #fff;
+    border-radius: 9px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+
+.itemTitle {
+  display: flex;
+  align-items: center;
+  font-weight: 500;
+  font-size: 15px;
+  color: #777777;
+
+  &.itemTitleActive {
+    color: #131415;
+
+    .arrow {
+      background: url('./image/icon-arrow-down.png');
+      background-size: contain;
+    }
+  }
+
+  .arrow {
+    width: 16px;
+    height: 17px;
+    display: inline-block;
+    margin-right: 6px;
+    background: url('./image/icon-arrow.png');
+    background-size: contain;
+
+    &.arrowActive {
+      background: url('./image/icon-arrow-down.png');
+      background-size: contain;
+    }
+  }
+}
+
+.collapseParent {
+  padding: 0 20px;
+
+  :global {
+    .van-cell {
+      padding: 16px 0;
+    }
+
+    .van-collapse-item--border:after {
+      left: 0;
+      right: 0;
+    }
+
+    .van-collapse-item__content {
+      padding-top: 0;
+      padding-bottom: 0;
+      padding-right: 0;
+    }
+  }
+}
+
+.collapseChild {
+
+  :global {
+    .van-collapse-item__content {
+      padding-left: 22px;
+
+      .van-cell {
+        padding: 12px 0;
+      }
+
+      .van-cell__title {
+        flex: 1 auto;
+      }
+
+      .van-cell__value {
+        flex-shrink: 0;
+        flex: 1 auto;
+      }
+    }
+  }
+}
+
+.radioBtn {
+  width: 18px;
+  height: 18px;
+}
+
+.btnGroup {
+  padding: 10px 25px calc(20px + env(safe-area-inset-bottom));
+  border-top: 1px solid #F2F2F2;
+  background-color: #fff;
+
+  :global {
+    .van-button {
+      border-radius: 8px;
+    }
+  }
+}
+
+.popupRule {
+  width: 315px;
+  background: #FFFFFF;
+  border-radius: 12px;
+  padding: 20px;
+
+  .title {
+    font-weight: 600;
+    font-size: 18px;
+    color: #333333;
+    line-height: 25px;
+    text-align: center;
+  }
+
+  .content {
+    font-size: 15px;
+    color: #666666;
+    line-height: 26px;
+    padding-bottom: 20px;
+    padding-top: 15px;
+  }
+}

+ 381 - 0
src/views/choise-homework/classroom-detail/index.tsx

@@ -0,0 +1,381 @@
+import request from '@/helpers/request';
+import { state } from '@/state';
+import { Button, Cell, Collapse, CollapseItem, Popup } from 'vant';
+import { defineComponent, onMounted, reactive, TransitionGroup } from 'vue';
+import styles from './index.module.less';
+import { useRoute, useRouter } from 'vue-router';
+import { postMessage } from '@/helpers/native-message';
+import iconQuestion from './image/icon-question.png';
+import iconNoCheck from './image/icon-nocheck.png';
+import iconImm from './image/icon-imm.png';
+import iconCheck from './image/icon-check.png';
+import iconCheckDisabled from './image/icon-check-disabled.png';
+
+import { browser } from '@/helpers/utils';
+import OEmpty from '@/components/o-empty';
+import iconList from './image/icon-list.png';
+import OHeader from '@/components/o-header';
+import { useEventListener } from '@vant/use';
+import OLoading from '@/components/o-loading';
+import OSticky from '@/components/o-sticky';
+import ChildNode, { onMenuChange, onParentChangeStatus } from './child-node';
+export default defineComponent({
+  name: 'courseList',
+  setup() {
+    const route = useRoute();
+    const router = useRouter();
+    const browserInfo = browser();
+    const data = reactive({
+      titleOpacity: 0,
+      catchStatus: false,
+      catchItem: {} as any,
+      loading: true,
+      detail: {
+        cover: '',
+        name: '',
+        des: ''
+      },
+      list: [] as any,
+      ruleStatus: false,
+      isDownloading: false, // 是否在下载资源
+      parentCollapse: '' as any,
+      childrenCollapse: '' as any,
+      defaultValue: [1073557, 1073558]
+    });
+
+    /** 获取课件详情 */
+    const getDetail = async () => {
+      const res: any = await request.get(
+        `${state.platformApi}/lessonCourseware/getLessonCoursewareDetail/${route.query.id}`
+      );
+      if (res?.data) {
+        data.detail.cover = res.data.coverImg;
+        data.detail.name = res.data.name;
+        data.detail.des = res.data.lessonTargetDesc;
+      }
+    };
+    const getList = async () => {
+      data.loading = true;
+      try {
+        const res: any = await request.get(
+          state.platformApi +
+            '/lessonCourseware/getLessonCoursewareCourseList/' +
+            route.query.id
+        );
+        if (Array.isArray(res?.data)) {
+          res.data.forEach((item: any) => {
+            item.status = 'nochecked';
+            item.children = item.knowledgePointList || [];
+            item.id = item.coursewareDetailId;
+            item.name = item.coursewareDetailName;
+            // const tempK = item.knowledgePointList || [];
+            formatDataList(item.children);
+          });
+
+          data.list = res.data;
+          console.log(data.list, 'data.list');
+        }
+      } catch (error) {
+        //
+      }
+      data.loading = false;
+    };
+    const formatDataList = (list: any = []) => {
+      const tempList: any = [];
+      list.forEach((item: any) => {
+        let tempChild: any = {};
+        item.status = 'nochecked';
+        item.collapse = '';
+        if (Array.isArray(item.materialList)) {
+          item.materialList.forEach((item: any) => {
+            if (data.defaultValue.includes(item.id)) {
+              item.status = 'disabled';
+            } else {
+              item.status = 'nochecked';
+            }
+          });
+        }
+        tempChild = item;
+        if (Array.isArray(item.children)) {
+          tempChild.children = formatDataList(item.children);
+        }
+
+        tempList.push({
+          ...tempChild
+        });
+      });
+      return tempList;
+    };
+
+    const getKnowledgeMaterialIds = (list: any = []) => {
+      const tempList: any = [];
+      list.forEach((item: any) => {
+        if (Array.isArray(item.materialList)) {
+          item.materialList.forEach((item: any) => {
+            if (item.status === 'checked') {
+              tempList.push({
+                id: item.id,
+                name: item.name
+              });
+            }
+          });
+        }
+        if (Array.isArray(item.children)) {
+          tempList.push(...getKnowledgeMaterialIds(item.children));
+        }
+      });
+      return tempList;
+    };
+
+    onMounted(() => {
+      getDetail();
+      getList();
+    });
+
+    // const handleClick = async (item: any) => {
+    //   if (!item.knowledgePointList) {
+    //     showConfirmDialog({
+    //       message: '该课件暂无知识点'
+    //     });
+    //     return;
+    //   }
+
+    //   if (!item.hasCache) {
+    //     // const hasFree = String(item.accessScope) === '0';
+    //     // if (!hasFree) {
+
+    //     // 下载中不提示
+    //     if (item.downloadStatus == 1) {
+    //       // 取消下载
+    //       postMessage({ api: 'cancelDownloadCourseware' });
+    //       setTimeout(() => {
+    //         postMessage({ api: 'cancelDownloadCourseware' });
+    //         item.downloadStatus = 0;
+    //         data.isDownloading = false;
+    //       }, 1000);
+    //       showLoadingToast({
+    //         message: '取消中...',
+    //         forbidClick: false,
+    //         loadingType: 'spinner',
+    //         duration: 1000
+    //       });
+    //       return;
+    //     }
+
+    //     data.catchStatus = true;
+    //     data.catchItem = item;
+    //     return;
+    //   }
+    // };
+
+    useEventListener('scroll', () => {
+      const height =
+        window.scrollY ||
+        window.pageYOffset ||
+        document.documentElement.scrollTop;
+      data.titleOpacity = height > 100 ? 1 : height / 100;
+    });
+
+    // const onChangeCheck = () => {
+    // data.list.forEach((item: any) => {});
+    // };
+
+    // watch(
+    //   () => data.list,
+    //   () => {
+    //     //
+
+    //     onChangeCheck();
+    //   },
+    //   {
+    //     immediate: true,
+    //     deep: true
+    //   }
+    // );
+    return () => (
+      <div class={styles.courseList}>
+        <OHeader
+          border={false}
+          background={`rgba(255,255,255, ${data.titleOpacity})`}
+          title="教材详情"
+        />
+        <div class={styles.periodContent}>
+          <div class={styles.cover}>
+            <img
+              src={data.detail.cover}
+              onLoad={(e: Event) => {
+                if (e.target) {
+                  (e.target as any).style.opacity = 1;
+                }
+              }}
+            />
+          </div>
+          <div>
+            <div class={styles.contentTitle}>{data.detail.name}</div>
+            <div class={styles.contentLabel}>教学目标:{data.detail.des}</div>
+          </div>
+        </div>
+
+        <TransitionGroup name="van-fade">
+          {!data.loading && (
+            <>
+              <div key="periodTitle" class={styles.periodTitle}>
+                <div class={styles.left}>
+                  <img class={styles.pIcon} src={iconList} />
+                  <div class={styles.pTitle}>课程列表</div>
+                  <div class={styles.pNum}>共{data.list.length}课</div>
+                </div>
+                <div class={styles.right}>
+                  <img
+                    src={iconQuestion}
+                    class={styles.iconQuestion}
+                    onClick={() => (data.ruleStatus = true)}
+                  />
+                </div>
+              </div>
+
+              <div key="list" class={styles.periodList}>
+                <Collapse
+                  modelValue={data.parentCollapse}
+                  onUpdate:modelValue={(val: any) => {
+                    data.parentCollapse = val;
+                    data.childrenCollapse = ''; // 重置子项选择
+                  }}
+                  class={styles.collapseParent}
+                  border={false}
+                  accordion>
+                  {data.list.map((item: any) => (
+                    <CollapseItem
+                      center
+                      name={item.coursewareDetailId}
+                      clickable={false}>
+                      {{
+                        title: () => (
+                          <div
+                            class={[
+                              styles.itemTitle,
+                              data.parentCollapse === item.coursewareDetailId
+                                ? styles.itemTitleActive
+                                : ''
+                            ]}>
+                            <i class={[styles.arrow]}></i>
+                            {item.coursewareDetailName}
+                          </div>
+                        ),
+                        default: () => (
+                          <>
+                            {Array.isArray(item?.materialList) &&
+                              item.materialList.map((n: any) => (
+                                <Cell>
+                                  {{
+                                    title: () => n.name,
+                                    value: () => (
+                                      <img
+                                        src={iconCheck}
+                                        class={[styles.radioBtn]}
+                                      />
+                                    )
+                                  }}
+                                </Cell>
+                              ))}
+                            {Array.isArray(item?.children) && (
+                              <ChildNode
+                                list={item.children}
+                                collapse={item.collapse}
+                                onUpdate:collapse={val => {
+                                  item.collapse = val;
+                                }}
+                                onMenuChange={() => {
+                                  onParentChangeStatus(item);
+                                }}
+                              />
+                            )}
+                          </>
+                        ),
+                        'right-icon': () => (
+                          <img
+                            src={
+                              item.status === 'indeterminate'
+                                ? iconImm
+                                : item.status === 'disabled'
+                                ? iconCheckDisabled
+                                : item.status === 'checked'
+                                ? iconCheck
+                                : iconNoCheck
+                            }
+                            class={[styles.radioBtn]}
+                            onClick={(e: any) => {
+                              e.stopPropagation();
+                              if (item.status === 'disabled') {
+                                return;
+                              }
+                              if (item.status === 'checked') {
+                                item.status = 'nochecked';
+                              } else {
+                                item.status = 'checked';
+                              }
+                              if (Array.isArray(item.knowledgePointList)) {
+                                item.knowledgePointList.forEach(
+                                  (knowledgePoint: any) => {
+                                    onMenuChange(knowledgePoint, item.status);
+                                  }
+                                );
+                              }
+                            }}
+                          />
+                        )
+                      }}
+                    </CollapseItem>
+                  ))}
+                </Collapse>
+              </div>
+            </>
+          )}
+        </TransitionGroup>
+        {data.loading && <OLoading />}
+        {!data.loading && !data.list.length && <OEmpty tips="暂无内容" />}
+
+        <OSticky position="bottom">
+          <div class={styles.btnGroup}>
+            <Button
+              block
+              type="primary"
+              color="linear-gradient( 132deg, #60DBC7 0%, #01C1B5 100%)"
+              onClick={() => {
+                const ids = getKnowledgeMaterialIds(data.list);
+
+                const body = {
+                  api: 'onCoursewareSelectResult',
+                  content: {
+                    result: ids
+                  }
+                };
+                postMessage(body);
+              }}>
+              确定
+            </Button>
+          </div>
+        </OSticky>
+
+        <Popup v-model:show={data.ruleStatus} class={styles.popupRule}>
+          <div class={styles.title}>温馨提示</div>
+          <div class={styles.content}>
+            1、作业只能选择视频资源布置
+            <br />
+            2、单个练习组最大可添加20个练习
+            <br />
+            3、同练习组单个视频资源不可多次添加
+          </div>
+          <div class={styles.popupBtnGroup}>
+            <Button
+              block
+              color="linear-gradient( 132deg, #60DBC7 0%, #01C1B5 100%)"
+              onClick={() => (data.ruleStatus = false)}>
+              我知道了
+            </Button>
+          </div>
+        </Popup>
+      </div>
+    );
+  }
+});

BIN
src/views/choise-homework/images/banner-bg.png


+ 156 - 0
src/views/choise-homework/index.module.less

@@ -0,0 +1,156 @@
+.choiseHomework {
+  min-height: 100vh;
+  background: url('./images/banner-bg.png') no-repeat top center;
+  background-size: cover;
+  // padding: 10px 0;
+  box-sizing: border-box;
+
+  :global {
+    .van-sticky--fixed {
+      // background: url('./images/banner-bg.png') no-repeat top center;
+      // background-size: cover;
+    }
+
+    .van-tabs__nav {
+      background: transparent;
+    }
+
+    // .van-tab {
+    //   font-size: 14px;
+    //   color: rgba(0, 0, 0, 0.55);
+    // }
+
+    // .van-tab--active {
+    //   font-size: 16px;
+    //   font-weight: 500;
+    //   color: #000000;
+    // }
+
+    // .van-tabs__line {
+    //   width: 14px;
+    //   height: 4px;
+    //   border-radius: 2px;
+    //   bottom: 20px
+    // }
+
+
+    // .van-tab--shrink {
+    //   padding: 0 12px;
+    // }
+  }
+}
+
+.topTabs {
+  background-color: #fff;
+  margin: 0 12px;
+  border-radius: 12px 12px 0 0;
+
+  :global {
+    .van-tab {
+      font-weight: 600;
+      font-size: 16px;
+      color: #333333;
+      line-height: 22px;
+    }
+
+    .van-tabs__nav--line {
+      padding: 0;
+    }
+
+    .van-tabs__line {
+      width: 26px;
+      height: 3px;
+      background: #01C1B5;
+      border-radius: 2px;
+      bottom: 4px;
+    }
+  }
+}
+
+.classroomTab {
+  :global {
+    margin: 0 12px;
+
+    .van-tab {
+      font-size: 14px;
+      color: rgba(0, 0, 0, 0.55);
+    }
+
+    .van-tab--active {
+      font-weight: 500;
+      font-size: 16px;
+      color: #00B2A7;
+    }
+
+    .van-tabs__line {
+      display: none;
+    }
+  }
+}
+
+.container {
+  min-height: calc(100vh - var(--header-height));
+  background-color: #fff;
+  margin: 0 12px;
+  padding: 10px 14px 0;
+
+  &.containerClass {
+    padding-left: 7px;
+    padding-right: 7px;
+    padding-top: 4px;
+
+    &>div {
+      padding: 0;
+
+      &>div {
+        margin-bottom: 0;
+      }
+    }
+
+    :global {
+      .courseItem {
+        margin-top: 0;
+        margin-bottom: 20px;
+      }
+    }
+  }
+}
+
+.items {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+
+  >div {
+    margin-bottom: 12px;
+    width: 100%;
+    text-align: center;
+
+    .inner {
+      width: 100%;
+      margin: 0 auto;
+    }
+
+    .img {
+      margin-top: 0;
+    }
+
+    :global {
+
+      .van-image,
+      .van-image__error,
+      .van-image__loading {
+        width: 100%;
+        min-height: 128px;
+      }
+    }
+
+    // >div{
+    //   margin-top: 7px;
+    //   font-size: 14px;
+    //   font-weight: 500;
+    //   color: #1A1A1A;
+    //   line-height: 20px;
+    // }
+  }
+}

+ 309 - 0
src/views/choise-homework/index.tsx

@@ -0,0 +1,309 @@
+import request from '@/helpers/request';
+import { Tab, Tabs, Image } from 'vant';
+import { state as baseState } from '@/state';
+import {
+  Transition,
+  computed,
+  defineComponent,
+  onMounted,
+  reactive
+} from 'vue';
+import styles from './index.module.less';
+import { useRoute, useRouter } from 'vue-router';
+import {
+  listenerMessage,
+  postMessage,
+  removeListenerMessage
+} from '@/helpers/native-message';
+import { browser } from '@/helpers/utils';
+import OHeader from '@/components/o-header';
+import OSticky from '@/components/o-sticky';
+import OEmpty from '@/components/o-empty';
+import CourseItem from '../lessonCourseware/component/CourseItem';
+import OLoading from '@/components/o-loading';
+export default defineComponent({
+  name: 'courseList',
+  setup() {
+    // music | video | onlymusic
+    const route = useRoute();
+    const router = useRouter();
+    const browserInfo = browser();
+
+    const pageType = route.query.type as any;
+    let topType = 'music';
+
+    if (pageType !== 'onlymusic') {
+      topType = pageType;
+    }
+
+    const state = reactive({
+      topKey: topType || ('music' as 'music' | 'video'),
+      list: [] as any,
+      show: true,
+      actionKey: 0,
+      loading: true,
+      subjectList: [] as any,
+      classList: [] as any
+    });
+
+    const FetchList = async () => {
+      try {
+        const res = await request.get(
+          baseState.platformApi + '/sysMusicScoreCategories/queryTree'
+        );
+        // 在管乐迷后台分部配置已经有显示对应教材,所以不用单独过滤了
+        state.list = res.data || [];
+        state.show = state.list.length > 0 ? true : false;
+      } catch (error) {
+        //
+      }
+    };
+
+    // 课件
+    const getList = async () => {
+      state.loading = true;
+      try {
+        const res: any = await request.post(
+          baseState.platformApi + `/lessonCourseware/queryLessonCourseware`,
+          {
+            data: {
+              subjectId: state.actionKey ? state.actionKey : null,
+              page: 1,
+              rows: 999
+            }
+          }
+        );
+        const _data = res.data.rows.map((n: any) => {
+          return {
+            ...n,
+            coverImg: n.cover,
+            name: n.name,
+            id: n.id,
+            courseNum: n.courseNum
+          };
+        });
+        state.classList = _data;
+      } catch (error) {
+        //
+      }
+      state.loading = false;
+    };
+
+    const getSubjectList = async () => {
+      try {
+        const res = await request.get(
+          baseState.platformApi +
+            '/lessonCourseware/getLessonCoursewareSubjectList'
+        );
+
+        state.subjectList = res.data || [];
+      } catch {
+        //
+      }
+    };
+
+    const _initClassroom = async () => {
+      try {
+        await getSubjectList();
+        const studentSubjectIds =
+          baseState.platformType === 'TEACHER'
+            ? baseState.user.data.subjectId
+            : baseState.user.data.student.subjectIdList;
+        const id = studentSubjectIds ? studentSubjectIds.split(',')[0] : 0;
+        state.subjectList.forEach((subject: any) => {
+          if (Number(id) === subject.id) {
+            state.actionKey = Number(id);
+          }
+        });
+
+        await getList();
+      } catch (e) {
+        //
+        console.log(e, 'e');
+      }
+      state.loading = false;
+
+      // 获取默认数据
+      postMessage(
+        {
+          api: 'getCousewareSelectResult'
+        },
+        (res: any) => {
+          if (res?.content?.data) {
+            baseState.max = res.content.data.max;
+            baseState.vIds = res.content.data.corusewareId || [];
+            return;
+          }
+        }
+      );
+    };
+
+    const onChangeTopTabs = () => {
+      if (state.topKey === 'music') {
+        //
+        if (state.list.length <= 0) {
+          FetchList();
+        }
+      } else if (state.topKey === 'video') {
+        if (state.subjectList.length <= 0 || state.classList.length <= 0) {
+          _initClassroom();
+        }
+      }
+    };
+
+    onMounted(async () => {
+      //
+      if (state.topKey === 'music') {
+        FetchList();
+      } else {
+        _initClassroom();
+      }
+    });
+
+    const actions = computed(() => {
+      const _list = state.subjectList.map((item: any) => {
+        return {
+          id: item.id,
+          name: item.name,
+          text: item.name,
+          value: item.id
+        };
+      });
+      _list.unshift({
+        id: '',
+        name: '课程类型',
+        text: '全部',
+        value: 0
+      });
+      return _list;
+    });
+
+    const toDetail = (item: any) => {
+      const childLength = item.sysMusicScoreCategoriesList
+        ? item.sysMusicScoreCategoriesList.length
+        : 0;
+      // 如果二级分类只有一个分类,则直接跳转到曲目列表
+      sessionStorage.setItem(
+        'musicScoreList',
+        JSON.stringify(item.sysMusicScoreCategoriesList)
+      );
+      if (childLength > 1) {
+        router.push({
+          path: '/classify',
+          query: {
+            parentId: item.id, // 父级元素编号
+            ...route.query
+          }
+        });
+      } else {
+        router.push({
+          path:
+            '/music-list/' +
+            (childLength == 1
+              ? item.sysMusicScoreCategoriesList[0]?.id
+              : item.id),
+          query: {
+            ...route.query
+          }
+        });
+      }
+    };
+
+    const handleClick = (item: any) => {
+      router.push({
+        path: '/classroom-detail',
+        query: {
+          id: item.lessonCoursewareId
+        }
+      });
+    };
+    return () => (
+      <div class={styles.choiseHomework}>
+        <OSticky position="top">
+          <OHeader
+            border={false}
+            background="transparent"
+            color="#131415"></OHeader>
+
+          {pageType !== 'onlymusic' && (
+            <div class={styles.topTabs}>
+              <Tabs
+                border={false}
+                v-model:active={state.topKey}
+                onClickTab={(val: any) => {
+                  state.topKey = val.name;
+                  onChangeTopTabs();
+                }}>
+                <Tab title="云教练" name="music"></Tab>
+                <Tab title="云课堂" name="video"></Tab>
+              </Tabs>
+
+              {state.topKey === 'video' && (
+                <Tabs
+                  class={styles.classroomTab}
+                  v-model:active={state.actionKey}
+                  shrink
+                  onClickTab={(val: any) => {
+                    state.actionKey = val.name;
+                    getList();
+                  }}>
+                  {actions.value.map((item: any) => (
+                    <Tab title={item.text} name={item.value}></Tab>
+                  ))}
+                </Tabs>
+              )}
+            </div>
+          )}
+        </OSticky>
+        <div
+          class={[
+            styles.container,
+            state.topKey === 'video' ? styles.containerClass : ''
+          ]}>
+          {state.topKey === 'music' ? (
+            <Transition name="van-fade">
+              <div class={styles.items}>
+                {state.show ? (
+                  state.list.map((item: any) => (
+                    <div key={item.id} onClick={() => toDetail(item)}>
+                      <div class={styles.inner}>
+                        <Image src={item.coverImg} class={styles.img} />
+                      </div>
+                    </div>
+                  ))
+                ) : (
+                  <OEmpty tips="暂无数据" />
+                )}
+              </div>
+            </Transition>
+          ) : (
+            <>
+              <Transition name="van-fade">
+                {!state.loading &&
+                  Object.values(state.classList).length > 0 && (
+                    <CourseItem
+                      // term={key}
+                      list={state.classList}
+                      onItemClick={row => handleClick(row)}
+                    />
+                  )}
+              </Transition>
+              {state.loading && <OLoading />}
+
+              {!state.loading && !Object.values(state.classList).length && (
+                <div
+                  style={{
+                    minHeight: `calc(100vh - var(--header-height))`,
+                    display: 'flex',
+                    alignItems: 'center'
+                  }}>
+                  <OEmpty tips="暂无课件" />
+                </div>
+              )}
+            </>
+          )}
+        </div>
+      </div>
+    );
+  }
+});

BIN
src/views/choise-homework/music-list/icons/icon_search.png


BIN
src/views/choise-homework/music-list/icons/music.png


+ 22 - 0
src/views/choise-homework/music-list/icons/recording.svg

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="79px" height="26px" viewBox="0 0 79 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 62 (91390) - https://sketch.com -->
+    <title>矩形</title>
+    <desc>Created with Sketch.</desc>
+    <g id="退团" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="伴奏备份-4" transform="translate(-280.000000, -239.000000)">
+            <g id="矩形" transform="translate(281.000000, 240.000000)">
+                <g>
+                    <rect stroke="#01C1B5" stroke-width="1.1" opacity="0.49702381" x="0" y="0" width="77" height="24" rx="12"></rect>
+                    <g id="编组" transform="translate(6.000000, 7.000000)" fill="#01C1B5" fill-rule="nonzero">
+                        <path d="M2.17142857,2 L4.82857143,2 C4.94285714,2 5,2.05555556 5,2.16666667 L5,3.16666667 C5,3.27777778 4.94285714,3.33333333 4.82857143,3.33333333 L2.17142857,3.33333333 C2.05714286,3.33333333 2,3.27777778 2,3.16666667 L2,2.16666667 C2,2.05555556 2.05714286,2 2.17142857,2 Z" id="路径"></path>
+                        <path d="M11.5698113,1.59528409 C11.4758441,1.5953536 11.3845899,1.62647469 11.3105198,1.68371212 L9.42878149,3.11354167 L9.42878149,0.636363636 C9.42878149,0.284909705 9.14095813,0 8.78591002,0 L0.642871465,0 C0.287823359,0 0,0.284909705 0,0.636363636 L0,8.6969697 C0,9.04842363 0.287823359,9.33333333 0.642871465,9.33333333 L8.78591002,9.33333333 C9.14095813,9.33333333 9.42878149,9.04842363 9.42878149,8.6969697 L9.42878149,6.23318182 L11.3105198,7.66274621 C11.3845175,7.71992784 11.4756681,7.75104573 11.5695435,7.75117424 C11.7914681,7.75117424 12,7.57776515 12,7.32613636 L12,2.02032197 C12.0002673,1.76869318 11.7917359,1.59528409 11.5698113,1.59528409 Z M8.46447429,8.37878788 L0.964307198,8.37878788 L0.964307198,0.954545455 L8.46447429,0.954545455 L8.46447429,8.37878788 Z M11.1431054,6.4657197 L9.42878149,5.16382576 L9.42878149,4.18276515 L11.1431054,2.88034091 L11.1431054,6.4657197 Z" id="形状"></path>
+                    </g>
+                    <text id="边录边播" font-family="PingFangSC-Regular, PingFang SC" font-size="12" font-weight="normal" fill="#01C1B5">
+                        <tspan x="21.7" y="16">边录边播</tspan>
+                    </text>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/views/choise-homework/music-list/icons/tips.png


BIN
src/views/choise-homework/music-list/icons/vip.png


+ 296 - 0
src/views/choise-homework/music-list/index.module.less

@@ -0,0 +1,296 @@
+
+.accompany {
+  min-height: 100vh;
+  overflow: hidden;
+  :global{
+    .van-sticky--fixed{
+      box-shadow: 5px 5px 5px 0 rgba(0, 0, 0, .03);
+    }
+    .van-dropdown-menu__bar{
+      box-shadow: none;
+      background-color: #F8F9FC;
+      height: auto;
+      padding: 15px 0 5px; // 为了处理下拉框看上去居中
+    }
+    .van-nav-bar .van-icon {
+      color:#333;
+    }
+    .van-nav-bar__text{
+      color:#01C1B5
+    }
+  }
+
+  [class*='van-hairline']::after{
+      display: none;
+  }
+  /deep/ .van-tab{
+      color: #808080;
+  }
+  /deep/ .van-tab--active{
+      span{
+          color: #01C1B5;
+      }
+  }
+
+  .playIcon {
+    font-size: 65px;
+    display: flex;
+    align-items: center;
+    :global{
+      .van-badge__wrapper{
+        margin: auto;
+      }
+    }
+  }
+  :global{
+    .van-notice-bar{
+      height: 100%;
+    }
+  }
+}
+.extra{
+  display: flex!important;
+  align-items: center;
+  img{
+    width: 2rem;
+    height: auto;
+  }
+}
+.song {
+  :global{
+    .van-cell{
+      height: 70px;
+    }
+  }
+  /deep/ .van-cell {
+      &.playing{
+          color: #FF3838;
+          font-weight: 500;
+          .iconMusic{
+              img{
+                  animation: spin 2s linear infinite;
+                  animation-play-state: paused;
+              }
+          }
+          &.active{
+              .iconMusic{
+                  img{
+                      animation-play-state: running;
+                  }
+              }
+          }
+      }
+  }
+}
+.vipTip {
+  text-align: center;
+  padding: 12px 0;
+  font-size: 14px;
+  color: #808080;
+  background-color: #F3F4F8;
+  .strong{
+      color: #FF4F4F;
+      font-weight: 500;
+  }
+}
+.search{
+  // background-color: #F8F9FC;
+  padding-right: 8px;
+  /deep/ .van-field__left-icon{
+      i{
+          font-size: 18px;
+          color: #999;
+      }
+  }
+  /deep/ .van-search__content {
+      // border-radius: 0.06rem;
+      background-color: #f3f4f8;
+      .van-cell {
+        background-color: #fff !important;
+      }
+  }
+  /deep/ .van-search__action{
+      font-size: 15px;
+      // padding: 0;
+      // padding-left: .12rem;
+      color: #333;
+      // display: none;
+      // position: absolute;
+      // right: 20px;
+  }
+  :global{
+    .van-search__content{
+      padding-left: 0;
+    }
+    .van-search__label{
+      display: flex;
+      align-items: center;
+      padding-right: 10px;
+      .van-cell{
+        padding: 0.26667rem 0.42667rem;
+      }
+    }
+    .van-dropdown-menu__title{
+      font-size: 12px;
+    }
+    .van-dropdown-menu{
+      height: 100%;
+    }
+    .van-dropdown-menu__bar{
+      height: 100%;
+      background-color: transparent;
+      padding: 0; // 为了处理下拉框看上去居中
+    }
+    .van-search {
+        flex: 1;
+        background: #F8F9FC;
+        margin: 15px 0;
+        border-radius: 50%;
+        overflow: hidden;
+        .van-cell {
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
+
+        .van-field__control {
+            font-size: 16px;
+        }
+      }
+      .van-cell__value {
+          height: 24px !important;
+      }
+      .van-search__action {
+          color: #333;
+          background-color: #fff;
+          border-radius: 0 20px 20px 0;
+      }
+      .van-search__content {
+          background-color: #fff !important;
+          border-radius: 20px 0 0 20px;
+          overflow: hidden;
+      }
+      .van-field__left-icon {
+          margin-top: 1px;
+          height: 21px;
+          margin-left: 8px;
+      }
+      .van-field__left-icon {
+        color: #2DC7AA;
+      }
+  }
+  .search_btn {
+    background: #01C1B5;
+    font-size: 14px;
+    color: #fff;
+    padding: 4px 9px;
+    border-radius: 15px;
+  }
+}
+/deep/.van-tab {
+  padding: 0 10px;
+}
+.item{
+  padding: 14px 18px;
+  .title{
+      font-size: 16px;
+  }
+}
+
+/deep/.van-cell {
+  justify-content: center;
+  align-items: center;
+}
+.iconSearch {
+  width: 20px;
+  height: 20px;
+  /deep/.van-icon__image {
+    margin: auto;
+    vertical-align: middle;
+    // padding-right: .1rem;
+  }
+}
+.iconMusic {
+  /deep/.van-icon__image {
+      width: 3px;
+      height: 3px;
+      margin: auto;
+      vertical-align: middle;
+      // padding-right: .1rem;
+  }
+}
+
+.icon-status{
+  position: relative;
+  font-size: 36px;
+  display: flex;
+  :global{
+    .van-badge__wrapper{
+      margin: auto;
+    }
+  }
+}
+
+.audio-container {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  box-shadow: 0 5px 11px 2px #ccc;
+  visibility: visible;
+  opacity: 1;
+  &.hidden{
+      opacity: 0;
+      visibility: hidden;
+  }
+}
+.searchSelect{
+  :global{
+    .van-dropdown-menu__bar{
+      height: auto;
+    }
+  }
+}
+
+.tags{
+  :global(.van-dropdown-item__content) {
+    display: flex;
+    box-sizing: border-box;
+    // justify-content: space-between;
+    padding: 20px;
+    padding-top: 0;
+    // flex-direction: column;
+    flex-wrap: wrap;
+    >span{
+      box-sizing: border-box;
+      display: inline-block;
+      width: 30%;
+      height: 36px;
+      line-height: 36px;
+      margin-top: 10px;
+      border-radius: 2px;
+      // border: 1px solid #E2E0E0;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: pre;
+      &:not(:nth-child(3n)) {
+        margin-right: calc(10% / 2);
+      }
+      // margin-right: 10px;
+    }
+  }
+}
+
+.icon{
+  display: flex;
+  align-items: center;
+  margin-left: 15PX;
+  img{
+    width: 40PX;
+    margin-top: -2PX;
+  }
+}
+
+.notice{
+  padding-left: 4PX!important;
+}

+ 584 - 0
src/views/choise-homework/music-list/index.tsx

@@ -0,0 +1,584 @@
+import { defineComponent } from 'vue';
+import {
+  Sticky,
+  Search,
+  List,
+  Empty,
+  CellGroup,
+  Cell,
+  Icon,
+  NoticeBar,
+  DropdownMenu,
+  DropdownItem,
+  Popup,
+  showLoadingToast,
+  closeToast,
+  showToast
+} from 'vant';
+import qs from 'query-string';
+import { find } from 'lodash';
+import requestOrigin from 'umi-request';
+import { postMessage } from '@/helpers/native-message';
+import request from '@/helpers/request';
+import { state as baseState } from '@/state';
+import state, { getInitState, appState } from '../pageState';
+import ChoosePartName from './modals/choosePartName';
+import VipIcon from './icons/vip.png';
+import iconSearch from './icons/icon_search.png';
+import styles from './index.module.less';
+import MusicIcon from './icons/music.png';
+import { browser } from '@/helpers/utils';
+import OHeader from '@/components/o-header';
+import OSticky from '@/components/o-sticky';
+
+const searchParse = qs.parse(location.search);
+// const isTestTeacher = false;
+
+export default defineComponent({
+  name: 'music-list',
+  data() {
+    return {
+      pageInfo: {
+        page: 1,
+        rows: 20
+      },
+      // loading: false,
+      firstLoading: true,
+      show: true,
+      isFirstLoad: false,
+      userinfo: null,
+      isApp: browser().isApp,
+      tempLevelId: 0,
+      chooseShow: false,
+      partNames: [] as string[],
+      selectedPartName: '',
+      selectedPartIndex: 0,
+      activeRow: null as any,
+      liveConfig: false,
+      musicScoreList: []
+    };
+  },
+  created() {
+    const searchParse = qs.parse(location.search);
+    this.liveConfig = !!searchParse.liveConfig;
+    // console.log(this.liveConfig, " this.liveConfig");
+    // console.log(searchParse, "parseSearch");
+  },
+  async mounted() {
+    await this.initList();
+  },
+  methods: {
+    async initList() {
+      const initState = getInitState();
+      for (const key in initState) {
+        if (Object.prototype.hasOwnProperty.call(initState, key)) {
+          state[key] = initState[key];
+        }
+      }
+
+      this.firstLoading = true;
+      try {
+        state.parentId = this.$route.params.id || 1;
+        state.list = [];
+        await this.FetchListTree();
+        await this.FetchLevel();
+        await this.FetchList();
+      } catch {
+        //
+      }
+      this.firstLoading = false;
+
+      window.scrollTo(1, 0);
+    },
+    async FetchListTree() {
+      try {
+        const musicScoreList = sessionStorage.getItem('musicScoreList');
+        const musicList = musicScoreList ? JSON.parse(musicScoreList) : [];
+        let childClass = [];
+        for (const music of musicList) {
+          if (music.id == state.parentId) {
+            childClass = music.sysMusicScoreCategoriesList || [];
+          }
+        }
+        if (childClass.length > 0) {
+          this.musicScoreList = childClass || [];
+        } else {
+          const { data } = await request.get(
+            baseState.platformApi + '/sysMusicScoreCategories/queryTree1',
+            {
+              requestType: 'form',
+              params: {
+                parentId: this.$route.params.id
+              }
+            }
+          );
+          this.musicScoreList = data || [];
+        }
+        // console.log(1);
+      } catch (error) {
+        console.log(error);
+      }
+    },
+    async FetchLevel() {
+      try {
+        const childClass = this.musicScoreList || [];
+        // console.log(childClass);
+        state.levelOptions = [
+          ...childClass.map((item: any) => ({
+            value: item.id,
+            text: item.name,
+            childs: item.sysMusicScoreCategoriesList
+          }))
+        ];
+        if (state.levelOptions.length && !state.levelId) {
+          state.levelId = state.levelOptions[0].value;
+          const active = find(state.levelOptions, { value: state.levelId });
+          if (active) {
+            if (active.childs) {
+              // console.log(active, "showInfo");
+              state.typeOptions = [
+                { value: 0, text: '全部' },
+                ...active.childs.map((item: any) => ({
+                  value: item.id,
+                  text: item.name
+                }))
+              ];
+            } else {
+              state.typeOptions = null;
+            }
+          }
+        }
+        // console.log(2);
+      } catch (error) {
+        //
+      }
+    },
+    async FetchCats() {
+      try {
+        const res = await request.get(
+          baseState.platformApi + '/sysMusicScoreAccompaniment/querySubjectIds',
+          {
+            params: {
+              categoriesId: this.$route.params.id || 1
+            }
+          }
+        );
+        appState.subjectOptions = [
+          { value: 0, text: '全部声部' },
+          ...res.data
+            .filter((item: any) => !!item)
+            .map((item: any) => ({
+              value: item.id,
+              text: item.name
+            }))
+        ];
+      } catch (error) {
+        //
+      }
+    },
+    async FetchList() {
+      state.error = false;
+      if (state.loading) {
+        return;
+      }
+      state.loading = true;
+      // console.log(appState.subjectOptions);
+      // if (getRequestHostname() !== '/api-student') {
+      await this.FetchCats();
+      // }
+
+      // if (getRequestHostname() === '/api-student') {
+      //   // 因为学生端加了一个扩展声部
+      //   appState.subjectOptions = [
+      //     ...appState.extSubjectIds
+      //       .filter((item: any) => !!item)
+      //       .map((item: any) => ({
+      //         value: item.id,
+      //         text: item.name
+      //       }))
+      //   ];
+      // }
+      // if (getRequestHostname() != '/api-student') {
+      // 每次请求前设置,确保列表中有值。不在声部列表中清空掉,此处不会影响详情
+      const ids: number[] = appState.subjectOptions
+        .map((item: any) => item.value)
+        .filter((item: number) => item > 0);
+      // console.log(ids, appState.subjectId);
+      if (ids.includes(5)) {
+        ids.push(6);
+      }
+      if (!ids.includes(appState.subjectId)) {
+        appState.subjectId = 0;
+      }
+      try {
+        state.subjectId = appState.subjectId;
+        const params = state.params;
+        const parentId = this.$route.params.id;
+        // 如果根级分类没有,则取父级分类
+        // let categoriesId = 181
+        // console.log(3);
+        // console.log(state.levelId, state.typeId, parentId);
+        const categoriesId =
+          (state.levelId || state.typeId) === 0
+            ? undefined
+            : state.typeId || state.levelId;
+        // 如果是合奏则不显示声部,且搜索条件也去掉
+        const subjectId =
+          appState.subjectId === 0 || this.$route.params.id == '43'
+            ? undefined
+            : appState.subjectId;
+
+        const res = await request.get(
+          baseState.platformApi + '/sysMusicScore/queryPage2',
+          {
+            params: {
+              ...params,
+              rows: 20,
+              // ...this.pageInfo,
+              clientType: 'SMART_PRACTICE',
+              subjectId,
+              categoriesId,
+              search: state.search
+            }
+          }
+        );
+        const { data } = res;
+        // 处理接口重复请求,过滤数据
+        if (state.list.length > 0 && data.pageNo == 1) {
+          return;
+        }
+        state.list = [...state.list, ...data.rows];
+        if (params.page >= Math.ceil(data.total / 20)) {
+          state.finished = true;
+          // Toast('列表加载完成')
+        }
+        state.params.page = data.nextPage;
+        // this.pageInfo.page = this.pageInfo.page + 1
+        if (state.list.length <= 0) {
+          this.show = false;
+        }
+      } catch (e) {
+        state.error = true;
+      }
+      state.loading = false;
+    },
+    async getPartNames(xmlUrl: string) {
+      const partNames: string[] = [];
+      showLoadingToast({
+        type: 'loading',
+        overlay: true
+      });
+      try {
+        const res = await requestOrigin.get(xmlUrl, { mode: 'cors' });
+        const xml: any = new DOMParser().parseFromString(res, 'text/xml');
+
+        for (const item of xml.getElementsByTagName('part-name')) {
+          if (item.textContent) {
+            partNames.push(item.textContent);
+          }
+        }
+        closeToast();
+      } catch (error) {
+        closeToast();
+        showToast('读取分谱信息失败,请重试');
+      }
+      return partNames;
+    },
+    async openDetail() {
+      const row = this.activeRow;
+      const _id = row.id + '';
+
+      // if (searchParse.mode === 'select') {
+      const res = await requestOrigin.get(row.xmlUrl, {
+        mode: 'cors'
+      });
+      const xmlParse = new DOMParser().parseFromString(res, 'text/xml');
+      const parts = xmlParse.getElementsByTagName('part');
+      const firstMeasures = parts[0]?.getElementsByTagName('measure');
+      const body = {
+        api: 'onAccompanySelectResult',
+        content: {
+          id: _id,
+          name: row.name,
+          noteLength: firstMeasures.length,
+          partIndex: this.selectedPartIndex + ''
+        }
+      };
+      postMessage(body);
+      return;
+      // }
+      // try {
+      //   const needIds = row.rankIds?.split(",")?.filter(Boolean) || [];
+      //   const sid = String(appState.user?.student?.memberRankSettingId);
+      //   if (Array.isArray(needIds) && needIds.length && appState.user && !needIds.includes(sid) && getRequestHostname() === "/api-student") {
+      //     detailState.vipShow = true;
+      //     return;
+      //   }
+      // } catch (error) {}
+
+      // if (browser().isApp) {
+      //   const url = `${location.origin}${
+      //     location.pathname
+      //   }?campId=${sessionStorage.getItem('campId')}#/musicDetail?musicId=${
+      //     row.id
+      //   }`;
+      //   postMessage({
+      //     api: 'openWebView',
+      //     content: {
+      //       url,
+      //       orientation: 1,
+      //       isHideTitle: true,
+      //       statusBarTextColor: false,
+      //       isOpenLight: false
+      //     }
+      //   });
+      // } else {
+      //   (this as any).$router.push({
+      //     path: '/musicDetail',
+      //     query: {
+      //       musicId: row.id
+      //     }
+      //   });
+      // }
+    },
+    async toDetail(row: any) {
+      this.activeRow = { ...row };
+
+      this.selectedPartName = '';
+      this.selectedPartIndex = 0;
+      const partNames = await this.getPartNames(row.xmlUrl);
+      this.partNames = partNames;
+      let multitrack = false;
+      try {
+        const _multitrack = JSON.parse(row.extConfigJson).multitrack;
+        multitrack = _multitrack > 1 ? true : false;
+      } catch (error) {
+        //
+      }
+      // 多声轨,  不是单声部多声轨, 不是老师布置作业选择曲谱
+
+      this.selectedPartName = partNames[0];
+
+      // this.openDetail();
+    },
+    onSelectedPartName(part: number) {
+      this.selectedPartIndex = part;
+      // this.selectedPartName = partNames[part]
+      this.chooseShow = false;
+      this.openDetail();
+    },
+    onPayVideo(evt: MouseEvent, data: any) {
+      evt.stopPropagation();
+      postMessage({
+        api: 'recordHomeworkVideo',
+        content: {
+          ...data,
+          partIndex: this.selectedPartIndex
+        }
+      });
+    },
+    openRecordingWebview(evt: MouseEvent, row: any) {
+      evt.stopPropagation();
+      postMessage({
+        api: 'recordHomeworkVideo',
+        content: {
+          id: row.id,
+          partIndex: this.selectedPartIndex
+          // url: location.origin + '/accompany/#/detail/' + row.examSongId + '?mode=homework',
+          // orientation: 0,
+          // isHideTitle: true,
+          // statusBarTextColor: false,
+          // isOpenLight: true,
+        }
+      });
+    },
+    onSearch() {
+      state.params.page = 1;
+      state.list = [];
+      this.show = true;
+      state.finished = false;
+      state.loading = false;
+      this.FetchList();
+    },
+    subjectChange(val: number) {
+      appState.subjectId = val;
+      this.onSearch();
+    },
+    onClickLeft() {
+      (this as any).$router.replace('/');
+    },
+    goBack() {
+      // (this as any).$router.push({
+      //   path:'/',
+      //   query: {
+      //     ...this.$route.query,
+      //   },
+      // })
+      (this as any).$router.go(-1);
+    }
+  },
+  render() {
+    return (
+      <div class={styles.accompany}>
+        <Popup
+          show={this.chooseShow}
+          teleport="body"
+          closeable
+          style={{
+            borderRadius: '8px'
+          }}
+          onClickOverlay={() => (this.chooseShow = false)}
+          onClickCloseIcon={() => (this.chooseShow = false)}>
+          <ChoosePartName
+            partNames={this.partNames}
+            onSelectedPartName={this.onSelectedPartName}
+          />
+        </Popup>
+
+        <OSticky position="top">
+          <OHeader
+            isBack={true}
+            border={false}
+            isFixed={false}
+            backIconColor="white"></OHeader>
+          {state.typeOptions ||
+          (state.levelOptions && state.levelOptions.length > 0) ? (
+            <DropdownMenu activeColor="#01C1B5">
+              {state.levelOptions && state.levelOptions.length > 0 ? (
+                <DropdownItem
+                  modelValue={state.levelId}
+                  options={state.levelOptions}
+                  onChange={(val: number) => {
+                    state.levelId = val;
+                    state.typeId = 0;
+                    const active = find(state.levelOptions, { value: val });
+                    if (active) {
+                      if (active.childs) {
+                        state.typeOptions = [
+                          { value: 0, text: '全部' },
+                          ...active.childs.map((item: any) => ({
+                            value: item.id,
+                            text: item.name
+                          }))
+                        ];
+                      } else {
+                        state.typeOptions = null;
+                      }
+                    }
+                    this.onSearch();
+                  }}
+                />
+              ) : null}
+              {state.typeOptions ? (
+                <DropdownItem
+                  class={styles.searchSelect}
+                  get-container="#app"
+                  modelValue={state.typeId}
+                  options={state.typeOptions}
+                  onChange={(val: any) => {
+                    state.typeId = val;
+                    this.onSearch();
+                  }}
+                />
+              ) : null}
+            </DropdownMenu>
+          ) : null}
+          <Search
+            class={[styles.search]}
+            placeholder="请输入搜索关键词"
+            modelValue={state.search}
+            background="#F8F9FC"
+            onUpdate:model-value={(text: string) => (state.search = text)}
+            showAction
+            onSearch={this.onSearch}
+            vSlots={{
+              'left-icon': () => (
+                <img class={styles.iconSearch} src={iconSearch} />
+              ),
+              label: () => {
+                return (
+                  <DropdownMenu
+                    activeColor="#01C1B5"
+                    onClick={(evt: MouseEvent) => {
+                      evt.preventDefault();
+                    }}>
+                    <DropdownItem
+                      class={styles.searchSelect}
+                      get-container="#app"
+                      modelValue={appState.subjectId}
+                      options={appState.subjectOptions}
+                      onChange={(val: any) => {
+                        appState.subjectId = val;
+                        this.onSearch();
+                      }}
+                    />
+                  </DropdownMenu>
+                );
+              },
+              action: () => (
+                <span class={styles.search_btn} onClick={this.onSearch}>
+                  搜索
+                </span>
+              )
+            }}
+          />
+        </OSticky>
+
+        <div class={[styles.accompanyList, styles.song]}>
+          {this.show ? (
+            <List
+              loading={state.loading}
+              finishedText="加载完毕"
+              error={state.error}
+              offset={100}
+              finished={state.finished}
+              immediateCheck={false}
+              onLoad={() => {
+                if (!this.firstLoading) this.FetchList();
+              }}
+              vSlots={{
+                error: () => (
+                  <span onClick={this.FetchList}>加载失败,请点击重试</span>
+                )
+              }}>
+              <CellGroup>
+                {state.list.map((item: any) => (
+                  <Cell
+                    style={{ display: item.id ? '' : 'none' }}
+                    size="large"
+                    onClick={() => this.toDetail(item)}
+                    vSlots={{
+                      icon: () => (
+                        <div class={styles['icon-status']}>
+                          <Icon class={styles.iconMusic} name={MusicIcon} />
+                          {item.rankIds ? (
+                            <div class={styles.icon}>
+                              <img src={VipIcon} />
+                            </div>
+                          ) : null}
+                        </div>
+                      ),
+                      title: () => (
+                        <NoticeBar
+                          background="none"
+                          color="#444"
+                          style={{
+                            paddingLeft:
+                              (item.rankIds ? '4PX' : '15PX') + '!important'
+                          }}
+                          text={item.name}
+                          key="notactive"
+                        />
+                      )
+                    }}></Cell>
+                ))}
+              </CellGroup>
+            </List>
+          ) : (
+            <Empty description="暂无数据" />
+          )}
+        </div>
+      </div>
+    );
+  }
+});

+ 31 - 0
src/views/choise-homework/music-list/modals/choosePartName/index-rem.module.less

@@ -0,0 +1,31 @@
+.container{
+  width: 312px;
+  border-radius: 8px;
+  padding: 23px 17px;
+  box-sizing: border-box;
+  h3{
+    margin: 0;
+    font-size: 18px;
+    color: #333333;
+    font-weight: normal;
+    display: flex;
+    align-items: center;
+    &::before{
+      content: '';
+      width: 4px;
+      height: 18px;
+      border-radius: 2px;
+      background: #01C1B5;
+      margin-right: 6px;
+      display: block;
+    }
+  }
+  .picker{
+    padding: 10px;
+  }
+  .button{
+    width: 260px;
+    height: 42px;
+    margin: auto;
+  }
+}

+ 44 - 0
src/views/choise-homework/music-list/modals/choosePartName/index.module.less

@@ -0,0 +1,44 @@
+.container{
+  width: 312PX;
+  border-radius: 8PX;
+  padding: 23PX 17PX;
+  box-sizing: border-box;
+  h3{
+    margin: 0;
+    font-size: 18PX;
+    color: #333333;
+    font-weight: normal;
+    display: flex;
+    align-items: center;
+    &::before{
+      content: '';
+      width: 4PX;
+      height: 18PX;
+      border-radius: 2PX;
+      background: #01C1B5;
+      margin-right: 6PX;
+      display: block;
+    }
+  }
+  .picker{
+    padding: 10PX;
+    :global{
+      // .van-picker__columns{
+      //   max-height: 50vh;
+      // }
+      .van-picker-column__item{
+        font-size: 18PX;
+      }
+    }
+  }
+  .button{
+    width: 260PX;
+    height: 42PX;
+    margin: auto;
+    :global{
+      .van-button__text{
+        font-size: 18PX;
+      }
+    }
+  }
+}

+ 64 - 0
src/views/choise-homework/music-list/modals/choosePartName/index.tsx

@@ -0,0 +1,64 @@
+import { defineComponent } from 'vue';
+import { Picker, Button } from 'vant';
+import styles from './index.module.less';
+import stylesRem from './index-rem.module.less';
+import { getVoiceChinesName } from '@/views/choise-homework/pageState';
+
+export default defineComponent({
+  name: 'choosePartName',
+  props: {
+    isRem: {
+      type: Boolean,
+      default: true
+    },
+    partNames: {
+      type: Array,
+      default: () => [] as string[]
+    }
+  },
+  emits: ['selectedPartName'],
+  mounted() {
+    if (this.isRem) {
+      this.styles = stylesRem;
+    } else {
+      this.styles = styles;
+    }
+  },
+  data() {
+    return {
+      selectedPart: 0,
+      styles: {} as any
+    };
+  },
+  render() {
+    const styles = this.styles;
+    return (
+      <div class={styles.container}>
+        <h3>请选择您练习的分谱</h3>
+        <Picker
+          class={styles.picker}
+          showToolbar={false}
+          columns={this.partNames
+            .filter((text: string) => text.toLocaleUpperCase() !== 'COMMON')
+            .map((partName, index) => ({
+              text: getVoiceChinesName(partName as string),
+              value: index
+            }))}
+          onChange={val => {
+            this.selectedPart = val.value;
+          }}
+          visibleOptionNum={this.isRem ? 6 : 4}
+        />
+        <Button
+          class={styles.button}
+          type="primary"
+          round
+          block
+          color="#01C1B5"
+          onClick={() => this.$emit('selectedPartName', this.selectedPart)}>
+          确定
+        </Button>
+      </div>
+    );
+  }
+});

+ 92 - 0
src/views/choise-homework/pageState.tsx

@@ -0,0 +1,92 @@
+import { reactive, watch } from 'vue';
+import storejs from 'store';
+import deepClone from '@/helpers/deep-clone';
+
+export const getVoiceChinesName = (
+  name: string | undefined,
+  onlyName?: boolean
+) => {
+  let viewname = name || '';
+  if (name) {
+    const cname = appState.chinesePartName[name];
+    if (!cname) {
+      const names = Object.keys(appState.chinesePartName);
+      for (const n of names) {
+        if (name.match(n)) {
+          // console.table({
+          //   输入名称: name,
+          //   循环名称: n,
+          //   匹配: name.match(n)
+          // })
+          viewname = name.replace(n, appState.chinesePartName[n]);
+          break;
+        }
+      }
+    } else {
+      viewname = cname;
+    }
+  }
+  if (onlyName) {
+    return viewname ? viewname : '';
+  } else {
+    return viewname
+      ? name + (name !== viewname ? ' (' + viewname + ')' : '')
+      : '';
+  }
+};
+
+export const appState = reactive({
+  subjectId: 0,
+  subjectLoading: false,
+  origanSubjectId: 0, // 原始用户声部
+  subjectOptions: [{ value: 0, text: '全部声部' }] as any[],
+  chinesePartName: {} as any,
+  MusicalInstrumentClassification: {} as any,
+  tenantId: null as any,
+  organId: null as any,
+  extSubjectIds: [] as any[] // 学生的扩展声部
+});
+
+const initState = {
+  list: [] as any[],
+  params: {
+    page: 1,
+    rows: 10
+  },
+  error: false,
+  levelId: 0,
+  typeId: 0,
+  levelOptions: [] as any[],
+  typeOptions: null as null | any[],
+  finished: false,
+  loading: false,
+  search: '',
+  subjectId: 0,
+  parentId: 1
+};
+
+export const getInitState = () => deepClone(initState);
+
+const state = reactive(getInitState());
+
+watch(state, () => {
+  storejs.set('state', state);
+});
+
+export const save = () => {
+  storejs.set('state', state);
+};
+
+export const sync = () => {
+  const oldState = storejs.get('state');
+  if (oldState) {
+    for (const key in oldState) {
+      if (Object.prototype.hasOwnProperty.call(oldState, key)) {
+        const item: any = oldState[key];
+        (state as any)[key] = item;
+      }
+    }
+  }
+};
+
+export default state;

+ 173 - 24
src/views/coursewarePlay/index.tsx

@@ -43,6 +43,8 @@ import Tool, { ToolItem, ToolType } from './component/tool';
 import Pen from './component/tools/pen';
 // import VideoItem from './component/video-item';
 import VideoPlay from './component/video-play';
+import deepClone from '@/helpers/deep-clone';
+import { useInterval, useIntervalFn } from '@vueuse/core';
 
 export default defineComponent({
   name: 'CoursewarePlay',
@@ -122,7 +124,7 @@ export default defineComponent({
       knowledgePointList: [] as any,
       itemList: [] as any,
       showHead: true,
-      isCourse: false,
+      // isCourse: false,
       isRecordPlay: false,
       videoRefs: {},
 
@@ -766,6 +768,152 @@ export default defineComponent({
       return {};
     });
     let closeModelTimer: any = null;
+
+    /**
+     * 统计视频播放时间段
+     */
+    const intervalFnRef = ref(); // 定时任务
+    // 播放视频总时长
+    const videoIntervalRef = useInterval(1000, { controls: true });
+    videoIntervalRef.pause();
+    /**
+     * 格式化视屏播放有效时间 - 合并区间
+     * @param intervals [[], []]
+     * @example [[4, 8],[0, 4],[10, 30]]
+     * @returns [[0, 8], [10, 30]]
+     */
+    const formatEffectiveTime = (intervals: any[]) => {
+      const res: any = [];
+      intervals.sort((a, b) => a[0] - b[0]);
+      let prev = intervals[0];
+      for (let i = 1; i < intervals.length; i++) {
+        const cur = intervals[i];
+        if (prev[1] >= cur[0]) {
+          // 有重合
+          prev[1] = Math.max(cur[1], prev[1]);
+        } else {
+          // 不重合,prev推入res数组
+          res.push(prev);
+          prev = cur; // 更新 prev
+        }
+      }
+      res.push(prev);
+      // console.log(res, 'formatEffectiveTime')
+
+      return res;
+    };
+    /**
+     * 获取数据有效期
+     * @param intervals [[], []]
+     * @returns 0s
+     */
+    const formatTimer = (intervals: any[]) => {
+      const afterIntervals = formatEffectiveTime(intervals);
+      let time = 0;
+      afterIntervals.forEach((t: any) => {
+        time += t[1] - t[0];
+      });
+      return time;
+    };
+
+    // 保存零时时间
+    // const moreTime: any = ref([]) // 多个观看时间段 已经放到列表里面了
+    let tempTime: any = []; // 临时存储时间
+    const currentTimer = useInterval(1000, { controls: true });
+    // 监听播放状态,
+    watch(
+      () => videoIntervalRef.isActive.value,
+      (newVal: boolean) => {
+        initVideoCount(newVal);
+      }
+    );
+
+    /**
+     * 初始化视频时长
+     * @param newVal 播放状态
+     * @param repeat 是否为定时发送的
+     */
+    const initVideoCount = (newVal: any, repeat = false) => {
+      // console.log('watch', forms.player.currentTime)
+      const activeVideoRef = data.videoItemRef?.getPlyrRef();
+      const initTime = deepClone(tempTime);
+      if (repeat) {
+        if (tempTime.length > 0) {
+          // console.log('join video', tempTime, 'initTime', initTime)
+          tempTime[1] = Math.floor(activeVideoRef.currentTime());
+        }
+      } else {
+        if (newVal) {
+          tempTime[0] = Math.floor(activeVideoRef.currentTime());
+        } else {
+          tempTime[1] = Math.floor(activeVideoRef.currentTime());
+        }
+      }
+
+      if (tempTime.length >= 2) {
+        // console.log(tempTime, 'tempTime', moreTime.value)
+        // 处理在短时间内的时间差 【视屏拖动,点击可能会导致时间差太大】
+        const diffTime =
+          tempTime[1] - tempTime[0] - currentTimer.counter.value > 2;
+        // 结束时间,如果 大于开始时间则清除
+        if (tempTime[1] >= tempTime[0] && !diffTime) {
+          data.itemList[popupData.activeIndex].moreTime.push(tempTime);
+          // moreTime.value.push(tempTime)
+        }
+        if (repeat) {
+          tempTime = deepClone(initTime);
+        } else {
+          tempTime = [];
+          currentTimer.counter.value = 0;
+        }
+      }
+    };
+    // 更新时间
+    const updateStat = async () => {
+      try {
+        const itemList = data.itemList;
+        const params: any = [];
+        itemList.forEach((item: any) => {
+          if (item.moreTime.length > 0) {
+            const videoBrowseData = formatEffectiveTime(item.moreTime);
+            const time =
+              videoBrowseData.length > 0 ? formatTimer(videoBrowseData) : 0;
+            const temp = {
+              lessonCoursewareDetailId: route.query.id,
+              browseTime: time, // 播放时长
+              videoBrowseData: JSON.stringify(videoBrowseData), // 播放的数据
+              videoTime: item.videoTime, // 视频时长
+              materialId: item.materialId
+            };
+            params.push(temp);
+          }
+        });
+
+        // 只有学生才统计数据
+        if (params.length > 0 && state.platformType === 'STUDENT') {
+          await request.post(
+            `${state.platformApi}/studentCoursewareMaterialRelation/save`,
+            {
+              data: params
+            }
+          );
+        }
+      } catch {
+        //
+      }
+    };
+
+    onMounted(() => {
+      // 间隔多少时间同步数据
+      intervalFnRef.value = useIntervalFn(async () => {
+        // 同步数据时先进行有效时间进行保存
+        initVideoCount(false, true);
+
+        await updateStat();
+        videoIntervalRef.counter.value = 0;
+      }, 10000);
+    });
+    /** 统计视频播放时间段 */
     return () => (
       <div id="playContent" class={styles.playContent}>
         <div
@@ -797,25 +945,6 @@ export default defineComponent({
                   : { opacity: 0, zIndex: -1 }
               }
               class={styles.itemDiv}>
-              {/* <VideoItem
-                ref={(el: any) => (data.videoItemRef = el)}
-                item={activeVideoItem.value}
-                activeModel={activeData.model}
-                onClose={setModelOpen}
-                onPlay={() => {
-                  data.videoState = 'play';
-                }}
-                onPause={() => {
-                  clearTimeout(activeData.timer);
-                  activeData.model = true;
-                }}
-                onEnded={() => {
-                  const _index = popupData.activeIndex + 1;
-                  if (_index < data.itemList.length) {
-                    handleSwipeChange(_index);
-                  }
-                }}
-              /> */}
               <VideoPlay
                 ref={(el: any) => (data.videoItemRef = el)}
                 item={activeVideoItem.value}
@@ -831,9 +960,29 @@ export default defineComponent({
                     activeVideoItem.value.isprepare = true;
                   }
                 }}
+                onSeeked={() => {
+                  videoIntervalRef.isActive.value && videoIntervalRef.pause();
+                }}
+                onSeeking={() => {
+                  videoIntervalRef.isActive.value && videoIntervalRef.pause();
+                }}
+                onWaiting={() => {
+                  videoIntervalRef.isActive.value && videoIntervalRef.pause();
+                }}
+                onTimeupdate={() => {
+                  const activeVideoRef = data.videoItemRef?.getPlyrRef();
+                  if (
+                    !videoIntervalRef.isActive.value &&
+                    activeVideoRef?.currentTime() > 0 &&
+                    !activeVideoRef?.paused()
+                  ) {
+                    videoIntervalRef.resume();
+                  }
+                }}
                 onPause={() => {
                   clearTimeout(activeData.timer);
                   activeData.model = true;
+                  videoIntervalRef.pause();
                 }}
                 onEnded={async () => {
                   const _index = popupData.activeIndex + 1;
@@ -964,7 +1113,7 @@ export default defineComponent({
                       <img src={iconTouping} />
                       <span>投屏</span>
                     </div> */}
-                  {data.isCourse && (
+                  {/* {data.isCourse && (
                     <>
                       <div
                         class={styles.fullBtn}
@@ -979,7 +1128,7 @@ export default defineComponent({
                         <span>签退</span>
                       </div>
                     </>
-                  )}
+                  )} */}
                 </div>
               </div>
             )}
@@ -1038,9 +1187,9 @@ export default defineComponent({
             <Icon name={iconBack} />
             返回
           </div>
-          {data.isCourse && (
+          {/* {data.isCourse && (
             <PlayRecordTime ref={playRef} list={data.knowledgePointList} />
-          )}
+          )} */}
           <div
             class={styles.menu}
             onClick={() => {

+ 306 - 0
src/views/exercise-after-class/index.module.less

@@ -0,0 +1,306 @@
+.playContent {
+  width: 100vw;
+  height: 100vh;
+  background-color: #000;
+  overflow: hidden;
+}
+
+.coursewarePlay {
+  position: relative;
+  height: 100vh;
+  margin: 0 auto;
+  overflow: hidden;
+}
+
+.playModel {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  box-shadow: inset 0px 0px 164px 0px rgba(0, 0, 0, 1);
+  pointer-events: none;
+}
+
+.headerContainer {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  z-index: 1;
+  padding: 10px 24px;
+  display: flex;
+  align-items: center;
+  color: #fff;
+  font-size: 12px;
+  background: linear-gradient(180deg, rgba(0, 0, 0, 0.6), transparent);
+}
+
+.backBtn {
+  color: #fff;
+  width: 40px;
+  height: 26px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  z-index: 10;
+}
+
+// .btnGroup {
+//   position: absolute;
+//   top: 50%;
+//   right: 12px;
+//   z-index: 10;
+//   transform: translateY(-50%);
+
+//   .btnItem {
+//     background: rgba(0, 0, 0, 0.3);
+//     border-radius: 6px;
+//     width: 42px;
+//     height: 50px;
+//     font-size: 12px;
+//     color: #FFFFFF;
+//     text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.13);
+//     display: flex;
+//     align-items: center;
+//     flex-direction: column;
+//     justify-content: center;
+//     cursor: pointer;
+//   }
+
+//   .btnImg {
+//     width: 24px;
+//     height: 24px;
+//     margin-bottom: 2px;
+//   }
+// }
+.goPractice {
+  width: 89px;
+  height: 32px;
+  background: url('../coursewarePlay/image/btn_go_practice.png') no-repeat center;
+  background-size: contain;
+  position: absolute;
+  right: 12px;
+  bottom: 70px;
+  z-index: 11;
+  transition: all .5s ease;
+
+  &.hide {
+    transform: translateX(65px);
+  }
+}
+
+.menu {
+  flex: 1;
+  display: flex;
+  justify-content: center;
+  color: #fff;
+}
+
+.tabsContent {
+  width: 100vw;
+  height: 100vh;
+
+  :global {
+    .van-tabs__wrap {
+      display: none !important;
+    }
+
+    .van-tabs__content {
+      width: 100%;
+      height: 100%;
+    }
+  }
+}
+
+.loadWrap {
+  position: absolute;
+  left: 0;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(45deg, #21232a, #111218);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.itemDiv {
+  position: relative;
+  width: 100%;
+  height: 100%;
+
+  video {
+    width: 100%;
+    height: 100%;
+  }
+
+  img {
+    display: block;
+    width: 100%;
+    height: 100%;
+    object-fit: contain;
+  }
+
+  .videoSection {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 8;
+  }
+}
+
+.videoModel {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+
+  &>img {
+    width: 50px;
+    height: 50px;
+  }
+}
+
+.rightFixedBtns {
+  position: fixed;
+  top: 50%;
+  transform: translateY(-50%);
+  right: 20px;
+
+  .point {
+    margin-top: 10px;
+    border-bottom-left-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .point+.fullBtn {
+    border-top-left-radius: 0;
+    border-top-right-radius: 0;
+  }
+}
+
+.leftFixedBtns {
+  position: fixed;
+  top: 50%;
+  transform: translateY(-50%);
+  left: 20px;
+
+  .prePoint {
+    margin-bottom: 8px;
+  }
+}
+
+.fullBtn {
+  width: 38px;
+  height: 55px;
+  background: rgba(51, 51, 51, 0.15);
+  border-radius: 8px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  color: #fff;
+  justify-content: space-evenly;
+
+  &:active {
+    opacity: 0.8;
+  }
+}
+
+.bottomFixedContainer {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  z-index: 10;
+  background: linear-gradient(0deg, rgba(0, 0, 0, 0.5), transparent);
+  padding: 0 30px;
+
+  .time {
+    display: flex;
+    // justify-content: space-between;
+    color: #fff;
+    font-size: 10px;
+    padding: 4px 0;
+  }
+
+  .slider {
+    padding: 10px 0;
+  }
+
+  .actions {
+    display: flex;
+    justify-content: space-between;
+    color: #fff;
+    font-size: 12px;
+    align-items: center;
+
+    .actionBtn {
+      display: flex;
+    }
+
+    .actionBtn>img {
+      width: 26px;
+      height: 26px;
+      display: block;
+      padding: 8px 8px 14px 8px;
+    }
+  }
+}
+
+.popup {
+  background: rgba(0, 0, 0, 0.5);
+}
+
+.overlayClass {
+  --van-overlay-background: transparent;
+}
+
+:global {
+
+  .top-enter-active,
+  .top-leave-active {
+    transition: transform 0.5s;
+  }
+
+  .top-enter-from,
+  .top-leave-to {
+    transform: translateY(-100%);
+  }
+
+  .left-enter-active,
+  .left-leave-active {
+    transition: all 0.5s;
+  }
+
+  .left-enter-from,
+  .left-leave-to {
+    left: -60px;
+  }
+
+  .right-enter-active,
+  .right-leave-active {
+    transition: all 0.5s;
+  }
+
+  .right-enter-from,
+  .right-leave-to {
+    right: -60px;
+  }
+
+  .bottom-enter-active,
+  .bottom-leave-active {
+    transition: transform 0.5s;
+  }
+
+  .bottom-enter-from,
+  .bottom-leave-to {
+    transform: translateY(100%);
+  }
+}

+ 413 - 0
src/views/exercise-after-class/index.tsx

@@ -0,0 +1,413 @@
+import { Icon, showConfirmDialog, showToast, Swipe, SwipeItem } from 'vant';
+import {
+  defineComponent,
+  onMounted,
+  reactive,
+  onUnmounted,
+  ref,
+  Transition,
+  watch
+} from 'vue';
+import styles from './index.module.less';
+import 'plyr/dist/plyr.css';
+import request from '@/helpers/request';
+import { state } from '@/state';
+import { useRoute, useRouter } from 'vue-router';
+import iconBack from '../coursewarePlay/image/back.svg';
+import { postMessage } from '@/helpers/native-message';
+import { browser } from '@/helpers/utils';
+import qs from 'query-string';
+import { Vue3Lottie } from 'vue3-lottie';
+import playLoadData from '../coursewarePlay/datas/data.json';
+// import { handleCheckVip } from '../hook/useFee';
+import VideoClass from './video-class';
+import { usePageVisibility } from '@vant/use';
+
+const materialType = {
+  视频: 'VIDEO',
+  图片: 'IMG',
+  曲目: 'SONG'
+};
+
+export default defineComponent({
+  name: 'exercise-after-class',
+  setup() {
+    const pageVisibility = usePageVisibility();
+    /** 设置播放容器 16:9 */
+    const parentContainer = reactive({
+      width: '100vw'
+    });
+    const setContainer = () => {
+      const min = Math.min(screen.width, screen.height);
+      const max = Math.max(screen.width, screen.height);
+      const width = min * (16 / 9);
+      if (width > max) {
+        parentContainer.width = '100vw';
+        return;
+      } else {
+        parentContainer.width = width + 'px';
+      }
+    };
+    const handleInit = (type = 0) => {
+      setContainer();
+      // 横屏
+      postMessage({
+        api: 'setRequestedOrientation',
+        content: {
+          orientation: type
+        }
+      });
+      // 安卓的状态栏
+      postMessage({
+        api: 'setStatusBarVisibility',
+        content: {
+          isVisibility: type
+        }
+      });
+    };
+    handleInit();
+    onUnmounted(() => {
+      handleInit(1);
+    });
+
+    const route = useRoute();
+    const query = route.query;
+    const browserInfo = browser();
+    const headeRef = ref();
+    const data = reactive({
+      isMember: false, // 是否为会员
+      videoData: null as any,
+      trainings: [] as any[],
+      expireTimeFlag: false, // 作业是否结束
+      trainingTimes: 0,
+      itemList: [] as any,
+      showHead: true,
+      loading: true,
+      recordLoading: false,
+      isPlayBaseStatus: true, // 初始状态是否播放完
+      isPlayAll: true // 是否全部做完
+    });
+    const activeData = reactive({
+      nowTime: 0,
+      model: true, // 遮罩
+      timer: null as any,
+      item: null as any
+    });
+    // 获取课后练习记录
+    const getTrainingRecord = async () => {
+      try {
+        const res: any = await request.post(
+          state.platformApi +
+            `/studentLessonTraining/trainingRecord/${query.courseScheduleId}?userId=${state.user?.data?.id}`,
+          {
+            hideLoading: true
+          }
+        );
+        data.expireTimeFlag = res.data?.expireTimeFlag || false;
+        if (Array.isArray(res?.data?.trainings)) {
+          const trainings = res?.data?.trainings || [];
+          const tempLessonTraining: any = [];
+          trainings.forEach((item: any) => {
+            tempLessonTraining.push(
+              ...(item.studentLessonTrainingDetails || [])
+            );
+          });
+          // 没有播放完
+          tempLessonTraining.forEach((item: any) => {
+            let trainingContent: any = {};
+            try {
+              trainingContent = JSON.parse(item.trainingContent);
+            } catch (error) {
+              trainingContent = '';
+            }
+            if (trainingContent.practiceTimes !== item.trainingTimes + '') {
+              data.isPlayAll = false;
+            }
+
+            if (item.materialId == route.query.materialId) {
+              popupData.tabName = item.knowledgePointName;
+            }
+          });
+
+          return tempLessonTraining;
+        }
+      } catch (error) {
+        //
+      }
+      return [];
+    };
+    const setRecord = async (trainings: any[]) => {
+      if (Array.isArray(trainings)) {
+        data.trainings = trainings.map((n: any) => {
+          const materialRefs = n.materialRefs ? n.materialRefs : [];
+          const materialMusicId =
+            materialRefs.length > 0 ? materialRefs[0].resourceId : null;
+          try {
+            n.trainingContent = JSON.parse(n.trainingContent);
+          } catch (error) {
+            n.trainingContent = '';
+          }
+          return {
+            ...n,
+            materialMusicId,
+            currentTime: 0,
+            duration: 100,
+            paused: true,
+            loop: false,
+            videoEle: null,
+            timer: null,
+            // muted: state.user.data?.vipMember ? false : true, // 静音
+            muted: true,
+            autoplay: state.user.data?.vipMember ? true : false //自动播放
+          };
+        });
+        console.log(data.trainings, 'trainings');
+        data.itemList = data.trainings.filter(
+          (n: any) => n.materialId == route.query.materialId
+        );
+        data.videoData = data.itemList[0];
+        handleExerciseCompleted();
+      }
+    };
+
+    onMounted(async () => {
+      const trainings = await getTrainingRecord();
+      // 初始化状态
+      trainings.forEach((record: any) => {
+        let trainingContent: any = {};
+        try {
+          trainingContent = JSON.parse(record.trainingContent);
+        } catch (error) {
+          trainingContent = '';
+        }
+        if (trainingContent.practiceTimes !== record.trainingTimes + '') {
+          data.isPlayBaseStatus = false;
+        }
+      });
+
+      setRecord(trainings);
+      // 是否为会员
+      // data.isMember = handleCheckVip();
+    });
+    // 返回
+    const goback = () => {
+      postMessage({ api: 'back' });
+    };
+
+    const swipeRef = ref();
+    const popupData = reactive({
+      firstIndex: 0,
+      open: false,
+      activeIndex: -1,
+      tabActive: '',
+      tabName: '',
+      itemActive: '',
+      itemName: ''
+    });
+
+    // 达到指标,记录
+    const addTrainingRecord = async (m: any) => {
+      if (data.recordLoading || data.expireTimeFlag) return;
+      console.log('记录观看次数');
+      data.recordLoading = true;
+      const query = route.query;
+      const body = {
+        materialType: 'VIDEO',
+        record: {
+          sourceTime: m.duration,
+          clientType: state.platformType,
+          feature: 'LESSON_TRAINING',
+          deviceType: browserInfo.android
+            ? 'ANDROID'
+            : browserInfo.isApp
+            ? 'IOS'
+            : 'WEB'
+        },
+        courseScheduleId: query.courseScheduleId,
+        lessonTrainingId: query.lessonTrainingId,
+        materialId: data.videoData?.materialId || ''
+      };
+      try {
+        await request.post(
+          state.platformApi + '/studentLessonTraining/lessonTrainingRecord',
+          {
+            data: body,
+            hideLoading: true
+          }
+        );
+      } catch (error) {
+        //
+      }
+      data.recordLoading = false;
+      try {
+        const trainings: any[] = await getTrainingRecord();
+        if (Array.isArray(trainings)) {
+          const item = trainings.find(
+            (n: any) => n.materialId == data.videoData?.materialId
+          );
+          if (item) {
+            data.videoData.trainingTimes = item.trainingTimes;
+            handleExerciseCompleted();
+          }
+        }
+      } catch (error) {
+        //
+      }
+    };
+    // 停止所有的播放
+    const handleStopVideo = () => {
+      data.itemList.forEach((m: any) => {
+        m.videoEle?.pause();
+      });
+    };
+
+    // 判断练习是否完成
+    const handleExerciseCompleted = () => {
+      if (
+        data?.videoData?.trainingTimes != 0 &&
+        data?.videoData?.trainingTimes + '' ===
+          data.videoData?.trainingContent?.practiceTimes
+      ) {
+        let isLastIndex = false;
+        let itemIndex = 0;
+        // console.log(data.isPlayBaseStatus, data.isPlayAll, data.trainings)
+        if (data.isPlayBaseStatus) {
+          itemIndex = data.trainings.findIndex(
+            (n: any) => n.materialId == data.videoData?.materialId
+          );
+          isLastIndex = itemIndex === data.trainings.length - 1;
+        } else {
+          let i = -1;
+          let status = true;
+          data.trainings.forEach((item: any, index: number) => {
+            if (
+              item.trainingContent.practiceTimes !== item.trainingTimes + '' &&
+              i === -1
+            ) {
+              // console.log(i, item.trainingContent.practiceTimes, item.trainingTimes, index)
+              i = index;
+            }
+            if (
+              item.trainingContent.practiceTimes !==
+              item.trainingTimes + ''
+            ) {
+              status = false;
+            }
+          });
+          itemIndex = i != -1 ? i - 1 : -1;
+          // console.log(status)
+          isLastIndex = status;
+        }
+
+        showConfirmDialog({
+          title: '课后作业',
+          message: '你已完成该练习~',
+          confirmButtonColor: 'var(--van-primary)',
+          confirmButtonText: isLastIndex ? '完成' : '下一题',
+          cancelButtonText: '继续'
+        })
+          .then(() => {
+            if (!isLastIndex) {
+              const nextItem = data.trainings[itemIndex + 1];
+              data.videoData?.expired;
+              if (nextItem.expired) {
+                showToast('该资源已过期');
+                return;
+              }
+              if (nextItem.knowledgePointName) {
+                popupData.tabName = nextItem.knowledgePointName;
+              }
+              if (nextItem?.type === materialType.视频) {
+                data.itemList = [nextItem];
+                data.videoData = nextItem;
+                handleExerciseCompleted();
+              }
+            } else {
+              postMessage({ api: 'goBack' });
+            }
+          })
+          .catch(() => {
+            data.trainings[itemIndex].currentTime = 0;
+          });
+      }
+    };
+
+    watch(pageVisibility, (value: any) => {
+      handleStopVideo();
+      if (value == 'visible') {
+        // 横屏
+        postMessage(
+          {
+            api: 'setRequestedOrientation',
+            content: {
+              orientation: 0
+            }
+          },
+          () => {
+            // console.log(234);
+          }
+        );
+      }
+    });
+
+    return () => (
+      <div class={styles.playContent}>
+        <div
+          class={styles.coursewarePlay}
+          style={{ width: parentContainer.width }}>
+          <Swipe
+            style={{ height: '100%' }}
+            ref={swipeRef}
+            showIndicators={false}
+            loop={false}
+            vertical
+            lazyRender={true}
+            touchable={false}
+            duration={0}>
+            {data.itemList.map((m: any) => {
+              return (
+                <SwipeItem>
+                  <>
+                    <VideoClass
+                      item={m}
+                      isMember={data.isMember}
+                      modal={activeData.model}
+                      onEnded={(m: any) => addTrainingRecord(m)}
+                      onChangeModal={(status: boolean) => {
+                        activeData.model = status;
+                      }}
+                    />
+                    {m.muted && (
+                      <div class={styles.loadWrap}>
+                        <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
+                      </div>
+                    )}
+                  </>
+                </SwipeItem>
+              );
+            })}
+          </Swipe>
+
+          <Transition name="top">
+            {activeData.model && (
+              <div class={styles.headerContainer} ref={headeRef}>
+                <div class={styles.backBtn} onClick={() => goback()}>
+                  <Icon name={iconBack} />
+                  返回
+                </div>
+                <div class={styles.menu}>{popupData.tabName}</div>
+                {/* 判断作业是否过期 */}
+                {!data.expireTimeFlag && (
+                  <div class={styles.nums}>
+                    观看视频模仿并练习:{data.videoData?.trainingTimes || 0}/
+                    {data.videoData?.trainingContent?.practiceTimes || 0}
+                  </div>
+                )}
+              </div>
+            )}
+          </Transition>
+        </div>
+      </div>
+    );
+  }
+});

+ 6 - 0
src/views/exercise-after-class/types.ts

@@ -0,0 +1,6 @@
+export const featureType = {
+    UNIT_TEST: '',
+    PRACTICE: '练习',
+    EVALUATION: '评测',
+    LESSON_TRAINING: '课后作业'
+}

+ 233 - 0
src/views/exercise-after-class/video-class.tsx

@@ -0,0 +1,233 @@
+import {
+  defineComponent,
+  onMounted,
+  ref,
+  watch,
+  Transition,
+  toRefs,
+  nextTick
+} from 'vue';
+import styles from './index.module.less';
+import { Slider } from 'vant';
+import iconplay from '../coursewarePlay/image/icon-play.svg';
+import iconpause from '../coursewarePlay/image/icon-pause.svg';
+import iconVideobg from '../coursewarePlay/image/icon-videobg.png';
+import { getSecondRPM } from '@/helpers/utils';
+import TCPlayer from 'tcplayer.js';
+import 'tcplayer.js/dist/tcplayer.min.css';
+export default defineComponent({
+  name: 'video-class',
+  props: {
+    item: {
+      type: Object,
+      default: () => {
+        return {};
+      }
+    },
+    /** 是否会员 */
+    isMember: {
+      type: Boolean,
+      default: true
+    },
+    modal: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: [
+    'loadedmetadata',
+    'togglePlay',
+    'ended',
+    'reset',
+    'error',
+    'close',
+    'changeModal'
+  ],
+  setup(props, { emit }) {
+    const { item, modal, isMember } = toRefs(props);
+    const videoItem = ref();
+    const videoID = 'video' + Date.now() + Math.floor(Math.random() * 100);
+
+    const __init = () => {
+      if (videoItem.value) {
+        nextTick(() => {
+          videoItem.value?.pause();
+        });
+        // console.log(props.item, item.value, '-----')
+        videoItem.value.poster(props.item.coverImg); // 封面
+        videoItem.value.src(props.item.content); // url 播放地址
+        videoItem.value.loop(props.item.loop);
+        // videoItem.value.muted(props.item.muted)
+        videoItem.value.autoplay(props.item.autoplay);
+
+        // 初步加载时
+        videoItem.value.one('loadedmetadata', (e: any) => {
+          // if (item.value.autoplay && videoItem.value) {
+          //   videoItem.value?.play()
+          // } else {
+          //   item.value.muted = false
+          //   item.value.videoEle?.muted(false)
+          //   item.value.videoEle?.volume(1)
+          //   item.value.videoEle?.pause()
+          // }
+
+          // 获取时长
+          const videoEle = videoItem.value;
+          item.value.duration = videoEle.duration();
+          item.value.videoEle = videoEle;
+          item.value.loaded = true;
+          emit('loadedmetadata', videoItem.value);
+
+          if (item.value.autoplay && videoItem.value) {
+            item.value.muted = false;
+            videoItem.value?.muted(false);
+            videoItem.value?.volume(1);
+            // videoItem.value?.pause()
+            videoItem.value?.play();
+          } else {
+            item.value.muted = false;
+            videoItem.value?.muted(false);
+            videoItem.value?.volume(1);
+            videoItem.value?.pause();
+          }
+        });
+
+        // 视频播放时加载
+        videoItem.value.on('timeupdate', () => {
+          if (!item.value.loaded) return;
+          const videoEle = videoItem.value;
+          item.value.currentTime = videoEle.currentTime();
+        });
+
+        // 视频播放结束
+        videoItem.value.on('ended', () => {
+          emit('ended', item.value);
+        });
+
+        //
+        videoItem.value.on('pause', () => {
+          console.log('暂停');
+          //暂停
+          item.value.paused = true;
+          videoItem.value?.pause();
+          setTimeout(() => {
+            videoItem.value?.pause();
+          }, 100);
+        });
+
+        videoItem.value.on('play', () => {
+          item.value.paused = false;
+          // 播放
+          // console.log(JSON.parse(JSON.stringify(item.value)), 'play ------ ')
+          if (item.value.muted) {
+            item.value.muted = false;
+            videoItem.value?.muted(false);
+            videoItem.value?.volume(1);
+            videoItem.value?.pause();
+          }
+        });
+
+        // 视频播放异常
+        videoItem.value.on('error', () => {
+          emit('error');
+        });
+      }
+    };
+
+    onMounted(() => {
+      videoItem.value = TCPlayer(videoID, {
+        appID: '',
+        controls: false,
+        loop: item.value.loop,
+        muted: false
+        // autoplay: true
+      }); // player-container-id 为播放器容器 ID,必须与 html 中一致
+
+      __init();
+    });
+
+    watch(
+      () => props.item,
+      () => {
+        // item.value.videoEle?.pause()
+        __init();
+      }
+    );
+    return () => (
+      <>
+        <div
+          class={styles.itemDiv}
+          onClick={() => {
+            clearTimeout(item.value.timer);
+            // activeData.model = !activeData.model
+            emit('changeModal', !modal.value);
+          }}>
+          <video
+            id={videoID}
+            style={{ height: '100%', width: '100%' }}
+            playsinline="false"
+            preload="auto"
+            class="player"
+            poster={iconVideobg}
+            data-vid={item.value.id}
+            src={item.value.content}
+            // loop={item.value.loop}
+            // autoplay={item.value.autoplay}
+            // muted={item.value.muted}
+          >
+            <source src={item.value.content} type="video/mp4" />
+          </video>
+          <div class={styles.videoSection}></div>
+        </div>
+        <Transition name="bottom">
+          {modal.value && !item.value.muted && (
+            <div class={styles.bottomFixedContainer}>
+              <div class={styles.time}>
+                <span>{getSecondRPM(item.value.currentTime)}</span>/
+                <span>{getSecondRPM(item.value.duration)}</span>
+              </div>
+              <div class={styles.slider}>
+                {item.value.duration && (
+                  <Slider
+                    buttonSize={16}
+                    modelValue={item.value.currentTime}
+                    min={0}
+                    max={item.value.duration}
+                  />
+                )}
+              </div>
+
+              <div class={styles.actions}>
+                <div class={styles.actionBtn}>
+                  {item.value.paused ? (
+                    <img
+                      src={iconplay}
+                      onClick={() => {
+                        clearTimeout(item.value.timer);
+                        item.value.videoEle?.play();
+                        item.value.paused = false;
+                        item.value.timer = setTimeout(() => {
+                          // activeData.model = false
+                          emit('changeModal', false);
+                        }, 4000);
+                      }}
+                    />
+                  ) : (
+                    <img
+                      src={iconpause}
+                      onClick={() => {
+                        clearTimeout(item.value.timer);
+                        item.value.videoEle?.pause();
+                        item.value.paused = true;
+                      }}
+                    />
+                  )}
+                </div>
+              </div>
+            </div>
+          )}
+        </Transition>
+      </>
+    );
+  }
+});

+ 3 - 1
src/views/lessonCourseware/component/CourseItem/index.tsx

@@ -23,7 +23,9 @@ export default defineComponent({
         <div class={styles.wrap}>
           {prop.list.map((item: any) => {
             return (
-              <div class={styles.item} onClick={() => emit('itemClick', item)}>
+              <div
+                class={[styles.item, 'courseItem']}
+                onClick={() => emit('itemClick', item)}>
                 <div class={styles.cover}>
                   <div class={styles.coverImg}>
                     <img

Some files were not shown because too many files changed in this diff