提交 2f3ac403 编写于 作者: DCloud_JSON's avatar DCloud_JSON

Initial commit

上级
uniCloud/cloudfunctions/common/uni-im-ext/_registered_ext.js
## 2.0.11(2023-06-12)
- 修复 当引用的内容较长且包含换行符时,引用内容会盖住输入框的问题
## 2.0.10(2023-05-25)
- 修复 时间错误
## 2.0.10(2023-05-25)
- 修复 Vue3-web-pc端 敲完回车会先执行换行再发送消息的问题
## 2.0.9(2023-05-25)
- 修复 因为`2.0.8`优化sqlite,引起的web端报`ReferenceError: sqlite is not defined`的问题
## 2.0.8(2023-05-24)
- 修复 app-android端 部分情况下发送消息会卡在发送中,再次点击雪花图标后才能发送的问题
## 2.0.7(2023-05-23)
- 修复 当聊天对话输入框,文字内容超过一行时,切换到语音输入模式;录音按钮位置不正确的问题。
- 修复 不选择任何好友直接创建群聊,客户端不显示创建者的加群记录的问题
- 优化 当会话为群聊时,标题栏显示群人数
- 优化 代码浏览功能的tab-size为4
## 2.0.6(2023-05-22)
- 修复 Vue3下报 `ReferenceError: Cannot access 'getCloudMsgIng' before initialization`的问题
## 2.0.5(2023-05-19)
- 修复 iOS端 应用切换到后台之后收到消息,再打开应用,部分情况下会丢消息的问题
- 修复 微信小程序端 播放语音报错的问题
- 修复 微信小程序端 发送视频,显示为文件格式,没有用video组件显示的问题
- 修复 微信小程序端 打开对话窗口 偶尔不能自动滚动到最新一条消息的问题
- 修复 微信小程序端 在非tabbar界面收到消息或系统通知后tabbar的角标不更新的问题
- 重构 app-nvue代码浏览模块
- 修复 云存储临时链接过期后播放视频语音等报错的问题
- 修复 部分情况下 tabbar角标不更新的问题
## 2.0.4(2023-04-24)
- 修复 web端 部分情况下 收到新消息需要延迟滚动到最新消息的问题
## 2.0.3(2023-04-20)
- 新增 限制只能撤回2分钟内的消息(群主不受任何限制)
- 修复 微信小程序端发送图片报错的问题
- 修复 Vue2 H5端dom加载慢时,showLast报错
- 修复 新发送的消息 时间不刷新的问题
## 2.0.2(2023-04-18)
- 修复 Vue2模式 聊天时间当消息过长会消失的问题
## 2.0.1(2023-04-17)
1. 修复 当用户接收到消息后关闭im,消息发送者再撤回消息。且在push指令离线消息时效过期后,用户再打开im,撤回无效的问题。
2. 修复 微信小程序端滑动快的时候会抖动的问题
3. 修复 部分情况下,群聊消息发不出去的问题
4. 修复 Vue2模式下 消息不满一屏插入消息无效
5. 修复 创建群聊时,如果没有选择任何用户。报res 不存在的错误
6. 修复 部分情况下群聊消息必须刷新后才能撤回的问题
## 2.0.0(2023-04-14)
【重要】v2版正式发布
## 1.6.3(2023-03-06)
- 新增 移动APP端,应用桌面角标数,动态同步未读消息数
- 修复 同一个账号同时在多台设备登录,其中一台设备发送消息,其他设备未同步消息的问题
- 修复 当应用被切换到后台时,应用进程未被关闭,但socket进程被关闭的情况下。切回到前台,期间的消息丢失的问题
## 1.6.2(2023-03-03)
- 修复 当项目一启动且token无效时,直达与某个用户对话。跳转至登录页面后返回会话页面报`无效的conversation_id`的问题
## 1.6.1(2023-02-27)
- 修复 因版本号1.5.9引起的微信小程序端拿不到globalData的问题
## 1.5.9(2023-02-24)
- 修复 群聊消息时间不显示的问题
- 修复 部分情况下 加好友不显示昵称的问题
- 修复 部分情况下 web手机端创建群聊后不会自动返回的问题
## 1.5.8(2023-02-23)
- 修复 部分情况下 非uniCloud项目接入uni-im 联登成功后报找不到uniIdCo的问题
## 1.5.7(2023-02-22)
- 更新 优化会话表查询性能,防止数据量大时慢查询
## 1.5.6(2023-02-20)
- 修复 部分情况下 群聊功能,提示有新用户进群的消息样式不正确的问题
## 1.5.5(2023-02-17)
- 修复 Vue2下不支持“可选链操作符”导致的报错问题
## 1.5.4(2023-02-16)
- 修复 pc端 当消息不满一页时,来回切换同一个用户 会一直提示正在加载历史消息的问题
## 1.5.3(2023-02-15)
- 修复 在safari浏览器下的兼容问题
- 修复 快速滚动消息列表 偶发加载不了更多消息的问题
## 1.5.2(2023-02-15)
- 修复 iOS端 部分情况下不会自动滚动到最后一条消息的问题
## 1.5.1(2023-02-14)
- 修复 部分情况下会话列表页面 最新一条消息不刷新,未读消息数不递增的问题
## 1.5.0(2023-02-11)
- 更新示例项目 演示分包加载 uni-im
- 更新 抽离聊天对话页面的消息列表,为独立组件; 分层简化代码 更清晰方便二开
- 修复 因iOS端 微信小程序平台 键盘弹出后 引起的输入框偶尔位置不正确的问题
## 1.4.4(2023-02-03)
- 更新示例项目 采用分包使用uni-id-pages
- 更新 默认不启用代码浏览模块
## 1.4.3(2023-01-29)
- 注释多余的`console.log`代码
## 1.4.2(2023-01-29)
- 优化 微信小程序平台 部分全面屏挡住UI操作不方便的问题
- 修复 因iOS端 微信小程序平台 键盘弹出后 调用 pageScrollTo 偶尔会导致 textarea 组件的 adjust-position=false 失效,而引起的 输入框错位的问题(兼容方案,后续微信小程序官方修复后可移除相关代码)
## 1.4.1(2023-01-28)
- 优化 切换发送消息类型的性能(软键盘不再频繁收起和弹出)
- 修复 iOS端 微信小程序平台 键盘弹出后 连续发送消息输入框跟随移动的问题
- 修复 iOS端 部分机型 发送语音功能 蒙版显示不完整的问题
- 修复 uni-im-co 某些情况下调用this.uniIdCommon 报错的问题
## 1.4.0(2023-01-18)
- 【重要】新增 群聊功能
- 【重要】新增 好友关系管理功能
**注意:** 这是一个不兼容的更新,需要执行jql修改相关字段,详情查看:[升级旧项目为 uni-im 1.4.0(群聊版) 注意事项](https://uniapp.dcloud.net.cn/uniCloud/uni-im.html#%E5%8D%87%E7%BA%A7%E6%97%A7%E9%A1%B9%E7%9B%AE%E4%B8%BA-uni-im-1-4-0-%E7%BE%A4%E8%81%8A%E7%89%88-%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)
## 1.3.3(2022-12-05)
- 新增 移动端支持emoji表情
## 1.3.2(2022-12-05)
- 修复 因vue2与vue3下 :key 的位置要求不同 引起的chat页面报错问题
## 1.3.1(2022-12-05)
- 修复 1.3.0版引起的截图粘贴发送失败的问题
- 修复 集成到 uni-admin 中样式设置失败的问题
## 1.3.0(2022-12-02)
- 修复 APP端当消息未满半屏时,弹出的键盘会将消息顶出界面外
- 新增 消息类型支持代码模式
- 新增 支持超长文本(仅代码模式支持,后续会支持普通消息模式)
- 新增 多媒体消息(含:语音、图片、视频、任意文件),App和小程序端支持使用第三方程序打开文件
## 1.2.1(2022-11-25)
- 修复 某些情况下 非uniCloud 开发的项目 接入uni-im 登录后会话列表不更新的问题
## 1.2.0(2022-11-23)
- 【重要】全端支持Vue3
- 修复 当历史消息超长时,APP端键盘弹起,不能滚动到最后一条消息
- 修复 键盘收起时,会自动滚动到最后一条消息的问题
- 修复 部分情况下,切换登录的账号,会话列表没有更新的问题
## 1.1.2(2022-11-21)
修复 某些情况下 iOS端 输入框内容发生变化时 页面重新排版,导致输入框被键盘挡住的问题
## 1.1.1(2022-11-18)
修复 向长时间未登录的用户(push_clientid已过期)发送消息,引起的报错问题。将数据写入云数据库,当用户再次登录时从服务端拉取
## 1.1.0(2022-11-18)
- 新增 支持 非uniCloud(比如:应用服务端的开发语言是php、java、go、c#、python等)或 不基于uni-id-pages 开发的项目 接入uni-im
- 简化部署流程 app.vue 页面仅需init uni-im即可(更加模块化,内部:监听应用生命周期onShow、onHidden实现相关功能、初始化依赖的globalData等)
## 1.0.3(2022-11-15)
降低uni-im使用的HBuilderX版本为`3.6.4`。 注意**APP端**:仅支持Vue2,且HBuilderX的版本为3.6.9+,否则chat页面存在滚动锚定问题(后续会修复此问题)
## 1.0.2(2022-11-14)
使用 1.2.3 版的 uni-list-chat 解决部署在腾讯云版uniCloud的uni-im项目 头像不能显示的问题
## 1.0.1(2022-11-14)
修复 因nvue下行间样式无法覆盖导致的 样式错误
## 0.0.1(2022-11-04)
init
\ No newline at end of file
<template>
<view id="uni-im-chat-input-box">
<div contenteditable="true" id="uni-im-chat-input" @blur="onBlur" @focus="onFocus" @input="onInput">
</div>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
let inputElement;
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
}
},
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]);
}
}
} else if (node.nodeType === 3) {
arr.push(node.nodeValue);
}
}
await deep(div);
return arr.join('');
}
},
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(() => {});
}
},
focus(focus, oldFocus) {
if (focus) {
inputElement.focus();
} else {
inputElement.blur();
}
}
},
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();
},
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('截流');
}else{
// console.log('不截流');
this.lock = true;
}
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);
},
setContent(data) {
if(typeof data === 'string'){
this.setHtml(data);
}else{
inputElement.innerHTML = this.arrDomJsonToHtml(data);
this.exportValue();
}
},
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>
<style lang="scss">
#uni-im-chat-input-box {
flex: 1;
overflow-y: auto;
}
#uni-im-chat-input {
flex: 1;
}
#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;
}
</style>
\ No newline at end of file
<template>
<view class="root" v-if="isShow" @contextmenu.prevent.native="closeMe">
<view class="uni-im-contextmenu-mark" @touchstart.prevent="closeMe" @click="closeMe"></view>
<!-- 注意微信小程序下style不支持传obj -->
<view class="contextmenu" ref="contextmenu" :style="{'left':style.left,'top':style.top,'opacity':style.opacity}">
<text class="item" v-for="(item,index) in menuList" :key="index" @click="doAction(item.action)">{{item.title}}</text>
</view>
</view>
</template>
<script>
// let funList = {}
let onCloseFnList = [];
export default {
data() {
return {
"isShow":false,
"style":{
"top":"",
"left":"",
"opacity":1
},
"menuList":[
{
"title":"title",
"action":"defaultAction"
},
{
"title":"title2",
"action":"defaultAction2"
},
{
"title":"测试其中一个特别长",
"action":"defaultAction2"
}
]
}
},
methods: {
onClose(fn){
onCloseFnList.push(fn)
},
doAction(actionName){
actionName()
this.closeMe()
},
show(position,menuList){
let {
top = '',
left = '',
estimateWidth = 150, // 预估尺寸应该不小于实际渲染尺寸
estimateHeight = 150,
} = position
// 根据给定的位置和尺寸,结合窗口大小,计算出 top/left 定位参数,
// 确保不超出窗口边界。
let calcPosition = (top, left, width, height) => {
let { windowWidth, windowHeight } = uni.getWindowInfo()
// console.log({windowWidth,windowHeight,top, left, width, height})
let style = {}
if (top) {
if (top > windowHeight - height) {
style.top = `${windowHeight - height}px`
} else {
style.top = `${top}px`
}
}
if (left) {
if (left > windowWidth - width) {
style.left = `${windowWidth - width}px`
} else {
style.left = `${left}px`
}
}
return style
}
// 根据预估的组件大小初步设定组件的位置
this.style = calcPosition(top, left, estimateWidth, estimateHeight)
this.isShow = true
this.menuList = menuList
this.style.opacity = 0
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(){
this.isShow = false
onCloseFnList.forEach(fn => {
if(typeof fn === 'function'){
fn()
}
})
onCloseFnList = []
// console.log('closeMe');
}
}
}
</script>
<style scoped lang="scss">
.root{
}
.contextmenu{
position: fixed;
background-color: #fff;
z-index: 99999;
border-radius: 10px;
box-shadow: 0 0 10px #888;
background-color:#FFF;
// opacity: 0.98;
border: 1px solid #999;
padding: 3px;
flex-direction: column;
}
.item{
padding: 5px 10px;
font-size: 18px;
border-radius: 5px;
margin: 5px;
opacity: 0.9;
text-align: left;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
/* #ifdef APP-NVUE */
width: 120px;
height: 40px;
/* #endif */
}
.item:hover{
background-color: #1099ff;
color: #fff;
}
.uni-im-contextmenu-mark{
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 */
background-color: rgba(0,0,0,0.5);
}
</style>
export default function changeMute(conversation) {
conversation.mute = !conversation.mute
const db = uniCloud.database();
db.collection('uni-im-conversation')
.doc(conversation._id)
.update({
"mute": conversation.mute
})
.then((e) => {
console.log('updated 消息免打扰设置', e.result.updated, conversation._id)
})
.catch(() => {
uni.showToast({
title: '服务端错误,消息免打扰设置失败,请稍后重试',
icon: 'none'
});
conversation.mute = !conversation.mute
})
}
<template>
<view>
<!-- #ifdef H5 -->
<scroll-view
ref="conversation-list"
class="conversation-list"
: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"
>
<!-- #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>
</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>
<!-- #ifndef H5 -->
</uni-list>
<!-- #endif -->
<!-- #ifdef H5 -->
</scroll-view>
<!-- #endif -->
<!-- #ifdef H5 -->
<uni-im-contextmenu ref="uni-im-contextmenu" />
<!-- #endif -->
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const db = uniCloud.database();
let currentScrollTop = 0;
export default {
props: {
keyword: {
type: String,
default: ''
},
activeConversationId: {
type: [String, Boolean],
default: ''
},
canCheck: {
type: Boolean,
default: false
},
checkedList: {
type: Array,
default: () => []
}
},
data() {
return {
listScrollTop: 0,
}
},
computed: {
conversationHasMore() {
return uniIm.conversation.hasMore
},
// #ifdef APP-NVUE
wHeight() {
return uni.getSystemInfoSync().windowHeight + 'px'
},
// #endif
loadMoreContentText() {
return {
contentrefresh: "正在加载...",
contentnomore: (this.conversationList.length ? "没有更多数据了" : "没有会话数据")
}
},
conversationList() {
return uniIm.conversation.dataList
// #ifdef H5
.filter(i => i.title.toLowerCase().includes(this.keyword.toLowerCase()))
// #endif
.filter(i => !i.hidden)
}
},
watch: {
conversationList: {
handler() {
// console.log('更新会话列表',this.conversationList)
this.$emit('change', this.conversationList)
},
deep: true
},
activeConversationId(activeConversationId) {
// #ifdef H5
// 重试次数
let tryIndex = 0
const scrollToCurrentConversation = () => {
if (tryIndex > 5) {
return
}
const query = uni.createSelectorQuery().in(this);
query.select('#' + activeConversationId).boundingClientRect(data => {
if (!data) {
// console.log('找不到 showMsgByIndex #'+activeConversationId,'tryIndex:'+tryIndex);
tryIndex++
setTimeout(() => scrollToCurrentConversation(), 300)
return
} else {
// console.log(data);
}
let listHeight = document.querySelector('.conversation-list')?.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()
}
scrollToCurrentConversation()
// #endif
}
},
mounted() {},
methods: {
onScroll(e) {
currentScrollTop = e.detail.scrollTop
this.$emit('onScroll', e)
},
isCheck(item) {
return this.checkedList.some(i => i.id === item.id)
},
clickItem(item) {
if (this.canCheck) {
// 判断是否选中
let checkedList = this.checkedList;
if (this.isCheck(item)) {
checkedList.splice(checkedList.findIndex(i => i.id === item.id), 1)
} else {
checkedList.push(item)
}
this.$emit('update:checkedList', checkedList)
}
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 position = {
"top": e.clientY + 35,
"left": e.clientX
}
let menuList = [{
"title": "置顶",
"action": () => {
conversation.pinned = !conversation.pinned
db.collection('uni-im-conversation')
.doc(conversation._id)
.update({
"pinned": conversation.pinned
})
.then((e) => {
console.log('updated 置顶', e.result.updated, conversation._id)
})
.catch(() => {
uni.showToast({
title: '服务端错误,置顶失败,请稍后重试',
icon: 'none'
});
conversation.pinned = !conversation.pinned
})
}
},
{
"title": conversation.mute ? "允许消息通知" : "消息免打扰",
"action": () => {
console.log('mute 允许消息通知 / 消息免打扰')
conversation.changeMute()
}
},
// {
// "title":"复制会话id",
// "action":()=>{
// uni.setClipboardData({
// data:conversation.id,
// showToast:false
// })
// }
// },
{
"title": "移除会话",
"action": () => conversation.hide()
}
]
if (conversation.pinned) {
menuList[0].title = "取消置顶"
}
myContextmenu.show(position, menuList)
myContextmenu.onClose(() => {
conversation.focus = false
})
// #endif
},
async loadMore() {
let data = await uniIm.conversation.loadMore()
// console.log('加载到新的会话数据',data);
return data
}
}
}
</script>
<style lang="scss" scoped>
.conversation-list,
.tip {
flex: 1;
}
.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;
}
.check-box {
border: 1px solid #ccc;
width: 20px;
height: 20px;
border-radius: 2px;
margin-right: 10px;
}
.check-box.checked {
background-color: #00a953;
border-color: #00a953;
}
</style>
\ No newline at end of file
<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"
@click="handleClick"
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>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import {
markRaw
} from "vue";
export default {
name: 'UniImConversation',
props: {
conversation: {
type: Object,
default: () => {}
},
canCheck: {
type: Boolean,
default: false
}
},
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 '[已禁言]'
}
}
},
emits: ['click'],
data() {
// 调用扩展点,扩展程序可以为该会话增加覆盖的图标元素。
let avatarOverlayList = uniIm.extensions
.invokeExts("conversation-avatar-overlay", this.conversation)
.filter((result) => result && result.component)
.map((result) => {
return {
component: markRaw(result.component),
props: result.props || {},
handlers: result.handlers || {},
};
});
return {
avatarUrl: "/uni_modules/uni-im/static/avatarUrl.png",
avatarOverlayList
};
},
watch: {
'conversation.avatar_file': {
async handler(avatar_file) {
if (typeof avatar_file == 'object' && avatar_file.url) {
this.avatarUrl = await uniIm.utils.getTempFileURL(avatar_file.url);
}
},
immediate: true,
},
},
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>
<view v-if="loading" class="hint">正在查找……</view>
<scroll-view v-else class="conversation-list" ref="filtered-conversation-list" scroll-y="true">
<template v-if="noMatched">
<view class="hint">没有匹配的内容</view>
</template>
<template v-if="matchedFriends.data?.length">
<view class="category-name">联系人</view>
<uni-list-chat
class="conversation-list-item"
v-for="item in matchedFriends.data"
:key="item.id"
:title="item.title"
note=" "
:avatar="item.avatar_file?.url || '/uni_modules/uni-im/static/avatarUrl.png'"
clickable
@click="$emit('to-chat', {friend_uid:item.friend_uid})"
/>
<view v-if="matchedFriends.loading" class="category-loadmore">正在加载……</view>
<view v-else-if="matchedFriends.hasMore || matchedFriends.localMore" class="category-loadmore">
<view
class="btn-loadmore"
@click="loadMoreFriends"
>显示更多</view>
</view>
</template>
<template v-if="matchedGroups.data?.length">
<view class="category-name">群聊</view>
<uni-list-chat
class="conversation-list-item"
v-for="item in matchedGroups.data"
:key="item.id"
:title="item.title"
note=" "
:avatar="item.avatar_file?.url || '/uni_modules/uni-im/static/avatarUrl.png'"
:tags="['群聊']"
clickable
@click="$emit('to-chat', {group_id:item.group_id})"
/>
<view v-if="matchedGroups.loading" class="category-loadmore">正在加载……</view>
<view v-else-if="matchedGroups.hasMore || matchedGroups.localMore" class="category-loadmore">
<view
class="btn-loadmore"
@click="loadMoreGroups"
>显示更多</view>
</view>
</template>
<template v-if="matchedConversations.data?.length">
<view class="category-name">聊天记录</view>
<uni-list-chat
class="conversation-list-item"
v-for="item in matchedConversations.data"
:key="item.id"
:title="item.title"
:note="`${item.count}条相关聊天记录`"
:avatar="item.avatar_file?.url || '/uni_modules/uni-im/static/avatarUrl.png'"
:tags="item.type==2 ? ['群聊'] : []"
clickable
@click="$emit('to-chat-filtered', { conversation_id: item.id, count: item.count, keyword: keyword })"
/>
<view v-if="matchedConversations.loading" class="category-loadmore">正在加载……</view>
<view v-else-if="matchedConversations.hasMore || matchedConversations.localMore" class="category-loadmore">
<view
class="btn-loadmore"
@click="loadMoreConversations"
>显示更多</view>
</view>
</template>
</scroll-view>
</view>
</template>
<script>
// 防抖
function debounce(delay = 200) {
let timer = null
let busy = false
let last_kw = ''
let debouncedFn = function(kw, cb) {
last_kw = kw
if (busy) {
// 如果上一次请求还没完成,则本次不执行,只记录参数,事后补发
return
}
if (timer) {
// 如果还在防抖期间,则重新计算防抖期
clearTimeout(timer)
}
timer = setTimeout(async () => {
// 防抖时间到,调用 cb 执行请求
let kw = last_kw
timer = null
busy = true
last_kw = ''
await cb(kw)
busy = false
// 如果执行期间又有请求进来,则补发请求
if (last_kw) {
debouncedFn(last_kw, cb)
}
}, delay)
}
return debouncedFn
}
const debouncedSearch = debounce()
const uniImCo = uniCloud.importObject("uni-im-co", {
customUI: true
})
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
emits:['to-chat', 'to-chat-filtered'],
props: {
keyword: {
type: String,
default: ''
},
},
data() {
return {
loading: true,
matchedFriends: {},
matchedGroups: {},
matchedConversations: {},
canCheck: false,
}
},
computed: {
noMatched() {
return this.matchedFriends.data.length == 0
&& this.matchedGroups.data.length == 0
// && this.matchedConversations.data.length == 0
}
},
watch: {
keyword: {
handler(keyword) {
debouncedSearch(keyword, async (keyword) => {
this.loading = true
let {
matchedFriends,
matchedGroups,
matchedConversations
} = await uniImCo.getFilteredConversationList({ keyword })
this.matchedFriends = matchedFriends
this.matchedGroups = matchedGroups
this.matchedConversations = matchedConversations
if (this.matchedFriends.data.length > 5) {
this.matchedFriends.localMore = this.matchedFriends.data.splice(5)
}
if (this.matchedGroups.data.length > 5) {
this.matchedGroups.localMore = this.matchedGroups.data.splice(5)
}
if (this.matchedConversations.data.length > 5) {
this.matchedConversations.localMore = this.matchedConversations.data.splice(5)
}
this.loading = false
})
},
immediate: true
}
},
methods: {
loadMoreFriends() {
this._loadMore(this.matchedFriends, 'getSingleConversationsByFriendName')
},
loadMoreGroups() {
this._loadMore(this.matchedGroups, 'getGroupConversationsByName')
},
loadMoreConversations() {
this._loadMore(this.matchedConversations, 'getConversationsByMessage')
},
async _loadMore(list, method) {
if (list.localMore) {
list.data.push(...list.localMore)
delete list.localMore
return
}
if (list.loading) return
if (!list.hasMore) return
list.loading = true
let more = await uniImCo[method]({
keyword: this.keyword,
skip: list.skip,
})
list.data.push(...more.data)
list.hasMore = more.hasMore
list.skip = more.skip
list.loading = false
}
}
}
</script>
<style lang="scss" scoped>
.hint {
text-align: center;
color: #aaa;
}
.category-name {
font-size: 14px;
color: #999;
margin: 0.3em 0.5em;
}
.category-loadmore {
background-color: #fff;
padding: 6px 15px 10px;
flex-direction: row;
font-size: 12px;
color: #576b95;
}
.btn-loadmore {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.btn-loadmore:hover {
color: #7c8cae;
}
.conversation-list,
.tip {
flex: 1;
}
.conversation-list .conversation-list-item.focus {
border: 2px solid #1ab94e;
}
.conversation-list .conversation-list-item:hover {
background-color: #f8f8f8;
}
.conversation-list .conversation-list-item {
/* #ifdef H5 */
cursor: pointer;
/* #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;
}
/* #endif */
.conversation-list .conversation-list-item.activeConversation {
background-color: #f8f8f8;
}
.check-box{
border: 1px solid #ccc;
width: 20px;
height: 20px;
border-radius: 2px;
margin-right: 10px;
}
.check-box.checked{
background-color: #00a953;
border-color: #00a953;
}
</style>
<template>
<view class="system-msg-box">
<view class="system-msg group-notification">
<view class="title-box">
<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>
<text class="create_time">公告时间:{{friendlyTime}}</text>
</view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
data() {
return {
notification:{
content:"",
create_time:0
}
}
},
props: {
content:{
type: String,
default:""
},
create_time:{
type: Number,
default:0
}
},
mounted() {
this.notification.content = this.content
this.notification.create_time = this.create_time
},
computed: {
friendlyTime() {
return uniIm.utils.toFriendlyTime(this.notification.create_time || Date.now())
}
}
}
</script>
<style lang="scss">
.system-msg-box{
margin: 0 150rpx;
}
.hidden {
height: 0;
}
.system-msg {
background-color: #f2f2f2;
color: #9d9e9d;
font-size: 14px;
line-height: 30px;
padding: 0 15rpx;
border-radius: 8px;
}
.group-notification {
padding:14px 16px;
background-color: #FFFFFF;
width: 600rpx;
font-size: 18px;
margin-top: 10px;
}
.group-notification .title-box{
flex-direction: row;
}
.group-notification .title-box .title{
padding-left: 5px;
color: #888;
}
.group-notification .content{
color: #555;
padding: 6px 0;
/* #ifndef APP-NVUE */
word-break: break-all;
/* #endif */
}
</style>
\ No newline at end of file
<template>
<text :style="{ color: color, 'font-size': iconSize }" @click="_onClick" class="uni-im-icons">{{unicode}}</text>
</template>
<script>
const getVal = (val) => {
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() {
return {
}
},
props: {
code: {
type: String,
default(){
return ''
}
},
color: {
type: String,
default: '#333333'
},
size: {
type: [Number, String],
default: 16
},
},
computed:{
unicode(){
return unescape(`%u${this.code}`)
},
iconSize(){
return getVal(this.size)
}
},
methods:{
_onClick(e) {
this.$emit('click',e)
}
}
}
</script>
<style>
.uni-im-icons {
font-family: uni-im-icons !important;
font-size: 16px;
font-style: normal;
/* #ifdef H5 */
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');
}
.uni-im-share:before {
content: "\e6c4";
}
.uni-im-copy:before {
content: "\e67e";
}
.uni-im-delete:before {
content: "\e63d";
}
/* #endif */
</style>
<template>
<image @load="load" :src="url" :mode="mode" :style="{width,height}" @click="handleClick"></image>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
emits: ['click'],
props: {
src: {
type: String,
default: ''
},
mode: {
type: String,
default: ''
},
maxWidth: {
type: [String,Boolean],
default: false
},
},
data() {
return {
width:"1px",
height:"1px",
url: ''
}
},
watch: {
src:{
async handler(src) {
if(src){
this.url = await uniIm.utils.getTempFileURL(src)
// src 是cloud://开头的云存储地址,是腾讯云,否则是阿里云
if(src.indexOf('cloud://') === 0){
this.url += '?imageMogr2/thumbnail/400x400>'
}else if(src.indexOf('http') === 0){
// 因为还可能是 base64 blob 的本地图片,所以这里判断是不是http开头的
this.url += '?x-oss-process=image/resize,w_100/quality,q_80'
}
}
},
immediate: true
}
},
methods: {
load(e){
// console.log('img load',e)
this.width = e.detail.width
const maxWidth = uni.upx2px(parseInt(this.maxWidth))
// console.log('this.width',this.width)
// console.log('maxWidth',maxWidth)
if(maxWidth && this.width > maxWidth){
this.width = maxWidth + 'px'
this.height = maxWidth * e.detail.height / e.detail.width + 'px'
// console.error('超了',this.width,this.height)
}else{
this.width = e.detail.width + 'px'
this.height = e.detail.height + 'px'
}
},
handleClick(){
this.$emit('click')
}
}
}
</script>
<style>
</style>
<template>
<view class="refresh-box">
<image v-if="status == 'loading'" class="refresh-icon" src="/uni_modules/uni-im/static/run.gif" mode="widthFix"></image>
<text class="refresh-text">{{status == 'loading'?contentText.contentrefresh:contentText.contentnomore}}</text>
</view>
</template>
<script>
export default {
props: {
status: {
type: String,
default: "loading"
},
contentText: {
type: Object,
default () {
return {
"contentrefresh": "加载中...",
"contentnomore": "- 没有相关数据 -"
}
}
}
}
}
</script>
<style>
/* #ifndef APP-NVUE */
view {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* #endif */
.refresh-box {
height: 50px;
justify-content: center;
align-items: center;
flex-direction: row;
/* #ifdef APP-NVUE */
width: 750rpx;
/* #endif */
}
.refresh-icon {
width: 25px;
height: 25px;
margin: 5px;
}
.refresh-text {
color: #bbb;
font-size: 14px;
}
</style>
\ No newline at end of file
<template>
<!-- #ifdef APP-NVUE -->
<cell :keep-scroll-position="true">
<slot></slot>
</cell>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<view>
<slot></slot>
</view>
<!-- #endif -->
</template>
<script>
export default {
data() {
return {}
},
props: {
},
computed:{
},
methods:{
}
}
</script>
<style>
</style>
<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>
<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() {
}
}
</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 */
.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>
此差异已折叠。
<template>
<view v-if="isShow" :style="{opacity}" id="popup-control">
<view class="control-mark" @touchstart.prevent="closeMe" @click="closeMe" :style="{opacity}">
</view>
<view ref="content" class="content" :style="{top:controlData.top,left:controlData.left,right:controlData.right,opacity}">
<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>
<text class="control-item-text">{{item.title}}</text>
</view>
</template>
</view>
<view class="icon" :class="{isInTop:controlData.isInTop}" :style="{right:iconBoxRight,left:iconBoxLeft,top:controlData.top,opacity}"></view>
</view>
<!-- todo:多个节点都放:style="{opacity}"是为了解决nvue下,当前情况外层不能控制内层透明度的问题 -->
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
data(){
return {
controlList:[],
isShow:false,
controlData: {
top:'',
bottom:'',
left:'unset',
right:'unset',
width:'',
msg:{},
msgContentDomInfo:{},
isInTop:false
},
opacity:0
}
},
computed: {
...uniIm.mapState(['isWidescreen']),
iconBoxLeft(){
if(this.controlData.left != 'unset'){
const {left:mLeft,width:mWidth} = this.controlData.msgContentDomInfo
return mLeft + mWidth/2 + 'px'
}else{
return ''
}
},
iconBoxRight(){
if(this.controlData.right != 'unset'){
const {left:mLeft,right:mRight,width:mWidth} = this.controlData.msgContentDomInfo
const metrics = uniIm.utils.getScreenMetrics()
return metrics.pageWidth - mRight + mWidth/2 + 'px'
}else{
return ''
}
}
},
mounted() {},
methods:{
chooseMore(){
this.$emit('chooseMore',[this.controlData.msg])
},
share(e){
// 仅支持web pc端
if(!uniIm.isWidescreen){
uni.showToast({
title:'仅支持web pc端',
icon:'none'
})
return
}
this.$emit('share',[this.controlData.msg])
},
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.revokeMsg(),
canDisplay:this.canRevoke,
},
// {
// title:'删除',
// action:()=>this.deleteMsg(),
// canDisplay:msg._id != undefined,
// },
{
title:'转发',
action:()=>this.share(),
canDisplay:this.isWidescreen && msg._id != undefined,
},
{
title:'多选',
action:()=>this.chooseMore(),
canDisplay:this.isWidescreen && msg._id != undefined,
}
]
// 拿到扩展点的数据
let extensionsControlList = uniIm.extensions.invokeExts('msg-popup-controls',msg)
this.controlList = this.controlList.concat(...extensionsControlList)
this.controlList.map(item=>{
const oldAction = item.action
item.action = ()=>{
this.isShow = false
oldAction()
}
})
},
async show({isSelf,msg,msgContentDomInfo}){
this.initControlList(msg)
// 先显示出来,设置透明度为0,拿到宽度,再设置透明度为1
this.opacity = 0
this.controlData.msg = msg
this.isShow = true
await this.$nextTick()
// #ifdef H5
// 当蒙版弹出,鼠标右键就关闭msg-popup-control
const popupControl = document.getElementById('popup-control')
popupControl.addEventListener('contextmenu',(e) => {
if(this.isShow){
this.isShow = false
}
e.preventDefault()
})
// #endif
const controlData = {
msgContentDomInfo,
msg,
isInTop: false
}
// #ifndef APP-NVUE
const query = uni.createSelectorQuery().in(this);
await new Promise(resolve => {
query.selectAll('.content').boundingClientRect(data => {
controlData.width = data[0].width + 'px'
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)
let metrics = uniIm.utils.getScreenMetrics()
if (isSelf) {
controlData.left = 'unset'
const metrics = uniIm.utils.getScreenMetrics()
// console.log('msgContentDomInfo',msgContentDomInfo)
controlData.right = metrics.pageWidth - msgContentDomInfo.right + msgContentDomInfo.width/2 - parseInt(controlData.width)/2 + 'px'
} else {
controlData.left = msgContentDomInfo.left + msgContentDomInfo.width / 2 - parseInt(controlData.width)/2 + 'px'
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 + 'px'
} else {
// #ifdef APP
let n = 8
// #endif
// #ifdef H5
let n = 55
// #endif
// #ifdef MP
let n = 10
// #endif
controlData.top = msgContentDomInfo.bottom + n + 'px'
}
if(parseInt(controlData.right) < 60){
controlData.right = '60px'
}
if(parseInt(controlData.left) < msgContentDomInfo.left){
controlData.left = msgContentDomInfo.left + 'px'
}
// console.error('dddddddta',controlData)
this.controlData = controlData
this.$nextTick(()=>{
this.opacity = 1
})
},
copyContent(){
// console.log('setClipboardData');
// console.log('this.controlData',this.controlData);
let data = this.controlData.msg.body
switch (this.controlData.msg.type){
case 'userinfo-card':
data = location.origin + '/#/?user_id='+this.controlData.msg.body.user_id
break;
case 'rich-text':
data = JSON.stringify({"uni-im-rich-text-data":data})
break;
default:
break;
}
// #ifdef H5
if (this.controlData.msg.type === 'rich-text') {
// 如果富文本消息是纯图片,则直接把图片内容放入剪贴板
let [n1, n2, n3] = this.controlData.msg.body
if (
n1?.type === 'text' && n1?.text === '' &&
n2?.name === 'img' && n2?.attrs?.src?.startsWith('data:image/') &&
n3?.type === 'text' && n3?.text === ''
) {
// data:image/png;base64,iVBORw0K...
let dataurl = n2.attrs.src
let m = /^data:(image\/.*);base64,(.*)$/.exec(dataurl)
if (m) {
let [_, mime, base64] = m
let byteCharacters = atob(base64)
let byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
let byteArray = new Uint8Array(byteNumbers)
let blob = new Blob([byteArray], { type: mime })
let file = new File([blob], mime.replace('/', '.'), { type: mime })
let clipboardItem = new ClipboardItem({ [mime]: file })
navigator.clipboard.write([clipboardItem])
uni.hideToast()
return
}
}
}
if (this.controlData.msg.type === 'image') {
let { name, url } = this.controlData.msg.body
fetch(url).then(response => response.blob()).then(blob => {
let mime = blob.type
let file = new File([blob], mime.replace('/', '.'), { type: mime })
let clipboardItem = new ClipboardItem({ [mime]: file })
navigator.clipboard.write([clipboardItem])
})
uni.hideToast()
return
}
// #endif
uni.setClipboardData({
data,
complete:(e)=> {
uni.hideToast()
// console.log(e);
}
})
},
canRevoke() {
let current_uid = uniCloud.getCurrentUserInfo().uid
let {group_id,from_uid,conversation_id,create_time} = this.controlData.msg || {}
// console.log('create_time',create_time,'group_id',group_id);
// console.log('from_uid current_uid',this.controlData.msg,from_uid,current_uid);
// console.log('this.controlData.msg.state',this.controlData.msg);
//自己发的,状态 不是发送成功不能撤回 (发送成功的数据才有_id)
if(!this.controlData.msg._id){
return false
}
if(this.uniIDHasRole('uni-im-admin')){
// 如果是管理员
return true
}
let isGroupAdmin = false
if(group_id){
let conversation = uniIm.conversation.dataList.find(i=>i.id == conversation_id)
isGroupAdmin = conversation.group_info.user_id == current_uid
}
// console.log('isGroupAdmin',isGroupAdmin);
// 如果是群主
if(isGroupAdmin){
return true
}
// 以上都不是,这需要是消息的发送者,且消息创建时间小于2分钟
return from_uid == current_uid && ( Date.now() - create_time < 1000*60*2 )
},
async revokeMsg(){
// 点击后直接关闭,异步提示撤回情况
// 再判断一遍防止,分钟在2分钟的时候右键了,然后到了第3分钟才点下的情况
if(this.canRevoke()){
const {conversation_id,_id:msg_id} = this.controlData.msg
const conversation = await uniIm.conversation.get(conversation_id)
conversation.revokeMsg(msg_id)
}else{
uni.showToast({
title: '已超过2分钟,不能撤回',
icon: 'none'
});
}
// console.log('this.controlData.msg',this.controlData.msg);
},
async answer(){
// console.log('answer')
this.$emit('answer',this.controlData.msg._id)
},
async deleteMsg(){
// #ifndef H5
return this.other()
// #endif
this.controlData.msg.is_delete = true
// 存到本地
let conversation = await uniIm.conversation.get(this.controlData.msg.conversation_id)
conversation.msgManager.localMsg.update(this.controlData.msg.unique_id,this.controlData.msg)
},
other(){
uni.showToast({
title: '暂不支持',
icon: 'none'
});
},
closeMe(evt){
// 触摸屏忽略 click 事件
if (uniIm.isTouchable && evt.type === 'click') return
this.isShow = false
}
}
}
</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;
// width: 375rpx;
position:fixed;
top:0;
border-radius: 5px;
flex-direction: row;
justify-content: space-around;
align-items: center;
/* #ifndef APP-NVUE */
z-index: 9999;
/* #endif */
}
.control-item{
padding: 0 15px;
justify-content: center;
align-items: center;
}
.control-item-text{
font-size: 12px;
color:#FFFFFF;
margin-top: 1px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
/* #ifdef H5 */
.control-item-text:hover{
color:#c9e7ff;
}
/* #endif */
.control-mark{
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 */
background-color: rgba(0,0,0,0.1);
}
.icon {
position: fixed;
transform:translate(0,-5px) rotate(45deg);
background-color: #252a30;
width: 10px;
height: 10px;
/* #ifndef APP-NVUE */
z-index: 9999;
/* #endif */
}
.isInTop{
transform:translate(0,50px) rotate(45deg);
}
/* #ifdef H5 */
@media screen and (min-device-width:960px){
.control-mark {
background-color: rgba(255,255,255,0.05);
}
}
/* #endif */
</style>
\ No newline at end of file
<template>
<view class="code-view-root" >
<view class="copy-btn-box" @click.stop>
<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 -->
<!-- #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 -->
</scroll-view>
<view class="show-full-btn" v-if="!isWidescreen && showFullBtn && overflow" @click="toCodePage">
<text class="show-full-text">全屏查看</text>
</view>
</view>
</template>
<script>
// hljs是由 Highlight.js 经兼容性修改后的文件,请勿直接升级。否则会造成uni-app-vue3-Android下有兼容问题
import hljs from "@/uni_modules/uni-im/sdk/utils/highlight/highlight-uni.min.js";
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
// 初始化 MarkdownIt库
const MarkdownIt = uniIm.utils.markdownIt;
const markdownIt = MarkdownIt({
// 在源码中启用 HTML 标签
html: true,
// 如果结果以 <pre ... 开头内部包装器则会跳过
highlight: function (str, language) {
// 把tab换成两个空格
str = str.replace(/\t/g, ' ')
// 经过highlight.js处理后的html
let preCode = ""
try {
preCode = hljs.highlightAuto(str).value
} catch (err) {
// console.log('err',err);
preCode = markdownIt.utils.escapeHtml(str);
}
// console.log('preCode',preCode);
// 以换行进行分割
const lines = preCode.split(/\n/).slice(0, -1)
// 添加自定义行号
let html = lines.map((item, index) => {
// 去掉空行
if (item == '') {
return ''
}
const style = `transform: translateX(${lines.length > 99?10:0}px);height:20px;line-height: 20px;`
return `<li style="${style}"><span class="line-num" data-line="${index + 1}"></span>${item}</li>`
}).join('')
html = '<ol>' + html + '</ol>'
let htmlCode = `<div>`
htmlCode += `<pre class="hljs" style="padding-bottom:${uniIm.isWidescreen?15:0}px;overflow: auto;display: block;"><code>${html}</code></pre>`;
htmlCode += '</div>'
return htmlCode
}
})
export default {
computed: {
...uniIm.mapState(['isWidescreen']),
code(){
return this.msg.body || ''
}
},
data() {
return {
nodes: [],
language: '',
// #ifdef APP-NVUE
webViewHeight: "100px",
// #endif
htmlString: '',
overflow: false,
boxHeight: '20px'
}
},
props: {
msg: {
type: Object,
default () {
return {}
}
},
showFullBtn: {
type: Boolean,
default: true
}
},
watch: {
code: {
handler(code, oldValue) {
// console.log('code',code);
// 判断markdown中代码块标识符的数量是否为偶数
this.htmlString = markdownIt.render("``` \n\n" + code + " \n\n ```")
// console.log('this.htmlString',this.htmlString);
// #ifdef APP-NVUE
this.setWebViewConetnt(this.htmlString)
// #endif
// #ifndef APP-NVUE
this.nodes = this.htmlString
// #endif
const codeLine = this.code.split(/\n/).slice(0, -1).filter(item=>item).length + 1
let height = codeLine * 20
this.webViewHeight = height + 'px'
const maxHeight = 200
if (height > maxHeight) {
this.overflow = true
this.boxHeight = maxHeight + 'px'
}else{
this.boxHeight = height + 'px'
}
},
immediate: true
}
},
mounted() {},
methods: {
// #ifdef APP-NVUE
setWebViewConetnt() {
if (this.$refs.web) {
// console.log('this.htmlString',this.htmlString);
this.$refs.web.evalJs(`setHtml(${JSON.stringify(this.htmlString)})`)
}
},
onWebViewMsg(e) {
let [data] = e.detail.data
if (data.action == 'onJSBridgeReady') {
this.setWebViewConetnt()
}
},
// #endif
copyCode(e) {
uni.setClipboardData({
data: this.code,
showToast: false,
success() {
uni.showToast({
title: '复制成功',
icon: 'none'
});
}
})
},
toCodePage() {
uni.navigateTo({
url: '/uni_modules/uni-im/pages/common/view-code-page/view-code-page?code='+ encodeURIComponent(JSON.stringify(this.code))
})
},
trOnclick(e) {
console.log('e', e);
}
}
}
</script>
<style lang="scss">
/* #ifndef APP-NVUE */
@import '@/uni_modules/uni-im/sdk/utils/highlight/github-dark.min.css';
/* #endif */
.code-view-root {
flex: 1;
border-radius: 3px;
background-color: #0d1117;
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
}
.copy-btn-box {
font-size: 12px;
top: 0px;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
padding:5px;
}
.language {
color: #888;
}
.copy-btn {
font-size: 12px;
padding: 0 5px;
color: #888;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
/* #ifdef H5 */
.copy-btn:hover {
color: #259939;
}
/* #endif */
.code-view-box {
position: relative;
padding: 0;
margin-bottom: 10px;
/* #ifndef APP-NVUE */
padding: 0 5px;
overflow: auto;
width: 100%;
/* #endif */
// 设置与背景一样的黑色,防止滚动条样式变化太大
background-color: #0d1117;
}
/* #ifdef H5 */
.code-view-box * {
user-select: text;
cursor: text;
}
/* #endif */
.code-view-rich-text {
// flex: 1;
font-size: 14px;
border-radius: 10px;
}
.msg-code {
/* #ifdef MP */
width: 600rpx;
/* #endif */
}
.show-full-btn {
justify-content: center;
align-items: center;
height: 30px;
border-top: 1px solid #30363d;
}
.show-full-text {
font-size: 12px;
color: #888;
}
</style>
\ No newline at end of file
<template>
<view class="msg-content file-msg-box" @click="downLoadFile" ref="msg-content">
<view class="file-msg-info">
<text class="file-msg-info-name">
{{ fileName }}
</text>
<text class="file-msg-info-size">
{{ fileSize }}
</text>
</view>
<uni-im-icons code="e858" size="50" color="#EEEEEE" class="file-icon" />
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
props: {
msg: {
type: Object,
default () {
return {
body: ""
}
}
},
},
computed: {
fileSize() {
if (this.msg.type == 'file') {
let size = this.msg.body.size
if (size < Math.pow(1024, 1)) {
return parseInt(size * 10) / 10 + 'B'
} else if (size < Math.pow(1024, 2)) {
return parseInt(size / Math.pow(1024, 1) * 10) / 10 + 'KB'
} else if (size < Math.pow(1024, 3)) {
return parseInt(size / Math.pow(1024, 2) * 10) / 10 + 'MB'
} else {
return 'err'
}
}
return 'err'
},
fileName() {
if (this.msg.type == 'file') {
let name = this.msg.body.name
if (name.length < 30) {
return name
} else {
return name.slice(0, 15) + '...' + name.slice(-15)
}
}
return ''
}
},
data() {
return {
}
},
methods: {
async downLoadFile() {
const url = await uniIm.utils.getTempFileURL(this.msg.body.url)
// #ifdef H5
return window.open(url)
// #endif
// #ifndef H5
uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200) {
// console.log('下载成功');
// console.log(res.tempFilePath);
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (res) => {
// console.log('res',res);
uni.openDocument({
filePath: res.savedFilePath
})
}
});
}
}
});
// #endif
},
}
}
</script>
<style>
.file-msg-box {
background-color: #FFFFFF;
width: 500rpx;
padding: 10px;
border-radius: 8px;
flex-direction: row;
justify-content: space-between;
}
.file-msg-info {
width: 300rpx;
flex-direction: column;
justify-content: space-around;
}
.file-msg-info-name {
/* #ifndef APP-NVUE */
word-wrap: break-word;
/* #endif */
font-size: 16px;
}
.file-msg-info-size {
font-size: 12px;
color: #666;
}
</style>
\ No newline at end of file
<template>
<view class="history-msg" @click="viewMsg">
<view class="title">{{title}}</view>
<view class="item-text-list">
<view class="item-text" v-for="(item,index) in msgList.slice(0,3)" :key="index">
{{item.nickname}}{{item.content}}
</view>
</view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
emits: ['viewMsg'],
props: {
msg: {
type: Object,
default: {}
},
},
data() {
return {
msgList: [],
title: ''
}
},
mounted() {
const msg = this.msg
// console.log(msg)
this.msgList = msg.body.msgList || []
for (let i = 0; i < this.msgList.length; i++) {
let msg = this.msgList[i]
this.setNickname(msg)
// 获取body 去掉所有 html 标签
msg.content = typeof(msg.body) === 'string' ? msg.body.replace(/<[^>]+>/g, "") : '[多媒体类型]'
}
if(!this.msgList[0].group_id){
let currentUid = uniCloud.getCurrentUserInfo().uid;
let currentUidInfo = uniIm.users[currentUid];
let currentNickname = currentUidInfo?currentUidInfo.nickname:'';
// 找到不是自己的nickname去重
let nickname = this.msgList.map(item=>item.nickname).filter(nickname=>nickname!==currentNickname)[0]
this.title = currentNickname + (nickname?''+nickname:'')+'的聊天记录';
// console.log(this.title)
}else{
this.title = msg.body.title
}
},
methods: {
viewMsg(){
this.$emit('viewMsg',this.msgList)
},
setNickname(msg) {
let users = uniIm.users[msg.from_uid]
if (users) {
msg.nickname = users.nickname;
} else {
const dbJQL = uniCloud.databaseForJQL();
dbJQL.collection('uni-id-users').doc(msg.from_uid).field('nickname').get().then(res => {
msg.nickname = res.data[0].nickname;
})
}
}
}
}
</script>
<style>
/* #ifdef H5 */
.history-msg,.history-msg * {
cursor: pointer;
}
/* #endif */
.history-msg {
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 {
text-align: left;
font-size: 16px;
height: 36px;
color: #333;
padding: 5px;
border-bottom: 1px solid #eee;
}
.item-text {
color: #888;
margin-top: 5px;
}
</style>
\ No newline at end of file
<template>
<view>
<!-- 注意:根节点都view不能去掉,否则鼠标右键出不来菜单 -->
<uni-im-img max-width="400rpx" :src="msg.body.url" mode="widthFix" @click="previewImage" class="img" />
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
props: {
msg: {
type: Object,
default: () => {}
}
},
data() {
return {
}
},
methods: {
async previewImage() {
// console.log(213);
uni.showLoading();
let url = await uniIm.utils.getTempFileURL(this.msg.body.url)
uni.previewImage({
urls: [url],
complete() {
uni.hideLoading()
}
})
}
}
}
</script>
<style>
.img {
width: 400rpx;
}
</style>
<template>
<view class="uni-im-rich-text"
:class="{'isFromSelf':isFromSelf, 'only1u': trBody.length === 0 &&webInfoList.length === 1}">
<template v-for="(item,index) in trBody" :index="index">
<template v-if="item.name == 'span'">
<text v-if="item.attrs && item.attrs.class == 'nickname'" class="text nickname"
:class="{pointer:canPrivateChat}"
@click="privateChat(item.attrs.user_id)"
>
{{item.children[0].text}}
</text>
<text v-else class="text">
{{item.children[0].text}}
</text>
<uni-im-icons class="text isRead" v-if="isFromSelf && 'isRead' in item" :code="item.isRead?'e609':'e741'"
size="14px" :color="item.isRead?'#25882a':'#bbb'"></uni-im-icons>
</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="400rpx" @click="previewImage(item.attrs.src)"
:src="item.attrs.src" mode="widthFix" class="img" />
<uni-link class="link" v-else-if="item.name == 'a'" :href="item.attrs.href" color="#007fff"
:text="item.children[0].text"></uni-link>
</template>
<!-- <view class="web-info" v-for="(item,index) in webInfoList" :key="index">
<view class="title-box">
<image v-if="item.icon" :src="item.icon" mode="widthFix" class="web-icon" @error="item.icon = false" />
<view v-if="item.title" class="title">{{item.title}}</view>
</view>
<view class="content">
<view v-if="item.description" class="description">{{item.description}}</view>
<image v-if="item.thumbnail" class="web-thumbnail" :src="item.thumbnail" mode="widthFix"
@error="item.thumbnail = false" />
</view>
<view class="link-box" v-if="item.url">
<uni-link class="link" :href="item.url" color="#007fff" :text="item.url"></uni-link>
<uni-im-icons @click="copy(item.url)" class="copy" code="e67e"></uni-im-icons>
</view>
</view> -->
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
props: {
msg: {
type: Object,
default: () => {
return {
reader_list: [],
body: []
}
}
}
},
data() {
return {
webInfoList: []
}
},
async mounted() {
// let aList = this.msg.body.filter(item => item.name == 'a')
// // .filter(item => {
// // return item.attrs && item.attrs.href &&
// // item.attrs.href.includes('dcloud.net.cn') ||
// // item.attrs.href.includes('dcloud.io')
// // })
// // console.log('aList',aList)
// for(let i = 0; i < aList.length; i++){
// const uniImCo = uniCloud.importObject("uni-im-co",{customUI:true})
// let res = await uniImCo.getWebInfo(aList[i].attrs.href)
// // console.log('getWebInfo',res.data)
// res.data.url = aList[i].attrs.href
// if(res.data.title){
// res.data.title = getStr(res.data.title, 60)
// res.data.description = getStr(res.data.description, 60)
// // // 取字符串的前20个字符,如果超出加...
// function getStr(str='', len) {
// if (str.length > len) {
// return str.substring(0, len) + "...";
// } else {
// return str;
// }
// }
// this.webInfoList.push(res.data)
// }
// }
},
computed: {
imgList() {
return this.msg.body.filter(item => item.name == 'img').map(item => item.attrs.src)
},
isFromSelf() {
return this.msg.from_uid === uniCloud.getCurrentUserInfo().uid
},
trBody() {
if (
this.webInfoList.length === 1 &&
this.msg.body.filter(i => !(i.type === 'text' && i.text === ' ')).length === 1 &&
this.webInfoList[0].url === this.msg.body[0].attrs.href
) {
// 只有一个链接,且链接的地址和消息体的地址一样,则不显示消息体
return []
} else {
return this.msg.body.map(node => {
if (node.name == 'span' && node.attrs && node.attrs.class == 'nickname' && node.attrs.user_id) {
// 改写/设置 nickname
node.children = [{
type: 'text',
text: '@' + this.getNicknameByUid(node.attrs.user_id)
}]
// 设置是否已读
node.isRead = this.msg.reader_list ? this.msg.reader_list.find(item => item.user_id == node.attrs.user_id) : false
}
return node
})
}
},
canPrivateChat() {
if(this.uniIDHasRole('staff')){
return true
}
const {group_member} = uniIm.convasation?.dataList?.find(item => item.id === this.msg.conversation_id)||{}
return group_member ? group_member[currentUserId]?.role.includes('admin') : false
}
},
methods: {
getNicknameByUid(uid) {
let users = uniIm.users[uid]
if (users) {
return users.nickname
} else {
return ''
}
},
previewImage(src) {
uni.previewImage({
urls: this.imgList,
current: src
})
},
copy(text) {
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({
title: '复制成功',
icon: 'none'
})
}
})
},
privateChat(user_id) {
if (this.canPrivateChat) {
if (uniIm.isWidescreen) {
uni.$emit('uni-im-toChat', {
user_id
})
} else {
uni.navigateTo({
url: '/uni_modules/uni-im/pages/chat/chat?user_id=' + user_id,
animationDuration: 300
})
}
}
},
}
}
</script>
<style lang="scss" scoped>
/* #ifdef H5 */
.pointer {
cursor: pointer !important;
}
.uni-im-rich-text {
max-width: 100%;
}
@media screen and (min-device-width:960px) {
.uni-im-rich-text .img {
max-width: 480px !important;
}
}
/* #endif */
/* #ifndef APP-NVUE */
.uni-im-rich-text {
display: inline-block;
}
.uni-im-rich-text .text {
display: inline;
word-wrap: break-word;
user-select: text;
cursor: text;
}
.uni-im-rich-text .link {
display: inline;
user-select: all;
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 {
background-color: #c9e7ff !important;
}
.uni-im-rich-text.only1u {
padding: 0;
}
.uni-im-rich-text.only1u .web-info {
margin-top: 0;
}
.uni-im-rich-text .img {
margin: 5px 0 !important;
/* #ifdef H5 */
max-width: 460rpx !important;
display: block !important;
box-shadow: #eee 0 0 5px;
cursor: pointer;
/* #endif */
}
.uni-im-rich-text .nickname {
color: #0b65ff;
margin: 0 2px;
font-size: 15px;
}
.uni-im-rich-text .isRead {
position: relative;
top: -3px;
margin-right: 3px;
}
.web-info {
background-color: #FFF;
padding: 10px;
border-radius: 10px;
margin-top: 10px;
border: 1px solid #eee;
}
.web-info .title-box {
flex-direction: row;
}
.web-info .title-box .web-icon {
width: 16px;
height: 16px;
margin: 4px 5px 0 0;
}
.web-info .title {
font-size: 16px;
flex: 1;
font-weight: bold;
}
.web-info .content {
flex-direction: row;
}
.web-info .content .description {
font-size: 14px;
color: #666;
margin-top: 5px;
flex: 1;
/* #ifndef APP-NVUE */
beak-word: break-all;
/* #endif */
}
.web-info .content .web-thumbnail {
height: 100px;
width: 100px;
margin: 5px;
}
.web-info .link-box {
flex-direction: row;
border-top: 1px solid #eee;
padding-top: 5px;
justify-content: space-between;
align-items: flex-end;
}
.web-info .link-box .link {
font-size: 12px;
// 不会换行
/* #ifndef APP-NVUE */
white-space: nowrap;
/* #endif */
flex: 1;
overflow: hidden;
}
.web-info .link-box .copy {
opacity: 0.5;
margin-left: 20px;
}
.web-info .link-box .copy:hover {
opacity: 0.8;
}
</style>
\ No newline at end of file
<template>
<view class="text selfText sound-box" :class="{reverse:!self}" :style="{width:soundBoxWidth}" @click="playSound">
<text class="sound-time">
{{ msg.body.time }}''
</text>
<view class="sound-icon-box" :class="{rotate:!self}">
<image v-if="soundPlayState" src="@/uni_modules/uni-im/static/sound-ing.gif" style="width: 18px;height: 18px;"
mode="widthFix" />
<uni-im-icons v-else :class="{'sound-icon-active':soundPlayState}" code="e6f5" size="18px" color="#000000" />
</view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const audioContext = uniIm.audioContext
export default {
data() {
return {
soundPlayState: 0
}
},
props: {
msg: {
type: Object,
default () {
return {
from_uid: '',
body: {
time: 0
}
}
}
},
soundBoxWidth: {
type: String,
default: '100px'
}
},
computed: {
self() {
return this.msg.from_uid === uniCloud.getCurrentUserInfo().uid
}
},
mounted() {
this.onPlay = async () => {
// console.log('soundPlayStart------------------');
let currentAudioUrl = await uniIm.utils.getTempFileURL(this.msg.body.url)
let src = uniIm.audioContext.src
if (src == currentAudioUrl) {
this.soundPlayState = 1
} else {
this.soundPlayState = 0
}
}
audioContext.onPlay(this.onPlay);
this.soundPlayEnd = () => {
// console.log('soundPlayEnd------------------');
this.soundPlayState = 0
}
audioContext.onPause(this.soundPlayEnd);
audioContext.onStop(this.soundPlayEnd);
audioContext.onEnded(this.soundPlayEnd);
audioContext.onError(this.soundPlayEnd);
},
destroyed() {
console.log('unmounted');
audioContext.offPlay(this.onPlay);
audioContext.offPause(this.soundPlayEnd);
audioContext.offStop(this.soundPlayEnd);
audioContext.offEnded(this.soundPlayEnd);
audioContext.offError(this.soundPlayEnd);
},
methods: {
async playSound() {
audioContext.src = await uniIm.utils.getTempFileURL(this.msg.body.url)
// 下一个事件循环执行
setTimeout(() => {
// console.log(78998797,audioContext);
if (this.soundPlayState === 1) {
// console.log('播放中,执行关闭');
audioContext.stop()
} else {
audioContext.stop()
audioContext.play();
}
}, 0)
}
}
}
</script>
<style scoped>
.sound-box{
/* #ifdef H5 */
cursor: pointer !important;
/* #endif */
}
.sound-box {
flex-direction: row;
background-color: #94EB6A;
height: 44px;
padding: 10px;
width: 66px;
border-radius: 5px;
justify-content: flex-end;
align-items: center;
}
.sound-icon-box {
width: 18px;
height: 18px;
justify-content: center;
}
.sound-time {
font-size: 14px;
margin: 0 4px;
}
.reverse {
flex-direction: row-reverse;
}
.rotate {
transform: rotate(180deg);
}
.sound-icon-active {
transform: option;
opacity: 10;
background-color: #007AFF;
transition-property: background-color;
transition-duration: 0.3s;
transition-delay: 0.1s;
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1.0);
}
</style>
\ No newline at end of file
<template>
<view class="system-msg-box">
<template v-if="content">
<uni-im-group-notification v-if="msg.action === 'update-group-info-notification'" :content="content" :create_time="create_time"></uni-im-group-notification>
<text v-else class="system-msg">{{friendlyTime}} {{content}}</text>
</template>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
data() {
return {
content: false,
create_time:false
}
},
props: {
msg: {
type: Object,
default () {
return {}
}
},
},
computed: {
friendlyTime() {
return uniIm.utils.toFriendlyTime(this.create_time || this.msg.create_time || this.msg.client_create_time)
}
},
watch: {
msg: {
handler: async function (msg, oldMsg) {
if(msg.action.indexOf("update-group-info-") === 0){
const key = Object.keys(msg.body.updateData)[0]
const value = msg.body.updateData[key];
if (key == "notification"){
this.content = value.content
this.create_time = value.create_time
}else if(key == "avatar_file"){
this.content = "群聊头像已更新"// 已在 msg-list 组件隐藏此类型消息
}
// mute_all_members
else if(key == "mute_all_members"){
this.content = value ? "已开启“全员禁言”" : "已关闭“全员禁言”"
} else{
this.content = {
"name":" 群聊名称",
"introduction":"群简介"
}[key] + "已更新为:" + value
}
}else if( ["join-group","group-exit","group-expel"].includes(msg.action) ){
let nicknameList = (await uniIm.users.get(msg.body.user_id_list)).map(item => item.nickname)
let actionName = {
"join-group":"加入群聊",
"group-exit":"退出群聊",
"group-expel":"被踢出群聊"
}[msg.action];
this.content = nicknameList.join(' , ') + actionName
}else if(msg.action === "group-dissolved"){
this.content = '此群聊已被解散'
}else if(msg.action === "set-group-admin"){
const {user_id,addRole,delRole} = msg.body
const nickname = (await uniIm.users.get(user_id)).nickname
this.content = `已将"${nickname}"${addRole.includes("admin") ? "添加为群管理员" : "从群管理员中移除"}`
}else{
this.content = msg.body
}
},
deep: true,
immediate: true
}
},
async mounted() {}
}
</script>
<style lang="scss">
.system-msg-box{
align-items: center;
}
// 如果是 pc 端
// #ifdef H5
@media screen and (min-device-width:960px){
.system-msg-box{
max-width: 550px!important;
word-break: break-all;
margin: 0 auto;
}
}
// #endif
.hidden {
height: 0;
}
.system-msg {
background-color: #f2f2f2;
color: #9d9e9d;
font-size: 12px;
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;
background-color: #FFFFFF;
width: 600rpx;
font-size: 18px;
margin-top: 10px;
}
.group-notification .title-box{
flex-direction: row;
}
.group-notification .title-box .title{
padding-left: 5px;
color: #888;
}
.group-notification .content{
color: #555;
padding: 6px 0;
}
</style>
\ No newline at end of file
<template>
<view class="msg-text-box" :class="msgClass">
<msgRichText v-if="htmlNodes.length" :msg="{...msg,...{'body':htmlNodes}}" />
<text v-else class="msg-text" :decode="true" space="ensp">{{ msg.body }}</text>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import msgRichText from './rich-text.vue'
export default {
components: {
msgRichText,
},
props: {
msg: {
type: Object,
default () {
return {
body: ""
}
}
}
},
data() {
return {
htmlNodes: []
}
},
computed: {
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": {
handler() {
let htmlString = this.msg.body.replace(/</g, "&lt;").replace(/>/g, "&gt;")
// 将字符串的url转换为链接
htmlString = uniIm.utils.replaceUrlToLink(htmlString)
/* 如需要自己补:
// 手机号正则
const regPhone = /(13[0-9]|14[5-9]|15[012356789]|166|17[0-8]|18[0-9]|19[8-9])[0-9]{8}/g;
htmlString = htmlString.replace(regPhone, " <a href='tel:$&'>$&</a>")
// 固定电话正则
const regTel = /(([0\+]\d{2,3}-)?(0\d{2,3})-)(\d{7,8})(-(\d{3,}))?/g;
htmlString = htmlString.replace(regTel, " <a href='tel:$&'>$&</a>")
// 邮箱正则
const regMail = /([a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,4})/ig;
htmlString = htmlString.replace(regMail, " <a href='mailto:$&'>$&</a>")
*/
if (this.msg.body != htmlString) {
try {
let htmlNodes = uniIm.utils.parseHtml(htmlString)
// console.log('htmlNodes',htmlNodes);
htmlNodes.map(item => {
// console.log('item',item);
if (item.attrs && item.attrs.class) {
item.attrs.class += " msg-text"
} else {
item.attrs = {
class: "msg-text"
}
}
return item
})
this.htmlNodes = htmlNodes
} catch (e) {
console.error('htmlString error:', e);
}
}
},
immediate: true
}
},
mounted() {},
}
</script>
<style>
.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;
}
/* #endif */
.self{
background-color: #c9e7ff;
}
</style>
\ No newline at end of file
<template>
<view class="msg-userinfo-card" @click="onClick">
<cloud-image
class="avatar"
width="40px"
height="40px"
border-radius="5px"
:src="avatarUrl||'/uni_modules/uni-im/static/avatarUrl.png'"
mode="widthFix"
/>
<text
:decode="true"
space="ensp"
class="msg-text"
>
{{ nickname }}
</text>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
props: {
msg: {
type: Object,
default () {
return {
body: ""
}
}
},
},
data() {
return {
avatarUrl:{},
nickname:'[...加载中]',
};
},
async mounted() {
let user = await uniIm.users.get(this.msg.body.user_id) || {}
this.avatarUrl = user.avatar_file?.url ?? '/uni_modules/uni-im/static/avatarUrl.png'
this.nickname = this.msg.nickname || user.nickname
},
methods: {
onClick() {
if (uniIm.isWidescreen) {
uni.$emit('uni-im-toChat', { user_id: this.msg.body.user_id })
} else {
uni.navigateTo({
url: '/uni_modules/uni-im/pages/chat/chat?user_id=' + this.msg.body.user_id,
animationDuration: 300
})
}
},
}
}
</script>
<style>
.msg-userinfo-card {
flex-direction: row;
align-items: center;
background-color: #fff;
padding: 10px;
border-radius: 10px;
/* #ifndef APP-NVUE */
min-width: 250px;
/* #endif */
}
/* #ifdef H5 */
.msg-userinfo-card,.msg-userinfo-card * {
cursor: pointer;
}
/* #endif */
.avatar {
border-radius: 5px;
margin-right: 10px;
}
</style>
<template>
<view class="video-box" @click="playVideo">
<image class="video-img" mode="aspectFill" :src="videoPoster" />
<view class="video-box-mark" />
<uni-im-icons code="e650" size="35" color="#FFF" class="play-video-icon" />
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
props: {
msg: {
type: Object,
default () {
return {
body: ""
}
}
},
},
data() {
return {
videoPoster: '', //视频封面
videoUrl: '', //视频地址
}
},
watch: {
'msg.body': {
async handler(msgBody) {
this.videoUrl = await uniIm.utils.getTempFileURL(this.msg.body.url)
//设置videoPoster
if (this.videoUrl.indexOf('blob:') === 0) {
// #ifdef H5
try {
let videoEl = document.createElement("video");
videoEl.src = this.videoUrl
videoEl.currentTime = 1
let canvasEl = document.createElement("canvas");
let context = canvasEl.getContext("2d");
// console.log('videoEl',videoEl);
videoEl.addEventListener('loadeddata', () => {
canvasEl.width = videoEl.videoWidth;
canvasEl.height = videoEl.videoHeight;
context.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height);
let firstFrameUrl = canvasEl.toDataURL();
// console.log('firstFrameUrl',firstFrameUrl);
this.videoPoster = firstFrameUrl
});
} catch (e) {
console.error('浏览器环境,获取本地视频封面失败。将使用默认图片', e)
this.videoPoster = '/uni_modules/uni-im/static/msg/video-uploading.gif'
}
// #endif
// #ifndef H5
this.videoPoster = '/uni_modules/uni-im/static/msg/video-uploading.gif'
// #endif
}else{
// 文件存储的服务商
let storageProvider = this.msg.body.url.substring(0, 8) == "cloud://" ? 'tencent' : 'aliyun'
this.videoPoster = this.videoUrl + (storageProvider == 'aliyun' ? '?x-oss-process=video/snapshot,t_1000,f_jpg,w_200,m_fast,ar_auto':'?imageView2/0/w/200')
}
},
deep: true,
immediate: true
}
},
mounted() {
},
methods: {
async playVideo() {
let url = await uniIm.utils.getTempFileURL(this.msg.body.url)
if (uniIm.isWidescreen) {
uni.$emit('uni-im-playVideo', url)
} else {
uni.navigateTo({
url: "/uni_modules/uni-im/pages/common/video/video?url=" + url,
animationDuration: 300,
animationType: "fade-in"
})
}
},
}
}
</script>
<style>
.video-box {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
width: 200rpx;
height: 200rpx;
position: relative;
}
.video-img {
width: 200rpx;
height: 200rpx;
}
.play-video-icon {
position: absolute;
width: 35px;
height: 35px;
top: 35px;
left: 35px;
border-radius: 50%;
border: 2px solid #FFF;
justify-content: center;
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 {
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.1);
}
</style>
\ No newline at end of file
此差异已折叠。
<template>
<view
v-if="isOpen"
class="share-msg-root"
@click="close"
>
<view class="share-msg-content" @click.stop>
<view class="conversation-list-box">
<uni-search-bar
id="search-bar"
v-model="keyword"
radius="5"
placeholder="搜索会话"
clear-button="auto"
cancel-button="none"
/>
<uni-im-conversation-list
id="conversation-list-box"
v-model:checked-list="checkedList"
:keyword="keyword"
:can-check="true"
:show-unread-count="false"
/>
</view>
<view class="conversation-detail-box">
<text class="title">
转发给:
</text>
<!-- 已经选择的会话列表 -->
<scroll-view scroll-y class="selected-conversation-list">
<view class="scroll-content">
<view
v-for="(item,index) in checkedList"
:key="index"
class="selected-conversation-item"
>
<image
class="avatar"
:src="item.avatar_file&&item.avatar_file.url ? item.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'"
mode="widthFix"
/>
<text class="title">
{{ item.title }}
</text>
<uni-icons
class="close-icon"
type="clear"
color="#ccc"
@click="removeCheckedItem(index)"
/>
</view>
</view>
</scroll-view>
<!-- 消息详情 -->
<view v-if="!noMsgList" class="msg">
<uni-list-item
title="转发的消息内容"
link
@click="viewMsg"
/>
</view>
<!-- 输入留言 -->
<view v-if="!noComment" class="input-box">
<textarea
v-model="inputText"
class="textarea"
placeholder="留言..."
/>
</view>
<!-- 操作按钮 -->
<view class="action-btns">
<button
class="btn cancel"
plain
@click="close"
>
取消
</button>
<button
class="btn"
:disabled="checkedList.length === 0"
type="primary"
@click="send"
>
发送
</button>
</view>
</view>
<!-- 消息详情 -->
<uni-im-view-msg v-if="!noMsgList" ref="view-msg" />
</view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
name: 'UniImShareMsg',
props: {
noMsgList: { // 不显示转发的消息列表
type: Boolean,
default: false
},
noComment: { // 不显示留言
type: Boolean,
default: false
}
},
data() {
return {
inputText: '',
keyword: '',
checkedList: [],
isOpen: false,
msgList: [],
merge: false
}
},
computed: {},
methods: {
open(msgList, merge) {
// console.info('msgList', msgList);
this.isOpen = true;
this.merge = merge;
this.msgList = msgList.map(msg => {
let {body,from_uid,type,conversation_id,create_time,group_id} = msg;
let data = {body,from_uid,type,conversation_id,create_time,group_id};
if (!merge) {
// 如果不是合并转发,则删除会话id和群id
data.conversation_id = '';
data.group_id = '';
}
return data;
});
},
removeCheckedItem(index) {
this.checkedList.splice(index, 1);
},
close() {
this.isOpen = false;
this.checkedList = [];
this.inputText = '';
},
createMsg(msg, conversation) {
},
viewMsg() {
console.info('viewMsg', this.msgList);
this.$refs['view-msg'].open(this.msgList);
},
async send() {
if (!this.merge && this.inputText.length != 0) {
this.msgList.push({
"body": this.inputText,
"type": "text"
})
}
for (var cidIndex = 0; cidIndex < this.checkedList.length; cidIndex++) {
const conversation = this.checkedList[cidIndex];
const {
friend_uid,
group_id,
id: conversation_id
} = conversation;
// 基本消息信息
const baseMsgInfo = {
"to_uid": friend_uid,
conversation_id,
group_id,
"from_uid": uniCloud.getCurrentUserInfo().uid,
"state": 0,
"client_create_time": Date.now(),
"is_read": false,
// 接收消息的appId,默认为当前应用的appId。如果你是2个不同appId的应用相互发,请修改此值为相对的appId
appId: uniIm.systemInfo.appId,
}
if (this.merge) {
let msg = Object.assign(baseMsgInfo, {
"type": "history",
"body": {
"title": this.msgList[0].group_id ? "群聊天记录" : "", // 是否转带真实群名称待定
"msgList": JSON.parse(JSON.stringify(this.msgList))
}
});
console.info('msg', msg);
await sendMsgToConversation(conversation, msg);
if (this.merge && this.inputText.length != 0) {
await sendMsgToConversation(conversation, {
...baseMsgInfo,
"body": this.inputText,
"type": "text"
});
}
} else {
for (let msgIndex = 0; msgIndex < this.msgList.length; msgIndex++) {
let msg = JSON.parse(JSON.stringify(this.msgList[msgIndex]));
msg = Object.assign(msg, baseMsgInfo);
// console.info('msg',msg);
await sendMsgToConversation(conversation, msg);
}
}
}
this.close();
async function sendMsgToConversation(conversation, msg) {
// 插到消息列表
conversation.msgList.push(msg);
// 保存到本地数据库
await conversation.msgManager.localMsg.add(msg);
const uniImCo = uniCloud.importObject("uni-im-co");
await uniImCo.sendMsg(msg)
.then(async e => {
console.log('uniImCo.sendMsg',e);
msg.state = e.errCode === 0 ? 100 : -100;
msg.create_time = e.data.create_time;
msg._id = e.data._id;
await 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 updateMsg(msg)
});
}
async function updateMsg(msg) {
const conversation = await uniIm.conversation.get(msg.conversation_id);
// console.log('conversation', conversation);
let index = conversation.msgList.findIndex(_msg => _msg.unique_id == msg.unique_id)
if (index === -1) {
throw 'updateMsg msg 不存在'
}
conversation.msgList.splice(index, 1, Object.assign({}, msg))
conversation.msgManager.localMsg.update(msg.unique_id, msg)
}
}
}
}
</script>
<style>
/* #ifdef H5*/
.share-msg-root,
.share-msg-root * {
max-width: none !important;
max-height: none !important;
}
/* #endif */
.share-msg-root {
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 750rpx;
/* #ifdef H5 */
width: 100vw;
height: 100vh;
/* #endif */
flex: 1;
justify-content: center;
align-items: center;
background-color: rgb(0, 0, 0, 0.3);
}
.share-msg-content {
background-color: #fff;
width: 750px;
height: 70vh;
flex-direction: row;
border-radius: 15px;
position: relative;
}
.conversation-list-box {
width: 300px;
height: 100%;
border-right: 1px solid #eee;
}
.conversation-list-box ::v-deep .conversation-list {
max-height: calc(70vh - 60px) !important;
height: calc(70vh - 60px) !important;
}
.conversation-list-box ::v-deep .conversation-list .refresh-box {
background-color: #fff;
}
.conversation-detail-box {
flex: 1;
}
.conversation-detail-box .title {
font-size: 18px;
color: #333;
margin: 15px;
}
.selected-conversation-list {
height: 0;
flex: 1;
}
.scroll-content {
flex-direction: row;
flex-wrap: wrap;
margin: 0 5px 35px 5px;
}
.selected-conversation-item {
flex-direction: row;
padding: 3px;
justify-content: center;
align-items: center;
border-radius: 5px;
background-color: rgba(250, 250, 250, 1);
margin: 3px;
}
.selected-conversation-item .avatar {
width: 35px;
height: 35px;
border-radius: 50%;
}
.selected-conversation-item .title {
font-size: 12px;
color: #333;
text-align: center;
margin: 0 5px;
}
.close-icon {
cursor: pointer;
}
.msg {}
.input-box {
padding: 15px 10px;
border: 1px solid #EEE;
border-width: 1px 0;
}
.textarea {
width: 100%;
border-radius: 5px;
font-size: 14px;
outline: none;
}
.action-btns {
flex-direction: row;
justify-content: flex-end;
padding: 10px;
}
.action-btns .btn {
margin: 0 10px;
width: 100px;
}
.action-btns .btn.cancel {
color: #999;
border-color: #999;
}
</style>
\ No newline at end of file
此差异已折叠。
此差异已折叠。
uni-im源码使用许可协议
2022年10月
本许可协议,是数字天堂(北京)网络技术有限公司(以下简称DCloud)对其所拥有著作权的“DCloud uni-im”(以下简称软件),提供的使用许可协议。
您对“软件”的复制、使用、修改及分发受本许可协议的条款的约束,如您不接受本协议,则不能使用、复制、修改本软件。
授权许可范围
a) 授予您永久性的、全球性的、免费的、非独占的、不可撤销的本软件的源码使用许可,您可以使用这些源码制作自己的应用。
b) 您只能在DCloud产品体系内使用本软件及其源码。您不能将源码修改后运行在DCloud产品体系之外的环境,比如客户端脱离uni-app,或服务端脱离uniCloud。
c) DCloud未向您授权商标使用许可。您在根据本软件源码制作自己的应用时,需以自己的名义发布软件,而不是以DCloud名义发布。
d) 本协议不构成代理关系。
DCloud的责任限制
“软件”在提供时不带任何明示或默示的担保。在任何情况下,DCloud不对任何人因使用“软件”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
您的责任限制
a) 您需要在授权许可范围内使用软件。
b) 您在分发自己的应用时,不得侵犯DCloud商标和名誉权利。
c) 您不得进行破解、反编译、套壳等侵害DCloud知识产权的行为。您不得利用DCloud系统漏洞谋利或侵害DCloud利益,如您发现DCloud系统漏洞应第一时间通知DCloud。您不得进行攻击DCloud的服务器、网络等妨碍DCloud运营的行为。您不得利用DCloud的产品进行与DCloud争夺开发者的行为。
d) 如您违反本许可协议,需承担因此给DCloud造成的损失。
本协议签订地点为中华人民共和国北京市海淀区。
根据发展,DCloud可能会对本协议进行修改。修改时,DCloud会在产品或者网页中显著的位置发布相关信息以便及时通知到用户。如果您选择继续使用本框架,即表示您同意接受这些修改。
条款结束
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
export default `😀,😁,😂,🤣,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗,😙,😚,☺️,🙂,🤗,🤩,🤔,🤨,😐,😑,😶,🙄,😏,😣,😥,😮,🤐,😯,😪,😫,😴,😌,😛,😜,😝,🤤,😒,😓,😔,😕,🙃,🤑,😲,☹️,🙁,😖,😞,😟,😤,😢,😭,😦,😧,😨,😩,🤯,😬,😰,😱,😳,🤪,😵,😡,😠,🤬,😷,🤒,🤕,🤢,🤮,🤧,😇,🤠,🤡,🤥,🤫,🤭,🧐,🤓,😈,👿,👹,👺,💀,☠️,👻,👽,🤖,😺,😸,😹,😻,😼,😽,🙀,😿,😾,🙈,🙉,🙊,👶,🧒,👦,👧,🧑,👨,👩,🧓,👴,👵,👨‍⚕️,👩‍⚕️,👨‍🎓,👩‍🎓,👨‍🏫,👩‍🏫,👨‍⚖️,👩‍⚖️,👨‍🌾,👩‍🌾,👨‍🍳,👩‍🍳,👨‍🔧,👩‍🔧,👨‍🏭,👩‍🏭,👨‍💼,👩‍💼,👨‍🔬,👩‍🔬,👨‍💻,👩‍💻,👨‍🎤,👩‍🎤,👨‍🎨,👩‍🎨,👨‍✈️,👩‍✈️,👨‍🚀,👩‍🚀,👨‍🚒,👩‍🚒,👮,👮‍♂️,👮‍♀️,🕵️,🕵️‍♂️,🕵️‍♀️,💂,💂‍♂️,💂‍♀️,👷,👷‍♂️,👷‍♀️,🤴,👸,👳,👳‍♂️,👳‍♀️,👲,🧕,🧔,👱,👱‍♂️,👱‍♀️,🤵,👰,🤰,🤱,👼,🎅,🤶,🧙,🧙‍♀️,🧙‍♂️,🧚,🧚‍♀️,🧚‍♂️,🧛,🧛‍♀️,🧛‍♂️,🧜,🧜‍♀️,🧜‍♂️,🧝,🧝‍♀️,🧝‍♂️,🧞,🧞‍♀️,🧟,🧟‍♀️,🙍,🙍‍♂️,🙍‍♀️,🙎,🙎‍♂️,🙎‍♀️,🙅,🙅‍♂️,🙅‍♀️,🙆,🙆‍♂️,🙆‍♀️,💁,💁‍♂️,💁‍♀️,🙋,🙋‍♂️,🙋‍♀️,🙇,🙇‍♂️,🙇‍♀️,🤦,🤦‍♂️,🤦‍♀️,🤷,🤷‍♂️,🤷‍♀️,💆,💆‍♂️,💆‍♀️,💇,💇‍♂️,💇‍♀️,🚶,🚶‍♂️,🚶‍♀️,🏃,🏃‍♂️,🏃‍♀️,💃,🕺,👯,👯‍♂️,👯‍♀️,🧖,🧖‍♀️,🧖‍♂️,🧗,🧗‍♀️,🧗‍♂️,🧘,🧘‍♀️,🧘‍♂️,🕴️,👤,👥,👫,👬,👭,💏,👨‍❤️‍💋‍👨,👩‍❤️‍💋‍👩,💑,👨‍❤️‍👨,👩‍❤️‍👩,👪,👨‍👩‍👧,👨‍👩‍👧‍👦,👨‍👩‍👦‍👦,👨‍👩‍👧‍👧,👨‍👨‍👦,👨‍👨‍👧,👨‍👨‍👧‍👦,👨‍👨‍👦‍👦,👨‍👨‍👧‍👧,👩‍👩‍👦,👩‍👩‍👧,👩‍👩‍👧‍👦,👩‍👩‍👦‍👦,👩‍👩‍👧‍👧,👨‍👦,👨‍👧,👨‍👧‍👦,👨‍👧‍👧,👩‍👦‍👦,👩‍👧,👩‍👧‍👦,🤳,👃,👅,👄,💋,💘,❤️,💓,💔,💕,💖,💗,💙,💚,💛,🧡,💜,🖤,💝,💞,❣️,💌,💬,🌬️,☃️,⛄,🎎,🗿,👾,💩,🛀,🛌,💅,👂,👣,👀,👁️,🧠,💭,👓,👔,👕,👖,🧣,🧤,🧥,🧦,👗,👘,👙,👚,👛,👜,👝,🎒,👞,👟,👠,👡,👢,👑,👒,🎩,🎓,🧢,📿,💄,💍,💎,🥄,🔪,🏺,🗺️,🗾,🎠,🎡,🎢,💈,🎪,🛰️,🚀,🛸,🛎️,⌛,⏳,⌚,⏰,🕰️,🌡️,🌂,☂️,☔,⛱️,⚡,🎃,🎄,🎆,🎇,🎈,🎉,🎊,🎏,🎐,🎑,🎀,🎁,🎗️,🎟️,🎫,🔮,🎮,🕹️,🎰,🃏,🎴,🎭,🖼️,🎨,🔇,🔈,🔉,🔊,📢,📣,📯,🔔,🔕,🎼,🎵,🎶,🎙️,🎚️,🎛️,🎤,🎧,📻,🎷,🎸,🎹,🎺,🎻,🥁,📱,📲,☎️,📞,📟,📠,🔋,🔌,💻,🖥️,🖨️,⌨️,🖱️,🖲️,💽,💾,💿,📀,🎥,🎞️,📽️,🎬,📺,📷,📸,📹,📼,🔍,🔎,💡,🔦,🏮,📔,📕,📖,📗,📘,📙,📚,📓,📒,📃,📜,📄,📰,📑,🔖,💰,💴,💵,💶,💷,💸,💳,✉️,📧,📨,📩,📤,📥,📦,📫,📪,📬,📭,📮,✏️,✒️,📝,💼,📁,📂,📅,📆,📇,📈,📉,📊,📋,📌,📍,📎,📏,📐,✂️,🔒,🔓,🔏,🔐,🔑,🔨,🔫,🔧,🔩,🔬,🔭,📡,💉,💊,🚪,🚽,🚿,🛁,🛒,🚬,🔅,🔆,⚜️,🔱,📛,🚂,🚃,🚄,🚅,🚆,🚇,🚈,🚉,🚊,🚝,🚞,🚋,🚌,🚍,🚎,🚐,🚑,🚒,🚓,🚔,🚕,🚖,🚗,🚘,🚙,🚚,🚛,🚜,🚲,🛴,🛵,🚏,🛣️,🛤️,🛢️,⛽,🚨,🚥,🚦,🛑,🚧,⛵,🛶,🚤,🛳️,⛴️,🛥️,🚢,✈️,🛩️,🛫,🛬,💺,🚁,🚟,🚠,🚡,⚠️,⛔,🦗,🍇,🍈,🍉,🍊,🍋,🍌,🍍,🍎,🍏,🍐,🍑,🍒,🍓,🥝,🍅,🥥,🥑,🍆,🥔,🥕,🌽,🌶️,🥒,🥦,🥜,🍞,🥐,🥖,🥨,🥞,🧀,🍖,🍗,🥩,🥓,🍔,🍟,🍕,🌭,🥪,🌮,🌯,🥙,🥚,🍳,🥘,🍲,🥣,🥗,🍿,🥫,🍱,🍘,🍙,🍚,🍛,🍜,🍝,🍠,🍢,🍣,🍤,🍥,🍡,🥟,🥠,🥡,🍦,🍧,🍨,🍩,🍪,🎂,🍰,🥧,🍫,🍬,🍭,🍮,🍯,🍼,🥛,☕,🍵,🍶,🍾,🍷,🍸,🍹,🍺,🍻,🥂,🥃,🥤,🥢,🍽️,🍴`.split(',')
此差异已折叠。
此差异已折叠。
<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">
.text-box,
.code-view {
flex: 1;
width: 750rpx;
/* #ifndef APP-NVUE */
width: 100vw;
height: 100vh;
/* #endif */
}
</style>
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
{
"pages": []
}
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
文件已添加
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册