【HarmonyOS实战】React Native 鸿蒙版实战:Calendar 日历组件完全指南

React Native 鸿蒙版:Calendar 日历组件完全指南

本文详细介绍如何在 OpenHarmony 6.0.0 (API 20) 平台上使用 React Native 实现功能完整的日历组件,包括日期选择、事件标记、本地化配置等核心功能,并提供完整的适配要点和性能优化建议。


欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

一、项目背景

1.1 为什么需要 Calendar 组件?

日历组件是移动应用中的高频组件,广泛应用于:

  • 日程管理应用
  • 预约系统
  • 考勤打卡
  • 数据统计
  • 事件提醒

1.2 OpenHarmony 平台挑战

复制代码
┌─────────────────────────────────────────────────────────┐
│            OpenHarmony 日历组件面临的技术挑战             │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  渲染引擎差异    与标准 ECMAScript 规范存在细微差异     │
│       ↓                                              日期格式化 API 与 JavaScript 的 Intl API    │
│  日期处理           不完全兼容                          │
│       ↓                                              需要桥接 React Native 与 OpenHarmony      │
│  国际化机制      国际化方案                             │
│       ↓                                              可能影响日历组件的性能和交互体验          │
│  JS Bridge 通信                                        │
│                                                         │
└─────────────────────────────────────────────────────────┘

1.3 核心依赖

bash 复制代码
# 核心包
npm install react-native@0.72.5

# 鸿蒙适配包(关键)
npm install @react-native-oh/react-native-harmony@^0.72.108

# 可选:日历组件库
npm install react-native-calendars

二、架构设计

2.1 React Native 在 OpenHarmony 上的执行流程

复制代码
┌─────────────────────────────────────────────────────────┐
│                  React Native → OpenHarmony               │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  JavaScript 代码                                         │
│       ↓                                                 │
│  ┌─────────────┐                                        │
│  │  Metro 打包  │  JS Bundle                           │
│  └─────────────┘                                        │
│       ↓                                                 │
│  ┌─────────────┐                                        │
│  │ JSI 接口     │  高效通信                            │
│  │ (替代 Bridge)│  同步调用                            │
│  └─────────────┘                                        │
│       ↓                                                 │
│  ┌─────────────────────────────────────┐                │
│  │  React Native Core Modules          │                │
│  │  • 日期处理                          │                │
│  │  • 布局计算                          │                │
│  │  • 事件分发                          │                │
│  └─────────────────────────────────────┘                │
│       ↓                                                 │
│  ┌─────────────────────────────────────┐                │
│  │  OpenHarmony 原生模块                │                │
│  │  • UI 渲染                           │                │
│  │  • 平台 API 调用                     │                │
│  └─────────────────────────────────────┘                │
│       ↓                                                 │
│  最终渲染                                              │
│                                                         │
└─────────────────────────────────────────────────────────┘

2.2 日期处理适配要点

问题类型 标准实现 OpenHarmony 适配方案
时区处理 基于 UTC 时间 显式指定时区,避免依赖系统默认
日期格式化 Intl.DateTimeFormat 使用 @ohos.intl 模块替代
夏令时转换 自动处理 处理边界情况,避免计算错误

三、类型定义与数据结构

3.1 核心类型定义

typescript 复制代码
// src/types/calendar.ts

/**
 * 日历日期数据模型
 */
export interface CalendarDay {
  /** 日期数字 */
  day: number;
  /** 是否属于当前月份 */
  isCurrentMonth: boolean;
  /** 是否是今天 */
  isToday: boolean;
  /** 日期字符串 (YYYY-MM-DD) */
  dateString?: string;
}

/**
 * 日历组件配置
 */
export interface CalendarConfig {
  /** 当前显示的日期 */
  currentDate: Date;
  /** 选中的日期字符串 */
  selectedDate?: string;
  /** 最小可选日期 */
  minDate?: Date;
  /** 最大可选日期 */
  maxDate?: Date;
  /** 首日是星期几 (0-6, 0=周日) */
  firstDayOfWeek?: number;
  /** 星期标题 */
  weekDays?: string[];
  /** 月份名称 */
  monthNames?: string[];
}

