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

【重要更新】去掉所有nvue页面,消息输入框改用renderjs实现,消息列表改用翻转消息列表实现

上级 255ae957
export default `😀,😁,😂,🤣,😃,😄,😅,😆,😉,😊,😋,😎,😍,😘,😗,😙,😚,☺️,🙂,🤗,🤩,🤔,🤨,😐,😑,😶,🙄,😏,😣,😥,😮,🤐,😯,😪,😫,😴,😌,😛,😜,😝,🤤,😒,😓,😔,😕,🙃,🤑,😲,☹️,🙁,😖,😞,😟,😤,😢,😭,😦,😧,😨,😩,🤯,😬,😰,😱,😳,🤪,😵,😡,😠,🤬,😷,🤒,🤕,🤢,🤮,🤧,😇,🤠,🤡,🤥,🤫,🤭,🧐,🤓,😈,👿,👹,👺,💀,☠️,👻,👽,🤖,😺,😸,😹,😻,😼,😽,🙀,😿,😾,🙈,🙉,🙊,👶,🧒,👦,👧,🧑,👨,👩,🧓,👴,👵,👨‍⚕️,👩‍⚕️,👨‍🎓,👩‍🎓,👨‍🏫,👩‍🏫,👨‍⚖️,👩‍⚖️,👨‍🌾,👩‍🌾,👨‍🍳,👩‍🍳,👨‍🔧,👩‍🔧,👨‍🏭,👩‍🏭,👨‍💼,👩‍💼,👨‍🔬,👩‍🔬,👨‍💻,👩‍💻,👨‍🎤,👩‍🎤,👨‍🎨,👩‍🎨,👨‍✈️,👩‍✈️,👨‍🚀,👩‍🚀,👨‍🚒,👩‍🚒,👮,👮‍♂️,👮‍♀️,🕵️,🕵️‍♂️,🕵️‍♀️,💂,💂‍♂️,💂‍♀️,👷,👷‍♂️,👷‍♀️,🤴,👸,👳,👳‍♂️,👳‍♀️,👲,🧕,🧔,👱,👱‍♂️,👱‍♀️,🤵,👰,🤰,🤱,👼,🎅,🤶,🧙,🧙‍♀️,🧙‍♂️,🧚,🧚‍♀️,🧚‍♂️,🧛,🧛‍♀️,🧛‍♂️,🧜,🧜‍♀️,🧜‍♂️,🧝,🧝‍♀️,🧝‍♂️,🧞,🧞‍♀️,🧟,🧟‍♀️,🙍,🙍‍♂️,🙍‍♀️,🙎,🙎‍♂️,🙎‍♀️,🙅,🙅‍♂️,🙅‍♀️,🙆,🙆‍♂️,🙆‍♀️,💁,💁‍♂️,💁‍♀️,🙋,🙋‍♂️,🙋‍♀️,🙇,🙇‍♂️,🙇‍♀️,🤦,🤦‍♂️,🤦‍♀️,🤷,🤷‍♂️,🤷‍♀️,💆,💆‍♂️,💆‍♀️,💇,💇‍♂️,💇‍♀️,🚶,🚶‍♂️,🚶‍♀️,🏃,🏃‍♂️,🏃‍♀️,💃,🕺,👯,👯‍♂️,👯‍♀️,🧖,🧖‍♀️,🧖‍♂️,🧗,🧗‍♀️,🧗‍♂️,🧘,🧘‍♀️,🧘‍♂️,🕴️,👤,👥,👫,👬,👭,💏,👨‍❤️‍💋‍👨,👩‍❤️‍💋‍👩,💑,👨‍❤️‍👨,👩‍❤️‍👩,👪,👨‍👩‍👧,👨‍👩‍👧‍👦,👨‍👩‍👦‍👦,👨‍👩‍👧‍👧,👨‍👨‍👦,👨‍👨‍👧,👨‍👨‍👧‍👦,👨‍👨‍👦‍👦,👨‍👨‍👧‍👧,👩‍👩‍👦,👩‍👩‍👧,👩‍👩‍👧‍👦,👩‍👩‍👦‍👦,👩‍👩‍👧‍👧,👨‍👦,👨‍👧,👨‍👧‍👦,👨‍👧‍👧,👩‍👦‍👦,👩‍👧,👩‍👧‍👦,🤳,👃,👅,👄,💋,💘,❤️,💓,💔,💕,💖,💗,💙,💚,💛,🧡,💜,🖤,💝,💞,❣️,💌,💬,🌬️,☃️,⛄,🎎,🗿,👾,💩,🛀,🛌,💅,👂,👣,👀,👁️,🧠,💭,👓,👔,👕,👖,🧣,🧤,🧥,🧦,👗,👘,👙,👚,👛,👜,👝,🎒,👞,👟,👠,👡,👢,👑,👒,🎩,🎓,🧢,📿,💄,💍,💎,🥄,🔪,🏺,🗺️,🗾,🎠,🎡,🎢,💈,🎪,🛰️,🚀,🛸,🛎️,⌛,⏳,⌚,⏰,🕰️,🌡️,🌂,☂️,☔,⛱️,⚡,🎃,🎄,🎆,🎇,🎈,🎉,🎊,🎏,🎐,🎑,🎀,🎁,🎗️,🎟️,🎫,🔮,🎮,🕹️,🎰,🃏,🎴,🎭,🖼️,🎨,🔇,🔈,🔉,🔊,📢,📣,📯,🔔,🔕,🎼,🎵,🎶,🎙️,🎚️,🎛️,🎤,🎧,📻,🎷,🎸,🎹,🎺,🎻,🥁,📱,📲,☎️,📞,📟,📠,🔋,🔌,💻,🖥️,🖨️,⌨️,🖱️,🖲️,💽,💾,💿,📀,🎥,🎞️,📽️,🎬,📺,📷,📸,📹,📼,🔍,🔎,💡,🔦,🏮,📔,📕,📖,📗,📘,📙,📚,📓,📒,📃,📜,📄,📰,📑,🔖,💰,💴,💵,💶,💷,💸,💳,✉️,📧,📨,📩,📤,📥,📦,📫,📪,📬,📭,📮,✏️,✒️,📝,💼,📁,📂,📅,📆,📇,📈,📉,📊,📋,📌,📍,📎,📏,📐,✂️,🔒,🔓,🔏,🔐,🔑,🔨,🔫,🔧,🔩,🔬,🔭,📡,💉,💊,🚪,🚽,🚿,🛁,🛒,🚬,🔅,🔆,⚜️,🔱,📛,🚂,🚃,🚄,🚅,🚆,🚇,🚈,🚉,🚊,🚝,🚞,🚋,🚌,🚍,🚎,🚐,🚑,🚒,🚓,🚔,🚕,🚖,🚗,🚘,🚙,🚚,🚛,🚜,🚲,🛴,🛵,🚏,🛣️,🛤️,🛢️,⛽,🚨,🚥,🚦,🛑,🚧,⛵,🛶,🚤,🛳️,⛴️,🛥️,🚢,✈️,🛩️,🛫,🛬,💺,🚁,🚟,🚠,🚡,⚠️,⛔,🦗,🍇,🍈,🍉,🍊,🍋,🍌,🍍,🍎,🍏,🍐,🍑,🍒,🍓,🥝,🍅,🥥,🥑,🍆,🥔,🥕,🌽,🌶️,🥒,🥦,🥜,🍞,🥐,🥖,🥨,🥞,🧀,🍖,🍗,🥩,🥓,🍔,🍟,🍕,🌭,🥪,🌮,🌯,🥙,🥚,🍳,🥘,🍲,🥣,🥗,🍿,🥫,🍱,🍘,🍙,🍚,🍛,🍜,🍝,🍠,🍢,🍣,🍤,🍥,🍡,🥟,🥠,🥡,🍦,🍧,🍨,🍩,🍪,🎂,🍰,🥧,🍫,🍬,🍭,🍮,🍯,🍼,🥛,☕,🍵,🍶,🍾,🍷,🍸,🍹,🍺,🍻,🥂,🥃,🥤,🥢,🍽️,🍴`.split(',')
......@@ -87,22 +87,12 @@
setTimeout(() => {
// 根据实际渲染出的组件大小修正定位
// #ifdef APP-NVUE
const dom = weex.requireModule("dom");
const result = dom.getComponentRect(this.$refs.contextmenu, option => {
const {top, left, width, height} = option.size
this.style = calcPosition(top, left, width, height)
});
// #endif
// #ifndef APP-NVUE
const query = uni.createSelectorQuery().in(this)
query.select('.contextmenu').boundingClientRect(option => {
const { width, height } = option
this.style = calcPosition(top, left, width, height)
this.style.opacity = 1
}).exec()
// #endif
}, 0)
},
closeMe(){
......@@ -126,7 +116,7 @@
.contextmenu{
position: fixed;
background-color: #fff;
z-index: 99999;
z-index: 99;
border-radius: 10px;
box-shadow: 0 0 10px #888;
background-color:#FFF;
......@@ -143,15 +133,11 @@
margin: 5px;
opacity: 0.9;
text-align: left;
height: 40px;
align-content: center;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
/* #ifdef APP-NVUE */
width: 120px;
height: 40px;
/* #endif */
}
.item:hover{
......@@ -163,21 +149,9 @@
position: fixed;
top: 0;
left: 0;
width: 750rpx;
flex: 1;
height: 9000px;
/* #ifndef APP-NVUE */
width: 100vw;
height: 100vh;
z-index: 9999;
/* #endif */
/* #ifdef H5 */
// 解决:通过*选择器设置了最大宽高 引起的大小撑不开的问题
max-height:100vh;
max-width:100vw;
/* #endif */
z-index:10;
background-color: rgba(0,0,0,0.5);
}
</style>
<template>
<view>
<!-- #ifdef H5 -->
<scroll-view
<view class="root">
<scroll-view
ref="conversation-list"
class="conversation-list"
:class="{canCheck}"
:style="{'background-color': conversationList.length?'#FFF':''}"
:scroll-top="listScrollTop"
scroll-y="true"
@scrolltolower="loadMore()"
@scroll="onScroll"
>
<!-- #endif -->
<!-- #ifndef H5 -->
<uni-list
ref="conversation-list"
class="conversation-list"
:style="{'height':wHeight,'width':'750rpx'}"
:border="false"
<uni-im-conversation v-for="(item,index) in conversationList" :key="item.id"
class="conversation-list-item" :class="{'activeConversation':activeConversationId == item.id,'focus':item.focus}"
:conversation="item" :id="item.id"
@click="clickItem(item)" @contextmenu.prevent="openConversationMenu($event,index)"
@longpress="openConversationMenu($event,index)"
>
<!-- #endif -->
<!-- 注意:nvue下 list 内必须是 cell,所以元素都需要在uni-list-chat内 -->
<uni-im-conversation v-for="(item,index) in conversationList" :key="item.id" :id="item.id"
:conversation="item"
class="conversation-list-item" :class="{'activeConversation':activeConversationId == item.id,'focus':item.focus}"
@click="clickItem(item)"
@contextmenu.prevent.native="openConversationMenu($event,index)"
>
<template v-if="canCheck" #left>
<view style="justify-content: center;">
<view class="check-box" :class="{checked:isCheck(item)}">
<uni-icons
v-if="isCheck(item)"
color="#FFF"
type="checkmarkempty"
/>
</view>
<template v-if="canCheck" #left>
<view style="justify-content: center;">
<view class="check-box" :class="{checked:isCheck(item)}">
<uni-icons
v-if="isCheck(item)"
color="#FFF"
type="checkmarkempty"
/>
</view>
</template>
</uni-im-conversation>
</view>
</template>
</uni-im-conversation>
<!-- #ifdef APP-NVUE -->
<!-- nvue端appear(元素一旦显示在可视窗口中)就触发加载更多。-->
<cell v-if="conversationList.length" @appear="loadMore()" />
<!-- #endif -->
<uni-list-item :custom-style="{backgroundColor:'#f5f5f5',padding:0}" style="margin-top: 5px;" :border="false">
<template #body>
<template v-if="!keyword">
<uni-im-load-state
class="tip"
:content-text="loadMoreContentText"
:status="conversationHasMore?'loading':'noMore'"
/>
</template>
<template v-else>
<text v-if="conversationList.length === 0" style="text-align: center;flex: 1;margin: 8px;color: #aaa;">
没有相关数据
</text>
</template>
</template>
</uni-list-item>
<view style="margin-top: 5px;backgroundColor:'#f5f5f5'">
<template v-if="!keyword">
<uni-im-load-state
:content-text="loadMoreContentText"
:status="conversationHasMore?'loading':'noMore'"
/>
</template>
<text v-else-if="conversationList.length === 0"
style="text-align: center;flex: 1;margin: 8px;color: #aaa;"
>没有相关数据</text>
</view>
<!-- #ifndef H5 -->
</uni-list>
<!-- #endif -->
<!-- #ifdef H5 -->
</scroll-view>
<!-- #endif -->
<!-- #ifdef H5 -->
<uni-im-contextmenu ref="uni-im-contextmenu" />
<!-- #endif -->
</view>
</template>
......@@ -80,6 +52,7 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const db = uniCloud.database();
let currentScrollTop = 0;
export default {
emits: ['change', 'clickItem', 'onScroll'],
props: {
keyword: {
type: String,
......@@ -107,11 +80,6 @@ export default {
conversationHasMore() {
return uniIm.conversation.hasMore
},
// #ifdef APP-NVUE
wHeight() {
return uni.getSystemInfoSync().windowHeight + 'px'
},
// #endif
loadMoreContentText() {
return {
contentrefresh: "正在加载...",
......@@ -190,13 +158,16 @@ export default {
this.$emit('clickItem', item)
},
openConversationMenu(e, index) {
// #ifdef H5
let conversation = this.conversationList[index]
conversation.focus = true
const myContextmenu = this.$refs['uni-im-contextmenu']
const clientY = e.clientY || e.changedTouches[0].clientY
const clientX = e.clientX || e.changedTouches[0].clientX
const position = {
"top": e.clientY + 35,
"left": e.clientX
"top": clientY + 35,
"left": clientX
}
let menuList = [{
"title": "置顶",
......@@ -247,7 +218,6 @@ export default {
myContextmenu.onClose(() => {
conversation.focus = false
})
// #endif
},
async loadMore() {
let data = await uniIm.conversation.loadMore()
......@@ -260,35 +230,32 @@ export default {
<style lang="scss" scoped>
.conversation-list,
.root {
height: 100%;
flex: 1;
}
.tip {
flex: 1;
}
.canCheck ::v-deep {
.note-box,
.time,
.state {
display: none;
}
.title {
font-size: 16px !important;
}
}
.conversation-list .conversation-list-item.focus {
border: 2px solid #1ab94e;
}
.conversation-list .conversation-list-item {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
border: 2px solid transparent;
border-radius: 5px;
margin: 0 5px;
}
/* #ifdef H5 */
.conversation-list .conversation-list-item ::v-deep .uni-list-chat__content-title {
font-size: 14px;
}
.conversation-list ::v-deep .conversation-list-item .uni-list--border {
display: none;
}
.conversation-list ::v-deep .conversation-list-item .uni-list-chat__container {
padding: 8px 8px 8px 6px;
}
/* #endif */
.conversation-list .conversation-list-item.activeConversation {
background-color: #f1f1f1;
......
<template>
<uni-list-chat
:id="conversation.id"
:note="canCheck?'':conversation.note"
:show-badge="!canCheck && conversation.unread_count>0"
:badge-text="!canCheck ? conversation.unread_count : ''"
:title="conversation.title"
:avatar="avatarUrl"
:time="canCheck?'':friendlyTime(conversation.time)"
:is-pinned="conversation.pinned"
:is-mute="canCheck?false:conversation.mute"
:red-note="redNote"
:tags="conversation.tag"
<template>
<uni-im-info-card
:id="conversation.id"
@click="handleClick"
:title="conversation.title"
:note="conversation.note"
:red-note="conversation.redNote"
:tags="conversation.tags"
:avatarUrl="avatarUrl"
:time="friendlyTime"
:badge="conversation.unread_count"
:mute="conversation.mute"
:pinned="conversation.pinned"
link
>
<template v-slot:left>
<slot name="left"></slot>
</template>
<template v-slot:over-avatar>
<template v-for="overlay in avatarOverlayList" :key="overlay.component.name">
<component
:is="overlay.component"
v-bind="overlay.props"
cementing="ConversationAvatarOverlay"
/>
</template>
</template>
</uni-list-chat>
>
<template #left>
<slot name="left"></slot>
</template>
<template #avatar-overlay-list>
<template v-for="overlay in avatarOverlayList" :key="overlay.component.name">
<component :is="overlay.component" v-bind="overlay.props" cementing="ConversationAvatarOverlay" />
</template>
</template>
</uni-im-info-card>
</template>
<script>
......@@ -40,26 +35,24 @@
conversation: {
type: Object,
default: () => {}
},
canCheck: {
type: Boolean,
default: false
}
},
emits: ['click'],
computed: {
redNote() {
if (this.canCheck) {
return ''
} else if (this.conversation.call_list.length > 0) {
return '[有人@我]'
} else if (this.conversation.hasDraft) {
return '[草稿]'
} else if (this.conversation.group_id && this.conversation.group_info.mute_all_members) {
return '[已禁言]'
friendlyTime() {
// 使得时间会随着心跳动态更新
let timestamp = this.conversation.time + uniIm.heartbeat * 0
let friendlyTime = uniIm.utils.toFriendlyTime(timestamp)
// console.log('friendlyTime',friendlyTime);
let friendlyTimeArr = friendlyTime.split(' ')
let friendlyTimeArrL = friendlyTimeArr.length
// 如果含 年/月(不在3天内,且不是同一周),去掉时间
if (friendlyTimeArrL == 3 && friendlyTime.includes('/')) {
friendlyTime = friendlyTimeArr[0]
}
return friendlyTime
}
},
emits: ['click'],
data() {
// 调用扩展点,扩展程序可以为该会话增加覆盖的图标元素。
let avatarOverlayList = uniIm.extensions
......@@ -90,23 +83,8 @@
methods: {
handleClick() {
this.$emit('click', this.conversation)
},
friendlyTime(timestamp) {
// 使得时间会随着心跳动态更新
timestamp = timestamp + uniIm.heartbeat * 0
let friendlyTime = uniIm.utils.toFriendlyTime(timestamp)
// console.log('friendlyTime',friendlyTime);
let friendlyTimeArr = friendlyTime.split(' ')
let friendlyTimeArrL = friendlyTimeArr.length
// 如果含 年/月(不在3天内,且不是同一周),去掉时间
if (friendlyTimeArrL == 3 && friendlyTime.includes('/')) {
friendlyTime = friendlyTimeArr[0]
}
return friendlyTime
}
}
};
</script>
<style lang="scss">
</style>
\ No newline at end of file
<template>
<view class="uni-im-editor-box">
<div class="uni-im-editor" contenteditable="true" ></div>
<!-- 与rmd通讯专用 -->
<view :change:prop="rdm.$callMethod" :prop="callRmdParam"></view>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
emits: ["input", "confirm"],
data() {
return {
callRmdParam: []
}
},
props: {
modelValue: {
type: [String, Object],
default: ""
},
placeholder: {
type: String,
default: ""
},
maxlength: {
type: Number,
default: 140
}
},
mounted() {
// 与rmd通讯专用
this.callRmd = async (funcName, ...params) => {
this.callRmdParam = []
this.$nextTick(() => {
return new Promise((resolve, reject) => {
this.callRmdParam = [funcName, ...params,param=>{
resolve(param)
}]
})
})
}
// #ifndef H5
uni.onKeyboardHeightChange((res) => {
if(res.height === 0){
// console.error('@##键盘被收起,可以失去焦点')
// this.callRmd('$blur')
}
});
// #endif
// #ifdef H5
const uniImEditor = document.querySelector('.uni-im-editor')
uniIm.utils.appEvent.onAppActivate(() => {
// 主窗口激活时设置输入焦点到这里的文本编辑框
uniImEditor.focus()
})
let shiftIsDown = false;
window.addEventListener('keydown', (e) => {
if (e.key == 'Shift') {
shiftIsDown = true
}
})
window.addEventListener('keyup', (e) => {
if (e.key == 'Shift') {
shiftIsDown = false
}
})
let isComposing = false;
// 输入法开始输入
uniImEditor.addEventListener('compositionstart', () => {
isComposing = true
uniImEditor.isComposing = isComposing;
});
// 输入法结束输入
uniImEditor.addEventListener('compositionend', () => {
isComposing = false
uniImEditor.isComposing = isComposing;
});
uniImEditor.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
if (isComposing) {
console.log('输入法正在输入中,此时回车不发送消息')
event.preventDefault();
} else {
if (shiftIsDown) {
console.log('shift键处于按下状态,为换行不是confirm发送')
} else {
this.$emit('confirm');
// 防止,回车执行发送的同时执行换行
event.preventDefault();
}
}
}
});
uniImEditor.addEventListener('paste', async event => {
// console.log('粘贴', event);
if (event.clipboardData || event.originalEvent) {
//某些chrome版本使用的是event.originalEvent
let clipboardData = event.clipboardData || event.originalEvent.clipboardData;
// 获取粘贴的图片
let items = clipboardData.items;
for (const item of items) {
if (item.type.indexOf("image") !== -1) {
event.preventDefault();
const file = item.getAsFile();
const blobUrl = URL.createObjectURL(file)
console.log('blobUrl',blobUrl)
this.$addHtmlToCursor(`<img src="${blobUrl}" />`)
}
}
let htmlString = clipboardData.getData('text/html');
// console.log('htmlString111',htmlString);
if (htmlString) {
event.preventDefault();
htmlString = await filterHTML(htmlString);
// console.log('htmlString222', htmlString);
const tmpDom = document.createElement('div');
tmpDom.innerHTML = htmlString;
if (tmpDom.innerText.length > 50000) {
uni.showModal({
content: '你粘贴的文本长度超过50000,将被截断。',
complete: e => {
if (e.confirm) {
this.$addHtmlToCursor(htmlString.substring(0, 50000))
}
}
});
} else {
this.$addHtmlToCursor(htmlString)
}
// 检查图片加载失败,删除图片
const imgs = uniImEditor.querySelectorAll('img');
for (const img of imgs) {
img.onerror = () => {
img.remove();
}
}
} else {
let text = clipboardData.getData('text');
try {
let obj = JSON.parse(text)
this.$addHtmlToCursor(this.$arrDomJsonToHtml(obj['uni-im-rich-text-data']))
} catch (e) {
this.$addHtmlToCursor(text)
}
event.preventDefault();
}
}
})
async function filterHTML(htmlString) {
// 过滤html字符串,只保留:文字,图片,链接
// html字符串转dom对象,并深度遍历
let div = document.createElement('div');
div.innerHTML = htmlString;
let arr = [];
async function deep(node) {
if (node.nodeType === 1) {
if (node.tagName === 'IMG') {
// 只保留src属性
// 处理图片跨域问题
if (node.src.indexOf('blob:http') != 0 && !node.src.includes(
'https://im-res.dcloud.net.cn')) {
const uniImCo = uniCloud.importObject("uni-im-co", {
loadingOptions: { // loading相关配置
title: '处理跨域图片...',
mask: true
}
})
let res = await uniImCo.getImgBase64(node.src)
function base64ToBlob(base64) {
// 将Base64字符串分割为数据和应用信息
const byteChars = atob(base64.split(',')[1]);
// 创建一个长度为byteChars长度的数组
const byteArrays = new Array(byteChars.length);
// 将二进制字符串转换为Uint8Array
for (let i = 0; i < byteChars.length; i++) {
byteArrays[i] = byteChars.charCodeAt(i);
}
// 返回一个Blob对象
return new Blob([new Uint8Array(byteArrays)], {type: 'image/png'});
}
node.src = URL.createObjectURL( base64ToBlob(res.data) )
}
arr.push(`<img src="${node.src}" />`);
} else if (node.tagName === 'A') {
// 只保留href属性
arr.push(`<a href="${node.href}">${node.innerText}</a>`);
}
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
await deep(node.childNodes[i]);
}
}
} else if (node.nodeType === 3) {
arr.push(node.nodeValue);
}
}
await deep(div);
return arr.join('');
}
// #endif
},
methods: {
oninput(e) {
this.$emit('input', e)
}
}
}
</script>
<script module="rdm" lang="renderjs">
let uniImEditor,lastFocusNode,lastCursor;
// #ifdef APP
function setSoftinputTemporary() {
console.log('setSoftinputTemporary')
// 设置当前窗口键盘弹出后不做变化
const currentWebview = plus.webview.currentWebview();
currentWebview.setSoftinputTemporary({
mode:'nothing',
position:{top: 0,height: 0}
});
}
// #endif
export default {
name: 'uni-im-editor',
data() {
return {}
},
mounted() {
uniImEditor = document.querySelector('.uni-im-editor')
uniImEditor.addEventListener('input', e => {
setTimeout(()=>this.$oninput(e.data),0);
});
uniImEditor.addEventListener('click', e => {
// console.log('click', e);
// #ifdef APP
setSoftinputTemporary()
// #endif
});
uniImEditor.addEventListener('blur', e => {
// console.error('###blur', e);
});
// 键盘敲左右
uniImEditor.addEventListener('keydown', e => {
setTimeout(this.$refreshLastCursor, 0);
});
// 监听nickname后面的空格被删除(提醒此空格在margin-right: -3px;内,用于解决办法浏览器非文本节点后的光标定位不正确的问题)
const observer = new MutationObserver(function(mutations) {
mutations.forEach(mutation=> {
// 判断是否为删除节点的操作
const [removedNode] = mutation.removedNodes
if(removedNode){
if(removedNode && mutation?.previousSibling?.className === "nickname"){
mutation.previousSibling.remove()
}
}
});
})
.observe(uniImEditor, {
childList: true, // 监听子节点的变化
});
},
watch: {
modelValue(modelValue, oldModelValue) {
this.$nextTick(() => {
// console.log('modelValue', modelValue);
if (modelValue.length === 0) {
uniImEditor.innerHTML = ''
} else if (typeof modelValue === 'string' && modelValue != this.$inputText()) {
uniImEditor.innerHTML = modelValue
} else if (typeof modelValue === 'object' && modelValue.html != uniImEditor.innerHTML) {
uniImEditor.innerHTML = modelValue.html
}
});
},
},
methods: {
// 刷新光标信息
$refreshLastCursor() {
// console.log('刷新光标信息');
lastCursor = this.$getCursor();
lastFocusNode = window.getSelection().focusNode;
},
$oninput(data){
this.$refreshLastCursor()
// 耗时计算
let start = new Date().getTime();
let param = '';
let val = uniImEditor.innerHTML;
const hasImg = uniImEditor.querySelector('img');
const hasNickname = uniImEditor.querySelector('.nickname');
const hasA = uniImEditor.querySelector('a');
if (hasImg || hasNickname || hasA) {
param = {
// "rich-text": //uniIm.utils.parseHtml( 执行比较消耗内存,改为chat页面 confirm时执行,
"html": val,
"text": uniImEditor.innerText,
"aboutUserIds": Array.from(uniImEditor.querySelectorAll('.nickname')).map(i=>i.getAttribute('uid'))
}
} else {
param = this.$inputText()
}
// 打印耗时
const spendTime = new Date().getTime() - start
if(spendTime > 10){
console.log('耗时', );
}
this.$ownerInstance.callMethod('oninput',{
data,// 本次输入的数据
value: param // 当前输入框的值
})
},
$callMethod([funcName, ...params]) {
// console.log('$callMethod funcName', funcName)
// console.log('$callMethod funcName', funcName === null)
// console.log('$callMethod params', params)
// console.log('$callMethod typeof funcName', typeof funcName)
try {
if(typeof funcName == 'string'){
const res = this[funcName](...params)
// console.log('res', res)
}
} catch (e) {
console.error("调用renderjs模块的方法失败", e,funcName,params)
}
},
$getCursor() {
const selection = window.getSelection();
return selection.focusOffset;
},
// 恢复光标位置
$restoreCursor(focus = true) {
this.$focus();
// 获取焦点时所在的子元素
try{
const range = document.createRange();
const sel = window.getSelection();
range.setStart(lastFocusNode || uniImEditor, lastCursor);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}catch(e){
console.error('恢复光标位置失败',e)
//TODO handle the exception
}
},
$deleteLeftChar(n = 1) {
this.$restoreCursor(true);
const selection = window.getSelection();
if (!selection.isCollapsed){
console.error('不要删除已选中的内容')
return;
}
const range = selection.getRangeAt(0).cloneRange();
if (range.startOffset > 0) {
range.setStart(range.startContainer, range.startOffset - n);
range.deleteContents();
selection.removeAllRanges();
selection.addRange(range);
} else if (range.startContainer.previousSibling) {
const container = range.startContainer;
const sibling = container.previousSibling;
range.setStart(sibling, sibling.length - n);
range.setEnd(sibling, sibling.length);
range.deleteContents();
selection.removeAllRanges();
selection.addRange(range);
}
lastCursor = this.$getCursor();
this.$oninput();
},
$addHtmlToCursor(html,focus = true) {
this.$restoreCursor(focus);
const selection = window.getSelection();
if (selection.getRangeAt && selection.rangeCount) {
let range = selection.getRangeAt(0);
range.deleteContents();
var ele = document.createElement("div");
ele.innerHTML = html;
var frag = document.createDocumentFragment(),
node, lastNode;
while ((node = ele.firstChild)) {
lastNode = frag.appendChild(node);
}
range.insertNode(frag);
// 设置光标到插入内容之后的位置
if (lastNode) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
selection.removeAllRanges(); // 清除现有的选择区域
selection.addRange(range); // 将更新后的范围添加回选择区域
}
}else{
uniImEditor.innerHTML += html;
}
this.$oninput(html);
},
$inputText() {
return uniImEditor.innerHTML.replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "");
},
$focus() {
if(document.activeElement.className === 'uni-im-editor'){
document.activeElement.blur();
}
// #ifdef APP
setSoftinputTemporary()
// #endif
// console.error('获取焦点',document.activeElement.className);
uniImEditor.focus();
// console.error('获取焦点',document.activeElement.className);
},
$blur(){
// console.error('失去焦点1',document.activeElement.className);
uniImEditor.blur();
// console.error('失去焦点2',document.activeElement.className);
setTimeout(()=>{
// console.error('失去焦点3',document.activeElement.className);
},1000)
},
$onBlur() {
// this.$emit('update:focus', false);
// console.log('blur');
this.$emit('blur');
},
$onFocus() {
// console.log('$onFocus');
this.$emit('focus');
// this.$emit('update:focus', true);
},
$setContent(data) {
if (typeof data === 'string') {
uniImEditor.innerHTML = data
this.$oninput(data);
} else {
uniImEditor.innerHTML = this.$arrDomJsonToHtml(data);
this.$oninput(uniImEditor.innerHTML);
}
},
$arrDomJsonToHtml(arr) {
function parseItem(item) {
if (item.type === "text") {
return item.text;
}
let html = `<${item.name}`;
if (item.attrs) {
for (const key in item.attrs) {
html += ` ${key}="${item.attrs[key]}"`;
}
}
if (item.children) {
html += ">";
for (const child of item.children) {
html += parseItem(child);
}
html += `</${item.name}>`;
} else {
html += " />";
}
return html;
}
let result = "";
for (const item of arr) {
result += parseItem(item);
}
return result;
}
}
}
</script>
<style lang="scss" scoped>
.uni-im-editor {
min-height: 26px;
max-height: 110px;
overflow: auto;
// 解决ios下不能编辑的问题
-webkit-user-modify: read-write-plaintext-only;
/* #ifdef APP */
&,* {
-webkit-user-select:text;
}
/* #endif */
&:focus {
outline: none;
}
& ::v-deep {
img {
max-width: 90%;
display: block;
}
.nickname {
color: #0b65ff !important;
user-select: text;
margin-right: -3px;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
}
}
</style>
\ No newline at end of file
......@@ -8,7 +8,7 @@
<template v-if="matchedFriends.data?.length">
<view class="category-name">联系人</view>
<uni-list-chat
<uni-im-info-card
class="conversation-list-item"
v-for="item in matchedFriends.data"
:key="item.id"
......@@ -29,7 +29,7 @@
<template v-if="matchedGroups.data?.length">
<view class="category-name">群聊</view>
<uni-list-chat
<uni-im-info-card
class="conversation-list-item"
v-for="item in matchedGroups.data"
:key="item.id"
......@@ -51,7 +51,7 @@
<template v-if="matchedConversations.data?.length">
<view class="category-name">聊天记录</view>
<uni-list-chat
<uni-im-info-card
class="conversation-list-item"
v-for="item in matchedConversations.data"
:key="item.id"
......@@ -242,7 +242,9 @@
.tip {
flex: 1;
}
.conversation-list .conversation-list-item {
background-color: #fff;
}
.conversation-list .conversation-list-item.focus {
border: 2px solid #1ab94e;
}
......@@ -257,9 +259,6 @@
/* #endif */
}
/* #ifdef H5 */
.conversation-list .conversation-list-item ::v-deep .uni-list-chat__content-title {
font-size: 14px;
}
.conversation-list ::v-deep .conversation-list-item .uni-list--border {
display: none;
}
......
......@@ -7,15 +7,6 @@
const reg = /^[0-9]*$/g
return (typeof val === 'number' || reg.test(val) )? val + 'px' : val;
}
// #ifdef APP-NVUE
// import iconUrl from './uni-im-icons.ttf'
const domModule = uni.requireNativePlugin('dom')
domModule.addRule('fontFace', {
'fontFamily': "uni-im-icons",
// 'src': "url('"+iconUrl+"')"
'src': "url('https://at.alicdn.com/t/c/font_3726059_96d6x1ujhb.ttf?t=1712807858973')"
});
// #endif
export default {
emits:['click'],
data() {
......@@ -64,7 +55,6 @@
cursor: pointer;
/* #endif */
}
/* #ifndef APP-NVUE */
@font-face {
font-family: "uni-im-icons"; /* Project id 3726059 */
src: url('https://at.alicdn.com/t/c/font_3726059_96d6x1ujhb.ttf?t=1712807858973') format('truetype');
......@@ -80,5 +70,4 @@
.uni-im-delete:before {
content: "\e63d";
}
/* #endif */
</style>
<template>
<view class="info-card" :class="{link}" @click="$emit('click')">
<slot name="left"></slot>
<view class="avatar-box">
<image class="avatar" :src="avatarUrl" mode="widthFix"></image>
<slot name="avatar-overlay-list"></slot>
</view>
<view class="main">
<view class="row">
<text class="title">{{title}}</text>
<view class="tag-box">
<view class="tag" v-for="(tag,index) in tags" :key="tag">{{tag}}</view>
</view>
<view class="time" v-if="time">{{time}}</view>
</view>
<view class="row">
<view class="note-box">
<text class="red-note">{{redNote}}</text>
<text class="note">{{note}}</text>
</view>
<view class="state">
<image v-if="mute" class="mute" mode="widthFix" src="@/uni_modules/uni-im/static/mute.png">
</image>
<template v-if="badge">
<view v-if="mute" class="red-point"></view>
<view v-else class="badge">{{badge > 99 ? '99+' : badge}}</view>
</template>
</view>
</view>
</view>
<image v-if="pinned" class="pinned" mode="widthFix" src="@/uni_modules/uni-im/static/pinned.png">
</image>
<slot></slot>
<slot name="right"></slot>
</view>
</template>
<script>
export default {
emits: ['click'],
props: {
avatarUrl: {
type: String,
default: '/uni_modules/uni-im/static/avatarUrl.png'
},
title: {
type: String,
default: ''
},
tags: {
type: Array,
default: () => []
},
time: {
type: String,
default: ''
},
note: {
type: String,
default: ''
},
badge: {
type: Number,
default: 0
},
mute: {
type: Boolean,
default: false
},
pinned: {
type: Boolean,
default: false
},
redNote: {
type: String,
default: ''
},
link: {
type: Boolean,
default: false
}
},
computed: {
},
data() {
return {
}
},
methods: {}
}
</script>
<style lang="scss" scoped>
// 限制只能一行,超出显示省略号
@mixin ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.info-card {
position: relative;
padding:10px 12px;
flex-direction: row;
::after {
content: "";
position: absolute;
left: 50px;
right: 0;
bottom: 0;
height: 1px;
transform: scaleY(0.1);
background-color: #eee;
}
/* #ifdef H5 */
@media screen and (min-device-width:960px){
::after {
display: none;
}
}
&.link{
cursor: pointer;
&:hover {
background-color: #f0f0f0;
}
}
/* #endif */
.avatar-box{
position: relative;
.avatar {
width: 40px;
height: 40px;
border-radius: 5px;
flex-shrink: 0;
}
}
.main {
flex-grow: 1;
justify-content: center;
overflow: hidden;
margin-left: 5px;
.row {
flex-direction: row;
align-items: center;
// min-height: 20px;
.title {
font-size: 16px;
color: #333;
@include ellipsis;
}
.tag-box {
transform: scale(0.8);
margin-right: 5px;
.tag {
word-break: keep-all;
font-size: 12px;
color: #1a6dfe;
border: 1px solid #1a6dfe;
border-radius: 2px;
padding: 0 2px;
}
}
.time {
// 不换行
white-space: nowrap;
font-size: 10px;
color: #999;
margin-left: auto;
}
.state {
margin-left: auto;
flex-direction: row;
align-items: center;
.red-point {
width: 8px;
height: 8px;
background-color: #f00;
border-radius: 50%;
margin-left: 2px;
}
.mute {
width: 13px;
height: 13px;
opacity: 0.7;
}
.badge {
transform: scale(0.9);
font-size: 12px;
color: #fff;
background-color: #f00;
border-radius: 10px;
min-width: 16px;
min-height: 16px;
padding: 0 5px;
justify-content: center;
align-items: center;
margin:0 2px;
align-self: flex-end;
}
}
.note-box {
flex-direction: row;
overflow: hidden;
align-items: center;
.red-note {
font-size: 12px;
white-space: nowrap;
color: #f00;
}
.note {
font-size: 14px;
color: #aaa;
@include ellipsis;
}
}
}
}
.pinned {
position: absolute;
right: 3px;
top: 3px;
width: 8px;
height: 8px;
}
}
</style>
\ No newline at end of file
......@@ -25,25 +25,13 @@
}
</script>
<style>
/* #ifndef APP-NVUE */
view {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* #endif */
<style lang="scss" scoped>
.refresh-box {
height: 50px;
justify-content: center;
align-items: center;
flex-direction: row;
/* #ifdef APP-NVUE */
width: 750rpx;
/* #endif */
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
}
.refresh-icon {
width: 25px;
......
<template>
<view class="list-root">
<!-- #ifdef APP-NVUE -->
<list class="list" :bounce="false" :render-reverse="true">
<slot></slot>
<!-- #ifdef APP-NVUE -->
<!-- 解决APP端的滚动锚定问题,在最后一个cell 设置 keep-scroll-position 和 render-reverse-position -->
<cell :keep-scroll-position="true" :render-reverse-position="true" ref="uni-im-list-last-item">
<!-- 高度为0的 最后一个元素用于方便滚动到最后一个元素 -->
</cell>
<!-- #endif -->
</list>
<view :style="{height:paddingBottom}"></view>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<!-- fast-deceleration 滑动减速速率控制,enable-passive 开启 passive 特性,能优化一定的滚动性能 -->
<scroll-view :scroll-top="scrollTop" :scroll-into-view="scrollIntoView"
:enhanced="true" :bounces="false" :enable-passive="false" :fast-deceleration="false"
class="scroll-view" :scroll-anchoring="true" :scroll-y="scrollY" :scroll-with-animation="false"
:style="{paddingBottom}" :enable-flex="true"
@dragstart="dragstart" @dragend="dragend"
@scroll="onScroll"
@scrolltolower="onScrollToLower"
>
<slot></slot>
<view id="uni-im-list-last-item" key="uni-im-list-last-item">
<!-- 高度为0的 最后一个元素用于方便滚动到最后一个元素 -->
</view>
</scroll-view>
<view class="scroll-view fake-scroll-view">
<slot name="floating-block"></slot>
</view>
<!-- #endif -->
</view>
<template>
<scroll-view class="uni-im-msg-scroll-view" :scroll-anchoring="true" :enable-flex="true" :bounces="false"
:scroll-with-animation="false" :scroll-y="scrollY" :scroll-top="scrollTop" :scroll-into-view="scrollIntoView"
@scroll="onScroll" @scrolltolower="onScrollToLower" :show-scrollbar="true">
<view class="scroll-content">
<slot></slot>
<view id="uni-im-list-last-item" key="uni-im-list-last-item">
<!-- 高度为0的 最后一个元素用于方便滚动到最后一个元素 -->
</view>
</view>
</scroll-view>
</template>
<script>
/**
* uni-im-list 组件,渲染一个列表。
*
* @module
*/
export default {
data() {
return {}
},
props: {
scrollY: {
default: true
},
scrollTop: {
default: 0
},
scrollIntoView: {
type: String,
default: ''
},
paddingBottom: {
default: 0
}
},
methods: {
onScroll(e) {
this.$emit('scroll', e)
},
onScrollToLower(e) {
this.$emit('scrolltolower', e)
},
dragstart(e) {
// console.log('dragstart');
this.$emit('dragstart', e)
},
dragend(e) {
this.$emit('dragend', e)
},
},
mounted() {
}
}
/**
* uni-im-list 组件,渲染一个列表。
*
* @module
*/
export default {
emits: ['scroll', 'scrolltolower'],
data() {
return {}
},
props: {
scrollY: {
default: true
},
scrollTop: {
default: 0
},
scrollIntoView: {
type: String,
default: ''
}
},
methods: {
onScroll(e) {
this.$emit('scroll', e)
},
onScrollToLower(e) {
this.$emit('scrolltolower', e)
}
},
mounted() {}
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
.scroll-view{
overflow-anchor: auto !important;
/* #ifdef MP-WEIXIN */
height: 100vh;
/* #endif */
/* #ifdef H5 */
height: calc(100vh - 44px);
/* #endif */
}
/* #endif */
/* #ifdef APP-NVUE */
.list-root,.list{
flex: 1;
}
/* #endif */
<style lang="scss" scoped>
.uni-im-msg-scroll-view {
overflow-anchor: auto !important;
height: 100%;
-webkit-overflow-scrolling: touch;
// 上下翻转
transform: scale(1, -1);
.scroll-content {
min-height: 100%;
flex-direction: column-reverse;
}
& ::v-deep .uni-scroll-view {
/* 滚动条的样式 */
&::-webkit-scrollbar {
width: 5px; /* 滚动条宽度 */
}
/* 滚动条滑块的样式 */
&::-webkit-scrollbar-thumb {
background-color: #bbb; /* 滑块颜色 */
border-radius: 5px; /* 滑块圆角 */
}
}
}
.fake-scroll-view {
position: absolute;
top:0;
left:0;
right:0;
background-color: transparent;
/* #ifdef H5 || MP */
pointer-events: none;
/* #endif */
color: #576b95;
}
.fake-scroll-view:hover {
color: #7c8cae;
}
</style>
/* #ifdef H5 */
@media screen and (min-device-width:960px) {
.uni-im-msg-scroll-view {
// 关闭上下翻转
transform: scale(1, 1);
.scroll-content {
flex-direction: column;
}
}
}
/* #endif */
</style>
\ No newline at end of file
<template>
<view v-if="isShow" :style="{opacity}" id="popup-control">
<view class="control-mark" @touchstart.prevent="closeMe" @click="closeMe" :style="{opacity}">
<view class="control-mark" @touchstart.prevent="closeMe" @click="closeMe">
</view>
<view ref="content" class="content" :style="{top:controlData.top,left:controlData.left,right:controlData.right,opacity}">
<view ref="content" class="content" :style="{top:controlData.top,left:controlData.left,right:controlData.right}">
<template v-for="(item,index) in controlList">
<view :key="index" class="control-item" v-if="typeof item.canDisplay == 'function' ? item.canDisplay() : item.canDisplay " @click="item.action">
<uni-im-icons v-if="item.icon" :code="item.icon" size="16" color="#FFF"></uni-im-icons>
......@@ -10,9 +10,8 @@
</view>
</template>
</view>
<view class="icon" :class="{isInTop:controlData.isInTop}" :style="{right:iconBoxRight,left:iconBoxLeft,top:controlData.top,opacity}"></view>
<view class="icon" :class="{isInTop:controlData.isInTop}" :style="{right:iconBoxRight,left:iconBoxLeft,top:controlData.top}"></view>
</view>
<!-- todo:多个节点都放:style="{opacity}"是为了解决nvue下,当前情况外层不能控制内层透明度的问题 -->
</template>
<script>
......@@ -142,7 +141,6 @@
isInTop: false
}
// #ifndef APP-NVUE
const query = uni.createSelectorQuery().in(this);
await new Promise(resolve => {
query.selectAll('.content').boundingClientRect(data => {
......@@ -150,18 +148,6 @@
resolve()
}).exec();
})
// #endif
// #ifdef APP-NVUE
let ref = this.$refs['content']
await new Promise(resolve => {
const dom = weex.requireModule('dom')
dom.getComponentRect(ref, e => {
controlData.width = e.size.width + 'px'
resolve()
})
})
// #endif
// console.error('controlData.width',controlData.width)
......@@ -253,7 +239,8 @@
}
return
*/
data = data.map(i=>i.name == 'img' ? '[图片]' : i.text).join(' ')
console.log('data',data)
data = data.map(i=>i.children?i.children[0]:i).map(i=>i.name == 'img' ? '[图片]' : i.text).join(' ')
break;
default:
break;
......@@ -346,13 +333,6 @@
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
view {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* #endif */
.content{
background-color:#252a30;
height: 55px;
......@@ -363,9 +343,7 @@
flex-direction: row;
justify-content: space-around;
align-items: center;
/* #ifndef APP-NVUE */
z-index: 9999;
/* #endif */
z-index: 10;
}
.control-item{
padding: 0 15px;
......@@ -389,21 +367,9 @@
position: fixed;
top: 0;
left: 0;
width: 750rpx;
flex: 1;
height: 9000px;
/* #ifndef APP-NVUE */
width: 100vw !important;
height: 100vh !important;
z-index: 9999;
/* #endif */
/* #ifdef H5 */
// 解决:通过*选择器设置了最大宽高 引起的大小撑不开的问题
max-height:100vh !important;
max-width:100vw !important;
/* #endif */
z-index: 9;
background-color: rgba(0,0,0,0.1);
}
......@@ -413,9 +379,7 @@
background-color: #252a30;
width: 10px;
height: 10px;
/* #ifndef APP-NVUE */
z-index: 9999;
/* #endif */
z-index: 9;
}
.isInTop{
......
......@@ -4,14 +4,14 @@
<text class="language">{{language}}</text>
<text class="copy-btn" @click="copyCode">复制代码</text>
</view>
<scroll-view :scroll-y="overflow" class="code-view-box" :style="{height:showFullBtn ? boxHeight: 'auto'}">
<!-- #ifndef APP-NVUE -->
<rich-text v-if="nodes.length" space="nbsp" :nodes="nodes" @itemclick="trOnclick" :style='{"height":webViewHeight}' class="code-view-rich-text"></rich-text>
<!-- #endif -->
<scroll-view :scroll-y="overflow" :enable-flex="true" class="code-view-box" :style="{height:showFullBtn ? boxHeight: 'auto'}">
<!-- #ifdef APP-NVUE -->
<web-view ref="web" @onPostMessage="onWebViewMsg" src="/static/app-plus/mp-html/uni-im-code-view-local.html" :style='{"height":webViewHeight}' class="web-view"></web-view>
<!-- #endif -->
<!-- #ifndef APP-NVUE -->
<rich-text v-if="nodes.length" space="nbsp" :nodes="nodes" @itemclick="trOnclick" :style='{"height":webViewHeight}' class="code-view-rich-text"></rich-text>
<!-- #endif -->
</scroll-view>
<view class="show-full-btn" v-if="!isWidescreen && showFullBtn && overflow" @click="toCodePage">
<text class="show-full-text">全屏查看</text>
......@@ -166,7 +166,7 @@
<style lang="scss">
/* #ifndef APP-NVUE */
@import '@/uni_modules/uni-im/sdk/utils/highlight/github-dark.min.css';
@import '@/uni_modules/uni-im/sdk/utils/highlight/github-dark.min.scss';
/* #endif */
.code-view-root {
......@@ -207,13 +207,14 @@
position: relative;
padding: 0;
margin-bottom: 10px;
// 设置与背景一样的黑色,防止滚动条样式变化太大
background-color: #0d1117;
/* #ifndef APP-NVUE */
padding: 0 5px;
overflow: auto;
width: 100%;
/* #endif */
// 设置与背景一样的黑色,防止滚动条样式变化太大
background-color: #0d1117;
box-sizing: border-box;
}
/* #ifdef H5 */
......
......@@ -107,9 +107,7 @@
}
.file-msg-info-name {
/* #ifndef APP-NVUE */
word-wrap: break-word;
/* #endif */
font-size: 16px;
}
......
......@@ -76,16 +76,12 @@
padding: 10px;
background-color: #fff;
width: 600rpx;
/* #ifndef APP-NVUE */
max-height:200px !important;
overflow: hidden;
/* #endif */
}
.item-text-list{
/* #ifndef APP-NVUE */
max-height: 150px !important;
overflow: hidden;
/* #endif */
}
.title {
......
......@@ -38,5 +38,8 @@
<style>
.img {
width: 400rpx;
/* border: 1px solid #aaa; */
box-shadow: 0 0px 2px -1px #aaa;
border-radius: 5px;
}
</style>
......@@ -185,7 +185,6 @@
}
/* #endif */
/* #ifndef APP-NVUE */
.uni-im-rich-text {
display: inline-block;
}
......@@ -193,8 +192,12 @@
.uni-im-rich-text .text {
display: inline;
word-wrap: break-word;
user-select: text;
cursor: text;
cursor: text;
/* #ifdef H5 */
@media screen and (min-device-width:960px){
user-select: text;
}
/* #endif */
}
.uni-im-rich-text .link {
......@@ -203,18 +206,12 @@
margin: 0 2px;
word-break: break-all;
}
/* #endif */
.uni-im-rich-text {
background-color: #fff;
padding: 10px;
border-radius: 10px;
/* #ifdef APP-NVUE */
width: 600rpx;
/* #endif */
/* #ifdef MP */
max-width: 600rpx;
/* #endif */
}
.uni-im-rich-text.isFromSelf {
......@@ -284,9 +281,7 @@
color: #666;
margin-top: 5px;
flex: 1;
/* #ifndef APP-NVUE */
beak-word: break-all;
/* #endif */
}
.web-info .content .web-thumbnail {
......@@ -306,9 +301,7 @@
.web-info .link-box .link {
font-size: 12px;
// 不会换行
/* #ifndef APP-NVUE */
white-space: nowrap;
/* #endif */
flex: 1;
overflow: hidden;
}
......@@ -321,10 +314,8 @@
.web-info .link-box .copy:hover {
opacity: 0.8;
}
/* #ifndef APP-NVUE */
.uni-im-rich-text.isSingeImg {
background-color: transparent !important;
padding: 0;
}
/* #endif */
</style>
\ No newline at end of file
......@@ -12,8 +12,8 @@
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const audioContext = uniIm.audioContext
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
let audioContext = uniIm.audioContext;
export default {
data() {
return {
......@@ -41,18 +41,21 @@
self() {
return this.msg.from_uid === uniCloud.getCurrentUserInfo().uid
}
},
beforeCreate() {
audioContext = uniIm.audioContext;
},
mounted() {
this.onPlay = async () => {
// console.log('soundPlayStart------------------');
console.log('soundPlayStart------------------',this.msg.body);
let currentAudioUrl = await uniIm.utils.getTempFileURL(this.msg.body.url)
let src = uniIm.audioContext.src
let src = audioContext.src
if (src == currentAudioUrl) {
this.soundPlayState = 1
} else {
this.soundPlayState = 0
}
}
}
audioContext.onPlay(this.onPlay);
this.soundPlayEnd = () => {
// console.log('soundPlayEnd------------------');
......
......@@ -101,12 +101,7 @@
line-height: 30px;
padding: 0 15rpx;
border-radius: 8px;
/* #ifdef APP-NVUE */
margin: 0 20px;
/* #endif */
/* #ifndef APP-NVUE */
margin: 0 2em;
/* #endif */
}
.group-notification {
padding:14px 16px;
......
<template>
<view class="msg-text-box" :class="msgClass">
<view class="msg-text-box" :class="{self}">
<msgRichText v-if="htmlNodes.length" :msg="{...msg,...{'body':htmlNodes}}" />
<text v-else class="msg-text" :decode="true" space="ensp">{{ msg.body }}</text>
</view>
......@@ -31,20 +31,6 @@
self() {
return this.msg.from_uid === uniCloud.getCurrentUserInfo().uid
},
msgClass() {
let msgClass = ''
let textLength = (this.msg.body + '').replace(/[\u0000-\u007f]/g, "a")
.replace(/[\u0080-\u07ff]/g, "aa")
.replace(/[\u0800-\uffff]/g, "aa").length
if (textLength > 30) {
msgClass += ' exceed'
}
if (this.self) {
msgClass += ' self'
}
return msgClass
}
},
watch: {
"msg.body": {
......@@ -93,32 +79,24 @@
}
</script>
<style>
<style lang="scss" scoped>
.msg-text-box {
border-radius: 10px;
background-color: #FFFFFF;
/* #ifndef APP-NVUE */
max-width: 100%;
/* #endif */
}
.msg-text {
padding: 6px;
font-size: 15px;
justify-content: space-between;
/* #ifndef APP-NVUE */
word-break: break-all;
user-select: text;
cursor: text;
/* #endif */
}
/* #ifdef APP-NVUE */
.exceed {
width: 600rpx;
min-width: 30px;
.msg-text {
padding: 10px;
font-size: 15px;
justify-content: space-between;
word-break: break-all;
cursor: text;
/* #ifdef H5 */
@media screen and (min-device-width:960px){
user-select: text;
}
/* #endif */
}
}
/* #endif */
.self{
background-color: #c9e7ff;
}
......
......@@ -64,9 +64,7 @@ export default {
background-color: #fff;
padding: 10px;
border-radius: 10px;
/* #ifndef APP-NVUE */
min-width: 250px;
/* #endif */
}
/* #ifdef H5 */
.msg-userinfo-card,.msg-userinfo-card * {
......
......@@ -112,9 +112,6 @@
align-items: center;
background-color: rgba(0, 0, 0, 0.2);
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
/* #ifndef APP-NVUE */
box-sizing: content-box;
/* #endif */
}
.video-box-mark {
......
<template>
<view v-if="!msg.is_delete" class="uni-im-msg" :class="{self}" @appear="onAppear">
<view v-if="!msg.is_delete" class="uni-im-msg" :class="{self}">
<msg-system v-if="msg.type === 'system'" class="system-msg-box" :msg="msg" />
<template v-else>
<view v-if="!noTime" class="friendlyTime">
......@@ -25,10 +25,10 @@
<cloud-image ref="avatar" width="40px" height="40px" border-radius="5px"
:src="avatarUrl||'/uni_modules/uni-im/static/avatarUrl.png'" mode="widthFix"
:class="{'pointer':canPrivateChat}"
@click="toChat" @longpress.stop="chatInputContentAddcallUser" />
@click.stop="toChat" @longpress.stop="longpressMsgAvatar" />
<view class="msg-main">
<view v-if="!self" class="nickname-box">
<text :selectable="true" class="nickname" @click="chatInputContentAddcallUser">{{ msg.nickname || users.nickname }}</text>
<text :selectable="true" class="nickname" @click="longpressMsgAvatar">{{ msg.nickname || users.nickname }}</text>
<text class="isFromAdmin" v-if="isFromAdmin">管理员</text>
</view>
......@@ -163,7 +163,7 @@
default: false
},
},
emits: ['viewMsg', 'showControl', 'showMsgById','loadMore','chatInputContentAddcallUser','putChatInputContent'],
emits: ['viewMsg', 'showControl', 'showMsgById','loadMore','longpressMsgAvatar','putChatInputContent'],
data() {
let currentUser = {
user_id: uniCloud.getCurrentUserInfo().uid,
......@@ -222,7 +222,7 @@
},
isFromAdmin() {
const conversation = uniIm.conversation.getCached(this.msg.conversation_id)
return conversation.group_id && conversation.group_member[this.msg.from_uid]?.role?.includes('admin')
return conversation?.group_id && conversation.group_member[this.msg.from_uid]?.role?.includes('admin')
},
mineId() {
return uniCloud.getCurrentUserInfo().uid
......@@ -239,8 +239,12 @@
return uni.upx2px(750 / 60 * this.msg.body.time) + 50 + 'px'
},
canPrivateChat(){
// 当前登录的账号是管理员,或者当前消息是群管理员发的
return this.uniIDHasRole('staff') || this.isFromAdmin
const conversation = uniIm.conversation.getCached(this.msg.conversation_id)
const currentUserId = uniCloud.getCurrentUserInfo().uid;
// 当前登录的账号是管理员,或者是群管理员,或者当前消息是群管理员发的
return this.uniIDHasRole('staff') ||
conversation.group_member?.[currentUserId]?.role?.includes('admin') ||
this.isFromAdmin
}
},
async mounted() {
......@@ -261,7 +265,7 @@
if (avatarRef) {
avatarRef.$el.addEventListener('contextmenu', (e) => {
if (uniIm.isWidescreen) {
this.chatInputContentAddcallUser()
this.longpressMsgAvatar()
}
e.preventDefault()
})
......@@ -291,7 +295,6 @@
onDisappear() {},
async showControl(e) {
let msgContentDomInfo;
// #ifndef APP-NVUE
const query = uni.createSelectorQuery().in(this);
await new Promise(callback => {
query.selectAll('.msg-content-box .msg-content').boundingClientRect(data => {
......@@ -300,20 +303,6 @@
callback(msgContentDomInfo)
}).exec();
})
// #endif
// #ifdef APP-NVUE
let ref = this.$refs['msg-content']
await new Promise(callback => {
const dom = weex.requireModule('dom')
console.log('ref', dom);
dom.getComponentRect(ref, e => {
console.log('msgContentDomInfo e.size', e.size);
msgContentDomInfo = e.size
callback(e)
})
})
// #endif
this.$emit('showControl', {
msgId: this.msg._id,
......@@ -345,10 +334,10 @@
})
}
},
chatInputContentAddcallUser() {
longpressMsgAvatar() {
if (this.msg.group_id) {
// console.log('~~~~this.msg.from_uid', this.msg.from_uid)
this.$emit('chatInputContentAddcallUser', this.msg.from_uid)
this.$emit('longpressMsgAvatar', this.msg.from_uid)
}
},
async initAboutMsg() {
......@@ -393,15 +382,8 @@
}
</script>
<style lang="scss" scoped>
/* #ifndef APP-NVUE */
view {
display: flex;
flex-direction: column;
box-sizing: border-box;
}
/* #endif */
.uni-im-msg {
position: relative;
flex: 1;
/* #ifdef H5 */
width: 100%;
......@@ -416,6 +398,7 @@
padding: 0 8px;
margin: 8px 0;
position: relative;
width: 100%;
}
/* #ifdef H5 */
......@@ -430,26 +413,41 @@
/* #endif */
.msg-main {
flex: 1;
/* #ifndef APP-NVUE */
width: 100%;
/* #endif */
width: 80%;
margin: 0 8px;
// overflow: hidden;
}
/* 回复引用某条消息提示框 */
.cite-box {
margin:8px 0;
margin-right: auto;
background-color: #e3e3e3;
color: #6a6a6a;
border-radius: 5px;
max-width: 100%;
justify-content: center;
padding: 5px 10px;
}
.self .cite-box {
margin-right: 0;
margin-left: auto;
}
.cite-box-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
}
.self .msg-main {
align-items: flex-end;
}
.msg-content-box {
margin: 0 8px;
flex-direction: row;
align-items: center;
justify-content: flex-start;
/* #ifdef APP-NVUE */
width: 600rpx;
/* #endif */
/* #ifndef APP-NVUE */
width: calc(95% - 80px);
/* #endif */
}
.self .msg-content-box {
justify-content: flex-end;
......@@ -527,38 +525,8 @@
/* #endif */
}
/* 回复引用某条消息提示框 */
.cite-box {
margin: 3px 8px;
background-color: #e3e3e3;
color: #6a6a6a;
border-radius: 5px;
/* #ifdef H5 */
max-width: 500rpx;
/* #endif */
/* #ifndef H5 */
width: 500rpx;
/* #endif */
// height: 26px;
justify-content: center;
padding: 5px 10px;
}
.cite-box-text {
/* #ifndef APP-NVUE */
white-space: nowrap;
overflow: hidden;
/* #endif */
lines: 2;
text-overflow: ellipsis;
font-size: 14px;
}
.friendlyTime {
height: 22px;
/* #ifndef APP-NVUE */
// display: block;
/* #endif */
}
.format-time-text {
......
<template>
<view>
<view v-if="soundState" :style="{bottom:markBottom}" class="mark"></view>
<view @touchmove="touchmove" @touchstart="soundStart" @touchend="soundEnd" @touchcancel="soundEnd"
class="sound-buttom" :class="{soundState}">
<view class="sound-buttom-box">
<view @touchmove="touchmove" @touchstart="recordStart" @touchend="recordEnd" @touchcancel="recordEnd"
class="sound-buttom" :class="{recordState}">
<view v-if="soundProgress" class="sound-progress" :style="{'width':soundProgress}"></view>
<text class="sound-text">{{soundState?'录音中('+time+'s)':'按住 说话'}}</text>
<view class="sound-tip" v-if="soundState">
<text class="sound-text">{{recordState?'录音中('+time+'s)':'按住 说话'}}</text>
<view class="sound-tip" v-if="recordState">
<text class="sound-tip-text" :style="{color:cancel?'#f70000':'#FFFFFF'}">{{cancel?'松手取消':'松手发送,上划取消'}}</text>
<view class="closeIcon" :style="{'background-color':cancel?'#f70000':'#EEEEEE'}">
<uni-im-icons code="e61a" size="10px" color="#FFFFFF"></uni-im-icons>
<uni-im-icons class="icon" code="e61a" size="10px" color="#FFFFFF"></uni-im-icons>
</view>
</view>
</view>
<view v-if="recordState" :style="{bottom:markBottom}" class="mark"></view>
</view>
</template>
......@@ -23,10 +23,10 @@
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
let soundInterval,soundPath,startTime;
export default {
emits: ['success'],
emits: ['sendSoundMsg'],
data() {
return {
soundState:0,
recordState:0,
soundProgress:0,
cancel:false,
time:0,
......@@ -36,9 +36,9 @@
computed:{
...uniIm.mapState(['systemInfo']),
markBottom(){
let markBottom = 58;
let markBottom = 67;
// #ifndef H5
markBottom += this.systemInfo.safeAreaInsets.bottom
markBottom += this.systemInfo.safeAreaInsets.bottom/2
// #endif
console.log('markBottom',markBottom);
return markBottom + 'px'
......@@ -47,7 +47,7 @@
created() {
// #ifndef H5
recorderManager.onStop((res)=> {
// console.log('recorderManager.onStop',{res});
console.log('recorderManager.onStop',{res});
if(!this.cancel){
if(this.time < 2){
return uni.showToast({
......@@ -64,9 +64,9 @@
cloudPath:'uni-im/' + uniCloud.getCurrentUserInfo().uid + '/sound/' + Date.now() + '.mp3',
// fileType:"audio",
success: (e) => {
// console.log('uniCloud.uploadFile-success',e,'success',{"url":e.fileID,time:this.time});
// console.log('uniCloud.uploadFile-sendSoundMsg',e,'sendSoundMsg',{"url":e.fileID,time:this.time});
try{
this.$emit('success',{"url":e.fileID,time:this.time})
this.$emit('sendSoundMsg',{"url":e.fileID,time:this.time})
}catch(e){
console.error(e);
}
......@@ -103,21 +103,13 @@
},
methods: {
touchmove(e){
// #ifndef APP-NVUE
let y = e.touches[0].clientY + this.systemInfo.safeArea.top + (this.systemInfo.screenHeight - this.systemInfo.safeArea.bottom)
// #endif
// #ifdef APP-NVUE
let y = e.touches[0].screenY
// #endif
if(this.systemInfo.safeArea.bottom - y > 58){
this.cancel = true
}else{
this.cancel = false
}
let touchY = e.touches[0].clientY + this.systemInfo.statusBarHeight + this.systemInfo.safeArea.top
// #ifdef H5
touchY += 44
// #endif
this.cancel = this.systemInfo.safeArea.bottom - touchY > 66
},
soundStart(e){
recordStart(e){
// 关闭正在播放的sound
uniIm.audioContext.stop()
this.time = 0
......@@ -128,10 +120,10 @@
// #endif
// #ifdef H5
return uni.showToast({
title: 'h5端不支持语音功能',
icon: 'none'
});
// return uni.showToast({
// title: 'h5端不支持语音功能',
// icon: 'none'
// });
// #endif
// #ifndef H5
......@@ -144,10 +136,10 @@
startTime = Date.now()
console.log('soundStart');
console.log('recordStart');
//进度条
this.soundState = 1
this.recordState = 1
soundInterval = setInterval(()=>{
this.soundProgress = parseInt(this.soundProgress) + uni.upx2px(450/60) +'px'
// console.log('this.soundProgress',this.soundProgress);
......@@ -155,14 +147,14 @@
},1000)
// e.preventDefault();
},
soundEnd(){
recordEnd(){
// #ifndef H5
recorderManager.stop();
// #endif
console.log('soundEnd');
console.log('recordEnd');
clearInterval(soundInterval)
setTimeout(()=> {
this.soundState = 0
this.recordState = 0
this.soundProgress = 0
this.cancel = false
}, 300);
......@@ -172,18 +164,19 @@
</script>
<style lang="scss">
.sound-buttom-box {
height: 100%;
}
.sound-buttom {
background-color: #ffffff;
padding: 10px;
width: 450rpx;
height: 46px;
width: 100%;
flex: 1;
// border-radius: 10px;
font-size: 16px;
align-items: flex-start;
justify-content: center;
/* #ifndef APP-NVUE */
overflow:hidden;
/* #endif */
}
.sound-text{
position: relative;
......@@ -196,6 +189,7 @@
position: fixed;
left: 0;
bottom: 110px;
z-index: 9;
width: 750rpx;
text-align: center;
justify-content: center;
......@@ -214,6 +208,13 @@
justify-content: center;
align-items: center;
}
/* #ifdef MP-WEIXIN */
// 纠正微信小程序的icon位置
.closeIcon .icon{
position: relative;
top: -2px;
}
/* #endif */
.sound-progress {
// border-radius: 10px;
height: 44px;
......@@ -226,7 +227,7 @@
opacity: 0.3;
}
.soundState{
.recordState{
background-color: #efefef;
}
......
......@@ -54,7 +54,7 @@ export default {
<style lang="scss" scoped>
.uni-im-view-msg {
position: fixed;
position: absolute;
top: 0;
left: 0;
z-index: 20;
......
......@@ -53,8 +53,7 @@
"vue3": "y"
},
"App": {
"app-vue": "y",
"app-nvue": "y"
"app-vue": "y"
},
"H5-mobile": {
"Safari": "y",
......
<template>
<!-- 设置导航栏标题,如与谁对话,群人数为几人等 -->
<page-meta v-if="!isWidescreen">
<navigation-bar :title="navTitle" background-color="#f8f8f8" front-color="#000000" />
</page-meta>
<text v-else style="position: absolute;top: 25px;left: 15px;" :selectable="true">{{ navTitle }}</text>
<view class="page">
<view class="head">
<view v-if="count == 0 && loading" class="hint">正在加载……</view>
<view v-else class="hint">{{ count }} 条与“{{ keyword }}”相关的聊天记录</view>
<view
v-if="conversation_id"
class="enter-chat"
@click="onEnterConversation(conversation_id)"
>
<uni-icons type="chatbubble" size="16"></uni-icons>
进入会话
</view>
</view>
<scroll-view
class="message-list"
scroll-y
:scroll-into-view="autoScrollToEl"
@scrolltoupper="onScrollToUpper"
>
<uni-im-msg
v-for="msg in msgList"
:key="msg._id"
:id="`msg-${msg._id}`"
:msg="msg"
no-time
no-jump
@loadMore="cb => cb()"
>
<view class="float-info">
<view>{{ toFriendlyTime(msg) }}</view>
<view class="enter-fragment" @click="onOpenFragment(msg)">查看上下文</view>
</view>
</uni-im-msg>
<view id="bottom-el" style="height: 1px;"></view>
</scroll-view>
<chat-fragment
v-if="fragment"
:entry="fragment"
@close="onCloseFragment"
/>
</view>
</template>
<script>
/**
* chat-filtered 组件,渲染一个会话中经过滤选择的消息列表,用于显示某个会话的消息搜索结果。
*
* 点击某条消息的“查看上下文”按钮可以打开 {@link module:chat-fragment} 组件。
*
* @module
*/
const uniImCo = uniCloud.importObject("uni-im-co", {
customUI: true
})
import uniIm from '@/uni_modules/uni-im/sdk/index.js'
import ChatFragment from './cmp/chat-fragment'
export default {
components: {
ChatFragment,
},
emits: ['to-chat'],
data() {
return {
loading: true,
count: 0,
keyword: '',
msgList: [],
hasMore: true,
skip: Number.MAX_SAFE_INTEGER,
conversation_id: '',
autoScrollToEl: '',
// 当前会话对象
conversation: {},
// 聊天记录片段的入口消息
fragment: null,
};
},
computed: {
...uniIm.mapState(['isWidescreen']),
navTitle() {
let title = this.conversation.title
if (this.conversation.group_id) {
title += `(${Object.keys(this.conversation.group_member).length})`;
}
return title
}
},
async onLoad(param) {
// 调用load方法,因为pc宽屏时本页面是以组件形式展示,如 $refs.chatFiltered.load(conversation_id)
await this.load(param);
},
methods: {
async load({ keyword, count, conversation_id }) {
// 根据入口参数进行初始化
this.loading = true
this.count = count
this.keyword = keyword
this.msgList = []
this.hasMore = true
this.skip = Number.MAX_SAFE_INTEGER
this.conversation_id = conversation_id
this.conversation = await uniIm.conversation.get(conversation_id)
this.autoScrollToEl = ''
this.fragment = null
// 加载第一批匹配的聊天记录
this.loadData(() => {
// 自动滚动到底
this.autoScrollToEl = 'bottom-el'
})
},
async loadData(afterLoaded) {
this.loading = true
let result = await uniImCo.getFilteredMessageOfConversation({
keyword: this.keyword,
skip: this.skip,
conversation_id: this.conversation_id,
})
this.msgList.unshift(...result.data.reverse())
if (this.count < this.msgList.length) {
// 计数以传入的 count 为准,除非实际查询到的更多
this.count = this.msgList.length
}
this.hasMore = result.hasMore
this.skip = result.skip
this.loading = false
this.$nextTick(afterLoaded)
},
onScrollToUpper(evt) {
if (this.loading) return
if (!this.hasMore) return
let elId = 'bottom-el'
if (this.msgList.length > 0) {
elId = 'msg-' + this.msgList[0]._id
}
this.autoScrollToEl = ''
this.loadData(() => {
this.autoScrollToEl = elId
})
},
onEnterConversation(conversation_id) {
this.$emit('to-chat', { conversation_id })
},
onOpenFragment(msg) {
this.fragment = msg
},
onCloseFragment() {
this.fragment = null
},
toFriendlyTime(msg) {
return uniIm.utils.toFriendlyTime(msg.create_time || msg.client_create_time)
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.page {
flex: 1;
height: 100%;
background-color: #efefef;
}
/* #ifdef H5 */
.pc {
// .pc内的元素只有pc端打开才显示,样式在index页面
display: none;
}
/* #endif */
.head {
flex-direction: row;
justify-content: space-between;
height: 30px;
line-height: 30px;
padding: 0px 15px;
font-size: 12px;
border-bottom: 1px solid #ddd;
}
.hint {
color: #999;
}
.enter-chat {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
flex-direction: row;
padding: 0 5px;
}
/* #ifdef H5 */
.enter-chat:hover {
background-color: #ddd;
}
/* #endif */
.message-list {
height: calc(100% - 30px);
}
.uni-im-msg ::v-deep .msg-content {
width: calc(95% - 40px);
}
.float-info {
align-items: flex-end;
position: absolute;
top: 0;
right: 0px;
font-size: 12px;
color: #999;
padding: 10px;
}
.enter-fragment {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
color: #576b95;
}
/* #ifdef H5 */
.enter-fragment:hover {
color: #7c8cae;
}
.uni-im-msg:hover .enter-fragment {
display: block;
}
/* #endif */
.chat-fragment {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: white;
}
</style>
此差异已折叠。
<template>
<view class="chat-fragment">
<view class="head">
<uni-icons class="btn-back" @click="onBack" type="arrow-left" size="20"></uni-icons>
</view>
<scroll-view
class="message-list"
scroll-y
:scroll-into-view="autoScrollToEl"
@scrolltoupper="onScrollToUpper"
@scrolltolower="onScrollToLower"
>
<uni-im-msg
v-for="(msg, index) in msgList"
:key="msg._id"
:id="`msg-${msg._id}`"
:msg="msg"
:equalPrevTime="equalPrevTime(index)"
no-jump
:class="{highlight:msg.highlight}"
@loadMore="cb => cb()"
/>
</scroll-view>
</view>
</template>
<script>
/**
* chat-fragment 组件,渲染会话中一个片段的消息列表,用于显示某条消息搜索结果的上下文列表。
*
* @module
*/
const uniImCo = uniCloud.importObject("uni-im-co", {
customUI: true
})
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
emits: ['close'],
props: {
entry: {
type: Object
}
},
data() {
// 因为要修改 msg 对象的属性值,所以 clone 一下,以切断响应性,避免干扰原数据
let { ...cloneEntry } = this.entry
cloneEntry.highlight = true
return {
msgList: [cloneEntry],
hasMoreBackward: true,
skipBackward: this.entry.create_time,
hasMoreForward: true,
skipForward: this.entry.create_time,
autoScrollToEl: '',
};
},
mounted() {
this.loadDataForward(() => {
this.loadDataBackward(() => {
this.autoScrollToEl = 'msg-' + this.entry._id
}, 15)
}, 15)
},
methods: {
async loadDataForward(afterLoaded, limit = 30) {
this.loading = true
let result = await uniImCo.getFragmentMessageOfConversation({
conversation_id: this.entry.conversation_id,
skip: this.skipForward,
limit,
forward: true,
})
this.msgList.push(...result.data)
this.hasMoreForward = result.hasMore
this.skipForward = result.skip
this.loading = false
this.$nextTick(afterLoaded)
},
async loadDataBackward(afterLoaded, limit = 30) {
this.loading = true
let result = await uniImCo.getFragmentMessageOfConversation({
conversation_id: this.entry.conversation_id,
skip: this.skipBackward,
limit,
forward: false,
})
this.msgList.unshift(...result.data.reverse())
this.hasMoreBackward = result.hasMore
this.skipBackward = result.skip
this.loading = false
this.$nextTick(afterLoaded)
},
onScrollToUpper(evt) {
if (this.loading) return
if (!this.hasMoreBackward) return
let elId = 'msg-' + this.msgList[0]._id
this.autoScrollToEl = ''
this.loadDataBackward(() => {
this.autoScrollToEl = elId
})
},
onScrollToLower(evt) {
if (this.loading) return
if (!this.hasMoreForward) return
this.loadDataForward(() => {})
},
onBack() {
this.$emit('close')
},
equalPrevTime(index) {
if (index === 0) {
return false
} else if (index == this.msgList.length - 1) {
return false
} else {
const getFriendlyTime = (msg) => {
return uniIm.utils.toFriendlyTime(msg.create_time || msg.client_create_time)
}
return getFriendlyTime(this.msgList[index]) == getFriendlyTime(this.msgList[index - 1])
}
},
}
}
</script>
<style>
@import "@/uni_modules/uni-im/static/flex.scss";
.chat-fragment {
padding: 5px;
background-color: #efefef;
}
.message-list {
height: calc(100% - 30px);
}
.head {
flex-direction: row;
border-bottom: 1px solid #ddd;
}
.btn-back {
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.highlight {
background-color: #f9f3de;
}
</style>
<template>
<view class="container">
<uni-list :border="false" class="list">
<uni-im-info-card
:avatarUrl="friend_info.avatar_file ? friend_info.avatar_file.url:'/uni_modules/uni-im/static/avatarUrl.png'"
:title="friend_info.nickname"
:note="friend_info.nickname != friend_info.email ? friend_info.email : ''"
/>
<uni-list-item
title="消息免打扰"
:switch-checked="conversation.mute"
:show-switch="true"
@switchChange="changeConversationMute"
/>
<!-- #ifdef H5 -->
<!-- 发送名片消息(仅内部人员可用) -->
<uni-list-item
v-if="uniIDHasRole('staff')"
:link="true"
title="发送他(她)的名片"
@click="sendNameCard"
/>
<!-- #endif -->
<!-- 选择某个用户创建群聊(仅dcloud员工可用) -->
<uni-list-item
v-if="uniIDHasRole('staff') && friend_uid != currentUid"
:link="true"
title="选此用户创建群聊"
@click="createGroup"
/>
<template v-for="item in UserinfoMenu" :key="item.component.name">
<component :is="item.component" :conversation="conversation" cementing="UserinfoMenu" />
</template>
</uni-list>
<button
v-if="isFriend"
class="btn"
plain
type="warn"
@click="deteleFriend"
>
删除好友
</button>
<!-- #ifdef H5 -->
<uni-im-share-msg
ref="share-msg"
no-msg-list
no-comment
/>
<!-- #endif -->
</view>
</template>
<script>
const db = uniCloud.database();
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
import { ref,markRaw } from "vue"
export default {
data() {
// 调用扩展点,扩展程序可以在消息输入框新增一个工具类的项
let UserinfoMenu = uniIm.extensions
.invokeExts("userinfo-menu-extra",)
.filter((result) => result && result.component)
.map((result) => {
return {
component: markRaw(result.component),
props: result.props
};
});
return {
UserinfoMenu,
conversation: {},
friend_uid: '',
friend_info: {
username: '',
nickname: '',
avatar_file: {
url: ''
}
}
}
},
computed: {
...uniIm.mapState(['isWidescreen']),
isFriend() {
let friendList = uniIm.friend.get()
return friendList.find(i => i._id == this.friend_uid)
},
currentUid() {
return uniCloud.getCurrentUserInfo().uid
}
},
async onLoad(options) {
this.load(options)
},
methods: {
async load(options) {
console.log('options',options);
let conversation_id = options.conversation_id || options.id
// 如果只传了user_id,需要先获取conversation_id
if(!conversation_id){
if(!options.user_id){
console.error('参数错误')
return uni.showToast({
title: '参数错误',
icon: 'none'
});
}
conversation_id = await uniIm.utils.getConversationId(options.user_id)
console.log('conversation_id',conversation_id);
}
let conversation = await uniIm.conversation.get(conversation_id)
this.conversation = conversation
this.friend_uid = conversation.friend_uid
let field = '_id,nickname,avatar_file'
if (this.uniIDHasRole('staff')) {
field += ',email'
}
let res = await db.collection('uni-id-users')
.doc(this.friend_uid)
.field(field)
.get()
// console.log("res: ",res);
this.friend_info = res.result.data[0]
},
changeConversationMute() {
console.log('changeConversationMute',this.conversation)
this.conversation.changeMute()
},
async deteleFriend() {
uni.showModal({
title: '确认要删除好友吗',
content: '此操作不可撤销',
showCancel: true,
cancelText: '取消',
confirmText: '确认',
complete: async (e) => {
if (e.confirm) {
uni.showLoading({
mask: true
});
try {
await db.collection('uni-im-friend').where({
friend_uid: this.friend_uid,
user_id: uniCloud.getCurrentUserInfo().uid
}).remove()
// 收到push消息后store会自动,将此用户从列表中移除
uni.navigateBack({ delta: 2 })
} catch (e) {
uni.showModal({
content: JSON.stringify(e.message),
showCancel: false
});
}
uni.hideLoading()
}
}
});
},
async createGroup(){
console.log('createGroup')
const user_ids = [this.friend_uid]
const uniImCo = uniCloud.importObject("uni-im-co")
let res = await uniImCo.chooseUserIntoGroup({
user_ids
})
uni.$emit('uni-im-toChat','group_'+res.data.group_id)
},
// #ifdef H5
async sendNameCard() {
let msg = {
type: 'userinfo-card',
body: {
user_id: this.conversation.friend_uid,
name: this.conversation.title,
},
from_uid: uniCloud.getCurrentUserInfo().uid,
create_time: Date.now(),
}
this.$refs['share-msg'].open([msg], false)
}
// #endif
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.container {
width: 750rpx;
height: 100vh;
align-items: center;
background-color: #fff;
}
.list {
width: 750rpx;
border-bottom: 1px solid #ececec;
}
/* #ifdef H5 */
.list ::v-deep .info-card {
.title, .note {
cursor: text;
user-select: text;
}
}
.list ::v-deep .uni-list-item + .uni-list-item{
cursor: pointer;
}
/* #endif */
.btn {
margin-top: 15px;
width: 600rpx;
/* height: 45px; */
text-align: center;
line-height: 45px;
border-radius: 20rpx;
}
</style>
\ No newline at end of file
<template>
<view v-if="url" class="video-box" :class="{'is-float-mode':mode == 'float'}">
<view class="mask" v-if="mode == 'float'"></view>
<video @click="showCloseBtnFn" :src="url" :autoplay="true" :page-gesture="true" :show-mute-btn="true" :show-fullscreen-btn="mode == 'float'" class="video"></video>
<uni-icons v-if="showCloseBtn" @click="close" size="30px" color="#FFFFFF" type="closeempty" class="close-icon"></uni-icons>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
export default {
data() {
return {
url:"",
showCloseBtn:true,
// 全屏模式和小窗模式,fullscreen为全屏模式,float为小窗模式
mode: 'fullscreen',
};
},
onLoad({url}) {
// console.log({url});
this.url = url
setTimeout(()=> {
this.showCloseBtn = false
}, 1000);
},
mounted(){
// console.log('mounted');
uni.$on('uni-im-playVideo',(url)=>{
this.mode = 'float'
this.url = url
this.showCloseBtn = true
})
// 监听esc按键,关闭视频
// #ifdef H5
uniIm.utils.appEvent.onKeyDown(evt => this.onDownEscapeKey, {
order: 1000,
match: {
key: 'Escape',
altKey: false,
ctrlKey: false,
shiftKey: false,
}
})
// #endif
},
beforeDestroy(){
// #ifdef H5
uniIm.utils.appEvent.offKeyDown(this.onDownEscapeKey)
// #endif
},
methods:{
onDownEscapeKey() {
if (this.url.length) {
this.close()
}
return true
},
close(){
if(this.mode == 'fullscreen'){
uni.navigateBack()
}else{
this.url = ''
}
},
showCloseBtnFn(){
// console.log('showCloseBtnFn');
this.showCloseBtn = true
if(this.mode == 'fullscreen'){
setTimeout(()=> {
this.showCloseBtn = false
}, 5000);
}
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
page {
height: 100%;
}
.video-box,.video{
width: 100vw;
height: 100%;
}
.close-icon{
position: absolute;
top: 80rpx;
left: 30rpx;
z-index: 10;
text-shadow: 0 0 15px black;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.is-float-mode,
.is-float-mode .video{
position: fixed;
top: 10vh;
left: calc(10vw + 250px);
width: calc(80vw - 220px) !important;
height: 80vh !important;
z-index: 9;
}
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
</style>
<template>
<view class="code-view">
<!-- 默认不启用代码浏览模块,如有需要请关闭注释 -->
<uni-im-code-view :msg="msg" :showFullBtn="false"></uni-im-code-view>
</view>
</template>
<script>
import uniImCodeView from '@/uni_modules/uni-im/components/uni-im-msg/types/code.vue'
export default {
components: {
uniImCodeView
},
data() {
return {
msg: {
type: "code",
body: ""
}
}
},
onLoad({
code
}) {
console.log(code)
this.msg.body = JSON.parse(decodeURIComponent(code));
},
methods: {}
}
</script>
<style lang="scss">
@import "@/uni_modules/uni-im/static/flex.scss";
.text-box,
.code-view {
width: 750rpx;
height: 100vh;
}
</style>
\ No newline at end of file
<template>
<view>
<uni-nav-bar color="#999" :fixed="true" background-color="#ffffff" status-bar left-icon="left" @clickLeft="back">
<view class="segmented-box">
<uni-segmented-control :current="current" :values="items" @clickItem="setActiveIndex" styleType="button" activeColor="#5fc08e" style="width:120px;"></uni-segmented-control>
</view>
</uni-nav-bar>
<view class="content">
<uni-search-bar :placeholder="activeIndex?'搜索群名称/群号':'搜索手机号/用户名/用户昵称'" :radius="100"
class="search-bar"
bgColor="#eeeeee"
v-model="keyword"
@confirm="doSearch"
@focus="searchFocus = true"
@blur="searchFocus = false"
@cancel="doClear"
@clear="doClear"
></uni-search-bar>
<view v-if="activeIndex === 0">
<!-- 搜索 -->
<view v-if="usersList.length">
<uni-im-info-card v-for="(item,index) in usersList" :key="index"
:title="item.nickname" :avatarCircle="true"
:avatar="item.avatar_file&&item.avatar_file.url ? item.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'"
>
<text v-if="item.isFriend" class="chat-custom-right grey">已添加</text>
<text v-else @click="addUser(index)" class="chat-custom-right">加为好友</text>
</uni-im-info-card>
<!--
v-if="keyword.length"
<template v-else>
<uni-list-item v-for="(tab,index) in tabs" :key="index"
class="tab-item" :title="tab.title"
:to="tab.url" showArrow :border="false"
></uni-list-item>
</template> -->
</view>
<uni-im-load-state v-else :status="loading?'loading':(hasMore?'hasMore':'noMore')"></uni-im-load-state>
</view>
<view v-if="activeIndex === 1">
<view v-if="groupList.length">
<uni-im-info-card v-for="(item,index) in groupList" :key="index"
:title="item.name"
:avatar="item.avatar_file && item.avatar_file.url ? item.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'"
>
<text v-if="item.isExist" class="chat-custom-right grey">已加入</text>
<text v-else @click="addUser(index)" class="chat-custom-right">申请加入</text>
</uni-im-info-card>
</view>
<uni-im-load-state v-else :status="loading?'loading':(hasMore?'hasMore':'noMore')"></uni-im-load-state>
</view>
</view>
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog mode="input" :title="activeIndex?'申请加群':'申请添加好友'"
placeholder="请输入验证信息" confirmText="发送" message="成功消息"
:duration="2000" :before-close="true" :value="value"
@close="close" @confirm="confirm"
></uni-popup-dialog>
</uni-popup>
</view>
</template>
<script>
import uniIm from '@/uni_modules/uni-im/sdk/index.js';
const db = uniCloud.database();
export default {
data() {
return {
current:0,
loading:true,
hasMore: false,
activeIndex:0,
value:'',
items: ['找人', '找群'],
searchFocus:false,//是否展示搜索列表
keyword:'',
tabs:[
{
'title':'添加手机联系人',
'url':''
},
{
'title':'扫一扫加好友',
'url':''
},
{
'title':'查找陌生人',
'url':''
}
],
usersData: [],
checkIndex:'',//申请加的群index
groupData:[]
}
},
computed: {
usersList() {
let current_uid = uniCloud.getCurrentUserInfo().uid
let friendList = uniIm.friend.dataList
return this.usersData.map(item => {
const isFriend = friendList.find(i=>i._id == item._id)
return {
...item,
isFriend
}
})
},
groupList() {
let groupList = uniIm.group.dataList
console.log('已经加入的groupList',groupList);
console.log('查到的groupList',this.groupData);
// return this.groupData.filter(item=> groupList.find(i=>i._id == item._id))
return this.groupData.map(item => {
const isExist = groupList.find(i=>i._id == item._id)
return {
...item,
isExist
}
})
}
},
onLoad(param) {
this.setParam(param)
},
methods: {
setParam(param){
console.log("param: ",param);
if(param.group_id){
this.current = 1
this.setActiveIndex({currentIndex: 1})
this.keyword = param.group_id
return this.doSearch()
}
this.getUserList()
this.getGroupsList()
},
async getGroupsList(){
const limit = 1000
const skip = this.groupData.length/limit + 1
const res = await db.collection('uni-im-group')
.where(`"user_id" != "${uniCloud.getCurrentUserInfo().uid}"`)
.field('_id,name,avatar_file')
.orderBy('create_date', 'desc')
.limit(limit)
.skip(skip)
.get()
// console.log("uni-im-group: ",res);
if(res.result.data.length){
this.loading = false
this.hasMore = true
this.groupData = res.result.data
}
},
async getUserList(){
try{
let res = await db.collection('uni-id-users')
.field('_id,nickname,avatar_file')
.get()
let data = res.result.data
// console.log("data: ",data);
if(data.length){
this.loading = false
this.hasMore = true
this.usersData = data
}
}catch(e){
console.log(e);
}
},
back() {
uni.navigateBack()
},
async doSearch(e){
console.log("doSearch: ",e,this.keyword);
uni.showLoading({
title: '搜索中'
})
if(this.activeIndex){
let res = await db.collection('uni-im-group')
.where(`
/${this.keyword}/.test(name) ||
"_id" == "${this.keyword}"
`)
.get()
console.log(res);
this.groupData = res.result.data
}else{
const whereString = [
"_id",
"username",
"nickname",
"email",
"mobile"
].map(item => `"${item}" == "${this.keyword}"`).join(' || ')
// console.log('whereString',whereString);
let res = await db.collection('uni-id-users')
.where(whereString)
.field('_id,nickname,avatar_file')
.get()
// tip:用户表数据少,或者已做好优化,可以使用:/${this.keyword}/.test(nickname) 模糊匹配用户昵称
console.log(res);
this.usersData = res.result.data
}
uni.hideLoading()
},
doClear() {
if(this.keyword){
this.keyword = ''
this.usersData = []
this.groupData = []
this.getUserList()
this.getGroupsList()
}
},
setActiveIndex(e) {
// console.log("activeIndex: ",e);
if (this.activeIndex != e.currentIndex) {
this.activeIndex = e.currentIndex;
}
},
addUser(index){
this.checkIndex = index
this.$refs.popup.open()
},
async confirm(value){
// if(!value){
// uni.showToast({
// title: '验证信息不能为空!',
// icon:'none'
// });
// return
// }
// console.log('提供的验证信息',value);
this.value = value
this.$refs.popup.close()
if(this.activeIndex === 0){
//添加好友
const uniImCo = uniCloud.importObject("uni-im-co")
await uniImCo.addFriendInvite({
"to_uid": this.usersList[this.checkIndex]._id,
"message": this.value
}).then((res)=>{
console.log("res: ",res);
uni.showToast({
title: '已申请',
icon: 'none'
});
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
})
}else{
// console.log('1233123132123132',this.groupData,this.checkIndex);
//申请加群
db.collection('uni-im-group-join').add({
"group_id":this.groupList[this.checkIndex]._id,
"message":this.value
}).then((res) => {
console.log("res: ",res);
uni.showToast({
icon: 'none',
title: '已申请'
})
}).catch((err) => {
uni.showModal({
content: err.message || '请求服务失败',
showCancel: false
})
})
}
setTimeout(()=> {
this.value = ''
}, 100);
},
close(){
console.log('取消了');
this.$refs.popup.close()
}
}
}
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
.segmented-box{
flex: 1;
justify-content: center;
align-items: center;
}
.tab-item{
border-bottom: #f5f5f5 solid 1px;
height:60px;
justify-content: center;
padding: 0 15rpx;
}
.background{
background-color: #f5f5f5;
}
.grey{
color: #ddd;
}
.chat-custom-right {
width:70px;
height:30px;
line-height: 30px;
color: #666;
font-size: 12px;
text-align: center;
background-color: #efefef;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
border-radius: 100px;
}
.border{
border: #ddd solid 1px;
}
.state-text{
text-align: center;
font-size: 28rpx;
}
/* #ifdef H5 */
@media screen and (min-device-width:960px){
.content {
margin-top: 0;
height: calc(100vh - 175px);
overflow: auto;
}
::v-deep .uni-navbar__header-btns-left,
::v-deep .uni-navbar__placeholder,
{
display: none;
}
::v-deep .uni-navbar--fixed{
position: relative;
left: 0
}
}
/* #endif */
</style>
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
<template>
<view>
<unicloud-db ref="udb" @load="handleLoad" v-slot:default="{ data, loading, pagination, error, options }"
collection="uni-id-users" field="_id,nickname,avatar_file" :where="udbWhere">
<view v-if="error" class="error">
<text>{{ error.message }}</text>
</view>
<uni-list v-else>
<uni-list-chat v-for="(item, index) in data" :key="item._id" link
<uni-im-info-card v-for="(item, index) in data" :key="item._id" link
:title="item.nickname"
:avatar="item.avatar_file ? item.avatar_file.url : '/uni_modules/uni-im/static/avatarUrl.png'"
@click="toChat(item._id)"></uni-list-chat>
@click="toChat(item._id)"></uni-im-info-card>
</uni-list>
<uni-im-load-state :status="loading ? 'loading' : loadMoreStatus"></uni-im-load-state>
</unicloud-db>
......@@ -110,4 +111,5 @@ import uniIm from '@/uni_modules/uni-im/sdk/index.js';
};
</script>
<style lang="scss" scoped>
@import "@/uni_modules/uni-im/static/flex.scss";
</style>
\ No newline at end of file
......@@ -17,7 +17,6 @@ uni-im是云端一体的、全平台的、免费的、开源即时通讯系统
## 特点优势
- 性价比高;前后端代码均免费开源,相比竞品使用uni-im仅需花费极少的托管在uniCloud(serverless服务器)产生的费用[详情查看](#cost)
- 全端可用
- App端支持nvue,更好的长列表性能。list组件性能优势[详情参考](https://uniapp.dcloud.net.cn/component/list.html)
- 中心化响应式数据管理,切换会话无需重新加载数据,更流畅的体验
- App端聚合多个手机厂商推送通道,app不在线也可以收到消息
......
......@@ -16,8 +16,4 @@ export default (version)=>{
// console.log('clear history storage success');
}
// #endif
// #ifdef APP
console.log('APP端未实现checkVersion,待补充')
// #endif
}
\ No newline at end of file
此差异已折叠。
......@@ -137,7 +137,7 @@ msgEvent.onMsg(async res=>{
}
// #ifdef APP
console.log('notification type=>',res.type);
// console.log('notification type=>',res.type);
if (res.type == 'click'){
let currentPages = getCurrentPages()
let topViewRoute = currentPages[currentPages.length - 1].route
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
static/pinned.png

2.1 KB | W: | H:

static/pinned.png

2.6 KB | W: | H:

static/pinned.png
static/pinned.png
static/pinned.png
static/pinned.png
  • 2-up
  • Swipe
  • Onion skin
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册