【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Sidebar 侧边导航(绑定当前选中项的索引)

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


开源鸿蒙跨平台开发者社区诚邀开发者共建生态,提供React Native侧边栏组件代码示例。该组件包含可折叠侧边栏功能,支持自定义图标(以Base64格式SVG实现)、徽章提示和回调事件。示例代码展示了如何实现包含首页、用户、设置等菜单项的侧边栏,使用TypeScript接口定义数据结构,支持头部和底部自定义内容。通过Animated实现平滑动画效果,适合跨平台应用开发。欢迎访问社区官网参与鸿蒙生态建设。


React Native 侧边导航组件设计与鸿蒙跨端适配深度解析

在移动跨端开发领域,侧边导航(Sidebar)作为应用核心的导航载体,其交互体验与跨端适配能力直接决定了应用的整体使用感受。本文以一个功能完整的 React Native 侧边导航组件为例,从组件架构设计、核心功能实现、TypeScript 类型约束、动画交互优化等维度展开深度解读,并系统探讨该组件向鸿蒙(HarmonyOS)跨端适配的核心技术路径,为跨端导航组件开发提供可落地的实践参考。

一、组件整体架构与设计理念

该 React Native 侧边导航组件采用「容器-内容-交互」三层架构设计,遵循 React 组件化思想与跨端开发的「逻辑复用、UI 适配」核心原则,整体结构可拆解为:基础常量层(图标资源)、类型定义层(TypeScript 接口)、核心逻辑层(Sidebar 组件)、示例展示层(SidebarComponentApp 容器)、样式层(StyleSheet)。

这种分层设计的核心优势在于:将导航渲染、折叠动画、选中状态管理等核心逻辑与 UI 样式、平台特定 API 解耦,既保证了 React Native 端的高性能运行,也为鸿蒙跨端适配提供了清晰的逻辑复用路径。组件具备可折叠、徽章数字提示、选中状态高亮、自定义样式、头部/底部插槽等企业级应用所需的核心功能,完全满足中后台管理系统、电商应用等场景的导航需求。

二、React Native 端核心技术实现解析

1. TypeScript 类型约束:构建健壮的组件接口

作为企业级组件,类型安全是保证代码可维护性的基础,该组件通过 TypeScript 接口定义实现了全链路的类型约束,核心类型设计如下:

(1)导航项类型定义
typescript 复制代码
// Sidebar Item Interface
interface SidebarItem {
  id: string;
  title: string;
  icon: keyof typeof SIDEBAR_ICONS;
  badge?: number;
}

SidebarItem 接口精准定义了导航项的核心属性:id 作为唯一标识保证列表渲染的稳定性;title 为导航文本;icon 通过 keyof typeof SIDEBAR_ICONS 实现「图标键值」的强类型约束,避免传入不存在的图标名称;badge 为可选属性,用于消息数字提示,这种可选属性设计既满足了功能灵活性,又通过类型约束避免了非法值传入。

(2)组件 Props 类型定义
typescript 复制代码
// Sidebar Component
interface SidebarProps {
  items: SidebarItem[];
  selectedIndex: number;
  onSelect: (index: number) => void;
  header?: React.ReactNode;
  footer?: React.ReactNode;
  collapsible?: boolean;
  width?: number;
  backgroundColor?: string;
  activeBackgroundColor?: string;
  textColor?: string;
  activeTextColor?: string;
}

SidebarProps 接口覆盖了组件的所有配置项:必选属性 items(导航列表)、selectedIndex(当前选中索引)、onSelect(选中回调)保证组件的核心功能;可选属性 header/footer 采用 React.ReactNode 类型,支持传入任意 React 节点,实现头部/底部内容的自定义插槽;collapsible 控制折叠功能开关,各类样式属性(如 backgroundColor)则通过可选字符串类型,支持组件样式的个性化定制。

TypeScript 的强类型约束在该组件中不仅避免了「传参类型错误」「属性不存在」等运行时错误,也为开发者提供了清晰的接口提示,同时为鸿蒙跨端适配时的接口对齐提供了明确的参考标准。

2. 图标资源处理:Base64 内嵌的跨端优势

组件中所有导航图标均采用 Base64 编码内嵌的方式存储在 SIDEBAR_ICONS 对象中,而非传统的本地图片或远程图片:

typescript 复制代码
// Base64 Icons for Sidebar component
const SIDEBAR_ICONS = {
  home: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMiA5TDQgOWMxLjEgMCAyIC45IDIgMnY3YzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJ2LTdjMC0xLjEuOS0yIDItMmgxbC0xMC03WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTE2IDE1SDgiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAxMVg4IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  // 其他图标省略...
};

这种处理方式在跨端开发中具备三大核心优势:

  • 资源路径无关性 :React Native 端无需处理 iOS/Android 不同分辨率图片的命名规范(如 @2x/@3x),鸿蒙端也无需配置资源目录,只需解析 Base64 字符串即可渲染图标;
  • 加载性能优化:Base64 图标随 JS 代码一同加载,避免了图片的异步请求,减少了组件渲染的白屏时间;
  • 跨端兼容性 :SVG 格式的 Base64 图标在 React Native 和鸿蒙端均具备良好的渲染支持,且支持通过 tintColor(React Native)/fill(鸿蒙)动态修改图标颜色,满足选中/未选中状态的样式切换需求。

