UI优化,红包领取后缺少头像,名称字段

This commit is contained in:
bobobobo
2026-01-19 23:30:08 +08:00
parent d2a22b9419
commit 651d20b909
41 changed files with 2189 additions and 1827 deletions

View File

@@ -103,4 +103,9 @@
}
}
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.tui-navigation {
border-bottom: 2rpx solid #0000000a;
box-sizing: border-box;
}
</style>

View File

@@ -40,7 +40,7 @@
}
.message-input-toolbar-uni {
background-color: #ebf0f6;
background-color: #ffffff;
flex-direction: column;
z-index: 100;

View File

@@ -1,5 +1,15 @@
@import "../../../../../assets/styles/common";
@import "./color";
@import "./web";
@import "./h5";
@import "./uni";
@import '../../../../../assets/styles/common';
@import './color';
@import './web';
@import './h5';
@import './uni';
.toolbar-item-container-icon {
background: #f4f4f4;
}
.toolbar-item-container-uni-title {
font-weight: 500;
font-size: 24rpx;
color: #666666;
}

View File

@@ -212,7 +212,7 @@ defineExpose({
flex-direction: column;
border: none;
overflow: hidden;
background: #ebf0f6;
background: #ffffff;
&-h5 {
padding: 10px 10px 15px;

View File

@@ -2,22 +2,21 @@
<div
:class="{
'message-input-container': true,
'message-input-container-h5': !isPC,
'message-input-container-h5': !isPC
}"
>
<div
v-if="props.isMuted"
class="message-input-mute"
>
<div v-if="props.isMuted" class="message-input-mute">
{{ props.muteText }}
</div>
<!-- #ifdef APP-PLUS -->
<div
v-if="inputToolbarDisplayType === 'emojiPicker' || inputToolbarDisplayType === 'tools'"
v-if="
inputToolbarDisplayType === 'emojiPicker' ||
inputToolbarDisplayType === 'tools'
"
class="input-click-mask"
@tap.stop.prevent="handleMaskClick"
>
</div>
></div>
<!-- #endif -->
<input
id="editor"
@@ -27,7 +26,7 @@
cursor-spacing="20"
confirm-type="send"
:confirm-hold="true"
:focus="programmaticFocus"
:focus="programmaticFocus"
maxlength="140"
type="text"
placeholder-class="input-placeholder"
@@ -38,284 +37,314 @@
@input="onInput"
@blur="onBlur"
@focus="onFocus"
>
/>
</div>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted, onUnmounted } from '../../../adapter-vue';
import { TUIStore, StoreName, IConversationModel, IMessageModel } from '@tencentcloud/chat-uikit-engine-lite';
import { TUIGlobal } from '@tencentcloud/universal-api';
import DraftManager from '../utils/conversationDraft';
import { transformTextWithEmojiNamesToKeys } from '../emoji-config';
import { isPC } from '../../../utils/env';
import { sendMessages } from '../utils/sendMessage';
import { ISendMessagePayload, ToolbarDisplayType } from '../../../interface';
import {
ref,
watch,
onMounted,
onUnmounted
} from '../../../adapter-vue'
import {
TUIStore,
StoreName,
IConversationModel,
IMessageModel
} from '@tencentcloud/chat-uikit-engine-lite'
import { TUIGlobal } from '@tencentcloud/universal-api'
import DraftManager from '../utils/conversationDraft'
import { transformTextWithEmojiNamesToKeys } from '../emoji-config'
import { isPC } from '../../../utils/env'
import { sendMessages } from '../utils/sendMessage'
import {
ISendMessagePayload,
ToolbarDisplayType
} from '../../../interface'
const props = defineProps({
placeholder: {
type: String,
default: 'this is placeholder',
},
replayOrReferenceMessage: {
type: Object,
default: () => ({}),
required: false,
},
isMuted: {
type: Boolean,
default: true,
},
muteText: {
type: String,
default: '',
},
enableInput: {
type: Boolean,
default: true,
},
enableAt: {
type: Boolean,
default: true,
},
enableTyping: {
type: Boolean,
default: true,
},
isGroup: {
type: Boolean,
default: false,
},
inputToolbarDisplayType: {
type: String,
defult: '',
}
});
const emits = defineEmits(['onTyping', 'onFocus', 'onAt']);
const inputText = ref('');
const inputRef = ref();
const inputBlur = ref(true);
const programmaticFocus = ref(false);
const inputContentEmpty = ref(true);
const allInsertedAtInfo = new Map();
const currentConversation = ref<IConversationModel>();
const currentConversationID = ref<string>('');
const currentQuoteMessage = ref<{ message: IMessageModel; type: string }>();
onMounted(() => {
TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated,
});
TUIStore.watch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated,
});
uni.$on('insert-emoji', (data) => {
inputText.value += data?.emoji?.name;
});
uni.$on('send-message-in-emoji-picker', () => {
handleSendMessage();
});
});
onUnmounted(() => {
if (currentConversationID.value) {
DraftManager.setStore(currentConversationID.value, inputText.value, inputText.value, currentQuoteMessage.value);
}
uni.$off('insertEmoji');
uni.$off('send-message-in-emoji-picker');
TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated,
});
TUIStore.unwatch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated,
});
reset();
});
const handleSendMessage = () => {
const messageList = getEditorContent();
resetEditor();
sendMessages(messageList as any, currentConversation.value!);
};
const insertAt = (atInfo: any) => {
if (!allInsertedAtInfo?.has(atInfo?.id)) {
allInsertedAtInfo?.set(atInfo?.id, atInfo?.label);
}
inputText.value += atInfo?.label;
};
const getEditorContent = () => {
let text = inputText.value;
text = transformTextWithEmojiNamesToKeys(text);
const atUserList: string[] = [];
allInsertedAtInfo?.forEach((value: string, key: string) => {
if (text?.includes('@' + value)) {
atUserList.push(key);
}
});
const payload: ISendMessagePayload = {
text,
};
if (atUserList?.length) {
payload.atUserList = atUserList;
}
return [
{
type: 'text',
payload,
const props = defineProps({
placeholder: {
type: String,
default: 'this is placeholder'
},
];
};
const resetEditor = () => {
inputText.value = '';
inputContentEmpty.value = true;
allInsertedAtInfo?.clear();
};
const setEditorContent = (content: any) => {
inputText.value = content;
};
const onBlur = () => {
inputBlur.value = true;
programmaticFocus.value = false;
};
const onFocus = (e: any) => {
inputBlur.value = false;
emits('onFocus', e?.detail?.height);
uni.$emit('scroll-to-bottom');
};
const isEditorContentEmpty = () => {
inputContentEmpty.value = inputText?.value?.length ? false : true;
};
const onInput = (e: any) => {
// uni-app recognizes mention messages
const text = e?.detail?.value;
isEditorContentEmpty();
if (props.isGroup && (text.endsWith('@') || text.endsWith('@\n'))) {
TUIGlobal?.hideKeyboard();
emits('onAt', true);
}
};
watch(
() => [inputContentEmpty.value, inputBlur.value],
(newVal: any, oldVal: any) => {
if (newVal !== oldVal) {
emits('onTyping', inputContentEmpty.value, inputBlur.value);
replayOrReferenceMessage: {
type: Object,
default: () => ({}),
required: false
},
isMuted: {
type: Boolean,
default: true
},
muteText: {
type: String,
default: ''
},
enableInput: {
type: Boolean,
default: true
},
enableAt: {
type: Boolean,
default: true
},
enableTyping: {
type: Boolean,
default: true
},
isGroup: {
type: Boolean,
default: false
},
inputToolbarDisplayType: {
type: String,
defult: ''
}
},
{
immediate: true,
deep: true,
},
);
})
function onCurrentConversationUpdated(conversation: IConversationModel) {
const prevConversationID = currentConversationID.value;
currentConversation.value = conversation;
currentConversationID.value = conversation?.conversationID;
if (prevConversationID !== currentConversationID.value) {
if (prevConversationID) {
DraftManager.setStore(
prevConversationID,
inputText.value,
inputText.value,
currentQuoteMessage.value,
);
}
resetEditor();
const emits = defineEmits(['onTyping', 'onFocus', 'onAt'])
const inputText = ref('')
const inputRef = ref()
const inputBlur = ref(true)
const programmaticFocus = ref(false)
const inputContentEmpty = ref(true)
const allInsertedAtInfo = new Map()
const currentConversation = ref<IConversationModel>()
const currentConversationID = ref<string>('')
const currentQuoteMessage = ref<{
message: IMessageModel
type: string
}>()
onMounted(() => {
TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated
})
TUIStore.watch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated
})
uni.$on('insert-emoji', data => {
inputText.value += data?.emoji?.name
})
uni.$on('send-message-in-emoji-picker', () => {
handleSendMessage()
})
})
onUnmounted(() => {
if (currentConversationID.value) {
DraftManager.getStore(currentConversationID.value, setEditorContent);
DraftManager.setStore(
currentConversationID.value,
inputText.value,
inputText.value,
currentQuoteMessage.value
)
}
uni.$off('insertEmoji')
uni.$off('send-message-in-emoji-picker')
TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdated
})
TUIStore.unwatch(StoreName.CHAT, {
quoteMessage: onQuoteMessageUpdated
})
reset()
})
const handleSendMessage = () => {
const messageList = getEditorContent()
resetEditor()
sendMessages(messageList as any, currentConversation.value!)
}
const insertAt = (atInfo: any) => {
if (!allInsertedAtInfo?.has(atInfo?.id)) {
allInsertedAtInfo?.set(atInfo?.id, atInfo?.label)
}
inputText.value += atInfo?.label
}
const getEditorContent = () => {
let text = inputText.value
text = transformTextWithEmojiNamesToKeys(text)
const atUserList: string[] = []
allInsertedAtInfo?.forEach((value: string, key: string) => {
if (text?.includes('@' + value)) {
atUserList.push(key)
}
})
const payload: ISendMessagePayload = {
text
}
if (atUserList?.length) {
payload.atUserList = atUserList
}
return [
{
type: 'text',
payload
}
]
}
const resetEditor = () => {
inputText.value = ''
inputContentEmpty.value = true
allInsertedAtInfo?.clear()
}
const setEditorContent = (content: any) => {
inputText.value = content
}
const onBlur = () => {
inputBlur.value = true
programmaticFocus.value = false
}
const onFocus = (e: any) => {
inputBlur.value = false
emits('onFocus', e?.detail?.height)
uni.$emit('scroll-to-bottom')
}
const isEditorContentEmpty = () => {
inputContentEmpty.value = inputText?.value?.length ? false : true
}
const onInput = (e: any) => {
// uni-app recognizes mention messages
const text = e?.detail?.value
isEditorContentEmpty()
if (props.isGroup && (text.endsWith('@') || text.endsWith('@\n'))) {
TUIGlobal?.hideKeyboard()
emits('onAt', true)
}
}
}
function onQuoteMessageUpdated(options?: { message: IMessageModel; type: string }) {
currentQuoteMessage.value = options;
}
watch(
() => [inputContentEmpty.value, inputBlur.value],
(newVal: any, oldVal: any) => {
if (newVal !== oldVal) {
emits('onTyping', inputContentEmpty.value, inputBlur.value)
}
},
{
immediate: true,
deep: true
}
)
function reset() {
inputBlur.value = true;
currentConversation.value = null;
currentConversationID.value = '';
currentQuoteMessage.value = null;
resetEditor();
}
function onCurrentConversationUpdated(
conversation: IConversationModel
) {
const prevConversationID = currentConversationID.value
currentConversation.value = conversation
currentConversationID.value = conversation?.conversationID
if (prevConversationID !== currentConversationID.value) {
if (prevConversationID) {
DraftManager.setStore(
prevConversationID,
inputText.value,
inputText.value,
currentQuoteMessage.value
)
}
resetEditor()
if (currentConversationID.value) {
DraftManager.getStore(
currentConversationID.value,
setEditorContent
)
}
}
}
function onQuoteMessageUpdated(options?: {
message: IMessageModel
type: string
}) {
currentQuoteMessage.value = options
}
function handleMaskClick(e: Event) {
// #ifdef APP-PLUS
e.stopPropagation();
emits('onFocus');
uni.$emit('scroll-to-bottom');
setTimeout(() => {
programmaticFocus.value = true;
// IOS set 500ms timeout
}, 100);
// #endif
}
defineExpose({
insertAt,
resetEditor,
setEditorContent,
getEditorContent,
});
function reset() {
inputBlur.value = true
currentConversation.value = null
currentConversationID.value = ''
currentQuoteMessage.value = null
resetEditor()
}
function handleMaskClick(e: Event) {
// #ifdef APP-PLUS
e.stopPropagation()
emits('onFocus')
uni.$emit('scroll-to-bottom')
setTimeout(() => {
programmaticFocus.value = true
// IOS set 500ms timeout
}, 100)
// #endif
}
defineExpose({
insertAt,
resetEditor,
setEditorContent,
getEditorContent
})
</script>
<style lang="scss" scoped>
@import "../../../assets/styles/common";
@import '../../../assets/styles/common';
.message-input-container {
display: flex;
flex-direction: column;
flex: 1;
padding: 3px 10px 10px;
overflow: hidden;
position: relative;
&-h5 {
flex: 1;
height: auto;
background: #fff;
border-radius: 10px;
padding: 7px 0 7px 10px;
font-size: 16px !important;
max-height: 86px;
}
.message-input-mute {
flex: 1;
.message-input-container {
display: flex;
color: #999;
font-size: 14px;
justify-content: center;
align-items: center;
}
.message-input-area {
flex-direction: column;
flex: 1;
overflow-y: scroll;
min-height: 25px;
}
}
padding: 3px 10px 10px;
overflow: hidden;
position: relative;
border-radius: 64rpx !important;
background: #f4f4f4 !important;
.input-click-mask {
background-color: transparent;
position: absolute;
inset: 0;
z-index: 1;
}
&-h5 {
flex: 1;
height: auto;
background: #fff;
border-radius: 10px;
padding: 7px 0 7px 10px;
font-size: 16px !important;
max-height: 86px;
}
.message-input-mute {
flex: 1;
display: flex;
color: #999;
font-size: 14px;
justify-content: center;
align-items: center;
}
.message-input-area {
flex: 1;
overflow-y: scroll;
min-height: 25px;
}
}
.input-click-mask {
background-color: transparent;
position: absolute;
inset: 0;
z-index: 1;
}
</style>

