Vue3.0網頁版聊天|Vue3.x+ElementPlus仿微信/QQ界面|vue3聊天實例


一、項目簡介

基於vue3.x+vuex+vue-router+element-plus+v3layer+v3scroll等技術構建的仿微信web桌面端聊天實戰項目Vue3-Webchat。基本上實現發送消息+emoj表情、圖片/視頻查看、鏈接預覽、粘貼截圖/拖拽發送圖片、紅包/朋友圈等功能。

二、使用技術

  • 編碼器:Vscode
  • 技術框架:Vue3.0.5+Vuex4+VueRouter@4
  • UI組件庫:Element-Plus (餓了么桌面端vue3組件庫)
  • 彈窗組件:V3Layer(基於vue3.x自定義對話框組件)
  • 滾動條組件:V3Scroll(基於vue3.x自定義虛擬美化滾動條組件)
  • 字體圖標:阿里iconfont圖標庫

三、項目結構目錄

◆ 一覽效果

◆ vue3.x封裝自定義彈窗組件

為了整體效果一致性,項目中用到的所有彈窗功能均是自定義組件v3layer來實現。

V3Layer 基於vue3.0開發的pc端自定義彈窗組件,支持拖拽(自定義拖拽區)、縮放、最大化、全屏、置頂彈框等功能。

由於之前有過一篇詳細的介紹分享,感興趣的話可以去看下哈。

https://www.cnblogs.com/xiaoyan2017/p/14221729.html

其實v3layer彈窗是在原先的vue2版本中演變而來,專門為vue3項目而開發的,並且在功能及效果上和v2版的保持一致。

vue2.x pc端自定義全局彈窗組件|vue2桌面端對話框組件

◆ vue3.x自定義美化模擬滾動條

為了使得項目中頁面滾動條更加精致,這里采用了自定義模擬滾動條vscroll組件來替代原生滾動條。

V3Scroll 基於vue3.0開發的小巧模擬滾動條組件。支持自定義滾動條大小、顏色、層級及自動隱藏等功能。

並且支持實時監測DOM尺寸改變來動態更新滾動條。

https://www.cnblogs.com/xiaoyan2017/p/14242983.html

◆ vue3.x聊天主面板

項目整體分為右上按鈕、側邊欄、中間區、主體內容區三個模塊。

<div :class="['vui__wrapper', store.state.isWinMaximize&&'maximize']">
  <div class="vui__board flexbox">
    <div class="flex1 flexbox">
      <!-- 頂部按鈕(最大、最小、關閉) -->
      <WinBar v-if="!route.meta.hideWinBar" />

      <!-- 側邊欄 -->
      <SideBar v-if="!route.meta.hideSideBar" class="nt__sidebar flexbox flex-col" />

      <!-- 中間欄 -->
      <Middle v-show="!route.meta.hideMiddle" />

      <!-- 主內容區 -->
      <router-view class="nt__mainbox flex1 flexbox flex-col"></router-view>
    </div>
  </div>
</div>

◆ 引入|注冊公共組件

// 引入餓了么vue3組件庫
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css'

// 引入vue3.x彈窗組件
import V3Layer from '../components/v3layer'

// 引入vue3.x滾動條組件
import V3Scroll from '@components/v3scroll'

// 引入公共組件
import WinBar from '../layouts/winbar.vue'
import SideBar from '../layouts/sidebar'
import Middle from '../layouts/middle'

import Utils from './utils'

const Plugins = app => {
    app.use(ElementPlus)
    app.use(V3Layer)
    app.use(V3Scroll)

    // 注冊公共組件
    app.component('WinBar', WinBar)
    app.component('SideBar', SideBar)
    app.component('Middle', Middle)

    app.provide('utils', Utils)
}

項目中背景整體采用虛化毛玻璃效果。通過 svg filter 來實現。

<!-- //虛化背景(毛玻璃) -->
<div class="vui__bgblur">
  <svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100%" height="100%" class="blur-svg" viewBox="0 0 1920 875" preserveAspectRatio="none">
    <filter id="blur_mkvvpnf"><feGaussianBlur in="SourceGraphic" stdDeviation="50"></feGaussianBlur></filter>
    <image :xlink:href="store.state.skin" x="0" y="0" width="100%" height="100%" externalResourcesRequired="true" xmlns:xlink="http://www.w3.org/1999/xlink" style="filter:url(#blur_mkvvpnf)" preserveAspectRatio="none"></image>
  </svg>
  <div class="blur-cover"></div>
</div>

◆ vue3.x表單驗證/登錄狀態攔截

vue3中實現表單驗證+60s倒計時操作。

