從零打造前沿Web聊天組件:從設計到交互

作者現在制作一款網頁端聊天室(青春版),之前一直有這個想法,現在總算是邁出了第一步開始制作了… 雄關漫道真如鐵,而今邁步從頭越!

啟程

當前已經完成左側聊天室列表顯示,通過http://localhost:10086/chatRoom/chatRoomList接口進行獲取,可選傳入當前用戶id,這樣將返回所有存在此用戶的聊天室數據。

數據從node + express后端獲取,存儲數據庫為mongoDB

  • 獲取到的數據:

在這里插入圖片描述

  • 當前效果:

在這里插入圖片描述


組件設計

在聊天室項目里,右側的聊天內容區域組件設計要慎重,畢竟屬于項目當中的重中之重。

因為聊天一般分為群聊和私聊,所以我準備開發兩種復用組件groupChat(群聊)和privateChat(私聊)。

關于組件的引入,有兩種方案可選:動態組件模式單一組件條件渲染

這里我選擇使用 動態組件模式,其優點:

  1. 類型隔離:群聊/私聊邏輯完全解耦
  2. 性能優化key 保證切換時完全重建實例,可以進行異步加載
  3. 可擴展性:后續想到新的聊天類型,添加起來也非常方便

組件通信規范
- 父組件 → 子組件:Props
- 子組件 → 父組件:Emit
- 跨層級通信:Provide/Inject

這兩種聊天組件當中也具有相同的結構,實現私聊和群聊組件的高復用性設計,關鍵在于將通用邏輯特殊邏輯分離。

在這里插入圖片描述

對于設計思路,準備使用組合式API + 插槽組件的方式。

動手:組件創建

創建相應的組件文件,groupChat.vueprivateChat.vueBaseMessage.vue三個組件文件

在這里插入圖片描述

BaseMessage.vue中主要由三部分組成,這也是群聊和私聊的共同點,三部分分別是:

  1. 頂部:群聊顯示聊天室名稱,私聊顯示對方用戶名
  2. 內容:消息的滾動條列表,用戶頭像、消息氣泡、消息時間,間隔時間較久的消息間顯示時間分割線
  3. 底部:消息輸入框,可輸入文字、圖片、表情包

可以先添加相應插槽進入每一塊元素,這樣可以增強組件的可擴展性。

在這里插入圖片描述

接下來在群聊和私聊組件當中引入基礎插件,傳入聊天室名稱

在這里插入圖片描述

將群聊組件和私聊組件引入聊天室當中,使用動態切換的方式進行加載。

<div class="chat-main"><!-- 動態組件模式 --><component :is="curChatRoom.key" :key="curChatRoom.key" :chatRoom="curChatRoom" />
</div>
...
<script>import privateChat from "@/components/privateChat.vue";import groupChat from "@/components/groupChat.vue";// 定義組件選項,這樣下面可以直接用字符串名稱代表組件defineOptions({components:{groupChat,privateChat}})// 定義當前聊天的聊天室,動態組件的key,用于強制組件重新渲染const curChatRoom = reactive({roomId: "",key: "groupChat",roomName: "",roomType: "public"});// 切換聊天室,設置當前聊天室和組件const switchChatRoom = (room) => {curChatRoom.roomId = room.roomId;curChatRoom.key = room.roomType == "private" ? "privateChat" : "groupChat";curChatRoom.roomName = room.roomName;}
</script>

當前效果可做到點擊切換左側聊天室,右側群聊的名稱相應改變

在這里插入圖片描述

現在已經生成了可復用的群聊組件。

組件交互

在最外層數據傳入群聊groupChat和私聊private組件后,組件都需要根據變化的聊天室roomId去獲取此聊天室的聊天記錄并展示,因為當前我尚未在后端編寫相應接口,先用模擬數據代替。

這里用戶頭像數據不放在聊天數據一起,這能夠減輕聊天數據負擔。

// 聊天數據
const mockChatHistory = [{username: '張三',time: new Date('2024-07-01 10:00:00').toLocaleString(),content: '大家好,今天天氣不錯!'},{username: '李四',time: new Date('2024-07-01 10:05:00').toLocaleString(),content: '是的,適合出去走走。'},{username: '王五',time: new Date('2024-07-01 10:10:00').toLocaleString(),content: '有沒有推薦的地方?'}
];// 對應的頭像
mockChatUserAvatar = {"張三": 'icon-animal-4',"李四": 'icon-animal-9',"王五": 'icon-animal-1'
}

