【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

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

相关推荐
胖鱼罐头21 小时前
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