3. 折叠动画实现:Animated 驱动的流畅交互

组件的折叠功能基于 React Native 内置的 Animated 库实现,核心逻辑如下:

typescript 复制代码
const [collapsed, setCollapsed] = useState(false);
const animatedWidth = useRef(new Animated.Value(width)).current;

const toggleCollapse = () => {
  if (collapsible) {
    const newCollapsed = !collapsed;
    setCollapsed(newCollapsed);
    Animated.spring(animatedWidth, {
      toValue: newCollapsed ? 80 : width,
      useNativeDriver: false,
    }).start();
  }
};
  • 动画值管理 :通过 useRef 保存 Animated.Value 实例,避免组件重渲染时重复创建,保证动画状态的连续性;
  • 弹簧动画配置 :使用 Animated.spring 而非 Animated.timing,模拟物理弹簧的弹性效果,让侧边栏的展开/折叠更符合真实世界的交互体验;
  • 原生驱动限制 :由于动画目标是 width 属性(非布局属性),因此设置 useNativeDriver: false,这是 React Native 动画开发中的常见细节,若错误设置为 true 会导致动画失效。

动画值最终绑定到侧边栏容器的宽度属性上:

typescript 复制代码
<Animated.View 
  style={[
    styles.sidebarContainer, 
    { 
      width: animatedWidth,
      backgroundColor 
    }
  ]}
>
  {/* 导航内容 */}
</Animated.View>

这种基于声明式动画的实现方式,相较于原生开发的 imperative 动画,更符合 React 的开发范式,且动画逻辑与 UI 渲染解耦,便于跨端适配时替换为鸿蒙的动画 API。

4. 核心渲染逻辑:状态驱动的条件渲染

组件的导航项渲染、选中状态高亮、徽章显示等核心功能均基于 React 的状态驱动设计:

(1)导航项渲染与选中状态
typescript 复制代码
const renderItem = (item: SidebarItem, index: number) => {
  const isActive = index === selectedIndex;
  
  return (
    <TouchableOpacity
      key={item.id}
      style={[
        styles.sidebarItem,
        isActive && styles.activeSidebarItem,
        { backgroundColor: isActive ? activeBackgroundColor : 'transparent' }
      ]}
      onPress={() => onSelect(index)}
    >
      <View style={styles.itemIconContainer}>
        <Image 
          source={{ uri: SIDEBAR_ICONS[item.icon] }} 
          style={[
            styles.itemIcon,
            { tintColor: isActive ? activeTextColor : textColor }
          ]} 
        />
        {renderBadge(item.badge || 0)}
      </View>
      
      {!collapsed && (
        <Text style={[
          styles.itemText,
          { color: isActive ? activeTextColor : textColor }
        ]}>
          {item.title}
        </Text>
      )}
      
      {!collapsed && isActive && (
        <View style={styles.activeIndicator} />
      )}
    </TouchableOpacity>
  );
};

核心设计要点:

  • 选中状态联动 :通过 isActive = index === selectedIndex 判断当前项是否选中,联动修改背景色、图标颜色、文本颜色,实现视觉上的高亮反馈;
  • 折叠状态条件渲染 :折叠状态下(collapsed = true)隐藏导航文本和选中指示器,仅保留图标,最大化屏幕空间利用率;
  • TouchableOpacity 封装 :使用 TouchableOpacity 包裹导航项,既实现了点击交互,又自带点击透明度变化的反馈效果,提升用户体验。
(2)徽章数字渲染
typescript 复制代码
const renderBadge = (badgeCount: number) => {
  if (!badgeCount) return null;
  
  return (
    <View style={styles.badgeContainer}>
      <View style={styles.badge}>
        <Text style={styles.badgeText}>
          {badgeCount > 99 ? '99+' : badgeCount}
        </Text>
      </View>
    </View>
  );
};

徽章组件采用「条件渲染 + 数字格式化」的设计:无数字时不渲染,数字超过99时显示「99+」,避免徽章尺寸过大影响UI美观;通过绝对定位(badgeContainerposition: 'absolute')将徽章置于图标右上角,符合移动端导航的交互习惯。

5. 样式设计:Flex 布局的跨端适配基础

组件样式基于 React Native 的 StyleSheet.create 构建,核心采用 Flex 布局:

typescript 复制代码
const styles = StyleSheet.create({
  sidebarContainer: {
    height: '100%',
    borderRightWidth: 1,
    borderRightColor: '#1e293b',
  },
  sidebarItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 16,
    paddingHorizontal: 20,
    position: 'relative',
  },
  // 其他样式省略...
});

Flex 布局是 React Native、鸿蒙、Web 等多端通用的布局标准,该组件的样式设计完全基于 Flex 体系:

  • 横向排列 :导航项通过 flexDirection: 'row' 实现图标与文本的横向排列;
  • 垂直居中alignItems: 'center' 保证导航项内容垂直居中;
  • 高度自适应 :侧边栏容器设置 height: '100%',适配不同设备的屏幕高度。

