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,258 @@
<template>
<div :class="['tui-contact-list-card', !isPC && 'tui-contact-list-card-h5']">
<div class="tui-contact-list-card-left">
<Avatar
class="tui-contact-list-card-left-avatar"
useSkeletonAnimation
:url="generateAvatar(props.item)"
/>
<div
v-if="props.displayOnlineStatus && props.item"
:class="{
'online-status': true,
'online-status-online': isOnline,
'online-status-offline': !isOnline,
}"
/>
</div>
<div class="tui-contact-list-card-main">
<div class="tui-contact-list-card-main-name">
{{ generateName(props.item) }}
</div>
<div
v-if="otherContentForSow"
class="tui-contact-list-card-main-other"
>
{{ otherContentForSow }}
</div>
</div>
<div class="tui-contact-list-card-right">
<div
v-if="groupTypeForShow"
class="tui-contact-list-card-right-group-type"
>
{{ groupTypeForShow }}
</div>
<div
v-if="showApplicationStatus"
class="tui-contact-list-card-right-application"
>
<div
v-if="showApplicationStatus.style === 'text'"
class="tui-contact-list-card-right-application-text"
>
{{ TUITranslateService.t(`TUIContact.${showApplicationStatus.label}`) }}
</div>
<button
v-else-if="showApplicationStatus.style === 'button'"
class="tui-contact-list-card-right-application-button"
@click.stop="showApplicationStatus.onClick"
>
{{ TUITranslateService.t(`TUIContact.${showApplicationStatus.label}`) }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, withDefaults, inject, watch, ref, Ref } from '../../../../adapter-vue';
import TUIChatEngine, {
TUITranslateService,
IGroupModel,
FriendApplication,
Friend,
} from '@tencentcloud/chat-uikit-engine';
import { IContactInfoType, IUserStatus } from '../../../../interface';
import Avatar from '../../../common/Avatar/index.vue';
import { generateAvatar, generateName, acceptFriendApplication } from '../../utils';
import { isPC } from '../../../../utils/env';
const props = withDefaults(
defineProps<{
item: IContactInfoType;
displayOnlineStatus?: boolean;
}>(),
{
item: () => ({} as IContactInfoType),
displayOnlineStatus: false,
},
);
const userOnlineStatusMap = inject<Ref<Map<string, IUserStatus>>>('userOnlineStatusMap');
const isOnline = ref<boolean>(false);
const groupType = {
[TUIChatEngine.TYPES.GRP_WORK]: 'Work',
[TUIChatEngine.TYPES.GRP_AVCHATROOM]: 'AVChatRoom',
[TUIChatEngine.TYPES.GRP_PUBLIC]: 'Public',
[TUIChatEngine.TYPES.GRP_MEETING]: 'Meeting',
[TUIChatEngine.TYPES.GRP_COMMUNITY]: 'Community',
};
const otherContentForSow = computed((): string => {
let content = '';
if (
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
|| (props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME
) {
content = (props.item as FriendApplication)?.wording || '';
} else if ((props.item as IGroupModel)?.groupID) {
content = `ID:${(props.item as IGroupModel)?.groupID}`;
}
return content;
});
const groupTypeForShow = computed((): string => {
let type = '';
if ((props.item as IGroupModel)?.groupID) {
type = groupType[(props.item as IGroupModel)?.type];
}
return type;
});
const showApplicationStatus = computed(() => {
if (
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME
) {
return {
style: 'text',
label: '等待验证',
};
} else if (
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
) {
return {
style: 'button',
label: '同意',
onClick: () => {
acceptFriendApplication((props.item as FriendApplication)?.userID);
},
};
}
return false;
});
watch(
() => userOnlineStatusMap?.value,
() => {
isOnline.value = getOnlineStatus();
},
{
immediate: true,
deep: true,
},
);
function getOnlineStatus(): boolean {
return !!(
props.displayOnlineStatus
&& userOnlineStatusMap?.value
&& (props.item as Friend)?.userID
&& userOnlineStatusMap.value?.[(props.item as Friend).userID]?.statusType === TUIChatEngine.TYPES.USER_STATUS_ONLINE
);
}
</script>
<style lang="scss" scoped>
.tui-contact-list-card {
padding: 5px 0;
display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
user-select: none;
overflow: hidden;
flex: 1;
&-left {
position: relative;
width: 36px;
height: 36px;
&-avatar {
width: 36px;
height: 36px;
border-radius: 5px;
}
.online-status {
box-sizing: border-box;
position: absolute;
width: 10px;
height: 10px;
left: 30px;
top: 30px;
border: 2px solid #fff;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
border-radius: 50%;
&-online {
background: #29cc85;
}
&-offline {
background: #a4a4a4;
}
}
}
&-main {
flex: 1;
padding: 0 10px;
overflow: hidden;
&-name,
&-other {
font-size: 14px;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-other {
color: #999;
}
}
&-right {
width: fit-content;
&-group-type {
padding: 0 4px;
line-height: 14px;
font-size: 12px;
border-radius: 1px;
font-weight: 400;
color: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(0, 0, 0, 0.3);
}
&-application {
&-text {
color: #999;
font-size: 12px;
}
&-button {
border: 1px solid #006eff;
background: #006eff;
color: #fff;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
text-align: center;
cursor: pointer;
user-select: none;
line-height: 150%;
&::after {
border: none;
}
}
}
}
}
.tui-contact-list-card-h5 {
cursor: none !important;
}
</style>

View File

@@ -0,0 +1,3 @@
import ContactList from './index.vue';
export default ContactList;

View File

@@ -0,0 +1,365 @@
<template>
<ul
v-if="!contactSearchingStatus"
:class="['tui-contact-list', !isPC && 'tui-contact-list-h5']"
>
<li
v-for="(contactListObj, key) in contactListMap"
:key="key"
class="tui-contact-list-item"
>
<header
class="tui-contact-list-item-header"
@click="toggleCurrentContactList(key)"
>
<div class="tui-contact-list-item-header-left">
<Icon
:file="currentContactListKey === key ? downSVG : rightSVG"
width="16px"
height="16px"
/>
<div>{{ TUITranslateService.t(`TUIContact.${contactListObj.title}`) }}</div>
</div>
<div class="tui-contact-list-item-header-right">
<span
v-if="contactListObj.unreadCount"
class="tui-contact-list-item-header-right-unread"
>
{{ contactListObj.unreadCount }}
</span>
</div>
</header>
<ul :class="['tui-contact-list-item-main', currentContactListKey === key ? '' : 'hidden']">
<li
v-for="contactListItem in contactListObj.list"
:key="contactListItem.renderKey"
class="tui-contact-list-item-main-item"
:class="['selected']"
@click="selectItem(contactListItem)"
>
<ContactListItem
:key="contactListItem.renderKey"
:item="deepCopy(contactListItem)"
:displayOnlineStatus="displayOnlineStatus && key === 'friendList'"
/>
</li>
</ul>
</li>
</ul>
<ul
v-else
class="tui-contact-list"
>
<li
v-for="(item, key) in contactSearchResult"
:key="key"
class="tui-contact-list-item"
>
<div
v-if="item.list[0]"
class="tui-contact-search-list"
>
<div class="tui-contact-search-list-title">
{{ TUITranslateService.t(`TUIContact.${item.label}`) }}
</div>
<div
v-for="(listItem, index) in item.list"
:key="index"
class="tui-contact-search-list-item"
:class="['selected']"
@click="selectItem(listItem)"
>
<ContactListItem
:item="listItem"
:displayOnlineStatus="false"
/>
</div>
</div>
</li>
<div
v-if="isContactSearchNoResult"
class="tui-contact-search-list-default"
>
{{ TUITranslateService.t("TUIContact.无搜索结果") }}
</div>
</ul>
</template>
<script setup lang="ts">
import {
TUITranslateService,
TUIStore,
StoreName,
IGroupModel,
TUIFriendService,
Friend,
FriendApplication,
TUIUserService,
} from '@tencentcloud/chat-uikit-engine';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { ref, computed, onMounted, onUnmounted, provide } from '../../../adapter-vue';
import Icon from '../../common/Icon.vue';
import downSVG from '../../../assets/icon/down-icon.svg';
import rightSVG from '../../../assets/icon/right-icon.svg';
import {
IContactList,
IContactSearchResult,
IBlackListUserItem,
IUserStatus,
IUserStatusMap,
IContactInfoType,
} from '../../../interface';
import ContactListItem from './contact-list-item/index.vue';
import { deepCopy } from '../../TUIChat/utils/utils';
import { isPC } from '../../../utils/env';
const currentContactListKey = ref<keyof IContactList>('');
const currentContactInfo = ref<IContactInfoType>({} as IContactInfoType);
const contactListMap = ref<IContactList>({
friendApplicationList: {
key: 'friendApplicationList',
title: '新的联系人',
list: [] as FriendApplication[],
unreadCount: 0,
},
blackList: {
key: 'blackList',
title: '黑名单',
list: [] as IBlackListUserItem[],
},
groupList: {
key: 'groupList',
title: '我的群聊',
list: [] as IGroupModel[],
},
friendList: {
key: 'friendList',
title: '我的好友',
list: [] as Friend[],
},
});
const contactSearchingStatus = ref<boolean>(false);
const contactSearchResult = ref<IContactSearchResult>();
const displayOnlineStatus = ref<boolean>(false);
const userOnlineStatusMap = ref<IUserStatusMap>();
const isContactSearchNoResult = computed((): boolean => {
return (
!contactSearchResult?.value?.user?.list[0]
&& !contactSearchResult?.value?.group?.list[0]
);
});
onMounted(() => {
TUIStore.watch(StoreName.APP, {
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated,
});
TUIStore.watch(StoreName.GRP, {
groupList: onGroupListUpdated,
});
TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated,
});
TUIStore.watch(StoreName.FRIEND, {
friendList: onFriendListUpdated,
friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated,
});
TUIStore.watch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.APP, {
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated,
});
TUIStore.unwatch(StoreName.GRP, {
groupList: onGroupListUpdated,
});
TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated,
});
TUIStore.unwatch(StoreName.FRIEND, {
friendList: onFriendListUpdated,
friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated,
});
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated,
});
});
function toggleCurrentContactList(key: keyof IContactList) {
if (currentContactListKey.value === key) {
currentContactListKey.value = '';
currentContactInfo.value = {} as IContactInfoType;
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', '');
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', {} as IContactInfoType);
} else {
currentContactListKey.value = key;
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', key);
if (key === 'friendApplicationList') {
TUIFriendService.setFriendApplicationRead();
}
}
}
function selectItem(item: any) {
currentContactInfo.value = item;
// For a result in the search list, before viewing the contactInfo details,
// it is necessary to update the data for the "already in the group list/already in the friend list" situation to obtain more detailed information
if (contactSearchingStatus.value) {
let targetListItem;
if ((currentContactInfo.value as Friend)?.userID) {
targetListItem = contactListMap.value?.friendList?.list?.find(
(item: IContactInfoType) => (item as Friend)?.userID === (currentContactInfo.value as Friend)?.userID,
);
} else if ((currentContactInfo.value as IGroupModel)?.groupID) {
targetListItem = contactListMap.value?.groupList?.list?.find(
(item: IContactInfoType) => (item as IGroupModel)?.groupID === (currentContactInfo.value as IGroupModel)?.groupID,
);
}
if (targetListItem) {
currentContactInfo.value = targetListItem;
}
}
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', currentContactInfo.value);
}
function onDisplayOnlineStatusUpdated(status: boolean) {
displayOnlineStatus.value = status;
}
function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
if (list?.size > 0) {
userOnlineStatusMap.value = Object.fromEntries(list?.entries());
}
}
function onCustomerServiceCommercialPluginUpdated(isEnabled: boolean) {
if (!isEnabled) {
return;
}
// After the customer purchases the customer service plug-in,
// the engine updates the enabledCustomerServicePlugin to true through the commercial capability bit.
const contactListExtensionID = TUIConstants.TUIContact.EXTENSION.CONTACT_LIST.EXT_ID;
const tuiContactExtensionList = TUICore.getExtensionList(contactListExtensionID);
const customerData = tuiContactExtensionList.find((extension: any) => {
const { name, accountList = [] } = extension.data || {};
return name === 'customer' && accountList.length > 0;
});
if (customerData) {
const { data, text } = customerData;
const { accountList } = (data || {}) as { accountList: string[] };
TUIUserService.getUserProfile({ userIDList: accountList })
.then((res) => {
if (res.data.length > 0) {
const customerList = {
title: text,
list: res.data.map((item: any, index: number) => {
return {
...item,
renderKey: generateRenderKey('customerList', item, index),
infoKeyList: [],
btnKeyList: ['enterC2CConversation'],
};
}),
key: 'customerList',
};
contactListMap.value = { ...contactListMap.value, customerList };
}
})
.catch(() => { });
}
}
function onGroupListUpdated(groupList: IGroupModel[]) {
updateContactListMap('groupList', groupList);
}
function onUserBlacklistUpdated(userBlacklist: IBlackListUserItem[]) {
updateContactListMap('blackList', userBlacklist);
}
function onFriendApplicationUnreadCountUpdated(friendApplicationUnreadCount: number) {
contactListMap.value.friendApplicationList.unreadCount = friendApplicationUnreadCount;
}
function onFriendListUpdated(friendList: Friend[]) {
updateContactListMap('friendList', friendList);
}
function onFriendApplicationListUpdated(friendApplicationList: FriendApplication[]) {
updateContactListMap('friendApplicationList', friendApplicationList);
}
function updateContactListMap(key: keyof IContactList, list: IContactInfoType[]) {
contactListMap.value[key].list = list;
contactListMap.value[key].list.map((item: IContactInfoType, index: number) => item.renderKey = generateRenderKey(key, item, index));
updateCurrentContactInfoFromList(contactListMap.value[key].list, key);
}
function updateCurrentContactInfoFromList(list: IContactInfoType[], type: keyof IContactList) {
if (
!(currentContactInfo.value as Friend)?.userID
&& !(currentContactInfo.value as IGroupModel)?.groupID
) {
return;
}
if (type === currentContactListKey.value || contactSearchingStatus.value) {
currentContactInfo.value = list?.find(
(item: any) =>
(item?.groupID && item?.groupID === (currentContactInfo.value as IGroupModel)?.groupID) || (item?.userID && item?.userID === (currentContactInfo.value as Friend)?.userID),
) || {} as IContactInfoType;
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', currentContactInfo.value);
}
}
function generateRenderKey(contactListMapKey: keyof IContactList, contactInfo: IContactInfoType, index: number) {
return `${contactListMapKey}-${(contactInfo as Friend).userID || (contactInfo as IGroupModel).groupID || ('index' + index)}`;
}
function onCurrentContactSearchResultUpdated(searchResult: IContactSearchResult) {
contactSearchResult.value = searchResult;
}
function onCurrentContactSearchingStatusUpdated(searchingStatus: boolean) {
contactSearchingStatus.value = searchingStatus;
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', {} as IContactInfoType);
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', '');
}
function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) {
currentContactInfo.value = contactInfo;
}
function onCurrentContactListKeyUpdated(contactListKey: string) {
currentContactListKey.value = contactListKey;
}
provide('userOnlineStatusMap', userOnlineStatusMap);
</script>
<style lang="scss" scoped src="./style/index.scss"></style>

