<script setup> /** * 聊天页面 */ // 腾讯云聊天 import TencentCloudChat from '@tencentcloud/chat'; import { ref, reactive, nextTick, onUnmounted, onMounted, 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 newsTemplate from './components/news-temp' // 表情 import emoji from './emoji.vue' // 语音条 import JyVoice from './jy-voice.vue' // 加号菜单 import JyPlus from './jy-plus.vue' // 顶部 import apex from '@/components/header/apex.vue' import { useStore } from 'vuex' const { proxy } = getCurrentInstance() const store = useStore() // 聊天对象 const msg = reactive({ // 聊天对象 id: '', // 聊天类型 C2C单聊 GROUP群聊 type: '', // 群人数 num: '', // 是否客服聊天 isCustomer: false, }) // 输入的内容 const content = ref('') // 加载 const loading = ref(false) // 用户信息 const userinfo = computed(() => { let result = store.state.userinfo return result }) // 列表数据 const list = reactive({ // 消息列表 messageList: [], // 用于续拉,分页续拉时需传入该字段 nextReqMessageID: undefined, // 表示是否已经拉完所有消息 isCompleted: false, }) // 页面标题 const pageTitle = ref('') // 滚动条位置 const top = ref(0) // 工具条的高度 const toolHeight = ref(0) // 当前操作的元素 const messageItem = ref({}) // 工具栏状态 voice录音 input输入框 emoji表情 plus加号菜单 const toolStatus = ref('input') // video路径 const videoUrl = ref('') // 视频上下文 const videoContext = ref(null) // 红包对象 const redPacket = reactive({}) // 输入框聚焦 const inputFocus = ref(false) onLoad(option => { // 标题 let title = '' // 标题 if (option.name) title = option.name // 聊天对象id if (option.msgId) msg.id = option.msgId // 聊天类型 if (option.type) { msg.type = option.type // 如果是群组 if (option.type == 'GROUP') { msg.num = option.num title = `(${option.num})${option.name}` } } // 标题 if (title) pageTitle.value = title // 是否客服 if (option.isCustomer) msg.isCustomer = option.isCustomer // 开启消息监听 addListener() // 获取历史消息 getHistory({ callback: scrollToBottom }) // #ifdef APP uni.onKeyboardHeightChange((rs) => { ghostBox.value.height = rs.height + 'px' nextTick(() => { scrollToBottom() }) }) // #endif }) onReady(() => { uni.createSelectorQuery().in(proxy).select('#tool').boundingClientRect((rect) => { toolHeight.value = rect.height }).exec(); // videoContext.value = uni.createVideoContext('video') }) onPageScroll((ev) => { onContentScroll(ev) }) onUnload(() => { // #ifdef APP uni.offKeyboardHeightChange(() => {}) // #endif videoContext.value.stop() }) // 开启监听消息 function addListener() { let onMessageReceived = function(event) { console.log('TencentCloudChat.EVENT.MESSAGE_RECEIVED', event) setTimeout(() => { // 获取历史记录 getHistory({ msgId: '', limit: 1, }) }, 200) } uni.$chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, onMessageReceived); } // 监听内容滚动 function onContentScroll(ev) { if (ev.scrollTop == 50) getMoreHistroy() debounce(() => { top.value = ev.detail.scrollTop }) } // 点击发送 function handleSend() { inputFocus.value = false // 发送消息 sendMsg({ query: { formId: userinfo.value.id, toUserId: msg.id, msgType: TencentCloudChat.TYPES.MSG_TEXT, }, data: { text: content.value }, success: () => { // 清空已发送的消息 content.value = '' inputFocus.value = true } }) } /** * 加号菜单发送 * @param {Object} message 消息对象 */ function handlePlusSend(message) { sendMsg(message) } /** * 发送消息 * @param {Object} param */ function sendMsg(param) { // let request = api.news.sendUserMsg // if (msg.type == 'GROUP') request = api.news.sendGroupMsg // if (msg.isCustomer) request = api.news.sendCusomterService // 发送消息 request({ query: param.query, data: param.data, }).then((rs) => { if (rs.code == 200) { // getHistory({ // msgId: '', // limit: 1, // }) param.success ? param.success() : '' return } util.alert({ content: rs.msg, showCancel: false, }) }).catch((rs) => { console.log('sendMsg error:', rs); }) } /** * 打开红包详情 * @param {Object} ev */ function handleRedPacket(ev) { messageItem.value = ev api.news.getRedPacketInfo({ query: { // 红包id redPacketId: ev.callbackData.callback_json[0].businessId } }).then(rs => { if (rs.code == 200) { proxy.$refs.RedPacketRef.open() Object.assign(redPacket, rs.data) return } util.alert({ content: rs.msg, showCancel: false, }) }) } // 领取红包 function handleOpenReadPacket() { // 如果不能领取 if (redPacket.redStatus == false) return // 红包过期 // if (redPacket.isStale == 1) return // 抢红包 api.news.getRedPacket({ query: { // 红包id redPacketId: redPacket.id, // 领取人id userId: userinfo.value.id, // 群聊类型 sendType: { 'C2C': '1', 'GROUP': '2', } [msg.type], } }).then(rs => { if (rs.code == 200) { // 修改领取状态 redPacket.redStatus = false // 获取金额 redPacket.amount = rs.data 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' else toolStatus.value = val } // 获取更多消息记录 function getMoreHistroy() { // 获取第一条消息记录 if (list.total <= list.data.length) return getHistory({ msgId: list.data[0].id }) } /** * 获取历史记录 * @param {Object} param */ function getHistory(param = {}) { // 验证sdk是否准备完毕 // let isReady = uni.$chat.isReady(); // if (!isReady && userinfo.value.id) { // setTimeout(function() { // getHistory() // }, 200); // return // } loading.value = true console.log('getHistory', msg, `${msg.type}${msg.id}`) // 获取历史记录 getHistoryMsg({ conversationID: `${msg.type}${msg.id}`, }).then(res => { console.log('getHistoryMsg', res) if (res.code === 0) { // 结果 const result = res.data if (!list.nextReqMessageID) list.messageList.length = 0 // 下次拉取的消息id list.nextReqMessageID = result.nextReqMessageID // 是否拉取完毕 list.isCompleted = result.isCompleted // 消息体 list.messageList = result.messageList.map(item => { console.log('msg item', item) return item }) nextTick(() => { param.callback && param.callback() }) return } util.alert({ content: res.msg, showCancel: false, }) }).catch(rs => { console.log('err', rs) }).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 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() } // 监听视频是否全屏 function onScreenChange(ev) { if (!ev.fullScreen) videoContext.value.pause() } /** * 更多 * @param {Object} ev */ function handleMore(ev) { // 配置 const url = { 'GROUP': util.setUrl('/pages/news/detail/group', { groupId: msg.id, }), 'C2C': util.setUrl('/pages/index/videoHome', { userId: msg.id, }) } [msg.type] // 跳转详情 uni.navigateTo({ url, }) } </script> <template> <apex :title="pageTitle"> <template #right> <view> <uni-icons type="more-filled" size="40rpx" @click="handleMore" /> </view> </template> </apex> <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.messageList" :key="index"> <!-- 系统消息 --> <template v-if="item.from == 'administrator'"></template> <!-- 群提示消息 --> <template v-else-if="TencentCloudChat.TYPES.MSG_GRP_TIP == item.type"> <template v-if="item.payload.operationType == 1"> <template v-if="item.payload.memberList"> <view class="systemMsg"> <text>{{item.payload.operatorInfo.nick}} 邀请</text> <text class="ml10" v-for="(user,index) in item.payload.memberList">{{user.nick}}</text> <text class="ml10">加入群聊</text> </view> </template> <template v-else> <view class="systemMsg">{{item.payload.operatorInfo.nick}} 加入了群聊</view> </template> </template> <template v-else-if="item.payload.operationType == 2"> <view class="systemMsg">{{item.payload.operatorInfo.nick}} 退出了群聊</view> </template> <template v-else-if="item.payload.operationType == 3"> <view class="systemMsg"> <text>{{item.payload.operatorInfo.nick}} 已将</text> <text class="ml10" v-for="(user,index) in item.payload.memberList">{{user.nick}}</text> <text class="ml10">移出群聊</text> </view> </template> <template v-else-if="item.payload.operationType == 4"> <view class="systemMsg">有群成员被设为管理员</view> </template> <template v-else-if="item.payload.operationType == 5"> <view class="systemMsg">有群成员被撤销管理员</view> </template> <template v-else-if="item.payload.operationType == 6"> <view class="systemMsg"> <text>{{item.payload.operatorInfo.nick}} 修改了群</text> <template v-if="item.payload.newGroupProfile.avatar"> <text>头像</text> </template> <template v-else-if="item.payload.newGroupProfile.groupName"> <text>名称为 {{item.payload.newGroupProfile.groupName}}</text> </template> <template v-else> <text>资料</text> </template> <text></text> </view> </template> <template v-else-if="item.payload.operationType == 7"> <view class="systemMsg">群成员禁言</view> </template> </template> <!-- 群系统通知消息 --> <template v-else-if="TencentCloudChat.TYPES.MSG_GRP_SYS_NOTICE == item.type"> </template> <!-- 普通消息 --> <template v-else> <view class="message" :class="[item.from == userinfo.id ? 'self' : 'friend']"> <image :src="item.avatar" class="avatar wh80" mode="aspectFill" /> <view class="df fdc mlr20"> <!-- 昵称 --> <view class="df fdc c999 f20" v-if="item.from != userinfo.id && msg.type == 'GROUP'"> <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 bfff" id="tool"> <view class="tool-group"> <!-- 语音 --> <image src="/static/news-voice.png" 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="发送" :focus="inputFocus" /> </template> <!-- 表情 --> <image src="/static/news-emoji.png" mode="widthFix" class="thumb" @click="handleTool('emoji')"></image> <!-- 加号 --> <template v-if="!content"> <image src="/static/news-plus.png" 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="RedPacketRef" 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="redPacket.fromUrl" mode="scaleToFill" /> </view> <view class="ml15 f32">{{ redPacket.fromName }}的红包</view> </view> <view class="app_name mt15 mlr30 tac f40">{{ redPacket.blessing }}</view> <!-- --> <view class="cfff f32 tac mt50" v-if="redPacket.isStale == 1">红包已过期</view> <view class="cfff f32 tac mt50" v-else-if="redPacket.remainingCount == 0">来晚啦,红包已被抢完</view> <!-- redPacket.redStatus true可以领取 false不可领取 --> <view class="amount f32" v-if="!redPacket.redStatus"> <text class="">已领取</text> <text class="value">{{ redPacket.amount }}</text> <text class="unit" v-if="redPacket.payType == 1">余额</text> <text class="unit" v-else-if="redPacket.payType == 2">积分</text> </view> </view> <view class="open_rbag_btn pr fmid" @click="handleOpenReadPacket"> <text v-if="redPacket.amount">已</text> <text>开</text> </view> </view> <view class="tac mt35" @click.stop="$refs.RedPacketRef.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; } // 系统消息 .systemMsg { margin: 30rpx 0; text-align: center; color: #999; font-size: 22rpx; } // .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>