Преглед изворни кода

Merge branch '2023-3-28_bai-ban' into online

liushengqiang пре 1 година
родитељ
комит
ba80ce22d0

+ 3 - 6
src/views/coursewarePlay/component/musicScore.tsx

@@ -39,7 +39,7 @@ export default defineComponent({
     const renderSuccess = ref(false)
     const Authorization = sessionStorage.getItem('Authorization') || ''
     const origin = /(localhost|192)/.test(location.host)
-      ? 'https://ponline.colexiu.com' //'http://localhost:3000' ////
+      ? 'https://test.lexiaoya.cn'
       : location.origin
     const query = qs.stringify({
       id: props.music.content,
@@ -119,14 +119,11 @@ export default defineComponent({
             }}
           >
             <img src={iconStart} />
-            {/* {isLoading.value && (
-              <Loading class={styles.loading} color="rgba(63,134,237,1)" size={16} />
-            )} */}
           </div>
         )}
-        {<div class={styles.skeletonWrap}>
+        <div class={styles.skeletonWrap}>
           <Skeleton class={styles.skeleton} row={8} />
-        </div>}
+        </div>
       </div>
     )
   }

+ 32 - 0
src/views/coursewarePlay/component/tool.module.less

@@ -0,0 +1,32 @@
+.tool {
+    position: relative;
+    width: 220px;
+    box-sizing: border-box;
+    height: 100vh;
+    color: #fff;
+    font-size: 13px;
+    font-weight: 500;
+    line-height: 18px;
+    * {
+        box-sizing: border-box;
+    }
+}
+
+.title {
+    padding: 12px 18px;
+}
+.grid{
+    :global{
+        .van-grid-item__content{
+            background: transparent;
+            padding: var(--van-padding-xs) var(--van-padding-xs);
+        }
+        .van-grid-item__text{
+            color: inherit;
+            margin-top: 2px;
+        }
+        .van-grid-item__icon{
+            font-size: 22px;
+        }
+    }
+}

+ 40 - 0
src/views/coursewarePlay/component/tool.tsx

@@ -0,0 +1,40 @@
+import { Grid, GridItem } from 'vant'
+import { defineComponent } from 'vue'
+import styles from './tool.module.less'
+import iconPen from '../image/icon-pen.png'
+
+export type ToolType =  'init' | 'pen'
+
+export type ToolItem = {
+    type: ToolType
+    name: string
+    icon: string
+}
+
+export default defineComponent({
+  name: 'tool',
+  emits: ['handleTool'],
+  setup(props, { emit }) {
+    const tool: ToolItem[] = [
+      {
+        type: 'pen',
+        icon: iconPen,
+        name: '批注'
+      }
+    ]
+    return () => (
+      <div class={styles.tool}>
+        <div class={styles.title}>教学功能</div>
+        <Grid class={styles.grid} columnNum={3} border={false}>
+          {tool.map((item) => (
+            <GridItem
+              icon={item.icon}
+              text={item.name}
+              onClick={() => emit('handleTool', item)}
+            ></GridItem>
+          ))}
+        </Grid>
+      </div>
+    )
+  }
+})

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

