KeFuChatBox.vue 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <template>
  2. <el-container v-if="showChatBox" class="kefu">
  3. <el-header>
  4. <div class="kefu-title">{{ keFuConversation.userNickname }}</div>
  5. </el-header>
  6. <el-main class="kefu-content" style="overflow: visible">
  7. <el-scrollbar ref="scrollbarRef" always height="calc(100vh - 495px)">
  8. <div ref="innerRef" class="w-[100%] pb-3px">
  9. <div
  10. v-for="item in messageList"
  11. :key="item.id"
  12. :class="[
  13. item.senderType === UserTypeEnum.MEMBER
  14. ? `ss-row-left`
  15. : item.senderType === UserTypeEnum.ADMIN
  16. ? `ss-row-right`
  17. : ''
  18. ]"
  19. class="flex mb-20px w-[100%]"
  20. >
  21. <el-avatar
  22. v-show="item.senderType === UserTypeEnum.MEMBER"
  23. :src="keFuConversation.userAvatar"
  24. alt="avatar"
  25. />
  26. <div class="kefu-message p-10px">
  27. <!-- 文本消息 -->
  28. <template v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType">
  29. <div
  30. v-dompurify-html="replaceEmoji(item.content)"
  31. :class="[
  32. item.senderType === UserTypeEnum.MEMBER
  33. ? `ml-10px`
  34. : item.senderType === UserTypeEnum.ADMIN
  35. ? `mr-10px`
  36. : ''
  37. ]"
  38. class="flex items-center"
  39. ></div>
  40. </template>
  41. <template v-else>
  42. {{ item.content }}
  43. </template>
  44. </div>
  45. <el-avatar
  46. v-show="item.senderType === UserTypeEnum.ADMIN"
  47. :src="item.senderAvatar"
  48. alt="avatar"
  49. />
  50. </div>
  51. </div>
  52. </el-scrollbar>
  53. </el-main>
  54. <el-footer height="230px">
  55. <div class="h-[100%]">
  56. <div class="chat-tools">
  57. <EmojiSelectPopover @select-emoji="handleEmojiSelect" />
  58. </div>
  59. <el-input v-model="message" :rows="6" type="textarea" />
  60. <div class="h-45px flex justify-end">
  61. <el-button class="mt-10px" type="primary" @click="handleSendMessage">发送</el-button>
  62. </div>
  63. </div>
  64. </el-footer>
  65. </el-container>
  66. <el-empty v-else description="请选择左侧的一个会话后开始" />
  67. </template>
  68. <script lang="ts" setup>
  69. import { ElScrollbar as ElScrollbarType } from 'element-plus'
  70. import { KeFuMessageApi, KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
  71. import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
  72. import EmojiSelectPopover from './EmojiSelectPopover.vue'
  73. import { Emoji, replaceEmoji } from './emoji'
  74. import { KeFuMessageContentTypeEnum } from './constants'
  75. import { isEmpty } from '@/utils/is'
  76. import { UserTypeEnum } from '@/utils/constants'
  77. defineOptions({ name: 'KeFuMessageBox' })
  78. const messageTool = useMessage()
  79. const message = ref('') // 消息
  80. const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
  81. const keFuConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 用户会话
  82. const poller = ref<any>(null) // TODO puhui999: 轮训定时器,暂时模拟 websocket
  83. // 获得消息 TODO puhui999: 先不考虑下拉加载历史消息
  84. const getMessageList = async (conversation: KeFuConversationRespVO) => {
  85. keFuConversation.value = conversation
  86. const { list } = await KeFuMessageApi.getKeFuMessagePage({
  87. pageNo: 1,
  88. conversationId: conversation.id
  89. })
  90. messageList.value = list.reverse()
  91. // TODO puhui999: 首次加载时滚动到最新消息,如果加载的是历史消息则不滚动
  92. await scrollToBottom()
  93. // TODO puhui999: 轮训相关,功能完善后移除
  94. if (!poller.value) {
  95. poller.value = setInterval(() => {
  96. getMessageList(conversation)
  97. }, 1000)
  98. }
  99. }
  100. defineExpose({ getMessageList })
  101. // 是否显示聊天区域
  102. const showChatBox = computed(() => !isEmpty(keFuConversation.value))
  103. // 处理表情选择
  104. const handleEmojiSelect = (item: Emoji) => {
  105. message.value += item.name
  106. }
  107. // 发送消息
  108. const handleSendMessage = async () => {
  109. // 1. 校验消息是否为空
  110. if (isEmpty(unref(message.value))) {
  111. messageTool.warning('请输入消息后再发送哦!')
  112. }
  113. // 2. 组织发送消息
  114. const msg = {
  115. conversationId: keFuConversation.value.id,
  116. contentType: KeFuMessageContentTypeEnum.TEXT,
  117. content: message.value
  118. }
  119. await KeFuMessageApi.sendKeFuMessage(msg)
  120. message.value = ''
  121. // 3. 加载消息列表
  122. await getMessageList(keFuConversation.value)
  123. // 滚动到最新消息处
  124. await scrollToBottom()
  125. }
  126. const innerRef = ref<HTMLDivElement>()
  127. const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
  128. // 滚动到底部
  129. const scrollToBottom = async () => {
  130. await nextTick()
  131. scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
  132. }
  133. // TODO puhui999: 轮训相关,功能完善后移除
  134. onBeforeUnmount(() => {
  135. if (!poller.value) {
  136. return
  137. }
  138. clearInterval(poller.value)
  139. })
  140. </script>
  141. <style lang="scss" scoped>
  142. .kefu {
  143. &-title {
  144. border-bottom: #e4e0e0 solid 1px;
  145. height: 60px;
  146. line-height: 60px;
  147. }
  148. &-content {
  149. .ss-row-left {
  150. justify-content: flex-start;
  151. .kefu-message {
  152. margin-left: 20px;
  153. position: relative;
  154. &::before {
  155. content: '';
  156. width: 10px;
  157. height: 10px;
  158. left: -19px;
  159. top: calc(50% - 10px);
  160. position: absolute;
  161. border-left: 5px solid transparent;
  162. border-bottom: 5px solid transparent;
  163. border-top: 5px solid transparent;
  164. border-right: 5px solid #ffffff;
  165. }
  166. }
  167. }
  168. .ss-row-right {
  169. justify-content: flex-end;
  170. .kefu-message {
  171. margin-right: 20px;
  172. position: relative;
  173. &::after {
  174. content: '';
  175. width: 10px;
  176. height: 10px;
  177. right: -19px;
  178. top: calc(50% - 10px);
  179. position: absolute;
  180. border-left: 5px solid #ffffff;
  181. border-bottom: 5px solid transparent;
  182. border-top: 5px solid transparent;
  183. border-right: 5px solid transparent;
  184. }
  185. }
  186. }
  187. // 消息气泡
  188. .kefu-message {
  189. color: #333;
  190. border-radius: 5px;
  191. box-shadow: 3px 5px 15px rgba(0, 0, 0, 0.2);
  192. padding: 5px 10px;
  193. width: auto;
  194. max-width: 50%;
  195. text-align: left;
  196. display: inline-block !important;
  197. position: relative;
  198. word-break: break-all;
  199. background-color: #ffffff;
  200. transition: all 0.2s;
  201. &:hover {
  202. transform: scale(1.03);
  203. }
  204. }
  205. }
  206. .chat-tools {
  207. width: 100%;
  208. border: #e4e0e0 solid 1px;
  209. height: 44px;
  210. display: flex;
  211. align-items: center;
  212. }
  213. ::v-deep(textarea) {
  214. resize: none;
  215. }
  216. }
  217. </style>