feat: 新增目标页面引导功能及相关组件
- 在目标页面中集成用户引导功能,帮助用户了解页面各项功能 - 创建 GoalsPageGuide 组件,支持多步骤引导和动态高亮功能区域 - 实现引导状态的检查、标记和重置功能,确保用户体验 - 添加开发测试按钮,方便开发者重置引导状态 - 更新相关文档,详细描述引导功能的实现和使用方法
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
import CreateGoalModal from '@/components/CreateGoalModal';
|
import CreateGoalModal from '@/components/CreateGoalModal';
|
||||||
|
import { GoalsPageGuide } from '@/components/GoalsPageGuide';
|
||||||
|
import { GuideTestButton } from '@/components/GuideTestButton';
|
||||||
import { TaskCard } from '@/components/TaskCard';
|
import { TaskCard } from '@/components/TaskCard';
|
||||||
import { TaskFilterTabs, TaskFilterType } from '@/components/TaskFilterTabs';
|
import { TaskFilterTabs, TaskFilterType } from '@/components/TaskFilterTabs';
|
||||||
import { TaskProgressCard } from '@/components/TaskProgressCard';
|
import { TaskProgressCard } from '@/components/TaskProgressCard';
|
||||||
@@ -10,6 +12,7 @@ import { useColorScheme } from '@/hooks/useColorScheme';
|
|||||||
import { clearErrors, createGoal } from '@/store/goalsSlice';
|
import { clearErrors, createGoal } from '@/store/goalsSlice';
|
||||||
import { clearErrors as clearTaskErrors, fetchTasks, loadMoreTasks } from '@/store/tasksSlice';
|
import { clearErrors as clearTaskErrors, fetchTasks, loadMoreTasks } from '@/store/tasksSlice';
|
||||||
import { CreateGoalRequest, TaskListItem } from '@/types/goals';
|
import { CreateGoalRequest, TaskListItem } from '@/types/goals';
|
||||||
|
import { checkGuideCompleted, markGuideCompleted } from '@/utils/guideHelpers';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -46,15 +49,32 @@ export default function GoalsScreen() {
|
|||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [selectedFilter, setSelectedFilter] = useState<TaskFilterType>('all');
|
const [selectedFilter, setSelectedFilter] = useState<TaskFilterType>('all');
|
||||||
const [modalKey, setModalKey] = useState(0); // 用于强制重新渲染弹窗
|
const [modalKey, setModalKey] = useState(0); // 用于强制重新渲染弹窗
|
||||||
|
const [showGuide, setShowGuide] = useState(false); // 控制引导显示
|
||||||
|
|
||||||
// 页面聚焦时重新加载数据
|
// 页面聚焦时重新加载数据
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
console.log('useFocusEffect - loading tasks');
|
console.log('useFocusEffect - loading tasks');
|
||||||
loadTasks();
|
loadTasks();
|
||||||
|
checkAndShowGuide();
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 检查并显示用户引导
|
||||||
|
const checkAndShowGuide = async () => {
|
||||||
|
try {
|
||||||
|
const hasCompletedGuide = await checkGuideCompleted('GOALS_PAGE');
|
||||||
|
if (!hasCompletedGuide) {
|
||||||
|
// 延迟显示引导,确保页面完全加载
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowGuide(true);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查引导状态失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 加载任务列表
|
// 加载任务列表
|
||||||
const loadTasks = async () => {
|
const loadTasks = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -182,6 +202,17 @@ export default function GoalsScreen() {
|
|||||||
setSelectedFilter(filter);
|
setSelectedFilter(filter);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理引导完成
|
||||||
|
const handleGuideComplete = async () => {
|
||||||
|
try {
|
||||||
|
await markGuideCompleted('GOALS_PAGE');
|
||||||
|
setShowGuide(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存引导状态失败:', error);
|
||||||
|
setShowGuide(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 渲染任务项
|
// 渲染任务项
|
||||||
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
|
const renderTaskItem = ({ item }: { item: TaskListItem }) => (
|
||||||
<TaskCard
|
<TaskCard
|
||||||
@@ -272,7 +303,7 @@ export default function GoalsScreen() {
|
|||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View>
|
<View>
|
||||||
<Text style={[styles.pageTitle, { color: '#FFFFFF' }]}>
|
<Text style={[styles.pageTitle, { color: '#FFFFFF' }]}>
|
||||||
等待完成
|
今日目标
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[styles.pageTitle2, { color: '#FFFFFF' }]}>
|
<Text style={[styles.pageTitle2, { color: '#FFFFFF' }]}>
|
||||||
让我们检查你的目标!
|
让我们检查你的目标!
|
||||||
@@ -290,6 +321,9 @@ export default function GoalsScreen() {
|
|||||||
style={[styles.cardGoalsButton, { borderColor: colorTokens.primary }]}
|
style={[styles.cardGoalsButton, { borderColor: colorTokens.primary }]}
|
||||||
onPress={handleNavigateToGoals}
|
onPress={handleNavigateToGoals}
|
||||||
>
|
>
|
||||||
|
<Text style={[styles.cardGoalsButtonText, { color: colorTokens.primary }]}>
|
||||||
|
回顾
|
||||||
|
</Text>
|
||||||
<MaterialIcons name="flag" size={16} color={colorTokens.primary} />
|
<MaterialIcons name="flag" size={16} color={colorTokens.primary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -345,6 +379,16 @@ export default function GoalsScreen() {
|
|||||||
onSuccess={handleModalSuccess}
|
onSuccess={handleModalSuccess}
|
||||||
loading={createLoading}
|
loading={createLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 目标页面引导 */}
|
||||||
|
<GoalsPageGuide
|
||||||
|
visible={showGuide}
|
||||||
|
onComplete={handleGuideComplete}
|
||||||
|
tasks={tasks}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 开发测试按钮 */}
|
||||||
|
<GuideTestButton visible={__DEV__} />
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
@@ -505,6 +549,11 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#F3F4F6',
|
backgroundColor: '#F3F4F6',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
|
cardGoalsButtonText: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#374151',
|
||||||
|
},
|
||||||
cardAddButton: {
|
cardAddButton: {
|
||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
|
|||||||
457
components/GoalsPageGuide.tsx
Normal file
457
components/GoalsPageGuide.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
53
components/GuideTestButton.tsx
Normal file
53
components/GuideTestButton.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@ import { TaskListItem } from '@/types/goals';
|
|||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import React from 'react';
|
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 {
|
interface TaskCardProps {
|
||||||
task: TaskListItem;
|
task: TaskListItem;
|
||||||
@@ -22,6 +22,20 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
const { showConfirm } = useGlobalDialog();
|
const { showConfirm } = useGlobalDialog();
|
||||||
const router = useRouter();
|
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) => {
|
const getStatusText = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -191,18 +205,15 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
|
|
||||||
{/* 进度条 */}
|
{/* 进度条 */}
|
||||||
<View style={styles.progressBar}>
|
<View style={styles.progressBar}>
|
||||||
<View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.progressFill,
|
styles.progressFill,
|
||||||
{
|
{
|
||||||
width: task.progressPercentage > 0 ? `${Math.min(task.progressPercentage, 100)}%` : '2%',
|
width: progressAnimation.interpolate({
|
||||||
backgroundColor: task.progressPercentage >= 100
|
inputRange: [0, 100],
|
||||||
? '#10B981'
|
outputRange: ['0%', '100%'],
|
||||||
: task.progressPercentage >= 50
|
}),
|
||||||
? '#F59E0B'
|
backgroundColor: task.progressPercentage > 0 ? colorTokens.primary : '#E5E7EB',
|
||||||
: task.progressPercentage > 0
|
|
||||||
? colorTokens.primary
|
|
||||||
: '#E5E7EB',
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
158
docs/user-guide-implementation.md
Normal file
158
docs/user-guide-implementation.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# 用户引导功能实现文档
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
用户引导功能用于在用户首次进入特定页面时,提供功能说明和操作指导,帮助用户更好地理解和使用应用功能。
|
||||||
|
|
||||||
|
## 已实现的引导
|
||||||
|
|
||||||
|
### 1. Goals页面 - 全面功能引导
|
||||||
|
|
||||||
|
**位置**: `app/(tabs)/goals.tsx`
|
||||||
|
|
||||||
|
**引导内容**:
|
||||||
|
- 欢迎介绍:介绍目标页面的整体功能
|
||||||
|
- 任务进度统计:说明任务进度卡片显示当天任务的完成情况
|
||||||
|
- 目标管理:说明如何查看和管理目标
|
||||||
|
- 创建新目标:说明如何快速创建新目标
|
||||||
|
- 任务筛选:说明如何使用标签筛选任务
|
||||||
|
- 任务列表:说明任务列表的功能和操作
|
||||||
|
|
||||||
|
**组件**: `GoalsPageGuide`
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
1. **GoalsPageGuide** (`components/GoalsPageGuide.tsx`)
|
||||||
|
- 引导弹窗组件
|
||||||
|
- 支持多步骤引导
|
||||||
|
- 动态高亮不同功能区域
|
||||||
|
- 平滑动画效果
|
||||||
|
- 回顾功能支持
|
||||||
|
|
||||||
|
2. **引导状态管理** (`utils/guideHelpers.ts`)
|
||||||
|
- 使用AsyncStorage存储引导完成状态
|
||||||
|
- 提供检查、标记、重置功能
|
||||||
|
|
||||||
|
### 引导状态键
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const GUIDE_KEYS = {
|
||||||
|
GOALS_PAGE: '@guide_goals_page_completed',
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 在页面中集成引导
|
||||||
|
|
||||||
|
1. **导入必要组件和工具**:
|
||||||
|
```typescript
|
||||||
|
import { GoalsPageGuide } from '@/components/GoalsPageGuide';
|
||||||
|
import { checkGuideCompleted, markGuideCompleted } from '@/utils/guideHelpers';
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **添加状态管理**:
|
||||||
|
```typescript
|
||||||
|
const [showGuide, setShowGuide] = useState(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **检查引导状态**:
|
||||||
|
```typescript
|
||||||
|
const checkAndShowGuide = async () => {
|
||||||
|
try {
|
||||||
|
const hasCompletedGuide = await checkGuideCompleted('GOALS_PAGE');
|
||||||
|
if (!hasCompletedGuide) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setShowGuide(true);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查引导状态失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **处理引导完成**:
|
||||||
|
```typescript
|
||||||
|
const handleGuideComplete = async () => {
|
||||||
|
try {
|
||||||
|
await markGuideCompleted('GOALS_PAGE');
|
||||||
|
setShowGuide(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存引导状态失败:', error);
|
||||||
|
setShowGuide(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **在页面中渲染组件**:
|
||||||
|
```typescript
|
||||||
|
<GoalsPageGuide
|
||||||
|
visible={showGuide}
|
||||||
|
onComplete={handleGuideComplete}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发测试
|
||||||
|
|
||||||
|
### 测试按钮
|
||||||
|
|
||||||
|
在开发模式下,Goals页面右上角会显示"重置引导"按钮,用于测试引导功能。
|
||||||
|
|
||||||
|
### 手动重置引导状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { resetGuideStates } from '@/utils/devTools';
|
||||||
|
|
||||||
|
// 重置所有引导状态
|
||||||
|
await resetGuideStates();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查引导状态
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getAllGuideStatus } from '@/utils/guideHelpers';
|
||||||
|
|
||||||
|
// 获取所有引导状态
|
||||||
|
const status = await getAllGuideStatus();
|
||||||
|
console.log('引导状态:', status);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 添加新的引导
|
||||||
|
|
||||||
|
### 1. 创建引导组件
|
||||||
|
|
||||||
|
参考 `TaskProgressCardGuide` 的结构创建新的引导组件。
|
||||||
|
|
||||||
|
### 2. 添加状态键
|
||||||
|
|
||||||
|
在 `utils/guideHelpers.ts` 中添加新的引导键:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const GUIDE_KEYS = {
|
||||||
|
GOALS_PAGE: '@guide_goals_page_completed',
|
||||||
|
NEW_GUIDE: '@guide_new_guide_completed', // 新增
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 在页面中集成
|
||||||
|
|
||||||
|
按照上述使用方法在目标页面中集成引导功能。
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
1. **用户友好**: 引导内容简洁明了,避免信息过载
|
||||||
|
2. **可跳过**: 用户可以选择跳过引导
|
||||||
|
3. **可回顾**: 用户可以回顾之前的步骤
|
||||||
|
4. **一次性**: 引导只显示一次,避免重复打扰
|
||||||
|
5. **响应式**: 引导内容适应不同屏幕尺寸
|
||||||
|
6. **可测试**: 提供开发测试工具
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 引导应该在页面完全加载后显示,避免布局问题
|
||||||
|
2. 引导状态应该持久化存储,避免重复显示
|
||||||
|
3. 引导内容应该与页面功能紧密相关
|
||||||
|
4. 测试时注意重置引导状态,确保功能正常
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { STORAGE_KEYS } from '@/services/api';
|
import { STORAGE_KEYS } from '@/services/api';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { resetAllGuides } from './guideHelpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 开发工具函数 - 清除隐私同意状态
|
* 开发工具函数 - 清除隐私同意状态
|
||||||
@@ -45,3 +46,15 @@ export const clearAllUserData = async (): Promise<void> => {
|
|||||||
console.error('清除用户数据失败:', error);
|
console.error('清除用户数据失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置所有用户引导状态(仅用于开发测试)
|
||||||
|
*/
|
||||||
|
export const resetGuideStates = async () => {
|
||||||
|
try {
|
||||||
|
await resetAllGuides();
|
||||||
|
console.log('✅ 所有用户引导状态已重置');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 重置用户引导状态失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
67
utils/guideHelpers.ts
Normal file
67
utils/guideHelpers.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
// 引导状态存储键
|
||||||
|
const GUIDE_KEYS = {
|
||||||
|
GOALS_PAGE: '@guide_goals_page_completed',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否已经完成特定引导
|
||||||
|
* @param guideKey 引导键名
|
||||||
|
* @returns Promise<boolean> 是否已完成
|
||||||
|
*/
|
||||||
|
export const checkGuideCompleted = async (guideKey: keyof typeof GUIDE_KEYS): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const completed = await AsyncStorage.getItem(GUIDE_KEYS[guideKey]);
|
||||||
|
return completed === 'true';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查引导状态失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记引导为已完成
|
||||||
|
* @param guideKey 引导键名
|
||||||
|
*/
|
||||||
|
export const markGuideCompleted = async (guideKey: keyof typeof GUIDE_KEYS): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(GUIDE_KEYS[guideKey], 'true');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存引导状态失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置所有引导状态(用于测试或重置用户引导)
|
||||||
|
*/
|
||||||
|
export const resetAllGuides = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const keys = Object.values(GUIDE_KEYS);
|
||||||
|
await AsyncStorage.multiRemove(keys);
|
||||||
|
console.log('所有引导状态已重置');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置引导状态失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有引导状态
|
||||||
|
* @returns Promise<Record<string, boolean>> 所有引导的完成状态
|
||||||
|
*/
|
||||||
|
export const getAllGuideStatus = async (): Promise<Record<string, boolean>> => {
|
||||||
|
try {
|
||||||
|
const result: Record<string, boolean> = {};
|
||||||
|
const keys = Object.values(GUIDE_KEYS);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const completed = await AsyncStorage.getItem(key);
|
||||||
|
result[key] = completed === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取引导状态失败:', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user