这种样式设计为鸿蒙跨端适配提供了极大便利,只需将 React Native 的 StyleSheet 属性映射为鸿蒙的 ComponentStyle 属性,即可快速实现样式迁移。

三、鸿蒙跨端适配核心技术路径

1. 技术栈映射:React Native 与鸿蒙 ArkTS 核心对应关系

要实现组件的跨端适配,首先需明确 React Native 与鸿蒙 ArkTS 的核心技术栈映射关系,这是逻辑复用和 UI 迁移的基础:

React Native 技术点 鸿蒙 ArkTS 对应技术点 适配说明
函数式组件 + Props 结构化组件 + @Prop/@Link React 的 Props 对应鸿蒙的 @Prop(单向传递)/@Link(双向绑定)
useState/useRef @State/@Ref React 的状态钩子对应鸿蒙的状态装饰器,useRef 对应 @Ref
Animated 动画 Animator 动画 React Native 的声明式动画对应鸿蒙的属性动画/显式动画
View/Text/Image Column/Text/Image 基础UI组件一一对应,功能完全兼容
TouchableOpacity Button + onClick / GestureDetector 点击交互可通过 Button 组件或手势检测器实现
StyleSheet ComponentStyle + 内联样式 Flex 布局属性完全通用,样式属性名称略有差异(如 backgroundColor 对应 background.color
ScrollView Scroll 滚动容器功能一致,API 设计略有不同

2. 核心逻辑复用:抽离无平台依赖的纯函数

跨端适配的核心原则是「逻辑复用,UI 重写」,该组件中可直接复用的纯逻辑包括:导航项渲染逻辑、徽章数字格式化、折叠状态判断等,这些逻辑不依赖任何 React Native 特定 API,只需少量修改即可在鸿蒙端运行。

(1)类型定义复用(适配鸿蒙 ArkTS)
typescript 复制代码
// 鸿蒙 ArkTS 类型定义(复用 React Native 端的类型逻辑)
type SIDEBAR_ICONS_KEY = 'home' | 'user' | 'settings' | 'message' | 'chart' | 'folder' | 'calendar' | 'help' | 'logout';

interface SidebarItem {
  id: string;
  title: string;
  icon: SIDEBAR_ICONS_KEY;
  badge?: number;
}

interface SidebarProps {
  items: SidebarItem[];
  selectedIndex: number;
  onSelect: (index: number) => void;
  header?: ReactNode;
  footer?: ReactNode;
  collapsible?: boolean;
  width?: number;
  backgroundColor?: string;
  activeBackgroundColor?: string;
  textColor?: string;
  activeTextColor?: string;
}

鸿蒙 ArkTS 同样支持 TypeScript 类型语法,只需将 keyof typeof SIDEBAR_ICONS 替换为显式的联合类型(鸿蒙对 keyof 的支持与 React Native 一致,此处为兼容性优化),即可实现类型约束的复用。

(2)徽章渲染逻辑复用
typescript 复制代码
// 鸿蒙 ArkTS 徽章渲染逻辑(完全复用 React Native 端代码)
@Builder
renderBadge(badgeCount: number) {
  if (!badgeCount) return;
  
  Column() {
    Text(badgeCount > 99 ? '99+' : badgeCount.toString())
      .fontSize(10)
      .fontWeight(FontWeight.Bold)
      .fontColor('#ffffff');
  }
  .backgroundColor('#ef4444')
  .borderRadius(10)
  .minWidth(20)
  .height(20)
  .justifyContent(FlexAlign.Center)
  .alignItems(ItemAlign.Center)
  .padding({ left: 6, right: 6 })
  .position({ top: -6, right: -6 });
}

鸿蒙的 @Builder 装饰器对应 React 的自定义渲染函数,核心逻辑(数字判断、格式化、样式)完全复用,仅需将 React Native 的 View/Text 替换为鸿蒙的 Column/Text,样式属性调整为鸿蒙的 API 格式。

3. 动画交互适配:鸿蒙 Animator 替代 Animated

React Native 的 Animated 动画在鸿蒙端可通过 Animator 动画实现,核心适配逻辑如下:

(1)动画状态管理
typescript 复制代码
// 鸿蒙 ArkTS 动画状态管理
@State collapsed: boolean = false;
@State sidebarWidth: number = 280; // 初始宽度
@Ref animator: Animator = new Animator();

// 初始化动画配置
aboutToAppear() {
  this.animator = new Animator({
    duration: 300,
    easing: Easing.Spring(0.8, 100), // 模拟弹簧效果
    fill: FillMode.Forwards,
  });
}

// 折叠/展开切换逻辑
toggleCollapse() {
  if (!this.collapsible) return;
  this.collapsed = !this.collapsed;
  const targetWidth = this.collapsed ? 80 : 280;
  
  // 绑定动画目标属性
  this.animator.update({
    values: [this.sidebarWidth, targetWidth],
    onUpdate: (value: number) => {
      this.sidebarWidth = value;
    },
  });
  this.animator.play();
}

鸿蒙的 Animator 采用命令式 API,相较于 React Native 的声明式 Animated,核心差异在于:

  • 动画实例需手动创建和配置,通过 update 方法更新动画目标值;
  • 通过 onUpdate 回调更新组件状态,驱动 UI 重渲染;
  • 弹簧效果通过 Easing.Spring 配置,参数(阻尼、刚度)可精细调整动画的弹性效果。
(2)动画绑定到UI
typescript 复制代码
// 鸿蒙 ArkTS 动画绑定
Column() {
  // 导航内容
}
.width(this.sidebarWidth)
.height('100%')
.backgroundColor(this.backgroundColor)
.borderRight({ width: 1, color: '#1e293b' });

将动画驱动的 sidebarWidth 绑定到 Column 容器的宽度属性,实现与 React Native 端一致的折叠/展开效果。

4. 完整鸿蒙适配示例(核心组件)

以下是该侧边导航组件在鸿蒙 ArkTS 中的完整适配实现,保留了 React Native 端的所有核心功能:

typescript 复制代码
import { ReactNode } from 'react';
import { Column, Row, Text, Image, TouchableOpacity, Scroll, StyleSheet, Animator, Easing, FillMode, FlexAlign, ItemAlign, Position } from '@ohos/react';

// Base64 Icons(完全复用 React Native 端)
const SIDEBAR_ICONS = {
  home: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMiA5TDQgOWMxLjEgMCAyIC45IDIgMnY3YzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJ2LTdjMC0xLjEuOS0yIDItMmgxbC0xMC03WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTE2IDE1SDgiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAxMVg4IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  user: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNy41OCAyIDQgNS41OCA0IDEwdjRjMCA0LjQyIDMuNTggOCA4IDhzOCAzLjU4IDggOHYtNGMwLTQuNDItMy41OC04LTgtOFYxMFoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik04IDEwYzAtMi4yIDEuOC00IDQtNCAyLjIgMCA0IDEuOCA0IDQgMCAyLjItMS44IDQtNCA0cy00LTEuOC00LTRaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  settings: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTIgNHYxNCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8cGF0aCBkPSJNNyAxMmgxMCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K',
  message: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDJIMTBjLTEuMSAwLTIgLjktMiAydjRoLTJjLTEuMSAwLTIgLjktMiAydjEwYzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJWNmMwLTEuMS0uOS0yLTItMmgtMlYyem0wIDR2MmgydjJINHYtNFoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
  chart: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMNiA4aDR2MTJINnoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xOCA4aC00djEyaDR6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  folder: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDRINmEyIDIgMCAwIDAtMiAydjEyaDE2YTIgMiAwIDAgMi0yLTIiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0yMiA4LjV2LTIuN2EyLjIgMi4yIDAgMCAwLS43LTEuN2wtNS40LTIuNmEyLjIgMi4yIDAgMCAwLTEuNy0uN0gyYTIgMiAwIDAgMC0yIDJ2MTJhMiAyIDAgMCAwIDIgMmg3IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  calendar: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDRINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY2YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNmgxNHYxNHoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAydjRoIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSI4IDJ2NCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iMyAxMGgxOCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
  help: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMjIi8+CjxwYXRoIGQ9Ik0xMiAxNnYzIi8+CjxwYXRoIGQ9Ik0xMiA4aDAuMDEiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
  logout: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDN2NGgyYTEgMSAwIDAgMSAxIDF2OGExIDEgMCAwIDEtMSAxaC0yYTIgMiAwIDAgMCAwIDRoM2EyIDIgMCAwIDAgMi0yVjVhMiAyIDAgMCAwLTItMmgtM3oiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik04IDEydjRsLTQtNC00IDR2LTRhMiAyIDAgMCAxIDItMmg4YTIgMiAwIDAgMSAyIDJ6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K'
};

// 类型定义(适配鸿蒙 ArkTS)
type SIDEBAR_ICONS_KEY = keyof typeof SIDEBAR_ICONS;

interface SidebarItem {
  id: string;
  title: string;
  icon: SIDEBAR_ICONS_KEY;
  badge?: number;
}

interface SidebarProps {
  items: SidebarItem[];
  selectedIndex: number;
  onSelect: (index: number) => void;
  header?: ReactNode;
  footer?: ReactNode;
  collapsible?: boolean;
  width?: number;
  backgroundColor?: string;
  activeBackgroundColor?: string;
  textColor?: string;
  activeTextColor?: string;
}

// 鸿蒙 Sidebar 组件
const Sidebar: React.FC<SidebarProps> = ({
  items,
  selectedIndex,
  onSelect,
  header,
  footer,
  collapsible = false,
  width = 280,
  backgroundColor = '#0f172a',
  activeBackgroundColor = '#3b82f6',
  textColor = '#94a3b8',
  activeTextColor = '#ffffff'
}) => {
  // 状态管理
  const [collapsed, setCollapsed] = React.useState(false);
  const [sidebarWidth, setSidebarWidth] = React.useState(width);
  const animatorRef = React.useRef<Animator>(new Animator());

  // 初始化动画
  React.useEffect(() => {
    const animator = new Animator({
      duration: 300,
      easing: Easing.Spring(0.8, 100),
      fill: FillMode.Forwards,
      onUpdate: (value: number) => {
        setSidebarWidth(value);
      }
    });
    animatorRef.current = animator;
    return () => {
      animator.stop();
    };
  }, []);

  // 折叠/展开切换
  const toggleCollapse = () => {
    if (!collapsible) return;
    const newCollapsed = !collapsed;
    setCollapsed(newCollapsed);
    const targetWidth = newCollapsed ? 80 : width;
    
    animatorRef.current.update({
      values: [sidebarWidth, targetWidth]
    });
    animatorRef.current.play();
  };

  // 徽章渲染
  const renderBadge = (badgeCount: number) => {
    if (!badgeCount) return null;
    
    return (
      <Column style={styles.badgeContainer}>
        <Column style={styles.badge}>
          <Text style={styles.badgeText}>
            {badgeCount > 99 ? '99+' : badgeCount}
          </Text>
        </Column>
      </Column>
    );
  };

  // 导航项渲染
  const renderItem = (item: SidebarItem, index: number) => {
    const isActive = index === selectedIndex;
    
    return (
      <TouchableOpacity
        key={item.id}
        style={[
          styles.sidebarItem,
          { backgroundColor: isActive ? activeBackgroundColor : 'transparent' },
          isActive && styles.activeSidebarItem
        ]}
        onPress={() => onSelect(index)}
      >
        <Column style={styles.itemIconContainer}>
          <Image
            source={{ uri: SIDEBAR_ICONS[item.icon] }}
            style={[
              styles.itemIcon,
              { tintColor: isActive ? activeTextColor : textColor }
            ]}
          />
          {renderBadge(item.badge || 0)}
        </Column>
        
        {!collapsed && (
          <Text style={[
            styles.itemText,
            { color: isActive ? activeTextColor : textColor }
          ]}>
            {item.title}
          </Text>
        )}
        
        {!collapsed && isActive && (
          <Column style={styles.activeIndicator} />
        )}
      </TouchableOpacity>
    );
  };

  return (
    <Column
      style={[
        styles.sidebarContainer,
        {
          width: sidebarWidth,
          backgroundColor
        }
      ]}
    >
      {header && (
        <Column style={styles.sidebarHeader}>
          <Column style={styles.headerContent}>
            {header}
          </Column>
          {collapsible && (
            <TouchableOpacity
              style={styles.collapseButton}
              onPress={toggleCollapse}
            >
              <Text style={[styles.collapseText, { color: textColor }]}>
                {collapsed ? '>>' : '<<'}
              </Text>
            </TouchableOpacity>
          )}
        </Column>
      )}
      
      <Scroll style={styles.sidebarContent}>
        {items.map((item, index) => renderItem(item, index))}
      </Scroll>
      
      {footer && (
        <Column style={styles.sidebarFooter}>
          {footer}
        </Column>
      )}
    </Column>
  );
};

// 样式定义(适配鸿蒙)
const styles = StyleSheet.create({
  sidebarContainer: {
    height: '100%',
    borderRightWidth: 1,
    borderRightColor: '#1e293b',
    flexDirection: 'column'
  },
  sidebarHeader: {
    padding: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#1e293b',
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center'
  },
  headerContent: {
    flex: 1
  },
  collapseButton: {
    padding: 8,
    borderRadius: 20
  },
  collapseText: {
    fontSize: 18,
    fontWeight: 'bold'
  },
  sidebarContent: {
    flex: 1
  },
  sidebarItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 16,
    paddingHorizontal: 20,
    position: 'relative'
  },
  activeSidebarItem: {
    borderRightWidth: 3,
    borderRightColor: '#ffffff'
  },
  itemIconContainer: {
    width: 24,
    height: 24,
    marginRight: 16,
    position: 'relative'
  },
  itemIcon: {
    width: 24,
    height: 24
  },
  badgeContainer: {
    position: 'absolute',
    top: -6,
    right: -6
  },
  badge: {
    backgroundColor: '#ef4444',
    borderRadius: 10,
    minWidth: 20,
    height: 20,
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 6
  },
  badgeText: {
    color: '#ffffff',
    fontSize: 10,
    fontWeight: 'bold'
  },
  itemText: {
    fontSize: 16,
    fontWeight: '500',
    flex: 1
  },
  activeIndicator: {
    position: 'absolute',
    right: 0,
    top: 0,
    bottom: 0,
    width: 3,
    backgroundColor: '#ffffff'
  },
  sidebarFooter: {
    padding: 20,
    borderTopWidth: 1,
    borderTopColor: '#1e293b'
  }
});

