【HarmonyOS】DAY21:React Native for OpenHarmony:Calendar 日历组件实现指南


React Native for OpenHarmony:Calendar 日历组件实现指南


🌸你好呀!我是 lbb小魔仙
🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

description: 深入解析在 OpenHarmony 6.0.0 平台上使用 React Native 0.72.5 实现日历组件的技术方案,包含平台适配要点、最佳实践和完整代码示例
tags:

  • react-native
  • openharmony
  • calendar
  • cross-platform
    category: 移动开发

React Native for OpenHarmony:Calendar 日历组件实现指南

概述

日历组件是移动应用中的核心UI元素,广泛应用于日程管理、预订系统、活动提醒等场景。本文将详细讲解如何在 OpenHarmony 6.0.0 (API 20) 平台上使用 React Native 0.72.5 实现功能完善的日历组件。

技术背景

平台架构

React Native 在 OpenHarmony 平台上的运行依赖于以下核心组件:

复制代码
┌─────────────────────────────────────────────────────────┐
│                   JavaScript 层                          │
│  (React Native 业务逻辑、状态管理、UI 组件)              │
└────────────────────┬────────────────────────────────────┘
                     │ JSI (JavaScript Interface)
┌────────────────────┴────────────────────────────────────┐
│                 桥接适配层                               │
│  (@react-native-oh/react-native-harmony)                │
└────────────────────┬────────────────────────────────────┘
                     │ Native Module Bridge
┌────────────────────┴────────────────────────────────────┐
│              OpenHarmony 原生层                          │
│  (ArkUI 组件系统、渲染引擎、原生 API)                    │
└─────────────────────────────────────────────────────────┘

核心特性对比

特性 iOS/Android OpenHarmony 6.0.0 适配策略
渲染引擎 原生渲染 ArkUI 渲染引擎 通过桥接层适配
日期格式化 Intl API @ohos.intl 模块 统一使用 JS 标准API
时区处理 系统自动 需显式指定 使用 UTC 时间存储
本地化 系统支持 有限支持 手动配置多语言资源

实现方案

环境要求

json 复制代码
{
  "dependencies": {
    "react": "18.2.0",
    "react-native": "0.72.5",
    "@react-native-oh/react-native-harmony": "^0.72.108"
  },
  "devDependencies": {
    "typescript": "^4.8.4"
  }
}

组件设计

类型定义
typescript 复制代码
// types/calendar.ts
export interface DateCell {
  day: number;
  dateString: string;
  isCurrentMonth: boolean;
  isToday: boolean;
  isDisabled?: boolean;
}

export interface CalendarProps {
  currentDate: Date;
  selectedDate?: string;
  minDate?: Date;
  maxDate?: Date;
  onDateSelect: (dateString: string) => void;
  onMonthChange?: (date: Date) => void;
}

export interface MonthData {
  year: number;
  month: number;
  days: DateCell[];
}
日期工具类
typescript 复制代码
// utils/dateUtils.ts
export class DateUtils {
  private static readonly WEEK_DAYS = ['日', '一', '二', '三', '四', '五', '六'];
  private static readonly MONTH_NAMES = [
    '一月', '二月', '三月', '四月', '五月', '六月',
    '七月', '八月', '九月', '十月', '十一月', '十二月'
  ];

  static getWeekDays(): readonly string[] {
    return this.WEEK_DAYS;
  }

  static getMonthName(month: number): string {
    return this.MONTH_NAMES[month] || '';
  }

  static formatDateString(year: number, month: number, day: number): string {
    return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  }

  static getMonthData(date: Date, minDate?: Date, maxDate?: Date): MonthData {
    const year = date.getFullYear();
    const month = date.getMonth();
    const firstDay = new Date(year, month, 1);
    const lastDay = new Date(year, month + 1, 0);
    const firstDayOfWeek = firstDay.getDay();

    const days: DateCell[] = [];
    const today = new Date();

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

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

      const dateString = this.formatDateString(year, month, i);
      let isDisabled = false;

      if (minDate && currentDate < minDate) isDisabled = true;
      if (maxDate && currentDate > maxDate) isDisabled = true;

      days.push({
        day: i,
        dateString,
        isCurrentMonth: true,
        isToday,
        isDisabled,
      });
    }

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

    return { year, month, days };
  }

  static isSameDay(date1: Date, date2: Date): boolean {
    return (
      date1.getFullYear() === date2.getFullYear() &&
      date1.getMonth() === date2.getMonth() &&
      date1.getDate() === date2.getDate()
    );
  }
}

组件实现

typescript 复制代码
// components/Calendar.tsx
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  ScrollView,
  Dimensions,
  Platform,
} from 'react-native';
import { DateUtils, DateCell } from '../utils/dateUtils';

interface CalendarProps {
  initialDate?: Date;
  minDate?: Date;
  maxDate?: Date;
  onDateSelect?: (dateString: string) => void;
  theme?: 'light' | 'dark';
}

