基于Vue3+Pinia+ElementPlus仿微信网页聊天模板Vite5-Vue3-Wechat

vite-wechat使用最新前端技术vite5+vue3+vue-router@4+pinia+element-plus搭建网页端仿微信界面聊天系统。包含了聊天、通讯录、朋友圈、短视频、我的等功能模块。支持收缩侧边栏、背景壁纸换肤、锁屏、最大化等功能。

一、技术栈

  • 开发工具:vscode
  • 技术框架:vite5.2+vue3.4+vue-router4.3+pinia2
  • UI组件库:element-plus^2.7.5 (饿了么网页端vue3组件库)
  • 状态管理:pinia^2.1.7
  • 地图插件:@amap/amap-jsapi-loader(高德地图组件)
  • 视频滑动:swiper^11.1.4
  • 富文本编辑器:wangeditor^4.7.15(笔记/朋友圈富文本编辑器)
  • 样式编译:sass^1.77.4
  • 构建工具:vite^5.2.0

二、项目结构

vite-wechat聊天项目使用 vite5.x 构建工具搭建模板,采用 vue3 setup 语法糖编码开发模式。

main.js入口配置

import { createApp } from 'vue'
import './style.scss'
import App from './App.vue' // 引入组件库
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import VEPlus from 've-plus'
import 've-plus/dist/ve-plus.css' // 引入路由/状态管理
import Router from './router'
import Pinia from './pinia' const app = createApp(App) app
.use(ElementPlus)
.use(VEPlus)
.use(Router)
.use(Pinia)
.mount('#app')

目前该项目已经发布到我的原创作品集,感兴趣的话可以去看一看。

https://gf.bilibili.com/item/detail/1106226011

vue3实现上滑数字解锁

vue3-wechat项目没有使用传统的文本框输入验证,改为采用上滑数字密码解锁新模式。

