基于鸿蒙OS开发附近社交游戏平台(二十七)-通知系统与消息中心
通知系统与消息中心
目录
- 通知系统概述
- NotificationItem数据模型
- NotificationType枚举
- 消息中心Tab页面
- 通知列表渲染
- 已读/未读状态管理
- 拉黑过滤通知
- 通知点击跳转
- MockNotificationData
1. 通知系统概述
NearPlay作为一款基于位置和兴趣的社交应用,通知系统是连接用户与社交动态的核心桥梁。当附近有新用户上线、有人报名参加活动、收到游戏邀请或系统发布重要公告时,通知系统负责将这些事件及时、有序地传达给用户,同时确保通知的展示不会对用户造成信息过载或打扰。
1.1 通知系统的设计目标
NearPlay通知系统的设计围绕三个核心目标展开:及时性、精准性和可控性。
及时性要求通知能够在事件发生后尽快到达用户,尤其是社交类通知(如游戏邀请、活动报名),延迟展示可能导致错过最佳互动时机。NearPlay当前版本采用页面内嵌的通知列表方式展示通知,用户切换到消息Tab时即可看到最新通知。未来版本可以集成HarmonyOS的ohos.permission.NOTIFICATION权限和wantAgent机制,实现系统级推送通知,让用户在不打开应用的情况下也能收到重要通知。
精准性要求通知内容准确反映事件的具体信息,而不是笼统的"你有一条新消息"。每条通知都包含触发者信息(头像、昵称)、事件类型(报名/邀请/系统)和具体内容(哪个活动、什么邀请),让用户无需点进去就能判断通知的重要程度和是否需要立即处理。
可控性要求用户能够自主管理通知的接收范围。NearPlay通过黑名单机制实现通知过滤——被拉黑用户触发的通知自动隐藏,不会出现在通知列表中。这种"推+拉"结合的模式既保证了通知的及时推送,又给了用户过滤噪音的权利。
1.2 通知的数据流向
NearPlay通知系统的数据流向遵循"数据源→过滤→展示"的管道模型:
- 数据源——MockNotifyData.getNotifications()生成原始通知列表,包含所有类型的通知数据
- 过滤——通过.filter()方法排除来自被拉黑用户的通知,确保用户不会看到不想看到的人的通知
- 展示——过滤后的通知列表通过ForEach渲染为UI组件,每条通知以NotifyItem的数据驱动显示
这种管道式设计使得数据处理的每个环节都职责清晰、可独立测试,新增过滤条件(如按类型过滤、按时间排序)只需要在管道中添加新的处理步骤即可。
1.3 通知在应用中的分布
NearPlay的通知数据在多个页面中被消费:
- Index.ets消息Tab——展示全局通知列表,包含所有类型的通知,已过滤拉黑用户
- ActivityDetail.ets——展示特定活动相关的通知(如报名通知),按activityId过滤
- 未来扩展——聊天消息、匹配成功通知等新型通知可以复用现有的数据模型和渲染逻辑
通知数据的共享通过直接调用MockNotifyData.getNotifications()实现。当前版本中,每次调用都返回新创建的通知数组,这意味着不同页面持有不同的通知实例,修改一处通知的isRead状态不会影响其他页面的展示。这种设计在Mock数据阶段是合理的,但在接入真实后端后,需要引入全局状态管理(如AppStorage或分布式数据管理)来同步通知状态。
2. NotificationItem数据模型
NotificationItem(在代码中命名为NotifyItem)是NearPlay通知系统的核心数据结构,定义在entry/src/main/ets/model/NotifyModel.ets中。它封装了一条通知所需的全部信息,包括来源、内容、类型和状态。
2.1 字段详解
export class NotifyItem {
id: string = ''
type: NotifyType = NotifyType.SIGNUP
title: string = ''
content: string = ''
fromUserId: string = ''
fromNickname: string = ''
fromAvatar: string = ''
activityId: string = ''
timestamp: number = 0
isRead: boolean = false
}
id字段——通知的唯一标识符,采用’n1’、'n2’等递增编号格式。id在ForEach中用作key,确保列表渲染的稳定性。每条通知的id必须全局唯一,重复的id会导致ForEach的key冲突,引发渲染异常。
type字段——通知类型,类型为NotifyType枚举。type决定了通知的展示风格、交互行为和跳转目标。例如,SIGNUP类型的通知点击后跳转到对应活动详情页,GAME_INVITE类型的通知可能触发游戏房间加入流程。type字段的默认值为NotifyType.SIGNUP,这是最常见的通知类型。
title字段——通知的短标题,如"新报名"、“游戏邀请”、“系统通知”。标题以14vp字号、Medium字重显示在通知卡片的左侧区域,是用户识别通知类型的第一视觉线索。不同类型的通知标题格式不同,但都保持简洁——通常不超过4个汉字。
content字段——通知的详细内容,描述了具体发生了什么。例如"小明 报名了你的活动’周末狼人杀聚会’"、“大壮 邀请你加入狼人杀”。content以12vp字号、灰色字体显示,单行截断(maxLines: 1,textOverflow: Ellipsis)。这种设计在保持信息密度的同时避免了长文本撑开列表布局。
fromUserId字段——通知触发者的用户ID。对于用户触发的通知(SIGNUP、GAME_INVITE),fromUserId标识了是谁触发了这条通知。对于系统通知(SYSTEM),fromUserId为空字符串。fromUserId的主要用途是黑名单过滤——通过与blockedUserIds数组进行includes检查,过滤掉被拉黑用户触发的通知。
fromNickname字段——通知触发者的昵称。在通知内容中被引用,如"小明 报名了你的活动"。fromNickname为空字符串表示系统通知。昵称的存在使得通知内容更加人性化,比显示用户ID更友好。
fromAvatar字段——通知触发者的头像,使用emoji字符。在通知列表中,fromAvatar以28vp字号显示在通知卡片的左侧。对于系统通知,fromAvatar为空字符串,此时显示默认的📢图标。这种条件渲染逻辑为:
Text(item.fromAvatar !== '' ? item.fromAvatar : '📢')
activityId字段——关联的活动ID。对于活动相关通知(如报名通知),activityId指向对应的活动,用于点击跳转时传递路由参数。对于非活动通知(如游戏邀请),activityId为空字符串,点击时不执行跳转逻辑。
timestamp字段——通知的时间戳,由Date.now()生成。当前版本未在UI中展示时间信息,但timestamp为未来的时间显示和排序功能提供了数据基础。当接入真实后端后,可以基于timestamp实现"刚刚"、“5分钟前”、"昨天"等相对时间显示。
isRead字段——通知的已读状态,布尔值,初始为false。未读通知在UI上有视觉区分:背景色为’#FFF8E1’(淡黄色),右侧显示8x8的红色圆点。已读通知背景为白色,无红点。isRead在用户点击通知时被设为true,但修改仅影响当前页面的实例,不会全局同步。
2.2 静态工厂方法of()
NotifyItem使用静态工厂方法创建实例,确保所有字段被正确初始化:
static of(id: string, type: NotifyType, title: string, content: string,
fromId: string, fromNick: string, fromAvatar: string, actId: string): NotifyItem {
const n = new NotifyItem()
n.id = id; n.type = type; n.title = title; n.content = content
n.fromUserId = fromId; n.fromNickname = fromNick; n.fromAvatar = fromAvatar
n.activityId = actId; n.timestamp = Date.now()
return n
}
工厂方法的参数包括id、type、title、content、fromId、fromNick、fromAvatar、actId,共8个参数。timestamp不在参数中,因为它总是使用创建时的当前时间。isRead也不在参数中,因为新通知总是未读的。
2.3 数据模型的扩展性
NotifyItem的字段设计为未来扩展预留了空间。可能的新增字段包括:
- imageUrl——通知附带的图片URL,用于富媒体通知展示
- actionUrl——通知的深层链接,替代当前的activityId实现更灵活的跳转
- priority——通知优先级,用于排序和静默策略
- expireTime——通知过期时间,过期通知自动清除
这些字段的添加不会破坏现有的代码结构,因为ArkTS类的字段有默认值,新增字段不需要修改已有的创建代码。
3. NotificationType枚举
NotifyType定义了NearPlay中通知的类型分类,每种类型对应不同的业务场景和交互逻辑。
3.1 枚举定义
export enum NotifyType {
SIGNUP = 0,
SYSTEM = 1,
GAME_INVITE = 2
}
3.2 SIGNUP——活动报名通知
SIGNUP(值为0)是活动报名通知类型。当其他用户报名了你发布的活动时,系统生成SIGNUP类型的通知。这是NearPlay中最常见的通知类型,因为活动社交是应用的核心功能之一。
SIGNUP通知的特征:
- fromUserId非空——总是由某个具体用户触发
- activityId非空——总是关联某个具体活动
- title固定为"新报名"
- content格式为"{昵称} 报名了你的活动’{活动名}'"
在UI中的展示效果:左侧显示报名者的头像emoji,中间显示"新报名"标题和报名详情,右侧显示未读红点。点击后跳转到对应活动的详情页,方便活动发起人查看报名者的完整信息和活动状态。
3.3 SYSTEM——系统通知
SYSTEM(值为1)是系统通知类型,由应用自身生成,不关联特定用户。系统通知用于向用户传达应用级别的信息,如新功能上线、附近用户动态、版本更新提醒等。
SYSTEM通知的特征:
- fromUserId为空——不是由用户触发
- fromNickname为空——无触发者昵称
- fromAvatar为空——显示默认的📢图标
- activityId通常为空——不关联具体活动
- content为系统生成的描述文字
在UI中的展示效果:左侧显示📢图标,中间显示"系统通知"标题和具体内容,无用户关联信息。点击后不执行跳转(activityId为空),仅标记为已读。
3.4 GAME_INVITE——游戏邀请通知
GAME_INVITE(值为2)是游戏邀请通知类型。当其他用户邀请你加入某个游戏时,系统生成GAME_INVITE类型的通知。游戏邀请是NearPlay社交互动的重要入口,用户通过邀请可以快速进入游戏房间与好友互动。
GAME_INVITE通知的特征:
- fromUserId非空——由邀请者触发
- activityId为空——游戏邀请不关联活动(游戏是即时性的,不像活动有固定的时空安排)
- title固定为"游戏邀请"
- content格式为"{昵称} 邀请你加入{游戏名}"
在UI中的展示效果与SIGNUP类似,左侧显示邀请者的头像,中间显示邀请信息。点击后的行为在当前版本中与SIGNUP相同(检查activityId后不跳转),未来版本可以实现直接进入游戏房间的跳转逻辑。
3.5 未来可能的通知类型
随着NearPlay功能的扩展,可能新增以下通知类型:
- CHAT——聊天消息通知,当收到私信时生成
- LIKE——点赞通知,当其他用户点赞你的运动计划或食谱时生成
- MATCH——匹配通知,当系统发现高匹配度的附近用户时生成
- LOCATION——位置变动通知,当关注的用户进入附近区域时生成
每种新类型的添加只需要在NotifyType枚举中新增一个值,并在通知点击跳转逻辑中添加对应的路由映射即可。枚举的扩展不影响已有类型的处理逻辑。
4. 消息中心Tab页面
消息中心是NearPlay应用的核心功能页面之一,位于Index.ets的第四个Tab位,通过"消息"标签切换访问。消息中心整合了通知和聊天两大社交通信功能,以分区列表的形式呈现。
4.1 页面结构设计
消息中心(MessageContent构建器)采用上下分区的布局结构:
上半区——通知区域:
- 标题"通知"(16vp,Medium字重)
- 通知列表(高度固定180vp,带1vp间距)
- 白色圆角卡片容器
下半区——聊天区域:
- 标题"聊天"(16vp,Medium字重)
- 聊天列表(flexGrow自适应高度)
- 白色圆角卡片容器
两个区域通过16vp的垂直间距分隔,形成视觉上的明确分区。通知区域的高度固定为180vp,确保通知不会占据过多空间而挤压聊天列表。这种固定+自适应的混合布局策略在信息密度和空间利用之间取得了平衡。
4.2 页面顶部
消息中心的顶部是页面标题"消息",以24vp字号、Bold字重显示,左侧16vp边距,上下各12vp和4vp的外边距。标题下方直接开始通知区域,没有额外的搜索或筛选功能,保持了页面的简洁性。
4.3 通知区域详解

