import CreateGoalModal from '@/components/CreateGoalModal'; import { TimeTabSelector, TimeTabType } from '@/components/TimeTabSelector'; import { TimelineSchedule } from '@/components/TimelineSchedule'; import { TodoItem } from '@/components/TodoCard'; import { TodoCarousel } from '@/components/TodoCarousel'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { clearErrors, completeGoal, createGoal, fetchGoals } from '@/store/goalsSlice'; import { CreateGoalRequest, GoalListItem } from '@/types/goals'; import { getMonthDaysZh, getMonthTitleZh, getTodayIndexInMonth } from '@/utils/date'; import { useFocusEffect } from '@react-navigation/native'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Alert, SafeAreaView, ScrollView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; dayjs.extend(isBetween); // 将目标转换为TodoItem的辅助函数 const goalToTodoItem = (goal: GoalListItem): TodoItem => { return { id: goal.id, title: goal.title, description: goal.description || `${goal.frequency}次/${getRepeatTypeLabel(goal.repeatType)}`, time: dayjs().startOf('day').add(goal.startTime, 'minute').toISOString() || '', category: getCategoryFromGoal(goal.category), priority: getPriorityFromGoal(goal.priority), isCompleted: goal.status === 'completed', }; }; // 获取重复类型标签 const getRepeatTypeLabel = (repeatType: string): string => { switch (repeatType) { case 'daily': return '每日'; case 'weekly': return '每周'; case 'monthly': return '每月'; default: return '自定义'; } }; // 从目标分类获取TodoItem分类 const getCategoryFromGoal = (category?: string): TodoItem['category'] => { if (!category) return 'personal'; if (category.includes('运动') || category.includes('健身')) return 'workout'; if (category.includes('工作')) return 'work'; if (category.includes('健康')) return 'health'; if (category.includes('财务')) return 'finance'; return 'personal'; }; // 从目标优先级获取TodoItem优先级 const getPriorityFromGoal = (priority: number): TodoItem['priority'] => { if (priority >= 8) return 'high'; if (priority >= 5) return 'medium'; return 'low'; }; // 将目标转换为时间轴事件的辅助函数 const goalToTimelineEvent = (goal: GoalListItem) => { return { id: goal.id, title: goal.title, startTime: dayjs().startOf('day').add(goal.startTime, 'minute').toISOString(), endTime: goal.endTime ? dayjs().startOf('day').add(goal.endTime, 'minute').toISOString() : undefined, category: getCategoryFromGoal(goal.category), isCompleted: goal.status === 'completed', }; }; export default function GoalsScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const dispatch = useAppDispatch(); // Redux状态 const { goals, goalsLoading, goalsError, createLoading, createError } = useAppSelector((state) => state.goals); const [selectedTab, setSelectedTab] = useState('day'); const [selectedDate, setSelectedDate] = useState(new Date()); const [showCreateModal, setShowCreateModal] = useState(false); // 页面聚焦时重新加载数据 useFocusEffect( useCallback(() => { console.log('useFocusEffect'); // 只在需要时刷新数据,比如从后台返回或从其他页面返回 dispatch(fetchGoals({ status: 'active', page: 1, pageSize: 200, })); }, [dispatch]) ); // 处理错误提示 useEffect(() => { console.log('goalsError', goalsError); console.log('createError', createError); if (goalsError) { Alert.alert('错误', goalsError); dispatch(clearErrors()); } if (createError) { Alert.alert('创建失败', createError); dispatch(clearErrors()); } }, [goalsError, createError, dispatch]); // 创建目标处理函数 const handleCreateGoal = async (goalData: CreateGoalRequest) => { try { await dispatch(createGoal(goalData)).unwrap(); setShowCreateModal(false); Alert.alert('成功', '目标创建成功!'); } catch (error) { // 错误已在useEffect中处理 } }; // tab切换处理函数 const handleTabChange = (tab: TimeTabType) => { setSelectedTab(tab); // 当切换到周或月模式时,如果当前选择的日期不是今天,则重置为今天 const today = new Date(); const currentDate = selectedDate; if (tab === 'week' || tab === 'month') { // 如果当前选择的日期不是今天,重置为今天 if (!dayjs(currentDate).isSame(dayjs(today), 'day')) { setSelectedDate(today); setSelectedIndex(getTodayIndexInMonth()); } } else if (tab === 'day') { // 天模式下也重置为今天 setSelectedDate(today); setSelectedIndex(getTodayIndexInMonth()); } }; // 日期选择器相关状态 (参考 statistics.tsx) const days = getMonthDaysZh(); const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth()); const monthTitle = getMonthTitleZh(); // 日期条自动滚动到选中项 const daysScrollRef = useRef(null); const [scrollWidth, setScrollWidth] = useState(0); const DAY_PILL_WIDTH = 48; const DAY_PILL_SPACING = 8; const scrollToIndex = (index: number, animated = true) => { if (!daysScrollRef.current || scrollWidth === 0) return; const itemWidth = DAY_PILL_WIDTH + DAY_PILL_SPACING; const baseOffset = index * itemWidth; const centerOffset = Math.max(0, baseOffset - (scrollWidth / 2 - DAY_PILL_WIDTH / 2)); // 确保不会滚动超出边界 const maxScrollOffset = Math.max(0, (days.length * itemWidth) - scrollWidth); const finalOffset = Math.min(centerOffset, maxScrollOffset); daysScrollRef.current.scrollTo({ x: finalOffset, animated }); }; useEffect(() => { if (scrollWidth > 0) { scrollToIndex(selectedIndex, false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [scrollWidth]); // 当选中索引变化时,滚动到对应位置 useEffect(() => { if (scrollWidth > 0) { scrollToIndex(selectedIndex, true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedIndex]); // 日期选择处理 const onSelectDate = (index: number) => { setSelectedIndex(index); const targetDate = days[index]?.date?.toDate(); if (targetDate) { setSelectedDate(targetDate); // 在周模式下,如果用户选择了新日期,更新周的显示范围 if (selectedTab === 'week') { // 自动滚动到新选择的日期 setTimeout(() => { scrollToIndex(index, true); }, 100); } } }; // 将目标转换为TodoItem数据 const todayTodos = useMemo(() => { const today = dayjs(); const activeGoals = goals.filter(goal => goal.status === 'active' && (goal.repeatType === 'daily' || (goal.repeatType === 'weekly' && today.day() !== 0) || (goal.repeatType === 'monthly' && today.date() <= 28)) ); return activeGoals.map(goalToTodoItem); }, [goals]); // 将目标转换为时间轴事件数据 const filteredTimelineEvents = useMemo(() => { const selected = dayjs(selectedDate); let filteredGoals: GoalListItem[] = []; switch (selectedTab) { case 'day': filteredGoals = goals.filter(goal => { if (goal.status !== 'active') return false; if (goal.repeatType === 'daily') return true; if (goal.repeatType === 'weekly') return selected.day() !== 0; if (goal.repeatType === 'monthly') return selected.date() <= 28; return false; }); break; case 'week': filteredGoals = goals.filter(goal => goal.status === 'active' && (goal.repeatType === 'daily' || goal.repeatType === 'weekly') ); break; case 'month': filteredGoals = goals.filter(goal => goal.status === 'active'); break; default: filteredGoals = goals.filter(goal => goal.status === 'active'); } return filteredGoals.map(goalToTimelineEvent); }, [selectedTab, selectedDate, goals]); console.log('filteredTimelineEvents', filteredTimelineEvents); const handleTodoPress = (item: TodoItem) => { console.log('Goal pressed:', item.title); // 这里可以导航到目标详情页面 }; const handleToggleComplete = async (item: TodoItem) => { try { await dispatch(completeGoal({ goalId: item.id, completionData: { completionCount: 1, notes: '通过待办卡片完成' } })).unwrap(); } catch (error) { Alert.alert('错误', '记录完成失败'); } }; const handleEventPress = (event: any) => { console.log('Event pressed:', event.title); // 这里可以处理时间轴事件点击 }; return ( {/* 背景渐变 */} {/* 标题区域 */} 今日 setShowCreateModal(true)} > + {/* 今日待办事项卡片 */} {/* 时间筛选选项卡 */} {/* 日期选择器 - 在周和月模式下显示 */} {(selectedTab === 'week' || selectedTab === 'month') && ( {monthTitle} setScrollWidth(e.nativeEvent.layout.width)} > {days.map((d, i) => { const selected = i === selectedIndex; const isFutureDate = d.date.isAfter(dayjs(), 'day'); // 根据选择的tab模式决定是否显示该日期 let shouldShow = true; if (selectedTab === 'week') { // 周模式:只显示选中日期所在周的日期 const selectedWeekStart = dayjs(selectedDate).startOf('week'); const selectedWeekEnd = dayjs(selectedDate).endOf('week'); shouldShow = d.date.isBetween(selectedWeekStart, selectedWeekEnd, 'day', '[]'); } if (!shouldShow) return null; return ( !isFutureDate && onSelectDate(i)} activeOpacity={isFutureDate ? 1 : 0.8} disabled={isFutureDate} > {d.weekdayZh} {d.dayOfMonth} ); })} )} {/* 时间轴安排 */} {/* 创建目标弹窗 */} setShowCreateModal(false)} onSubmit={handleCreateGoal} loading={createLoading} /> ); } const styles = StyleSheet.create({ container: { flex: 1, }, backgroundGradient: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, }, content: { flex: 1, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 16, }, pageTitle: { fontSize: 28, fontWeight: '800', marginBottom: 4, }, addButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#6366F1', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, addButtonText: { color: '#FFFFFF', fontSize: 24, fontWeight: '600', lineHeight: 24, }, pageSubtitle: { fontSize: 16, fontWeight: '500', }, timelineSection: { flex: 1, backgroundColor: 'rgba(255, 255, 255, 0.95)', borderTopLeftRadius: 24, borderTopRightRadius: 24, overflow: 'hidden', }, // 日期选择器样式 (参考 statistics.tsx) dateSelector: { paddingHorizontal: 20, paddingVertical: 16, }, monthTitle: { fontSize: 24, fontWeight: '800', marginBottom: 14, }, daysContainer: { paddingBottom: 8, }, dayItemWrapper: { alignItems: 'center', width: 48, marginRight: 8, }, dayPill: { width: 40, height: 60, borderRadius: 24, alignItems: 'center', justifyContent: 'center', }, dayPillNormal: { backgroundColor: 'transparent', }, dayPillSelected: { backgroundColor: '#FFFFFF', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, dayPillDisabled: { backgroundColor: 'transparent', opacity: 0.4, }, dayLabel: { fontSize: 11, fontWeight: '700', color: 'gray', marginBottom: 2, }, dayLabelSelected: { color: '#192126', }, dayLabelDisabled: { }, dayDate: { fontSize: 12, fontWeight: '800', color: 'gray', }, dayDateSelected: { color: '#192126', }, dayDateDisabled: { }, });