Files

238 lines
6.9 KiB
TypeScript
Raw Permalink Normal View History

import React, { useState, useMemo } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Keyboard,
Platform,
useColorScheme,
ViewStyle,
TextStyle
} from 'react-native';
import DateTimePickerModal from 'react-native-modal-datetime-picker';
import dayjs from 'dayjs';
import { FontAwesome6 } from '@expo/vector-icons';
// --------------------------------------------------------
// 1. 配置 Dayjs
// --------------------------------------------------------
// 即使服务端返回 '2023-10-20T10:00:00Z' (UTC)
// dayjs(utcString).format() 会自动转为手机当前的本地时区显示。
// 如果需要传回给后端,我们再转回 ISO 格式。
interface SmartDateInputProps {
label?: string; // 表单标题 (可选)
value?: string | null; // 服务端返回的时间字符串 (ISO 8601, 带 T)
onChange: (isoDate: string) => void; // 回调给父组件的值,依然是标准 ISO 字符串
placeholder?: string;
mode?: 'date' | 'time' | 'datetime'; // 支持日期、时间、或两者
displayFormat?: string; // UI展示的格式默认 YYYY-MM-DD
error?: string; // 错误信息
// 样式自定义(可选)
containerStyle?: ViewStyle; // 外层容器样式
inputStyle?: ViewStyle; // 输入框样式
textStyle?: TextStyle; // 文字样式
labelStyle?: TextStyle; // 标签样式
placeholderTextStyle?: TextStyle; // 占位符文字样式
errorTextStyle?: TextStyle; // 错误信息文字样式
iconColor?: string; // 图标颜色
iconSize?: number; // 图标大小
}
export const SmartDateInput = ({
label,
value,
onChange,
placeholder = '请选择',
mode = 'date',
displayFormat,
error,
containerStyle,
inputStyle,
textStyle,
labelStyle,
placeholderTextStyle,
errorTextStyle,
iconColor,
iconSize = 18
}: SmartDateInputProps) => {
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const colorScheme = useColorScheme();
const isDark = colorScheme === 'dark';
// 默认展示格式
const format = displayFormat || (mode === 'time' ? 'HH:mm' : 'YYYY-MM-DD');
// --------------------------------------------------------
// 2. 核心:数据转换逻辑
// --------------------------------------------------------
// 解析服务端值确保无效值不传给控件time 模式兼容仅时间字符串
const parsedValue = useMemo(() => {
if (!value) return null;
const direct = dayjs(value);
if (direct.isValid()) return direct;
if (mode === 'time') {
const timeOnly = dayjs(`1970-01-01T${value}`);
if (timeOnly.isValid()) return timeOnly;
}
return null;
}, [value, mode]);
// A. 将字符串转为 JS Date 对象给控件使用
// 如果 value 是空或无效,回退到当前时间
const dateObjectForPicker = useMemo(() => {
return parsedValue ? parsedValue.toDate() : new Date();
}, [parsedValue]);
// B. 将 Date 对象转为展示字符串
const displayString = useMemo(() => {
if (!parsedValue) return '';
return parsedValue.format(format);
}, [parsedValue, format]);
// --------------------------------------------------------
// 3. 核心:交互逻辑 (解决键盘遮挡/无法收起)
// --------------------------------------------------------
const showDatePicker = () => {
// 【关键点】打开日期控件前,必须强制收起键盘!
// 否则键盘会遮挡 iOS 的底部滚轮,或者导致 Android 焦点混乱
Keyboard.dismiss();
setDatePickerVisibility(true);
};
const hideDatePicker = () => {
setDatePickerVisibility(false);
};
const handleConfirm = (date: Date) => {
hideDatePicker();
// 采用带本地偏移的 ISO 字符串,避免 date 模式在非 UTC 时区出现跨天
const serverString = dayjs(date).format(format);
onChange(serverString);
};
// 根据 mode 选择图标
const iconName = mode === 'time' ? 'clock' : 'calendar';
return (
<View style={[styles.container, containerStyle]}>
{/* 标题 */}
{label && <Text style={[styles.label, labelStyle]}>{label}</Text>}
{/*
TouchableOpacity Input
*/}
<TouchableOpacity
style={[
styles.inputBox,
error ? styles.inputBoxError : null,
inputStyle
]}
onPress={showDatePicker}
activeOpacity={0.7}
>
<Text
style={[
styles.text,
textStyle,
!value && styles.placeholder,
!value && placeholderTextStyle
]}
numberOfLines={1}
>
{displayString || placeholder}
</Text>
<FontAwesome6
name={iconName}
size={iconSize}
color={iconColor || (value ? '#4B5563' : '#9CA3AF')}
style={styles.icon}
/>
</TouchableOpacity>
{error && <Text style={[styles.errorText, errorTextStyle]}>{error}</Text>}
{/*
DateTimePickerModal React Native Modal
View
*/}
<DateTimePickerModal
isVisible={isDatePickerVisible}
mode={mode}
date={dateObjectForPicker} // 传入 Date 对象
onConfirm={handleConfirm}
onCancel={hideDatePicker}
// iOS 只有用这个 display 样式才最稳,避免乱七八糟的 inline 样式
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
// 自动适配系统深色模式,或者根据 isDark 变量控制
isDarkModeEnabled={isDark}
// 强制使用中文环境
locale="zh-CN"
confirmTextIOS="确定"
cancelTextIOS="取消"
/>
</View>
);
};
// 设计样式
const styles = StyleSheet.create({
container: {
marginBottom: 20,
},
label: {
fontSize: 14,
fontWeight: '600',
color: '#374151', // Gray 700
marginBottom: 8,
marginLeft: 2,
},
inputBox: {
height: 52, // 增加高度提升触控体验
backgroundColor: '#FFFFFF',
borderRadius: 12, // 更圆润的角
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
borderWidth: 1,
borderColor: '#E5E7EB', // Gray 200
// 增加轻微阴影提升层次感 (iOS)
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
// Android
elevation: 1,
},
inputBoxError: {
borderColor: '#EF4444', // Red 500
backgroundColor: '#FEF2F2', // Red 50
},
text: {
fontSize: 16,
color: '#111827', // Gray 900
flex: 1,
},
placeholder: {
color: '#9CA3AF', // Gray 400 - 标准占位符颜色
},
icon: {
marginLeft: 12,
},
errorText: {
marginTop: 4,
marginLeft: 2,
fontSize: 12,
color: '#EF4444',
}
});