【HarmonyOS】DAY25:React Native for OpenHarmony:日期选择功能完整实现指南

React Native for OpenHarmony:日期选择功能完整实现指南


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

概述

日期选择功能是移动应用中最常见的交互组件之一。本文将系统讲解在 OpenHarmony 6.0.0 (API 20) 平台上使用 React Native 0.72.5 实现日期选择功能的完整方案。

技术选型

方案对比

方案 优点 缺点 推荐场景
react-native-calendars 功能丰富、可定制性强 包体积较大 需要高度定制的场景
纯 JS 自定义实现 轻量、完全可控 开发成本高 轻量级应用
@react-native-community/datetimepicker 原生体验好 OpenHarmony 支持有限 简单日期/时间选择
第三方付费库 功能完善、支持好 成本高 商业项目

推荐方案

对于 OpenHarmony 6.0.0 平台,推荐使用纯 JS 自定义实现的方案,原因如下:

  1. 避免原生适配复杂度
  2. 完全掌控 UI 和交互
  3. 更好的性能优化空间
  4. 减少包体积

架构设计

整体架构

复制代码
┌─────────────────────────────────────────────────────┐
│                    表现层 (UI)                       │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐         │
│  │ Calendar │  │ DatePicker│  │ RangePicker│       │
│  └──────────┘  └──────────┘  └──────────┘         │
└────────────────────┬────────────────────────────────┘
                     │
┌────────────────────┴────────────────────────────────┐
│                    业务逻辑层                        │
│  ┌──────────────┐  ┌──────────────┐                │
│  │ DateManager  │  │ RangeManager │                │
│  └──────────────┘  └──────────────┘                │
└────────────────────┬────────────────────────────────┘
                     │
┌────────────────────┴────────────────────────────────┐
│                    工具层 (Utils)                    │
│  ┌──────────────┐  ┌──────────────┐                │
│  │ DateUtils    │  │ Formatter    │                │
│  └──────────────┘  └──────────────┘                │
└─────────────────────────────────────────────────────┘

核心模块

1. 日期工具类
typescript 复制代码
// utils/dateHelper.ts
/**
 * 日期助手类 - 统一日期处理逻辑
 */
export class DateHelper {
  /**
   * 格式化日期为 ISO 字符串
   */
  static toISOString(date: Date): string {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

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

  /**
   * 获取月份的天数
   */
  static getDaysInMonth(year: number, month: number): number {
    return new Date(year, month, 0).getDate();
  }

  /**
   * 判断是否为同一天
   */
  static isSameDay(date1: Date, date2: Date): boolean {
    return (
      date1.getFullYear() === date2.getFullYear() &&
      date1.getMonth() === date2.getMonth() &&
      date1.getDate() === date2.getDate()
    );
  }

  /**
   * 判断是否为今天
   */
  static isToday(date: Date): boolean {
    return this.isSameDay(date, new Date());
  }

  /**
   * 日期比较
   */
  static compare(date1: Date, date2: Date): number {
    return date1.getTime() - date2.getTime();
  }

  /**
   * 获取日期范围
   */
  static getDateRange(startDate: Date, endDate: Date): Date[] {
    const dates: Date[] = [];
    const current = new Date(startDate);

    while (current <= endDate) {
      dates.push(new Date(current));
      current.setDate(current.getDate() + 1);
    }

    return dates;
  }

  /**
   * 添加天数
   */
  static addDays(date: Date, days: number): Date {
    const result = new Date(date);
    result.setDate(result.getDate() + days);
    return result;
  }

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

  /**
   * 获取月份第一天
   */
  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 getDayOfWeek(date: Date): number {
    return date.getDay();
  }
}
2. 日期管理器
typescript 复制代码
// managers/dateSelectionManager.ts
import { DateHelper } from '../utils/dateHelper';

export interface SelectionConfig {
  minDate?: Date;
  maxDate?: Date;
  disabledDates?: Date[];
}

export class DateSelectionManager {
  private config: SelectionConfig;

