From a249a5ee621539b58864bda84bd98fdfbb04a21c Mon Sep 17 00:00:00 2001 From: xcbyy <2578898740@qq.com> Date: Wed, 21 Oct 2026 11:20:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(voice):=20=E5=BD=95=E9=9F=B3=E6=97=B6?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E8=AF=BB=E7=A7=92=EF=BC=8C60=20=E7=A7=92?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=81=9C=E6=AD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 入驻申请页/大神工作台:点击开始录音后,按钮显示已录秒数,状态栏显示剩余秒数(每秒刷新) - 录音中按钮转为红橙渐变并呼吸闪烁,状态文字高亮;离开页面/出错自动清理计时器 - 60 秒到点自动 stop 录音,避免超长被服务端拒绝 - voice-intro-consistency 增补读秒护栏(107 套件/710 用例全绿) --- pwuniapp/pages/expert/apply.vue | 48 +++++++++++++-- pwuniapp/pages/expert/workspace.vue | 62 ++++++++++++++++++-- server/tests/voice-intro-consistency.spec.ts | 13 ++++ 3 files changed, 112 insertions(+), 11 deletions(-) diff --git a/pwuniapp/pages/expert/apply.vue b/pwuniapp/pages/expert/apply.vue index 8950888..40832bf 100644 --- a/pwuniapp/pages/expert/apply.vue +++ b/pwuniapp/pages/expert/apply.vue @@ -187,13 +187,13 @@ (可选,≤60 秒;审核与买家试听用) - - - {{ voiceStatusText }} + {{ voiceStatusText }} 语音沟通在游戏内或微信进行,请勿在录音中透露联系方式。 @@ -244,25 +244,49 @@ const voiceRecorded = ref(false); const voicePlaying = ref(false); const voiceUploading = ref(false); const voiceDuration = ref(0); +const voiceSeconds = ref(0); let voiceTempPath = ''; let recorderManager = null; let voiceAudioCtx = null; +let voiceTimer = null; const voiceStatusText = computed(() => { if (voiceUploading.value) return '上传中…'; + if (voiceRecording.value) return `录音中 · 剩余 ${Math.max(0, 60 - voiceSeconds.value)}s`; if (voiceRecorded.value) return `已录制 ${voiceDuration.value} 秒(提交后自动上传)`; return '未录制(可跳过)'; }); +const stopVoiceTimer = () => { + if (voiceTimer) { + clearInterval(voiceTimer); + voiceTimer = null; + } +}; + +const startVoiceTimer = () => { + stopVoiceTimer(); + voiceSeconds.value = 0; + voiceTimer = setInterval(() => { + voiceSeconds.value += 1; + if (voiceSeconds.value >= 60) { + stopVoiceTimer(); + try { recorderManager && recorderManager.stop(); } catch (_) {} + } + }, 1000); +}; + const ensureRecorder = () => { if (recorderManager) return recorderManager; recorderManager = uni.getRecorderManager(); recorderManager.onStop((res) => { + stopVoiceTimer(); voiceRecording.value = false; voiceTempPath = res.tempFilePath || ''; voiceDuration.value = Math.max(1, Math.round(Number(res.duration || 0) / 1000) || 1); voiceRecorded.value = Boolean(voiceTempPath); }); recorderManager.onError(() => { + stopVoiceTimer(); voiceRecording.value = false; uni.showToast({ title: '录音失败,请检查麦克风权限', icon: 'none' }); }); @@ -278,7 +302,7 @@ const toggleRecord = () => { const start = () => { rm.start({ duration: 60000, format: 'mp3', sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000 }); voiceRecording.value = true; - uni.showToast({ title: '开始录音,最长 60 秒', icon: 'none' }); + startVoiceTimer(); }; uni.authorize({ scope: 'scope.record', @@ -305,6 +329,7 @@ const playRecorded = () => { }; onUnmounted(() => { + stopVoiceTimer(); try { if (recorderManager && voiceRecording.value) recorderManager.stop(); } catch (_) {} try { if (voiceAudioCtx) { voiceAudioCtx.destroy(); voiceAudioCtx = null; } } catch (_) {} }); @@ -639,10 +664,23 @@ onShow(() => { color: #FFFFFF; background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%); } +.voice-btn.recording { + background: linear-gradient(135deg, #EF4444 0%, #F97316 100%); + animation: voice-pulse 1.2s ease-in-out infinite; +} +@keyframes voice-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.72; } +} .voice-status { font-size: 22rpx; color: var(--text-muted, #94A3B8); } +.voice-status.recording { + color: #F97316; + font-weight: 600; + font-variant-numeric: tabular-nums; +} .voice-tip { display: block; margin-top: 12rpx; diff --git a/pwuniapp/pages/expert/workspace.vue b/pwuniapp/pages/expert/workspace.vue index 6d97855..f6869af 100644 --- a/pwuniapp/pages/expert/workspace.vue +++ b/pwuniapp/pages/expert/workspace.vue @@ -204,14 +204,14 @@ ≤60 秒 · 仅本人录音 - - - - {{ voiceUploading ? '上传中…' : (expert.voiceIntroUrl ? `已上传 · ${expert.voiceDuration || 0} 秒` : '暂未上传') }} + + {{ voiceRecording ? `录音中 · 剩余 ${Math.max(0, 60 - voiceSeconds)}s` : (voiceUploading ? '上传中…' : (expert.voiceIntroUrl ? `已上传 · ${expert.voiceDuration || 0} 秒` : '暂未上传')) }} 语音仅用于买家试听介绍,实际语音沟通在游戏内或微信进行。 @@ -607,18 +607,41 @@ const loadExpertServices = async () => { const voiceRecording = ref(false); const voiceUploading = ref(false); const voicePlaying = ref(false); +const voiceSeconds = ref(0); let recorderManager = null; let voiceAudioCtx = null; +let voiceTimer = null; + +const stopVoiceTimer = () => { + if (voiceTimer) { + clearInterval(voiceTimer); + voiceTimer = null; + } +}; + +const startVoiceTimer = () => { + stopVoiceTimer(); + voiceSeconds.value = 0; + voiceTimer = setInterval(() => { + voiceSeconds.value += 1; + if (voiceSeconds.value >= 60) { + stopVoiceTimer(); + try { recorderManager && recorderManager.stop(); } catch (_) {} + } + }, 1000); +}; const ensureRecorder = () => { if (recorderManager) return recorderManager; recorderManager = uni.getRecorderManager(); recorderManager.onStop((res) => { + stopVoiceTimer(); voiceRecording.value = false; const duration = Math.max(1, Math.round(Number(res.duration || 0) / 1000) || 1); uploadRecordedVoice(res.tempFilePath, duration); }); recorderManager.onError(() => { + stopVoiceTimer(); voiceRecording.value = false; uni.showToast({ title: '录音失败,请检查麦克风权限', icon: 'none' }); }); @@ -648,7 +671,7 @@ const toggleRecord = () => { const start = () => { rm.start({ duration: 60000, format: 'mp3', sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000 }); voiceRecording.value = true; - uni.showToast({ title: '开始录音,最长 60 秒', icon: 'none' }); + startVoiceTimer(); }; uni.authorize({ scope: 'scope.record', @@ -676,6 +699,7 @@ const playVoice = () => { }; onUnmounted(() => { + stopVoiceTimer(); try { if (recorderManager && voiceRecording.value) recorderManager.stop(); } catch (_) {} try { if (voiceAudioCtx) { voiceAudioCtx.destroy(); voiceAudioCtx = null; } } catch (_) {} }); @@ -2296,6 +2320,27 @@ const switchBackToPlayer = () => { font-size: 24rpx; color: var(--text-muted); } + .voice-meta.recording { + color: #F97316; + font-weight: 600; + font-variant-numeric: tabular-nums; + } + .btn-wd-records { + margin: 0; + padding: 0 26rpx; + height: 62rpx; + line-height: 62rpx; + border-radius: 31rpx; + font-size: 24rpx; + color: #FFFFFF; + background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%); + + &::after { display: none; } + } + .btn-wd-records.recording { + background: linear-gradient(135deg, #EF4444 0%, #F97316 100%); + animation: voice-pulse 1.2s ease-in-out infinite; + } .voice-tip { display: block; margin-top: 12rpx; @@ -2305,6 +2350,11 @@ const switchBackToPlayer = () => { } } +@keyframes voice-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.72; } +} + /* 服务包操作与表单弹窗 */ .srv-ops { display: flex; diff --git a/server/tests/voice-intro-consistency.spec.ts b/server/tests/voice-intro-consistency.spec.ts index 0fa7873..4f19f99 100644 --- a/server/tests/voice-intro-consistency.spec.ts +++ b/server/tests/voice-intro-consistency.spec.ts @@ -29,6 +29,19 @@ describe('第九十一批语音介绍三端一致性闸门', () => { expect(apply).toContain('duration: 60000'); }); + it('录音时实时读秒并在 60 秒自动停止', () => { + const workspace = fs.readFileSync(path.join(root, 'pwuniapp/pages/expert/workspace.vue'), 'utf-8'); + for (const src of [apply, workspace]) { + expect(src).toContain('startVoiceTimer'); + expect(src).toContain('stopVoiceTimer'); + expect(src).toContain('setInterval'); + expect(src).toContain('voiceSeconds.value >= 60'); + expect(src).toContain('recorderManager.stop()'); + } + expect(apply).toContain('剩余 ${Math.max(0, 60 - voiceSeconds.value)}s'); + expect(workspace).toContain('剩余 ${Math.max(0, 60 - voiceSeconds)}s'); + }); + const scanSensitive = (dir: string): string[] => { const hits: string[] = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {