- 在目标页面中集成用户引导功能,帮助用户了解页面各项功能 - 创建 GoalsPageGuide 组件,支持多步骤引导和动态高亮功能区域 - 实现引导状态的检查、标记和重置功能,确保用户体验 - 添加开发测试按钮,方便开发者重置引导状态 - 更新相关文档,详细描述引导功能的实现和使用方法
458 lines
11 KiB
TypeScript
458 lines
11 KiB
TypeScript
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',
|
|
},
|
|
});
|