通知区域的容器是一个Column组件,内部包含"通知"标题和通知列表:
Column() {
Text('通知')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ left: 16, top: 8, bottom: 4 })
.width('100%')
List({ space: 1 }) {
ForEach(this.notifications, (item: NotifyItem) => {
// 通知项渲染
}, (item: NotifyItem) => item.id)
}
.width('100%')
.height(180)
}
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ left: 16, right: 16, top: 4 })
List组件的高度固定为180vp,这是一个经过计算的设计值——假设每个通知项的高度约为45vp(28vp头像 + 上下padding),180vp可以完整展示约4条通知,覆盖了大多数使用场景的通知数量。如果通知超过4条,List内部支持滚动浏览。
通知列表的space参数设为1,在通知项之间创建1vp的细微分隔线效果,配合白色背景形成视觉层次。
4.4 聊天区域详解
聊天区域的结构与通知区域类似,但高度不固定:
Text('聊天')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ left: 16, top: 16, bottom: 4 })
.width('100%')
List({ space: 1 }) {
ForEach(this.chatConversations, (conv: ChatConversation) => {
// 聊天项渲染
}, (conv: ChatConversation) => conv.id)
}
.width('100%')
.layoutWeight(1)
.margin({ top: 4 })
.backgroundColor(Color.White)
.borderRadius(12)
.margin({ left: 16, right: 16 })
聊天列表使用layoutWeight(1)占据通知区域下方的剩余空间,当聊天会话较多时可以完整滚动。每个聊天项显示更大的头像(36vp)、会话名称、最后一条消息预览和未读计数徽章。
4.5 通知与聊天的交互差异
通知和聊天在点击行为上有明确差异:
- 通知点击——检查activityId,非空则跳转到活动详情页,同时标记通知为已读
- 聊天点击——跳转到ChatPage,传递会话ID、目标用户信息和头像
这种差异反映了两类通信的不同性质:通知是信息的单向推送,点击后查看详情即可;聊天是双向互动,点击后进入持续的对话界面。
4.6 消息中心的整体布局
整个消息中心页面采用Column布局,背景色为#F5F5F5(浅灰色),与NearPlay其他页面保持一致。通知区域和聊天区域作为独立的白色卡片浮在灰色背景上,通过12vp圆角和16vp外边距形成卡片式设计语言。这种设计既美观又实用——白色卡片与灰色背景的对比使消息内容更突出,卡片之间的间距提供了清晰的视觉分隔。
5. 通知列表渲染
通知列表的渲染是消息中心的核心功能,通过ForEach组件结合NotifyItem数据驱动实现。每条通知以NotificationCard(内联Builder)的形式呈现,包含头像、标题、内容和状态指示。
5.1 ForEach渲染机制
List({ space: 1 }) {
ForEach(this.notifications, (item: NotifyItem) => {
ListItem() {
Row() {
// 通知内容
}
}
}, (item: NotifyItem) => item.id)
}
ForEach的三个参数:
- 数据源:this.notifications,经过黑名单过滤的通知数组
- 子项生成器:(item: NotifyItem) => ListItem(),为每条通知创建UI组件
- 键值生成器:(item: NotifyItem) => item.id,使用通知的id作为唯一key
key的作用至关重要——当通知列表更新时(如新增通知、标记已读),ForEach通过key判断哪些项需要重新渲染、哪些可以复用。使用id作为key确保了每条通知的渲染状态独立且稳定,不会因为列表顺序变化而错乱。
5.2 通知卡片布局
每条通知卡片采用Row布局,从左到右排列三个区域:
左侧头像区:
Text(item.fromAvatar !== '' ? item.fromAvatar : '📢')
.fontSize(28)
头像使用28vp字号的emoji,系统通知显示📢图标。28vp的尺寸在通知列表的紧凑布局中既能识别头像又不占用过多空间。
中间内容区:
Column() {
Text(item.title)
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text(item.content)
.fontSize(12)
.fontColor('#666666')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
.layoutWeight(1)
内容区使用Column垂直排列标题和详情。标题14vp、Medium字重,详情12vp、灰色、单行截断。layoutWeight(1)让内容区占据头像和状态区之间的全部剩余空间。
右侧状态区:
if (!item.isRead) {
Column()
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor('#F44336')
}
未读通知显示8x8红色圆点,已读通知不显示。红点的醒目位置和颜色确保用户一眼就能识别未读通知。
5.3 通知卡片的背景色
通知卡片的背景色根据已读状态动态设置:
.backgroundColor(item.isRead ? Color.White : '#FFF8E1')
未读通知的背景色为#FFF8E1(淡黄色),已读通知为白色。淡黄色背景与白色列表背景形成细微对比,在不使用大面积颜色的情况下突出了未读通知的存在感。当用户点击通知标记为已读后,背景色从淡黄色变为白色,提供了一个即时的视觉反馈。
5.4 通知卡片的交互区域
整个通知卡片(Row)设置了onClick事件,点击区域覆盖整行:
.onClick(() => {
if (item.activityId !== '') {
const params: ActivityDetailParams = { activityId: item.activityId }
this.getUIContext().getRouter().pushUrl({ url: 'pages/ActivityDetail', params: params })
}
item.isRead = true
})
点击后的行为分两步:首先检查是否有可跳转的活动,然后标记通知为已读。即使不跳转(系统通知),也会标记已读并触发背景色变化。但需要注意,由于isRead的修改是直接操作对象属性而非重新赋值@State数组,UI可能不会立即刷新。在实际运行中,ArkUI的对象属性变化检测可能会捕获这个修改,但更安全的做法是重新赋值notifications数组来确保刷新。
6. 已读/未读状态管理
已读/未读状态是通知系统中最基础的状态管理,它决定了通知在列表中的视觉表现和用户的注意力引导。
6.1 isRead字段的初始状态
所有新创建的NotifyItem的isRead默认为false(未读)。在of()工厂方法中,isRead不被设置为参数,因为它总是以未读状态创建。这种设计符合通知的自然语义——新产生的通知对于用户而言都是未读的,只有用户主动查看后才变为已读。
6.2 标记已读的时机
在NearPlay中,通知在用户点击时被标记为已读:
.onClick(() => {
// 跳转逻辑...
item.isRead = true
})
这种"点击即已读"的策略是社交应用中最常见的做法,它假设用户点击通知意味着已经阅读了通知内容。替代策略包括:
- 展开即已读——通知展开详情时标记已读,适用于通知有简略/完整两种展示模式的设计
- 手动标记——提供"标记已读"按钮,用户主动操作,给予最大控制权
- 滚动过即已读——通知在列表中滚动可见时自动标记,适用于信息流式的通知展示
NearPlay选择"点击即已读"是因为其通知列表是紧凑模式,通知的全部信息(标题+内容)已经在列表中可见,点击更多是为了跳转而非查看详情。
6.3 未读计数的实现
MockNotifyData提供了getUnreadCount()方法返回未读通知数量:
static getUnreadCount(): number {
return 3
}
当前版本返回固定值3,未与实际通知的isRead状态同步。在接入真实后端后,这个方法应该计算isRead===false的通知数量。未读计数可以在Tab标签上显示为徽章,提醒用户有待处理的通知。
6.4 状态管理的局限性
当前版本的isRead状态管理存在两个局限:
非全局同步——修改item.isRead只影响当前页面的通知实例。如果在Index页面标记某通知为已读,然后导航到ActivityDetail页面,后者的通知列表仍然显示该通知为未读。这是因为每次调用MockNotifyData.getNotifications()都创建新的数组,不同页面持有不同的对象实例。
非持久化——应用重启后所有通知恢复为未读状态。isRead的修改仅存在于内存中,不会写入持久化存储。
这两个局限在Mock数据阶段是可以接受的,但在生产环境中需要通过全局状态管理(如AppStorage或PersistentStorage)和后端同步来解决。
7. 拉黑过滤通知
黑名单过滤是NearPlay通知系统中最重要的数据净化机制,确保被拉黑用户触发的通知不会出现在通知列表中。
7.1 过滤实现
在Index.ets的refreshFilteredData方法中,通知的过滤逻辑为:
this.notifications = MockNotifyData.getNotifications()
.filter((n: NotifyItem) => !this.blockedUserIds.includes(n.fromUserId))
这行代码做了三件事:
- 获取全部通知数据
- 对每条通知检查其fromUserId是否在blockedUserIds数组中
- 排除(filter out)fromUserId在黑名单中的通知
filter方法创建一个新数组,原数组不受影响。每条通知的fromUserId与blockedUserIds数组进行includes检查,返回true表示该通知来自被拉黑用户,需要过滤掉。取反操作符!将匹配结果反转——只有在黑名单中的用户的通知被排除,其他通知保留。
7.2 过滤的触发时机
通知过滤在refreshFilteredData()方法中执行,该方法在以下场景被调用:
- aboutToAppear——页面初始化时,过滤初始通知数据
- syncBlockState——黑名单变化时(添加或移除拉黑用户),重新过滤所有数据
syncBlockState方法是黑名单与通知过滤的连接点:
syncBlockState(): void {
this.blockedUsers = getBlockedUsers()
this.blockedUserIds = this.blockedUsers.map((u: BlockedUser) => u.id)
this.refreshFilteredData()
}
当用户拉黑或取消拉黑某人时,syncBlockState更新blockedUserIds数组,然后调用refreshFilteredData重新过滤通知。这意味着:
- 新拉黑用户后,其通知立即从列表消失
- 取消拉黑后,其通知重新出现在列表中
这种实时响应的黑名单过滤确保了用户对社交边界的控制是即时生效的,不需要刷新页面或重启应用。
7.3 系统通知的过滤处理
系统通知(NotifyType.SYSTEM)的fromUserId为空字符串。空字符串不在blockedUserIds中(因为blockedUserIds只包含真实用户ID),所以系统通知不会被过滤,始终显示在通知列表中。这是正确的行为——系统通知代表应用级别的信息,与个人社交关系无关,不应该因为黑名单而被隐藏。
7.4 过滤的性能考量
当前实现使用Array.includes()进行黑名单检查,时间复杂度为O(n)。对于NearPlay的规模(通知数量在几十条以内,黑名单用户在个位数),这种线性查找的性能完全可以接受。但如果未来通知量和黑名单规模大幅增长,可以考虑以下优化:
- 将blockedUserIds从数组改为Set,使查找复杂度降为O(1)
- 在通知创建时预计算过滤标记,避免每次渲染时重新过滤
- 使用虚拟列表(LazyForEach)替代ForEach,只渲染可见区域的通知
7.5 过滤与ActivityDetail的差异
ActivityDetail页面的通知过滤逻辑与Index不同,它按activityId而非黑名单过滤:
this.notifications = MockNotifyData.getNotifications()
.filter((n: NotifyItem) => n.activityId === this.activityId)
这是因为ActivityDetail关注的是与特定活动相关的所有通知,不应因黑名单而遗漏活动参与者信息。例如,即使用户拉黑了某人,如果那人报名了你发布的活动,你仍然需要看到这条报名通知来管理活动参与人员。两个页面的过滤逻辑差异体现了不同使用场景的需求差异。
8. 通知点击跳转
通知点击跳转是将通知与功能页面连接的交互桥梁,根据通知的类型和关联数据决定跳转目标。
8.1 当前跳转逻辑
NearPlay当前版本的跳转逻辑基于activityId字段:
.onClick(() => {
if (item.activityId !== '') {
const params: ActivityDetailParams = { activityId: item.activityId }
this.getUIContext().getRouter().pushUrl({ url: 'pages/ActivityDetail', params: params })
}
item.isRead = true
})
逻辑非常简洁:如果通知关联了某个活动(activityId非空),就跳转到该活动的详情页;否则不执行跳转,仅标记已读。
8.2 按类型的跳转映射设计
虽然当前实现只检查activityId,但更完整的设计应该基于NotifyType建立类型到路由的映射:
| 通知类型 | activityId | 跳转目标 | 参数 |
|---|---|---|---|
| SIGNUP | 非空 | ActivityDetail | activityId |
| SYSTEM | 空 | 无跳转 | - |
| GAME_INVITE | 空 | GameRoom | gameId |
这种映射设计将通知类型与跳转逻辑解耦,新增通知类型时只需要在映射表中添加一行即可。实现方式可以使用if-else链或策略模式:
onClick(item: NotifyItem): void {
switch (item.type) {
case NotifyType.SIGNUP:
if (item.activityId !== '') {
router.pushUrl({ url: 'pages/ActivityDetail', params: { activityId: item.activityId } })
}
break
case NotifyType.GAME_INVITE:
// 未来跳转到游戏房间
break
case NotifyType.SYSTEM:
// 系统通知不跳转
break
}
item.isRead = true
}
8.3 跳转参数的传递
跳转参数通过Router的params字段传递。对于活动详情跳转,参数为ActivityDetailParams类型,包含activityId字段。参数在目标页面的aboutToAppear中通过getParams()获取:
const params = this.getUIContext().getRouter().getParams() as ActivityDetailParams
this.activityId = params?.activityId ?? ''
参数传递的安全性通过可选链操作符(?.)和空值合并操作符(??)保证——如果params为null或activityId未定义,使用空字符串作为默认值,避免运行时错误。
8.4 跳转的异步特性
router.pushUrl是异步操作,调用后立即返回,不会阻塞当前页面的渲染。跳转过程中,当前页面仍然可见,直到目标页面完成渲染后系统执行页面切换动画。在动画期间,当前页面的onClick事件仍然可以响应,可能导致重复点击触发多次跳转。为避免此问题,可以在onClick中添加防重复点击逻辑,如在跳转开始时设置一个标志位,跳转完成后清除。
8.5 无跳转通知的交互
对于没有跳转目标的通知(系统通知、GAME_INVITE在当前版本中),点击后仅标记已读。这种"点击无反应"的交互可能让用户感到困惑——预期点击通知应该打开某个页面,但实际什么都没发生。更好的做法是在点击时显示一个简短的Toast提示或展开通知详情,给用户一个明确的反馈。
8.6 未来扩展:深层链接跳转
随着NearPlay功能扩展,通知跳转可以引入深层链接(Deep Link)机制。每条通知携带一个actionUrl字段,格式如"nearplay://activity/detail?id=a1"或"nearplay://game/room?id=g1",通知点击时统一解析actionUrl并路由到对应页面。这种机制将跳转逻辑完全配置化,新增跳转目标不需要修改代码,只需在通知数据中添加新的actionUrl即可。
9. MockNotificationData
MockNotifyData是通知系统的Mock数据提供者,为开发和测试阶段提供模拟通知数据。定义在entry/src/main/ets/model/NotifyModel.ets中。
9.1 数据结构
export class MockNotifyData {
static getNotifications(): NotifyItem[] {
return [
NotifyItem.of('n1', NotifyType.SIGNUP, '新报名', '小明 报名了你的活动"周末狼人杀聚会"', 'u1', '小明', '👦', 'a1'),
NotifyItem.of('n2', NotifyType.SIGNUP, '新报名', '阿花 报名了你的活动"周末狼人杀聚会"', 'u2', '阿花', '👧', 'a1'),
NotifyItem.of('n3', NotifyType.GAME_INVITE, '游戏邀请', '大壮 邀请你加入狼人杀', 'u3', '大壮', '🧑', ''),
NotifyItem.of('n4', NotifyType.SYSTEM, '系统通知', '附近有3位新用户上线', '', '', '', ''),
NotifyItem.of('n5', NotifyType.SIGNUP, '新报名', '小雪 报名了你的活动"你画我猜欢乐局"', 'u8', '小雪', '👩🦰', 'a3'),
]
}
static getUnreadCount(): number {
return 3
}
}
Mock数据包含了三种通知类型的典型样例:
- 2条SIGNUP通知(n1、n2)关联同一活动a1,模拟多人报名同一活动的场景
- 1条GAME_INVITE通知(n3),activityId为空
- 1条SYSTEM通知(n4),fromUserId为空
- 1条SIGNUP通知(n5),关联不同活动a3
数据的fromUserId值(u1、u2、u3、u8)与MockUserData中的用户ID对应,确保通知的触发者可以在附近用户列表中找到。a1和a3对应MockActivityData中的活动ID,保证点击跳转的目标页面有数据可展示。
getUnreadCount()返回固定值3,与SIGNUP和GAME_INVITE通知的数量一致(假设系统通知默认已读),在开发阶段提供未读徽章的展示数据。
10. 通知系统的HarmonyOS推送集成展望
10.1 本地通知与推送通知的区别
NearPlay当前的通知系统是"本地通知"——通知数据存储在应用内存中,用户打开应用后才能看到。推送通知则是系统级功能——即使应用未运行,系统也能在通知栏展示通知。HarmonyOS提供了@kit.NotificationKit的notificationManager API实现推送通知,NearPlay未来版本应集成此能力,确保游戏邀请和活动报名等重要通知不会因为用户未打开应用而错过。
10.2 推送通知的权限申请
推送通知需要用户授权。NearPlay的PermissionPage已经申请了通知权限(ohos.permission.NOTIFICATION_CONTROLLER),但当前只获取了权限状态,未实际调用通知API。接入推送后,需要在合适的时机调用notificationManager.publish()发送通知,并处理用户点击通知打开应用时的路由跳转。
10.3 通知渠道的分类设计
HarmonyOS支持通知渠道(NotificationSlot),可以按渠道设置不同的提醒方式(声音、振动、横幅)。NearPlay的通知渠道设计建议:游戏邀请渠道(高优先级、声音+振动+横幅)、活动报名渠道(中优先级、声音)、系统通知渠道(低优先级、静默)。这种分类确保了不同重要程度的通知有不同的打扰级别——游戏邀请需要立即响应,系统通知可以稍后查看。
11. 通知系统与六种游戏的集成
11.1 狼人杀的通知场景
狼人杀游戏中,通知场景包括:游戏开始通知(所有玩家)、你的回合通知(当前发言者)、投票结果通知(所有玩家)、游戏结束通知(所有玩家)。其中"你的回合"通知是最关键的——如果用户将应用切到后台等待轮到自己,系统通知可以提醒用户回来发言,避免超时跳过。
11.2 你画我猜的通知场景
你画我猜的通知场景包括:轮到你画的通知(当前画手)、有人猜对的通知(画手和猜对者)、游戏结束通知。其中"有人猜对"通知是实时的反馈——画手需要知道自己的画是否被理解,猜对者需要确认自己的猜测是否正确。
11.3 剧本杀的通知场景
剧本杀的通知场景包括:新幕开启通知(所有玩家,包含幕的背景描述)、NPC对话通知(特定玩家,角色专属信息)、投票开始通知(所有玩家)、投票结果通知。NPC对话通知是剧本杀的特色——不同角色收到不同的NPC信息,体现了剧本杀的信息不对称设计。
11.4 游戏邀请通知的处理流程
游戏邀请通知(GAME_INVITE类型)的完整处理流程:组织者在GameRoom发起邀请→系统创建NotifyItem→通知推送到被邀请者的设备→被邀请者点击通知→跳转到GameRoom页面→自动加入游戏房间。当前版本中,邀请通知点击后不执行跳转(因为GAME_INVITE的activityId为空),这是一个已知的功能缺口。
12. 通知系统的性能与体验优化
12.1 大量通知的渲染优化
当通知数量增长到数百条时,ForEach一次性渲染所有通知会导致加载变慢和内存占用增加。LazyForEach是ArkUI提供的懒加载方案,只渲染可见区域的通知项,滚动到新区域时才加载。迁移到LazyForEach需要实现IDataSource接口,管理通知数据的增删改查,并在数据变化时通知LazyForEach刷新对应项。
12.2 通知去重策略
在某些场景下,同一事件可能产生多条通知——例如,三个人在短时间内依次报名同一活动,产生三条SIGNUP通知。去重策略是将这些通知合并为一条——“小明、阿花、大壮 报名了你的活动”,减少通知列表的信息冗余。去重逻辑需要考虑时间窗口(五秒内同一活动的报名合并)和合并上限(超过三人显示"等N人报名了你的活动")。
12.3 通知的自动清理
通知不应无限积累。建议的清理策略是:已读通知保留七天,之后自动删除;未读通知永久保留,直到用户标记已读或手动删除。清理操作可以在应用启动时执行,检查每条通知的创建时间,删除超期的已读通知。这种自动清理机制防止了通知列表的无限膨胀,同时确保未读通知不会意外丢失。
13. 通知系统的全局状态同步
13.1 跨页面已读状态同步问题
当前实现中,用户在Index页面点击通知标记为已读后,导航到ActivityDetail页面,后者加载的通知仍然是未读状态(因为每次调用MockNotifyData.getNotifications()都创建新对象)。解决此问题需要引入全局通知状态管理——使用AppStorage存储通知列表,所有页面通过@StorageLink访问同一份数据,修改自动同步到所有页面。
13.2 AppStorage方案的设计
将通知数据存储在AppStorage中需要解决两个问题:键的设计和变更通知。键可以使用固定字符串"notifications",值类型为NotifyItem[]。变更通知通过重新赋值数组触发——当某条通知标记为已读时,创建新数组替换AppStorage中的旧数组,所有@StorageLink绑定页面自动刷新。
13.3 后端同步的架构设计
在接入真实后端后,通知数据应从服务端推送而非本地Mock。推送架构设计:WebSocket长连接接收实时通知→本地缓存通知数据→AppStorage驱动UI渲染→已读状态定期批量上报服务端。WebSocket与HTTP的结合确保了实时性和可靠性——WebSocket处理实时推送,HTTP处理历史通知拉取和已读状态上报。
14. 通知的智能排序与优先级算法
14.1 当前排序的局限
当前通知列表按Mock数据的固定顺序展示,没有排序逻辑。用户看到的第一条通知可能是最旧的而非最重要的。在通知数量增长后,用户需要滚动才能看到最新的通知,体验很差。
14.2 时间倒序排序
最基本的排序策略是时间倒序——最新的通知排在最前面。这符合用户"先看最新消息"的阅读习惯。但纯时间排序的问题是把重要通知(如游戏邀请)和次要通知(如系统通知)混在一起,用户可能错过需要立即处理的通知。
14.3 加权优先级排序
更好的排序策略是加权优先级——每条通知有一个优先级分数,按分数降序排列。分数的计算公式:优先级分数等于基础权重加时效权重加关系权重。基础权重按类型设定:游戏邀请权重最高(因为有时效性),报名通知次之,系统通知最低。时效权重随时间衰减——刚收到的通知时效权重最高,一小时后衰减为零。关系权重考虑发送者与用户的关系——好友的通知权重高于陌生人的通知。
14.4 置顶与免打扰
某些通知需要置顶——如"你的回合到了"(狼人杀游戏中),无论排序如何都应显示在最前面。某些通知需要免打扰——如系统通知,用户可以选择不弹出横幅只显示在列表中。置顶和免打扰是优先级排序的补充机制,为特定场景提供了确定性的控制。
15. 通知免打扰模式设计
15.1 场景驱动的免打扰
NearPlay的免打扰模式应基于场景自动切换——用户正在游戏中时自动屏蔽非紧急通知(如报名通知、系统通知),只显示紧急通知(如游戏邀请超时提醒);用户在非游戏页面时恢复正常通知展示。这种场景驱动的免打扰比手动切换更智能——用户不需要记得"进游戏前开免打扰、出来后关免打扰"。
15.2 全局免打扰时段
用户可以设置全局免打扰时段——如晚上十一点到早上八点不弹出通知横幅,但通知仍然记录在列表中供用户醒来后查看。这种时段免打扰在社交应用中很常见,尊重了用户的休息时间。
15.3 单会话免打扰
用户可以对特定聊天会话或特定活动设置免打扰——如某个活动已经结束了,不想再收到该活动的报名通知;某个群聊太活跃了,不想每次消息都收到通知。单会话免打扰是精细化的通知控制,避免了"一刀切"的粗暴方式。
16. 通知聚合与摘要展示
16.1 同类通知聚合
当多条同类通知在短时间内到达时,应聚合为一条展示——如"小明、阿花、大壮报名了你的活动"而非三条独立通知。聚合的条件是:同一活动(activityId相同)、同一类型(都是SIGNUP)、五分钟内到达。聚合后的通知展开后可以看到每个人的具体报名时间。
16.2 通知摘要
每日通知摘要将一天的通知按类别归纳——“今天收到三条报名通知、一条游戏邀请、两条系统通知”。摘要以卡片形式展示在通知列表顶部,用户可以快速了解今天的通知概览,然后按需深入查看每个类别。
17. 通知模板系统设计
17.1 模板化的必要性
当前通知的标题和内容是在代码中硬编码的字符串。新增通知类型时需要修改代码,不够灵活。通知模板系统将标题和内容的格式抽取为模板,新增通知类型时只需在模板配置中添加一行,无需修改代码。
17.2 模板格式设计
通知模板使用占位符语法——如"{{userName}}报名了你的活动{{activityName}}"。占位符在运行时由通知数据中的实际值替换。模板存储在JSON配置文件中,支持多语言版本(中文模板、英文模板等)。这种设计将通知文案与代码逻辑彻底解耦,运营人员可以直接修改模板而无需开发介入。
18. 通知系统的用户体验设计原则
18.1 有用性原则
每条通知都应该对用户"有用"——要么提供需要处理的信息(如"有人报名了你的活动"),要么提供值得关注的内容(如"附近有新用户上线")。纯粹"提醒你存在NearPlay"的推送是骚扰,不是通知。有用性的判断标准是:如果这条通知不出现,用户是否会错过重要信息?如果是,通知有用;如果否,通知是无用的打扰。
18.2 及时性原则
通知应在事件发生后尽快推送,延迟会降低通知的价值——“有人报名了你的活动"在报名后一分钟内推送是通知,在报名后一小时推送是打扰(用户已经不关心了)。及时性要求WebSocket长连接的稳定性——连接断开期间的通知需要缓存,重连后立即推送积压的通知,但积压超过一定数量(如十条)时应聚合为摘要推送,避免"通知轰炸”。
18.3 控制性原则
用户应能控制通知的方方面面——哪些类型的通知接收、接收的时段、通知的展示方式(横幅/列表/无)、免打扰的粒度(全局/单个活动/单个会话)。控制性是尊重用户的表现——应用的通知是"服务"而非"侵扰",用户有权选择接受或不接受。NearPlay的通知设置页面应提供细粒度的控制选项,让每个用户都能找到适合自己的通知节奏。
18.4 通知的视觉一致性
所有通知在列表中的展示风格应保持一致——相同的头像尺寸、相同的标题字号、相同的内容截断规则、相同的已读/未读区分方式。一致性降低了用户的认知负荷——不需要为不同类型的通知学习不同的阅读方式。唯一的视觉差异是类型标签的颜色(报名通知橙色、游戏邀请绿色、系统通知灰色),帮助用户快速识别通知类型。
18.5 通知的无障碍设计
通知系统应考虑无障碍访问——视障用户通过屏幕阅读器听取通知内容,需要确保通知标题和内容文本的语义清晰;色盲用户可能无法区分"橙色"和"绿色"的类型标签,需要额外的图标或文字辅助;大字体模式下通知卡片的布局应自适应,避免文字溢出或截断。无障碍设计不仅是社会责任,也是HarmonyOS应用上架审核的基本要求。
18.6 通知的情感化设计
通知不仅是信息传递,更是情感连接。措辞的温度影响用户感受——"有人报名了你的活动"是中性描述,“太棒了!小明报名了你的狼人杀之夜"则更有温度。NearPlay的通知文案应在信息完整性和情感表达之间取得平衡——游戏邀请类通知可以活泼(“快来加入!三缺一了”),系统通知类保持中性(“应用已更新到最新版本”)。情感化设计让通知从"不得不看的系统消息"变成"期待看到的社交互动”。
更多推荐




所有评论(0)