detail.uvue 13 KB

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