案例开源地址:https://atomgit.com/nutpi/rn_openharmony_buy

写在前面

请添加图片描述

消息中心是 App 和用户沟通的重要渠道。订单发货了、有新的促销活动、系统有重要通知,都可以通过消息推送给用户。

消息列表页要展示所有消息,区分已读和未读,支持查看详情、标记已读、删除等操作。这篇文章来实现这些功能。

消息数据结构

先看看消息数据长什么样:

interface Message {
  id: number;
  title: string;      // 消息标题
  content: string;    // 消息内容
  time: string;       // 发送时间
  read: boolean;      // 是否已读
  type: 'system' | 'order' | 'promotion';  // 消息类型
}

消息分三种类型:系统通知、订单消息、促销活动。不同类型显示不同的图标和标签。

类型配置

用配置对象管理不同类型的图标和标签:

const typeIcons: Record<string, string> = {
  system: '📢', 
  order: '📦', 
  promotion: '🎁'
};

const typeLabels: Record<string, string> = {
  system: '系统通知', 
  order: '订单消息', 
  promotion: '促销活动'
};

系统通知用喇叭图标,订单消息用包裹图标,促销活动用礼物图标。这样用户一眼就能看出是什么类型的消息。

为什么用配置对象?

如果在代码里写 if (type === 'system') return '📢',后面要加新类型或改图标都很麻烦。用配置对象的话,改一个地方就够了,而且代码更简洁:typeIcons[type]

引入依赖

import React from 'react';
import {View, Text, FlatList, StyleSheet, TouchableOpacity, Alert} from 'react-native';
import {useApp} from '../store/AppContext';
import {Header} from '../components/Header';
import {Empty} from '../components/Empty';
import {Message} from '../types';

标准的列表页依赖,加上 Alert 用于删除确认。

组件主体

export const MessageListScreen = () => {
  const {messages, navigate, markAllRead, deleteMessage, unreadCount} = useApp();

从全局状态拿消息列表和相关方法:

  • messages:消息列表
  • markAllRead:标记全部已读
  • deleteMessage:删除消息
  • unreadCount:未读消息数量

删除处理

const handleDelete = (id: number) => {
  Alert.alert('删除消息', '确定要删除这条消息吗?', [
    {text: '取消', style: 'cancel'},
    {text: '删除', style: 'destructive', onPress: () => deleteMessage(id)},
  ]);
};

删除需要二次确认,避免误操作。删除后消息就没了,不像已读还能恢复。

消息卡片渲染

const renderMessage = ({item}: {item: Message}) => (
  <TouchableOpacity
    style={styles.messageCard}
    onPress={() => navigate('messageDetail', {message: item})}
    onLongPress={() => handleDelete(item.id)}
  >

点击进入消息详情,长按删除消息。长按删除是移动端常见的交互方式,不占用界面空间。

为什么用长按删除?

如果每条消息都显示删除按钮,界面会很乱。长按删除既能实现功能,又保持界面简洁。不过要在某个地方提示用户"长按可删除",不然用户可能不知道。

图标区域

    <View style={styles.iconContainer}>
      <View style={[styles.iconWrap, !item.read && styles.iconWrapUnread]}>
        <Text style={styles.icon}>{typeIcons[item.type]}</Text>
      </View>
      {!item.read && <View style={styles.dot} />}
    </View>

左边是消息类型图标,根据类型显示不同的 emoji。未读消息的图标背景是浅蓝色,已读的是灰色。

未读消息右上角有个红点,这是最常见的未读标记方式。

内容区域

    <View style={styles.content}>
      <View style={styles.titleRow}>
        <Text style={[styles.title, !item.read && styles.unreadTitle]} numberOfLines={1}>
          {item.title}
        </Text>
        <Text style={styles.time}>{item.time}</Text>
      </View>

第一行是标题和时间。未读消息的标题加粗,视觉上更突出。标题限制一行,超出显示省略号。

      <Text style={styles.typeLabel}>{typeLabels[item.type]}</Text>
      <Text style={styles.preview} numberOfLines={2}>{item.content}</Text>
    </View>
  </TouchableOpacity>
);

第二行是消息类型标签,蓝色小字。第三行是内容预览,限制两行。用户看预览就能大概知道消息内容,决定要不要点进去看详情。

页面头部

return (
  <View style={styles.container}>
    <Header
      title="消息中心"
      rightElement={
        unreadCount > 0 ? (
          <TouchableOpacity onPress={markAllRead}>
            <Text style={styles.markAllBtn}>全部已读</Text>
          </TouchableOpacity>
        ) : undefined
      }
    />

头部右边有个"全部已读"按钮,只在有未读消息时显示。点击把所有消息标记为已读。

全部已读要不要确认?

我这里没加确认。标记已读不是什么严重的操作,用户想看未读消息还是能看到内容的,只是红点没了。如果加确认反而显得啰嗦。

消息统计

    {messages.length > 0 && (
      <View style={styles.summary}>
        <Text style={styles.summaryText}>共 {messages.length} 条消息</Text>
        {unreadCount > 0 && <Text style={styles.unreadText}>{unreadCount} 条未读</Text>}
      </View>
    )}

列表上方显示消息总数和未读数。让用户对消息情况有个整体了解。

列表渲染

    {messages.length === 0 ? (
      <Empty icon="🔔" title="暂无消息" subtitle="您还没有收到任何消息" />
    ) : (
      <FlatList
        data={messages}
        keyExtractor={item => item.id.toString()}
        contentContainerStyle={styles.list}
        renderItem={renderMessage}
      />
    )}
  </View>
);
};

