基于React Native鸿蒙跨平台通过setBooks([book, ...books])将新书添加到书籍列表头部(展开运算符...确保不可变数据更新),完成书籍列表会实时同步更新
本文介绍了一个基于React Native的阅读追踪应用,采用函数组件和useState Hook实现状态管理。应用包含书籍列表、详情页、添加书籍和统计功能,通过TypeScript接口确保数据结构一致性。核心设计包括: 使用useState管理5个关键状态(页面切换、书籍数据、详情页控制等) 采用Base64编码图标实现跨平台资源管理 布局使用标签页导航和Flexbox系统 数据处理包含书籍CR
ReadingTrackerApp 组件采用了现代 React 函数组件架构,结合 useState Hook 实现了复杂的状态管理。应用通过多个状态变量控制不同的 UI 状态:activeTab 管理当前激活的标签页,books 存储书籍列表,selectedBook 和 showBookDetail 控制书籍详情页的显示,newBook 管理添加新书的表单数据。
在类型定义上,使用了 TypeScript 的 Book 接口明确数据结构,包含书籍的各项属性如标题、作者、页数、当前页数、进度等。这种类型定义在跨端开发中尤为重要,确保了在不同平台上的数据结构一致性,减少了类型错误的可能性。
状态管理的设计考虑了应用的复杂性,通过集中管理状态变量,使得组件逻辑清晰,易于维护和扩展。特别是书籍数据的管理,通过 setBooks 函数实现了数据的增删改查,确保了数据的一致性。
图标系统
应用使用了 Base64 编码的图标资源,通过 ICONS_BASE64 对象集中管理。这种资源管理方式在跨端开发中具有明显优势:
- 减少网络请求:Base64 编码的图标直接嵌入代码中,无需额外的网络请求,提高了应用加载速度。
- 跨平台兼容:避免了不同平台对资源文件格式和路径的差异,确保了图标在所有平台上的一致显示。
- 代码简洁:通过对象集中管理图标,使得代码更加模块化,易于维护。
- 性能优化:减少了应用的打包体积,特别是在图标数量较多的情况下。
布局
应用采用了标签页导航结构,通过 activeTab 状态控制不同内容的显示。主要包含以下几个部分:
- 标签页导航:底部或顶部的标签页按钮,用于切换不同的功能模块。
- 主页:显示书籍列表,包括正在阅读和已完成的书籍。
- 添加书籍:表单页面,用于添加新的书籍。
- 统计页面:显示阅读统计数据,如总书籍数、已完成书籍数等。
- 个人页面:用户个人信息和设置。
- 书籍详情:显示选中书籍的详细信息和阅读进度。
布局设计上,应用使用了 SafeAreaView 确保在不同设备上的显示兼容性,使用 ScrollView 和 FlatList 确保在内容较长时可以滚动查看。视觉设计简洁明了,通过不同的样式区分不同的功能区域和状态。
用户体验
应用实现了丰富的交互功能,包括:
- 标签页切换:点击标签页按钮切换
activeTab状态,实现功能模块的切换。 - 书籍详情查看:点击书籍项打开书籍详情页,显示选中书籍的详细信息。
- 添加新书:填写表单并提交,添加新的书籍到阅读列表。
- 更新阅读进度:通过
updateProgress函数更新书籍的阅读进度。 - 统计数据计算:实时计算总书籍数、已完成书籍数、总页数等统计数据。
这些交互功能的实现遵循了 React 的最佳实践,通过状态更新驱动 UI 变化,确保了交互的一致性和可靠性。特别是书籍进度的更新,通过 Math.min(100, Math.round((newPage / book.pages) * 100)) 确保了进度值不会超过 100%,并自动将进度达到 100% 的书籍标记为已完成。
数据处理
应用实现了多个数据处理和计算函数,包括:
- 统计数据计算:计算总书籍数、已完成书籍数、进行中书籍数、总页数和已读页数。
- 添加新书:验证表单数据,创建新的书籍对象,并添加到书籍列表。
- 更新阅读进度:根据新的页码更新书籍的进度和状态。
这些数据处理函数确保了应用的数据一致性和准确性,特别是在添加新书时的表单验证,通过 if (!newBook.title || !newBook.author || !newBook.pages) 确保了必填字段的完整性。
在 React Native 与鸿蒙系统跨端开发中,该应用展现了多项兼容性设计:
-
基础组件选择:使用了
SafeAreaView、ScrollView、TouchableOpacity、TextInput、FlatList等基础组件,这些组件在 React Native 和鸿蒙系统中都有对应的实现。 -
样式管理:通过
StyleSheet.create管理样式,确保了在不同平台上的一致表现。 -
资源管理:使用 Base64 编码的图标资源,避免了不同平台对资源文件格式和路径的差异。
-
状态管理:使用
useStateHook 进行状态管理,在鸿蒙系统中可以通过相应的状态管理机制(如@State装饰器)实现类似功能。 -
类型定义:使用 TypeScript 类型定义,确保了在不同平台上的数据结构一致性。
-
布局系统:使用了 Flexbox 布局系统,这是 React Native 和鸿蒙系统都支持的布局方式,确保了在不同平台上的一致布局效果。
-
API 兼容性:使用了
Alert.alert等跨平台 API,确保了在不同平台上的一致表现。
该阅读追踪应用展示了一个功能完整、设计优雅的 React Native 应用实现,涵盖了状态管理、资源管理、布局设计、交互处理等多个方面的技术点。通过合理的组件架构和状态管理,以及对跨端兼容性的考虑,该应用不仅在 React Native 环境下运行良好,也为后续的鸿蒙系统适配奠定了基础。
阅读追踪APP的核心交互,本质是状态的更新与联动——页面切换、书籍数据更新、表单输入、详情页展示,均依赖状态管理。代码片段中,未引入任何第三方状态管理库(如Redux、MobX),而是采用React原生的useState钩子实现全量状态管理,这种轻量型方案既简化了跨端开发的复杂度,又确保了状态管理逻辑在鸿蒙系统中的兼容性。我们来看核心状态定义与实现代码:
const ReadingTrackerApp = () => {
// 底部导航页面切换状态:首页、添加书籍、统计、个人中心
const [activeTab, setActiveTab] = useState<'home' | 'add' | 'stats' | 'profile'>('home');
// 书籍列表核心数据状态
const [books, setBooks] = useState<Book[]>([
{
id: '1',
title: '活着',
author: '余华',
pages: 191,
currentPage: 120,
progress: 63,
startDate: '2023-05-15',
targetDate: '2023-06-15',
category: '小说',
isCompleted: false,
description: '一部讲述人生苦难与坚韧的小说...',
rating: 4.8
},
// 其余书籍数据省略...
]);
// 选中书籍状态(用于详情页展示)
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
// 详情页显示/隐藏状态
const [showBookDetail, setShowBookDetail] = useState(false);
// 新增书籍表单输入状态
const [newBook, setNewBook] = useState({
title: '',
author: '',
pages: '',
category: '小说',
description: ''
});
// 计算总统计数据(依赖books状态,自动联动更新)
const totalBooks = books.length;
const completedBooks = books.filter(book => book.isCompleted).length;
const inProgressBooks = books.filter(book => !book.isCompleted).length;
const totalPages = books.reduce((sum, book) => sum + book.pages, 0);
const totalReadPages = books.reduce((sum, book) => sum + book.currentPage, 0);
// 其余方法省略...
};
这段代码是APP的核心状态中枢,涵盖5个关键状态,每个状态的设计都兼顾了跨端兼容性与业务需求,我们逐一对接代码片段,拆解其实现逻辑与鸿蒙适配细节:
-
页面切换状态(activeTab):用于管理底部导航对应的四大核心页面,其类型定义采用TypeScript联合类型(‘home’ | ‘add’ | ‘stats’ | ‘profile’),确保状态值的类型安全,避免因类型错误导致的鸿蒙设备上页面切换异常。在鸿蒙系统中,setActiveTab(状态更新函数)通过适配层映射为鸿蒙原生的状态更新机制,页面切换时的渲染性能与其他平台保持一致——比如点击底部“添加”导航项,setActiveTab(‘add’)触发状态更新,APP会自动渲染添加书籍页面,无卡顿、无延迟,同时导航项的高亮反馈(通过activeTab === 'add’判断样式),鸿蒙系统可正常解析,确保交互一致性。
-
书籍列表状态(books):这是APP的核心数据载体,初始值为4本示例书籍,类型为Book[](严格遵循前文定义的Book接口)。该状态的核心价值在于“跨端数据联动”,后续的添加新书、更新阅读进度、标记书籍完成等操作,本质都是通过setBooks更新该状态,进而驱动UI渲染。这里重点解读其鸿蒙适配细节:setBooks的更新逻辑采用不可变数据更新方式(通过展开运算符…实现,如后续addNewBook方法中的setBooks([book, …books])),这种方式符合React最佳实践,也完全兼容鸿蒙系统的状态更新机制——鸿蒙适配层对RN的不可变数据更新有良好的支持,能够准确捕捉状态变化,驱动UI同步更新,避免出现“数据更新但UI未变化”的鸿蒙适配bug。同时,books状态中的progress(阅读进度)、isCompleted(是否完成)等属性,会直接影响统计数据(totalBooks、completedBooks等),这种依赖状态的计算逻辑,在鸿蒙系统中会自动联动更新,无需额外编写适配代码。
-
详情页相关状态(selectedBook、showBookDetail):这两个状态联动实现书籍详情页的展示与隐藏。selectedBook用于存储当前点击的书籍数据(类型为Book | null),showBookDetail用于控制详情页的显示/隐藏(boolean类型)。在鸿蒙系统中,这两个状态的更新逻辑完全兼容——比如点击某本进行中的书籍,setSelectedBook(item)存储当前书籍数据,setShowBookDetail(true)显示详情页;点击详情页“返回”按钮,setShowBookDetail(false)、setSelectedBook(null)重置状态,关闭详情页,整个交互流程与iOS、Android端完全一致,无任何适配差异。
-
表单输入状态(newBook):用于管理添加书籍页面的表单输入数据,状态结构与表单字段一一对应,其中title、author、pages、description为字符串类型,category为默认值为“小说”的字符串类型。该状态的核心适配点在于“表单输入与状态同步”——后续TextInput组件的onChangeText事件,会实时更新该状态(如title输入框的onChangeText={(text) => setNewBook({…newBook, title: text})}),在鸿蒙系统中,TextInput的输入事件与状态更新逻辑完全兼容,无论是普通文本输入(书名、作者)、数字输入(页数),还是多行输入(简介),都能实现“输入即更新”,响应流畅,同时表单分类选择的状态更新(setNewBook({…newBook, category})),也能正常驱动UI反馈(选中分类高亮)。
最后补充:代码中通过books状态计算的统计数据(totalBooks、completedBooks等),属于“派生状态”,这种写法无需额外引入useMemo钩子(中小型应用可简化),在鸿蒙设备上不会出现性能问题,因为这些计算逻辑简单,适配层能够高效处理,同时也减少了代码复杂度,符合“轻量跨端”的开发思路。
状态管理是基础,核心功能的方法封装则是APP落地的关键。代码片段中封装了addNewBook(添加新书)、updateProgress(更新阅读进度)、showBookDetails(显示详情页)、closeBookDetail(关闭详情页)4个核心方法,所有方法的封装均遵循“通用化、无平台依赖”原则,确保在鸿蒙系统中可直接复用。我们逐一看代码片段与技术解读:
4.1 添加新书:
// 添加新书
const addNewBook = () => {
if (!newBook.title || !newBook.author || !newBook.pages) {
Alert.alert('错误', '请填写完整的书籍信息');
return;
}
const book: Book = {
id: Date.now().toString(),
title: newBook.title,
author: newBook.author,
pages: parseInt(newBook.pages),
currentPage: 0,
progress: 0,
startDate: new Date().toISOString().split('T')[0],
targetDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 默认30天后
category: newBook.category,
isCompleted: false,
description: newBook.description,
rating: 0
};
setBooks([book, ...books]);
setNewBook({ title: '', author: '', pages: '', category: '小说', description: '' });
Alert.alert('成功', '书籍已添加到阅读列表');
};
这段代码实现了“添加新书”的全流程逻辑,从表单校验、书籍对象创建,到状态更新、弹窗提示,每一步都兼顾了业务逻辑与鸿蒙跨端适配,核心解读如下:
-
表单校验与Alert组件适配:方法开篇通过简单的非空校验,判断书名、作者、页数是否填写完整,若未填写,通过Alert.alert弹出错误提示。在鸿蒙系统中,Alert组件会被适配层渲染为鸿蒙原生弹窗样式(而非RN模拟弹窗),既贴合鸿蒙系统的原生视觉风格,又确保弹窗的交互流畅性(如点击确认按钮关闭弹窗),其API用法与RN原生完全一致,无需额外适配。
-
书籍对象创建的类型规范:创建新书籍对象时,严格遵循Book接口类型,其中pages属性通过parseInt(newBook.pages)转为number类型(因为newBook.pages是表单输入的字符串类型),避免类型错误导致的鸿蒙适配bug。同时,startDate(开始日期)采用new Date().toISOString().split(‘T’)[0]获取当前日期(格式为YYYY-MM-DD),targetDate(目标日期)默认设置为30天后,这种日期处理方式是跨端通用的,鸿蒙系统对RN的Date API完全兼容,无需担心日期格式异常。
-
状态更新与表单重置:通过setBooks([book, …books])将新书添加到书籍列表头部(展开运算符…确保不可变数据更新),该逻辑在鸿蒙系统中可正常运行,书籍列表会实时同步更新,显示新增的书籍;setNewBook重置表单输入状态,确保添加完成后表单清空,为下一次添加做准备,其状态更新逻辑完全兼容鸿蒙系统;最后通过Alert.alert弹出添加成功提示,与错误提示一致,适配鸿蒙原生弹窗。
关键注意点:newBook.pages是表单输入的字符串类型,必须转为number类型才能符合Book接口的pages属性要求,否则TypeScript会报错,同时在鸿蒙设备上,可能会导致后续阅读进度计算(progress = (currentPage / pages) * 100)出现NaN异常,这是跨端开发中容易忽略的细节。
4.2 更新阅读进度:
// 更新阅读进度
const updateProgress = (bookId: string, newPage: number) => {
setBooks(books.map(book => {
if (book.id === bookId) {
const progress = Math.min(100, Math.round((newPage / book.pages) * 100));
const isCompleted = progress === 100;
return {
...book,
currentPage: newPage,
progress,
isCompleted
};
}
return book;
}));
};
这是APP的核心交互方法之一,用于更新指定书籍的阅读进度,同时自动标记书籍是否完成,其逻辑设计充分考虑了跨端兼容性,核心解读如下:
-
进度计算的跨端通用性:通过(newPage / book.pages) * 100计算阅读进度,Math.round用于取整,Math.min(100, …)确保进度不会超过100%(避免newPage超过书籍总页数时,进度显示异常)。这种计算逻辑是纯JavaScript逻辑,无任何平台依赖,在鸿蒙系统中可正常运行,无论是整数页数还是小数页数(实际开发中页数均为整数),都能准确计算进度。
-
不可变数据更新的适配:通过books.map遍历书籍列表,找到对应id的书籍,通过展开运算符…复制原书籍对象,再更新currentPage(当前页数)、progress(阅读进度)、isCompleted(是否完成)属性,最后通过setBooks更新状态。这种不可变数据更新方式,符合React最佳实践,也完全兼容鸿蒙系统的状态更新机制——鸿蒙适配层能够准确捕捉到数组中单个书籍对象的变化,仅重新渲染该书籍对应的UI(如书籍卡片的进度条),而非重新渲染整个列表,确保鸿蒙设备上的渲染性能。
-
状态联动的自动适配:isCompleted属性由progress === 100判断,当进度达到100%时,自动标记书籍为已完成,该状态变化会自动联动更新completedBooks、inProgressBooks等统计数据,同时联动更新书籍列表的UI(已完成书籍卡片样式变化),整个联动过程无需额外编写适配代码,鸿蒙系统会自动完成渲染更新。
4.3 详情页控制:
// 显示书籍详情
const showBookDetails = (book: Book) => {
setSelectedBook(book);
setShowBookDetail(true);
};
// 关闭书籍详情
const closeBookDetail = () => {
setShowBookDetail(false);
setSelectedBook(null);
};
这两个方法实现了详情页的显示与关闭,逻辑简洁,且完全兼容鸿蒙系统,核心解读如下:
-
数据传递的跨端通用性:showBookDetails方法接收Book类型的参数(当前点击的书籍对象),通过setSelectedBook(book)存储该书籍数据,为详情页渲染提供数据源。这种数据传递方式是RN组件间通信的常用方式,无任何平台依赖,鸿蒙系统中可正常传递复杂对象数据,无需担心数据丢失或类型转换异常。
-
状态联动的适配:showBookDetails方法同时更新两个状态(selectedBook、showBookDetail),鸿蒙系统支持RN的批量状态更新机制,会一次性完成两个状态的更新,避免出现“详情页显示但无数据”或“有数据但详情页未显示”的异常;closeBookDetail方法重置两个状态,确保详情页关闭后,数据与UI完全重置,为下一次点击详情页做准备,其状态更新逻辑在鸿蒙设备上无任何延迟。
五、组件渲染:
组件化是RN跨端开发的核心优势,代码片段中采用“页面级组件拆分+业务组件复用”的方式,将四大核心页面与详情页分别封装为独立的渲染函数,所有组件的渲染逻辑均遵循RN通用规范,同时结合鸿蒙系统的渲染特性,进行了细微优化,确保在鸿蒙设备上的适配性与流畅性。我们以核心渲染函数为例,结合代码片段解读:
5.1 主渲染逻辑:
return (
<SafeAreaView style={styles.container}>
{/* 头部 */}
<View style={styles.header}>
<Text style={styles.title}>看书管理记录</Text>
<TouchableOpacity onPress={() => Alert.alert('搜索', '搜索功能')}>
<Text style={styles.searchIcon}>🔍</Text>
</TouchableOpacity>
</View>
{/* 内容区域:根据showBookDetail和activeTab切换渲染内容 */}
{!showBookDetail ? (
<>
{activeTab === 'home' && renderHomeContent()}
{activeTab === 'add' && renderAddBookContent()}
{activeTab === 'stats' && renderStatsContent()}
{activeTab === 'profile' && renderProfileContent()}
</>
) : (
renderBookDetail()
)}
{/* 底部导航:详情页隐藏 */}
{!showBookDetail && (
<View style={styles.bottomNav}>
{/* 导航项省略... */}
</View>
)}
</SafeAreaView>
);
这段代码是APP的主渲染入口,核心逻辑是“根据状态切换渲染内容”,其鸿蒙适配细节主要体现在3个方面:
-
根组件的适配:根组件采用SafeAreaView,如前文所述,鸿蒙适配层会自动适配设备安全区域,确保头部标题不会被刘海屏遮挡;外层容器style={styles.container}设置flex: 1,占据整个屏幕空间,鸿蒙系统完全支持RN的flex布局,能够正常解析容器的布局逻辑,确保页面充满屏幕。
-
条件渲染的适配:通过{!showBookDetail ? (…) : (…)}判断渲染首页/添加页面/统计页面/个人中心,还是渲染详情页;通过{activeTab === ‘home’ && renderHomeContent()}根据activeTab状态切换四大核心页面。这种条件渲染逻辑是RN通用写法,鸿蒙系统可正常解析,页面切换时的渲染流畅,无卡顿、无闪烁,同时详情页显示时隐藏底部导航({!showBookDetail && (…)}),这种UI联动逻辑也完全兼容鸿蒙系统。
-
底部导航的适配:底部导航容器style={styles.bottomNav}采用flex横向布局,导航项均匀分布,鸿蒙系统可正常解析flex布局,确保导航项在不同尺寸的鸿蒙设备上均匀排列;导航项采用TouchableOpacity组件,点击事件(setActiveTab)在鸿蒙系统中可正常响应,同时导航项的高亮样式(根据activeTab判断),也能正常渲染,贴合鸿蒙系统的原生交互风格。
5.2 首页渲染:
首页是APP的核心展示页面,包含阅读概览卡片、快速操作按钮、进行中书籍列表、已完成书籍列表四大模块,代码片段中的renderHomeContent方法封装了其完整渲染逻辑,我们重点解读列表渲染的鸿蒙适配细节:
// 渲染主页内容
const renderHomeContent = () => (
<ScrollView style={styles.content}>
{/* 今日概览卡片、快速操作按钮省略... */}
{/* 进行中的书籍 */}
<Text style={styles.sectionTitle}>进行中的书籍</Text>
<FlatList
data={books.filter(book => !book.isCompleted)}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.bookItem}
onPress={() => showBookDetails(item)}
>
<View style={styles.bookIcon}>
<Text style={styles.bookIconText}>📖</Text>
</View>
<View style={styles.bookInfo}>
<Text style={styles.bookTitle}>{item.title}</Text>
<Text style={styles.bookAuthor}>作者: {item.author}</Text>
<View style={styles.progressContainer}>
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${item.progress}%` }]} />
</View>
<Text style={styles.progressText}>{item.progress}%</Text>
</View>
{/* 其余信息省略... */}
</View>
<TouchableOpacity
style={styles.updateButton}
onPress={(e) => {
e.stopPropagation(); // 防止触发外层点击事件
const newPage = item.currentPage + 10;
updateProgress(item.id, newPage > item.pages ? item.pages : newPage);
}}
>
<Text style={styles.updateButtonText}>+10页</Text>
</TouchableOpacity>
</TouchableOpacity>
)}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
/>
{/* 已完成的书籍省略... */}
</ScrollView>
);
这段代码的核心是FlatList组件的使用,以及书籍卡片的渲染,其鸿蒙适配细节如下:
-
FlatList的鸿蒙适配:FlatList的data属性为过滤后的进行中书籍列表(books.filter(book => !book.isCompleted)),renderItem属性渲染单个书籍卡片,keyExtractor属性指定item.id为唯一标识(避免列表渲染异常),showsVerticalScrollIndicator={false}隐藏滚动条。这些属性在鸿蒙系统中均完全生效,适配层会将FlatList映射为鸿蒙原生的ListContainer组件,实现高效列表渲染,同时支持下拉刷新、上拉加载(本次代码未实现,可直接扩展),无需额外适配。
-
书籍卡片的交互适配:书籍卡片采用TouchableOpacity组件,点击卡片触发showBookDetails(item)方法,跳转详情页;卡片内部的“+10页”按钮,也采用TouchableOpacity组件,点击触发进度更新,同时通过e.stopPropagation()阻止事件冒泡(避免点击按钮时同时触发卡片点击事件)。在鸿蒙系统中,TouchableOpacity的点击反馈(透明度变化)与其他平台保持一致,事件冒泡机制也完全兼容,无需额外处理。
-
进度条的动态适配:进度条由两个View组件实现(外层progressBar为进度条背景,内层progressFill为进度填充),其中progressFill的宽度通过{ width:
${item.progress}%}动态设置,与阅读进度联动。这种动态样式绑定逻辑,鸿蒙系统可正常解析,进度更新时,进度条会平滑变化,无卡顿,同时进度条的圆角、背景色等样式(通过StyleSheet定义),也能正常渲染,与其他平台保持视觉一致。
六、样式设计:
样式设计是RN鸿蒙跨端开发中最容易出现平台差异的环节,代码片段中通过StyleSheet.create定义所有样式,遵循“动态尺寸+全局规范+鸿蒙特性适配”的原则,确保样式在鸿蒙设备上的正常渲染与视觉一致性。我们结合代码片段中的样式定义,解读其跨端适配技巧:
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',
},
// 其余样式省略...
bookItem: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 12,
flexDirection: 'row',
alignItems: 'center',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
progressBar: {
flex: 1,
height: 8,
backgroundColor: '#e2e8f0',
borderRadius: 4,
marginRight: 8,
},
progressFill: {
height: '100%',
backgroundColor: '#3b82f6',
borderRadius: 4,
},
});
核心适配解读如下:
-
全局样式规范:代码中定义了统一的颜色规范(主色调#3b82f6、背景色#f8fafc、#ffffff等)、圆角规范(卡片borderRadius: 12、按钮borderRadius: 8、进度条borderRadius: 4),所有组件的样式均遵循该规范,鸿蒙系统与其他平台一样,支持RGB颜色值与圆角样式,确保跨端视觉风格一致。
-
flex布局的全量使用:所有组件的布局均采用flex布局(flexDirection、justifyContent、alignItems等属性),鸿蒙系统完全支持RN的flex布局语法,能够正常解析组件的排列逻辑——比如header组件的flexDirection: ‘row’(横向排列)、justifyContent: ‘space-between’(两端对齐),bookItem组件的flexDirection: ‘row’,均能在鸿蒙设备上正常渲染,确保组件排列合理。
-
阴影效果的跨端适配:这是鸿蒙样式适配的重点,RN在iOS端通过shadowColor、shadowOffset、shadowOpacity、shadowRadius四个属性实现阴影,而鸿蒙系统与Android系统一样,通过elevation属性实现阴影。代码中所有需要添加阴影的组件(如bookItem、overviewCard),均同时定义了iOS端阴影属性和elevation属性,确保阴影效果在鸿蒙设备上正常显示,且与iOS端保持一致——比如bookItem组件的elevation: 2,对应iOS端的阴影效果,适配层会自动识别平台,渲染对应的阴影样式,避免出现“鸿蒙设备无阴影”或“阴影样式异常”的问题。
-
动态样式的适配:代码中通过动态绑定样式(如progressFill的width:
${item.progress}%),实现进度条的动态变化,这种动态样式绑定逻辑,鸿蒙系统可正常解析,样式会随着状态更新实时变化;同时,结合前文提到的Dimensions API获取屏幕宽度,实现动态尺寸适配(如快速操作按钮的width: width / 4.5),确保样式在不同尺寸的鸿蒙设备上均能正常显示。
通过对阅读追踪APP代码片段的逐行深度解读,我们可以总结出RN鸿蒙跨端开发的核心逻辑:以RN官方通用组件与API为基础,以TypeScript规范数据类型,以useState钩子实现轻量状态管理,以组件化封装提升复用性,以StyleSheet的平台适配优化确保视觉与交互一致性,借助鸿蒙系统的RN适配层,实现“一套代码,多端流畅运行”。
真实演示案例代码:
// app.tsx
import React, { useState } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, TextInput, FlatList } from 'react-native';
// Base64 图标库
const ICONS_BASE64 = {
home: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
book: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
reading: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
history: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
stats: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
profile: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
plus: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
check: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};
const { width } = Dimensions.get('window');
// 书籍类型
type Book = {
id: string;
title: string;
author: string;
pages: number;
currentPage: number;
progress: number;
startDate: string;
targetDate: string;
category: string;
isCompleted: boolean;
description: string;
rating: number;
};
const ReadingTrackerApp = () => {
const [activeTab, setActiveTab] = useState<'home' | 'add' | 'stats' | 'profile'>('home');
const [books, setBooks] = useState<Book[]>([
{
id: '1',
title: '活着',
author: '余华',
pages: 191,
currentPage: 120,
progress: 63,
startDate: '2023-05-15',
targetDate: '2023-06-15',
category: '小说',
isCompleted: false,
description: '一部讲述人生苦难与坚韧的小说,通过福贵的人生经历展现了生命的顽强与希望。',
rating: 4.8
},
{
id: '2',
title: '百年孤独',
author: '加西亚·马尔克斯',
pages: 368,
currentPage: 200,
progress: 54,
startDate: '2023-05-10',
targetDate: '2023-07-10',
category: '文学',
isCompleted: false,
description: '魔幻现实主义的经典之作,讲述了布恩迪亚家族七代人的传奇故事。',
rating: 4.7
},
{
id: '3',
title: '人类简史',
author: '尤瓦尔·赫拉利',
pages: 443,
currentPage: 300,
progress: 68,
startDate: '2023-05-01',
targetDate: '2023-08-01',
category: '历史',
isCompleted: false,
description: '从认知革命到今天,人类如何成为地球的主宰,又将走向何方?',
rating: 4.6
},
{
id: '4',
title: '高效能人士的七个习惯',
author: '史蒂芬·柯维',
pages: 381,
currentPage: 381,
progress: 100,
startDate: '2023-04-01',
targetDate: '2023-04-30',
category: '自我提升',
isCompleted: true,
description: '全球畅销30年的经典著作,教你如何成为高效能人士,实现个人和事业的成功。',
rating: 4.9
},
]);
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
const [showBookDetail, setShowBookDetail] = useState(false);
const [newBook, setNewBook] = useState({
title: '',
author: '',
pages: '',
category: '小说',
description: ''
});
// 计算总统计数据
const totalBooks = books.length;
const completedBooks = books.filter(book => book.isCompleted).length;
const inProgressBooks = books.filter(book => !book.isCompleted).length;
const totalPages = books.reduce((sum, book) => sum + book.pages, 0);
const totalReadPages = books.reduce((sum, book) => sum + book.currentPage, 0);
// 添加新书
const addNewBook = () => {
if (!newBook.title || !newBook.author || !newBook.pages) {
Alert.alert('错误', '请填写完整的书籍信息');
return;
}
const book: Book = {
id: Date.now().toString(),
title: newBook.title,
author: newBook.author,
pages: parseInt(newBook.pages),
currentPage: 0,
progress: 0,
startDate: new Date().toISOString().split('T')[0],
targetDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 默认30天后
category: newBook.category,
isCompleted: false,
description: newBook.description,
rating: 0
};
setBooks([book, ...books]);
setNewBook({ title: '', author: '', pages: '', category: '小说', description: '' });
Alert.alert('成功', '书籍已添加到阅读列表');
};
// 更新阅读进度
const updateProgress = (bookId: string, newPage: number) => {
setBooks(books.map(book => {
if (book.id === bookId) {
const progress = Math.min(100, Math.round((newPage / book.pages) * 100));
const isCompleted = progress === 100;
return {
...book,
currentPage: newPage,
progress,
isCompleted
};
}
return book;
}));
};
// 显示书籍详情
const showBookDetails = (book: Book) => {
setSelectedBook(book);
setShowBookDetail(true);
};
// 关闭书籍详情
const closeBookDetail = () => {
setShowBookDetail(false);
setSelectedBook(null);
};
// 渲染书籍详情页面
const renderBookDetail = () => (
<View style={styles.detailContainer}>
<View style={styles.detailHeader}>
<TouchableOpacity onPress={closeBookDetail}>
<Text style={styles.backButton}>← 返回</Text>
</TouchableOpacity>
<Text style={styles.detailTitle}>{selectedBook?.title}</Text>
</View>
<ScrollView style={styles.detailContent}>
<View style={styles.detailCard}>
<Text style={styles.detailBookTitle}>{selectedBook?.title}</Text>
<Text style={styles.detailAuthor}>作者: {selectedBook?.author}</Text>
<Text style={styles.detailCategory}>分类: {selectedBook?.category}</Text>
<Text style={styles.detailRating}>评分: ⭐ {selectedBook?.rating}</Text>
<View style={styles.progressContainer}>
<Text style={styles.progressLabel}>阅读进度</Text>
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${selectedBook?.progress}%` }]} />
</View>
<Text style={styles.progressText}>{selectedBook?.progress}% ({selectedBook?.currentPage}/{selectedBook?.pages}页)</Text>
</View>
<Text style={styles.detailSectionTitle}>简介</Text>
<Text style={styles.detailDescription}>{selectedBook?.description}</Text>
<Text style={styles.detailSectionTitle}>阅读计划</Text>
<Text style={styles.detailPlan}>
开始日期: {selectedBook?.startDate}
{'\n'}目标完成: {selectedBook?.targetDate}
</Text>
<View style={styles.detailActions}>
<TouchableOpacity
style={styles.detailButton}
onPress={() => {
if (selectedBook) {
const newPage = selectedBook.currentPage + 10;
updateProgress(selectedBook.id, newPage > selectedBook.pages ? selectedBook.pages : newPage);
Alert.alert('进度更新', `已更新到第 ${Math.min(newPage, selectedBook.pages)} 页`);
}
}}
>
<Text style={styles.detailButtonText}>+10页</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.detailButton}
onPress={() => {
if (selectedBook) {
const newPage = selectedBook.currentPage + 20;
updateProgress(selectedBook.id, newPage > selectedBook.pages ? selectedBook.pages : newPage);
Alert.alert('进度更新', `已更新到第 ${Math.min(newPage, selectedBook.pages)} 页`);
}
}}
>
<Text style={styles.detailButtonText}>+20页</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.detailButton}
onPress={() => {
if (selectedBook) {
updateProgress(selectedBook.id, selectedBook.pages);
Alert.alert('恭喜', '已完成此书!');
}
}}
>
<Text style={styles.detailButtonText}>完成</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
</View>
);
// 渲染主页内容
const renderHomeContent = () => (
<ScrollView style={styles.content}>
{/* 今日概览卡片 */}
<View style={styles.overviewCard}>
<Text style={styles.overviewTitle}>阅读概览</Text>
<View style={styles.statsRow}>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{totalBooks}</Text>
<Text style={styles.statLabel}>本书籍</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{completedBooks}</Text>
<Text style={styles.statLabel}>本完成</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{inProgressBooks}</Text>
<Text style={styles.statLabel}>进行中</Text>
</View>
</View>
</View>
{/* 快速操作按钮 */}
<View style={styles.quickActions}>
<TouchableOpacity style={styles.quickAction} onPress={() => setActiveTab('add')}>
<Text style={styles.quickActionIcon}>📚</Text>
<Text style={styles.quickActionText}>添加书籍</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.quickAction} onPress={() => Alert.alert('阅读目标', '设置每日阅读目标')}>
<Text style={styles.quickActionIcon}>🎯</Text>
<Text style={styles.quickActionText}>阅读目标</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.quickAction} onPress={() => Alert.alert('读书笔记', '查看我的读书笔记')}>
<Text style={styles.quickActionIcon}>📝</Text>
<Text style={styles.quickActionText}>读书笔记</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.quickAction} onPress={() => Alert.alert('推荐书籍', '查看推荐书籍')}>
<Text style={styles.quickActionIcon}>💡</Text>
<Text style={styles.quickActionText}>推荐书籍</Text>
</TouchableOpacity>
</View>
{/* 进行中的书籍 */}
<Text style={styles.sectionTitle}>进行中的书籍</Text>
<FlatList
data={books.filter(book => !book.isCompleted)}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.bookItem}
onPress={() => showBookDetails(item)}
>
<View style={styles.bookIcon}>
<Text style={styles.bookIconText}>📖</Text>
</View>
<View style={styles.bookInfo}>
<Text style={styles.bookTitle}>{item.title}</Text>
<Text style={styles.bookAuthor}>作者: {item.author}</Text>
<View style={styles.progressContainer}>
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${item.progress}%` }]} />
</View>
<Text style={styles.progressText}>{item.progress}%</Text>
</View>
<Text style={styles.bookDetails}>
{item.currentPage}/{item.pages}页 • {item.category}
</Text>
</View>
<TouchableOpacity
style={styles.updateButton}
onPress={(e) => {
e.stopPropagation(); // 防止触发外层点击事件
const newPage = item.currentPage + 10;
updateProgress(item.id, newPage > item.pages ? item.pages : newPage);
}}
>
<Text style={styles.updateButtonText}>+10页</Text>
</TouchableOpacity>
</TouchableOpacity>
)}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
/>
{/* 已完成的书籍 */}
<Text style={styles.sectionTitle}>已完成的书籍</Text>
<FlatList
data={books.filter(book => book.isCompleted)}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.completedBookItem}
onPress={() => showBookDetails(item)}
>
<View style={styles.bookIcon}>
<Text style={styles.bookIconText}>✅</Text>
</View>
<View style={styles.bookInfo}>
<Text style={styles.bookTitle}>{item.title}</Text>
<Text style={styles.bookAuthor}>作者: {item.author}</Text>
<Text style={styles.completedText}>已完成 • {item.startDate} 开始 • {item.targetDate} 完成</Text>
</View>
</TouchableOpacity>
)}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
/>
</ScrollView>
);
// 渲染添加书籍页面
const renderAddBookContent = () => (
<ScrollView style={styles.content}>
<Text style={styles.sectionTitle}>添加新书籍</Text>
<View style={styles.formCard}>
{/* 书籍标题 */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>书籍标题</Text>
<TextInput
style={styles.inputField}
value={newBook.title}
onChangeText={(text) => setNewBook({...newBook, title: text})}
placeholder="请输入书籍标题"
keyboardType="default"
/>
</View>
{/* 作者 */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>作者</Text>
<TextInput
style={styles.inputField}
value={newBook.author}
onChangeText={(text) => setNewBook({...newBook, author: text})}
placeholder="请输入作者姓名"
keyboardType="default"
/>
</View>
{/* 页数 */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>总页数</Text>
<TextInput
style={styles.inputField}
value={newBook.pages}
onChangeText={(text) => setNewBook({...newBook, pages: text})}
placeholder="请输入总页数"
keyboardType="numeric"
/>
</View>
{/* 分类 */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>分类</Text>
<View style={styles.pickerContainer}>
{['小说', '文学', '历史', '自我提升', '科幻', '其他'].map(category => (
<TouchableOpacity
key={category}
style={[
styles.pickerOption,
newBook.category === category && styles.pickerOptionSelected
]}
onPress={() => setNewBook({...newBook, category})}
>
<Text style={styles.pickerOptionText}>{category}</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* 简介 */}
<View style={styles.inputGroup}>
<Text style={styles.inputLabel}>简介</Text>
<TextInput
style={[styles.inputField, styles.textArea]}
value={newBook.description}
onChangeText={(text) => setNewBook({...newBook, description: text})}
placeholder="简要描述书籍内容"
multiline
/>
</View>
{/* 提交按钮 */}
<TouchableOpacity style={styles.submitButton} onPress={addNewBook}>
<Text style={styles.submitButtonText}>添加书籍</Text>
</TouchableOpacity>
</View>
{/* 阅读建议卡片 */}
<View style={styles.suggestionCard}>
<Text style={styles.suggestionTitle}>阅读建议</Text>
<Text style={styles.suggestionText}>• 每天坚持阅读30分钟</Text>
<Text style={styles.suggestionText}>• 做好读书笔记加深理解</Text>
<Text style={styles.suggestionText}>• 设定合理的阅读目标</Text>
<Text style={styles.suggestionText}>• 选择适合自己的阅读环境</Text>
<Text style={styles.suggestionText}>• 定期回顾已完成的书籍</Text>
</View>
</ScrollView>
);
// 渲染统计分析页面
const renderStatsContent = () => (
<ScrollView style={styles.content}>
{/* 统计概览卡片 */}
<View style={styles.statsOverviewCard}>
<Text style={styles.overviewTitle}>阅读统计</Text>
<View style={styles.statsRow}>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{totalBooks}</Text>
<Text style={styles.statLabel}>总书籍</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{totalPages}</Text>
<Text style={styles.statLabel}>总页数</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{totalReadPages}</Text>
<Text style={styles.statLabel}>已读页数</Text>
</View>
</View>
</View>
{/* 完成率卡片 */}
<View style={styles.rateCard}>
<Text style={styles.rateTitle}>完成率</Text>
<View style={styles.rateRow}>
<Text style={styles.rateLabel}>已完成</Text>
<Text style={styles.rateValue}>{completedBooks}</Text>
</View>
<View style={styles.rateRow}>
<Text style={styles.rateLabel}>进行中</Text>
<Text style={styles.rateValue}>{inProgressBooks}</Text>
</View>
<View style={styles.rateRow}>
<Text style={styles.rateLabel}>完成比例</Text>
<Text style={styles.rateValue}>{totalBooks > 0 ? Math.round((completedBooks / totalBooks) * 100) : 0}%</Text>
</View>
</View>
{/* 类别分布 */}
<Text style={styles.sectionTitle}>类别分布</Text>
<View style={styles.categoryStats}>
{['小说', '文学', '历史', '自我提升', '科幻', '其他'].map(cat => {
const count = books.filter(b => b.category === cat).length;
if (count === 0) return null;
return (
<View key={cat} style={styles.categoryItem}>
<Text style={styles.categoryName}>{cat}</Text>
<Text style={styles.categoryCount}>{count} 本</Text>
</View>
);
})}
</View>
{/* 阅读进度卡片 */}
<Text style={styles.sectionTitle}>阅读进度</Text>
<View style={styles.progressCard}>
<Text style={styles.progressTitle}>总进度</Text>
<View style={styles.progressBarLarge}>
<View
style={[styles.progressFillLarge, {
width: `${totalPages > 0 ? (totalReadPages / totalPages) * 100 : 0}%`
}]}
/>
</View>
<Text style={styles.progressTextLarge}>
{totalPages > 0 ? Math.round((totalReadPages / totalPages) * 100) : 0}% ({totalReadPages}/{totalPages}页)
</Text>
</View>
{/* 阅读时间统计 */}
<Text style={styles.sectionTitle}>阅读时间统计</Text>
<View style={styles.timeStatsCard}>
<View style={styles.timeStatItem}>
<Text style={styles.timeStatLabel}>平均每日阅读页数</Text>
<Text style={styles.timeStatValue}>15页</Text>
</View>
<View style={styles.timeStatItem}>
<Text style={styles.timeStatLabel}>平均完成一本书所需时间</Text>
<Text style={styles.timeStatValue}>12天</Text>
</View>
<View style={styles.timeStatItem}>
<Text style={styles.timeStatLabel}>最常阅读时间段</Text>
<Text style={styles.timeStatValue}>晚上8-10点</Text>
</View>
</View>
</ScrollView>
);
// 渲染个人资料页面
const renderProfileContent = () => (
<ScrollView style={styles.content}>
<Text style={styles.sectionTitle}>个人阅读档案</Text>
<View style={styles.profileCard}>
<View style={styles.profileHeader}>
<Text style={styles.profileName}>张三</Text>
<Text style={styles.profileLevel}>阅读达人</Text>
</View>
<View style={styles.profileStats}>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{totalBooks}</Text>
<Text style={styles.statLabel}>总书籍</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>{completedBooks}</Text>
<Text style={styles.statLabel}>已完成</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statNumber}>3</Text>
<Text style={styles.statLabel}>年经验</Text>
</View>
</View>
</View>
{/* 最喜欢的类别 */}
<Text style={styles.sectionTitle}>最喜欢阅读的类别</Text>
<View style={styles.favoriteCategoryCard}>
<Text style={styles.favoriteCategoryText}>小说</Text>
</View>
{/* 设置选项 */}
<Text style={styles.sectionTitle}>设置选项</Text>
<View style={styles.settingCard}>
<TouchableOpacity style={styles.listItem} onPress={() => Alert.alert('提醒设置', '设置阅读提醒时间')}>
<Text style={styles.listItemText}>阅读提醒设置</Text>
<Text style={styles.listItemIcon}>→</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.listItem} onPress={() => Alert.alert('目标管理', '设置年度阅读目标')}>
<Text style={styles.listItemText}>年度目标管理</Text>
<Text style={styles.listItemIcon}>→</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.listItem} onPress={() => Alert.alert('数据备份', '备份阅读记录')}>
<Text style={styles.listItemText}>数据备份</Text>
<Text style={styles.listItemIcon}>→</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.listItem} onPress={() => Alert.alert('隐私设置', '管理数据权限')}>
<Text style={styles.listItemText}>隐私设置</Text>
<Text style={styles.listItemIcon}>→</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
return (
<SafeAreaView style={styles.container}>
{/* 头部 */}
<View style={styles.header}>
<Text style={styles.title}>看书管理记录</Text>
<TouchableOpacity onPress={() => Alert.alert('搜索', '搜索功能')}>
<Text style={styles.searchIcon}>🔍</Text>
</TouchableOpacity>
</View>
{/* 内容区域 */}
{!showBookDetail ? (
<>
{activeTab === 'home' && renderHomeContent()}
{activeTab === 'add' && renderAddBookContent()}
{activeTab === 'stats' && renderStatsContent()}
{activeTab === 'profile' && renderProfileContent()}
</>
) : (
renderBookDetail()
)}
{/* 底部导航 */}
{!showBookDetail && (
<View style={styles.bottomNav}>
<TouchableOpacity
style={styles.navItem}
onPress={() => setActiveTab('home')}
>
<Text style={activeTab === 'home' ? styles.navIconActive : styles.navIcon}>🏠</Text>
<Text style={activeTab === 'home' ? styles.navTextActive : styles.navText}>首页</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => setActiveTab('add')}
>
<Text style={activeTab === 'add' ? styles.navIconActive : styles.navIcon}>📚</Text>
<Text style={activeTab === 'add' ? styles.navTextActive : styles.navText}>添加</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => setActiveTab('stats')}
>
<Text style={activeTab === 'stats' ? styles.navIconActive : styles.navIcon}>📊</Text>
<Text style={activeTab === 'stats' ? styles.navTextActive : styles.navText}>统计</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => setActiveTab('profile')}
>
<Text style={activeTab === 'profile' ? styles.navIconActive : styles.navIcon}>👤</Text>
<Text style={activeTab === 'profile' ? styles.navTextActive : 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: 18,
fontWeight: 'bold',
color: '#1e293b',
},
searchIcon: {
fontSize: 20,
color: '#64748b',
},
content: {
flex: 1,
padding: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
marginVertical: 12,
},
overviewCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
overviewTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 12,
},
statsRow: {
flexDirection: 'row',
justifyContent: 'space-around',
},
statItem: {
alignItems: 'center',
},
statNumber: {
fontSize: 24,
fontWeight: 'bold',
color: '#3b82f6',
marginBottom: 4,
},
statLabel: {
fontSize: 12,
color: '#64748b',
},
quickActions: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 20,
},
quickAction: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
alignItems: 'center',
width: width / 4.5,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
quickActionIcon: {
fontSize: 24,
marginBottom: 8,
},
quickActionText: {
fontSize: 12,
color: '#1e293b',
textAlign: 'center',
},
bookItem: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 12,
flexDirection: 'row',
alignItems: 'center',
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
completedBookItem: {
backgroundColor: '#f0fdf4',
borderRadius: 12,
padding: 16,
marginBottom: 12,
flexDirection: 'row',
alignItems: 'center',
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
bookIcon: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#dbeafe',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
bookIconText: {
fontSize: 20,
},
bookInfo: {
flex: 1,
},
bookTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
},
bookAuthor: {
fontSize: 14,
color: '#64748b',
marginBottom: 4,
},
progressContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
progressBar: {
flex: 1,
height: 8,
backgroundColor: '#e2e8f0',
borderRadius: 4,
marginRight: 8,
},
progressFill: {
height: '100%',
backgroundColor: '#3b82f6',
borderRadius: 4,
},
progressText: {
fontSize: 12,
color: '#64748b',
minWidth: 30,
},
bookDetails: {
fontSize: 12,
color: '#94a3b8',
},
completedText: {
fontSize: 12,
color: '#10b981',
fontWeight: 'bold',
},
updateButton: {
backgroundColor: '#3b82f6',
padding: 8,
borderRadius: 6,
},
updateButtonText: {
color: '#ffffff',
fontSize: 12,
fontWeight: 'bold',
},
formCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
inputGroup: {
marginBottom: 16,
},
inputLabel: {
fontSize: 14,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 8,
},
pickerContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
},
pickerOption: {
backgroundColor: '#f1f5f9',
padding: 8,
borderRadius: 8,
marginRight: 8,
marginBottom: 8,
},
pickerOptionSelected: {
backgroundColor: '#3b82f6',
},
pickerOptionText: {
fontSize: 14,
color: '#1e293b',
},
inputField: {
backgroundColor: '#f8fafc',
borderWidth: 1,
borderColor: '#cbd5e1',
borderRadius: 8,
padding: 12,
fontSize: 16,
color: '#1e293b',
},
textArea: {
height: 80,
textAlignVertical: 'top',
},
submitButton: {
backgroundColor: '#3b82f6',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginTop: 8,
},
submitButtonText: {
color: '#ffffff',
fontWeight: 'bold',
fontSize: 16,
},
suggestionCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
suggestionTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 8,
},
suggestionText: {
fontSize: 14,
color: '#64748b',
marginBottom: 4,
},
statsOverviewCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
rateCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
rateTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 12,
},
rateRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
rateLabel: {
fontSize: 14,
color: '#64748b',
},
rateValue: {
fontSize: 14,
fontWeight: 'bold',
color: '#1e293b',
},
categoryStats: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
categoryItem: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
categoryName: {
fontSize: 14,
color: '#1e293b',
},
categoryCount: {
fontSize: 14,
color: '#3b82f6',
fontWeight: 'bold',
},
progressCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
progressTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 12,
},
progressBarLarge: {
height: 12,
backgroundColor: '#e2e8f0',
borderRadius: 6,
marginBottom: 8,
},
progressFillLarge: {
height: '100%',
backgroundColor: '#10b981',
borderRadius: 6,
},
progressTextLarge: {
fontSize: 14,
color: '#64748b',
textAlign: 'center',
},
timeStatsCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
timeStatItem: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 8,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
timeStatLabel: {
fontSize: 14,
color: '#1e293b',
},
timeStatValue: {
fontSize: 14,
color: '#3b82f6',
fontWeight: 'bold',
},
profileCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginBottom: 20,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
profileHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
profileName: {
fontSize: 20,
fontWeight: 'bold',
color: '#1e293b',
},
profileLevel: {
fontSize: 14,
color: '#3b82f6',
fontWeight: 'bold',
},
profileStats: {
flexDirection: 'row',
justifyContent: 'space-around',
},
favoriteCategoryCard: {
backgroundColor: '#dbeafe',
borderRadius: 12,
padding: 16,
marginBottom: 20,
alignItems: 'center',
},
favoriteCategoryText: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
},
settingCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 0,
marginBottom: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
listItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 16,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
listItemText: {
fontSize: 16,
color: '#1e293b',
},
listItemIcon: {
fontSize: 18,
color: '#94a3b8',
},
bottomNav: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
paddingVertical: 12,
},
navItem: {
alignItems: 'center',
flex: 1,
},
navIcon: {
fontSize: 20,
color: '#94a3b8',
marginBottom: 4,
},
navIconActive: {
fontSize: 20,
color: '#3b82f6',
marginBottom: 4,
},
navText: {
fontSize: 12,
color: '#94a3b8',
},
navTextActive: {
fontSize: 12,
color: '#3b82f6',
fontWeight: '500',
},
detailContainer: {
flex: 1,
backgroundColor: '#f8fafc',
},
detailHeader: {
padding: 20,
backgroundColor: '#ffffff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
backButton: {
fontSize: 16,
color: '#3b82f6',
marginBottom: 8,
},
detailTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#1e293b',
},
detailContent: {
flex: 1,
padding: 16,
},
detailCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
detailBookTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 8,
},
detailAuthor: {
fontSize: 16,
color: '#64748b',
marginBottom: 4,
},
detailCategory: {
fontSize: 14,
color: '#3b82f6',
marginBottom: 4,
},
detailRating: {
fontSize: 14,
color: '#f59e0b',
marginBottom: 12,
},
progressLabel: {
fontSize: 14,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 8,
},
detailSectionTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginVertical: 12,
},
detailDescription: {
fontSize: 14,
color: '#64748b',
lineHeight: 20,
marginBottom: 12,
},
detailPlan: {
fontSize: 14,
color: '#64748b',
marginBottom: 16,
},
detailActions: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 16,
},
detailButton: {
backgroundColor: '#3b82f6',
padding: 12,
borderRadius: 8,
flex: 1,
marginHorizontal: 4,
alignItems: 'center',
},
detailButtonText: {
color: '#ffffff',
fontWeight: 'bold',
},
});
export default ReadingTrackerApp;

打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

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

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

本文介绍了一个基于React Native的阅读追踪应用,采用函数组件和useState Hook实现状态管理。应用包含书籍列表、详情页、添加书籍和统计功能,通过TypeScript接口确保数据结构一致性。核心设计包括:
- 使用useState管理5个关键状态(页面切换、书籍数据、详情页控制等)
- 采用Base64编码图标实现跨平台资源管理
- 布局使用标签页导航和Flexbox系统
- 数据处理包含书籍CRUD和统计计算
- 特别考虑了React Native与鸿蒙系统的兼容性
应用展示了轻量级状态管理方案在跨端开发中的优势,通过原生Hook实现状态联动,确保在不同平台的功能一致性。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐



所有评论(0)