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

一、前言

在跨端应用开发中,日期选择是一个极其常见且高频使用的功能。随着OpenHarmony生态的蓬勃发展,React Native for OpenHarmony(以下简称RNOH)为开发者提供了在OpenHarmony设备上运行React Native应用的能力。本文将详细介绍如何在RNOH框架下完整实现一个日期选择功能,并提供可直接复用的代码示例。

二、环境准备

在开始实现日期选择功能前,请确保您的开发环境满足以下条件:

2.1 基础环境

  • Node.js 16+
  • OpenHarmony SDK API 9+
  • DevEco Studio 4.0+
  • React Native 0.72+

2.2 项目初始化

bash 复制代码
# 创建新的RNOH项目
npx @react-native-oh/create-react-native-app my-datepicker-app
cd my-datepicker-app

# 安装依赖
npm install

三、日期选择器方案选型

在RNOH中,实现日期选择主要有三种方案:

方案 优点 缺点 适用场景
原生日期选择器 性能最好,符合系统交互 定制性有限 简单日期选择
自定义UI组件 完全可控,样式统一 开发成本较高 复杂业务场景
第三方库适配 功能丰富 可能存在兼容性问题 快速开发

本文将以原生日期选择器 为主,同时提供自定义UI组件的实现作为备选方案。

四、原生日期选择器实现

4.1 创建日期选择桥接模块

首先,我们需要在OpenHarmony原生侧实现日期选择器的桥接模块:

entry/src/main/ets/DatePickerModule.ets

typescript 复制代码
import { TurboModule, TurboModuleContext } from '@rnoh/react-native-openharmony/ts';
import { BusinessError } from '@ohos.base';
import picker from '@ohos.picker';

export class DatePickerTurboModule extends TurboModule {
  constructor(ctx: TurboModuleContext) {
    super(ctx);
  }

  showDatePicker(options: DatePickerOptions): Promise<DatePickerResult> {
    return new Promise((resolve, reject) => {
      try {
        const datePicker = new picker.DatePickerDialog();
        
        // 设置初始日期
        let date = options.date ? new Date(options.date) : new Date();
        datePicker.setDate({
          year: date.getFullYear(),
          month: date.getMonth() + 1,
          day: date.getDate()
        }, false);

        // 设置最小/最大日期
        if (options.minDate) {
          let minDate = new Date(options.minDate);
          datePicker.setMinDate({
            year: minDate.getFullYear(),
            month: minDate.getMonth() + 1,
            day: minDate.getDate()
          }, false);
        }

        if (options.maxDate) {
          let maxDate = new Date(options.maxDate);
          datePicker.setMaxDate({
            year: maxDate.getFullYear(),
            month: maxDate.getMonth() + 1,
            day: maxDate.getDate()
          }, false);
        }

        // 显示日期选择器
        datePicker.show({
          onAccept: (result) => {
            const selectedDate = `${result.year}-${result.month}-${result.day}`;
            resolve({
              year: result.year,
              month: result.month,
              day: result.day,
              dateString: selectedDate,
              timestamp: new Date(result.year, result.month - 1, result.day).getTime()
            });
          },
          onCancel: () => {
            reject(new Error('用户取消选择'));
          }
        });
      } catch (error) {
        reject(error as BusinessError);
      }
    });
  }
}

interface DatePickerOptions {
  date?: number;
  minDate?: number;
  maxDate?: number;
}

interface DatePickerResult {
  year: number;
  month: number;
  day: number;
  dateString: string;
  timestamp: number;
}

4.2 注册TurboModule

entry/src/main/ets/rnoh/Library.ets

typescript 复制代码
import { DatePickerTurboModule } from '../DatePickerModule.ets';

export function createDatePickerTurboModule(ctx: TurboModuleContext) {
  return new DatePickerTurboModule(ctx);
}

entry/src/main/ets/rnoh/generated/RNOHCorePackage/Index.ets

typescript 复制代码
export * from './RNOHCorePackage';
// 添加以下导出
export * from '../../DatePickerModule';

4.3 JavaScript侧封装

src/utils/DatePicker.ts

typescript 复制代码
import { TurboModule, TurboModuleRegistry } from 'react-native';