/**
 * 日历事件
 */
export interface CalendarEvent {
  /** 日期字符串 */
  date: string;
  /** 事件类型 */
  type: 'dot' | 'custom';
  /** 颜色 */
  color?: string;
  /** 自定义内容 */
  customComponent?: React.ReactNode;
}

/**
 * 日历组件回调
 */
export interface CalendarCallbacks {
  /** 日期点击事件 */
  onDayPress?: (date: Date, dateString: string) => void;
  /** 月份变化事件 */
  onMonthChange?: (date: Date) => void;
  /** 日期长按事件 */
  onDayLongPress?: (date: Date, dateString: string) => void;
}

3.2 常量定义

typescript 复制代码
// src/constants/calendar.ts

/**
 * 星期标题(默认周日开始)
 */
export const DEFAULT_WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六'] as const;

/**
 * 月份名称
 */
export const DEFAULT_MONTH_NAMES = [
  '一月', '二月', '三月', '四月', '五月', '六月',
  '七月', '八月', '九月', '十月', '十一月', '十二月'
] as const;

/**
 * 日历常量
 */
export const CALENDAR_CONSTANTS = {
  /** 每周天数 */
  DAYS_PER_WEEK: 7,
  /** 日历行数 */
  CALENDAR_ROWS: 6,
  /** 总天数 (6行 x 7天) */
  TOTAL_DAYS: 42,
} as const;

四、核心组件实现

4.1 日历工具类

typescript 复制代码
// src/utils/CalendarUtils.ts

import type { CalendarDay } from '../types/calendar';

/**
 * 日历工具类
 * 负责日期计算和数据处理
 */
export class CalendarUtils {
  /**
   * 获取月份的所有天数数据
   * @param date 目标日期
   * @param firstDayOfWeek 首日是星期几 (0-6)
   * @returns 日历天数数组
   */
  static getMonthData(date: Date, firstDayOfWeek: number = 0): CalendarDay[] {
    const year = date.getFullYear();
    const month = date.getMonth();
    const today = new Date();

    // 获取当月第一天和最后一天
    const firstDayOfMonth = new Date(year, month, 1);
    const lastDayOfMonth = new Date(year, month + 1, 0);

    const days: CalendarDay[] = [];
    const firstDayIndex = firstDayOfMonth.getDay();

    // 填充上个月的日期
    const prevMonthLastDay = new Date(year, month, 0).getDate();
    for (let i = firstDayIndex - 1; i >= 0; i--) {
      days.push({
        day: prevMonthLastDay - i,
        isCurrentMonth: false,
        isToday: false,
      });
    }

    // 填充当月日期
    for (let i = 1; i <= lastDayOfMonth.getDate(); i++) {
      const isToday = (
        i === today.getDate() &&
        month === today.getMonth() &&
        year === today.getFullYear()
      );

      days.push({
        day: i,
        isCurrentMonth: true,
        isToday,
        dateString: this.formatDateString(year, month, i),
      });
    }

    // 填充下个月的日期(补齐到42天)
    const remainingDays = CALENDAR_CONSTANTS.TOTAL_DAYS - days.length;
    for (let i = 1; i <= remainingDays; i++) {
      days.push({
        day: i,
        isCurrentMonth: false,
        isToday: false,
      });
    }

    return days;
  }

  /**
   * 格式化日期为 YYYY-MM-DD 字符串
   */
  static formatDateString(year: number, month: number, day: number): string {
    return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  }

  /**
   * 解析日期字符串为 Date 对象
   */
  static parseDateString(dateString: string): Date | null {
    try {
      const [year, month, day] = dateString.split('-').map(Number);
      return new Date(year, month - 1, day);
    } catch {
      return null;
    }
  }

  /**
   * 检查两个日期是否是同一天
   */
  static isSameDay(date1: Date, date2: Date): boolean {
    return (
      date1.getFullYear() === date2.getFullYear() &&
      date1.getMonth() === date2.getMonth() &&
      date1.getDate() === date2.getDate()
    );
  }

