This commit is contained in:
pengxiaolong
2025-05-13 19:39:53 +08:00
parent 37da6765b8
commit c006a8e63d
1232 changed files with 96963 additions and 883 deletions

View File

@@ -0,0 +1,185 @@
<template>
<div
ref="emojiPickerDialog"
:class="{
'emoji-picker': true,
'emoji-picker-h5': !isPC
}"
>
<ul
ref="emojiPickerListRef"
:class="['emoji-picker-list', !isPC && 'emoji-picker-h5-list']"
>
<li
v-for="(childrenItem, childrenIndex) in currentEmojiList"
:key="childrenIndex"
class="emoji-picker-list-item"
@click="select(childrenItem, childrenIndex)"
>
<img
v-if="currentTabItem.type === EMOJI_TYPE.BASIC"
class="emoji"
:src="currentTabItem.url + BASIC_EMOJI_URL_MAPPING[childrenItem]"
>
<img
v-else-if="currentTabItem.type === EMOJI_TYPE.BIG"
class="emoji-big"
:src="currentTabItem.url + childrenItem + '@2x.png'"
>
<img
v-else
class="emoji-custom emoji-big"
:src="currentTabItem.url + childrenItem"
>
</li>
</ul>
<ul class="emoji-picker-tab">
<li
v-for="(item, index) in list"
:key="index"
class="emoji-picker-tab-item"
@click="toggleEmojiTab(index)"
>
<Icon
v-if="item.type === EMOJI_TYPE.BASIC"
class="icon"
:file="faceIcon"
/>
<img
v-else-if="item.type === EMOJI_TYPE.BIG"
class="icon-big"
:src="item.url + item.list[0] + '@2x.png'"
>
<img
v-else
class="icon-custom icon-big"
:src="item.url + item.list[0]"
>
</li>
<li
v-if="isUniFrameWork"
class="send-btn"
@click="sendMessage"
>
发送
</li>
</ul>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from '../../../../adapter-vue';
import {
TUIChatService,
TUIStore,
StoreName,
IConversationModel,
SendMessageParams,
} from '@tencentcloud/chat-uikit-engine';
import Icon from '../../../common/Icon.vue';
import faceIconLight from '../../../../assets/icon/face-light.svg';
import faceIconDark from '../../../../assets/icon/face-dark.svg';
import { EMOJI_TYPE } from '.././../../../constant';
import { isPC, isUniFrameWork } from '../../../../utils/env';
import { IEmojiGroupList, IEmojiGroup } from '../../../../interface';
import { isEnabledMessageReadReceiptGlobal } from '../../utils/utils';
import { EMOJI_GROUP_LIST, BASIC_EMOJI_URL_MAPPING, convertKeyToEmojiName } from '../../emoji-config';
import TUIChatConfig from '../../config';
const faceIcon = TUIChatConfig.getTheme() === 'dark' ? faceIconDark : faceIconLight;
const emits = defineEmits(['insertEmoji', 'onClose', 'sendMessage']);
const currentTabIndex = ref<number>(0);
const currentConversation = ref();
const emojiPickerDialog = ref();
const emojiPickerListRef = ref();
const featureConfig = TUIChatConfig.getFeatureConfig();
const list = ref<IEmojiGroupList>(initEmojiList());
const currentTabItem = ref<IEmojiGroup>(list?.value[0]);
const currentEmojiList = ref<string[]>(list?.value[0]?.list);
onMounted(() => {
TUIStore.watch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CONV, {
currentConversation: onCurrentConversationUpdate,
});
});
const toggleEmojiTab = (index: number) => {
currentTabIndex.value = index;
currentTabItem.value = list?.value[index];
currentEmojiList.value = list?.value[index]?.list;
// web & h5 side scroll to top
if (!isUniFrameWork) {
emojiPickerListRef?.value && (emojiPickerListRef.value.scrollTop = 0);
}
};
const select = (item: any, index: number) => {
const options: any = {
emoji: { key: item, name: convertKeyToEmojiName(item) },
type: currentTabItem?.value?.type,
};
switch (currentTabItem?.value?.type) {
case EMOJI_TYPE.BASIC:
options.url = currentTabItem?.value?.url + BASIC_EMOJI_URL_MAPPING[item];
if (isUniFrameWork) {
uni.$emit('insert-emoji', options);
} else {
emits('insertEmoji', options);
}
break;
case EMOJI_TYPE.BIG:
sendFaceMessage(index, currentTabItem.value);
break;
case EMOJI_TYPE.CUSTOM:
sendFaceMessage(index, currentTabItem.value);
break;
default:
break;
}
isPC && emits('onClose');
};
const sendFaceMessage = (index: number, listItem: IEmojiGroup) => {
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
index: listItem.emojiGroupID,
data: listItem.list[index],
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
} as SendMessageParams;
TUIChatService.sendFaceMessage(options);
};
function sendMessage() {
uni.$emit('send-message-in-emoji-picker');
}
function onCurrentConversationUpdate(conversation: IConversationModel) {
currentConversation.value = conversation;
}
function initEmojiList() {
return EMOJI_GROUP_LIST.filter((item) => {
if (item.type === EMOJI_TYPE.BASIC) {
return featureConfig.InputEmoji;
}
if (item.type === EMOJI_TYPE.BIG) {
return featureConfig.InputStickers;
}
if (item.type === EMOJI_TYPE.CUSTOM) {
return featureConfig.InputStickers;
}
});
}
</script>
<style lang="scss" scoped src="./style/index.scss"></style>

View File

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

View File

