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,201 @@
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import {
CONTACT_INFO_LABEL_POSITION,
CONTACT_INFO_MORE_EDIT_TYPE,
CONTACT_INFO_BUTTON_TYPE,
} from '../../../constant';
import {
updateFriendRemark,
deleteFriend,
enterConversation,
quitGroup,
dismissGroup,
joinGroup,
addFriend,
acceptFriendApplication,
refuseFriendApplication,
addToBlacklist,
removeFromBlacklist,
} from '../utils/index';
export const contactMoreInfoConfig = {
// set friends' remark
setRemark: {
key: 'setRemark',
label: '备注名',
data: '',
labelPosition: CONTACT_INFO_LABEL_POSITION.LEFT,
editable: true,
editType: CONTACT_INFO_MORE_EDIT_TYPE.INPUT,
editing: false,
editSubmitHandler: (props: {
item: any;
contactInfoData: any;
[propsName: string]: any;
}) => {
if (props?.isBothFriend) {
const newRemarkValue = props?.item?.data;
updateFriendRemark(props?.contactInfoData?.userID, newRemarkValue);
props?.item?.editing && (props.item.editing = false);
props?.item?.data && (props.item.data = props?.contactInfoData?.remark);
} else {
props?.item?.editing && (props.item.editing = false);
}
},
},
// blocked list
blackList: {
key: 'blackList',
label: '加入黑名单',
data: false,
labelPosition: CONTACT_INFO_LABEL_POSITION.LEFT,
editable: true,
editType: CONTACT_INFO_MORE_EDIT_TYPE.SWITCH,
editing: true,
editSubmitHandler: (props: {
item: any;
contactInfoData: any;
[propsName: string]: any;
}) => {
if (props?.isInBlackList) {
removeFromBlacklist(props?.contactInfoData?.userID);
} else {
addToBlacklist(props?.contactInfoData?.userID);
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', 'blackList');
}
},
},
// Fill in verification words (applicant)
setWords: {
key: 'setWords',
label: '请填写验证信息',
data: '',
labelPosition: CONTACT_INFO_LABEL_POSITION.TOP,
editable: true,
editType: CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA,
editing: true,
},
// Display verification words (application recipient)
displayWords: {
key: 'displayWords',
label: '验证信息',
data: '',
labelPosition: CONTACT_INFO_LABEL_POSITION.LEFT,
editable: false,
},
};
export const contactButtonConfig = {
// ---------------------
// group command config
// ---------------------
dismissGroup: {
key: 'dismissGroup',
label: '解散群聊',
type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
dismissGroup(props?.contactInfoData?.groupID);
},
},
quitGroup: {
key: 'quitGroup',
label: '退出群聊',
type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
quitGroup(props?.contactInfoData?.groupID);
},
},
joinGroup: {
key: 'joinGroup',
label: '发送申请',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: {
contactInfoData: any;
contactInfoMoreList: any;
[propsName: string]: any;
}) => {
joinGroup(
props?.contactInfoData?.groupID,
props?.contactInfoMoreList[0]?.data,
);
},
},
joinAVChatGroup: {
key: 'joinAVChatGroup',
label: '加入直播群',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: {
contactInfoData: any;
contactInfoMoreList: any;
[propsName: string]: any;
}) => {
joinGroup(props?.contactInfoData?.groupID);
},
},
enterGroupConversation: {
key: 'enterGroupConversation',
label: '进入群聊',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
enterConversation(props?.contactInfoData);
},
},
// ---------------------
// friend command config
// ---------------------
addFriend: {
key: 'addFriend',
label: '发送申请',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: {
contactInfoData: any;
contactInfoMoreList: any;
[propsName: string]: any;
}) => {
addFriend({
to: props?.contactInfoData?.userID,
source: 'AddSource_Type_Web',
remark: props?.contactInfoMoreList[1]?.data,
wording: props?.contactInfoMoreList[0]?.data,
});
},
},
deleteFriend: {
key: 'deleteFriend',
label: '删除好友',
type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
deleteFriend(props?.contactInfoData?.userID);
},
},
enterC2CConversation: {
key: 'enterC2CConversation',
label: '发送消息',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
enterConversation(props?.contactInfoData);
},
},
// ---------------------
// friend application command config
// ---------------------
acceptFriendApplication: {
key: 'acceptFriendApplication',
label: '同意',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
acceptFriendApplication(props?.contactInfoData?.userID);
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', 'friendList');
},
},
refuseFriendApplication: {
key: 'refuseFriendApplication',
label: '拒绝',
type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
refuseFriendApplication(props?.contactInfoData?.userID);
},
},
};

View File

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

View File

