|
|
@@ -0,0 +1,463 @@
|
|
|
+<template>
|
|
|
+ <view class="kecheng-study-page">
|
|
|
+
|
|
|
+ <customNavbarVue title="课程" :show-back-btn="true" @back="goUpPage"></customNavbarVue>
|
|
|
+ <!-- 播放器 -->
|
|
|
+ <!-- <videoPlayVue v-if="showVideo" ref="videoRef" class="phone-video-box" @play-end="onPlayEnd" @play-play="onPlay"
|
|
|
+ @play-pause="onPause" @play-timeupdate="onTimeupdate"></videoPlayVue> -->
|
|
|
+ <view class="video-wrapper">
|
|
|
+ <video v-if="showVideo" :src="videoUrl" :poster="videoPoster" controls class="phone-video-box"
|
|
|
+ @play="onVideoPlay" @pause="onVideoPause" @ended="onVideoEnd" @error="onVideoError" id="myVideo"
|
|
|
+ object-fit="contain" custom-cache @fullscreenchange="onVideoFullscreenChange"></video>
|
|
|
+ <view v-else class="phone-video-box master-image" :style="{ backgroundImage: 'url(' + imgsArr.videoBj + ')' }">
|
|
|
+ <!-- <view class="video-player-icon" @click="handlePlayFirst"></view> -->
|
|
|
+ </view>
|
|
|
+ <zmm-watermark :watermark="watermark" class="watermark-component" ></zmm-watermark>
|
|
|
+ <view class="static-watermark">保密内容 · 请勿外传</view>
|
|
|
+ <!-- 视频背景图 -->
|
|
|
+
|
|
|
+ </view>
|
|
|
+ <!-- 播放引导 -->
|
|
|
+ <view class="yindao-shadow-image" v-if="!shaDowImgFlag"
|
|
|
+ :style="{ backgroundImage: 'url(' + imgsArr.pointIcon + ')' }"> </view>
|
|
|
+ <!-- 中间区域 -->
|
|
|
+ <view class="kc-name-box">
|
|
|
+ <view>{{name}}</view>
|
|
|
+ </view>
|
|
|
+ <view class="kc-info-box">
|
|
|
+ <view>时长:{{period}}</view>
|
|
|
+ <view>{{userCount}}人学习</view>
|
|
|
+ </view>
|
|
|
+ <!-- 大纲 -->
|
|
|
+ <view class="phone-tab-box">
|
|
|
+ <uni-segmented-control :current="current" :values="items" style-type="text" :active-color="activeColor"
|
|
|
+ @clickItem="onClickItem" />
|
|
|
+ </view>
|
|
|
+ <view class="kecheng-content-box">
|
|
|
+ <!-- 目录 -->
|
|
|
+ <kechengMuluVue v-if="current === 0" :chapterArr="list" @play="handlePlay" :isHasProgress="true"
|
|
|
+ :activeKjId="curPlayData&&curPlayData.kjId"></kechengMuluVue>
|
|
|
+ <!-- 介绍 -->
|
|
|
+ <rich-text :nodes="intro || '暂无内容'" v-if="current === 1 && intro" class="kecheng-jieshao-box"></rich-text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import cacheManager from '@/utils/cacheManager.js';
|
|
|
+ import * as kechengApi from "@/api/kecheng.js";
|
|
|
+ import customNavbarVue from "@/components/custom-navbar/custom-navbar.vue";
|
|
|
+ import videoPlayVue from "@/components/videoPlay/videoPlay.vue";
|
|
|
+ import kechengMuluVue from "@/components/kecheng-mulu/kecheng-mulu.vue";
|
|
|
+ import {
|
|
|
+ useUserCache
|
|
|
+ } from "@/utils/userCache.js"
|
|
|
+ import {
|
|
|
+ formatSecondsToCnhms
|
|
|
+ } from "@/utils/common.js"
|
|
|
+ import {
|
|
|
+ useKechengTools
|
|
|
+ } from "./useKechengCache.js"
|
|
|
+
|
|
|
+ const {
|
|
|
+ getCurKjIndex,
|
|
|
+ saveKechengData,
|
|
|
+ getKechengDataFromHistory,
|
|
|
+ saveKechengSectionPage,
|
|
|
+ getKechengSectionPageFromHistory,
|
|
|
+ mergeProgress,
|
|
|
+ initCourseProgressAll,
|
|
|
+ saveCourseProgress,
|
|
|
+ updateSectionProgress,
|
|
|
+ } = useKechengTools();
|
|
|
+
|
|
|
+ export default {
|
|
|
+ components: {
|
|
|
+ videoPlayVue,
|
|
|
+ kechengMuluVue,
|
|
|
+ customNavbarVue
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ showVideo: false,
|
|
|
+ shaDowImgFlag: false,
|
|
|
+ items: ['目录', '介绍'],
|
|
|
+ colors: ['#007aff', '#4cd964', '#dd524d'],
|
|
|
+ activeColor: '#3fd2a1',
|
|
|
+ current: 0, // 激活的选项卡
|
|
|
+ operId: '', // 课程
|
|
|
+ name: '',
|
|
|
+ period: 0, // 时长
|
|
|
+ userCount: 0, // 学习人数
|
|
|
+ list: [],
|
|
|
+ intro: '',
|
|
|
+ curPlayData: null,
|
|
|
+ timer1: null,
|
|
|
+ kcId: null,
|
|
|
+ from: '',
|
|
|
+ jzId: null,
|
|
|
+ watermark: '<h5>我是h5标签我是h5标签我是h5标签我是h5标签</h5><p style="color:#f00">我是p标签</p><br>/>',
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ videoUrl: '', // 视频地址
|
|
|
+ videoPoster: '', // 视频封面
|
|
|
+ videoInfo: {}, // 视频信息
|
|
|
+ loading: false, // 加载状态
|
|
|
+ error: false, // 错误状态
|
|
|
+ errorMessage: '', // 错误信息
|
|
|
+ videoContext: null, // 视频上下文
|
|
|
+ imgsArr: {}
|
|
|
+
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onLoad(options) {
|
|
|
+ this.kcId = options.kcId;
|
|
|
+ this.from = options.from;
|
|
|
+ this.jzId = options.jzId;
|
|
|
+ this.imgsArr.videoBj = cacheManager.get('projectImg').video_bj;
|
|
|
+ this.imgsArr.pointIcon = cacheManager.get('projectImg').course_point_icon;
|
|
|
+ this.init();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ onVideoFullscreenChange(e) {
|
|
|
+ console.log('全屏状态变化:', e.detail.fullScreen);
|
|
|
+ // 改用uniapp专属选择器,获取视频容器(避免document兼容问题)
|
|
|
+ const query = uni.createSelectorQuery().in(this);
|
|
|
+ query.select('.video-wrapper')
|
|
|
+ .fields({ node: true, dataset: true })
|
|
|
+ .exec((res) => {
|
|
|
+ if (res[0] && res[0].node) {
|
|
|
+ const videoWrapper = res[0].node;
|
|
|
+ if (e.detail.fullScreen) {
|
|
|
+ // 全屏:添加wx-video-fullscreen类(匹配样式)
|
|
|
+ videoWrapper.classList.add('wx-video-fullscreen');
|
|
|
+ } else {
|
|
|
+ // 退出全屏:移除类,恢复默认样式
|
|
|
+ videoWrapper.classList.remove('wx-video-fullscreen');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 播放视频
|
|
|
+ playVideo() {
|
|
|
+ if (this.videoUrl) {
|
|
|
+ this.videoContext.play()
|
|
|
+ } else {
|
|
|
+ uni.showToast({
|
|
|
+ title: '暂无视频可播放',
|
|
|
+ icon: 'none'
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 暂停视频
|
|
|
+ pauseVideo() {
|
|
|
+ this.videoContext.pause()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 重新播放
|
|
|
+ replayVideo() {
|
|
|
+ this.videoContext.seek(0)
|
|
|
+ this.videoContext.play()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 视频开始播放
|
|
|
+ onVideoPlay() {
|
|
|
+ console.log('视频开始播放')
|
|
|
+ },
|
|
|
+
|
|
|
+ // 视频暂停
|
|
|
+ onVideoPause() {
|
|
|
+ console.log('视频暂停')
|
|
|
+ },
|
|
|
+
|
|
|
+ // 视频播放结束
|
|
|
+ onVideoEnd() {
|
|
|
+ console.log('视频播放结束')
|
|
|
+ uni.showToast({
|
|
|
+ title: '播放完成',
|
|
|
+ icon: 'success'
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ // 视频播放错误
|
|
|
+ onVideoError(e) {
|
|
|
+ console.error('视频播放错误:', e.detail.errMsg)
|
|
|
+ this.error = true
|
|
|
+ this.errorMessage = '视频播放出错,请检查视频地址或格式'
|
|
|
+ },
|
|
|
+
|
|
|
+ // 重新加载视频
|
|
|
+ retryLoadVideo() {
|
|
|
+ this.getVideoData()
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ handlePlayFirst() {
|
|
|
+
|
|
|
+ console.log('this.list', this.list);
|
|
|
+ if (this.list && this.list[0].jieList && this.list[0].jieList[0].kejianList) {
|
|
|
+ // 设置默认展开项
|
|
|
+ this.list[0].open = true;
|
|
|
+ this.list[0].jieList[0].open = true;
|
|
|
+ // 设置播放可见
|
|
|
+ const kejian = this.list[0].jieList[0].kejianList[0];
|
|
|
+ this.handlePlay(kejian)
|
|
|
+
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onPause() {
|
|
|
+ clearInterval(this.timer1);
|
|
|
+ this.timer1 = null;
|
|
|
+ },
|
|
|
+ onPlay() {
|
|
|
+ clearInterval(this.timer1);
|
|
|
+ this.timer1 = null;
|
|
|
+ // this.timer = setInterval(() => {
|
|
|
+ // updateSectionProgress(this.operId);
|
|
|
+ // }, 1000 * 1 * 60) // 自动保存进度 1分钟已保存
|
|
|
+
|
|
|
+ },
|
|
|
+ sectionPlayerProgress(progress) {
|
|
|
+ let sectionPage = getKechengSectionPageFromHistory(this.operId);
|
|
|
+ let maxProcess = this.list[sectionPage.iChapter].jieList[sectionPage.iSection].kejianList.find(it1 => it1
|
|
|
+ .kjId == sectionPage.kjId).maxProcess;
|
|
|
+ // 更新缓存进度
|
|
|
+ this.list[sectionPage.iChapter].jieList[sectionPage.iSection].kejianList.find(it1 => it1.kjId ==
|
|
|
+ sectionPage.kjId).curProcess = progress;
|
|
|
+ if (progress < 100) {
|
|
|
+ // 播放进度小于100
|
|
|
+ if (progress < maxProcess) {
|
|
|
+ this.list[sectionPage.iChapter].jieList[sectionPage.iSection].kejianList.find(it1 => it1.kjId ==
|
|
|
+ sectionPage.kjId).maxProcess = maxProcess
|
|
|
+ } else {
|
|
|
+ this.list[sectionPage.iChapter].jieList[sectionPage.iSection].kejianList.find(it1 => it1.kjId ==
|
|
|
+ sectionPage.kjId).maxProcess = progress
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 播放进度大于100
|
|
|
+ this.list[sectionPage.iChapter].jieList[sectionPage.iSection].kejianList.find(it1 => it1.kjId ==
|
|
|
+ sectionPage.kjId).maxProcess = 100
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onTimeupdate(time) {
|
|
|
+ const progress = parseInt(time / this.curPlayData.duration * 100);
|
|
|
+ this.curPlayData.curProgress = parseInt(progress >= 100 ? '99' : progress);
|
|
|
+ // 保存进度
|
|
|
+ // saveCourseProgress(time, this.curPlayData.duration, this.operId)
|
|
|
+ // 更新进度
|
|
|
+ // this.sectionPlayerProgress(progress)
|
|
|
+ },
|
|
|
+ onPlayEnd() {
|
|
|
+ clearInterval(this.timer1);
|
|
|
+ this.timer1 = null;
|
|
|
+ // saveCourseProgress(this.curPlayData.duration, this.curPlayData.duration, this.operId, 'end');
|
|
|
+ // updateSectionProgress(this.operId, 'end', 'video', () => {
|
|
|
+ // this.curPlayData.maxProcess = 99;
|
|
|
+ // this.curPlayData.curProcess = 99;
|
|
|
+ // });
|
|
|
+ console.log('end')
|
|
|
+ },
|
|
|
+ goUpPage() {
|
|
|
+ const pages = getCurrentPages();
|
|
|
+ if (pages.length > 1) {
|
|
|
+ uni.navigateBack()
|
|
|
+ } else {
|
|
|
+ /* if (this.from == 'kechengList') {
|
|
|
+ uni.redirectTo({
|
|
|
+ url: '/pages/client/Kecheng/list'
|
|
|
+ })
|
|
|
+ } else if (this.from == 'shouye') {
|
|
|
+ uni.redirectTo({
|
|
|
+ url: '/pages/client/ShouYe/shouye'
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ uni.redirectTo({
|
|
|
+ url: '/pages/client/ShouYe/shouye'
|
|
|
+ })
|
|
|
+ } */
|
|
|
+ history.back();
|
|
|
+ }
|
|
|
+
|
|
|
+ },
|
|
|
+ onClickItem(e) {
|
|
|
+ if (this.current !== e.currentIndex) {
|
|
|
+ this.current = e.currentIndex
|
|
|
+ }
|
|
|
+ // if (this.current == 0) {
|
|
|
+ // if (!this.showVideo) {
|
|
|
+ // this.shaDowImgFlag = false
|
|
|
+ // } else {
|
|
|
+ // this.shaDowImgFlag = true
|
|
|
+ // }
|
|
|
+ // } else {
|
|
|
+ // this.shaDowImgFlag = true
|
|
|
+ // }
|
|
|
+ this.shaDowImgFlag = this.current !== 0 || this.showVideo
|
|
|
+ },
|
|
|
+ formatData(data) {
|
|
|
+ data.forEach(zhang => {
|
|
|
+ zhang.open = false;
|
|
|
+ zhang.jieList.forEach(jie => {
|
|
|
+ jie.open = false;
|
|
|
+ })
|
|
|
+ })
|
|
|
+ return data;
|
|
|
+ },
|
|
|
+ handlePlay(data) {
|
|
|
+ this.showVideo = true;
|
|
|
+ this.shaDowImgFlag = true;
|
|
|
+ if (this.curPlayData && this.curPlayData.url == data.url) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ console.log('data', data);
|
|
|
+ this.curPlayData = data;
|
|
|
+
|
|
|
+ //this.videoUrl = 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/2minute-demo.mp4'
|
|
|
+
|
|
|
+ kechengApi.getVideoId({
|
|
|
+ videoId: data.url
|
|
|
+ }).then(res => {
|
|
|
+ this.videoUrl = res.data
|
|
|
+ console.log('res', res);
|
|
|
+ // const progress = data.curProcess || 0;
|
|
|
+ // this.$refs.videoRef.init({
|
|
|
+ // videoId: data.url,
|
|
|
+ // playAuth: res.data,
|
|
|
+ // seekTime: data.duration * progress / 100,
|
|
|
+ // isPlay: false
|
|
|
+ // })
|
|
|
+
|
|
|
+ })
|
|
|
+ },
|
|
|
+ initFirstVideo() {
|
|
|
+
|
|
|
+ if (this.list && this.list[0].jieList && this.list[0].jieList[0].kejianList) {
|
|
|
+ // 设置默认展开项
|
|
|
+ this.list[0].open = true;
|
|
|
+ this.list[0].jieList[0].open = true;
|
|
|
+ // 设置播放可见
|
|
|
+ const kejian = this.list[0].jieList[0].kejianList[0];
|
|
|
+ // this.handlePlay(kejian)
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ //saveKechengSectionPage(this.operId, sectionPage)
|
|
|
+ },
|
|
|
+
|
|
|
+ init() {
|
|
|
+ kechengApi.getClientKechengStart({
|
|
|
+ kcId: this.kcId,
|
|
|
+ jzId: this.jzId
|
|
|
+ }).then(res => {
|
|
|
+ const {
|
|
|
+ userCount,
|
|
|
+ period,
|
|
|
+ name,
|
|
|
+ kejianUserVo,
|
|
|
+ intro,
|
|
|
+ operId,
|
|
|
+ } = res.data;
|
|
|
+ this.userCount = userCount;
|
|
|
+ this.period = formatSecondsToCnhms(period, true);
|
|
|
+ this.name = name;
|
|
|
+ this.formatData(kejianUserVo.zhangList)
|
|
|
+ this.list = kejianUserVo.zhangList;
|
|
|
+ this.intro = intro;
|
|
|
+ this.operId = operId;
|
|
|
+
|
|
|
+ // 获取课程缓存 && 课件缓存(课件缓存点击后产生)
|
|
|
+ //let historyArrKecheng = getKechengDataFromHistory(this.operId)
|
|
|
+ // let sectionPageHistory = getKechengSectionPageFromHistory(this.operId)
|
|
|
+ // // 判断是否有前台缓存
|
|
|
+ // if (historyArrKecheng && sectionPageHistory) {
|
|
|
+ // // 有缓存---- 把start接口中,返回数据进度100%,更新到前台缓存
|
|
|
+ // const arrKecheng = mergeProgress(kejianUserVo && kejianUserVo.zhangList,
|
|
|
+ // historyArrKecheng);
|
|
|
+ // // 后台数据 同步前台缓存
|
|
|
+ // saveKechengData(this.operId, arrKecheng)
|
|
|
+ // } else {
|
|
|
+ // // 无缓存----把start接口中,返回的所有数据,更新到前台缓存
|
|
|
+ // saveKechengData(this.operId, kejianUserVo && kejianUserVo.zhangList)
|
|
|
+ // }
|
|
|
+ // 初始化页面 常规数据
|
|
|
+ //initCourseProgressAll(this.operId)
|
|
|
+
|
|
|
+ console.log('初始化播放首1123次')
|
|
|
+ // 设置播放视频
|
|
|
+ this.initFirstVideo();
|
|
|
+ }).catch(err => {
|
|
|
+ this.goUpPage();
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+</script>
|
|
|
+
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.video-wrapper {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 400rpx;
|
|
|
+ background-color: #000;
|
|
|
+ overflow: hidden; /* 防止水印溢出 */
|
|
|
+
|
|
|
+ .phone-video-box {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ z-index: 1; /* 明确视频层级,低于水印 */
|
|
|
+ }
|
|
|
+
|
|
|
+ // 1. 确保水印组件样式完全生效(铺满、置顶、不遮挡操作)
|
|
|
+ .watermark-component {
|
|
|
+ position: absolute !important; /* 强制绝对定位,避免组件自身样式覆盖 */
|
|
|
+ top: 0 !important;
|
|
|
+ left: 0 !important;
|
|
|
+ width: 100% !important;
|
|
|
+ height: 100% !important;
|
|
|
+ z-index: 999 !important; /* 高于视频z-index:1 */
|
|
|
+ pointer-events: none !important;
|
|
|
+ background: transparent !important; /* 避免组件背景遮挡视频 */
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 优化静态水印:提高对比度,确保可见
|
|
|
+ .static-watermark {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%) rotate(-30deg);
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.3) !important; /* 提高不透明度,增强可见性 */
|
|
|
+ font-weight: bold;
|
|
|
+ white-space: nowrap;
|
|
|
+ pointer-events: none;
|
|
|
+ z-index: 999 !important;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 全屏样式优化:确保水印铺满屏幕
|
|
|
+.video-wrapper.wx-video-fullscreen {
|
|
|
+ position: fixed !important;
|
|
|
+ top: 0 !important;
|
|
|
+ left: 0 !important;
|
|
|
+ width: 100vw !important;
|
|
|
+ height: 100vh !important;
|
|
|
+ z-index: 9999 !important;
|
|
|
+ background-color: #000 !important;
|
|
|
+
|
|
|
+ .static-watermark {
|
|
|
+ font-size: 80rpx !important;
|
|
|
+ color: rgba(255, 255, 255, 0.4) !important; /* 全屏时进一步提高可见性 */
|
|
|
+ }
|
|
|
+
|
|
|
+ .watermark-component {
|
|
|
+ width: 100vw !important;
|
|
|
+ height: 100vh !important;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|