【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Pagination 分页(绑定当前页码)

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


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依赖的逻辑:

  • 抽离纯逻辑层:将 getPageNumbershandlePageChange 等方法抽离为独立的工具函数,不依赖任何 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);
  }
}

跨端开发的最佳实践总结

  1. 逻辑与UI分离:开发跨端组件时,优先将分页算法、数据校验等核心逻辑抽离为纯函数,不依赖任何平台特定的API,确保逻辑层可复用;
  2. 利用通用布局体系:基于 flex 布局开发UI,因为 flex 是 React Native、鸿蒙、Web 等多端通用的布局标准,可大幅降低适配成本;
  3. 资源统一处理:采用 Base64 内嵌图标、统一的颜色/尺寸常量等方式,减少跨端资源适配的工作量;
  4. 接口标准化:通过 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);
  }
};

这一方法实现了四层安全校验:

  1. 下限校验page >= 1 保证页码不会小于1;
  2. 上限校验page <= totalPages 保证页码不会超过总页数;
  3. 重复校验page !== current 避免重复触发相同页码的变更回调;
  4. 状态校验!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 中依赖组件内部状态的 totalPagescurrent 改为函数参数,即可实现完全复用。

(2)页码变更校验逻辑复用
typescript 复制代码
// 鸿蒙 ArkTS 中复用页码变更校验逻辑
handlePageChange(page: number) {
  if (page >= 1 && page <= this.totalPages && page !== this.current && !this.disabled) {
    this.onChange?.(page);
  }
}

鸿蒙端通过类组件的属性访问 totalPagescurrentdisabled,回调函数调用方式调整为 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工程目录去:

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



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

相关推荐
谢尔登1 小时前
深入React19任务调度器Scheduler
开发语言·前端·javascript
不爱吃糖的程序媛1 小时前
Flutter 插件适配 HarmonyOS 实战:以屏幕方向控制为例
flutter·华为·harmonyos
星空22231 小时前
鸿蒙跨平台实战:React Native 在 OpenHarmony 上的 PixelFormat 图片格式处理
react native·华为·harmonyos
阿珊和她的猫2 小时前
深入解析如何监听浏览器的页面关闭事件
前端·javascript·vue.js
敲代码的柯基2 小时前
一篇文章理解tsconfig.json和vue.config.js
javascript·vue.js·json
henry1010102 小时前
DeepSeek生成的HTML5小游戏 -- 黑8台球
前端·javascript·css·游戏·html
getyefang2 小时前
react-native使用字体库如何在安卓显示
javascript·react native·react.js
摸鱼的春哥2 小时前
春哥的Agent通关秘籍11:本地RAG实战(中上)
前端·javascript·后端
加农炮手Jinx7 小时前
Flutter for OpenHarmony: Flutter 三方库 icon_font_generator 自动化将 SVG 图标集转化为字体文件(鸿蒙矢量资源全自动管理)
运维·flutter·华为·自动化·harmonyos·devops