欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
React Native 分页组件设计与鸿蒙跨端适配解析
在跨端应用开发领域,React Native 凭借其「一次编写,多端运行」的特性成为移动开发的主流选择,而鸿蒙(HarmonyOS)作为国产分布式操作系统,也逐渐成为跨端开发的重要目标平台。本文将以一个功能完整的 React Native 分页组件为例,深入解读其设计思路、核心实现逻辑,并探讨该组件向鸿蒙跨端适配的关键技术要点。
组件核心架构与 React Native 实现细节
1. 组件整体设计思路
该分页组件采用 React Native 函数式组件 + TypeScript 的组合方式开发,遵循「单一职责」设计原则,将分页逻辑与 UI 渲染解耦,同时通过 Props 实现灵活的配置扩展。组件核心分为三个部分:基础常量定义(分页图标)、分页逻辑处理(Pagination 核心组件)、示例展示(PaginationComponentApp 容器组件),这种分层设计既保证了组件的复用性,也为跨端适配奠定了结构基础。
2. 关键技术点实现解析
(1)TypeScript 类型约束
组件通过 PaginationProps 接口定义了所有入参类型,包括必选的 total(总数据量)、current(当前页码),以及可选的 pageSize(每页条数)、onChange(页码变更回调)等。类型约束不仅提升了代码的可维护性,也为鸿蒙跨端时的类型适配提供了清晰的接口定义,避免因类型不兼容导致的跨端运行错误。
typescript
interface PaginationProps {
total: number;
current: number;
pageSize?: number;
onChange?: (page: number) => void;
showSizeChanger?: boolean;
showQuickJumper?: boolean;
simple?: boolean;
disabled?: boolean;
}
(2)智能页码计算逻辑
getPageNumbers 方法实现了「智能页码显示」核心逻辑:当总页数≤5时直接展示所有页码;当总页数>5时,根据当前页码位置动态展示「首页-省略号-当前页区间-省略号-尾页」的组合,既避免了页码过多导致的UI溢出,也符合用户分页操作的习惯。这种逻辑在鸿蒙端适配时,只需保留核心算法,替换UI渲染层即可复用。
(3)Base64 图标处理
分页组件的图标(首页、上一页、下一页等)采用 Base64 编码内嵌,而非远程图片或本地资源,这一设计在跨端开发中优势显著:React Native 端无需处理图片资源的多分辨率适配,鸿蒙端也无需额外配置资源路径,只需解析 Base64 字符串即可渲染图标,大幅降低了跨端资源适配成本。
(4)状态管理与交互逻辑
组件通过 useState 管理输入框的页码状态,handlePageChange 方法封装了页码合法性校验(边界值、禁用状态),handleInputChange 实现了输入框跳转功能。React Native 的状态管理逻辑与鸿蒙 ArkTS 的状态管理(如 @State、@Link)逻辑同源,只需将 React 的 useState 替换为 ArkTS 的状态装饰器,即可实现逻辑迁移。
(5)样式设计与适配
通过 StyleSheet.create 定义的样式遵循 React Native 最佳实践:使用弹性布局(flex)实现自适应,通过 Dimensions.get('window') 获取设备尺寸,结合 borderRadius、shadow 等属性实现现代化UI。鸿蒙端的样式适配可复用这些布局思路,将 React Native 的 StyleSheet 转换为鸿蒙的 FlexLayout、ComponentStyle 等API,核心布局逻辑(如 flexDirection、alignItems、justifyContent)完全通用。
从 React Native 到鸿蒙的跨端适配要点
1. 技术栈映射关系
React Native 与鸿蒙 ArkTS 的核心能力存在清晰的映射关系,是跨端适配的基础:
- 组件体系:React Native 的 View/Text/TextInput/TouchableOpacity 对应鸿蒙的 Column/Text/TextInput/Button(或 GestureDetector);
- 状态管理:React 的
useState对应 ArkTS 的@State,父子组件通信的 Props 对应 ArkTS 的@Prop/@Link; - 样式系统:React Native 的 flex 布局、样式属性(如 backgroundColor、padding)与鸿蒙的 FlexLayout、组件样式属性高度兼容;
- 事件处理:React Native 的 onPress 对应鸿蒙的 onClick,文本变更事件 onChangeText 对应鸿蒙的 onChange。
2. 核心逻辑复用策略
该分页组件的核心价值在于「分页算法」和「交互逻辑」,跨端适配时应优先复用这些无UI依赖的逻辑:
- 抽离纯逻辑层:将
getPageNumbers、handlePageChange等方法抽离为独立的工具函数,不依赖任何 React Native/鸿蒙的API,实现跨端复用; - 适配UI渲染层:根据不同平台的UI组件特性,重新实现渲染逻辑。例如 React Native 的 Image 组件加载 Base64 图标,鸿蒙可通过 Image 组件的
src属性直接解析 Base64 字符串;React Native 的 TouchableOpacity 实现点击交互,鸿蒙可通过 Button 组件或给 Column 绑定点击手势实现。
3. 适配示例(核心片段)
以下是鸿蒙 ArkTS 对分页组件核心逻辑的适配示例(保留原分页算法,替换UI层):
typescript
// 鸿蒙 ArkTS 分页组件核心逻辑(复用原分页算法)
@Entry
@Component
struct Pagination {
@State currentPage: number = 1;
@State totalItems: number = 100;
@State pageSize: number = 10;
@State inputPage: string = '';
// 复用原 React Native 中的分页算法(纯逻辑无依赖)
getPageNumbers(): (number | string)[] {
const totalPages = Math.ceil(this.totalItems / this.pageSize);
const pages: (number | string)[] = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
if (this.currentPage <= 3) {
for (let i = 1; i <= 4; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
} else if (this.currentPage >= totalPages - 2) {
pages.push(1);
pages.push('ellipsis');
for (let i = totalPages - 3; i <= totalPages; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push('ellipsis');
for (let i = this.currentPage - 1; i <= this.currentPage + 1; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
}
}
return pages;
}
// 复用原页码变更逻辑
handlePageChange(page: number) {
const totalPages = Math.ceil(this.totalItems / this.pageSize);
if (page >= 1 && page <= totalPages && page !== this.currentPage) {
this.currentPage = page;
}
}
build() {
Column({ space: 8 }) {
// 首页按钮
Button()
.backgroundColor('#f1f5f9')
.width(36)
.height(36)
.borderRadius(18)
.onClick(() => this.handlePageChange(1))
.child(
Image('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTExIDE3TDE2IDEybC01LTVtLTctMTBoMTJ2MTJINHpNMTggMTJMNiAxMnoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==')
.width(16)
.height(16)
)
.enabled(this.currentPage !== 1);
// 页码列表(动态渲染)
Row({ space: 2 }) {
ForEach(this.getPageNumbers(), (page: number | string) => {
if (page === 'ellipsis') {
// 省略号图标
Image('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDEyYzAgMS4xLS45IDIgMiAycy0yLS45LTIgMnptMCAwYzAgMS4xLjkgMiAyIDJzLTIgLjktMiAyeiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==')
.width(16)
.height(16)
.margin(10);
} else {
// 页码按钮
Button(page.toString())
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor(this.currentPage === page ? '#3b82f6' : '#f1f5f9')
.fontColor(this.currentPage === page ? '#ffffff' : '#0f172a')
.onClick(() => this.handlePageChange(page as number));
}
})
})
// 快速跳转输入框
Row({ space: 8 }) {
Text('跳至')
.fontSize(14)
.fontColor('#0f172a');
TextInput({ placeholder: '页' })
.width(60)
.height(36)
.border({ width: 1, color: '#cbd5e1', radius: 8 })
.padding(12)
.fontSize(14)
.onChange((e) => {
this.inputPage = e.value;
const pageNum = parseInt(e.value);
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= Math.ceil(this.totalItems / this.pageSize)) {
this.handlePageChange(pageNum);
}
});
}
}
.padding(20)
.width('100%')
.justifyContent(FlexAlign.Center);
}
}
跨端开发的最佳实践总结
- 逻辑与UI分离:开发跨端组件时,优先将分页算法、数据校验等核心逻辑抽离为纯函数,不依赖任何平台特定的API,确保逻辑层可复用;
- 利用通用布局体系:基于 flex 布局开发UI,因为 flex 是 React Native、鸿蒙、Web 等多端通用的布局标准,可大幅降低适配成本;
- 资源统一处理:采用 Base64 内嵌图标、统一的颜色/尺寸常量等方式,减少跨端资源适配的工作量;
- 接口标准化:通过 TypeScript/ArkTS 定义统一的组件接口(Props),确保多端组件的入参、回调函数格式一致,提升代码的可维护性。
该 React Native 分页组件的设计充分体现了「逻辑复用、UI适配」的跨端开发思想,通过合理的架构设计,既能在 React Native 端稳定运行,也能以极低的成本适配鸿蒙平台,是跨端组件开发的典型范例。
React Native 分页组件设计与鸿蒙跨端适配深度解析
在移动应用开发中,分页组件是处理大数据列表展示的核心UI元素,其交互体验与跨端适配能力直接影响应用的整体可用性。本文以一个功能完整的 React Native 分页组件为例,从组件架构设计、核心算法实现、TypeScript 类型约束、交互逻辑封装等维度展开深度解读,并系统探讨该组件向鸿蒙(HarmonyOS)跨端适配的核心技术路径,为跨端分页组件开发提供可落地的实践参考。
一、组件整体架构与设计理念
该 React Native 分页组件遵循「单一职责 + 配置化扩展」的设计原则,整体架构分为三层:基础资源层(Base64 图标)、核心逻辑层(分页算法与交互)、UI 渲染层(多模式展示)。组件支持标准分页、简洁分页、快速跳转、禁用状态等企业级应用所需的核心功能,同时通过 TypeScript 类型约束保证接口的规范性,为鸿蒙跨端适配奠定了坚实的架构基础。
从跨端设计角度看,该组件的核心优势在于:将分页计算、页码生成等核心逻辑与 React Native 特定的 UI 渲染解耦,仅在渲染层依赖 RN 组件库;采用 Flex 布局和 Base64 图标资源,最大化降低跨端适配的样式与资源迁移成本;通过配置化的 Props 设计,保证了 React Native 端与鸿蒙端接口的一致性。
二、React Native 端核心技术实现解析
1. TypeScript 类型约束:构建健壮的组件接口
作为企业级分页组件,类型安全是保证代码可维护性和跨端兼容性的基础,该组件通过精准的接口定义实现了全链路的类型约束:
(1)组件 Props 类型定义
typescript
interface PaginationProps {
total: number;
current: number;
pageSize?: number;
onChange?: (page: number) => void;
showSizeChanger?: boolean;
showQuickJumper?: boolean;
simple?: boolean;
disabled?: boolean;
}
PaginationProps 接口覆盖了分页组件的所有核心配置项:
- 必选核心属性 :
total(总数据条数)、current(当前页码)是分页计算的基础,通过必选约束保证组件的核心功能可用; - 可选配置属性 :
pageSize(每页条数,默认值10)、showSizeChanger(是否显示条数切换器)、showQuickJumper(是否显示快速跳转)、simple(是否简洁模式)、disabled(是否禁用),通过可选属性实现组件功能的灵活扩展; - 回调函数约束 :
onChange回调函数明确接收number类型的页码参数,保证了页码变更逻辑的类型安全。
这种类型设计不仅在开发阶段就能发现「传入非数字页码」「回调函数参数类型错误」等问题,也为鸿蒙跨端适配时的接口对齐提供了明确的参考标准,避免了跨端开发中常见的接口不一致问题。
(2)组件类型封装
typescript
const Pagination: React.FC<PaginationProps> = ({
total,
current,
pageSize = 10,
onChange,
showSizeChanger = false,
showQuickJumper = false,
simple = false,
disabled = false
}) => {
// 组件逻辑实现
};
通过 React.FC<PaginationProps> 类型封装,明确了该组件是 React 函数式组件,且接收 PaginationProps 类型的入参,结合默认参数值,既保证了类型安全,又提升了组件的易用性。
2. 核心算法:智能页码生成逻辑
分页组件的核心难点在于「智能页码展示」------当总页数较多时,不能全部展示所有页码,需根据当前页码动态生成合理的页码列表,该组件通过 getPageNumbers 方法实现了这一核心算法:
typescript
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
// 总页数小于等于5,展示所有页码
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// 总页数大于5,分三种场景动态生成页码
if (current <= 3) {
// 当前页码在前3页,展示前4页 + 省略号 + 最后一页
for (let i = 1; i <= 4; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
} else if (current >= totalPages - 2) {
// 当前页码在后3页,展示第一页 + 省略号 + 最后4页
pages.push(1);
pages.push('ellipsis');
for (let i = totalPages - 3; i <= totalPages; i++) {
pages.push(i);
}
} else {
// 当前页码在中间,展示第一页 + 省略号 + 当前页前后各1页 + 省略号 + 最后一页
pages.push(1);
pages.push('ellipsis');
for (let i = current - 1; i <= current + 1; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
}
}
return pages;
};
该算法的设计亮点在于:
- 用户体验优先:始终保证当前页码在可视区域内,同时保留首尾页的快速访问入口,符合用户的分页操作习惯;
- 边界条件处理:针对总页数较少、当前页在开头/结尾/中间等不同场景做了精细化处理,避免出现页码缺失或重复的问题;
- 跨端复用性:该算法完全基于纯 JavaScript 实现,不依赖任何 React Native 特定 API,可直接复用于鸿蒙端,仅需调整 UI 渲染逻辑。
3. 交互逻辑封装:安全的页码变更机制
分页组件的交互核心是页码变更,该组件通过 handlePageChange 方法封装了安全的页码变更逻辑:
typescript
const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages && page !== current && !disabled) {
onChange && onChange(page);
}
};
这一方法实现了四层安全校验:
- 下限校验 :
page >= 1保证页码不会小于1; - 上限校验 :
page <= totalPages保证页码不会超过总页数; - 重复校验 :
page !== current避免重复触发相同页码的变更回调; - 状态校验 :
!disabled保证禁用状态下无法变更页码。
这种校验机制避免了因用户异常操作(如点击禁用的按钮、输入超出范围的页码)导致的程序错误,同时也为跨端适配提供了统一的交互校验标准------鸿蒙端只需复用这一校验逻辑,即可保证分页交互的一致性。
4. 多模式渲染:条件渲染的优雅实现
该组件支持标准模式、简洁模式两种核心展示模式,通过条件渲染实现了不同模式的UI差异化展示:
(1)简洁模式渲染
typescript
if (simple) {
return (
<View style={styles.simpleContainer}>
<TouchableOpacity
style={[styles.navButton, disabled && styles.disabledButton]}
onPress={() => handlePageChange(current - 1)}
disabled={current === 1 || disabled}
>
<Image
source={{ uri: PAGINATION_ICONS.prev }}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</TouchableOpacity>
<Text style={[styles.simpleText, disabled && styles.disabledText]}>
{current} / {totalPages}
</Text>
<TouchableOpacity
style={[styles.navButton, disabled && styles.disabledButton]}
onPress={() => handlePageChange(current + 1)}
disabled={current === totalPages || disabled}
>
<Image
source={{ uri: PAGINATION_ICONS.next }}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</TouchableOpacity>
</View>
);
}
简洁模式仅保留「上一页/下一页」按钮和当前页码/总页数的文本展示,适用于对UI空间要求较高的场景,其设计要点在于:
- 禁用状态联动 :通过
disabled属性控制按钮的可点击状态和样式,保证交互的一致性; - 样式条件绑定 :通过数组语法合并基础样式和状态样式(如
[styles.navButton, disabled && styles.disabledButton]),实现样式的动态切换; - TouchableOpacity 封装 :使用 RN 原生的
TouchableOpacity组件实现点击交互,自带点击透明度反馈,提升用户体验。
(2)标准模式渲染
标准模式包含完整的分页控件:首尾页按钮、上一页/下一页按钮、智能页码列表、快速跳转输入框,其核心渲染逻辑在于 renderPageButton 方法:
typescript
const renderPageButton = (page: number | string, index: number) => {
if (page === 'ellipsis') {
return (
<View key={`ellipsis-${index}`} style={styles.ellipsisContainer}>
<Image
source={{ uri: PAGINATION_ICONS.ellipsis }}
style={styles.ellipsisIcon}
/>
</View>
);
}
const pageNumber = page as number;
const isActive = pageNumber === current;
return (
<TouchableOpacity
key={pageNumber}
style={[
styles.pageButton,
isActive && styles.activePageButton,
disabled && styles.disabledButton
]}
onPress={() => handlePageChange(pageNumber)}
disabled={disabled}
>
<Text style={[
styles.pageButtonText,
isActive && styles.activePageText,
disabled && styles.disabledText
]}>
{pageNumber}
</Text>
</TouchableOpacity>
);
};
该方法实现了两种类型的渲染:
- 省略号渲染 :当页码为
ellipsis时,渲染省略号图标,提示用户存在更多页码; - 页码按钮渲染:根据当前页码是否激活,动态切换按钮样式,保证激活状态的视觉突出性。
5. 快速跳转功能:输入校验与交互优化
快速跳转功能允许用户直接输入页码进行跳转,该组件通过 handleInputChange 方法实现了输入校验与自动跳转:
typescript
const [inputPage, setInputPage] = useState('');
const handleInputChange = (text: string) => {
setInputPage(text);
const pageNum = parseInt(text);
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
handlePageChange(pageNum);
}
};
其设计亮点在于:
- 实时校验:用户输入时实时解析数字并校验范围,符合条件则自动触发跳转,无需额外的确认按钮;
- 类型安全 :通过
parseInt转换输入文本,结合!isNaN校验,避免非数字输入导致的错误; - 状态同步 :通过
useState维护输入框状态,保证输入内容与组件状态的一致性。
6. 资源与样式设计:跨端适配的基础
(1)Base64 图标资源处理
组件中所有分页图标(首页、上一页、下一页、尾页、省略号)均采用 Base64 编码内嵌的方式存储:
typescript
const PAGINATION_ICONS = {
first: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTExIDE3TDE2IDEybC01LTVtLTctMTBoMTJ2MTJINHpNMTggMTJMNiAxMnoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==',
// 其他图标省略...
};
这种处理方式在跨端开发中具备三大核心优势:
- 资源路径无关性:无需处理 React Native 端 iOS/Android 不同分辨率图片的命名规范,也无需在鸿蒙端配置资源目录,只需解析 Base64 字符串即可渲染;
- 加载性能优化:图标随 JS 代码一同加载,避免了图片的异步请求,减少了组件渲染的白屏时间;
- 样式可控性 :SVG 格式的 Base64 图标支持通过
tintColor动态修改颜色,适配不同的激活/禁用状态。
(2)Flex 布局样式设计
组件样式基于 React Native 的 StyleSheet.create 构建,核心采用 Flex 布局:
typescript
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
simpleContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
pagesContainer: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 8,
},
// 其他样式省略...
});
Flex 布局是 React Native、鸿蒙、Web 等多端通用的布局标准,该组件的样式设计完全基于 Flex 体系:
- 横向排列 :通过
flexDirection: 'row'实现分页控件的横向布局; - 居中对齐 :
alignItems: 'center'和justifyContent: 'center'保证控件垂直和水平居中; - 间距控制 :通过
marginHorizontal控制控件间的水平间距,保证UI的美观性。
这种样式设计为鸿蒙跨端适配提供了极大便利,只需将 React Native 的 StyleSheet 属性映射为鸿蒙的 ComponentStyle 属性,即可快速实现样式迁移。
三、鸿蒙跨端适配核心技术路径
1. 技术栈映射:React Native 与鸿蒙 ArkTS 核心对应关系
要实现分页组件的跨端适配,首先需明确 React Native 与鸿蒙 ArkTS 的核心技术栈映射关系,这是逻辑复用和 UI 迁移的基础:
| React Native 技术点 | 鸿蒙 ArkTS 对应技术点 | 适配说明 |
|---|---|---|
| 函数式组件 + Props | 结构化组件 + @Prop/@Link | React 的 Props 对应鸿蒙的 @Prop(单向传递)/@Link(双向绑定),React.FC 对应鸿蒙的组件函数 |
| useState | @State/@Link | React 的状态钩子对应鸿蒙的状态装饰器,useState('') 对应 @State inputPage: string = '' |
| TouchableOpacity | Button + onClick / GestureDetector | 点击交互可通过 Button 组件或手势检测器实现,保留点击回调逻辑 |
| View/Text/Image | Column/Row/Text/Image | 基础UI组件一一对应,功能完全兼容 |
| StyleSheet | ComponentStyle + 内联样式 | Flex 布局属性完全通用,样式属性名称略有差异(如 backgroundColor 对应 background.color) |
| TextInput | TextInput | 输入框组件功能一致,属性名称略有调整(如 onChangeText 对应 onChange) |
2. 核心逻辑复用:抽离无平台依赖的纯函数
跨端适配的核心原则是「逻辑复用,UI 重写」,该分页组件中可直接复用的纯逻辑包括:
- 页码生成算法(
getPageNumbers) - 页码变更校验(
handlePageChange) - 输入框校验逻辑(
handleInputChange)
这些逻辑不依赖任何 React Native 特定 API,只需少量语法调整即可在鸿蒙端运行:
(1)页码生成算法复用(鸿蒙 ArkTS)
typescript
// 鸿蒙 ArkTS 中复用页码生成算法
getPageNumbers(totalPages: number, current: number): (number | string)[] {
const pages: (number | string)[] = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
if (current <= 3) {
for (let i = 1; i <= 4; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
} else if (current >= totalPages - 2) {
pages.push(1);
pages.push('ellipsis');
for (let i = totalPages - 3; i <= totalPages; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push('ellipsis');
for (let i = current - 1; i <= current + 1; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
}
}
return pages;
}
仅需将 React Native 中依赖组件内部状态的 totalPages 和 current 改为函数参数,即可实现完全复用。
(2)页码变更校验逻辑复用
typescript
// 鸿蒙 ArkTS 中复用页码变更校验逻辑
handlePageChange(page: number) {
if (page >= 1 && page <= this.totalPages && page !== this.current && !this.disabled) {
this.onChange?.(page);
}
}
鸿蒙端通过类组件的属性访问 totalPages、current、disabled,回调函数调用方式调整为 this.onChange?.(page),核心校验逻辑完全复用。
3. UI 渲染层适配:鸿蒙组件替换与样式映射
UI 渲染层是跨端适配的主要工作量,需将 React Native 组件替换为鸿蒙 ArkTS 组件,并调整样式属性:
(1)基础组件替换
| React Native 组件 | 鸿蒙 ArkTS 组件 | 替换说明 |
|---|---|---|
| View | Column/Row | 根据布局方向选择 Column(垂直)或 Row(水平) |
| Text | Text | 直接替换,样式属性调整 |
| Image | Image | Base64 图片源格式一致,source={``{ uri: xxx }} 对应 src: xxx |
| TouchableOpacity | Button | 通过 Button 组件的 onClick 实现点击交互 |
| TextInput | TextInput | 输入回调 onChangeText 对应 onChange,属性名称微调 |
(2)样式属性映射
| React Native 样式属性 | 鸿蒙 ArkTS 样式属性 | 映射说明 |
|---|---|---|
| backgroundColor | background.color | 背景色属性映射 |
| flexDirection | flexDirection | 完全一致 |
| alignItems | alignItems | 完全一致 |
| justifyContent | justifyContent | 完全一致 |
| marginHorizontal | marginLeft/marginRight | 拆分为左右边距 |
| paddingHorizontal | paddingLeft/paddingRight | 拆分为左右内边距 |
| borderRadius | borderRadius | 完全一致 |
| tintColor | fill | SVG 图标颜色映射 |
(3)完整鸿蒙适配示例(核心组件)
typescript
import { ReactNode } from 'react';
import { Column, Row, Text, Image, Button, TextInput, StyleSheet, FlexAlign, JustifyContent } from '@ohos/react';
// Base64 Icons(完全复用 React Native 端)
const PAGINATION_ICONS = {
first: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTExIDE3TDE2IDEybC01LTVtLTctMTBoMTJ2MTJINHpNMTggMTJMNiAxMnoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==',
prev: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDE1TDEwIDEybDUtMyIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K',
next: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDE1bDUtNW0wIDBsLTUgNSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K',
last: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzIDE3TDggMTJsNS01bTcgMTBINHYxMmgxMHpNOCAxMmw4LTEyeiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K',
ellipsis: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDEyYzAgMS4xLS45IDIgMiAycy0yLS45LTIgMnptMCAwYzAgMS4xLjkgMiAyIDJzLTIgLjktMiAyeiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg=='
};
// 类型定义(适配鸿蒙 ArkTS)
interface PaginationProps {
total: number;
current: number;
pageSize?: number;
onChange?: (page: number) => void;
showSizeChanger?: boolean;
showQuickJumper?: boolean;
simple?: boolean;
disabled?: boolean;
}
// 鸿蒙分页组件实现
export default class Pagination extends React.Component<PaginationProps> {
constructor(props: PaginationProps) {
super(props);
this.state = {
inputPage: ''
};
}
// 计算总页数
get totalPages(): number {
return Math.ceil(this.props.total / (this.props.pageSize || 10));
}
// 智能页码生成算法(复用 React Native 逻辑)
getPageNumbers(): (number | string)[] {
const { current } = this.props;
const totalPages = this.totalPages;
const pages: (number | string)[] = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
if (current <= 3) {
for (let i = 1; i <= 4; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
} else if (current >= totalPages - 2) {
pages.push(1);
pages.push('ellipsis');
for (let i = totalPages - 3; i <= totalPages; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push('ellipsis');
for (let i = current - 1; i <= current + 1; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
}
}
return pages;
}
// 页码变更校验逻辑(复用 React Native 逻辑)
handlePageChange(page: number) {
const { current, disabled, onChange } = this.props;
const totalPages = this.totalPages;
if (page >= 1 && page <= totalPages && page !== current && !disabled) {
onChange && onChange(page);
}
}
// 输入框变更逻辑(复用 React Native 逻辑)
handleInputChange(text: string) {
this.setState({ inputPage: text });
const pageNum = parseInt(text);
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= this.totalPages) {
this.handlePageChange(pageNum);
}
}
// 渲染页码按钮
renderPageButton(page: number | string, index: number) {
const { current, disabled } = this.props;
if (page === 'ellipsis') {
return (
<Row key={`ellipsis-${index}`} style={styles.ellipsisContainer}>
<Image
src={PAGINATION_ICONS.ellipsis}
style={styles.ellipsisIcon}
/>
</Row>
);
}
const pageNumber = page as number;
const isActive = pageNumber === current;
return (
<Button
key={pageNumber}
style={[
styles.pageButton,
isActive && styles.activePageButton,
disabled && styles.disabledButton
]}
onClick={() => this.handlePageChange(pageNumber)}
disabled={disabled}
>
<Text style={[
styles.pageButtonText,
isActive && styles.activePageText,
disabled && styles.disabledText
]}>
{pageNumber}
</Text>
</Button>
);
}
render() {
const { current, disabled, simple, showQuickJumper } = this.props;
const totalPages = this.totalPages;
const { inputPage } = this.state;
// 简洁模式
if (simple) {
return (
<Row style={styles.simpleContainer}>
<Button
style={[styles.navButton, disabled && styles.disabledButton]}
onClick={() => this.handlePageChange(current - 1)}
disabled={current === 1 || disabled}
>
<Image
src={PAGINATION_ICONS.prev}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</Button>
<Text style={[styles.simpleText, disabled && styles.disabledText]}>
{current} / {totalPages}
</Text>
<Button
style={[styles.navButton, disabled && styles.disabledButton]}
onClick={() => this.handlePageChange(current + 1)}
disabled={current === totalPages || disabled}
>
<Image
src={PAGINATION_ICONS.next}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</Button>
</Row>
);
}
// 标准模式
return (
<Row style={styles.container}>
<Button
style={[styles.navButton, disabled && styles.disabledButton]}
onClick={() => this.handlePageChange(1)}
disabled={current === 1 || disabled}
>
<Image
src={PAGINATION_ICONS.first}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</Button>
<Button
style={[styles.navButton, disabled && styles.disabledButton]}
onClick={() => this.handlePageChange(current - 1)}
disabled={current === 1 || disabled}
>
<Image
src={PAGINATION_ICONS.prev}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</Button>
<Row style={styles.pagesContainer}>
{this.getPageNumbers().map((page, index) => this.renderPageButton(page, index))}
</Row>
<Button
style={[styles.navButton, disabled && styles.disabledButton]}
onClick={() => this.handlePageChange(current + 1)}
disabled={current === totalPages || disabled}
>
<Image
src={PAGINATION_ICONS.next}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</Button>
<Button
style={[styles.navButton, disabled && styles.disabledButton]}
onClick={() => this.handlePageChange(totalPages)}
disabled={current === totalPages || disabled}
>
<Image
src={PAGINATION_ICONS.last}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</Button>
{showQuickJumper && (
<Row style={styles.jumperContainer}>
<Text style={[styles.jumperText, disabled && styles.disabledText]}>跳至</Text>
<TextInput
style={[styles.jumperInput, disabled && styles.disabledInput]}
value={inputPage}
onChange={(e) => this.handleInputChange(e.value)}
type="number"
disabled={disabled}
placeholder="页"
/>
</Row>
)}
</Row>
);
}
}
// 鸿蒙样式定义(映射 React Native 样式)
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: FlexAlign.Center,
justifyContent: JustifyContent.Center,
},
simpleContainer: {
flexDirection: 'row',
alignItems: FlexAlign.Center,
justifyContent: JustifyContent.Center,
},
navButton: {
width: 36,
height: 36,
borderRadius: 18,
background: { color: '#f1f5f9' },
justifyContent: JustifyContent.Center,
alignItems: FlexAlign.Center,
marginLeft: 4,
marginRight: 4,
},
disabledButton: {
opacity: 0.5,
},
navIcon: {
width: 16,
height: 16,
fill: '#0f172a',
},
pagesContainer: {
flexDirection: 'row',
alignItems: FlexAlign.Center,
marginLeft: 8,
marginRight: 8,
},
pageButton: {
width: 36,
height: 36,
borderRadius: 18,
background: { color: '#f1f5f9' },
justifyContent: JustifyContent.Center,
alignItems: FlexAlign.Center,
marginLeft: 2,
marginRight: 2,
},
activePageButton: {
background: { color: '#3b82f6' },
},
pageButtonText: {
fontSize: 14,
color: '#0f172a',
fontWeight: 500,
},
activePageText: {
color: '#ffffff',
},
ellipsisContainer: {
width: 36,
height: 36,
justifyContent: JustifyContent.Center,
alignItems: FlexAlign.Center,
marginLeft: 2,
marginRight: 2,
},
ellipsisIcon: {
width: 16,
height: 16,
fill: '#94a3b8',
},
simpleText: {
fontSize: 16,
color: '#0f172a',
fontWeight: 500,
marginLeft: 12,
marginRight: 12,
},
jumperContainer: {
flexDirection: 'row',
alignItems: FlexAlign.Center,
marginLeft: 16,
},
jumperText: {
fontSize: 14,
color: '#0f172a',
marginRight: 8,
},
jumperInput: {
width: 60,
height: 36,
border: { width: 1, color: '#cbd5e1' },
borderRadius: 8,
paddingLeft: 12,
paddingRight: 12,
fontSize: 14,
color: '#0f172a',
},
disabledText: {
color: '#94a3b8',
},
disabledInput: {
background: { color: '#f1f5f9' },
border: { width: 1, color: '#e2e8f0' },
},
});
四、跨端适配最佳实践总结
1. 架构设计层面
- 逻辑与UI彻底解耦:开发跨端分页组件时,需将页码生成、校验等核心逻辑抽离为纯函数,不依赖任何平台特定 API;UI 渲染层则针对不同平台的组件特性单独实现,确保逻辑复用率最大化;
- 接口标准化:通过 TypeScript/ArkTS 定义统一的组件接口(Props),保证 React Native 端与鸿蒙端的入参、回调函数格式一致,降低跨端开发的沟通成本;
- 资源统一管理:优先采用 Base64 内嵌图标,避免平台间资源路径、命名规范的差异导致的适配问题,同时提升资源加载性能。
2. 技术实现层面
- 算法逻辑复用:分页组件的核心价值在于页码生成算法,该部分应完全复用,仅调整参数传递方式以适配不同平台的组件模型;
- 交互逻辑复用:页码变更校验、输入框校验等交互逻辑应保持一致,仅调整回调函数的调用方式以适配不同平台的事件系统;
- 样式适配策略:基于 Flex 布局的样式可最大化复用,只需映射不同平台的样式属性名称,避免重复编写样式逻辑。
3. 性能优化层面
- 避免过度渲染 :在 React Native 端使用
useMemo/useCallback缓存页码列表和回调函数,鸿蒙端则通过@Memo装饰器优化组件重渲染; - 输入性能优化:快速跳转输入框应做防抖处理,避免用户快速输入时频繁触发页码变更逻辑;
- 资源加载优化:Base64 图标虽便捷,但过多的 Base64 编码会增加 JS 包体积,可结合按需加载、图片压缩等方式平衡性能与适配成本。
真实演示案例代码:
js
import React, { useState } from 'react';
import { View, Text, TextInput, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Image } from 'react-native';
// Base64 Icons for Pagination component
const PAGINATION_ICONS = {
first: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTExIDE3TDE2IDEybC01LTVtLTctMTBoMTJ2MTJINHpNMTggMTJMNiAxMnoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==',
prev: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDE1TDEwIDEybDUtMyIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K',
next: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDE1bDUtNW0wIDBsLTUgNSIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K',
last: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEzIDE3TDggMTJsNS01bTcgMTBINHYxMmgxMHpNOCAxMmw4LTEyeiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K',
ellipsis: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDEyYzAgMS4xLS45IDIgMiAycy0yLS45LTIgMnptMCAwYzAgMS4xLjkgMiAyIDJzLTIgLjktMiAyeiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg=='
};
// Pagination Component
interface PaginationProps {
total: number;
current: number;
pageSize?: number;
onChange?: (page: number) => void;
showSizeChanger?: boolean;
showQuickJumper?: boolean;
simple?: boolean;
disabled?: boolean;
}
const Pagination: React.FC<PaginationProps> = ({
total,
current,
pageSize = 10,
onChange,
showSizeChanger = false,
showQuickJumper = false,
simple = false,
disabled = false
}) => {
const totalPages = Math.ceil(total / pageSize);
const [inputPage, setInputPage] = useState('');
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
if (current <= 3) {
for (let i = 1; i <= 4; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
} else if (current >= totalPages - 2) {
pages.push(1);
pages.push('ellipsis');
for (let i = totalPages - 3; i <= totalPages; i++) {
pages.push(i);
}
} else {
pages.push(1);
pages.push('ellipsis');
for (let i = current - 1; i <= current + 1; i++) {
pages.push(i);
}
pages.push('ellipsis');
pages.push(totalPages);
}
}
return pages;
};
const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages && page !== current && !disabled) {
onChange && onChange(page);
}
};
const handleInputChange = (text: string) => {
setInputPage(text);
const pageNum = parseInt(text);
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
handlePageChange(pageNum);
}
};
const renderPageButton = (page: number | string, index: number) => {
if (page === 'ellipsis') {
return (
<View key={`ellipsis-${index}`} style={styles.ellipsisContainer}>
<Image
source={{ uri: PAGINATION_ICONS.ellipsis }}
style={styles.ellipsisIcon}
/>
</View>
);
}
const pageNumber = page as number;
const isActive = pageNumber === current;
return (
<TouchableOpacity
key={pageNumber}
style={[
styles.pageButton,
isActive && styles.activePageButton,
disabled && styles.disabledButton
]}
onPress={() => handlePageChange(pageNumber)}
disabled={disabled}
>
<Text style={[
styles.pageButtonText,
isActive && styles.activePageText,
disabled && styles.disabledText
]}>
{pageNumber}
</Text>
</TouchableOpacity>
);
};
if (simple) {
return (
<View style={styles.simpleContainer}>
<TouchableOpacity
style={[styles.navButton, disabled && styles.disabledButton]}
onPress={() => handlePageChange(current - 1)}
disabled={current === 1 || disabled}
>
<Image
source={{ uri: PAGINATION_ICONS.prev }}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</TouchableOpacity>
<Text style={[styles.simpleText, disabled && styles.disabledText]}>
{current} / {totalPages}
</Text>
<TouchableOpacity
style={[styles.navButton, disabled && styles.disabledButton]}
onPress={() => handlePageChange(current + 1)}
disabled={current === totalPages || disabled}
>
<Image
source={{ uri: PAGINATION_ICONS.next }}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</TouchableOpacity>
</View>
);
}
return (
<View style={styles.container}>
<TouchableOpacity
style={[styles.navButton, disabled && styles.disabledButton]}
onPress={() => handlePageChange(1)}
disabled={current === 1 || disabled}
>
<Image
source={{ uri: PAGINATION_ICONS.first }}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</TouchableOpacity>
<TouchableOpacity
style={[styles.navButton, disabled && styles.disabledButton]}
onPress={() => handlePageChange(current - 1)}
disabled={current === 1 || disabled}
>
<Image
source={{ uri: PAGINATION_ICONS.prev }}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</TouchableOpacity>
<View style={styles.pagesContainer}>
{getPageNumbers().map((page, index) => renderPageButton(page, index))}
</View>
<TouchableOpacity
style={[styles.navButton, disabled && styles.disabledButton]}
onPress={() => handlePageChange(current + 1)}
disabled={current === totalPages || disabled}
>
<Image
source={{ uri: PAGINATION_ICONS.next }}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</TouchableOpacity>
<TouchableOpacity
style={[styles.navButton, disabled && styles.disabledButton]}
onPress={() => handlePageChange(totalPages)}
disabled={current === totalPages || disabled}
>
<Image
source={{ uri: PAGINATION_ICONS.last }}
style={[styles.navIcon, disabled && styles.disabledText]}
/>
</TouchableOpacity>
{showQuickJumper && (
<View style={styles.jumperContainer}>
<Text style={[styles.jumperText, disabled && styles.disabledText]}>跳至</Text>
<TextInput
style={[styles.jumperInput, disabled && styles.disabledInput]}
value={inputPage}
onChangeText={handleInputChange}
keyboardType="numeric"
editable={!disabled}
placeholder="页"
/>
</View>
)}
</View>
);
};
// Main App Component
const PaginationComponentApp = () => {
const [currentPage, setCurrentPage] = useState(1);
const [totalItems] = useState(100);
const [pageSize] = useState(10);
const mockData = Array.from({ length: pageSize }, (_, index) => ({
id: (currentPage - 1) * pageSize + index + 1,
name: `数据项 ${(currentPage - 1) * pageSize + index + 1}`,
description: `这是第 ${(currentPage - 1) * pageSize + index + 1} 条数据的描述信息`
}));
return (
<View style={styles.appContainer}>
<View style={styles.header}>
<Text style={styles.headerTitle}>分页组件</Text>
<Text style={styles.headerSubtitle}>绑定当前页码的分页导航</Text>
</View>
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.dataSection}>
<Text style={styles.sectionTitle}>数据列表 (第 {currentPage} 页)</Text>
{mockData.map((item) => (
<View key={item.id} style={styles.dataItem}>
<Text style={styles.dataName}>{item.name}</Text>
<Text style={styles.dataDescription}>{item.description}</Text>
</View>
))}
</View>
<View style={styles.paginationSection}>
<Text style={styles.sectionTitle}>标准分页</Text>
<Pagination
total={totalItems}
current={currentPage}
pageSize={pageSize}
onChange={setCurrentPage}
showQuickJumper={true}
/>
</View>
<View style={styles.paginationSection}>
<Text style={styles.sectionTitle}>简洁分页</Text>
<Pagination
total={totalItems}
current={currentPage}
pageSize={pageSize}
onChange={setCurrentPage}
simple={true}
/>
</View>
<View style={styles.paginationSection}>
<Text style={styles.sectionTitle}>禁用状态</Text>
<Pagination
total={totalItems}
current={currentPage}
pageSize={pageSize}
onChange={setCurrentPage}
disabled={true}
/>
</View>
<View style={styles.featuresSection}>
<Text style={styles.featuresTitle}>功能特性</Text>
<View style={styles.featureList}>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>智能页码显示</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>首尾页快速跳转</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>页码输入跳转</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>简洁模式支持</Text>
</View>
<View style={styles.featureItem}>
<Text style={styles.featureBullet}>•</Text>
<Text style={styles.featureText}>禁用状态控制</Text>
</View>
</View>
</View>
<View style={styles.usageSection}>
<Text style={styles.usageTitle}>使用说明</Text>
<Text style={styles.usageText}>
分页组件用于处理大量数据的分页显示,
支持多种显示模式和交互方式,提升用户体验。
</Text>
</View>
</ScrollView>
<View style={styles.footer}>
<Text style={styles.footerText}>© 2023 分页组件 | 现代化UI组件库</Text>
</View>
</View>
);
};
const { width, height } = Dimensions.get('window');
const styles = StyleSheet.create({
appContainer: {
flex: 1,
backgroundColor: '#ffffff',
},
header: {
backgroundColor: '#0f172a',
paddingTop: 30,
paddingBottom: 25,
paddingHorizontal: 20,
borderBottomWidth: 1,
borderBottomColor: '#1e293b',
},
headerTitle: {
fontSize: 28,
fontWeight: '700',
color: '#f8fafc',
textAlign: 'center',
marginBottom: 5,
},
headerSubtitle: {
fontSize: 16,
color: '#94a3b8',
textAlign: 'center',
},
content: {
padding: 20,
},
dataSection: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 20,
marginBottom: 30,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
color: '#0f172a',
marginBottom: 15,
},
dataItem: {
backgroundColor: '#f8fafc',
borderRadius: 8,
padding: 16,
marginBottom: 12,
},
dataName: {
fontSize: 16,
fontWeight: '500',
color: '#0f172a',
marginBottom: 4,
},
dataDescription: {
fontSize: 14,
color: '#64748b',
},
paginationSection: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 20,
marginBottom: 30,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
},
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
simpleContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
navButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#f1f5f9',
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 4,
},
disabledButton: {
opacity: 0.5,
},
navIcon: {
width: 16,
height: 16,
tintColor: '#0f172a',
},
pagesContainer: {
flexDirection: 'row',
alignItems: 'center',
marginHorizontal: 8,
},
pageButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#f1f5f9',
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 2,
},
activePageButton: {
backgroundColor: '#3b82f6',
},
pageButtonText: {
fontSize: 14,
color: '#0f172a',
fontWeight: '500',
},
activePageText: {
color: '#ffffff',
},
ellipsisContainer: {
width: 36,
height: 36,
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 2,
},
ellipsisIcon: {
width: 16,
height: 16,
tintColor: '#94a3b8',
},
simpleText: {
fontSize: 16,
color: '#0f172a',
fontWeight: '500',
marginHorizontal: 12,
},
jumperContainer: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 16,
},
jumperText: {
fontSize: 14,
color: '#0f172a',
marginRight: 8,
},
jumperInput: {
width: 60,
height: 36,
borderWidth: 1,
borderColor: '#cbd5e1',
borderRadius: 8,
paddingHorizontal: 12,
fontSize: 14,
color: '#0f172a',
},
disabledText: {
color: '#94a3b8',
},
disabledInput: {
backgroundColor: '#f1f5f9',
borderColor: '#e2e8f0',
},
featuresSection: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 20,
marginBottom: 25,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
},
featuresTitle: {
fontSize: 18,
fontWeight: '600',
color: '#0f172a',
marginBottom: 15,
},
featureList: {
paddingLeft: 15,
},
featureItem: {
flexDirection: 'row',
marginBottom: 10,
},
featureBullet: {
fontSize: 16,
color: '#3b82f6',
marginRight: 8,
},
featureText: {
fontSize: 16,
color: '#64748b',
flex: 1,
},
usageSection: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 20,
marginBottom: 30,
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.08,
shadowRadius: 2,
},
usageTitle: {
fontSize: 18,
fontWeight: '600',
color: '#0f172a',
marginBottom: 10,
},
usageText: {
fontSize: 16,
color: '#64748b',
lineHeight: 24,
},
footer: {
paddingVertical: 20,
alignItems: 'center',
backgroundColor: '#0f172a',
},
footerText: {
color: '#94a3b8',
fontSize: 14,
},
});
export default PaginationComponentApp;
打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

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

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