@@ -0,0 +1,43 @@
+.pen{
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    top: 0;
+    z-index: 11;
+}
+.open{
+    display: block;
+}
+.hide{
+    display: none;
+}
+.iframe{
+    display: block;
+    width: 100%;
+    height: 100%;
+    border: 0;
+}
+.dely{
+    opacity: 0;
+}
+.rightItem{
+    position: absolute;
+    right: 15Px;
+    bottom: 0;
+    bottom: constant(safe-area-inset-bottom);
+    bottom: env(safe-area-inset-bottom);
+    width: 50Px;
+    height: 54Px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+.img{
+    position: absolute;
+    left: 0;
+    top: 0;
+    width: 100%;
+    height: 100%;
+    display: block;
+}

+ 141 - 0
src/views/coursewarePlay/component/tools/pen.tsx

@@ -0,0 +1,141 @@
+import { promisefiyPostMessage } from '@/helpers/native-message'
+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'
+
+export default defineComponent({
+  name: 'pen',
+  props: {
+    show: {
+      type: Boolean,
+      default: false
+    },
+    close: {
+      type: Function,
+      default: () => {}
+    }
+  },
+  setup(props) {
+    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`
+
+    const exportImg = (event: MessageEvent) => {
+      const data = event.data
+      // console.log('🚀 ~ event:', data)
+      if (data.api === 'excalidraw_exportImg') {
+        imgs.base64 = data.base64
+        imgs.exported = true
+        nextTick(() => {
+          onSaveImg()
+        })
+      }
+    }
+    onMounted(() => {
+      window.addEventListener('message', exportImg)
+    })
+    onUnmounted(() => {
+      window.removeEventListener('message', exportImg)
+    })
+
+    const imgs = reactive({
+      exported: false,
+      saveLoading: false,
+      base64: '',
+      image: ''
+    })
+
+    const saveImg = async () => {
+      showLoadingToast({ message: '图片生成中...', forbidClick: true })
+      setTimeout(() => {
+        imgs.saveLoading = false
+      }, 100)
+      const res = await promisefiyPostMessage({
+        api: 'savePicture',
+        content: {
+          base64: imgs.image
+        }
+      })
+      if (res?.content?.status === 'success') {
+        showSuccessToast('保存成功')
+      } else {
+        showFailToast('保存失败')
+      }
+      imgs.exported = false
+    }
+
+    const onSaveImg = async () => {
+      // 判断是否在保存中...
+      if (imgs.saveLoading) {
+        return
+      }
+      console.log('开始')
+      imgs.saveLoading = true
+      const container: any = document.getElementById(`app`)
+      html2canvas(container, {
+        allowTaint: true,
+        useCORS: true,
+        backgroundColor: null
+      })
+        .then(async (canvas) => {
+          // console.log("🚀 ~ canvas:", canvas)
+          // document.body.appendChild(canvas)
+          // const url = await canvas.toDataURL()
+          try {
+            imgs.image = canvas.toDataURL()
+            
+          } catch (error) {
+            console.log(error)
+          }
+          console.log("🚀 ~ imgs.image:", imgs.image)
+          saveImg()
+        })
+        .catch((error) => {
+          console.log("🚀 ~ error:", error)
+          closeToast()
+          imgs.saveLoading = false
+          imgs.exported = false
+        })
+    }
+
+    return () => (
+      <div
+        class={[
+          styles.pen,
+          firstRender.value ? styles.dely : '',
+          show.value ? styles.open : styles.hide
+        ]}
+      >
+        <iframe
+          class={styles.iframe}
+          frameborder="0"
+          width="100vw"
+          height="100vh"
+          src={src}
+          onLoad={() => {
+            firstRender.value = false
+          }}
+        ></iframe>
+        {imgs.exported ? (
+          <img crossorigin="anonymous" class={styles.img} src={imgs.base64} />
+        ) : (
+          <div
+            class={styles.rightItem}
+            onClick={() => props.close()}
+          >
+            <svg width="22px" height="20px" viewBox="0 0 22 20">
+              <path
+                transform="translate(-1.000000, -2.000000)"
+                fill="#FFFFFF"
+                d="M13,2 C13.5522847,2 14,2.44771525 14,3 C14,3.51283584 13.6139598,3.93550716 13.1166211,3.99327227 L13,4 L3,4 L3,20 L13,20 C13.5128358,20 13.9355072,20.3860402 13.9932723,20.8833789 L14,21 C14,21.5128358 13.6139598,21.9355072 13.1166211,21.9932723 L13,22 L2,22 C1.48716416,22 1.06449284,21.6139598 1.00672773,21.1166211 L1,21 L1,3 C1,2.48716416 1.38604019,2.06449284 1.88337887,2.00672773 L2,2 L13,2 Z M17.7071068,7.05025253 L21.9497475,11.2928932 L21.9497475,11.2928932 C22.3402718,11.6834175 22.3402718,12.3165825 21.9497475,12.7071068 L17.7071068,16.9497475 C17.3165825,17.3402718 16.6834175,17.3402718 16.2928932,16.9497475 C15.9023689,16.5592232 15.9023689,15.9260582 16.2928932,15.5355339 L18.828,12.999 L9.29368112,13 C8.74139637,13 8.29368112,12.5522847 8.29368112,12 C8.29368112,11.4871642 8.67972131,11.0644928 9.17706,11.0067277 L9.29368112,11 L18.827,10.999 L16.2928932,8.46446609 C15.9023689,8.0739418 15.9023689,7.44077682 16.2928932,7.05025253 C16.6834175,6.65972824 17.3165825,6.65972824 17.7071068,7.05025253 Z"
+              ></path>
+            </svg>
+          </div>
+        )}
+      </div>
+    )
+  }
+})

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


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


+ 31 - 14
src/views/coursewarePlay/index.module.less

@@ -33,18 +33,23 @@
   display: flex;
   align-items: center;
   justify-content: space-between;
+  height: 40px;
   background: linear-gradient(180deg, rgba(0, 0, 0, 0.6), transparent);
   transition: transform 0.5s;
+  box-sizing: border-box;
+  div{
+    box-sizing: border-box;
+  }
 }
 
 .backBtn {
   color: #fff;
-  height: 26px;
+  height: 100%;
   display: flex;
   justify-content: space-between;
   align-items: center;
   z-index: 10;
-  padding: 4px 10px 4px 15px;
+  padding: 0 15px;
 
   :global {
     .van-icon {
@@ -52,6 +57,27 @@
     }
   }
 }
+.headRight{
+  position: relative;
+  z-index: 10;
+  display: flex;
+  align-items: center;
+  margin-left: auto;
+  height: 100%;
+  padding-right: 15px;
+  .rightBtn{
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    padding: 0 10px;
+    img{
+      width: 22px;
+      height: 22px;
+      display: block;
+    }
+  }
+}
 
 .menu {
   position: absolute;
@@ -330,15 +356,6 @@
   }
 }
 
-// .preItem{
-//   transform: translate3d(0, 0, -800px) rotateX(180deg);
-// }
-// .nextItem {
-//   transform: translate3d(0, 0, -800px) rotateX(-180deg);
-// }
-// .preItem{
-//   transform: translate3d(-100%, 0, -800px);
-// }
-// .nextItem {
-//   transform: translate3d(100%, 0, -800px);
-// }
+.popupMore{
+  background: rgba(0, 0, 0, 0.8);
+}

+ 104 - 23
src/views/coursewarePlay/index.tsx

@@ -34,12 +34,9 @@ import iconMenu from './image/icon-menu.svg'
 import iconDian from './image/icon-dian.svg'
 import iconTouping from './image/icon-touping.svg'
 import iconPoint from './image/icon-point.svg'
-import iconLoop from './image/icon-loop.svg'
-import iconLoopActive from './image/icon-loop-active.svg'
-import iconplay from './image/icon-play.svg'
-import iconpause from './image/icon-pause.svg'
 import iconUp from './image/icon-up.svg'
 import iconDown from './image/icon-down.svg'
+import iconMore from './image/icon-more.png'
 import Points from './component/points'
 import { browser, getSecondRPM } from '@/helpers/utils'
 import { Vue3Lottie } from 'vue3-lottie'
@@ -63,6 +60,9 @@ import 'swiper/less/effect-flip'
 import 'swiper/less/effect-creative'
 import { handleCheckVip } from '../hook/useFee'
 import OGuide from '@/components/o-guide'
+import Tool, { ToolItem, ToolType } from './component/tool'
+import Tools from './component/tools/pen'
+import Pen from './component/tools/pen'
 
 export default defineComponent({
   name: 'CoursewarePlay',
@@ -152,6 +152,7 @@ export default defineComponent({
       videoRefs: {}
     })
     const activeData = reactive({
+      isAutoPlay: true, // 是否自动播放
       nowTime: 0,
       model: true, // 遮罩
       isAnimation: true, // 是否动画
@@ -226,7 +227,11 @@ export default defineComponent({
       let _firstIndex = list.findIndex((n: any) => n.materialId == route.query.kId)
       _firstIndex = _firstIndex > -1 ? _firstIndex : 0
       const item = list[_firstIndex]
-      item.autoPlay = true
+
+      // 是否自动播放
+      if (activeData.isAutoPlay) {
+        item.autoPlay = true
+      }
       popupData.activeIndex = _firstIndex
       popupData.tabName = item.tabName
       popupData.tabActive = item.knowledgePointId
@@ -265,7 +270,7 @@ export default defineComponent({
           })
           showDialog({
             title: '温馨提示',
-            message: '课件已锁定',
+            message: '课件已锁定'
           }).then((value) => {
             goback()
           })
@@ -390,7 +395,8 @@ export default defineComponent({
       tabName: '',
       itemActive: '',
       itemName: '',
-      guideOpen: false
+      guideOpen: false,
+      toolOpen: false // 工具弹窗控制
     })
 
     /**停止所有的播放 */
@@ -423,6 +429,17 @@ export default defineComponent({
         Object.values(data.videoRefs).map((n: any) => n.toggleHideControl(false))
       }, 4000)
     }
+    /** 立即收起所有的模态框 */
+    const clearModel = () => {
+      clearTimeout(activeData.timer)
+      closeToast()
+      activeData.model = false
+      Object.values(data.videoRefs).map((n: any) => n.toggleHideControl(false))
+    }
+    const toggleModel = (type: boolean = true) => {
+      activeData.model = type
+      Object.values(data.videoRefs).map((n: any) => n.toggleHideControl(type))
+    }
 
     // 去点名,签退
     const gotoRollCall = (pageTag: string) => {
@@ -594,8 +611,49 @@ export default defineComponent({
       }
     }
 
+    /** 弹窗关闭 */
+    const handleClosePopup = () => {
+      const item = data.itemList[popupData.activeIndex]
+      if (item?.type == 'VIDEO' && !item.videoEle?.paused) {
+        setModelOpen()
+      }
+    }
+
+    /** 教学数据 */
+    const studyData = reactive({
+      type: '' as ToolType,
+      penShow: false
+    })
+
+    /** 打开教学工具 */
+    const openStudyTool = (item: ToolItem) => {
+      const activeItem = data.itemList[popupData.activeIndex]
+      // 暂停视频和曲谱的播放
+      if (activeItem.type === 'VIDEO' && activeItem.videoEle) {
+        activeItem.videoEle.pause()
+      }
+      if (activeItem.type === 'SONG') {
+        activeItem.iframeRef?.contentWindow?.postMessage({ api: 'setPlayState' }, '*')
+      }
+      clearModel()
+      popupData.toolOpen = false
+      studyData.type = item.type
+
+      switch (item.type) {
+        case 'pen':
+          studyData.penShow = true
+          break
+      }
+    }
+
+    /** 关闭教学工具 */
+    const closeStudyTool = () => {
+      studyData.type = 'init'
+      toggleModel()
+    }
+
     return () => (
-      <div class={styles.playContent}>
+      <div id="playContent" class={styles.playContent}>
         <div
           onClick={() => {
             clearTimeout(activeData.timer)
@@ -724,10 +782,10 @@ export default defineComponent({
                   </div>
 
                   <div class={[styles.btnsWrap, styles.btnsBottom]}>
-                    <div class={styles.fullBtn} onClick={() => (popupData.guideOpen = true)}>
+                    {/* <div class={styles.fullBtn} onClick={() => (popupData.guideOpen = true)}>
                       <img src={iconTouping} />
                       <span>投屏</span>
-                    </div>
+                    </div> */}
                     {data.isCourse && (
                       <>
                         <div
@@ -783,6 +841,7 @@ export default defineComponent({
             <Icon name={iconBack} />
             返回
           </div>
+          {data.isCourse && <PlayRecordTime ref={playRef} list={data.knowledgePointList} />}
           <div
             class={styles.menu}
             onClick={() => {
@@ -793,21 +852,44 @@ export default defineComponent({
           >
             {popupData.tabName}
           </div>
-          {data.isCourse && <PlayRecordTime ref={playRef} list={data.knowledgePointList} />}
+
+          {state.platformType == 'TEACHER' && (
+            <div
+              class={styles.headRight}
+              onClick={(e: Event) => {
+                e.stopPropagation()
+                clearTimeout(activeData.timer)
+              }}
+            >
+              <div class={styles.rightBtn} onClick={() => (popupData.guideOpen = true)}>
+                <img src={iconTouping} />
+              </div>
+              <div class={styles.rightBtn} onClick={() => (popupData.toolOpen = true)}>
+                <img src={iconMore} />
+              </div>
+            </div>
+          )}
         </div>
 
+        {/* 更多弹窗 */}
+        <Popup
+          class={styles.popupMore}
+          overlayClass={styles.overlayClass}
+          position="right"
+          round
+          v-model:show={popupData.toolOpen}
+          onClose={handleClosePopup}
+        >
+          <Tool onHandleTool={openStudyTool} />
+        </Popup>
+
         <Popup
           class={styles.popup}
           overlayClass={styles.overlayClass}
           position="right"
           round
           v-model:show={popupData.open}
-          onClose={() => {
-            const item = data.itemList[popupData.activeIndex]
-            if (item?.type == 'VIDEO' && !item.videoEle?.paused) {
-              setModelOpen()
-            }
-          }}
+          onClose={handleClosePopup}
         >
           <Points
             data={data.knowledgePointList}
@@ -826,15 +908,14 @@ export default defineComponent({
           position="right"
           round
           v-model:show={popupData.guideOpen}
-          onClose={() => {
-            const item = data.itemList[popupData.activeIndex]
-            if (item?.type == 'VIDEO' && !item.videoEle?.paused) {
-              setModelOpen()
-            }
-          }}
+          onClose={handleClosePopup}
         >
           <OGuide />
         </Popup>
+
+        {studyData.penShow && (
+          <Pen show={studyData.type === 'pen'} close={() => closeStudyTool()} />
+        )}
       </div>
     )
   }