  /**
   * 获取月份的第一天
   */
  static getFirstDayOfMonth(date: Date): Date {
    return new Date(date.getFullYear(), date.getMonth(), 1);
  }

  /**
   * 获取月份的最后一天
   */
  static getLastDayOfMonth(date: Date): Date {
    return new Date(date.getFullYear(), date.getMonth() + 1, 0);
  }

  /**
   * 添加月份
   */
  static addMonths(date: Date, months: number): Date {
    const newDate = new Date(date);
    newDate.setMonth(newDate.getMonth() + months);
    return newDate;
  }

  /**
   * 检查日期是否在范围内
   */
  static isDateInRange(date: Date, minDate?: Date, maxDate?: Date): boolean {
    if (minDate && date < minDate) return false;
    if (maxDate && date > maxDate) return false;
    return true;
  }
}

// 导入常量
const CALENDAR_CONSTANTS = {
  DAYS_PER_WEEK: 7,
  CALENDAR_ROWS: 6,
  TOTAL_DAYS: 42,
} as const;

4.2 日历单元格组件

typescript 复制代码
// src/components/CalendarDayCell.tsx

import React, { memo } from 'react';
import { TouchableOpacity, Text, View, StyleSheet } from 'react-native';
import type { CalendarDay } from '../types/calendar';

interface CalendarDayCellProps {
  /** 日期数据 */
  day: CalendarDay;
  /** 是否被选中 */
  isSelected: boolean;
  /** 点击事件 */
  onPress: () => void;
  /** 自定义样式 */
  customStyles?: {
    cell?: object;
    text?: object;
  };
}

/**
 * 日历单元格组件
 * 使用 memo 优化渲染性能
 */
export const CalendarDayCell = memo<CalendarDayCellProps>(({
  day,
  isSelected,
  onPress,
  customStyles,
}) => {
  const cellStyle = [
    styles.cell,
    !day.isCurrentMonth && styles.cellDisabled,
    day.isToday && !isSelected && styles.cellToday,
    isSelected && styles.cellSelected,
    customStyles?.cell,
  ];

  const textStyle = [
    styles.text,
    !day.isCurrentMonth && styles.textDisabled,
    day.isToday && styles.textToday,
    isSelected && styles.textSelected,
    customStyles?.text,
  ];

  return (
    <TouchableOpacity
      style={cellStyle}
      onPress={onPress}
      disabled={!day.isCurrentMonth}
      activeOpacity={0.7}
    >
      <Text style={textStyle}>{day.day}</Text>

      {/* 今天标记(未选中时显示) */}
      {day.isToday && !isSelected && (
        <View style={styles.todayDot} />
      )}
    </TouchableOpacity>
  );
}, (prevProps, nextProps) => {
  // 自定义比较函数
  return (
    prevProps.day.day === nextProps.day.day &&
    prevProps.day.isCurrentMonth === nextProps.day.isCurrentMonth &&
    prevProps.isSelected === nextProps.isSelected
  );
});

const styles = StyleSheet.create({
  cell: {
    width: '100%',
    aspectRatio: 1,
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: 8,
    marginVertical: 2,
    marginHorizontal: 2,
  },
  cellDisabled: {
    opacity: 0.3,
  },
  cellToday: {
    borderWidth: 1,
    borderColor: '#007AFF',
  },
  cellSelected: {
    backgroundColor: '#007AFF',
  },
  text: {
    fontSize: 16,
    color: '#333',
  },
  textDisabled: {
    color: '#999',
  },
  textToday: {
    color: '#007AFF',
    fontWeight: '600',
  },
  textSelected: {
    color: '#FFFFFF',
    fontWeight: '600',
  },
  todayDot: {
    position: 'absolute',
    bottom: 4,
    width: 4,
    height: 4,
    borderRadius: 2,
    backgroundColor: '#007AFF',
  },
});

4.3 日历主组件

typescript 复制代码
// src/components/Calendar.tsx