export default Sidebar;

四、跨端适配最佳实践总结

1. 架构设计层面

  • 逻辑与UI彻底解耦:开发跨端组件时,需将分页算法、徽章格式化、状态判断等核心逻辑抽离为纯函数,不依赖任何平台特定 API;UI 渲染层则针对不同平台的组件特性单独实现,确保逻辑复用率最大化。
  • 接口标准化:通过 TypeScript/ArkTS 定义统一的组件接口(Props),保证 React Native 端与鸿蒙端的入参、回调函数格式一致,降低跨端开发的沟通成本。
  • 资源统一管理:优先采用 Base64 内嵌图标、远程配置样式等方式,避免平台间资源路径、命名规范的差异导致的适配问题。

2. 技术实现层面

  • 动画交互适配 :React Native 的 Animated 动画可映射为鸿蒙的 Animator 动画,核心是保留动画的时序、缓动效果配置,替换动画驱动的 API 实现;
  • 样式适配 :Flex 布局属性可完全复用,只需将 React Native 的 StyleSheet 属性(如 backgroundColor)映射为鸿蒙的 ComponentStyle 属性(如 background.color);
  • 交互组件适配 :React Native 的 TouchableOpacity 可替换为鸿蒙的 Button 组件或 GestureDetector,核心是保留点击回调逻辑,替换交互触发的载体。

