563 lines
14 KiB
JavaScript
563 lines
14 KiB
JavaScript
|
|
// 🎤 语音录制组件逻辑
|
|||
|
|
const voiceMessageManager = require('../../utils/voice-message-manager.js');
|
|||
|
|
|
|||
|
|
Component({
|
|||
|
|
properties: {
|
|||
|
|
// 是否显示录音界面
|
|||
|
|
visible: {
|
|||
|
|
type: Boolean,
|
|||
|
|
value: false,
|
|||
|
|
observer: 'onVisibleChange'
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 最大录音时长(毫秒)
|
|||
|
|
maxDuration: {
|
|||
|
|
type: Number,
|
|||
|
|
value: 60000
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 最小录音时长(毫秒)
|
|||
|
|
minDuration: {
|
|||
|
|
type: Number,
|
|||
|
|
value: 1000
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
data: {
|
|||
|
|
// 录音状态:idle, recording, paused, completed, error
|
|||
|
|
recordingState: 'idle',
|
|||
|
|
|
|||
|
|
// 状态文本
|
|||
|
|
statusText: '准备录音',
|
|||
|
|
|
|||
|
|
// 录音时长
|
|||
|
|
recordingDuration: 0,
|
|||
|
|
|
|||
|
|
// 实时波形数据
|
|||
|
|
realtimeWaveform: [],
|
|||
|
|
|
|||
|
|
// 最终波形数据
|
|||
|
|
finalWaveform: [],
|
|||
|
|
|
|||
|
|
// 录音文件信息
|
|||
|
|
tempFilePath: '',
|
|||
|
|
fileSize: 0,
|
|||
|
|
|
|||
|
|
// 播放状态
|
|||
|
|
isPlaying: false,
|
|||
|
|
|
|||
|
|
// 错误信息
|
|||
|
|
errorMessage: '',
|
|||
|
|
|
|||
|
|
// 权限引导
|
|||
|
|
showPermissionGuide: false,
|
|||
|
|
|
|||
|
|
// 定时器
|
|||
|
|
durationTimer: null,
|
|||
|
|
waveformTimer: null
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
lifetimes: {
|
|||
|
|
attached() {
|
|||
|
|
console.log('🎤 语音录制组件加载');
|
|||
|
|
this.initComponent();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
detached() {
|
|||
|
|
console.log('🎤 语音录制组件卸载');
|
|||
|
|
this.cleanup();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
methods: {
|
|||
|
|
// 事件阻断占位
|
|||
|
|
noop() {},
|
|||
|
|
// 初始化组件
|
|||
|
|
initComponent() {
|
|||
|
|
// 注册语音管理器事件
|
|||
|
|
this.registerVoiceEvents();
|
|||
|
|
|
|||
|
|
// 初始化波形数据
|
|||
|
|
this.initWaveform();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 可见性变化处理
|
|||
|
|
onVisibleChange(visible) {
|
|||
|
|
if (visible) {
|
|||
|
|
this.resetRecorder();
|
|||
|
|
} else {
|
|||
|
|
this.cleanup();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 注册语音管理器事件
|
|||
|
|
registerVoiceEvents() {
|
|||
|
|
// 录音开始事件
|
|||
|
|
voiceMessageManager.on('recordStart', () => {
|
|||
|
|
this.setData({
|
|||
|
|
recordingState: 'recording',
|
|||
|
|
statusText: '正在录音...',
|
|||
|
|
recordingDuration: 0
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.startDurationTimer();
|
|||
|
|
this.startWaveformAnimation();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 录音停止事件
|
|||
|
|
voiceMessageManager.on('recordStop', (data) => {
|
|||
|
|
this.setData({
|
|||
|
|
recordingState: 'completed',
|
|||
|
|
statusText: '录音完成',
|
|||
|
|
recordingDuration: data.duration,
|
|||
|
|
tempFilePath: data.tempFilePath,
|
|||
|
|
fileSize: data.fileSize
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.stopTimers();
|
|||
|
|
this.generateFinalWaveform();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 录音暂停事件
|
|||
|
|
voiceMessageManager.on('recordPause', () => {
|
|||
|
|
this.setData({
|
|||
|
|
recordingState: 'paused',
|
|||
|
|
statusText: '录音已暂停'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.stopTimers();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 录音恢复事件
|
|||
|
|
voiceMessageManager.on('recordResume', () => {
|
|||
|
|
this.setData({
|
|||
|
|
recordingState: 'recording',
|
|||
|
|
statusText: '正在录音...'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.startDurationTimer();
|
|||
|
|
this.startWaveformAnimation();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 录音错误事件
|
|||
|
|
voiceMessageManager.on('recordError', (error) => {
|
|||
|
|
console.error('🎤 录音错误:', error);
|
|||
|
|
|
|||
|
|
this.setData({
|
|||
|
|
recordingState: 'error',
|
|||
|
|
statusText: '录音失败',
|
|||
|
|
errorMessage: this.getErrorMessage(error)
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.stopTimers();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 录音帧数据事件
|
|||
|
|
voiceMessageManager.on('recordFrame', (data) => {
|
|||
|
|
this.updateRealtimeWaveform(data.frameBuffer);
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 🎤 ===== 录音控制 =====
|
|||
|
|
|
|||
|
|
// 开始录音
|
|||
|
|
async startRecording() {
|
|||
|
|
console.log('🎤 开始录音');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await voiceMessageManager.startRecording({
|
|||
|
|
duration: this.properties.maxDuration,
|
|||
|
|
format: 'mp3'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('🎤 开始录音失败:', error);
|
|||
|
|
|
|||
|
|
if (error.message.includes('权限')) {
|
|||
|
|
this.setData({
|
|||
|
|
showPermissionGuide: true
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
this.setData({
|
|||
|
|
recordingState: 'error',
|
|||
|
|
statusText: '录音失败',
|
|||
|
|
errorMessage: this.getErrorMessage(error)
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 停止录音
|
|||
|
|
stopRecording() {
|
|||
|
|
console.log('🎤 停止录音');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 检查最小录音时长
|
|||
|
|
if (this.data.recordingDuration < this.properties.minDuration) {
|
|||
|
|
wx.showToast({
|
|||
|
|
title: `录音时长不能少于${this.properties.minDuration / 1000}秒`,
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
voiceMessageManager.stopRecording();
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('🎤 停止录音失败:', error);
|
|||
|
|
this.setData({
|
|||
|
|
recordingState: 'error',
|
|||
|
|
statusText: '停止录音失败',
|
|||
|
|
errorMessage: this.getErrorMessage(error)
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 暂停录音
|
|||
|
|
pauseRecording() {
|
|||
|
|
console.log('🎤 暂停录音');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
voiceMessageManager.pauseRecording();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('🎤 暂停录音失败:', error);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 恢复录音
|
|||
|
|
resumeRecording() {
|
|||
|
|
console.log('🎤 恢复录音');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
voiceMessageManager.resumeRecording();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('🎤 恢复录音失败:', error);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 取消录音
|
|||
|
|
cancelRecording() {
|
|||
|
|
console.log('🎤 取消录音');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
voiceMessageManager.cancelRecording();
|
|||
|
|
this.resetRecorder();
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('🎤 取消录音失败:', error);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 丢弃录音
|
|||
|
|
discardRecording() {
|
|||
|
|
console.log('🎤 丢弃录音');
|
|||
|
|
this.resetRecorder();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 🔊 ===== 播放控制 =====
|
|||
|
|
|
|||
|
|
// 播放预览
|
|||
|
|
async playPreview() {
|
|||
|
|
if (!this.data.tempFilePath) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
if (this.data.isPlaying) {
|
|||
|
|
voiceMessageManager.stopPlaying();
|
|||
|
|
this.setData({ isPlaying: false });
|
|||
|
|
} else {
|
|||
|
|
await voiceMessageManager.playVoiceMessage(this.data.tempFilePath);
|
|||
|
|
this.setData({ isPlaying: true });
|
|||
|
|
|
|||
|
|
// 监听播放结束
|
|||
|
|
const onPlayEnd = () => {
|
|||
|
|
this.setData({ isPlaying: false });
|
|||
|
|
voiceMessageManager.off('playEnd', onPlayEnd);
|
|||
|
|
};
|
|||
|
|
voiceMessageManager.on('playEnd', onPlayEnd);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('🎤 播放预览失败:', error);
|
|||
|
|
wx.showToast({
|
|||
|
|
title: '播放失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 📤 ===== 发送录音 =====
|
|||
|
|
|
|||
|
|
// 发送录音
|
|||
|
|
async sendRecording() {
|
|||
|
|
if (!this.data.tempFilePath || !this.data.recordingDuration) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('📤 发送录音');
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
wx.showLoading({
|
|||
|
|
title: '上传中...',
|
|||
|
|
mask: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 上传语音文件
|
|||
|
|
const uploadResult = await voiceMessageManager.uploadVoiceFile(
|
|||
|
|
this.data.tempFilePath,
|
|||
|
|
this.data.recordingDuration
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
wx.hideLoading();
|
|||
|
|
|
|||
|
|
if (uploadResult.success) {
|
|||
|
|
// 触发发送事件
|
|||
|
|
this.triggerEvent('send', {
|
|||
|
|
type: 'voice',
|
|||
|
|
url: uploadResult.url,
|
|||
|
|
duration: uploadResult.duration,
|
|||
|
|
size: uploadResult.size,
|
|||
|
|
tempFilePath: this.data.tempFilePath
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 关闭录音界面
|
|||
|
|
this.closeRecorder();
|
|||
|
|
|
|||
|
|
wx.showToast({
|
|||
|
|
title: '发送成功',
|
|||
|
|
icon: 'success'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
} else {
|
|||
|
|
throw new Error('上传失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
wx.hideLoading();
|
|||
|
|
console.error('📤 发送录音失败:', error);
|
|||
|
|
|
|||
|
|
wx.showToast({
|
|||
|
|
title: '发送失败',
|
|||
|
|
icon: 'none'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 🎨 ===== 界面控制 =====
|
|||
|
|
|
|||
|
|
// 关闭录音界面
|
|||
|
|
closeRecorder() {
|
|||
|
|
console.log('❌ 关闭录音界面');
|
|||
|
|
|
|||
|
|
// 如果正在录音,先停止
|
|||
|
|
if (this.data.recordingState === 'recording') {
|
|||
|
|
this.cancelRecording();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果正在播放,先停止
|
|||
|
|
if (this.data.isPlaying) {
|
|||
|
|
voiceMessageManager.stopPlaying();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 先自隐,再通知父级,提升关闭成功率
|
|||
|
|
this.setData({ visible: false });
|
|||
|
|
this.triggerEvent('close');
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 遮罩点击
|
|||
|
|
onOverlayTap() {
|
|||
|
|
// 点击遮罩关闭
|
|||
|
|
this.closeRecorder();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 🔐 ===== 权限处理 =====
|
|||
|
|
|
|||
|
|
// 取消权限申请
|
|||
|
|
cancelPermission() {
|
|||
|
|
this.setData({
|
|||
|
|
showPermissionGuide: false
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 打开设置页面
|
|||
|
|
openSettings() {
|
|||
|
|
wx.openSetting({
|
|||
|
|
success: (res) => {
|
|||
|
|
if (res.authSetting['scope.record']) {
|
|||
|
|
this.setData({
|
|||
|
|
showPermissionGuide: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
wx.showToast({
|
|||
|
|
title: '权限已开启',
|
|||
|
|
icon: 'success'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 🔧 ===== 工具方法 =====
|
|||
|
|
|
|||
|
|
// 重置录音器
|
|||
|
|
resetRecorder() {
|
|||
|
|
this.setData({
|
|||
|
|
recordingState: 'idle',
|
|||
|
|
statusText: '准备录音',
|
|||
|
|
recordingDuration: 0,
|
|||
|
|
tempFilePath: '',
|
|||
|
|
fileSize: 0,
|
|||
|
|
isPlaying: false,
|
|||
|
|
errorMessage: '',
|
|||
|
|
showPermissionGuide: false
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.stopTimers();
|
|||
|
|
this.initWaveform();
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 初始化波形
|
|||
|
|
initWaveform() {
|
|||
|
|
const waveform = Array(20).fill(0).map(() => Math.random() * 30 + 10);
|
|||
|
|
this.setData({
|
|||
|
|
realtimeWaveform: waveform,
|
|||
|
|
finalWaveform: []
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 开始时长计时器
|
|||
|
|
startDurationTimer() {
|
|||
|
|
this.stopTimers();
|
|||
|
|
|
|||
|
|
this.data.durationTimer = setInterval(() => {
|
|||
|
|
const duration = this.data.recordingDuration + 100;
|
|||
|
|
this.setData({
|
|||
|
|
recordingDuration: duration
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 检查最大时长
|
|||
|
|
if (duration >= this.properties.maxDuration) {
|
|||
|
|
this.stopRecording();
|
|||
|
|
}
|
|||
|
|
}, 100);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 开始波形动画
|
|||
|
|
startWaveformAnimation() {
|
|||
|
|
this.data.waveformTimer = setInterval(() => {
|
|||
|
|
const waveform = Array(20).fill(0).map(() => Math.random() * 80 + 20);
|
|||
|
|
this.setData({
|
|||
|
|
realtimeWaveform: waveform
|
|||
|
|
});
|
|||
|
|
}, 150);
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 停止定时器
|
|||
|
|
stopTimers() {
|
|||
|
|
if (this.data.durationTimer) {
|
|||
|
|
clearInterval(this.data.durationTimer);
|
|||
|
|
this.data.durationTimer = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (this.data.waveformTimer) {
|
|||
|
|
clearInterval(this.data.waveformTimer);
|
|||
|
|
this.data.waveformTimer = null;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 更新实时波形
|
|||
|
|
updateRealtimeWaveform(frameBuffer) {
|
|||
|
|
if (!frameBuffer) return;
|
|||
|
|
|
|||
|
|
// 简化的波形数据处理
|
|||
|
|
const waveform = Array(20).fill(0).map(() => Math.random() * 80 + 20);
|
|||
|
|
this.setData({
|
|||
|
|
realtimeWaveform: waveform
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 生成最终波形
|
|||
|
|
generateFinalWaveform() {
|
|||
|
|
const duration = this.data.recordingDuration;
|
|||
|
|
const barCount = Math.min(Math.max(Math.floor(duration / 200), 15), 40);
|
|||
|
|
|
|||
|
|
const waveform = Array(barCount).fill(0).map(() => Math.random() * 70 + 15);
|
|||
|
|
this.setData({
|
|||
|
|
finalWaveform: waveform
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 格式化时长
|
|||
|
|
formatDuration(duration) {
|
|||
|
|
if (!duration || duration <= 0) return '00:00';
|
|||
|
|
|
|||
|
|
const totalSeconds = Math.floor(duration / 1000);
|
|||
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|||
|
|
const seconds = totalSeconds % 60;
|
|||
|
|
|
|||
|
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 获取文件大小文本
|
|||
|
|
getFileSizeText(fileSize) {
|
|||
|
|
if (!fileSize || fileSize <= 0) return '';
|
|||
|
|
|
|||
|
|
if (fileSize < 1024) {
|
|||
|
|
return `${fileSize}B`;
|
|||
|
|
} else if (fileSize < 1024 * 1024) {
|
|||
|
|
return `${(fileSize / 1024).toFixed(1)}KB`;
|
|||
|
|
} else {
|
|||
|
|
return `${(fileSize / (1024 * 1024)).toFixed(1)}MB`;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 获取质量文本
|
|||
|
|
getQualityText(duration) {
|
|||
|
|
if (!duration || duration <= 0) return '';
|
|||
|
|
|
|||
|
|
const seconds = Math.floor(duration / 1000);
|
|||
|
|
if (seconds < 3) return '音质:一般';
|
|||
|
|
if (seconds < 10) return '音质:良好';
|
|||
|
|
return '音质:优秀';
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 获取错误消息
|
|||
|
|
getErrorMessage(error) {
|
|||
|
|
if (error.message) {
|
|||
|
|
if (error.message.includes('权限')) {
|
|||
|
|
return '需要录音权限';
|
|||
|
|
} else if (error.message.includes('timeout')) {
|
|||
|
|
return '录音超时';
|
|||
|
|
} else if (error.message.includes('fail')) {
|
|||
|
|
return '录音失败';
|
|||
|
|
}
|
|||
|
|
return error.message;
|
|||
|
|
}
|
|||
|
|
return '未知错误';
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 清理资源
|
|||
|
|
cleanup() {
|
|||
|
|
this.stopTimers();
|
|||
|
|
|
|||
|
|
// 如果正在录音,取消录音
|
|||
|
|
if (this.data.recordingState === 'recording') {
|
|||
|
|
voiceMessageManager.cancelRecording();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果正在播放,停止播放
|
|||
|
|
if (this.data.isPlaying) {
|
|||
|
|
voiceMessageManager.stopPlaying();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移除事件监听器
|
|||
|
|
voiceMessageManager.off('recordStart');
|
|||
|
|
voiceMessageManager.off('recordStop');
|
|||
|
|
voiceMessageManager.off('recordPause');
|
|||
|
|
voiceMessageManager.off('recordResume');
|
|||
|
|
voiceMessageManager.off('recordError');
|
|||
|
|
voiceMessageManager.off('recordFrame');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|