vue3-vitechat原创vue3+vite5+element-plus网页聊天模板

2,957 阅读5分钟

经过两周爆肝式潜心开发,实战原创新作vite-vue3-chat网页聊天室出项目成果了。

image.png

full-360截图20240621235058728.png

vite-webchat使用vite5.x构建项目模板,采用vue3 setup语法糖编码开发页面。

p2.gif

技术栈

  • 开发工具:Vscode
  • 技术框架:vite5+vue3+vue-router4+pinia2
  • 组件库: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

p8.gif

ViteChat项目包含了聊天、通讯录、朋友圈、短视频、我的等模块。支持展开/收缩侧边栏、桌面壁纸背景、锁屏、最大化全屏等功能。

004360截图20240621220710261.png

013360截图20240621222703654.png

015360截图20240621223832187.png

搭建项目结构

使用最新版vite5.x搭建项目模板。安装依赖,通过yarn serve即可运行项目。

360截图20240622172259595.png

入口配置

引入element-plus组件库,配置路由/状态管理。

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

vue3数字解锁验证

采用全新的数字密码解锁验证模式。上滑动画切换数字输入表盘,可自定义密码长度。

001360截图20240621220442752.png

002360截图20240621220540327.png

<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'},
    ...
  ])
  
  //...

  // 触摸事件(开始/更新)
  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>

p1.gif

vite-chat布局面板

项目整体分为左侧菜单区域+侧边栏+右侧聊天主面板三大部分。

image.png

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

005360截图20240621220905605.png

006360截图20240621221151381.png

007360截图20240621221234148.png

008360截图20240621221405918.png

010360截图20240621221603373.png

011360截图20240621221756237.png

vue3朋友圈模块

p5.gif

朋友圈发布编辑器使用了wangEditor富文本编辑器组件。

014360截图20240621223131623.png

image.png

vue3短视频功能

如下图:vite-chat项目加入了短视频模块。

p6.gif

使用swiper组件实现丝滑上下滑动切换小视频。使用slider组件实现mini视频播放进度条。还支持点击进度条/拖拽进度条到指定播放节点。

image.png

<!-- 短视频模块 -->
<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>

vue3聊天面板

p9-1.gif

聊天面板底部编辑器采用Editor封装组件,支持光标处插入gif图片、多行图文混排输入。

def00c9bbca9930c36b04eba978450f3_1289798-20240622182202761-902228363.png

聊天模板片段

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

使用高德地图@amap/amap-jsapi-loader插件,定位当前位置/根据经纬度加载地图。

030360截图20240621232500406.png

025360截图20240621231331642.png

// 拾取地图位置
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
}

地图功能比较简单,大家可以在此基础拓展一些其它功能。

Ok,综上就是vite5+vue3+elementPlus开发网页聊天实例的一些知识分享,限于篇幅就先分享到这里,希望以上分享对大家有所帮助!

juejin.cn/post/737134…

juejin.cn/post/736312…

juejin.cn/post/731918…

duitang.gif