interface DatePickerResult {
  year: number;
  month: number;
  day: number;
  dateString: string;
  timestamp: number;
}

interface DatePickerOptions {
  date?: Date;
  minDate?: Date;
  maxDate?: Date;
}

class DatePicker {
  private turboModule: TurboModule;

  constructor() {
    this.turboModule = TurboModuleRegistry.getEnforcing('DatePickerTurboModule');
  }

  async showDatePicker(options: DatePickerOptions = {}): Promise<DatePickerResult> {
    try {
      const pickerOptions = {
        date: options.date?.getTime(),
        minDate: options.minDate?.getTime(),
        maxDate: options.maxDate?.getTime(),
      };

      const result = await this.turboModule.showDatePicker(pickerOptions);
      return result;
    } catch (error) {
      throw error;
    }
  }
}

export default new DatePicker();

4.4 完整日期选择组件

src/components/DatePickerComponent.tsx

typescript 复制代码
import React, { useState } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Alert,
} from 'react-native';
import DatePicker from '../utils/DatePicker';

interface DatePickerComponentProps {
  onDateSelected?: (date: string) => void;
  initialDate?: Date;
  minDate?: Date;
  maxDate?: Date;
  mode?: 'date' | 'datetime' | 'time';
  format?: string;
}

const DatePickerComponent: React.FC<DatePickerComponentProps> = ({
  onDateSelected,
  initialDate = new Date(),
  minDate,
  maxDate,
  mode = 'date',
}) => {
  const [selectedDate, setSelectedDate] = useState(initialDate);
  const [showPicker, setShowPicker] = useState(false);

  const handleDateSelection = async () => {
    try {
      const result = await DatePicker.showDatePicker({
        date: selectedDate,
        minDate,
        maxDate,
      });

      const newDate = new Date(result.timestamp);
      setSelectedDate(newDate);
      setShowPicker(false);
      
      if (onDateSelected) {
        onDateSelected(formatDate(newDate, 'YYYY-MM-DD'));
      }

      Alert.alert('成功', `您选择的日期是: ${result.dateString}`);
    } catch (error) {
      if (error.message !== '用户取消选择') {
        Alert.alert('错误', '日期选择失败,请重试');
      }
    }
  };

  const formatDate = (date: Date, format: string): string => {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    
    return format
      .replace('YYYY', year.toString())
      .replace('MM', month)
      .replace('DD', day);
  };

  return (
    <View style={styles.container}>
      <TouchableOpacity 
        style={styles.dateButton} 
        onPress={handleDateSelection}
        activeOpacity={0.7}
      >
        <Text style={styles.dateButtonText}>
          选择日期: {formatDate(selectedDate, 'YYYY-MM-DD')}
        </Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    padding: 16,
  },
  dateButton: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 20,
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  dateButtonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: '500',
  },
});

export default DatePickerComponent;

五、自定义日期选择器实现

如果原生日期选择器无法满足UI定制需求,可以采用自定义实现方案:

src/components/CustomDatePicker.tsx

typescript 复制代码
import React, { useState, useEffect } from 'react';
import {
  View,
  Text,
  Modal,
  TouchableOpacity,
  StyleSheet,
  Platform,
  ScrollView,
} from 'react-native';

interface CustomDatePickerProps {
  visible: boolean;
  onClose: () => void;
  onConfirm: (date: Date) => void;
  initialDate?: Date;
  minDate?: Date;
  maxDate?: Date;
}

