Files
digital-pilates/components/TaskCard.tsx
richarjiang 8a7599f630 feat: 新增目标页面引导功能及相关组件
- 在目标页面中集成用户引导功能,帮助用户了解页面各项功能
- 创建 GoalsPageGuide 组件,支持多步骤引导和动态高亮功能区域
- 实现引导状态的检查、标记和重置功能,确保用户体验
- 添加开发测试按钮,方便开发者重置引导状态
- 更新相关文档,详细描述引导功能的实现和使用方法
2025-08-23 13:53:18 +08:00

442 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useGlobalDialog } from '@/components/ui/DialogProvider';
import { Colors } from '@/constants/Colors';
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';
interface TaskCardProps {
task: TaskListItem;
}
export const TaskCard: React.FC<TaskCardProps> = ({
task,
}) => {
const theme = useColorScheme() ?? 'light';
const colorTokens = Colors[theme];
const dispatch = useAppDispatch();
const { showConfirm } = useGlobalDialog();
const router = useRouter();
// 创建进度条动画值
const progressAnimation = React.useRef(new Animated.Value(0)).current;
// 当任务进度变化时,启动动画
React.useEffect(() => {
const targetProgress = task.progressPercentage > 0 ? Math.min(task.progressPercentage, 100) : 2;
Animated.timing(progressAnimation, {
toValue: targetProgress,
duration: 800, // 动画持续时间800毫秒
useNativeDriver: false, // 因为我们要动画width属性所以不能使用原生驱动
}).start();
}, [task.progressPercentage, progressAnimation]);
const getStatusText = (status: string) => {
switch (status) {
case 'completed':
return '已完成';
case 'in_progress':
return '进行中';
case 'overdue':
return '已过期';
case 'skipped':
return '已跳过';
default:
return '待开始';
}
};
const getPriorityColor = (status: string) => {
switch (status) {
case 'overdue':
return '#EF4444'; // High - 过期任务
case 'in_progress':
return '#F59E0B'; // Medium - 进行中
case 'completed':
return '#10B981'; // Low - 已完成
default:
return '#6B7280'; // Default - 待开始
}
};
const getPriorityText = (status: string) => {
switch (status) {
case 'overdue':
return '高';
case 'in_progress':
return '中';
case 'completed':
return '低';
default:
return '';
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const month = date.toLocaleDateString('zh-CN', { month: 'short' });
const day = date.getDate();
return `${day} ${month}`;
};
const handleCompleteTask = async () => {
// 如果任务已经完成,不执行任何操作
if (task.status === 'completed') {
return;
}
try {
// 调用完成任务 API
await dispatch(completeTask({
taskId: task.id,
completionData: {
count: 1,
notes: '通过任务卡片完成'
}
})).unwrap();
} catch (error) {
Alert.alert('错误', '完成任务失败,请重试');
}
};
const handleSkipTask = async () => {
// 如果任务已经完成或已跳过,不执行任何操作
if (task.status === 'completed' || task.status === 'skipped') {
return;
}
// 显示确认弹窗
showConfirm(
{
title: '确认跳过任务',
message: `确定要跳过任务"${task.title}"吗?\n\n跳过后的任务将不会显示在任务列表中且无法恢复。`,
confirmText: '跳过',
cancelText: '取消',
destructive: true,
icon: 'warning',
iconColor: '#F59E0B',
},
async () => {
try {
// 调用跳过任务 API
await dispatch(skipTask({
taskId: task.id,
skipData: {
reason: '用户主动跳过'
}
})).unwrap();
} catch (error) {
Alert.alert('错误', '跳过任务失败,请重试');
}
}
);
};
const handleTaskPress = () => {
router.push(`/task-detail?taskId=${task.id}`);
};
const renderActionIcons = () => {
if (task.status === 'completed' || task.status === 'overdue' || task.status === 'skipped') {
return null;
}
return (
<View style={styles.actionIconsContainer}>
{/* 完成任务图标 */}
<TouchableOpacity
style={styles.iconContainer}
onPress={handleCompleteTask}
>
<Image
source={require('@/assets/images/task/iconTaskHeader.png')}
style={styles.taskIcon}
resizeMode="contain"
/>
</TouchableOpacity>
{/* 跳过任务图标 - 仅对进行中的任务显示 */}
{task.status === 'pending' && (
<TouchableOpacity
style={styles.skipIconContainer}
onPress={handleSkipTask}
>
<MaterialIcons name="skip-next" size={20} color="#6B7280" />
</TouchableOpacity>
)}
</View>
);
};
return (
<TouchableOpacity
style={[styles.container, { backgroundColor: colorTokens.background }]}
onPress={handleTaskPress}
activeOpacity={0.7}
>
{/* 头部区域 */}
<View style={styles.header}>
<View style={styles.titleSection}>
<Text style={[styles.title, { color: colorTokens.text }]} numberOfLines={2}>
{task.title}
</Text>
{renderActionIcons()}
</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 style={[styles.priorityTag, { backgroundColor: getPriorityColor(task.status) }]}>
<MaterialIcons name="flag" size={12} color="#FFFFFF" />
<Text style={styles.priorityTagText}>{getPriorityText(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>
</View>
{/* 底部信息 */}
<View style={styles.footer}>
<View style={styles.teamSection}>
{/* 团队成员头像 */}
<View style={styles.avatars}>
<View style={styles.avatar}>
<Image
source={require('@/assets/images/Sealife.jpeg')}
style={styles.avatarImage}
resizeMode="cover"
/>
</View>
</View>
</View>
<View style={styles.infoSection}>
<View style={styles.infoTag}>
<MaterialIcons name="event" size={12} color="#6B7280" />
<Text style={styles.infoTagText}>{formatDate(task.startDate)}</Text>
</View>
<View style={styles.infoTag}>
<MaterialIcons name="chat-bubble-outline" size={12} color="#6B7280" />
<Text style={styles.infoTagText}>2</Text>
</View>
</View>
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
padding: 16,
borderRadius: 12,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
header: {
marginBottom: 12,
},
titleSection: {
flexDirection: 'row',
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,
backgroundColor: '#7A5AF8',
alignItems: 'center',
justifyContent: 'center',
},
skipIconContainer: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#F3F4F6',
alignItems: 'center',
justifyContent: 'center',
borderWidth: 1,
borderColor: '#E5E7EB',
},
taskIcon: {
width: 20,
height: 20,
},
tagsContainer: {
flexDirection: 'row',
gap: 8,
marginBottom: 12,
},
statusTag: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: '#F3F4F6',
},
statusTagText: {
fontSize: 12,
fontWeight: '500',
color: '#374151',
},
priorityTag: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
priorityTagText: {
fontSize: 12,
fontWeight: '500',
color: '#FFFFFF',
},
progressBar: {
height: 6,
backgroundColor: '#F3F4F6',
borderRadius: 3,
marginBottom: 16,
overflow: 'visible',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
position: 'relative',
},
progressFill: {
height: '100%',
borderRadius: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 3,
},
progressGlow: {
position: 'absolute',
right: 0,
top: 0,
width: 8,
height: '100%',
backgroundColor: 'rgba(255, 255, 255, 0.6)',
borderRadius: 3,
},
progressTextContainer: {
position: 'absolute',
right: 0,
top: -6,
backgroundColor: '#FFFFFF',
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
borderWidth: 1,
borderColor: '#E5E7EB',
zIndex: 1,
},
progressText: {
fontSize: 10,
fontWeight: '600',
color: '#374151',
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
teamSection: {
flexDirection: 'row',
alignItems: 'center',
},
avatars: {
flexDirection: 'row',
alignItems: 'center',
},
avatar: {
width: 24,
height: 24,
borderRadius: 24,
marginRight: -8,
borderWidth: 2,
borderColor: '#FFFFFF',
overflow: 'hidden',
},
avatarImage: {
width: '100%',
height: '100%',
},
infoSection: {
flexDirection: 'row',
gap: 8,
},
infoTag: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: '#F3F4F6',
},
infoTagText: {
fontSize: 12,
fontWeight: '500',
color: '#374151',
},
});