This commit is contained in:
pengxiaolong
2025-05-13 19:39:53 +08:00
parent 37da6765b8
commit c006a8e63d
1232 changed files with 96963 additions and 883 deletions

View File

@@ -0,0 +1,310 @@
<template>
<div
ref="conversationListInnerDomRef"
class="tui-conversation-list"
>
<ActionsMenu
v-if="isShowOverlay"
:selectedConversation="currentConversation"
:actionsMenuPosition="actionsMenuPosition"
:selectedConversationDomRect="currentConversationDomRect"
@closeConversationActionMenu="closeConversationActionMenu"
/>
<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="30px"
/>
<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 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"
/>
</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';
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';
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 + (isH5 ? 44 : 0),
// @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;
}
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;
}
</style>

View File

@@ -0,0 +1,77 @@
.tui-conversation {
&-item {
&-pinned {
background: #eef0f3;
}
&-selected,
&-toggled {
background: rgba(0, 110, 255, 0.1);
}
.left {
.num {
background: red;
color: #fff;
&-notify {
background: red;
color: #fff;
}
}
}
.content-header {
&-label {
color: #000;
}
.name {
font-weight: 400;
letter-spacing: 0;
color: #000;
}
}
.middle-box {
&-at, &-draft {
color: #fb5059 !important;
font-family: PingFangSC-Regular;
font-weight: 400;
}
&-content {
font-weight: 400;
color: #999;
letter-spacing: 0;
}
}
.content-footer {
color: #999;
.time {
color: #bbb;
}
}
}
&-content {
.dialog {
background: #fff;
&-item {
background: #fff;
border: 1px solid #e0e0e0;
box-shadow: 0 -4px 12px 0 rgba(0, 0, 0, 0.06);
}
.conversation-options {
font-family: PingFangSC-Regular;
font-weight: 400;
color: #4f4f4f;
letter-spacing: 0;
}
}
}
}

View File

@@ -0,0 +1,43 @@
.tui-conversation-list-h5 {
.tui-conversation-content {
.dialog {
left: auto;
right: 18px;
padding: 0;
.conversation-options {
padding: 12px;
font-size: 16px;
}
&-item-up {
top: -70px;
}
}
.tui-conversation-item {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
.content {
.name {
font-size: 16px;
}
.middle-box {
p {
font-size: 14px;
}
}
}
.time {
font-size: 14px;
}
}
}
}

View File

@@ -0,0 +1,4 @@
@import '../../../../assets/styles/common';
@import './color';
@import './web';
@import './h5';

View File

@@ -0,0 +1,186 @@
.tui-conversation-list {
font-family: PingFangSC-Regular;
font-weight: 400;
letter-spacing: 0;
flex: 1;
overflow: auto;
}
.tui-conversation {
&-item {
padding: 12px;
display: flex;
align-items: center;
cursor: pointer;
box-sizing: border-box;
overflow: hidden;
.left {
position: relative;
width: 36px;
height: 36px;
.num {
position: absolute;
display: inline-block;
right: 0;
top: -5px;
min-width: 10px;
width: fit-content;
padding: 0 2.5px;
height: 15px;
font-size: 10px;
text-align: center;
line-height: 15px;
border-radius: 7.5px;
}
.num-notify {
position: absolute;
display: inline-block;
right: 2px;
top: -2px;
width: 6px;
height: 6px;
font-size: 10px;
text-align: center;
line-height: 15px;
border-radius: 65%;
}
.avatar {
width: 30px;
height: 30px;
border-radius: 5px;
}
.online-status {
box-sizing: border-box;
position: absolute;
width: 10px;
height: 10px;
left: 24px;
top: 22px;
border: 2px solid #fff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
border-radius: 50%;
&-online {
background: #29cc85;
}
&-offline {
background: #a4a4a4;
}
}
}
.content-footer {
line-height: 16px;
display: flex;
flex-direction: column;
.time {
font-size: 12px;
line-height: 16px;
display: inline-block;
white-space: nowrap;
}
}
.content {
display: flex;
flex: 1;
padding-left: 8px;
justify-content: space-between;
box-sizing: border-box;
overflow: hidden;
.content-footer {
align-items: flex-end;
.icon {
display: inline-block;
width: 16px;
height: 16px;
margin: 0;
}
}
}
.content-header {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
&-label {
flex: 1;
font-size: 14px;
}
.name {
width: 110px;
letter-spacing: 0;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.middle-box {
flex: 1;
display: flex;
align-items: center;
&-at,
&-draft {
font-size: 12px;
}
&-content {
flex: 1;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
line-height: 16px;
}
}
}
&-content {
position: relative;
.tui-conversation-item:hover {
background: rgba(0, 110, 255, 0.1);
}
.dialog {
position: absolute;
z-index: 5;
padding: 2px 20px;
cursor: pointer;
&-item {
top: 30px;
left: 164px;
border-radius: 8px;
}
.conversation-options {
padding: 5px 0;
height: 17px;
font-size: 12px;
line-height: 17px;
}
&-item-up {
top: -50px;
}
}
}
}
}