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

3.4.31

上级 7d18bad4
/* #ifdef H5 */
::v-deep {
/* #endif */
#app,
page,
view,
label
......@@ -34,7 +35,6 @@
text,
textarea,
video {
position: relative;
box-sizing: border-box;
}
......
export default {
// 云存储服务商
cloudFile:{
// 云存储服务商,支持:aliyun、tencent、qiniu
provider: 'qiniu',
},
uniPush: {
// 消息渠道设置,避免被限量推送、静默推送(静音且需下拉系统通知栏才可见通知内容)详情文档:https://doc.dcloud.net.cn/uniCloud/uni-cloud-push/api.html#channel
channel:{
......@@ -7,5 +12,7 @@ export default {
// 消息渠道描述,会显示在手机系统关于当前应用的通知设置中
desc: "客服消息"
}
}
},
// web端绑定的域名,用于生成二维码等
domain: 'https://im.dcloud.net.cn',
}
\ No newline at end of file
<template>
<template>
<view class="uni-im-chat-input" :style="{height:chatInputBoxHeight}">
<view id="drag-line"></view>
<view class="top-menu">
......@@ -9,7 +9,9 @@
></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>
<component :is="item.component" v-bind="item.props" @sendCodeMsg="sendCodeMsg"
@showMenberList="showMenberList"
></component>
</view>
<!-- #endif -->
</view>
......@@ -26,7 +28,7 @@
<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" @change="updateModelValue" class="editor" ref="editor"></uni-im-editor>
<uni-im-editor @touchmove.stop v-show="!soundIsShow" @input="oninput" @confirm="confirm" @change="updateModelValue" class="editor" ref="editor"></uni-im-editor>
</view>
<slot name="about-msg"></slot>
</view>
......@@ -61,17 +63,17 @@
<text class="send-btn-tip">↵ 发送 / shift + ↵ 换行</text>
<button @click="confirm" :disabled="!canSend" class="send" type="primary">发送</button>
</view>
</view>
</template>
<script>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import {markRaw} from "vue";
import {markRaw} from "vue";
import emojiCodes from './emojiCodes.js';
let currentModelValue = '';
export default {
let currentModelValue = '';
export default {
name: 'uni-im-chat-input',
emits: ["showAboutMenber","update:modelValue","confirm","input","sendSoundMsg","sendCodeMsg"],
emits: ["update:modelValue","confirm","input","sendSoundMsg","sendCodeMsg","showMenberList"],
data() {
return {
bHeight:uniIm.systemInfo.safeAreaInsets.bottom/2,
......@@ -105,18 +107,22 @@
extToolBar(){
// 调用扩展点,扩展程序可以在消息输入框新增一个工具类的项
const currentConversationId = uniIm.currentConversationId
return uniIm.extensions
.invokeExts("input-msg-tool-bar",uniIm.conversation.getCached(currentConversationId))
.filter((result) => result && result.component)
.map((result) => {
return {
component: markRaw(result.component),
props: result.props||{}
};
});
if (currentConversationId) {
return uniIm.extensions
.invokeExts("input-msg-tool-bar",uniIm.conversation.find(currentConversationId))
.filter((result) => result && result.component)
.map((result) => {
return {
component: markRaw(result.component),
props: result.props||{}
};
});
}else{
return []
}
}
// #endif
},
},
props: {
modelValue: {
type: [String, Object],
......@@ -130,7 +136,7 @@
type: Number,
default: 0
}
},
},
mounted() {
currentModelValue = this.modelValue;
// #ifdef H5
......@@ -169,21 +175,20 @@
});
}
// #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)
const {html} = modelValue
this.setContent(html?{html}:modelValue)
}
},
deep: true,
immediate: true
}
},
},
methods: {
sendSoundMsg(e) {
this.$emit('sendSoundMsg',e)
......@@ -191,11 +196,11 @@
sendCodeMsg(e) {
this.$emit('sendCodeMsg',e)
},
showMenberList(e) {
this.$emit('showMenberList',e)
},
oninput(e) {
currentModelValue = e.value;
if(e.data == '@') {
this.$emit('showAboutMenber')
}
this.$emit('update:modelValue', e.value)
this.$emit('input',e)
},
......@@ -247,7 +252,7 @@
this.$refs.editor.callRmd('$addHtmlToCursor',html,focus)
},
setContent(content) {
console.log('setContent', content);
// console.log('setContent', content);
this.$refs.editor.callRmd('$setContent',content)
// this.$emit('update:modelValue', content)
},
......@@ -280,10 +285,10 @@
console.log('confirm');
this.$emit('confirm');
}
}
}
</script>
}
}
</script>
<style lang="scss">
.uni-im-chat-input {
padding:0 5px;
......@@ -414,7 +419,7 @@
flex-direction: column;
#drag-line {
// 鼠标变上下拖动的光标
cursor: ns-resize;
cursor: row-resize;
height: 5px;
position: absolute;
left: 0;
......@@ -476,5 +481,5 @@
}
}
/* #endif */
/* #endif */
</style>
\ No newline at end of file
<template>
<view class="user-list">
<text class="title" v-if="title">{{title}}</text>
<view class="item" v-for="(user,index) in userList" :key="index">
<uni-im-img class="avatar" width="20" height="20" borderRadius="100%"
:src="user?.avatar_file?.url || '/uni_modules/uni-im/static/avatarUrl.png'"></uni-im-img>
<text class="nickname">{{user.nickname}}</text>
<uni-im-icons class="delete" @click="deleteItem(index)" code="e61a" color="#888" size="10px"></uni-im-icons>
</view>
<view class="add-icon" v-if="multiple || userList.length === 0">
<uni-icons @click="showMemberList" color="#aaa" size="16px" type="plusempty"></uni-icons>
</view>
<uni-im-member-list ref="member-list" :conversationId="conversationId" :memberListData="memberListData"></uni-im-member-list>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
emits: ['update:modelValue'],
props: {
modelValue: {
type: Array,
default: []
},
filterUids: {
type: [Array,null],
default: null
},
conversationId: {
default: '',
},
memberListData: {
type: [Array,null],
default: null
},
title: {
type: [String,null],
default: null
},
multiple: {
type: Boolean,
default: true
}
},
data() {
return {
}
},
computed: {
userList() {
return uniIm.users.find(this.modelValue)
}
},
methods: {
deleteItem(index) {
this.$emit('update:modelValue', this.modelValue.filter((_, i) => i !== index))
},
showMemberList() {
this.$refs['member-list'].show({
title: '添加话题成员',
forceShowSearch: true,
filter: member => !(this.filterUids || this.modelValue).includes(member.users._id),
confirm: (uid) => {
console.log('uid--showMenberList-*', uid)
this.$emit('update:modelValue', this.modelValue.concat(uid))
}
})
},
}
}
</script>
<style lang="scss">
.user-list {
flex-direction: row;
flex-wrap: wrap;
align-items: center;
.title {
font-size: 14px;
color: #555;
font-weight: unset;
}
.item {
font-size: 14px;
padding: 0 3px;
margin-left: 6px;
margin-top: 5px;
border: 1px solid #eee;
border-radius: 5px;
flex-direction: row;
justify-content: center;
align-items: center;
.avatar {
margin-right: 5px;
}
.nickname {
color: #333;
}
.delete {
margin: 0 5px;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
height: 26px;
}
.add-icon {
cursor: pointer;
border: 1px #aaa dashed;
height: 26px;
width: 26px;
justify-content: center;
margin-left: 10px;
}
}
</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 class="uni-im-conversation-list">
<view class="conversation-type">
<view class="item" v-for="item in conversation.typeList" :key="item.value" @click="changeType(item.value)" :class="{'active':activeTypeId === item.value}">
{{item.title}}
</view>
</view>
<scroll-view
ref="conversation-list"
class="conversation-list"
:class="{canCheck}"
:style="{'background-color': conversationList.length?'#FFF':''}"
:scroll-top="listScrollTop"
scroll-y="true"
@scrolltolower="loadMore()"
@scrolltolower="conversation.loading ? '' : loadMore()"
@scroll="onScroll"
>
<uni-im-conversation v-for="(item,index) in conversationList" :key="item.id"
<view v-for="(item,index) in conversationList" :key="item.id"
class="conversation-list-item" :class="{'activeConversation':activeConversationId == item.id,'focus':focusConversationId === item.id}"
:conversation="item" :id="item.id"
@click="clickItem(item)" @contextmenu.prevent="openConversationMenu($event,index)"
@longpress="onScroll.isScrolling?'':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"
/>
<uni-im-conversation
:conversation="item" :id="item.id"
@click="clickItem(item)" @contextmenu.prevent="openConversationMenu($event,index)"
@longpress="onScroll.isScrolling?'':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>
</view>
</template>
</uni-im-conversation>
</template>
</uni-im-conversation>
</view>
<view style="margin-top: 5px;backgroundColor:'#f5f5f5'">
<template v-if="!keyword">
<uni-im-load-state
:content-text="loadMoreContentText"
:status="conversationHasMore?'loading':'noMore'"
:status="conversation.hasMore?'loading':'noMore'"
/>
</template>
<text v-else-if="conversationList.length === 0"
......@@ -52,7 +58,7 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const db = uniCloud.database();
let currentScrollTop = 0;
export default {
emits: ['change', 'clickItem', 'onScroll'],
emits: ['change', 'clickItem', 'onScroll', 'update:checkedList'],
props: {
keyword: {
type: String,
......@@ -74,13 +80,12 @@ export default {
data() {
return {
listScrollTop: 0,
focusConversationId: ''
focusConversationId: '',
activeTypeId: 'all'
}
},
computed: {
conversationHasMore() {
return uniIm.conversation.hasMore
},
conversation:()=>uniIm.conversation,
loadMoreContentText() {
return {
contentrefresh: "正在加载...",
......@@ -88,11 +93,31 @@ export default {
}
},
conversationList() {
return uniIm.conversation.dataList
const conversationList = uniIm.conversation.dataList
// #ifdef H5
.filter(i => i.title.toLowerCase().includes(this.keyword.toLowerCase()))
// #endif
.filter(i => !i.hidden)
.filter(conversation=>{
if(typeof this.activeTypeId === 'object' && Object.keys(this.activeTypeId)[0] === 'group_type'){
return conversation.group?.type === this.activeTypeId.group_type
}else{
const action = {
all:()=>true,
unread:()=>conversation.unread_count > 0 || this.oldUnreadCid?.includes(conversation.id),
group:()=>conversation.group_id,
single:()=>!conversation.group_id,
is_star:()=>conversation.is_star
}
return action[this.activeTypeId]?.()
}
})
if(this.activeTypeId === 'unread'){
this.oldUnreadCid = conversationList?.map(i=>i.id)
}else{
this.oldUnreadCid = []
}
return conversationList
}
},
watch: {
......@@ -108,7 +133,7 @@ export default {
// 重试次数
let tryIndex = 0
const scrollToCurrentConversation = () => {
if (tryIndex > 5) {
if (!activeConversationId || tryIndex > 5) {
return
}
const query = uni.createSelectorQuery().in(this);
......@@ -132,12 +157,38 @@ export default {
})
}).exec()
}
scrollToCurrentConversation()
setTimeout(() => {
scrollToCurrentConversation()
},300)
// #endif
}
},
mounted() {},
async mounted() {
// 监听鼠标移入conversation-type 后滚动事件,并设置滚动条位置
// #ifdef H5
const tagBox = this.$el.querySelector('.conversation-type')
function scrollFunc(e) {
tagBox.scrollLeft += e.deltaY/5
}
tagBox.addEventListener('mouseover', (e) => {
tagBox.addEventListener('wheel', scrollFunc)
})
tagBox.addEventListener('mouseout', (e) => {
tagBox.removeEventListener('wheel', scrollFunc)
})
// #endif
},
methods: {
changeType(tagId){
this.activeTypeId = tagId
uniIm.conversation.hasMore = true
uniIm.conversation.loadMore.type = tagId
this.listScrollTop = ''
this.$nextTick(()=>{
this.listScrollTop = 0
})
this.loadMore()
},
onScroll(e) {
currentScrollTop = e.detail.scrollTop
this.$emit('onScroll', e)
......@@ -204,6 +255,12 @@ export default {
conversation.setUnreadCount(conversation.unread_count ? 0 : 1)
}
},
{
"title": (conversation.is_star ? "取消" : "设为") + "星标",
"action": () => {
conversation.changeIsStar()
}
},
{
"title": conversation.mute ? "允许消息通知" : "消息免打扰",
"action": () => {
......@@ -245,8 +302,35 @@ export default {
<style lang="scss">
.uni-im-conversation-list{
&,.conversation-list{
height: 100%;
height: 0;
flex: 1;
overflow: hidden;
/* #ifdef MP-WEIXIN */
height: 100%;
/* #endif */
.conversation-type {
margin: 0 10px;
flex-direction: row;
overflow: scroll;
// 滚动条样式,高度设置
&::-webkit-scrollbar {
width: 0;
height: 0;
}
.item {
color: #333;
text-align: center;
padding: 5px 0;
margin:0 15px;
cursor: pointer;
&.active {
color: #1ab94e;
border-bottom: 2px solid #1ab94e;
}
}
}
.conversation-list{
height:0;
flex: 1;
}
.tip {
......@@ -264,16 +348,18 @@ export default {
}
}
.conversation-list .conversation-list-item.focus {
border: 2px solid #1ab94e;
}
.conversation-list .conversation-list-item {
border-radius: 5px;
}
.conversation-list .conversation-list-item.activeConversation {
background-color: #f1f1f1;
.conversation-list {
.conversation-list-item {
border-radius: 5px;
overflow: hidden;
&.focus
{
border: 2px solid #1ab94e;
}
&.activeConversation {
background-color: #f1f1f1;
}
}
}
.check-box {
......
......@@ -4,13 +4,14 @@
@click="handleClick"
:title="conversation.title"
:note="conversation.note"
:red-note="conversation.hasDraft?'[草稿]&nbsp;':''"
:red-note="redNote"
:tags="conversation.tags"
:avatarUrl="avatarUrl"
:time="friendlyTime"
:badge="conversation.unread_count"
:mute="conversation.mute"
:pinned="conversation.pinned"
:is_star="conversation.is_star"
link
>
<template #left>
......@@ -39,6 +40,14 @@
},
emits: ['click'],
computed: {
redNote() {
if (this.conversation.hasDraft){
return '[草稿]'
}else if (this.conversation.call_list.length){
return '[@我]'
}
return ''
},
friendlyTime() {
// 使得时间会随着心跳动态更新
let timestamp = this.conversation.time + uniIm.heartbeat * 0
......
......@@ -20,8 +20,8 @@
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
emits: ["input", "confirm", "change"],
data() {
return {
data() {
return {
callRmdParam: [],
// #ifdef MP
"textareaValue":this.modelValue,
......@@ -139,116 +139,135 @@
}
});
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();
uniImEditor.addEventListener('paste', async event => {
// 阻止默认行为
event.preventDefault();
// 删除选中的文本
const selection = window.getSelection();
if (!selection.isCollapsed) { // console.log('选中文本', selection.toString());
const range = selection.getRangeAt(0);
range.deleteContents();
selection.removeAllRanges();
}
//某些chrome版本使用的是event.originalEvent
const clipboardData = event.clipboardData || event.originalEvent?.clipboardData;
let htmlString = clipboardData.getData('text/html');
console.log('paste',{htmlString});
if (htmlString) {
// 获取html字符串
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 items = clipboardData.items;
for (const item of items) {
if (item.type.indexOf("image") !== -1) {
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();
}
}
}
}
// 获取文本
const clipboardDataText = clipboardData.getData('text');
if (clipboardDataText) {
this.$addHtmlToCursor(clipboardDataText)
}
}
})
async function filterHTML(htmlString) {
// 过滤html字符串,只保留:文字,图片,链接
// html字符串转dom对象,并深度遍历
let div = document.createElement('div');
div.innerHTML = htmlString;
let tmpDiv = document.createElement('div');
tmpDiv.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);
await deepTr(tmpDiv);
// 销毁tmpDiv
tmpDiv = null;
return arr.join('');
async function deepTr(node) {
// console.log('node', node);
if (node.nodeType === 1) {
const action = {
async 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'});
}
// 返回一个Blob对象
return new Blob([new Uint8Array(byteArrays)], {type: 'image/png'});
node.src = URL.createObjectURL( base64ToBlob(res.data) )
}
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);
}
arr.push(`<img src="${node.src}" />`);
},
A(){
// 只保留href属性
arr.push(`<a href="${node.href}">${node.innerText}</a>`);
}
}
await action[node.tagName]?.();
for (let i = 0; i < node.childNodes.length; i++) {
await deepTr(node.childNodes[i]);
}
} else if (node.nodeType === 3) {
// 排除非内容的文本节点
const tagNameList = ['A','IMG','STYLE','SCRIPT'];
if (!tagNameList.includes(node.parentNode.tagName)) {
// 把node中的<>&转义
node.nodeValue = node.nodeValue.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
arr.push(node.nodeValue.trim());
// 如果父节点是需要换行的标签,且当前节点是最后一个节点,则添加换行
const tagNameList2 = ['P','DIV','FONT','H1','H2','H3','H4','H5','H6','LI','UL','OL','BR'];
if (tagNameList2.includes(node.parentNode.tagName) && node.parentNode.lastChild === node) {
arr.push('\n');
}
}else{
// console.log('非内容文本节点', node.parentNode.tagName,node);
}
}else{
// console.log('其他节点', node);
}
}
await deep(div);
return arr.join('');
}
// #endif
},
......@@ -266,7 +285,7 @@
value
}
// #endif
// console.error('input',e)
// console.error('input',e)
this.$emit('input', e)
}
}
......@@ -274,7 +293,7 @@
</script>
<!-- #ifndef MP -->
<script module="rdm" lang="renderjs">
let uniImEditor,lastFocusNode,lastCursor;
let lastFocusNode,lastCursor;
// #ifdef APP
function setSoftinputTemporary() {
console.log('setSoftinputTemporary')
......@@ -289,25 +308,27 @@
export default {
name: 'uni-im-editor',
data() {
return {}
},
return {
uniImEditor: null,
}
},
mounted() {
uniImEditor = document.querySelector('.uni-im-editor')
uniImEditor.addEventListener('input', e => {
this.uniImEditor = document.querySelector('.uni-im-editor')
this.uniImEditor.addEventListener('input', e => {
setTimeout(()=>this.$oninput(e.data),0);
});
uniImEditor.addEventListener('click', e => {
this.uniImEditor.addEventListener('click', e => {
// console.log('click', e);
// #ifdef APP
setSoftinputTemporary()
// #endif
setTimeout(this.$refreshLastCursor, 0);
});
uniImEditor.addEventListener('blur', e => {
this.uniImEditor.addEventListener('blur', e => {
// console.error('###blur', e);
});
// 键盘敲左右
uniImEditor.addEventListener('keydown', e => {
this.uniImEditor.addEventListener('keydown', e => {
setTimeout(this.$refreshLastCursor, 0);
});
......@@ -323,7 +344,7 @@
}
});
})
.observe(uniImEditor, {
.observe(this.uniImEditor, {
childList: true, // 监听子节点的变化
});
},
......@@ -332,15 +353,15 @@
this.$nextTick(() => {
// console.log('modelValue', modelValue);
if (modelValue.length === 0) {
uniImEditor.innerHTML = ''
this.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
this.uniImEditor.innerHTML = modelValue
} else if (typeof modelValue === 'object' && modelValue.html != this.uniImEditor.innerHTML) {
this.uniImEditor.innerHTML = modelValue.html
}
});
},
},
},
methods: {
// 刷新光标信息
$refreshLastCursor() {
......@@ -352,24 +373,22 @@
// });
},
$oninput(data){
this.$refreshLastCursor()
// 耗时计算
let start = new Date().getTime();
let param = '';
let val = uniImEditor.innerHTML;
let val = this.uniImEditor.innerHTML;
const hasImg = uniImEditor.querySelector('img');
const hasNickname = uniImEditor.querySelector('.nickname');
const hasA = uniImEditor.querySelector('a');
const hasImg = this.uniImEditor.querySelector('img');
const hasNickname = this.uniImEditor.querySelector('.nickname');
const hasA = this.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('user_id'))
"text": this.uniImEditor.innerText,
"aboutUserIds": Array.from(this.uniImEditor.querySelectorAll('.nickname')).map(i=>i.getAttribute('user_id'))
}
} else {
param = this.$inputText()
......@@ -411,7 +430,7 @@
try{
const range = document.createRange();
const sel = window.getSelection();
range.setStart(lastFocusNode || uniImEditor, lastCursor);
range.setStart(lastFocusNode || this.uniImEditor, lastCursor);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
......@@ -468,12 +487,12 @@
selection.addRange(range); // 将更新后的范围添加回选择区域
}
}else{
uniImEditor.innerHTML += html;
this.uniImEditor.innerHTML += html;
}
this.$oninput(html);
},
$inputText() {
return uniImEditor.innerHTML.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "");
$inputText() {
return this.uniImEditor.innerText.trim();
},
$focus() {
if(document.activeElement.className === 'uni-im-editor'){
......@@ -483,12 +502,12 @@
setSoftinputTemporary()
// #endif
// console.error('获取焦点',document.activeElement.className);
uniImEditor.focus();
this.uniImEditor.focus();
// console.error('获取焦点',document.activeElement.className);
},
$blur(){
// console.error('失去焦点1',document.activeElement.className);
uniImEditor.blur();
this.uniImEditor.blur();
// console.error('失去焦点2',document.activeElement.className);
setTimeout(()=>{
// console.error('失去焦点3',document.activeElement.className);
......@@ -505,12 +524,15 @@
// this.$emit('update:focus', true);
},
$setContent(data) {
if (typeof data === 'string') {
uniImEditor.innerHTML = data
if (typeof data === 'string') {
// 为兼容旧版- s &gt; 转换为 > $lt; 转换为 < &amp; 转换为 &
data = data.replace(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/g, '&');
// 为兼容旧版- e
this.uniImEditor.innerText = data;
this.$oninput(data);
} else {
uniImEditor.innerHTML = this.$arrDomJsonToHtml(data);
this.$oninput(uniImEditor.innerHTML);
} else if (typeof data === 'object') {
this.uniImEditor.innerHTML = data.html || this.$arrDomJsonToHtml(data);
this.$oninput(this.uniImEditor.innerHTML);
}
},
$arrDomJsonToHtml(arr) {
......@@ -541,7 +563,7 @@
result += parseItem(item);
}
return result;
}
}
}
}
</script>
......
<template>
<view class="format-time-text" @click="onclick">
<text class="text">{{friendlyTime}}</text>
<text class="text detail" v-if="showDetail">({{timeString}})</text>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
props: {
time: {
type: Number,
default: 0
},
},
computed: {
friendlyTime() {
let time = this.time
// 使得时间会随着心跳动态更新
time = time + uniIm.heartbeat * 0
return uniIm.utils.toFriendlyTime(time)
},
},
data() {
return {
timeString: new Date(this.time).toLocaleString(),
showDetail: false
}
},
mounted() {
// #ifdef H5
// 鼠标移入移出事件 设置显示时间
this.$el.addEventListener('mouseenter', () => {
this.showDetail = true
})
this.$el.addEventListener('mouseleave', () => {
this.showDetail = false
})
// #endif
},
methods: {
onclick() {
this.showDetail = true
setTimeout(() => {
this.showDetail = false
}, 2000)
}
}
}
</script>
<style lang="scss">
.format-time-text {
flex-direction: row !important;
justify-content: center;
align-items: center;
.text {
font-size: 12px;
text-align: center;
color: #999999;
line-height: 22px;
}
.detail {
margin-left: 5px;
}
}
</style>
......@@ -50,7 +50,7 @@
<style lang="scss">
.system-msg-box{
margin: 0 150rpx;
margin: 0 30px;
}
.hidden {
height: 0;
......@@ -66,7 +66,6 @@
.group-notification {
padding:14px 0;
background-color: #FFFFFF;
width: 600rpx;
margin-top: 10px;
}
.group-notification .title-box{
......
......@@ -57,7 +57,7 @@
}
@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');
src: url('https://at.alicdn.com/t/c/font_3726059_aoe3vlbh3os.ttf?t=1729588562823') format('truetype');
}
.uni-im-share:before {
content: "\e6c4";
......
<template>
<image @load="load" :src="url" :mode="mode" :style='{"width":viewWidth,"height":viewHeight}' @click="handleClick"></image>
<image @load="load" :src="url" :mode="mode" :style='{"width":viewWidth,"height":viewHeight,borderRadius}' @click="handleClick"></image>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import config from '@/uni_modules/uni-im/common/config.js';
export default {
emits: ['click'],
props: {
......@@ -16,7 +17,7 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
default: ''
},
maxWidth: {
type: [String,Number],
type: [String,Number,Boolean],
default: false
},
maxHeight: {
......@@ -30,6 +31,10 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
height: {
type: [String,Number],
default: ''
},
borderRadius: {
type: [String,Number],
default: ''
}
},
data() {
......@@ -66,14 +71,27 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
src:{
async handler(src) {
if(src){
// 判断是否为base64 blob ./ / @/开头的本地图片
if(src.indexOf('blob:') === 0 || src.indexOf('./') === 0 || src.indexOf('/') === 0 || src.indexOf('@/') === 0){
this.url = src
return
}
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_200/quality,q_80'
// 文件存储的服务商
const {provider} = config.cloudFile
switch(provider){
case 'aliyun':
this.url += '?x-oss-process=image/resize,w_400/quality,q_80'
break
case 'tencent':
this.url += '?imageMogr2/thumbnail/400x400>'
break
case 'qiniu':
this.url += '?imageMogr2/thumbnail/400x400>'
break
}
}
},
immediate: true
......@@ -96,8 +114,8 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
}
}
},
handleClick(){
this.$emit('click')
handleClick(event){
this.$emit('click',event)
},
// 如果是rpx单位,转换为px
toPx(val){
......
......@@ -18,11 +18,11 @@
<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>
<view class="state">
<image v-if="mute" class="mute" mode="widthFix" src="@/uni_modules/uni-im/static/mute.png"/>
<image v-if="is_star" class="star" mode="widthFix" src="@/uni_modules/uni-im/static/star.png"/>
<template v-if="badge">
<view v-if="mute" class="red-point"></view>
<view v-if="mute || badge == 'dot'" class="red-point"></view>
<view v-else class="badge">{{badge > 99 ? '99+' : badge}}</view>
</template>
</view>
......@@ -60,7 +60,7 @@
default: ''
},
badge: {
type: Number,
type: [String,Number],
default: 0
},
mute: {
......@@ -78,6 +78,10 @@
link: {
type: Boolean,
default: false
},
is_star: {
type: Boolean,
default: false
}
},
computed: {
......@@ -127,16 +131,18 @@
/* #endif */
.avatar-box{
position: relative;
width: 40px;
height: 40px;
border-radius: 5px;
overflow: hidden;
.avatar {
width: 40px;
height: 40px;
border-radius: 5px;
flex-shrink: 0;
width: 100%;
height: 100%;
}
}
.main {
flex-grow: 1;
justify-content: center;
justify-content: space-around;
overflow: hidden;
margin-left: 5px;
flex-shrink: 1;
......@@ -146,7 +152,7 @@
// min-height: 20px;
.title {
font-size: 16px;
font-size: 14px;
color: #333;
@include ellipsis;
}
......@@ -186,10 +192,10 @@
margin-left: 2px;
}
.mute {
width: 13px;
height: 13px;
opacity: 0.7;
.star,.mute {
width: 12px;
height: 12px;
opacity: 0.5;
}
.badge {
......@@ -217,7 +223,8 @@
.red-note {
font-size: 12px;
white-space: nowrap;
color: #f00;
color: #f00;
margin-right: 3px;
}
.note {
......
<template>
<view v-if="isShow && ((isWidescreen && !forceShowSearch) ? memberList.length != 0 : 1)" class="member-list-box">
<view class="member-list-mark" @click.stop="hide()"></view>
<view class="content">
<view class="head">
<view class="close">
<uni-icons @click="hide()" type="back" color="#000" size="12px"></uni-icons>
</view>
<text class="title">{{title}}</text>
</view>
<uni-search-bar v-if="!isWidescreen || forceShowSearch" v-model="keyword" :focus="true" placeholder="搜索" cancelButton="none" class="search" />
<scroll-view class="member-list" :scroll-y="true" :scroll-top="scrollTop" :show-scrollbar="true">
<view v-for="(item,index) in memberList" :key="item._id" class="member-list-item"
:class="{'member-list-item-active':activeUid == item.users._id}" @mouseover="activeUid = item.users._id"
@click="confirm(item.users._id)" :id="'a'+item.users._id">
<text class="nickname">{{item.users.nickname}}</text>
<text class="real_name">{{item.users.real_name}}</text>
</view>
<view class="null-about-menber-tip" v-if="memberList.length === 0">没有与"{{keyword}}"相关成员</view>
</scroll-view>
</view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
let confirmFunc = () => {}
export default {
props: {
conversationId: {
default: ''
},
memberListData: {
type: [Array,null],
default: null
}
},
computed: {
...uniIm.mapState(['isWidescreen']),
memberList() {
let memberList = []
if(this.memberListData){
memberList = this.memberListData
}else{
memberList = uniIm.conversation.find(this.conversationId)?.group?.member.dataList || []
}
if (this.mapFn) {
memberList = memberList.map(this.mapFn)
}
if (this.filterFn) {
memberList = memberList.filter(this.filterFn)
}
if (this.keyword) {
memberList = memberList.filter(item => item.users.nickname.toLowerCase().includes(this.keyword.toLowerCase()))
}
// 按昵称排序
memberList.sort((a, b) => {
return a.users.nickname.localeCompare(b.users.nickname)
})
this.activeUid = memberList[0]?.users._id
return memberList
},
},
data() {
return {
isShow: false,
scrollTop: 0,
activeUid: '',
filterFn: null,
mapFn: null,
title: '',
keyword: '',
forceShowSearch: false
}
},
methods: {
confirm(uid) {
confirmFunc(uid)
this.hide()
},
onChatInput(e){
const enterText = e.data
if (!this.isShow) return
// console.log('enterText',enterText)
if(enterText == null){
// 敲了删除键
this.keyword = this.keyword.slice(0,-1)
}
else if(enterText == '@'){
this.keyword = ""
}else{
setTimeout(()=>{
// 输入法正在输入中
let isComposing = false
// #ifdef H5
isComposing = this.isWidescreen ? document.querySelector('.uni-im-editor').isComposing : e.isComposing
// #endif
if (isComposing) {
console.log('输入法正在输入中')
}else{
// console.log('enterText')
this.keyword += enterText
}
},0)
}
},
onChatInputKeydown(e) {
if (!this.isShow) return
if(e.key == 'Enter'){
if(this.memberList.length){
// console.log('选中要@的人')
this.confirm(this.activeUid)
}
}else if(["ArrowUp", "ArrowDown"].includes(e.key)){
// console.log('上下箭头选择@谁')
let index = this.memberList.findIndex(i => i.users._id == this.activeUid)
// console.log('index',index);
if (e.key == "ArrowUp") {
index--
} else {
index++
}
if (index < 0 || index > this.memberList.length - 1) {
index = 0
}
this.activeUid = this.memberList[index].users._id
// 防止选中的成员看不到,触发滚动
this.scrollTop = (index - 3) * 45
// console.log('this.scrollTop',this.scrollTop);
e.preventDefault();
}else if(["ArrowLeft", "ArrowRight"].includes(e.key)){
this.hide()
}else if(e.key == 'Backspace'){
setTimeout(() => {
// 获取e.target 元素内不包含在标签内的文字内容
let newValue = e.target.innerText
// console.log('删除键',newValue,this.oldChatInputValue);
// 拿到newValue 和 this.oldChatInputValue 中 包含的@字符的个数
let newAtN = newValue.replace(/[^@]/g, "").length
let oldAtN = this.oldChatInputValue?.replace(/[^@]/g, "")?.length || 0
if(newAtN === 0 || newAtN < oldAtN){
// console.log('删除了@成员的昵称');
this.hide()
}
this.oldChatInputValue = newValue
}, 0);
}
},
show({confirm,...param}) {
if(this.isShow){
return console.log('已经显示了')
}
// console.log('show',param, confirm)
// 默认选中第一个
// console.log('this.memberList[0]',this.memberList[0])
this.isShow = true
if (confirm) {
confirmFunc = confirm
}
this.keyword = ''
this.filterFn = param.filter
this.mapFn = param.map
this.title = param.title || '选择提醒的人'
this.forceShowSearch = param.forceShowSearch
},
hide() {
this.keyword = ''
this.isShow = false
}
}
}
</script>
<style lang="scss">
.member-list-box {
.content {
position: fixed;
width: 750rpx;
height: 80vh;
bottom: 0;
z-index: 999;
background-color: #ffffff;
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;
width: unset;
min-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;
/* #ifdef H5 */
cursor: pointer;
z-index: 1;
/* #endif */
}
.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: 0;
flex-grow: 1;
padding: 10px;
.member-list-item {
overflow: hidden;
height: 40px;
width: 100%;
font-size: 14px;
line-height: 40px;
padding:0 15px;
border-radius: 10px;
margin-bottom: 10px;
text-align: left;
flex-direction: row;
/* #ifdef H5 */
@media screen and (min-device-width:960px){
margin:0;
margin-bottom: 5px;
cursor: pointer;
}
/* #endif */
.real_name {
margin-left: 10px;
font-size: 12px;
color: #666;
}
}
.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: 999;
}
}
</style>
<template>
<view class="filter-contorl" v-if="userIdList.length">
<text class="title">已进入话题</text>
<uni-im-icons class="exit" @click="userIdList=[]" code="e61a" color="#999" size="12px"></uni-im-icons>
<uni-im-choose-user title="相关成员" :conversationId="conversationId" v-model="userIdList"></uni-im-choose-user>
</view>
</template>
<script>
export default {
emits: ['showMenberList', 'change'],
props: {
conversationId: {
default: ''
},
},
data() {
return {
userIdList: []
}
},
watch: {
userIdList(newVal) {
this.$emit('change', newVal)
}
},
methods: {
onchange(userIdList) {
this.userIdList = userIdList
}
}
}
</script>
<style lang="scss">
.filter-contorl {
justify-content: center;
border-bottom: 2px #bee1ff solid;
position: sticky;
top: 0;
background-color: #FFF;
font-size: 14px;
padding: 8px;
width: 100%;
z-index: 999;
.title {
font-size: 16px;
font-weight: bold;
color: #1399ff;
margin-bottom: 5px;
}
.exit {
position: absolute;
right: 10px;
top: 10px;
opacity: 0.7;
&:hover {
opacity: 1;
}
}
}
</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>
......@@ -81,7 +81,7 @@
{
title:'复制',
action:()=>this.copyContent(),
canDisplay: ["userinfo-card","rich-text","text","image"].includes(msg.type) && (msg.type != 'image' || uniIm.systemInfo.uniPlatform === "web"),
canDisplay: (uniIm.systemInfo.uniPlatform === "web" && ["userinfo-card","rich-text","image"].includes(msg.type) || msg.type == 'text' ),
},
{
title:'撤回',
......@@ -105,6 +105,14 @@
}
]
if(msg.about_msg_id){
this.controlList.push({
title:'进入会话',
action:()=> this.$emit('intoTopic', msg._id),
canDisplay:true,
})
}
// 拿到扩展点的数据
let extensionsControlList = uniIm.extensions.invokeExts('msg-popup-controls',msg)
this.controlList = this.controlList.concat(...extensionsControlList)
......@@ -199,63 +207,74 @@
})
},
async copyContent(){
// console.log('setClipboardData');
// console.log('this.controlData',this.controlData);
// #ifdef H5
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])
})
return
}
// #endif
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':
/*
let html = data.map(i=>i.name == 'img' ? `<img src="${i.attrs.src}">` : i.text).join(' ')
// 把data写到剪切板
// 将HTML字符串转换为ArrayBuffer,确保UTF-8编码
const encoder = new TextEncoder();
const htmlArrayBuffer = encoder.encode(html);
// 创建一个包含HTML内容的ClipboardItem
const clipboardItem = new ClipboardItem({
'text/html': new Blob([htmlArrayBuffer], { type: 'text/html' }),
});
// 尝试将ClipboardItem写入剪切板
try {
await navigator.clipboard.write([clipboardItem]);
console.log('HTML内容已写入剪切板');
} catch (err) {
console.error('写入剪切板失败: ', err);
let data = ''// 需要复制的数据
const msgBody = this.controlData.msg.body
const action = {
text(){
data = msgBody
},
'userinfo-card'(){
data = location.origin + '/#/?user_id='+msgBody.user_id
},
// #ifdef H5
async image(){
let url = msgBody.url
url = await uniIm.utils.getTempFileURL(url)
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])
})
},
async 'rich-text'(){
let html = ""
for(let i of msgBody){
if(i.name == 'img'){
const url = await uniIm.utils.getTempFileURL(i.attrs.src)
html += `<img src="${url}">`
}else if(i.name == 'a'){
html += `<a href="${i.attrs.href}">${i.children[0].text}</a>`
}else{
html += i.text
}
return
*/
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;
}
data = html
// 把data写到剪切板
// 将HTML字符串转换为ArrayBuffer,确保UTF-8编码
const encoder = new TextEncoder();
const htmlArrayBuffer = encoder.encode(html);
// 创建一个包含HTML内容的ClipboardItem
const clipboardItem = new ClipboardItem({
'text/html': new Blob([htmlArrayBuffer], { type: 'text/html' }),
});
data = clipboardItem
// 尝试将ClipboardItem写入剪切板
try {
await navigator.clipboard.write([clipboardItem]);
// console.log('HTML内容已写入剪切板');
} catch (err) {
console.error('写入剪切板失败: ', err);
}
}
// #endif
}
await action[this.controlData.msg.type]?.()
uni.setClipboardData({
data,
complete:(e)=> {
uni.hideToast()
// console.log(e);
}
})
if(typeof data === 'string'){
uni.setClipboardData({
data,
complete:(e)=> {
uni.hideToast()
// console.log(e);
}
})
}else{
// console.log('非字符串',data);
}
},
canRevoke() {
let current_uid = uniCloud.getCurrentUserInfo().uid
let current_uid = uniIm.currentUser._id
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);
......@@ -273,8 +292,8 @@
let isGroupAdmin = false
if(group_id){
let conversation = uniIm.conversation.getCached(conversation_id)
isGroupAdmin = conversation.group_info.user_id == current_uid
let conversation = uniIm.conversation.find(conversation_id)
isGroupAdmin = conversation.group.user_id == current_uid
}
// console.log('isGroupAdmin',isGroupAdmin);
// 如果是群主
......@@ -286,14 +305,10 @@
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)
uniIm.conversation.find(conversation_id).revokeMsg(msg_id)
}else{
uni.showToast({
title: '已超过2分钟,不能撤回',
......@@ -314,7 +329,7 @@
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)
// conversation.msgManager.localMsg.update(this.controlData.msg.unique_id,this.controlData.msg)
},
other(){
......
<template>
<view class="encryption-tip">
此消息为加密消息{{hasKey?',与配置的密钥不匹配':',当前未配置密钥'}},无法查看
</view>
</template>
<script>
/**
* 只有未被成功解密的消息才会显示在这个组件
*/
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
props: {
msg: {
type: Object,
default: () => {}
}
},
computed: {
hasKey() {
uniIm.ext.encryptionKey?.[uniIm.currentConversationId]
}
},
methods: {
}
}
</script>
<style>
.encryption-tip {
padding:8px 15px;
margin-top:5px;
background-color: #FFF;
border-radius: 10px;
color: #999;
border: 1px dashed #eee;
}
</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>
<text class="name">{{fileName}}</text>
<text class="size">{{fileSize}}</text>
</view>
<uni-im-icons code="e858" size="50" color="#EEEEEE" class="file-icon" />
<uni-im-icons code="e7d0" size="50" color="#EEEEEE" class="file-icon" />
</view>
</template>
......@@ -90,29 +86,27 @@
}
</script>
<style>
.file-msg-box {
<style lang="scss">
.file-msg-box.msg-content {
background-color: #FFFFFF;
width: 500rpx;
padding: 10px;
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 {
word-wrap: break-word;
font-size: 16px;
}
.file-msg-info-size {
font-size: 12px;
color: #666;
flex-direction: row;
justify-content: space-between;
.file-msg-info {
flex-direction: column;
justify-content: space-around;
.name {
word-wrap: break-word;
font-size: 16px;
}
.size {
font-size: 12px;
color: #666;
}
}
.file-icon {
}
}
</style>
\ No newline at end of file
......@@ -36,9 +36,8 @@
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:'';
let currentUid = uniIm.currentUser._id;
let currentNickname = uniIm.users.getNickname(currentUid);
// 找到不是自己的nickname去重
let nickname = this.msgList.map(item=>item.nickname).filter(nickname=>nickname!==currentNickname)[0]
this.title = currentNickname + (nickname?''+nickname:'')+'的聊天记录';
......@@ -66,7 +65,7 @@
}
</script>
<style>
<style lang="scss">
/* #ifdef H5 */
.history-msg,.history-msg * {
cursor: pointer;
......@@ -74,27 +73,24 @@
/* #endif */
.history-msg {
padding: 10px;
border-radius: 5px;
background-color: #fff;
width: 600rpx;
max-height:200px !important;
overflow: hidden;
}
.item-text-list{
max-height: 150px !important;
overflow: hidden;
}
.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;
.title {
text-align: left;
font-size: 16px;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
margin-bottom: 5px;
}
.item-text-list{
max-height: 150px !important;
overflow: hidden;
.item-text {
color: #999;
font-size: 14px;
}
}
}
</style>
\ No newline at end of file
<template>
<view class="msg-order">
<text class="title">订单信息</text>
<view class="item">
<text class="title">订单编号:</text>
<text :selectable="true" class="value">{{orderInfo.order_id}}</text>
</view>
<view class="item">
<text class="title">产品/服务名称:</text>
<text class="value">{{orderInfo.product.name}}</text>
</view>
<view class="item">
<text class="title">订单价格:</text>
<text class="value">{{orderInfo.product.price}}</text>
</view>
<view class="item">
<text class="title">所属用户:</text>
<text class="value">{{nickName}}</text>
</view>
<view class="item">
<text class="title">订单状态:</text>
<text class="value">{{status}}</text>
</view>
<template v-if="orderInfo.status === 1">
<view class="item">
<text class="title">支付时间:</text>
<text class="value">{{new Date(orderInfo.pay_time).toLocaleString()}}</text>
</view>
<view class="item">
<text class="title">支付渠道:</text>
<text class="value">{{orderInfo.pay_channel}}</text>
</view>
</template>
<template v-else-if="isMineOrder && !isExpired">
<button class="btn" type="primary" @click="toPay" :disabled="isExpired">前往支付</button>
<view class="tip">请在 {{new Date(orderInfo.expire_time).toLocaleString()}} 前完成支付 </view>
</template>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
props: {
msg: {
type: Object,
default: () => {
return {}
}
}
},
computed: {
orderInfo() {
return this.msg.body
},
nickName() {
return uniIm.users.find(this.orderInfo.user_id)[0]?.nickname || '未知'
},
isExpired() {
let now = Date.now() + uniIm.heartbeat * 0
return now > this.orderInfo.expire_time
},
isMineOrder() {
// console.log('isMineOrder',this.orderInfo.user_id,uniIm.currentUser._id)
return this.orderInfo.user_id == uniIm.currentUser._id
},
status() {
if(this.orderInfo.status == 1) {
return '已支付'
}
if(this.isExpired) {
return '已过期'
}
if(this.orderInfo.status == 0) {
return '待支付'
}
return '待支付'
}
},
data() {
return {
}
},
methods: {
async toPay() {
const uniIdSpaceConfig = {
provider: 'private',
spaceName: 'uni-id-server',
spaceId: 'uni-id-server',
clientSecret: 'ba461799-fde8-429f-8cc4-4b6d306e2339',
endpoint: 'https://account.dcloud.net.cn'
}
const uniIdCenterEnv = uniCloud.init(uniIdSpaceConfig)
const uniIdCenterObj = uniIdCenterEnv.importObject('uni-id-co')
let oauthToken;
try {
let res = await uniIdCenterObj.getOauthToken()
console.log('getOauthToken',res)
oauthToken = res.data.access_token
} catch (e) {
return uni.showModal({
content: JSON.stringify(e),
showCancel: false
});
}
const url = `https://dev.dcloud.net.cn/pages/common/pay?return_url=`
+ encodeURIComponent('/uni_modules/uni-trade/pages/order-payment/order-payment?order_id='+this.orderInfo.order_id)
+ '&oauthToken=' + oauthToken
console.log('url',url)
window.open(url, '_blank')
}
}
}
</script>
<style lang="scss">
.msg-order {
background-color: #FFF;
border-radius: 10px;
padding: 25px;
max-width:100%;
& > .title {
font-size: 18px;
font-weight: bold;
}
.item {
flex-direction: row;
justify-content: start;
margin-top: 10px;
.title {
color: #666;
// 不换行
white-space: nowrap;
}
.value {
color: #333;
font-size: 14px;
// 换行
white-space: pre-wrap;
word-break: break-all;
}
}
.tip {
font-size: 14px;
color: #666;
margin-top: 10px;
}
.btn {
margin-top: 20px;
height: 36px;
line-height: 36px;
padding: 0;
}
}
</style>
<template>
<view class="pay-notify">
<text class="title">支付成功通知</text>
<view class="item">
<text class="title">订单编号:</text>
<text :selectable="true" class="value">{{msg.body.order_id}}</text>
</view>
<view class="item">
<text class="title">支付时间:</text>
<text :selectable="true" class="value">{{new Date(msg.create_time).toLocaleString()}}</text>
</view>
<view class="item">
<text class="title">支付金额:</text>
<text :selectable="true" class="value">{{msg.body.price / 100}}</text>
</view>
<view class="item">
<text class="title">支付渠道:</text>
<text :selectable="true" class="value">{{msg.body.pay_channel}}</text>
</view>
</view>
</template>
<script>
export default {
props: {
msg: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {
}
},
methods: {
}
}
</script>
<style lang="scss">
.pay-notify {
background-color: #FFF;
border-radius: 10px;
padding: 25px;
max-width: 100%;
& > .title {
font-size: 18px;
font-weight: bold;
}
.item {
flex-direction: row;
justify-content: start;
margin-top: 10px;
.title {
color: #666;
// 不换行
white-space: nowrap;
}
.value {
color: #333;
font-size: 14px;
// 换行
white-space: pre-wrap;
word-break: break-all;
}
}
.tip {
font-size: 14px;
color: #666;
margin-top: 10px;
}
.btn {
margin-top: 20px;
height: 36px;
line-height: 36px;
padding: 0;
}
}
</style>
<template>
<view class="uni-im-rich-text"
<template>
<view class="uni-im-rich-text"
:class="{'isFromSelf':isFromSelf, 'only1u': trBody.length === 0 &&webInfoList.length === 1 , isSingeImg}">
<template v-for="(item,index) in trBody" :key="index">
<template v-if="item.name == 'span'">
<text v-if="item.attrs && item.attrs.class == 'nickname'" class="text nickname"
:class="{pointer:canPrivateChat}"
:class="{'pointer':canPrivateChat,'isCallMe':item.attrs.user_id == currentUid}"
@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>
</text>
<uni-im-icons class="text read-state" v-if="isFromSelf && 'isRead' in item"
:class="{isRead:item.isRead}" :code="item.isRead?'e609':'e741'"
:size="item.isRead?'12px':'10px'" :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="200px" @click="previewImage(item.attrs.src)"
<text class="text" v-else-if="item.type == 'text'" :decode="true" space="ensp">{{trText(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' && item.children && typeof(item.children[0]) === 'object'" :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)
// }
// }
},
<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>
<!-- <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: {
currentUid() {
return uniIm.currentUser._id
},
isSingeImg() {
return this.msg.body.filter(item => item.name != 'img' && item.text || item?.attrs?.class === "nickname" ).length === 0
},
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
})
}
},
imgList() {
return this.msg.body.filter(item => item.name == 'img').map(item => item.attrs.src)
},
isFromSelf() {
return this.msg.from_uid === this.currentUid
},
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)
}]
if(node.attrs.user_id == '__ALL'){
delete node.isRead
}else{
// 设置是否已读
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 {conversation_id,from_uid} = this.msg
const {group_member} = uniIm.conversation.getCached(conversation_id)
return group_member ? group_member[from_uid]?.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'
})
}
})
const {group_member} = uniIm.conversation.find(conversation_id)
return group_member ? group_member.find(from_uid)?.role.includes('admin') : false
}
},
methods: {
trText(str) {
// TODO:临时方案,解决 \n 被转义的问题
return str.replace(/\\n/g, "\\\\n")
},
getNicknameByUid(uid) {
if(uid === "__ALL"){
return "所有人"
}
return uniIm.users.getNickname(uid);
},
async previewImage(src) {
const urls = []
for (let i = 0; i < this.imgList.length; i++) {
const url = await uniIm.utils.getTempFileURL(this.imgList[i])
urls.push(url)
}
src = await uniIm.utils.getTempFileURL(src)
uni.previewImage({
urls,
current: src
})
},
copy(text) {
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({
title: '复制成功',
icon: 'none'
})
}
})
},
privateChat(user_id) {
if (this.canPrivateChat) {
if (this.canPrivateChat && user_id != '__ALL') {
uniIm.toChat({
user_id,
source:{
......@@ -165,18 +181,22 @@
}
})
}
}
}
}
</script>
}
}
}
</script>
<style lang="scss">
.uni-im-rich-text {
display: inline-block;
// todo: 由于web端首页引入的sass热更新不能影响组件样式,导致baseStyle.scss的样式使用了 deep,导致这里需要加 !important
display: inline-block!important;
background-color: #fff;
padding: 10px;
border-radius: 10px;
max-width: 100%;
&.isFromSelf {
background-color: #c9e7ff;
}
/* #ifdef H5 */
@media screen and (min-device-width:960px) {
.img {
......@@ -205,10 +225,6 @@
word-break: break-all;
}
.isFromSelf {
background-color: #c9e7ff !important;
}
.only1u {
padding: 0;
.web-info {
......@@ -228,14 +244,20 @@
.nickname {
color: #0b65ff;
margin: 0 2px;
font-size: 15px;
padding: 1px 4px;
font-size: 14px;
}
.isCallMe {
color: #FFF;
background-color: #0080ff;
border-radius: 5px;
margin-right: 4px;
}
.isRead {
.read-state {
position: relative;
top: -3px;
margin-right: 3px;
top: -5px;
}
.isSingeImg {
......
<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>
<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';
let audioContext = uniIm.audioContext;
let 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
}
},
props: {
msg: {
type: Object,
default () {
return {
from_uid: '',
body: {
time: 0
}
}
}
},
soundBoxWidth: {
type: String,
default: '100px'
}
},
computed: {
self() {
return this.msg.from_uid === uniIm.currentUser._id
}
},
beforeCreate() {
audioContext = uniIm.audioContext;
},
mounted() {
this.onPlay = async () => {
console.log('soundPlayStart------------------',this.msg.body);
let currentAudioUrl = await uniIm.utils.getTempFileURL(this.msg.body.url)
let src = audioContext.src
if (src == currentAudioUrl) {
this.soundPlayState = 1
} else {
this.soundPlayState = 0
}
},
mounted() {
this.onPlay = async () => {
console.log('soundPlayStart------------------',this.msg.body);
let currentAudioUrl = await uniIm.utils.getTempFileURL(this.msg.body.url)
let src = 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.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);
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)
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>
}
}
</script>
<style lang="scss">
.sound-box {
/* #ifdef H5 */
......@@ -133,5 +133,5 @@
transition-delay: 0.1s;
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1.0);
}
}
}
</style>
\ No newline at end of file
......@@ -12,8 +12,7 @@
export default {
data() {
return {
content: false,
create_time:false
create_time:0
}
},
props: {
......@@ -27,49 +26,50 @@
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
},
content(){
const msg = this.msg
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.create_time = value.create_time
return value.content
}else if(key == "avatar_file"){
return "群聊头像已更新"// 已在 msg-list 组件隐藏此类型消息
}
// mute_all_members
else if(key == "mute_all_members"){
return value ? "已开启“全员禁言”" : "已关闭“全员禁言”"
} else{
return {
"name":" 群聊名称",
"introduction":"群简介"
}[key] + "已更新为:" + value
}
},
deep: true,
immediate: true
}else if( ["join-group","group-exit","group-expel"].includes(msg.action) ){
const nicknameList = []
msg.body.user_id_list.forEach(async (user_id) => {
const nickname = uniIm.users.getNickname(user_id)
// console.log(nickname)
nicknameList.push(nickname)
})
let actionName = {
"join-group":"加入群聊",
"group-exit":"退出群聊",
"group-expel":"被踢出群聊"
}[msg.action];
return nicknameList.join(' , ') + actionName
}else if(msg.action === "group-dissolved"){
return '此群聊已被解散'
}else if(msg.action === "set-group-admin"){
const {user_id,addRole,delRole} = msg.body
const nickname = uniIm.users.getNickname(user_id)
// console.log(nickname)
return `已将"${nickname}"${addRole.includes("admin") ? "添加为群管理员" : "从群管理员中移除"}`
}else{
return msg.body
}
}
},
async mounted() {}
......
<template>
<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>
</template>
<script>
<msgRichText v-if="htmlNodes.length" :msg="{...msg,...{'body':htmlNodes}}" />
<text v-else class="msg-text" :decode="true" space="ensp">{{ trText(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() {
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
},
}
},
computed: {
self() {
return this.msg.from_uid === uniIm.currentUser._id
},
},
methods: {
trText(str) {
// TODO:临时方案,解决 \n 被转义的问题
return str.replace(/\\n/g, "\\\\n")
},
},
watch: {
"msg.body": {
handler() {
let htmlString = this.msg.body.replace(/</g, "&lt;").replace(/>/g, "&gt;")
// 将字符串的url转换为链接
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>")
/* 如需要自己补:
// 手机号正则
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) {
......@@ -74,14 +80,14 @@
},
immediate: true
}
},
mounted() {},
}
</script>
<style lang="scss">
.msg-text-box {
border-radius: 10px;
},
mounted() {},
}
</script>
<style lang="scss">
.msg-text-box {
border-radius: 10px;
background-color: #FFFFFF;
min-width: 30px;
flex-shrink: 1;
......@@ -100,5 +106,5 @@
/* #endif */
}
}
</style>
\ No newline at end of file
<template>
<view class="msg-userinfo-card" @click="onClick">
<cloud-image
<uni-im-img
class="avatar"
width="40px"
height="40px"
border-radius="5px"
:src="avatarUrl||'/uni_modules/uni-im/static/avatarUrl.png'"
:src="avatarUrl"
mode="widthFix"
/>
<text
......@@ -35,14 +35,14 @@ export default {
data() {
return {
avatarUrl:{},
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
this.nickname = user.nickname
},
methods: {
onClick() {
......
......@@ -8,6 +8,7 @@
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import config from '@/uni_modules/uni-im/common/config.js';
export default {
props: {
msg: {
......@@ -58,8 +59,20 @@
// #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')
const {provider} = config.cloudFile
switch (provider){
case 'aliyun':
this.videoPoster = this.videoUrl + '?x-oss-process=video/snapshot,t_1000,f_jpg,w_200,m_fast,ar_auto'
break;
case 'tencent':
this.videoPoster = this.videoUrl + '?imageView2/0/w/200'
break;
case 'qiniu':
this.videoPoster = this.videoUrl + '?vframe/jpg/offset/1/w/200'
break;
default:
break;
}
}
},
deep: true,
......@@ -85,39 +98,37 @@
}
</script>
<style>
<style lang="scss">
.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);
position: relative;
.video-img {
width: 200rpx;
height: 200rpx;
}
.video-box-mark {
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.1);
}
.play-video-icon {
position: absolute;
top: calc(50% - 17.5px);
left: calc(50% - 17.5px);
width: 35px;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
border: 2px solid #FFF;
background-color: rgba(0, 0, 0, 0.2);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
}
}
.video-box-mark {
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.1);
}
</style>
\ No newline at end of file
......@@ -2,10 +2,8 @@
<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">
<text class="format-time-text" :class="{'dup-format-time-text':equalPrevTime}">
{{ friendlyTime }}
</text>
<view v-if="!noTime" class="friendlyTime" :class="{'hide':equalPrevTime}">
<uni-im-friendly-time :time="msg.create_time || msg.client_create_time"></uni-im-friendly-time>
</view>
<view class="msg-box">
<view v-if="msg.is_revoke" class="revoke-text-box">
......@@ -22,30 +20,34 @@
</text>
</view>
<template v-else>
<cloud-image ref="avatar" width="40px" height="40px" border-radius="5px"
:src="avatarUrl||'/uni_modules/uni-im/static/avatarUrl.png'" mode="widthFix"
<uni-im-img class="avatar" ref="avatar" width="40px" height="40px" border-radius="5px"
:src="avatarUrl" mode="widthFix"
:class="{'pointer':canPrivateChat}"
@click.stop="toChat" @longpress.stop="longpressMsgAvatar" />
<view class="msg-main">
<view v-if="!self" class="nickname-box">
<text :selectable="true" class="nickname" @click="longpressMsgAvatar">{{ msg.nickname || users.nickname }}</text>
<text class="isFromAdmin" v-if="isFromAdmin">管理员</text>
<text :selectable="true" class="nickname" :class="{noPreview:!preview}" @click="longpressMsgAvatar">{{nickname}}</text>
<text :selectable="true" class="real_name" v-if="real_name" @click="showRealName">{{real_name}}</text>
<text class="badge" v-if="isFromAdmin">管理员</text>
<template v-for="(item,index) in extMsgBadge" :key="index">
<component :is="item.component" v-bind="item.props"></component>
</template>
</view>
<view v-if="msg.about_msg_id" class="cite-box">
<view v-if="msg.about_msg_id && !preview" class="cite-box">
<template v-if="aboutMsg.body">
<text v-if="aboutMsg.is_revoke" class="cite-box-text">
回复的消息已被撤回
</text>
<text v-else class="cite-box-text" :class="{'pointer':!noJump}" @click="showAboutMsg">
{{ getNicknameByUid(aboutMsg.from_uid) }}{{ aboutMsg.body }}
{{ getNickname(aboutMsg.from_uid) }}{{aboutMsgNote}}
</text>
</template>
<text v-else class="cite-box-text">
[加载中]
</text>
</view>
<view class="msg-content-box" @longpress="showControl">
<view class="msg-content-box" @longpress.prevent="showControl">
<uni-icons v-if="self && msg.state != 100 && msgStateIcon" :color="msg.state===0?'#999':'#d22'"
:type="msgStateIcon" class="msgStateIcon" @click="retriesSendMsg" />
<component :is="'msg-'+msg.type" :class="'msg-'+msg.type" class="msg-content" ref="msg-content" :msg="msg"
......@@ -74,9 +76,6 @@
</template>
<script>
import {
store as uniIdStore,
} from '@/uni_modules/uni-id-pages/common/store'
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
// 导入各类型的消息组件
import msgSystem from './types/system.vue'
......@@ -89,8 +88,11 @@
import msgText from './types/text.vue'
import msgSound from './types/sound.vue'
import msgImage from './types/image.vue'
import msgOrder from './types/order.vue'
import msgPayNotify from './types/pay-notify.vue'
import msgEncryption from './types/encryption.vue'
import { markRaw } from 'vue'
import { markRaw,computed } from 'vue'
/**
* uni-im-msg 组件,渲染一条消息。
......@@ -120,7 +122,10 @@
msgCode,
msgText,
msgSound,
msgImage
msgImage,
msgOrder,
msgPayNotify,
msgEncryption
},
props: {
msg: {
......@@ -137,14 +142,6 @@
return false
}
},
avatar_file: {
type: [Object, String, Boolean],
default () {
return {
url: "/uni_modules/uni-im/static/avatarUrl.png"
}
}
},
index: {
type: Number
},
......@@ -162,15 +159,15 @@
type: Boolean,
default: false
},
preview: { // 是否预览模式
type: Boolean,
default: false,
}
},
emits: ['viewMsg', 'showControl', 'showMsgById','loadMore','longpressMsgAvatar','putChatInputContent'],
emits: ['viewMsg', 'showControl', 'showMsgById','longpressMsgAvatar','putChatInputContent','intoTopic','retriesSendMsg'],
data() {
let currentUser = {
user_id: uniCloud.getCurrentUserInfo().uid,
isInternalUser: this.uniIDHasRole('staff'),
}
// 调用扩展点,正在渲染一条消息,扩展程序可以返回扩展组件,为该消息增加一些显示元素。
let extraComponents = uniIm.extensions.invokeExts('msg-extra', this.msg, currentUser)
const extraComponents = this.preview ? {} : uniIm.extensions.invokeExts('msg-extra', this.msg)
.filter(result => result && result.component)
.map(result => {
return {
......@@ -179,9 +176,7 @@
handlers: result.handlers || {},
}
})
return {
nickname: "用户名",
videoUrl: '',
soundPlayState: 0,
aboutMsg: {},
......@@ -189,61 +184,66 @@
};
},
computed: {
extMsgBadge(){
return uniIm.extensions.invokeExts('msg-badge', this)
.filter(result => result && result.component)
.map(result => {
return {
component: markRaw(result.component),
props: result.props || {},
handlers: result.handlers || {},
}
})
},
currentConversation() {
return uniIm.conversation.getCached(this.msg.conversation_id)
if(this.preview) return false
return uniIm.conversation.find(this.msg.conversation_id)
},
friendlyTime() {
let time = this.msg.create_time || this.msg.client_create_time
// 使得时间会随着心跳动态更新
time = time + uniIm.heartbeat * 0
return uniIm.utils.toFriendlyTime(time)
currentUid() {
return uniIm.currentUser._id
},
users() {
return uniIm.users[this.msg.from_uid] || {}
const users = uniIm.users[this.msg.from_uid]
return users?.__loading ? null : users
},
msgStateIcon() {
switch (this.msg.state) {
case 0:
// 发送中
return 'spinner-cycle'
break;
case -100:
// 发送失败
return 'refresh-filled'
break;
case -200:
// 禁止发送(内容不合法)
return 'info-filled'
break;
default:
return false
break;
return {
"0": 'spinner-cycle',
"-100": 'refresh-filled',
"-200": 'info-filled',
}[this.msg.state]
},
aboutMsgNote() {
if(this.aboutMsg.from_uid){
return uniIm.utils.getMsgNote(this.aboutMsg)
}else{
return '加载中...'
}
},
isFromAdmin() {
const conversation = uniIm.conversation.getCached(this.msg.conversation_id)
return conversation?.group_id && conversation.group_member[this.msg.from_uid]?.role?.includes('admin')
if(this.preview) return false
const conversation = uniIm.conversation.find(this.msg.conversation_id)
return conversation?.group_id && conversation.group.member.find(this.msg.from_uid)?.role?.includes('admin')
},
nickname() {
return this.users?.nickname || this.msg.nickname || uniIm.users.getNickname(this.msg.from_uid)
},
mineId() {
return uniCloud.getCurrentUserInfo().uid
real_name() {
return uniIm.users[this.msg.from_uid]?.realname_auth?.real_name
},
avatarUrl() {
if (this.self) {
// console.error('uniIdStore.userInfo',uniIdStore.userInfo)
return uniIdStore.userInfo.avatar_file?.url
} else {
return this.users.avatar_file?.url
}
return (this.self ? uniIm.currentUser.avatar_file?.url : this.users?.avatar_file?.url || this.msg?.avatar_file?.url) || '/uni_modules/uni-im/static/avatarUrl.png'
},
soundBoxWidth() {
return uni.upx2px(750 / 60 * this.msg.body.time) + 50 + 'px'
},
canPrivateChat(){
const conversation = uniIm.conversation.getCached(this.msg.conversation_id)
const currentUserId = uniCloud.getCurrentUserInfo().uid;
if(this.preview) return false
const conversation = uniIm.conversation.find(this.msg.conversation_id)
const currentMember = conversation.group?.member?.find(this.currentUid)
// 当前登录的账号是管理员,或者是群管理员,或者当前消息是群管理员发的
return this.uniIDHasRole('staff') ||
conversation.group_member?.[currentUserId]?.role?.includes('admin') ||
currentMember?.role?.includes('admin') ||
this.isFromAdmin
}
},
......@@ -273,13 +273,8 @@
// #endif
},
methods: {
getNicknameByUid(uid) {
let users = uniIm.users[uid]
if (users) {
return users.nickname
} else {
return ''
}
getNickname(uid) {
return uniIm.users.getNickname(uid)
},
showAboutMsg() {
this.$emit('showMsgById', this.aboutMsg._id)
......@@ -341,13 +336,13 @@
}
},
async initAboutMsg() {
if (this.preview) return
// 处理引用消息
const {
about_msg_id
} = this.msg
if (about_msg_id) {
const _aboutMsg = this.currentConversation.msgList.find(i => i._id == about_msg_id) || false
let aboutMsg = JSON.parse(JSON.stringify(_aboutMsg))
let aboutMsg = this.currentConversation.msg.find(about_msg_id) || false
// 本地不存在联网查找
if (!aboutMsg) {
const db = uniCloud.database();
......@@ -371,13 +366,46 @@
return
}
}
aboutMsg.body = uniIm.utils.getMsgNote(aboutMsg)
this.aboutMsg = aboutMsg
}
},
putChatInputContent() {
this.$emit('putChatInputContent', JSON.parse(JSON.stringify(this.msg.before_revoke_body)))
}
async putChatInputContent() {
const msgBody = JSON.parse(JSON.stringify(this.msg.before_revoke_body))
if(msgBody.url){
msgBody.url = await uniIm.utils.getTempFileURL(msgBody.url)
}else if(Array.isArray(msgBody)){
for (let i = 0; i < msgBody.length; i++) {
if (msgBody[i].name === 'img') {
msgBody[i].attrs.src = await uniIm.utils.getTempFileURL(msgBody[i].attrs.src)
}
}
}
this.$emit('putChatInputContent', msgBody)
},
showRealName() {
if(!uniIm.isWidescreen){
uni.showModal({
title: '企业名称',
content: this.real_name,
showCancel: true,
confirmText: '复制',
cancelText: '关闭',
success: (res) => {
if (res.confirm) {
uni.setClipboardData({
data: this.real_name,
success: () => {
uni.showToast({
title: '复制成功',
icon: 'none'
})
}
})
}
}
});
}
},
}
}
</script>
......@@ -394,10 +422,6 @@
&,& > * {
cursor: default;
}
.file-msg-box,
.cloud-image {
cursor: pointer;
}
/* #endif */
.msg-main {
margin: 0 8px;
......@@ -448,35 +472,65 @@
.nickname-box {
flex-direction: row;
align-items: center;
}
.nickname {
font-size: 13px;
color: #666666;
padding-left: 2px;
width: 100%;
.nickname {
font-size: 14px;
color: #666666;
padding-left: 2px;
}
.real_name {
font-size: 12px;
color: #999;
padding:0 4px;
background-color: #FFF;
border-radius: 2px;
box-shadow: 0 0 2px #ddd;
margin-left: 3px;
position: relative;
bottom: 2px;
// 溢出隐藏,且显示省略号
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&::before {
content: "\eab8";
color: #fd7e08;
font-size: 10px;
margin-right: 2px;
font-family: uni-im-icons;
}
}
.badge {
position: relative;
bottom: 2px;
font-size: 12px;
color: #FFFFFF;
background-color: #1ab94d;
padding: 1px 3px;
border-radius: 2px;
// 缩小字体
transform: scale(0.8);
&.grey {
background-color: #bbb;
}
}
}
/* #ifdef H5 */
.nickname-box .nickname:hover {
color: #0b65ff;
cursor: pointer;
}
.nickname-box .nickname:hover::after {
content: '@';
margin-left: 3px;
.nickname-box {
.nickname.noPreview{
&:hover {
color: #0b65ff;
cursor: pointer;
&::after {
content: '@';
margin-left: 3px;
}
}
}
}
/* #endif */
.isFromAdmin {
font-size: 14px;
color: #FFFFFF;
background-color: #1ab94d;
padding: 2px 3px;
border-radius: 2px;
transform: scale(0.7);
margin-left: -4px;
}
.rich-text {
background-color: transparent;
width: 500rpx;
......@@ -518,20 +572,14 @@
height: 22px;
}
.format-time-text {
font-size: 12px;
text-align: center;
color: #999999;
line-height: 22px;
}
/* #ifdef H5 */
.dup-format-time-text {
.hide .format-time-text {
display: none;
}
&:hover .dup-format-time-text {
display: unset;
&:hover .hide .format-time-text {
display: flex;
}
.pointer {
cursor: pointer;
}
......
......@@ -125,8 +125,8 @@ export default {
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};
let data = Object.assign({},msg);
delete data._id
if (!merge) {
// 如果不是合并转发,则删除会话id和群id
data.conversation_id = '';
......@@ -170,7 +170,7 @@ export default {
"to_uid": friend_uid,
conversation_id,
group_id,
"from_uid": uniCloud.getCurrentUserInfo().uid,
"from_uid": uniIm.currentUser._id,
"state": 0,
"client_create_time": Date.now(),
"is_read": false,
......@@ -207,9 +207,9 @@ export default {
this.close();
async function sendMsgToConversation(conversation, msg) {
// 插到消息列表
conversation.msgList.push(msg);
msg = conversation.msg.add(msg);
// 保存到本地数据库
await conversation.msgManager.localMsg.add(msg);
// await conversation.msgManager.localMsg.add(msg);
const uniImCo = uniCloud.importObject("uni-im-co");
await uniImCo.sendMsg(msg)
.then(async e => {
......@@ -217,7 +217,6 @@ export default {
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({
......@@ -229,19 +228,8 @@ export default {
// 必须要有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)
}
}
}
}
......@@ -288,11 +276,6 @@ export default {
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;
}
......
......@@ -61,7 +61,7 @@
});
uniCloud.uploadFile({
filePath:res.tempFilePath,
cloudPath:'uni-im/' + uniCloud.getCurrentUserInfo().uid + '/sound/' + Date.now() + '.mp3',
cloudPath:'uni-im/' + uniIm.currentUser._id + '/sound/' + Date.now() + '.mp3',
// fileType:"audio",
success: (e) => {
// console.log('uniCloud.uploadFile-sendSoundMsg',e,'sendSoundMsg',{"url":e.fileID,time:this.time});
......
......@@ -12,17 +12,20 @@
<view class="close" @click="close">
<uni-icons
type="clear"
size="25px"
size="20px"
color="#ccc"
/>
</view>
</view>
<scroll-view :scroll-y="true" class="content">
<uni-im-msg
v-for="(msg,index) in msgList"
:key="index"
:msg="msg"
/>
<scroll-view :scroll-y="true" class="scroll-view">
<view class="scroll-content">
<uni-im-msg
v-for="(msg,index) in msgList"
:key="index"
:msg="msg"
:preview="true"
/>
</view>
</scroll-view>
</view>
</view>
......@@ -41,11 +44,9 @@ export default {
open(msgList) {
this.showMsgList = true;
this.msgList = msgList;
// document.getElementById('dialog').showModal();
},
close() {
this.showMsgList = false;
// document.getElementById('dialog').close();
this.msgList = [];
}
}
......@@ -54,68 +55,50 @@ export default {
<style lang="scss">
.uni-im-view-msg {
position: absolute;
position: fixed !important;
top: 0;
left: 0;
z-index: 20;
width: 100vw;
height: 100vh;
max-width: 100vw !important;
max-height: 100vh !important;
z-index: 10;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
.msg-list {
background-color: #fff;
top: 25%;
}
/* #ifdef H5 */
@media screen and (min-device-width:960px) {
.msg-list {
position: fixed;
width: 600px;
left: calc(50% - 300px);
}
}
/* #endif */
.msg-list .header {
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
background-color: rgba(250, 250, 250, 1);
padding: 10px;
}
.msg-list .header .close {
cursor: pointer;
}
.msg-list .title {
font-size: 20px;
color: #333;
}
.msg-list .content {
padding: 10px;
height: 400px;
position: absolute;
bottom: 0;
width: 100%;
overflow: hidden;
border-radius: 10px;
background-color: rgba(245, 245, 245, 1);
}
/* #ifdef H5 */
@media screen and (min-device-width:960px) {
.msg-list .content ::v-deep * {
max-height: none !important;
height: unset !important;
padding-bottom: 10px;
.header {
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #eee;
padding: 10px;
.close {
cursor: pointer;
}
}
#dialog {
border-radius: 10px;
border: none;
padding: 0;
.title {
font-size: 16px;
color: #333;
}
// 关闭 #dialog选中时的蓝色边框
#dialog:focus {
display: none;
.scroll-view {
height: 400px;
.scroll-content {
margin: 10px;
}
}
#dialog::backdrop {
background: rgba(0, 0, 0, .5);
}
/* #ifdef H5 */
@media screen and (min-device-width:960px) {
.msg-list {
position: relative;
width: 60%;
top: 25%;
left: 20%;
}
}
/* #endif */
......
{
"id": "uni-im",
"displayName": "uni-im",
"version": "3.1.6",
"version": "3.0.4",
"description": "uni-im是云端一体的、全平台的、免费的、开源即时通讯系统",
"keywords": [
"im,即时通讯,客服,聊天"
......
<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>
<template>
<view class="chat-filtered">
<!-- #ifdef H5 -->
<!-- web-pc端会话标题 设置导航栏标题,如与谁对话,群人数为几人等 -->
<text v-if="isWidescreen" id="web-pc-chat-title" :selectable="true">{{navTitle}}</text>
<!-- #endif -->
<view class="head">
<view v-if="count == 0 && loading" class="hint">正在加载……</view>
<view v-else class="hint">{{ count }} 条与“{{ keyword }}”相关的聊天记录</view>
<view class="hint">
<template v-if="count == 0 && loading">正在加载……</template>
<template v-else>{{ count }} 条与“{{ keyword }}”相关的聊天记录</template>
</view>
<view
v-if="conversation_id"
class="enter-chat"
......@@ -18,35 +19,38 @@
进入会话
</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>
<view>
<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>
</view>
</scroll-view>
<chat-fragment
v-if="fragment"
:entry="fragment"
@close="onCloseFragment"
/>
</view>
</view>
</template>
<script>
......@@ -61,7 +65,7 @@
customUI: true
})
import uniIm from '@/uni_modules/uni-im/sdk/index.js'
import ChatFragment from './cmp/chat-fragment'
import ChatFragment from './components/chat-fragment'
export default {
components: {
......@@ -89,7 +93,7 @@
navTitle() {
let title = this.conversation.title
if (this.conversation.group_id) {
title += `(${Object.keys(this.conversation.group_member).length})`;
title += `(${this.conversation.group.member.count()})`;
}
return title
}
......@@ -166,16 +170,10 @@
<style lang="scss">
@import "@/uni_modules/uni-im/common/baseStyle.scss";
.chat-filtered {
height: 0;
flex: 1;
height: 100%;
flex-grow: 1 !important;
background-color: #efefef;
/* #ifdef H5 */
.pc {
// .pc内的元素只有pc端打开才显示,样式在index页面
display: none;
}
/* #endif */
.head {
flex-direction: row;
justify-content: space-between;
......@@ -184,28 +182,24 @@
padding: 0px 15px;
font-size: 12px;
border-bottom: 1px solid #ddd;
.hint {
color: #999;
}
.enter-chat {
flex-direction: row;
padding: 0 5px;
/* #ifdef H5 */
cursor: pointer;
&:hover {
background-color: #ddd;
}
/* #endif */
}
}
.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);
height: 1px;
flex: 1;
}
.uni-im-msg ::v-deep .msg-content {
......
此差异已折叠。
export default `😀,😁,😂,🤣,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗,😙,😚,☺️,🙂,🤗,🤩,🤔,🤨,😐,😑,😶,🙄,😏,😣,😥,😮,🤐,😯,😪,😫,😴,😌,😛,😜,😝,🤤,😒,😓,😔,😕,🙃,🤑,😲,☹️,🙁,😖,😞,😟,😤,😢,😭,😦,😧,😨,😩,🤯,😬,😰,😱,😳,🤪,😵,😡,😠,🤬,😷,🤒,🤕,🤢,🤮,🤧,😇,🤠,🤡,🤥,🤫,🤭,🧐,🤓,😈,👿,👹,👺,💀,☠️,👻,👽,🤖,😺,😸,😹,😻,😼,😽,🙀,😿,😾,🙈,🙉,🙊,👶,🧒,👦,👧,🧑,👨,👩,🧓,👴,👵,👨‍⚕️,👩‍⚕️,👨‍🎓,👩‍🎓,👨‍🏫,👩‍🏫,👨‍⚖️,👩‍⚖️,👨‍🌾,👩‍🌾,👨‍🍳,👩‍🍳,👨‍🔧,👩‍🔧,👨‍🏭,👩‍🏭,👨‍💼,👩‍💼,👨‍🔬,👩‍🔬,👨‍💻,👩‍💻,👨‍🎤,👩‍🎤,👨‍🎨,👩‍🎨,👨‍✈️,👩‍✈️,👨‍🚀,👩‍🚀,👨‍🚒,👩‍🚒,👮,👮‍♂️,👮‍♀️,🕵️,🕵️‍♂️,🕵️‍♀️,💂,💂‍♂️,💂‍♀️,👷,👷‍♂️,👷‍♀️,🤴,👸,👳,👳‍♂️,👳‍♀️,👲,🧕,🧔,👱,👱‍♂️,👱‍♀️,🤵,👰,🤰,🤱,👼,🎅,🤶,🧙,🧙‍♀️,🧙‍♂️,🧚,🧚‍♀️,🧚‍♂️,🧛,🧛‍♀️,🧛‍♂️,🧜,🧜‍♀️,🧜‍♂️,🧝,🧝‍♀️,🧝‍♂️,🧞,🧞‍♀️,🧟,🧟‍♀️,🙍,🙍‍♂️,🙍‍♀️,🙎,🙎‍♂️,🙎‍♀️,🙅,🙅‍♂️,🙅‍♀️,🙆,🙆‍♂️,🙆‍♀️,💁,💁‍♂️,💁‍♀️,🙋,🙋‍♂️,🙋‍♀️,🙇,🙇‍♂️,🙇‍♀️,🤦,🤦‍♂️,🤦‍♀️,🤷,🤷‍♂️,🤷‍♀️,💆,💆‍♂️,💆‍♀️,💇,💇‍♂️,💇‍♀️,🚶,🚶‍♂️,🚶‍♀️,🏃,🏃‍♂️,🏃‍♀️,💃,🕺,👯,👯‍♂️,👯‍♀️,🧖,🧖‍♀️,🧖‍♂️,🧗,🧗‍♀️,🧗‍♂️,🧘,🧘‍♀️,🧘‍♂️,🕴️,👤,👥,👫,👬,👭,💏,👨‍❤️‍💋‍👨,👩‍❤️‍💋‍👩,💑,👨‍❤️‍👨,👩‍❤️‍👩,👪,👨‍👩‍👧,👨‍👩‍👧‍👦,👨‍👩‍👦‍👦,👨‍👩‍👧‍👧,👨‍👨‍👦,👨‍👨‍👧,👨‍👨‍👧‍👦,👨‍👨‍👦‍👦,👨‍👨‍👧‍👧,👩‍👩‍👦,👩‍👩‍👧,👩‍👩‍👧‍👦,👩‍👩‍👦‍👦,👩‍👩‍👧‍👧,👨‍👦,👨‍👧,👨‍👧‍👦,👨‍👧‍👧,👩‍👦‍👦,👩‍👧,👩‍👧‍👦,🤳,👃,👅,👄,💋,💘,❤️,💓,💔,💕,💖,💗,💙,💚,💛,🧡,💜,🖤,💝,💞,❣️,💌,💬,🌬️,☃️,⛄,🎎,🗿,👾,💩,🛀,🛌,💅,👂,👣,👀,👁️,🧠,💭,👓,👔,👕,👖,🧣,🧤,🧥,🧦,👗,👘,👙,👚,👛,👜,👝,🎒,👞,👟,👠,👡,👢,👑,👒,🎩,🎓,🧢,📿,💄,💍,💎,🥄,🔪,🏺,🗺️,🗾,🎠,🎡,🎢,💈,🎪,🛰️,🚀,🛸,🛎️,⌛,⏳,⌚,⏰,🕰️,🌡️,🌂,☂️,☔,⛱️,⚡,🎃,🎄,🎆,🎇,🎈,🎉,🎊,🎏,🎐,🎑,🎀,🎁,🎗️,🎟️,🎫,🔮,🎮,🕹️,🎰,🃏,🎴,🎭,🖼️,🎨,🔇,🔈,🔉,🔊,📢,📣,📯,🔔,🔕,🎼,🎵,🎶,🎙️,🎚️,🎛️,🎤,🎧,📻,🎷,🎸,🎹,🎺,🎻,🥁,📱,📲,☎️,📞,📟,📠,🔋,🔌,💻,🖥️,🖨️,⌨️,🖱️,🖲️,💽,💾,💿,📀,🎥,🎞️,📽️,🎬,📺,📷,📸,📹,📼,🔍,🔎,💡,🔦,🏮,📔,📕,📖,📗,📘,📙,📚,📓,📒,📃,📜,📄,📰,📑,🔖,💰,💴,💵,💶,💷,💸,💳,✉️,📧,📨,📩,📤,📥,📦,📫,📪,📬,📭,📮,✏️,✒️,📝,💼,📁,📂,📅,📆,📇,📈,📉,📊,📋,📌,📍,📎,📏,📐,✂️,🔒,🔓,🔏,🔐,🔑,🔨,🔫,🔧,🔩,🔬,🔭,📡,💉,💊,🚪,🚽,🚿,🛁,🛒,🚬,🔅,🔆,⚜️,🔱,📛,🚂,🚃,🚄,🚅,🚆,🚇,🚈,🚉,🚊,🚝,🚞,🚋,🚌,🚍,🚎,🚐,🚑,🚒,🚓,🚔,🚕,🚖,🚗,🚘,🚙,🚚,🚛,🚜,🚲,🛴,🛵,🚏,🛣️,🛤️,🛢️,⛽,🚨,🚥,🚦,🛑,🚧,⛵,🛶,🚤,🛳️,⛴️,🛥️,🚢,✈️,🛩️,🛫,🛬,💺,🚁,🚟,🚠,🚡,⚠️,⛔,🦗,🍇,🍈,🍉,🍊,🍋,🍌,🍍,🍎,🍏,🍐,🍑,🍒,🍓,🥝,🍅,🥥,🥑,🍆,🥔,🥕,🌽,🌶️,🥒,🥦,🥜,🍞,🥐,🥖,🥨,🥞,🧀,🍖,🍗,🥩,🥓,🍔,🍟,🍕,🌭,🥪,🌮,🌯,🥙,🥚,🍳,🥘,🍲,🥣,🥗,🍿,🥫,🍱,🍘,🍙,🍚,🍛,🍜,🍝,🍠,🍢,🍣,🍤,🍥,🍡,🥟,🥠,🥡,🍦,🍧,🍨,🍩,🍪,🎂,🍰,🥧,🍫,🍬,🍭,🍮,🍯,🍼,🥛,☕,🍵,🍶,🍾,🍷,🍸,🍹,🍺,🍻,🥂,🥃,🥤,🥢,🍽️,🍴`.split(',')
......@@ -2,7 +2,7 @@
<view class="chat-info">
<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'"
:avatarUrl="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 : ''"
/>
......@@ -15,7 +15,7 @@
<!-- #ifdef H5 -->
<!-- 发送名片消息(仅内部人员可用) -->
<uni-list-item
v-if="uniIDHasRole('staff')"
v-if="isWidescreen && uniIDHasRole('staff')"
:link="true"
title="发送他(她)的名片"
@click="sendNameCard"
......@@ -85,11 +85,10 @@ export default {
computed: {
...uniIm.mapState(['isWidescreen']),
isFriend() {
let friendList = uniIm.friend.get()
return friendList.find(i => i._id == this.friend_uid)
return this.friend_uid ? uniIm.friend.find(this.friend_uid) : false
},
currentUid() {
return uniCloud.getCurrentUserInfo().uid
return uniIm.currentUser._id
}
},
async onLoad(options) {
......@@ -146,10 +145,12 @@ export default {
try {
await db.collection('uni-im-friend').where({
friend_uid: this.friend_uid,
user_id: uniCloud.getCurrentUserInfo().uid
user_id: this.currentUid
}).remove()
// 收到push消息后store会自动,将此用户从列表中移除
uni.navigateBack({ delta: 2 })
if (!uniIm.isWidescreen) {
// 收到push消息后store会自动,将此用户从列表中移除
uni.navigateBack({ delta: 2 })
}
} catch (e) {
uni.showModal({
content: JSON.stringify(e.message),
......@@ -178,7 +179,7 @@ export default {
user_id: this.conversation.friend_uid,
name: this.conversation.title,
},
from_uid: uniCloud.getCurrentUserInfo().uid,
from_uid: this.currentUid,
create_time: Date.now(),
}
this.$refs['share-msg'].open([msg], false)
......
......@@ -34,7 +34,7 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
// 监听esc按键,关闭视频
// #ifdef H5
uniIm.utils.appEvent.onKeyDown(evt => this.onDownEscapeKey, {
uniIm.utils.appEvent.onKeyDown(evt => this.onDownEscapeKey(), {
order: 1000,
match: {
key: 'Escape',
......@@ -83,7 +83,8 @@ page {
height: 100%;
}
.video-box {
&.video{
flex: 1;
.video{
width: 100vw;
height: 100%;
}
......
......@@ -23,9 +23,7 @@
onLoad({
msgId,conversationId
}) {
this.msg = uniIm.conversation.dataList
.find(item => item.id === conversationId)
.msgList.find(item => item._id === msgId)
this.msg = uniIm.conversation.find(conversationId).msg.find(msgId)
},
methods: {}
}
......
......@@ -22,7 +22,7 @@
<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'"
:avatar="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>
......@@ -42,7 +42,7 @@
<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'"
:avatar="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>
......@@ -56,7 +56,7 @@
<uni-popup-dialog mode="input" :title="activeIndex?'申请加群':'申请添加好友'"
placeholder="请输入验证信息" confirmText="发送" message="成功消息"
:duration="2000" :before-close="true" :value="value"
@close="close" @confirm="confirm"
@close="close" @confirm="confirm" :maxlength="100"
></uni-popup-dialog>
</uni-popup>
</view>
......@@ -97,7 +97,6 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
},
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)
......@@ -127,7 +126,7 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
},
methods: {
setParam(param){
console.log("param: ",param);
// console.log("param: ",param);
if(param.group_id){
this.current = 1
this.setActiveIndex({currentIndex: 1})
......@@ -138,16 +137,16 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
this.getGroupsList()
},
async getGroupsList(){
const limit = 1000
const skip = this.groupData.length/limit + 1
const limit = 100
const skip = this.groupData.length/limit
const res = await db.collection('uni-im-group')
.where(`"user_id" != "${uniCloud.getCurrentUserInfo().uid}"`)
.where(`"user_id" != "${uniIm.currentUser._id}"`)
.field('_id,name,avatar_file')
.orderBy('create_date', 'desc')
.limit(limit)
.skip(skip)
.limit(limit)
.get()
// console.log("uni-im-group: ",res);
// console.error("uni-im-group: ",res);
if(res.result.data.length){
this.loading = false
this.hasMore = true
......@@ -174,18 +173,22 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
uni.navigateBack()
},
async doSearch(e){
console.log("doSearch: ",e,this.keyword);
// console.log("doSearch: ",e,this.keyword);
uni.showLoading({
title: '搜索中'
})
if(this.activeIndex){
let res = await db.collection('uni-im-group')
.where(`
let where = {};
if(this.keyword){
where = `
/${this.keyword}/.test(name) ||
"_id" == "${this.keyword}"
`)
`
}
let res = await db.collection('uni-im-group')
.where(where)
.get()
console.log(res);
// console.log(res);
this.groupData = res.result.data
}else{
const whereString = [
......@@ -334,7 +337,7 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
@media screen and (min-device-width:960px){
.content {
margin-top: 0;
height: calc(100vh - 175px);
height: calc(100vh - 150px);
overflow: auto;
}
::v-deep .uni-navbar__header-btns-left,
......
......@@ -212,7 +212,7 @@
});
await db.collection('uni-im-friend').where({
friend_uid: item._id,
user_id: uniCloud.getCurrentUserInfo().uid
user_id: uniIm.currentUser._id
}).remove()
uni.hideLoading()
// 收到push消息后会自动,将此用户从列表中移除
......
......@@ -4,12 +4,15 @@
<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-list class="content-box" :border="false">
<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'">
:avatarUrl="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 class="checkbox" :class="{
'disabled': groupMemberUid.includes(item._id),
'checked': checkFriendIds.includes(item._id)}
">
<uni-icons type="checkmarkempty" color="#FFF" v-if="groupMemberUid.includes(item._id) || checkFriendIds.includes(item._id)"></uni-icons>
</view>
</template>
</uni-im-info-card>
......@@ -31,6 +34,7 @@
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const db = uniCloud.database();
export default {
emits: ['done'],
data() {
return {
loading: true,
......@@ -47,9 +51,7 @@
friendList() {
return this.friendData.filter(item => {
//转小写筛选
return !this.groupMemberUid.includes(item._id)
&&
(this.keyword == '' || item.nickname.toLowerCase().includes(this.keyword.toLowerCase()))
return (this.keyword == '' || item.nickname.toLowerCase().includes(this.keyword.toLowerCase()))
})
},
checkFriendNum() {
......@@ -84,9 +86,11 @@
console.log("group_id", options);
if (options.group_id) {
this.group_id = options.group_id
uni.setNavigationBarTitle({
title: '邀请新成员'
})
if(!uniIm.isWidescreen){
uni.setNavigationBarTitle({
title: '邀请新成员'
})
}
//查本群,成员,
let res = await db.collection('uni-im-group-member').where({
group_id: options.group_id
......@@ -94,7 +98,7 @@
.get()
console.log("res:查本群,成员 ", res);
this.groupMemberUid = res.result.data.map(item => item.user_id)
console.log('this.groupMemberUid', this.groupMemberUid);
// console.log('this.groupMemberUid', this.groupMemberUid);
}
this.getFriendsData()
},
......@@ -132,6 +136,9 @@
this.getFriendsData()
},
checkboxChange(user_id) {
if (this.groupMemberUid.includes(user_id)){
return console.log('已经在群里了');
}
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);
......@@ -146,9 +153,13 @@
this.checkFriendIds = []
// console.log('createGroup',res);
if (this.group_id) {
uni.navigateBack({
delta: 1
})
if (uniIm.isWidescreen) {
this.$emit('done')
} else {
uni.navigateBack({
delta: 1
})
}
} else {
// #ifdef H5
if (uniIm.isWidescreen) {
......@@ -209,12 +220,22 @@
height: 20px;
justify-content: center;
align-items: center;
border-radius: 3px;
border-radius: 100px;
&.disabled {
background-color: #1189e9;
opacity: 0.5;
border: none;
cursor: not-allowed;
}
&.checked {
background-color: #1189e9;
border: none;
}
}
.foot-box {
position: fixed;
bottom: 0;
bottom: 15px;
// 注意:此页面可能显示在pc端所以不能用750rpx,而用100%
width: 100%;
height: 60px;
......@@ -228,15 +249,13 @@
/* #ifdef H5 */
@media screen and (min-device-width:960px){
.create-group-box {
width: 100%;
margin: 10px auto;
}
width: 100%;
margin: 10px auto;
&.join-grpop {
width: 800px;
}
.content-box {
height: calc(95vh - 200px);
height: calc(100vh - 175px);
}
.content-box ::v-deep .info-card {
cursor: pointer;
......
......@@ -6,11 +6,11 @@
></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'">
@click="toChat(item._id)" link
:title="item?.name"
:avatar="item?.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>
<uni-im-load-state class="uni-im-load-state" :status="groupHasMore?'loading':'noMore'"></uni-im-load-state>
</view>
</view>
</template>
......@@ -30,10 +30,10 @@
return uniIm.isWidescreen
},
groupList() {
let groupList = uniIm.group.get()
const groupList = uniIm.group.dataList
if(this.keyword){
return groupList.filter(item=>{
return item.group_info.name.includes(this.keyword) || item.group_info._id.includes(this.keyword)
return item.name.includes(this.keyword) || item._id.includes(this.keyword)
})
}else{
return groupList
......@@ -43,6 +43,15 @@
return uniIm.group.hasMore
}
},
mounted() {
uni.createIntersectionObserver(this, { observeAll: true })
.relativeTo('body', {})
.observe('.uni-im-load-state', (res) => {
if (res.intersectionRatio > 0) {
uniIm.group.loadMore()
}
})
},
async onLoad(options) {
this.setParam(options)
},
......@@ -69,7 +78,7 @@
/* #ifdef H5 */
@media screen and (min-device-width:960px){
.uni-list {
height: calc(100vh - 185px);
height: calc(100vh - 165px);
overflow: auto;
}
}
......
......@@ -44,7 +44,6 @@ export default async function({
state:confirm?100:-100
})
.then((res) => {
uni.hideLoading()
callback()
}).catch((err) => {
console.log(err);
......@@ -53,6 +52,9 @@ export default async function({
showCancel: false
})
})
.finally(() => {
uni.hideLoading()
})
break;
default:
console.log({subType})
......
......@@ -4,9 +4,9 @@
<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'"
:clickable="true" :badge="item.is_read?'':'dot'" badgePositon="left"
:title="item.payload.title || item.title" :note="item.payload.content||item.content||'无'"
:avatar="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">
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
......@@ -5,10 +5,10 @@
<view v-if="error" class="error">
<text>{{ error.message }}</text>
</view>
<uni-list v-else>
<uni-list v-else :border="false">
<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'"
:avatar="item.avatar_file?.url || '/uni_modules/uni-im/static/avatarUrl.png'"
@click="toChat(item._id)"></uni-im-info-card>
</uni-list>
<uni-im-load-state :status="loading ? 'loading' : loadMoreStatus"></uni-im-load-state>
......
# 简介
**uni-im 已开放需求征集和投票** [点此前往](https://vote.dcloud.net.cn/#/?name=uni-im)
# 简介
uni-im是云端一体的、全平台的、免费的、开源即时通讯系统。
- 基于uni-app,App、小程序、web全端兼容
- 基于uniCloud,前后端都使用js开发
- 基于[uni-push2](https://uniapp.dcloud.net.cn/unipush-v2.html),专业稳定的全端推送系统
- 基于[uni-id](https://uniapp.dcloud.net.cn/uniCloud/uni-id-summary.html),完善的账户体系
- 基于[uni-id](https://uniapp.dcloud.net.cn/uniCloud/uni-id/summary.html),完善的账户体系
- 支持服务端为非uniCloud(比如:应用服务端的开发语言是php、java、go、.net、python、c#等)或 不基于uni-id-pages 开发的项目接入
案例:
1. 应用名称:DCloud,该 App 的内置聊天模块,即基于 uni-im 开发。下载地址为:[https://im.dcloud.net.cn/uni-portal.html](https://im.dcloud.net.cn/uni-portal.html)
<img width="600px" src="https://qiniu-web-assets.dcloud.net.cn/ext/uni-im/20230228110007.jpg"></img>
如图:在插件市场任意插件详情页面,点击咨询作者按钮,即可看到基于uni-im搭建的客服系统。
2. 如图:在插件市场任意插件详情页面,点击“进入交流群”按钮,即可看到基于uni-im搭建的客服系统。
<img width="600px" src="https://web-ext-storage.dcloud.net.cn/unicloud/uni-im/img/17198039234743b6c4dd0-27b8-11eb-9e1d-136fabf12402.png">
下载地址:[https://ext.dcloud.net.cn/plugin?name=uni-im](https://ext.dcloud.net.cn/plugin?name=uni-im)
## 特点优势
- 性价比高;前后端代码均免费开源,相比品使用uni-im仅需花费极少的托管在uniCloud(serverless服务器)产生的费用[详情查看](#cost)
- 性价比高;前后端代码均免费开源,相比同类产品使用uni-im仅需花费极少的托管在uniCloud(serverless服务器)产生的费用[详情查看](#cost)
- 全端可用
- App端支持nvue,更好的长列表性能。list组件性能优势[详情参考](https://uniapp.dcloud.net.cn/component/list.html)
- 中心化响应式数据管理,切换会话无需重新加载数据,更流畅的体验
- App端聚合多个手机厂商推送通道,app不在线也可以收到消息
优先开发哪些,取决于开发者的反馈。同时也欢迎开发者共建这个开源项目。
> uni-im相关功能建议或问题,可以加入由uni-im(本插件)搭建的交流群,[点此加入](https://im.dcloud.net.cn/#/?joinGroup=63ef49711d358337456f4d67),备用QQ群(当系统处于维护中使用)群号:[854520009](https://qm.qq.com/cgi-bin/qm/qr?k=DJNSajXAYHnYcr9pouOfxF9Rwwl1AJHc&jump_from=webapi&authKey=HZ1fG58Eudp3o0GCoyx1/UPMY9Fv1sGT5jdqYqPJlTGT0XVUip3Bk8E+UyToQOMo)
> uni-im相关功能建议或问题,可以加入由uni-im(本插件)搭建的交流群[点此加入](https://im.dcloud.net.cn/#/?joinGroup=63ef49711d358337456f4d67)
## 使用uniCloud产生的费用说明@cost
## 使用uniCloud产生的费用说明@cost
uni-im本身并不收费,实际使用中需要依赖uniCloud云服务,会产生费用;而uniCloud的价格很实惠:
- 调用10000次云函数仅需0.0133元
- 调用10000次数据库查询仅需0.015元
......@@ -53,4 +57,4 @@ uni-im本身并不收费,实际使用中需要依赖uniCloud云服务,会产
相比市面上同类型产品,使用uni-im仅需花费如此便宜的uniCloud(serverless服务器)费用;在价格这块uni-im性价比极高。
## 开发文档[详情查看](https://uniapp.dcloud.net.cn/uniCloud/uni-im.html)
## 开发文档[详情查看](https://uniapp.dcloud.net.cn/uniCloud/uni-im.html)
\ No newline at end of file
此差异已折叠。
此差异已折叠。
......@@ -6,7 +6,7 @@ export async function init(version) {
showCancel: false
});
}
const maxIndexDBVersion = version || 2023101601
const maxIndexDBVersion = Date.now() // todo 临时方案,使得每次打开indexDB都会清空。等项目稳定后,再恢复
let currentIndexDBVersion = uni.getStorageSync('uni-im-currentIndexDBVersion') || 0
if (currentIndexDBVersion < maxIndexDBVersion) {
// console.log('clear indexedDB database');
......@@ -88,4 +88,4 @@ export async function init(version) {
}
}
})
}
}
\ No newline at end of file
import $state from '../state/index.js';
export default () => {
$state.conversation.dataList = [];
$state.conversation.hasMore = true;
$state.notification.dataList = [];
$state.notification.hasMore = true;
$state.friend.dataList = [];
$state.friend.hasMore = true;
$state.group.dataList = [];
$state.group.hasMore = true;
$state.currentConversationId = false;
$state.conversation.reset()
$state.notification.reset()
$state.friend.reset()
$state.group.reset()
$state.currentConversationId = false
}
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
import $state from '../state/index.js';
let registeredExtensionPoints = $state.ext._extensionPoints
let registeredExtensionPoints = {}// $state.ext._extensionPoints
/**
* 由扩展模块调用,在指定的扩展点上挂接一个扩展程序。
......
......@@ -7,6 +7,7 @@ export default {
async loadMore({
friend_uid
} = {}) {
const limit = 1000
let whereString = '"user_id" == $cloudEnv_uid'
if (friend_uid) {
whereString += `&& "friend_uid" == "${friend_uid}"`
......@@ -15,9 +16,9 @@ export default {
let res = await db.collection(
db.collection('uni-im-friend').where(whereString).field('friend_uid,mark,class_name')
.getTemp(),
db.collection('uni-id-users').field('_id,nickname,avatar_file').getTemp()
db.collection('uni-id-users').field('_id,nickname,avatar_file,realname_auth').getTemp()
)
.limit(500)
.limit(limit)
.get()
let data = res.result.data
// console.log('data',data);
......@@ -28,7 +29,7 @@ export default {
$state.users[uid] = item.friend_uid[0]
}
})
$state.friend.hasMore = data.length == 500
$state.friend.hasMore = data.length == limit
$state.friend.dataList.push(...data)
},
remove(friend_uid) {
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册