View File

@@ -616,16 +616,35 @@
const onClaim = (item: IMessageModel, index: number) => {
const { conversationType, cloudCustomData, flow, payload } = item
const data = JSON.parse(payload.data)
// 群聊
if (conversationType === TYPES.value.CONV_GROUP) {
console.log(item)
console.log(data)
receiveRedEnvelope({
redPacketId: data.id
})
} else {
// 个人红包
getRedEnvelopeDetail(data.id).then(async (res: any) => {
getRedEnvelopeDetail(data.id).then(async (res: any) => {
// 群聊
if (conversationType === TYPES.value.CONV_GROUP) {
if (res.data.hasReceived) {
// 直接去详情页
navigateTo('/pages/red-packet/details', {
id: data.id,
type: conversationType
})
} else {
const show = await showDialog('提示', '是否领取该红包?')
if (show) {
// newMessage.in = true
// customRefMessage.value[index].updateClaimStatus(newMessage)
// item.modifyMessage({
// cloudCustomData: JSON.stringify(newMessage)
// })
await receiveRedEnvelope({
redPacketId: data.id
})
navigateTo('/pages/red-packet/details', {
id: data.id,
type: conversationType
})
}
}
} else {
// 个人红包
console.log(res)
let newMessage = {
// ...data,
@@ -639,23 +658,37 @@
if (flow === 'in') {
if (res.data.hasReceived) {
// 直接去详情页
navigateTo('/pages/red-packet/details', {
id: data.id,
type: conversationType
})
} else {
const show = await showDialog('提示', '是否领取该红包?')
if (show) {
newMessage.in = true
customRefMessage.value[index].updateClaimStatus(newMessage)
item.modifyMessage({
cloudCustomData: JSON.stringify(newMessage)
// newMessage.in = true
// customRefMessage.value[index].updateClaimStatus(newMessage)
// item.modifyMessage({
// cloudCustomData: JSON.stringify(newMessage)
// })
await receiveRedEnvelope({
redPacketId: data.id
})
navigateTo('/pages/red-packet/details', {
id: data.id,
type: conversationType
})
}
}
} else {
newMessage.out = true
customRefMessage.value[index].updateClaimStatus(newMessage)
item.modifyMessage({
cloudCustomData: JSON.stringify(newMessage)
navigateTo('/pages/red-packet/details', {
id: data.id,
type: conversationType
})
// newMessage.out = true
// customRefMessage.value[index].updateClaimStatus(newMessage)
// item.modifyMessage({
// cloudCustomData: JSON.stringify(newMessage)
// })
// .then(() => {
// navigateTo('/pages/red-packet/details', {
// id: data.id,
@@ -663,23 +696,8 @@
// })
// })
}
})
return
if (flow === 'in') {
// 修改后的消息
const newMessage = {
...data,
isOpen: true
}
item.modifyMessage({
cloudCustomData: JSON.stringify(newMessage)
})
receiveRedEnvelope({
redPacketId: data.id
})
}
}
})
}
const resendMessageConfirm = () => {

View File

@@ -21,19 +21,24 @@
<Avatar
useSkeletonAnimation
:url="message.avatar || ''"
:style="{flex: '0 0 auto'}"
:style="{ flex: '0 0 auto' }"
/>
<main
class="message-body"
@click.stop
>
<main class="message-body" @click.stop>
<div
v-if="message.flow === 'in' && message.conversationType === 'GROUP'"
v-if="
message.flow === 'in' &&
message.conversationType === 'GROUP'
"
class="message-body-nick-name"
>
{{ props.content.showName }}
</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'
]"
>
<div
:class="[
'blink',
@@ -42,18 +47,22 @@
message.hasRiskContent && 'content-has-risk',
isNoPadding ? 'content-no-padding' : '',
isNoPadding && isBlink ? 'blink-shadow' : '',
!isNoPadding && isBlink ? 'blink-content' : '',
!isNoPadding && isBlink ? 'blink-content' : ''
]"
>
<div class="content-main">
<img
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"
>
/>
<template v-else>
<slot />
</template>
@@ -67,10 +76,7 @@
</div>
</div>
<!-- audio unplay mark -->
<div
v-if="isDisplayUnplayMark"
class="audio-unplay-mark"
/>
<div v-if="isDisplayUnplayMark" class="audio-unplay-mark" />
<!-- Fail Icon -->
<div
v-if="message.status === 'fail' || message.hasRiskContent"
@@ -81,7 +87,10 @@
</div>
<!-- Loading Icon -->
<Icon
v-if="message.status === 'unSend' && needLoadingIconMessageType.includes(message.type)"
v-if="
message.status === 'unSend' &&
needLoadingIconMessageType.includes(message.type)
"
class="message-label loading-circle"
:file="loadingIcon"
:width="'15px'"
@@ -122,353 +131,157 @@
</template>
<script lang="ts" setup>
import { computed, toRefs } from '../../../../adapter-vue';
import TUIChatEngine, { TUITranslateService, IMessageModel } from '@tencentcloud/chat-uikit-engine-lite';
import Icon from '../../../common/Icon.vue';
import ReadStatus from './read-status/index.vue';
import MessageQuote from './message-quote/index.vue';
import Avatar from '../../../common/Avatar/index.vue';
import MessageTranslate from './message-translate/index.vue';
import MessageConvert from './message-convert/index.vue';
import RadioSelect from '../../../common/RadioSelect/index.vue';
import loadingIcon from '../../../../assets/icon/loading.png';
import { shallowCopyMessage } from '../../utils/utils';
import { isPC } from '../../../../utils/env';
import { computed, toRefs } from '../../../../adapter-vue'
import TUIChatEngine, {
TUITranslateService,
IMessageModel
} from '@tencentcloud/chat-uikit-engine-lite'
import Icon from '../../../common/Icon.vue'
import ReadStatus from './read-status/index.vue'
import MessageQuote from './message-quote/index.vue'
import Avatar from '../../../common/Avatar/index.vue'
import MessageTranslate from './message-translate/index.vue'
import MessageConvert from './message-convert/index.vue'
import RadioSelect from '../../../common/RadioSelect/index.vue'
import loadingIcon from '../../../../assets/icon/loading.png'
import { shallowCopyMessage } from '../../utils/utils'
import { isPC } from '../../../../utils/env'
interface IProps {
messageItem: IMessageModel;
content?: any;
classNameList?: string[];
blinkMessageIDList?: string[];
isMultipleSelectMode?: boolean;
isAudioPlayed?: boolean | undefined;
multipleSelectedMessageIDList?: string[];
}
interface IEmits {
(e: 'resendMessage'): void;
(e: 'blinkMessage', messageID: string): void;
(e: 'setReadReceiptPanelVisible', visible: boolean, message?: IMessageModel): void;
(e: 'changeSelectMessageIDList', options: { type: 'add' | 'remove' | 'clearAll'; messageID: string }): void;
// Only for uni-app
(e: 'scrollTo', scrollHeight: number): void;
}
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
isAudioPlayed: false,
messageItem: () => ({} as IMessageModel),
content: () => ({}),
blinkMessageIDList: () => [],
classNameList: () => [],
isMultipleSelectMode: false,
multipleSelectedMessageIDList: () => [],
});
const TYPES = TUIChatEngine.TYPES;
const riskImageReplaceUrl = 'https://web.sdk.qcloud.com/component/TUIKit/assets/has_risk_default.png';
const needLoadingIconMessageType = [
TYPES.MSG_LOCATION,
TYPES.MSG_TEXT,
TYPES.MSG_CUSTOM,
TYPES.MSG_MERGER,
TYPES.MSG_FACE,
];
const { blinkMessageIDList, messageItem: message } = toRefs(props);
const isMultipleSelected = computed<boolean>(() => {
return props.multipleSelectedMessageIDList.includes(message.value.ID);
});
const isDisplayUnplayMark = computed<boolean>(() => {
return message.value.flow === 'in'
&& message.value.status === 'success'
&& message.value.type === TYPES.MSG_AUDIO
&& !props.isAudioPlayed;
});
const containerClassNameList = computed(() => {
return [
'message-bubble',
isMultipleSelected.value ? 'multiple-selected' : '',
...props.classNameList,
];
});
const isNoPadding = computed(() => {
return [TYPES.MSG_IMAGE, TYPES.MSG_VIDEO, TYPES.MSG_MERGER].includes(message.value.type);
});
const riskContentText = computed<string>(() => {
let content = TUITranslateService.t('TUIChat.涉及敏感内容') + ', ';
if (message.value.flow === 'out') {
content += TUITranslateService.t('TUIChat.发送失败');
} else {
content += TUITranslateService.t(
message.value.type === TYPES.MSG_AUDIO ? 'TUIChat.无法收听' : 'TUIChat.无法查看',
);
interface IProps {
messageItem: IMessageModel
content?: any
classNameList?: string[]
blinkMessageIDList?: string[]
isMultipleSelectMode?: boolean
isAudioPlayed?: boolean | undefined
multipleSelectedMessageIDList?: string[]
}
return content;
});
const isBlink = computed(() => {
if (message.value?.ID) {
return blinkMessageIDList?.value?.includes(message.value.ID);
interface IEmits {
(e: 'resendMessage'): void
(e: 'blinkMessage', messageID: string): void
(
e: 'setReadReceiptPanelVisible',
visible: boolean,
message?: IMessageModel
): void
(
e: 'changeSelectMessageIDList',
options: { type: 'add' | 'remove' | 'clearAll'; messageID: string }
): void
// Only for uni-app
(e: 'scrollTo', scrollHeight: number): void
}
return false;
});
function toggleMultipleSelect(isSelected: boolean) {
emits('changeSelectMessageIDList', {
type: isSelected ? 'add' : 'remove',
messageID: message.value.ID,
});
}
const emits = defineEmits<IEmits>()
function resendMessage() {
if (!message.value?.hasRiskContent) {
emits('resendMessage');
const props = withDefaults(defineProps<IProps>(), {
isAudioPlayed: false,
messageItem: () => ({} as IMessageModel),
content: () => ({}),
blinkMessageIDList: () => [],
classNameList: () => [],
isMultipleSelectMode: false,
multipleSelectedMessageIDList: () => []
})
const TYPES = TUIChatEngine.TYPES
const riskImageReplaceUrl =
'https://web.sdk.qcloud.com/component/TUIKit/assets/has_risk_default.png'
const needLoadingIconMessageType = [
TYPES.MSG_LOCATION,
TYPES.MSG_TEXT,
TYPES.MSG_CUSTOM,
TYPES.MSG_MERGER,
TYPES.MSG_FACE
]
const { blinkMessageIDList, messageItem: message } = toRefs(props)
const isMultipleSelected = computed<boolean>(() => {
return props.multipleSelectedMessageIDList.includes(message.value.ID)
})
const isDisplayUnplayMark = computed<boolean>(() => {
return (
message.value.flow === 'in' &&
message.value.status === 'success' &&
message.value.type === TYPES.MSG_AUDIO &&
!props.isAudioPlayed
)
})
const containerClassNameList = computed(() => {
return [
'message-bubble',
isMultipleSelected.value ? 'multiple-selected' : '',
...props.classNameList
]
})
const isNoPadding = computed(() => {
return [TYPES.MSG_IMAGE, TYPES.MSG_VIDEO, TYPES.MSG_MERGER].includes(
message.value.type
)
})
const riskContentText = computed<string>(() => {
let content = TUITranslateService.t('TUIChat.涉及敏感内容') + ', '
if (message.value.flow === 'out') {
content += TUITranslateService.t('TUIChat.发送失败')
} else {
content += TUITranslateService.t(
message.value.type === TYPES.MSG_AUDIO
? 'TUIChat.无法收听'
: 'TUIChat.无法查看'
)
}
return content
})
const isBlink = computed(() => {
if (message.value?.ID) {
return blinkMessageIDList?.value?.includes(message.value.ID)
}
return false
})
function toggleMultipleSelect(isSelected: boolean) {
emits('changeSelectMessageIDList', {
type: isSelected ? 'add' : 'remove',
messageID: message.value.ID
})
}
}
function blinkMessage(messageID: string) {
emits('blinkMessage', messageID);
}
function resendMessage() {
if (!message.value?.hasRiskContent) {
emits('resendMessage')
}
}
function scrollTo(scrollHeight: number) {
emits('scrollTo', scrollHeight);
}
function blinkMessage(messageID: string) {
emits('blinkMessage', messageID)
}
function openReadUserPanel() {
emits('setReadReceiptPanelVisible', true, message.value);
}
function scrollTo(scrollHeight: number) {
emits('scrollTo', scrollHeight)
}
function openReadUserPanel() {
emits('setReadReceiptPanelVisible', true, message.value)
}
</script>
<style lang="scss" scoped>
:not(not) {
display: flex;
flex-direction: column;
min-width: 0;
box-sizing: border-box;
}
.flex-row {
display: flex;
}
.reverse {
display: flex;
flex-direction: row-reverse;
justify-content: flex-start;
}
.message-bubble {
padding: 10px 15px;
display: flex;
flex-direction: row;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
&.multiple-selected {
background-color: #f0f0f0;
}
.multiple-select-radio {
margin-right: 12px;
flex: 0 0 auto;
}
.control-reverse {
flex: 1 1 auto;
flex-direction: row-reverse;
}
.message-bubble-main-content {
:not(not) {
display: flex;
flex-direction: row;
flex-direction: column;
min-width: 0;
box-sizing: border-box;
}
.message-avatar {
display: block;
width: 36px;
height: 36px;
border-radius: 5px;
flex: 0 0 auto;
}
.message-body {
display: flex;
flex: 0 1 auto;
flex-direction: column;
align-items: flex-start;
margin: 0 8px;
.message-body-nick-name {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #999;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-body-main {
max-width: 100%;
display: flex;
flex-direction: row;
min-width: 0;
box-sizing: border-box;
&-reverse {
flex-direction: row-reverse;
}
.audio-unplay-mark {
flex: 0 0 auto;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #f00;
margin: 5px;
}
.message-body-content {
display: flex;
flex-direction: column;
min-width: 0;
box-sizing: border-box;
padding: 12px;
font-size: 14px;
color: #000;
letter-spacing: 0;
word-wrap: break-word;
word-break: break-all;
position: relative;
.content-main {
box-sizing: border-box;
display: flex;
flex-direction: column;
flex-shrink: 0;
align-content: flex-start;
border: 0 solid black;
margin: 0;
padding: 0;
min-width: 0;
.message-risk-replace {
width: 130px;
height: 130px;
}
}
.content-has-risk-tips {
font-size: 12px;
color: #fa5151;
font-family: PingFangSC-Regular;
margin-top: 5px;
border-top: 1px solid #e5c7c7;
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;
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);
}
}
}
.align-self-bottom {
align-self: flex-end;
}
}
}
.flex-row {
display: flex;
}
.reverse {
@@ -477,9 +290,222 @@ function openReadUserPanel() {
justify-content: flex-start;
}
.message-bubble-extra-content {
.message-bubble {
padding: 10px 15px;
display: flex;
flex-direction: column;
flex-direction: row;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
&.multiple-selected {
background-color: #f0f0f0;
}
.multiple-select-radio {
margin-right: 12px;
flex: 0 0 auto;
}
.control-reverse {
flex: 1 1 auto;
flex-direction: row-reverse;
}
.message-bubble-main-content {
display: flex;
flex-direction: row;
.message-avatar {
display: block;
width: 36px;
height: 36px;
border-radius: 5px;
flex: 0 0 auto;
}
.message-body {
display: flex;
flex: 0 1 auto;
flex-direction: column;
align-items: flex-start;
margin: 0 8px;
.message-body-nick-name {
display: block;
margin-bottom: 4px;
font-size: 12px;
color: #999;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-body-main {
max-width: 100%;
display: flex;
flex-direction: row;
min-width: 0;
box-sizing: border-box;
&-reverse {
flex-direction: row-reverse;
}
.audio-unplay-mark {
flex: 0 0 auto;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #f00;
margin: 5px;
}
.message-body-content {
display: flex;
flex-direction: column;
min-width: 0;
box-sizing: border-box;
padding: 16rpx 20rpx;
font-size: 14px;
color: #333;
letter-spacing: 0;
word-wrap: break-word;
word-break: break-all;
position: relative;
.content-main {
box-sizing: border-box;
display: flex;
flex-direction: column;
flex-shrink: 0;
align-content: flex-start;
border: 0 solid black;
margin: 0;
padding: 0;
min-width: 0;
.message-risk-replace {
width: 130px;
height: 130px;
}
}
.content-has-risk-tips {
font-size: 12px;
color: #fa5151;
font-family: PingFangSC-Regular;
margin-top: 5px;
border-top: 1px solid #e5c7c7;
padding-top: 5px;
}
}
.content-in {
background: #f4f4f4;
border-radius: 0 10px 10px;
}
.content-out {
background: #00D9C5;
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;
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);
}
}
}
.align-self-bottom {
align-self: flex-end;
}
}
}
}
.reverse {
display: flex;
flex-direction: row-reverse;
justify-content: flex-start;
}
.message-bubble-extra-content {
display: flex;
flex-direction: column;
}
}
}
</style>

View File

@@ -8,4 +8,12 @@
flex-direction: column;
box-sizing: border-box;
min-width: 0;
}
.tui-message-list {
background: #fff;
}
.message-li {
margin-top: 0 !important;
}

View File

@@ -89,7 +89,12 @@ export const sendMessages = async (
options.payload.atUserList = content.payload.atUserList;
await TUIChatService.sendTextAtMessage(options, sendMessageOptions);
} else {
await TUIChatService.sendTextMessage(options, sendMessageOptions);
try {
await TUIChatService.sendTextMessage(options, sendMessageOptions);
} catch (err) {
console.log('发送失败,对方不是你的好友')
}
}
break;
case 'image':

View File

@@ -1,28 +1,34 @@
<template>
<div
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']"
>
<Navigation>
<template #left>
<div @click="resetContactSearchingUIData">
<Icon
:file="backSVG"
/>
<Icon :file="backSVG" />
</div>
</template>
</Navigation>
<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
:class="[
'tui-contact-info-basic-text',
!isPC && 'tui-contact-info-h5-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',
!isPC && 'tui-contact-info-h5-basic-text-name'
]"
>
{{ generateContactInfoName(contactInfoData) }}
@@ -32,7 +38,7 @@
:key="item.label"
:class="[
'tui-contact-info-basic-text-other',
!isPC && 'tui-contact-info-h5-basic-text-other',
!isPC && 'tui-contact-info-h5-basic-text-other'
]"
>
{{
@@ -44,14 +50,17 @@
<img
:class="[
'tui-contact-info-basic-avatar',
!isPC && 'tui-contact-info-h5-basic-avatar',
!isPC && 'tui-contact-info-h5-basic-avatar'
]"
:src="generateAvatar(contactInfoData)"
>
/>
</div>
<div
v-if="contactInfoMoreList[0]"
:class="['tui-contact-info-more', !isPC && 'tui-contact-info-h5-more']"
:class="[
'tui-contact-info-more',
!isPC && 'tui-contact-info-h5-more'
]"
>
<div
v-for="item in contactInfoMoreList"
@@ -61,13 +70,13 @@
!isPC && 'tui-contact-info-h5-more-item',
item.labelPosition === CONTACT_INFO_LABEL_POSITION.TOP
? 'tui-contact-info-more-item-top'
: 'tui-contact-info-more-item-left',
: 'tui-contact-info-more-item-left'
]"
>
<div
:class="[
'tui-contact-info-more-item-label',
!isPC && 'tui-contact-info-h5-more-item-label',
!isPC && 'tui-contact-info-h5-more-item-label'
]"
>
{{ `${TUITranslateService.t(`TUIContact.${item.label}`)}` }}
@@ -75,20 +84,20 @@
<div
:class="[
'tui-contact-info-more-item-content',
!isPC && 'tui-contact-info-h5-more-item-content',
!isPC && 'tui-contact-info-h5-more-item-content'
]"
>
<div
v-if="!item.editing"
:class="[
'tui-contact-info-more-item-content-text',
!isPC && 'tui-contact-info-h5-more-item-content-text',
!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',
!isPC && 'tui-contact-info-h5-more-item-content-text-data'
]"
>
{{ item.data }}
@@ -97,39 +106,41 @@
v-if="item.editable"
:class="[
'tui-contact-info-more-item-content-text-icon',
!isPC && 'tui-contact-info-h5-more-item-content-text-icon',
!isPC && 'tui-contact-info-h5-more-item-content-text-icon'
]"
@click="setEditing(item)"
>
<Icon
:file="editSVG"
width="14px"
height="14px"
/>
<Icon :file="editSVG" width="14px" height="14px" />
</div>
</div>
<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"
:class="[
'tui-contact-info-more-item-content-input',
!isPC && 'tui-contact-info-h5-more-item-content-input',
!isPC && 'tui-contact-info-h5-more-item-content-input'
]"
type="text"
@confirm="onContactInfoEmitSubmit(item)"
@keyup.enter="onContactInfoEmitSubmit(item)"
>
/>
<textarea
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA"
v-else-if="
item.editType === CONTACT_INFO_MORE_EDIT_TYPE.TEXTAREA
"
v-model="item.data"
:class="[
'tui-contact-info-more-item-content-textarea',
!isPC && 'tui-contact-info-h5-more-item-content-textarea',
!isPC && 'tui-contact-info-h5-more-item-content-textarea'
]"
confirm-type="done"
/>
<div
v-else-if="item.editType === CONTACT_INFO_MORE_EDIT_TYPE.SWITCH"
v-else-if="
item.editType === CONTACT_INFO_MORE_EDIT_TYPE.SWITCH
"
@click="onContactInfoEmitSubmit(item)"
>
<SwitchBar :value="item.data" />
@@ -140,7 +151,7 @@
<div
:class="[
'tui-contact-info-button',
!isPC && 'tui-contact-info-h5-button',
!isPC && 'tui-contact-info-h5-button'
]"
>
<button
@@ -151,7 +162,7 @@
!isPC && 'tui-contact-info-h5-button-item',
item.type === CONTACT_INFO_BUTTON_TYPE.CANCEL
? `tui-contact-info-button-item-cancel`
: `tui-contact-info-button-item-submit`,
: `tui-contact-info-button-item-submit`
]"
@click="onContactInfoButtonClicked(item)"
>
@@ -161,263 +172,339 @@
</div>
</template>
<script setup lang="ts">
import TUIChatEngine, {
TUIStore,
StoreName,
TUITranslateService,
IGroupModel,
Friend,
FriendApplication,
} from '@tencentcloud/chat-uikit-engine-lite';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, computed, onMounted, onUnmounted } from '../../../adapter-vue';
import { isPC } from '../../../utils/env';
import TUIChatEngine, {
TUIStore,
StoreName,
TUITranslateService,
IGroupModel,
Friend,
FriendApplication,
TUIUserService
} from '@tencentcloud/chat-uikit-engine-lite'
import { TUIGlobal } from '@tencentcloud/universal-api'
import {
ref,
computed,
onMounted,
onUnmounted
} from '../../../adapter-vue'
import { isPC } from '../../../utils/env'
import {
generateAvatar,
generateContactInfoName,
generateContactInfoBasic,
isFriend,
isApplicationType,
} from '../utils/index';
import {
contactMoreInfoConfig,
contactButtonConfig,
} from './contact-info-config';
import Navigation from '../../common/Navigation/index.vue';
import Icon from '../../common/Icon.vue';
import editSVG from '../../../assets/icon/edit.svg';
import backSVG from '../../../assets/icon/back.svg';
import SwitchBar from '../../common/SwitchBar/index.vue';
import {
IBlackListUserItem,
IContactInfoMoreItem,
IContactInfoButton,
} from '../../../interface';
import {
CONTACT_INFO_LABEL_POSITION,
CONTACT_INFO_MORE_EDIT_TYPE,
CONTACT_INFO_BUTTON_TYPE,
CONTACT_INFO_TITLE,
} from '../../../constant';
import { deepCopy } from '../../TUIChat/utils/utils';
import {
generateAvatar,
generateContactInfoName,
generateContactInfoBasic,
isFriend,
isApplicationType
} from '../utils/index'
import {
contactMoreInfoConfig,
contactButtonConfig
} from './contact-info-config'
import Navigation from '../../common/Navigation/index.vue'
import Icon from '../../common/Icon.vue'
import editSVG from '../../../assets/icon/edit.svg'
import backSVG from '../../../assets/icon/back.svg'
import SwitchBar from '../../common/SwitchBar/index.vue'
import {
IBlackListUserItem,
IContactInfoMoreItem,
IContactInfoButton
} from '../../../interface'
import {
CONTACT_INFO_LABEL_POSITION,
CONTACT_INFO_MORE_EDIT_TYPE,
CONTACT_INFO_BUTTON_TYPE,
CONTACT_INFO_TITLE
} from '../../../constant'
import { deepCopy } from '../../TUIChat/utils/utils'
import { useUI } from '../../../../utils/use-ui'
type IContactInfoType = IGroupModel | Friend | FriendApplication | IBlackListUserItem;
type IContactInfoType =
| IGroupModel
| Friend
| FriendApplication
| IBlackListUserItem
const emits = defineEmits(['switchConversation']);
const { showLoading, hideLoading } = useUI()
const contactInfoData = ref<IContactInfoType>({} as IContactInfoType);
const contactInfoBasicList = ref<Array<{ label: string; data: string }>>([]);
const contactInfoMoreList = ref<IContactInfoMoreItem[]>([]);
const contactInfoButtonList = ref<IContactInfoButton[]>([]);
const contactInfoTitle = ref<string>('');
const emits = defineEmits(['switchConversation'])
const setEditing = (item: any) => {
item.editing = true;
};
const contactInfoData = ref<IContactInfoType>({} as IContactInfoType)
const contactInfoBasicList = ref<
Array<{ label: string; data: string }>
>([])
const contactInfoMoreList = ref<IContactInfoMoreItem[]>([])
const contactInfoButtonList = ref<IContactInfoButton[]>([])
const contactInfoTitle = ref<string>('')
const isGroup = computed((): boolean =>
(contactInfoData.value as IGroupModel)?.groupID ? true : false,
);
const isApplication = computed((): boolean => {
return isApplicationType(contactInfoData?.value);
});
// is both friend, if is group type always false
const isBothFriend = ref<boolean>(false);
// is group member, including ordinary member, admin, group owner
const isGroupMember = computed((): boolean => {
return (contactInfoData.value as IGroupModel)?.selfInfo?.userID ? true : false;
});
// is in black list, if is group type always false
const isInBlackList = computed((): boolean => {
return (
!isGroup.value
&& blackList.value?.findIndex(
(item: IBlackListUserItem) =>
item?.userID === (contactInfoData.value as IBlackListUserItem)?.userID,
) >= 0
);
});
const blackList = ref<IBlackListUserItem[]>([]);
onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
});
TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated,
currentContactListKey: onCurrentContactListKeyUpdated,
});
TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated,
});
});
const onCurrentContactListKeyUpdated = (key: string) => {
if (CONTACT_INFO_TITLE[key]) {
contactInfoTitle.value = TUITranslateService.t(`TUIContact.${CONTACT_INFO_TITLE[key]}`);
const setEditing = (item: any) => {
item.editing = true
}
};
const resetContactInfoUIData = () => {
contactInfoData.value = {} as IContactInfoType;
contactInfoBasicList.value = [];
contactInfoMoreList.value = [];
contactInfoButtonList.value = [];
};
const isGroup = computed((): boolean =>
(contactInfoData.value as IGroupModel)?.groupID ? true : false
)
const resetContactSearchingUIData = () => {
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', {});
TUIStore.update(StoreName.CUSTOM, 'currentContactSearchingStatus', false);
TUIGlobal?.closeSearching && TUIGlobal?.closeSearching();
};
const isApplication = computed((): boolean => {
return isApplicationType(contactInfoData?.value)
})
const onContactInfoEmitSubmit = (item: any) => {
item.editSubmitHandler
&& item.editSubmitHandler({
item,
contactInfoData: contactInfoData.value,
isBothFriend: isBothFriend.value,
isInBlackList: isInBlackList.value,
});
};
// is both friend, if is group type always false
const isBothFriend = ref<boolean>(false)
const onContactInfoButtonClicked = (item: any) => {
item.onClick
&& item.onClick({
contactInfoData: contactInfoData.value,
contactInfoMoreList: contactInfoMoreList.value,
});
if (
item.key === 'enterGroupConversation'
|| item.key === 'enterC2CConversation'
) {
emits('switchConversation', contactInfoData.value);
resetContactSearchingUIData();
// is group member, including ordinary member, admin, group owner
const isGroupMember = computed((): boolean => {
return (contactInfoData.value as IGroupModel)?.selfInfo?.userID
? true
: false
})
// is in black list, if is group type always false
const isInBlackList = computed((): boolean => {
return (
!isGroup.value &&
blackList.value?.findIndex(
(item: IBlackListUserItem) =>
item?.userID ===
(contactInfoData.value as IBlackListUserItem)?.userID
) >= 0
)
})
const blackList = ref<IBlackListUserItem[]>([])
onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated,
currentContactListKey: onCurrentContactListKeyUpdated
})
TUIStore.watch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated
})
})
onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, {
currentContactInfo: onCurrentContactInfoUpdated,
currentContactListKey: onCurrentContactListKeyUpdated
})
TUIStore.unwatch(StoreName.USER, {
userBlacklist: onUserBlacklistUpdated
})
})
const onCurrentContactListKeyUpdated = (key: string) => {
if (CONTACT_INFO_TITLE[key]) {
contactInfoTitle.value = TUITranslateService.t(
`TUIContact.${CONTACT_INFO_TITLE[key]}`
)
}
}
};
const generateMoreInfo = async () => {
if (!isApplication.value) {
if (
(!isGroup.value && !isBothFriend.value && !isInBlackList.value)
|| (isGroup.value
&& !isGroupMember.value
&& (contactInfoData.value as IGroupModel)?.type !== TUIChatEngine?.TYPES?.GRP_AVCHATROOM)
) {
contactMoreInfoConfig.setWords.data = '';
contactInfoMoreList.value.push(contactMoreInfoConfig.setWords);
}
if (!isGroup.value && !isInBlackList.value) {
contactMoreInfoConfig.setRemark.data
= (contactInfoData.value as Friend)?.remark || '';
contactMoreInfoConfig.setRemark.editing = false;
contactInfoMoreList.value.push(contactMoreInfoConfig.setRemark);
}
if (!isGroup.value && (isBothFriend.value || isInBlackList.value)) {
contactMoreInfoConfig.blackList.data = isInBlackList.value || false;
contactInfoMoreList.value.push(contactMoreInfoConfig.blackList);
}
} else {
contactMoreInfoConfig.displayWords.data
= (contactInfoData.value as FriendApplication)?.wording || '';
contactInfoMoreList.value.push(contactMoreInfoConfig.displayWords);
const resetContactInfoUIData = () => {
contactInfoData.value = {} as IContactInfoType
contactInfoBasicList.value = []
contactInfoMoreList.value = []
contactInfoButtonList.value = []
}
};
const generateButton = () => {
if (isInBlackList.value) {
return;
const resetContactSearchingUIData = () => {
TUIStore.update(StoreName.CUSTOM, 'currentContactInfo', {})
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchingStatus',
false
)
TUIGlobal?.closeSearching && TUIGlobal?.closeSearching()
}
if (isApplication.value) {
if (
(contactInfoData.value as FriendApplication)?.type
=== TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
) {
contactInfoButtonList?.value?.push(
contactButtonConfig.refuseFriendApplication,
);
contactInfoButtonList?.value?.push(
contactButtonConfig.acceptFriendApplication,
);
}
} else {
if (isGroup.value && isGroupMember.value) {
switch ((contactInfoData.value as IGroupModel)?.selfInfo?.role) {
case 'Owner':
contactInfoButtonList?.value?.push(contactButtonConfig.dismissGroup);
break;
default:
contactInfoButtonList?.value?.push(contactButtonConfig.quitGroup);
break;
}
contactInfoButtonList?.value?.push(
contactButtonConfig.enterGroupConversation,
);
} else if (!isGroup.value && isBothFriend.value) {
contactInfoButtonList?.value?.push(contactButtonConfig.deleteFriend);
contactInfoButtonList?.value?.push(
contactButtonConfig.enterC2CConversation,
);
} else {
if (isGroup.value) {
contactInfoButtonList?.value?.push(
(contactInfoData.value as IGroupModel)?.type === TUIChatEngine?.TYPES?.GRP_AVCHATROOM
? contactButtonConfig.joinAVChatGroup
: contactButtonConfig.joinGroup,
);
const onContactInfoEmitSubmit = (item: any) => {
if (item.key === 'blackList') {
// item.data = true
// resetContactSearchingUIData()
const userID = (contactInfoData.value as { userID: string }).userID
if (item.data) {
showLoading()
TUIUserService.removeFromBlacklist({ userIDList: [userID] })
.then(() => {
item.data = false
})
.finally(() => {
hideLoading()
})
} else {
contactInfoButtonList?.value?.push(contactButtonConfig.addFriend);
showLoading()
TUIUserService.addToBlacklist({ userIDList: [userID] })
.then(() => {
item.data = true
})
.finally(() => {
hideLoading()
})
}
} else {
item.editSubmitHandler &&
item.editSubmitHandler({
item,
contactInfoData: contactInfoData.value,
isBothFriend: isBothFriend.value,
isInBlackList: isInBlackList.value
})
}
}
const onContactInfoButtonClicked = (item: any) => {
item.onClick &&
item.onClick({
contactInfoData: contactInfoData.value,
contactInfoMoreList: contactInfoMoreList.value
})
if (
item.key === 'enterGroupConversation' ||
item.key === 'enterC2CConversation'
) {
emits('switchConversation', contactInfoData.value)
resetContactSearchingUIData()
}
}
const generateMoreInfo = async () => {
if (!isApplication.value) {
if (
(!isGroup.value && !isBothFriend.value && !isInBlackList.value) ||
(isGroup.value &&
!isGroupMember.value &&
(contactInfoData.value as IGroupModel)?.type !==
TUIChatEngine?.TYPES?.GRP_AVCHATROOM)
) {
contactMoreInfoConfig.setWords.data = ''
contactInfoMoreList.value.push(contactMoreInfoConfig.setWords)
}
if (!isGroup.value && !isInBlackList.value) {
contactMoreInfoConfig.setRemark.data =
(contactInfoData.value as Friend)?.remark || ''
contactMoreInfoConfig.setRemark.editing = false
contactInfoMoreList.value.push(contactMoreInfoConfig.setRemark)
}
if (!isGroup.value && (isBothFriend.value || isInBlackList.value)) {
contactMoreInfoConfig.blackList.data =
isInBlackList.value || false
contactInfoMoreList.value.push(contactMoreInfoConfig.blackList)
}
} else {
contactMoreInfoConfig.displayWords.data =
(contactInfoData.value as FriendApplication)?.wording || ''
contactInfoMoreList.value.push(contactMoreInfoConfig.displayWords)
}
}
const generateButton = () => {
if (isInBlackList.value) {
return
}
if (isApplication.value) {
if (
(contactInfoData.value as FriendApplication)?.type ===
TUIChatEngine?.TYPES?.SNS_APPLICATION_SENT_TO_ME
) {
contactInfoButtonList?.value?.push(
contactButtonConfig.refuseFriendApplication
)
contactInfoButtonList?.value?.push(
contactButtonConfig.acceptFriendApplication
)
}
} else {
if (isGroup.value && isGroupMember.value) {
switch ((contactInfoData.value as IGroupModel)?.selfInfo?.role) {
case 'Owner':
contactInfoButtonList?.value?.push(
contactButtonConfig.dismissGroup
)
break
default:
contactInfoButtonList?.value?.push(
contactButtonConfig.quitGroup
)
break
}
contactInfoButtonList?.value?.push(
contactButtonConfig.enterGroupConversation
)
} else if (!isGroup.value && isBothFriend.value) {
contactInfoButtonList?.value?.push(
contactButtonConfig.deleteFriend
)
contactInfoButtonList?.value?.push(
contactButtonConfig.enterC2CConversation
)
} else {
if (isGroup.value) {
contactInfoButtonList?.value?.push(
(contactInfoData.value as IGroupModel)?.type ===
TUIChatEngine?.TYPES?.GRP_AVCHATROOM
? contactButtonConfig.joinAVChatGroup
: contactButtonConfig.joinGroup
)
} else {
contactInfoButtonList?.value?.push(
contactButtonConfig.addFriend
)
}
}
}
}
};
function onUserBlacklistUpdated(userBlacklist: IBlackListUserItem[]) {
blackList.value = userBlacklist;
}
function onUserBlacklistUpdated(userBlacklist: IBlackListUserItem[]) {
blackList.value = userBlacklist
}
async function onCurrentContactInfoUpdated(contactInfo: IContactInfoType) {
if (
contactInfoData.value
&& contactInfo
&& JSON.stringify(contactInfoData.value) === JSON.stringify(contactInfo)
async function onCurrentContactInfoUpdated(
contactInfo: IContactInfoType
) {
return;
if (
contactInfoData.value &&
contactInfo &&
JSON.stringify(contactInfoData.value) ===
JSON.stringify(contactInfo)
) {
return
}
resetContactInfoUIData()
// deep clone
contactInfoData.value = deepCopy(contactInfo) || {}
if (
!contactInfoData.value ||
Object.keys(contactInfoData.value)?.length === 0
) {
return
}
contactInfoBasicList.value = generateContactInfoBasic(
contactInfoData.value
)
isBothFriend.value = await isFriend(contactInfoData.value)
generateMoreInfo()
generateButton()
if (contactInfo.infoKeyList) {
contactInfoMoreList.value = contactInfo.infoKeyList.map(
(key: string) => {
return (contactMoreInfoConfig as any)[key]
}
)
}
if (contactInfo.btnKeyList) {
contactInfoButtonList.value = contactInfo.btnKeyList.map(
(key: string) => {
return (contactButtonConfig as any)[key]
}
)
}
}
resetContactInfoUIData();
// deep clone
contactInfoData.value = deepCopy(contactInfo) || {};
if (!contactInfoData.value || Object.keys(contactInfoData.value)?.length === 0) {
return;
}
contactInfoBasicList.value = generateContactInfoBasic(
contactInfoData.value,
);
isBothFriend.value = await isFriend(contactInfoData.value);
generateMoreInfo();
generateButton();
if (contactInfo.infoKeyList) {
contactInfoMoreList.value = contactInfo.infoKeyList.map((key: string) => {
return (contactMoreInfoConfig as any)[key];
});
}
if (contactInfo.btnKeyList) {
contactInfoButtonList.value = contactInfo.btnKeyList.map((key: string) => {
return (contactButtonConfig as any)[key];
});
}
}
</script>
<style lang="scss" scoped src="./style/index.scss"></style>
<style lang="scss" scoped>
.tui-contact-info-basic-avatar {
border-radius: 100rpx;
}
</style>

View File

@@ -1,28 +1,34 @@
<template>
<div
v-if="Object.keys(friendListData.map).length > 0"
class="friend-list"
>
<ul
v-for="(groupData, groupKey) in friendListData.map"
:key="groupKey"
<view>
<view
v-if="Object.keys(friendListData.map).length > 0"
class="friend-list"
>
<div class="friend-group-title">
{{ groupKey }} ({{ groupData.length }})
</div>
<li
v-for="contactListItem in groupData"
:key="contactListItem.renderKey"
class="friend-item"
@click="enterConversation(contactListItem)"
<ul
v-for="(groupData, groupKey) in friendListData.map"
:key="groupKey"
>
<ContactListItem
<div class="friend-group-title">
{{ groupKey }} ({{ groupData.length }})
</div>
<li
v-for="contactListItem in groupData"
:key="contactListItem.renderKey"
:item="deepCopy(contactListItem)"
/>
</li>
</ul>
</div>
class="friend-item"
@click="enterConversation(contactListItem)"
>
<ContactListItem
:key="contactListItem.renderKey"
:item="deepCopy(contactListItem)"
/>
</li>
</ul>
</view>
<cb-empty
v-if="Object.keys(friendListData.map).length === 0"
name="您还没有好友"
></cb-empty>
</view>
</template>
<script setup lang="ts">
@@ -83,7 +89,7 @@
.friend-group-title {
padding: 8px 16px;
background-color: #f8f9fa;
background-color: #ffffff;
font-size: 14px;
font-weight: 500;
color: #666;

View File

@@ -3,7 +3,7 @@
<div class="tui-contact-list-card-left">
<Avatar
useSkeletonAnimation
size="30px"
size="62rpx"
:url="generateAvatar(props.item)"
/>
<div

View File

@@ -3,9 +3,9 @@
v-if="!contactSearchingStatus"
:class="['tui-contact-list', !isPC && 'tui-contact-list-h5']"
>
<div v-if="!currentContactListKey">
<ul>
<li
<view v-if="!currentContactListKey">
<view class="top-list_box">
<view
v-for="(contactListObj, key) in contactListMap"
:key="key"
class="tui-contact-list-item"
@@ -14,11 +14,11 @@
class="tui-contact-list-item-header"
@click="toggleCurrentContactList(key)"
>
<div class="tui-contact-list-item-header-left">
<view class="tui-contact-list-item-header-left">
<Icon
v-if="contactListObj.icon"
:file="contactListObj.icon"
size="30px"
size="96rpx"
/>
<span
v-if="contactListObj.unreadCount"
@@ -26,25 +26,21 @@
>
{{ contactListObj.unreadCount }}
</span>
</div>
<div class="tui-contact-list-item-header-right">
<div>
{{
TUITranslateService.t(
`TUIContact.${contactListObj.title}`
)
}}
</div>
<Icon
</view>
<view class="tui-contact-list-item-header-right">
<text>
{{ contactListObj.title }}
</text>
<!-- <Icon
:file="currentContactListKey === key ? downSVG : rightSVG"
size="20px"
/>
</div>
/> -->
</view>
</header>
</li>
</ul>
</view>
</view>
<FriendList @enterConversation="selectFriend" />
</div>
</view>
<template v-else>
<li
v-for="contactListItem in contactListMap[currentContactListKey]
@@ -119,9 +115,10 @@
import downSVG from '../../../assets/icon/down-icon.svg'
import rightSVG from '../../../assets/icon/right-icon.svg'
import newContactsSVG from '../../../assets/icon/new-contacts.svg'
import groupSVG from '../../../assets/icon/groups.svg'
import blackListSVG from '../../../assets/icon/black-list.svg'
import newContactsSVG from '../../../assets/icon/friend-request-icon.svg'
import groupSVG from '../../../assets/icon/my-group-chat.svg'
import blackListSVG from '../../../assets/icon/blacklist-icon.svg'
import addFrienIcon from '../../../assets/icon/add-frien-icon.svg'
import type {
IContactList,
@@ -143,10 +140,16 @@
friendApplicationList: {
icon: newContactsSVG,
key: 'friendApplicationList',
title: '新的联系人',
title: '好友请求',
list: [] as FriendApplication[],
unreadCount: 0
},
currentContactSearchingStatus: {
icon: addFrienIcon,
key: 'currentContactSearchingStatus',
title: '添加好友',
list: [] as IGroupModel[]
},
groupList: {
icon: groupSVG,
key: 'groupList',
@@ -233,6 +236,10 @@
})
function toggleCurrentContactList(key: keyof IContactList) {
if (key === 'currentContactSearchingStatus') {
TUIStore.update(StoreName.CUSTOM, key, true)
return
}
if (currentContactListKey.value === key) {
currentContactListKey.value = ''
currentContactInfo.value = {} as IContactInfoType
@@ -465,4 +472,31 @@
height: 100% !important;
overflow: hidden;
}
.tui-contact-search-list-title {
padding: 16rpx 0;
}
.tui-contact-list-item-header {
&::after {
display: none !important;
}
}
.top-list_box {
display: flex;
justify-content: space-between;
padding: 32rpx 50rpx;
.tui-contact-list-item {
.tui-contact-list-item-header {
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
&:active {
background: none !important;
}
}
}
}
</style>

View File

@@ -3,175 +3,179 @@
<div
:class="[
'tui-contact-search-main',
!isPC && 'tui-contact-search-h5-main',
!isPC && 'tui-contact-search-h5-main'
]"
>
<input
v-model="searchValue"
class="tui-contact-search-main-input"
type="text"
:placeholder="searchingPlaceholder"
placeholder="请输入用户 / 群组搜索"
enterkeyhint="search"
@keyup.enter="search"
@blur="search"
@confirm="search"
>
<div
class="tui-contact-search-main-cancel"
@click="cancel"
>
{{ TUITranslateService.t("取消") }}
/>
<div class="tui-contact-search-main-cancel" @click="cancel">
{{ TUITranslateService.t('取消') }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from '../../../adapter-vue';
import {
TUITranslateService,
TUIStore,
StoreName,
} from '@tencentcloud/chat-uikit-engine-lite';
import TUICore, { TUIConstants } from '@tencentcloud/tui-core-lite';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { isPC } from '../../../utils/env';
import { IContactSearchResult } from '../../../interface';
import { onMounted, ref, watch } from '../../../adapter-vue'
import {
TUITranslateService,
TUIStore,
StoreName
} from '@tencentcloud/chat-uikit-engine-lite'
import TUICore, { TUIConstants } from '@tencentcloud/tui-core-lite'
import { TUIGlobal } from '@tencentcloud/universal-api'
import { isPC } from '../../../utils/env'
import { IContactSearchResult } from '../../../interface'
const searchingPlaceholder = TUITranslateService.t('TUIContact.输入ID');
const searchValue = ref<string>('');
const searchResult = ref<IContactSearchResult>({
user: {
label: '联系人',
list: [],
},
group: {
label: '群聊',
list: [],
},
});
const cancel = () => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchingStatus',
false,
);
};
const search = async () => {
if (!searchValue.value) {
return;
}
TUICore.callService({
serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_USER,
params: {
userID: searchValue.value,
const searchValue = ref<string>('')
const searchResult = ref<IContactSearchResult>({
user: {
label: '联系人',
list: []
},
group: {
label: '群聊',
list: []
}
})
.then((res: any) => {
searchResult.value.user.list = res.data;
})
.catch((error: any) => {
searchResult.value.user.list = [];
console.warn('search user error', error);
});
TUICore.callService({
serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_GROUP,
params: {
groupID: searchValue.value,
},
})
.then((res: any) => {
searchResult.value.group.list = [res.data.group];
})
.catch((error: any) => {
searchResult.value.group.list = [];
console.warn('search group error', error);
});
};
watch(
() => searchResult.value,
() => {
const cancel = () => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchResult',
searchResult.value,
);
},
{
deep: true,
immediate: true,
},
);
'currentContactSearchingStatus',
false
)
}
onMounted(() => {
searchValue.value = '';
searchResult.value.user.list = [];
searchResult.value.group.list = [];
});
const search = async () => {
if (!searchValue.value) {
return
}
TUICore.callService({
serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_USER,
params: {
userID: searchValue.value
}
})
.then((res: any) => {
searchResult.value.user.list = res.data
})
.catch((error: any) => {
searchResult.value.user.list = []
console.warn('search user error', error)
})
TUICore.callService({
serviceName: TUIConstants.TUISearch.SERVICE.NAME,
method: TUIConstants.TUISearch.SERVICE.METHOD.SEARCH_GROUP,
params: {
groupID: searchValue.value
}
})
.then((res: any) => {
searchResult.value.group.list = [res.data.group]
})
.catch((error: any) => {
searchResult.value.group.list = []
console.warn('search group error', error)
})
}
watch(
() => searchResult.value,
() => {
TUIStore.update(
StoreName.CUSTOM,
'currentContactSearchResult',
searchResult.value
)
},
{
deep: true,
immediate: true
}
)
TUIGlobal.updateContactSearch = search;
TUIGlobal.closeSearching = () => {
searchValue.value = '';
searchResult.value.user.list = [];
searchResult.value.group.list = [];
};
onMounted(() => {
searchValue.value = ''
searchResult.value.user.list = []
searchResult.value.group.list = []
})
TUIGlobal.updateContactSearch = search
TUIGlobal.closeSearching = () => {
searchValue.value = ''
searchResult.value.user.list = []
searchResult.value.group.list = []
}
</script>
<style lang="scss" scoped>
.tui-contact-search {
position: sticky;
top: 0;
z-index: 1;
padding: 12px;
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
border-bottom: 1px solid #f4f5f9;
flex-direction: column;
&-main {
width: 100%;
height: 30px;
.tui-contact-search {
position: sticky;
top: 0;
z-index: 1;
padding: 12px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
background: #fff;
border-bottom: 1px solid #f4f5f9;
flex-direction: column;
&-main {
display: flex;
flex-direction: row;
flex: 1;
justify-content: center;
align-items: center;
width: 100%;
&-input {
flex: 1;
font-size: 14px;
border-radius: 5px;
padding: 7px;
border: 1px solid #ddd;
}
&-input:focus {
outline: none;
border: 1px solid #006eff;
}
&-cancel {
padding-left: 10px;
user-select: none;
cursor: pointer;
}
}
&-h5 {
&-header {
&-main {
width: 100%;
height: 30px;
display: flex;
flex-direction: row;
align-items: center;
}
&-main {
display: flex;
flex-direction: row;
flex: 1;
justify-content: center;
align-items: center;
width: 100%;
&-input {
flex: 1;
font-size: 14px;
border-radius: 5px;
padding: 7px;
border: 1px solid #ddd;
}
&-input:focus {
outline: none;
border: 1px solid #006eff;
}
&-cancel {
padding-left: 10px;
user-select: none;
cursor: pointer;
}
}
&-h5 {
&-header {
width: 100%;
}
}
}
}
.tui-contact-search-main-input {
border-radius: 64rpx;
height: 64rpx;
padding: 0 32rpx;
background: #f4f4f4;
}
</style>

View File

@@ -4,13 +4,7 @@
v-else-if="isShowContactList"
:class="['tui-contact', !isPC && 'tui-contact-h5']"
>
<Navigation
:title="
currentContactKey
? contactInfoTitle
: '通讯录'
"
>
<Navigation :title="currentContactKey ? contactInfoTitle : '通讯录'">
<template #left>
<div v-show="currentContactKey" @click="resetContactType">
<Icon :file="backSVG" />
@@ -67,7 +61,7 @@
import ContactList from './contact-list/index.vue'
import ContactInfo from './contact-info/index.vue'
import addCircle from '../../assets/icon/add-circle.svg'
import addCircle from '../../assets/icon/add-friend.svg'
import backSVG from '../../assets/icon/back.svg'
import { CONTACT_INFO_TITLE } from '../../constant'

View File

@@ -379,4 +379,12 @@
-ms-user-select: none;
user-select: none;
}
.tui-conversation-item {
&::after {
display: none !important;
}
&:active {
background: #f4f4f4 !important;
}
}
</style>

View File

@@ -53,19 +53,19 @@
<!-- <Icon :file="rightIcon" /> -->
</span>
</div>
<article v-if="!isPC" class="group-h5-list-item-introduction">
<!-- <article v-if="!isPC" class="group-h5-list-item-introduction">
<label class="introduction-name">
{{ groupTypeDetail.label }}
</label>
<span class="introduction-detail">
{{ groupTypeDetail.detail }}
</span>
<!-- <a :href="documentLink.product.url" target="view_window">
<a :href="documentLink.product.url" target="view_window">
{{
TUITranslateService.t(`TUIGroup.${groupTypeDetail.src}`)
}}
</a> -->
</article>
</a>
</article> -->
</li>
</ul>
</ul>
@@ -370,6 +370,9 @@
</script>
<style lang="scss" scoped src="./style/index.scss"></style>
<style lang="scss" scoped>
.group-list {
margin: 0 !important;
}
.popup-content {
display: flex;
flex-direction: column;

View File

@@ -5,7 +5,7 @@
'tui-search',
!isPC && 'tui-search-h5',
`tui-search-main-${currentSearchType}`,
isFullScreen && 'tui-search-h5-full-screen',
isFullScreen && 'tui-search-h5-full-screen'
]"
>
<div
@@ -16,7 +16,7 @@
<div
:class="[
'tui-search-global-header',
!isPC && 'tui-search-h5-global-header',
!isPC && 'tui-search-h5-global-header'
]"
>
<SearchInput
@@ -40,12 +40,13 @@
</div>
<div
v-else-if="
(currentSearchType === 'conversation' && isShowInConversationSearch) ||
isUniFrameWork
(currentSearchType === 'conversation' &&
isShowInConversationSearch) ||
isUniFrameWork
"
:class="[
'tui-search-conversation',
!isPC && 'tui-search-h5-conversation',
!isPC && 'tui-search-h5-conversation'
]"
>
<SearchContainer
@@ -55,9 +56,7 @@
@closeInConversationSearch="closeInConversationSearch"
>
<template #input>
<SearchInput
:searchType="currentSearchType"
/>
<SearchInput :searchType="currentSearchType" />
</template>
<template #result>
<SearchResult
@@ -70,149 +69,176 @@
</div>
</template>
<script lang="ts" setup>
import {
ref,
onMounted,
computed,
withDefaults,
onUnmounted,
watch,
} from '../../adapter-vue';
import { TUIStore, StoreName } from '@tencentcloud/chat-uikit-engine-lite';
import { TUIGlobal, outsideClick } from '@tencentcloud/universal-api';
import SearchInput from './search-input/index.vue';
import SearchContainer from './search-container/index.vue';
import SearchResult from './search-result/index.vue';
import { searchMessageTypeDefault } from './search-type-list';
import { searchMessageTimeDefault } from './search-time-list';
import { isPC, isUniFrameWork } from '../../utils/env';
import { ISearchingStatus, SEARCH_TYPE } from './type';
import {
ref,
onMounted,
computed,
withDefaults,
onUnmounted,
watch
} from '../../adapter-vue'
import {
TUIStore,
StoreName
} from '@tencentcloud/chat-uikit-engine-lite'
import { TUIGlobal, outsideClick } from '@tencentcloud/universal-api'
import SearchInput from './search-input/index.vue'
import SearchContainer from './search-container/index.vue'
import SearchResult from './search-result/index.vue'
import { searchMessageTypeDefault } from './search-type-list'
import { searchMessageTimeDefault } from './search-time-list'
import { isPC, isUniFrameWork } from '../../utils/env'
import { ISearchingStatus, SEARCH_TYPE } from './type'
const props = withDefaults(
defineProps<{
searchType?: SEARCH_TYPE;
}>(),
{
searchType: () => {
return 'global';
},
},
);
const globalSearchRef = ref<HTMLElement | null>();
const currentConversationID = ref<string>('');
const searchingStatus = ref<boolean>(false);
const currentSearchType = ref<SEARCH_TYPE>('global');
const isShowSearch = ref<boolean>(false);
// Whether to display the search in the chat
const isShowInConversationSearch = ref<boolean>(isUniFrameWork);
// Whether to search in full screen - Search in full screen when the mobile terminal is searching
const isFullScreen = computed(
() =>
!isPC
&& ((currentSearchType.value === 'global' && searchingStatus.value)
|| (currentSearchType.value === 'conversation' && isShowInConversationSearch.value)),
);
watch(() => [currentConversationID.value, isShowInConversationSearch.value], (data) => {
if (isUniFrameWork && data[0]) {
currentSearchType.value = 'conversation';
} else {
currentSearchType.value = props.searchType;
}
isShowSearch.value = currentSearchType.value === 'global'
|| ((currentSearchType.value === 'conversation' || (!currentSearchType.value && isUniFrameWork))
&& !!data[1]);
}, { immediate: true, deep: true });
const initSearchValue = (searchType: SEARCH_TYPE) => {
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
value: '',
searchType: searchType,
});
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageType', {
value: searchMessageTypeDefault[searchType],
searchType: searchType,
});
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageTime', {
value: searchMessageTimeDefault,
searchType: searchType,
});
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
isSearching: false,
searchType: currentSearchType.value,
});
};
function onCurrentConversationIDUpdate(conversationID: string) {
if (!isUniFrameWork && currentConversationID.value !== conversationID) {
// PC side single page switch session, close search
closeInConversationSearch();
}
if (!conversationID && isUniFrameWork) {
initSearchValue('global');
}
currentConversationID.value = conversationID;
}
function onCurrentSearchingStatusChange(value: ISearchingStatus) {
if (value?.searchType === currentSearchType.value) {
searchingStatus.value = value?.isSearching;
// global search ui bind on click outside close
if (value?.searchType === 'global' && globalSearchRef.value) {
if (isPC && value.isSearching) {
outsideClick.listen({
domRefs: globalSearchRef.value,
handler: closeGlobalSearch,
});
const props = withDefaults(
defineProps<{
searchType?: SEARCH_TYPE
}>(),
{
searchType: () => {
return 'global'
}
}
if (value?.searchType === 'global' && isUniFrameWork) {
// hide tab bar in uni-app when global searching
value.isSearching ? TUIGlobal?.hideTabBar()?.catch(() => { /* ignore */ }) : TUIGlobal?.showTabBar()?.catch(() => { /* ignore */ });
)
const globalSearchRef = ref<HTMLElement | null>()
const currentConversationID = ref<string>('')
const searchingStatus = ref<boolean>(false)
const currentSearchType = ref<SEARCH_TYPE>('global')
const isShowSearch = ref<boolean>(false)
// Whether to display the search in the chat
const isShowInConversationSearch = ref<boolean>(isUniFrameWork)
// Whether to search in full screen - Search in full screen when the mobile terminal is searching
const isFullScreen = computed(
() =>
!isPC &&
((currentSearchType.value === 'global' && searchingStatus.value) ||
(currentSearchType.value === 'conversation' &&
isShowInConversationSearch.value))
)
watch(
() => [currentConversationID.value, isShowInConversationSearch.value],
data => {
if (isUniFrameWork && data[0]) {
currentSearchType.value = 'conversation'
} else {
currentSearchType.value = props.searchType
}
isShowSearch.value =
currentSearchType.value === 'global' ||
((currentSearchType.value === 'conversation' ||
(!currentSearchType.value && isUniFrameWork)) &&
!!data[1])
},
{ immediate: true, deep: true }
)
const initSearchValue = (searchType: SEARCH_TYPE) => {
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
value: '',
searchType: searchType
})
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageType', {
value: searchMessageTypeDefault[searchType],
searchType: searchType
})
TUIStore.update(StoreName.SEARCH, 'currentSearchMessageTime', {
value: searchMessageTimeDefault,
searchType: searchType
})
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
isSearching: false,
searchType: currentSearchType.value
})
}
function onCurrentConversationIDUpdate(conversationID: string) {
if (
!isUniFrameWork &&
currentConversationID.value !== conversationID
) {
// PC side single page switch session, close search
closeInConversationSearch()
}
if (!conversationID && isUniFrameWork) {
initSearchValue('global')
}
currentConversationID.value = conversationID
}
function onCurrentSearchingStatusChange(value: ISearchingStatus) {
if (value?.searchType === currentSearchType.value) {
searchingStatus.value = value?.isSearching
// global search ui bind on click outside close
if (value?.searchType === 'global' && globalSearchRef.value) {
if (isPC && value.isSearching) {
outsideClick.listen({
domRefs: globalSearchRef.value,
handler: closeGlobalSearch
})
}
}
if (value?.searchType === 'global' && isUniFrameWork) {
// hide tab bar in uni-app when global searching
value.isSearching
? TUIGlobal?.hideTabBar()?.catch(() => {
/* ignore */
})
: TUIGlobal?.showTabBar()?.catch(() => {
/* ignore */
})
}
}
}
}
function onIsShowInConversationSearchChange(value: boolean) {
isShowInConversationSearch.value = value ? true : false;
isShowInConversationSearch.value && initSearchValue(currentSearchType.value);
}
function onIsShowInConversationSearchChange(value: boolean) {
isShowInConversationSearch.value = value ? true : false
isShowInConversationSearch.value &&
initSearchValue(currentSearchType.value)
}
onMounted(() => {
// init with default value
['global', 'conversation'].forEach((type: string) => {
initSearchValue(type as SEARCH_TYPE);
});
// watch store change
TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdate,
});
TUIStore.watch(StoreName.SEARCH, {
currentSearchingStatus: onCurrentSearchingStatusChange,
isShowInConversationSearch: onIsShowInConversationSearchChange,
});
});
onMounted(() => {
// init with default value
;['global', 'conversation'].forEach((type: string) => {
initSearchValue(type as SEARCH_TYPE)
})
// watch store change
TUIStore.watch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdate
})
TUIStore.watch(StoreName.SEARCH, {
currentSearchingStatus: onCurrentSearchingStatusChange,
isShowInConversationSearch: onIsShowInConversationSearchChange
})
})
onUnmounted(() => {
// unwatch store change
TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdate,
});
TUIStore.unwatch(StoreName.SEARCH, {
currentSearchingStatus: onCurrentSearchingStatusChange,
isShowInConversationSearch: onIsShowInConversationSearchChange,
});
});
onUnmounted(() => {
// unwatch store change
TUIStore.unwatch(StoreName.CONV, {
currentConversationID: onCurrentConversationIDUpdate
})
TUIStore.unwatch(StoreName.SEARCH, {
currentSearchingStatus: onCurrentSearchingStatusChange,
isShowInConversationSearch: onIsShowInConversationSearchChange
})
})
function closeGlobalSearch() {
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
isSearching: false,
searchType: currentSearchType.value,
});
}
function closeGlobalSearch() {
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
isSearching: false,
searchType: currentSearchType.value
})
}
function closeInConversationSearch() {
TUIStore.update(StoreName.SEARCH, 'isShowInConversationSearch', false);
}
function closeInConversationSearch() {
TUIStore.update(StoreName.SEARCH, 'isShowInConversationSearch', false)
}
</script>
<style lang="scss" scoped src="./style/index.scss"></style>
<style lang="scss" scoped>
.tui-search-global {
// background: red;
padding: 0 !important;
}
</style>