得到這些數據后,可以將數據傳入BaseMessage.vue基礎組件當中,當然外部切換聊天室需要內部對roomId進行監聽。

	// 假設這里有一個外部動態變化的響應式變量 propswatch(() => props.chatRoom?.roomId, async (newRoomId, oldRoomId) => {if (newRoomId) {await getChatUserAvatar();await getChatHistory();}});

注意:接下來很多內容都是在BaseMessage.vue基礎組件中,基礎組件由頭部、消息列表、輸入框三大部分組成

時間分割線

在聊天窗中判斷是否添加時間切割線,通常可參考時間維度聊天活躍度維度標準:

  1. 時間維度
  • 日期變化 :當新消息與上一條消息不在同一天時,添加時間切割線,這是最常見的判斷方式,能清晰區分不同日期的聊天記錄。
  • 設定時間間隔 :根據預設的時間間隔(如每 5 分鐘、每小時等)來判斷。當兩條消息的時間間隔超過設定值,就添加時間切割線,方便用戶按特定時間范圍查看聊天記錄 。
  1. 聊天活躍度維度
  • 消息密集程度 :若一段時間內聊天信息較為密集,即使在同一天,也可能添加多個時間切割線,以區分不同活躍時段的聊天內容;反之,若聊天信息比較稀疏,即使跨越幾天,也可能不添加切割線。

根據傳入的時間判斷時間分割線是否顯示:

// 判斷時間分隔線是否顯示
const shouldShowTimeDivider = (index) => {if (index === 0) return true; // 第一個消息顯示分隔線const currentTime = new Date(props.mockChatHistory[index].time);const previousTime = new Date(props.mockChatHistory[index - 1].time);if (currentTime - previousTime > 5 * 60 * 1000) { // 超過5分鐘顯示分隔線return true;} else {return false;}
}
// 時間分割線時間顯示形式
const formatTime = (time) => {const date = new Date(time);const now = new Date();const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());const yesterday = new Date(today);yesterday.setDate(yesterday.getDate() - 1);const dayBeforeYesterday = new Date(today);dayBeforeYesterday.setDate(dayBeforeYesterday.getDate() - 2);// 今天:顯示小時:分鐘if (date >= today) {return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });} // 昨天:顯示昨天 小時:分鐘else if (date >= yesterday) {return `昨天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;}// 前天:顯示前天 小時:分鐘else if (date >= dayBeforeYesterday) {return `前天 ${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;}// 今年:顯示月份-日期else if (date.getFullYear() === now.getFullYear()) {return `${date.getMonth() + 1}${date.getDate()}${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;}// 往年:顯示xxxx年else {return `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}`;}
}

對于時間分割線上時間內容顯示:

  • 當日時間:hh:mm
  • 昨天/前天: 昨天/前天 hh:mm
  • 今年內: 月份日期 hh:mm
  • 以往年份:年份月份日期 hh:mm

在這里插入圖片描述

PS:這里對聊天內容界面的元素布局和樣式等內容就不詳細說明了,都算比較基礎的了


消息輸入框

聊天消息輸入框需要兼顧用戶體驗、功能完備性和技術實現優雅性,當然現在我只進行基礎的設計

需要考慮的內容

  • 輸入框高度調節
  • 功能擴展區
  • @成員提及
  • 表情插入

高度調節

給底部chat-footer元素添加偽類,高度3px,設置ns-resize雙向調整大小光標

在這里插入圖片描述

CSS樣式:

&::before {content: '';position: absolute;top: 0px;left: 0;right: 0;height: 3px;background-color: #ccc;cursor: ns-resize;z-index: 1;
}

效果:

在這里插入圖片描述

現在光設置css樣式還不能拖動邊線,需要添加相應的Js方法:(這里在onDragMove移動方法中,設置可移動的最大200px最小100px,chat-content是底部輸入框上面的聊天內容區域)