<script setup>
import { ref, computed, inject, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { authState } from '@/pinia/modules/auth'
import { uuid, guid } from '@/utils' const authstate = authState()
const router = useRouter() // 启动页
const splashScreen = ref(true)
const authPassed = ref(false)
// 滑动距离
const touchY = ref(0)
const touchable = ref(false)
// 数字键盘输入值
const pwdValue = ref('')
const keyNumbers = ref([
{letter: 'a'},
{letter: 'b'},
{letter: 'c'},
{letter: 'd'},
{letter: 'e'},
{letter: 'f'},
{letter: 'g'},
{letter: 'h'},
{letter: 'i'},
{letter: 'j'},
{letter: 'k'},
{letter: 'l'},
{letter: 'm'},
{letter: 'n'},
{letter: 'o'},
{letter: 'p'},
{letter: 'q'},
{letter: 'r'},
{letter: 's'},
{letter: 't'},
{letter: 'u'},
{letter: 'v'},
{letter: 'w'},
{letter: 'x'},
{letter: 'y'},
{letter: 'z'},
{letter: '1'},
{letter: '2'},
{letter: '3'},
{letter: '4'},
{letter: '5'},
{letter: '6'},
{letter: '7'},
{letter: '8'},
{letter: '9'},
{letter: '0'},
{letter: '@'},
{letter: '#'},
{letter: '%'},
{letter: '&'},
{letter: '!'},
{letter: '*'},
]) //... // 触摸事件(开始/更新)
const handleTouchStart = (e) => {
touchY.value = e.clientY
touchable.value = true
}
const handleTouchUpdate = (e) => {
let swipeY = touchY.value - e.clientY
if(touchable.value && swipeY > 100) {
splashScreen.value = false
touchable.value = false
}
}
const handleTouchEnd = (e) => {
touchY.value = 0
touchable.value = false
} // 点击数字键盘
const handleClickNum = (num) => {
let pwdLen = passwordArr.value.length
if(pwdValue.value.length >= pwdLen) return
pwdValue.value += num
if(pwdValue.value.length == pwdLen) {
// 验证通过
if(pwdValue.value == password.value) {
// ...
}else {
setTimeout(() => {
pwdValue.value = ''
}, 200)
}
}
}
// 删除
const handleDel = () => {
let num = Array.from(pwdValue.value)
num.splice(-1, 1)
pwdValue.value = num.join('')
} // 清空
const handleClear = () => {
pwdValue.value = ''
} // 返回
const handleBack = () => {
splashScreen.value = true
}
</script> <template>
<div class="uv3__launch">
<div
v-if="splashScreen"
class="uv3__launch-splash"
@mousedown="handleTouchStart"
@mousemove="handleTouchUpdate"
@mouseup="handleTouchEnd"
>
<div class="uv3__launch-splashwrap">
...
</div>
</div>
<div v-else class="uv3__launch-keyboard">
<div class="uv3__launch-pwdwrap">
<div class="text">密码解锁</div>
<div class="circle flexbox">
<div v-for="(num, index) in passwordArr" :key="index" class="dot" :class="{'active': num <= pwdValue.length}"></div>
</div>
</div>
<div class="uv3__launch-numwrap">
<div v-for="(item, index) in keyNumbers" :key="index" class="numbox flex-c" @click="handleClickNum(item.letter)">
<div class="num">{{item.letter}}</div>
</div>
</div>
<div class="foot flexbox">
<Button round icon="ve-icon-clean" @click="handleClear">清空</Button>
<Button type="danger" v-if="pwdValue" round icon="ve-icon-backspace" @click="handleDel">删除</Button>
<Button v-else round icon="ve-icon-rollback" @click="handleBack">返回</Button>
</div>
</div>
</div>
</template>

公共布局模板

整体布局模板分为左侧菜单操作栏+侧边栏+右侧内容主体区域三大模块。

<template>
<div class="vu__container" :style="{'--themeSkin': appstate.config.skin}">
<div class="vu__layout">
<div class="vu__layout-body">
<!-- 菜单栏 -->
<slot v-if="!route?.meta?.hideMenuBar" name="menubar">
<MenuBar />
</slot> <!-- 侧边栏 -->
<div v-if="route?.meta?.showSideBar" class="vu__layout-sidebar" :class="{'hidden': appstate.config.collapsed}">
<aside class="vu__layout-sidebar__body flexbox flex-col">
<slot name="sidebar">
<SideBar />
</slot> <!-- 折叠按钮 -->
<Collapse />
</aside>
</div> <!-- 主内容区 -->
<div class="vu__layout-main flex1 flexbox flex-col">
<Winbtn v-if="!route?.meta?.hideWinBar" />
<router-view v-slot="{ Component, route }">
<keep-alive>
<component :is="Component" :key="route.path" />
</keep-alive>
</router-view>
</div>
</div>
</div>
</div>
</template>

vue3路由配置

左侧菜单栏、侧边栏可以通过配置路由meta参数控制是否显示。

/**
* 路由管理Router
* @author andy
*/ import { createRouter, createWebHashHistory } from 'vue-router'
import { authState } from '@/pinia/modules/auth' import Layout from '@/layouts/index.vue' // 批量导入路由
const modules = import.meta.glob('./modules/*.js', { eager: true })
const patchRouters = Object.keys(modules).map(key => modules[key].default).flat() /**
* meta配置
* @param meta.requireAuth 需登录验证页面
* @param meta.hideWinBar 隐藏右上角按钮组
* @param meta.hideMenuBar 隐藏菜单栏
* @param meta.showSideBar 显示侧边栏
* @param meta.canGoBack 是否可回退上一页
*/
const routes = [
...patchRouters,
// 错误模块
{
path: '/:pathMatch(.*)*',
redirect: '/404',
component: Layout,
meta: {
title: '404error',
hideMenuBar: true,
hideWinBar: true,
},
children: [
{
path: '404',
component: () => import('@/views/error/404.vue'),
}
]
},
] const router = createRouter({
history: createWebHashHistory(),
routes,
}) // 全局路由钩子拦截
router.beforeEach((to, from) => {
const authstate = authState()
// 登录验证
if(to?.meta?.requireAuth && !authstate.authorization) {
console.log('你还未登录!')
return {
path: '/login'
}
}
}) router.afterEach((to, from) => {
// 阻止浏览器回退
if(to?.meta?.canGoBack == false && from.path != null) {
history.pushState(history.state, '', document.URL)
}
})

vue3状态管理

使用新的状态管理Pinia插件。配合 pinia-plugin-persistedstate 插件管理本地持久化存储服务。

/**
* 状态管理Pinia
* @author andy
*/ import { createPinia } from 'pinia'
// 引入pinia持久化存储
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) export default pinia

vue3短视频模块

vue3-wechat项目加入了短视频模块。使用swiper组件实现上下丝滑切换小视频。

底部mini播放进度条,采用Slider组件实现功能。支持实时显示当前播放进度、拖拽到指定时间点

