Files
digital-pilates/components/TaskCard.tsx
richarjiang b807e498ed feat: 新增任务详情页面及相关功能
- 创建任务详情页面,展示任务的详细信息,包括状态、描述、优先级和进度
- 实现任务完成和跳过功能,用户可通过按钮操作更新任务状态
- 添加评论功能,用户可以对任务进行评论并发送
- 优化任务卡片,点击后可跳转至任务详情页面
- 更新相关样式,确保界面一致性和美观性
2025-08-23 13:33:39 +08:00

431 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, 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 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}>
<View
style={[
styles.progressFill,
{
width: task.progressPercentage > 0 ? `${Math.min(task.progressPercentage, 100)}%` : '2%',
backgroundColor: task.progressPercentage >= 100
? '#10B981'
: task.progressPercentage >= 50
? '#F59E0B'
: 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',
},
});