export const Calendar: React.FC<CalendarProps> = ({
  initialDate = new Date(),
  minDate,
  maxDate,
  onDateSelect,
  theme = 'light',
}) => {
  const [currentDate, setCurrentDate] = useState<Date>(initialDate);
  const [selectedDate, setSelectedDate] = useState<string>('');
  const screenWidth = Dimensions.get('window').width;

  // 使用 ref 避免闭包问题
  const onDateSelectRef = useRef(onDateSelect);
  useEffect(() => {
    onDateSelectRef.current = onDateSelect;
  }, [onDateSelect]);

  // 获取月份数据
  const monthData = useMemo(() => {
    return DateUtils.getMonthData(currentDate, minDate, maxDate);
  }, [currentDate, minDate, maxDate]);

  // 切换月份
  const changeMonth = useCallback((offset: number) => {
    setCurrentDate((prev) => {
      const newDate = new Date(prev);
      newDate.setMonth(newDate.getMonth() + offset);
      return newDate;
    });
  }, []);

  // 选择日期
  const selectDate = useCallback((day: DateCell) => {
    if (!day.isDisabled && day.isCurrentMonth && day.dateString) {
      setSelectedDate(day.dateString);
      onDateSelectRef.current?.(day.dateString);
    }
  }, []);

  // 渲染星期标题
  const weekHeader = useMemo(() => {
    return (
      <View style={styles.weekHeader}>
        {DateUtils.getWeekDays().map((day, index) => (
          <View key={index} style={styles.weekDayCell}>
            <Text style={styles.weekDayText}>{day}</Text>
          </View>
        ))}
      </View>
    );
  }, []);

  // 渲染日期网格
  const calendarGrid = useMemo(() => {
    const rows: React.ReactNode[] = [];

    for (let i = 0; i < 6; i++) {
      const rowDays = monthData.days.slice(i * 7, (i + 1) * 7);
      const isSelected = (day: DateCell) => selectedDate === day.dateString;

      rows.push(
        <View key={i} style={styles.weekRow}>
          {rowDays.map((day, index) => (
            <TouchableOpacity
              key={index}
              style={[
                styles.dayCell,
                !day.isCurrentMonth && styles.dayCellDisabled,
                day.isToday && styles.dayCellToday,
                isSelected(day) && styles.dayCellSelected,
                day.isDisabled && styles.dayCellDisabled,
              ]}
              onPress={() => selectDate(day)}
              disabled={day.isDisabled || !day.isCurrentMonth}
              activeOpacity={0.7}
            >
              <Text
                style={[
                  styles.dayText,
                  !day.isCurrentMonth && styles.dayTextDisabled,
                  day.isToday && styles.dayTextToday,
                  isSelected(day) && styles.dayTextSelected,
                  day.isDisabled && styles.dayTextDisabled,
                ]}
              >
                {day.day}
              </Text>
              {day.isToday && !isSelected(day) && <View style={styles.todayDot} />}
            </TouchableOpacity>
          ))}
        </View>
      );
    }

    return rows;
  }, [monthData.days, selectedDate, selectDate]);

  return (
    <View style={[styles.container, theme === 'dark' && styles.containerDark]}>
      {/* 平台信息 */}
      {Platform.OS === 'harmony' && (
        <View style={styles.platformBanner}>
          <Text style={styles.platformText}>
            OpenHarmony 6.0.0 Compatible
          </Text>
        </View>
      )}

      {/* 日历卡片 */}
      <View style={[styles.calendarCard, { width: Math.min(screenWidth - 32, 400) }]}>
        {/* 月份导航 */}
        <View style={styles.monthNavigation}>
          <TouchableOpacity
            style={styles.navButton}
            onPress={() => changeMonth(-1)}
            activeOpacity={0.7}
          >
            <Text style={styles.navButtonText}>‹</Text>
          </TouchableOpacity>
          <Text style={styles.monthText}>
            {monthData.year}年 {DateUtils.getMonthName(monthData.month)}
          </Text>
          <TouchableOpacity
            style={styles.navButton}
            onPress={() => changeMonth(1)}
            activeOpacity={0.7}
          >
            <Text style={styles.navButtonText}>›</Text>
          </TouchableOpacity>
        </View>

        {weekHeader}
        <View style={styles.daysContainer}>{calendarGrid}</View>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    backgroundColor: '#f5f5f5',
    padding: 16,
  },
  containerDark: {
    backgroundColor: '#1a1a1a',
  },
  platformBanner: {
    backgroundColor: '#007AFF',
    paddingVertical: 6,
    paddingHorizontal: 12,
    borderRadius: 6,
    alignSelf: 'flex-start',
    marginBottom: 12,
  },
  platformText: {
    color: '#fff',
    fontSize: 11,
    fontWeight: '600',
  },
  calendarCard: {
    backgroundColor: '#fff',
    borderRadius: 16,
    padding: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 4,
  },
  monthNavigation: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 16,
  },
  monthText: {
    fontSize: 18,
    fontWeight: '700',
    color: '#1a1a1a',
  },
  navButton: {
    width: 36,
    height: 36,
    borderRadius: 18,
    backgroundColor: '#f0f0f0',
    justifyContent: 'center',
    alignItems: 'center',
  },
  navButtonText: {
    fontSize: 22,
    color: '#007AFF',
    fontWeight: '600',
  },
  weekHeader: {
    flexDirection: 'row',
    marginBottom: 8,
  },
  weekDayCell: {
    flex: 1,
    height: 32,
    justifyContent: 'center',
    alignItems: 'center',
  },
  weekDayText: {
    fontSize: 13,
    fontWeight: '600',
    color: '#666',
  },
  daysContainer: {
    marginTop: 4,
  },
  weekRow: {
    flexDirection: 'row',
    marginBottom: 4,
  },
  dayCell: {
    flex: 1,
    height: 44,
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: 22,
    margin: 1,
  },
  dayCellDisabled: {
    opacity: 0.3,
  },
  dayCellToday: {
    borderWidth: 2,
    borderColor: '#007AFF',
  },
  dayCellSelected: {
    backgroundColor: '#007AFF',
  },
  dayText: {
    fontSize: 16,
    color: '#1a1a1a',
    fontWeight: '500',
  },
  dayTextDisabled: {
    color: '#999',
  },
  dayTextToday: {
    color: '#007AFF',
    fontWeight: '700',
  },
  dayTextSelected: {
    color: '#fff',
    fontWeight: '700',
  },
  todayDot: {
    position: 'absolute',
    bottom: 6,
    width: 4,
    height: 4,
    borderRadius: 2,
    backgroundColor: '#007AFF',
  },
});

