消息
This commit is contained in:
2
TUIKit/components/TUIChat/message-input/index.ts
Normal file
2
TUIKit/components/TUIChat/message-input/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import MessageInput from './index.vue';
|
||||
export default MessageInput;
|
||||
241
TUIKit/components/TUIChat/message-input/index.vue
Normal file
241
TUIKit/components/TUIChat/message-input/index.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<template>
|
||||
<div :class="['message-input', !isPC && 'message-input-h5']">
|
||||
<div class="audio-main-content-line">
|
||||
<MessageInputAudio
|
||||
v-if="(isWeChat || isApp) && isRenderVoice"
|
||||
:class="{
|
||||
'message-input-wx-audio-open': displayType === 'audio',
|
||||
}"
|
||||
:isEnableAudio="displayType === 'audio'"
|
||||
@changeDisplayType="changeDisplayType"
|
||||
/>
|
||||
<MessageInputEditor
|
||||
v-show="displayType === 'editor'"
|
||||
ref="editor"
|
||||
class="message-input-editor"
|
||||
:placeholder="props.placeholder"
|
||||
:isMuted="props.isMuted"
|
||||
:muteText="props.muteText"
|
||||
:enableInput="props.enableInput"
|
||||
:enableAt="props.enableAt"
|
||||
:enableTyping="props.enableTyping"
|
||||
:isGroup="isGroup"
|
||||
@onTyping="onTyping"
|
||||
@onAt="onAt"
|
||||
@onFocus="onFocus"
|
||||
/>
|
||||
<MessageInputAt
|
||||
v-if="props.enableAt"
|
||||
ref="messageInputAtRef"
|
||||
@insertAt="insertAt"
|
||||
@onAtListOpen="onAtListOpen"
|
||||
/>
|
||||
<Icon
|
||||
v-if="isRenderEmojiPicker"
|
||||
class="icon icon-face"
|
||||
:file="faceIcon"
|
||||
:size="'23px'"
|
||||
:hotAreaSize="'3px'"
|
||||
@onClick="changeToolbarDisplayType('emojiPicker')"
|
||||
/>
|
||||
<Icon
|
||||
v-if="isRenderMore"
|
||||
class="icon icon-more"
|
||||
:file="moreIcon"
|
||||
:size="'23px'"
|
||||
:hotAreaSize="'3px'"
|
||||
@onClick="changeToolbarDisplayType('tools')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<MessageQuote
|
||||
:style="{minWidth: 0}"
|
||||
:displayType="displayType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import TUIChatEngine, {
|
||||
TUIStore,
|
||||
StoreName,
|
||||
IMessageModel,
|
||||
IConversationModel,
|
||||
} from '@tencentcloud/chat-uikit-engine';
|
||||
import { ref, watch, onMounted, onUnmounted } from '../../../adapter-vue';
|
||||
import MessageInputEditor from './message-input-editor.vue';
|
||||
import MessageInputAt from './message-input-at/index.vue';
|
||||
import MessageInputAudio from './message-input-audio.vue';
|
||||
import MessageQuote from './message-input-quote/index.vue';
|
||||
import Icon from '../../common/Icon.vue';
|
||||
import faceIcon from '../../../assets/icon/face-uni.png';
|
||||
import moreIcon from '../../../assets/icon/more-uni.png';
|
||||
import { isPC, isH5, isWeChat, isApp } from '../../../utils/env';
|
||||
import { sendTyping } from '../utils/sendMessage';
|
||||
import { ToolbarDisplayType, InputDisplayType } from '../../../interface';
|
||||
import TUIChatConfig from '../config';
|
||||
|
||||
interface IProps {
|
||||
placeholder: string;
|
||||
isMuted?: boolean;
|
||||
muteText?: string;
|
||||
enableInput?: boolean;
|
||||
enableAt?: boolean;
|
||||
enableTyping?: boolean;
|
||||
replyOrReference?: Record<string, any>;
|
||||
inputToolbarDisplayType: ToolbarDisplayType;
|
||||
}
|
||||
interface IEmits {
|
||||
(e: 'changeToolbarDisplayType', displayType: ToolbarDisplayType): void;
|
||||
}
|
||||
|
||||
const emits = defineEmits<IEmits>();
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
placeholder: 'this is placeholder',
|
||||
replyOrReference: () => ({}),
|
||||
isMuted: true,
|
||||
muteText: '',
|
||||
enableInput: true,
|
||||
enableAt: true,
|
||||
enableTyping: true,
|
||||
inputToolbarDisplayType: 'none',
|
||||
});
|
||||
|
||||
const editor = ref();
|
||||
const messageInputAtRef = ref();
|
||||
const currentConversation = ref<IConversationModel>();
|
||||
const isGroup = ref<boolean>(false);
|
||||
const displayType = ref<InputDisplayType>('editor');
|
||||
const featureConfig = TUIChatConfig.getFeatureConfig();
|
||||
const isRenderVoice = ref<boolean>(featureConfig.InputVoice);
|
||||
const isRenderEmojiPicker = ref<boolean>(featureConfig.InputEmoji || featureConfig.InputStickers);
|
||||
const isRenderMore = ref<boolean>(featureConfig.InputImage || featureConfig.InputVideo || featureConfig.InputEvaluation || featureConfig.InputQuickReplies);
|
||||
|
||||
onMounted(() => {
|
||||
TUIStore.watch(StoreName.CONV, {
|
||||
currentConversation: onCurrentConversationUpdated,
|
||||
});
|
||||
|
||||
TUIStore.watch(StoreName.CHAT, {
|
||||
quoteMessage: onQuoteMessageUpdated,
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
TUIStore.unwatch(StoreName.CONV, {
|
||||
currentConversation: onCurrentConversationUpdated,
|
||||
});
|
||||
|
||||
TUIStore.unwatch(StoreName.CHAT, {
|
||||
quoteMessage: onQuoteMessageUpdated,
|
||||
});
|
||||
});
|
||||
|
||||
watch(() => props.inputToolbarDisplayType, (newVal: ToolbarDisplayType) => {
|
||||
if (newVal !== 'none') {
|
||||
changeDisplayType('editor');
|
||||
}
|
||||
});
|
||||
|
||||
function changeDisplayType(display: InputDisplayType) {
|
||||
displayType.value = display;
|
||||
if (display === 'audio') {
|
||||
emits('changeToolbarDisplayType', 'none');
|
||||
}
|
||||
}
|
||||
|
||||
function changeToolbarDisplayType(displayType: ToolbarDisplayType) {
|
||||
emits('changeToolbarDisplayType', displayType);
|
||||
}
|
||||
|
||||
const onTyping = (inputContentEmpty: boolean, inputBlur: boolean) => {
|
||||
sendTyping(inputContentEmpty, inputBlur);
|
||||
};
|
||||
|
||||
const onAt = (show: boolean) => {
|
||||
messageInputAtRef?.value?.toggleAtList(show);
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
if (isH5) {
|
||||
emits('changeToolbarDisplayType', 'none');
|
||||
}
|
||||
};
|
||||
|
||||
const insertEmoji = (emoji: any) => {
|
||||
editor?.value?.addEmoji && editor?.value?.addEmoji(emoji);
|
||||
};
|
||||
|
||||
const insertAt = (atInfo: any) => {
|
||||
editor?.value?.insertAt && editor?.value?.insertAt(atInfo);
|
||||
};
|
||||
|
||||
const onAtListOpen = () => {
|
||||
editor?.value?.blur && editor?.value?.blur();
|
||||
};
|
||||
|
||||
const reEdit = (content: any) => {
|
||||
editor?.value?.resetEditor();
|
||||
editor?.value?.setEditorContent(content);
|
||||
};
|
||||
|
||||
function onCurrentConversationUpdated(conversation: IConversationModel) {
|
||||
currentConversation.value = conversation;
|
||||
isGroup.value = currentConversation.value?.type === TUIChatEngine.TYPES.CONV_GROUP;
|
||||
}
|
||||
|
||||
function onQuoteMessageUpdated(options?: { message: IMessageModel; type: string }) {
|
||||
// switch text input mode when there is a quote message
|
||||
if (options?.message && options?.type === 'quote') {
|
||||
changeDisplayType('editor');
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
insertEmoji,
|
||||
reEdit,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../../assets/styles/common";
|
||||
|
||||
:not(not) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
background: #ebf0f6;
|
||||
|
||||
&-h5 {
|
||||
padding: 10px 10px 15px;
|
||||
}
|
||||
|
||||
&-editor {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
&-wx-audio-open {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.audio-main-content-line {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<BottomPopup
|
||||
:show="showAtList"
|
||||
@onClose="closeAt"
|
||||
>
|
||||
<div
|
||||
ref="MessageInputAt"
|
||||
:class="[isPC ? 'message-input-at' : 'message-input-at-h5']"
|
||||
>
|
||||
<div
|
||||
ref="dialog"
|
||||
class="member-list"
|
||||
>
|
||||
<header
|
||||
v-if="!isPC"
|
||||
class="member-list-title"
|
||||
>
|
||||
<span class="title">{{
|
||||
TUITranslateService.t("TUIChat.选择提醒的人")
|
||||
}}</span>
|
||||
</header>
|
||||
<ul class="member-list-box">
|
||||
<li
|
||||
v-for="(item, index) in showMemberList"
|
||||
:key="index"
|
||||
ref="memberListItems"
|
||||
class="member-list-box-body"
|
||||
:class="[index === selectedIndex && 'selected']"
|
||||
@click="selectItem(index)"
|
||||
>
|
||||
<img
|
||||
class="member-list-box-body-avatar"
|
||||
:src="handleMemberAvatar(item)"
|
||||
>
|
||||
<span class="member-list-box-body-name">
|
||||
{{ handleMemberName(item) }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</BottomPopup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import TUIChatEngine, {
|
||||
TUIStore,
|
||||
StoreName,
|
||||
TUIGroupService,
|
||||
TUITranslateService,
|
||||
} from '@tencentcloud/chat-uikit-engine';
|
||||
import { TUIGlobal } from '@tencentcloud/universal-api';
|
||||
import { ref, watch } from '../../../../adapter-vue';
|
||||
import { isPC, isH5 } from '../../../../utils/env';
|
||||
import BottomPopup from '../../../common/BottomPopup/index.vue';
|
||||
|
||||
const emits = defineEmits(['onAtListOpen', 'insertAt']);
|
||||
|
||||
const MessageInputAt = ref();
|
||||
const memberListItems = ref();
|
||||
|
||||
const showAtList = ref(false);
|
||||
const memberList = ref<Array<any>>();
|
||||
const allMemberList = ref<Array<any>>();
|
||||
const showMemberList = ref<Array<any>>();
|
||||
const isGroup = ref(false);
|
||||
const position = ref({
|
||||
left: 0,
|
||||
top: 0,
|
||||
});
|
||||
const selectedIndex = ref(0);
|
||||
const currentConversationID = ref('');
|
||||
|
||||
const all = {
|
||||
userID: TUIChatEngine.TYPES.MSG_AT_ALL,
|
||||
nick: '所有人',
|
||||
isAll: true,
|
||||
avatar: 'https://web.sdk.qcloud.com/im/assets/images/at.svg',
|
||||
};
|
||||
|
||||
TUIStore.watch(StoreName.CONV, {
|
||||
currentConversationID: (id: string) => {
|
||||
if (id !== currentConversationID.value) {
|
||||
currentConversationID.value = id;
|
||||
memberList.value = [];
|
||||
allMemberList.value = [];
|
||||
showMemberList.value = [];
|
||||
isGroup.value = false;
|
||||
TUIStore.update(StoreName.CUSTOM, 'memberList', memberList.value);
|
||||
if (currentConversationID?.value?.startsWith('GROUP')) {
|
||||
isGroup.value = true;
|
||||
const groupID = currentConversationID?.value?.substring(5);
|
||||
TUIGroupService.switchGroup(groupID);
|
||||
} else {
|
||||
TUIGroupService.switchGroup('');
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
TUIStore.watch(StoreName.GRP, {
|
||||
currentGroupMemberList: (list: Array<any>) => {
|
||||
memberList.value = list;
|
||||
allMemberList.value = [all, ...memberList.value];
|
||||
showMemberList.value = allMemberList.value;
|
||||
TUIStore.update(StoreName.CUSTOM, 'memberList', memberList.value);
|
||||
},
|
||||
});
|
||||
|
||||
const toggleAtList = (show: boolean) => {
|
||||
if (!isGroup.value) {
|
||||
return;
|
||||
}
|
||||
showAtList.value = show;
|
||||
if (showAtList.value) {
|
||||
emits('onAtListOpen');
|
||||
}
|
||||
};
|
||||
const handleAtListPosition = (positionData: { top: number; left: number }) => {
|
||||
position.value = positionData;
|
||||
};
|
||||
const setCurrentSelectIndex = (index: any) => {
|
||||
selectedIndex.value = index;
|
||||
memberListItems.value?.[selectedIndex.value]?.scrollIntoView(false);
|
||||
};
|
||||
const setShowMemberList = (list: any) => {
|
||||
showMemberList.value = list;
|
||||
};
|
||||
|
||||
TUIGlobal.toggleAtList = toggleAtList;
|
||||
TUIGlobal.handleAtListPosition = handleAtListPosition;
|
||||
TUIGlobal.setCurrentSelectIndex = setCurrentSelectIndex;
|
||||
TUIGlobal.setShowMemberList = setShowMemberList;
|
||||
|
||||
defineExpose({
|
||||
toggleAtList,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [position.value, MessageInputAt?.value],
|
||||
() => {
|
||||
if (isH5 || !MessageInputAt?.value || !MessageInputAt?.value?.style) {
|
||||
return;
|
||||
}
|
||||
MessageInputAt.value.style.left = position.value.left + 'px';
|
||||
MessageInputAt.value.style.top
|
||||
= position.value.top - MessageInputAt.value.clientHeight + 'px';
|
||||
},
|
||||
);
|
||||
|
||||
const closeAt = () => {
|
||||
showAtList.value = false;
|
||||
showMemberList.value = allMemberList.value;
|
||||
position.value = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
};
|
||||
|
||||
const selectItem = (index: number) => {
|
||||
if (isPC && TUIGlobal.selectItem) {
|
||||
TUIGlobal.selectItem(index);
|
||||
} else {
|
||||
if (showMemberList?.value?.length) {
|
||||
const item = showMemberList?.value[index];
|
||||
emits('insertAt', {
|
||||
id: (item as any)?.userID,
|
||||
label: (item as any)?.nick || (item as any)?.userID,
|
||||
});
|
||||
}
|
||||
}
|
||||
closeAt();
|
||||
};
|
||||
|
||||
const handleMemberAvatar = (item: any) => {
|
||||
return (
|
||||
(item as any)?.avatar
|
||||
|| 'https://web.sdk.qcloud.com/component/TUIKit/assets/avatar_21.png'
|
||||
);
|
||||
};
|
||||
|
||||
const handleMemberName = (item: any) => {
|
||||
return (item as any)?.nick ? (item as any)?.nick : (item as any)?.userID;
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "../../../../assets/styles/common";
|
||||
|
||||
.message-input-at {
|
||||
position: fixed;
|
||||
max-width: 15rem;
|
||||
max-height: 10rem;
|
||||
overflow: hidden auto;
|
||||
background: #fff;
|
||||
box-shadow: 0 0.06rem 0.63rem 0 rgba(2,16,43,0.15);
|
||||
border-radius: 0.13rem;
|
||||
}
|
||||
|
||||
.member-list-box {
|
||||
&-header {
|
||||
height: 2.5rem;
|
||||
padding-top: 5px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0,110,255,0.1);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: PingFangSC-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
letter-spacing: 0;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
&-body {
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.selected,
|
||||
&:hover {
|
||||
background: rgba(0,110,255,0.1);
|
||||
}
|
||||
|
||||
&-name {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&-avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: rgba(0,110,255,0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.message-input-at-h5 {
|
||||
.member-list {
|
||||
height: auto;
|
||||
max-height: 500px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background: white;
|
||||
border-radius: 12px 12px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&-title {
|
||||
height: fit-content;
|
||||
width: calc(100% - 30px);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding: 15px;
|
||||
|
||||
.title {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.close {
|
||||
vertical-align: middle;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&-box {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
|
||||
&-body {
|
||||
padding: 10px;
|
||||
|
||||
img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
340
TUIKit/components/TUIChat/message-input/message-input-audio.vue
Normal file
340
TUIKit/components/TUIChat/message-input/message-input-audio.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'message-input-audio': true,
|
||||
'message-input-audio-open': isAudioTouchBarShow,
|
||||
}"
|
||||
>
|
||||
<Icon
|
||||
class="audio-message-icon"
|
||||
:file="audioIcon"
|
||||
:size="'23px'"
|
||||
:hotAreaSize="'3px'"
|
||||
@onClick="switchAudio"
|
||||
/>
|
||||
<view
|
||||
v-if="props.isEnableAudio"
|
||||
class="audio-input-touch-bar"
|
||||
@touchstart="handleTouchStart"
|
||||
@longpress="handleLongPress"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<span>{{ TUITranslateService.t(`TUIChat.${touchBarText}`) }}</span>
|
||||
<view
|
||||
v-if="isRecording"
|
||||
class="record-modal"
|
||||
>
|
||||
<div class="red-mask" />
|
||||
<view class="float-element moving-slider" />
|
||||
<view class="float-element modal-title">
|
||||
{{ TUITranslateService.t(`TUIChat.${modalText}`) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from '../../../adapter-vue';
|
||||
import {
|
||||
TUIStore,
|
||||
StoreName,
|
||||
TUIChatService,
|
||||
SendMessageParams,
|
||||
IConversationModel,
|
||||
TUITranslateService,
|
||||
} from '@tencentcloud/chat-uikit-engine';
|
||||
import { TUIGlobal } from '@tencentcloud/universal-api';
|
||||
import Icon from '../../common/Icon.vue';
|
||||
import audioIcon from '../../../assets/icon/audio.svg';
|
||||
import { Toast, TOAST_TYPE } from '../../common/Toast/index';
|
||||
import { throttle } from '../../../utils/lodash';
|
||||
import { isEnabledMessageReadReceiptGlobal } from '../utils/utils';
|
||||
import { InputDisplayType } from '../../../interface';
|
||||
|
||||
interface IProps {
|
||||
isEnableAudio: boolean;
|
||||
}
|
||||
|
||||
interface IEmits {
|
||||
(e: 'changeDisplayType', type: InputDisplayType): void;
|
||||
}
|
||||
|
||||
interface RecordResult {
|
||||
tempFilePath: string;
|
||||
duration?: number;
|
||||
fileSize?: number;
|
||||
}
|
||||
|
||||
type TouchBarText = '按住说话' | '抬起发送' | '抬起取消';
|
||||
type ModalText = '正在录音' | '继续上滑可取消' | '松开手指 取消发送';
|
||||
|
||||
const emits = defineEmits<IEmits>();
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
isEnableAudio: false,
|
||||
});
|
||||
|
||||
let recordTime: number = 0;
|
||||
let isManualCancelBySlide = false;
|
||||
let recordTimer: number | undefined;
|
||||
let firstTouchPageY: number = -1;
|
||||
let isFingerTouchingScreen = false;
|
||||
let isFirstAuthrizedRecord = false;
|
||||
const recorderManager = TUIGlobal?.getRecorderManager();
|
||||
|
||||
const isRecording = ref(false);
|
||||
const touchBarText = ref<TouchBarText>('按住说话');
|
||||
const modalText = ref<ModalText>('正在录音');
|
||||
const isAudioTouchBarShow = ref<boolean>(false);
|
||||
const currentConversation = ref<IConversationModel>();
|
||||
|
||||
const recordConfig = {
|
||||
// Duration of the recording, in ms, with a maximum value of 600000 (10 minutes)
|
||||
duration: 60000,
|
||||
// Sampling rate
|
||||
sampleRate: 44100,
|
||||
// Number of recording channels
|
||||
numberOfChannels: 1,
|
||||
// Encoding bit rate
|
||||
encodeBitRate: 192000,
|
||||
// Audio format
|
||||
// Select this format to create audio messages that can be interoperable across all chat platforms (Android, iOS, WeChat Mini Programs, and Web).
|
||||
format: 'mp3',
|
||||
};
|
||||
|
||||
function switchAudio() {
|
||||
emits('changeDisplayType', props.isEnableAudio ? 'editor' : 'audio');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Register events for the audio recording manager
|
||||
recorderManager.onStart(onRecorderStart);
|
||||
recorderManager.onStop(onRecorderStop);
|
||||
recorderManager.onError(onRecorderError);
|
||||
|
||||
TUIStore.watch(StoreName.CONV, {
|
||||
currentConversation: onCurrentConverstaionUpdated,
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
TUIStore.unwatch(StoreName.CONV, {
|
||||
currentConversation: onCurrentConverstaionUpdated,
|
||||
});
|
||||
});
|
||||
|
||||
function onCurrentConverstaionUpdated(conversation: IConversationModel) {
|
||||
currentConversation.value = conversation;
|
||||
}
|
||||
|
||||
function initRecorder() {
|
||||
initRecorderData();
|
||||
initRecorderView();
|
||||
}
|
||||
|
||||
function initRecorderView() {
|
||||
isRecording.value = false;
|
||||
touchBarText.value = '按住说话';
|
||||
modalText.value = '正在录音';
|
||||
}
|
||||
|
||||
function initRecorderData(options?: { hasError: boolean }) {
|
||||
clearInterval(recordTimer);
|
||||
recordTimer = undefined;
|
||||
recordTime = 0;
|
||||
firstTouchPageY = -1;
|
||||
isManualCancelBySlide = false;
|
||||
if (!options?.hasError) {
|
||||
recorderManager.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchStart() {
|
||||
if (isFingerTouchingScreen) {
|
||||
// Compatibility: Ignore the recording generated by the user's first authorization on the APP.
|
||||
isFirstAuthrizedRecord = true;
|
||||
}
|
||||
}
|
||||
|
||||
function handleLongPress() {
|
||||
isFingerTouchingScreen = true;
|
||||
recorderManager.start(recordConfig);
|
||||
}
|
||||
|
||||
const handleTouchMove = throttle(function (e) {
|
||||
if (isRecording.value) {
|
||||
const pageY = e.changedTouches[e.changedTouches.length - 1].pageY;
|
||||
if (firstTouchPageY < 0) {
|
||||
firstTouchPageY = pageY;
|
||||
}
|
||||
const offset = (firstTouchPageY as number) - pageY;
|
||||
if (offset > 150) {
|
||||
touchBarText.value = '抬起取消';
|
||||
modalText.value = '松开手指 取消发送';
|
||||
isManualCancelBySlide = true;
|
||||
} else if (offset > 50) {
|
||||
touchBarText.value = '抬起发送';
|
||||
modalText.value = '继续上滑可取消';
|
||||
isManualCancelBySlide = false;
|
||||
} else {
|
||||
touchBarText.value = '抬起发送';
|
||||
modalText.value = '正在录音';
|
||||
isManualCancelBySlide = false;
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
function handleTouchEnd() {
|
||||
isFingerTouchingScreen = false;
|
||||
recorderManager.stop();
|
||||
}
|
||||
|
||||
function onRecorderStart() {
|
||||
if (!isFingerTouchingScreen) {
|
||||
// If recording starts but the finger leaves the screen,
|
||||
// it means that the initial authorization popup interrupted the recording and it should be ignored.
|
||||
isFirstAuthrizedRecord = true;
|
||||
recorderManager.stop();
|
||||
return;
|
||||
}
|
||||
recordTimer = setInterval(() => {
|
||||
recordTime += 1;
|
||||
}, 1000);
|
||||
touchBarText.value = '抬起发送';
|
||||
isRecording.value = true;
|
||||
}
|
||||
|
||||
function onRecorderStop(res: RecordResult) {
|
||||
if (isFirstAuthrizedRecord) {
|
||||
// Compatibility: Ignore the recording generated by the user's first authorization on WeChat. This is not applicable to the APP.
|
||||
isFirstAuthrizedRecord = false;
|
||||
initRecorder();
|
||||
return;
|
||||
}
|
||||
if (isManualCancelBySlide || !isRecording.value) {
|
||||
initRecorder();
|
||||
return;
|
||||
}
|
||||
clearInterval(recordTimer);
|
||||
/**
|
||||
* Compatible with uniapp for building apps
|
||||
* Compatible with uniapp voice messages without duration
|
||||
* Duration and fileSize need to be supplemented by the user
|
||||
* File size = (Audio bitrate) * Length of time (in seconds) / 8
|
||||
* res.tempFilePath stores the temporary path of the recorded audio file
|
||||
*/
|
||||
const tempFilePath = res.tempFilePath;
|
||||
const duration = res.duration ? res.duration : recordTime * 1000;
|
||||
const fileSize = res.fileSize ? res.fileSize : ((48 * recordTime) / 8) * 1024;
|
||||
|
||||
if (duration < 1000) {
|
||||
Toast({
|
||||
message: '录音时间太短',
|
||||
type: TOAST_TYPE.NORMAL,
|
||||
duration: 1500,
|
||||
});
|
||||
} else {
|
||||
const options = {
|
||||
to:
|
||||
currentConversation?.value?.groupProfile?.groupID
|
||||
|| currentConversation?.value?.userProfile?.userID,
|
||||
conversationType: currentConversation?.value?.type,
|
||||
payload: { file: { duration, tempFilePath, fileSize } },
|
||||
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
|
||||
} as SendMessageParams;
|
||||
TUIChatService?.sendAudioMessage(options);
|
||||
}
|
||||
initRecorder();
|
||||
}
|
||||
|
||||
function onRecorderError() {
|
||||
initRecorderData({ hasError: true });
|
||||
initRecorderView();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../../assets/styles/common";
|
||||
|
||||
.message-input-audio {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.audio-message-icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.audio-input-touch-bar {
|
||||
height: 39px;
|
||||
flex: 1;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
|
||||
.record-modal {
|
||||
height: 300rpx;
|
||||
width: 60vw;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 9999;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.red-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: rgba(#ff3e48, 0.5);
|
||||
opacity: 0;
|
||||
transition: opacity 10ms linear;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.moving-slider {
|
||||
margin: 10vw;
|
||||
width: 40rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 4rpx;
|
||||
background-color: #006fff;
|
||||
animation: loading 1s ease-in-out infinite alternate;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.float-element {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(30vw, 0);
|
||||
background-color: #f5634a;
|
||||
width: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&-open {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
103
TUIKit/components/TUIChat/message-input/message-input-button.vue
Normal file
103
TUIKit/components/TUIChat/message-input/message-input-button.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div :class="['message-input-button', !isPC && 'message-input-button-h5']">
|
||||
<button
|
||||
v-if="props.enableSend"
|
||||
class="message-input-button-cont"
|
||||
data-type="text"
|
||||
:disabled="false"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<p
|
||||
v-if="displayHover"
|
||||
class="message-input-button-hover"
|
||||
>
|
||||
{{ TUITranslateService.t("TUIChat.按Enter发送,Ctrl+Enter换行") }}
|
||||
</p>
|
||||
{{ TUITranslateService.t("发送") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from '../../../adapter-vue';
|
||||
import { TUITranslateService } from '@tencentcloud/chat-uikit-engine';
|
||||
import { TUIConstants } from '@tencentcloud/tui-core';
|
||||
import { isPC } from '../../../utils/env';
|
||||
import TUIChatConfig from '../config';
|
||||
|
||||
const props = defineProps({
|
||||
enableSend: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const displayHover = ref(TUIChatConfig.getChatType() !== TUIConstants.TUIChat.TYPE.ROOM);
|
||||
|
||||
const emits = defineEmits(['sendMessage']);
|
||||
|
||||
const sendMessage = () => {
|
||||
emits('sendMessage');
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "../../../assets/styles/common";
|
||||
|
||||
.message-input-button {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
|
||||
&-h5 {
|
||||
position: static;
|
||||
}
|
||||
|
||||
&-cont {
|
||||
padding: 8px 20px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
background: #006eff;
|
||||
color: #fff;
|
||||
letter-spacing: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-hover {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 120%;
|
||||
word-break: keep-all;
|
||||
height: 30px;
|
||||
white-space: nowrap;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
opacity: 0.3;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
right: -20px;
|
||||
border: 10px solid transparent;
|
||||
border-left: 10px solid #000;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.message-input-button-hover {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
285
TUIKit/components/TUIChat/message-input/message-input-editor.vue
Normal file
285
TUIKit/components/TUIChat/message-input/message-input-editor.vue
Normal file
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'message-input-container': true,
|
||||
'message-input-container-h5': !isPC,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="props.isMuted"
|
||||
class="message-input-mute"
|
||||
>
|
||||
{{ props.muteText }}
|
||||
</div>
|
||||
<input
|
||||
id="editor"
|
||||
ref="inputRef"
|
||||
v-model="inputText"
|
||||
:adjust-position="true"
|
||||
cursor-spacing="20"
|
||||
confirm-type="send"
|
||||
:confirm-hold="true"
|
||||
maxlength="140"
|
||||
type="text"
|
||||
placeholder-class="input-placeholder"
|
||||
class="message-input-area"
|
||||
:placeholder="props.placeholder"
|
||||
auto-blur
|
||||
@confirm="handleSendMessage"
|
||||
@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';
|
||||
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 } 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,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['onTyping', 'onFocus', 'onAt']);
|
||||
const inputText = ref('');
|
||||
const inputRef = ref();
|
||||
const inputBlur = ref(true);
|
||||
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 resetEditor = () => {
|
||||
inputText.value = '';
|
||||
inputContentEmpty.value = true;
|
||||
allInsertedAtInfo?.clear();
|
||||
};
|
||||
|
||||
const setEditorContent = (content: any) => {
|
||||
inputText.value = content;
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
inputBlur.value = true;
|
||||
};
|
||||
|
||||
const onFocus = (e: any) => {
|
||||
inputBlur.value = false;
|
||||
emits('onFocus', e?.detail?.height);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
{
|
||||
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();
|
||||
if (currentConversationID.value) {
|
||||
DraftManager.getStore(currentConversationID.value, setEditorContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onQuoteMessageUpdated(options?: { message: IMessageModel; type: string }) {
|
||||
currentQuoteMessage.value = options;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
inputBlur.value = true;
|
||||
currentConversation.value = null;
|
||||
currentConversationID.value = '';
|
||||
currentQuoteMessage.value = null;
|
||||
resetEditor();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
insertAt,
|
||||
resetEditor,
|
||||
setEditorContent,
|
||||
getEditorContent,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../../assets/styles/common";
|
||||
|
||||
.message-input-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 3px 10px 10px;
|
||||
overflow: hidden;
|
||||
|
||||
&-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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="Boolean(quoteMessage) && props.displayType !== 'audio'"
|
||||
:class="{
|
||||
'input-quote-container': true,
|
||||
'input-quote-container-uni': isUniFrameWork,
|
||||
'input-quote-container-h5': isH5,
|
||||
}"
|
||||
>
|
||||
<div class="input-quote-content">
|
||||
<div class="max-one-line">
|
||||
{{ quoteMessage.nick || quoteMessage.from }}: {{ quoteContentText }}
|
||||
</div>
|
||||
<Icon
|
||||
class="input-quote-close-icon"
|
||||
:file="closeIcon"
|
||||
width="11px"
|
||||
height="11px"
|
||||
@onClick="cancelQuote"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from '../../../../adapter-vue';
|
||||
import TUIChatEngine, {
|
||||
TUIStore,
|
||||
StoreName,
|
||||
TUITranslateService,
|
||||
IMessageModel,
|
||||
} from '@tencentcloud/chat-uikit-engine';
|
||||
import Icon from '../../../common/Icon.vue';
|
||||
import closeIcon from '../../../../assets/icon/icon-close.svg';
|
||||
import { isH5, isUniFrameWork } from '../../../../utils/env';
|
||||
import { transformTextWithKeysToEmojiNames } from '../../emoji-config';
|
||||
import { InputDisplayType } from '../../../../interface';
|
||||
|
||||
interface IProps {
|
||||
displayType?: InputDisplayType;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
displayType: 'editor',
|
||||
});
|
||||
|
||||
const TYPES = TUIChatEngine.TYPES;
|
||||
const quoteMessage = ref<IMessageModel>();
|
||||
|
||||
onMounted(() => {
|
||||
TUIStore.watch(StoreName.CHAT, {
|
||||
quoteMessage: onQuoteMessageUpdated,
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
TUIStore.unwatch(StoreName.CHAT, {
|
||||
quoteMessage: onQuoteMessageUpdated,
|
||||
});
|
||||
});
|
||||
|
||||
const quoteContentText = computed(() => {
|
||||
let _quoteContentText;
|
||||
switch (quoteMessage.value?.type) {
|
||||
case TYPES.MSG_TEXT:
|
||||
_quoteContentText = transformTextWithKeysToEmojiNames(quoteMessage.value.payload?.text);
|
||||
break;
|
||||
case TYPES.MSG_IMAGE:
|
||||
_quoteContentText = TUITranslateService.t('TUIChat.图片');
|
||||
break;
|
||||
case TYPES.MSG_AUDIO:
|
||||
_quoteContentText = TUITranslateService.t('TUIChat.语音');
|
||||
break;
|
||||
case TYPES.MSG_VIDEO:
|
||||
_quoteContentText = TUITranslateService.t('TUIChat.视频');
|
||||
break;
|
||||
case TYPES.MSG_FILE:
|
||||
_quoteContentText = TUITranslateService.t('TUIChat.文件');
|
||||
break;
|
||||
case TYPES.MSG_CUSTOM:
|
||||
_quoteContentText = TUITranslateService.t('TUIChat.自定义');
|
||||
break;
|
||||
case TYPES.MSG_FACE:
|
||||
_quoteContentText = TUITranslateService.t('TUIChat.表情');
|
||||
break;
|
||||
case TYPES.MSG_MERGER:
|
||||
_quoteContentText = TUITranslateService.t('TUIChat.聊天记录');
|
||||
break;
|
||||
default:
|
||||
_quoteContentText = TUITranslateService.t('TUIChat.消息');
|
||||
break;
|
||||
}
|
||||
return _quoteContentText;
|
||||
});
|
||||
|
||||
function cancelQuote() {
|
||||
TUIStore.update(StoreName.CHAT, 'quoteMessage', { message: undefined, type: 'quote' });
|
||||
}
|
||||
|
||||
function onQuoteMessageUpdated(options?: { message: IMessageModel; type: string }) {
|
||||
if (options?.message && options?.type === 'quote') {
|
||||
quoteMessage.value = options.message;
|
||||
} else {
|
||||
quoteMessage.value = undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
%common-container-style {
|
||||
margin: 5px 100px 5px 8px;
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
|
||||
.input-quote-content {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
background-color: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
align-items: center;
|
||||
line-height: 16px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
|
||||
.max-one-line {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.input-quote-close-icon {
|
||||
margin-left: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-quote-container {
|
||||
@extend %common-container-style;
|
||||
}
|
||||
|
||||
.input-quote-container-uni {
|
||||
@extend %common-container-style;
|
||||
|
||||
margin: 5px 60px 0 30px;
|
||||
}
|
||||
|
||||
.input-quote-container-h5 {
|
||||
@extend %common-container-style;
|
||||
|
||||
margin: 5px 0 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user