// 底部footer輸入框拖動方法
const footerRef = ref(null);
const isDragging = ref(false);
const startY = ref(0);
const startHeight = ref(0);
// 底部輸入框頂部邊拖動 鼠標按下事件
const onDragStart = (e) => {e.preventDefault(); // 阻止默認行為isDragging.value = true;startY.value = e.clientY;startHeight.value = footerRef.value.offsetHeight;document.addEventListener('mousemove', onDragMove);document.addEventListener('mouseup', onDragEnd);document.body.style.userSelect = 'none'; // 禁用文本選擇
};
// 底部輸入框頂部邊拖動 鼠標移動事件
const onDragMove = (e) => {if (!isDragging.value) return;const dy = e.clientY - startY.value;const newHeight = startHeight.value - dy;if (newHeight >= 100 && newHeight <= 200) {footerRef.value.style.height = `${newHeight}px`;document.querySelector('.chat-content').style.height = `calc(100% - 50px - ${newHeight}px)`;}
};
// 底部輸入框頂部邊拖動 鼠標松開事件
const onDragEnd = () => {isDragging.value = false;document.removeEventListener('mousemove', onDragMove);document.removeEventListener('mouseup', onDragEnd);document.body.style.userSelect = ''; // 恢復文本選擇
};onMounted(() => {footerRef.value = document.querySelector('.chat-footer');// 直接在footer元素上監聽mousedown事件,通過event.target判斷是否點擊了頂部邊框footerRef.value.addEventListener('mousedown', (e) => {if (e.offsetY <= 3) { // 判斷是否點擊了頂部3px區域onDragStart(e);}});
});

在這里插入圖片描述

功能擴展區

有最基礎的表情和聊天記錄icon按鈕,如果后續有需要可以繼續添加擴展功能,考慮到后續可能群里和私聊功能不同,添加了slot擴展

<!-- 功能列表 -->
<div class="input-function-list"><div class="input-function-item" v-for="funItem in inputFunctionList" :key="funItem.name" :title="funItem.text"><svg class="icon" aria-hidden="true"><use :xlink:href="'#' + funItem.icon"></use></svg></div><slot name="input-extra-function"></slot>
</div>

基礎輸入框功能:

// 輸入框功能列表
const inputFunctionList = [{ name: 'emoji', icon: 'icon-biaoqing', text: '表情', event: () => { console.log('點擊了表情') }   },{ name: 'record', icon: 'icon-liaotianjilu', text: '聊天記錄', event: () => { console.log('點擊了聊天記錄')} }
];

可編輯DIV輸入框

使用div的contenteditable去制作一個可輸入消息框,讓div元素可編輯

<div class="input-area"><!-- 可擴展的輸入區域 --><slot name="input-area-solt"><div ref="editableDiv" class="editable-area" contenteditable @input="handleInput"@keydown.enter.exact.prevent="sendMessage"@paste="handlePaste"></div></slot>
</div>

這里設計:

  • enter:發送消息
  • shift + enter:換行

通過@keydown.enter.exact.prevent已經讓回車鍵和發送消息方法關聯起來了,現在設置shift + enter 配置換行。在div中配置 @keydown.shift.enter.prevent="handleShiftEnter"

handleShiftEnter方法用于獲取當前選區,在輸入框文本對象末尾添加換行節點br,并重新設置光標位置

// 輸入框換行事件處理
const handleShiftEnter = (e) => {e.preventDefault();const selection = window.getSelection();// 檢查選區是否在可編輯區域內if (!selection.containsNode(editableDiv.value, true)) {return;}const range = selection.getRangeAt(0);// 創建并插入換行節點const br = document.createElement('br');range.insertNode(br);// 創建新范圍并設置光標位置const newRange = document.createRange();newRange.setStartAfter(br);  // 在新創建的 <br> 元素后設置光標位置newRange.collapse(true);// 更新選區selection.removeAllRanges();selection.addRange(newRange);
}

效果:

在這里插入圖片描述

當前的復制會將顏色復制過來,所以需要設置handlePaste復制方法,只復制文本內容

在這里插入圖片描述

這里對于圖片數據復制時,也需要進行在輸入框自動縮小圖片尺寸,主要修改包括:

  • 檢測剪貼板中的圖片數據
  • 使用 FileReader 讀取原始圖片數據(event.target.result)創建 img 元素
  • 保持原有的 maxWidth 樣式設置將圖片縮小到最大寬度100px
  • 保持圖片比例不變
  • 將縮小后的圖片插入到可編輯區域
  • 仍然保留原有的純文本粘貼功能
// 輸入框復制事件處理
const handlePaste = (e) => {e.preventDefault();// 檢查是否由圖片數據const clipboardData = e.clipboardData;if (clipboardData.files && clipboardData.files.length > 0) {const file = clipboardData.files[0];if (file.type.startsWith('image/')) {const reader = new FileReader();reader.onload = (event) => {const range = window.getSelection().getRangeAt(0);range.deleteContents();const imgElement = document.createElement('img');imgElement.src = event.target.result;imgElement.style.maxWidth = '100px';range.insertNode(imgElement);};reader.readAsDataURL(file);return;}}// 處理純文本粘貼const text = e.clipboardData.getData('text/plain');document.execCommand('insertHTML', false, text);
}

