wordpress0商业网站,门户网站建设内,全军采购信息招标网,seo白帽和黑帽的区别有一个需求需要在微信小程序上实现一个长按时进行语音录制#xff0c;录制时间最大为60秒#xff0c;录制完成后#xff0c;可点击播放#xff0c;播放时再次点击停止播放#xff0c;可以反复录制#xff0c;新录制的语音把之前的语音覆盖掉#xff0c;也可以主动长按删…有一个需求需要在微信小程序上实现一个长按时进行语音录制录制时间最大为60秒录制完成后可点击播放播放时再次点击停止播放可以反复录制新录制的语音把之前的语音覆盖掉也可以主动长按删除
// index.js
const recorderManager wx.getRecorderManager()
const innerAudioContext wx.createInnerAudioContext()
let recordingTimerInterval null // 录音时长计时器
let countdownTimerInterval null // 倒计时计时器
let playbackCountdownInterval null // 播放倒计时计时器Page({
/*** 页面的初始数据*/data: {// 语音输入部分inputType: input,count: null, // 录制倒计时longPress: 1, // 1显示 按住说话 2显示 说话中delShow: false, // 删除提示框显示隐藏time: 0, // 录音时长recordedDuration: 0, // 已录制音频的时长duration: 60000, // 录音最大值ms 60000/1分钟tempFilePath: , //音频路径playStatus: 0, //录音播放状态 0:未播放 1:正在播放currentTime: 0, // 当前播放进度(秒)remain: 0, // 当前剩余时长(秒) duration - currentTimewarningShown: false, // 是否已显示50秒提示minDuration: 2, // 录音最小时长秒数animationArray: Array.from({ length: 15 }, (_, index) {// length 这个名字就不再需要因为我们已经在这里写死了 15const centerIndex Math.floor((15 - 1) / 2) // 7const distance Math.abs(index - centerIndex)// 中心延迟为 0向外越来越大const delay distance * 0.2return { delay }})},/*** 开始录音倒计时* param {number} val - 倒计时秒数*/startCountdown(val) {this.setData({count: Number(val)})countdownTimerInterval setInterval(() {if (this.data.count 0) {this.setData({count: this.data.count - 1})} else {this.setData({longPress: 1})clearInterval(countdownTimerInterval)countdownTimerInterval null}}, 1000)},/*** 开始录音时长计时*/startRecordingTimer() {if (recordingTimerInterval) return // 防止重复启动计时器recordingTimerInterval setInterval(() {this.setData({time: this.data.time 1})// 当录音时长达到50秒且未显示提示时显示提示if (this.data.time 50 !this.data.warningShown) {wx.showToast({title: 录音即将结束,icon: none,duration: 2000})this.setData({warningShown: true})}// 如果录音时长达到最大值自动停止录音if (this.data.time this.data.duration / 1000) {wx.showToast({title: 录音已达到最大时长,icon: none})this.touchendBtn()}}, 1000)},/*** 停止录音时长计时* param {string} newTempFilePath - 新录音的文件路径*/stopRecordingTimer(newTempFilePath) {if (recordingTimerInterval) {clearInterval(recordingTimerInterval)recordingTimerInterval null}const duration this.data.timeif (duration this.data.minDuration) {this.setData({recordedDuration: duration,tempFilePath: newTempFilePath},() {console.log(录音已停止时长:, this.data.recordedDuration, 秒)})} else {// 录音时长过短提示用户wx.showToast({title: 录音时间太短,icon: none})// 不覆盖之前的 tempFilePath保留旧的录音// 仅重置 timethis.setData({time: 0},() {console.log(录音时间太短不保存此次录音。)})}},/*** 开始播放倒计时* param {number} val - 播放倒计时秒数*/startPlaybackCountdown(val) {// 先停止可能存在的旧计时器if (playbackCountdownInterval) {clearInterval(playbackCountdownInterval)playbackCountdownInterval null}this.setData({count: Number(val)})playbackCountdownInterval setInterval(() {if (this.data.count 0) {this.setData({count: this.data.count - 1})} else {// 播放结束this.setData({playStatus: 0,count: null})innerAudioContext.stop()clearInterval(playbackCountdownInterval)playbackCountdownInterval null}}, 1000)},/*** 停止播放倒计时*/stopPlaybackCountdown() {if (playbackCountdownInterval) {clearInterval(playbackCountdownInterval)playbackCountdownInterval null}this.setData({count: null})},/*** 清除所有计时器*/clearAllTimers() {if (recordingTimerInterval) {clearInterval(recordingTimerInterval)recordingTimerInterval null}if (countdownTimerInterval) {clearInterval(countdownTimerInterval)countdownTimerInterval null}if (playbackCountdownInterval) {clearInterval(playbackCountdownInterval)playbackCountdownInterval null}},/*** 重置录音状态*/resetRecordingState() {this.setData({longPress: 1,time: 0,recordedDuration: 0,count: null,warningShown: false // 重置警告提示})this.stopRecordingTimer()this.stopCountdownTimer()},/*** 处理输入类型变化* param {object} e - 事件对象*/handleChangeInputType(e) {const { type } e.currentTarget.datasetthis.setData({inputType: type})},/*** 检查录音权限*/checkRecordPermission() {wx.getSetting({success: res {if (!res.authSetting[scope.record]) {// 没有录音权限尝试授权wx.authorize({scope: scope.record,success: () {// 授权成功可以开始录音this.startRecording()},fail: () {// 授权失败提示用户前往设置授权wx.showModal({title: 授权提示,content: 录音权限未授权请前往设置授权,success: res {if (res.confirm) {wx.openSetting()}}})}})} else {// 已经授权可以开始录音this.startRecording()}},fail: () {// 获取设置失败提示用户wx.showToast({title: 获取权限失败请重试,icon: none})}})},/*** 开始录音的封装函数*/startRecording() {this.setData({longPress: 2,time: 0, // 在开始录音前重置 timewarningShown: false // 重置警告提示})this.startCountdown(this.data.duration / 1000) // 录音倒计时60秒//recorderManager.stop() // 确保之前的录音已停止this.startRecordingTimer()const options {duration: this.data.duration * 1000, // 指定录音的时长单位 mssampleRate: 16000, // 采样率numberOfChannels: 1, // 录音通道数encodeBitRate: 96000, // 编码码率format: mp3, // 音频格式有效值 aac/mp3frameSize: 10 // 指定帧大小单位 KB}recorderManager.start(options)},/*** 长按录音事件*/longpressBtn() {this.checkRecordPermission()},/*** 长按松开录音事件*/touchendBtn() {this.setData({longPress: 1})recorderManager.stop()this.stopCountdownTimer()},/*** 停止倒计时计时器*/stopCountdownTimer() {if (countdownTimerInterval) {clearInterval(countdownTimerInterval)countdownTimerInterval null}this.setData({count: null})},/*** 播放录音*/playBtn() {if (!this.data.tempFilePath) {wx.showToast({title: 没有录音文件,icon: none})return}// 如果已经在播放就先停止if (this.data.playStatus 1) {innerAudioContext.stop()// 重置状态this.setData({playStatus: 0,currentTime: 0,remain: 0})} else {// 重新开始播放console.log(开始播放, this.data.tempFilePath)innerAudioContext.src this.data.tempFilePath// 在 iOS 下即使系统静音也能播放音频innerAudioContext.obeyMuteSwitch false// 播放innerAudioContext.play()// playStatus 会在 onPlay 中置为 1// 如果想在点击之后就立即把 playStatus 置为 1 也行}},/*** 生命周期函数--监听页面加载*/onLoad(options) {// 绑定录音停止事件recorderManager.onStop(res {// 将新录音的文件路径传递给 stopRecordingTimerthis.stopRecordingTimer(res.tempFilePath)console.log(录音已停止文件路径:, res.tempFilePath)console.log(录音时长:, this.data.recordedDuration, 秒)})// 绑定录音开始事件recorderManager.onStart(res {console.log(录音开始, res)})// 绑定录音错误事件recorderManager.onError(err {console.error(录音错误:, err)wx.showToast({title: 录音失败请重试,icon: none})this.resetRecordingState()})// 当音频真正开始播放时innerAudioContext.onPlay(() {console.log(onPlay 音频开始播放)// 设置为播放状态this.setData({playStatus: 1})})// 绑定音频播放结束事件innerAudioContext.onEnded(() {console.log(onEnded 音频播放结束)// 停止播放并重置this.setData({playStatus: 0,currentTime: 0,remain: 0})// 如果想让界面上回到音频的总时长也可以手动 set remain recordedDuration// 但通常播放结束就显示 0 或不显示都行})innerAudioContext.onTimeUpdate(() {const current Math.floor(innerAudioContext.currentTime) // 取整或保留小数都可const total Math.floor(innerAudioContext.duration)// 若 total 不准确(部分手机可能最初获取到是 0)可做一些保护if (total 0) {const remain total - currentthis.setData({currentTime: current,remain: remain 0 ? remain : 0})}})// 绑定音频播放错误事件innerAudioContext.onError(err {console.error(播放错误:, err)wx.showToast({title: 播放失败请重试,icon: none})this.setData({playStatus: 0,currentTime: 0,remain: 0})})},/*** 生命周期函数--监听页面卸载*/onUnload() {this.clearAllTimers()recorderManager.stop()innerAudioContext.stop()innerAudioContext.destroy()},
})// index.wxml
view wx:else classvoice-inputview wx:if{{tempFilePath ! }} classvoice-msg bind:tapplayBtnimagesrc{{ playStatus 0 ? /sendingaudio.png : /voice.gif }}modeaspectFillstyletransform: rotate(180deg); width: 22rpx; height: 32rpx/text classvoice-msg-text{{ playStatus 1 ? (remain ) : (recordedDuration ) }}/text/viewviewclassvoice-input-btn {{longPress 1 ? : record-btn-2}}bind:longpresslongpressBtnbind:touchendtouchendBtn!-- 语音音阶动画 --view classprompt-layer prompt-layer-1 wx:if{{longPress 2}}!-- view classprompt-layer prompt-layer-1 wx:if{{longPress 2}} --view classprompt-loaderviewclassemwx:for{{animationArray}}wx:keyindexstyle--delay: {{item.delay}}s;/view/viewtext classp{{剩余 count s (warningShown ? 即将结束录音 : )}}/texttext classspan松手结束录音/text/viewtext classvoice-input-btn-text{{longPress 1 ? 按住 说话 : 说话中...}}/text/view/view/* index.wxss */
.voice-btn {box-sizing: border-box;padding: 6rpx 16rpx;background: #2197ee;border-radius: 28rpx;display: flex;align-items: center;justify-content: center;gap: 10rpx;
}.voice-text {line-height: 42rpx;color: #ffffff;font-size: 30rpx;
}.voice-input {box-sizing: border-box;display: flex;flex-direction: column;padding: 30rpx 76rpx;
}.voice-msg {width: 100%;height: 56rpx;background: #95ec69;border-radius: 10rpx;box-shadow: 0 3rpx 6rpx rgba(0, 0, 0, 0.13);margin-bottom: 26rpx;box-sizing: border-box;padding: 0 20rpx;display: flex;align-items: center;gap: 16rpx;
}.voice-msg-text {color: #000000;font-size: 30rpx;line-height: 56rpx;
}.voice-input-btn {width: 100%;box-sizing: border-box;padding: 12rpx 0;background: #ffffff;border: 2rpx solid;border-color: #1f75e3;border-radius: 8rpx;box-sizing: border-box;text-align: center;position: relative;
}.voice-input-btn-text {color: #1f75e3;font-size: 36rpx;line-height: 50rpx;
}/* 提示小弹窗 */
.prompt-layer {border-radius: 16rpx;background: #2197ee;padding: 16rpx 32rpx;box-sizing: border-box;position: absolute;left: 50%;transform: translateX(-50%);
}.prompt-layer::after {content: ;display: block;border: 12rpx solid rgba(0, 0, 0, 0);border-top-color: #2197ee;position: absolute;bottom: -20rpx;left: 50%;transform: translateX(-50%);
}.prompt-layer-1 {font-size: 32rpx;width: 80%;text-align: center;display: flex;flex-direction: column;align-items: center;justify-content: center;top: -178rpx;
}
.prompt-layer-1 .p {color: #ffffff;
}
.prompt-layer-1 .span {color: rgba(255, 255, 255, 0.6);
}/* 语音音阶------------- */
/* 容器样式 */
.prompt-loader {width: 250rpx;height: 40rpx;display: flex;align-items: center; /* 对齐到容器底部 */justify-content: space-between;margin-bottom: 12rpx;
}/* 音阶条样式 */
.prompt-loader .em {background: #ffffff;width: 6rpx;border-radius: 6rpx;height: 40rpx;margin-right: 5rpx;/* 通用动画属性 */animation: load 2.5s infinite linear;animation-delay: var(--delay);will-change: transform;transform-origin: center
}/* 移除最后一个元素的右边距 */
.prompt-loader .em:last-child {margin-right: 0;
}/* 动画关键帧 */
keyframes load {0% {transform: scaleY(1);}50% {transform: scaleY(0.1);}100% {transform: scaleY(1);}
}
.record-btn-2 {background-color: rgba(33, 151, 238, 0.2);
}