@@ -0,0 +1,81 @@
<template>
<ToolbarItemContainer
ref="container"
:iconFile="faceIcon"
title="表情"
@onDialogShow="onDialogShow"
@onDialogClose="onDialogClose"
>
<EmojiPickerDialog
@insertEmoji="insertEmoji"
@sendMessage="sendMessage"
@onClose="onClose"
/>
</ToolbarItemContainer>
</template>
<script lang="ts" setup>
import {
TUIStore,
StoreName,
IConversationModel,
} from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../../adapter-vue';
import faceIconLight from '../../../../assets/icon/face-light.svg';
import faceIconDark from '../../../../assets/icon/face-dark.svg';
import EmojiPickerDialog from './emoji-picker-dialog.vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue';
import { isH5 } from '../../../../utils/env';
import { ToolbarDisplayType } from '../../../../interface';
import TUIChatConfig from '../../config';
interface IEmits {
(e: 'sendMessage'): void;
(e: 'toggleComponent'): void;
(e: 'insertEmoji', emoji: any): void;
(e: 'dialogShowInH5', dialogRef: HTMLElement): void;
(e: 'dialogCloseInH5', dialogRef: HTMLElement): void;
(e: 'changeToolbarDisplayType', type: ToolbarDisplayType): void;
}
const faceIcon = TUIChatConfig.getTheme() === 'dark' ? faceIconDark : faceIconLight;
const emits = defineEmits<IEmits>();
const currentConversation = ref();
const container = ref<InstanceType<typeof ToolbarItemContainer>>();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
});
const onDialogShow = (dialogRef: any) => {
if (!isH5) {
return;
}
emits('changeToolbarDisplayType', 'emojiPicker');
emits('dialogShowInH5', dialogRef.value);
};
const onDialogClose = (dialogRef: any) => {
if (!isH5) {
return;
}
emits('changeToolbarDisplayType', 'none');
emits('dialogCloseInH5', dialogRef.value);
};
const insertEmoji = (emojiObj) => {
emits('insertEmoji', emojiObj);
};
const sendMessage = () => {
emits('sendMessage');
};
const onClose = () => {
container.value?.toggleDialogDisplay(false);
};
defineExpose({
closeEmojiPicker: onClose,
});
</script>
<style lang="scss" scoped src="./style/index.scss"></style>

View File

@@ -0,0 +1,25 @@
.emoji-picker-h5 {
width: 100%;
&-list {
justify-content: space-between;
}
&-list::after {
content: "";
display: block;
flex: 1 1 auto;
}
.send-btn {
width: 50px;
height: 30px;
background-color: #55C06A;
position: absolute;
right: 10px;
font-size: 16px;
color: #fff;
text-align: center;
line-height: 30px;
}
}

View File

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

View File

@@ -0,0 +1,55 @@
.emoji-picker {
width: 405px;
height: 300px;
display: flex;
flex-direction: column;
&-list {
flex: 1;
display: flex;
flex-wrap: wrap;
overflow-y: auto;
margin: 2px;
&::-webkit-scrollbar {
display: none;
}
&-item {
cursor: pointer;
padding: 5px;
.emoji {
width: 30px;
height: 30px;
}
.emoji-big {
width: 70px;
height: 70px;
}
}
}
&-tab {
display: flex;
align-items: center;
&-item {
padding: 0 10px;
cursor: pointer;
.icon {
margin: 10px;
width: 20px;
height: 20px;
&-big {
margin: 2px 0;
width: 30px;
height: 30px;
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,211 @@
<template>
<ToolbarItemContainer
ref="container"
:iconFile="evaluateIcon"
title="评价"
:needBottomPopup="true"
:iconWidth="isUniFrameWork ? '26px' : '20px'"
:iconHeight="isUniFrameWork ? '26px' : '20px'"
@onDialogShow="onDialogShow"
@onDialogClose="onDialogClose"
>
<div :class="['evaluate', !isPC && 'evaluate-h5']">
<div :class="['evaluate-header', !isPC && 'evaluate-h5-header']">
<div
:class="[
'evaluate-header-content',
!isPC && 'evaluate-h5-header-content',
]"
>
{{ TUITranslateService.t("Evaluate.请对本次服务进行评价") }}
</div>
<div
v-if="!isPC"
:class="[
'evaluate-header-close',
!isPC && 'evaluate-h5-header-close',
]"
@click.stop="closeDialog"
>
{{ TUITranslateService.t("关闭") }}
</div>
</div>
<div :class="['evaluate-content', !isPC && 'evaluate-h5-content']">
<ul
:class="[
'evaluate-content-list',
!isPC && 'evaluate-h5-content-list',
]"
>
<li
v-for="(item, index) in starList"
:key="index"
:class="[
'evaluate-content-list-item',
!isPC && 'evaluate-h5-content-list-item',
]"
@click.stop="selectStar(index)"
>
<Icon
v-if="index <= currentStarIndex"
:file="starLightIcon"
:width="isPC ? '20px' : '30px'"
:height="isPC ? '20px' : '30px'"
/>
<Icon
v-else
:file="starIcon"
:width="isPC ? '20px' : '30px'"
:height="isPC ? '20px' : '30px'"
/>
</li>
</ul>
<textarea
v-model="comment"
:class="[
'evaluate-content-text',
!isPC && 'evaluate-h5-content-text',
]"
/>
<div
:class="[
'evaluate-content-button',
!isPC && 'evaluate-h5-content-button',
]"
>
<button
:class="['btn', isEvaluateValid ? 'btn-valid' : 'btn-invalid']"
@click="submitEvaluate"
>
{{ TUITranslateService.t("Evaluate.提交评价") }}
</button>
</div>
</div>
<div :class="['evaluate-adv', !isPC && 'evaluate-h5-adv']">
{{ TUITranslateService.t("Evaluate.服务评价工具") }}
{{ "(" + TUITranslateService.t("Evaluate.使用") }}
<a @click="openLink(Link.customMessage)">
{{ TUITranslateService.t(`Evaluate.${Link.customMessage.label}`) }}
</a>
{{ TUITranslateService.t("Evaluate.搭建") + ")" }}
</div>
</div>
</ToolbarItemContainer>
</template>
<script setup lang="ts">
import TUIChatEngine, {
TUITranslateService,
TUIStore,
StoreName,
IConversationModel,
TUIChatService,
SendMessageParams,
SendMessageOptions,
} from '@tencentcloud/chat-uikit-engine';
import { ref, computed } from '../../../../adapter-vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue';
import evaluateIconLight from '../../../../assets/icon/evalute-light.svg';
import evaluateIconDark from '../../../../assets/icon/evalute-dark.svg';
import Link from '../../../../utils/documentLink';
import Icon from '../../../common/Icon.vue';
import starIcon from '../../../../assets/icon/star.png';
import starLightIcon from '../../../../assets/icon/star-light.png';
import { CHAT_MSG_CUSTOM_TYPE } from '../../../../constant';
import { isPC, isH5, isUniFrameWork } from '../../../../utils/env';
import { isEnabledMessageReadReceiptGlobal } from '../../utils/utils';
import OfflinePushInfoManager, { IOfflinePushInfoCreateParams } from '../../offlinePushInfoManager/index';
import TUIChatConfig from '../../config';
const evaluateIcon = TUIChatConfig.getTheme() === 'dark' ? evaluateIconDark : evaluateIconLight;
const props = defineProps({
starTotal: {
type: Number,
default: 5,
},
});
const emits = defineEmits(['onDialogPopupShowOrHide']);
const container = ref();
const starList = ref<number>(props.starTotal);
const currentStarIndex = ref<number>(-1);
const comment = ref('');
const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
});
const isEvaluateValid = computed(() => comment.value.length || currentStarIndex.value >= 0);
const onDialogShow = () => {
emits('onDialogPopupShowOrHide', true);
};
const onDialogClose = () => {
resetEvaluate();
emits('onDialogPopupShowOrHide', false);
};
const openLink = () => {
if (isPC || isH5) {
window.open(Link?.customMessage?.url);
}
};
const closeDialog = () => {
container?.value?.toggleDialogDisplay(false);
};
const resetEvaluate = () => {
currentStarIndex.value = -1;
comment.value = '';
};
const selectStar = (starIndex?: any) => {
if (currentStarIndex.value === starIndex) {
currentStarIndex.value = currentStarIndex.value - 1;
} else {
currentStarIndex.value = starIndex;
}
};
const submitEvaluate = () => {
// The evaluate message must have at least one star or comment to be submitted.
if (currentStarIndex.value < 0 && !comment.value.length) {
return;
}
const payload = {
data: JSON.stringify({
businessID: CHAT_MSG_CUSTOM_TYPE.EVALUATE,
version: 1,
score: currentStarIndex.value + 1,
comment: comment.value,
}),
description: '对本次的服务评价',
extension: '对本次的服务评价',
};
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload,
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
};
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value,
payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_CUSTOM,
};
const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
};
TUIChatService.sendCustomMessage(options as SendMessageParams, sendMessageOptions);
// close dialog after submit evaluate
container?.value?.toggleDialogDisplay(false);
};
</script>
<style scoped lang="scss" src="./style/index.scss"></style>