import React, {
  useState,
  useCallback,
  useMemo,
  useRef,
  useEffect,
} from 'react';
import {
  View,
  Text,
  StyleSheet,
  ScrollView,
  TouchableOpacity,
  Dimensions,
  Platform,
} from 'react-native';
import { CalendarUtils } from '../utils/CalendarUtils';
import { CalendarDayCell } from './CalendarDayCell';
import type { CalendarConfig, CalendarCallbacks, CalendarEvent } from '../types/calendar';
import { DEFAULT_WEEK_DAYS, DEFAULT_MONTH_NAMES } from '../constants/calendar';

interface CalendarProps extends Partial<CalendarConfig>, CalendarCallbacks {
  /** 事件标记 */
  markedDates?: Record<string, CalendarEvent>;
  /** 自定义样式 */
  customStyles?: {
    container?: object;
    header?: object;
    weekDay?: object;
  };
  /** 是否显示星期标题 */
  showWeekDays?: boolean;
}

/**
 * 日历组件
 * 完整的日历功能实现
 */
export const Calendar: React.FC<CalendarProps> = ({
  currentDate: initialDate = new Date(),
  selectedDate: initialSelectedDate,
  minDate,
  maxDate,
  firstDayOfWeek = 0,
  weekDays = DEFAULT_WEEK_DAYS,
  monthNames = DEFAULT_MONTH_NAMES,
  markedDates = {},
  onDayPress,
  onMonthChange,
  customStyles,
  showWeekDays = true,
}) => {
  // ==================== 状态管理 ====================
  const [currentDate, setCurrentDate] = useState<Date>(initialDate);
  const [selectedDate, setSelectedDate] = useState<string | undefined>(initialSelectedDate);
  const [currentMonthData, setCurrentMonthData] = useState(() =>
    CalendarUtils.getMonthData(currentDate, firstDayOfWeek)
  );

  const screenWidth = Dimensions.get('window').width;

  // ==================== 月份导航 ====================
  const changeMonth = useCallback((offset: number) => {
    setCurrentDate((prev) => {
      const newDate = CalendarUtils.addMonths(prev, offset);

      // 触发月份变化回调
      if (onMonthChange) {
        onMonthChange(newDate);
      }

      return newDate;
    });
  }, [onMonthChange]);

  // ==================== 日期选择 ====================
  const handleDayPress = useCallback((day: CalendarDay) => {
    if (!day.isCurrentMonth || !day.dateString) {
      return;
    }

    // 检查日期范围
    const date = CalendarUtils.parseDateString(day.dateString);
    if (date && !CalendarUtils.isDateInRange(date, minDate, maxDate)) {
      return;
    }

    setSelectedDate(day.dateString);

    // 触发日期点击回调
    if (onDayPress && date) {
      onDayPress(date, day.dateString);
    }
  }, [onDayPress, minDate, maxDate]);

  // ==================== 日期数据更新 ====================
  useEffect(() => {
    setCurrentMonthData(CalendarUtils.getMonthData(currentDate, firstDayOfWeek));
  }, [currentDate, firstDayOfWeek]);

  // ==================== 渲染函数 ====================

  /**
   * 渲染星期标题行
   */
  const renderWeekHeader = useCallback(() => {
    if (!showWeekDays) return null;

    return (
      <View style={[styles.weekHeader, customStyles?.weekDay]}>
        {weekDays.map((day, index) => (
          <View key={index} style={styles.weekDayCell}>
            <Text style={styles.weekDayText}>{day}</Text>
          </View>
        ))}
      </View>
    );
  }, [weekDays, showWeekDays, customStyles]);

  /**
   * 渲染日历网格
   */
  const renderCalendarGrid = useCallback(() => {
    const rows = [];
    const { DAYS_PER_WEEK } = require('../constants/calendar').CALENDAR_CONSTANTS;

    for (let i = 0; i < 6; i++) {
      const rowDays = currentMonthData.slice(i * DAYS_PER_WEEK, (i + 1) * DAYS_PER_WEEK);

      rows.push(
        <View key={i} style={styles.weekRow}>
          {rowDays.map((day, index) => {
            const isSelected = selectedDate === day.dateString;
            const hasEvent = markedDates[day.dateString || ''];

            return (
              <View key={index} style={styles.dayCellWrapper}>
                <CalendarDayCell
                  day={day}
                  isSelected={isSelected}
                  onPress={() => handleDayPress(day)}
                />

                {/* 事件标记 */}
                {hasEvent && hasEvent.type === 'dot' && (
                  <View
                    style={[
                      styles.eventDot,
                      { backgroundColor: hasEvent.color || '#FF3B30' },
                    ]}
                  />
                )}
              </View>
            );
          })}
        </View>
      );
    }

    return rows;
  }, [currentMonthData, selectedDate, markedDates, handleDayPress]);

  // ==================== 主渲染 ====================
  return (
    <ScrollView style={[styles.container, customStyles?.container]}>
      {/* 月份导航 */}
      <View style={[styles.monthNavigation, customStyles?.header]}>
        <TouchableOpacity
          style={styles.navButton}
          onPress={() => changeMonth(-1)}
          hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
        >
          <Text style={styles.navButtonText}>‹</Text>
        </TouchableOpacity>

        <Text style={styles.monthText}>
          {currentDate.getFullYear()}年 {monthNames[currentDate.getMonth()]}
        </Text>

        <TouchableOpacity
          style={styles.navButton}
          onPress={() => changeMonth(1)}
          hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
        >
          <Text style={styles.navButtonText}>›</Text>
        </TouchableOpacity>
      </View>

      {/* 星期标题 */}
      {renderWeekHeader()}

      {/* 日历网格 */}
      <View style={styles.daysContainer}>
        {renderCalendarGrid()}
      </View>

      {/* 选中日期信息 */}
      {selectedDate && (
        <View style={styles.selectedInfo}>
          <Text style={styles.selectedInfoText}>
            已选择: {selectedDate}
          </Text>
        </View>
      )}
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#FFFFFF',
  },
  monthNavigation: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingVertical: 16,
    paddingHorizontal: 20,
  },
  navButton: {
    padding: 8,
  },
  navButtonText: {
    fontSize: 24,
    color: '#007AFF',
    fontWeight: '300',
  },
  monthText: {
    fontSize: 18,
    fontWeight: '600',
    color: '#333',
  },
  weekHeader: {
    flexDirection: 'row',
    paddingBottom: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#E5E5E5',
    marginBottom: 8,
  },
  weekDayCell: {
    flex: 1,
    alignItems: 'center',
  },
  weekDayText: {
    fontSize: 14,
    fontWeight: '500',
    color: '#666',
  },
  daysContainer: {
    paddingHorizontal: 8,
  },
  weekRow: {
    flexDirection: 'row',
  },
  dayCellWrapper: {
    flex: 1,
    alignItems: 'center',
  },
  eventDot: {
    position: 'absolute',
    bottom: 4,
    width: 6,
    height: 6,
    borderRadius: 3,
  },
  selectedInfo: {
    alignItems: 'center',
    paddingVertical: 16,
    borderTopWidth: 1,
    borderTopColor: '#E5E5E5',
    marginTop: 8,
  },
  selectedInfoText: {
    fontSize: 16,
    color: '#007AFF',
    fontWeight: '500',
  },
});

