【Vue】uniapp(vue3)+node.js+websocket(实现实时通信效果)
CrazyPanda发表于:2023-12-05 19:48:47浏览:296次
概要
uniapp基于vue3,小程序的聊天功能
项目是基于node.js服务器搭建的简易双向通信网页,实现了实时更新在线人数以及用户间即时通讯的功能。
整体架构流程
后台接口代码
1、首先我们可以通过Express 应用程序生成器快速搭建一个后台框架。(这快可以参考官网)
2、服务端
/** * WebSocket模块 */ const { WebSocketServer } = require('ws'); // 端口号 const server = new WebSocketServer({ port: 8082 }); // 存储聊天信息 let chatInfo = [] // 存储在线用户 let onlineUser = [] server.on("connection", (socket, req, res) => { console.log('链接成功'); // 用户注册 // 获取当前连接websocket的用户uid let uid = req.url.substr(1) socket["id"] = uid let index = onlineUser.findIndex(val => val.id === uid) if (index == -1) { onlineUser.push(socket) } // 从客户端接收消息 socket.on("message", (data) => { // 将收到的消息转成对象格式 let msg = data.toString() let message = JSON.parse(msg) // console.log(message, '123456789'); // 通知当前用户消息发送成功 socket.send(JSON.stringify({ message: message.message, types: message.types, toid: message.fid, tip: 0 })) // 存储当前用户发送的数据 chatInfo.push({ from: uid, to: message.fid, message: message.message, types: message.types }) // console.log(onlineUser); // 根据fid匹配到接收消息的用户并发送通知 onlineUser.forEach(item => { if (item.id == message.fid) { console.log("发送消息成功"); item.send(JSON.stringify({ message: message.message, types: message.types, from: uid, tip: 1 })) } }) }); // 连接关闭后清除在线用户 socket.on('close', res => { let newUserArr = [] let newUserIds = [] for (let i = 0; i < onlineUser.length; i++) { let val = onlineUser[i] if (val.id !== uid) { newUserArr.push(val) newUserIds.push(val.id) } } onlineUser = newUserArr let User = JSON.stringify({ onlineUser: newUserIds, type: 'users' }) onlineUser.forEach((client) => client.websocket.send(User)) console.log('用户离开了'); }) socket.send(JSON.stringify({ msg: '连接已建立', type: 'system' })) });
3、用户端(好友列表)
3.1、首先在onLoad生命周期中判断是否链接后端
onLoad(() => { uni.connectSocket({ url: 'ws://localhost:8082/' + store.userid, success: data => { console.log(data); } }) uni.onSocketOpen(function(res) { socketOpen = true; for (var i = 0; i < socketMsgQueue.length; i++) { sendSocketMessage(socketMsgQueue[i]); } socketMsgQueue = []; }); uni.onSocketClose(function(res) { console.log('WebSocket 已关闭!'); }); uni.onSocketError(function(res) { console.log('WebSocket连接打开失败,请检查!'); }); receiveSocketMsg() })
3.2、获取好友列表
// 获取好友数据 const getFrineds = () => { uni.request({ url: 'http://localhost:3461/wsb/getfreind', data: { uid: store.userid, }, method: 'POST', success: (data) => { let code = data.data.code if (code == 200) { let res = data.data.query if (res.length > 0) { for (let i = 0; i < res.length; i++) { List.value.push(res[i]) } } List.value = myfun.paixu(List.value, 'lastTime', 0) // console.log(List.value); // 获取好友内信息 for (let i = 0; i < List.value.length; i++) { getLastMsg(List.value, i) getunread(List.value, i) } } else { uni.showToast({ title: "服务器出错啦!", icon: "none", duration: 2000 }) } } }) }
3.3、好友列表实时通信
// socket聊天数据接收 const receiveSocketMsg = () => { uni.onSocketMessage(function(res) { console.log('收到服务器内容123:' + res.data); const msggs = JSON.parse(res.data) let nmsg = '' if (msggs.types == 0) { nmsg = msggs.message } else if (msggs.types == 1) { nmsg = '[图片]' } else if (msggs.types == 2) { nmsg = '[音频]' } else if (msggs.types == 3) { nmsg = '[位置]' } for (let i = 0; i < List.value.length; i++) { if (List.value[i].id == msggs.from) { let e = List.value[i] e.lastTime = new Date() e.msg = nmsg e.tip++ // 删除原来数据项 List.value.splice(i, 1) // 新消息插入到最顶部 List.value.unshift(e) // 更改最后一条消息时间 uni.request({ url: 'http://localhost:3461/wsb/updatetime', data: { uid: store.userid, fid: msggs.from }, method: 'POST', success: (data) => { let code = data.data.code if (code == 200) {} else { uni.showToast({ title: "服务器出错啦!", icon: "none", duration: 2000 }) } } }) } } }); }
效果:
如果还没有打开就会是未读消息,消息数加一,就在我们的头像右上角,大家可以看到小红数,当打开聊天框后小红数才会消失
4、用户端(聊天页面)
4.1、用户端uniapp是一个基于vue3的框架,首先在onMounted生命周期中判断是否链接后端
onMounted(() => { uni.connectSocket({ // 接口 url: 'ws://localhost:8082/' + user.uid, success: data => { console.log(data); } }); uni.onSocketOpen(function(res) { socketOpen = true; for (var i = 0; i < socketMsgQueue.length; i++) { sendSocketMessage(socketMsgQueue[i]); } socketMsgQueue = []; }); uni.onSocketClose(function(res) { console.log('WebSocket 已关闭!'); }); uni.onSocketError(function(res) { console.log('WebSocket连接打开失败,请检查!'); }); receiveSocketMsg() })
4.2、获取好友聊天内容,把内容存储到List数组中
const getList = () => { uni.request({ url: 'http://localhost:3461/wsb/msg', data: { uid: user.uid, fid: friend.fid }, method: 'POST', success: (data) => { let code = data.data.code if (code == 200) { let msg = data.data.query msg.reverse() if (msg.length > 0) { // console.log(msg); let oldtime = msg[0].time let imgarr = [] for (var i = 1; i < msg.length; i++) { // 时间间隔 if (i < msg.length - 1) { let t = myfun.spaceTime(oldtime, msg[i].time) if (t) { oldtime = t } msg[i].time = t } // 匹配最大时间 if (msg[i].time > oldTime.value) { oldTime.value = msg[i].time } // 补充图片地址 if (msg[i].types == 1) { msg[i].message = 'http://localhost:3461/' + msg[i].message imgarr.push(msg[i].message) } // json字符串还原 if (msg[i].types == 3) { msg[i].message = JSON.parse(msg[i].message) } // List.value.unshift(msg[i]) nextTick(() => { scrollToView.value = 'msg' + List.value[i - 1].id }) } // 两个数组拼接 List.value = msg.concat(List.value) getimg.value = imgarr.concat(getimg.value) } } else { uni.showToast({ title: "服务器出错啦!", icon: "none", duration: 2000 }) } } }) }
4.3、把你要发送的内容发给服务器,然后在看服务器是否接收
// 聊天数据发送给服务端(socket) const sendSocket = (e) => { uni.request({ url: 'http://localhost:3461/wsb/insertMsg', data: JSON.stringify({ msg: e.message, types: e.types, uid: user.uid, fid: friend.fid }), method: 'POST', success: (data) => { let code = data.data.code if (code == 200) { // console.log("添加成功"); } else { uni.showToast({ title: "服务器出错啦!", icon: "none", duration: 2000 }) } } }) // json字符串还原 if(e.types == 2){ e.message = JSON.parse(e.message) } uni.sendSocketMessage({ data: JSON.stringify({ message: e.message, types: e.types, uid: user.uid, fid: friend.fid }) }); }
4.4、服务器接收到消息后存储到用户消息里面,然后发送给客户端
// socket聊天数据接收 const receiveSocketMsg = () => { uni.onSocketMessage(function(res) { console.log('收到服务器内容123:' + res.data); const msggs = JSON.parse(res.data) if (msggs.from == friend.fid && msggs.tip==1) { let len = List.value.length; let nowTime = new Date() // 时间间隔 let t = myfun.spaceTime(oldTime.value, nowTime) if (t) { oldTime.value = t } // 判断是否加ip if (msggs.types == 1) { msggs.message = 'http://localhost:3461' + msggs.message } if ( msggs.types == 2) { msggs.message.voice = 'http://localhost:3461' + msggs.message.voice } nowTime = t let data = { fromId: msggs.from, //发送者的id imgurl: friend.fimgurl, time: nowTime, message: msggs.message, types: msggs.types, id: len }; List.value.push(data) // 图片 if (msggs.types == 1) { getimg.value.push(msggs.message) } nextTick(() => { scrollToView.value = 'msg' + len }) } }); }
表情效果:
表情可以看作是线上文字交流的重要补充。相较于面对面的沟通,人们在线上文字对话时较难感知对方的状态或情绪,而发送表情刚好可以弥补这一缺憾。
语音效果:
语音可以帮助我们快速回复对方,在发语音时,我们可以通过声音来表达自己的想法和情感,与对方进行交流和互动。
完整代码
完成效果:
服务端
/** * WebSocket模块 */ const { WebSocketServer } = require('ws'); // 端口号 const server = new WebSocketServer({ port: 8082 }); // 存储聊天信息 let chatInfo = [] // 存储在线用户 let onlineUser = [] server.on("connection", (socket, req, res) => { console.log('链接成功'); // 用户注册 // 获取当前连接websocket的用户uid let uid = req.url.substr(1) socket["id"] = uid let index = onlineUser.findIndex(val => val.id === uid) if (index == -1) { onlineUser.push(socket) } // 从客户端接收消息 socket.on("message", (data) => { // 将收到的消息转成对象格式 let msg = data.toString() let message = JSON.parse(msg) // console.log(message, '123456789'); // 通知当前用户消息发送成功 socket.send(JSON.stringify({ message: message.message, types: message.types, toid: message.fid, tip: 0 })) // 存储当前用户发送的数据 chatInfo.push({ from: uid, to: message.fid, message: message.message, types: message.types }) // console.log(onlineUser); // 根据fid匹配到接收消息的用户并发送通知 onlineUser.forEach(item => { if (item.id == message.fid) { console.log("发送消息成功"); item.send(JSON.stringify({ message: message.message, types: message.types, from: uid, tip: 1 })) } }) }); // 连接关闭后清除在线用户 socket.on('close', res => { let newUserArr = [] let newUserIds = [] for (let i = 0; i < onlineUser.length; i++) { let val = onlineUser[i] if (val.id !== uid) { newUserArr.push(val) newUserIds.push(val.id) } } onlineUser = newUserArr let User = JSON.stringify({ onlineUser: newUserIds, type: 'users' }) onlineUser.forEach((client) => client.websocket.send(User)) console.log('用户离开了'); }) socket.send(JSON.stringify({ msg: '连接已建立', type: 'system' })) });
客户端:
<!-- 详情页 --> <template> <view> <view> <view @click="back"> <image src="../../../static/icons/返回.png"></image> </view> <view> <view> {{friend.fname}} </view> </view> </view> <scroll-view scroll-y="true" scroll-with-animation="true" :scroll-into-view="scrollToView"> <view :style="{paddingBottom:inputh + 'px'}"> <view v-for="(item,index) in List" :key="index" :id="'msg'+item.id"> <view v-if="item.time != ''"> {{Datas(item.time)}} </view> <!-- 左用户 --> <view class="msg-m msg-left" v-if="item.fromId != user.uid"> <image :src="item.imgurl"></image> <!-- 文字 --> <view v-if="item.types == 0"> <view> {{item.message}} </view> </view> <!-- 图片 --> <view v-if="item.types == 1"> <image :src="item.message" mode="widthFix" @tap="previewImg(item.message)"> </image> </view> <!-- 音频 --> <view v-if="item.types == 2"> <view class="msg-text voice" :style="{width:item.message.time*4+'px'}" @tap="playVoice(item.message.voice)"> <image src="../../../static/submit/yp.png" mode=""></image> {{item.message.time}}″ </view> </view> <!-- 定位 --> <view v-if="item.types == 3"> <view @click="openLocations(item.message)"> <view> {{item.message.name}} </view> <view> {{item.message.address}} </view> <image src="../../../static/submit/dt.jpg" mode="aspectFill"></image> <!-- <map :longitude="item.message.longitude" :latitude="item.message.latitude" :markers="covers(item.message)"></map> --> </view> </view> </view> <!-- 右用户 --> <view class="msg-m msg-right" v-if="item.fromId == user.uid"> <image :src="item.imgurl"></image> <view v-if="item.types == 0"> <view> {{item.message}} </view> </view> <!-- 照片 --> <view v-if="item.types == 1"> <image :src="item.message" mode="widthFix" @tap="previewImg(item.message)"> </image> </view> <!-- 音频 --> <view v-if="item.types == 2"> <view class="msg-text voice" :style="{width:item.message.time*4+'px'}" @tap="playVoice(item.message.voice)"> {{item.message.time}}″ <image src="../../../static/submit/yp.png" mode=""></image> </view> </view> <!-- 定位 --> <view v-if="item.types == 3"> <view @click="openLocations(item.message)"> <view> {{item.message.name}} </view> <view> {{item.message.address}} </view> <!-- <map :longitude="item.message.longitude" :latitude="item.message.latitude" :markers="covers(item.message)"></map> --> <image src="../../../static/submit/dt.jpg" mode="aspectFill"></image> </view> </view> </view> </view> </view> <view></view> </scroll-view> <!-- 底部导航栏 --> <submit></submit> </view> </template> <script setup> import submit from '@/pages/buyCar/chat/submit.vue' import { onLoad, } from '@dcloudio/uni-app' import datas from '@/pages/buyCar/datas.js' import myfun from '@/pages/buyCar/myfun.js' // pinia import { useStore } from '@/store/users.js' // 获取用户id const store = useStore() // 用户信息 const user = reactive({ uid: '', uimgurl: '', uname: '' }) // 获取用户信息 const getUser = () => { user.uid = store.userid user.uimgurl = store.uimgurl user.uname = store.uname } import { nextTick, onMounted, reactive, ref } from "vue"; // 音频 const innerAudioContext = uni.createInnerAudioContext(); // 总数据 const List = ref([]) //获取所以消息 const getimg = ref([]) //获取所以图片 const oldTime = ref(0) //时间差 const scrollToView = ref('') // 让他定位到最后一条数据 const inputh = ref(72) // websocket信息存储 var socketOpen = false; var socketMsgQueue = []; // 对方的信息(前面的页面传递过来的信息) const friend = reactive({ fid: '', fimgurl: '', fname: '' }) // 进页面渲染(获取聊天数据) onLoad((options) => { friend.fid = options.id friend.fimgurl = options.img friend.fname = options.name getUser() getList() }) onMounted(() => { uni.connectSocket({ // 接口 url: 'ws://localhost:8082/' + user.uid, success: data => { console.log(data); } }); uni.onSocketOpen(function(res) { socketOpen = true; for (var i = 0; i < socketMsgQueue.length; i++) { sendSocketMessage(socketMsgQueue[i]); } socketMsgQueue = []; }); uni.onSocketClose(function(res) { console.log('WebSocket 已关闭!'); }); uni.onSocketError(function(res) { console.log('WebSocket连接打开失败,请检查!'); }); receiveSocketMsg() }) // const getList1 = () => { // let msg = datas.message() // for (let i = 0; i < msg.length; i++) { // msg[i].imgurl = '../../../static/' + msg[i].imgurl; // // 时间间隔 // if (i < msg.length - 1) { // let t = myfun.spaceTime(new Date(), msg[i].time) // if (t) { // oldTime = t // } // msg[i].time = t // } // // 补充图片地址 // if (msg[i].types == 1) { // msg[i].message = '../../../static/' + msg[i].message; // getimg.value.unshift(msg[i].message) // } // List.value.unshift(msg[i]) // nextTick(() => { // scrollToView.value = 'msg' + List.value[i - 1].tip // }) // } // // 让他定位到最后一条数据 // // console.log(List.value); // // console.log(scrollToView.value); // } const getList = () => { uni.request({ url: 'http://localhost:3461/wsb/msg', data: { uid: user.uid, fid: friend.fid }, method: 'POST', success: (data) => { let code = data.data.code if (code == 200) { let msg = data.data.query msg.reverse() if (msg.length > 0) { // console.log(msg); let oldtime = msg[0].time let imgarr = [] for (var i = 1; i < msg.length; i++) { // 时间间隔 if (i < msg.length - 1) { let t = myfun.spaceTime(oldtime, msg[i].time) if (t) { oldtime = t } msg[i].time = t } // 匹配最大时间 if (msg[i].time > oldTime.value) { oldTime.value = msg[i].time } // 补充图片地址 if (msg[i].types == 1) { msg[i].message = 'http://localhost:3461/' + msg[i].message imgarr.push(msg[i].message) } // json字符串还原 if (msg[i].types == 3) { msg[i].message = JSON.parse(msg[i].message) } // List.value.unshift(msg[i]) nextTick(() => { scrollToView.value = 'msg' + List.value[i - 1].id }) } // 两个数组拼接 List.value = msg.concat(List.value) getimg.value = imgarr.concat(getimg.value) } } else { uni.showToast({ title: "服务器出错啦!", icon: "none", duration: 2000 }) } } }) } // 接收输入框的内容 // uni.$on('inputs', (e) => { // // console.log(e); // let len = List.value.length; // let nowTime = new Date() // // 时间间隔 // let t = myfun.spaceTime(new Date(), nowTime) // if (t) { // oldTime = t // } // nowTime = t // let data = { // _id: "b", // imgurl: "../../../static/07.jpg", // time: nowTime, // message: e.message, // types: e.types, // tip: len // }; // List.value.push(data) // nextTick(() => { // scrollToView.value = 'msg' + len // }) // if (e.types == 1) { // getimg.value.push(e.message) // } // }) uni.$on('inputs', (e) => { getInput(e, user.uid, user.uimgurl, 0) console.log(e); }) // 接收消息 const getInput = (e, id, img, tip) => { // tip=0表示自己发的。tip=1表示对方发的 // socket提交 // 文字或者地图 if (e.types == 0 || e.types == 3) { sendSocket(e) } // 图片 if (e.types == 1) { getimg.value.push(e.message) // 提交图片处理 const uploadTask = uni.uploadFile({ url: 'http://localhost:3461/files/upload', //仅为示例,非真实的接口地址 filePath: e.message, name: 'file', formData: { url: 'picture', name: new Date().getTime() + user.uid + Math.ceil(Math.random() * 10), }, success: (uploadFileRes) => { // console.log(uploadFileRes); let data = { message: uploadFileRes.data, types: e.types } sendSocket(data) // let path = uploadFileRes.data // img.value.push('http://localhost:3461/'+path) // console.log(uploadFileRes.data); }, }); uploadTask.onProgressUpdate((res) => { // console.log('上传进度' + res.progress); // console.log('已经上传的数据长度' + res.totalBytesSent); // console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend); // 测试条件,取消上传任务。 // if (res.progress > 50) { // uploadTask.abort(); // } }); } // 音频 if (e.types == 2) { // 提交音频处理 const uploadTask = uni.uploadFile({ url: 'http://localhost:3461/files/upload', //仅为示例,非真实的接口地址 filePath: e.message.voice, name: 'file', formData: { url: 'voice', name: new Date().getTime() + user.uid + Math.ceil(Math.random() * 10), }, success: (uploadFileRes) => { // console.log(uploadFileRes); let data = { // json转json字符串 message: JSON.stringify({ voice:uploadFileRes.data, time:e.message.time }), types: e.types } sendSocket(data) // let path = uploadFileRes.data // img.value.push('http://localhost:3461/'+path) // console.log(uploadFileRes.data); }, }); uploadTask.onProgressUpdate((res) => { // console.log('上传进度' + res.progress); // console.log('已经上传的数据长度' + res.totalBytesSent); // console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend); // 测试条件,取消上传任务。 // if (res.progress > 50) { // uploadTask.abort(); // } }); } // 前端处理 let len = List.value.length; let nowTime = new Date() // 时间间隔 let t = myfun.spaceTime(oldTime.value, nowTime) if (t) { oldTime.value = t } nowTime = t // json字符串还原 if(e.types == 3){ e.message = JSON.parse(e.message) } let data = { fromId: id, imgurl: img, time: nowTime, message: e.message, types: e.types, id: len }; List.value.push(data) nextTick(() => { scrollToView.value = 'msg' + len }) } // 聊天数据发送给服务端(socket) const sendSocket = (e) => { uni.request({ url: 'http://localhost:3461/wsb/insertMsg', data: JSON.stringify({ msg: e.message, types: e.types, uid: user.uid, fid: friend.fid }), method: 'POST', success: (data) => { let code = data.data.code if (code == 200) { // console.log("添加成功"); } else { uni.showToast({ title: "服务器出错啦!", icon: "none", duration: 2000 }) } } }) // json字符串还原 if(e.types == 2){ e.message = JSON.parse(e.message) } uni.sendSocketMessage({ data: JSON.stringify({ message: e.message, types: e.types, uid: user.uid, fid: friend.fid }) }); } // socket聊天数据接收 const receiveSocketMsg = () => { uni.onSocketMessage(function(res) { console.log('收到服务器内容123:' + res.data); const msggs = JSON.parse(res.data) if (msggs.from == friend.fid && msggs.tip==1) { let len = List.value.length; let nowTime = new Date() // 时间间隔 let t = myfun.spaceTime(oldTime.value, nowTime) if (t) { oldTime.value = t } // 判断是否加ip if (msggs.types == 1) { msggs.message = 'http://localhost:3461' + msggs.message } if ( msggs.types == 2) { msggs.message.voice = 'http://localhost:3461' + msggs.message.voice } nowTime = t let data = { fromId: msggs.from, //发送者的id imgurl: friend.fimgurl, time: nowTime, message: msggs.message, types: msggs.types, id: len }; List.value.push(data) // 图片 if (msggs.types == 1) { getimg.value.push(msggs.message) } nextTick(() => { scrollToView.value = 'msg' + len }) } }); } // 输入框的高度 uni.$on('heights', (e) => { // console.log(e); inputh.value = e.msg // console.log(inputh .value); goBottom() }) // 滚动到底部 const goBottom = () => { scrollToView.value = '' nextTick(() => { let len = List.value.length - 1 scrollToView.value = 'msg' + List.value[len].id }) } // 处理时间 const Datas = (data) => { return myfun.dateTime(data) } // 返回上一页 const back = () => { uni.navigateBack({ delta: 1 }); } // 图片预览 const previewImg = (e) => { let index = 0; // 点击图片后通过for循环查找出第几张是该点击得图片 for (let i = 0; i < getimg.value.length; i++) { if (getimg.value[i] == e) { index = i; } } // uni中的api,图片预览 uni.previewImage({ current: index, // 点击图片看第几张 urls: getimg.value, longPressActions: { itemList: ['发送给朋友', '保存图片', '收藏'], success: function(data) { console.log('选中了第' + (data.tapIndex + 1) + '个按钮,第' + (data.index + 1) + '张图片'); }, fail: function(err) { console.log(err.errMsg); } } }); } // 音频播放 const playVoice = (e) => { innerAudioContext.src = e; innerAudioContext.play(); } // 获取位置 const covers = (e) => { let map = [{ latitude: e.latitude, longitude: e.longitude, iconPath: '../../../static/submit/dw1.png' }] return map } // 导航定位 const openLocations = (e) => { uni.openLocation({ latitude: e.latitude, longitude: e.longitude, name: e.name, address: e.address, success: function() { console.log('success'); } }); } </script> <style> .chatroom { width: 100%; height: 100%; background: rgba(0, 0, 0, 0.1); .top-bar { position: fixed; z-index: 1001; top: 0; left: 0; width: 100%; height: 120rpx; padding-top: var(--status-bar-height); background: #fff; border-bottom: 1rpx solid #ccc; .top-bar-left { float: left; padding-left: 5rpx; image { margin-top: 10rpx; width: 68rpx; height: 88rpx; border-radius: 16rpx; } } .top-bar-center { float: auto; text-align: center; .title { font-size: 40rpx; color: #000; line-height: 120rpx; } } } } .chat { height: 100%; .padbt { width: 100%; height: 40rpx; } .chat-main { padding-left: 32rpx; padding-right: 32rpx; padding-top: 160rpx; display: flex; flex-direction: column; } .chat-ls { .chat-time { font-size: 24rpx; color: rgba(39, 40, 50, 0.3); line-height: 34rpx; padding: 25rpx 0; text-align: center; } .msg-m { display: flex; padding: 20rpx 0; .user-img { flex: none; width: 80rpx; height: 80rpx; border-radius: 20rpx; } .massage { flex: none; max-width: 480rpx; } .msg-text { font-size: 32rpx; color: rgba(39, 40, 50, 1); line-height: 44rpx; padding: 18rpx 24rpx; } .msg-img { max-width: 400rpx; border-radius: 20rpx; } .msg-map { background-color: #fff; width: 460rpx; height: 284rpx; overflow: hidden; .map-name { font-size: 32rpx; color: rgba(39, 40, 50, 1); line-height: 44rpx; padding: 18rpx 24rpx 0 24rpx; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; } .map-address { font-size: 26rpx; color: rgba(39, 40, 50, 0.4); padding: 0rpx 24rpx; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; overflow: hidden; } .map { padding-top: 8rpx; width: 460rpx; height: 190rpx; } } .voice { min-width: 80rpx; max-width: 400rpx; } .voice-img { width: 28rpx; height: 36rpx; } } .msg-left { flex-direction: row; .msg-text { margin-left: 16rpx; background: #fff; border-radius: 0rpx 20rpx 20rpx 20rpx; } .msg-img { margin-left: 16rpx; } .msg-map { margin-left: 16rpx; border-radius: 0rpx 20rpx 20rpx 20rpx; } .voice { width: 200rpx; text-align: left; } .voice-img { width: 28rpx; height: 36rpx; padding-top: 4rpx; } } .msg-right { flex-direction: row-reverse; .msg-text { margin-right: 16rpx; background: #fff260; border-radius: 20rpx 0rpx 20rpx 20rpx; } .msg-img { margin-right: 16rpx; } .msg-map { margin-right: 16rpx; border-radius: 20rpx 0rpx 20rpx 20rpx; } .voice { width: 200rpx; text-align: right; } .voice-img { float: right; transform: rotate(180deg); width: 28rpx; height: 36rpx; padding-top: 4rpx; } } } } </style>
猜你喜欢
- 【Vue】vue中的$set的作用
- $set 的作用是响应式地设置对象或数组的属性,确保更改被 vue.js 追踪和更新,从而触发视图重新渲染。其用法包括:vue.set(target, key, value),适用于直接赋值不起作用、添加或删除对象或数组属性的情况。Vue.js 中 $set 的作用$set 是 Vue.js 中一个用于响应式地设置对象或数组属性的函数。它的主要作用是确保对对象或数组的更改能够被 Vue.js 追踪和更新,从而触发视图的重新渲染。如何使用 $set$set 函数有以下用法:Vue.set(targ
- 【Vue】Antd Pro Vue的使用(十) —— a-form表单赋值
- 在Ant Design Pro Vue中,为表单赋值通常涉及到两个方面:一是使用v-model进行双向绑定,二是直接通过this.form.setFieldsValue()方法设置表单字段的值。以下是一个简单的例子,展示如何为Ant Design Pro Vue中的表单赋值:首先,确保你已经在组件中引入了a-form-model和相关的表单字段组件比如a-form-model-item和a-input。在data函数中定义一个form对象,它包含了你想要绑定的表单字段。使用v-model指令将输
- 【Vue】Vue中使用Vuex管理全局状态详解和示例
- vue.js是一种流行的前端框架,它提供了很多方便的功能,但当应用变得越来越复杂时,我们很快就会发现向子组件传递大量数据变得非常困难。这就是为什么vuex在vue中变得如此重要的原因。vuex是一个全局状态管理器,使得数据和状态的共享变得更容易。在本文中,我们将深入了解vuex的工作原理并演示如何将其集成到您的vue应用程序中。什么是VuexVuex是一个用于Vue.js应用程序的状态管理模式和库,常用于解决跨层级、多组件、多页面共享状态问题。它将应用程序的状态集中存储到一个单一的store中,
- 【Vue】前端框架 Vue3框架 使用总结(一) Vue框架的基础使用
- 目录一、Vue3框架基础1、创建项目2、项目结构3、Vue基础语法4、组件之间通信5、组合式api二、VueRouter的基础使用1、安装2、使用案例3、完整案例步骤4、调优-路由懒加载三、Vuex数据管理1、实现案例 2、更改store状态,同步操作3、store中的计算属性4、redux里的异步操作Action5、模块化管理四、网络请求Vue3官方文档:Vue.js - 渐进式 JavaScript 框架 | Vue.js基础部分见官方文档一、Vue3框架基础1、创建项目安装yar
- 【Vue】Antd Pro Vue的使用(十二) —— 菜单选中高亮显示问题
- Antd Pro Vue2这套框架的路由菜单有两个问题,1、页面迁移子页面,父页面对应的菜单未能选中高亮显示2、登录后默认的菜单或页面刷新后原来的菜单未选中高亮显示网上查到的一些菜单配置都是新版本的,老版本并不支持这些方法,这里总结一下我的解决方法,如果大家有更好的解决方案,欢迎交流。一、解决第一个问题以我的菜单为例商品列表是菜单页面,添加编辑商品是隐藏菜单,我把他们做成了兄弟菜单,而不是子菜单/src/config/router.config.js配置如下然后再/src/layouts/Bas
- 【Vue】vue中sync作用
- vue 中的 sync 修饰符用于在父组件和子组件之间实现双向数据绑定。它通过生成一个 v-model 指令,将子组件的 prop 与父组件的 prop 绑定在一起,从而实现数据同步。用法如下:1. 在子组件中使用 v-bind:prop.sync="parentprop",其中 prop 是子组件的 prop 名称,parentprop 是父组件绑定的 prop 名称。Vue 中 sync 作用在 Vue 中,sync 修饰符是一种特殊的语法糖,它允许在父组件和子组件之间进
- 【Vue】vue是什么模式的前端框架
- vue 中的 mvvm 架构将应用程序分为 model、view 和 viewmodel:model:包含数据和业务逻辑,独立于视图。view:显示 model 中的数据,使用模板语法进行数据绑定。viewmodel:model 和 view 之间的桥梁,包含与 view 交互的数据和方法,并更新 view。mvvm 在 vue 中的优势包括响应式数据绑定、代码可重用性、提高生产力、易于调试。Vue:MVVM 架构什么是 MVVM?MVVM(Model-View-ViewModel)是一种软件设
- 【Vue】vue2vue3项目使用antd
- 前言项目研发需要,已经用了两年的ant design pro(react),因为会的人比较少,更多的人在使用vue,所以新项目决定使用antd vue来开发,好在比较熟悉了ant design组件的使用,也算是有一些基础。ant design 官网https://ant.design/index-cnantd vue 官网https://www.antdv.com/components/overview-cn 当前版本V4.1.2vue2项目-引入antd参考:https://www.antdv
- 【VUE】Vue3 实现文件预览 Word Excel pdf 图片 视频等格式 大全!!!!
- 先上效果图 插件安装先说 word 文件是docx-preview插件 excel文件是用 xlsx 插件 介绍后端返回的数据因为在拦截器处 做了对数据的处理 最后你调接口拿到的数据是 一个对象 里面包含:url : blob对象转换的用于访问Blob数据的临时链接。这个链接可以被用于在网页中展示二进制数据,比如显示图像或者播放音视频文件blobs:&
栏目分类全部>
推荐文章
- 【C#】从零开始用C#写一个桌面应用程序(一)基础操作
- 【UniApp】uniapp中使用PhotoSphereViewer全景图
- 【PHP】php中实现3DES算法(ECB加密模式PKCS5Padding填充)
- 【Python】解析matplotlib散点图绘制的简明步骤
- 【PHP】支付宝小程序授权登录踩坑记录
- 【Python】简单入门matplotlib:快速教程
- 【Vue】Antd Pro Vue的使用(八) —— 表单组件的常用配置
- 【Vue】Vue中的组件生命周期以及应用场景介绍
- 【Python】如何使用Python中的pickle和JSON进行对象序列化和反序列化
- 【Python】查看pandas版本的方法