@@ -0,0 +1,429 @@
<template>
<div
v-if="typeof contactInfoData === 'object' && Object.keys(contactInfoData).length"
:class="['tui-contact-info', !isPC && 'tui-contact-info-h5']"
>
<div
v-if="!isPC"
:class="[
'tui-contact-info-header',
!isPC && 'tui-contact-info-h5-header',
]"
>
<div
:class="[
'tui-contact-info-header-icon',
!isPC && 'tui-contact-info-h5-header-icon',
]"
@click="resetContactSearchingUIData"
>
<Icon :file="backSVG" />
</div>
<div
:class="[
'tui-contact-info-header-title',
!isPC && 'tui-contact-info-h5-header-title',
]"
>
{{ TUITranslateService.t("TUIContact.添加好友/群聊") }}
</div>
</div>
<div :class="['tui-contact-info-basic', !isPC && 'tui-contact-info-h5-basic']">
<div
:class="[
'tui-contact-info-basic-text',
!isPC && 'tui-contact-info-h5-basic-text',
]"
>
<div
:class="[
'tui-contact-info-basic-text-name',
!isPC && 'tui-contact-info-h5-basic-text-name',
]"
>
{{ generateContactInfoName(contactInfoData) }}
</div>
<div
v-for="item in contactInfoBasicList"
:key="item.label"
:class="[
'tui-contact-info-basic-text-other',
!isPC && 'tui-contact-info-h5-basic-text-other',
]"
>
{{
`${TUITranslateService.t(`TUIContact.${item.label}`)}:
${item.data}`
}}
</div>
</div>
<img
:class="[
'tui-contact-info-basic-avatar',
!isPC && 'tui-contact-info-h5-basic-avatar',
]"
:src="generateAvatar(contactInfoData)"
>
</div>
<div
v-if="contactInfoMoreList[0]"
:class="['tui-contact-info-more', !isPC && 'tui-contact-info-h5-more']"
>
<div
v-for="item in contactInfoMoreList"
:key="item.key"
:class="[
'tui-contact-info-more-item',
!isPC && 'tui-contact-info-h5-more-item',
item.labelPosition === CONTACT_INFO_LABEL_POSITION.TOP
? 'tui-contact-info-more-item-top'
: 'tui-contact-info-more-item-left',
]"
>
<div
:class="[
'tui-contact-info-more-item-label',
!isPC && 'tui-contact-info-h5-more-item-label',
]"
>
{{ `${TUITranslateService.t(`TUIContact.${item.label}`)}` }}
</div>
<div
:class="[
'tui-contact-info-more-item-content',
!isPC && 'tui-contact-info-h5-more-item-content',
]"
>
<div
v-if="!item.editing"
:class="[
'tui-contact-info-more-item-content-text',
!isPC && 'tui-contact-info-h5-more-item-content-text',
]"
>
<div
:class="[
'tui-contact-info-more-item-content-text-data',
!isPC && 'tui-contact-info-h5-more-item-content-text-data',
]"
>
{{ item.data }}
</div>
<div
v-if="item.editable"
:class="[
'tui-contact-info-more-item-content-text-icon',
!isPC && 'tui-contact-info-h5-more-item-content-text-icon',
]"
@click="setEditing(item)"
>
<Icon
:file="editSVG"
width="14px"
height="14px"
/>
</div>
</div>
<input
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.INPUT"
v-model="item.data"
:class="[
'tui-contact-info-more-item-content-input',
!isPC && 'tui-contact-info-h5-more-item-content-input',
]"
type="text"
@confirm="onContactInfoEmitSubmit(item)"
@keyup.enter="onContactInfoEmitSubmit(item)"
>
<textarea
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA"
v-model="item.data"
:class="[
'tui-contact-info-more-item-content-textarea',
!isPC && 'tui-contact-info-h5-more-item-content-textarea',
]"
confirm-type="done"
/>
<div
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.SWITCH"
@click="onContactInfoEmitSubmit(item)"
>
<SwitchBar :value="item.data" />
</div>
</div>
</div>
</div>
<div
:class="[
'tui-contact-info-button',
!isPC && 'tui-contact-info-h5-button',
]"
>
<button
v-for="item in contactInfoButtonList"
:key="item.key"
:class="[
'tui-contact-info-button-item',
!isPC && 'tui-contact-info-h5-button-item',
item.type === CONTACT_INFO_BUTTON_TYPE.CANCEL
? `tui-contact-info-button-item-cancel`
: `tui-contact-info-button-item-submit`,
]"
@click="onContactInfoButtonClicked(item)"
>
{{ TUITranslateService.t(`TUIContact.${item.label}`) }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import TUIChatEngine, {
TUIStore,
StoreName,
TUITranslateService,
IGroupModel,
Friend,
FriendApplication,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, computed, onMounted, onUnmounted } from '../../../adapter-vue';
import { isPC } from '../../../utils/env';
import {
generateAvatar,
generateContactInfoName,
generateContactInfoBasic,
isFriend,
isApplicationType,
} from '../utils/index';
import {
contactMoreInfoConfig,
contactButtonConfig,
} from './contact-info-config';
import Icon from '../../common/Icon.vue';
import editSVG from '../../../assets/icon/edit.svg';
import backSVG from '../../../assets/icon/back.svg';
import SwitchBar from '../../common/SwitchBar/index.vue';
import {
IBlackListUserItem,
IContactInfoMoreItem,
IContactInfoButton,
} from '../../../interface';
import {
CONTACT_INFO_LABEL_POSITION,
CONTACT_INFO_MORE_EDIT_TYPE,
CONTACT_INFO_BUTTON_TYPE,
} from '../../../constant';
import { deepCopy } from '../../TUIChat/utils/utils';
type IContactInfoType = IGroupModel | Friend | FriendApplication | IBlackListUserItem;
const emits = defineEmits(['switchConversation']);
const contactInfoData = ref<IContactInfoType>({} as IContactInfoType);
const contactInfoBasicList = ref<Array<{ label: string; data: string }>>([]);
const contactInfoMoreList = ref<IContactInfoMoreItem[]>([]);
const contactInfoButtonList = ref<IContactInfoButton[]>([]);
const setEditing = (item: any) => {
item.editing = true;
};
const isGroup = computed((): boolean =>
(contactInfoData.value as IGroupModel)?.groupID ? true : false,
);
const isApplication = computed((): boolean => {
return isApplicationType(contactInfoData?.value);
});
// is both friend, if is group type always false
const isBothFriend = ref<boolean>(false);
// is group member, including ordinary member, admin, group owner
const isGroupMember = computed((): boolean => {
return (contactInfoData.value as IGroupModel)?.selfInfo?.userID ? true : false;
});
// is in black list, if is group type always false
const isInBlackList = computed((): boolean => {
return (
!isGroup.value
&& blackList.value?.findIndex(
(item: IBlackListUserItem) =>
item?.userID === (contactInfoData.value as IBlackListUserItem)?.userID,
) >= 0
);
});
const blackList = ref<IBlackListUserItem[]>([]);
onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated,
});
TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated,
});
TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
});
});
const resetContactInfoUIData = () => {
contactInfoData.value = {} as IContactInfoType;
contactInfoBasicList.value = [];
contactInfoMoreList.value = [];
contactInfoButtonList.value = [];
};
const resetContactSearchingUIData = () => {
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', {});
TUIStore.update(StoreName.CUSTOM, 'currentContactSearchingStatus', false);
TUIGlobal?.closeSearching && TUIGlobal?.closeSearching();
};
const onContactInfoEmitSubmit = (item: any) => {
item.editSubmitHandler
&& item.editSubmitHandler({
item,
contactInfoData: contactInfoData.value,
isBothFriend: isBothFriend.value,
isInBlackList: isInBlackList.value,
});
};
const onContactInfoButtonClicked = (item: any) => {
item.onClick
&& item.onClick({
contactInfoData: contactInfoData.value,
contactInfoMoreList: contactInfoMoreList.value,
});
if (
item.key === 'enterGroupConversation'
|| item.key === 'enterC2CConversation'
) {
emits('switchConversation', contactInfoData.value);
resetContactSearchingUIData();
}
};
const generateMoreInfo = async () => {
if (!isApplication.value) {
if (
(!isGroup.value && !isBothFriend.value && !isInBlackList.value)
|| (isGroup.value
&& !isGroupMember.value
&& (contactInfoData.value as IGroupModel)?.type !== TUIChatEngine?.TYPES?.GRP_AVCHATROOM)
) {
contactMoreInfoConfig.setWords.data = '';
contactInfoMoreList.value.push(contactMoreInfoConfig.setWords);
}
if (!isGroup.value && !isInBlackList.value) {
contactMoreInfoConfig.setRemark.data
= (contactInfoData.value as Friend)?.remark || '';
contactMoreInfoConfig.setRemark.editing = false;
contactInfoMoreList.value.push(contactMoreInfoConfig.setRemark);
}
if (!isGroup.value && (isBothFriend.value || isInBlackList.value)) {
contactMoreInfoConfig.blackList.data = isInBlackList.value || false;
contactInfoMoreList.value.push(contactMoreInfoConfig.blackList);
}
} else {
contactMoreInfoConfig.displayWords.data
= (contactInfoData.value as FriendApplication)?.wording || '';
contactInfoMoreList.value.push(contactMoreInfoConfig.displayWords);
}
};
const generateButton = () => {
if (isInBlackList.value) {
return;
}
if (isApplication.value) {
if (
(contactInfoData.value as FriendApplication)?.type
=== TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
) {
contactInfoButtonList?.value?.push(
contactButtonConfig.refuseFriendApplication,
);
contactInfoButtonList?.value?.push(
contactButtonConfig.acceptFriendApplication,
);
}
} else {
if (isGroup.value && isGroupMember.value) {
switch ((contactInfoData.value as IGroupModel)?.selfInfo?.role) {
case 'Owner':
contactInfoButtonList?.value?.push(contactButtonConfig.dismissGroup);
break;
default:
contactInfoButtonList?.value?.push(contactButtonConfig.quitGroup);
break;
}
contactInfoButtonList?.value?.push(
contactButtonConfig.enterGroupConversation,
);
} else if (!isGroup.value && isBothFriend.value) {
contactInfoButtonList?.value?.push(contactButtonConfig.deleteFriend);
contactInfoButtonList?.value?.push(
contactButtonConfig.enterC2CConversation,
);
} else {
if (isGroup.value) {
contactInfoButtonList?.value?.push(
(contactInfoData.value as IGroupModel)?.type === TUIChatEngine?.TYPES?.GRP_AVCHATROOM
? contactButtonConfig.joinAVChatGroup
: contactButtonConfig.joinGroup,
);
} else {
contactInfoButtonList?.value?.push(contactButtonConfig.addFriend);
}
}
}
};
function onUserBlacklistUpdated(userBlacklist: IBlackListUserItem[]) {
blackList.value = userBlacklist;
}
async function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) {
if (
contactInfoData.value
&& contactInfo
&& JSON.stringify(contactInfoData.value) === JSON.stringify(contactInfo)
) {
return;
}
resetContactInfoUIData();
// deep clone
contactInfoData.value = deepCopy(contactInfo) || {};
if (!contactInfoData.value || Object.keys(contactInfoData.value)?.length === 0) {
return;
}
contactInfoBasicList.value = generateContactInfoBasic(
contactInfoData.value,
);
isBothFriend.value = await isFriend(contactInfoData.value);
generateMoreInfo();
generateButton();
if (contactInfo.infoKeyList) {
contactInfoMoreList.value = contactInfo.infoKeyList.map((key: string) => {
return (contactMoreInfoConfig as any)[key];
});
}
if (contactInfo.btnKeyList) {
contactInfoButtonList.value = contactInfo.btnKeyList.map((key: string) => {
return (contactButtonConfig as any)[key];
});
}
}
</script>
<style lang="scss" scoped src="./style/index.scss"></style>