<script>
import { reactive, toRefs, inject, getCurrentInstance } from 'vue'
export default {
    components: {},
    setup() {
        const { ctx } = getCurrentInstance()
        const v3layer = inject('v3layer')
        const utils = inject('utils')

        const formObj = reactive({})
        const data = reactive({
            vcodeText: '獲取驗證碼',
            disabled: false,
            time: 0,
        })

        const VTips = (content) => {
            v3layer({
                content: content, layerStyle: 'background:#ff5151;color:#fff;', time: 2
            })
        }

        const handleSubmit = () => {
            if(!formObj.tel){
                VTips('手機號不能為空!')
            }else if(!utils.checkTel(formObj.tel)){
                VTips('手機號格式不正確!')
            }else if(!formObj.pwd){
                VTips('密碼不能為空!')
            }else if(!formObj.vcode){
                VTips('驗證碼不能為空!')
            }else{
                ctx.$store.commit('SET_TOKEN', utils.setToken());
                ctx.$store.commit('SET_USER', formObj.tel);

                // ...
            }
        }

        // 60s倒計時
        const handleVcode = () => {
            if(!formObj.tel) {
                VTips('手機號不能為空!')
            }else if(!utils.checkTel(formObj.tel)) {
                VTips('手機號格式不正確!')
            }else {
                data.time = 60
                data.disabled = true
                countDown()
            }
        }
        const countDown = () => {
            if(data.time > 0) {
                data.vcodeText = '獲取驗證碼('+ data.time +')'
                data.time--
                setTimeout(countDown, 1000)
            }else{
                data.vcodeText = '獲取驗證碼'
                data.time = 0
                data.disabled = false
            }
        }

        return {
            formObj,
            ...toRefs(data),
            handleSubmit,
            handleVcode
        }
    }
}
</script>

vue3路由鈎子實現全局登錄狀態攔截判斷。

import { createRouter, createWebHistory } from 'vue-router'

import store from '../store'

import V3Layer from '@components/v3layer'

const routesLS = [
    // 登錄|注冊
    {
        name: 'login', path: '/login',
        component: () => import('../views/auth/login.vue'),
        meta: { hideWinBar: true, hideSideBar: true, hideMiddle: true }
    },

    // ...
]

const router = createRouter({
    history: createWebHistory(),
    routes: routesLS,
})

// 全局鈎子攔截登錄狀態
router.beforeEach((to, from, next) => {
    const token = store.state.token

    // 判斷當前路由地址是否需要登錄權限
    if(to.meta.requireAuth) {
        if(token) {
            next()
        }else {
            // 未登錄授權
            V3Layer({
                content: '還未登錄授權!', position: 'top', time: 2,
                onEnd: () => {
                    next({ path: '/login' })
                }
            })
        }
    }else {
        next()
    }
})

◆ vue3.x聊天模塊

聊天編輯器模塊繼續采用分離公共調用方式。支持多行文本、文字+emoj表情混排、光標處插入表情、粘貼截圖發送等功能。

/**
 * @Desc     vue3.x仿微信桌面端聊天
 * @Time     andy by 2021-01
 * @About    Q:282310962  wx:xy190310
 */
<script>
import { onMounted, ref, reactive, toRefs, watch, nextTick, inject } from 'vue'
import { useRoute } from 'vue-router'

import Editor from './editor.vue'
import SendRedPacket from './redPacket.vue'
import GroupSet from './groupInfo.vue'

// ...

