...
 
Commits (20)
    https://gitcode.net/dcloud/uni-im/-/commit/1b8462d521bdf030c05778b1ef8f60fefb8f302e 修复 编译到 app 端报错的问题 2024-06-05T13:22:19+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/4ccbc20746755f55b2f6a7601263f9a6019d7a44 3.0.5 2024-06-05T14:17:01+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/eaf23db3a46c8b8053a8144ba3dc210682664c82 更新 push 推送结果不再返回给客户端 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/2a29fe9749cf28f66fc71d708332affcb13fcb18 更新 优化用户体验,把复制按钮和回复按钮换个位置,避免点回复误点成撤回 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/f5fc925f5c1c4fd5eafe1e5640d37fb274dea25f 优化 群公告的显示效果 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/beae4161ef74e82f73d6f8c8524114d3001c19b2 修复 当群公告被刷屏,导致后打开会话的用户看不到的问题 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/62d0da938db146de8dc0253e1d34cf0406cd42ee 修复 部分情况下富文本类型消息渲染出错的问题 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/75900adf634c0149e4e6ced3ff9ad2d8f2998207 优化 仅在会话列表点开“群”会话时显示 loading 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/255ae957b13c23456697726c648fb4af20addadd 修复 置顶会话存在未读公告,且此公告离当前会话最后一条消息在 30... 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io 修复 置顶会话存在未读公告,且此公告离当前会话最后一条消息在 30 条消息之前(这种公告会直接以弹窗展示),且此时快速切换到另一个会话(从插件市场点进来会自动切到指定会话)确会弹出置顶会话的公告内容,但点击后的关闭公告的逻辑不能同步到服务端的问题 https://gitcode.net/dcloud/uni-im/-/commit/acaaa53ae933cf27f16457d92bf25508ea971a1d 【重要更新】去掉所有nvue页面,消息输入框改用renderjs实现,消息列表改用翻转消息列表实现 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/a2811185a14c2d1f81b41ca0d5208bce218d6f52 修复 本地不存在的会话收到 1 条消息,角标会+2 的问题 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/d727d998182c8fb983c8d8a3cca8240eb02bf3a0 更新 修复草稿字样丢了的问题 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/21257be0af29e0dbe88b71d2f6146cc6c1c6da89 更新 优化样式 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/dbfac4fdb68023d4da2a58cac481887404760ac3 更新 新增群成员活跃时间(最后一场发消息时间)的字段 2024-06-17T17:28:45+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/3eb19dc5aac211d9a96cd292c2f411f178360c21 更新 修复部分平台,会在较早的时机执行 onAppActivateStateChange内的逻辑,此时currentPage还未初始化完成,引起的报错问题 2024-06-17T20:20:16+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/0ef754024d3b017b573cb17f293c94d81e2f066f 修复 部分情况下,插入的表情包的位置不正确的问题 2024-06-17T20:20:16+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/c7240327250f8ea7ee7a49ad52e9b12e1951d0f6 更新 小程序端消息输入框,暂不支持富文本输入(包括:@用户、图片表情包等,@用户后续会支持) 2024-06-17T20:20:16+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/b559574bc87fb11bfbef4c6d2fd170b63cda6485 更新 修复部分平台拖拽消息输入框高度的线层级不够的问题 2024-06-17T20:51:57+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/36cf743363f8ba9f3bf8852588b0e4f34c84f222 更新 flex.scss 2024-06-17T20:51:57+08:00 DCloud_JSON linju@dcloud.io https://gitcode.net/dcloud/uni-im/-/commit/e8de928bf437bc4a2c789b06f8a0bd6b2d8b44df 3.1.0 2024-06-17T20:54:13+08:00 DCloud_JSON linju@dcloud.io
