2026年基于最新前端技术栈Vue3+Vite7+Pinia3+Vant4纯手搓移动端聊天室。包含聊天+通讯录+我的模块,支持图文消息/gif动图、图片/视频预览、红包/朋友圈等功能。
技术实现
- 编辑器:vscode
- 技术框架:vite7+vue3.5+pinia3+vue-router4
- UI组件库:Vant-UI4.x (有赞移动端Vue3组件库)
- 弹层组件:V3Popup(基于vue3.0自定义弹窗组件)
- iconfont图标:阿里字体图标库
- 自定义顶部导航条+底部tabBar
项目框架目录
使用最新vite7+vue3搭建项目模板。
项目入口配置
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// 引入路由/状态管理
import Router from './router'
import Pinia from './pinia'
import Plugins from './plugins'
const app = createApp(App)
app
.use(Router)
.use(Pinia)
.use(Plugins)
.mount('#app')
vue3自定义导航栏+底部菜单栏
vue3聊天功能模块
/**
* vue3聊天模块 wx:xy190310
*/
<script setup>
import { onMounted, onUnmounted, ref, nextTick, inject } from 'vue'
import { useRouter } from 'vue-router'
import Editor from './editor.vue'
import { showImagePreview } from 'vant'
import msgJSON from './mock/mock-chat.js'
import emoJSON from './mock/mock-emoj.js'
...
onMounted(() => {
nextTick(() => {
imgLoaded(scrollview)
})
})
onUnmounted(() => {
window.removeEventListener('popstate', handlePopStateClosed, false)
})
// 监听地址打开
const handlePopStateOpen = () => {
if(window.history && window.history.pushState) {
history.pushState(null, null, document.URL)
window.addEventListener('popstate', handlePopStateClosed, false)
}
}
// 监听地址关闭(手机回退按钮事件)
const handlePopStateClosed = () => {
// console.log('监听关闭视频事件!')
isShowLinkView.value = false
isShowVideoPlayer.value = false
}
/**
* 图片加载完成处理函数
* @param arr 图片的src集合
* @returns {Promise}
*/
const preloadImages = (arr) => {
let loadedCount = 0
let imgs = []
return new Promise(function(resolve, reject) {
for(let i = 0; i < arr.length; i++) {
imgs[i] = new Image()
imgs[i].src = arr[i]
imgs[i].onload = function() {
loadedCount++
if(loadedCount == arr.length) {
resolve()
}
}
imgs[i].onerror = function() {
reject()
}
}
})
}
/**
* 图片加载完成,聊天信息滚到最底部
* @param ref 容器ref
*/
const imgLoaded = (ref) => {
scrollBottom(ref)
let msgBox = ref.value
if(msgBox) {
let imgs = msgBox.querySelectorAll('img')
if(imgs) {
let arr = []
for(let i = 0; i < imgs.length; i++) {
arr[i] = imgs[i].src
}
preloadImages(arr).then(() => {
scrollBottom(ref)
}).catch(function() {
scrollBottom(ref)
})
}
}
}
/**
* 滚动条到底部
* @param ref 容器ref
*/
const scrollBottom = (ref) => {
let viewport = ref.value
if(viewport) {
viewport.scrollTop = viewport.scrollHeight
}
}
// 点击聊天消息区域
const handleMsgPanelClicked = () => {
if(!isShowFootBar.value) return
isShowFootBar.value = false
}
// 点击消息过滤
const handleMsgClicked = (e) => {
let target = e.target
// 链接
if(target.tagName === 'A') {
e.preventDefault()
// console.log('触发点击链接事件!')
isShowLinkView.value = true
linkView.value = target.href
// 监听手机回退按钮事件
handlePopStateOpen()
}
// 图片
if (target.tagName === 'IMG' && target.classList.contains('img-view')) {
// ...
}
}
/**
* 表情|选择区切换
* @param index 切换索引
*/
const handleEmojChooseView = (index) => {
isShowFootBar.value = true
showFootBarIndex.value = index
nextTick(() => { imgLoaded(scrollview) })
}
/**
* 表情Tab切换
* @param index 索引index
*/
const handleEmojTab = (index) => {
let emojLs = emojList.value
for(var i = 0, len = emojLs.length; i < len; i++) {
emojLs[i].selected = false
}
emojLs[index].selected = true
emojList.value = emojLs
}
// 消息处理
const sendMessage = (message) => {
if(typeof message != 'object') return
msgList.value.push(message)
// 滚动底部
nextTick(() => { imgLoaded(scrollview) })
}
/* ---------- { 编辑器|表情模块 } ---------- */
// 点击编辑器
const handleEditorClick = () => {
// console.log('点击编辑器')
isShowFootBar.value = false
}
// 编辑器获取焦点
const handleEditorFocus = () => {
// console.log('编辑器获取焦点')
}
// 编辑器失去焦点
const handleEditorBlur = () => {
// console.log('编辑器失去焦点')
}
// 点击表情
const handleEmojClicked = (e) => {
let faceimg = e.target.cloneNode(true)
editorRef.value.insertHtmlAtCursor(faceimg)
}
// 点击表情gif
const handleGifClicked = (path) => {
// 消息队列
let message = {
id: utils.guid(),
msgtype: 4,
isme: true,
avatar: '/static/uimg/img-avatar08.jpg',
author: 'AKA',
msg: '',
imgsrc: path,
videosrc: ''
}
sendMessage(message)
}
// 点击删除
const handleDelClicked = () => {
editorRef.value.handleDel()
}
// 发送消息
const isEmpty = (html) => {
html = html.replace(/<br[\s/]{0,2}>/ig, "\r\n")
html = html.replace(/<[^img].*?>/ig, "")
html = html.replace(/ /ig, "")
return html.replace(/\r\n|\n|\r/, "").replace(/(?:^[ \t\n\r]+)|(?:[ \t\n\r]+$)/g, "") == ""
}
const transferHTML = (html) => {
let reg = /(http:\/\/|https:\/\/)((\w|=|\?|\.|\/|&|-)+)/g
return html.replace(reg, "<a href='$1$2'>$1$2</a>")
}
const handleSubmit = () => {
// console.log(editorText.value)
// 判断编辑器是否为空
if(isEmpty(editorText.value)) return
// 消息队列
let message = {
id: utils.guid(),
msgtype: 3,
isme: true,
avatar: '/static/uimg/img-avatar08.jpg',
author: 'AKA',
msg: transferHTML(editorText.value),
imgsrc: '',
videosrc: ''
}
sendMessage(message)
// 清空文本框内容
nextTick(() => {
editorText.value = ''
editorRef.value.handleClear()
})
}
/* ---------- { 选择功能模块 } ---------- */
// 选择图片
const handleChooseImage = () => {
// 消息队列
let message = {
id: utils.guid(),
msgtype: 5,
isme: true,
avatar: '/static/uimg/img-avatar08.jpg',
author: 'AKA',
msg: '',
imgsrc: '',
videosrc: ''
}
let file = pickImageRef.value.files[0]
if(!file) return
let size = Math.floor(file.size / 1024)
if(size > 2*1024) {
v3popup({content: '请选择2MB以内的图片!'})
return false
}
var reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function() {
let img = this.result
message['imgsrc'] = img
sendMessage(message)
}
}
// 预览图片
const handleImgPreview = (src) => {
showImagePreview({
images: [
src
],
showIndex: false,
showIndicators: true,
closeable: true,
});
}
// 选择视频
const handleChooseVideo = () => {
// 消息队列
let message = {
id: utils.guid(),
msgtype: 6,
isme: true,
avatar: '/static/uimg/img-avatar08.jpg',
author: 'AKA',
msg: '',
imgsrc: '',
videosrc: ''
}
let file = pickVideoRef.value.files[0]
if(!file) return
let size = Math.floor(file.size / 1024)
if(size > 5*1024) {
v3popup({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.autoplay = true
$video.play()
$video.muted = true
$video.addEventListener('timeupdate', () => {
if($video.currentTime > .1) {
$video.pause()
}
})
// 截取视频第一帧为封面
$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)
message['imgsrc'] = canvas.toDataURL('image/png')
message['videosrc'] = videoUrl
sendMessage(message)
}, 16);
})
}
/* ---------- { 音视频功能模块 } ---------- */
// 预览视频
const handleVideoPlayer = (item) => {
isShowVideoPlayer.value = true
videoList.value = item
nextTick(() => {
playerRef.value.play()
})
// 监听手机回退按钮事件
handlePopStateOpen()
}
// 预览音频
const handleAudioPlayer = (item) => {
audioPlayerVisible.value = false
audioPlayerData.value = null
nextTick(() => {
audioPlayerVisible.value = true
audioPlayerData.value = item
// 设为已读
msgList.value.map(it => {
if(it.id == item.id) {
it.msg.unread = false
}
})
})
}
...
</script>
2026版uniapp+mphtml调用deepseek [小程序.安卓.H5] 流式输出ai
2026最新款Vite7+Vue3+DeepSeek-V3.2+Markdown移动端流式输出AI会话
electron38.2-vue3os系统|Vite7+Electron38+Pinia3+ArcoDesign桌面版OS后台管理
基于electron38+vite7+vue3 setup+elementPlus电脑端仿微信/QQ聊天软件
2025最新款Electron38+Vite7+Vue3+ElementPlus电脑端后台系统Exe
基于uni-app+vue3+uvui跨三端仿微信app聊天模板【h5+小程序+app】
基于uniapp+vue3+uvue短视频+聊天+直播app系统
自研2025版flutter3.38实战抖音app短视频+聊天+直播商城系统
基于flutter3.32+window_manager仿macOS/Wins风格桌面os系统
flutter3.27+bitsdojo_window电脑端仿微信Exe应用
自研tauri2.0+vite6.x+vue3+rust+arco-design桌面版os管理系统Tauri2-ViteOS