在這里插入圖片描述

to be continued

讓作者先歇歇吧,成員提及和表情包插入功能尚未開發,后續還要加入最重要WebSocket發送機制

這些都將再下篇文章中出現,先到此這里吧

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/909013.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/909013.shtml
英文地址,請注明出處:http://en.pswp.cn/news/909013.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

計算機網絡 : 傳輸層協議UDP與TCP

計算機網絡 &#xff1a; 傳輸層協議UDP與TCP 目錄 計算機網絡 &#xff1a; 傳輸層協議UDP與TCP引言1. 傳輸層協議UDP1.2 UDP協議段格式1.3 UDP的特點1.4 面向數據報1.5 UDP的緩沖區1.6 基于UDP的應用層協議及使用注意事項 2. 傳輸層協議TCP2.1 再談端口號2.2 TCP協議段格式2.…

Java高頻面試之并發編程-27

hello啊&#xff0c;各位觀眾姥爺們&#xff01;&#xff01;&#xff01;本baby今天又來報道了&#xff01;哈哈哈哈哈嗝&#x1f436; 面試&#xff1a;詳細說說AtomicInteger 的原理 AtomicInteger 的原理詳解 AtomicInteger 是 Java 并發包 (java.util.concurrent.atomic)…

冒險島的魔法果實-多重背包

問題描述 在冒險島的深處&#xff0c;小萌探索到了一個傳說中的魔法果實園。這里滿是各種神奇的魔法果實&#xff0c;吃了可以增加不同的魔法能量。 小萌想帶一些魔法果實回去&#xff0c;但是他的背包空間有限。看著這些琳瑯滿目的魔法果實&#xff0c;小萌很是糾結&#xf…

atomicity of memory accesses

文章目錄 atomicity of memory accesses? 正確認識原子性的邊界對于 **Load**&#xff1a;? 正確的原子性邊界是&#xff1a;對于 **Store**&#xff1a;? 正確的原子性邊界是&#xff1a; &#x1f504; 修正原文中的說法&#xff08;對照分析&#xff09;? 原子性邊界最終…

VScode安裝配置PYQT6

開始是準備安裝PYQT5的&#xff0c;但是安裝不下去&#xff0c;就改成安裝PYQT6 一.安裝pyqt5&#xff0c;成功。 c:\PYQT>pip install pyqt5 Defaulting to user installation because normal site-packages is not writeable Collecting pyqt5 Downloading PyQt5-5.15.…

SpringBoot使用oshi獲取服務器相關信息

概念 OSHI是Java的免費基于JNA的&#xff08;本機&#xff09;操作系統和硬件信息庫。它不需要安裝任何其他本機庫&#xff0c;并且旨在提供一種跨平臺的實現來檢索系統信息&#xff0c;例如操作系統版本&#xff0c;進程&#xff0c;內存和CPU使用率&#xff0c;磁盤和分區&a…

Spring Boot 3 集成 MyBatis 連接 MySQL 數據庫

Spring Boot 3 集成 MyBatis 連接 MySQL 數據庫的步驟&#xff1a; 以下是集成 Spring Boot 3、MyBatis、HikariCP 連接池并操作 MySQL 數據庫的完整步驟和代碼&#xff1a; 一、創建 Spring Boot 項目 添加以下依賴&#xff1a; <dependencies><!-- Spring Web --…

基于React + FastAPI + LangChain + 通義千問的智能醫療問答系統

&#x1f4cc; 文章摘要&#xff1a; 本文詳細介紹了如何在前端通過 Fetch 實現與 FastAPI 后端的 流式響應通信&#xff0c;并支持圖文多模態數據上傳。通過構建 multipart/form-data 請求&#xff0c;配合 ReadableStream 實時讀取 AI 回復內容&#xff0c;實現類似 ChatGPT…

YOLOv8 升級之路:主干網絡嵌入 SCINet,優化黑暗環境目標檢測

文章目錄 引言1. 低照度圖像檢測的挑戰1.1 低照度環境對目標檢測的影響1.2 傳統解決方案的局限性2. SCINet網絡原理2.1 SCINet核心思想2.2 網絡架構3. YOLOv8與SCINet的集成方案3.1 總體架構設計3.2 關鍵集成代碼3.3 訓練策略4. 實驗結果與分析4.1 實驗設置4.2 性能對比4.3 可視…

所有的Linux桌面環境

