Files
digital-pilates/components/GoalCard.tsx
richarjiang 4f2bd76b8f feat: 实现目标列表左滑删除功能及相关组件
- 在目标列表中添加左滑删除功能,用户可通过左滑手势显示删除按钮并确认删除目标
- 修改 GoalCard 组件,使用 Swipeable 组件包装卡片内容,支持删除操作
- 更新目标列表页面,集成删除目标的逻辑,确保与 Redux 状态管理一致
- 添加开发模式下的模拟数据,方便测试删除功能
- 更新相关文档,详细描述左滑删除功能的实现和使用方法
2025-08-23 17:58:39 +08:00

290 lines
7.2 KiB
TypeScript

import { GoalListItem } from '@/types/goals';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import React, { useRef } from 'react';
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Swipeable } from 'react-native-gesture-handler';
interface GoalCardProps {
goal: GoalListItem;
onPress?: (goal: GoalListItem) => void;
onDelete?: (goalId: string) => void;
showStatus?: boolean;
}
export const GoalCard: React.FC<GoalCardProps> = ({
goal,
onPress,
onDelete,
showStatus = true
}) => {
const swipeableRef = useRef<Swipeable>(null);
// 获取重复类型显示文本
const getRepeatTypeText = (goal: GoalListItem) => {
switch (goal.repeatType) {
case 'daily':
return '每日';
case 'weekly':
return '每周';
case 'monthly':
return '每月';
default:
return '每日';
}
};
// 获取目标状态显示文本
const getStatusText = (goal: GoalListItem) => {
switch (goal.status) {
case 'active':
return '进行中';
case 'paused':
return '已暂停';
case 'completed':
return '已完成';
case 'cancelled':
return '已取消';
default:
return '进行中';
}
};
// 获取目标状态颜色
const getStatusColor = (goal: GoalListItem) => {
switch (goal.status) {
case 'active':
return '#10B981';
case 'paused':
return '#F59E0B';
case 'completed':
return '#3B82F6';
case 'cancelled':
return '#EF4444';
default:
return '#10B981';
}
};
// 获取目标图标
const getGoalIcon = (goal: GoalListItem) => {
// 根据目标类别或标题返回不同的图标
const title = goal.title.toLowerCase();
const category = goal.category?.toLowerCase();
if (title.includes('运动') || title.includes('健身') || title.includes('跑步')) {
return 'fitness-center';
} else if (title.includes('喝水') || title.includes('饮水')) {
return 'local-drink';
} else if (title.includes('睡眠') || title.includes('睡觉')) {
return 'bedtime';
} else if (title.includes('学习') || title.includes('读书')) {
return 'school';
} else if (title.includes('冥想') || title.includes('放松')) {
return 'self-improvement';
} else if (title.includes('早餐') || title.includes('午餐') || title.includes('晚餐')) {
return 'restaurant';
} else {
return 'flag';
}
};
// 处理删除操作
const handleDelete = () => {
Alert.alert(
'确认删除',
`确定要删除目标"${goal.title}"吗?此操作无法撤销。`,
[
{
text: '取消',
style: 'cancel',
},
{
text: '删除',
style: 'destructive',
onPress: () => {
onDelete?.(goal.id);
swipeableRef.current?.close();
},
},
]
);
};
// 渲染删除按钮
const renderRightActions = () => {
return (
<TouchableOpacity
style={styles.deleteButton}
onPress={handleDelete}
activeOpacity={0.8}
>
<MaterialIcons name="delete" size={24} color="#EF4444" />
</TouchableOpacity>
);
};
return (
<Swipeable
ref={swipeableRef}
renderRightActions={renderRightActions}
rightThreshold={40}
overshootRight={false}
>
<TouchableOpacity
style={styles.goalCard}
onPress={() => onPress?.(goal)}
activeOpacity={0.7}
>
{/* 左侧图标 */}
<View style={styles.goalIcon}>
<MaterialIcons name={getGoalIcon(goal)} size={20} color="#7A5AF8" />
<View style={styles.iconStars}>
<View style={styles.star} />
<View style={styles.star} />
</View>
</View>
{/* 中间内容 */}
<View style={styles.goalContent}>
<Text style={styles.goalTitle} numberOfLines={1}>
{goal.title}
</Text>
{/* 底部信息行 */}
<View style={styles.goalInfo}>
{/* 积分 */}
<View style={styles.infoItem}>
<Text style={styles.infoText}>+1</Text>
</View>
{/* 目标数量 */}
<View style={styles.infoItem}>
<Text style={styles.infoText}>
{goal.targetCount || goal.frequency}
</Text>
</View>
{/* 提醒图标(如果有提醒) */}
{goal.hasReminder && (
<View style={styles.infoItem}>
<MaterialIcons name="notifications" size={12} color="#9CA3AF" />
</View>
)}
{/* 提醒时间(如果有提醒) */}
{goal.hasReminder && goal.reminderTime && (
<View style={styles.infoItem}>
<Text style={styles.infoText}>{goal.reminderTime}</Text>
</View>
)}
{/* 重复图标 */}
<View style={styles.infoItem}>
<MaterialIcons name="loop" size={12} color="#9CA3AF" />
</View>
{/* 重复类型 */}
<View style={styles.infoItem}>
<Text style={styles.infoText}>{getRepeatTypeText(goal)}</Text>
</View>
</View>
</View>
{/* 右侧状态指示器 */}
{showStatus && (
<View style={[styles.statusIndicator, { backgroundColor: getStatusColor(goal) }]}>
<Text style={styles.statusText}>{getStatusText(goal)}</Text>
</View>
)}
</TouchableOpacity>
</Swipeable>
);
};
const styles = StyleSheet.create({
goalCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
},
goalIcon: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#F3F4F6',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
position: 'relative',
},
iconStars: {
position: 'absolute',
top: -2,
right: -2,
flexDirection: 'row',
gap: 1,
},
star: {
width: 4,
height: 4,
borderRadius: 2,
backgroundColor: '#FFFFFF',
},
goalContent: {
flex: 1,
marginRight: 12,
},
goalTitle: {
fontSize: 16,
fontWeight: '600',
color: '#1F2937',
marginBottom: 8,
},
goalInfo: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
infoItem: {
flexDirection: 'row',
alignItems: 'center',
},
infoText: {
fontSize: 12,
color: '#9CA3AF',
fontWeight: '500',
},
statusIndicator: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
minWidth: 60,
alignItems: 'center',
},
statusText: {
fontSize: 10,
color: '#FFFFFF',
fontWeight: '600',
},
deleteButton: {
width: 60,
height: '100%',
justifyContent: 'center',
alignItems: 'center',
},
deleteButtonText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
marginTop: 4,
},
});