zaijin 2 лет назад
Родитель
Сommit
b8209326aa

+ 272 - 0
h5_web/components/video-box/video-box.vue

@@ -0,0 +1,272 @@
+<template>
+  <view class="video-box" v-loading="loading">
+    <video
+      v-if="videoUrl"
+      class="video-box-body"
+      id="myVideo"
+      @timeupdate="timeUpdate"
+      :src="videoUrl"
+      controls
+      :initial-time="initial_time"
+      object-fit="fill"
+      play-btn-position="center"
+      @ended="ended"
+      @tap="videoClick"
+      @loadedmetadata="loadedmetadata"
+    >
+      <cover-view class="video-control">
+        <cover-view class="multi rate" @tap.stop="showSwitchRate">x {{ currentRate }}</cover-view>
+      </cover-view>
+      <cover-view class="multi-list rate" :class="{ active: rateShow }">
+        <cover-view
+          v-for="(item, index) in rateList"
+          :key="index"
+          class="multi-item rate"
+          :data-rate="item"
+          @tap="switchRate"
+          :class="{ active: item == currentRate }"
+        >
+          {{ item }}
+        </cover-view>
+      </cover-view>
+    </video>
+    <view class="video-box-body" v-else>
+      <u-empty text="视频地址为空" mode="page"></u-empty>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'VideoBox',
+  props: {
+    rateList: {
+      type: Array,
+      default: () => ['0.5', '1.0', '1.5', '2.0']
+    },
+    videoUrl: {
+      type: String,
+      default: ''
+    }
+  },
+  data() {
+    return {
+      // 加载层
+      loading: false,
+      // 视频创建dom
+      videoContext: uni.createVideoContext('myVideo', this),
+      // 视频实时时间
+      initial_time: 0,
+      // 视频已经播放时间
+      playedTime: 0,
+      // 倍速浮层
+      rateShow: false,
+      // 默认倍速
+      currentRate: 1.0,
+      // 视频实际时间
+      video_real_time: 0,
+      // 视频总时长
+      duration: 0,
+      // 当前视频选中播放
+      currentDuration: 0,
+      // 视频信息
+      videoInfo: {},
+      // 选中课程下标
+      videoIndex: 0
+    };
+  },
+  methods: {
+    /**
+     * 加载视频
+     * @date 2022-10-19
+     * @returns {any}
+     */
+    loadVideo(data) {
+      this.loading = true;
+      this.videoInfo = data;
+      this.initial_time = Number(data.currentDuration);
+      this.video_real_time = Number(data.playDuration);
+      this.playedTime = Number(data.playDuration);
+    },
+    /**
+     * 记录时间
+     * @date 2022-10-19
+     * @param { Number } index
+     * @returns {any}
+     */
+    recordDuration(index, refresh) {
+      this.videoIndex = index;
+      let playDuration = this.video_real_time;
+      if (this.videoInfo.playDuration > this.video_real_time) {
+        this.currentDuration = this.video_real_time;
+        playDuration = this.videoInfo.playDuration;
+      } else {
+        this.currentDuration = this.video_real_time;
+      }
+      this.$emit('recordDuration', { playDuration, duration: this.duration, currentDuration: this.currentDuration }, index, refresh);
+    },
+    /**
+     * 获取视频总时长
+     * @param {Object} data
+     */
+    loadedmetadata(data) {
+      this.duration = data.detail.duration;
+      this.loading = false;
+    },
+    /**
+     * 显示倍速浮层
+     * @date 2022-10-19
+     * @returns {any}
+     */
+    showSwitchRate() {
+      this.rateShow = true;
+    },
+    /**
+     * 切换倍速
+     * @param {Object} e
+     */
+    switchRate(e) {
+      let rate = Number(e.currentTarget.dataset.rate);
+      this.currentRate = rate;
+      this.rateShow = false;
+      this.videoContext.playbackRate(rate);
+    },
+    /**
+     * 视频点击
+     * @param {Object} e
+     */
+    videoClick(e) {
+      this.rateShow = false;
+    },
+    /**
+     * 视频停止
+     * @date 2022-10-19
+     * @param {any} e
+     * @returns {any}
+     */
+    pause(e) {
+      console.log(e);
+    },
+    /**
+     * 控制视频不能快进
+     * @param {Object} e
+     */
+    timeUpdate(e) {
+      //播放的总时长
+      let duration = e.detail.duration;
+      //实时播放进度 秒数
+      let jumpTime = parseInt(e.detail.currentTime);
+      //当前视频进度
+      if (jumpTime - this.playedTime > 3) {
+        // 差别过大,调用seek方法跳转到实际观看时间
+        this.videoContext.seek(this.playedTime);
+        wx.showToast({
+          title: '未完整看完该视频,不能快进',
+          icon: 'none',
+          duration: 2000
+        });
+      } else {
+        this.video_real_time = parseInt(e.detail.currentTime);
+        this.currentDuration = e.detail.currentTime
+        if (this.video_real_time > this.playedTime) {
+          this.playedTime = this.video_real_time;
+        }
+      }
+    },
+    /**
+     * 视频结束
+     */
+    ended() {
+      // 用户把进度条拉到最后,但是实际观看时间不够,跳转回去会自动暂停。
+      // 这里加个判断。
+      if (this.playedTime < this.duration) {
+        // this.videoContext.pause();
+        this.$emit(
+          'recordDuration',
+          { playDuration: this.playedTime, duration: this.duration, currentDuration: this.currentDuration },
+          this.videoIndex,
+          false
+        );
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.video-box {
+  &-body {
+    width: 100%;
+    height: 400rpx;
+    .video-control {
+      background-color: rgba(0, 0, 0, 0.1);
+      height: 90rpx;
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      z-index: 997;
+    }
+    .video-wrap {
+      position: relative;
+    }
+    .multi-list.full-screen.vertical {
+      height: 100vh !important;
+      padding: 8vh 0;
+    }
+    .multi-list.full-screen.horizontal {
+      height: 100vw !important;
+      padding: 8vw 0;
+    }
+    .multi {
+      position: absolute;
+      right: 30rpx;
+      top: 50%;
+      transform: translateY(-50%);
+      z-index: 998;
+      width: 100rpx;
+      color: #fff;
+      padding: 10rpx 0;
+      text-align: center;
+      transition: color ease 0.3s;
+    }
+    .multi.rate {
+      right: 30rpx;
+    }
+    .multi-list {
+      position: absolute;
+      height: 100%;
+      width: 0;
+      background-color: rgba(0, 0, 0, 0.65);
+      top: 0;
+      right: 0;
+      transition: width 0.3s ease;
+      z-index: 999;
+      box-sizing: border-box;
+      padding: 20rpx 0;
+    }
+    .multi-list.rate {
+      padding: 25rpx 0;
+    }
+    .multi-list.active {
+      width: 200rpx;
+    }
+    .multi-item {
+      text-align: center;
+      color: #fff;
+      line-height: 80rpx;
+      transition: color ease 0.3s;
+    }
+    .multi-item.rate {
+      line-height: 70rpx;
+    }
+    .multi-item:hover,
+    .multi:hover {
+      color: #00d8ff;
+    }
+    .multi-item.active {
+      color: #00d8ff;
+    }
+  }
+}
+</style>

+ 445 - 440
h5_web/components/wyb-pagination/wyb-pagination.vue

@@ -1,455 +1,460 @@
 <template>
-	<view class="wyb-pagination-box" :style="{
-		paddingLeft: padding + 'rpx',
-		paddingRight: padding + 'rpx',
-		'--hover': autoHover}">
-		<view class="wyb-pagination-left" :style="{opacity: currentPage === 1 ? 0.5 : 1}">
-			<view
-			 v-if="showFirst"
-			 :class="'wyb-pagination-first-page-' + (showIcon ? 'i' : 't')"
-			 :style="btnStyleStr" 
-			 :hover-class="currentPage === 1 ? '' : 'wyb-pagination-hover'"
-			 @tap="onPageBtnTap('first-page')">
-				<view v-if="showIcon" class="iconfont icon-shuangjiantou left-arrow" />
-				<text v-else>{{firstText}}</text>
-			</view>
-			<view 
-			 :class="'wyb-pagination-prev-page-' + (showIcon ? 'i' : 't')"
-			 :style="btnStyleStr"
-			 :hover-class="currentPage === 1 ? '' : 'wyb-pagination-hover'"
-			 @tap="onPageBtnTap('prev-page')">
-				<view v-if="showIcon" class="iconfont icon-danjiantou left-arrow" />
-				<text v-else>{{prevText}}</text>
-			</view>
-		</view>
-		<view class="wyb-pagination-info" @tap.stop="onInfoTap">
-			<view class="wyb-pagination-num" v-if="!infoClick">
-				<text :style="{color: currentColor}">{{currentPage}}</text>
-				<text class="wyb-pagination-span" :style="{color: pageInfoColor}">/</text>
-				<text :style="{color: pageInfoColor}">{{totalPage}}</text>
-				<text 
-				 v-if="showTotalItem"
-				 class="wyb-pagination-info-total"
-				 :style="{color: RGBChange(pageInfoColor, 0.5, 'light')}">
-					({{totalItems}})
-				</text>
-			</view>
-			<!-- #ifndef MP-WEIXIN || APP-VUE || APP-NVUE || APP-PLUS || APP-PLUS-NVUE -->
-			<view class="wyb-pagination-input" v-else>
-				<input 
-				 type="number" 
-				 v-model="inputPage" 
-				 :onpaste="false"
-				 :focus="infoFocus" 
-				 :value="currentPage"
-				 :style="{color: currentColor}"
-				 :cursor-spacing="cursorSpacing"
-				 @confirm="onInfoConfirm" 
-				 @blur="onInfoBlur" />
-			</view>
-			<!-- #endif -->
-			<!-- #ifdef MP-WEIXIN || APP-VUE || APP-NVUE || APP-PLUS || APP-PLUS-NVUE -->
-			<view class="wyb-pagination-input" v-else>
-				<input 
-				 type="number" 
-				 v-model="inputPage" 
-				 :focus="infoFocus" 
-				 :name="currentPage"
-				 :style="{color: currentColor}"
-				 :cursor-spacing="cursorSpacing"
-				 @confirm="onInfoConfirm" 
-				 @blur="onInfoBlur" />
-			</view>
-			<!-- #endif -->
-		</view>
-		<view class="wyb-pagination-right" :style="{opacity: currentPage === totalPage ? 0.5 : 1}">
-			<view
-			 :class="'wyb-pagination-next-page-' + (showIcon ? 'i' : 't')"
-			 :style="btnStyleStr"
-			 :hover-class="currentPage === totalPage ? '' : 'wyb-pagination-hover'"
-			 @tap="onPageBtnTap('next-page')">
-				<view v-if="showIcon" class="iconfont icon-danjiantou right-arrow" />
-				<text v-else>{{nextText}}</text>
-			</view>
-			<view
-			 v-if="showLast"
-			 :class="'wyb-pagination-last-page-' + (showIcon ? 'i' : 't')"
-			 :style="btnStyleStr"
-			 :hover-class="currentPage === totalPage ? '' : 'wyb-pagination-hover'"
-			 @tap="onPageBtnTap('last-page')">
-				<view v-if="showIcon" class="iconfont icon-shuangjiantou right-arrow" />
-				<text v-else>{{lastText}}</text>
-			</view>
-		</view>
-	</view>
+  <view
+    class="wyb-pagination-box"
+    :style="{
+      paddingLeft: padding + 'rpx',
+      paddingRight: padding + 'rpx',
+      '--hover': autoHover
+    }"
+  >
+    <view class="wyb-pagination-left" :style="{ opacity: currentPage === 1 ? 0.5 : 1 }">
+      <view
+        v-if="showFirst"
+        :class="'wyb-pagination-first-page-' + (showIcon ? 'i' : 't')"
+        :style="btnStyleStr"
+        :hover-class="currentPage === 1 ? '' : 'wyb-pagination-hover'"
+        @tap="onPageBtnTap('first-page')"
+      >
+        <view v-if="showIcon" class="iconfont icon-shuangjiantou left-arrow" />
+        <text v-else>{{ firstText }}</text>
+      </view>
+      <view
+        :class="'wyb-pagination-prev-page-' + (showIcon ? 'i' : 't')"
+        :style="btnStyleStr"
+        :hover-class="currentPage === 1 ? '' : 'wyb-pagination-hover'"
+        @tap="onPageBtnTap('prev-page')"
+      >
+        <view v-if="showIcon" class="iconfont icon-danjiantou left-arrow" />
+        <text v-else>{{ prevText }}</text>
+      </view>
+    </view>
+    <view class="wyb-pagination-info" @tap.stop="onInfoTap">
+      <view class="wyb-pagination-num" v-if="!infoClick">
+        <text :style="{ color: currentColor }">{{ currentPage }}</text>
+        <text class="wyb-pagination-span" :style="{ color: pageInfoColor }">/</text>
+        <text :style="{ color: pageInfoColor }">{{ totalPage }}</text>
+        <text v-if="showTotalItem" class="wyb-pagination-info-total" :style="{ color: RGBChange(pageInfoColor, 0.5, 'light') }">
+          ({{ totalItems }})
+        </text>
+      </view>
+      <!-- #ifndef MP-WEIXIN || APP-VUE || APP-NVUE || APP-PLUS || APP-PLUS-NVUE -->
+      <view class="wyb-pagination-input" v-else>
+        <input
+          type="number"
+          v-model="inputPage"
+          :onpaste="false"
+          :focus="infoFocus"
+          :value="currentPage"
+          :style="{ color: currentColor }"
+          :cursor-spacing="cursorSpacing"
+          @confirm="onInfoConfirm"
+          @blur="onInfoBlur"
+        />
+      </view>
+      <!-- #endif -->
+      <!-- #ifdef MP-WEIXIN || APP-VUE || APP-NVUE || APP-PLUS || APP-PLUS-NVUE -->
+      <view class="wyb-pagination-input" v-else>
+        <input
+          type="number"
+          v-model="inputPage"
+          :focus="infoFocus"
+          :name="currentPage"
+          :style="{ color: currentColor }"
+          :cursor-spacing="cursorSpacing"
+          @confirm="onInfoConfirm"
+          @blur="onInfoBlur"
+        />
+      </view>
+      <!-- #endif -->
+    </view>
+    <view class="wyb-pagination-right" :style="{ opacity: currentPage === totalPage ? 0.5 : 1 }">
+      <view
+        :class="'wyb-pagination-next-page-' + (showIcon ? 'i' : 't')"
+        :style="btnStyleStr"
+        :hover-class="currentPage === totalPage ? '' : 'wyb-pagination-hover'"
+        @tap="onPageBtnTap('next-page')"
+      >
+        <view v-if="showIcon" class="iconfont icon-danjiantou right-arrow" />
+        <text v-else>{{ nextText }}</text>
+      </view>
+      <view
+        v-if="showLast"
+        :class="'wyb-pagination-last-page-' + (showIcon ? 'i' : 't')"
+        :style="btnStyleStr"
+        :hover-class="currentPage === totalPage ? '' : 'wyb-pagination-hover'"
+        @tap="onPageBtnTap('last-page')"
+      >
+        <view v-if="showIcon" class="iconfont icon-shuangjiantou right-arrow" />
+        <text v-else>{{ lastText }}</text>
+      </view>
+    </view>
+  </view>
 </template>
 
 <script>
-	export default {
-		data() {
-			return {
-				currentPage: this.current || 1,
-				inputPage: '',
-				infoClick: false,
-				infoFocus: false
-			}
-		},
-		computed: {
-			totalPage() {
-				return Math.ceil(parseFloat(this.totalItems) / parseFloat(this.pageItems))
-			},
-			autoHover() {
-				if (this.btnStyle.backgroundColor) {
-					return this.RGBChange(this.btnStyle.backgroundColor, 0.1, 'dark')
-				} else {
-					return this.RGBChange('#f8f8f8', 0.05, 'dark')
-				}
-			},
-			btnStyleStr() {
-				let styleStr = ''
-				for (let key in this.btnStyle) {
-					styleStr += `${this.sortFieldMatch(key)}: ${this.btnStyle[key]}; `
-				}
-				return styleStr
-			}
-		},
-		watch: {
-			current(val) {
-				const oPage = this.currentPage
-				if (!Object.is(oPage, val)) {
-					this.currentPage = val
-					this.$emit('change', {
-						type: 'prop-page', 
-						current: this.currentPage,
-					})
-				}
-			}
-		},
-		props: {
-			totalItems: {
-				type: [String, Number],
-				default: 20
-			},
-			pageItems: {
-				type: [String, Number],
-				default: 5
-			},
-			current: {
-				type: Number,
-				default: 1
-			},
-			prevText: {
-				type: String,
-				default: '上一页'
-			},
-			nextText: {
-				type: String,
-				default: '下一页'
-			},
-			firstText: {
-				type: String,
-				default: '首页'
-			},
-			lastText: {
-				type: String,
-				default: '尾页'
-			},
-			pageInfoColor: {
-				type: String,
-				default: '#494949'
-			},
-			currentColor: {
-				type: String,
-				default: '#007aff'
-			},
-			padding: {
-				type: [String, Number],
-				default: 15
-			},
-			btnStyle: {
-				type: Object,
-				default () {
-					return {}
-				}
-			},
-			showIcon: {
-				type: Boolean,
-				default: false
-			},
-			showTotalItem: {
-				type: Boolean,
-				default: false
-			},
-			showFirst: {
-				type: Boolean,
-				default: true
-			},
-			showLast: {
-				type: Boolean,
-				default: true
-			},
-			couldInput: {
-				type: Boolean,
-				default: true
-			},
-			cursorSpacing: {
-				type: Number,
-				default: 0
-			}
-		},
-		methods: {
-			onPageBtnTap(type) {
-				switch (type) {
-					case 'first-page':
-						if (!Object.is(this.currentPage, 1)) {
-							this.currentPage = 1
-							this.$emit('change', {type, current: this.currentPage})
-						}
-						break
-					case 'prev-page':
-						if (!Object.is(this.currentPage, 1)) {
-							this.currentPage--
-							this.$emit('change', {type, current: this.currentPage})
-						}
-						break
-					case 'next-page':
-						if (!Object.is(this.currentPage, this.totalPage)) {
-							this.currentPage++
-							this.$emit('change', {type, current: this.currentPage})
-						}
-						break
-					case 'last-page':
-						if (!Object.is(this.currentPage, this.totalPage)) {
-							this.currentPage = this.totalPage
-							this.$emit('change', {type, current: this.currentPage})
-						}
-						break
-				}
-			},
-			onInfoTap() {
-				if (this.couldInput) {
-					this.infoClick = true
-					this.inputPage = this.currentPage
-					setTimeout(() => {
-						this.infoFocus = true
-					}, 10)
-				}
-			},
-			onInfoConfirm(e) {
-				let input = e.detail.value
-				const oPage = this.currentPage
-				if (parseFloat(input) > this.totalPage) {
-					this.currentPage = this.totalPage
-				} else if (parseFloat(input) < 1) {
-					this.currentPage = 1
-				} else if (input === '') {
-					this.currentPage = oPage
-				} else {
-					this.currentPage = parseFloat(input)
-				}
-				if (!Object.is(oPage, this.currentPage)) {
-					this.$emit('change', {
-						type: 'input-page', 
-						current: this.currentPage,
-					})
-				}
-				this.infoClick = false
-				this.$nextTick(() => {
-					this.infoFocus = false
-				})
-			},
-			onInfoBlur(e) {
-				let input = e.detail.value
-				const oPage = this.currentPage
-				if (parseFloat(input) > this.totalPage) {
-					this.currentPage = this.totalPage
-				} else if (parseFloat(input) < 1) {
-					this.currentPage = 1
-				} else if (input === '') {
-					this.currentPage = oPage
-				} else {
-					this.currentPage = parseFloat(input)
-				}
-				if (!Object.is(oPage, this.currentPage)) {
-					this.$emit('change', {
-						type: 'input-page', 
-						current: this.currentPage,
-					})
-				}
-				this.infoClick = false
-				this.$nextTick(() => {
-					this.infoFocus = false
-				})
-			},
-			RGBChange(color, level, type) {
-				// 判断颜色类型
-				let r = 0,
-					g = 0,
-					b = 0,
-					hasAlpha = false,
-					alpha = 1
-				if (color.indexOf('#') !== -1) {
-					// hex转rgb
-					if (color.length === 4) {
-						let arr = color.split('')
-						color = '#' + arr[1] + arr[1] + arr[2] + arr[2] + arr[3] + arr[3]
-					}
-					let color16List = [color.substring(1, 3), color.substring(3, 5), color.substring(5, 7)]
-					r = parseInt(color16List[0], 16)
-					g = parseInt(color16List[1], 16)
-					b = parseInt(color16List[2], 16)
+export default {
+  data() {
+    return {
+      currentPage: this.current || 1,
+      inputPage: '',
+      infoClick: false,
+      infoFocus: false
+    };
+  },
+  computed: {
+    totalPage() {
+      return Math.ceil(parseFloat(this.totalItems) / parseFloat(this.pageItems));
+    },
+    autoHover() {
+      if (this.btnStyle.backgroundColor) {
+        return this.RGBChange(this.btnStyle.backgroundColor, 0.1, 'dark');
+      } else {
+        return this.RGBChange('#f8f8f8', 0.05, 'dark');
+      }
+    },
+    btnStyleStr() {
+      let styleStr = '';
+      for (let key in this.btnStyle) {
+        styleStr += `${this.sortFieldMatch(key)}: ${this.btnStyle[key]}; `;
+      }
+      return styleStr;
+    }
+  },
+  watch: {
+    current(val) {
+      const oPage = this.currentPage;
+      if (!Object.is(oPage, val)) {
+        this.currentPage = val;
+        this.$emit('change', {
+          type: 'prop-page',
+          current: this.currentPage
+        });
+      }
+    }
+  },
+  props: {
+    totalItems: {
+      type: [String, Number],
+      default: 20
+    },
+    pageItems: {
+      type: [String, Number],
+      default: 5
+    },
+    current: {
+      type: Number,
+      default: 1
+    },
+    prevText: {
+      type: String,
+      default: '上一页'
+    },
+    nextText: {
+      type: String,
+      default: '下一页'
+    },
+    firstText: {
+      type: String,
+      default: '首页'
+    },
+    lastText: {
+      type: String,
+      default: '尾页'
+    },
+    pageInfoColor: {
+      type: String,
+      default: '#494949'
+    },
+    currentColor: {
+      type: String,
+      default: '#007aff'
+    },
+    padding: {
+      type: [String, Number],
+      default: 15
+    },
+    btnStyle: {
+      type: Object,
+      default() {
+        return {};
+      }
+    },
+    showIcon: {
+      type: Boolean,
+      default: false
+    },
+    showTotalItem: {
+      type: Boolean,
+      default: false
+    },
+    showFirst: {
+      type: Boolean,
+      default: true
+    },
+    showLast: {
+      type: Boolean,
+      default: true
+    },
+    couldInput: {
+      type: Boolean,
+      default: true
+    },
+    cursorSpacing: {
+      type: Number,
+      default: 0
+    }
+  },
+  methods: {
+    onPageBtnTap(type) {
+      switch (type) {
+        case 'first-page':
+          if (!Object.is(this.currentPage, 1)) {
+            this.currentPage = 1;
+            this.$emit('change', { type, current: this.currentPage });
+          }
+          break;
+        case 'prev-page':
+          if (!Object.is(this.currentPage, 1)) {
+            this.currentPage--;
+            this.$emit('change', { type, current: this.currentPage });
+          }
+          break;
+        case 'next-page':
+          if (!Object.is(this.currentPage, this.totalPage)) {
+            this.currentPage++;
+            this.$emit('change', { type, current: this.currentPage });
+          }
+          break;
+        case 'last-page':
+          if (!Object.is(this.currentPage, this.totalPage)) {
+            this.currentPage = this.totalPage;
+            this.$emit('change', { type, current: this.currentPage });
+          }
+          break;
+      }
+    },
+    onInfoTap() {
+      if (this.couldInput) {
+        this.infoClick = true;
+        this.inputPage = this.currentPage;
+        setTimeout(() => {
+          this.infoFocus = true;
+        }, 10);
+      }
+    },
+    onInfoConfirm(e) {
+      let input = e.detail.value;
+      const oPage = this.currentPage;
+      if (parseFloat(input) > this.totalPage) {
+        this.currentPage = this.totalPage;
+      } else if (parseFloat(input) < 1) {
+        this.currentPage = 1;
+      } else if (input === '') {
+        this.currentPage = oPage;
+      } else {
+        this.currentPage = parseFloat(input);
+      }
+      if (!Object.is(oPage, this.currentPage)) {
+        this.$emit('change', {
+          type: 'input-page',
+          current: this.currentPage
+        });
+      }
+      this.infoClick = false;
+      this.$nextTick(() => {
+        this.infoFocus = false;
+      });
+    },
+    onInfoBlur(e) {
+      let input = e.detail.value;
+      const oPage = this.currentPage;
+      if (parseFloat(input) > this.totalPage) {
+        this.currentPage = this.totalPage;
+      } else if (parseFloat(input) < 1) {
+        this.currentPage = 1;
+      } else if (input === '') {
+        this.currentPage = oPage;
+      } else {
+        this.currentPage = parseFloat(input);
+      }
+      if (!Object.is(oPage, this.currentPage)) {
+        this.$emit('change', {
+          type: 'input-page',
+          current: this.currentPage
+        });
+      }
+      this.infoClick = false;
+      this.$nextTick(() => {
+        this.infoFocus = false;
+      });
+    },
+    RGBChange(color, level, type) {
+      // 判断颜色类型
+      let r = 0,
+        g = 0,
+        b = 0,
+        hasAlpha = false,
+        alpha = 1;
+      if (color.indexOf('#') !== -1) {
+        // hex转rgb
+        if (color.length === 4) {
+          let arr = color.split('');
+          color = '#' + arr[1] + arr[1] + arr[2] + arr[2] + arr[3] + arr[3];
+        }
+        let color16List = [color.substring(1, 3), color.substring(3, 5), color.substring(5, 7)];
+        r = parseInt(color16List[0], 16);
+        g = parseInt(color16List[1], 16);
+        b = parseInt(color16List[2], 16);
+      } else {
+        hasAlpha = color.indexOf('a') !== -1;
+        let root = color.slice();
+        let idx = root.indexOf('(') + 1;
+        root = root.substring(idx);
+        let firstDotIdx = root.indexOf(',');
+        r = parseFloat(root.substring(0, firstDotIdx));
+        root = root.substring(firstDotIdx + 1);
+        let secondDotIdx = root.indexOf(',');
+        g = parseFloat(root.substring(0, secondDotIdx));
+        root = root.substring(secondDotIdx + 1);
+        if (hasAlpha) {
+          let thirdDotIdx = root.indexOf(',');
+          b = parseFloat(root.substring(0, thirdDotIdx));
+          alpha = parseFloat(root.substring(thirdDotIdx + 1));
+        } else {
+          b = parseFloat(root);
+        }
+      }
 
-				} else {
-					hasAlpha = color.indexOf('a') !== -1
-					let root = color.slice()
-					let idx = root.indexOf('(') + 1
-					root = root.substring(idx)
-					let firstDotIdx = root.indexOf(',')
-					r = parseFloat(root.substring(0, firstDotIdx))
-					root = root.substring(firstDotIdx + 1)
-					let secondDotIdx = root.indexOf(',')
-					g = parseFloat(root.substring(0, secondDotIdx))
-					root = root.substring(secondDotIdx + 1)
-					if (hasAlpha) {
-						let thirdDotIdx = root.indexOf(',')
-						b = parseFloat(root.substring(0, thirdDotIdx))
-						alpha = parseFloat(root.substring(thirdDotIdx + 1))
-					} else {
-						b = parseFloat(root)
-					}
-				}
+      let rgbc = [r, g, b];
+      // 减淡或加深
+      for (var i = 0; i < 3; i++)
+        type === 'light' ? (rgbc[i] = Math.floor((255 - rgbc[i]) * level + rgbc[i])) : (rgbc[i] = Math.floor(rgbc[i] * (1 - level)));
 
-				let rgbc = [r, g, b]
-				// 减淡或加深
-				for (var i = 0; i < 3; i++)
-					type === 'light' ? rgbc[i] = Math.floor((255 - rgbc[i]) * level + rgbc[i]) : rgbc[i] = Math.floor(rgbc[i] * (1 -
-						level))
-
-				if (hasAlpha) {
-					return `rgba(${rgbc[0]}, ${rgbc[1]}, ${rgbc[2]}, ${alpha})`
-				} else {
-					return `rgb(${rgbc[0]}, ${rgbc[1]}, ${rgbc[2]})`
-				}
-			},
-			sortFieldMatch(field) {
-				const stringArray = field.split('')
-				let newField = field
-				stringArray.forEach(t => {
-					if (/[A-Z]/.test(t)) {
-						newField = field.replace(t, `-${t.toLowerCase()}`)
-					}
-				})
-				return newField
-			}
-		}
-	}
+      if (hasAlpha) {
+        return `rgba(${rgbc[0]}, ${rgbc[1]}, ${rgbc[2]}, ${alpha})`;
+      } else {
+        return `rgb(${rgbc[0]}, ${rgbc[1]}, ${rgbc[2]})`;
+      }
+    },
+    sortFieldMatch(field) {
+      const stringArray = field.split('');
+      let newField = field;
+      stringArray.forEach((t) => {
+        if (/[A-Z]/.test(t)) {
+          newField = field.replace(t, `-${t.toLowerCase()}`);
+        }
+      });
+      return newField;
+    }
+  }
+};
 </script>
 
 <style>
-	@import 'iconfont.css';
-	.wyb-pagination-box {
-		width: 100%;
-		display: flex;
-		flex-direction: row;
-		align-items: center;
-		box-sizing: border-box;
-		justify-content: space-between;
-		flex-wrap: nowrap;
-	}
-	
-	.wyb-pagination-left {
-		flex: 1;
-		display: flex;
-		flex-direction: row;
-		align-items: center;
-		flex-wrap: nowrap;
-		justify-content: flex-start;
-	}
-	
-	.wyb-pagination-right {
-		flex: 1;
-		display: flex;
-		flex-direction: row;
-		align-items: center;
-		flex-wrap: nowrap;
-		justify-content: flex-end;
-	}
+@import 'iconfont.css';
+.wyb-pagination-box {
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  box-sizing: border-box;
+  justify-content: space-between;
+  flex-wrap: nowrap;
+}
+
+.wyb-pagination-left {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: nowrap;
+  justify-content: flex-start;
+}
+
+.wyb-pagination-right {
+  flex: 1;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-wrap: nowrap;
+  justify-content: flex-end;
+}
+
+.wyb-pagination-first-page-t,
+.wyb-pagination-prev-page-t,
+.wyb-pagination-next-page-t,
+.wyb-pagination-last-page-t {
+  font-size: 27rpx;
+  padding: 14rpx 25rpx;
+  box-sizing: border-box;
+  background-color: #f8f8f8;
+  border: 1px solid #e5e5e5;
+  white-space: nowrap;
+}
+
+.wyb-pagination-first-page-i,
+.wyb-pagination-prev-page-i,
+.wyb-pagination-next-page-i,
+.wyb-pagination-last-page-i {
+  font-size: 27rpx;
+  padding: 14rpx 33rpx;
+  box-sizing: border-box;
+  background-color: #f8f8f8;
+  border: 1px solid #e5e5e5;
+  white-space: nowrap;
+}
+
+.wyb-pagination-first-page-t,
+.wyb-pagination-first-page-i {
+  margin-right: 15rpx;
+}
+
+.wyb-pagination-last-page-t,
+.wyb-pagination-last-page-i {
+  margin-left: 15rpx;
+}
+
+.wyb-pagination-info {
+  font-size: 33rpx;
+  white-space: nowrap;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  flex: 1;
+}
+
+.wyb-pagination-input input {
+  text-align: center;
+}
 
-	.wyb-pagination-first-page-t,
-	.wyb-pagination-prev-page-t,
-	.wyb-pagination-next-page-t,
-	.wyb-pagination-last-page-t {
-		font-size: 27rpx;
-		padding: 14rpx 25rpx;
-		box-sizing: border-box;
-		background-color: #f8f8f8;
-		border: 1px solid #e5e5e5;
-		white-space: nowrap;
-	}
-	
-	.wyb-pagination-first-page-i,
-	.wyb-pagination-prev-page-i,
-	.wyb-pagination-next-page-i,
-	.wyb-pagination-last-page-i {
-		font-size: 27rpx;
-		padding: 14rpx 33rpx;
-		box-sizing: border-box;
-		background-color: #f8f8f8;
-		border: 1px solid #e5e5e5;
-		white-space: nowrap;
-	}
+.wyb-pagination-span {
+  margin: 0 2rpx;
+}
 
-	.wyb-pagination-first-page-t,
-	.wyb-pagination-first-page-i {
-		margin-right: 15rpx;
-	}
+.wyb-pagination-info-total {
+  margin-left: 10rpx;
+}
 
-	.wyb-pagination-last-page-t,
-	.wyb-pagination-last-page-i {
-		margin-left: 15rpx;
-	}
+.wyb-pagination-first-page-t:active,
+.wyb-pagination-prev-page-t:active,
+.wyb-pagination-next-page-t:active,
+.wyb-pagination-last-page-t:active,
+.wyb-pagination-first-page-i:active,
+.wyb-pagination-prev-page-i:active,
+.wyb-pagination-next-page-i:active,
+.wyb-pagination-last-page-i:active {
+  background-color: var(--hover) !important;
+}
 
-	.wyb-pagination-info {
-		font-size: 33rpx;
-		white-space: nowrap;
-		display: flex;
-		flex-direction: row;
-		align-items: center;
-		justify-content: center;
-		flex: 1;
-	}
-	
-	.wyb-pagination-input input {
-		text-align: center;
-	}
+.left-arrow {
+  transform: scale(0.9);
+  margin-right: 5rpx;
+}
 
-	.wyb-pagination-span {
-		margin: 0 2rpx;
-	}
-	
-	.wyb-pagination-info-total {
-		margin-left: 10rpx;
-	}
-	
-	.wyb-pagination-first-page-t:active,
-	.wyb-pagination-prev-page-t:active,
-	.wyb-pagination-next-page-t:active,
-	.wyb-pagination-last-page-t:active,
-	.wyb-pagination-first-page-i:active,
-	.wyb-pagination-prev-page-i:active,
-	.wyb-pagination-next-page-i:active,
-	.wyb-pagination-last-page-i:active {
-		background-color: var(--hover) !important;
-	}
-	
-	.left-arrow {
-		transform: scale(0.9);
-		margin-right: 5rpx;
-	}
-	
-	.right-arrow {
-		margin-left: 5rpx;
-		transform: scale(0.9) rotate(180deg);
-		-webkit-transform: scale(0.8) rotate(180deg);
-	}
+.right-arrow {
+  margin-left: 5rpx;
+  transform: scale(0.9) rotate(180deg);
+  -webkit-transform: scale(0.8) rotate(180deg);
+}
 </style>

+ 25 - 150
h5_web/pages/basicTraining/onlineTrainingDetails/onlineTrainingDetails.vue

@@ -2,36 +2,7 @@
   <view class="details">
     <u-navbar back-text="" title="" back-icon-color="#FFFFFF" :background="{ background: '#3D5D4C' }" :border-bottom="false"></u-navbar>
     <!-- 视频 -->
-    <view class="details-video" v-if="isPaly" v-loading="videoLoading">
-      <video
-        class="details-video-con"
-        id="myVideo"
-        @timeupdate="timeUpdate"
-        :src="videoInfo.videoUrl"
-        controls
-        :initial-time="initial_time"
-        object-fit="fill"
-        play-btn-position="center"
-        @tap="videoClick"
-        @loadedmetadata="loadedmetadata"
-      >
-        <cover-view class="video-control">
-          <cover-view class="multi rate" @tap.stop="showSwitchRate">x {{ currentRate }}</cover-view>
-        </cover-view>
-        <cover-view class="multi-list rate" :class="{ active: rateShow }">
-          <cover-view
-            v-for="(item, index) in ['0.5', '1.0', '1.5', '2.0']"
-            :key="index"
-            class="multi-item rate"
-            :data-rate="item"
-            @tap="switchRate"
-            :class="{ active: item == currentRate }"
-          >
-            {{ item }}
-          </cover-view>
-        </cover-view>
-      </video>
-    </view>
+    <video-box ref="videoBox" v-if="isPlay" :videoUrl="videoInfo.videoUrl" @recordDuration="recordDuration"></video-box>
     <!-- 介绍 -->
     <view class="details-content">
       <view class="details-content-title">{{ videoInfo.chapterName }}</view>
@@ -115,24 +86,13 @@
 </template>
 
 <script>
-import wybPagination from '@/components/wyb-pagination/wyb-pagination.vue';
 export default {
   data() {
     return {
       info: {},
-      videoContext: uni.createVideoContext('myVideo', this),
       videoInfo: {},
-      videoIndex: 0,
-      // 视频实时时间
-      initial_time: 0,
-      // 视频已经播放时间
-      playedTime: 0,
-      rateShow: false, // 倍速浮层
-      currentRate: 1.0, // 默认倍速
-      // 视频实际时间
-      video_real_time: 0,
       classesId: '',
-      isPaly: true,
+      isPlay: true,
       query: {
         pageNum: 1,
         pageSize: 5,
@@ -145,9 +105,7 @@ export default {
         starLevel: 0,
         content: ''
       },
-      currentDuration: 0,
-      duration: 0,
-      videoLoading: false
+      videoIndex: 0
     };
   },
   onLoad(page) {
@@ -159,43 +117,9 @@ export default {
     }
   },
   beforeDestroy() {
-    this.confirmSubmitDuration();
+    this.$refs['videoBox'].recordDuration(this.videoIndex, false);
   },
   methods: {
-    /**
-     * 获取视频总时长
-     * @param {Object} data
-     */
-    loadedmetadata(data) {
-      this.duration = data.detail.duration;
-      this.videoLoading = false;
-    },
-    /**
-     * 显示倍速浮层
-     * @param {Object} rate
-     */
-    showSwitchRate(rate) {
-      let that = this;
-      that.rateShow = true;
-    },
-    /**
-     * 切换倍速
-     * @param {Object} e
-     */
-    switchRate(e) {
-      let that = this;
-      let rate = Number(e.currentTarget.dataset.rate);
-      that.currentRate = rate;
-      that.rateShow = false;
-      this.videoContext.playbackRate(rate);
-    },
-    /**
-     * 视频点击
-     * @param {Object} e
-     */
-    videoClick(e) {
-      this.rateShow = false;
-    },
     /**
      * 获取课程详情
      * @param {Object} id
@@ -210,11 +134,11 @@ export default {
           if (res.code === 200) {
             this.info = res.data;
             this.videoInfo = res.data.chapterList[this.videoIndex];
-            this.initial_time = Number(res.data.chapterList[this.videoIndex].currentDuration);
-            this.video_real_time = Number(res.data.chapterList[this.videoIndex].playDuration);
-            this.playedTime = Number(res.data.chapterList[this.videoIndex].playDuration);
             this.query.pageNum = 1;
-            this.isPaly = true;
+            this.isPlay = true;
+            this.$nextTick(() => {
+              this.$refs['videoBox'].loadVideo(this.videoInfo);
+            });
             this.getCommentList();
           }
         });
@@ -224,57 +148,28 @@ export default {
      * @param {Object} index
      */
     classesClick(index) {
-      this.isPaly = false;
-      let playDuration = this.video_real_time;
-      if (this.videoInfo.playDuration > this.video_real_time) {
-        this.currentDuration = this.video_real_time;
-        playDuration = this.videoInfo.playDuration;
-      } else {
-        this.currentDuration = this.video_real_time;
-      }
-      this.submitTimeLong(
-        {
-          tabId: this.videoInfo.id,
-          playDuration: playDuration,
-          duration: this.duration,
-          currentDuration: this.currentDuration
-        },
-        index
-      );
+      this.$refs['videoBox'].recordDuration(index, true);
+      this.videoIndex = index;
+      this.isPlay = false;
     },
     /**
-     * 控制视频不能快进
-     * @param {Object} e
+     * 描述
+     * @date 2022-10-19
+     * @param {any} obj
+     * @param {any} index
+     * @returns {any}
      */
-    timeUpdate(e) {
-      //播放的总时长
-      let duration = e.detail.duration;
-      //实时播放进度 秒数
-      let jumpTime = parseInt(e.detail.currentTime);
-      //当前视频进度
-      if (jumpTime - this.playedTime > 3) {
-        // 差别过大,调用seek方法跳转到实际观看时间
-        this.videoContext.seek(this.playedTime);
-        wx.showToast({
-          title: '未完整看完该视频,不能快进',
-          icon: 'none',
-          duration: 2000
-        });
-      } else {
-        this.video_real_time = parseInt(e.detail.currentTime);
-        if (this.video_real_time > this.playedTime) {
-          this.playedTime = this.video_real_time;
-        }
-      }
-      if (parseInt(this.duration) !== 0 && parseInt(this.duration) === parseInt(this.video_real_time)) {
-        this.videoContext.pause();
-        this.confirmSubmitDuration();
-      }
+    recordDuration(obj, index, refresh = true) {
+      this.submitTimeLong(
+        { tabId: this.videoInfo.id, playDuration: obj.playDuration, currentDuration: obj.currentDuration, duration: obj.duration },
+        index || this.videoIndex,
+        refresh
+      );
     },
     /**
      * 提交课程时长
      */
-    submitTimeLong({ tabId, playDuration, duration, currentDuration }, index, flag) {
+    submitTimeLong({ tabId, playDuration, duration, currentDuration }, index, refresh) {
       this.$u.api.training
         .videoTimeLongApi({
           tabId,
@@ -284,13 +179,12 @@ export default {
         })
         .then((res) => {
           if (res.code === 200) {
-            if (!flag) {
+            this.videoInfo = this.info.chapterList[index];
+            if (refresh) {
               this.$refs.uToast.show({
                 title: '已记录章节时长!',
                 type: 'success'
               });
-              this.videoInfo = this.info.chapterList[index];
-              this.videoIndex = index;
               this.getClassesDetails(this.classesId);
             }
           } else {
@@ -353,25 +247,6 @@ export default {
           type: 'warning'
         });
       }
-    },
-    confirmSubmitDuration() {
-      let playDuration = this.video_real_time;
-      if (this.videoInfo.playDuration > this.video_real_time) {
-        this.currentDuration = this.video_real_time;
-        playDuration = this.videoInfo.playDuration;
-      } else {
-        this.currentDuration = this.video_real_time;
-      }
-      this.submitTimeLong(
-        {
-          tabId: this.videoInfo.id,
-          playDuration: playDuration,
-          duration: this.duration,
-          currentDuration: this.currentDuration
-        },
-        0,
-        true
-      );
     }
   }
 };

+ 410 - 0
h5_web/pages/skillsTraining/courseDetailed/courseDetailed copy.vue

@@ -0,0 +1,410 @@
+<template>
+  <view class="details">
+    <u-navbar back-text="" title="" back-icon-color="#FFFFFF" :background="{ background: '#3D5D4C' }" :border-bottom="false"></u-navbar>
+    <!-- 视频 -->
+    <view class="details-video" v-if="isPlay" v-loading="videoLoading">
+      <video
+        class="details-video-con"
+        id="myVideo"
+        @timeupdate="timeUpdate"
+        :src="videoInfo.videoUrl"
+        controls
+        :initial-time="initial_time"
+        object-fit="fill"
+        play-btn-position="center"
+        @ended="ended"
+        @tap="videoClick"
+        @loadedmetadata="loadedmetadata"
+      >
+        <cover-view class="video-control">
+          <cover-view class="multi rate" @tap.stop="showSwitchRate">x {{ currentRate }}</cover-view>
+        </cover-view>
+        <cover-view class="multi-list rate" :class="{ active: rateShow }">
+          <cover-view
+            v-for="(item, index) in ['0.5', '1.0', '1.5', '2.0']"
+            :key="index"
+            class="multi-item rate"
+            :data-rate="item"
+            @tap="switchRate"
+            :class="{ active: item == currentRate }"
+          >
+            {{ item }}
+          </cover-view>
+        </cover-view>
+      </video>
+    </view>
+    <!-- 介绍 -->
+    <view class="details-content">
+      <view class="details-content-title">{{ videoInfo.chapterName || '-' }}</view>
+      <view class="details-content-progress"
+        >本课程 共{{ info.amount || 0 }}课,已学完{{ info.finishCount || 0 }}课,共进度{{ info.finishPercent || 0 }}%</view
+      >
+      <view class="details-content-teacher">主讲老师:{{ info.presenter || '-' }}</view>
+      <view class="details-content-info">{{ videoInfo.chapterInfo || '-' }}</view>
+    </view>
+
+    <!-- 课程章节 -->
+    <view class="details-classes">
+      <view class="details-classes-header">
+        <view>精选课程</view>
+        <view>
+          更多
+          <u-icon name="arrow-right" size="22" color="#A3A3A3" />
+        </view>
+      </view>
+      <view class="details-classes-list">
+        <view
+          class="details-classes-list-item"
+          v-for="(item, index) in info.chapterList"
+          :key="index"
+          :class="{ active: index === videoIndex }"
+          @click="classesClick(index)"
+        >
+          <view>{{ index + 1 }}</view>
+          <view>{{ item.flag === 2 ? '已学' : item.finishPercent + '%' }}</view>
+        </view>
+      </view>
+    </view>
+    <view class="details-line">
+      <view></view>
+    </view>
+    <!-- 评论 -->
+    <view class="details-comment">
+      <view class="details-comment-header">
+        <view>课程评论</view>
+        <view>共{{ total || 0 }}条评论</view>
+      </view>
+      <view class="details-comment-list">
+        <view class="details-comment-list-item" v-for="(item, index) in commentList" :key="index">
+          <view class="left">
+            <u-avatar :src="item.createByAvatar" size="96" mode="square"></u-avatar>
+          </view>
+          <view class="right">
+            <view>{{ item.createBy }}</view>
+            <view>
+              <u-rate :count="5" size="28" disabled="" active-color="#FFBC00" v-model="item.starLevel"> </u-rate>
+              <text>{{ item.createTime }}</text>
+            </view>
+            <view>{{ item.content }}</view>
+          </view>
+        </view>
+      </view>
+      <view class="details-comment-page" v-if="total">
+        <wyb-pagination :padding="0" :totalItems="total" :current="query.pageNum" @change="pageChange" />
+      </view>
+      <view class="details-comment-mine"><text>我的评论</text></view>
+      <view class="details-comment-conent">
+        <view class="details-comment-conent-star">
+          <u-rate :count="5" size="40" active-color="#FFBC00" v-model="form.starLevel"></u-rate>
+        </view>
+        <view class="details-comment-content-textarea">
+          <u-input
+            v-model="form.content"
+            placeholder="请输入您的评价"
+            type="textarea"
+            :custom-style="{ backgroundColor: '#F5F5F5', padding: '30rpx', borderRadius: '10rpx', minHeight: '280rpx' }"
+          >
+          </u-input>
+        </view>
+        <view class="details-comment-conent-button" @click="submitCommet">提交</view>
+      </view>
+    </view>
+    <u-toast ref="uToast" />
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      info: {},
+      videoContext: uni.createVideoContext('myVideo', this),
+      videoInfo: {},
+      videoIndex: 0,
+      // 视频实时时间
+      initial_time: 0,
+      // 视频已经播放时间
+      playedTime: 0,
+      rateShow: false, // 倍速浮层
+      currentRate: 1.0, // 默认倍速
+      // 视频实际时间
+      video_real_time: 0,
+      classesId: '',
+      isPlay: true,
+      query: {
+        pageNum: 1,
+        pageSize: 5,
+        tabId: ''
+      },
+      total: 0,
+      commentList: [],
+      form: {
+        tabId: '',
+        starLevel: 0,
+        content: ''
+      },
+      currentDuration: 0,
+      duration: 0,
+      isApply: 1,
+      // 视频加载层
+      videoLoading: false
+    };
+  },
+  onLoad(page) {
+    if (page.id) {
+      this.getClassesDetails(page.id);
+      this.classesId = page.id;
+      this.query.tabId = this.classesId;
+      this.form.tabId = this.classesId;
+      this.isApply = page.isApply || 1;
+    }
+  },
+  beforeDestroy() {
+    this.confirmSubmitDuration();
+  },
+  methods: {
+    /**
+     * 获取视频总时长
+     * @param {Object} data
+     */
+    loadedmetadata(data) {
+      this.duration = data.detail.duration;
+      this.videoLoading = false;
+    },
+    /**
+     * 显示倍速浮层
+     * @param {Object} rate
+     */
+    showSwitchRate(rate) {
+      let that = this;
+      that.rateShow = true;
+    },
+    /**
+     * 切换倍速
+     * @param {Object} e
+     */
+    switchRate(e) {
+      let that = this;
+      let rate = Number(e.currentTarget.dataset.rate);
+      that.currentRate = rate;
+      that.rateShow = false;
+      this.videoContext.playbackRate(rate);
+    },
+    /**
+     * 视频点击
+     * @param {Object} e
+     */
+    videoClick(e) {
+      this.rateShow = false;
+    },
+    pause(e) {
+      console.log(e);
+    },
+    /**
+     * 获取课程详情
+     * @param {Object} id
+     */
+    getClassesDetails(id) {
+      this.videoLoading = true;
+      this.$u.api.school
+        .getPackageCourseDetail({
+          id
+        })
+        .then((res) => {
+          if (res.code === 200) {
+            this.info = res.data;
+            this.videoInfo = res.data.chapterList[this.videoIndex];
+            this.initial_time = Number(res.data.chapterList[this.videoIndex].currentDuration);
+            this.video_real_time = Number(res.data.chapterList[this.videoIndex].playDuration);
+            this.playedTime = Number(res.data.chapterList[this.videoIndex].playDuration);
+            this.query.pageNum = 1;
+            this.isPlay = true;
+            this.getCommentList();
+          }
+        });
+    },
+    /**
+     * 课程章节点击
+     * @param {Object} index
+     */
+    classesClick(index) {
+      this.isPlay = false;
+      let playDuration = this.video_real_time;
+      if (this.videoInfo.playDuration > this.video_real_time) {
+        this.currentDuration = this.video_real_time;
+        playDuration = this.videoInfo.playDuration;
+      } else {
+        this.currentDuration = this.video_real_time;
+      }
+      this.submitTimeLong(
+        {
+          tabId: this.videoInfo.id,
+          playDuration: playDuration,
+          duration: this.duration,
+          currentDuration: this.currentDuration
+        },
+        index
+      );
+    },
+    /**
+     * 控制视频不能快进
+     * @param {Object} e
+     */
+    timeUpdate(e) {
+      //播放的总时长
+      let duration = e.detail.duration;
+      //实时播放进度 秒数
+      let jumpTime = parseInt(e.detail.currentTime);
+      //当前视频进度
+      if (jumpTime - this.playedTime > 3) {
+        // 差别过大,调用seek方法跳转到实际观看时间
+        this.videoContext.seek(this.playedTime);
+        wx.showToast({
+          title: '未完整看完该视频,不能快进',
+          icon: 'none',
+          duration: 2000
+        });
+      } else {
+        this.video_real_time = parseInt(e.detail.currentTime);
+        if (this.video_real_time > this.playedTime) {
+          this.playedTime = this.video_real_time;
+        }
+      }
+      if (parseInt(this.duration) !== 0 && parseInt(this.duration) === parseInt(this.video_real_time)) {
+        this.videoContext.pause();
+        this.confirmSubmitDuration();
+      }
+    },
+    /**
+     * 视频结束
+     */
+    ended() {
+      // 用户把进度条拉到最后,但是实际观看时间不够,跳转回去会自动暂停。
+      // 这里加个判断。
+      if (this.playedTime < this.duration) {
+        this.videoContext.play();
+        this.confirmSubmitDuration();
+      }
+    },
+    /**
+     * 提交课程时长
+     */
+    submitTimeLong({ tabId, playDuration, currentDuration, duration }, index, flag) {
+      if (tabId) {
+        this.$u.api.training
+          .videoTimeLongApi({
+            tabId,
+            playDuration,
+            currentDuration,
+            duration
+          })
+          .then((res) => {
+            if (res.code === 200) {
+              if (!flag) {
+                this.$refs.uToast.show({
+                  title: '已记录章节时长!',
+                  type: 'success'
+                });
+                this.videoInfo = this.info.chapterList[index];
+                this.videoIndex = index;
+                this.getClassesDetails(this.classesId);
+              }
+            } else {
+              this.$refs.uToast.show({
+                title: res.msg,
+                type: 'error'
+              });
+            }
+          });
+      }
+    },
+    /**
+     * 获取评论列表
+     */
+    getCommentList() {
+      if (this.query.tabId) {
+        this.$u.api.training.getClassesCommentApi(this.query).then((res) => {
+          if (res.code === 200) {
+            this.total = Number(res.total);
+            this.commentList = res.rows;
+          }
+        });
+      }
+    },
+    /**
+     * @param {Object} e 分页触发
+     */
+    pageChange(e) {
+      this.query.pageNum = e.current;
+      this.getCommentList();
+    },
+    /**
+     * 提交评论
+     */
+    submitCommet() {
+      if (this.form.tabId) {
+        if (this.form.starLevel && this.form.content) {
+          this.$u.api.training.addClassesCommentApi(this.form).then((res) => {
+            if (res.code === 200) {
+              this.$refs.uToast.show({
+                title: '评论成功!',
+                type: 'success'
+              });
+              this.form.content = '';
+              this.form.starLevel = 0;
+              this.getCommentList();
+            } else {
+              this.$refs.uToast.show({
+                title: res.msg,
+                type: 'error'
+              });
+            }
+          });
+        }
+        if (!this.form.starLevel) {
+          this.$refs.uToast.show({
+            title: '请选择星级',
+            type: 'warning'
+          });
+        }
+        if (!this.form.content) {
+          this.$refs.uToast.show({
+            title: '请输入评论内容',
+            type: 'warning'
+          });
+        }
+      } else {
+        this.$refs.uToast.show({
+          title: '未找到课程章节,无法提交评论!',
+          type: 'warning'
+        });
+      }
+    },
+    confirmSubmitDuration() {
+      let playDuration = this.video_real_time;
+      if (this.videoInfo.playDuration > this.video_real_time) {
+        this.currentDuration = this.video_real_time;
+        playDuration = this.videoInfo.playDuration;
+      } else {
+        this.currentDuration = this.video_real_time;
+      }
+      if (Number(this.isApply) === 1) {
+        this.submitTimeLong(
+          {
+            tabId: this.videoInfo.id,
+            playDuration: playDuration,
+            currentDuration: this.currentDuration,
+            duration: this.duration
+          },
+          0,
+          true
+        );
+      }
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+@import './courseDetailed.scss';
+</style>

+ 0 - 90
h5_web/pages/skillsTraining/courseDetailed/courseDetailed.scss

@@ -5,14 +5,6 @@ page {
 	
 	.details {
 		
-		&-video {
-			
-			&-con {
-				width: 100%;
-				height: 400rpx;
-			}
-		}
-		
 		&-content {
 			background-color: #fff;
 			padding: 20rpx 30rpx;
@@ -134,11 +126,6 @@ page {
 					margin-bottom: 60rpx;
 					display: flex;
 					align-items: center;
-					// &:last-child {
-					// 	padding-bottom: 60rpx;
-					// 	margin-bottom: 20rpx;
-					// 	border-bottom: solid 1px #DBDBDB;
-					// }
 					.left {
 						margin-right: 24rpx;
 					}
@@ -194,80 +181,3 @@ page {
 		}
 	}
 }
-
-.video-control {
-	background-color: rgba(0, 0, 0, 0.1);
-	height: 90rpx;
-	position: absolute;
-	top: 0;
-	left: 0;
-	width: 100%;
-	z-index: 997;
-}
-.video-wrap {
-	position: relative;
-}
-.multi-list.full-screen.vertical {
-	height: 100vh !important;
-	padding: 8vh 0;
-}
-.multi-list.full-screen.horizontal {
-	height: 100vw !important;
-	padding: 8vw 0;
-}
-.multi {
-	position: absolute;
-	right: 30rpx;
-	top: 50%;
-	transform: translateY(-50%);
-	z-index: 998;
-	width: 100rpx;
-	color: #fff;
-	padding: 10rpx 0;
-	text-align: center;
-	transition: color ease 0.3s;
-}
-.multi.rate {
-	right: 30rpx;
-}
-.multi-list {
-	position: absolute;
-	height: 100%;
-	width: 0;
-	background-color: rgba(0, 0, 0, 0.65);
-	top: 0;
-	right: 0;
-	transition: width 0.3s ease;
-	z-index: 999;
-	box-sizing: border-box;
-	padding: 20rpx 0;
-}
-.multi-list.rate {
-	padding: 25rpx 0;
-}
-.multi-list.active {
-	width: 200rpx;
-}
-.multi-item {
-	text-align: center;
-	color: #fff;
-	line-height: 80rpx;
-	transition: color ease 0.3s;
-}
-.multi-item.rate {
-	line-height: 70rpx;
-}
-.multi-item:hover,
-.multi:hover {
-	color: #00d8ff;
-}
-.multi-item.active {
-	color: #00d8ff;
-}
-.mask-loading {
-	width: 100%;
-	height: 100vh;
-	display: flex;
-	justify-content: center;
-	align-items: center;
-}

+ 26 - 162
h5_web/pages/skillsTraining/courseDetailed/courseDetailed.vue

@@ -2,37 +2,7 @@
   <view class="details">
     <u-navbar back-text="" title="" back-icon-color="#FFFFFF" :background="{ background: '#3D5D4C' }" :border-bottom="false"></u-navbar>
     <!-- 视频 -->
-    <view class="details-video" v-if="isPlay" v-loading="videoLoading">
-      <video
-        class="details-video-con"
-        id="myVideo"
-        @timeupdate="timeUpdate"
-        :src="videoInfo.videoUrl"
-        controls
-        :initial-time="initial_time"
-        object-fit="fill"
-        play-btn-position="center"
-        @ended="ended"
-        @tap="videoClick"
-        @loadedmetadata="loadedmetadata"
-      >
-        <cover-view class="video-control">
-          <cover-view class="multi rate" @tap.stop="showSwitchRate">x {{ currentRate }}</cover-view>
-        </cover-view>
-        <cover-view class="multi-list rate" :class="{ active: rateShow }">
-          <cover-view
-            v-for="(item, index) in ['0.5', '1.0', '1.5', '2.0']"
-            :key="index"
-            class="multi-item rate"
-            :data-rate="item"
-            @tap="switchRate"
-            :class="{ active: item == currentRate }"
-          >
-            {{ item }}
-          </cover-view>
-        </cover-view>
-      </video>
-    </view>
+    <video-box ref="videoBox" v-if="isPlay" :videoUrl="videoInfo.videoUrl" @recordDuration="recordDuration"></video-box>
     <!-- 介绍 -->
     <view class="details-content">
       <view class="details-content-title">{{ videoInfo.chapterName || '-' }}</view>
@@ -118,17 +88,8 @@ export default {
   data() {
     return {
       info: {},
-      videoContext: uni.createVideoContext('myVideo', this),
       videoInfo: {},
       videoIndex: 0,
-      // 视频实时时间
-      initial_time: 0,
-      // 视频已经播放时间
-      playedTime: 0,
-      rateShow: false, // 倍速浮层
-      currentRate: 1.0, // 默认倍速
-      // 视频实际时间
-      video_real_time: 0,
       classesId: '',
       isPlay: true,
       query: {
@@ -143,11 +104,7 @@ export default {
         starLevel: 0,
         content: ''
       },
-      currentDuration: 0,
-      duration: 0,
-      isApply: 1,
-      // 视频加载层
-      videoLoading: false
+      isApply: 1
     };
   },
   onLoad(page) {
@@ -160,52 +117,14 @@ export default {
     }
   },
   beforeDestroy() {
-    this.confirmSubmitDuration();
+    this.$refs['videoBox'].recordDuration(this.videoIndex, false);
   },
   methods: {
-    /**
-     * 获取视频总时长
-     * @param {Object} data
-     */
-    loadedmetadata(data) {
-      this.duration = data.detail.duration;
-      this.videoLoading = false;
-    },
-    /**
-     * 显示倍速浮层
-     * @param {Object} rate
-     */
-    showSwitchRate(rate) {
-      let that = this;
-      that.rateShow = true;
-    },
-    /**
-     * 切换倍速
-     * @param {Object} e
-     */
-    switchRate(e) {
-      let that = this;
-      let rate = Number(e.currentTarget.dataset.rate);
-      that.currentRate = rate;
-      that.rateShow = false;
-      this.videoContext.playbackRate(rate);
-    },
-    /**
-     * 视频点击
-     * @param {Object} e
-     */
-    videoClick(e) {
-      this.rateShow = false;
-    },
-    pause(e) {
-      console.log(e);
-    },
     /**
      * 获取课程详情
      * @param {Object} id
      */
     getClassesDetails(id) {
-      this.videoLoading = true;
       this.$u.api.school
         .getPackageCourseDetail({
           id
@@ -214,11 +133,11 @@ export default {
           if (res.code === 200) {
             this.info = res.data;
             this.videoInfo = res.data.chapterList[this.videoIndex];
-            this.initial_time = Number(res.data.chapterList[this.videoIndex].currentDuration);
-            this.video_real_time = Number(res.data.chapterList[this.videoIndex].playDuration);
-            this.playedTime = Number(res.data.chapterList[this.videoIndex].playDuration);
             this.query.pageNum = 1;
             this.isPlay = true;
+            this.$nextTick(() => {
+              this.$refs['videoBox'].loadVideo(this.videoInfo);
+            });
             this.getCommentList();
           }
         });
@@ -228,68 +147,35 @@ export default {
      * @param {Object} index
      */
     classesClick(index) {
+      this.$refs['videoBox'].recordDuration(index, true);
+      this.videoIndex = index;
       this.isPlay = false;
-      let playDuration = this.video_real_time;
-      if (this.videoInfo.playDuration > this.video_real_time) {
-        this.currentDuration = this.video_real_time;
-        playDuration = this.videoInfo.playDuration;
-      } else {
-        this.currentDuration = this.video_real_time;
-      }
-      this.submitTimeLong(
-        {
-          tabId: this.videoInfo.id,
-          playDuration: playDuration,
-          duration: this.duration,
-          currentDuration: this.currentDuration
-        },
-        index
-      );
     },
     /**
-     * 控制视频不能快进
-     * @param {Object} e
+     * 描述
+     * @date 2022-10-19
+     * @param {any} obj
+     * @param {any} index
+     * @returns {any}
      */
-    timeUpdate(e) {
-      //播放的总时长
-      let duration = e.detail.duration;
-      //实时播放进度 秒数
-      let jumpTime = parseInt(e.detail.currentTime);
-      //当前视频进度
-      if (jumpTime - this.playedTime > 3) {
-        // 差别过大,调用seek方法跳转到实际观看时间
-        this.videoContext.seek(this.playedTime);
-        wx.showToast({
-          title: '未完整看完该视频,不能快进',
-          icon: 'none',
-          duration: 2000
-        });
+    recordDuration(obj, index, refresh = true) {
+      if (Number(this.isApply) === 1) {
+        this.submitTimeLong(
+          { tabId: this.videoInfo.id, playDuration: obj.playDuration, currentDuration: obj.currentDuration, duration: obj.duration },
+          index || this.videoIndex,
+          refresh
+        );
       } else {
-        this.video_real_time = parseInt(e.detail.currentTime);
-        if (this.video_real_time > this.playedTime) {
-          this.playedTime = this.video_real_time;
+        if (refresh) {
+          this.videoInfo = this.info.chapterList[index];
+          this.getClassesDetails(this.classesId);
         }
       }
-      if (parseInt(this.duration) !== 0 && parseInt(this.duration) === parseInt(this.video_real_time)) {
-        this.videoContext.pause();
-        this.confirmSubmitDuration();
-      }
-    },
-    /**
-     * 视频结束
-     */
-    ended() {
-      // 用户把进度条拉到最后,但是实际观看时间不够,跳转回去会自动暂停。
-      // 这里加个判断。
-      if (this.playedTime < this.duration) {
-        this.videoContext.play();
-        this.confirmSubmitDuration();
-      }
     },
     /**
      * 提交课程时长
      */
-    submitTimeLong({ tabId, playDuration, currentDuration, duration }, index, flag) {
+    submitTimeLong({ tabId, playDuration, currentDuration, duration }, index, refresh) {
       if (tabId) {
         this.$u.api.training
           .videoTimeLongApi({
@@ -300,13 +186,12 @@ export default {
           })
           .then((res) => {
             if (res.code === 200) {
-              if (!flag) {
+              this.videoInfo = this.info.chapterList[index];
+              if (refresh) {
                 this.$refs.uToast.show({
                   title: '已记录章节时长!',
                   type: 'success'
                 });
-                this.videoInfo = this.info.chapterList[index];
-                this.videoIndex = index;
                 this.getClassesDetails(this.classesId);
               }
             } else {
@@ -379,27 +264,6 @@ export default {
           type: 'warning'
         });
       }
-    },
-    confirmSubmitDuration() {
-      let playDuration = this.video_real_time;
-      if (this.videoInfo.playDuration > this.video_real_time) {
-        this.currentDuration = this.video_real_time;
-        playDuration = this.videoInfo.playDuration;
-      } else {
-        this.currentDuration = this.video_real_time;
-      }
-      if (Number(this.isApply) === 1) {
-        this.submitTimeLong(
-          {
-            tabId: this.videoInfo.id,
-            playDuration: playDuration,
-            currentDuration: this.currentDuration,
-            duration: this.duration
-          },
-          0,
-          true
-        );
-      }
     }
   }
 };

+ 27 - 152
h5_web/pages/upgrade/courseDetail/courseDetail.vue

@@ -1,36 +1,7 @@
 <template>
   <view class="details">
     <!-- 视频 -->
-    <view class="details-video" v-if="isPaly" v-loading="videoLoading">
-      <video
-        class="details-video-con"
-        id="myVideo"
-        @timeupdate="timeUpdate"
-        :src="videoInfo.videoUrl"
-        controls
-        :initial-time="initial_time"
-        object-fit="fill"
-        play-btn-position="center"
-        @tap="videoClick"
-        @loadedmetadata="loadedmetadata"
-      >
-        <cover-view class="video-control">
-          <cover-view class="multi rate" @tap.stop="showSwitchRate">x {{ currentRate }}</cover-view>
-        </cover-view>
-        <cover-view class="multi-list rate" :class="{ active: rateShow }">
-          <cover-view
-            v-for="(item, index) in ['0.5', '1.0', '1.5', '2.0']"
-            :key="index"
-            class="multi-item rate"
-            :data-rate="item"
-            @tap="switchRate"
-            :class="{ active: item == currentRate }"
-          >
-            {{ item }}
-          </cover-view>
-        </cover-view>
-      </video>
-    </view>
+    <video-box ref="videoBox" v-if="isPlay" :videoUrl="videoInfo.videoUrl" @recordDuration="recordDuration"></video-box>
     <!-- 介绍 -->
     <view class="details-content">
       <view class="details-content-title">{{ videoInfo.chapterName }}</view>
@@ -118,19 +89,10 @@ export default {
   data() {
     return {
       info: {},
-      videoContext: uni.createVideoContext('myVideo', this),
       videoInfo: {},
       videoIndex: 0,
-      // 视频实时时间
-      initial_time: 0,
-      // 视频已经播放时间
-      playedTime: 0,
-      rateShow: false, // 倍速浮层
-      currentRate: 1.0, // 默认倍速
-      // 视频实际时间
-      video_real_time: 0,
       classesId: '',
-      isPaly: true,
+      isPlay: true,
       query: {
         pageNum: 1,
         pageSize: 5,
@@ -142,10 +104,7 @@ export default {
         tabId: '',
         starLevel: 0,
         content: ''
-      },
-      currentDuration: 0,
-      duration: 0,
-			videoLoading: false
+      }
     };
   },
   onLoad(page) {
@@ -157,63 +116,28 @@ export default {
     }
   },
   beforeDestroy() {
-    this.confirmSubmitDuration();
+    this.$refs['videoBox'].recordDuration(this.videoIndex, false);
   },
   methods: {
-    /**
-     * 获取视频总时长
-     * @param {Object} data
-     */
-    loadedmetadata(data) {
-      this.duration = data.detail.duration;
-			this.videoLoading = false
-    },
-    /**
-     * 显示倍速浮层
-     * @param {Object} rate
-     */
-    showSwitchRate(rate) {
-      let that = this;
-      that.rateShow = true;
-    },
-    /**
-     * 切换倍速
-     * @param {Object} e
-     */
-    switchRate(e) {
-      let that = this;
-      let rate = Number(e.currentTarget.dataset.rate);
-      that.currentRate = rate;
-      that.rateShow = false;
-      this.videoContext.playbackRate(rate);
-    },
-    /**
-     * 视频点击
-     * @param {Object} e
-     */
-    videoClick(e) {
-      this.rateShow = false;
-    },
     /**
      * 获取课程详情
      * @param {Object} id
      */
     getClassesDetails(id) {
-			this.videoLoading = true
+      this.videoLoading = true;
       this.$u.api.school
         .getPackageCourseDetail({
           id
         })
         .then((res) => {
           if (res.code === 200) {
-            console.log(res);
             this.info = res.data;
             this.videoInfo = res.data.chapterList[this.videoIndex];
-            this.initial_time = Number(res.data.chapterList[this.videoIndex].currentDuration);
-            this.video_real_time = Number(res.data.chapterList[this.videoIndex].playDuration);
-            this.playedTime = Number(res.data.chapterList[this.videoIndex].playDuration);
-            this.query.pageNum = res.data.page;
-            this.isPaly = true;
+            this.query.pageNum = 1;
+            this.isPlay = true;
+            this.$nextTick(() => {
+              this.$refs['videoBox'].loadVideo(this.videoInfo);
+            });
             this.getCommentList();
           }
         });
@@ -223,57 +147,28 @@ export default {
      * @param {Object} index
      */
     classesClick(index) {
-      this.isPaly = false;
-      let playDuration = this.video_real_time;
-      if (this.videoInfo.playDuration > this.video_real_time) {
-        this.currentDuration = this.video_real_time;
-        playDuration = this.videoInfo.playDuration;
-      } else {
-        this.currentDuration = this.video_real_time;
-      }
-      this.submitTimeLong(
-        {
-          tabId: this.videoInfo.id,
-          playDuration: playDuration,
-          duration: this.duration,
-          currentDuration: this.currentDuration
-        },
-        index
-      );
+      this.$refs['videoBox'].recordDuration(index, true);
+      this.videoIndex = index;
+      this.isPlay = false;
     },
     /**
-     * 控制视频不能快进
-     * @param {Object} e
+     * 描述
+     * @date 2022-10-19
+     * @param {any} obj
+     * @param {any} index
+     * @returns {any}
      */
-    timeUpdate(e) {
-      //播放的总时长
-      let duration = e.detail.duration;
-      //实时播放进度 秒数
-      let jumpTime = parseInt(e.detail.currentTime);
-      //当前视频进度
-      if (jumpTime - this.playedTime > 3) {
-        // 差别过大,调用seek方法跳转到实际观看时间
-        this.videoContext.seek(this.playedTime);
-        wx.showToast({
-          title: '未完整看完该视频,不能快进',
-          icon: 'none',
-          duration: 2000
-        });
-      } else {
-        this.video_real_time = parseInt(e.detail.currentTime);
-        if (this.video_real_time > this.playedTime) {
-          this.playedTime = this.video_real_time;
-        }
-      }
-      if (parseInt(this.duration) !== 0 && parseInt(this.duration) === parseInt(this.video_real_time)) {
-        this.videoContext.pause();
-        this.confirmSubmitDuration();
-      }
+    recordDuration(obj, index, refresh = true) {
+      this.submitTimeLong(
+        { tabId: this.videoInfo.id, playDuration: obj.playDuration, currentDuration: obj.currentDuration, duration: obj.duration },
+        index || this.videoIndex,
+        refresh
+      );
     },
     /**
      * 提交课程时长
      */
-    submitTimeLong({ tabId, playDuration, duration, currentDuration }, index, flag) {
+    submitTimeLong({ tabId, playDuration, duration, currentDuration }, index, refresh) {
       if (tabId) {
         this.$u.api.training
           .videoTimeLongApi({
@@ -284,13 +179,12 @@ export default {
           })
           .then((res) => {
             if (res.code === 200) {
-              if (!flag) {
+              this.videoInfo = this.info.chapterList[index];
+              if (refresh) {
                 this.$refs.uToast.show({
                   title: '已记录章节时长!',
                   type: 'success'
                 });
-                this.videoInfo = this.info.chapterList[index];
-                this.videoIndex = index;
                 this.getClassesDetails(this.classesId);
               }
             } else {
@@ -363,25 +257,6 @@ export default {
           type: 'warning'
         });
       }
-    },
-    confirmSubmitDuration() {
-      let playDuration = this.video_real_time;
-      if (this.videoInfo.playDuration > this.video_real_time) {
-        this.currentDuration = this.video_real_time;
-        playDuration = this.videoInfo.playDuration;
-      } else {
-        this.currentDuration = this.video_real_time;
-      }
-      this.submitTimeLong(
-        {
-          tabId: this.videoInfo.id,
-          playDuration: playDuration,
-          duration: this.duration,
-          currentDuration: this.currentDuration
-        },
-        0,
-        true
-      );
     }
   }
 };