這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 Vue3+TS(uniapp)手擼一個聊天頁面 前言 最近在自己的小程式中做了一個智能客服,API使用的是雲廠商的API,然後聊天頁面...嗯,找了一下關於UniApp(vite/ts)版本的好像不多,有一個官方的但其中的其他代碼太多了, ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
Vue3+TS(uniapp)手擼一個聊天頁面
前言
最近在自己的小程式中做了一個智能客服,API使用的是雲廠商的API,然後聊天頁面...嗯,找了一下關於UniApp(vite/ts)版本的好像不多,有一個官方的但其中的其他代碼太多了,去看懂再刪除那些對我無用的代碼不如自己手擼一個,先看效果:
好,下麵開始介紹如何一步一步實現
重難點調研
1. 如何編寫氣泡
可以發現一般的氣泡是有個“小箭頭”,一般是指向用戶的頭像,所以這裡我們的初步思路就是通過before
與after
偽類來放置這個小三角形,這個小三角形通過隱藏border的其餘三邊來實現。
然後其中一個細節就是聊天氣泡的最大寬度不超過對方的頭像,超過就換行。這個簡單,設置一個max-width: cacl(100vw - XX)
就可以了
2. 如何編寫輸入框
考慮到用戶可能輸入多行文字,這裡使用的是<textarea>
標簽,點開微信發個消息試試,發現它是自適應的,這裡去調研瞭解了一下,發現小程式自帶組件有這個實現,好,那直接用:
然後我們繼續註意到發送按鈕與輸入框的底線保持水平,這個flex
里有對應屬性可以實現,跳過...
3.如何實現滾動條始終居於底部
當聊天消息較多時,我們發現我們繼續輸入消息,頁面並沒有更新(滾動)。打開微信聊天框一看,當消息過多時,你發一條消息,頁面就自動滾動到了最新的消息,這又是怎實現的呢?
繼續調研,發現小程式自帶的<scroll-view>
標簽中有個屬性scroll-into-view
可以自動跳轉:
<scroll-view scroll-y="true" :scroll-into-view="`msg${messages.length-1}`" :scroll-with-animation="true"> <view class="msg-list" :id="`msg${index}`" v-for="(msg, index) in messages" :key="msg.time"> <view class="msg-item"> 略 </view> </view> </scroll-view>
概述
簡單分析下來好像一點都不難,如下是我的文件列表,話不多說,開始擼代碼!
chat ├─ chat.vue ├─ leftBubble.vue └─ rightBubble.vue
左氣泡模塊
左氣泡模塊就是剛剛分析的那一部分,然後增加一點點細節,如下:
<template> <view class="left-bubble-container"> <view class="left"> <image :src="props.avatarUrl"></image> </view> <view class="right"> <view class="bubble"> <text>{{ props.message }}</text> </view> </view> </view> </template> <script setup lang="ts"> import { userDefaultData } from "@/const"; interface propsI { message: string; avatarUrl: string; } const props = withDefaults(defineProps<propsI>(), { avatarUrl: userDefaultData.avatarUrl, }); </script> <style lang="scss" scoped> .left-bubble-container { margin: 10px 0; display: flex; .left { image { height: 50px; width: 50px; border-radius: 5px; } } } .bubble { max-width: calc(100vw - 160px); min-height: 25px; border-radius: 10px; background-color: #ffffff; position: relative; margin-left: 20px; padding: 15px; text { height: 25px; line-height: 25px; } } .bubble::before { position: absolute; top: 15px; left: -20px; content: ""; width: 0; height: 0; border-right: 10px solid #ffffff; border-bottom: 10px solid transparent; border-left: 10px solid transparent; border-top: 10px solid transparent; } </style>
右氣泡模塊
右氣泡模塊我們需要將三角形放在右邊,這個好實現。然後這整個氣泡我們需要讓它處於水平居右,所以這裡我使用了:
display: flex; direction: rtl;
這個屬性,但使用的過程中發現氣泡中的內容(符號與文字)會出現翻轉,“遇事不決,再加一層”,所以我們在內容節點外再套一層:
<span style="direction: ltr; unicode-bidi: bidi-override"> <text>{{ props.message }}</text> </span>
然後繼續增加一點點細節:
<template> <view class="left-bubble-container"> <view class="right"> <image :src="props.avatarUrl"></image> </view> <view class="left"> <view class="bubble"> <span style="direction: ltr; unicode-bidi: bidi-override"> <text>{{ props.message }}</text> </span> </view> </view> </view> </template> <script setup lang="ts"> import { userDefaultData } from "@/const"; interface propsI { message: string; avatarUrl: string; } const props = withDefaults(defineProps<propsI>(), { avatarUrl: userDefaultData.avatarUrl, }); </script> <style lang="scss" scoped> .left-bubble-container { display: flex; direction: rtl; margin: 10px 0; .right { image { height: 50px; width: 50px; border-radius: 5px; } } } .bubble { max-width: calc(100vw - 160px); min-height: 25px; border-radius: 10px; background-color: #ffffff; position: relative; margin-right: 20px; padding: 15px; text-align: left; text { height: 25px; line-height: 25px; } } .bubble::after { position: absolute; top: 15px; right: -20px; content: ""; width: 0; height: 0; border-right: 10px solid transparent; border-bottom: 10px solid transparent; border-left: 10px solid #ffffff; border-top: 10px solid transparent; } </style>
輸入模塊
沒啥說的,需要註意的是:Button
記得防抖
<view class="bottom-input"> <view class="textarea-container"> <textarea auto-height fixed="true" confirm-type="send" v-model="input" @confirm="submit" /> </view> <button style=" width: 70px; height: 40px; line-height: 34px; margin: 0 10px; background-color: #ffffff; border: 3px solid #0256ff; color: #0256ff; " @click="submit" > 發送 </button>
整體
1)考慮如何存儲消息
這裡僅考慮記憶體中如何存儲,不考慮本地存儲,後續思考中會聊到。
export interface messagesI { left: boolean; text: string; time: number; }
如上是消息列表中的一項,為了區分是渲染到左氣泡還是右氣泡,這裡用left
來區分了一下;
const messages: Ref<messagesI[]> = ref([]);
2)如何推薦消息
這邊我封裝的服務端介面是這樣的:
mutation chat{ customerChat(talk: "你好啊"){ knowledge text recommend } }
recommend
是用戶可能輸入了錯誤的消息,這裡是預測用戶的輸入字元串,所以我們需要在得到這個字元串後直接顯示,然後用戶可以一鍵通過這條消息回覆:
function submit(){ // 略... const finalMsg = receive?.knowledge || receive?.text || "你是否想問: " + receive?.recommend; // 略... if (receive?.recommend) { input.value = receive?.recommend; } else { input.value = ""; } }
如上,得益於Vue框架,這裡實現起來也非常簡單,當用戶提交之後,如果有推薦的消息,就直接修改input.value
從而修改輸入框的文字;如果沒有就直接清空方便下一次輸入。
接下來繼續增加一點點細節(chat.vue
文件)
<template> <view class="chat-container"> <view class="msg-container"> <!-- https://github.com/wepyjs/wepy-wechat-demo/issues/7 --> <scroll-view scroll-y="true" :scroll-into-view="`msg${messages.length-1}`" :scroll-with-animation="true"> <view class="msg-list" :id="`msg${index}`" v-for="(msg, index) in messages" :key="msg.time"> <view class="msg-item"> <left-bubble v-if="msg.left" :message="msg.text" :avatar-url="meStore.user?.avatarUrl"></left-bubble> <right-bubble v-else :message="msg.text" :avatar-url="logoUrl"></right-bubble> </view> </view> </scroll-view> </view> <view class="bottom-input"> <view class="textarea-container"> <textarea auto-height fixed="true" confirm-type="send" v-model="input" @confirm="submit" /> </view> <button style=" width: 70px; height: 40px; line-height: 34px; margin: 0 10px; background-color: #ffffff; border: 3px solid #0256ff; color: #0256ff; " @click="submit" > 發送 </button> </view> </view> </template> <script setup lang="ts"> import { ref, type Ref } from "vue"; import leftBubble from "./leftBubble.vue"; import rightBubble from "./rightBubble.vue"; import type { messagesI } from "./chat.interface"; import { chatGQL } from "@/graphql/me.graphql"; import { useMutation } from "villus"; import { logoUrl } from "@/const"; import { useMeStore } from "@/stores/me.store"; const meStore = useMeStore(); const messages: Ref<messagesI[]> = ref([]); const input = ref(""); async function submit() { if (input.value === "") return; messages.value.push({ left: true, text: input.value, time: new Date().getTime(), }); const { execute } = useMutation(chatGQL); const { error, data } = await execute({ talk: input.value }) if (error) { uni.showToast({ title: `載入錯誤`, icon: "error", duration: 3000, }); throw new Error(`載入錯誤: ${error}`); } const receive = data?.customerChat; const finalMsg = receive?.knowledge || receive?.text || "你是否想問: " + receive?.recommend; messages.value.push({ left: false, text: finalMsg, time: new Date().getTime(), }); if (receive?.recommend) { input.value = receive?.recommend; } else { input.value = ""; } } </script> <style lang="scss" scoped> .chat-container { .msg-container { padding: 20px 5px 100px 5px; height: calc(100vh - 120px); scroll-view { height: 100%; } } .bottom-input { display: flex; align-items: flex-end; position: fixed; bottom: 0px; background-color: #fbfbfb; padding: 20px; box-shadow: 0px -10px 30px #eeeeee; .textarea-container { background-color: #ffffff; padding: 10px; textarea { width: calc(100vw - 146px); background-color: #ffffff; } } } } </style>
思考
如何保存到本地,然後每次載入最新消息,然後向上滾動進行懶載入?
我這裡沒有實現該功能,畢竟只是一個客服,前端沒必要保存消息記錄到本地如Localstorage。
這裡拋磚引玉,想到了一個最基礎的數據結構--鏈表,用Localstorage-key/value的形式來實現消息隊列在本地的多段存儲:
當然,有效性有待驗證,這裡僅僅屬於一些想法
最後
然後,我擼了小半天的頁面,準備給朋友看看來著,他告訴我微信小程式自帶一個客服系統,只需要讓button
的open-type
屬性等於contract
;