import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { DailyStatusItem, fetchDailyStatusRange } from '@/services/checkins'; import { loadMonthCheckins } from '@/store/checkinSlice'; import { getMonthDaysZh } from '@/utils/date'; import dayjs from 'dayjs'; import { useRouter } from 'expo-router'; import React, { useEffect, useMemo, useState } from 'react'; import { Dimensions, FlatList, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; function formatDate(d: Date) { const y = d.getFullYear(); const m = `${d.getMonth() + 1}`.padStart(2, '0'); const day = `${d.getDate()}`.padStart(2, '0'); return `${y}-${m}-${day}`; } export default function CheckinCalendarScreen() { const router = useRouter(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const dispatch = useAppDispatch(); const checkin = useAppSelector((s) => (s as any).checkin); const insets = useSafeAreaInsets(); const [cursor, setCursor] = useState(dayjs()); const days = useMemo(() => getMonthDaysZh(cursor), [cursor]); const monthTitle = useMemo(() => `${cursor.format('YYYY年M月')} 打卡`, [cursor]); const [statusMap, setStatusMap] = useState>({}); useEffect(() => { dispatch(loadMonthCheckins({ year: cursor.year(), month1Based: cursor.month() + 1 })); const y = cursor.year(); const m = cursor.month() + 1; const pad = (n: number) => `${n}`.padStart(2, '0'); const startDate = `${y}-${pad(m)}-01`; const endDate = `${y}-${pad(m)}-${pad(new Date(y, m, 0).getDate())}`; fetchDailyStatusRange(startDate, endDate) .then((list: DailyStatusItem[]) => { const next: Record = {}; for (const it of list) { if (typeof it?.date === 'string') next[it.date] = !!it?.checkedIn; } setStatusMap(next); }) .catch(() => setStatusMap({})); }, [cursor, dispatch]); const goPrevMonth = () => setCursor((c) => c.subtract(1, 'month')); const goNextMonth = () => setCursor((c) => c.add(1, 'month')); return ( router.back()} withSafeTop={false} transparent /> 上一月 {monthTitle} 下一月 item.date.format('YYYY-MM-DD')} numColumns={5} columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }} contentContainerStyle={{ paddingHorizontal: 20, paddingTop: 10, paddingBottom: insets.bottom + 20 }} renderItem={({ item }) => { const d = item.date.toDate(); const dateStr = formatDate(d); const hasAny = statusMap[dateStr] ?? !!(checkin?.byDate?.[dateStr]?.items?.length); const isToday = formatDate(new Date()) === dateStr; return ( { // 通过路由参数传入日期,便于目标页初始化 router.push({ pathname: '/checkin', params: { date: dateStr } }); }} activeOpacity={0.8} style={[styles.dayCell, { backgroundColor: colorTokens.card }, hasAny && styles.dayCellCompleted, isToday && styles.dayCellToday]} > {item.dayOfMonth} {hasAny && } ); }} /> ); } const { width } = Dimensions.get('window'); const cellSize = (width - 40 - 4 * 12) / 5; // 20 padding *2, 12 spacing *4 const styles = StyleSheet.create({ safeArea: { flex: 1, backgroundColor: '#F7F8FA' }, container: { flex: 1, backgroundColor: '#F7F8FA' }, headerRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingTop: 6 }, monthTitle: { fontSize: 18, fontWeight: '800' }, monthBtn: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 999 }, monthBtnText: { fontWeight: '700' }, dayCell: { width: cellSize, height: cellSize, borderRadius: 16, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 3, position: 'relative', }, dayCellCompleted: { backgroundColor: '#ECFDF5', borderWidth: 1, borderColor: '#A7F3D0' }, dayCellToday: { borderWidth: 1, borderColor: '#BBF246' }, dayNumber: { fontWeight: '800', fontSize: 16 }, dot: { position: 'absolute', top: 6, right: 6, width: 8, height: 8, borderRadius: 4, backgroundColor: '#10B981' }, });