import CreateGoalModal from '@/components/CreateGoalModal'; import { DateSelector } from '@/components/DateSelector'; 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 { useRouter } from 'expo-router'; 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 GoalsDetailScreen() { const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const dispatch = useAppDispatch(); const router = useRouter(); // 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 - loading goals'); 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); // 这里可以处理时间轴事件点击 }; const handleBackPress = () => { router.back(); }; return ( {/* 背景渐变 */} {/* 装饰性圆圈 */} {/* 标题区域 */} 目标管理 setShowCreateModal(true)} > + {/* 今日待办事项卡片 */} {/* 时间筛选选项卡 */} {/* 日期选择器 - 在周和月模式下显示 */} {(selectedTab === 'week' || selectedTab === 'month') && ( onSelectDate(index)} showMonthTitle={true} disableFutureDates={true} /> )} {/* 时间轴安排 */} {/* 创建目标弹窗 */} setShowCreateModal(false)} onSubmit={handleCreateGoal} loading={createLoading} /> ); } const styles = StyleSheet.create({ container: { flex: 1, }, gradientBackground: { position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, opacity: 0.6, }, decorativeCircle1: { position: 'absolute', top: -20, right: -20, width: 60, height: 60, borderRadius: 30, backgroundColor: '#0EA5E9', opacity: 0.1, }, decorativeCircle2: { position: 'absolute', bottom: -15, left: -15, width: 40, height: 40, borderRadius: 20, backgroundColor: '#0EA5E9', opacity: 0.05, }, content: { flex: 1, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 16, }, backButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255, 255, 255, 0.8)', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3, }, backButtonText: { color: '#0EA5E9', fontSize: 20, fontWeight: '600', }, pageTitle: { fontSize: 24, fontWeight: '700', flex: 1, textAlign: 'center', }, addButton: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#0EA5E9', 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, }, timelineSection: { flex: 1, backgroundColor: 'rgba(255, 255, 255, 0.95)', borderTopLeftRadius: 24, borderTopRightRadius: 24, overflow: 'hidden', }, // 日期选择器样式 dateSelector: { paddingHorizontal: 20, paddingVertical: 16, }, });