video.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. import { defineComponent, onMounted, reactive, ref, watch } from 'vue'
  2. import styles from './video.module.less'
  3. import poster from './images/video_bg.png'
  4. import { Button, Loading } from 'vant'
  5. import { browser } from '@/helpers/utils'
  6. import Plyr from 'plyr'
  7. import 'plyr/dist/plyr.css'
  8. import { useInterval, useIntervalFn } from '@vueuse/core'
  9. import { useRoute, useRouter } from 'vue-router'
  10. import request from '@/helpers/request'
  11. import qs from 'query-string'
  12. export default defineComponent({
  13. name: 'pre-register',
  14. setup() {
  15. const route = useRoute()
  16. const router = useRouter()
  17. const openId = sessionStorage.getItem('active-open-id')
  18. // 页面定时
  19. const pageTimer = useInterval(1000, { controls: true })
  20. pageTimer.pause()
  21. const forms = reactive({
  22. coverImg: '',
  23. introductionVideo: '',
  24. videoBrowsePoint: 0,
  25. saveId: route.query.saveId,
  26. orchestraId: route.query.id,
  27. openId: route.query.openId || openId,
  28. loading: false,
  29. player: null as any,
  30. intervalFnRef: null as any
  31. })
  32. // 播放视频总时长
  33. const videoIntervalRef = useInterval(1000, { controls: true })
  34. videoIntervalRef.pause()
  35. /**
  36. * 格式化视屏播放有效时间 - 合并区间
  37. * @param intervals [[], []]
  38. * @example [[4, 8],[0, 4],[10, 30]]
  39. * @returns [[0, 8], [10, 30]]
  40. */
  41. const formatEffectiveTime = (intervals: any[]) => {
  42. const res: any = []
  43. intervals.sort((a, b) => a[0] - b[0])
  44. let prev = intervals[0]
  45. for (let i = 1; i < intervals.length; i++) {
  46. const cur = intervals[i]
  47. if (prev[1] >= cur[0]) {
  48. // 有重合
  49. prev[1] = Math.max(cur[1], prev[1])
  50. } else {
  51. // 不重合,prev推入res数组
  52. res.push(prev)
  53. prev = cur // 更新 prev
  54. }
  55. }
  56. res.push(prev)
  57. return res
  58. }
  59. /**
  60. * 获取数据有效期
  61. * @param intervals [[], []]
  62. * @returns 0s
  63. */
  64. const formatTimer = (intervals: any[]) => {
  65. const afterIntervals = formatEffectiveTime(intervals)
  66. let time = 0
  67. afterIntervals.forEach((t: any) => {
  68. time += t[1] - t[0]
  69. })
  70. return time
  71. }
  72. /**
  73. * 视屏累计时长
  74. * 1、视屏开始播放时-开始计时
  75. * 2、视频暂停时暂停-停止计时
  76. * 3、视频加载时-停止计时
  77. * 4、视频倍数播放时,时间正常计时
  78. * 5、点击视频进度或拖动进度时,时间暂停
  79. */
  80. const _init = () => {
  81. const controls = [
  82. 'play-large',
  83. 'play',
  84. 'progress',
  85. 'captions',
  86. 'current-time',
  87. 'duration',
  88. 'settings',
  89. 'fullscreen'
  90. ]
  91. const params: any = {
  92. controls: controls,
  93. settings: ['speed'],
  94. speed: { selected: 1, options: [0.5, 1, 1.5, 2] },
  95. i18n: {
  96. speed: '速度',
  97. normal: '默认'
  98. },
  99. invertTime: false
  100. }
  101. if (browser().iPhone) {
  102. params.fullscreen = {
  103. enabled: true,
  104. fallback: 'force',
  105. iosNative: true
  106. }
  107. }
  108. forms.player = new Plyr('#register-video', params)
  109. forms.player.on('loadedmetadata', () => {
  110. console.log('loadedmetadata')
  111. forms.loading = false
  112. forms.player.currentTime = forms.videoBrowsePoint
  113. })
  114. // forms.player.on('seeking', (val: any) => {
  115. // console.log('seeking')
  116. // })
  117. // // // 拖动结束时
  118. // forms.player.on('seeked', (val: any) => {
  119. // console.log('seeked')
  120. // })
  121. // 正在搜索中
  122. forms.player.on('waiting', () => {
  123. console.log('waiting')
  124. videoIntervalRef.pause()
  125. })
  126. // 如何视频在缓存不会触发
  127. forms.player.on('timeupdate', () => {
  128. console.log('timeupdate', forms.player.currentTime)
  129. // 判断视频计时器是否暂停,如果暂停则恢复
  130. // 添加 「forms.player.playing」 是由会跳转到上次播放时间,会触发些方法
  131. if (
  132. !videoIntervalRef.isActive.value &&
  133. forms.player.currentTime > 0 &&
  134. forms.player.playing
  135. ) {
  136. videoIntervalRef.resume()
  137. }
  138. })
  139. // 开始播放
  140. forms.player.on('play', () => {
  141. // 判断视频计时器是否暂停,如果暂停则恢复
  142. if (!videoIntervalRef.isActive.value) {
  143. videoIntervalRef.resume()
  144. }
  145. })
  146. // 暂停播放
  147. forms.player.on('pause', () => {
  148. console.log('pause')
  149. videoIntervalRef.pause()
  150. })
  151. forms.player.on('enterfullscreen', () => {
  152. console.log('fullscreen')
  153. const i = document.createElement('i')
  154. i.id = 'fullscreen-back'
  155. i.className = 'van-icon van-icon-arrow-left video-back'
  156. i.addEventListener('click', () => {
  157. forms.player.fullscreen.exit()
  158. })
  159. console.log(document.getElementsByClassName('plyr'))
  160. document.getElementsByClassName('plyr')[0].appendChild(i)
  161. })
  162. forms.player.on('exitfullscreen', () => {
  163. console.log('exitfullscreen')
  164. const i = document.getElementById('fullscreen-back')
  165. i && i.remove()
  166. })
  167. }
  168. // 保存零时时间
  169. const moreTime: any = ref([]) // 多个观看时间段
  170. let tempTime: any = [] // 临时存储时间
  171. // 监听播放状态,
  172. watch(
  173. () => videoIntervalRef.isActive.value,
  174. (newVal: boolean) => {
  175. // console.log(newVal, 'videoIntervalRef.isActive.value in')
  176. // console.log('watch', forms.player.currentTime)
  177. // console.log('保留两个小数:', forms.player.currentTime.toFixed(2))
  178. // console.log('向下取整:', Math.floor(forms.player.currentTime))
  179. // console.log('向上取整:', Math.ceil(forms.player.currentTime))
  180. // console.log('四舍五入:', Math.round(forms.player.currentTime))
  181. if (newVal) {
  182. tempTime[0] = Math.round(forms.player.currentTime)
  183. } else {
  184. tempTime[1] = Math.round(forms.player.currentTime)
  185. }
  186. console.log(tempTime.length, 'tempTime.length')
  187. if (tempTime.length >= 2) {
  188. moreTime.value.push(tempTime)
  189. tempTime = []
  190. }
  191. // console.log('观看的时间', moreTime)
  192. }
  193. )
  194. // 更新时间
  195. const updateStat = async (pageBrowseTime = 10) => {
  196. try {
  197. const videoBrowseData = moreTime.value.length > 0 ? formatEffectiveTime(moreTime.value) : []
  198. const time = moreTime.value.length > 0 ? formatTimer(moreTime.value) : 0
  199. const rate = Math.floor((time / forms.player.duration) * 100)
  200. await request.post('/api-student/open/studentBrowseRecord/updateStat', {
  201. data: {
  202. id: forms.saveId,
  203. pageBrowseTime, // 固定10秒
  204. videoBrowseData: JSON.stringify(videoBrowseData), // 视屏播放数据
  205. videoBrowseDataTime: time || 0, // 视屏观看百分比时长
  206. videoBrowsePercentage: rate || 0, // 视频观看百分比
  207. videoBrowseTime: videoIntervalRef?.counter.value, // 视频观看时长
  208. videoBrowsePoint: Math.floor(forms.player.currentTime || 0) // 视频最后观看点 - 向下取整
  209. }
  210. })
  211. } catch {
  212. //
  213. }
  214. }
  215. // 提交
  216. const onSubmit = async () => {
  217. try {
  218. // 暂停回调
  219. forms.intervalFnRef?.pause()
  220. // 页面计时暂停
  221. pageTimer.pause()
  222. await updateStat()
  223. window.location.href =
  224. window.location.origin +
  225. window.location.pathname +
  226. '/project/preRegister.html?' +
  227. qs.stringify({
  228. orchestraId: forms.orchestraId,
  229. openId: forms.openId
  230. })
  231. // window.location.href =
  232. // window.location.origin +
  233. // '/project/preRegister.html?' +
  234. // qs.stringify({
  235. // orchestraId: forms.orchestraId,
  236. // openId: forms.openId
  237. // })
  238. } catch (e) {
  239. console.log(e, 'e')
  240. // 还原
  241. forms.intervalFnRef?.resume()
  242. pageTimer.resume()
  243. }
  244. }
  245. onMounted(async () => {
  246. try {
  247. const { data } = await request.get('/api-student//open/studentBrowseRecord/query', {
  248. params: {
  249. openId: forms.openId,
  250. orchestraId: forms.orchestraId
  251. }
  252. })
  253. forms.videoBrowsePoint = data.videoBrowsePoint || 0
  254. if (forms.player) {
  255. forms.player.currentTime = data.videoBrowsePoint || 0
  256. }
  257. forms.introductionVideo = data.introductionVideo
  258. forms.coverImg = data.coverImg
  259. console.log(data)
  260. moreTime.value = data.videoBrowseData ? JSON.parse(data.videoBrowseData) : []
  261. _init()
  262. // 间隔多少时间同步数据
  263. forms.intervalFnRef = useIntervalFn(async () => {
  264. // 页面时间恢复
  265. pageTimer.counter.value = 0
  266. pageTimer.resume()
  267. await updateStat()
  268. videoIntervalRef.counter.value = 0
  269. }, 10000)
  270. } catch {
  271. //
  272. }
  273. })
  274. // 判断是否有openId
  275. if (!forms.openId) {
  276. router.replace({
  277. path: '/pre-register-video',
  278. query: {
  279. id: forms.orchestraId
  280. }
  281. })
  282. }
  283. return () => (
  284. <div class={styles['pre-register-video']}>
  285. <div class={styles.videoContainer}>
  286. <div class={styles['video-content']}>
  287. <video
  288. id="register-video"
  289. class={styles['video']}
  290. src={forms.introductionVideo + '?time' + Date.now()}
  291. playsinline={true}
  292. poster={forms.coverImg}
  293. preload="auto"
  294. ></video>
  295. {/* 加载视频使用 */}
  296. {forms.loading && (
  297. <div class={styles.loadingVideo}>
  298. <Loading
  299. size={36}
  300. color="#FF8057"
  301. vertical
  302. style={{ height: '100%', justifyContent: 'center' }}
  303. >
  304. 加载中...
  305. </Loading>
  306. </div>
  307. )}
  308. </div>
  309. </div>
  310. <div class={styles.messageContainer}>
  311. <div class={styles.messageContent}>
  312. <p>家长您好!</p>
  313. <p class={styles.c1}>
  314. 请家长们合理安排时间,<span>认真观看</span>家长会内容。在<span>详细了解</span>
  315. 所有要求后,有意向让孩子加入乐团的家长,请在<span>明晚20:00前</span>,为孩子完成
  316. <span>乐团报名</span>
  317. </p>
  318. <p class={styles.c1}>
  319. 下周,专业老师将针对意向入团学员进行身体条件确认。谢谢各位的支持!
  320. </p>
  321. <p class={styles.bottom}>
  322. 注:乐团于下学期正式开始训练,训练时间下 学期开学前另行通知,训练时间会与学校其他
  323. 社团错开,家长无需担心时间冲突问题。
  324. </p>
  325. </div>
  326. <Button class={styles.submitBtn} onClick={onSubmit}></Button>
  327. </div>
  328. </div>
  329. )
  330. }
  331. })