eagleye

Quasar QChatMessage 组件企业级实现文档

Quasar QChatMessage 组件企业级实现文档

一、组件概述

QChatMessage 是基于 Quasar 框架和 Vue 3 组合式 API 开发的企业级聊天消息组件,支持文本/图片/文件消息展示、消息状态追踪、回复/转发/删除等交互功能,并通过智能分组、响应式设计和 TypeScript 类型安全,满足复杂聊天场景需求。

二、核心特性

  • 多消息类型支持:文本、图片、文件消息统一渲染,支持自定义消息气泡样式
  • 消息状态可视化:发送中/已发送/已送达/已读状态指示,实时反馈消息状态
  • 智能消息分组:按日期(今天/昨天/具体日期)和时间间隔(>30分钟)自动分组,提升可读性
  • 连续消息优化:同一用户2分钟内连续发送的消息自动合并,隐藏重复头像和名称
  • 丰富交互功能:回复、转发、复制、删除消息,支持附件上传(文件/图片)
  • TypeScript 全类型支持:完整类型定义,确保数据结构规范和开发体验
  • 响应式设计:自适应不同屏幕尺寸,消息气泡和操作菜单布局自动调整

三、类型定义

3.1 核心类型(types/chat.ts)

// types/chat.ts

export interface User {

id: string; // 用户唯一标识

name: string; // 用户名(显示用)

avatar: string; // 头像URL

online?: boolean; // 在线状态(可选)

}

export type MessageStatus = 'sending' | 'sent' | 'delivered' | 'read' | 'error'; // 消息状态

export type MessageType = 'text' | 'image' | 'file' | 'system'; // 消息类型

export interface ChatMessage {

id: string; // 消息唯一标识

content: string; // 消息内容(文本/文件路径/图片URL)

timestamp: Date; // 发送时间戳

sender: User; // 发送者信息

status: MessageStatus; // 消息状态

type: MessageType; // 消息类型

replyTo?: string; // 回复目标消息ID(可选)

fileName?: string; // 文件名(文件类型消息)

fileSize?: number; // 文件大小(字节,文件类型消息)

imageUrl?: string; // 图片URL(图片类型消息)

}

export interface ChatRoom {

id: string; // 聊天室ID

name: string; // 聊天室名称

participants: User[]; // 参与者列表

lastMessage?: ChatMessage;// 最后一条消息

unreadCount: number; // 未读消息数

isGroup: boolean; // 是否为群聊

}

四、组件实现

4.1 完整组件代码(ChatMessages.vue)

<template>

<div class="chat-container q-pa-md">

<!-- 聊天标题区域 -->

<div class="chat-header q-mb-md">

<div class="text-h5">企业聊天应用</div>

<q-badge color="green" v-if="isConnected">

在线 ({{ onlineUsers }}人在线)

</q-badge>

<q-badge color="red" v-else>

离线

</q-badge>

</div>

<!-- 聊天消息区域(滚动容器) -->

<div class="chat-messages" ref="messagesContainer">

<!-- 1. 日期分隔符(按天分组) -->

<div

v-for="(dateGroup, dateIndex) in groupedMessages"

:key="`date-${dateIndex}`"

class="date-separator"

>

<q-separator />

<div class="date-label text-center q-my-md">

<q-badge color="primary">

{{ formatDateSeparator(dateGroup.date) }} <!-- 今天/昨天/YYYY年MM月DD日 -->

</q-badge>

</div>

</div>

<!-- 2. 消息分组(按时间间隔 >30分钟) -->

<div

v-for="(messageGroup, groupIndex) in groupedMessages"

:key="`group-${groupIndex}`"

>

<!-- 时间分隔符(组内首条消息或间隔>30分钟显示) -->

<div

v-if="shouldShowTimeSeparator(messageGroup, groupIndex)"

class="time-separator text-center q-my-sm"

>

<q-badge color="grey-6" rounded>

{{ formatTimeSeparator(messageGroup.messages[0].timestamp) }} <!-- HH:mm -->

</q-badge>

</div>

<!-- 3. 消息列表 -->

<div

v-for="(message, msgIndex) in messageGroup.messages"

:key="message.id"

class="message-wrapper"

:class="{

'consecutive': isConsecutiveMessage(messageGroup.messages, msgIndex), // 连续消息样式

'first-in-group': msgIndex === 0 // 组内首条消息样式

}"