View File

@@ -0,0 +1,130 @@
.tui-contact-info-h5 {
padding: 0;
overflow: hidden;
&-header {
background-color: #fff;
padding: 10px !important;
display: flex;
flex-direction: row;
&-title {
flex: 1;
text-align: center;
font-weight: 500;
font-size: 14px;
margin-right: 30px;
}
}
&-basic {
padding: 10px !important;
background: #fff;
margin-top: 10px !important;
display: flex;
flex-direction: row-reverse;
justify-content: left;
border-bottom: none;
&-text {
&-name {
font-size: 20px;
padding-bottom: 1px;
}
&-other {
font-size: 14px;
padding: 3px 0;
}
}
&-avatar {
border-radius: 10px;
margin-right: 10px;
}
}
&-more {
background: #fff;
margin-top: 10px !important;
overflow: hidden;
padding: 0;
&-item {
width: 100%;
box-sizing: border-box;
overflow: hidden;
padding: 10px !important;
border-bottom: 1px solid #eee;
&-label {
color: #000;
}
&-content {
overflow: hidden;
color: #979797;
display: flex;
flex-direction: row;
justify-content: flex-end;
&-text {
overflow: hidden;
display: flex;
flex-direction: row;
&-data {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
&-item:last-child {
border-bottom: none;
}
}
&-button {
margin-top: 10px !important;
display: flex;
flex-direction: column;
padding: 0;
background-color: #fff;
&-item {
width: 100%;
margin: 0;
border: none;
padding: 16px !important;
font-size: 16px;
border-bottom: 1px solid #eee;
height: fit-content;
&::after {
border: none;
}
&-textarea {
background-color: #f8f8f8;
}
}
&-item:last-child {
border-bottom: none;
}
.tui-contact-info-button-item-cancel {
background-color: #fff;
color: #e54545;
}
.tui-contact-info-button-item-submit {
background-color: #fff;
color: #006eff;
}
}
}

View File

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

View File

@@ -0,0 +1,151 @@
.tui-contact-info {
width: 100%;
height: 100%;
background: #f7f8fa;
display: flex;
padding: 30px;
box-sizing: border-box;
flex-direction: column;
overflow: hidden;
&-basic {
display: flex;
justify-content: space-between;
padding-bottom: 15px;
border-bottom: 1px solid #ddd;
overflow: hidden;
box-sizing: border-box;
width: 100%;
&-text {
flex: 1;
&-name {
font-size: 24px;
padding-bottom: 10px;
}
&-other {
font-size: 16px;
padding: 6px 0;
font-weight: 400;
color: #999;
}
}
&-avatar {
width: 80px;
height: 80px;
}
}
&-more {
padding: 15px 0;
overflow: hidden;
&-item {
display: flex;
padding: 6px 0;
font-size: 16px;
font-weight: 400;
min-height: 56px;
&-label {
color: #999;
height: fit-content;
}
&-left {
flex-direction: row;
align-items: center;
.tui-contact-info-more-item-label {
width: 80px;
}
}
&-top {
flex-direction: column;
}
&-content {
flex: 1;
display: flex;
flex-direction: row;
color: #333;
overflow: hidden;
&-text {
display: flex;
overflow: hidden;
&-data {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&-icon {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
}
&-input,
&-textarea {
flex: 1;
border: 1px solid #e8e8e9;
border-radius: 4px;
padding: 4px;
color: inherit;
}
&-input {
height: 22px;
font-size: 16px;
}
&-textarea {
resize: none;
height: 100px;
}
}
}
}
&-button {
display: flex;
padding: 30px;
justify-content: center;
&-item {
margin: 15px;
min-width: 142px;
height: 36px;
padding: 8px 20px;
border-radius: 4px;
border: none;
font-size: 14px;
text-align: center;
line-height: 20px;
font-weight: 400;
letter-spacing: 0;
cursor: pointer;
user-select: none;
&-submit {
border: 1px solid #006eff;
background: #006eff;
color: #fff;
}
&-cancel {
border: 1px solid #e54545;
background: transparent;
color: #e54545;
}
}
}
}

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;
}
}

View File

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

View File

@@ -0,0 +1,250 @@
<template>
<div :class="['tui-contact-search', !isPC && 'tui-contact-search-h5']">
<div
v-if="!isSearching || !isPC"
:class="[
'tui-contact-search-header',
!isPC && 'tui-contact-search-h5-header',
isSearching && 'tui-contact-searching-h5-header',
]"
@click="changeContactSearchingStatus(true)"
>
<div
:class="[
'tui-contact-search-header-icon',
!isPC && 'tui-contact-search-h5-header-icon',
]"
@click.stop="changeContactSearchingStatus(!isSearching)"
>
<Icon
:file="isSearching ? backSVG : addSVG"
:width="isSearching ? '20px' : '14px'"
:height="isSearching ? '20px' : '14px'"
/>
</div>
<div
:class="[
'tui-contact-search-header-title',
!isPC && 'tui-contact-search-h5-header-title',
]"
>
{{ TUITranslateService.t("TUIContact.添加好友/群聊") }}
</div>
</div>
<div
v-if="isSearching"
:class="[
'tui-contact-search-main',
!isPC && 'tui-contact-search-h5-main',
]"
>
<input
v-model="searchValue"
class="tui-contact-search-main-input"
type="text"
:placeholder="searchingPlaceholder"
enterkeyhint="search"
@keyup.enter="search"
@blur="search"
@confirm="search"
>
<div
class="tui-contact-search-main-cancel"
@click="isSearching = false"
>
{{ TUITranslateService.t("取消") }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from '../../../adapter-vue';
import {
TUITranslateService,
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { TUIGlobal } from '@tencentcloud/universal-api';
import Icon from '../../common/Icon.vue';
import addSVG from '../../../assets/icon/add.svg';
import backSVG from '../../../assets/icon/back.svg';
import { isPC } from '../../../utils/env';
import { debounce } from '../../../utils/lodash';
import { IContactSearchResult } from '../../../interface';
const searchingPlaceholder = TUITranslateService.t('TUIContact.输入ID');
const isSearching = ref<boolean>(false);
const searchValue = ref<string>('');
const searchResult = ref<IContactSearchResult>({
user: {
label: '联系人',
list: [],
},
group: {
label: '群聊',
list: [],
},
});
const changeContactSearchingStatus = debounce(function (status: boolean) {
isSearching.value = status;
}, 200);
const search = async () => {
if (!searchValue.value) {
return;
}
TUICore.callService({
serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_USER,
params: {
userID: searchValue.value,
},
})
.then((res: any) => {
searchResult.value.user.list = res.data;
})
.catch((error: any) => {
searchResult.value.user.list = [];
console.warn('search user error', error);
});
TUICore.callService({
serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_GROUP,
params: {
groupID: searchValue.value,
},
})
.then((res: any) => {
searchResult.value.group.list = [res.data.group];
})
.catch((error: any) => {
searchResult.value.group.list = [];
console.warn('search group error', error);
});
};
watch(
() => searchResult.value,
() => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchResult',
searchResult.value,
);
},
{
deep: true,
immediate: true,
},
);
watch(
() => isSearching.value,
() => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchingStatus',
isSearching.value,
);
if (isSearching.value) {
searchValue.value = '';
searchResult.value.user.list = [];
searchResult.value.group.list = [];
}
},
{
deep: true,
immediate: true,
},
);
TUIGlobal.updateContactSearch = search;
TUIGlobal.closeSearching = () => {
isSearching.value = false;
};
</script>
<style lang="scss" scoped>
.tui-contact-search {
position: sticky;
top: 0;
z-index: 1;
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-bottom: 1px solid #f4f5f9;
flex-direction: column;
&-header,
&-main {
width: 100%;
height: 30px;
display: flex;
flex-direction: row;
align-items: center;
}
&-header {
user-select: none;
cursor: pointer;
&-icon {
padding-right: 10px;
}
&-title {
font-size: 14px;
}
}
&-main {
display: flex;
flex-direction: row;
flex: 1;
justify-content: center;
align-items: center;
width: 100%;
&-input {
flex: 1;
font-size: 14px;
border-radius: 5px;
padding: 7px;
border: 1px solid #ddd;
}
&-input:focus {
outline: none;
border: 1px solid #006eff;
}
&-cancel {
padding-left: 10px;
user-select: none;
cursor: pointer;
}
}
&-h5 {
&-header {
width: 100%;
}
}
}
.tui-contact-searching-h5-header {
padding-bottom: 10px;
display: flex;
flex-direction: row;
.tui-contact-search-h5-header-title {
flex: 1;
text-align: center;
font-weight: 500;
font-size: 14px;
margin-right: 30px;
}
}
</style>