3. 性能优化层面

  • 避免过度渲染 :在 React Native 端使用 useMemo/useCallback 缓存计算结果和回调函数,鸿蒙端则通过 @Memo 装饰器优化组件重渲染;
  • 动画性能优化 :优先使用原生驱动的动画(React Native 的 useNativeDriver: true、鸿蒙的属性动画),避免 JS 线程与 UI 线程的频繁通信;
  • 资源加载优化:Base64 图标虽便捷,但过多的 Base64 编码会增加 JS 包体积,可结合按需加载、图片压缩等方式平衡性能与适配成本。

五、总结

本文以 React Native 侧边导航组件为例,系统解读了其核心技术实现,并详细探讨了向鸿蒙跨端适配的完整路径。该组件的设计充分体现了跨端开发的「逻辑复用、UI 适配」核心思想,通过 TypeScript 类型约束保证了代码的健壮性,通过 Base64 图标、Flex 布局等技术降低了跨端适配成本,通过 Animated 动画提升了用户交互体验。


真实演示案例代码:

js 复制代码
import React, { useState, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Image, Animated } from 'react-native';

// Base64 Icons for Sidebar component
const SIDEBAR_ICONS = {
  home: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMiA5TDQgOWMxLjEgMCAyIC45IDIgMnY3YzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJ2LTdjMC0xLjEuOS0yIDItMmgxbC0xMC03WiIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTE2IDE1SDgiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAxMVg4IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  user: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNy41OCAyIDQgNS41OCA0IDEwdjRjMCA0LjQyIDMuNTggOCA4IDhzOCAzLjU4IDggOHYtNGMwLTQuNDItMy41OC04LTgtOFYxMFoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik04IDEwYzAtMi4yIDEuOC00IDQtNCAyLjIgMCA0IDEuOCA0IDQgMCAyLjItMS44IDQtNCA0cy00LTEuOC00LTRaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  settings: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTIgNHYxNCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8cGF0aCBkPSJNNyAxMmgxMCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K',
  message: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDJIMTBjLTEuMSAwLTIgLjktMiAydjRoLTJjLTEuMSAwLTIgLjktMiAydjEwYzAgMS4xLjkgMiAyIDJoMTJjMS4xIDAgMi0uOSAyLTJWNmMwLTEuMS0uOS0yLTItMmgtMlYyem0wIDR2MmgydjJINHYtNFoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
  chart: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMNiA4aDR2MTJINnoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xOCA4aC00djEyaDR6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  folder: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwIDRINmEyIDIgMCAwIDAtMiAydjEyaDE2YTIgMiAwIDAgMi0yLTIiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0yMiA4LjV2LTIuN2EyLjIgMi4yIDAgMCAwLS43LTEuN2wtNS40LTIuNmEyLjIgMi4yIDAgMCAwLTEuNy0uN0gyYTIgMiAwIDAgMC0yIDJ2MTJhMiAyIDAgMCAwIDIgMmg3IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K',
  calendar: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE5IDRINWMtMS4xIDAtMiAuOS0yIDJ2MTRjMCAxLjEuOSAyIDIgMmgxNGMxLjEgMCAyLS45IDItMlY2YzAtMS4xLS45LTItMi0yem0wIDE2SDVWNmgxNHYxNHoiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xNiAydjRoIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSI4IDJ2NCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iMyAxMGgxOCIgc3Ryb2tlPSIjRkZGRkZGIiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg==',
  help: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJaIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8cGF0aCBkPSJNMTIgMTZ2LTMiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik0xMiA4aDAuMDEiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+Cjwvc3ZnPgo=',
  logout: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE1IDN2NGgyYTEgMSAwIDAgMSAxIDF2OGExIDEgMCAwIDEtMSAxaC0yYTIgMiAwIDAgMCAwIDRoM2EyIDIgMCAwIDAgMi0yVjVhMiAyIDAgMCAwLTItMmgtM3oiIHN0cm9rZT0iI0ZGRkZGRiIgc3Ryb2tlLXdpZHRoPSIyIi8+CjxwYXRoIGQ9Ik04IDEydjRsLTQtNC00IDR2LTRhMiAyIDAgMCAxIDItMmg4YTIgMiAwIDAgMSAyIDJ6IiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iMiIvPgo8L3N2Zz4K'
};

