@ -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 |