浏览代码

Merge branch 'iteration-20241018-courseware' into jenkins-dev

lex-xin 4 月之前
父节点
当前提交
64b7f46f28
共有 38 个文件被更改,包括 1521 次插入422 次删除
  1. 17 0
      src/components/globalTools/globalTools.ts
  2. 二进制
      src/components/globalTools/images/g-arrow-right.png
  3. 二进制
      src/components/globalTools/images/icon-note.png
  4. 二进制
      src/components/globalTools/images/icon-tool.png
  5. 二进制
      src/components/globalTools/images/icon-whiteboard.png
  6. 102 0
      src/components/globalTools/index.module.less
  7. 231 0
      src/components/globalTools/index.tsx
  8. 8 1
      src/components/o-guide/index.module.less
  9. 14 6
      src/views/courseList/index.module.less
  10. 22 25
      src/views/courseList/index.tsx
  11. 65 0
      src/views/coursewarePlay/component/courseware-tips/index.module.less
  12. 28 0
      src/views/coursewarePlay/component/courseware-tips/index.tsx
  13. 177 0
      src/views/coursewarePlay/component/courseware-type/index.module.less
  14. 28 0
      src/views/coursewarePlay/component/courseware-type/index.tsx
  15. 47 0
      src/views/coursewarePlay/component/play-loading/index.module.less
  16. 16 0
      src/views/coursewarePlay/component/play-loading/index.tsx
  17. 22 6
      src/views/coursewarePlay/component/point.module.less
  18. 10 3
      src/views/coursewarePlay/component/points.tsx
  19. 二进制
      src/views/coursewarePlay/component/tips/icon-close.png
  20. 73 0
      src/views/coursewarePlay/component/tips/index.module.less
  21. 37 0
      src/views/coursewarePlay/component/tips/index.tsx
  22. 二进制
      src/views/coursewarePlay/component/tips/top-bg.png
  23. 2 2
      src/views/coursewarePlay/component/tools/pen.module.less
  24. 24 3
      src/views/coursewarePlay/component/tools/pen.tsx
  25. 50 24
      src/views/coursewarePlay/component/video-play.tsx
  26. 32 23
      src/views/coursewarePlay/component/video.module.less
  27. 二进制
      src/views/coursewarePlay/image/back.png
  28. 0 11
      src/views/coursewarePlay/image/back.svg
  29. 二进制
      src/views/coursewarePlay/image/btn_go_practice.png
  30. 二进制
      src/views/coursewarePlay/image/icon-current.png
  31. 0 0
      src/views/coursewarePlay/image/icons.json
  32. 122 50
      src/views/coursewarePlay/index.module.less
  33. 175 196
      src/views/coursewarePlay/index.tsx
  34. 4 2
      src/views/coursewarePlay/playRecordTime.tsx
  35. 121 27
      src/views/exercise-after-class/index.module.less
  36. 56 21
      src/views/exercise-after-class/index.tsx
  37. 37 21
      src/views/exercise-after-class/video-class.tsx
  38. 1 1
      vite.config.ts

+ 17 - 0
src/components/globalTools/globalTools.ts

@@ -0,0 +1,17 @@
+import { ref } from "vue"
+
+/** 工具栏状态 */
+export const toolOpen = ref(false)
+// 批注
+export const penShow = ref(false)
+// 白板
+export const whitePenShow = ref(false)
+// 是否正在播放
+export const isPlay = ref(false)
+// 是否隐藏
+export const isHidden = ref(true)
+
+/** 是否隐藏工具栏 */
+export const handleHidden = (status = true) => {
+   isHidden.value = status
+}

二进制
src/components/globalTools/images/g-arrow-right.png


二进制
src/components/globalTools/images/icon-note.png


二进制
src/components/globalTools/images/icon-tool.png


二进制
src/components/globalTools/images/icon-whiteboard.png


+ 102 - 0
src/components/globalTools/index.module.less

@@ -0,0 +1,102 @@
+.globalTools {
+  &.isPlay {
+     .iconTools,
+     .expendTools {
+        opacity: 0.4;
+     }
+  }
+  &.isHidden {
+     .iconTools,
+     .expendTools {
+        opacity: 0;
+        display: none;
+     }
+  }
+
+  .mask {
+     position: fixed;
+     left: 0;
+     right: 0;
+     top: 0;
+     bottom: 0;
+     background-color: transparent;
+     z-index: 2998;
+  }
+  .iconTools,
+  .expendTools {
+     position: fixed;
+     right: -2px;
+     top: 0;
+     transform: translateY(var(--toolTranslateY));
+     // margin-top: -29px;
+     z-index: 2999;
+     // padding: 0 5px;
+     background: rgba(0, 0, 0, 0.4);
+     border-radius: 200px 0px 0px 200px;
+     border: 1px solid rgba(255, 255, 255, 0.3);
+     border-right-width: 0;
+     cursor: pointer;
+     font-size: 0;
+     // transition: transform 0.2s ease;
+     img {
+        padding: 5px 8px;
+        width: 24px;
+        height: 24px;
+        box-sizing: content-box;
+        -moz-user-select: none;
+        /* 火狐浏览器 */
+        -webkit-user-drag: none;
+        /* 谷歌、Safari和Opera浏览器 */
+        -webkit-user-select: none;
+        /* 谷歌、Safari和Opera浏览器 */
+        -ms-user-select: none;
+        /* IE10+浏览器 */
+        user-select: none;
+        /* 通用 */
+        -webkit-touch-callout: none;
+        /* iOS Safari */
+
+        &:hover {
+           opacity: 0.8;
+        }
+     }
+  }
+
+  .iconTools {
+     // transition-delay: 0.2s;
+  }
+
+  .expendTools {
+     // transform: translateX(100%);
+     display: none;
+     img {
+        cursor: pointer;
+        padding-left: 12px;
+        padding-right: 12px;
+     }
+     .iconWhiteboard {
+        // margin: 0 30px;
+        padding-left: 8px;
+        padding-right: 8px;
+     }
+     .iconArrow {
+        padding: 7px 12px;
+        width: 18px;
+        height: 18px;
+     }
+  }
+
+  .hideTools {
+     // transition: transform 0.2s ease;
+     transform: translateY(var(--toolTranslateY));
+     display: none;
+  }
+
+  .showTools {
+     // transition: transform 0.2s ease;
+     // transition-delay: 0.2s;
+     transform: translateY(var(--toolTranslateY));
+     display: flex;
+     align-items: center;
+  }
+}

+ 231 - 0
src/components/globalTools/index.tsx