@mouseenter="hoveredMessage = message.id"

@mouseleave="hoveredMessage = null"

>

<!-- Quasar QChatMessage 核心组件 -->

<q-chat-message

:name="message.sender.name" <!-- 发送者名称 -->

:avatar="message.sender.avatar" <!-- 发送者头像 -->

:text="[message.content]" <!-- 消息内容(数组格式支持多行) -->

:stamp="formatMessageTime(message.timestamp)" <!-- 消息时间戳 -->

:sent="message.sender.id === currentUser.id" <!-- 是否为当前用户发送(右对齐) -->

:bg-color="message.sender.id === currentUser.id ? 'primary' : 'grey-4'" <!-- 气泡颜色 -->

text-color="white" <!-- 文本颜色 -->

size="8" <!-- 头像大小(8px*8=64px) -->

class="chat-message"

>

<!-- 4. 消息状态指示器(仅当前用户消息) -->

<template v-if="message.sender.id === currentUser.id" v-slot:stamp>

<div class="message-status">

{{ formatMessageTime(message.timestamp) }}

<q-icon

v-if="message.status === 'sent'"

name="done"

size="14px"

class="q-ml-xs"

/>

<q-icon

v-else-if="message.status === 'delivered'"

name="done_all"

size="14px"

class="q-ml-xs"

/>

<q-icon

v-else-if="message.status === 'read'"

name="done_all"

size="14px"

color="blue"

class="q-ml-xs"

/>

<q-icon

v-else-if="message.status === 'error'"

name="error"

size="14px"

color="red"

class="q-ml-xs"

/>

</div>

</template>

<!-- 5. 消息内容与操作菜单 -->

<template v-slot:default>

<div class="message-content">

<!-- 文本内容 -->

{{ message.content }}

<!-- 图片附件 -->

<div v-if="message.type === 'image'" class="image-attachment">

<q-img

:src="message.imageUrl"

style="max-width: 200px; max-height: 200px; border-radius: 8px;"

class="q-mt-xs"

/>

</div>

<!-- 文件附件 -->

<div v-if="message.type === 'file'" class="file-attachment">

<q-icon name="attach_file" size="16px" />

<span class="file-name">{{ message.fileName }}</span>

<span class="file-size">({{ formatFileSize(message.fileSize || 0) }})</span>

</div>

<!-- 6. 消息操作按钮(悬停显示) -->

<div v-if="hoveredMessage === message.id" class="message-actions">

<q-btn

flat

round

dense

icon="reply"

size="sm"

@click="replyToMessage(message)"

/>

<q-btn

flat

round

dense

icon="delete"

size="sm"

@click="deleteMessage(message.id)"

v-if="message.sender.id === currentUser.id" <!-- 仅自己可删除 -->

/>

<q-btn

flat

round

dense

icon="more_vert"

size="sm"

>

<q-menu> <!-- 更多操作菜单 -->

<q-list style="min-width: 100px">

<q-item clickable v-close-popup @click="copyMessage(message)">

<q-item-section>复制</q-item-section>

</q-item>

<q-item clickable v-close-popup @click="forwardMessage(message)">

<q-item-section>转发</q-item-section>

</q-item>

</q-list>

</q-menu>

</q-btn>

</div>

</div>

</template>

</q-chat-message>

</div>

</div>

</div>

<!-- 7. 回复消息预览 -->

<div v-if="replyingTo" class="reply-preview q-pa-xs bg-grey-2 rounded-borders">

<div class="row items-center">

<div class="col">

<div class="text-caption text-weight-bold">回复 {{ replyingTo.sender.name }}</div>

<div class="text-caption text-grey-8 text-ellipsis">

{{ truncateText(replyingTo.content, 50) }} <!-- 截断长文本 -->

</div>

</div>

<div class="col-auto">

<q-btn flat round dense icon="close" size="sm" @click="cancelReply" />

</div>

</div>

</div>

<!-- 8. 消息输入区域 -->

<div class="chat-input q-mt-md">

<div class="row items-center">

<!-- 附件按钮 -->

<div class="col-auto">

<q-btn flat round icon="attach_file" @click="toggleAttachmentMenu">

<q-menu v-model="showAttachmentMenu">

<q-list>

<q-item clickable @click="attachFile">

<q-item-section avatar>

<q-icon name="insert_drive_file" />

