jiuyiUniapp/jiuyi2/pages/news/chat/chat.vue

672 lines
15 KiB
Vue

<script setup>
/**
* 聊天页面
*/
// 腾讯云聊天
import TencentCloudChat from '@tencentcloud/chat';
import {
ref,
reactive,
nextTick,
onUnmounted,
onMounted,
inject,
provide,
computed,
getCurrentInstance,
watch,
} from 'vue'
// api
import api from '@/api/index.js'
// 工具库
import util from '@/common/js/util.js'
import {
onLoad,
onReady,
onPageScroll,
onUnload
} from "@dcloudio/uni-app"
import {
getHistoryMsg
} from './fn.js'
import newsTempSystem from './components/news-temp-system'
import newsTemplate from './components/news-temp'
import NewsEmoji from '@/static/news-emoji.png'
import NewsPlus from '@/static/news-plus.png'
import NewsVoice from '@/static/news-voice.png'
import emoji from './emoji.vue'
import JyVoice from './jy-voice.vue'
import JyPlus from './jy-plus.vue'
import {
useStore
} from 'vuex'
const {
proxy
} = getCurrentInstance()
const store = useStore()
// 聊天对象
const msg = reactive({
// 聊天对象
id: '',
// 聊天类型
type: '',
})
// 输入的内容
const content = ref('')
const loading = ref(false)
const isLast = ref(false)
// 用户信息
const userinfo = computed(() => {
let result = store.state.userinfo
return result
})
//显示的数据
const list = ref([])
// 消息分页id 腾讯云im需要
const nextReqMessageID = ref(undefined)
// 滚动条位置
const top = ref(0)
// 工具条的高度
const toolHeight = ref(0)
// 页码
const page = ref(1)
// 当前操作的元素
const messageItem = ref({})
// 工具栏状态 voice录音 input输入框 emoji表情 plus加号菜单
const toolStatus = ref('input')
// video路径
const videoUrl = ref('')
// 视频上下文
const videoContext = ref(null)
onLoad(option => {
// 用户昵称
if (option.name) uni.setNavigationBarTitle({
title: option.name
})
// 用户id
if (option.msgId) msg.id = option.msgId
// 聊天类型
if (option.type) msg.type = option.type
// 开启消息监听
addListener()
// 获取历史消息
getHistory(scrollToBottom)
//
uni.onKeyboardHeightChange((rs) => {
ghostBox.value.height = rs.height + 'px'
nextTick(() => {
scrollToBottom()
})
})
})
onReady(() => {
uni.createSelectorQuery().in(proxy).select('#tool').boundingClientRect((rect) => {
toolHeight.value = rect.height
}).exec();
//
videoContext.value = uni.createVideoContext('video')
})
onPageScroll((ev) => {
onContentScroll(ev)
})
onUnload(() => {
uni.offKeyboardHeightChange(() => {})
videoContext.value.stop()
})
// 开启监听消息
function addListener() {
let onMessageReceived = function(event) {
//
list.value.push(...event.data)
}
uni.$chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived);
}
// 移除监听消息
function removeListener() {
uni.$chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED);
}
// 获取更多消息记录
function getMoreHistroy() {
if (nextReqMessageID.value) getHistory()
}
// 获取历史
function getHistory(callback = () => {}) {
// 验证sdk是否准备完毕
let isReady = uni.$chat.isReady();
//
if (!isReady && userinfo.value.userId) {
setTimeout(function() {
getHistory(callback)
}, 200);
return
}
if (loading.value) {
// 提示加载中
util.showToastAndRedirect("加载中")
return
}
if (!isLast.value) {
loading.value = true
// 获取历史记录
getHistoryMsg({
msgId: msg.id,
chatType: msg.type,
nextReqMessageID: nextReqMessageID.value,
}).then(res => {
const result = res.data
if (page.value == 1) list.value.length = 0
// 消息列表
list.value.unshift(...result.messageList.map(item => {
try {
// 1普通消息 2系统消息
item.messageType = 1
// console.log('check 1', item.type, TencentCloudChat.TYPES.MSG_CUSTOM, item.payload.data)
// 如果是自定义消息
if (item.type === TencentCloudChat.TYPES.MSG_CUSTOM && item.payload.data) {
if (item.payload.data) item.payload.data = JSON.parse(item.payload.data)
if (item.payload.data.data) item.payload.data.data = JSON.parse(item.payload.data.data)
// 视频消息类型 新建群聊
if (['1', 'group_create'].includes(item.payload.data.businessID)) {
item.messageType = 2
}
}
} catch (err) {
console.log('map catch', err)
}
return item
}))
page.value++
console.log('getHistoryMsg then', result.messageList.length, list.value, res)
// 用于续拉,分页续拉时需传入该字段。
nextReqMessageID.value = result.nextReqMessageID || undefined;
nextTick(() => {
callback()
})
}).finally(() => {
loading.value = false
})
}
}
// 滚动至底部
function scrollToBottom() {
uni.createSelectorQuery().in(proxy).select('#scroll-content').boundingClientRect((res) => {
top.value = res.height
uni.pageScrollTo({
scrollTop: top.value,
duration: 0
})
// console.log('top.value', top.value)
}).exec();
}
// 防抖
function debounce(func, wait = 500) {
let timeout = null;
return function(...args) {
clearTimeout(timeout)
timeout = setTimeout(() => {
func.apply(this, args)
}, wait);
}
}
// 监听内容滚动
function onContentScroll(ev) {
if (ev.scrollTop == 0) getMoreHistroy()
debounce(() => {
top.value = ev.detail.scrollTop
})
}
// 点击发送
function handleSend() {
// 创建文本消息
let message = uni.$chat.createTextMessage({
to: msg.id,
conversationType: msg.type,
payload: {
text: content.value
},
// 消息已读回执
needReadReceipt: true,
// 消息自定义数据(云端保存,会发送到对端,程序卸载重装后还能拉取到)
// cloudCustomData: 'content.value',
});
// 发送消息
sendMsg({
message,
success: () => {
// 清空已发送的消息
content.value = ''
}
})
}
/**
* 加号菜单发送
* @param {Object} message 消息对象
*/
function handlePlusSend(message) {
console.log('handlePlusSend', message)
sendMsg({
message,
})
}
/**
* 发送消息
* @param {Object} param
*/
function sendMsg(param) {
// 发送消息
uni.$chat.sendMessage(param.message).then((rs) => {
console.log('rs', rs)
param.success ? param.success() : ''
//
getHistory(scrollToBottom())
}).catch((rs) => {
console.log('sendMsg error:', rs);
})
}
/**
* 打开红包
* @param {Object} ev
*/
function handleRedPacket(ev) {
messageItem.value = ev
api.news.getRedbag({
data: {
// 红包id
bagId: ev.payload.data.id
}
}).then(rs => {
console.log('getRedbag', rs)
if (rs.code == 200) {
const result = rs.data
messageItem.value.payload.data = {
...result,
businessID: 'redPacket',
}
// 同步修改消息
let message = {
...messageItem.value
}
message.payload.data = JSON.stringify(msg.payload.data)
uni.$chat.modifyMessage(message).then(rs => {
console.log('modifyMessage success', rs)
}).catch(rs => {
console.log('modifyMessage catch', rs)
})
return
}
util.alert({
content: rs.msg,
showCancel: false,
})
})
proxy.$refs.RedPacket.open()
}
// 领取红包
function handleOpenReadPacket() {
//
if (messageItem.value.payload.data.status != 0) return
api.news.grabred({
data: {
// 红包id
id: messageItem.value.payload.data.id
}
}).then(rs => {
if (rs.code == 200) {
handleRedPacket(messageItem.value)
return
}
util.alert({
content: rs.msg,
showCancel: false,
})
})
}
// 选择的emoji
function emojiTap(val) {
content.value = content.value + val
}
// 点击工具栏
function handleTool(val) {
if (toolStatus.value === val) {
toolStatus.value = 'input'
return
}
toolStatus.value = val
}
// 输入框聚焦
function onFocus() {
handleTool('input')
}
// 输入语音
function voiceSend(message) {
console.log('handlePlusSend', message)
sendMsg({
message,
})
}
// 监听滚动
const handleScroll = (e) => {
if (e.detail.scrollTop === 0) {
getHistory()
}
}
// 撑起键盘的高度 打开该元素
const showGhost = ref(false)
// 给元素加高度
const ghostBox = ref({
height: '0px',
duration: '0.25s'
})
// 监听键盘高度变化
function keyboardheightchange(res) {
ghostBox.value = res.detail
nextTick(() => {
showGhost.value = res.detail.height > 0 ? true : false
})
}
/**
* 看视频
* @param {Object} item 聊天消息对象
*/
function handleViewVideo(item) {
videoUrl.value = item.payload.videoUrl
// 进入全屏
videoContext.value.requestFullScreen()
videoContext.value.requestFullScreen()
}
// 监听视频是否全屏
function onScreenChange(ev) {
console.log('onScreenChange', ev)
if (!ev.fullScreen) videoContext.value.pause()
}
</script>
<template>
<view class="app">
<scroll-view class="scroll-view" scroll-y :scroll-with-animation="true" :scroll-top="top" @scroll="onContentScroll" @scrolltoupper="getMoreHistroy">
<view id="scroll-content" style="padding: 30rpx 30rpx">
<view v-for="(item, index) in list" :key="index">
<!-- 系统消息 -->
<template v-if="item.messageType == 2">
<!-- 创建群聊 -->
<view class="mtb30 tac c999 f22" v-if="item.payload.data.businessID === 'group_create'">群组创建成功</view>
</template>
<!-- 普通消息 -->
<template v-else-if="item.messageType == 1">
<view class="message" :class="[item.from === userinfo.userId ? 'self' : 'friend']">
<!-- 如果是我自己 -->
<view v-if="item.from === userinfo.userId">
<image :src="util.format_url(userinfo.userPortrait, 'img')" class="avatar" mode="widthFix" />
</view>
<!-- 如果是用户 -->
<view v-else>
<image :src="item.avatar" class="avatar" mode="widthFix" />
</view>
<view class="df fdc mlr20">
<!-- 昵称 -->
<view class="df fdc" v-if="item.from != userinfo.userId">
<view class="name">{{ item.nick }}</view>
</view>
<!-- 消息 -->
<newsTemplate :item="item" :msg="msg" @openRedBag="handleRedPacket" @viewVideo="handleViewVideo" />
</view>
</view>
</template>
</view>
<view v-if="showGhost" :style="{ height: `${ghostBox.height}px`, transition: `${ghostBox.duration}s` }"></view>
<view class="ghost" :style="{height: toolHeight + 'px'}"></view>
</view>
</scroll-view>
</view>
<view class="tool" id="tool">
<view class="tool-group" style="background: #F6F6F6;">
<!-- 语音 -->
<image :src="NewsVoice" mode="widthFix" class="thumb" @click="handleTool('voice')"></image>
<!-- 摁住说话 -->
<template v-if="toolStatus == 'voice'">
<JyVoice @send="voiceSend" :msg="msg" />
</template>
<!-- 输入框 -->
<template v-if="toolStatus != 'voice'">
<uni-easyinput @focus="onFocus" type="text" v-model="content" :clearable="false" class="input" :adjust-position="false" @keyboardheightchange="keyboardheightchange" placeholder="请输入你的问题" confirmType="发送" />
</template>
<!-- 表情 -->
<image :src="NewsEmoji" mode="widthFix" class="thumb" @click="handleTool('emoji')"></image>
<!-- 加号 -->
<template v-if="!content">
<image :src="NewsPlus" mode="widthFix" class="thumb" @click="handleTool('plus')" />
</template>
<!-- 文本发送按钮 -->
<template v-else>
<view class="send" @click="handleSend">发送</view>
</template>
</view>
<view v-if="showGhost" :style="{ height: `${ghostBox.height}px`, transition: `${ghostBox.duration}s` }"></view>
<!-- 表情 -->
<template v-if="toolStatus == 'emoji'">
<emoji @setEmoj="emojiTap"></emoji>
</template>
<!-- 加号 -->
<template v-if="toolStatus == 'plus'">
<JyPlus @send="handlePlusSend" :msg="msg"></JyPlus>
</template>
</view>
<!-- -->
<video :src="videoUrl" id="video" @fullscreenchange="onScreenChange" />
<!-- 红包封面 -->
<uni-popup ref="RedPacket" type="center">
<view class="red-bag br20" @touchmove.stop.prevent="">
<view class="rbag_top">
<view class="user fmid">
<view class="avatar">
<image class="wh80 cir" :src="messageItem.avatar" mode="scaleToFill" />
</view>
<view class="ml15 f32">{{messageItem.nick}}的红包</view>
</view>
<view class="app_name mt15 tac f40">{{ messageItem.payload.data.name }}</view>
<view class="amount f32" v-if="messageItem.payload.data.receive">
<text class="">已领取</text>
<text class="value">{{messageItem.payload.data.randomAmount}}</text>
<text class="unit" v-if="messageItem.payload.data.type == 1">积分</text>
<text class="unit" v-else-if="messageItem.payload.data.type == 2">余额</text>
</view>
</view>
<view class="open_rbag_btn pr fmid" @click="handleOpenReadPacket">
<text v-if="messageItem.payload.data.receive">已</text>
<text>开</text>
</view>
</view>
<view class="tac mt35" @click.stop="$refs.RedPacket.close()">
<uni-icons type="close" color="#fbd977" size="32" />
</view>
</uni-popup>
</template>
<style lang="scss" scoped>
@import './index.scss';
//
#video {
position: fixed;
top: 100%;
left: 0;
}
//
.red-bag {
position: relative;
width: 528rpx;
height: 60vh;
color: #ECCD97;
background-color: #e0534a;
box-shadow: 0 0 20rpx #00000033;
.rbag_top {
padding-top: 60rpx;
height: 70%;
background-color: #e0534a;
border-radius: 0 0 500rpx 500rpx / 0 0 200rpx 200rpx;
box-shadow: 0 5rpx 5rpx rgba(0, 0, 0, 0.2);
.amount {
margin-top: 120rpx;
text-align: center;
letter-spacing: 1rpx;
.value {
font-size: 60rpx;
font-weight: bold;
}
}
}
.open_rbag_btn {
width: 180rpx;
height: 180rpx;
margin: -90rpx auto 0;
color: #fef5e8;
font-size: 74rpx;
font-weight: bold;
background-color: #ffd287;
box-shadow: 2rpx 2rpx 6rpx rgba(0, 0, 0, 0.2);
border-radius: 50%;
z-index: 1;
}
// 打开红包
.open_rbag_model {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: rgba(0, 0, 0, 0.3);
z-index: 1000;
.rbag_conbg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 80%;
height: 840rpx;
margin: auto;
z-index: 1001;
}
.open_rbag_con {
z-index: 1002;
.open_title {
height: 120rpx;
line-height: 120rpx;
text-align: center;
font-size: 38rpx;
letter-spacing: 2rpx;
color: #e46965;
}
.rbag_detail {
margin-top: 90rpx;
.open_money {
text-align: center;
font-size: 80rpx;
color: #c95948;
font-weight: bold;
display: flex;
justify-content: center;
.danwei {
font-size: 30rpx;
margin-left: 16rpx;
margin-top: 24rpx;
}
}
.open_tips {
text-align: center;
font-size: 30rpx;
color: #d26762;
margin-top: 30rpx;
}
}
.lookbag_box {
margin-top: 300rpx;
display: flex;
justify-content: center;
.lookbag_btn {
width: 70%;
height: 90rpx;
line-height: 90rpx;
text-align: center;
font-size: 32rpx;
color: #c95948;
letter-spacing: 2rpx;
background-color: #ffd356;
border-radius: 50rpx;
box-shadow: 0rpx 0rpx 4rpx rgba(0, 0, 0, 0.2);
}
}
.hide_btn {
position: absolute;
bottom: -110rpx;
left: 0;
right: 0;
width: 80rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
margin: 0 auto;
}
}
}
}
</style>