feat: 实现目标列表左滑删除功能及相关组件
- 在目标列表中添加左滑删除功能,用户可通过左滑手势显示删除按钮并确认删除目标 - 修改 GoalCard 组件,使用 Swipeable 组件包装卡片内容,支持删除操作 - 更新目标列表页面,集成删除目标的逻辑,确保与 Redux 状态管理一致 - 添加开发模式下的模拟数据,方便测试删除功能 - 更新相关文档,详细描述左滑删除功能的实现和使用方法
This commit is contained in:
@@ -1,197 +1,289 @@
|
||||
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 GoalItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
time: string;
|
||||
category: 'workout' | 'finance' | 'personal' | 'work' | 'health';
|
||||
priority?: 'high' | 'medium' | 'low';
|
||||
}
|
||||
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 {
|
||||
item: GoalItem;
|
||||
onPress?: (item: GoalItem) => void;
|
||||
goal: GoalListItem;
|
||||
onPress?: (goal: GoalListItem) => void;
|
||||
onDelete?: (goalId: string) => void;
|
||||
showStatus?: boolean;
|
||||
}
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
const CARD_WIDTH = (screenWidth - 60) * 0.65; // 显示1.5张卡片
|
||||
export const GoalCard: React.FC<GoalCardProps> = ({
|
||||
goal,
|
||||
onPress,
|
||||
onDelete,
|
||||
showStatus = true
|
||||
}) => {
|
||||
const swipeableRef = useRef<Swipeable>(null);
|
||||
|
||||
const getCategoryIcon = (category: GoalItem['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 getRepeatTypeText = (goal: GoalListItem) => {
|
||||
switch (goal.repeatType) {
|
||||
case 'daily':
|
||||
return '每日';
|
||||
case 'weekly':
|
||||
return '每周';
|
||||
case 'monthly':
|
||||
return '每月';
|
||||
default:
|
||||
return '每日';
|
||||
}
|
||||
};
|
||||
|
||||
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';
|
||||
}
|
||||
};
|
||||
// 获取目标状态显示文本
|
||||
const getStatusText = (goal: GoalListItem) => {
|
||||
switch (goal.status) {
|
||||
case 'active':
|
||||
return '进行中';
|
||||
case 'paused':
|
||||
return '已暂停';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
case 'cancelled':
|
||||
return '已取消';
|
||||
default:
|
||||
return '进行中';
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: GoalItem['priority']) => {
|
||||
switch (priority) {
|
||||
case 'high':
|
||||
return '#FF4757';
|
||||
case 'medium':
|
||||
return '#FFA502';
|
||||
case 'low':
|
||||
return '#2ED573';
|
||||
default:
|
||||
return '#747D8C';
|
||||
}
|
||||
};
|
||||
// 获取目标状态颜色
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
export function GoalCard({ item, onPress }: GoalCardProps) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorTokens = Colors[theme];
|
||||
// 获取目标图标
|
||||
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 categoryColor = getCategoryColor(item.category);
|
||||
const categoryIcon = getCategoryIcon(item.category);
|
||||
const priorityColor = getPriorityColor(item.priority);
|
||||
// 处理删除操作
|
||||
const handleDelete = () => {
|
||||
Alert.alert(
|
||||
'确认删除',
|
||||
`确定要删除目标"${goal.title}"吗?此操作无法撤销。`,
|
||||
[
|
||||
{
|
||||
text: '取消',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: '删除',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
onDelete?.(goal.id);
|
||||
swipeableRef.current?.close();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const timeFormatted = dayjs(item.time).format('HH:mm');
|
||||
// 渲染删除按钮
|
||||
const renderRightActions = () => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={handleDelete}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialIcons name="delete" size={24} color="#EF4444" />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.container, { backgroundColor: colorTokens.card }]}
|
||||
onPress={() => onPress?.(item)}
|
||||
activeOpacity={0.8}
|
||||
<Swipeable
|
||||
ref={swipeableRef}
|
||||
renderRightActions={renderRightActions}
|
||||
rightThreshold={40}
|
||||
overshootRight={false}
|
||||
>
|
||||
{/* 顶部标签和优先级 */}
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{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}
|
||||
{/* 中间内容 */}
|
||||
<View style={styles.goalContent}>
|
||||
<Text style={styles.goalTitle} numberOfLines={1}>
|
||||
{goal.title}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 底部信息行 */}
|
||||
<View style={styles.goalInfo}>
|
||||
{/* 积分 */}
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoText}>+1</Text>
|
||||
</View>
|
||||
|
||||
{/* 底部时间 */}
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.timeContainer}>
|
||||
<Ionicons
|
||||
name='time-outline'
|
||||
size={14}
|
||||
color={colorTokens.textMuted}
|
||||
/>
|
||||
<Text style={[styles.timeText, { color: colorTokens.textMuted }]}>
|
||||
{timeFormatted}
|
||||
</Text>
|
||||
{/* 目标数量 */}
|
||||
<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>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 右侧状态指示器 */}
|
||||
{showStatus && (
|
||||
<View style={[styles.statusIndicator, { backgroundColor: getStatusColor(goal) }]}>
|
||||
<Text style={styles.statusText}>{getStatusText(goal)}</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Swipeable>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: CARD_WIDTH,
|
||||
height: 140,
|
||||
marginHorizontal: 8,
|
||||
borderRadius: 20,
|
||||
goalCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
elevation: 6,
|
||||
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',
|
||||
},
|
||||
header: {
|
||||
iconStars: {
|
||||
position: 'absolute',
|
||||
top: -2,
|
||||
right: -2,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
gap: 1,
|
||||
},
|
||||
categoryBadge: {
|
||||
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,
|
||||
backgroundColor: '#4ECDC4',
|
||||
minWidth: 60,
|
||||
alignItems: 'center',
|
||||
},
|
||||
categoryText: {
|
||||
statusText: {
|
||||
fontSize: 10,
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
marginLeft: 4,
|
||||
textTransform: 'capitalize',
|
||||
},
|
||||
priorityDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#FF4757',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
deleteButton: {
|
||||
width: 60,
|
||||
height: '100%',
|
||||
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: {
|
||||
deleteButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '500',
|
||||
marginLeft: 4,
|
||||
fontWeight: '600',
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useAppDispatch } from '@/hooks/redux';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { completeTask, skipTask } from '@/store/tasksSlice';
|
||||
import { TaskListItem } from '@/types/goals';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { Alert, Animated, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
@@ -27,7 +26,7 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
||||
|
||||
// 当任务进度变化时,启动动画
|
||||
React.useEffect(() => {
|
||||
const targetProgress = task.progressPercentage > 0 ? Math.min(task.progressPercentage, 100) : 2;
|
||||
const targetProgress = task.progressPercentage > 0 ? Math.min(task.progressPercentage, 100) : 6;
|
||||
|
||||
Animated.timing(progressAnimation, {
|
||||
toValue: targetProgress,
|
||||
@@ -36,7 +35,6 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
||||
}).start();
|
||||
}, [task.progressPercentage, progressAnimation]);
|
||||
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
@@ -179,51 +177,93 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.container, { backgroundColor: colorTokens.background }]}
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: task.status === 'completed'
|
||||
? 'rgba(248, 250, 252, 0.8)' // 已完成任务使用稍微透明的背景色
|
||||
: colorTokens.background
|
||||
}
|
||||
]}
|
||||
onPress={handleTaskPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{/* 头部区域 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={[styles.title, { color: colorTokens.text }]} numberOfLines={2}>
|
||||
<View style={styles.cardContent}>
|
||||
{/* 左侧图标区域 */}
|
||||
<View style={styles.iconSection}>
|
||||
<View style={[
|
||||
styles.iconCircle,
|
||||
{
|
||||
backgroundColor: task.status === 'completed'
|
||||
? '#EDE9FE' // 完成状态使用更深的紫色背景
|
||||
: '#F3E8FF',
|
||||
}
|
||||
]}>
|
||||
<Image
|
||||
source={require('@/assets/images/task/icon-copy.png')}
|
||||
style={styles.taskIcon}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 右侧信息区域 */}
|
||||
<View style={styles.infoSection}>
|
||||
{/* 任务标题 */}
|
||||
<Text style={[
|
||||
styles.title,
|
||||
{
|
||||
color: task.status === 'completed'
|
||||
? '#6B7280'
|
||||
: colorTokens.text,
|
||||
}
|
||||
]} numberOfLines={1}>
|
||||
{task.title}
|
||||
</Text>
|
||||
{renderActionIcons()}
|
||||
|
||||
{/* 进度条 */}
|
||||
<View style={styles.progressContainer}>
|
||||
{/* 背景进度条 */}
|
||||
<View style={styles.progressBackground} />
|
||||
|
||||
{/* 实际进度条 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{
|
||||
width: progressAnimation.interpolate({
|
||||
inputRange: [0, 100],
|
||||
outputRange: ['6%', '100%'], // 最小显示6%确保可见
|
||||
}),
|
||||
backgroundColor: task.status === 'completed'
|
||||
? '#8B5CF6' // 完成状态也使用紫色
|
||||
: task.progressPercentage > 0
|
||||
? '#8B5CF6'
|
||||
: '#C7D2FE', // 浅紫色,表示待开始
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* 进度文字 */}
|
||||
<Text style={[
|
||||
styles.progressText,
|
||||
{
|
||||
color: task.progressPercentage > 20 || task.status === 'completed'
|
||||
? '#FFFFFF'
|
||||
: '#374151', // 进度较少时使用深色文字
|
||||
}
|
||||
]}>
|
||||
{task.currentCount}/{task.targetCount} 次
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 状态和优先级标签 */}
|
||||
<View style={styles.tagsContainer}>
|
||||
<View style={styles.statusTag}>
|
||||
<MaterialIcons name="schedule" size={12} color="#6B7280" />
|
||||
<Text style={styles.statusTagText}>{getStatusText(task.status)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 进度条 */}
|
||||
<View style={styles.progressBar}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.progressFill,
|
||||
{
|
||||
width: progressAnimation.interpolate({
|
||||
inputRange: [0, 100],
|
||||
outputRange: ['0%', '100%'],
|
||||
}),
|
||||
backgroundColor: task.progressPercentage > 0 ? colorTokens.primary : '#E5E7EB',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{task.progressPercentage > 0 && task.progressPercentage < 100 && (
|
||||
<View style={styles.progressGlow} />
|
||||
)}
|
||||
{/* 进度百分比文本 */}
|
||||
<View style={styles.progressTextContainer}>
|
||||
<Text style={styles.progressText}>{task.currentCount}/{task.targetCount}</Text>
|
||||
</View>
|
||||
{/* 操作按钮 */}
|
||||
{renderActionIcons()}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -231,8 +271,8 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
padding: 14,
|
||||
borderRadius: 30,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
@@ -240,6 +280,91 @@ const styles = StyleSheet.create({
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
cardContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 10,
|
||||
},
|
||||
iconSection: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
iconCircle: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#F3E8FF', // 浅紫色背景
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
infoSection: {
|
||||
flex: 1,
|
||||
gap: 8,
|
||||
},
|
||||
title: {
|
||||
fontSize: 15,
|
||||
fontWeight: '600',
|
||||
lineHeight: 20,
|
||||
color: '#1F2937', // 深蓝紫色文字
|
||||
marginBottom: 2,
|
||||
},
|
||||
progressContainer: {
|
||||
position: 'relative',
|
||||
height: 14,
|
||||
justifyContent: 'center',
|
||||
marginTop: 4,
|
||||
},
|
||||
progressBackground: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 14,
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderRadius: 10,
|
||||
},
|
||||
progressBar: {
|
||||
height: 14,
|
||||
borderRadius: 10,
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
minWidth: '6%', // 确保最小宽度可见
|
||||
},
|
||||
progressText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#FFFFFF',
|
||||
textAlign: 'center',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
},
|
||||
actionIconsContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
flexShrink: 0,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#F3F4F6',
|
||||
},
|
||||
skipIconContainer: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: '#F3F4F6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
taskIcon: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
},
|
||||
// 保留其他样式以备后用
|
||||
header: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
@@ -248,38 +373,6 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
lineHeight: 22,
|
||||
flex: 1,
|
||||
},
|
||||
actionIconsContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flexShrink: 0,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#F3F4F6',
|
||||
},
|
||||
skipIconContainer: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F3F4F6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
taskIcon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
tagsContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
@@ -312,7 +405,7 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '500',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
progressBar: {
|
||||
progressBarOld: {
|
||||
height: 6,
|
||||
backgroundColor: '#F3F4F6',
|
||||
borderRadius: 3,
|
||||
@@ -360,11 +453,6 @@ const styles = StyleSheet.create({
|
||||
borderColor: '#E5E7EB',
|
||||
zIndex: 1,
|
||||
},
|
||||
progressText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
@@ -391,10 +479,6 @@ const styles = StyleSheet.create({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
infoSection: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
infoTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -409,3 +493,4 @@ const styles = StyleSheet.create({
|
||||
color: '#374151',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { TaskListItem } from '@/types/goals';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
interface TaskProgressCardProps {
|
||||
tasks: TaskListItem[];
|
||||
@@ -12,23 +13,39 @@ export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
|
||||
tasks,
|
||||
headerButtons,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
// 计算各状态的任务数量
|
||||
const pendingTasks = tasks.filter(task => task.status === 'pending');
|
||||
const completedTasks = tasks.filter(task => task.status === 'completed');
|
||||
const skippedTasks = tasks.filter(task => task.status === 'skipped');
|
||||
|
||||
// 处理跳转到目标列表
|
||||
const handleNavigateToGoals = () => {
|
||||
router.push('/goals-list');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* 标题区域 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>统计</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.goalsIconButton}
|
||||
onPress={handleNavigateToGoals}
|
||||
>
|
||||
<MaterialIcons name="flag" size={18} color="#7A5AF8" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.headerActions}>
|
||||
|
||||
{headerButtons && (
|
||||
<View style={styles.headerButtons}>
|
||||
{headerButtons}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{headerButtons && (
|
||||
<View style={styles.headerButtons}>
|
||||
{headerButtons}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 状态卡片区域 */}
|
||||
@@ -85,15 +102,27 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
headerButtons: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
goalsIconButton: {
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
lineHeight: 24,
|
||||
height: 24,
|
||||
color: '#1F2937',
|
||||
marginBottom: 4,
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ const OxygenSaturationCard: React.FC<OxygenSaturationCardProps> = ({
|
||||
return (
|
||||
<HealthDataCard
|
||||
title="血氧饱和度"
|
||||
value={oxygenSaturation !== null && oxygenSaturation !== undefined ? (oxygenSaturation * 100).toFixed(1) : '--'}
|
||||
value={oxygenSaturation !== null && oxygenSaturation !== undefined ? oxygenSaturation.toFixed(1) : '--'}
|
||||
unit="%"
|
||||
icon={oxygenIcon}
|
||||
style={style}
|
||||
|
||||
Reference in New Issue
Block a user