Selaa lähdekoodia

feat(课程详情): 改进录音交互并优化页面样式

- 添加录音倒计时进度环和时长验证,提升用户体验
- 重新设计录音界面,增加动画提示和视觉反馈
- 调整返回按钮样式和布局间距
- 修复类型转换问题,确保页面状态正确传递
- 优化组件生命周期管理,防止音频内存泄漏
- 添加实验引导语音播放功能
408249787 3 päivää sitten
vanhempi
commit
31afbef417
4 muutettua tiedostoa jossa 82 lisäystä ja 24 poistoa
  1. 2 2
      components/back.uvue
  2. 79 21
      pages/catalog/detail.uvue
  3. 1 1
      pages/catalog/web-view.uvue
  4. BIN
      static/rain_experiment.mp3

+ 2 - 2
components/back.uvue

@@ -4,12 +4,12 @@ import { router } from "@/.cool";
 </script>
 <template>
   <view class="back" @click="router.back()">
-    <cl-image src="https://oss.xiaoxiongcode.com/static/home/back.png" height="22" width="30" mode="heightFix" />
+    <cl-image src="https://oss.xiaoxiongcode.com/static/home/back.png" height="100%" mode="heightFix" />
   </view>
 </template>
 <style lang="scss" scoped>
 .back {
-  @apply fixed top-5 left-0 w-[60px] h-[40px] z-10;
+  @apply fixed top-5 left-0 w-[60px] h-[40px] z-10 p-[5px] pr-[10px];
   background: #D0F1FF;
   border-radius: 0px 25px 23px 0px;
   border: 1px solid #1D4BD9;

+ 79 - 21
pages/catalog/detail.uvue

@@ -1,7 +1,7 @@
 <script setup lang='ts'>
 import Back from '@/components/back.uvue'
 import Loading from '@/components/loading.uvue'
-import { ref, onMounted, watchEffect } from 'vue'
+import { ref, onMounted, watchEffect, nextTick, onUnmounted } from 'vue'
 import { type SubjectCourseResult, fetchSubjectCourseApp, updateSubjectProgress } from '@/services/subject/course'
 import { router } from '@/.cool'
 const isLoading = ref(true)
@@ -39,6 +39,13 @@ function initOnlibeRecord() {
   innerAudioContext.value.autoplay = true;
   recorderManager.value.onStop(function (res) {
     console.log('recorder stop' + JSON.stringify(res));
+    if (res.duration < 5000) {
+      uni.showToast({
+        title: '录音时间过短',
+        icon: 'none'
+      })
+      return
+    }
     voicePath.value = res.tempFilePath;
   });
 }
@@ -59,10 +66,10 @@ async function fetchCatalog() {
     data.value.videoSrc = course.value?.detailItem?.observeVideoPath || ''
     data.value.webviewSrc = course.value?.detailItem?.observeAnimationPath || ''
   } else {
-    progress.value = 1
-    progress2.value = 1
-    // progress.value = course.value?.courseUserProgress.mainProgress
-    // progress2.value = course.value?.courseUserProgress.assistantProgress
+    // progress.value = 1
+    // progress2.value = 1
+    progress.value = course.value?.courseUserProgress.mainProgress
+    progress2.value = course.value?.courseUserProgress.assistantProgress
   }
 }
 onMounted(async () => {
@@ -72,7 +79,7 @@ onMounted(async () => {
   setTimeout(() => {
     showProgress.value = false
     isLoading.value = false
-  }, 2000)
+  }, 1000)
 })
 async function handleEnded() {
   // await updateSubjectProgress({
@@ -107,10 +114,10 @@ onShow(() => {
   var pages = getCurrentPages();
   const prevPage = pages[pages.length - 1];
   console.log('prevPage', prevPage)
-  status.value = prevPage?.status || 'wait'
+  status.value = (prevPage as any)?.status || 'wait'
   if (status.value === 'success') {
     progress2.value++
-    prevPage.status = 'wait'
+      ; (prevPage as any).status = 'wait'
   }
   isLoading.value = false
 
@@ -132,6 +139,9 @@ watchEffect(() => {
         data.value.videoSrc = course.value?.detailItem?.questionVideoPath
         break
       case 3:
+        setTimeout(() => {
+          playVoice('/static/rain_experiment.mp3')
+        }, 2000)
         // data.value.webviewSrc = course.value?.detailItem?.assumeAnimationPath || data.value.webviewSrc
         // router.push({
         //   path: "/pages/catalog/web-view",
@@ -161,25 +171,36 @@ watchEffect(() => {
     }
   }
 })
+const timers = ref(100)
+const timer = ref<any>()
+
 function handleTop() {
   progress2.value = 4
   console.log(progress2.value)
 }
 function startRecord() {
   console.log('开始录音');
+  timer.value = setInterval(() => {
+    timers.value--
+    if (timers.value <= 0) {
+      endRecord()
+    }
+  }, 800)
   recorderManager.value.start();
 }
 function endRecord() {
-  console.log('录音结束');
+  clearInterval(timer.value)
+  timer.value = null
+  timers.value = 100
   recorderManager.value.stop();
 }
-function playVoice() {
-  console.log('播放录音');
-  if (voicePath.value) {
-    innerAudioContext.value.src = voicePath.value;
-    innerAudioContext.value.play();
-  }
+function playVoice(val) {
+  innerAudioContext.value.src = val;
+  innerAudioContext.value.play();
 }
+onUnmounted(() => {
+  innerAudioContext.value.stop();
+})
 </script>
 <template>
   <Loading v-show="isLoading" />
@@ -193,13 +214,46 @@ function playVoice() {
         @ended="handleEnded">
       </video>
       <view v-else class="w-full h-full ">
-        <img src="https://oss.xiaoxiongcode.com/static/home/2.png" alt="" class="w-full h-full object-cover" />
-        <view class="absolute top-1/2 left-1/2 text-[20px] font-bold text-white text-center">
+        <img src="https://oss.xiaoxiongcode.com/static/home/assert_1.gif" alt="" class="w-full h-full object-cover" />
+
+        <!-- 顶部提示文字 -->
+        <view class="absolute text-[20px] top-[20vh] w-full flex flex-row items-center justify-center">
+          <!-- <cl-icon name="notification-3-fill" color="#FCE762" :size="40"></cl-icon> -->
+          <text>🔊</text>
+          <text class=" font-bold text-white">为什么下雨后,盐和糖消失了,烤肉却还在呢?</text>
+        </view>
 
-          <button @tap="startRecord">开始录音</button>
-          <button @tap="endRecord">停止录音</button>
-          <button @tap="playVoice">播放录音</button>
-          <button @tap="handleTop">下一步</button>
+        <!-- 居中麦克风按钮 -->
+        <view class=" mic flex flex-col items-center w-[120px] h-[120px]">
+          <view v-show="timer">
+            <cl-progress-circle :value="timers"></cl-progress-circle>
+          </view>
+
+          <view class="mic  w-[120px] h-[120px] bg-[#3CB8FF]  rounded-full  flex items-center justify-center  z-[2]"
+            @tap="startRecord" v-if="!timer">
+            <cl-icon name="mic-fill" color="white" :size="60"></cl-icon>
+          </view>
+          <template v-else>
+            <view class="mic  w-[120px] h-[120px] bg-[#3CB8FF]  rounded-full  flex items-center justify-center  z-[2]"
+              @tap="endRecord">
+              <view class="w-[50px] h-[50px] rounded-[5px] bg-white"></view>
+            </view>
+          </template>
+
+          <!-- 指示手指 -->
+          <view class="absolute bottom-[-20px] right-[-20px] animate-bounce pointer-events-none z-[2]"
+            v-if="!timer && !voicePath">
+            <cl-icon name="arrow-left-up-line" color="white" :size="60"></cl-icon>
+          </view>
+        </view>
+        <!-- 下一步按钮 (录音完成后显示) -->
+        <view v-show="voicePath" class="mic flex flex-row items-center justify-center gap-[200px]">
+          <button
+            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"
+            @tap="playVoice(voicePath)">🔊</button>
+          <button
+            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"
+            @tap="handleTop">√</button>
         </view>
       </view>
       <view class="video-fullscreen_title" v-show="showControls && !showProgress">
@@ -314,4 +368,8 @@ video::-webkit-media-controls-seek-back-button,
 video::-webkit-media-controls-seek-forward-button {
   display: none !important;
 }
+
+.mic {
+  @apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
+}
 </style>

+ 1 - 1
pages/catalog/web-view.uvue

@@ -20,7 +20,7 @@ onMounted(() => {
 function handleMessage(e) {
   var pages = getCurrentPages();
   const prevPage = pages[pages.length - 2];
-  prevPage.status = "success"
+  (prevPage as any).status = "success"
 }
 </script>
 <template>

BIN
static/rain_experiment.mp3