Linux操作系統提供了多種桌面環境&#xff0c;每種都有其獨特的特點和適用場景。以下是一些常見的Linux桌面環境&#xff1a; 輕量級桌面環境 Xfce&#xff1a;廣泛使用的輕量級桌面環境&#xff0c;適合資源有限的設備。Xfce 4.18帶來了性能改進和新功能&#xff0c;如Thuna…

@component、@bean、@Configuration的區別

詳細解析Spring框架中這三個最核心、也最容易混淆的注解&#xff1a;Component、Bean和Configuration。 為了快速理解&#xff0c;我們先看一個總結性的表格&#xff1a; 注解應用級別作用使用場景Component類級別將類標識為Spring組件&#xff0c;讓Spring自動掃描并創建實例…

Android多媒體——音/視同步數據處理(二十)

在多媒體播放過程中,音頻數據的處理不僅要保證其解碼和輸出的連續性,還需要與視頻幀保持時間上的嚴格對齊,以實現良好的觀看體驗。Android 多媒體框架中的 NuPlayerRenderer 是負責最終渲染音視頻數據的核心組件之一。 一、Audio數據處理 NuPlayerRenderer 是 Android 原生…

MYSQL 使用命令mysqldump備份數據庫的時候需要用戶具備什么權限

背景 之前都是使用數據庫root用戶備份數據庫&#xff0c;沒有權限問題&#xff0c;今天使用一個數據庫基本用戶備份數據庫&#xff0c;提示一直沒有權限&#xff0c;提示的很明顯 mysqldump: Error: Access denied; you need (at least one of) the PROCESS privilege(s) for …

WebRTC源碼線程-1

1、概述 本篇主要是簡單介紹WebRTC中的線程&#xff0c;WebRTC源碼對線程做了很多的封裝。 1.1 WebRTC中線程的種類 1.1.1 信令線程 用于與應用層的交互&#xff0c;比如創建offer&#xff0c;answer&#xff0c;candidate等絕大多數的操作 1.1.2 工作線程 負責內部的處理邏輯&…

spring:使用標簽xml靜態工廠方法獲取bean

在spring可以直接通過配置文件獲取bean對象&#xff0c;如果獲取的bean對象還有若干設置&#xff0c;需要自動完成&#xff0c;可以通過工廠方法獲取bean對象。 靜態工廠類&#xff0c;其中InterfaceUserDao和InterfaceUserService都是自定義的接口&#xff0c;可以自己替換。…

linux 用戶態時間性能優化工具perf/strace/gdb/varlind/gprof

1. perf top -g或者top分析卡頓(cpu占用比較高的函數) gdb 是 GNU 調試器,可以用于分析程序的時間性能。雖然 info time 不是直接用于性能分析的命令,但 gdb 提供了與時間相關的功能,例如通過 timer 命令設置計時器或通過 info proc 查看進程的時間信息。 #include <…

客戶端和服務器已成功建立 TCP 連接【輸出解析】

文章目錄 圖片**1. 連接狀態解析****第一條記錄&#xff08;服務器監聽&#xff09;****第二條記錄&#xff08;客戶端 → 服務器&#xff09;****第三條記錄&#xff08;服務器 → 客戶端&#xff09;** **2. 關鍵概念澄清****(1) 0.0.0.0 的含義****(2) 端口號的分配規則** *…

Win系統下的Linux系統——WSL 使用手冊

我們在復現一些項目的時候&#xff0c;有些依賴包只能在 linux 環境下使用&#xff0c;還不打算使用遠程服務器&#xff0c;那么此時我們可以使用 WSL 創建一個 ubutu 系統&#xff0c;在這個系統里創建虛擬環境、下載依賴包。然后&#xff0c;我們就可以在 windows 下的 vscod…

電腦同時連接內網和外網的方法,附外網連接局域網的操作設置

對于工作一般都設置在內網網段中&#xff0c;而同時由于需求需要連接外網&#xff0c;一般只能通過內網和外網的不斷切換進行設置&#xff0c;如果可以同時連接內網和外網會更加便利&#xff0c;同時連接內網和外網方法具體如下。 一、電腦怎么弄可以同時連接內網和外網&#…

C++11:原子操作與內存順序:從理論到實踐的無鎖并發實現

文章目錄 0.簡介1.并發編程需要保證的特性2.原子操作2.1 原子操作的特性 3.內存順序3.1 順序一致性3.2 釋放-獲取&#xff08;Release-Acquire)3.3 寬松順序&#xff08;Relaxed)3.4 內存順序 4.無鎖并發5. 使用建議 0.簡介 在并發編程中&#xff0c;原子性、可見性和有序性是…