View File

@@ -0,0 +1,5 @@
import TUIContact from './index.vue';
import Server from './server';
Server.getInstance();
export default TUIContact;

View File

@@ -0,0 +1,131 @@
<template>
<SelectFriend v-if="isShowSelectFriend" />
<div
v-else-if="isShowContactList"
:class="['tui-contact', !isPC && 'tui-contact-h5']"
>
<div :class="['tui-contact-left', !isPC && 'tui-contact-h5-left']">
<ContactSearch />
<ContactList :class="['tui-contact-left-list', !isPC && 'tui-contact-h5-left-list']" />
</div>
<div
v-if="isShowContactInfo"
:class="['tui-contact-right', !isPC && 'tui-contact-h5-right']"
>
<ContactInfo @switchConversation="switchConversation" />
</div>
</div>
</template>
<script lang="ts" setup>
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, watchEffect } from '../../adapter-vue';
import { isPC, isUniFrameWork } from '../../utils/env';
import SelectFriend from './select-friend/index.vue';
import ContactSearch from './contact-search/index.vue';
import ContactList from './contact-list/index.vue';
import ContactInfo from './contact-info/index.vue';
const emits = defineEmits(['switchConversation']);
const props = defineProps({
// web/h5 single page application display format, uniapp please ignore
displayType: {
type: String,
default: 'contactList', // "contactList" / "selectFriend"
require: false,
},
});
const displayTypeRef = ref<string>(props.displayType || 'contactList');
const isShowSelectFriend = ref(false);
const isShowContactList = ref(true);
const isShowContactInfo = ref(true);
watchEffect(() => {
isShowContactList.value = props?.displayType !== 'selectFriend';
});
TUIStore.watch(StoreName.CUSTOM, {
isShowSelectFriendComponent: (data: any) => {
if (!isUniFrameWork && props?.displayType === 'selectFriend') {
isShowSelectFriend.value = data;
isShowContactList.value = false;
return;
}
if (data) {
isShowSelectFriend.value = true;
if (isUniFrameWork) {
displayTypeRef.value = 'selectFriend';
TUIGlobal?.hideTabBar();
}
} else {
isShowSelectFriend.value = false;
if (isUniFrameWork) {
displayTypeRef.value = props.displayType;
TUIGlobal?.showTabBar()?.catch(() => { /* ignore */ });
}
}
},
currentContactInfo: (contactInfo: any) => {
isShowContactInfo.value = isPC || (contactInfo && typeof contactInfo === 'object' && Object.keys(contactInfo)?.length > 0);
},
});
const switchConversation = (data: any) => {
isUniFrameWork
&& TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index',
});
emits('switchConversation', data);
};
</script>
<style lang="scss" scoped>
@import "../../assets/styles/common";
.tui-contact {
width: 100%;
height: 100%;
box-sizing: border-box;
display: flex;
overflow: hidden;
&-left {
min-width: 285px;
flex: 0 0 24%;
overflow: hidden;
display: flex;
flex-direction: column;
}
&-right {
border-left: 1px solid #f4f5f9;
flex: 1;
overflow: hidden;
}
}
.tui-contact-h5 {
position: relative;
&-left,
&-right {
width: 100%;
height: 100%;
flex: 1;
}
&-right {
position: absolute;
z-index: 100;
}
&-left {
&-list {
overflow-y: auto;
}
}
}
</style>

