1、腾讯IM模块格式化

This commit is contained in:
cuiyouliang 2025-06-27 09:53:36 +08:00
parent 99dfe74b26
commit d77452dc25
206 changed files with 4258 additions and 7118 deletions

View File

@ -3,10 +3,7 @@ import { TUIGlobal } from '@tencentcloud/universal-api';
let vueVersion: number; let vueVersion: number;
let framework = 'vue2'; let framework = 'vue2';
let createVNode = ( let createVNode = (arg1: any, arg2: any): { component: any; props: any; data: any } => {
arg1: any,
arg2: any,
): { component: any; props: any; data: any } => {
return {} as { component: any; props: any; data: any }; return {} as { component: any; props: any; data: any };
}; };
let render = (arg1: any, arg2: any) => { let render = (arg1: any, arg2: any) => {
@ -14,17 +11,11 @@ let render = (arg1: any, arg2: any) => {
}; };
try { try {
if ( if ((Vue as any)?.default?.version && (Vue as any)?.default?.version?.startsWith('2.7.')) {
(Vue as any)?.default?.version
&& (Vue as any)?.default?.version?.startsWith('2.7.')
) {
// >= Vue 2.7.0 // >= Vue 2.7.0
vueVersion = 2.7; vueVersion = 2.7;
TUIGlobal.Vue = (Vue as any)?.getCurrentInstance()?.appContext?.app; TUIGlobal.Vue = (Vue as any)?.getCurrentInstance()?.appContext?.app;
} else if ( } else if ((Vue as any)?.default?.version && (Vue as any)?.default?.version?.startsWith('2.')) {
(Vue as any)?.default?.version
&& (Vue as any)?.default?.version?.startsWith('2.')
) {
// < Vue 2.7.0 // < Vue 2.7.0
vueVersion = 2; vueVersion = 2;
TUIGlobal.Vue = (Vue as any).default; TUIGlobal.Vue = (Vue as any).default;

View File

@ -8,10 +8,7 @@
<Icon :file="backSVG" /> <Icon :file="backSVG" />
</div> </div>
<div class="chat-header-container"> <div class="chat-header-container">
<div <div v-if="isNotRoomChat" :class="['chat-header-content', !isPC && 'chat-header-h5-content']">
v-if="isNotRoomChat"
:class="['chat-header-content', !isPC && 'chat-header-h5-content']"
>
{{ currentConversationName }} {{ currentConversationName }}
</div> </div>
<div> <div>
@ -19,11 +16,7 @@
</div> </div>
</div> </div>
<div :class="['chat-header-setting', !isPC && 'chat-header-h5-setting']"> <div :class="['chat-header-setting', !isPC && 'chat-header-h5-setting']">
<div <div v-for="(item, index) in props.headerExtensionList" :key="index" @click.stop="handleExtensions(item)">
v-for="(item, index) in props.headerExtensionList"
:key="index"
@click.stop="handleExtensions(item)"
>
<Icon :file="item.icon" /> <Icon :file="item.icon" />
</div> </div>
</div> </div>
@ -31,12 +24,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, withDefaults } from '../../../adapter-vue'; import { ref, onMounted, onUnmounted, withDefaults } from '../../../adapter-vue';
import { import { TUIStore, StoreName, TUITranslateService, IConversationModel } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
TUITranslateService,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUIConstants, ExtensionInfo } from '@tencentcloud/tui-core'; import { TUIConstants, ExtensionInfo } from '@tencentcloud/tui-core';
// import { JoinGroupCard } from '@tencentcloud/call-uikit-vue'; // import { JoinGroupCard } from '@tencentcloud/call-uikit-vue';
import Icon from '../../common/Icon.vue'; import Icon from '../../common/Icon.vue';
@ -49,8 +37,8 @@ const props = withDefaults(
headerExtensionList: ExtensionInfo[]; headerExtensionList: ExtensionInfo[];
}>(), }>(),
{ {
headerExtensionList: () => ([]), headerExtensionList: () => []
}, }
); );
const emits = defineEmits(['closeChat']); const emits = defineEmits(['closeChat']);
@ -62,21 +50,21 @@ const isNotRoomChat = ref<boolean>(TUIChatConfig.getChatType() !== TUIConstants.
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated, currentConversation: onCurrentConversationUpdated
}); });
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
typingStatus: onTypingStatusUpdated, typingStatus: onTypingStatusUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, { TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated, currentConversation: onCurrentConversationUpdated
}); });
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
typingStatus: onTypingStatusUpdated, typingStatus: onTypingStatusUpdated
}); });
}); });
@ -102,7 +90,6 @@ function onTypingStatusUpdated(status: boolean) {
currentConversationName.value = currentConversation.value?.getShowName() || ''; currentConversationName.value = currentConversation.value?.getShowName() || '';
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chat-header { .chat-header {
@ -152,7 +139,7 @@ function onTypingStatusUpdated(status: boolean) {
align-items: center; align-items: center;
} }
&-content{ &-content {
margin: 0 20px; margin: 0 20px;
text-align: center; text-align: center;
} }

View File

@ -26,7 +26,7 @@ class TUIChatConfig {
InputQuickReplies: true, InputQuickReplies: true,
InputMention: true, InputMention: true,
MessageSearch: true, MessageSearch: true,
ReadStatus: true, ReadStatus: true
}; };
this.theme = 'light'; this.theme = 'light';
} }
@ -76,8 +76,6 @@ class TUIChatConfig {
const ChatConfig = TUIChatConfig.getInstance(); const ChatConfig = TUIChatConfig.getInstance();
const hideTUIChatFeatures = ChatConfig.hideTUIChatFeatures.bind(ChatConfig); const hideTUIChatFeatures = ChatConfig.hideTUIChatFeatures.bind(ChatConfig);
export { export { hideTUIChatFeatures };
hideTUIChatFeatures,
};
export default ChatConfig; export default ChatConfig;

View File

@ -1,15 +1,15 @@
import { IEmojiGroupList } from '../../../interface'; import { IEmojiGroupList } from '../../../interface';
/** /**
* Custom big emoji * Custom big emoji
*/ */
export const CUSTOM_BIG_EMOJI_URL: string = ''; export const CUSTOM_BIG_EMOJI_URL: string = '';
export const CUSTOM_BIG_EMOJI_GROUP_LIST: IEmojiGroupList = []; export const CUSTOM_BIG_EMOJI_GROUP_LIST: IEmojiGroupList = [];
/** /**
* Custom basic emoji * Custom basic emoji
*/ */
export const CUSTOM_BASIC_EMOJI_URL: string = ''; export const CUSTOM_BASIC_EMOJI_URL: string = '';
export const CUSTOM_BASIC_EMOJI_URL_MAPPING: Record<string, string> = {}; export const CUSTOM_BASIC_EMOJI_URL_MAPPING: Record<string, string> = {};

View File

@ -77,7 +77,7 @@ export const DEFAULT_BASIC_EMOJI_URL_MAPPING: Record<string, string> = {
'[TUIEmoji_Prohibit]': 'emoji_58@2x.png', '[TUIEmoji_Prohibit]': 'emoji_58@2x.png',
'[TUIEmoji_Convinced]': 'emoji_59@2x.png', '[TUIEmoji_Convinced]': 'emoji_59@2x.png',
'[TUIEmoji_Knife]': 'emoji_60@2x.png', '[TUIEmoji_Knife]': 'emoji_60@2x.png',
'[TUIEmoji_Like]': 'emoji_61@2x.png', '[TUIEmoji_Like]': 'emoji_61@2x.png'
}; };
export const BIG_EMOJI_GROUP_LIST: IEmojiGroupList = [ export const BIG_EMOJI_GROUP_LIST: IEmojiGroupList = [
@ -85,30 +85,60 @@ export const BIG_EMOJI_GROUP_LIST: IEmojiGroupList = [
emojiGroupID: 1, emojiGroupID: 1,
type: EMOJI_TYPE.BIG, type: EMOJI_TYPE.BIG,
url: DEFAULT_BIG_EMOJI_URL, url: DEFAULT_BIG_EMOJI_URL,
list: ['yz00', 'yz01', 'yz02', 'yz03', 'yz04', 'yz05', 'yz06', 'yz07', 'yz08', list: [
'yz09', 'yz10', 'yz11', 'yz12', 'yz13', 'yz14', 'yz15', 'yz16', 'yz17'], 'yz00',
'yz01',
'yz02',
'yz03',
'yz04',
'yz05',
'yz06',
'yz07',
'yz08',
'yz09',
'yz10',
'yz11',
'yz12',
'yz13',
'yz14',
'yz15',
'yz16',
'yz17'
]
}, },
{ {
emojiGroupID: 2, emojiGroupID: 2,
type: EMOJI_TYPE.BIG, type: EMOJI_TYPE.BIG,
url: DEFAULT_BIG_EMOJI_URL, url: DEFAULT_BIG_EMOJI_URL,
list: ['ys00', 'ys01', 'ys02', 'ys03', 'ys04', 'ys05', 'ys06', 'ys07', 'ys08', list: ['ys00', 'ys01', 'ys02', 'ys03', 'ys04', 'ys05', 'ys06', 'ys07', 'ys08', 'ys09', 'ys10', 'ys11', 'ys12', 'ys13', 'ys14', 'ys15']
'ys09', 'ys10', 'ys11', 'ys12', 'ys13', 'ys14', 'ys15'],
}, },
{ {
emojiGroupID: 3, emojiGroupID: 3,
type: EMOJI_TYPE.BIG, type: EMOJI_TYPE.BIG,
url: DEFAULT_BIG_EMOJI_URL, url: DEFAULT_BIG_EMOJI_URL,
list: ['gcs00', 'gcs01', 'gcs02', 'gcs03', 'gcs04', 'gcs05', 'gcs06', 'gcs07', list: [
'gcs08', 'gcs09', 'gcs10', 'gcs11', 'gcs12', 'gcs13', 'gcs14', 'gcs15', 'gcs16'], 'gcs00',
}, 'gcs01',
'gcs02',
'gcs03',
'gcs04',
'gcs05',
'gcs06',
'gcs07',
'gcs08',
'gcs09',
'gcs10',
'gcs11',
'gcs12',
'gcs13',
'gcs14',
'gcs15',
'gcs16'
]
}
]; ];
export const BASIC_EMOJI_NAME_TO_KEY_MAPPING = { export const BASIC_EMOJI_NAME_TO_KEY_MAPPING = {
...Object.fromEntries( ...Object.fromEntries(Object.entries(emojiCNLocales)?.map(([key, val]) => [val, key])),
Object.entries(emojiCNLocales)?.map(([key, val]) => [val, key]), ...Object.fromEntries(Object.entries(emojiENLocales)?.map(([key, val]) => [val, key]))
),
...Object.fromEntries(
Object.entries(emojiENLocales)?.map(([key, val]) => [val, key]),
),
}; };

View File

@ -1,6 +1,12 @@
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine'; import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { CUSTOM_BASIC_EMOJI_URL, CUSTOM_BIG_EMOJI_URL, CUSTOM_BASIC_EMOJI_URL_MAPPING, CUSTOM_BIG_EMOJI_GROUP_LIST } from './custom-emoji'; import { CUSTOM_BASIC_EMOJI_URL, CUSTOM_BIG_EMOJI_URL, CUSTOM_BASIC_EMOJI_URL_MAPPING, CUSTOM_BIG_EMOJI_GROUP_LIST } from './custom-emoji';
import { DEFAULT_BASIC_EMOJI_URL, BIG_EMOJI_GROUP_LIST, DEFAULT_BASIC_EMOJI_URL_MAPPING, BASIC_EMOJI_NAME_TO_KEY_MAPPING, DEFAULT_BIG_EMOJI_URL } from './default-emoji'; import {
DEFAULT_BASIC_EMOJI_URL,
BIG_EMOJI_GROUP_LIST,
DEFAULT_BASIC_EMOJI_URL_MAPPING,
BASIC_EMOJI_NAME_TO_KEY_MAPPING,
DEFAULT_BIG_EMOJI_URL
} from './default-emoji';
import { default as emojiCNLocales } from './locales/zh_cn'; import { default as emojiCNLocales } from './locales/zh_cn';
import { IEmojiGroupList } from '../../../interface'; import { IEmojiGroupList } from '../../../interface';
import { EMOJI_TYPE } from '../../../constant'; import { EMOJI_TYPE } from '../../../constant';
@ -17,10 +23,10 @@ const EMOJI_GROUP_LIST: IEmojiGroupList = [
emojiGroupID: 0, emojiGroupID: 0,
type: EMOJI_TYPE.BASIC, type: EMOJI_TYPE.BASIC,
url: BASIC_EMOJI_URL, url: BASIC_EMOJI_URL,
list: Object.keys(BASIC_EMOJI_URL_MAPPING), list: Object.keys(BASIC_EMOJI_URL_MAPPING)
}, },
...BIG_EMOJI_GROUP_LIST, ...BIG_EMOJI_GROUP_LIST,
...CUSTOM_BIG_EMOJI_GROUP_LIST, ...CUSTOM_BIG_EMOJI_GROUP_LIST
]; ];
/** /**
@ -49,7 +55,7 @@ const transformTextWithKeysToEmojiNames = (text: string): string => {
const reg = /(\[.+?\])/g; const reg = /(\[.+?\])/g;
let txt: string = text; let txt: string = text;
if (reg.test(text)) { if (reg.test(text)) {
txt = text.replace(reg, match => BASIC_EMOJI_URL_MAPPING[match] ? convertKeyToEmojiName(match) : match); txt = text.replace(reg, (match) => (BASIC_EMOJI_URL_MAPPING[match] ? convertKeyToEmojiName(match) : match));
} }
return txt; return txt;
}; };
@ -68,20 +74,20 @@ const transformTextWithEmojiNamesToKeys = (text: string) => {
const reg = /(\[.+?\])/g; const reg = /(\[.+?\])/g;
let txt: string = text; let txt: string = text;
if (reg.test(text)) { if (reg.test(text)) {
txt = text.replace(reg, match => BASIC_EMOJI_NAME_TO_KEY_MAPPING[match] || match); txt = text.replace(reg, (match) => BASIC_EMOJI_NAME_TO_KEY_MAPPING[match] || match);
} }
return txt; return txt;
}; };
/** /**
* The configuration aims to provide compatibility with versions prior to 2.2.0 * The configuration aims to provide compatibility with versions prior to 2.2.0
*/ */
const emojiConfig = { const emojiConfig = {
emojiBaseUrl: BASIC_EMOJI_URL, emojiBaseUrl: BASIC_EMOJI_URL,
emojiUrlMapping: BASIC_EMOJI_URL_MAPPING, emojiUrlMapping: BASIC_EMOJI_URL_MAPPING,
emojiNameMapping: { emojiNameMapping: {
...emojiCNLocales, ...emojiCNLocales
}, }
}; };
/** /**
@ -136,5 +142,5 @@ export {
parseTextToRenderArray, parseTextToRenderArray,
transformTextWithKeysToEmojiNames, transformTextWithKeysToEmojiNames,
transformTextWithEmojiNamesToKeys, transformTextWithEmojiNamesToKeys,
emojiConfig, emojiConfig
}; };

View File

@ -60,7 +60,7 @@ const Emoji = {
'[TUIEmoji_666]': '[666]', '[TUIEmoji_666]': '[666]',
'[TUIEmoji_857]': '[857]', '[TUIEmoji_857]': '[857]',
'[TUIEmoji_Knife]': '[Knife]', '[TUIEmoji_Knife]': '[Knife]',
'[TUIEmoji_Like]': '[Like]', '[TUIEmoji_Like]': '[Like]'
}; };
export default Emoji; export default Emoji;

View File

@ -60,7 +60,7 @@ const Emoji: Record<string, string> = {
'[TUIEmoji_666]': '[666]', '[TUIEmoji_666]': '[666]',
'[TUIEmoji_857]': '[857]', '[TUIEmoji_857]': '[857]',
'[TUIEmoji_Knife]': '[刀]', '[TUIEmoji_Knife]': '[刀]',
'[TUIEmoji_Like]': '[赞]', '[TUIEmoji_Like]': '[赞]'
}; };
export default Emoji; export default Emoji;

View File

@ -60,7 +60,7 @@ const Emoji: Record<string, string> = {
'[TUIEmoji_666]': '[666]', '[TUIEmoji_666]': '[666]',
'[TUIEmoji_857]': '[857]', '[TUIEmoji_857]': '[857]',
'[TUIEmoji_Knife]': '[刀]', '[TUIEmoji_Knife]': '[刀]',
'[TUIEmoji_Like]': '[讚]', '[TUIEmoji_Like]': '[讚]'
}; };
export default Emoji; export default Emoji;

View File

@ -1,8 +1,5 @@
<template> <template>
<Overlay <Overlay :visible="isShowForwardPanel" :useMask="false">
:visible="isShowForwardPanel"
:useMask="false"
>
<Transfer <Transfer
:title="TUITranslateService.t('TUIChat.转发')" :title="TUITranslateService.t('TUIChat.转发')"
:isSearch="false" :isSearch="false"
@ -17,12 +14,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref } from '../../../adapter-vue'; import { onMounted, onUnmounted, ref } from '../../../adapter-vue';
import TUIChatEngine, { import TUIChatEngine, { TUIStore, StoreName, TUIChatService, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
TUIChatService,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import Overlay from '../../common/Overlay/index.vue'; import Overlay from '../../common/Overlay/index.vue';
import Transfer from '../../common/Transfer/index.vue'; import Transfer from '../../common/Transfer/index.vue';
import { Toast, TOAST_TYPE } from '../../../components/common/Toast'; import { Toast, TOAST_TYPE } from '../../../components/common/Toast';
@ -45,14 +37,14 @@ const customConversationList = ref();
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, { TUIStore.watch(StoreName.CUSTOM, {
singleForwardMessageID: onSingleForwardMessageIDUpdated, singleForwardMessageID: onSingleForwardMessageIDUpdated,
multipleForwardMessageID: onMultipleForwardMessageIDUpdated, multipleForwardMessageID: onMultipleForwardMessageIDUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, { TUIStore.unwatch(StoreName.CUSTOM, {
singleForwardMessageID: onSingleForwardMessageIDUpdated, singleForwardMessageID: onSingleForwardMessageIDUpdated,
multipleForwardMessageID: onMultipleForwardMessageIDUpdated, multipleForwardMessageID: onMultipleForwardMessageIDUpdated
}); });
// tuistore data must be cleared when closing the forward panel // tuistore data must be cleared when closing the forward panel
@ -72,10 +64,7 @@ function onMultipleForwardMessageIDUpdated(params: { isMergeForward: boolean; me
return; return;
} }
isMergeForward = false; isMergeForward = false;
const { const { isMergeForward: _isMergeForward, messageIDList: selectedMessageIDList } = params || {};
isMergeForward: _isMergeForward,
messageIDList: selectedMessageIDList,
} = params || {};
if (selectedMessageIDList?.length > 0) { if (selectedMessageIDList?.length > 0) {
isMergeForward = _isMergeForward; isMergeForward = _isMergeForward;
selectedToForwardMessageIDList = selectedMessageIDList; selectedToForwardMessageIDList = selectedMessageIDList;
@ -83,7 +72,7 @@ function onMultipleForwardMessageIDUpdated(params: { isMergeForward: boolean; me
} else { } else {
Toast({ Toast({
message: TUITranslateService.t('TUIChat.未选择消息'), message: TUITranslateService.t('TUIChat.未选择消息'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
} }
} }
@ -107,35 +96,29 @@ function openForwardPanel(): void {
function finishSelected(selectedConvIDWrapperList: Array<{ userID: string }>): void { function finishSelected(selectedConvIDWrapperList: Array<{ userID: string }>): void {
if (selectedConvIDWrapperList?.length === 0) return; if (selectedConvIDWrapperList?.length === 0) return;
// to reuse Transfer, so we have to get conversationModel by userID instead of ConversationID // to reuse Transfer, so we have to get conversationModel by userID instead of ConversationID
const selectedConversationList = selectedConvIDWrapperList.map(IDWrapper => TUIStore.getConversationModel(IDWrapper.userID)); const selectedConversationList = selectedConvIDWrapperList.map((IDWrapper) => TUIStore.getConversationModel(IDWrapper.userID));
const unsentMessageQueue = selectedToForwardMessageIDList const unsentMessageQueue = selectedToForwardMessageIDList.map((messageID) => TUIStore.getMessageModel(messageID)).sort((a, b) => a.time - b.time);
.map(messageID => TUIStore.getMessageModel(messageID))
.sort((a, b) => a.time - b.time);
const forwardPromises = selectedConversationList.map((conversation) => { const forwardPromises = selectedConversationList.map((conversation) => {
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = { const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation, conversation,
messageType: TUIChatEngine.TYPES.MSG_MERGER, messageType: TUIChatEngine.TYPES.MSG_MERGER
}; };
return TUIChatService.sendForwardMessage( return TUIChatService.sendForwardMessage([conversation], unsentMessageQueue, {
[conversation], needMerge: isMergeForward,
unsentMessageQueue, offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
{ params: {
needMerge: isMergeForward, needReadReceipt: isEnabledMessageReadReceiptGlobal()
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams), }
params: { });
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
},
},
);
}); });
Promise.allSettled(forwardPromises).then((results) => { Promise.allSettled(forwardPromises).then((results) => {
for (const result of results) { for (const result of results) {
const { status } = result; const { status } = result;
if (status === 'rejected') { if (status === 'rejected') {
const errorMessage = result.reason.code === 80001 ? TUITranslateService.t('TUIChat.内容包含敏感词汇') : result.reason.message as string; const errorMessage = result.reason.code === 80001 ? TUITranslateService.t('TUIChat.内容包含敏感词汇') : (result.reason.message as string);
Toast({ Toast({
message: errorMessage, message: errorMessage,
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
break; break;
} }
@ -152,7 +135,7 @@ function getTransforRenderDataList() {
// To achieve reusability of Transfer, userID is used here instead of ConversationID // To achieve reusability of Transfer, userID is used here instead of ConversationID
userID: conversation.conversationID, userID: conversation.conversationID,
nick: conversation.getShowName(), nick: conversation.getShowName(),
avatar: conversation.getAvatar(), avatar: conversation.getAvatar()
}; };
}); });
} }

View File

@ -6,75 +6,31 @@
'emoji-picker-h5': !isPC 'emoji-picker-h5': !isPC
}" }"
> >
<ul <ul ref="emojiPickerListRef" :class="['emoji-picker-list', !isPC && 'emoji-picker-h5-list']">
ref="emojiPickerListRef"
:class="['emoji-picker-list', !isPC && 'emoji-picker-h5-list']"
>
<li <li
v-for="(childrenItem, childrenIndex) in currentEmojiList" v-for="(childrenItem, childrenIndex) in currentEmojiList"
:key="childrenIndex" :key="childrenIndex"
class="emoji-picker-list-item" class="emoji-picker-list-item"
@click="select(childrenItem, childrenIndex)" @click="select(childrenItem, childrenIndex)"
> >
<img <img v-if="currentTabItem.type === EMOJI_TYPE.BASIC" class="emoji" :src="currentTabItem.url + BASIC_EMOJI_URL_MAPPING[childrenItem]" />
v-if="currentTabItem.type === EMOJI_TYPE.BASIC" <img v-else-if="currentTabItem.type === EMOJI_TYPE.BIG" class="emoji-big" :src="currentTabItem.url + childrenItem + '@2x.png'" />
class="emoji" <img v-else class="emoji-custom emoji-big" :src="currentTabItem.url + childrenItem" />
:src="currentTabItem.url + BASIC_EMOJI_URL_MAPPING[childrenItem]"
>
<img
v-else-if="currentTabItem.type === EMOJI_TYPE.BIG"
class="emoji-big"
:src="currentTabItem.url + childrenItem + '@2x.png'"
>
<img
v-else
class="emoji-custom emoji-big"
:src="currentTabItem.url + childrenItem"
>
</li> </li>
</ul> </ul>
<ul class="emoji-picker-tab"> <ul class="emoji-picker-tab">
<li <li v-for="(item, index) in list" :key="index" class="emoji-picker-tab-item" @click="toggleEmojiTab(index)">
v-for="(item, index) in list" <Icon v-if="item.type === EMOJI_TYPE.BASIC" class="icon" :file="faceIcon" />
:key="index" <img v-else-if="item.type === EMOJI_TYPE.BIG" class="icon-big" :src="item.url + item.list[0] + '@2x.png'" />
class="emoji-picker-tab-item" <img v-else class="icon-custom icon-big" :src="item.url + item.list[0]" />
@click="toggleEmojiTab(index)"
>
<Icon
v-if="item.type === EMOJI_TYPE.BASIC"
class="icon"
:file="faceIcon"
/>
<img
v-else-if="item.type === EMOJI_TYPE.BIG"
class="icon-big"
:src="item.url + item.list[0] + '@2x.png'"
>
<img
v-else
class="icon-custom icon-big"
:src="item.url + item.list[0]"
>
</li>
<li
v-if="isUniFrameWork"
class="send-btn"
@click="sendMessage"
>
发送
</li> </li>
<li v-if="isUniFrameWork" class="send-btn" @click="sendMessage">发送</li>
</ul> </ul>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../adapter-vue'; import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
import { import { TUIChatService, TUIStore, StoreName, IConversationModel, SendMessageParams } from '@tencentcloud/chat-uikit-engine';
TUIChatService,
TUIStore,
StoreName,
IConversationModel,
SendMessageParams,
} from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../common/Icon.vue'; import Icon from '../../../common/Icon.vue';
import faceIconLight from '../../../../assets/icon/face-light.svg'; import faceIconLight from '../../../../assets/icon/face-light.svg';
import faceIconDark from '../../../../assets/icon/face-dark.svg'; import faceIconDark from '../../../../assets/icon/face-dark.svg';
@ -98,13 +54,13 @@ const currentEmojiList = ref<string[]>(list?.value[0]?.list);
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate, currentConversation: onCurrentConversationUpdate
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, { TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate, currentConversation: onCurrentConversationUpdate
}); });
}); });
@ -121,7 +77,7 @@ const toggleEmojiTab = (index: number) => {
const select = (item: any, index: number) => { const select = (item: any, index: number) => {
const options: any = { const options: any = {
emoji: { key: item, name: convertKeyToEmojiName(item) }, emoji: { key: item, name: convertKeyToEmojiName(item) },
type: currentTabItem?.value?.type, type: currentTabItem?.value?.type
}; };
switch (currentTabItem?.value?.type) { switch (currentTabItem?.value?.type) {
case EMOJI_TYPE.BASIC: case EMOJI_TYPE.BASIC:
@ -146,15 +102,13 @@ const select = (item: any, index: number) => {
const sendFaceMessage = (index: number, listItem: IEmojiGroup) => { const sendFaceMessage = (index: number, listItem: IEmojiGroup) => {
const options = { const options = {
to: to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type, conversationType: currentConversation?.value?.type,
payload: { payload: {
index: listItem.emojiGroupID, index: listItem.emojiGroupID,
data: listItem.list[index], data: listItem.list[index]
}, },
needReadReceipt: isEnabledMessageReadReceiptGlobal(), needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams; } as SendMessageParams;
TUIChatService.sendFaceMessage(options); TUIChatService.sendFaceMessage(options);
}; };

View File

@ -1,24 +1,10 @@
<template> <template>
<ToolbarItemContainer <ToolbarItemContainer ref="container" :iconFile="faceIcon" title="表情" @onDialogShow="onDialogShow" @onDialogClose="onDialogClose">
ref="container" <EmojiPickerDialog @insertEmoji="insertEmoji" @sendMessage="sendMessage" @onClose="onClose" />
:iconFile="faceIcon"
title="表情"
@onDialogShow="onDialogShow"
@onDialogClose="onDialogClose"
>
<EmojiPickerDialog
@insertEmoji="insertEmoji"
@sendMessage="sendMessage"
@onClose="onClose"
/>
</ToolbarItemContainer> </ToolbarItemContainer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { TUIStore, StoreName, IConversationModel } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../../adapter-vue'; import { ref } from '../../../../adapter-vue';
import faceIconLight from '../../../../assets/icon/face-light.svg'; import faceIconLight from '../../../../assets/icon/face-light.svg';
import faceIconDark from '../../../../assets/icon/face-dark.svg'; import faceIconDark from '../../../../assets/icon/face-dark.svg';
@ -45,7 +31,7 @@ const container = ref<InstanceType<typeof ToolbarItemContainer>>();
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => { currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation; currentConversation.value = conversation;
}, }
}); });
const onDialogShow = (dialogRef: any) => { const onDialogShow = (dialogRef: any) => {
@ -75,7 +61,7 @@ const onClose = () => {
}; };
defineExpose({ defineExpose({
closeEmojiPicker: onClose, closeEmojiPicker: onClose
}); });
</script> </script>
<style lang="scss" scoped src="./style/index.scss"></style> <style lang="scss" scoped src="./style/index.scss"></style>

View File

@ -11,83 +11,39 @@
> >
<div :class="['evaluate', !isPC && 'evaluate-h5']"> <div :class="['evaluate', !isPC && 'evaluate-h5']">
<div :class="['evaluate-header', !isPC && 'evaluate-h5-header']"> <div :class="['evaluate-header', !isPC && 'evaluate-h5-header']">
<div <div :class="['evaluate-header-content', !isPC && 'evaluate-h5-header-content']">
:class="[ {{ TUITranslateService.t('Evaluate.请对本次服务进行评价') }}
'evaluate-header-content',
!isPC && 'evaluate-h5-header-content',
]"
>
{{ TUITranslateService.t("Evaluate.请对本次服务进行评价") }}
</div> </div>
<div <div v-if="!isPC" :class="['evaluate-header-close', !isPC && 'evaluate-h5-header-close']" @click.stop="closeDialog">
v-if="!isPC" {{ TUITranslateService.t('关闭') }}
:class="[
'evaluate-header-close',
!isPC && 'evaluate-h5-header-close',
]"
@click.stop="closeDialog"
>
{{ TUITranslateService.t("关闭") }}
</div> </div>
</div> </div>
<div :class="['evaluate-content', !isPC && 'evaluate-h5-content']"> <div :class="['evaluate-content', !isPC && 'evaluate-h5-content']">
<ul <ul :class="['evaluate-content-list', !isPC && 'evaluate-h5-content-list']">
:class="[
'evaluate-content-list',
!isPC && 'evaluate-h5-content-list',
]"
>
<li <li
v-for="(item, index) in starList" v-for="(item, index) in starList"
:key="index" :key="index"
:class="[ :class="['evaluate-content-list-item', !isPC && 'evaluate-h5-content-list-item']"
'evaluate-content-list-item',
!isPC && 'evaluate-h5-content-list-item',
]"
@click.stop="selectStar(index)" @click.stop="selectStar(index)"
> >
<Icon <Icon v-if="index <= currentStarIndex" :file="starLightIcon" :width="isPC ? '20px' : '30px'" :height="isPC ? '20px' : '30px'" />
v-if="index <= currentStarIndex" <Icon v-else :file="starIcon" :width="isPC ? '20px' : '30px'" :height="isPC ? '20px' : '30px'" />
:file="starLightIcon"
:width="isPC ? '20px' : '30px'"
:height="isPC ? '20px' : '30px'"
/>
<Icon
v-else
:file="starIcon"
:width="isPC ? '20px' : '30px'"
:height="isPC ? '20px' : '30px'"
/>
</li> </li>
</ul> </ul>
<textarea <textarea v-model="comment" :class="['evaluate-content-text', !isPC && 'evaluate-h5-content-text']" />
v-model="comment" <div :class="['evaluate-content-button', !isPC && 'evaluate-h5-content-button']">
:class="[ <button :class="['btn', isEvaluateValid ? 'btn-valid' : 'btn-invalid']" @click="submitEvaluate">
'evaluate-content-text', {{ TUITranslateService.t('Evaluate.提交评价') }}
!isPC && 'evaluate-h5-content-text',
]"
/>
<div
:class="[
'evaluate-content-button',
!isPC && 'evaluate-h5-content-button',
]"
>
<button
:class="['btn', isEvaluateValid ? 'btn-valid' : 'btn-invalid']"
@click="submitEvaluate"
>
{{ TUITranslateService.t("Evaluate.提交评价") }}
</button> </button>
</div> </div>
</div> </div>
<div :class="['evaluate-adv', !isPC && 'evaluate-h5-adv']"> <div :class="['evaluate-adv', !isPC && 'evaluate-h5-adv']">
{{ TUITranslateService.t("Evaluate.服务评价工具") }} {{ TUITranslateService.t('Evaluate.服务评价工具') }}
{{ "(" + TUITranslateService.t("Evaluate.使用") }} {{ '(' + TUITranslateService.t('Evaluate.使用') }}
<a @click="openLink(Link.customMessage)"> <a @click="openLink(Link.customMessage)">
{{ TUITranslateService.t(`Evaluate.${Link.customMessage.label}`) }} {{ TUITranslateService.t(`Evaluate.${Link.customMessage.label}`) }}
</a> </a>
{{ TUITranslateService.t("Evaluate.搭建") + ")" }} {{ TUITranslateService.t('Evaluate.搭建') + ')' }}
</div> </div>
</div> </div>
</ToolbarItemContainer> </ToolbarItemContainer>
@ -100,7 +56,7 @@ import TUIChatEngine, {
IConversationModel, IConversationModel,
TUIChatService, TUIChatService,
SendMessageParams, SendMessageParams,
SendMessageOptions, SendMessageOptions
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import { ref, computed } from '../../../../adapter-vue'; import { ref, computed } from '../../../../adapter-vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue'; import ToolbarItemContainer from '../toolbar-item-container/index.vue';
@ -120,8 +76,8 @@ const evaluateIcon = TUIChatConfig.getTheme() === 'dark' ? evaluateIconDark : ev
const props = defineProps({ const props = defineProps({
starTotal: { starTotal: {
type: Number, type: Number,
default: 5, default: 5
}, }
}); });
const emits = defineEmits(['onDialogPopupShowOrHide']); const emits = defineEmits(['onDialogPopupShowOrHide']);
@ -135,7 +91,7 @@ const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => { currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation; currentConversation.value = conversation;
}, }
}); });
const isEvaluateValid = computed(() => comment.value.length || currentStarIndex.value >= 0); const isEvaluateValid = computed(() => comment.value.length || currentStarIndex.value >= 0);
@ -182,26 +138,24 @@ const submitEvaluate = () => {
businessID: CHAT_MSG_CUSTOM_TYPE.EVALUATE, businessID: CHAT_MSG_CUSTOM_TYPE.EVALUATE,
version: 1, version: 1,
score: currentStarIndex.value + 1, score: currentStarIndex.value + 1,
comment: comment.value, comment: comment.value
}), }),
description: '对本次的服务评价', description: '对本次的服务评价',
extension: '对本次的服务评价', extension: '对本次的服务评价'
}; };
const options = { const options = {
to: to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type, conversationType: currentConversation?.value?.type,
payload, payload,
needReadReceipt: isEnabledMessageReadReceiptGlobal(), needReadReceipt: isEnabledMessageReadReceiptGlobal()
}; };
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = { const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value, conversation: currentConversation.value,
payload: options.payload, payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_CUSTOM, messageType: TUIChatEngine.TYPES.MSG_CUSTOM
}; };
const sendMessageOptions: SendMessageOptions = { const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams), offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams)
}; };
TUIChatService.sendCustomMessage(options as SendMessageParams, sendMessageOptions); TUIChatService.sendCustomMessage(options as SendMessageParams, sendMessageOptions);
// close dialog after submit evaluate // close dialog after submit evaluate

View File

@ -8,14 +8,7 @@
@onIconClick="onIconClick" @onIconClick="onIconClick"
> >
<div :class="['file-upload', !isPC && 'file-upload-h5']"> <div :class="['file-upload', !isPC && 'file-upload-h5']">
<input <input ref="inputRef" title="文件" type="file" data-type="file" accept="*" @change="sendFileMessage" />
ref="inputRef"
title="文件"
type="file"
data-type="file"
accept="*"
@change="sendFileMessage"
>
</div> </div>
</ToolbarItemContainer> </ToolbarItemContainer>
</template> </template>
@ -26,7 +19,7 @@ import TUIChatEngine, {
StoreName, StoreName,
IConversationModel, IConversationModel,
SendMessageParams, SendMessageParams,
SendMessageOptions, SendMessageOptions
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../../adapter-vue'; import { ref } from '../../../../adapter-vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue'; import ToolbarItemContainer from '../toolbar-item-container/index.vue';
@ -44,7 +37,7 @@ const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => { currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation; currentConversation.value = conversation;
}, }
}); });
const onIconClick = () => { const onIconClick = () => {
@ -60,27 +53,25 @@ const sendFileMessage = (e: any) => {
return; return;
} }
const options = { const options = {
to: to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type, conversationType: currentConversation?.value?.type,
payload: { payload: {
file: e?.target, file: e?.target
}, },
needReadReceipt: isEnabledMessageReadReceiptGlobal(), needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams; } as SendMessageParams;
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = { const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value, conversation: currentConversation.value,
payload: options.payload, payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_FILE, messageType: TUIChatEngine.TYPES.MSG_FILE
}; };
const sendMessageOptions: SendMessageOptions = { const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams), offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams)
}; };
TUIChatService.sendFileMessage(options, sendMessageOptions); TUIChatService.sendFileMessage(options, sendMessageOptions);
e.target.value = ''; e.target.value = '';
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
</style> </style>

View File

@ -7,10 +7,7 @@
:needDialog="false" :needDialog="false"
@onIconClick="onIconClick" @onIconClick="onIconClick"
> >
<div <div v-if="!isUniFrameWork" :class="['image-upload', !isPC && 'image-upload-h5']">
v-if="!isUniFrameWork"
:class="['image-upload', !isPC && 'image-upload-h5']"
>
<input <input
ref="inputRef" ref="inputRef"
title="图片" title="图片"
@ -18,7 +15,7 @@
data-type="image" data-type="image"
accept="image/gif,image/jpeg,image/jpg,image/png,image/bmp,image/webp" accept="image/gif,image/jpeg,image/jpg,image/png,image/bmp,image/webp"
@change="sendImageInWeb" @change="sendImageInWeb"
> />
</div> </div>
</ToolbarItemContainer> </ToolbarItemContainer>
</template> </template>
@ -29,7 +26,7 @@ import TUIChatEngine, {
StoreName, StoreName,
IConversationModel, IConversationModel,
SendMessageParams, SendMessageParams,
SendMessageOptions, SendMessageOptions
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, computed } from '../../../../adapter-vue'; import { ref, computed } from '../../../../adapter-vue';
@ -49,8 +46,8 @@ const props = defineProps({
// camera: Take a photo using the camera // camera: Take a photo using the camera
imageSourceType: { imageSourceType: {
type: String, type: String,
default: 'album', default: 'album'
}, }
}); });
const inputRef = ref(); const inputRef = ref();
@ -59,29 +56,27 @@ const theme = TUIChatConfig.getTheme();
const IMAGE_TOOLBAR_SHOW_MAP = { const IMAGE_TOOLBAR_SHOW_MAP = {
web_album: { web_album: {
icon: theme === 'dark' ? imageIconDark : imageIconLight, icon: theme === 'dark' ? imageIconDark : imageIconLight,
title: '图片', title: '图片'
}, },
uni_album: { uni_album: {
icon: imageUniIcon, icon: imageUniIcon,
title: '图片', title: '图片'
}, },
uni_camera: { uni_camera: {
icon: cameraUniIcon, icon: cameraUniIcon,
title: '拍照', title: '拍照'
}, }
}; };
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => { currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation; currentConversation.value = conversation;
}, }
}); });
const imageToolbarForShow = computed((): { icon: string; title: string } => { const imageToolbarForShow = computed((): { icon: string; title: string } => {
if (isUniFrameWork) { if (isUniFrameWork) {
return props.imageSourceType === 'camera' return props.imageSourceType === 'camera' ? IMAGE_TOOLBAR_SHOW_MAP['uni_camera'] : IMAGE_TOOLBAR_SHOW_MAP['uni_album'];
? IMAGE_TOOLBAR_SHOW_MAP['uni_camera']
: IMAGE_TOOLBAR_SHOW_MAP['uni_album'];
} else { } else {
return IMAGE_TOOLBAR_SHOW_MAP['web_album']; return IMAGE_TOOLBAR_SHOW_MAP['web_album'];
} }
@ -98,7 +93,7 @@ const onIconClick = () => {
sourceType: [props.imageSourceType], // Use camera or select from album. sourceType: [props.imageSourceType], // Use camera or select from album.
success: function (res: any) { success: function (res: any) {
sendImageMessage(res); sendImageMessage(res);
}, }
}); });
} else { } else {
// uni-app H5/App send image // uni-app H5/App send image
@ -107,7 +102,7 @@ const onIconClick = () => {
sourceType: [props.imageSourceType], // Use camera or select from album. sourceType: [props.imageSourceType], // Use camera or select from album.
success: function (res) { success: function (res) {
sendImageMessage(res); sendImageMessage(res);
}, }
}); });
} }
} else { } else {
@ -130,27 +125,25 @@ const sendImageMessage = (files: any) => {
return; return;
} }
const options = { const options = {
to: to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type, conversationType: currentConversation?.value?.type,
payload: { payload: {
file: files, file: files
}, },
needReadReceipt: isEnabledMessageReadReceiptGlobal(), needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams; } as SendMessageParams;
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = { const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value, conversation: currentConversation.value,
payload: options.payload, payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_IMAGE, messageType: TUIChatEngine.TYPES.MSG_IMAGE
}; };
const sendMessageOptions: SendMessageOptions = { const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams), offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams)
}; };
TUIChatService.sendImageMessage(options, sendMessageOptions); TUIChatService.sendImageMessage(options, sendMessageOptions);
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
</style> </style>

View File

@ -1,18 +1,6 @@
<template> <template>
<div <div :class="['message-input-toolbar', !isPC && 'message-input-toolbar-h5', isUniFrameWork && 'message-input-toolbar-uni']">
:class="[ <div :class="['message-input-toolbar-list', !isPC && 'message-input-toolbar-h5-list', isUniFrameWork && 'message-input-toolbar-uni-list']">
'message-input-toolbar',
!isPC && 'message-input-toolbar-h5',
isUniFrameWork && 'message-input-toolbar-uni',
]"
>
<div
:class="[
'message-input-toolbar-list',
!isPC && 'message-input-toolbar-h5-list',
isUniFrameWork && 'message-input-toolbar-uni-list',
]"
>
<EmojiPicker <EmojiPicker
v-if="isRenderedEmojiPicker" v-if="isRenderedEmojiPicker"
ref="emojiPickerRef" ref="emojiPickerRef"
@ -21,15 +9,9 @@
@dialogCloseInH5="dialogCloseInH5" @dialogCloseInH5="dialogCloseInH5"
@changeToolbarDisplayType="(type) => emits('changeToolbarDisplayType', type)" @changeToolbarDisplayType="(type) => emits('changeToolbarDisplayType', type)"
/> />
<ImageUpload <ImageUpload v-if="featureConfig.InputImage" imageSourceType="album" />
v-if="featureConfig.InputImage"
imageSourceType="album"
/>
<FileUpload v-if="featureConfig.InputFile" /> <FileUpload v-if="featureConfig.InputFile" />
<VideoUpload <VideoUpload v-if="featureConfig.InputVideo" videoSourceType="album" />
v-if="featureConfig.InputVideo"
videoSourceType="album"
/>
<Evaluate v-if="featureConfig.InputEvaluation" /> <Evaluate v-if="featureConfig.InputEvaluation" />
<Words v-if="featureConfig.InputQuickReplies" /> <Words v-if="featureConfig.InputQuickReplies" />
<template v-if="extensionListShowInStart[0]"> <template v-if="extensionListShowInStart[0]">
@ -45,10 +27,7 @@
/> />
</template> </template>
</div> </div>
<div <div v-if="extensionListShowInEnd[0] && isPC" :class="['message-input-toolbar-list-end']">
v-if="extensionListShowInEnd[0] && isPC"
:class="['message-input-toolbar-list-end']"
>
<ToolbarItemContainer <ToolbarItemContainer
v-for="(extension, index) in extensionListShowInEnd" v-for="(extension, index) in extensionListShowInEnd"
:key="index" :key="index"
@ -68,21 +47,12 @@
@submit="onUserSelectorSubmit" @submit="onUserSelectorSubmit"
@cancel="onUserSelectorCancel" @cancel="onUserSelectorCancel"
/> />
<div <div v-if="isH5" ref="h5Dialog" :class="['message-input-toolbar-h5-dialog']" />
v-if="isH5"
ref="h5Dialog"
:class="['message-input-toolbar-h5-dialog']"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from '../../../adapter-vue'; import { ref, computed, onMounted, onUnmounted, watch } from '../../../adapter-vue';
import TUIChatEngine, { import TUIChatEngine, { IConversationModel, TUIStore, StoreName, TUIReportService } from '@tencentcloud/chat-uikit-engine';
IConversationModel,
TUIStore,
StoreName,
TUIReportService,
} from '@tencentcloud/chat-uikit-engine';
import TUICore, { ExtensionInfo, TUIConstants } from '@tencentcloud/tui-core'; import TUICore, { ExtensionInfo, TUIConstants } from '@tencentcloud/tui-core';
import EmojiPicker from './emoji-picker/index.vue'; import EmojiPicker from './emoji-picker/index.vue';
import ImageUpload from './image-upload/index.vue'; import ImageUpload from './image-upload/index.vue';
@ -103,10 +73,10 @@ interface IProps {
interface IEmits { interface IEmits {
(e: 'scrollToLatestMessage'): void; (e: 'scrollToLatestMessage'): void;
(e: 'changeToolbarDisplayType', type: ToolbarDisplayType): void; (e: 'changeToolbarDisplayType', type: ToolbarDisplayType): void;
(e: 'insertEmoji', emoji: any): void (e: 'insertEmoji', emoji: any): void;
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
displayType: 'none', displayType: 'none'
}); });
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
@ -124,23 +94,26 @@ isRenderedEmojiPicker.value = featureConfig.InputEmoji || featureConfig.InputSti
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, { TUIStore.watch(StoreName.CUSTOM, {
activeConversation: onActiveConversationUpdate, activeConversation: onActiveConversationUpdate
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, { TUIStore.unwatch(StoreName.CUSTOM, {
activeConversation: onActiveConversationUpdate, activeConversation: onActiveConversationUpdate
}); });
}); });
watch(() => props.displayType, (newValue) => { watch(
if (newValue === 'none') { () => props.displayType,
emojiPickerRef.value?.closeEmojiPicker(); (newValue) => {
} else { if (newValue === 'none') {
emits('scrollToLatestMessage'); emojiPickerRef.value?.closeEmojiPicker();
} else {
emits('scrollToLatestMessage');
}
} }
}); );
const onActiveConversationUpdate = (conversationID: string) => { const onActiveConversationUpdate = (conversationID: string) => {
if (!conversationID) { if (!conversationID) {
@ -162,25 +135,25 @@ const getExtensionList = () => {
params.filterVideo = true; params.filterVideo = true;
enableSampleTaskStatus('customerService'); enableSampleTaskStatus('customerService');
} }
currentExtensionList.value = [ currentExtensionList.value = [...TUICore.getExtensionList(TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID, params)].filter(
...TUICore.getExtensionList(TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID, params), (extension: ExtensionInfo) => {
].filter((extension: ExtensionInfo) => { if (extension?.data?.name === 'search') {
if (extension?.data?.name === 'search') { return featureConfig.MessageSearch;
return featureConfig.MessageSearch; }
return true;
} }
return true; );
});
reportExtension(currentExtensionList.value); reportExtension(currentExtensionList.value);
}; };
function reportExtension(extensionList:ExtensionInfo[]){ function reportExtension(extensionList: ExtensionInfo[]) {
extensionList.forEach((extension: ExtensionInfo)=>{ extensionList.forEach((extension: ExtensionInfo) => {
const _name = extension?.data?.name; const _name = extension?.data?.name;
if(_name === 'voiceCall'){ if (_name === 'voiceCall') {
TUIReportService.reportFeature(203, 'voice-call'); TUIReportService.reportFeature(203, 'voice-call');
} else if (_name === 'videoCall') { } else if (_name === 'videoCall') {
TUIReportService.reportFeature(203, 'video-call'); TUIReportService.reportFeature(203, 'video-call');
} else if(_name === 'quickRoom'){ } else if (_name === 'quickRoom') {
TUIReportService.reportFeature(204); TUIReportService.reportFeature(204);
} }
}); });
@ -196,7 +169,7 @@ const extensionListShowInStart = computed<ExtensionInfo[]>(() => {
const extensionListShowInEnd = computed<ExtensionInfo[]>(() => { const extensionListShowInEnd = computed<ExtensionInfo[]>(() => {
if (isPC) { if (isPC) {
const searchExtension = currentExtensionList.value.find(extension => extension?.data?.name === 'search'); const searchExtension = currentExtensionList.value.find((extension) => extension?.data?.name === 'search');
return searchExtension ? [searchExtension] : []; return searchExtension ? [searchExtension] : [];
} }
return []; return [];
@ -222,7 +195,7 @@ const onCallExtensionClicked = (extension: ExtensionInfo, callType: number) => {
if (currentConversation?.value?.type === TUIChatEngine.TYPES.CONV_C2C) { if (currentConversation?.value?.type === TUIChatEngine.TYPES.CONV_C2C) {
extension.listener?.onClicked?.({ extension.listener?.onClicked?.({
userIDList: [currentConversation?.value?.conversationID?.slice(3)], userIDList: [currentConversation?.value?.conversationID?.slice(3)],
type: callType, type: callType
}); });
} else if (isGroup.value) { } else if (isGroup.value) {
currentUserSelectorExtension.value = extension; currentUserSelectorExtension.value = extension;
@ -268,7 +241,7 @@ const dialogCloseInH5 = (dialogDom: any) => {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../assets/styles/common"; @import '../../../assets/styles/common';
.message-input-toolbar { .message-input-toolbar {
border-top: 1px solid #f4f5f9; border-top: 1px solid #f4f5f9;

View File

@ -1,30 +1,9 @@
<template> <template>
<div <div ref="toolbarItemRef" :class="['toolbar-item-container', !isPC && 'toolbar-item-container-h5', isUniFrameWork && 'toolbar-item-container-uni']">
ref="toolbarItemRef" <div :class="['toolbar-item-container-icon', isUniFrameWork && 'toolbar-item-container-uni-icon']" @click="toggleToolbarItem">
:class="[ <Icon :file="props.iconFile" class="icon" :width="props.iconWidth" :height="props.iconHeight" />
'toolbar-item-container',
!isPC && 'toolbar-item-container-h5',
isUniFrameWork && 'toolbar-item-container-uni',
]"
>
<div
:class="[
'toolbar-item-container-icon',
isUniFrameWork && 'toolbar-item-container-uni-icon',
]"
@click="toggleToolbarItem"
>
<Icon
:file="props.iconFile"
class="icon"
:width="props.iconWidth"
:height="props.iconHeight"
/>
</div> </div>
<div <div v-if="isUniFrameWork" :class="['toolbar-item-container-uni-title']">
v-if="isUniFrameWork"
:class="['toolbar-item-container-uni-title']"
>
{{ props.title }} {{ props.title }}
</div> </div>
<div <div
@ -34,7 +13,7 @@
'toolbar-item-container-dialog', 'toolbar-item-container-dialog',
isDark && 'toolbar-item-container-dialog-dark', isDark && 'toolbar-item-container-dialog-dark',
!isPC && 'toolbar-item-container-h5-dialog', !isPC && 'toolbar-item-container-h5-dialog',
isUniFrameWork && 'toolbar-item-container-uni-dialog', isUniFrameWork && 'toolbar-item-container-uni-dialog'
]" ]"
> >
<BottomPopup <BottomPopup
@ -61,30 +40,30 @@ import TUIChatConfig from '../../config';
const props = defineProps({ const props = defineProps({
iconFile: { iconFile: {
type: String, type: String,
required: true, required: true
}, },
title: { title: {
type: String, type: String,
default: '', default: ''
}, },
needDialog: { needDialog: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
iconWidth: { iconWidth: {
type: String, type: String,
default: '20px', default: '20px'
}, },
iconHeight: { iconHeight: {
type: String, type: String,
default: '20px', default: '20px'
}, },
// Whether to display the bottom popup dialog on mobile devices // Whether to display the bottom popup dialog on mobile devices
// Invalid on PC // Invalid on PC
needBottomPopup: { needBottomPopup: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}); });
const emits = defineEmits(['onIconClick', 'onDialogClose', 'onDialogShow']); const emits = defineEmits(['onIconClick', 'onDialogClose', 'onDialogShow']);
@ -99,7 +78,7 @@ const toggleToolbarItem = () => {
if (isPC) { if (isPC) {
outsideClick.listen({ outsideClick.listen({
domRefs: toolbarItemRef.value, domRefs: toolbarItemRef.value,
handler: closeToolbarItem, handler: closeToolbarItem
}); });
} }
if (!props.needDialog) { if (!props.needDialog) {
@ -132,7 +111,7 @@ const onPopupClose = () => {
}; };
defineExpose({ defineExpose({
toggleDialogDisplay, toggleDialogDisplay
}); });
</script> </script>
<style lang="scss" scoped src="./style/index.scss"></style> <style lang="scss" scoped src="./style/index.scss"></style>

View File

@ -1,12 +1,5 @@
<template> <template>
<Dialog <Dialog :show="show" :isH5="!isPC" :isHeaderShow="false" :isFooterShow="false" :background="false" @update:show="toggleShow">
:show="show"
:isH5="!isPC"
:isHeaderShow="false"
:isFooterShow="false"
:background="false"
@update:show="toggleShow"
>
<Transfer <Transfer
:isSearch="true" :isSearch="true"
:title="title" :title="title"
@ -20,10 +13,7 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import { TUIGroupService, TUIUserService } from '@tencentcloud/chat-uikit-engine';
TUIGroupService,
TUIUserService,
} from '@tencentcloud/chat-uikit-engine';
import { ref, computed, watch } from '../../../../adapter-vue'; import { ref, computed, watch } from '../../../../adapter-vue';
import Dialog from '../../../common/Dialog/index.vue'; import Dialog from '../../../common/Dialog/index.vue';
import Transfer from '../../../common/Transfer/index.vue'; import Transfer from '../../../common/Transfer/index.vue';
@ -33,16 +23,16 @@ const props = defineProps({
// type: voiceCall/groupCall/... // type: voiceCall/groupCall/...
type: { type: {
type: String, type: String,
default: '', default: ''
}, },
currentConversation: { currentConversation: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, },
isGroup: { isGroup: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}); });
const emits = defineEmits(['submit', 'cancel']); const emits = defineEmits(['submit', 'cancel']);
const show = ref<boolean>(false); const show = ref<boolean>(false);
@ -52,7 +42,7 @@ const searchMemberList = ref<any[]>([]);
const selfUserID = ref<string>(''); const selfUserID = ref<string>('');
const titleMap: any = { const titleMap: any = {
voiceCall: '发起群语音', voiceCall: '发起群语音',
videoCall: '发起群视频', videoCall: '发起群视频'
}; };
const title = computed(() => { const title = computed(() => {
return titleMap[props.type] ? titleMap[props.type] : ''; return titleMap[props.type] ? titleMap[props.type] : '';
@ -71,11 +61,9 @@ watch(
if (props.isGroup && show.value) { if (props.isGroup && show.value) {
groupID.value = props.currentConversation.groupProfile.groupID; groupID.value = props.currentConversation.groupProfile.groupID;
TUIGroupService.getGroupMemberList({ TUIGroupService.getGroupMemberList({
groupID: groupID.value, groupID: groupID.value
}).then((res: any) => { }).then((res: any) => {
memberList.value = res?.data?.memberList?.filter( memberList.value = res?.data?.memberList?.filter((user: any) => user?.userID !== selfUserID.value);
(user: any) => user?.userID !== selfUserID.value,
);
searchMemberList.value = memberList.value; searchMemberList.value = memberList.value;
}); });
} else { } else {
@ -86,14 +74,12 @@ watch(
} }
}, },
{ {
immediate: true, immediate: true
}, }
); );
const search = (searchInfo: string) => { const search = (searchInfo: string) => {
const results = memberList.value?.filter( const results = memberList.value?.filter((member: any) => member?.userID === searchInfo);
(member: any) => member?.userID === searchInfo,
);
searchMemberList.value = results?.length ? results : memberList.value; searchMemberList.value = results?.length ? results : memberList.value;
}; };
@ -122,6 +108,6 @@ const toggleShow = (showStatus: boolean) => {
}; };
defineExpose({ defineExpose({
toggleShow, toggleShow
}); });
</script> </script>

View File

@ -4,23 +4,11 @@
:title="handleTitle()" :title="handleTitle()"
:needDialog="false" :needDialog="false"
:iconWidth="isUniFrameWork ? '32px' : '20px'" :iconWidth="isUniFrameWork ? '32px' : '20px'"
:iconHeight="isUniFrameWork :iconHeight="isUniFrameWork ? (props.videoSourceType === 'album' ? '20px' : '25px') : '18px'"
? props.videoSourceType === 'album'
? '20px'
: '25px'
: '18px'
"
@onIconClick="onIconClick" @onIconClick="onIconClick"
> >
<div :class="['video-upload', !isPC && 'video-upload-h5']"> <div :class="['video-upload', !isPC && 'video-upload-h5']">
<input <input ref="inputRef" title="视频" type="file" data-type="video" accept="video/*" @change="sendVideoInWeb" />
ref="inputRef"
title="视频"
type="file"
data-type="video"
accept="video/*"
@change="sendVideoInWeb"
>
</div> </div>
</ToolbarItemContainer> </ToolbarItemContainer>
</template> </template>
@ -31,7 +19,7 @@ import TUIChatEngine, {
StoreName, StoreName,
IConversationModel, IConversationModel,
SendMessageParams, SendMessageParams,
SendMessageOptions, SendMessageOptions
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref } from '../../../../adapter-vue'; import { ref } from '../../../../adapter-vue';
@ -51,8 +39,8 @@ const props = defineProps({
// camera: Take a video using the camera // camera: Take a video using the camera
videoSourceType: { videoSourceType: {
type: String, type: String,
default: 'album', default: 'album'
}, }
}); });
const inputRef = ref(); const inputRef = ref();
@ -61,7 +49,7 @@ const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => { currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation; currentConversation.value = conversation;
}, }
}); });
const handleIcon = (): string => { const handleIcon = (): string => {
@ -99,7 +87,7 @@ const onIconClick = () => {
maxDuration: 60, maxDuration: 60,
success: function (res: any) { success: function (res: any) {
sendVideoMessage(res); sendVideoMessage(res);
}, }
}); });
} else { } else {
TUIGlobal?.chooseVideo({ TUIGlobal?.chooseVideo({
@ -108,7 +96,7 @@ const onIconClick = () => {
compressed: false, compressed: false,
success: function (res: any) { success: function (res: any) {
sendVideoMessage(res); sendVideoMessage(res);
}, }
}); });
} }
} else { } else {
@ -129,27 +117,25 @@ const sendVideoMessage = (file: any) => {
return; return;
} }
const options = { const options = {
to: to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type, conversationType: currentConversation?.value?.type,
payload: { payload: {
file, file
}, },
needReadReceipt: isEnabledMessageReadReceiptGlobal(), needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams; } as SendMessageParams;
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = { const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value, conversation: currentConversation.value,
payload: options.payload, payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_VIDEO, messageType: TUIChatEngine.TYPES.MSG_VIDEO
}; };
const sendMessageOptions: SendMessageOptions = { const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams), offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams)
}; };
TUIChatService.sendVideoMessage(options, sendMessageOptions); TUIChatService.sendVideoMessage(options, sendMessageOptions);
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
</style> </style>

View File

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

View File

@ -12,23 +12,12 @@
<div :class="['words', !isPC && 'words-h5']"> <div :class="['words', !isPC && 'words-h5']">
<div :class="['words-header', !isPC && 'words-h5-header']"> <div :class="['words-header', !isPC && 'words-h5-header']">
<span :class="['words-header-title', !isPC && 'words-h5-header-title']"> <span :class="['words-header-title', !isPC && 'words-h5-header-title']">
{{ TUITranslateService.t("Words.常用语-快捷回复工具") }} {{ TUITranslateService.t('Words.常用语-快捷回复工具') }}
</span>
<span
v-if="!isPC"
:class="['words-header-close', !isPC && 'words-h5-header-close']"
@click="closeDialog"
>
关闭
</span> </span>
<span v-if="!isPC" :class="['words-header-close', !isPC && 'words-h5-header-close']" @click="closeDialog"> 关闭 </span>
</div> </div>
<ul :class="['words-list', !isPC && 'words-h5-list']"> <ul :class="['words-list', !isPC && 'words-h5-list']">
<li <li v-for="(item, index) in wordsList" :key="index" :class="['words-list-item', !isPC && 'words-h5-list-item']" @click="selectWord(item)">
v-for="(item, index) in wordsList"
:key="index"
:class="['words-list-item', !isPC && 'words-h5-list-item']"
@click="selectWord(item)"
>
{{ TUITranslateService.t(`Words.${item.value}`) }} {{ TUITranslateService.t(`Words.${item.value}`) }}
</li> </li>
</ul> </ul>
@ -36,14 +25,7 @@
</ToolbarItemContainer> </ToolbarItemContainer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import { TUITranslateService, TUIStore, StoreName, IConversationModel, SendMessageParams, TUIChatService } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
TUIStore,
StoreName,
IConversationModel,
SendMessageParams,
TUIChatService,
} from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../../adapter-vue'; import { ref } from '../../../../adapter-vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue'; import ToolbarItemContainer from '../toolbar-item-container/index.vue';
import wordsIconLight from '../../../../assets/icon/words-light.svg'; import wordsIconLight from '../../../../assets/icon/words-light.svg';
@ -61,19 +43,17 @@ const container = ref();
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => { currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation; currentConversation.value = conversation;
}, }
}); });
const selectWord = (item: any) => { const selectWord = (item: any) => {
const options = { const options = {
to: to: currentConversation?.value?.groupProfile?.groupID || currentConversation?.value?.userProfile?.userID,
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type, conversationType: currentConversation?.value?.type,
payload: { payload: {
text: TUITranslateService.t(`Words.${item.value}`), text: TUITranslateService.t(`Words.${item.value}`)
}, },
needReadReceipt: isEnabledMessageReadReceiptGlobal(), needReadReceipt: isEnabledMessageReadReceiptGlobal()
} as SendMessageParams; } as SendMessageParams;
TUIChatService.sendTextMessage(options); TUIChatService.sendTextMessage(options);
// close dialog after submit evaluate // close dialog after submit evaluate

View File

@ -14,27 +14,15 @@
@onTyping="onTyping" @onTyping="onTyping"
@onAt="onAt" @onAt="onAt"
/> />
<MessageInputButton <MessageInputButton v-if="!props.isMuted" @sendMessage="sendMessage" />
v-if="!props.isMuted" <MessageInputAt v-if="props.enableAt" ref="messageInputAtRef" @insertAt="insertAt" @onAtListOpen="onAtListOpen" />
@sendMessage="sendMessage"
/>
<MessageInputAt
v-if="props.enableAt"
ref="messageInputAtRef"
@insertAt="insertAt"
@onAtListOpen="onAtListOpen"
/>
</div> </div>
<MessageInputQuote /> <MessageInputQuote />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import { TUIStore, StoreName, IConversationModel } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../adapter-vue'; import { ref } from '../../../adapter-vue';
import MessageInputEditor from './message-input-editor.vue'; import MessageInputEditor from './message-input-editor.vue';
import MessageInputAt from './message-input-at/index.vue'; import MessageInputAt from './message-input-at/index.vue';
@ -47,32 +35,32 @@ import { isPC, isH5 } from '../../../utils/env';
const props = defineProps({ const props = defineProps({
placeholder: { placeholder: {
type: String, type: String,
default: 'this is placeholder', default: 'this is placeholder'
}, },
isMuted: { isMuted: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
muteText: { muteText: {
type: String, type: String,
default: '', default: ''
}, },
enableInput: { enableInput: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
enableAt: { enableAt: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
enableDragUpload: { enableDragUpload: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
enableTyping: { enableTyping: {
type: Boolean, type: Boolean,
default: true, default: true
}, }
}); });
const emit = defineEmits(['sendMessage', 'resetReplyOrReference', 'onTyping']); const emit = defineEmits(['sendMessage', 'resetReplyOrReference', 'onTyping']);
@ -83,7 +71,7 @@ const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => { currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation; currentConversation.value = conversation;
}, }
}); });
const onTyping = (inputContentEmpty: boolean, inputBlur: boolean) => { const onTyping = (inputContentEmpty: boolean, inputBlur: boolean) => {
@ -103,10 +91,7 @@ const sendMessage = async () => {
} }
return editor; return editor;
}); });
await sendMessages( await sendMessages(editorContentList, currentConversation.value);
editorContentList,
currentConversation.value,
);
emit('sendMessage'); emit('sendMessage');
editor.value?.resetEditor(); editor.value?.resetEditor();
}; };
@ -132,12 +117,12 @@ const reEdit = (content: any) => {
defineExpose({ defineExpose({
insertEmoji, insertEmoji,
reEdit, reEdit
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "../../../assets/styles/common"; @import '../../../assets/styles/common';
.message-input-wrapper { .message-input-wrapper {
box-sizing: border-box; box-sizing: border-box;

View File

@ -1,8 +1,5 @@
import { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion'; import { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion';
import TUIChatEngine, { import TUIChatEngine, { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import { isH5 } from '../../../../utils/env'; import { isH5 } from '../../../../utils/env';
@ -13,17 +10,13 @@ let showMemberList: any[] = [];
let selectedIndex: number = 0; let selectedIndex: number = 0;
let isGroup = false; let isGroup = false;
let command: let command:
| ((props: { | ((props: { id?: string | undefined; userID?: string | undefined; isAll?: boolean | undefined }) => void)
id?: string | undefined;
userID?: string | undefined;
isAll?: boolean | undefined;
}) => void)
| ((arg0: { id: any; label: any }) => any); | ((arg0: { id: any; label: any }) => any);
const all = { const all = {
userID: TUIChatEngine.TYPES.MSG_AT_ALL, userID: TUIChatEngine.TYPES.MSG_AT_ALL,
nick: '所有人', nick: '所有人',
isAll: true, isAll: true,
avatar: 'https://web.sdk.qcloud.com/im/assets/images/at.svg', avatar: 'https://web.sdk.qcloud.com/im/assets/images/at.svg'
}; };
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
@ -40,7 +33,7 @@ TUIStore.watch(StoreName.CONV, {
showMemberList = []; showMemberList = [];
} }
} }
}, }
}); });
TUIStore.watch(StoreName.CUSTOM, { TUIStore.watch(StoreName.CUSTOM, {
@ -50,7 +43,7 @@ TUIStore.watch(StoreName.CUSTOM, {
allMemberList = [all, ...memberList]; allMemberList = [all, ...memberList];
showMemberList = allMemberList; showMemberList = allMemberList;
} }
}, }
}); });
const MessageInputAtSuggestion = () => { const MessageInputAtSuggestion = () => {
@ -62,8 +55,7 @@ const MessageInputAtSuggestion = () => {
} }
const queryResult = allMemberList?.filter( const queryResult = allMemberList?.filter(
(item: { nick: string; userID: string }) => (item: { nick: string; userID: string }) =>
item?.nick?.toLowerCase()?.startsWith(props?.query?.toLowerCase()) item?.nick?.toLowerCase()?.startsWith(props?.query?.toLowerCase()) || item?.userID?.toLowerCase()?.startsWith(props?.query?.toLowerCase())
|| item?.userID?.toLowerCase()?.startsWith(props?.query?.toLowerCase()),
); );
showMemberList = queryResult?.length ? queryResult : allMemberList; showMemberList = queryResult?.length ? queryResult : allMemberList;
TUIGlobal.setShowMemberList(showMemberList); TUIGlobal.setShowMemberList(showMemberList);
@ -76,7 +68,7 @@ const MessageInputAtSuggestion = () => {
id?: string; id?: string;
userID?: string; userID?: string;
isAll?: boolean; isAll?: boolean;
}>, }>
) => { ) => {
if (!isGroup) { if (!isGroup) {
return; return;
@ -89,7 +81,7 @@ const MessageInputAtSuggestion = () => {
if (rect?.left && rect?.top && !isH5) { if (rect?.left && rect?.top && !isH5) {
TUIGlobal.handleAtListPosition({ TUIGlobal.handleAtListPosition({
left: rect?.left, left: rect?.left,
top: rect?.top, top: rect?.top
}); });
} }
command = props.command; command = props.command;
@ -106,7 +98,7 @@ const MessageInputAtSuggestion = () => {
if (rect?.left && rect?.top && !isH5) { if (rect?.left && rect?.top && !isH5) {
TUIGlobal.handleAtListPosition({ TUIGlobal.handleAtListPosition({
left: rect?.left, left: rect?.left,
top: rect?.top, top: rect?.top
}); });
} }
}, },
@ -147,18 +139,17 @@ const MessageInputAtSuggestion = () => {
showMemberList = allMemberList; showMemberList = allMemberList;
TUIGlobal.handleAtListPosition({ TUIGlobal.handleAtListPosition({
left: 0, left: 0,
top: 0, top: 0
}); });
}, }
}; };
}, }
}; };
}; };
const upHandler = () => { const upHandler = () => {
if (!showMemberList?.length) return; if (!showMemberList?.length) return;
selectedIndex selectedIndex = (selectedIndex + showMemberList?.length - 1) % showMemberList?.length;
= (selectedIndex + showMemberList?.length - 1) % showMemberList?.length;
TUIGlobal.setCurrentSelectIndex(selectedIndex); TUIGlobal.setCurrentSelectIndex(selectedIndex);
}; };
@ -176,11 +167,11 @@ const selectItem = (index: number) => {
if (!showMemberList?.length) return; if (!showMemberList?.length) return;
const item = showMemberList[index]; const item = showMemberList[index];
if (item) { if (item) {
command command &&
&& command({ command({
id: (item as any)?.userID, id: (item as any)?.userID,
label: (item as any)?.nick || (item as any)?.userID, label: (item as any)?.nick || (item as any)?.userID
}); });
} }
}; };
TUIGlobal.selectItem = selectItem; TUIGlobal.selectItem = selectItem;

View File

@ -1,23 +1,9 @@
<template> <template>
<BottomPopup <BottomPopup :show="showAtList" @onClose="closeAt">
:show="showAtList" <div ref="MessageInputAt" :class="[isPC ? 'message-input-at' : 'message-input-at-h5']">
@onClose="closeAt" <div ref="dialog" class="member-list">
> <header v-if="!isPC" class="member-list-title">
<div <span class="title">{{ TUITranslateService.t('TUIChat.选择提醒的人') }}</span>
ref="MessageInputAt"
:class="[isPC ? 'message-input-at' : 'message-input-at-h5']"
>
<div
ref="dialog"
class="member-list"
>
<header
v-if="!isPC"
class="member-list-title"
>
<span class="title">{{
TUITranslateService.t("TUIChat.选择提醒的人")
}}</span>
</header> </header>
<ul class="member-list-box"> <ul class="member-list-box">
<li <li
@ -28,10 +14,7 @@
:class="[index === selectedIndex && 'selected']" :class="[index === selectedIndex && 'selected']"
@click="selectItem(index)" @click="selectItem(index)"
> >
<img <img class="member-list-box-body-avatar" :src="handleMemberAvatar(item)" />
class="member-list-box-body-avatar"
:src="handleMemberAvatar(item)"
>
<span class="member-list-box-body-name"> <span class="member-list-box-body-name">
{{ handleMemberName(item) }} {{ handleMemberName(item) }}
</span> </span>
@ -42,12 +25,7 @@
</BottomPopup> </BottomPopup>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import TUIChatEngine, { import TUIChatEngine, { TUIStore, StoreName, TUIGroupService, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
TUIGroupService,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, watch } from '../../../../adapter-vue'; import { ref, watch } from '../../../../adapter-vue';
import { isPC, isH5 } from '../../../../utils/env'; import { isPC, isH5 } from '../../../../utils/env';
@ -65,7 +43,7 @@ const showMemberList = ref<Array<any>>();
const isGroup = ref(false); const isGroup = ref(false);
const position = ref({ const position = ref({
left: 0, left: 0,
top: 0, top: 0
}); });
const selectedIndex = ref(0); const selectedIndex = ref(0);
const currentConversationID = ref(''); const currentConversationID = ref('');
@ -74,7 +52,7 @@ const all = {
userID: TUIChatEngine.TYPES.MSG_AT_ALL, userID: TUIChatEngine.TYPES.MSG_AT_ALL,
nick: '所有人', nick: '所有人',
isAll: true, isAll: true,
avatar: 'https://web.sdk.qcloud.com/im/assets/images/at.svg', avatar: 'https://web.sdk.qcloud.com/im/assets/images/at.svg'
}; };
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
@ -94,7 +72,7 @@ TUIStore.watch(StoreName.CONV, {
TUIGroupService.switchGroup(''); TUIGroupService.switchGroup('');
} }
} }
}, }
}); });
TUIStore.watch(StoreName.GRP, { TUIStore.watch(StoreName.GRP, {
@ -103,7 +81,7 @@ TUIStore.watch(StoreName.GRP, {
allMemberList.value = [all, ...memberList.value]; allMemberList.value = [all, ...memberList.value];
showMemberList.value = allMemberList.value; showMemberList.value = allMemberList.value;
TUIStore.update(StoreName.CUSTOM, 'memberList', memberList.value); TUIStore.update(StoreName.CUSTOM, 'memberList', memberList.value);
}, }
}); });
const toggleAtList = (show: boolean) => { const toggleAtList = (show: boolean) => {
@ -132,7 +110,7 @@ TUIGlobal.setCurrentSelectIndex = setCurrentSelectIndex;
TUIGlobal.setShowMemberList = setShowMemberList; TUIGlobal.setShowMemberList = setShowMemberList;
defineExpose({ defineExpose({
toggleAtList, toggleAtList
}); });
watch( watch(
@ -142,9 +120,8 @@ watch(
return; return;
} }
MessageInputAt.value.style.left = position.value.left + 'px'; MessageInputAt.value.style.left = position.value.left + 'px';
MessageInputAt.value.style.top MessageInputAt.value.style.top = position.value.top - MessageInputAt.value.clientHeight + 'px';
= position.value.top - MessageInputAt.value.clientHeight + 'px'; }
},
); );
const closeAt = () => { const closeAt = () => {
@ -152,7 +129,7 @@ const closeAt = () => {
showMemberList.value = allMemberList.value; showMemberList.value = allMemberList.value;
position.value = { position.value = {
left: 0, left: 0,
top: 0, top: 0
}; };
}; };
@ -164,7 +141,7 @@ const selectItem = (index: number) => {
const item = showMemberList?.value[index]; const item = showMemberList?.value[index];
emits('insertAt', { emits('insertAt', {
id: (item as any)?.userID, id: (item as any)?.userID,
label: (item as any)?.nick || (item as any)?.userID, label: (item as any)?.nick || (item as any)?.userID
}); });
} }
} }
@ -172,10 +149,7 @@ const selectItem = (index: number) => {
}; };
const handleMemberAvatar = (item: any) => { const handleMemberAvatar = (item: any) => {
return ( return (item as any)?.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png';
(item as any)?.avatar
|| 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
);
}; };
const handleMemberName = (item: any) => { const handleMemberName = (item: any) => {
@ -183,7 +157,7 @@ const handleMemberName = (item: any) => {
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.message-input-at { .message-input-at {
position: fixed; position: fixed;
@ -191,7 +165,7 @@ const handleMemberName = (item: any) => {
max-height: 10rem; max-height: 10rem;
overflow: hidden auto; overflow: hidden auto;
background: #fff; background: #fff;
box-shadow: 0 0.06rem 0.63rem 0 rgba(2,16,43,0.15); box-shadow: 0 0.06rem 0.63rem 0 rgba(2, 16, 43, 0.15);
border-radius: 0.13rem; border-radius: 0.13rem;
} }
@ -202,7 +176,7 @@ const handleMemberName = (item: any) => {
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: rgba(0,110,255,0.1); background: rgba(0, 110, 255, 0.1);
} }
} }
@ -223,7 +197,7 @@ const handleMemberName = (item: any) => {
.selected, .selected,
&:hover { &:hover {
background: rgba(0,110,255,0.1); background: rgba(0, 110, 255, 0.1);
} }
&-name { &-name {
@ -242,7 +216,7 @@ const handleMemberName = (item: any) => {
} }
.selected { .selected {
background: rgba(0,110,255,0.1); background: rgba(0, 110, 255, 0.1);
} }
} }

View File

@ -1,19 +1,10 @@
<template> <template>
<div :class="['message-input-button', !isPC && 'message-input-button-h5']"> <div :class="['message-input-button', !isPC && 'message-input-button-h5']">
<button <button v-if="props.enableSend" class="message-input-button-cont" data-type="text" :disabled="false" @click="sendMessage">
v-if="props.enableSend" <p v-if="displayHover" class="message-input-button-hover">
class="message-input-button-cont" {{ TUITranslateService.t('TUIChat.按Enter发送Ctrl+Enter换行') }}
data-type="text"
:disabled="false"
@click="sendMessage"
>
<p
v-if="displayHover"
class="message-input-button-hover"
>
{{ TUITranslateService.t("TUIChat.按Enter发送Ctrl+Enter换行") }}
</p> </p>
{{ TUITranslateService.t("发送") }} {{ TUITranslateService.t('发送') }}
</button> </button>
</div> </div>
</template> </template>
@ -27,8 +18,8 @@ import TUIChatConfig from '../config';
const props = defineProps({ const props = defineProps({
enableSend: { enableSend: {
type: Boolean, type: Boolean,
default: true, default: true
}, }
}); });
const displayHover = ref(TUIChatConfig.getChatType() !== TUIConstants.TUIChat.TYPE.ROOM); const displayHover = ref(TUIChatConfig.getChatType() !== TUIConstants.TUIChat.TYPE.ROOM);
@ -40,7 +31,7 @@ const sendMessage = () => {
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "../../../assets/styles/common"; @import '../../../assets/styles/common';
.message-input-button { .message-input-button {
position: absolute; position: absolute;
@ -84,7 +75,7 @@ const sendMessage = () => {
opacity: 0.3; opacity: 0.3;
&::before { &::before {
content: ""; content: '';
position: absolute; position: absolute;
width: 0; width: 0;
height: 0; height: 0;

View File

@ -1,11 +1,6 @@
<template> <template>
<div <div :class="['message-input-editor-container', isH5 && 'message-input-editor-container-h5']">
:class="['message-input-editor-container', isH5 && 'message-input-editor-container-h5']" <div v-if="isMuted" class="message-input-mute">
>
<div
v-if="isMuted"
class="message-input-mute"
>
{{ muteText }} {{ muteText }}
</div> </div>
<div <div
@ -24,11 +19,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { toRefs, ref, onMounted, watch, onUnmounted } from '../../../adapter-vue'; import { toRefs, ref, onMounted, watch, onUnmounted } from '../../../adapter-vue';
import { import { TUIStore, StoreName, IMessageModel } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
IMessageModel,
} from '@tencentcloud/chat-uikit-engine';
import { Editor, JSONContent, Extension } from '@tiptap/core'; import { Editor, JSONContent, Extension } from '@tiptap/core';
import Document from '@tiptap/extension-document'; import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph'; import Paragraph from '@tiptap/extension-paragraph';
@ -47,36 +38,36 @@ import DraftManager from '../utils/conversationDraft';
const props = defineProps({ const props = defineProps({
placeholder: { placeholder: {
type: String, type: String,
default: 'this is placeholder', default: 'this is placeholder'
}, },
replayOrReferenceMessage: { replayOrReferenceMessage: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, },
isMuted: { isMuted: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
muteText: { muteText: {
type: String, type: String,
default: '', default: ''
}, },
enableInput: { enableInput: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
enableAt: { enableAt: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
enableDragUpload: { enableDragUpload: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
enableTyping: { enableTyping: {
type: Boolean, type: Boolean,
default: true, default: true
}, }
}); });
const emits = defineEmits(['sendMessage', 'onTyping', 'onAt']); const emits = defineEmits(['sendMessage', 'onTyping', 'onAt']);
@ -92,9 +83,9 @@ const fileMap = new Map<string, any>();
const DisableDefaultEnter = Extension.create({ const DisableDefaultEnter = Extension.create({
addKeyboardShortcuts() { addKeyboardShortcuts() {
return { return {
Enter: () => true, Enter: () => true
}; };
}, }
}); });
function onCurrentConversationIDUpdated(conversationID: string) { function onCurrentConversationIDUpdated(conversationID: string) {
@ -104,7 +95,7 @@ function onCurrentConversationIDUpdated(conversationID: string) {
currentConversationID.value, currentConversationID.value,
getEditorHTML(), getEditorHTML(),
DraftManager.generateAbstract(getEditorContent()), DraftManager.generateAbstract(getEditorContent()),
currentQuoteMessage.value, currentQuoteMessage.value
); );
} }
resetEditor(); resetEditor();
@ -122,94 +113,90 @@ function onQuoteMessageUpdated(options?: { message: IMessageModel; type: string
onMounted(() => { onMounted(() => {
editor = isPC editor = isPC
? new Editor({ ? new Editor({
element: editorDom.value, element: editorDom.value,
extensions: [ extensions: [
Document, Document,
Paragraph, Paragraph,
Text, Text,
HardBreak, HardBreak,
DisableDefaultEnter, DisableDefaultEnter,
Placeholder.configure({ Placeholder.configure({
emptyEditorClass: 'is-editor-empty', emptyEditorClass: 'is-editor-empty',
placeholder: placeholder.value, placeholder: placeholder.value
}), }),
Mention.configure({ Mention.configure({
HTMLAttributes: { HTMLAttributes: {
class: 'mention', class: 'mention'
}, },
suggestion: enableAt.value && (MessageInputAtSuggestion() as any), suggestion: enableAt.value && (MessageInputAtSuggestion() as any)
}), }),
CustomImage.configure({ CustomImage.configure({
inline: true, inline: true,
allowBase64: true, allowBase64: true
}), })
], ],
autofocus: !isH5, autofocus: !isH5,
editable: true, editable: true,
injectCSS: false, injectCSS: false,
editorProps: { editorProps: {
handlePaste() { handlePaste() {
// prevent editor's default paste for resolve emoji & marked down line break // prevent editor's default paste for resolve emoji & marked down line break
return true; return true;
}
}, },
}, // handle input editor typing (only in C2C and enable typing)
// handle input editor typing (only in C2C and enable typing) onUpdate({ editor, transaction }) {
onUpdate({ editor, transaction }) { if (!enableTyping.value || !isC2C.value) return;
if (!enableTyping.value || !isC2C.value) return; isEditorBlur.value = !editor.isFocused;
isEditorBlur.value = !editor.isFocused; if (transaction?.doc?.content?.size > 2) {
if (transaction?.doc?.content?.size > 2) { isEditorEmpty.value = false;
isEditorEmpty.value = false; } else {
} else { isEditorEmpty.value = true;
isEditorEmpty.value = true; }
},
onFocus() {
if (isH5 && document?.getElementById('app')?.style) {
// set app height when keyboard popup
const keyboardHeight = document.body.scrollHeight - window.innerHeight;
(document.getElementById('app') as any).style.marginBottom = `${keyboardHeight}px`;
(document.getElementById('app') as any).style.height = `calc(100% - ${keyboardHeight}px)`;
}
if (!enableTyping.value || !isC2C.value) return;
isEditorBlur.value = true;
},
onBlur() {
if (isH5 && document?.getElementById('app')?.style) {
// reset app height to normal
(document.getElementById('app') as any).style.marginBottom = ``;
(document.getElementById('app') as any).style.height = `100%`;
}
if (!enableTyping.value || !isC2C.value) return;
isEditorBlur.value = true;
} }
}, })
onFocus() {
if (isH5 && document?.getElementById('app')?.style) {
// set app height when keyboard popup
const keyboardHeight = document.body.scrollHeight - window.innerHeight;
(
document.getElementById('app') as any
).style.marginBottom = `${keyboardHeight}px`;
(
document.getElementById('app') as any
).style.height = `calc(100% - ${keyboardHeight}px)`;
}
if (!enableTyping.value || !isC2C.value) return;
isEditorBlur.value = true;
},
onBlur() {
if (isH5 && document?.getElementById('app')?.style) {
// reset app height to normal
(document.getElementById('app') as any).style.marginBottom = ``;
(document.getElementById('app') as any).style.height = `100%`;
}
if (!enableTyping.value || !isC2C.value) return;
isEditorBlur.value = true;
},
})
: null; : null;
if (isH5) { if (isH5) {
const targetBottomDom = document.querySelector('.message-input-toolbar') as HTMLElement || editorDom.value; const targetBottomDom = (document.querySelector('.message-input-toolbar') as HTMLElement) || editorDom.value;
riseInput(editorDom.value, targetBottomDom); riseInput(editorDom.value, targetBottomDom);
} }
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated
}); });
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated, quoteMessage: onQuoteMessageUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, { TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated
}); });
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated, quoteMessage: onQuoteMessageUpdated
}); });
// clear map store // clear map store
@ -289,25 +276,19 @@ async function handleFileDropOrPaste(e: any, type: string) {
if (!enableDragUpload.value && type === 'drop') { if (!enableDragUpload.value && type === 'drop') {
return; return;
} }
if ( if ((type === 'drop' && e.dataTransfer) || (type === 'paste' && e.clipboardData)) {
(type === 'drop' && e.dataTransfer) const files = type === 'drop' ? e?.dataTransfer?.files : e?.clipboardData?.files;
|| (type === 'paste' && e.clipboardData)
) {
const files
= type === 'drop' ? e?.dataTransfer?.files : e?.clipboardData?.files;
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = files[i]; const file = files[i];
const isImage = file.type.startsWith('image/'); const isImage = file.type.startsWith('image/');
const fileSrc = isImage const fileSrc = isImage ? URL.createObjectURL(file) : await drawFileCanvasToImageUrl(file);
? URL.createObjectURL(file)
: await drawFileCanvasToImageUrl(file);
editor?.commands?.insertContent({ editor?.commands?.insertContent({
type: 'custom-image', type: 'custom-image',
attrs: { attrs: {
src: fileSrc, src: fileSrc,
alt: file?.name, alt: file?.name,
class: isImage ? 'normal' : 'file', class: isImage ? 'normal' : 'file'
}, }
}); });
fileMap.set(fileSrc, file); fileMap.set(fileSrc, file);
if (i === files.length - 1) { if (i === files.length - 1) {
@ -324,7 +305,7 @@ async function handleFileDropOrPaste(e: any, type: string) {
* create file icon image * create file icon image
* To avoid creating img dom with the same icon repeatedly, record the previous type of icon that has been created. * To avoid creating img dom with the same icon repeatedly, record the previous type of icon that has been created.
* The format of the record is map<icon type, img dom>. * The format of the record is map<icon type, img dom>.
*/ */
const fileIconDomMap = new Map<string, HTMLImageElement>(); const fileIconDomMap = new Map<string, HTMLImageElement>();
const addImageProcess = (src: string, type: string) => { const addImageProcess = (src: string, type: string) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -375,18 +356,7 @@ const drawFileCanvasToImageUrl = async (file: any) => {
const handleFileIconForShow = (type: string) => { const handleFileIconForShow = (type: string) => {
const urlBase = 'https://web.sdk.qcloud.com/component/TUIKit/assets/file-'; const urlBase = 'https://web.sdk.qcloud.com/component/TUIKit/assets/file-';
const fileTypes = [ const fileTypes = ['image', 'pdf', 'text', 'ppt', 'presentation', 'sheet', 'zip', 'word', 'video', 'unknown'];
'image',
'pdf',
'text',
'ppt',
'presentation',
'sheet',
'zip',
'word',
'video',
'unknown',
];
let url = ''; let url = '';
let iconType = ''; let iconType = '';
fileTypes.forEach((typeName: string) => { fileTypes.forEach((typeName: string) => {
@ -397,7 +367,7 @@ const handleFileIconForShow = (type: string) => {
}); });
return { return {
iconSrc: url ? url : urlBase + 'unknown.svg', iconSrc: url ? url : urlBase + 'unknown.svg',
iconType: iconType ? iconType : 'unknown', iconType: iconType ? iconType : 'unknown'
}; };
}; };
@ -431,16 +401,13 @@ function parsePCEditorContent(): ITipTapEditorContent[] {
const content: ITipTapEditorContent[] = []; const content: ITipTapEditorContent[] = [];
handleEditorContent(editorJSON, content); handleEditorContent(editorJSON, content);
if ( if (
content.length > 0 content.length > 0 &&
&& content[content.length - 1] content[content.length - 1] &&
&& content[content.length - 1].type === 'text' content[content.length - 1].type === 'text' &&
&& content[content.length - 1].payload?.text?.endsWith('\n') content[content.length - 1].payload?.text?.endsWith('\n')
) { ) {
const text = content[content.length - 1].payload.text || ''; const text = content[content.length - 1].payload.text || '';
content[content.length - 1].payload.text = text?.substring( content[content.length - 1].payload.text = text?.substring(0, text.lastIndexOf('\n'));
0,
text.lastIndexOf('\n'),
);
} }
return content; return content;
} }
@ -449,11 +416,7 @@ function handleEditorContent(root: JSONContent, content: ITipTapEditorContent[])
if (!root || !root.type) { if (!root || !root.type) {
return; return;
} }
if ( if (root.type !== 'text' && root.type !== 'custom-image' && root.type !== 'mention') {
root.type !== 'text'
&& root.type !== 'custom-image'
&& root.type !== 'mention'
) {
if (root.type === 'paragraph' || root.type === 'hardBreak') { if (root.type === 'paragraph' || root.type === 'hardBreak') {
handleEditorNode(root, content); handleEditorNode(root, content);
} }
@ -471,69 +434,49 @@ function handleEditorContent(root: JSONContent, content: ITipTapEditorContent[])
function handleEditorNode(node: JSONContent, content: ITipTapEditorContent[]) { function handleEditorNode(node: JSONContent, content: ITipTapEditorContent[]) {
// handle enter // handle enter
if (node.type === 'hardBreak') { if (node.type === 'hardBreak') {
if (content.length > 0 if (content.length > 0 && content[content.length - 1] && content[content.length - 1]?.type === 'text') {
&& content[content.length - 1]
&& content[content.length - 1]?.type === 'text'
) {
content[content.length - 1].payload.text += '\n'; content[content.length - 1].payload.text += '\n';
} else { } else {
content.push({ content.push({
type: 'text', type: 'text',
payload: { text: '\n' }, payload: { text: '\n' }
}); });
} }
} else if (node.type === 'paragraph') { } else if (node.type === 'paragraph') {
if ( if (content.length > 0 && content[content.length - 1] && content[content.length - 1]?.type === 'text') {
content.length > 0
&& content[content.length - 1]
&& content[content.length - 1]?.type === 'text'
) {
content[content.length - 1].payload.text += '\n'; content[content.length - 1].payload.text += '\n';
} }
} else if ( } else if (node.type === 'text' || (node.type === 'custom-image' && node?.attrs?.class?.includes('emoji'))) {
node.type === 'text'
|| (node.type === 'custom-image' && node?.attrs?.class?.includes('emoji'))
) {
// Process text and emojis // Process text and emojis
const text = node.type === 'text' ? node?.text : node?.attrs?.alt; const text = node.type === 'text' ? node?.text : node?.attrs?.alt;
if ( if (content.length > 0 && content[content.length - 1] && content[content.length - 1]?.type === 'text') {
content.length > 0
&& content[content.length - 1]
&& content[content.length - 1]?.type === 'text'
) {
content[content.length - 1].payload.text += text; content[content.length - 1].payload.text += text;
} else { } else {
content.push({ content.push({
type: 'text', type: 'text',
payload: { text: text }, payload: { text: text }
}); });
} }
} else if ( } else if (node.type === 'custom-image' && node?.attrs?.class?.includes('normal')) {
node.type === 'custom-image' && node?.attrs?.class?.includes('normal')
) {
// Process rich text images // Process rich text images
content.push({ content.push({
type: 'image', type: 'image',
payload: { file: fileMap?.get(node?.attrs?.src) }, payload: { file: fileMap?.get(node?.attrs?.src) }
}); });
} else if (node.type === 'custom-image' && node?.attrs?.class?.includes('file')) { } else if (node.type === 'custom-image' && node?.attrs?.class?.includes('file')) {
const file = fileMap?.get(node?.attrs?.src); const file = fileMap?.get(node?.attrs?.src);
content.push({ content.push({
type: file?.type?.includes('video') ? 'video' : 'file', type: file?.type?.includes('video') ? 'video' : 'file',
payload: { file }, payload: { file }
}); });
} else if (node.type === 'mention') { } else if (node.type === 'mention') {
const text = '@' + node?.attrs?.label + ' '; const text = '@' + node?.attrs?.label + ' ';
if ( if (content.length > 0 && content[content.length - 1] && content[content.length - 1]?.type === 'text') {
content.length > 0
&& content[content.length - 1]
&& content[content.length - 1]?.type === 'text'
) {
content[content.length - 1].payload.text += text; content[content.length - 1].payload.text += text;
} else { } else {
content.push({ content.push({
type: 'text', type: 'text',
payload: { text: text }, payload: { text: text }
}); });
} }
if (content[content.length - 1]?.payload?.atUserList) { if (content[content.length - 1]?.payload?.atUserList) {
@ -551,10 +494,10 @@ function parseH5EditorContent() {
try { try {
for (const child of root.childNodes) { for (const child of root.childNodes) {
if ( if (
child.nodeName === '#text' child.nodeName === '#text' ||
|| child.nodeName === 'SPAN' child.nodeName === 'SPAN' ||
|| (child as HTMLElement).classList?.contains('custom-image-emoji') (child as HTMLElement).classList?.contains('custom-image-emoji') ||
|| (child as HTMLElement).classList?.contains('mention') (child as HTMLElement).classList?.contains('mention')
) { ) {
text += child.nodeValue || (child as any).alt || child.innerHTML || ''; text += child.nodeValue || (child as any).alt || child.innerHTML || '';
if (child.classList?.contains('mention') && child.id && !atUserList?.includes(child.id)) { if (child.classList?.contains('mention') && child.id && !atUserList?.includes(child.id)) {
@ -572,9 +515,9 @@ function parseH5EditorContent() {
type: 'text', type: 'text',
payload: { payload: {
text, text,
atUserList, atUserList
}, }
}, }
]; ];
} }
@ -586,8 +529,8 @@ function addEmoji(emojiData: any) {
src: emojiData?.url, src: emojiData?.url,
alt: emojiData?.emoji.key, alt: emojiData?.emoji.key,
title: emojiData?.emoji.key, title: emojiData?.emoji.key,
class: 'emoji', class: 'emoji'
}, }
}); });
} else { } else {
const emojiImgNode = document.createElement('img'); const emojiImgNode = document.createElement('img');
@ -714,8 +657,8 @@ watch(
}, },
{ {
immediate: true, immediate: true,
deep: true, deep: true
}, }
); );
defineExpose({ defineExpose({
@ -726,12 +669,12 @@ defineExpose({
setEditorContent, setEditorContent,
getEditorHTML, getEditorHTML,
insertEditorContent, insertEditorContent,
blur, blur
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "../../../assets/styles/common"; @import '../../../assets/styles/common';
.message-input-editor { .message-input-editor {
&-container { &-container {
@ -833,7 +776,6 @@ defineExpose({
} }
img { img {
/* stylelint-disable-next-line selector-class-pattern */ /* stylelint-disable-next-line selector-class-pattern */
&.ProseMirror-selectednode { &.ProseMirror-selectednode {
outline: 2px solid #68cef8; outline: 2px solid #68cef8;

View File

@ -8,31 +8,30 @@ export default Image.extend({
...(Image.config as any).addAttributes(), ...(Image.config as any).addAttributes(),
class: { class: {
default: 'image', default: 'image',
rendered: false, rendered: false
}, }
}; };
}, },
addCommands() { addCommands() {
return { return {
setImage: options => ({ tr, commands }) => { setImage:
if ((tr.selection as any)?.node?.type?.name == 'custom-image') { (options) =>
return commands.updateAttributes('custom-image', options); ({ tr, commands }) => {
} else { if ((tr.selection as any)?.node?.type?.name == 'custom-image') {
return commands.insertContent({ return commands.updateAttributes('custom-image', options);
type: this.name, } else {
attrs: options, return commands.insertContent({
}); type: this.name,
attrs: options
});
}
} }
},
}; };
}, },
renderHTML({ node, HTMLAttributes }) { renderHTML({ node, HTMLAttributes }) {
HTMLAttributes.class = (node.attrs.class?.includes('custom-image-') ? '' : 'custom-image-') + node.attrs.class; HTMLAttributes.class = (node.attrs.class?.includes('custom-image-') ? '' : 'custom-image-') + node.attrs.class;
return [ return ['img', HTMLAttributes];
'img', }
HTMLAttributes,
];
},
}); });

View File

@ -4,32 +4,19 @@
:class="{ :class="{
'input-quote-container': true, 'input-quote-container': true,
'input-quote-container-uni': isUniFrameWork, 'input-quote-container-uni': isUniFrameWork,
'input-quote-container-h5': isH5, 'input-quote-container-h5': isH5
}" }"
> >
<div class="input-quote-content"> <div class="input-quote-content">
<div class="max-one-line"> <div class="max-one-line">{{ quoteMessage.nick || quoteMessage.from }}: {{ quoteContentText }}</div>
{{ quoteMessage.nick || quoteMessage.from }}: {{ quoteContentText }} <Icon class="input-quote-close-icon" :file="closeIcon" width="11px" height="11px" @onClick="cancelQuote" />
</div>
<Icon
class="input-quote-close-icon"
:file="closeIcon"
width="11px"
height="11px"
@onClick="cancelQuote"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from '../../../../adapter-vue'; import { ref, computed, onMounted, onUnmounted } from '../../../../adapter-vue';
import TUIChatEngine, { import TUIChatEngine, { TUIStore, StoreName, TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
TUITranslateService,
IMessageModel,
} from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../common/Icon.vue'; import Icon from '../../../common/Icon.vue';
import closeIcon from '../../../../assets/icon/icon-close.svg'; import closeIcon from '../../../../assets/icon/icon-close.svg';
import { isH5, isUniFrameWork } from '../../../../utils/env'; import { isH5, isUniFrameWork } from '../../../../utils/env';
@ -41,7 +28,7 @@ interface IProps {
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
displayType: 'editor', displayType: 'editor'
}); });
const TYPES = TUIChatEngine.TYPES; const TYPES = TUIChatEngine.TYPES;
@ -49,13 +36,13 @@ const quoteMessage = ref<IMessageModel>();
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated, quoteMessage: onQuoteMessageUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated, quoteMessage: onQuoteMessageUpdated
}); });
}); });

View File

@ -1,60 +1,28 @@
<template> <template>
<div <div class="tui-chat" :class="[isH5 ? 'tui-chat-h5' : '']">
class="tui-chat"
:class="[isH5 ? 'tui-chat-h5' : '']"
>
<!-- <JoinGroupCard v-if="isH5" /> --> <!-- <JoinGroupCard v-if="isH5" /> -->
<div <div id="tui-chat-main" class="tui-chat-main" @click="closeChatPop">
id="tui-chat-main"
class="tui-chat-main"
@click="closeChatPop"
>
<!-- Safe Tips --> <!-- Safe Tips -->
<div <div v-if="isOfficial" class="tui-chat-safe-tips">
v-if="isOfficial"
class="tui-chat-safe-tips"
>
<span> <span>
{{ {{
TUITranslateService.t( TUITranslateService.t(
"TUIChat.【安全提示】本 APP 仅用于体验腾讯云即时通信 IM 产品功能,不可用于业务洽谈与拓展。请勿轻信汇款、中奖等涉及钱款的信息,勿轻易拨打陌生电话,谨防上当受骗。" 'TUIChat.【安全提示】本 APP 仅用于体验腾讯云即时通信 IM 产品功能,不可用于业务洽谈与拓展。请勿轻信汇款、中奖等涉及钱款的信息,勿轻易拨打陌生电话,谨防上当受骗。'
) )
}} }}
</span> </span>
<a @click="openComplaintLink(Link.complaint)"> <a @click="openComplaintLink(Link.complaint)">
{{ TUITranslateService.t("TUIChat.点此投诉") }} {{ TUITranslateService.t('TUIChat.点此投诉') }}
</a> </a>
</div> </div>
<MessageGroupApplication <MessageGroupApplication v-if="isGroup" :key="props.groupID" :groupID="props.groupID" />
v-if="isGroup"
:key="props.groupID"
:groupID="props.groupID"
/>
<!-- Message List --> <!-- Message List -->
<ul <ul id="messageScrollList" ref="messageListRef" class="tui-message-list" @click="onMessageListBackgroundClick">
id="messageScrollList" <p v-if="!isCompleted" class="message-more" @click="getHistoryMessageList">
ref="messageListRef" {{ TUITranslateService.t('TUIChat.查看更多') }}
class="tui-message-list"
@click="onMessageListBackgroundClick"
>
<p
v-if="!isCompleted"
class="message-more"
@click="getHistoryMessageList"
>
{{ TUITranslateService.t("TUIChat.查看更多") }}
</p> </p>
<li <li v-for="(item, index) in messageList" :id="'tui-' + item.ID" :key="item.ID" ref="messageElementListRef" class="message-li">
v-for="(item, index) in messageList" <MessageTimestamp :currTime="item.time" :prevTime="index > 0 ? messageList[index - 1].time : 0" />
:id="'tui-' + item.ID"
:key="item.ID"
ref="messageElementListRef"
class="message-li"
>
<MessageTimestamp
:currTime="item.time"
:prevTime="index > 0 ? messageList[index - 1].time : 0"
/>
<div class="message-item"> <div class="message-item">
<MessageTip <MessageTip
v-if="item.type === TYPES.MSG_GRP_TIP || isCreateGroupCustomMessage(item)" v-if="item.type === TYPES.MSG_GRP_TIP || isCreateGroupCustomMessage(item)"
@ -79,7 +47,7 @@
<div <div
v-else v-else
:class="{ :class="{
'message-event-bind-div': true, 'message-event-bind-div': true
}" }"
@longpress="handleToggleMessageItem($event, item, true)" @longpress="handleToggleMessageItem($event, item, true)"
@click.prevent.right="handleToggleMessageItemForPC($event, item)" @click.prevent.right="handleToggleMessageItemForPC($event, item)"
@ -100,31 +68,12 @@
@setReadReceiptPanelVisible="setReadReceiptPanelVisible" @setReadReceiptPanelVisible="setReadReceiptPanelVisible"
> >
<template #messageElement> <template #messageElement>
<MessageText <MessageText v-if="item.type === TYPES.MSG_TEXT" :content="item.getMessageContent()" :messageItem="item" />
v-if="item.type === TYPES.MSG_TEXT" <ProgressMessage v-else-if="item.type === TYPES.MSG_IMAGE" :content="item.getMessageContent()" :messageItem="item">
:content="item.getMessageContent()" <MessageImage :content="item.getMessageContent()" :messageItem="item" @previewImage="handleImagePreview" />
:messageItem="item"
/>
<ProgressMessage
v-else-if="item.type === TYPES.MSG_IMAGE"
:content="item.getMessageContent()"
:messageItem="item"
>
<MessageImage
:content="item.getMessageContent()"
:messageItem="item"
@previewImage="handleImagePreview"
/>
</ProgressMessage> </ProgressMessage>
<ProgressMessage <ProgressMessage v-else-if="item.type === TYPES.MSG_VIDEO" :content="item.getMessageContent()" :messageItem="item">
v-else-if="item.type === TYPES.MSG_VIDEO" <MessageVideo :content="item.getMessageContent()" :messageItem="item" />
:content="item.getMessageContent()"
:messageItem="item"
>
<MessageVideo
:content="item.getMessageContent()"
:messageItem="item"
/>
</ProgressMessage> </ProgressMessage>
<MessageAudio <MessageAudio
v-else-if="item.type === TYPES.MSG_AUDIO" v-else-if="item.type === TYPES.MSG_AUDIO"
@ -132,40 +81,19 @@
:messageItem="item" :messageItem="item"
@setAudioPlayed="setAudioPlayed" @setAudioPlayed="setAudioPlayed"
/> />
<ProgressMessage <ProgressMessage v-else-if="item.type === TYPES.MSG_FILE" :content="item.getMessageContent()" :messageItem="item">
v-else-if="item.type === TYPES.MSG_FILE" <MessageFile :content="item.getMessageContent()" :messageItem="item" />
:content="item.getMessageContent()"
:messageItem="item"
>
<MessageFile
:content="item.getMessageContent()"
:messageItem="item"
/>
</ProgressMessage> </ProgressMessage>
<MessageRecord <MessageRecord v-else-if="item.type === TYPES.MSG_MERGER" :renderData="item.payload" :messageItem="item" />
v-else-if="item.type === TYPES.MSG_MERGER" <MessageFace v-else-if="item.type === TYPES.MSG_FACE" :content="item.getMessageContent()" />
:renderData="item.payload" <MessageLocation v-else-if="item.type === TYPES.MSG_LOCATION" :content="item.getMessageContent()" />
:messageItem="item"
/>
<MessageFace
v-else-if="item.type === TYPES.MSG_FACE"
:content="item.getMessageContent()"
/>
<MessageLocation
v-else-if="item.type === TYPES.MSG_LOCATION"
:content="item.getMessageContent()"
/>
<MessageStreamMarkdown <MessageStreamMarkdown
v-else-if="isBotMessage(item)" v-else-if="isBotMessage(item)"
:payloadData="item.payload.data" :payloadData="item.payload.data"
:message="item" :message="item"
@onStreaming="scrollStreamMessageToBottom" @onStreaming="scrollStreamMessageToBottom"
/> />
<MessageCustom <MessageCustom v-else-if="item.type === TYPES.MSG_CUSTOM" :content="item.getMessageContent()" :messageItem="item" />
v-else-if="item.type === TYPES.MSG_CUSTOM"
:content="item.getMessageContent()"
:messageItem="item"
/>
</template> </template>
<template #TUIEmojiPlugin> <template #TUIEmojiPlugin>
<TUIEmojiPlugin <TUIEmojiPlugin
@ -192,20 +120,13 @@
@toggleMultipleSelectMode="() => emits('toggleMultipleSelectMode')" @toggleMultipleSelectMode="() => emits('toggleMultipleSelectMode')"
> >
<template #TUIEmojiPlugin> <template #TUIEmojiPlugin>
<TUIEmojiPlugin <TUIEmojiPlugin v-if="isShowEmojiPlugin" :message="item" :emojiConfig="emojiConfig" />
v-if="isShowEmojiPlugin"
:message="item"
:emojiConfig="emojiConfig"
/>
</template> </template>
</MessageTool> </MessageTool>
</div> </div>
</li> </li>
</ul> </ul>
<ScrollButton <ScrollButton ref="scrollButtonInstanceRef" @scrollToLatestMessage="scrollToLatestMessage" />
ref="scrollButtonInstanceRef"
@scrollToLatestMessage="scrollToLatestMessage"
/>
<Dialog <Dialog
v-if="reSendDialogShow" v-if="reSendDialogShow"
class="resend-dialog" class="resend-dialog"
@ -217,15 +138,10 @@
@update:show="(e) => (reSendDialogShow = e)" @update:show="(e) => (reSendDialogShow = e)"
> >
<p class="delDialog-title"> <p class="delDialog-title">
{{ TUITranslateService.t("TUIChat.确认重发该消息?") }} {{ TUITranslateService.t('TUIChat.确认重发该消息?') }}
</p> </p>
</Dialog> </Dialog>
<ImagePreviewer <ImagePreviewer v-if="showImagePreview" :currentImage="currentImagePreview" :imageList="imageMessageList" @close="onImagePreviewerClose" />
v-if="showImagePreview"
:currentImage="currentImagePreview"
:imageList="imageMessageList"
@close="onImagePreviewerClose"
/>
<ReadReceiptPanel <ReadReceiptPanel
v-if="isShowReadUserStatusPanel" v-if="isShowReadUserStatusPanel"
:message="Object.assign({}, readStatusMessage)" :message="Object.assign({}, readStatusMessage)"
@ -237,13 +153,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, nextTick, computed, onMounted, onUnmounted, watch } from '../../../adapter-vue'; import { ref, nextTick, computed, onMounted, onUnmounted, watch } from '../../../adapter-vue';
import TUIChatEngine, { import TUIChatEngine, { IMessageModel, TUIStore, StoreName, TUITranslateService, TUIChatService } from '@tencentcloud/chat-uikit-engine';
IMessageModel,
TUIStore,
StoreName,
TUITranslateService,
TUIChatService,
} from '@tencentcloud/chat-uikit-engine';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core'; import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { outsideClick, getBoundingClientRect, getScrollInfo } from '@tencentcloud/universal-api'; import { outsideClick, getBoundingClientRect, getScrollInfo } from '@tencentcloud/universal-api';
import { TUIEmojiPlugin } from '@tencentcloud/tui-emoji-plugin'; import { TUIEmojiPlugin } from '@tencentcloud/tui-emoji-plugin';
@ -306,7 +216,7 @@ const props = withDefaults(defineProps<IProps>(), {
isGroup: false, isGroup: false,
groupID: '', groupID: '',
isNotInGroup: false, isNotInGroup: false,
isMultipleSelectMode: false, isMultipleSelectMode: false
}); });
let groupType: string | undefined; let groupType: string | undefined;
@ -346,7 +256,7 @@ const currentImagePreview = ref<IMessageModel>();
const imageMessageList = computed(() => const imageMessageList = computed(() =>
messageList?.value?.filter((item: IMessageModel) => { messageList?.value?.filter((item: IMessageModel) => {
return !item.isRevoked && !item.hasRiskContent && item.type === TYPES.value.MSG_IMAGE; return !item.isRevoked && !item.hasRiskContent && item.type === TYPES.value.MSG_IMAGE;
}), })
); );
// resend message dialog // resend message dialog
@ -355,7 +265,7 @@ const resendMessageData = ref();
const isShowEmojiPlugin = computed(() => { const isShowEmojiPlugin = computed(() => {
const msgPopMenuExtensionList = TUICore.getExtensionList(TUIConstants.TUIChat.EXTENSION.MSG_POP_MENU.EXT_ID, { const msgPopMenuExtensionList = TUICore.getExtensionList(TUIConstants.TUIChat.EXTENSION.MSG_POP_MENU.EXT_ID, {
enabledEmojiPlugin, enabledEmojiPlugin
}); });
return msgPopMenuExtensionList.some((item) => { return msgPopMenuExtensionList.some((item) => {
return item.text === 'TUIEmojiPlugin'; return item.text === 'TUIEmojiPlugin';
@ -369,15 +279,15 @@ onMounted(() => {
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
messageList: onMessageListUpdated, messageList: onMessageListUpdated,
messageSource: onMessageSourceUpdated, messageSource: onMessageSourceUpdated,
isCompleted: isCompletedUpdated, isCompleted: isCompletedUpdated
}); });
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated
}); });
TUIStore.watch(StoreName.CUSTOM, { TUIStore.watch(StoreName.CUSTOM, {
isShowMessagePopMenu: isShowMessagePopMenuUpdated, isShowMessagePopMenu: isShowMessagePopMenuUpdated
}); });
}); });
@ -389,15 +299,15 @@ onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
messageList: onMessageListUpdated, messageList: onMessageListUpdated,
messageSource: onMessageSourceUpdated, messageSource: onMessageSourceUpdated,
isCompleted: isCompletedUpdated, isCompleted: isCompletedUpdated
}); });
TUIStore.unwatch(StoreName.CONV, { TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated
}); });
TUIStore.unwatch(StoreName.CUSTOM, { TUIStore.unwatch(StoreName.CUSTOM, {
isShowMessagePopMenu: isShowMessagePopMenuUpdated, isShowMessagePopMenu: isShowMessagePopMenuUpdated
}); });
messageListRef.value?.removeEventListener('scroll', handelScrollListScroll); messageListRef.value?.removeEventListener('scroll', handelScrollListScroll);
@ -429,11 +339,7 @@ async function onMessageListUpdated(list: IMessageModel[]) {
} }
const newLastMessage = messageList.value?.[messageList.value?.length - 1]; const newLastMessage = messageList.value?.[messageList.value?.length - 1];
if (messageTarget.value) { if (messageTarget.value) {
if ( if (messageList.value?.findIndex((message: IMessageModel) => message?.ID === messageTarget.value?.ID) >= 0) {
messageList.value?.findIndex(
(message: IMessageModel) => message?.ID === messageTarget.value?.ID,
) >= 0
) {
const tempMessage = messageTarget.value; const tempMessage = messageTarget.value;
messageTarget.value = undefined; messageTarget.value = undefined;
await scrollToPosition({ scrollToMessage: tempMessage }); await scrollToPosition({ scrollToMessage: tempMessage });
@ -441,15 +347,12 @@ async function onMessageListUpdated(list: IMessageModel[]) {
} }
} else if (beforeHistoryGetScrollHeight.value) { } else if (beforeHistoryGetScrollHeight.value) {
await scrollToPosition({ await scrollToPosition({
scrollToOffset: { bottom: beforeHistoryGetScrollHeight.value }, scrollToOffset: { bottom: beforeHistoryGetScrollHeight.value }
}); });
beforeHistoryGetScrollHeight.value = 0; beforeHistoryGetScrollHeight.value = 0;
} else if (scrollButtonInstanceRef.value?.isScrollButtonVisible && newLastMessage?.flow === 'in') { } else if (scrollButtonInstanceRef.value?.isScrollButtonVisible && newLastMessage?.flow === 'in') {
return; return;
} else if ( } else if (newLastMessage?.ID && JSON.stringify(oldLastMessage) !== JSON.stringify(newLastMessage)) {
newLastMessage?.ID
&& JSON.stringify(oldLastMessage) !== JSON.stringify(newLastMessage)
) {
await scrollToPosition({ scrollToBottom: true }); await scrollToPosition({ scrollToBottom: true });
} else if (hasEmojiReaction && isCurrentListInBottomPosition()) { } else if (hasEmojiReaction && isCurrentListInBottomPosition()) {
await scrollToPosition({ scrollToBottom: true }); await scrollToPosition({ scrollToBottom: true });
@ -462,13 +365,11 @@ async function onMessageListUpdated(list: IMessageModel[]) {
function isCurrentListInBottomPosition() { function isCurrentListInBottomPosition() {
return ( return (
messageListRef.value messageListRef.value &&
&& typeof messageListRef.value.scrollTop === 'number' typeof messageListRef.value.scrollTop === 'number' &&
&& typeof messageListRef.value.scrollHeight === 'number' typeof messageListRef.value.scrollHeight === 'number' &&
&& typeof messageListRef.value.clientHeight === 'number' typeof messageListRef.value.clientHeight === 'number' &&
&& Math.ceil( Math.ceil(messageListRef.value.scrollTop + messageListRef.value.clientHeight) >= messageListRef.value.scrollHeight
messageListRef.value.scrollTop + messageListRef.value.clientHeight,
) >= messageListRef.value.scrollHeight
); );
} }
@ -486,9 +387,7 @@ async function scrollToPosition(config: ScrollConfig = {}): Promise<void> {
if (config.scrollToBottom) { if (config.scrollToBottom) {
container!.scrollTop = container!.scrollHeight; container!.scrollTop = container!.scrollHeight;
} else if (config.scrollToMessage) { } else if (config.scrollToMessage) {
const targetMessageDom = messageElementListRef.value?.find( const targetMessageDom = messageElementListRef.value?.find((dom: HTMLElement) => dom?.id === `tui-${config.scrollToMessage?.ID}`);
(dom: HTMLElement) => dom?.id === `tui-${config.scrollToMessage?.ID}`,
);
if (targetMessageDom?.scrollIntoView) { if (targetMessageDom?.scrollIntoView) {
targetMessageDom.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); targetMessageDom.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} }
@ -511,11 +410,7 @@ async function onMessageSourceUpdated(message: IMessageModel) {
// Only the second case needs to add scrollToTarget when listening here // Only the second case needs to add scrollToTarget when listening here
messageTarget.value = message; messageTarget.value = message;
if (messageTarget.value) { if (messageTarget.value) {
if ( if (messageList.value?.findIndex((message: IMessageModel) => message?.ID === messageTarget.value?.ID) >= 0) {
messageList.value?.findIndex(
(message: IMessageModel) => message?.ID === messageTarget.value?.ID,
) >= 0
) {
const tempMessage = messageTarget.value; const tempMessage = messageTarget.value;
messageTarget.value = undefined; messageTarget.value = undefined;
await scrollToPosition({ scrollToMessage: tempMessage }); await scrollToPosition({ scrollToMessage: tempMessage });
@ -600,7 +495,7 @@ const handleToggleMessageItemForPC = (e: MouseEvent, message: IMessageModel) =>
domRefs: targetMessageDom.value, domRefs: targetMessageDom.value,
ignoreDomRefs: ignoreDomRefs, ignoreDomRefs: ignoreDomRefs,
handler: closeChatPop, handler: closeChatPop,
button: e.button, button: e.button
}); });
filterTopMessageDom(e.target); filterTopMessageDom(e.target);
}); });
@ -683,16 +578,16 @@ async function scrollToLatestMessage() {
} }
} }
const handelScrollListScroll = throttle(function (e: Event) { const handelScrollListScroll = throttle(
scrollButtonInstanceRef.value?.judgeScrollOverOneScreen(e); function (e: Event) {
}, 150, { leading: true }); scrollButtonInstanceRef.value?.judgeScrollOverOneScreen(e);
},
150,
{ leading: true }
);
async function bindIntersectionObserver() { async function bindIntersectionObserver() {
if ( if (!messageList.value || !messageListRef.value || messageList.value.length === 0) {
!messageList.value
|| !messageListRef.value
|| messageList.value.length === 0
) {
return; return;
} }
@ -701,32 +596,34 @@ async function bindIntersectionObserver() {
return; return;
} }
const mappingFromIDToMessage: Record<string, { const mappingFromIDToMessage: Record<
msgDom: HTMLElement; string,
msgModel: IMessageModel | undefined; {
}> = {}; msgDom: HTMLElement;
msgModel: IMessageModel | undefined;
}
> = {};
observer?.disconnect(); observer?.disconnect();
observer = new IntersectionObserver((entries) => { observer = new IntersectionObserver(
entries.forEach((entry) => { (entries) => {
const { isIntersecting, target } = entry; entries.forEach((entry) => {
if (isIntersecting) { const { isIntersecting, target } = entry;
const { msgDom, msgModel } = mappingFromIDToMessage[target.id]; if (isIntersecting) {
if ( const { msgDom, msgModel } = mappingFromIDToMessage[target.id];
msgModel if (msgModel && !msgModel.readReceiptInfo?.isPeerRead && !sentReceiptMessageIDSet.has(msgModel.ID)) {
&& !msgModel.readReceiptInfo?.isPeerRead TUIChatService.sendMessageReadReceipt([msgModel]);
&& !sentReceiptMessageIDSet.has(msgModel.ID) sentReceiptMessageIDSet.add(msgModel.ID);
) { observer?.unobserve(msgDom);
TUIChatService.sendMessageReadReceipt([msgModel]); }
sentReceiptMessageIDSet.add(msgModel.ID);
observer?.unobserve(msgDom);
} }
} });
}); },
}, { {
root: messageListRef.value, root: messageListRef.value,
threshold: 0.7, threshold: 0.7
}); }
);
const arrayOfMessageLi = messageListRef.value?.querySelectorAll('.message-li'); const arrayOfMessageLi = messageListRef.value?.querySelectorAll('.message-li');
if (arrayOfMessageLi) { if (arrayOfMessageLi) {
@ -735,14 +632,10 @@ async function bindIntersectionObserver() {
const matchingMessage = messageList.value.find((message: IMessageModel) => { const matchingMessage = messageList.value.find((message: IMessageModel) => {
return messageElement.id.slice(4) === message.ID; return messageElement.id.slice(4) === message.ID;
}); });
if ( if (matchingMessage && matchingMessage.needReadReceipt && matchingMessage.flow === 'in') {
matchingMessage
&& matchingMessage.needReadReceipt
&& matchingMessage.flow === 'in'
) {
mappingFromIDToMessage[messageElement.id] = { mappingFromIDToMessage[messageElement.id] = {
msgDom: messageElement, msgDom: messageElement,
msgModel: matchingMessage, msgModel: matchingMessage
}; };
observer?.observe(messageElement); observer?.observe(messageElement);
} }
@ -750,7 +643,6 @@ async function bindIntersectionObserver() {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const isSignalingMessage = (message: IMessageModel) => { const isSignalingMessage = (message: IMessageModel) => {
return message?.type === TYPES.value.MSG_CUSTOM && message?.getSignalingInfo(); return message?.type === TYPES.value.MSG_CUSTOM && message?.getSignalingInfo();
}; };
@ -775,14 +667,17 @@ function onMessageListBackgroundClick() {
emits('closeInputToolBar'); emits('closeInputToolBar');
} }
watch(() => props.isMultipleSelectMode, (newValue) => { watch(
if (!newValue) { () => props.isMultipleSelectMode,
changeSelectMessageIDList({ (newValue) => {
type: 'clearAll', if (!newValue) {
messageID: '', changeSelectMessageIDList({
}); type: 'clearAll',
messageID: ''
});
}
} }
}); );
function changeSelectMessageIDList({ type, messageID }: { type: 'add' | 'remove' | 'clearAll'; messageID: string }) { function changeSelectMessageIDList({ type, messageID }: { type: 'add' | 'remove' | 'clearAll'; messageID: string }) {
// TODO need to delete this // TODO need to delete this
@ -791,35 +686,35 @@ function changeSelectMessageIDList({ type, messageID }: { type: 'add' | 'remove'
} else if (type === 'add' && !multipleSelectedMessageIDList.value.includes(messageID)) { } else if (type === 'add' && !multipleSelectedMessageIDList.value.includes(messageID)) {
multipleSelectedMessageIDList.value.push(messageID); multipleSelectedMessageIDList.value.push(messageID);
} else if (type === 'remove') { } else if (type === 'remove') {
multipleSelectedMessageIDList.value = multipleSelectedMessageIDList.value.filter(id => id !== messageID); multipleSelectedMessageIDList.value = multipleSelectedMessageIDList.value.filter((id) => id !== messageID);
} }
} }
function mergeForwardMessage() { function mergeForwardMessage() {
TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', { TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', {
isMergeForward: true, isMergeForward: true,
messageIDList: multipleSelectedMessageIDList.value, messageIDList: multipleSelectedMessageIDList.value
}); });
} }
function oneByOneForwardMessage() { function oneByOneForwardMessage() {
TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', { TUIStore.update(StoreName.CUSTOM, 'multipleForwardMessageID', {
isMergeForward: false, isMergeForward: false,
messageIDList: multipleSelectedMessageIDList.value, messageIDList: multipleSelectedMessageIDList.value
}); });
} }
function setAudioPlayed(messageID: string) { function setAudioPlayed(messageID: string) {
audioPlayedMapping.value = { audioPlayedMapping.value = {
...audioPlayedMapping.value, ...audioPlayedMapping.value,
[messageID]: true, [messageID]: true
}; };
} }
defineExpose({ defineExpose({
oneByOneForwardMessage, oneByOneForwardMessage,
mergeForwardMessage, mergeForwardMessage,
scrollToLatestMessage, scrollToLatestMessage
}); });
</script> </script>

View File

@ -1,23 +1,23 @@
const Link = { const Link = {
product: { product: {
label: '产品文档', label: '产品文档',
url: 'https://cloud.tencent.com/document/product/269/1499#.E7.BE.A4.E7.BB.84.E5.8A.9F.E8.83.BD', url: 'https://cloud.tencent.com/document/product/269/1499#.E7.BE.A4.E7.BB.84.E5.8A.9F.E8.83.BD'
}, },
customMessage: { customMessage: {
label: '自定义消息', label: '自定义消息',
url: 'https://web.sdk.qcloud.com/im/doc/zh-cn/SDK.html#createCustomMessage', url: 'https://web.sdk.qcloud.com/im/doc/zh-cn/SDK.html#createCustomMessage'
}, },
complaint: { complaint: {
label: '点此投诉', label: '点此投诉',
url: 'https://cloud.tencent.com/apply/p/xc3oaubi98g', url: 'https://cloud.tencent.com/apply/p/xc3oaubi98g'
}, },
implement: { implement: {
label: '集成TUICallKit', label: '集成TUICallKit',
url: 'https://cloud.tencent.com/document/product/269/79861', url: 'https://cloud.tencent.com/document/product/269/79861'
}, },
purchase: { purchase: {
label: '开通腾讯实时音视频服务', label: '开通腾讯实时音视频服务',
url: 'https://cloud.tencent.com/document/product/1640/79968', url: 'https://cloud.tencent.com/document/product/1640/79968'
}, }
}; };
export default Link; export default Link;

View File

@ -1,32 +1,15 @@
<template> <template>
<div <div
class="message-audio" class="message-audio"
:class="[ :class="[isMobile && 'message-audio-h5', message.flow === 'out' && 'reserve', message.hasRiskContent && 'disable']"
isMobile && 'message-audio-h5',
message.flow === 'out' && 'reserve',
message.hasRiskContent && 'disable',
]"
@click.stop="play" @click.stop="play"
> >
<div class="audio-icon-container"> <div class="audio-icon-container">
<div :class="{ 'mask': true, 'play': isAudioPlaying }" /> <div :class="{ 'mask': true, 'play': isAudioPlaying }" />
<Icon <Icon class="icon" width="16px" height="20px" :file="audioIcon" />
class="icon"
width="16px"
height="20px"
:file="audioIcon"
/>
</div> </div>
<span <span class="time" :style="{ width: `${data.second * 10 + 20}px` }"> {{ data.second || 1 }} " </span>
class="time" <audio ref="audioRef" :src="data.url" />
:style="{ width: `${data.second * 10 + 20}px` }"
>
{{ data.second || 1 }} "
</span>
<audio
ref="audioRef"
:src="data.url"
/>
</div> </div>
</template> </template>
@ -44,12 +27,12 @@ const emits = defineEmits<IEmits>();
const props = defineProps({ const props = defineProps({
content: { content: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, },
messageItem: { messageItem: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, }
}); });
const data = ref(); const data = ref();
@ -109,7 +92,7 @@ function onAudioPaused() {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
$flow-in-bg-color: #fbfbfb; $flow-in-bg-color: #fbfbfb;
$flow-out-bg-color: #dceafd; $flow-out-bg-color: #dceafd;

View File

@ -1,12 +1,7 @@
<template> <template>
<div :class="containerClassNameList"> <div :class="containerClassNameList">
<!-- multiple select radio --> <!-- multiple select radio -->
<RadioSelect <RadioSelect v-if="props.isMultipleSelectMode" class="multiple-select-radio" :isSelected="isMultipleSelected" @onChange="toggleMultipleSelect" />
v-if="props.isMultipleSelectMode"
class="multiple-select-radio"
:isSelected="isMultipleSelected"
@onChange="toggleMultipleSelect"
/>
<div <div
:class="{ :class="{
'control-reverse': message.flow === 'out' 'control-reverse': message.flow === 'out'
@ -14,22 +9,10 @@
> >
<!-- message-bubble-container --> <!-- message-bubble-container -->
<div class="message-bubble-content"> <div class="message-bubble-content">
<div <div class="message-bubble-main-content" :class="[message.flow === 'in' ? '' : 'reverse']">
class="message-bubble-main-content" <Avatar useSkeletonAnimation :url="message.avatar || ''" />
:class="[message.flow === 'in' ? '' : 'reverse']" <main class="message-body" @click.stop>
> <div v-if="message.flow === 'in' && message.conversationType === 'GROUP'" class="message-body-nick-name">
<Avatar
useSkeletonAnimation
:url="message.avatar || ''"
/>
<main
class="message-body"
@click.stop
>
<div
v-if="message.flow === 'in' && message.conversationType === 'GROUP'"
class="message-body-nick-name"
>
{{ props.content.showName }} {{ props.content.showName }}
</div> </div>
<div :class="['message-body-main', message.flow === 'out' && 'message-body-main-reverse']"> <div :class="['message-body-main', message.flow === 'out' && 'message-body-main-reverse']">
@ -41,44 +24,29 @@
message.hasRiskContent && 'content-has-risk', message.hasRiskContent && 'content-has-risk',
isNoPadding ? 'content-no-padding' : '', isNoPadding ? 'content-no-padding' : '',
isNoPadding && isBlink ? 'blink-shadow' : '', isNoPadding && isBlink ? 'blink-shadow' : '',
!isNoPadding && isBlink ? 'blink-content' : '', !isNoPadding && isBlink ? 'blink-content' : ''
]" ]"
> >
<div class="content-main"> <div class="content-main">
<img <img
v-if=" v-if="(message.type === TYPES.MSG_IMAGE || message.type === TYPES.MSG_VIDEO) && message.hasRiskContent"
(message.type === TYPES.MSG_IMAGE || message.type === TYPES.MSG_VIDEO) &&
message.hasRiskContent
"
:class="['message-risk-replace', !isPC && 'message-risk-replace-h5']" :class="['message-risk-replace', !isPC && 'message-risk-replace-h5']"
:src="riskImageReplaceUrl" :src="riskImageReplaceUrl"
> />
<template v-else> <template v-else>
<slot name="messageElement" /> <slot name="messageElement" />
<slot name="TUIEmojiPlugin" /> <slot name="TUIEmojiPlugin" />
</template> </template>
</div> </div>
<!-- Risk Content Tips --> <!-- Risk Content Tips -->
<div <div v-if="message.hasRiskContent" class="content-has-risk-tips">
v-if="message.hasRiskContent"
class="content-has-risk-tips"
>
{{ riskContentText }} {{ riskContentText }}
</div> </div>
</div> </div>
<!-- audio unplay mark --> <!-- audio unplay mark -->
<div <div v-if="isDisplayUnplayMark" class="audio-unplay-mark" />
v-if="isDisplayUnplayMark"
class="audio-unplay-mark"
/>
<!-- Send Fail Icon --> <!-- Send Fail Icon -->
<div <div v-if="message.status === 'fail' || message.hasRiskContent" class="message-label fail" @click="resendMessage()">!</div>
v-if="message.status === 'fail' || message.hasRiskContent"
class="message-label fail"
@click="resendMessage()"
>
!
</div>
<!-- Loading Icon --> <!-- Loading Icon -->
<Icon <Icon
v-if="message.status === 'unSend' && needLoadingIconMessageType.includes(message.type)" v-if="message.status === 'unSend' && needLoadingIconMessageType.includes(message.type)"
@ -88,28 +56,16 @@
:height="'15px'" :height="'15px'"
/> />
<!-- Read & Unread --> <!-- Read & Unread -->
<ReadStatus <ReadStatus class="message-label align-self-bottom" :message="shallowCopyMessage(message)" @openReadUserPanel="openReadUserPanel" />
class="message-label align-self-bottom"
:message="shallowCopyMessage(message)"
@openReadUserPanel="openReadUserPanel"
/>
</div> </div>
</main> </main>
</div> </div>
<!-- message extra area --> <!-- message extra area -->
<div <div class="message-bubble-extra-content">
class="message-bubble-extra-content"
>
<!-- extra: message translation --> <!-- extra: message translation -->
<MessageTranslate <MessageTranslate :class="message.flow === 'out' ? 'reverse' : 'flex-row'" :message="message" />
:class="message.flow === 'out' ? 'reverse' : 'flex-row'"
:message="message"
/>
<!-- extra: message convert voice to text --> <!-- extra: message convert voice to text -->
<MessageConvert <MessageConvert :class="message.flow === 'out' ? 'reverse' : 'flex-row'" :message="message" />
:class="message.flow === 'out' ? 'reverse' : 'flex-row'"
:message="message"
/>
<!-- extra: message quote --> <!-- extra: message quote -->
<MessageQuote <MessageQuote
:class="message.flow === 'out' ? 'reverse' : 'flex-row'" :class="message.flow === 'out' ? 'reverse' : 'flex-row'"
@ -159,24 +115,18 @@ interface IEmits {
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
messageItem: () => ({} as IMessageModel), messageItem: () => ({}) as IMessageModel,
content: () => ({}), content: () => ({}),
isAudioPlayed: false, isAudioPlayed: false,
blinkMessageIDList: () => [], blinkMessageIDList: () => [],
classNameList: () => [], classNameList: () => [],
isMultipleSelectMode: false, isMultipleSelectMode: false,
multipleSelectedMessageIDList: () => [], multipleSelectedMessageIDList: () => []
}); });
const TYPES = TUIChatEngine.TYPES; const TYPES = TUIChatEngine.TYPES;
const riskImageReplaceUrl = 'https://web.sdk.qcloud.com/component/TUIKit/assets/has_risk_default.png'; const riskImageReplaceUrl = 'https://web.sdk.qcloud.com/component/TUIKit/assets/has_risk_default.png';
const needLoadingIconMessageType = [ const needLoadingIconMessageType = [TYPES.MSG_LOCATION, TYPES.MSG_TEXT, TYPES.MSG_CUSTOM, TYPES.MSG_MERGER, TYPES.MSG_FACE];
TYPES.MSG_LOCATION,
TYPES.MSG_TEXT,
TYPES.MSG_CUSTOM,
TYPES.MSG_MERGER,
TYPES.MSG_FACE,
];
const { blinkMessageIDList, messageItem: message } = toRefs(props); const { blinkMessageIDList, messageItem: message } = toRefs(props);
@ -185,18 +135,11 @@ const isMultipleSelected = computed<boolean>(() => {
}); });
const isDisplayUnplayMark = computed<boolean>(() => { const isDisplayUnplayMark = computed<boolean>(() => {
return message.value.flow === 'in' return message.value.flow === 'in' && message.value.status === 'success' && message.value.type === TYPES.MSG_AUDIO && !props.isAudioPlayed;
&& message.value.status === 'success'
&& message.value.type === TYPES.MSG_AUDIO
&& !props.isAudioPlayed;
}); });
const containerClassNameList = computed(() => { const containerClassNameList = computed(() => {
return [ return ['message-bubble', isMultipleSelected.value ? 'multiple-selected' : '', ...props.classNameList];
'message-bubble',
isMultipleSelected.value ? 'multiple-selected' : '',
...props.classNameList,
];
}); });
// When an emoji is deleted, the `reactionList` will update the corresponding emoji's `totalUserCount`. // When an emoji is deleted, the `reactionList` will update the corresponding emoji's `totalUserCount`.
@ -213,9 +156,7 @@ const riskContentText = computed<string>(() => {
if (message.value.flow === 'out') { if (message.value.flow === 'out') {
content += TUITranslateService.t('TUIChat.发送失败'); content += TUITranslateService.t('TUIChat.发送失败');
} else { } else {
content += TUITranslateService.t( content += TUITranslateService.t(message.value.type === TYPES.MSG_AUDIO ? 'TUIChat.无法收听' : 'TUIChat.无法查看');
message.value.type === TYPES.MSG_AUDIO ? 'TUIChat.无法收听' : 'TUIChat.无法查看',
);
} }
return content; return content;
}); });
@ -230,7 +171,7 @@ const isBlink = computed(() => {
function toggleMultipleSelect(isSelected: boolean) { function toggleMultipleSelect(isSelected: boolean) {
emits('changeSelectMessageIDList', { emits('changeSelectMessageIDList', {
type: isSelected ? 'add' : 'remove', type: isSelected ? 'add' : 'remove',
messageID: message.value.ID, messageID: message.value.ID
}); });
} }
@ -373,96 +314,96 @@ function openReadUserPanel() {
margin-top: 5px; margin-top: 5px;
border-top: 1px solid #e5c7c7; border-top: 1px solid #e5c7c7;
padding-top: 5px; padding-top: 5px;
}
}
.content-in {
background: #fbfbfb;
border-radius: 0 10px 10px;
}
.content-out {
background: #dceafd;
border-radius: 10px 0 10px 10px;
}
.content-no-padding {
padding: 0;
background: transparent;
border-radius: 10px;
overflow: hidden;
}
.content-no-padding.content-has-risk {
padding: 12px;
}
.content-has-risk {
background: rgba(250, 81, 81, 0.16);
}
.blink-shadow {
@keyframes shadow-blink {
50% {
box-shadow: rgba(255, 156, 25, 1) 0 0 10px 0;
} }
} }
box-shadow: rgba(255, 156, 25, 0) 0 0 10px 0; .content-in {
animation: shadow-blink 1s linear 3; background: #fbfbfb;
} border-radius: 0 10px 10px;
}
.blink-content { .content-out {
@keyframes reference-blink { background: #dceafd;
50% { border-radius: 10px 0 10px 10px;
background-color: #ff9c19; }
.content-no-padding {
padding: 0;
background: transparent;
border-radius: 10px;
overflow: hidden;
}
.content-no-padding.content-has-risk {
padding: 12px;
}
.content-has-risk {
background: rgba(250, 81, 81, 0.16);
}
.blink-shadow {
@keyframes shadow-blink {
50% {
box-shadow: rgba(255, 156, 25, 1) 0 0 10px 0;
}
}
box-shadow: rgba(255, 156, 25, 0) 0 0 10px 0;
animation: shadow-blink 1s linear 3;
}
.blink-content {
@keyframes reference-blink {
50% {
background-color: #ff9c19;
}
}
animation: reference-blink 1s linear 3;
}
.message-label {
align-self: flex-end;
font-family: PingFangSC-Regular;
font-size: 12px;
color: #b6b8ba;
word-break: keep-all;
flex: 0 0 auto;
margin: 0 8px;
&.fail {
width: 15px;
height: 15px;
border-radius: 15px;
background: red;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
&.loading-circle {
opacity: 0;
animation: circle-loading 2s linear 1s infinite;
}
@keyframes circle-loading {
0% {
transform: rotate(0);
opacity: 1;
}
100% {
opacity: 1;
transform: rotate(360deg);
}
} }
} }
animation: reference-blink 1s linear 3; .align-self-bottom {
} align-self: flex-end;
.message-label {
align-self: flex-end;
font-family: PingFangSC-Regular;
font-size: 12px;
color: #b6b8ba;
word-break: keep-all;
flex: 0 0 auto;
margin: 0 8px;
&.fail {
width: 15px;
height: 15px;
border-radius: 15px;
background: red;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
&.loading-circle {
opacity: 0;
animation: circle-loading 2s linear 1s infinite;
}
@keyframes circle-loading {
0% {
transform: rotate(0);
opacity: 1;
}
100% {
opacity: 1;
transform: rotate(360deg);
}
}
}
.align-self-bottom {
align-self: flex-end;
} }
} }
} }

View File

@ -3,7 +3,7 @@
class="message-convert-container" class="message-convert-container"
:style="{ :style="{
height: calculateHeight > 0 ? `${calculateHeight}px` : 'auto', height: calculateHeight > 0 ? `${calculateHeight}px` : 'auto',
width: calculateWidth > 0 ? `${calculateWidth}px` : 'auto', width: calculateWidth > 0 ? `${calculateWidth}px` : 'auto'
}" }"
> >
<div <div
@ -11,7 +11,7 @@
ref="convertContentRef" ref="convertContentRef"
:class="{ :class="{
'convert-content': true, 'convert-content': true,
'occur': calculateHeight > 0, 'occur': calculateHeight > 0
}" }"
> >
{{ convertText }} {{ convertText }}
@ -30,10 +30,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch, nextTick } from '../../../../../adapter-vue'; import { ref, watch, nextTick } from '../../../../../adapter-vue';
import { import { IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { getBoundingClientRectSync } from '@tencentcloud/universal-api'; import { getBoundingClientRectSync } from '@tencentcloud/universal-api';
import { convertor } from '../../../utils/convertVoiceToText'; import { convertor } from '../../../utils/convertVoiceToText';
@ -50,8 +47,8 @@ interface IEmits {
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel), message: () => ({}) as IMessageModel,
isSingleConvert: false, isSingleConvert: false
}); });
const convertFinished = ref<boolean>(false); const convertFinished = ref<boolean>(false);
@ -62,46 +59,51 @@ const calculateWidth = ref<number>(0);
const convertLoadingRef = ref<HTMLDivElement>(); const convertLoadingRef = ref<HTMLDivElement>();
const convertContentRef = ref<HTMLDivElement>(); const convertContentRef = ref<HTMLDivElement>();
watch(() => props.contentVisible, (newVal: boolean) => { watch(
if (newVal) { () => props.contentVisible,
convertor.get(props.message) (newVal: boolean) => {
.then((text) => { if (newVal) {
convertFinished.value = true; convertor
convertText.value = text; .get(props.message)
nextTick(() => { .then((text) => {
const { height: originHeight, width: originWidth } = getBoundingClientRectSync(convertLoadingRef.value!); convertFinished.value = true;
const { height, width } = getBoundingClientRectSync(convertContentRef.value!); convertText.value = text;
calculateHeight.value = originHeight; nextTick(() => {
calculateWidth.value = originWidth; const { height: originHeight, width: originWidth } = getBoundingClientRectSync(convertLoadingRef.value!);
requestAnimationFrame(() => { const { height, width } = getBoundingClientRectSync(convertContentRef.value!);
calculateHeight.value = height; calculateHeight.value = originHeight;
calculateWidth.value = width; calculateWidth.value = originWidth;
if (props.isSingleConvert) { requestAnimationFrame(() => {
nextTick(() => { calculateHeight.value = height;
const { bottom } = getBoundingClientRectSync(props.convertWrapperRef); calculateWidth.value = width;
const { bottom: bottomWindow } = getBoundingClientRectSync('#messageScrollList'); if (props.isSingleConvert) {
if (bottom > bottomWindow) { nextTick(() => {
const timer = setTimeout(() => { const { bottom } = getBoundingClientRectSync(props.convertWrapperRef);
props.convertWrapperRef!.scrollIntoView({ block: 'end', behavior: 'smooth' }); const { bottom: bottomWindow } = getBoundingClientRectSync('#messageScrollList');
clearTimeout(timer); if (bottom > bottomWindow) {
}, 150); const timer = setTimeout(() => {
} props.convertWrapperRef!.scrollIntoView({ block: 'end', behavior: 'smooth' });
}); clearTimeout(timer);
} }, 150);
}
});
}
});
}); });
})
.catch((err) => {
convertFinished.value = true;
emits('toggleErrorStatus', true);
const { height: originHeight } = getBoundingClientRectSync(convertLoadingRef.value!);
calculateHeight.value = originHeight;
convertText.value = err.message;
}); });
}) }
.catch((err) => { },
convertFinished.value = true; {
emits('toggleErrorStatus', true); immediate: true
const { height: originHeight } = getBoundingClientRectSync(convertLoadingRef.value!);
calculateHeight.value = originHeight;
convertText.value = err.message;
});
} }
}, { );
immediate: true,
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -109,7 +111,9 @@ watch(() => props.contentVisible, (newVal: boolean) => {
min-height: 20px; min-height: 20px;
min-width: 80px; min-width: 80px;
position: relative; position: relative;
transition: width 0.15s ease-out, height 0.15s ease-out, ; transition:
width 0.15s ease-out,
height 0.15s ease-out;
font-size: 14px; font-size: 14px;
.loading { .loading {

View File

@ -5,7 +5,7 @@
:class="{ :class="{
'message-convert': true, 'message-convert': true,
'reverse': props.message.flow === 'out', 'reverse': props.message.flow === 'out',
'error': hasConvertError, 'error': hasConvertError
}" }"
> >
<ConvertContent <ConvertContent
@ -20,11 +20,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../../adapter-vue'; import { ref, onMounted, onUnmounted } from '../../../../../adapter-vue';
import { import { TUIStore, StoreName, IMessageModel } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
IMessageModel,
} from '@tencentcloud/chat-uikit-engine';
import ConvertContent from './convert-content.vue'; import ConvertContent from './convert-content.vue';
import { IConvertInfo } from '../../../../../interface'; import { IConvertInfo } from '../../../../../interface';
@ -33,7 +29,7 @@ interface IProps {
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel), message: () => ({}) as IMessageModel
}); });
const convertVisible = ref<boolean>(false); const convertVisible = ref<boolean>(false);
@ -44,13 +40,13 @@ let isSingleConvert = true;
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
voiceToTextInfo: onMessageConvertUpdated, voiceToTextInfo: onMessageConvertUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
voiceToTextInfo: onMessageConvertUpdated, voiceToTextInfo: onMessageConvertUpdated
}); });
}); });

View File

@ -4,22 +4,11 @@
<div> <div>
<h1> <h1>
<label>{{ extension.title }}</label> <label>{{ extension.title }}</label>
<a <a v-if="extension.hyperlinks_text" :href="extension.hyperlinks_text.value" target="view_window">{{ extension.hyperlinks_text.key }}</a>
v-if="extension.hyperlinks_text"
:href="extension.hyperlinks_text.value"
target="view_window"
>{{ extension.hyperlinks_text.key }}</a>
</h1> </h1>
<ul v-if="extension.item && extension.item.length > 0"> <ul v-if="extension.item && extension.item.length > 0">
<li <li v-for="(item, index) in extension.item" :key="index">
v-for="(item, index) in extension.item" <a v-if="isUrl(item.value)" :href="item.value" target="view_window">{{ item.key }}</a>
:key="index"
>
<a
v-if="isUrl(item.value)"
:href="item.value"
target="view_window"
>{{ item.key }}</a>
<p v-else> <p v-else>
{{ item.key }} {{ item.key }}
</p> </p>
@ -30,30 +19,18 @@
</template> </template>
<template v-else-if="customData.businessID === CHAT_MSG_CUSTOM_TYPE.EVALUATE"> <template v-else-if="customData.businessID === CHAT_MSG_CUSTOM_TYPE.EVALUATE">
<div class="evaluate"> <div class="evaluate">
<h1>{{ TUITranslateService.t("message.custom.对本次服务评价") }}</h1> <h1>{{ TUITranslateService.t('message.custom.对本次服务评价') }}</h1>
<ul class="evaluate-list"> <ul class="evaluate-list">
<li <li v-for="(item, index) in Math.max(customData.score, 0)" :key="index" class="evaluate-list-item">
v-for="(item, index) in Math.max(customData.score, 0)" <Icon :file="star" class="file-icon" />
:key="index"
class="evaluate-list-item"
>
<Icon
:file="star"
class="file-icon"
/>
</li> </li>
</ul> </ul>
<article>{{ customData.comment }}</article> <article>{{ customData.comment }}</article>
</div> </div>
</template> </template>
<template v-else-if="customData.businessID === CHAT_MSG_CUSTOM_TYPE.ORDER"> <template v-else-if="customData.businessID === CHAT_MSG_CUSTOM_TYPE.ORDER">
<div <div class="order" @click="openLink(customData.link)">
class="order" <img :src="customData.imageUrl" />
@click="openLink(customData.link)"
>
<img
:src="customData.imageUrl"
>
<main> <main>
<h1>{{ customData.title }}</h1> <h1>{{ customData.title }}</h1>
<p>{{ customData.description }}</p> <p>{{ customData.description }}</p>
@ -64,12 +41,7 @@
<template v-else-if="customData.businessID === CHAT_MSG_CUSTOM_TYPE.LINK"> <template v-else-if="customData.businessID === CHAT_MSG_CUSTOM_TYPE.LINK">
<div class="textLink"> <div class="textLink">
<p>{{ customData.text }}</p> <p>{{ customData.text }}</p>
<a <a :href="customData.link" target="view_window">{{ TUITranslateService.t('message.custom.查看详情>>') }}</a>
:href="customData.link"
target="view_window"
>{{
TUITranslateService.t("message.custom.查看详情>>")
}}</a>
</div> </div>
</template> </template>
<template v-else> <template v-else>
@ -93,14 +65,14 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
messageItem: undefined, messageItem: undefined,
content: undefined, content: undefined
}); });
const custom = ref(); const custom = ref();
const message = ref<IMessageModel>(); const message = ref<IMessageModel>();
const extension = ref(); const extension = ref();
const customData = ref<ICustomMessagePayload>({ const customData = ref<ICustomMessagePayload>({
businessID: '', businessID: ''
}); });
watchEffect(() => { watchEffect(() => {
@ -118,7 +90,7 @@ const openLink = (url: any) => {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
a { a {
color: #679ce1; color: #679ce1;

View File

@ -1,12 +1,6 @@
<template> <template>
<div <div class="message-image">
class="message-image" <img mode="aspectFit" class="message-image" :src="url" />
>
<img
mode="aspectFit"
class="message-image"
:src="url"
>
</div> </div>
</template> </template>
@ -17,8 +11,8 @@ import { CUSTOM_BIG_EMOJI_URL } from '../../emoji-config';
const props = defineProps({ const props = defineProps({
content: { content: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, }
}); });
const url = ref(props.content.url); const url = ref(props.content.url);
@ -35,7 +29,7 @@ onMounted(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.message-image { .message-image {
width: 80px; width: 80px;

View File

@ -1,13 +1,6 @@
<template> <template>
<div <div class="file-message-montainer" :title="TUITranslateService.t('TUIChat.单击下载')" @click="download">
class="file-message-montainer" <Icon :file="files" class="file-icon" />
:title="TUITranslateService.t('TUIChat.单击下载')"
@click="download"
>
<Icon
:file="files"
class="file-icon"
/>
<div> <div>
<div>{{ props.content.name }}</div> <div>{{ props.content.name }}</div>
<div>{{ props.content.size }}</div> <div>{{ props.content.size }}</div>
@ -28,9 +21,9 @@ const props = withDefaults(
messageItem: IMessageModel; messageItem: IMessageModel;
}>(), }>(),
{ {
content: () => ({} as IFileMessageContent), content: () => ({}) as IFileMessageContent,
messageItem: () => ({} as IMessageModel), messageItem: () => ({}) as IMessageModel
}, }
); );
const download = () => { const download = () => {
@ -40,13 +33,13 @@ const download = () => {
const option = { const option = {
mode: 'cors', mode: 'cors',
headers: new Headers({ headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded'
}), })
} as RequestInit; } as RequestInit;
// If the browser supports fetch, use blob to download, so as to avoid the browser clicking the a tag and jumping to the preview of the new page // If the browser supports fetch, use blob to download, so as to avoid the browser clicking the a tag and jumping to the preview of the new page
if ((window as any)?.fetch) { if ((window as any)?.fetch) {
fetch(props.content.url, option) fetch(props.content.url, option)
.then(res => res.blob()) .then((res) => res.blob())
.then((blob) => { .then((blob) => {
const a = document.createElement('a'); const a = document.createElement('a');
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
@ -64,7 +57,7 @@ const download = () => {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.file-message-montainer { .file-message-montainer {
display: flex; display: flex;

View File

@ -1,15 +1,11 @@
<template> <template>
<div <div ref="skeletonDomRef" class="image-container" @click.self="toggleShow">
ref="skeletonDomRef"
class="image-container"
@click.self="toggleShow"
>
<img <img
:class="['message-image', !isPC && 'message-image-h5']" :class="['message-image', !isPC && 'message-image-h5']"
:src="props.content.url" :src="props.content.url"
:width="props.content.width" :width="props.content.width"
:height="props.content.height" :height="props.content.height"
> />
</div> </div>
</template> </template>
@ -28,17 +24,13 @@ const props = withDefaults(
}>(), }>(),
{ {
content: () => ({}), content: () => ({}),
messageItem: () => ({} as IMessageModel), messageItem: () => ({}) as IMessageModel
}, }
); );
const skeletonDomRef = ref(); const skeletonDomRef = ref();
onMounted(() => { onMounted(() => {
if ( if (props.messageItem?.status === 'success' || props.messageItem?.status === 'fail' || props.messageItem?.progress === 1) {
props.messageItem?.status === 'success'
|| props.messageItem?.status === 'fail'
|| props.messageItem?.progress === 1
) {
autoFixSkeletonSize(); autoFixSkeletonSize();
} }
}); });
@ -49,7 +41,7 @@ watch(
if (newVal > oldVal) { if (newVal > oldVal) {
autoFixSkeletonSize(); autoFixSkeletonSize();
} }
}, }
); );
function autoFixSkeletonSize() { function autoFixSkeletonSize() {
@ -70,7 +62,7 @@ function toggleShow() {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.image-container { .image-container {
overflow: hidden; overflow: hidden;

View File

@ -1,12 +1,7 @@
<template> <template>
<a <a class="message-location" :href="data.href" target="_blank" title="点击查看详情">
class="message-location"
:href="data.href"
target="_blank"
title="点击查看详情"
>
<span class="el-icon-location-outline">{{ data.description }}</span> <span class="el-icon-location-outline">{{ data.description }}</span>
<img :src="data.url"> <img :src="data.url" />
</a> </a>
</template> </template>
@ -15,8 +10,8 @@ import { watchEffect, ref } from '../../../../adapter-vue';
const props = defineProps({ const props = defineProps({
content: { content: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, }
}); });
const data = ref(); const data = ref();
watchEffect(() => { watchEffect(() => {
@ -24,7 +19,7 @@ watchEffect(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.message-location { .message-location {
display: flex; display: flex;

View File

@ -3,33 +3,20 @@
v-if="hasQuoteContent" v-if="hasQuoteContent"
:class="{ :class="{
'reference-content': true, 'reference-content': true,
'reverse': message.flow === 'out', 'reverse': message.flow === 'out'
}" }"
@click="scrollToOriginalMessage" @click="scrollToOriginalMessage"
> >
<div <div v-if="isMessageRevoked" class="revoked-text">
v-if="isMessageRevoked"
class="revoked-text"
>
{{ TUITranslateService.t('TUIChat.引用内容已撤回') }} {{ TUITranslateService.t('TUIChat.引用内容已撤回') }}
</div> </div>
<div <div v-else class="max-double-line">{{ messageQuoteContent.messageSender }}: {{ transformTextWithKeysToEmojiNames(messageQuoteText) }}</div>
v-else
class="max-double-line"
>
{{ messageQuoteContent.messageSender }}: {{ transformTextWithKeysToEmojiNames(messageQuoteText) }}
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, onMounted } from '../../../../../adapter-vue'; import { computed, ref, onMounted } from '../../../../../adapter-vue';
import { import { TUIStore, StoreName, IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { getBoundingClientRect, getScrollInfo } from '@tencentcloud/universal-api'; import { getBoundingClientRect, getScrollInfo } from '@tencentcloud/universal-api';
import { isUniFrameWork } from '../../../../../utils/env'; import { isUniFrameWork } from '../../../../../utils/env';
import { Toast, TOAST_TYPE } from '../../../../../components/common/Toast/index'; import { Toast, TOAST_TYPE } from '../../../../../components/common/Toast/index';
@ -47,7 +34,7 @@ export interface IEmits {
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel), message: () => ({}) as IMessageModel
}); });
let selfAddValue = 0; let selfAddValue = 0;
@ -116,12 +103,7 @@ function performQuoteContent(params: IQuoteContent) {
messageKey = '[消息]'; messageKey = '[消息]';
break; break;
} }
if ( if ([MessageQuoteTypeEnum.TYPE_TEXT, MessageQuoteTypeEnum.TYPE_MERGER].includes(params.messageType)) {
[
MessageQuoteTypeEnum.TYPE_TEXT,
MessageQuoteTypeEnum.TYPE_MERGER,
].includes(params.messageType)
) {
quoteContent = params.messageAbstract; quoteContent = params.messageAbstract;
} }
return quoteContent ? quoteContent : TUITranslateService.t(`TUIChat.${messageKey}`); return quoteContent ? quoteContent : TUITranslateService.t(`TUIChat.${messageKey}`);
@ -133,7 +115,7 @@ async function scrollToOriginalMessage() {
} }
const originMessageID = messageQuoteContent.value?.messageID; const originMessageID = messageQuoteContent.value?.messageID;
const currentMessageList = TUIStore.getData(StoreName.CHAT, 'messageList'); const currentMessageList = TUIStore.getData(StoreName.CHAT, 'messageList');
const isOriginalMessageInScreen = currentMessageList.some(msg => msg.ID === originMessageID); const isOriginalMessageInScreen = currentMessageList.some((msg) => msg.ID === originMessageID);
if (originMessageID && isOriginalMessageInScreen) { if (originMessageID && isOriginalMessageInScreen) {
try { try {
const scrollViewRect = await getBoundingClientRect('#messageScrollList', 'messageList'); const scrollViewRect = await getBoundingClientRect('#messageScrollList', 'messageList');
@ -156,7 +138,7 @@ async function scrollToOriginalMessage() {
} else { } else {
Toast({ Toast({
message: TUITranslateService.t('TUIChat.无法定位到原消息'), message: TUITranslateService.t('TUIChat.无法定位到原消息'),
type: TOAST_TYPE.WARNING, type: TOAST_TYPE.WARNING
}); });
} }
} }
@ -180,8 +162,8 @@ async function scrollToOriginalMessage() {
} }
.reverse.reference-content { .reverse.reference-content {
margin-right: 44px; margin-right: 44px;
margin-left: auto; margin-left: auto;
} }
.revoked-text { .revoked-text {

View File

@ -56,5 +56,5 @@ export enum MessageQuoteTypeEnum {
/** /**
* merge forward message * merge forward message
*/ */
TYPE_MERGER = 10, TYPE_MERGER = 10
} }

View File

@ -1,20 +1,11 @@
<template> <template>
<div> <div>
<div <div class="message-record-container" @click="openMergeDetail">
class="message-record-container" <div class="record-title">
@click="openMergeDetail"
>
<div
class="record-title"
>
{{ props.renderData.title }} {{ props.renderData.title }}
</div> </div>
<div class="record-abstract-container"> <div class="record-abstract-container">
<div <div v-for="(item, index) in props.renderData.abstractList.slice(0, 7)" :key="index" class="record-abstract-item">
v-for="(item, index) in props.renderData.abstractList.slice(0, 7)"
:key="index"
class="record-abstract-item"
>
{{ transformTextWithKeysToEmojiNames(item) }} {{ transformTextWithKeysToEmojiNames(item) }}
</div> </div>
</div> </div>
@ -22,11 +13,7 @@
{{ TUITranslateService.t('TUIChat.聊天记录') }} {{ TUITranslateService.t('TUIChat.聊天记录') }}
</div> </div>
</div> </div>
<Overlay <Overlay v-if="!props.disabled && isPC" :visible="isMessageListVisible" @onOverlayClick="isMessageListVisible = false">
v-if="!props.disabled && isPC"
:visible="isMessageListVisible"
@onOverlayClick="isMessageListVisible = false"
>
<SimpleMessageList <SimpleMessageList
:isMounted="isMessageListVisible" :isMounted="isMessageListVisible"
:renderData="props.renderData" :renderData="props.renderData"
@ -83,7 +70,7 @@ interface IProps {
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
messageItem: () => ({}) as IMessageModel, messageItem: () => ({}) as IMessageModel,
disabled: false, disabled: false
}); });
const isMessageListVisible = ref(false); const isMessageListVisible = ref(false);

View File

@ -1,14 +1,7 @@
<template> <template>
<div class="message-stream"> <div class="message-stream">
<pre <pre ref="messageContentRef" :class="['message-marked', 'message-typewriter']" v-html="markedContent" />
ref="messageContentRef" <StreamOperation v-show="isOperationShow" :content="streamContent" />
:class="['message-marked', 'message-typewriter']"
v-html="markedContent"
/>
<StreamOperation
v-show="isOperationShow"
:content="streamContent"
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -30,7 +23,7 @@ const props = withDefaults(defineProps<IProps>(), {
payloadData: () => '', payloadData: () => '',
enableMarkdown: true, enableMarkdown: true,
enableStreaming: true, enableStreaming: true,
enableOperation: true, enableOperation: true
}); });
const emits = defineEmits(['onStreaming']); const emits = defineEmits(['onStreaming']);
@ -54,7 +47,7 @@ const typeWriter = new TypeWriter({
}, },
onComplete() { onComplete() {
isStreaming.value = false; isStreaming.value = false;
}, }
}); });
const generateMarkedContent = (content: string) => { const generateMarkedContent = (content: string) => {
@ -77,16 +70,17 @@ function startStreaming(content: string[]) {
} }
} }
watch(() => props.payloadData, watch(
() => props.payloadData,
(newValue: string, oldValue: string) => { (newValue: string, oldValue: string) => {
if (newValue === oldValue) { if (newValue === oldValue) {
return; return;
} }
if(props.enableMarkdown){ if (props.enableMarkdown) {
TUIReportService.reportFeature(206); TUIReportService.reportFeature(206);
} }
if(props.enableStreaming){ if (props.enableStreaming) {
TUIReportService.reportFeature(207); TUIReportService.reportFeature(207);
} }
@ -125,31 +119,36 @@ watch(() => props.payloadData,
} }
prevChunksLength.value = chunks.value?.length; prevChunksLength.value = chunks.value?.length;
}, {
deep: true,
immediate: true,
}, },
{
deep: true,
immediate: true
}
); );
onMounted(() => { onMounted(() => {
watch(() => isStreaming.value, (newValue: boolean, oldValue: boolean) => { watch(
if (newValue === oldValue) { () => isStreaming.value,
return; (newValue: boolean, oldValue: boolean) => {
if (newValue === oldValue) {
return;
}
if (!newValue && oldValue) {
emits('onStreaming', streamContent.value, streamContent.value);
}
if (!newValue) {
const codeCopyBtns = messageContentRef.value?.querySelectorAll('.message-marked_copy-btn');
codeCopyBtns?.forEach((btn: HTMLElement) => {
if (btn.addEventListener) {
btn.addEventListener('click', copyCode);
}
});
}
},
{
immediate: true
} }
if (!newValue && oldValue) { );
emits('onStreaming', streamContent.value, streamContent.value);
}
if (!newValue) {
const codeCopyBtns = messageContentRef.value?.querySelectorAll('.message-marked_copy-btn');
codeCopyBtns?.forEach((btn: HTMLElement) => {
if (btn.addEventListener) {
btn.addEventListener('click', copyCode);
}
});
}
}, {
immediate: true,
});
}); });
onUnmounted(() => { onUnmounted(() => {
@ -168,6 +167,5 @@ function copyCode(event: Event) {
CopyManager.copyTextOrHtml(codeElement.textContent, 'text'); CopyManager.copyTextOrHtml(codeElement.textContent, 'text');
} }
} }
</script> </script>
<style lang="scss" src="./index.scss"></style> <style lang="scss" src="./index.scss"></style>

View File

@ -15,19 +15,19 @@ const escapeReplacements: Iescape = {
'<': '&lt;', '<': '&lt;',
'>': '&gt;', '>': '&gt;',
'"': '&quot;', '"': '&quot;',
'\'': '&#39;', "'": '&#39;'
}; };
const getEscapeReplacement = (ch: string): string => escapeReplacements[ch as keyof Iescape]; const getEscapeReplacement = (ch: string): string => escapeReplacements[ch as keyof Iescape];
export const marked = new Marked( export const marked = new Marked(
{ {
mangle: false, mangle: false,
headerIds: false, headerIds: false
}, },
markedHighlight({ markedHighlight({
highlight(code: string) { highlight(code: string) {
return hljs.highlightAuto(code).value; return hljs.highlightAuto(code).value;
}, }
}), }),
{ {
renderer: { renderer: {
@ -48,9 +48,9 @@ export const marked = new Marked(
}, },
link(this: any, href: string | null, title: string | null, text: string) { link(this: any, href: string | null, title: string | null, text: string) {
return `<a target="_blank" rel="noreferrer noopenner" class="message-marked_link" href="${href}" title="${title}">${text}</a>`; return `<a target="_blank" rel="noreferrer noopenner" class="message-marked_link" href="${href}" title="${title}">${text}</a>`;
}, }
}, }
}, }
); );
export const markedWithPurify = (text: string) => { export const markedWithPurify = (text: string) => {

View File

@ -1,25 +1,17 @@
<template> <template>
<div <div ref="operationContainerRef" class="message-stream_operation_container">
ref="operationContainerRef"
class="message-stream_operation_container"
>
<div class="message-stream_operation_list"> <div class="message-stream_operation_list">
<div <div
v-for="(operation, key) in operationConfig" v-for="(operation, key) in operationConfig"
:key="key" :key="key"
:class="{ :class="{
'message-stream_operation_item': true, 'message-stream_operation_item': true,
'message-stream_operation_item_disabled': operation.isDisabled, 'message-stream_operation_item_disabled': operation.isDisabled
}" }"
:title="operation.name" :title="operation.name"
@click="(e) => operation.onClick(e, operation.key)" @click="(e) => operation.onClick(e, operation.key)"
> >
<Icon <Icon class="message-stream_operation_icon" :file="operation.icon" :name="operation.name" size="14px" />
class="message-stream_operation_icon"
:file="operation.icon"
:name="operation.name"
size="14px"
/>
</div> </div>
</div> </div>
</div> </div>
@ -45,7 +37,7 @@ interface IEmits {
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
operations: () => [IOperationType.Copy], operations: () => [IOperationType.Copy]
}); });
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
@ -57,8 +49,8 @@ const defaultOperationConfig: Record<string, IOperation> = {
isDisabled: false, isDisabled: false,
onClick: () => { onClick: () => {
CopyManager.copyTextOrHtml(props.content, 'text'); CopyManager.copyTextOrHtml(props.content, 'text');
}, }
}, }
}; };
const operationConfig = computed(() => { const operationConfig = computed(() => {
@ -76,34 +68,34 @@ const operationConfig = computed(() => {
} }
config.onClick(e, key); config.onClick(e, key);
emits('onOperationClick', e, key); emits('onOperationClick', e, key);
}, }
}; };
}) })
.filter(Boolean) as IOperation[]; .filter(Boolean) as IOperation[];
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.message-stream_operation_container { .message-stream_operation_container {
display: flex;
flex-direction: row;
align-self: flex-end;
padding: 5px;
border-radius: 3px;
margin: 3px;
.message-stream_operation_list {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-self: flex-end;
padding: 5px;
border-radius: 3px;
margin: 3px;
.message-stream_operation_list { .message-stream_operation_item {
display: flex; display: flex;
flex-direction: row; }
.message-stream_operation_item { .message-stream_operation_item_disabled {
display: flex; * {
} cursor: not-allowed;
.message-stream_operation_item_disabled {
* {
cursor: not-allowed;
}
} }
} }
} }
}
</style> </style>

View File

@ -1,6 +1,6 @@
/** type & interface */ /** type & interface */
export enum IOperationType { export enum IOperationType {
Copy = 'copy', Copy = 'copy'
// Retry is not supported now // Retry is not supported now
// Retry = 'retry', // Retry = 'retry',
} }

View File

@ -1,7 +1,7 @@
const chineseRegex = /[\u4e00-\u9fa5]/; const chineseRegex = /[\u4e00-\u9fa5]/;
const wordAndNonWordRegex = /\b\w+\b|[^\w]+/g; const wordAndNonWordRegex = /\b\w+\b|[^\w]+/g;
const isStringArray = (test: any): boolean => { const isStringArray = (test: any): boolean => {
return Array.isArray(test) && !test.some(value => typeof value !== 'string'); return Array.isArray(test) && !test.some((value) => typeof value !== 'string');
}; };
export class TypeWriter { export class TypeWriter {
@ -141,7 +141,7 @@ export class TypeWriter {
return; return;
} }
if ((this.curArrayPos >= this.strings.length)) { if (this.curArrayPos >= this.strings.length) {
this.isTyping = false; this.isTyping = false;
this.onComplete?.(this); this.onComplete?.(this);
return; return;

View File

@ -1,28 +1,13 @@
<template> <template>
<div :class="['message-text-container', isPC && 'text-select']"> <div :class="['message-text-container', isPC && 'text-select']">
<span <span v-for="(item, index) in processedContent" :key="index">
v-for="(item, index) in processedContent" <span v-if="item.name === 'text'" class="text">
:key="index"
>
<span
v-if="item.name === 'text'"
class="text"
>
{{ item.text }} {{ item.text }}
</span> </span>
<span <span v-else-if="item.name === 'url'" class="url-link" @click="navigateToUrl(item.url)">
v-else-if="item.name === 'url'"
class="url-link"
@click="navigateToUrl(item.url)"
>
{{ item.text }} {{ item.text }}
</span> </span>
<img <img v-else class="emoji" :src="item.src" :alt="item.emojiKey" />
v-else
class="emoji"
:src="item.src"
:alt="item.emojiKey"
>
</span> </span>
</div> </div>
</template> </template>
@ -51,8 +36,8 @@ interface TextItem {
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
content: () => ({}), content: () => ({}),
messageItem: () => ({} as IMessageModel), messageItem: () => ({}) as IMessageModel,
enableURLHighlight: false, enableURLHighlight: false
}); });
const processedContent = ref<TextItem>([]); const processedContent = ref<TextItem>([]);
@ -64,12 +49,12 @@ watch(
return; return;
} }
if(props.enableURLHighlight){ if (props.enableURLHighlight) {
TUIReportService.reportFeature(208); TUIReportService.reportFeature(208);
} }
if(props.messageItem.getMessageContent){ if (props.messageItem.getMessageContent) {
processedContent.value = props.messageItem.getMessageContent()?.text; processedContent.value = props.messageItem.getMessageContent()?.text;
} else { } else {
processedContent.value = TUIStore.getMessageModel(props.messageItem.ID)?.getMessageContent()?.text; processedContent.value = TUIStore.getMessageModel(props.messageItem.ID)?.getMessageContent()?.text;
} }
@ -80,45 +65,47 @@ watch(
return; return;
} }
processedContent.value = processedContent.value.map((item: TextItem) => { processedContent.value = processedContent.value
// handle custom emoji .map((item: TextItem) => {
if (item.name === 'img' && item?.type === 'custom') { // handle custom emoji
if (!CUSTOM_BASIC_EMOJI_URL) { if (item.name === 'img' && item?.type === 'custom') {
console.warn('CUSTOM_BASIC_EMOJI_URL is required for custom emoji.'); if (!CUSTOM_BASIC_EMOJI_URL) {
return item; console.warn('CUSTOM_BASIC_EMOJI_URL is required for custom emoji.');
return item;
}
if (!item.emojiKey || !CUSTOM_BASIC_EMOJI_URL_MAPPING[item.emojiKey]) {
console.warn('emojiKey is required for custom emoji.');
return item;
}
return {
...item,
src: CUSTOM_BASIC_EMOJI_URL + CUSTOM_BASIC_EMOJI_URL_MAPPING[item.emojiKey]
};
} }
if (!item.emojiKey || !CUSTOM_BASIC_EMOJI_URL_MAPPING[item.emojiKey]) {
console.warn('emojiKey is required for custom emoji.');
return item;
}
return {
...item,
src: CUSTOM_BASIC_EMOJI_URL + CUSTOM_BASIC_EMOJI_URL_MAPPING[item.emojiKey]
};
}
// handle url // handle url
if (props.enableURLHighlight && item.name === 'text' && item.text) { if (props.enableURLHighlight && item.name === 'text' && item.text) {
if(!parseTextAndValidateUrls){ if (!parseTextAndValidateUrls) {
console.warn('parseTextAndValidateUrls not found. Please update @tencentcloud/universal-api to 2.3.7 or higher.'); console.warn('parseTextAndValidateUrls not found. Please update @tencentcloud/universal-api to 2.3.7 or higher.');
return item; return item;
}
const segments = parseTextAndValidateUrls(item.text);
if (segments.length) {
return segments.map((segment) => ({
name: segment.type,
text: segment.text,
url: segment.url
}));
}
} }
const segments = parseTextAndValidateUrls(item.text);
if (segments.length) {
return segments.map((segment)=>({
name: segment.type,
text: segment.text,
url: segment.url,
}));
}
}
return item; return item;
})?.flat(); })
?.flat();
}, },
{ {
deep: true, deep: true,
immediate: true, immediate: true
} }
); );
@ -152,7 +139,9 @@ function navigateToUrl(url: string) {
user-select: text; user-select: text;
} }
.text,.emoji,.url-link{ .text,
.emoji,
.url-link {
&::selection { &::selection {
background-color: #b4d5fe; background-color: #b4d5fe;
color: inherit; color: inherit;
@ -167,7 +156,8 @@ function navigateToUrl(url: string) {
height: 20px; height: 20px;
} }
.text, .url-link { .text,
.url-link {
font-size: 14px; font-size: 14px;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;

View File

@ -1,8 +1,5 @@
<template> <template>
<div <div v-if="timestampShowFlag" class="message-timestamp">
v-if="timestampShowFlag"
class="message-timestamp"
>
{{ timestampShowContent }} {{ timestampShowContent }}
</div> </div>
</template> </template>
@ -13,12 +10,12 @@ import { calculateTimestamp } from '../../utils/utils';
const props = defineProps({ const props = defineProps({
currTime: { currTime: {
type: Number, type: Number,
default: 0, default: 0
}, },
prevTime: { prevTime: {
type: Number, type: Number,
default: 0, default: 0
}, }
}); });
const { currTime, prevTime } = toRefs(props); const { currTime, prevTime } = toRefs(props);
const timestampShowFlag = ref(false); const timestampShowFlag = ref(false);
@ -48,19 +45,16 @@ watch(
if (newVal?.toString() === oldVal?.toString()) { if (newVal?.toString() === oldVal?.toString()) {
return; return;
} else { } else {
timestampShowContent.value = handleItemTime( timestampShowContent.value = handleItemTime(currTime.value, prevTime.value);
currTime.value,
prevTime.value,
);
} }
}, },
{ {
immediate: true, immediate: true
}, }
); );
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.message-timestamp { .message-timestamp {
margin: 10px auto; margin: 10px auto;

View File

@ -9,13 +9,13 @@ import { computed } from '../../../../adapter-vue';
const props = defineProps({ const props = defineProps({
content: { content: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, }
}); });
const tipContent = computed(() => props.content?.text || props.content?.custom || ''); const tipContent = computed(() => props.content?.text || props.content?.custom || '');
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.message-tip { .message-tip {
margin: 0 auto; margin: 0 auto;

View File

@ -5,7 +5,7 @@
:class="{ :class="{
'message-translation': true, 'message-translation': true,
'reverse': props.message.flow === 'out', 'reverse': props.message.flow === 'out',
'error': hasTranslationError, 'error': hasTranslationError
}" }"
> >
<TranslationContent <TranslationContent
@ -16,10 +16,7 @@
@toggleErrorStatus="toggleErrorStatus" @toggleErrorStatus="toggleErrorStatus"
/> />
<div class="copyright"> <div class="copyright">
<Icon <Icon :file="checkIcon" size="13px" />
:file="checkIcon"
size="13px"
/>
<div class="copyright-text"> <div class="copyright-text">
{{ TUITranslateService.t('TUIChat.由IM提供翻译支持') }} {{ TUITranslateService.t('TUIChat.由IM提供翻译支持') }}
</div> </div>
@ -29,12 +26,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../../adapter-vue'; import { ref, onMounted, onUnmounted } from '../../../../../adapter-vue';
import { import { TUIStore, StoreName, IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../../common/Icon.vue'; import Icon from '../../../../common/Icon.vue';
import TranslationContent from './translation-content.vue'; import TranslationContent from './translation-content.vue';
import checkIcon from '../../../../../assets/icon/check-sm.svg'; import checkIcon from '../../../../../assets/icon/check-sm.svg';
@ -45,7 +37,7 @@ interface IProps {
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel), message: () => ({}) as IMessageModel
}); });
const translationVisible = ref<boolean>(false); const translationVisible = ref<boolean>(false);
@ -57,13 +49,13 @@ let isSingleTranslation = true;
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
translateTextInfo: onMessageTranslationUpdated, translateTextInfo: onMessageTranslationUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
translateTextInfo: onMessageTranslationUpdated, translateTextInfo: onMessageTranslationUpdated
}); });
}); });

View File

@ -3,7 +3,7 @@
class="message-translation-container" class="message-translation-container"
:style="{ :style="{
height: calculateHeight > 0 ? `${calculateHeight}px` : 'auto', height: calculateHeight > 0 ? `${calculateHeight}px` : 'auto',
width: calculateWidth > 0 ? `${calculateWidth}px` : 'auto', width: calculateWidth > 0 ? `${calculateWidth}px` : 'auto'
}" }"
> >
<div <div
@ -11,25 +11,13 @@
ref="translationContentRef" ref="translationContentRef"
:class="{ :class="{
'translation-content': true, 'translation-content': true,
'occur': calculateHeight > 0, 'occur': calculateHeight > 0
}" }"
> >
<template <template v-if="translationTextList.length > 0">
v-if="translationTextList.length > 0" <span v-for="(text, index) in translationTextList" :key="index">
> <img v-if="text.type === 'face'" class="text-face" :src="text.value" />
<span <span v-else class="text-plain">{{ text.value }}</span>
v-for="(text, index) in translationTextList"
:key="index"
>
<img
v-if="text.type === 'face'"
class="text-face"
:src="text.value"
>
<span
v-else
class="text-plain"
>{{ text.value }}</span>
</span> </span>
</template> </template>
<template v-else> <template v-else>
@ -50,10 +38,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch, nextTick } from '../../../../../adapter-vue'; import { ref, watch, nextTick } from '../../../../../adapter-vue';
import { import { IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { getBoundingClientRectSync } from '@tencentcloud/universal-api'; import { getBoundingClientRectSync } from '@tencentcloud/universal-api';
import { TranslationTextType, translator } from '../../../utils/translation'; import { TranslationTextType, translator } from '../../../utils/translation';
@ -70,7 +55,7 @@ interface IEmits {
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
message: () => ({} as IMessageModel), message: () => ({}) as IMessageModel
}); });
const translationFinished = ref<boolean>(false); const translationFinished = ref<boolean>(false);
@ -82,46 +67,51 @@ const calculateWidth = ref<number>(0);
const translationLoadingRef = ref<HTMLDivElement>(); const translationLoadingRef = ref<HTMLDivElement>();
const translationContentRef = ref<HTMLDivElement>(); const translationContentRef = ref<HTMLDivElement>();
watch(() => props.translationContentVisible, (newVal: boolean) => { watch(
if (newVal) { () => props.translationContentVisible,
translator.get(props.message) (newVal: boolean) => {
.then((result) => { if (newVal) {
translationFinished.value = true; translator
translationTextList.value = result; .get(props.message)
.then((result) => {
translationFinished.value = true;
translationTextList.value = result;
nextTick(() => { nextTick(() => {
const { height: originHeight, width: originWidth } = getBoundingClientRectSync(translationLoadingRef.value!); const { height: originHeight, width: originWidth } = getBoundingClientRectSync(translationLoadingRef.value!);
const { height, width } = getBoundingClientRectSync(translationContentRef.value!); const { height, width } = getBoundingClientRectSync(translationContentRef.value!);
calculateHeight.value = originHeight; calculateHeight.value = originHeight;
calculateWidth.value = originWidth; calculateWidth.value = originWidth;
requestAnimationFrame(() => { requestAnimationFrame(() => {
calculateHeight.value = height; calculateHeight.value = height;
calculateWidth.value = width; calculateWidth.value = width;
if (props.isSingleTranslation) { if (props.isSingleTranslation) {
nextTick(() => { nextTick(() => {
const { bottom } = getBoundingClientRectSync(props.translationWrapperRef); const { bottom } = getBoundingClientRectSync(props.translationWrapperRef);
const { bottom: bottomWindow } = getBoundingClientRectSync('#messageScrollList'); const { bottom: bottomWindow } = getBoundingClientRectSync('#messageScrollList');
if (bottom > bottomWindow) { if (bottom > bottomWindow) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
props.translationWrapperRef!.scrollIntoView({ block: 'end', behavior: 'smooth' }); props.translationWrapperRef!.scrollIntoView({ block: 'end', behavior: 'smooth' });
clearTimeout(timer); clearTimeout(timer);
}, 150); }, 150);
} }
}); });
} }
});
}); });
})
.catch((err) => {
translationFinished.value = true;
const { height: originHeight } = getBoundingClientRectSync(translationLoadingRef.value!);
calculateHeight.value = originHeight;
translationTextList.value = [];
emits('toggleErrorStatus', true);
translationErrorText.value = err.message;
}); });
}) }
.catch((err) => { },
translationFinished.value = true; { immediate: true }
const { height: originHeight } = getBoundingClientRectSync(translationLoadingRef.value!); );
calculateHeight.value = originHeight;
translationTextList.value = [];
emits('toggleErrorStatus', true);
translationErrorText.value = err.message;
});
}
}, { immediate: true });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -129,7 +119,9 @@ watch(() => props.translationContentVisible, (newVal: boolean) => {
min-height: 16px; min-height: 16px;
min-width: 80px; min-width: 80px;
position: relative; position: relative;
transition: width 0.15s ease-out, height 0.15s ease-out, ; transition:
width 0.15s ease-out,
height 0.15s ease-out;
font-size: 14px; font-size: 14px;
.loading { .loading {

View File

@ -3,21 +3,15 @@
<div <div
ref="skeleton" ref="skeleton"
class="message-video-box" class="message-video-box"
:class="[ :class="[(!props.messageItem.progress || props.messageItem.progress === 1) && !isPC && 'message-video-cover']"
(!props.messageItem.progress || props.messageItem.progress === 1)
&& !isPC
&& 'message-video-cover',
]"
@click="toggleVideoPreviewer" @click="toggleVideoPreviewer"
> >
<img <img
v-if="(props.messageItem.progress > 0 && props.messageItem.progress < 1 && poster) || v-if="(props.messageItem.progress > 0 && props.messageItem.progress < 1 && poster) || (!isPC && poster)"
(!isPC && poster)
"
class="message-img" class="message-img"
:class="[isWidth ? 'is-width' : 'is-height']" :class="[isWidth ? 'is-width' : 'is-height']"
:src="poster" :src="poster"
> />
<video <video
v-else-if="!isPC" v-else-if="!isPC"
ref="videoRef" ref="videoRef"
@ -27,51 +21,21 @@
preload="auto" preload="auto"
muted muted
/> />
<video <video v-else ref="videoRef" class="message-img video-web" :src="props.content.url" controls preload="metadata" :poster="poster" />
v-else
ref="videoRef"
class="message-img video-web"
:src="props.content.url"
controls
preload="metadata"
:poster="poster"
/>
</div> </div>
<div <div v-if="isShow && !isPC" class="dialog-video">
v-if="isShow && !isPC" <div class="dialog-video-close" @click.stop="toggleVideoPreviewer">
class="dialog-video"
>
<div
class="dialog-video-close"
@click.stop="toggleVideoPreviewer"
>
<Icon :file="closeSVG" /> <Icon :file="closeSVG" />
</div> </div>
<div <div class="dialog-video-box" :class="[!isPC ? 'dialog-video-h5' : '']" @click.self="toggleVideoPreviewer">
class="dialog-video-box" <video :class="[isWidth ? 'is-width' : 'is-height']" :src="props.content.url" controls autoplay />
:class="[!isPC ? 'dialog-video-h5' : '']"
@click.self="toggleVideoPreviewer"
>
<video
:class="[isWidth ? 'is-width' : 'is-height']"
:src="props.content.url"
controls
autoplay
/>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { ref, watch, computed, nextTick, watchEffect, withDefaults } from '../../../../adapter-vue';
ref,
watch,
computed,
nextTick,
watchEffect,
withDefaults,
} from '../../../../adapter-vue';
import { IMessageModel } from '@tencentcloud/chat-uikit-engine'; import { IMessageModel } from '@tencentcloud/chat-uikit-engine';
import { handleSkeletonSize } from '../../utils/utils'; import { handleSkeletonSize } from '../../utils/utils';
import Icon from '../../../common/Icon.vue'; import Icon from '../../../common/Icon.vue';
@ -85,9 +49,9 @@ const props = withDefaults(
messageItem: IMessageModel; messageItem: IMessageModel;
}>(), }>(),
{ {
content: () => ({} as IVideoMessageContent), content: () => ({}) as IVideoMessageContent,
messageItem: () => ({} as IMessageModel), messageItem: () => ({}) as IMessageModel
}, }
); );
const emits = defineEmits(['uploading']); const emits = defineEmits(['uploading']);
@ -103,8 +67,7 @@ watchEffect(async () => {
if (!props.content) return; if (!props.content) return;
poster.value = await handlePosterUrl(props.content, props.messageItem); poster.value = await handlePosterUrl(props.content, props.messageItem);
nextTick(async () => { nextTick(async () => {
const containerWidth const containerWidth = document.getElementById('messageScrollList')?.clientWidth || 0;
= document.getElementById('messageScrollList')?.clientWidth || 0;
const max = !isPC ? Math.min(containerWidth - 172, 300) : 300; const max = !isPC ? Math.min(containerWidth - 172, 300) : 300;
let size; let size;
if (props.messageItem.status === 'success') { if (props.messageItem.status === 'success') {
@ -116,10 +79,8 @@ watchEffect(async () => {
snapshotHeight = posterHeight.value; snapshotHeight = posterHeight.value;
} }
size = handleSkeletonSize(snapshotWidth, snapshotHeight, max, max); size = handleSkeletonSize(snapshotWidth, snapshotHeight, max, max);
skeleton?.value?.style skeleton?.value?.style && (skeleton.value.style.width = `${size.width}px`);
&& (skeleton.value.style.width = `${size.width}px`); skeleton?.value?.style && (skeleton.value.style.height = `${size.height}px`);
skeleton?.value?.style
&& (skeleton.value.style.height = `${size.height}px`);
if (isPC) { if (isPC) {
videoRef?.value?.style && (videoRef.value.style.width = `${size.width}px`); videoRef?.value?.style && (videoRef.value.style.width = `${size.width}px`);
videoRef?.value?.style && (videoRef.value.style.height = `${size.height}px`); videoRef?.value?.style && (videoRef.value.style.height = `${size.height}px`);
@ -135,11 +96,14 @@ const isWidth = computed(() => {
return snapshotWidth >= snapshotHeight; return snapshotWidth >= snapshotHeight;
}); });
watch(() => props.messageItem.status, (newVal: string, oldVal: string) => { watch(
if (newVal === 'success' && oldVal !== 'success') { () => props.messageItem.status,
emits('uploading'); (newVal: string, oldVal: string) => {
if (newVal === 'success' && oldVal !== 'success') {
emits('uploading');
}
} }
}); );
function toggleVideoPreviewer() { function toggleVideoPreviewer() {
// Video upload process does not support full-screen playback. // Video upload process does not support full-screen playback.
@ -172,7 +136,7 @@ function getVideoBase64(url: string) {
posterHeight.value = height; posterHeight.value = height;
resolve(dataURL); resolve(dataURL);
}, },
{ once: true }, { once: true }
); );
}); });
} }
@ -183,19 +147,17 @@ async function handlePosterUrl(messgeContent: IVideoMessageContent, messageItem:
return await getVideoBase64(messgeContent.url); return await getVideoBase64(messgeContent.url);
} else { } else {
return ( return (
(messgeContent.snapshotUrl !== transparentPosterUrl && messgeContent.snapshotUrl) (messgeContent.snapshotUrl !== transparentPosterUrl && messgeContent.snapshotUrl) ||
|| (messageItem?.payload?.snapshotUrl !== transparentPosterUrl (messageItem?.payload?.snapshotUrl !== transparentPosterUrl && messageItem?.payload?.snapshotUrl) ||
&& messageItem?.payload?.snapshotUrl) (messageItem.payload?.thumbUrl !== transparentPosterUrl && messageItem?.payload?.thumbUrl) ||
|| (messageItem.payload?.thumbUrl !== transparentPosterUrl (await getVideoBase64(messgeContent.url))
&& messageItem?.payload?.thumbUrl)
|| (await getVideoBase64(messgeContent.url))
); );
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.message-video { .message-video {
position: relative; position: relative;
@ -223,7 +185,7 @@ async function handlePosterUrl(messgeContent: IVideoMessageContent, messageItem:
border-radius: 10px; border-radius: 10px;
} }
img[src=""], img[src=''],
img:not([src]) { img:not([src]) {
opacity: 0; opacity: 0;
} }
@ -236,7 +198,7 @@ async function handlePosterUrl(messgeContent: IVideoMessageContent, messageItem:
&::before { &::before {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
content: ""; content: '';
width: 0; width: 0;
height: 0; height: 0;
border: 10px solid transparent; border: 10px solid transparent;

View File

@ -4,7 +4,7 @@
:class="{ :class="{
'message-label': true, 'message-label': true,
'unread': isUseUnreadStyle, 'unread': isUseUnreadStyle,
'finger-point': isHoverFingerPointer, 'finger-point': isHoverFingerPointer
}" }"
@click="openReadUserPanel" @click="openReadUserPanel"
> >
@ -14,12 +14,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from '../../../../../adapter-vue'; import { computed, ref, onMounted, onUnmounted } from '../../../../../adapter-vue';
import TUIChatEngine, { import TUIChatEngine, { TUIStore, StoreName, IMessageModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
IMessageModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatConfig from '../../../config'; import TUIChatConfig from '../../../config';
interface IProps { interface IProps {
@ -32,7 +27,7 @@ interface IEmits {
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
message: () => ({}) as IMessageModel, message: () => ({}) as IMessageModel
}); });
const ReadStatus = TUIChatConfig.getFeatureConfig('ReadStatus'); const ReadStatus = TUIChatConfig.getFeatureConfig('ReadStatus');
@ -41,7 +36,7 @@ enum ReadState {
Unread, Unread,
AllRead, AllRead,
NotShow, NotShow,
PartiallyRead, PartiallyRead
} }
const TYPES = TUIChatEngine.TYPES; const TYPES = TUIChatEngine.TYPES;
@ -50,13 +45,13 @@ const isDisplayMessageReadReceipt = ref<boolean>(TUIStore.getData(StoreName.USER
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.USER, { TUIStore.watch(StoreName.USER, {
displayMessageReadReceipt: onDisplayMessageReadReceiptUpdate, displayMessageReadReceipt: onDisplayMessageReadReceiptUpdate
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.USER, { TUIStore.unwatch(StoreName.USER, {
displayMessageReadReceipt: onDisplayMessageReadReceiptUpdate, displayMessageReadReceipt: onDisplayMessageReadReceiptUpdate
}); });
}); });
@ -67,16 +62,7 @@ const isShowReadStatus = computed<boolean>(() => {
if (!isDisplayMessageReadReceipt.value) { if (!isDisplayMessageReadReceipt.value) {
return false; return false;
} }
const { const { ID, type, flow, status, hasRiskContent, conversationID, conversationType, needReadReceipt = false } = props.message;
ID,
type,
flow,
status,
hasRiskContent,
conversationID,
conversationType,
needReadReceipt = false,
} = props.message;
// Asynchronous message strike: Determine if there is risky content after the message has been sent // Asynchronous message strike: Determine if there is risky content after the message has been sent
if (hasRiskContent) { if (hasRiskContent) {
@ -163,9 +149,9 @@ const isUseUnreadStyle = computed(() => {
const isHoverFingerPointer = computed<boolean>(() => { const isHoverFingerPointer = computed<boolean>(() => {
return ( return (
props.message.needReadReceipt props.message.needReadReceipt &&
&& props.message.conversationType === 'GROUP' props.message.conversationType === 'GROUP' &&
&& (readState.value === ReadState.PartiallyRead || readState.value === ReadState.Unread) (readState.value === ReadState.PartiallyRead || readState.value === ReadState.Unread)
); );
}); });

View File

@ -2,19 +2,12 @@
<div <div
:class="{ :class="{
'simple-message-list-container': true, 'simple-message-list-container': true,
'simple-message-list-container-mobile': isMobile, 'simple-message-list-container-mobile': isMobile
}" }"
> >
<div class="header-container"> <div class="header-container">
<span <span class="back" @click="backPreviousLevel">
class="back" <Icon class="close-icon" :file="addIcon" :size="'18px'" />
@click="backPreviousLevel"
>
<Icon
class="close-icon"
:file="addIcon"
:size="'18px'"
/>
<span v-if="isReturn">{{ TUITranslateService.t('TUIChat.返回') }}</span> <span v-if="isReturn">{{ TUITranslateService.t('TUIChat.返回') }}</span>
<span v-else>{{ TUITranslateService.t('TUIChat.关闭') }}</span> <span v-else>{{ TUITranslateService.t('TUIChat.关闭') }}</span>
</span> </span>
@ -23,135 +16,61 @@
{{ currentMergeMessageInfo.title }} {{ currentMergeMessageInfo.title }}
</span> </span>
</div> </div>
<div v-if="isDownloadOccurError"> <div v-if="isDownloadOccurError">Load Merge Message Error</div>
Load Merge Message Error <div v-else-if="isMergeMessageInfoLoaded" ref="simpleMessageListRef" class="message-list">
</div>
<div
v-else-if="isMergeMessageInfoLoaded"
ref="simpleMessageListRef"
class="message-list"
>
<div <div
v-for="item in currentMergeMessageInfo.messageList" v-for="item in currentMergeMessageInfo.messageList"
:key="item.ID" :key="item.ID"
:class="{ :class="{
'message-item': true, 'message-item': true
}" }"
> >
<MessageContainer <MessageContainer :sender="item.nick" :avatar="item.avatar" :type="item.messageBody[0].type" :time="item.time">
:sender="item.nick"
:avatar="item.avatar"
:type="item.messageBody[0].type"
:time="item.time"
>
<!-- text --> <!-- text -->
<div <div v-if="item.messageBody[0].type === TYPES.MSG_TEXT" class="message-text">
v-if="item.messageBody[0].type === TYPES.MSG_TEXT"
class="message-text"
>
<span <span
v-for="(textInfo, index) in parseTextToRenderArray(item.messageBody[0].payload['text'])" v-for="(textInfo, index) in parseTextToRenderArray(item.messageBody[0].payload['text'])"
:key="index" :key="index"
class="message-text-container" class="message-text-container"
> >
<span <span v-if="textInfo.type === 'text'" class="text">{{ textInfo.content }}</span>
v-if="textInfo.type === 'text'" <img v-else class="simple-emoji" :src="textInfo.content" alt="small-face" />
class="text"
>{{ textInfo.content }}</span>
<img
v-else
class="simple-emoji"
:src="textInfo.content"
alt="small-face"
>
</span> </span>
</div> </div>
<!-- image --> <!-- image -->
<div <div v-else-if="item.messageBody[0].type === TYPES.MSG_IMAGE" class="message-image">
v-else-if="item.messageBody[0].type === TYPES.MSG_IMAGE" <img class="image" :src="item.messageBody[0].payload['imageInfoArray'][2]['url']" mode="widthFix" alt="image" />
class="message-image"
>
<img
class="image"
:src="(item.messageBody[0].payload)['imageInfoArray'][2]['url']"
mode="widthFix"
alt="image"
>
</div> </div>
<!-- video --> <!-- video -->
<div <div v-else-if="item.messageBody[0].type === TYPES.MSG_VIDEO" class="message-video">
v-else-if="item.messageBody[0].type === TYPES.MSG_VIDEO" <div v-if="isUniFrameWork" @click="previewVideoInUniapp(item.messageBody[0].payload['remoteVideoUrl'])">
class="message-video" <image class="image" :src="item.messageBody[0].payload['thumbUrl']" mode="widthFix" alt="image" />
> <Icon class="video-play-icon" :file="playIcon" />
<div
v-if="isUniFrameWork"
@click="previewVideoInUniapp((item.messageBody[0].payload)['remoteVideoUrl'])"
>
<image
class="image"
:src="(item.messageBody[0].payload)['thumbUrl']"
mode="widthFix"
alt="image"
/>
<Icon
class="video-play-icon"
:file="playIcon"
/>
</div> </div>
<video <video v-else class="video" controls :poster="item.messageBody[0].payload['thumbUrl']">
v-else <source :src="item.messageBody[0].payload['remoteVideoUrl']" type="video/mp4" />
class="video"
controls
:poster="(item.messageBody[0].payload)['thumbUrl']"
>
<source
:src="(item.messageBody[0].payload)['remoteVideoUrl']"
type="video/mp4"
>
</video> </video>
</div> </div>
<!-- audio --> <!-- audio -->
<div <div v-else-if="item.messageBody[0].type === TYPES.MSG_AUDIO" class="message-audio">
v-else-if="item.messageBody[0].type === TYPES.MSG_AUDIO" <span>{{ TUITranslateService.t('TUIChat.语音') }}&nbsp;</span>
class="message-audio"
>
<span>{{ TUITranslateService.t("TUIChat.语音") }}&nbsp;</span>
<span>{{ item.messageBody[0].payload.second }}s</span> <span>{{ item.messageBody[0].payload.second }}s</span>
</div> </div>
<!-- big face --> <!-- big face -->
<div <div v-else-if="item.messageBody[0].type === TYPES.MSG_FACE" class="message-face">
v-else-if="item.messageBody[0].type === TYPES.MSG_FACE" <img class="image" :src="resolveBigFaceUrl(item.messageBody[0].payload.data)" alt="face" />
class="message-face"
>
<img
class="image"
:src="resolveBigFaceUrl(item.messageBody[0].payload.data)"
alt="face"
>
</div> </div>
<!-- file --> <!-- file -->
<div <div v-else-if="item.messageBody[0].type === TYPES.MSG_FILE" class="message-file">
v-else-if="item.messageBody[0].type === TYPES.MSG_FILE"
class="message-file"
>
{{ TUITranslateService.t('TUIChat.[文件]') }} {{ TUITranslateService.t('TUIChat.[文件]') }}
</div> </div>
<!-- location --> <!-- location -->
<div <div v-else-if="item.messageBody[0].type === TYPES.MSG_LOCATION">
v-else-if="item.messageBody[0].type === TYPES.MSG_LOCATION"
>
{{ TUITranslateService.t('TUIChat.[地理位置]') }} {{ TUITranslateService.t('TUIChat.[地理位置]') }}
</div> </div>
<!-- merger --> <!-- merger -->
<div <div v-else-if="item.messageBody[0].type === TYPES.MSG_MERGER" class="message-merger" @click.capture="entryNextLevel($event, item)">
v-else-if="item.messageBody[0].type === TYPES.MSG_MERGER" <MessageRecord disabled :renderData="item.messageBody[0].payload" />
class="message-merger"
@click.capture="entryNextLevel($event, item)"
>
<MessageRecord
disabled
:renderData="item.messageBody[0].payload"
/>
</div> </div>
<!-- custom --> <!-- custom -->
<div v-else-if="item.messageBody[0].type === TYPES.MSG_CUSTOM"> <div v-else-if="item.messageBody[0].type === TYPES.MSG_CUSTOM">
@ -165,11 +84,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from '../../../../../adapter-vue'; import { computed, ref, watch } from '../../../../../adapter-vue';
import TUIChatEngine, { import TUIChatEngine, { TUIStore, TUIChatService, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUIStore,
TUIChatService,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import addIcon from '../../../../../assets/icon/back.svg'; import addIcon from '../../../../../assets/icon/back.svg';
import playIcon from '../../../../../assets/icon/video-play.png'; import playIcon from '../../../../../assets/icon/video-play.png';
import Icon from '../../../../common/Icon.vue'; import Icon from '../../../../common/Icon.vue';
@ -197,7 +112,7 @@ interface IEmits {
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
messageID: '', messageID: '',
isMounted: false, isMounted: false
}); });
const TYPES = TUIChatEngine.TYPES; const TYPES = TUIChatEngine.TYPES;
@ -205,45 +120,52 @@ const isDownloadOccurError = ref(false);
const messageListStack = ref<IMergeMessageContent[]>([]); const messageListStack = ref<IMergeMessageContent[]>([]);
const currentMergeMessageInfo = ref<Partial<IMergeMessageContent>>({ const currentMergeMessageInfo = ref<Partial<IMergeMessageContent>>({
title: '', title: '',
messageList: [], messageList: []
}); });
const simpleMessageListRef = ref<HTMLElement>(); const simpleMessageListRef = ref<HTMLElement>();
watch(() => messageListStack.value.length, async (newValue) => { watch(
isDownloadOccurError.value = false; () => messageListStack.value.length,
if (newValue < 1) { async (newValue) => {
return; isDownloadOccurError.value = false;
} if (newValue < 1) {
const stackTopMessageInfo = messageListStack.value[messageListStack.value.length - 1]; return;
if (stackTopMessageInfo.downloadKey && stackTopMessageInfo.messageList.length === 0) {
try {
const res = await TUIChatService.downloadMergedMessages({
payload: stackTopMessageInfo,
type: TUIChatEngine.TYPES.MSG_MERGER,
} as any);
// if download complete message, cover the original message in stack top
messageListStack.value[messageListStack.value.length - 1] = res.payload;
} catch (error) {
isDownloadOccurError.value = true;
} }
const stackTopMessageInfo = messageListStack.value[messageListStack.value.length - 1];
if (stackTopMessageInfo.downloadKey && stackTopMessageInfo.messageList.length === 0) {
try {
const res = await TUIChatService.downloadMergedMessages({
payload: stackTopMessageInfo,
type: TUIChatEngine.TYPES.MSG_MERGER
} as any);
// if download complete message, cover the original message in stack top
messageListStack.value[messageListStack.value.length - 1] = res.payload;
} catch (error) {
isDownloadOccurError.value = true;
}
}
currentMergeMessageInfo.value = messageListStack.value[messageListStack.value.length - 1];
} }
currentMergeMessageInfo.value = messageListStack.value[messageListStack.value.length - 1]; );
});
watch(() => props.isMounted, (newValue) => { watch(
// For compatibility with uniapp, use watch to implement onMounted () => props.isMounted,
if (newValue) { (newValue) => {
if (!props.messageID) { // For compatibility with uniapp, use watch to implement onMounted
throw new Error('messageID is required when first render of simple-message-list.'); if (newValue) {
if (!props.messageID) {
throw new Error('messageID is required when first render of simple-message-list.');
}
const sdkMessagePayload = TUIStore.getMessageModel(props.messageID).getMessage().payload;
messageListStack.value = [sdkMessagePayload];
} else {
messageListStack.value = [];
} }
const sdkMessagePayload = TUIStore.getMessageModel(props.messageID).getMessage().payload; },
messageListStack.value = [sdkMessagePayload]; {
} else { immediate: true
messageListStack.value = [];
} }
}, { );
immediate: true,
});
const isReturn = computed(() => { const isReturn = computed(() => {
return messageListStack.value.length > 1; return messageListStack.value.length > 1;
@ -269,7 +191,7 @@ function previewVideoInUniapp(url: string) {
if (isUniFrameWork) { if (isUniFrameWork) {
const encodedUrl = encodeURIComponent(url); const encodedUrl = encodeURIComponent(url);
uni.navigateTo({ uni.navigateTo({
url: `/TUIKit/components/TUIChat/video-play?videoUrl=${encodedUrl}`, url: `/TUIKit/components/TUIChat/video-play?videoUrl=${encodedUrl}`
}); });
} }
} }
@ -291,7 +213,7 @@ function resolveBigFaceUrl(bigFaceKey: string): string {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
:not(not){ :not(not) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
@ -344,7 +266,6 @@ function resolveBigFaceUrl(bigFaceKey: string): string {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
} }
.message-list { .message-list {

View File

@ -17,7 +17,7 @@
<slot /> <slot />
</div> </div>
<div class="timestamp"> <div class="timestamp">
{{ calculateTimestamp(props.time*1000) }} {{ calculateTimestamp(props.time * 1000) }}
</div> </div>
</div> </div>
</div> </div>
@ -39,7 +39,7 @@ interface IProps {
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
sender: '', sender: '',
avatar: '', avatar: ''
}); });
const TYPES = TUIChatEngine.TYPES; const TYPES = TUIChatEngine.TYPES;
@ -50,7 +50,7 @@ const isNoPadding = computed(() => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
:not(not){ :not(not) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;

View File

@ -1,17 +1,9 @@
<template> <template>
<div> <div>
<div <div v-if="groupApplicationCount > 0" class="application-tips">
v-if="groupApplicationCount > 0" <div>{{ groupApplicationCount }}{{ TUITranslateService.t('TUIChat.条入群申请') }}</div>
class="application-tips" <div class="application-tips-btn" @click="toggleGroupApplicationDrawerShow">
> {{ TUITranslateService.t('TUIChat.点击处理') }}
<div>
{{ groupApplicationCount }}{{ TUITranslateService.t("TUIChat.条入群申请") }}
</div>
<div
class="application-tips-btn"
@click="toggleGroupApplicationDrawerShow"
>
{{ TUITranslateService.t("TUIChat.点击处理") }}
</div> </div>
</div> </div>
<Drawer <Drawer
@ -25,12 +17,12 @@
bottom: { bottom: {
minHeight: '60vh', minHeight: '60vh',
maxHeight: '80vh', maxHeight: '80vh',
borderRadius: '12px 12px 0 0', borderRadius: '12px 12px 0 0'
}, },
right: { right: {
width: '360px', width: '360px',
borderRadius: '12px 0 0 12px', borderRadius: '12px 0 0 12px',
boxShadow: '0 0 10px 0 #d0d0d0', boxShadow: '0 0 10px 0 #d0d0d0'
} }
}" }"
@onOverlayClick="toggleGroupApplicationDrawerShow" @onOverlayClick="toggleGroupApplicationDrawerShow"
@ -41,18 +33,10 @@
}" }"
> >
<header class="application-header"> <header class="application-header">
<div <div @click="toggleGroupApplicationDrawerShow">
@click="toggleGroupApplicationDrawerShow" <Icon v-if="isPC" :file="closeIcon" :size="'16px'" />
>
<Icon
v-if="isPC"
:file="closeIcon"
:size="'16px'"
/>
<div v-else> <div v-else>
{{ {{ TUITranslateService.t('关闭') }}
TUITranslateService.t('关闭')
}}
</div> </div>
</div> </div>
</header> </header>
@ -62,12 +46,12 @@
:key="item.nick" :key="item.nick"
:class="{ :class="{
'application-item': true, 'application-item': true,
'removed': item.isRemoved, 'removed': item.isRemoved
}" }"
> >
<Avatar <Avatar
:style="{ :style="{
flex: '0 0 auto', flex: '0 0 auto'
}" }"
:url="item.avatar" :url="item.avatar"
:useSkeletonAnimation="true" :useSkeletonAnimation="true"
@ -77,23 +61,15 @@
{{ item.nick }} {{ item.nick }}
</div> </div>
<div class="application-item-note"> <div class="application-item-note">
{{ TUITranslateService.t("TUIChat.申请加入") }} {{ TUITranslateService.t('TUIChat.申请加入') }}
</div> </div>
</div> </div>
<div <div class="application-item-operation">
class="application-item-operation" <div class="agree" @click="handleApplication(item, 'Agree', index)">
> {{ TUITranslateService.t('TUIChat.同意') }}
<div
class="agree"
@click="handleApplication(item, 'Agree', index)"
>
{{ TUITranslateService.t("TUIChat.同意") }}
</div> </div>
<div <div class="reject" @click="handleApplication(item, 'Reject', index)">
class="reject" {{ TUITranslateService.t('TUIChat.拒绝') }}
@click="handleApplication(item, 'Reject', index)"
>
{{ TUITranslateService.t("TUIChat.拒绝") }}
</div> </div>
</div> </div>
</div> </div>
@ -105,13 +81,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from '../../../../adapter-vue'; import { ref, onMounted, onUnmounted, watch } from '../../../../adapter-vue';
import { import { TUIStore, StoreName, TUITranslateService, TUIUserService, TUIGroupService } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
TUITranslateService,
TUIUserService,
TUIGroupService,
} from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../common/Icon.vue'; import Icon from '../../../common/Icon.vue';
import Avatar from '../../../common/Avatar/index.vue'; import Avatar from '../../../common/Avatar/index.vue';
import Drawer from '../../../common/Drawer/index.vue'; import Drawer from '../../../common/Drawer/index.vue';
@ -130,7 +100,7 @@ interface ICustomGroupApplication {
} }
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
groupID: '', groupID: ''
}); });
const drawerDomInstanceRef = ref<InstanceType<typeof Drawer>>(); const drawerDomInstanceRef = ref<InstanceType<typeof Drawer>>();
@ -147,11 +117,14 @@ watch(isGroupApplicationDrawerShow, (newVal) => {
} }
}); });
watch(() => customGroupApplicationList.value.length, (newVal, oldVal) => { watch(
if (oldVal > 0 && newVal === 0) { () => customGroupApplicationList.value.length,
isGroupApplicationDrawerShow.value = false; (newVal, oldVal) => {
if (oldVal > 0 && newVal === 0) {
isGroupApplicationDrawerShow.value = false;
}
} }
}); );
/** /**
* Retrieves the current group application list based on the provided groupID. * Retrieves the current group application list based on the provided groupID.
@ -160,7 +133,7 @@ watch(() => customGroupApplicationList.value.length, (newVal, oldVal) => {
*/ */
async function getCurrentGroupApplicationList(): Promise<IGroupApplication[]> { async function getCurrentGroupApplicationList(): Promise<IGroupApplication[]> {
const result: IChatResponese<{ applicationList: IGroupApplication[] }> = await TUIGroupService.getGroupApplicationList(); const result: IChatResponese<{ applicationList: IGroupApplication[] }> = await TUIGroupService.getGroupApplicationList();
const currentGroupApplicationList = result.data.applicationList.filter(application => application.groupID === props.groupID); const currentGroupApplicationList = result.data.applicationList.filter((application) => application.groupID === props.groupID);
return currentGroupApplicationList; return currentGroupApplicationList;
} }
@ -173,8 +146,8 @@ async function generateCustomGroupApplicationList(): Promise<ICustomGroupApplica
if (applicationList.length === 0) { if (applicationList.length === 0) {
return []; return [];
} }
const userIDList = applicationList.map(application => application.applicationType === 0 ? application.applicant : application.userID); const userIDList = applicationList.map((application) => (application.applicationType === 0 ? application.applicant : application.userID));
const { data: userProfileList } = await TUIUserService.getUserProfile({ userIDList }) as IChatResponese<IUserProfile[]>; const { data: userProfileList } = (await TUIUserService.getUserProfile({ userIDList })) as IChatResponese<IUserProfile[]>;
const mappingFromUserID2Profile: Record<string, IUserProfile> = {}; const mappingFromUserID2Profile: Record<string, IUserProfile> = {};
userProfileList.forEach((profile: IUserProfile) => { userProfileList.forEach((profile: IUserProfile) => {
mappingFromUserID2Profile[profile.userID] = profile; mappingFromUserID2Profile[profile.userID] = profile;
@ -185,7 +158,7 @@ async function generateCustomGroupApplicationList(): Promise<ICustomGroupApplica
nick: profile.nick || profile.userID || 'anonymous', nick: profile.nick || profile.userID || 'anonymous',
avatar: profile.avatar || '', avatar: profile.avatar || '',
isRemoved: false, isRemoved: false,
application: application, application: application
}; };
}); });
@ -195,16 +168,18 @@ async function generateCustomGroupApplicationList(): Promise<ICustomGroupApplica
function handleApplication(customApplication: ICustomGroupApplication, action: 'Agree' | 'Reject', index: number) { function handleApplication(customApplication: ICustomGroupApplication, action: 'Agree' | 'Reject', index: number) {
TUIGroupService.handleGroupApplication({ TUIGroupService.handleGroupApplication({
handleAction: action, handleAction: action,
application: customApplication.application, application: customApplication.application
}).then(() => { })
customGroupApplicationList.value[index].isRemoved = true; .then(() => {
setTimeout(() => { customGroupApplicationList.value[index].isRemoved = true;
customGroupApplicationList.value.splice(index, 1); setTimeout(() => {
groupApplicationCount.value -= 1; customGroupApplicationList.value.splice(index, 1);
}, 150); groupApplicationCount.value -= 1;
}).catch(() => { }, 150);
// TODO: handle error })
}); .catch(() => {
// TODO: handle error
});
} }
// --------------- mounted function --------------- // --------------- mounted function ---------------
@ -215,13 +190,13 @@ onMounted(() => {
}); });
TUIStore.watch(StoreName.GRP, { TUIStore.watch(StoreName.GRP, {
groupSystemNoticeList: onGroupSystemNoticeListUpdated, groupSystemNoticeList: onGroupSystemNoticeListUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.GRP, { TUIStore.unwatch(StoreName.GRP, {
groupSystemNoticeList: onGroupSystemNoticeListUpdated, groupSystemNoticeList: onGroupSystemNoticeListUpdated
}); });
}); });
@ -253,7 +228,7 @@ function onGroupSystemNoticeListUpdated() {
width: 100%; width: 100%;
padding: 5px 0; padding: 5px 0;
font-size: 14px; font-size: 14px;
background-color: #fce4d3; background-color: #fce4d3;
.application-tips-btn { .application-tips-btn {
color: #006eff; color: #006eff;
@ -316,15 +291,15 @@ function onGroupSystemNoticeListUpdated() {
flex: 0 0 auto; flex: 0 0 auto;
font-size: 14px; font-size: 14px;
.agree{ .agree {
color: #679ce1; color: #679ce1;
cursor: pointer cursor: pointer;
} }
.reject{ .reject {
margin-left: 12px; margin-left: 12px;
color: #fb355d; color: #fb355d;
cursor: pointer cursor: pointer;
} }
} }
} }

View File

@ -4,26 +4,11 @@
ref="messageToolDom" ref="messageToolDom"
:class="['dialog-item', !isPC ? 'dialog-item-h5' : 'dialog-item-web']" :class="['dialog-item', !isPC ? 'dialog-item-h5' : 'dialog-item-web']"
> >
<slot <slot v-if="featureConfig.EmojiReaction" name="TUIEmojiPlugin" />
v-if="featureConfig.EmojiReaction" <div class="dialog-item-list" :class="!isPC ? 'dialog-item-list-h5' : 'dialog-item-list-web'">
name="TUIEmojiPlugin"
/>
<div
class="dialog-item-list"
:class="!isPC ? 'dialog-item-list-h5' : 'dialog-item-list-web'"
>
<template v-for="(item, index) in actionItems"> <template v-for="(item, index) in actionItems">
<div <div v-if="item.renderCondition()" :key="item.key" class="list-item" @click="getFunction(index)" @mousedown="beforeCopy(item.key)">
v-if="item.renderCondition()" <Icon :file="item.iconUrl" :size="'15px'" />
:key="item.key"
class="list-item"
@click="getFunction(index)"
@mousedown="beforeCopy(item.key)"
>
<Icon
:file="item.iconUrl"
:size="'15px'"
/>
<span class="list-item-text">{{ item.text }}</span> <span class="list-item-text">{{ item.text }}</span>
</div> </div>
</template> </template>
@ -32,12 +17,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import TUIChatEngine, { import TUIChatEngine, { TUIStore, StoreName, TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
TUITranslateService,
IMessageModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, watchEffect, computed, onMounted, onUnmounted } from '../../../../adapter-vue'; import { ref, watchEffect, computed, onMounted, onUnmounted } from '../../../../adapter-vue';
import Icon from '../../../common/Icon.vue'; import Icon from '../../../common/Icon.vue';
@ -73,7 +53,7 @@ interface IEmits {
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
isMultipleSelectMode: false, isMultipleSelectMode: false,
messageItem: () => ({}) as IMessageModel, messageItem: () => ({}) as IMessageModel
}); });
const featureConfig = TUIChatConfig.getFeatureConfig(); const featureConfig = TUIChatConfig.getFeatureConfig();
@ -86,11 +66,9 @@ const actionItems = ref([
iconUrl: copyIcon, iconUrl: copyIcon,
renderCondition() { renderCondition() {
if (!featureConfig.DownloadFile || !message.value) return false; if (!featureConfig.DownloadFile || !message.value) return false;
return isPC && (message.value?.type === TYPES.MSG_FILE return isPC && (message.value?.type === TYPES.MSG_FILE || message.value.type === TYPES.MSG_VIDEO || message.value.type === TYPES.MSG_IMAGE);
|| message.value.type === TYPES.MSG_VIDEO
|| message.value.type === TYPES.MSG_IMAGE);
}, },
clickEvent: openMessage, clickEvent: openMessage
}, },
{ {
key: 'copy', key: 'copy',
@ -100,7 +78,7 @@ const actionItems = ref([
if (!featureConfig.CopyMessage || !message.value) return false; if (!featureConfig.CopyMessage || !message.value) return false;
return message.value.type === TYPES.MSG_TEXT; return message.value.type === TYPES.MSG_TEXT;
}, },
clickEvent: copyMessage, clickEvent: copyMessage
}, },
{ {
key: 'revoke', key: 'revoke',
@ -110,7 +88,7 @@ const actionItems = ref([
if (!featureConfig.RevokeMessage || !message.value) return false; if (!featureConfig.RevokeMessage || !message.value) return false;
return message.value.flow === 'out' && message.value.status === 'success'; return message.value.flow === 'out' && message.value.status === 'success';
}, },
clickEvent: revokeMessage, clickEvent: revokeMessage
}, },
{ {
key: 'delete', key: 'delete',
@ -120,7 +98,7 @@ const actionItems = ref([
if (!featureConfig.DeleteMessage || !message.value) return false; if (!featureConfig.DeleteMessage || !message.value) return false;
return message.value.status === 'success'; return message.value.status === 'success';
}, },
clickEvent: deleteMessage, clickEvent: deleteMessage
}, },
{ {
key: 'forward', key: 'forward',
@ -130,7 +108,7 @@ const actionItems = ref([
if (!featureConfig.ForwardMessage || !message.value) return false; if (!featureConfig.ForwardMessage || !message.value) return false;
return message.value.status === 'success'; return message.value.status === 'success';
}, },
clickEvent: forwardSingleMessage, clickEvent: forwardSingleMessage
}, },
{ {
key: 'quote', key: 'quote',
@ -141,7 +119,7 @@ const actionItems = ref([
const _message = TUIStore.getMessageModel(message.value.ID); const _message = TUIStore.getMessageModel(message.value.ID);
return message.value.status === 'success' && !_message.getSignalingInfo(); return message.value.status === 'success' && !_message.getSignalingInfo();
}, },
clickEvent: quoteMessage, clickEvent: quoteMessage
}, },
{ {
key: 'translate', key: 'translate',
@ -152,7 +130,7 @@ const actionItems = ref([
if (!featureConfig.TranslateMessage || !message.value) return false; if (!featureConfig.TranslateMessage || !message.value) return false;
return message.value.status === 'success' && message.value.type === TYPES.MSG_TEXT; return message.value.status === 'success' && message.value.type === TYPES.MSG_TEXT;
}, },
clickEvent: translateMessage, clickEvent: translateMessage
}, },
{ {
key: 'convert', key: 'convert',
@ -163,7 +141,7 @@ const actionItems = ref([
if (!featureConfig.VoiceToText || !message.value) return false; if (!featureConfig.VoiceToText || !message.value) return false;
return message.value.status === 'success' && message.value.type === TYPES.MSG_AUDIO; return message.value.status === 'success' && message.value.type === TYPES.MSG_AUDIO;
}, },
clickEvent: convertVoiceToText, clickEvent: convertVoiceToText
}, },
{ {
key: 'multi-select', key: 'multi-select',
@ -173,8 +151,8 @@ const actionItems = ref([
if (!featureConfig.MultiSelection || !message.value) return false; if (!featureConfig.MultiSelection || !message.value) return false;
return message.value.status === 'success'; return message.value.status === 'success';
}, },
clickEvent: multipleSelectMessage, clickEvent: multipleSelectMessage
}, }
]); ]);
const message = ref<IMessageModel>(); const message = ref<IMessageModel>();
@ -183,14 +161,14 @@ const messageToolDom = ref<HTMLElement>();
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
translateTextInfo: onMessageTranslationInfoUpdated, translateTextInfo: onMessageTranslationInfoUpdated,
voiceToTextInfo: onMessageConvertInfoUpdated, voiceToTextInfo: onMessageConvertInfoUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
translateTextInfo: onMessageTranslationInfoUpdated, translateTextInfo: onMessageTranslationInfoUpdated,
voiceToTextInfo: onMessageConvertInfoUpdated, voiceToTextInfo: onMessageConvertInfoUpdated
}); });
}); });
@ -242,7 +220,7 @@ function revokeMessage() {
const message = TUITranslateService.t('TUIChat.已过撤回时限'); const message = TUITranslateService.t('TUIChat.已过撤回时限');
Toast({ Toast({
message, message,
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
} }
}); });
@ -257,7 +235,7 @@ function deleteMessage() {
async function copyMessage() { async function copyMessage() {
if (isUniFrameWork) { if (isUniFrameWork) {
TUIGlobal?.setClipboardData({ TUIGlobal?.setClipboardData({
data: transformTextWithKeysToEmojiNames(message.value?.payload?.text), data: transformTextWithKeysToEmojiNames(message.value?.payload?.text)
}); });
} else { } else {
// uni-app conditional compilation will not run the following code // uni-app conditional compilation will not run the following code
@ -295,17 +273,17 @@ function translateMessage() {
if (!enable) { if (!enable) {
Toast({ Toast({
message: TUITranslateService.t('TUIChat.请开通翻译功能'), message: TUITranslateService.t('TUIChat.请开通翻译功能'),
type: TOAST_TYPE.WARNING, type: TOAST_TYPE.WARNING
}); });
return; return;
} }
if (!message.value) return; if (!message.value) return;
const index = actionItems.value.findIndex(item => item.key === 'translate'); const index = actionItems.value.findIndex((item) => item.key === 'translate');
TUIStore.update(StoreName.CHAT, 'translateTextInfo', { TUIStore.update(StoreName.CHAT, 'translateTextInfo', {
conversationID: message.value.conversationID, conversationID: message.value.conversationID,
messageID: message.value.ID, messageID: message.value.ID,
visible: !actionItems.value[index].visible, visible: !actionItems.value[index].visible
}); });
} }
@ -313,17 +291,17 @@ function convertVoiceToText() {
const enable = TUIStore.getData(StoreName.APP, 'enabledVoiceToText'); const enable = TUIStore.getData(StoreName.APP, 'enabledVoiceToText');
if (!enable) { if (!enable) {
Toast({ Toast({
message: TUITranslateService.t('TUIChat.请开通语音转文字功能'), message: TUITranslateService.t('TUIChat.请开通语音转文字功能')
}); });
return; return;
} }
if (!message.value) return; if (!message.value) return;
const index = actionItems.value.findIndex(item => item.key === 'convert'); const index = actionItems.value.findIndex((item) => item.key === 'convert');
TUIStore.update(StoreName.CHAT, 'voiceToTextInfo', { TUIStore.update(StoreName.CHAT, 'voiceToTextInfo', {
conversationID: message.value.conversationID, conversationID: message.value.conversationID,
messageID: message.value.ID, messageID: message.value.ID,
visible: !actionItems.value[index].visible, visible: !actionItems.value[index].visible
}); });
} }
@ -334,7 +312,7 @@ function multipleSelectMessage() {
function onMessageTranslationInfoUpdated(info: Map<string, ITranslateInfo[]>) { function onMessageTranslationInfoUpdated(info: Map<string, ITranslateInfo[]>) {
if (info === undefined) return; if (info === undefined) return;
const translationInfoList = info.get(props.messageItem.conversationID) || []; const translationInfoList = info.get(props.messageItem.conversationID) || [];
const idx = actionItems.value.findIndex(item => item.key === 'translate'); const idx = actionItems.value.findIndex((item) => item.key === 'translate');
for (let i = 0; i < translationInfoList.length; ++i) { for (let i = 0; i < translationInfoList.length; ++i) {
const { messageID, visible } = translationInfoList[i]; const { messageID, visible } = translationInfoList[i];
if (messageID === props.messageItem.ID) { if (messageID === props.messageItem.ID) {
@ -349,7 +327,7 @@ function onMessageTranslationInfoUpdated(info: Map<string, ITranslateInfo[]>) {
function onMessageConvertInfoUpdated(info: Map<string, IConvertInfo[]>) { function onMessageConvertInfoUpdated(info: Map<string, IConvertInfo[]>) {
if (info === undefined) return; if (info === undefined) return;
const convertInfoList = info.get(props.messageItem.conversationID) || []; const convertInfoList = info.get(props.messageItem.conversationID) || [];
const idx = actionItems.value.findIndex(item => item.key === 'convert'); const idx = actionItems.value.findIndex((item) => item.key === 'convert');
for (let i = 0; i < convertInfoList.length; ++i) { for (let i = 0; i < convertInfoList.length; ++i) {
const { messageID, visible } = convertInfoList[i]; const { messageID, visible } = convertInfoList[i];
if (messageID === props.messageItem.ID) { if (messageID === props.messageItem.ID) {
@ -362,12 +340,12 @@ function onMessageConvertInfoUpdated(info: Map<string, IConvertInfo[]>) {
} }
defineExpose({ defineExpose({
messageToolDom, messageToolDom
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.dialog-item-web { .dialog-item-web {
background: #fff; background: #fff;

View File

@ -1,14 +1,10 @@
<template> <template>
<div class="revoke"> <div class="revoke">
<span v-if="message.flow === 'in'">{{ message.nick || message.from }}</span> <span v-if="message.flow === 'in'">{{ message.nick || message.from }}</span>
<span v-else-if="message.from === message.revoker">{{ TUITranslateService.t("TUIChat.您") }}</span> <span v-else-if="message.from === message.revoker">{{ TUITranslateService.t('TUIChat.您') }}</span>
<span v-else>{{ message.revoker }}</span> <span v-else>{{ message.revoker }}</span>
<span>{{ TUITranslateService.t("TUIChat.撤回了一条消息") }}</span> <span>{{ TUITranslateService.t('TUIChat.撤回了一条消息') }}</span>
<span <span v-if="message.flow === 'out' && isEditMsg" class="edit" @click="messageEdit">{{ TUITranslateService.t('TUIChat.重新编辑') }}</span>
v-if="message.flow === 'out' && isEditMsg"
class="edit"
@click="messageEdit"
>{{ TUITranslateService.t("TUIChat.重新编辑") }}</span>
</div> </div>
</template> </template>
@ -18,12 +14,12 @@ import { TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-eng
const props = defineProps({ const props = defineProps({
isEdit: { isEdit: {
type: Boolean, type: Boolean,
default: () => false, default: () => false
}, },
messageItem: { messageItem: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, }
}); });
const message = ref<IMessageModel>(); const message = ref<IMessageModel>();
@ -39,7 +35,7 @@ const messageEdit = () => {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../../assets/styles/common"; @import '../../../../assets/styles/common';
.revoke { .revoke {
display: flex; display: flex;

View File

@ -1,27 +1,19 @@
<template> <template>
<Overlay <Overlay :maskColor="'transparent'" @onOverlayClick="closeReadReceiptPanel">
:maskColor="'transparent'"
@onOverlayClick="closeReadReceiptPanel"
>
<div <div
:class="{ :class="{
'read-receipt-panel': true, 'read-receipt-panel': true,
'read-receipt-panel-mobile': isMobile, 'read-receipt-panel-mobile': isMobile,
'read-receipt-panel-uni': isUniFrameWork, 'read-receipt-panel-uni': isUniFrameWork,
'read-receipt-panel-close-mobile': isMobile && isPanelClose, 'read-receipt-panel-close-mobile': isMobile && isPanelClose
}" }"
> >
<div class="header"> <div class="header">
<div class="header-text"> <div class="header-text">
{{ TUITranslateService.t("TUIChat.消息详情") }} {{ TUITranslateService.t('TUIChat.消息详情') }}
</div> </div>
<div class="header-close-icon"> <div class="header-close-icon">
<Icon <Icon size="12px" hotAreaSize="8" :file="closeIcon" @onClick="closeReadReceiptPanel" />
size="12px"
hotAreaSize="8"
:file="closeIcon"
@onClick="closeReadReceiptPanel"
/>
</div> </div>
</div> </div>
<div class="read-status-counter-container"> <div class="read-status-counter-container">
@ -30,7 +22,7 @@
:key="tabName" :key="tabName"
:class="{ :class="{
'read-status-counter': true, 'read-status-counter': true,
'active': tabName === currentTabName, 'active': tabName === currentTabName
}" }"
@click="toggleTabName(tabName)" @click="toggleTabName(tabName)"
> >
@ -38,60 +30,34 @@
{{ tabInfo[tabName].tabName }} {{ tabInfo[tabName].tabName }}
</div> </div>
<div class="status-count"> <div class="status-count">
{{ tabInfo[tabName].count === undefined ? "" : tabInfo[tabName].count }} {{ tabInfo[tabName].count === undefined ? '' : tabInfo[tabName].count }}
</div> </div>
</div> </div>
</div> </div>
<div class="read-status-member-list"> <div class="read-status-member-list">
<div <div v-if="tabInfo[currentTabName].count === 0 && isFirstLoadFinished" class="empty-list-tip">
v-if="tabInfo[currentTabName].count === 0 && isFirstLoadFinished"
class="empty-list-tip"
>
- {{ TUITranslateService.t('TUIChat.空') }} - - {{ TUITranslateService.t('TUIChat.空') }} -
</div> </div>
<template v-else-if="isFirstLoadFinished"> <template v-else-if="isFirstLoadFinished">
<template v-if="currentTabName === 'unread'"> <template v-if="currentTabName === 'unread'">
<div <div v-for="item in tabInfo[currentTabName].memberList" :key="item.userID" class="read-status-member-container">
v-for="item in tabInfo[currentTabName].memberList" <Avatar class="read-status-avatar" useSkeletonAnimation :url="item.avatar || ''" />
:key="item.userID"
class="read-status-member-container"
>
<Avatar
class="read-status-avatar"
useSkeletonAnimation
:url="item.avatar || ''"
/>
<div class="username"> <div class="username">
{{ item.nick || item.userID }} {{ item.nick || item.userID }}
</div> </div>
</div> </div>
</template> </template>
<template v-if="currentTabName === 'read'"> <template v-if="currentTabName === 'read'">
<div <div v-for="item in tabInfo[currentTabName].memberList" :key="item.userID" class="read-status-member-container">
v-for="item in tabInfo[currentTabName].memberList" <Avatar class="read-status-avatar" useSkeletonAnimation :url="item.avatar" />
:key="item.userID"
class="read-status-member-container"
>
<Avatar
class="read-status-avatar"
useSkeletonAnimation
:url="item.avatar"
/>
<div class="username"> <div class="username">
{{ item.nick || item.userID }} {{ item.nick || item.userID }}
</div> </div>
</div> </div>
</template> </template>
</template> </template>
<div <div v-if="isFirstLoadFinished" class="fetch-more-container">
v-if="isFirstLoadFinished" <FetchMore :isFetching="isPullDownFetching" :isTerminateObserve="isStopFetchMore" @onExposed="pullDownFetchMoreData" />
class="fetch-more-container"
>
<FetchMore
:isFetching="isPullDownFetching"
:isTerminateObserve="isStopFetchMore"
@onExposed="pullDownFetchMoreData"
/>
</div> </div>
</div> </div>
</div> </div>
@ -122,7 +88,7 @@ interface IEmits {
const emits = defineEmits<IEmits>(); const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), { const props = withDefaults(defineProps<IProps>(), {
message: () => ({}) as IMessageModel, message: () => ({}) as IMessageModel
}); });
let lastUnreadCursor: string = ''; let lastUnreadCursor: string = '';
@ -131,7 +97,7 @@ const tabNameList: TabName[] = ['unread', 'read'];
const isListFetchCompleted: Record<TabName, boolean> = { const isListFetchCompleted: Record<TabName, boolean> = {
unread: false, unread: false,
read: false, read: false,
close: false, close: false
}; };
const isPullDownFetching = ref<boolean>(false); const isPullDownFetching = ref<boolean>(false);
@ -152,7 +118,7 @@ watch(
() => props.message.readReceiptInfo.readCount, () => props.message.readReceiptInfo.readCount,
() => { () => {
initAndRefetchReceiptInfomation(); initAndRefetchReceiptInfomation();
}, }
); );
async function fetchGroupMessageRecriptMemberListByType(readType: ReadType = 'all') { async function fetchGroupMessageRecriptMemberListByType(readType: ReadType = 'all') {
@ -166,7 +132,7 @@ async function fetchGroupMessageRecriptMemberListByType(readType: ReadType = 'al
message, message,
filter: 1, filter: 1,
cursor: lastUnreadCursor, cursor: lastUnreadCursor,
count: 100, count: 100
}); });
if (unreadResult) { if (unreadResult) {
lastUnreadCursor = unreadResult.data.cursor; lastUnreadCursor = unreadResult.data.cursor;
@ -181,7 +147,7 @@ async function fetchGroupMessageRecriptMemberListByType(readType: ReadType = 'al
message, message,
filter: 0, filter: 0,
cursor: lastReadCursor, cursor: lastReadCursor,
count: 100, count: 100
}); });
if (readResult) { if (readResult) {
lastReadCursor = readResult.data.cursor; lastReadCursor = readResult.data.cursor;
@ -197,12 +163,12 @@ async function fetchGroupMessageRecriptMemberListByType(readType: ReadType = 'al
return { return {
unreadResult: { unreadResult: {
count: totalUnreadCount, count: totalUnreadCount,
...unreadResult.data, ...unreadResult.data
}, },
readResult: { readResult: {
count: totalReadCount, count: totalReadCount,
...readResult.data, ...readResult.data
}, }
}; };
} }
@ -211,7 +177,7 @@ async function pullDownFetchMoreData() {
* Use isPullDownFetching to control the state of the FetchMore component * Use isPullDownFetching to control the state of the FetchMore component
* Also, implement locking for intersectionObserver under uniapp * Also, implement locking for intersectionObserver under uniapp
* Because there is no isIntersecting in uniapp, it is impossible to determine whether the observed element has entered or exited the observation area * Because there is no isIntersecting in uniapp, it is impossible to determine whether the observed element has entered or exited the observation area
*/ */
if (isListFetchCompleted[currentTabName.value] || isPullDownFetching.value) { if (isListFetchCompleted[currentTabName.value] || isPullDownFetching.value) {
return; return;
} }
@ -281,18 +247,18 @@ function generateInitalTabInfo(): ITabInfo {
read: { read: {
tabName: TUITranslateService.t('TUIChat.已读'), tabName: TUITranslateService.t('TUIChat.已读'),
count: undefined, count: undefined,
memberList: [], memberList: []
}, },
unread: { unread: {
tabName: TUITranslateService.t('TUIChat.未读'), tabName: TUITranslateService.t('TUIChat.未读'),
count: undefined, count: undefined,
memberList: [], memberList: []
}, },
close: { close: {
tabName: TUITranslateService.t('TUIChat.关闭'), tabName: TUITranslateService.t('TUIChat.关闭'),
count: undefined, count: undefined,
memberList: [], memberList: []
}, }
}; };
} }

View File

@ -1,14 +1,6 @@
<template> <template>
<div <div v-if="isScrollButtonVisible" class="scroll-button" @click="scrollToMessageListBottom">
v-if="isScrollButtonVisible" <Icon width="10px" height="10px" :file="doubleArrowIcon" />
class="scroll-button"
@click="scrollToMessageListBottom"
>
<Icon
width="10px"
height="10px"
:file="doubleArrowIcon"
/>
<div class="scroll-button-text"> <div class="scroll-button-text">
{{ scrollButtonContent }} {{ scrollButtonContent }}
</div> </div>
@ -17,13 +9,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed, watch } from '../../../../adapter-vue'; import { ref, onMounted, onUnmounted, computed, watch } from '../../../../adapter-vue';
import { import { TUIStore, StoreName, IMessageModel, IConversationModel, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
IMessageModel,
IConversationModel,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../common/Icon.vue'; import Icon from '../../../common/Icon.vue';
import doubleArrowIcon from '../../../../assets/icon/double-arrow.svg'; import doubleArrowIcon from '../../../../assets/icon/double-arrow.svg';
import { getBoundingClientRect } from '@tencentcloud/universal-api'; import { getBoundingClientRect } from '@tencentcloud/universal-api';
@ -42,36 +28,37 @@ const isScrollOverOneScreen = ref<boolean>(false);
const isExistLastMessage = ref<boolean>(false); const isExistLastMessage = ref<boolean>(false);
const isScrollButtonVisible = ref<boolean>(false); const isScrollButtonVisible = ref<boolean>(false);
const scrollButtonContent = computed(() => const scrollButtonContent = computed(() =>
newMessageCount.value ? `${newMessageCount.value}${TUITranslateService.t('TUIChat.条新消息')}` : TUITranslateService.t('TUIChat.回到最新位置'), newMessageCount.value ? `${newMessageCount.value}${TUITranslateService.t('TUIChat.条新消息')}` : TUITranslateService.t('TUIChat.回到最新位置')
); );
watch(() => [isScrollOverOneScreen.value, isExistLastMessage.value], watch(
() => [isScrollOverOneScreen.value, isExistLastMessage.value],
() => { () => {
isScrollButtonVisible.value = isScrollOverOneScreen.value || isExistLastMessage.value; isScrollButtonVisible.value = isScrollOverOneScreen.value || isExistLastMessage.value;
if (!isScrollButtonVisible.value) { if (!isScrollButtonVisible.value) {
resetNewMessageCount(); resetNewMessageCount();
} }
}, },
{ immediate: true }, { immediate: true }
); );
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CHAT, { TUIStore.watch(StoreName.CHAT, {
messageList: onMessageListUpdated, messageList: onMessageListUpdated,
newMessageList: onNewMessageListUpdated, newMessageList: onNewMessageListUpdated
}); });
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated, currentConversation: onCurrentConversationUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CHAT, { TUIStore.unwatch(StoreName.CHAT, {
messageList: onMessageListUpdated, messageList: onMessageListUpdated,
newMessageList: onNewMessageListUpdated, newMessageList: onNewMessageListUpdated
}); });
TUIStore.unwatch(StoreName.CONV, { TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated, currentConversation: onCurrentConversationUpdated
}); });
}); });
@ -82,15 +69,19 @@ function isTypingMessage(message: IMessageModel): boolean {
function onMessageListUpdated(newMessageList: IMessageModel[]) { function onMessageListUpdated(newMessageList: IMessageModel[]) {
messageList.value = newMessageList || []; messageList.value = newMessageList || [];
const lastMessage = messageList.value?.[messageList.value?.length - 1]; const lastMessage = messageList.value?.[messageList.value?.length - 1];
isExistLastMessage.value = !!( isExistLastMessage.value = !!(lastMessage && lastMessage?.time < currentLastMessageTime?.value);
lastMessage && lastMessage?.time < currentLastMessageTime?.value
);
} }
function onNewMessageListUpdated(newMessageList: IMessageModel[]) { function onNewMessageListUpdated(newMessageList: IMessageModel[]) {
if (Array.isArray(newMessageList) && isScrollButtonVisible.value) { if (Array.isArray(newMessageList) && isScrollButtonVisible.value) {
newMessageList.forEach((message: IMessageModel) => { newMessageList.forEach((message: IMessageModel) => {
if (message && message.conversationID === currentConversationID.value && !message.isDeleted && !message.isRevoked && !isTypingMessage(message)) { if (
message &&
message.conversationID === currentConversationID.value &&
!message.isDeleted &&
!message.isRevoked &&
!isTypingMessage(message)
) {
newMessageCount.value += 1; newMessageCount.value += 1;
} }
}); });
@ -109,7 +100,7 @@ function onCurrentConversationUpdated(conversation: IConversationModel | undefin
async function judgeScrollOverOneScreen(e: Event) { async function judgeScrollOverOneScreen(e: Event) {
if (e.target) { if (e.target) {
try { try {
const { height } = await getBoundingClientRect(`#${(e.target as HTMLElement)?.id}`, 'messageList') || {}; const { height } = (await getBoundingClientRect(`#${(e.target as HTMLElement)?.id}`, 'messageList')) || {};
const scrollHeight = (e.target as HTMLElement)?.scrollHeight || (e.detail as HTMLElement)?.scrollHeight; const scrollHeight = (e.target as HTMLElement)?.scrollHeight || (e.detail as HTMLElement)?.scrollHeight;
const scrollTop = (e.target as HTMLElement)?.scrollTop || (e.detail as HTMLElement)?.scrollTop || 0; const scrollTop = (e.target as HTMLElement)?.scrollTop || (e.detail as HTMLElement)?.scrollTop || 0;
// while scroll over one screen show this scroll button. // while scroll over one screen show this scroll button.
@ -144,7 +135,7 @@ function scrollToMessageListBottom() {
defineExpose({ defineExpose({
judgeScrollOverOneScreen, judgeScrollOverOneScreen,
isScrollButtonVisible, isScrollButtonVisible
}); });
</script> </script>

View File

@ -2,52 +2,35 @@
<div <div
:class="{ :class="{
'mulitple-select-panel': true, 'mulitple-select-panel': true,
'mulitple-select-panel-mobile': isMobile, 'mulitple-select-panel-mobile': isMobile
}" }"
> >
<div <div class="forward-button" @click="oneByOneForwardMessage">
class="forward-button" <Icon :file="ForwardEachIcon" :size="iconSize" />
@click="oneByOneForwardMessage"
>
<Icon
:file="ForwardEachIcon"
:size="iconSize"
/>
<span <span
:class="{ :class="{
'forward-button-text': true, 'forward-button-text': true,
'forward-button-text-mobile': isMobile, 'forward-button-text-mobile': isMobile
}" }"
>{{ TUITranslateService.t('TUIChat.逐条转发') }}</span> >{{ TUITranslateService.t('TUIChat.逐条转发') }}</span
>
</div> </div>
<div <div class="forward-button" @click="mergeForwardMessage">
class="forward-button" <Icon :file="ForwardMergeIcon" :size="iconSize" />
@click="mergeForwardMessage"
>
<Icon
:file="ForwardMergeIcon"
:size="iconSize"
/>
<span <span
:class="{ :class="{
'forward-button-text': true, 'forward-button-text': true,
'forward-button-text-mobile': isMobile, 'forward-button-text-mobile': isMobile
}" }"
>{{ TUITranslateService.t('TUIChat.合并转发') }}</span> >{{ TUITranslateService.t('TUIChat.合并转发') }}</span
>
</div> </div>
<div <div class="forward-button" @click="cancelMultipleSelect">
class="forward-button" <Icon class="cancel-button-icon" :file="AddIcon" :size="iconSize" />
@click="cancelMultipleSelect"
>
<Icon
class="cancel-button-icon"
:file="AddIcon"
:size="iconSize"
/>
<span <span
:class="{ :class="{
'forward-button-text': true, 'forward-button-text': true,
'forward-button-text-mobile': isMobile, 'forward-button-text-mobile': isMobile
}" }"
> >
{{ TUITranslateService.t('TUIChat.取消') }} {{ TUITranslateService.t('TUIChat.取消') }}
@ -58,9 +41,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from '../../../adapter-vue'; import { ref } from '../../../adapter-vue';
import { import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import Icon from '../../common/Icon.vue'; import Icon from '../../common/Icon.vue';
import ForwardEachIcon from '../../../assets/icon/forward-each.svg'; import ForwardEachIcon from '../../../assets/icon/forward-each.svg';
import ForwardMergeIcon from '../../../assets/icon/forward-merge.svg'; import ForwardMergeIcon from '../../../assets/icon/forward-merge.svg';
@ -104,7 +85,7 @@ function cancelMultipleSelect() {
flex-direction: row; flex-direction: row;
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
background-color: #EBF0F6; background-color: #ebf0f6;
&-mobile { &-mobile {
height: 64px; height: 64px;

View File

@ -8,10 +8,10 @@ export const DEFAULT_DESC: any = {
[TUIChatEngine.TYPES.MSG_VIDEO]: '[视频]', [TUIChatEngine.TYPES.MSG_VIDEO]: '[视频]',
[TUIChatEngine.TYPES.MSG_LOCATION]: '[地理位置]', [TUIChatEngine.TYPES.MSG_LOCATION]: '[地理位置]',
[TUIChatEngine.TYPES.MSG_MERGER]: '[聊天记录]', [TUIChatEngine.TYPES.MSG_MERGER]: '[聊天记录]',
[TUIChatEngine.TYPES.MSG_CUSTOM]: '[自定义消息]', [TUIChatEngine.TYPES.MSG_CUSTOM]: '[自定义消息]'
}; };
export enum PUSH_SCENE { export enum PUSH_SCENE {
CHAT = 'chat', CHAT = 'chat',
CALL = 'call', CALL = 'call'
} }

View File

@ -2,7 +2,7 @@ import { IChatOfflinePushInfo, ICallOfflinePushInfo } from './interface';
export const chatOfflinePushInfo: IChatOfflinePushInfo = { export const chatOfflinePushInfo: IChatOfflinePushInfo = {
androidInfo: {}, androidInfo: {},
apnsInfo: {}, apnsInfo: {}
}; };
export const callOfflinePushInfo: ICallOfflinePushInfo = {}; export const callOfflinePushInfo: ICallOfflinePushInfo = {};

View File

@ -1,9 +1,6 @@
import TUIChatEngine, { IConversationModel, StoreName, TUIStore, TUITranslateService } from '@tencentcloud/chat-uikit-engine'; import TUIChatEngine, { IConversationModel, StoreName, TUIStore, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
import { transformTextWithKeysToEmojiNames } from '../emoji-config'; import { transformTextWithKeysToEmojiNames } from '../emoji-config';
import { import { IChatOfflinePushInfo, IOfflinePushInfoCreateParams } from './interface';
IChatOfflinePushInfo,
IOfflinePushInfoCreateParams,
} from './interface';
import { chatOfflinePushInfo, callOfflinePushInfo } from './info'; import { chatOfflinePushInfo, callOfflinePushInfo } from './info';
import { DEFAULT_DESC, PUSH_SCENE } from './const'; import { DEFAULT_DESC, PUSH_SCENE } from './const';
@ -16,7 +13,7 @@ class OfflinePushInfoManager {
private constructor() { private constructor() {
this.offlinePushInfo = { this.offlinePushInfo = {
[PUSH_SCENE.CHAT]: chatOfflinePushInfo, [PUSH_SCENE.CHAT]: chatOfflinePushInfo,
[PUSH_SCENE.CALL]: callOfflinePushInfo, [PUSH_SCENE.CALL]: callOfflinePushInfo
}; };
} }
@ -62,13 +59,13 @@ class OfflinePushInfoManager {
nickName: userInfo?.nick, nickName: userInfo?.nick,
chatType: conversation.type === TUIChatEngine.TYPES.CONV_GROUP ? 2 : 1, chatType: conversation.type === TUIChatEngine.TYPES.CONV_GROUP ? 2 : 1,
version: 1, version: 1,
action: 1, action: 1
}; };
return { return {
title: this.genTitle(conversation, userInfo), title: this.genTitle(conversation, userInfo),
description: this.genDesc(messageType, payload), description: this.genDesc(messageType, payload),
extension: JSON.stringify({ entity }), extension: JSON.stringify({ entity }),
...this.offlinePushInfo[PUSH_SCENE.CHAT], ...this.offlinePushInfo[PUSH_SCENE.CHAT]
}; };
} }
} }

View File

@ -1,10 +1,5 @@
import TUICore, { TUIConstants } from '@tencentcloud/tui-core'; import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { import { IMessageModel, TUIStore, StoreName, TUIChatService } from '@tencentcloud/chat-uikit-engine';
IMessageModel,
TUIStore,
StoreName,
TUIChatService,
} from '@tencentcloud/chat-uikit-engine';
import TUIChatConfig from './config'; import TUIChatConfig from './config';
export default class TUIChatServer { export default class TUIChatServer {
@ -21,7 +16,7 @@ export default class TUIChatServer {
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversationID: (id: string) => { currentConversationID: (id: string) => {
this.currentConversationID = id; this.currentConversationID = id;
}, }
}); });
} }
@ -66,8 +61,8 @@ export default class TUIChatServer {
} }
/** /**
* Listen for the success notification. * Listen for the success notification.
*/ */
public onNotifyEvent(eventName: string, subKey: string, params?: Record<string, any>) { public onNotifyEvent(eventName: string, subKey: string, params?: Record<string, any>) {
if (eventName === TUIConstants.TUITheme.EVENT.THEME_CHANGED) { if (eventName === TUIConstants.TUITheme.EVENT.THEME_CHANGED) {
switch (subKey) { switch (subKey) {

View File

@ -4,7 +4,7 @@ import {
StoreName, StoreName,
TUIConversationService, TUIConversationService,
TUIStore, TUIStore,
TUITranslateService, TUITranslateService
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import { transformTextWithKeysToEmojiNames } from '../emoji-config'; import { transformTextWithKeysToEmojiNames } from '../emoji-config';
import { JSONToObject } from '../../../utils/index'; import { JSONToObject } from '../../../utils/index';
@ -13,7 +13,7 @@ class ConversationDraftManager {
private static instance: ConversationDraftManager | null = null; private static instance: ConversationDraftManager | null = null;
private quoteMessageMap = new Map<string, IMessageModel>(); private quoteMessageMap = new Map<string, IMessageModel>();
private constructor() { } private constructor() {}
public static getInstance(): ConversationDraftManager { public static getInstance(): ConversationDraftManager {
if (!ConversationDraftManager.instance) { if (!ConversationDraftManager.instance) {
@ -22,7 +22,12 @@ class ConversationDraftManager {
return ConversationDraftManager.instance; return ConversationDraftManager.instance;
} }
public setStore(conversationID: string, draftContent: string, abstract: string, quoteMessage?: { type: 'quote' | 'reply'; message: IMessageModel }) { public setStore(
conversationID: string,
draftContent: string,
abstract: string,
quoteMessage?: { type: 'quote' | 'reply'; message: IMessageModel }
) {
if (conversationID && (this.isEditorNotEmpty(draftContent) || quoteMessage?.message?.ID)) { if (conversationID && (this.isEditorNotEmpty(draftContent) || quoteMessage?.message?.ID)) {
let additionalDraftInfo = {}; let additionalDraftInfo = {};
if (quoteMessage?.message?.ID) { if (quoteMessage?.message?.ID) {
@ -34,8 +39,8 @@ class ConversationDraftManager {
draftInfo: { draftInfo: {
html: draftContent, html: draftContent,
abstract: abstract, abstract: abstract,
...additionalDraftInfo, ...additionalDraftInfo
}, }
}; };
TUIConversationService.setConversationDraft(draftParams); TUIConversationService.setConversationDraft(draftParams);
TUIStore.update(StoreName.CHAT, 'quoteMessage', { message: undefined, type: 'quote' }); TUIStore.update(StoreName.CHAT, 'quoteMessage', { message: undefined, type: 'quote' });
@ -49,7 +54,10 @@ class ConversationDraftManager {
} }
if (conversation.conversationID && conversation.draftText) { if (conversation.conversationID && conversation.draftText) {
const draftObject = JSONToObject(conversation.draftText); const draftObject = JSONToObject(conversation.draftText);
TUIStore.update(StoreName.CHAT, 'quoteMessage', { message: this.quoteMessageMap.get(draftObject.messageID) || undefined, type: draftObject.type }); TUIStore.update(StoreName.CHAT, 'quoteMessage', {
message: this.quoteMessageMap.get(draftObject.messageID) || undefined,
type: draftObject.type
});
setEditorContentCallback(draftObject.html); setEditorContentCallback(draftObject.html);
} }
TUIConversationService.setConversationDraft({ conversationID: conversation.conversationID }); TUIConversationService.setConversationDraft({ conversationID: conversation.conversationID });

View File

@ -1,8 +1,4 @@
import { import { IMessageModel, TUIChatService, TUIStore } from '@tencentcloud/chat-uikit-engine';
IMessageModel,
TUIChatService,
TUIStore,
} from '@tencentcloud/chat-uikit-engine';
import { IChatResponese } from '../../../interface'; import { IChatResponese } from '../../../interface';
class Convertor { class Convertor {
@ -35,7 +31,7 @@ class Convertor {
} }
// step3: get response from api // step3: get response from api
const response: IChatResponese<{ result: string }> = await TUIChatService.convertVoiceToText({ const response: IChatResponese<{ result: string }> = await TUIChatService.convertVoiceToText({
message: currentMessage, message: currentMessage
}); });
let { data: { result } = {} } = response; let { data: { result } = {} } = response;
if (result) { if (result) {

View File

@ -33,7 +33,7 @@ class CopyManager {
anchorNode: selection.anchorNode, anchorNode: selection.anchorNode,
anchorOffset: selection.anchorOffset, anchorOffset: selection.anchorOffset,
focusNode: selection.focusNode, focusNode: selection.focusNode,
focusOffset: selection.focusOffset, focusOffset: selection.focusOffset
}; };
} }
@ -49,9 +49,8 @@ class CopyManager {
} }
const range = document.createRange(); const range = document.createRange();
const isForwardSelection = anchorNode === focusNode const isForwardSelection =
? anchorOffset <= focusOffset anchorNode === focusNode ? anchorOffset <= focusOffset : anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_FOLLOWING;
: anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_FOLLOWING;
if (isForwardSelection) { if (isForwardSelection) {
range.setStart(anchorNode, anchorOffset); range.setStart(anchorNode, anchorOffset);
range.setEnd(focusNode, focusOffset); range.setEnd(focusNode, focusOffset);
@ -88,7 +87,7 @@ class CopyManager {
} }
public async copyTextOrHtml(content: string, type: 'text' | 'html'): Promise<void> { public async copyTextOrHtml(content: string, type: 'text' | 'html'): Promise<void> {
const mimeType = (type === 'html') ? 'text/html' : 'text/plain'; const mimeType = type === 'html' ? 'text/html' : 'text/plain';
// prefer use clipboard api to copy node or text // prefer use clipboard api to copy node or text
if (navigator.clipboard) { if (navigator.clipboard) {
try { try {
@ -97,7 +96,7 @@ class CopyManager {
await navigator.clipboard.write([clipboardItem]); await navigator.clipboard.write([clipboardItem]);
Toast({ Toast({
message: TUITranslateService.t('TUIChat.复制成功'), message: TUITranslateService.t('TUIChat.复制成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
return; return;
} catch (err) { } catch (err) {
@ -132,12 +131,12 @@ class CopyManager {
document.execCommand('copy'); document.execCommand('copy');
Toast({ Toast({
message: TUITranslateService.t('TUIChat.复制成功'), message: TUITranslateService.t('TUIChat.复制成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
} catch (err) { } catch (err) {
Toast({ Toast({
message: TUITranslateService.t('TUIChat.此机型暂不支持复制'), message: TUITranslateService.t('TUIChat.此机型暂不支持复制'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
console.warn('use document.execCommand copy failed:', err); console.warn('use document.execCommand copy failed:', err);
} finally { } finally {

View File

@ -4,7 +4,7 @@ import TUIChatEngine, {
StoreName, StoreName,
TUITranslateService, TUITranslateService,
IConversationModel, IConversationModel,
SendMessageParams, SendMessageParams
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import { Toast, TOAST_TYPE } from '../../common/Toast/index'; import { Toast, TOAST_TYPE } from '../../common/Toast/index';
import { isEnabledMessageReadReceiptGlobal } from '../utils/utils'; import { isEnabledMessageReadReceiptGlobal } from '../utils/utils';
@ -21,7 +21,7 @@ export const sendMessageErrorCodeMap: Map<number, string> = new Map([
[8001, '消息长度超出限制,消息长度不要超过12K'], [8001, '消息长度超出限制,消息长度不要超过12K'],
[80001, '消息或者资料中文本存在敏感内容,发送失败'], [80001, '消息或者资料中文本存在敏感内容,发送失败'],
[80004, '消息中图片存在敏感内容,发送失败'], [80004, '消息中图片存在敏感内容,发送失败'],
[10017, '您已被禁止聊天'], [10017, '您已被禁止聊天']
]); ]);
export const createOfflinePushInfo = (conversation: IConversationModel) => { export const createOfflinePushInfo = (conversation: IConversationModel) => {
@ -33,12 +33,12 @@ export const createOfflinePushInfo = (conversation: IConversationModel) => {
nickName: userInfo.nick, nickName: userInfo.nick,
chatType: conversation.type === TUIChatEngine.TYPES.CONV_GROUP ? 2 : 1, chatType: conversation.type === TUIChatEngine.TYPES.CONV_GROUP ? 2 : 1,
version: 1, version: 1,
action: 1, action: 1
}; };
return { return {
extension: JSON.stringify({ entity }), extension: JSON.stringify({ entity }),
androidInfo, androidInfo,
apnsInfo, apnsInfo
}; };
}; };
@ -47,10 +47,7 @@ export const createOfflinePushInfo = (conversation: IConversationModel) => {
* @param messageList * @param messageList
* @param currentConversation * @param currentConversation
*/ */
export const sendMessages = async ( export const sendMessages = async (messageList: ITipTapEditorContent[], currentConversation: IConversationModel) => {
messageList: ITipTapEditorContent[],
currentConversation: IConversationModel,
) => {
// In case of messageJumping, the sent message is automatically cleared and returns to the bottom // In case of messageJumping, the sent message is automatically cleared and returns to the bottom
if (TUIStore.getData(StoreName.CHAT, 'messageSource')) { if (TUIStore.getData(StoreName.CHAT, 'messageSource')) {
TUIStore.update(StoreName.CHAT, 'messageSource', undefined); TUIStore.update(StoreName.CHAT, 'messageSource', undefined);
@ -61,17 +58,17 @@ export const sendMessages = async (
to: currentConversation?.groupProfile?.groupID || currentConversation?.userProfile?.userID, to: currentConversation?.groupProfile?.groupID || currentConversation?.userProfile?.userID,
conversationType: currentConversation?.type as any, conversationType: currentConversation?.type as any,
payload: {}, payload: {},
needReadReceipt: isEnabledMessageReadReceiptGlobal(), needReadReceipt: isEnabledMessageReadReceiptGlobal()
}; };
// handle message typing // handle message typing
let textMessageContent; let textMessageContent;
const sendMessageOptions = { const sendMessageOptions = {
offlinePushInfo: {}, offlinePushInfo: {}
}; };
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = { const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation, conversation: currentConversation,
payload: content.payload, payload: content.payload,
messageType: '', messageType: ''
}; };
switch (content?.type) { switch (content?.type) {
case 'text': case 'text':
@ -81,7 +78,7 @@ export const sendMessages = async (
break; break;
} }
options.payload = { options.payload = {
text: textMessageContent, text: textMessageContent
}; };
offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_TEXT; offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_TEXT;
sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams); sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams);
@ -94,7 +91,7 @@ export const sendMessages = async (
break; break;
case 'image': case 'image':
options.payload = { options.payload = {
file: content.payload?.file, file: content.payload?.file
}; };
offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_IMAGE; offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_IMAGE;
sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams); sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams);
@ -102,7 +99,7 @@ export const sendMessages = async (
break; break;
case 'video': case 'video':
options.payload = { options.payload = {
file: content.payload?.file, file: content.payload?.file
}; };
offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_VIDEO; offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_VIDEO;
sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams); sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams);
@ -110,7 +107,7 @@ export const sendMessages = async (
break; break;
case 'file': case 'file':
options.payload = { options.payload = {
file: content.payload?.file, file: content.payload?.file
}; };
offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_FILE; offlinePushInfoCreateParams.messageType = TUIChatEngine.TYPES.MSG_FILE;
sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams); sendMessageOptions.offlinePushInfo = OfflinePushInfoManager.create(offlinePushInfoCreateParams);
@ -125,7 +122,7 @@ export const sendMessages = async (
message: sendMessageErrorCodeMap.get(error?.code) message: sendMessageErrorCodeMap.get(error?.code)
? TUITranslateService.t(`TUIChat.${sendMessageErrorCodeMap.get(error.code) as string}`) ? TUITranslateService.t(`TUIChat.${sendMessageErrorCodeMap.get(error.code) as string}`)
: error?.message, : error?.message,
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
// If the message fails to be sent and the message is a reference message, clear the reference message information // If the message fails to be sent and the message is a reference message, clear the reference message information
if (TUIStore.getData(StoreName.CHAT, 'quoteMessage')) { if (TUIStore.getData(StoreName.CHAT, 'quoteMessage')) {
@ -141,7 +138,7 @@ export const handleMessageWithTyping = (cloudCustomData: any) => {
} }
cloudCustomData.messageFeature = { cloudCustomData.messageFeature = {
needTyping: 1, needTyping: 1,
version: 1, version: 1
}; };
return cloudCustomData; return cloudCustomData;
}; };

View File

@ -1,10 +1,4 @@
import TUIChatEngine, { import TUIChatEngine, { IMessageModel, TUIChatService, TUIStore, TUITranslateService, TUIUserService } from '@tencentcloud/chat-uikit-engine';
IMessageModel,
TUIChatService,
TUIStore,
TUITranslateService,
TUIUserService,
} from '@tencentcloud/chat-uikit-engine';
import { IChatResponese, IUserProfile } from '../../../interface'; import { IChatResponese, IUserProfile } from '../../../interface';
/** /**
@ -80,20 +74,22 @@ class Translator {
// step4: filter plain text to be translated // step4: filter plain text to be translated
const needTranslateTextIndex: number[] = []; const needTranslateTextIndex: number[] = [];
const needTranslateText = textList.filter((item, index) => { const needTranslateText = textList
if (item.type === 'text' && item.value.trim() !== '') { .filter((item, index) => {
needTranslateTextIndex.push(index); if (item.type === 'text' && item.value.trim() !== '') {
return true; needTranslateTextIndex.push(index);
} return true;
return false; }
}).map(item => item.value); return false;
})
.map((item) => item.value);
if (needTranslateText.length === 0) { if (needTranslateText.length === 0) {
this.translationCache.set(currentMessage.ID, textList); this.translationCache.set(currentMessage.ID, textList);
return textList; return textList;
} }
// step5: get final translation result // step5: get final translation result
const translationResult = await this.getTranslationStandard(needTranslateText) as string[]; const translationResult = (await this.getTranslationStandard(needTranslateText)) as string[];
translationResult.forEach((item, index) => { translationResult.forEach((item, index) => {
textList[needTranslateTextIndex[index]].value = item; textList[needTranslateTextIndex[index]].value = item;
}); });
@ -122,11 +118,11 @@ class Translator {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
TUIChatService.translateText({ TUIChatService.translateText({
sourceTextList: originTextList, sourceTextList: originTextList,
sourceLanguage: 'auto', sourceLanguage: 'auto'
}) })
.then((response: IChatResponese<{ translatedTextList: string[] }>) => { .then((response: IChatResponese<{ translatedTextList: string[] }>) => {
const { const {
data: { translatedTextList }, data: { translatedTextList }
} = response; } = response;
resolve(translatedTextList); resolve(translatedTextList);
}) })
@ -149,7 +145,7 @@ class Translator {
splittingList.push(`@${TUITranslateService.t('TUIChat.所有人')}`); splittingList.push(`@${TUITranslateService.t('TUIChat.所有人')}`);
} }
if (atUserList.length > 0) { if (atUserList.length > 0) {
const { data: userProfileList } = await TUIUserService.getUserProfile({ userIDList: atUserList }) as IChatResponese<IUserProfile[]>; const { data: userProfileList } = (await TUIUserService.getUserProfile({ userIDList: atUserList })) as IChatResponese<IUserProfile[]>;
userProfileList.forEach((user) => { userProfileList.forEach((user) => {
const atNick = `@${user.nick || user.userID}`; const atNick = `@${user.nick || user.userID}`;
splittingList.push(atNick); splittingList.push(atNick);
@ -195,7 +191,7 @@ class Translator {
} }
return { return {
transSplitingList, transSplitingList,
atNickList, atNickList
}; };
} }
} }

View File

@ -27,21 +27,13 @@ export function deepCopy(data: any, hash = new WeakMap()) {
return newData; return newData;
} }
export const handleSkeletonSize = ( export const handleSkeletonSize = (width: number, height: number, maxWidth: number, maxHeight: number): { width: number; height: number } => {
width: number,
height: number,
maxWidth: number,
maxHeight: number,
): { width: number; height: number } => {
const widthToHeight = width / height; const widthToHeight = width / height;
const maxWidthToHeight = maxWidth / maxHeight; const maxWidthToHeight = maxWidth / maxHeight;
if (width <= maxWidth && height <= maxHeight) { if (width <= maxWidth && height <= maxHeight) {
return { width, height }; return { width, height };
} }
if ( if ((width <= maxWidth && height > maxHeight) || (width > maxWidth && height > maxHeight && widthToHeight <= maxWidthToHeight)) {
(width <= maxWidth && height > maxHeight)
|| (width > maxWidth && height > maxHeight && widthToHeight <= maxWidthToHeight)
) {
return { width: width * (maxHeight / height), height: maxHeight }; return { width: width * (maxHeight / height), height: maxHeight };
} }
return { width: maxWidth, height: height * (maxWidth / width) }; return { width: maxWidth, height: height * (maxWidth / width) };
@ -76,10 +68,7 @@ export function getImgLoad(container: any, className: string, callback: any) {
} }
export const isCreateGroupCustomMessage = (message: IMessageModel) => { export const isCreateGroupCustomMessage = (message: IMessageModel) => {
return ( return message.type === TUIChatEngine.TYPES.MSG_CUSTOM && message?.getMessageContent()?.businessID === 'group_create';
message.type === TUIChatEngine.TYPES.MSG_CUSTOM
&& message?.getMessageContent()?.businessID === 'group_create'
);
}; };
/** /**
@ -91,8 +80,7 @@ export const isCreateGroupCustomMessage = (message: IMessageModel) => {
* @return {boolean} - Returns a boolean value indicating if the message read receipt is enabled globally. * @return {boolean} - Returns a boolean value indicating if the message read receipt is enabled globally.
*/ */
export function isEnabledMessageReadReceiptGlobal(): boolean { export function isEnabledMessageReadReceiptGlobal(): boolean {
return TUIStore.getData(StoreName.USER, 'displayMessageReadReceipt') return TUIStore.getData(StoreName.USER, 'displayMessageReadReceipt') && TUIStore.getData(StoreName.APP, 'enabledMessageReadReceipt');
&& TUIStore.getData(StoreName.APP, 'enabledMessageReadReceipt');
} }
export function shallowCopyMessage(message: IMessageModel) { export function shallowCopyMessage(message: IMessageModel) {
@ -102,15 +90,7 @@ export function shallowCopyMessage(message: IMessageModel) {
// calculate timestamp // calculate timestamp
export function calculateTimestamp(timestamp: number): string { export function calculateTimestamp(timestamp: number): string {
const todayZero = new Date().setHours(0, 0, 0, 0); const todayZero = new Date().setHours(0, 0, 0, 0);
const thisYear = new Date( const thisYear = new Date(new Date().getFullYear(), 0, 1, 0, 0, 0, 0).getTime();
new Date().getFullYear(),
0,
1,
0,
0,
0,
0,
).getTime();
const target = new Date(timestamp); const target = new Date(timestamp);
const oneDay = 24 * 60 * 60 * 1000; const oneDay = 24 * 60 * 60 * 1000;
@ -127,35 +107,17 @@ export function calculateTimestamp(timestamp: number): string {
return `${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`; return `${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
} else if (diff <= oneDay) { } else if (diff <= oneDay) {
// yesterday, display yesterday:hour:minute // yesterday, display yesterday:hour:minute
return `${TUITranslateService.t('time.昨天')} ${formatNum( return `${TUITranslateService.t('time.昨天')} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
target.getHours(),
)}:${formatNum(target.getMinutes())}`;
} else if (diff <= oneWeek - oneDay) { } else if (diff <= oneWeek - oneDay) {
// Within a week, display weekday hour:minute // Within a week, display weekday hour:minute
const weekdays = [ const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
'星期日',
'星期一',
'星期二',
'星期三',
'星期四',
'星期五',
'星期六',
];
const weekday = weekdays[target.getDay()]; const weekday = weekdays[target.getDay()];
return `${TUITranslateService.t('time.' + weekday)} ${formatNum( return `${TUITranslateService.t('time.' + weekday)} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
target.getHours(),
)}:${formatNum(target.getMinutes())}`;
} else if (target.getTime() >= thisYear) { } else if (target.getTime() >= thisYear) {
// Over a week, within this year, display mouth/day hour:minute // Over a week, within this year, display mouth/day hour:minute
return `${target.getMonth() + 1}/${target.getDate()} ${formatNum( return `${target.getMonth() + 1}/${target.getDate()} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
target.getHours(),
)}:${formatNum(target.getMinutes())}`;
} else { } else {
// Not within this year, display year/mouth/day hour:minute // Not within this year, display year/mouth/day hour:minute
return `${target.getFullYear()}/${ return `${target.getFullYear()}/${target.getMonth() + 1}/${target.getDate()} ${formatNum(target.getHours())}:${formatNum(target.getMinutes())}`;
target.getMonth() + 1
}/${target.getDate()} ${formatNum(target.getHours())}:${formatNum(
target.getMinutes(),
)}`;
} }
} }

View File

@ -1,29 +1,29 @@
export const wordsList = [ export const wordsList = [
{ {
value: '在吗?在吗?在吗?重要的话说三遍。', value: '在吗?在吗?在吗?重要的话说三遍。'
}, },
{ {
value: '好久没聊天了,快来和我说说话~', value: '好久没聊天了,快来和我说说话~'
}, },
{ {
value: '好的,就这么说定了。', value: '好的,就这么说定了。'
}, },
{ {
value: '感恩的心,感谢有你。', value: '感恩的心,感谢有你。'
}, },
{ {
value: '糟糕!是心动的感觉!', value: '糟糕!是心动的感觉!'
}, },
{ {
value: '心疼地抱抱自己,我太难了!', value: '心疼地抱抱自己,我太难了!'
}, },
{ {
value: '没关系,别在意,事情过去就过去了。', value: '没关系,别在意,事情过去就过去了。'
}, },
{ {
value: '早上好,今天也是让人期待的一天呢!', value: '早上好,今天也是让人期待的一天呢!'
}, },
{ {
value: '熬夜有什么用,又没人陪你聊天,早点休息吧。', value: '熬夜有什么用,又没人陪你聊天,早点休息吧。'
}, }
]; ];

View File

@ -1,9 +1,5 @@
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine'; import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
import { import { CONTACT_INFO_LABEL_POSITION, CONTACT_INFO_MORE_EDIT_TYPE, CONTACT_INFO_BUTTON_TYPE } from '../../../constant';
CONTACT_INFO_LABEL_POSITION,
CONTACT_INFO_MORE_EDIT_TYPE,
CONTACT_INFO_BUTTON_TYPE,
} from '../../../constant';
import { import {
updateFriendRemark, updateFriendRemark,
deleteFriend, deleteFriend,
@ -15,7 +11,7 @@ import {
acceptFriendApplication, acceptFriendApplication,
refuseFriendApplication, refuseFriendApplication,
addToBlacklist, addToBlacklist,
removeFromBlacklist, removeFromBlacklist
} from '../utils/index'; } from '../utils/index';
export const contactMoreInfoConfig = { export const contactMoreInfoConfig = {
@ -28,11 +24,7 @@ export const contactMoreInfoConfig = {
editable: true, editable: true,
editType: CONTACT_INFO_MORE_EDIT_TYPE.INPUT, editType: CONTACT_INFO_MORE_EDIT_TYPE.INPUT,
editing: false, editing: false,
editSubmitHandler: (props: { editSubmitHandler: (props: { item: any; contactInfoData: any; [propsName: string]: any }) => {
item: any;
contactInfoData: any;
[propsName: string]: any;
}) => {
if (props?.isBothFriend) { if (props?.isBothFriend) {
const newRemarkValue = props?.item?.data; const newRemarkValue = props?.item?.data;
updateFriendRemark(props?.contactInfoData?.userID, newRemarkValue); updateFriendRemark(props?.contactInfoData?.userID, newRemarkValue);
@ -41,7 +33,7 @@ export const contactMoreInfoConfig = {
} else { } else {
props?.item?.editing && (props.item.editing = false); props?.item?.editing && (props.item.editing = false);
} }
}, }
}, },
// blocked list // blocked list
blackList: { blackList: {
@ -52,18 +44,14 @@ export const contactMoreInfoConfig = {
editable: true, editable: true,
editType: CONTACT_INFO_MORE_EDIT_TYPE.SWITCH, editType: CONTACT_INFO_MORE_EDIT_TYPE.SWITCH,
editing: true, editing: true,
editSubmitHandler: (props: { editSubmitHandler: (props: { item: any; contactInfoData: any; [propsName: string]: any }) => {
item: any;
contactInfoData: any;
[propsName: string]: any;
}) => {
if (props?.isInBlackList) { if (props?.isInBlackList) {
removeFromBlacklist(props?.contactInfoData?.userID); removeFromBlacklist(props?.contactInfoData?.userID);
} else { } else {
addToBlacklist(props?.contactInfoData?.userID); addToBlacklist(props?.contactInfoData?.userID);
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', 'blackList'); TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', 'blackList');
} }
}, }
}, },
// Fill in verification words (applicant) // Fill in verification words (applicant)
setWords: { setWords: {
@ -73,7 +61,7 @@ export const contactMoreInfoConfig = {
labelPosition: CONTACT_INFO_LABEL_POSITION.TOP, labelPosition: CONTACT_INFO_LABEL_POSITION.TOP,
editable: true, editable: true,
editType: CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA, editType: CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA,
editing: true, editing: true
}, },
// Display verification words (application recipient) // Display verification words (application recipient)
displayWords: { displayWords: {
@ -81,8 +69,8 @@ export const contactMoreInfoConfig = {
label: '验证信息', label: '验证信息',
data: '', data: '',
labelPosition: CONTACT_INFO_LABEL_POSITION.LEFT, labelPosition: CONTACT_INFO_LABEL_POSITION.LEFT,
editable: false, editable: false
}, }
}; };
export const contactButtonConfig = { export const contactButtonConfig = {
@ -95,7 +83,7 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.CANCEL, type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => { onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
dismissGroup(props?.contactInfoData?.groupID); dismissGroup(props?.contactInfoData?.groupID);
}, }
}, },
quitGroup: { quitGroup: {
key: 'quitGroup', key: 'quitGroup',
@ -103,34 +91,23 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.CANCEL, type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => { onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
quitGroup(props?.contactInfoData?.groupID); quitGroup(props?.contactInfoData?.groupID);
}, }
}, },
joinGroup: { joinGroup: {
key: 'joinGroup', key: 'joinGroup',
label: '发送申请', label: '发送申请',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT, type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { onClick: (props: { contactInfoData: any; contactInfoMoreList: any; [propsName: string]: any }) => {
contactInfoData: any; joinGroup(props?.contactInfoData?.groupID, props?.contactInfoMoreList[0]?.data);
contactInfoMoreList: any; }
[propsName: string]: any;
}) => {
joinGroup(
props?.contactInfoData?.groupID,
props?.contactInfoMoreList[0]?.data,
);
},
}, },
joinAVChatGroup: { joinAVChatGroup: {
key: 'joinAVChatGroup', key: 'joinAVChatGroup',
label: '加入直播群', label: '加入直播群',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT, type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { onClick: (props: { contactInfoData: any; contactInfoMoreList: any; [propsName: string]: any }) => {
contactInfoData: any;
contactInfoMoreList: any;
[propsName: string]: any;
}) => {
joinGroup(props?.contactInfoData?.groupID); joinGroup(props?.contactInfoData?.groupID);
}, }
}, },
enterGroupConversation: { enterGroupConversation: {
key: 'enterGroupConversation', key: 'enterGroupConversation',
@ -138,7 +115,7 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT, type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => { onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
enterConversation(props?.contactInfoData); enterConversation(props?.contactInfoData);
}, }
}, },
// --------------------- // ---------------------
@ -148,18 +125,14 @@ export const contactButtonConfig = {
key: 'addFriend', key: 'addFriend',
label: '发送申请', label: '发送申请',
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT, type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { onClick: (props: { contactInfoData: any; contactInfoMoreList: any; [propsName: string]: any }) => {
contactInfoData: any;
contactInfoMoreList: any;
[propsName: string]: any;
}) => {
addFriend({ addFriend({
to: props?.contactInfoData?.userID, to: props?.contactInfoData?.userID,
source: 'AddSource_Type_Web', source: 'AddSource_Type_Web',
remark: props?.contactInfoMoreList[1]?.data, remark: props?.contactInfoMoreList[1]?.data,
wording: props?.contactInfoMoreList[0]?.data, wording: props?.contactInfoMoreList[0]?.data
}); });
}, }
}, },
deleteFriend: { deleteFriend: {
key: 'deleteFriend', key: 'deleteFriend',
@ -167,7 +140,7 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.CANCEL, type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => { onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
deleteFriend(props?.contactInfoData?.userID); deleteFriend(props?.contactInfoData?.userID);
}, }
}, },
enterC2CConversation: { enterC2CConversation: {
key: 'enterC2CConversation', key: 'enterC2CConversation',
@ -175,7 +148,7 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.SUBMIT, type: CONTACT_INFO_BUTTON_TYPE.SUBMIT,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => { onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
enterConversation(props?.contactInfoData); enterConversation(props?.contactInfoData);
}, }
}, },
// --------------------- // ---------------------
@ -188,7 +161,7 @@ export const contactButtonConfig = {
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => { onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
acceptFriendApplication(props?.contactInfoData?.userID); acceptFriendApplication(props?.contactInfoData?.userID);
TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', 'friendList'); TUIStore.update(StoreName.CUSTOM, 'currentContactListKey', 'friendList');
}, }
}, },
refuseFriendApplication: { refuseFriendApplication: {
key: 'refuseFriendApplication', key: 'refuseFriendApplication',
@ -196,6 +169,6 @@ export const contactButtonConfig = {
type: CONTACT_INFO_BUTTON_TYPE.CANCEL, type: CONTACT_INFO_BUTTON_TYPE.CANCEL,
onClick: (props: { contactInfoData: any; [propsName: string]: any }) => { onClick: (props: { contactInfoData: any; [propsName: string]: any }) => {
refuseFriendApplication(props?.contactInfoData?.userID); refuseFriendApplication(props?.contactInfoData?.userID);
}, }
}, }
}; };

View File

@ -3,53 +3,23 @@
v-if="typeof contactInfoData === 'object' && Object.keys(contactInfoData).length" v-if="typeof contactInfoData === 'object' && Object.keys(contactInfoData).length"
:class="['tui-contact-info', !isPC && 'tui-contact-info-h5']" :class="['tui-contact-info', !isPC && 'tui-contact-info-h5']"
> >
<div <div v-if="!isPC" :class="['tui-contact-info-header', !isPC && 'tui-contact-info-h5-header']">
v-if="!isPC" <div :class="['tui-contact-info-header-icon', !isPC && 'tui-contact-info-h5-header-icon']" @click="resetContactSearchingUIData">
: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" /> <Icon :file="backSVG" />
</div> </div>
<div <div :class="['tui-contact-info-header-title', !isPC && 'tui-contact-info-h5-header-title']">
:class="[ {{ TUITranslateService.t('TUIContact.添加好友/群聊') }}
'tui-contact-info-header-title',
!isPC && 'tui-contact-info-h5-header-title',
]"
>
{{ TUITranslateService.t("TUIContact.添加好友/群聊") }}
</div> </div>
</div> </div>
<div :class="['tui-contact-info-basic', !isPC && 'tui-contact-info-h5-basic']"> <div :class="['tui-contact-info-basic', !isPC && 'tui-contact-info-h5-basic']">
<div <div :class="['tui-contact-info-basic-text', !isPC && 'tui-contact-info-h5-basic-text']">
:class="[ <div :class="['tui-contact-info-basic-text-name', !isPC && 'tui-contact-info-h5-basic-text-name']">
'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) }} {{ generateContactInfoName(contactInfoData) }}
</div> </div>
<div <div
v-for="item in contactInfoBasicList" v-for="item in contactInfoBasicList"
:key="item.label" :key="item.label"
:class="[ :class="['tui-contact-info-basic-text-other', !isPC && 'tui-contact-info-h5-basic-text-other']"
'tui-contact-info-basic-text-other',
!isPC && 'tui-contact-info-h5-basic-text-other',
]"
> >
{{ {{
`${TUITranslateService.t(`TUIContact.${item.label}`)}: `${TUITranslateService.t(`TUIContact.${item.label}`)}:
@ -57,117 +27,62 @@
}} }}
</div> </div>
</div> </div>
<img <img :class="['tui-contact-info-basic-avatar', !isPC && 'tui-contact-info-h5-basic-avatar']" :src="generateAvatar(contactInfoData)" />
:class="[
'tui-contact-info-basic-avatar',
!isPC && 'tui-contact-info-h5-basic-avatar',
]"
:src="generateAvatar(contactInfoData)"
>
</div> </div>
<div <div v-if="contactInfoMoreList[0]" :class="['tui-contact-info-more', !isPC && 'tui-contact-info-h5-more']">
v-if="contactInfoMoreList[0]"
:class="['tui-contact-info-more', !isPC && 'tui-contact-info-h5-more']"
>
<div <div
v-for="item in contactInfoMoreList" v-for="item in contactInfoMoreList"
:key="item.key" :key="item.key"
:class="[ :class="[
'tui-contact-info-more-item', 'tui-contact-info-more-item',
!isPC && 'tui-contact-info-h5-more-item', !isPC && 'tui-contact-info-h5-more-item',
item.labelPosition === CONTACT_INFO_LABEL_POSITION.TOP item.labelPosition === CONTACT_INFO_LABEL_POSITION.TOP ? 'tui-contact-info-more-item-top' : 'tui-contact-info-more-item-left'
? 'tui-contact-info-more-item-top'
: 'tui-contact-info-more-item-left',
]" ]"
> >
<div <div :class="['tui-contact-info-more-item-label', !isPC && 'tui-contact-info-h5-more-item-label']">
:class="[
'tui-contact-info-more-item-label',
!isPC && 'tui-contact-info-h5-more-item-label',
]"
>
{{ `${TUITranslateService.t(`TUIContact.${item.label}`)}` }} {{ `${TUITranslateService.t(`TUIContact.${item.label}`)}` }}
</div> </div>
<div <div :class="['tui-contact-info-more-item-content', !isPC && 'tui-contact-info-h5-more-item-content']">
:class="[ <div v-if="!item.editing" :class="['tui-contact-info-more-item-content-text', !isPC && 'tui-contact-info-h5-more-item-content-text']">
'tui-contact-info-more-item-content', <div :class="['tui-contact-info-more-item-content-text-data', !isPC && 'tui-contact-info-h5-more-item-content-text-data']">
!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 }} {{ item.data }}
</div> </div>
<div <div
v-if="item.editable" v-if="item.editable"
:class="[ :class="['tui-contact-info-more-item-content-text-icon', !isPC && 'tui-contact-info-h5-more-item-content-text-icon']"
'tui-contact-info-more-item-content-text-icon',
!isPC && 'tui-contact-info-h5-more-item-content-text-icon',
]"
@click="setEditing(item)" @click="setEditing(item)"
> >
<Icon <Icon :file="editSVG" width="14px" height="14px" />
:file="editSVG"
width="14px"
height="14px"
/>
</div> </div>
</div> </div>
<input <input
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.INPUT" v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.INPUT"
v-model="item.data" v-model="item.data"
:class="[ :class="['tui-contact-info-more-item-content-input', !isPC && 'tui-contact-info-h5-more-item-content-input']"
'tui-contact-info-more-item-content-input',
!isPC && 'tui-contact-info-h5-more-item-content-input',
]"
type="text" type="text"
@confirm="onContactInfoEmitSubmit(item)" @confirm="onContactInfoEmitSubmit(item)"
@keyup.enter="onContactInfoEmitSubmit(item)" @keyup.enter="onContactInfoEmitSubmit(item)"
> />
<textarea <textarea
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA" v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA"
v-model="item.data" v-model="item.data"
:class="[ :class="['tui-contact-info-more-item-content-textarea', !isPC && 'tui-contact-info-h5-more-item-content-textarea']"
'tui-contact-info-more-item-content-textarea',
!isPC && 'tui-contact-info-h5-more-item-content-textarea',
]"
confirm-type="done" confirm-type="done"
/> />
<div <div v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.SWITCH" @click="onContactInfoEmitSubmit(item)">
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.SWITCH"
@click="onContactInfoEmitSubmit(item)"
>
<SwitchBar :value="item.data" /> <SwitchBar :value="item.data" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div <div :class="['tui-contact-info-button', !isPC && 'tui-contact-info-h5-button']">
:class="[
'tui-contact-info-button',
!isPC && 'tui-contact-info-h5-button',
]"
>
<button <button
v-for="item in contactInfoButtonList" v-for="item in contactInfoButtonList"
:key="item.key" :key="item.key"
:class="[ :class="[
'tui-contact-info-button-item', 'tui-contact-info-button-item',
!isPC && 'tui-contact-info-h5-button-item', !isPC && 'tui-contact-info-h5-button-item',
item.type === CONTACT_INFO_BUTTON_TYPE.CANCEL item.type === CONTACT_INFO_BUTTON_TYPE.CANCEL ? `tui-contact-info-button-item-cancel` : `tui-contact-info-button-item-submit`
? `tui-contact-info-button-item-cancel`
: `tui-contact-info-button-item-submit`,
]" ]"
@click="onContactInfoButtonClicked(item)" @click="onContactInfoButtonClicked(item)"
> >
@ -177,43 +92,19 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import TUIChatEngine, { import TUIChatEngine, { TUIStore, StoreName, TUITranslateService, IGroupModel, Friend, FriendApplication } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
TUITranslateService,
IGroupModel,
Friend,
FriendApplication,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, computed, onMounted, onUnmounted } from '../../../adapter-vue'; import { ref, computed, onMounted, onUnmounted } from '../../../adapter-vue';
import { isPC } from '../../../utils/env'; import { isPC } from '../../../utils/env';
import { import { generateAvatar, generateContactInfoName, generateContactInfoBasic, isFriend, isApplicationType } from '../utils/index';
generateAvatar, import { contactMoreInfoConfig, contactButtonConfig } from './contact-info-config';
generateContactInfoName,
generateContactInfoBasic,
isFriend,
isApplicationType,
} from '../utils/index';
import {
contactMoreInfoConfig,
contactButtonConfig,
} from './contact-info-config';
import Icon from '../../common/Icon.vue'; import Icon from '../../common/Icon.vue';
import editSVG from '../../../assets/icon/edit.svg'; import editSVG from '../../../assets/icon/edit.svg';
import backSVG from '../../../assets/icon/back.svg'; import backSVG from '../../../assets/icon/back.svg';
import SwitchBar from '../../common/SwitchBar/index.vue'; import SwitchBar from '../../common/SwitchBar/index.vue';
import { import { IBlackListUserItem, IContactInfoMoreItem, IContactInfoButton } from '../../../interface';
IBlackListUserItem, import { CONTACT_INFO_LABEL_POSITION, CONTACT_INFO_MORE_EDIT_TYPE, CONTACT_INFO_BUTTON_TYPE } from '../../../constant';
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'; import { deepCopy } from '../../TUIChat/utils/utils';
type IContactInfoType = IGroupModel | Friend | FriendApplication | IBlackListUserItem; type IContactInfoType = IGroupModel | Friend | FriendApplication | IBlackListUserItem;
@ -229,9 +120,7 @@ const setEditing = (item: any) => {
item.editing = true; item.editing = true;
}; };
const isGroup = computed((): boolean => const isGroup = computed((): boolean => ((contactInfoData.value as IGroupModel)?.groupID ? true : false));
(contactInfoData.value as IGroupModel)?.groupID ? true : false,
);
const isApplication = computed((): boolean => { const isApplication = computed((): boolean => {
return isApplicationType(contactInfoData?.value); return isApplicationType(contactInfoData?.value);
@ -248,11 +137,8 @@ const isGroupMember = computed((): boolean => {
// is in black list, if is group type always false // is in black list, if is group type always false
const isInBlackList = computed((): boolean => { const isInBlackList = computed((): boolean => {
return ( return (
!isGroup.value !isGroup.value &&
&& blackList.value?.findIndex( blackList.value?.findIndex((item: IBlackListUserItem) => item?.userID === (contactInfoData.value as IBlackListUserItem)?.userID) >= 0
(item: IBlackListUserItem) =>
item?.userID === (contactInfoData.value as IBlackListUserItem)?.userID,
) >= 0
); );
}); });
@ -260,19 +146,19 @@ const blackList = ref<IBlackListUserItem[]>([]);
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, { TUIStore.watch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated, currentContactInfo: onCurrentContactInfoUpdated
}); });
TUIStore.watch(StoreName.USER, { TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated, userBlacklist: onUserBlacklistUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, { TUIStore.unwatch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated, currentContactInfo: onCurrentContactInfoUpdated
}); });
TUIStore.unwatch(StoreName.USER, { TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated, userBlacklist: onUserBlacklistUpdated
}); });
}); });
@ -290,25 +176,22 @@ const resetContactSearchingUIData = () => {
}; };
const onContactInfoEmitSubmit = (item: any) => { const onContactInfoEmitSubmit = (item: any) => {
item.editSubmitHandler item.editSubmitHandler &&
&& item.editSubmitHandler({ item.editSubmitHandler({
item, item,
contactInfoData: contactInfoData.value, contactInfoData: contactInfoData.value,
isBothFriend: isBothFriend.value, isBothFriend: isBothFriend.value,
isInBlackList: isInBlackList.value, isInBlackList: isInBlackList.value
}); });
}; };
const onContactInfoButtonClicked = (item: any) => { const onContactInfoButtonClicked = (item: any) => {
item.onClick item.onClick &&
&& item.onClick({ item.onClick({
contactInfoData: contactInfoData.value, contactInfoData: contactInfoData.value,
contactInfoMoreList: contactInfoMoreList.value, contactInfoMoreList: contactInfoMoreList.value
}); });
if ( if (item.key === 'enterGroupConversation' || item.key === 'enterC2CConversation') {
item.key === 'enterGroupConversation'
|| item.key === 'enterC2CConversation'
) {
emits('switchConversation', contactInfoData.value); emits('switchConversation', contactInfoData.value);
resetContactSearchingUIData(); resetContactSearchingUIData();
} }
@ -317,17 +200,14 @@ const onContactInfoButtonClicked = (item: any) => {
const generateMoreInfo = async () => { const generateMoreInfo = async () => {
if (!isApplication.value) { if (!isApplication.value) {
if ( if (
(!isGroup.value && !isBothFriend.value && !isInBlackList.value) (!isGroup.value && !isBothFriend.value && !isInBlackList.value) ||
|| (isGroup.value (isGroup.value && !isGroupMember.value && (contactInfoData.value as IGroupModel)?.type !== TUIChatEngine?.TYPES?.GRP_AVCHATROOM)
&& !isGroupMember.value
&& (contactInfoData.value as IGroupModel)?.type !== TUIChatEngine?.TYPES?.GRP_AVCHATROOM)
) { ) {
contactMoreInfoConfig.setWords.data = ''; contactMoreInfoConfig.setWords.data = '';
contactInfoMoreList.value.push(contactMoreInfoConfig.setWords); contactInfoMoreList.value.push(contactMoreInfoConfig.setWords);
} }
if (!isGroup.value && !isInBlackList.value) { if (!isGroup.value && !isInBlackList.value) {
contactMoreInfoConfig.setRemark.data contactMoreInfoConfig.setRemark.data = (contactInfoData.value as Friend)?.remark || '';
= (contactInfoData.value as Friend)?.remark || '';
contactMoreInfoConfig.setRemark.editing = false; contactMoreInfoConfig.setRemark.editing = false;
contactInfoMoreList.value.push(contactMoreInfoConfig.setRemark); contactInfoMoreList.value.push(contactMoreInfoConfig.setRemark);
} }
@ -336,8 +216,7 @@ const generateMoreInfo = async () => {
contactInfoMoreList.value.push(contactMoreInfoConfig.blackList); contactInfoMoreList.value.push(contactMoreInfoConfig.blackList);
} }
} else { } else {
contactMoreInfoConfig.displayWords.data contactMoreInfoConfig.displayWords.data = (contactInfoData.value as FriendApplication)?.wording || '';
= (contactInfoData.value as FriendApplication)?.wording || '';
contactInfoMoreList.value.push(contactMoreInfoConfig.displayWords); contactInfoMoreList.value.push(contactMoreInfoConfig.displayWords);
} }
}; };
@ -347,16 +226,9 @@ const generateButton = () => {
return; return;
} }
if (isApplication.value) { if (isApplication.value) {
if ( if ((contactInfoData.value as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME) {
(contactInfoData.value as FriendApplication)?.type contactInfoButtonList?.value?.push(contactButtonConfig.refuseFriendApplication);
=== TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME contactInfoButtonList?.value?.push(contactButtonConfig.acceptFriendApplication);
) {
contactInfoButtonList?.value?.push(
contactButtonConfig.refuseFriendApplication,
);
contactInfoButtonList?.value?.push(
contactButtonConfig.acceptFriendApplication,
);
} }
} else { } else {
if (isGroup.value && isGroupMember.value) { if (isGroup.value && isGroupMember.value) {
@ -368,20 +240,16 @@ const generateButton = () => {
contactInfoButtonList?.value?.push(contactButtonConfig.quitGroup); contactInfoButtonList?.value?.push(contactButtonConfig.quitGroup);
break; break;
} }
contactInfoButtonList?.value?.push( contactInfoButtonList?.value?.push(contactButtonConfig.enterGroupConversation);
contactButtonConfig.enterGroupConversation,
);
} else if (!isGroup.value && isBothFriend.value) { } else if (!isGroup.value && isBothFriend.value) {
contactInfoButtonList?.value?.push(contactButtonConfig.deleteFriend); contactInfoButtonList?.value?.push(contactButtonConfig.deleteFriend);
contactInfoButtonList?.value?.push( contactInfoButtonList?.value?.push(contactButtonConfig.enterC2CConversation);
contactButtonConfig.enterC2CConversation,
);
} else { } else {
if (isGroup.value) { if (isGroup.value) {
contactInfoButtonList?.value?.push( contactInfoButtonList?.value?.push(
(contactInfoData.value as IGroupModel)?.type === TUIChatEngine?.TYPES?.GRP_AVCHATROOM (contactInfoData.value as IGroupModel)?.type === TUIChatEngine?.TYPES?.GRP_AVCHATROOM
? contactButtonConfig.joinAVChatGroup ? contactButtonConfig.joinAVChatGroup
: contactButtonConfig.joinGroup, : contactButtonConfig.joinGroup
); );
} else { } else {
contactInfoButtonList?.value?.push(contactButtonConfig.addFriend); contactInfoButtonList?.value?.push(contactButtonConfig.addFriend);
@ -395,11 +263,7 @@ function onUserBlacklistUpdated(userBlacklist: IBlackListUserItem[]) {
} }
async function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) { async function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) {
if ( if (contactInfoData.value && contactInfo && JSON.stringify(contactInfoData.value) === JSON.stringify(contactInfo)) {
contactInfoData.value
&& contactInfo
&& JSON.stringify(contactInfoData.value) === JSON.stringify(contactInfo)
) {
return; return;
} }
resetContactInfoUIData(); resetContactInfoUIData();
@ -408,9 +272,7 @@ async function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) {
if (!contactInfoData.value || Object.keys(contactInfoData.value)?.length === 0) { if (!contactInfoData.value || Object.keys(contactInfoData.value)?.length === 0) {
return; return;
} }
contactInfoBasicList.value = generateContactInfoBasic( contactInfoBasicList.value = generateContactInfoBasic(contactInfoData.value);
contactInfoData.value,
);
isBothFriend.value = await isFriend(contactInfoData.value); isBothFriend.value = await isFriend(contactInfoData.value);
generateMoreInfo(); generateMoreInfo();
generateButton(); generateButton();

View File

@ -1,17 +1,13 @@
<template> <template>
<div :class="['tui-contact-list-card', !isPC && 'tui-contact-list-card-h5']"> <div :class="['tui-contact-list-card', !isPC && 'tui-contact-list-card-h5']">
<div class="tui-contact-list-card-left"> <div class="tui-contact-list-card-left">
<Avatar <Avatar class="tui-contact-list-card-left-avatar" useSkeletonAnimation :url="generateAvatar(props.item)" />
class="tui-contact-list-card-left-avatar"
useSkeletonAnimation
:url="generateAvatar(props.item)"
/>
<div <div
v-if="props.displayOnlineStatus && props.item" v-if="props.displayOnlineStatus && props.item"
:class="{ :class="{
'online-status': true, 'online-status': true,
'online-status-online': isOnline, 'online-status-online': isOnline,
'online-status-offline': !isOnline, 'online-status-offline': !isOnline
}" }"
/> />
</div> </div>
@ -19,28 +15,16 @@
<div class="tui-contact-list-card-main-name"> <div class="tui-contact-list-card-main-name">
{{ generateName(props.item) }} {{ generateName(props.item) }}
</div> </div>
<div <div v-if="otherContentForSow" class="tui-contact-list-card-main-other">
v-if="otherContentForSow"
class="tui-contact-list-card-main-other"
>
{{ otherContentForSow }} {{ otherContentForSow }}
</div> </div>
</div> </div>
<div class="tui-contact-list-card-right"> <div class="tui-contact-list-card-right">
<div <div v-if="groupTypeForShow" class="tui-contact-list-card-right-group-type">
v-if="groupTypeForShow"
class="tui-contact-list-card-right-group-type"
>
{{ groupTypeForShow }} {{ groupTypeForShow }}
</div> </div>
<div <div v-if="showApplicationStatus" class="tui-contact-list-card-right-application">
v-if="showApplicationStatus" <div v-if="showApplicationStatus.style === 'text'" class="tui-contact-list-card-right-application-text">
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}`) }} {{ TUITranslateService.t(`TUIContact.${showApplicationStatus.label}`) }}
</div> </div>
<button <button
@ -56,12 +40,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, withDefaults, inject, watch, ref, Ref } from '../../../../adapter-vue'; import { computed, withDefaults, inject, watch, ref, Ref } from '../../../../adapter-vue';
import TUIChatEngine, { import TUIChatEngine, { TUITranslateService, IGroupModel, FriendApplication, Friend } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
IGroupModel,
FriendApplication,
Friend,
} from '@tencentcloud/chat-uikit-engine';
import { IContactInfoType, IUserStatus } from '../../../../interface'; import { IContactInfoType, IUserStatus } from '../../../../interface';
import Avatar from '../../../common/Avatar/index.vue'; import Avatar from '../../../common/Avatar/index.vue';
import { generateAvatar, generateName, acceptFriendApplication } from '../../utils'; import { generateAvatar, generateName, acceptFriendApplication } from '../../utils';
@ -73,9 +52,9 @@ const props = withDefaults(
displayOnlineStatus?: boolean; displayOnlineStatus?: boolean;
}>(), }>(),
{ {
item: () => ({} as IContactInfoType), item: () => ({}) as IContactInfoType,
displayOnlineStatus: false, displayOnlineStatus: false
}, }
); );
const userOnlineStatusMap = inject<Ref<Map<string, IUserStatus>>>('userOnlineStatusMap'); const userOnlineStatusMap = inject<Ref<Map<string, IUserStatus>>>('userOnlineStatusMap');
const isOnline = ref<boolean>(false); const isOnline = ref<boolean>(false);
@ -85,14 +64,14 @@ const groupType = {
[TUIChatEngine.TYPES.GRP_AVCHATROOM]: 'AVChatRoom', [TUIChatEngine.TYPES.GRP_AVCHATROOM]: 'AVChatRoom',
[TUIChatEngine.TYPES.GRP_PUBLIC]: 'Public', [TUIChatEngine.TYPES.GRP_PUBLIC]: 'Public',
[TUIChatEngine.TYPES.GRP_MEETING]: 'Meeting', [TUIChatEngine.TYPES.GRP_MEETING]: 'Meeting',
[TUIChatEngine.TYPES.GRP_COMMUNITY]: 'Community', [TUIChatEngine.TYPES.GRP_COMMUNITY]: 'Community'
}; };
const otherContentForSow = computed((): string => { const otherContentForSow = computed((): string => {
let content = ''; let content = '';
if ( if (
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME (props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME ||
|| (props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME (props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME
) { ) {
content = (props.item as FriendApplication)?.wording || ''; content = (props.item as FriendApplication)?.wording || '';
} else if ((props.item as IGroupModel)?.groupID) { } else if ((props.item as IGroupModel)?.groupID) {
@ -110,22 +89,18 @@ const groupTypeForShow = computed((): string => {
}); });
const showApplicationStatus = computed(() => { const showApplicationStatus = computed(() => {
if ( if ((props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME) {
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME
) {
return { return {
style: 'text', style: 'text',
label: '等待验证', label: '等待验证'
}; };
} else if ( } else if ((props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME) {
(props.item as FriendApplication)?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
) {
return { return {
style: 'button', style: 'button',
label: '同意', label: '同意',
onClick: () => { onClick: () => {
acceptFriendApplication((props.item as FriendApplication)?.userID); acceptFriendApplication((props.item as FriendApplication)?.userID);
}, }
}; };
} }
return false; return false;
@ -138,16 +113,16 @@ watch(
}, },
{ {
immediate: true, immediate: true,
deep: true, deep: true
}, }
); );
function getOnlineStatus(): boolean { function getOnlineStatus(): boolean {
return !!( return !!(
props.displayOnlineStatus props.displayOnlineStatus &&
&& userOnlineStatusMap?.value userOnlineStatusMap?.value &&
&& (props.item as Friend)?.userID (props.item as Friend)?.userID &&
&& userOnlineStatusMap.value?.[(props.item as Friend).userID]?.statusType === TUIChatEngine.TYPES.USER_STATUS_ONLINE userOnlineStatusMap.value?.[(props.item as Friend).userID]?.statusType === TUIChatEngine.TYPES.USER_STATUS_ONLINE
); );
} }
</script> </script>

View File

@ -1,30 +1,13 @@
<template> <template>
<ul <ul v-if="!contactSearchingStatus" :class="['tui-contact-list', !isPC && 'tui-contact-list-h5']">
v-if="!contactSearchingStatus" <li v-for="(contactListObj, key) in contactListMap" :key="key" class="tui-contact-list-item">
:class="['tui-contact-list', !isPC && 'tui-contact-list-h5']" <header class="tui-contact-list-item-header" @click="toggleCurrentContactList(key)">
>
<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"> <div class="tui-contact-list-item-header-left">
<Icon <Icon :file="currentContactListKey === key ? downSVG : rightSVG" width="16px" height="16px" />
:file="currentContactListKey === key ? downSVG : rightSVG"
width="16px"
height="16px"
/>
<div>{{ TUITranslateService.t(`TUIContact.${contactListObj.title}`) }}</div> <div>{{ TUITranslateService.t(`TUIContact.${contactListObj.title}`) }}</div>
</div> </div>
<div class="tui-contact-list-item-header-right"> <div class="tui-contact-list-item-header-right">
<span <span v-if="contactListObj.unreadCount" class="tui-contact-list-item-header-right-unread">
v-if="contactListObj.unreadCount"
class="tui-contact-list-item-header-right-unread"
>
{{ contactListObj.unreadCount }} {{ contactListObj.unreadCount }}
</span> </span>
</div> </div>
@ -46,19 +29,9 @@
</ul> </ul>
</li> </li>
</ul> </ul>
<ul <ul v-else class="tui-contact-list">
v-else <li v-for="(item, key) in contactSearchResult" :key="key" class="tui-contact-list-item">
class="tui-contact-list" <div v-if="item.list[0]" class="tui-contact-search-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"> <div class="tui-contact-search-list-title">
{{ TUITranslateService.t(`TUIContact.${item.label}`) }} {{ TUITranslateService.t(`TUIContact.${item.label}`) }}
</div> </div>
@ -69,18 +42,12 @@
:class="['selected']" :class="['selected']"
@click="selectItem(listItem)" @click="selectItem(listItem)"
> >
<ContactListItem <ContactListItem :item="listItem" :displayOnlineStatus="false" />
:item="listItem"
:displayOnlineStatus="false"
/>
</div> </div>
</div> </div>
</li> </li>
<div <div v-if="isContactSearchNoResult" class="tui-contact-search-list-default">
v-if="isContactSearchNoResult" {{ TUITranslateService.t('TUIContact.无搜索结果') }}
class="tui-contact-search-list-default"
>
{{ TUITranslateService.t("TUIContact.无搜索结果") }}
</div> </div>
</ul> </ul>
</template> </template>
@ -93,21 +60,14 @@ import {
TUIFriendService, TUIFriendService,
Friend, Friend,
FriendApplication, FriendApplication,
TUIUserService, TUIUserService
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core'; import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { ref, computed, onMounted, onUnmounted, provide } from '../../../adapter-vue'; import { ref, computed, onMounted, onUnmounted, provide } from '../../../adapter-vue';
import Icon from '../../common/Icon.vue'; import Icon from '../../common/Icon.vue';
import downSVG from '../../../assets/icon/down-icon.svg'; import downSVG from '../../../assets/icon/down-icon.svg';
import rightSVG from '../../../assets/icon/right-icon.svg'; import rightSVG from '../../../assets/icon/right-icon.svg';
import { import { IContactList, IContactSearchResult, IBlackListUserItem, IUserStatus, IUserStatusMap, IContactInfoType } from '../../../interface';
IContactList,
IContactSearchResult,
IBlackListUserItem,
IUserStatus,
IUserStatusMap,
IContactInfoType,
} from '../../../interface';
import ContactListItem from './contact-list-item/index.vue'; import ContactListItem from './contact-list-item/index.vue';
import { deepCopy } from '../../TUIChat/utils/utils'; import { deepCopy } from '../../TUIChat/utils/utils';
import { isPC } from '../../../utils/env'; import { isPC } from '../../../utils/env';
@ -119,23 +79,23 @@ const contactListMap = ref<IContactList>({
key: 'friendApplicationList', key: 'friendApplicationList',
title: '新的联系人', title: '新的联系人',
list: [] as FriendApplication[], list: [] as FriendApplication[],
unreadCount: 0, unreadCount: 0
}, },
blackList: { blackList: {
key: 'blackList', key: 'blackList',
title: '黑名单', title: '黑名单',
list: [] as IBlackListUserItem[], list: [] as IBlackListUserItem[]
}, },
groupList: { groupList: {
key: 'groupList', key: 'groupList',
title: '我的群聊', title: '我的群聊',
list: [] as IGroupModel[], list: [] as IGroupModel[]
}, },
friendList: { friendList: {
key: 'friendList', key: 'friendList',
title: '我的好友', title: '我的好友',
list: [] as Friend[], list: [] as Friend[]
}, }
}); });
const contactSearchingStatus = ref<boolean>(false); const contactSearchingStatus = ref<boolean>(false);
const contactSearchResult = ref<IContactSearchResult>(); const contactSearchResult = ref<IContactSearchResult>();
@ -143,67 +103,64 @@ const displayOnlineStatus = ref<boolean>(false);
const userOnlineStatusMap = ref<IUserStatusMap>(); const userOnlineStatusMap = ref<IUserStatusMap>();
const isContactSearchNoResult = computed((): boolean => { const isContactSearchNoResult = computed((): boolean => {
return ( return !contactSearchResult?.value?.user?.list[0] && !contactSearchResult?.value?.group?.list[0];
!contactSearchResult?.value?.user?.list[0]
&& !contactSearchResult?.value?.group?.list[0]
);
}); });
onMounted(() => { onMounted(() => {
TUIStore.watch(StoreName.APP, { TUIStore.watch(StoreName.APP, {
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated, enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated
}); });
TUIStore.watch(StoreName.GRP, { TUIStore.watch(StoreName.GRP, {
groupList: onGroupListUpdated, groupList: onGroupListUpdated
}); });
TUIStore.watch(StoreName.USER, { TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated, userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated, displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated, userStatusList: onUserStatusListUpdated
}); });
TUIStore.watch(StoreName.FRIEND, { TUIStore.watch(StoreName.FRIEND, {
friendList: onFriendListUpdated, friendList: onFriendListUpdated,
friendApplicationList: onFriendApplicationListUpdated, friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated, friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated
}); });
TUIStore.watch(StoreName.CUSTOM, { TUIStore.watch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated, currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated, currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated, currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated, currentContactInfo: onCurrentContactInfoUpdated
}); });
}); });
onUnmounted(() => { onUnmounted(() => {
TUIStore.unwatch(StoreName.APP, { TUIStore.unwatch(StoreName.APP, {
enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated, enabledCustomerServicePlugin: onCustomerServiceCommercialPluginUpdated
}); });
TUIStore.unwatch(StoreName.GRP, { TUIStore.unwatch(StoreName.GRP, {
groupList: onGroupListUpdated, groupList: onGroupListUpdated
}); });
TUIStore.unwatch(StoreName.USER, { TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated, userBlacklist: onUserBlacklistUpdated,
displayOnlineStatus: onDisplayOnlineStatusUpdated, displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated, userStatusList: onUserStatusListUpdated
}); });
TUIStore.unwatch(StoreName.FRIEND, { TUIStore.unwatch(StoreName.FRIEND, {
friendList: onFriendListUpdated, friendList: onFriendListUpdated,
friendApplicationList: onFriendApplicationListUpdated, friendApplicationList: onFriendApplicationListUpdated,
friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated, friendApplicationUnreadCount: onFriendApplicationUnreadCountUpdated
}); });
TUIStore.unwatch(StoreName.CUSTOM, { TUIStore.unwatch(StoreName.CUSTOM, {
currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated, currentContactSearchingStatus: onCurrentContactSearchingStatusUpdated,
currentContactSearchResult: onCurrentContactSearchResultUpdated, currentContactSearchResult: onCurrentContactSearchResultUpdated,
currentContactListKey: onCurrentContactListKeyUpdated, currentContactListKey: onCurrentContactListKeyUpdated,
currentContactInfo: onCurrentContactInfoUpdated, currentContactInfo: onCurrentContactInfoUpdated
}); });
}); });
@ -230,11 +187,11 @@ function selectItem(item: any) {
let targetListItem; let targetListItem;
if ((currentContactInfo.value as Friend)?.userID) { if ((currentContactInfo.value as Friend)?.userID) {
targetListItem = contactListMap.value?.friendList?.list?.find( targetListItem = contactListMap.value?.friendList?.list?.find(
(item: IContactInfoType) => (item as Friend)?.userID === (currentContactInfo.value as Friend)?.userID, (item: IContactInfoType) => (item as Friend)?.userID === (currentContactInfo.value as Friend)?.userID
); );
} else if ((currentContactInfo.value as IGroupModel)?.groupID) { } else if ((currentContactInfo.value as IGroupModel)?.groupID) {
targetListItem = contactListMap.value?.groupList?.list?.find( targetListItem = contactListMap.value?.groupList?.list?.find(
(item: IContactInfoType) => (item as IGroupModel)?.groupID === (currentContactInfo.value as IGroupModel)?.groupID, (item: IContactInfoType) => (item as IGroupModel)?.groupID === (currentContactInfo.value as IGroupModel)?.groupID
); );
} }
if (targetListItem) { if (targetListItem) {
@ -283,15 +240,15 @@ function onCustomerServiceCommercialPluginUpdated(isEnabled: boolean) {
...item, ...item,
renderKey: generateRenderKey('customerList', item, index), renderKey: generateRenderKey('customerList', item, index),
infoKeyList: [], infoKeyList: [],
btnKeyList: ['enterC2CConversation'], btnKeyList: ['enterC2CConversation']
}; };
}), }),
key: 'customerList', key: 'customerList'
}; };
contactListMap.value = { ...contactListMap.value, customerList }; contactListMap.value = { ...contactListMap.value, customerList };
} }
}) })
.catch(() => { }); .catch(() => {});
} }
} }
@ -317,28 +274,27 @@ function onFriendApplicationListUpdated(friendApplicationList: FriendApplication
function updateContactListMap(key: keyof IContactList, list: IContactInfoType[]) { function updateContactListMap(key: keyof IContactList, list: IContactInfoType[]) {
contactListMap.value[key].list = list; contactListMap.value[key].list = list;
contactListMap.value[key].list.map((item: IContactInfoType, index: number) => item.renderKey = generateRenderKey(key, item, index)); contactListMap.value[key].list.map((item: IContactInfoType, index: number) => (item.renderKey = generateRenderKey(key, item, index)));
updateCurrentContactInfoFromList(contactListMap.value[key].list, key); updateCurrentContactInfoFromList(contactListMap.value[key].list, key);
} }
function updateCurrentContactInfoFromList(list: IContactInfoType[], type: keyof IContactList) { function updateCurrentContactInfoFromList(list: IContactInfoType[], type: keyof IContactList) {
if ( if (!(currentContactInfo.value as Friend)?.userID && !(currentContactInfo.value as IGroupModel)?.groupID) {
!(currentContactInfo.value as Friend)?.userID
&& !(currentContactInfo.value as IGroupModel)?.groupID
) {
return; return;
} }
if (type === currentContactListKey.value || contactSearchingStatus.value) { if (type === currentContactListKey.value || contactSearchingStatus.value) {
currentContactInfo.value = list?.find( currentContactInfo.value =
(item: any) => list?.find(
(item?.groupID && item?.groupID === (currentContactInfo.value as IGroupModel)?.groupID) || (item?.userID && item?.userID === (currentContactInfo.value as Friend)?.userID), (item: any) =>
) || {} as IContactInfoType; (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); TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', currentContactInfo.value);
} }
} }
function generateRenderKey(contactListMapKey: keyof IContactList, contactInfo: IContactInfoType, index: number) { function generateRenderKey(contactListMapKey: keyof IContactList, contactInfo: IContactInfoType, index: number) {
return `${contactListMapKey}-${(contactInfo as Friend).userID || (contactInfo as IGroupModel).groupID || ('index' + index)}`; return `${contactListMapKey}-${(contactInfo as Friend).userID || (contactInfo as IGroupModel).groupID || 'index' + index}`;
} }
function onCurrentContactSearchResultUpdated(searchResult: IContactSearchResult) { function onCurrentContactSearchResultUpdated(searchResult: IContactSearchResult) {

View File

@ -2,43 +2,21 @@
<div :class="['tui-contact-search', !isPC && 'tui-contact-search-h5']"> <div :class="['tui-contact-search', !isPC && 'tui-contact-search-h5']">
<div <div
v-if="!isSearching || !isPC" v-if="!isSearching || !isPC"
:class="[ :class="['tui-contact-search-header', !isPC && 'tui-contact-search-h5-header', isSearching && 'tui-contact-searching-h5-header']"
'tui-contact-search-header',
!isPC && 'tui-contact-search-h5-header',
isSearching && 'tui-contact-searching-h5-header',
]"
@click="changeContactSearchingStatus(true)" @click="changeContactSearchingStatus(true)"
> >
<div <div
:class="[ :class="['tui-contact-search-header-icon', !isPC && 'tui-contact-search-h5-header-icon']"
'tui-contact-search-header-icon',
!isPC && 'tui-contact-search-h5-header-icon',
]"
@click.stop="changeContactSearchingStatus(!isSearching)" @click.stop="changeContactSearchingStatus(!isSearching)"
> >
<Icon <Icon :file="isSearching ? backSVG : addSVG" :width="isSearching ? '20px' : '14px'" :height="isSearching ? '20px' : '14px'" />
:file="isSearching ? backSVG : addSVG"
:width="isSearching ? '20px' : '14px'"
:height="isSearching ? '20px' : '14px'"
/>
</div> </div>
<div <div :class="['tui-contact-search-header-title', !isPC && 'tui-contact-search-h5-header-title']">
:class="[ {{ TUITranslateService.t('TUIContact.添加好友/群聊') }}
'tui-contact-search-header-title',
!isPC && 'tui-contact-search-h5-header-title',
]"
>
{{ TUITranslateService.t("TUIContact.添加好友/群聊") }}
</div> </div>
</div> </div>
<div <div v-if="isSearching" :class="['tui-contact-search-main', !isPC && 'tui-contact-search-h5-main']">
v-if="isSearching"
:class="[
'tui-contact-search-main',
!isPC && 'tui-contact-search-h5-main',
]"
>
<input <input
v-model="searchValue" v-model="searchValue"
class="tui-contact-search-main-input" class="tui-contact-search-main-input"
@ -48,23 +26,16 @@
@keyup.enter="search" @keyup.enter="search"
@blur="search" @blur="search"
@confirm="search" @confirm="search"
> />
<div <div class="tui-contact-search-main-cancel" @click="isSearching = false">
class="tui-contact-search-main-cancel" {{ TUITranslateService.t('取消') }}
@click="isSearching = false"
>
{{ TUITranslateService.t("取消") }}
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from '../../../adapter-vue'; import { ref, watch } from '../../../adapter-vue';
import { import { TUITranslateService, TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core'; import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import Icon from '../../common/Icon.vue'; import Icon from '../../common/Icon.vue';
@ -80,12 +51,12 @@ const searchValue = ref<string>('');
const searchResult = ref<IContactSearchResult>({ const searchResult = ref<IContactSearchResult>({
user: { user: {
label: '联系人', label: '联系人',
list: [], list: []
}, },
group: { group: {
label: '群聊', label: '群聊',
list: [], list: []
}, }
}); });
const changeContactSearchingStatus = debounce(function (status: boolean) { const changeContactSearchingStatus = debounce(function (status: boolean) {
@ -100,8 +71,8 @@ const search = async () => {
serviceName: TUIConstants.TUISearch.SERVICE.NAME, serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_USER, method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_USER,
params: { params: {
userID: searchValue.value, userID: searchValue.value
}, }
}) })
.then((res: any) => { .then((res: any) => {
searchResult.value.user.list = res.data; searchResult.value.user.list = res.data;
@ -114,8 +85,8 @@ const search = async () => {
serviceName: TUIConstants.TUISearch.SERVICE.NAME, serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_GROUP, method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_GROUP,
params: { params: {
groupID: searchValue.value, groupID: searchValue.value
}, }
}) })
.then((res: any) => { .then((res: any) => {
searchResult.value.group.list = [res.data.group]; searchResult.value.group.list = [res.data.group];
@ -128,25 +99,17 @@ const search = async () => {
watch( watch(
() => searchResult.value, () => searchResult.value,
() => { () => {
TUIStore.update( TUIStore.update(StoreName.CUSTOM, 'currentContactSearchResult', searchResult.value);
StoreName.CUSTOM,
'currentContactSearchResult',
searchResult.value,
);
}, },
{ {
deep: true, deep: true,
immediate: true, immediate: true
}, }
); );
watch( watch(
() => isSearching.value, () => isSearching.value,
() => { () => {
TUIStore.update( TUIStore.update(StoreName.CUSTOM, 'currentContactSearchingStatus', isSearching.value);
StoreName.CUSTOM,
'currentContactSearchingStatus',
isSearching.value,
);
if (isSearching.value) { if (isSearching.value) {
searchValue.value = ''; searchValue.value = '';
searchResult.value.user.list = []; searchResult.value.user.list = [];
@ -155,8 +118,8 @@ watch(
}, },
{ {
deep: true, deep: true,
immediate: true, immediate: true
}, }
); );
TUIGlobal.updateContactSearch = search; TUIGlobal.updateContactSearch = search;

View File

@ -1,17 +1,11 @@
<template> <template>
<SelectFriend v-if="isShowSelectFriend" /> <SelectFriend v-if="isShowSelectFriend" />
<div <div v-else-if="isShowContactList" :class="['tui-contact', !isPC && 'tui-contact-h5']">
v-else-if="isShowContactList"
:class="['tui-contact', !isPC && 'tui-contact-h5']"
>
<div :class="['tui-contact-left', !isPC && 'tui-contact-h5-left']"> <div :class="['tui-contact-left', !isPC && 'tui-contact-h5-left']">
<ContactSearch /> <ContactSearch />
<ContactList :class="['tui-contact-left-list', !isPC && 'tui-contact-h5-left-list']" /> <ContactList :class="['tui-contact-left-list', !isPC && 'tui-contact-h5-left-list']" />
</div> </div>
<div <div v-if="isShowContactInfo" :class="['tui-contact-right', !isPC && 'tui-contact-h5-right']">
v-if="isShowContactInfo"
:class="['tui-contact-right', !isPC && 'tui-contact-h5-right']"
>
<ContactInfo @switchConversation="switchConversation" /> <ContactInfo @switchConversation="switchConversation" />
</div> </div>
</div> </div>
@ -34,8 +28,8 @@ const props = defineProps({
displayType: { displayType: {
type: String, type: String,
default: 'contactList', // "contactList" / "selectFriend" default: 'contactList', // "contactList" / "selectFriend"
require: false, require: false
}, }
}); });
const displayTypeRef = ref<string>(props.displayType || 'contactList'); const displayTypeRef = ref<string>(props.displayType || 'contactList');
@ -64,26 +58,27 @@ TUIStore.watch(StoreName.CUSTOM, {
isShowSelectFriend.value = false; isShowSelectFriend.value = false;
if (isUniFrameWork) { if (isUniFrameWork) {
displayTypeRef.value = props.displayType; displayTypeRef.value = props.displayType;
TUIGlobal?.showTabBar()?.catch(() => { /* ignore */ }); TUIGlobal?.showTabBar()?.catch(() => {
/* ignore */
});
} }
} }
}, },
currentContactInfo: (contactInfo: any) => { currentContactInfo: (contactInfo: any) => {
isShowContactInfo.value = isPC || (contactInfo && typeof contactInfo === 'object' && Object.keys(contactInfo)?.length > 0); isShowContactInfo.value = isPC || (contactInfo && typeof contactInfo === 'object' && Object.keys(contactInfo)?.length > 0);
}, }
}); });
const switchConversation = (data: any) => { const switchConversation = (data: any) => {
isUniFrameWork isUniFrameWork &&
&& TUIGlobal?.navigateTo({ TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index', url: '/TUIKit/components/TUIChat/index'
}); });
emits('switchConversation', data); emits('switchConversation', data);
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../assets/styles/common"; @import '../../assets/styles/common';
.tui-contact { .tui-contact {
width: 100%; width: 100%;

View File

@ -9,12 +9,7 @@
/> />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { TUIFriendService, TUIStore, StoreName, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUIFriendService,
TUIStore,
StoreName,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { ref, watchEffect } from '../../../adapter-vue'; import { ref, watchEffect } from '../../../adapter-vue';
import { Toast, TOAST_TYPE } from '../../common/Toast/index'; import { Toast, TOAST_TYPE } from '../../common/Toast/index';
import TUICore from '@tencentcloud/tui-core'; import TUICore from '@tencentcloud/tui-core';
@ -30,7 +25,7 @@ const TUISearchServer = ref<any>(null);
const selectOptions = ref({ const selectOptions = ref({
isRadio: false, isRadio: false,
isNeedSearch: false, isNeedSearch: false,
title: '', title: ''
}); });
const generateSearchServer = (isNeedSearch: any) => { const generateSearchServer = (isNeedSearch: any) => {
@ -50,12 +45,14 @@ watchEffect(() => {
if (params.isNeedSearch) { if (params.isNeedSearch) {
generateSearchServer(params.isNeedSearch); generateSearchServer(params.isNeedSearch);
} }
TUIFriendService.getFriendList().then((res: any) => { TUIFriendService.getFriendList()
friendList.value = res.data.map((item: any) => item.profile); .then((res: any) => {
userList.value = friendList.value; friendList.value = res.data.map((item: any) => item.profile);
}).catch((err: any) => { userList.value = friendList.value;
console.warn('getFriendList error:', err); })
}); .catch((err: any) => {
console.warn('getFriendList error:', err);
});
}); });
const handleSelectedResult = (memberList: Array<any>) => { const handleSelectedResult = (memberList: Array<any>) => {
@ -67,14 +64,14 @@ const handleSelectedResult = (memberList: Array<any>) => {
const searchFail = () => { const searchFail = () => {
Toast({ Toast({
message: TUITranslateService.t('TUIGroup.该用户不存在'), message: TUITranslateService.t('TUIGroup.该用户不存在'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
userList.value = [...friendList.value]; userList.value = [...friendList.value];
}; };
const handleSearch = async (val: string) => { const handleSearch = async (val: string) => {
if (!val) { if (!val) {
return userList.value = friendList.value; return (userList.value = friendList.value);
} }
try { try {

View File

@ -35,9 +35,10 @@ export default class TUIContactServer {
this.onCallCallbackMap.set(method, callback); this.onCallCallbackMap.set(method, callback);
if (method === TUIConstants.TUIContact.SERVICE.METHOD.SELECT_FRIEND) { if (method === TUIConstants.TUIContact.SERVICE.METHOD.SELECT_FRIEND) {
TUIStore.update(StoreName.CUSTOM, 'isShowSelectFriendComponent', true); TUIStore.update(StoreName.CUSTOM, 'isShowSelectFriendComponent', true);
isUniFrameWork && TUIGlobal?.reLaunch({ isUniFrameWork &&
url: '/TUIKit/components/TUIContact/index', TUIGlobal?.reLaunch({
}); url: '/TUIKit/components/TUIContact/index'
});
} }
} }
} }

View File

@ -5,69 +5,49 @@ import TUIChatEngine, {
TUIUserService, TUIUserService,
TUITranslateService, TUITranslateService,
AddFriendParams, AddFriendParams,
JoinGroupParams, JoinGroupParams
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import { Toast, TOAST_TYPE } from '../../common/Toast/index'; import { Toast, TOAST_TYPE } from '../../common/Toast/index';
export const generateAvatar = (item: any): string => { export const generateAvatar = (item: any): string => {
return ( return (
item?.avatar item?.avatar ||
|| item?.profile?.avatar item?.profile?.avatar ||
|| (item?.groupID && 'https://web.sdk.qcloud.com/im/assets/images/Public.svg') (item?.groupID && 'https://web.sdk.qcloud.com/im/assets/images/Public.svg') ||
|| 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png' 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
); );
}; };
export const generateName = (item: any): string => { export const generateName = (item: any): string => {
return ( return item?.remark || item?.name || item?.profile?.nick || item?.nick || item?.groupID || item?.userID || '';
item?.remark
|| item?.name
|| item?.profile?.nick
|| item?.nick
|| item?.groupID
|| item?.userID
|| ''
);
}; };
export const generateContactInfoName = (item: any): string => { export const generateContactInfoName = (item: any): string => {
return ( return item?.name || item?.profile?.nick || item?.nick || item?.groupID || item?.userID || '';
item?.name
|| item?.profile?.nick
|| item?.nick
|| item?.groupID
|| item?.userID
|| ''
);
}; };
// Parse the basic information display content of the contactInfo module // Parse the basic information display content of the contactInfo module
// Group information display: group ID group type // Group information display: group ID group type
// User information display: user ID personal signature // User information display: user ID personal signature
export const generateContactInfoBasic = ( export const generateContactInfoBasic = (contactInfo: any): any[] => {
contactInfo: any,
): any[] => {
const res = [ const res = [
{ {
label: contactInfo?.groupID ? '群ID' : 'ID', label: contactInfo?.groupID ? '群ID' : 'ID',
data: contactInfo?.groupID || contactInfo?.userID || '', data: contactInfo?.groupID || contactInfo?.userID || ''
}, }
]; ];
if (!isApplicationType(contactInfo)) { if (!isApplicationType(contactInfo)) {
res.push({ res.push({
label: contactInfo?.groupID ? '群类型' : '个性签名', label: contactInfo?.groupID ? '群类型' : '个性签名',
data: contactInfo?.type || contactInfo?.profile?.selfSignature || '', data: contactInfo?.type || contactInfo?.profile?.selfSignature || ''
}); });
} }
return res; return res;
}; };
export const isApplicationType = (info: any) => { export const isApplicationType = (info: any) => {
return ( return info?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME || info?.type === TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_BY_ME;
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> => { export const isFriend = (info: any): Promise<boolean> => {
@ -82,7 +62,7 @@ export const isFriend = (info: any): Promise<boolean> => {
} }
TUIFriendService.checkFriend({ TUIFriendService.checkFriend({
userIDList: [info?.userID], userIDList: [info?.userID],
type: TUIChatEngine.TYPES.SNS_CHECK_TYPE_BOTH, type: TUIChatEngine.TYPES.SNS_CHECK_TYPE_BOTH
}) })
.then((res: any) => { .then((res: any) => {
switch (res?.data?.successUserIDList[0]?.relation) { switch (res?.data?.successUserIDList[0]?.relation) {
@ -116,29 +96,28 @@ export const isFriend = (info: any): Promise<boolean> => {
// Change friends remark // Change friends remark
export const updateFriendRemark = (userID: string, remark: string) => { export const updateFriendRemark = (userID: string, remark: string) => {
// eslint-disable-next-line no-control-regex
if (remark?.replace(/[^\u0000-\u00ff]/g, 'aa')?.length > 96) { if (remark?.replace(/[^\u0000-\u00ff]/g, 'aa')?.length > 96) {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.修改备注失败: 备注长度不得超过 96 字节'), message: TUITranslateService.t('TUIContact.修改备注失败: 备注长度不得超过 96 字节'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
return; return;
} }
TUIFriendService.updateFriend({ TUIFriendService.updateFriend({
userID, userID,
remark, remark
}) })
.then(() => { .then(() => {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.修改备注成功'), message: TUITranslateService.t('TUIContact.修改备注成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
}) })
.catch((error: any) => { .catch((error: any) => {
console.warn('update friend remark failed:', error); console.warn('update friend remark failed:', error);
Toast({ Toast({
message: TUITranslateService.t('TUIContact.修改备注失败'), message: TUITranslateService.t('TUIContact.修改备注失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
@ -147,19 +126,19 @@ export const updateFriendRemark = (userID: string, remark: string) => {
export const deleteFriend = (userID: string) => { export const deleteFriend = (userID: string) => {
TUIFriendService.deleteFriend({ TUIFriendService.deleteFriend({
userIDList: [userID], userIDList: [userID],
type: TUIChatEngine.TYPES.SNS_DELETE_TYPE_BOTH, type: TUIChatEngine.TYPES.SNS_DELETE_TYPE_BOTH
}) })
.then((res: any) => { .then((res: any) => {
const { successUserIDList } = res.data; const { successUserIDList } = res.data;
if (successUserIDList[0].userID === userID) { if (successUserIDList[0].userID === userID) {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.删除好友成功'), message: TUITranslateService.t('TUIContact.删除好友成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
} else { } else {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.删除好友失败'), message: TUITranslateService.t('TUIContact.删除好友失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
} }
}) })
@ -167,7 +146,7 @@ export const deleteFriend = (userID: string) => {
console.warn('delete friend failed:', error); console.warn('delete friend failed:', error);
Toast({ Toast({
message: TUITranslateService.t('TUIContact.删除好友失败'), message: TUITranslateService.t('TUIContact.删除好友失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
@ -178,51 +157,47 @@ export const addFriend = (params: AddFriendParams) => {
.then(() => { .then(() => {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.申请已发送'), message: TUITranslateService.t('TUIContact.申请已发送'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
}) })
.catch((error: any) => { .catch((error: any) => {
console.warn('delete friend failed:', error); console.warn('delete friend failed:', error);
Toast({ Toast({
message: TUITranslateService.t('TUIContact.申请发送失败'), message: TUITranslateService.t('TUIContact.申请发送失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
// Enter conversation // Enter conversation
export const enterConversation = (item: any) => { export const enterConversation = (item: any) => {
const conversationID = item?.groupID const conversationID = item?.groupID ? `GROUP${item?.groupID}` : `C2C${item?.userID}`;
? `GROUP${item?.groupID}` TUIConversationService.switchConversation(conversationID).catch((error: any) => {
: `C2C${item?.userID}`; console.warn('switch conversation failed:', error);
TUIConversationService.switchConversation(conversationID).catch( Toast({
(error: any) => { message: TUITranslateService.t('TUIContact.进入会话失败'),
console.warn('switch conversation failed:', error); type: TOAST_TYPE.ERROR
Toast({ });
message: TUITranslateService.t('TUIContact.进入会话失败'), });
type: TOAST_TYPE.ERROR,
});
},
);
}; };
// Accept friend application // Accept friend application
export const acceptFriendApplication = (userID: string) => { export const acceptFriendApplication = (userID: string) => {
TUIFriendService.acceptFriendApplication({ TUIFriendService.acceptFriendApplication({
userID, userID,
type: TUIChatEngine.TYPES.SNS_APPLICATION_AGREE_AND_ADD, type: TUIChatEngine.TYPES.SNS_APPLICATION_AGREE_AND_ADD
}) })
.then(() => { .then(() => {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.添加好友成功'), message: TUITranslateService.t('TUIContact.添加好友成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
}) })
.catch((error: any) => { .catch((error: any) => {
console.warn('accept friend application failed:', error); console.warn('accept friend application failed:', error);
Toast({ Toast({
message: TUITranslateService.t('TUIContact.同意好友申请失败'), message: TUITranslateService.t('TUIContact.同意好友申请失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
@ -233,14 +208,14 @@ export const refuseFriendApplication = (userID: string) => {
.then(() => { .then(() => {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.拒绝成功'), message: TUITranslateService.t('TUIContact.拒绝成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
}) })
.catch((error: any) => { .catch((error: any) => {
console.warn('accept friend application failed:', error); console.warn('accept friend application failed:', error);
Toast({ Toast({
message: TUITranslateService.t('TUIContact.拒绝好友申请失败'), message: TUITranslateService.t('TUIContact.拒绝好友申请失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
@ -251,7 +226,7 @@ export const dismissGroup = (groupID: string) => {
.then(() => { .then(() => {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.解散群聊成功'), message: TUITranslateService.t('TUIContact.解散群聊成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
TUIGlobal?.updateContactSearch && TUIGlobal?.updateContactSearch(); TUIGlobal?.updateContactSearch && TUIGlobal?.updateContactSearch();
}) })
@ -259,7 +234,7 @@ export const dismissGroup = (groupID: string) => {
console.warn('dismiss group failed:', error); console.warn('dismiss group failed:', error);
Toast({ Toast({
message: TUITranslateService.t('TUIContact.解散群聊失败'), message: TUITranslateService.t('TUIContact.解散群聊失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
@ -270,14 +245,14 @@ export const quitGroup = (groupID: string) => {
.then(() => { .then(() => {
Toast({ Toast({
message: TUITranslateService.t('TUIContact.退出群组成功'), message: TUITranslateService.t('TUIContact.退出群组成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
}) })
.catch((error: any) => { .catch((error: any) => {
console.warn('quit group failed:', error); console.warn('quit group failed:', error);
Toast({ Toast({
message: TUITranslateService.t('TUIContact.退出群组失败'), message: TUITranslateService.t('TUIContact.退出群组失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
@ -286,26 +261,26 @@ export const quitGroup = (groupID: string) => {
export const joinGroup = (groupID: string, applyMessage?: string) => { export const joinGroup = (groupID: string, applyMessage?: string) => {
TUIGroupService.joinGroup({ TUIGroupService.joinGroup({
groupID, groupID,
applyMessage, applyMessage
} as JoinGroupParams) } as JoinGroupParams)
.then((imResponse: { data: { status?: string } }) => { .then((imResponse: { data: { status?: string } }) => {
switch (imResponse?.data?.status) { switch (imResponse?.data?.status) {
case TUIChatEngine.TYPES.JOIN_STATUS_WAIT_APPROVAL: // Wait for administrator approval case TUIChatEngine.TYPES.JOIN_STATUS_WAIT_APPROVAL: // Wait for administrator approval
Toast({ Toast({
message: TUITranslateService.t('TUIContact.等待管理员同意'), message: TUITranslateService.t('TUIContact.等待管理员同意'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
break; break;
case TUIChatEngine.TYPES.JOIN_STATUS_SUCCESS: // Join group successfully case TUIChatEngine.TYPES.JOIN_STATUS_SUCCESS: // Join group successfully
Toast({ Toast({
message: TUITranslateService.t('TUIContact.加群成功'), message: TUITranslateService.t('TUIContact.加群成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
break; break;
case TUIChatEngine.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // Already in the group case TUIChatEngine.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // Already in the group
Toast({ Toast({
message: TUITranslateService.t('TUIContact.您已是群成员'), message: TUITranslateService.t('TUIContact.您已是群成员'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
break; break;
default: default:
@ -316,7 +291,7 @@ export const joinGroup = (groupID: string, applyMessage?: string) => {
console.warn('join group failed:', error); console.warn('join group failed:', error);
Toast({ Toast({
message: '申请入群失败', message: '申请入群失败',
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
@ -324,7 +299,7 @@ export const joinGroup = (groupID: string, applyMessage?: string) => {
// Add to blacklist // Add to blacklist
export const addToBlacklist = (userID: string, successCallBack?: () => void) => { export const addToBlacklist = (userID: string, successCallBack?: () => void) => {
TUIUserService.addToBlacklist({ TUIUserService.addToBlacklist({
userIDList: [userID], userIDList: [userID]
}) })
.then(() => { .then(() => {
successCallBack && successCallBack(); successCallBack && successCallBack();
@ -333,18 +308,15 @@ export const addToBlacklist = (userID: string, successCallBack?: () => void) =>
console.warn('add to blacklist failed:', error); console.warn('add to blacklist failed:', error);
Toast({ Toast({
message: TUITranslateService.t('TUIContact.加入黑名单失败'), message: TUITranslateService.t('TUIContact.加入黑名单失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
// Remove from Blacklist // Remove from Blacklist
export const removeFromBlacklist = ( export const removeFromBlacklist = (userID: string, successCallBack?: () => void) => {
userID: string,
successCallBack?: () => void,
) => {
TUIUserService.removeFromBlacklist({ TUIUserService.removeFromBlacklist({
userIDList: [userID], userIDList: [userID]
}) })
.then(() => { .then(() => {
successCallBack && successCallBack(); successCallBack && successCallBack();
@ -353,7 +325,7 @@ export const removeFromBlacklist = (
console.warn('remove from blacklist failed:', error); console.warn('remove from blacklist failed:', error);
Toast({ Toast({
message: TUITranslateService.t('TUIContact.移除黑名单失败'), message: TUITranslateService.t('TUIContact.移除黑名单失败'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };

View File

@ -1,54 +1,44 @@
<template> <template>
<Overlay <Overlay :maskColor="'transparent'" @onOverlayClick="() => emits('closeConversationActionMenu')">
:maskColor="'transparent'"
@onOverlayClick="() => emits('closeConversationActionMenu')"
>
<div <div
id="conversation-actions-menu" id="conversation-actions-menu"
ref="actionsMenuDomRef" ref="actionsMenuDomRef"
:class="[ :class="[isPC && 'actions-menu-pc', 'actions-menu', !isHiddenActionsMenu && 'cancel-hidden']"
isPC && 'actions-menu-pc',
'actions-menu',
!isHiddenActionsMenu && 'cancel-hidden',
]"
:style="{ :style="{
top: `${_actionsMenuPosition.top}px`, top: `${_actionsMenuPosition.top}px`,
left: `${_actionsMenuPosition.left}px`, left: `${_actionsMenuPosition.left}px`
}" }"
> >
<div <div :class="['actions-menu-item']" @click.stop="deleteConversation()">
:class="['actions-menu-item']" {{ TUITranslateService.t('TUIConversation.删除会话') }}
@click.stop="deleteConversation()"
>
{{ TUITranslateService.t("TUIConversation.删除会话") }}
</div> </div>
<div <div
v-if="!(props.selectedConversation && props.selectedConversation.isPinned)" v-if="!(props.selectedConversation && props.selectedConversation.isPinned)"
:class="['actions-menu-item']" :class="['actions-menu-item']"
@click.stop="handleItem({ name: CONV_OPERATION.ISPINNED })" @click.stop="handleItem({ name: CONV_OPERATION.ISPINNED })"
> >
{{ TUITranslateService.t("TUIConversation.置顶会话") }} {{ TUITranslateService.t('TUIConversation.置顶会话') }}
</div> </div>
<div <div
v-if="props.selectedConversation && props.selectedConversation.isPinned" v-if="props.selectedConversation && props.selectedConversation.isPinned"
:class="['actions-menu-item']" :class="['actions-menu-item']"
@click.stop="handleItem({ name: CONV_OPERATION.DISPINNED })" @click.stop="handleItem({ name: CONV_OPERATION.DISPINNED })"
> >
{{ TUITranslateService.t("TUIConversation.取消置顶") }} {{ TUITranslateService.t('TUIConversation.取消置顶') }}
</div> </div>
<div <div
v-if="!(props.selectedConversation && props.selectedConversation.isMuted)" v-if="!(props.selectedConversation && props.selectedConversation.isMuted)"
:class="['actions-menu-item']" :class="['actions-menu-item']"
@click.stop="handleItem({ name: CONV_OPERATION.MUTE })" @click.stop="handleItem({ name: CONV_OPERATION.MUTE })"
> >
{{ TUITranslateService.t("TUIConversation.消息免打扰") }} {{ TUITranslateService.t('TUIConversation.消息免打扰') }}
</div> </div>
<div <div
v-if="props.selectedConversation && props.selectedConversation.isMuted" v-if="props.selectedConversation && props.selectedConversation.isMuted"
:class="['actions-menu-item']" :class="['actions-menu-item']"
@click.stop="handleItem({ name: CONV_OPERATION.NOTMUTE })" @click.stop="handleItem({ name: CONV_OPERATION.NOTMUTE })"
> >
{{ TUITranslateService.t("TUIConversation.取消免打扰") }} {{ TUITranslateService.t('TUIConversation.取消免打扰') }}
</div> </div>
</div> </div>
<Dialog <Dialog
@ -66,18 +56,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { import { ref, nextTick, onMounted, computed, getCurrentInstance } from '../../../adapter-vue';
ref, import TUIChatEngine, { IConversationModel, TUIStore, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
nextTick,
onMounted,
computed,
getCurrentInstance,
} from '../../../adapter-vue';
import TUIChatEngine, {
IConversationModel,
TUIStore,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import { CONV_OPERATION } from '../../../constant'; import { CONV_OPERATION } from '../../../constant';
import { isPC, isUniFrameWork } from '../../../utils/env'; import { isPC, isUniFrameWork } from '../../../utils/env';
@ -101,9 +81,7 @@ const thisInstance = getCurrentInstance()?.proxy || getCurrentInstance();
const actionsMenuDomRef = ref<HTMLElement | null>(); const actionsMenuDomRef = ref<HTMLElement | null>();
const isHiddenActionsMenu = ref(true); const isHiddenActionsMenu = ref(true);
const isShowDeleteConversationDialog = ref<boolean>(false); const isShowDeleteConversationDialog = ref<boolean>(false);
const currentConversation = TUIStore.getConversationModel( const currentConversation = TUIStore.getConversationModel(props.selectedConversation?.conversationID || '');
props.selectedConversation?.conversationID || '',
);
const _actionsMenuPosition = ref<{ const _actionsMenuPosition = ref<{
top: number; top: number;
left?: number; left?: number;
@ -117,7 +95,9 @@ onMounted(() => {
const deleteConversationDialogTitle = computed(() => { const deleteConversationDialogTitle = computed(() => {
return props.selectedConversation?.type === TUIChatEngine.TYPES.CONV_C2C return props.selectedConversation?.type === TUIChatEngine.TYPES.CONV_C2C
? 'TUIConversation.删除后,将清空该聊天的消息记录' ? 'TUIConversation.删除后,将清空该聊天的消息记录'
: props.selectedConversation?.type === TUIChatEngine.TYPES.CONV_GROUP ? 'TUIConversation.删除后,将清空该群聊的消息记录' : ''; : props.selectedConversation?.type === TUIChatEngine.TYPES.CONV_GROUP
? 'TUIConversation.删除后,将清空该群聊的消息记录'
: '';
}); });
function checkExceedBounds() { function checkExceedBounds() {
// When the component is initially rendered, it executes and self-checks whether the boundary exceeds the screen, and handles it in nextTick. // When the component is initially rendered, it executes and self-checks whether the boundary exceeds the screen, and handles it in nextTick.
@ -133,10 +113,7 @@ function checkExceedBounds() {
if (data.bottom > TUIGlobal?.getWindowInfo?.().windowHeight) { if (data.bottom > TUIGlobal?.getWindowInfo?.().windowHeight) {
_actionsMenuPosition.value = { _actionsMenuPosition.value = {
...props.actionsMenuPosition, ...props.actionsMenuPosition,
top: top: props.actionsMenuPosition.top - (props.actionsMenuPosition.conversationHeight || 0) - data.height
props.actionsMenuPosition.top
- (props.actionsMenuPosition.conversationHeight || 0)
- data.height,
}; };
} }
// check if actionsMenu is exceed right of the screen // check if actionsMenu is exceed right of the screen
@ -155,10 +132,7 @@ function checkExceedBounds() {
_actionsMenuPosition.value.left = props.actionsMenuPosition.left; _actionsMenuPosition.value.left = props.actionsMenuPosition.left;
} }
if (rect && rect.bottom > window.innerHeight) { if (rect && rect.bottom > window.innerHeight) {
_actionsMenuPosition.value.top _actionsMenuPosition.value.top = props.actionsMenuPosition.top - (props.actionsMenuPosition.conversationHeight || 0) - rect.height;
= props.actionsMenuPosition.top
- (props.actionsMenuPosition.conversationHeight || 0)
- rect.height;
} }
isHiddenActionsMenu.value = false; isHiddenActionsMenu.value = false;
} }

View File

@ -1,54 +1,20 @@
<template> <template>
<div <div :ref="convHeaderRef" class="tui-conversation-header">
:ref="convHeaderRef" <ul v-if="menuList.length > 0" class="list">
class="tui-conversation-header" <li v-for="(item, index) in menuList" :key="index" class="list-item">
> <main class="tui-conversation-header-item" @click.stop="handleMenu(item)">
<ul <Icon v-if="item.icon && !item.data.children" class="tui-conversation-header-item-icon" :file="item.icon" />
v-if="menuList.length > 0" <i v-else class="plus" />
class="list"
>
<li
v-for="(item, index) in menuList"
:key="index"
class="list-item"
>
<main
class="tui-conversation-header-item"
@click.stop="handleMenu(item)"
>
<Icon
v-if="item.icon && !item.data.children"
class="tui-conversation-header-item-icon"
:file="item.icon"
/>
<i
v-else
class="plus"
/>
<h1 class="tui-conversation-header-item-title"> <h1 class="tui-conversation-header-item-title">
{{ item.text }} {{ item.text }}
</h1> </h1>
</main> </main>
</li> </li>
</ul> </ul>
<ul <ul v-if="showChildren.length > 0" class="tui-conversation-header-children list">
v-if="showChildren.length > 0" <li v-for="(childrenItem, childrenIndex) in showChildren" :key="childrenIndex" class="list-item">
class="tui-conversation-header-children list" <main class="tui-conversation-header-item" @click="handleMenu(childrenItem)">
> <Icon v-if="childrenItem.icon" class="tui-conversation-header-item-icon" :file="childrenItem.icon" />
<li
v-for="(childrenItem, childrenIndex) in showChildren"
:key="childrenIndex"
class="list-item"
>
<main
class="tui-conversation-header-item"
@click="handleMenu(childrenItem)"
>
<Icon
v-if="childrenItem.icon"
class="tui-conversation-header-item-icon"
:file="childrenItem.icon"
/>
<h1 class="tui-conversation-header-item-title"> <h1 class="tui-conversation-header-item-title">
{{ childrenItem.text }} {{ childrenItem.text }}
</h1> </h1>
@ -74,7 +40,10 @@ onMounted(() => {
}); });
const handleMenu = (item: IMenuItem) => { const handleMenu = (item: IMenuItem) => {
const { data: { children }, listener = { onClicked: () => {} } } = item; const {
data: { children },
listener = { onClicked: () => {} }
} = item;
if (children) { if (children) {
showChildren.value = showChildren.value.length > 0 ? [] : children; showChildren.value = showChildren.value.length > 0 ? [] : children;
} else { } else {
@ -88,9 +57,8 @@ const closeChildren = () => {
}; };
defineExpose({ defineExpose({
closeChildren, closeChildren
}); });
</script> </script>
<style lang="scss" scoped src="../style/index.scss"></style> <style lang="scss" scoped src="../style/index.scss"></style>

View File

@ -30,13 +30,15 @@ export default class ConversationHeaderServer {
public getMenu(): any[] { public getMenu(): any[] {
const list = this.generateMenuList(); const list = this.generateMenuList();
if (!isPC && list.length > 0) { if (!isPC && list.length > 0) {
return [{ return [
text: TUITranslateService.t('TUIConversation.发起会话'), {
data: { text: TUITranslateService.t('TUIConversation.发起会话'),
name: 'all', data: {
children: list, name: 'all',
}, children: list
}]; }
}
];
} }
return list; return list;
} }
@ -47,22 +49,22 @@ export default class ConversationHeaderServer {
icon: C2C, icon: C2C,
text: TUITranslateService.t('TUIConversation.发起单聊'), text: TUITranslateService.t('TUIConversation.发起单聊'),
data: { data: {
name: CONV_CREATE_TYPE.TYPEC2C, name: CONV_CREATE_TYPE.TYPEC2C
}, },
listener: { listener: {
onClicked: this.createConversation.bind(this), onClicked: this.createConversation.bind(this)
}, }
}, },
{ {
icon: createGroup, icon: createGroup,
text: TUITranslateService.t('TUIConversation.发起群聊'), text: TUITranslateService.t('TUIConversation.发起群聊'),
data: { data: {
name: CONV_CREATE_TYPE.TYPEGROUP, name: CONV_CREATE_TYPE.TYPEGROUP
}, },
listener: { listener: {
onClicked: this.createConversation.bind(this), onClicked: this.createConversation.bind(this)
}, }
}, }
]; ];
return list; return list;
} }
@ -72,7 +74,7 @@ export default class ConversationHeaderServer {
TUICore.callService({ TUICore.callService({
serviceName: TUIConstants.TUIConversation.SERVICE.NAME, serviceName: TUIConstants.TUIConversation.SERVICE.NAME,
method: TUIConstants.TUIConversation.SERVICE.METHOD.CREATE_CONVERSATION, method: TUIConstants.TUIConversation.SERVICE.METHOD.CREATE_CONVERSATION,
params: item, params: item
}); });
} }
} }

View File

@ -1,8 +1,5 @@
<template> <template>
<div <div ref="conversationListInnerDomRef" class="tui-conversation-list">
ref="conversationListInnerDomRef"
class="tui-conversation-list"
>
<ActionsMenu <ActionsMenu
v-if="isShowOverlay" v-if="isShowOverlay"
:selectedConversation="currentConversation" :selectedConversation="currentConversation"
@ -14,55 +11,36 @@
v-for="(conversation, index) in conversationList" v-for="(conversation, index) in conversationList"
:id="`convlistitem-${index}`" :id="`convlistitem-${index}`"
:key="index" :key="index"
:class="[ :class="['tui-conversation-content', isMobile && 'tui-conversation-content-h5 disable-select']"
'tui-conversation-content',
isMobile && 'tui-conversation-content-h5 disable-select',
]"
> >
<div <div
:class="[ :class="[
isPC && 'isPC', isPC && 'isPC',
'tui-conversation-item', 'tui-conversation-item',
currentConversationID === conversation.conversationID && currentConversationID === conversation.conversationID && 'tui-conversation-item-selected',
'tui-conversation-item-selected', conversation.isPinned && 'tui-conversation-item-pinned'
conversation.isPinned && 'tui-conversation-item-pinned',
]" ]"
@click="enterConversationChat(conversation.conversationID)" @click="enterConversationChat(conversation.conversationID)"
@longpress="showConversationActionMenu($event, conversation, index)" @longpress="showConversationActionMenu($event, conversation, index)"
@contextmenu="showConversationActionMenu($event, conversation, index, true)" @contextmenu="showConversationActionMenu($event, conversation, index, true)"
> >
<aside class="left"> <aside class="left">
<Avatar <Avatar useSkeletonAnimation :url="conversation.getAvatar()" size="30px" />
useSkeletonAnimation
:url="conversation.getAvatar()"
size="30px"
/>
<div <div
v-if="userOnlineStatusMap && isShowUserOnlineStatus(conversation)" v-if="userOnlineStatusMap && isShowUserOnlineStatus(conversation)"
:class="[ :class="[
'online-status', 'online-status',
Object.keys(userOnlineStatusMap).length > 0 && Object.keys(userOnlineStatusMap).length > 0 &&
Object.keys(userOnlineStatusMap).includes( Object.keys(userOnlineStatusMap).includes(conversation.userProfile.userID) &&
conversation.userProfile.userID userOnlineStatusMap[conversation.userProfile.userID].statusType === 1
) &&
userOnlineStatusMap[conversation.userProfile.userID]
.statusType === 1
? 'online-status-online' ? 'online-status-online'
: 'online-status-offline', : 'online-status-offline'
]" ]"
/> />
<span <span v-if="conversation.unreadCount > 0 && !conversation.isMuted" class="num">
v-if="conversation.unreadCount > 0 && !conversation.isMuted" {{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
class="num"
>
{{
conversation.unreadCount > 99 ? "99+" : conversation.unreadCount
}}
</span> </span>
<span <span v-if="conversation.unreadCount > 0 && conversation.isMuted" class="num-notify" />
v-if="conversation.unreadCount > 0 && conversation.isMuted"
class="num-notify"
/>
</aside> </aside>
<div class="content"> <div class="content">
<div class="content-header"> <div class="content-header">
@ -70,29 +48,22 @@
<p class="name">{{ conversation.getShowName() }}</p> <p class="name">{{ conversation.getShowName() }}</p>
</label> </label>
<div class="middle-box"> <div class="middle-box">
<span v-if="conversation.draftText && conversation.conversationID !== currentConversationID" class="middle-box-draft">{{
TUITranslateService.t('TUIChat.[草稿]')
}}</span>
<span <span
v-if="conversation.draftText && conversation.conversationID !== currentConversationID" v-else-if="conversation.type === 'GROUP' && conversation.groupAtInfoList && conversation.groupAtInfoList.length > 0"
class="middle-box-draft"
>{{ TUITranslateService.t('TUIChat.[草稿]') }}</span>
<span
v-else-if="
conversation.type === 'GROUP' &&
conversation.groupAtInfoList &&
conversation.groupAtInfoList.length > 0
"
class="middle-box-at" class="middle-box-at"
>{{ conversation.getGroupAtInfo() }}</span> >{{ conversation.getGroupAtInfo() }}</span
>
<div class="middle-box-content"> <div class="middle-box-content">
{{ conversation.getLastMessage("text") }} {{ conversation.getLastMessage('text') }}
</div> </div>
</div> </div>
</div> </div>
<div class="content-footer"> <div class="content-footer">
<span class="time">{{ conversation.getLastMessage("time") }}</span> <span class="time">{{ conversation.getLastMessage('time') }}</span>
<Icon <Icon v-if="conversation.isMuted" :file="muteIcon" />
v-if="conversation.isMuted"
:file="muteIcon"
/>
</div> </div>
</div> </div>
</div> </div>
@ -111,13 +82,7 @@ interface IUserStatusMap {
} }
import { ref, onMounted, onUnmounted } from '../../../adapter-vue'; import { ref, onMounted, onUnmounted } from '../../../adapter-vue';
import TUIChatEngine, { import TUIChatEngine, { TUIStore, StoreName, TUIConversationService, TUITranslateService, IConversationModel } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
TUIConversationService,
TUITranslateService,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal, isIOS, addLongPressListener } from '@tencentcloud/universal-api'; import { TUIGlobal, isIOS, addLongPressListener } from '@tencentcloud/universal-api';
import Icon from '../../common/Icon.vue'; import Icon from '../../common/Icon.vue';
import Avatar from '../../common/Avatar/index.vue'; import Avatar from '../../common/Avatar/index.vue';
@ -140,7 +105,7 @@ const actionsMenuPosition = ref<{
}>({ }>({
top: 0, top: 0,
left: undefined, left: undefined,
conversationHeight: undefined, conversationHeight: undefined
}); });
const displayOnlineStatus = ref(false); const displayOnlineStatus = ref(false);
const userOnlineStatusMap = ref<IUserStatusMap>(); const userOnlineStatusMap = ref<IUserStatusMap>();
@ -151,12 +116,12 @@ onMounted(() => {
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated,
conversationList: onConversationListUpdated, conversationList: onConversationListUpdated,
currentConversation: onCurrentConversationUpdated, currentConversation: onCurrentConversationUpdated
}); });
TUIStore.watch(StoreName.USER, { TUIStore.watch(StoreName.USER, {
displayOnlineStatus: onDisplayOnlineStatusUpdated, displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated, userStatusList: onUserStatusListUpdated
}); });
if (!isUniFrameWork && isIOS && !isPC) { if (!isUniFrameWork && isIOS && !isPC) {
@ -168,28 +133,20 @@ onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, { TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdated, currentConversationID: onCurrentConversationIDUpdated,
conversationList: onConversationListUpdated, conversationList: onConversationListUpdated,
currentConversation: onCurrentConversationUpdated, currentConversation: onCurrentConversationUpdated
}); });
TUIStore.unwatch(StoreName.USER, { TUIStore.unwatch(StoreName.USER, {
displayOnlineStatus: onDisplayOnlineStatusUpdated, displayOnlineStatus: onDisplayOnlineStatusUpdated,
userStatusList: onUserStatusListUpdated, userStatusList: onUserStatusListUpdated
}); });
}); });
const isShowUserOnlineStatus = (conversation: IConversationModel): boolean => { const isShowUserOnlineStatus = (conversation: IConversationModel): boolean => {
return ( return displayOnlineStatus.value && conversation.type === TUIChatEngine.TYPES.CONV_C2C;
displayOnlineStatus.value
&& conversation.type === TUIChatEngine.TYPES.CONV_C2C
);
}; };
const showConversationActionMenu = ( const showConversationActionMenu = (event: Event, conversation: IConversationModel, index: number, isContextMenuEvent?: boolean) => {
event: Event,
conversation: IConversationModel,
index: number,
isContextMenuEvent?: boolean,
) => {
if (isContextMenuEvent) { if (isContextMenuEvent) {
event.preventDefault(); event.preventDefault();
if (isUniFrameWork) { if (isUniFrameWork) {
@ -203,10 +160,7 @@ const showConversationActionMenu = (
const closeConversationActionMenu = () => { const closeConversationActionMenu = () => {
// Prevent continuous triggering of overlay tap events // Prevent continuous triggering of overlay tap events
if ( if (lastestOpenActionsMenuTime && Date.now() - lastestOpenActionsMenuTime > 300) {
lastestOpenActionsMenuTime
&& Date.now() - lastestOpenActionsMenuTime > 300
) {
currentConversation.value = undefined; currentConversation.value = undefined;
isShowOverlay.value = false; isShowOverlay.value = false;
} }
@ -218,25 +172,28 @@ const getActionsMenuPosition = (event: Event, index: number) => {
emits('getPassingRef', conversationListDomRef); emits('getPassingRef', conversationListDomRef);
} }
const query = TUIGlobal?.createSelectorQuery().in(conversationListDomRef.value); const query = TUIGlobal?.createSelectorQuery().in(conversationListDomRef.value);
query.select(`#convlistitem-${index}`).boundingClientRect((data) => { query
if (data) { .select(`#convlistitem-${index}`)
actionsMenuPosition.value = { .boundingClientRect((data) => {
// The uni-page-head of uni-h5 is not considered a member of the viewport, so the height of the head is manually increased. if (data) {
top: data.bottom + (isH5 ? 44 : 0), actionsMenuPosition.value = {
// @ts-expect-error in uniapp event has touches property // The uni-page-head of uni-h5 is not considered a member of the viewport, so the height of the head is manually increased.
left: event.touches[0].pageX, top: data.bottom + (isH5 ? 44 : 0),
conversationHeight: data.height, // @ts-expect-error in uniapp event has touches property
}; left: event.touches[0].pageX,
isShowOverlay.value = true; conversationHeight: data.height
} };
}).exec(); isShowOverlay.value = true;
}
})
.exec();
} else { } else {
const rect = ((event.currentTarget || event.target) as HTMLElement)?.getBoundingClientRect() || {}; const rect = ((event.currentTarget || event.target) as HTMLElement)?.getBoundingClientRect() || {};
if (rect) { if (rect) {
actionsMenuPosition.value = { actionsMenuPosition.value = {
top: rect.bottom, top: rect.bottom,
left: isPC ? (event as MouseEvent).clientX : undefined, left: isPC ? (event as MouseEvent).clientX : undefined,
conversationHeight: rect.height, conversationHeight: rect.height
}; };
} }
isShowOverlay.value = true; isShowOverlay.value = true;
@ -260,9 +217,9 @@ function addLongPressHandler() {
}, },
options: { options: {
eventDelegation: { eventDelegation: {
subSelector: '.tui-conversation-content', subSelector: '.tui-conversation-content'
}, }
}, }
}); });
} }
@ -284,13 +241,10 @@ function onDisplayOnlineStatusUpdated(status: boolean) {
function onUserStatusListUpdated(list: Map<string, IUserStatus>) { function onUserStatusListUpdated(list: Map<string, IUserStatus>) {
if (list.size !== 0) { if (list.size !== 0) {
userOnlineStatusMap.value = [...list.entries()].reduce( userOnlineStatusMap.value = [...list.entries()].reduce((obj, [key, value]) => {
(obj, [key, value]) => { obj[key] = value;
obj[key] = value; return obj;
return obj; }, {} as IUserStatusMap);
},
{} as IUserStatusMap,
);
} }
} }
// Expose to the parent component and close actionsMenu when a sliding event is detected // Expose to the parent component and close actionsMenu when a sliding event is detected
@ -300,11 +254,11 @@ defineExpose({ closeChildren: closeConversationActionMenu });
<style lang="scss" scoped src="./style/index.scss"></style> <style lang="scss" scoped src="./style/index.scss"></style>
<style lang="scss" scoped> <style lang="scss" scoped>
.disable-select { .disable-select {
-webkit-touch-callout:none; -webkit-touch-callout: none;
-webkit-user-select:none; -webkit-user-select: none;
-khtml-user-select:none; -khtml-user-select: none;
-moz-user-select:none; -moz-user-select: none;
-ms-user-select:none; -ms-user-select: none;
user-select:none; user-select: none;
} }
</style> </style>

View File

@ -1,32 +1,21 @@
<template> <template>
<div <div v-if="isNotNetwork" class="network">
v-if="isNotNetwork"
class="network"
>
<i class="icon icon-error">!</i> <i class="icon icon-error">!</i>
<p class="network-content"> <p class="network-content">
{{ {{ TUITranslateService.t('TUIConversation.网络异常,请您检查网络设置') }}
TUITranslateService.t("TUIConversation.网络异常,请您检查网络设置")
}}
</p> </p>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import TUIChatEngine, { import TUIChatEngine, { TUIStore, StoreName, TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUIStore, import { ref } from '../../../adapter-vue';
StoreName,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import {
ref,
} from '../../../adapter-vue';
const isNotNetwork = ref(false); const isNotNetwork = ref(false);
TUIStore.watch(StoreName.USER, { TUIStore.watch(StoreName.USER, {
netStateChange: (value: string) => { netStateChange: (value: string) => {
isNotNetwork.value = (value === TUIChatEngine.TYPES.NET_STATE_DISCONNECTED); isNotNetwork.value = value === TUIChatEngine.TYPES.NET_STATE_DISCONNECTED;
}, }
}); });
</script> </script>

View File

@ -1,5 +1,5 @@
import TUIConversation from "./index.vue"; import TUIConversation from './index.vue';
import TUIConversationServer from "./server"; import TUIConversationServer from './server';
new TUIConversationServer(); new TUIConversationServer();
export default TUIConversation; export default TUIConversation;

View File

@ -1,12 +1,6 @@
<template> <template>
<div <div class="tui-conversation" @click="handleClickConv">
class="tui-conversation" <ConversationHeader v-if="isShowConversationHeader" ref="headerRef" />
@click="handleClickConv"
>
<ConversationHeader
v-if="isShowConversationHeader"
ref="headerRef"
/>
<ConversationNetwork /> <ConversationNetwork />
<ConversationList @handleSwitchConversation="handleSwitchConversation" /> <ConversationList @handleSwitchConversation="handleSwitchConversation" />
</div> </div>
@ -27,13 +21,13 @@ const isShowConversationHeader = ref<boolean>(true);
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
totalUnreadCount: (count: number) => { totalUnreadCount: (count: number) => {
totalUnreadCount.value = count; totalUnreadCount.value = count;
}, }
}); });
TUIStore.watch(StoreName.CUSTOM, { TUIStore.watch(StoreName.CUSTOM, {
isShowConversationHeader: (showStatus: boolean) => { isShowConversationHeader: (showStatus: boolean) => {
isShowConversationHeader.value = showStatus !== false; isShowConversationHeader.value = showStatus !== false;
}, }
}); });
const handleSwitchConversation = (conversationID: string) => { const handleSwitchConversation = (conversationID: string) => {

View File

@ -1,10 +1,5 @@
import TUICore, { TUIConstants } from '@tencentcloud/tui-core'; import TUICore, { TUIConstants } from '@tencentcloud/tui-core';
import { import { TUITranslateService, TUIConversationService, TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
TUIConversationService,
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import { CONV_CREATE_TYPE } from '../../constant'; import { CONV_CREATE_TYPE } from '../../constant';
import { isUniFrameWork } from '../../utils/env'; import { isUniFrameWork } from '../../utils/env';
@ -61,23 +56,23 @@ export default class TUIConversationServer {
icon: createC2CIcon, icon: createC2CIcon,
text: TUITranslateService.t('TUIConversation.发起单聊'), text: TUITranslateService.t('TUIConversation.发起单聊'),
data: { data: {
name: CONV_CREATE_TYPE.TYPEC2C, name: CONV_CREATE_TYPE.TYPEC2C
}, },
listener: { listener: {
onClicked: this.createConversation.bind(this), onClicked: this.createConversation.bind(this)
}, }
}, },
{ {
weight: 100, weight: 100,
icon: createGroupIcon, icon: createGroupIcon,
text: TUITranslateService.t('TUIConversation.发起群聊'), text: TUITranslateService.t('TUIConversation.发起群聊'),
data: { data: {
name: CONV_CREATE_TYPE.TYPEGROUP, name: CONV_CREATE_TYPE.TYPEGROUP
}, },
listener: { listener: {
onClicked: this.createConversation.bind(this), onClicked: this.createConversation.bind(this)
}, }
}, }
]; ];
return list; return list;
} }
@ -91,7 +86,7 @@ export default class TUIConversationServer {
params: { params: {
title: item.text, title: item.text,
isRadio: item.data.name !== CONV_CREATE_TYPE.TYPEGROUP, isRadio: item.data.name !== CONV_CREATE_TYPE.TYPEGROUP,
isNeedSearch: !TUIStore.getData(StoreName.APP, 'isOfficial'), isNeedSearch: !TUIStore.getData(StoreName.APP, 'isOfficial')
}, },
callback: async (memberList: any[]) => { callback: async (memberList: any[]) => {
if (!memberList || memberList.length === 0) { if (!memberList || memberList.length === 0) {
@ -107,7 +102,7 @@ export default class TUIConversationServer {
await this.generateConversation(`C2C${userID}`); await this.generateConversation(`C2C${userID}`);
this.routerForward(`C2C${userID}`); this.routerForward(`C2C${userID}`);
} }
}, }
}); });
} }
@ -117,7 +112,7 @@ export default class TUIConversationServer {
method: TUIConstants.TUIGroup.SERVICE.METHOD.CREATE_GROUP, method: TUIConstants.TUIGroup.SERVICE.METHOD.CREATE_GROUP,
params: { params: {
title: TUITranslateService.t('TUIConversation.发起群聊'), title: TUITranslateService.t('TUIConversation.发起群聊'),
memberList, memberList
}, },
callback: async (group: any) => { callback: async (group: any) => {
let conversationID: string | null = null; let conversationID: string | null = null;
@ -127,18 +122,18 @@ export default class TUIConversationServer {
conversationID = `GROUP${groupID}`; conversationID = `GROUP${groupID}`;
} }
this.routerForward(conversationID); this.routerForward(conversationID);
}, }
}); });
} }
private async routerForward(conversationID: string | null): Promise<void> { private async routerForward(conversationID: string | null): Promise<void> {
if (isUniFrameWork) { if (isUniFrameWork) {
await TUIGlobal?.reLaunch({ await TUIGlobal?.reLaunch({
url: '/TUIKit/components/TUIConversation/index', url: '/TUIKit/components/TUIConversation/index'
}); });
if (conversationID) { if (conversationID) {
TUIGlobal?.navigateTo({ TUIGlobal?.navigateTo({
url: '/TUIKit/components/TUIChat/index', url: '/TUIKit/components/TUIChat/index'
}); });
} }
} }

View File

@ -6,36 +6,36 @@ const groupIntroConfig = [
label: '陌生人社交群Public', label: '陌生人社交群Public',
type: TUIChatEngine.TYPES.GRP_PUBLIC, type: TUIChatEngine.TYPES.GRP_PUBLIC,
detail: '类似 QQ 群,创建后群主可以指定群管理员,用户搜索群 ID 发起加群申请后,需要群主或管理员审批通过才能入群。详见', detail: '类似 QQ 群,创建后群主可以指定群管理员,用户搜索群 ID 发起加群申请后,需要群主或管理员审批通过才能入群。详见',
src: '产品文档', src: '产品文档'
}, },
{ {
icon: 'https://web.sdk.qcloud.com/im/assets/images/Meeting.svg', icon: 'https://web.sdk.qcloud.com/im/assets/images/Meeting.svg',
label: '临时会议群Meeting', label: '临时会议群Meeting',
type: TUIChatEngine.TYPES.GRP_MEETING, type: TUIChatEngine.TYPES.GRP_MEETING,
detail: '创建后可以随意进出,且支持查看入群前消息;适合用于音视频会议场景、在线教育场景等与实时音视频产品结合的场景。详见', detail: '创建后可以随意进出,且支持查看入群前消息;适合用于音视频会议场景、在线教育场景等与实时音视频产品结合的场景。详见',
src: '产品文档', src: '产品文档'
}, },
{ {
icon: 'https://web.sdk.qcloud.com/im/assets/images/Work.svg', icon: 'https://web.sdk.qcloud.com/im/assets/images/Work.svg',
label: '好友工作群Work', label: '好友工作群Work',
type: TUIChatEngine.TYPES.GRP_WORK, type: TUIChatEngine.TYPES.GRP_WORK,
detail: '类似普通微信群,创建后仅支持已在群内的好友邀请加群,且无需被邀请方同意或群主审批。详见', detail: '类似普通微信群,创建后仅支持已在群内的好友邀请加群,且无需被邀请方同意或群主审批。详见',
src: '产品文档', src: '产品文档'
}, },
{ {
icon: 'https://web.sdk.qcloud.com/im/assets/images/AVChatroom.svg', icon: 'https://web.sdk.qcloud.com/im/assets/images/AVChatroom.svg',
label: '直播群AVChatroom', label: '直播群AVChatroom',
type: TUIChatEngine.TYPES.GRP_AVCHATROOM, type: TUIChatEngine.TYPES.GRP_AVCHATROOM,
detail: '创建后可以随意进出,没有群成员数量上限,但不支持历史消息存储;适合与直播产品结合,用于弹幕聊天场景。详见', detail: '创建后可以随意进出,没有群成员数量上限,但不支持历史消息存储;适合与直播产品结合,用于弹幕聊天场景。详见',
src: '产品文档', src: '产品文档'
}, },
{ {
icon: 'https://web.sdk.qcloud.com/im/assets/images/Community.png', icon: 'https://web.sdk.qcloud.com/im/assets/images/Community.png',
label: '社群Community', label: '社群Community',
type: TUIChatEngine.TYPES.GRP_COMMUNITY, type: TUIChatEngine.TYPES.GRP_COMMUNITY,
detail: '创建后可以随意进出最多支持100000人支持历史消息存储用户搜索群 ID 发起加群申请后,无需管理员审批即可进群。详见', detail: '创建后可以随意进出最多支持100000人支持历史消息存储用户搜索群 ID 发起加群申请后,无需管理员审批即可进群。详见',
src: '产品文档', src: '产品文档'
}, }
]; ];
const findGroupIntroConfig = (type: string) => { const findGroupIntroConfig = (type: string) => {
@ -44,7 +44,4 @@ const findGroupIntroConfig = (type: string) => {
})[0]; })[0];
}; };
export { export { groupIntroConfig, findGroupIntroConfig };
groupIntroConfig,
findGroupIntroConfig,
};

View File

@ -1,34 +1,18 @@
<template> <template>
<ul class="group-introduction-list select"> <ul class="group-introduction-list select">
<li <li v-for="(item, index) in type" :key="index" class="select-item" :class="[selectType === item.type && 'selected']" @click="selected(item)">
v-for="(item, index) in type"
:key="index"
class="select-item"
:class="[selectType === item.type && 'selected']"
@click="selected(item)"
>
<main class="select-item-type"> <main class="select-item-type">
<div class="select-item-header"> <div class="select-item-header">
<aside class="left"> <aside class="left">
<Icon <Icon class="icon" :file="item.icon" />
class="icon"
:file="item.icon"
/>
<span class="select-item-label">{{ TUITranslateService.t(`TUIGroup.${item.label}`) }}</span> <span class="select-item-label">{{ TUITranslateService.t(`TUIGroup.${item.label}`) }}</span>
</aside> </aside>
<Icon <Icon v-if="selectType === item.type" :file="selectedIcon" />
v-if="selectType === item.type"
:file="selectedIcon"
/>
</div> </div>
<span class="select-item-detail">{{ TUITranslateService.t(`TUIGroup.${item.detail}`) }}</span> <span class="select-item-detail">{{ TUITranslateService.t(`TUIGroup.${item.detail}`) }}</span>
<a <a class="link" :href="documentLink.product.url" target="_blank" @click="openUrl(documentLink.product.url)">{{
class="link" TUITranslateService.t(`TUIGroup.${item.src}`)
:href="documentLink.product.url" }}</a>
target="_blank"
@click="openUrl(documentLink.product.url)"
>{{
TUITranslateService.t(`TUIGroup.${item.src}`) }}</a>
</main> </main>
</li> </li>
</ul> </ul>
@ -46,8 +30,8 @@ import { isUniFrameWork } from '../../../../utils/env';
const props = defineProps({ const props = defineProps({
groupType: { groupType: {
type: String, type: String,
default: '', default: ''
}, }
}); });
const type = groupIntroConfig; const type = groupIntroConfig;
@ -70,6 +54,5 @@ const openUrl = (link: string) => {
TUIGlobal?.open(link); TUIGlobal?.open(link);
} }
}; };
</script> </script>
<style lang="scss" scoped src="../style/index.scss"></style> <style lang="scss" scoped src="../style/index.scss"></style>

View File

@ -1,54 +1,23 @@
<template> <template>
<Dialog <Dialog :show="true" :isH5="!isPC" :isHeaderShow="false" :isFooterShow="false" :background="false" @update:show="closeCreated">
:show="true" <div class="group" :class="[!isPC ? 'group-h5' : '']">
:isH5="!isPC"
:isHeaderShow="false"
:isFooterShow="false"
:background="false"
@update:show="closeCreated"
>
<div
class="group"
:class="[!isPC ? 'group-h5' : '']"
>
<div class="group-box"> <div class="group-box">
<header class="group-box-header"> <header class="group-box-header">
<Icon <Icon :file="isPC ? closeIcon : backIcon" class="icon-close" size="16px" @onClick="closeCreated" />
:file="isPC ? closeIcon : backIcon"
class="icon-close"
size="16px"
@onClick="closeCreated"
/>
<h1 class="group-box-header-title"> <h1 class="group-box-header-title">
{{ headerTitle }} {{ headerTitle }}
</h1> </h1>
</header> </header>
<ul <ul v-if="!groupInfo.isEdit" class="group-list">
v-if="!groupInfo.isEdit"
class="group-list"
>
<li class="group-list-item"> <li class="group-list-item">
<label class="group-list-item-label">{{ TUITranslateService.t('TUIGroup.群头像') }}</label> <label class="group-list-item-label">{{ TUITranslateService.t('TUIGroup.群头像') }}</label>
<Avatar :url="groupInfo.profile.avatar" /> <Avatar :url="groupInfo.profile.avatar" />
</li> </li>
<ul> <ul>
<li <li v-for="(item, index) in createInfo" :key="index" class="group-list-item">
v-for="(item, index) in createInfo"
:key="index"
class="group-list-item"
>
<label class="group-list-item-label">{{ item.name }}</label> <label class="group-list-item-label">{{ item.name }}</label>
<input <input v-if="isPC" v-model="groupInfo.profile[item.key]" type="text" :placeholder="item.placeholder" />
v-if="isPC" <span v-else class="group-h5-list-item-content" @click="edit(item.key)">
v-model="groupInfo.profile[item.key]"
type="text"
:placeholder="item.placeholder"
>
<span
v-else
class="group-h5-list-item-content"
@click="edit(item.key)"
>
<p class="content">{{ groupInfo.profile[item.key] }}</p> <p class="content">{{ groupInfo.profile[item.key] }}</p>
<Icon :file="rightIcon" /> <Icon :file="rightIcon" />
</span> </span>
@ -56,30 +25,16 @@
<li class="group-list-introduction"> <li class="group-list-introduction">
<div class="group-list-item"> <div class="group-list-item">
<label class="group-list-item-label">{{ TUITranslateService.t('TUIGroup.群类型') }}</label> <label class="group-list-item-label">{{ TUITranslateService.t('TUIGroup.群类型') }}</label>
<GroupIntroduction <GroupIntroduction v-if="isPC" :groupType="groupInfo.profile.type" @selectType="selected" />
v-if="isPC" <span v-else class="group-h5-list-item-content" @click="edit('type')">
:groupType="groupInfo.profile.type"
@selectType="selected"
/>
<span
v-else
class="group-h5-list-item-content"
@click="edit('type')"
>
<p class="content">{{ groupTypeDetail.label }}</p> <p class="content">{{ groupTypeDetail.label }}</p>
<Icon :file="rightIcon" /> <Icon :file="rightIcon" />
</span> </span>
</div> </div>
<article <article v-if="!isPC" class="group-h5-list-item-introduction">
v-if="!isPC"
class="group-h5-list-item-introduction"
>
<label class="introduction-name">{{ groupTypeDetail.label }}</label> <label class="introduction-name">{{ groupTypeDetail.label }}</label>
<span class="introduction-detail">{{ groupTypeDetail.detail }}</span> <span class="introduction-detail">{{ groupTypeDetail.detail }}</span>
<a <a :href="documentLink.product.url" target="view_window">
:href="documentLink.product.url"
target="view_window"
>
{{ TUITranslateService.t(`TUIGroup.${groupTypeDetail.src}`) }} {{ TUITranslateService.t(`TUIGroup.${groupTypeDetail.src}`) }}
</a> </a>
</article> </article>
@ -87,37 +42,21 @@
</ul> </ul>
</ul> </ul>
<!-- Edit Group Name --> <!-- Edit Group Name -->
<div <div v-else class="group-list group-list-edit">
v-else
class="group-list group-list-edit"
>
<input <input
v-if="groupInfo.groupConfig.type === 'input'" v-if="groupInfo.groupConfig.type === 'input'"
v-model="groupInfo.groupConfig.value" v-model="groupInfo.groupConfig.value"
class="group-name-input" class="group-name-input"
type="text" type="text"
:placeholder="TUITranslateService.t(`TUIGroup.${groupInfo.groupConfig.placeholder}`)" :placeholder="TUITranslateService.t(`TUIGroup.${groupInfo.groupConfig.placeholder}`)"
>
<GroupIntroduction
v-else
class="group-introduction-list"
:groupType="groupInfo.groupConfig.value"
@selectType="selected"
/> />
<GroupIntroduction v-else class="group-introduction-list" :groupType="groupInfo.groupConfig.value" @selectType="selected" />
</div> </div>
<footer class="group-profile-footer"> <footer class="group-profile-footer">
<button <button v-if="isPC && !groupInfo.isEdit" class="btn-default" @click="closeCreated">
v-if="isPC && !groupInfo.isEdit"
class="btn-default"
@click="closeCreated"
>
{{ TUITranslateService.t('TUIGroup.取消') }} {{ TUITranslateService.t('TUIGroup.取消') }}
</button> </button>
<button <button class="btn-submit" :disabled="submitDisabledStatus" @click="submit">
class="btn-submit"
:disabled="submitDisabledStatus"
@click="submit"
>
{{ TUITranslateService.t('TUIGroup.确认') }} {{ TUITranslateService.t('TUIGroup.确认') }}
</button> </button>
</footer> </footer>
@ -126,12 +65,7 @@
</Dialog> </Dialog>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import TUIChatEngine, { import TUIChatEngine, { TUITranslateService, TUIGroupService, TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
TUIGroupService,
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import { computed, reactive, watchEffect } from '../../../adapter-vue'; import { computed, reactive, watchEffect } from '../../../adapter-vue';
import documentLink from '../../../utils/documentLink'; import documentLink from '../../../utils/documentLink';
import { isPC } from '../../../utils/env'; import { isPC } from '../../../utils/env';
@ -159,16 +93,16 @@ const groupInfo = reactive<any>({
notification: '', notification: '',
// joinOption: '', // joinOption: '',
memberList: [], memberList: [],
isSupportTopic: false, isSupportTopic: false
}, },
groupConfig: { groupConfig: {
title: '', title: '',
value: '', value: '',
key: '', key: '',
type: '', type: '',
placeholder: '', placeholder: ''
}, },
isEdit: false, isEdit: false
}); });
watchEffect(() => { watchEffect(() => {
@ -193,16 +127,14 @@ const createInfo = computed(() => {
const groupNameInput = { const groupNameInput = {
name: TUITranslateService.t('TUIGroup.群名称'), name: TUITranslateService.t('TUIGroup.群名称'),
key: 'name', key: 'name',
placeholder: TUITranslateService.t('TUIGroup.请输入群名称'), placeholder: TUITranslateService.t('TUIGroup.请输入群名称')
}; };
const groupIDInput = { const groupIDInput = {
name: `${TUITranslateService.t('TUIGroup.群ID')}(${TUITranslateService.t('TUIGroup.选填')})`, name: `${TUITranslateService.t('TUIGroup.群ID')}(${TUITranslateService.t('TUIGroup.选填')})`,
key: 'groupID', key: 'groupID',
placeholder: TUITranslateService.t('TUIGroup.请输入群ID'), placeholder: TUITranslateService.t('TUIGroup.请输入群ID')
}; };
return groupInfo.profile.type === TUIChatEngine.TYPES.GRP_COMMUNITY return groupInfo.profile.type === TUIChatEngine.TYPES.GRP_COMMUNITY ? [groupNameInput] : [groupNameInput, groupIDInput];
? [groupNameInput]
: [groupNameInput, groupIDInput];
}); });
const submitDisabledStatus = computed(() => { const submitDisabledStatus = computed(() => {
@ -232,18 +164,18 @@ const createGroup = async (options: any) => {
if (type === TUIChatEngine.TYPES.GRP_AVCHATROOM) { if (type === TUIChatEngine.TYPES.GRP_AVCHATROOM) {
await TUIGroupService.joinGroup({ await TUIGroupService.joinGroup({
groupID: res.data.group.groupID, groupID: res.data.group.groupID,
applyMessage: '', applyMessage: ''
}); });
} }
handleCompleteCreate(res.data.group); handleCompleteCreate(res.data.group);
Toast({ Toast({
message: TUITranslateService.t('TUIGroup.群组创建成功'), message: TUITranslateService.t('TUIGroup.群组创建成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
} catch (err: any) { } catch (err: any) {
Toast({ Toast({
message: err.message, message: err.message,
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
} }
}; };
@ -252,7 +184,7 @@ const submit = () => {
const { profile } = groupInfo; const { profile } = groupInfo;
if (groupInfo.isEdit) { if (groupInfo.isEdit) {
groupInfo.profile[groupInfo.groupConfig.key] = groupInfo.groupConfig.value; groupInfo.profile[groupInfo.groupConfig.key] = groupInfo.groupConfig.value;
return groupInfo.isEdit = !groupInfo.isEdit; return (groupInfo.isEdit = !groupInfo.isEdit);
} else { } else {
createGroup(profile); createGroup(profile);
} }
@ -260,7 +192,7 @@ const submit = () => {
const closeCreated = () => { const closeCreated = () => {
if (groupInfo.isEdit) { if (groupInfo.isEdit) {
return groupInfo.isEdit = !groupInfo.isEdit; return (groupInfo.isEdit = !groupInfo.isEdit);
} }
handleCompleteCreate(null); handleCompleteCreate(null);
}; };
@ -292,6 +224,5 @@ const handleCompleteCreate = (group: any) => {
const callback = TUIGroupServer.getOnCallCallback(TUIConstants.TUIGroup.SERVICE.METHOD.CREATE_GROUP); const callback = TUIGroupServer.getOnCallCallback(TUIConstants.TUIGroup.SERVICE.METHOD.CREATE_GROUP);
callback && callback(group); callback && callback(group);
}; };
</script> </script>
<style lang="scss" scoped src="./style/index.scss"></style> <style lang="scss" scoped src="./style/index.scss"></style>

View File

@ -6,10 +6,7 @@
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine';
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../adapter-vue'; import { ref } from '../../adapter-vue';
import CreateGroup from './create-group/index.vue'; import CreateGroup from './create-group/index.vue';
@ -41,9 +38,8 @@ TUIStore.watch(StoreName.GRP, {
} else { } else {
isShowSelectMember.value = false; isShowSelectMember.value = false;
} }
}, }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tui-group { .tui-group {

View File

@ -1,36 +1,16 @@
<template> <template>
<div <div ref="manageRef" class="manage">
ref="manageRef" <header v-if="!isUniFrameWork || currentTab === 'admin'" class="manage-header">
class="manage" <Icon :file="backSVG" @onClick="back" />
>
<header
v-if="!isUniFrameWork || currentTab ==='admin'"
class="manage-header"
>
<Icon
:file="backSVG"
@onClick="back"
/>
<div class="manage-header-content"> <div class="manage-header-content">
{{ TUITranslateService.t(`TUIGroup.${TabName}`) }} {{ TUITranslateService.t(`TUIGroup.${TabName}`) }}
</div> </div>
<div /> <div />
</header> </header>
<main <main v-if="!currentTab || (isUniFrameWork && currentTab != 'admin')" class="main">
v-if="!currentTab || (isUniFrameWork && currentTab != 'admin')" <ManageName class="space-top" :isAuthor="isOwner || isAdmin || isWorkGroup" :data="currentGroup" @update="updateProfile" />
class="main"
>
<ManageName
class="space-top"
:isAuthor="isOwner || isAdmin || isWorkGroup"
:data="currentGroup"
@update="updateProfile"
/>
<div class="user-info space-top"> <div class="user-info space-top">
<header <header class="user-info-header" @click="setCurrentTab('member')">
class="user-info-header"
@click="setCurrentTab('member')"
>
<label class="user-info-header-left"> <label class="user-info-header-left">
{{ TUITranslateService.t(`TUIGroup.群成员`) }} {{ TUITranslateService.t(`TUIGroup.群成员`) }}
</label> </label>
@ -43,143 +23,71 @@
</div> </div>
</header> </header>
<ol class="user-info-list"> <ol class="user-info-list">
<dl <dl v-for="(item, index) in groupMemberList.slice(0, showUserNum)" :key="index" class="user-info-list-item">
v-for="(item, index) in groupMemberList.slice(0, showUserNum)" <dt class="user-info-list-item-main" @click="handleMemberProfileShow(item)">
:key="index"
class="user-info-list-item"
>
<dt
class="user-info-list-item-main"
@click="handleMemberProfileShow(item)"
>
<img <img
class="avatar" class="avatar"
:src=" :src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'" onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
> />
</dt> </dt>
<dd class="user-info-list-item-info"> <dd class="user-info-list-item-info">
{{ item.nick || item.userID }} {{ item.nick || item.userID }}
</dd> </dd>
</dl> </dl>
<dl <dl v-if="isShowAddMember" class="user-info-list-item">
v-if="isShowAddMember" <dt class="avatar" @click="toggleMask('add')">+</dt>
class="user-info-list-item"
>
<dt
class="avatar"
@click="toggleMask('add')"
>
+
</dt>
</dl> </dl>
<dl <dl v-if="currentSelfRole === 'Owner'" class="user-info-list-item">
v-if="currentSelfRole === 'Owner'" <dt class="avatar" @click="toggleMask('remove')">-</dt>
class="user-info-list-item"
>
<dt
class="avatar"
@click="toggleMask('remove')"
>
-
</dt>
</dl> </dl>
</ol> </ol>
</div> </div>
<ul <ul class="content list space-top" @click="editLableName = ''">
class="content list space-top" <li class="list-item" @click="setCurrentTab('notification')">
@click="editLableName = ''"
>
<li
class="list-item"
@click="setCurrentTab('notification')"
>
<aside class="aside"> <aside class="aside">
<label class="label">{{ <label class="label">{{ TUITranslateService.t(`TUIGroup.群公告`) }}</label>
TUITranslateService.t(`TUIGroup.群公告`)
}}</label>
<article class="article"> <article class="article">
{{ currentGroup.notification }} {{ currentGroup.notification }}
</article> </article>
</aside> </aside>
<Icon <Icon :file="rightIcon" class="end" />
:file="rightIcon"
class="end"
/>
</li> </li>
<li <li v-if="(isAdmin || isOwner) && isSetMuteTime" class="list-item" @click="setCurrentTab('admin')">
v-if="(isAdmin || isOwner) && isSetMuteTime" <label class="label">{{ TUITranslateService.t(`TUIGroup.群管理`) }}</label>
class="list-item"
@click="setCurrentTab('admin')"
>
<label class="label">{{
TUITranslateService.t(`TUIGroup.群管理`)
}}</label>
<Icon :file="rightIcon" /> <Icon :file="rightIcon" />
</li> </li>
<li class="list-item"> <li class="list-item">
<label class="label">{{ <label class="label">{{ TUITranslateService.t(`TUIGroup.群ID`) }}</label>
TUITranslateService.t(`TUIGroup.群ID`)
}}</label>
<div class="groupID"> <div class="groupID">
<span class="span">{{ currentGroupID }}</span> <span class="span">{{ currentGroupID }}</span>
</div> </div>
</li> </li>
<li class="list-item"> <li class="list-item">
<label class="label">{{ <label class="label">{{ TUITranslateService.t(`TUIGroup.群头像`) }}</label>
TUITranslateService.t(`TUIGroup.群头像`)
}}</label>
<img <img
class="avatar" class="avatar"
:src=" :src="currentGroup.avatar || 'https://web.sdk.qcloud.com/im/demo/TUIkit/web/img/constomer.svg'"
currentGroup.avatar ||
'https://web.sdk.qcloud.com/im/demo/TUIkit/web/img/constomer.svg'
"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/im/demo/TUIkit/web/img/constomer.svg'" onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/im/demo/TUIkit/web/img/constomer.svg'"
> />
</li> </li>
<li class="list-item"> <li class="list-item">
<label class="label">{{ <label class="label">{{ TUITranslateService.t(`TUIGroup.群类型`) }}</label>
TUITranslateService.t(`TUIGroup.群类型`) <span class="span">{{ TUITranslateService.t(`TUIGroup.${typeName[currentGroup.type]}`) }}</span>
}}</label>
<span class="span">{{
TUITranslateService.t(`TUIGroup.${typeName[currentGroup.type]}`)
}}</span>
</li> </li>
<li class="list-item"> <li class="list-item">
<label class="label">{{ <label class="label">{{ TUITranslateService.t(`TUIGroup.加群方式`) }}</label>
TUITranslateService.t(`TUIGroup.加群方式`) <span class="span">{{ TUITranslateService.t(`TUIGroup.${typeName[currentGroup.joinOption]}`) }}</span>
}}</label>
<span class="span">{{
TUITranslateService.t(
`TUIGroup.${typeName[currentGroup.joinOption]}`
)
}}</span>
</li> </li>
</ul> </ul>
<ul class="footer list space-top"> <ul class="footer list space-top">
<li <li v-if="currentSelfRole === 'Owner' && groupMemberList.length > 1" class="list-item" @click.stop="toggleMask('changeOwner')">
v-if="currentSelfRole === 'Owner' && groupMemberList.length > 1"
class="list-item"
@click.stop="toggleMask('changeOwner')"
>
{{ TUITranslateService.t(`TUIGroup.转让群组`) }} {{ TUITranslateService.t(`TUIGroup.转让群组`) }}
</li> </li>
<li <li v-if="canIDissmissGroup" class="list-item" @click.stop="dismissGroup(currentGroup)">
v-if="canIDissmissGroup"
class="list-item"
@click.stop="dismissGroup(currentGroup)"
>
{{ TUITranslateService.t(`TUIGroup.解散群聊`) }} {{ TUITranslateService.t(`TUIGroup.解散群聊`) }}
</li> </li>
<li <li v-else class="list-item" @click.stop="quitGroup(currentGroup)">
v-else
class="list-item"
@click.stop="quitGroup(currentGroup)"
>
{{ TUITranslateService.t(`TUIGroup.退出群组`) }} {{ TUITranslateService.t(`TUIGroup.退出群组`) }}
</li> </li>
</ul> </ul>
@ -195,11 +103,7 @@
@handleMemberProfileShow="handleMemberProfileShow" @handleMemberProfileShow="handleMemberProfileShow"
@close="setCurrentTab('')" @close="setCurrentTab('')"
/> />
<ManageProfile <ManageProfile v-if="currentTab === 'profile'" :userInfo="currentMember" @close="setCurrentTab('')" />
v-if="currentTab === 'profile'"
:userInfo="currentMember"
@close="setCurrentTab('')"
/>
<ManageNotification <ManageNotification
v-if="currentTab === 'notification'" v-if="currentTab === 'notification'"
:isAuthor="isOwner || isAdmin || isWorkGroup" :isAuthor="isOwner || isAdmin || isWorkGroup"
@ -220,10 +124,7 @@
@removeMute="toggleMask('removeMute')" @removeMute="toggleMask('removeMute')"
@close="setCurrentTab('')" @close="setCurrentTab('')"
/> />
<MaskLayer <MaskLayer :show="mask" @update:show="(e) => (mask = e)">
:show="mask"
@update:show="(e) => (mask = e)"
>
<Transfer <Transfer
:title="TUITranslateService.t(`TUIGroup.${transferTitle}`)" :title="TUITranslateService.t(`TUIGroup.${transferTitle}`)"
:list="transferList" :list="transferList"
@ -246,29 +147,17 @@
@submit="handleManage(deletedUserList, 'remove')" @submit="handleManage(deletedUserList, 'remove')"
@update:show="(e) => (delDialogShow = e)" @update:show="(e) => (delDialogShow = e)"
> >
<p <p v-if="deletedUserList.length === 1" class="del-dialog-title">
v-if="deletedUserList.length === 1"
class="del-dialog-title"
>
{{ TUITranslateService.t(`TUIGroup.确定从群聊中删除该成员?`) }} {{ TUITranslateService.t(`TUIGroup.确定从群聊中删除该成员?`) }}
</p> </p>
<p <p v-if="deletedUserList.length > 1" class="del-dialog-title">
v-if="deletedUserList.length > 1"
class="del-dialog-title"
>
{{ TUITranslateService.t(`TUIGroup.确定从群聊中删除所选成员?`) }} {{ TUITranslateService.t(`TUIGroup.确定从群聊中删除所选成员?`) }}
</p> </p>
</Dialog> </Dialog>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { ref, computed, watchEffect, onMounted, nextTick } from '../../../adapter-vue';
ref,
computed,
watchEffect,
onMounted,
nextTick,
} from '../../../adapter-vue';
import TUIChatEngine, { import TUIChatEngine, {
TUITranslateService, TUITranslateService,
TUIGroupService, TUIGroupService,
@ -277,7 +166,7 @@ import TUIChatEngine, {
StoreName, StoreName,
IGroupModel, IGroupModel,
TUIConversationService, TUIConversationService,
IConversationModel, IConversationModel
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal, outsideClick } from '@tencentcloud/universal-api'; import { TUIGlobal, outsideClick } from '@tencentcloud/universal-api';
import MaskLayer from '../../common/MaskLayer/index.vue'; import MaskLayer from '../../common/MaskLayer/index.vue';
@ -303,12 +192,12 @@ const TUIConstants = TUIGroupServer.constants;
const props = defineProps({ const props = defineProps({
groupID: { groupID: {
type: String, type: String,
default: '', default: ''
}, },
groupCurrentTab: { groupCurrentTab: {
type: String, type: String,
default: '', default: ''
}, }
}); });
const manageRef = ref<any>(undefined); const manageRef = ref<any>(undefined);
@ -319,7 +208,7 @@ const transferType = ref('');
const mask = ref(false); const mask = ref(false);
const currentGroupID = ref(''); const currentGroupID = ref('');
const userInfo = ref({ const userInfo = ref({
list: [] as IGroupMember[], list: [] as IGroupMember[]
}); });
const currentMember = ref<IGroupMember>({}); const currentMember = ref<IGroupMember>({});
const typeName = ref({ const typeName = ref({
@ -330,12 +219,12 @@ const typeName = ref({
[TUIChatEngine.TYPES.GRP_COMMUNITY]: '社群', [TUIChatEngine.TYPES.GRP_COMMUNITY]: '社群',
[TUIChatEngine.TYPES.JOIN_OPTIONS_FREE_ACCESS]: '自由加入', [TUIChatEngine.TYPES.JOIN_OPTIONS_FREE_ACCESS]: '自由加入',
[TUIChatEngine.TYPES.JOIN_OPTIONS_NEED_PERMISSION]: '需要验证', [TUIChatEngine.TYPES.JOIN_OPTIONS_NEED_PERMISSION]: '需要验证',
[TUIChatEngine.TYPES.JOIN_OPTIONS_DISABLE_APPLY]: '禁止加群', [TUIChatEngine.TYPES.JOIN_OPTIONS_DISABLE_APPLY]: '禁止加群'
}); });
const member = ref({ const member = ref({
admin: [] as IGroupMember[], admin: [] as IGroupMember[],
member: [] as IGroupMember[], member: [] as IGroupMember[],
muteMember: [] as IGroupMember[], muteMember: [] as IGroupMember[]
}); });
const transferList = ref<IGroupMember[]>([]); const transferList = ref<IGroupMember[]>([]);
const transferTitle = ref(''); const transferTitle = ref('');
@ -354,7 +243,7 @@ onMounted(() => {
if (manageRef.value && !isUniFrameWork) { if (manageRef.value && !isUniFrameWork) {
outsideClick.listen({ outsideClick.listen({
domRefs: manageRef.value, domRefs: manageRef.value,
handler: handleCompleteManage, handler: handleCompleteManage
}); });
} }
}); });
@ -372,7 +261,7 @@ TUIStore.watch(StoreName.GRP, {
member.value = { member.value = {
admin: [], admin: [],
member: [], member: [],
muteMember: [], muteMember: []
}; };
Array.from(memberList).map((item: any) => { Array.from(memberList).map((item: any) => {
switch (item?.role) { switch (item?.role) {
@ -388,16 +277,14 @@ TUIStore.watch(StoreName.GRP, {
return item; return item;
}); });
const time: number = new Date().getTime(); const time: number = new Date().getTime();
member.value.muteMember = Array.from(memberList).filter( member.value.muteMember = Array.from(memberList).filter((item: any) => item?.muteUntil * 1000 - time > 0);
(item: any) => item?.muteUntil * 1000 - time > 0, }
);
},
}); });
TUIStore.watch(StoreName.CONV, { TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => { currentConversation: (conversation: IConversationModel) => {
groupIDValue.value = conversation?.groupProfile?.groupID; groupIDValue.value = conversation?.groupProfile?.groupID;
}, }
}); });
watchEffect(() => { watchEffect(() => {
@ -460,7 +347,7 @@ const updateProfile = async (newGroupProfile: any) => {
const { key, value } = newGroupProfile; const { key, value } = newGroupProfile;
const options: any = { const options: any = {
groupID: currentGroup.value.groupID, groupID: currentGroup.value.groupID,
[key]: value, [key]: value
}; };
TUIGroupService.updateGroupProfile(options) TUIGroupService.updateGroupProfile(options)
.then((res: any) => { .then((res: any) => {
@ -470,7 +357,7 @@ const updateProfile = async (newGroupProfile: any) => {
.catch((error: any) => { .catch((error: any) => {
Toast({ Toast({
message: error?.message, message: error?.message,
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
}); });
}; };
@ -504,9 +391,7 @@ const toggleMask = async (type?: string) => {
break; break;
case 'remove': case 'remove':
isRadio.value = false; isRadio.value = false;
transferList.value = groupMemberList.value.filter( transferList.value = groupMemberList.value.filter((item: any) => item.userID !== currentGroup?.value?.selfInfo?.userID);
(item: any) => item.userID !== currentGroup?.value?.selfInfo?.userID,
);
transferTitle.value = '删除成员'; transferTitle.value = '删除成员';
break; break;
case 'addAdmin': case 'addAdmin':
@ -550,22 +435,14 @@ const toggleMask = async (type?: string) => {
const friendList = async () => { const friendList = async () => {
const imResponse = await TUIFriendService.getFriendList(); const imResponse = await TUIFriendService.getFriendList();
const friendList = imResponse.data.map((item: any) => item?.profile); const friendList = imResponse.data.map((item: any) => item?.profile);
return friendList.filter( return friendList.filter((item: any) => !userInfo.value.list.some((infoItem: any) => infoItem.userID === item.userID));
(item: any) =>
!userInfo.value.list.some(
(infoItem: any) => infoItem.userID === item.userID,
),
);
}; };
const canIDissmissGroup = computed(() => { const canIDissmissGroup = computed(() => {
const userRole = currentGroup?.value?.selfInfo?.role; const userRole = currentGroup?.value?.selfInfo?.role;
const groupType = currentGroup?.value?.type; const groupType = currentGroup?.value?.type;
return ( return userRole === TUIChatEngine.TYPES.GRP_MBR_ROLE_OWNER && groupType !== TUIChatEngine.TYPES.GRP_WORK;
userRole === TUIChatEngine.TYPES.GRP_MBR_ROLE_OWNER
&& groupType !== TUIChatEngine.TYPES.GRP_WORK
);
}); });
const isShowAddMember = computed(() => { const isShowAddMember = computed(() => {
@ -589,7 +466,7 @@ const getMember = async (type?: string) => {
const options = { const options = {
groupID, groupID,
count: 100, count: 100,
offset: type && type === 'more' ? userInfo.value.list.length : 0, offset: type && type === 'more' ? userInfo.value.list.length : 0
}; };
await TUIGroupService.getGroupMemberList(options).then((res: any) => { await TUIGroupService.getGroupMemberList(options).then((res: any) => {
if (type && type === 'more') { if (type && type === 'more') {
@ -620,7 +497,7 @@ const dismissGroup = async (group: any) => {
enableSampleTaskStatus('dismissGroup'); enableSampleTaskStatus('dismissGroup');
Toast({ Toast({
message: TUITranslateService.t('TUIGroup.群组解散成功'), message: TUITranslateService.t('TUIGroup.群组解散成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
clearGroupInfo(); clearGroupInfo();
}; };
@ -628,7 +505,7 @@ const dismissGroup = async (group: any) => {
const clearGroupInfo = () => { const clearGroupInfo = () => {
if (isUniFrameWork) { if (isUniFrameWork) {
TUIGlobal?.switchTab({ TUIGlobal?.switchTab({
url: '/TUIKit/components/TUIConversation/index', url: '/TUIKit/components/TUIConversation/index'
}); });
} else { } else {
handleCompleteManage(); handleCompleteManage();
@ -643,12 +520,12 @@ const setAllMuteTime = (value: boolean) => {
enableSampleTaskStatus('muteGroup'); enableSampleTaskStatus('muteGroup');
Toast({ Toast({
message: TUITranslateService.t('TUIGroup.禁言设置成功'), message: TUITranslateService.t('TUIGroup.禁言设置成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
} else { } else {
Toast({ Toast({
message: TUITranslateService.t('TUIGroup.取消禁言成功'), message: TUITranslateService.t('TUIGroup.取消禁言成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
} }
}; };
@ -658,15 +535,13 @@ const handleSearchMember = async (value: string) => {
let imMemberResponse: any = {}; let imMemberResponse: any = {};
const options: any = { const options: any = {
groupID: currentGroupID.value, groupID: currentGroupID.value,
userIDList: [value], userIDList: [value]
}; };
switch (transferType.value) { switch (transferType.value) {
case 'add': case 'add':
try { try {
imMemberResponse = await TUIGroupService.getGroupMemberProfile(options); imMemberResponse = await TUIGroupService.getGroupMemberProfile(options);
transferList.value = transferList.value.filter( transferList.value = transferList.value.filter((item: any) => item.userID !== imResponse.data[0]?.userID);
(item: any) => item.userID !== imResponse.data[0]?.userID,
);
transferList.value = [...transferList.value, ...imResponse.data]; transferList.value = [...transferList.value, ...imResponse.data];
if (imMemberResponse?.data?.memberList.length > 0) { if (imMemberResponse?.data?.memberList.length > 0) {
transferList.value = transferList.value.map((item: any) => { transferList.value = transferList.value.map((item: any) => {
@ -680,7 +555,7 @@ const handleSearchMember = async (value: string) => {
const message = TUITranslateService.t('TUIGroup.该用户不存在'); const message = TUITranslateService.t('TUIGroup.该用户不存在');
Toast({ Toast({
message, message,
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
} }
break; break;
@ -691,23 +566,18 @@ const handleSearchMember = async (value: string) => {
const message = TUITranslateService.t('TUIGroup.该用户不在群组内'); const message = TUITranslateService.t('TUIGroup.该用户不在群组内');
Toast({ Toast({
message, message,
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
} }
transferList.value = transferList.value.filter( transferList.value = transferList.value.filter((item: any) => item.userID !== imResponse?.data?.memberList[0]?.userID);
(item: any) => item.userID !== imResponse?.data?.memberList[0]?.userID,
);
if (imResponse?.data?.memberList.length) { if (imResponse?.data?.memberList.length) {
transferList.value = [ transferList.value = [...transferList.value, ...imResponse.data.memberList];
...transferList.value,
...imResponse.data.memberList,
];
} }
} catch (error: any) { } catch (error: any) {
const message = TUITranslateService.t('TUIGroup.该用户不存在'); const message = TUITranslateService.t('TUIGroup.该用户不存在');
Toast({ Toast({
message, message,
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
} }
break; break;
@ -752,7 +622,7 @@ const handleManage = (userList: any, type: any) => {
const addMember = async (userIDList: any) => { const addMember = async (userIDList: any) => {
const options: any = { const options: any = {
groupID: currentGroupID.value, groupID: currentGroupID.value,
userIDList, userIDList
}; };
await TUIGroupService.addGroupMember(options); await TUIGroupService.addGroupMember(options);
}; };
@ -760,7 +630,7 @@ const addMember = async (userIDList: any) => {
const changeOwner = async (userID: any) => { const changeOwner = async (userID: any) => {
const options: any = { const options: any = {
groupID: currentGroupID.value, groupID: currentGroupID.value,
newOwnerID: userID, newOwnerID: userID
}; };
const imResponse = await TUIGroupService.changeGroupOwner(options); const imResponse = await TUIGroupService.changeGroupOwner(options);
currentGroup.value = {}; currentGroup.value = {};
@ -771,7 +641,7 @@ const setMemberMuteTime = async (userID: string, type?: string) => {
const options: any = { const options: any = {
groupID: currentGroupID.value, groupID: currentGroupID.value,
userID, userID,
muteTime: type === 'add' ? 60 * 60 * 24 * 30 : 0, muteTime: type === 'add' ? 60 * 60 * 24 * 30 : 0
}; };
await TUIGroupService.setGroupMemberMuteTime(options); await TUIGroupService.setGroupMemberMuteTime(options);
}; };
@ -789,7 +659,7 @@ const handleAdmin = async (user: any) => {
const options: any = { const options: any = {
groupID: currentGroupID.value, groupID: currentGroupID.value,
userID: user.userID, userID: user.userID,
role, role
}; };
await TUIGroupService.setGroupMemberRole(options); await TUIGroupService.setGroupMemberRole(options);
}; };
@ -798,7 +668,7 @@ const deleteGroupMember = async (userIDList: any) => {
const options: any = { const options: any = {
groupID: currentGroupID.value, groupID: currentGroupID.value,
userIDList, userIDList,
reason: '', reason: ''
}; };
await TUIGroupService.deleteGroupMember(options); await TUIGroupService.deleteGroupMember(options);
}; };

View File

@ -5,123 +5,66 @@
{{ TUITranslateService.t(`TUIGroup.群管理员`) }} {{ TUITranslateService.t(`TUIGroup.群管理员`) }}
</div> </div>
<ul class="admin-manage-list"> <ul class="admin-manage-list">
<li <li v-for="(item, index) in memberAdmin.admin" :key="index" class="admin-manage-list-item">
v-for="(item, index) in memberAdmin.admin"
:key="index"
class="admin-manage-list-item"
>
<div class="item-main"> <div class="item-main">
<img <img
class="item-main-avatar" class="item-main-avatar"
:src=" :src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'" onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
> />
</div> </div>
<div class="item-name"> <div class="item-name">
{{ item.nick || item.userID }} {{ item.nick || item.userID }}
</div> </div>
</li> </li>
<li class="admin-manage-list-item"> <li class="admin-manage-list-item">
<div <div class="item-main" @click="addAdmin">
class="item-main" <Icon :file="plusSVG" width="16px" height="16px" />
@click="addAdmin"
>
<Icon
:file="plusSVG"
width="16px"
height="16px"
/>
</div> </div>
</li> </li>
<li class="admin-manage-list-item"> <li class="admin-manage-list-item">
<div <div v-if="memberAdmin.admin.length > 0" class="item-main" @click="removeAdmin">
v-if="memberAdmin.admin.length > 0" <Icon :file="minusSVG" width="16px" height="16px" />
class="item-main"
@click="removeAdmin"
>
<Icon
:file="minusSVG"
width="16px"
height="16px"
/>
</div> </div>
</li> </li>
</ul> </ul>
</div> </div>
<div <div v-if="isAdminSetMuteTime" class="admin-mute-all">
v-if="isAdminSetMuteTime"
class="admin-mute-all"
>
<div> <div>
<div class="admin-mute-all-title"> <div class="admin-mute-all-title">
{{ TUITranslateService.t(`TUIGroup.全员禁言`) }} {{ TUITranslateService.t(`TUIGroup.全员禁言`) }}
</div> </div>
<div class="admin-mute-all-content"> <div class="admin-mute-all-content">
{{ {{ TUITranslateService.t(`TUIGroup.全员禁言开启后,只允许群主和管理员发言。`) }}
TUITranslateService.t(
`TUIGroup.全员禁言开启后,只允许群主和管理员发言。`
)
}}
</div> </div>
</div> </div>
<Slider <Slider :open="currentGroupAdmin.muteAllMembers" @change="setAllMuteTime" />
:open="currentGroupAdmin.muteAllMembers"
@change="setAllMuteTime"
/>
</div> </div>
<div <div v-if="isAdminSetMuteTime" class="admin-mute">
v-if="isAdminSetMuteTime"
class="admin-mute"
>
<div class="admin-mute-header"> <div class="admin-mute-header">
{{ TUITranslateService.t(`TUIGroup.单独禁言人员`) }} {{ TUITranslateService.t(`TUIGroup.单独禁言人员`) }}
</div> </div>
<ul class="admin-mute-list"> <ul class="admin-mute-list">
<li <li v-for="(item, index) in memberAdmin.muteMember" :key="index" class="admin-mute-list-item">
v-for="(item, index) in memberAdmin.muteMember"
:key="index"
class="admin-mute-list-item"
>
<div class="item-main"> <div class="item-main">
<img <img
class="item-main-avatar" class="item-main-avatar"
:src=" :src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'" onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
> />
</div> </div>
<div class="item-name"> <div class="item-name">
{{ item.nick || item.userID }} {{ item.nick || item.userID }}
</div> </div>
</li> </li>
<li class="admin-mute-list-item"> <li class="admin-mute-list-item">
<div <div class="item-main" @click="addMute">
class="item-main" <Icon :file="plusSVG" width="16px" height="16px" />
@click="addMute"
>
<Icon
:file="plusSVG"
width="16px"
height="16px"
/>
</div> </div>
</li> </li>
<li class="admin-mute-list-item"> <li class="admin-mute-list-item">
<div <div v-if="memberAdmin.muteMember.length > 0" class="item-main" @click="removeMute">
v-if="memberAdmin.muteMember.length > 0" <Icon :file="minusSVG" width="16px" height="16px" />
class="item-main"
@click="removeMute"
>
<Icon
:file="minusSVG"
width="16px"
height="16px"
/>
</div> </div>
</li> </li>
</ul> </ul>
@ -130,10 +73,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { TUITranslateService, IGroupModel } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
IGroupModel,
} from '@tencentcloud/chat-uikit-engine';
import { watchEffect, ref } from '../../../adapter-vue'; import { watchEffect, ref } from '../../../adapter-vue';
import Slider from '../../common/Slider/index.vue'; import Slider from '../../common/Slider/index.vue';
import Icon from '../../common/Icon.vue'; import Icon from '../../common/Icon.vue';
@ -144,23 +84,23 @@ import { IGroupMember } from '../../../interface';
const props = defineProps({ const props = defineProps({
member: { member: {
type: Object, type: Object,
default: () => {}, default: () => {}
}, },
isSetMuteTime: { isSetMuteTime: {
type: Boolean, type: Boolean,
default: () => false, default: () => false
}, },
currentGroup: { currentGroup: {
type: Object, type: Object,
default: () => {}, default: () => {}
}, }
}); });
const isAdminSetMuteTime = ref(false); const isAdminSetMuteTime = ref(false);
const memberAdmin = ref({ const memberAdmin = ref({
admin: [] as Array<IGroupMember>, admin: [] as Array<IGroupMember>,
member: [] as Array<IGroupMember>, member: [] as Array<IGroupMember>,
muteMember: [] as Array<IGroupMember>, muteMember: [] as Array<IGroupMember>
}); });
const currentGroupAdmin = ref<IGroupModel>(); const currentGroupAdmin = ref<IGroupModel>();
@ -174,14 +114,7 @@ watchEffect(() => {
currentGroupAdmin.value = props.currentGroup; currentGroupAdmin.value = props.currentGroup;
}); });
const emits = defineEmits([ const emits = defineEmits(['addAdmin', 'removeAdmin', 'setAllMuteTime', 'addMute', 'removeMute', 'close']);
'addAdmin',
'removeAdmin',
'setAllMuteTime',
'addMute',
'removeMute',
'close',
]);
const addAdmin = () => { const addAdmin = () => {
emits('addAdmin'); emits('addAdmin');
@ -205,7 +138,7 @@ const removeMute = () => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../assets/styles/common"; @import '../../../assets/styles/common';
.admin { .admin {
width: 100%; width: 100%;
@ -218,7 +151,7 @@ const removeMute = () => {
padding: 10px; padding: 10px;
&-left { &-left {
font-family: "PingFang SC", sans-serif; font-family: 'PingFang SC', sans-serif;
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 500;
line-height: 50px; line-height: 50px;
@ -227,7 +160,7 @@ const removeMute = () => {
} }
&-close { &-close {
font-family: "PingFang SC", sans-serif; font-family: 'PingFang SC', sans-serif;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 48px; line-height: 48px;
@ -253,7 +186,7 @@ const removeMute = () => {
&-header { &-header {
padding-left: 10px; padding-left: 10px;
font-family: "PingFang SC", sans-serif; font-family: 'PingFang SC', sans-serif;
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 20px; line-height: 20px;
@ -314,7 +247,7 @@ const removeMute = () => {
&-title { &-title {
padding-left: 10px; padding-left: 10px;
font-family: "PingFang SC", sans-serif; font-family: 'PingFang SC', sans-serif;
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 20px; line-height: 20px;
@ -325,7 +258,7 @@ const removeMute = () => {
&-content { &-content {
color: #999; color: #999;
padding-left: 10px; padding-left: 10px;
font-family: "PingFang SC", sans-serif; font-family: 'PingFang SC', sans-serif;
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
line-height: 17px; line-height: 17px;

View File

@ -1,99 +1,50 @@
<template> <template>
<main <main v-if="!isUniFrameWork" class="member">
v-if="!isUniFrameWork"
class="member"
>
<ul class="list"> <ul class="list">
<li <li v-for="(item, index) in memberList" :key="index" class="list-item">
v-for="(item, index) in memberList" <aside class="aside" @click="handleMemberProfileShow(item)">
:key="index"
class="list-item"
>
<aside
class="aside"
@click="handleMemberProfileShow(item)"
>
<img <img
class="avatar" class="avatar"
:src=" :src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'" onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
> />
<span class="name">{{ item.nick || item.userID }}</span> <span class="name">{{ item.nick || item.userID }}</span>
<span>{{ handleRoleName(item) }}</span> <span>{{ handleRoleName(item) }}</span>
</aside> </aside>
<div @click="submit(item)"> <div @click="submit(item)">
<Icon <Icon v-if="item.role !== 'Owner' && isShowDeleteBtn" :file="delIcon" :width="'16px'" :height="'16px'" />
v-if="item.role !== 'Owner' && isShowDeleteBtn"
:file="delIcon"
:width="'16px'"
:height="'16px'"
/>
</div> </div>
</li> </li>
<li <li v-if="memberList.length < totalMember" class="list-item" @click="getMore">
v-if="memberList.length < totalMember"
class="list-item"
@click="getMore"
>
{{ TUITranslateService.t(`TUIGroup.查看更多`) }} {{ TUITranslateService.t(`TUIGroup.查看更多`) }}
</li> </li>
</ul> </ul>
</main> </main>
<div <div v-else class="edit-h5">
v-else
class="edit-h5"
>
<main class="main"> <main class="main">
<header class="edit-h5-header"> <header class="edit-h5-header">
<aside class="left"> <aside class="left">
<h1>{{ TUITranslateService.t(`TUIGroup.群成员`) }}</h1> <h1>{{ TUITranslateService.t(`TUIGroup.群成员`) }}</h1>
</aside> </aside>
<span <span class="close" @click="close('member')">{{ TUITranslateService.t(`关闭`) }}</span>
class="close"
@click="close('member')"
>{{
TUITranslateService.t(`关闭`)
}}</span>
</header> </header>
<div class="member"> <div class="member">
<ul class="list list-uniapp"> <ul class="list list-uniapp">
<li <li v-for="(item, index) in memberList" :key="index" class="list-item">
v-for="(item, index) in memberList" <aside class="aside" @click="handleMemberProfileShow(item)">
:key="index"
class="list-item"
>
<aside
class="aside"
@click="handleMemberProfileShow(item)"
>
<img <img
class="avatar" class="avatar"
:src=" :src="item.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'" onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
> />
<span class="name">{{ item.nick || item.userID }}</span> <span class="name">{{ item.nick || item.userID }}</span>
<span>{{ handleRoleName(item) }}</span> <span>{{ handleRoleName(item) }}</span>
</aside> </aside>
<div @click="submit(item)"> <div @click="submit(item)">
<Icon <Icon v-if="item.role !== 'Owner' && isShowDeleteBtn" :file="delIcon" :width="'16px'" :height="'16px'" />
v-if="item.role !== 'Owner' && isShowDeleteBtn"
:file="delIcon"
:width="'16px'"
:height="'16px'"
/>
</div> </div>
</li> </li>
<li <li v-if="memberList.length < totalMember" class="list-item" @click="getMore">
v-if="memberList.length < totalMember"
class="list-item"
@click="getMore"
>
{{ TUITranslateService.t(`TUIGroup.查看更多`) }} {{ TUITranslateService.t(`TUIGroup.查看更多`) }}
</li> </li>
</ul> </ul>
@ -103,9 +54,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import TUIChatEngine, { import TUIChatEngine, { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine';
import { watchEffect, ref } from '../../../adapter-vue'; import { watchEffect, ref } from '../../../adapter-vue';
import Icon from '../../common/Icon.vue'; import Icon from '../../common/Icon.vue';
import delIcon from '../../../assets/icon/del-icon.svg'; import delIcon from '../../../assets/icon/del-icon.svg';
@ -115,20 +64,20 @@ import { isUniFrameWork } from '../../../utils/env';
const props = defineProps({ const props = defineProps({
list: { list: {
type: Array, type: Array,
default: () => [], default: () => []
}, },
total: { total: {
type: Number, type: Number,
default: () => 0, default: () => 0
}, },
isShowDel: { isShowDel: {
type: Boolean, type: Boolean,
default: () => false, default: () => false
}, },
self: { self: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, }
}); });
const totalMember = ref(0); const totalMember = ref(0);
@ -182,7 +131,7 @@ const close = (tabName: string) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../assets/styles/common"; @import '../../../assets/styles/common';
.member { .member {
flex: 1; flex: 1;

View File

@ -4,83 +4,38 @@
<div <div
v-if="isEdit" v-if="isEdit"
:class="{ :class="{
'edit-h5': isMobile, 'edit-h5': isMobile
}" }"
> >
<main class="edit-h5-main"> <main class="edit-h5-main">
<header <header v-if="!isPC" class="edit-h5-header">
v-if="!isPC"
class="edit-h5-header"
>
<aside class="left"> <aside class="left">
<h1>{{ TUITranslateService.t(`TUIGroup.修改群聊名称`) }}</h1> <h1>{{ TUITranslateService.t(`TUIGroup.修改群聊名称`) }}</h1>
<span>{{ <span>{{ TUITranslateService.t(`TUIGroup.修改群聊名称后,将在群内通知其他成员`) }}</span>
TUITranslateService.t(
`TUIGroup.修改群聊名称后,将在群内通知其他成员`
)
}}</span>
</aside> </aside>
<span <span class="close" @click="toggleEditStatus">{{ TUITranslateService.t(`关闭`) }}</span>
class="close"
@click="toggleEditStatus"
>{{
TUITranslateService.t(`关闭`)
}}</span>
</header> </header>
<div class="input-box"> <div class="input-box">
<input <input v-if="isEdit" ref="nameInputRef" v-model="inputGroupName" class="input" type="text" @blur="updateProfile" />
v-if="isEdit" <span v-if="!isPC" class="tip">{{ TUITranslateService.t(`TUIGroup.仅限中文字母数字和下划线2-20个字`) }}</span>
ref="nameInputRef"
v-model="inputGroupName"
class="input"
type="text"
@blur="updateProfile"
>
<span
v-if="!isPC"
class="tip"
>{{
TUITranslateService.t(
`TUIGroup.仅限中文、字母、数字和下划线2-20个字`
)
}}</span>
</div> </div>
<footer <footer v-if="!isPC" class="edit-h5-footer">
v-if="!isPC" <button class="btn" @click="updateProfile">
class="edit-h5-footer"
>
<button
class="btn"
@click="updateProfile"
>
{{ TUITranslateService.t(`确认`) }} {{ TUITranslateService.t(`确认`) }}
</button> </button>
</footer> </footer>
</main> </main>
</div> </div>
<p <p v-if="!isEdit || !isPC" class="name" @click="toggleEditStatus">
v-if="!isEdit || !isPC"
class="name"
@click="toggleEditStatus"
>
<span>{{ groupProfile.name }}</span> <span>{{ groupProfile.name }}</span>
<Icon <Icon v-if="isAuthor" class="icon" :file="editIcon" width="14px" height="14px" />
v-if="isAuthor"
class="icon"
:file="editIcon"
width="14px"
height="14px"
/>
</p> </p>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watchEffect, ref, nextTick, watch } from '../../../adapter-vue'; import { watchEffect, ref, nextTick, watch } from '../../../adapter-vue';
import { import { TUITranslateService, IGroupModel } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
IGroupModel,
} from '@tencentcloud/chat-uikit-engine';
import Icon from '../../common/Icon.vue'; import Icon from '../../common/Icon.vue';
import editIcon from '../../../assets/icon/edit.svg'; import editIcon from '../../../assets/icon/edit.svg';
import { Toast, TOAST_TYPE } from '../../common/Toast/index'; import { Toast, TOAST_TYPE } from '../../common/Toast/index';
@ -89,12 +44,12 @@ import { isMobile, isPC } from '../../../utils/env';
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, },
isAuthor: { isAuthor: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}); });
const groupProfile = ref<IGroupModel>({}); const groupProfile = ref<IGroupModel>({});
@ -111,7 +66,7 @@ const updateProfile = () => {
if (!inputGroupName.value) { if (!inputGroupName.value) {
Toast({ Toast({
message: TUITranslateService.t('TUIGroup.群名称不能为空'), message: TUITranslateService.t('TUIGroup.群名称不能为空'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
} else { } else {
if (inputGroupName.value !== groupProfile.value.name) { if (inputGroupName.value !== groupProfile.value.name) {
@ -120,7 +75,7 @@ const updateProfile = () => {
inputGroupName.value = ''; inputGroupName.value = '';
Toast({ Toast({
message: TUITranslateService.t('TUIGroup.群名称修改成功'), message: TUITranslateService.t('TUIGroup.群名称修改成功'),
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS
}); });
} }
toggleEditStatus(); toggleEditStatus();
@ -144,12 +99,12 @@ watch(
nameInputRef.value?.focus(); nameInputRef.value?.focus();
}); });
} }
}, }
); );
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../assets/styles/common"; @import '../../../assets/styles/common';
.group-name { .group-name {
padding: 14px 20px; padding: 14px 20px;

View File

@ -1,14 +1,6 @@
<template> <template>
<main <main v-if="!isUniFrameWork" class="notification">
v-if="!isUniFrameWork" <textarea v-if="isEdit" v-model="input" class="textarea" @keyup.enter="updateProfile" />
class="notification"
>
<textarea
v-if="isEdit"
v-model="input"
class="textarea"
@keyup.enter="updateProfile"
/>
<section v-else> <section v-else>
<p v-if="!groupProfile.notification"> <p v-if="!groupProfile.notification">
{{ TUITranslateService.t(`TUIGroup.暂无公告`) }} {{ TUITranslateService.t(`TUIGroup.暂无公告`) }}
@ -18,75 +10,37 @@
</article> </article>
</section> </section>
<footer v-if="isAuthorNotification"> <footer v-if="isAuthorNotification">
<button <button v-if="isEdit" class="btn" @click="updateProfile">
v-if="isEdit"
class="btn"
@click="updateProfile"
>
{{ TUITranslateService.t(`TUIGroup.发布`) }} {{ TUITranslateService.t(`TUIGroup.发布`) }}
</button> </button>
<button <button v-else class="btn" @click="isEdit = !isEdit">
v-else
class="btn"
@click="isEdit = !isEdit"
>
{{ TUITranslateService.t(`TUIGroup.编辑`) }} {{ TUITranslateService.t(`TUIGroup.编辑`) }}
</button> </button>
</footer> </footer>
</main> </main>
<div <div v-else class="edit-h5">
v-else
class="edit-h5"
>
<main class="edit-h5-main"> <main class="edit-h5-main">
<header class="edit-h5-header"> <header class="edit-h5-header">
<aside class="left"> <aside class="left">
<h1>{{ TUITranslateService.t(`TUIGroup.群公告`) }}</h1> <h1>{{ TUITranslateService.t(`TUIGroup.群公告`) }}</h1>
</aside> </aside>
<span <span class="close" @click="close('notification')">{{ TUITranslateService.t(`关闭`) }}</span>
class="close"
@click="close('notification')"
>{{
TUITranslateService.t(`关闭`)
}}</span>
</header> </header>
<div class="notification"> <div class="notification">
<textarea <textarea v-if="isEdit" v-model="input" :class="[isUniFrameWork ? 'uni-height' : '', 'textarea']" @keyup.enter="updateProfile" />
v-if="isEdit" <section v-else class="row">
v-model="input" <p v-if="!groupProfile.notification" class="row-p">
:class="[isUniFrameWork ? 'uni-height' : '', 'textarea']"
@keyup.enter="updateProfile"
/>
<section
v-else
class="row"
>
<p
v-if="!groupProfile.notification"
class="row-p"
>
{{ TUITranslateService.t(`TUIGroup.暂无公告`) }} {{ TUITranslateService.t(`TUIGroup.暂无公告`) }}
</p> </p>
<article v-else> <article v-else>
{{ groupProfile.notification }} {{ groupProfile.notification }}
</article> </article>
</section> </section>
<footer <footer v-if="isAuthorNotification" class="footer">
v-if="isAuthorNotification" <button v-if="isEdit" class="btn" @click="updateProfile">
class="footer"
>
<button
v-if="isEdit"
class="btn"
@click="updateProfile"
>
{{ TUITranslateService.t(`TUIGroup.发布`) }} {{ TUITranslateService.t(`TUIGroup.发布`) }}
</button> </button>
<button <button v-else class="btn" @click="isEdit = !isEdit">
v-else
class="btn"
@click="isEdit = !isEdit"
>
{{ TUITranslateService.t(`TUIGroup.编辑`) }} {{ TUITranslateService.t(`TUIGroup.编辑`) }}
</button> </button>
</footer> </footer>
@ -97,10 +51,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick } from '../../../adapter-vue'; import { nextTick } from '../../../adapter-vue';
import { import { TUITranslateService, IGroupModel } from '@tencentcloud/chat-uikit-engine';
TUITranslateService,
IGroupModel,
} from '@tencentcloud/chat-uikit-engine';
import { watchEffect, ref } from '../../../adapter-vue'; import { watchEffect, ref } from '../../../adapter-vue';
import { Toast, TOAST_TYPE } from '../../common/Toast/index'; import { Toast, TOAST_TYPE } from '../../common/Toast/index';
import { isUniFrameWork } from '../../../utils/env'; import { isUniFrameWork } from '../../../utils/env';
@ -108,12 +59,12 @@ import { isUniFrameWork } from '../../../utils/env';
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, },
isAuthor: { isAuthor: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}); });
const groupProfile = ref<IGroupModel>({}); const groupProfile = ref<IGroupModel>({});
@ -133,7 +84,7 @@ const updateProfile = () => {
if (input.value.length > 150) { if (input.value.length > 150) {
Toast({ Toast({
message: TUITranslateService.t('TUIGroup.群公告字数超出限制最大长度为150'), message: TUITranslateService.t('TUIGroup.群公告字数超出限制最大长度为150'),
type: TOAST_TYPE.ERROR, type: TOAST_TYPE.ERROR
}); });
return; return;
} }
@ -152,7 +103,7 @@ const close = (tabName: string) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../assets/styles/common"; @import '../../../assets/styles/common';
.notification { .notification {
flex: 1; flex: 1;

View File

@ -1,17 +1,11 @@
<template> <template>
<div <div v-if="!isUniFrameWork" class="memeber-profile">
v-if="!isUniFrameWork"
class="memeber-profile"
>
<div class="memeber-profile-main"> <div class="memeber-profile-main">
<img <img
class="avatar" class="avatar"
:src=" :src="userInfoManager.avatar || 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
userInfoManager.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
"
onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'" onerror="this.onerror=null;this.src='https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'"
> />
<ul class="list"> <ul class="list">
<h2>{{ userInfoManager.nick || userInfoManager.userID }}</h2> <h2>{{ userInfoManager.nick || userInfoManager.userID }}</h2>
<li> <li>
@ -19,44 +13,28 @@
<span>{{ userInfoManager.userID }}</span> <span>{{ userInfoManager.userID }}</span>
</li> </li>
<li> <li>
<label>{{ TUITranslateService.t("TUIContact.个性签名") }}</label> <label>{{ TUITranslateService.t('TUIContact.个性签名') }}</label>
<span>{{ userInfoManager.selfSignature }}</span> <span>{{ userInfoManager.selfSignature }}</span>
</li> </li>
</ul> </ul>
</div> </div>
<div class="memeber-profile-footer"> <div class="memeber-profile-footer">
<div <div v-if="showEnter()" class="button" @click="enter(userInfoManager.userID, 'C2C')">
v-if="showEnter()" {{ TUITranslateService.t('TUIContact.发送消息') }}
class="button"
@click="enter(userInfoManager.userID, 'C2C')"
>
{{ TUITranslateService.t("TUIContact.发送消息") }}
</div> </div>
</div> </div>
</div> </div>
<div <div v-else class="edit-h5">
v-else
class="edit-h5"
>
<main class="main"> <main class="main">
<header class="edit-h5-header"> <header class="edit-h5-header">
<aside class="left"> <aside class="left">
<h1>{{ TUITranslateService.t(`TUIGroup.群成员`) }}</h1> <h1>{{ TUITranslateService.t(`TUIGroup.群成员`) }}</h1>
</aside> </aside>
<span <span class="close" @click="close('profile')">{{ TUITranslateService.t(`关闭`) }}</span>
class="close"
@click="close('profile')"
>{{
TUITranslateService.t(`关闭`)
}}</span>
</header> </header>
<div class="edit-h5-profile"> <div class="edit-h5-profile">
<div class="memeber-profile-main"> <div class="memeber-profile-main">
<Avatar <Avatar class="avatar" :url="userInfoManager.avatar" size="60px" />
class="avatar"
:url="userInfoManager.avatar"
size="60px"
/>
<ul class="list"> <ul class="list">
<h1>{{ userInfoManager.nick || userInfoManager.userID }}</h1> <h1>{{ userInfoManager.nick || userInfoManager.userID }}</h1>
<li> <li>
@ -64,18 +42,14 @@
<span>{{ userInfoManager.userID }}</span> <span>{{ userInfoManager.userID }}</span>
</li> </li>
<li> <li>
<label>{{ TUITranslateService.t("TUIContact.个性签名") }}</label> <label>{{ TUITranslateService.t('TUIContact.个性签名') }}</label>
<span>{{ userInfoManager.selfSignature }}</span> <span>{{ userInfoManager.selfSignature }}</span>
</li> </li>
</ul> </ul>
</div> </div>
<div class="memeber-profile-footer"> <div class="memeber-profile-footer">
<div <div v-if="showEnter()" class="button" @click="enter(userInfoManager.userID, 'C2C')">
v-if="showEnter()" {{ TUITranslateService.t('TUIContact.发送消息') }}
class="button"
@click="enter(userInfoManager.userID, 'C2C')"
>
{{ TUITranslateService.t("TUIContact.发送消息") }}
</div> </div>
</div> </div>
</div> </div>
@ -90,7 +64,7 @@ import TUIChatEngine, {
TUIConversationService, TUIConversationService,
TUIFriendService, TUIFriendService,
TUIStore, TUIStore,
StoreName, StoreName
} from '@tencentcloud/chat-uikit-engine'; } from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api'; import { TUIGlobal } from '@tencentcloud/universal-api';
import Avatar from '../../common/Avatar/index.vue'; import Avatar from '../../common/Avatar/index.vue';
@ -100,8 +74,8 @@ import { isUniFrameWork } from '../../../utils/env';
const props = defineProps({ const props = defineProps({
userInfo: { userInfo: {
type: Object, type: Object,
default: () => ({}), default: () => ({})
}, }
}); });
const isFriendShip = ref(false); const isFriendShip = ref(false);
@ -110,26 +84,22 @@ const userInfoManager = ref<IUserProfile>({});
watchEffect(() => { watchEffect(() => {
userInfoManager.value = props.userInfo; userInfoManager.value = props.userInfo;
}); });
const emits = defineEmits([ const emits = defineEmits(['handleSwitchConversation', 'close', 'openConversation']);
'handleSwitchConversation',
'close',
'openConversation',
]);
watch( watch(
() => props.userInfo, () => props.userInfo,
async (newVal: any, oldVal: any) => { async (newVal: any, oldVal: any) => {
if (newVal === oldVal) return; if (newVal === oldVal) return;
const res = await TUIUserService.getUserProfile({ const res = await TUIUserService.getUserProfile({
userIDList: [props.userInfo.userID], userIDList: [props.userInfo.userID]
}); });
userInfoManager.value = res?.data[0]; userInfoManager.value = res?.data[0];
checkFriend(); checkFriend();
}, },
{ {
deep: true, deep: true,
immediate: true, immediate: true
}, }
); );
const enter = async (ID: any, type: string) => { const enter = async (ID: any, type: string) => {
@ -151,10 +121,10 @@ const checkFriend = async () => {
if (!(userInfoManager.value as any).userID) return; if (!(userInfoManager.value as any).userID) return;
TUIFriendService.checkFriend({ TUIFriendService.checkFriend({
userIDList: [userInfoManager.value.userID], userIDList: [userInfoManager.value.userID],
type: TUIChatEngine.TYPES.SNS_CHECK_TYPE_BOTH, type: TUIChatEngine.TYPES.SNS_CHECK_TYPE_BOTH
}).then((res: any) => { }).then((res: any) => {
const relation = res?.data?.successUserIDList?.[0]?.relation; const relation = res?.data?.successUserIDList?.[0]?.relation;
isFriendShip.value = (relation === TUIChatEngine.TYPES.SNS_TYPE_BOTH_WAY); isFriendShip.value = relation === TUIChatEngine.TYPES.SNS_TYPE_BOTH_WAY;
}); });
}; };
@ -167,7 +137,7 @@ const close = (tabName: string) => {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../../assets/styles/common"; @import '../../../assets/styles/common';
.memeber-profile { .memeber-profile {
flex: 1; flex: 1;
@ -267,7 +237,6 @@ const close = (tabName: string) => {
.avatar { .avatar {
margin: 20px; margin: 20px;
} }
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More