@ -0,0 +1,21 @@ |
|||
const emoTextList = ['微笑', '撇嘴', '色', '发呆', '得意', '流泪', '害羞', '闭嘴', '睡', '大哭', '尴尬', '发怒', '调皮', '呲牙', '惊讶', '难过', '酷', '冷汗', '抓狂', '吐', '偷笑', '可爱', '白眼', '傲慢', '饥饿', '困', '惊恐', '流汗', '憨笑', '大兵', '奋斗', '咒骂', '疑问', '嘘', '晕', '折磨', '衰', '骷髅', '敲打', '再见', '擦汗', '抠鼻', '鼓掌', '糗大了', '坏笑', '左哼哼', '右哼哼', '哈欠', '鄙视', '委屈', '快哭了', '阴险', '亲亲', '吓', '可怜', '菜刀', '西瓜', '啤酒', '篮球', '乒乓', '咖啡', '饭', '猪头', '玫瑰', '凋谢', '示爱', '爱心', '心碎', '蛋糕', '闪电', '炸弹', '刀', '足球', '瓢虫', '便便', '月亮', '太阳', '礼物', '拥抱', '强', '弱', '握手', '胜利', '抱拳', '勾引', '拳头', '差劲', '爱你', 'NO', 'OK', '爱情', '飞吻', '跳跳', '发抖', '怄火', '转圈', '磕头', '回头', '跳绳', '挥手', '激动', '街舞', '献吻', '左太极', '右太极']; |
|||
|
|||
|
|||
let transform = (content) => { |
|||
return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, textToImg); |
|||
} |
|||
|
|||
// 将匹配结果替换表情图片
|
|||
let textToImg = (emoText) => { |
|||
let word = emoText.replace(/\#|\;/gi, ''); |
|||
let idx = emoTextList.indexOf(word); |
|||
let url = `/static/emoji/${idx}.gif`; |
|||
return `<img src="${url}" style="vertical-align:bottom;"/>` |
|||
} |
|||
|
|||
|
|||
export default { |
|||
emoTextList, |
|||
transform, |
|||
textToImg |
|||
} |
|||
@ -0,0 +1,171 @@ |
|||
let wsurl = ""; |
|||
let accessToken = ""; |
|||
let messageCallBack = null; |
|||
let openCallBack = null; |
|||
let isConnect = false; //连接标识 避免重复连接
|
|||
let hasLogin = false; |
|||
|
|||
let createWebSocket = (url, token) => { |
|||
wsurl = url; |
|||
accessToken = token; |
|||
closeWebSocket().then(() => { |
|||
initWebSocket(); |
|||
}); |
|||
|
|||
}; |
|||
|
|||
let initWebSocket = () => { |
|||
console.log("初始化WebSocket"); |
|||
uni.connectSocket({ |
|||
url: wsurl, |
|||
success: (res) => { |
|||
console.log("websocket连接成功"); |
|||
}, |
|||
fail: (err) => { |
|||
console.log(e); |
|||
console.log("websocket连接失败"); |
|||
reConnect(); //如果无法连接上webSocket 那么重新连接!可能会因为服务器重新部署,或者短暂断网等导致无法创建连接
|
|||
} |
|||
}); |
|||
|
|||
uni.onSocketOpen((res) => { |
|||
console.log("WebSocket连接已打开"); |
|||
isConnect = true; |
|||
// 发送登录命令
|
|||
let loginInfo = { |
|||
cmd: 0, |
|||
data: { |
|||
accessToken: accessToken |
|||
} |
|||
}; |
|||
uni.sendSocketMessage({ |
|||
data: JSON.stringify(loginInfo) |
|||
}); |
|||
}) |
|||
|
|||
|
|||
uni.onSocketMessage((res) => { |
|||
let sendInfo = JSON.parse(res.data) |
|||
if (sendInfo.cmd == 0) { |
|||
hasLogin = true; |
|||
heartCheck.start() |
|||
console.log('WebSocket登录成功') |
|||
// 登录成功才算连接完成
|
|||
openCallBack && openCallBack(); |
|||
} else if (sendInfo.cmd == 1) { |
|||
// 重新开启心跳定时
|
|||
heartCheck.reset(); |
|||
} else { |
|||
// 其他消息转发出去
|
|||
console.log("接收到消息",sendInfo); |
|||
messageCallBack && messageCallBack(sendInfo.cmd, sendInfo.data) |
|||
} |
|||
}) |
|||
|
|||
uni.onSocketClose((res) => { |
|||
console.log(res) |
|||
console.log('WebSocket连接关闭') |
|||
isConnect = false; //断开后修改标识
|
|||
//reConnect();
|
|||
}) |
|||
|
|||
uni.onSocketError((err) => { |
|||
console.log(err) |
|||
isConnect = false; //连接断开修改标识
|
|||
uni.showModal({ |
|||
content: '连接失败,可能是websocket服务不可用,请稍后再试', |
|||
showCancel: false, |
|||
}) |
|||
}) |
|||
|
|||
}; |
|||
|
|||
//定义重连函数
|
|||
let reConnect = () => { |
|||
console.log("尝试重新连接"); |
|||
if (isConnect) return; //如果已经连上就不在重连了
|
|||
rec && clearTimeout(rec); |
|||
rec = setTimeout(function() { // 延迟5秒重连 避免过多次过频繁请求重连
|
|||
initWebSocket(); |
|||
}, 5000); |
|||
}; |
|||
|
|||
//设置关闭连接
|
|||
let closeWebSocket = () => { |
|||
return new Promise((resolve, reject) => { |
|||
if (!isConnect) { |
|||
resolve(); |
|||
return; |
|||
} |
|||
console.log("关闭websocket连接"); |
|||
uni.closeSocket({ |
|||
code: 1000, |
|||
complete: (res) => { |
|||
hasLogin = false; |
|||
isConnect = false; |
|||
resolve(); |
|||
} |
|||
}) |
|||
}) |
|||
|
|||
|
|||
}; |
|||
|
|||
|
|||
//心跳设置
|
|||
var heartCheck = { |
|||
timeout: 10000, //每段时间发送一次心跳包 这里设置为30s
|
|||
timeoutObj: null, //延时发送消息对象(启动心跳新建这个对象,收到消息后重置对象)
|
|||
start: function() { |
|||
if (isConnect) { |
|||
console.log('发送WebSocket心跳') |
|||
let heartBeat = { |
|||
cmd: 1, |
|||
data: {} |
|||
}; |
|||
uni.sendSocketMessage({ |
|||
data: JSON.stringify(heartBeat), |
|||
fail(res) { |
|||
console.log(res); |
|||
} |
|||
}) |
|||
} |
|||
}, |
|||
reset: function() { |
|||
clearTimeout(this.timeoutObj); |
|||
this.timeoutObj = setTimeout(function() { |
|||
heartCheck.start(); |
|||
}, this.timeout); |
|||
} |
|||
|
|||
} |
|||
|
|||
// 实际调用的方法
|
|||
function sendMessage(agentData) { |
|||
uni.sendSocketMessage({ |
|||
data: agentData |
|||
}) |
|||
} |
|||
|
|||
|
|||
function onmessage(callback) { |
|||
messageCallBack = callback; |
|||
} |
|||
|
|||
|
|||
function onopen(callback) { |
|||
openCallBack = callback; |
|||
if (hasLogin) { |
|||
openCallBack(); |
|||
} |
|||
} |
|||
|
|||
|
|||
// 将方法暴露出去
|
|||
export { |
|||
createWebSocket, |
|||
closeWebSocket, |
|||
sendMessage, |
|||
onmessage, |
|||
onopen |
|||
} |
|||
@ -0,0 +1,134 @@ |
|||
<template> |
|||
<view class="chat-item" @click="showChatBox()"> |
|||
<view class="left"> |
|||
<image class="head-image" :src="chat.headImage" mode="aspectFill" lazy-load="true" ></image> |
|||
<view v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</view> |
|||
</view> |
|||
<view class="mid"> |
|||
<view class="show-name">{{ chat.showName}}</view> |
|||
<view class="msg-text" v-html="$emo.transform(chat.lastContent)"></view> |
|||
</view> |
|||
<view class="right "> |
|||
<view class="msg-time"> |
|||
<chat-time :time="chat.lastSendTime"></chat-time> |
|||
</view> |
|||
<view></view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "chatItem", |
|||
data() { |
|||
return {} |
|||
}, |
|||
props: { |
|||
chat: { |
|||
type: Object |
|||
}, |
|||
index: { |
|||
type: Number |
|||
} |
|||
}, |
|||
methods: { |
|||
showChatBox(){ |
|||
uni.navigateTo({ |
|||
url: "/pages/chat/chat-box?chatIdx="+this.index |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scode> |
|||
.chat-item { |
|||
height: 120rpx; |
|||
display: flex; |
|||
margin-bottom: 1rpx; |
|||
position: relative; |
|||
padding-left: 30rpx; |
|||
align-items: center; |
|||
padding-right: 10rpx; |
|||
background-color: #fafafa; |
|||
white-space: nowrap; |
|||
&:hover { |
|||
background-color: #eeeeee; |
|||
} |
|||
|
|||
.left { |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
width: 100rpx; |
|||
height: 100rpx; |
|||
|
|||
.head-image{ |
|||
width: 100%; |
|||
height: 100%; |
|||
border-radius: 10%; |
|||
} |
|||
|
|||
.unread-text { |
|||
position: absolute; |
|||
background-color: red; |
|||
right: -12rpx; |
|||
top: -12rpx; |
|||
color: white; |
|||
border-radius: 30rpx; |
|||
padding: 5rpx 6rpx; |
|||
font-size: 10px; |
|||
text-align: center; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
|
|||
|
|||
.mid { |
|||
margin-left: 20rpx; |
|||
flex: 2; |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100%; |
|||
flex-shrink: 0; |
|||
overflow: hidden; |
|||
|
|||
|
|||
.show-name { |
|||
display: flex; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
font-size: 36rpx; |
|||
flex: 3; |
|||
} |
|||
|
|||
.msg-text { |
|||
flex: 2; |
|||
font-size: 28rpx; |
|||
color: #888888; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
|
|||
.right { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: flex-end; |
|||
height: 80rpx; |
|||
flex-shrink: 0; |
|||
overflow: hidden; |
|||
|
|||
.msg-time { |
|||
display: flex; |
|||
justify-content: flex-start; |
|||
align-items: center; |
|||
font-size: 14px; |
|||
color: #888888; |
|||
white-space: nowrap; |
|||
} |
|||
} |
|||
} |
|||
|
|||
</style> |
|||
@ -0,0 +1,379 @@ |
|||
<template> |
|||
<view class="chat-msg-item"> |
|||
<view class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</view> |
|||
|
|||
<view class="chat-msg-normal" v-show="msgInfo.type!=$enums.MESSAGE_TYPE.RECALL" :class="{'chat-msg-mine':msgInfo.selfSend}"> |
|||
<view class="avatar"> |
|||
<image class="head-image" :src="headImage"></image> |
|||
</view> |
|||
|
|||
<view class="chat-msg-content"> |
|||
<view class="chat-msg-top"> |
|||
<text>{{showName}}</text> |
|||
<chat-time :time="msgInfo.sendTime"></chat-time> |
|||
</view> |
|||
|
|||
<view class="chat-msg-bottom"> |
|||
<rich-text class="chat-msg-text" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TEXT" :nodes="$emo.transform(msgInfo.content)"></rich-text> |
|||
<view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.FILE"> |
|||
<view class="img-load-box" > |
|||
<image class="send-image" :src="JSON.parse(msgInfo.content).thumbUrl" lazy-load="true" ></image> |
|||
<loading v-show="loading"></loading> |
|||
</view> |
|||
<text title="发送失败" v-show="loadFail" class="send-fail iconfont icon-warning-circle-fill"></text> |
|||
</view> |
|||
<!-- |
|||
<view class="chat-msg-file" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE"> |
|||
<view class="chat-file-box" v-loading="loading"> |
|||
<view class="chat-file-info"> |
|||
<el-link class="chat-file-name" :underline="true" target="_blank" type="primary" :href="data.url">{{data.name}}</el-link> |
|||
<view class="chat-file-size">{{fileSize}}</view> |
|||
</view> |
|||
<view class="chat-file-icon"> |
|||
<text type="primary" class="el-icon-document"></text> |
|||
</view> |
|||
</view> |
|||
<text title="发送失败" v-show="loadFail" @click="handleSendFail" class="send-fail el-icon-warning"></text> |
|||
</view> |
|||
<view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="handlePlayVoice()"> |
|||
<audio controls :src="JSON.parse(msgInfo.content).url"></audio> |
|||
</view> |
|||
--> |
|||
</view> |
|||
|
|||
</view> |
|||
|
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "chat-message-item", |
|||
props: { |
|||
headImage: { |
|||
type: String, |
|||
required: true |
|||
}, |
|||
showName: { |
|||
type: String, |
|||
required: true |
|||
}, |
|||
msgInfo: { |
|||
type: Object, |
|||
required: true |
|||
}, |
|||
menu:{ |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
audioPlayState: 'STOP', |
|||
rightMenu: { |
|||
show: false, |
|||
pos: { |
|||
x: 0, |
|||
y: 0 |
|||
} |
|||
} |
|||
} |
|||
|
|||
}, |
|||
methods: { |
|||
handleSendFail() { |
|||
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送") |
|||
}, |
|||
showFullImageBox() { |
|||
let imageUrl = JSON.parse(this.msgInfo.content).originUrl; |
|||
if (imageUrl) { |
|||
this.$store.commit('showFullImageBox', imageUrl); |
|||
} |
|||
}, |
|||
handlePlayVoice() { |
|||
if (!this.audio) { |
|||
this.audio = new Audio(); |
|||
} |
|||
this.audio.src = JSON.parse(this.msgInfo.content).url; |
|||
this.audio.play(); |
|||
this.handlePlayVoice = 'RUNNING'; |
|||
}, |
|||
showRightMenu(e) { |
|||
this.rightMenu.pos = { |
|||
x: e.x, |
|||
y: e.y |
|||
}; |
|||
this.rightMenu.show = "true"; |
|||
}, |
|||
handleSelectMenu(item) { |
|||
this.$emit(item.key.toLowerCase(), this.msgInfo); |
|||
} |
|||
}, |
|||
computed: { |
|||
loading() { |
|||
return !this.isTimeout && this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading"; |
|||
}, |
|||
loadFail() { |
|||
return this.msgInfo.loadStatus && (this.isTimeout || this.msgInfo.loadStatus === "fail"); |
|||
}, |
|||
isTimeout(){ |
|||
return (new Date().getTime() - new Date(this.msgInfo.sendTime).getTime()) > 30*1000; |
|||
|
|||
}, |
|||
data() { |
|||
return JSON.parse(this.msgInfo.content) |
|||
}, |
|||
fileSize() { |
|||
let size = this.data.size; |
|||
if (size > 1024 * 1024) { |
|||
return Math.round(size / 1024 / 1024) + "M"; |
|||
} |
|||
if (size > 1024) { |
|||
return Math.round(size / 1024) + "KB"; |
|||
} |
|||
return size + "B"; |
|||
}, |
|||
menuItems() { |
|||
let items = []; |
|||
items.push({ |
|||
key: 'DELETE', |
|||
name: '删除', |
|||
icon: 'el-icon-delete' |
|||
}); |
|||
if (this.msgInfo.selfSend && this.msgInfo.id > 0) { |
|||
items.push({ |
|||
key: 'RECALL', |
|||
name: '撤回', |
|||
icon: 'el-icon-refresh-left' |
|||
}); |
|||
} |
|||
return items; |
|||
} |
|||
}, |
|||
mounted() { |
|||
//console.log(this.msgInfo); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.chat-msg-item { |
|||
padding: 20rpx; |
|||
.chat-msg-tip { |
|||
line-height: 50px; |
|||
} |
|||
|
|||
.chat-msg-normal { |
|||
position: relative; |
|||
font-size: 0; |
|||
margin-bottom: 15rpx; |
|||
padding-left: 120rpx; |
|||
min-height: 120rpx; |
|||
|
|||
.avatar { |
|||
position: absolute; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
width: 100rpx; |
|||
height: 100rpx; |
|||
top: 0; |
|||
left: 0; |
|||
|
|||
.head-image{ |
|||
width: 100%; |
|||
height: 100%; |
|||
border-radius: 5%; |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
.chat-msg-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
.chat-msg-top { |
|||
display: flex; |
|||
flex-wrap: nowrap; |
|||
color: #333; |
|||
font-size: 14px; |
|||
line-height: 20px; |
|||
|
|||
text { |
|||
margin-right: 12px; |
|||
} |
|||
} |
|||
|
|||
.chat-msg-bottom { |
|||
text-align: left; |
|||
|
|||
.chat-msg-text { |
|||
position: relative; |
|||
line-height: 22px; |
|||
margin-top: 10px; |
|||
padding: 10px; |
|||
background-color: #eeeeee; |
|||
border-radius: 3px; |
|||
color: #333; |
|||
display: inline-block; |
|||
font-size: 14px; |
|||
word-break: break-all; |
|||
white-space: pre-line; |
|||
|
|||
&:after { |
|||
content: ""; |
|||
position: absolute; |
|||
left: -10px; |
|||
top: 13px; |
|||
width: 0; |
|||
height: 0; |
|||
border-style: solid dashed dashed; |
|||
border-color: #eeeeee transparent transparent; |
|||
overflow: hidden; |
|||
border-width: 10px; |
|||
} |
|||
} |
|||
|
|||
|
|||
.chat-msg-image { |
|||
display: flex; |
|||
flex-wrap: nowrap; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
|
|||
.img-load-box{ |
|||
position: relative; |
|||
|
|||
.send-image { |
|||
min-width: 200rpx; |
|||
min-height: 150rpx; |
|||
max-width: 400rpx; |
|||
max-height: 300rpx; |
|||
border: #dddddd solid 1px; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
|
|||
.send-fail { |
|||
color: #e60c0c; |
|||
font-size: 30px; |
|||
cursor: pointer; |
|||
margin: 0 20px; |
|||
} |
|||
} |
|||
|
|||
.chat-msg-file { |
|||
display: flex; |
|||
flex-wrap: nowrap; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
cursor: pointer; |
|||
|
|||
.chat-file-box { |
|||
display: flex; |
|||
flex-wrap: nowrap; |
|||
align-items: center; |
|||
width: 20%; |
|||
min-height: 80px; |
|||
border: #dddddd solid 1px; |
|||
border-radius: 3px; |
|||
background-color: #eeeeee; |
|||
padding: 10px 15px; |
|||
|
|||
.chat-file-info { |
|||
flex: 1; |
|||
height: 100%; |
|||
text-align: left; |
|||
font-size: 14px; |
|||
|
|||
.chat-file-name { |
|||
font-size: 16px; |
|||
font-weight: 600; |
|||
margin-bottom: 15px; |
|||
} |
|||
} |
|||
|
|||
.chat-file-icon { |
|||
font-size: 50px; |
|||
color: #d42e07; |
|||
} |
|||
} |
|||
|
|||
.send-fail { |
|||
color: #e60c0c; |
|||
font-size: 30px; |
|||
cursor: pointer; |
|||
margin: 0 20px; |
|||
} |
|||
|
|||
} |
|||
|
|||
.chat-msg-voice { |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
|
|||
audio { |
|||
height: 45px; |
|||
padding: 5px 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
&.chat-msg-mine { |
|||
text-align: right; |
|||
padding-left: 0; |
|||
padding-right: 120rpx; |
|||
|
|||
.avatar { |
|||
left: auto; |
|||
right: 0; |
|||
} |
|||
|
|||
.chat-msg-content { |
|||
|
|||
.chat-msg-top { |
|||
flex-direction: row-reverse; |
|||
|
|||
text { |
|||
margin-left: 12px; |
|||
margin-right: 0; |
|||
} |
|||
} |
|||
|
|||
.chat-msg-bottom { |
|||
text-align: right; |
|||
|
|||
.chat-msg-text { |
|||
margin-left: 10px; |
|||
background-color: #5fb878; |
|||
color: #fff; |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
font-size: 14px; |
|||
|
|||
&:after { |
|||
left: auto; |
|||
right: -10px; |
|||
border-top-color: #5fb878; |
|||
} |
|||
} |
|||
|
|||
.chat-msg-image { |
|||
flex-direction: row-reverse; |
|||
} |
|||
|
|||
.chat-msg-file { |
|||
flex-direction: row-reverse; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,45 @@ |
|||
<template> |
|||
<view> |
|||
<text>{{formatDate}}</text> |
|||
</view> |
|||
|
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "chat-time", |
|||
data() { |
|||
return {} |
|||
}, |
|||
props: { |
|||
time: { |
|||
type: Number |
|||
} |
|||
}, |
|||
computed: { |
|||
formatDate() { |
|||
let time = new Date(this.time); |
|||
let strtime = ""; |
|||
|
|||
let todayTime = new Date(); |
|||
todayTime.setHours(0, 0, 0, 0) |
|||
let dayDiff = Math.floor((todayTime.getTime() - time.getTime()) / (24 * 3600 * 1000)); |
|||
if (time.getTime() > todayTime.getTime()) { |
|||
strtime = time.getHours() <= 9 ? "0" + time.getHours() : time.getHours(); |
|||
strtime += ":" |
|||
strtime += time.getMinutes() <= 9 ? "0" + time.getMinutes() : time.getMinutes(); |
|||
} else if (dayDiff < 1) { |
|||
strtime = "昨天"; |
|||
} else if (dayDiff < 7) { |
|||
strtime = `${dayDiff+1}天前`; |
|||
} else { |
|||
strtime = time.getMonth() + 1 + "月" + time.getDate() + "日"; |
|||
} |
|||
return strtime; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
|||
@ -0,0 +1,101 @@ |
|||
<template> |
|||
<view @click="selectAndUpload()"> |
|||
<slot></slot> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "image-upload", |
|||
data() { |
|||
return { |
|||
uploadHeaders: { |
|||
"accessToken": uni.getStorageSync('loginInfo').accessToken |
|||
} |
|||
} |
|||
}, |
|||
props: { |
|||
maxSize: { |
|||
type: Number, |
|||
default: null |
|||
}, |
|||
onBefore: { |
|||
type: Function, |
|||
default: null |
|||
}, |
|||
onSuccess: { |
|||
type: Function, |
|||
default: null |
|||
}, |
|||
onError: { |
|||
type: Function, |
|||
default: null |
|||
} |
|||
}, |
|||
methods: { |
|||
selectAndUpload() { |
|||
console.log("selectAndUpload"); |
|||
uni.chooseImage({ |
|||
count: 9, //最多可以选择的图片张数,默认9 |
|||
sourceType: ['album'], //album 从相册选图,camera 使用相机,默认二者都有。如需直接开相机或直接选相册,请只使用一个选项 |
|||
sizeType: ['original'], //original 原图,compressed 压缩图,默认二者都有 |
|||
success: (res) => { |
|||
res.tempFiles.forEach((file) => { |
|||
console.log("选择文件"); |
|||
// 校验大小 |
|||
if (this.maxSize && file.size > this.maxSize) { |
|||
this.$message.error(`文件大小不能超过 ${this.fileSizeStr}!`); |
|||
this.$emit("fail", file); |
|||
return; |
|||
} |
|||
|
|||
if (!this.onBefore || this.onBefore(file)) { |
|||
// 调用上传图片的接口 |
|||
this.uploadImage(file); |
|||
} |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
uploadImage(file) { |
|||
console.log("上传文件") |
|||
uni.uploadFile({ |
|||
url: process.env.BASE_URL + '/image/upload', |
|||
header: { |
|||
accessToken: uni.getStorageSync("loginInfo").accessToken |
|||
}, |
|||
filePath: file.path, // 要上传文件资源的路径 |
|||
name: 'file', |
|||
success: (res) => { |
|||
let data = JSON.parse(res.data); |
|||
if(data.code != 200){ |
|||
this.onError && this.onError(file, data); |
|||
}else{ |
|||
console.log("上传成功") |
|||
this.onSuccess && this.onSuccess(file, data); |
|||
} |
|||
}, |
|||
fail: (err) => { |
|||
console.log("上传失败") |
|||
console.log(this.onError) |
|||
this.onError && this.onError(file, err); |
|||
} |
|||
}) |
|||
} |
|||
}, |
|||
computed: { |
|||
fileSizeStr() { |
|||
if (this.maxSize > 1024 * 1024) { |
|||
return Math.round(this.maxSize / 1024 / 1024) + "M"; |
|||
} |
|||
if (this.maxSize > 1024) { |
|||
return Math.round(this.maxSize / 1024) + "KB"; |
|||
} |
|||
return this.maxSize + "B"; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
</style> |
|||
@ -0,0 +1,49 @@ |
|||
<template> |
|||
<view class="loading-box"> |
|||
<view class="rotate iconfont icon-loading" ></view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return {}; |
|||
}, |
|||
methods: { |
|||
}, |
|||
computed: { |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style> |
|||
.loading-box { |
|||
width: 100%; |
|||
height: 100%; |
|||
background-color: rgba(0, 0, 0, 0.4); |
|||
position: absolute; |
|||
left: 0; |
|||
top: 0; |
|||
z-index: 10000; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
|||
|
|||
|
|||
|
|||
.rotate { |
|||
animation: rotate 2s ease-in-out infinite; |
|||
font-size: 100rpx; |
|||
} |
|||
|
|||
@keyframes rotate { |
|||
from { |
|||
transform: rotate(0deg) |
|||
} |
|||
|
|||
to { |
|||
transform: rotate(360deg) |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,343 @@ |
|||
<template> |
|||
<view class="chat-box"> |
|||
<view class="header"> |
|||
<text class="title">{{title}}</text> |
|||
<uni-icons class="btn-side" type="more-filled" size="30"></uni-icons> |
|||
</view> |
|||
<view class="chat-msg" @click="showToolBox(false)"> |
|||
<scroll-view class="scroll-box" scroll-y="true" :scroll-into-view="'chat-item-'+scrollMsgIdx"> |
|||
<view v-for="(msgInfo,idx) in chat.messages" :key="idx"> |
|||
<chat-message-item :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" |
|||
:id="'chat-item-'+idx" :msgInfo="msgInfo"> |
|||
</chat-message-item> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
|
|||
<view class="send-bar"> |
|||
<view class="iconfont icon-voice-circle"></view> |
|||
<view class="send-text"> |
|||
<textarea class="send-text-area" v-model="sendText" ref="sendBox" auto-height :show-confirm-bar="false" |
|||
cursor-spacing="10" @keydown.enter="sendTextMessage()" @click="showToolBox(false)"></textarea> |
|||
</view> |
|||
<view class="iconfont icon-icon_emoji"></view> |
|||
<view v-show="sendText==''" class="iconfont icon-add-circle" @click="showToolBox(true)" ></view> |
|||
<button v-show="sendText!=''" class="btn-send" type="primary" @click="sendTextMessage()" size="mini">发送</button> |
|||
</view> |
|||
|
|||
<view v-show="showTools" class="chat-tools" > |
|||
<view class="chat-tools-item"> |
|||
<image-upload :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess" |
|||
:onError="onUploadImageFail"> |
|||
<view class="tool-icon iconfont icon-picture"></view> |
|||
</image-upload> |
|||
<view class="tool-name">相册</view> |
|||
</view> |
|||
<view class="chat-tools-item" v-for="(tool, idx) in tools" @click="onClickTool(tool)"> |
|||
<view class="tool-icon iconfont" :class="tool.icon"></view> |
|||
<view class="tool-name">{{ tool.name }}</view> |
|||
</view> |
|||
</view> |
|||
|
|||
|
|||
|
|||
<!-- |
|||
<emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion> |
|||
<chat-voice :visible="showVoice" @close="closeVoiceBox" @send="handleSendVoice"></chat-voice> |
|||
<chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group" :groupMembers="groupMembers" @close="closeHistoryBox"></chat-history> |
|||
--> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
chat: {}, |
|||
friend: {}, |
|||
group: {}, |
|||
groupMembers: [], |
|||
sendText: "", |
|||
showVoice: false, // 是否显示语音录制弹窗 |
|||
showSide: false, // 是否显示群聊信息栏 |
|||
showEmotion: false, // 是否显示emoji表情 |
|||
showHistory: false, // 是否显示历史聊天记录 |
|||
scrollMsgIdx: 0, // 滚动条定位为到哪条消息 |
|||
showTools: false, |
|||
tools: [{ |
|||
name: "拍摄", |
|||
icon: "icon-camera" |
|||
}, |
|||
{ |
|||
name: "语音输入", |
|||
icon: "icon-microphone" |
|||
}, |
|||
{ |
|||
name: "文件", |
|||
icon: "icon-folder" |
|||
}, |
|||
{ |
|||
name: "呼叫", |
|||
icon: "icon-call" |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
methods: { |
|||
headImage(msgInfo) { |
|||
if (this.chat.type == 'GROUP') { |
|||
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); |
|||
return member ? member.headImage : ""; |
|||
} else { |
|||
return msgInfo.selfSend ? this.mine.headImageThumb : this.chat.headImage |
|||
} |
|||
}, |
|||
showName(msgInfo) { |
|||
if (this.chat.type == 'GROUP') { |
|||
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); |
|||
return member ? member.aliasName : ""; |
|||
} else { |
|||
return msgInfo.selfSend ? this.mine.nickName : this.chat.showName |
|||
} |
|||
|
|||
}, |
|||
sendTextMessage() { |
|||
if (!this.sendText.trim()) { |
|||
return uni.showToast({ |
|||
title: "不能发送空白信息", |
|||
icon: "none" |
|||
}); |
|||
} |
|||
let msgInfo = { |
|||
content: this.sendText, |
|||
type: 0 |
|||
} |
|||
// 填充对方id |
|||
this.fillTargetId(msgInfo, this.chat.targetId); |
|||
this.sendText = ""; |
|||
this.$http({ |
|||
url: this.messageAction, |
|||
method: 'POST', |
|||
data: msgInfo |
|||
}).then((id) => { |
|||
msgInfo.id = id; |
|||
msgInfo.sendTime = new Date().getTime(); |
|||
msgInfo.sendId = this.$store.state.userStore.userInfo.id; |
|||
msgInfo.selfSend = true; |
|||
this.$store.commit("insertMessage", msgInfo); |
|||
uni.showToast({ |
|||
title: "发送成功", |
|||
icon: "none" |
|||
}) |
|||
}).finally(() => { |
|||
// 滚动到底部 |
|||
this.scrollToBottom(); |
|||
}); |
|||
}, |
|||
fillTargetId(msgInfo, targetId) { |
|||
if (this.chat.type == "GROUP") { |
|||
msgInfo.groupId = targetId; |
|||
} else { |
|||
msgInfo.recvId = targetId; |
|||
} |
|||
}, |
|||
scrollToBottom() { |
|||
this.$nextTick(() => { |
|||
this.scrollMsgIdx = this.chat.messages.length - 1; |
|||
}) |
|||
|
|||
}, |
|||
showToolBox(v){ |
|||
this.showTools = v; |
|||
}, |
|||
onUploadImageBefore(file) { |
|||
let data = { |
|||
originUrl: file.path, |
|||
thumbUrl: file.path |
|||
} |
|||
let msgInfo = { |
|||
id: 0, |
|||
fileId: file.uid, |
|||
sendId: this.mine.id, |
|||
content: JSON.stringify(data), |
|||
sendTime: new Date().getTime(), |
|||
selfSend: true, |
|||
type: 1, |
|||
loadStatus: "loading" |
|||
} |
|||
// 填充对方id |
|||
this.fillTargetId(msgInfo, this.chat.targetId); |
|||
// 插入消息 |
|||
this.$store.commit("insertMessage", msgInfo); |
|||
// 借助file对象保存 |
|||
file.msgInfo = msgInfo; |
|||
// 滚到最低部 |
|||
this.scrollToBottom(); |
|||
return true; |
|||
}, |
|||
onUploadImageSuccess(file, res) { |
|||
console.log("onUploadImageSuccess") |
|||
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); |
|||
msgInfo.content = JSON.stringify(res.data); |
|||
this.$http({ |
|||
url: this.messageAction, |
|||
method: 'POST', |
|||
data: msgInfo |
|||
}).then((id) => { |
|||
msgInfo.loadStatus = 'ok'; |
|||
msgInfo.id = id; |
|||
this.$store.commit("insertMessage", msgInfo); |
|||
}) |
|||
}, |
|||
onUploadImageFail(file, err) { |
|||
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); |
|||
msgInfo.loadStatus = 'fail'; |
|||
this.$store.commit("insertMessage", msgInfo); |
|||
}, |
|||
onClickTool(tool) { |
|||
switch (tool.name) { |
|||
case "相册": |
|||
break; |
|||
|
|||
} |
|||
console.log(tool); |
|||
} |
|||
}, |
|||
computed: { |
|||
mine() { |
|||
return this.$store.state.userStore.userInfo; |
|||
}, |
|||
title() { |
|||
if (!this.chat) { |
|||
return ""; |
|||
} |
|||
let title = this.chat.showName; |
|||
if (this.chat.type == "GROUP") { |
|||
let size = this.groupMembers.filter(m => !m.quit).length; |
|||
title += `(${size})`; |
|||
} |
|||
return title; |
|||
}, |
|||
messageAction() { |
|||
return `/message/${this.chat.type.toLowerCase()}/send`; |
|||
} |
|||
}, |
|||
onLoad(options) { |
|||
console.log("onLoad") |
|||
let chatIdx = options.chatIdx; |
|||
this.chat = this.$store.state.chatStore.chats[chatIdx]; |
|||
this.scrollToBottom(); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.chat-box { |
|||
position: relative; |
|||
background: white; |
|||
border: #dddddd solid 1px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: calc(100vh - 44px); |
|||
|
|||
.header { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 60rpx; |
|||
padding: 5px; |
|||
background-color: white; |
|||
line-height: 50px; |
|||
font-size: 40rpx; |
|||
font-weight: 600; |
|||
border: #dddddd solid 1px; |
|||
|
|||
|
|||
.btn-side { |
|||
position: absolute; |
|||
right: 30rpx; |
|||
line-height: 60rpx; |
|||
font-size: 28rpx; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
|
|||
.chat-msg { |
|||
flex: 1; |
|||
padding: 0; |
|||
border: #dddddd solid 1px; |
|||
overflow: hidden; |
|||
position: relative; |
|||
|
|||
.scroll-box { |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
.send-bar { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 3rpx; |
|||
border: #dddddd solid 1px; |
|||
background-color: #eeeeee; |
|||
|
|||
.iconfont{ |
|||
font-size: 50rpx; |
|||
} |
|||
.send-text { |
|||
flex: 1; |
|||
background-color: #f8f8f8 !important; |
|||
overflow: auto; |
|||
margin: 0rpx 12rpx; |
|||
padding: 12rpx; |
|||
background-color: #fff; |
|||
border-radius: 10rpx; |
|||
max-height: 225rpx; |
|||
min-height: 65rpx; |
|||
box-sizing: border-box; |
|||
|
|||
.send-text-area { |
|||
width: 100%; |
|||
} |
|||
|
|||
} |
|||
|
|||
.btn-send { |
|||
text-align: right; |
|||
} |
|||
} |
|||
|
|||
.chat-tools { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
justify-content: space-between; |
|||
background-color: whitesmoke; |
|||
|
|||
.chat-tools-item { |
|||
width: 140rpx; |
|||
padding: 20rpx; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
|
|||
.tool-icon { |
|||
padding: 15rpx; |
|||
font-size: 60rpx; |
|||
background-color: white; |
|||
border-radius: 13%; |
|||
} |
|||
|
|||
.tool-name { |
|||
height: 50rpx; |
|||
line-height: 50rpx; |
|||
font-size: 25rpx; |
|||
flex: 3; |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
} |
|||
</style> |
|||
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 971 B |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 1015 B |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 824 B |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |