优化日历模态框,添加日期选择功能,支持最小和最大日期限制,更新国际化文本,改进样式和交互体验

This commit is contained in:
yuchenglong
2026-01-12 17:52:19 +08:00
parent 278ddf031a
commit c1990203ed

View File

@@ -1,8 +1,11 @@
import { ThemedText } from "@/components/themed-text"; import { ThemedText } from "@/components/themed-text";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme"; import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Modal, Pressable, StyleSheet, View } from "react-native"; import { Modal, Pressable, StyleSheet, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
interface CalendarModalProps { interface CalendarModalProps {
visible: boolean; visible: boolean;
@@ -18,11 +21,24 @@ export function CalendarModal({
onSelectDate, onSelectDate,
}: CalendarModalProps) { }: CalendarModalProps) {
const { theme } = useTheme(); const { theme } = useTheme();
const { i18n } = useTranslation();
const isDark = theme === "dark"; const isDark = theme === "dark";
const bg = isDark ? "#1C1C1E" : "#FFFFFF"; const bg = isDark ? "#1C1C1E" : "#FFFFFF";
const textColor = isDark ? "#FFFFFF" : "#000000";
const subTextColor = isDark ? "#8E8E93" : "#8E8E93";
const [currentMonth, setCurrentMonth] = useState(new Date(selectedDate)); const [currentMonth, setCurrentMonth] = useState(new Date(selectedDate));
const today = useMemo(() => new Date(), []);
const minDate = useMemo(
() => new Date(today.getFullYear() - 1, today.getMonth(), 1),
[today]
);
const maxDate = useMemo(
() => new Date(today.getFullYear() + 1, today.getMonth(), 1),
[today]
);
const daysInMonth = useMemo(() => { const daysInMonth = useMemo(() => {
const year = currentMonth.getFullYear(); const year = currentMonth.getFullYear();
const month = currentMonth.getMonth(); const month = currentMonth.getMonth();
@@ -35,110 +51,145 @@ export function CalendarModal({
return days; return days;
}, [currentMonth]); }, [currentMonth]);
const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const weekDays = i18n.language.startsWith("zh")
? ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
// Add empty slots for start of month // Add empty slots for start of month
const startDay = daysInMonth[0]?.getDay() || 0; const startDay = daysInMonth[0]?.getDay() || 0;
const blanks = Array(startDay).fill(null); const blanks = Array(startDay).fill(null);
// Calculate padding cells to keep 6 rows (42 cells total)
const totalCells = 42;
const paddingBlanks = useMemo(() => {
const spaceLeft = totalCells - (blanks.length + daysInMonth.length);
return spaceLeft > 0 ? Array(spaceLeft).fill(null) : [];
}, [blanks.length, daysInMonth.length]);
const handleDayPress = (date: Date) => { const handleDayPress = (date: Date) => {
onSelectDate(date); onSelectDate(new Date(date));
onClose(); onClose();
}; };
const monthTitle = currentMonth.toLocaleString(i18n.language, {
month: "long",
year: "numeric",
});
const canGoBack = currentMonth > minDate;
const canGoForward = currentMonth < maxDate;
return ( return (
<Modal <Modal
visible={visible} visible={visible}
transparent transparent
animationType="fade" animationType="slide"
onRequestClose={onClose} onRequestClose={onClose}
> >
<Pressable style={styles.overlay} onPress={onClose}> <View style={styles.overlay}>
<View style={[styles.calendarContainer, { backgroundColor: bg }]}> <Pressable style={styles.dismissArea} onPress={onClose} />
{/* Header */} <View style={[styles.sheet, { backgroundColor: bg }]}>
<View style={styles.header}> <SafeAreaView edges={["bottom"]}>
<Pressable <View style={styles.header}>
onPress={() => <Pressable
setCurrentMonth( disabled={!canGoBack}
new Date( style={[styles.navBtn, !canGoBack && { opacity: 0.2 }]}
currentMonth.getFullYear(), onPress={() =>
currentMonth.getMonth() - 1, setCurrentMonth(
1 new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() - 1,
1
)
) )
) }
} >
> <IconSymbol name="chevron-back" size={24} color={textColor} />
<ThemedText style={styles.navText}>{"<"}</ThemedText> </Pressable>
</Pressable>
<ThemedText type="subtitle">
{currentMonth.toLocaleString("default", {
month: "long",
year: "numeric",
})}
</ThemedText>
<Pressable
onPress={() =>
setCurrentMonth(
new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() + 1,
1
)
)
}
>
<ThemedText style={styles.navText}>{">"}</ThemedText>
</Pressable>
</View>
{/* Week Headers */} <ThemedText type="subtitle" style={styles.titleText}>
<View style={styles.weekRow}> {monthTitle}
{weekDays.map((day) => (
<ThemedText key={day} style={styles.weekDayText}>
{day}
</ThemedText> </ThemedText>
))}
</View>
{/* Days Grid */} <View style={styles.headerRight}>
<View style={styles.daysGrid}>
{blanks.map((_, index) => (
<View key={`blank-${index}`} style={styles.dayCell} />
))}
{daysInMonth.map((date) => {
const isSelected =
date.getDate() === selectedDate.getDate() &&
date.getMonth() === selectedDate.getMonth() &&
date.getFullYear() === selectedDate.getFullYear();
return (
<Pressable <Pressable
key={date.toISOString()} disabled={!canGoForward}
style={[ style={[styles.navBtn, !canGoForward && { opacity: 0.2 }]}
styles.dayCell, onPress={() =>
isSelected && { setCurrentMonth(
backgroundColor: Colors[theme].tint, new Date(
borderRadius: 20, currentMonth.getFullYear(),
}, currentMonth.getMonth() + 1,
]} 1
onPress={() => handleDayPress(date)} )
)
}
> >
<ThemedText <IconSymbol
style={[ name="chevron-forward"
isSelected && { color: "#fff", fontWeight: "bold" }, size={24}
]} color={textColor}
> />
{date.getDate()}
</ThemedText>
</Pressable> </Pressable>
); <Pressable style={styles.closeBtn} onPress={onClose}>
})} <IconSymbol name="close" size={24} color={subTextColor} />
</View> </Pressable>
</View>
</View>
<Pressable style={styles.closeBtn} onPress={onClose}> <View style={styles.weekRow}>
<ThemedText>Close</ThemedText> {weekDays.map((day) => (
</Pressable> <ThemedText key={day} style={styles.weekDayText}>
{day}
</ThemedText>
))}
</View>
<View style={styles.daysGrid}>
{blanks.map((_, index) => (
<View key={`blank-${index}`} style={styles.dayCell} />
))}
{daysInMonth.map((date) => {
const isSelected =
date.getDate() === selectedDate.getDate() &&
date.getMonth() === selectedDate.getMonth() &&
date.getFullYear() === selectedDate.getFullYear();
const isToday =
new Date().toDateString() === date.toDateString();
return (
<Pressable
key={date.toISOString()}
style={[
styles.dayCell,
isSelected && {
backgroundColor: Colors[theme].tint,
borderRadius: 12,
},
]}
onPress={() => handleDayPress(date)}
>
<ThemedText
style={[
styles.dayText,
isSelected && { color: "#fff", fontWeight: "bold" },
!isSelected && isToday && { color: Colors[theme].tint },
]}
>
{date.getDate()}
</ThemedText>
</Pressable>
);
})}
{/* Add padding blanks to keep constant height (6 rows) */}
{paddingBlanks.map((_, index) => (
<View key={`pad-blank-${index}`} style={styles.dayCell} />
))}
</View>
</SafeAreaView>
</View> </View>
</Pressable> </View>
</Modal> </Modal>
); );
} }
@@ -146,49 +197,65 @@ export function CalendarModal({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
overlay: { overlay: {
flex: 1, flex: 1,
backgroundColor: "rgba(0,0,0,0.5)", backgroundColor: "rgba(0,0,0,0.4)",
justifyContent: "center", justifyContent: "flex-end",
padding: 20,
}, },
calendarContainer: { dismissArea: {
borderRadius: 16, flex: 1,
padding: 20, },
sheet: {
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 20,
}, },
header: { header: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
alignItems: "center", alignItems: "center",
marginBottom: 20, marginBottom: 20,
height: 44,
}, },
navText: { titleText: {
fontSize: 20, fontSize: 18,
padding: 10, fontWeight: "600",
},
headerRight: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
navBtn: {
padding: 8,
},
closeBtn: {
padding: 8,
}, },
weekRow: { weekRow: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-around", justifyContent: "space-around",
marginBottom: 10, marginBottom: 16,
}, },
weekDayText: { weekDayText: {
opacity: 0.5, color: "#8E8E93",
width: 30, width: 44,
textAlign: "center", textAlign: "center",
fontSize: 12, fontSize: 14,
}, },
daysGrid: { daysGrid: {
flexDirection: "row", flexDirection: "row",
flexWrap: "wrap", flexWrap: "wrap",
paddingBottom: 20,
}, },
dayCell: { dayCell: {
width: "14.28%", // 100% / 7 width: "14.28%",
aspectRatio: 1, aspectRatio: 1,
justifyContent: "center", justifyContent: "center",
alignItems: "center", alignItems: "center",
marginBottom: 5, marginBottom: 4,
}, },
closeBtn: { dayText: {
alignItems: "center", fontSize: 16,
marginTop: 20,
padding: 10,
}, },
}); });