</q-item-section>

<q-item-section>文件</q-item-section>

</q-item>

<q-item clickable @click="attachImage">

<q-item-section avatar>

<q-icon name="image" />

</q-item-section>

<q-item-section>图片</q-item-section>

</q-item>

</q-list>

</q-menu>

</q-btn>

</div>

<!-- 输入框 -->

<div class="col">

<q-input

v-model="newMessage"

outlined

dense

placeholder="输入消息..."

@keypress.enter="sendMessage" <!-- 回车发送 -->

:disable="!isConnected" <!-- 离线时禁用 -->

>

<template v-slot:append>

<q-icon

name="send"

class="cursor-pointer"

@click="sendMessage"

:color="newMessage.trim() ? 'primary' : 'grey'" <!-- 有内容时高亮发送按钮 -->

/>

</template>

</q-input>

</div>

</div>

</div>

</div>

</template>

<script setup lang="ts">

import { ref, computed, onMounted, nextTick, watch } from 'vue';

import { date, useQuasar } from 'quasar';

import { ChatMessage, User } from './types/chat'; // 导入类型定义

// 1. 组件接口定义(Props & Emits)

interface MessageGroup { // 内部消息分组类型

date: string; // 日期(YYYY-MM-DD)

messages: ChatMessage[]; // 该日期下的消息

}

// Props(外部传入参数)

const props = withDefaults(defineProps<{

initialMessages?: ChatMessage[]; // 初始消息列表

currentUser: User; // 当前用户信息

isConnected?: boolean; // 连接状态(在线/离线)

onlineUsers?: number; // 在线用户数

}>(), {

initialMessages: () => [],

isConnected: false,

onlineUsers: 0

});

// Emits(对外事件)

const emit = defineEmits<{

(e: 'send-message', message: Partial<ChatMessage>): void; // 发送消息

(e: 'delete-message', messageId: string): void; // 删除消息

(e: 'reply-to-message', message: ChatMessage, reply: string): void; // 回复消息

(e: 'forward-message', message: ChatMessage): void; // 转发消息

}>();

// 2. 依赖与状态初始化

const $q = useQuasar(); // Quasar工具(通知、对话框等)

const messages = ref<ChatMessage[]>(props.initialMessages); // 消息列表(响应式)

const newMessage = ref(''); // 输入框内容

const replyingTo = ref<ChatMessage | null>(null); // 当前回复目标消息

const hoveredMessage = ref<string | null>(null); // 悬停中的消息ID

const showAttachmentMenu = ref(false); // 附件菜单显示状态

const messagesContainer = ref<HTMLElement | null>(null); // 消息容器(滚动用)

// 3. 计算属性(消息分组逻辑)

const groupedMessages = computed((): MessageGroup[] => {

const groups: Record<string, ChatMessage[]> = {}; // 按日期分组的临时对象

// 按日期(YYYY-MM-DD)分组消息

messages.value.forEach(message => {

const dateKey = date.formatDate(message.timestamp, 'YYYY-MM-DD'); // 标准化日期

if (!groups[dateKey]) groups[dateKey] = [];

groups[dateKey].push(message);

});

// 转换为数组并按日期排序(升序)

return Object.entries(groups)

.map(([date, messages]) => ({ date, messages }))

.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());

});

// 4. 核心方法实现

// 发送消息

const sendMessage = (): void => {

if (!newMessage.value.trim()) return; // 空消息不发送

const messageData: Partial<ChatMessage> = {

content: newMessage.value.trim(),

timestamp: new Date(),

sender: props.currentUser,

status: 'sent',

type: 'text'

};

if (replyingTo.value) { // 如处于回复状态,附加回复目标ID

messageData.replyTo = replyingTo.value.id;

emit('reply-to-message', replyingTo.value, newMessage.value.trim()); // 触发回复事件

} else {

emit('send-message', messageData); // 触发发送事件

}

newMessage.value = ''; // 清空输入框

cancelReply(); // 重置回复状态

scrollToBottom(); // 滚动到底部

};

// 删除消息

const deleteMessage = (messageId: string): void => {

emit('delete-message', messageId); // 触发删除事件

};

// 回复消息

const replyToMessage = (message: ChatMessage): void => {

replyingTo.value = message; // 设置回复目标

nextTick(() => {

(document.querySelector('input') as HTMLInputElement)?.focus(); // 聚焦输入框

});

};

