feat: 实现目标列表左滑删除功能及相关组件
- 在目标列表中添加左滑删除功能,用户可通过左滑手势显示删除按钮并确认删除目标 - 修改 GoalCard 组件,使用 Swipeable 组件包装卡片内容,支持删除操作 - 更新目标列表页面,集成删除目标的逻辑,确保与 Redux 状态管理一致 - 添加开发模式下的模拟数据,方便测试删除功能 - 更新相关文档,详细描述左滑删除功能的实现和使用方法
This commit is contained in:
288
app/task-list.tsx
Normal file
288
app/task-list.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user