没有消息时显示空状态,有消息时用 FlatList 渲染列表。

Context 里的消息操作

看看全局状态里消息相关的方法:

const [messages, setMessages] = useState<Message[]>(mockMessages);

初始化了一些模拟数据。

标记单条已读:

const markMessageRead = (id: number) => {
  setMessages(prev => prev.map(m => (m.id === id ? {...m, read: true} : m)));
};

把指定消息的 read 设为 true。这个方法在消息详情页调用,用户打开详情就标记已读。

标记全部已读:

const markAllRead = () => {
  setMessages(prev => prev.map(m => ({...m, read: true})));
};

把所有消息的 read 都设为 true

删除消息:

const deleteMessage = (id: number) => {
  setMessages(prev => prev.filter(m => m.id !== id));
};

filter 过滤掉指定消息。

计算未读数量:

const unreadCount = messages.filter(m => !m.read).length;

这是个派生状态,从 messages 实时计算出来。

样式详解

消息卡片的样式:

messageCard: {
  flexDirection: 'row',
  backgroundColor: '#fff',
  borderRadius: 12,
  padding: 16,
  marginBottom: 12,
},

横向布局,左边图标右边内容。

未读红点的样式:

dot: {
  position: 'absolute',
  top: 0,
  right: 0,
  width: 10,
  height: 10,
  borderRadius: 5,
  backgroundColor: '#e74c3c',
  borderWidth: 2,
  borderColor: '#fff',
},

绝对定位在图标右上角,红色圆点。borderColor: '#fff' 加了白色边框,让红点和背景有个分隔,更醒目。

图标容器的样式:

iconWrap: {
  width: 48,
  height: 48,
  borderRadius: 24,
  backgroundColor: '#f5f5f5',
  justifyContent: 'center',
  alignItems: 'center',
},
iconWrapUnread: {backgroundColor: '#e8f4fc'},

圆形容器,已读是灰色背景,未读是浅蓝色背景。

消息分组

当前所有消息混在一起,可以按类型或日期分组:

const groupByType = (messages: Message[]) => {
  const groups: Record<string, Message[]> = {};
  messages.forEach(m => {
    if (!groups[m.type]) groups[m.type] = [];
    groups[m.type].push(m);
  });
  return Object.entries(groups).map(([type, items]) => ({
    title: typeLabels[type],
    data: items,
  }));
};

然后用 SectionList 渲染,每种类型一个 section。

滑动操作

可以加滑动操作,左滑显示"删除"和"标记已读"按钮:

import {Swipeable} from 'react-native-gesture-handler';

<Swipeable
  renderRightActions={() => (
    <View style={styles.swipeActions}>
      <TouchableOpacity onPress={() => markMessageRead(item.id)}>
        <Text>已读</Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => handleDelete(item.id)}>
        <Text>删除</Text>
      </TouchableOpacity>
    </View>
  )}
>
  {/* 消息卡片内容 */}
</Swipeable>

滑动操作比长按更直观,用户更容易发现。

消息推送

当前消息是写死的,正式项目应该接入推送服务:

  1. 用户打开 App 时,从服务器拉取消息列表
  2. App 在后台时,通过推送服务接收新消息
  3. 收到新消息时,更新本地消息列表,显示角标

推送服务可以用 Firebase Cloud Messaging、极光推送等。

写在最后

消息列表页的核心是已读/未读状态的管理和视觉区分。未读消息要有明显的标记(红点、加粗、不同背景色),让用户一眼就能看出哪些是新消息。

长按删除、全部已读这些操作要考虑用户体验,该确认的确认,不该确认的别啰嗦。

下一篇写消息详情页,展示消息的完整内容。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