index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. import { Icon, Popup, showConfirmDialog, showToast, Swipe, SwipeItem } from 'vant'
  2. import { defineComponent, onMounted, reactive, onUnmounted, ref, Transition, watch } from 'vue'
  3. import styles from './index.module.less'
  4. import 'plyr/dist/plyr.css'
  5. import request from '@/helpers/request'
  6. import { state } from '@/state'
  7. import { useRoute, useRouter } from 'vue-router'
  8. import iconBack from '../coursewarePlay/image/back.png'
  9. import { postMessage } from '@/helpers/native-message'
  10. // import iconLoop from '../coursewarePlay/image/icon-loop.svg'
  11. // import iconLoopActive from '../coursewarePlay/image/icon-loop-active.svg'
  12. // import iconplay from '../coursewarePlay/image/icon-play.svg'
  13. // import iconpause from '../coursewarePlay/image/icon-pause.svg'
  14. // import iconGoPractice from '../coursewarePlay/image/icon-go-practice.svg'
  15. // import iconVideobg from '../coursewarePlay/image/icon-videobg.png'
  16. import { browser, getSecondRPM } from '@/helpers/utils'
  17. import qs from 'query-string'
  18. import { Vue3Lottie } from 'vue3-lottie'
  19. import playLoadData from '../coursewarePlay/datas/data.json'
  20. import { handleCheckVip } from '../hook/useFee'
  21. import VideoClass from './video-class'
  22. import item from '@/student/coupons/item'
  23. import { usePageVisibility } from '@vant/use'
  24. import CoursewareTips from '../coursewarePlay/component/courseware-tips'
  25. const materialType = {
  26. 视频: 'VIDEO',
  27. 图片: 'IMG',
  28. 曲目: 'SONG'
  29. }
  30. export default defineComponent({
  31. name: 'exercise-after-class',
  32. setup() {
  33. const pageVisibility = usePageVisibility()
  34. /** 设置播放容器 16:9 */
  35. const parentContainer = reactive({
  36. width: '100vw'
  37. })
  38. // const setContainer = () => {
  39. // const min = Math.min(screen.width, screen.height)
  40. // const max = Math.max(screen.width, screen.height)
  41. // const width = min * (16 / 9)
  42. // if (width > max) {
  43. // parentContainer.width = '100vw'
  44. // return
  45. // } else {
  46. // parentContainer.width = width + 'px'
  47. // }
  48. // }
  49. const handleInit = (type = 0) => {
  50. // setContainer()
  51. // 横屏
  52. postMessage({
  53. api: 'setRequestedOrientation',
  54. content: {
  55. orientation: type
  56. }
  57. })
  58. // 头,包括返回箭头
  59. // postMessage({
  60. // api: 'setTitleBarVisibility',
  61. // content: {
  62. // status: type
  63. // }
  64. // })
  65. // 安卓的状态栏
  66. postMessage({
  67. api: 'setStatusBarVisibility',
  68. content: {
  69. isVisibility: type
  70. }
  71. })
  72. }
  73. handleInit()
  74. onUnmounted(() => {
  75. handleInit(1)
  76. })
  77. const route = useRoute()
  78. const router = useRouter()
  79. const query = route.query
  80. const browserInfo = browser()
  81. const headeRef = ref()
  82. const data = reactive({
  83. videoData: null as any,
  84. trainings: [] as any[],
  85. expireTimeFlag: false, // 作业是否结束
  86. trainingTimes: 0,
  87. itemList: [] as any,
  88. showHead: true,
  89. loading: true,
  90. recordLoading: false,
  91. isPlayBaseStatus: true, // 初始状态是否播放完
  92. isPlayAll: true // 是否全部做完
  93. })
  94. const activeData = reactive({
  95. nowTime: 0,
  96. model: true, // 遮罩
  97. timer: null as any,
  98. item: null as any
  99. })
  100. const onTitleTip = (type: "phaseGoals" | "checkItem", text: string) => {
  101. console.log(type, text, 'text')
  102. handleStopVideo()
  103. popupData.pointOpen = true
  104. popupData.pointContent = text
  105. if(type === "checkItem") {
  106. popupData.pointTitle = '检查事项'
  107. } else if(type === "phaseGoals") {
  108. popupData.pointTitle = '阶段目标'
  109. }
  110. }
  111. // 获取课后练习记录
  112. const getTrainingRecord = async () => {
  113. try {
  114. const res: any = await request.post(
  115. state.platformApi +
  116. `/studentLessonTraining/trainingRecord/${query.courseScheduleId}?userId=${state.user?.data?.id}`,
  117. {
  118. hideLoading: true
  119. }
  120. )
  121. data.expireTimeFlag = res.data?.expireTimeFlag || false
  122. if (Array.isArray(res?.data?.trainings)) {
  123. const trainings = res?.data?.trainings || []
  124. const tempLessonTraining: any = []
  125. trainings.forEach((item: any) => {
  126. tempLessonTraining.push(...(item.studentLessonTrainingDetails || []))
  127. })
  128. // 没有播放完
  129. tempLessonTraining.forEach((item: any) => {
  130. let trainingContent: any = {}
  131. try {
  132. trainingContent = JSON.parse(item.trainingContent)
  133. } catch (error) {
  134. trainingContent = ''
  135. }
  136. if (trainingContent.practiceTimes !== item.trainingTimes + '') {
  137. data.isPlayAll = false
  138. }
  139. if (item.materialId == route.query.materialId) {
  140. popupData.tabName = item.knowledgePointName
  141. }
  142. })
  143. return tempLessonTraining
  144. }
  145. } catch (error) {}
  146. return []
  147. }
  148. const setRecord = async (trainings: any[]) => {
  149. if (Array.isArray(trainings)) {
  150. data.trainings = trainings.map((n: any) => {
  151. const materialRefs = n.materialRefs ? n.materialRefs : []
  152. const materialMusicId = materialRefs.length > 0 ? materialRefs[0].resourceId : null
  153. try {
  154. n.trainingContent = JSON.parse(n.trainingContent)
  155. } catch (error) {
  156. n.trainingContent = ''
  157. }
  158. return {
  159. ...n,
  160. materialMusicId,
  161. currentTime: 0,
  162. duration: 100,
  163. paused: true,
  164. loop: false,
  165. videoEle: null,
  166. timer: null,
  167. // muted: state.user.data?.vipMember ? false : true, // 静音
  168. muted: true,
  169. autoplay: state.user.data?.vipMember ? true : false //自动播放
  170. }
  171. })
  172. data.itemList = data.trainings.filter((n: any) => n.materialId == route.query.materialId)
  173. data.videoData = data.itemList[0]
  174. console.log(data.trainings, 'trainings', data.itemList)
  175. handleExerciseCompleted()
  176. }
  177. }
  178. onMounted(async () => {
  179. const trainings = await getTrainingRecord()
  180. // 初始化状态
  181. trainings.forEach((record: any) => {
  182. let trainingContent: any = {}
  183. try {
  184. trainingContent = JSON.parse(record.trainingContent)
  185. } catch (error) {
  186. trainingContent = ''
  187. }
  188. if (trainingContent.practiceTimes !== record.trainingTimes + '') {
  189. data.isPlayBaseStatus = false
  190. }
  191. })
  192. setRecord(trainings)
  193. handleCheckVip()
  194. console.log(activeData.model, data.itemList, 'itemList')
  195. })
  196. // 返回
  197. const goback = () => {
  198. postMessage({ api: 'back' })
  199. }
  200. const swipeRef = ref()
  201. const popupData = reactive({
  202. pointOpen: false,
  203. pointContent: "",
  204. pointTitle: "",
  205. firstIndex: 0,
  206. open: false,
  207. activeIndex: -1,
  208. tabActive: '',
  209. tabName: '',
  210. itemActive: '',
  211. itemName: ''
  212. })
  213. // 达到指标,记录
  214. const addTrainingRecord = async (m: any) => {
  215. if (data.recordLoading || data.expireTimeFlag) return
  216. console.log('记录观看次数')
  217. data.recordLoading = true
  218. const query = route.query
  219. const body = {
  220. materialType: 'VIDEO',
  221. record: {
  222. sourceTime: m.duration,
  223. clientType: state.platformType,
  224. feature: 'LESSON_TRAINING',
  225. deviceType: browserInfo.android ? 'ANDROID' : browserInfo.isApp ? 'IOS' : 'WEB'
  226. },
  227. courseScheduleId: query.courseScheduleId,
  228. lessonTrainingId: query.lessonTrainingId,
  229. materialId: data.videoData?.materialId || ''
  230. }
  231. try {
  232. const res: any = await request.post(
  233. state.platformApi + '/studentLessonTraining/lessonTrainingRecord',
  234. {
  235. data: body,
  236. hideLoading: true
  237. }
  238. )
  239. } catch (error) {}
  240. data.recordLoading = false
  241. try {
  242. const trainings: any[] = await getTrainingRecord()
  243. if (Array.isArray(trainings)) {
  244. const item = trainings.find((n: any) => n.materialId == data.videoData?.materialId)
  245. if (item) {
  246. data.videoData.trainingTimes = item.trainingTimes
  247. handleExerciseCompleted()
  248. }
  249. }
  250. } catch (error) {}
  251. }
  252. // 停止所有的播放
  253. const handleStopVideo = () => {
  254. data.itemList.forEach((m: any) => {
  255. m.videoEle?.pause()
  256. })
  257. }
  258. // 判断练习是否完成
  259. const handleExerciseCompleted = () => {
  260. if (
  261. data?.videoData?.trainingTimes != 0 &&
  262. data?.videoData?.trainingTimes + '' === data.videoData?.trainingContent?.practiceTimes
  263. ) {
  264. let isLastIndex = false
  265. let itemIndex = 0
  266. // console.log(data.isPlayBaseStatus, data.isPlayAll, data.trainings)
  267. if (data.isPlayBaseStatus) {
  268. itemIndex = data.trainings.findIndex(
  269. (n: any) => n.materialId == data.videoData?.materialId
  270. )
  271. isLastIndex = itemIndex === data.trainings.length - 1
  272. } else {
  273. let i = -1
  274. let status = true
  275. data.trainings.forEach((item: any, index: number) => {
  276. if (item.trainingContent.practiceTimes !== item.trainingTimes + '' && i === -1) {
  277. // console.log(i, item.trainingContent.practiceTimes, item.trainingTimes, index)
  278. i = index
  279. }
  280. if (item.trainingContent.practiceTimes !== item.trainingTimes + '') {
  281. status = false
  282. }
  283. })
  284. itemIndex = i != -1 ? i - 1 : -1
  285. // console.log(status)
  286. isLastIndex = status
  287. }
  288. showConfirmDialog({
  289. title: '课后作业',
  290. message: '你已完成该练习~',
  291. confirmButtonColor: 'var(--van-primary)',
  292. confirmButtonText: isLastIndex ? '完成' : '下一题',
  293. cancelButtonText: '继续'
  294. })
  295. .then(() => {
  296. if (!isLastIndex) {
  297. const nextItem = data.trainings[itemIndex + 1]
  298. data.videoData?.expired
  299. if (nextItem.expired) {
  300. showToast('该资源已过期')
  301. return
  302. }
  303. if (nextItem.knowledgePointName) {
  304. popupData.tabName = nextItem.knowledgePointName
  305. }
  306. if (nextItem?.type === materialType.视频) {
  307. data.itemList = [nextItem]
  308. data.videoData = nextItem
  309. handleExerciseCompleted()
  310. }
  311. if (nextItem?.type === materialType.曲目) {
  312. handleInit(1)
  313. goback()
  314. const parmas = qs.stringify({
  315. id: nextItem.content,
  316. courseScheduleId: query.courseScheduleId,
  317. lessonTrainingId: query.lessonTrainingId,
  318. materialId: nextItem.materialId
  319. })
  320. const src = `${location.origin}/orchestra-music-score/?` + parmas
  321. postMessage({
  322. api: 'openAccompanyWebView',
  323. content: {
  324. url: src,
  325. orientation: 0,
  326. c_orientation: 0,
  327. isHideTitle: true,
  328. statusBarTextColor: false,
  329. isOpenLight: true
  330. }
  331. })
  332. }
  333. } else {
  334. postMessage({ api: 'goBack' })
  335. }
  336. })
  337. .catch(() => {
  338. data.trainings[itemIndex].currentTime = 0
  339. })
  340. }
  341. }
  342. watch(pageVisibility, (value: any) => {
  343. handleStopVideo()
  344. if (value == 'visible') {
  345. // 横屏
  346. postMessage(
  347. {
  348. api: 'setRequestedOrientation',
  349. content: {
  350. orientation: 0
  351. }
  352. },
  353. () => {
  354. console.log(234)
  355. }
  356. )
  357. }
  358. })
  359. // 去练习
  360. const gotoPractice = (e: any) => {
  361. handleStopVideo()
  362. e.stopPropagation()
  363. const parmas = qs.stringify({
  364. id: data.videoData.materialMusicId
  365. })
  366. const src = `${location.origin}/orchestra-music-score/?` + parmas
  367. console.log(src, 'src')
  368. postMessage({
  369. api: 'openAccompanyWebView',
  370. content: {
  371. url: src,
  372. orientation: 0,
  373. c_orientation: 0,
  374. isHideTitle: true,
  375. statusBarTextColor: false,
  376. isOpenLight: true
  377. }
  378. })
  379. }
  380. return () => (
  381. <div class={styles.playContent}>
  382. <div class={styles.coursewarePlay} style={{ width: parentContainer.width }}>
  383. <Swipe
  384. style={{ height: '100%' }}
  385. ref={swipeRef}
  386. showIndicators={false}
  387. loop={false}
  388. vertical
  389. lazyRender={true}
  390. touchable={false}
  391. duration={0}
  392. >
  393. {data.itemList.map((m: any, mIndex: number) => {
  394. return (
  395. <SwipeItem>
  396. <>
  397. <VideoClass
  398. item={m}
  399. modal={activeData.model}
  400. onEnded={(m: any) => addTrainingRecord(m)}
  401. onChangeModal={(status: boolean) => {
  402. activeData.model = status
  403. }}
  404. />
  405. {m.muted && (
  406. <div class={styles.loadWrap}>
  407. <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
  408. </div>
  409. )}
  410. </>
  411. </SwipeItem>
  412. )
  413. })}
  414. </Swipe>
  415. <Transition name="top">
  416. {activeData.model && (
  417. <div class={styles.headerContainer} ref={headeRef}>
  418. <div class={styles.backBtn}>
  419. <Icon name={iconBack} onClick={() => goback()} />
  420. <div class={styles.titleSection}>
  421. <div class={styles.title}>{popupData.tabName}</div>
  422. <div class={styles.titleContent}>
  423. <p>{data.itemList[0]?.materialName}</p>
  424. {/* {data.detail?.lessonTargetDesc ? <span onClick={() => onTitleTip('phaseGoals', data.detail?.lessonTargetDesc)}>阶段目标</span>: ""} */}
  425. {data.itemList[0]?.checkItem ? <span onClick={() => onTitleTip('checkItem', data.itemList[0]?.checkItem)}>检查事项</span> : ""}
  426. </div>
  427. </div>
  428. </div>
  429. {/* 判断作业是否过期 */}
  430. {!data.expireTimeFlag && (
  431. <div class={styles.nums}>
  432. <div class={styles.timeLoad}></div>
  433. <div>
  434. 观看视频模仿并练习:{data.videoData?.trainingTimes || 0}/
  435. {data.videoData?.trainingContent?.practiceTimes || 0}
  436. </div>
  437. </div>
  438. )}
  439. </div>
  440. )}
  441. </Transition>
  442. {/* <Transition name="right"> */}
  443. {/* 学校端不显示按钮 */}
  444. {data.videoData?.materialMusicId &&
  445. state.platformType !== 'SCHOOL' &&
  446. !data.videoData?.expired && (
  447. <div
  448. class={[styles.goPractice, activeData.model ? '' : styles.hide]}
  449. onClick={gotoPractice}
  450. ></div>
  451. )}
  452. {/* // <div class={styles.btnGroup}>
  453. // <div class={styles.btnItem} onClick={gotoPractice}>
  454. // <img src={iconGoPractice} class={styles.btnImg} />
  455. // <span>去练习</span>
  456. // </div>
  457. // </div> */}
  458. {/* {item.value.materialMusicId && (
  459. <div
  460. class={[styles.goPractice, data.showBar ? '' : styles.hide]}
  461. onClick={gotoAccomany}
  462. ></div>
  463. )} */}
  464. {/* </Transition> */}
  465. </div>
  466. <Popup
  467. class={[styles.popup, styles.popupPoint]}
  468. round
  469. style={{ background: 'transparent !important' }}
  470. v-model:show={popupData.pointOpen}>
  471. <CoursewareTips show onClose={() => {
  472. popupData.pointOpen = false
  473. }} content={popupData.pointContent} titleName={popupData.pointTitle} />
  474. </Popup>
  475. </div>
  476. )
  477. }
  478. })