View File

@@ -0,0 +1,12 @@
.tui-contact-list-h5 {
.tui-contact-list-item {
.tui-contact-list-item-header {
cursor: none;
}
.tui-contact-list-item-header:active,
.tui-contact-list-item-main-item:active {
background-color: #eef0f3;
}
}
}

View File

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

View File

@@ -0,0 +1,85 @@
.tui-contact-list {
flex: 1;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow-y: auto;
list-style: none;
&-item {
display: flex;
flex-direction: column;
&-header {
display: flex;
flex-direction: row;
font-size: 14px;
cursor: pointer;
user-select: none;
padding: 10px 15px;
justify-content: space-between;
&-left {
display: flex;
flex-direction: row;
align-items: center;
}
&-right {
display: flex;
justify-content: center;
align-items: center;
&-unread {
display: flex;
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;
background: red;
align-items: center;
justify-content: center;
color: #fff;
}
}
}
&-main {
padding: 0 15px !important;
&.hidden{
display: none;
}
&-item {
padding: 5px 0;
}
}
}
}
.tui-contact-search-list {
padding: 0 15px !important;
&-title {
font-size: 14px;
color: #999;
border-bottom: 1px solid #f4f5f9;
}
&-item {
padding: 5px 0;
}
&-default {
padding: 20px;
text-align: center;
font-size: 14px;
color: #999;
}
}