feat: 新增目标管理功能及相关组件
- 创建目标管理演示页面,展示高保真的目标管理界面 - 实现待办事项卡片的横向滑动展示,支持时间筛选功能 - 新增时间轴组件,展示选中日期的具体任务 - 更新底部导航,添加目标管理和演示页面的路由 - 优化个人页面菜单项,提供目标管理的快速访问 - 编写目标管理功能实现文档,详细描述功能和组件架构
This commit is contained in:
248
components/TodoCard.tsx
Normal file
248
components/TodoCard.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import dayjs from 'dayjs';
|
||||
import React from 'react';
|
||||
import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
export interface TodoItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
time: string;
|
||||
category: 'workout' | 'finance' | 'personal' | 'work' | 'health';
|
||||
isCompleted?: boolean;
|
||||
priority?: 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
interface TodoCardProps {
|
||||
item: TodoItem;
|
||||
onPress?: (item: TodoItem) => void;
|
||||
onToggleComplete?: (item: TodoItem) => void;
|
||||
}
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
const CARD_WIDTH = (screenWidth - 60) * 0.65; // 显示1.5张卡片
|
||||
|
||||
const getCategoryIcon = (category: TodoItem['category']) => {
|
||||
switch (category) {
|
||||
case 'workout':
|
||||
return 'fitness-outline';
|
||||
case 'finance':
|
||||
return 'card-outline';
|
||||
case 'personal':
|
||||
return 'person-outline';
|
||||
case 'work':
|
||||
return 'briefcase-outline';
|
||||
case 'health':
|
||||
return 'heart-outline';
|
||||
default:
|
||||
return 'checkmark-circle-outline';
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryColor = (category: TodoItem['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';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: TodoItem['priority']) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return '#FF4757';
|
||||
case 'medium':
|
||||
return '#FFA502';
|
||||
case 'low':
|
||||
return '#2ED573';
|
||||
default:
|
||||
return '#747D8C';
|
||||
}
|
||||
};
|
||||
|
||||
export function TodoCard({ item, onPress, onToggleComplete }: TodoCardProps) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorTokens = Colors[theme];
|
||||
|
||||
const categoryColor = getCategoryColor(item.category);
|
||||
const categoryIcon = getCategoryIcon(item.category);
|
||||
const priorityColor = getPriorityColor(item.priority);
|
||||
|
||||
const timeFormatted = dayjs(item.time).format('HH:mm');
|
||||
const isToday = dayjs(item.time).isSame(dayjs(), 'day');
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.container, { backgroundColor: colorTokens.card }]}
|
||||
onPress={() => onPress?.(item)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{/* 顶部标签和优先级 */}
|
||||
<View style={styles.header}>
|
||||
<View style={[styles.categoryBadge, { backgroundColor: categoryColor }]}>
|
||||
<Ionicons name={categoryIcon as any} size={12} color="#fff" />
|
||||
<Text style={styles.categoryText}>{item.category}</Text>
|
||||
</View>
|
||||
|
||||
{item.priority && (
|
||||
<View style={[styles.priorityDot, { backgroundColor: priorityColor }]} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 主要内容 */}
|
||||
<View style={styles.content}>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]} numberOfLines={2}>
|
||||
{item.title}
|
||||
</Text>
|
||||
|
||||
{item.description && (
|
||||
<Text style={[styles.description, { color: colorTokens.textSecondary }]} numberOfLines={2}>
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部时间和完成状态 */}
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.timeContainer}>
|
||||
<Ionicons
|
||||
name={isToday ? "time" : "calendar-outline"}
|
||||
size={14}
|
||||
color={colorTokens.textMuted}
|
||||
/>
|
||||
<Text style={[styles.timeText, { color: colorTokens.textMuted }]}>
|
||||
{timeFormatted}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.completeButton,
|
||||
{
|
||||
backgroundColor: item.isCompleted ? colorTokens.primary : 'transparent',
|
||||
borderColor: item.isCompleted ? colorTokens.primary : colorTokens.border
|
||||
}
|
||||
]}
|
||||
onPress={() => onToggleComplete?.(item)}
|
||||
>
|
||||
{item.isCompleted && (
|
||||
<Ionicons name="checkmark" size={16} color={colorTokens.onPrimary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* 完成状态遮罩 */}
|
||||
{item.isCompleted && (
|
||||
<View style={styles.completedOverlay}>
|
||||
<Ionicons name="checkmark-circle" size={24} color={colorTokens.primary} />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: CARD_WIDTH,
|
||||
height: 140,
|
||||
marginHorizontal: 8,
|
||||
borderRadius: 20,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 6,
|
||||
position: 'relative',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
categoryBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#4ECDC4',
|
||||
},
|
||||
categoryText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
marginLeft: 4,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
priorityDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#FF4757',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
lineHeight: 20,
|
||||
marginBottom: 4,
|
||||
},
|
||||
description: {
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
opacity: 0.7,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 12,
|
||||
},
|
||||
timeContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
timeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
marginLeft: 4,
|
||||
},
|
||||
completeButton: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1.5,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
completedOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user