export default {
    components: {
        Editor,
        SendRedPacket,
        GroupSet
    },
    setup() {
        const scrollRef = ref(null)
        const editorRef = ref(null)

        const route = useRoute()

        const v3layer = inject('v3layer')

        const data = reactive({
            editorText: '',

            showEmojView: false,

            isSubmitDisabled: true,

            // ...
        })

        // ...

        // 獲取群組信息
        const getGroupJSON = () => {
            msgJSON.map((item) => {
                if(item.cid == route.query.id) {
                    data.groupLs = item
                }
            })
            // 定位消息到底部
            nextTick(() => {
                imgLoaded(scrollRef)
            })
        }

        // ...

        /**
         * 編輯器粘貼事件
         * @param img 返回粘貼圖片地址
        */
        const handleEditorPaste = (img) => {            
            let msgLs = data.groupLs.msglist
            let len = msgLs.length
            // 消息隊列
            let arrLS = {
                // ...
            }
            msgLs = msgLs.concat(arrLS)
            data.groupLs.msglist = msgLs

            nextTick(() => { imgLoaded(scrollRef) })
        }

        // 點擊表情
        const handleEmojClicked = (e) => {
            let faceimg = e.target.cloneNode(true)
            editorRef.value.insertHtmlAtCursor(faceimg)
            data.showEmojView = false
        }

        // 點擊表情gif
        const handleEmojGifClicked = (path) => {
            let msgLs = data.groupLs.msglist
            let len = msgLs.length
            // 消息隊列
            let arrLS = {
                // ...
            }
            msgLs = msgLs.concat(arrLS)
            data.groupLs.msglist = msgLs
            data.showEmojView = false

            nextTick(() => { imgLoaded(scrollRef) })
        }

        /* ---------- { 選擇功能模塊 } ---------- */
        // 選擇視頻
        const handleChooseVideo = () => {
            let msgLs = data.groupLs.msglist
            let len = msgLs.length
            // 消息隊列
            let arrLS = {
                // ...
            }

            let file = pickVideoRef.value.files[0]
            if(!file) return
            let size = Math.floor(file.size / 1024)
            if(size > 5*1024) {
                v3layer({content: '請選擇5MB以內的視頻!'})
                return false
            }
            // 獲取視頻地址
            let videoUrl
            if(window.createObjectURL != undefined) {
                videoUrl = window.createObjectURL(file)
            } else if (window.URL != undefined) {
                videoUrl = window.URL.createObjectURL(file)
            } else if (window.webkitURL != undefined) {
                videoUrl = window.webkitURL.createObjectURL(file)
            }

            let $video = document.createElement('video')
            $video.src = videoUrl
            // 截取視頻第一幀為封面
            $video.addEventListener('loadeddata', function() {
                setTimeout(() => {
                    var canvas = document.createElement('canvas')
                    canvas.width = $video.videoWidth * .8
                    canvas.height = $video.videoHeight * .8
                    canvas.getContext('2d').drawImage($video, 0, 0, canvas.width, canvas.height)
                    arrLS.imgsrc = canvas.toDataURL('image/png')

                    arrLS.videosrc = videoUrl
                    msgLs = msgLs.concat(arrLS)
                    data.groupLs.msglist = msgLs

                    nextTick(() => { imgLoaded(scrollRef) })
                }, 16);
            })
        }

        /* ---------- { 拖拽功能模塊 } ---------- */
        const handleDragEnter = (e) => {
            e.stopPropagation()
            e.preventDefault()
        }
        const handleDragOver = (e) => {
            e.stopPropagation()
            e.preventDefault()
        }
        const handleDrop = (e) => {
            e.stopPropagation()
            e.preventDefault()
            // console.log(e.dataTransfer)

            handleFileList(e.dataTransfer)
        }
        // 獲取拖拽文件列表
        const handleFileList = (filelist) => {
            let files = filelist.files
            if(files.length >= 2) {
                v3layer.message({icon: 'error', content: '暫時支持拖拽一張圖片', shade: true, layerStyle: {background:'#ffefe6',color:'#ff3838'}})
                return false
            }
            for(let i = 0; i < files.length; i++) {
                if(files[i].type != '') {
                    handleFileAdd(files[i])
                }else {
                    v3layer.message({icon: 'error', content: '目前不支持文件夾拖拽功能', shade: true, layerStyle: {background:'#ffefe6',color:'#ff3838'}})
                }
            }
        }
        const handleFileAdd = (file) => {
            let msgLs = data.groupLs.msglist
            let len = msgLs.length
            // 消息隊列
            let arrLS = {
                // ...
            }

            if(file.type.indexOf('image') == -1) {
                v3layer.message({icon: 'error', content: '目前不支持非圖片拖拽功能', shade: true, layerStyle: {background:'#ffefe6',color:'#ff3838'}})
            }else {
                let reader = new FileReader()
                reader.readAsDataURL(file)
                reader.onload = function() {
                    let img = this.result

                    // ...
                }
            }
        }

        /* ---------- { 其他功能模塊 } ---------- */
        // 提示信息
        const handleTipsLayer = (e) => {
            let pos = [e.clientX+25, e.clientY-110]
            v3layer.popover({
                icon: 'info',
                title: 'Tips',
                content: '<div class="pb-10">編輯框支持<b class="bg-00e077 c-fff">拖拽</b>或<b class="bg-00e077 c-fff">截屏粘貼</b>發送圖片!<br />支持自動<b class="bg-00e077 c-fff">鏈接</b>識別!</div>',
                follow: pos,
                shade: true,
                opacity: .2,
            })
        }

        // 紅包彈窗
        const handleRedpacketLayer = (item) => {
            data.isShowRedPacket = true
            data.redPacketList = item
        }

        // ...

        return {
            ...toRefs(data),

            scrollRef,
            editorRef,

            handleMsgClicked,

            handleEmojView,
            handleEmojTab,

            handleEditorClick,
            handleEditorFocus,
            handleEditorBlur,
            handleEditorPaste,
            handleEmojClicked,
            
            // ...
        }
    } 
}
</script>

Ok,以上就是使用Vue3+ElementPlus開發網頁端仿微信/QQ界面聊天的分享。💪🏻✍🏻

最后放上一個Nuxt.js+Vant聊天實例項目

https://www.cnblogs.com/xiaoyan2017/p/13823195.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM