一、前言
在跨端应用开发中,日期选择是一个极其常见且高频使用的功能。随着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框架下实现日期选择功能的完整方案,包括:
- 原生桥接方案:通过TurboModule调用OpenHarmony原生DatePickerDialog
- 自定义UI方案:使用React Native组件完全自定义日期选择器
- 完整应用示例:整合两种方案,实现完整的生日选择器
两种方案各有优劣,原生方案性能更好、开发成本低;自定义方案样式完全可控、交互体验统一。开发者可以根据实际业务场景选择合适的实现方式。
随着OpenHarmony生态的不断完善,React Native for OpenHarmony将支持更多原生组件和API,为开发者提供更便捷的开发体验。建议开发者持续关注官方文档,及时了解最新特性和最佳实践。
九、参考资料
- React Native for OpenHarmony官方文档:https://gitee.com/openharmony-sig/ohos_react_native
- OpenHarmony日期选择器API文档:https://developer.harmonyos.com
- React Native官方文档:https://reactnative.dev
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net