Files
jurong_circle_front_app/pages/message/chat.vue
Sun_sun 22c4b5d4d5 2025-09-29
聊天图片上传
录音修改
2025-09-29 14:42:41 +08:00

661 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="chat-container">
<u-navbar title="聊天" id="navBarId" :background="{background: 'transparent' }" :border-bottom="false"
back-icon-color="#000" title-color="#000">
<template v-slot:right>
<image class="collection" src="/static/icon/Bookmark.png" mode=""></image>
</template>
</u-navbar>
<view class="bottom-view" id="bottomId">
<!-- 麦克风/文本切换 -->
<view class="icon u-m-r-10" @click="handleChangeMic">
<image src="/static/icon/Mic.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<textarea v-if="!showMic" name="" v-model="text" class="text" maxlength="3000"></textarea>
<u-button v-else class="btn-mic" @click="handleSay">点击说话</u-button>
<!-- 表情包 -->
<view v-show="text==null || text==''" class="icon u-m-r-10 u-m-l-10">
<image src="/static/icon/expression.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<!-- 文本域展开 -->
<view v-show="text!=null && text!=''" class="icon u-m-r-10 u-m-l-10" @click="handleMagnify">
<image src="/static/icon/big.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<!-- 相机 -->
<view v-show="text==null || text==''" class="icon" @click="handleCamera">
<image src="/static/icon/Camera.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
<!-- 文本发送 -->
<view v-show="text!=null && text!=''" class="icon u-m-r-10 u-m-l-10" @click="handleSendText">
<image src="/static/icon/send.png" mode="" style="width: 100%;height: 100%;"></image>
</view>
</view>
<scroll-view :style="'height:'+scrollHeight+'px'" scroll-y="true" class="scroll-main" :scroll-top="scrollTop"
:scroll-with-animation="true">
<view id="scroll-main">
<view v-for="(item, index) in dataList">
<view v-if="item.createId==userId" class="my-message">
<view class="msg-main">
<view class="msg-content" v-if="item.type=='text'">
<view class="text">
{{item.content}}
</view>
</view>
<view class="mp3-content" v-if="item.type=='mp3'">
<view class="mp3" @click="handlePlayMp3(item.content)">
<view class="mp3-icon">
<image style="width: 100%;height: 100%;" src="/static/icon/listen.png" mode="">
</image>
</view>
点击播放
</view>
</view>
<view class="img-content" v-if="item.type=='img'">
<u-image @click="handlePreView(item.content)" height="100%" :src="item.content"
:lazy-load="true" mode="heightFix"></u-image>
</view>
<!-- 头像 -->
<view class="msg-avatar">
<image style="width: 100%;height: 100%;" :src="getImageUrl(item.userInfo.avatar)"
mode="">
</image>
</view>
</view>
</view>
<view v-else class="other-message">
<view class="msg-main">
<view class="msg-avatar">
<image style="width: 100%;height: 100%;" :src="getImageUrl(item.userInfo.avatar)"
mode="">
</image>
</view>
<view class="msg-content">
<view class="text">
{{item.content}}
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<u-popup v-model="showLongText" mode="bottom" height="80%">
<view class="long-text">
<u-input v-model="text" type="textarea" maxlength="3000" />
</view>
</u-popup>
<u-mask :show="showMask" @click="handleStopMic" blur="10">
<view class="mask-warp">
<view class="mask-text">
<template v-if="!showMaskBtn">
正在说话...点击结束
</template>
<template v-else>
<u-image :fade="false" width="50" height="50"
:src="isPlayRecord?'/static/icon/record_ing.png':'/static/icon/record_play.png'"
:show-loading="false"></u-image>{{isPlayRecord?'播放中...':'点击播放录音'}}
</template>
</view>
<view v-if="showMaskBtn" class="mask-btn">
<u-button class="btn" @click.stop="handleCancel">取消</u-button>
<u-button class="btn" @click.stop="handleSendMp3">发送</u-button>
</view>
</view>
</u-mask>
<u-mask :show="showPreMask" @click="showPreMask=false" blur="10">
<u-image width="100%" class="mask-pre-img" :src="preImg" :lazy-load="true" mode="aspectFit"></u-image>
</u-mask>
<u-toast ref="msgToast" duration="6000" />
</view>
</template>
<script setup lang="ts">
import { onMounted, ref, getCurrentInstance, onBeforeMount, nextTick } from 'vue';
import {
onLoad, onLaunch
} from "@dcloudio/uni-app";
import io from '@hyoga/uni-socket.io';
import { getImageUrl } from '../../util/common';
import { permissionUtil } from '@/uni_modules/colorful-uni-perm';
const navBarRef = ref()
const instance = getCurrentInstance();
const scrollHeight = ref(0)
const loadHeight = () => {
uni.getSystemInfo({
success(res) {
let screenHeight = res.screenHeight
uni.createSelectorQuery().in(instance.proxy).select("#navBarId").boundingClientRect((data : any) => {
scrollHeight.value = screenHeight - data.height
}).exec()
uni.createSelectorQuery().in(instance.proxy).select("#bottomId").boundingClientRect((data : any) => {
scrollHeight.value = scrollHeight.value - data.height
}).exec()
}
})
}
const dataList = ref([])
const mockData = () => {
for (var i = 0; i < 5; i++) {
if (i % 2 == 0) {
dataList.value.push({
"createId": 3641,
"groupId": "1",
"content": "即使在没有空格的地方也 ",
"type": "text",
"messageId": 44,
"createTime": "2025-09-27T18:32:42.000Z",
"userInfo": {
"id": 9958,
"username": "15867461647",
"userType": "user",
"isSystemAccount": 0,
"avatar": "/uploads/documents/1753833656669_524106424.jpg"
}
})
} else {
dataList.value.push({
"createId": 9955,
"groupId": "1",
"content": "2222",
"type": "text",
"messageId": 44,
"createTime": "2025-09-27T18:32:42.000Z",
"userInfo": {
"id": 9958,
"username": "15867461647",
"userType": "user",
"isSystemAccount": 0,
"avatar": "/uploads/documents/1753833656669_524106424.jpg"
}
})
}
dataList.value.push(
{
"createId": 3641,
"groupId": "5",
"content": "http://114.55.111.44:9000/jurongquan/app_records/1759049412254.mp3",
"type": "mp3",
"messageId": 56,
"createTime": "2025-09-28T00:50:16.000Z",
"userInfo": {
"id": 3641,
"username": "15867461617",
"userType": "agent",
"isSystemAccount": 0,
"avatar": "/uploads/documents/1753833656669_524106424.jpg"
}
})
dataList.value.push(
{
"createId": 3641,
"groupId": "5",
"content": "http://114.55.111.44:9000/jurongquan/app_records/1759110192420_Screenshot_20250929_084636_com.bilibili.star.bili.jpg",
"type": "img",
"messageId": 62,
"createTime": "2025-09-28T17:43:13.000Z",
"userInfo": {
"id": 3641,
"username": "15867461617",
"userType": "agent",
"isSystemAccount": 0,
"avatar": "/uploads/documents/1753833656669_524106424.jpg"
}
})
}
}
// 发送消息
const text = ref('')
const showLongText = ref(false)
const handleSendText = () => {
uni.hideKeyboard()
socket.value.emit('clientMsg', {
createId: userId.value,
groupId: groupId.value,
content: text.value,
type: "text"
})
text.value = ''
}
// 文本放大
const handleMagnify = () => {
showLongText.value = true
}
// 录音
const showMic = ref(false)
const showMask = ref(false)
const showMaskBtn = ref(false)
const msgToast = ref(null)
const voicePath = ref('')
const recorderManager = uni.getRecorderManager(); // 录音管理器
const innerAudioContext = uni.createInnerAudioContext(); // 音频管理器
const options = {
duration: 60000, // 录音时长,单位为毫秒
sampleRate: 44100, // 采样率
numberOfChannels: 2, // 通道数
format: 'mp3', // 音频格式
};
const isPlayRecord = ref(false)
// 选择麦克风
const handleChangeMic = () => {
showMic.value = !showMic.value
}
// 开始录音
const handleSay = () => {
permissionUtil.requestAndroidPermission('android.permission.RECORD_AUDIO').then((status) => {
// status 为 1 表示用户已授权0 表示用户已拒绝, -1 表示用户永久拒绝
console.log('权限申请结果:', status);
if (status == -1) {
msgErrorRcord()
} else if (status == 1) {
// start方法无法被及时调用的处理方法
while (!showMask.value) {
recorderManager.start(options);
recorderManager.onStart(() => {
console.log('录音开始');
showMask.value = true
});
}
} else if (status == 0) {
msgErrorRcord()
}
});
}
const msgErrorRcord = () => {
msgToast.value.show({
title: '请确认您的麦克风权限是否开启',
type: 'error'
})
}
// 停止录音
const handleStopMic = () => {
showMaskBtn.value = true
if (voicePath.value) {
isPlayRecord.value = true
innerAudioContext.src = voicePath.value
innerAudioContext.play();
innerAudioContext.onEnded(() => {
isPlayRecord.value = false
})
} else {
recorderManager.stop()
// showMask.value = false
recorderManager.onStop(function (res) {
console.log('recorder stop' + JSON.stringify(res));
voicePath.value = res.tempFilePath;
});
}
}
// 取消发送录音
const handleCancel = () => {
showMask.value = false
showMaskBtn.value = false
voicePath.value = ''
innerAudioContext.stop()
}
// 发送录音文件
const handleSendMp3 = () => {
innerAudioContext.stop()
const token = uni.getStorageSync("token")
uni.uploadFile({
// url: 'http://192.168.0.4:3005/upload',
url: 'http://192.168.0.15:3007/upload/file',
filePath: voicePath.value,
header: {
"Authorization": "Bearer " + token
},
name: "file",
success: (uploadFileRes) => {
let result = JSON.parse(uploadFileRes.data)
if (result.data.fileUrl) {
// 上传文件成功,添加消息记录
socket.value.emit('clientMsg', {
createId: userId.value,
groupId: groupId.value,
content: result.data.fileUrl,
type: "mp3"
})
showMask.value = false
showMaskBtn.value = false
voicePath.value = ''
}
},
fail: (res) => {
console.log(JSON.stringify(res));
}
})
}
// 打开相册
const handleCamera = () => {
permissionUtil.requestAndroidPermission('android.permission.READ_EXTERNAL_STORAGE').then((status) => {
console.log('相册权限申请结果:', status);
if (status == -1) {
// 弹窗提示开启权限
msgToast.value.show({
title: '您已关闭读取相册权限,请在设置开启应用相册权限',
type: 'warning'
})
} else if (status == 1) {
uni.chooseImage({
count: 10, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: ['album'], //从相册选择
success: function (res) {
uploadImages(res.tempFilePaths)
}
});
}
})
}
// 上传图片
const uploadImages = (imgList : any) => {
const token = uni.getStorageSync("token")
imgList.forEach((item : string) => {
uni.uploadFile({
// url: 'http://192.168.0.4:3005/upload',
url: 'http://192.168.0.15:3007/upload/file',
filePath: item,
header: {
"Authorization": "Bearer " + token
},
name: "file",
success: (uploadFileRes) => {
let result = JSON.parse(uploadFileRes.data)
if (result.data.fileUrl) {
// 上传文件成功,添加消息记录
socket.value.emit('clientMsg', {
createId: userId.value,
groupId: groupId.value,
content: result.data.fileUrl,
type: "img"
})
}
},
fail: (res) => {
console.log(res);
}
})
})
}
// 图片预览
const showPreMask = ref(false)
const preImg = ref('')
const handlePreView = (value) => {
preImg.value = value
showPreMask.value = true
}
const playError = () => {
msgToast.value.show({
title: '音频错误,无法播放',
type: 'error'
})
}
// 播放录音
const handlePlayMp3 = (value) => {
innerAudioContext.src = value
innerAudioContext.play()
innerAudioContext.onPlay(() => {
// 正在播放录音
});
innerAudioContext.onError((res) => {
playError()
});
}
const groupId = ref()
const userId = ref()
onLoad((val) => {
groupId.value = val.groupId
});
const socket = ref()
// 通信连接
const connection = () => {
socket.value = io('http://192.168.0.15:3007', {
query: {},
transports: ['websocket', 'polling'],
timeout: 5000,
});
socket.value.on('connect', async () => {
console.log("连接成功");
})
socket.value.emit('addGroup', {
groupId: groupId.value,
})
socket.value.on('serverMsg', async (message:any) => {
dataList.value.push(message)
// console.log(dataList.value);
setTimeout(()=>{
scrollToBottom()
}, 500)
})
}
const scrollTop = ref(0)
const scrollToBottom = () => {
uni.createSelectorQuery().in(this).select("#scroll-main").boundingClientRect((res : any) => {
let top = res.height - scrollHeight.value;
if (top > 0) {
scrollTop.value = top;
}
}).exec()
}
const loadData = () => {
userId.value = uni.getStorageSync("user").id
}
onMounted(() => {
// mockData()
loadData()
loadHeight()
connection()
})
</script>
<style scoped lang="scss">
.chat-container {
width: 100%;
height: 100vh;
background: linear-gradient(180deg, #E3E8FF 0%, #FFFFFF 100%);
background-blend-mode: lighten;
.collection {
width: 48rpx;
height: 48rpx;
margin-right: 24rpx;
}
.bottom-view {
width: 100%;
height: 100rpx;
// background-color: #aaffff;
position: absolute;
bottom: 0;
display: flex;
align-items: center;
padding: 10rpx;
.icon {
width: 60rpx;
height: 60rpx;
}
.text {
flex: 1;
height: 96%;
border: 2rpx solid #DEEFFF;
border-radius: 10rpx;
padding: 0 10rpx;
}
.btn-mic {
flex: 1;
width: 100%;
height: 100%;
}
}
.scroll-main {
.my-message {
text-align: right;
margin-bottom: 20rpx;
.msg-main {
display: flex;
justify-content: flex-end;
padding: 0 10rpx;
.msg-content {
// background-color: #00ffff;
padding: 2rpx;
width: 70%;
margin-right: 10rpx;
// border: 1rpx solid #c7c7c7;
box-sizing: content-box;
.text {
overflow-wrap: break-word;
text-align: right;
}
}
.mp3-content {
padding: 2rpx;
// width: 70%;
margin-right: 10rpx;
box-sizing: content-box;
.mp3 {
display: flex;
// border: 1rpx solid #000;
padding: 15rpx 10rpx;
background-color: #55ff7f;
justify-content: flex-end;
align-items: center;
.mp3-icon {
margin: 0 10rpx;
width: 25rpx;
height: 25rpx;
}
}
}
.img-content {
margin-right: 10rpx;
height: 200rpx;
}
.msg-avatar {
width: 80rpx;
height: 80rpx;
}
}
}
.other-message {
margin-bottom: 20rpx;
.msg-main {
display: flex;
justify-content: flex-start;
padding: 0 10rpx;
.msg-content {
// background-color: #00ffff;
padding: 2rpx;
width: 70%;
// border: 1rpx solid #c7c7c7;
box-sizing: content-box;
.text {
overflow-wrap: break-word;
text-align: left;
}
}
.msg-avatar {
width: 80rpx;
height: 80rpx;
margin-right: 10rpx;
}
}
}
}
.long-text {
padding: 40rpx 20rpx;
}
.mask-warp {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
.mask-text {
font-size: 36rpx;
color: #fff;
display: flex;
align-items: center;
}
.mask-btn {
position: absolute;
bottom: 0;
width: 100%;
height: 80rpx;
display: flex;
.btn {
width: 50%;
}
}
}
// 图片预览
.mask-pre-img {
position: absolute;
top: 0;
bottom: 0;
}
}
</style>