import { closeToast, Icon, Loading, Popup, showDialog, showToast, Slider, Swipe, SwipeInstance, SwipeItem } from 'vant' import { defineComponent, onMounted, reactive, nextTick, onUnmounted, ref, watch, Transition, TransitionGroup, onBeforeUnmount } from 'vue' import iconBack from './image/back.svg' 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 { listenerMessage, postMessage, promisefiyPostMessage } from '@/helpers/native-message' import MusicScore from './component/musicScore' 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 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' import playLoadData from './datas/data.json' import { usePageVisibility, useRect } from '@vant/use' import PlayRecordTime from './playRecordTime' import VideoPlay from './component/video-play' import { Pagination, Navigation, Virtual, EffectFade, EffectFlip, EffectCreative, Lazy } from 'swiper' import { Swiper, SwiperSlide } from 'swiper/vue' import 'swiper/less' import 'swiper/less/effect-fade' 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' import iconPen from './image/icon-pen.png' import { useThrottle, useThrottleFn, useDebounceFn } from '@vueuse/core' export default defineComponent({ name: 'CoursewarePlay', setup() { const pageVisibility = usePageVisibility() const isPlay = ref(false) /** 页面显示和隐藏 */ watch(pageVisibility, (value) => { const activeItem = data.itemList[popupData.activeIndex] if (activeItem.type != 'VIDEO') return if (value == 'hidden') { isPlay.value = !activeItem.videoEle?.paused togglePlay(activeItem, false) } else { // 页面显示,并且 if (isPlay.value) togglePlay(activeItem, true) } }) /** 设置播放容器 16:9 */ const parentContainer = reactive({ width: '100vw' }) const setContainer = () => { const min = Math.min(screen.width, screen.height) const max = Math.max(screen.width, screen.height) const width = min * (16 / 9) if (width > max) { parentContainer.width = '100vw' return } else { parentContainer.width = width + 'px' } } const handleInit = (type = 0) => { //设置容器16:9 setContainer() // 横屏 postMessage( { api: 'setRequestedOrientation', content: { orientation: type } }, () => { console.log(234) } ) // 头,包括返回箭头 // postMessage({ // api: 'setTitleBarVisibility', // content: { // status: type // } // }) // 安卓的状态栏 postMessage({ api: 'setStatusBarVisibility', content: { isVisibility: type } }) // 进入页面设置常量 postMessage({ api: 'keepScreenLongLight', content: { isOpenLight: type ? true : false } }) } handleInit() onUnmounted(() => { handleInit(1) window.removeEventListener('message', iframeHandle) }) const route = useRoute() const router = useRouter() const headeRef = ref() const data = reactive({ detail: null, knowledgePointList: [] as any, itemList: [] as any, showHead: true, isCourse: false, isRecordPlay: false, videoRefs: {} }) const activeData = reactive({ isAutoPlay: true, // 是否自动播放 nowTime: 0, model: true, // 遮罩 isAnimation: true, // 是否动画 videoBtns: true, // 视频 currentTime: 0, duration: 0, timer: null as any, item: null as any }) // 获取缓存路径 const getCacheFilePath = async (material: any) => { const res = await promisefiyPostMessage({ api: 'getCourseFilePath', content: { url: material.content, localPath: '', materialId: material.materialId, updateTime: material.updateTime, type: material.type // SONG VIDEO IMAGE } }) // console.log('缓存路径返回', res) return res } // 获取当前课程是否签退 const getCourseSchedule = async () => { if (!route.query.courseId) return try { const res = await request.get( `${state.platformApi}/courseSchedule/detail/${route.query.courseId}`, { hideLoading: true } ) if (res?.data) { data.isCourse = res.data.status === 'ING' && state.platformType == 'TEACHER' ? true : false // data.isRecordPlay = Date.now() > dayjs(res.data.startTime).valueOf() } } catch (e) { console.log(e) } } const getTempList = async (materialList: any, name: any) => { const list: any = [] const browserInfo = browser() for (let j = 0; j < materialList.length; j++) { const material = materialList[j] //请求本地缓存 if (browserInfo.isApp && ['VIDEO', 'IMG'].includes(material.type)) { const localData = await getCacheFilePath(material) if (localData?.content?.localPath) { material.url = material.content material.content = localData.content.localPath } } list.push({ ...material, iframeRef: null, videoEle: null, tabName: name, autoPlay: false, //加载完成是否自动播放 isprepare: false, // 视频是否加载完成 isRender: false // 是否渲染了 }) } return list } const getItemList = async () => { const list: any = [] for (let i = 0; i < data.knowledgePointList.length; i++) { const item = data.knowledgePointList[i] if (item.materialList && item.materialList.length > 0) { const tempList = await getTempList(item.materialList, item.name) list.push(...tempList) } // 第二层级 if (item.children && item.children.length > 0) { const childrenList = item.children || [] for (let j = 0; j < childrenList.length; j++) { const childItem = childrenList[j] const tempList = await getTempList(childItem.materialList, childItem.name) list.push(...tempList) } } } // console.log(list, 'list') let _firstIndex = list.findIndex( (n: any) => n.knowledgePointMaterialRelationId == route.query.kId || n.materialId == route.query.kId ) _firstIndex = _firstIndex > -1 ? _firstIndex : 0 const item = list[_firstIndex] // console.log(_firstIndex, '_firstIndex', route.query.kId, 'route.query.kId', item) // 是否自动播放 if (activeData.isAutoPlay) { item.autoPlay = true } popupData.activeIndex = _firstIndex popupData.playIndex = _firstIndex popupData.tabName = item.tabName popupData.tabActive = item.knowledgePointId popupData.itemActive = item.id popupData.itemName = item.name nextTick(() => { data.itemList = list checkedAnimation(popupData.activeIndex) postMessage({ api: 'courseLoading', content: { show: false, type: 'fullscreen' } }) //检测是否录屏 // handleLimitScreenRecord() }) } const getDetail = async () => { try { const res: any = await request.get( state.platformApi + `/lessonCoursewareDetail/detail/${route.query.id}`, { hideLoading: true } ) data.detail = res.data if (res?.data?.lockFlag) { postMessage({ api: 'courseLoading', content: { show: false, type: 'fullscreen' } }) showDialog({ title: '温馨提示', message: '课件已锁定' }).then((value) => { goback() }) return } if (Array.isArray(res?.data?.knowledgePointList)) { let index = 0 data.knowledgePointList = res.data.knowledgePointList.map((n: any) => { if (Array.isArray(n.materialList)) { n.materialList = n.materialList.map((item: any) => { index++ return { ...item, knowledgePointId: [item.knowledgePointId], materialId: item.id, id: index + '' } }) } if (Array.isArray(n.children)) { n.children = n.children.map((cn: any) => { cn.materialList = cn.materialList.map((item: any) => { index++ return { ...item, knowledgePointId: [n.id, item.knowledgePointId], materialId: item.id, id: index + '' } }) return cn }) } return n }) getItemList() } } catch (error) {} } // ifram事件处理 const iframeHandle = (ev: MessageEvent) => { if (ev.data?.api === 'headerTogge') { activeData.model = ev.data.show || (ev.data.playState == 'play' ? false : true) } } //录屏时间触发 const handleLimitScreenRecord = async () => { const result = await promisefiyPostMessage({ api: 'getDeviceStatus', content: { type: 'video' } }) const { status } = result?.content || {} if (status == '1') { data.itemList.forEach((item: any) => (item.autoPlay = false)) handleStop() showDialog({ title: '温馨提示', message: '课件内容请勿录屏', beforeClose: () => { return new Promise(async (resolve) => { const { content } = (await promisefiyPostMessage({ api: 'getDeviceStatus', content: { type: 'video' } })) || {} if (content?.status == '1') { resolve(false) } else { const activeItem = data.itemList[popupData.activeIndex] togglePlay(activeItem, true) resolve(true) } }) } }) } } onMounted(() => { const hasVip = handleCheckVip() if (!hasVip) { nextTick(() => { postMessage({ api: 'courseLoading', content: { show: false, type: 'fullscreen' } }) }) return } getDetail() getCourseSchedule() window.addEventListener('message', iframeHandle) //禁止录屏 ios // listenerMessage('setVideoPlayer', (result) => { // if (result?.content?.status == 'pause') { // handleLimitScreenRecord() // } // }) // 安卓 // postMessage({ // api: 'limitScreenRecord', // content: { // type: 1 // } // }) }) onBeforeUnmount(() => { // postMessage({ // api: 'limitScreenRecord', // content: { // type: 0 // } // }) }) const playRef = ref() // 返回 const goback = () => { try { playRef.value?.handleOut() } catch (error) {} postMessage({ api: 'goBack' }) } const popupData = reactive({ open: false, activeIndex: 0, playIndex: 0, tabActive: '', tabName: '', itemActive: '', itemName: '', guideOpen: false, toolOpen: false // 工具弹窗控制 }) /**停止所有的播放 */ const handleStop = () => { for (let i = 0; i < data.itemList.length; i++) { const activeItem = data.itemList[i] if (activeItem.type === 'VIDEO') { activeItem.videoEle?.pause() activeItem.videoEle?.stop() } // console.log('🚀 ~ activeItem:', activeItem) // 停止曲谱的播放 if (activeItem.type === 'SONG') { activeItem.iframeRef?.contentWindow?.postMessage({ api: 'setPlayState' }, '*') } } } // 切换素材 const toggleMaterial = (itemActive: any) => { const index = data.itemList.findIndex((n: any) => n.id == itemActive) if (index > -1) { handleSwipeChange(index) } } /** 延迟收起模态框 */ const setModelOpen = () => { clearTimeout(activeData.timer) closeToast() activeData.timer = setTimeout(() => { activeData.model = false 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 = true) => { activeData.model = type Object.values(data.videoRefs).map((n: any) => n?.toggleHideControl(type)) } // 去点名,签退 const gotoRollCall = (pageTag: string) => { postMessage({ api: 'open_app_page', content: { action: 'app', pageTag: pageTag, url: '', params: JSON.stringify({ courseId: route.query.courseId }) } }) } // 双击 const handleDbClick = (item: any) => { if (item && item.type === 'VIDEO') { const videoEle: HTMLVideoElement = item.videoEle if (videoEle) { if (videoEle?.paused) { closeToast() videoEle.play() } else { showToast('已暂停') videoEle.pause() } } } } // 切换播放 const togglePlay = (m: any, isPlay: boolean) => { if (isPlay) { m.videoEle?.play() } else { m.videoEle?.pause() } } const showIndex = ref(-4) const effectIndex = ref(0) const effects = [ { prev: { transform: 'translate3d(0, 0, -800px) rotateX(180deg)' }, next: { transform: 'translate3d(0, 0, -800px) rotateX(-180deg)' } }, { prev: { transform: 'translate3d(-100%, 0, -800px)' }, next: { transform: 'translate3d(100%, 0, -800px)' } }, { prev: { transform: 'translate3d(-50%, 0, -800px) rotateY(80deg)' }, next: { transform: 'translate3d(50%, 0, -800px) rotateY(-80deg)' } }, { prev: { transform: 'translate3d(-100%, 0, -800px) rotateY(-120deg)' }, next: { transform: 'translate3d(100%, 0, -800px) rotateY(120deg)' } }, // 风车4 { prev: { transform: 'translate3d(-50%, 50%, -800px) rotateZ(-14deg)', opacity: 0 }, next: { transform: 'translate3d(50%, 50%, -800px) rotateZ(14deg)', opacity: 0 } }, // 翻页5 { prev: { transform: 'translateZ(-800px) rotate3d(0, -1, 0, 90deg)', opacity: 0 }, next: { transform: 'translateZ(-800px) rotate3d(0, 1, 0, 90deg)', opacity: 0 }, current: { transitionDelay: '700ms' } } ] const acitveTimer = ref() // 轮播切换 const handleSwipeChange = (index: number) => { // 如果是当前正在播放 或者是视频最后一个 if (popupData.activeIndex == index) return handleStop() clearTimeout(acitveTimer.value) const oldIndex = popupData.activeIndex checkedAnimation(popupData.activeIndex, index) popupData.activeIndex = index acitveTimer.value = setTimeout( () => { popupData.playIndex = index const item = data.itemList[index] if (item) { popupData.tabActive = item.knowledgePointId popupData.itemActive = item.id popupData.itemName = item.name popupData.tabName = item.tabName if (item.type == 'SONG') { activeData.model = true } } requestAnimationFrame(() => { const _effectIndex = effectIndex.value + 1 effectIndex.value = _effectIndex >= effects.length - 1 ? 0 : _effectIndex if (item && item.type === 'VIDEO') { // 自动播放下一个视频 clearTimeout(activeData.timer) closeToast() item.autoPlay = true nextTick(() => { item.videoEle?.play() }) } }) }, activeData.isAnimation ? 800 : 0 ) } const onChangeSwiper = useDebounceFn((type: string, itemActive?: any) => { if (type === 'up') { handlePreAndNext('up') } if (type === 'down') { handlePreAndNext('down') } if (type === 'change') { popupData.open = false toggleMaterial(itemActive) } }, 200) /** 是否有转场动画 */ const checkedAnimation = (index: number, nextIndex?: number) => { const item = data.itemList[index] const nextItem = data.itemList[nextIndex!] if (nextItem) { if (nextItem.knowledgePointId != item.knowledgePointId) { activeData.isAnimation = true return } const videoEle = item.videoEle const nextVideo = nextItem.videoEle if (videoEle && videoEle.duration < 8 && index < nextIndex!) { activeData.isAnimation = false } else if (nextVideo && nextVideo.duration < 8 && index > nextIndex!) { activeData.isAnimation = false } else { activeData.isAnimation = true } } else { activeData.isAnimation = item?.adviseStudyTimeSecond < 8 ? false : true } } // 上一个知识点, 下一个知识点 const handlePreAndNext = (type: string) => { if (type === 'up') { handleSwipeChange(popupData.activeIndex - 1) } else { handleSwipeChange(popupData.activeIndex + 1) } } /** 弹窗关闭 */ 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 () => (
{ clearTimeout(activeData.timer) activeData.model = !activeData.model Object.values(data.videoRefs).map((n: any) => n?.toggleHideControl(activeData.model)) }} >
{ e.stopPropagation() setModelOpen() }} >
{data.itemList.map((m: any, mIndex: number) => { const isRenderItem = Math.abs(popupData.activeIndex - mIndex) < 2 const isRender = Math.abs(popupData.playIndex - mIndex) < 2 const isEmtry = popupData.activeIndex != mIndex // 判断是否是当前选中的元素 const activeEle = popupData.playIndex === mIndex ? true : false return isRenderItem ? (
popupData.activeIndex ? effects[effectIndex.value].next : {} } onClick={(e: Event) => { e.stopPropagation() clearTimeout(activeData.timer) if (Date.now() - activeData.nowTime < 300) { handleDbClick(m) return } activeData.nowTime = Date.now() activeData.timer = setTimeout(() => { activeData.model = !activeData.model Object.values(data.videoRefs).map((n: any) => n?.toggleHideControl(activeData.model) ) if (activeData.model) { setModelOpen() } }, 300) }} > {isRender && m.type === 'VIDEO' && ( <> (data.videoRefs[mIndex] = v)} item={m} isActive={activeEle} isEmtry={isEmtry} onLoadedmetadata={(videoItem: any) => { m.videoEle = videoItem }} 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() } }} /> {!m.isprepare && (
)}
)} {isRender && m.type === 'IMG' && } {isRender && m.type === 'SONG' && ( { m.iframeRef = el }} /> )} {!isRender && (
)}
) : ( '' ) })}
{activeData.model && (
{ e.stopPropagation() clearTimeout(activeData.timer) }} >
(popupData.open = true)} > 知识点
{/*
(popupData.guideOpen = true)}> 投屏
*/} {data.isCourse && ( <>
gotoRollCall('student_roll_call')} > 点名
gotoRollCall('sign_out')}> 签退
)}
)}
{activeData.model && (
e.stopPropagation()}> {popupData.activeIndex != 0 && (
{ // useThrottleFn(() => { // handlePreAndNext('up') // }, 300) // onChangeSwiper('up') handlePreAndNext('up') }} > 上一个
)} {popupData.activeIndex != data.itemList.length - 1 && (
{ // console.log('click down') // useThrottleFn(() => { // console.log('click down pass') // handlePreAndNext('down') // }, 300) // onChangeSwiper('down') handlePreAndNext('down') }} > 下一个
)}
)}
goback()}> 返回
{data.isCourse && }
{ const _effectIndex = effectIndex.value + 1 effectIndex.value = _effectIndex >= effects.length - 1 ? 0 : _effectIndex setModelOpen() }} > {popupData.tabName}
{state.platformType == 'TEACHER' && (
{ e.stopPropagation() clearTimeout(activeData.timer) }} >
(popupData.guideOpen = true)}>
{ openStudyTool({ type: 'pen', icon: iconPen, name: '批注' }) }} >
{/*
(popupData.toolOpen = true)}>
*/}
)}
{/* 更多弹窗 */} { // onChangeSwiper('change', res.itemActive) popupData.open = false toggleMaterial(res.itemActive) }} /> {studyData.penShow && ( closeStudyTool()} /> )}
) } })