index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import {
  2. Icon,
  3. showConfirmDialog,
  4. Slider,
  5. Swipe,
  6. SwipeItem
  7. } from 'vant'
  8. import {
  9. defineComponent,
  10. onMounted,
  11. reactive,
  12. onUnmounted,
  13. ref,
  14. watch,
  15. Transition,
  16. } from 'vue'
  17. import styles from './index.module.less'
  18. import 'plyr/dist/plyr.css'
  19. import request from '@/helpers/request'
  20. import { state } from '@/state'
  21. import { useRoute, useRouter } from 'vue-router'
  22. import iconBack from '../coursewarePlay/image/back.svg'
  23. import { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
  24. import iconLoop from '../coursewarePlay/image/icon-loop.svg'
  25. import iconLoopActive from '../coursewarePlay/image/icon-loop-active.svg'
  26. import iconplay from '../coursewarePlay/image/icon-play.svg'
  27. import iconpause from '../coursewarePlay/image/icon-pause.svg'
  28. import iconVideobg from '../coursewarePlay/image/icon-videobg.png'
  29. import { browser, getSecondRPM } from '@/helpers/utils'
  30. import qs from 'query-string'
  31. const materialType = {
  32. 视频: 'VIDEO',
  33. 图片: 'IMG',
  34. 曲目: 'SONG'
  35. }
  36. export default defineComponent({
  37. name: 'exercise-after-class',
  38. setup() {
  39. const handleInit = (type = 0) => {
  40. // 横屏
  41. postMessage({
  42. api: 'setRequestedOrientation',
  43. content: {
  44. orientation: type
  45. }
  46. })
  47. // 头,包括返回箭头
  48. postMessage({
  49. api: 'setTitleBarVisibility',
  50. content: {
  51. status: type
  52. }
  53. })
  54. // 安卓的状态栏
  55. postMessage({
  56. api: 'setStatusBarVisibility',
  57. content: {
  58. isVisibility: type
  59. }
  60. })
  61. }
  62. handleInit()
  63. onUnmounted(() => {
  64. handleInit(1)
  65. })
  66. const route = useRoute()
  67. watch(
  68. () => route.query,
  69. () => {
  70. getDetail()
  71. trainingRecord()
  72. }
  73. )
  74. const router = useRouter()
  75. const query = route.query
  76. const browserInfo = browser()
  77. const headeRef = ref()
  78. const data = reactive({
  79. videoData: null as any,
  80. details: [] as any,
  81. trainingTimes: 0,
  82. itemList: [] as any,
  83. showHead: true,
  84. loading: true,
  85. recordLoading: false
  86. })
  87. const activeData = reactive({
  88. nowTime: 0,
  89. model: true, // 遮罩
  90. timer: null as any,
  91. item: null as any
  92. })
  93. const getDetail = async () => {
  94. data.itemList = []
  95. let details = []
  96. try {
  97. const res: any = await request.get(
  98. state.platformApi + `/lessonTraining/courseSchedule/${route.query.courseScheduleId}`
  99. )
  100. if (Array.isArray(res?.data)) {
  101. const studentLevel = state.user?.data?.studentLevel || 1
  102. details = res.data.find((n: any) => n.studentLevel === studentLevel)?.details || []
  103. }
  104. } catch (error) {
  105. console.log('error')
  106. }
  107. if (details.length) {
  108. data.details = details
  109. const videoData: any =
  110. details.find((n: any) => n.materialId == route.query.materialId) || {}
  111. try {
  112. videoData.training = JSON.parse(videoData?.lessonTrainingTemp?.trainingConfigJson)
  113. } catch (error) {}
  114. data.itemList.push({
  115. ...videoData,
  116. id: videoData.materialId,
  117. currentTime: 0,
  118. duration: 100,
  119. paused: true,
  120. loop: false,
  121. videoEle: null,
  122. timer: null,
  123. muted: true, // 静音
  124. autoplay: true, //自动播放
  125. })
  126. popupData.itemActive = videoData.id
  127. popupData.tabName = videoData.materialName
  128. data.videoData = videoData
  129. handleExerciseCompleted()
  130. }
  131. }
  132. const getTrainingTimes = (res: any) => {
  133. let trainingTimes = 0
  134. if (Array.isArray(res?.trainings)) {
  135. const train = res.trainings.find((n: any) => n.materialId === route.query?.materialId)
  136. if (train) {
  137. trainingTimes = train.trainingTimes
  138. }
  139. }
  140. data.trainingTimes = trainingTimes
  141. }
  142. // 获取课后练习记录
  143. const trainingRecord = async () => {
  144. try {
  145. const res: any = await request.post(
  146. state.platformApi +
  147. `/studentLessonTraining/trainingRecord/${query.courseScheduleId}?userId=${state.user?.data?.id}`
  148. )
  149. if (res?.data) {
  150. getTrainingTimes(res.data)
  151. handleExerciseCompleted()
  152. }
  153. } catch (error) {}
  154. }
  155. onMounted(() => {
  156. getDetail()
  157. trainingRecord()
  158. })
  159. // 返回
  160. const goback = () => {
  161. postMessage({ api: 'back' })
  162. }
  163. const swipeRef = ref()
  164. const popupData = reactive({
  165. firstIndex: 0,
  166. open: false,
  167. activeIndex: -1,
  168. tabActive: '',
  169. tabName: '',
  170. itemActive: '',
  171. itemName: ''
  172. })
  173. // 达到指标,记录
  174. const addTrainingRecord = async (m: any) => {
  175. if (data.recordLoading) return
  176. console.log('记录观看次数')
  177. data.recordLoading = true
  178. const query = route.query
  179. const body = {
  180. materialType: 'VIDEO',
  181. record: {
  182. sourceTime: m.duration,
  183. clientType: state.platformType,
  184. feature: 'LESSON_TRAINING',
  185. deviceType: browserInfo.android ? 'ANDROID' : browserInfo.isApp ? 'IOS' : 'WEB'
  186. },
  187. courseScheduleId: query.courseScheduleId,
  188. lessonTrainingId: query.lessonTrainingId,
  189. materialId: query.materialId
  190. }
  191. try {
  192. const res: any = await request.post(
  193. state.platformApi + '/studentLessonTraining/lessonTrainingRecord',
  194. {
  195. data: body
  196. }
  197. )
  198. trainingRecord()
  199. } catch (error) {}
  200. data.recordLoading = false
  201. }
  202. // 停止所有的播放
  203. const handleStopVideo = () => {
  204. data.itemList.forEach((m: any) => {
  205. m.videoEle?.pause()
  206. })
  207. }
  208. // 判断练习是否完成
  209. const handleExerciseCompleted = () => {
  210. if (
  211. data.trainingTimes != 0 &&
  212. data.trainingTimes == (data.videoData as any)?.training?.practiceTimes
  213. ) {
  214. handleStopVideo()
  215. const itemIndex = data.details.findIndex(
  216. (n: any) => n.materialId == data.videoData?.materialId
  217. )
  218. const isLastIndex = itemIndex === data.details.length - 1
  219. showConfirmDialog({
  220. title: '课后训练',
  221. message: '你已完成该练习~',
  222. confirmButtonColor: 'var(--van-primary)',
  223. confirmButtonText: isLastIndex ? '完成' : '下一题',
  224. cancelButtonText: '继续'
  225. })
  226. .then(() => {
  227. if (!isLastIndex) {
  228. const nextItem = data.details[itemIndex + 1]
  229. if (nextItem?.type === materialType.视频) {
  230. // console.log('下一题视频', data.details[itemIndex].materialId, nextItem.materialId)
  231. router.replace({
  232. path: '/exerciseAfterClass',
  233. query: {
  234. ...query,
  235. materialId: nextItem.materialId
  236. }
  237. })
  238. }
  239. if (nextItem?.type === materialType.曲目) {
  240. handleInit(1)
  241. goback()
  242. const parmas = qs.stringify({
  243. id: nextItem.content,
  244. courseScheduleId: query.courseScheduleId,
  245. lessonTrainingId: query.lessonTrainingId,
  246. materialId: nextItem.materialId
  247. })
  248. let src = `${location.origin}/orchestra-music-score/?` + parmas
  249. // console.log("🚀 ~ src", src)
  250. postMessage({
  251. api: 'openAccompanyWebView',
  252. content: {
  253. url: src,
  254. orientation: 0,
  255. isHideTitle: true,
  256. statusBarTextColor: false,
  257. isOpenLight: true
  258. }
  259. })
  260. }
  261. }
  262. })
  263. .catch(() => {
  264. data.details[itemIndex].currentTime = 0
  265. })
  266. }
  267. }
  268. return () => (
  269. <div class={styles.coursewarePlay}>
  270. <Swipe
  271. style={{ height: '100vh' }}
  272. ref={swipeRef}
  273. showIndicators={false}
  274. loop={false}
  275. vertical
  276. lazyRender={true}
  277. touchable={false}
  278. >
  279. {data.itemList.map((m: any, mIndex: number) => {
  280. return (
  281. <SwipeItem>
  282. <>
  283. <div
  284. class={styles.itemDiv}
  285. onClick={() => {
  286. clearTimeout(m.timer)
  287. activeData.model = !activeData.model
  288. }}
  289. >
  290. <video
  291. playsinline="false"
  292. preload="auto"
  293. class="player"
  294. poster={iconVideobg}
  295. data-vid={m.id}
  296. src={m.content}
  297. loop={m.loop}
  298. autoplay={m.autoplay}
  299. muted={m.muted}
  300. onLoadedmetadata={async (e: Event) => {
  301. const videoEle = e.target as unknown as HTMLVideoElement
  302. m.duration = videoEle.duration
  303. m.videoEle = videoEle
  304. }}
  305. onTimeupdate={(e: Event) => {
  306. const videoEle = e.target as unknown as HTMLVideoElement
  307. m.currentTime = videoEle.currentTime
  308. console.log("🚀 ~ videoEle.currentTime", videoEle.currentTime)
  309. }}
  310. onPlay={() => {
  311. // 播放
  312. m.paused = false
  313. if (m.muted){
  314. m.muted = false
  315. m.videoEle.pause()
  316. }
  317. }}
  318. onPause={() => {
  319. //暂停
  320. m.paused = true
  321. }}
  322. onEnded={() => addTrainingRecord(m)}
  323. >
  324. <source src={m.content} type="video/mp4" />
  325. </video>
  326. </div>
  327. <Transition name="bottom">
  328. {activeData.model && (
  329. <div class={styles.bottomFixedContainer}>
  330. <div class={styles.time}>
  331. <span>{getSecondRPM(m.currentTime)}</span>
  332. <span>{getSecondRPM(m.duration)}</span>
  333. </div>
  334. <div class={styles.slider}>
  335. {m.duration && (
  336. <Slider
  337. buttonSize={16}
  338. modelValue={m.currentTime}
  339. min={0}
  340. max={m.duration}
  341. />
  342. )}
  343. </div>
  344. <div class={styles.actions}>
  345. <div class={styles.actionBtn}>
  346. {m.paused ? (
  347. <img
  348. src={iconplay}
  349. onClick={(e: Event) => {
  350. clearTimeout(m.timer)
  351. m.videoEle?.play()
  352. m.paused = false
  353. m.timer = setTimeout(() => {
  354. activeData.model = false
  355. }, 4000)
  356. }}
  357. />
  358. ) : (
  359. <img
  360. src={iconpause}
  361. onClick={(e: Event) => {
  362. clearTimeout(m.timer)
  363. m.videoEle?.pause()
  364. m.paused = true
  365. }}
  366. />
  367. )}
  368. </div>
  369. </div>
  370. </div>
  371. )}
  372. </Transition>
  373. </>
  374. </SwipeItem>
  375. )
  376. })}
  377. </Swipe>
  378. <Transition name="top">
  379. {activeData.model && (
  380. <div class={styles.headerContainer} ref={headeRef}>
  381. <div class={styles.backBtn} onClick={() => goback()}>
  382. <Icon name={iconBack} />
  383. 返回
  384. </div>
  385. <div class={styles.menu}>{popupData.tabName}</div>
  386. <div class={styles.nums}>
  387. 练习次数:{data.trainingTimes}/
  388. {(data.videoData as any)?.training?.practiceTimes || 0}
  389. </div>
  390. </div>
  391. )}
  392. </Transition>
  393. </div>
  394. )
  395. }
  396. })