index.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. <template>
  2. <div
  3. v-if="isScrollButtonVisible"
  4. class="scroll-button"
  5. @click="scrollToMessageListBottom"
  6. >
  7. <Icon
  8. width="10px"
  9. height="10px"
  10. :file="doubleArrowIcon"
  11. />
  12. <div class="scroll-button-text">
  13. {{ scrollButtonContent }}
  14. </div>
  15. </div>
  16. </template>
  17. <script lang="ts" setup>
  18. import { ref, onMounted, onUnmounted, computed, watch } from '../../../../adapter-vue';
  19. import {
  20. TUIStore,
  21. StoreName,
  22. IMessageModel,
  23. IConversationModel,
  24. TUITranslateService,
  25. } from '@tencentcloud/chat-uikit-engine';
  26. import Icon from '../../../common/Icon.vue';
  27. import doubleArrowIcon from '../../../../assets/icon/double-arrow.svg';
  28. import { getBoundingClientRect } from '@tencentcloud/universal-api';
  29. import { JSONToObject } from '../../../../utils';
  30. interface IEmits {
  31. (key: 'scrollToLatestMessage'): void;
  32. }
  33. const emits = defineEmits<IEmits>();
  34. const messageList = ref<IMessageModel[]>([]);
  35. const currentConversationID = ref<string>('');
  36. const currentLastMessageTime = ref<number>(0);
  37. const newMessageCount = ref<number>(0);
  38. const isScrollOverOneScreen = ref<boolean>(false);
  39. const isExistLastMessage = ref<boolean>(false);
  40. const isScrollButtonVisible = ref<boolean>(false);
  41. const scrollButtonContent = computed(() =>
  42. newMessageCount.value ? `${newMessageCount.value}${TUITranslateService.t('TUIChat.条新消息')}` : TUITranslateService.t('TUIChat.回到最新位置'),
  43. );
  44. watch(() => [isScrollOverOneScreen.value, isExistLastMessage.value],
  45. () => {
  46. isScrollButtonVisible.value = isScrollOverOneScreen.value || isExistLastMessage.value;
  47. if (!isScrollButtonVisible.value) {
  48. resetNewMessageCount();
  49. }
  50. },
  51. { immediate: true },
  52. );
  53. onMounted(() => {
  54. TUIStore.watch(StoreName.CHAT, {
  55. messageList: onMessageListUpdated,
  56. newMessageList: onNewMessageListUpdated,
  57. });
  58. TUIStore.watch(StoreName.CONV, {
  59. currentConversation: onCurrentConversationUpdated,
  60. });
  61. });
  62. onUnmounted(() => {
  63. TUIStore.unwatch(StoreName.CHAT, {
  64. messageList: onMessageListUpdated,
  65. newMessageList: onNewMessageListUpdated,
  66. });
  67. TUIStore.unwatch(StoreName.CONV, {
  68. currentConversation: onCurrentConversationUpdated,
  69. });
  70. });
  71. function isTypingMessage(message: IMessageModel): boolean {
  72. return JSONToObject(message.payload?.data)?.businessID === 'user_typing_status';
  73. }
  74. function onMessageListUpdated(newMessageList: IMessageModel[]) {
  75. messageList.value = newMessageList || [];
  76. const lastMessage = messageList.value?.[messageList.value?.length - 1];
  77. isExistLastMessage.value = !!(
  78. lastMessage && lastMessage?.time < currentLastMessageTime?.value
  79. );
  80. }
  81. function onNewMessageListUpdated(newMessageList: IMessageModel[]) {
  82. if (Array.isArray(newMessageList) && isScrollButtonVisible.value) {
  83. newMessageList.forEach((message: IMessageModel) => {
  84. if (message && message.conversationID === currentConversationID.value && !message.isDeleted && !message.isRevoked && !isTypingMessage(message)) {
  85. newMessageCount.value += 1;
  86. }
  87. });
  88. }
  89. }
  90. function onCurrentConversationUpdated(conversation: IConversationModel | undefined) {
  91. if (conversation?.conversationID !== currentConversationID.value) {
  92. resetNewMessageCount();
  93. }
  94. currentConversationID.value = conversation?.conversationID || '';
  95. currentLastMessageTime.value = conversation?.lastMessage?.lastTime || 0;
  96. }
  97. // When the scroll height of the message list upwards is greater than one screen, show scrolling to the latest tips.
  98. async function judgeScrollOverOneScreen(e: Event) {
  99. if (e.target) {
  100. try {
  101. const { height } = await getBoundingClientRect(`#${(e.target as HTMLElement)?.id}`, 'messageList') || {};
  102. const scrollHeight = (e.target as HTMLElement)?.scrollHeight || (e.detail as HTMLElement)?.scrollHeight;
  103. const scrollTop = (e.target as HTMLElement)?.scrollTop || (e.detail as HTMLElement)?.scrollTop || 0;
  104. // while scroll over one screen show this scroll button.
  105. if (scrollHeight - scrollTop > 2 * height) {
  106. isScrollOverOneScreen.value = true;
  107. return;
  108. }
  109. isScrollOverOneScreen.value = false;
  110. } catch (error) {
  111. isScrollOverOneScreen.value = false;
  112. }
  113. }
  114. }
  115. // reset messageSource
  116. function resetMessageSource() {
  117. if (TUIStore.getData(StoreName.CHAT, 'messageSource') !== undefined) {
  118. TUIStore.update(StoreName.CHAT, 'messageSource', undefined);
  119. }
  120. }
  121. // reset newMessageCount
  122. function resetNewMessageCount() {
  123. newMessageCount.value = 0;
  124. }
  125. function scrollToMessageListBottom() {
  126. resetMessageSource();
  127. resetNewMessageCount();
  128. emits('scrollToLatestMessage');
  129. }
  130. defineExpose({
  131. judgeScrollOverOneScreen,
  132. isScrollButtonVisible,
  133. });
  134. </script>
  135. <style scoped lang="scss">
  136. .scroll-button {
  137. position: absolute;
  138. bottom: 10px;
  139. right: 10px;
  140. width: 92px;
  141. height: 28px;
  142. background: #fff;
  143. border: 1px solid #e0e0e0;
  144. box-shadow: 0 4px 12px -5px rgba(0, 0, 0, 0.1);
  145. display: flex;
  146. flex-direction: row;
  147. align-items: center;
  148. justify-content: center;
  149. border-radius: 3px;
  150. cursor: pointer;
  151. -webkit-tap-highlight-color: transparent;
  152. &-text {
  153. font-family: PingFangSC-Regular, system-ui;
  154. font-size: 10px;
  155. color: #147aff;
  156. margin-left: 3px;
  157. }
  158. }
  159. </style>