mirror of
https://gitee.com/beecue/fastbee.git
synced 2025-12-17 16:36:03 +08:00
智慧宿舍系统小程序
This commit is contained in:
337
wechat/miniprogram/components/chatroom/chatroom.js
Normal file
337
wechat/miniprogram/components/chatroom/chatroom.js
Normal file
@@ -0,0 +1,337 @@
|
||||
const FATAL_REBUILD_TOLERANCE = 10
|
||||
const SETDATA_SCROLL_TO_BOTTOM = {
|
||||
scrollTop: 100000,
|
||||
scrollWithAnimation: true,
|
||||
}
|
||||
|
||||
Component({
|
||||
properties: {
|
||||
envId: String,
|
||||
collection: String,
|
||||
groupId: String,
|
||||
groupName: String,
|
||||
userInfo: Object,
|
||||
onGetUserInfo: {
|
||||
type: Function,
|
||||
},
|
||||
getOpenID: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
chats: [],
|
||||
textInputValue: '',
|
||||
openId: '',
|
||||
scrollTop: 0,
|
||||
scrollToMessage: '',
|
||||
hasKeyboard: false,
|
||||
},
|
||||
|
||||
methods: {
|
||||
onGetUserInfo(e) {
|
||||
this.properties.onGetUserInfo(e)
|
||||
},
|
||||
|
||||
getOpenID() {
|
||||
return this.properties.getOpenID()
|
||||
},
|
||||
|
||||
mergeCommonCriteria(criteria) {
|
||||
return {
|
||||
groupId: this.data.groupId,
|
||||
...criteria,
|
||||
}
|
||||
},
|
||||
|
||||
async initRoom() {
|
||||
this.try(async () => {
|
||||
await this.initOpenID()
|
||||
|
||||
const { envId, collection } = this.properties
|
||||
this.db = wx.cloud.database({
|
||||
env: envId,
|
||||
})
|
||||
const db = this.db
|
||||
const _ = db.command
|
||||
|
||||
const { data: initList } = await db.collection(collection).where(this.mergeCommonCriteria()).orderBy('sendTimeTS', 'desc').get()
|
||||
|
||||
console.log('init query chats', initList)
|
||||
|
||||
this.setData({
|
||||
chats: initList.reverse(),
|
||||
scrollTop: 10000,
|
||||
})
|
||||
|
||||
this.initWatch(initList.length ? {
|
||||
sendTimeTS: _.gt(initList[initList.length - 1].sendTimeTS),
|
||||
} : {})
|
||||
}, '初始化失败')
|
||||
},
|
||||
|
||||
async initOpenID() {
|
||||
return this.try(async () => {
|
||||
const openId = await this.getOpenID()
|
||||
|
||||
this.setData({
|
||||
openId,
|
||||
})
|
||||
}, '初始化 openId 失败')
|
||||
},
|
||||
|
||||
async initWatch(criteria) {
|
||||
this.try(() => {
|
||||
const { collection } = this.properties
|
||||
const db = this.db
|
||||
const _ = db.command
|
||||
|
||||
console.warn(`开始监听`, criteria)
|
||||
this.messageListener = db.collection(collection).where(this.mergeCommonCriteria(criteria)).watch({
|
||||
onChange: this.onRealtimeMessageSnapshot.bind(this),
|
||||
onError: e => {
|
||||
if (!this.inited || this.fatalRebuildCount >= FATAL_REBUILD_TOLERANCE) {
|
||||
this.showError(this.inited ? '监听错误,已断开' : '初始化监听失败', e, '重连', () => {
|
||||
this.initWatch(this.data.chats.length ? {
|
||||
sendTimeTS: _.gt(this.data.chats[this.data.chats.length - 1].sendTimeTS),
|
||||
} : {})
|
||||
})
|
||||
} else {
|
||||
this.initWatch(this.data.chats.length ? {
|
||||
sendTimeTS: _.gt(this.data.chats[this.data.chats.length - 1].sendTimeTS),
|
||||
} : {})
|
||||
}
|
||||
},
|
||||
})
|
||||
}, '初始化监听失败')
|
||||
},
|
||||
|
||||
onRealtimeMessageSnapshot(snapshot) {
|
||||
console.warn(`收到消息`, snapshot)
|
||||
|
||||
if (snapshot.type === 'init') {
|
||||
this.setData({
|
||||
chats: [
|
||||
...this.data.chats,
|
||||
...[...snapshot.docs].sort((x, y) => x.sendTimeTS - y.sendTimeTS),
|
||||
],
|
||||
})
|
||||
this.scrollToBottom()
|
||||
this.inited = true
|
||||
} else {
|
||||
let hasNewMessage = false
|
||||
let hasOthersMessage = false
|
||||
const chats = [...this.data.chats]
|
||||
for (const docChange of snapshot.docChanges) {
|
||||
switch (docChange.queueType) {
|
||||
case 'enqueue': {
|
||||
hasOthersMessage = docChange.doc._openid !== this.data.openId
|
||||
const ind = chats.findIndex(chat => chat._id === docChange.doc._id)
|
||||
if (ind > -1) {
|
||||
if (chats[ind].msgType === 'image' && chats[ind].tempFilePath) {
|
||||
chats.splice(ind, 1, {
|
||||
...docChange.doc,
|
||||
tempFilePath: chats[ind].tempFilePath,
|
||||
})
|
||||
} else chats.splice(ind, 1, docChange.doc)
|
||||
} else {
|
||||
hasNewMessage = true
|
||||
chats.push(docChange.doc)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setData({
|
||||
chats: chats.sort((x, y) => x.sendTimeTS - y.sendTimeTS),
|
||||
})
|
||||
if (hasOthersMessage || hasNewMessage) {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async onConfirmSendText(e) {
|
||||
this.try(async () => {
|
||||
if (!e.detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const { collection } = this.properties
|
||||
const db = this.db
|
||||
const _ = db.command
|
||||
|
||||
const doc = {
|
||||
_id: `${Math.random()}_${Date.now()}`,
|
||||
groupId: this.data.groupId,
|
||||
avatar: this.data.userInfo.avatarUrl,
|
||||
nickName: this.data.userInfo.nickName,
|
||||
msgType: 'text',
|
||||
textContent: e.detail.value,
|
||||
sendTime: new Date(),
|
||||
sendTimeTS: Date.now(), // fallback
|
||||
}
|
||||
|
||||
this.setData({
|
||||
textInputValue: '',
|
||||
chats: [
|
||||
...this.data.chats,
|
||||
{
|
||||
...doc,
|
||||
_openid: this.data.openId,
|
||||
writeStatus: 'pending',
|
||||
},
|
||||
],
|
||||
})
|
||||
this.scrollToBottom(true)
|
||||
|
||||
await db.collection(collection).add({
|
||||
data: doc,
|
||||
})
|
||||
|
||||
this.setData({
|
||||
chats: this.data.chats.map(chat => {
|
||||
if (chat._id === doc._id) {
|
||||
return {
|
||||
...chat,
|
||||
writeStatus: 'written',
|
||||
}
|
||||
} else return chat
|
||||
}),
|
||||
})
|
||||
}, '发送文字失败')
|
||||
},
|
||||
|
||||
async onChooseImage(e) {
|
||||
wx.chooseImage({
|
||||
count: 1,
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async res => {
|
||||
const { envId, collection } = this.properties
|
||||
const doc = {
|
||||
_id: `${Math.random()}_${Date.now()}`,
|
||||
groupId: this.data.groupId,
|
||||
avatar: this.data.userInfo.avatarUrl,
|
||||
nickName: this.data.userInfo.nickName,
|
||||
msgType: 'image',
|
||||
sendTime: new Date(),
|
||||
sendTimeTS: Date.now(), // fallback
|
||||
}
|
||||
|
||||
this.setData({
|
||||
chats: [
|
||||
...this.data.chats,
|
||||
{
|
||||
...doc,
|
||||
_openid: this.data.openId,
|
||||
tempFilePath: res.tempFilePaths[0],
|
||||
writeStatus: 0,
|
||||
},
|
||||
]
|
||||
})
|
||||
this.scrollToBottom(true)
|
||||
|
||||
const uploadTask = wx.cloud.uploadFile({
|
||||
cloudPath: `${this.data.openId}/${Math.random()}_${Date.now()}.${res.tempFilePaths[0].match(/\.(\w+)$/)[1]}`,
|
||||
filePath: res.tempFilePaths[0],
|
||||
config: {
|
||||
env: envId,
|
||||
},
|
||||
success: res => {
|
||||
this.try(async () => {
|
||||
await this.db.collection(collection).add({
|
||||
data: {
|
||||
...doc,
|
||||
imgFileID: res.fileID,
|
||||
},
|
||||
})
|
||||
}, '发送图片失败')
|
||||
},
|
||||
fail: e => {
|
||||
this.showError('发送图片失败', e)
|
||||
},
|
||||
})
|
||||
|
||||
uploadTask.onProgressUpdate(({ progress }) => {
|
||||
this.setData({
|
||||
chats: this.data.chats.map(chat => {
|
||||
if (chat._id === doc._id) {
|
||||
return {
|
||||
...chat,
|
||||
writeStatus: progress,
|
||||
}
|
||||
} else return chat
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onMessageImageTap(e) {
|
||||
wx.previewImage({
|
||||
urls: [e.target.dataset.fileid],
|
||||
})
|
||||
},
|
||||
|
||||
scrollToBottom(force) {
|
||||
if (force) {
|
||||
console.log('force scroll to bottom')
|
||||
this.setData(SETDATA_SCROLL_TO_BOTTOM)
|
||||
return
|
||||
}
|
||||
|
||||
this.createSelectorQuery().select('.body').boundingClientRect(bodyRect => {
|
||||
this.createSelectorQuery().select(`.body`).scrollOffset(scroll => {
|
||||
if (scroll.scrollTop + bodyRect.height * 3 > scroll.scrollHeight) {
|
||||
console.log('should scroll to bottom')
|
||||
this.setData(SETDATA_SCROLL_TO_BOTTOM)
|
||||
}
|
||||
}).exec()
|
||||
}).exec()
|
||||
},
|
||||
|
||||
async onScrollToUpper() {
|
||||
if (this.db && this.data.chats.length) {
|
||||
const { collection } = this.properties
|
||||
const _ = this.db.command
|
||||
const { data } = await this.db.collection(collection).where(this.mergeCommonCriteria({
|
||||
sendTimeTS: _.lt(this.data.chats[0].sendTimeTS),
|
||||
})).orderBy('sendTimeTS', 'desc').get()
|
||||
this.data.chats.unshift(...data.reverse())
|
||||
this.setData({
|
||||
chats: this.data.chats,
|
||||
scrollToMessage: `item-${data.length}`,
|
||||
scrollWithAnimation: false,
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async try(fn, title) {
|
||||
try {
|
||||
await fn()
|
||||
} catch (e) {
|
||||
this.showError(title, e)
|
||||
}
|
||||
},
|
||||
|
||||
showError(title, content, confirmText, confirmCallback) {
|
||||
console.error(title, content)
|
||||
wx.showModal({
|
||||
title,
|
||||
content: content.toString(),
|
||||
showCancel: confirmText ? true : false,
|
||||
confirmText,
|
||||
success: res => {
|
||||
res.confirm && confirmCallback()
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
ready() {
|
||||
global.chatroom = this
|
||||
this.initRoom()
|
||||
this.fatalRebuildCount = 0
|
||||
},
|
||||
})
|
||||
4
wechat/miniprogram/components/chatroom/chatroom.json
Normal file
4
wechat/miniprogram/components/chatroom/chatroom.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
85
wechat/miniprogram/components/chatroom/chatroom.wxml
Normal file
85
wechat/miniprogram/components/chatroom/chatroom.wxml
Normal file
@@ -0,0 +1,85 @@
|
||||
<view class="chatroom">
|
||||
<view class="header">
|
||||
<!-- display number of people in the room -->
|
||||
<view class="left"></view>
|
||||
<!-- room name -->
|
||||
<view class="middle">{{groupName}}</view>
|
||||
<!-- reserved -->
|
||||
<view class="right"></view>
|
||||
</view>
|
||||
|
||||
<!-- chats -->
|
||||
<scroll-view
|
||||
class="body"
|
||||
scroll-y
|
||||
scroll-with-animation="{{scrollWithAnimation}}"
|
||||
scroll-top="{{scrollTop}}"
|
||||
scroll-into-view="{{scrollToMessage}}"
|
||||
bindscrolltoupper="onScrollToUpper"
|
||||
>
|
||||
<view
|
||||
wx:for="{{chats}}"
|
||||
wx:key="{{item._id}}"
|
||||
id="item-{{index}}"
|
||||
class="message {{openId == item._openid ? 'message__self' : ''}}"
|
||||
>
|
||||
<image
|
||||
class="avatar"
|
||||
src="{{item.avatar}}"
|
||||
mode="scaleToFill"
|
||||
></image>
|
||||
<view class="main">
|
||||
<view class="nickname">{{item.nickName}}</view>
|
||||
<block wx:if="{{item.msgType === 'image'}}">
|
||||
<view class="image-wrapper">
|
||||
<view class="loading" wx:if="{{item.writeStatus > -1}}">{{item.writeStatus}}%</view>
|
||||
<image
|
||||
src="{{item.tempFilePath || item.imgFileID}}"
|
||||
data-fileid="{{item.tempFilePath || item.imgFileID}}"
|
||||
class="image-content"
|
||||
style="{{item.imgStyle}}"
|
||||
mode="scallToFill"
|
||||
bindtap="onMessageImageTap"></image>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="text-wrapper">
|
||||
<view class="loading" wx:if="{{item.writeStatus === 'pending'}}">···</view>
|
||||
<view class="text-content">{{item.textContent}}</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- message sender -->
|
||||
<view class="footer">
|
||||
<view class="message-sender" wx:if="{{userInfo}}">
|
||||
<input
|
||||
class="text-input"
|
||||
type="text"
|
||||
confirm-type="send"
|
||||
bindconfirm="onConfirmSendText"
|
||||
cursor-spacing="20"
|
||||
value="{{textInputValue}}"
|
||||
></input>
|
||||
|
||||
<image
|
||||
src="./photo.png"
|
||||
class="btn-send-image"
|
||||
mode="scaleToFill"
|
||||
bindtap="onChooseImage"
|
||||
></image>
|
||||
</view>
|
||||
|
||||
<view class="message-sender" wx:if="{{!userInfo}}">
|
||||
<button
|
||||
open-type="getUserInfo"
|
||||
bindgetuserinfo="onGetUserInfo"
|
||||
class="userinfo"
|
||||
>请先登录后参与聊天</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
161
wechat/miniprogram/components/chatroom/chatroom.wxss
Normal file
161
wechat/miniprogram/components/chatroom/chatroom.wxss
Normal file
@@ -0,0 +1,161 @@
|
||||
.chatroom {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chatroom .header {
|
||||
flex-basis: fit-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 20rpx 0 30rpx;
|
||||
font-size: 30rpx;
|
||||
/* background: rgb(34, 187, 47);
|
||||
color: rgba(255, 255, 255, 1) */
|
||||
/* font-family: 'Microsoft YaHei' */
|
||||
}
|
||||
|
||||
.chatroom .header .left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chatroom .header .middle {
|
||||
flex: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chatroom .header .right {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chatroom .body {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: rgb(237,237,237);
|
||||
padding-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.body .message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
margin: 12rpx 0;
|
||||
}
|
||||
|
||||
.body .message.message__self {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.body .message .avatar {
|
||||
position: relative;
|
||||
top: 5rpx;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 5rpx;
|
||||
margin: 15rpx;
|
||||
}
|
||||
|
||||
.body .message .main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.body .message.message__self .main {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.body .message .nickname {
|
||||
font-size: 24rpx;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.body .message .text-content {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
margin: 2px 0 0 0;
|
||||
padding: 4px 10px;
|
||||
font-size: 30rpx;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.body .message.message__self .text-content {
|
||||
background-color: paleturquoise;
|
||||
}
|
||||
|
||||
.body .message .text-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.body .message.message__self .text-wrapper .loading{
|
||||
font-size: 16rpx;
|
||||
margin-right: 18rpx;
|
||||
}
|
||||
|
||||
.body .message .image-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.body .message .image-content {
|
||||
max-width: 240rpx;
|
||||
max-height: 240rpx;
|
||||
}
|
||||
|
||||
.body .message.message__self .image-wrapper .loading {
|
||||
font-size: 20rpx;
|
||||
margin-right: 18rpx;
|
||||
}
|
||||
|
||||
.chatroom .footer {
|
||||
flex-basis: fit-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-top: 1px solid #ddd;
|
||||
font-size: 10rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
background: rgb(246,246,246);
|
||||
}
|
||||
|
||||
.chatroom .footer .message-sender {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.message-sender .text-input {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
padding: 3px 6px;
|
||||
margin: 0 10px 0 5px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.message-sender .btn-send-image {
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
button {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
button.userinfo {
|
||||
background: darkturquoise;
|
||||
color: aliceblue;
|
||||
padding: 0 100rpx;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
}
|
||||
BIN
wechat/miniprogram/components/chatroom/dots.gif
Normal file
BIN
wechat/miniprogram/components/chatroom/dots.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 244 KiB |
BIN
wechat/miniprogram/components/chatroom/photo.png
Normal file
BIN
wechat/miniprogram/components/chatroom/photo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
Reference in New Issue
Block a user