五、使用示例

5.1 基础用法

typescript 复制代码
// App.tsx

import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { Calendar } from './src/components/Calendar';

export default function App() {
  const handleDayPress = (date: Date, dateString: string) => {
    console.log('Selected:', dateString);
  };

  const handleMonthChange = (date: Date) => {
    console.log('Month changed to:', date.toISOString());
  };

  return (
    <View style={styles.container}>
      <Calendar
        currentDate={new Date()}
        onDayPress={handleDayPress}
        onMonthChange={handleMonthChange}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
});

5.2 带事件标记的日历

typescript 复制代码
// EventCalendarExample.tsx

import React, { useState } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { Calendar } from './src/components/Calendar';
import type { CalendarEvent } from './src/types/calendar';

export default function EventCalendarExample() {
  const [markedDates, setMarkedDates] = useState<Record<string, CalendarEvent>>({
    [new Date().toISOString().split('T')[0]]: {
      type: 'dot',
      color: '#FF3B30',
    },
    // 添加更多事件标记...
  });

  return (
    <View style={styles.container}>
      <Text style={styles.title}>事件日历示例</Text>
      <Calendar
        currentDate={new Date()}
        markedDates={markedDates}
        onDayPress={(date, dateString) => {
          console.log('Selected:', dateString);
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
    paddingTop: 60,
  },
  title: {
    fontSize: 20,
    fontWeight: '600',
    textAlign: 'center',
    marginBottom: 16,
  },
});

六、OpenHarmony 适配要点

6.1 平台检测与适配

typescript 复制代码
// src/utils/PlatformAdapter.ts

import { Platform } from 'react-native';

/**
 * 平台适配工具类
 */
export class PlatformAdapter {
  /**
   * 检测是否运行在 OpenHarmony 平台
   */
  static isOpenHarmony(): boolean {
    return Platform.OS === 'harmony';
  }

  /**
   * 获取平台特定的日期格式化配置
   */
  static getDateFormatConfig() {
    if (this.isOpenHarmony()) {
      return {
        locale: 'zh-CN',
        options: {
          year: 'numeric',
          month: 'long',
          day: 'numeric',
        },
      };
    }

    // 其他平台的默认配置
    return {
      locale: 'en-US',
      options: {
        year: 'numeric',
        month: 'short',
        day: 'numeric',
      },
    };
  }

  /**
   * 获取本地化的星期标题
   */
  static getLocalizedWeekDays(): string[] {
    if (this.isOpenHarmony()) {
      // OpenHarmony 使用本地化数据
      return ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
    }

    return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
  }

  /**
   * 获取本地化的月份名称
   */
  static getLocalizedMonthNames(): string[] {
    if (this.isOpenHarmony()) {
      return [
        '一月', '二月', '三月', '四月', '五月', '六月',
        '七月', '八月', '九月', '十月', '十一月', '十二月'
      ];
    }

    return [
      'January', 'February', 'March', 'April', 'May', 'June',
      'July', 'August', 'September', 'October', 'November', 'December'
    ];
  }
}

6.2 适配清单

适配项 OpenHarmony 特殊处理 推荐方案
本地化 @ohos.intl 模块 手动配置本地化数据
日期格式化 不完全兼容 Intl 使用自定义格式化函数
Flexbox 嵌套容器有差异 避免深层嵌套,测试验证
触摸事件 hitSlop 处理 增加触摸区域
性能优化 渲染引擎差异 使用 memouseCallback

七、性能优化

7.1 优化策略

复制代码
┌─────────────────────────────────────────────────────────┐
│                   日历组件性能优化策略                      │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  1. 组件级优化                                          │
│     • React.memo 包装日期单元格                         │
│     • useCallback 缓存事件处理函数                       │
│     • useMemo 缓存计算结果                              │
│                                                         │
│  2. 数据优化                                            │
│     • 避免不必要的重新计算                              │
│     • 使用原始数据类型                                  │
│     • 减少对象创建                                      │
│                                                         │
│  3. 渲染优化                                            │
│     • 避免深层嵌套                                      │
│     • 使用绝对定位而非嵌套 Flex                         │
│     • 移除不必要的透明度变化                            │
│                                                         │
│  4. 内存优化                                            │
│     • 懒加载非关键功能                                  │
│     • 及时清理订阅和定时器                              │
│                                                         │
└─────────────────────────────────────────────────────────┘

7.2 性能优化代码示例

typescript 复制代码
// src/components/OptimizedCalendar.tsx

import React, { useMemo, useCallback, memo } from 'react';
import { View } from 'react-native';

/**
 * 优化的日历行组件
 */
export const CalendarRow = memo(({ days, onDayPress }) => {
  // 缓存渲染的单元格
  const cells = useMemo(() => {
    return days.map((day, index) => (
      <CalendarDayCell
        key={`${day.dateString}-${day.isCurrentMonth}`}
        day={day}
        isSelected={false}
        onPress={() => onDayPress(day)}
      />
    ));
  }, [days, onDayPress]);

  return (
    <View style={styles.row}>
      {cells}
    </View>
  );
});

/**
 * 性能监控 Hook
 */
export function usePerformanceMonitor(componentName: string) {
  if (__DEV__) {
    const renderCount = useRef(0);
    const lastRenderTime = useRef(Date.now());

    useEffect(() => {
      renderCount.current += 1;
      const currentTime = Date.now();
      const timeSinceLastRender = currentTime - lastRenderTime.current;

      console.log(
        `[Performance] ${componentName} render #${renderCount.current}, ` +
        `time since last: ${timeSinceLastRender}ms`
      );

      lastRenderTime.current = currentTime;
    });
  }
}

八、完整示例应用

typescript 复制代码
// CalendarDemoApp.tsx

/**
 * 日历组件完整演示应用
 * 展示所有核心功能和适配要点
 */

import React, { useState, useCallback } from 'react';
import {
  SafeAreaView,
  ScrollView,
  StyleSheet,
  View,
  Text,
  TouchableOpacity,
  Alert,
} from 'react-native';
import { Calendar } from './src/components/Calendar';
import { PlatformAdapter } from './src/utils/PlatformAdapter';
import type { CalendarEvent } from './src/types/calendar';

export default function CalendarDemoApp() {
  const [selectedDate, setSelectedDate] = useState<string>('');
  const [markedDates, setMarkedDates] = useState<Record<string, CalendarEvent>>({});

  // 获取本地化配置
  const weekDays = PlatformAdapter.getLocalizedWeekDays();
  const monthNames = PlatformAdapter.getLocalizedMonthNames();

  const handleDayPress = useCallback((date: Date, dateString: string) => {
    setSelectedDate(dateString);
    Alert.alert('日期选择', `您选择了: ${dateString}`);
  }, []);

  const handleAddEvent = useCallback(() => {
    if (!selectedDate) {
      Alert.alert('提示', '请先选择一个日期');
      return;
    }

    setMarkedDates(prev => ({
      ...prev,
      [selectedDate]: {
        type: 'dot',
        color: '#FF3B30',
      },
    }));

    Alert.alert('成功', `已为 ${selectedDate} 添加事件标记`);
  }, [selectedDate]);

  return (
    <SafeAreaView style={styles.container}>
      {/* 平台信息 */}
      <View style={styles.platformBanner}>
        <Text style={styles.platformText}>
          {PlatformAdapter.isOpenHarmony() ? 'OpenHarmony' : 'React Native'} 日历组件
        </Text>
      </View>

      {/* 日历组件 */}
      <Calendar
        currentDate={new Date()}
        selectedDate={selectedDate}
        weekDays={weekDays}
        monthNames={monthNames}
        markedDates={markedDates}
        onDayPress={handleDayPress}
      />

      {/* 操作按钮 */}
      <View style={styles.actions}>
        <TouchableOpacity
          style={styles.button}
          onPress={handleAddEvent}
        >
          <Text style={styles.buttonText}>添加事件</Text>
        </TouchableOpacity>

        <TouchableOpacity
          style={[styles.button, styles.buttonSecondary]}
          onPress={() => setSelectedDate('')}
        >
          <Text style={styles.buttonText}>清除选择</Text>
        </TouchableOpacity>
      </View>

      {/* 功能说明 */}
      <View style={styles.features}>
        <Text style={styles.featuresTitle}>核心功能</Text>
        <FeatureItem text="日期选择 - 点击日期进行选择" />
        <FeatureItem text="月份导航 - 左右切换月份" />
        <FeatureItem text="事件标记 - 红点标记有事件的日期" />
        <FeatureItem text="本地化 - 自动适配平台语言" />
      </View>
    </SafeAreaView>
  );
}

function FeatureItem({ text }: { text: string }) {
  return (
    <View style={styles.featureItem}>
      <Text style={styles.featureBullet}>•</Text>
      <Text style={styles.featureText}>{text}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  platformBanner: {
    backgroundColor: '#007AFF',
    paddingVertical: 12,
    paddingHorizontal: 20,
  },
  platformText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: '600',
    textAlign: 'center',
  },
  actions: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    padding: 20,
  },
  button: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 32,
    paddingVertical: 12,
    borderRadius: 8,
  },
  buttonSecondary: {
    backgroundColor: '#8E8E93',
  },
  buttonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: '600',
  },
  features: {
    backgroundColor: '#FFFFFF',
    margin: 20,
    padding: 20,
    borderRadius: 12,
  },
  featuresTitle: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 16,
  },
  featureItem: {
    flexDirection: 'row',
    marginBottom: 12,
  },
  featureBullet: {
    fontSize: 20,
    color: '#007AFF',
    marginRight: 8,
  },
  featureText: {
    fontSize: 14,
    color: '#333',
  },
});

