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

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

- [React Native for OpenHarmony:日期选择功能完整实现指南](#React Native for OpenHarmony:日期选择功能完整实现指南)
概述
日期选择功能是移动应用中最常见的交互组件之一。本文将系统讲解在 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 自定义实现的方案,原因如下:
- 避免原生适配复杂度
- 完全掌控 UI 和交互
- 更好的性能优化空间
- 减少包体积
架构设计
整体架构
┌─────────────────────────────────────────────────────┐
│ 表现层 (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 适配要点
- 日期格式统一:使用 ISO 8601 格式
- 时区处理:内部使用 UTC 时间
- 本地化配置:手动配置多语言资源
- 简化样式:避免复杂的阴影和渐变
- 触摸响应:确保足够的触摸区域
总结
本文系统讲解了在 OpenHarmony 平台上实现日期选择功能的完整方案,包括技术选型、架构设计、核心模块和完整代码实现。
相关资源
- 完整项目 Demo
- OpenHarmony 跨平台社区
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
📕个人领域 :Linux/C++/java/AI
🚀 个人主页 :有点流鼻涕 · CSDN
💬 座右铭 : "向光而行,沐光而生。"
