index.vue 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. <template>
  2. <div :class="['message-input', !isPC && 'message-input-h5']">
  3. <div class="flex-row">
  4. <MessageInputAudio
  5. v-if="isWeChat || isApp"
  6. :class="{
  7. 'message-input-audio': true,
  8. 'message-input-wx-audio': isWeChat,
  9. 'message-input-wx-audio-open': displayType === 'audio',
  10. }"
  11. :isEnableAudio="displayType === 'audio'"
  12. @changeDisplayType="changeDisplayType"
  13. />
  14. <MessageInputEditor
  15. v-show="displayType === 'editor'"
  16. ref="editor"
  17. class="message-input-editor"
  18. :placeholder="props.placeholder"
  19. :isMuted="props.isMuted"
  20. :muteText="props.muteText"
  21. :enableInput="props.enableInput"
  22. :enableAt="props.enableAt"
  23. :enableTyping="props.enableTyping"
  24. :jobInfoData="props.jobInfoData"
  25. :isGroup="isGroup"
  26. @onTyping="onTyping"
  27. @onAt="onAt"
  28. @onFocus="onFocus"
  29. />
  30. <MessageInputAt
  31. v-if="props.enableAt"
  32. ref="messageInputAtRef"
  33. @insertAt="insertAt"
  34. @onAtListOpen="onAtListOpen"
  35. />
  36. <div
  37. class="message-input-emoji"
  38. @click="changeToolbarDisplayType('emojiPicker')"
  39. >
  40. <Icon :file="faceIcon" class="icon icon-face" />
  41. </div>
  42. <div
  43. class="message-input-more"
  44. @click="changeToolbarDisplayType('tools')"
  45. >
  46. <Icon :file="moreIcon" class="icon icon-more" />
  47. </div>
  48. </div>
  49. <div>
  50. <MessageQuote :style="{ minWidth: 0 }" :displayType="displayType" />
  51. </div>
  52. </div>
  53. </template>
  54. <script setup lang="ts">
  55. import TUIChatEngine, {
  56. TUIStore,
  57. StoreName,
  58. IMessageModel,
  59. IConversationModel,
  60. } from "@tencentcloud/chat-uikit-engine";
  61. import { ref, watch, onMounted, onUnmounted } from "../../../adapter-vue";
  62. import MessageInputEditor from "./message-input-editor.vue";
  63. import MessageInputAt from "./message-input-at/index.vue";
  64. import MessageInputAudio from "./message-input-audio.vue";
  65. import MessageQuote from "./message-input-quote/index.vue";
  66. import Icon from "../../common/Icon.vue";
  67. import faceIcon from "../../../assets/icon/face-uni.png";
  68. import moreIcon from "../../../assets/icon/more-uni.png";
  69. import { isPC, isH5, isWeChat, isApp } from "../../../utils/env";
  70. import { sendTyping } from "../utils/sendMessage";
  71. import { ToolbarDisplayType, InputDisplayType } from "../../../interface";
  72. interface IProps {
  73. placeholder: string;
  74. isMuted?: boolean;
  75. muteText?: string;
  76. enableInput?: boolean;
  77. enableAt?: boolean;
  78. enableTyping?: boolean;
  79. replyOrReference?: Record<string, any>;
  80. inputToolbarDisplayType: ToolbarDisplayType;
  81. jobInfoData?: Object;
  82. }
  83. interface IEmits {
  84. (e: "changeToolbarDisplayType", displayType: ToolbarDisplayType): void;
  85. }
  86. const emits = defineEmits<IEmits>();
  87. const props = withDefaults(defineProps<IProps>(), {
  88. placeholder: "this is placeholder",
  89. replyOrReference: () => ({}),
  90. isMuted: true,
  91. muteText: "",
  92. enableInput: true,
  93. enableAt: true,
  94. enableTyping: true,
  95. inputToolbarDisplayType: "none",
  96. });
  97. const editor = ref();
  98. const messageInputAtRef = ref();
  99. const currentConversation = ref<IConversationModel>();
  100. const isGroup = ref<boolean>(false);
  101. const displayType = ref<InputDisplayType>("editor");
  102. onMounted(() => {
  103. TUIStore.watch(StoreName.CONV, {
  104. currentConversation: onCurrentConversationUpdated,
  105. });
  106. TUIStore.watch(StoreName.CHAT, {
  107. quoteMessage: onQuoteMessageUpdated,
  108. });
  109. });
  110. onUnmounted(() => {
  111. TUIStore.unwatch(StoreName.CONV, {
  112. currentConversation: onCurrentConversationUpdated,
  113. });
  114. TUIStore.unwatch(StoreName.CHAT, {
  115. quoteMessage: onQuoteMessageUpdated,
  116. });
  117. });
  118. watch(
  119. () => props.inputToolbarDisplayType,
  120. (newVal: ToolbarDisplayType) => {
  121. if (newVal !== "none") {
  122. changeDisplayType("editor");
  123. }
  124. }
  125. );
  126. function changeDisplayType(display: InputDisplayType) {
  127. displayType.value = display;
  128. if (display === "audio") {
  129. emits("changeToolbarDisplayType", "none");
  130. }
  131. }
  132. function changeToolbarDisplayType(displayType: ToolbarDisplayType) {
  133. emits("changeToolbarDisplayType", displayType);
  134. }
  135. const onTyping = (inputContentEmpty: boolean, inputBlur: boolean) => {
  136. sendTyping(inputContentEmpty, inputBlur);
  137. };
  138. const onAt = (show: boolean) => {
  139. messageInputAtRef?.value?.toggleAtList(show);
  140. };
  141. const onFocus = () => {
  142. if (isH5) {
  143. emits("changeToolbarDisplayType", "none");
  144. }
  145. };
  146. const insertEmoji = (emoji: any) => {
  147. editor?.value?.addEmoji && editor?.value?.addEmoji(emoji);
  148. };
  149. const insertAt = (atInfo: any) => {
  150. editor?.value?.insertAt && editor?.value?.insertAt(atInfo);
  151. };
  152. const onAtListOpen = () => {
  153. editor?.value?.blur && editor?.value?.blur();
  154. };
  155. // 消息撤回后重新编辑
  156. const reEdit = (content: any) => {
  157. editor?.value?.resetEditor();
  158. editor?.value?.setEditorContent(content);
  159. };
  160. function onCurrentConversationUpdated(conversation: IConversationModel) {
  161. currentConversation.value = conversation;
  162. isGroup.value =
  163. currentConversation.value?.type === TUIChatEngine.TYPES.CONV_GROUP;
  164. }
  165. function onQuoteMessageUpdated(options?: {
  166. message: IMessageModel;
  167. type: string;
  168. }) {
  169. // 当有引用消息时切换为文字输入模式
  170. // switch text input mode when there is a quote message
  171. if (options?.message && options?.type === "quote") {
  172. changeDisplayType("editor");
  173. }
  174. }
  175. defineExpose({
  176. insertEmoji,
  177. reEdit,
  178. });
  179. </script>
  180. <style scoped lang="scss">
  181. @import "../../../assets/styles/common";
  182. .message-input {
  183. position: relative;
  184. display: flex;
  185. flex-direction: column;
  186. border: none;
  187. height: 100%;
  188. width: 100%;
  189. max-height: 100%;
  190. max-width: 100%;
  191. overflow: hidden;
  192. background: #ebf0f6;
  193. &-editor {
  194. flex: 1;
  195. display: flex;
  196. }
  197. &-button {
  198. width: fit-content;
  199. }
  200. .icon {
  201. width: 23px;
  202. height: 23px;
  203. &-face {
  204. margin: 7px;
  205. }
  206. &-more {
  207. margin: 7px 0;
  208. }
  209. }
  210. &-wx {
  211. &-audio {
  212. &-open {
  213. flex: 1;
  214. }
  215. }
  216. }
  217. &-emoji-picker {
  218. padding-top: 10px;
  219. }
  220. }
  221. .message-input-h5 {
  222. display: flex;
  223. flex-direction: column;
  224. height: calc(100% - 20px);
  225. width: calc(100% - 20px);
  226. max-height: 100%;
  227. max-width: calc(100% - 20px);
  228. padding: 10px;
  229. overflow: hidden;
  230. }
  231. .flex-row {
  232. display: flex;
  233. flex-direction: row;
  234. }
  235. </style>