Vite-Wechat網頁聊天室|vite5.x+vue3+pinia+element-plus仿微信客戶端

xiaoyan2017發表於2024-06-22

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

相關文章