detail.uvue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  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, queryNextSubjectCourse } from '@/services/subject/course'
  6. import { router, user } from '@/.cool'
  7. const isLoading = ref(true)
  8. const showControls = ref(true)
  9. const showVideo = ref(true)
  10. import { useUi } from "@/uni_modules/cool-ui";
  11. const ui = useUi();
  12. const data = ref({
  13. videoSrc: '',
  14. webviewSrc: '',
  15. })
  16. // const menuItems = [
  17. // { id: 'watch', name: '看课', icon: 'https://oss.xiaoxiongcode.com/static/home/kanke.png' },
  18. // // { id: 'practice', name: '练习', icon: 'https://oss.xiaoxiongcode.com/static/home/lianxi.png' },
  19. // { id: 'experiment', name: '虚拟实验', icon: 'https://oss.xiaoxiongcode.com/static/home/shiyan.png' },
  20. // // { id: 'diary', name: '科学日记', icon: 'https://oss.xiaoxiongcode.com/static/home/riji.png' }
  21. // ]
  22. const menu2Items = [
  23. { id: '1', name: '观察', icon: 'https://oss.xiaoxiongcode.com/static/home/观察.png' },
  24. { id: '2', name: '交互', icon: 'https://oss.xiaoxiongcode.com/static/home/总结.png' },
  25. { id: '3', name: '思考', icon: 'https://oss.xiaoxiongcode.com/static/home/假设.png' },
  26. { id: '4', name: '假设', icon: 'https://oss.xiaoxiongcode.com/static/home/卡通扁平化功能图标设计.png' },
  27. { id: '5', name: '总结', icon: 'https://oss.xiaoxiongcode.com/static/home/拓展.png' },
  28. { id: '6', name: '实验', icon: 'https://oss.xiaoxiongcode.com/static/home/shiyan.png' },
  29. ]
  30. //当前进度
  31. const progress = ref(1)
  32. const progress2 = ref(0)
  33. const progressStatus = ref(0)
  34. const recorderManager = ref<any>()
  35. const innerAudioContext = ref<any>()
  36. const voicePath = ref('')
  37. function initOnlibeRecord() {
  38. recorderManager.value = uni.getRecorderManager();
  39. innerAudioContext.value = uni.createInnerAudioContext();
  40. innerAudioContext.value.autoplay = true;
  41. recorderManager.value?.onStop(function (res) {
  42. if (res.duration < 1000) {
  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. const nextCourse = ref<SubjectCourseResult>()
  54. //通过路由参数获取课程id
  55. async function fetchCatalog() {
  56. course.value = await fetchSubjectCourseApp({ id: user.courseId.value })
  57. progressStatus.value = course.value?.courseUserProgress.status
  58. progress2.value = Number(router.query().progress)
  59. console.log(progress2.value);
  60. // if (!course.value?.courseUserProgress) {
  61. // await updateSubjectProgress({
  62. // courseId: course.value?.id,
  63. // mainProgress: 1,
  64. // assistantProgress: 1,
  65. // status: 0
  66. // })
  67. // progress.value = 1
  68. // progress2.value = 1
  69. // } else if (course.value?.courseUserProgress.status == 0) {
  70. // } else {
  71. // progressStatus.value = course.value?.courseUserProgress.status
  72. // progress2.value = 1
  73. // }
  74. }
  75. async function fetchNextCatalog(id: string) {
  76. nextCourse.value = await fetchSubjectCourseApp({ id })
  77. }
  78. onMounted(async () => {
  79. isLoading.value = true
  80. await fetchCatalog()
  81. initOnlibeRecord()
  82. setTimeout(() => {
  83. isLoading.value = false
  84. }, 1000)
  85. })
  86. async function handleEnded() {
  87. progress2.value++
  88. }
  89. const status = ref('wait')
  90. const visible = ref(false)
  91. const visible2 = ref(false)
  92. onShow(async () => {
  93. isLoading.value = true
  94. var pages = getCurrentPages();
  95. const prevPage = pages[pages.length - 1];
  96. status.value = (prevPage as any)?.status || 'wait'
  97. console.log(status.value);
  98. if (status.value === 'success') {
  99. if (progress2.value === 6) {
  100. let nextCourseId = ''
  101. // if (progressStatus.value === 0) {
  102. nextCourseId = await updateSubjectProgress({
  103. courseId: course.value?.id,
  104. mainProgress: 1,
  105. assistantProgress: 6,
  106. status: 1
  107. })
  108. if (nextCourseId == '' || nextCourseId == null) {
  109. router.back({
  110. delta: 2
  111. })
  112. return
  113. }
  114. console.log(nextCourseId);
  115. user.setCourseId(nextCourseId)
  116. await fetchNextCatalog(nextCourseId)
  117. uni.createVideoContext('video1').pause()
  118. console.log(222);
  119. isLoading.value = false
  120. visible.value = true;
  121. // } else {
  122. // nextCourseId = await queryNextSubjectCourse({
  123. // id: course.value?.id,
  124. // })
  125. // if (nextCourseId == '' || nextCourseId == null) {
  126. // router.back({
  127. // delta: 2
  128. // })
  129. // return
  130. // }
  131. // console.log(nextCourseId);
  132. // user.setCourseId(nextCourseId)
  133. // await fetchNextCatalog(nextCourseId)
  134. // uni.createVideoContext('video1').pause()
  135. // console.log(222);
  136. // isLoading.value = false
  137. // visible.value = true
  138. // }
  139. (prevPage as any).status = 'wait'
  140. return
  141. }
  142. progress2.value++;
  143. (prevPage as any).status = 'wait'
  144. }
  145. isLoading.value = false
  146. })
  147. function handleControlsToggle(e) {
  148. showControls.value = e.detail.show
  149. }
  150. watch(() => progress2.value, (newVal, oldVal) => {
  151. isLoading.value = true
  152. if (course.value?.id && progressStatus.value == 0) {
  153. updateSubjectProgress({
  154. courseId: course.value?.id,
  155. mainProgress: 1,
  156. assistantProgress: progress2.value,
  157. status: progressStatus.value
  158. })
  159. }
  160. console.log(progress2.value);
  161. switch (progress2.value) {
  162. case 1:
  163. setTimeout(() => {
  164. isLoading.value = false
  165. data.value.videoSrc = course.value?.detailItem?.observeVideoPath
  166. }, 2000)
  167. break
  168. case 2:
  169. data.value.webviewSrc = course.value?.detailItem?.summaryVideoPath
  170. router.push({
  171. path: "/pages/catalog/web-view",
  172. query: {
  173. src: data.value.webviewSrc,
  174. progress: progress2.value,
  175. }
  176. });
  177. data.value.videoSrc = course.value?.detailItem?.observeVideoPath
  178. break
  179. case 3:
  180. data.value.videoSrc = ''
  181. setTimeout(() => {
  182. isLoading.value = false
  183. data.value.videoSrc = course.value?.detailItem?.questionVideoPath
  184. }, 2000)
  185. break
  186. case 4:
  187. data.value.videoSrc = ''
  188. setTimeout(() => {
  189. isLoading.value = false
  190. playVoice(course.value?.detailItem?.assumeAnimationPath)
  191. }, 2000)
  192. break
  193. case 5:
  194. data.value.videoSrc = ''
  195. setTimeout(() => {
  196. isLoading.value = false
  197. data.value.videoSrc = course.value?.detailItem?.testVideoPath
  198. }, 2000)
  199. break
  200. case 6:
  201. console.log(course.value?.testItem?.animationPath);
  202. data.value.webviewSrc = course.value?.testItem?.animationPath
  203. router.push({
  204. path: "/pages/catalog/web-view",
  205. query: {
  206. src: data.value.webviewSrc,
  207. progress: progress2.value,
  208. }
  209. });
  210. data.value.videoSrc = course.value?.detailItem?.testVideoPath
  211. break
  212. }
  213. }, { immediate: true })
  214. const timers = ref(100)
  215. const timer = ref<any>()
  216. function handleTop() {
  217. progress2.value = 5
  218. voicePath.value = ''
  219. }
  220. function startRecord() {
  221. const res = uni.getAppAuthorizeSetting()
  222. console.log(res);
  223. if (res.microphoneAuthorized == 'authorized') {
  224. console.log('开始录音');
  225. timer.value = setInterval(() => {
  226. timers.value--
  227. console.log(timers.value);
  228. if (timers.value <= 0) {
  229. endRecord()
  230. }
  231. }, 800)
  232. recorderManager.value.start();
  233. } else {
  234. ui.showConfirm({
  235. title: "获取麦克风权限",
  236. message: "本应用仅在你主动触发录音操作时调用麦克风,仅用于临时实时录音,录音内容仅在当前页面本地播放,供你本人收听。你可随时在系统设置中关闭麦克风权限,关闭后将无法使用录音功能,不影响应用其他功能正常使用",
  237. callback(action) {
  238. if (action === "confirm") {
  239. console.log('开始录音');
  240. timer.value = setInterval(() => {
  241. timers.value--
  242. console.log(timers.value);
  243. if (timers.value <= 0) {
  244. endRecord()
  245. }
  246. }, 800)
  247. recorderManager.value.start();
  248. // 执行删除操作
  249. } else {
  250. console.log("用户取消操作");
  251. }
  252. },
  253. });
  254. }
  255. }
  256. function endRecord() {
  257. clearInterval(timer.value)
  258. timer.value = null
  259. timers.value = 100
  260. recorderManager.value.stop();
  261. }
  262. function playVoice(val) {
  263. innerAudioContext.value.src = val;
  264. innerAudioContext.value.play();
  265. }
  266. function handleLoadedMetadata() {
  267. // 视频加载完成后,设置全屏播放
  268. // uni.createVideoContext('video1').requestFullScreen()
  269. }
  270. function playNext() {
  271. visible.value = false
  272. course.value = nextCourse.value
  273. progressStatus.value = nextCourse.value?.courseUserProgress.status
  274. progress2.value = 1
  275. }
  276. onUnmounted(() => {
  277. clearInterval(timer.value)
  278. innerAudioContext.value.stop();
  279. })
  280. </script>
  281. <template>
  282. <Loading v-show="isLoading" />
  283. <cl-page v-show="!isLoading">
  284. <Back />
  285. <view class="video-container">
  286. <video v-if="progress2 !== 4" id="video1" class="w-full h-full " enable-play-gesture :src="data.videoSrc"
  287. enable-auto-rotation :show-center-play-btn="false" :show-background-playback-button="false"
  288. :show-fullscreen-btn="false" :show-casting-button="false" autoplay @controlstoggle="handleControlsToggle"
  289. @ended="handleEnded" @play="handleLoadedMetadata">
  290. </video>
  291. <view v-else class="w-full h-full ">
  292. <image mode="aspectFill" src="https://oss.xiaoxiongcode.com/static/home/7101.jpg" alt=""
  293. class="w-full h-full object-cover" />
  294. <cl-image lazy-load src="https://oss.xiaoxiongcode.com/static/home/3.gif" mode="heightFix"
  295. class="!absolute bottom-2 left-10 !w-[160rpx] !h-[200rpx] z-[1]" />
  296. <!-- <cl-image lazy-load src="https://oss.xiaoxiongcode.com/static/home/-3.gif" mode="heightFix"
  297. class="!absolute bottom-2 right-10 !w-[160rpx] !h-[200rpx] z-[1]" /> -->
  298. <!-- 顶部提示文字 -->
  299. <view class="absolute text-[20px] top-[20vh] w-full flex flex-row items-center justify-center">
  300. <!-- <cl-icon name="notification-3-fill" color="#FCE762" :size="40"></cl-icon> -->
  301. <cl-image src="https://oss.xiaoxiongcode.com/static/home/guangbo.png" mode="heightFix" class="!h-[25px]"
  302. @tap="playVoice(course?.detailItem?.assumeAnimationPath)"></cl-image>
  303. <text class=" font-bold text-white">{{ course?.detailItem?.assumeAnimationJavascriptPath }}</text>
  304. </view>
  305. <!-- 居中麦克风按钮 -->
  306. <view class=" mic flex flex-col items-center w-[220px] h-[220px]">
  307. <!-- <view v-show="timer" class="mic">
  308. <cl-progress-circle :value="timers"></cl-progress-circle>
  309. </view> -->
  310. <view
  311. class="mic w-[120px] h-[120px] bg-[#3CB8FF] rounded-full flex items-center justify-center z-[2] border-[4px] border-white border-solid"
  312. @tap="startRecord" v-if="!timer">
  313. <cl-image src="https://oss.xiaoxiongcode.com/static/home/maikefeng.png" mode="heightFix"
  314. class="!h-[60px]"></cl-image>
  315. </view>
  316. <template v-else>
  317. <view class="mic w-[120px] h-[120px] bg-[#3CB8FF] rounded-full flex items-center justify-center z-[2]"
  318. @tap="endRecord">
  319. <view
  320. class="w-[50px] h-[50px] rounded-[5px] bg-white flex items-center justify-center text-[20px] font-bold text-[#3CB8FF]">
  321. <text class="text-[20px] font-bold text-[#3CB8FF]">{{ timers }}</text>
  322. </view>
  323. </view>
  324. </template>
  325. <!-- 指示手指 -->
  326. <view class="absolute bottom-[20px] right-[20px] animate-bounce pointer-events-none z-[2] !h-[60px] w-[100px]"
  327. v-if="!timer && !voicePath">
  328. <cl-image src="https://oss.xiaoxiongcode.com/static/home/shou.png" mode="heightFix" width="100%"
  329. class=""></cl-image>
  330. </view>
  331. </view>
  332. <!-- 下一步按钮 (录音完成后显示) -->
  333. <view v-show="voicePath" class="mic flex flex-row items-center justify-center gap-[200px]">
  334. <button
  335. 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"
  336. @tap="playVoice(voicePath)">
  337. <cl-image src="https://oss.xiaoxiongcode.com/static/home/guangbo-bai.png" mode="heightFix"
  338. class="!h-[25px]"></cl-image>
  339. </button>
  340. <button
  341. 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"
  342. @tap="handleTop">
  343. <cl-image src="https://oss.xiaoxiongcode.com/static/home/6241.png" mode="heightFix"
  344. class="!h-[25px]"></cl-image>
  345. </button>
  346. </view>
  347. </view>
  348. <view class="video-fullscreen_title" v-show="showControls">
  349. <Back />
  350. <view class="text-[20px] font-bold text-white">
  351. <text class="text-[20px] font-bold text-[#fff]">
  352. {{ course?.mainTitle }}
  353. </text>
  354. </view>
  355. <!-- <view class="control-progress">
  356. <view class="before"></view>
  357. <view v-for="(item, i) in menu2Items" :key="item.id"
  358. class="py-2 px-3 flex items-center flex-row justify-center ov gap-[5px] relative z-[1]"
  359. :class="{ '!bg-[#fff] rounded-l-full': progress2 == i + 1 }" @tap="progress2 = i + 1">
  360. <cl-image :src="item.icon" mode="heightFix" class="!h-[28px] !w-[50px]"></cl-image>
  361. <text class="text-[12px] font-bold" :class="{ '!text-[#2BA0F3]': progress2 == i + 1 }">{{ item.name
  362. }}</text>
  363. </view>
  364. </view> -->
  365. </view>
  366. </view>
  367. <view class="course-detail-page">
  368. <!-- 顶部标题栏 -->
  369. <view class="flex-[1] h-[100vh] relative">
  370. <!-- <view class="flex flex-row w-full pr-[3vw] pl-[10vw] items-center justify-between absolute top-[30px]">
  371. <text class="text-[5vh] font-bold">{{ course?.mainTitle }}</text>
  372. <text
  373. class="rounded-full p-[1vh] px-[2vh] border-2 border-[#fff] border-solid text-[#fff] text-[3vh] font-bold">课程收获</text>
  374. </view> -->
  375. </view>
  376. <!-- 右侧功能菜单 -->
  377. <!-- <view class="w-[25vw] h-[100vh] bg-[#5CBDFD] flex flex-col justify-center p-5">
  378. <view v-for="(item, i) in menuItems" :key="item.id" class="h-[20vh] flex flex-row items-center ">
  379. <view
  380. 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]"
  381. :class="{ '!bg-[#fff]': progress >= i }">
  382. <cl-image :src="item.icon" mode="heightFix" class="!h-[7vh]"></cl-image>
  383. <text class="text-[2vh] font-bold">{{ item.name }}</text>
  384. </view>
  385. <view class="flex items-center h-[20vh] ml-[2vh] ">
  386. <view class="w-[1vh] bg-[#B4DBF7] flex-1"
  387. :class="{ '!bg-[#71C73D]': progress >= i, '!opacity-0': i === 0 }">
  388. </view>
  389. <view class="w-[5vh] bg-[#fff] h-[5vh] rounded-full" :class="{ '!bg-[#71C73D]': progress >= i }"></view>
  390. <view class="w-[1vh] bg-[#B4DBF7] flex-1"
  391. :class="{ '!bg-[#71C73D]': progress >= i, '!opacity-0': i === menuItems.length - 1 }"></view>
  392. </view>
  393. </view>
  394. </view> -->
  395. </view>
  396. </cl-page>
  397. <cl-popup v-model="visible" :width="300" direction="center" :show-header="false" :maskClosable="false">
  398. <view class="p-2">
  399. <cl-image lazy-load :src="nextCourse?.ossIconPath" mode="heightFix"
  400. class="!w-full !h-[150px] mb-[10px] rounded-xl"></cl-image>
  401. <cl-row :gutter="12">
  402. <cl-col :span="12"> <cl-button size="small" type="info" @click="router.back()">返回</cl-button></cl-col>
  403. <cl-col :span="12"> <cl-button size="small" type="success" @click="playNext">播放下集</cl-button></cl-col>
  404. </cl-row>
  405. </view>
  406. </cl-popup>
  407. <cl-popup v-model="visible2" showClose :size="400" :show-header="false" direction="center">
  408. <view class="p-[40px] ">
  409. <cl-text class="text-center" color="#000" :size="20">
  410. 暂无下集
  411. </cl-text>
  412. <cl-button class="mt-4" size="large" type="warn" block @tap="visible2 = false"> 返回 </cl-button>
  413. </view>
  414. </cl-popup>
  415. </template>
  416. <style lang="scss" scoped>
  417. .translate50 {
  418. transform: translateY(-50%);
  419. }
  420. .course-detail-page {
  421. @apply flex flex-row items-center;
  422. background-color: #3498DB;
  423. height: 100vh;
  424. color: black;
  425. transition: all 1s ease-in-out;
  426. }
  427. .video-container {
  428. width: 100vw;
  429. height: 100vh;
  430. top: 0;
  431. left: 0;
  432. transform: translate(0, 0);
  433. animation: spin 2s linear 1;
  434. transition: all 2s ease-in-out;
  435. }
  436. @keyframes spin {
  437. 0% {
  438. opacity: 50%;
  439. }
  440. 100% {
  441. opacity: 1;
  442. }
  443. }
  444. .hidden {
  445. opacity: 0;
  446. transition: all 3s ease-in-out;
  447. }
  448. .video-fullscreen_title {
  449. @apply absolute top-3 left-0 pl-[80px] w-[100vw] z-10 flex items-center justify-between flex-row;
  450. color: #fff;
  451. .control-progress {
  452. @apply fixed top-1/2 right-[0px] flex flex-col gap-[3px] z-[100] h-[280px] w-[105px];
  453. transform: translateY(-50%);
  454. view {
  455. overflow: initial !important;
  456. }
  457. .before {
  458. @apply absolute top-0 right-0 h-full w-full bg-[#2BA0F3];
  459. border-radius: 25px 0 0 25px;
  460. }
  461. .ov {
  462. overflow: initial !important;
  463. }
  464. }
  465. }
  466. video::-webkit-media-controls-fullscreen-button,
  467. video::-webkit-media-controls-enter-fullscreen-button,
  468. video::-webkit-media-controls-rotate-button,
  469. video::-webkit-media-controls-seek-back-button,
  470. video::-webkit-media-controls-seek-forward-button {
  471. display: none !important;
  472. }
  473. .mic {
  474. @apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
  475. }
  476. </style>