【HarmonyOS】DAY24:React Native for OpenHarmony:日期范围选择器实现
日期范围选择器是预订系统、数据筛选等场景的核心组件。本文将详细讲解如何在 OpenHarmony 平台上实现一个高效、易用的日期范围选择功能。
·
React Native for OpenHarmony:日期范围选择器实现

🌟 感谢陪伴~ 小白博主在线求友
🌿 跟着小白学Linux/Java/Python
📖 专栏汇总:
《Linux》专栏 | 《Java》专栏 | 《Python》专栏

概述
日期范围选择器是预订系统、数据筛选等场景的核心组件。本文将详细讲解如何在 OpenHarmony 平台上实现一个高效、易用的日期范围选择功能。
核心概念
状态机设计
日期范围选择的核心是状态管理,通过状态机可以清晰地描述用户交互流程:
┌─────────────────┐
│ IDLE (空闲) │
│ 未选择任何日期 │
└────────┬────────┘
│ 用户选择第一个日期
▼
┌─────────────────┐ 用户选择第二个日期
│ START_SELECTED │ ────────────────────────┐
│ 已选起始日期 │ │
└─────────────────┘ ▼
│ ┌─────────────────────┐
│ 用户重新选择 │ RANGE_SELECTED │
└───────────────────►│ 已选完整日期范围 │
└─────────────────────┘
数据结构定义
// types/dateRange.ts
export type RangeState = 'idle' | 'start_selected' | 'range_selected';
export interface DateRange {
startDate: string | null;
endDate: string | null;
}
export interface RangeSelectionState {
state: RangeState;
range: DateRange;
markedDates: MarkedDates;
}
export interface MarkedDates {
[dateString: string]: DateMarker;
}
export interface DateMarker {
startingDay?: boolean;
endingDay?: boolean;
color?: string;
textColor?: string;
}
核心实现
范围选择 Hook
// hooks/useDateRangeSelection.ts
import { useState, useCallback, useMemo } from 'react';
import { DateRange, RangeState, RangeSelectionState, MarkedDates } from '../types/dateRange';
interface UseDateRangeSelectionOptions {
minDate?: Date;
maxDate?: Date;
onRangeChange?: (range: DateRange) => void;
}
export const useDateRangeSelection = (options: UseDateRangeSelectionOptions = {}) => {
const { minDate, maxDate, onRangeChange } = options;
const [selectionState, setSelectionState] = useState<RangeSelectionState>({
state: 'idle',
range: { startDate: null, endDate: null },
markedDates: {},
});
// 计算日期范围内所有日期的标记
const generateRangeMarkers = useCallback((
startDate: string,
endDate: string
): MarkedDates => {
const markers: MarkedDates = {};
const start = new Date(startDate);
const end = new Date(endDate);
// 标记起始日
markers[startDate] = {
startingDay: true,
color: '#FF9800',
textColor: '#ffffff',
};
// 标记结束日
markers[endDate] = {
endingDay: true,
color: '#FF9800',
textColor: '#ffffff',
};
// 标记中间日期
const current = new Date(start);
current.setDate(current.getDate() + 1);
while (current < end) {
const dateStr = current.toISOString().split('T')[0];
markers[dateStr] = {
color: '#FFE0B2',
textColor: '#FF9800',
};
current.setDate(current.getDate() + 1);
}
return markers;
}, []);
// 选择日期
const selectDate = useCallback((dateString: string) => {
setSelectionState((prev) => {
const newRange: DateRange = { startDate: null, endDate: null };
const newMarkers: MarkedDates = {};
let newState: RangeState = 'idle';
switch (prev.state) {
case 'idle':
// 第一次选择,设置为起始日期
newRange.startDate = dateString;
newMarkers[dateString] = {
startingDay: true,
color: '#FF9800',
textColor: '#ffffff',
};
newState = 'start_selected';
break;
case 'start_selected':
// 已有起始日期,判断是重新选择还是选择结束日期
const start = new Date(prev.range.startDate!);
const current = new Date(dateString);
if (current < start) {
// 选择了一个更早的日期,重新设置为起始日期
newRange.startDate = dateString;
newMarkers[dateString] = {
startingDay: true,
color: '#FF9800',
textColor: '#ffffff',
};
newState = 'start_selected';
} else {
// 选择结束日期
newRange.startDate = prev.range.startDate;
newRange.endDate = dateString;
Object.assign(newMarkers, generateRangeMarkers(prev.range.startDate!, dateString));
newState = 'range_selected';
}
break;
case 'range_selected':
// 已有完整范围,重新开始选择
newRange.startDate = dateString;
newMarkers[dateString] = {
startingDay: true,
color: '#FF9800',
textColor: '#ffffff',
};
newState = 'start_selected';
break;
}
const result = {
state: newState,
range: newRange,
markedDates: newMarkers,
};
onRangeChange?.(newRange);
return result;
});
}, [generateRangeMarkers, onRangeChange]);
// 重置选择
const resetSelection = useCallback(() => {
setSelectionState({
state: 'idle',
range: { startDate: null, endDate: null },
markedDates: {},
});
onRangeChange?.({ startDate: null, endDate: null });
}, [onRangeChange]);
// 计算范围天数
const rangeDays = useMemo(() => {
const { startDate, endDate } = selectionState.range;
if (!startDate || !endDate) return 0;
const start = new Date(startDate);
const end = new Date(endDate);
const diffTime = end.getTime() - start.getTime();
return Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
}, [selectionState.range]);
// 判断日期是否在范围内
const isDateInRange = useCallback((dateString: string) => {
const { startDate, endDate } = selectionState.range;
if (!startDate || !endDate) return false;
const date = new Date(dateString);
const start = new Date(startDate);
const end = new Date(endDate);
return date >= start && date <= end;
}, [selectionState.range]);
// 获取日期的标记状态
const getDateMarker = useCallback((dateString: string) => {
return selectionState.markedDates[dateString];
}, [selectionState.markedDates]);
return {
selectionState,
selectDate,
resetSelection,
rangeDays,
isDateInRange,
getDateMarker,
};
};
日期范围选择组件
// components/DateRangePicker.tsx
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Alert,
} from 'react-native';
import { useDateRangeSelection } from '../hooks/useDateRangeSelection';
import { DateUtils } from '../utils/dateUtils';
interface DateRangePickerProps {
onRangeConfirm?: (startDate: string, endDate: string, days: number) => void;
minDate?: Date;
maxDate?: Date;
themeColor?: string;
}
export const DateRangePicker: React.FC<DateRangePickerProps> = ({
onRangeConfirm,
minDate,
maxDate,
themeColor = '#FF9800',
}) => {
const {
selectionState,
selectDate,
resetSelection,
rangeDays,
getDateMarker,
} = useDateRangeSelection({
minDate,
maxDate,
});
const [currentDate, setCurrentDate] = React.useState(new Date());
// 获取月份数据
const monthData = React.useMemo(() => {
return DateUtils.getMonthData(currentDate);
}, [currentDate]);
// 切换月份
const changeMonth = (offset: number) => {
setCurrentDate((prev) => {
const newDate = new Date(prev);
newDate.setMonth(newDate.getMonth() + offset);
return newDate;
});
};
// 确认选择
const handleConfirm = () => {
const { startDate, endDate } = selectionState.range;
if (!startDate) {
Alert.alert('提示', '请选择起始日期');
return;
}
if (!endDate) {
Alert.alert('提示', '请选择结束日期');
return;
}
onRangeConfirm?.(startDate, endDate, rangeDays);
};
// 渲染日期单元格
const renderDayCell = (day: any) => {
const marker = getDateMarker(day.dateString);
const isSelected = marker !== undefined;
return (
<TouchableOpacity
key={day.dateString || day.day}
style={[
styles.dayCell,
!day.isCurrentMonth && styles.dayCellDisabled,
day.isToday && styles.dayCellToday,
marker?.startingDay && styles.dayCellStart,
marker?.endingDay && styles.dayCellEnd,
marker?.color === '#FFE0B2' && styles.dayCellRange,
]}
onPress={() => day.isCurrentMonth && selectDate(day.dateString)}
disabled={!day.isCurrentMonth}
activeOpacity={0.7}
>
<Text
style={[
styles.dayText,
!day.isCurrentMonth && styles.dayTextDisabled,
day.isToday && { color: themeColor },
(marker?.startingDay || marker?.endingDay) && styles.dayTextSelected,
marker?.color === '#FFE0B2' && styles.dayTextRange,
]}
>
{day.day}
</Text>
</TouchableOpacity>
);
};
// 渲染日历网格
const renderCalendar = () => {
const rows: React.ReactNode[] = [];
for (let i = 0; i < 6; i++) {
const rowDays = monthData.days.slice(i * 7, (i + 1) * 7);
rows.push(
<View key={i} style={styles.weekRow}>
{rowDays.map(renderDayCell)}
</View>
);
}
return rows;
};
// 状态提示文本
const getStatusText = () => {
const { state, range } = selectionState;
switch (state) {
case 'idle':
return '请选择起始日期';
case 'start_selected':
return `已选起始: ${range.startDate},请选择结束日期`;
case 'range_selected':
return `从 ${range.startDate} 到 ${range.endDate},共 ${rangeDays} 天`;
default:
return '';
}
};
return (
<ScrollView style={styles.container}>
{/* 状态提示 */}
<View style={[styles.statusCard, { borderLeftColor: themeColor }]}>
<Text style={styles.statusText}>{getStatusText()}</Text>
</View>
{/* 日历卡片 */}
<View style={styles.calendarCard}>
{/* 月份导航 */}
<View style={styles.monthNavigation}>
<TouchableOpacity
style={styles.navButton}
onPress={() => changeMonth(-1)}
>
<Text style={[styles.navButtonText, { color: themeColor }]}>‹</Text>
</TouchableOpacity>
<Text style={styles.monthText}>
{monthData.year}年 {DateUtils.getMonthName(monthData.month)}
</Text>
<TouchableOpacity
style={styles.navButton}
onPress={() => changeMonth(1)}
>
<Text style={[styles.navButtonText, { color: themeColor }]}>›</Text>
</TouchableOpacity>
</View>
{/* 星期标题 */}
<View style={styles.weekHeader}>
{DateUtils.getWeekDays().map((day, index) => (
<View key={index} style={styles.weekDayCell}>
<Text style={styles.weekDayText}>{day}</Text>
</View>
))}
</View>
{/* 日期网格 */}
<View style={styles.daysContainer}>
{renderCalendar()}
</View>
</View>
{/* 操作按钮 */}
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.confirmButton, { backgroundColor: themeColor }]}
onPress={handleConfirm}
>
<Text style={styles.buttonText}>确认选择</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.resetButton]}
onPress={resetSelection}
>
<Text style={styles.buttonText}>重置</Text>
</TouchableOpacity>
</View>
{/* 范围说明 */}
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>选择说明</Text>
<View style={styles.infoStep}>
<View style={[styles.stepNumber, { backgroundColor: themeColor }]}>
<Text style={styles.stepNumberText}>1</Text>
</View>
<Text style={styles.infoText}>选择起始日期</Text>
</View>
<View style={styles.infoStep}>
<View style={[styles.stepNumber, { backgroundColor: themeColor }]}>
<Text style={styles.stepNumberText}>2</Text>
</View>
<Text style={styles.infoText}>选择结束日期(需晚于起始日期)</Text>
</View>
<View style={styles.infoStep}>
<View style={[styles.stepNumber, { backgroundColor: themeColor }]}>
<Text style={styles.stepNumberText}>3</Text>
</View>
<Text style={styles.infoText}>确认或重新选择</Text>
</View>
</View>
{/* 标记图例 */}
<View style={styles.legendCard}>
<Text style={styles.legendTitle}>标记说明</Text>
<View style={styles.legendItem}>
<View style={[styles.legendBox, { backgroundColor: themeColor }]} />
<Text style={styles.legendText}>起始/结束日期</Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendBox, { backgroundColor: '#FFE0B2' }]} />
<Text style={styles.legendText}>范围中间日期</Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendBox, styles.legendBoxDefault]} />
<Text style={styles.legendText}>未选择日期</Text>
</View>
</View>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
statusCard: {
backgroundColor: '#fff',
margin: 16,
marginTop: 16,
padding: 14,
borderRadius: 10,
borderLeftWidth: 4,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
statusText: {
fontSize: 14,
color: '#333',
lineHeight: 20,
},
calendarCard: {
backgroundColor: '#fff',
borderRadius: 16,
margin: 16,
marginTop: 8,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4,
},
monthNavigation: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
},
monthText: {
fontSize: 18,
fontWeight: '700',
color: '#1a1a1a',
},
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: 40,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 20,
margin: 1,
},
dayCellDisabled: {
opacity: 0.3,
},
dayCellToday: {
borderWidth: 1,
borderColor: '#FF9800',
},
dayCellStart: {
backgroundColor: '#FF9800',
borderRadius: 20,
},
dayCellEnd: {
backgroundColor: '#FF9800',
borderRadius: 20,
},
dayCellRange: {
backgroundColor: '#FFE0B2',
borderRadius: 0,
},
dayText: {
fontSize: 16,
color: '#333',
fontWeight: '500',
},
dayTextDisabled: {
color: '#999',
},
dayTextSelected: {
color: '#fff',
fontWeight: '700',
},
dayTextRange: {
color: '#FF9800',
fontWeight: '600',
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-around',
margin: 16,
marginTop: 8,
},
button: {
paddingHorizontal: 32,
paddingVertical: 12,
borderRadius: 10,
minWidth: 120,
alignItems: 'center',
},
confirmButton: {},
resetButton: {
backgroundColor: '#f44336',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
infoCard: {
backgroundColor: '#fff',
borderRadius: 12,
margin: 16,
marginTop: 8,
padding: 16,
},
infoTitle: {
fontSize: 16,
fontWeight: '700',
color: '#1a1a1a',
marginBottom: 12,
},
infoStep: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
stepNumber: {
width: 24,
height: 24,
borderRadius: 12,
justifyContent: 'center',
alignItems: 'center',
marginRight: 10,
},
stepNumberText: {
color: '#fff',
fontSize: 12,
fontWeight: '700',
},
infoText: {
fontSize: 14,
color: '#555',
flex: 1,
},
legendCard: {
backgroundColor: '#fff',
borderRadius: 12,
margin: 16,
marginTop: 8,
marginBottom: 24,
padding: 16,
},
legendTitle: {
fontSize: 16,
fontWeight: '700',
color: '#1a1a1a',
marginBottom: 12,
},
legendItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
legendBox: {
width: 20,
height: 20,
borderRadius: 4,
marginRight: 10,
},
legendBoxDefault: {
backgroundColor: '#f0f0f0',
borderWidth: 1,
borderColor: '#ddd',
},
legendText: {
fontSize: 14,
color: '#666',
},
});
使用示例
// Example.tsx
import React from 'react';
import { View, StyleSheet, Alert } from 'react-native';
import { DateRangePicker } from './components/DateRangePicker';
const Example: React.FC = () => {
const handleRangeConfirm = (startDate: string, endDate: string, days: number) => {
Alert.alert(
'选择成功',
`日期范围: ${startDate} 至 ${endDate}\n共 ${days} 天`
);
};
return (
<View style={styles.container}>
<DateRangePicker
onRangeConfirm={handleRangeConfirm}
themeColor="#FF9800"
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
export default Example;
OpenHarmony 适配要点
-
日期计算优化
- 使用 UTC 时间避免时区问题
- 缓存计算结果减少重复运算
-
渲染性能
- 限制一次性渲染的日期数量
- 使用
useMemo和useCallback优化
-
交互体验
- 提供清晰的状态反馈
- 支持触摸取消选择
总结
本文介绍了在 OpenHarmony 平台上实现日期范围选择器的完整方案,包括状态机设计、Hook 封装、组件实现等核心技术点。
相关资源
- 完整项目 Demo
- OpenHarmony 跨平台社区
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
📕个人领域 :Linux/C++/java/AI
🚀 个人主页 :有点流鼻涕 · CSDN
💬 座右铭 : “向光而行,沐光而生。”

更多推荐



所有评论(0)