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

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 { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Modal, Pressable, StyleSheet, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
interface CalendarModalProps {
visible: boolean;
@@ -18,11 +21,24 @@ export function CalendarModal({
onSelectDate,
}: CalendarModalProps) {
const { theme } = useTheme();
const { i18n } = useTranslation();
const isDark = theme === "dark";
const bg = isDark ? "#1C1C1E" : "#FFFFFF";
const textColor = isDark ? "#FFFFFF" : "#000000";
const subTextColor = isDark ? "#8E8E93" : "#8E8E93";
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 year = currentMonth.getFullYear();
const month = currentMonth.getMonth();
@@ -35,110 +51,145 @@ export function CalendarModal({
return days;
}, [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
const startDay = daysInMonth[0]?.getDay() || 0;
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) => {
onSelectDate(date);
onSelectDate(new Date(date));
onClose();
};
const monthTitle = currentMonth.toLocaleString(i18n.language, {
month: "long",
year: "numeric",
});
const canGoBack = currentMonth > minDate;
const canGoForward = currentMonth < maxDate;
return (
<Modal
visible={visible}
transparent
animationType="fade"
animationType="slide"
onRequestClose={onClose}
>
<Pressable style={styles.overlay} onPress={onClose}>
<View style={[styles.calendarContainer, { backgroundColor: bg }]}>
{/* Header */}
<View style={styles.header}>
<Pressable
onPress={() =>
setCurrentMonth(
new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() - 1,
1
<View style={styles.overlay}>
<Pressable style={styles.dismissArea} onPress={onClose} />
<View style={[styles.sheet, { backgroundColor: bg }]}>
<SafeAreaView edges={["bottom"]}>
<View style={styles.header}>
<Pressable
disabled={!canGoBack}
style={[styles.navBtn, !canGoBack && { opacity: 0.2 }]}
onPress={() =>
setCurrentMonth(
new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() - 1,
1
)
)
)
}
>
<ThemedText style={styles.navText}>{"<"}</ThemedText>
</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>
}
>
<IconSymbol name="chevron-back" size={24} color={textColor} />
</Pressable>
{/* Week Headers */}
<View style={styles.weekRow}>
{weekDays.map((day) => (
<ThemedText key={day} style={styles.weekDayText}>
{day}
<ThemedText type="subtitle" style={styles.titleText}>
{monthTitle}
</ThemedText>
))}
</View>
{/* Days Grid */}
<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 (
<View style={styles.headerRight}>
<Pressable
key={date.toISOString()}
style={[
styles.dayCell,
isSelected && {
backgroundColor: Colors[theme].tint,
borderRadius: 20,
},
]}
onPress={() => handleDayPress(date)}
disabled={!canGoForward}
style={[styles.navBtn, !canGoForward && { opacity: 0.2 }]}
onPress={() =>
setCurrentMonth(
new Date(
currentMonth.getFullYear(),
currentMonth.getMonth() + 1,
1
)
)
}
>
<ThemedText
style={[
isSelected && { color: "#fff", fontWeight: "bold" },
]}
>
{date.getDate()}
</ThemedText>
<IconSymbol
name="chevron-forward"
size={24}
color={textColor}
/>
</Pressable>
);
})}
</View>
<Pressable style={styles.closeBtn} onPress={onClose}>
<IconSymbol name="close" size={24} color={subTextColor} />
</Pressable>
</View>
</View>
<Pressable style={styles.closeBtn} onPress={onClose}>
<ThemedText>Close</ThemedText>
</Pressable>
<View style={styles.weekRow}>
{weekDays.map((day) => (
<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>
</Pressable>
</View>
</Modal>
);
}
@@ -146,49 +197,65 @@ export function CalendarModal({
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "center",
padding: 20,
backgroundColor: "rgba(0,0,0,0.4)",
justifyContent: "flex-end",
},
calendarContainer: {
borderRadius: 16,
padding: 20,
dismissArea: {
flex: 1,
},
sheet: {
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 20,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20,
height: 44,
},
navText: {
fontSize: 20,
padding: 10,
titleText: {
fontSize: 18,
fontWeight: "600",
},
headerRight: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
navBtn: {
padding: 8,
},
closeBtn: {
padding: 8,
},
weekRow: {
flexDirection: "row",
justifyContent: "space-around",
marginBottom: 10,
marginBottom: 16,
},
weekDayText: {
opacity: 0.5,
width: 30,
color: "#8E8E93",
width: 44,
textAlign: "center",
fontSize: 12,
fontSize: 14,
},
daysGrid: {
flexDirection: "row",
flexWrap: "wrap",
paddingBottom: 20,
},
dayCell: {
width: "14.28%", // 100% / 7
width: "14.28%",
aspectRatio: 1,
justifyContent: "center",
alignItems: "center",
marginBottom: 5,
marginBottom: 4,
},
closeBtn: {
alignItems: "center",
marginTop: 20,
padding: 10,
dayText: {
fontSize: 16,
},
});