九、总结

9.1 核心要点

主题 关键要点
架构设计 JSI 替代 Bridge,提供更高效的通信机制
日期处理 使用工具类封装,确保 OpenHarmony 兼容性
本地化 手动配置本地化数据,避免依赖系统 API
性能优化 memo + useCallback + useMemo 减少重渲染
平台适配 平台检测 + 条件渲染处理差异

9.2 最佳实践

  1. 类型安全:使用 TypeScript 定义完整的类型系统
  2. 组件拆分:将日历组件拆分为可复用的子组件
  3. 性能优化:使用 React.memo 和 useCallback 优化渲染
  4. 错误处理:添加边界检查和错误恢复机制
  5. 文档完善:提供清晰的 API 文档和使用示例

9.3 下一步

  • 实现范围选择功能
  • 添加自定义日期渲染
  • 支持多选日期
  • 实现日程事件视图
  • 添加动画效果

十、资源链接


结语:通过本文的详细介绍和代码实现,我们展示了如何在 OpenHarmony 平台上使用 React Native 构建功能完整的日历组件。关键在于理解平台差异、做好类型定义、优化性能,并遵循最佳实践。希望这份指南能帮助你在鸿蒙生态中开发出优秀的日历组件!
📕个人领域 :Linux/C++/java/AI