## 3.1.0(2024-06-17)
- 【重要】去掉所有nvue页面,消息输入框改用renderjs实现,消息列表改用翻转消息列表实现
- 更新 新增群成员活跃时间(最后一场发消息时间)的字段,后续将利用此字段时间优化群消息送达顺序,群会话打开速度等
- 修复 本地不存在的会话收到 1 条消息,角标会+2 的问题
## 3.0.5(2024-06-05)
修复 编译到App端报`Cannot assign to "fileType" because it is a constant`的问题
## 3.0.4(2024-05-09)
- 优化 对于含有图片的消息数据,新增了图片的宽高值,使图片加载前可以固定容器高度,从而避免列表抖动
- 优化 多个会话切换的草稿功能,提升性能消息输入的性能
......
export default `😀,😁,😂,🤣,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗,😙,😚,☺️,🙂,🤗,🤩,🤔,🤨,😐,😑,😶,🙄,😏,😣,😥,😮,🤐,😯,😪,😫,😴,😌,😛,😜,😝,🤤,😒,😓,😔,😕,🙃,🤑,😲,☹️,🙁,😖,😞,😟,😤,😢,😭,😦,😧,😨,😩,🤯,😬,😰,😱,😳,🤪,😵,😡,😠,🤬,😷,🤒,🤕,🤢,🤮,🤧,😇,🤠,🤡,🤥,🤫,🤭,🧐,🤓,😈,👿,👹,👺,💀,☠️,👻,👽,🤖,😺,😸,😹,😻,😼,😽,🙀,😿,😾,🙈,🙉,🙊,👶,🧒,👦,👧,🧑,👨,👩,🧓,👴,👵,👨‍⚕️,👩‍⚕️,👨‍🎓,👩‍🎓,👨‍🏫,👩‍🏫,👨‍⚖️,👩‍⚖️,👨‍🌾,👩‍🌾,👨‍🍳,👩‍🍳,👨‍🔧,👩‍🔧,👨‍🏭,👩‍🏭,👨‍💼,👩‍💼,👨‍🔬,👩‍🔬,👨‍💻,👩‍💻,👨‍🎤,👩‍🎤,👨‍🎨,👩‍🎨,👨‍✈️,👩‍✈️,👨‍🚀,👩‍🚀,👨‍🚒,👩‍🚒,👮,👮‍♂️,👮‍♀️,🕵️,🕵️‍♂️,🕵️‍♀️,💂,💂‍♂️,💂‍♀️,👷,👷‍♂️,👷‍♀️,🤴,👸,👳,👳‍♂️,👳‍♀️,👲,🧕,🧔,👱,👱‍♂️,👱‍♀️,🤵,👰,🤰,🤱,👼,🎅,🤶,🧙,🧙‍♀️,🧙‍♂️,🧚,🧚‍♀️,🧚‍♂️,🧛,🧛‍♀️,🧛‍♂️,🧜,🧜‍♀️,🧜‍♂️,🧝,🧝‍♀️,🧝‍♂️,🧞,🧞‍♀️,🧟,🧟‍♀️,🙍,🙍‍♂️,🙍‍♀️,🙎,🙎‍♂️,🙎‍♀️,🙅,🙅‍♂️,🙅‍♀️,🙆,🙆‍♂️,🙆‍♀️,💁,💁‍♂️,💁‍♀️,🙋,🙋‍♂️,🙋‍♀️,🙇,🙇‍♂️,🙇‍♀️,🤦,🤦‍♂️,🤦‍♀️,🤷,🤷‍♂️,🤷‍♀️,💆,💆‍♂️,💆‍♀️,💇,💇‍♂️,💇‍♀️,🚶,🚶‍♂️,🚶‍♀️,🏃,🏃‍♂️,🏃‍♀️,💃,🕺,👯,👯‍♂️,👯‍♀️,🧖,🧖‍♀️,🧖‍♂️,🧗,🧗‍♀️,🧗‍♂️,🧘,🧘‍♀️,🧘‍♂️,🕴️,👤,👥,👫,👬,👭,💏,👨‍❤️‍💋‍👨,👩‍❤️‍💋‍👩,💑,👨‍❤️‍👨,👩‍❤️‍👩,👪,👨‍👩‍👧,👨‍👩‍👧‍👦,👨‍👩‍👦‍👦,👨‍👩‍👧‍👧,👨‍👨‍👦,👨‍👨‍👧,👨‍👨‍👧‍👦,👨‍👨‍👦‍👦,👨‍👨‍👧‍👧,👩‍👩‍👦,👩‍👩‍👧,👩‍👩‍👧‍👦,👩‍👩‍👦‍👦,👩‍👩‍👧‍👧,👨‍👦,👨‍👧,👨‍👧‍👦,👨‍👧‍👧,👩‍👦‍👦,👩‍👧,👩‍👧‍👦,🤳,👃,👅,👄,💋,💘,❤️,💓,💔,💕,💖,💗,💙,💚,💛,🧡,💜,🖤,💝,💞,❣️,💌,💬,🌬️,☃️,⛄,🎎,🗿,👾,💩,🛀,🛌,💅,👂,👣,👀,👁️,🧠,💭,👓,👔,👕,👖,🧣,🧤,🧥,🧦,👗,👘,👙,👚,👛,👜,👝,🎒,👞,👟,👠,👡,👢,👑,👒,🎩,🎓,🧢,📿,💄,💍,💎,🥄,🔪,🏺,🗺️,🗾,🎠,🎡,🎢,💈,🎪,🛰️,🚀,🛸,🛎️,⌛,⏳,⌚,⏰,🕰️,🌡️,🌂,☂️,☔,⛱️,⚡,🎃,🎄,🎆,🎇,🎈,🎉,🎊,🎏,🎐,🎑,🎀,🎁,🎗️,🎟️,🎫,🔮,🎮,🕹️,🎰,🃏,🎴,🎭,🖼️,🎨,🔇,🔈,🔉,🔊,📢,📣,📯,🔔,🔕,🎼,🎵,🎶,🎙️,🎚️,🎛️,🎤,🎧,📻,🎷,🎸,🎹,🎺,🎻,🥁,📱,📲,☎️,📞,📟,📠,🔋,🔌,💻,🖥️,🖨️,⌨️,🖱️,🖲️,💽,💾,💿,📀,🎥,🎞️,📽️,🎬,📺,📷,📸,📹,📼,🔍,🔎,💡,🔦,🏮,📔,📕,📖,📗,📘,📙,📚,📓,📒,📃,📜,📄,📰,📑,🔖,💰,💴,💵,💶,💷,💸,💳,✉️,📧,📨,📩,📤,📥,📦,📫,📪,📬,📭,📮,✏️,✒️,📝,💼,📁,📂,📅,📆,📇,📈,📉,📊,📋,📌,📍,📎,📏,📐,✂️,🔒,🔓,🔏,🔐,🔑,🔨,🔫,🔧,🔩,🔬,🔭,📡,💉,💊,🚪,🚽,🚿,🛁,🛒,🚬,🔅,🔆,⚜️,🔱,📛,🚂,🚃,🚄,🚅,🚆,🚇,🚈,🚉,🚊,🚝,🚞,🚋,🚌,🚍,🚎,🚐,🚑,🚒,🚓,🚔,🚕,🚖,🚗,🚘,🚙,🚚,🚛,🚜,🚲,🛴,🛵,🚏,🛣️,🛤️,🛢️,⛽,🚨,🚥,🚦,🛑,🚧,⛵,🛶,🚤,🛳️,⛴️,🛥️,🚢,✈️,🛩️,🛫,🛬,💺,🚁,🚟,🚠,🚡,⚠️,⛔,🦗,🍇,🍈,🍉,🍊,🍋,🍌,🍍,🍎,🍏,🍐,🍑,🍒,🍓,🥝,🍅,🥥,🥑,🍆,🥔,🥕,🌽,🌶️,🥒,🥦,🥜,🍞,🥐,🥖,🥨,🥞,🧀,🍖,🍗,🥩,🥓,🍔,🍟,🍕,🌭,🥪,🌮,🌯,🥙,🥚,🍳,🥘,🍲,🥣,🥗,🍿,🥫,🍱,🍘,🍙,🍚,🍛,🍜,🍝,🍠,🍢,🍣,🍤,🍥,🍡,🥟,🥠,🥡,🍦,🍧,🍨,🍩,🍪,🎂,🍰,🥧,🍫,🍬,🍭,🍮,🍯,🍼,🥛,☕,🍵,🍶,🍾,🍷,🍸,🍹,🍺,🍻,🥂,🥃,🥤,🥢,🍽️,🍴`.split(',')
<template>
<view id="uni-im-chat-input-box">
<div contenteditable="true" id="uni-im-chat-input" @blur="onBlur" @focus="onFocus" @input="onInput">
</div>
<view class="uni-im-chat-input" :style="{height:chatInputBoxHeight}">
<view id="drag-line"></view>
<view class="top-menu">
<uni-im-icons v-for="(item,index) in menuList" :key="index" class="item"
@click.native.stop="clickMenu(index,$event)"
:title.native="`选择${item.title},并发送`"
:code="item.iconCode" size="24" color="#666666"
></uni-im-icons>
<!-- #ifdef H5 -->
<view v-for="(item,index) in extToolBar" :key="index" class="item">
<component :is="item.component" v-bind="item.props" @sendCodeMsg="sendCodeMsg" ></component>
</view>
<!-- #endif -->
</view>
<view class="main">
<!-- 切换为语音模式 -->
<!-- #ifdef H5 -->
<!-- <view class="camera-filled-box icon" @click="chooseFileSendMsg('image')">
<uni-icons type="camera-filled" size="20px" color="#FFF"></uni-icons>
</view> -->
<!-- #endif -->
<uni-im-icons @click="changeSoundIsShow" :code="soundIsShow?'e69f':'e684'" size="30" class="icon"></uni-im-icons>
<view class="editor-box-parent">
<view class="editor-box">
<uni-im-sound v-show="soundIsShow" @sendSoundMsg="sendSoundMsg" class="uni-im-sound"></uni-im-sound>
<uni-im-editor v-show="!soundIsShow" @input="oninput" @confirm="confirm" class="editor" ref="editor"></uni-im-editor>
</view>
<slot name="about-msg"></slot>
</view>
<uni-im-icons @click="setShowMore('emoji')" :code="emojiIsShow?'e69f':'e646'" size="30" class="icon"></uni-im-icons>
<text v-if="!soundIsShow&&canSend" @click="confirm" class="icon confirm">发送</text>
<uni-im-icons v-else @click="setShowMore('menu')" code="e75a" size="30" class="icon"></uni-im-icons>
</view>
<view v-if="showMore" class="media-box" :style="{height:keyboardMaxHeight - bHeight +'px'}">
<view v-if="showMore == 'menu'" class="menu">
<view class="menu-item" v-for="(item,index) in menuList" :key="index" @click.stop="clickMenu(index,$event)">
<view class="menu-item-icon">
<uni-im-icons :code="item.iconCode" size="26"></uni-im-icons>
</view>
<text class="menu-item-text">{{item.title}}</text>
</view>
</view>
<scroll-view v-if="showMore == 'emoji'" :scroll-y="true" class="emoji-list-box">
<text v-for="(uniCodeEmoji,index) in emojiCodes" :key="index"
@click.stop="clickEmojiItem(uniCodeEmoji,$event)" class="item">{{uniCodeEmoji}}</text>
</scroll-view>
</view>
<!-- #ifndef H5 -->
<!-- 调整输入框的位置,占位专用:键盘高度 或 全面屏底部不可用区域高度 -->
<view v-else-if="raiseEditor || keyboardHeight" :style="{height: keyboardMaxHeight - bHeight + 'px'}"></view>
<view :style="{height: bHeight + 'px'}"></view>
<!-- #endif -->
<view class="send-msg-btn-box">
<text class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text>
<button @click="confirm" :disabled="!canSend" class="send" type="primary">发送</button>
</view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
let inputElement;
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import {markRaw} from "vue";
import emojiCodes from './emojiCodes.js';
let currentModelValue = '';
export default {
name: 'uni-im-chat-input',
emits: ["update:modelValue", "blur", "focus", "input", "confirm", "cursor"],
props: {
modelValue: {
type: [String, Object],
default: ""
},
placeholder: {
type: String,
default: ""
},
cursor: {
type: Number,
default: 0
},
maxlength: {
type: Number,
default: 140
},
focus: {
type: Boolean,
default: false
name: 'uni-im-chat-input',
emits: ["showAboutMenber","update:modelValue","confirm","input","sendSoundMsg","sendCodeMsg"],
data() {
return {
bHeight:uniIm.systemInfo.safeAreaInsets.bottom/2,
raiseEditor: false,
showMore: false,
soundIsShow: false,
emojiIsShow: false,
menuIsShow: false,
menuList: [{
"title": "图片",
"iconCode": "e7be"
},
{
"title": "视频",
"iconCode": "e690"
},
{
"title": "文件",
"iconCode": "e69e"
}
],
emojiCodes,
chatInputBoxHeight:'auto',
extToolBar: []
}
},
data() {
return {}
},
mounted() {
inputElement = document.getElementById('uni-im-chat-input');
uniIm.utils.appEvent.onAppActivate(() => {
// 主窗口激活时设置输入焦点到这里的文本编辑框
inputElement.focus()
})
let shiftIsDown = false;
window.addEventListener('keydown', (e) => {
if (e.key == 'Shift') {
shiftIsDown = true
}
})
window.addEventListener('keyup', (e) => {
if (e.key == 'Shift') {
shiftIsDown = false
}
})
let isComposing = false;
// 输入法开始输入
inputElement.addEventListener('compositionstart', () =>{
isComposing = true
inputElement.isComposing = isComposing;
});
// 输入法结束输入
inputElement.addEventListener('compositionend', () =>{
isComposing = false
inputElement.isComposing = isComposing;
});
inputElement.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
if (isComposing) {
console.log('输入法正在输入中,此时回车不发送消息')
event.preventDefault();
} else {
if (shiftIsDown) {
console.log('shift键处于按下状态,为换行不是confirm发送')
} else {
this.$emit('confirm');
// 防止,回车执行发送的同时执行换行
event.preventDefault();
}
}
}
});
inputElement.addEventListener('paste',async event => {
// console.log('粘贴', event);
if (event.clipboardData || event.originalEvent) {
//某些chrome版本使用的是event.originalEvent
let clipboardData = event.clipboardData || event.originalEvent.clipboardData;
// 获取粘贴的图片
let items = clipboardData.items;
for (const item of items) {
if (item.type.indexOf("image") !== -1) {
event.preventDefault();
const file = item.getAsFile();
const reader = new FileReader();
reader.onload = (readerEvent) => {
const blobUrl = readerEvent.target.result;
this.addHtmlToCursor(`<img src="${blobUrl}" />`)
};
reader.readAsDataURL(file);
}
}
let htmlString = clipboardData.getData('text/html');
// console.log('htmlString111',htmlString);
if (htmlString) {
event.preventDefault();
htmlString = await filterHTML(htmlString);
// console.log('htmlString222', htmlString);
const tmpDom = document.createElement('div');
tmpDom.innerHTML = htmlString;
if (tmpDom.innerText > 50000) {
uni.showModal({
content: '你粘贴的文本长度超过50000,将被截断。',
complete: e => {
if (e.confirm) {
this.addHtmlToCursor(htmlString.substring(0, 50000))
}
}
});
} else {
this.addHtmlToCursor(htmlString)
}
// 检查图片加载失败,删除图片
const imgs = inputElement.querySelectorAll('img');
for (const img of imgs) {
img.onerror = () => {
img.remove();
}
}
} else {
let text = clipboardData.getData('text');
try {
let obj = JSON.parse(text)
this.addHtmlToCursor(this.arrDomJsonToHtml(obj['uni-im-rich-text-data']))
} catch (e) {
this.addHtmlToCursor(text)
}
event.preventDefault();
}
}
})
async function filterHTML(htmlString) {
// 过滤html字符串,只保留:文字,图片,链接
// html字符串转dom对象,并深度遍历
let div = document.createElement('div');
div.innerHTML = htmlString;
let arr = [];
async function deep(node) {
if (node.nodeType === 1) {
if (node.tagName === 'IMG') {
// 只保留src属性
// 处理图片跨域问题
if (node.src.indexOf('data:image/png;base64,') != 0 && !node.src.includes('https://im-res.dcloud.net.cn')) {
const uniImCo = uniCloud.importObject("uni-im-co",{
loadingOptions: { // loading相关配置
title: '处理跨域图片...',
mask: true
}
})
let res = await uniImCo.getImgBase64(node.src)
// console.log('res.data base64', res.data);
node.src = res.data
}
arr.push(`<img src="${node.src}" />`);
} else if (node.tagName === 'A') {
// 只保留href属性
arr.push(`<a href="${node.href}">${node.innerText}</a>`);
}
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
await deep(node.childNodes[i]);
}
computed: {
canSend() {
return typeof this.modelValue != "string" || this.modelValue.trim().length > 0;
}
},
props: {
modelValue: {
type: [String, Object],
default: ""
},
keyboardMaxHeight: {
type: Number,
default: 0
},
keyboardHeight: {
type: Number,
default: 0
}
},
mounted() {
// 调用扩展点,扩展程序可以在消息输入框新增一个工具类的项
this.extToolBar = uniIm.extensions
.invokeExts("input-msg-tool-bar",this.conversation)
.filter((result) => result && result.component)
.map((result) => {
return {
component: markRaw(result.component),
props: result.props||{}
};
});
currentModelValue = this.modelValue;
// #ifdef H5
// pc宽屏支持拖动改变输入框高度
if(uniIm.isWidescreen){
this.chatInputBoxHeight = uni.getStorageSync('uni-im-data:chatInputBoxHeight') || '300px';
// 拖动uni-im-chat-input的顶部,改变高度
let startY,startHeight,isMove = false;
const dragLine = document.querySelector('#drag-line');
dragLine.addEventListener('mousedown', (e) => {
startY = e.clientY;
startHeight = parseInt(this.chatInputBoxHeight);
isMove = true;
});
document.addEventListener('mousemove', (e) => {
if (isMove) {
const moveY = startY - e.clientY;
const height = startHeight + moveY;
if(height > 800){
// 改变鼠标样式,为向下箭头
document.body.style.cursor = 's-resize';
}else if(height < 200){
// 改变鼠标样式,为向上箭头
document.body.style.cursor = 'n-resize';
}else{
this.chatInputBoxHeight = height + 'px';
}
} else if (node.nodeType === 3) {
arr.push(node.nodeValue);
}
}
await deep(div);
return arr.join('');
}
});
document.addEventListener('mouseup', () => {
if(isMove){
document.body.style.cursor = '';
isMove = false;
uni.setStorageSync('uni-im-data:chatInputBoxHeight',this.chatInputBoxHeight)
}
});
}
// #endif
},
watch: {
modelValue:{
handler(modelValue,oldValue) {
// console.log('###modelValue', modelValue,JSON.stringify(currentModelValue) != JSON.stringify(modelValue) );
if(JSON.stringify(currentModelValue) != JSON.stringify(modelValue) ) {
this.setContent(modelValue?.html || modelValue)
}
},
deep: true,
immediate: true
}
},
watch: {
modelValue(modelValue, oldModelValue) {
this.$nextTick(() => {
// console.log('modelValue', modelValue);
if (modelValue.length === 0) {
inputElement.innerHTML = ''
} else if (typeof modelValue === 'string' && modelValue != this.inputText()) {
inputElement.innerHTML = modelValue
} else if (typeof modelValue === 'object' && modelValue.html != inputElement.innerHTML) {
inputElement.innerHTML = modelValue.html
}
});
},
cursor(cursor, oldCursor) {
// console.log('cursor',cursor);
if (cursor != oldCursor) {
this.$nextTick(() => {});
}
methods: {
sendSoundMsg(e) {
this.$emit('sendSoundMsg',e)
},
focus(focus, oldFocus) {
if (focus) {
inputElement.focus();
} else {
inputElement.blur();
sendCodeMsg(e) {
this.$emit('sendCodeMsg',e)
},
oninput(e) {
currentModelValue = e.value;
if(e.data == '@') {
this.$emit('showAboutMenber')
}
}
},
methods: {
// 设置焦点在最后
focusToLast() {
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(inputElement);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
inputElement.focus();
},
deleteLeftChar(n = 1) {
inputElement.focus();
const selection = window.getSelection();
if (!selection.isCollapsed) return; //不要删除已选中的内容
const range = selection.getRangeAt(0).cloneRange();
if (range.startOffset > 0) {
range.setStart(range.startContainer, range.startOffset - n);
range.deleteContents();
selection.removeAllRanges();
selection.addRange(range);
} else if (range.startContainer.previousSibling) {
const container = range.startContainer;
const sibling = container.previousSibling;
range.setStart(sibling, sibling.length - n);
range.setEnd(sibling, sibling.length);
range.deleteContents();
selection.removeAllRanges();
selection.addRange(range);
}
this.exportValue();
},
addHtmlToCursor(html) {
inputElement.focus();
const selection = window.getSelection();
if (selection.getRangeAt && selection.rangeCount) {
let range = selection.getRangeAt(0);
range.deleteContents();
var ele = document.createElement("div");
ele.innerHTML = html;
var frag = document.createDocumentFragment(),
node, lastNode;
while ((node = ele.firstChild)) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
// 设置光标到插入内容之后的位置
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
selection.removeAllRanges(); // 清除现有的选择区域
selection.addRange(range); // 将更新后的范围添加回选择区域
}
}
this.exportValue();
},
setHtml(html) {
inputElement.innerHTML = html
this.exportValue();
this.$emit('update:modelValue', e.value)
this.$emit('input',e)
},
confirm(){
this.$emit('confirm');
},
exportValue() {
// 截流 1000毫秒响应一次
if (this.lock){
if(this.timer){
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
// console.log('执行');
this.lock = false;
this.exportValue();
}, 1000);
// return console.log('截流');
focus() {
// console.log('focus');
this.$refs.editor.callRmd('$focus')
},
async addCallUser({uid, nickname},needDeleteLeftART = true,DL=0) {
// 隐藏发送语音消息模式
this.soundIsShow = false;
this.raiseEditor = true;
setTimeout(()=>this.raiseEditor = false,2000)
if(needDeleteLeftART){
// console.error('needDeleteLeftART');
// DL 是需要删除的个数,因为web-pc端用户可以输入关键词筛选用户导致多出字符
this.$refs.editor.callRmd('$deleteLeftChar', 1+DL)
}else{
// console.log('不截流');
this.lock = true;
// console.error('no needDeleteLeftART');
await uniIm.utils.sleep(100)
this.$refs.editor.callRmd('$focus')
}
await uniIm.utils.sleep(10)
let param = '';
let val = inputElement.innerHTML;
if (val.includes(`<img src="`) || val.includes(`<span class="nickname"`) || val.includes(`<a href="`)) {
param = {
// "rich-text": //uniIm.utils.parseHtml( 执行比较消耗内存,改为chat页面执行,
"html": val,
"text": inputElement.innerText
}
} else {
param = this.inputText()
}
this.$emit('update:modelValue', param)
},
inputText() {
return inputElement.innerHTML.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "");
},
onInput(e) {
// console.log('onInput', e);
this.$emit('input', {
"data": e.data,
"detail": {
"cursor": 0
}
});
this.exportValue();
},
onBlur() {
// this.$emit('update:focus', false);
// console.log('blur');
this.$emit('blur');
},
onFocus() {
// console.log('onFocus');
this.$emit('focus');
// this.$emit('update:focus', true);
// 提醒末尾的此空格在margin-right: -3px;内,用于解决办法浏览器非文本节点后的光标定位不正确的问题
const html =`<span class="nickname" contenteditable="false" uid="${uid}">@${nickname}</span>&nbsp;`
this.addHtmlToCursor(html)
// setTimeout(()=>this.$refs.editor.callRmd('$restoreCursor'),500)
// #ifndef H5
// uni.onKeyboardHeightChange((res) => {
// console.log('键盘高度变化', res.height);
// });
// #endif
},
setContent(data) {
if(typeof data === 'string'){
this.setHtml(data);
}else{
inputElement.innerHTML = this.arrDomJsonToHtml(data);
this.exportValue();
setShowMore(type) {
if (this.showMore == type && this.keyboardHeight === 0) {
this.showMore = false;
} else {
this.showMore = type;
if(this.soundIsShow && type === 'emoji'){
this.soundIsShow = false
}
}
},
arrDomJsonToHtml(arr) {
function parseItem(item) {
if (item.type === "text") {
return item.text;
}
let html = `<${item.name}`;
if (item.attrs) {
for (const key in item.attrs) {
html += ` ${key}="${item.attrs[key]}"`;
}
}
if (item.children) {
html += ">";
for (const child of item.children) {
html += parseItem(child);
}
html += `</${item.name}>`;
} else {
html += " />";
}
return html;
}
let result = "";
for (const item of arr) {
result += parseItem(item);
}
return result;
}
},
clickEmojiItem(uniCodeEmoji, event) {
console.log('clickEmojiItem', uniCodeEmoji, event);
this.addHtmlToCursor(uniCodeEmoji,false)
},
addHtmlToCursor(html,focus=true) {
// 因为可能在点击某个元素后失去了光标,所以需要延迟让lastCursor更新后在插入
setTimeout(()=>{
this.$refs.editor.callRmd('$addHtmlToCursor',html,focus)
},100)
},
setContent(content) {
console.log('setContent', content);
this.$refs.editor.callRmd('$setContent',content)
// this.$emit('update:modelValue', content)
},
clickMenu(index, event) {
console.log('clickMenu', index, event);
let parrent = this.$parent
// #ifdef H5
parrent = this.$parent.$parent.$parent
// #endif
if (index < 2) {
parrent.chooseFileSendMsg(index === 0 ? 'image' : 'video')
}
if (index === 2) {
// #ifdef APP
return uni.showToast({
title: '暂不支持,发送文件',
icon: 'none'
});
// #endif
parrent.chooseFileSendMsg('all')
}
},
changeSoundIsShow() {
console.log('changeSoundIsShow');
this.soundIsShow = !this.soundIsShow;
this.showMore = false;
},
confirm() {
console.log('confirm');
this.$emit('confirm');
}
}
}
</script>
<style lang="scss">
#uni-im-chat-input-box {
flex: 1;
overflow-y: auto;
}
#uni-im-chat-input {
flex: 1;
<style lang="scss" scoped>
.uni-im-chat-input {
padding:0 5px;
border-top: 1px solid #f5f5f5;
& > view {
// justify-content: center;
// align-items: center;
// align-content: center;
}
#uni-im-chat-input:focus {
outline: none;
}
#uni-im-chat-input ::v-deep img {
max-width: 90%;
display: block;
}
#uni-im-chat-input ::v-deep .nickname {
color: #0b65ff !important;
margin: 0 2px;
user-select: text;
}
.top-menu,.send-msg-btn-box{
// 默认隐藏 pc端才显示
display: none;
}
.main {
flex: 1;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
overflow: hidden;
.icon {
margin:0 5px;
margin-bottom: 20px;
width: 30px;
height: 30px;
align-content: center;
flex-shrink: 0;
&.confirm {
width: auto;
color: #fff;
font-size: .75rem;
border-radius: 6px;
background-color: #2faf4c;
height: 28px;
line-height: 28px;
padding: 0 8px;
text-align: center;
}
}
.camera-filled-box {
background-color: #3b81fd;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 100px;
}
.editor-box-parent{
flex: 1;
height: 100%;
overflow: hidden;
.editor-box {
position: relative;
overflow: hidden;
flex: 1;
margin:10px 5px;
padding: 10px 8px;
background-color: #FFF;
min-height: 46px;
&,.editor {
border-radius: 10px;
}
.editor {
// flex: 1;
}
.uni-im-sound {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 10;
}
}
}
}
.media-box {
.menu {
flex-direction: row;
justify-content: self-start;
align-items: self-start;
padding: 10px 0;
// height: 100%;
.menu-item {
margin: 15px;
width: 60px;
height: 60px;
justify-content: center;
align-items: center;
.menu-item-icon {
width: 40px;
height: 40px;
justify-content: center;
align-items: center;
background-color: #FFF;
padding: 8px;
border-radius: 10px;
margin-bottom: 5px;
}
.menu-item-text {
font-size: 12px;
color: #666666;
}
}
}
.emoji-list-box {
height: 100%;
padding: 15rpx;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
.item {
text-align: center;
font-size: 65rpx;
width: 87rpx;
height: 87rpx;
justify-content: center;
align-items: center;
display: inline-block;
}
}
}
}
/* #ifdef H5 */
@media screen and (min-device-width:560px){
.uni-im-chat-input {
position: relative;
flex-direction: column;
#drag-line {
// 鼠标变上下拖动的光标
cursor: ns-resize;
height: 5px;
position: absolute;
left: 0;
top: -3px;
width: 100%;
z-index: 1;
}
.main {
.icon {
display: none;
}
.editor-box-parent {
flex-direction: column-reverse;
}
.editor-box {
height: 200px;
padding: 0;
background-color: transparent !important;
& ::v-deep {
.uni-im-editor-box {
height: 100%;
}
.uni-im-editor{
height: 100%;
max-height: 100%;
}
}
}
}
.top-menu {
display: flex;
height: 45px;
flex-direction: row;
justify-content: flex-start;
align-items: center;
background-color: #f5f5f5;
.item {
margin:0 20px 0 10px;
}
}
.send-msg-btn-box {
display: flex;
flex-direction: row;
height: 45px;
justify-content: flex-end;
align-items: center;
padding:0 5px;
.send-btn-tip {
font-size: 12px;
color: #666666;
}
.send {
font-size: 12px;
margin:0 10px;
color: #fff;
}
}
}
}
/* #endif */
</style>
\ No newline at end of file
......@@ -87,22 +87,12 @@
setTimeout(() => {
// 根据实际渲染出的组件大小修正定位
// #ifdef APP-NVUE
const dom = weex.requireModule("dom");
const result = dom.getComponentRect(this.$refs.contextmenu, option => {
const {top, left, width, height} = option.size
this.style = calcPosition(top, left, width, height)
});
// #endif
// #ifndef APP-NVUE
const query = uni.createSelectorQuery().in(this)
query.select('.contextmenu').boundingClientRect(option => {
const { width, height } = option
this.style = calcPosition(top, left, width, height)
this.style.opacity = 1
}).exec()
// #endif
}, 0)
},
closeMe(){
......@@ -126,7 +116,7 @@
.contextmenu{
position: fixed;
background-color: #fff;
z-index: 99999;
z-index: 99;
border-radius: 10px;
box-shadow: 0 0 10px #888;
background-color:#FFF;
......@@ -143,15 +133,11 @@
margin: 5px;
opacity: 0.9;
text-align: left;
height: 40px;
align-content: center;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
/* #ifdef APP-NVUE */
width: 120px;
height: 40px;
/* #endif */
}
.item:hover{
......@@ -163,21 +149,9 @@
position: fixed;
top: 0;
left: 0;
width: 750rpx;
flex: 1;
height: 9000px;
/* #ifndef APP-NVUE */
width: 100vw;
height: 100vh;
z-index: 9999;
/* #endif */
/* #ifdef H5 */
// 解决:通过*选择器设置了最大宽高 引起的大小撑不开的问题
max-height:100vh;
max-width:100vw;
/* #endif */
z-index:10;
background-color: rgba(0,0,0,0.5);
}
</style>
<template>
<view>
<!-- #ifdef H5 -->
<scroll-view
<view class="root">
<scroll-view
ref="conversation-list"
class="conversation-list"
:class="{canCheck}"
:style="{'background-color': conversationList.length?'#FFF':''}"
:scroll-top="listScrollTop"
scroll-y="true"
@scrolltolower="loadMore()"
@scroll="onScroll"
>
<!-- #endif -->
<!-- #ifndef H5 -->
<uni-list
ref="conversation-list"
class="conversation-list"
:style="{'height':wHeight,'width':'750rpx'}"
:border="false"
<uni-im-conversation v-for="(item,index) in conversationList" :key="item.id"
class="conversation-list-item" :class="{'activeConversation':activeConversationId == item.id,'focus':item.focus}"
:conversation="item" :id="item.id"
@click="clickItem(item)" @contextmenu.prevent="openConversationMenu($event,index)"
@longpress="openConversationMenu($event,index)"
>
<!-- #endif -->
<!-- 注意:nvue下 list 内必须是 cell,所以元素都需要在uni-list-chat内 -->
<uni-im-conversation v-for="(item,index) in conversationList" :key="item.id" :id="item.id"
:conversation="item"
class="conversation-list-item" :class="{'activeConversation':activeConversationId == item.id,'focus':item.focus}"
@click="clickItem(item)"
@contextmenu.prevent.native="openConversationMenu($event,index)"
>
<template v-if="canCheck" #left>
<view style="justify-content: center;">
<view class="check-box" :class="{checked:isCheck(item)}">
<uni-icons
v-if="isCheck(item)"
color="#FFF"
type="checkmarkempty"
/>
</view>
<template v-if="canCheck" #left>
<view style="justify-content: center;">
<view class="check-box" :class="{checked:isCheck(item)}">
<uni-icons
v-if="isCheck(item)"
color="#FFF"
type="checkmarkempty"
/>
</view>
</template>
</uni-im-conversation>
</view>
</template>
</uni-im-conversation>
<!-- #ifdef APP-NVUE -->
<!-- nvue端appear(元素一旦显示在可视窗口中)就触发加载更多。-->
<cell v-if="conversationList.length" @appear="loadMore()" />
<!-- #endif -->
<uni-list-item :custom-style="{backgroundColor:'#f5f5f5',padding:0}" style="margin-top: 5px;" :border="false">
<template #body>
<template v-if="!keyword">
<uni-im-load-state
class="tip"
:content-text="loadMoreContentText"
:status="conversationHasMore?'loading':'noMore'"
/>
</template>
<template v-else>
<text v-if="conversationList.length === 0" style="text-align: center;flex: 1;margin: 8px;color: #aaa;">
没有相关数据
</text>
</template>
</template>
</uni-list-item>
<view style="margin-top: 5px;backgroundColor:'#f5f5f5'">
<template v-if="!keyword">
<uni-im-load-state
:content-text="loadMoreContentText"
:status="conversationHasMore?'loading':'noMore'"
/>
</template>
<text v-else-if="conversationList.length === 0"
style="text-align: center;flex: 1;margin: 8px;color: #aaa;"
>没有相关数据</text>
</view>
<!-- #ifndef H5 -->
</uni-list>
<!-- #endif -->
<!-- #ifdef H5 -->
</scroll-view>
<!-- #endif -->
<!-- #ifdef H5 -->
<uni-im-contextmenu ref="uni-im-contextmenu" />
<!-- #endif -->
</view>
</template>
......@@ -80,6 +52,7 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const db = uniCloud.database();
let currentScrollTop = 0;
export default {
emits: ['change', 'clickItem', 'onScroll'],
props: {
keyword: {
type: String,
......@@ -107,11 +80,6 @@ export default {
conversationHasMore() {
return uniIm.conversation.hasMore
},
// #ifdef APP-NVUE
wHeight() {
return uni.getSystemInfoSync().windowHeight + 'px'
},
// #endif
loadMoreContentText() {
return {
contentrefresh: "正在加载...",
......@@ -190,13 +158,16 @@ export default {
this.$emit('clickItem', item)
},
openConversationMenu(e, index) {
// #ifdef H5
let conversation = this.conversationList[index]
conversation.focus = true
const myContextmenu = this.$refs['uni-im-contextmenu']
const clientY = e.clientY || e.changedTouches[0].clientY
const clientX = e.clientX || e.changedTouches[0].clientX
const position = {
"top": e.clientY + 35,
"left": e.clientX
"top": clientY + 35,
"left": clientX
}
let menuList = [{
"title": "置顶",
......@@ -247,7 +218,6 @@ export default {
myContextmenu.onClose(() => {
conversation.focus = false
})
// #endif
},
async loadMore() {
let data = await uniIm.conversation.loadMore()
......@@ -260,35 +230,32 @@ export default {
<style lang="scss" scoped>
.conversation-list,
.root {
height: 100%;
flex: 1;
}
.tip {
flex: 1;
}
.canCheck ::v-deep {
.note-box,
.time,
.state {
display: none;
}
.title {
font-size: 16px !important;
}
}
.conversation-list .conversation-list-item.focus {
border: 2px solid #1ab94e;
}
.conversation-list .conversation-list-item {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
border: 2px solid transparent;
border-radius: 5px;
margin: 0 5px;
}
/* #ifdef H5 */
.conversation-list .conversation-list-item ::v-deep .uni-list-chat__content-title {
font-size: 14px;
}
.conversation-list ::v-deep .conversation-list-item .uni-list--border {
display: none;
}
.conversation-list ::v-deep .conversation-list-item .uni-list-chat__container {
padding: 8px 8px 8px 6px;
}
/* #endif */
.conversation-list .conversation-list-item.activeConversation {
background-color: #f1f1f1;
......
<template>
<uni-list-chat
:id="conversation.id"
:note="canCheck?'':conversation.note"
:show-badge="!canCheck && conversation.unread_count>0"
:badge-text="!canCheck ? conversation.unread_count : ''"
:title="conversation.title"
:avatar="avatarUrl"
:time="canCheck?'':friendlyTime(conversation.time)"
:is-pinned="conversation.pinned"
:is-mute="canCheck?false:conversation.mute"
:red-note="redNote"
:tags="conversation.tag"
<template>
<uni-im-info-card
:id="conversation.id"
@click="handleClick"
:title="conversation.title"
:note="conversation.note"
:red-note="conversation.hasDraft?'[草稿]&nbsp;':''"
:tags="conversation.tags"
:avatarUrl="avatarUrl"
:time="friendlyTime"
:badge="conversation.unread_count"
:mute="conversation.mute"
:pinned="conversation.pinned"
link
>
<template v-slot:left>
<slot name="left"></slot>
</template>
<template v-slot:over-avatar>
<template v-for="overlay in avatarOverlayList" :key="overlay.component.name">
<component
:is="overlay.component"
v-bind="overlay.props"
cementing="ConversationAvatarOverlay"
/>
</template>
</template>
</uni-list-chat>
>
<template #left>
<slot name="left"></slot>
</template>
<template #avatar-overlay-list>
<template v-for="overlay in avatarOverlayList" :key="overlay.component.name">
<component :is="overlay.component" v-bind="overlay.props" cementing="ConversationAvatarOverlay" />
</template>
</template>
</uni-im-info-card>
</template>
<script>
......@@ -40,26 +35,24 @@
conversation: {
type: Object,
default: () => {}
},
canCheck: {
type: Boolean,
default: false
}
},
emits: ['click'],
computed: {
redNote() {
if (this.canCheck) {
return ''
} else if (this.conversation.call_list.length > 0) {
return '[有人@我]'
} else if (this.conversation.hasDraft) {
return '[草稿]'
} else if (this.conversation.group_id && this.conversation.group_info.mute_all_members) {
return '[已禁言]'
friendlyTime() {
// 使得时间会随着心跳动态更新
let timestamp = this.conversation.time + uniIm.heartbeat * 0
let friendlyTime = uniIm.utils.toFriendlyTime(timestamp)
// console.log('friendlyTime',friendlyTime);
let friendlyTimeArr = friendlyTime.split(' ')
let friendlyTimeArrL = friendlyTimeArr.length
// 如果含 年/月(不在3天内,且不是同一周),去掉时间
if (friendlyTimeArrL == 3 && friendlyTime.includes('/')) {
friendlyTime = friendlyTimeArr[0]
}
return friendlyTime
}
},
emits: ['click'],
data() {
// 调用扩展点,扩展程序可以为该会话增加覆盖的图标元素。
let avatarOverlayList = uniIm.extensions
......@@ -90,23 +83,8 @@
methods: {
handleClick() {
this.$emit('click', this.conversation)
},
friendlyTime(timestamp) {
// 使得时间会随着心跳动态更新
timestamp = timestamp + uniIm.heartbeat * 0
let friendlyTime = uniIm.utils.toFriendlyTime(timestamp)
// console.log('friendlyTime',friendlyTime);
let friendlyTimeArr = friendlyTime.split(' ')
let friendlyTimeArrL = friendlyTimeArr.length
// 如果含 年/月(不在3天内,且不是同一周),去掉时间
if (friendlyTimeArrL == 3 && friendlyTime.includes('/')) {
friendlyTime = friendlyTimeArr[0]
}
return friendlyTime
}
}
};
</script>
<style lang="scss">
</style>
\ No newline at end of file
<template>
<view class="uni-im-editor-box">
<!-- #ifdef MP -->
<textarea class="uni-im-editor-mp" v-model="textareaValue" @input="oninput" auto-height :maxlength="maxlength"
:show-confirm-bar="false" :adjust-position="false" @blur="lastCursor = $event.detail.cursor"
></textarea>
<!-- #endif -->
<!-- #ifndef MP -->
<div class="uni-im-editor" contenteditable="true" ></div>
<!-- 与rmd通讯专用 -->
<view :change:prop="rdm.$callMethod" :prop="callRmdParam"></view>
<!-- #endif -->
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
emits: ["input", "confirm"],
data() {
return {
callRmdParam: [],
// #ifdef MP
"textareaValue":this.modelValue,
lastCursor:this.modelValue.length
// #endif
}
},
props: {
modelValue: {
type: [String, Object],
default: ""
},
placeholder: {
type: String,
default: ""
},
maxlength: {
type: Number,
default: 140
}
},
mounted() {
// 与rmd通讯专用
this.callRmd = async (funcName, ...params) => {
// #ifdef MP
switch (funcName){
case '$setContent':
this.textareaValue = params[0]
break;
case '$addHtmlToCursor':
// 在第lastCursor位置添加内容
setTimeout(()=>{
this.textareaValue = this.textareaValue.slice(0,this.lastCursor) + params[0] + this.textareaValue.slice(this.lastCursor)
},300)
break;
default:
console.error('小程序暂不支持与rmd通讯',funcName,params)
break;
}
return
// #endif
this.callRmdParam = []
this.$nextTick(() => {
return new Promise((resolve, reject) => {
this.callRmdParam = [funcName, ...params,param=>{
resolve(param)
}]
})
})
}
// #ifndef H5
uni.onKeyboardHeightChange((res) => {
if(res.height === 0){
// console.error('@##键盘被收起,可以失去焦点')
// this.callRmd('$blur')
}
});
// #endif
// #ifdef H5
const uniImEditor = document.querySelector('.uni-im-editor')
uniIm.utils.appEvent.onAppActivate(() => {
// 主窗口激活时设置输入焦点到这里的文本编辑框
uniImEditor.focus()
})
let shiftIsDown = false;
window.addEventListener('keydown', (e) => {
if (e.key == 'Shift') {
shiftIsDown = true
}
})
window.addEventListener('keyup', (e) => {
if (e.key == 'Shift') {
shiftIsDown = false
}
})
let isComposing = false;
// 输入法开始输入
uniImEditor.addEventListener('compositionstart', () => {
isComposing = true
uniImEditor.isComposing = isComposing;
});
// 输入法结束输入
uniImEditor.addEventListener('compositionend', () => {
isComposing = false
uniImEditor.isComposing = isComposing;
});
uniImEditor.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
if (isComposing) {
console.log('输入法正在输入中,此时回车不发送消息')
event.preventDefault();
} else {
if (shiftIsDown) {
console.log('shift键处于按下状态,为换行不是confirm发送')
} else {
this.$emit('confirm');
// 防止,回车执行发送的同时执行换行
event.preventDefault();
}
}
}
});
uniImEditor.addEventListener('paste', async event => {
// console.log('粘贴', event);
if (event.clipboardData || event.originalEvent) {
//某些chrome版本使用的是event.originalEvent
let clipboardData = event.clipboardData || event.originalEvent.clipboardData;
// 获取粘贴的图片
let items = clipboardData.items;
for (const item of items) {
if (item.type.indexOf("image") !== -1) {
event.preventDefault();
const file = item.getAsFile();
const blobUrl = URL.createObjectURL(file)
console.log('blobUrl',blobUrl)
this.$addHtmlToCursor(`<img src="${blobUrl}" />`)
}
}
let htmlString = clipboardData.getData('text/html');
// console.log('htmlString111',htmlString);
if (htmlString) {
event.preventDefault();
htmlString = await filterHTML(htmlString);
// console.log('htmlString222', htmlString);
const tmpDom = document.createElement('div');
tmpDom.innerHTML = htmlString;
if (tmpDom.innerText.length > 50000) {
uni.showModal({
content: '你粘贴的文本长度超过50000,将被截断。',
complete: e => {
if (e.confirm) {
this.$addHtmlToCursor(htmlString.substring(0, 50000))
}
}
});
} else {
this.$addHtmlToCursor(htmlString)
}
// 检查图片加载失败,删除图片
const imgs = uniImEditor.querySelectorAll('img');
for (const img of imgs) {
img.onerror = () => {
img.remove();
}
}
} else {
let text = clipboardData.getData('text');
try {
let obj = JSON.parse(text)
this.$addHtmlToCursor(this.$arrDomJsonToHtml(obj['uni-im-rich-text-data']))
} catch (e) {
this.$addHtmlToCursor(text)
}
event.preventDefault();
}
}
})
async function filterHTML(htmlString) {
// 过滤html字符串,只保留:文字,图片,链接
// html字符串转dom对象,并深度遍历
let div = document.createElement('div');
div.innerHTML = htmlString;
let arr = [];
async function deep(node) {
if (node.nodeType === 1) {
if (node.tagName === 'IMG') {
// 只保留src属性
// 处理图片跨域问题
if (node.src.indexOf('blob:http') != 0 && !node.src.includes(
'https://im-res.dcloud.net.cn')) {
const uniImCo = uniCloud.importObject("uni-im-co", {
loadingOptions: { // loading相关配置
title: '处理跨域图片...',
mask: true
}
})
let res = await uniImCo.getImgBase64(node.src)
function base64ToBlob(base64) {
// 将Base64字符串分割为数据和应用信息
const byteChars = atob(base64.split(',')[1]);
// 创建一个长度为byteChars长度的数组
const byteArrays = new Array(byteChars.length);
// 将二进制字符串转换为Uint8Array
for (let i = 0; i < byteChars.length; i++) {
byteArrays[i] = byteChars.charCodeAt(i);
}
// 返回一个Blob对象
return new Blob([new Uint8Array(byteArrays)], {type: 'image/png'});
}
node.src = URL.createObjectURL( base64ToBlob(res.data) )
}
arr.push(`<img src="${node.src}" />`);
} else if (node.tagName === 'A') {
// 只保留href属性
arr.push(`<a href="${node.href}">${node.innerText}</a>`);
}
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
await deep(node.childNodes[i]);
}
}
} else if (node.nodeType === 3) {
arr.push(node.nodeValue);
}
}
await deep(div);
return arr.join('');
}
// #endif
},
methods: {
oninput(e) {
// #ifdef MP
let oldValue = this.oninput.oldValue || '';
// 当前输入框的值
const value = e.detail.value
// 本次输入的数据
const data = value.replace(oldValue, '')
this.oninput.oldValue = value
e = {
data,
value
}
// #endif
// console.error('input',e)
this.$emit('input', e)
}
}
}
</script>
<!-- #ifndef MP -->
<script module="rdm" lang="renderjs">
let uniImEditor,lastFocusNode,lastCursor;
// #ifdef APP
function setSoftinputTemporary() {
console.log('setSoftinputTemporary')
// 设置当前窗口键盘弹出后不做变化
const currentWebview = plus.webview.currentWebview();
currentWebview.setSoftinputTemporary({
mode:'nothing',
position:{top: 0,height: 0}
});
}
// #endif
export default {
name: 'uni-im-editor',
data() {
return {}
},
mounted() {
uniImEditor = document.querySelector('.uni-im-editor')
uniImEditor.addEventListener('input', e => {
setTimeout(()=>this.$oninput(e.data),0);
});
uniImEditor.addEventListener('click', e => {
// console.log('click', e);
// #ifdef APP
setSoftinputTemporary()
// #endif
});
uniImEditor.addEventListener('blur', e => {
// console.error('###blur', e);
setTimeout(this.$refreshLastCursor, 0);
});
// 键盘敲左右
uniImEditor.addEventListener('keydown', e => {
setTimeout(this.$refreshLastCursor, 0);
});
// 监听nickname后面的空格被删除(提醒此空格在margin-right: -3px;内,用于解决办法浏览器非文本节点后的光标定位不正确的问题)
const observer = new MutationObserver(function(mutations) {
mutations.forEach(mutation=> {
// 判断是否为删除节点的操作
const [removedNode] = mutation.removedNodes
if(removedNode){
if(removedNode && mutation?.previousSibling?.className === "nickname"){
mutation.previousSibling.remove()
}
}
});
})
.observe(uniImEditor, {
childList: true, // 监听子节点的变化
});
},
watch: {
modelValue(modelValue, oldModelValue) {
this.$nextTick(() => {
// console.log('modelValue', modelValue);
if (modelValue.length === 0) {
uniImEditor.innerHTML = ''
} else if (typeof modelValue === 'string' && modelValue != this.$inputText()) {
uniImEditor.innerHTML = modelValue
} else if (typeof modelValue === 'object' && modelValue.html != uniImEditor.innerHTML) {
uniImEditor.innerHTML = modelValue.html
}
});
},
},
methods: {
// 刷新光标信息
$refreshLastCursor() {
lastCursor = this.$getCursor();
lastFocusNode = window.getSelection().focusNode;
// console.log('刷新光标信息',{
// lastCursor,
// lastFocusNode
// });
},
$oninput(data){
this.$refreshLastCursor()
// 耗时计算
let start = new Date().getTime();
let param = '';
let val = uniImEditor.innerHTML;
const hasImg = uniImEditor.querySelector('img');
const hasNickname = uniImEditor.querySelector('.nickname');
const hasA = uniImEditor.querySelector('a');
if (hasImg || hasNickname || hasA) {
param = {
// "rich-text": //uniIm.utils.parseHtml( 执行比较消耗内存,改为chat页面 confirm时执行,
"html": val,
"text": uniImEditor.innerText,
"aboutUserIds": Array.from(uniImEditor.querySelectorAll('.nickname')).map(i=>i.getAttribute('uid'))
}
} else {
param = this.$inputText()
}
// 打印耗时
const spendTime = new Date().getTime() - start
if(spendTime > 10){
console.log('耗时', );
}
this.$ownerInstance.callMethod('oninput',{
data,// 本次输入的数据
value: param // 当前输入框的值
})
},
$callMethod([funcName, ...params]) {
// console.log('$callMethod funcName', funcName)
// console.log('$callMethod funcName', funcName === null)
// console.log('$callMethod params', params)
// console.log('$callMethod typeof funcName', typeof funcName)
try {
if(typeof funcName == 'string'){
const res = this[funcName](...params)
// console.log('res', res)
}
} catch (e) {
console.error("调用renderjs模块的方法失败", e,funcName,params)
}
},
$getCursor() {
const selection = window.getSelection();
return selection.focusOffset;
},
// 恢复光标位置
$restoreCursor(focus = true) {
this.$focus();
// 获取焦点时所在的子元素
try{
const range = document.createRange();
const sel = window.getSelection();
range.setStart(lastFocusNode || uniImEditor, lastCursor);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}catch(e){
console.error('恢复光标位置失败',e)
//TODO handle the exception
}
},
$deleteLeftChar(n = 1) {
this.$restoreCursor(true);
const selection = window.getSelection();
if (!selection.isCollapsed){
console.error('不要删除已选中的内容')
return;
}
const range = selection.getRangeAt(0).cloneRange();
if (range.startOffset > 0) {
range.setStart(range.startContainer, range.startOffset - n);
range.deleteContents();
selection.removeAllRanges();
selection.addRange(range);
} else if (range.startContainer.previousSibling) {
const container = range.startContainer;
const sibling = container.previousSibling;
range.setStart(sibling, sibling.length - n);
range.setEnd(sibling, sibling.length);
range.deleteContents();
selection.removeAllRanges();
selection.addRange(range);
}
lastCursor = this.$getCursor();
this.$oninput();
},
$addHtmlToCursor(html,focus = true) {
this.$restoreCursor(focus);
const selection = window.getSelection();
if (selection.getRangeAt && selection.rangeCount) {
let range = selection.getRangeAt(0);
range.deleteContents();
var ele = document.createElement("div");
ele.innerHTML = html;
var frag = document.createDocumentFragment(),
node, lastNode;
while ((node = ele.firstChild)) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
// 设置光标到插入内容之后的位置
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
selection.removeAllRanges(); // 清除现有的选择区域
selection.addRange(range); // 将更新后的范围添加回选择区域
}
}else{
uniImEditor.innerHTML += html;
}
this.$oninput(html);
},
$inputText() {
return uniImEditor.innerHTML.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "");
},
$focus() {
if(document.activeElement.className === 'uni-im-editor'){
document.activeElement.blur();
}
// #ifdef APP
setSoftinputTemporary()
// #endif
// console.error('获取焦点',document.activeElement.className);
uniImEditor.focus();
// console.error('获取焦点',document.activeElement.className);
},
$blur(){
// console.error('失去焦点1',document.activeElement.className);
uniImEditor.blur();
// console.error('失去焦点2',document.activeElement.className);
setTimeout(()=>{
// console.error('失去焦点3',document.activeElement.className);
},1000)
},
$onBlur() {
// this.$emit('update:focus', false);
// console.log('blur');
this.$emit('blur');
},
$onFocus() {
// console.log('$onFocus');
this.$emit('focus');
// this.$emit('update:focus', true);
},
$setContent(data) {
if (typeof data === 'string') {
uniImEditor.innerHTML = data
this.$oninput(data);
} else {
uniImEditor.innerHTML = this.$arrDomJsonToHtml(data);
this.$oninput(uniImEditor.innerHTML);
}
},
$arrDomJsonToHtml(arr) {
function parseItem(item) {
if (item.type === "text") {
return item.text;
}
let html = `<${item.name}`;
if (item.attrs) {
for (const key in item.attrs) {
html += ` ${key}="${item.attrs[key]}"`;
}
}
if (item.children) {
html += ">";
for (const child of item.children) {
html += parseItem(child);
}
html += `</${item.name}>`;
} else {
html += " />";
}
return html;
}
let result = "";
for (const item of arr) {
result += parseItem(item);
}
return result;
}
}
}
</script>
<!-- #endif -->
<style lang="scss" scoped>
/* #ifdef MP */
.uni-im-editor-mp {
width: 100%;
height: auto;
}
/* #endif */
.uni-im-editor {
min-height: 26px;
max-height: 110px;
overflow: auto;
// 解决ios下不能编辑的问题
-webkit-user-modify: read-write-plaintext-only;
/* #ifdef APP */
&,* {
-webkit-user-select:text;
}
/* #endif */
&:focus {
outline: none;
}
& ::v-deep {
img {
max-width: 90%;
display: block;
}
.nickname {
color: #0b65ff !important;
user-select: text;
margin-right: -3px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
}
}
</style>
\ No newline at end of file
......@@ -8,7 +8,7 @@
<template v-if="matchedFriends.data?.length">
<view class="category-name">联系人</view>
<uni-list-chat
<uni-im-info-card
class="conversation-list-item"
v-for="item in matchedFriends.data"
:key="item.id"
......@@ -29,7 +29,7 @@
<template v-if="matchedGroups.data?.length">
<view class="category-name">群聊</view>
<uni-list-chat
<uni-im-info-card
class="conversation-list-item"
v-for="item in matchedGroups.data"
:key="item.id"
......@@ -51,7 +51,7 @@
<template v-if="matchedConversations.data?.length">
<view class="category-name">聊天记录</view>
<uni-list-chat
<uni-im-info-card
class="conversation-list-item"
v-for="item in matchedConversations.data"
:key="item.id"
......@@ -242,7 +242,9 @@
.tip {
flex: 1;
}
.conversation-list .conversation-list-item {
background-color: #fff;
}
.conversation-list .conversation-list-item.focus {
border: 2px solid #1ab94e;
}
......@@ -257,9 +259,6 @@
/* #endif */
}
/* #ifdef H5 */
.conversation-list .conversation-list-item ::v-deep .uni-list-chat__content-title {
font-size: 14px;
}
.conversation-list ::v-deep .conversation-list-item .uni-list--border {
display: none;
}
......
......@@ -5,7 +5,7 @@
<uni-icons size="26" type="sound-filled" color="#0cc8fa"></uni-icons>
<text class="title">群公告</text>
</view>
<text :selectable="true" :user-select="true" space="nbsp" :decode="true" class="content">{{notification.content}}</text>
<textMsg :msg="{body:notification.content}"></textMsg>
<text class="create_time">公告时间:{{friendlyTime}}</text>
</view>
</view>
......@@ -13,7 +13,11 @@
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import textMsg from '@/uni_modules/uni-im/components/uni-im-msg/types/text.vue';
export default {
components:{
textMsg
},
data() {
return {
notification:{
......@@ -38,7 +42,7 @@
},
computed: {
friendlyTime() {
return uniIm.utils.toFriendlyTime(this.notification.create_time || Date.now())
return uniIm.utils.toFriendlyTime(this.notification.create_time + uniIm.heartbeat * 0)
}
}
}
......@@ -60,7 +64,7 @@
border-radius: 8px;
}
.group-notification {
padding:14px 16px;
padding:14px 0;
background-color: #FFFFFF;
width: 600rpx;
margin-top: 10px;
......@@ -73,12 +77,9 @@
color: #888;
font-size: 18px;
}
.group-notification .content{
color: #555;
padding: 6px 0;
/* #ifndef APP-NVUE */
word-break: break-all;
/* #endif */
font-size: 16px;
.title-box,.create_time {
color: #888;
font-size: 14px;
padding-left: 15px;
}
</style>
\ No newline at end of file
......@@ -7,15 +7,6 @@
const reg = /^[0-9]*$/g
return (typeof val === 'number' || reg.test(val) )? val + 'px' : val;
}
// #ifdef APP-NVUE
// import iconUrl from './uni-im-icons.ttf'
const domModule = uni.requireNativePlugin('dom')
domModule.addRule('fontFace', {
'fontFamily': "uni-im-icons",
// 'src': "url('"+iconUrl+"')"
'src': "url('https://at.alicdn.com/t/c/font_3726059_96d6x1ujhb.ttf?t=1712807858973')"
});
// #endif
export default {
emits:['click'],
data() {
......@@ -64,7 +55,6 @@
cursor: pointer;
/* #endif */
}
/* #ifndef APP-NVUE */
@font-face {
font-family: "uni-im-icons"; /* Project id 3726059 */
src: url('https://at.alicdn.com/t/c/font_3726059_96d6x1ujhb.ttf?t=1712807858973') format('truetype');
......@@ -80,5 +70,4 @@
.uni-im-delete:before {
content: "\e63d";
}
/* #endif */
</style>
<template>
<view class="info-card" :class="{link}" @click="$emit('click')">
<slot name="left"></slot>
<view class="avatar-box">
<image class="avatar" :src="avatarUrl" mode="widthFix"></image>
<slot name="avatar-overlay-list"></slot>
</view>
<view class="main">
<view class="row">
<text class="title">{{title}}</text>
<view class="tag-box">
<view class="tag" v-for="(tag,index) in tags" :key="tag">{{tag}}</view>
</view>
<view class="time" v-if="time">{{time}}</view>
</view>
<view class="row">
<view class="note-box">
<text class="red-note">{{redNote}}</text>
<text class="note">{{note}}</text>
</view>
<view class="state">
<image v-if="mute" class="mute" mode="widthFix" src="@/uni_modules/uni-im/static/mute.png">
</image>
<template v-if="badge">
<view v-if="mute" class="red-point"></view>
<view v-else class="badge">{{badge > 99 ? '99+' : badge}}</view>
</template>
</view>
</view>
</view>
<image v-if="pinned" class="pinned" mode="widthFix" src="@/uni_modules/uni-im/static/pinned.png">
</image>
<slot></slot>
<slot name="right"></slot>
</view>
</template>
<script>
export default {
emits: ['click'],
props: {
avatarUrl: {
type: String,
default: '/uni_modules/uni-im/static/avatarUrl.png'
},
title: {
type: String,
default: ''
},
tags: {
type: Array,
default: () => []
},
time: {
type: String,
default: ''
},
note: {
type: String,
default: ''
},
badge: {
type: Number,
default: 0
},
mute: {
type: Boolean,
default: false
},
pinned: {
type: Boolean,
default: false
},
redNote: {
type: String,
default: ''
},
link: {
type: Boolean,
default: false
}
},
computed: {
},
data() {
return {
}
},
methods: {}
}
</script>
<style lang="scss" scoped>
// 限制只能一行,超出显示省略号
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.info-card {
position: relative;
padding:10px 12px;
flex-direction: row;
::after {
content: "";
position: absolute;
left: 50px;
right: 0;
bottom: 0;
height: 1px;
transform: scaleY(0.1);
background-color: #eee;
}
/* #ifdef H5 */
@media screen and (min-device-width:960px){
::after {
display: none;
}
}
&.link{
cursor: pointer;
&:hover {
background-color: #f0f0f0;
}
}
/* #endif */
.avatar-box{
position: relative;
.avatar {
width: 40px;
height: 40px;
border-radius: 5px;
flex-shrink: 0;
}
}
.main {
flex-grow: 1;
justify-content: center;
overflow: hidden;
margin-left: 5px;
flex-shrink: 1;
.row {
flex-direction: row;
align-items: center;
// min-height: 20px;
.title {
font-size: 16px;
color: #333;
@include ellipsis;
}
.tag-box {
transform: scale(0.8);
margin-right: 5px;
.tag {
word-break: keep-all;
font-size: 12px;
color: #1a6dfe;
border: 1px solid #1a6dfe;
border-radius: 2px;
padding: 0 2px;
}
}
.time {
// 不换行
white-space: nowrap;
font-size: 10px;
color: #999;
margin-left: auto;
}
.state {
margin-left: auto;
flex-direction: row;
align-items: center;
.red-point {
width: 8px;
height: 8px;
background-color: #f00;
border-radius: 50%;
margin-left: 2px;
}
.mute {
width: 13px;
height: 13px;
opacity: 0.7;
}
.badge {
transform: scale(0.9);
font-size: 12px;
color: #fff;
background-color: #f00;
border-radius: 10px;
min-width: 16px;
min-height: 16px;
padding: 0 5px;
justify-content: center;
align-items: center;
margin:0 2px;
align-self: flex-end;
}
}
.note-box {
flex-direction: row;
overflow: hidden;
align-items: center;
flex-shrink: 1;
.red-note {
font-size: 12px;
white-space: nowrap;
color: #f00;
}
.note {
font-size: 14px;
color: #aaa;
@include ellipsis;
}
}
}
}
.pinned {
position: absolute;
right: 3px;
top: 3px;
width: 8px;
height: 8px;
}
}
</style>
\ No newline at end of file
......@@ -25,25 +25,13 @@
}
</script>
<style>
/* #ifndef APP-NVUE */
view {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* #endif */
<style lang="scss" scoped>
.refresh-box {
height: 50px;
justify-content: center;
align-items: center;
flex-direction: row;
/* #ifdef APP-NVUE */
width: 750rpx;
/* #endif */
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
}
.refresh-icon {
width: 25px;
......
<template>
<view class="list-root">
<!-- #ifdef APP-NVUE -->
<list class="list" :bounce="false" :render-reverse="true">
<slot></slot>
<!-- #ifdef APP-NVUE -->
<!-- 解决APP端的滚动锚定问题,在最后一个cell 设置 keep-scroll-position 和 render-reverse-position -->
<cell :keep-scroll-position="true" :render-reverse-position="true" ref="uni-im-list-last-item">
<!-- 高度为0的 最后一个元素用于方便滚动到最后一个元素 -->
</cell>
<!-- #endif -->
</list>
<view :style="{height:paddingBottom}"></view>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<!-- fast-deceleration 滑动减速速率控制,enable-passive 开启 passive 特性,能优化一定的滚动性能 -->
<scroll-view :scroll-top="scrollTop" :scroll-into-view="scrollIntoView"
:enhanced="true" :bounces="false" :enable-passive="false" :fast-deceleration="false"
class="scroll-view" :scroll-anchoring="true" :scroll-y="scrollY" :scroll-with-animation="false"
:style="{paddingBottom}" :enable-flex="true"
@dragstart="dragstart" @dragend="dragend"
@scroll="onScroll"
@scrolltolower="onScrollToLower"
>
<slot></slot>
<view id="uni-im-list-last-item" key="uni-im-list-last-item">
<!-- 高度为0的 最后一个元素用于方便滚动到最后一个元素 -->
</view>
</scroll-view>
<view class="scroll-view fake-scroll-view">
<slot name="floating-block"></slot>
</view>
<!-- #endif -->
</view>
<template>
<scroll-view class="uni-im-msg-scroll-view" :scroll-anchoring="true" :enable-flex="true" :bounces="false"
:scroll-with-animation="false" :scroll-y="scrollY" :scroll-top="scrollTop" :scroll-into-view="scrollIntoView"
@scroll="onScroll" @scrolltolower="onScrollToLower" :show-scrollbar="true">
<view class="scroll-content">
<slot></slot>
<view id="uni-im-list-last-item" key="uni-im-list-last-item">
<!-- 高度为0的 最后一个元素用于方便滚动到最后一个元素 -->
</view>
</view>
</scroll-view>
</template>
<script>
/**
* uni-im-list 组件,渲染一个列表。
*
* @module
*/
export default {
data() {
return {}
},
props: {
scrollY: {
default: true
},
scrollTop: {
default: 0
},
scrollIntoView: {
type: String,
default: ''
},
paddingBottom: {
default: 0
}
},
methods: {
onScroll(e) {
this.$emit('scroll', e)
},
onScrollToLower(e) {
this.$emit('scrolltolower', e)
},
dragstart(e) {
// console.log('dragstart');
this.$emit('dragstart', e)
},
dragend(e) {
this.$emit('dragend', e)
},
},
mounted() {
}
}
/**
* uni-im-list 组件,渲染一个列表。
*
* @module
*/
export default {
emits: ['scroll', 'scrolltolower'],
data() {
return {}
},
props: {
scrollY: {
default: true
},
scrollTop: {
default: 0
},
scrollIntoView: {
type: String,
default: ''
}
},
methods: {
onScroll(e) {
this.$emit('scroll', e)
},
onScrollToLower(e) {
this.$emit('scrolltolower', e)
}
},
mounted() {}
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
.scroll-view{
overflow-anchor: auto !important;
/* #ifdef MP-WEIXIN */
height: 100vh;
/* #endif */
/* #ifdef H5 */
height: calc(100vh - 44px);
/* #endif */
}
/* #endif */
/* #ifdef APP-NVUE */
.list-root,.list{
flex: 1;
}
/* #endif */
<style lang="scss" scoped>
.uni-im-msg-scroll-view {
overflow-anchor: auto !important;
height: 100%;
-webkit-overflow-scrolling: touch;
// 上下翻转
transform: scale(1, -1);
.scroll-content {
min-height: 100%;
flex-direction: column-reverse;
}
& ::v-deep .uni-scroll-view {
/* 滚动条的样式 */
&::-webkit-scrollbar {
width: 5px; /* 滚动条宽度 */
}
/* 滚动条滑块的样式 */
&::-webkit-scrollbar-thumb {
background-color: #bbb; /* 滑块颜色 */
border-radius: 5px; /* 滑块圆角 */
}
}
}
.fake-scroll-view {
position: absolute;
top:0;
left:0;
right:0;
background-color: transparent;
/* #ifdef H5 || MP */
pointer-events: none;
/* #endif */
color: #576b95;
}
.fake-scroll-view:hover {
color: #7c8cae;
}
</style>
/* #ifdef H5 */
@media screen and (min-device-width:960px) {
.uni-im-msg-scroll-view {
// 关闭上下翻转
transform: scale(1, 1);
.scroll-content {
flex-direction: column;
}
}
}
/* #endif */
</style>
\ No newline at end of file
<template>
<view class="root">
<uni-im-list v-if="visibleMsgList.length" class="uni-im-list" :scrollTop="scrollTop" :scroll-into-view="scrollIntoView" :paddingBottom="paddingBottom" ref="uni-im-list" @scroll="onScroll" @scrolltolower="onScrollToLower">
<view class="uni-im-msg-list-root">
<uni-im-list class="uni-im-list" :scrollTop="scrollTop"
:scroll-into-view="scrollIntoView" ref="uni-im-list"
@scroll="onScroll" @scrolltolower="onScrollToLower"
>
<template v-for="(msg,index) in visibleMsgList" :key="msg.unique_id || msg._id">
<uni-im-list-item :ref="'item-'+index">
<view v-if="index === 0" class="data-state-tip-box" @appear="beforeLoadMore">
<uni-im-load-state v-if="canAnchor || !isMpPlatform" :status="hasMore?'loading':'noMore'"
:contentText='{"contentrefresh": "正在加载历史消息","contentnomore": "没有更多历史消息"}'></uni-im-load-state>
<template v-else>
<button v-if="hasMore" class="loadMore-btn" size="mini" @click="beforeLoadMore" :loading="loadMoreIng">{{loadMoreIng ? '加载中':'点击加载更多'}}</button>
<text v-else class="data-state-tip-text">没有更多历史消息</text>
</template>
</view>
<view :class="['item',msg.type]" :id="'item-'+index" @click="clickItem">
<label class="msg-box" :class="{'active-msg':msg._id === activeMsgId || msg.unique_id === activeMsgId}" @click="checkMsg(msg)">
<template v-if="chooseMore">
<checkbox :checked="checkedMsgList.find(i=>i._id == msg._id) != undefined" class="checkbox" />
<view class="mask"></view>
</template>
<!-- <text style="width: 750rpx;text-align: center;border: 1px solid #000;">{{'item-'+index}}</text> -->
<uni-im-msg :msg="msg" :id="msg._id" :self="current_uid() == msg.from_uid" :index="index" @appear="msgOnAppear(msg._id)"
@putChatInputContent="putChatInputContent" :equalPrevTime="equalPrevTime(index)"
:avatar_file="conversation.avatar_file" @showMsgById="showMsgById" @showControl="showControl"
@loadMore="loadMore" @chatInputContentAddcallUser="chatInputContentAddcallUser" @retriesSendMsg="retriesSendMsg"
@viewMsg="viewMsg" :ref="'uni-im-msg'" class="uni-im-msg"
>
</uni-im-msg>
</label>
</view>
</uni-im-list-item>
<view class="uni-im-list-item" :ref="'item-'+index">
<view v-if="index === 0" class="data-state-tip-box">
<uni-im-load-state :status="hasMore?'loading':'noMore'"
:contentText='{"contentrefresh": "正在加载历史消息","contentnomore": "没有更多历史消息"}'></uni-im-load-state>
</view>
<view :class="['item',msg.type]" :id="'item-'+index" @click="clickItem">
<label class="msg-box" :class="{'active-msg':msg._id === activeMsgId || msg.unique_id === activeMsgId}" @click="checkMsg(msg)">
<template v-if="chooseMore">
<checkbox :checked="checkedMsgList.find(i=>i._id == msg._id) != undefined" class="checkbox" />
<view class="mask"></view>
</template>
<!-- <text style="width: 750rpx;text-align: center;border: 1px solid #000;">{{'item-'+index}}</text> -->
<uni-im-msg :msg="msg" :id="msg._id" :self="current_uid() == msg.from_uid" :index="index"
@putChatInputContent="putChatInputContent" :equalPrevTime="equalPrevTime(index)"
:avatar_file="conversation.avatar_file" @showMsgById="showMsgById" @showControl="showControl"
@loadMore="loadMore" @longpressMsgAvatar="longpressMsgAvatar" @retriesSendMsg="retriesSendMsg"
@viewMsg="viewMsg" :ref="'uni-im-msg'" class="uni-im-msg"
>
</uni-im-msg>
</label>
</view>
</view>
</template>
<template v-slot:floating-block>
<view v-if="hasNewMsg" class="new-msg-bar" @click="showLast">
<uni-icons type="pulldown" size="18" color="#007fff"></uni-icons>
<text>有新消息</text>
</view>
</template>
<uni-im-load-state v-if="visibleMsgList.length === 0" :status="hasMore?'loading':'noMore'" class="uni-im-list-item" :contentText='{"contentrefresh": "加载中","contentnomore": "- 没有聊天记录 -"}'></uni-im-load-state>
</uni-im-list>
<uni-im-load-state v-else :status="hasMore?'loading':'noMore'" class="mg-15" :contentText='{"contentrefresh": "加载中","contentnomore": "- 没有聊天记录 -"}'></uni-im-load-state>
<!-- <view class="slider-box">
val:{{val}} scrollTop:{{scrollTop}}
<slider value="1" @change="sliderChange" min="1" max="14" step="1" />
</view> -->
<!-- <view style="position: fixed;top: 100px;width: 500rpx;">
paddingBottom:{{paddingBottom}}
scrollTop:{{scrollTop}}
visibleMsgList.length:{{visibleMsgList.length}}
scrollIntoView:{{scrollIntoView}}
<button @click="showLast">showLast</button>
</view> -->
<view v-if="hasNewMsg" class="new-msg-bar" @click="showLast">
<uni-icons type="pulldown" size="18" color="#007fff"></uni-icons>
<text>有新消息</text>
</view>
<view style="position: fixed;top: 100px;width: 500rpx;">
<!-- hasNewMsg:{{hasNewMsg}} -->
<!-- scrollTop:{{scrollTop}} -->
<!-- scrollIntoView:{{scrollIntoView}}
visibleMsgList.length:{{visibleMsgList.length}} -->
<!-- <button @click="showLast">showLast</button> -->
</view>
<view v-if="call_list.length" class="showCallMe" @click="showCallMe">@回复我({{call_list.length}})</view>
<uni-popup @change="closeGroupNotification" ref="group-notification-popup" type="center" class="group-notification-popup">
<uni-popup @change="$event.show?'':closeGroupNotification()" ref="group-notification-popup" type="center" class="group-notification-popup">
<uni-im-group-notification ref="group-notification"></uni-im-group-notification>
</uni-popup>
<!-- #ifdef H5 -->
<uni-im-view-msg ref="view-msg"></uni-im-view-msg>
<!-- #endif -->
</view>
</template>
......@@ -72,7 +63,6 @@
* @module
* @see module:chat
* @see module:uni-im-list
* @see module:uni-im-list-item
*/
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import {
......@@ -80,43 +70,19 @@
} from '@/uni_modules/uni-id-pages/common/store';
import uniImList from './components/uni-im-list/uni-im-list';
import uniImListItem from './components/uni-im-list-item/uni-im-list-item';
// 一页多少条数据
let pageLimit = 30
// 当前页面滚动条高度
let currentScrollTop = 0
// 跟踪卷滚条的位置,以判定当前卷滚条是否为贴底状态
class ScrollTracker {
position = 0
at_bottom = false
scroll(position) {
// scrolltolower 事件触发后仍会有 scroll 事件,
// 所以只要位置在递增就仍然是贴底状态,
// 如果逆向变化则脱离贴底状态。
if (this.position > position) {
this.at_bottom = false
}
this.position = position
}
reachBottom() {
this.at_bottom = true
}
isAtBottom() {
return this.at_bottom
}
}
let intersectionObserver = null;
let appearObj = {};
export default {
components: {
uniImList,
uniImListItem
uniImList
},
emits:['checkedMsgList','chatInputContentAddcallUser','showControl','clickItem','retriesSendMsg','putChatInputContent'],
emits:['checkedMsgList','longpressMsgAvatar','showControl','clickItem','retriesSendMsg','putChatInputContent'],
computed: {
...uniIm.mapState(['systemInfo', 'isWidescreen']),
loadState() {
......@@ -132,44 +98,27 @@
// 返回倒数 laterRenderIndex 条消息,实现懒渲染
.slice(-this.laterRenderIndex)
this.$nextTick(() => {
uniIm.utils.throttle(this.setIntersectionObserver, 1000);
uniIm.utils.throttle(this.setIntersectionObserver, 3000);
})
return visibleMsgList
},
canAnchor() {
// #ifdef APP-NVUE
return true
// #endif
// #ifndef APP-NVUE
const {
browserName,
model
} = this.systemInfo
return browserName != "safari" && !model.includes("iPhone")
// #endif
},
isMpPlatform() {
// #ifdef MP
return true
// #endif
// #ifndef MP
return false
// #endif
}
},
data() {
return {
val: 0,
conversation: {},
conversation: {
hasMore: true,
has_unread_group_notification: false,
group_info: {
notification: false
}
},
scrollIntoView: "",
scrollTop: 0,
scrollTracker: new ScrollTracker(),
hasNewMsg: false,
call_list: [],
activeMsgId: "",
loadMoreIng: false,
// 延迟渲染,避免页面卡顿
// 延迟渲染,避免页面卡顿
laterRenderIndex: pageLimit
}
},
......@@ -177,20 +126,29 @@
'conversation.call_list'(call_list) {
this.call_list = call_list
},
'conversation.unreadGroupNotification': {
handler(unreadGroupNotification) {
'conversation.has_unread_group_notification': {
async handler(hasUnreadGroupNotification) {
const group_notification = this.conversation?.group_info?.notification
const conversation_id = this.conversationId
// 弹出群公告
if (unreadGroupNotification && unreadGroupNotification.content) {
if (hasUnreadGroupNotification && group_notification && group_notification.content) {
await uniIm.utils.sleep(1000)
// TODO 临时解决,公告还没弹出来就切换会话,导致弹出多次
if(conversation_id !== this.conversationId){
return
}
// 判断列表中是否已经渲染了此群公告,是则 call 当前用户。否则弹框提示
let groupNotificationMsg = [...this.visibleMsgList].reverse().find(msg => msg.action ===
'update-group-info-notification')
console.log('groupNotificationMsg', groupNotificationMsg);
// console.log('groupNotificationMsg', groupNotificationMsg,this.visibleMsgList);
if (groupNotificationMsg) {
this.conversation.call_list.push(groupNotificationMsg._id)
this.closeGroupNotification()
} else {
this.$refs["group-notification-popup"].open()
this.$nextTick(() => {
this.$refs["group-notification"].notification = unreadGroupNotification
this.$refs["group-notification"].notification = group_notification
})
}
}
......@@ -199,9 +157,6 @@
}
},
props: {
paddingBottom: {
default: ''
},
conversationId: {
default () {
return false
......@@ -222,24 +177,25 @@
// t:i
// })
// }
//
},
destroyed() {
// console.log('destroyed')
if (intersectionObserver){
intersectionObserver.disconnect()
if (this.intersectionObserver){
this.intersectionObserver.disconnect()
}
},
methods: {
async init() {
if (intersectionObserver){
intersectionObserver.disconnect()
if (this.intersectionObserver){
this.intersectionObserver.disconnect()
}
this.conversation = await uniIm.conversation.get(this.conversationId)
// init data --start
this.scrollIntoView = ''
this.scrollTop = 0
this.laterRenderIndex = pageLimit
currentScrollTop = 0
this.laterRenderIndex = pageLimit
if (!this.conversation.isInit) {
this.conversation.hasMore = true
......@@ -251,16 +207,15 @@
}
}
// #ifndef APP
// app端会自动固定在底部,其他端需要执行一次显示最后一条
this.$nextTick(() => {
this.showLast(300)
// TODO 延迟300毫秒 临时解决,未知情况下$nextTick后仍然不能滚到最后一条的问题
setTimeout(() => {
this.showLast(300)
}, 300);
})
// #endif
if(this.isWidescreen){
this.$nextTick(()=>{
this.showLast()
// TODO:未知原因,部分情况下 $nextTick时机执行不能定位到最后一条,需要用setTimeout 500再次定位
setTimeout(() => {
this.showLast()
},500)
})
}
},
async loadMore(callback) {
let datas = []
......@@ -292,7 +247,7 @@
return datas
},
async beforeLoadMore() {
if (!this.loadMoreIng && this.hasMore && currentScrollTop < 30) {
if (!this.loadMoreIng && this.hasMore) {
this.loadMoreIng = true
await this.loadMore()
this.loadMoreIng = false
......@@ -304,40 +259,64 @@
if (index == -1) {
return //因为是异步的,可能已经被销毁了替换了新对象
}
// console.log('msgOnAppear',index);
}, 1000);
},
async setIntersectionObserver() {
// console.log('setIntersectionObserver');
if (intersectionObserver) {
// console.log('intersectionObserver存在','执行销毁');
intersectionObserver.disconnect()
if (this.intersectionObserver) {
// console.log('this.intersectionObserver存在','执行销毁');
this.intersectionObserver.disconnect()
}
intersectionObserver = uni.createIntersectionObserver(this, { observeAll: true });
intersectionObserver.relativeTo('.uni-im-list .scroll-view', { top: -50, bottom: -50 })
await uniIm.utils.sleep(1000)
this.intersectionObserver = uni.createIntersectionObserver(this, { observeAll: true })
.relativeTo('.uni-im-list', {})
.observe('.uni-im-msg', (res) => {
const msgId = res.id
const msgRef = this.$refs['uni-im-msg'].find(item => item.msg._id == msgId)
if (!msgRef) {
// console.log('找不到msgRef', msgId);
// console.error('找不到msgRef,或会话已切走', msgId);
return
}
const isAppear = appearObj[msgId] || false;
if (res.intersectionRatio > 0 && !isAppear) {
// hasBeenDisplayed表示是否已经显示过了
const hasBeenDisplayed = appearObj[msgId] || false;
// 新显示的
const isAppear = res.intersectionRatio > 0 && !hasBeenDisplayed
// 是否为最后一条消息
const isLastMsg = [...this.visibleMsgList].pop()?._id === msgId
if(isLastMsg){
this.lastMsgIsShow = isAppear
if(this.lastMsgIsShow){
this.hasNewMsg = false
}
}
if (isAppear) {
appearObj[msgId] = true;
msgRef.onAppear()
this.msgOnAppear(msgId)
// console.error('出现了',msgRef.msg.body)
const isFirstMsg = this.visibleMsgList[0]?._id === msgId
// 是否为第一个消息
if (isFirstMsg) {
// console.log('第一个消息出现在视窗内,加载更多',this.visibleMsgList[0]?._id , msgId);
this.beforeLoadMore()
}
// 调用扩展点,通知消息列表某条消息进入显示区。
uniIm.extensions.invokeExts('msg-appear', msgRef.msg, {
user_id: uniCloud.getCurrentUserInfo().uid,
isInternalUser: this.uniIDHasRole('staff'),
})
} else if (!res.intersectionRatio > 0 && isAppear) {
} else if (!res.intersectionRatio > 0 && hasBeenDisplayed) {
appearObj[msgId] = false;
msgRef.onDisappear()
// console.error('消失了',msgRef.msg.body)
// 调用扩展点,通知消息列表某条消息离开显示区。
uniIm.extensions.invokeExts('msg-disappear', msgRef.msg, {
user_id: uniCloud.getCurrentUserInfo().uid,
......@@ -350,89 +329,66 @@
this.$refs['view-msg'].open(msgList);
},
async onScroll(e) {
this.scrollTracker.scroll(e.detail.scrollTop)
// 记录当前滚动条高度
currentScrollTop = e.detail.scrollTop
// console.log('onScroll', e.detail.scrollTop)
// TODO:滚动停止后,将end置为true
this.onScroll.end = false
if (this.onScroll.timeoutId) {
clearTimeout(this.onScroll.timeoutId)
}
this.onScroll.timeoutId = setTimeout(() => {
this.onScroll.end = true
}, 500)
currentScrollTop = e.detail.scrollTop
// #ifndef APP-NVUE
// 移动端 safari内核浏览器有滚动锚定问题,需要通过点击点击“加载按钮”加载历史消息
if (!this.canAnchor && this.isMpPlatform) {
return
}
// #endif
await this.beforeLoadMore()
},
async onScrollToLower() {
this.scrollTracker.reachBottom()
this.hasNewMsg = false
},
async canHoldScrollDo(fn){
return new Promise((resolve) => {
// #ifdef APP-NVUE
fn()
resolve()
// #endif
// #ifndef APP-NVUE
let tryIndex = 0
const tryInsert = () => {
tryIndex++
if (tryIndex > 100) {
console.error("防止未意料的情况下“死循环”限制最多尝试100次");
// 防止死循环,最多尝试100次
fn()
return resolve()
}
const msgListIsNoFull = this.visibleMsgList.length < 10
if (msgListIsNoFull || currentScrollTop > 9 || !this.canAnchor) {
if (!this.canAnchor) {
if (this.visibleMsgList.length && this.onScroll.end === false) {
return setTimeout(() => {
tryInsert()
}, 500)
}
fn()
this.scrollTop = currentScrollTop
this.$nextTick(async () => {
let itemScrollTop = await this.getItemScrollTopByIndex(pageLimit - 1)
// console.log('itemScrollTop',itemScrollTop);
if (this.isWidescreen) {
itemScrollTop -= 110
} else {
itemScrollTop -= 30
}
this.scrollTop = itemScrollTop
currentScrollTop = itemScrollTop
})
} else {
/**
* 解决web-pc端,部分情况下插入消息滚动内容会跳动的问题
*/
// #ifdef H5
if(this.isWidescreen){
if(this.systemInfo.browserName === "chrome"){
// console.error('currentScrollTop',currentScrollTop)
if(currentScrollTop === 0 && this.visibleMsgList.length != 0){
this.scrollTop = currentScrollTop
currentScrollTop += 1
await this.$nextTick(()=>this.scrollTop = currentScrollTop)
if(!this.canHoldScrollDo.tryIndex){
this.canHoldScrollDo.tryIndex = 1
}else{
this.canHoldScrollDo.tryIndex ++
}
if(this.canHoldScrollDo.tryIndex > 100){
console.error('canHoldScrollDo tryIndex > 100')
fn()
}else{
this.$nextTick(()=>this.canHoldScrollDo(fn))
}
resolve()
} else {
// console.error('不能插入',currentScrollTop);
this.scrollTop = currentScrollTop
this.$nextTick(async () => {
this.scrollTop = 10
currentScrollTop = 10
tryInsert()
})
}else{
fn()
}
}else{
const getScrollContentHeight = ()=>document.querySelector('.scroll-content').offsetHeight
let scrollContent = getScrollContentHeight()
fn()
this.$nextTick(()=>{
const diff = getScrollContentHeight() - scrollContent
console.error( diff, diff)
this.scrollTop = currentScrollTop
currentScrollTop += diff
this.$nextTick(()=> this.scrollTop = currentScrollTop)
})
}
tryInsert()
// #endif
})
return
}
// #endif
fn()
},
/**
* @description 防止非app-nvue端,部分情况下在顶部插入消息时滚动锚定生效的问题
*/
async insertMsg(data) {
// 重新获取会话对象,防止web pc端 切换太快引起的会话对象指向错误
const conversation = await uniIm.conversation.get(data[0].conversation_id)
......@@ -459,78 +415,59 @@
console.log('msgId', msgId)
this.showMsgById(msgId)
},
showLast(duration = 300) {
showLast() {
let mLength = this.visibleMsgList.length
this.showMsgByIndex(mLength - 1, duration)
this.showMsgByIndex(mLength - 1)
this.hasNewMsg = false
this.scrollTracker.reachBottom()
},
notifyNewMsg() {
this.hasNewMsg = true
if (this.scrollTracker.isAtBottom()) {
// 如果当前在底部,则自动显示最新消息
if (this.lastMsgIsShow) {
this.showLast()
}
},
// #ifndef APP-NVUE
async getItemScrollTopByIndex(index) {
async getElInfo(select){
return await new Promise((resolve, rejece) => {
const query = uni.createSelectorQuery().in(this);
query.select('#item-' + index).boundingClientRect(data => {
query.select(select).boundingClientRect(data => {
if (!data) {
return console.log('找不到 showMsgByIndex #item-' + index);
}
let val = currentScrollTop + data.top + data.height
if (val < 0) {
val = 0
console.log('找不到 showMsgByIndex' + select);
return rejece(false)
}
resolve(val)
resolve(data)
}).exec()
})
},
// #endif
async showMsgByIndex(index, duration = 300) {
async showMsgByIndex(index) {
if (index == -1) {
return
}
// #ifdef APP-NVUE
let target = this.$refs['item-' + index][0];
// console.log('滚动到第', index, target);
const nativePluginDom = uni.requireNativePlugin('dom')
nativePluginDom.scrollToElement(target, {
// animated: duration != 0,
// offset: 999
});
// #endif
// #ifndef APP-NVUE
let listHeight = this.systemInfo.windowHeight
// #ifdef H5
if (uniIm.isWidescreen) {
listHeight = document.querySelector('.uni-im-list uni-scroll-view').clientHeight
// console.log('listHeight',listHeight)
} else {
listHeight -= 44
const listHeight = (await this.getElInfo('.uni-im-list')).height
const targetInfo = await this.getElInfo('#item-' + index)
const itemScrollTop = targetInfo.top
// console.error('currentScrollTop',currentScrollTop)
// console.error('itemScrollTop',itemScrollTop,listHeight,currentScrollTop,index)
let val = 0;
let m = listHeight - targetInfo.height
if(m < 0){
m = 10
}
// #endif
const itemScrollTop = await this.getItemScrollTopByIndex(index)
const val = itemScrollTop - listHeight * 0.7 + parseInt(this.paddingBottom)
if(this.isWidescreen){
val = itemScrollTop + currentScrollTop - 0.5 * m
}else{
val = itemScrollTop * -1 + currentScrollTop + 0.3 * m
}
// console.error('val',val)
// 赋值为当前滚动条的高度
this.scrollTop = currentScrollTop
// 设置一个新值触发视图更新 -> 滚动
this.$nextTick(async () => {
this.scrollTop = val
currentScrollTop = val
// console.error('currentScrollTop',currentScrollTop)
})
// #endif
},
// 测试专用
async sliderChange(e) {
let index = e.detail.value
console.log(index)
this.val = index
this.showMsgByIndex(index)
},
async showMsgById(msgId) {
// 找到消息的索引
......@@ -555,21 +492,38 @@
}
this.activeMsgId = msgId
setTimeout(() => {
if(this.showMsgActiveColorActionT){
clearTimeout(this.showMsgActiveColorActionT)
}
this.showMsgActiveColorActionT = setTimeout(() => {
this.activeMsgId = ''
}, 2000);
this.showMsgByIndex(index)
// 如果是显示群公告,则设置未读的群公告内容为 false
if (this.visibleMsgList[index].action === "update-group-info-notification") {
this.conversation.unreadGroupNotification = false
this.closeGroupNotification()
}
},
closeGroupNotification(e) {
if (e.show === false) {
this.conversation.unreadGroupNotification = false
}
closeGroupNotification() {
// console.log('######关闭群公告',this.conversationId)
const db = uniCloud.database();
db.collection('uni-im-conversation')
.where({
id:this.conversationId,
user_id: uniCloud.getCurrentUserInfo().uid
})
.update({
has_unread_group_notification: false
}).then(res => {
this.conversation.has_unread_group_notification = false
// console.log('关闭群公告成功', res)
}).catch(err => {
console.error('关闭群公告失败', err)
})
},
isChecked(msg) {
return this.checkedMsgList.some(i => i._id === msg._id)
......@@ -593,8 +547,8 @@
showControl(e) {
this.$emit('showControl', e)
},
chatInputContentAddcallUser(e) {
this.$emit('chatInputContentAddcallUser', e)
longpressMsgAvatar(e) {
this.$emit('longpressMsgAvatar', e)
},
retriesSendMsg(e) {
this.$emit('retriesSendMsg', e)
......@@ -611,19 +565,31 @@
</script>
<style lang="scss" scoped>
.root,
.uni-im-msg-list-root,
.uni-im-list {
flex: 1;
background-color: transparent;
height: 100%;
}
.uni-im-list-item {
transform: scale(1, -1);
}
/* #ifdef H5 */
@media screen and (min-device-width:960px) {
.uni-im-list-item {
// 关闭上下翻转
transform: scale(1, 1);
}
}
/* #endif */
.item {
margin:10px 0;
// border: solid 1px #0055ff;
}
.mg-15 {
margin: 15px;
.uni-im-list-item .system .checkbox {
display: none;
}
.data-state-tip-box {
......@@ -644,26 +610,6 @@
color: #999999;
}
/* #ifndef APP-NVUE */
.data-state-tip-icon {
justify-content: center;
align-items: center;
display: flex;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* #endif */
/* #ifdef H5 */
.loadMore-btn {
font-size: 14px;
......@@ -700,17 +646,6 @@
background-color: #f9f9f9;
}
.slider-box {
border: 1px solid #000;
position: fixed;
width: 750rpx;
height: 55px;
top: 60px;
right: 0;
z-index: 999;
background-color: #FFF;
}
.showCallMe {
background-color: #62caf8;
border-radius: 50px;
......
<template>
<view v-if="isShow" :style="{opacity}" id="popup-control">
<view class="control-mark" @touchstart.prevent="closeMe" @click="closeMe" :style="{opacity}">
<view class="control-mark" @touchstart.prevent="closeMe" @click="closeMe">
</view>
<view ref="content" class="content" :style="{top:controlData.top,left:controlData.left,right:controlData.right,opacity}">
<view ref="content" class="content" :style="{top:controlData.top,left:controlData.left,right:controlData.right}">
<template v-for="(item,index) in controlList">
<view :key="index" class="control-item" v-if="typeof item.canDisplay == 'function' ? item.canDisplay() : item.canDisplay " @click="item.action">
<uni-im-icons v-if="item.icon" :code="item.icon" size="16" color="#FFF"></uni-im-icons>
......@@ -10,9 +10,8 @@
</view>
</template>
</view>
<view class="icon" :class="{isInTop:controlData.isInTop}" :style="{right:iconBoxRight,left:iconBoxLeft,top:controlData.top,opacity}"></view>
<view class="icon" :class="{isInTop:controlData.isInTop}" :style="{right:iconBoxRight,left:iconBoxLeft,top:controlData.top}"></view>
</view>
<!-- todo:多个节点都放:style="{opacity}"是为了解决nvue下,当前情况外层不能控制内层透明度的问题 -->
</template>
<script>
......@@ -74,16 +73,16 @@
},
initControlList(msg){
this.controlList = [
{
title:'复制',
action:()=>this.copyContent(),
canDisplay: ["userinfo-card","rich-text","text","image"].includes(msg.type) && (msg.type != 'image' || uniIm.systemInfo.uniPlatform === "web"),
},
{
title:'回复',
action:()=>this.answer(),
canDisplay:msg._id != undefined,// 只有发送成功的消息才能回复
},
{
title:'复制',
action:()=>this.copyContent(),
canDisplay: ["userinfo-card","rich-text","text","image"].includes(msg.type) && (msg.type != 'image' || uniIm.systemInfo.uniPlatform === "web"),
},
{
title:'撤回',
action:()=>this.revokeMsg(),
......@@ -142,7 +141,6 @@
isInTop: false
}
// #ifndef APP-NVUE
const query = uni.createSelectorQuery().in(this);
await new Promise(resolve => {
query.selectAll('.content').boundingClientRect(data => {
......@@ -150,18 +148,6 @@
resolve()
}).exec();
})
// #endif
// #ifdef APP-NVUE
let ref = this.$refs['content']
await new Promise(resolve => {
const dom = weex.requireModule('dom')
dom.getComponentRect(ref, e => {
controlData.width = e.size.width + 'px'
resolve()
})
})
// #endif
// console.error('controlData.width',controlData.width)
......@@ -253,7 +239,8 @@
}
return
*/
data = data.map(i=>i.name == 'img' ? '[图片]' : i.text).join(' ')
console.log('data',data)
data = data.map(i=>i.children?i.children[0]:i).map(i=>i.name == 'img' ? '[图片]' : i.text).join(' ')
break;
default:
break;
......@@ -346,13 +333,6 @@
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
view {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* #endif */
.content{
background-color:#252a30;
height: 55px;
......@@ -363,9 +343,7 @@
flex-direction: row;
justify-content: space-around;
align-items: center;
/* #ifndef APP-NVUE */
z-index: 9999;
/* #endif */
z-index: 10;
}
.control-item{
padding: 0 15px;
......@@ -389,21 +367,9 @@
position: fixed;
top: 0;
left: 0;
width: 750rpx;
flex: 1;
height: 9000px;
/* #ifndef APP-NVUE */
width: 100vw !important;
height: 100vh !important;
z-index: 9999;
/* #endif */
/* #ifdef H5 */
// 解决:通过*选择器设置了最大宽高 引起的大小撑不开的问题
max-height:100vh !important;
max-width:100vw !important;
/* #endif */
z-index: 9;
background-color: rgba(0,0,0,0.1);
}
......@@ -413,9 +379,7 @@
background-color: #252a30;
width: 10px;
height: 10px;
/* #ifndef APP-NVUE */
z-index: 9999;
/* #endif */
z-index: 9;
}
.isInTop{
......
......@@ -4,14 +4,14 @@
<text class="language">{{language}}</text>
<text class="copy-btn" @click="copyCode">复制代码</text>
</view>
<scroll-view :scroll-y="overflow" class="code-view-box" :style="{height:showFullBtn ? boxHeight: 'auto'}">
<!-- #ifndef APP-NVUE -->
<rich-text v-if="nodes.length" space="nbsp" :nodes="nodes" @itemclick="trOnclick" :style='{"height":webViewHeight}' class="code-view-rich-text"></rich-text>
<!-- #endif -->
<scroll-view :scroll-y="overflow" :enable-flex="true" class="code-view-box" :style="{height:showFullBtn ? boxHeight: 'auto'}">
<!-- #ifdef APP-NVUE -->
<web-view ref="web" @onPostMessage="onWebViewMsg" src="/static/app-plus/mp-html/uni-im-code-view-local.html" :style='{"height":webViewHeight}' class="web-view"></web-view>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<rich-text v-if="nodes.length" space="nbsp" :nodes="nodes" @itemclick="trOnclick" :style='{"height":webViewHeight}' class="code-view-rich-text"></rich-text>
<!-- #endif -->
</scroll-view>
<view class="show-full-btn" v-if="!isWidescreen && showFullBtn && overflow" @click="toCodePage">
<text class="show-full-text">全屏查看</text>
......@@ -166,7 +166,7 @@
<style lang="scss">
/* #ifndef APP-NVUE */
@import '@/uni_modules/uni-im/sdk/utils/highlight/github-dark.min.css';
@import '@/uni_modules/uni-im/sdk/utils/highlight/github-dark.min.scss';
/* #endif */
.code-view-root {
......@@ -207,13 +207,14 @@
position: relative;
padding: 0;
margin-bottom: 10px;
// 设置与背景一样的黑色,防止滚动条样式变化太大
background-color: #0d1117;
/* #ifndef APP-NVUE */
padding: 0 5px;
overflow: auto;
width: 100%;
/* #endif */
// 设置与背景一样的黑色,防止滚动条样式变化太大
background-color: #0d1117;
box-sizing: border-box;
}
/* #ifdef H5 */
......
......@@ -107,9 +107,7 @@
}
.file-msg-info-name {
/* #ifndef APP-NVUE */
word-wrap: break-word;
/* #endif */
font-size: 16px;
}
......
......@@ -76,16 +76,12 @@
padding: 10px;
background-color: #fff;
width: 600rpx;
/* #ifndef APP-NVUE */
max-height:200px !important;
overflow: hidden;
/* #endif */
}
.item-text-list{
/* #ifndef APP-NVUE */
max-height: 150px !important;
overflow: hidden;
/* #endif */
}
.title {
......
......@@ -38,5 +38,8 @@
<style>
.img {
width: 400rpx;
/* border: 1px solid #aaa; */
box-shadow: 0 0px 2px -1px #aaa;
border-radius: 5px;
}
</style>
......@@ -17,8 +17,8 @@
</template>
<text class="text" v-else-if="item.type == 'text'" :decode="true" space="ensp">{{item.text}}</text>
<uni-im-img v-else-if="item.name == 'img'" max-width="200px" @click="previewImage(item.attrs.src)"
:src="item.attrs.src" :width="item.attrs.width" :height="item.attrs.height" mode="widthFix" class="img" />
<uni-link class="link" v-else-if="item.name == 'a'" :href="item.attrs.href" color="#007fff"
:src="item.attrs.src" :width="item.attrs.width" :height="item.attrs.height" mode="widthFix" class="img" />
<uni-link class="link" v-else-if="item.name == 'a' && item.children && typeof(item.children[0]) === 'object'" :href="item.attrs.href" color="#007fff"
:text="item.children[0].text"></uni-link>
</template>
......@@ -185,7 +185,6 @@
}
/* #endif */
/* #ifndef APP-NVUE */
.uni-im-rich-text {
display: inline-block;
}
......@@ -193,8 +192,12 @@
.uni-im-rich-text .text {
display: inline;
word-wrap: break-word;
user-select: text;
cursor: text;
cursor: text;
/* #ifdef H5 */
@media screen and (min-device-width:960px){
user-select: text;
}
/* #endif */
}
.uni-im-rich-text .link {
......@@ -203,18 +206,12 @@
margin: 0 2px;
word-break: break-all;
}
/* #endif */
.uni-im-rich-text {
background-color: #fff;
padding: 10px;
border-radius: 10px;
/* #ifdef APP-NVUE */
width: 600rpx;
/* #endif */
/* #ifdef MP */
max-width: 600rpx;
/* #endif */
}
.uni-im-rich-text.isFromSelf {
......@@ -284,9 +281,7 @@
color: #666;
margin-top: 5px;
flex: 1;
/* #ifndef APP-NVUE */
beak-word: break-all;
/* #endif */
}
.web-info .content .web-thumbnail {
......@@ -306,9 +301,7 @@
.web-info .link-box .link {
font-size: 12px;
// 不会换行
/* #ifndef APP-NVUE */
white-space: nowrap;
/* #endif */
flex: 1;
overflow: hidden;
}
......@@ -321,10 +314,8 @@
.web-info .link-box .copy:hover {
opacity: 0.8;
}
/* #ifndef APP-NVUE */
.uni-im-rich-text.isSingeImg {
background-color: transparent !important;
padding: 0;
}
/* #endif */
</style>
\ No newline at end of file
......@@ -12,8 +12,8 @@
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const audioContext = uniIm.audioContext
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
let audioContext = uniIm.audioContext;
export default {
data() {
return {
......@@ -41,18 +41,21 @@
self() {
return this.msg.from_uid === uniCloud.getCurrentUserInfo().uid
}
},
beforeCreate() {
audioContext = uniIm.audioContext;
},
mounted() {
this.onPlay = async () => {
// console.log('soundPlayStart------------------');
console.log('soundPlayStart------------------',this.msg.body);
let currentAudioUrl = await uniIm.utils.getTempFileURL(this.msg.body.url)
let src = uniIm.audioContext.src
let src = audioContext.src
if (src == currentAudioUrl) {
this.soundPlayState = 1
} else {
this.soundPlayState = 0
}
}
}
audioContext.onPlay(this.onPlay);
this.soundPlayEnd = () => {
// console.log('soundPlayEnd------------------');
......
......@@ -101,12 +101,7 @@
line-height: 30px;
padding: 0 15rpx;
border-radius: 8px;
/* #ifdef APP-NVUE */
margin: 0 20px;
/* #endif */
/* #ifndef APP-NVUE */
margin: 0 2em;
/* #endif */
}
.group-notification {
padding:14px 16px;
......
<template>
<view class="msg-text-box" :class="msgClass">
<view class="msg-text-box" :class="{self}">
<msgRichText v-if="htmlNodes.length" :msg="{...msg,...{'body':htmlNodes}}" />
<text v-else class="msg-text" :decode="true" space="ensp">{{ msg.body }}</text>
</view>
......@@ -31,20 +31,6 @@
self() {
return this.msg.from_uid === uniCloud.getCurrentUserInfo().uid
},
msgClass() {
let msgClass = ''
let textLength = (this.msg.body + '').replace(/[\u0000-\u007f]/g, "a")
.replace(/[\u0080-\u07ff]/g, "aa")
.replace(/[\u0800-\uffff]/g, "aa").length
if (textLength > 30) {
msgClass += ' exceed'
}
if (this.self) {
msgClass += ' self'
}
return msgClass
}
},
watch: {
"msg.body": {
......@@ -93,32 +79,25 @@
}
</script>
<style>
<style lang="scss" scoped>
.msg-text-box {
border-radius: 10px;
background-color: #FFFFFF;
/* #ifndef APP-NVUE */
max-width: 100%;
/* #endif */
}
.msg-text {
padding: 6px;
font-size: 15px;
justify-content: space-between;
/* #ifndef APP-NVUE */
word-break: break-all;
user-select: text;
cursor: text;
/* #endif */
}
/* #ifdef APP-NVUE */
.exceed {
width: 600rpx;
min-width: 30px;
flex-shrink: 1;
.msg-text {
padding: 10px;
font-size: 15px;
justify-content: space-between;
word-break: break-all;
cursor: text;
/* #ifdef H5 */
@media screen and (min-device-width:960px){
user-select: text;
}
/* #endif */
}
}
/* #endif */
.self{
background-color: #c9e7ff;
}
......
......@@ -64,9 +64,7 @@ export default {
background-color: #fff;
padding: 10px;
border-radius: 10px;
/* #ifndef APP-NVUE */
min-width: 250px;
/* #endif */
}
/* #ifdef H5 */
.msg-userinfo-card,.msg-userinfo-card * {
......
......@@ -112,9 +112,6 @@
align-items: center;
background-color: rgba(0, 0, 0, 0.2);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
/* #ifndef APP-NVUE */
box-sizing: content-box;
/* #endif */
}
.video-box-mark {
......
<template>
<view v-if="!msg.is_delete" class="uni-im-msg" :class="{self}" @appear="onAppear">
<view v-if="!msg.is_delete" class="uni-im-msg" :class="{self}">
<msg-system v-if="msg.type === 'system'" class="system-msg-box" :msg="msg" />
<template v-else>
<view v-if="!noTime" class="friendlyTime">
......@@ -25,10 +25,10 @@
<cloud-image ref="avatar" width="40px" height="40px" border-radius="5px"
:src="avatarUrl||'/uni_modules/uni-im/static/avatarUrl.png'" mode="widthFix"
:class="{'pointer':canPrivateChat}"
@click="toChat" @longpress.stop="chatInputContentAddcallUser" />
@click.stop="toChat" @longpress.stop="longpressMsgAvatar" />
<view class="msg-main">
<view v-if="!self" class="nickname-box">
<text :selectable="true" class="nickname" @click="chatInputContentAddcallUser">{{ msg.nickname || users.nickname }}</text>
<text :selectable="true" class="nickname" @click="longpressMsgAvatar">{{ msg.nickname || users.nickname }}</text>
<text class="isFromAdmin" v-if="isFromAdmin">管理员</text>
</view>
......@@ -163,7 +163,7 @@
default: false
},
},
emits: ['viewMsg', 'showControl', 'showMsgById','loadMore','chatInputContentAddcallUser','putChatInputContent'],
emits: ['viewMsg', 'showControl', 'showMsgById','loadMore','longpressMsgAvatar','putChatInputContent'],
data() {
let currentUser = {
user_id: uniCloud.getCurrentUserInfo().uid,
......@@ -222,7 +222,7 @@
},
isFromAdmin() {
const conversation = uniIm.conversation.getCached(this.msg.conversation_id)
return conversation.group_id && conversation.group_member[this.msg.from_uid]?.role?.includes('admin')
return conversation?.group_id && conversation.group_member[this.msg.from_uid]?.role?.includes('admin')
},
mineId() {
return uniCloud.getCurrentUserInfo().uid
......@@ -239,8 +239,12 @@
return uni.upx2px(750 / 60 * this.msg.body.time) + 50 + 'px'
},
canPrivateChat(){
// 当前登录的账号是管理员,或者当前消息是群管理员发的
return this.uniIDHasRole('staff') || this.isFromAdmin
const conversation = uniIm.conversation.getCached(this.msg.conversation_id)
const currentUserId = uniCloud.getCurrentUserInfo().uid;
// 当前登录的账号是管理员,或者是群管理员,或者当前消息是群管理员发的
return this.uniIDHasRole('staff') ||
conversation.group_member?.[currentUserId]?.role?.includes('admin') ||
this.isFromAdmin
}
},
async mounted() {
......@@ -261,7 +265,7 @@
if (avatarRef) {
avatarRef.$el.addEventListener('contextmenu', (e) => {
if (uniIm.isWidescreen) {
this.chatInputContentAddcallUser()
this.longpressMsgAvatar()
}
e.preventDefault()
})
......@@ -291,7 +295,6 @@
onDisappear() {},
async showControl(e) {
let msgContentDomInfo;
// #ifndef APP-NVUE
const query = uni.createSelectorQuery().in(this);
await new Promise(callback => {
query.selectAll('.msg-content-box .msg-content').boundingClientRect(data => {
......@@ -300,20 +303,6 @@
callback(msgContentDomInfo)
}).exec();
})
// #endif
// #ifdef APP-NVUE
let ref = this.$refs['msg-content']
await new Promise(callback => {
const dom = weex.requireModule('dom')
console.log('ref', dom);
dom.getComponentRect(ref, e => {
console.log('msgContentDomInfo e.size', e.size);
msgContentDomInfo = e.size
callback(e)
})
})
// #endif
this.$emit('showControl', {
msgId: this.msg._id,
......@@ -345,10 +334,10 @@
})
}
},
chatInputContentAddcallUser() {
longpressMsgAvatar() {
if (this.msg.group_id) {
// console.log('~~~~this.msg.from_uid', this.msg.from_uid)
this.$emit('chatInputContentAddcallUser', this.msg.from_uid)
this.$emit('longpressMsgAvatar', this.msg.from_uid)
}
},
async initAboutMsg() {
......@@ -393,15 +382,8 @@
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
view {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* #endif */
.uni-im-msg {
position: relative;
flex: 1;
/* #ifdef H5 */
width: 100%;
......@@ -416,6 +398,7 @@
padding: 0 8px;
margin: 8px 0;
position: relative;
width: 100%;
}
/* #ifdef H5 */
......@@ -430,26 +413,41 @@
/* #endif */
.msg-main {
flex: 1;
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
width: 80%;
margin: 0 8px;
// overflow: hidden;
}
/* 回复引用某条消息提示框 */
.cite-box {
margin:8px 0;
margin-right: auto;
background-color: #e3e3e3;
color: #6a6a6a;
border-radius: 5px;
max-width: 100%;
justify-content: center;
padding: 5px 10px;
}
.self .cite-box {
margin-right: 0;
margin-left: auto;
}
.cite-box-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
}
.self .msg-main {
align-items: flex-end;
}
.msg-content-box {
margin: 0 8px;
flex-direction: row;
align-items: center;
justify-content: flex-start;
/* #ifdef APP-NVUE */
width: 600rpx;
/* #endif */
/* #ifndef APP-NVUE */
width: calc(95% - 80px);
/* #endif */
}
.self .msg-content-box {
justify-content: flex-end;
......@@ -463,7 +461,7 @@
.nickname {
font-size: 13px;
color: #666666;
padding-left: 9px;
padding-left: 2px;
}
/* #ifdef H5 */
......@@ -527,38 +525,8 @@
/* #endif */
}
/* 回复引用某条消息提示框 */
.cite-box {
margin: 3px 8px;
background-color: #e3e3e3;
color: #6a6a6a;
border-radius: 5px;
/* #ifdef H5 */
max-width: 500rpx;
/* #endif */
/* #ifndef H5 */
width: 500rpx;
/* #endif */
// height: 26px;
justify-content: center;
padding: 5px 10px;
}
.cite-box-text {
/* #ifndef APP-NVUE */
white-space: nowrap;
overflow: hidden;
/* #endif */
lines: 2;
text-overflow: ellipsis;
font-size: 14px;
}
.friendlyTime {
height: 22px;
/* #ifndef APP-NVUE */
// display: block;
/* #endif */
}
.format-time-text {
......
<template>
<view>
<view v-if="soundState" :style="{bottom:markBottom}" class="mark"></view>
<view @touchmove="touchmove" @touchstart="soundStart" @touchend="soundEnd" @touchcancel="soundEnd"
class="sound-buttom" :class="{soundState}">
<view class="sound-buttom-box">
<view @touchmove="touchmove" @touchstart="recordStart" @touchend="recordEnd" @touchcancel="recordEnd"
class="sound-buttom" :class="{recordState}">
<view v-if="soundProgress" class="sound-progress" :style="{'width':soundProgress}"></view>
<text class="sound-text">{{soundState?'录音中('+time+'s)':'按住 说话'}}</text>
<view class="sound-tip" v-if="soundState">
<text class="sound-text">{{recordState?'录音中('+time+'s)':'按住 说话'}}</text>
<view class="sound-tip" v-if="recordState">
<text class="sound-tip-text" :style="{color:cancel?'#f70000':'#FFFFFF'}">{{cancel?'松手取消':'松手发送,上划取消'}}</text>
<view class="closeIcon" :style="{'background-color':cancel?'#f70000':'#EEEEEE'}">
<uni-im-icons code="e61a" size="10px" color="#FFFFFF"></uni-im-icons>
<uni-im-icons class="icon" code="e61a" size="10px" color="#FFFFFF"></uni-im-icons>
</view>
</view>
</view>
<view v-if="recordState" :style="{bottom:markBottom}" class="mark"></view>
</view>
</template>
......@@ -23,10 +23,10 @@
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
let soundInterval,soundPath,startTime;
export default {
emits: ['success'],
emits: ['sendSoundMsg'],
data() {
return {
soundState:0,
recordState:0,
soundProgress:0,
cancel:false,
time:0,
......@@ -36,9 +36,9 @@
computed:{
...uniIm.mapState(['systemInfo']),
markBottom(){
let markBottom = 58;
let markBottom = 67;
// #ifndef H5
markBottom += this.systemInfo.safeAreaInsets.bottom
markBottom += this.systemInfo.safeAreaInsets.bottom/2
// #endif
console.log('markBottom',markBottom);
return markBottom + 'px'
......@@ -47,7 +47,7 @@
created() {
// #ifndef H5
recorderManager.onStop((res)=> {
// console.log('recorderManager.onStop',{res});
console.log('recorderManager.onStop',{res});
if(!this.cancel){
if(this.time < 2){
return uni.showToast({
......@@ -64,9 +64,9 @@
cloudPath:'uni-im/' + uniCloud.getCurrentUserInfo().uid + '/sound/' + Date.now() + '.mp3',
// fileType:"audio",
success: (e) => {
// console.log('uniCloud.uploadFile-success',e,'success',{"url":e.fileID,time:this.time});
// console.log('uniCloud.uploadFile-sendSoundMsg',e,'sendSoundMsg',{"url":e.fileID,time:this.time});
try{
this.$emit('success',{"url":e.fileID,time:this.time})
this.$emit('sendSoundMsg',{"url":e.fileID,time:this.time})
}catch(e){
console.error(e);
}
......@@ -103,21 +103,13 @@
},
methods: {
touchmove(e){
// #ifndef APP-NVUE
let y = e.touches[0].clientY + this.systemInfo.safeArea.top + (this.systemInfo.screenHeight - this.systemInfo.safeArea.bottom)
// #endif
// #ifdef APP-NVUE
let y = e.touches[0].screenY
// #endif
if(this.systemInfo.safeArea.bottom - y > 58){
this.cancel = true
}else{
this.cancel = false
}
let touchY = e.touches[0].clientY + this.systemInfo.statusBarHeight + this.systemInfo.safeArea.top
// #ifdef H5
touchY += 44
// #endif
this.cancel = this.systemInfo.safeArea.bottom - touchY > 66
},
soundStart(e){
recordStart(e){
// 关闭正在播放的sound
uniIm.audioContext.stop()
this.time = 0
......@@ -128,10 +120,10 @@
// #endif
// #ifdef H5
return uni.showToast({
title: 'h5端不支持语音功能',
icon: 'none'
});
// return uni.showToast({
// title: 'h5端不支持语音功能',
// icon: 'none'
// });
// #endif
// #ifndef H5
......@@ -144,10 +136,10 @@
startTime = Date.now()
console.log('soundStart');
console.log('recordStart');
//进度条
this.soundState = 1
this.recordState = 1
soundInterval = setInterval(()=>{
this.soundProgress = parseInt(this.soundProgress) + uni.upx2px(450/60) +'px'
// console.log('this.soundProgress',this.soundProgress);
......@@ -155,14 +147,14 @@
},1000)
// e.preventDefault();
},
soundEnd(){
recordEnd(){
// #ifndef H5
recorderManager.stop();
// #endif
console.log('soundEnd');
console.log('recordEnd');
clearInterval(soundInterval)
setTimeout(()=> {
this.soundState = 0
this.recordState = 0
this.soundProgress = 0
this.cancel = false
}, 300);
......@@ -172,18 +164,19 @@
</script>
<style lang="scss">
.sound-buttom-box {
height: 100%;
}
.sound-buttom {
background-color: #ffffff;
padding: 10px;
width: 450rpx;
height: 46px;
width: 100%;
flex: 1;
// border-radius: 10px;
font-size: 16px;
align-items: flex-start;
justify-content: center;
/* #ifndef APP-NVUE */
overflow:hidden;
/* #endif */
}
.sound-text{
position: relative;
......@@ -196,6 +189,7 @@
position: fixed;
left: 0;
bottom: 110px;
z-index: 9;
width: 750rpx;
text-align: center;
justify-content: center;
......@@ -214,6 +208,13 @@
justify-content: center;
align-items: center;
}
/* #ifdef MP-WEIXIN */
// 纠正微信小程序的icon位置
.closeIcon .icon{
position: relative;
top: -2px;
}
/* #endif */
.sound-progress {
// border-radius: 10px;
height: 44px;
......@@ -226,7 +227,7 @@
opacity: 0.3;
}
.soundState{
.recordState{
background-color: #efefef;
}
......
......@@ -54,7 +54,7 @@ export default {
<style lang="scss" scoped>
.uni-im-view-msg {
position: fixed;
position: absolute;
top: 0;
left: 0;
z-index: 20;
......
{
"id": "uni-im",
"displayName": "uni-im",
"version": "3.0.4",
"version": "3.0.5",
"description": "uni-im是云端一体的、全平台的、免费的、开源即时通讯系统",
"keywords": [
"im,即时通讯,客服,聊天"
......@@ -53,8 +53,7 @@
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
"app-vue": "y"
},
"H5-mobile": {
"Safari": "y",
......
<template>
<!-- 设置导航栏标题,如与谁对话,群人数为几人等 -->
<page-meta v-if="!isWidescreen">
<navigation-bar :title="navTitle" background-color="#f8f8f8" front-color="#000000" />
</page-meta>
<text v-else style="position: absolute;top: 25px;left: 15px;" :selectable="true">{{ navTitle }}</text>
<view class="page">
<view class="head">
<view v-if="count == 0 && loading" class="hint">正在加载……</view>
<view v-else class="hint">{{ count }} 条与“{{ keyword }}”相关的聊天记录</view>
<view
v-if="conversation_id"
class="enter-chat"
@click="onEnterConversation(conversation_id)"
>
<uni-icons type="chatbubble" size="16"></uni-icons>
进入会话
</view>
</view>
<scroll-view
class="message-list"
scroll-y
:scroll-into-view="autoScrollToEl"
@scrolltoupper="onScrollToUpper"
>
<uni-im-msg
v-for="msg in msgList"
:key="msg._id"
:id="`msg-${msg._id}`"
:msg="msg"
no-time
no-jump
@loadMore="cb => cb()"
>
<view class="float-info">
<view>{{ toFriendlyTime(msg) }}</view>
<view class="enter-fragment" @click="onOpenFragment(msg)">查看上下文</view>
</view>
</uni-im-msg>
<view id="bottom-el" style="height: 1px;"></view>
</scroll-view>
<chat-fragment
v-if="fragment"
:entry="fragment"
@close="onCloseFragment"
/>
</view>
</template>
<script>
/**
* chat-filtered 组件,渲染一个会话中经过滤选择的消息列表,用于显示某个会话的消息搜索结果。
*
* 点击某条消息的“查看上下文”按钮可以打开 {@link module:chat-fragment} 组件。
*
* @module
*/
const uniImCo = uniCloud.importObject("uni-im-co", {
customUI: true
})
import uniIm from '@/uni_modules/uni-im/sdk/index.js'
import ChatFragment from './cmp/chat-fragment'
export default {
components: {
ChatFragment,
},
emits: ['to-chat'],
data() {
return {
loading: true,
count: 0,
keyword: '',
msgList: [],
hasMore: true,
skip: Number.MAX_SAFE_INTEGER,
conversation_id: '',
autoScrollToEl: '',
// 当前会话对象
conversation: {},
// 聊天记录片段的入口消息
fragment: null,
};
},
computed: {
...uniIm.mapState(['isWidescreen']),
navTitle() {
let title = this.conversation.title
if (this.conversation.group_id) {
title += `(${Object.keys(this.conversation.group_member).length})`;
}
return title
}
},
async onLoad(param) {
// 调用load方法,因为pc宽屏时本页面是以组件形式展示,如 $refs.chatFiltered.load(conversation_id)
await this.load(param);
},
methods: {
async load({ keyword, count, conversation_id }) {
// 根据入口参数进行初始化
this.loading = true
this.count = count
this.keyword = keyword
this.msgList = []
this.hasMore = true
this.skip = Number.MAX_SAFE_INTEGER
this.conversation_id = conversation_id
this.conversation = await uniIm.conversation.get(conversation_id)
this.autoScrollToEl = ''
this.fragment = null
// 加载第一批匹配的聊天记录
this.loadData(() => {
// 自动滚动到底
this.autoScrollToEl = 'bottom-el'
})
},
async loadData(afterLoaded) {
this.loading = true
let result = await uniImCo.getFilteredMessageOfConversation({
keyword: this.keyword,
skip: this.skip,
conversation_id: this.conversation_id,
})
this.msgList.unshift(...result.data.reverse())
if (this.count < this.msgList.length) {
// 计数以传入的 count 为准,除非实际查询到的更多
this.count = this.msgList.length
}
this.hasMore = result.hasMore
this.skip = result.skip
this.loading = false
this.$nextTick(afterLoaded)
},
onScrollToUpper(evt) {
if (this.loading) return
if (!this.hasMore) return
let elId = 'bottom-el'
if (this.msgList.length > 0) {
elId = 'msg-' + this.msgList[0]._id
}
this.autoScrollToEl = ''
this.loadData(() => {
this.autoScrollToEl = elId
})
},
onEnterConversation(conversation_id) {
this.$emit('to-chat', { conversation_id })
},
onOpenFragment(msg) {
this.fragment = msg
},
onCloseFragment() {
this.fragment = null
},
toFriendlyTime(msg) {
return uniIm.utils.toFriendlyTime(msg.create_time || msg.client_create_time)
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.page {
flex: 1;
height: 100%;
background-color: #efefef;
}
/* #ifdef H5 */
.pc {
// .pc内的元素只有pc端打开才显示,样式在index页面
display: none;
}
/* #endif */
.head {
flex-direction: row;
justify-content: space-between;
height: 30px;
line-height: 30px;
padding: 0px 15px;
font-size: 12px;
border-bottom: 1px solid #ddd;
}
.hint {
color: #999;
}
.enter-chat {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
flex-direction: row;
padding: 0 5px;
}
/* #ifdef H5 */
.enter-chat:hover {
background-color: #ddd;
}
/* #endif */
.message-list {
height: calc(100% - 30px);
}
.uni-im-msg ::v-deep .msg-content {
width: calc(95% - 40px);
}
.float-info {
align-items: flex-end;
position: absolute;
top: 0;
right: 0px;
font-size: 12px;
color: #999;
padding: 10px;
}
.enter-fragment {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
color: #576b95;
}
/* #ifdef H5 */
.enter-fragment:hover {
color: #7c8cae;
}
.uni-im-msg:hover .enter-fragment {
display: block;
}
/* #endif */
.chat-fragment {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: white;
}
</style>
......@@ -858,7 +858,7 @@
name,
size
} = tempFile;
const {fileType} = tempFile
let {fileType} = tempFile
if (!['image', 'video'].includes(fileType)) {
fileType = 'file'
}
......
<template>
<view id="uni-im-chat" class="page" :class="{'pc':isWidescreen}">
<!-- #ifdef H5 -->
<!-- web-pc端会话标题 -->
<text v-if="isWidescreen" id="web-pc-chat-title" :selectable="true">{{navTitle}}</text>
<!-- H5端 左上角显示未读消息数 ,app端使用setTitleNViewButtonBadge设置,小程序端暂未支持-->
<view @click="tapUnreadCount" class="unread_count" v-if="unread_count != 0">
{{ unread_count > 99 ? '99+' : unread_count }}
</view>
<!-- #endif -->
<!-- 消息列表 -->
<uni-im-msg-list :conversationId="conversation.id" ref="msg-list"
@showControl="showControl" @longpressMsgAvatar="onLongpressMsgAvatar"
@retriesSendMsg="retriesSendMsg" class="msg-list"
@clickItem="onclickMsgList" @putChatInputContent="putChatInputContent" :chooseMore="chooseMoreMsg"
:checkedMsgList.sync="checkedMsgList"
></uni-im-msg-list>
<!-- 聊天数据输入框 键盘弹出后要抬高底部内边距 全面屏的安全距离 -->
<text v-if="conversation.leave" class="disable-chat-foot">- 你不是此群成员 -</text>
<text v-else-if="conversation.isMuteAllMembers" class="disable-chat-foot">- 全群禁言禁止输入 -</text>
<view v-else class="chat-foot">
<uni-im-chat-input
ref="chat-input"
v-model="chatInputContent"
:keyboardMaxHeight="keyboardMaxHeight"
:keyboardHeight="keyboardHeight"
@showAboutMenber="showAboutMenber"
@confirm="chatInputConfirm"
@input="onInput"
@sendSoundMsg="sendSoundMsg"
@sendCodeMsg="beforeSendMsg"
>
<template #about-msg>
<view class="answer-msg" v-if="answerMsg !== false">
<text class="answer-msg-text">{{getNicknameByUid(answerMsg.from_uid)}}{{answerMsgNote(answerMsg)}}</text>
<uni-icons class="close-icon" @click="answerMsg = false" type="clear" color="#ccc" size="18px"></uni-icons>
</view>
</template>
</uni-im-chat-input>
</view>
<msg-popup-control ref="msg-popup-control" @answer="setAnswerMsg" @share="shareMsg" @chooseMore="chooseMoreMsg = true;checkedMsgList = $event"></msg-popup-control>
<template v-if="aboutMenberIsShow && (isWidescreen ? memberList.length != 0 : 1 )">
<view class="member-list-mark" @click.stop="aboutMenberIsShow = false"></view>
<view class="member-list-box">
<view class="head">
<view class="close">
<uni-icons @click="aboutMenberIsShow = false" type="back" color="#000" size="12px"></uni-icons>
</view>
<text class="title">选择提醒的人</text>
</view>
<uni-search-bar v-if="!isWidescreen" v-model="aboutUserKeyword" placeholder="搜索" cancelButton="none" class="search" />
<scroll-view class="member-list" :scroll-y="true" :scroll-top="memberListScrollTop" :show-scrollbar="true">
<view v-for="(item,index) in memberList" :key="item._id" class="member-list-item"
:class="{'member-list-item-active':callAboutUid == item.users._id}" @mouseover="callAboutUid = item.users._id"
@click="setCallAboutUid(item.users._id)" :id="'a'+item.users._id">
{{item.users.nickname}}
</view>
<view class="null-about-menber-tip" v-if="memberList.length === 0">没有与"{{aboutUserKeyword}}"相关成员</view>
</scroll-view>
</view>
</template>
<!-- #ifdef H5 -->
<uni-im-share-msg id="uni-im-share-msg" ref="share-msg"></uni-im-share-msg>
<!-- #endif -->
<view style="position: fixed;top: 200px;left: 0;background-color: #FFFFFF;z-index: 9999;">
<!-- aboutMenberIsShow:{{aboutMenberIsShow}}-
keyboardMaxHeight:{{keyboardMaxHeight}}
aboutUserKeyword:{{aboutUserKeyword}}-
conversation.leave:{{conversation.leave}}
chatInputContent:{{chatInputContent}}
keyboardHeight:{{keyboardHeight}}
systemInfo.osName:{{systemInfo.osName}}
chooseMoreMsg:{{chooseMoreMsg}}
checkedMsgList:{{checkedMsgList}} -->
</view>
<view v-if="chooseMoreMsg" class="toolbar" @click="chooseMoreMsg = false">
<view class="item" @click="shareMsg(checkedMsgList)">
<view class="icons-box">
<uni-icons size="35" type="redo"></uni-icons>
</view>
<text class="title">逐条转发</text>
</view>
<view class="item" @click="shareMsg(checkedMsgList,true)">
<view class="icons-box">
<uni-icons size="35" type="paperplane"></uni-icons>
</view>
<text class="title">合并转发</text>
</view>
<view class="item" @click="toolBarNext">
<view class="icons-box">
<uni-icons size="35" type="folder-add"></uni-icons>
</view>
<text class="title">收藏</text>
</view>
<view class="item" @click="toolBarNext">
<view class="icons-box">
<uni-icons size="35" type="download"></uni-icons>
</view>
<text class="title">保存至电脑</text>
</view>
<view class="item" @click="toolBarNext">
<view class="icons-box">
<uni-icons size="35" type="trash"></uni-icons>
</view>
<text class="title">删除</text>
</view>
<uni-icons @click="chooseMoreMsg = false" color="#999" size="35" type="closeempty"></uni-icons>
</view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import msgPopupControl from '@/uni_modules/uni-im/components/uni-im-msg/popup-control.vue';
import {markRaw} from "vue";
import {
store as uniIdStore
} from '@/uni_modules/uni-id-pages/common/store';
let shiftKeyPressed = false;
let lastKeyDown = ""
let currentCursor = ''
/**
* chat 组件,渲染一个完整的会话,包括头部、消息列表、输入区。
*
* @module
*
* @see 用于渲染消息列表的组件 {@link module:uni-im-msg-list}
*/
export default {
components: {
msgPopupControl
},
data() {
return {
// 当前会话对象
conversation: {
id: false,
leave:false,
title: ''
},
//聊天输入框高度
textareaHeight: 26,
navTitle:"",//导航栏标题
keyboardHeight: 0,
keyboardMaxHeight: 0,
answerMsg: false,
callAboutUid: false,
aboutMenberIsShow: false,
inputCursor: '',
// @用户时的搜索词
aboutUserKeyword: '',
memberListScrollTop: 0,
chooseMoreMsg: false,
checkedMsgList: [],
// 聊天输入框内容
chatInputContent: '',
};
},
props: {
// #ifdef VUE3
conversation_id: {
default: ''
}
// #endif
},
computed: {
...uniIm.mapState(['currentConversationId', 'conversationDatas', 'isWidescreen', 'systemInfo']),
unread_count() {
// 所有会话的未读消息数
const unreadCount = uniIm.conversation.unreadCount()
// #ifdef APP-PLUS
// 给标题栏返回按钮添加数字角标,表示有几条其他会话的未读消息数
plus.webview.currentWebview().setTitleNViewButtonBadge({
index: 0,
text: unreadCount
})
// #endif
return unreadCount
},
group_member() {
let group_member = this.conversation.group_member
if (!this.callAboutUid && typeof group_member == 'object') {
this.callAboutUid = Object.keys(this.conversation.group_member)[0]
}
return group_member
},
memberList() {
// 当前输入框已经@了的用户id 要过滤掉
let callUidList = this.getCallUid()
let group_member = this.group_member || {}
let memberList = []
for (let key in group_member) {
let member = group_member[key]
if(member.users.nickname){
let nickname = member.users.nickname.toLowerCase()
if (nickname.indexOf(this.aboutUserKeyword.toLowerCase()) != -1 && !callUidList.includes(member.users._id)) {
memberList.push(member)
}
}
}
if (memberList.length) {
// 按昵称排序
memberList.sort((a, b) => {
return a.users.nickname.localeCompare(b.users.nickname)
})
this.callAboutUid = memberList[0].users._id
return memberList
} else {
// 注意:没有数据时也不需要隐藏aboutMenberIsShow,因为用户可能回删@用户的关键词
return []
}
},
//当前用户自己的uid
current_uid() {
return uniIdStore.userInfo._id;
}
},
created() {
// console.log('chat created',this.systemInfo)
// 监听推送消息
this.onImMsg = (res) => {
if(uniIm.isDisabled){
return console.log('uniIm isDisabled')
}
//获取透传内容
const {
type,
data
} = res.data.payload;
//判断消息类型是否为im,且为当前页面对应会话消息
if (
type == "uni-im"
&& data.conversation_id == this.currentConversationId
&& data.from_uid != this.current_uid
&& uniIm.utils.isReadableMsg(data)
) {
// 已经打开相应会话时,收到消息,则设置此会话为已读。注意无需判断,本地会话未读数是否为0,因为云端可能不为0
this.conversation.clearUnreadCount();
console.log('聊天页面-收到消息: ', JSON.stringify(res));
// 需要重新设置滚动条的高,以至于用户可以立即看到(即:滚动到最后一条消息)
// console.log(66666);
// 注:为兼容web-PC端这里必须使用setTimeout 0
setTimeout(() => {
this.$refs['msg-list']?.notifyNewMsg()
}, 0);
}
}
uniIm.onMsg(this.onImMsg);
// 优化 提前拿到键盘高度,防止第一次在会话点击输入框时,输入框抬起慢。(缺省值300,是为了解决模拟器调试没有键盘高度而设置)
this.keyboardMaxHeight = uniIm.keyboardMaxHeight || 300
// #ifdef H5
//通过监听窗口变化 获取键盘弹出或收起事件
window.addEventListener('resize', () => {
if (this.currentConversationId) {
this.showLast(0);
}
})
// #endif
this.onKeyboardHeightChange = ({
height
}) => {
this.keyboardHeight = height
// console.log('height',height)
if (height > this.keyboardMaxHeight) {
this.keyboardMaxHeight = height
}
this.$nextTick(() => {
this.showLast();
});
}
// #ifndef H5
// 监听键盘高度变化显示最后一条消息
uni.onKeyboardHeightChange(this.onKeyboardHeightChange);
// #endif
// #ifdef H5
const oldWindowHeight = window.innerHeight;
window.onresize = ()=>{
this.onKeyboardHeightChange({"height": oldWindowHeight - window.innerHeight})
}
// #endif
},
mounted() {
// #ifdef H5
// 以下为实现拖拽或粘贴图片至聊天页面,直接发送的逻辑
const chatBodyDom = document.getElementById("uni-im-chat")
// 阻止默认事件
chatBodyDom.addEventListener(
'dragover',
function(event) {
event.preventDefault();
},
false
);
// 拖拽结束时触发
chatBodyDom.addEventListener(
'drop',
e => {
//取消默认浏览器拖拽效果
e.preventDefault();
//获取文件对象
let fileList = e.dataTransfer.files;
if (fileList.length == 0) {
return false;
}
const [file] = fileList
let fileType = file.type.split('/')[0] || 'file'
if (fileType === 'image') {
let blobUrl = window.URL.createObjectURL(file);
if(this.chatInputContent === ''){
this.$refs["chat-input"].setContent(`<img src="${blobUrl}" />`)
}else{
this.$refs["chat-input"].addHtmlToCursor(`<img src="${blobUrl}" />`)
}
return false; // 拖拽图片内容进入输入框先不发送
}
// 其他文件类型,直接发送
let {
name,
size
} = file
// console.log(78979798,fileList);
const blobUrl = window.URL.createObjectURL(file);
this.uploadFileAndSendMsg({
tempFiles:[{
size,
name,
path:blobUrl,
fileType
}]
})
},
false
);
const chatInput = document.querySelector('.pc .uni-im-editor');
if (chatInput) {
//键盘按下时
let oldValue = ''
chatInput.addEventListener('keydown',async e => {
if (this.aboutMenberIsShow) {
if(e.key == 'Enter'){
if(this.memberList.length){
console.log('选中要@的人')
this.setCallAboutUid(this.callAboutUid)
}
}else if(["ArrowUp", "ArrowDown"].includes(e.key)){
console.log('上下箭头选择@谁')
let index = this.memberList.findIndex(i => i.users._id == this.callAboutUid)
// console.log('index',index);
if (e.key == "ArrowUp") {
index--
} else {
index++
}
if (index < 0 || index > this.memberList.length - 1) {
index = 0
}
this.callAboutUid = this.memberList[index].users._id
// 防止选中的成员看不到,触发滚动
this.memberListScrollTop = (index - 3) * 50
// console.log('this.memberListScrollTop',this.memberListScrollTop);
e.preventDefault();
}else if(["ArrowLeft", "ArrowRight"].includes(e.key)){
this.aboutMenberIsShow = false
}else if(e.key == 'Backspace'){
setTimeout(() => {
// 获取e.target 元素内不包含在标签内的文字内容
let newValue = e.target.innerText
console.log('删除键',newValue,oldValue);
// 拿到newValue 和 oldValue 中 包含的@字符的个数
let newAtN = newValue.replace(/[^@]/g, "").length
let oldAtN = oldValue.replace(/[^@]/g, "").length
if(newAtN === 0 || newAtN < oldAtN){
console.log('删除了@成员的昵称');
this.aboutMenberIsShow = false
}
oldValue = newValue
}, 0);
}
}
})
}
// #endif
},
onShow() {
if (this.conversation.id) {
// 用于打开会话窗口后,切换到后台,再切回时设置当前会话id。
uniIm.currentConversationId = this.conversation.id
// 用于从后台切到前台时,设置当前会话为已读
this.clearConversationUnreadCount()
}
},
onUnload() {
// console.log('onUnload');
// 关闭监听消息推送事件
uniIm.offMsg(this.onImMsg);
// #ifndef H5
uni.offKeyboardHeightChange(this.onKeyboardHeightChange)
// #endif
//页面销毁之前销毁 全局变量 正在聊天的用户的id
uniIm.currentConversationId = false
// console.log('beforeDestroy');
// 关闭sound播放
uniIm.audioContext.stop()
},
beforeDestroy() {
//页面销毁之前销毁 全局变量 正在聊天的用户的id
uniIm.currentConversationId = false
// console.log('beforeDestroy');
// 关闭sound播放
uniIm.audioContext.stop()
},
onHide() {
uniIm.currentConversationId = false
// 关闭sound播放
uniIm.audioContext.stop()
},
async onLoad(param) {
for (const key in param) {
try{
param[key] = JSON.parse(param[key])
}catch(_){}
}
//调用load方法,因为pc宽屏时本页面是以组件形式展示。如$refs.chatView.loadconversation_id()执行
await this.load(param);
},
onBackPress(e) {
console.log('onBackPress',e);
if(this.aboutMenberIsShow){
this.aboutMenberIsShow = false
return true
}
},
watch: {
// 监听群昵称变化
'conversation.title':{
handler(){
this.updateNavTitle()
},
deep:true,
immediate:true
},
// 监听群成员数变化
'conversation.group_member':{
handler(){
this.updateNavTitle()
},
deep:true,
immediate:true
}
},
methods: {
async load(param) {
this.answerMsg = false
// conversation_id = "single_eff0518ad35e16a8a025cc8af03e0388"
if(this.conversation.id){
// 设置(含清空)上一个会话的chatInputContent 实现多个会话之间的草稿功能
this.conversation.chatInputContent = this.chatInputContent
}
// console.log('conversation_id',conversation_id);
this.conversation = await uniIm.conversation.get(param)
// 初始化会话的chatInputContent
this.chatInputContent = this.conversation.chatInputContent
// this.conversation.call_list = []
// console.log('this.conversation',this.conversation)
//设置全局的app当前正在聊天的会话id(注:本页面可能是直达页)
uniIm.currentConversationId = this.conversation.id
this.$nextTick(() => {
this.$refs['msg-list'].init()
})
// console.log('this.conversation',this.conversation);
//debug用模拟一次性自动发送100条数据
// for (var i = 0; i < 20; i++) {
// this.chatInputContent = '这是第'+i+'条消息'
// this.beforeSendMsg()
// }*/
// 清除当前会话未读数
this.clearConversationUnreadCount()
// #ifdef H5
if(this.isWidescreen){
// 切换到新的会话后把输入焦点设置到输入框(考虑到可能有草稿文字,需延迟设置)
setTimeout(() => {
this.$refs["chat-input"]?.focus()
}, 100)
}
// #endif
},
clearConversationUnreadCount(){
if(this.conversation.unread_count){
this.conversation.clearUnreadCount();
}
},
onclickMsgList(){
this.$refs["chat-input"]?.setShowMore(false)
uni.hideKeyboard()
},
putChatInputContent(value){
this.$refs["chat-input"]?.setContent(value)
},
getNicknameByUid(uid) {
let users = uniIm.users[uid]
if (users) {
return users.nickname
} else {
return ''
}
},
answerMsgNote(answerMsg) {
return uniIm.utils.getMsgNote(answerMsg)
},
onChatInputFocus() {
// console.log('onChatInputFocus');
this.$refs["chat-input"]?.focus()
},
uploadFileAndSendMsg({tempFiles}){
// console.log(res, 'image');
// console.log('this.uploadFileAndSendMsg res',res);
tempFiles.forEach(async tempFile => {
// console.log('tempFile~',tempFile);
const {
path:url,
name,
size
} = tempFile;
let {fileType} = tempFile
if (!['image', 'video'].includes(fileType)) {
fileType = 'file'
}
// console.log('fileType===>', fileType);
// console.error('tempFile~~~~~~~~~', tempFile,size/1000/1024+'mb');
const sizeMB = size/1000/1024
if(fileType == 'image' && sizeMB > 2){
return uni.showToast({
title: '图片大小不能超过2mb',
icon: 'none'
});
} else if(sizeMB > 100){
return uni.showToast({
title: '文件大小不能超过100mb',
icon: 'none'
});
}
const data = {};
const fileInfo = {
url,
size,
name
};
if(fileType == 'image'){
const {width,height} = await uni.getImageInfo({src:url});
fileInfo.width = width
fileInfo.height = height
}
data[fileType] = fileInfo
let msg = await this.beforeSendMsg(data,false)
// console.log('~~~beforeSendMsg',msg);
try{
const result = await uniCloud.uploadFile({
filePath: tempFile.path,
cloudPath: Date.now() + uniCloud.getCurrentUserInfo().uid + '.' + name.split('.').pop(),
});
// console.log('result.fileID',result.fileID);
msg.body.url = result.fileID
await this.updateMsg(msg)
this.sendMsg(msg)
}catch(e){
console.error('uploadFile error:',e)
}
});
},
async chooseFileSendMsg(type,_config={}) {
// console.log('type',type);
//先创建发送消息的
let objFn = {
'image':()=>{
uni.chooseImage({
// count:9,
crop:{
"quality":100,
"width":800,
"height":800
},
// sourceType,
// extension,
success:res=> beforeUploadFileAndSendMsg(res,'image'),
"fail":alertFail
});
},
'video':()=>{
uni.chooseVideo({
sourceType: ['camera', 'album'],
success:res=> beforeUploadFileAndSendMsg(res,'video'),
"fail":alertFail
});
},
'all':()=>{
let chooseFile = uni.chooseFile;
// #ifdef MP-WEIXIN
chooseFile = wx.chooseMedia;
// #endif
chooseFile({
type: 'all',
// count:10,
sourceType:['album','camera'],
"success":this.uploadFileAndSendMsg,
"fail":alertFail
})
}
};
objFn[type]();
const _this = this;
function beforeUploadFileAndSendMsg(res,fileType){
// console.log(111,res)
// 视频只能选择单文件,为了参数统一,这里转成数组
if(fileType == 'video'){
// #ifndef H5
res.tempFile = {
size: res.size,
width: res.width,
height: res.height
}
// #endif
res.tempFile.path = res.tempFilePath
res.tempFiles = [res.tempFile]
}
res.tempFiles.forEach(item=>{
//如果没有type,默认为:用户选择的类型
if(!item.fileType){
item.fileType = fileType
}
// 如果没有name,默认为:用户id+随机数+时间戳生成一个
if(!item.name){
item.name = _this.current_uid + Math.random().toString(36).substr(2) + Date.now()
}
})
// console.log(222,res)
_this.uploadFileAndSendMsg(res)
}
function alertFail(res){
console.error('res',res);
// uni.showModal({
// content: JSON.stringify(res),
// showCancel: false
// });
}
},
sendSoundMsg(sound){
this.beforeSendMsg({sound})
},
onInput(e) {
// console.log('onInput',e.data);
// 记录按下@之后的内容
const enterText = e.data
if(this.aboutMenberIsShow && enterText && enterText != '@'){
setTimeout(()=>{
// 输入法正在输入中
let isComposing = false
// #ifdef H5
// isComposing = this.isWidescreen ? document.querySelector('#uni-im-chat-input').isComposing : e.isComposing
// #endif
if (isComposing) {
console.log('输入法正在输入中')
}else{
this.aboutUserKeyword += enterText
}
},0)
}else{
this.aboutUserKeyword = ""
}
},
async setAnswerMsg(msgId) {
this.answerMsg = this.conversation.msgList.find(msg => msg._id == msgId)
this.$refs["chat-input"]?.focus()
},
async chatInputConfirm() {
if(this.aboutMenberIsShow && this.memberList.length){
console.log('正在执行选中要@的人,不发送数据')
return
}
if (typeof this.chatInputContent == 'object'){
// 富文本(图文混排、@某人)消息
// 把字符串中带url的链接转为html的a标签的形式。1.将字符串,按是否为标签,切割为数组
const htmlStr = this.chatInputContent.html.split(/(<[^>]+>)/)
// 2.找到不以<开头的字符串内的url并替换为 html的a
.map(str=>str.startsWith('<') ? str : uniIm.utils.replaceUrlToLink(str))
.join('')
// 先插到消息列表
let msg = await this.beforeSendMsg({
"rich-text":uniIm.utils.parseHtml(htmlStr)
},false)
// 上传消息中的图片
let promiseArr = []
msg.body.forEach(async item=>{
if(item.type === 'text'){
item.text = item.text.trim();
}else if(item.name === 'img'){
promiseArr.push(new Promise((resolve,reject)=>{
uni.getImageInfo({
src:item.attrs.src,
success:res=>{
item.attrs.width = res.width
item.attrs.height = res.height
resolve()
},
fail:reject
});
}))
if(item.attrs.src.indexOf('blob:http') === 0){
promiseArr.push(new Promise((resolve,reject)=>{
uniCloud.uploadFile({
filePath: item.attrs.src,
cloudPath: Date.now() + uniCloud.getCurrentUserInfo().uid + '.' + name.split('.').pop(),
}).then(res=>{
item.attrs.src = res.fileID
console.log('上传成功',res);
resolve()
}).catch(e=>{
reject()
})
}))
}
}
})
await Promise.all(promiseArr)
// 传完更新
await this.updateMsg(msg)
// 执行发送
this.sendMsg(msg)
}else{
// 把this.chatInputContent中的&nbsp;变成空格,再把头尾的空格去掉
this.chatInputContent = this.chatInputContent.replace(/&nbsp;/g, ' ').trim()
// 普通消息
await this.beforeSendMsg()
}
},
showAboutMenber(){
if(!this.conversation.group_id){
return // 非群聊会话无需@用户的功能
}
// #ifdef MP
return // 小程序暂不支持@用户的功能
// #endif
uni.hideKeyboard()
this.aboutUserKeyword = '';
console.log('showAboutMenber',this.memberList[0]);
this.callAboutUid = this.memberList[0].users._id
this.aboutMenberIsShow = true
},
async beforeSendMsg(param = {},_continue = true) {
console.log('beforeSendMsg',{param});
let msg = {
type: 'text',
to_uid: this.conversation.friend_uid,
conversation_id: this.conversation.id,
group_id: this.conversation.group_id,
client_create_time: Date.now(),
from_uid: uniCloud.getCurrentUserInfo().uid,
state: 0,
body: this.chatInputContent
}
// 根据传入的参数设置消息类型和内容
for (let key in param) {
if (param[key]) {
msg.type = key
msg.body = JSON.parse(JSON.stringify(param[key]))
}
}
// 如果是文本类型需要做一些处理
if (msg.type === 'text') {
//清除空格
msg.body = msg.body.trim();
// 阻止发送空消息
if (!msg.body.length) {
this.resetChatInput()
return uni.showToast({
title: '不能发送空消息',
icon: 'none'
});
}
}
//如果是回复某一条消息,需要带上相关id
if (this.answerMsg !== false) {
msg.about_msg_id = this.answerMsg._id
}
// 消息列表追加此消息。此时消息状态值为0,表示发送中
let resMsg = this.conversation.msgList.push(msg)
// 代码是另一个输入框,这里不能清空
if(msg.type !== 'code'){
this.resetChatInput()
}
this.$nextTick(() => {
this.showLast()
})
msg.state = 0
// console.error('sendMsg-sendMsg-sendMsg', msg);
// 存到本地数据库
await this.conversation.msgManager.localMsg.add(msg)
// console.log('msg被localMsg.add引用 会新增一个unique_id',msg)
if(_continue){
this.sendMsg(msg);
}else{
return msg;
}
},
resetChatInput() {
this.chatInputContent = ''
this.textareaHeight = 26
// 关闭引用的消息
this.answerMsg = false
},
getCallUid(){
return this.chatInputContent?.aboutUserIds || []
},
sendMsg(msg, callback) {
if(this.conversation.source){
msg.chat_source = this.conversation.source
}
// console.log('sendMsg-sendMsg-sendMsg', msg);
const uniImCo = uniCloud.importObject('uni-im-co', {
customUI: true
});
// 接收消息的appId,默认为当前应用的appId。如果你是2个不同appId的应用相互发,请修改此值为相对的appId
msg.appId = this.systemInfo.appId
// 拿到当前消息的索引值
let index = this.conversation.msgList.findIndex(i => i.unique_id == msg.unique_id)
// 生成新对象,否则不触发更新
msg = Object.assign({}, msg)
// 检查内容不是包含 个推 两个字,有则 改成 个 + 零宽字符 + 推
let tmpBody = JSON.stringify(msg.body)
if(tmpBody.includes('个推')){
msg.body = JSON.parse(tmpBody.replace(/个推/g,'\u200b推'))
}
uniImCo.sendMsg(msg)
.then(async e => {
// console.log('uniImCo.sendMsg',{e,msg});
msg.state = e.errCode === 0 ? 100 : -100;
msg.create_time = e.data.create_time;
msg._id = e.data._id;
await this.updateMsg(msg)
})
.catch(async e => {
uni.showModal({
content: e.message,
showCancel: false,
confirmText: '关闭',
});
console.error('uniImCo.sendMsg error:', e.errCode, e.message);
// 必须要有create_time的值,否则indexDB通过创建时间索引找不到数据
msg.create_time = Date.now();
msg.state = -200;
await this.updateMsg(msg)
})
.finally(e => {
if (callback) {
callback(e);
}
});
},
// 更新消息
async updateMsg(msg){
if(!msg.conversation_id){
throw 'msg.conversation_id不能为空'
}
if(!msg.unique_id){
throw 'msg.unique_id不能为空'
}
let conversation = await uniIm.conversation.get(msg.conversation_id)
let index = conversation.msgList.findIndex(_msg => _msg.unique_id == msg.unique_id)
if(index === -1){
throw 'updateMsg msg 不存在'
}
/* TODO: splice 更新方式会把原本的 msg 对象从数组中踢出,导致已经渲染的消息组件无法响应后续的变更(比如 read_msg),
所以这里使用 merge 更新方式,虽然此方式在 vue2 中有问题。*/
let oldMsg = conversation.msgList[index]
Object.assign(oldMsg, msg)
// conversation.msgList.splice(index, 1, msg)
conversation.msgManager.localMsg.update(msg.unique_id, msg)
// console.log('change after',conversation.msgList[index]);
},
retriesSendMsg(msg) {
uni.showLoading({
mask: true
});
// console.log('retriesSendMsg', msg);
msg.isRetries = true
this.sendMsg(msg, e => {
uni.hideLoading();
});
},
showLast(duration = 300) {
let msgListRef = this.$refs['msg-list']
if (msgListRef) {
msgListRef.showLast(duration)
}
},
onLongpressMsgAvatar(uid){
// 当前输入框已经@了的用户id 要过滤掉
let callUidList = this.getCallUid()
if(callUidList.includes(uid)){
console.log('此用户id已经@过');
uni.showToast({
title: '此用户已经@过',
icon: 'none'
});
}else{
this.$refs['chat-input'].raiseEditor = true
this.$nextTick(()=>{
this.setCallAboutUid(uid,false)
})
}
},
setCallAboutUid(uid,needDeleteLeftART = true) {
this.aboutMenberIsShow = false
this.$refs['chat-input'].addCallUser({
uid,
nickname: this.group_member[uid].users.nickname
},needDeleteLeftART,this.aboutUserKeyword.length)
},
linechange(e) {
//console.log(e.detail);
let {
height,
lineCount
} = e.detail;
// console.log(height,lineCount);
if (lineCount === 1) {
this.textareaHeight = 26;
} else if (height <= 100) {
this.textareaHeight = height - 2;
}
},
async showControl({
msgId,
msgContentDomInfo
}) {
const msg = this.conversation.msgList.find(msg => msg._id === msgId)
let isSelf = msg.from_uid == uniCloud.getCurrentUserInfo().uid
this.$refs['msg-popup-control'].show({isSelf,msg,msgContentDomInfo})
/*
let controlData = {
msg,
isInTop: false
};
let metrics = uniIm.utils.getScreenMetrics()
if (isSelf) {
controlData.left = 'unset'
controlData.right = metrics.pageWidth - msgContentDomInfo.right
} else {
controlData.left = msgContentDomInfo.left + msgContentDomInfo.width / 2
controlData.right = 'unset'
}
controlData.isInTop = msgContentDomInfo.top > 60
if (controlData.isInTop) {
// #ifdef H5
let n = -20
// #endif
// #ifndef H5
let n = -65
// #endif
controlData.top = msgContentDomInfo.top + n
} else {
// #ifdef APP
let n = 8
// #endif
// #ifdef H5
let n = 55
// #endif
// #ifdef MP
let n = 10
// #endif
controlData.top = msgContentDomInfo.bottom + n
}
console.log('msgContentDomInfo',msgContentDomInfo)
controlData.msgWidth = msgContentDomInfo.width
this.$refs['msg-popup-control'].show(controlData)
*/
},
shareMsg(msgList,merge = false) {
this.$refs['share-msg'].open(msgList,merge)
},
toolBarNext(){
uni.showToast({
title: '暂不支持',
icon: 'none',
duration: 2000
});
this.chooseMoreMsg = false
},
clickMenu(index, e) {
// console.log('clickMenu-',index);
if (index < 2) {
this.chooseFileSendMsg(index === 0 ? 'image' : 'video')
}
if (index === 2) {
// #ifdef APP-PLUS
return uni.showToast({
title: '暂不支持,发送文件',
icon: 'none'
});
// #endif
this.chooseFileSendMsg('all')
}
e.stopPropagation()
},
clickEmojiItem(emojiUniCode, e) {
this.chatInputContent += emojiUniCode
e.stopPropagation()
},
tapUnreadCount() {
//点击未读消息文字按钮事件
if (this.isWidescreen) {
// this.$emit('tapUnreadCount') //点击后会话列表自动滚动 置顶 待读项
// console.log('tapUnreadCount');
} else {
uni.navigateBack();
}
},
updateNavTitle(){
this.navTitle = this.conversation.title
if (this.conversation.group_id) {
this.navTitle += '(' + Object.keys(this.conversation.group_member).length + ")";
}
if(this.navTitle && !this.isWidescreen){
uni.setNavigationBarTitle({
title: this.navTitle
});
}
}
},
onNavigationBarButtonTap(e) {
if (e.index === 0) {
if (this.conversation.group_id) {
uni.navigateTo({
url: "/uni_modules/uni-im/pages/group/info?conversation_id=" + this.conversation.id
})
} else {
// console.log(this.conversation,6565);
uni.navigateTo({
url: `/uni_modules/uni-im/pages/chat/info?user_id=${this.conversation.friend_uid}&conversation_id=${this.conversation.id}`
})
// uni.showToast({
// title: '仅群里可见,详细信息',
// icon: 'none'
// });
}
}
// uni.navigateBack();
}
};
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.page {
position: relative;
height: 100vh;
/* #ifdef H5 */
height: calc(100vh - 44px);
/* #endif */
background-color: #efefef;
}
.msg-list {
/* height: 1px; 覆盖掉 组件内的height:100%,使得flex-grow: 1作用在容器内被撑开*/
height: 1px !important;
flex-grow: 1;
}
.chat-foot,.disable-chat-foot{
flex-direction: column;
border-top: 1rpx solid #BBBBBB;
background-color: #F7F7F7;
}
.disable-chat-foot{
padding: 20px;
text-align: center;
justify-content: center;
color: #777777;
}
.answer-msg {
padding: 2px 10px;
background-color: #eee;
border-radius: 3px;
margin-bottom: 10px;
flex-direction: row;
align-items: center;
::v-deep .uni-icons {
margin-left: 5px;
}
/* #ifdef H5 */
.close-icon{
cursor: pointer;
}
@media screen and (min-device-width:960px){
margin: 5px;
margin-bottom: -18px;
top: 0;
}
/* #endif */
.answer-msg-text {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
color: #333;
}
}
/* #ifdef H5 */
.vue-codemirror {
position: fixed;
top: 100px;
left: 50%;
width: 500px;
}
/* #endif */
/* #ifdef H5 */
.chat-foot {
border: none;
}
/* #endif */
/* #ifdef H5 */
.unread_count {
position: absolute;
top: -30px;
left: 70rpx;
z-index: 10;
background-color: #dfe2e9;
padding: 0 14rpx;
height: 14px;
line-height: 14px;
border-radius: 9px;
color: #0c1013;
font-size: 12px;
margin-top: 3px;
}
/* #endif */
.member-list-box {
position: absolute;
width: 750rpx;
height: 80vh;
bottom: 0;
z-index: 10;
background-color: #ffffff;
overflow: hidden;
border-radius: 15px 15px 0 0;
box-shadow: 0 0 100px rgba(0, 0, 0, 0.2);
/* #ifdef H5 */
@media screen and (min-device-width:960px){
border-radius: 15px;
max-height: 300px;
height: auto;
width: 260px;
left: auto;
right: calc(50vw - 300px);
bottom: 300px;
}
/* #endif */
.head{
flex-direction: row;
position: relative;
.close {
position: absolute;
left: 5px;
background-color: #eee;
height: 18px;
width: 18px;
margin: 12px;
transform: rotate(270deg);
border-radius: 50%;
justify-content: center;
align-items: center;
}
.title {
flex: 1;
text-align: center;
margin-top: 10px;
font-size: 14px;
color: #000;
}
}
.search {
::v-deep .uni-searchbar__box {
margin: 0;
height: 30px;
.uni-searchbar__box-icon-search {
padding: 0 5px;
}
.uni-searchbar__text-placeholder {
// font-size: 12px;
}
}
}
.member-list {
height: 100%;
overflow-y: auto;
padding: 10px;
.member-list-item {
overflow: hidden;
height: 40px;
width: 100%;
font-size: 14px;
line-height: 40px;
padding-left: 15px;
border-radius: 10px;
margin-bottom: 10px;
text-align: left;
/* #ifdef H5 */
@media screen and (min-device-width:960px){
margin:0;
margin-bottom: 5px;
cursor: pointer;
}
/* #endif */
}
.member-list-item-active {
background-color: #efefef;
}
.null-about-menber-tip {
color: #666;
font-size: 12px;
align-items: center;
justify-content: center;
height: 100px;
}
}
}
.member-list-mark {
position: fixed;
top: 0;
left: 0;
width: 750rpx;
flex: 1;
width: 100vw;
height: 100vh;
z-index: 9;
background-color: rgba(0, 0, 0, 0.1);
}
.toolbar{
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 260px;
background-color: #ededed;
border-top: solid 1px #ededed;
flex-direction: row;
justify-content: space-around;
align-items: center;
z-index: 9;
}
.toolbar .item {
align-items: center;
}
/* #ifdef H5 */
.toolbar ::v-deep .uni-icons {
cursor: pointer;
}
/* #endif */
.toolbar .icons-box {
background-color: #fff;
justify-content: center;
width: 60px;
height: 60px;
border-radius: 100px;
margin-bottom: 10px;
}
</style>
\ No newline at end of file
<template>
<view class="chat-fragment">
<view class="head">
<uni-icons class="btn-back" @click="onBack" type="arrow-left" size="20"></uni-icons>
</view>
<scroll-view
class="message-list"
scroll-y
:scroll-into-view="autoScrollToEl"
@scrolltoupper="onScrollToUpper"
@scrolltolower="onScrollToLower"
>
<uni-im-msg
v-for="(msg, index) in msgList"
:key="msg._id"
:id="`msg-${msg._id}`"
:msg="msg"
:equalPrevTime="equalPrevTime(index)"
no-jump
:class="{highlight:msg.highlight}"
@loadMore="cb => cb()"
/>
</scroll-view>
</view>
</template>
<script>
/**
* chat-fragment 组件,渲染会话中一个片段的消息列表,用于显示某条消息搜索结果的上下文列表。
*
* @module
*/
const uniImCo = uniCloud.importObject("uni-im-co", {
customUI: true
})
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
emits: ['close'],
props: {
entry: {
type: Object
}
},
data() {
// 因为要修改 msg 对象的属性值,所以 clone 一下,以切断响应性,避免干扰原数据
let { ...cloneEntry } = this.entry
cloneEntry.highlight = true
return {
msgList: [cloneEntry],
hasMoreBackward: true,
skipBackward: this.entry.create_time,
hasMoreForward: true,
skipForward: this.entry.create_time,
autoScrollToEl: '',
};
},
mounted() {
this.loadDataForward(() => {
this.loadDataBackward(() => {
this.autoScrollToEl = 'msg-' + this.entry._id
}, 15)
}, 15)
},
methods: {
async loadDataForward(afterLoaded, limit = 30) {
this.loading = true
let result = await uniImCo.getFragmentMessageOfConversation({
conversation_id: this.entry.conversation_id,
skip: this.skipForward,
limit,
forward: true,
})
this.msgList.push(...result.data)
this.hasMoreForward = result.hasMore
this.skipForward = result.skip
this.loading = false
this.$nextTick(afterLoaded)
},
async loadDataBackward(afterLoaded, limit = 30) {
this.loading = true
let result = await uniImCo.getFragmentMessageOfConversation({
conversation_id: this.entry.conversation_id,
skip: this.skipBackward,
limit,
forward: false,
})
this.msgList.unshift(...result.data.reverse())
this.hasMoreBackward = result.hasMore
this.skipBackward = result.skip
this.loading = false
this.$nextTick(afterLoaded)
},
onScrollToUpper(evt) {
if (this.loading) return
if (!this.hasMoreBackward) return
let elId = 'msg-' + this.msgList[0]._id
this.autoScrollToEl = ''
this.loadDataBackward(() => {
this.autoScrollToEl = elId
})
},
onScrollToLower(evt) {
if (this.loading) return
if (!this.hasMoreForward) return
this.loadDataForward(() => {})
},
onBack() {
this.$emit('close')
},
equalPrevTime(index) {
if (index === 0) {
return false
} else if (index == this.msgList.length - 1) {
return false
} else {
const getFriendlyTime = (msg) => {
return uniIm.utils.toFriendlyTime(msg.create_time || msg.client_create_time)
}
return getFriendlyTime(this.msgList[index]) == getFriendlyTime(this.msgList[index - 1])
}
},
}
}
</script>
<style>
@import "@/uni_modules/uni-im/static/flex.scss";
.chat-fragment {
padding: 5px;
background-color: #efefef;
}
.message-list {
height: calc(100% - 30px);
}
.head {
flex-direction: row;
border-bottom: 1px solid #ddd;
}
.btn-back {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.highlight {
background-color: #f9f3de;
}
</style>
<template>
<view class="container">
<uni-list :border="false" class="list">
<uni-im-info-card
:avatarUrl="friend_info.avatar_file ? friend_info.avatar_file.url:'/uni_modules/uni-im/static/avatarUrl.png'"
:title="friend_info.nickname"
:note="friend_info.nickname != friend_info.email ? friend_info.email : ''"
/>
<uni-list-item
title="消息免打扰"
:switch-checked="conversation.mute"
:show-switch="true"
@switchChange="changeConversationMute"
/>
<!-- #ifdef H5 -->
<!-- 发送名片消息(仅内部人员可用) -->
<uni-list-item
v-if="uniIDHasRole('staff')"
:link="true"
title="发送他(她)的名片"
@click="sendNameCard"
/>
<!-- #endif -->
<!-- 选择某个用户创建群聊(仅dcloud员工可用) -->
<uni-list-item
v-if="uniIDHasRole('staff') && friend_uid != currentUid"
:link="true"
title="选此用户创建群聊"
@click="createGroup"
/>
<template v-for="item in UserinfoMenu" :key="item.component.name">
<component :is="item.component" :conversation="conversation" cementing="UserinfoMenu" />
</template>
</uni-list>
<button
v-if="isFriend"
class="btn"
plain
type="warn"
@click="deteleFriend"
>
删除好友
</button>
<!-- #ifdef H5 -->
<uni-im-share-msg
ref="share-msg"
no-msg-list
no-comment
/>
<!-- #endif -->
</view>
</template>
<script>
const db = uniCloud.database();
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import { ref,markRaw } from "vue"
export default {
data() {
// 调用扩展点,扩展程序可以在消息输入框新增一个工具类的项
let UserinfoMenu = uniIm.extensions
.invokeExts("userinfo-menu-extra",)
.filter((result) => result && result.component)
.map((result) => {
return {
component: markRaw(result.component),
props: result.props
};
});
return {
UserinfoMenu,
conversation: {},
friend_uid: '',
friend_info: {
username: '',
nickname: '',
avatar_file: {
url: ''
}
}
}
},
computed: {
...uniIm.mapState(['isWidescreen']),
isFriend() {
let friendList = uniIm.friend.get()
return friendList.find(i => i._id == this.friend_uid)
},
currentUid() {
return uniCloud.getCurrentUserInfo().uid
}
},
async onLoad(options) {
this.load(options)
},
methods: {
async load(options) {
console.log('options',options);
let conversation_id = options.conversation_id || options.id
// 如果只传了user_id,需要先获取conversation_id
if(!conversation_id){
if(!options.user_id){
console.error('参数错误')
return uni.showToast({
title: '参数错误',
icon: 'none'
});
}
conversation_id = await uniIm.utils.getConversationId(options.user_id)
console.log('conversation_id',conversation_id);
}
let conversation = await uniIm.conversation.get(conversation_id)
this.conversation = conversation
this.friend_uid = conversation.friend_uid
let field = '_id,nickname,avatar_file'
if (this.uniIDHasRole('staff')) {
field += ',email'
}
let res = await db.collection('uni-id-users')
.doc(this.friend_uid)
.field(field)
.get()
// console.log("res: ",res);
this.friend_info = res.result.data[0]
},
changeConversationMute() {
console.log('changeConversationMute',this.conversation)
this.conversation.changeMute()
},
async deteleFriend() {
uni.showModal({
title: '确认要删除好友吗',
content: '此操作不可撤销',
showCancel: true,
cancelText: '取消',
confirmText: '确认',
complete: async (e) => {
if (e.confirm) {
uni.showLoading({
mask: true
});
try {
await db.collection('uni-im-friend').where({
friend_uid: this.friend_uid,
user_id: uniCloud.getCurrentUserInfo().uid
}).remove()
// 收到push消息后store会自动,将此用户从列表中移除
uni.navigateBack({ delta: 2 })
} catch (e) {
uni.showModal({
content: JSON.stringify(e.message),
showCancel: false
});
}
uni.hideLoading()
}
}
});
},
async createGroup(){
console.log('createGroup')
const user_ids = [this.friend_uid]
const uniImCo = uniCloud.importObject("uni-im-co")
let res = await uniImCo.chooseUserIntoGroup({
user_ids
})
uni.$emit('uni-im-toChat','group_'+res.data.group_id)
},
// #ifdef H5
async sendNameCard() {
let msg = {
type: 'userinfo-card',
body: {
user_id: this.conversation.friend_uid,
name: this.conversation.title,
},
from_uid: uniCloud.getCurrentUserInfo().uid,
create_time: Date.now(),
}
this.$refs['share-msg'].open([msg], false)
}
// #endif
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.container {
width: 750rpx;
height: 100vh;
align-items: center;
background-color: #fff;
}
.list {
width: 750rpx;
border-bottom: 1px solid #ececec;
}
/* #ifdef H5 */
.list ::v-deep .info-card {
.title, .note {
cursor: text;
user-select: text;
}
}
.list ::v-deep .uni-list-item + .uni-list-item{
cursor: pointer;
}
/* #endif */
.btn {
margin-top: 15px;
width: 600rpx;
/* height: 45px; */
text-align: center;
line-height: 45px;
border-radius: 20rpx;
}
</style>
\ No newline at end of file
<template>
<view v-if="url" class="video-box" :class="{'is-float-mode':mode == 'float'}">
<view class="mask" v-if="mode == 'float'"></view>
<video @click="showCloseBtnFn" :src="url" :autoplay="true" :page-gesture="true" :show-mute-btn="true" :show-fullscreen-btn="mode == 'float'" class="video"></video>
<uni-icons v-if="showCloseBtn" @click="close" size="30px" color="#FFFFFF" type="closeempty" class="close-icon"></uni-icons>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
data() {
return {
url:"",
showCloseBtn:true,
// 全屏模式和小窗模式,fullscreen为全屏模式,float为小窗模式
mode: 'fullscreen',
};
},
onLoad({url}) {
// console.log({url});
this.url = url
setTimeout(()=> {
this.showCloseBtn = false
}, 1000);
},
mounted(){
// console.log('mounted');
uni.$on('uni-im-playVideo',(url)=>{
this.mode = 'float'
this.url = url
this.showCloseBtn = true
})
// 监听esc按键,关闭视频
// #ifdef H5
uniIm.utils.appEvent.onKeyDown(evt => this.onDownEscapeKey, {
order: 1000,
match: {
key: 'Escape',
altKey: false,
ctrlKey: false,
shiftKey: false,
}
})
// #endif
},
beforeDestroy(){
// #ifdef H5
uniIm.utils.appEvent.offKeyDown(this.onDownEscapeKey)
// #endif
},
methods:{
onDownEscapeKey() {
if (this.url.length) {
this.close()
}
return true
},
close(){
if(this.mode == 'fullscreen'){
uni.navigateBack()
}else{
this.url = ''
}
},
showCloseBtnFn(){
// console.log('showCloseBtnFn');
this.showCloseBtn = true
if(this.mode == 'fullscreen'){
setTimeout(()=> {
this.showCloseBtn = false
}, 5000);
}
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
page {
height: 100%;
}
.video-box,.video{
width: 100vw;
height: 100%;
}
.close-icon{
position: absolute;
top: 80rpx;
left: 30rpx;
z-index: 10;
text-shadow: 0 0 15px black;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.is-float-mode,
.is-float-mode .video{
position: fixed;
top: 10vh;
left: calc(10vw + 250px);
width: calc(80vw - 220px) !important;
height: 80vh !important;
z-index: 9;
}
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
</style>
<template>
<view class="code-view">
<!-- 默认不启用代码浏览模块,如有需要请关闭注释 -->
<uni-im-code-view :msg="msg" :showFullBtn="false"></uni-im-code-view>
</view>
</template>
<script>
import uniImCodeView from '@/uni_modules/uni-im/components/uni-im-msg/types/code.vue'
export default {
components: {
uniImCodeView
},
data() {
return {
msg: {
type: "code",
body: ""
}
}
},
onLoad({
code
}) {
console.log(code)
this.msg.body = JSON.parse(decodeURIComponent(code));
},
methods: {}
}
</script>
<style lang="scss">
@import "@/uni_modules/uni-im/static/flex.scss";
.text-box,
.code-view {
width: 750rpx;
height: 100vh;
}
</style>
\ No newline at end of file
<template>
<view>
<uni-nav-bar color="#999" :fixed="true" background-color="#ffffff" status-bar left-icon="left" @clickLeft="back">
<view class="segmented-box">
<uni-segmented-control :current="current" :values="items" @clickItem="setActiveIndex" styleType="button" activeColor="#5fc08e" style="width:120px;"></uni-segmented-control>
</view>
</uni-nav-bar>
<view class="content">
<uni-search-bar :placeholder="activeIndex?'搜索群名称/群号':'搜索手机号/用户名/用户昵称'" :radius="100"
class="search-bar"
bgColor="#eeeeee"
v-model="keyword"
@confirm="doSearch"
@focus="searchFocus = true"
@blur="searchFocus = false"
@cancel="doClear"
@clear="doClear"
></uni-search-bar>
<view v-if="activeIndex === 0">
<!-- 搜索 -->
<view v-if="usersList.length">
<uni-im-info-card v-for="(item,index) in usersList" :key="index"
:title="item.nickname" :avatarCircle="true"
:avatar="item.avatar_file&&item.avatar_file.url ? item.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'"
>
<text v-if="item.isFriend" class="chat-custom-right grey">已添加</text>
<text v-else @click="addUser(index)" class="chat-custom-right">加为好友</text>
</uni-im-info-card>
<!--
v-if="keyword.length"
<template v-else>
<uni-list-item v-for="(tab,index) in tabs" :key="index"
class="tab-item" :title="tab.title"
:to="tab.url" showArrow :border="false"
></uni-list-item>
</template> -->
</view>
<uni-im-load-state v-else :status="loading?'loading':(hasMore?'hasMore':'noMore')"></uni-im-load-state>
</view>
<view v-if="activeIndex === 1">
<view v-if="groupList.length">
<uni-im-info-card v-for="(item,index) in groupList" :key="index"
:title="item.name"
:avatar="item.avatar_file && item.avatar_file.url ? item.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'"
>
<text v-if="item.isExist" class="chat-custom-right grey">已加入</text>
<text v-else @click="addUser(index)" class="chat-custom-right">申请加入</text>
</uni-im-info-card>
</view>
<uni-im-load-state v-else :status="loading?'loading':(hasMore?'hasMore':'noMore')"></uni-im-load-state>
</view>
</view>
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog mode="input" :title="activeIndex?'申请加群':'申请添加好友'"
placeholder="请输入验证信息" confirmText="发送" message="成功消息"
:duration="2000" :before-close="true" :value="value"
@close="close" @confirm="confirm"
></uni-popup-dialog>
</uni-popup>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const db = uniCloud.database();
export default {
data() {
return {
current:0,
loading:true,
hasMore: false,
activeIndex:0,
value:'',
items: ['找人', '找群'],
searchFocus:false,//是否展示搜索列表
keyword:'',
tabs:[
{
'title':'添加手机联系人',
'url':''
},
{
'title':'扫一扫加好友',
'url':''
},
{
'title':'查找陌生人',
'url':''
}
],
usersData: [],
checkIndex:'',//申请加的群index
groupData:[]
}
},
computed: {
usersList() {
let current_uid = uniCloud.getCurrentUserInfo().uid
let friendList = uniIm.friend.dataList
return this.usersData.map(item => {
const isFriend = friendList.find(i=>i._id == item._id)
return {
...item,
isFriend
}
})
},
groupList() {
let groupList = uniIm.group.dataList
console.log('已经加入的groupList',groupList);
console.log('查到的groupList',this.groupData);
// return this.groupData.filter(item=> groupList.find(i=>i._id == item._id))
return this.groupData.map(item => {
const isExist = groupList.find(i=>i._id == item._id)
return {
...item,
isExist
}
})
}
},
onLoad(param) {
this.setParam(param)
},
methods: {
setParam(param){
console.log("param: ",param);
if(param.group_id){
this.current = 1
this.setActiveIndex({currentIndex: 1})
this.keyword = param.group_id
return this.doSearch()
}
this.getUserList()
this.getGroupsList()
},
async getGroupsList(){
const limit = 1000
const skip = this.groupData.length/limit + 1
const res = await db.collection('uni-im-group')
.where(`"user_id" != "${uniCloud.getCurrentUserInfo().uid}"`)
.field('_id,name,avatar_file')
.orderBy('create_date', 'desc')
.limit(limit)
.skip(skip)
.get()
// console.log("uni-im-group: ",res);
if(res.result.data.length){
this.loading = false
this.hasMore = true
this.groupData = res.result.data
}
},
async getUserList(){
try{
let res = await db.collection('uni-id-users')
.field('_id,nickname,avatar_file')
.get()
let data = res.result.data
// console.log("data: ",data);
if(data.length){
this.loading = false
this.hasMore = true
this.usersData = data
}
}catch(e){
console.log(e);
}
},
back() {
uni.navigateBack()
},
async doSearch(e){
console.log("doSearch: ",e,this.keyword);
uni.showLoading({
title: '搜索中'
})
if(this.activeIndex){
let res = await db.collection('uni-im-group')
.where(`
/${this.keyword}/.test(name) ||
"_id" == "${this.keyword}"
`)
.get()
console.log(res);
this.groupData = res.result.data
}else{
const whereString = [
"_id",
"username",
"nickname",
"email",
"mobile"
].map(item => `"${item}" == "${this.keyword}"`).join(' || ')
// console.log('whereString',whereString);
let res = await db.collection('uni-id-users')
.where(whereString)
.field('_id,nickname,avatar_file')
.get()
// tip:用户表数据少,或者已做好优化,可以使用:/${this.keyword}/.test(nickname) 模糊匹配用户昵称
console.log(res);
this.usersData = res.result.data
}
uni.hideLoading()
},
doClear() {
if(this.keyword){
this.keyword = ''
this.usersData = []
this.groupData = []
this.getUserList()
this.getGroupsList()
}
},
setActiveIndex(e) {
// console.log("activeIndex: ",e);
if (this.activeIndex != e.currentIndex) {
this.activeIndex = e.currentIndex;
}
},
addUser(index){
this.checkIndex = index
this.$refs.popup.open()
},
async confirm(value){
// if(!value){
// uni.showToast({
// title: '验证信息不能为空!',
// icon:'none'
// });
// return
// }
// console.log('提供的验证信息',value);
this.value = value
this.$refs.popup.close()
if(this.activeIndex === 0){
//添加好友
const uniImCo = uniCloud.importObject("uni-im-co")
await uniImCo.addFriendInvite({
"to_uid": this.usersList[this.checkIndex]._id,
"message": this.value
}).then((res)=>{
console.log("res: ",res);
uni.showToast({
title: '已申请',
icon: 'none'
});
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
})
}else{
// console.log('1233123132123132',this.groupData,this.checkIndex);
//申请加群
db.collection('uni-im-group-join').add({
"group_id":this.groupList[this.checkIndex]._id,
"message":this.value
}).then((res) => {
console.log("res: ",res);
uni.showToast({
icon: 'none',
title: '已申请'
})
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
})
}
setTimeout(()=> {
this.value = ''
}, 100);
},
close(){
console.log('取消了');
this.$refs.popup.close()
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.segmented-box{
flex: 1;
justify-content: center;
align-items: center;
}
.tab-item{
border-bottom: #f5f5f5 solid 1px;
height:60px;
justify-content: center;
padding: 0 15rpx;
}
.background{
background-color: #f5f5f5;
}
.grey{
color: #ddd;
}
.chat-custom-right {
width:70px;
height:30px;
line-height: 30px;
color: #666;
font-size: 12px;
text-align: center;
background-color: #efefef;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
border-radius: 100px;
}
.border{
border: #ddd solid 1px;
}
.state-text{
text-align: center;
font-size: 28rpx;
}
/* #ifdef H5 */
@media screen and (min-device-width:960px){
.content {
margin-top: 0;
height: calc(100vh - 175px);
overflow: auto;
}
::v-deep .uni-navbar__header-btns-left,
::v-deep .uni-navbar__placeholder,
{
display: none;
}
::v-deep .uni-navbar--fixed{
position: relative;
left: 0
}
}
/* #endif */
</style>
<template>
<view @click="hiddenDeleteBtn" class="contacts-pages">
<uni-list :border="false" class="menu-list-box" v-if="showMenu">
<uni-list-item v-for="(menu,menuIndex) in menuList" :key="menuIndex" :title="menu.title" link
@click="openPages(menu)" :showBadge="true" :class="{activeMenu:isOpenItemTitle === menu.title}">
<template v-slot:header>
<view class="slot-icon-box green">
<image class="slot-icon" :src="'/uni_modules/uni-im/static/noticeIcon/' + menu.srcName + '.png'"
mode="widthFix"></image>
</view>
</template>
</uni-list-item>
<uni-list-item v-for="(item,index) in noticeList" :key="item.id" :title="item.title" :showBadge="true"
:badgeText="item.badge" :badgeStyle="item.badgeStyle" link @click="openPages(item)" :border="false"
:class="{activeMenu:isOpenItemTitle === item.title}">
<template v-slot:header>
<view class="slot-icon-box blue">
<image class="slot-icon" :src="item.icon" mode="widthFix"></image>
</view>
</template>
</uni-list-item>
</uni-list>
<text class="title">好友列表</text>
<uni-list v-if="showUser" :border="false" class="user-list-box" :scroll-y="true">
<uni-list-item v-for="(item, index) in friendList" :key="item._id" :customStyle="{padding:0}"
class="user-list-item">
<template v-slot:body>
<scroll-view scroll-x="true" @scroll="scroll" :scroll-left="activeIndex === index ?'':scrollLeft[index]"
:show-scrollbar="false" :scroll-with-animation="true" class="user-list-item-scroll-view">
<view class="user-list-item-scroll-view-item" @click="toChat(item)"
@touchstart.passive="activeIndex = index">
<image class="avatar"
:src="item.avatar_file&&item.avatar_file.url ? item.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'"
mode="widthFix"></image>
<text class="username">{{item.nickname}}</text>
<button @click.stop="deleteItem(item,index,$event)" class="delete-btn" size="mini"
type="warn">删除</button>
</view>
</scroll-view>
</template>
</uni-list-item>
<uni-list-item :customStyle="{padding:0,backgroundColor:'#FFFFFF'}">
<template v-slot:body>
<uni-im-load-state :status="friendHasMore?'loading':'noMore'"></uni-im-load-state>
</template>
</uni-list-item>
</uni-list>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const db = uniCloud.database()
export default {
emits: ['clickMenu'],
props: {
// pc端时会控制隐藏
showMenu: {
type: Boolean,
default: true
},
// pc端时会控制隐藏
showUser: {
type: Boolean,
default: true
},
},
data() {
return {
isOpenItemTitle: '',
scrollLeft: {
0: 0,
1: 1
},
activeIndex: false,
menuList: [{
title: '加人/加群',
path: './addPeopleGroups/addPeopleGroups',
srcName: 'search'
},
{
title: '群聊列表',
path: './groupList/groupList',
srcName: 'group'
},
{
title: '创建群聊',
path: './createGroup/createGroup',
srcName: 'createGroup'
}
]
}
},
computed: {
//是否为pc宽屏(width>960px)
isWidescreen() {
return uniIm.isWidescreen
},
friendList() {
return uniIm.friend.dataList
},
friendHasMore() {
return uniIm.friend.hasMore
},
noticeList() {
return [{
title: "新朋友",
param: {
type: ['uni-im-friend-invite']
},
icon: "/uni_modules/uni-im/static/noticeIcon/newFriend.png"
},
{
title: "群通知",
param: {
type: ['uni-im-group-join-request']
},
icon: "/uni_modules/uni-im/static/noticeIcon/groupNotice.png"
},
{
title: "系统通知",
param: {
excludeType: ['uni-im-group-join-request', 'uni-im-friend-invite']
},
icon: "/uni_modules/uni-im/static/noticeIcon/notification.png"
}
].reduce((sum, item, index) => {
let {
param: filterNotice,
title
} = item,
param = {
filterNotice,
title
}
// console.log('param----',param);
sum.push({
title: item.title,
badge: this.getUnreadCount(item.param),
badgeStyle: {
backgroundColor: '#d60000'
},
path: "./notification/notification?param=" + encodeURIComponent(JSON.stringify(param)),
param,
icon: item.icon,
id: Date.now() + '-' + index
})
return sum
}, [])
}
},
onPullDownRefresh() {
this.$refs.udb.loadData({
clear: true
}, () => {
uni.stopPullDownRefresh()
})
},
onReachBottom() {
// this.$refs.udb.loadMore()
},
mounted() {},
methods: {
openPages(item) {
this.isOpenItemTitle = item.title
// #ifdef H5
if (this.isWidescreen) {
let componentName = 'uni-im-' + item.path.split('/')[1],
param = item.param
return this.$emit('clickMenu', {
componentName,
param,
title: item.title
})
}
// #endif
// console.log('item',item);
uni.navigateTo({
url: item.path,
fail: (e) => {
console.error(e, item.path);
}
})
},
getUnreadCount(param) {
return uniIm.notification.unreadCount(param)
},
toChat(item) {
uniIm.toChat({user_id: item._id})
},
hiddenDeleteBtn() {
this.activeIndex = false
this.$nextTick(() => {
for (let i in this.scrollLeft) {
this.$set(this.scrollLeft, i, 0)
// this.scrollLeft[i] = 0
}
})
},
async deleteItem(item, index, event) {
uni.showModal({
title: '确认要删除好友吗',
content: '此操作不可撤销',
showCancel: true,
cancelText: '取消',
confirmText: '确认',
complete: async (e) => {
if (e.confirm) {
uni.showLoading({
mask: true
});
await db.collection('uni-im-friend').where({
friend_uid: item._id,
user_id: uniCloud.getCurrentUserInfo().uid
}).remove()
uni.hideLoading()
// 收到push消息后会自动,将此用户从列表中移除
}
}
});
this.hiddenDeleteBtn()
event.stopPropagation()
event.preventDefault()
},
scroll(e) {
// console.log(this.inMove);
this.$set(this.scrollLeft, this.activeIndex, e.detail.scrollLeft)
// this.scrollLeft[this.activeIndex] = e.detail.scrollLeft
for (let i in this.scrollLeft) {
if (i != this.activeIndex) {
this.$set(this.scrollLeft, i, 0)
// this.scrollLeft[i] = 0
}
}
},
handleItemClick(id) {
uni.navigateTo({
url: './detail?id=' + id
})
},
fabClick() {
// 打开新增页面
uni.navigateTo({
url: './add',
events: {
// 监听新增数据成功后, 刷新当前页面数据
refreshData: () => {
this.$refs.udb.loadData({
clear: true
})
}
}
})
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.contacts-pages {
flex: 1;
position: relative;
}
/* #ifdef H5 */
@media screen and (min-device-width:960px) {
.contacts-pages {
height: 100%;
}
.user-list-box {
overflow: auto;
}
}
/* #endif */
.title {
padding: 8px;
font-size: 14px;
}
.user-list-box {
flex: 1;
}
.user-list-item {
padding: 0;
}
.user-list-item-scroll-view {
width: 750rpx;
background-color: #ffffff;
}
.user-list-item-scroll-view-item {
width: 880rpx;
position: relative;
height: 60px;
align-items: center;
padding: 8px 15px;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
}
.avatar {
background-color: #fefefe;
width: 40px;
height: 40px;
border-radius: 5px;
}
.username {
line-height: 30px;
margin-left: 30rpx;
font-size: 16px;
}
.delete-btn {
border-radius: 0;
position: absolute;
right: 0;
top: 0;
height: 60px;
line-height: 60px;
width: 130rpx;
font-size: 26rpx;
padding: 0;
}
.slot-icon-box {
width: 45px;
height: 45px;
align-items: center;
justify-content: center;
border-radius: 10rpx;
margin-right: 20rpx;
}
.slot-icon {
width: 25px;
height: 25px;
}
.warn {
background-color: #FA9E3B;
}
.green {
background-color: #08C060;
}
.blue {
background-color: #5DBAFF;
}
@media screen and (min-device-width:960px) {
.activeMenu {
background-color: #f5f5f5 !important;
}
}
</style>
\ No newline at end of file
<template>
<view class="create-group-box" :class="{'join-grpop':group_id}">
<view class="header-box">
<uni-search-bar v-model="keyword" placeholder="搜索" bgColor="#fff" :radius="100"
@cancel="doClear();isFocus = true" @clear="doClear" :isFocus="isFocus" @focus="isFocus = true" @blur="isFocus = false" ></uni-search-bar>
</view>
<uni-list class="content-box">
<uni-im-info-card @click="checkboxChange(item._id)" v-for="(item,index) in friendList" :key="index" :avatar-circle="true" :title="item.nickname" :border="false" :clickable="true"
:avatar="item.avatar_file && item.avatar_file.url ? item.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'">
<template v-slot:left>
<view class="checkbox">
<uni-icons type="checkmarkempty" color="#007aff" v-if="checkFriendIds.includes(item._id)"></uni-icons>
</view>
</template>
</uni-im-info-card>
<uni-im-load-state :status="loading?'loading':(hasMore?'hasMore':'noMore')"
:contentText='{"contentnomore": friendList.length?"没有更多好友":"没有可以选择的好友"}'></uni-im-load-state>
<uni-list-item style="height: 60px;" :border="false">
<!-- 占位,用于此元素上方显示 操作按钮 -->
</uni-list-item>
</uni-list>
<view class="foot-box">
<!-- 创建新群可以不选择好友直接创建,邀请好友进群必须选择好友 -->
<button :disabled="group_id? !checkFriendIds.length : false " class="btn" type="primary"
@click="createGroup">{{btnText}}{{checkFriendNum}}</button>
</view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const db = uniCloud.database();
export default {
data() {
return {
loading: true,
hasMore: false,
keyword: '',
checkFriendIds: [],
friendData: [],
groupMemberUid: [], //选人进群时,已经在群里的人的id
group_id: false,
isFocus: false
}
},
computed: {
friendList() {
return this.friendData.filter(item => {
//转小写筛选
return !this.groupMemberUid.includes(item._id)
&&
(this.keyword == '' || item.nickname.toLowerCase().includes(this.keyword.toLowerCase()))
})
},
checkFriendNum() {
return this.checkFriendIds.length > 0 ? '' + this.checkFriendIds.length + '' : ''
},
btnText() {
return this.group_id ? '立即邀请' : '立即创建'
},
checkFriendsWidth() {
return this.checkFriendIds.length > 6 ? '100%' : this.checkFriendIds.length * 80 + 'px'
},
// checkFriendsSearchWidth() {
// return this.checkFriendIds.length > 6 ? '360' : 720 - (this.checkFriendIds.length * 60)
// },
translateXWidth() {
return this.checkFriendIds.length > 6 ? this.checkFriendIds.length * 65 : '60'
},
checkFriendImg() {
return this.friendList.reduce((sum, current) => {
if (this.checkFriendIds.includes(current._id)) {
sum.push(current)
}
return sum
}, []).map(item => item.avatar_file)
}
},
async onLoad(options) {
this.setParam(options)
},
methods: {
async setParam(options = {}) {
console.log("group_id", options);
if (options.group_id) {
this.group_id = options.group_id
uni.setNavigationBarTitle({
title: '邀请新成员'
})
//查本群,成员,
let res = await db.collection('uni-im-group-member').where({
group_id: options.group_id
})
.get()
console.log("res:查本群,成员 ", res);
this.groupMemberUid = res.result.data.map(item => item.user_id)
console.log('this.groupMemberUid', this.groupMemberUid);
}
this.getFriendsData()
},
async getFriendsData() {
let whereString = {}
if (this.keyword) {
whereString = `
"_id" == "${this.keyword}" ||
"username" == "${this.keyword}" ||
"nickname" == "${this.keyword}" ||
"email" == "${this.keyword}" ||
"mobile" == "${this.keyword}"
`
}
let res = await db.collection(
db.collection('uni-im-friend').where('"user_id" == $cloudEnv_uid').field('friend_uid,mark,class_name')
.getTemp(),
db.collection('uni-id-users').where(whereString).field('_id,nickname,avatar_file').getTemp()
).get()
// console.log(res);
let data = res.result.data
data.forEach((item, index) => {
if (item.friend_uid[0]) {
data[index] = item.friend_uid[0]
} else {
delete data[index]
}
})
this.friendData = data
this.loading = false
this.hasMore = this.friendList.length != 0
},
doClear() {
this.keyword = ''
this.getFriendsData()
},
checkboxChange(user_id) {
console.log("checkboxChange-value",user_id);
this.checkFriendIds = this.checkFriendIds.includes(user_id) ? this.checkFriendIds.filter(item => item != user_id) : this.checkFriendIds.concat(user_id)
console.log("checkboxChange",this.checkFriendIds);
},
async createGroup() {
// console.log('创建', this.checkFriendIds.length)
const uniImCo = uniCloud.importObject("uni-im-co")
let res = await uniImCo.chooseUserIntoGroup({
user_ids: this.checkFriendIds,
group_id: this.group_id
})
this.checkFriendIds = []
// console.log('createGroup',res);
if (this.group_id) {
uni.navigateBack({
delta: 1
})
} else {
// #ifdef H5
if (uniIm.isWidescreen) {
uni.$emit('uni-im-toChat', 'group_' + res.data.group_id)
} else {
uni.redirectTo({
url: '/uni_modules/uni-im/pages/chat/chat?conversation_id=' + 'group_' + res.data.group_id,
animationDuration: 300,
fail: (e) => {
console.log(e);
}
})
}
// #endif
// #ifndef H5
uni.redirectTo({
url: '/uni_modules/uni-im/pages/chat/chat?conversation_id=' + 'group_' + res.data.group_id,
animationDuration: 300,
complete: (e) => {
console.log(e);
}
})
// #endif
}
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.create-group-box {
position: relative;
background-color: #F9F9F9;
flex: 1;
}
.header-box {
flex-direction: column;
background-color: #F9F9F9;
}
.content-box {
width: 100%;
overflow: auto;
}
.label-box {
flex-direction: row;
align-items: center;
background-color: #FFF;
padding: 5px 0;
}
.checkbox {
margin: 12px 10px 0 0;
border: 1px solid #DDD;
width: 20px;
height: 20px;
justify-content: center;
align-items: center;
border-radius: 3px;
}
.foot-box {
position: fixed;
bottom: 0;
// 注意:此页面可能显示在pc端所以不能用750rpx,而用100%
width: 100%;
height: 60px;
justify-content: center;
align-items: center;
}
/* #ifdef H5 */
@media screen and (min-device-width:960px){
.create-group-box {
width: 100%;
margin: 10px auto;
}
.join-grpop {
width: 800px;
}
.content-box {
height: calc(95vh - 200px);
}
.content-box ::v-deep .info-card {
cursor: pointer;
}
.foot-box {
position: absolute;
}
}
/* #endif */
.foot-box .btn {
width: 300px;
}
</style>
\ No newline at end of file
<template>
<view>
<uni-search-bar placeholder="搜索群号/群名称" :radius="100" bgColor="#eeeeee" v-model="keyword"
@cancel="doClear"
@clear="doClear"
></uni-search-bar>
<view class="uni-list">
<uni-im-info-card v-for="(item,index) in groupList" :key="index"
@click="toChat(item.group_info._id)" link
:title="item.group_info.name"
:avatar="item.group_info.avatar_file&&item.group_info.avatar_file.url ? item.group_info.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'">
</uni-im-info-card>
<uni-im-load-state :status="groupHasMore?'loading':'noMore'"></uni-im-load-state>
</view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
data() {
return {
keyword: '',
groupData:false
}
},
computed: {
//是否为pc宽屏(width>960px)
isWidescreen(){
return uniIm.isWidescreen
},
groupList() {
let groupList = uniIm.group.get()
if(this.keyword){
return groupList.filter(item=>{
return item.group_info.name.includes(this.keyword) || item.group_info._id.includes(this.keyword)
})
}else{
return groupList
}
},
groupHasMore(){
return uniIm.group.hasMore
}
},
async onLoad(options) {
this.setParam(options)
},
methods: {
setParam(param = {}){
if(param.group_id){
this.keyword = param.group_id
}
},
doClear(){
this.keyword = ''
},
toChat(group_id) {
let conversation_id = 'group_' + group_id
uniIm.toChat({conversation_id})
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
/* #ifdef H5 */
@media screen and (min-device-width:960px){
.uni-list {
height: calc(100vh - 185px);
overflow: auto;
}
}
/* #endif */
</style>
<template>
<view class="notification-box">
<text class="tips" v-if="tips">{{tips}}</text>
<uni-list :border="false">
<template v-if="notificationDatas && notificationDatas.length">
<uni-im-info-card v-for="(item,index) in notificationDatas" :key="item.id" :avatarCircle="true"
:clickable="true" :badge-text="item.is_read?'':'dot'" badgePositon="left"
:title="item.payload.title||item.title" :note="item.payload.content||item.content||'无'"
:avatar="item.payload.avatar_file&&item.payload.avatar_file.url ? item.payload.avatar_file.url : '/uni_modules/uni-im/static/noticeIcon/notification2.png'"
@click.native="clickHandle(index,item)" direction="column" :time="friendlyTime(item.create_time)">
<view class="handle-box">
<template v-if="item.payload.state">
<text class="handle done">
{{''+(item.payload.state == 'confirm'?item.payload.confirmText:item.payload.cancelText)}}
</text>
</template>
<template v-else>
<text class="handle" @click.stop="doAction(index,0)"
v-if="item.payload.cancelText">{{item.payload.cancelText}}</text>
<text class="handle" @click.stop="doAction(index,1)"
v-if="item.payload.confirmText">{{item.payload.confirmText}}</text>
<uni-icons v-if="!item.payload.cancelText && !item.payload.confirmText && item.path" type="right"
color="#cccccc"></uni-icons>
</template>
</view>
</uni-im-info-card>
</template>
<uni-list-item v-else class="load-more">
<template v-slot:body>
<uni-im-load-state :contentText="contentText" class="tip" :status="hasMore?'loading':'noMore'"></uni-im-load-state>
</template>
</uni-list-item>
</uni-list>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import action from './action.js';
const db = uniCloud.database();
export default {
data() {
return {
contentText: {
"contentrefresh": "加载中...",
"contentnomore": "- 暂无相关数据 -"
},
filterNotice: {},
tips: "",
hasMore: true
// notificationDatas:[]
}
},
async onLoad({
param
}) {
// console.log(param,decodeURIComponent(param))
param = JSON.parse(decodeURIComponent(param))
// console.log(param)
this.setParam(param)
},
computed: {
//是否为pc宽屏(width>960px)
isWidescreen() {
return uniIm.isWidescreen
},
notificationDatas() {
let notificationDatas = uniIm.notification.get(this.filterNotice)
if (notificationDatas.length == 0) {
setTimeout(() => {
this.hasMore = false
}, 100);
}
return notificationDatas
}
},
mounted() {
this.hasMore = uniIm.notification.hasMore
},
methods: {
setParam({
filterNotice,
title
}) {
if (typeof filterNotice == 'string') {
filterNotice = JSON.parse(decodeURIComponent(filterNotice))
}
this.filterNotice = filterNotice
console.log('filterNotice', filterNotice)
uni.setNavigationBarTitle({
title
})
if (title == '新朋友' && !this.isWidescreen) {
this.tips = '好友请求通知'
}
},
async setItem({
_id
}, param) {
const datas = uniIm.notification.get(this.filterNotice)
for (let i = 0; i < datas.length; i++) {
if (datas[i]._id == _id) {
datas[i] = deepAssign(datas[i], param)
uniIm.notificationDatas = datas
console.log('uniIm.notificationDatas', uniIm.notificationDatas)
break;
}
}
let ares = await db.collection('uni-im-notification')
.where(`"_id" == "${_id}" && "user_id" == $cloudEnv_uid`)
.get()
// console.log(13231,ares);
let res = await db.collection('uni-im-notification')
.where(`"_id" == "${_id}" && "user_id" == $cloudEnv_uid`)
.update(param)
// console.log('res---66666',param,res.result.updated);
/**
*判断对象是否是一个纯粹的对象
*/
function isPlainObject(obj) {
return typeof obj === 'object' && Object.prototype.toString.call(obj) === '[object Object]'
}
/**
*深度合并多个对象的方法
*/
function deepAssign() {
let len = arguments.length,
target = arguments[0]
if (!isPlainObject(target)) {
target = {}
}
for (let i = 1; i < len; i++) {
let source = arguments[i]
if (isPlainObject(source)) {
for (let s in source) {
if (s === '__proto__' || target === source[s]) {
continue
}
if (isPlainObject(source[s])) {
target[s] = deepAssign(target[s], source[s])
} else {
target[s] = source[s]
}
}
}
}
return target
}
},
async clickHandle(index, item) {
// console.log('index',index,item);
//如果未读,设置为已读
if (!item.is_read) {
this.setItem(item, {
is_read: true
})
}
//存在链接就跳转
let path = item.path || item.payload.path
if (path) {
uni.navigateTo({
url: path,
fail: (e) => {
console.error(e);
}
})
}
// let item = this.notificationDatas[index]
// item.data.is_read = true
// this.notificationDatas[index] = Object.assign({},item)
// console.log(this.notificationDatas);
},
doAction(index, type) {
let item = this.notificationDatas[index]
let e = {
subType: item.payload.subType,
confirm: type === 1,
cancel: type === 0,
item
}
action(e, data => {
console.log('doAction', data)
this.setItem(item, {
is_read: true,
payload: {
state: type === 1 ? 'confirm' : 'cancel'
}
})
})
// console.log(index);
},
friendlyTime(timestamp) {
return uniIm.utils.toFriendlyTime(timestamp)
},
handleText(state) {
switch (state) {
case 0:
return '同意'
break;
case 100:
return '已同意'
break;
case -100:
return '已拒绝'
break;
default:
return '其他'
break;
}
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.notification-box {
height: 100vh;
background-color: #f5f5f5;
}
.tips {
height: 40px;
line-height: 40px;
padding-left: 20rpx;
font-size: 26rpx;
color: #666;
}
.handle-box {
flex-direction: row;
height: 40px;
align-items: center;
}
.handle {
width: 50px;
text-align: center;
height: 25px;
line-height: 25px;
background-color: #efefef;
border-radius: 50px;
font-size: 12px;
margin: 0 5px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.done {
width: 50px;
background-color: #FFF;
color: #aaa;
/* #ifdef H5 */
cursor: default;
/* #endif */
}
.load-more {
background-color: #f5f5f5 !important;
justify-content: center;
}
.tip {
position: relative;
left: -15px;
width: 750rpx;
/* pc宽屏时需要使用100vw */
width: 100%;
}
/* #ifdef MP-WEIXIN */
.load-more ::v-deep .uni-list-item {
background-color: #f5f5f5 !important;
}
/* #endif */
</style>
\ No newline at end of file
<template>
<view class="container">
<view class="qr-code">
<view class="code-info">
<text class="group-name">{{name}}</text>
<text class="group-id" @click="copyGroupID">群号:{{group_id}}</text>
<!--uqrcode 组件来源,插件Sansnn-uQRCode 链接地址:https://ext.dcloud.net.cn/plugin?id=1287 -->
<!-- #ifndef MP-WEIXIN -->
<!-- <uqrcode ref="uqrcode" :start="false" :size="200" canvas-id="qrcode" :value="qrcodeData"></uqrcode> -->
<!-- #endif -->
</view>
<image class="group-avatar" :src="avatar_file || '/uni_modules/uni-im/static/avatarUrl.png'" mode="">
</image>
</view>
<!-- <view class="btn-box">
<view class="btn-item" @click="save">
<uni-icons type="arrow-down" size="30"></uni-icons>
<text class="btn-text">保存</text>
</view>
<view class="btn-item" @click="share">
<uni-icons type="paperplane" size="30"></uni-icons>
<text class="btn-text">分享</text>
</view>
</view> -->
</view>
</template>
<script>
import uqrcode from "@/uni_modules/Sansnn-uQRCode/components/uqrcode/uqrcode"
export default {
components: {
uqrcode
},
data() {
return {
group_id: '',
name: '',
avatar_file: ''
}
},
computed: {
qrcodeData() {
let data = {
"type": "uni-im",
"subType": "groupInfo",
"data": {
group_id: this.group_id,
name: this.name,
avatar_file: this.avatar_file
}
}
return JSON.stringify(data)
}
},
onLoad(options) {
// console.log("options: ",options);
this.group_id = options.id
this.name = options.name
this.avatar_file = options.avatar_file
},
onReady(){
setTimeout(()=>{
this.$refs.uqrcode.make({
success: () => {
// console.log('生成成功');
},
fail: err => {
// console.log(err)
}
});
},1000)
},
methods: {
copyGroupID() {
uni.setClipboardData({
data: this.group_id,
success: function() {
console.log('success');
}
});
},
save() {
console.log('保存');
},
share() {
console.log('分享');
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.container {
height: 100vh;
padding-top: 200rpx;
// justify-content: center;
align-items: center;
background-color: #f5f5f5;
}
.qr-code {
width: 550rpx;
height: 780rpx;
align-items: center;
justify-content: center;
border-radius: 20rpx;
background-color: #fff;
position: relative;
}
.code-info {
align-items: center;
justify-content: center;
}
.group-avatar {
width: 150rpx;
height: 150rpx;
border-radius: 100rpx;
position: absolute;
top: -70rpx;
}
.group-name {
width: 400rpx;
font-size: 46rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 120rpx;
text-align: center;
}
.group-id {
margin: 40rpx 0;
font-size: 30rpx;
}
.btn-box {
flex-direction: row;
margin-top: 100rpx;
}
.btn-item {
align-items: center;
justify-content: center;
width: 130rpx;
height: 130rpx;
border-radius: 100rpx;
background-color: #fff;
margin: 0 80rpx;
border: 1px solid #eee;
}
.btn-text {
font-size: 28rpx;
}
</style>
<template>
<view class="group-info-box" v-if="conversation_id">
<uni-im-group-members :conversation_id="conversation_id"></uni-im-group-members>
<uni-list>
<uni-list-item v-if="!leave_group" title="消息免打扰" @switchChange="changeConversationMute"
:switchChecked="conversation.mute" :showSwitch="true"></uni-list-item>
<uni-list-item v-for="(val,key) in editorFields" :key="key" @click.native="openPopupInfo(key)" :title="val"
:showArrow="isAdmin" :clickable="isAdmin">
<template v-slot:footer>
<text
class="group-info-text">{{ (key == "notification" ? conversation.group_info[key]?.content : conversation.group_info[key]) || '未设置' }}</text>
</template>
</uni-list-item>
<uni-list-item @click.native="setAvatar" title="群头像" :clickable="isAdmin">
<template v-slot:footer>
<image class="logo" :src="logoUrl||'/uni_modules/uni-im/static/avatarUrl.png'" mode=""></image>
</template>
</uni-list-item>
<template v-if="isAdmin">
<uni-list-item @click.native="setAddGroupType" title="加群方式" note="申请加入本群的验证规则" :clickable="isAdmin">
<template v-slot:footer>
<text class="join_option">{{join_option}}</text>
</template>
</uni-list-item>
<uni-list-item title="全员禁言" @switchChange="setMuteAllMembers"
:switchChecked="conversation.group_info.mute_all_members" :showSwitch="true"></uni-list-item>
</template>
<!-- #ifdef H5 -->
<uni-list-item @click="share" :clickable="true" title="分享此群">
<template v-slot:footer>
<uni-icons size="25px" color="#666" type="redo"></uni-icons>
</template>
</uni-list-item>
<!-- #endif -->
</uni-list>
<view v-if="leave_group">
<text style="padding: 15px;text-align: center;color: #666;">- 你不是此群成员 -</text>
<!-- <text @click="joinGroup" style="padding: 15px;text-align: center;color: #005eca;cursor: pointer;">申请加入</text> -->
</view>
<text v-else class="exitGroup" @click="exitGroup">{{isGroupCreator?'解散群聊':'退出群聊'}}</text>
<uni-popup ref="popupInfo" type="dialog">
<uni-popup-dialog mode="input" :title="editorFields[editorType]" :placeholder="'请输入'+editorFields[editorType]"
:duration="2000" :before-close="true" :value="editorDefaultValue" @close="closePopupInfo"
@confirm="confirmPopupInfo"></uni-popup-dialog>
</uni-popup>
</view>
</template>
<script>
const db = uniCloud.database()
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import members from '@/uni_modules/uni-im/pages/group/members';
export default {
components: {
'uni-im-group-members':members
},
data() {
return {
conversation: {
group_info: {
user_id: "",
mute_all_members: false
},
group_member: {},
mute: false
},
leave_group: false,
member_list: [],
editorFields: {
"name": " 群聊名称",
"introduction": "群简介",
"notification": "群公告"
},
editorType: '',
editorDefaultValue: '',
groupType: '',
isAdmin: false,
keyword: '',
mute_all_members: false,
showAllMember: false,
conversation_id:'',
// 鼠标在哪个用户id上
hoverUserId: ''
};
},
computed: {
...uniIm.mapState(['isWidescreen']),
logoUrl() {
return this.conversation.group_info.avatar_file ? this.conversation.group_info.avatar_file.url : false
},
join_option() {
let val = this.conversation.group_info.join_option
return {
needPermission: "需要验证权限",
freeAccess: "自由加入",
disableApply: "禁止加入"
} [val]
},
memberList() {
return this.member_list
// 根据关键词搜索
.filter(member => {
// 忽略大小写
return member.users.nickname.toLowerCase().includes(this.keyword.toLowerCase())
})
// 是管理员排序靠前
.sort((a, b) => {
if (a.role.includes('admin') && !b.role.includes('admin')) {
return -1
} else if (!a.role.includes('admin') && b.role.includes('admin')) {
return 1
} else {
return 0
}
})
},
isGroupCreator() {
return this.conversation.group_info.user_id == uniCloud.getCurrentUserInfo().uid
},
canPrivateChat(){
// 当前登录的账号是管理员,或者当前消息是群管理员发的
return this.uniIDHasRole('staff') || this.hoverUserId && this.conversation.group_member[this.hoverUserId].role.includes('admin')
}
},
watch: {
"conversation.group_info.user_id"(adminUserId) {
// 当前用户是群的创建者或者管理员(在群成员中找到当前用户的角色包含admin)
const currentUserId = uniCloud.getCurrentUserInfo().uid
this.isAdmin = this.isGroupCreator || this.conversation.group_member[currentUserId]?.role.includes('admin')
},
"conversation.group_member": {
handler(group_member, oldValue) {
// console.log('group_member',group_member);
this.member_list = []
for (let key in group_member) {
this.member_list.push(group_member[key])
}
if (!uniIm.isWidescreen) { // pc宽屏不需要
let title = "群信息(" + this.member_list.length + "人)"
uni.setNavigationBarTitle({
title
});
}
},
deep: true,
immediate: true
},
// (后续)通过监听实现实时切换管理员实时刷新权限
// console.log('isAdmin',isAdmin);
conversation: {
handler(conversation, oldValue) {
const currentUserId = uniCloud.getCurrentUserInfo().uid
this.isAdmin = this.isGroupCreator || this.conversation.group_member[currentUserId]?.role.includes('admin')
this.leave_group = conversation.leave
this.mute_all_members = conversation.group_info.mute_all_members
},
deep: true
},
},
async onLoad(e) {
if (!e.conversation_id) {
throw new Error("会话id不能为空")
}
this.load(e.conversation_id)
},
onShow() {
// console.log("this.conversation: ", this.conversation.group_member);
},
methods: {
navToMembers(conversation_id){
uni.navigateTo({
url:"/uni_modules/uni-im/pages/group/members?conversation_id=" + conversation_id
})
},
async load(conversation_id) {
this.conversation_id = conversation_id
this.conversation = await uniIm.conversation.get(conversation_id)
},
async exitGroup() {
const group_id = this.conversation.group_info._id
if (this.isGroupCreator) {
uni.showModal({
title: '确认要解散群聊吗?',
content: '不能撤销,请谨慎操作',
cancelText: '取消',
confirmText: '确认',
complete: async (e) => {
// console.log(e);
if (e.confirm) {
uni.showLoading({
mask: true
});
let res = await db.collection('uni-im-group')
.where({
_id: group_id
})
.remove()
.finally((res) => {
// uni.navigateBack({ // 收到离群的推送通知会自动关闭当前页面
// delta: 2
// })
uni.hideLoading()
})
// console.log('exitGroup', res);
}
}
});
} else {
uni.showModal({
title: '确认要退出群聊吗?',
content: '不能撤销,请谨慎操作',
cancelText: '取消',
confirmText: '确认',
complete: async (e) => {
// console.log(e);
if (e.confirm) {
// uni.navigateBack({ // 收到离群的推送通知会自动关闭当前页面
// delta: 2
// })
uni.showLoading({
mask: true
});
let res = await db.collection('uni-im-group-member').where({
user_id: uniCloud.getCurrentUserInfo().uid,
group_id
})
.remove()
// console.log(res.result);
if (res.result.deleted) {
uni.showToast({
title: '成功退出',
icon: 'none'
});
}
uni.hideLoading()
// console.log('exitGroup', res);
}
}
});
}
},
openPopupInfo(type) {
// console.log(type);
if (!this.isAdmin) return
this.editorType = type
this.editorDefaultValue = this.conversation.group_info[type]
if (this.editorType == "notification") {
this.editorDefaultValue = this.editorDefaultValue?.content || ""
}
this.$refs.popupInfo.open()
},
closePopupInfo() {
this.$refs.popupInfo.close()
},
confirmPopupInfo(value) {
if (!value) {
uni.showToast({
title: '内容不能为空!',
icon: 'none'
});
return
}
// console.log('value', value);
const updateData = {};
if (this.editorType == 'notification') {
updateData[this.editorType] = {
"content": value
}; // 创建时间服务端生成
} else {
updateData[this.editorType] = value;
}
this.updateGroupInfo(updateData)
this.$refs.popupInfo.close()
},
setAddGroupType() {
if (!this.isAdmin) return
uni.showActionSheet({
itemList: ['自由加入', '需要验证权限', '禁止加入'],
success: (e) => {
let join_option = ['freeAccess', 'needPermission', 'disableApply'][e.tapIndex]
this.updateGroupInfo({
join_option
})
},
fail: (err) => {
console.error("err: ", err);
}
})
},
async updateGroupInfo(group_info) {
// console.log('group_info---------',group_info);
this.conversation.group_info = Object.assign(this.conversation.group_info, group_info)
let res = await db.collection('uni-im-group')
.doc(this.conversation.group_id)
.update(group_info)
// console.log('change group info', res.result.updated,this.conversation);
},
async setAvatar() {
if (!this.isAdmin) return
const crop = {
quality: 100,
width: 600,
height: 600,
resize: true
};
uni.chooseImage({
count: 1,
crop,
success: async (res) => {
let tempFile = res.tempFiles[0],
avatar_file = {
// #ifdef H5
extname: tempFile.name.split('.')[tempFile.name.split('.').length - 1],
// #endif
// #ifndef H5
extname: tempFile.path.split('.')[tempFile.path.split('.').length - 1]
// #endif
},
filePath = res.tempFilePaths[0]
// #ifndef APP-PLUS
//非app端用前端组件剪裁头像,app端用内置的原生裁剪
let isPC = false
// #ifdef H5
isPC = !['ios', 'android'].includes(uni.getSystemInfoSync().platform)
// #endif
if (!isPC) {
filePath = await new Promise((callback) => {
uni.navigateTo({
url: '/uni_modules/uni-id-pages/pages/users/cropImage/cropImage?path=' +
filePath + `&options=${JSON.stringify(crop)}`,
animationType: "fade-in",
events: {
success: url => {
callback(url)
}
},
complete(e) {
// console.log(e);
}
});
})
}
// #endif
// console.log(this.users);
let cloudPath = uniCloud.getCurrentUserInfo().uid + '' + Date.now()
avatar_file.name = cloudPath
uni.showLoading({
title: "更新中",
mask: true
});
let {
fileID
} = await uniCloud.uploadFile({
filePath,
cloudPath,
fileType: "image"
});
// console.log(result)
avatar_file.url = fileID
// console.log({avatar_file});
uni.hideLoading()
this.updateGroupInfo({
avatar_file
})
}
})
},
// #ifdef H5
share() {
// 获取当前域名
const data = location.origin + "/#/?joinGroup=" + this.conversation.group_info._id
uni.setClipboardData({
data,
showToast: false,
success: () => {
uni.showToast({
title: "已成功复制分享链接",
duration: 2000,
icon: 'none'
});
}
})
// uni.navigateTo({
// url: '/uni_modules/uni-im/pages/group/groupQRCode?id=' +
// this.conversation.group_info._id +
// '&name=' + this.conversation.group_info.name +
// '&avatar_file=' + url,
// complete: (e) => {
// // console.log(e);
// }
// });
},
// #endif
joinGroup() {
db.collection('uni-im-group-join').add({
"group_id": this.conversation.group_id,
"message": ''
}).then((res) => {
// console.log("res: ", res);
uni.showToast({
icon: 'none',
title: '已申请'
})
})
},
setMuteAllMembers(e) {
for (let user_id in this.conversation.group_member) {
const member = this.conversation.group_member[user_id]
member.mute_type += (e.value ? 1 : -1)
}
this.updateGroupInfo({
"mute_all_members": e.value
})
},
changeConversationMute(e) {
this.conversation.changeMute()
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
page,
.group-info-box {
width: 750rpx;
flex: 1;
background-color: #f5f5f5;
/* #ifdef H5 */
@media screen and (min-device-width:960px) {
height: calc(95vh - 70px);
overflow-y: auto;
flex: none;
}
/* #endif */
}
.logo {
width: 50px;
height: 50px;
}
.exitGroup {
margin: 10px 0;
background-color: #FFFFFF;
padding: 6px 0;
color: #e64141;
border-radius: 0;
font-size: 16px;
text-align: center;
padding: 15px 0;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.exitGroup::after {
display: none;
}
.group-info-text {
color: #666;
font-size: 14px;
max-width: 560rpx;
text-align: right;
}
.join_option {
color: #666;
font-size: 14px;
}
</style>
\ No newline at end of file
<template>
<view class="group-members-box">
<uni-search-bar v-model="keyword" class="search-bar" radius="5" placeholder="输入昵称搜索" clearButton="auto"
cancelButton="none"></uni-search-bar>
<view v-if="!leave_group" class="members-list-container" :class="{'show-less':!showAllMember}">
<view class="invite-box item" v-if="isAdmin">
<view class="invite-icon">
<uni-icons @click="invite" color="#989898" size="20px" type="plusempty"></uni-icons>
</view>
<text class="invite-text">邀请</text>
</view>
<template v-for="(member,index) in memberList" :key="index">
<view class="item" :title="member.users.nickname"
:class="{'pointer': canPrivateChat,'focus':member.focus}" @click="toChat(member.users._id)"
@longpress.prevent="openConversationMenu($event,index)"
@contextmenu.prevent="openConversationMenu($event,index)"
@mousemove="hoverUserId = member.users._id"
>
<image class="avatar"
:src="(member.users.avatar_file && member.users.avatar_file.url) ? member.users.avatar_file.url:'/uni_modules/uni-im/static/avatarUrl.png'"
mode="widthFix"></image>
<text class="nickname">{{member.users.nickname||'匿名用户'}}</text>
<text v-if="member.role.includes('admin')" class="group-admin"></text>
<text v-if="!mute_all_members && member.mute_type" class="mute-type-1">已被禁言</text>
</view>
</template>
</view>
<text v-if="showMoreBtn" class="show-all-menber-btn" @click="onClickShowAllMenber">{{showAllMember ? '收起' : '查看更多'}}</text>
<uni-im-contextmenu ref="uni-im-contextmenu"></uni-im-contextmenu>
</view>
</template>
<script>
const db = uniCloud.database()
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
data() {
return {
conversation: {
group_info: {
user_id: "",
mute_all_members: false
},
group_member: {},
mute: false
},
leave_group: false,
member_list: [],
editorType: '',
editorDefaultValue: '',
groupType: '',
isAdmin: false,
keyword: '',
mute_all_members: false,
showAllMember: false,
// 鼠标在哪个用户id上
hoverUserId: '',
showMoreBtn: true,
// 延迟渲染,避免页面卡顿
laterRenderIndex: 1
};
},
computed: {
...uniIm.mapState(['isWidescreen']),
memberList() {
let memberList = this.member_list
// 根据关键词搜索
.filter(member => {
// 忽略大小写
return member.users.nickname.toLowerCase().includes(this.keyword.toLowerCase())
})
// 是管理员排序靠前
.sort((a, b) => {
if (a.role.includes('admin') && !b.role.includes('admin')) {
return -1
} else if (!a.role.includes('admin') && b.role.includes('admin')) {
return 1
} else {
return 0
}
})
// laterRenderIndex
.filter((item, index) => {
return index < this.laterRenderIndex * 50
})
const memberCount = memberList.length
if(memberCount && memberCount < 10){
this.showMoreBtn = false
}
if (!this.showAllMember) {
memberList = memberList.slice(0, 9)
}
return memberList
},
isGroupCreator() {
return this.conversation.group_info.user_id == uniCloud.getCurrentUserInfo().uid
},
canPrivateChat(){
// 当前登录的账号是管理员,或者是群管理员,或者要私信管理员
const currentUserId = uniCloud.getCurrentUserInfo().uid
return this.uniIDHasRole('staff') ||
this.conversation.group_member[currentUserId].role.includes('admin') ||
this.hoverUserId && this.conversation.group_member[this.hoverUserId].role.includes('admin')
}
},
onReachBottom() {
this.laterRenderIndex++
},
watch: {
"conversation.group_info.user_id"(adminUserId) {
// 当前用户是群的创建者或者管理员(在群成员中找到当前用户的角色包含admin)
const currentUserId = uniCloud.getCurrentUserInfo().uid
this.isAdmin = this.isGroupCreator || this.conversation.group_member[currentUserId]?.role.includes('admin')
},
"conversation.group_member": {
handler(group_member, oldValue) {
// console.log('group_member',group_member);
this.member_list = []
for (let key in group_member) {
this.member_list.push(group_member[key])
}
if (!uniIm.isWidescreen && this.showMoreBtn === false) { // pc宽屏不需要
let title = "群成员列表(" + this.member_list.length + "人)"
uni.setNavigationBarTitle({
title
});
}
},
deep: true,
immediate: true
},
// (后续)通过监听实现实时切换管理员实时刷新权限
// console.log('isAdmin',isAdmin);
conversation: {
handler(conversation, oldValue) {
const currentUserId = uniCloud.getCurrentUserInfo().uid
this.isAdmin = this.isGroupCreator || this.conversation.group_member[currentUserId]?.role.includes('admin')
this.leave_group = conversation.leave
this.mute_all_members = conversation.group_info.mute_all_members
},
deep: true
},
},
props: {
conversation_id: {
default () {
return false
}
}
},
async onLoad(e) {
if (!e.conversation_id) {
throw new Error("会话id不能为空")
}
// 以页面的方式打开,不需要显示更多的按钮和隐藏部分用户
this.showMoreBtn = false
this.showAllMember = true
this.load(e.conversation_id)
},
mounted() { //pc端以组件模式加载时逻辑
if (this.conversation_id) {
this.load(this.conversation_id)
}
},
methods: {
async load(conversation_id) {
this.conversation = await uniIm.conversation.get(conversation_id)
},
toChat(user_id) {
if (this.canPrivateChat) {
uniIm.toChat({
user_id,
source:{
group_id: this.conversation.group_id
}
})
}
},
onClickShowAllMenber() {
if(this.isWidescreen){
this.showAllMember = !this.showAllMember
}else{
uni.navigateTo({
url: '/uni_modules/uni-im/pages/group/members?conversation_id=' + this.conversation.id,
animationType:"slide-in-right"
})
}
},
invite() {
uni.navigateTo({
url: '/uni_modules/uni-im/pages/contacts/createGroup/createGroup?group_id=' + this.conversation.group_info._id
})
},
async expel(item) {
uni.showModal({
title: '确定要将该用户移出本群吗?',
content: '不能撤销,请谨慎操作',
cancelText: '取消',
confirmText: '确认',
complete: async (e) => {
// console.log(e);
if (e.confirm) {
uni.showLoading({
mask: true
});
try {
let res = await db.collection('uni-im-group-member').where({
user_id: item.users._id,
group_id: this.conversation.group_info._id
})
.remove()
if (res.result.deleted) {
uni.showToast({
title: '成功移除',
icon: 'none',
complete: () => {}
});
// console.log('exitGroup', res);
}
} catch (error) {
uni.showToast({
title: error.message,
icon: 'error',
complete: () => {}
});
}
uni.hideLoading()
}
}
});
},
async expelAndToBlack(item) {
uni.showModal({
title: '确定要将该用户移出本群并拉黑吗?',
content: '拉黑后此用户将不能再次加入本群,不能撤销,请谨慎操作',
cancelText: '取消',
confirmText: '确认',
complete: async (e) => {
// console.log(e);
if (e.confirm) {
uni.showLoading({
mask: true
});
try {
let res = await db.collection('uni-im-group-member').where({
user_id: item.users._id,
group_id: this.conversation.group_info._id
})
.remove()
console.log('expel', res);
const uniImCo = uniCloud.importObject("uni-im-co")
res = await uniImCo.addToGroupMenberBlackList({
user_id: item.users._id,
group_id: this.conversation.group_info._id
})
console.log('expelAndToBlack', res);
} catch (error) {
uni.showToast({
title: error.message,
icon: 'error',
complete: () => {}
});
}
uni.hideLoading()
}
}
});
},
async changeMemberMute(item) {
let nickname = item.users.nickname || '匿名用户'
uni.showModal({
title: '确定要' + (item.mute_type ? `为"${nickname}"解除禁言吗?` : `禁言"${nickname}"吗?`),
cancelText: '取消',
confirmText: '确认',
complete: async (e) => {
// console.log(e);
if (e.confirm) {
uni.showLoading({
mask: true
});
try {
let res = await db.collection('uni-im-group-member').where({
_id: item._id,
mute_type: item.mute_type // 防止此时云端已经变化
})
.update({
mute_type: item.mute_type ? 0 : 1
})
// console.log('mute_type', res);
if (res.result.updated) {
item.mute_type = item.mute_type ? 0 : 1
uni.showToast({
title: '设置成功',
icon: 'none',
complete: () => {}
});
}
} catch (error) {
// console.log('error',merror)
uni.showToast({
title: error.message,
icon: 'error',
complete: () => {}
});
}
uni.hideLoading()
}
}
});
},
openConversationMenu(e, index) {
if (!this.isAdmin) {
return
}
const member = this.memberList[index]
const menuList = []
menuList.unshift({
"title": "移除",
"action": () => {
// console.log('移除');
this.expel(member)
}
})
menuList.unshift({
"title": "移除并拉黑",
"action": () => {
console.log('移除并拉黑');
this.expelAndToBlack(member)
}
})
if (!this.conversation.group_info.mute_all_members) {
menuList.unshift({
"title": member.mute_type ? "解除禁言" : '设为禁言',
"action": () => {
// console.log('禁言');
this.changeMemberMute(member)
}
})
}
const isAdmin = member.role.includes('admin')
menuList.push({
"title": isAdmin ? "取消管理员" : "设置管理员",
"action": () => {
let role = member.role;
if (isAdmin) {
// console.log('取消管理员');
role = member.role.filter(item => item !== 'admin')
} else {
role.push('admin')
// console.log('设置管理员');
}
uni.showLoading({
mask: true
});
db.collection('uni-im-group-member').doc(member._id).update({
"role": role
}).then(res => {
// console.log('res', res);
member.role = role
})
.catch(err => {
console.error(err)
uni.showToast({
title: err.message,
icon: 'none'
});
})
.finally(() => {
uni.hideLoading()
})
}
})
if (menuList.length > 0) {
member.focus = true
const myContextmenu = this.$refs['uni-im-contextmenu']
const position = {
"contextmenu": {
"top": e.clientY,
"left": e.clientX
},
"longpress": {
"top": e.touches[0].screenY || e.touches[0].clientY,
"left": e.touches[0].screenX || e.touches[0].clientX
}
} [e.type]
// #ifdef H5
position.top = position.top + 120
// #endif
myContextmenu.show(position, menuList)
myContextmenu.onClose(() => {
member.focus = false
})
}
},
}
}
</script>
<style lang="less" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
page,
.group-members-box {
width: 750rpx;
background-color: #f5f5f5;
}
.members-list-container {
width: 750rpx;
flex-direction: row;
flex-wrap: wrap;
}
.item {
position: relative;
width: 150rpx;
height: 140rpx;
margin: 5px 0;
align-items: center;
justify-content: space-around;
}
.invite-icon {
border: 1px dashed #ccc;
border-radius: 10px;
width: 90rpx;
height: 90rpx;
justify-content: center;
}
/* #ifdef H5 */
// 收缩
.show-less {
max-height: 400px;
}
.windows .item {
width: 146rpx;
}
/* #endif */
.item.focus {
border: 1px dashed #ccc;
}
.group-admin {
position: absolute;
top: 5px;
right: 5px;
padding: 1px 3px;
border-radius: 6px;
background-color: #e64141;
color: #fff;
font-size: 12px;
}
.mute-type-1 {
position: absolute;
padding: 1px 10px;
background-color: #0b60ff;
color: #fff;
font-size: 10px;
bottom: 28px;
}
/* #ifdef H5 */
.pointer {
cursor: pointer;
}
/* #endif */
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 10px;
box-shadow: 0 0 1px #aaa;
}
.invite-text,.nickname {
width: 140rpx;
text-align: center;
font-size: 12px;
color: #666;
padding: 0 16rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.show-all-menber-btn {
text-align: center;
font-size: 14px;
color: #666;
height: 40px;
line-height: 40px;
}
/* #ifdef H5 */
.show-all-menber-btn {
cursor: pointer;
}
.show-all-menber-btn:hover {
color: #333;
}
/* #endif */
</style>
\ No newline at end of file
......@@ -551,47 +551,22 @@
async toChat(param) {
this.chatInfoIsShow = false;
// console.log('toChat param',param);
let conversation_id = ''
if (typeof param == 'string') {
conversation_id = param
} else {
if (param.conversation_id) {
conversation_id = param.conversation_id
} else if (param.group_id) {
conversation_id = 'group_' + param.group_id
} else if (param.user_id || param.friend_uid) {
conversation_id = (await uniIm.conversation.get(param)).id
// console.log('conversation_id',conversation_id)
// // #ifdef H5
// setTimeout(async ()=>{
// const query = uni.createSelectorQuery().in(this);
// query.select('#'+conversation_id).boundingClientRect(data => {
// if(!data){
// // console.log('找不到 showMsgByIndex #'+conversation_id);
// return
// }else{
// // console.log(data);
// }
// let listHeight = document.querySelector('#user-list-box').clientHeight
// if(data.top - 70 < listHeight){
// return
// }
// this.listScrollTop = ''
// this.$nextTick(()=>{
// this.listScrollTop = currentScrollTop - listHeight + data.top + data.height
// // console.log('this.listScrollTop',this.listScrollTop);
// })
// }).exec()
// },0);
// // #endif
} else {
throw new Error("toChat param is error")
}
}
let conversation_id = getConversationId(param)
this.keyword = ''
this.filteredConversationId = false
uniIm.currentConversationId = conversation_id
console.log('conversation_id',conversation_id);
if(conversation_id.indexOf('group_') === 0){
uni.showLoading({
title: '加载中',
mask: false
});
const conversation = await uniIm.conversation.get(conversation_id)
uni.hideLoading()
}
if (this.isWidescreen) { // 若为宽屏,则切换右侧的组件
this.$nextTick(() => {
......@@ -606,6 +581,22 @@
animationDuration: 300
})
}
function getConversationId(param){
if (typeof param == 'string') {
return param
} else {
if (param.conversation_id) {
return param.conversation_id
} else if (param.group_id) {
return 'group_' + param.group_id
} else if (param.user_id || param.friend_uid) {
return uniIm.utils.getConversationId(param.user_id || param.friend_uid)
} else {
throw new Error("toChat param is error")
}
}
}
},
showChatInfo() {
this.chatInfoIsShow = !this.chatInfoIsShow
......
......@@ -2,12 +2,13 @@
flex: 1;
}
/* #ifndef APP-NVUE */
#center-view{
height: 100vh;
/* #ifdef H5 */
height: calc(100vh - 44px);
/* #endif */
background-color: #f5f5f5 !important;
}
/* #endif */
/* #ifdef H5 */
@media screen and (min-device-width:960px){
......@@ -22,6 +23,151 @@
width: 100vw;
background-color: #2e2e3e;
min-width:960px;
#left-view,
#center-view,
#right-view {
height: 95vh;
position: relative;
top: -2vh;
background-color: #FFFFFF;
}
#center-view {
#conversation-list-box ::v-deep .conversation-list{
max-height: calc(95vh - 70px);
.conversation-list-item{
margin: 5px;
}
}
}
#left-view {
& > *{
cursor: pointer;
margin-top: 36px;
}
& > *:hover{
/* border: 1px solid #fefefe; */
box-shadow: 0 0 20px 2px #fefefe;
border-radius: 10px;
}
width:60px;
align-items: center;
padding-top: 100px;
background-color: #ececec;
border-radius: 20px 0 0 20px;
}
#center-view{
width:300px;
#search-bar-box {
flex-direction: row;
align-items: center;
height: 70px;
padding:0 10px;
}
#search-bar {
flex: 1;
}
#uni-im-contacts-box {
z-index: 10;
height: 95vh;
width: 100%;
position: absolute;
background-color: #f5f5f5;
/* pc端隐藏删除好友的按钮,后续改成长按(右键)可见 */
::v-deep .delete-btn{
display: none;
}
}
}
#right-view {
width: calc(100% - 370px);
border-radius: 0 20px 20px 0;
overflow: hidden;
position: relative;
#chat-view-box {
#ccid-is-null-tip{
height: 95vh;
padding-top: 30vh;
align-items: center;
background-color: #efefef;
.img {
width: 130px;
margin-bottom: 15px;
}
.text {
color: #999;
}
}
}
#chat-header{
height: 70px;
padding:0 20px;
flex-direction: row;
justify-content: end;
align-items: center;
.more{
cursor: pointer;
}
}
#chat-view {
max-height:calc(95vh - 70px);
}
#chat-view ::v-deep #uni-im-chat{
#web-pc-chat-title
{
position: absolute;
z-index: 9;
top: -45px;
left: 15px;
}
.unread_count {
display: none;
}
.msg-list .uni-im-list .uni-im-msg{
padding:0 10px;
}
}
.chatInfoBox {
z-index: 999;
position: absolute;
right:0;
top: 70px;
background-color: rgba(0,0,0,0.2);
height: calc(95vh - 70px);
align-items: flex-end;
width: 100%;
::v-deep .group-info-text{
max-width: 250px !important;
text-align: left;
word-break: break-all;
}
}
#dynamic-component-box {
z-index: 9;
width: 100%;
height: 95vh;
background-color: #fff;
position: absolute;
right: 0;
.dynamic-component-title{
padding-left: 15px;
height: 70px;
justify-content: center;
border: 1px solid #efefef;
}
.system-notice-box{
width: 100%;
}
.create-group-box .header-box{
width: 100%;
.uni-searchbar {
width: 100%;
}
}
}
}
}
#foot {
......@@ -29,226 +175,29 @@
bottom: 10px;
flex-direction: row;
color: #FFFFFF;
.uni-link:hover{
color: #00aa55 !important;
}
.item {
flex-direction: row;
margin:0 50px;
align-items: center;
.icon {
width: 20px;
height: 20px;
margin-right: 5px;
}
.link {
margin-left: 5px;
}
}
}
#foot .uni-link:hover{
color: #00aa55 !important;
}
#foot .item {
flex-direction: row;
margin:0 50px;
align-items: center;
}
#foot .item .icon {
width: 20px;
height: 20px;
margin-right: 5px;
}
#foot .item .link {
margin-left: 5px;
}
#left-view,
#center-view,
#right-view{
height: 95vh;
position: relative;
top: -2vh;
background-color: #FFFFFF;
}
#center-view #conversation-list-box ::v-deep .conversation-list{
max-height: calc(95vh - 70px);
padding-top: 5px;
}
#left-view > *{
cursor: pointer;
margin-top: 36px;
}
#left-view > *:hover{
/* border: 1px solid #fefefe; */
box-shadow: 0 0 20px 2px #fefefe;
border-radius: 10px;
}
#left-view{
width:60px;
align-items: center;
padding-top: 100px;
background-color: #ececec;
border-radius: 20px 0 0 20px;
}
#center-view{
width:300px;
}
#center-view #search-bar-box {
flex-direction: row;
align-items: center;
height: 70px;
padding:0 10px;
}
#center-view #search-bar {
flex: 1;
}
.windows #center-view #conversation-list-box ::v-deep .conversation-list-item .uni-list-chat__container{
padding-right: 10px;
}
#right-view{
width: calc(100% - 370px);
border-radius: 0 20px 20px 0;
overflow: hidden;
position: relative;
}
#right-view #chat-header{
height: 70px;
padding:0 20px;
flex-direction: row;
justify-content: end;
align-items: center;
}
#right-view #chat-header .more{
cursor: pointer;
}
#right-view #chat-view
{
max-height:calc(95vh - 70px);
}
#right-view #chat-view ::v-deep .page {
flex: auto;
}
#right-view #chat-view ::v-deep .unread_count {
display: none;
}
#right-view #chat-view ::v-deep .msg-list .uni-im-list .scroll-view{
height: calc(95vh - 330px);
}
#right-view #chat-view ::v-deep .msg-list .uni-im-list .uni-im-msg{
padding:0 10px;
}
#right-view #chat-view ::v-deep .msg-list .uni-im-list .uni-im-msg .cite-box{
max-width:500px;
}
#right-view #chat-view ::v-deep .chat-foot,
#right-view #chat-view ::v-deep .disable-input
{
position: relative;
bottom: 0;
width:100%;
}
#right-view #chat-view ::v-deep .chat-foot .pc,
#right-view #chat-view ::v-deep .disable-input
{
display: flex;
height: 260px;
position: absolute;
bottom:0;
background-color: #f7f7f7;
width: 100%;
justify-content: center;
align-items: center;
padding: 0 16px;
}
#right-view #chat-view ::v-deep .chat-foot .pc > *{
width: 100%;
}
#right-view #chat-view ::v-deep .chat-foot .pc .tool-bar,
#right-view #chat-view ::v-deep .chat-foot .pc .tool-bar .icons{
flex-direction: row;
height: 50px;
align-items: center;
}
#right-view #chat-view ::v-deep .chat-foot .pc .tool-bar .icons .uni-im-icons{
padding-right:30px;
}
#right-view #chat-view ::v-deep .chat-foot .pc .answer-msg{
padding: 6px 10px;
margin-bottom: 6px;
}
#right-view #chat-view ::v-deep .chat-foot .pc .textarea{
height: 200px;
background-color: #f7f7f7;
}
#right-view .chatInfoBox {
z-index: 999;
position: absolute;
right:0;
top: 70px;
background-color: rgba(0,0,0,0.2);
height: calc(95vh - 70px);
align-items: flex-end;
width: 100%;
}
#right-view .chatInfoBox ::v-deep .group-info-text{
max-width: 250px !important;
text-align: justify;
}
#uni-im-contacts-box {
z-index: 10;
height: 95vh;
width: 100%;
position: absolute;
background-color: #f5f5f5;
}
/* pc端隐藏删除好友的按钮,后续改成长按(右键)可见 */
#uni-im-contacts-box ::v-deep .delete-btn{
display: none;
}
#ccid-is-null-tip{
height: 95vh;
padding-top: 15vh;
align-items: center;
background-color: #efefef;
color: #999;
}
#dynamic-component-box {
z-index: 9;
width: 100%;
height: 95vh;
background-color: #fff;
position: absolute;
right: 0;
}
#dynamic-component-box .dynamic-component-title{
padding-left: 15px;
height: 70px;
justify-content: center;
border: 1px solid #efefef;
}
#right-view #dynamic-component-box .system-notice-box{
width: 100%;
}
#right-view ::v-deep #dynamic-component-box .create-group-box .header-box{
width: 100%;
}
#right-view ::v-deep #dynamic-component-box .create-group-box .header-box .uni-searchbar {
width: 100%;
}
.windows {
#center-view #conversation-list-box ::v-deep .conversation-list-item .uni-list-chat__container{
padding-right: 10px;
}
}
}
/* #endif */
\ No newline at end of file
<template>
<view id="page">
<!-- #ifdef H5 -->
<!-- 底部栏 -->
<view id="foot" v-if="isWidescreen">
<view class="item">
<image class="icon" src="https://web-assets.dcloud.net.cn/unidoc/zh/git-1.png" mode="widthFix"></image>
<text>本项目已开源</text>
<uni-link class="link" href="https://gitcode.net/dcloud/hello-uni-im/-/tree/v3" text="git仓库地址"></uni-link>
</view>
<view class="item">
<image class="icon" src="https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/hx.png" mode="widthFix"></image>
<text>离线通知插件</text>
<uni-link class="link" href="https://ext.dcloud.net.cn/plugin?id=16984" text="下载地址"></uni-link>
</view>
</view>
<!-- 布局最左侧 菜单栏 -->
<view id="left-view" v-if="isWidescreen" @click="chatInfoIsShow = false;">
<cloud-image class="user-avatar" @click="toUcenter" :src="avatarUrl" width="40px" height="40px"
borderRadius="100px"></cloud-image>
<!-- {{currentUserInfo.username}} -->
<uni-badge @contextmenu.prevent.native="openConversationMenu($event,'unreadMsgCount')" class="chat-icon" size="small" :text="unreadMsgCount" absolute="rightTop" type="error">
<uni-icons @click="showChatView"
:color="contactsViewIsShow?'#c5c5c5':'#5fc08e'" size="32" type="chatbubble-filled"></uni-icons>
</uni-badge>
<uni-badge id="show-contacts-btn" size="small" :text="unreadnotificationCount" absolute="rightTop" type="error">
<uni-icons @click="showContactsView" :color="contactsViewIsShow?'#5fc08e':'#c5c5c5'" size="32"
type="staff-filled"></uni-icons>
</uni-badge>
</view>
<!-- #endif -->
<!-- 会话列表 -->
<view id="center-view">
<!-- #ifdef H5 -->
<template v-if="isWidescreen">
<!-- 搜索会话用户、聊天记录... -->
<view id="search-bar-box">
<uni-search-bar v-model="keyword" id="search-bar" radius="5" placeholder="搜索" clearButton="auto" cancelButton="none"></uni-search-bar>
<uni-icons class="pointer" @click="beforeJoinGroup" color="#aaa" size="26" type="plus"></uni-icons>
</view>
<view id="uni-im-contacts-box" v-show="contactsViewIsShow">
<uni-im-contacts @clickMenu="clickMenu" id="uni-im-contacts" ref="uni-im-contacts"></uni-im-contacts>
</view>
<!-- 会话查找结果列表 -->
<uni-im-filtered-conversation-list
v-if="keyword"
ref="uni-im-filtered-conversation-list"
id="conversation-list-box"
:keyword="keyword"
@to-chat="toChat($event)"
@to-chat-filtered="toChatFiltered($event)"
></uni-im-filtered-conversation-list>
</template>
<!-- #endif -->
<!-- 会话用户列表 -->
<uni-im-conversation-list
v-if="!keyword"
ref="uni-im-conversation-list" @clickItem="toChat($event.id)"
@change="conversationList = $event" :active-conversation-id="currentConversationId"
id="conversation-list-box"
></uni-im-conversation-list>
</view>
<!-- #ifdef H5 -->
<view id="right-view" v-if="isWidescreen">
<!-- 聊天窗口 -->
<view id="chat-view-box">
<template v-if="!contactsViewIsShow && currentConversationId">
<view id="chat-header" v-if="!contactsViewIsShow || filteredConversationId">
<uni-icons @click="showChatInfo" class="more" type="more-filled" size="20"></uni-icons>
</view>
<view id="chat-view">
<chat-filtered
v-if="filteredConversationId"
ref="chat-filtered"
@to-chat="toChat($event)"
/>
<chat-view v-else ref="chat-view"></chat-view>
</view>
<view v-if="chatInfoIsShow" class="chatInfoBox" @click.stop="chatInfoIsShow = false">
<view @click.native.stop>
<uni-im-group-info v-if="currentConversation.group_id" ref="group-info"></uni-im-group-info>
<uni-im-chat-info v-else ref="chat-info"></uni-im-chat-info>
</view>
</view>
</template>
<view v-else id="ccid-is-null-tip">
<image class="img" src="https://im.dcloud.net.cn/static/favicon.ico" mode="widthFix"></image>
<text class="text">未选择会话对象</text>
</view>
</view>
<view id="dynamic-component-box" v-show="contactsViewIsShow">
<view class="dynamic-component-title">{{view2Title}}</view>
<component ref="dynamicComponent" :is="dynamicComponentName"></component>
</view>
</view>
<uniImVideo></uniImVideo>
<uni-im-contextmenu ref="uni-im-contextmenu" />
<!-- #endif -->
</view>
</template>
<script>
import {
store as uniIdStore,
mutations as uniIdMutations
} from '@/uni_modules/uni-id-pages/common/store.js';
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import contacts from '@/uni_modules/uni-im/pages/contacts/contacts';
// #ifdef H5
import chatView from '@/uni_modules/uni-im/pages/chat/chat';
import chatFiltered from '@/uni_modules/uni-im/pages/chat/chat-filtered';
import notification from '@/uni_modules/uni-im/pages/contacts/notification/notification';
import addPeopleGroups from '@/uni_modules/uni-im/pages/contacts/addPeopleGroups/addPeopleGroups';
import groupList from '@/uni_modules/uni-im/pages/contacts/groupList/groupList';
import createGroup from '@/uni_modules/uni-im/pages/contacts/createGroup/createGroup';
import chatInfo from '@/uni_modules/uni-im/pages/chat/info';
import groupInfo from '@/uni_modules/uni-im/pages/group/info';
import uniImVideo from '@/uni_modules/uni-im/pages/common/video/video';
let currentScrollTop = 0
// #endif
let lastConversationId = false
export default {
// #ifdef H5
components: {
chatView,
chatFiltered,
"uni-im-contacts": contacts,
"uni-im-notification": notification,
"uni-im-addPeopleGroups": addPeopleGroups,
"uni-im-groupList": groupList,
"uni-im-createGroup": createGroup,
"uni-im-chat-info": chatInfo,
"uni-im-group-info": groupInfo,
uniImVideo
},
// #endif
data() {
return {
users: {},
dynamicComponentName: 'uni-im-addPeopleGroups', //通过动态组件引入页面在pc端显示
view2Title: '加人/加群',
contactsViewIsShow: false,
chatInfoIsShow: false,
currentConversation: {},
keyword:'',
conversationList: [],
filteredConversationId: false, // 仅显示匹配的聊天记录的会话
};
},
computed: {
// 导入uniIm响应式数据,支持别名:比如:['a as b']
...uniIm.mapState(['currentConversationId', 'isWidescreen']),
unreadMsgCount() {
return uniIm.conversation.unreadCount()
},
unreadnotificationCount() {
return uniIm.notification.unreadCount()
},
currentUserInfo() {
return uniIdStore.userInfo
},
avatarUrl() {
if (this.currentUserInfo.avatar_file && this.currentUserInfo.avatar_file.url) {
return this.currentUserInfo.avatar_file.url
}
return '/uni_modules/uni-im/static/avatarUrl.png'
}
},
watch: {
unreadMsgCount: {
handler(unreadMsgCount) {
// console.log({
// unreadMsgCount
// });
// #ifdef APP
plus.runtime.setBadgeNumber(unreadMsgCount)
// #endif
if (unreadMsgCount == 0) {
uni.removeTabBarBadge({
index: 0,
complete: (e) => {
// console.log(e)
}
})
} else {
uni.setTabBarBadge({
index: 0,
text: unreadMsgCount + '',
complete: (e) => {
// console.log(e)
}
})
}
// 调用扩展点,更新未读消息总数。
uniIm.extensions.invokeExts('ui-update-unread-count', unreadMsgCount)
},
immediate: true,
},
contactsViewIsShow(contactsViewIsShow) {
if (contactsViewIsShow) {
lastConversationId = this.currentConversationId
uniIm.currentConversationId = false
} else {
if (lastConversationId) {
uniIm.currentConversationId = lastConversationId
this.$nextTick(() => {
this.toChat(lastConversationId)
})
}
}
},
// 根据当前会话id,设置会话对象
async currentConversationId(id) {
this.currentConversation = await uniIm.conversation.get(id)
// 如果是被隐藏的会话,取消隐藏
if(this.currentConversation.hidden){
this.currentConversation.hidden = false
}
}
},
async onLoad(param) {
console.log('onLoad',param)
/**
* 打开index页面之前的扩展点,用于自己扩展登录等逻辑
*/
await Promise.all(uniIm.extensions.invokeExts('index-load-before-extra', param))
const {tokenExpired} = uniCloud.getCurrentUserInfo()
if (tokenExpired < Date.now()) {
console.info('当前用户的登录状态无效,将自动跳转至登录页面。', param)
let url = '/uni_modules/uni-id-pages/pages/login/login-withpwd?uniIdRedirectUrl='
let paramString = '/uni_modules/uni-im/pages/index/index?'
for (let key in param) {
paramString += `${key}=${param[key]}&`
}
paramString = paramString.substring(0, paramString.length - 1) //携带参数,实现登录成功后 跳回首页时传回
url += encodeURIComponent(paramString)
return uni.redirectTo({
url
})
}
uniIm.onInitDataAfter(()=>{
// console.log('onUniImInitDataAfter');
// 执行当前页面初始化
this.init(param)
})
},
async onReady() {
uni.$on('uni-im-toChat', param => {
if (param) {
// 关闭最后一次的会话id,防止切回后重新显示最后一次会话,而指定显示到某个会话
lastConversationId = false
this.toChat(param)
}
this.contactsViewIsShow = false
})
// #ifdef H5
const shortcutKeyFn = (keyName,event)=>{
const index = this.conversationList.findIndex(item=>item.id == this.currentConversationId)
if(keyName == 'ArrowUp' && index > 0){
this.toChat( this.conversationList[index - 1].id )
event.preventDefault();
}else if(keyName == 'ArrowDown' && index < this.conversationList.length){
this.toChat( this.conversationList[index + 1].id )
event.preventDefault();
}
}
uniIm.utils.shortcutKey.withMeta(shortcutKeyFn)
uniIm.utils.shortcutKey.withCtrl(shortcutKeyFn)
let systemInfo = uni.getSystemInfoSync()
uniIm.systemInfo = systemInfo
if (systemInfo.browserName != 'chrome' && (this.isWidescreen == true || systemInfo.osName != 'ios')) {
let newElement = document.createElement('div')
newElement.innerHTML = `
<div id="tip-browser-compatibility" style="background: #fff9ea;color: #ff9a43;position: fixed;top: 0;left: 0;width: 100vw;padding: 15px;font-size: 18px;">
注意:本系统仅兼容chrome浏览器,其他浏览器可能出现异常。<a href="https://www.google.cn/chrome/">点此下载chrome浏览器</a>
</div>
`
document.body.appendChild(newElement)
}
// 设置 right-view的宽为屏幕的百分之99
// document.querySelector('#right-view').style.width = systemInfo.screenWidth - 360 + 'px'
// #endif
},
onUnload() {
},
onHide() {},
methods: {
clickMenu(data) {
// console.log('79879789798898798978789', data);
this.dynamicComponentName = data.componentName
if (data.title) {
this.view2Title = data.title
}
this.$nextTick(() => {
this.$refs.dynamicComponent.setParam(data.param || {})
if (data.componentName == 'uni-im-createGroup') {
this.$refs.dynamicComponent.getFriendsData()
}
})
// console.log('data.componentName----',data.componentName);
},
/**
* @description 根据群id,申请加入群聊
* @param {Object} 群id
*/
joinGroup(group_id){
console.log('group_id',group_id);
const db = uniCloud.database();
uni.showLoading({
title: '正在申请加群...',
mask: true
});
db.collection('uni-im-group-join').add({
group_id,
"message":''
}).then((res) => {
console.log("res: ",res);
if(res.result.isPass){
this.toChat("group_" + group_id)
}else{
uni.showToast({
icon: 'none',
title: '已提交加群申请,等待管理员审核'
})
}
}).catch((err) => {
if(err.message === "已经是群成员"){
console.log('已经是群成员 直接打开对应页面');
return this.toChat("group_" + group_id)
}
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
})
.finally(()=>{
uni.hideLoading()
})
},
// #ifdef H5
beforeJoinGroup(){
let group_id = prompt("请输入群id", "");
if (group_id) {
this.joinGroup(group_id)
}
},
// #endif
readQrCode() {
uni.scanCode({
complete: (e) => {
// console.log(e);
try {
let data = JSON.parse(e.result)
// console.log(data);
if (data.type == 'uni-im' && data.subType == "groupInfo") {
}
} catch (e) {
}
}
})
},
async init({
conversation_id,
goods,
user_id,
joinGroup
}) {
// console.log('init', {
// conversation_id,
// goods,
// user_id
// });
// 如果列表小于30个会话,尝试着从云端拉取一次
if( this.conversationList.length < 30 ){
await this.$nextTick()
await this.$refs['uni-im-conversation-list'].loadMore()
}else{
console.log('会话列表已满一页,需要用户自己滚动到底,再拉取更多');
}
// console.log('this.conversationList.length',this.conversationList.length);
if (conversation_id) {
console.log('conversation_id', conversation_id);
this.toChat(conversation_id)
} else if (user_id) {
//创建会话
const currentConversation = await uniIm.conversation.get({
friend_uid: user_id
})
// console.log('currentConversation', currentConversation);
// 当前用户给对方发个消息
this.toChat(currentConversation.id)
}
if(user_id){
// 如果初始化时,指定了要访问的user会话。将指定要访问的会话排序位置置顶,方便看到
// 场景:插件市场,点击联系作者。自动将此会话放到首个
setTimeout(()=> {
this.currentConversation.customIndex = Date.now()
}, 0);
}
// 传递参数goods(对象格式,包含:商品名称name,链接url。自动设置对话框默认内容
if (this.isWidescreen && goods) {
// console.log(goods);
if (typeof goods != 'object') {
goods = JSON.parse(goods)
}
const {
name,
url
} = goods
if (name && url) {
setTimeout(()=>{
this.$refs['chat-view'].chatInputContent = '' + name + ':' + url + ''
}, 1000);
}
}
/**
* 在本页面链接传递参数 joinGroup=group_id即可申请加入群,
* 比如:http://localhost:8082/#/uni_modules/uni-im/pages/index/index?joinGroup=xxx
*/
if(joinGroup){
// #ifdef H5
//删除URL后面的参数(主要是删除joinGroup=xxx),且不刷新页面
history.pushState({}, '', '/#/');
// #endif
this.joinGroup(joinGroup)
};
},
search(e) {
// console.log("search-e: " + JSON.stringify(e));
uni.showToast({
title: '加好友功能入口,暂时在左侧菜单的通讯录中',
icon: 'none',
duration: 3000
});
},
addUser() {
uni.showToast({
title: '加好友功能入口,暂时在左侧菜单的通讯录中',
icon: 'none',
duration: 3000
});
},
showChatView() {
this.contactsViewIsShow = false
// 拿到所有存在未读消息的会话对象
const ucId = uniIm.conversation.dataList
.filter(item => item.unread_count > 0)
.filter(item => !item.mute)
.map(item => item.id)
if(ucId.length > 0){
let index = ucId.findIndex(item => item.id == this.currentConversation.id)
// 如果当前会话存在未读消息,则切换到下一个会话。如果当前会话不存在未读消息,则切换到第一个存在未读消息的会话
index >= 0 ? index ++ : index = 0;
this.toChat(ucId[index])
}
},
showContactsView() {
this.contactsViewIsShow = true
// 判断是不是第一次打开
if(!this.showContactsView.firstOpen){
this.showContactsView.firstOpen = true
this.$nextTick(() => {
const contactsRef = this.$refs['uni-im-contacts']
contactsRef.openPages(contactsRef.menuList[0])
})
}
},
toChatFiltered({ conversation_id, count, keyword }) {
this.chatInfoIsShow = false;
this.filteredConversationId = conversation_id
uniIm.currentConversationId = conversation_id
if (this.isWidescreen) { // 若为宽屏,则切换右侧的组件
this.$nextTick(() => {
let chatFilteredRef = this.$refs['chat-filtered']
if (chatFilteredRef) {
chatFilteredRef.load({
conversation_id,
keyword,
count,
})
}
})
} else { // 若为窄屏,则打开新窗体
uni.navigateTo({
url: '/uni_modules/uni-im/pages/chat/chat-filtered'
+ `?conversation_id=${conversation_id}`
+ `&keyword=${encodeURIComponent(keyword)}`
+ `&count=${count}`,
animationDuration: 300
})
}
},
async toChat(param) {
this.chatInfoIsShow = false;
// console.log('toChat param',param);
let conversation_id = await getConversationId(param)
this.keyword = ''
this.filteredConversationId = false
uniIm.currentConversationId = conversation_id
// console.log('conversation_id',conversation_id);
if(conversation_id.indexOf('group_') === 0){
uni.showLoading({
title: '加载中',
mask: false
});
const conversation = await uniIm.conversation.get(conversation_id)
uni.hideLoading()
}
if (this.isWidescreen) { // 若为宽屏,则切换右侧的组件
this.$nextTick(() => {
let chatViewRef = this.$refs['chat-view']
if (chatViewRef) {
chatViewRef.load(conversation_id)
}
})
} else { // 若为窄屏,则打开新窗体
uni.navigateTo({
url: '/uni_modules/uni-im/pages/chat/chat?conversation_id=' + conversation_id,
animationDuration: 300
})
}
async function getConversationId(param){
if (typeof param == 'string') {
return param
} else {
if (param.conversation_id) {
return param.conversation_id
} else if (param.group_id) {
return 'group_' + param.group_id
} else if (param.user_id || param.friend_uid) {
// 注:这里不使用静态方法,为了将打开的会话可能不存在的情况,需要本地创建的情况
return (await uniIm.conversation.get(param)).id
} else {
throw new Error("toChat param is error")
}
}
}
},
showChatInfo() {
this.chatInfoIsShow = !this.chatInfoIsShow
if(this.chatInfoIsShow){
this.$nextTick(()=>{
if(this.currentConversation.group_id){
this.$refs['group-info'].load(this.currentConversation.id)
}else{
this.$refs['chat-info'].load(this.currentConversation)
}
})
}
},
toUcenter() {
uni.navigateTo({
url: '/uni_modules/uni-id-pages/pages/userinfo/userinfo?showLoginManage=true',
complete(e) {
// console.log("e: " + JSON.stringify(e));
}
})
},
openConversationMenu(e,name){
const myContextmenu = this.$refs['uni-im-contextmenu']
let menuList = []
if(name == 'unreadMsgCount' && this.unreadMsgCount > 0){
menuList.push({
"title": "清空所有未读消息数",
"action": () => {
console.log('清空所有未读消息数')
uniIm.conversation.clearUnreadCount()
}
})
}
if(menuList.length == 0){
return
}
console.log('menuList.length',menuList.length)
myContextmenu.show({
"top": e.clientY + 35,
"left": e.clientX
}, menuList)
// myContextmenu.onClose(() => {
// console.log('关闭右键菜单')
// })
}
},
async onReachBottom() {
await this.$refs['uni-im-conversation-list']?.loadMore()
},
onNavigationBarButtonTap() {
uni.navigateTo({
url: '/uni_modules/uni-id-pages/pages/userinfo/userinfo?showLoginManage=true',
complete: e => {
// console.log(e);
}
});
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
@import url("./index.scss");
</style>
\ No newline at end of file
<template>
<view>
<unicloud-db ref="udb" @load="handleLoad" v-slot:default="{ data, loading, pagination, error, options }"
collection="uni-id-users" field="_id,nickname,avatar_file" :where="udbWhere">
<view v-if="error" class="error">
<text>{{ error.message }}</text>
</view>
<uni-list v-else>
<uni-list-chat v-for="(item, index) in data" :key="item._id" link
<uni-im-info-card v-for="(item, index) in data" :key="item._id" link
:title="item.nickname"
:avatar="item.avatar_file ? item.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'"
@click="toChat(item._id)"></uni-list-chat>
@click="toChat(item._id)"></uni-im-info-card>
</uni-list>
<uni-im-load-state :status="loading ? 'loading' : loadMoreStatus"></uni-im-load-state>
</unicloud-db>
......@@ -110,4 +111,5 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
};
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
</style>
\ No newline at end of file
......@@ -17,7 +17,6 @@ uni-im是云端一体的、全平台的、免费的、开源即时通讯系统
## 特点优势
- 性价比高;前后端代码均免费开源,相比竞品使用uni-im仅需花费极少的托管在uniCloud(serverless服务器)产生的费用[详情查看](#cost)
- 全端可用
- App端支持nvue,更好的长列表性能。list组件性能优势[详情参考](https://uniapp.dcloud.net.cn/component/list.html)
- 中心化响应式数据管理,切换会话无需重新加载数据,更流畅的体验
- App端聚合多个手机厂商推送通道,app不在线也可以收到消息
......
......@@ -360,7 +360,7 @@ export default class Message {
//重新拉取 群成员
await uniIm.group.loadMember(_currentConversation.group_id)
// 获取群公告
_currentConversation.unreadGroupNotification = _currentConversation.group_info.notification
_currentConversation.has_unread_group_notification = !!_currentConversation.group_info.notification
}else{
// 将新成员加入到群成员列表
for (let i = 0; i < new_member_list.length; i++) {
......@@ -390,7 +390,7 @@ export default class Message {
const {notification} = data.body.updateData
if(data.action === "update-group-info-notification"){
console.log('收到群公告');
_currentConversation.unreadGroupNotification = notification
_currentConversation.has_unread_group_notification = true
}
}else if(data.action === "set-group-admin"){
const {user_id,addRole,delRole} = data.body
......
......@@ -16,8 +16,4 @@ export default (version)=>{
// console.log('clear history storage success');
}
// #endif
// #ifdef APP
console.log('APP端未实现checkVersion,待补充')
// #endif
}
\ No newline at end of file
......@@ -112,9 +112,15 @@ export default async function (initParam) {
// 查询一次缺失的云端消息
modules.getCloudMsg()
// #endif
// 拿到当前页面的路由
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const isLoginPage = currentPage?.route.includes('uni_modules/uni-id-pages/pages/login')
// 第二次 activate 之后开始 检查token是否已经过期,如果已经过期则重定向至登录页面
const { tokenExpired } = uniCloud.getCurrentUserInfo()
if (count > 1 && tokenExpired < Date.now()) {
if (!isLoginPage && count > 1 && tokenExpired < Date.now()) {
console.info('uni-im检测到,当前用户登录过且当前登录状态已过期,将自动跳转至登录页面。')
uni.reLaunch({
url: '/uni_modules/uni-id-pages/pages/login/login-withpwd',
......@@ -130,7 +136,21 @@ export default async function (initParam) {
$state.onMsg = modules.msgEvent.onMsg
$state.offMsg = modules.msgEvent.offMsg
// #ifndef H5
// 提前拿到键盘高度,防止第一次在会话点击输入框时,输入框的高度弹起速度慢
$state.keyboardMaxHeight = uni.getStorageSync('uni-im-data-keyboard-max-height') || 0;
console.log('键盘高度', $state.keyboardMaxHeight);
// if ($state.keyboardMaxHeight === 0) {
// uni.onKeyboardHeightChange((e) => {
// if (e.height > $state.keyboardMaxHeight) {
// $state.keyboardMaxHeight = e.height;
// uni.setStorageSync('uni-im-data-keyboard-max-height', e.height);
// console.log('获取到键盘高度,并存储', e.height);
// }
// });
// }
// #endif
// #ifdef APP
// 初始化 本地数据库
......
......@@ -61,7 +61,12 @@ msgEvent.onMsg(async res=>{
} = msg
// console.log('msgmsgmsgmsgmsg.msg',msg);
// 拿到收到消息的会话对象
let conversation = await $state.conversation.get(conversation_id)
let conversation = $state.conversation.getCached(conversation_id)
let isNewCreateConversation = false
if (!conversation) {
isNewCreateConversation = true
conversation = await $state.conversation.get(conversation_id)
}
// 处理其他设备已读某会话的情况
if (msg.type == 'clear-conversation-unreadCount') {
......@@ -126,7 +131,9 @@ msgEvent.onMsg(async res=>{
isReadableMsg &&
// 消息不是系统配置了免打扰的
!isMuteMsg &&
msg.from_uid != uniCloud.getCurrentUserInfo().uid
msg.from_uid != uniCloud.getCurrentUserInfo().uid &&
// 新创建的会话直接读取云端的未读消息数,本地不需要 ++
!isNewCreateConversation
) {
conversation.unread_count++
}
......@@ -137,7 +144,7 @@ msgEvent.onMsg(async res=>{
}
// #ifdef APP
console.log('notification type=>',res.type);
// console.log('notification type=>',res.type);
if (res.type == 'click'){
let currentPages = getCurrentPages()
let topViewRoute = currentPages[currentPages.length - 1].route
......
......@@ -56,9 +56,9 @@ class Conversation {
}
return all
}, {})
this.tag = Object.keys(tags)
if (this.tag.length == 0) {
this.tag = ['群聊']
this.tags = Object.keys(tags)
if (this.tags.length == 0) {
this.tags = ['群聊']
}
// 3. 初始化字段:群简介、群公告、群头像
......
......@@ -106,7 +106,7 @@ export default {
conversationData.source = param.source
}
this.add(conversationData)
conversationData = this.add(conversationData)[0]
conversationDatas.push(conversationData)
} else {
throw new Error("未找到此群会话")
......@@ -118,12 +118,12 @@ export default {
// 指定获取某个id的群会话时,判断如果群会话的 群成员为空就从云端拉取
if (conversationData.group_id && Object.keys(conversationData.group_member).length == 0 && !conversationData
.leave) {
uni.showLoading({
title: '加载中',
mask: true
});
// uni.showLoading({
// title: '加载中',
// mask: true
// });
await $group.loadMember(conversationData.group_id)
uni.hideLoading()
// uni.hideLoading()
}
// console.log('conversationData*-*--*-**-',conversationData)
......@@ -163,6 +163,9 @@ export default {
}
// console.log('maxUpdateTime', maxUpdateTime);
try {
const uniImCo = uniCloud.importObject("uni-im-co",{
customUI: true
})
res = await uniImCo.getConversationList({
maxUpdateTime,
limit: 30,
......
......@@ -2,10 +2,24 @@ import {default as createObservable,$watch} from '@/uni_modules/uni-im/sdk/utils
import data from './data';
const observable = createObservable(data);
$watch(() => observable.conversation.dataList, (conversationDataList,old) => {
// console.log('会话数据变化',{conversationDataList,old})
let lastKey = ''
$watch(() => observable.conversation.dataList, (conversationDataList) => {
// 会话数据排序
conversationDataList.sort(function(a, b) {
const currentKey = sortConversationDataList(conversationDataList.slice()).map(item => item.id).join(',')
if (currentKey !== lastKey) {
// TODO:优化未知错误需要加setTimeout 0,使得在下一次js引擎的事件循环执行,后续可以考虑优化
setTimeout(()=>sortConversationDataList(conversationDataList),0)
lastKey = currentKey
}else{
// console.error('---无需重新排序')
}
},{
deep: true,
immediate: true
})
function sortConversationDataList(conversationDataList) {
return conversationDataList.sort(function(a, b) {
if (a.pinned != b.pinned) {
return a.pinned ? -1 : 1;
}
......@@ -16,30 +30,27 @@ $watch(() => observable.conversation.dataList, (conversationDataList,old) => {
}
return b.time - a.time
})
// 异步存到storage// TODO 暂时不离线存储会话数据
/*const {uid} = uniCloud.getCurrentUserInfo();
if (uid) {
uni.setStorage({
key: 'uni-im-conversation-list' + '_uid:' + uid,
data: conversationList.map(item => {
let _item = {}
for (let key in item) {
if (!["msgManager"].includes(key)) {
_item[key] = item[key]
}
// 清空防止 localStorage 的数据量过大。// 记录最后一个消息,用于会话列表显示last_msg_note,更多消息启动后再从缓存中读取
if (key === "msgList" && item.msgList.length != 0) {
_item[key] = [item.msgList[item.msgList.length - 1]]
}
}
// 异步存到storage// TODO 暂时不离线存储会话数据
/*const {uid} = uniCloud.getCurrentUserInfo();
if (uid) {
uni.setStorage({
key: 'uni-im-conversation-list' + '_uid:' + uid,
data: conversationList.map(item => {
let _item = {}
for (let key in item) {
if (!["msgManager"].includes(key)) {
_item[key] = item[key]
}
return _item
})
// 清空防止 localStorage 的数据量过大。// 记录最后一个消息,用于会话列表显示last_msg_note,更多消息启动后再从缓存中读取
if (key === "msgList" && item.msgList.length != 0) {
_item[key] = [item.msgList[item.msgList.length - 1]]
}
}
return _item
})
}*/
},{
deep: true,
immediate: true
})
})
}*/
export default observable;
\ No newline at end of file
.code-view-box ::v-deep {
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub Dark
Description: Dark theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-dark
Current colors taken from GitHub's CSS
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
}
\ No newline at end of file
......@@ -80,7 +80,6 @@ export default {
'safeArea',
'safeAreaInsets',
].forEach(name => { metrics[name] = $state.systemInfo[name] })
// #ifndef APP-NVUE
try {
uni.createSelectorQuery()
.select('uni-page-body > #page, uni-page-body > .page')
......@@ -96,7 +95,6 @@ export default {
} catch (e) {
console.error('调用 uni.createSelectorQuery 时机过早:', e)
}
// #endif
return metrics
},
isMuteMsg(msg){
......@@ -145,7 +143,8 @@ export default {
"location": "[位置]",
"system": "[系统通知]",
"code": "[代码]",
"rich-text": "[富文本消息]"
"rich-text": "[富文本消息]",
"revoke_msg": "[消息已被撤回]",
} [type] || `[${type}]`
if (type == "system") {
......
page ::v-deep {
uni-view,
uni-label,
uni-swiper-item,
uni-scroll-view {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-basis: auto;
align-items: stretch;
align-content: flex-start;
}
uni-button{
margin: 0;
}
uni-view,
uni-label,
uni-swiper-item {
flex-direction: column;
}
uni-view,
uni-image,
uni-input,
uni-scroll-view,
uni-swiper,
uni-swiper-item,
uni-text,
uni-textarea,
uni-video {
position: relative;
border: 0px solid #000000;
box-sizing: border-box;
}
uni-swiper-item {
position: absolute;
}
}
\ No newline at end of file
static/pinned.png

2.1 KB | W: | H:

static/pinned.png

2.6 KB | W: | H:

static/pinned.png
static/pinned.png
static/pinned.png
static/pinned.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -777,6 +777,7 @@ async function _processPush({ msgData, isMute, appId }) {
}
}
console.log('state : ============> ' + state);*/
res.data = {} // 不返回给客户端发送结果
} else if (msgData.group_id) {
// console.error('群聊消息*-----------------',pushParam);
// 如果是群聊则调用sendMsgToGroup云方法,递归发送(500个为用户一批)
......
......@@ -100,15 +100,6 @@ async function afterReadAction({
// 禁止昵称用邮箱 脱敏处理
item.nickname = hideEmailStr(item.nickname)
}
// 特殊处理崔博
if (item._id == "b9839630-a479-11ea-b772-0f6ad6cf835c") {
item.nickname = "DCloud地图服务"
item.avatar_file = {
url: "https://dcloud-chjh-web.oss-cn-hangzhou.aliyuncs.com/ext/uni-im/uni%402x.png"
}
}
})
function hideUsernameStr(username) {
......
......@@ -57,7 +57,7 @@ module.exports = {
},
async beforeUpdate({where,updateData,userInfo}){
//只开放部分字段
let canUpdateField = ["unread_count","pinned","hidden","mute"]
let canUpdateField = ["unread_count","pinned","hidden","mute","has_unread_group_notification"]
Object.keys(updateData).forEach(field=>{
if(!canUpdateField.includes(field)){
throw new Error('uni-im-conversation.schema.ext.js beforeUpdate:限制只能更新的字段:'+canUpdateField.join(''))
......@@ -76,7 +76,6 @@ module.exports = {
}).update({
is_read:true
})
// console.log('uni-im-conversation beforeUpdate res:',res);
}
},
......
......@@ -86,6 +86,11 @@
"bsonType": "bool",
"description": "是否免打扰(右键设置会话消息免打扰)",
"defaultValue": false
},
"has_unread_group_notification":{
"bsonType": "bool",
"description": "是否有未读的群通知",
"defaultValue": false
}
}
}
\ No newline at end of file
......@@ -48,6 +48,10 @@
"forceDefaultValue": {
"$env": "now"
}
}
},
"active_time": {
"bsonType": "timestamp",
"description": "活跃时间(最后一场发消息时间)"
}
}
}
\ No newline at end of file
......@@ -26,6 +26,11 @@ module.exports = {
if(field == 'notification'){
// 如果是群公告,增加一个公告的创建时间
updateData[field].create_time = Date.now()
await db.collection('uni-im-conversation').where({
group_id,
}).update({
has_unread_group_notification: true
})
}
},
async afterUpdate({where,updateData,clientInfo,userInfo}){
......