View File

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

View File

@@ -0,0 +1,92 @@
<template>
<SelectUser
:isRadio="selectOptions.isRadio"
:isNeedSearch="selectOptions.isNeedSearch"
:title="selectOptions.title"
:userList="userList"
@search="handleSearch"
@complete="handleSelectedResult"
/>
</template>
<script lang="ts" setup>
import {
TUIFriendService,
TUIStore,
StoreName,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { ref, watchEffect } from '../../../adapter-vue';
import { Toast, TOAST_TYPE } from '../../common/Toast/index';
import TUICore from '@tencentcloud/tui-core';
import SelectUser from '../../common/SelectUser/index.vue';
import Server from '../server';
const TUIContactServer = Server.getInstance();
const TUIConstants = TUIContactServer.constants;
const needSearch = ref(false);
const friendList = ref([]);
const userList = ref([]);
const TUISearchServer = ref<any>(null);
const selectOptions = ref({
isRadio: false,
isNeedSearch: false,
title: '',
});
const generateSearchServer = (isNeedSearch: any) => {
TUISearchServer.value = TUICore.getService(TUIConstants.TUISearch.SERVICE.NAME);
if (TUISearchServer.value) {
needSearch.value = isNeedSearch;
} else {
console.warn('请添加 TUISearch 组件');
}
};
watchEffect(() => {
const params = TUIContactServer.getOnCallParams(TUIConstants.TUIContact.SERVICE.METHOD.SELECT_FRIEND);
selectOptions.value.title = params.title;
selectOptions.value.isRadio = params.isRadio;
selectOptions.value.isNeedSearch = params.isNeedSearch;
if (params.isNeedSearch) {
generateSearchServer(params.isNeedSearch);
}
TUIFriendService.getFriendList().then((res: any) => {
friendList.value = res.data.map((item: any) => item.profile);
userList.value = friendList.value;
}).catch((err: any) => {
console.warn('getFriendList error:', err);
});
});
const handleSelectedResult = (memberList: Array<any>) => {
TUIStore.update(StoreName.CUSTOM, 'isShowSelectFriendComponent', false);
const callback = TUIContactServer.getOnCallCallback(TUIConstants.TUIContact.SERVICE.METHOD.SELECT_FRIEND);
callback && callback(memberList);
};
const searchFail = () => {
Toast({
message: TUITranslateService.t('TUIGroup.该用户不存在'),
type: TOAST_TYPE.ERROR,
});
userList.value = [...friendList.value];
};
const handleSearch = async (val: string) => {
if (!val) {
return userList.value = friendList.value;
}
try {
const imResponse: any = await TUISearchServer.value.searchUser(val);
if (!imResponse.data[0]) {
return searchFail();
}
userList.value = imResponse.data;
const searchAllResult = friendList.value.filter((item: any) => item.userID === imResponse.data[0].userID);
friendList.value = searchAllResult.length ? friendList.value : [...friendList.value, ...userList.value];
} catch (error) {
return searchFail();
}
};
</script>

View File

@@ -0,0 +1,43 @@
import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { isUniFrameWork } from '../../utils/env';
export default class TUIContactServer {
static instance: TUIContactServer;
private onCallParamsMap: Map<string, any>;
private onCallCallbackMap: Map<string, () => void>;
public constants: typeof TUIConstants;
constructor() {
TUICore.registerService(TUIConstants.TUIContact.SERVICE.NAME, this);
this.onCallParamsMap = new Map();
this.onCallCallbackMap = new Map();
this.constants = TUIConstants;
}
static getInstance(): TUIContactServer {
if (!TUIContactServer.instance) {
TUIContactServer.instance = new TUIContactServer();
}
return TUIContactServer.instance;
}
public getOnCallParams(method: string): any {
return this.onCallParamsMap.get(method);
}
public getOnCallCallback(method: string) {
return this.onCallCallbackMap.get(method);
}
public async onCall(method: string, params: Record<string, any>, callback: () => void): Promise<void> {
this.onCallParamsMap.set(method, params);
this.onCallCallbackMap.set(method, callback);
if (method === TUIConstants.TUIContact.SERVICE.METHOD.SELECT_FRIEND) {
TUIStore.update(StoreName.CUSTOM, 'isShowSelectFriendComponent', true);
isUniFrameWork && TUIGlobal?.reLaunch({
url: '/TUIKit/components/TUIContact/index',
});
}
}
}

View File

@@ -0,0 +1,359 @@
import TUIChatEngine, {
TUIFriendService,
TUIConversationService,
TUIGroupService,
TUIUserService,
TUITranslateService,
AddFriendParams,
JoinGroupParams,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { Toast, TOAST_TYPE } from '../../common/Toast/index';
export const generateAvatar = (item: any): string => {
return (
item?.avatar
|| item?.profile?.avatar
|| (item?.groupID && 'https://web.sdk.qcloud.com/im/assets/images/Public.svg')
|| 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
);
};
export const generateName = (item: any): string => {
return (
item?.remark
|| item?.name
|| item?.profile?.nick
|| item?.nick
|| item?.groupID
|| item?.userID
|| ''
);
};
export const generateContactInfoName = (item: any): string => {
return (
item?.name
|| item?.profile?.nick
|| item?.nick
|| item?.groupID
|| item?.userID
|| ''
);
};
// Parse the basic information display content of the contactInfo module
// Group information display: group ID group type
// User information display: user ID personal signature
export const generateContactInfoBasic = (
contactInfo: any,
): any[] => {
const res = [
{
label: contactInfo?.groupID ? '群ID' : 'ID',
data: contactInfo?.groupID || contactInfo?.userID || '',
},
];
if (!isApplicationType(contactInfo)) {
res.push({
label: contactInfo?.groupID ? '群类型' : '个性签名',
data: contactInfo?.type || contactInfo?.profile?.selfSignature || '',
});
}
return res;
};
export const isApplicationType = (info: any) => {
return (
info?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
|| info?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME
);
};
export const isFriend = (info: any): Promise<boolean> => {
return new Promise((resolve, reject) => {
if (info?.groupID || !info?.userID) {
resolve(false);
return;
}
if (info?.addTime) {
resolve(true);
return;
}
TUIFriendService.checkFriend({
userIDList: [info?.userID],
type: TUIChatEngine.TYPES.SNS_CHECK_TYPE_BOTH,
})
.then((res: any) => {
switch (res?.data?.successUserIDList[0]?.relation) {
// No friend relationship: A does not have B in his friend list, and B does not have A in his friend list
case TUIChatEngine.TYPES.SNS_TYPE_NO_RELATION:
resolve(false);
break;
// Single-item friend: A has B in his friend list, but B does not have A in his friend list
case TUIChatEngine.TYPES.SNS_TYPE_A_WITH_B:
resolve(false);
break;
// Single-item friend: A does not have B in his friend list, but B has A in his friend list
case TUIChatEngine.TYPES.SNS_TYPE_B_WITH_A:
resolve(false);
break;
// Two-way friendship
case TUIChatEngine.TYPES.SNS_TYPE_BOTH_WAY:
resolve(true);
break;
default:
resolve(false);
break;
}
})
.catch((error: any) => {
console.warn('checkFriend error', error);
reject(error);
});
});
};
// Change friends remark
export const updateFriendRemark = (userID: string, remark: string) => {
// eslint-disable-next-line no-control-regex
if (remark?.replace(/[^\u0000-\u00ff]/g, 'aa')?.length > 96) {
Toast({
message: TUITranslateService.t('TUIContact.修改备注失败: 备注长度不得超过 96 字节'),
type: TOAST_TYPE.ERROR,
});
return;
}
TUIFriendService.updateFriend({
userID,
remark,
})
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.修改备注成功'),
type: TOAST_TYPE.SUCCESS,
});
})
.catch((error: any) => {
console.warn('update friend remark failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.修改备注失败'),
type: TOAST_TYPE.ERROR,
});
});
};
// Delete one friend
export const deleteFriend = (userID: string) => {
TUIFriendService.deleteFriend({
userIDList: [userID],
type: TUIChatEngine.TYPES.SNS_DELETE_TYPE_BOTH,
})
.then((res: any) => {
const { successUserIDList } = res.data;
if (successUserIDList[0].userID === userID) {
Toast({
message: TUITranslateService.t('TUIContact.删除好友成功'),
type: TOAST_TYPE.SUCCESS,
});
} else {
Toast({
message: TUITranslateService.t('TUIContact.删除好友失败'),
type: TOAST_TYPE.ERROR,
});
}
})
.catch((error: any) => {
console.warn('delete friend failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.删除好友失败'),
type: TOAST_TYPE.ERROR,
});
});
};
// Add friend
export const addFriend = (params: AddFriendParams) => {
TUIFriendService.addFriend(params)
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.申请已发送'),
type: TOAST_TYPE.SUCCESS,
});
})
.catch((error: any) => {
console.warn('delete friend failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.申请发送失败'),
type: TOAST_TYPE.ERROR,
});
});
};
// Enter conversation
export const enterConversation = (item: any) => {
const conversationID = item?.groupID
? `GROUP${item?.groupID}`
: `C2C${item?.userID}`;
TUIConversationService.switchConversation(conversationID).catch(
(error: any) => {
console.warn('switch conversation failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.进入会话失败'),
type: TOAST_TYPE.ERROR,
});
},
);
};
// Accept friend application
export const acceptFriendApplication = (userID: string) => {
TUIFriendService.acceptFriendApplication({
userID,
type: TUIChatEngine.TYPES.SNS_APPLICATION_AGREE_AND_ADD,
})
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.添加好友成功'),
type: TOAST_TYPE.SUCCESS,
});
})
.catch((error: any) => {
console.warn('accept friend application failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.同意好友申请失败'),
type: TOAST_TYPE.ERROR,
});
});
};
// Refuse friend application
export const refuseFriendApplication = (userID: string) => {
TUIFriendService.refuseFriendApplication(userID)
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.拒绝成功'),
type: TOAST_TYPE.SUCCESS,
});
})
.catch((error: any) => {
console.warn('accept friend application failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.拒绝好友申请失败'),
type: TOAST_TYPE.ERROR,
});
});
};
// Dismiss group
export const dismissGroup = (groupID: string) => {
TUIGroupService.dismissGroup(groupID)
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.解散群聊成功'),
type: TOAST_TYPE.SUCCESS,
});
TUIGlobal?.updateContactSearch && TUIGlobal?.updateContactSearch();
})
.catch((error: any) => {
console.warn('dismiss group failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.解散群聊失败'),
type: TOAST_TYPE.ERROR,
});
});
};
// Quit group
export const quitGroup = (groupID: string) => {
TUIGroupService.quitGroup(groupID)
.then(() => {
Toast({
message: TUITranslateService.t('TUIContact.退出群组成功'),
type: TOAST_TYPE.SUCCESS,
});
})
.catch((error: any) => {
console.warn('quit group failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.退出群组失败'),
type: TOAST_TYPE.ERROR,
});
});
};
// Join group
export const joinGroup = (groupID: string, applyMessage?: string) => {
TUIGroupService.joinGroup({
groupID,
applyMessage,
} as JoinGroupParams)
.then((imResponse: { data: { status?: string } }) => {
switch (imResponse?.data?.status) {
case TUIChatEngine.TYPES.JOIN_STATUS_WAIT_APPROVAL: // Wait for administrator approval
Toast({
message: TUITranslateService.t('TUIContact.等待管理员同意'),
type: TOAST_TYPE.SUCCESS,
});
break;
case TUIChatEngine.TYPES.JOIN_STATUS_SUCCESS: // Join group successfully
Toast({
message: TUITranslateService.t('TUIContact.加群成功'),
type: TOAST_TYPE.SUCCESS,
});
break;
case TUIChatEngine.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // Already in the group
Toast({
message: TUITranslateService.t('TUIContact.您已是群成员'),
type: TOAST_TYPE.SUCCESS,
});
break;
default:
break;
}
})
.catch((error: any) => {
console.warn('join group failed:', error);
Toast({
message: '申请入群失败',
type: TOAST_TYPE.ERROR,
});
});
};
// Add to blacklist
export const addToBlacklist = (userID: string, successCallBack?: () => void) => {
TUIUserService.addToBlacklist({
userIDList: [userID],
})
.then(() => {
successCallBack && successCallBack();
})
.catch((error: any) => {
console.warn('add to blacklist failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.加入黑名单失败'),
type: TOAST_TYPE.ERROR,
});
});
};
// Remove from Blacklist
export const removeFromBlacklist = (
userID: string,
successCallBack?: () => void,
) => {
TUIUserService.removeFromBlacklist({
userIDList: [userID],
})
.then(() => {
successCallBack && successCallBack();
})
.catch((error: any) => {
console.warn('remove from blacklist failed:', error);
Toast({
message: TUITranslateService.t('TUIContact.移除黑名单失败'),
type: TOAST_TYPE.ERROR,
});
});
};