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