当前位置:   article > 正文

vue3.0+websocket 实现聊天室功能_vue 聊天室

vue 聊天室
  1. <template>
  2. <div class="container" ref="chatWrapper">
  3. <div class="header">
  4. <head-bar class="chat-head" :title="'Chat'" @goback="goBack"></head-bar>
  5. </div>
  6. <div class="content" ref="chatContainer">
  7. <lottieLoad v-if="loadingMore"></lottieLoad>
  8. <div
  9. class="chat-item"
  10. :id="`chat-item-${index}`"
  11. v-for="(item, index) in chatList"
  12. :key="index"
  13. :style="{ flexDirection: item.sender === 'me' ? 'row-reverse' : 'row' }"
  14. >
  15. <div class="message-avatar">
  16. <img v-if="item.from_client_avatar" :src="item.from_client_avatar" />
  17. <img v-else src="@/assets/images/userProfile.png" alt="" />
  18. </div>
  19. <div class="message-content">
  20. <div
  21. class="message-header"
  22. :style="{ textAlign: item.sender === 'me' ? 'right' : 'left' }"
  23. >
  24. <span class="message-username">{{ item.from_client_name }}</span>
  25. <span class="message-time">
  26. {{ item.time ? item.time.split(' ')[1].substr(0, 5) : '' }}
  27. </span>
  28. </div>
  29. <div
  30. class="message-text"
  31. v-html="item.content"
  32. v-if="!item.is_img"
  33. :style="{ float: item.sender === 'me' ? 'right' : 'left' }"
  34. ></div>
  35. <div
  36. v-else
  37. class="message-text massage-img"
  38. @click="showMoreImg(item, index)"
  39. :style="{ float: item.sender === 'me' ? 'right' : 'left' }"
  40. >
  41. <!-- <van-image :src="item.content">
  42. @click="showMoreImg(item, index)"
  43. <template v-slot:loading>
  44. <van-loading type="spinner" size="20" />
  45. </template>
  46. </van-image> -->
  47. <img v-if="item.content" :src="item.content" alt="" />
  48. </div>
  49. </div>
  50. </div>
  51. <div class="toBottom" @click="scrollToBottom1" v-if="newMessageNum > 0">
  52. <img src="@/assets/icons/new-message.png" alt="" />
  53. <span class="msg-num">{{ newMessageNum }}</span>
  54. <span>See the latest news</span>
  55. </div>
  56. </div>
  57. <!-- 聊天输入 -->
  58. <div class="footer">
  59. <van-row gutter="10">
  60. <van-col span="18">
  61. <div class="searchInput" ref="mesageInput">
  62. <V3Emoji
  63. size="small"
  64. :textAreaOption="{ placeholder: 'Your Message' }"
  65. :custom-theme="customTheme"
  66. :textArea="true"
  67. ref="emoji"
  68. v-model="chatContent"
  69. class="emoji"
  70. >
  71. <img
  72. class="publicIcon photoImg"
  73. src="@/assets/icons/photoAlbum.png"
  74. alt=""
  75. @click.stop="handlePhoto"
  76. />
  77. <img class="publicIcon" src="@/assets/icons/emote.png" alt="" />
  78. </V3Emoji>
  79. </div>
  80. </van-col>
  81. <van-col span="6">
  82. <van-button
  83. style="height: 50px"
  84. round
  85. block
  86. type="primary"
  87. native-type="submit"
  88. @click="sendMessage"
  89. >
  90. Send
  91. <!-- {{ t('ey.send') }} -->
  92. </van-button>
  93. </van-col>
  94. </van-row>
  95. <!-- 图片 -->
  96. <input
  97. type="file"
  98. ref="fileInput"
  99. style="display: none"
  100. accept="image/*"
  101. @change="handleFileChange"
  102. />
  103. </div>
  104. </div>
  105. </template>