const CustomDatePicker: React.FC<CustomDatePickerProps> = ({
  visible,
  onClose,
  onConfirm,
  initialDate = new Date(),
  minDate = new Date(1900, 0, 1),
  maxDate = new Date(2100, 11, 31),
}) => {
  const [selectedYear, setSelectedYear] = useState(initialDate.getFullYear());
  const [selectedMonth, setSelectedMonth] = useState(initialDate.getMonth());
  const [selectedDay, setSelectedDay] = useState(initialDate.getDate());

  // 生成年份列表
  const years = Array.from(
    { length: maxDate.getFullYear() - minDate.getFullYear() + 1 },
    (_, i) => minDate.getFullYear() + i
  );

  // 月份列表
  const months = Array.from({ length: 12 }, (_, i) => i + 1);

  // 根据年月计算天数
  const getDaysInMonth = (year: number, month: number) => {
    return new Date(year, month + 1, 0).getDate();
  };

  const days = Array.from(
    { length: getDaysInMonth(selectedYear, selectedMonth) },
    (_, i) => i + 1
  );

  const handleConfirm = () => {
    const selectedDate = new Date(selectedYear, selectedMonth, selectedDay);
    onConfirm(selectedDate);
    onClose();
  };

  const renderPickerColumn = (
    items: number[],
    selectedValue: number,
    onSelect: (value: number) => void
  ) => {
    return (
      <ScrollView
        showsVerticalScrollIndicator={false}
        style={styles.pickerColumn}
        contentContainerStyle={styles.pickerColumnContent}
      >
        {items.map((item) => (
          <TouchableOpacity
            key={item}
            style={[
              styles.pickerItem,
              item === selectedValue && styles.pickerItemSelected,
            ]}
            onPress={() => onSelect(item)}
          >
            <Text
              style={[
                styles.pickerItemText,
                item === selectedValue && styles.pickerItemTextSelected,
              ]}
            >
              {item}
            </Text>
          </TouchableOpacity>
        ))}
      </ScrollView>
    );
  };

  return (
    <Modal
      visible={visible}
      transparent={true}
      animationType="slide"
      onRequestClose={onClose}
    >
      <View style={styles.modalOverlay}>
        <View style={styles.modalContent}>
          <View style={styles.header}>
            <TouchableOpacity onPress={onClose}>
              <Text style={styles.cancelButton}>取消</Text>
            </TouchableOpacity>
            <Text style={styles.title}>选择日期</Text>
            <TouchableOpacity onPress={handleConfirm}>
              <Text style={styles.confirmButton}>确定</Text>
            </TouchableOpacity>
          </View>

          <View style={styles.pickerContainer}>
            {renderPickerColumn(years, selectedYear, setSelectedYear)}
            {renderPickerColumn(months, selectedMonth + 1, (value) => {
              setSelectedMonth(value - 1);
            })}
            {renderPickerColumn(days, selectedDay, setSelectedDay)}
          </View>
        </View>
      </View>
    </Modal>
  );
};

const styles = StyleSheet.create({
  modalOverlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'flex-end',
  },
  modalContent: {
    backgroundColor: '#FFFFFF',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    paddingBottom: 20,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 20,
    paddingVertical: 16,
    borderBottomWidth: 1,
    borderBottomColor: '#F0F0F0',
  },
  cancelButton: {
    color: '#999999',
    fontSize: 16,
  },
  confirmButton: {
    color: '#007AFF',
    fontSize: 16,
    fontWeight: '500',
  },
  title: {
    fontSize: 16,
    fontWeight: '500',
    color: '#333333',
  },
  pickerContainer: {
    flexDirection: 'row',
    height: 200,
  },
  pickerColumn: {
    flex: 1,
  },
  pickerColumnContent: {
    paddingVertical: 80,
  },
  pickerItem: {
    height: 40,
    justifyContent: 'center',
    alignItems: 'center',
  },
  pickerItemSelected: {
    backgroundColor: '#F5F5F5',
  },
  pickerItemText: {
    fontSize: 16,
    color: '#666666',
  },
  pickerItemTextSelected: {
    fontSize: 18,
    color: '#007AFF',
    fontWeight: '500',
  },
});

export default CustomDatePicker;

六、完整示例:生日选择器应用

将以上组件整合,实现一个完整的生日选择器应用:

src/App.tsx

typescript 复制代码
import React, { useState } from 'react';
import {
  SafeAreaView,
  View,
  Text,
  StyleSheet,
  StatusBar,
  Alert,
} from 'react-native';
import DatePickerComponent from './components/DatePickerComponent';
import CustomDatePicker from './components/CustomDatePicker';

