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,
},
});