Vue3 + Vite + element ui/vant + ws 写了一个聊天系统

87 阅读3分钟

界面大概效果

image.png

页面样式大概就是仿了微信、lark等。

这期先说前端大概的吧。 后面一期我再说一下后端。

后管端

后管主要用到的是Vue3 + vite + elemnt ui.其他界面没有比较复杂的。就聊天界面,需要保持,左侧的聊天框和中间的对话框以及右边的用户端 进行联动。

Vue3用到的状态管理是 pinia。项目中大概建立了这么些个store用来控制管理。

image.png

图片预览用到的是v-viewer。具体使用这里就不阐述了,网上一大堆。

image.png

然后就是前段这里处理字节,上传文件了。

/**
 * 文件处理成字节
 *
 * @param file
 * @param callback
 */
export function readFileAsByteArray(file: any, callback: Function) {
  var reader: any = new FileReader()
  reader.onload = function (event: any) {
    var byteArray = new Uint8Array(reader.result)
    callback(byteArray)
  }
  reader.readAsArrayBuffer(file)
}

至于websocket地方基本就是 依据后端定义好的类型。 进行处理会话,由于不可能每次来消息都要请求后端,类似未读消息,前端这里会存入store里。 还有一些置顶啊,标记未读啊等方法。

import { defineStore } from 'pinia'
import { useUserStore } from './userStore'
import { useMessageStore } from './messageStore'
import { useChatListStore } from './chatListStore'
//@ts-ignore
import { v4 as uuidv4 } from 'uuid'
import { SessionChatRecordResponse } from '@/server/session_request'
import moment from 'moment'

// import { Base64 } from 'js-base64'

interface IWebSocket {
  ws: any
  isConnected: boolean
  //是否主动关闭socket
  isTakeClose: boolean
  interval: any
  sessionId: string
  userId: string
  merchantInfo: IMessageMerchantParam
  userInfo: IMessageUserParam
}

interface IMessageMerchantParam {
  merchantId?: string
  merchantName?: string
  merchantImgUrl?: string
}

interface IMessageUserParam {
  userId?: string
  userImgUrl?: string
}

interface IMessageChatParam {
  userType?: UserType
  MessageType?: MessageType
  content?: string
}

interface IMessageParam {
  message: IMessageChatParam[]
}

/**
 * 消息类型
 */
export enum MessageType {
  Audio = 'AUDIO',
  Emoji = 'EMOJI',
  File = 'FILE',
  Image = 'IMAGE',
  Text = 'TEXT',
  Tip = 'TIP',
  Video = 'VIDEO'
}

export enum UserType {
  CustomerService = 'CUSTOMER_SERVICE',
  System = 'SYSTEM',
  User = 'USER'
}

export const useWebSocketStore = defineStore('websocket', {
  state: (): IWebSocket => {
    return {
      ws: null,
      isConnected: true,
      isTakeClose: false,
      interval: null,
      sessionId: '',
      userId: '',
      merchantInfo: {},
      userInfo: {}
    }
  },

  actions: {
    //---------------------------功能相关------------------------------------
    /**
     * 塞入sessionId
     * @param value
     */
    setSessionId(value: string) {
      if (!value) return
      this.sessionId = value
    },
    /**
     * 塞入UserId
     */
    setUserId(value: string) {
      if (!value) return
      this.userId = value
    },

    //--------------------------websocket相关--------------------------------------
    //连接
    connect() {
      if (this.ws && this.ws.readyState == 1) {
        console.log(`连接已建立,无需再次连接`)
        return
      }
      const wsUrl = import.meta.env.VITE_WS_URL
      this.ws = new WebSocket(`${wsUrl}`)

      this.ws.addEventListener('open', () => {
        console.log('WebSocket连接已建立')
        this.isConnected = true
        this.isTakeClose = false
        // this.cancelHeart()
      })

      this.ws.addEventListener('close', (e: any) => {
        this.isConnected = false
        console.error('WebSocket连接断开', JSON.stringify(e))
        if (!this.isTakeClose) {
          console.error('WebSocket意外断开,正在尝试重新连接...')
          setTimeout(() => {
            this.connect()
          }, 2000)
        }
      })

      this.ws.addEventListener('message', (event: any) => {
        //一大坨逻辑。这里就不放出来了。
             
      })
    },
    /**
     * 发送websocket消息
     *
     * @param data
     */
    send(data: any) {
      try {
        if (this.isConnected) {
          this.ws.send(data)
        } else {
          console.warn('webSocket is closed')
        }
      } catch (error) {
        console.log(error)
      }
    },
    /**
     * 关闭websocket连接
     */
    close() {
      this.isConnected = false
      this.isTakeClose = true
      //清空定时器

      this.ws && this.ws.close()
      this.ws &&
        this.ws.removeEventListener('message', () => {
          console.log('断开消息监听')
        })
      this.ws &&
        this.ws.removeEventListener('open', () => {
          console.log('断开连接监听')
        })
      this.ws &&
        this.ws.removeEventListener('close', () => {
          console.log('断开关闭监听')
        })

      this.cancelHeart()
    },
    /**
     * 维持socket心跳
     *
     */
    keepHeart() {
      if (this.interval) {
        console.log('定时器已存在,无需ping')
        return
      }

      this.interval = setInterval(() => {
        this.send('ping')
      }, 6000)
    },

    /**
     * 取消socket心跳
     */
    cancelHeart() {
      this.interval && clearInterval(this.interval)
    },

    updateChatListStore(messageInfo: any, isNeedUpdate: boolean) {
    },

    presist: {
      path: ['interval'],
      storage: sessionStorage
    }
  }
})

使用emoji.

emoji H5这里,我使用的是。vue3-emoji-picker

image.png

后管端,是fork了一份开源的项目,改了一下。 QQ20230508-160944-HD.gif

icon

icon方案一开始用的是iconfont . 后面使用了 iconpark. 然后使用的时候注意一下,在main.ts里设置一下。标记iconpark为自定义组件。 不然会一直报错。

   //解决iconpark-icon报错问题 (Failed to resolve component: iconpark-icon)
app.config.compilerOptions.isCustomElement = (tag) => tag === 'iconpark-icon'

用户端

用户端更没啥说的。一个很简单的聊天界面。 适配做好就行, 我这里的适配方案基本用的是 vw + vh+ rem

以上

个人比较懒 想到什么就写什么。源码暂时先不分享了。因为前端权限管理那里,我本来图省事,组件、路由啥,基本都是v-if来控制,不够优雅。等我改一下,改成RBAC模式。后端用的是Sa-token