View File

@@ -0,0 +1,57 @@
.evaluate {
background: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
&-header {
&-content {
font-weight: 500;
color: #1c1c1c;
}
}
&-adv {
font-weight: 500;
color: #999;
a {
color: #006eff;
}
}
&-content {
&-text {
background: #f8f8f8;
border: 1px solid #ececec;
}
&-list {
&-item {
font-weight: 400;
color: #50545c;
}
}
}
&-H5 {
&-main {
background: rgba(0, 0, 0, 0.5);
.evaluate-main-content {
background: #fff;
p {
a {
color: #3370ff;
}
}
.close {
font-family: PingFangSC-Regular;
font-weight: 400;
color: #3370ff;
letter-spacing: 0;
}
}
}
}
}

View File

@@ -0,0 +1,63 @@
.evaluate-h5 {
position: static;
width: 100%;
height: fit-content;
border-radius: 0;
background: #fff;
padding: 23px !important;
box-sizing: border-box;
&-header {
display: flex;
justify-content: space-between;
&-content {
font-size: 18px;
}
&-close {
font-size: 18px;
line-height: 27px;
font-weight: 400;
color: #3370ff;
}
}
&-content {
order: 1;
&-list {
&-item {
width: 40px;
height: 24px;
text-align: center;
cursor: auto;
font-size: 12px;
}
}
&-text {
font-size: 16px;
width: 100%;
}
&-button {
width: 100%;
display: flex;
.btn {
flex: 1;
padding: 14px 0;
font-size: 18px;
cursor: auto;
}
}
}
&-adv {
font-size: 14px;
font-weight: normal;
text-align: left;
color: #000;
}
}

View File

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

View File

@@ -0,0 +1,93 @@
.evaluate {
position: absolute;
z-index: 5;
width: 315px;
top: -255px;
padding: 12px;
display: flex;
flex-direction: column;
border-radius: 8px;
background: url("https://web.sdk.qcloud.com/im/assets/images/login-background.png") no-repeat;
background-color: #fff;
background-size: cover;
background-position-x: 128px;
background-position-y: 77px;
user-select: none;
&-header {
&-content {
font-style: normal;
font-size: 12px;
line-height: 17px;
text-align: center;
}
}
&-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
&-list {
flex: 1;
display: flex;
&-item {
width: 24px;
height: 24px;
text-align: center;
cursor: pointer;
padding: 4px 0;
font-size: 12px;
padding-right: 15px;
&:last-child {
padding-right: 0 !important;
}
}
}
&-text {
box-sizing: border-box;
width: 288px;
height: 90px;
margin: 12px 0;
padding: 12px;
border-radius: 2px;
resize: none;
}
&-button {
.btn {
border: none;
border-radius: 5px;
font-size: 12px;
text-align: center;
line-height: 24px;
padding: 2px 46px;
font-weight: 400;
color: #fff;
}
.btn-valid {
background-color: #3370ff;
cursor: pointer;
}
.btn-invalid{
background-color: rgb(160, 207, 255);
cursor: not-allowed;
}
}
}
&-adv {
font-size: 12px;
text-align: center;
a {
display: inline-block;
}
}
}

View File

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

View File

@@ -0,0 +1,86 @@
<template>
<ToolbarItemContainer
:iconFile="fileIcon"
title="文件"
:iconWidth="isUniFrameWork ? '32px' : '20px'"
:iconHeight="isUniFrameWork ? '25px' : '18px'"
:needDialog="false"
@onIconClick="onIconClick"
>
<div :class="['file-upload', !isPC && 'file-upload-h5']">
<input
ref="inputRef"
title="文件"
type="file"
data-type="file"
accept="*"
@change="sendFileMessage"
>
</div>
</ToolbarItemContainer>
</template>
<script lang="ts" setup>
import TUIChatEngine, {
TUIChatService,
TUIStore,
StoreName,
IConversationModel,
SendMessageParams,
SendMessageOptions,
} from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../../adapter-vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue';
import fileIconLight from '../../../../assets/icon/file-light.svg';
import fileIconDark from '../../../../assets/icon/file-dark.svg';
import { isPC, isUniFrameWork } from '../../../../utils/env';
import { isEnabledMessageReadReceiptGlobal } from '../../utils/utils';
import OfflinePushInfoManager, { IOfflinePushInfoCreateParams } from '../../offlinePushInfoManager/index';
import TUIChatConfig from '../../config';
const fileIcon = TUIChatConfig.getTheme() === 'dark' ? fileIconDark : fileIconLight;
const inputRef = ref();
const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
});
const onIconClick = () => {
if (isUniFrameWork) {
return;
} else {
inputRef?.value?.click && inputRef?.value?.click();
}
};
const sendFileMessage = (e: any) => {
if (e?.target?.files?.length <= 0) {
return;
}
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
file: e?.target,
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
} as SendMessageParams;
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value,
payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_FILE,
};
const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
};
TUIChatService.sendFileMessage(options, sendMessageOptions);
e.target.value = '';
};
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
</style>

