• Vite-Wechat网页聊天室|vite5.x+vue3+pinia+element-plus仿微信客户端


    基于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">
        
        <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('
    ')) } // 定位失败的回调函数 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: "
    我是 marker 的 label 标签
    ", //设置文本标注内容 })
    */ //鼠标点击marker弹出自定义的信息窗体 marker.on('click', function () { infoWindow.open(map, marker.getPosition()) }) const infoWindow = new AMap.InfoWindow({ offset: new AMap.Pixel(0, -60), content: `

    ${data.name}

    ${data.address}

    ` }) } }).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

     

  • 相关阅读:
    GBase 8c产品简介
    【pytest官方文档】解读- 如何自定义mark标记,并将测试用例的数据传递给fixture函数
    Java开发学习(二十六)----SpringMVC返回响应结果
    【python海洋专题三十八】海洋指数画法--折线图样式二
    web系统字典统一中文翻译问题
    Vue3 - Tree Shaking 摇树优化(它是什么?跟 Vue3 有什么关系?)
    人机环境系统中的“人”、“机”、“环境”
    Splunk 之 filed 恢复
    C++(17):lambda捕获this的副本
    基于白鲸优化的BP神经网络(分类应用) - 附代码
  • 原文地址:https://www.cnblogs.com/xiaoyan2017/p/18262622