- 在项目中引入expo-notifications库,支持本地推送通知功能 - 实现通知权限管理,用户可选择开启或关闭通知 - 新增通知发送、定时通知和重复通知功能 - 更新个人页面,集成通知开关和权限请求逻辑 - 编写推送通知功能实现文档,详细描述功能和使用方法 - 优化心情日历页面,确保数据实时刷新
374 lines
9.3 KiB
TypeScript
374 lines
9.3 KiB
TypeScript
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 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 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}"吗?跳过后将无法恢复。`,
|
|
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 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={() => {}}
|
|
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: `${Math.min(task.progressPercentage, 100)}%`,
|
|
backgroundColor: colorTokens.primary,
|
|
},
|
|
]}
|
|
/>
|
|
</View>
|
|
|
|
{/* 底部信息 */}
|
|
<View style={styles.footer}>
|
|
<View style={styles.teamSection}>
|
|
{/* 模拟团队成员头像 */}
|
|
<View style={styles.avatars}>
|
|
<View style={[styles.avatar, { backgroundColor: '#FBBF24' }]}>
|
|
<Text style={styles.avatarText}>A</Text>
|
|
</View>
|
|
<View style={[styles.avatar, { backgroundColor: '#34D399' }]}>
|
|
<Text style={styles.avatarText}>B</Text>
|
|
</View>
|
|
<View style={[styles.avatar, { backgroundColor: '#60A5FA' }]}>
|
|
<Text style={styles.avatarText}>C</Text>
|
|
</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: 2,
|
|
backgroundColor: '#E5E7EB',
|
|
borderRadius: 1,
|
|
marginBottom: 16,
|
|
overflow: 'hidden',
|
|
},
|
|
progressFill: {
|
|
height: '100%',
|
|
borderRadius: 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: 12,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginRight: -8,
|
|
borderWidth: 2,
|
|
borderColor: '#FFFFFF',
|
|
},
|
|
avatarText: {
|
|
fontSize: 10,
|
|
fontWeight: '600',
|
|
color: '#FFFFFF',
|
|
},
|
|
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',
|
|
},
|
|
});
|