@@ -0,0 +1,231 @@
+import {
+  toolOpen,
+  whitePenShow,
+  penShow,
+  isPlay,
+  isHidden
+} from './globalTools';
+import { defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import styles from './index.module.less';
+import iconTool from './images/icon-tool.png';
+import iconNote from './images/icon-note.png';
+import iconWhiteboard from './images/icon-whiteboard.png';
+import gArrowRight from './images/g-arrow-right.png';
+import { nextTick } from 'process';
+import { useNetwork } from '@vueuse/core';
+import { showToast } from 'vant';
+import Pen from '@/views/coursewarePlay/component/tools/pen';
+
+export default defineComponent({
+  name: 'globalTools',
+  setup() {
+    const { isOnline } = useNetwork()
+    const isMask = ref(false); // 是否显示遮罩层,为了处理云教练里面拖动不了的问题
+    const route = useRoute();
+    // watch(
+    //   () => route.path,
+    //   () => {
+    //     handleStatus();
+    //   }
+    // );
+    const iconToolsDom = ref<HTMLDivElement>();
+    const expendToolsDom = ref<HTMLDivElement>();
+
+    function openTool() {
+      if (isLock) return;
+      isPlay.value = false
+      toolOpen.value = !toolOpen.value;
+    }
+
+    function openType(type: 'note' | 'whiteboard') {
+      if (isLock) return;
+      console.log(isOnline.value, 'isOnline.value')
+      if(!isOnline.value) {
+        showToast('网络异常')
+        return
+      }
+      if (type === 'note') {
+        penShow.value = true;
+
+        isHidden.value = true;
+      } else if (type === 'whiteboard') {
+        whitePenShow.value = true;
+        isHidden.value = true;
+      }
+    }
+
+    function handleStatus() {
+      isHidden.value = route.path === '/login' ? true : false;
+    }
+
+    function computePos(type: 'width' | 'height', value: number) {
+      const clientNum =
+        type == 'width'
+          ? document.documentElement.clientWidth
+          : document.documentElement.clientHeight;
+      console.log(value, clientNum)
+      return {
+        pos: ((clientNum - value) / 2).toFixed(5)
+      };
+    }
+
+    /* 拖拽还没有兼容rem */
+    let isLock = false;
+    let toolMoveY = 0; // 移动的距离
+    function drag(el: HTMLElement) {
+      function mousedown(e: MouseEvent | TouchEvent) {
+        const isTouchEv = isTouchEvent(e);
+        const event = isTouchEv ? e.touches[0] : e;
+        isLock = false;
+        isMask.value = true;
+        const parentElement = el;
+        const parentElementRect = parentElement.getBoundingClientRect();
+        const downX = event.clientX;
+        const downY = event.clientY;
+        // const clientWidth = document.documentElement.clientWidth
+        const clientHeight = document.documentElement.clientHeight;
+        // const minLeft = 0
+        const minTop = 0;
+        // const maxLeft = clientWidth - parentElementRect.width
+        const maxTop = clientHeight - parentElementRect.height;
+        function onMousemove(e: MouseEvent | TouchEvent) {
+          const event = isTouchEvent(e) ? e.touches[0] : e;
+          // let moveX = parentElementRect.left + (e.clientX - downX)
+          let moveY = parentElementRect.top + (event.clientY - downY);
+          // let moveY = e.clientY - downY
+          // moveX = moveX < minLeft ? minLeft : moveX > maxLeft ? maxLeft : moveX
+          moveY = moveY < minTop ? minTop : moveY > maxTop ? maxTop : moveY;
+          toolMoveY = moveY;
+          document.documentElement.style.setProperty(
+            '--toolTranslateY',
+            `${moveY}px`
+          );
+
+          // 计算移动的距离
+          const cX = event.clientX - downX;
+          const cY = event.clientY - downY;
+
+          // 如果移动距离超过一定阈值,则认为是拖动
+          if (Math.abs(cX) > 3 || Math.abs(cY) > 3) {
+            isLock = true; // 设置为拖动状态
+          }
+        }
+        function onMouseup() {
+          document.removeEventListener(
+            isTouchEv ? 'touchmove' : 'mousemove',
+            onMousemove
+          );
+          document.removeEventListener(
+            isTouchEv ? 'touchend' : 'mouseup',
+            onMouseup
+          );
+          isMask.value = false;
+        }
+        document.addEventListener(
+          isTouchEv ? 'touchmove' : 'mousemove',
+          onMousemove
+        );
+        document.addEventListener(
+          isTouchEv ? 'touchend' : 'mouseup',
+          onMouseup
+        );
+      }
+      el.addEventListener('mousedown', mousedown);
+      el.addEventListener('touchstart', mousedown);
+    }
+    function isTouchEvent(e: MouseEvent | TouchEvent): e is TouchEvent {
+      return window.TouchEvent && e instanceof window.TouchEvent;
+    }
+    //重新计算位置 居中
+    function refreshPos() {
+      // computePos("height", iconToolsDom.value?.clientHeight ||
+      console.log(iconToolsDom.value?.clientHeight);
+      const posHeight = computePos(
+        'height',
+        iconToolsDom.value?.clientHeight || 0
+      );
+      if (iconToolsDom.value) {
+        document.documentElement.style.setProperty(
+          '--toolTranslateY',
+          `${posHeight.pos}px`
+        );
+      }
+    }
+    let rect: any;
+    function onResize() {
+      rect = rect ? rect : iconToolsDom.value?.getBoundingClientRect();
+      const clientHeight = document.documentElement.clientHeight;
+      const maxTop = clientHeight - rect.height;
+      if (toolMoveY >= maxTop) {
+        document.documentElement.style.setProperty(
+          '--toolTranslateY',
+          `${maxTop}px`
+        );
+      }
+    }
+    onMounted(() => {
+      handleStatus();
+      drag(iconToolsDom.value!);
+      drag(expendToolsDom.value!);
+      nextTick(() => {
+        refreshPos();
+      })
+      window.addEventListener('resize', onResize);
+    });
+
+    onUnmounted(() => {
+      window.removeEventListener('resize', onResize);
+    });
+    return () => (
+      <div>
+        <div
+          class={[
+            styles.globalTools,
+            isPlay.value ? styles.isPlay : '',
+            isHidden.value ? styles.isHidden : ''
+          ]}>
+          {isMask.value && <div class={styles.mask}></div>}
+
+          <div
+            class={[[styles.iconTools, toolOpen.value ? styles.hideTools : '']]}
+            ref={iconToolsDom}>
+            <img onClick={openTool} src={iconTool} />
+          </div>
+          <div
+            class={[styles.expendTools, toolOpen.value ? styles.showTools : '']}
+            ref={expendToolsDom}>
+            <img onClick={() => openType('note')} src={iconNote} />
+            <img
+              onClick={() => openType('whiteboard')}
+              class={styles.iconWhiteboard}
+              src={iconWhiteboard}
+            />
+            <img
+              onClick={openTool}
+              class={styles.iconArrow}
+              src={gArrowRight}
+            />
+          </div>
+        </div>
+        <Pen
+          show={penShow.value}
+          tip="请确认是否退出批注?"
+          close={() => {
+            penShow.value = false;
+            isHidden.value = false;
+          }}
+        />
+        <Pen
+          show={whitePenShow.value}
+          isWhite
+          tip="请确认是否退出白板?"
+          close={() => {
+            whitePenShow.value = false;
+            isHidden.value = false;
+          }}
+        />
+      </div>
+    );
+  }
+});

+ 8 - 1
src/components/o-guide/index.module.less

@@ -4,10 +4,14 @@
   :global {
     .van-tabs__nav {
       background-color: transparent;
+      --van-tabs-bottom-bar-width: 16px;
       .van-tab {
-        color: #fff;
+        color: rgba(255,255,255,.5);
         font-size: 16px;
       }
+      .van-tab--active {
+        color: #fff;
+      }
     }
     .van-tab__panel {
       height: calc(100vh - var(--van-tabs-line-height));
@@ -20,6 +24,9 @@
     }
     .van-tabs__line{
       bottom: .5rem;
+      background: linear-gradient( 135deg, #FF9A60 0%, #FF4E2B 100%);
+      width: 16px;
+      height: 4px;
     }
   }
   .content {

+ 14 - 6
src/views/courseList/index.module.less

@@ -192,13 +192,21 @@
 
 .courseDialog {
   padding: 20px;
+  border-radius: 20px;
+  &::before {
+    content: '';
+    width: 100%;
+    height: 49px;
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    border-top-left-radius: 20px;
+    border-top-right-radius: 20px;
+    background: linear-gradient(to bottom, #FFEADA, #ffffff);
+  }
+
 
-  // :global {
-  //   .van-popup__close-icon {
-  //     color: #333;
-  //     top: 24px;
-  //   }
-  // }
   .iconClose {
     width: 18px;
     height: 19px;

+ 22 - 25
src/views/courseList/index.tsx

@@ -4,28 +4,15 @@ import {
   Button,
   Cell,
   CellGroup,
-  Dialog,
-  Empty,
-  Grid,
-  GridItem,
-  Icon,
-  Image,
-  Loading,
   Popup,
   showConfirmDialog,
   showLoadingToast,
-  showToast,
-  Skeleton,
-  SkeletonImage,
-  Space
 } from 'vant'
 import {
   defineComponent,
   onMounted,
   reactive,
   onUnmounted,
-  nextTick,
-  Transition,
   TransitionGroup
 } from 'vue'
 import styles from './index.module.less'
@@ -233,17 +220,27 @@ export default defineComponent({
     // 去课件播放
     const gotoPlay = (item: any) => {
       data.catchStatus = false
-      postMessage({
-        api: 'openWebView',
-        content: {
-          url: `${location.origin}${location.pathname}#/coursewarePlay?id=${item.lessonCoursewareDetailId}&source=my-course`,
-          orientation: 0,
-          isHideTitle: true,
-          statusBarTextColor: false,
-          isOpenLight: true,
-          showLoadingAnim: true
-        }
-      })
+      if(browser().isApp) {
+        postMessage({
+          api: 'openWebView',
+          content: {
+            url: `${location.origin}${location.pathname}#/coursewarePlay?id=${item.lessonCoursewareDetailId}&source=my-course`,
+            orientation: 0,
+            isHideTitle: true,
+            statusBarTextColor: false,
+            isOpenLight: true,
+            showLoadingAnim: true
+          }
+        })
+      } else {
+        router.push({
+          path: '/coursewarePlay',
+          query: {
+            id: item.lessonCoursewareDetailId,
+            source: 'my-course'
+          }
+        })
+      }
     }
     // 检查数据的缓存状态
     const checkCoursewareCache = (list: []): Promise<any[]> => {
@@ -449,7 +446,7 @@ export default defineComponent({
         {data.loading && <OLoading />}
         {!data.loading && !data.list.length && <OEmpty tips="暂无内容" />}
 
-        <Popup v-model:show={data.catchStatus} round class={styles.courseDialog}>
+        <Popup v-model:show={data.catchStatus} class={styles.courseDialog}>
           <i class={styles.iconClose} onClick={() => (data.catchStatus = false)}></i>
           <div class={styles.title}>下载提醒</div>
 

+ 65 - 0
src/views/coursewarePlay/component/courseware-tips/index.module.less

@@ -0,0 +1,65 @@
+.container {
+  position: relative;
+  width: 453px;
+  height: 80vh;
+  max-height: 302px;
+  // background: url('../../image/tips-bg.png') top center no-repeat #fff;
+  // background-size: contain;
+  border-radius: 20px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  background-color: #fff;
+
+  &::before {
+    content: '';
+    width: 100%;
+    height: 46px;
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    border-top-left-radius: 20px;
+    border-top-right-radius: 20px;
+    background: linear-gradient(to bottom, #FFEADA, #ffffff);
+  }
+
+  .iconClose {
+    position: relative;
+    z-index: 1;
+    width: 18px;
+    height: 19px;
+    position: absolute;
+    top: 14px;
+    right: 20px;
+    z-index: 9;
+    background: url('../tips/icon-close.png') no-repeat center;
+    background-size: contain;
+  }
+
+  .title {
+    position: relative;
+    z-index: 1;
+    font-weight: 600;
+    font-size: 16px;
+    color: #131415;
+    line-height: 22px;
+    text-align: center;
+    padding: 12px 0;
+    flex-shrink: 0;
+  }
+
+  .content {
+    flex: 1;
+    overflow-x: hidden;
+    overflow-y: auto;
+    padding: 0 20px;
+    margin-bottom: 16px;
+    font-size: 14px;
+    line-height: 1.6;
+
+    &::-webkit-scrollbar {
+      display: none;
+    }
+  }
+}

+ 28 - 0
src/views/coursewarePlay/component/courseware-tips/index.tsx

@@ -0,0 +1,28 @@
+import { defineComponent, PropType } from "vue";
+import styles from './index.module.less'
+
+export default defineComponent({
+  name: 'coruseware-tips',
+  props: {
+    titleName: {
+      type: String,
+      default: '阶段目标'
+    },
+    content: {
+      type: String,
+      default: ''
+    }
+  },
+  emits: ['close'],
+  setup(props, { emit }) {
+    return () => <div class={styles.container}>
+      <i
+          class={styles.iconClose}
+          onClick={() => (emit("close"))}></i>
+      <div class={styles.title}>
+        {props.titleName}
+      </div>
+      <div class={styles.content} v-html={props.content}></div>
+    </div>
+  }
+})

+ 177 - 0
src/views/coursewarePlay/component/courseware-type/index.module.less

@@ -0,0 +1,177 @@
+.container {
+  display: flex;
+  flex-direction: column;
+  min-width: 300px;
+  max-width: 300px;
+  height: 100vh;
+  color: #fff;
+  font-size: 12px;
+  box-sizing: border-box;
+}
+
+.pointHead {
+  display: flex;
+  align-items: center;
+  padding: 13px 10px 15px 15px;
+  flex-shrink: 0;
+  font-size: 16px;
+  font-weight: 500;
+
+  img {
+    width: 20px;
+    height: 20px;
+    margin-right: 6px;
+  }
+  span {
+    line-height: 1.3;
+    padding-top: 1px;
+  }
+}
+
+.content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 0 8px;
+}
+
+
+.item {
+  display: inline-block;
+  margin: 0 0 18px;
+  padding: 0 8px;
+  width: calc(33.333% - 16px);
+  box-sizing: content-box;
+
+  .cover {
+    position: relative;
+    border-radius: 2px;
+    overflow: hidden;
+    margin-bottom: 8px;
+    box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.2);
+  }
+
+  .currentText {
+    width: 28px;
+    height: 13px;
+    position: absolute;
+    top: 2px;
+    left: 2px;
+    background: url('../../image/icon-current.png') no-repeat center center;
+    background-size: contain;
+  }
+
+  .model {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(0, 0, 0, 0.4);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #fff;
+    font-size: 12px;
+    line-height: 18px;
+
+    img {
+      width: 12px;
+      height: 12px;
+      margin-right: 4px;
+    }
+  }
+
+  .coverNum {
+    position: absolute;
+    bottom: 12px;
+    left: 50%;
+    transform: translateX(-50%);
+    border-radius: 20px;
+    color: rgba(116, 44, 0, 1);
+    background-color: #fff;
+    padding: 4px 6px;
+    line-height: 1;
+    font-size: 12px;
+    z-index: 1;
+    white-space: nowrap;
+    word-break: break-all;
+    min-width: 50px;
+    text-align: center;
+  }
+
+  .coverImg {
+    width: 100%;
+    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%;
+    border-radius: 2px;
+    overflow: hidden;
+
+    &>img {
+      display: block;
+      width: 100%;
+      height: 95px;
+      opacity: 0;
+      transition: opacity .3s;
+    }
+
+    :global {
+      .van-image__loading {
+        position: relative;
+        height: 95px;
+        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;
+    }
+  }
+
+  .name {
+    width: 100%;
+    font-size: 12px;
+    font-weight: 600;
+    color: rgba(255,255,255,0.5);
+    line-height: 20px !important;
+
+    :global {
+      .van-notice-bar {
+        padding: 0 !important;
+        line-height: 20px !important;
+        height: 20px !important;
+      }
+    }
+
+    &.active {
+      :global {
+        .van-notice-bar__content {
+          color: #FFFFFF;
+          transition-property: transform;
+        }
+      }
+    }
+
+    &.disabled {
+      :global {
+        .van-notice-bar__content {
+          color: rgba(255,255,255,0.5);
+          transition-duration: 0s !important;
+          transform: none !important;
+          width: 100%;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+      }
+    }
+  }
+}

文件差异内容过多而无法显示
+ 28 - 0
src/views/coursewarePlay/component/courseware-type/index.tsx


+ 47 - 0
src/views/coursewarePlay/component/play-loading/index.module.less

@@ -0,0 +1,47 @@
+.audioAnimate {
+  // position: absolute;
+  // left: 0;
+  // right: 0;
+  // top: 0;
+  // bottom: 0;
+  // background-color: rgba(0, 0, 0, .6);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding-bottom: 30%;
+
+  div {
+    width: 2px;
+    height: 10px;
+    background: linear-gradient(135deg, #FF8057; 0%, #FF8057 100%);
+    transform-origin: bottom;
+    border-radius: 5px;
+    margin: 0 1px 0;
+  }
+
+  & div:nth-child(1) {
+    animation: musicWave 0.5s infinite linear both alternate;
+  }
+
+  & div:nth-child(2) {
+    animation: musicWave 0.2s infinite linear both alternate;
+  }
+
+  & div:nth-child(3) {
+    animation: musicWave 0.6s infinite linear both alternate;
+  }
+
+  & div:nth-child(4) {
+    animation: musicWave 0.3s infinite linear both alternate;
+  }
+}
+
+@keyframes musicWave {
+  0% {
+    height: 3px;
+  }
+
+  100% {
+    height: 10px;
+  }
+}

+ 16 - 0
src/views/coursewarePlay/component/play-loading/index.tsx

@@ -0,0 +1,16 @@
+import { defineComponent } from 'vue';
+import styles from './index.module.less';
+
+export default defineComponent({
+  name: 'playLoading',
+  setup() {
+    return () => (
+      <div class={styles.audioAnimate}>
+        <div></div>
+        <div></div>
+        <div></div>
+        <div></div>
+      </div>
+    );
+  }
+});

+ 22 - 6
src/views/coursewarePlay/component/point.module.less

@@ -13,11 +13,16 @@
   align-items: center;
   padding: 13px 10px 15px 15px;
   flex-shrink: 0;
-  font-size: 14px;
+  font-weight: 500;
+  font-size: 16px;
   img {
-    width: 16px;
-    height: 16px;
-    margin-right: 7px;
+    width: 20px;
+    height: 20px;
+    margin-right: 6px;
+  }
+  span {
+    line-height: 1.3;
+    padding-top: 1px;
   }
 }
 .content {
@@ -107,9 +112,10 @@
   display: flex;
   align-items: center;
   justify-content: flex-start;
-  padding: 4px 5px 4px 0;
+  padding: 5px 5px 5px 0;
   border-radius: 6px;
   font-size: 12px;
+  position: relative;
   :global {
     .van-icon {
       display: none;
@@ -124,11 +130,21 @@
   }
 }
 .itemActive {
-  background: rgba(0, 0, 0, 0.15);
+  background: rgba(255, 128, 87, .15);
   color: var(--van-primary);
   :global {
     .van-icon {
       display: block;
     }
   }
+  .playLoading {
+    display: block;
+  }
+}
+.playLoading {
+  display: none;
+  font-size: 12px;
+  position: absolute;
+  right: 4px;
+  top: 9px;
 }

+ 10 - 3
src/views/coursewarePlay/component/points.tsx

@@ -11,6 +11,7 @@ import {
   iconSongActive
 } from '../image/icons.json'
 import { Collapse, CollapseItem, Icon, Image } from 'vant'
+import PlayLoading from './play-loading'
 export default defineComponent({
   name: 'points-list',
   props: {
@@ -59,7 +60,7 @@ export default defineComponent({
       <div class={styles.container}>
         <div class={styles.pointHead}>
           <img src={iconMulv} />
-          课程目录
+          <span>知识点目录</span>
         </div>
         <div class={styles.content}>
           <Collapse
@@ -103,7 +104,10 @@ export default defineComponent({
                                 <span style={{ width: '80%' }} class="van-ellipsis">
                                   {n.name}
                                 </span>
-                                <Icon name={iconZhibo} />
+                                {/* <Icon name={iconZhibo} /> */}
+                                <div class={styles.playLoading}>
+                                  <PlayLoading />
+                                </div>
                               </div>
                             )
                           })}
@@ -153,7 +157,10 @@ export default defineComponent({
                                                 <span style={{ width: '73%' }} class="van-ellipsis">
                                                   {n.name}
                                                 </span>
-                                                <Icon name={iconZhibo} />
+                                                {/* <Icon name={iconZhibo} /> */}
+                                                <div class={styles.playLoading}>
+                                                  <PlayLoading />
+                                                </div>
                                               </div>
                                             )
                                           })}

二进制
src/views/coursewarePlay/component/tips/icon-close.png


+ 73 - 0
src/views/coursewarePlay/component/tips/index.module.less

@@ -0,0 +1,73 @@
+
+.courseDialog {
+  padding: 20px !important;
+  max-width: 310px !important;
+  min-width: 295px !important;
+  // background: url('./top-bg.png') no-repeat top center #fff !important;
+  // background-size: contain !important;
+  overflow: hidden;
+  border-radius: 20px !important;
+  overflow: hidden;
+  background-color: #fff;
+
+  &::before {
+    content: '';
+    width: 100%;
+    height: 49px;
+    display: block;
+    position: absolute;
+    top: 0;
+    left: 0;
+    border-top-left-radius: 20px;
+    border-top-right-radius: 20px;
+    background: linear-gradient(to bottom, #FFEADA, #ffffff);
+  }
+
+  .iconClose {
+    position: relative;
+    width: 18px;
+    height: 19px;
+    position: absolute;
+    top: 23px;
+    right: 20px;
+    z-index: 9;
+    background: url('./icon-close.png') no-repeat center;
+    background-size: contain;
+  }
+
+  .title {
+    position: relative;
+    font-size: 18px;
+    font-weight: 600;
+    color: #1A1A1A;
+    line-height: 25px;
+    text-align: center;
+  }
+
+  .content {
+    padding: 20px 0 25px;
+    font-size: 16px;
+    color: #666666;
+    line-height: 24px;
+    text-align: center;
+  }
+
+  .popupBtnGroup {
+    display: flex;
+    align-items: center;
+
+    &>button {
+      flex: 1;
+      font-weight: 500;
+      font-size: 16px !important;
+
+      &:last-child {
+        margin-left: 15px;
+      }
+    }
+
+    :global {
+      --van-button-default-height: 40px;
+    }
+  }
+}

+ 37 - 0
src/views/coursewarePlay/component/tips/index.tsx

@@ -0,0 +1,37 @@
+import { Button, Popup } from 'vant';
+import { defineComponent, reactive } from 'vue';
+import styles from './index.module.less';
+
+export const tipState = reactive({
+  show: false,
+  title: '温馨提示',
+  content: '退出后将清空批注内容',
+  cancelText: '取消',
+  confirmText: '确认退出'
+})
+
+export default defineComponent({
+  name: 'tips-popup',
+  emits: ['confirm'],
+  setup(props, { emit }) {
+    return () => (
+      <Popup v-model:show={tipState.show} round class={styles.courseDialog}>
+        <i
+          class={styles.iconClose}
+          onClick={() => (tipState.show = false)}></i>
+        <div class={styles.title}>{tipState.title}</div>
+
+        <div class={styles.content}>
+          {tipState.content}
+        </div>
+
+        <div class={styles.popupBtnGroup}>
+          <Button round onClick={() => tipState.show = false}>{tipState.cancelText}</Button>
+          <Button round type="primary" onClick={() => emit("confirm")}>
+            {tipState.confirmText}
+          </Button>
+        </div>
+      </Popup>
+    );
+  }
+});

二进制
src/views/coursewarePlay/component/tips/top-bg.png


+ 2 - 2
src/views/coursewarePlay/component/tools/pen.module.less

@@ -4,7 +4,7 @@
     right: 0;
     bottom: 0;
     top: 0;
-    z-index: 11;
+    z-index: 9999;
 }
 .open{
     display: block;
@@ -23,7 +23,7 @@
 }
 .rightItem{
     position: absolute;
-    right: 15Px;
+    right: 8Px;
     bottom: 0;
     bottom: constant(safe-area-inset-bottom);
     bottom: env(safe-area-inset-bottom);

+ 24 - 3
src/views/coursewarePlay/component/tools/pen.tsx

@@ -3,10 +3,19 @@ import html2canvas from 'html2canvas'
 import { closeToast, Icon, showFailToast, showLoadingToast, showSuccessToast } from 'vant'
 import { defineComponent, toRefs, ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
 import styles from './pen.module.less'
+import Tips, { tipState } from '../tips'
 
 export default defineComponent({
   name: 'pen',
   props: {
+    isWhite: {
+      type: Boolean,
+      default: false
+    },
+    tip: {
+      type: String,
+      default: '请确认是否退出?'
+    },
     show: {
       type: Boolean,
       default: false
@@ -20,8 +29,9 @@ export default defineComponent({
     const { show } = toRefs(props)
     const firstRender = ref(true)
     const src = /(localhost|192)/.test(location.host)
-      ? 'https://test.lexiaoya.cn/whiteboard-noCollab'
-      : `${location.origin}/whiteboard-noCollab`
+    ? 'https://test.lexiaoya.cn/whiteboard-noCollab?platform=orchestra'
+    : `https://online.lexiaoya.cn/whiteboard-noCollab?platform=orchestra`;
+
 
     const exportImg = (event: MessageEvent) => {
       const data = event.data
@@ -110,6 +120,9 @@ export default defineComponent({
       >
         <iframe
           class={styles.iframe}
+          style={{
+            background: props.isWhite ? '#fff' : 'transparent'
+          }}
           frameborder="0"
           width="100vw"
           height="100vh"
@@ -121,7 +134,10 @@ export default defineComponent({
         {imgs.exported ? (
           <img crossorigin="anonymous" class={styles.img} src={imgs.base64} />
         ) : (
-          <div class={styles.rightItem} onClick={() => props.close()}>
+          <div class={styles.rightItem} onClick={() => {
+            tipState.content = props.tip
+            tipState.show = true
+          }}>
             <svg width="22px" height="20px" viewBox="0 0 22 20">
               <path
                 transform="translate(-1.000000, -2.000000)"
@@ -131,6 +147,11 @@ export default defineComponent({
             </svg>
           </div>
         )}
+
+        <Tips onConfirm={() => {
+          props.close()
+          tipState.show = false
+        }} />
       </div>
     )
   }

+ 50 - 24
src/views/coursewarePlay/component/video-play.tsx

@@ -13,7 +13,7 @@ import qs from 'query-string'
 import { iconSpeed } from '../image/icons.json'
 import TCPlayer from 'tcplayer.js'
 import 'tcplayer.js/dist/tcplayer.min.css'
-import { Slider } from 'vant'
+import { showToast, Slider } from 'vant'
 import { state } from '@/state'
 import useErrorLog from '@/hooks/useErrorLog';
 
@@ -273,7 +273,24 @@ export default defineComponent({
       videoErrorCount++
     }
 
+    /** 设置播放容器 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'
+      }
+    }
+
     onMounted(() => {
+      setContainer()
       videoItem.value = TCPlayer(videoID, {
         appID: '',
         controls: false,
@@ -349,16 +366,19 @@ export default defineComponent({
 
     return () => (
       <div class={styles.videoWrap}>
-        <video
-          style={{ width: '100%', height: '100%' }}
-          src={item.value.content}
-          ref={videoRef}
-          id={videoID}
-          preload="auto"
-          playsinline
-          webkit-playsinline
-        ></video>
-        <div class={styles.videoSection}></div>
+        <div style={{ width: parentContainer.width, height: '100%', margin: '0 auto' }}>
+          <video
+            style={{ width: '100%', height: '100%' }}
+            src={item.value.content}
+            ref={videoRef}
+            id={videoID}
+            preload="auto"
+            playsinline
+            webkit-playsinline
+          ></video>
+          <div class={styles.videoSection}></div>
+        </div>
+        
 
         <div
           class={[styles.controls, data.showBar ? '' : styles.hide]}
@@ -370,10 +390,11 @@ export default defineComponent({
           //   emit('close')
           // }}
         >
-          <div class={styles.time}>
-            <div>{getSecondRPM(data.currentTime)}</div>/<div>{getSecondRPM(data.duration)}</div>
-          </div>
+          
           <div class={styles.slider}>
+            <div class={styles.time}>
+              <div>{getSecondRPM(data.currentTime)}</div>/<div>{getSecondRPM(data.duration)}</div>
+            </div>
             <Slider
               step={0.01}
               class={styles.timeProgress}
@@ -395,23 +416,28 @@ export default defineComponent({
               >
                 <img src={data.playState === 'pause' ? iconplay : iconpause} />
               </div>
-              <div class={styles.actionBtn} onClick={toggleLoop}>
+              <div class={styles.actionBtn} onClick={() => {
+                  toggleLoop()
+                  showToast(data.loop ? '已打开循环播放' : '已关闭循环播放')
+                }}>
                 <img src={data.loop ? iconLoopActive : iconLoop} />
               </div>
               <div class={styles.actionBtn} id={speedBtnId}>
                 <img src={iconSpeed} />
               </div>
             </div>
-            <div class={styles.name}>{item.value.name}</div>
+            {/* <div class={styles.name}>{item.value.name}</div> */}
+
+            {item.value.materialMusicId && state.platformType !== 'SCHOOL' && (
+              <div
+                class={[styles.goPractice, data.showBar ? '' : styles.hide]}
+                onClick={gotoAccomany}
+              ></div>
+            )}
           </div>
         </div>
 
-        {item.value.materialMusicId && state.platformType !== 'SCHOOL' && (
-          <div
-            class={[styles.goPractice, data.showBar ? '' : styles.hide]}
-            onClick={gotoAccomany}
-          ></div>
-        )}
+        
 
         <div
           style={{
@@ -438,7 +464,7 @@ export default defineComponent({
               }}
             ></i>
             <Slider
-              min={0.6}
+              min={0.5}
               max={1.5}
               step={0.1}
               v-model={data.defaultSpeed}
@@ -463,7 +489,7 @@ export default defineComponent({
             <i
               class={[styles.iconCut]}
               onClick={() => {
-                if (data.defaultSpeed <= 0.6) {
+                if (data.defaultSpeed <= 0.5) {
                   return
                 }
                 if (videoItem.value) {

文件差异内容过多而无法显示
+ 32 - 23
src/views/coursewarePlay/component/video.module.less


二进制
src/views/coursewarePlay/image/back.png


+ 0 - 11
src/views/coursewarePlay/image/back.svg

@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="19px" height="19px" viewBox="0 0 19 19" 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" stroke-linecap="round" stroke-linejoin="round">
-        <g id="课件播放(老师)" transform="translate(-40.000000, -13.000000)" stroke="#FFFFFF" stroke-width="2">
-            <g id="图标/通用/返回" transform="translate(40.000000, 13.000000)">
-                <polyline id="Stroke-1" points="13.5 18 5 9.5 13.5 1"></polyline>
-            </g>
-        </g>
-    </g>
-</svg>

二进制
src/views/coursewarePlay/image/btn_go_practice.png


二进制
src/views/coursewarePlay/image/icon-current.png


文件差异内容过多而无法显示
+ 0 - 0
src/views/coursewarePlay/image/icons.json


+ 122 - 50
src/views/coursewarePlay/index.module.less

@@ -31,9 +31,9 @@
   right: 0;
   z-index: 10;
   display: flex;
-  align-items: center;
+  align-items: flex-start;
   justify-content: space-between;
-  height: 40px;
+  // height: 40px;
   background: linear-gradient(180deg, rgba(0, 0, 0, 0.6), transparent);
   transition: transform 0.5s;
   box-sizing: border-box;
@@ -48,15 +48,46 @@
   height: 100%;
   display: flex;
   justify-content: space-between;
-  align-items: center;
+  align-items: flex-start;
   z-index: 10;
-  padding: 0 15px;
+  font-size: 18px;
+  padding: 12px 15px 20px 40px;
 
   :global {
     .van-icon {
       margin-right: 8px;
     }
   }
+
+  .titleSection {
+    .title {
+      font-weight: 600;
+      font-size: 16px;
+      color: #ffffff;
+      line-height: 22px;
+    }
+  }
+  .titleContent {
+    display: flex;
+    align-items: center;
+    padding-top: 6px;
+    p {
+      font-size: 14px;
+      color: #ffffff;
+      line-height: 20px;
+    }
+    span {
+      margin-left: 6px;
+      font-size: 11px;
+      color: #ffffff;
+      line-height: 1.3;
+      background: rgba(0, 0, 0, 0.1);
+      border-radius: 10px;
+      border: 1px solid rgba(255, 255, 255, 0.7);
+      padding: 2px 8px;
+      box-sizing: content-box;
+    }
+  }
 }
 
 .headRight {
@@ -66,18 +97,26 @@
   align-items: center;
   margin-left: auto;
   height: 100%;
-  padding-right: 15px;
+  padding-right: 40px;
+  padding-top: 12px;
+
+  .pointBtn {
+    font-weight: 600;
+    font-size: 14px;
+    color: #FFFFFF;
+    line-height: 20px;
+    padding-right: 20px;
+  }
 
   .rightBtn {
     display: flex;
     justify-content: center;
     align-items: center;
     height: 100%;
-    padding: 0 10px;
 
     img {
-      width: 22px;
-      height: 22px;
+      width: 24px;
+      height: 24px;
       display: block;
     }
   }
@@ -139,7 +178,7 @@
   }
 
   &.acitveAnimation {
-    transition-duration: .8s;
+    transition-duration: 0.8s;
   }
 
   &.show {
@@ -164,8 +203,9 @@
 }
 
 .fullBtn {
-  width: 38px;
-  height: 46px;
+  width: 24px;
+  height: 24px;
+  padding: 10px 9px;
   display: flex;
   flex-direction: column;
   align-items: center;
@@ -173,10 +213,21 @@
   justify-content: space-evenly;
   overflow: hidden;
   white-space: nowrap;
+  box-sizing: content-box;
+  border-radius: 7px;
 
   &:active {
     background: rgba(255, 255, 255, 0.2);
   }
+
+  &.disabled {
+    opacity: 0.4;
+  }
+
+  img {
+    width: inherit;
+    height: inherit;
+  }
 }
 
 .rightFixedBtns {
@@ -186,21 +237,20 @@
   right: 12px;
   z-index: 10;
 
-  .btnsBottom {
-    margin-top: 10px;
-  }
 }
 
 .leftFixedBtns {
   position: absolute;
   top: 50%;
   transform: translateY(-50%);
-  left: 12px;
+  left: 40px;
   z-index: 10;
+  background: rgba(0, 0, 0, 0.4);
+  border-radius: 7px;
 
-  .prePoint {
-    margin-bottom: 8px;
-  }
+  // .prePoint {
+  //   margin-bottom: 8px;
+  // }
 }
 
 .btnsWrap {
@@ -248,7 +298,7 @@
       display: flex;
     }
 
-    .actionBtn>img {
+    .actionBtn > img {
       width: 30px;
       height: 30px;
       display: block;
@@ -260,13 +310,28 @@
 .popup {
   background: rgba(0, 0, 0, 0.5);
 }
+.popupCoursewarePlay {
+  background: rgba(0, 0, 0, 0.8) !important;
+  box-shadow: -6px 0px 20px 0px rgba(0,0,0,0.3) !important;
+  border-radius: 16px 0px 0px 16px !important;
+  backdrop-filter: blur(12px);
+}
+.popupPoint {
+  :global {
+    .van-popup__close-icon {
+      font-size: 18px;
+      top: 14px;
+      right: 20px;
+      color: #333333;
+    }
+  }
+}
 
 .overlayClass {
   --van-overlay-background: transparent;
 }
 
 :global {
-
   .top-enter-active,
   .top-leave-active {
     transition: transform 0.5s;
@@ -312,9 +377,9 @@
 
 .loadWrap {
   position: absolute;
-  left: -6Px;
+  left: -6px;
   top: 0;
-  right: -6Px;
+  right: -6px;
   bottom: 0;
   background: linear-gradient(45deg, #21232a, #111218);
   display: flex;
@@ -322,42 +387,49 @@
   align-items: center;
 }
 
+.playRecordTimeWrap {
+  position: absolute;
+  top: 11px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
 .playRecordTime {
-  width: 90px;
-  margin-right: 10px;
-  background: rgba(0, 0, 0, 0.4);
+  // width: 90px;
+  // margin-right: 10px;
+  background: rgba(0, 0, 0, 0.1);
   border-radius: 20px;
   font-size: 12px;
-  padding: 6px;
+  padding: 6px 8px;
   display: flex;
   align-items: center;
   justify-content: center;
   color: #fff;
 
   .timeLoad {
-    width: 5px;
-    height: 5px;
+    width: 4px;
+    height: 4px;
     background: #ff4e19;
-    border: 0.5px solid #ffffff;
+    // border: 0.5px solid #ffffff;
     border-radius: 50%;
-    margin-right: 3px;
-    animation: loadFade 1s ease-in-out infinite;
+    margin-right: 6px;
+    // animation: loadFade 1s ease-in-out infinite;
   }
 }
 
-@keyframes loadFade {
-  0% {
-    opacity: 0;
-  }
+// @keyframes loadFade {
+//   0% {
+//     opacity: 0;
+//   }
 
-  50% {
-    opacity: 0.5;
-  }
+//   50% {
+//     opacity: 0.5;
+//   }
 
-  100% {
-    opacity: 1;
-  }
-}
+//   100% {
+//     opacity: 1;
+//   }
+// }
 
 .swiperContainer {
   :global {
@@ -371,19 +443,19 @@
   background: rgba(0, 0, 0, 0.8);
 }
 
+
 .goPractice {
-  width: 89px;
-  height: 32px;
+  position: absolute;
+  right: 39px;
+  bottom: 12px;
+  width: 86px;
+  height: 30px;
   background: url('./image/btn_go_practice.png') no-repeat center;
   background-size: contain;
-  position: absolute;
-  right: 12px;
-  bottom: 60px;
   z-index: 11;
   transition: all .5s ease;
 
-
   &.hide {
-    transform: translateX(66px);
+    transform: translateY(55px);
   }
-}
+}

+ 175 - 196
src/views/coursewarePlay/index.tsx

@@ -9,9 +9,10 @@ import {
   watch,
   Transition,
   computed,
-  onBeforeUnmount
+  onBeforeUnmount,
+  shallowRef
 } from 'vue'
-import iconBack from './image/back.svg'
+import iconBack from './image/back.png'
 import styles from './index.module.less'
 import 'plyr/dist/plyr.css'
 import request from '@/helpers/request'
@@ -20,15 +21,15 @@ import { useRoute } from 'vue-router'
 import { listenerMessage, postMessage, promisefiyPostMessage } from '@/helpers/native-message'
 import qs from 'query-string'
 import MusicScore from './component/musicScore'
-import iconDian from './image/icon-dian.svg'
-import iconPoint from './image/icon-point.svg'
-import { iconUp, iconDown, iconPen, iconTouping, iconMenu } from './image/icons.json'
+// import iconDian from './image/icon-dian.svg'
+// import iconPoint from './image/icon-point.svg'
+import { iconUp, iconDown, iconPen, iconTouping, iconMenu, iconCourseType } from './image/icons.json'
 import Points from './component/points'
 import { browser } from '@/helpers/utils'
 import { Vue3Lottie } from 'vue3-lottie'
 import playLoadData from './datas/data.json'
 import { usePageVisibility } from '@vant/use'
-import { useInterval, useIntervalFn } from '@vueuse/core'
+import { useInterval, useIntervalFn, useNetwork } from '@vueuse/core'
 import PlayRecordTime from './playRecordTime'
 import { handleCheckVip } from '../hook/useFee'
 import OGuide from '@/components/o-guide'
@@ -37,11 +38,16 @@ import Pen from './component/tools/pen'
 import VideoItem from './component/video-item'
 import deepClone from '@/helpers/deep-clone'
 import VideoPlay from './component/video-play'
+import CoursewareType from './component/courseware-type'
+import CoursewareTips from './component/courseware-tips'
+import GlobalTools from '@/components/globalTools'
+import { isPlay, penShow, toolOpen, whitePenShow } from '@/components/globalTools/globalTools'
 
 export default defineComponent({
   name: 'CoursewarePlay',
   setup() {
     const pageVisibility = usePageVisibility()
+    const { isOnline } = useNetwork()
     /** 页面显示和隐藏 */
     watch(
       () => pageVisibility.value,
@@ -68,7 +74,7 @@ export default defineComponent({
     }
     const handleInit = (type = 0) => {
       //设置容器16:9
-      setContainer()
+      // setContainer()
       // 横屏
       postMessage(
         {
@@ -111,7 +117,9 @@ export default defineComponent({
 
     const route = useRoute()
     const headeRef = ref()
+    const isCurrentCoursewareMenu = shallowRef(true) // 是否为当前选的课程类型
     const data = reactive({
+      currentId: route.query.id as any,
       detail: null as any,
       knowledgePointList: [] as any,
       itemList: [] as any,
@@ -120,7 +128,7 @@ export default defineComponent({
       isCourse: false,
       isRecordPlay: false,
       videoRefs: {},
-
+      refLevelList: [] as any,
       videoState: 'init' as 'init' | 'play',
       videoItemRef: null as any,
       animationState: 'start' as 'start' | 'end',
@@ -272,15 +280,17 @@ export default defineComponent({
         }, 500)
       })
     }
-    const getDetail = async () => {
+    const getDetail = async (id?: any) => {
       try {
         const res: any = await request.get(
-          state.platformApi + `/lessonCoursewareDetail/detail/${route.query.id}`,
+          state.platformApi + `/lessonCoursewareDetail/detail/${id || route.query.id}`,
           {
             hideLoading: true
           }
         )
-        data.detail = res.data
+        const result = res.data || {}
+        result.lessonTargetDesc = result.lessonTargetDesc ? result.lessonTargetDesc.replace(/\n/g, "<br />") : ""
+        data.detail = result;
         if (res?.data?.lockFlag) {
           postMessage({
             api: 'courseLoading',
@@ -336,11 +346,24 @@ export default defineComponent({
           })
           getItemList()
         }
+        return true
       } catch (error) {
         console.log(error)
       }
     }
 
+    const onTitleTip = (type: "phaseGoals" | "checkItem", text: string) => {
+      handleStop()
+      popupData.pointOpen = true
+      popupData.pointContent = text
+      if(type === "checkItem") {
+        popupData.pointTitle = '检查事项'
+      } else if(type === "phaseGoals") {
+        popupData.pointTitle = '阶段目标'
+      }
+    }
+
+
     // ifram事件处理
     const iframeHandle = (ev: MessageEvent) => {
       if (ev.data?.api === 'headerTogge') {
@@ -443,12 +466,31 @@ export default defineComponent({
         //
       }
     }
+
+    const getRefLevel = async (id?: any) => {
+      try {
+        const res = await request.post(state.platformApi + '/lessonCoursewareDetail/refLevel', {
+          data: {
+            lessonCoursewareDetailId: id || route.query.id
+          }
+        })
+        data.refLevelList = res.data || []
+        return true
+      } catch {
+        // 
+      }
+    }
+
     onMounted(async () => {
       await sysParamConfig()
 
       if (state.platformType === 'STUDENT') {
         await getLookVideoData()
       }
+      // 只有老师有 课程类型 切换
+      if(state.platformType === "TEACHER") {
+        await getRefLevel()
+      }
       await getDetail()
       const hasFree = String(data.detail?.accessScope) === '0'
       if (!hasFree) {
@@ -507,6 +549,10 @@ export default defineComponent({
     }
 
     const popupData = reactive({
+      pointOpen: false,
+      pointContent: "", 
+      pointTitle: "",
+      coursewareOpen: false,
       open: false,
       activeIndex: 0,
       playIndex: 0,
@@ -855,6 +901,28 @@ export default defineComponent({
       }
     )
 
+    // 白板的批注打开时暂停播放
+    watch(
+      () => [whitePenShow.value, penShow.value],
+      () => {
+        if (whitePenShow.value || penShow.value) {
+          handleStop()
+        }
+      }
+    )
+    // 是否收起
+    watch(
+      () => activeData.model,
+      () => {
+        if (activeData.model) {
+            isPlay.value = false
+        } else {
+            isPlay.value = true
+            toolOpen.value = false
+        }
+      }
+    )
+
     /**
      * 初始化视频时长
      * @param newVal 播放状态
@@ -965,59 +1033,10 @@ export default defineComponent({
                       zIndex: 15,
                       opacity: 1
                     }
-                  : { opacity: 0, zIndex: -1, pointerEvents: "none" }
+                  : { opacity: 0, zIndex: -1, pointerEvents: 'none' }
               }
               class={styles.itemDiv}
             >
-              {/* <VideoItem
-                ref={(el: any) => (data.videoItemRef = el)}
-                item={activeVideoItem.value}
-                activeModel={activeData.model}
-                onClose={setModelOpen}
-                onPlay={() => {
-                  data.videoState = 'play'
-
-                  // 设置视频时长
-                  const videoTime = data.videoItemRef.getPlyrRef().duration || 0
-                  data.itemList[popupData.activeIndex].videoTime = Math.floor(videoTime)
-                }}
-                onPause={() => {
-                  clearTimeout(activeData.timer)
-                  activeData.model = true
-                  videoIntervalRef.pause()
-                }}
-                onEnded={async () => {
-                  const _index = popupData.activeIndex + 1
-                  if (_index < data.itemList.length) {
-                    handleSwipeChange(_index)
-                  } else {
-                    // 说明是最后一个
-                    intervalFnRef.value.pause()
-                    // 同步数据时先进行有效时间进行保存
-                    initVideoCount(false, true)
-                    await updateStat()
-                  }
-                }}
-                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?.playing
-                  ) {
-                    videoIntervalRef.resume()
-                  }
-                }}
-              /> */}
               <VideoPlay
                 ref={(el: any) => (data.videoItemRef = el)}
                 item={activeVideoItem.value}
@@ -1026,6 +1045,9 @@ export default defineComponent({
                 onPlay={() => {
                   data.videoState = 'play'
                   data.animationState = 'end'
+                  if(whitePenShow.value || penShow.value || popupData.coursewareOpen || popupData.open || popupData.guideOpen || popupData.pointOpen) {
+                    handleStop()
+                  } 
                 }}
                 onLoadedmetadata={(videoItem: any) => {
                   data.videoState = 'play'
@@ -1036,7 +1058,7 @@ export default defineComponent({
                 }}
                 onPause={() => {
                   clearTimeout(activeData.timer)
-                  activeData.model = true
+                  // activeData.model = true
                   videoIntervalRef.pause()
                 }}
                 onSeeked={() => {
@@ -1118,59 +1140,6 @@ export default defineComponent({
                       : {}
                   }
                 >
-                  {/* {m.type === 'VIDEO' && (
-                      <>
-                        <VideoPlay
-                          ref={(v: any) => (data.videoRefs[mIndex] = v)}
-                          item={m}
-                          isActive={activeEle}
-                          isEmtry={isEmtry}
-                          onPrepare={(val) => {
-                            m.isprepare = val
-                          }}
-                          onLoadedmetadata={(videoItem: any) => {
-                            m.videoEle = videoItem
-                            if (!m.isprepare) {
-                              m.isprepare = true
-                            }
-                          }}
-                          onTogglePlay={(paused: boolean) => {
-                            // console.log('播放切换', paused)
-                            if (!m.isprepare) {
-                              m.isprepare = true
-                            }
-                            m.autoPlay = false
-                            if (paused || popupData.open || popupData.guideOpen) {
-                              clearTimeout(activeData.timer)
-                            } else {
-                              setModelOpen()
-                            }
-                          }}
-                          onEnded={() => {
-                            const _index = popupData.activeIndex + 1
-                            if (_index < data.itemList.length) {
-                              handleSwipeChange(_index)
-                            }
-                          }}
-                          onReset={() => {
-                            if (!m.videoEle?.paused) {
-                              setModelOpen()
-                            }
-                          }}
-                          onError={() => {
-                            // 视屏异常
-                            m.error = true
-                          }}
-                        />
-                        <Transition name="van-fade">
-                          {!m.isprepare && (
-                            <div class={styles.loadWrap}>
-                              <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
-                            </div>
-                          )}
-                        </Transition>
-                      </>
-                    )} */}
                   <Transition name="van-fade">
                     {m.type === 'VIDEO' &&
                       data.animationState !== 'end' &&
@@ -1226,87 +1195,37 @@ export default defineComponent({
               )
             })}
           </div>
-          <Transition name="right">
-            {activeData.model && (
-              <div
-                class={styles.rightFixedBtns}
-                onClick={(e: Event) => {
-                  e.stopPropagation()
-                  clearTimeout(activeData.timer)
-                }}
-              >
-                <div class={styles.btnsWrap}>
-                  <div
-                    class={[styles.fullBtn, styles.point]}
-                    onClick={() => (popupData.open = true)}
-                  >
-                    <img src={iconMenu} />
-                    <span>知识点</span>
-                  </div>
-                </div>
-
-                <div class={[styles.btnsWrap, styles.btnsBottom]}>
-                  {/* <div class={styles.fullBtn} onClick={() => (popupData.guideOpen = true)}>
-                      <img src={iconTouping} />
-                      <span>投屏</span>
-                    </div> */}
-                  {data.isCourse && (
-                    <>
-                      <div class={styles.fullBtn} onClick={() => gotoRollCall('student_roll_call')}>
-                        <img src={iconDian} />
-                        <span>点名</span>
-                      </div>
-                      <div class={styles.fullBtn} onClick={() => gotoRollCall('sign_out')}>
-                        <img src={iconPoint} />
-                        <span>签退</span>
-                      </div>
-                    </>
-                  )}
-                </div>
-              </div>
-            )}
-          </Transition>
 
           <Transition name="left">
             {activeData.model && (
               <div class={styles.leftFixedBtns} onClick={(e: Event) => e.stopPropagation()}>
-                {popupData.activeIndex != 0 && (
-                  <div class={[styles.btnsWrap, styles.prePoint]}>
+                <div class={[styles.btnsWrap, styles.prePoint]}>
+                  <div class={styles.fullBtn} onClick={() => (popupData.coursewareOpen = true)}>
+                    <img src={iconCourseType} />
+                  </div>
+                  <div class={styles.fullBtn} onClick={() => (popupData.open = true)}>
+                    <img src={iconMenu} />
+                    {/* <span>知识点</span> */}
+                  </div>
                     <div
-                      class={styles.fullBtn}
+                      class={[styles.fullBtn, !(popupData.activeIndex != 0) && styles.disabled]}
                       onClick={() => {
-                        // useThrottleFn(() => {
-                        //   handlePreAndNext('up')
-                        // }, 300)
-                        // onChangeSwiper('up')
-                        handlePreAndNext('up')
+                        if(popupData.activeIndex != 0) handlePreAndNext('up')
                       }}
                     >
                       <img src={iconUp} />
-                      <span style={{ textAlign: 'center' }}>上一个</span>
+                      {/* <span style={{ textAlign: 'center' }}>上一个</span> */}
                     </div>
-                  </div>
-                )}
-                {popupData.activeIndex != data.itemList.length - 1 && (
-                  <div class={styles.btnsWrap}>
                     <div
-                      class={styles.fullBtn}
+                      class={[styles.fullBtn, !(popupData.activeIndex != data.itemList.length - 1) && styles.disabled]}
                       onClick={() => {
-                        // console.log('click down')
-                        // useThrottleFn(() => {
-                        //   console.log('click down pass')
-                        //   handlePreAndNext('down')
-                        // }, 300)
-                        // onChangeSwiper('down')
-                        handlePreAndNext('down')
-                        
+                        if(popupData.activeIndex != data.itemList.length - 1) handlePreAndNext('down')
                       }}
                     >
-                      <span style={{ textAlign: 'center' }}>下一个</span>
+                      {/* <span style={{ textAlign: 'center' }}>下一个</span> */}
                       <img src={iconDown} />
                     </div>
-                  </div>
-                )}
+                 </div>
               </div>
             )}
           </Transition>
@@ -1318,12 +1237,19 @@ export default defineComponent({
           class={styles.headerContainer}
           ref={headeRef}
         >
-          <div class={styles.backBtn} onClick={() => goback()}>
-            <Icon name={iconBack} />
-            返回
+          <div class={styles.backBtn}>
+            <Icon name={iconBack}  onClick={() => goback()} />
+            <div class={styles.titleSection}>
+              <div class={styles.title}  onClick={() => goback()}>{popupData.tabName}</div>
+                <div class={styles.titleContent}>
+                  <p onClick={() => goback()}>{data.itemList[popupData.activeIndex]?.name}</p>
+                  {data.detail?.lessonTargetDesc ? <span onClick={() => onTitleTip('phaseGoals', data.detail?.lessonTargetDesc)}>阶段目标</span>: ""}
+                  {data.itemList[popupData.activeIndex]?.checkItem ? <span onClick={() => onTitleTip('checkItem', data.itemList[popupData.activeIndex]?.checkItem)}>检查事项</span> : ""}
+                </div>
+            </div>
           </div>
-          {data.isCourse && <PlayRecordTime ref={playRef} list={data.knowledgePointList} />}
-          <div
+          {data.isCourse && <PlayRecordTime ref={playRef} isCurrentCoursewareMenu={isCurrentCoursewareMenu.value} list={data.knowledgePointList} />}
+          {/* <div
             class={styles.menu}
             onClick={() => {
               const _effectIndex = effectIndex.value + 1
@@ -1332,9 +1258,9 @@ export default defineComponent({
             }}
           >
             {popupData.tabName}
-          </div>
+          </div> */}
 
-          {state.platformType == 'TEACHER' && (
+          {state.platformType === 'TEACHER' && (
             <div
               class={styles.headRight}
               onClick={(e: Event) => {
@@ -1342,10 +1268,22 @@ export default defineComponent({
                 clearTimeout(activeData.timer)
               }}
             >
+              {data.isCourse && (
+                <>
+                  <div class={styles.pointBtn} onClick={() => gotoRollCall('student_roll_call')}>
+                    {/* <img src={iconDian} /> */}
+                    <span>点名</span>
+                  </div>
+                  <div class={styles.pointBtn} onClick={() => gotoRollCall('sign_out')}>
+                    {/* <img src={iconPoint} /> */}
+                    <span>签退</span>
+                  </div>
+                </>
+              )}
               <div class={styles.rightBtn} onClick={() => (popupData.guideOpen = true)}>
                 <img src={iconTouping} />
               </div>
-              <div
+              {/* <div
                 class={styles.rightBtn}
                 onClick={() => {
                   openStudyTool({
@@ -1356,7 +1294,7 @@ export default defineComponent({
                 }}
               >
                 <img src={iconPen} />
-              </div>
+              </div> */}
               {/* <div class={styles.rightBtn} onClick={() => (popupData.toolOpen = true)}>
                 <img src={iconMore} />
               </div> */}
@@ -1366,7 +1304,7 @@ export default defineComponent({
 
         {/* 更多弹窗 */}
         <Popup
-          class={styles.popupMore}
+          class={[styles.popupMore, styles.popupCoursewarePlay]}
           overlayClass={styles.overlayClass}
           position="right"
           round
@@ -1377,8 +1315,7 @@ export default defineComponent({
         </Popup>
 
         <Popup
-          class={styles.popup}
-          style={{ background: 'rgba(0,0,0, 0.75)' }}
+          class={[styles.popup, styles.popupCoursewarePlay]}
           overlayClass={styles.overlayClass}
           position="right"
           round
@@ -1398,7 +1335,40 @@ export default defineComponent({
         </Popup>
 
         <Popup
-          class={styles.popup}
+          class={[styles.popup, styles.popupCoursewarePlay]}
+          overlayClass={styles.overlayClass}
+          position="right"
+          round
+          v-model:show={popupData.coursewareOpen}
+          onClose={handleClosePopup}>
+            {/* 课件类型 */}
+            <CoursewareType list={data.refLevelList} onConfirm={async (item: any) => {
+              // 判断是否为当前课程类型
+              if(data.currentId === item.id) {
+                return
+              }
+              
+              // 
+              const n = await getDetail(item.id);
+              const s = await getRefLevel(item.id);
+              if(n && s) {
+                data.currentId = item.id;
+                isCurrentCoursewareMenu.value = item.id === route.query.id ? true : false
+                popupData.coursewareOpen = false;
+                popupData.activeIndex = 0;
+                nextTick(() => {
+                  popupData.open = true
+                })
+              } else {
+                if(!isOnline.value) {
+                  showToast('网络异常')
+                }
+              }
+            }} />
+        </Popup>
+
+        <Popup
+          class={[styles.popup, styles.popupCoursewarePlay]}
           overlayClass={styles.overlayClass}
           position="right"
           round
@@ -1408,9 +1378,18 @@ export default defineComponent({
           <OGuide />
         </Popup>
 
-        {studyData.penShow && (
-          <Pen show={studyData.type === 'pen'} close={() => closeStudyTool()} />
-        )}
+        <Popup
+          class={[styles.popup, styles.popupPoint]}
+          round
+          style={{ background: 'transparent !important' }}
+          v-model:show={popupData.pointOpen}
+          onClose={handleClosePopup}>
+          <CoursewareTips onClose={() => {
+            popupData.pointOpen = false
+          }} content={popupData.pointContent} titleName={popupData.pointTitle} />
+        </Popup>
+
+        <GlobalTools />
       </div>
     )
   }

+ 4 - 2
src/views/coursewarePlay/playRecordTime.tsx

@@ -2,7 +2,6 @@ import request from '@/helpers/request'
 import { getSecondRPM } from '@/helpers/utils'
 import { state } from '@/state'
 import { usePageVisibility } from '@vant/use'
-import dayjs from 'dayjs'
 import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
 import { useRoute } from 'vue-router'
 import styles from './index.module.less'
@@ -13,6 +12,10 @@ export default defineComponent({
     list: {
       type: Array,
       default: () => []
+    },
+    isCurrentCoursewareMenu: {
+      type: Boolean,
+      default: false,
     }
   },
   setup(props, { expose }) {
@@ -73,7 +76,6 @@ export default defineComponent({
       const playTime = saveModel.currentTime - saveModel.startTime
       // 1分钟记录1次
       if (playTime >= 5 || isOut) {
-        console.log('isOut', isOut)
         saveModel.startTime = saveModel.currentTime
         request.post(`${state.platformApi}/courseSchedule/coursewarePlayTime`, {
           params: {

+ 121 - 27
src/views/exercise-after-class/index.module.less

@@ -28,22 +28,60 @@
   left: 0;
   right: 0;
   z-index: 1;
-  padding: 10px 24px;
   display: flex;
-  align-items: center;
-  color: #fff;
-  font-size: 12px;
+  align-items: flex-start;
+  justify-content: space-between;
   background: linear-gradient(180deg, rgba(0, 0, 0, 0.6), transparent);
+  transition: transform 0.5s;
+  box-sizing: border-box;
 }
 
+
 .backBtn {
   color: #fff;
-  width: 40px;
-  height: 26px;
+  height: 100%;
   display: flex;
   justify-content: space-between;
-  align-items: center;
+  align-items: flex-start;
   z-index: 10;
+  font-size: 18px;
+  padding: 12px 15px 20px 40px;
+
+  :global {
+    .van-icon {
+      margin-right: 8px;
+    }
+  }
+
+  .titleSection {
+    .title {
+      font-weight: 600;
+      font-size: 16px;
+      color: #ffffff;
+      line-height: 22px;
+    }
+  }
+  .titleContent {
+    display: flex;
+    align-items: center;
+    padding-top: 6px;
+    p {
+      font-size: 14px;
+      color: #ffffff;
+      line-height: 20px;
+    }
+    span {
+      margin-left: 6px;
+      font-size: 11px;
+      color: #ffffff;
+      line-height: 1.3;
+      background: rgba(0, 0, 0, 0.1);
+      border-radius: 10px;
+      border: 1px solid rgba(255, 255, 255, 0.7);
+      padding: 2px 8px;
+      box-sizing: content-box;
+    }
+  }
 }
 
 // .btnGroup {
@@ -74,19 +112,43 @@
 //     margin-bottom: 2px;
 //   }
 // }
+
+.nums {
+  background: rgba(0, 0, 0, 0.4);
+  border-radius: 20px;
+  font-size: 12px;
+  padding: 6px 8px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #fff;
+  margin-right: 40px;
+  margin-top: 12px;
+
+  .timeLoad {
+    width: 4px;
+    height: 4px;
+    background: #ff4e19;
+    // border: 0.5px solid #ffffff;
+    border-radius: 50%;
+    margin-right: 6px;
+    // animation: loadFade 1s ease-in-out infinite;
+  }
+}
+
 .goPractice {
-  width: 89px;
-  height: 32px;
+  position: absolute;
+  right: 39px;
+  bottom: 12px;
+  width: 86px;
+  height: 30px;
   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);
+    transform: translateY(55px);
   }
 }
 
@@ -220,36 +282,68 @@
   bottom: 0;
   z-index: 10;
   background: linear-gradient(0deg, rgba(0, 0, 0, 0.5), transparent);
-  padding: 0 30px;
+  padding: 10px 0 0;
 
   .time {
     display: flex;
-    // justify-content: space-between;
+    flex-shrink: 0;
     color: #fff;
-    font-size: 10px;
-    padding: 4px 0;
+    font-size: 14px;
+    font-weight: 400;
+    padding: 4px 0 4px 0;
+    width: 96px;
   }
 
   .slider {
-    padding: 10px 0;
+    // padding: 10px 0;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    // width: 100%;
+    padding: 0 40px 0;
+
+    --van-slider-button-width: 13px !important;
+    --van-slider-button-height: 13px !important;
+    --van-slider-active-background-color: #FF8057 !important;
+    --van-slider-bar-height: 2px !important;
+
+    :global {
+      .n-slider {
+        --n-handle-size: 13px !important;
+        --n-fill-color: var(--van-primary-color) !important;
+        --n-fill-color-hover: var(--van-primary-color) !important;
+      }
+
+      .van-loading {
+        width: 100%;
+        height: 100%;
+      }
+      .van-slider__button {
+        box-shadow: none;
+      }
+    }
   }
 
   .actions {
+    position: relative;
     display: flex;
-    justify-content: space-between;
-    color: #fff;
-    font-size: 12px;
     align-items: center;
+    justify-content: space-between;
+    padding: 8px 40px 12px 36px;
 
     .actionBtn {
       display: flex;
-    }
+      width: 28px;
+      height: 28px;
+      margin-right: 16px;
+      background: transparent;
+      box-sizing: content-box;
+
 
-    .actionBtn>img {
-      width: 26px;
-      height: 26px;
-      display: block;
-      padding: 8px 8px 14px 8px;
+      >img {
+        width: 100%;
+        height: 100%;
+      }
     }
   }
 }

+ 56 - 21
src/views/exercise-after-class/index.tsx

@@ -1,12 +1,12 @@
-import { Icon, showConfirmDialog, showToast, Swipe, SwipeItem } from 'vant'
+import { Icon, Popup, 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, promisefiyPostMessage } from '@/helpers/native-message'
+import iconBack from '../coursewarePlay/image/back.png'
+import { postMessage } from '@/helpers/native-message'
 // import iconLoop from '../coursewarePlay/image/icon-loop.svg'
 // import iconLoopActive from '../coursewarePlay/image/icon-loop-active.svg'
 // import iconplay from '../coursewarePlay/image/icon-play.svg'
@@ -21,6 +21,7 @@ import { handleCheckVip } from '../hook/useFee'
 import VideoClass from './video-class'
 import item from '@/student/coupons/item'
 import { usePageVisibility } from '@vant/use'
+import CoursewareTips from '../coursewarePlay/component/courseware-tips'
 
 const materialType = {
   视频: 'VIDEO',
@@ -36,19 +37,19 @@ export default defineComponent({
     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 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()
+      // setContainer()
       // 横屏
       postMessage({
         api: 'setRequestedOrientation',
@@ -99,6 +100,16 @@ export default defineComponent({
       timer: null as any,
       item: null as any
     })
+    const onTitleTip = (type: "phaseGoals" | "checkItem", text: string) => {
+      handleStopVideo()
+      popupData.pointOpen = true
+      popupData.pointContent = text
+      if(type === "checkItem") {
+        popupData.pointTitle = '检查事项'
+      } else if(type === "phaseGoals") {
+        popupData.pointTitle = '阶段目标'
+      }
+    }
     // 获取课后练习记录
     const getTrainingRecord = async () => {
       try {
@@ -186,6 +197,8 @@ export default defineComponent({
 
       setRecord(trainings)
       handleCheckVip()
+
+      console.log(activeData.model, data.itemList, 'itemList')
     })
     // 返回
     const goback = () => {
@@ -194,6 +207,9 @@ export default defineComponent({
 
     const swipeRef = ref()
     const popupData = reactive({
+      pointOpen: false,
+      pointContent: "", 
+      pointTitle: "",
       firstIndex: 0,
       open: false,
       activeIndex: -1,
@@ -414,16 +430,25 @@ export default defineComponent({
           <Transition name="top">
             {activeData.model && (
               <div class={styles.headerContainer} ref={headeRef}>
-                <div class={styles.backBtn} onClick={() => goback()}>
-                  <Icon name={iconBack} />
-                  返回
+                <div class={styles.backBtn}>
+                  <Icon name={iconBack}  onClick={() => goback()} />
+                  <div class={styles.titleSection}>
+                    <div class={styles.title}  onClick={() => goback()}>{popupData.tabName}</div>
+                      <div class={styles.titleContent}>
+                        <p>{data.itemList[0]?.materialName}</p>
+                        {/* {data.detail?.lessonTargetDesc ? <span onClick={() => onTitleTip('phaseGoals', data.detail?.lessonTargetDesc)}>阶段目标</span>: ""} */}
+                        {data.itemList[0]?.checkItem ? <span onClick={() => onTitleTip('checkItem', data.itemList[0]?.checkItem)}>检查事项</span> : ""}
+                      </div>
+                  </div>
                 </div>
-                <div class={styles.menu}>{popupData.tabName}</div>
                 {/* 判断作业是否过期 */}
                 {!data.expireTimeFlag && (
                   <div class={styles.nums}>
-                    观看视频模仿并练习:{data.videoData?.trainingTimes || 0}/
-                    {data.videoData?.trainingContent?.practiceTimes || 0}
+                    <div class={styles.timeLoad}></div>
+                    <div>
+                      观看视频模仿并练习:{data.videoData?.trainingTimes || 0}/
+                      {data.videoData?.trainingContent?.practiceTimes || 0}
+                    </div>
                   </div>
                 )}
               </div>
@@ -456,6 +481,16 @@ export default defineComponent({
             )} */}
           {/* </Transition> */}
         </div>
+
+        <Popup
+          class={[styles.popup, styles.popupPoint]}
+          round
+          style={{ background: 'transparent !important' }}
+          v-model:show={popupData.pointOpen}>
+          <CoursewareTips onClose={() => {
+            popupData.pointOpen = false
+          }} content={popupData.pointContent} titleName={popupData.pointTitle} />
+        </Popup>
       </div>
     )
   }

+ 37 - 21
src/views/exercise-after-class/video-class.tsx

@@ -1,4 +1,4 @@
-import { defineComponent, onMounted, ref, watch, Transition, toRefs, nextTick } from 'vue'
+import { defineComponent, onMounted, ref, watch, Transition, toRefs, nextTick, reactive } from 'vue'
 import styles from './index.module.less'
 import { Slider } from 'vant'
 import iconplay from '../coursewarePlay/image/icon-play.svg'
@@ -116,7 +116,24 @@ export default defineComponent({
       }
     }
 
+    /** 设置播放容器 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'
+      }
+    }
+
     onMounted(() => {
+      setContainer()
       videoItem.value = TCPlayer(videoID, {
         appID: '',
         controls: false,
@@ -145,31 +162,30 @@ export default defineComponent({
             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 style={{ width: parentContainer.width, height: '100%', margin: '0 auto' }}>
+            <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}
+            >
+              <source src={item.value.content} type="video/mp4" />
+            </video>
+            <div class={styles.videoSection}></div>
+          </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}>
+                <div class={styles.time}>
+                  <span>{getSecondRPM(item.value.currentTime)}</span>/
+                  <span>{getSecondRPM(item.value.duration)}</span>
+                </div>
                 {item.value.duration && (
                   <Slider
                     buttonSize={16}

+ 1 - 1
vite.config.ts

@@ -12,7 +12,7 @@ function resolve(dir: string) {
 // https://vitejs.dev/config/
 // https://github.com/vitejs/vite/issues/1930 .env
 // const proxyUrl = 'https://online.lexiaoya.cn/';
-const proxyUrl = 'https://test.lexiaoya.cn/'
+const proxyUrl = 'https://dev.lexiaoya.cn/'
 // const proxyUrl = 'http://47.98.131.38:8989/'
 // const proxyUrl = 'http://192.168.3.20:8989/' // 邹旋
 // const proxyUrl = 'http://192.168.3.143:8989/' // 尚科

部分文件因为文件数量过多而无法显示