vite7+vue3.5仿微信聊天|vue3.5+vant4移动端聊天模板

0 阅读3分钟

2026年基于最新前端技术栈Vue3+Vite7+Pinia3+Vant4纯手搓移动端聊天室。包含聊天+通讯录+我的模块,支持图文消息/gif动图、图片/视频预览、红包/朋友圈等功能。

未标题-2.png

p4.gif

技术实现

  • 编辑器:vscode
  • 技术框架:vite7+vue3.5+pinia3+vue-router4
  • UI组件库:Vant-UI4.x (有赞移动端Vue3组件库)
  • 弹层组件:V3Popup(基于vue3.0自定义弹窗组件)
  • iconfont图标:阿里字体图标库
  • 自定义顶部导航条+底部tabBar

未标题-7.png

p2.gif

项目框架目录

使用最新vite7+vue3搭建项目模板。

360截图20260227110408025.png

p1.gif

项目入口配置

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')

a28fed67839e6f6136320a1ac390d614_1289798-20260228091047568-1118747482.png

vue3自定义导航栏+底部菜单栏

image.png

001360截图20260227101351711.png

002360截图20260227101808533.png

002360截图20260227101842375.png

002360截图20260227101858709.png

003360截图20260227102022896.png

003360截图20260227102151545.png

003360截图20260227102221542.png

003360截图20260227102221543.png

004360截图20260227102425427.png

004360截图20260227102459869.png

004360截图20260227102514591.png

005360截图20260227102659175.png

006360截图20260227102800624.png

006360截图20260227102938754.png

006360截图20260227103047436.png

007360截图20260227103433292.png

007360截图20260227103452120.png

007360截图20260227103544160.png

007360截图20260227103620130.png

007360截图20260227104050847.png

007360截图20260227105132120.png

007360截图20260227105347994.png

007360截图20260227105500683.png

vue3聊天功能模块

007360截图20260227104050847.png

/**
 * 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(/&nbsp;/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