View File

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

View File

@@ -0,0 +1,156 @@
<template>
<ToolbarItemContainer
:iconFile="imageToolbarForShow.icon"
:title="imageToolbarForShow.title"
:iconWidth="isUniFrameWork ? '32px' : '20px'"
:iconHeight="isUniFrameWork ? '25px' : '18px'"
:needDialog="false"
@onIconClick="onIconClick"
>
<div
v-if="!isUniFrameWork"
:class="['image-upload', !isPC && 'image-upload-h5']"
>
<input
ref="inputRef"
title="图片"
type="file"
data-type="image"
accept="image/gif,image/jpeg,image/jpg,image/png,image/bmp,image/webp"
@change="sendImageInWeb"
>
</div>
</ToolbarItemContainer>
</template>
<script lang="ts" setup>
import TUIChatEngine, {
TUIChatService,
TUIStore,
StoreName,
IConversationModel,
SendMessageParams,
SendMessageOptions,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref, computed } from '../../../../adapter-vue';
import { isPC, isWeChat, isUniFrameWork } from '../../../../utils/env';
import ToolbarItemContainer from '../toolbar-item-container/index.vue';
import imageIconLight from '../../../../assets/icon/image-light.svg';
import imageIconDark from '../../../../assets/icon/image-dark.svg';
import imageUniIcon from '../../../../assets/icon/image-uni.png';
import cameraUniIcon from '../../../../assets/icon/camera-uni.png';
import { isEnabledMessageReadReceiptGlobal } from '../../utils/utils';
import OfflinePushInfoManager, { IOfflinePushInfoCreateParams } from '../../offlinePushInfoManager/index';
import TUIChatConfig from '../../config';
const props = defineProps({
// Image source: only valid for uni-app version, web version only supports selecting images from the album.
// album: Select from album
// camera: Take a photo using the camera
imageSourceType: {
type: String,
default: 'album',
},
});
const inputRef = ref();
const currentConversation = ref<IConversationModel>();
const theme = TUIChatConfig.getTheme();
const IMAGE_TOOLBAR_SHOW_MAP = {
web_album: {
icon: theme === 'dark' ? imageIconDark : imageIconLight,
title: '图片',
},
uni_album: {
icon: imageUniIcon,
title: '图片',
},
uni_camera: {
icon: cameraUniIcon,
title: '拍照',
},
};
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
});
const imageToolbarForShow = computed((): { icon: string; title: string } => {
if (isUniFrameWork) {
return props.imageSourceType === 'camera'
? IMAGE_TOOLBAR_SHOW_MAP['uni_camera']
: IMAGE_TOOLBAR_SHOW_MAP['uni_album'];
} else {
return IMAGE_TOOLBAR_SHOW_MAP['web_album'];
}
});
const onIconClick = () => {
// uni-app send image
if (isUniFrameWork) {
if (isWeChat && TUIGlobal?.chooseMedia) {
TUIGlobal?.chooseMedia({
count: 1,
mediaType: ['image'],
sizeType: ['original', 'compressed'],
sourceType: [props.imageSourceType], // Use camera or select from album.
success: function (res: any) {
sendImageMessage(res);
},
});
} else {
// uni-app H5/App send image
TUIGlobal?.chooseImage({
count: 1,
sourceType: [props.imageSourceType], // Use camera or select from album.
success: function (res) {
sendImageMessage(res);
},
});
}
} else {
if (inputRef.value?.click) {
inputRef.value.click();
}
}
};
const sendImageInWeb = (e: any) => {
if (e?.target?.files?.length <= 0) {
return;
}
sendImageMessage(e?.target);
e.target.value = '';
};
const sendImageMessage = (files: any) => {
if (!files) {
return;
}
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
file: files,
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
} as SendMessageParams;
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value,
payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_IMAGE,
};
const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
};
TUIChatService.sendImageMessage(options, sendMessageOptions);
};
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
</style>

View File

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

View File

