You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

747 lines
20 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<!-- 聊天底部栏组件 -->
<view class="chat-chat-btn-cn">
<!-- 组件内容 -->
<view class="cn-content">
<!-- 底部栏 -->
<!-- :style="{bottom: 'calc(' + keyboardHeight + 'rpx)'}" -->
<view class="jc-footer-safe border-top-ed bg-f7">
<view class="padding-tb-15 flex align-end" :style="{height: bottomBarHight + 'rpx'}">
<!-- 左侧 切换图标 -->
<view class="line-height-70 text-center" style="width: 90rpx;">
<!-- 切换为语音图标 -->
<text class="tyIcon-xingzhuangjiehe text-98 text-50"
v-if="sendType == 1 || sendType == 3 || sendType == 4" @click="sendTypeChange(2)"></text>
<!-- 切换为文本图标 -->
<text class="tyIcon-jianpan1 text-98 text-50" v-if="sendType == 2"
@click="sendTypeChange(1)"></text>
</view>
<!-- 中间 文本框
高度最高展示10行
@keyboardheightchange事件只有微信小程序兼容 @keyboardheightchange="keyboardHeightChange"-->
<textarea class="flex-one padding-lr-20 padding-tb-15 line-40 radius-10 bg-ff"
:style="{height: textareaHighet + 'rpx'}" :show-confirm-bar="false" cursor-spacing="100"
maxlength="-1" v-model="content" :focus="isTextareaFocus" @focus="focus" @blur="blur"
@linechange="textAreaLineChange" @click="sendTypeChange(1)"
v-if="sendType == 1 || sendType == 3 || sendType == 4"></textarea>
<!-- 中间 发送语音按钮 -->
<view class="flex-one line-height-70 radius-10 bg-ff text-center text-28" @touchstart="touchStart"
@touchend="touchEnd" @touchmove="touchMove" v-if="sendType == 2">
{{voiceText}}
</view>
<!-- 右侧 更多发送类型 -->
<view class="padding-lr-20 flex align-center justify-between">
<!-- emoji图标 -->
<view class="margin-right-20 line-height-70" style="width: 50rpx;">
<!-- 切换为表情图标 -->
<text class="tyIcon-biaoqing text-98 text-50" v-if="sendType != 4"
@click="sendTypeChange(4)"></text>
<!-- 切换为文本图标 -->
<text class="tyIcon-jianpan1 text-98 text-50" v-if="sendType == 4"
@click="sendTypeChange(1)"></text>
</view>
<!-- <image v-if="is_need_emoji === true" class="smil" src="/static/smil.png"></image> -->
<!-- 加号图标 选择图片 -->
<view class="line-height-70" style="width: 50rpx;"
v-if="!((sendType == 1 || sendType == 4) && content !== '')" @click="sendTypeChange(3)">
<text class="tyIcon-tianjia text-98 text-50"></text>
</view>
<!-- 发送图标 选择图片 -->
<view class="width-100 line-height-50 radius-6 bg-main text-center text-26 text-ff"
v-if="(sendType == 1 || sendType == 4) && content !== ''" @click="sendMessage(1)">
发送
</view>
</view>
</view>
<!-- 其他未展示类型功能 不是浮层方式 底部栏下方 显示或隐藏-->
<view class="send-type-other bg-f7 border-top-ed" style="padding-top: 78rpx;height: 330rpx;"
v-if="sendType == 3">
<view class="flex align-center flex-wrap justify-around" style="padding: 0 90rpx;">
<view class="other-type-item" @click="chooseImage('album')">
<view class="icon-block">
<text class="tyIcon-tu line-110 text-50 text-4d"></text>
</view>
<view class="margin-top-30 text-24 text-80">相册</view>
</view>
<view class="other-type-item" @click="chooseImage('camera')">
<view class="icon-block">
<text class="tyIcon-paizhao line-110 text-50 text-4d"></text>
</view>
<view class="margin-top-30 text-24 text-80">拍摄</view>
</view>
<view class="other-type-item" @click="chooseLocation()">
<view class="icon-block">
<text class="tyIcon-weizhi line-110 text-50 text-4d"></text>
</view>
<view class="margin-top-30 text-24 text-80">位置</view>
</view>
</view>
</view>
<!-- emoji表情 不是浮层方式 底部栏下方 显示或隐藏 -->
<view class="send-type-other bg-f7 border-top-ed" v-if="sendType == 4">
<ty-utils-emoji-list @emojiAdd="emojiAdd"></ty-utils-emoji-list>
</view>
</view>
</view>
<!-- 组件浮层 -->
<view class="cn-layer">
<!-- 录音中状态,显示浮层 -->
<view v-if="sendType == 2 && recordStatus == 1">
<ty-layer-pop-up :overlay="false">
<view @click.stop="">
<view class="voice-record1">
<image class="jc-image-280" src="../../../static/chat/voice.gif"></image>
</view>
</view>
</ty-layer-pop-up>
</view>
<!-- 录音中状态录音时长剩下不到10s显示浮层 -->
<view v-if="sendType == 2 && recordStatus == 2">
<ty-layer-pop-up :overlay="false">
<view @click.stop="">
<view class="voice-record flex flex-direction align-center">
<view class="text-ff" style="height: 208rpx; line-height: 208rpx; font-size: 160rpx;">
{{maxRecordSeconds-recordSeconds}}
</view>
<view class="text-24 text-ff">手指上滑,取消发送</view>
</view>
</view>
</ty-layer-pop-up>
</view>
<!-- 录音时长太短小于1s -->
<view v-if="sendType == 2 && showSpeechShortLayer">
<ty-layer-pop-up :overlay="false">
<view @click.stop="">
<view class="voice-record flex flex-direction align-center">
<view class="margin-top-40" style="height: 128rpx;">
<text class="tyIcon-zhuyi text-ff"
style="line-height: 128rpx;font-size: 128rpx;"></text>
</view>
<view class="margin-top-40 text-24 text-ff">说话时间太短</view>
</view>
</view>
</ty-layer-pop-up>
</view>
</view>
</view>
</template>
<script>
import record from '@/components/hzjc/utils/functions/record.js'
import image from '@/components/hzjc/utils/functions/image.js'
// #ifdef H5
let jweixin = require('jweixin-module')
// #endif
// #ifdef APP-PLUS|MP-WEIXIN
const recorderManager = uni.getRecorderManager()
// #endif
export default {
props: {
},
data() {
return {
// 底部栏高度
bottomBarHight: 100,
// 文本框高度
textareaHighet: 70,
keyboardHeight: 0,
// 发送方式 1--文字 2--语音 3--其他类型发送浮层 4--emoji表情
sendType: 1,
// 输入文本
content: '',
// 录音文件地址
voiceUrl: '',
// 图片路径
imageUrl: '',
// 输入文本时,输入行数
textLine: 1,
// 输入文本时,文本框是否聚焦
isTextareaFocus: false,
// 录音状态 0--未开始 1--录音中 2--录音时长剩下不到10s
recordStatus: 0,
// 录音时长
recordSeconds: 0,
// 最长录音时长
maxRecordSeconds: 60,
// 显示录音时长
// showRecordSeconds: 0,
// 语音提示文案
voiceText: '按住 说话',
// 是否显示说话太短浮层
showSpeechShortLayer: false,
// 定时器 - 录音每秒倒计时
recordIntervalTimer: '',
// 定时器 - 录音执行延时
recordTimeOutTimer: '',
// 录音开始时间
recordStartTime: '',
// 手势是否移动
isTouchMove: false,
// 移动开始Y轴距离
touchStartY: 0,
// 移动结束Y轴距离
touchEndY: 0,
}
},
created() {
// #ifdef H5
// 公众号
if (this.cn.userAgent() == 'weixin') {
// 监听录音自动停止接口,超过最大录音时间自动停止
jweixin.onVoiceRecordEnd({
success: (res) => {
console.log('--监听到录音自动结束--')
// 录音结束之后数据重置
this.dataResetafterRecordStop()
// 修改录音状态为结束,浮层消失
this.recordStatus = 0
// 当前录音时长为最大数值
this.recordSeconds = this.maxRecordSeconds
// 上传录音文件到云端
record.uploadVoice(res.localId).then(res => {
// 语音文件路径
this.voiceUrl = res
this.sendMessage(2)
})
}
})
}
// #endif
// #ifdef MP-WEIXIN|APP-PLUS
recorderManager.onError((res) => {
console.log('recorderManagerOnError', res)
})
// 录音停止事件,会回调文件地址
recorderManager.onStop((res) => {
console.log('recorderManagerOnStop', res)
/* res格式
{duration: 6647.211074829102, tempFilePath: "wxfile://tmp_b93d40bd1e3d53bcbdf7b88e05924f96.mp3", fileSize: 39921} */
// 录音结束之后数据重置
this.dataResetafterRecordStop()
// 获取录音文件的临时路径
let tempFilePath = res.tempFilePath
// 上传录音文件到OSS
record.uploadRecordFile(tempFilePath).then(res => {
let path = res[0]
// 录音文件
this.voiceUrl = path
// 发送语音消息
if (this.recordSeconds > 0) {
this.sendMessage(2)
}
})
})
// #endif
},
methods: {
/**
* 选择是输入文字还是语音
* @param {int} type 1--文字 2--语音 3--其他 4--表情
* @date 2022-08-10
*/
sendTypeChange(type) {
// 更改发送方式
this.sendType = type
// 底部栏下面的块浮层高度
let otherBlockHeight = 0
// 文字类型
if (this.sendType == 1) {
// 更改底部栏高度 文本框高度+ 上下间距
this.bottomBarHight = this.textLine * 40 + 30 + 30
// 文本框聚焦
this.isTextareaFocus = true
// 非文字类型
} else {
// 文本框固定高度100
this.bottomBarHight = 100
// 文本框取消聚焦
this.isTextareaFocus = false
if (type == 3) {
// +展开 类型高度为330
otherBlockHeight = 330
} else if (type == 4) {
// emoji高度为330
otherBlockHeight = 520
}
}
// 底部栏高度变化事件
this.$emit('bottomHightChange', {
// 底部条高度 + 其他块高度(选择图片、选择表情) + 键盘高度
bottomHight: this.bottomBarHight + otherBlockHeight
})
},
/**
* 输入文本框行高发生变化
* @param {object} e
* @date 2022-08-10
*/
textAreaLineChange(e) {
console.log(8999, e)
this.textLine = 1
// 最多显示10行文本
if (e.detail.lineCount <= 12) {
// if(this.textLine == 2 && e.detail.lineCount == 4){
// this.textLine = 3;
// }else if(this.textLine == 6 && e.detail.lineCount == 8){
// this.textLine = 7;
// }else{
// this.textLine = e.detail.lineCount
// }
if (e.detail.lineCount <= 7 && e.detail.lineCount > 3) {
this.textLine = e.detail.lineCount - 1
} else if (e.detail.lineCount > 7) {
this.textLine = e.detail.lineCount - 2
} else {
this.textLine = e.detail.lineCount
}
} else {
this.textLine = 10
}
// 更改文本框高度 文本高度 + 上下间距
this.textareaHighet = this.textLine * 40 + 30
// 更改底部栏高度 文本框高度+ 上下间距
this.bottomBarHight = this.textLine * 40 + 30 + 30
// 底部栏高度变化事件
if (this.textLine > 1) {
this.$emit('bottomHightChange', {
bottomHight: this.bottomBarHight
})
}
},
/**
* textarea获得焦点
* @date 2022-09-21
*/
focus(e) {
let systemInfo = uni.getSystemInfoSync()
// console.log(systemInfo.safeAreaInsets)
this.keyboardHeight = this.cn.px2rpx(e.detail.height - systemInfo.safeAreaInsets.bottom)
},
/**
* textarea失去焦点
* @date 2022-09-21
*/
blur(e) {
this.keyboardHeight = 0
},
/**
* 开始录音
* @param {object} e
* @date 2022-08-10
*/
touchStart(e) {
// 触发开始录音事件
this.$emit('recordStart')
// 阻止长按默认事件
e.preventDefault()
// 修改展示文案
this.voiceText = '松开 发送'
// 清除可能存在的录音
// record.stopRecord()
// 录音开始时间
this.recordStartTime = new Date().getTime()
// 延迟执行录音事件 防止误点触发录音
this.recordTimeOutTimer = setTimeout(() => {
// 判断授权
record.isAuth().then(res => {
// 没有授权,调用录音接口弹出授权,并立即停止录音
if (!res) {
// #ifdef H5
if (this.cn.userAgent() == 'weixin') {
record.record().then(res => {
record.stopRecord(0).then(res => {
this.voiceText = '按住 说话'
uni.setStorageSync('is_auth_record', true)
})
}).catch(res => {
this.voiceText = '按住 说话'
})
return false
}
// #endif
// #ifdef MP-WEIXIN
this.voiceText = '按住 说话'
return false
// #endif
} else {
/* 修改录音状态为已开始
在开始录音前就修改,防止在回调的过程中触发endVoive被拦截导致录音不能终止 */
this.recordStatus = 1
// 延迟200ms录音避免发生误点的情况
record.record().then(res => {
// 倒计时60s时间
this.recordIntervalTimer = setInterval(() => {
// 录音秒数 +1
this.recordSeconds++
// 录音时长达到60s
if (this.recordSeconds >= this.maxRecordSeconds) {
// 清空读秒定时器
this.recordStatus = 0;
clearInterval(this.recordIntervalTimer)
// that.stopVoiceOrSend(1)
// 录音秒数只剩10秒钟之内
} else if (this.recordSeconds >= this
.maxRecordSeconds - 10) {
// 更改录音状态
this.recordStatus = 2
}
}, 1000)
})
}
})
}, 200)
},
/**
* 手势移动 上滑取消录音
* @param {object} e
* @date 2022-08-11
*/
touchMove: function(e) {
// 录音状态,正在录音时,上滑取消录音
if (this.sendType == 2 && this.recordStatus != 0) {
// 第一次移动抓取
if (!this.isTouchMove) {
// 更改状态为移动中
this.isTouchMove = true
// 记录移动开始Y轴距离
this.touchStartY = e.touches[0].clientY
// 移动过程中移动抓取
} else {
// 实时记录移动Y轴距离更新移动结束Y轴距离
this.touchEndY = e.touches[0].clientY
}
}
},
/**
* 结束录音
* @param {object} e
* @date 2022-08-10
*/
touchEnd(e) {
// 手势移动距离
let touchMoveDistance = this.touchStartY - this.touchEndY
// 录音结束之后数据重置
this.dataResetafterRecordStop()
// 300ms以内的误点事件处理
if (new Date().getTime() - this.recordStartTime < 300) {
// 清除延迟执行定时
clearTimeout(this.recordTimeOutTimer)
return false
}
// 如果有当前为未录音状态,一般为自动监听到录音结束事件
if (this.recordStatus == 0) {
return false
}
// 修改录音状态为结束,浮层消失
this.recordStatus = 0
// 向上滑动超过屏幕的五分之一则为取消录音
const res = uni.getSystemInfoSync()
if (touchMoveDistance > parseFloat(res.windowHeight) / 5) {
// 停止录音
record.stopRecord(0)
// 录音秒数归0
this.recordSeconds = 0
return false
}
// 录音时长小于1s
if (this.recordSeconds < 1) {
// 停止录音
record.stopRecord(0)
// 录音秒数归0
this.recordSeconds = 0
// 显示说话太短浮层
this.showSpeechShortLayer = true
setTimeout(() => {
// 1s后关闭说话太短浮层
this.showSpeechShortLayer = false
}, 1000)
return false
}
// 结束录音并上传至云端
record.stopRecord().then(res => {
// #ifdef H5
if (this.cn.userAgent() == 'weixin') {
// 录音文件
this.voiceUrl = res
// 发送语音消息
this.sendMessage(2)
}
// #endif
})
},
/**
* 选择图片
* @param {string} type camera--使用相机 album--从相册选图
* @date 2022-08-14
*/
chooseImage: function(type) {
let sourceType = [type]
image.chooseImageUpload(1, sourceType).then(res => {
this.imageUrl = res[0]
// 发送消息
this.sendMessage(3)
})
},
/**
* 选择emoji表情
* @param {Object} e
* @date 2022-08-10
*/
emojiAdd(e) {
// 文本追加emoji表情
this.content += e.alt
},
/**
* 打开地图选择位置
* @date 2022-09-21
*/
chooseLocation() {
uni.chooseLocation({
success: (res) => {
console.log('chooseLocationSuccess', res)
/* 发送位置
attach携带数据 name--位置名称 address--详细地址 latitude--纬度 longitude--经度 */
this.sendMessage(4, {
name: res.name,
address: res.address,
lat: res.latitude,
lng: res.longitude
})
},
fail: (res) => {
console.log('chooseLocationFail', res)
}
})
},
//------------------------------ 以下为封装方法--------------------
/**
* 结束录音数据重置
* 2022-08-12
*/
dataResetafterRecordStop() {
// 更改语音提示文案
this.voiceText = '按住 说话'
// 清空计时器
clearInterval(this.recordIntervalTimer)
// 修改移动状态为未移动
this.isTouchMove = false
// 修改移动起始和结束Y轴距离为0
this.touchStartY = 0
this.touchEndY = 0
},
/**
* 发送信息
* @param {int} messageType 1--文本 2--语音 3--图片
* @date 2022-08-10
*/
sendMessage(messageType, attach = {}) {
switch (messageType) {
// 发送文本消息
case 1:
// 去除2边空格之后没有消息
if (this.content.trim().length < 1) {
this.cn.alert('不能发送空白消息')
return false
// 消息字数过多
} else if (this.content.length > 2000) {
this.cn.alert('发送字数要求在2000字以内')
return false
}
let content = this.content
// 发送成功之后清空文本框
this.content = ''
// 向外触发事件
this.$emit('sendMessage', {
message_type: 1,
content: content
})
break;
// 发送语音消息
case 2:
let recordSeconds = this.recordSeconds
console.log(recordSeconds)
let voiceUrl = this.voiceUrl
// 发送成功之后清空数据
this.voiceUrl = ''
this.recordSeconds = 0
// 向外触发事件
this.$emit('sendMessage', {
message_type: 2,
content: voiceUrl,
attach: {
recordSeconds: recordSeconds
}
})
break;
// 发送图片消息
case 3:
let imageUrl = this.imageUrl
// 发送成功之后清空数据
this.imageUrl = ''
// 向外触发事件
this.$emit('sendMessage', {
message_type: 3,
content: imageUrl
})
break;
case 4:
// 向外触发事件
this.$emit('sendMessage', {
message_type: 4,
content: attach.name,
attach: {
name: attach.name,
address: attach.address,
lat: attach.lat,
lng: attach.lng
}
})
break;
default:
break;
}
},
}
}
</script>
<style scoped>
/* 底部栏 */
.bottom-bar {
position: fixed;
left: 0;
padding-bottom: env(safe-area-inset-bottom);
z-index: 998;
width: 100%;
box-sizing: border-box;
}
/* 录音中状态,显示浮层 */
.voice-record1 {
position: fixed;
top: calc(50vh - 140rpx);
left: calc(50vw - 140rpx);
height: 280rpx;
width: 280rpx;
}
.voice-record {
position: fixed;
top: calc(50vh - 140rpx);
left: calc(50vw - 140rpx);
height: 280rpx;
width: 280rpx;
background: rgba(0, 0, 0, .5);
border-radius: 10rpx;
}
.other-type-item {
display: flex;
flex-direction: column;
align-items: center;
}
.other-type-item .icon-block {
display: block;
width: 110rpx;
height: 110rpx;
border-radius: 10rpx;
background: #fff;
text-align: center;
}
</style>