index.vue 5.3 KB

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