View File

@@ -12,10 +12,7 @@
v-if="!searchingStatus && props.searchType === 'global'"
:class="['tui-search-input', !isPC && 'tui-search-input-h5']"
>
<div
class="tui-search-input-place"
@click="onSearchInputClick"
>
<div class="tui-search-input-place" @click="onSearchInputClick">
<Icon
class="icon"
:file="searchIcon"
@@ -47,200 +44,197 @@
@blur="onBlur"
@keyup.enter="search"
@confirm="search"
>
/>
<div
v-if="searchingStatus"
class="tui-search-input-right"
@click="endSearching"
>
<Icon
class="icon"
:file="closeIcon"
width="14px"
height="14px"
/>
<Icon class="icon" :file="closeIcon" width="14px" height="14px" />
</div>
</div>
<div
v-if="!isPC && searchingStatus && props.searchType === 'global'"
:class="[
'tui-search-input-cancel',
!isPC && 'tui-search-input-h5-cancel',
!isPC && 'tui-search-input-h5-cancel'
]"
@click="endSearching"
>
{{ TUITranslateService.t("TUISearch.取消") }}
{{ TUITranslateService.t('TUISearch.取消') }}
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../adapter-vue';
import {
TUIStore,
StoreName,
TUITranslateService,
} from '@tencentcloud/chat-uikit-engine-lite';
import { TUIGlobal } from '@tencentcloud/universal-api';
import Icon from '../../common/Icon.vue';
import searchIcon from '../../../assets/icon/search.svg';
import closeIcon from '../../../assets/icon/input-close.svg';
import { isPC } from '../../../utils/env';
import { ISearchInputValue, ISearchingStatus } from '../type';
const props = defineProps({
placeholder: {
type: String,
default: () => TUITranslateService.t('TUISearch.搜索'),
},
searchType: {
type: String,
default: 'global', // "global" / "conversation"
validator(value: string) {
return ['global', 'conversation'].includes(value);
import { ref, onMounted, onUnmounted } from '../../../adapter-vue'
import {
TUIStore,
StoreName,
TUITranslateService
} from '@tencentcloud/chat-uikit-engine-lite'
import { TUIGlobal } from '@tencentcloud/universal-api'
import Icon from '../../common/Icon.vue'
import searchIcon from '../../../assets/icon/search.svg'
import closeIcon from '../../../assets/icon/input-close.svg'
import { isPC } from '../../../utils/env'
import { ISearchInputValue, ISearchingStatus } from '../type'
const props = defineProps({
placeholder: {
type: String,
default: () => TUITranslateService.t('TUISearch.搜索')
},
},
});
searchType: {
type: String,
default: 'global', // "global" / "conversation"
validator(value: string) {
return ['global', 'conversation'].includes(value)
}
}
})
const searchValueModel = ref<string>('');
const currentSearchInputValue = ref<string>('');
const searchingStatus = ref<boolean>(false);
const searchValueModel = ref<string>('')
const currentSearchInputValue = ref<string>('')
const searchingStatus = ref<boolean>(false)
function onCurrentSearchInputValueChange(obj: ISearchInputValue) {
if (obj?.searchType === props?.searchType) {
currentSearchInputValue.value = obj?.value;
searchValueModel.value = obj?.value;
function onCurrentSearchInputValueChange(obj: ISearchInputValue) {
if (obj?.searchType === props?.searchType) {
currentSearchInputValue.value = obj?.value
searchValueModel.value = obj?.value
}
}
}
function onCurrentSearchingStatusChange(obj: ISearchingStatus) {
if (obj?.searchType === props?.searchType) {
searchingStatus.value = obj?.isSearching;
function onCurrentSearchingStatusChange(obj: ISearchingStatus) {
if (obj?.searchType === props?.searchType) {
searchingStatus.value = obj?.isSearching
}
}
}
onMounted(() => {
TUIStore.watch(StoreName.SEARCH, {
currentSearchInputValue: onCurrentSearchInputValueChange,
currentSearchingStatus: onCurrentSearchingStatusChange,
});
});
onMounted(() => {
TUIStore.watch(StoreName.SEARCH, {
currentSearchInputValue: onCurrentSearchInputValueChange,
currentSearchingStatus: onCurrentSearchingStatusChange
})
})
onUnmounted(() => {
TUIStore.unwatch(StoreName.SEARCH, {
currentSearchInputValue: onCurrentSearchInputValueChange,
currentSearchingStatus: onCurrentSearchingStatusChange,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.SEARCH, {
currentSearchInputValue: onCurrentSearchInputValueChange,
currentSearchingStatus: onCurrentSearchingStatusChange
})
})
const search = () => {
// Avoid duplicate searches
if (searchValueModel.value === currentSearchInputValue.value) {
return;
const search = () => {
// Avoid duplicate searches
if (searchValueModel.value === currentSearchInputValue.value) {
return
}
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
value: searchValueModel.value,
searchType: props.searchType
})
}
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
value: searchValueModel.value,
searchType: props.searchType,
});
};
const endSearching = () => {
searchingStatus.value = false;
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
isSearching: false,
searchType: props.searchType,
});
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
value: '',
searchType: props.searchType,
});
};
const endSearching = () => {
searchingStatus.value = false
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
isSearching: false,
searchType: props.searchType
})
TUIStore.update(StoreName.SEARCH, 'currentSearchInputValue', {
value: '',
searchType: props.searchType
})
}
const onSearchInputClick = () => {
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
isSearching: true,
searchType: props.searchType,
});
};
const onSearchInputClick = () => {
TUIStore.update(StoreName.SEARCH, 'currentSearchingStatus', {
isSearching: true,
searchType: props.searchType
})
}
const onBlur = () => {
TUIGlobal?.hideKeyboard?.();
};
const onBlur = () => {
TUIGlobal?.hideKeyboard?.()
}
</script>
<style lang="scss" scoped>
.tui-search-input-container {
display: flex;
flex-direction: row;
box-sizing: border-box;
border-radius: 8px;
padding: 0 2px;
&-global {
flex: 1;
}
.tui-search-input {
flex: 1;
.tui-search-input-container {
display: flex;
flex-direction: row;
margin: 10px;
background: #FEFEFE;
justify-content: center;
align-items: center;
height: 28px;
box-sizing: border-box;
border-radius: 8px;
&-main {
padding: 0 2px;
&-global {
flex: 1;
background: transparent;
border: none;
caret-color: #007aff;
font-size: 14px;
&:focus {
border: none;
outline: none;
}
&::placeholder {
color: #666;
font-size: 12px;
}
}
&-left,
&-right {
.tui-search-input {
flex: 1;
display: flex;
width: 14px;
height: 14px;
padding: 0 7px;
flex-direction: row;
margin: 0 26rpx;
margin-top: 12rpx;
background: #F4F4F4;
justify-content: center;
align-items: center;
height: 64rpx;
border-radius: 64rpx;
margin-bottom: 12rpx;
&-main {
flex: 1;
background: transparent;
border: none;
caret-color: #007aff;
font-size: 14px;
&:focus {
border: none;
outline: none;
}
&::placeholder {
color: #666;
font-size: 12px;
}
}
&-left,
&-right {
display: flex;
width: 14px;
height: 14px;
padding: 0 7px;
}
}
.tui-search-input-place {
flex: 1;
display: flex;
gap: 5px;
justify-content: flex-start;
align-items: center;
font-family: PingFang SC;
font-weight: 400;
color: #bbbbbb;
padding-left: 32rpx;
}
}
.tui-search-input-place {
flex: 1;
display: flex;
gap: 5px;
justify-content: center;
align-items: center;
font-family: PingFang SC;
font-weight: 400;
color: #BBBBBB;
}
}
.tui-search-input-container-h5 {
.tui-search-input-h5 {
height: 40px;
}
.tui-search-input-container-h5 {
.tui-search-input-h5 {
height: 40px;
.tui-search-input-cancel {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #007aff;
font-size: 16px;
padding: 7px 10px 7px 3px;
font-family: 'PingFang SC', sans-serif;
}
}
.tui-search-input-cancel {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #007aff;
font-size: 16px;
padding: 7px 10px 7px 3px;
font-family: "PingFang SC", sans-serif;
}
}
</style>

View File

@@ -1,5 +1,5 @@
.tui-search {
background: #EBF0F6;
background: #fff;
&-main-global {
width: 100%;
@@ -14,7 +14,7 @@
}
.tui-search-global {
padding-bottom: 10px;
padding-bottom: 32rpx;
&-header {
display: flex;
flex-direction: row;

View File

@@ -4,7 +4,7 @@
:style="{
width: avatarSize,
height: avatarSize,
borderRadius: avatarBorderRadius,
borderRadius: '80rpx'
}"
>
<template v-if="isUniFrameWork">
@@ -29,7 +29,7 @@
:src="avatarImageUrl || defaultAvatarUrl"
@load="avatarLoadSuccess"
@error="avatarLoadFailed"
>
/>
<div
v-if="useAvatarSkeletonAnimation && !isImgLoaded"
:class="{
@@ -42,106 +42,107 @@
</template>
<script setup lang="ts">
import { ref, toRefs } from '../../../adapter-vue';
import { isUniFrameWork } from '../../../utils/env';
import { ref, toRefs } from '../../../adapter-vue'
import { isUniFrameWork } from '../../../utils/env'
interface IProps {
url: string;
size?: string;
borderRadius?: string;
useSkeletonAnimation?: boolean;
}
interface IEmits {
(key: 'onLoad', e: Event): void;
(key: 'onError', e: Event): void;
}
const defaultAvatarUrl = ref('https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png');
const emits = defineEmits<IEmits>();
const props = withDefaults(defineProps<IProps>(), {
// uniapp vue2 does not support constants in defineProps
url: 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png',
size: '36px',
borderRadius: '5px',
useSkeletonAnimation: false,
});
const {
size: avatarSize,
url: avatarImageUrl,
borderRadius: avatarBorderRadius,
useSkeletonAnimation: useAvatarSkeletonAnimation,
} = toRefs(props);
let reloadAvatarTime = 0;
const isImgLoaded = ref<boolean>(false);
const loadErrorInUniapp = ref<boolean>(false);
function avatarLoadSuccess(e: Event) {
isImgLoaded.value = true;
emits('onLoad', e);
}
function avatarLoadFailed(e: Event) {
reloadAvatarTime += 1;
if (reloadAvatarTime > 3) {
return;
interface IProps {
url: string
size?: string
borderRadius?: string
useSkeletonAnimation?: boolean
}
if (isUniFrameWork) {
loadErrorInUniapp.value = true;
} else {
(e.currentTarget as HTMLImageElement).src = defaultAvatarUrl.value;
interface IEmits {
(key: 'onLoad', e: Event): void
(key: 'onError', e: Event): void
}
const defaultAvatarUrl = ref(
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
)
const emits = defineEmits<IEmits>()
const props = withDefaults(defineProps<IProps>(), {
// uniapp vue2 does not support constants in defineProps
url: 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png',
size: '36px',
borderRadius: '5px',
useSkeletonAnimation: false
})
const {
size: avatarSize,
url: avatarImageUrl,
borderRadius: avatarBorderRadius,
useSkeletonAnimation: useAvatarSkeletonAnimation
} = toRefs(props)
let reloadAvatarTime = 0
const isImgLoaded = ref<boolean>(false)
const loadErrorInUniapp = ref<boolean>(false)
function avatarLoadSuccess(e: Event) {
isImgLoaded.value = true
emits('onLoad', e)
}
function avatarLoadFailed(e: Event) {
reloadAvatarTime += 1
if (reloadAvatarTime > 3) {
return
}
if (isUniFrameWork) {
loadErrorInUniapp.value = true
} else {
;(e.currentTarget as HTMLImageElement).src = defaultAvatarUrl.value
}
emits('onError', e)
}
emits('onError', e);
}
</script>
<style scoped lang="scss">
:not(not) {
display: flex;
flex-direction: column;
box-sizing: border-box;
min-width: 0;
}
:not(not) {
display: flex;
flex-direction: column;
box-sizing: border-box;
min-width: 0;
}
.avatar-container {
position: relative;
justify-content: center;
align-items: center;
overflow: hidden;
flex: 0 0 auto;
.avatar-container {
position: relative;
justify-content: center;
align-items: center;
overflow: hidden;
flex: 0 0 auto;
.placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #ececec;
transition:
opacity 0.3s,
background-color 0.1s ease-out;
.placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #ececec;
transition: opacity 0.3s, background-color 0.1s ease-out;
&.skeleton-animation {
animation: breath 2s linear 0.3s infinite;
}
&.skeleton-animation {
animation: breath 2s linear 0.3s infinite;
}
&.hidden {
opacity: 0;
&.hidden {
opacity: 0;
}
}
}
.avatar-image {
width: 100%;
height: 100%;
width: 80rpx !important;
height: 80rpx !important;
border-radius: 80rpx !important;
}
}
@keyframes breath {
50% {
/* stylelint-disable-next-line scss/no-global-function-names */
background-color: darken(#ececec, 10%);
@keyframes breath {
50% {
/* stylelint-disable-next-line scss/no-global-function-names */
background-color: darken(#ececec, 10%);
}
}
}
</style>

View File

@@ -9,10 +9,7 @@
<slot name="left" />
</div>
<div
v-if="title"
class="tui-navigation-title"
>
<div v-if="title" class="tui-navigation-title">
<h1 class="tui-navigation-title-text">
{{ title }}
</h1>
@@ -25,82 +22,85 @@
</template>
<script lang="ts" setup>
import { ref, onMounted } from '../../../adapter-vue';
import { isH5 } from '../../../utils/env';
import { ref, onMounted } from '../../../adapter-vue'
import { isH5 } from '../../../utils/env'
interface Props {
title?: string;
customStyle?: string;
}
const props = withDefaults(defineProps<Props>(), {
title: '',
customStyle: '',
});
const statusBarHeight = ref<number>(0);
onMounted(() => {
if (isH5) {
statusBarHeight.value = 0;
} else {
const sysInfo = uni.getSystemInfoSync();
statusBarHeight.value = sysInfo.statusBarHeight;
interface Props {
title?: string
customStyle?: string
}
});
const props = withDefaults(defineProps<Props>(), {
title: '',
customStyle: ''
})
const statusBarHeight = ref<number>(0)
onMounted(() => {
if (isH5) {
statusBarHeight.value = 0
} else {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight
}
})
</script>
<style lang="scss" scoped>
.tui-navigation {
display: flex;
flex-direction: row;
align-items: center;
background: #EBF0F6;
min-height: 44px;
padding: 0 12px;
&-left {
.tui-navigation {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 8px;
flex: 1;
}
// background: #ebf0f6;
background: #fff;
min-height: 44px;
padding: 0 12px;
border-bottom: 2rpx solid #0000000a;
box-sizing: border-box;
&-title {
flex: 10;
text-align: center;
min-width: 0;
&-left {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
flex: 1;
}
&-text {
overflow: hidden;
word-break: keep-all;
text-overflow: ellipsis;
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
&-title {
flex: 10;
text-align: center;
min-width: 0;
&-text {
overflow: hidden;
word-break: keep-all;
text-overflow: ellipsis;
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
}
&-right {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex: 1;
}
&-back-btn {
display: flex;
align-items: center;
cursor: pointer;
opacity: 0.8;
}
&-back-text {
font-size: 16px;
color: #007aff;
}
}
&-right {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex: 1;
}
&-back-btn {
display: flex;
align-items: center;
cursor: pointer;
opacity: 0.8;
}
&-back-text {
font-size: 16px;
color: #007AFF;
}
}
</style>

View File

@@ -1,16 +1,16 @@
<template>
<div
class="transfer"
:class="[!isPC ? 'transfer-h5' : '', isWeChat ? 'transfer-h5-wechat' : '']"
:class="[
!isPC ? 'transfer-h5' : '',
isWeChat ? 'transfer-h5-wechat' : ''
]"
>
<header
v-if="!isPC && transferTitle"
class="transfer-header transfer-h5-header"
>
<div
v-if="!props.isHiddenBackIcon"
@click="cancel"
>
<div v-if="!props.isHiddenBackIcon" @click="cancel">
<Icon
class="icon"
:file="backIcon"
@@ -29,22 +29,22 @@
v-if="isPC && isTransferSearch"
type="text"
:value="searchValue"
:placeholder="TUITranslateService.t('component.请输入userID')"
placeholder="搜素"
enterkeyhint="search"
:class="[isUniFrameWork ? 'left-uniapp-input' : '']"
@keyup.enter="handleInput"
>
/>
<!-- not PC triggers blur -->
<input
v-if="!isPC && isTransferSearch"
type="text"
:placeholder="TUITranslateService.t('component.请输入userID')"
placeholder="搜素"
enterkeyhint="search"
:value="searchValue"
:class="[isUniFrameWork ? 'left-uniapp-input' : '']"
@blur="handleInput"
@confirm="handleInput"
>
/>
</header>
<main class="transfer-left-main">
<ul class="transfer-list">
@@ -59,13 +59,10 @@
:width="'18px'"
:height="'18px'"
/>
<i
v-else
class="icon-unselected"
/>
<span class="select-all">{{
TUITranslateService.t("component.全选")
}}</span>
<i v-else class="icon-unselected" />
<span class="select-all">
{{ TUITranslateService.t('component.全选') }}
</span>
</li>
<li
v-for="item in transferList"
@@ -82,25 +79,27 @@
/>
<i
v-else
:class="[item.isDisabled && 'disabled', 'icon-unselected']"
:class="[
item.isDisabled && 'disabled',
'icon-unselected'
]"
/>
<template v-if="!isTransferCustomItem">
<img
class="avatar"
:src="
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
'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 v-if="item.isDisabled">{{ TUITranslateService.t("component.已在群中") }}</span>
<span v-if="item.isDisabled">
{{ TUITranslateService.t('component.已在群中') }}
</span>
</template>
<template v-else>
<slot
name="left"
:data="item"
/>
<slot name="left" :data="item" />
</template>
</li>
<li
@@ -108,29 +107,23 @@
class="transfer-list-item more"
@click="getMore"
>
{{ TUITranslateService.t("component.查看更多") }}
{{ TUITranslateService.t('component.查看更多') }}
</li>
</ul>
</main>
</div>
<div class="right">
<header
v-if="isPC"
class="transfer-header"
>
<header v-if="isPC" class="transfer-header">
{{ transferTitle }}
</header>
<ul
v-if="resultShow"
class="transfer-list"
>
<ul v-if="resultShow" class="transfer-list">
<p
v-if="transferSelectedList.length > 0 && isPC"
class="transfer-text"
>
{{ TUITranslateService.t("component.已选中")
{{ TUITranslateService.t('component.已选中')
}}{{ transferSelectedList.length
}}{{ TUITranslateService.t("component.人") }}
}}{{ TUITranslateService.t('component.人') }}
</p>
<li
v-for="(item, index) in transferSelectedList"
@@ -143,54 +136,36 @@
class="avatar"
:src="
item.avatar ||
'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
'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
v-if="isPC"
class="name"
>{{ item.nick || item.userID }}</span>
/>
<span v-if="isPC" class="name">
{{ item.nick || item.userID }}
</span>
</template>
<template v-else>
<slot
name="right"
:data="item"
/>
<slot name="right" :data="item" />
</template>
</aside>
<span
v-if="isPC"
@click="selected(item)"
>
<Icon
:file="cancelIcon"
:width="'18px'"
:height="'18px'"
/>
<span v-if="isPC" @click="selected(item)">
<Icon :file="cancelIcon" :width="'18px'" :height="'18px'" />
</span>
</li>
</ul>
<footer class="transfer-right-footer">
<button
class="btn btn-cancel"
@click="cancel"
>
{{ TUITranslateService.t("component.取消") }}
<button class="btn btn-cancel" @click="cancel">
{{ TUITranslateService.t('component.取消') }}
</button>
<button
v-if="transferSelectedList.length > 0"
class="btn"
class="btn btn-confirm"
@click="submit"
>
{{ TUITranslateService.t("component.完成") }}
{{ TUITranslateService.t('component.完成') }}
</button>
<button
v-else
class="btn btn-no"
@click="submit"
>
{{ TUITranslateService.t("component.完成") }}
<button v-else class="btn btn-no" @click="submit">
{{ TUITranslateService.t('component.完成') }}
</button>
</footer>
</div>
@@ -199,134 +174,175 @@
</template>
<script lang="ts" setup>
import { ref, watchEffect, computed } from '../../../adapter-vue';
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine-lite';
import { ITransferListItem } from '../../../interface';
import Icon from '../Icon.vue';
import selectedIcon from '../../../assets/icon/selected.svg';
import backIcon from '../../../assets/icon/back.svg';
import cancelIcon from '../../../assets/icon/cancel.svg';
import { isPC, isUniFrameWork, isWeChat } from '../../../utils/env';
import { ref, watchEffect, computed } from '../../../adapter-vue'
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine-lite'
import { ITransferListItem } from '../../../interface'
import Icon from '../Icon.vue'
import selectedIcon from '../../../assets/icon/selected.svg'
import backIcon from '../../../assets/icon/back.svg'
import cancelIcon from '../../../assets/icon/cancel.svg'
import searchIcon from '../../../assets/icon/search.svg'
import { isPC, isUniFrameWork, isWeChat } from '../../../utils/env'
const props = defineProps({
list: {
type: Array,
default: () => [],
},
selectedList: {
type: Array,
default: () => [],
},
isSearch: {
type: Boolean,
default: true,
},
isRadio: {
type: Boolean,
default: false,
},
isCustomItem: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
type: {
type: String,
default: '',
},
resultShow: {
type: Boolean,
default: true,
},
total: {
type: Number,
default: 0,
},
isHiddenBackIcon: {
type: Boolean,
default: false,
},
});
const transferList = ref<ITransferListItem[]>([]);
const transferTotal = ref<number>(0);
const transferSelectedList = ref<ITransferListItem[]>([]);
const isTransferSearch = ref(true);
const isTransferCustomItem = ref(false);
const transferTitle = ref('');
const searchValue = ref('');
watchEffect(() => {
if (props.isCustomItem) {
for (let index = 0; index < props.list.length; index++) {
if (
(props.list[index] as any).conversationID.indexOf('@TIM#SYSTEM') > -1
) {
// eslint-disable-next-line vue/no-mutating-props
props.list.splice(index, 1);
}
transferList.value = props.list as ITransferListItem[];
const props = defineProps({
list: {
type: Array,
default: () => []
},
selectedList: {
type: Array,
default: () => []
},
isSearch: {
type: Boolean,
default: true
},
isRadio: {
type: Boolean,
default: false
},
isCustomItem: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
},
type: {
type: String,
default: ''
},
resultShow: {
type: Boolean,
default: true
},
total: {
type: Number,
default: 0
},
isHiddenBackIcon: {
type: Boolean,
default: false
}
} else {
transferList.value = props.list as ITransferListItem[];
})
const transferList = ref<ITransferListItem[]>([])
const transferTotal = ref<number>(0)
const transferSelectedList = ref<ITransferListItem[]>([])
const isTransferSearch = ref(true)
const isTransferCustomItem = ref(false)
const transferTitle = ref('')
const searchValue = ref('')
watchEffect(() => {
if (props.isCustomItem) {
for (let index = 0; index < props.list.length; index++) {
if (
(props.list[index] as any).conversationID.indexOf(
'@TIM#SYSTEM'
) > -1
) {
// eslint-disable-next-line vue/no-mutating-props
props.list.splice(index, 1)
}
transferList.value = props.list as ITransferListItem[]
}
} else {
transferList.value = props.list as ITransferListItem[]
}
transferTotal.value = props.total ? props.total : props.list.length
transferSelectedList.value = (
props.selectedList && props.selectedList.length > 0
? props.selectedList
: transferSelectedList.value
) as any
isTransferSearch.value = props.isSearch
isTransferCustomItem.value = props.isCustomItem
transferTitle.value = props.title
})
const emit = defineEmits(['search', 'submit', 'cancel', 'getMore'])
const optional = computed(() =>
transferList.value.filter((item: any) => !item.isDisabled)
)
const handleInput = (e: any) => {
searchValue.value = e.target.value || e.detail.value
emit('search', searchValue.value)
}
transferTotal.value = props.total ? props.total : props.list.length;
transferSelectedList.value = (props.selectedList && props.selectedList.length > 0 ? props.selectedList : transferSelectedList.value) as any;
isTransferSearch.value = props.isSearch;
isTransferCustomItem.value = props.isCustomItem;
transferTitle.value = props.title;
});
const emit = defineEmits(['search', 'submit', 'cancel', 'getMore']);
const optional = computed(() =>
transferList.value.filter((item: any) => !item.isDisabled),
);
const handleInput = (e: any) => {
searchValue.value = e.target.value || e.detail.value;
emit('search', searchValue.value);
};
const selected = (item: any) => {
if (item.isDisabled) {
return;
const selected = (item: any) => {
if (item.isDisabled) {
return
}
let list: ITransferListItem[] = transferSelectedList.value
const index: number = list.indexOf(item)
if (index > -1) {
return transferSelectedList.value.splice(index, 1)
}
if (props.isRadio) {
list = []
}
list.push(item)
transferSelectedList.value = list
}
let list: ITransferListItem[] = transferSelectedList.value;
const index: number = list.indexOf(item);
if (index > -1) {
return transferSelectedList.value.splice(index, 1);
const selectedAll = () => {
if (transferSelectedList.value.length === optional.value.length) {
transferSelectedList.value = []
} else {
transferSelectedList.value = [...optional.value]
}
}
if (props.isRadio) {
list = [];
const submit = () => {
emit('submit', transferSelectedList.value)
searchValue.value = ''
}
list.push(item);
transferSelectedList.value = list;
};
const selectedAll = () => {
if (transferSelectedList.value.length === optional.value.length) {
transferSelectedList.value = [];
} else {
transferSelectedList.value = [...optional.value];
const cancel = () => {
emit('cancel')
searchValue.value = ''
}
};
const submit = () => {
emit('submit', transferSelectedList.value);
searchValue.value = '';
};
const cancel = () => {
emit('cancel');
searchValue.value = '';
};
const getMore = () => {
emit('getMore');
};
const getMore = () => {
emit('getMore')
}
</script>
<style lang="scss" scoped src="./style/transfer.scss"></style>
<style lang="scss" scoped>
.transfer-left-main {
// background: #f4f4f4;
}
.transfer-header {
padding: 26rpx 32rpx !important;
box-shadow: 0rpx 2rpx 8rpx 0rpx rgba(0, 0, 0, 0.04) !important;
margin-bottom: 12rpx !important;
}
.left-uniapp-input {
border-radius: 80rpx !important;
}
.avatar {
width: 80rpx !important;
height: 80rpx !important;
border-radius: 80rpx !important;
}
.transfer-right-footer {
button {
height: 64rpx;
padding: 0 36rpx;
line-height: 64rpx;
border-radius: 64rpx;
&::after {
display: none !important;
}
}
.btn-confirm {
background: #00D993;
}
}
</style>