一、項目簡介
基於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