@@ -0,0 +1,316 @@
<template>
<div
:class="[
'message-input-toolbar',
'message-input-toolbar-h5',
'message-input-toolbar-uni',
]"
>
<div v-if="props.displayType === 'emojiPicker'">
<EmojiPickerDialog />
</div>
<div v-else>
<swiper
:class="['message-input-toolbar-swiper']"
:indicator-dots="isSwiperIndicatorDotsEnable"
:autoplay="false"
:circular="false"
>
<swiper-item
:class="[
'message-input-toolbar-list',
'message-input-toolbar-h5-list',
'message-input-toolbar-uni-list',
]"
>
<ImageUpload
v-if="featureConfig.InputImage"
imageSourceType="camera"
/>
<ImageUpload
v-if="featureConfig.InputImage"
imageSourceType="album"
/>
<VideoUpload
v-if="featureConfig.InputVideo"
videoSourceType="album"
/>
<VideoUpload
v-if="featureConfig.InputVideo"
videoSourceType="camera"
/>
<template v-if="currentExtensionList.length > 0">
<div
v-for="(extension, index) in currentExtensionList.slice(0, slicePos)"
:key="index"
>
<ToolbarItemContainer
v-if="extension"
:iconFile="genExtensionIcon(extension)"
:title="genExtensionText(extension)"
iconWidth="25px"
iconHeight="25px"
:needDialog="false"
@onIconClick="onExtensionClick(extension)"
/>
</div>
</template>
<template v-if="neededCountFirstPage === 1">
<Evaluate
v-if="featureConfig.InputEvaluation"
@onDialogPopupShowOrHide="handleSwiperDotShow"
/>
<Words
v-else-if="featureConfig.InputQuickReplies"
@onDialogPopupShowOrHide="handleSwiperDotShow"
/>
</template>
<template v-if="neededCountFirstPage > 1">
<Evaluate
v-if="featureConfig.InputEvaluation"
@onDialogPopupShowOrHide="handleSwiperDotShow"
/>
<Words
v-if="featureConfig.InputQuickReplies"
@onDialogPopupShowOrHide="handleSwiperDotShow"
/>
</template>
</swiper-item>
<swiper-item
v-if="neededCountFirstPage <= 1"
:class="[
'message-input-toolbar-list',
'message-input-toolbar-h5-list',
'message-input-toolbar-uni-list',
]"
>
<div
v-for="(extension, index) in currentExtensionList.slice(slicePos)"
:key="index"
>
<ToolbarItemContainer
v-if="extension"
:iconFile="genExtensionIcon(extension)"
:title="genExtensionText(extension)"
iconWidth="25px"
iconHeight="25px"
:needDialog="false"
@onIconClick="onExtensionClick(extension)"
/>
</div>
<template v-if="neededCountFirstPage === 1">
<Words
v-if="featureConfig.InputQuickReplies"
@onDialogPopupShowOrHide="handleSwiperDotShow"
/>
</template>
<template v-else>
<Evaluate
v-if="featureConfig.InputEvaluation"
@onDialogPopupShowOrHide="handleSwiperDotShow"
/>
<Words
v-if="featureConfig.InputQuickReplies"
@onDialogPopupShowOrHide="handleSwiperDotShow"
/>
</template>
</swiper-item>
</swiper>
</div>
<UserSelector
ref="userSelectorRef"
:type="selectorShowType"
:currentConversation="currentConversation"
:isGroup="isGroup"
@submit="onUserSelectorSubmit"
@cancel="onUserSelectorCancel"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted, onMounted } from '../../../adapter-vue';
import TUIChatEngine, {
IConversationModel,
TUIStore,
StoreName,
TUIReportService,
} from '@tencentcloud/chat-uikit-engine';
import TUICore, { ExtensionInfo, TUIConstants } from '@tencentcloud/tui-core';
import ImageUpload from './image-upload/index.vue';
import VideoUpload from './video-upload/index.vue';
import Evaluate from './evaluate/index.vue';
import Words from './words/index.vue';
import ToolbarItemContainer from './toolbar-item-container/index.vue';
import EmojiPickerDialog from './emoji-picker/emoji-picker-dialog.vue';
import UserSelector from './user-selector/index.vue';
import TUIChatConfig from '../config';
import { enableSampleTaskStatus } from '../../../utils/enableSampleTaskStatus';
import { ToolbarDisplayType } from '../../../interface';
import OfflinePushInfoManager, { PUSH_SCENE } from '../offlinePushInfoManager/index';
interface IProps {
displayType: ToolbarDisplayType;
}
const props = withDefaults(defineProps<IProps>(), {
});
const currentConversation = ref<IConversationModel>();
const isGroup = ref<boolean>(false);
const selectorShowType = ref<string>('');
const userSelectorRef = ref();
const currentUserSelectorExtension = ref<ExtensionInfo | null>();
const currentExtensionList = ref<ExtensionInfo[]>([]);
const isSwiperIndicatorDotsEnable = ref<boolean>(false);
const featureConfig = TUIChatConfig.getFeatureConfig();
const neededCountFirstPage = ref<number>(8);
const slicePos = ref<number>(0);
const computeToolbarPaging = () => {
if (featureConfig.InputImage && featureConfig.InputVideo) {
neededCountFirstPage.value -= 4;
} else if (featureConfig.InputImage || featureConfig.InputVideo) {
neededCountFirstPage.value -= 2;
}
slicePos.value = neededCountFirstPage.value;
neededCountFirstPage.value -= currentExtensionList.value.length;
if (neededCountFirstPage.value === 1) {
isSwiperIndicatorDotsEnable.value = (featureConfig.InputEvaluation && featureConfig.InputQuickReplies);
} else if (neededCountFirstPage.value < 1) {
isSwiperIndicatorDotsEnable.value = featureConfig.InputEvaluation || featureConfig.InputQuickReplies;
}
};
onMounted(() => {
TUIStore.watch(StoreName.CUSTOM, {
activeConversation: onActiveConversationUpdate,
});
});
onUnmounted(() => {
TUIStore.unwatch(StoreName.CUSTOM, {
activeConversation: onActiveConversationUpdate,
});
});
const onActiveConversationUpdate = (conversationID: string) => {
if (!conversationID) {
return;
}
if (conversationID !== currentConversation.value?.conversationID) {
getExtensionList();
computeToolbarPaging();
currentConversation.value = TUIStore.getData(StoreName.CONV, 'currentConversation');
isGroup.value = conversationID.startsWith(TUIChatEngine.TYPES.CONV_GROUP);
}
};
const getExtensionList = () => {
const chatType = TUIChatConfig.getChatType();
const params: Record<string, boolean | string> = { chatType };
// Backward compatibility: When callkit does not have chatType judgment, use filterVoice and filterVideo to filter
if (chatType === TUIConstants.TUIChat.TYPE.CUSTOMER_SERVICE) {
params.filterVoice = true;
params.filterVideo = true;
enableSampleTaskStatus('customerService');
}
// uni-app build ios app has null in last index need to filter
currentExtensionList.value = [
...TUICore.getExtensionList(TUIConstants.TUIChat.EXTENSION.INPUT_MORE.EXT_ID, params),
].filter((extension: ExtensionInfo) => {
if (extension?.data?.name === 'search') {
return featureConfig.MessageSearch;
}
return true;
});
reportExtension(currentExtensionList.value);
};
function reportExtension(extensionList:ExtensionInfo[]){
extensionList.forEach((extension: ExtensionInfo)=>{
const _name = extension?.data?.name;
if(_name === 'voiceCall'){
TUIReportService.reportFeature(203, 'voice-call');
} else if (_name === 'videoCall') {
TUIReportService.reportFeature(203, 'video-call');
} else if(_name === 'quickRoom'){
TUIReportService.reportFeature(204);
}
});
}
// handle extensions onclick
const onExtensionClick = (extension: ExtensionInfo) => {
// uniapp vue2 build wx lose listener proto
const extensionModel = currentExtensionList.value.find(
targetExtension => targetExtension?.data?.name === extension?.data?.name,
);
switch (extensionModel?.data?.name) {
case 'voiceCall':
onCallExtensionClicked(extensionModel, 1);
break;
case 'videoCall':
onCallExtensionClicked(extensionModel, 2);
break;
case 'search':
extensionModel?.listener?.onClicked?.();
break;
default:
break;
}
};
const onCallExtensionClicked = (extension: ExtensionInfo, callType: number) => {
selectorShowType.value = extension?.data?.name;
if (currentConversation?.value?.type === TUIChatEngine.TYPES.CONV_C2C) {
extension?.listener?.onClicked?.({
userIDList: [currentConversation?.value?.conversationID?.slice(3)],
type: callType,
callParams: {
offlinePushInfo: OfflinePushInfoManager.getOfflinePushInfo(PUSH_SCENE.CALL),
},
});
} else if (isGroup.value) {
currentUserSelectorExtension.value = extension;
userSelectorRef?.value?.toggleShow && userSelectorRef.value.toggleShow(true);
}
};
const genExtensionIcon = (extension: any) => {
return extension?.icon;
};
const genExtensionText = (extension: any) => {
return extension?.text;
};
const onUserSelectorSubmit = (selectedInfo: any) => {
currentUserSelectorExtension.value?.listener?.onClicked?.({
...selectedInfo,
callParams: {
offlinePushInfo: OfflinePushInfoManager.getOfflinePushInfo(PUSH_SCENE.CALL),
},
});
currentUserSelectorExtension.value = null;
};
const onUserSelectorCancel = () => {
currentUserSelectorExtension.value = null;
};
const handleSwiperDotShow = (showStatus: boolean) => {
isSwiperIndicatorDotsEnable.value = (neededCountFirstPage.value <= 1 && !showStatus);
};
</script>
<script lang="ts">
export default {
options: {
styleIsolation: 'shared',
},
};
</script>
<style lang="scss">
@import '../../../assets/styles/common';
@import './style/uni';
</style>