const App: React.FC = () => {
  const [birthDate, setBirthDate] = useState<Date | null>(null);
  const [customPickerVisible, setCustomPickerVisible] = useState(false);
  const [selectedDate, setSelectedDate] = useState(new Date());

  const calculateAge = (birthDate: Date): string => {
    const today = new Date();
    let age = today.getFullYear() - birthDate.getFullYear();
    const monthDiff = today.getMonth() - birthDate.getMonth();
    
    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
      age--;
    }
    
    return age.toString();
  };

  const handleNativeDateSelected = (dateString: string) => {
    const date = new Date(dateString);
    setBirthDate(date);
    const age = calculateAge(date);
    Alert.alert('生日信息', `您的生日是:${dateString}\n您的年龄是:${age}岁`);
  };

  const handleCustomDateConfirm = (date: Date) => {
    setSelectedDate(date);
    setBirthDate(date);
    const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
    const age = calculateAge(date);
    Alert.alert('生日信息', `您的生日是:${formattedDate}\n您的年龄是:${age}岁`);
  };

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
      
      <View style={styles.header}>
        <Text style={styles.headerTitle}>生日选择器</Text>
        <Text style={styles.headerSubtitle}>React Native for OpenHarmony</Text>
      </View>

      <View style={styles.content}>
        {/* 原生日期选择器示例 */}
        <View style={styles.card}>
          <Text style={styles.cardTitle}>原生日期选择器</Text>
          <Text style={styles.cardDesc}>
            基于OpenHarmony DatePickerDialog实现,性能最佳
          </Text>
          <DatePickerComponent
            onDateSelected={handleNativeDateSelected}
            minDate={new Date(1900, 0, 1)}
            maxDate={new Date()}
          />
        </View>

        {/* 自定义日期选择器示例 */}
        <View style={styles.card}>
          <Text style={styles.cardTitle}>自定义日期选择器</Text>
          <Text style={styles.cardDesc}>
            完全自定义UI,样式统一,交互可控
          </Text>
          <View style={styles.customPickerContainer}>
            <Text style={styles.selectedDateText}>
              当前选择: {selectedDate.toLocaleDateString('zh-CN')}
            </Text>
            <TouchableOpacity
              style={styles.customPickerButton}
              onPress={() => setCustomPickerVisible(true)}
            >
              <Text style={styles.customPickerButtonText}>
                选择生日
              </Text>
            </TouchableOpacity>
          </View>
        </View>

        {/* 生日信息展示 */}
        {birthDate && (
          <View style={styles.resultCard}>
            <Text style={styles.resultTitle}>🎂 生日信息</Text>
            <View style={styles.resultRow}>
              <Text style={styles.resultLabel}>出生日期:</Text>
              <Text style={styles.resultValue}>
                {birthDate.getFullYear()}年{birthDate.getMonth() + 1}月{birthDate.getDate()}日
              </Text>
            </View>
            <View style={styles.resultRow}>
              <Text style={styles.resultLabel}>年龄:</Text>
              <Text style={styles.resultValue}>
                {calculateAge(birthDate)}岁
              </Text>
            </View>
          </View>
        )}
      </View>

      <CustomDatePicker
        visible={customPickerVisible}
        onClose={() => setCustomPickerVisible(false)}
        onConfirm={handleCustomDateConfirm}
        initialDate={selectedDate}
        minDate={new Date(1900, 0, 1)}
        maxDate={new Date()}
      />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F7FA',
  },
  header: {
    backgroundColor: '#FFFFFF',
    paddingHorizontal: 20,
    paddingVertical: 30,
    borderBottomWidth: 1,
    borderBottomColor: '#EEEEEE',
  },
  headerTitle: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#333333',
  },
  headerSubtitle: {
    fontSize: 14,
    color: '#666666',
    marginTop: 5,
  },
  content: {
    flex: 1,
    padding: 20,
  },
  card: {
    backgroundColor: '#FFFFFF',
    borderRadius: 12,
    padding: 20,
    marginBottom: 20,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.05,
    shadowRadius: 15,
    elevation: 3,
  },
  cardTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#333333',
    marginBottom: 5,
  },
  cardDesc: {
    fontSize: 14,
    color: '#999999',
    marginBottom: 20,
  },
  customPickerContainer: {
    alignItems: 'center',
  },
  selectedDateText: {
    fontSize: 16,
    color: '#333333',
    marginBottom: 15,
  },
  customPickerButton: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 30,
    paddingVertical: 12,
    borderRadius: 8,
  },
  customPickerButtonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: '500',
  },
  resultCard: {
    backgroundColor: '#E8F4FD',
    borderRadius: 12,
    padding: 20,
    marginTop: 10,
  },
  resultTitle: {
    fontSize: 18,
    fontWeight: '600',
    color: '#007AFF',
    marginBottom: 15,
  },
  resultRow: {
    flexDirection: 'row',
    marginBottom: 8,
  },
  resultLabel: {
    fontSize: 15,
    color: '#666666',
    width: 80,
  },
  resultValue: {
    fontSize: 15,
    color: '#333333',
    fontWeight: '500',
  },
});