平台适配要点

OpenHarmony 特殊处理

  1. 日期格式统一

    • 始终使用 ISO 8601 格式 (YYYY-MM-DD)
    • 避免依赖系统默认格式
  2. 时区处理

    • 内部存储使用 UTC 时间
    • 显示时转换为本地时间
  3. 本地化配置

    typescript 复制代码
    // 手动配置本地化资源
    const LOCALE_CONFIG = {
      'zh-CN': {
        monthNames: ['一月', '二月', /* ... */],
        weekDays: ['日', '一', /* ... */],
      },
      'en-US': {
        monthNames: ['January', 'February', /* ... */],
        weekDays: ['Sun', 'Mon', /* ... */],
      },
    };
  4. 性能优化

    • 使用 useMemo 缓存计算结果
    • 使用 useCallback 稳定函数引用
    • 限制渲染范围(±1年)

最佳实践

  1. 状态管理

    • 使用单一数据源管理日期状态
    • 避免在渲染过程中创建新对象
  2. 事件处理

    • 使用 ref 传递回调函数避免闭包陷阱
    • 添加防抖处理频繁事件
  3. 样式适配

    • 避免使用复杂的 CSS 效果
    • 使用内联样式便于调试
    • 测试不同屏幕尺寸
  4. 类型安全

    • 使用 TypeScript 定义清晰的接口
    • 避免使用 any 类型

总结

本文介绍了在 OpenHarmony 平台上实现 React Native 日历组件的完整方案。通过合理的架构设计和平台适配,可以构建出高效、稳定的跨平台日历应用。


相关资源

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

📕个人领域 :Linux/C++/java/AI

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

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

相关推荐
胖鱼罐头1 天前
RNGH:指令式 vs JSX 形式深度对比
前端·react native
麟听科技1 天前
HarmonyOS 6.0+ APP智能种植监测系统开发实战:农业传感器联动与AI种植指导落地
人工智能·分布式·学习·华为·harmonyos
前端不太难1 天前
HarmonyOS PC 焦点系统重建
华为·状态模式·harmonyos
空白诗1 天前
基础入门 Flutter for Harmony:Text 组件详解
javascript·flutter·harmonyos
lbb 小魔仙1 天前
【HarmonyOS】React Native实战+Popover内容自适应
react native·华为·harmonyos
motosheep1 天前
鸿蒙开发(四)播放 Lottie 动画实战(Canvas 渲染 + 资源加载踩坑总结)
华为·harmonyos
左手厨刀右手茼蒿1 天前
Flutter for OpenHarmony 实战:Barcode — 纯 Dart 条形码与二维码生成全指南
android·flutter·ui·华为·harmonyos
lbb 小魔仙1 天前
【HarmonyOS】React Native of HarmonyOS实战:手势组合与协同
react native·华为·harmonyos
果粒蹬i1 天前
【HarmonyOS】React Native实战项目+NativeStack原生导航
react native·华为·harmonyos
waeng_luo1 天前
HarmonyOS 应用开发 Skills
华为·harmonyos