React Native鸿蒙跨平台响应式统计可以采用@Watch装饰器实现,依赖数组[messages]指定了效果触发的条件,避免了不必要的计算开销
本文分析了一个基于React Native构建的消息回执管理应用,重点探讨了其核心架构设计和交互实现。应用采用精细化的消息状态模型(sent/delivered/read/failed)和动态状态可视化机制,通过颜色编码和图标直观展示消息状态。系统实现了响应式统计功能,自动计算各类消息数量。交互设计上,采用长按手势触发详情查看,模态框展示丰富的回执信息。文章还对比了React Native与鸿蒙平
概述
本文分析的是一个基于React Native构建的消息回执管理应用,集成了消息状态追踪、已读回执统计、消息详情查看等核心功能。该应用采用了状态驱动的UI设计、动态数据统计和丰富的交互操作,展现了消息类应用的典型技术架构。在鸿蒙OS的跨端适配场景中,这种涉及实时状态更新和多维度数据展示的应用具有重要的技术参考价值。
核心架构设计深度解析
状态驱动的消息模型
应用定义了精细化的消息状态模型,支持四种不同的消息状态:
type Message = {
id: string;
sender: string;
content: string;
time: string;
status: 'sent' | 'delivered' | 'read' | 'failed';
readBy?: string[];
sentAt: string;
};
这种状态设计具有明确的业务语义,每个状态都对应着消息生命周期的特定阶段。sent表示已发送但未送达,delivered表示已送达但未读,read表示已读,failed表示发送失败。readBy数组则记录了具体的已读用户,为已读回执功能提供了数据基础。
在鸿蒙ArkUI体系中,类型定义采用接口形式,支持类似的枚举状态:
interface Message {
id: string;
sender: string;
content: string;
time: string;
status: 'sent' | 'delivered' | 'read' | 'failed';
readBy?: string[];
sentAt: string;
}
动态状态可视化
MessageItem组件实现了状态到视觉元素的动态映射:
const getStatusIcon = () => {
switch (message.status) {
case 'sent':
return ICONS.sent;
case 'delivered':
return ICONS.unread;
case 'read':
return ICONS.read;
case 'failed':
return '❌';
default:
return ICONS.sent;
}
};
const getStatusColor = () => {
switch (message.status) {
case 'read':
return '#10b981';
case 'delivered':
return '#f59e0b';
case 'sent':
return '#94a3b8';
case 'failed':
return '#ef4444';
default:
return '#94a3b8';
}
};
这种映射机制将业务状态转化为直观的视觉反馈,用户可以通过颜色和图标快速识别消息状态。绿色表示已读,黄色表示已送达,灰色表示已发送,红色表示失败,这种颜色编码遵循了常见的用户认知模式。
鸿蒙平台的实现需要将样式逻辑转换为声明式结构:
@Component
struct MessageItem {
@Prop message: Message;
getStatusIcon(): string {
switch (this.message.status) {
case 'sent': return '📤';
case 'delivered': return '✉️';
case 'read': return '✅';
case 'failed': return '❌';
default: return '📤';
}
}
getStatusColor(): Color {
switch (this.message.status) {
case 'read': return Color.Green;
case 'delivered': return Color.Yellow;
case 'sent': return Color.Gray;
case 'failed': return Color.Red;
default: return Color.Gray;
}
}
build() {
Column() {
// 消息内容布局
Text(this.message.status)
.fontColor(this.getStatusColor())
}
}
}
实时统计系统
应用通过useEffect钩子实现了消息统计的自动更新:
useEffect(() => {
const total = messages.length;
const read = messages.filter(m => m.status === 'read').length;
const delivered = messages.filter(m => m.status === 'delivered').length;
const sent = messages.filter(m => m.status === 'sent').length;
setStats({ total, read, delivered, sent });
}, [messages]);
这种响应式的统计机制确保每当messages状态变化时,统计数据会自动重新计算。依赖数组[messages]指定了效果触发的条件,避免了不必要的计算开销。
鸿蒙平台的响应式统计可以采用@Watch装饰器实现:
@State messages: Message[] = [];
@State stats: { total: number, read: number, delivered: number, sent: number } = {
total: 0,
read: 0,
delivered: 0,
sent: 0
};
@Watch('messages')
onMessagesChange() {
this.stats = {
total: this.messages.length,
read: this.messages.filter(m => m.status === 'read').length,
delivered: this.messages.filter(m => m.status === 'delivered').length,
sent: this.messages.filter(m => m.status === 'sent').length
};
}
交互设计与用户体验
长按查看详情交互
应用通过长按手势触发消息详情查看:
const handleLongPress = (message: Message) => {
setSelectedMessage(message);
};
// 在MessageItem中使用
<TouchableOpacity
style={styles.messageItem}
onLongPress={() => onLongPress(message)}
>
这种交互方式平衡了界面简洁性和功能可发现性。常规点击可能用于进入聊天界面,而长按则触发辅助功能,符合移动应用的设计惯例。
鸿蒙平台的手势识别需要通过Gesture组件实现:
Gesture({
onLongPress: () => {
this.selectedMessage = this.message;
}
})
模态框详情展示
ReadReceiptDetail组件提供了丰富的已读回执信息:
const ReadReceiptDetail = ({ message }) => {
return (
<View style={styles.receiptDetail}>
<Text style={styles.receiptTitle}>已读回执详情</Text>
<Text style={styles.receiptSubtitle}>消息: {message.content.substring(0, 20)}...</Text>
{message.status === 'read' && message.readBy && message.readBy.length > 0 ? (
<View>
<Text style={styles.readByTitle}>已读用户 ({message.readBy.length}):</Text>
{message.readBy.map((user, index) => (
<View key={index} style={styles.readUserItem}>
<Text style={styles.readUserIcon}>👤</Text>
<Text style={styles.readUserName}>{user}</Text>
</View>
))}
</View>
) : (
<Text style={styles.noReadText}>暂无用户阅读此消息</Text>
)}
</View>
);
};
这种详情设计不仅展示了基本信息,还根据状态动态显示不同的内容区块。对于已读消息,会列出具体的已读用户;对于其他状态,则显示相应的提示信息。
鸿蒙的模态框实现需要完全重构,使用自定义弹窗组件:
@CustomDialog
struct ReadReceiptDetail {
@Prop message: Message;
build() {
Column() {
Text('已读回执详情')
Text(`消息: ${this.message.content.substring(0, 20)}...`)
if (this.message.status === 'read' && this.message.readBy && this.message.readBy.length > 0) {
Text(`已读用户 (${this.message.readBy.length}):`)
ForEach(this.message.readBy, (user: string) => {
Row() {
Text('👤')
Text(user)
}
})
} else {
Text('暂无用户阅读此消息')
}
}
}
}
跨端适配技术方案
组件映射策略
| React Native组件 | 鸿蒙ArkUI组件 | 适配说明 |
|---|---|---|
| FlatList | List | 列表实现方式不同 |
| TouchableOpacity | Button/Gesture | 交互反馈机制差异 |
| Modal | CustomDialog | 弹窗系统完全不同 |
| ActivityIndicator | Progress | 加载指示器样式可配置 |
状态管理迁移
React Native的状态管理:
const [messages, setMessages] = useState<Message[]>([]);
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
鸿蒙的状态管理:
@State messages: Message[] = [];
@State selectedMessage: Message | null = null;
样式系统转换
阴影效果转换示例:
// React Native
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
// 鸿蒙
.shadow({
radius: 2,
color: '#000000',
offsetX: 0,
offsetY: 1
})
性能优化与最佳实践
列表渲染优化
使用FlatList替代ScrollView+map:
<FlatList
data={messages}
keyExtractor={item => item.id}
renderItem={({ item }) => <MessageItem message={item} />}
/>
鸿蒙的优化列表:
List() {
ForEach(this.messages, (message: Message) => {
ListItem() {
MessageItem({ message: message })
}
})
}
组件记忆化
避免不必要的重渲染:
const MessageItem = React.memo(({ message }) => {
return <View>{/* 组件内容 */}</View>;
});
总结与实施建议
这个React Native消息回执应用展示了消息类应用的典型技术架构,其精细的状态管理、动态的统计系统和丰富的交互设计为鸿蒙跨端适配提供了优秀的基础。
React Native × 鸿蒙跨端技术解读:消息列表、已读回执与自定义弹窗
这段代码实现一个“消息回执”页面,包含消息列表、状态标识与已读用户明细,以及顶部统计、刷新模拟与自定义弹窗。整体基于 React Native 函数组件与 FlatList 虚拟化列表的常规架构;在 iOS/Android 上走 RN 标准组件栈;面向鸿蒙(OpenHarmony),需要通过 RN 的鸿蒙适配层把滚动、状态映射、弹窗叠层与分享/转发这类系统能力桥接到 ArkUI 与 Ability 服务,保证一致的交互与表现。
消息项组件:状态语义到图标/颜色的稳定映射
MessageItem 将消息的头部(发送者/时间)、正文与尾部(状态/操作)聚合在一条卡片内。状态展示通过两个纯函数派生:
const getStatusIcon = () => {
switch (message.status) {
case 'sent': return ICONS.sent;
case 'delivered': return ICONS.unread;
case 'read': return ICONS.read;
case 'failed': return '❌';
default: return ICONS.sent;
}
};
const getStatusColor = () => {
switch (message.status) {
case 'read': return '#10b981';
case 'delivered': return '#f59e0b';
case 'sent': return '#94a3b8';
case 'failed': return '#ef4444';
default: return '#94a3b8';
}
};
- 映射策略将业务状态与视觉表现解耦,便于统一维护与测试;生产建议把状态枚举与映射表抽到常量模块并复用在统计卡片与弹窗细节,避免“同一语义不同色”的认知负担。
- 点击交互采用 onLongPress 打开详情弹窗;为提升原生手感,建议用 Pressable 并在三端注入一致的涟漪/震动反馈,鸿蒙端通过 ArkUI 的交互能力实现。
状态尾部文案还包含已读人数的派生逻辑:
<Text style={[styles.statusText, { color: getStatusColor() }]}>
{getStatusIcon()} {message.status === 'read' && message.readBy && message.readBy.length > 0
? `已读(${message.readBy.length})`
: message.status === 'read' ? '已读'
: message.status === 'delivered' ? '已送达'
: message.status === 'sent' ? '已发送' : '发送失败'}
</Text>
这部分统一了“状态 + 已读计数”的呈现;如果后续加入“部分已读/全部已读”的细颗粒状态,可继续在这里派生文案,不影响组件边界。
已读回执详情:自定义弹窗与叠层能力
ReadReceiptDetail 接收一条消息并列出 readBy 用户的列表;页面通过 selectedMessage 条件渲染自定义蒙层与内容:
{selectedMessage && (
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>消息详情</Text>
<TouchableOpacity onPress={closeReceiptDetail}><Text style={styles.closeText}>×</Text></TouchableOpacity>
</View>
<ReadReceiptDetail message={selectedMessage} />
<View style={styles.modalActions}>
<TouchableOpacity onPress={() => Alert.alert('转发', `转发消息给其他联系人`)}>
<Text>{ICONS.message} 转发</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => Alert.alert('删除', `删除这条消息`)}>
<Text>{ICONS.delete} 删除</Text>
</TouchableOpacity>
</View>
</View>
</View>
)}
- 叠层行为当前用条件渲染 + 绝对定位覆盖层实现,适合原型阶段;生产建议改用 RN 的 Modal 或 Portal 以获得更稳定的系统层级、返回键/手势关闭与可访问性焦点迁移。鸿蒙端尤其需要在适配层正确映射 ArkUI 的叠层规范与系统返回行为。
- 弹窗操作“转发/删除”以 Alert 占位;真正的转发应桥接系统分享/路由能力(iOS Share/Activity、Android Intent、鸿蒙 Ability/Router),并统一返回码与错误处理。
顶部统计与刷新:派生统计和占位网络层
统计通过 useEffect 在 messages 变更时同步计算:
useEffect(() => {
const total = messages.length;
const read = messages.filter(m => m.status === 'read').length;
const delivered = messages.filter(m => m.status === 'delivered').length;
const sent = messages.filter(m => m.status === 'sent').length;
setStats({ total, read, delivered, sent });
}, [messages]);
- 这段计算保持纯派生与不可变更新;如果消息量较大、更新频繁,可改为增量更新或在服务层做统计聚合,避免在 UI 层做 O(n) 计算。
- refreshMessages 用 setTimeout 模拟网络追加一条 sent 状态消息,并用 ActivityIndicator 提示过程态。生产建议替换为统一网络模块(支持取消、重试与离线队列),并对失败路径进行回滚与提示。
列表渲染:FlatList 的虚拟化与稳定键
消息列表使用 FlatList 渲染,保证在消息增多时的滚动性能:
<FlatList
data={messages}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<MessageItem message={item} onLongPress={handleLongPress} />
)}
showsVerticalScrollIndicator={false}
/>
- keyExtractor 为稳定且唯一的 id,满足虚拟化 diff 的基本要求;为进一步优化,建议将 renderItem 用 useCallback 包裹,并为 MessageItem 使用 React.memo,降低重渲染。
- 如需“已读/未读分组”与“吸顶分节”,可以改用 SectionList 并在适配层下验证滚动物理的一致表现(回弹/阻尼/惯性)。
图标与时间:本地化与一致性
页面大量使用 emoji 图标(消息/未读/已读/草稿/归档/删除/更多);在不同系统字体下容易出现渲染差异(大小、基线对齐)。生产建议迁移到统一图标栈(SVG/字体)并在鸿蒙端通过 ArkUI 的图形能力桥接,确保像素与对齐一致。
时间字段同时出现“time(展示)”与“sentAt(ISO 存储)”,这是合理的双轨策略:展示本地化(toLocaleTimeString)时需统一 locale 与时区,存储建议保持 ISO(UTC)并在渲染层进行本地化转换,避免跨时区或夏令时偏差。鸿蒙端需确认适配层对 locale 与时区映射的正确性。
RN × 鸿蒙跨端落地的关键点
- 滚动与虚拟化:FlatList 的滚动物理(回弹/阻尼/惯性)与 onEnd 事件窗口在不同平台存在细微差异;鸿蒙适配层应桥接到 ArkUI 滚动容器,确保手感一致。
- 弹窗与叠层:自定义蒙层建议替换为 RN Modal/Portal,适配层需正确处理 ArkUI 的层级、返回键与无障碍焦点;Alert 的外观与按钮布局由平台控制,如需统一视觉应改用自绘弹窗。
- 分享/转发能力:转发操作通过 Ability/Router/Share 桥接原生分享面板或路由跳转,统一错误码与回调;页面仅关心结果与提示。
- 图标与触觉反馈:用 Pressable + Haptics/涟漪实现一致交互反馈;鸿蒙端通过 ArkUI 交互能力注入。
- 尺寸与旋转:本页使用 Dimensions 的初始宽度参与卡片布局;横竖屏/分屏场景建议改用 useWindowDimensions 并监听窗口事件,鸿蒙端需保证事件传递到 RN 层。
完整的代码:
// app.tsx
import React, { useState, useEffect } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, FlatList, ActivityIndicator } from 'react-native';
// 图标库
const ICONS = {
message: '💬',
unread: '✉️',
read: '✅',
sent: '📤',
draft: '📝',
archive: '🗄️',
delete: '🗑️',
more: '⋮',
};
const { width } = Dimensions.get('window');
// 消息类型
type Message = {
id: string;
sender: string;
content: string;
time: string;
status: 'sent' | 'delivered' | 'read' | 'failed';
readBy?: string[];
sentAt: string;
};
// 消息项组件
const MessageItem = ({
message,
onLongPress
}: {
message: Message;
onLongPress: (message: Message) => void
}) => {
const getStatusIcon = () => {
switch (message.status) {
case 'sent':
return ICONS.sent;
case 'delivered':
return ICONS.unread;
case 'read':
return ICONS.read;
case 'failed':
return '❌';
default:
return ICONS.sent;
}
};
const getStatusColor = () => {
switch (message.status) {
case 'read':
return '#10b981';
case 'delivered':
return '#f59e0b';
case 'sent':
return '#94a3b8';
case 'failed':
return '#ef4444';
default:
return '#94a3b8';
}
};
return (
<TouchableOpacity
style={styles.messageItem}
onLongPress={() => onLongPress(message)}
>
<View style={styles.messageHeader}>
<Text style={styles.senderName}>{message.sender}</Text>
<Text style={styles.messageTime}>{message.time}</Text>
</View>
<View style={styles.messageContent}>
<Text style={styles.messageText}>{message.content}</Text>
</View>
<View style={styles.messageFooter}>
<Text style={[styles.statusText, { color: getStatusColor() }]}>
{getStatusIcon()} {message.status === 'read' && message.readBy && message.readBy.length > 0
? `已读(${message.readBy.length})`
: message.status === 'read' ? '已读' :
message.status === 'delivered' ? '已送达' :
message.status === 'sent' ? '已发送' : '发送失败'}
</Text>
<TouchableOpacity style={styles.moreButton}>
<Text style={styles.moreText}>{ICONS.more}</Text>
</TouchableOpacity>
</View>
</TouchableOpacity>
);
};
// 已读回执详情组件
const ReadReceiptDetail = ({
message
}: {
message: Message
}) => {
return (
<View style={styles.receiptDetail}>
<Text style={styles.receiptTitle}>已读回执详情</Text>
<Text style={styles.receiptSubtitle}>消息: {message.content.substring(0, 20)}{message.content.length > 20 ? '...' : ''}</Text>
{message.status === 'read' && message.readBy && message.readBy.length > 0 ? (
<View>
<Text style={styles.readByTitle}>已读用户 ({message.readBy.length}):</Text>
{message.readBy.map((user, index) => (
<View key={index} style={styles.readUserItem}>
<Text style={styles.readUserIcon}>👤</Text>
<Text style={styles.readUserName}>{user}</Text>
</View>
))}
</View>
) : (
<Text style={styles.noReadText}>暂无用户阅读此消息</Text>
)}
</View>
);
};
// 消息统计卡片组件
const StatsCard = ({ title, value, color }: { title: string; value: string | number; color: string }) => {
return (
<View style={styles.statsCard}>
<Text style={[styles.statsValue, { color }]}>{value}</Text>
<Text style={styles.statsTitle}>{title}</Text>
</View>
);
};
// 主页面组件
const SportMessageReceiptApp: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([
{
id: '1',
sender: '运动伙伴小李',
content: '今天一起去晨跑吧,天气很好!',
time: '08:30',
status: 'read',
readBy: ['小李', '小王'],
sentAt: '2023-06-15T08:30:00Z'
},
{
id: '2',
sender: '跑步俱乐部',
content: '本周六有马拉松活动,有兴趣的朋友报名参加!',
time: '10:15',
status: 'read',
readBy: ['小李', '小王', '小张'],
sentAt: '2023-06-15T10:15:00Z'
},
{
id: '3',
sender: '健身教练',
content: '记得今晚7点来健身房,我们有新的训练计划',
time: '14:20',
status: 'delivered',
sentAt: '2023-06-15T14:20:00Z'
},
{
id: '4',
sender: '游泳队队长',
content: '明天上午9点游泳馆集合,准备下周比赛',
time: '16:45',
status: 'sent',
sentAt: '2023-06-15T16:45:00Z'
},
{
id: '5',
sender: '瑜伽老师',
content: '新课程安排已发布,请查收',
time: '18:30',
status: 'read',
readBy: ['小李'],
sentAt: '2023-06-15T18:30:00Z'
},
]);
const [selectedMessage, setSelectedMessage] = useState<Message | null>(null);
const [loading, setLoading] = useState(false);
const [stats, setStats] = useState({
total: 0,
read: 0,
delivered: 0,
sent: 0
});
// 计算统计数据
useEffect(() => {
const total = messages.length;
const read = messages.filter(m => m.status === 'read').length;
const delivered = messages.filter(m => m.status === 'delivered').length;
const sent = messages.filter(m => m.status === 'sent').length;
setStats({ total, read, delivered, sent });
}, [messages]);
const handleLongPress = (message: Message) => {
setSelectedMessage(message);
};
const closeReceiptDetail = () => {
setSelectedMessage(null);
};
const refreshMessages = () => {
setLoading(true);
setTimeout(() => {
setMessages(prev => [
...prev,
{
id: (prev.length + 1).toString(),
sender: '新朋友',
content: '你好!很高兴认识你',
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
status: 'sent',
sentAt: new Date().toISOString()
}
]);
setLoading(false);
}, 1000);
};
return (
<SafeAreaView style={styles.container}>
{/* 头部 */}
<View style={styles.header}>
<Text style={styles.title}>消息回执</Text>
<TouchableOpacity style={styles.refreshButton} onPress={refreshMessages}>
<Text style={styles.refreshText}>{ICONS.draft} 刷新</Text>
</TouchableOpacity>
</View>
{/* 统计卡片 */}
<View style={styles.statsContainer}>
<StatsCard title="总消息" value={stats.total} color="#3b82f6" />
<StatsCard title="已读" value={stats.read} color="#10b981" />
<StatsCard title="已送达" value={stats.delivered} color="#f59e0b" />
</View>
{/* 消息列表 */}
<View style={styles.content}>
{loading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#3b82f6" />
<Text style={styles.loadingText}>加载中...</Text>
</View>
) : (
<FlatList
data={messages}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<MessageItem
message={item}
onLongPress={handleLongPress}
/>
)}
showsVerticalScrollIndicator={false}
/>
)}
</View>
{/* 已读回执详情弹窗 */}
{selectedMessage && (
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>消息详情</Text>
<TouchableOpacity onPress={closeReceiptDetail}>
<Text style={styles.closeText}>×</Text>
</TouchableOpacity>
</View>
<ReadReceiptDetail message={selectedMessage} />
<View style={styles.modalActions}>
<TouchableOpacity
style={styles.modalActionButton}
onPress={() => Alert.alert('转发', `转发消息给其他联系人`)}
>
<Text style={styles.modalActionText}>{ICONS.message} 转发</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.modalActionButton}
onPress={() => Alert.alert('删除', `删除这条消息`)}
>
<Text style={styles.modalActionText}>{ICONS.delete} 删除</Text>
</TouchableOpacity>
</View>
</View>
</View>
)}
{/* 底部导航 */}
<View style={styles.bottomNav}>
<TouchableOpacity style={styles.navItem}>
<Text style={styles.navIcon}>{ICONS.message}</Text>
<Text style={styles.navText}>消息</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navItem}>
<Text style={styles.navIcon}>{ICONS.archive}</Text>
<Text style={styles.navText}>已读</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.navItem, styles.activeNavItem]}>
<Text style={styles.navIcon}>{ICONS.read}</Text>
<Text style={styles.navText}>回执</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.navItem}>
<Text style={styles.navIcon}>{ICONS.delete}</Text>
<Text style={styles.navText}>删除</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
backgroundColor: '#ffffff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#1e293b',
},
refreshButton: {
backgroundColor: '#3b82f6',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
},
refreshText: {
color: '#ffffff',
fontSize: 14,
fontWeight: '500',
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
padding: 16,
backgroundColor: '#ffffff',
marginBottom: 16,
},
statsCard: {
alignItems: 'center',
padding: 12,
backgroundColor: '#f8fafc',
borderRadius: 8,
width: (width - 48) / 3,
},
statsValue: {
fontSize: 18,
fontWeight: 'bold',
},
statsTitle: {
fontSize: 12,
color: '#64748b',
marginTop: 4,
},
content: {
flex: 1,
padding: 16,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 16,
color: '#64748b',
},
messageItem: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 12,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
messageHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
senderName: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
},
messageTime: {
fontSize: 12,
color: '#64748b',
},
messageContent: {
marginBottom: 8,
},
messageText: {
fontSize: 14,
color: '#334155',
lineHeight: 20,
},
messageFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
statusText: {
fontSize: 12,
fontWeight: '500',
},
moreButton: {
padding: 4,
},
moreText: {
fontSize: 16,
color: '#94a3b8',
},
receiptDetail: {
padding: 16,
},
receiptTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 8,
},
receiptSubtitle: {
fontSize: 14,
color: '#64748b',
marginBottom: 16,
},
readByTitle: {
fontSize: 16,
fontWeight: '500',
color: '#1e293b',
marginBottom: 8,
},
readUserItem: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
},
readUserIcon: {
fontSize: 20,
marginRight: 8,
},
readUserName: {
fontSize: 14,
color: '#334155',
},
noReadText: {
fontSize: 14,
color: '#64748b',
fontStyle: 'italic',
},
modalOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 100,
},
modalContent: {
backgroundColor: '#ffffff',
borderRadius: 12,
width: width * 0.8,
maxHeight: '70%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
modalTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
},
closeText: {
fontSize: 24,
color: '#94a3b8',
},
modalActions: {
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
},
modalActionButton: {
flex: 1,
padding: 16,
alignItems: 'center',
borderRightWidth: 1,
borderRightColor: '#e2e8f0',
},
modalActionButtonLast: {
borderRightWidth: 0,
},
modalActionText: {
fontSize: 14,
fontWeight: '500',
color: '#3b82f6',
},
bottomNav: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
paddingVertical: 12,
},
navItem: {
alignItems: 'center',
flex: 1,
},
activeNavItem: {
paddingBottom: 2,
borderBottomWidth: 2,
borderBottomColor: '#3b82f6',
},
navIcon: {
fontSize: 20,
color: '#94a3b8',
marginBottom: 4,
},
activeNavIcon: {
color: '#3b82f6',
},
navText: {
fontSize: 12,
color: '#94a3b8',
},
activeNavText: {
color: '#3b82f6',
fontWeight: '500',
},
});
export default SportMessageReceiptApp;
打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐



所有评论(0)