video.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. import { defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
  2. import styles from './video.module.less'
  3. import { Button, Loading } from 'vant'
  4. import { browser } from '@/helpers/utils'
  5. import Plyr from 'plyr'
  6. import 'plyr/dist/plyr.css'
  7. import { useInterval, useIntervalFn } from '@vueuse/core'
  8. import { useRoute, useRouter } from 'vue-router'
  9. import request from '@/helpers/request'
  10. import qs from 'query-string'
  11. import { usePageVisibility } from '@vant/use'
  12. export default defineComponent({
  13. name: 'pre-register',
  14. setup() {
  15. const route = useRoute()
  16. const router = useRouter()
  17. const pageVisibility = usePageVisibility()
  18. const openId = sessionStorage.getItem('active-open-id')
  19. // 页面定时
  20. const pageTimer = useInterval(1000, { controls: true })
  21. pageTimer.pause()
  22. const forms = reactive({
  23. coverImg: '',
  24. introductionVideo: '',
  25. introductionVideoTime: 0, // 视频总时长
  26. videoBrowsePoint: 0, // 视频最后观看点
  27. saveId: route.query.saveId,
  28. orchestraId: route.query.id,
  29. openId: route.query.openId || openId,
  30. loading: false,
  31. player: null as any,
  32. playerSpeed: 1,
  33. intervalFnRef: null as any,
  34. videoDetails: [] as any, // 节点列表
  35. pointVideo: {} as any, // 需要处理有效的时间段
  36. pointVideoTime: 0, // 有效时长
  37. videoSelectId: null, // 选中的编号
  38. isPageHide: false, // 处理页面返回没有刷新的问题
  39. parentConferencesNotes: '',
  40. orchestraRegisterType: '',
  41. status: ''
  42. })
  43. // 播放视频总时长
  44. const videoIntervalRef = useInterval(1000, { controls: true })
  45. videoIntervalRef.pause()
  46. /**
  47. * 格式化视屏播放有效时间 - 合并区间
  48. * @param intervals [[], []]
  49. * @example [[4, 8],[0, 4],[10, 30]]
  50. * @returns [[0, 8], [10, 30]]
  51. */
  52. const formatEffectiveTime = (intervals: any[]) => {
  53. const res: any = []
  54. intervals.sort((a, b) => a[0] - b[0])
  55. let prev = intervals[0]
  56. for (let i = 1; i < intervals.length; i++) {
  57. const cur = intervals[i]
  58. if (prev[1] >= cur[0]) {
  59. // 有重合
  60. prev[1] = Math.max(cur[1], prev[1])
  61. } else {
  62. // 不重合,prev推入res数组
  63. res.push(prev)
  64. prev = cur // 更新 prev
  65. }
  66. }
  67. res.push(prev)
  68. // console.log(res, 'formatEffectiveTime')
  69. return formatEffectiveTimeToAfter(res)
  70. }
  71. const formatEffectiveTimeToAfter = (res: any[]) => {
  72. // 格式化有效时间
  73. const effective: any = []
  74. const startNode = forms.pointVideo.startNode
  75. const endNode = forms.pointVideo.endNode
  76. // console.log(startNode, endNode, 'startNode')
  77. res.forEach((item: any) => {
  78. // 开始时间大于 设置时间
  79. if (item[1] >= item[0]) {
  80. /**
  81. * 1、开始时间
  82. */
  83. if (item[0] >= startNode && item[0] <= endNode && item[1] <= endNode) {
  84. // console.log(1)
  85. effective.push(item)
  86. }
  87. if (item[0] >= startNode && item[0] <= endNode && item[1] > endNode) {
  88. // console.log(3)
  89. effective.push([item[0], endNode])
  90. }
  91. if (item[0] < startNode && item[1] > startNode && item[1] <= endNode) {
  92. // console.log(4)
  93. effective.push([startNode, item[1]])
  94. }
  95. if (item[0] < startNode && item[1] > startNode && item[1] > endNode) {
  96. // console.log(4)
  97. effective.push([startNode, endNode])
  98. }
  99. }
  100. })
  101. // console.log(effective, 'effective')
  102. return effective
  103. }
  104. /**
  105. * 获取数据有效期
  106. * @param intervals [[], []]
  107. * @returns 0s
  108. */
  109. const formatTimer = (intervals: any[]) => {
  110. const afterIntervals = formatEffectiveTime(intervals)
  111. // console.log(afterIntervals, 'afterIntervals')
  112. let time = 0
  113. afterIntervals.forEach((t: any) => {
  114. time += t[1] - t[0]
  115. })
  116. return time
  117. }
  118. const checkVideoDetails = (time: number) => {
  119. forms.videoDetails.forEach((item: any) => {
  120. if (item.startNode <= time && time <= item.endNode) {
  121. forms.videoSelectId = item.id
  122. }
  123. })
  124. }
  125. /**
  126. * 视屏累计时长
  127. * 1、视屏开始播放时-开始计时
  128. * 2、视频暂停时暂停-停止计时
  129. * 3、视频加载时-停止计时
  130. * 4、视频倍数播放时,时间正常计时
  131. * 5、点击视频进度或拖动进度时,时间暂停
  132. */
  133. const _init = () => {
  134. const controls = [
  135. 'play-large',
  136. 'play',
  137. 'progress',
  138. 'captions',
  139. 'current-time',
  140. 'duration',
  141. 'settings',
  142. 'fullscreen'
  143. ]
  144. const params: any = {
  145. controls: controls,
  146. settings: ['speed'],
  147. speed: { selected: 1, options: [0.5, 1, 1.5, 2] },
  148. i18n: {
  149. speed: '速度',
  150. normal: '默认'
  151. },
  152. autoplay: false,
  153. invertTime: false
  154. }
  155. if (browser().iPhone) {
  156. params.fullscreen = {
  157. enabled: true,
  158. fallback: 'force',
  159. iosNative: true
  160. }
  161. }
  162. const times: any = []
  163. forms.videoDetails.forEach((item: any) => {
  164. times.push({
  165. time: item.startNode,
  166. label: item.desc
  167. })
  168. })
  169. params.markers = { enabled: true, points: times }
  170. forms.player = new Plyr('#register-video', params)
  171. forms.player.on('ready', (item: any) => {
  172. // console.log('ready', item)
  173. // forms.player.pause()
  174. })
  175. forms.player.on('loadedmetadata', () => {
  176. console.log('loadedmetadata')
  177. forms.loading = false
  178. forms.player.currentTime = forms.videoBrowsePoint
  179. checkVideoDetails(forms.player.currentTime)
  180. })
  181. // 速度变化时
  182. forms.player.on('ratechange', () => {
  183. forms.playerSpeed =
  184. forms.playerSpeed < forms.player.speed ? forms.player.speed : forms.playerSpeed
  185. })
  186. forms.player.on('seeking', () => {
  187. console.log('seeking')
  188. videoIntervalRef.isActive.value && videoIntervalRef.pause()
  189. })
  190. // // 拖动结束时
  191. forms.player.on('seeked', () => {
  192. console.log('seeked')
  193. videoIntervalRef.isActive.value && videoIntervalRef.pause()
  194. })
  195. // 正在搜索中
  196. forms.player.on('waiting', () => {
  197. // console.log('waiting pause')
  198. videoIntervalRef.isActive.value && videoIntervalRef.pause()
  199. })
  200. // 如何视频在缓存不会触发
  201. forms.player.on('timeupdate', () => {
  202. // console.log('timeupdate', forms.player.currentTime)
  203. console.log(videoIntervalRef.isActive.value, 'timeupdate')
  204. // 时间变化时更新每一段的状态
  205. checkVideoDetails(forms.player.currentTime)
  206. // 判断视频计时器是否暂停,如果暂停则恢复
  207. // 添加 「forms.player.playing」 是由会跳转到上次播放时间,会触发些方法
  208. if (
  209. !videoIntervalRef.isActive.value &&
  210. forms.player.currentTime > 0 &&
  211. forms.player.playing
  212. ) {
  213. // console.log('timeupdate play')
  214. videoIntervalRef.resume()
  215. }
  216. })
  217. // 视屏播放时暂停
  218. forms.player.on('ended', () => {
  219. forms.player.pause()
  220. console.log(videoIntervalRef.isActive.value, 'ended')
  221. })
  222. // 开始播放
  223. forms.player.on('play', () => {
  224. console.log('play')
  225. // 判断视频计时器是否暂停,如果暂停则恢复
  226. videoIntervalRef.resume()
  227. })
  228. // 暂停播放
  229. forms.player.on('pause', () => {
  230. console.log('pause', videoIntervalRef.isActive.value)
  231. videoIntervalRef.pause()
  232. })
  233. forms.player.on('enterfullscreen', () => {
  234. console.log('fullscreen')
  235. const i = document.createElement('i')
  236. i.id = 'fullscreen-back'
  237. i.className = 'van-icon van-icon-arrow-left video-back'
  238. i.addEventListener('click', () => {
  239. forms.player.fullscreen.exit()
  240. })
  241. // console.log(document.getElementsByClassName('plyr'))
  242. document.getElementsByClassName('plyr')[0].appendChild(i)
  243. })
  244. forms.player.on('exitfullscreen', () => {
  245. console.log('exitfullscreen')
  246. const i = document.getElementById('fullscreen-back')
  247. i && i.remove()
  248. })
  249. checkVideoDetails(0)
  250. }
  251. // 保存零时时间
  252. const moreTime: any = ref([]) // 多个观看时间段
  253. let tempTime: any = [] // 临时存储时间
  254. const currentTimer = useInterval(1000, { controls: true })
  255. // 监听播放状态,
  256. watch(
  257. () => videoIntervalRef.isActive.value,
  258. (newVal: boolean) => {
  259. initVideoCount(newVal)
  260. }
  261. )
  262. const initVideoCount = (newVal: any) => {
  263. console.log(newVal, 'videoIntervalRef.isActive.value in')
  264. // console.log('watch', forms.player.currentTime)
  265. // console.log('保留两个小数:', forms.player.currentTime.toFixed(2))
  266. // console.log('向下取整:', Math.floor(forms.player.currentTime))
  267. // console.log('向上取整:', Math.ceil(forms.player.currentTime))
  268. // console.log('四舍五入:', Math.round(forms.player.currentTime))
  269. if (newVal) {
  270. tempTime[0] = Math.floor(forms.player.currentTime)
  271. } else {
  272. tempTime[1] = Math.floor(forms.player.currentTime)
  273. }
  274. // console.log(forms.player.speed, 'speed')
  275. if (tempTime.length >= 2) {
  276. // console.log(tempTime, 'tempTime', moreTime.value)
  277. // 处理在短时间内的时间差 【视屏拖动,点击可能会导致时间差太大】
  278. const diffTime =
  279. tempTime[1] - tempTime[0] - currentTimer.counter.value * forms.playerSpeed > 2
  280. // console.log(diffTime, 'diffTime', currentTimer.counter.value, 'value')
  281. // 结束时间,如果 大于开始时间则清除
  282. if (tempTime[1] >= tempTime[0] && !diffTime) moreTime.value.push(tempTime)
  283. tempTime = []
  284. currentTimer.counter.value = 0
  285. }
  286. // console.log('观看的时间', moreTime)
  287. }
  288. watch(pageVisibility, (value: any) => {
  289. console.log('watch', value)
  290. if (value == 'hidden') {
  291. forms.player.pause()
  292. }
  293. })
  294. // 更新时间
  295. const updateStat = async (pageBrowseTime = 10) => {
  296. try {
  297. const videoBrowseData = moreTime.value.length > 0 ? formatEffectiveTime(moreTime.value) : []
  298. // console.log(moreTime.value, videoBrowseData, 'video')
  299. const time = videoBrowseData.length > 0 ? formatTimer(videoBrowseData) : 0
  300. // const videoCountTime = videoIntervalRef?.counter.value
  301. // 判断如何视屏播放时间大于视屏播放有效时间则说明数据有问题,进行重置数据
  302. const rate = Math.floor((time / Math.floor(forms.pointVideoTime)) * 100)
  303. // console.log('videoIntervalRef?.counter.value', videoIntervalRef?.counter.value)
  304. await request.post('/api-student/open/studentBrowseRecord/updateStat', {
  305. data: {
  306. id: forms.saveId,
  307. pageBrowseTime, // 固定10秒
  308. videoBrowseData: JSON.stringify(videoBrowseData), // 视屏播放数据
  309. videoBrowseDataTime: time || 0, // 有效的视频观看时长
  310. videoBrowsePercentage: rate || 0, // 有效的视频观看时长百分比
  311. videoBrowseTime: videoIntervalRef?.counter.value, // 视频观看时长
  312. videoBrowsePoint: Math.floor(forms.player.currentTime || 0) // 视频最后观看点 - 向下取整
  313. }
  314. })
  315. } catch {
  316. //
  317. }
  318. }
  319. // 提交
  320. const onSubmit = async () => {
  321. try {
  322. forms.player.pause() // 视屏
  323. forms.intervalFnRef?.pause() // 页面订时器
  324. currentTimer.pause()
  325. videoIntervalRef.pause()
  326. // 页面计时暂停
  327. pageTimer.pause()
  328. initVideoCount(videoIntervalRef.isActive.value)
  329. await updateStat()
  330. console.log(forms.orchestraRegisterType)
  331. if (forms.orchestraRegisterType === 'PARENT_CONFERENCES') {
  332. window.location.href =
  333. window.location.origin +
  334. window.location.pathname +
  335. `/#/preApply?id=${forms.orchestraId}`
  336. } else if (forms.orchestraRegisterType === 'GROUP_BUY') {
  337. window.location.href =
  338. window.location.origin +
  339. window.location.pathname +
  340. `/#/preGoodsApply?id=${forms.orchestraId}`
  341. } else {
  342. window.location.href =
  343. window.location.origin +
  344. window.location.pathname +
  345. '/project/preRegister.html?' +
  346. qs.stringify({
  347. orchestraId: forms.orchestraId,
  348. openId: forms.openId
  349. })
  350. }
  351. } catch (e) {
  352. console.log(e, 'e')
  353. // 还原
  354. forms.intervalFnRef?.resume()
  355. pageTimer.resume()
  356. currentTimer.resume()
  357. }
  358. }
  359. onMounted(async () => {
  360. try {
  361. const { data } = await request.get('/api-student/open/studentBrowseRecord/query', {
  362. params: {
  363. openId: forms.openId,
  364. orchestraId: forms.orchestraId
  365. }
  366. })
  367. forms.videoBrowsePoint = data.videoBrowsePoint || 0
  368. if (forms.player) {
  369. forms.player.currentTime = data.videoBrowsePoint || 0
  370. }
  371. forms.introductionVideo = data.introductionVideo
  372. forms.introductionVideoTime = data.introductionVideoTime
  373. forms.coverImg = data.coverImg
  374. moreTime.value = data.videoBrowseData ? JSON.parse(data.videoBrowseData) : []
  375. forms.parentConferencesNotes = data.parentConferencesNotes
  376. forms.orchestraRegisterType = data.orchestraRegisterType
  377. const videoDetails = data.videoDetails || []
  378. videoDetails.forEach((video: any) => {
  379. forms.videoDetails.push({
  380. startNode: video.startNode,
  381. endNode: video.endNode,
  382. desc: video.desc,
  383. id: video.id
  384. })
  385. if (video.pointFlag) {
  386. forms.pointVideo = video
  387. forms.pointVideoTime = video.endNode - video.startNode
  388. }
  389. })
  390. _init()
  391. // 间隔多少时间同步数据
  392. forms.intervalFnRef = useIntervalFn(async () => {
  393. // 页面时间恢复
  394. pageTimer.counter.value = 0
  395. pageTimer.resume()
  396. await updateStat()
  397. videoIntervalRef.counter.value = 0
  398. }, 10000)
  399. // const arr = [
  400. // [10, 10],
  401. // [53, 53],
  402. // [64, 64],
  403. // [74, 74],
  404. // [155, 155],
  405. // [173, 173],
  406. // [183, 183],
  407. // [191, 201]
  408. // ]
  409. // console.log(formatEffectiveTime(arr))
  410. } catch {
  411. //
  412. }
  413. })
  414. onUnmounted(() => {
  415. forms.player?.fullscreen.exit()
  416. forms.intervalFnRef?.pause()
  417. currentTimer.pause()
  418. // 页面计时暂停
  419. pageTimer.pause()
  420. })
  421. // 判断是否有openId
  422. if (!forms.openId) {
  423. router.replace({
  424. path: '/pre-register-video',
  425. query: {
  426. id: forms.orchestraId
  427. }
  428. })
  429. }
  430. const onPageShow = () => {
  431. // console.log(forms.isPageHide, 'showInfo')
  432. if (forms.isPageHide) {
  433. window.location.reload()
  434. }
  435. }
  436. // 处理监听页面返回不刷新的问题
  437. window.addEventListener('pageshow', onPageShow)
  438. const onPageHide = () => {
  439. // console.log(forms.isPageHide, 'showInfo')
  440. forms.isPageHide = true
  441. }
  442. window.addEventListener('pagehide', onPageHide)
  443. onUnmounted(() => {
  444. window.removeEventListener('pageshow', onPageShow)
  445. window.removeEventListener('pagehide', onPageHide)
  446. })
  447. return () => (
  448. <div class={styles['pre-register-video']}>
  449. <div class={styles.videoContainer}>
  450. <div class={styles['video-content']}>
  451. <video
  452. id="register-video"
  453. class={styles['video']}
  454. src={forms.introductionVideo}
  455. // src={
  456. // 'https://cloud-coach.ks3-cn-beijing.ksyuncs.com/1684981545808.mp4?time' + Date.now()
  457. // }
  458. playsinline={true}
  459. poster={forms.coverImg}
  460. preload="auto"
  461. ></video>
  462. {/* 加载视频使用 */}
  463. {forms.loading && (
  464. <div class={styles.loadingVideo}>
  465. <Loading
  466. size={36}
  467. color="#FF8057"
  468. vertical
  469. style={{ height: '100%', justifyContent: 'center' }}
  470. >
  471. 加载中...
  472. </Loading>
  473. </div>
  474. )}
  475. </div>
  476. </div>
  477. <div class={styles.videoCount}>
  478. <div class={styles.videoTitle}></div>
  479. <div class={styles.videoCountContent}>
  480. {forms.videoDetails.map((item: any) => (
  481. <span
  482. class={[item.id === forms.videoSelectId ? styles.active : '']}
  483. onClick={() => {
  484. forms.player.currentTime = item.startNode
  485. forms.player.play()
  486. forms.videoBrowsePoint = item.startNode
  487. checkVideoDetails(forms.player.currentTime)
  488. }}
  489. >
  490. {item.desc}
  491. </span>
  492. ))}
  493. </div>
  494. </div>
  495. <div class={styles.messageContainer}>
  496. <div class={styles.messageContent}>
  497. {/* <p>家长您好!</p>
  498. <p class={styles.c1}>
  499. 请家长们合理安排时间,<span>认真观看</span>家长会内容。在<span>详细了解</span>
  500. 所有要求后,有意向让孩子加入乐团的家长,请在<span>明晚20:00前</span>,为孩子完成
  501. <span>乐团报名</span>。
  502. </p>
  503. <p class={styles.c1}>
  504. 下周,专业老师将针对意向入团学员进行身体条件确认。谢谢各位的支持!
  505. </p>
  506. <p class={styles.bottom}>
  507. 注:乐团于下学期正式开始训练,训练时间下学期开学前另行通知,训练时间会与学校其他社团错开,家长无需担心时间冲突问题。
  508. </p> */}
  509. <div v-html={forms.parentConferencesNotes}></div>
  510. <Button class={styles.submitBtn} onClick={onSubmit}></Button>
  511. </div>
  512. </div>
  513. </div>
  514. )
  515. }
  516. })