detail.uvue 13 KB

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