uni-app 消息系统
核心知识点
1. 消息系统架构
一个完整的消息系统通常包括以下几个核心部分:
- 消息发送模块:负责消息的创建和发送
- 消息存储模块:负责消息的存储和管理
- 消息接收模块:负责消息的接收和处理
- 消息展示模块:负责消息的展示和交互
- 消息状态管理:负责消息状态的更新和同步
2. 消息类型
在应用中,我们可以定义多种类型的消息:
- 系统消息:由系统发送的通知,如活动通知、系统更新等
- 用户消息:用户之间的私信或对话
- 业务消息:与业务相关的消息,如订单状态更新、支付通知等
- 通知消息:应用内的通知,如点赞、评论、关注等
- 广告消息:推广或营销相关的消息
3. 消息状态
消息通常有以下几种状态:
- 未读:消息已发送但未被阅读
- 已读:消息已被阅读
- 已删除:消息已被删除
- 已撤回:消息已被发送者撤回
- 已过期:消息超过了有效期
4. 消息发送机制
消息发送机制通常包括以下几种方式:
- 即时发送:消息立即发送并传递给接收者
- 延时发送:消息在指定时间后发送
- 批量发送:一次性发送消息给多个接收者
- 定时发送:按照预设的时间发送消息
5. 消息存储策略
消息存储策略通常包括以下几种:
- 本地存储:将消息存储在本地数据库中
- 服务器存储:将消息存储在服务器数据库中
- 混合存储:结合本地存储和服务器存储的优势
- 缓存策略:对消息进行缓存,提高读取速度
- 清理策略:定期清理过期或无用的消息
实用案例
案例:实现一个完整的站内信系统
1. 项目结构
src/
├── components/
│ ├── message-list.vue # 消息列表组件
│ ├── message-item.vue # 消息项组件
│ └── message-editor.vue # 消息编辑器组件
├── pages/
│ ├── message/
│ │ ├── index.vue # 消息中心页面
│ │ ├── detail.vue # 消息详情页面
│ │ └── chat.vue # 聊天页面
│ └── admin/
│ └── message-manage.vue # 消息管理后台
├── services/
│ └── message-api.js # 消息API服务
├── utils/
│ └── message-utils.js # 消息工具函数
└── store/
└── modules/
└── message.js # 消息状态管理2. 消息数据模型
// 消息数据模型
const messageModel = {
id: String, // 消息ID
senderId: String, // 发送者ID
senderName: String, // 发送者名称
receiverId: String, // 接收者ID
receiverName: String, // 接收者名称
type: String, // 消息类型:system, user, business, notification, ad
content: String, // 消息内容
status: String, // 消息状态:unread, read, deleted, recalled, expired
createTime: Date, // 创建时间
readTime: Date, // 阅读时间
expireTime: Date, // 过期时间
relatedId: String, // 相关业务ID,如订单ID、商品ID等
relatedType: String, // 相关业务类型
attachments: Array, // 附件列表
isSystem: Boolean, // 是否系统消息
isBroadcast: Boolean, // 是否广播消息
};3. 消息API服务
// services/message-api.js
// 获取消息列表
export const getMessageList = async (params) => {
const { page = 1, pageSize = 20, type, status } = params;
try {
const res = await uni.request({
url: 'https://api.example.com/message/list',
method: 'GET',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
data: {
page,
pageSize,
type,
status
}
});
return res.data;
} catch (error) {
console.error('获取消息列表失败:', error);
throw error;
}
};
// 获取消息详情
export const getMessageDetail = async (messageId) => {
try {
const res = await uni.request({
url: `https://api.example.com/message/${messageId}`,
method: 'GET',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
}
});
return res.data;
} catch (error) {
console.error('获取消息详情失败:', error);
throw error;
}
};
// 发送消息
export const sendMessage = async (data) => {
try {
const res = await uni.request({
url: 'https://api.example.com/message/send',
method: 'POST',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
data
});
return res.data;
} catch (error) {
console.error('发送消息失败:', error);
throw error;
}
};
// 标记消息为已读
export const markAsRead = async (messageId) => {
try {
const res = await uni.request({
url: `https://api.example.com/message/${messageId}/read`,
method: 'PUT',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
}
});
return res.data;
} catch (error) {
console.error('标记消息已读失败:', error);
throw error;
}
};
// 标记所有消息为已读
export const markAllAsRead = async (type) => {
try {
const res = await uni.request({
url: 'https://api.example.com/message/read-all',
method: 'PUT',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
data: {
type
}
});
return res.data;
} catch (error) {
console.error('标记所有消息已读失败:', error);
throw error;
}
};
// 删除消息
export const deleteMessage = async (messageId) => {
try {
const res = await uni.request({
url: `https://api.example.com/message/${messageId}`,
method: 'DELETE',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
}
});
return res.data;
} catch (error) {
console.error('删除消息失败:', error);
throw error;
}
};
// 批量删除消息
export const deleteMessages = async (messageIds) => {
try {
const res = await uni.request({
url: 'https://api.example.com/message/batch-delete',
method: 'DELETE',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
data: {
messageIds
}
});
return res.data;
} catch (error) {
console.error('批量删除消息失败:', error);
throw error;
}
};
// 撤回消息
export const recallMessage = async (messageId) => {
try {
const res = await uni.request({
url: `https://api.example.com/message/${messageId}/recall`,
method: 'PUT',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
}
});
return res.data;
} catch (error) {
console.error('撤回消息失败:', error);
throw error;
}
};
// 获取未读消息数量
export const getUnreadCount = async (type) => {
try {
const res = await uni.request({
url: 'https://api.example.com/message/unread-count',
method: 'GET',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
data: {
type
}
});
return res.data;
} catch (error) {
console.error('获取未读消息数量失败:', error);
throw error;
}
};
// 发送系统消息(管理员权限)
export const sendSystemMessage = async (data) => {
try {
const res = await uni.request({
url: 'https://api.example.com/message/system/send',
method: 'POST',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
data
});
return res.data;
} catch (error) {
console.error('发送系统消息失败:', error);
throw error;
}
};
// 发送广播消息(管理员权限)
export const sendBroadcastMessage = async (data) => {
try {
const res = await uni.request({
url: 'https://api.example.com/message/broadcast/send',
method: 'POST',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
data
});
return res.data;
} catch (error) {
console.error('发送广播消息失败:', error);
throw error;
}
};4. 消息工具函数
// utils/message-utils.js
// 格式化消息时间
export const formatMessageTime = (time) => {
const now = new Date();
const messageTime = new Date(time);
const diff = now - messageTime;
// 小于1分钟
if (diff < 60 * 1000) {
return '刚刚';
}
// 小于1小时
if (diff < 60 * 60 * 1000) {
return `${Math.floor(diff / (60 * 1000))}分钟前`;
}
// 小于24小时
if (diff < 24 * 60 * 60 * 1000) {
return `${Math.floor(diff / (60 * 60 * 1000))}小时前`;
}
// 小于7天
if (diff < 7 * 24 * 60 * 60 * 1000) {
return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`;
}
// 其他时间
return messageTime.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
};
// 获取消息类型图标
export const getMessageTypeIcon = (type) => {
const iconMap = {
system: '📢',
user: '💬',
business: '📋',
notification: '🔔',
ad: '📣'
};
return iconMap[type] || '📄';
};
// 获取消息类型名称
export const getMessageTypeName = (type) => {
const typeMap = {
system: '系统消息',
user: '私信',
business: '业务消息',
notification: '通知',
ad: '广告'
};
return typeMap[type] || '消息';
};
// 获取消息状态名称
export const getMessageStatusName = (status) => {
const statusMap = {
unread: '未读',
read: '已读',
deleted: '已删除',
recalled: '已撤回',
expired: '已过期'
};
return statusMap[status] || '未知';
};
// 检查消息是否为未读
export const isUnreadMessage = (status) => {
return status === 'unread';
};
// 检查消息是否可以撤回
export const canRecallMessage = (message, currentUserId) => {
// 只有发送者可以撤回消息,且消息发送时间不超过2分钟
if (message.senderId !== currentUserId) {
return false;
}
const now = new Date();
const sendTime = new Date(message.createTime);
const diff = now - sendTime;
return diff < 2 * 60 * 1000; // 2分钟内可以撤回
};
// 生成消息预览
export const generateMessagePreview = (content, maxLength = 20) => {
if (!content) return '';
// 移除HTML标签
const plainText = content.replace(/<[^>]+>/g, '');
// 截取预览文本
if (plainText.length <= maxLength) {
return plainText;
}
return plainText.substring(0, maxLength) + '...';
};
// 本地消息存储
export const saveMessageToLocal = (message) => {
try {
const messages = uni.getStorageSync('localMessages') || [];
messages.unshift(message);
// 限制本地存储的消息数量
if (messages.length > 100) {
messages.splice(100);
}
uni.setStorageSync('localMessages', messages);
return true;
} catch (error) {
console.error('保存消息到本地失败:', error);
return false;
}
};
// 从本地获取消息
export const getMessagesFromLocal = () => {
try {
return uni.getStorageSync('localMessages') || [];
} catch (error) {
console.error('从本地获取消息失败:', error);
return [];
}
};
// 清空本地消息
export const clearLocalMessages = () => {
try {
uni.removeStorageSync('localMessages');
return true;
} catch (error) {
console.error('清空本地消息失败:', error);
return false;
}
};5. 消息状态管理
// store/modules/message.js
import { getMessageList, getUnreadCount, markAsRead, deleteMessage } from '@/services/message-api';
const state = {
messages: [],
unreadCount: 0,
loading: false,
error: null,
currentPage: 1,
hasMore: true,
refreshing: false
};
const mutations = {
SET_MESSAGES(state, messages) {
state.messages = messages;
},
ADD_MESSAGES(state, messages) {
state.messages = [...state.messages, ...messages];
},
UPDATE_MESSAGE(state, updatedMessage) {
const index = state.messages.findIndex(msg => msg.id === updatedMessage.id);
if (index !== -1) {
state.messages.splice(index, 1, updatedMessage);
}
},
REMOVE_MESSAGE(state, messageId) {
state.messages = state.messages.filter(msg => msg.id !== messageId);
},
SET_UNREAD_COUNT(state, count) {
state.unreadCount = count;
},
SET_LOADING(state, loading) {
state.loading = loading;
},
SET_ERROR(state, error) {
state.error = error;
},
SET_CURRENT_PAGE(state, page) {
state.currentPage = page;
},
SET_HAS_MORE(state, hasMore) {
state.hasMore = hasMore;
},
SET_REFRESHING(state, refreshing) {
state.refreshing = refreshing;
},
RESET_STATE(state) {
state.messages = [];
state.currentPage = 1;
state.hasMore = true;
}
};
const actions = {
async fetchMessages({ commit, state }, params) {
commit('SET_LOADING', true);
commit('SET_ERROR', null);
try {
const result = await getMessageList({
page: state.currentPage,
pageSize: 20,
...params
});
if (result.code === 0) {
const messages = result.data.list || [];
if (state.currentPage === 1) {
commit('SET_MESSAGES', messages);
} else {
commit('ADD_MESSAGES', messages);
}
commit('SET_HAS_MORE', messages.length === 20);
commit('SET_CURRENT_PAGE', state.currentPage + 1);
} else {
commit('SET_ERROR', result.message);
}
} catch (error) {
commit('SET_ERROR', error.message);
} finally {
commit('SET_LOADING', false);
commit('SET_REFRESHING', false);
}
},
async refreshMessages({ commit, dispatch }, params) {
commit('RESET_STATE');
commit('SET_REFRESHING', true);
await dispatch('fetchMessages', params);
},
async fetchUnreadCount({ commit }, type) {
try {
const result = await getUnreadCount(type);
if (result.code === 0) {
commit('SET_UNREAD_COUNT', result.data.count || 0);
}
} catch (error) {
console.error('获取未读消息数量失败:', error);
}
},
async markMessageAsRead({ commit }, messageId) {
try {
const result = await markAsRead(messageId);
if (result.code === 0) {
commit('UPDATE_MESSAGE', {
id: messageId,
status: 'read',
readTime: new Date()
});
// 更新未读消息数量
commit('SET_UNREAD_COUNT', state => state.unreadCount - 1);
}
return result;
} catch (error) {
console.error('标记消息已读失败:', error);
throw error;
}
},
async deleteMessage({ commit }, messageId) {
try {
const result = await deleteMessage(messageId);
if (result.code === 0) {
commit('REMOVE_MESSAGE', messageId);
}
return result;
} catch (error) {
console.error('删除消息失败:', error);
throw error;
}
},
addMessage({ commit }, message) {
commit('ADD_MESSAGES', [message]);
if (message.status === 'unread') {
commit('SET_UNREAD_COUNT', state => state.unreadCount + 1);
}
}
};
const getters = {
messages: state => state.messages,
unreadCount: state => state.unreadCount,
loading: state => state.loading,
error: state => state.error,
hasMore: state => state.hasMore,
refreshing: state => state.refreshing,
unreadMessages: state => state.messages.filter(msg => msg.status === 'unread'),
systemMessages: state => state.messages.filter(msg => msg.type === 'system'),
userMessages: state => state.messages.filter(msg => msg.type === 'user'),
businessMessages: state => state.messages.filter(msg => msg.type === 'business'),
notificationMessages: state => state.messages.filter(msg => msg.type === 'notification'),
adMessages: state => state.messages.filter(msg => msg.type === 'ad')
};
export default {
namespaced: true,
state,
mutations,
actions,
getters
};6. 消息列表组件
<!-- components/message-list.vue -->
<template>
<view class="message-list">
<message-item
v-for="message in messages"
:key="message.id"
:message="message"
@click="handleMessageClick(message)"
@mark-read="handleMarkRead(message.id)"
@delete="handleDeleteMessage(message.id)"
/>
<view v-if="loading && messages.length === 0" class="loading">
加载中...
</view>
<view v-if="!loading && messages.length === 0" class="empty">
暂无消息
</view>
<view v-if="loading && messages.length > 0" class="loading-more">
加载更多...
</view>
<view v-if="!loading && !hasMore && messages.length > 0" class="no-more">
没有更多消息了
</view>
</view>
</template>
<script>
import MessageItem from './message-item';
export default {
name: 'MessageList',
components: {
MessageItem
},
props: {
messages: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
},
hasMore: {
type: Boolean,
default: true
}
},
methods: {
handleMessageClick(message) {
this.$emit('message-click', message);
},
handleMarkRead(messageId) {
this.$emit('mark-read', messageId);
},
handleDeleteMessage(messageId) {
this.$emit('delete', messageId);
}
}
};
</script>
<style scoped>
.message-list {
padding: 16rpx;
}
.loading,
.empty,
.loading-more,
.no-more {
text-align: center;
padding: 32rpx 0;
color: #999;
font-size: 24rpx;
}
</style>7. 消息项组件
<!-- components/message-item.vue -->
<template>
<view class="message-item" :class="{ 'unread': isUnread }">
<view class="message-icon">
{{ messageTypeIcon }}
</view>
<view class="message-content">
<view class="message-header">
<text class="message-sender">{{ message.senderName || '系统' }}</text>
<text class="message-time">{{ formattedTime }}</text>
<view v-if="isUnread" class="unread-badge"></view>
</view>
<text class="message-text">{{ messagePreview }}</text>
<view v-if="message.type === 'user'" class="message-footer">
<text class="message-type">{{ messageType }}</text>
</view>
</view>
<view class="message-actions">
<text
v-if="isUnread"
class="action-btn read-btn"
@click.stop="handleMarkRead"
>
标为已读
</text>
<text class="action-btn delete-btn" @click.stop="handleDelete">
删除
</text>
</view>
</view>
</template>
<script>
import {
formatMessageTime,
getMessageTypeIcon,
getMessageTypeName,
isUnreadMessage,
generateMessagePreview
} from '@/utils/message-utils';
export default {
name: 'MessageItem',
props: {
message: {
type: Object,
required: true
}
},
computed: {
formattedTime() {
return formatMessageTime(this.message.createTime);
},
messageTypeIcon() {
return getMessageTypeIcon(this.message.type);
},
messageType() {
return getMessageTypeName(this.message.type);
},
isUnread() {
return isUnreadMessage(this.message.status);
},
messagePreview() {
return generateMessagePreview(this.message.content);
}
},
methods: {
handleMarkRead() {
this.$emit('mark-read', this.message.id);
},
handleDelete() {
uni.showModal({
title: '确认删除',
content: '确定要删除这条消息吗?',
success: (res) => {
if (res.confirm) {
this.$emit('delete', this.message.id);
}
}
});
}
}
};
</script>
<style scoped>
.message-item {
display: flex;
padding: 16rpx;
background-color: white;
border-radius: 8rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.message-item.unread {
background-color: #f5f9ff;
}
.message-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.message-content {
flex: 1;
min-width: 0;
}
.message-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
position: relative;
}
.message-sender {
font-size: 28rpx;
font-weight: bold;
margin-right: 16rpx;
flex-shrink: 0;
}
.message-time {
font-size: 22rpx;
color: #999;
flex-shrink: 0;
}
.unread-badge {
position: absolute;
right: 0;
top: 0;
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: #ff3b30;
}
.message-text {
font-size: 24rpx;
color: #666;
line-height: 1.4;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.message-footer {
display: flex;
align-items: center;
}
.message-type {
font-size: 20rpx;
color: #999;
background-color: #f0f0f0;
padding: 2rpx 12rpx;
border-radius: 12rpx;
}
.message-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-left: 16rpx;
flex-shrink: 0;
}
.action-btn {
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
margin-bottom: 8rpx;
}
.read-btn {
background-color: #e3f2fd;
color: #2196f3;
}
.delete-btn {
background-color: #ffebee;
color: #f44336;
}
</style>8. 消息中心页面
<!-- pages/message/index.vue -->
<template>
<view class="message-center">
<view class="header">
<text class="title">消息中心</text>
<view class="header-actions">
<text class="action-btn" @click="handleMarkAllRead">全部已读</text>
<text class="action-btn" @click="handleDeleteAll">全部删除</text>
</view>
</view>
<view class="tab-bar">
<view
v-for="tab in tabs"
:key="tab.type"
class="tab-item"
:class="{ 'active': activeTab === tab.type }"
@click="switchTab(tab.type)"
>
<text class="tab-text">{{ tab.name }}</text>
<view
v-if="tab.badge && getUnreadCount(tab.type) > 0"
class="tab-badge"
>
{{ getUnreadCount(tab.type) > 99 ? '99+' : getUnreadCount(tab.type) }}
</view>
</view>
</view>
<scroll-view
class="message-scroll"
scroll-y
@scrolltolower="handleLoadMore"
refresher-enabled
@refresherpulling="handleRefresh"
@refresherrefresh="handleRefresh"
>
<message-list
:messages="filteredMessages"
:loading="loading"
:has-more="hasMore"
@message-click="handleMessageClick"
@mark-read="handleMarkRead"
@delete="handleDeleteMessage"
/>
</scroll-view>
</view>
</template>
<script>
import MessageList from '@/components/message-list';
import { mapState, mapGetters, mapActions } from 'vuex';
import { markAllAsRead, deleteMessages } from '@/services/message-api';
export default {
name: 'MessageCenter',
components: {
MessageList
},
data() {
return {
activeTab: 'all',
tabs: [
{ type: 'all', name: '全部', badge: true },
{ type: 'system', name: '系统', badge: true },
{ type: 'user', name: '私信', badge: true },
{ type: 'business', name: '业务', badge: true },
{ type: 'notification', name: '通知', badge: true }
]
};
},
computed: {
...mapState('message', ['loading', 'hasMore']),
...mapGetters('message', [
'messages',
'unreadMessages',
'systemMessages',
'userMessages',
'businessMessages',
'notificationMessages'
]),
filteredMessages() {
switch (this.activeTab) {
case 'system':
return this.systemMessages;
case 'user':
return this.userMessages;
case 'business':
return this.businessMessages;
case 'notification':
return this.notificationMessages;
default:
return this.messages;
}
}
},
onLoad() {
this.fetchMessages();
this.fetchUnreadCount();
},
methods: {
...mapActions('message', ['fetchMessages', 'refreshMessages', 'markMessageAsRead', 'deleteMessage', 'fetchUnreadCount']),
switchTab(type) {
this.activeTab = type;
this.refreshMessages({ type: type === 'all' ? '' : type });
},
handleRefresh() {
this.refreshMessages({ type: this.activeTab === 'all' ? '' : this.activeTab });
},
handleLoadMore() {
if (this.hasMore && !this.loading) {
this.fetchMessages({ type: this.activeTab === 'all' ? '' : this.activeTab });
}
},
handleMessageClick(message) {
// 标记消息为已读
if (message.status === 'unread') {
this.markMessageAsRead(message.id);
}
// 根据消息类型跳转到不同页面
if (message.type === 'user') {
uni.navigateTo({
url: `/pages/message/chat?userId=${message.senderId}&userName=${message.senderName}`
});
} else {
uni.navigateTo({
url: `/pages/message/detail?id=${message.id}`
});
}
},
handleMarkRead(messageId) {
this.markMessageAsRead(messageId);
},
handleDeleteMessage(messageId) {
this.deleteMessage(messageId);
},
async handleMarkAllRead() {
try {
await markAllAsRead(this.activeTab === 'all' ? '' : this.activeTab);
uni.showToast({ title: '已全部标记为已读', icon: 'success' });
// 刷新消息列表
this.refreshMessages({ type: this.activeTab === 'all' ? '' : this.activeTab });
} catch (error) {
uni.showToast({ title: '操作失败', icon: 'none' });
}
},
async handleDeleteAll() {
uni.showModal({
title: '确认删除',
content: '确定要删除所有消息吗?',
success: async (res) => {
if (res.confirm) {
try {
const messageIds = this.filteredMessages.map(msg => msg.id);
if (messageIds.length > 0) {
await deleteMessages(messageIds);
uni.showToast({ title: '已全部删除', icon: 'success' });
// 刷新消息列表
this.refreshMessages({ type: this.activeTab === 'all' ? '' : this.activeTab });
} else {
uni.showToast({ title: '没有消息可删除', icon: 'none' });
}
} catch (error) {
uni.showToast({ title: '操作失败', icon: 'none' });
}
}
}
});
},
getUnreadCount(type) {
switch (type) {
case 'system':
return this.systemMessages.filter(msg => msg.status === 'unread').length;
case 'user':
return this.userMessages.filter(msg => msg.status === 'unread').length;
case 'business':
return this.businessMessages.filter(msg => msg.status === 'unread').length;
case 'notification':
return this.notificationMessages.filter(msg => msg.status === 'unread').length;
default:
return this.unreadMessages.length;
}
}
}
};
</script>
<style scoped>
.message-center {
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 32rpx;
background-color: white;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.title {
font-size: 32rpx;
font-weight: bold;
}
.header-actions {
display: flex;
}
.action-btn {
font-size: 24rpx;
color: #007AFF;
margin-left: 24rpx;
}
.tab-bar {
display: flex;
background-color: white;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
overflow-x: auto;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 0;
position: relative;
min-width: 120rpx;
}
.tab-item.active {
color: #007AFF;
border-bottom: 4rpx solid #007AFF;
}
.tab-text {
font-size: 24rpx;
}
.tab-badge {
position: absolute;
top: 12rpx;
right: 20rpx;
background-color: #ff3b30;
color: white;
font-size: 16rpx;
padding: 2rpx 8rpx;
border-radius: 12rpx;
min-width: 24rpx;
text-align: center;
}
.message-scroll {
flex: 1;
height: calc(100vh - 200rpx);
}
</style>9. 消息详情页面
<!-- pages/message/detail.vue -->
<template>
<view class="message-detail">
<view class="header">
<text class="back-btn" @click="handleBack">←</text>
<text class="title">消息详情</text>
</view>
<view class="message-content" v-if="message">
<view class="message-header">
<view class="sender-info">
<view class="sender-icon">{{ messageTypeIcon }}</view>
<view class="sender-details">
<text class="sender-name">{{ message.senderName || '系统' }}</text>
<text class="send-time">{{ formatDateTime(message.createTime) }}</text>
</view>
</view>
<view class="message-type-tag">{{ messageType }}</view>
</view>
<view class="message-body">
<rich-text :nodes="message.content"></rich-text>
</view>
<view v-if="message.relatedId" class="message-footer">
<view class="related-info">
<text class="related-label">相关内容:</text>
<text class="related-value">{{ message.relatedType }}</text>
</view>
<button class="related-btn" @click="handleRelatedClick">查看详情</button>
</view>
</view>
<view v-else class="loading">加载中...</view>
</view>
</template>
<script>
import { getMessageDetail } from '@/services/message-api';
import {
getMessageTypeIcon,
getMessageTypeName
} from '@/utils/message-utils';
export default {
name: 'MessageDetail',
data() {
return {
message: null,
loading: true
};
},
onLoad(options) {
this.messageId = options.id;
this.fetchMessageDetail();
},
computed: {
messageTypeIcon() {
if (!this.message) return '';
return getMessageTypeIcon(this.message.type);
},
messageType() {
if (!this.message) return '';
return getMessageTypeName(this.message.type);
}
},
methods: {
async fetchMessageDetail() {
this.loading = true;
try {
const result = await getMessageDetail(this.messageId);
if (result.code === 0) {
this.message = result.data;
} else {
uni.showToast({ title: '获取消息详情失败', icon: 'none' });
}
} catch (error) {
uni.showToast({ title: '获取消息详情失败', icon: 'none' });
} finally {
this.loading = false;
}
},
handleBack() {
uni.navigateBack();
},
handleRelatedClick() {
// 根据相关类型跳转到不同页面
if (this.message.relatedType === 'order') {
uni.navigateTo({
url: `/pages/order/detail?id=${this.message.relatedId}`
});
} else if (this.message.relatedType === 'product') {
uni.navigateTo({
url: `/pages/product/detail?id=${this.message.relatedId}`
});
} else if (this.message.relatedType === 'activity') {
uni.navigateTo({
url: `/pages/activity/detail?id=${this.message.relatedId}`
});
}
},
formatDateTime(time) {
const date = new Date(time);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
}
};
</script>
<style scoped>
.message-detail {
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
display: flex;
align-items: center;
padding: 16rpx 32rpx;
background-color: white;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.back-btn {
font-size: 32rpx;
margin-right: 24rpx;
}
.title {
font-size: 32rpx;
font-weight: bold;
}
.message-content {
background-color: white;
margin: 16rpx;
border-radius: 8rpx;
padding: 24rpx;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.message-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24rpx;
padding-bottom: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.sender-info {
display: flex;
align-items: center;
}
.sender-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
margin-right: 16rpx;
}
.sender-details {
flex: 1;
}
.sender-name {
display: block;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.send-time {
font-size: 20rpx;
color: #999;
}
.message-type-tag {
background-color: #f0f0f0;
padding: 4rpx 16rpx;
border-radius: 16rpx;
font-size: 20rpx;
color: #666;
}
.message-body {
margin-bottom: 24rpx;
line-height: 1.6;
}
.message-body :deep() {
font-size: 28rpx;
line-height: 1.6;
}
.message-body :deep(img) {
max-width: 100%;
height: auto;
margin: 16rpx 0;
border-radius: 8rpx;
}
.message-footer {
padding-top: 24rpx;
border-top: 1rpx solid #f0f0f0;
}
.related-info {
margin-bottom: 16rpx;
}
.related-label {
font-size: 24rpx;
color: #666;
}
.related-value {
font-size: 24rpx;
color: #333;
font-weight: bold;
}
.related-btn {
width: 100%;
padding: 16rpx;
background-color: #007AFF;
color: white;
border-radius: 8rpx;
font-size: 24rpx;
}
.loading {
text-align: center;
padding: 64rpx 0;
color: #999;
}
</style>10. 聊天页面
<!-- pages/message/chat.vue -->
<template>
<view class="chat-page">
<view class="header">
<text class="back-btn" @click="handleBack">←</text>
<text class="title">{{ userName }}</text>
<view class="header-actions">
<text class="action-btn">更多</text>
</view>
</view>
<scroll-view
class="message-list"
scroll-y
:scroll-top="scrollTop"
@scrolltolower="handleLoadMore"
@scroll="handleScroll"
>
<view
v-for="(message, index) in messages"
:key="message.id"
class="message-wrapper"
:class="{ 'own': message.senderId === currentUserId }"
>
<view class="message-content">
<view class="message-bubble" :class="{ 'own': message.senderId === currentUserId }">
<text class="message-text">{{ message.content }}</text>
</view>
<text class="message-time">{{ formatMessageTime(message.createTime) }}</text>
</view>
</view>
<view v-if="loading" class="loading">加载中...</view>
</scroll-view>
<view class="input-area">
<input
v-model="inputContent"
class="input"
placeholder="输入消息..."
@keyup.enter="handleSend"
/>
<button class="send-btn" @click="handleSend">发送</button>
</view>
</view>
</template>
<script>
import { sendMessage, getMessageList } from '@/services/message-api';
import { formatMessageTime } from '@/utils/message-utils';
import { getCurrentUser } from '@/utils/auth';
export default {
name: 'ChatPage',
data() {
return {
userId: '',
userName: '',
currentUserId: '',
messages: [],
inputContent: '',
loading: false,
scrollTop: 0,
hasMore: true,
currentPage: 1
};
},
onLoad(options) {
this.userId = options.userId;
this.userName = options.userName || '用户';
const currentUser = getCurrentUser();
this.currentUserId = currentUser?.id || '';
this.fetchMessages();
},
methods: {
async fetchMessages() {
if (!this.hasMore || this.loading) return;
this.loading = true;
try {
const result = await getMessageList({
page: this.currentPage,
pageSize: 20,
type: 'user',
receiverId: this.userId
});
if (result.code === 0) {
const newMessages = result.data.list || [];
this.messages = [...newMessages, ...this.messages];
this.hasMore = newMessages.length === 20;
this.currentPage++;
}
} catch (error) {
console.error('获取消息失败:', error);
} finally {
this.loading = false;
}
},
async handleSend() {
if (!this.inputContent.trim()) return;
const content = this.inputContent.trim();
this.inputContent = '';
// 临时添加消息到列表
const tempMessage = {
id: `temp_${Date.now()}`,
senderId: this.currentUserId,
senderName: getCurrentUser()?.nickname || getCurrentUser()?.username || '我',
receiverId: this.userId,
receiverName: this.userName,
type: 'user',
content,
status: 'unread',
createTime: new Date().toISOString()
};
this.messages.push(tempMessage);
this.scrollToBottom();
// 发送消息
try {
const result = await sendMessage({
receiverId: this.userId,
receiverName: this.userName,
type: 'user',
content
});
if (result.code === 0) {
// 替换临时消息
const index = this.messages.findIndex(msg => msg.id === tempMessage.id);
if (index !== -1) {
this.messages.splice(index, 1, result.data);
}
} else {
uni.showToast({ title: '发送失败', icon: 'none' });
// 移除临时消息
const index = this.messages.findIndex(msg => msg.id === tempMessage.id);
if (index !== -1) {
this.messages.splice(index, 1);
}
}
} catch (error) {
uni.showToast({ title: '发送失败', icon: 'none' });
// 移除临时消息
const index = this.messages.findIndex(msg => msg.id === tempMessage.id);
if (index !== -1) {
this.messages.splice(index, 1);
}
}
},
handleLoadMore() {
this.fetchMessages();
},
handleScroll(e) {
this.scrollTop = e.detail.scrollTop;
},
scrollToBottom() {
setTimeout(() => {
uni.createSelectorQuery().select('.message-list').boundingClientRect(rect => {
this.scrollTop = rect.height;
}).exec();
}, 100);
},
formatMessageTime(time) {
return formatMessageTime(time);
},
handleBack() {
uni.navigateBack();
}
}
};
</script>
<style scoped>
.chat-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
display: flex;
align-items: center;
padding: 16rpx 32rpx;
background-color: white;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.back-btn {
font-size: 32rpx;
margin-right: 24rpx;
}
.title {
flex: 1;
font-size: 32rpx;
font-weight: bold;
text-align: center;
}
.header-actions {
margin-left: 24rpx;
}
.action-btn {
font-size: 24rpx;
color: #007AFF;
}
.message-list {
flex: 1;
height: calc(100vh - 200rpx);
padding: 16rpx;
}
.message-wrapper {
display: flex;
margin-bottom: 24rpx;
}
.message-wrapper.own {
justify-content: flex-end;
}
.message-content {
max-width: 70%;
}
.message-bubble {
padding: 16rpx;
border-radius: 16rpx;
margin-bottom: 8rpx;
background-color: white;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.message-bubble.own {
background-color: #007AFF;
color: white;
}
.message-text {
font-size: 24rpx;
line-height: 1.4;
}
.message-bubble.own .message-text {
color: white;
}
.message-time {
font-size: 18rpx;
color: #999;
text-align: center;
}
.input-area {
display: flex;
padding: 16rpx;
background-color: white;
box-shadow: 0 -2rpx 4rpx rgba(0, 0, 0, 0.1);
}
.input {
flex: 1;
padding: 16rpx;
border: 1rpx solid #ddd;
border-radius: 24rpx;
font-size: 24rpx;
margin-right: 16rpx;
}
.send-btn {
padding: 0 32rpx;
background-color: #007AFF;
color: white;
border-radius: 24rpx;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.loading {
text-align: center;
padding: 16rpx;
color: #999;
font-size: 20rpx;
}
</style>学习目标
通过本教程的学习,你应该能够:
理解消息系统的基本架构和核心概念
- 掌握消息系统的分层架构
- 了解不同类型的消息和状态
- 熟悉消息发送和存储机制
掌握 uni-app 中消息系统的实现方法
- 学会设计消息数据模型
- 掌握消息API的设计和实现
- 学会开发消息列表、详情等核心组件
- 了解消息状态管理的实现方法
实现完整的消息系统功能
- 掌握消息的发送和接收功能
- 了解消息的存储和管理
- 学会实现消息的已读、删除等操作
- 掌握聊天功能的实现方法
应用消息系统最佳实践
- 掌握消息推送和通知的实现
- 了解消息的缓存和清理策略
- 学会优化消息系统的性能
构建安全可靠的消息系统
- 掌握消息内容的安全处理
- 了解用户隐私的保护措施
- 学会防止消息系统的滥用
通过本教程的学习,你将能够在 uni-app 中构建功能完善、性能优化的消息系统,为应用提供完整的站内信和聊天功能。