基于React Native鸿蒙跨平台一款阅读追踪应用完成进度条的增加与减少,可以实现任务的进度计算逻辑
本文深入解析了React Native(RN)鸿蒙跨端开发中工具类APP的实现要点,以一款阅读追踪应用为例,重点剖析了多端适配的核心技术。文章从依赖引入、基础组件适配、Base64图标优化、TypeScript类型规范等方面,详细阐述了如何实现"一套代码多端运行"的目标。通过RN官方通用组件与鸿蒙适配层的无缝对接,确保了UI一致性、交互流畅性和数据联动性。特别强调了轻量级状态管
在React Native(RN)鸿蒙跨端开发领域,工具类APP的核心诉求是「多端UI一致性、数据联动流畅性、基础组件适配兼容性」,尤其是涉及表单交互、列表渲染、状态联动等高频场景时,鸿蒙系统的适配质量直接决定用户体验。本次解读的代码片段,是一款完整的阅读追踪APP(看书管理记录)的核心实现,涵盖首页概览、书籍添加、统计分析、个人中心四大模块,支持书籍管理、进度更新、数据统计等核心功能,完全遵循RN跨端开发规范,可直接在鸿蒙设备上编译运行,无需额外修改核心逻辑。
本文将以技术博客解读的语气,逐模块拆解这段代码的实现逻辑,重点聚焦RN与鸿蒙系统的跨端适配细节——从依赖引入、类型定义、状态管理,到组件封装、样式适配、交互逻辑,深入剖析每一个技术点背后的跨端设计思路,帮助开发者吃透工具类APP的RN鸿蒙跨端开发技巧,理解「一套代码,多端流畅运行」的核心实现逻辑,同时规避跨端开发中常见的适配坑,为同类工具类应用跨端开发提供可复用的实战参考。
一、依赖引入:
RN鸿蒙跨端开发的核心原则之一是「优先使用RN官方通用组件与API」,避免引入平台专属依赖,借助鸿蒙系统提供的RN适配层(HarmonyOS React Native Adapter),实现API的自动映射,减少手动适配成本。我们先看代码开篇的依赖引入与基础定义部分,这是整个APP跨端运行的核心基石:
// 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==',
// 其余图标省略...
};
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;
};
这部分代码看似简单,实则奠定了整个APP的跨端适配基础,没有任何平台专属代码,完全基于RN官方能力构建,我们逐点拆解其核心技术点与鸿蒙适配细节:
1.1 核心依赖:
代码引入的React核心(useState钩子)与RN官方组件,均为鸿蒙适配层完全兼容的通用能力,无需任何手动适配即可在鸿蒙设备上正常运行,具体适配细节如下,也是工具类APP跨端开发的最优实践:
-
SafeAreaView:鸿蒙设备(尤其是刘海屏、曲面屏)的核心适配组件,鸿蒙RN适配层会自动识别当前设备的安全区域参数(状态栏高度、导航栏高度),动态调整组件内边距,确保APP内容不会被状态栏、导航栏遮挡。其适配效果与iOS、Android端完全一致,无需开发者单独判断设备类型(如鸿蒙手机与iOS手机的安全区域计算逻辑,适配层已自动兼容),省去了手动适配安全区域的繁琐代码——这对于工具类APP而言,能极大提升多设备适配效率。
-
基础布局与交互组件(View、Text、TouchableOpacity):这三个是RN开发中最基础的组件,也是工具类APP的核心组件,鸿蒙适配层已将其完全映射为鸿蒙原生组件——View对应鸿蒙的ComponentContainer,用于构建布局容器;Text对应TextComponent,用于文本渲染;TouchableOpacity对应鸿蒙的ButtonComponent,并保留RN的透明度点击反馈效果(点击时透明度降低,松开后恢复)。
需要重点说明的是,TouchableOpacity的点击事件(onPress)在鸿蒙系统中响应流畅,无延迟,且点击反馈与其他平台保持一致,无需额外处理兼容性问题。对于阅读追踪APP来说,这类基础交互的一致性是提升用户体验的关键,而这段代码完全复用RN原生逻辑,实现了跨端交互统一。
- 滚动与尺寸组件(ScrollView、FlatList、Dimensions):ScrollView用于实现纵向滚动布局(如各页面的内容滚动),鸿蒙适配层会将其映射为鸿蒙原生的ScrollView组件,支持纵向滚动、隐藏滚动条(showsVerticalScrollIndicator={false}),且滚动流畅不卡顿,无滚动错位、卡顿等适配问题;FlatList用于高效渲染长列表(如进行中书籍、已完成书籍列表),其核心属性(data、renderItem、keyExtractor)在鸿蒙系统中均完全兼容,鸿蒙适配层会将FlatList映射为鸿蒙原生的ListContainer组件,避免传统map遍历渲染长列表导致的性能问题,确保鸿蒙设备上列表滚动流畅。
Dimensions.get(‘window’)用于获取设备屏幕可视宽度(不含状态栏、导航栏),其返回值格式在鸿蒙系统中与iOS、Android端完全一致({ width: number, height: number }),代码中const { width } = Dimensions.get(‘window’)获取屏幕宽度,用于后续动态样式适配(如快速操作按钮宽度、画廊图片宽度),确保在不同尺寸的鸿蒙设备上(小屏手机、大屏平板),UI布局均匀合理,避免出现“小屏溢出、大屏留白”的问题——这是工具类APP多设备适配的核心技巧之一。
- 表单与弹窗组件(TextInput、Alert):TextInput用于实现表单输入(如添加书籍时的标题、作者、页数输入),鸿蒙适配层完全支持其核心属性(value、onChangeText、placeholder、keyboardType),无论是输入响应、键盘类型切换(数字键盘、默认键盘),还是占位提示显示,均与其他平台保持一致,无需额外适配;Alert用于弹出提示弹窗(如输入校验失败、操作成功提示),鸿蒙适配层会将其渲染为鸿蒙原生弹窗样式(而非RN模拟弹窗),贴合鸿蒙系统的原生视觉风格,提升用户亲切感,且API用法(Alert.alert(标题, 内容))与RN原生完全一致,无需修改任何代码即可正常使用。
1.2 Base64图标:
代码中定义的ICONS_BASE64对象,存储了APP所需的所有图标(首页、书籍、阅读、历史等),均采用Base64格式编码。这种方式在工具类APP跨端开发中优势极为明显,完美解决了传统图片适配的痛点,具体适配优势如下:
-
无平台格式差异:鸿蒙、iOS、Android均完美支持Base64图片渲染,无需单独为鸿蒙设备准备专属图标(如鸿蒙的.svg格式、iOS的.png格式),实现“一套图标,多端复用”,极大降低了图标资源的维护成本——对于工具类APP而言,图标数量不多但复用性强,这种方式能显著提升开发效率。
-
无需路径适配:避免了RN跨端开发中常见的“图片路径找不到”问题——传统图片适配中,iOS需要将图片放入images.xcassets文件夹,Android放入drawable文件夹,鸿蒙放入media文件夹,路径规则不同,容易出现路径错误导致图片加载失败。而Base64图标可直接通过uri引用(Image source={{ uri: ICONS_BASE64.home }}),简化开发流程,杜绝路径适配bug。
-
优化性能,适配工具类场景:工具类APP中的图标均为小型图标(尺寸较小),采用Base64格式,可减少APP的网络请求(无需远程加载图标)和本地资源占用,提升鸿蒙设备上的APP启动速度和渲染流畅度。对于工具类APP来说,启动速度和操作流畅度直接影响用户留存,这种适配方式恰好契合了工具类APP的性能需求。
注意:Base64格式仅适合小型图标(建议尺寸≤100x100px),大型图片(如Banner、封面图)仍建议使用远程图片(http/https链接)或平台通用格式图片(.png),避免Base64格式导致代码冗余、渲染性能下降。本案例中所有图标均为小型图标,采用Base64格式是最优选择。
1.3 TypeScript类型:
代码采用TypeScript编写,通过type定义了Book接口,规范了书籍数据的结构:包含id、title、author等10个属性,明确了每个属性的类型(如pages为数字、title为字符串、isCompleted为布尔值)。这一设计在跨端开发中至关重要,尤其是工具类APP,数据结构的一致性直接决定了列表渲染、表单提交、数据统计等核心功能的稳定性,其核心价值在于“统一跨端数据结构”,避免因平台差异导致的数据类型不兼容问题。
-
鸿蒙系统中,RN适配层对TypeScript的类型校验有良好的支持,Book接口明确了每一个属性的类型,确保开发者在开发阶段即可规避“数据类型错误”(如将pages赋值为字符串),避免该类错误被带入鸿蒙设备的运行阶段,导致列表渲染异常、表单提交失败等bug,提升开发效率。
-
后续的书籍数据源(books数组)、新增书籍状态(newBook)、数据更新方法(updateProgress)均严格遵循Book接口规范,确保跨端数据结构一致。无论是鸿蒙、iOS还是Android端,数据的传递与渲染逻辑完全复用,无需额外适配,实现了“一套数据逻辑,多端复用”,降低了跨端维护成本——这对于工具类APP而言,能有效减少多端数据同步的繁琐工作。
二、useState钩子:
阅读追踪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
},
// 其余书籍数据省略...
]);
const [newBook, setNewBook] = useState({
title: '',
author: '',
pages: '',
category: '小说'
});
// 计算总统计数据
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);
// 其余方法省略...
};
2.1 页面切换状态(activeTab):
activeTab状态用于管理底部导航的页面切换,类型限定为<‘home’ | ‘add’ | ‘stats’ | ‘profile’>,初始值为’home’(默认显示首页),通过setActiveTab方法更新状态,实现首页、添加书籍、统计分析、个人中心四大页面的切换。
适配细节:鸿蒙系统对RN的状态更新与条件渲染完全兼容,页面切换逻辑({activeTab === ‘home’ && renderHomeContent()})能够正常执行,状态更新后,UI会同步渲染对应页面,无卡顿、无延迟、无页面闪烁。同时,底部导航的选中状态(文字颜色、图标颜色切换)会同步更新,用户能够清晰感知当前所在页面——这种状态管理方式完全复用RN原生逻辑,无需任何鸿蒙专属适配,实现了跨端页面切换的一致性。
2.2 核心业务状态(books、newBook):
这两个状态是APP核心业务数据的载体,直接决定了书籍管理、表单提交等核心功能的跨端适配效果,也是工具类APP状态管理的核心:
- books状态:用于存储所有书籍数据,类型为Book[]数组,初始值为4条模拟书籍数据,涵盖小说、文学、历史、自我提升等类别,包含已完成和进行中两种状态。books状态的更新(添加书籍、更新进度)均采用不可变数据更新方式(展开运算符…),确保鸿蒙系统能够准确捕捉到状态变化,驱动UI实时渲染——例如,添加书籍时setBooks([book, …books]),更新进度时setBooks(books.map(…)),这种方式既符合React的最佳实践,也完全兼容鸿蒙系统的状态更新机制。
适配细节:在鸿蒙系统中,books数组的map、filter、reduce等方法均能正常运行,无语法错误;书籍数据的传递(如列表渲染、详情页展示)不会出现数据丢失、类型转换异常等问题,确保跨端数据联动的一致性——这对于工具类APP而言,数据的准确性是核心,这种状态管理方式能有效规避跨端数据异常。
- newBook状态:用于管理新增书籍的表单数据,类型为对象,包含title、author、pages、category四个属性,初始值为空(category默认值为’小说’)。newBook状态通过onChangeText方法实时更新表单输入内容,确保表单输入与状态同步,为后续表单提交做准备。
适配细节:newBook状态的更新与表单输入的联动在鸿蒙系统中响应流畅,无延迟;表单输入的校验逻辑(如判断标题、作者、页数是否为空)能够正常执行,弹窗提示(Alert.alert)能够正常显示,确保跨端表单交互的一致性。
2.3 衍生统计状态(totalBooks、completedBooks等):
代码中通过books状态衍生出5个统计状态,用于实现APP的数据统计功能(如首页概览、统计分析页面),这些衍生状态的计算逻辑均为纯JavaScript逻辑,无任何平台依赖,完全支持跨端复用:
-
totalBooks:总书籍数量,通过books.length计算;
-
completedBooks:已完成书籍数量,通过books.filter(book => book.isCompleted).length计算;
-
inProgressBooks:进行中书籍数量,通过books.filter(book => !book.isCompleted).length计算;
-
totalPages:总页数,通过books.reduce((sum, book) => sum + book.pages, 0)计算;
-
totalReadPages:已读页数,通过books.reduce((sum, book) => sum + book.currentPage, 0)计算。
适配细节:这些衍生状态的计算逻辑在鸿蒙系统中能够正常执行,计算结果准确无误;当books状态更新时,衍生状态会自动同步更新,驱动统计页面、首页概览的UI实时渲染,无数据延迟——这对于工具类APP的统计功能而言,数据的实时性至关重要,这种衍生状态的设计方式完美适配跨端需求。
状态管理是基础,核心方法的封装则是APP落地的关键。代码片段中封装了3个核心方法(addNewBook、updateProgress、renderXXXContent系列方法),所有方法的封装均遵循“通用化、无平台依赖”原则,确保在鸿蒙系统中可直接复用,无需任何手动适配。这些方法覆盖了APP的所有核心交互,是跨端代码复用的核心载体,我们逐一看代码片段与技术解读:
// 添加新书
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
};
setBooks([book, ...books]);
setNewBook({ title: '', author: '', pages: '', category: '小说' });
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 renderHomeContent = () => (/* 主页渲染逻辑省略... */);
3.1 表单提交方法(addNewBook):
addNewBook方法用于处理新增书籍的表单提交逻辑,涵盖表单校验、书籍数据构造、状态更新、弹窗提示四个核心步骤,是工具类APP表单交互的核心方法,其鸿蒙适配的核心是“表单校验与数据构造的跨端复用”:
-
表单校验逻辑:通过判断newBook.title、newBook.author、newBook.pages是否为空,实现表单必填项校验,若有一项为空,通过Alert.alert弹出错误提示。适配细节:表单校验逻辑为纯JavaScript逻辑,无平台依赖,在鸿蒙系统中能够正常执行,弹窗提示样式与交互均贴合鸿蒙原生风格,无需额外适配。
-
书籍数据构造:根据newBook状态的表单数据,构造符合Book接口规范的书籍对象,其中id采用Date.now().toString()生成唯一标识,startDate为当前日期,targetDate为默认30天后的日期,currentPage和progress初始值为0,isCompleted初始值为false。适配细节:日期处理逻辑(new Date().toISOString().split(‘T’)[0])在鸿蒙系统中能够正常运行,生成的日期格式与其他平台一致;数据类型转换(如pages从字符串转为数字parseInt(newBook.pages))无异常,确保构造的书籍数据符合Book接口规范,避免数据类型错误。
-
状态更新逻辑:通过setBooks([book, …books])将新书籍添加到books数组头部,同时通过setNewBook重置表单状态,实现表单提交后清空输入框。适配细节:状态更新逻辑完全复用RN原生语法,鸿蒙系统能够准确捕捉到状态变化,驱动列表UI实时渲染(新增书籍会立即显示在列表顶部),无延迟、无数据异常;表单重置逻辑在鸿蒙系统中响应流畅,输入框会同步清空,提升用户体验。
-
成功提示逻辑:表单提交成功后,通过Alert.alert弹出成功提示,告知用户书籍已添加。适配细节:弹窗提示在鸿蒙系统中正常显示,点击确认按钮后弹窗关闭,交互逻辑与其他平台保持一致,无需额外适配。
3.2 数据更新方法(updateProgress)
updateProgress方法用于更新书籍的阅读进度,接收bookId(书籍唯一标识)和newPage(新页码)两个参数,涵盖进度计算、状态更新两个核心步骤,是阅读追踪APP的核心业务方法,其鸿蒙适配的核心是“不可变数据更新与状态联动的跨端复用”:
-
进度计算逻辑:通过Math.round((newPage / book.pages) * 100)计算阅读进度,同时通过Math.min(100, …)确保进度不超过100%;若进度为100%,则将isCompleted状态设为true(标记书籍为已完成)。适配细节:进度计算逻辑为纯JavaScript逻辑,无平台依赖,在鸿蒙系统中计算准确无误;数据类型判断(如newPage与book.pages的除法运算)无异常,避免出现NaN等错误。
-
状态更新逻辑:通过books.map遍历书籍数组,找到对应bookId的书籍,采用不可变数据更新方式(展开运算符…)更新currentPage、progress、isCompleted三个属性,其余书籍保持不变。适配细节:map方法在鸿蒙系统中能够正常运行,状态更新后,列表UI会同步渲染更新后的进度和书籍状态(如进度条宽度变化、已完成书籍样式切换),无延迟、无渲染异常;这种不可变数据更新方式,确保鸿蒙系统能够准确捕捉到状态变化,避免出现UI与状态不一致的问题——这是跨端数据联动适配的关键技巧。
3.3 页面渲染方法:
代码中封装了4个页面渲染方法(renderHomeContent、renderAddBookContent、renderStatsContent、renderProfileContent),分别对应首页、添加书籍、统计分析、个人中心四大页面的渲染逻辑,采用“拆分渲染”的设计思路,将复杂的页面渲染逻辑拆分为独立方法,提升代码可读性和复用性,同时确保跨端渲染的一致性——这是工具类APP跨端开发的常用设计模式。
适配细节:这4个渲染方法均返回RN组件,无任何平台专属代码,完全基于RN官方组件构建,鸿蒙适配层能够正常解析所有组件和样式,实现跨端页面渲染的一致性。每个渲染方法内部的组件布局、样式、交互逻辑,均遵循RN通用规范,无需额外修改即可在鸿蒙设备上正常渲染——例如,首页的概览卡片、快速操作按钮、书籍列表,添加书籍页面的表单组件,统计页面的统计卡片,均能在鸿蒙设备上完美适配,与其他平台保持一致的视觉效果和交互体验。
3.4 方法封装的跨端优势(工具类APP适配核心参考)
代码中所有方法的封装都遵循“通用化、无平台依赖”原则,没有引入任何iOS/Android平台专属API,也没有编写任何鸿蒙专属适配代码,其跨端优势极为明显,非常适合工具类这类轻量级应用的跨端开发:
-
代码复用率100%:所有方法可直接在鸿蒙、iOS、Android端复用,无需修改任何逻辑,极大降低了跨端开发成本,缩短开发周期——对于工具类APP来说,快速迭代、多端覆盖是核心需求,这种封装方式恰好契合这一需求。
-
适配成本低:借助鸿蒙RN适配层的自动映射能力,方法中的RN API(setState、Alert、数组操作等)均能正常运行,无需手动编写鸿蒙原生方法,开发者无需掌握鸿蒙原生开发技术,仅需熟悉RN开发,即可完成鸿蒙跨端适配。
-
维护成本低:方法逻辑与平台解耦,后续修改业务逻辑(如添加书籍的校验规则、进度更新的计算方式),只需修改一处代码,即可同步应用到所有平台,无需单独维护各平台的方法逻辑,降低了后续维护成本。
ReadingTrackerApp 组件采用了现代 React 函数组件架构,结合 useState Hook 实现了复杂的状态管理。应用通过多个状态变量控制不同的 UI 状态:activeTab 管理当前激活的标签页,books 存储书籍列表,newBook 管理添加新书的表单数据。
在类型定义上,使用了 TypeScript 的 Book 接口明确数据结构,包含书籍的各项属性如标题、作者、页数、当前页数、进度等。这种类型定义在跨端开发中尤为重要,确保了在不同平台上的数据结构一致性,减少了类型错误的可能性。
数据处理
应用实现了多个数据处理和计算函数,包括统计数据计算、添加新书和更新阅读进度等。统计数据计算通过数组方法如 filter 和 reduce 实现,计算总书籍数、已完成书籍数、进行中书籍数、总页数和已读页数。
添加新书功能通过表单验证确保必填字段的完整性,然后创建新的书籍对象并添加到书籍列表。更新阅读进度功能通过 map 方法更新指定书籍的进度,并自动将进度达到 100% 的书籍标记为已完成。
视觉设计
应用采用了标签页导航结构,通过 activeTab 状态控制不同内容的显示。主页内容包括阅读概览卡片、快速操作按钮和进行中的书籍列表。布局设计上,使用了 ScrollView 和 FlatList 确保在内容较长时可以滚动查看。
视觉设计简洁明了,通过不同的样式区分不同的功能区域和状态。快速操作按钮使用了 emoji 图标,增强了视觉效果和用户体验。进度条的设计直观显示了书籍的阅读进度,通过动态宽度实现了进度的可视化。
交互
应用实现了丰富的交互功能,包括标签页切换、添加新书、更新阅读进度和快速操作等。这些交互功能的实现遵循了 React 的最佳实践,通过状态更新驱动 UI 变化,确保了交互的一致性和可靠性。
特别是书籍进度的更新,自动将进度达到 100% 的书籍标记为已完成,提升了用户体验。表单验证和操作反馈通过 Alert.alert 提供明确的操作指导,增强了用户的操作信心。
跨端
在 React Native 与鸿蒙系统跨端开发中,该应用展现了多项兼容性设计:
-
基础组件选择:使用了
ScrollView、TouchableOpacity、FlatList等基础组件,这些组件在 React Native 和鸿蒙系统中都有对应的实现。 -
样式管理:通过
StyleSheet.create管理样式,确保了在不同平台上的一致表现。 -
状态管理:使用
useStateHook 进行状态管理,在鸿蒙系统中可以通过相应的状态管理机制(如@State装饰器)实现类似功能。 -
类型定义:使用 TypeScript 类型定义,确保了在不同平台上的数据结构一致性。
-
布局系统:使用了 Flexbox 布局系统,这是 React Native 和鸿蒙系统都支持的布局方式,确保了在不同平台上的一致布局效果。
-
API 兼容性:使用了
Alert.alert等跨平台 API,确保了在不同平台上的一致表现。
该阅读追踪应用展示了一个功能完整、设计优雅的 React Native 应用实现,涵盖了状态管理、数据处理、布局设计、交互处理等多个方面的技术点。通过合理的组件架构和状态管理,以及对跨端兼容性的考虑,该应用不仅在 React Native 环境下运行良好,也为后续的鸿蒙系统适配奠定了基础。
真实演示案例代码:
// 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;
};
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
},
{
id: '2',
title: '百年孤独',
author: '加西亚·马尔克斯',
pages: 368,
currentPage: 200,
progress: 54,
startDate: '2023-05-10',
targetDate: '2023-07-10',
category: '文学',
isCompleted: false
},
{
id: '3',
title: '人类简史',
author: '尤瓦尔·赫拉利',
pages: 443,
currentPage: 300,
progress: 68,
startDate: '2023-05-01',
targetDate: '2023-08-01',
category: '历史',
isCompleted: false
},
{
id: '4',
title: '高效能人士的七个习惯',
author: '史蒂芬·柯维',
pages: 381,
currentPage: 381,
progress: 100,
startDate: '2023-04-01',
targetDate: '2023-04-30',
category: '自我提升',
isCompleted: true
},
]);
const [newBook, setNewBook] = useState({
title: '',
author: '',
pages: '',
category: '小说'
});
// 计算总统计数据
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
};
setBooks([book, ...books]);
setNewBook({ title: '', author: '', pages: '', category: '小说' });
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 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 }) => (
<View style={styles.bookItem}>
<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={() => {
const newPage = item.currentPage + 10;
updateProgress(item.id, newPage > item.pages ? item.pages : newPage);
}}
>
<Text style={styles.updateButtonText}>+10页</Text>
</TouchableOpacity>
</View>
)}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
/>
{/* 已完成的书籍 */}
<Text style={styles.sectionTitle}>已完成的书籍</Text>
<FlatList
data={books.filter(book => book.isCompleted)}
renderItem={({ item }) => (
<View style={styles.completedBookItem}>
<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>
</View>
)}
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>
{/* 提交按钮 */}
<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>
{/* 内容区域 */}
{activeTab === 'home' && renderHomeContent()}
{activeTab === 'add' && renderAddBookContent()}
{activeTab === 'stats' && renderStatsContent()}
{activeTab === 'profile' && renderProfileContent()}
{/* 底部导航 */}
<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',
},
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',
},
});
export default ReadingTrackerApp;

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

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

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

本文深入解析了React Native(RN)鸿蒙跨端开发中工具类APP的实现要点,以一款阅读追踪应用为例,重点剖析了多端适配的核心技术。文章从依赖引入、基础组件适配、Base64图标优化、TypeScript类型规范等方面,详细阐述了如何实现"一套代码多端运行"的目标。通过RN官方通用组件与鸿蒙适配层的无缝对接,确保了UI一致性、交互流畅性和数据联动性。特别强调了轻量级状态管理、安全区域适配、性能优化等关键技巧,为同类工具类应用的跨端开发提供了可复用的实战方案,有效规避了平台差异带来的适配问题。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐



所有评论(0)