View File

@@ -0,0 +1,111 @@
/* stylelint-disable */
.message-input-toolbar {
border-top: 1px solid #f4f5f9;
width: 100%;
max-width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
z-index: 100;
user-select: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
&-list {
display: flex;
flex-direction: row;
align-items: center;
.extension-list {
list-style: none;
display: flex;
&-item {
width: 20px;
height: 20px;
padding: 12px 10px 1px;
cursor: pointer;
}
}
}
}
.message-input-toolbar-h5 {
padding: 5px 10px;
box-sizing: border-box;
flex-direction: column;
}
.message-input-toolbar-uni {
background-color: #ebf0f6;
flex-direction: column;
z-index: 100;
&-list {
flex: 1;
display: grid;
grid-template-columns: repeat(4, 25%);
grid-template-rows: repeat(2, 100px);
}
}
// uniapp swiper style
wx-swiper .wx-swiper-wrapper,
wx-swiper .wx-swiper-slides,
wx-swiper .wx-swiper-slide-frame,
.message-input-toolbar-list {
overflow: visible !important;
}
.message-input-toolbar {
.bottom-popup,
.bottom-popup-h5,
.bottom-popup-uni {
position: sticky !important;
}
}
.message-input-toolbar-swiper {
width: 100%;
height: 220px;
::v-deep .uni-swiper-wrapper,
wx-swiper .wx-swiper-wrapper {
overflow: visible !important;
.uni-swiper-slides,
.wx-swiper-slides,
wx-swiper .wx-swiper-slides {
overflow: visible !important;
.uni-swiper-slide-frame,
.wx-swiper-slide-frame,
wx-swiper .wx-swiper-slide-frame {
overflow: visible !important;
.message-input-toolbar-list {
overflow: visible !important;
}
.toolbar-item-container-uni {
position: static !important;
}
.toolbar-item-container-dialog {
position: absolute !important;
background: transparent;
left: -10px;
bottom: -5px;
.bottom-popup-uni {
position: sticky !important;
}
}
}
}
}
}

View File

