import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import React, { useMemo } from 'react'; import { Dimensions, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { GoalItem } from './GoalCard'; interface TimelineEvent { id: string; title: string; startTime: string; endTime?: string; category: GoalItem['category']; isCompleted?: boolean; color?: string; } interface TimelineScheduleProps { events: TimelineEvent[]; selectedDate: Date; onEventPress?: (event: TimelineEvent) => void; } const { width: screenWidth } = Dimensions.get('window'); const HOUR_HEIGHT = 60; const TIME_LABEL_WIDTH = 60; // 生成24小时的时间标签 const generateTimeLabels = () => { const labels = []; for (let hour = 0; hour < 24; hour++) { labels.push(dayjs().hour(hour).minute(0).format('HH:mm')); } return labels; }; // 获取事件在时间轴上的位置和高度 const getEventStyle = (event: TimelineEvent) => { const startTime = dayjs(event.startTime); const endTime = event.endTime ? dayjs(event.endTime) : startTime.add(1, 'hour'); const startMinutes = startTime.hour() * 60 + startTime.minute(); const endMinutes = endTime.hour() * 60 + endTime.minute(); const durationMinutes = endMinutes - startMinutes; // 计算top位置时需要加上时间标签的paddingTop偏移(8px) const top = (startMinutes / 60) * HOUR_HEIGHT + 8; const height = Math.max((durationMinutes / 60) * HOUR_HEIGHT, 30); // 最小高度30 return { top, height }; }; // 获取分类颜色 const getCategoryColor = (category: GoalItem['category']) => { switch (category) { case 'workout': return '#FF6B6B'; case 'finance': return '#4ECDC4'; case 'personal': return '#45B7D1'; case 'work': return '#96CEB4'; case 'health': return '#FFEAA7'; default: return '#DDA0DD'; } }; export function TimelineSchedule({ events, selectedDate, onEventPress }: TimelineScheduleProps) { const theme = useColorScheme() ?? 'light'; const colorTokens = Colors[theme]; const timeLabels = generateTimeLabels(); // 按开始时间分组事件,处理同一时间的多个事件 const groupedEvents = useMemo(() => { const groups: { [key: string]: TimelineEvent[] } = {}; events.forEach(event => { const startHour = dayjs(event.startTime).format('HH:mm'); if (!groups[startHour]) { groups[startHour] = []; } groups[startHour].push(event); }); return groups; }, [events]); const renderTimelineEvent = (event: TimelineEvent, index: number, groupSize: number) => { const { top, height } = getEventStyle(event); const categoryColor = getCategoryColor(event.category); // 计算水平偏移和宽度,用于处理重叠事件 const availableWidth = screenWidth - TIME_LABEL_WIDTH - 48; // 减少一些边距 const eventWidth = availableWidth / Math.max(groupSize, 1); const leftOffset = index * eventWidth; // 判断是否应该显示时间段 - 当卡片高度小于50或宽度小于80时隐藏时间段 const shouldShowTimeRange = height >= 50 && eventWidth >= 80; return ( onEventPress?.(event)} activeOpacity={0.7} > {/* 顶部行:标题和分类标签 */} {event.title} {event.category === 'workout' ? '运动' : event.category === 'finance' ? '财务' : event.category === 'personal' ? '个人' : event.category === 'work' ? '工作' : event.category === 'health' ? '健康' : '其他'} {/* 底部行:时间和图标 */} {shouldShowTimeRange && ( {dayjs(event.startTime).format('HH:mm A')} {event.isCompleted ? ( ) : ( )} )} {/* 完成状态指示 */} {event.isCompleted && !shouldShowTimeRange && ( )} ); }; return ( {/* 时间轴 */} {/* 日期标题 */} {dayjs(selectedDate).format('YYYY年M月D日 dddd')} {events.length} 个目标 {/* 时间标签 */} {timeLabels.map((label, index) => ( {label} ))} {/* 事件容器 */} {Object.entries(groupedEvents).map(([time, timeEvents]) => timeEvents.map((event, index) => renderTimelineEvent(event, index, timeEvents.length) ) )} {/* 当前时间线 */} {dayjs(selectedDate).isSame(dayjs(), 'day') && ( )} {/* 空状态 */} {events.length === 0 && ( 今天暂无安排 )} ); } // 当前时间指示线组件 function CurrentTimeLine() { const theme = useColorScheme() ?? 'light'; const colorTokens = Colors[theme]; const now = dayjs(); const currentMinutes = now.hour() * 60 + now.minute(); // 当前时间线也需要加上时间标签的paddingTop偏移(8px) const top = (currentMinutes / 60) * HOUR_HEIGHT + 8; return ( ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: 'transparent', }, dateHeader: { paddingHorizontal: 20, paddingVertical: 16, }, dateText: { fontSize: 16, fontWeight: '700', marginBottom: 4, }, eventCount: { fontSize: 12, fontWeight: '500', }, timelineContainer: { flex: 1, }, timeline: { position: 'relative', minHeight: 24 * HOUR_HEIGHT, }, timeLabelsContainer: { paddingLeft: 20, }, timeLabelRow: { flexDirection: 'row', alignItems: 'flex-start', paddingTop: 8, }, timeLabel: { width: TIME_LABEL_WIDTH, fontSize: 12, fontWeight: '500', textAlign: 'right', paddingRight: 12, }, timeLine: { flex: 1, borderBottomWidth: 1, marginLeft: 8, marginRight: 24, }, eventsContainer: { position: 'absolute', top: 0, left: 0, right: 0, height: 24 * HOUR_HEIGHT, }, eventContainer: { position: 'absolute', borderRadius: 12, borderLeftWidth: 0, shadowColor: '#000', shadowOffset: { width: 0, height: 2, }, shadowOpacity: 0.08, shadowRadius: 6, elevation: 4, }, eventContent: { flex: 1, padding: 12, justifyContent: 'space-between', }, eventHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8, }, eventTitle: { fontSize: 14, fontWeight: '700', lineHeight: 18, flex: 1, }, categoryTag: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12, }, categoryText: { fontSize: 11, fontWeight: '600', }, eventFooter: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, timeContainer: { flexDirection: 'row', alignItems: 'center', }, eventTime: { fontSize: 12, fontWeight: '500', marginLeft: 6, color: '#8E8E93', }, iconContainer: { flexDirection: 'row', alignItems: 'center', gap: 8, }, completedIcon: { position: 'absolute', top: 4, right: 4, }, currentTimeLine: { position: 'absolute', left: TIME_LABEL_WIDTH + 24, right: 24, height: 2, zIndex: 10, }, currentTimeDot: { position: 'absolute', left: -6, top: -4, width: 10, height: 10, borderRadius: 5, }, emptyState: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingVertical: 60, }, emptyText: { fontSize: 16, fontWeight: '500', marginTop: 12, }, });