// Sidebar Item Interface
interface SidebarItem {
  id: string;
  title: string;
  icon: keyof typeof SIDEBAR_ICONS;
  badge?: number;
}

// Sidebar Component
interface SidebarProps {
  items: SidebarItem[];
  selectedIndex: number;
  onSelect: (index: number) => void;
  header?: React.ReactNode;
  footer?: React.ReactNode;
  collapsible?: boolean;
  width?: number;
  backgroundColor?: string;
  activeBackgroundColor?: string;
  textColor?: string;
  activeTextColor?: string;
}

const Sidebar: React.FC<SidebarProps> = ({ 
  items, 
  selectedIndex, 
  onSelect,
  header,
  footer,
  collapsible = false,
  width = 280,
  backgroundColor = '#0f172a',
  activeBackgroundColor = '#3b82f6',
  textColor = '#94a3b8',
  activeTextColor = '#ffffff'
}) => {
  const [collapsed, setCollapsed] = useState(false);
  const animatedWidth = useRef(new Animated.Value(width)).current;

  const toggleCollapse = () => {
    if (collapsible) {
      const newCollapsed = !collapsed;
      setCollapsed(newCollapsed);
      Animated.spring(animatedWidth, {
        toValue: newCollapsed ? 80 : width,
        useNativeDriver: false,
      }).start();
    }
  };

  const renderBadge = (badgeCount: number) => {
    if (!badgeCount) return null;
    
    return (
      <View style={styles.badgeContainer}>
        <View style={styles.badge}>
          <Text style={styles.badgeText}>
            {badgeCount > 99 ? '99+' : badgeCount}
          </Text>
        </View>
      </View>
    );
  };

  const renderItem = (item: SidebarItem, index: number) => {
    const isActive = index === selectedIndex;
    
    return (
      <TouchableOpacity
        key={item.id}
        style={[
          styles.sidebarItem,
          isActive && styles.activeSidebarItem,
          { backgroundColor: isActive ? activeBackgroundColor : 'transparent' }
        ]}
        onPress={() => onSelect(index)}
      >
        <View style={styles.itemIconContainer}>
          <Image 
            source={{ uri: SIDEBAR_ICONS[item.icon] }} 
            style={[
              styles.itemIcon,
              { tintColor: isActive ? activeTextColor : textColor }
            ]} 
          />
          {renderBadge(item.badge || 0)}
        </View>
        
        {!collapsed && (
          <Text style={[
            styles.itemText,
            { color: isActive ? activeTextColor : textColor }
          ]}>
            {item.title}
          </Text>
        )}
        
        {!collapsed && isActive && (
          <View style={styles.activeIndicator} />
        )}
      </TouchableOpacity>
    );
  };

  return (
    <Animated.View 
      style={[
        styles.sidebarContainer, 
        { 
          width: animatedWidth,
          backgroundColor 
        }
      ]}
    >
      {header && (
        <View style={styles.sidebarHeader}>
          {header}
          {collapsible && (
            <TouchableOpacity 
              style={styles.collapseButton}
              onPress={toggleCollapse}
            >
              <Text style={[styles.collapseText, { color: textColor }]}>
                {collapsed ? '>>' : '<<'}
              </Text>
            </TouchableOpacity>
          )}
        </View>
      )}
      
      <ScrollView style={styles.sidebarContent}>
        {items.map((item, index) => renderItem(item, index))}
      </ScrollView>
      
      {footer && (
        <View style={styles.sidebarFooter}>
          {footer}
        </View>
      )}
    </Animated.View>
  );
};