  constructor(config: SelectionConfig = {}) {
    this.config = config;
  }

  /**
   * 检查日期是否可选
   */
  isDateSelectable(date: Date): boolean {
    // 检查最小日期
    if (this.config.minDate && DateHelper.compare(date, this.config.minDate) < 0) {
      return false;
    }

    // 检查最大日期
    if (this.config.maxDate && DateHelper.compare(date, this.config.maxDate) > 0) {
      return false;
    }

    // 检查禁用日期
    if (this.config.disabledDates) {
      return !this.config.disabledDates.some(disabled =>
        DateHelper.isSameDay(date, disabled)
      );
    }

    return true;
  }

  /**
   * 获取禁用日期集合(用于快速查找)
   */
  private getDisabledDateSet(): Set<string> {
    return new Set(
      this.config.disabledDates?.map(d => DateHelper.toISOString(d)) ?? []
    );
  }

  /**
   * 生成月历数据
   */
  generateMonthData(year: number, month: number) {
    const firstDay = new Date(year, month, 1);
    const lastDay = DateHelper.getLastDayOfMonth(firstDay);
    const firstDayOfWeek = DateHelper.getDayOfWeek(firstDay);

    const days: DayCell[] = [];
    const today = new Date();
    const disabledSet = this.getDisabledDateSet();

    // 填充上月日期
    const prevMonthLastDay = DateHelper.getLastDayOfMonth(
      new Date(year, month - 1, 1)
    ).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 dateString = DateHelper.toISOString(currentDate);
      const isDisabled = !this.isDateSelectable(currentDate);

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

    // 填充下月日期
    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 };
  }

  /**
   * 更新配置
   */
  updateConfig(config: Partial<SelectionConfig>) {
    this.config = { ...this.config, ...config };
  }
}

interface DayCell {
  day: number;
  dateString: string;
  isCurrentMonth: boolean;
  isToday: boolean;
  isDisabled: boolean;
}

完整组件实现

通用日历组件

typescript 复制代码
// components/UniversalCalendar.tsx
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  ScrollView,
  Dimensions,
} from 'react-native';
import { DateHelper } from '../utils/dateHelper';
import { DateSelectionManager, SelectionConfig } from '../managers/dateSelectionManager';

interface UniversalCalendarProps {
  initialDate?: Date;
  selectionConfig?: SelectionConfig;
  onDateSelect?: (dateString: string) => void;
  theme?: {
    primaryColor?: string;
    todayColor?: string;
    textColor?: string;
    backgroundColor?: string;
  };
}

const DEFAULT_THEME = {
  primaryColor: '#007AFF',
  todayColor: '#007AFF',
  textColor: '#1a1a1a',
  backgroundColor: '#f5f5f5',
};

const LOCALE = {
  weekDays: ['日', '一', '二', '三', '四', '五', '六'] as const,
  monthNames: [
    '一月', '二月', '三月', '四月', '五月', '六月',
    '七月', '八月', '九月', '十月', '十一月', '十二月'
  ] as const,
};