// 取消回复

const cancelReply = (): void => {

replyingTo.value = null;

};

// 复制消息内容

const copyMessage = (message: ChatMessage): void => {

navigator.clipboard.writeText(message.content)

.then(() => {

$q.notify({ type: 'positive', message: '消息已复制' }); // 复制成功通知

})

.catch(err => console.error('复制失败:', err));

};

// 转发消息

const forwardMessage = (message: ChatMessage): void => {

emit('forward-message', message); // 触发转发事件

$q.notify({ type: 'info', message: '转发功能待实现' });

};

// 附件处理(文件/图片)

const attachFile = (): void => {

showAttachmentMenu.value = false;

$q.notify({ type: 'info', message: '文件附件功能待实现' });

};

const attachImage = (): void => {

showAttachmentMenu.value = false;

$q.notify({ type: 'info', message: '图片附件功能待实现' });

};

// 切换附件菜单显示

const toggleAttachmentMenu = (): void => {

showAttachmentMenu.value = !showAttachmentMenu.value;

};

// 滚动到底部(新消息时自动滚动)

const scrollToBottom = (): void => {

nextTick(() => {

if (messagesContainer.value) {

messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;

}

});

};

// 5. 辅助方法(格式化与判断逻辑)

// 格式化消息时间(HH:mm)

const formatMessageTime = (timestamp: Date): string => {

return date.formatDate(timestamp, 'HH:mm');

};

// 格式化日期分隔符(今天/昨天/YYYY年MM月DD日)

const formatDateSeparator = (dateStr: string): string => {

const today = new Date();

const yesterday = new Date(today);

yesterday.setDate(yesterday.getDate() - 1);

const targetDate = new Date(dateStr);

if (date.formatDate(targetDate, 'YYYY-MM-DD') === date.formatDate(today, 'YYYY-MM-DD')) {

return '今天';

} else if (date.formatDate(targetDate, 'YYYY-MM-DD') === date.formatDate(yesterday, 'YYYY-MM-DD')) {

return '昨天';

} else {

return date.formatDate(targetDate, 'YYYY年MM月DD日');

}

};

// 格式化时间分隔符(HH:mm)

const formatTimeSeparator = (timestamp: Date): string => {

return date.formatDate(timestamp, 'HH:mm');

};

// 判断是否显示时间分隔符(组内首条消息或与前一组间隔>30分钟)

const shouldShowTimeSeparator = (group: MessageGroup, groupIndex: number): boolean => {

if (group.messages.length === 0) return false;

if (groupIndex === 0) return true; // 第一组始终显示

const prevGroup = groupedMessages.value[groupIndex - 1];

if (!prevGroup?.messages.length) return true;

const lastPrevTime = prevGroup.messages[prevGroup.messages.length - 1].timestamp;

const firstCurrentTime = group.messages[0].timestamp;

const timeDiff = firstCurrentTime.getTime() - lastPrevTime.getTime();

return timeDiff > 30 * 60 * 1000; // 间隔>30分钟显示

};

// 判断是否为连续消息(同一用户2分钟内发送)

const isConsecutiveMessage = (messages: ChatMessage[], index: number): boolean => {

if (index === 0) return false; // 第一条不是连续消息

const current = messages[index];

const previous = messages[index - 1];

const isSameSender = current.sender.id === previous.sender.id;

const timeDiff = current.timestamp.getTime() - previous.timestamp.getTime();

return isSameSender && timeDiff < 2 * 60 * 1000; // 2分钟内

};

// 格式化文件大小(Bytes/KB/MB/GB)

const formatFileSize = (bytes: number): string => {

if (bytes === 0) return '0 Bytes';

const k = 1024;

const sizes = ['Bytes', 'KB', 'MB', 'GB'];

const i = Math.floor(Math.log(bytes) / Math.log(k));

return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;

};

// 截断长文本(超出maxLength显示...)

const truncateText = (text: string, maxLength: number): string => {

return text.length <= maxLength ? text : `${text.substring(0, maxLength)}...`;

};

// 5. 生命周期与监听

// 初始化时滚动到底部

onMounted(() => {

scrollToBottom();

});

// 监听消息列表变化,自动滚动到底部

watch(messages, () => {

scrollToBottom();

}, { deep: true }); // 深度监听数组变化