<!-- 短视频模块 -->
<div class="vu__video-container">
<!-- tabs操作栏 -->
<div class="vu__video-tabswrap flexbox">
<el-tabs v-model="activeName" class="vu__video-tabs">
<el-tab-pane label="关注" name="attention" />
<el-tab-pane label="推荐" name="recommend" />
</el-tabs>
</div>
<swiper-container
class="vu__swiper"
direction="vertical"
:speed="150"
:grabCursor="true"
:mousewheel="{invert: true}"
@swiperslidechange="onSlideChange"
>
<swiper-slide v-for="(item, index) in videoList" :key="index">
<!-- 视频层 -->
<video
class="vu__player"
:id="'vuplayer-' + index"
:src="item.src"
:poster="item.poster"
loop
preload="auto"
:autoplay="index == currentVideo"
webkit-playsinline="true"
x5-video-player-type="h5-page"
x5-video-player-fullscreen="true"
playsinline
@click="handleVideoClicked"
>
</video>
<div v-if="!isPlaying" class="vu__player-btn" @click="handleVideoClicked"></div> <!-- 右侧操作栏 -->
<div class="vu__video-toolbar">
...
</div> <!-- 底部信息区域 -->
<div class="vu__video-footinfo flexbox flex-col">
<div class="name">@{{item.author}}</div>
<div class="content">{{item.desc}}</div>
</div>
</swiper-slide>
</swiper-container>
<!-- ///底部进度条 -->
<el-slider class="vu__video-progressbar" v-model="progressBar" @input="handleSlider" @change="handlePlay" />
<div v-if="isDraging" class="vu__video-duration">{{videoTime}} / {{videoDuration}}</div>
</div>

vite-wechat聊天模块

聊天模块编辑器封装为独立组件,支持多行文本输入、光标处插入gif图片、粘贴截图发送图片等功能。

<template>
<!-- 顶部导航 -->
... <!-- 内容区 -->
<div class="vu__layout-main__body">
<Scrollbar ref="scrollRef" autohide gap="2">
<!-- 渲染聊天内容 -->
<div class="vu__chatview" @dragenter="handleDragEnter" @dragover="handleDragOver" @drop="handleDrop">
...
</div>
</Scrollbar>
</div> <!-- 底部操作栏 -->
<div class="vu__footview">
<div class="vu__toolbar flexbox">
...
</div>
<div class="vu__editor">
<Editor ref="editorRef" v-model="editorValue" @paste="handleEditorPaste" />
</div>
<div class="vu__submit">
<button @click="handleSubmit">发送(S)</button>
</div>
</div> ...
</template>

拖拽图片到聊天区域,实现本地上传预览图片。

/**
* ==========拖拽上传模块==========
*/
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) {
Message.danger('仅允许拖拽一张图片')
return false
}
console.log(files)
for(let i = 0; i < files.length; i++) {
if(files[i].type != '') {
handleFileAdd(files[i])
}else {
Message.danger('不支持文件夹拖拽功能')
}
}
}
const handleFileAdd = (file) => {
// 消息队列
let message = {
'id': guid(),
'msgtype': 5,
'isme': true,
'avatar': '/static/avatar/uimg13.jpg',
'author': 'Andy',
'content': '',
'image': '',
'video': '',
} if(file.type.indexOf('image') == -1) {
Message.danger('不支持非图片拖拽功能')
}else {
let reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function() {
let img = this.result message['image'] = img
sendMessage(message)
}
}
}

vue3操作高德地图。定位当前位置和根据经纬度展示地图信息。