export const UniversalCalendar: React.FC<UniversalCalendarProps> = ({
  initialDate = new Date(),
  selectionConfig,
  onDateSelect,
  theme = DEFAULT_THEME,
}) => {
  const [currentDate, setCurrentDate] = useState(initialDate);
  const [selectedDate, setSelectedDate] = useState<string>('');

  const manager = useMemo(
    () => new DateSelectionManager(selectionConfig),
    [selectionConfig]
  );

  const onDateSelectRef = useRef(onDateSelect);
  useEffect(() => {
    onDateSelectRef.current = onDateSelect;
  }, [onDateSelect]);

  // 获取月份数据
  const monthData = useMemo(() => {
    return manager.generateMonthData(
      currentDate.getFullYear(),
      currentDate.getMonth()
    );
  }, [currentDate, manager]);

  // 切换月份
  const changeMonth = useCallback((offset: number) => {
    setCurrentDate(DateHelper.addMonths(currentDate, offset));
  }, [currentDate]);

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

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

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

      rows.push(
        <View key={i} style={styles.weekRow}>
          {rowCells.map((cell, index) => (
            <TouchableOpacity
              key={index}
              style={[
                styles.dayCell,
                !cell.isCurrentMonth && styles.dayCellDisabled,
                cell.isToday && { borderColor: theme.todayColor },
                isSelected(cell) && {
                  backgroundColor: theme.primaryColor,
                },
                cell.isDisabled && styles.dayCellDisabled,
              ]}
              onPress={() => selectDate(cell)}
              disabled={cell.isDisabled || !cell.isCurrentMonth}
              activeOpacity={0.7}
            >
              <Text
                style={[
                  styles.dayText,
                  !cell.isCurrentMonth && styles.dayTextDisabled,
                  cell.isToday && { color: theme.todayColor },
                  isSelected(cell) && styles.dayTextSelected,
                  cell.isDisabled && styles.dayTextDisabled,
                ]}
              >
                {cell.day}
              </Text>
              {cell.isToday && !isSelected(cell) && (
                <View style={[styles.todayDot, { backgroundColor: theme.todayColor }]} />
              )}
            </TouchableOpacity>
          ))}
        </View>
      );
    }

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

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

  return (
    <ScrollView
      style={[styles.container, { backgroundColor: theme.backgroundColor }]}
      contentContainerStyle={styles.scrollContent}
    >
      {/* 日历卡片 */}
      <View
        style={[
          styles.calendarCard,
          { width: Math.min(screenWidth - 32, 400) }
        ]}
      >
        {/* 月份导航 */}
        <View style={styles.monthNavigation}>
          <TouchableOpacity
            style={styles.navButton}
            onPress={() => changeMonth(-1)}
          >
            <Text style={[styles.navButtonText, { color: theme.primaryColor }]}>
              ‹
            </Text>
          </TouchableOpacity>
          <Text style={[styles.monthText, { color: theme.textColor }]}>
            {monthData.year}年 {LOCALE.monthNames[monthData.month]}
          </Text>
          <TouchableOpacity
            style={styles.navButton}
            onPress={() => changeMonth(1)}
          >
            <Text style={[styles.navButtonText, { color: theme.primaryColor }]}>
              ›
            </Text>
          </TouchableOpacity>
        </View>

        {/* 星期标题 */}
        <View style={styles.weekHeader}>
          {LOCALE.weekDays.map((day, index) => (
            <View key={index} style={styles.weekDayCell}>
              <Text style={[styles.weekDayText, { color: theme.textColor }]}>
                {day}
              </Text>
            </View>
          ))}
        </View>

        {/* 日期网格 */}
        <View style={styles.daysContainer}>{calendarGrid}</View>
      </View>

      {/* 选中日期显示 */}
      {selectedDate && (
        <View style={[styles.infoCard, { backgroundColor: '#fff' }]}>
          <Text style={[styles.infoText, { color: theme.primaryColor }]}>
            已选择:{selectedDate}
          </Text>
        </View>
      )}
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  scrollContent: {
    padding: 16,
  },
  calendarCard: {
    backgroundColor: '#fff',
    borderRadius: 16,
    padding: 16,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 8,
    elevation: 4,
    alignSelf: 'center',
  },
  monthNavigation: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 16,
  },
  monthText: {
    fontSize: 18,
    fontWeight: '700',
  },
  navButton: {
    width: 36,
    height: 36,
    borderRadius: 18,
    backgroundColor: '#f0f0f0',
    justifyContent: 'center',
    alignItems: 'center',
  },
  navButtonText: {
    fontSize: 22,
    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,
    borderWidth: 2,
    borderColor: 'transparent',
  },
  dayCellDisabled: {
    opacity: 0.3,
  },
  dayText: {
    fontSize: 16,
    fontWeight: '500',
  },
  dayTextDisabled: {
    color: '#999',
  },
  dayTextSelected: {
    color: '#fff',
    fontWeight: '700',
  },
  todayDot: {
    position: 'absolute',
    bottom: 6,
    width: 4,
    height: 4,
    borderRadius: 2,
  },
  infoCard: {
    marginTop: 16,
    padding: 16,
    borderRadius: 10,
    alignItems: 'center',
  },
  infoText: {
    fontSize: 18,
    fontWeight: '600',
  },
});