@@ -0,0 +1,138 @@
<template>
<div
ref="toolbarItemRef"
:class="[
'toolbar-item-container',
!isPC && 'toolbar-item-container-h5',
isUniFrameWork && 'toolbar-item-container-uni',
]"
>
<div
:class="[
'toolbar-item-container-icon',
isUniFrameWork && 'toolbar-item-container-uni-icon',
]"
@click="toggleToolbarItem"
>
<Icon
:file="props.iconFile"
class="icon"
:width="props.iconWidth"
:height="props.iconHeight"
/>
</div>
<div
v-if="isUniFrameWork"
:class="['toolbar-item-container-uni-title']"
>
{{ props.title }}
</div>
<div
v-show="showDialog"
ref="dialogRef"
:class="[
'toolbar-item-container-dialog',
isDark && 'toolbar-item-container-dialog-dark',
!isPC && 'toolbar-item-container-h5-dialog',
isUniFrameWork && 'toolbar-item-container-uni-dialog',
]"
>
<BottomPopup
v-if="props.needBottomPopup && !isPC"
class="toolbar-bottom-popup"
:show="showDialog"
@touchmove.stop.prevent
@onClose="onPopupClose"
>
<slot />
</BottomPopup>
<slot v-else />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from '../../../../adapter-vue';
import { outsideClick } from '@tencentcloud/universal-api';
import Icon from '../../../common/Icon.vue';
import BottomPopup from '../../../common/BottomPopup/index.vue';
import { isPC, isUniFrameWork } from '../../../../utils/env';
import TUIChatConfig from '../../config';
const props = defineProps({
iconFile: {
type: String,
required: true,
},
title: {
type: String,
default: '',
},
needDialog: {
type: Boolean,
default: true,
},
iconWidth: {
type: String,
default: '20px',
},
iconHeight: {
type: String,
default: '20px',
},
// Whether to display the bottom popup dialog on mobile devices
// Invalid on PC
needBottomPopup: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['onIconClick', 'onDialogClose', 'onDialogShow']);
const isDark = ref(TUIChatConfig.getTheme() === 'dark');
const showDialog = ref(false);
const toolbarItemRef = ref();
const dialogRef = ref();
const toggleToolbarItem = () => {
emits('onIconClick', dialogRef);
if (isPC) {
outsideClick.listen({
domRefs: toolbarItemRef.value,
handler: closeToolbarItem,
});
}
if (!props.needDialog) {
return;
}
toggleDialogDisplay(!showDialog.value);
};
const closeToolbarItem = () => {
showDialog.value = false;
emits('onDialogClose', dialogRef);
};
const toggleDialogDisplay = (showStatus: boolean) => {
if (showDialog.value === showStatus) {
return;
}
showDialog.value = showStatus;
switch (showStatus) {
case true:
emits('onDialogShow', dialogRef);
break;
case false:
emits('onDialogClose', dialogRef);
}
};
const onPopupClose = () => {
showDialog.value = false;
};
defineExpose({
toggleDialogDisplay,
});
</script>
<style lang="scss" scoped src="./style/index.scss"></style>

View File

@@ -0,0 +1,6 @@
.toolbar-item-container {
&-dialog {
background: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
}

View File

@@ -0,0 +1,7 @@
.toolbar-item-container-h5 {
&-dialog {
position: static !important;
width: 100%;
box-shadow: none;
}
}

View File

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

View File

@@ -0,0 +1,36 @@
.toolbar-item-container-uni {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: static;
&-icon {
background: #fff;
border-radius: 15px;
width: 60px;
height: 60px;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
}
&-title {
font-size: 14px;
color: #8F959D;
}
&-dialog{
position: absolute !important;
background: transparent;
left: -10px;
bottom: -5px;
.toolbar-bottom-popup{
position: sticky;
}
}
}

View File

@@ -0,0 +1,24 @@
.toolbar-item-container {
position: relative;
&-icon {
padding: 8px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
&-dialog {
z-index: 5;
position: absolute;
background: #fff;
box-shadow: 0 2px 4px -3px rgba(32, 77, 141, 0.03), 0 6px 10px 1px rgba(32, 77, 141, 0.06), 0 3px 14px 2px rgba(32, 77, 141, 0.05);
width: fit-content;
height: fit-content;
bottom: 35px;
}
&-dialog-dark {
background: #22262E;
box-shadow: 0 8px 40px 0 rgba(23, 25, 31, 0.6), 0 4px 12px 0 rgba(23, 25, 31, 0.8);
}
}

View File

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

View File

@@ -0,0 +1,127 @@
<template>
<Dialog
:show="show"
:isH5="!isPC"
:isHeaderShow="false"
:isFooterShow="false"
:background="false"
@update:show="toggleShow"
>
<Transfer
:isSearch="true"
:title="title"
:list="searchMemberList"
:isH5="!isPC"
:isRadio="false"
@search="search"
@submit="submit"
@cancel="cancel"
/>
</Dialog>
</template>
<script setup lang="ts">
import {
TUIGroupService,
TUIUserService,
} from '@tencentcloud/chat-uikit-engine';
import { ref, computed, watch } from '../../../../adapter-vue';
import Dialog from '../../../common/Dialog/index.vue';
import Transfer from '../../../common/Transfer/index.vue';
import { isPC } from '../../../../utils/env';
const props = defineProps({
// type: voiceCall/groupCall/...
type: {
type: String,
default: '',
},
currentConversation: {
type: Object,
default: () => ({}),
},
isGroup: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['submit', 'cancel']);
const show = ref<boolean>(false);
const groupID = ref<string>('');
const memberList = ref<any[]>([]);
const searchMemberList = ref<any[]>([]);
const selfUserID = ref<string>('');
const titleMap: any = {
voiceCall: '发起群语音',
videoCall: '发起群视频',
};
const title = computed(() => {
return titleMap[props.type] ? titleMap[props.type] : '';
});
TUIUserService.getUserProfile().then((res: any) => {
if (res?.data?.userID) {
selfUserID.value = res.data.userID;
}
});
watch(
() => [props?.currentConversation?.conversationID, show.value],
(newVal: any, oldVal: any) => {
if (newVal && newVal !== oldVal) {
if (props.isGroup && show.value) {
groupID.value = props.currentConversation.groupProfile.groupID;
TUIGroupService.getGroupMemberList({
groupID: groupID.value,
}).then((res: any) => {
memberList.value = res?.data?.memberList?.filter(
(user: any) => user?.userID !== selfUserID.value,
);
searchMemberList.value = memberList.value;
});
} else {
groupID.value = '';
memberList.value = [];
searchMemberList.value = memberList.value;
}
}
},
{
immediate: true,
},
);
const search = (searchInfo: string) => {
const results = memberList.value?.filter(
(member: any) => member?.userID === searchInfo,
);
searchMemberList.value = results?.length ? results : memberList.value;
};
const submit = (selectedMemberList: string[]) => {
const userIDList: string[] = [];
selectedMemberList?.forEach((user: any) => {
user?.userID && userIDList.push(user.userID);
});
if (props.type === 'voiceCall') {
emits('submit', { userIDList, groupID: groupID.value, type: 1 });
} else if (props.type === 'videoCall') {
emits('submit', { userIDList, groupID: groupID.value, type: 2 });
}
searchMemberList.value = memberList.value;
toggleShow(false);
};
const cancel = () => {
searchMemberList.value = memberList.value;
emits('cancel');
toggleShow(false);
};
const toggleShow = (showStatus: boolean) => {
show.value = showStatus;
};
defineExpose({
toggleShow,
});
</script>

View File

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

View File

@@ -0,0 +1,155 @@
<template>
<ToolbarItemContainer
:iconFile="handleIcon()"
:title="handleTitle()"
:needDialog="false"
:iconWidth="isUniFrameWork ? '32px' : '20px'"
:iconHeight="isUniFrameWork
? props.videoSourceType === 'album'
? '20px'
: '25px'
: '18px'
"
@onIconClick="onIconClick"
>
<div :class="['video-upload', !isPC && 'video-upload-h5']">
<input
ref="inputRef"
title="视频"
type="file"
data-type="video"
accept="video/*"
@change="sendVideoInWeb"
>
</div>
</ToolbarItemContainer>
</template>
<script lang="ts" setup>
import TUIChatEngine, {
TUIChatService,
TUIStore,
StoreName,
IConversationModel,
SendMessageParams,
SendMessageOptions,
} from '@tencentcloud/chat-uikit-engine';
import { TUIGlobal } from '@tencentcloud/universal-api';
import { ref } from '../../../../adapter-vue';
import { isPC, isWeChat, isUniFrameWork } from '../../../../utils/env';
import ToolbarItemContainer from '../toolbar-item-container/index.vue';
import videoIconLight from '../../../../assets/icon/video-light.svg';
import videoIconDark from '../../../../assets/icon/video-dark.svg';
import videoUniIcon from '../../../../assets/icon/video-uni.png';
import cameraUniIcon from '../../../../assets/icon/camera-uni.png';
import { isEnabledMessageReadReceiptGlobal } from '../../utils/utils';
import OfflinePushInfoManager, { IOfflinePushInfoCreateParams } from '../../offlinePushInfoManager/index';
import TUIChatConfig from '../../config';
const props = defineProps({
// Video source, only valid for uni-app version, web version only supports selecting videos from files
// album: Select from files
// camera: Take a video using the camera
videoSourceType: {
type: String,
default: 'album',
},
});
const inputRef = ref();
const currentConversation = ref<IConversationModel>();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
});
const handleIcon = (): string => {
if (isUniFrameWork) {
switch (props.videoSourceType) {
case 'album':
return videoUniIcon;
case 'camera':
return cameraUniIcon;
default:
return videoUniIcon;
}
} else {
const videoIcon = TUIChatConfig.getTheme() === 'dark' ? videoIconDark : videoIconLight;
return videoIcon;
}
};
const handleTitle = (): string => {
if (isUniFrameWork && props.videoSourceType === 'camera') {
return '录制';
} else {
return '视频';
}
};
const onIconClick = () => {
// uni-app send video
if (isUniFrameWork) {
if (isWeChat && TUIGlobal?.chooseMedia) {
TUIGlobal?.chooseMedia({
mediaType: ['video'],
count: 1,
sourceType: [props.videoSourceType],
maxDuration: 60,
success: function (res: any) {
sendVideoMessage(res);
},
});
} else {
TUIGlobal?.chooseVideo({
count: 1,
sourceType: [props.videoSourceType],
compressed: false,
success: function (res: any) {
sendVideoMessage(res);
},
});
}
} else {
inputRef?.value?.click && inputRef?.value?.click();
}
};
const sendVideoInWeb = (e: any) => {
if (e?.target?.files?.length <= 0) {
return;
}
sendVideoMessage(e?.target);
e.target.value = '';
};
const sendVideoMessage = (file: any) => {
if (!file) {
return;
}
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
file,
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
} as SendMessageParams;
const offlinePushInfoCreateParams: IOfflinePushInfoCreateParams = {
conversation: currentConversation.value,
payload: options.payload,
messageType: TUIChatEngine.TYPES.MSG_VIDEO,
};
const sendMessageOptions: SendMessageOptions = {
offlinePushInfo: OfflinePushInfoManager.create(offlinePushInfoCreateParams),
};
TUIChatService.sendVideoMessage(options, sendMessageOptions);
};
</script>
<style lang="scss" scoped>
@import "../../../../assets/styles/common";
</style>

View File

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

View File

@@ -0,0 +1,95 @@
<template>
<ToolbarItemContainer
ref="container"
:iconFile="wordsIcon"
title="常用语"
:needBottomPopup="true"
:iconWidth="isUniFrameWork ? '26px' : '20px'"
:iconHeight="isUniFrameWork ? '26px' : '20px'"
@onDialogShow="onDialogShow"
@onDialogClose="onDialogClose"
>
<div :class="['words', !isPC && 'words-h5']">
<div :class="['words-header', !isPC && 'words-h5-header']">
<span :class="['words-header-title', !isPC && 'words-h5-header-title']">
{{ TUITranslateService.t("Words.常用语-快捷回复工具") }}
</span>
<span
v-if="!isPC"
:class="['words-header-close', !isPC && 'words-h5-header-close']"
@click="closeDialog"
>
关闭
</span>
</div>
<ul :class="['words-list', !isPC && 'words-h5-list']">
<li
v-for="(item, index) in wordsList"
:key="index"
:class="['words-list-item', !isPC && 'words-h5-list-item']"
@click="selectWord(item)"
>
{{ TUITranslateService.t(`Words.${item.value}`) }}
</li>
</ul>
</div>
</ToolbarItemContainer>
</template>
<script setup lang="ts">
import {
TUITranslateService,
TUIStore,
StoreName,
IConversationModel,
SendMessageParams,
TUIChatService,
} from '@tencentcloud/chat-uikit-engine';
import { ref } from '../../../../adapter-vue';
import ToolbarItemContainer from '../toolbar-item-container/index.vue';
import wordsIconLight from '../../../../assets/icon/words-light.svg';
import wordsIconDark from '../../../../assets/icon/words-dark.svg';
import { wordsList } from '../../utils/wordsList';
import { isEnabledMessageReadReceiptGlobal } from '../../utils/utils';
import { isPC, isUniFrameWork } from '../../../../utils/env';
import TUIChatConfig from '../../config';
const wordsIcon = TUIChatConfig.getTheme() === 'dark' ? wordsIconDark : wordsIconLight;
const emits = defineEmits(['onDialogPopupShowOrHide']);
const currentConversation = ref<IConversationModel>();
const container = ref();
TUIStore.watch(StoreName.CONV, {
currentConversation: (conversation: IConversationModel) => {
currentConversation.value = conversation;
},
});
const selectWord = (item: any) => {
const options = {
to:
currentConversation?.value?.groupProfile?.groupID
|| currentConversation?.value?.userProfile?.userID,
conversationType: currentConversation?.value?.type,
payload: {
text: TUITranslateService.t(`Words.${item.value}`),
},
needReadReceipt: isEnabledMessageReadReceiptGlobal(),
} as SendMessageParams;
TUIChatService.sendTextMessage(options);
// close dialog after submit evaluate
container?.value?.toggleDialogDisplay(false);
};
const closeDialog = () => {
container?.value?.toggleDialogDisplay(false);
};
const onDialogShow = () => {
emits('onDialogPopupShowOrHide', true);
};
const onDialogClose = () => {
emits('onDialogPopupShowOrHide', false);
};
</script>
<style scoped lang="scss" src="./style/index.scss"></style>

View File

@@ -0,0 +1,8 @@
.words {
background-color: #ffffff;
&-header {
&-close {
color: #3370ff;
}
}
}

View File

@@ -0,0 +1,29 @@
.words-h5 {
width: 100%;
box-sizing: border-box;
max-height: 80vh;
height: fit-content;
overflow: hidden;
display: flex;
flex-direction: column;
&-header {
&-title {
font-size: 18px;
line-height: 40px;
}
}
&-list {
flex: 1;
overflow-y: scroll;
&-item {
cursor: none;
-webkit-tap-highlight-color: transparent;
-moz-tap-highlight-color: transparent;
padding: 12px 0;
font-size: 16px;
color: #50545c;
line-height: 18px;
border-bottom: 1px solid #eeeeee;
}
}
}

View File

@@ -0,0 +1,5 @@
@import url("../../../../../assets/styles/common.scss");
@import "./color.scss";
@import "./web.scss";
@import "./h5.scss";

View File

@@ -0,0 +1,32 @@
.words {
z-index: 5;
width: 315px;
padding: 12px;
display: flex;
flex-direction: column;
width: 19.13rem;
height: 12.44rem;
overflow-y: auto;
&-header {
display: flex;
justify-content: space-between;
font-size: 14px;
font-weight: 500;
}
&-list {
flex: 1;
display: flex;
flex-direction: column;
cursor: pointer;
&-item {
cursor: pointer;
padding: 4px 0;
font-size: 14px;
color: #50545c;
line-height: 18px;
}
&-item:hover {
color: #006eff;
}
}
}