Files
jurong_circle_front_app/pages/message/chat.vue
Sun_sun ad2ce26c08 2025-10-15
添加字体
项目描述
聊天记录加载修改
2025-10-15 17:22:54 +08:00

845 lines
21 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="chatTitle" 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/More horizontal.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>
{{handleFormatDate(item.createTime)}}
</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>
{{handleFormatDate(item.createTime)}}
</view>
</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" v-if="item.type=='text'">
<view class="text">
{{item.content}}
</view>
<view>
{{handleFormatDate(item.createTime)}}
</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>
{{handleFormatDate(item.createTime)}}
</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, onReady, onUnload, onPullDownRefresh
} from "@dcloudio/uni-app";
import io from '@hyoga/uni-socket.io';
import { getImageUrl } from '../../util/common';
import { permissionUtil } from '@/uni_modules/colorful-uni-perm';
import { messageAPI } from '../../api/message';
import { groupAPI } from '../../api/group';
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 handleFormatDate = (date) => {
const inputDate = new Date(date.replace(/-/g, '/'));
const now = new Date();
// 计算日期差(天)
const diffDays = Math.floor((now - inputDate) / (1000 * 60 * 60 * 24));
// 格式化时间
const hours = inputDate.getHours().toString().padStart(2, '0');
const minutes = inputDate.getMinutes().toString().padStart(2, '0');
const year = inputDate.getFullYear();
const month = (inputDate.getMonth() + 1).toString().padStart(2, '0');
const day = inputDate.getDate().toString().padStart(2, '0');
if (diffDays === 0) {
return `今天 ${hours}:${minutes}`;
} else if (diffDays === 1) {
return '昨天';
} else {
return `${year}-${month}-${day}`;
}
}
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"
}
})
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()
const chatTitle = ref()
const scrollTop = ref(0)
// 通信连接
const socket = ref()
const connection = async () => {
console.log("连接服务器");
socket.value = io(`http://192.168.0.15:3007?userId=${userId.value}`, {
query: {},
transports: ['websocket', 'polling'],
timeout: 5000,
});
socket.value.on('connect', () => {
console.log("连接成功");
// socket.value.emit('cleanup')
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 disConnection = () => {
console.log("disConnection");
socket.value.emit('leaveGroup', {
groupId: groupId.value
})
}
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 loadUnReadData = () => {
// 获取用户ID
userId.value = uni.getStorageSync("user").id
// 获取标题
groupAPI.getById(groupId.value).then(res => {
// chatTitle.value = res.data.program.linkman.username + "(" + res.data.program.name + ")"
chatTitle.value = res.data.program.name
})
loadHistoryData() // 最近10条
setTimeout(()=>{
scrollToBottom()
}, 1000)
// 未读变已读
// let data = { groupId: groupId.value, userId: userId.value }
// messageAPI.unread(data).then(res => {
// if (res.code == 200) {
// dataList.value = dataList.value.concat(res.data)
// }
// })
messageAPI.read({ groupId: groupId.value, userId: userId.value })
}
const params = ref({
page: 1,
size: 10,
groupId: null
})
const loadFix = ref(true)
// 历史记录
const loadHistoryData = async () => {
params.value.groupId = groupId.value
await messageAPI.list(params.value).then(res => {
// console.log(res);
if (res.data.list.length != 0) {
res.data.list.forEach(item => {
dataList.value.unshift(item)
})
params.value.page++
}
})
}
onPullDownRefresh(() => {
loadHistoryData()
uni.stopPullDownRefresh()
})
onLoad((val) => {
groupId.value = val.groupId
});
onReady(() => {
})
onMounted(() => {
loadHeight()
// mockData()
loadUnReadData()
connection()
})
onUnload(() => {
disConnection()
})
</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;
background-color: #4873FF;
display: block;
padding: 10rpx;
color: #fff;
box-shadow: 0rpx 4rpx 8rpx 0rpx #00000040;
}
}
.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-start;
// align-items: center;
.mp3-icon {
// margin: 0 10rpx;
width: 25rpx;
height: 25rpx;
}
}
}
.img-content {
margin-right: 10rpx;
height: 200rpx;
margin-bottom: 30rpx;
}
.msg-avatar {
width: 80rpx;
height: 80rpx;
}
}
}
.other-message {
margin-bottom: 20rpx;
.msg-main {
display: flex;
justify-content: flex-start;
padding: 0 10rpx;
.msg-avatar {
width: 80rpx;
height: 80rpx;
}
.msg-content {
// background-color: #00ffff;
padding: 2rpx;
width: 70%;
margin-left: 10rpx;
// border: 1rpx solid #c7c7c7;
box-sizing: content-box;
.text {
overflow-wrap: break-word;
text-align: left;
background-color: #F0F3FF;
display: block;
padding: 10rpx;
box-shadow: 0rpx 4rpx 8rpx 0rpx #00000040;
}
}
.mp3-content {
padding: 2rpx;
// width: 70%;
margin-left: 10rpx;
box-sizing: content-box;
.mp3 {
display: flex;
// border: 1rpx solid #000;
padding: 15rpx 10rpx;
background-color: #55ff7f;
justify-content: flex-start;
align-items: center;
.mp3-icon {
margin: 0 10rpx;
width: 25rpx;
height: 25rpx;
}
}
}
.img-content {
margin-left: 10rpx;
height: 200rpx;
margin-bottom: 30rpx;
}
}
}
}
.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>