一、项目简介

基于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界面聊天的分享。

最新文章

  1. 数塔取数 基础dp
  2. gradle各版本下载地址
  3. C#-WinForm-Treeview-树状模型
  4. 水果姐逛水果街Ⅱ codevs 3305
  5. 说一下linux中shell的后台进程与前台进程
  6. 解决cxf+spring发布的webservice,types,portType和message以import方式导入
  7. plsql查询数据显示为乱码解决方法
  8. 【转】linux之e2label命令
  9. vim file save as
  10. openerp 产品图片的批量写入
  11. [转] GCC __builtin_expect的作用
  12. 【转】各个层次的gcc警告 #pragma GCC diagnostic ignored &quot;-Wunused-parameter&quot; --不错
  13. CSS中padding和margin以及用法
  14. Dodobox一个基于所有平台的嵌入式操作系统(OS)
  15. np.mgrid的用法
  16. dotnetcore ef 调用多个数据库时用户命令执行操作报错
  17. eclipse编辑环境下导入springmvc的源码
  18. 第五天 py if使用
  19. Linux磁盘和文件系统简介
  20. PAT 甲级 1068 Find More Coins

热门文章

  1. 【kinetic】操作系统探索总结(八)键盘控制
  2. 【kinetic】操作系统探索总结(六)使用smartcar进行仿真
  3. Java IO流 FileOutputStream、FileInputStream的用法   
  4. java连接mongodb数据库
  5. ThreadLocal源码深度剖析
  6. C/C++ 弱符号
  7. Linux 设置静态IP
  8. try catch finally语句块中存在return语句时的执行情况剖析
  9. tp where使用数组条件,如何设置or,and
  10. docker基础总结