Files
uniapp-im-shop/TUIKit/components/TUIConversation/conversation-list/index.vue
2026-01-22 01:23:57 +08:00

395 lines
11 KiB
Vue

<template>
<div ref="conversationListInnerDomRef" class="tui-conversation-list">
<ActionsMenu
v-if="isShowOverlay"
:selectedConversation="currentConversation"
:actionsMenuPosition="actionsMenuPosition"
:selectedConversationDomRect="currentConversationDomRect"
@closeConversationActionMenu="closeConversationActionMenu"
/>
<cb-empty
v-if="conversationList.length === 0"
name="您还没有好友"
></cb-empty>
<div
v-for="(conversation, index) in conversationList"
:id="`convlistitem-${index}`"
:key="index"
:class="[
'tui-conversation-content',
isMobile && 'tui-conversation-content-h5 disable-select'
]"
>
<div
:class="[
isPC && 'isPC',
'tui-conversation-item',
currentConversationID === conversation.conversationID &&
'tui-conversation-item-selected',
conversation.isPinned && 'tui-conversation-item-pinned'
]"
@click="enterConversationChat(conversation.conversationID)"
@longpress="
showConversationActionMenu($event, conversation, index)
"
@contextmenu="
showConversationActionMenu($event, conversation, index, true)
"
>
<aside class="left">
<Avatar
useSkeletonAnimation
:url="conversation.getAvatar()"
size="40px"
/>
<div
v-if="
userOnlineStatusMap && isShowUserOnlineStatus(conversation)
"
:class="[
'online-status',
Object.keys(userOnlineStatusMap).length > 0 &&
Object.keys(userOnlineStatusMap).includes(
conversation.userProfile.userID
) &&
userOnlineStatusMap[conversation.userProfile.userID]
.statusType === 1
? 'online-status-online'
: 'online-status-offline'
]"
/>
<span
v-if="conversation.unreadCount > 0 && !conversation.isMuted"
class="num"
>
{{
conversation.unreadCount > 99
? '99+'
: conversation.unreadCount
}}
</span>
<span
v-if="conversation.unreadCount > 0 && conversation.isMuted"
class="num-notify"
/>
</aside>
<div class="content">
<div class="content-header">
<label class="content-header-label">
<p class="name">{{ conversation.getShowName() }}</p>
</label>
<div v-if="isRedEnvelope(conversation)" class="middle-box">
<div class="middle-box-content">
{{ redEnvelopeText(conversation) }}
</div>
</div>
<div v-else class="middle-box">
<span
v-if="
conversation.draftText &&
conversation.conversationID !== currentConversationID
"
class="middle-box-draft"
>
{{ TUITranslateService.t('TUIChat.[草稿]') }}
</span>
<span
v-else-if="
conversation.type === 'GROUP' &&
conversation.groupAtInfoList &&
conversation.groupAtInfoList.length > 0
"
class="middle-box-at"
>
{{ conversation.getGroupAtInfo() }}
</span>
<div class="middle-box-content">
{{ conversation.getLastMessage('text') }}
</div>
</div>
</div>
<div class="content-footer">
<span class="time">
{{ conversation.getLastMessage('time') }}
</span>
<Icon
v-if="conversation.isMuted"
:file="muteIcon"
size="16px"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
interface IUserStatus {
statusType: number
customStatus: string
}
interface IUserStatusMap {
[userID: string]: IUserStatus
}
import { ref, onMounted, onUnmounted } from '../../../adapter-vue'
import TUIChatEngine, {
TUIStore,
StoreName,
TUIConversationService,
TUITranslateService,
IConversationModel
} from '@tencentcloud/chat-uikit-engine-lite'
import {
TUIGlobal,
isIOS,
addLongPressListener
} from '@tencentcloud/universal-api'
import Icon from '../../common/Icon.vue'
import Avatar from '../../common/Avatar/index.vue'
import ActionsMenu from '../actions-menu/index.vue'
import muteIcon from '../../../assets/icon/mute.svg'
import {
isPC,
isH5,
isUniFrameWork,
isMobile
} from '../../../utils/env'
import { CHAT_MSG_CUSTOM_TYPE } from '../../../constant'
const emits = defineEmits(['handleSwitchConversation', 'getPassingRef'])
const currentConversation = ref<IConversationModel>()
const currentConversationID = ref<string>()
const currentConversationDomRect = ref<DOMRect>()
const isShowOverlay = ref<boolean>(false)
const conversationList = ref<IConversationModel[]>([])
const conversationListDomRef = ref<HTMLElement | undefined>()
const conversationListInnerDomRef = ref<HTMLElement | undefined>()
const actionsMenuPosition = ref<{
top: number
left: number | undefined
conversationHeight: number | undefined
}>({
top: 0,
left: undefined,
conversationHeight: undefined
})
const displayOnlineStatus = ref(false)
const userOnlineStatusMap = ref<IUserStatusMap>()
let lastestOpenActionsMenuTime: number | null = null
onMounted(() => {
TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated,
conversationList: onConversationListUpdated,
currentConversation: onCurrentConversationUpdated
})
TUIStore.watch(StoreName.USER, {
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated
})
if (!isUniFrameWork && isIOS && !isPC) {
addLongPressHandler()
}
})
onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated,
conversationList: onConversationListUpdated,
currentConversation: onCurrentConversationUpdated
})
TUIStore.unwatch(StoreName.USER, {
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated
})
})
const isShowUserOnlineStatus = (
conversation: IConversationModel
): boolean => {
return (
displayOnlineStatus.value &&
conversation.type === TUIChatEngine.TYPES.CONV_C2C
)
}
const showConversationActionMenu = (
event: Event,
conversation: IConversationModel,
index: number,
isContextMenuEvent?: boolean
) => {
if (isContextMenuEvent) {
event.preventDefault()
if (isUniFrameWork) {
return
}
}
currentConversation.value = conversation
lastestOpenActionsMenuTime = Date.now()
getActionsMenuPosition(event, index)
}
const closeConversationActionMenu = () => {
// Prevent continuous triggering of overlay tap events
if (
lastestOpenActionsMenuTime &&
Date.now() - lastestOpenActionsMenuTime > 300
) {
currentConversation.value = undefined
isShowOverlay.value = false
}
}
const getActionsMenuPosition = (event: Event, index: number) => {
if (isUniFrameWork) {
if (typeof conversationListDomRef.value === 'undefined') {
emits('getPassingRef', conversationListDomRef)
}
const query = TUIGlobal?.createSelectorQuery().in(
conversationListDomRef.value
)
query
.select(`#convlistitem-${index}`)
.boundingClientRect(data => {
if (data) {
actionsMenuPosition.value = {
// The uni-page-head of uni-h5 is not considered a member of the viewport, so the height of the head is manually increased.
top: data.bottom - 44,
// @ts-expect-error in uniapp event has touches property
left: event.touches[0].pageX,
conversationHeight: data.height
}
isShowOverlay.value = true
}
})
.exec()
} else {
const rect =
(
(event.currentTarget || event.target) as HTMLElement
)?.getBoundingClientRect() || {}
if (rect) {
actionsMenuPosition.value = {
top: rect.bottom,
left: isPC ? (event as MouseEvent).clientX : undefined,
conversationHeight: rect.height
}
}
isShowOverlay.value = true
}
}
const enterConversationChat = (conversationID: string) => {
emits('handleSwitchConversation', conversationID)
TUIConversationService.switchConversation(conversationID)
}
function addLongPressHandler() {
if (!conversationListInnerDomRef.value) {
return
}
addLongPressListener({
element: conversationListInnerDomRef.value,
onLongPress: (event, target) => {
const index = (
Array.from(
conversationListInnerDomRef.value!.children
) as HTMLElement[]
).indexOf(target!)
showConversationActionMenu(
event,
conversationList.value[index],
index
)
},
options: {
eventDelegation: {
subSelector: '.tui-conversation-content'
}
}
})
}
function onCurrentConversationUpdated(
conversation: IConversationModel
) {
currentConversation.value = conversation
}
/** 是否红包 */
const isRedEnvelope = (item: IConversationModel) => {
if (item?.lastMessage?.payload?.data) {
const businessID = JSON?.parse(
item?.lastMessage?.payload?.data
)?.businessID
return businessID === CHAT_MSG_CUSTOM_TYPE.RED_ENVELOPE
}
return false
}
/** 红包文案 */
const redEnvelopeText = (item: IConversationModel) => {
const payload = JSON.parse(item.lastMessage?.payload?.data)
const text = item.getLastMessage('text')?.split(':')
console.log(text)
if (text && text.length > 1) {
return `${text[0]}:[积分红包] ${payload.title}`
} else {
return `[积分红包] ${payload.title}`
}
}
function onConversationListUpdated(list: IConversationModel[]) {
conversationList.value = list
}
function onCurrentConversationIDUpdated(id: string) {
currentConversationID.value = id
}
function onDisplayOnlineStatusUpdated(status: boolean) {
displayOnlineStatus.value = status
}
function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
if (list.size !== 0) {
userOnlineStatusMap.value = [...list.entries()].reduce(
(obj, [key, value]) => {
obj[key] = value
return obj
},
{} as IUserStatusMap
)
}
}
// Expose to the parent component and close actionsMenu when a sliding event is detected
defineExpose({ closeChildren: closeConversationActionMenu })
</script>
<style lang="scss" scoped src="./style/index.scss"></style>
<style lang="scss" scoped>
.disable-select {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tui-conversation-item {
&::after {
display: none !important;
}
&:active {
background: #f4f4f4 !important;
}
}
</style>