Browse Source

添加页面

lex-xin 4 months ago
parent
commit
d4eca7bc8e

+ 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
+}

BIN
src/components/globalTools/images/g-arrow-right.png


BIN
src/components/globalTools/images/icon-note.png


BIN
src/components/globalTools/images/icon-tool.png


BIN
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>
+    );
+  }
+});

+ 0 - 1
src/views/courseList/index.tsx

@@ -241,7 +241,6 @@ export default defineComponent({
           }
         })
       }
-      
     }
     // 检查数据的缓存状态
     const checkCoursewareCache = (list: []): Promise<any[]> => {

+ 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>
+  }
+})

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

@@ -0,0 +1,176 @@
+.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: 20px;
+  }
+}
+
+.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;
+        }
+      }
+    }
+  }
+}

File diff suppressed because it is too large
+ 28 - 0
src/views/coursewarePlay/component/courseware-type/index.tsx


+ 1 - 1
src/views/coursewarePlay/component/points.tsx

@@ -59,7 +59,7 @@ export default defineComponent({
       <div class={styles.container}>
         <div class={styles.pointHead}>
           <img src={iconMulv} />
-          课程目录
+          知识点目录
         </div>
         <div class={styles.content}>
           <Collapse

BIN
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>
+    );
+  }
+});

BIN
src/views/coursewarePlay/component/tips/top-bg.png


+ 48 - 22
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={{

File diff suppressed because it is too large
+ 32 - 23
src/views/coursewarePlay/component/video.module.less


BIN
src/views/coursewarePlay/image/btn_go_practice.png


BIN
src/views/coursewarePlay/image/icon-current.png


File diff suppressed because it is too large
+ 0 - 0
src/views/coursewarePlay/image/icons.json


+ 41 - 16
src/views/coursewarePlay/index.module.less

@@ -80,7 +80,7 @@
       margin-left: 6px;
       font-size: 11px;
       color: #ffffff;
-      line-height: 1.4;
+      line-height: 1.3;
       background: rgba(0, 0, 0, 0.1);
       border-radius: 10px;
       border: 1px solid rgba(255, 255, 255, 0.7);
@@ -213,11 +213,17 @@
   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;
@@ -237,21 +243,20 @@
   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 {
-  // background: rgba(51, 51, 51, 0.4);
-  // border-radius: 6px;
-  // overflow: hidden;
+  background: rgba(51, 51, 51, 0.4);
+  border-radius: 6px;
+  overflow: hidden;
 }
 
 .bottomFixedContainer {
@@ -305,6 +310,22 @@
 .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;
@@ -422,18 +443,22 @@
   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;
+  // position: absolute;
+  // right: 16px;
+  // bottom: 60px;
   z-index: 11;
-  transition: all 0.5s ease;
+  transition: all .5s ease;
 
   &.hide {
-    transform: translateX(66px);
+    transform: translateY(55px);
   }
 }

+ 100 - 21
src/views/coursewarePlay/index.tsx

@@ -28,7 +28,7 @@ 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 +37,15 @@ 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'
 
 export default defineComponent({
   name: 'CoursewarePlay',
   setup() {
     const pageVisibility = usePageVisibility()
+    const { isOnline } = useNetwork()
     /** 页面显示和隐藏 */
     watch(
       () => pageVisibility.value,
@@ -112,6 +116,7 @@ export default defineComponent({
     const route = useRoute()
     const headeRef = ref()
     const data = reactive({
+      currentId: route.query.id as any,
       detail: null as any,
       knowledgePointList: [] as any,
       itemList: [] as any,
@@ -120,7 +125,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 +277,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 +343,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 +463,28 @@ export default defineComponent({
         //
       }
     }
+
+    const getRefLevel = async (id?: any) => {
+      try {
+        const res = await request.post(state.platformApi + '/tenantAlbumMusic/refLevel', {
+          data: {
+            lessonCoursewareDetailId: id || route.query.id
+          }
+        })
+        data.refLevelList = res.data || []
+        return true
+      } catch {
+        // 
+      }
+    }
+
     onMounted(async () => {
       await sysParamConfig()
 
       if (state.platformType === 'STUDENT') {
         await getLookVideoData()
       }
+      await getRefLevel()
       await getDetail()
       const hasFree = String(data.detail?.accessScope) === '0'
       if (!hasFree) {
@@ -507,6 +543,10 @@ export default defineComponent({
     }
 
     const popupData = reactive({
+      pointOpen: false,
+      pointContent: "", 
+      pointTitle: "",
+      coursewareOpen: false,
       open: false,
       activeIndex: 0,
       playIndex: 0,
@@ -1136,30 +1176,24 @@ export default defineComponent({
                     <img src={iconMenu} />
                     {/* <span>知识点</span> */}
                   </div>
-                {popupData.activeIndex != 0 && (
-                  
                     <div
-                      class={styles.fullBtn}
+                      class={[styles.fullBtn, !(popupData.activeIndex != 0) && styles.disabled]}
                       onClick={() => {
-                        handlePreAndNext('up')
+                        if(popupData.activeIndex != 0) handlePreAndNext('up')
                       }}
                     >
                       <img src={iconUp} />
                       {/* <span style={{ textAlign: 'center' }}>上一个</span> */}
                     </div>
-                 
-                )}
-                {popupData.activeIndex != data.itemList.length - 1 && (
                     <div
-                      class={styles.fullBtn}
+                      class={[styles.fullBtn, !(popupData.activeIndex != data.itemList.length - 1) && styles.disabled]}
                       onClick={() => {
-                        handlePreAndNext('down')
+                        if(popupData.activeIndex != data.itemList.length - 1) handlePreAndNext('down')
                       }}
                     >
                       {/* <span style={{ textAlign: 'center' }}>下一个</span> */}
                       <img src={iconDown} />
                     </div>
-                )}
                  </div>
               </div>
             )}
@@ -1175,12 +1209,18 @@ export default defineComponent({
           <div class={styles.backBtn} onClick={() => goback()}>
             <Icon name={iconBack} />
             <div class={styles.titleSection}>
-              <div class={styles.title}>{popupData.tabName}</div>
+              {/* <div class={styles.title}>{popupData.tabName}</div>
               <div class={styles.titleContent}>
                 <p>第3条练习6-15小节-演奏-练习</p>
                 <span>阶段目标</span>
                 <span>检查事项</span>
-              </div>
+              </div> */}
+              <div class={styles.title}>{popupData.tabName}</div>
+                <div class={styles.titleContent}>
+                  <p>{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} />}
@@ -1271,7 +1311,37 @@ 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
+              }
+              data.currentId = item.id;
+              const n = await getDetail(item.id);
+              const s = await getRefLevel(item.id);
+              if(n && s) {
+                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
@@ -1281,9 +1351,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>
     )
   }

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