feat: 新增目标页面引导功能及相关组件

- 在目标页面中集成用户引导功能,帮助用户了解页面各项功能
- 创建 GoalsPageGuide 组件,支持多步骤引导和动态高亮功能区域
- 实现引导状态的检查、标记和重置功能,确保用户体验
- 添加开发测试按钮,方便开发者重置引导状态
- 更新相关文档,详细描述引导功能的实现和使用方法
This commit is contained in:
2025-08-23 13:53:18 +08:00
parent b807e498ed
commit 8a7599f630
7 changed files with 819 additions and 11 deletions

View File

@@ -0,0 +1,457 @@
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',
},
});

View File

@@ -0,0 +1,53 @@
import { resetGuideStates } from '@/utils/devTools';
import React from 'react';
import { Alert, StyleSheet, Text, TouchableOpacity } from 'react-native';
interface GuideTestButtonProps {
visible?: boolean;
}
export const GuideTestButton: React.FC<GuideTestButtonProps> = ({ visible = false }) => {
if (!visible) return null;
const handleResetGuides = async () => {
Alert.alert(
'重置用户引导',
'确定要重置所有用户引导状态吗?这将清除用户已完成的引导记录。',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
style: 'destructive',
onPress: async () => {
await resetGuideStates();
Alert.alert('成功', '用户引导状态已重置,下次进入页面时将重新显示引导。');
},
},
]
);
};
return (
<TouchableOpacity style={styles.button} onPress={handleResetGuides}>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
position: 'absolute',
top: 50,
right: 20,
backgroundColor: '#FF6B6B',
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: 8,
zIndex: 1000,
},
buttonText: {
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
},
});

View File

@@ -7,7 +7,7 @@ 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';
import { Alert, Animated, Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
interface TaskCardProps {
task: TaskListItem;
@@ -21,6 +21,20 @@ export const TaskCard: React.FC<TaskCardProps> = ({
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) : 2;
Animated.timing(progressAnimation, {
toValue: targetProgress,
duration: 800, // 动画持续时间800毫秒
useNativeDriver: false, // 因为我们要动画width属性所以不能使用原生驱动
}).start();
}, [task.progressPercentage, progressAnimation]);
const getStatusText = (status: string) => {
@@ -191,18 +205,15 @@ export const TaskCard: React.FC<TaskCardProps> = ({
{/* 进度条 */}
<View style={styles.progressBar}>
<View
<Animated.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',
width: progressAnimation.interpolate({
inputRange: [0, 100],
outputRange: ['0%', '100%'],
}),
backgroundColor: task.progressPercentage > 0 ? colorTokens.primary : '#E5E7EB',
},
]}
/>