// 监听初始消息 props 变化,更新本地消息列表

watch(() => props.initialMessages, (newMessages) => {

messages.value = newMessages;

}, { immediate: true }); // 初始加载时立即执行

</script>

<style scoped lang="scss">

.chat-container {

height: 100%;

display: flex;

flex-direction: column;

max-width: 800px; // 限制最大宽度,提升大屏体验

margin: 0 auto; // 居中布局

}

.chat-header {

display: flex;

justify-content: space-between;

align-items: center;

}

.chat-messages {

flex: 1; // 占满剩余空间

overflow-y: auto; // 垂直滚动

padding: 8px;

scroll-behavior: smooth; // 平滑滚动

}

/* 日期分隔符样式 */

.date-separator {

position: relative;

.date-label {

position: relative;

z-index: 1; // 覆盖分隔线

:deep(.q-badge) {

padding: 4px 12px;

font-weight: normal;

}

}

}

/* 时间分隔符样式 */

.time-separator {

:deep(.q-badge) {

padding: 2px 8px;

opacity: 0.7; // 半透明

}

}

/* 消息容器样式 */

.message-wrapper {

margin-bottom: 4px;

&.consecutive { // 连续消息:隐藏重复头像和名称

:deep(.q-chat-message) {

margin-top: 2px;

.q-chat-message-name { display: none; } // 隐藏名称

.q-chat-message-avatar { visibility: hidden; } // 隐藏头像

.q-chat-message-stamp { margin-top: -4px; } // 调整时间戳位置

}

}

&.first-in-group { // 组内首条消息:增加顶部间距

margin-top: 12px;

}

}

/* 消息内容区域 */

.message-content {

position: relative; // 为操作按钮定位

}

/* 消息操作按钮(悬停显示) */

.message-actions {

position: absolute;

top: -24px; // 悬浮在消息上方

right: 8px;

background: white;

border-radius: 16px;

box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);

display: flex;

gap: 4px; // 按钮间距

padding: 2px;

}

/* 消息状态指示器(发送/已读等) */

.message-status {

display: flex;

align-items: center;

font-size: 0.75rem; // 小号字体

}

/* 文件附件样式 */

