Files
digital-pilates/app/task-list.tsx
richarjiang 4f2bd76b8f feat: 实现目标列表左滑删除功能及相关组件
- 在目标列表中添加左滑删除功能,用户可通过左滑手势显示删除按钮并确认删除目标
- 修改 GoalCard 组件,使用 Swipeable 组件包装卡片内容,支持删除操作
- 更新目标列表页面,集成删除目标的逻辑,确保与 Redux 状态管理一致
- 添加开发模式下的模拟数据,方便测试删除功能
- 更新相关文档,详细描述左滑删除功能的实现和使用方法
2025-08-23 17:58:39 +08:00

289 lines
7.7 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 { DateSelector } from '@/components/DateSelector';
import { TaskCard } from '@/components/TaskCard';
import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { TAB_BAR_BOTTOM_OFFSET, TAB_BAR_HEIGHT } from '@/constants/TabBar';
import { useColorScheme } from '@/hooks/useColorScheme';
import { tasksApi } from '@/services/tasksApi';
import { TaskListItem } from '@/types/goals';
import { getTodayIndexInMonth } from '@/utils/date';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Alert, FlatList, RefreshControl, SafeAreaView, StatusBar, StyleSheet, Text, View } from 'react-native';
export default function GoalsDetailScreen() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const router = useRouter();
// 本地状态管理
const [tasks, setTasks] = useState<TaskListItem[]>([]);
const [tasksLoading, setTasksLoading] = useState(false);
const [tasksError, setTasksError] = useState<string | null>(null);
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [refreshing, setRefreshing] = useState(false);
// 日期选择器相关状态
const [selectedIndex, setSelectedIndex] = useState(getTodayIndexInMonth());
// 加载任务列表
const loadTasks = async (targetDate?: Date) => {
try {
setTasksLoading(true);
setTasksError(null);
const dateToUse = targetDate || selectedDate;
console.log('Loading tasks for date:', dayjs(dateToUse).format('YYYY-MM-DD'));
const response = await tasksApi.getTasks({
startDate: dayjs(dateToUse).startOf('day').toISOString(),
endDate: dayjs(dateToUse).endOf('day').toISOString(),
});
console.log('Tasks API response:', response);
setTasks(response.list || []);
} catch (error: any) {
console.error('Failed to load tasks:', error);
setTasksError(error.message || '获取任务列表失败');
setTasks([]);
} finally {
setTasksLoading(false);
}
};
// 页面聚焦时重新加载数据
useFocusEffect(
useCallback(() => {
console.log('useFocusEffect - loading tasks');
loadTasks();
}, [])
);
// 下拉刷新
const onRefresh = async () => {
setRefreshing(true);
try {
await loadTasks();
} finally {
setRefreshing(false);
}
};
// 处理错误提示
useEffect(() => {
if (tasksError) {
Alert.alert('错误', tasksError);
setTasksError(null);
}
}, [tasksError]);
// 日期选择处理
const onSelectDate = async (index: number, date: Date) => {
console.log('Date selected:', dayjs(date).format('YYYY-MM-DD'));
setSelectedIndex(index);
setSelectedDate(date);
// 重新加载对应日期的任务数据
await loadTasks(date);
};
// 根据选中日期筛选任务,并将已完成的任务放到最后
const filteredTasks = useMemo(() => {
const selected = dayjs(selectedDate);
const filtered = tasks.filter(task => {
if (task.status === 'skipped') return false;
const taskDate = dayjs(task.startDate);
return taskDate.isSame(selected, 'day');
});
// 对筛选结果进行排序:已完成的任务放到最后
return [...filtered].sort((a, b) => {
const aCompleted = a.status === 'completed';
const bCompleted = b.status === 'completed';
// 如果a已完成而b未完成a排在后面
if (aCompleted && !bCompleted) {
return 1;
}
// 如果b已完成而a未完成b排在后面
if (bCompleted && !aCompleted) {
return -1;
}
// 如果都已完成或都未完成,保持原有顺序
return 0;
});
}, [selectedDate, tasks]);
const handleBackPress = () => {
router.back();
};
// 渲染任务项
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
<TaskCard
task={item}
/>
);
// 渲染空状态
const renderEmptyState = () => {
const selectedDateStr = dayjs(selectedDate).format('YYYY年M月D日');
if (tasksLoading) {
return (
<View style={styles.emptyState}>
<Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}>
...
</Text>
</View>
);
}
return (
<View style={styles.emptyState}>
<Text style={[styles.emptyStateTitle, { color: colorTokens.text }]}>
</Text>
<Text style={[styles.emptyStateSubtitle, { color: colorTokens.textSecondary }]}>
{selectedDateStr}
</Text>
</View>
);
};
return (
<SafeAreaView style={styles.container}>
<StatusBar
backgroundColor="transparent"
translucent
/>
{/* 背景渐变 */}
<LinearGradient
colors={['#F0F9FF', '#E0F2FE']}
style={styles.gradientBackground}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
{/* 装饰性圆圈 */}
<View style={styles.decorativeCircle1} />
<View style={styles.decorativeCircle2} />
<View style={styles.content}>
{/* 标题区域 */}
<HeaderBar
title="任务列表"
onBack={handleBackPress}
transparent={true}
withSafeTop={false}
/>
{/* 日期选择器 */}
<View style={styles.dateSelector}>
<DateSelector
selectedIndex={selectedIndex}
onDateSelect={onSelectDate}
showMonthTitle={true}
disableFutureDates={true}
/>
</View>
{/* 任务列表 */}
<View style={styles.taskListContainer}>
<FlatList
data={filteredTasks}
renderItem={renderTaskItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.taskList}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={['#0EA5E9']}
tintColor="#0EA5E9"
/>
}
ListEmptyComponent={renderEmptyState}
/>
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
opacity: 0.6,
},
decorativeCircle1: {
position: 'absolute',
top: -20,
right: -20,
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#0EA5E9',
opacity: 0.1,
},
decorativeCircle2: {
position: 'absolute',
bottom: -15,
left: -15,
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: '#0EA5E9',
opacity: 0.05,
},
content: {
flex: 1,
},
// 日期选择器样式
dateSelector: {
paddingHorizontal: 20,
paddingVertical: 16,
},
taskListContainer: {
flex: 1,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
overflow: 'hidden',
},
taskList: {
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: TAB_BAR_HEIGHT + TAB_BAR_BOTTOM_OFFSET + 20,
},
emptyState: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptyStateTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 8,
},
emptyStateSubtitle: {
fontSize: 14,
textAlign: 'center',
lineHeight: 20,
},
});