Kaynağa Gözat

fix: 修复录音总是"太短"——去掉mouseleave误触+异步竞态

根因:mic按钮的scale放大动画使鼠标瞬间脱离按钮触发mouseleave,
录音刚开就被停。改为:
- window级mouseup监听(松手在哪都能停),去掉mouseleave
- pressing标志处理getUserMedia异步竞态(await期间松手则放弃/补停)
- onaudioprocess不再依赖recording标志,避免漏帧
- 呼吸动画用box-shadow代替scale

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
simonlll 1 hafta önce
ebeveyn
işleme
03e0811473
1 değiştirilmiş dosya ile 44 ekleme ve 28 silme
  1. 44 28
      src/pages/ai-chat/index.vue

+ 44 - 28
src/pages/ai-chat/index.vue

@@ -120,12 +120,10 @@
120 120
       <!-- 麦克风按钮(同时支持触摸和鼠标,方便桌面浏览器测试) -->
121 121
       <view
122 122
         :class="['mic-btn', recording ? 'recording' : '']"
123
-        @touchstart.prevent="startRecord"
124
-        @touchend.prevent="stopRecord"
125
-        @touchcancel.prevent="cancelRecord"
126
-        @mousedown.prevent="startRecord"
127
-        @mouseup.prevent="stopRecord"
128
-        @mouseleave.prevent="onMicLeave"
123
+        @touchstart.prevent="onPressStart"
124
+        @touchend.prevent="onPressEnd"
125
+        @touchcancel.prevent="onPressCancel"
126
+        @mousedown.prevent="onMouseDown"
129 127
       >
130 128
         <text class="mic-icon">{{ recording ? '●' : '🎤' }}</text>
131 129
       </view>
@@ -175,6 +173,7 @@ export default {
175 173
       exampleTags: ['各班组查获排名', '打火机查获数量', '旅检一部人员资质', '全站党员人数'],
176 174
       // 录音相关(Web Audio 直接采集 PCM)
177 175
       recording: false,
176
+      pressing: false,
178 177
       recordCancelled: false,
179 178
       audioCtx: null,
180 179
       mediaStream: null,
@@ -185,14 +184,39 @@ export default {
185 184
     }
186 185
   },
187 186
   methods: {
188
-    // ============ 录音相关(Web Audio 直接采集原始 PCM,避免 decodeAudioData 失败)============
189
-    async startRecord(e) {
190
-      if (this.loading) return
187
+    // ============ 按压事件包装 ============
188
+    onMouseDown(e) {
189
+      // 鼠标:松手可能在按钮外,用 window 级监听
190
+      this._winUp = () => this.onPressEnd()
191
+      window.addEventListener('mouseup', this._winUp, { once: true })
192
+      this.onPressStart(e)
193
+    },
194
+    onPressStart(e) {
195
+      if (this.loading || this.pressing) return
196
+      this.pressing = true
197
+      this.startRecord()
198
+    },
199
+    onPressEnd(e) {
200
+      if (!this.pressing) return
201
+      this.pressing = false
202
+      this.stopRecord()
203
+    },
204
+    onPressCancel(e) {
205
+      if (!this.pressing) return
206
+      this.pressing = false
207
+      this.recordCancelled = true
208
+      this.stopRecord()
209
+    },
210
+
211
+    // ============ 录音相关(Web Audio 直接采集原始 PCM)============
212
+    async startRecord() {
191 213
       this.recordCancelled = false
192 214
       this.pcmChunks = []
193 215
 
194 216
       try {
195 217
         const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
218
+        // 异步竞态:若 await 期间已松手,直接放弃
219
+        if (!this.pressing) { stream.getTracks().forEach(t => t.stop()); return }
196 220
         this.mediaStream = stream
197 221
 
198 222
         const AudioCtx = window.AudioContext || window.webkitAudioContext
@@ -204,8 +228,7 @@ export default {
204 228
         const source = ctx.createMediaStreamSource(stream)
205 229
         const node = ctx.createScriptProcessor(4096, 1, 1)
206 230
         node.onaudioprocess = (ev) => {
207
-          if (!this.recording) return
208
-          // 复制一份当前帧的 Float32 PCM
231
+          // 只要节点还连着就采集(不依赖 recording 标志,避免漏帧)
209 232
           this.pcmChunks.push(new Float32Array(ev.inputBuffer.getChannelData(0)))
210 233
         }
211 234
         source.connect(node)
@@ -214,7 +237,11 @@ export default {
214 237
         this.sourceNode = source
215 238
         this.scriptNode = node
216 239
         this.recording = true
240
+
241
+        // 若设置过程中已松手,立即停止并提交
242
+        if (!this.pressing) this.stopRecord()
217 243
       } catch (err) {
244
+        this.recording = false
218 245
         let msg = '无法访问麦克风'
219 246
         if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
220 247
           msg = '当前环境不支持录音(需HTTPS或localhost)'
@@ -238,7 +265,7 @@ export default {
238 265
       this.audioCtx = null
239 266
     },
240 267
 
241
-    stopRecord(e) {
268
+    stopRecord() {
242 269
       if (!this.recording) return
243 270
       this.recording = false
244 271
       const rate = this.pcmSampleRate
@@ -247,18 +274,6 @@ export default {
247 274
       if (!this.recordCancelled) this.submitAudio(chunks, rate)
248 275
     },
249 276
 
250
-    cancelRecord(e) {
251
-      if (!this.recording) return
252
-      this.recordCancelled = true
253
-      this.recording = false
254
-      this._teardownAudio()
255
-    },
256
-
257
-    // 鼠标移出按钮:录音中则发送(等同松手)
258
-    onMicLeave(e) {
259
-      if (this.recording) this.stopRecord(e)
260
-    },
261
-
262 277
     async submitAudio(chunks, srcRate) {
263 278
       // 合并所有 PCM 帧
264 279
       let total = 0
@@ -742,14 +757,15 @@ $text-sub: #999;
742 757
   transition: background 0.2s;
743 758
   &.recording {
744 759
     background: #ff4d4f;
745
-    animation: pulse 0.8s infinite;
760
+    animation: micPulse 0.8s infinite;
746 761
   }
747 762
 }
748 763
 .mic-icon { font-size: 32rpx; line-height: 1; }
749 764
 
750
-@keyframes pulse {
751
-  0%, 100% { transform: scale(1); }
752
-  50% { transform: scale(1.12); }
765
+/* 用阴影呼吸代替 scale,避免缩放触发 mouseleave */
766
+@keyframes micPulse {
767
+  0%, 100% { box-shadow: 0 0 0 0 rgba(255,77,79,0.5); }
768
+  50% { box-shadow: 0 0 0 12rpx rgba(255,77,79,0); }
753 769
 }
754 770
 
755 771
 /* 录音浮层 */