// 拾取地图位置
let map = null
const handlePickMapLocation = () => {
popoverChooseRef?.value?.hide()
mapLocationVisible.value = true // 初始化地图
AMapLoader.load({
key: "af10789c28b6ef1929677bc5a2a3d443", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
}).then((AMap) => {
// JS API 加载完成后获取AMap对象
map = new AMap.Map("vu__mapcontainer", {
viewMode: "3D", // 默认使用 2D 模式
zoom: 10, // 初始化地图级别
resizeEnable: true,
}) // 获取当前位置
AMap.plugin('AMap.Geolocation', function() {
var geolocation = new AMap.Geolocation({
// 是否使用高精度定位,默认:true
enableHighAccuracy: true,
// 设置定位超时时间,默认:无穷大
timeout: 10000,
// 定位按钮的停靠位置的偏移量,默认:Pixel(10, 20)
buttonOffset: new AMap.Pixel(10, 20),
// 定位成功后调整地图视野范围使定位位置及精度范围视野内可见,默认:false
zoomToAccuracy: true,
// 定位按钮的排放位置, RB表示右下
buttonPosition: 'RB'
}) map.addControl(geolocation)
geolocation.getCurrentPosition(function(status, result){
if(status == 'complete'){
onComplete(result)
}else{
onError(result)
}
})
}) // 定位成功的回调函数
function onComplete(data) {
var str = ['定位成功']
str.push('经度:' + data.position.getLng())
str.push('纬度:' + data.position.getLat())
if(data.accuracy){
str.push('精度:' + data.accuracy + ' 米')
}
// 可以将获取到的经纬度信息进行使用
console.log(str.join('<br>'))
} // 定位失败的回调函数
function onError(data) {
console.log('定位失败:' + data.message)
}
}).catch((e) => {
// 加载错误提示
console.log('amapinfo', e)
})
} // 打开预览地图位置
const handleOpenMapLocation = (data) => {
mapLocationVisible.value = true // 初始化地图
AMapLoader.load({
key: "af10789c28b6ef1929677bc5a2a3d443", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
}).then((AMap) => {
// JS API 加载完成后获取AMap对象
map = new AMap.Map("vu__mapcontainer", {
viewMode: "3D", // 默认使用 2D 模式
zoom: 13, // 初始化地图级别
center: [data.longitude, data.latitude], // 初始化地图中心点位置
}) // 添加插件
AMap.plugin(["AMap.ToolBar", "AMap.Scale", "AMap.HawkEye"], function () {
//异步同时加载多个插件
map.addControl(new AMap.ToolBar()) // 缩放工具条
map.addControl(new AMap.HawkEye()) // 显示缩略图
map.addControl(new AMap.Scale()) // 显示当前地图中心的比例尺
}) mapPosition.value = [data.longitude, data.latitude]
addMarker() // 实例化点标记
function addMarker() {
const marker = new AMap.Marker({
icon: "//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png",
position: mapPosition.value,
offset: new AMap.Pixel(-26, -54),
})
marker.setMap(map)
/* marker.setLabel({
direction:'top',
offset: new AMap.Pixel(0, -10), //设置文本标注偏移量
content: "<div class='info'>我是 marker 的 label 标签</div>", //设置文本标注内容
}) */ //鼠标点击marker弹出自定义的信息窗体
marker.on('click', function () {
infoWindow.open(map, marker.getPosition())
})
const infoWindow = new AMap.InfoWindow({
offset: new AMap.Pixel(0, -60),
content: `
<div style="padding: 10px;">
<p style="font-size: 14px;">${data.name}</p>
<p style="color: #999; font-size: 12px;">${data.address}</p>
</div>
`
})
}
}).catch((e) => {
// 加载错误提示
console.log('amapinfo', e)
})
}
// 关闭预览地图位置
const handleCloseMapLocation = () => {
map?.destroy()
mapLocationVisible.value = false
}

Okey,综上就是vite5+pinia+element-plus开发网页聊天项目的一些知识分享,希望对大家有所帮助。✍

最后附上两个最新flutter3.x实例项目

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

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