export default App;

七、常见问题与解决方案

7.1 日期格式化问题

OpenHarmony原生日期选择器返回的月份是1-12,而JavaScript中Date对象的月份是0-11,需要进行转换:

typescript 复制代码
// 设置日期时
datePicker.setDate({
  year: date.getFullYear(),
  month: date.getMonth() + 1, // JS月份转换为OpenHarmony月份
  day: date.getDate()
});

// 获取日期时
const jsDate = new Date(result.year, result.month - 1, result.day);

7.2 兼容性问题

对于不同版本的OpenHarmony,API可能存在差异,建议添加版本判断:

typescript 复制代码
import deviceInfo from '@ohos.deviceInfo';

const apiVersion = parseInt(deviceInfo.sdkApiVersion);
if (apiVersion >= 9) {
  // 使用新版API
} else {
  // 降级方案
}

7.3 性能优化

自定义日期选择器渲染大量列表项时,建议使用FlashList替代ScrollView

bash 复制代码
npm install @shopify/flash-list

八、总结

本文详细介绍了在React Native for OpenHarmony框架下实现日期选择功能的完整方案,包括:

  1. 原生桥接方案:通过TurboModule调用OpenHarmony原生DatePickerDialog
  2. 自定义UI方案:使用React Native组件完全自定义日期选择器
  3. 完整应用示例:整合两种方案,实现完整的生日选择器

两种方案各有优劣,原生方案性能更好、开发成本低;自定义方案样式完全可控、交互体验统一。开发者可以根据实际业务场景选择合适的实现方式。

随着OpenHarmony生态的不断完善,React Native for OpenHarmony将支持更多原生组件和API,为开发者提供更便捷的开发体验。建议开发者持续关注官方文档,及时了解最新特性和最佳实践。

九、参考资料

  1. React Native for OpenHarmony官方文档:https://gitee.com/openharmony-sig/ohos_react_native
  2. OpenHarmony日期选择器API文档:https://developer.harmonyos.com
  3. React Native官方文档:https://reactnative.dev

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

相关推荐
熊猫钓鱼>_>3 小时前
【开源鸿蒙跨平台开发先锋训练营】Day 13:React Native 开发轻量级页面快速响应实践
人工智能·react native·华为·开源·harmonyos·鸿蒙·移动端
特立独行的猫a3 小时前
腾讯Kuikly框架实战:基于腾讯Kuikly框架实现Material3风格底部导航栏
android·harmonyos·compose·kmp·实战案例·kuikly
空白诗3 小时前
基础入门 Flutter for OpenHarmony:Stack 堆叠布局详解
flutter·harmonyos
空白诗3 小时前
基础入门 Flutter for OpenHarmony:Slider 滑块组件详解
flutter·harmonyos
lbb 小魔仙3 小时前
【HarmonyOS】React Native实战项目+智能文本省略Hook开发
react native·华为·harmonyos
_pengliang3 小时前
react native expo 开发 ios经验总结
react native·react.js·ios
星空22233 小时前
【HarmonyOS 】平台day26: React Native 实践:Overlay 遮罩层组件开发指南
react native·华为·harmonyos
lbb 小魔仙3 小时前
【HarmonyOS】React Native实战项目+Redux Toolkit状态管理
react native·华为·harmonyos
胖鱼罐头17 小时前
RNGH:指令式 vs JSX 形式深度对比
前端·react native