Files
digital-pilates/components/TaskCard.tsx

502 lines
12 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 { useRouter } from 'expo-router';
import React from 'react';
import { Alert, Animated, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface TaskCardProps {
task: TaskListItem;
onTaskCompleted?: (task: TaskListItem) => void; // 任务完成回调
}
export const TaskCard: React.FC<TaskCardProps> = ({
task,
onTaskCompleted,
}) => {
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) : 6;
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();
// 触发任务完成回调
onTaskCompleted?.(task);
} 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/icon-complete-gradient.png')}
style={styles.taskIcon}
resizeMode="contain"
/>
</TouchableOpacity>
{/* 跳过任务图标 - 仅对进行中的任务显示 */}
{task.status === 'pending' && (
<TouchableOpacity
style={styles.skipIconContainer}
onPress={handleSkipTask}
>
<Image
source={require('@/assets/images/task/icon-skip.png')}
style={styles.taskIcon}
resizeMode="contain"
/>
</TouchableOpacity>
)}
</View>
);
};
return (
<TouchableOpacity
style={[
styles.container,
{
backgroundColor: task.status === 'completed'
? 'rgba(248, 250, 252, 0.8)' // 已完成任务使用稍微透明的背景色
: colorTokens.background
}
]}
onPress={handleTaskPress}
activeOpacity={0.7}
>
<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>
{/* 进度条 */}
<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>
{/* 操作按钮 */}
{renderActionIcons()}
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
padding: 14,
borderRadius: 30,
marginBottom: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
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,
},
titleSection: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
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',
},
progressBarOld: {
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,
},
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%',
},
infoTag: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
},
infoTagText: {
fontSize: 12,
fontWeight: '500',
color: '#374151',
},
});