detail.uvue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <script setup lang='ts'>
  2. import Back from '@/components/back.uvue'
  3. import Loading from '@/components/loading.uvue'
  4. import { ref, onMounted, watchEffect, nextTick, onUnmounted } from 'vue'
  5. import { type SubjectCourseResult, fetchSubjectCourseApp, updateSubjectProgress } from '@/services/subject/course'
  6. import { router } from '@/.cool'
  7. const isLoading = ref(true)
  8. const showProgress = ref(true)
  9. const showControls = ref(true)
  10. const showVideo = ref(true)
  11. const data = ref({
  12. videoSrc: 'https://oss.xiaoxiongcode.com/course-1/animate/第1集_观察.mp4',
  13. webviewSrc: '',
  14. })
  15. const menuItems = [
  16. { id: 'watch', name: '看课', icon: 'https://oss.xiaoxiongcode.com/static/home/kanke.png' },
  17. // { id: 'practice', name: '练习', icon: 'https://oss.xiaoxiongcode.com/static/home/lianxi.png' },
  18. { id: 'experiment', name: '虚拟实验', icon: 'https://oss.xiaoxiongcode.com/static/home/shiyan.png' },
  19. // { id: 'diary', name: '科学日记', icon: 'https://oss.xiaoxiongcode.com/static/home/riji.png' }
  20. ]
  21. const menu2Items = [
  22. { id: '1', name: '观察', icon: 'https://oss.xiaoxiongcode.com/static/home/观察.png' },
  23. { id: '2', name: '学习', icon: 'https://oss.xiaoxiongcode.com/static/home/假设.png' },
  24. { id: '3', name: '假设', icon: 'https://oss.xiaoxiongcode.com/static/home/卡通扁平化功能图标设计.png' },
  25. { id: '4', name: '交互', icon: 'https://oss.xiaoxiongcode.com/static/home/总结.png' },
  26. { id: '5', name: '总结', icon: 'https://oss.xiaoxiongcode.com/static/home/拓展.png' },
  27. // { id: '6', name: '拓展', icon: 'https://oss.xiaoxiongcode.com/static/home/拓展.png' },
  28. ]
  29. //当前进度
  30. const progress = ref(1)
  31. const progress2 = ref(1)
  32. const progressStatus = ref(0)
  33. const recorderManager = ref<any>()
  34. const innerAudioContext = ref<any>()
  35. const voicePath = ref('')
  36. function initOnlibeRecord() {
  37. recorderManager.value = uni.getRecorderManager();
  38. innerAudioContext.value = uni.createInnerAudioContext();
  39. innerAudioContext.value.autoplay = true;
  40. recorderManager.value?.onStop(function (res) {
  41. console.log('recorder stop' + JSON.stringify(res));
  42. if (res.duration < 5000) {
  43. uni.showToast({
  44. title: '录音时间过短',
  45. icon: 'none'
  46. })
  47. return
  48. }
  49. voicePath.value = res.tempFilePath;
  50. });
  51. }
  52. const course = ref<SubjectCourseResult>()
  53. //通过路由参数获取课程id
  54. async function fetchCatalog() {
  55. course.value = await fetchSubjectCourseApp({ id: router.query().id })
  56. if (!course.value?.courseUserProgress) {
  57. await updateSubjectProgress({
  58. courseId: course.value?.id,
  59. mainProgress: 1,
  60. assistantProgress: 1,
  61. status: 0
  62. })
  63. progress.value = 1
  64. progress2.value = 1
  65. data.value.videoSrc = course.value?.detailItem?.observeVideoPath || ''
  66. data.value.webviewSrc = course.value?.detailItem?.observeAnimationPath || ''
  67. } else if (course.value?.courseUserProgress.status === 0) {
  68. // progress.value = 1
  69. // progress2.value = 1
  70. progress.value = course.value?.courseUserProgress.mainProgress
  71. progress2.value = course.value?.courseUserProgress.assistantProgress
  72. } else if (course.value?.courseUserProgress.status === 1) {
  73. progressStatus.value = 1
  74. progress.value = 1
  75. progress2.value = 1
  76. }
  77. }
  78. onMounted(async () => {
  79. await fetchCatalog()
  80. isLoading.value = false
  81. initOnlibeRecord()
  82. setTimeout(() => {
  83. showProgress.value = false
  84. isLoading.value = false
  85. }, 1000)
  86. })
  87. async function handleEnded() {
  88. if (progress2.value === 5) {
  89. if (progress.value === 2) {
  90. return
  91. }
  92. progress.value++
  93. } else {
  94. progress2.value++
  95. }
  96. if (progress.value === 2) {
  97. return
  98. }
  99. if (progress2.value === 5) {
  100. progress.value++
  101. }
  102. if (progressStatus.value === 1) {
  103. return
  104. }
  105. await updateSubjectProgress({
  106. courseId: course.value?.id,
  107. mainProgress: progress.value,
  108. assistantProgress: progress2.value,
  109. status: 0
  110. })
  111. // switch (progress2.value) {
  112. // case 1:
  113. // router.push({
  114. // path: "/pages/catalog/web-view",
  115. // query: {
  116. // src: data.value.webviewSrc,
  117. // progress: progress2.value,
  118. // }
  119. // });
  120. // break
  121. // case 2:
  122. // case 4:
  123. // case 5:
  124. // case 5:
  125. // progress2.value++
  126. // break
  127. // }
  128. }
  129. const status = ref('wait')
  130. onShow(async () => {
  131. isLoading.value = true
  132. var pages = getCurrentPages();
  133. const prevPage = pages[pages.length - 1];
  134. console.log('prevPage', prevPage)
  135. status.value = (prevPage as any)?.status || 'wait'
  136. if (status.value === 'test') {
  137. if (progress.value === 2) {
  138. progress2.value = 1
  139. progress.value = 1
  140. }
  141. (prevPage as any).status = 'wait'
  142. }
  143. if (status.value === 'success') {
  144. if (progress.value === 2) {
  145. if (progressStatus.value === 0) {
  146. await updateSubjectProgress({
  147. courseId: course.value?.id,
  148. mainProgress: 2,
  149. assistantProgress: 5,
  150. status: 1
  151. })
  152. }
  153. router.back()
  154. return
  155. }
  156. progress2.value++;
  157. (prevPage as any).status = 'wait'
  158. }
  159. isLoading.value = false
  160. })
  161. function handleControlsToggle(e) {
  162. console.log(e)
  163. showControls.value = e.detail.show
  164. }
  165. watchEffect(() => {
  166. if (progress.value === 1) {
  167. switch (progress2.value) {
  168. case 1:
  169. data.value.videoSrc = course.value?.detailItem?.observeVideoPath
  170. break
  171. case 2:
  172. data.value.videoSrc = course.value?.detailItem?.questionVideoPath
  173. break
  174. case 3:
  175. setTimeout(() => {
  176. playVoice(course.value?.detailItem?.assumeAnimationPath)
  177. }, 2000)
  178. // data.value.webviewSrc = course.value?.detailItem?.assumeAnimationPath || data.value.webviewSrc
  179. // router.push({
  180. // path: "/pages/catalog/web-view",
  181. // query: {
  182. // src: data.value.webviewSrc,
  183. // progress: progress2.value,
  184. // }
  185. // });
  186. break
  187. case 4:
  188. // data.value.videoSrc = course.value?.detailItem?.testVideoPath || data.value.videoSrc
  189. data.value.webviewSrc = course.value?.detailItem?.summaryVideoPath
  190. router.push({
  191. path: "/pages/catalog/web-view",
  192. query: {
  193. src: data.value.webviewSrc,
  194. progress: progress2.value,
  195. }
  196. });
  197. break
  198. case 5:
  199. data.value.videoSrc = course.value?.detailItem?.testVideoPath
  200. break
  201. case 6:
  202. data.value.videoSrc = course.value?.detailItem?.expandVideoPath
  203. break
  204. }
  205. } else if (progress.value === 2) {
  206. data.value.webviewSrc = course.value?.testItem?.animationPath
  207. router.push({
  208. path: "/pages/catalog/web-view",
  209. query: {
  210. src: data.value.webviewSrc,
  211. progress: progress2.value,
  212. }
  213. });
  214. }
  215. })
  216. const timers = ref(100)
  217. const timer = ref<any>()
  218. function handleTop() {
  219. progress2.value = 4
  220. console.log(progress2.value)
  221. }
  222. function startRecord() {
  223. console.log('开始录音');
  224. timer.value = setInterval(() => {
  225. timers.value--
  226. if (timers.value <= 0) {
  227. endRecord()
  228. }
  229. }, 800)
  230. recorderManager.value.start();
  231. }
  232. function endRecord() {
  233. clearInterval(timer.value)
  234. timer.value = null
  235. timers.value = 100
  236. recorderManager.value.stop();
  237. }
  238. function playVoice(val) {
  239. innerAudioContext.value.src = val;
  240. innerAudioContext.value.play();
  241. }
  242. onUnmounted(() => {
  243. clearInterval(timer.value)
  244. innerAudioContext.value.stop();
  245. })
  246. </script>
  247. <template>
  248. <Loading v-show="isLoading" />
  249. <cl-page v-show="!isLoading">
  250. <Back v-show="showProgress" />
  251. <view class="w-[64vw] h-[36vw] absolute top-1/2 z-[1] left-[7vw] translate50"
  252. :class="{ ' rounded-2xl p-[3px] bg-black mt-[25px] ': showProgress, 'video-container': !showProgress }">
  253. <video v-if="progress2 !== 3" id="video1" class="w-full h-full " :class="{ 'rounded-2xl': showProgress }"
  254. :src="data.videoSrc" :show-center-play-btn="false" :show-background-playback-button="false"
  255. :show-fullscreen-btn="false" :show-casting-button="false" autoplay @controlstoggle="handleControlsToggle"
  256. @ended="handleEnded">
  257. </video>
  258. <view v-else class="w-full h-full ">
  259. <img src="https://oss.xiaoxiongcode.com/static/home/assert_1.gif" alt="" class="w-full h-full object-cover" />
  260. <!-- 顶部提示文字 -->
  261. <view class="absolute text-[20px] top-[20vh] w-full flex flex-row items-center justify-center">
  262. <!-- <cl-icon name="notification-3-fill" color="#FCE762" :size="40"></cl-icon> -->
  263. <cl-image src="https://oss.xiaoxiongcode.com/static/home/guangbo.png" mode="heightFix" class="!h-[25px]"
  264. @tap="playVoice(course?.detailItem?.assumeAnimationPath)"></cl-image>
  265. <text class=" font-bold text-white">{{ course?.detailItem?.assumeAnimationJavascriptPath }}</text>
  266. </view>
  267. <!-- 居中麦克风按钮 -->
  268. <view class=" mic flex flex-col items-center w-[220px] h-[220px]">
  269. <view v-show="timer" class="mic">
  270. <cl-progress-circle :value="timers"></cl-progress-circle>
  271. </view>
  272. <view
  273. class="mic w-[120px] h-[120px] bg-[#3CB8FF] rounded-full flex items-center justify-center z-[2] border-[4px] border-white border-solid"
  274. @tap="startRecord" v-if="!timer">
  275. <cl-image src="https://oss.xiaoxiongcode.com/static/home/maikefeng.png" mode="heightFix"
  276. class="!h-[60px]"></cl-image>
  277. </view>
  278. <template v-else>
  279. <view class="mic w-[120px] h-[120px] bg-[#3CB8FF] rounded-full flex items-center justify-center z-[2]"
  280. @tap="endRecord">
  281. <view class="w-[50px] h-[50px] rounded-[5px] bg-white"></view>
  282. </view>
  283. </template>
  284. <!-- 指示手指 -->
  285. <view class="absolute bottom-[20px] right-[20px] animate-bounce pointer-events-none z-[2] !h-[60px] w-[100px]"
  286. v-if="!timer && !voicePath">
  287. <cl-image src="https://oss.xiaoxiongcode.com/static/home/shou.png" mode="heightFix" width="100%"
  288. class=""></cl-image>
  289. </view>
  290. </view>
  291. <!-- 下一步按钮 (录音完成后显示) -->
  292. <view v-show="voicePath" class="mic flex flex-row items-center justify-center gap-[200px]">
  293. <button
  294. class="bg-[#3CB8FF] text-white w-[80px] h-[80px] flex items-center justify-center rounded-full font-bold text-[25px] shadow-md active:opacity-80 border-[4px] border-white border-solid"
  295. @tap="playVoice(voicePath)">
  296. <cl-image src="https://oss.xiaoxiongcode.com/static/home/guangbo-bai.png" mode="heightFix"
  297. class="!h-[25px]"></cl-image>
  298. </button>
  299. <button
  300. class="bg-[#71C73D] text-white w-[80px] h-[80px] flex items-center justify-center rounded-full font-bold text-[25px] shadow-md active:opacity-80 border-[4px] border-white border-solid"
  301. @tap="handleTop">√</button>
  302. </view>
  303. </view>
  304. <view class="video-fullscreen_title" v-show="showControls && !showProgress">
  305. <Back />
  306. <view class="text-[20px] font-bold text-white">
  307. {{ course?.mainTitle }}
  308. </view>
  309. <view class="control-progress">
  310. <view class="before"></view>
  311. <view v-for="(item, i) in menu2Items" :key="item.id"
  312. class="px-0 py-3 flex items-center justify-center gap-[5px] relative z-[1]"
  313. :class="{ '!bg-[#fff] rounded-full': progress2 == i + 1 }" @tap="progress2 = i + 1">
  314. <cl-image :src="item.icon" mode="heightFix" class="!h-[28px] !w-[50px]"></cl-image>
  315. <text class="text-[12px] font-bold" :class="{ '!text-[#2BA0F3]': progress2 == i + 1 }">{{ item.name
  316. }}</text>
  317. </view>
  318. <view class="before"></view>
  319. </view>
  320. </view>
  321. </view>
  322. <view class="course-detail-page" :class="{ 'hidden': !showProgress }">
  323. <!-- 顶部标题栏 -->
  324. <view class="flex-[1] h-[100vh] relative">
  325. <!-- <view class="flex flex-row w-full pr-[3vw] pl-[10vw] items-center justify-between absolute top-[30px]">
  326. <text class="text-[5vh] font-bold">{{ course?.mainTitle }}</text>
  327. <text
  328. class="rounded-full p-[1vh] px-[2vh] border-2 border-[#fff] border-solid text-[#fff] text-[3vh] font-bold">课程收获</text>
  329. </view> -->
  330. </view>
  331. <!-- 右侧功能菜单 -->
  332. <view class="w-[25vw] h-[100vh] bg-[#5CBDFD] flex flex-col justify-center p-5">
  333. <view v-for="(item, i) in menuItems" :key="item.id" class="h-[20vh] flex flex-row items-center ">
  334. <view
  335. class="h-[15vh] w-[20vh] bg-[#999999] rounded-2xl border-[.5vh] border-[#254AD9] border-b-[1vh] border-solid flex items-center justify-center gap-[1vh]"
  336. :class="{ '!bg-[#fff]': progress >= i }">
  337. <cl-image :src="item.icon" mode="heightFix" class="!h-[7vh]"></cl-image>
  338. <text class="text-[2vh] font-bold">{{ item.name }}</text>
  339. </view>
  340. <view class="flex items-center h-[20vh] ml-[2vh] ">
  341. <view class="w-[1vh] bg-[#B4DBF7] flex-1"
  342. :class="{ '!bg-[#71C73D]': progress >= i, '!opacity-0': i === 0 }">
  343. </view>
  344. <view class="w-[5vh] bg-[#fff] h-[5vh] rounded-full" :class="{ '!bg-[#71C73D]': progress >= i }"></view>
  345. <view class="w-[1vh] bg-[#B4DBF7] flex-1"
  346. :class="{ '!bg-[#71C73D]': progress >= i, '!opacity-0': i === menuItems.length - 1 }"></view>
  347. </view>
  348. </view>
  349. </view>
  350. </view>
  351. </cl-page>
  352. </template>
  353. <style lang="scss" scoped>
  354. .translate50 {
  355. transform: translateY(-50%);
  356. }
  357. .course-detail-page {
  358. @apply flex flex-row items-center;
  359. background-color: #3498DB;
  360. height: 100vh;
  361. color: black;
  362. transition: all 1s ease-in-out;
  363. }
  364. .video-container {
  365. width: 100vw;
  366. height: 100vh;
  367. top: 0;
  368. left: 0;
  369. transform: translate(0, 0);
  370. animation: spin 2s linear 1;
  371. transition: all 2s ease-in-out;
  372. }
  373. @keyframes spin {
  374. 0% {
  375. opacity: 50%;
  376. }
  377. 100% {
  378. opacity: 1;
  379. }
  380. }
  381. .hidden {
  382. opacity: 0;
  383. transition: all 3s ease-in-out;
  384. }
  385. .video-fullscreen_title {
  386. @apply absolute top-3 left-0 pl-[80px] w-[100vw] z-10 flex items-center justify-between flex-row;
  387. color: #fff;
  388. .control-progress {
  389. @apply flex flex-row gap-[3px] relative justify-end;
  390. flex: 1;
  391. .before {
  392. @apply absolute top-1/2 right-0 w-[300px] h-[40px] bg-[#2BA0F3] rounded-l-full;
  393. transform: translateY(-30%);
  394. }
  395. }
  396. }
  397. video::-webkit-media-controls-fullscreen-button,
  398. video::-webkit-media-controls-enter-fullscreen-button,
  399. video::-webkit-media-controls-rotate-button,
  400. video::-webkit-media-controls-seek-back-button,
  401. video::-webkit-media-controls-seek-forward-button {
  402. display: none !important;
  403. }
  404. .mic {
  405. @apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
  406. }
  407. </style>