// Main App Component
const SidebarComponentApp = () => {
  const [selectedIndex, setSelectedIndex] = useState(0);
  
  const sidebarItems: SidebarItem[] = [
    { id: '1', title: '首页', icon: 'home', badge: 0 },
    { id: '2', title: '个人中心', icon: 'user', badge: 3 },
    { id: '3', title: '消息中心', icon: 'message', badge: 12 },
    { id: '4', title: '数据分析', icon: 'chart', badge: 0 },
    { id: '5', title: '文件管理', icon: 'folder', badge: 5 },
    { id: '6', title: '日程安排', icon: 'calendar', badge: 0 },
    { id: '7', title: '系统设置', icon: 'settings', badge: 0 },
    { id: '8', title: '帮助中心', icon: 'help', badge: 0 }
  ];

  const handleSelect = (index: number) => {
    setSelectedIndex(index);
    console.log(`选择了: ${sidebarItems[index].title}`);
  };

  const renderHeader = () => (
    <View style={styles.headerContent}>
      <View style={styles.logoContainer}>
        <Text style={styles.logoText}>管理系统</Text>
      </View>
    </View>
  );

  const renderFooter = () => (
    <TouchableOpacity 
      style={styles.logoutButton}
      onPress={() => console.log('退出登录')}
    >
      <Image 
        source={{ uri: SIDEBAR_ICONS.logout }} 
        style={styles.logoutIcon} 
      />
      <Text style={styles.logoutText}>退出登录</Text>
    </TouchableOpacity>
  );

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.headerTitle}>侧边导航组件</Text>
        <Text style={styles.headerSubtitle}>绑定当前选中项的索引</Text>
      </View>
      
      <View style={styles.content}>
        <Sidebar
          items={sidebarItems}
          selectedIndex={selectedIndex}
          onSelect={handleSelect}
          header={renderHeader()}
          footer={renderFooter()}
          collapsible={true}
          width={280}
        />
        
        <View style={styles.mainContent}>
          <View style={styles.contentCard}>
            <Text style={styles.contentTitle}>
              当前选中: {sidebarItems[selectedIndex]?.title}
            </Text>
            <Text style={styles.contentDescription}>
              这是 {sidebarItems[selectedIndex]?.title} 页面的内容区域。
              您可以通过侧边导航切换不同的功能模块。
            </Text>
          </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}>丰富的Base64图标</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>
        </View>
      </View>
      
      <View style={styles.footer}>
        <Text style={styles.footerText}>© 2023 侧边导航组件 | 现代化UI组件库</Text>
      </View>
    </View>
  );
};

