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