js 部分。

  1. <script lang="ts">
  2. import {
  3. defineComponent,
  4. ref,
  5. onMounted,
  6. toRefs,
  7. nextTick,
  8. watch,
  9. onUpdated,
  10. computed,
  11. } from 'vue'
  12. import { useRouter } from 'vue-router'
  13. import { useStore } from 'vuex'
  14. import headBar from '@/components/model/headModel.vue'
  15. import V3Emoji from 'vue3-emoji'
  16. import 'vue3-emoji/dist/style.css'
  17. import websocket from '@/utils/websocket'
  18. import { getChatRoomBind, uploadImg, getHistoryRecord } from '@/api/chatApi'
  19. import { useI18n } from 'vue-i18n'
  20. import lottieLoad from '@/components/model/lottieLoad.vue'
  21. import $okToast from '@/views/mobile/compontents/tipsDialog'
  22. import { load } from '@/views/mobile/compontents/lodding/index'
  23. export default defineComponent({
  24. name: 'userInfo',
  25. components: {
  26. headBar,
  27. friendRequest,
  28. V3Emoji,
  29. lottieLoad,
  30. },
  31. setup() {
  32. const { t } = useI18n()
  33. const store = useStore()
  34. const router = useRouter()
  35. const chatList: any = ref([])
  36. const isLogin = ref<boolean>(false)
  37. let ws
  38. const state = reactive({
  39. friendShow: false,
  40. })
  41. watch(
  42. () => store.state.chat.client_id,
  43. (id) => {
  44. client_id.value = id
  45. }
  46. )
  47. const loadingMore = ref(false)
  48. onMounted(async () => {
  49. await getHistory()
  50. const element: any = chatContainer.value
  51. if (element) {
  52. chatList.value.unshift(...sliceHistoryList.value.reverse())
  53. await nextTick()
  54. if (chatContainer.value) {
  55. scrollToBottom()
  56. }
  57. element.addEventListener('scroll', (e) => {
  58. let scrollH = element.scrollHeight
  59. let clientH = element.clientHeight
  60. let scrollT = element.scrollTop
  61. if (clientH + scrollT >= scrollH) {
  62. newMessageNum.value = 0
  63. }
  64. if (scrollT <= 0) {
  65. if (
  66. sliceNum.value <= historyList.value.length &&
  67. !loadingMore.value
  68. ) {
  69. sliceNum.value += 10
  70. }
  71. loadingMore.value = true
  72. if (sliceHistoryList.value.length > 0) {
  73. setTimeout(async () => {
  74. chatList.value.unshift(...sliceHistoryList.value.reverse())
  75. await nextTick()
  76. let firstEle = element.querySelector(
  77. `#chat-item-${sliceHistoryList.value.length}`
  78. )
  79. element.scrollTo({
  80. top: firstEle.offsetTop - 42,
  81. behavior: 'instant',
  82. })
  83. loadingMore.value = false
  84. }, 1000)
  85. } else {
  86. setTimeout(() => {
  87. loadingMore.value = false
  88. // message.warning('no more')
  89. }, 500)
  90. }
  91. }
  92. })
  93. }
  94. })
  95. const sliceNum = ref(0)
  96. const newMessageNum = ref(0)
  97. const client_id = ref()
  98. const isBind = ref(false)
  99. const chatContainer = ref()
  100. //监听信息
  101. const handleMessage = () => {
  102. ws.onMessage((data) => {
  103. let msg = JSON.parse(data)
  104. if (msg.type == 'connect') {
  105. client_id.value = msg.client_id
  106. // store.commit('setClientId', msg.client_id)
  107. if (isLogin.value) {
  108. getChatRoomBind(client_id.value).then((res: any) => {
  109. if (res.code == 200) {
  110. isBind.value = true
  111. }
  112. })
  113. }
  114. } else if (msg.type == 'login') {
  115. $okToast.show({
  116. type: 'warn',
  117. content: 'please login',
  118. textAlign: 'left',
  119. })
  120. } else {
  121. if (msg.from_user_id == store.getters.userInfo.user_id) {
  122. msg.sender = 'me'
  123. chatList.value.push(msg)
  124. newMessageNum.value = 0
  125. scrollToBottom()
  126. } else {
  127. msg.sender = 'other'
  128. chatList.value.push(msg)
  129. const element: any = chatContainer.value
  130. nextTick(() => {
  131. if (element) {
  132. const lastElement = element.querySelector(
  133. '.chat-item:last-child'
  134. )
  135. let observe = new IntersectionObserver((entries) => {
  136. //观察该el是否进入可视区域
  137. if (entries[0].isIntersecting) {
  138. newMessageNum.value = 0
  139. scrollToBottom()
  140. } else {
  141. newMessageNum.value += 1
  142. }
  143. //停止观察
  144. observe.unobserve(lastElement)
  145. })
  146. //开始观察
  147. observe.observe(lastElement)
  148. }
  149. })
  150. }
  151. }
  152. })
  153. }
  154. watch(
  155. () => store.getters.token,
  156. (token) => {
  157. if (token.access_token) {
  158. isLogin.value = true
  159. ws = new websocket('websocket地址')
  160. handleMessage()
  161. } else {
  162. isLogin.value = false
  163. ws && ws.close(0)
  164. }
  165. },
  166. {
  167. immediate: true,
  168. }
  169. )
  170. watch(
  171. () => store.state.dailyDate.chatStatus,
  172. (flag) => {
  173. if (flag) {
  174. scrollToBottom()
  175. }
  176. }
  177. )
  178. // onUpdated(() => {
  179. // if (newMessageNum.value == 0) {
  180. // scrollToBottom()
  181. // }
  182. // })
  183. //滚动到底部
  184. const scrollToBottom = () => {
  185. const element: any = chatContainer.value
  186. const lastElement = element?.querySelector('.chat-item:last-child')
  187. if (lastElement) {
  188. setTimeout(() => {
  189. newMessageNum.value = 0
  190. lastElement.scrollIntoView({ behavior: 'smooth' })
  191. }, 0)
  192. }
  193. }
  194. const handleLink = () => {
  195. router.push({ name: 'Demo' })
  196. }
  197. let socket: any = null
  198. const handleCancle = () => {
  199. state.friendShow = false
  200. }
  201. // 确认
  202. const handleSubmit = () => {
  203. state.friendShow = false
  204. }
  205. // e, isImg = false
  206. const sendMessage = () => {
  207. if (!isLogin.value) {
  208. $okToast.show({
  209. type: 'warn',
  210. content: 'please login',
  211. textAlign: 'left',
  212. })
  213. return
  214. }
  215. if (!isBind.value) {
  216. $okToast.show({
  217. type: 'warn',
  218. content: 'please reload',
  219. textAlign: 'left',
  220. })
  221. return
  222. }
  223. let msg = {
  224. chat: 'group',
  225. type: 'say',
  226. content: chatContent.value,
  227. is_img: false,
  228. }
  229. ws.send(msg)
  230. chatContent.value = ''
  231. }
  232. //获取历史信息
  233. const historyList = ref<any[]>([])
  234. const getHistory = () => {
  235. let params = {
  236. limit: 100,
  237. }
  238. let ajax = getHistoryRecord(params).then((res) => {
  239. if (res.code == 200) {
  240. res.data.forEach((item) => {
  241. if (item.from_user_id == store.getters.userInfo.user_id) {
  242. item.sender = 'me'
  243. }
  244. })
  245. historyList.value = res.data
  246. }
  247. })
  248. return ajax
  249. }
  250. const sliceHistoryList = computed(() => {
  251. return historyList.value.slice(sliceNum.value, sliceNum.value + 10)
  252. })
  253. //
  254. const customTheme = {
  255. 'V3Emoji-hoverColor': '#303237',
  256. 'V3Emoji-activeColor': '#303237',
  257. 'V3Emoji-shadowColor': 'none',
  258. 'V3Emoji-backgroundColor': '#222428',
  259. 'V3Emoji-fontColor': '#ffffff',
  260. }
  261. const chatContent = ref('')
  262. const fileInput = ref<HTMLInputElement | null>(null)
  263. const handlePhoto = () => {
  264. if (fileInput.value) {
  265. fileInput.value.click()
  266. }
  267. }
  268. // 相册图片发送
  269. const handleFileChange = () => {
  270. if (fileInput.value?.files) {
  271. // 处理文件
  272. const file = fileInput.value.files[0]
  273. if (file.size > 5 * 1024 * 1024) {
  274. $okToast.show({
  275. type: 'warn',
  276. content: 'The picture is too large',
  277. textAlign: 'left',
  278. })
  279. return
  280. }
  281. const formData = new FormData()
  282. formData.append('file', file)
  283. load.show('Image uploading in progress...')
  284. uploadImg(formData).then((res) => {
  285. if (res.code == 200) {
  286. let url = res.data
  287. const charContent = {
  288. chat: 'group',
  289. type: 'say',
  290. content: url,
  291. is_img: true,
  292. }
  293. if (fileInput.value) fileInput.value.value = ''
  294. ws.send(charContent)
  295. load.hide()
  296. } else {
  297. load.hide()
  298. }
  299. })
  300. // 发送formData到服务器 loadingDirective
  301. }
  302. }
  303. const goBack = () => {
  304. router.go(-1)
  305. }
  306. const scrollToBottom1 = () => {
  307. if (chatContainer.value) {
  308. chatContainer.value.scrollTop = chatContainer.value.scrollHeight
  309. }
  310. }
  311. const images = ref()
  312. const imgShow = ref(false)
  313. const imgIndex = ref(0)
  314. const showMoreImg = (e, index) => {
  315. imgShow.value = true
  316. const newImages: any = []
  317. chatList.value.map((item, indet) => {
  318. if (item.is_img) {
  319. newImages.push({
  320. img: item.content,
  321. indexs: indet,
  322. })
  323. }
  324. })
  325. images.value = newImages
  326. imgIndex.value = index
  327. const abs = images.value.findIndex(function (element) {
  328. return element.indexs === index
  329. })
  330. imgIndex.value = abs
  331. }
  332. return {
  333. ...toRefs(state),
  334. handleFileChange,
  335. fileInput,
  336. handlePhoto,
  337. customTheme,
  338. chatContent,
  339. handleLink,
  340. socket,
  341. t,
  342. chatList,
  343. handleCancle,
  344. sendMessage,
  345. scrollToBottom,
  346. newMessageNum,
  347. sliceHistoryList,
  348. chatContainer,
  349. loadingMore,
  350. goBack,
  351. scrollToBottom1,
  352. images,
  353. imgIndex,
  354. handleSubmit,
  355. showMoreImg,
  356. imgShow,
  357. }
  358. },
  359. })
  360. </script>