.file-attachment {

display: flex;

align-items: center;

margin-top: 4px;

padding: 4px 8px;

background: rgba(0, 0, 0, 0.05);

border-radius: 4px;

gap: 8px;

.file-name { font-weight: 500; }

.file-size { color: #666; font-size: 0.8em; }

}

/* 回复预览样式 */

.reply-preview {

margin-top: 8px;

border-left: 3px solid $primary; // 左侧高亮边框

}

/* 输入区域样式 */

.chat-input {

border-top: 1px solid #e0e0e0; // 顶部分隔线

padding-top: 16px;

}

/* 文本截断 */

.text-ellipsis {

white-space: nowrap;

overflow: hidden;

text-overflow: ellipsis;

}

</style>

五、使用示例(父组件集成)

5.1 父组件代码

<template>

<div class="q-pa-md" style="height: 100vh;">

<chat-messages

:initial-messages="messages"

:current-user="currentUser"

:is-connected="isConnected"

:online-users="onlineUsersCount"

@send-message="handleSendMessage"

@delete-message="handleDeleteMessage"

@reply-to-message="handleReplyToMessage"

@forward-message="handleForwardMessage"

/>

</div>

</template>

<script setup lang="ts">

import { ref, onMounted } from 'vue';

import ChatMessages from './components/ChatMessages.vue'; // 导入聊天组件

import { ChatMessage, User } from './types/chat'; // 导入类型定义

// 1. 模拟数据

// 当前用户

const currentUser = ref<User>({

id: 'user-1',

name: '张三',

avatar: 'https://cdn.quasar.dev/img/avatar.png' // Quasar默认头像

});

// 消息列表

const messages = ref<ChatMessage[]>([]);

// 连接状态

const isConnected = ref(true);

// 在线用户数

const onlineUsersCount = ref(5);

// 2. 初始化加载历史消息

onMounted(() => {

// 模拟API调用加载历史消息

setTimeout(() => {

messages.value = [

{

id: 'msg-1',

content: '你好,这个项目进展如何?',

timestamp: new Date(Date.now() - 1000 * 60 * 10), // 10分钟前

sender: {

id: 'user-2',

name: '李四',

avatar: 'https://cdn.quasar.dev/img/avatar2.png'

},

status: 'read',

type: 'text'

},

{

id: 'msg-2',

content: '一切顺利,我已经完成了前端部分。',

timestamp: new Date(Date.now() - 1000 * 60 * 5), // 5分钟前

sender: currentUser.value,

status: 'read',

type: 'text'

},

{

id: 'msg-3',

content: '这是设计稿最终版,请查收。',

timestamp: new Date(Date.now() - 1000 * 60 * 3), // 3分钟前

sender: {

id: 'user-2',

name: '李四',

avatar: 'https://cdn.quasar.dev/img/avatar2.png'

},

status: 'delivered',

type: 'file',

fileName: 'final-design.pdf',

fileSize: 2048000 // 2MB

}

];

}, 500);

});

// 3. 事件处理

// 处理发送消息

const handleSendMessage = (messageData: Partial<ChatMessage>): void => {

// 生成新消息(模拟后端返回ID)

const newMessage: ChatMessage = {

id: `msg-${Date.now()}`,

content: messageData.content || '',

timestamp: new Date(),

sender: currentUser.value,

status: 'sending', // 初始状态为发送中

type: messageData.type || 'text',

...messageData

};

messages.value.push(newMessage);

// 模拟消息发送成功(后端确认)

setTimeout(() => {

const index = messages.value.findIndex(m => m.id === newMessage.id);

if (index !== -1) {

messages.value[index].status = 'sent'; // 更新为已发送

}

// 模拟对方回复

setTimeout(() => {

messages.value.push({

id: `msg-${Date.now()}`,

content: '太好了!后端API也快完成了。',

timestamp: new Date(),

sender: {

id: 'user-2',

name: '李四',

avatar: 'https://cdn.quasar.dev/img/avatar2.png'

},

status: 'delivered',

type: 'text'

});

}, 2000);

}, 1000);

};

// 处理删除消息

const handleDeleteMessage = (messageId: string): void => {

messages.value = messages.value.filter(msg => msg.id !== messageId);

};

// 处理回复消息

const handleReplyToMessage = (targetMessage: ChatMessage, replyContent: string): void => {

console.log(`回复消息 [${targetMessage.id}]:`, replyContent);

// 实际项目中可调用API发送带引用的回复

};

// 处理转发消息

const handleForwardMessage = (message: ChatMessage): void => {

console.log('转发消息:', message);

// 实际项目中可打开转发对话框选择接收人

};

</script>

六、组件接口说明

6.1 Props(输入参数)

参数名

类型

默认值

说明

initialMessages

ChatMessage[]

[]

初始消息列表

currentUser

User

-

当前用户信息(必填,用于判断消息发送方)

isConnected

boolean

false

连接状态(在线/离线,控制输入框禁用)

onlineUsers

number

0

在线用户数(显示在标题栏)

6.2 Emits(输出事件)

事件名

参数类型

说明

send-message

Partial<ChatMessage>

发送文本消息时触发,包含消息内容等

delete-message

string(messageId)

删除消息时触发,传递消息ID

reply-to-message

ChatMessage(target),string(reply)

回复消息时触发,传递目标消息和回复内容

forward-message

ChatMessage

转发消息时触发,传递被转发消息

七、扩展建议

  • 消息搜索:添加按内容/发送者/时间的搜索功能,结合 Quasar QInput 和过滤器实现
  • 表情支持:集成表情选择器(如q-emoji-picker),支持消息中插入表情
  • 消息引用:回复时显示完整引用消息内容,而非仅显示摘要
  • 已读回执:群聊场景下显示“已读人员列表”,需后端配合维护消息已读状态
  • 消息撤回:支持2分钟内撤回已发送消息,通过状态字段控制消息显示/隐藏

八、总结

QChatMessage 组件通过组合式 API 逻辑拆分TypeScript 类型安全企业级交互设计,提供了开箱即用的聊天消息解决方案。核心优势在于:

  • 智能分组:按日期和时间间隔自动组织消息,提升可读性
  • 丰富交互:覆盖回复、转发、删除等高频操作,支持附件上传
  • 状态反馈:实时显示消息发送/已读状态,优化用户体验

通过扩展建议中的功能,可进一步满足复杂企业聊天场景需求。

 

posted on 2025-09-03 22:34  GoGrid  阅读(13)  评论(0)    收藏  举报

导航