381 lines
8.6 KiB
Vue
381 lines
8.6 KiB
Vue
<template>
|
|
<view class="voice-mask" v-show="showVoiceMask">
|
|
<!--语音条 -->
|
|
<view
|
|
class="voice-bar voice-del"
|
|
:class="{ voiceDel: needCancel }"
|
|
:style="{ width: getVoiceBarWidth }"
|
|
>
|
|
<image
|
|
src="../../static/images/chat/yinping.png"
|
|
class="voice-volume"
|
|
></image>
|
|
<view class="trangle-bottom" :class="{ trangleDel: needCancel }"></view>
|
|
</view>
|
|
<view class="voice-send">
|
|
<!-- 取消和转文字图标 -->
|
|
<view class="voice-middle-wrapper">
|
|
<!-- 取消 -->
|
|
<view class="voice-left-wrapper">
|
|
<view class="cancel-del" :class="{ delTip: needCancel }">
|
|
松开 取消
|
|
</view>
|
|
<view
|
|
class="voice-middle-inner close"
|
|
:class="{ bigger: needCancel }"
|
|
>
|
|
<u-icon name="close" color="#fff" size="24"></u-icon>
|
|
</view>
|
|
</view>
|
|
<!-- 转文字 -->
|
|
<view class="send-tip" :class="{ sendTipNone: needCancel }">
|
|
松开 发送
|
|
</view>
|
|
</view>
|
|
<!-- 底部语音按钮 -->
|
|
<view
|
|
ref="maskBottom"
|
|
class="mask-bottom"
|
|
@touchstart="handleTouchStart"
|
|
@touchmove="handleTouchMove"
|
|
@touchend="handleTouchEnd"
|
|
>
|
|
<image src="../../static/images/chat/voice.png"></image>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
const recorderManager = uni.getRecorderManager();
|
|
export default {
|
|
props: {
|
|
showVoiceMask: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
voicePopupTouches: {
|
|
type: Object,
|
|
default: {},
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
voicelength: 0,
|
|
needCancel: false,
|
|
startX: 0,
|
|
startY: 0,
|
|
timer: 0,
|
|
maskBottomRect: null,
|
|
isRecording: false,
|
|
isOutside: false,
|
|
tempFilePath: null,
|
|
startTime: 0,
|
|
isOutside: false,
|
|
};
|
|
},
|
|
computed: {
|
|
// 计算语音条宽度
|
|
getVoiceBarWidth() {
|
|
return 230 + this.voicelength * 4 + "rpx";
|
|
},
|
|
},
|
|
watch: {
|
|
showVoiceMask: {
|
|
handler: function (val, oldVal) {
|
|
if (val) {
|
|
console.log("showVoicePopupTouches", this.voicePopupTouches);
|
|
|
|
console.log(this.$refs["maskBottom"].$el.dispatchEvent);
|
|
const mockEvent = new TouchEvent("touchstart", {
|
|
touches: [{ pageX: 100, pageY: 100 }], // 模拟触摸坐标
|
|
});
|
|
|
|
this.$refs["maskBottom"].$el.dispatchEvent(mockEvent);
|
|
this.handleTouchStart(this.voicePopupTouches);
|
|
}
|
|
},
|
|
},
|
|
},
|
|
created() {
|
|
// 初始化录音管理器
|
|
recorderManager.onStart(() => {
|
|
console.log("录音开始");
|
|
this.isRecording = true;
|
|
this.startTime = Date.now();
|
|
});
|
|
|
|
recorderManager.onPause(() => {
|
|
console.log("录音暂停");
|
|
this.isRecording = false;
|
|
});
|
|
|
|
recorderManager.onResume(() => {
|
|
console.log("录音继续");
|
|
this.isRecording = true;
|
|
});
|
|
},
|
|
methods: {
|
|
async getMaskBottomRect() {
|
|
return new Promise((resolve) => {
|
|
const query = uni.createSelectorQuery().in(this);
|
|
query
|
|
.select(".mask-bottom")
|
|
.boundingClientRect((data) => {
|
|
this.maskBottomRect = data;
|
|
console.log("元素位置信息:", data); // 调试用
|
|
resolve(data);
|
|
})
|
|
.exec();
|
|
});
|
|
},
|
|
|
|
async handleTouchStart(e) {
|
|
console.log("handleTouchStart", e);
|
|
|
|
if (!this.maskBottomRect) {
|
|
await this.getMaskBottomRect();
|
|
}
|
|
|
|
recorderManager.start();
|
|
this.length = 1;
|
|
this.startX = e.touches[0].pageX;
|
|
this.startY = e.touches[0].pageY;
|
|
this.timer = setInterval(() => {
|
|
this.length += 1;
|
|
if (this.length >= 60) {
|
|
clearInterval(this.timer);
|
|
this.handleTouchEnd();
|
|
}
|
|
}, 1000);
|
|
this.isRecording = true;
|
|
this.isOutside = false;
|
|
},
|
|
|
|
handleTouchMove(e) {
|
|
// 检查是否在mask-bottom区域外
|
|
if (!this.maskBottomRect) return;
|
|
|
|
const touchX = e.touches[0].pageX;
|
|
const touchY = e.touches[0].pageY;
|
|
|
|
// 检查是否在mask-bottom区域外
|
|
// 更精确的边界检查
|
|
const SAFE_MARGIN = 5; // 像素单位
|
|
const isNowOutside =
|
|
touchX < this.maskBottomRect.left - SAFE_MARGIN ||
|
|
touchX >
|
|
this.maskBottomRect.left + this.maskBottomRect.width + SAFE_MARGIN ||
|
|
touchY < this.maskBottomRect.top - SAFE_MARGIN ||
|
|
touchY >
|
|
this.maskBottomRect.top + this.maskBottomRect.height + SAFE_MARGIN;
|
|
|
|
// 调试输出
|
|
console.log(
|
|
`触摸点: (${touchX}, ${touchY})`,
|
|
`元素区域: [${this.maskBottomRect.left}, ${this.maskBottomRect.top}, ${this.maskBottomRect.width}, ${this.maskBottomRect.height}]`,
|
|
`是否在外: ${isNowOutside}`
|
|
);
|
|
|
|
// 如果状态发生变化(进入或离开区域)
|
|
if (isNowOutside !== this.isOutside) {
|
|
this.isOutside = isNowOutside;
|
|
|
|
if (this.isOutside) {
|
|
// 离开区域,暂停录制
|
|
recorderManager.pause();
|
|
console.log("已离开区域,暂停录制");
|
|
} else {
|
|
// 回到区域,继续录制
|
|
recorderManager.resume();
|
|
console.log("已回到区域,继续录制");
|
|
}
|
|
}
|
|
|
|
// 原有的取消逻辑
|
|
if (this.startX - touchX > 14 && this.startY - touchY > 50) {
|
|
this.needCancel = true;
|
|
} else {
|
|
this.needCancel = false;
|
|
}
|
|
},
|
|
|
|
handleTouchEnd(e) {
|
|
clearInterval(this.timer);
|
|
recorderManager.stop();
|
|
recorderManager.onStop((res) => {
|
|
const message = {
|
|
voice: res.tempFilePath,
|
|
length: this.length,
|
|
};
|
|
if (!this.needCancel) {
|
|
this.inputSubmit(message, 2);
|
|
} else {
|
|
this.inputSubmit();
|
|
}
|
|
this.needCancel = false;
|
|
});
|
|
},
|
|
|
|
inputSubmit(msg, types) {
|
|
if (msg && types) {
|
|
this.$emit("voiceEnd", msg, types);
|
|
} else {
|
|
this.$emit("voiceEnd");
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
.voice-mask {
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
background-color: rgba(0, 0, 0, 0.8);
|
|
z-index: 9999; /* 确保足够高 */
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.voice-bar {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
transform: translate(-50%, -30%);
|
|
height: 150rpx;
|
|
background-color: #51ff50;
|
|
border-radius: 26rpx;
|
|
margin-bottom: 220rpx;
|
|
z-index: 9999;
|
|
}
|
|
.voiceDel {
|
|
left: 80rpx;
|
|
top: 50%;
|
|
width: 170rpx !important;
|
|
transform: translateX(0%);
|
|
transform: translateY(-30%);
|
|
background-color: red;
|
|
}
|
|
.voice-volume {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 160rpx;
|
|
height: 36rpx;
|
|
}
|
|
.volumeDel {
|
|
width: 80rpx;
|
|
}
|
|
.trangle-bottom {
|
|
position: absolute;
|
|
bottom: -38rpx;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
border-width: 20rpx;
|
|
border-style: solid;
|
|
border-color: #51ff50 transparent transparent transparent;
|
|
z-index: 9999;
|
|
}
|
|
.trangleDel {
|
|
border-color: red transparent transparent transparent;
|
|
}
|
|
.voice-send {
|
|
position: relative;
|
|
bottom: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 9999;
|
|
}
|
|
.voice-middle-wrapper {
|
|
width: 100%;
|
|
display: flex;
|
|
position: absolute;
|
|
justify-content: space-between;
|
|
align-items: flex-end;
|
|
margin-bottom: 40rpx;
|
|
bottom: 15%;
|
|
}
|
|
.voice-left-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: flex-end;
|
|
}
|
|
.cancel-del {
|
|
display: none;
|
|
}
|
|
.delTip {
|
|
display: block;
|
|
color: #bfbfbf;
|
|
margin: 0 22rpx 18rpx 0;
|
|
}
|
|
.voice-middle-inner {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
background-color: #595959;
|
|
width: 140rpx;
|
|
height: 140rpx;
|
|
border-radius: 50%;
|
|
}
|
|
.close {
|
|
transform: rotate(350deg);
|
|
margin-left: 80rpx;
|
|
}
|
|
.bigger {
|
|
width: 170rpx;
|
|
height: 170rpx;
|
|
background: #ececec;
|
|
}
|
|
.to-text {
|
|
transform: rotate(10deg);
|
|
margin-right: 80rpx;
|
|
}
|
|
.close-icon {
|
|
width: 80rpx;
|
|
height: 80rpx;
|
|
color: #fff;
|
|
}
|
|
.wen {
|
|
font-size: 40rpx;
|
|
color: #bfbfbf;
|
|
}
|
|
.send-tip {
|
|
position: absolute;
|
|
left: 50%;
|
|
bottom: 0rpx;
|
|
transform: translate(-50%, 36%);
|
|
color: #bfbfbf;
|
|
}
|
|
.sendTipNone {
|
|
display: none;
|
|
}
|
|
.mask-bottom {
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 100%;
|
|
height: 180rpx;
|
|
border-top: #bababb 8rpx solid;
|
|
border-radius: 50% 50% 0 0;
|
|
background-image: linear-gradient(#949794, #e1e3e1);
|
|
z-index: 9999;
|
|
}
|
|
.mask-bottom image {
|
|
position: absolute;
|
|
width: 60rpx;
|
|
height: 60rpx;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
margin: auto;
|
|
}
|
|
</style>
|