使用示例

typescript 复制代码
// App.tsx
import React, { useState } from 'react';
import { View, StyleSheet } from 'react-native';
import { UniversalCalendar } from './components/UniversalCalendar';
import { DateHelper } from './utils/dateHelper';

const App: React.FC = () => {
  const [selectedDate, setSelectedDate] = useState<string>('');

  // 配置日期选择范围
  const selectionConfig = {
    minDate: DateHelper.addMonths(new Date(), -6), // 最早6个月前
    maxDate: DateHelper.addMonths(new Date(), 6),  // 最晚6个月后
  };

  // 自定义主题
  const customTheme = {
    primaryColor: '#4CAF50',
    todayColor: '#4CAF50',
    textColor: '#1a1a1a',
    backgroundColor: '#f5f5f5',
  };

  return (
    <View style={styles.container}>
      <UniversalCalendar
        initialDate={new Date()}
        selectionConfig={selectionConfig}
        onDateSelect={(date) => setSelectedDate(date)}
        theme={customTheme}
      />
    </View>
  );
};

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

export default App;

性能优化清单

  • 使用 useMemo 缓存月份数据计算
  • 使用 useCallback 稳定函数引用
  • 限制日期范围(±1年)
  • 避免在 render 中创建新对象
  • 使用 ref 传递回调避免闭包
  • 组件卸载时清理状态

OpenHarmony 适配要点

  1. 日期格式统一:使用 ISO 8601 格式
  2. 时区处理:内部使用 UTC 时间
  3. 本地化配置:手动配置多语言资源
  4. 简化样式:避免复杂的阴影和渐变
  5. 触摸响应:确保足够的触摸区域

总结

本文系统讲解了在 OpenHarmony 平台上实现日期选择功能的完整方案,包括技术选型、架构设计、核心模块和完整代码实现。


相关资源

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

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

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

相关推荐
梦想不只是梦与想2 小时前
HarmonyOS应用分层架构设计
harmonyos·分层架构·一次开发,多端部署
MardaWang5 小时前
当滚动逃离了框架 ——HarmonyOS Web 与原生混排滚动的冲突本质与解法
harmonyos·arkts·鸿蒙·deveco studio
tsqtsqtsq03096 小时前
DevEco Studio 介绍
harmonyos
传奇开心果编程8 小时前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
HwJack209 小时前
【共创稿事节】HarmonyOS 7文旅展陈展厅大空间 3DGS 重建的分块策略与拼接踩坑
3d·华为·harmonyos
熊猫钓鱼>_>11 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 kotlin-inject 实现依赖注入适配
开发语言·华为·kotlin·ai编程·inject·鸿蒙·openharmony
m0_7381858211 小时前
Flutter 鸿蒙化实战:media_info 适配 OpenHarmony,媒体信息与缩略图
flutter·华为·harmonyos·鸿蒙·媒体
m0_7381858212 小时前
Flutter 鸿蒙化实战:just_audio 适配 OpenHarmony,功能强大的播放器
flutter·华为·harmonyos·鸿蒙
翼辉cto12 小时前
Kotlin Multiplatform 三方库 SQLDelight 的 OpenHarmony 鸿蒙化适配实战
开发语言·kotlin·harmonyos