const { width, height } = Dimensions.get('window');

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#f1f5f9',
  },
  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: {
    flex: 1,
    flexDirection: 'row',
  },
  mainContent: {
    flex: 1,
    padding: 20,
  },
  sidebarContainer: {
    height: '100%',
    borderRightWidth: 1,
    borderRightColor: '#1e293b',
  },
  sidebarHeader: {
    padding: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#1e293b',
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  headerContent: {
    flex: 1,
  },
  logoContainer: {
    alignItems: 'center',
  },
  logoText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#f8fafc',
  },
  collapseButton: {
    padding: 8,
    borderRadius: 20,
  },
  collapseText: {
    fontSize: 18,
    fontWeight: 'bold',
  },
  sidebarContent: {
    flex: 1,
  },
  sidebarItem: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 16,
    paddingHorizontal: 20,
    position: 'relative',
  },
  activeSidebarItem: {
    borderRightWidth: 3,
    borderRightColor: '#ffffff',
  },
  itemIconContainer: {
    width: 24,
    height: 24,
    marginRight: 16,
    position: 'relative',
  },
  itemIcon: {
    width: 24,
    height: 24,
  },
  badgeContainer: {
    position: 'absolute',
    top: -6,
    right: -6,
  },
  badge: {
    backgroundColor: '#ef4444',
    borderRadius: 10,
    minWidth: 20,
    height: 20,
    justifyContent: 'center',
    alignItems: 'center',
    paddingHorizontal: 6,
  },
  badgeText: {
    color: '#ffffff',
    fontSize: 10,
    fontWeight: 'bold',
  },
  itemText: {
    fontSize: 16,
    fontWeight: '500',
    flex: 1,
  },
  activeIndicator: {
    position: 'absolute',
    right: 0,
    top: 0,
    bottom: 0,
    width: 3,
    backgroundColor: '#ffffff',
  },
  sidebarFooter: {
    padding: 20,
    borderTopWidth: 1,
    borderTopColor: '#1e293b',
  },
  logoutButton: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingVertical: 12,
  },
  logoutIcon: {
    width: 20,
    height: 20,
    tintColor: '#94a3b8',
    marginRight: 12,
  },
  logoutText: {
    fontSize: 16,
    color: '#94a3b8',
    fontWeight: '500',
  },
  contentCard: {
    backgroundColor: '#ffffff',
    borderRadius: 12,
    padding: 20,
    marginBottom: 25,
    elevation: 2,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 1 },
    shadowOpacity: 0.08,
    shadowRadius: 2,
  },
  contentTitle: {
    fontSize: 20,
    fontWeight: '600',
    color: '#0f172a',
    marginBottom: 12,
  },
  contentDescription: {
    fontSize: 16,
    color: '#64748b',
    lineHeight: 24,
  },
  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,
    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 SidebarComponentApp;

打包

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

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

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


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

相关推荐
Java面试题总结2 小时前
Tube - Video Reactions
开发语言·前端·javascript
hellojackjiang20112 小时前
鸿蒙Next原生IM即时通讯RainbowTalk,纯ArkTS编写,基于开源MobileIMSDK框架
网络编程·信息与通信·harmonyos·即时通讯
lbb 小魔仙2 小时前
鸿蒙跨平台项目实战篇01:React Native Bundle版本管理详解
react native·react.js·harmonyos
2301_796512522 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Pagination 分页(绑定当前页码)
javascript·react native·react.js·ecmascript·harmonyos
谢尔登2 小时前
深入React19任务调度器Scheduler
开发语言·前端·javascript
不爱吃糖的程序媛2 小时前
Flutter 插件适配 HarmonyOS 实战:以屏幕方向控制为例
flutter·华为·harmonyos
星空22232 小时前
鸿蒙跨平台实战:React Native 在 OpenHarmony 上的 PixelFormat 图片格式处理
react native·华为·harmonyos
阿珊和她的猫2 小时前
深入解析如何监听浏览器的页面关闭事件
前端·javascript·vue.js
敲代码的柯基2 小时前
一篇文章理解tsconfig.json和vue.config.js
javascript·vue.js·json