feat: 移除目标管理功能模块
删除了完整的目标管理功能,包括目标创建、编辑、任务管理等相关页面和组件。同时移除了相关的API服务、Redux状态管理、类型定义和通知功能。应用版本从1.0.20升级到1.0.21。
This commit is contained in:
@@ -1,304 +0,0 @@
|
||||
import { GoalListItem } from '@/types/goals';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import React, { useRef } from 'react';
|
||||
import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
|
||||
interface GoalCardProps {
|
||||
goal: GoalListItem;
|
||||
onPress?: (goal: GoalListItem) => void;
|
||||
onDelete?: (goalId: string) => void;
|
||||
showStatus?: boolean;
|
||||
}
|
||||
|
||||
export const GoalCard: React.FC<GoalCardProps> = ({
|
||||
goal,
|
||||
onPress,
|
||||
onDelete,
|
||||
showStatus = true
|
||||
}) => {
|
||||
const swipeableRef = useRef<Swipeable>(null);
|
||||
|
||||
// 获取重复类型显示文本
|
||||
const getRepeatTypeText = (goal: GoalListItem) => {
|
||||
switch (goal.repeatType) {
|
||||
case 'daily':
|
||||
return '每日';
|
||||
case 'weekly':
|
||||
return '每周';
|
||||
case 'monthly':
|
||||
return '每月';
|
||||
default:
|
||||
return '每日';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取目标状态显示文本
|
||||
const getStatusText = (goal: GoalListItem) => {
|
||||
switch (goal.status) {
|
||||
case 'active':
|
||||
return '进行中';
|
||||
case 'paused':
|
||||
return '已暂停';
|
||||
case 'completed':
|
||||
return '已完成';
|
||||
case 'cancelled':
|
||||
return '已取消';
|
||||
default:
|
||||
return '进行中';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取目标状态颜色
|
||||
const getStatusColor = (goal: GoalListItem) => {
|
||||
switch (goal.status) {
|
||||
case 'active':
|
||||
return '#10B981';
|
||||
case 'paused':
|
||||
return '#F59E0B';
|
||||
case 'completed':
|
||||
return '#3B82F6';
|
||||
case 'cancelled':
|
||||
return '#EF4444';
|
||||
default:
|
||||
return '#10B981';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取目标图标
|
||||
const getGoalIcon = (goal: GoalListItem) => {
|
||||
// 根据目标类别或标题返回不同的图标
|
||||
const title = goal.title.toLowerCase();
|
||||
const category = goal.category?.toLowerCase();
|
||||
|
||||
if (title.includes('运动') || title.includes('健身') || title.includes('跑步')) {
|
||||
return 'fitness-center';
|
||||
} else if (title.includes('喝水') || title.includes('饮水')) {
|
||||
return 'local-drink';
|
||||
} else if (title.includes('睡眠') || title.includes('睡觉')) {
|
||||
return 'bedtime';
|
||||
} else if (title.includes('学习') || title.includes('读书')) {
|
||||
return 'school';
|
||||
} else if (title.includes('冥想') || title.includes('放松')) {
|
||||
return 'self-improvement';
|
||||
} else if (title.includes('早餐') || title.includes('午餐') || title.includes('晚餐')) {
|
||||
return 'restaurant';
|
||||
} else {
|
||||
return 'flag';
|
||||
}
|
||||
};
|
||||
|
||||
// 处理删除操作
|
||||
const handleDelete = () => {
|
||||
Alert.alert(
|
||||
'确认删除',
|
||||
`确定要删除目标"${goal.title}"吗?此操作无法撤销。`,
|
||||
[
|
||||
{
|
||||
text: '取消',
|
||||
style: 'cancel',
|
||||
},
|
||||
{
|
||||
text: '删除',
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
onDelete?.(goal.id);
|
||||
swipeableRef.current?.close();
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染删除按钮
|
||||
const renderRightActions = () => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={handleDelete}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<MaterialIcons style={{
|
||||
marginBottom: 10
|
||||
}} name="delete" size={24} color="#EF4444" />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Swipeable
|
||||
ref={swipeableRef}
|
||||
renderRightActions={renderRightActions}
|
||||
rightThreshold={40}
|
||||
overshootRight={false}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.goalCard}
|
||||
onPress={() => onPress?.(goal)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{/* 左侧图标 */}
|
||||
<View style={styles.goalIcon}>
|
||||
<MaterialIcons name={getGoalIcon(goal)} size={20} color="#7A5AF8" />
|
||||
<View style={styles.iconStars}>
|
||||
<View style={styles.star} />
|
||||
<View style={styles.star} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 中间内容 */}
|
||||
<View style={styles.goalContent}>
|
||||
<Text style={styles.goalTitle} numberOfLines={1}>
|
||||
{goal.title}
|
||||
</Text>
|
||||
|
||||
{/* 底部信息行 */}
|
||||
<View style={styles.goalInfo}>
|
||||
{/* 积分 */}
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoText}>+1</Text>
|
||||
</View>
|
||||
|
||||
{/* 目标数量 */}
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoText}>
|
||||
{goal.targetCount || goal.frequency}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 提醒图标(如果有提醒) */}
|
||||
{goal.hasReminder && (
|
||||
<View style={styles.infoItem}>
|
||||
<MaterialIcons name="notifications" size={12} color="#9CA3AF" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 提醒时间(如果有提醒) */}
|
||||
{goal.hasReminder && goal.reminderTime && (
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoText}>{goal.reminderTime}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 重复图标 */}
|
||||
<View style={styles.infoItem}>
|
||||
<MaterialIcons name="loop" size={12} color="#9CA3AF" />
|
||||
</View>
|
||||
|
||||
{/* 重复类型 */}
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoText}>{getRepeatTypeText(goal)}</Text>
|
||||
</View>
|
||||
|
||||
{/* 结束日期 */}
|
||||
{goal.endDate && (
|
||||
<View style={styles.infoItem}>
|
||||
<MaterialIcons
|
||||
name="calendar-month"
|
||||
size={12}
|
||||
color="#9CA3AF"
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
<Text style={styles.infoText}>{goal.endDate}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 右侧状态指示器 */}
|
||||
{showStatus && (
|
||||
<View style={[styles.statusIndicator, { backgroundColor: getStatusColor(goal) }]}>
|
||||
<Text style={styles.statusText}>{getStatusText(goal)}</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Swipeable>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
goalCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 8,
|
||||
elevation: 2,
|
||||
},
|
||||
goalIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 20,
|
||||
backgroundColor: '#F3F4F6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
position: 'relative',
|
||||
},
|
||||
iconStars: {
|
||||
position: 'absolute',
|
||||
top: -2,
|
||||
right: -2,
|
||||
flexDirection: 'row',
|
||||
gap: 1,
|
||||
},
|
||||
star: {
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
backgroundColor: '#FFFFFF',
|
||||
},
|
||||
goalContent: {
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
goalTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#1F2937',
|
||||
marginBottom: 8,
|
||||
},
|
||||
goalInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
infoItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
infoText: {
|
||||
fontSize: 12,
|
||||
color: '#9CA3AF',
|
||||
fontWeight: '500',
|
||||
},
|
||||
statusIndicator: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
minWidth: 60,
|
||||
alignItems: 'center',
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 10,
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '600',
|
||||
},
|
||||
deleteButton: {
|
||||
width: 60,
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
deleteButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import React, { useRef } from 'react';
|
||||
import { Dimensions, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { GoalCard, GoalItem } from './GoalCard';
|
||||
|
||||
interface GoalCarouselProps {
|
||||
goals: GoalItem[];
|
||||
onGoalPress?: (item: GoalItem) => void;
|
||||
}
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
|
||||
export function GoalCarousel({ goals, onGoalPress }: GoalCarouselProps) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorTokens = Colors[theme];
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
|
||||
if (!goals || goals.length === 0) {
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={[styles.emptyText, { color: colorTokens.textMuted }]}>
|
||||
今天暂无目标
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
snapToInterval={(screenWidth - 60) * 0.65 + 16} // 卡片宽度 + 间距
|
||||
snapToAlignment="start"
|
||||
decelerationRate="fast"
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
style={styles.scrollView}
|
||||
>
|
||||
{goals.map((item, index) => (
|
||||
<GoalCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
onPress={onGoalPress}
|
||||
/>
|
||||
))}
|
||||
{/* 占位符,确保最后一张卡片有足够的滑动空间 */}
|
||||
<View style={{ width: 20 }} />
|
||||
</ScrollView>
|
||||
|
||||
{/* 底部指示器 */}
|
||||
{/* <View style={styles.indicatorContainer}>
|
||||
{goals.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.indicator,
|
||||
{ backgroundColor: index === 0 ? colorTokens.primary : colorTokens.border }
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View> */}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
},
|
||||
scrollView: {
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyContainer: {
|
||||
height: 140,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 20,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
},
|
||||
indicatorContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
},
|
||||
indicator: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
},
|
||||
});
|
||||
@@ -1,457 +0,0 @@
|
||||
import { TaskListItem } from '@/types/goals';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
Dimensions,
|
||||
Modal,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
|
||||
interface GoalsPageGuideProps {
|
||||
visible: boolean;
|
||||
onComplete: () => void;
|
||||
tasks?: TaskListItem[]; // 添加任务数据,用于智能引导
|
||||
}
|
||||
|
||||
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
||||
|
||||
// 计算精确的高亮位置
|
||||
const calculateHighlightPosition = (stepIndex: number, hasTasks: boolean) => {
|
||||
const baseTop = 120; // 状态栏 + 标题区域高度
|
||||
const cardHeight = 180; // 任务进度卡片高度
|
||||
const filterHeight = 60; // 筛选标签高度
|
||||
const listHeight = 300; // 任务列表高度
|
||||
|
||||
switch (stepIndex) {
|
||||
case 0: // 欢迎标题
|
||||
return {
|
||||
top: baseTop - 40,
|
||||
left: 20,
|
||||
right: 20,
|
||||
height: 60,
|
||||
borderRadius: 12,
|
||||
};
|
||||
case 1: // 任务进度卡片
|
||||
return {
|
||||
top: baseTop + 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
height: cardHeight,
|
||||
borderRadius: 16,
|
||||
};
|
||||
case 2: // 目标管理按钮(有任务时)
|
||||
if (hasTasks) {
|
||||
return {
|
||||
top: baseTop + 40,
|
||||
right: 60,
|
||||
width: 40,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
top: baseTop + 40,
|
||||
right: 20,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
};
|
||||
}
|
||||
case 3: // 创建新目标按钮(有任务时)
|
||||
if (hasTasks) {
|
||||
return {
|
||||
top: baseTop + 40,
|
||||
right: 20,
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
};
|
||||
} else {
|
||||
return null; // 没有这一步
|
||||
}
|
||||
case 4: // 任务筛选标签
|
||||
return {
|
||||
top: baseTop + cardHeight + 40,
|
||||
left: 20,
|
||||
right: 20,
|
||||
height: filterHeight,
|
||||
borderRadius: 24,
|
||||
};
|
||||
case 5: // 任务列表
|
||||
return {
|
||||
top: baseTop + cardHeight + filterHeight + 60,
|
||||
left: 20,
|
||||
right: 20,
|
||||
height: listHeight,
|
||||
borderRadius: 24,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const GoalsPageGuide: React.FC<GoalsPageGuideProps> = ({
|
||||
visible,
|
||||
onComplete,
|
||||
tasks = [],
|
||||
}) => {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const scaleAnim = useRef(new Animated.Value(0.8)).current;
|
||||
|
||||
// 根据任务数据智能生成引导步骤
|
||||
const generateSteps = () => {
|
||||
const hasTasks = tasks.length > 0;
|
||||
const hasCompletedTasks = tasks.some(task => task.status === 'completed');
|
||||
const hasPendingTasks = tasks.some(task => task.status === 'pending');
|
||||
|
||||
const baseSteps = [
|
||||
{
|
||||
title: '欢迎来到目标页面',
|
||||
description: '这里是您的目标管理中心,让我们一起来了解各个功能。',
|
||||
icon: 'flag',
|
||||
},
|
||||
{
|
||||
title: '任务进度统计',
|
||||
description: '这里显示您当天的任务完成情况,包括待完成、已完成和已跳过的任务数量。',
|
||||
icon: 'analytics',
|
||||
},
|
||||
];
|
||||
|
||||
// 根据任务状态添加不同的引导内容
|
||||
if (!hasTasks) {
|
||||
baseSteps.push({
|
||||
title: '创建您的第一个目标',
|
||||
description: '点击加号按钮,创建您的第一个目标,系统会自动生成相应的任务。',
|
||||
icon: 'add',
|
||||
});
|
||||
} else {
|
||||
baseSteps.push(
|
||||
{
|
||||
title: '目标管理',
|
||||
description: '点击右上角的目标按钮,可以查看和管理您的所有目标。',
|
||||
icon: 'flag',
|
||||
},
|
||||
{
|
||||
title: '创建新目标',
|
||||
description: '点击加号按钮,可以快速创建新的目标。',
|
||||
icon: 'add',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
baseSteps.push({
|
||||
title: '任务筛选',
|
||||
description: '使用这些标签可以筛选查看不同状态的任务。',
|
||||
icon: 'filter-list',
|
||||
});
|
||||
|
||||
// 根据任务状态调整任务列表的引导内容
|
||||
if (!hasTasks) {
|
||||
baseSteps.push({
|
||||
title: '任务列表',
|
||||
description: '创建目标后,您的任务将显示在这里。',
|
||||
icon: 'list',
|
||||
});
|
||||
} else if (!hasPendingTasks && hasCompletedTasks) {
|
||||
baseSteps.push({
|
||||
title: '任务列表',
|
||||
description: '您已完成所有任务!可以创建新目标或查看历史记录。',
|
||||
icon: 'check-circle',
|
||||
});
|
||||
} else {
|
||||
baseSteps.push({
|
||||
title: '任务列表',
|
||||
description: '这里显示您的所有任务,可以标记完成或跳过。',
|
||||
icon: 'list',
|
||||
});
|
||||
}
|
||||
|
||||
return baseSteps;
|
||||
};
|
||||
|
||||
const steps = generateSteps();
|
||||
const hasTasks = tasks.length > 0;
|
||||
const currentHighlightPosition = calculateHighlightPosition(currentStep, hasTasks);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 0.8,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
}, [visible, fadeAnim, scaleAnim]);
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStep < steps.length - 1) {
|
||||
setCurrentStep(currentStep + 1);
|
||||
} else {
|
||||
handleComplete();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(currentStep - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
Animated.parallel([
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 0,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scaleAnim, {
|
||||
toValue: 0.8,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
setCurrentStep(0);
|
||||
onComplete();
|
||||
});
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
handleComplete();
|
||||
};
|
||||
|
||||
if (!visible || !currentHighlightPosition) return null;
|
||||
|
||||
const currentStepData = steps[currentStep];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="none"
|
||||
statusBarTranslucent
|
||||
>
|
||||
<StatusBar backgroundColor="rgba(0, 0, 0, 0.8)" barStyle="light-content" />
|
||||
|
||||
{/* 背景遮罩 */}
|
||||
<View style={styles.overlay}>
|
||||
{/* 高亮区域 */}
|
||||
<View style={[styles.highlightArea, currentHighlightPosition]}>
|
||||
<View style={[styles.highlightBorder, { borderRadius: currentHighlightPosition.borderRadius }]} />
|
||||
</View>
|
||||
|
||||
{/* 引导内容 */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.guideContainer,
|
||||
{
|
||||
opacity: fadeAnim,
|
||||
transform: [{ scale: scaleAnim }],
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* 步骤指示器 */}
|
||||
<View style={styles.stepIndicator}>
|
||||
{steps.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.stepDot,
|
||||
index === currentStep && styles.stepDotActive,
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 图标 */}
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialIcons
|
||||
name={currentStepData.icon as any}
|
||||
size={48}
|
||||
color="#7A5AF8"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* 标题 */}
|
||||
<Text style={styles.title}>{currentStepData.title}</Text>
|
||||
|
||||
{/* 描述 */}
|
||||
<Text style={styles.description}>{currentStepData.description}</Text>
|
||||
|
||||
{/* 按钮区域 */}
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity style={styles.skipButton} onPress={handleSkip}>
|
||||
<Text style={styles.skipButtonText}>跳过</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.navigationButtons}>
|
||||
{currentStep > 0 && (
|
||||
<TouchableOpacity style={styles.previousButton} onPress={handlePrevious}>
|
||||
<Text style={styles.previousButtonText}>回顾</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity style={styles.nextButton} onPress={handleNext}>
|
||||
<Text style={styles.nextButtonText}>
|
||||
{currentStep === steps.length - 1 ? '完成' : '下一步'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
highlightArea: {
|
||||
position: 'absolute',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
highlightBorder: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderWidth: 2,
|
||||
borderColor: '#7A5AF8',
|
||||
backgroundColor: 'rgba(122, 90, 248, 0.08)',
|
||||
shadowColor: '#7A5AF8',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
guideContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 120,
|
||||
left: 20,
|
||||
right: 20,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 20,
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 12,
|
||||
elevation: 8,
|
||||
},
|
||||
stepIndicator: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 20,
|
||||
},
|
||||
stepDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#E5E7EB',
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
stepDotActive: {
|
||||
backgroundColor: '#7A5AF8',
|
||||
},
|
||||
iconContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
backgroundColor: '#F3F4F6',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
textAlign: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
description: {
|
||||
fontSize: 16,
|
||||
color: '#6B7280',
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
marginBottom: 24,
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
},
|
||||
navigationButtons: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
skipButton: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
skipButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#6B7280',
|
||||
},
|
||||
previousButton: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#7A5AF8',
|
||||
backgroundColor: 'rgba(122, 90, 248, 0.1)',
|
||||
},
|
||||
previousButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#7A5AF8',
|
||||
},
|
||||
nextButton: {
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#7A5AF8',
|
||||
},
|
||||
nextButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
@@ -1,16 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Alert,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { useNotifications } from '../hooks/useNotifications';
|
||||
import { ThemedText } from './ThemedText';
|
||||
import { ThemedView } from './ThemedView';
|
||||
import { DailySummaryTest } from './DailySummaryTest';
|
||||
|
||||
export const NotificationTest: React.FC = () => {
|
||||
const {
|
||||
@@ -23,9 +21,7 @@ export const NotificationTest: React.FC = () => {
|
||||
cancelAllNotifications,
|
||||
getAllScheduledNotifications,
|
||||
sendWorkoutReminder,
|
||||
sendGoalAchievement,
|
||||
sendMoodCheckinReminder,
|
||||
debugNotificationStatus,
|
||||
} = useNotifications();
|
||||
|
||||
const [scheduledNotifications, setScheduledNotifications] = useState<any[]>([]);
|
||||
@@ -97,15 +93,6 @@ export const NotificationTest: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendGoalAchievement = async () => {
|
||||
try {
|
||||
await sendGoalAchievement('目标达成', '恭喜您完成了本周的运动目标!');
|
||||
Alert.alert('成功', '目标达成通知已发送');
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '发送目标达成通知失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendMoodCheckinReminder = async () => {
|
||||
try {
|
||||
await sendMoodCheckinReminder('心情打卡', '记得记录今天的心情状态哦');
|
||||
@@ -134,20 +121,11 @@ export const NotificationTest: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDebugNotificationStatus = async () => {
|
||||
try {
|
||||
await debugNotificationStatus();
|
||||
Alert.alert('调试完成', '请查看控制台输出');
|
||||
} catch (error) {
|
||||
Alert.alert('错误', '调试失败');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<ThemedText style={styles.title}>推送通知测试</ThemedText>
|
||||
|
||||
|
||||
<View style={styles.statusContainer}>
|
||||
<ThemedText style={styles.statusText}>
|
||||
初始化状态: {isInitialized ? '已初始化' : '未初始化'}
|
||||
@@ -193,13 +171,6 @@ export const NotificationTest: React.FC = () => {
|
||||
<ThemedText style={styles.buttonText}>发送运动提醒</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={handleSendGoalAchievement}
|
||||
>
|
||||
<ThemedText style={styles.buttonText}>发送目标达成通知</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={handleSendMoodCheckinReminder}
|
||||
@@ -214,13 +185,6 @@ export const NotificationTest: React.FC = () => {
|
||||
<ThemedText style={styles.buttonText}>获取已安排通知</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.debugButton]}
|
||||
onPress={handleDebugNotificationStatus}
|
||||
>
|
||||
<ThemedText style={styles.buttonText}>调试通知状态</ThemedText>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, styles.dangerButton]}
|
||||
onPress={handleCancelAllNotifications}
|
||||
@@ -247,9 +211,6 @@ export const NotificationTest: React.FC = () => {
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 每日总结推送测试 */}
|
||||
<DailySummaryTest userName="测试用户" />
|
||||
</ScrollView>
|
||||
</ThemedView>
|
||||
);
|
||||
|
||||
@@ -1,501 +0,0 @@
|
||||
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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
export type TaskFilterType = 'all' | 'pending' | 'completed' | 'skipped';
|
||||
|
||||
interface TaskFilterTabsProps {
|
||||
selectedFilter: TaskFilterType;
|
||||
onFilterChange: (filter: TaskFilterType) => void;
|
||||
taskCounts: {
|
||||
all: number;
|
||||
pending: number;
|
||||
completed: number;
|
||||
skipped: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const TaskFilterTabs: React.FC<TaskFilterTabsProps> = ({
|
||||
selectedFilter,
|
||||
onFilterChange,
|
||||
taskCounts,
|
||||
}) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.tabContainer}>
|
||||
{/* 全部 Tab */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
selectedFilter === 'all' && styles.activeTab
|
||||
]}
|
||||
onPress={() => onFilterChange('all')}
|
||||
>
|
||||
<Text style={[
|
||||
styles.tabText,
|
||||
selectedFilter === 'all' && styles.activeTabText
|
||||
]}>
|
||||
全部
|
||||
</Text>
|
||||
<View style={[
|
||||
styles.badge,
|
||||
selectedFilter === 'all' ? styles.activeBadge : styles.inactiveBadge
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.badgeText,
|
||||
selectedFilter === 'all' && styles.activeBadgeText
|
||||
]}>
|
||||
{taskCounts.all}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 待完成 Tab */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
selectedFilter === 'pending' && styles.activeTab
|
||||
]}
|
||||
onPress={() => onFilterChange('pending')}
|
||||
>
|
||||
<Text style={[
|
||||
styles.tabText,
|
||||
selectedFilter === 'pending' && styles.activeTabText
|
||||
]}>
|
||||
待完成
|
||||
</Text>
|
||||
<View style={[
|
||||
styles.badge,
|
||||
selectedFilter === 'pending' ? styles.activeBadge : styles.inactiveBadge
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.badgeText,
|
||||
selectedFilter === 'pending' && styles.activeBadgeText
|
||||
]}>
|
||||
{taskCounts.pending}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 已完成 Tab */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
selectedFilter === 'completed' && styles.activeTab
|
||||
]}
|
||||
onPress={() => onFilterChange('completed')}
|
||||
>
|
||||
<Text style={[
|
||||
styles.tabText,
|
||||
selectedFilter === 'completed' && styles.activeTabText
|
||||
]}>
|
||||
已完成
|
||||
</Text>
|
||||
<View style={[
|
||||
styles.badge,
|
||||
selectedFilter === 'completed' ? styles.activeBadge : styles.inactiveBadge
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.badgeText,
|
||||
selectedFilter === 'completed' && styles.activeBadgeText
|
||||
]}>
|
||||
{taskCounts.completed}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 已跳过 Tab */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.tab,
|
||||
selectedFilter === 'skipped' && styles.activeTab
|
||||
]}
|
||||
onPress={() => onFilterChange('skipped')}
|
||||
>
|
||||
<Text style={[
|
||||
styles.tabText,
|
||||
selectedFilter === 'skipped' && styles.activeTabText
|
||||
]}>
|
||||
已跳过
|
||||
</Text>
|
||||
<View style={[
|
||||
styles.badge,
|
||||
selectedFilter === 'skipped' ? styles.activeBadge : styles.inactiveBadge
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.badgeText,
|
||||
selectedFilter === 'skipped' && styles.activeBadgeText
|
||||
]}>
|
||||
{taskCounts.skipped}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
tabContainer: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 24,
|
||||
padding: 4,
|
||||
flexDirection: 'row',
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 2,
|
||||
elevation: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 20,
|
||||
gap: 6,
|
||||
},
|
||||
activeTab: {
|
||||
backgroundColor: '#7A5AF8',
|
||||
shadowColor: '#7A5AF8',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#374151',
|
||||
},
|
||||
activeTabText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '600',
|
||||
},
|
||||
badge: {
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 6,
|
||||
},
|
||||
inactiveBadge: {
|
||||
backgroundColor: '#E5E7EB',
|
||||
},
|
||||
activeBadge: {
|
||||
backgroundColor: '#EF4444',
|
||||
},
|
||||
badgeText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
},
|
||||
activeBadgeText: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
@@ -1,174 +0,0 @@
|
||||
import { TaskListItem } from '@/types/goals';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import { Image } from 'expo-image';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
interface TaskProgressCardProps {
|
||||
tasks: TaskListItem[];
|
||||
headerButtons?: ReactNode;
|
||||
}
|
||||
|
||||
export const TaskProgressCard: React.FC<TaskProgressCardProps> = ({
|
||||
tasks,
|
||||
headerButtons,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
// 计算各状态的任务数量
|
||||
const pendingTasks = tasks.filter(task => task.status === 'pending');
|
||||
const completedTasks = tasks.filter(task => task.status === 'completed');
|
||||
const skippedTasks = tasks.filter(task => task.status === 'skipped');
|
||||
|
||||
// 处理跳转到目标列表
|
||||
const handleNavigateToGoals = () => {
|
||||
router.push('/goals-list');
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* 标题区域 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>今日</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.goalsIconButton}
|
||||
onPress={handleNavigateToGoals}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-goal-edit.png' }}
|
||||
style={{ width: 18, height: 18 }}
|
||||
cachePolicy="memory-disk"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.headerActions}>
|
||||
|
||||
{headerButtons && (
|
||||
<View style={styles.headerButtons}>
|
||||
{headerButtons}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 状态卡片区域 */}
|
||||
<View style={styles.statusCards}>
|
||||
{/* 待完成 卡片 */}
|
||||
<View style={styles.statusCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<MaterialIcons name="pending" size={16} color="#7A5AF8" />
|
||||
<Text style={styles.cardLabel} numberOfLines={1}>待完成</Text>
|
||||
</View>
|
||||
<Text style={styles.cardCount}>{pendingTasks.length}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已完成 卡片 */}
|
||||
<View style={styles.statusCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<MaterialIcons name="check-circle" size={16} color="#10B981" />
|
||||
<Text style={styles.cardLabel} numberOfLines={1}>已完成</Text>
|
||||
</View>
|
||||
<Text style={styles.cardCount}>{completedTasks.length}</Text>
|
||||
</View>
|
||||
|
||||
{/* 已跳过 卡片 */}
|
||||
<View style={styles.statusCard}>
|
||||
<View style={styles.cardHeader}>
|
||||
<MaterialIcons name="skip-next" size={16} color="#F59E0B" />
|
||||
<Text style={styles.cardLabel} numberOfLines={1}>已跳过</Text>
|
||||
</View>
|
||||
<Text style={styles.cardCount}>{skippedTasks.length}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginHorizontal: 20,
|
||||
marginBottom: 20,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
header: {
|
||||
marginBottom: 20,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
headerButtons: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
headerActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
goalsIconButton: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
lineHeight: 24,
|
||||
height: 24,
|
||||
color: '#1F2937',
|
||||
marginBottom: 4,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: '#6B7280',
|
||||
fontWeight: '400',
|
||||
},
|
||||
statusCards: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
},
|
||||
statusCard: {
|
||||
flex: 1,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
alignItems: 'flex-start',
|
||||
minHeight: 80,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
gap: 6,
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
cardLabel: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
color: '#1F2937',
|
||||
lineHeight: 14,
|
||||
},
|
||||
cardCount: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
},
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user