Vite-Wechat网页聊天室|vite5.x+vue3+pinia+element-plus仿微信客户端的更多相关文章

  1. Java和WebSocket开发网页聊天室

    小编心语:咳咳咳,今天又是聊天室,到现在为止小编已经分享了不下两个了,这一次跟之前的又不大相同,这一次是网页聊天室,具体怎么着,还请各位看官往下看~ Java和WebSocket开发网页聊天室 一.项 ...

  2. JavaWeb网页聊天室(WebSocket即时通讯)

    原文:http://baike.xsoftlab.net/view/656.html Git地址 http://git.oschina.net/loopcc/WebSocketChat 概要: Web ...

  3. 基于flask的网页聊天室(四)

    基于flask的网页聊天室(四) 前言 接前天的内容,今天完成了消息的处理 具体内容 上次使用了flask_login做用户登录,但是直接访问login_requare装饰的函数会报401错误,这里可 ...

  4. 基于flask的网页聊天室(三)

    基于flask的网页聊天室(三) 前言 继续上一次的内容,今天完成了csrf防御的添加,用户头像的存储以及用户的登录状态 具体内容 首先是添加csrf的防御,为整个app添加防御: from flas ...

  5. 基于flask的网页聊天室(二)

    基于flask的网页聊天室(二) 前言 接上一次的内容继续完善,今天完成的内容不是很多,只是简单的用户注册登录,内容具体如下 具体内容 这次要加入与数据哭交互的操作,所以首先要建立相关表结构,这里使用 ...

  6. 基于flask的网页聊天室(一)

    基于flask的网页聊天室(一) 基本目标 基于flask实现的web聊天室,具有基本的登录注册,多人发送消息,接受消息 扩展目标 除基本目标外添加当前在线人数,消息回复,markdown支持,历史消 ...

  7. WebSocket 网页聊天室的实现(服务器端:.net + windows服务,前端:Html5)

    websocket是HTML5中的比较有特色一块,它使得以往在客户端软件中常用的socket在web程序中也能轻松的使用,较大的提高了效率.废话不多说,直接进入题. 网页聊天室包括2个部分,后端服务器 ...

  8. WebSocket 网页聊天室

    先给大家开一个原始的websocket的连接使用范例 <?php /* * recv是从套接口接收数据,也就是拿过来,但是不知道是什么 * read是读取拿过来的数据,就是要知道recv过来的是 ...

  9. electron聊天室|vue+electron-vue仿微信客户端|electron桌面聊天

    一.项目概况 基于Electron+vue+electron-vue+vuex+Nodejs+vueVideoPlayer+electron-builder等技术仿制微信电脑端界面聊天室实例,实现消息 ...

  10. vue仿微信网页版|vue+web端聊天室|仿微信客户端vue版

    一.项目介绍 基于Vue2.5.6+Vuex+vue-cli+vue-router+vue-gemini-scrollbar+swiper+elementUI等技术混合架构开发的仿微信web端聊天室— ...

随机推荐

  1. Pod进阶篇:污点-容忍度-亲和性-Affinity-调度(5)

    一.Pod资源清单详细解读 apiVersion: v1 #版本号,例如 v1 kind: Pod #资源类型,如 Pod metadata: #元数据 name: string # Pod 名字 n ...

  2. GeoHash实现附近的人功能(如微信附近的人、共享单车附近的车辆、美团附近的商家)

    如何查找当前点(118.818747°E,32.074497°N)附近500米的人? 这一类功能很常见(如微信附近的人.共享单车附近的车辆.美团附近的商家),那在java中是如何实 现的呢? 1 实现 ...

  3. CF1872G

    题意:一个正整数序列,\(a[i] < 10^9\),求 \(l\),\(r\),最大化 \[\sum_{i = 1}^{l - 1} a[i] + \prod_{i = l}^r a[i] + ...

  4. neovim 使用系统剪贴板

    neovim 使用系统剪贴板 1.vim 与 neovim 使用系统剪切板的不同 Nvim has no direct connection to the system clipboard. Inst ...

  5. Headless BI

    Headless的概念最初的来源与内容管理平台有关,一般是指内容管理平台中的一些应用不提供可视化界面,只是通过API方式把内容以数据的方式给前端.前端根据不同的设备类型,可以再去进行针对性地渲染和展现 ...

  6. Data LakeHouse_理解湖仓一体

    Data Lakehouse(湖仓一体)是数据管理领域中的一种新架构范例,结合了Data Warehouse和Data Lakes的最佳特性.数据分析师和数据科学家可以在同一个数据存储中对数据进行操作 ...

  7. C++ lambda的重载

    先说结论,lambda是不能重载的(至少到c++23依旧如此,以后会怎么样没人知道).而且即使代码完全一样的两个lambda也会有完全不同的类型. 但虽然不能直接实现lambda重载,我们有办法去模拟 ...

  8. sh角本操作数据库

    #!/bin/bash HOST="127.0.0.1" PORT="3306" USERNAME="root" PASSWORD=&quo ...

  9. ETSI GS MEC 012,无线网络信息服务 API

    目录 文章目录 目录 版本 功能理解 版本 ETSI GS MEC 012 V2.1.1 (2019-12) 功能理解 RNIS(Radio Network Information Service,无 ...

  10. 能碳双控| AIRIOT智慧能碳管理解决方案

    在当前全球气候变化和可持续发展的背景下,建设能碳管理平台成为组织迎接挑战.提升可持续性的重要一环,有助于组织实现可持续发展目标,提高社会责任形象,同时适应未来碳排放管理的挑战.能碳管理是一个涉及跟踪. ...