3.websocket.ts

  1. import store from '@/store'
  2. import { getChatRoomBind } from '@/api/chatApi'
  3. let heartBeat, //心跳得定时器
  4. serverHeartBeat, //服务器响应的定时器
  5. beat_time = 50000, //心跳时间间隔
  6. reconnectTimer,
  7. reconnectNum = 3, //重连次数
  8. reconnectFlag = true, //控制重连,一次一次来
  9. beat_data = {
  10. chat:"ping",
  11. }
  12. let client_id
  13. let isAccident = 1
  14. export default class WebSocketClient {
  15. private ws: WebSocket | any;
  16. private url;
  17. private message_func;
  18. constructor(url: string) {
  19. this.url = url
  20. this.initWebSocket(url)
  21. }
  22. initWebSocket(url) {
  23. this.ws = new WebSocket(url);
  24. this.ws.onopen = () => {
  25. //重连之后需要再绑定
  26. // if (reconnectNum < 3 && store.getters.token.access_token) {
  27. // getChatRoomBind(store.state.chat.client_id)
  28. // }
  29. // 开始心跳
  30. console.log('链接成功');
  31. this.heartBeat(1)
  32. reconnectTimer && clearTimeout(reconnectTimer)
  33. };
  34. this.ws.onmessage = (event) => {
  35. let msg = JSON.parse(event.data)
  36. if (msg.chat === 'pong') {
  37. console.log('正常');
  38. serverHeartBeat && clearTimeout(serverHeartBeat)
  39. }
  40. if (msg.type == 'connect' && reconnectNum < 3) {
  41. client_id = msg.client_id
  42. store.commit('setClientId', msg.client_id)
  43. if (reconnectNum < 3 && store.getters.token.access_token) {
  44. getChatRoomBind(client_id)
  45. reconnectNum = 3
  46. }
  47. }
  48. if (msg.chat !== 'pong') {
  49. if (this.message_func) {
  50. this.onMessage(this.message_func)
  51. }
  52. }
  53. }
  54. this.ws.onclose = () => {
  55. console.log('断线,onclose');
  56. if (store.getters.token.access_token && isAccident == 1) {
  57. this.reconnect()
  58. }
  59. this.heartBeat(2)
  60. };
  61. this.ws.onerror = (error: Event) => {
  62. if (store.getters.token.access_token) {
  63. this.reconnect()
  64. }
  65. this.heartBeat(2)
  66. };
  67. }
  68. public send(data: any) {
  69. if (this.ws.readyState === WebSocket.OPEN) {
  70. this.ws.send(JSON.stringify(data));
  71. } else {
  72. console.error('WebSocket未连接');
  73. }
  74. }
  75. public close(accident = 1) {
  76. console.log('断线,close');
  77. this.heartBeat(2)
  78. this.ws.close();
  79. isAccident = accident
  80. //是否是主动断开的,1意外,0主动
  81. if (accident == 1) {
  82. this.reconnect()
  83. }
  84. }
  85. // 监听消息
  86. onMessage(callback: (data: any) => void): void {
  87. this.ws.onmessage = event => {
  88. this.message_func = callback
  89. //忽略心跳返回信息
  90. let msg = JSON.parse(event.data)
  91. if (msg.chat !== 'pong') {
  92. callback(event.data);
  93. }
  94. if (msg.chat === 'pong') {
  95. serverHeartBeat && clearTimeout(serverHeartBeat)
  96. }
  97. }
  98. }
  99. onOpen(callback: (data:any) => void): void{
  100. this.ws?.addEventListener('open', (data) => {
  101. callback(data)
  102. })
  103. }
  104. heartBeat(opa = 1) {
  105. // 是否开启心跳
  106. if (opa == 1) {
  107. heartBeat = setInterval(() => {
  108. if (this.ws.readyState === WebSocket.OPEN) {
  109. console.log('心跳');
  110. this.send(beat_data)
  111. serverHeartBeat = setTimeout(() => {
  112. //3秒内没收到消息,断开重连
  113. this.close()
  114. clearInterval(heartBeat)
  115. }, 3000);
  116. }
  117. //
  118. }, beat_time)
  119. } else {
  120. heartBeat && clearInterval(heartBeat)
  121. serverHeartBeat && clearTimeout(serverHeartBeat)
  122. }
  123. }
  124. reconnect() {
  125. heartBeat && clearInterval(heartBeat)
  126. serverHeartBeat && clearTimeout(serverHeartBeat)
  127. if(!reconnectFlag) return
  128. if (reconnectNum > 0 && reconnectFlag) {
  129. reconnectTimer = setTimeout(() => {
  130. console.log('重连',reconnectNum);
  131. this.initWebSocket(this.url)
  132. reconnectNum -= 1
  133. reconnectFlag = true
  134. }, 5000);
  135. reconnectFlag = false
  136. } else {
  137. console.error('websocket error');
  138. }
  139. }
  140. }

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号