🚀 个人主页有点流鼻涕 · CSDN

💬 座右铭 : "向光而行,沐光而生。"

相关推荐
一只大侠的侠2 小时前
Flutter开源鸿蒙跨平台训练营 Day 3
flutter·开源·harmonyos
盐焗西兰花2 小时前
鸿蒙学习实战之路-Reader Kit自定义字体最佳实践
学习·华为·harmonyos
_waylau3 小时前
鸿蒙架构师修炼之道-架构师的职责是什么?
开发语言·华为·harmonyos·鸿蒙
一只大侠的侠3 小时前
【Harmonyos】Flutter开源鸿蒙跨平台训练营 Day 2 鸿蒙跨平台开发环境搭建与工程实践
flutter·开源·harmonyos
LYFlied4 小时前
从 Vue 到 React,再到 React Native:资深前端开发者的平滑过渡指南
vue.js·react native·react.js
王码码20356 小时前
Flutter for OpenHarmony 实战之基础组件:第三十一篇 Chip 系列组件 — 灵活的标签化交互
android·flutter·交互·harmonyos
坚果派·白晓明7 小时前
在鸿蒙设备上快速验证由lycium工具快速交叉编译的C/C++三方库
c语言·c++·harmonyos·鸿蒙·编程语言·openharmony·三方库
lbb 小魔仙7 小时前
【HarmonyOS实战】OpenHarmony + RN:自定义 useFormik 表单处理
react native·harmonyos
果粒蹬i8 小时前
【HarmonyOS】DAY7:鸿蒙跨平台 Tab 开发问题与列表操作难点深度复盘
华为·harmonyos